├── README.md └── claw.js /README.md: -------------------------------------------------------------------------------- 1 |

clawjs

2 |

VIM-like browser extension.

3 | 4 | Inspired by [Tridactyl](https://github.com/tridactyl/tridactyl/), but actually works in every browser you throw it at. 5 | 6 | ![image](https://github.com/Nuboctane/clawjs/assets/72999487/1250d71e-c176-4a8b-b76d-515f95b7801a) 7 | 8 | Works in (as far as I'm concerned) any browser, this can easily be put into a local browser extension, 9 | you can make a browser extension locally and just use this code and unpack it in your browser's extension library. 10 | 11 | If you don't know how to make a browser extension I recommend installing one of the following existing browser extensions to help you use my code. 12 | Such as: 13 | 14 | [Code Injector](https://addons.mozilla.org/en-US/firefox/addon/codeinjector/), 15 | [Tampermonkey](https://www.tampermonkey.net/), 16 | [(chrome) Pagemanipulator](https://chromewebstore.google.com/detail/page-manipulator/mdhellggnoabbnnchkeniomkpghbekko?hl=en/) 17 | 18 | These extensions should guide you on how to do what you want with my code. 19 | 20 | You can always also just paste this code in your browsers console for a one-time run. 21 | 22 |

whats different?

23 | 24 | - Scrolling automatically keeps updating the positions of the layered frames. 25 | - The script is activated more respectfully by pressing G and H simultaneously, as opposed to just pressing F in Tridactyl. 26 | - The script uses keys that are easier in finger reach range of mostly all keyboard layouts from 25% and above. 27 | - The script automatically keeps track of the buttons/elements currently in-frame and ignores other ones respectfully, updates when scrolling. 28 | - The script makes use of less cryptic key tags when activated, read more about this below on how to use them. 29 | 30 |

how to use

31 | Press SHIFT and ALT simultaneously to activate the frames on top of interactable elements. 32 | 33 | Press SHIFT and ALT simultaneously again to disable the frames. 34 | 35 | After enabling the frames, key tags are shown in the corner of each frame indicating which key you will have to press to interact with this element. 36 | When the key you want is pressed, all frames that did not have this key will disappear and the elements that remain will show new keys. 37 | this way you can narrow down quickly which element you want to interact with. Once there are no more combinations left, the element will be selected. 38 | 39 |

known issues/TODO

40 | 41 | - Elements that can be scrolled within themselves could show key labels not directly visible to the client on activation. 42 | 43 |

issues with workarounds

44 | 45 | - Event listeners of the page you're visiting might intervene with this script when/before activated. 46 | 47 | i have not yet found a way to optimally avoid keyboard keys the DOM is already using, for now you can change your initialize settings on the bottom of claw.js 48 | 49 | example initialize settings: 50 | 51 | ```js 52 | const claw = new ClawOverlay({ 53 | keys: "QWERTYUIOPASDFGHJKLZXCVBNM".split(''), // Available keys for overlay selection 54 | refreshTimeout: 500, // Milliseconds between overlay refreshes 55 | toggleKeys: ['SHIFT', 'ALT'] // Press SHIFT + ALT to toggle overlays 56 | }); 57 | ``` 58 | 59 | Available keys: 60 | QWERTYUIOPASDFGHJKLZXCVBNM1234567890 61 | 62 | Available modifiers: 63 | SHIFT, ALT, CONTROL and META (windows key) 64 | 65 | you can also use multiple or less toggle keys in order to trigger the overlay 66 | 67 | ```js 68 | toggleKeys: ['F', 'G', 'H'] // Press F + G + H to toggle overlays 69 | ``` 70 | 71 | or 72 | 73 | ```js 74 | toggleKeys: ['F'] // Press just F to toggle overlays 75 | ``` 76 | 77 | want to use even less keys when navigating through the overlay? 78 | 79 | try: 80 | 81 | ```js 82 | const claw = new ClawOverlay({ 83 | keys: "ASD".split(''), // Available keys for overlay selection 84 | refreshTimeout: 500, // Milliseconds between overlay refreshes 85 | toggleKeys: ['CONTROL', 'ALT'] // Press CONTROL + ALT to toggle overlays 86 | }); 87 | ``` 88 | 89 | this way there will only be boxes labeled with A, S and D. Allowing you to narrow down the element you want to select by using only these 3 keys, get creative with it. 90 | 91 |

TIPS

92 | 93 | When using tools like [Code Injector](https://addons.mozilla.org/en-US/firefox/addon/codeinjector/) to run my code, you could use the same code with different initialization settings for certain websites you usually visit. This way you can better shape your VIM-like experience while browsing on any website. 94 | -------------------------------------------------------------------------------- /claw.js: -------------------------------------------------------------------------------- 1 | // author: @n-ubo 2 | // version: 2.0 3 | // license: None 4 | 5 | class ClawOverlay { 6 | constructor(config = {}) { 7 | //want to edit the keys used for the overlay? edit the initialization of the class on the bottom of this file. 8 | this.defaultKeys = config.keys || "QWERTYUIOPLKJHGFDSAZXCVBNM1234567890".split(''); 9 | this.timeout = config.refreshTimeout || 500; 10 | this.toggleKeys = config.toggleKeys || ['G', 'H']; 11 | this.keyMap = new Map(); 12 | this.visible = false; 13 | this.heldKeys = new Set(); 14 | this.disabledListeners = []; 15 | 16 | this._injectStyle(); 17 | this._setupEvents(); 18 | } 19 | 20 | _injectStyle() { 21 | const style = document.createElement("style"); 22 | style.textContent = ` 23 | .claw-overlay { 24 | position: absolute !important; 25 | pointer-events: none !important; 26 | z-index: 999999999 !important; 27 | outline: 2px solid rgb(2, 20, 183) !important; 28 | } 29 | .claw-label { 30 | background: rgba(0, 20, 197, 0.562); 31 | font-family: Arial, sans-serif; 32 | font-size: 0.9em; 33 | color: cyan; 34 | padding: 1px 4px; 35 | pointer-events: none; 36 | position: absolute; 37 | }`; 38 | document.head.appendChild(style); 39 | } 40 | 41 | _setupEvents() { 42 | document.addEventListener("keydown", (e) => this._handleKeyDown(e), true); 43 | document.addEventListener("keyup", (e) => this._handleKeyUp(e), true); 44 | window.addEventListener("resize", () => this._refresh()); 45 | window.addEventListener("scroll", () => this._debouncedRefresh(this.timeout / 2)); 46 | window.addEventListener("wheel", () => this._debouncedRefresh(this.timeout / 2)); 47 | document.addEventListener("click", () => this._debouncedRefresh(this.timeout)); 48 | } 49 | 50 | _renderSubset(elements) { 51 | this._removeOverlays(); 52 | this.keyMap.clear(); 53 | 54 | for (let i = 0; i < elements.length; i++) { 55 | const el = elements[i]; 56 | const key = this.defaultKeys[i % this.defaultKeys.length]; 57 | 58 | if (!this.keyMap.has(key)) { 59 | this.keyMap.set(key, []); 60 | } 61 | this.keyMap.get(key).push(el); 62 | 63 | const rect = el.getBoundingClientRect(); 64 | const label = document.createElement("div"); 65 | label.className = "claw-overlay"; 66 | label.setAttribute("data-claw-key", key); 67 | label.style.left = `${rect.left + window.scrollX}px`; 68 | label.style.top = `${rect.top + window.scrollY}px`; 69 | label.style.width = `${rect.width}px`; 70 | label.style.height = `${rect.height}px`; 71 | 72 | const text = document.createElement("div"); 73 | text.className = "claw-label"; 74 | text.textContent = key; 75 | text.style.top = "2px"; 76 | text.style.left = "2px"; 77 | 78 | label.appendChild(text); 79 | document.body.appendChild(label); 80 | } 81 | } 82 | 83 | 84 | _handleKeyDown(e) { 85 | const key = e.key.toUpperCase(); 86 | this.heldKeys.add(key); 87 | 88 | if (this._isUserTyping()) return; 89 | 90 | if (this._toggleComboPressed(e)) { 91 | e.preventDefault(); 92 | this.toggle(); 93 | return; 94 | } 95 | 96 | if (!this.visible) return; 97 | 98 | const targets = this.keyMap.get(key); 99 | if (targets && targets.length > 0) { 100 | e.preventDefault(); 101 | 102 | if (targets.length === 1) { 103 | const el = targets[0]; 104 | el.focus(); 105 | el.click(); 106 | 107 | if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) { 108 | setTimeout(() => this._refresh(), this.timeout); 109 | } else { 110 | this.hide(); 111 | } 112 | } else { 113 | this._renderSubset(targets); 114 | } 115 | } 116 | } 117 | 118 | _handleKeyUp() { 119 | this.heldKeys.clear(); 120 | } 121 | 122 | _toggleComboPressed(e) { 123 | const combo = this.toggleKeys.map(k => k.toUpperCase()); 124 | const keySet = new Set(this.heldKeys); 125 | 126 | if (combo.includes("SHIFT") && !e.shiftKey) return false; 127 | if (combo.includes("ALT") && !e.altKey) return false; 128 | if (combo.includes("CONTROL") && !e.ctrlKey) return false; 129 | if (combo.includes("META") && !e.metaKey) return false; 130 | 131 | const nonModifierKeys = combo.filter(k => !["SHIFT", "ALT", "CONTROL", "META"].includes(k)); 132 | return nonModifierKeys.every(key => keySet.has(key)); 133 | } 134 | 135 | _debouncedRefresh(delay) { 136 | clearTimeout(this.refreshTimer); 137 | this.refreshTimer = setTimeout(() => this._refresh(), delay); 138 | } 139 | 140 | toggle() { 141 | if (this._isUserTyping()) return; 142 | this.visible ? this.hide() : this.show(); 143 | } 144 | 145 | show() { 146 | this._disableConflictingListeners(); 147 | this._generateOverlays(); 148 | this.visible = true; 149 | } 150 | 151 | hide() { 152 | this._removeOverlays(); 153 | this.keyMap.clear(); 154 | this.visible = false; 155 | 156 | for (const { type, intercept } of this.disabledListeners) { 157 | document.removeEventListener(type, intercept, true); 158 | } 159 | this.disabledListeners = []; 160 | } 161 | 162 | _refresh() { 163 | if (this.visible) { 164 | this.hide(); 165 | this.show(); 166 | } 167 | } 168 | 169 | _disableConflictingListeners() { 170 | const intercept = (e) => { 171 | if (this.visible) { 172 | e.preventDefault(); 173 | e.stopImmediatePropagation(); 174 | e.stopPropagation(); 175 | } 176 | }; 177 | 178 | ['keydown', 'keypress', 'keyup'].forEach(type => { 179 | document.addEventListener(type, intercept, true); 180 | this.disabledListeners.push({ type, intercept }); 181 | }); 182 | } 183 | 184 | _generateOverlays() { 185 | const interactiveTags = 'button, input:not([type="hidden"]), textarea, select, a[href]'; 186 | const elements = Array.from(document.querySelectorAll(interactiveTags)).filter(el => this._isVisible(el)); 187 | 188 | this.keyMap.clear(); 189 | for (let i = 0; i < elements.length; i++) { 190 | const el = elements[i]; 191 | const key = this.defaultKeys[i % this.defaultKeys.length]; 192 | 193 | if (!this.keyMap.has(key)) { 194 | this.keyMap.set(key, []); 195 | } 196 | this.keyMap.get(key).push(el); 197 | 198 | const rect = el.getBoundingClientRect(); 199 | const label = document.createElement("div"); 200 | label.className = "claw-overlay"; 201 | label.setAttribute("data-claw-key", key); 202 | label.style.left = `${rect.left + window.scrollX}px`; 203 | label.style.top = `${rect.top + window.scrollY}px`; 204 | label.style.width = `${rect.width}px`; 205 | label.style.height = `${rect.height}px`; 206 | 207 | const text = document.createElement("div"); 208 | text.className = "claw-label"; 209 | text.textContent = key; 210 | text.style.top = "2px"; 211 | text.style.left = "2px"; 212 | 213 | label.appendChild(text); 214 | document.body.appendChild(label); 215 | } 216 | } 217 | 218 | _showOnlyKeyOverlays(pressedKey) { 219 | const overlays = document.querySelectorAll(".claw-overlay"); 220 | overlays.forEach(o => { 221 | const key = o.getAttribute("data-claw-key"); 222 | o.style.display = key === pressedKey ? "block" : "none"; 223 | }); 224 | } 225 | 226 | _removeOverlays() { 227 | const overlays = document.querySelectorAll(".claw-overlay"); 228 | overlays.forEach(o => o.remove()); 229 | } 230 | 231 | _isVisible(el) { 232 | const rect = el.getBoundingClientRect(); 233 | return ( 234 | rect.width > 0 && 235 | rect.height > 0 && 236 | rect.bottom >= 0 && 237 | rect.right >= 0 && 238 | rect.top <= (window.innerHeight || document.documentElement.clientHeight) && 239 | rect.left <= (window.innerWidth || document.documentElement.clientWidth) 240 | ); 241 | } 242 | 243 | _isUserTyping() { 244 | const active = document.activeElement; 245 | return ( 246 | active && 247 | ( 248 | active.tagName === 'INPUT' || 249 | active.tagName === 'TEXTAREA' || 250 | active.isContentEditable || 251 | active.getAttribute('role') === 'textbox' 252 | ) && 253 | !active.readOnly && 254 | !active.disabled 255 | ); 256 | } 257 | } 258 | 259 | 260 | // Initialize 261 | const claw = new ClawOverlay({ 262 | keys: "QWERTYUIOPASDFGHJKLZXCVBNM".split(''), // Available keys for overlay selection 263 | refreshTimeout: 500, // Milliseconds between overlay refreshes 264 | toggleKeys: ['CONTROL', 'ALT'] // Press CONTROL + ALT to toggle overlays 265 | }); --------------------------------------------------------------------------------