Linky Square

Grab links by dragging a square.

// ==UserScript==
// @name        Linky Square
// @author 		eight <eight04@gmail.com>
// @version     0.2.1
// @namespace   eight04.blogspot.com
// @description Grab links by dragging a square.
// @include     *
// @grant       GM_openInTab
// @grant       GM_setClipboard
// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_registerMenuCommand
// @compatible  firefox
// @compatible  chrome
// @compatible  opera
// @require https://greasyfork.org/scripts/7212-gm-config-eight-s-version/code/GM_config%20(eight's%20version).js?version=156587
// ==/UserScript==

function createLinky(o){
	var delay = function(){
		function wrap(target) {
			target();
			target.delay = false;
		}
		
		return function(target) {
			if (!target.delay) {
				target.delay = true;
				setTimeout(wrap, 0, target);
			}
		};
	}();
	
	var tracker = function(o){
		var ox = 0, oy = 0, x = 0, y = 0,
			traceStart = false,
			linkCount = 0,
			enable = false;

		var ui = function(){
			GM_addStyle(".linky-info-box,.linky-select-box{position:absolute;z-index:65534;display:none}.linky .linky-anchor-box{background:#ff0}.linky .linky-anchor-box img{filter:url(data:image/svg+xml;charset=utf8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KCTxmaWx0ZXIgaWQ9ImZpbHRlciI+DQoJCTxmZUZsb29kIHJlc3VsdD0iZmxvb2RGaWxsIiB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIg0KCQkJCWZsb29kLWNvbG9yPSJ5ZWxsb3ciIGZsb29kLW9wYWNpdHk9IjEiLz4NCgkJPGZlQmxlbmQgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZmxvb2RGaWxsIiBtb2RlPSJtdWx0aXBseSIvPg0KCTwvZmlsdGVyPg0KPC9zdmc+DQo=#filter)}.linky-select-box{border:2px dashed red;box-sizing:border-box}.linky-info-box{color:#000;border:1px solid grey;background:#fff;padding:.3em .6em}.linky body{-moz-user-select:none;-webkit-user-select:none;pointer-events:none}.linky .linky-info-box,.linky .linky-select-box{display:block}");

			var selectBox = document.createElement("div");
			selectBox.className = "linky-select-box";

			var infoBox = document.createElement("div");
			infoBox.className = "linky-info-box";

			var body = document.body;
			body.appendChild(selectBox);
			body.appendChild(infoBox);
			
			function updateSelectBox(x, y, w, h){
				var s = selectBox.style;
				s.left = x + "px";
				s.top = y + "px";
				s.width = w + "px";
				s.height = h + "px";
			}
			
			function updateInfoBox(x, y, text) {
				var s = infoBox.style;
				s.left = x + 16 + "px";
				s.top = y + 16 + "px";
				infoBox.textContent = text;
			}
		
			function on(){
				document.documentElement.classList.add("linky");
			}
			
			function off(){
				document.documentElement.classList.remove("linky");
			}
			
			return {
				on: on,
				off: off,
				updateSelectBox: updateSelectBox,
				updateInfoBox: updateInfoBox
			};
		}();

		function getOffset(node){
			var rect = node.getBoundingClientRect();

			return {
				x: window.pageXOffset + rect.left,
				y: window.pageYOffset + rect.top,
				width: rect.width,
				height: rect.height
			};
		}

		function updateSelectBox(){
			ui.updateSelectBox(
				Math.min(Math.min(x, ox)),
				Math.min(y, oy),
				Math.abs(x - ox),
				Math.abs(y - oy)
			);
		}

		function inSelect(node){
			var pos = getOffset(node);
			var centerx = pos.x + pos.width / 2;
			var centery = pos.y + pos.height / 2;

			if (centerx < Math.min(ox, x)) {
				return false;
			}
			if (centerx > Math.max(ox, x)) {
				return false;
			}
			if (centery < Math.min(oy, y)) {
				return false;
			}
			if (centery > Math.max(oy, y)) {
				return false;
			}
			return true;
		}
		
		function isJSURL(node){
			return node.href.lastIndexOf("javascript:", 0) != -1;
		}

		function updateLinkList(){
			var l = document.querySelectorAll("a[href]"), i, k = [], len = l.length;
			for (i = 0; i < len; i++) {
				k.push(inSelect(l[i]) && !isJSURL(l[i]));
			}
			linkCount = 0;
			for (i = 0; i < len; i++) {
				l[i].classList.toggle("linky-anchor-box", k[i]);
				linkCount += +k[i];
			}
		}
		
		function updateInfoBox() {
			ui.updateInfoBox(x, y, "selected " + linkCount + " link(s)");
		}

		function update(){
			updateSelectBox();
			delay(updateLinkList);
			updateInfoBox();
		}

		function takeLinks(){
			var l = document.querySelectorAll("a[href]"), i, links = [];
			for (i = 0; i < l.length; i++) {
				if (inSelect(l[i]) && !isJSURL(l[i])) {
					links.push(l[i].href);
				}
			}
			return links;
		}

		function handler(e){
			if (e.type == "mousedown") {
				if (traceStart) {
					return;
				}
				if (!o.config.key.regular(e)) {
					return;
				}
				traceStart = true;
				
				on();
				// call it directly will cause firefox to stop srcolling. why?
				setTimeout(ui.on);
				
				x = ox = e.pageX;
				y = oy = e.pageY;
				
				update();
				
			} else if (e.type == "mousemove") {
				if (!traceStart) {
					return;
				}
				
				x = e.pageX;
				y = e.pageY;
				
				update();
				
			} else if (e.type == "mouseup" || e.type == "keydown") {
				if (
					!traceStart ||
					e.type == "keydown" && !o.config.key.copy(e) && !o.config.key.cancel(e)
				) {
					return;
				}
				traceStart = false;
				
				off();
				ui.off();
				o.callback(e, takeLinks());	
			}
		}
		
		function on(){
			if (enable) {
				return;
			}
			window.addEventListener("mousemove", handler);
			window.addEventListener("mouseup", handler);
			window.addEventListener("keydown", handler);
			enable = true;
		}
		
		function off(){
			window.removeEventListener("mousemove", handler);
			window.removeEventListener("mouseup", handler);
			window.removeEventListener("keydown", handler);
			enable = false;
		}
		
		function track() {
			window.addEventListener("mousedown", handler);
			handler(o.initEvent);
		}
		
		return {
			on: on,
			off: off,
			track: track
		};
	}(o);
	
	tracker.track();
}

