├── utils ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── duck.png ├── github.png ├── logo.png ├── bug-icon.png ├── linkedin.png ├── copy-icon.png └── logo-store.png ├── .gitignore ├── manifest.json ├── LICENSE ├── popup.html ├── README.md ├── styles.css └── popup.js /utils/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/1.png -------------------------------------------------------------------------------- /utils/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/2.png -------------------------------------------------------------------------------- /utils/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/3.png -------------------------------------------------------------------------------- /utils/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/4.png -------------------------------------------------------------------------------- /utils/duck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/duck.png -------------------------------------------------------------------------------- /utils/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/github.png -------------------------------------------------------------------------------- /utils/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/logo.png -------------------------------------------------------------------------------- /utils/bug-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/bug-icon.png -------------------------------------------------------------------------------- /utils/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/linkedin.png -------------------------------------------------------------------------------- /utils/copy-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/copy-icon.png -------------------------------------------------------------------------------- /utils/logo-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/logo-store.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js dependencies 2 | node_modules/ 3 | .idea/ 4 | # Logs 5 | logs/ 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # OS generated files 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # IDE specific files 16 | .idea/ 17 | .vscode/ 18 | *.sublime-project 19 | *.sublime-workspace 20 | 21 | # Build output 22 | dist/ 23 | build/ 24 | 25 | # Chrome extension specific files 26 | *.pem -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "quack", 4 | "description": "Quickly copy-paste GitHub, LinkedIn, and portfolio links for job applications using Quack.", 5 | "version": "0.0.0.2", 6 | "icons": { 7 | "16": "utils/duck.png", 8 | "48": "utils/duck.png", 9 | "128": "utils/duck.png" 10 | }, 11 | "action": { 12 | "default_popup": "popup.html", 13 | "default_icon": "utils/duck.png" 14 | }, 15 | "permissions": [ 16 | "clipboardWrite", 17 | "storage" 18 | ] 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Khanh Do 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Quack 8 | 9 | 14 | 15 | 16 | 17 | 18 | Report Bug 19 | 20 |

Quack

