[SNOLAB] Selection expander

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

  1. // ==UserScript==
  2. // @name [SNOLAB] Selection expander
  3. // @name:zh [SNOLAB] 选区扩展器
  4. // @namespace snomiao@gmail.com
  5. // @version 0.0.4
  6. // @description Shift+Alt+Right/Left to Expand/Shirink Selection to parent elements. (vise versa) just like vscode
  7. // @description:zh Shift+Alt+Right/Left to 扩大/缩小 文字选区,常用于代码复制等操作(反之也可)。 just like vscode
  8. // @author snomiao
  9. // @match *://*/*
  10. // @grant none
  11. // ==/UserScript==
  12. // note: migrated to https://gist.github.com/snomiao/7e4d17e1b618167654c4d1ae0dc23cd3f
  13.  
  14. globalThis?.selectionExpander?.unload?.();
  15.  
  16. function hotkeyMatch(event, hotkey) {
  17. if (Boolean(event.altKey) !== /alt|alter/i.test(hotkey)) return false;
  18. if (Boolean(event.ctrlKey) !== /ctrl|control/i.test(hotkey)) return false;
  19. if (Boolean(event.metaKey) !== /meta|win|cmd/i.test(hotkey)) return false;
  20. if (Boolean(event.shiftKey) !== /shift/i.test(hotkey)) return false;
  21. const key = hotkey.replace(/alt|alter|ctrl|control|meta|win|cmd|shift/gi, "");
  22. if (!key.toLowerCase().match(event.key.toLowerCase())) return false;
  23. event.preventDefault();
  24. return true;
  25. }
  26. const coreState = { sel: null };
  27. const expander = ([a, b]) => {
  28. // expand to sibling or parent
  29. const ap = a.previousSibling || a.parentNode;
  30. const bp = b.nextSibling || b.parentNode;
  31. if (ap?.contains(bp)) return [ap, ap];
  32. if (bp?.contains(ap)) return [bp, bp];
  33. if (ap && bp) return [ap, bp];
  34. return null;
  35. };
  36. const fnLister = (fn, val) => {
  37. const out = fn(val);
  38. return out ? [out, ...fnLister(fn, out)] : [];
  39. };
  40. const expanderLister = ([a, b]) => {
  41. return fnLister(expander, [a, b]);
  42. };
  43.  
  44. function updateCoreStateAndGetSel() {
  45. const sel = globalThis.getSelection();
  46. const { anchorNode, focusNode, anchorOffset, focusOffset } = sel;
  47. if (!coreState.sel) {
  48. coreState.sel = { anchorNode, focusNode, anchorOffset, focusOffset };
  49. }
  50. const coreNodes = [coreState.sel.anchorNode, coreState.sel.focusNode];
  51. if (!coreNodes.every((node) => sel.containsNode(node))) {
  52. coreState.sel = { anchorNode, focusNode, anchorOffset, focusOffset };
  53. }
  54. return { sel, coreNodes };
  55. }
  56. function selectionExpand() {
  57. const { sel } = updateCoreStateAndGetSel();
  58. // expand
  59. const expand = expander([sel.anchorNode, sel.focusNode]);
  60. if (!expand) return; // can't expand anymore
  61. const [anc, foc] = expand;
  62. sel.setBaseAndExtent(anc, 0, foc, foc.childNodes.length);
  63. }
  64. function selectionShirink() {
  65. const { sel, coreNodes } = updateCoreStateAndGetSel();
  66. const list = expanderLister(coreNodes);
  67. const rangeNodes = list
  68. .reverse()
  69. .find((rangeNodes) => rangeNodes.every((node) => sel.containsNode(node)));
  70. if (rangeNodes) {
  71. const [a, b] = rangeNodes;
  72. sel.setBaseAndExtent(a, 0, b, b.childNodes.length);
  73. return;
  74. }
  75. const { anchorNode, focusNode, anchorOffset, focusOffset } = coreState.sel;
  76. if (!sel.containsNode(anchorNode)) {
  77. sel.collapseToStart();
  78. return;
  79. }
  80. sel.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
  81. }
  82.  
  83. const handleKeydown = (event) => {
  84. if (hotkeyMatch(event, "alt+shift+arrowright")) selectionExpand();
  85. if (hotkeyMatch(event, "alt+shift+arrowleft")) selectionShirink();
  86. };
  87.  
  88. // load
  89. globalThis.addEventListener("keydown", handleKeydown);
  90. const unload = () => {
  91. globalThis.removeEventListener("keydown", handleKeydown);
  92. };
  93.  
  94. // export unload
  95. globalThis.selectionExpander = { unload };