├── logo.png ├── testing ├── test-kb-shortcuts-ext │ ├── background.js │ └── manifest.json ├── buttons-and-inputs-to-test-ctrl-i.html ├── inputs-for-gif-demo.html ├── page-with-inputs.html ├── page-with-inputs-and-textareas.html ├── find-links-with-cursor-pointer.html ├── page-with-all-input-types.html ├── page-with-links.html └── mark-js-iteration.js ├── src ├── icons │ ├── 16x16.png │ ├── 32x32.png │ ├── 48x48.png │ └── 128x128.png ├── popup.html ├── popup.css ├── background.js ├── common.js ├── popup.js ├── google.js ├── manifest.json ├── universal.js ├── mark.min.js └── mark.js ├── .gitignore ├── readme-assets ├── logo.png ├── link-finder.gif ├── google-navbar.png ├── load-unpacked.png ├── focus-next-input-element.gif ├── google-search-shortcuts.gif └── image-creation-notes.txt ├── chrome-store-assets ├── focus-input-box.png ├── marquee-promo-tile.png ├── marquee-promo-tile.pxz ├── small-promo-tile.png ├── small-promo-tile.pxz ├── find-in-page-shortcuts.png ├── google-results-shortcuts.png ├── icon128x128-with-padding.png ├── google-navigation-shortcuts.png └── readme.txt ├── pack-extension.sh ├── changelog.txt └── README.md /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/logo.png -------------------------------------------------------------------------------- /testing/test-kb-shortcuts-ext/background.js: -------------------------------------------------------------------------------- 1 | const cl = console.log 2 | 3 | cl(929292) 4 | -------------------------------------------------------------------------------- /src/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/src/icons/16x16.png -------------------------------------------------------------------------------- /src/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/src/icons/32x32.png -------------------------------------------------------------------------------- /src/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/src/icons/48x48.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | mouse-is-lava.crx 4 | mouse-is-lava.zip 5 | pack-extension-crx.sh 6 | -------------------------------------------------------------------------------- /readme-assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/readme-assets/logo.png -------------------------------------------------------------------------------- /src/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/src/icons/128x128.png -------------------------------------------------------------------------------- /readme-assets/link-finder.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/readme-assets/link-finder.gif -------------------------------------------------------------------------------- /readme-assets/google-navbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/readme-assets/google-navbar.png -------------------------------------------------------------------------------- /readme-assets/load-unpacked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/readme-assets/load-unpacked.png -------------------------------------------------------------------------------- /chrome-store-assets/focus-input-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/chrome-store-assets/focus-input-box.png -------------------------------------------------------------------------------- /chrome-store-assets/marquee-promo-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/chrome-store-assets/marquee-promo-tile.png -------------------------------------------------------------------------------- /chrome-store-assets/marquee-promo-tile.pxz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/chrome-store-assets/marquee-promo-tile.pxz -------------------------------------------------------------------------------- /chrome-store-assets/small-promo-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/chrome-store-assets/small-promo-tile.png -------------------------------------------------------------------------------- /chrome-store-assets/small-promo-tile.pxz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/chrome-store-assets/small-promo-tile.pxz -------------------------------------------------------------------------------- /readme-assets/focus-next-input-element.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/readme-assets/focus-next-input-element.gif -------------------------------------------------------------------------------- /readme-assets/google-search-shortcuts.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/readme-assets/google-search-shortcuts.gif -------------------------------------------------------------------------------- /chrome-store-assets/find-in-page-shortcuts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/chrome-store-assets/find-in-page-shortcuts.png -------------------------------------------------------------------------------- /chrome-store-assets/google-results-shortcuts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/chrome-store-assets/google-results-shortcuts.png -------------------------------------------------------------------------------- /chrome-store-assets/icon128x128-with-padding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/chrome-store-assets/icon128x128-with-padding.png -------------------------------------------------------------------------------- /chrome-store-assets/google-navigation-shortcuts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruns/the-mouse-is-lava/HEAD/chrome-store-assets/google-navigation-shortcuts.png -------------------------------------------------------------------------------- /pack-extension.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | filename=mouse-is-lava 6 | 7 | (cd src/ && zip -r "../${filename}.zip" . -x '*~') 8 | 9 | echo 10 | echo To publish MIL to the Chrome Store, upload "${filename}.zip" as a new package 11 | -------------------------------------------------------------------------------- /testing/buttons-and-inputs-to-test-ctrl-i.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 | 7 |
8 |
9 |
10 | 11 |
12 |
13 |
14 | 20 |
21 | 22 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Find Box 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /testing/test-kb-shortcuts-ext/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Keyboard Shortcuts Example2", 4 | "version": "1.0", 5 | "permissions": ["activeTab", "scripting"], 6 | "background": { 7 | "service_worker": "background.js" 8 | }, 9 | "commands": { 10 | "focusNextInputElement": { 11 | "suggested_key": { 12 | "default": "Ctrl+Shift+P" 13 | }, 14 | "description": "Focus the next input element" 15 | }, 16 | "openSearchBar": { 17 | "suggested_key": { 18 | "default": "Ctrl+Shift+I" 19 | }, 20 | "description": "Open the search bar" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /readme-assets/image-creation-notes.txt: -------------------------------------------------------------------------------- 1 | ### create logo from unicode emoji 2 | 3 | - open keynote 4 | - create slide with nothing but the emoji character as text 5 | - click `Format`, upper right, and set `Current Fill` to `No Fill` 6 | (transparent background) 7 | - File → Export To → Images → PNG with transparent background → Save 8 | - open the exported png in gimp 9 | - Image → Crop to Content 10 | - export as png 11 | - voila ✨ 12 | 13 | 14 | ### encode gif from mp4 with ffmpeg 15 | 16 | $ ffmpeg -i link-finder.mov -ss 0.6 -c:v libx264 -preset fast -an -y link-finder.mp4 17 | $ ffmpeg -i link-finder.mp4 -vf "fps=12,scale=800:-1:flags=lanczos,palettegen" -y palette.png 18 | $ ffmpeg -i link-finder.mp4 -i palette.png -filter_complex "fps=12,scale=800:-1:flags=lanczos[x];[x][1:v]paletteuse" -y link-finder.gif 19 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | v0.3.0 3 | ================================================================================ 4 | added: ctrl+b now iterates through both and 13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /testing/find-links-with-cursor-pointer.html: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | page with different elements and parent elements with `cursor: pointer` to test ctrl+i 14 |
15 |
16 | lolsup 17 |
18 |
19 |
20 | 21 | foobar 22 | 23 |
24 |
25 |
26 | 35 |
36 |
37 |
38 | only parent has cursor: pointer 39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | 81 | 82 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | // 2 | // The Mouse is Lava - Keyboard Shortcuts for the Web 3 | // 4 | // Ansgar Grunseid 5 | // grunseid.com 6 | // grunseid@gmail.com 7 | // 8 | // License: MIT 9 | // 10 | 11 | const cl = console.log 12 | 13 | // same as in popup.js. TODO(ans): refactor 14 | function callInActiveTab (fnName, ...args) { 15 | chrome.tabs.query({ active: true, currentWindow: true }, tabs => { 16 | if (tabs.length < 1 || !tabs[0].id) { 17 | return 18 | } 19 | 20 | const activeTabId = tabs[0].id 21 | chrome.scripting.executeScript({ 22 | target: { tabId: activeTabId }, 23 | args: [fnName, args], 24 | func: (fnName, args) => { 25 | const MIL = window.__MIL 26 | if (MIL && typeof MIL[fnName] === 'function') { 27 | MIL[fnName](...args) 28 | } else { 29 | console.error(`${fnName}() not found in window.__MIL`) 30 | } 31 | }, 32 | }).catch(error => { 33 | console.log('Script injection execution failed:', error) 34 | }) 35 | }) 36 | } 37 | 38 | chrome.commands.onCommand.addListener(command => { 39 | if (command === 'openSearchBar') { 40 | chrome.action.openPopup() 41 | } else { 42 | callInActiveTab(command) 43 | } 44 | }) 45 | 46 | chrome.runtime.onConnect.addListener(port => { 47 | if (port.name !== 'popup') { 48 | return 49 | } 50 | 51 | port.onDisconnect.addListener(() => { 52 | callInActiveTab('clearAllMatches') 53 | }) 54 | }) 55 | 56 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 57 | // there's no programmatic way to close the popup. so move focus to 58 | // the active tab, which auto-closes the popup 59 | if (message.closePopup) { 60 | chrome.windows.getCurrent(window => { 61 | if (window && window.id) { 62 | chrome.windows.update(window.id, { focused: true }) 63 | } 64 | }) 65 | } 66 | }) 67 | -------------------------------------------------------------------------------- /testing/page-with-all-input-types.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | All <input> Types 6 | 7 | 8 |

