├── 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 | 
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 | });
--------------------------------------------------------------------------------