[SNOLAB] Selection expander

Shift+Alt+Right/Left to Expand/Shirink Selection to parent elements. (vise versa) just like vscode

// ==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 };

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址