BetterFetLife

See website

// ==UserScript==
// @name        BetterFetLife
// @namespace   com.fetlife.better
// @include     https://fetlife.com/*
// @version     1.4
// @grant       none
// @description See website
// ==/UserScript==

$(document).ready(function(){
			
	// add custom css
	$('head').append('<style type="text/css">' + bfl_css + '</style>');
	
	// === USER POPUP === //
	
	// create user popup
	$('body').append('							' +
		'	<div id="bfl-user">					' +
		'		<a class="avatar-wrap">			' +
		'			<div class="avatar"></div>	' +
		'		</a>							' +
		'		<a class="name"></a>			' +
		'		<span class="status"></span>	' +
		'		<span class="location"></span>	' +
		'		<span class="photos"></span>	' +
		'	</div>								'
	);
	
	// show a user popup on hover
	$(document).hoverIntent({
		selector: 'a[href^="/users/"], a[href^="https://fetlife.com/users/"]',
		over: function(){
				
			var linkEl = this;
			var href = $(linkEl).attr('href');
				href = href.replace('https://fetlife.com', '');
			
			// prevent non-user links
			if( href.split('/').length != 3 ) return;
			
			// prevent self-links
			if( $(linkEl).closest('#bfl-user').length > 0 ) return;		
			
			// prevent closing
			clearTimeout(hideUserPopupTimeout);	
			
			// reset the popup
			hideUserPopup();
					
			// show the popup
			$('#bfl-user')
				.addClass('loading')
				.css({
					top: $(linkEl).offset().top + $(linkEl).height() + 8,
					left: $(linkEl).offset().left
				})
				.show();
									
			$.ajax({
				url: href,
	            dataType: "html",
				
				// cache is OK
				cache: true,
				
				// prevent 503, fetlife don't liking ajax calls
			    beforeSend: function(xhr) {
			        xhr.setRequestHeader(
			            'X-Requested-With',
			            {
			                toString: function() { return ''; }
			            }
			        );
			    },
				success: function(userDOM){
					
					var userAvatarEl = $(userDOM).find('#main_content a img');
					
					// avatar href
					$('#bfl-user')
						.attr('avatar-href', $(userAvatarEl).attr('src'));
										
					// avatar
					$('#bfl-user .avatar-wrap')
						.attr('href', href)
						
					$('#bfl-user .avatar')
						.css('background-image', 'url(' + $(userAvatarEl).attr('src') + ')')
	
					// name
					$('#bfl-user .name')
						.attr('href', href)
						.html( $(userAvatarEl).attr('alt') );
					
					// status (age+gender+orientation)
					$('#bfl-user .status')
						.html( $(userDOM).find('#profile h2 .small').html() );
					
					// location
					$('#bfl-user .location')
						.html( $(userDOM).find('#profile h2.bottom + p').html() );
					
					window.userDOM = userDOM;
					
					// photos
					var photos = $(userDOM).find('#profile .container a[href^="/users/"][href*="/pictures"]');
					photos = photos.filter(function(){
						return $(this).find('img').length > 0;
					})
					photos = photos.slice(0,5);
					$('#bfl-user .photos')
						.html('')
						.append( photos );
						
					// friends status
					// remove the link first
					$(userDOM).find('.friends_badge').find('a').remove()
					$('#bfl-user .friends_status')
						.html( $(userDOM).find('.friends_badge').text() );	
						
					$('#bfl-user')
						.removeClass('loading')	
				}
			});
			
		},
		
		out: function(e){
			
			var linkEl = this;
			var href = $(linkEl).attr('href');
			
			// prevent non-user links
			if( href.split('/').length != 3 ) return;
			
			clearTimeout(hideUserPopupTimeout);
			hideUserPopupTimeout = setTimeout(hideUserPopup, hideUserPopupDelay);
		}
	});
	$(document).on('mouseover', '#bfl-user', function(e){
		clearTimeout(hideUserPopupTimeout);
	});
	$(document).on('mouseleave', '#bfl-user', function(e){
		clearTimeout(hideUserPopupTimeout);
		hideUserPopupTimeout = setTimeout(hideUserPopup, hideUserPopupDelay);
	});

	var hideUserPopupTimeout = setTimeout('', 0);
	var hideUserPopupDelay = 500;
	
	function hideUserPopup() {
		
		$('#bfl-user .avatar').attr('style', '');
		$('#bfl-user .name').html('').attr('href', '');
		$('#bfl-user .status').html('');
		$('#bfl-user .location').html('');
		$('#bfl-user .friends_badge').html('');
		$('#bfl-user .photos').html('');
		$('#bfl-user').removeClass('loading');
		
		$('#bfl-user').hide();
	}
	
	// === IMAGE POPUP === //
	
	// create image popup
	$('body').append('										' +
		'	<div id="bfl-image">							' +
		'		<span class="header">						' +
		'			<span class="title"></span>				' +
		'			<span class="like-wrap">				' +
		'				<span class="like-count"></span>	' +
		'				<span class="like picto">k</span>	' +
		'			</span>									' +
		'		</span>										' +
		'		<a class="image-wrap">						' +
		'			<img class="image" />					' +
		'		</a>										' +
		'	</div>											'
	);
	
	
	$(document).hoverIntent({
		selector: 'a[href^="/users/"][href*="/pictures"], a[href^="https://fetlife.com/users/"][href*="/pictures"]',
		over: function(){
							
			var linkEl = this;
			var href = $(this).attr('href');
				href = href.replace('https://fetlife.com', '');
			
			// prevent non-image links
			if( href.split('/').length != 5 ) return;
			
			// prevent self-links
			if( $(linkEl).closest('#bfl-image').length > 0 ) return;
			
			// prevent 'next image'
			if( $(linkEl).children('.fake_img').length > 0 ) return;
			
			// reset the popup
			hideImagePopup();
			
			// show the popup
			var css = {
				top: $(linkEl).offset().top + $(linkEl).height() + 8
			}
			
			if( $(linkEl).offset().left > $(window).width()/2 ) {
				css.right = $(window).width() - $(linkEl).offset().left - $(linkEl).width();
				$('#bfl-image').addClass('alignright');
			} else {
				css.left = $(linkEl).offset().left;
			}
			
			$('#bfl-image')
				.addClass('loading')
				.css(css)
				.show();
			
			$.ajax({
	            url: href,
	            dataType: "html",
	            success: function(html){  
	               
	                var title = $(html).find('.s.i.caption').text();
	                
		            var likeUrl = href.split('/');  
		                likeUrl = likeUrl[ likeUrl.length-1 ];
		                likeUrl = "/pictures/" + likeUrl + "/likes"
					
					// extract the image src                                          
	                var src = $(html).find('style').first().html().match(/\(\'(.*?)\'\)/);
	                    src = src[0];
	                    src = src.replace("('", "");
	                    src = src.replace("')", "");
	                   					
					$('#bfl-image .title')
						.html(title)
						.attr('title', title)
					
					$('#bfl-image .like-wrap')
						.attr('data-href', likeUrl);
					
					$('#bfl-image .image-wrap')
						.attr('href', href);
						
					$('#bfl-image .image')
						.load(function(){
							$('#bfl-image').removeClass('loading')
						})
						.attr('src', src)
	               
					// get amount of likes
	                $.ajax({
	                    url: likeUrl,
	                    dataType: "json",
	                    success: function(data) {
	                        $('#bfl-image .like-wrap').toggle(data.user_can_like);
	                        if( data.is_liked_by_user ) {
	                            $('#bfl-image .like-wrap').addClass('liked');
	                        }
					
							$('#bfl-image .like-count')
								.html(data.total);
	                    }
	                });
	            }
	        });
			
		},
	
		out: function(e){
			
			var linkEl = this;
			var href = $(linkEl).attr('href');
			
			// prevent non-user links
			if( href.split('/').length != 5 ) return;
			
			clearTimeout(hideImagePopupTimeout);
			hideImagePopupTimeout = setTimeout(hideImagePopup, hideImagePopupDelay);
		}
	});
	$(document).on('mouseover', '#bfl-image', function(e){
		clearTimeout(hideImagePopupTimeout);	
		clearTimeout(hideUserPopupTimeout);
	});
	$(document).on('mouseleave', '#bfl-image', function(e){
		clearTimeout(hideImagePopupTimeout);
		hideImagePopupTimeout = setTimeout(hideImagePopup, hideImagePopupDelay);
	});

	var hideImagePopupTimeout = setTimeout('', 0);
	var hideImagePopupDelay = 500;
	
	function hideImagePopup() {
		
		$('#bfl-image .title').html('').attr('href', '');
		$('#bfl-image .image').attr('src', '');
		$('#bfl-image .like-wrap').attr('data-href', '');
		$('#bfl-image .like-count').html('');
		$('#bfl-image').removeClass('loading');
		$('#bfl-image').removeClass('alignright');
		$('#bfl-image').hide();
	}
	
	$(document).on('click', '#bfl-image .like-wrap', function(){
        var this_ = this;
        $.ajax({
            url: $(this_).data('href') + '/toggle',
            type: 'post',
            success: function(){
                
                if( $(this_).hasClass('liked') ) {
	                $('#bfl-image .like-count').html( parseInt( $('#bfl-image .like-count').html()) - 1 )
                } else {
	                $('#bfl-image .like-count').html( parseInt( $('#bfl-image .like-count').html()) + 1 )
                }
                
                $(this_).toggleClass('liked');
            }
        });
       
        return false;
    });
	
});

var bfl_css = '' +
'	#bfl-user {' +
'		position: absolute;' +
'		z-index: 100;' +
'		display: none;' +
'		padding: 4px;' +
'		min-width: 180px;' +
'		height: 80px;' +
'		padding-left: 92px;' +
'		padding-right: 8px;' +
'		background: #323232;' +
'		border: 3px solid #171717;' +
'	}' +
'	#bfl-user.loading {' +
'		padding-left: 84px;' +
'		padding-right: 4px;' +
'		min-width: 0;' +
'	}' +
'	#bfl-user:before,' +
'	#bfl-image:before {' +
'		position: absolute;' +
'		z-index: 101;' +
'		display: block;' +
'		content: "";' +
'		left: 7px;' +
'		top: -8px;' +
'		border: 8px solid transparent;' +
'		border-bottom-color: #171717;' +
'		border-top-width: 0;' +
'	}' +
'	#bfl-user:after,' +
'	#bfl-image:after {' +
'		position: absolute;' +
'		z-index: 102;' +
'		display: block;' +
'		content: "";' +
'		left: 10px;' +
'		top: -5px;' +
'		border: 5px solid transparent;' +
'		border-bottom-color: #323232;' +
'		border-top-width: 0;' +
'	}' +
'		#bfl-user .avatar {' +
'			position: absolute;' +
'			left: 4px;' +
'			width: 80px;' +
'			height: 80px;' +
'			padding: 0px;' +
'			margin-right: 8px;' +
'			background-color: transparent;' +
'			background-size: cover;' +
'			background-position: center center;' +
'			background-repeat: no-repeat;' +
'		}' +
'		#bfl-user.loading .avatar {' +
'			background-size: auto;' +
'			background-image: url(https://flassets.a.ssl.fastly.net/std/spinners/circle_big.gif);' +
'			margin-right: 0;' +
'		}' +
'		#bfl-user .name {' +
'			white-space: nowrap;' +
'		}'	+
'		#bfl-user .status {' +
'			white-space: nowrap;' +
'			color: #aaa;' +
'		}'	+
'		#bfl-user .location {' +
'			display: block;' +
'			font-size: 12px;' +
'			white-space: nowrap;' +
'		}' +
'		#bfl-user .friends_status {' +
'			float: right;' +
'			font-size: 12px;' +
'		}' +
'		#bfl-user .photos {' +
'			position: absolute;' +
'			right: 4px;' +
'			bottom: 4px;' +
'			font-size: 12px;' +
'		}' +
'			#bfl-user .photos a {' +
'				float: left;' +
'				font-size: 12px;' +
'			}' +
'				#bfl-user .photos a img {' +
'					float: left;' +
'					margin: 2px;' +
'					width: 25px;' +
'					height: 25px;' +
'					padding: 0;' +
'				}' +
'	#bfl-image {' +
'		position: absolute;' +
'		z-index: 100;' +
'		display: none;' +
'		background: #323232;' +
'		border: 3px solid #171717;' +
'		padding: 4px;' +
'	}' +
'	#bfl-image.alignright:before {' +
'		left: auto;' +
'		right: 7px;' +
'	}' +
'	#bfl-image.alignright:after {' +
'		left: auto;' +
'		right: 10px;' +
'	}' +
'	#bfl-image.loading {' +
'		width: 80px;' +
'		height: 80px;' +
'		background: #323232 url(https://flassets.a.ssl.fastly.net/std/spinners/circle_big.gif) no-repeat center center;' +
'	}' +
'		#bfl-image .header {' +
'			position: absolute;' +
'			left: 0;' +
'			right: 0;' +
'			top: 0;' +
'			background: #323232;' +
'			padding: 4px;' +
'			overflow: hidden;' +
'			font-size: 12px;;' +
'		}'	+
'		#bfl-image.loading .header {' +
'			display: none;' +
'		}' +
'			#bfl-image .title {' +
'				float: left;' +
'				width: 80%;' +
'				white-space: nowrap;' +
'				overflow: hidden;' +
'				text-overflow: ellipsis;' +
'			}'	+
'			#bfl-image .like-wrap {' +
'				float: right;' +
'				width: 10%;' +
'				white-space: nowrap;' +
'				text-align: right;' +
'				cursor: pointer;' +
'			}'	+
'			#bfl-image .like-wrap:hover {' +
'				color: #ffffff;' +
'			}'	+
'			#bfl-image .like-wrap:active {' +
'				color: #bbbbbb;' +
'			}'	+
'			#bfl-image .like-wrap.liked {' +
'				color: #DD0000;' +
'			}'	+
'			#bfl-image .like-wrap.liked:hover {' +
'				color: #FF0000;' +
'			}'	+
'			#bfl-image .like-wrap.liked:active {' +
'				color: #BB0000;' +
'			}'	+
'		#bfl-image .image {' +
'			display: block;' +
'			padding: 0;' +
'		}'	+
'		#bfl-image.loading .image {' +
'			opacity: 0;' +
'		}' +

/*!
 * hoverIntent v1.8.0 // 2014.06.29 // jQuery v1.9.1+
 * http://cherne.net/brian/resources/jquery.hoverIntent.html
 *
 * You may use hoverIntent under the terms of the MIT license. Basically that
 * means you are free to use hoverIntent as long as this header is left intact.
 * Copyright 2007, 2014 Brian Cherne
 */
 
/* hoverIntent is similar to jQuery's built-in "hover" method except that
 * instead of firing the handlerIn function immediately, hoverIntent checks
 * to see if the user's mouse has slowed down (beneath the sensitivity
 * threshold) before firing the event. The handlerOut function is only
 * called after a matching handlerIn.
 *
 * // basic usage ... just like .hover()
 * .hoverIntent( handlerIn, handlerOut )
 * .hoverIntent( handlerInOut )
 *
 * // basic usage ... with event delegation!
 * .hoverIntent( handlerIn, handlerOut, selector )
 * .hoverIntent( handlerInOut, selector )
 *
 * // using a basic configuration object
 * .hoverIntent( config )
 *
 * @param  handlerIn   function OR configuration object
 * @param  handlerOut  function OR selector for delegation OR undefined
 * @param  selector    selector OR undefined
 * @author Brian Cherne <brian(at)cherne(dot)net>
 */
(function($) {
    $.fn.hoverIntent = function(handlerIn,handlerOut,selector) {

        // default configuration values
        var cfg = {
            interval: 100,
            sensitivity: 6,
            timeout: 0
        };

        if ( typeof handlerIn === "object" ) {
            cfg = $.extend(cfg, handlerIn );
        } else if ($.isFunction(handlerOut)) {
            cfg = $.extend(cfg, { over: handlerIn, out: handlerOut, selector: selector } );
        } else {
            cfg = $.extend(cfg, { over: handlerIn, out: handlerIn, selector: handlerOut } );
        }

        // instantiate variables
        // cX, cY = current X and Y position of mouse, updated by mousemove event
        // pX, pY = previous X and Y position of mouse, set by mouseover and polling interval
        var cX, cY, pX, pY;

        // A private function for getting mouse position
        var track = function(ev) {
            cX = ev.pageX;
            cY = ev.pageY;
        };

        // A private function for comparing current and previous mouse position
        var compare = function(ev,ob) {
            ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
            // compare mouse positions to see if they've crossed the threshold
            if ( Math.sqrt( (pX-cX)*(pX-cX) + (pY-cY)*(pY-cY) ) < cfg.sensitivity ) {
                $(ob).off("mousemove.hoverIntent",track);
                // set hoverIntent state to true (so mouseOut can be called)
                ob.hoverIntent_s = true;
                return cfg.over.apply(ob,[ev]);
            } else {
                // set previous coordinates for next time
                pX = cX; pY = cY;
                // use self-calling timeout, guarantees intervals are spaced out properly (avoids JavaScript timer bugs)
                ob.hoverIntent_t = setTimeout( function(){compare(ev, ob);} , cfg.interval );
            }
        };

        // A private function for delaying the mouseOut function
        var delay = function(ev,ob) {
            ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
            ob.hoverIntent_s = false;
            return cfg.out.apply(ob,[ev]);
        };

        // A private function for handling mouse 'hovering'
        var handleHover = function(e) {
            // copy objects to be passed into t (required for event object to be passed in IE)
            var ev = $.extend({},e);
            var ob = this;

            // cancel hoverIntent timer if it exists
            if (ob.hoverIntent_t) { ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); }

            // if e.type === "mouseenter"
            if (e.type === "mouseenter") {
                // set "previous" X and Y position based on initial entry point
                pX = ev.pageX; pY = ev.pageY;
                // update "current" X and Y position based on mousemove
                $(ob).on("mousemove.hoverIntent",track);
                // start polling interval (self-calling timeout) to compare mouse coordinates over time
                if (!ob.hoverIntent_s) { ob.hoverIntent_t = setTimeout( function(){compare(ev,ob);} , cfg.interval );}

                // else e.type == "mouseleave"
            } else {
                // unbind expensive mousemove event
                $(ob).off("mousemove.hoverIntent",track);
                // if hoverIntent state is true, then call the mouseOut function after the specified delay
                if (ob.hoverIntent_s) { ob.hoverIntent_t = setTimeout( function(){delay(ev,ob);} , cfg.timeout );}
            }
        };

        // listen for mouseenter and mouseleave
        return this.on({'mouseenter.hoverIntent':handleHover,'mouseleave.hoverIntent':handleHover}, cfg.selector);
    };
})(jQuery);