// ==UserScript==
// @name [SNOLAB] Selection expander
// @name:zh [SNOLAB] 选区扩展器
// @namespace [email protected]
// @version 0.0.4
// @description Shift+Alt+Right/Left to Expand/Shirink Selection to parent elements. (vise versa) just like vscode
// @description:zh Shift+Alt+Right/Left to 扩大/缩小 文字选区,常用于代码复制等操作(反之也可)。 just like vscode
// @author snomiao
// @match *://*/*
// @grant none
// ==/UserScript==
// note: migrated to https://gist.github.com/snomiao/7e4d17e1b618167654c4d1ae0dc23cd3f
globalThis?.selectionExpander?.unload?.();
function hotkeyMatch(event, hotkey) {
if (Boolean(event.altKey) !== /alt|alter/i.test(hotkey)) return false;
if (Boolean(event.ctrlKey) !== /ctrl|control/i.test(hotkey)) return false;
if (Boolean(event.metaKey) !== /meta|win|cmd/i.test(hotkey)) return false;
if (Boolean(event.shiftKey) !== /shift/i.test(hotkey)) return false;
const key = hotkey.replace(/alt|alter|ctrl|control|meta|win|cmd|shift/gi, "");
if (!key.toLowerCase().match(event.key.toLowerCase())) return false;
event.preventDefault();
return true;
}
const coreState = { sel: null };
const expander = ([a, b]) => {
// expand to sibling or parent
const ap = a.previousSibling || a.parentNode;
const bp = b.nextSibling || b.parentNode;
if (ap?.contains(bp)) return [ap, ap];
if (bp?.contains(ap)) return [bp, bp];
if (ap && bp) return [ap, bp];
return null;
};
const fnLister = (fn, val) => {
const out = fn(val);
return out ? [out, ...fnLister(fn, out)] : [];
};
const expanderLister = ([a, b]) => {
return fnLister(expander, [a, b]);
};
function updateCoreStateAndGetSel() {
const sel = globalThis.getSelection();
const { anchorNode, focusNode, anchorOffset, focusOffset } = sel;
if (!coreState.sel) {
coreState.sel = { anchorNode, focusNode, anchorOffset, focusOffset };
}
const coreNodes = [coreState.sel.anchorNode, coreState.sel.focusNode];
if (!coreNodes.every((node) => sel.containsNode(node))) {
coreState.sel = { anchorNode, focusNode, anchorOffset, focusOffset };
}
return { sel, coreNodes };
}
function selectionExpand() {
const { sel } = updateCoreStateAndGetSel();
// expand
const expand = expander([sel.anchorNode, sel.focusNode]);
if (!expand) return; // can't expand anymore
const [anc, foc] = expand;
sel.setBaseAndExtent(anc, 0, foc, foc.childNodes.length);
}
function selectionShirink() {
const { sel, coreNodes } = updateCoreStateAndGetSel();
const list = expanderLister(coreNodes);
const rangeNodes = list
.reverse()
.find((rangeNodes) => rangeNodes.every((node) => sel.containsNode(node)));
if (rangeNodes) {
const [a, b] = rangeNodes;
sel.setBaseAndExtent(a, 0, b, b.childNodes.length);
return;
}
const { anchorNode, focusNode, anchorOffset, focusOffset } = coreState.sel;
if (!sel.containsNode(anchorNode)) {
sel.collapseToStart();
return;
}
sel.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
}
const handleKeydown = (event) => {
if (hotkeyMatch(event, "alt+shift+arrowright")) selectionExpand();
if (hotkeyMatch(event, "alt+shift+arrowleft")) selectionShirink();
};
// load
globalThis.addEventListener("keydown", handleKeydown);
const unload = () => {
globalThis.removeEventListener("keydown", handleKeydown);
};
// export unload
globalThis.selectionExpander = { unload };