21 |
22 | LinkedIn 23 | 24 | 25 | Copy 26 |
27 |
28 | GitHub 29 | 30 | 31 | Copy 32 |
33 |
34 | Portfolio 35 | 36 | 37 | Copy 38 |
39 | 40 |
41 | 42 | 43 | 44 |
45 |
46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quack 2 | 3 | Quickly copy-paste GitHub, LinkedIn, and portfolio links for job applications using Quack. 4 | 5 | ## Installation 6 | 7 | To install the extension, visit the [Chrome Web Store](https://chromewebstore.google.com/detail/quack/kbbkbaoiaeccjdbkcjngdfgphfeolcfj) and click "Add to Chrome". 8 | 9 | ![](utils/2.png) 10 | 11 | ## Usage 12 | 13 | ![](utils/3.png) 14 | ![](utils/4.png) 15 | 16 | ## Contributing 17 | 18 | We welcome contributions from the community! Here are some guidelines to help you get started: 19 | 20 | ### Reporting Bugs 21 | 22 | If you find a bug, please report it by opening an issue on the [GitHub repository](https://github.com/khanhdo05/quack/issues). Include as much detail as possible, such as steps to reproduce the bug, your operating system, and browser version. 23 | 24 | ### Feature Requests 25 | 26 | If you have an idea for a new feature, please open an issue on the [GitHub repository](https://github.com/khanhdo05/quack/issues) and describe your idea in detail. We appreciate all suggestions and feedback! 27 | 28 | ### Code Contributions 29 | 30 | 1. **Fork the repository**: Click the "Fork" button at the top right corner of the repository page. 31 | 2. **Clone your fork**: 32 | ```sh 33 | git clone git@github.com:khanhdo05/quack.git 34 | cd quack 35 | ``` 36 | 3. **Create a new branch**: 37 | ```sh 38 | git checkout -b feature-or-bugfix-name 39 | ``` 40 | 4. **Make your changes**: Implement your feature or bug fix. 41 | 5. **Commit your changes**: 42 | ```sh 43 | git add . 44 | git commit -m "Description of your changes" 45 | ``` 46 | 6. **Push to your fork**: 47 | ```sh 48 | git push origin feature-or-bugfix-name 49 | ``` 50 | 7. **Create a pull request**: Go to the original repository and click the "New pull request" button. Provide a detailed description of your changes. 51 | 52 | ### Code Style 53 | 54 | Please follow these guidelines to maintain a consistent code style: 55 | 56 | - Use 2 spaces for indentation. 57 | - Use camelCase for variable and function names. 58 | - Use descriptive names for variables and functions. 59 | - Add comments to explain complex logic. 60 | 61 | ### Testing 62 | 63 | Before submitting your changes, make sure to test your code thoroughly. You can try out your local version by following these steps: 64 | 65 | 1. Open Chrome and navigate to `chrome://extensions/`. 66 | 2. Enable "Developer mode" by toggling the switch in the top right corner. 67 | 3. Click on "Load unpacked" and select the repository folder. 68 | 69 | ## License 70 | 71 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 72 | 73 | ## Contact 74 | 75 | If you have any questions or need further assistance, feel free to open an issue on the [GitHub repository](https://github.com/khanhdo05/quack/issues). 76 | 77 | --- 78 | 79 | Thank you for contributing to Quack! -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, sans-serif; 3 | padding: 10px; 4 | background-color: #f7cc6f; 5 | position: relative; 6 | } 7 | .title { 8 | text-align: center; 9 | font-family: "Bagel Fat One", system-ui; 10 | font-weight: 400; 11 | font-style: normal; 12 | font-size: 9vw; 13 | } 14 | .input-container { 15 | display: flex; 16 | align-items: center; 17 | margin-bottom: 10px; 18 | width: 100%; /* Ensure full width */ 19 | } 20 | input { 21 | flex: 1; 22 | padding: 5px; 23 | border: 2px solid #000; 24 | border-radius: 5px; 25 | line-height: normal; 26 | color: #282828; 27 | box-sizing: border-box; 28 | } 29 | 30 | .icon { 31 | width: 20px; 32 | height: 20px; 33 | margin-right: 5px; 34 | border-radius: 50%; 35 | } 36 | .label{ 37 | min-width: 80px; 38 | max-width: 80px; /* Set a maximum width */ 39 | margin-right: 5px; 40 | margin-left: 5px; 41 | font-family: "Comfortaa", sans-serif; 42 | font-optical-sizing: auto; 43 | font-weight: 700; 44 | font-style: normal; 45 | white-space: nowrap; 46 | overflow: hidden; 47 | text-overflow: ellipsis; /* Add ellipsis for overflowing text */ 48 | } 49 | 50 | .custom-name { 51 | /* Reset input-specific styles */ 52 | outline: none; 53 | border: 0; 54 | background: none; 55 | padding: 2px; 56 | border-bottom: 1px solid #ccc; 57 | 58 | /* Label-like styling */ 59 | font-family: "Comfortaa", sans-serif; 60 | font-weight: 700; 61 | font-style: normal; 62 | color: #282828; 63 | white-space: nowrap; 64 | overflow: hidden; 65 | text-overflow: ellipsis; 66 | font-optical-sizing: auto; 67 | min-width: 80px; 68 | max-width: 80px; 69 | margin-right: 5px; 70 | margin-left: 5px; 71 | } 72 | 73 | .custom-link-url { 74 | flex: 1; 75 | padding: 5px; 76 | border: 2px solid #000; 77 | border-radius: 5px; 78 | line-height: normal; 79 | color: #282828; 80 | box-sizing: border-box; 81 | min-width: 0; /* Allow input to shrink */ 82 | } 83 | 84 | #settingsButton { 85 | margin-top: 10px; 86 | width: 100%; 87 | } 88 | 89 | @keyframes fadein { 90 | from {bottom: 0; opacity: 0;} 91 | to {bottom: 30px; opacity: 1;} 92 | } 93 | @keyframes fadeout { 94 | from {bottom: 30px; opacity: 1;} 95 | to {bottom: 0; opacity: 0;} 96 | } 97 | .short-input { 98 | width: 50px; 99 | margin-right: 5px; 100 | } 101 | .bug-report { 102 | position: absolute; 103 | top: 10px; 104 | right: 10px; 105 | text-decoration: none; 106 | } 107 | .bug-icon { 108 | width: 20px; 109 | height: 20px; 110 | } 111 | .button-35 { 112 | align-items: center; 113 | background-color: #fff; 114 | border-radius: 3px; 115 | box-shadow: transparent 0 0 0 3px,rgba(18, 18, 18, .1) 0 3px 6px; 116 | box-sizing: border-box; 117 | color: #121212; 118 | cursor: pointer; 119 | display: inline-flex; 120 | flex: 1 1 auto; 121 | font-family: Inter,sans-serif; 122 | font-size: 0.8rem; 123 | font-weight: 700; 124 | justify-content: center; 125 | line-height: 1; 126 | margin: 0; 127 | outline: none; 128 | padding: 0.2rem 0.5rem; 129 | text-align: center; 130 | text-decoration: none; 131 | transition: box-shadow .2s,-webkit-box-shadow .2s; 132 | white-space: nowrap; 133 | border: 0; 134 | user-select: none; 135 | -webkit-user-select: none; 136 | touch-action: manipulation; 137 | } 138 | 139 | .button-container .button-35 { 140 | margin: 0 5px; /* Add horizontal margin to buttons inside the container */ 141 | } 142 | 143 | .button-container .button-35:first-child { 144 | margin-left: 0; /* Remove left margin from the first button */ 145 | } 146 | 147 | .button-container .button-35:last-child { 148 | margin-right: 0; /* Remove right margin from the last button */ 149 | } 150 | 151 | .button-35:hover { 152 | box-shadow: #121212 0 0 0 2px, transparent 0 0 0 0; 153 | } 154 | 155 | .snackbar { 156 | font-family: "Poppins", sans-serif; 157 | font-weight: 500; 158 | font-style: normal; 159 | margin-top: 2px; 160 | } 161 | 162 | .custom-link { 163 | display: flex; 164 | align-items: center; 165 | margin-bottom: 10px; 166 | } 167 | 168 | .edit-icon, .button-35.delete-button, .button-35.plus-button, .copy-icon { 169 | flex: none; 170 | width: 20px; 171 | height: 20px; 172 | cursor: pointer; 173 | margin-left: 5px; 174 | } 175 | 176 | .button-35.save-button, .button-35.auto-save-enabled, .button-35.auto-save-disabled { 177 | height: 20px; 178 | flex: none; 179 | width: min-content; 180 | } 181 | 182 | .button-container { 183 | display: flex; 184 | justify-content: left; 185 | margin-top: 10px; 186 | } 187 | 188 | .auto-save-enabled { 189 | background-color: #4CAF50; 190 | color: white; 191 | } 192 | 193 | .auto-save-disabled { 194 | background-color: #f44336; 195 | color: white; 196 | } 197 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | const githubInput = document.getElementById('github'); 3 | const linkedinInput = document.getElementById('linkedin'); 4 | const portfolioInput = document.getElementById('portfolio'); 5 | const customLinksContainer = document.getElementById('custom-links'); 6 | const autoSaveButton = document.getElementById('autoSave'); 7 | const autoSaveDisabledString = 'auto-save-disabled'; 8 | const autoSaveEnabledString = 'auto-save-enabled'; 9 | 10 | // Check if auto-save is enabled 11 | function isAutoSaveEnabled() { 12 | return autoSaveButton.classList.contains(autoSaveEnabledString); 13 | } 14 | 15 | // Conditional saving 16 | function conditionalSave() { 17 | console.log("Auto-save condition checked."); 18 | if (isAutoSaveEnabled()) saveAllLinks(); 19 | } 20 | 21 | // Initialize auto-save functionality on inputs 22 | function initializeAutoSave(inputElement) { 23 | inputElement.addEventListener('input', () => { 24 | conditionalSave(); 25 | }); 26 | } 27 | 28 | function saveIfEnter(inputElement) { 29 | inputElement.addEventListener('keypress', (event) => { 30 | if (event.key === 'Enter') { 31 | event.preventDefault(); // Prevents any default form submission 32 | saveAllLinks(); 33 | showSnackbar('Saved!'); 34 | } 35 | }); 36 | } 37 | 38 | // Load saved links from Chrome storage 39 | chrome.storage.sync.get(['github', 'linkedin', 'portfolio', 'customLinks'], (result) => { 40 | if (result.github) githubInput.value = result.github; 41 | if (result.linkedin) linkedinInput.value = result.linkedin; 42 | if (result.portfolio) portfolioInput.value = result.portfolio; 43 | 44 | // Load custom links if available 45 | if (result.customLinks) { 46 | result.customLinks.forEach(link => { 47 | addCustomLink(link.name, link.url); 48 | }); 49 | } 50 | }); 51 | 52 | // Load auto-save preference 53 | chrome.storage.sync.get(['autoSave'], (result) => { 54 | const isAutoSaveEnabled = result.autoSave !== undefined ? result.autoSave : true; 55 | updateAutoSaveButton(isAutoSaveEnabled); 56 | }); 57 | 58 | autoSaveButton.addEventListener('click', toggleAutoSave); 59 | 60 | function toggleAutoSave() { 61 | const isCurrentlyEnabled = isAutoSaveEnabled(); 62 | if (isCurrentlyEnabled) { 63 | showSnackbar('Auto-save disabled'); 64 | } else { 65 | showSnackbar('Auto-save enabled'); 66 | } 67 | const newAutoSaveState = !isCurrentlyEnabled; 68 | chrome.storage.sync.set({ autoSave: newAutoSaveState }); 69 | updateAutoSaveButton(newAutoSaveState); 70 | } 71 | 72 | // Handle auto-save toggle 73 | function updateAutoSaveButton(isEnabled) { 74 | if (isEnabled) { 75 | autoSaveButton.classList.remove('auto-save-disabled'); 76 | autoSaveButton.classList.add('auto-save-enabled'); 77 | } else { 78 | autoSaveButton.classList.remove('auto-save-enabled'); 79 | autoSaveButton.classList.add('auto-save-disabled'); 80 | } 81 | } 82 | 83 | // Set up auto-save for main inputs 84 | [githubInput, linkedinInput, portfolioInput].forEach(initializeAutoSave); 85 | 86 | // Set up save if enter 87 | [githubInput, linkedinInput, portfolioInput].forEach(saveIfEnter); 88 | 89 | // Function to save all links 90 | function saveAllLinks() { 91 | console.log("Saving all links..."); 92 | 93 | const github = githubInput.value; 94 | const linkedin = linkedinInput.value; 95 | const portfolio = portfolioInput.value; 96 | 97 | const customLinks = Array.from(customLinksContainer.children).map(linkContainer => { 98 | const inputs = linkContainer.querySelectorAll('input'); 99 | return { 100 | name: inputs[0].value, // Link Name 101 | url: inputs[1].value // Link URL 102 | }; 103 | }); 104 | 105 | chrome.storage.sync.set({ github, linkedin, portfolio, customLinks }) 106 | .then(() => { 107 | console.log("Links saved successfully."); 108 | if (!isAutoSaveEnabled()) { 109 | showSnackbar('Saved!'); 110 | } 111 | }) 112 | .catch(error => { 113 | showSnackbar('Error saving links'); 114 | console.error("Error saving links:", error); 115 | }); 116 | } 117 | 118 | // Modify existing save button click event 119 | document.getElementById('save').addEventListener('click', () => { 120 | saveAllLinks() 121 | }); 122 | 123 | // Copy GitHub link 124 | document.getElementById('copyGithub').addEventListener('click', () => { 125 | copyToClipboard(githubInput.value); 126 | }); 127 | 128 | // Copy LinkedIn link 129 | document.getElementById('copyLinkedin').addEventListener('click', () => { 130 | copyToClipboard(linkedinInput.value); 131 | }); 132 | 133 | // Copy Portfolio link 134 | document.getElementById('copyPortfolio').addEventListener('click', () => { 135 | copyToClipboard(portfolioInput.value); 136 | }); 137 | 138 | function copyToClipboard(text) { 139 | navigator.clipboard.writeText(text).then(() => { 140 | showSnackbar('Copied to clipboard!'); 141 | }); 142 | } 143 | 144 | document.getElementById('addLink').addEventListener('click', () => { 145 | addCustomLink(); // Call with no arguments for new empty fields 146 | }); 147 | 148 | function showSnackbar(message) { 149 | const snackbar = document.getElementById('snackbar'); 150 | snackbar.textContent = message; 151 | snackbar.style.visibility = 'visible'; 152 | snackbar.style.animation = 'fadein 0.5s, fadeout 0.5s 2.5s'; 153 | setTimeout(() => { 154 | snackbar.style.visibility = 'hidden'; 155 | }, 3000); 156 | } 157 | 158 | /** 159 | * Fetches the favicon (website icon) for a specified URL. 160 | * 161 | * This function constructs a URL for the favicon using Google’s favicon service 162 | * and attempts to load it as an image. If successful, it returns the favicon URL; 163 | * if unsuccessful, it returns null and logs an error message. 164 | */ 165 | async function getFavicon(url) { 166 | try { 167 | // Construct the favicon URL using Google's favicon service 168 | const faviconUrl = `https://www.google.com/s2/favicons?domain=${url}`; 169 | 170 | const img = new Image(); 171 | img.src = faviconUrl; 172 | 173 | return new Promise((resolve, reject) => { 174 | img.onload = () => resolve(faviconUrl); // Resolve with the favicon URL 175 | img.onerror = () => { 176 | img.src = 'utils/duck.png'; // Default icon 177 | reject(new Error('Favicon not found')); 178 | } // Reject if not found 179 | }); 180 | } catch (error) { 181 | console.error('Error fetching favicon:', error); 182 | return null; 183 | } 184 | } 185 | 186 | /** 187 | * Adds a custom link with an associated icon, name, and URL input fields to the UI. 188 | * 189 | * This function creates a container for a new link entry, which includes: 190 | * - An icon representing the link (fetched from the provided URL if available) 191 | * - Input fields for the site name and URL 192 | * - A delete button to remove the link entry from the UI 193 | * - A copy icon for copying the URL to the clipboard 194 | */ 195 | async function addCustomLink(name = '', url = '') { 196 | const newLinkContainer = document.createElement('div'); 197 | newLinkContainer.className = 'input-container custom-link'; 198 | 199 | const newNameLabel = document.createElement('img'); 200 | newNameLabel.className = 'icon'; 201 | newNameLabel.src = 'utils/duck.png'; // Default icon 202 | newLinkContainer.appendChild(newNameLabel); 203 | 204 | const newNameInput = document.createElement('input'); 205 | newNameInput.className = 'custom-name'; 206 | newNameInput.type = 'text'; 207 | newNameInput.placeholder = 'site'; 208 | newNameInput.value = name; // Pre-fill if provided 209 | newLinkContainer.appendChild(newNameInput); 210 | 211 | const newLinkInput = document.createElement('input'); 212 | newLinkInput.type = 'text'; 213 | newLinkInput.value = url; 214 | newLinkInput.className = 'custom-link-url'; 215 | newLinkInput.placeholder = 'Enter URL'; 216 | newLinkContainer.appendChild(newLinkInput); 217 | 218 | // Create and add delete button 219 | const deleteButton = document.createElement('button'); 220 | deleteButton.textContent = 'x'; 221 | deleteButton.title = 'Delete field'; 222 | deleteButton.className = 'button-35 delete-button'; 223 | deleteButton.addEventListener('click', () => { 224 | newLinkContainer.remove(); // Remove the custom link container 225 | conditionalSave(); 226 | }); 227 | newLinkContainer.appendChild(deleteButton); 228 | 229 | const newCopyIcon = document.createElement('img'); 230 | newCopyIcon.src = 'utils/copy-icon.png'; 231 | newCopyIcon.className = 'copy-icon'; 232 | newCopyIcon.alt = 'Copy'; 233 | newCopyIcon.addEventListener('click', () => { 234 | copyToClipboard(newLinkInput.value); // Copy the link URL to the clipboard 235 | }); 236 | newLinkContainer.appendChild(newCopyIcon); 237 | 238 | customLinksContainer.appendChild(newLinkContainer); 239 | 240 | // Attach auto-save to each custom input 241 | initializeAutoSave(newNameInput); 242 | saveIfEnter(newNameInput); 243 | initializeAutoSave(newLinkInput); 244 | saveIfEnter(newLinkInput); 245 | 246 | conditionalSave(); 247 | 248 | // Fetch favicon asynchronously and update the icon if found 249 | // if (url) { 250 | // updateFavicon(newNameLabel, url); 251 | // } 252 | } 253 | 254 | /** 255 | * Updates the favicon for a specified icon element using the URL provided. 256 | */ 257 | async function updateFavicon(iconElement, url) { 258 | try { 259 | const favicon = await getFavicon(new URL(url).hostname); 260 | if (favicon) { 261 | iconElement.src = favicon; 262 | } 263 | } catch (error) { 264 | console.error('Error fetching favicon:', error); 265 | } 266 | } 267 | 268 | // Add event listener for window unload 269 | window.addEventListener('unload', () => { 270 | conditionalSave(); 271 | }); 272 | }); 273 | --------------------------------------------------------------------------------