├── .github └── workflows │ └── static.yml ├── .gitignore ├── Chrome_Extension ├── README.md ├── background.js ├── content-script.js ├── icon.png └── manifest.json ├── LICENSE ├── README.md ├── icon.png ├── index.html ├── mainonly.gif └── mainonly.js /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v4 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | # Upload entire repository 40 | path: '.' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | jsconfig.json -------------------------------------------------------------------------------- /Chrome_Extension/README.md: -------------------------------------------------------------------------------- 1 | ## chrome 拓展 2 | 3 | 通过拓展图标触发或右键菜单触发 -------------------------------------------------------------------------------- /Chrome_Extension/background.js: -------------------------------------------------------------------------------- 1 | 2 | chrome.contextMenus.create({ 3 | id: 'mainonly-contextMenu', 4 | title: 'mainonly', 5 | }); 6 | chrome.contextMenus.onClicked.addListener(function (info, tab) { 7 | 8 | sendMessageToContentScript('mainonly', (response) => { 9 | }); 10 | }); 11 | 12 | chrome.action.onClicked.addListener((tab) => { 13 | sendMessageToContentScript('mainonly', (response) => {}); 14 | }); 15 | 16 | 17 | function getCurrentTabId(callback) { 18 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 19 | if (callback) callback(tabs.length ? tabs[0].id : null); 20 | }); 21 | } 22 | 23 | function sendMessageToContentScript(message, callback) { 24 | getCurrentTabId((tabId) => { 25 | chrome.tabs.sendMessage(tabId, message, function (response) { 26 | if (callback) callback(response); 27 | }); 28 | }); 29 | } 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Chrome_Extension/content-script.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){ 2 | if(request === "mainonly"){ 3 | mainonly() 4 | } 5 | return true 6 | }); 7 | 8 | 9 | function mainonly() { 10 | // if re-run on the same page, remove the previous instance 11 | if (document.getElementById("mainonly")) { 12 | document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })); 13 | } 14 | 15 | var selectedElement = document.body; 16 | var lastStrategy = null; // which strategy is used to select the element 17 | 18 | // strategy overview 19 | // 1. if the selected element doesn't has `id`, then use `id` 20 | // (since it fixed the issue of pure text nodes can not be styled with CSS) 21 | // 2. otherwise fallback to use `class` 22 | if (!selectedElement.id) { 23 | // id 24 | lastStrategy = 'id'; 25 | selectedElement.id = "mainonly"; 26 | } else { 27 | // class 28 | lastStrategy = 'class'; 29 | selectedElement.classList.add("mainonly"); 30 | } 31 | 32 | const style = document.head.appendChild(document.createElement("style")); 33 | style.textContent = "#mainonly { outline: 2px solid red; } .mainonly { outline: 2px solid red; }"; 34 | 35 | // selection guide overlay 36 | const guideTextCn = '正在选择元素。按 Esc 键取消选择。向下滚动,或按下 =/. 键缩小选区。向上滚动,或按下 -/, 键扩大选区。' 37 | const guideTextEn = 'Selecting element. Press Esc to cancel selection. Scroll down, or press =/. to shrink the selection. Scroll up, or press -/,, to expand the selection.' 38 | const guide = document.body.appendChild(document.createElement("div")); 39 | guide.className = "mainonly-guide"; 40 | guide.innerHTML = `

${guideTextCn}

${guideTextEn}