function openLinks(links) {
	var i;
	for (i = 0; i < links.length; i++) {
		GM_openInTab(links[i], true);
	}
}

// do object property check
function objectProperties(a, b) {
	var key;
	for (key in b) {
		if (a[key] !== b[key]) return false;
	}
	return true;
}

function createConfig() {
	var config = {key: {
		regular: function(e) {
			return objectProperties(e, config.keyRegular);
		},
		copy: function(e) {
			return objectProperties(e, config.keyCopy);
		},
		cancel: function(e) {
			return objectProperties(e, config.keyCancel);
		}
	}};
	GM_config.setup({
		behavior: {
			label: "Default action after selecting",
			type: "select",
			default: "open",
			options: {
				open: "Open links in new tab",
				copy: "Copy URLs"
			}
		},
		keyRegular: {
			label: "Event to start selection",
			type: "textarea",
			default: JSON.stringify({altKey: true, ctrlKey: false, shiftKey: false, button: 0})
		},
		keyCopy: {
			label: "Event to copy url",
			type: "textarea",
			default: JSON.stringify({code: "KeyC"})
		},
		keyCancel: {
			label: "Event to cancel selection",
			type: "textarea",
			default: JSON.stringify({code: "Escape"})
		}
	}, function() {
		var o = GM_config.get();
		Object.assign(config, o);
		for (var key in o) {
			if (!key.startsWith("key")) {
				continue;
			}
			try {
				config[key] = JSON.parse(o[key]);
			} catch (err) {
				alert("Failed to create key config! Is your config broken?");
			}
		}
	});
	return config;
}

function copyLinks(links) {
	if (!links.length) return;
	GM_setClipboard(links.join("\n"));
}

var config = createConfig();

function handler(e, links) {
	if (e.type == "mouseup" && config.behavior == "open") {
		openLinks(links);
	} else if (e.type == "mouseup" || config.key.copy(e)) {
		copyLinks(links);
	}
}

window.addEventListener("mousedown", function init(e){
	if (config.key.regular(e)) {
		window.removeEventListener("mousedown", init);
		createLinky({
			callback: handler,
			initEvent: e,
			config: config
		});
	}
});