All <input> Types Demo

9 |
10 | 11 |

Textual Inputs

12 |
13 |
14 |
15 |
16 |
17 |
18 | 19 |

Numeric and Range Inputs

20 |
21 |
22 | 23 |

Date and Time Inputs

24 |
25 |
26 |
27 |
28 |
29 | 30 |

Boolean Inputs

31 |
32 |
33 | 34 |

File and Button Inputs

35 |
36 | 37 | 38 | 39 | 40 | 41 |

Other Inputs

42 | 43 |
44 | 45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /testing/page-with-links.html: -------------------------------------------------------------------------------- 1 | 2 | 34 | 35 | page with links to test ctrl+i find and [enter] and ctrl+[enter] 36 |
37 |
38 |
39 |
40 | 42 | 43 | 44 |
45 |
46 | google 47 |
48 |
49 | google 1 50 |
51 |
52 | wikipedia 11 2 53 |
54 |
55 | chatgpt 111 22 333 56 |
57 |
58 | google 59 |
60 |
61 | across text 62 |
63 |
64 |
65 | clickable parent depth=1 66 |
67 |
68 |
69 | 70 | clickable parent depth=2 71 | 72 |
73 |
74 |
75 | 76 | 77 | clickable parent depth=3 78 | 79 | 80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | google 119 |
120 |
121 | 122 | -------------------------------------------------------------------------------- /src/common.js: -------------------------------------------------------------------------------- 1 | // 2 | // The Mouse is Lava - Keyboard Shortcuts for the Web 3 | // 4 | // Ansgar Grunseid 5 | // grunseid.com 6 | // grunseid@gmail.com 7 | // 8 | // License: MIT 9 | // 10 | 11 | window.__MIL = {} 12 | 13 | ;(function () { 14 | const cl = console.log 15 | const MIL = window.__MIL 16 | 17 | function isAnyInputFocused () { 18 | const $activeEle = document.activeElement 19 | return ['input', 'textarea'].includes($activeEle.tagName.toLowerCase()) 20 | } 21 | MIL.isAnyInputFocused = isAnyInputFocused 22 | 23 | // returns true if at least half of the element is visible both 24 | // horizontally and vertically. otherwise false 25 | function isInViewport ($ele) { 26 | const $docEle = document.documentElement 27 | const pos = $ele.getBoundingClientRect() 28 | 29 | const eleHeight = pos.bottom - pos.top 30 | const eleWidth = pos.right - pos.left 31 | 32 | const visibleHeight = ( 33 | Math.min(window.innerHeight || $docEle.clientHeight, pos.bottom) 34 | - Math.max(0, pos.top)) 35 | const visibleWidth = ( 36 | Math.min(window.innerWidth || $docEle.clientWidth, pos.right) 37 | - Math.max(0, pos.left)) 38 | 39 | const atLeastHalfVisible = ( 40 | (visibleHeight >= eleHeight / 2) && (visibleWidth >= eleWidth / 2)) 41 | 42 | return atLeastHalfVisible 43 | } 44 | MIL.isInViewport = isInViewport 45 | 46 | function isVisible ($ele) { 47 | // check ele size and basic CSS visibility 48 | const rect = $ele.getBoundingClientRect() 49 | const style = window.getComputedStyle($ele) 50 | if (style.display === 'none' || style.visibility === 'hidden' || 51 | style.opacity === '0' || rect.width === 0 || rect.height === 0) { 52 | return false 53 | } 54 | 55 | return true 56 | } 57 | MIL.isVisible = isVisible 58 | 59 | function isOccluded ($ele) { 60 | // run a super simple occlusion check by making sure the four 61 | // corners + center of the element are visible and not 62 | // occluded. if the ele is outside the viewport, 63 | // elementFromPoint() doesn't work. so ignore those elements 64 | const rect = $ele.getBoundingClientRect() 65 | const points = [ 66 | [Math.floor((rect.left + rect.right) / 2), Math.floor((rect.top + rect.bottom) / 2)], 67 | [Math.floor(rect.left + 1), Math.floor(rect.top + 1)], 68 | [Math.floor(rect.right - 1), Math.floor(rect.top + 1)], 69 | [Math.floor(rect.left + 1), Math.floor(rect.bottom - 1)], 70 | [Math.floor(rect.right - 1), Math.floor(rect.bottom - 1)] 71 | ] 72 | 73 | for (const [x, y] of points) { 74 | // $ele is off screen; skip occlusion check with 75 | // elementFromPoint() 76 | const w = window 77 | if (x < 0 || y < 0 || x > w.innerWidth || y > w.innerHeight) { 78 | continue 79 | } 80 | 81 | const topEl = document.elementFromPoint(x, y) 82 | if (!$ele.contains(topEl) && topEl !== $ele) { 83 | return true 84 | } 85 | } 86 | 87 | return false 88 | } 89 | MIL.isOccluded = isOccluded 90 | })() 91 | -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | // 2 | // The Mouse is Lava - Keyboard Shortcuts for the Web 3 | // 4 | // Ansgar Grunseid 5 | // grunseid.com 6 | // grunseid@gmail.com 7 | // 8 | // License: MIT 9 | // 10 | 11 | const cl = console.log 12 | 13 | // same as in background.js. TODO(ans): refactor 14 | function callInActiveTab (fnName, ...args) { 15 | chrome.tabs.query({ active: true, currentWindow: true }, tabs => { 16 | if (tabs.length < 1) { 17 | return 18 | } 19 | 20 | const activeTabId = tabs[0].id 21 | chrome.scripting.executeScript({ 22 | target: { tabId: activeTabId }, 23 | args: [fnName, args], 24 | func: (fnName, args) => { 25 | const MIL = window.__MIL 26 | if (MIL && typeof MIL[fnName] === 'function') { 27 | MIL[fnName](...args) 28 | } else { 29 | console.error(`${fnName}() not found in window.__MIL`) 30 | } 31 | }, 32 | }).catch(error => { 33 | console.log('Script injection execution failed:', error) 34 | }) 35 | }) 36 | } 37 | 38 | function closePopup () { 39 | chrome.runtime.sendMessage({ closePopup: true }) 40 | } 41 | 42 | function selectPreviousMatch () { 43 | callInActiveTab('selectMatchFromOffset', -1) 44 | } 45 | function selectNextMatch () { 46 | callInActiveTab('selectMatchFromOffset', 1) 47 | } 48 | 49 | document.addEventListener('DOMContentLoaded', () => { 50 | const $findInput = document.getElementById('find-input') 51 | const $prevBtn = document.getElementById('prev-btn') 52 | const $nextBtn = document.getElementById('next-btn') 53 | const $closeBtn = document.getElementById('close-btn') 54 | 55 | // restore the previous value, if any 56 | const savedFindInputValue = localStorage.getItem('savedFindInputValue') || '' 57 | $findInput.value = savedFindInputValue 58 | if ($findInput.value) { 59 | $findInput.select() 60 | callInActiveTab('findInViewport', $findInput.value) 61 | } 62 | $findInput.focus() 63 | 64 | $findInput.addEventListener('input', () => { 65 | // persist the value across finds. like chrome's ctrl+f 66 | localStorage.setItem('savedFindInputValue', $findInput.value) 67 | 68 | const s = $findInput.value 69 | callInActiveTab('findInViewport', s) 70 | }) 71 | $prevBtn.addEventListener('click', selectPreviousMatch) 72 | $nextBtn.addEventListener('click', selectNextMatch) 73 | $closeBtn.addEventListener('click', closePopup) 74 | 75 | document.addEventListener('keydown', function (event) { 76 | // support both the Ctrl key and Meta on OS X 77 | const ctrlDown = event.ctrlKey || event.metaKey 78 | cl('ctrlDown', ctrlDown, event.ctrlKey, event.metaKey, event.key) 79 | if (ctrlDown && event.key === 'j') { 80 | selectNextMatch() 81 | } else if (ctrlDown && event.key === 'k') { 82 | selectPreviousMatch() 83 | } else if (event.shiftKey && event.key === 'Tab') { 84 | event.preventDefault() 85 | event.stopPropagation() 86 | selectPreviousMatch() 87 | } else if (event.key === 'Tab') { 88 | event.preventDefault() 89 | event.stopPropagation() 90 | selectNextMatch() 91 | } else if (event.key === 'Enter') { 92 | callInActiveTab('clickSelectedMatch', ctrlDown) 93 | 94 | $findInput.select() 95 | } 96 | }) 97 | }) 98 | 99 | const port = chrome.runtime.connect({ name: 'popup' }) 100 | window.addEventListener('unload', () => { 101 | port.disconnect() // triggers `onDisconnect` in the service worker 102 | }) 103 | -------------------------------------------------------------------------------- /testing/mark-js-iteration.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | // the input field 3 | var input = document.querySelector("input[type='search']"), 4 | // clear button 5 | clearBtn = document.querySelector("button[data-search='clear']"), 6 | // prev button 7 | prevBtn = document.querySelector("button[data-search='prev']"), 8 | // next button 9 | nextBtn = document.querySelector("button[data-search='next']"), 10 | // the context where to search 11 | content = document.querySelector(".content"), 12 | // NodeList to save elements 13 | results = [], 14 | // the class that will be appended to the current 15 | // focused element 16 | currentClass = "current", 17 | // top offset for the jump (the search bar) 18 | offsetTop = 50, 19 | // the current index of the focused element 20 | currentIndex = 0 21 | 22 | /** 23 | * Jumps to the element matching the currentIndex 24 | */ 25 | function jumpTo() { 26 | if (results.length) { 27 | var current = results[currentIndex] 28 | results.forEach(el => el.classList.remove(currentClass)) 29 | if (current) { 30 | current.classList.add(currentClass) 31 | var position = current.getBoundingClientRect().top + window.scrollY - offsetTop 32 | window.scrollTo({ top: position, behavior: 'smooth' }) 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * Searches for the entered keyword in the 39 | * specified context on input 40 | */ 41 | input.addEventListener('input', function() { 42 | var searchVal = this.value.trim() 43 | content.querySelectorAll('mark').forEach(mark => { 44 | var parent = mark.parentNode 45 | parent.replaceChild(document.createTextNode(mark.textContent), mark) 46 | }) 47 | 48 | if (searchVal) { 49 | var regex = new RegExp(searchVal, 'gi') 50 | var walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, null, false) 51 | var nodes = [] 52 | while (walker.nextNode()) { 53 | nodes.push(walker.currentNode) 54 | } 55 | 56 | nodes.forEach(node => { 57 | var matches = node.nodeValue.match(regex) 58 | if (matches) { 59 | var markWrapper = document.createElement('span') 60 | markWrapper.innerHTML = node.nodeValue.replace(regex, match => `${match}`) 61 | node.replaceWith(...markWrapper.childNodes) 62 | } 63 | }) 64 | } 65 | 66 | results = Array.from(content.querySelectorAll('mark')) 67 | currentIndex = 0 68 | jumpTo() 69 | }) 70 | 71 | /** 72 | * Clears the search 73 | */ 74 | clearBtn.addEventListener('click', function() { 75 | content.querySelectorAll('mark').forEach(mark => { 76 | var parent = mark.parentNode 77 | parent.replaceChild(document.createTextNode(mark.textContent), mark) 78 | }) 79 | input.value = '' 80 | input.focus() 81 | }) 82 | 83 | /** 84 | * Next and previous search jump to 85 | */ 86 | function navigateResults(forward) { 87 | if (results.length) { 88 | currentIndex += forward ? 1 : -1 89 | if (currentIndex < 0) { 90 | currentIndex = results.length - 1 91 | } 92 | if (currentIndex > results.length - 1) { 93 | currentIndex = 0 94 | } 95 | jumpTo() 96 | } 97 | } 98 | 99 | nextBtn.addEventListener('click', function() { 100 | navigateResults(true) 101 | }) 102 | 103 | prevBtn.addEventListener('click', function() { 104 | navigateResults(false) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /chrome-store-assets/readme.txt: -------------------------------------------------------------------------------- 1 | The Mouse is Lava (MIL) is an open source Chrome extension that adds keyboard shortcuts to navigate websites with your keyboard. The goal of the extension is to minimize the number of times you have to touch the mouse. Don't touch that mouse; it's lava. 2 | 3 | MIL includes both shortcuts that work on every website and shortcuts for specific, popular websites. Like Google. 4 | 5 | Quickstart shortcuts: 6 | ======================================================================== 7 | - [Ctrl+B] to focus the first, or next, and/or