Select like a Boss
// ==UserScript==
// @name Select like a Boss
// @namespace https://github.com/lcandy2/Select-like-a-Boss
// @version 2024.3.16
// @license MPL-2.0
// @description With this extension, you can easily select link text just like regular text, making it easier to copy. Just Select like a Boss! ;)
// @author seril🍋
// @match *://*/*
// @run-at document-end
// @homepageURL https://lcandy2.github.io/Select-like-a-Boss/
// @icon https://raw.githubusercontent.com/lcandy2/Select-like-a-Boss/main/src/icons/icon16.png
// @supportURL https://github.com/lcandy2/Select-like-a-Boss/issues
// @grant none
// ==/UserScript==
(function() {
'use strict';
const _bind = (evt, bind = true) => {
const events = Array.isArray(evt) ? evt : [evt];
const method = bind ? 'addEventListener' : 'removeEventListener';
events.forEach(e => document[method](e, handlers[e], true));
};
const _unbind = (evt) => _bind(evt, false);
function getCurrentAnchor(n) {
while (n && n !== document.body) {
if (n instanceof HTMLAnchorElement || n instanceof HTMLButtonElement) return n;
n = n.parentNode;
}
return null;
}
const stopEvent = (e) => {
return e.preventDefault(), e.stopPropagation(), false;
};
// browser compatibility
const getRangeFromPoint = (x, y) => {
if (document.caretPositionFromPoint) {
let range = document.createRange();
let p = document.caretPositionFromPoint(x, y);
range.setStart(p.offsetNode, p.offset);
return range;
} else return document.caretRangeFromPoint(x, y);
}
// user style
const _letUserSelect = (function () {
let n,
styleElm = document.createElement('style');
let _className = 'ext-Select-like-a-Boss',
_property = '-webkit-user-select:text!important;outline-width:0!important;';
document.head.appendChild(styleElm);
styleElm.sheet.insertRule(`.${_className}{${_property}}`, 0);
return (node) => {
if (node) {
(n = node).classList.add(_className);
} else if (n) {
n.classList.remove(_className);
n = null;
}
};
})();
let selection = document.getSelection();
let cursor = {},
movable,
needDetermineUserSelection,
needCreateStartSelection,
needStopClick,
userSelecting,
regexTDTH = /T[HD]/;
const mainMouseDownHandler = (e) => {
let t = e.target
// console.log(t)
if (e.button !== 0) return; // LMB only
// resetVars
needDetermineUserSelection = needCreateStartSelection = true;
userSelecting = needStopClick = false;
cursor.x = e.clientX;
cursor.y = e.clientY;
if (selection.type === 'Range') {
let range = getRangeFromPoint(cursor.x, cursor.y);
if (range && selection.getRangeAt(0).isPointInRange(range.startContainer, range.startOffset)
) return;
}
_letUserSelect();
if (t.nodeType === 3) t = t.parentNode
if (e.ctrlKey && regexTDTH.test(t.tagName) || e.altKey) return;
let n = getCurrentAnchor(t);
// console.log(n)
if (['HTMLTextAreaElement', 'HTMLCanvasElement'].includes(t.constructor.name) || t.textContent === '' || !n) return;
let rect = n.getBoundingClientRect();
movable = { n: n, x: Math.round(rect.left), y: Math.round(rect.top), c: 0 };
_bind(['mousemove', 'mouseup', 'dragend', 'dragstart']);
_letUserSelect(n);
};
// detection range setting
let D = 3,
K = 0.8;
function getOutFromMoveHandler() {
_unbind(['mousemove', 'mouseup', 'dragend', 'dragstart', 'click']);
_letUserSelect();
selection.removeAllRanges();
}
const handlers = {
mousemove: (e) => {
if (movable) {
if (movable.n.constructor !== HTMLAnchorElement && movable.n.draggable) {
movable = null;
return getOutFromMoveHandler();
}
if (movable.c++ < 12) {
let rect = movable.n.getBoundingClientRect();
if (
Math.round(rect.left) !== movable.x ||
Math.round(rect.top) !== movable.y
) {
_unbind(['mousemove', 'mouseup', 'dragend', 'dragstart', 'click']);
_letUserSelect();
selection.removeAllRanges();
return;
}
} else movable = null;
}
let x = e.clientX;
let y = e.clientY;
if (needCreateStartSelection) {
if (!e.altKey || !e.ctrlKey) selection.removeAllRanges();
let correct = x > cursor.x ? -2 : 2;
let range = getRangeFromPoint(x + correct, y);
if (range) {
selection.addRange(range);
needCreateStartSelection = false;
}
}
if (needDetermineUserSelection) {
let vx = Math.abs(cursor.x - x),
vy = Math.abs(cursor.y - y);
userSelecting = vy === 0 || vx / vy > K;
if (vx > D || vy > D) {
needDetermineUserSelection = false;
if (userSelecting) {
needStopClick = true;
_bind('click');
}
}
}
if (userSelecting) {
let range = getRangeFromPoint(x, y);
if (range) selection.extend(range.startContainer, range.startOffset);
}
},
dragstart: (e) => {
_unbind('dragstart');
if (userSelecting) return stopEvent(e);
},
mouseup: (e) => {
_unbind(['mousemove', 'mouseup', 'dragstart', 'dragend']);
if (!userSelecting && needStopClick) needStopClick = false;
setTimeout(() => _unbind('click'), 111);
if (selection.type !== 'Range') _letUserSelect();
},
dragend: () => {
_unbind(['dragend', 'mousemove', 'mouseup']);
},
click: function (e) {
_unbind('click');
if (selection.type !== 'Range') _letUserSelect();
if (needStopClick) return stopEvent(e);
},
};
document.addEventListener('mousedown', mainMouseDownHandler, true);
})();