`; 41 | const guideStyle = document.head.appendChild(document.createElement("style")); 42 | guideStyle.textContent = ` 43 | .mainonly-guide { 44 | position: fixed; 45 | top: 0; 46 | left: 50%; /* center the box horizontally */ 47 | transform: translate(-50%, 0); /* center the box horizontally */ 48 | padding: 0.5rem; 49 | font-size: 1rem; 50 | font-family: sans-serif; 51 | text-align: center; 52 | color: white; 53 | background-color: rgba(0, 0, 0, 0.5); 54 | border-radius: 0.5em; 55 | z-index: 999999999; 56 | kbd { 57 | display: inline-block; 58 | padding: 0.1em 0.3em; 59 | font-size: 0.8em; 60 | line-height: 1; 61 | color: #24292e; 62 | vertical-align: middle; 63 | background-color: #fafbfc; 64 | border: 1px solid #d1d5da; 65 | border-radius: 3px; 66 | box-shadow: inset 0 -1px 0 #d1d5da; 67 | } 68 | }`; 69 | 70 | /** @param {*} element */ 71 | function outlineElement(element) { 72 | if (element instanceof HTMLElement) { // Ignores non-HTMLElements 73 | // deselect previous element 74 | if (lastStrategy === 'id') { 75 | // id 76 | selectedElement.removeAttribute("id"); 77 | } else { 78 | // class 79 | selectedElement.classList.remove("mainonly"); 80 | } 81 | 82 | // select the new selected element 83 | selectedElement = element; 84 | 85 | if (!selectedElement.id) { 86 | // id 87 | lastStrategy = 'id'; 88 | selectedElement.id = "mainonly"; 89 | } else { 90 | // class 91 | lastStrategy = 'class'; 92 | selectedElement.classList.add("mainonly"); 93 | } 94 | } 95 | } 96 | 97 | /** @param {MouseEvent} event */ 98 | function onMouseOver(event) { 99 | outlineElement(event.target); 100 | } 101 | 102 | /** @param {MouseEvent} event */ 103 | function onClick(event) { 104 | event.preventDefault(); 105 | markParents(); 106 | if (lastStrategy === 'id') { 107 | // id 108 | style.textContent = `* { visibility: hidden; } #mainonly, #mainonly *, .mainonly_parents { visibility: visible; }`; 109 | } else { 110 | // class 111 | style.textContent = `* { visibility: hidden; } .mainonly, .mainonly *, .mainonly_parents { visibility: visible; }`; 112 | } 113 | cleanupEventListeners(); 114 | hideGuideOverlay(); 115 | } 116 | 117 | function hideGuideOverlay() { 118 | guide.remove(); 119 | guideStyle.remove(); 120 | } 121 | 122 | function markParents() { 123 | var parents = selectedElement; 124 | while (parents.parentElement) { 125 | parents = parents.parentElement; 126 | parents.classList.add("mainonly_parents"); 127 | } 128 | } 129 | 130 | function removeParents() { 131 | var parents = document.querySelectorAll(".mainonly_parents"); 132 | for (var i = 0; i < parents.length; i++) { 133 | parents[i].classList.remove("mainonly_parents"); 134 | } 135 | } 136 | 137 | /** @param {KeyboardEvent} event */ 138 | function onKeydown(event) { 139 | if (event.key === "Escape") { 140 | // Recover the hidden elements 141 | style.remove(); 142 | document.removeEventListener("keydown", onKeydown); 143 | cleanupEventListeners(); 144 | hideGuideOverlay(); 145 | // Restore the selected element to its original state 146 | if (lastStrategy === 'id') { 147 | // id 148 | selectedElement.removeAttribute("id"); 149 | } else { 150 | // class 151 | selectedElement.classList.remove("mainonly"); 152 | } 153 | removeParents(); 154 | } else if (event.key === ',' || event.key === '-') { 155 | // up, select parent element 156 | outlineElement(selectedElement.parentElement); 157 | } else if (event.key === '.' || event.key === '=') { 158 | // down, select first child element 159 | var childElement = selectedElement.querySelector(":hover"); 160 | if (childElement) { 161 | outlineElement(childElement); 162 | } 163 | } 164 | } 165 | 166 | /** @param {WheelEvent} event */ 167 | function onWheel(event) { 168 | event.preventDefault(); 169 | if (event.deltaY < 0) { 170 | // Scrolling up, select parent element 171 | outlineElement(selectedElement.parentElement); 172 | } else { 173 | // Scrolling down, select child element containing the cursor 174 | var childElement = selectedElement.querySelector(":hover"); 175 | if (childElement) { 176 | outlineElement(childElement); 177 | } 178 | } 179 | } 180 | 181 | function cleanupEventListeners() { 182 | document.removeEventListener("mouseover", onMouseOver); 183 | document.removeEventListener("click", onClick); 184 | document.removeEventListener("wheel", onWheel); 185 | } 186 | 187 | document.addEventListener("mouseover", onMouseOver); 188 | document.addEventListener("click", onClick); 189 | document.addEventListener("wheel", onWheel, { passive: false }); 190 | document.addEventListener("keydown", onKeydown); 191 | } 192 | -------------------------------------------------------------------------------- /Chrome_Extension/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerrylususu/mainonly/1b6030ed67b3f9a17f5317ba25001084fa2e8db3/Chrome_Extension/icon.png -------------------------------------------------------------------------------- /Chrome_Extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "mainonly", 4 | "version": "1.0", 5 | "description": "", 6 | "author": ".", 7 | "permissions": [ 8 | "activeTab", 9 | "contextMenus" 10 | ], 11 | "icons": { 12 | "32": "icon.png" 13 | }, 14 | "action": { 15 | "default_title": "mainonly" 16 | }, 17 | 18 | "background": 19 | { 20 | "service_worker": "background.js" 21 | }, 22 | "content_scripts": 23 | [ 24 | { 25 | "matches": [ "http://*/*", "https://*/*"], 26 | "js": ["content-script.js"], 27 | "run_at": "document_end" 28 | } 29 | ] 30 | 31 | 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 nekonull 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mainonly 2 | 3 | 4 | [Intro Page](https://nekonull.me/mainonly/) 5 | 6 | A JavaScript bookmarklet designed to isolate and highlight a specific element on a webpage, effectively hiding all other elements. 7 | 8 | (Created with GPT-4-turbo-1106 and refined through manual adjustments.) 9 | 10 | ![demo](mainonly.gif) 11 | 12 | ## How to Use 13 | 14 | 1. Copy the following code: 15 | 16 | ```JavaScript 17 | javascript:(function(){document.getElementById("mainonly")&&document.dispatchEvent(new KeyboardEvent("keydown",{key:"Escape"}));var e=document.body,n=null;e.id?(n="class",e.classList.add("mainonly")):(n="id",e.id="mainonly");let t=document.head.appendChild(document.createElement("style"));t.textContent="#mainonly { outline: 2px solid red; } .mainonly { outline: 2px solid red; }";let i=document.body.appendChild(document.createElement("div"));i.className="mainonly-guide",i.innerHTML=`

正在选择元素。按 Esc 键取消选择。向下滚动,或按下 =/. 键缩小选区。向上滚动,或按下 -/, 键扩大选区。

Selecting element. Press Esc to cancel selection. Scroll down, or press =/. to shrink the selection. Scroll up, or press -/,, to expand the selection.

`;let o=document.head.appendChild(document.createElement("style"));function l(t){t instanceof HTMLElement&&("id"===n?e.removeAttribute("id"):e.classList.remove("mainonly"),(e=t).id?(n="class",e.classList.add("mainonly")):(n="id",e.id="mainonly"))}function d(e){l(e.target)}function a(i){i.preventDefault(),function n(){for(var t=e;t.parentElement;)(t=t.parentElement).classList.add("mainonly_parents")}(),"id"===n?t.textContent="* { visibility: hidden; } #mainonly, #mainonly *, .mainonly_parents { visibility: visible; }":t.textContent="* { visibility: hidden; } .mainonly, .mainonly *, .mainonly_parents { visibility: visible; }",m(),r()}function r(){i.remove(),o.remove()}function s(i){if("Escape"===i.key)t.remove(),document.removeEventListener("keydown",s),m(),r(),"id"===n?e.removeAttribute("id"):e.classList.remove("mainonly"),function e(){for(var n=document.querySelectorAll(".mainonly_parents"),t=0;t