├── .github └── ISSUE_TEMPLATE │ └── problem-report.md ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── content-scripts │ ├── page-script.ts │ └── selectionchange.ts ├── icons │ ├── icon48.png │ └── icon96.png ├── libs │ ├── Sortable-license.txt │ ├── Sortable.js │ ├── iconv-lite-license.txt │ └── iconv-lite.js ├── manifest.json ├── res │ ├── msg-pages │ │ └── sss-intro.html │ └── sss-engine-icons │ │ ├── copy.png │ │ ├── open-link.png │ │ └── separator.png ├── search-variable-modifications.ts ├── settings │ ├── settings.html │ └── settings.ts ├── swift-selection-search.html └── swift-selection-search.ts ├── tests ├── shadow-dom-tests.html ├── tests.html ├── tests.ts └── visual tests.html └── tsconfig.json /.github/ISSUE_TEMPLATE/problem-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## ⚠ Development is on an indefinite break (Sorry!) ⚠ 11 | 12 | You can read more about this [here](https://github.com/CanisLupus/swift-selection-search/issues/230). 13 | 14 | - No feature suggestions! Sorry! 15 | - Please only report very problematic bugs. An issue with a single website is minor and probably due to specific formatting, JavaScript, or security policies in that website. 16 | 17 | ------------------- 18 | 19 | **Describe the problem** 20 | A clear description of what is happening and what should happen. 21 | 22 | **How to reproduce the problem** 23 | Steps to reproduce the behavior: 24 | 1. Go to '...' 25 | 2. Click on '....' 26 | 3. Scroll down to '....' 27 | 4. See error 28 | 29 | **Anything else you'd like to include** 30 | What you tried already, screenshots, attachments, examples, etc. 31 | 32 | **System info:** 33 | - Browser: [e.g. Firefox, Waterfox, etc] 34 | - Browser version: [e.g. 64] 35 | - System: [e.g. Windows 10, macOS] 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore these files 2 | Thumbs.db 3 | .vscode 4 | /node_modules 5 | *.js 6 | 7 | # but not these files 8 | !/src/libs/* 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Daniel Lobo 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Swift Selection Search 2 | ================= 3 | 4 | ------------------------------- 5 | 6 | ## ⚠ NOTE: Development is on an indefinite break (Sorry!) ⚠ 7 | 8 | - Please use the issues page _only_ to report very problematic bugs you may find. 9 | - This project is not accepting pull requests. 10 | - I do encourage you to work on your own version of SSS and publish it as an improved addon. Even for other browsers! 11 | 12 | You can read more about this [here](https://github.com/CanisLupus/swift-selection-search/issues/230). 13 | 14 | ------------------------------- 15 | 16 | ## Description 17 | 18 | Swift Selection Search (SSS) is a Firefox add-on for quickly searching for some text in a page using your favorite search engines. 19 | 20 | Select text on a page and a small popup box with search engines will appear above your cursor. Press one and you'll automatically search for the selected text using that engine! 21 | 22 | SSS is configurable. You can define which search engines appear on the popup, the appearance of the icons, what happens when you click them, where the popup appears, whether or not to hide it when the page scrolls, if you want to auto copy text on selection, and many more options. 23 | 24 | You also get an optional context menu for searching with any of your engines. You can disable this extra menu in the options if you want. You also have the option of disabling the popup itself and leaving only this context menu. Your choice. :) 25 | 26 | SSS is available at the Mozilla Add-ons website, here: 27 | 28 | https://addons.mozilla.org/firefox/addon/swift-selection-search 29 | 30 | ## How to build 31 | 32 | Firstly, since WebExtensions are made in JavaScript, we need to be able to convert TypeScript to JavaScript, preferably in a way that is as automatic as possible. 33 | 34 | 1. Install *npm*. Download *Node.js* from https://nodejs.org and you will get *npm* alongside it. 35 | 36 | 1. In the project's folder, run this in the command line: 37 | > npm install 38 | 39 | This will install the project's TypeScript dependencies locally in the folder *node_modules*. 40 | 41 | 1. To transpile all .ts scripts to .js (on Windows): 42 | > "node_modules/.bin/tsc.cmd" --watch -p tsconfig.json 43 | 44 | The command will stay alive and automatically re-transpile code after any changes to .ts files, which is useful while developing. 45 | 46 | Now you have .js files, so you can use SSS as a WebExtension. Yay! 47 | 48 | - For short development or just curiosity, the simpler way to try SSS is the [about:debugging#addons](about:debugging#addons) page in Firefox. Press the "Load Temporary Add-on" button, select a file in the Swift Selection Search "src" directory (for example *manifest.json*), and it will load the add-on until the browser is closed. Changes to most code require reloading the add-on again. 49 | 50 | - For more prolonged development and/or packaging, such as auto reloading after each change, please follow Mozilla's instructions [here](https://developer.mozilla.org/Add-ons/WebExtensions/Getting_started_with_web-ext) to install and use the *web-ext* tool. 51 | 52 | ## How to contribute code to SSS 53 | 54 | ### Preparation 55 | 56 | 1. Fork the repository on GitHub and clone it to your computer. 57 | 58 | 1. Follow the instructions above for **How to build**. 59 | 60 | In the end you should have a script that automatically transpiles code from TypeScript to JavaScript, and should know how to run your version of SSS on Firefox. 61 | 62 | 1. Learn that SSS has 3 main branches: 63 | - **master** - Final versions that are published to Mozilla Add-ons. "master" will usually point to a commit where the SSS version changes, since those are the ones published (commonly, there's a commit just to update the version). 64 | - **develop** - Where development happens and where new features will be merged to. Less stable, but shouldn't have incomplete features. 65 | - **addon-sdk** - Can be ignored, since it's the code for SSS before it was a WebExtension. Essentially an archive. 66 | 67 | 1. Read the code guidelines below. 68 | 69 | ### Code guidelines 70 | 71 | - Tabs for indentation, please! ;) (Spaces can be used to align things.) 72 | - No whitespace at the end of lines. Settings like VSCode's "files.trimTrailingWhitespace" help with this. 73 | - Try to follow the style you see in the existing code. If you think it's inconsistent in some places, feel free to do your own thing. It will be reviewed anyway. ;) 74 | - SSS currently supports Firefox 63 onward, which means that you must take extra care when using an WebExtensions API. Make sure it exists in Firefox 63, or at least that the code doesn't run and crash on older versions. Ideally, install Firefox 63 separately on your computer (having a different Firefox profile for that helps greatly) and disable its auto updates. 75 | 76 | ### Collaboration 77 | 78 | 1. Find an issue you'd like to solve in the issues list. 79 | 80 | 1. Ask if anyone is working on it already. Additionally, if it's missing details on how to implement or if you want to know of possible approaches, feel free to ask! 81 | 82 | 1. Create a new Git branch for the new feature or bug fix you are trying to implement. Create this branch from "develop", to simplify the merge later on (since "master" may not have all current changes). 83 | 84 | 1. Implement and commit/push the changes to the branch, possibly in multiple commits. 85 | 86 | 1. Finally create a pull request to merge your changes to the original repository. It will be subject to code review, discussion and possibly changes if needed. 87 | 88 | Thanks! 89 | 90 | ## TypeScript considerations and warnings 91 | 92 | SSS was ported at some point from JavaScript to TypeScript to get the benefits of type annotations, since many errors can be caught by the type checker. However, it should be noted that the interaction of WebExtensions with TypeScript is finicky at best, since TypeScript assumes that scripts can import code/data from other scripts, when in reality the WebExtensions environment has sandboxing and rarely allows that. 93 | 94 | Content scripts don't see code from the background script, or vice-versa, and the only way multiple content scripts can even see each other's code is if all of them are injected on the same page by the background script, similarly to how they would if they were included in an HTML page as scripts. TypeScript has no idea that this is how WebExtensions work, so just because something transpiles to JavaScript correctly it doesn't mean the WebExtension will work. 95 | 96 | An example that works is that we can declare types/classes in the background script and reference them in other scripts, since only the type checker cares about them. We get type checking and it's fine. 97 | 98 | However, if we try adding to those classes some methods, variable assignments, etc, then content scripts or options page scripts won't see these. When transpiling to JavaScript, they will be left as a reference to code in another script, which can't be referenced at runtime due to WebExtensions sandboxing. This is also true of enums (which have concrete values, so "data") UNLESS we declare them as `const enum`, in which case TypeScript copies their values to where they are used instead of creating JavaScript objects. 99 | 100 | So, if you see a class fully or partially declared in more than one script in this project (for example in both background script and page script), the above is why. 101 | 102 | Another thing that may fail is using `instanceof` to check if an object is of a certain class. Don't do this for custom created classes since it may fail due to the sandboxing. 103 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swift-selection-search", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/firefox-webext-browser": { 8 | "version": "82.0.0", 9 | "resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-82.0.0.tgz", 10 | "integrity": "sha512-zKHePkjMx42KIUUZCPcUiyu1tpfQXH9VR4iDYfns3HvmKVJzt/TAFT+DFVroos8BI9RH78YgF3Hi/wlC6R6cKA==" 11 | }, 12 | "typescript": { 13 | "version": "4.1.2", 14 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.2.tgz", 15 | "integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swift-selection-search", 3 | "version": "1.0.0", 4 | "description": "Swift Selection Search (SSS) is a simple Firefox add-on that lets you quickly search for some text in a page using your favorite search engines.", 5 | "main": "swift-selection-search.js", 6 | "homepage": "https://github.com/CanisLupus/swift-selection-search", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "Daniel Lobo", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@types/firefox-webext-browser": "^82.0.0", 14 | "typescript": "^4.1.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/content-scripts/page-script.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Content script that gets injected into all webpages (actually all frames) for the SSS popup to work. 4 | This script is initialized by the background script and, when needed, requests all the required info 5 | to show the popup. It also listens for text selections and applies formatting to the popup, based 6 | on the user's settings. 7 | 8 | */ 9 | 10 | var DEBUG_STATE: boolean; // avoid TS compilation errors but still get working JS code 11 | 12 | namespace ContentScript 13 | { 14 | export class SelectionData 15 | { 16 | isInInputField: boolean; // doesn't include "content editable" fields (use isInContentEditableField() for that) 17 | unprocessedText: string; 18 | text: string; 19 | element: HTMLElement; 20 | selection: Selection; 21 | } 22 | 23 | // TODO: move this enum to the background script and add all possible messages 24 | const enum MessageType { 25 | EngineClick = "engineClick", 26 | Log = "log", 27 | GetActivationSettings = "getActivationSettings", 28 | GetPopupSettings = "getPopupSettings", 29 | } 30 | 31 | abstract class Message 32 | { 33 | constructor(public type: MessageType) { this.type = type; } 34 | } 35 | 36 | class EngineClickMessage extends Message 37 | { 38 | selection: string; 39 | engine: SSS.SearchEngine; 40 | href: string; 41 | openingBehaviour: SSS.OpenResultBehaviour; 42 | 43 | constructor() { super(MessageType.EngineClick); } 44 | } 45 | 46 | class LogMessage extends Message 47 | { 48 | constructor(public log: any) { super(MessageType.Log); } 49 | } 50 | 51 | class GetPopupSettingsMessage extends Message 52 | { 53 | constructor() { super(MessageType.GetPopupSettings); } 54 | } 55 | 56 | const DEBUG = typeof DEBUG_STATE !== "undefined" && DEBUG_STATE === true; 57 | 58 | if (DEBUG) { 59 | // To have all log messages in the same console, we always request the background script to log. 60 | // Otherwise content script messages will be in the Web Console instead of the Dev Tools Console. 61 | var log = msg => browser.runtime.sendMessage(new LogMessage(msg)); 62 | } 63 | 64 | // Globals 65 | let popup: PopupCreator.SSSPopup = null; 66 | const selection: SelectionData = new SelectionData(); 67 | let mousePositionX: number = 0; 68 | let mousePositionY: number = 0; 69 | let canMiddleClickEngine: boolean = true; 70 | let activationSettings: SSS.ActivationSettings = null; 71 | let settings: SSS.Settings = null; 72 | let sssIcons: { [id: string] : SSS.SSSIconDefinition; } = null; 73 | let popupShowTimeout: number = null; 74 | 75 | setTimeout(() => PopupCreator.onSearchEngineClick = onSearchEngineClick, 0); 76 | 77 | // be prepared for messages from background script 78 | browser.runtime.onMessage.addListener(onMessageReceived); 79 | 80 | if (DEBUG) { log("content script has started!"); } 81 | 82 | // act when the background script requests something from this script 83 | function onMessageReceived(msg, sender, callbackFunc) 84 | { 85 | switch (msg.type) 86 | { 87 | case "isAlive": 88 | callbackFunc(true); // simply return true to say "I'm alive!" 89 | break; 90 | 91 | case "activate": 92 | // if this is not the first activation, reset everything first 93 | if (activationSettings !== null) { 94 | deactivate(); 95 | } 96 | 97 | // refuse activation on non-HTML documents like XML 98 | if (document.documentElement.nodeName !== "HTML") { 99 | break; 100 | } 101 | 102 | activate(msg.activationSettings, msg.isPageBlocked); // background script passes a few settings needed for setup 103 | break; 104 | 105 | case "showPopup": 106 | if (saveCurrentSelection()) { 107 | showPopupForSelection(null, true); 108 | } 109 | break; 110 | 111 | case "copyToClipboardAsHtml": 112 | copyToClipboardAsHtml(); 113 | break; 114 | 115 | case "copyToClipboardAsPlainText": 116 | copyToClipboardAsPlainText(); 117 | break; 118 | 119 | default: break; 120 | } 121 | } 122 | 123 | function copyToClipboardAsHtml() 124 | { 125 | document.execCommand("copy"); 126 | } 127 | 128 | function copyToClipboardAsPlainText() 129 | { 130 | document.addEventListener("copy", copyToClipboardAsPlainText_Listener); 131 | document.execCommand("copy"); 132 | document.removeEventListener("copy", copyToClipboardAsPlainText_Listener); 133 | } 134 | 135 | function copyToClipboardAsPlainText_Listener(e: ClipboardEvent) 136 | { 137 | if (saveCurrentSelection()) { 138 | e.clipboardData.setData("text/plain", selection.unprocessedText); 139 | e.preventDefault(); 140 | } 141 | } 142 | 143 | // default error handler for promises 144 | function getErrorHandler(text: string): (reason: any) => void 145 | { 146 | if (DEBUG) { 147 | return error => { log(`${text} (${error})`); }; 148 | } else { 149 | return undefined; 150 | } 151 | } 152 | 153 | function activate(_activationSettings: SSS.ActivationSettings, isPageBlocked: boolean) 154 | { 155 | activationSettings = _activationSettings; 156 | 157 | // now register with events based on user settings 158 | 159 | if (activationSettings.popupLocation === SSS.PopupLocation.Cursor) { 160 | document.addEventListener("mousemove", onMouseUpdate); 161 | document.addEventListener("mouseenter", onMouseUpdate); 162 | } 163 | 164 | if (activationSettings.useEngineShortcutWithoutPopup) { 165 | document.documentElement.addEventListener("keydown", onKeyDown); 166 | } 167 | 168 | if (!isPageBlocked) 169 | { 170 | if (activationSettings.popupOpenBehaviour === SSS.PopupOpenBehaviour.Auto || activationSettings.popupOpenBehaviour === SSS.PopupOpenBehaviour.HoldAlt) { 171 | selectionchange.start(); 172 | document.addEventListener("customselectionchange", onSelectionChange); 173 | } 174 | else if (activationSettings.popupOpenBehaviour === SSS.PopupOpenBehaviour.MiddleMouse) { 175 | document.addEventListener("mousedown", onMouseDown); 176 | document.addEventListener("mouseup", onMouseUp); 177 | } 178 | } 179 | 180 | if (DEBUG) { log("content script activated, url: " + window.location.href.substr(0, 40)); } 181 | } 182 | 183 | function deactivate() 184 | { 185 | // unregister with all events 186 | 187 | document.removeEventListener("mousemove", onMouseUpdate); 188 | document.removeEventListener("mouseenter", onMouseUpdate); 189 | document.removeEventListener("mousedown", onMouseDown); 190 | document.removeEventListener("mouseup", onMouseUp); 191 | 192 | document.removeEventListener("customselectionchange", onSelectionChange); 193 | selectionchange.stop(); 194 | 195 | document.documentElement.removeEventListener("keydown", onKeyDown); 196 | document.documentElement.removeEventListener("mousedown", maybeHidePopup); 197 | 198 | window.removeEventListener("scroll", maybeHidePopup); 199 | 200 | // remove the popup from the page (other listeners in the popup are destroyed along with their objects) 201 | if (popup !== null) { 202 | document.documentElement.removeChild(popup); 203 | popup = null; 204 | } 205 | 206 | // also clear any previously saved settings 207 | activationSettings = null; 208 | settings = null; 209 | 210 | if (DEBUG) { log("content script deactivated"); } 211 | } 212 | 213 | function onSelectionChange(ev: selectionchange.CustomSelectionChangeEvent) 214 | { 215 | if (popup && popup.isReceiverOfEvent(ev.event)) return; 216 | 217 | if (activationSettings.popupOpenBehaviour === SSS.PopupOpenBehaviour.Auto && activationSettings.popupDelay > 0) 218 | { 219 | clearPopupShowTimeout(); 220 | 221 | if (saveCurrentSelection()) { 222 | popupShowTimeout = window.setTimeout(() => showPopupForSelection(ev, false), activationSettings.popupDelay); // use "window.setTimeout" to avoid problems with TS type definitions 223 | } 224 | } 225 | else 226 | { 227 | if (saveCurrentSelection()) { 228 | showPopupForSelection(ev, false); 229 | } 230 | } 231 | } 232 | 233 | // called whenever selection changes or when we want to force the popup to appear for the current selection 234 | function showPopupForSelection(ev: Event, isForced: boolean) 235 | { 236 | clearPopupShowTimeout(); 237 | 238 | if (settings !== null) { 239 | // if we have settings already, use them... 240 | tryShowPopup(ev, isForced); 241 | } else { 242 | // ...otherwise ask the background script for all needed settings, store them, and THEN try to show the popup 243 | acquireSettings(() => tryShowPopup(ev, isForced)); 244 | } 245 | } 246 | 247 | function acquireSettings(onSettingsAcquired: () => void) 248 | { 249 | browser.runtime.sendMessage(new GetPopupSettingsMessage()).then( 250 | popupSettings => { 251 | settings = popupSettings.settings; 252 | sssIcons = popupSettings.sssIcons; 253 | onSettingsAcquired(); 254 | }, 255 | getErrorHandler("Error sending getPopupSettings message from content script.") 256 | ); 257 | } 258 | 259 | function clearPopupShowTimeout() 260 | { 261 | if (popupShowTimeout !== null) { 262 | clearTimeout(popupShowTimeout); 263 | popupShowTimeout = null; 264 | } 265 | } 266 | 267 | function saveCurrentSelection() 268 | { 269 | const elem: Element = document.activeElement; 270 | 271 | if (elem instanceof HTMLTextAreaElement || (elem instanceof HTMLInputElement && elem.type !== "password")) 272 | { 273 | selection.isInInputField = true; 274 | 275 | // for editable fields, getting the selected text is different 276 | selection.unprocessedText = (elem as HTMLTextAreaElement).value.substring(elem.selectionStart, elem.selectionEnd); 277 | selection.element = elem; 278 | } 279 | else 280 | { 281 | selection.isInInputField = false; 282 | 283 | // get selection, but exit if there's no text selected after all 284 | const selectionObject = window.getSelection(); 285 | if (selectionObject === null) return false; 286 | 287 | let selectedText = selectionObject.toString(); 288 | 289 | // if selection.toString() is empty, try to get string from the ranges instead (this can happen!) 290 | if (selectedText.length === 0) 291 | { 292 | selectedText = ""; 293 | for (let i = 0; i < selectionObject.rangeCount; i++) { 294 | selectedText += selectionObject.getRangeAt(i).toString(); 295 | } 296 | } 297 | 298 | selection.unprocessedText = selectedText; 299 | selection.selection = selectionObject; 300 | } 301 | 302 | selection.unprocessedText = selection.unprocessedText.trim(); 303 | 304 | let text = selection.unprocessedText; 305 | text = text.replace(/[\r\n]+/g, " "); // replace newlines with spaces 306 | text = text.replace(/\s\s+/g, " "); // replace consecutive whitespaces 307 | selection.text = text; 308 | 309 | return selection.text.length > 0; 310 | } 311 | 312 | // shows the popup if the conditions are proper, according to settings 313 | function tryShowPopup(ev: Event, isForced: boolean) 314 | { 315 | // [TypeScript]: Usually we would check for the altKey only if "ev instanceof selectionchange.CustomSelectionChangeEvent", 316 | // but ev has an undefined class type in pages outside the options page, so it doesn't match. We use ev["altKey"]. 317 | if (settings.popupOpenBehaviour === SSS.PopupOpenBehaviour.HoldAlt && !ev["altKey"]) return; 318 | 319 | if (settings.popupOpenBehaviour === SSS.PopupOpenBehaviour.Auto) 320 | { 321 | if ((settings.minSelectedCharacters > 0 && selection.text.length < settings.minSelectedCharacters) 322 | || (settings.maxSelectedCharacters > 0 && selection.text.length > settings.maxSelectedCharacters)) 323 | { 324 | return; 325 | } 326 | } 327 | 328 | if (!isForced) 329 | { 330 | // If showing popup for editable fields is not allowed, check if selection is in an editable field. 331 | if (!settings.allowPopupOnEditableFields) 332 | { 333 | if (selection.isInInputField) return; 334 | // even if this is not an input field, don't show popup in contentEditable elements, such as Gmail's compose window 335 | if (isInContentEditableField(selection.selection.anchorNode)) return; 336 | } 337 | // If editable fields are allowed, they are still not allowed for keyboard selections 338 | else 339 | { 340 | if (!ev["isMouse"] && isInContentEditableField(selection.selection.anchorNode)) return; 341 | } 342 | } 343 | 344 | if (settings.autoCopyToClipboard === SSS.AutoCopyToClipboard.Always 345 | || (settings.autoCopyToClipboard === SSS.AutoCopyToClipboard.NonEditableOnly 346 | && !selection.isInInputField 347 | && !isInContentEditableField(selection.selection.anchorNode))) 348 | { 349 | if (DEBUG) { log("auto copied to clipboard: " + selection.text); } 350 | document.execCommand("copy"); 351 | } 352 | 353 | if (DEBUG) { log("showing popup, previous value was: " + popup); } 354 | 355 | if (popup === null) { 356 | popup = createPopup(settings); 357 | } 358 | 359 | if (settings.showSelectionTextField === true) 360 | { 361 | popup.setInputFieldText(selection.text); 362 | 363 | if (isForced) { // if forced by keyboard, focus the text field 364 | setTimeout(() => popup.setFocusOnInputFieldText(), 0); 365 | } 366 | } 367 | 368 | popup.show(); // call "show" first so that popup size calculations are correct in setPopupPosition 369 | popup.setPopupPosition(settings, selection, mousePositionX, mousePositionY); 370 | 371 | if (settings.popupAnimationDuration > 0) { 372 | popup.playAnimation(settings); 373 | } 374 | } 375 | 376 | function createPopup(settings: SSS.Settings): PopupCreator.SSSPopup 377 | { 378 | // only define new element if not already defined (can get here multiple times if settings are reloaded) 379 | if (!customElements.get("sss-popup")) 380 | { 381 | // temp class that locks in settings as arguments to SSSPopup 382 | class SSSPopupWithSettings extends PopupCreator.SSSPopup { 383 | constructor() { super(getSettings(), getIcons()); } 384 | } 385 | 386 | customElements.define("sss-popup", SSSPopupWithSettings); 387 | } 388 | 389 | const popup = document.createElement("sss-popup") as PopupCreator.SSSPopup; 390 | 391 | document.documentElement.appendChild(popup); 392 | 393 | // register popup events 394 | if (!settings.useEngineShortcutWithoutPopup) { // if we didn't register the keydown listener before, do it now 395 | document.documentElement.addEventListener("keydown", onKeyDown); 396 | } 397 | document.documentElement.addEventListener("mousedown", maybeHidePopup); // hide popup from a press down anywhere... 398 | popup.addEventListener("mousedown", ev => ev.stopPropagation()); // ...except on the popup itself 399 | 400 | if (settings.hidePopupOnPageScroll) { 401 | window.addEventListener("scroll", maybeHidePopup); 402 | } 403 | 404 | return popup; 405 | } 406 | 407 | function getSettings(): SSS.Settings 408 | { 409 | return settings; 410 | } 411 | 412 | function getIcons(): { [id: string] : SSS.SSSIconDefinition; } 413 | { 414 | return sssIcons; 415 | } 416 | 417 | function isInContentEditableField(node): boolean 418 | { 419 | // to find if this element is editable, we go up the hierarchy until an element that specifies "isContentEditable" (or the root) 420 | for (let elem = node; elem !== document; elem = elem.parentNode) 421 | { 422 | const concreteElem = elem as HTMLElement; 423 | if (concreteElem.isContentEditable === undefined) { 424 | continue; // check parent for value 425 | } 426 | return concreteElem.isContentEditable; 427 | } 428 | 429 | return false; 430 | } 431 | 432 | function isAnyEditableFieldFocused() 433 | { 434 | const elem: Element = document.activeElement; 435 | return elem instanceof HTMLTextAreaElement || (elem instanceof HTMLInputElement && elem.type !== "password") || isInContentEditableField(elem); 436 | } 437 | 438 | function getEngineWithShortcut(key) 439 | { 440 | // Look through the search engines to find if one them has a shortcut that matches the pressed key 441 | key = key.toUpperCase(); 442 | return settings.searchEngines.find(e => e.shortcut === key); 443 | } 444 | 445 | function onKeyDown(ev) 446 | { 447 | // Check if the user pressed a shortcut. 448 | // The popup must be visible, unless 'Always enable shortcuts' is checked. 449 | 450 | const isPopupVisible = popup !== null && popup.isShown(); 451 | 452 | if (!ev.altKey && !ev.ctrlKey && !ev.metaKey && !ev.shiftKey // modifiers are not supported right now 453 | && !isAnyEditableFieldFocused()) // shortcuts are disabled in editable fields 454 | { 455 | if ((isPopupVisible && ev.originalTarget.className !== "sss-input-field") // if using popup, make sure we're not inside the popup's text field 456 | || (activationSettings.useEngineShortcutWithoutPopup && saveCurrentSelection())) // if outside popup, ensure a selection exists, otherwise we crash later 457 | { 458 | if (settings !== null) { 459 | searchWithEngineUsingShortcut(ev.key); 460 | } else { 461 | acquireSettings(() => searchWithEngineUsingShortcut(ev.key)); 462 | } 463 | } 464 | } 465 | 466 | // The code below should only work if the popup is showing. 467 | if (!isPopupVisible) return; 468 | 469 | // Loop the icons using "Tab" 470 | if (ev.key === "Tab") 471 | { 472 | const firstIcon = popup.enginesContainer.firstChild as HTMLImageElement; 473 | const lastIcon = popup.enginesContainer.lastChild as HTMLImageElement; 474 | 475 | // Focus the first icon for the first time, as well as after the last one so as to keep looping them. 476 | if (document.activeElement.nodeName !== "SSS-POPUP" || ev.originalTarget === lastIcon) { 477 | firstIcon.focus(); 478 | ev.preventDefault(); 479 | return; 480 | } 481 | } 482 | 483 | // if pressing the enter key 484 | if (ev.keyCode == 13 && popup.isReceiverOfEvent(ev)) 485 | { 486 | let engine; 487 | let openingBehaviour; 488 | 489 | // if we're inside the popup's text field, grab the first user-defined engine and search using that 490 | if (ev.originalTarget.nodeName === "INPUT") { 491 | engine = settings.searchEngines.find(e => e.type !== SSS.SearchEngineType.SSS); 492 | openingBehaviour = SSS.OpenResultBehaviour.NewBgTab; // for now, using enter is the same as ctrl-clicking 493 | } else { 494 | // if cycling the icons using "tab", grab the focused icon 495 | const engineIndex = [...popup.enginesContainer.children].indexOf(ev.originalTarget); 496 | engine = settings.searchEngines[engineIndex]; 497 | openingBehaviour = settings.shortcutBehaviour; 498 | } 499 | 500 | const message = createSearchMessage(engine, settings); 501 | message.openingBehaviour = openingBehaviour; 502 | browser.runtime.sendMessage(message); 503 | } 504 | else 505 | { 506 | maybeHidePopup(ev); 507 | } 508 | } 509 | 510 | function searchWithEngineUsingShortcut(key: string) 511 | { 512 | const engine = getEngineWithShortcut(key); 513 | if (engine) { 514 | const message = createSearchMessage(engine, settings); 515 | message.openingBehaviour = settings.shortcutBehaviour; 516 | browser.runtime.sendMessage(message); 517 | } 518 | } 519 | 520 | function maybeHidePopup(ev?) 521 | { 522 | if (popup === null) return; 523 | 524 | if (ev) 525 | { 526 | if (ev.type === "keydown") 527 | { 528 | // these keys shouldn't hide the popup 529 | if (ev.keyCode == 16) return; // shift 530 | if (ev.keyCode == 17) return; // ctrl 531 | if (ev.keyCode == 18) return; // alt 532 | if (ev.keyCode == 224) return; // mac cmd (224 only on Firefox) 533 | 534 | if (ev.keyCode == 27) { // escape forces hide 535 | popup.hide(); 536 | return; 537 | } 538 | 539 | // if event is a keydown on the text field, don't hide 540 | if (popup.isReceiverOfEvent(ev)) return; 541 | 542 | // Pressing a shortcut key should be treated as a click to an icon. 543 | // So we should only hide the popup when the user presses a shortcut key if 544 | // hidePopupOnSearch is true. 545 | if (!settings.hidePopupOnSearch && getEngineWithShortcut(ev.key)) return; 546 | } 547 | 548 | // if we pressed with right mouse button and that isn't supposed to hide the popup, don't hide 549 | if (ev.button === 2 && settings && settings.hidePopupOnRightClick === false) return; 550 | } 551 | 552 | popup.hide(); 553 | } 554 | 555 | function onMouseUpdate(ev: MouseEvent) 556 | { 557 | mousePositionX = ev.pageX; 558 | mousePositionY = ev.pageY; 559 | } 560 | 561 | function onSearchEngineClick(ev: MouseEvent, engine: SSS.SearchEngine, settings: SSS.Settings) 562 | { 563 | // if using middle mouse and can't, early out so we don't hide popup 564 | if (ev.button === 1 && !canMiddleClickEngine) return; 565 | 566 | if (settings.hidePopupOnSearch) { 567 | // If we hide the popup *immediately* and this is a right click, 568 | // it will consequently be detected on whatever's behind the popup 569 | // and call the context menu, which we don't want. 570 | setTimeout(() => maybeHidePopup(), 0); 571 | } 572 | 573 | if (ev.button === 0 || ev.button === 1 || ev.button === 2) 574 | { 575 | const message: EngineClickMessage = createSearchMessage(engine, settings); 576 | 577 | if (ev[selectionchange.modifierKey]) { 578 | message.openingBehaviour = SSS.OpenResultBehaviour.NewBgTab; 579 | } else if (ev.button === 0) { 580 | message.openingBehaviour = settings.mouseLeftButtonBehaviour; 581 | } else if (ev.button === 1) { 582 | message.openingBehaviour = settings.mouseMiddleButtonBehaviour; 583 | } else { 584 | message.openingBehaviour = settings.mouseRightButtonBehaviour; 585 | } 586 | 587 | browser.runtime.sendMessage(message); 588 | } 589 | } 590 | 591 | function createSearchMessage(engine: SSS.SearchEngine, settings: SSS.Settings): EngineClickMessage 592 | { 593 | const message = new EngineClickMessage(); 594 | // Due to shortcuts, we can search without a popup, so we only get the input field contents if popup is not null. 595 | message.selection = popup !== null && settings.showSelectionTextField === true ? popup.getInputFieldText() : selection.text; 596 | message.engine = engine; 597 | 598 | if (window.location) { 599 | message.href = window.location.href; 600 | } 601 | 602 | return message; 603 | } 604 | 605 | function onMouseDown(ev: MouseEvent) 606 | { 607 | if (ev.button !== 1) return; 608 | 609 | const selection: Selection = window.getSelection(); 610 | 611 | // for selections inside editable elements 612 | const elem: Element = document.activeElement; 613 | 614 | if (elem instanceof HTMLTextAreaElement || (elem instanceof HTMLInputElement && elem.type !== "password")) { 615 | if (forceSelectionIfWithinRect(ev, elem.getBoundingClientRect())) { 616 | return false; 617 | } 618 | } 619 | 620 | // for normal text selections 621 | for (let i = 0; i < selection.rangeCount; ++i) 622 | { 623 | const range: Range = selection.getRangeAt(i); // get the text range 624 | const bounds: ClientRect | DOMRect = range.getBoundingClientRect(); 625 | if (bounds.width > 0 && bounds.height > 0 && forceSelectionIfWithinRect(ev, bounds)) { 626 | return false; 627 | } 628 | } 629 | } 630 | 631 | function onMouseUp(ev: MouseEvent) 632 | { 633 | if (ev.button === 1) { 634 | // return value to normal to allow clicking engines again with middle mouse 635 | canMiddleClickEngine = true; 636 | } 637 | } 638 | 639 | // if ev position is within the given rect plus margin, try to show popup for the selection 640 | function forceSelectionIfWithinRect(ev: MouseEvent, rect: ClientRect | DOMRect) 641 | { 642 | const margin = activationSettings.middleMouseSelectionClickMargin; 643 | 644 | if (ev.clientX > rect.left - margin && ev.clientX < rect.right + margin 645 | && ev.clientY > rect.top - margin && ev.clientY < rect.bottom + margin) 646 | { 647 | // We got it! Event shouldn't do anything else. 648 | ev.preventDefault(); 649 | ev.stopPropagation(); 650 | 651 | if (saveCurrentSelection()) { 652 | ev["isMouse"] = true; 653 | showPopupForSelection(ev, false); 654 | } 655 | 656 | // blocks same middle click from triggering popup on down and then a search on up (on an engine icon) 657 | canMiddleClickEngine = false; 658 | return true; 659 | } 660 | return false; 661 | } 662 | } 663 | 664 | namespace PopupCreator 665 | { 666 | export let onSearchEngineClick = null; 667 | 668 | export class SSSPopup extends HTMLElement 669 | { 670 | content: HTMLDivElement; 671 | inputField: HTMLInputElement; 672 | enginesContainer: HTMLDivElement; 673 | 674 | constructor(settings: SSS.Settings, sssIcons: { [id: string] : SSS.SSSIconDefinition; }) 675 | { 676 | super(); 677 | 678 | Object.setPrototypeOf(this, SSSPopup.prototype); // needed so that instanceof and casts work 679 | 680 | const shadowRoot = this.attachShadow({mode: "closed"}); 681 | 682 | const css = this.generateStylesheet(settings); 683 | var style = document.createElement("style"); 684 | style.appendChild(document.createTextNode(css)); 685 | shadowRoot.appendChild(style); 686 | 687 | // create popup parent (will contain all icons) 688 | this.content = document.createElement("div"); 689 | this.content.classList.add("sss-content"); 690 | shadowRoot.appendChild(this.content); 691 | 692 | if (settings.showSelectionTextField) 693 | { 694 | this.inputField = document.createElement("input"); 695 | this.inputField.type = "text"; 696 | this.inputField.classList.add("sss-input-field"); 697 | this.content.appendChild(this.inputField); 698 | } 699 | 700 | if (this.inputField && settings.selectionTextFieldLocation === SSS.SelectionTextFieldLocation.Top) { 701 | this.content.appendChild(this.inputField); 702 | } 703 | 704 | this.enginesContainer = document.createElement("div"); 705 | this.enginesContainer.classList.add("sss-engines"); 706 | this.content.appendChild(this.enginesContainer); 707 | 708 | if (this.inputField && settings.selectionTextFieldLocation === SSS.SelectionTextFieldLocation.Bottom) { 709 | this.content.appendChild(this.inputField); 710 | } 711 | 712 | this.createPopupContent(settings, sssIcons); 713 | } 714 | 715 | generateStylesheet(settings: SSS.Settings) 716 | { 717 | // Due to "all: initial !important", all inherited properties that are 718 | // defined afterwards will also need to use !important. 719 | // Inherited properties: https://stackoverflow.com/a/5612360/2162837 720 | 721 | return ` 722 | :host { 723 | all: initial !important; 724 | } 725 | 726 | .sss-content { 727 | font-size: 0px !important; 728 | direction: ltr !important; 729 | position: absolute; 730 | z-index: 2147483647; 731 | user-select: none; 732 | -moz-user-select: none; 733 | box-shadow: rgba(0, 0, 0, 0.5) 0px 0px 3px; 734 | background-color: ${settings.popupBackgroundColor}; 735 | border-radius: ${settings.popupBorderRadius}px; 736 | padding: ${settings.popupPaddingY}px ${settings.popupPaddingX}px; 737 | text-align: center; 738 | ${this.generateStylesheet_Width(settings)} 739 | } 740 | 741 | .sss-content img { 742 | width: ${settings.popupItemSize}px; 743 | height: ${settings.popupItemSize}px; 744 | padding: ${3 + settings.popupItemVerticalPadding}px ${settings.popupItemPadding}px; 745 | border-radius: ${settings.popupItemBorderRadius}px; 746 | cursor: pointer; 747 | } 748 | 749 | .sss-content img:hover { 750 | ${this.generateStylesheet_IconHover(settings)} 751 | } 752 | 753 | .separator { 754 | ${this.generateStylesheet_Separator(settings)} 755 | } 756 | 757 | .sss-engines { 758 | ${this.generateStylesheet_TextAlign(settings)} 759 | } 760 | 761 | .sss-input-field { 762 | box-sizing: border-box; 763 | width: calc(100% - 8px); 764 | border: 1px solid #ccc; 765 | border-radius: ${settings.popupBorderRadius}px; 766 | padding: 4px 7px; 767 | margin: 4px 0px 2px 0px; 768 | } 769 | 770 | .sss-input-field:hover { 771 | border: 1px solid ${settings.popupHighlightColor}; 772 | } 773 | 774 | ${settings.useCustomPopupCSS === true ? settings.customPopupCSS : ""} 775 | `; 776 | } 777 | 778 | generateStylesheet_TextAlign(settings: SSS.Settings): string 779 | { 780 | let textAlign: string = "center"; 781 | 782 | if (!settings.useSingleRow) 783 | { 784 | switch (settings.iconAlignmentInGrid) { 785 | case SSS.IconAlignment.Left: textAlign = "left"; break; 786 | case SSS.IconAlignment.Right: textAlign = "right"; break; 787 | } 788 | } 789 | 790 | return `text-align: ${textAlign} !important;`; 791 | } 792 | 793 | generateStylesheet_Width(settings: SSS.Settings): string 794 | { 795 | let width: number; 796 | 797 | if (settings.useSingleRow) 798 | { 799 | // Calculate the final width of the popup based on the known widths and paddings of everything. 800 | // We need this so that if the popup is created too close to the borders of the page it still gets the right size. 801 | const nSeparators = settings.searchEngines.filter(e => e.type === SSS.SearchEngineType.SSS && (e as SSS.SearchEngine_SSS).id === "separator").length; 802 | const nPopupIcons: number = settings.searchEngines.length - nSeparators; 803 | width = nPopupIcons * (settings.popupItemSize + 2 * settings.popupItemPadding); 804 | width += nSeparators * (settings.popupItemSize * settings.popupSeparatorWidth / 100 + 2 * settings.popupItemPadding); 805 | } 806 | else 807 | { 808 | const nPopupIconsPerRow: number = Math.max(1, Math.min(settings.nPopupIconsPerRow, settings.searchEngines.length)); 809 | width = nPopupIconsPerRow * (settings.popupItemSize + 2 * settings.popupItemPadding); 810 | } 811 | 812 | return `width: ${width}px;`; 813 | } 814 | 815 | generateStylesheet_IconHover(settings: SSS.Settings): string 816 | { 817 | if (settings.popupItemHoverBehaviour === SSS.ItemHoverBehaviour.Highlight 818 | || settings.popupItemHoverBehaviour === SSS.ItemHoverBehaviour.HighlightAndMove) 819 | { 820 | let borderCompensation; 821 | if (settings.popupItemHoverBehaviour === SSS.ItemHoverBehaviour.HighlightAndMove) { 822 | const marginTopValue = Math.min(-3 - settings.popupItemVerticalPadding + 2, -2); // equal or less than -2 to counter the border's 2px 823 | borderCompensation = `margin-top: ${marginTopValue}px;`; 824 | } else { 825 | const paddingBottomValue = Math.max(3 + settings.popupItemVerticalPadding - 2, 0); // must be positive to counter the border's 2px 826 | borderCompensation = `padding-bottom: ${paddingBottomValue}px;`; 827 | } 828 | 829 | return ` 830 | border-bottom: 2px ${settings.popupHighlightColor} solid; 831 | border-radius: ${settings.popupItemBorderRadius == 0 ? 2 : settings.popupItemBorderRadius}px; 832 | ${borderCompensation} 833 | `; 834 | } 835 | else if (settings.popupItemHoverBehaviour === SSS.ItemHoverBehaviour.Scale) 836 | { 837 | // "backface-visibility: hidden" prevents blurriness 838 | return ` 839 | transform: scale(1.15); 840 | backface-visibility: hidden; 841 | `; 842 | } 843 | 844 | return ""; 845 | } 846 | 847 | generateStylesheet_Separator(settings: SSS.Settings): string 848 | { 849 | const separatorWidth = settings.popupItemSize * settings.popupSeparatorWidth / 100; 850 | const separatorMargin = (separatorWidth - settings.popupItemSize) / 2; 851 | 852 | return ` 853 | pointer-events: none !important; 854 | margin-left: ${separatorMargin}px; 855 | margin-right: ${separatorMargin}px; 856 | `; 857 | } 858 | 859 | createPopupContent(settings: SSS.Settings, sssIcons: { [id: string] : SSS.SSSIconDefinition; }) 860 | { 861 | // add each engine to the popup 862 | for (let i = 0; i < settings.searchEngines.length; i++) 863 | { 864 | const engine = settings.searchEngines[i]; 865 | let icon: HTMLImageElement; 866 | 867 | // special SSS icons with special functions 868 | if (engine.type === SSS.SearchEngineType.SSS) 869 | { 870 | const sssEngine = engine as SSS.SearchEngine_SSS; 871 | const sssIcon = sssIcons[sssEngine.id]; 872 | 873 | const iconImgSource = browser.extension.getURL(sssIcon.iconPath); 874 | const isInteractive = sssIcon.isInteractive !== false; // undefined or true means it's interactive 875 | icon = this.setupEngineIcon(sssEngine, iconImgSource, sssIcon.name, isInteractive, settings); 876 | 877 | if (sssEngine.id === "separator") { 878 | icon.classList.add("separator"); 879 | } 880 | } 881 | // "normal" custom search engines 882 | else 883 | { 884 | const userEngine = engine as SSS.SearchEngine_NonSSS; 885 | let iconImgSource: string; 886 | 887 | if (userEngine.iconUrl.startsWith("data:")) { 888 | iconImgSource = userEngine.iconUrl; // use "URL" directly, as it's pure image data 889 | } else { 890 | const cachedIcon = settings.searchEnginesCache[userEngine.iconUrl]; 891 | iconImgSource = cachedIcon ? cachedIcon : userEngine.iconUrl; // should have cached icon, but if not (for some reason) fall back to URL 892 | } 893 | 894 | icon = this.setupEngineIcon(userEngine, iconImgSource, userEngine.name, true, settings); 895 | } 896 | 897 | this.enginesContainer.appendChild(icon); 898 | } 899 | } 900 | 901 | setupEngineIcon(engine: SSS.SearchEngine, iconImgSource: string, iconTitle: string, isInteractive: boolean, settings: SSS.Settings): HTMLImageElement 902 | { 903 | const icon: HTMLImageElement = document.createElement("img"); 904 | icon.src = iconImgSource; 905 | icon.tabIndex = 0; // to allow cycling through the icons using "tab" 906 | 907 | // if icon responds to mouse interaction 908 | if (isInteractive) { 909 | icon.title = engine.shortcut ? `${iconTitle} (${engine.shortcut})` : iconTitle; // tooltip 910 | icon.addEventListener("mouseup", ev => onSearchEngineClick(ev, engine, settings)); // "mouse up" instead of "click" to support middle click 911 | // prevent context menu since icons have a right click behaviour 912 | icon.addEventListener("contextmenu", ev => { 913 | ev.preventDefault(); 914 | return false; 915 | }); 916 | } 917 | 918 | // prevents focus from changing to icon and breaking copy from input fields 919 | icon.addEventListener("mousedown", ev => ev.preventDefault()); 920 | // disable dragging popup images 921 | icon.ondragstart = _ => false; 922 | 923 | return icon; 924 | } 925 | 926 | setPopupPosition(settings: SSS.Settings, selection: ContentScript.SelectionData, mousePositionX: number, mousePositionY: number) 927 | { 928 | const bounds = this.content.getBoundingClientRect(); 929 | const width = bounds.width; 930 | const height = bounds.height; 931 | 932 | // position popup 933 | 934 | let positionLeft: number; 935 | let positionTop: number; 936 | 937 | // decide popup position based on settings 938 | if (settings.popupLocation === SSS.PopupLocation.Selection) { 939 | let rect; 940 | if (selection.isInInputField) { 941 | rect = selection.element.getBoundingClientRect(); 942 | } else { 943 | const range = selection.selection.getRangeAt(0); // get the text range 944 | rect = range.getBoundingClientRect(); 945 | } 946 | // lower right corner of selected text's "bounds" 947 | positionLeft = rect.right + window.pageXOffset; 948 | positionTop = rect.bottom + window.pageYOffset; 949 | } 950 | else if (settings.popupLocation === SSS.PopupLocation.Cursor) { 951 | // right above the mouse position 952 | positionLeft = mousePositionX; 953 | positionTop = mousePositionY - height - 10; // 10 is forced padding to avoid popup being too close to cursor 954 | } 955 | 956 | // center horizontally 957 | positionLeft -= width / 2; 958 | 959 | // apply user offsets from settings 960 | positionLeft += settings.popupOffsetX; 961 | positionTop -= settings.popupOffsetY; // invert sign because y is 0 at the top 962 | 963 | // don't const popup be outside of the viewport 964 | 965 | const margin: number = 5; 966 | 967 | // left/right checks 968 | if (positionLeft < margin + window.scrollX) { 969 | positionLeft = margin + window.scrollX; 970 | } else { 971 | const clientWidth = Math.max(document.body.clientWidth, document.documentElement.clientWidth); 972 | if (positionLeft + width + margin > clientWidth + window.scrollX) { 973 | positionLeft = clientWidth + window.scrollX - width - margin; 974 | } 975 | } 976 | 977 | // top/bottom checks 978 | if (positionTop < margin + window.scrollY) { 979 | positionTop = margin + window.scrollY; 980 | } else { 981 | const clientHeight = Math.max(document.body.clientHeight, document.documentElement.clientHeight); 982 | if (positionTop + height + margin > clientHeight + window.scrollY) { 983 | positionTop = clientHeight + window.scrollY - height - margin; 984 | } 985 | } 986 | 987 | // finally set the size and position values 988 | this.content.style.setProperty("left", positionLeft + "px"); 989 | this.content.style.setProperty("top", positionTop + "px"); 990 | } 991 | 992 | playAnimation(settings: SSS.Settings) 993 | { 994 | this.content.animate({ transform: ["scale(0.8)", "scale(1)"] } as PropertyIndexedKeyframes, settings.popupAnimationDuration); 995 | this.content.animate({ opacity: ["0", "1"] } as PropertyIndexedKeyframes, settings.popupAnimationDuration * 0.5); 996 | } 997 | 998 | isReceiverOfEvent(ev: Event) 999 | { 1000 | return ev.target === this; 1001 | } 1002 | 1003 | setFocusOnInputFieldText() 1004 | { 1005 | this.inputField.focus(); 1006 | } 1007 | 1008 | setInputFieldText(text: string) 1009 | { 1010 | this.inputField.value = text; 1011 | } 1012 | 1013 | getInputFieldText(): string 1014 | { 1015 | return this.inputField.value; 1016 | } 1017 | 1018 | isShown(): boolean 1019 | { 1020 | return this.content.style.display === "inline-block"; 1021 | } 1022 | 1023 | show() 1024 | { 1025 | this.content.style.setProperty("display", "inline-block"); 1026 | } 1027 | 1028 | hide() 1029 | { 1030 | this.content.style.setProperty("display", "none"); 1031 | } 1032 | } 1033 | } 1034 | -------------------------------------------------------------------------------- /src/content-scripts/selectionchange.ts: -------------------------------------------------------------------------------- 1 | // Original script by Jared Jacobs, located at github.com/2is10/selectionchange-polyfill 2 | // License: http://unlicense.org 3 | // Adapted for Swift Selection Search by Daniel Lobo. 4 | 5 | namespace selectionchange 6 | { 7 | const MAC = /^Mac/.test(navigator.platform); 8 | const MAC_MOVE_KEYS = new Set([65, 66, 69, 70, 78, 80]); // A, B, E, F, P, N from support.apple.com/en-ie/HT201236 9 | export const modifierKey = MAC ? "metaKey" : "ctrlKey"; 10 | 11 | let ranges = null; 12 | 13 | export function start() { 14 | ranges = getSelectedRanges(); 15 | document.addEventListener("input", onInput, true); 16 | document.addEventListener("keydown", onKeyDown, true); 17 | document.addEventListener("mouseup", onMouseUp, true); 18 | } 19 | 20 | export function stop() { 21 | ranges = null; 22 | document.removeEventListener("input", onInput, true); 23 | document.removeEventListener("keydown", onKeyDown, true); 24 | document.removeEventListener("mouseup", onMouseUp, true); 25 | } 26 | 27 | export class CustomSelectionChangeEvent extends CustomEvent 28 | { 29 | altKey: boolean; 30 | isMouse: boolean; 31 | event: Event; 32 | } 33 | 34 | function getSelectedRanges() 35 | { 36 | const selection = document.getSelection(); 37 | const newRanges = []; 38 | 39 | if (selection !== null) { 40 | for (let i = 0; i < selection.rangeCount; i++) { 41 | newRanges.push(selection.getRangeAt(i)); 42 | } 43 | } 44 | 45 | return newRanges; 46 | } 47 | 48 | function onInput(ev) 49 | { 50 | if (!isInputField(ev.target)) { 51 | dispatchEventIfSelectionChanged(true, ev, false); 52 | } 53 | } 54 | 55 | function onKeyDown(ev) 56 | { 57 | const code = ev.keyCode; 58 | 59 | if ((code === 65 && ev[modifierKey] && !ev.shiftKey && !ev.altKey) // Ctrl-A or Cmd-A 60 | || (code >= 35 && code <= 40 && ev.shiftKey) // home, end and arrow keys 61 | || (ev.ctrlKey && MAC && MAC_MOVE_KEYS.has(code))) 62 | { 63 | if (!isInputField(ev.target)) { // comment to enable selections with keyboard 64 | setTimeout(() => dispatchEventIfSelectionChanged(true, ev, false), 0); 65 | } 66 | } 67 | } 68 | 69 | function onMouseUp(ev) 70 | { 71 | if (ev.button === 0) { 72 | setTimeout(() => dispatchEventIfSelectionChanged(isInputField(ev.target), ev, true), 0); 73 | } 74 | } 75 | 76 | function dispatchEventIfSelectionChanged(force, ev, isMouse) 77 | { 78 | const newRanges = getSelectedRanges(); 79 | 80 | if (force || !areAllRangesEqual(newRanges, ranges)) { 81 | ranges = newRanges; 82 | const event = new CustomSelectionChangeEvent("customselectionchange"); 83 | event.altKey = ev.altKey; 84 | event.isMouse = isMouse; 85 | event.event = ev; 86 | setTimeout(() => document.dispatchEvent(event), 0); 87 | } 88 | } 89 | 90 | function isInputField(elem) 91 | { 92 | return elem.tagName === "INPUT" || elem.tagName === "TEXTAREA"; 93 | } 94 | 95 | // compares two lists of ranges to see if the ranges are the exact same 96 | function areAllRangesEqual(rs1, rs2) 97 | { 98 | if (rs1.length !== rs2.length) { 99 | return false; 100 | } 101 | 102 | for (let i = 0; i < rs1.length; i++) 103 | { 104 | const r1 = rs1[i]; 105 | const r2 = rs2[i]; 106 | 107 | const areEqual = r1.startContainer === r2.startContainer 108 | && r1.startOffset === r2.startOffset 109 | && r1.endContainer === r2.endContainer 110 | && r1.endOffset === r2.endOffset; 111 | 112 | if (!areEqual) { 113 | return false; 114 | } 115 | } 116 | 117 | return true; 118 | } 119 | } -------------------------------------------------------------------------------- /src/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanisLupus/swift-selection-search/f541b5115b19df0088ad34400457c2c6446f726c/src/icons/icon48.png -------------------------------------------------------------------------------- /src/icons/icon96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanisLupus/swift-selection-search/f541b5115b19df0088ad34400457c2c6446f726c/src/icons/icon96.png -------------------------------------------------------------------------------- /src/libs/Sortable-license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2013-2017 Lebedev Konstantin ibnRubaXa@gmail.com http://rubaxa.github.io/Sortable/ 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. -------------------------------------------------------------------------------- /src/libs/Sortable.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * Sortable 3 | * @author RubaXa 4 | * @license MIT 5 | */ 6 | 7 | (function sortableModule(factory) { 8 | "use strict"; 9 | 10 | if (typeof define === "function" && define.amd) { 11 | define(factory); 12 | } 13 | else if (typeof module != "undefined" && typeof module.exports != "undefined") { 14 | module.exports = factory(); 15 | } 16 | else { 17 | /* jshint sub:true */ 18 | window["Sortable"] = factory(); 19 | } 20 | })(function sortableFactory() { 21 | "use strict"; 22 | 23 | if (typeof window === "undefined" || !window.document) { 24 | return function sortableError() { 25 | throw new Error("Sortable.js requires a window with a document"); 26 | }; 27 | } 28 | 29 | var dragEl, 30 | parentEl, 31 | ghostEl, 32 | cloneEl, 33 | rootEl, 34 | nextEl, 35 | lastDownEl, 36 | 37 | scrollEl, 38 | scrollParentEl, 39 | scrollCustomFn, 40 | 41 | lastEl, 42 | lastCSS, 43 | lastParentCSS, 44 | 45 | oldIndex, 46 | newIndex, 47 | 48 | activeGroup, 49 | putSortable, 50 | 51 | autoScroll = {}, 52 | 53 | tapEvt, 54 | touchEvt, 55 | 56 | moved, 57 | 58 | /** @const */ 59 | R_SPACE = /\s+/g, 60 | R_FLOAT = /left|right|inline/, 61 | 62 | expando = 'Sortable' + (new Date).getTime(), 63 | 64 | win = window, 65 | document = win.document, 66 | parseInt = win.parseInt, 67 | setTimeout = win.setTimeout, 68 | 69 | $ = win.jQuery || win.Zepto, 70 | Polymer = win.Polymer, 71 | 72 | captureMode = false, 73 | passiveMode = false, 74 | 75 | supportDraggable = ('draggable' in document.createElement('div')), 76 | supportCssPointerEvents = (function (el) { 77 | // false when IE11 78 | if (!!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie)/i)) { 79 | return false; 80 | } 81 | el = document.createElement('x'); 82 | el.style.cssText = 'pointer-events:auto'; 83 | return el.style.pointerEvents === 'auto'; 84 | })(), 85 | 86 | _silent = false, 87 | 88 | abs = Math.abs, 89 | min = Math.min, 90 | 91 | savedInputChecked = [], 92 | touchDragOverListeners = [], 93 | 94 | _autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) { 95 | // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521 96 | if (rootEl && options.scroll) { 97 | var _this = rootEl[expando], 98 | el, 99 | rect, 100 | sens = options.scrollSensitivity, 101 | speed = options.scrollSpeed, 102 | 103 | x = evt.clientX, 104 | y = evt.clientY, 105 | 106 | winWidth = window.innerWidth, 107 | winHeight = window.innerHeight, 108 | 109 | vx, 110 | vy, 111 | 112 | scrollOffsetX, 113 | scrollOffsetY 114 | ; 115 | 116 | // Delect scrollEl 117 | if (scrollParentEl !== rootEl) { 118 | scrollEl = options.scroll; 119 | scrollParentEl = rootEl; 120 | scrollCustomFn = options.scrollFn; 121 | 122 | if (scrollEl === true) { 123 | scrollEl = rootEl; 124 | 125 | do { 126 | if ((scrollEl.offsetWidth < scrollEl.scrollWidth) || 127 | (scrollEl.offsetHeight < scrollEl.scrollHeight) 128 | ) { 129 | break; 130 | } 131 | /* jshint boss:true */ 132 | } while (scrollEl = scrollEl.parentNode); 133 | } 134 | } 135 | 136 | if (scrollEl) { 137 | el = scrollEl; 138 | rect = scrollEl.getBoundingClientRect(); 139 | vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens); 140 | vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens); 141 | } 142 | 143 | 144 | if (!(vx || vy)) { 145 | vx = (winWidth - x <= sens) - (x <= sens); 146 | vy = (winHeight - y <= sens) - (y <= sens); 147 | 148 | /* jshint expr:true */ 149 | (vx || vy) && (el = win); 150 | } 151 | 152 | 153 | if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) { 154 | autoScroll.el = el; 155 | autoScroll.vx = vx; 156 | autoScroll.vy = vy; 157 | 158 | clearInterval(autoScroll.pid); 159 | 160 | if (el) { 161 | autoScroll.pid = setInterval(function () { 162 | scrollOffsetY = vy ? vy * speed : 0; 163 | scrollOffsetX = vx ? vx * speed : 0; 164 | 165 | if ('function' === typeof(scrollCustomFn)) { 166 | return scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt); 167 | } 168 | 169 | if (el === win) { 170 | win.scrollTo(win.pageXOffset + scrollOffsetX, win.pageYOffset + scrollOffsetY); 171 | } else { 172 | el.scrollTop += scrollOffsetY; 173 | el.scrollLeft += scrollOffsetX; 174 | } 175 | }, 24); 176 | } 177 | } 178 | } 179 | }, 30), 180 | 181 | _prepareGroup = function (options) { 182 | function toFn(value, pull) { 183 | if (value === void 0 || value === true) { 184 | value = group.name; 185 | } 186 | 187 | if (typeof value === 'function') { 188 | return value; 189 | } else { 190 | return function (to, from) { 191 | var fromGroup = from.options.group.name; 192 | 193 | return pull 194 | ? value 195 | : value && (value.join 196 | ? value.indexOf(fromGroup) > -1 197 | : (fromGroup == value) 198 | ); 199 | }; 200 | } 201 | } 202 | 203 | var group = {}; 204 | var originalGroup = options.group; 205 | 206 | if (!originalGroup || typeof originalGroup != 'object') { 207 | originalGroup = {name: originalGroup}; 208 | } 209 | 210 | group.name = originalGroup.name; 211 | group.checkPull = toFn(originalGroup.pull, true); 212 | group.checkPut = toFn(originalGroup.put); 213 | group.revertClone = originalGroup.revertClone; 214 | 215 | options.group = group; 216 | } 217 | ; 218 | 219 | // Detect support a passive mode 220 | try { 221 | window.addEventListener('test', null, Object.defineProperty({}, 'passive', { 222 | get: function () { 223 | // `false`, because everything starts to work incorrectly and instead of d'n'd, 224 | // begins the page has scrolled. 225 | passiveMode = false; 226 | captureMode = { 227 | capture: false, 228 | passive: passiveMode 229 | }; 230 | } 231 | })); 232 | } catch (err) {} 233 | 234 | /** 235 | * @class Sortable 236 | * @param {HTMLElement} el 237 | * @param {Object} [options] 238 | */ 239 | function Sortable(el, options) { 240 | if (!(el && el.nodeType && el.nodeType === 1)) { 241 | throw 'Sortable: `el` must be HTMLElement, and not ' + {}.toString.call(el); 242 | } 243 | 244 | this.el = el; // root element 245 | this.options = options = _extend({}, options); 246 | 247 | 248 | // Export instance 249 | el[expando] = this; 250 | 251 | // Default options 252 | var defaults = { 253 | group: Math.random(), 254 | sort: true, 255 | disabled: false, 256 | store: null, 257 | handle: null, 258 | scroll: true, 259 | scrollSensitivity: 30, 260 | scrollSpeed: 10, 261 | draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*', 262 | ghostClass: 'sortable-ghost', 263 | chosenClass: 'sortable-chosen', 264 | dragClass: 'sortable-drag', 265 | ignore: 'a, img', 266 | filter: null, 267 | preventOnFilter: true, 268 | animation: 0, 269 | setData: function (dataTransfer, dragEl) { 270 | dataTransfer.setData('Text', dragEl.textContent); 271 | }, 272 | dropBubble: false, 273 | dragoverBubble: false, 274 | dataIdAttr: 'data-id', 275 | delay: 0, 276 | forceFallback: false, 277 | fallbackClass: 'sortable-fallback', 278 | fallbackOnBody: false, 279 | fallbackTolerance: 0, 280 | fallbackOffset: {x: 0, y: 0}, 281 | supportPointer: Sortable.supportPointer !== false 282 | }; 283 | 284 | 285 | // Set default options 286 | for (var name in defaults) { 287 | !(name in options) && (options[name] = defaults[name]); 288 | } 289 | 290 | _prepareGroup(options); 291 | 292 | // Bind all private methods 293 | for (var fn in this) { 294 | if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { 295 | this[fn] = this[fn].bind(this); 296 | } 297 | } 298 | 299 | // Setup drag mode 300 | this.nativeDraggable = options.forceFallback ? false : supportDraggable; 301 | 302 | // Bind events 303 | _on(el, 'mousedown', this._onTapStart); 304 | _on(el, 'touchstart', this._onTapStart); 305 | options.supportPointer && _on(el, 'pointerdown', this._onTapStart); 306 | 307 | if (this.nativeDraggable) { 308 | _on(el, 'dragover', this); 309 | _on(el, 'dragenter', this); 310 | } 311 | 312 | touchDragOverListeners.push(this._onDragOver); 313 | 314 | // Restore sorting 315 | options.store && this.sort(options.store.get(this)); 316 | } 317 | 318 | 319 | Sortable.prototype = /** @lends Sortable.prototype */ { 320 | constructor: Sortable, 321 | 322 | _onTapStart: function (/** Event|TouchEvent */evt) { 323 | var _this = this, 324 | el = this.el, 325 | options = this.options, 326 | preventOnFilter = options.preventOnFilter, 327 | type = evt.type, 328 | touch = evt.touches && evt.touches[0], 329 | target = (touch || evt).target, 330 | originalTarget = evt.target.shadowRoot && (evt.path && evt.path[0]) || target, 331 | filter = options.filter, 332 | startIndex; 333 | 334 | _saveInputCheckedState(el); 335 | 336 | 337 | // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group. 338 | if (dragEl) { 339 | return; 340 | } 341 | 342 | if (/mousedown|pointerdown/.test(type) && evt.button !== 0 || options.disabled) { 343 | return; // only left button or enabled 344 | } 345 | 346 | // cancel dnd if original target is content editable 347 | if (originalTarget.isContentEditable) { 348 | return; 349 | } 350 | 351 | target = _closest(target, options.draggable, el); 352 | 353 | if (!target) { 354 | return; 355 | } 356 | 357 | if (lastDownEl === target) { 358 | // Ignoring duplicate `down` 359 | return; 360 | } 361 | 362 | // Get the index of the dragged element within its parent 363 | startIndex = _index(target, options.draggable); 364 | 365 | // Check filter 366 | if (typeof filter === 'function') { 367 | if (filter.call(this, evt, target, this)) { 368 | _dispatchEvent(_this, originalTarget, 'filter', target, el, el, startIndex); 369 | preventOnFilter && evt.preventDefault(); 370 | return; // cancel dnd 371 | } 372 | } 373 | else if (filter) { 374 | filter = filter.split(',').some(function (criteria) { 375 | criteria = _closest(originalTarget, criteria.trim(), el); 376 | 377 | if (criteria) { 378 | _dispatchEvent(_this, criteria, 'filter', target, el, el, startIndex); 379 | return true; 380 | } 381 | }); 382 | 383 | if (filter) { 384 | preventOnFilter && evt.preventDefault(); 385 | return; // cancel dnd 386 | } 387 | } 388 | 389 | if (options.handle && !_closest(originalTarget, options.handle, el)) { 390 | return; 391 | } 392 | 393 | // Prepare `dragstart` 394 | this._prepareDragStart(evt, touch, target, startIndex); 395 | }, 396 | 397 | _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target, /** Number */startIndex) { 398 | var _this = this, 399 | el = _this.el, 400 | options = _this.options, 401 | ownerDocument = el.ownerDocument, 402 | dragStartFn; 403 | 404 | if (target && !dragEl && (target.parentNode === el)) { 405 | tapEvt = evt; 406 | 407 | rootEl = el; 408 | dragEl = target; 409 | parentEl = dragEl.parentNode; 410 | nextEl = dragEl.nextSibling; 411 | lastDownEl = target; 412 | activeGroup = options.group; 413 | oldIndex = startIndex; 414 | 415 | this._lastX = (touch || evt).clientX; 416 | this._lastY = (touch || evt).clientY; 417 | 418 | dragEl.style['will-change'] = 'all'; 419 | 420 | dragStartFn = function () { 421 | // Delayed drag has been triggered 422 | // we can re-enable the events: touchmove/mousemove 423 | _this._disableDelayedDrag(); 424 | 425 | // Make the element draggable 426 | dragEl.draggable = _this.nativeDraggable; 427 | 428 | // Chosen item 429 | _toggleClass(dragEl, options.chosenClass, true); 430 | 431 | // Bind the events: dragstart/dragend 432 | _this._triggerDragStart(evt, touch); 433 | 434 | // Drag start event 435 | _dispatchEvent(_this, rootEl, 'choose', dragEl, rootEl, rootEl, oldIndex); 436 | }; 437 | 438 | // Disable "draggable" 439 | options.ignore.split(',').forEach(function (criteria) { 440 | _find(dragEl, criteria.trim(), _disableDraggable); 441 | }); 442 | 443 | _on(ownerDocument, 'mouseup', _this._onDrop); 444 | _on(ownerDocument, 'touchend', _this._onDrop); 445 | _on(ownerDocument, 'touchcancel', _this._onDrop); 446 | _on(ownerDocument, 'selectstart', _this); 447 | options.supportPointer && _on(ownerDocument, 'pointercancel', _this._onDrop); 448 | 449 | if (options.delay) { 450 | // If the user moves the pointer or let go the click or touch 451 | // before the delay has been reached: 452 | // disable the delayed drag 453 | _on(ownerDocument, 'mouseup', _this._disableDelayedDrag); 454 | _on(ownerDocument, 'touchend', _this._disableDelayedDrag); 455 | _on(ownerDocument, 'touchcancel', _this._disableDelayedDrag); 456 | _on(ownerDocument, 'mousemove', _this._disableDelayedDrag); 457 | _on(ownerDocument, 'touchmove', _this._disableDelayedDrag); 458 | options.supportPointer && _on(ownerDocument, 'pointermove', _this._disableDelayedDrag); 459 | 460 | _this._dragStartTimer = setTimeout(dragStartFn, options.delay); 461 | } else { 462 | dragStartFn(); 463 | } 464 | 465 | 466 | } 467 | }, 468 | 469 | _disableDelayedDrag: function () { 470 | var ownerDocument = this.el.ownerDocument; 471 | 472 | clearTimeout(this._dragStartTimer); 473 | _off(ownerDocument, 'mouseup', this._disableDelayedDrag); 474 | _off(ownerDocument, 'touchend', this._disableDelayedDrag); 475 | _off(ownerDocument, 'touchcancel', this._disableDelayedDrag); 476 | _off(ownerDocument, 'mousemove', this._disableDelayedDrag); 477 | _off(ownerDocument, 'touchmove', this._disableDelayedDrag); 478 | _off(ownerDocument, 'pointermove', this._disableDelayedDrag); 479 | }, 480 | 481 | _triggerDragStart: function (/** Event */evt, /** Touch */touch) { 482 | touch = touch || (evt.pointerType == 'touch' ? evt : null); 483 | 484 | if (touch) { 485 | // Touch device support 486 | tapEvt = { 487 | target: dragEl, 488 | clientX: touch.clientX, 489 | clientY: touch.clientY 490 | }; 491 | 492 | this._onDragStart(tapEvt, 'touch'); 493 | } 494 | else if (!this.nativeDraggable) { 495 | this._onDragStart(tapEvt, true); 496 | } 497 | else { 498 | _on(dragEl, 'dragend', this); 499 | _on(rootEl, 'dragstart', this._onDragStart); 500 | } 501 | 502 | try { 503 | if (document.selection) { 504 | // Timeout neccessary for IE9 505 | _nextTick(function () { 506 | document.selection.empty(); 507 | }); 508 | } else { 509 | window.getSelection().removeAllRanges(); 510 | } 511 | } catch (err) { 512 | } 513 | }, 514 | 515 | _dragStarted: function () { 516 | if (rootEl && dragEl) { 517 | var options = this.options; 518 | 519 | // Apply effect 520 | _toggleClass(dragEl, options.ghostClass, true); 521 | _toggleClass(dragEl, options.dragClass, false); 522 | 523 | Sortable.active = this; 524 | 525 | // Drag start event 526 | _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, rootEl, oldIndex); 527 | } else { 528 | this._nulling(); 529 | } 530 | }, 531 | 532 | _emulateDragOver: function () { 533 | if (touchEvt) { 534 | if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY) { 535 | return; 536 | } 537 | 538 | this._lastX = touchEvt.clientX; 539 | this._lastY = touchEvt.clientY; 540 | 541 | if (!supportCssPointerEvents) { 542 | _css(ghostEl, 'display', 'none'); 543 | } 544 | 545 | var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY); 546 | var parent = target; 547 | var i = touchDragOverListeners.length; 548 | 549 | if (target && target.shadowRoot) { 550 | target = target.shadowRoot.elementFromPoint(touchEvt.clientX, touchEvt.clientY); 551 | parent = target; 552 | } 553 | 554 | if (parent) { 555 | do { 556 | if (parent[expando]) { 557 | while (i--) { 558 | touchDragOverListeners[i]({ 559 | clientX: touchEvt.clientX, 560 | clientY: touchEvt.clientY, 561 | target: target, 562 | rootEl: parent 563 | }); 564 | } 565 | 566 | break; 567 | } 568 | 569 | target = parent; // store last element 570 | } 571 | /* jshint boss:true */ 572 | while (parent = parent.parentNode); 573 | } 574 | 575 | if (!supportCssPointerEvents) { 576 | _css(ghostEl, 'display', ''); 577 | } 578 | } 579 | }, 580 | 581 | 582 | _onTouchMove: function (/**TouchEvent*/evt) { 583 | if (tapEvt) { 584 | var options = this.options, 585 | fallbackTolerance = options.fallbackTolerance, 586 | fallbackOffset = options.fallbackOffset, 587 | touch = evt.touches ? evt.touches[0] : evt, 588 | dx = (touch.clientX - tapEvt.clientX) + fallbackOffset.x, 589 | dy = (touch.clientY - tapEvt.clientY) + fallbackOffset.y, 590 | translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)'; 591 | 592 | // only set the status to dragging, when we are actually dragging 593 | if (!Sortable.active) { 594 | if (fallbackTolerance && 595 | min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) < fallbackTolerance 596 | ) { 597 | return; 598 | } 599 | 600 | this._dragStarted(); 601 | } 602 | 603 | // as well as creating the ghost element on the document body 604 | this._appendGhost(); 605 | 606 | moved = true; 607 | touchEvt = touch; 608 | 609 | _css(ghostEl, 'webkitTransform', translate3d); 610 | _css(ghostEl, 'mozTransform', translate3d); 611 | _css(ghostEl, 'msTransform', translate3d); 612 | _css(ghostEl, 'transform', translate3d); 613 | 614 | evt.preventDefault(); 615 | } 616 | }, 617 | 618 | _appendGhost: function () { 619 | if (!ghostEl) { 620 | var rect = dragEl.getBoundingClientRect(), 621 | css = _css(dragEl), 622 | options = this.options, 623 | ghostRect; 624 | 625 | ghostEl = dragEl.cloneNode(true); 626 | 627 | _toggleClass(ghostEl, options.ghostClass, false); 628 | _toggleClass(ghostEl, options.fallbackClass, true); 629 | _toggleClass(ghostEl, options.dragClass, true); 630 | 631 | _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10)); 632 | _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10)); 633 | _css(ghostEl, 'width', rect.width); 634 | _css(ghostEl, 'height', rect.height); 635 | _css(ghostEl, 'opacity', '0.8'); 636 | _css(ghostEl, 'position', 'fixed'); 637 | _css(ghostEl, 'zIndex', '100000'); 638 | _css(ghostEl, 'pointerEvents', 'none'); 639 | 640 | options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl); 641 | 642 | // Fixing dimensions. 643 | ghostRect = ghostEl.getBoundingClientRect(); 644 | _css(ghostEl, 'width', rect.width * 2 - ghostRect.width); 645 | _css(ghostEl, 'height', rect.height * 2 - ghostRect.height); 646 | } 647 | }, 648 | 649 | _onDragStart: function (/**Event*/evt, /**boolean*/useFallback) { 650 | var _this = this; 651 | var dataTransfer = evt.dataTransfer; 652 | var options = _this.options; 653 | 654 | _this._offUpEvents(); 655 | 656 | if (activeGroup.checkPull(_this, _this, dragEl, evt)) { 657 | cloneEl = _clone(dragEl); 658 | 659 | cloneEl.draggable = false; 660 | cloneEl.style['will-change'] = ''; 661 | 662 | _css(cloneEl, 'display', 'none'); 663 | _toggleClass(cloneEl, _this.options.chosenClass, false); 664 | 665 | // #1143: IFrame support workaround 666 | _this._cloneId = _nextTick(function () { 667 | rootEl.insertBefore(cloneEl, dragEl); 668 | _dispatchEvent(_this, rootEl, 'clone', dragEl); 669 | }); 670 | } 671 | 672 | _toggleClass(dragEl, options.dragClass, true); 673 | 674 | if (useFallback) { 675 | if (useFallback === 'touch') { 676 | // Bind touch events 677 | _on(document, 'touchmove', _this._onTouchMove); 678 | _on(document, 'touchend', _this._onDrop); 679 | _on(document, 'touchcancel', _this._onDrop); 680 | 681 | if (options.supportPointer) { 682 | _on(document, 'pointermove', _this._onTouchMove); 683 | _on(document, 'pointerup', _this._onDrop); 684 | } 685 | } else { 686 | // Old brwoser 687 | _on(document, 'mousemove', _this._onTouchMove); 688 | _on(document, 'mouseup', _this._onDrop); 689 | } 690 | 691 | _this._loopId = setInterval(_this._emulateDragOver, 50); 692 | } 693 | else { 694 | if (dataTransfer) { 695 | dataTransfer.effectAllowed = 'move'; 696 | options.setData && options.setData.call(_this, dataTransfer, dragEl); 697 | } 698 | 699 | _on(document, 'drop', _this); 700 | 701 | // #1143: Бывает элемент с IFrame внутри блокирует `drop`, 702 | // поэтому если вызвался `mouseover`, значит надо отменять весь d'n'd. 703 | // Breaking Chrome 62+ 704 | // _on(document, 'mouseover', _this); 705 | 706 | _this._dragStartId = _nextTick(_this._dragStarted); 707 | } 708 | }, 709 | 710 | _onDragOver: function (/**Event*/evt) { 711 | var el = this.el, 712 | target, 713 | dragRect, 714 | targetRect, 715 | revert, 716 | options = this.options, 717 | group = options.group, 718 | activeSortable = Sortable.active, 719 | isOwner = (activeGroup === group), 720 | isMovingBetweenSortable = false, 721 | canSort = options.sort; 722 | 723 | if (evt.preventDefault !== void 0) { 724 | evt.preventDefault(); 725 | !options.dragoverBubble && evt.stopPropagation(); 726 | } 727 | 728 | if (dragEl.animated) { 729 | return; 730 | } 731 | 732 | moved = true; 733 | 734 | if (activeSortable && !options.disabled && 735 | (isOwner 736 | ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list 737 | : ( 738 | putSortable === this || 739 | ( 740 | (activeSortable.lastPullMode = activeGroup.checkPull(this, activeSortable, dragEl, evt)) && 741 | group.checkPut(this, activeSortable, dragEl, evt) 742 | ) 743 | ) 744 | ) && 745 | (evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback 746 | ) { 747 | // Smart auto-scrolling 748 | _autoScroll(evt, options, this.el); 749 | 750 | if (_silent) { 751 | return; 752 | } 753 | 754 | target = _closest(evt.target, options.draggable, el); 755 | dragRect = dragEl.getBoundingClientRect(); 756 | 757 | if (putSortable !== this) { 758 | putSortable = this; 759 | isMovingBetweenSortable = true; 760 | } 761 | 762 | if (revert) { 763 | _cloneHide(activeSortable, true); 764 | parentEl = rootEl; // actualization 765 | 766 | if (cloneEl || nextEl) { 767 | rootEl.insertBefore(dragEl, cloneEl || nextEl); 768 | } 769 | else if (!canSort) { 770 | rootEl.appendChild(dragEl); 771 | } 772 | 773 | return; 774 | } 775 | 776 | 777 | if ((el.children.length === 0) || (el.children[0] === ghostEl) || 778 | (el === evt.target) && (_ghostIsLast(el, evt)) 779 | ) { 780 | //assign target only if condition is true 781 | if (el.children.length !== 0 && el.children[0] !== ghostEl && el === evt.target) { 782 | target = el.lastElementChild; 783 | } 784 | 785 | if (target) { 786 | if (target.animated) { 787 | return; 788 | } 789 | 790 | targetRect = target.getBoundingClientRect(); 791 | } 792 | 793 | _cloneHide(activeSortable, isOwner); 794 | 795 | if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt) !== false) { 796 | if (!dragEl.contains(el)) { 797 | el.appendChild(dragEl); 798 | parentEl = el; // actualization 799 | } 800 | 801 | this._animate(dragRect, dragEl); 802 | target && this._animate(targetRect, target); 803 | } 804 | } 805 | else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) { 806 | if (lastEl !== target) { 807 | lastEl = target; 808 | lastCSS = _css(target); 809 | lastParentCSS = _css(target.parentNode); 810 | } 811 | 812 | targetRect = target.getBoundingClientRect(); 813 | 814 | var width = targetRect.right - targetRect.left, 815 | height = targetRect.bottom - targetRect.top, 816 | floating = R_FLOAT.test(lastCSS.cssFloat + lastCSS.display) 817 | || (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0), 818 | isWide = (target.offsetWidth > dragEl.offsetWidth), 819 | isLong = (target.offsetHeight > dragEl.offsetHeight), 820 | halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5, 821 | nextSibling = target.nextElementSibling, 822 | after = false 823 | ; 824 | 825 | if (floating) { 826 | var elTop = dragEl.offsetTop, 827 | tgTop = target.offsetTop; 828 | 829 | if (elTop === tgTop) { 830 | after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide; 831 | } 832 | else if (target.previousElementSibling === dragEl || dragEl.previousElementSibling === target) { 833 | after = (evt.clientY - targetRect.top) / height > 0.5; 834 | } else { 835 | after = tgTop > elTop; 836 | } 837 | } else if (!isMovingBetweenSortable) { 838 | after = (nextSibling !== dragEl) && !isLong || halfway && isLong; 839 | } 840 | 841 | var moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, after); 842 | 843 | if (moveVector !== false) { 844 | if (moveVector === 1 || moveVector === -1) { 845 | after = (moveVector === 1); 846 | } 847 | 848 | _silent = true; 849 | setTimeout(_unsilent, 30); 850 | 851 | _cloneHide(activeSortable, isOwner); 852 | 853 | if (!dragEl.contains(el)) { 854 | if (after && !nextSibling) { 855 | el.appendChild(dragEl); 856 | } else { 857 | target.parentNode.insertBefore(dragEl, after ? nextSibling : target); 858 | } 859 | } 860 | 861 | parentEl = dragEl.parentNode; // actualization 862 | 863 | this._animate(dragRect, dragEl); 864 | this._animate(targetRect, target); 865 | } 866 | } 867 | } 868 | }, 869 | 870 | _animate: function (prevRect, target) { 871 | var ms = this.options.animation; 872 | 873 | if (ms) { 874 | var currentRect = target.getBoundingClientRect(); 875 | 876 | if (prevRect.nodeType === 1) { 877 | prevRect = prevRect.getBoundingClientRect(); 878 | } 879 | 880 | _css(target, 'transition', 'none'); 881 | _css(target, 'transform', 'translate3d(' 882 | + (prevRect.left - currentRect.left) + 'px,' 883 | + (prevRect.top - currentRect.top) + 'px,0)' 884 | ); 885 | 886 | target.offsetWidth; // repaint 887 | 888 | _css(target, 'transition', 'all ' + ms + 'ms'); 889 | _css(target, 'transform', 'translate3d(0,0,0)'); 890 | 891 | clearTimeout(target.animated); 892 | target.animated = setTimeout(function () { 893 | _css(target, 'transition', ''); 894 | _css(target, 'transform', ''); 895 | target.animated = false; 896 | }, ms); 897 | } 898 | }, 899 | 900 | _offUpEvents: function () { 901 | var ownerDocument = this.el.ownerDocument; 902 | 903 | _off(document, 'touchmove', this._onTouchMove); 904 | _off(document, 'pointermove', this._onTouchMove); 905 | _off(ownerDocument, 'mouseup', this._onDrop); 906 | _off(ownerDocument, 'touchend', this._onDrop); 907 | _off(ownerDocument, 'pointerup', this._onDrop); 908 | _off(ownerDocument, 'touchcancel', this._onDrop); 909 | _off(ownerDocument, 'pointercancel', this._onDrop); 910 | _off(ownerDocument, 'selectstart', this); 911 | }, 912 | 913 | _onDrop: function (/**Event*/evt) { 914 | var el = this.el, 915 | options = this.options; 916 | 917 | clearInterval(this._loopId); 918 | clearInterval(autoScroll.pid); 919 | clearTimeout(this._dragStartTimer); 920 | 921 | _cancelNextTick(this._cloneId); 922 | _cancelNextTick(this._dragStartId); 923 | 924 | // Unbind events 925 | _off(document, 'mouseover', this); 926 | _off(document, 'mousemove', this._onTouchMove); 927 | 928 | if (this.nativeDraggable) { 929 | _off(document, 'drop', this); 930 | _off(el, 'dragstart', this._onDragStart); 931 | } 932 | 933 | this._offUpEvents(); 934 | 935 | if (evt) { 936 | if (moved) { 937 | evt.preventDefault(); 938 | !options.dropBubble && evt.stopPropagation(); 939 | } 940 | 941 | ghostEl && ghostEl.parentNode && ghostEl.parentNode.removeChild(ghostEl); 942 | 943 | if (rootEl === parentEl || Sortable.active.lastPullMode !== 'clone') { 944 | // Remove clone 945 | cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl); 946 | } 947 | 948 | if (dragEl) { 949 | if (this.nativeDraggable) { 950 | _off(dragEl, 'dragend', this); 951 | } 952 | 953 | _disableDraggable(dragEl); 954 | dragEl.style['will-change'] = ''; 955 | 956 | // Remove class's 957 | _toggleClass(dragEl, this.options.ghostClass, false); 958 | _toggleClass(dragEl, this.options.chosenClass, false); 959 | 960 | // Drag stop event 961 | _dispatchEvent(this, rootEl, 'unchoose', dragEl, parentEl, rootEl, oldIndex); 962 | 963 | if (rootEl !== parentEl) { 964 | newIndex = _index(dragEl, options.draggable); 965 | 966 | if (newIndex >= 0) { 967 | // Add event 968 | _dispatchEvent(null, parentEl, 'add', dragEl, parentEl, rootEl, oldIndex, newIndex); 969 | 970 | // Remove event 971 | _dispatchEvent(this, rootEl, 'remove', dragEl, parentEl, rootEl, oldIndex, newIndex); 972 | 973 | // drag from one list and drop into another 974 | _dispatchEvent(null, parentEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex); 975 | _dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex); 976 | } 977 | } 978 | else { 979 | if (dragEl.nextSibling !== nextEl) { 980 | // Get the index of the dragged element within its parent 981 | newIndex = _index(dragEl, options.draggable); 982 | 983 | if (newIndex >= 0) { 984 | // drag & drop within the same list 985 | _dispatchEvent(this, rootEl, 'update', dragEl, parentEl, rootEl, oldIndex, newIndex); 986 | _dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex); 987 | } 988 | } 989 | } 990 | 991 | if (Sortable.active) { 992 | /* jshint eqnull:true */ 993 | if (newIndex == null || newIndex === -1) { 994 | newIndex = oldIndex; 995 | } 996 | 997 | _dispatchEvent(this, rootEl, 'end', dragEl, parentEl, rootEl, oldIndex, newIndex); 998 | 999 | // Save sorting 1000 | this.save(); 1001 | } 1002 | } 1003 | 1004 | } 1005 | 1006 | this._nulling(); 1007 | }, 1008 | 1009 | _nulling: function() { 1010 | rootEl = 1011 | dragEl = 1012 | parentEl = 1013 | ghostEl = 1014 | nextEl = 1015 | cloneEl = 1016 | lastDownEl = 1017 | 1018 | scrollEl = 1019 | scrollParentEl = 1020 | 1021 | tapEvt = 1022 | touchEvt = 1023 | 1024 | moved = 1025 | newIndex = 1026 | 1027 | lastEl = 1028 | lastCSS = 1029 | 1030 | putSortable = 1031 | activeGroup = 1032 | Sortable.active = null; 1033 | 1034 | savedInputChecked.forEach(function (el) { 1035 | el.checked = true; 1036 | }); 1037 | savedInputChecked.length = 0; 1038 | }, 1039 | 1040 | handleEvent: function (/**Event*/evt) { 1041 | switch (evt.type) { 1042 | case 'drop': 1043 | case 'dragend': 1044 | this._onDrop(evt); 1045 | break; 1046 | 1047 | case 'dragover': 1048 | case 'dragenter': 1049 | if (dragEl) { 1050 | this._onDragOver(evt); 1051 | _globalDragOver(evt); 1052 | } 1053 | break; 1054 | 1055 | case 'mouseover': 1056 | this._onDrop(evt); 1057 | break; 1058 | 1059 | case 'selectstart': 1060 | evt.preventDefault(); 1061 | break; 1062 | } 1063 | }, 1064 | 1065 | 1066 | /** 1067 | * Serializes the item into an array of string. 1068 | * @returns {String[]} 1069 | */ 1070 | toArray: function () { 1071 | var order = [], 1072 | el, 1073 | children = this.el.children, 1074 | i = 0, 1075 | n = children.length, 1076 | options = this.options; 1077 | 1078 | for (; i < n; i++) { 1079 | el = children[i]; 1080 | if (_closest(el, options.draggable, this.el)) { 1081 | order.push(el.getAttribute(options.dataIdAttr) || _generateId(el)); 1082 | } 1083 | } 1084 | 1085 | return order; 1086 | }, 1087 | 1088 | 1089 | /** 1090 | * Sorts the elements according to the array. 1091 | * @param {String[]} order order of the items 1092 | */ 1093 | sort: function (order) { 1094 | var items = {}, rootEl = this.el; 1095 | 1096 | this.toArray().forEach(function (id, i) { 1097 | var el = rootEl.children[i]; 1098 | 1099 | if (_closest(el, this.options.draggable, rootEl)) { 1100 | items[id] = el; 1101 | } 1102 | }, this); 1103 | 1104 | order.forEach(function (id) { 1105 | if (items[id]) { 1106 | rootEl.removeChild(items[id]); 1107 | rootEl.appendChild(items[id]); 1108 | } 1109 | }); 1110 | }, 1111 | 1112 | 1113 | /** 1114 | * Save the current sorting 1115 | */ 1116 | save: function () { 1117 | var store = this.options.store; 1118 | store && store.set(this); 1119 | }, 1120 | 1121 | 1122 | /** 1123 | * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. 1124 | * @param {HTMLElement} el 1125 | * @param {String} [selector] default: `options.draggable` 1126 | * @returns {HTMLElement|null} 1127 | */ 1128 | closest: function (el, selector) { 1129 | return _closest(el, selector || this.options.draggable, this.el); 1130 | }, 1131 | 1132 | 1133 | /** 1134 | * Set/get option 1135 | * @param {string} name 1136 | * @param {*} [value] 1137 | * @returns {*} 1138 | */ 1139 | option: function (name, value) { 1140 | var options = this.options; 1141 | 1142 | if (value === void 0) { 1143 | return options[name]; 1144 | } else { 1145 | options[name] = value; 1146 | 1147 | if (name === 'group') { 1148 | _prepareGroup(options); 1149 | } 1150 | } 1151 | }, 1152 | 1153 | 1154 | /** 1155 | * Destroy 1156 | */ 1157 | destroy: function () { 1158 | var el = this.el; 1159 | 1160 | el[expando] = null; 1161 | 1162 | _off(el, 'mousedown', this._onTapStart); 1163 | _off(el, 'touchstart', this._onTapStart); 1164 | _off(el, 'pointerdown', this._onTapStart); 1165 | 1166 | if (this.nativeDraggable) { 1167 | _off(el, 'dragover', this); 1168 | _off(el, 'dragenter', this); 1169 | } 1170 | 1171 | // Remove draggable attributes 1172 | Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) { 1173 | el.removeAttribute('draggable'); 1174 | }); 1175 | 1176 | touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1); 1177 | 1178 | this._onDrop(); 1179 | 1180 | this.el = el = null; 1181 | } 1182 | }; 1183 | 1184 | 1185 | function _cloneHide(sortable, state) { 1186 | if (sortable.lastPullMode !== 'clone') { 1187 | state = true; 1188 | } 1189 | 1190 | if (cloneEl && (cloneEl.state !== state)) { 1191 | _css(cloneEl, 'display', state ? 'none' : ''); 1192 | 1193 | if (!state) { 1194 | if (cloneEl.state) { 1195 | if (sortable.options.group.revertClone) { 1196 | rootEl.insertBefore(cloneEl, nextEl); 1197 | sortable._animate(dragEl, cloneEl); 1198 | } else { 1199 | rootEl.insertBefore(cloneEl, dragEl); 1200 | } 1201 | } 1202 | } 1203 | 1204 | cloneEl.state = state; 1205 | } 1206 | } 1207 | 1208 | 1209 | function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) { 1210 | if (el) { 1211 | ctx = ctx || document; 1212 | 1213 | do { 1214 | if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) { 1215 | return el; 1216 | } 1217 | /* jshint boss:true */ 1218 | } while (el = _getParentOrHost(el)); 1219 | } 1220 | 1221 | return null; 1222 | } 1223 | 1224 | 1225 | function _getParentOrHost(el) { 1226 | var parent = el.host; 1227 | 1228 | return (parent && parent.nodeType) ? parent : el.parentNode; 1229 | } 1230 | 1231 | 1232 | function _globalDragOver(/**Event*/evt) { 1233 | if (evt.dataTransfer) { 1234 | evt.dataTransfer.dropEffect = 'move'; 1235 | } 1236 | evt.preventDefault(); 1237 | } 1238 | 1239 | 1240 | function _on(el, event, fn) { 1241 | el.addEventListener(event, fn, captureMode); 1242 | } 1243 | 1244 | 1245 | function _off(el, event, fn) { 1246 | el.removeEventListener(event, fn, captureMode); 1247 | } 1248 | 1249 | 1250 | function _toggleClass(el, name, state) { 1251 | if (el) { 1252 | if (el.classList) { 1253 | el.classList[state ? 'add' : 'remove'](name); 1254 | } 1255 | else { 1256 | var className = (' ' + el.className + ' ').replace(R_SPACE, ' ').replace(' ' + name + ' ', ' '); 1257 | el.className = (className + (state ? ' ' + name : '')).replace(R_SPACE, ' '); 1258 | } 1259 | } 1260 | } 1261 | 1262 | 1263 | function _css(el, prop, val) { 1264 | var style = el && el.style; 1265 | 1266 | if (style) { 1267 | if (val === void 0) { 1268 | if (document.defaultView && document.defaultView.getComputedStyle) { 1269 | val = document.defaultView.getComputedStyle(el, ''); 1270 | } 1271 | else if (el.currentStyle) { 1272 | val = el.currentStyle; 1273 | } 1274 | 1275 | return prop === void 0 ? val : val[prop]; 1276 | } 1277 | else { 1278 | if (!(prop in style)) { 1279 | prop = '-webkit-' + prop; 1280 | } 1281 | 1282 | style[prop] = val + (typeof val === 'string' ? '' : 'px'); 1283 | } 1284 | } 1285 | } 1286 | 1287 | 1288 | function _find(ctx, tagName, iterator) { 1289 | if (ctx) { 1290 | var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length; 1291 | 1292 | if (iterator) { 1293 | for (; i < n; i++) { 1294 | iterator(list[i], i); 1295 | } 1296 | } 1297 | 1298 | return list; 1299 | } 1300 | 1301 | return []; 1302 | } 1303 | 1304 | 1305 | 1306 | function _dispatchEvent(sortable, rootEl, name, targetEl, toEl, fromEl, startIndex, newIndex) { 1307 | sortable = (sortable || rootEl[expando]); 1308 | 1309 | var evt = document.createEvent('Event'), 1310 | options = sortable.options, 1311 | onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); 1312 | 1313 | evt.initEvent(name, true, true); 1314 | 1315 | evt.to = toEl || rootEl; 1316 | evt.from = fromEl || rootEl; 1317 | evt.item = targetEl || rootEl; 1318 | evt.clone = cloneEl; 1319 | 1320 | evt.oldIndex = startIndex; 1321 | evt.newIndex = newIndex; 1322 | 1323 | rootEl.dispatchEvent(evt); 1324 | 1325 | if (options[onName]) { 1326 | options[onName].call(sortable, evt); 1327 | } 1328 | } 1329 | 1330 | 1331 | function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvt, willInsertAfter) { 1332 | var evt, 1333 | sortable = fromEl[expando], 1334 | onMoveFn = sortable.options.onMove, 1335 | retVal; 1336 | 1337 | evt = document.createEvent('Event'); 1338 | evt.initEvent('move', true, true); 1339 | 1340 | evt.to = toEl; 1341 | evt.from = fromEl; 1342 | evt.dragged = dragEl; 1343 | evt.draggedRect = dragRect; 1344 | evt.related = targetEl || toEl; 1345 | evt.relatedRect = targetRect || toEl.getBoundingClientRect(); 1346 | evt.willInsertAfter = willInsertAfter; 1347 | 1348 | fromEl.dispatchEvent(evt); 1349 | 1350 | if (onMoveFn) { 1351 | retVal = onMoveFn.call(sortable, evt, originalEvt); 1352 | } 1353 | 1354 | return retVal; 1355 | } 1356 | 1357 | 1358 | function _disableDraggable(el) { 1359 | el.draggable = false; 1360 | } 1361 | 1362 | 1363 | function _unsilent() { 1364 | _silent = false; 1365 | } 1366 | 1367 | 1368 | /** @returns {HTMLElement|false} */ 1369 | function _ghostIsLast(el, evt) { 1370 | var lastEl = el.lastElementChild, 1371 | rect = lastEl.getBoundingClientRect(); 1372 | 1373 | // 5 — min delta 1374 | // abs — нельзя добавлять, а то глюки при наведении сверху 1375 | return (evt.clientY - (rect.top + rect.height) > 5) || 1376 | (evt.clientX - (rect.left + rect.width) > 5); 1377 | } 1378 | 1379 | 1380 | /** 1381 | * Generate id 1382 | * @param {HTMLElement} el 1383 | * @returns {String} 1384 | * @private 1385 | */ 1386 | function _generateId(el) { 1387 | var str = el.tagName + el.className + el.src + el.href + el.textContent, 1388 | i = str.length, 1389 | sum = 0; 1390 | 1391 | while (i--) { 1392 | sum += str.charCodeAt(i); 1393 | } 1394 | 1395 | return sum.toString(36); 1396 | } 1397 | 1398 | /** 1399 | * Returns the index of an element within its parent for a selected set of 1400 | * elements 1401 | * @param {HTMLElement} el 1402 | * @param {selector} selector 1403 | * @return {number} 1404 | */ 1405 | function _index(el, selector) { 1406 | var index = 0; 1407 | 1408 | if (!el || !el.parentNode) { 1409 | return -1; 1410 | } 1411 | 1412 | while (el && (el = el.previousElementSibling)) { 1413 | if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && (selector === '>*' || _matches(el, selector))) { 1414 | index++; 1415 | } 1416 | } 1417 | 1418 | return index; 1419 | } 1420 | 1421 | function _matches(/**HTMLElement*/el, /**String*/selector) { 1422 | if (el) { 1423 | selector = selector.split('.'); 1424 | 1425 | var tag = selector.shift().toUpperCase(), 1426 | re = new RegExp('\\s(' + selector.join('|') + ')(?=\\s)', 'g'); 1427 | 1428 | return ( 1429 | (tag === '' || el.nodeName.toUpperCase() == tag) && 1430 | (!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length) 1431 | ); 1432 | } 1433 | 1434 | return false; 1435 | } 1436 | 1437 | function _throttle(callback, ms) { 1438 | var args, _this; 1439 | 1440 | return function () { 1441 | if (args === void 0) { 1442 | args = arguments; 1443 | _this = this; 1444 | 1445 | setTimeout(function () { 1446 | if (args.length === 1) { 1447 | callback.call(_this, args[0]); 1448 | } else { 1449 | callback.apply(_this, args); 1450 | } 1451 | 1452 | args = void 0; 1453 | }, ms); 1454 | } 1455 | }; 1456 | } 1457 | 1458 | function _extend(dst, src) { 1459 | if (dst && src) { 1460 | for (var key in src) { 1461 | if (src.hasOwnProperty(key)) { 1462 | dst[key] = src[key]; 1463 | } 1464 | } 1465 | } 1466 | 1467 | return dst; 1468 | } 1469 | 1470 | function _clone(el) { 1471 | if (Polymer && Polymer.dom) { 1472 | return Polymer.dom(el).cloneNode(true); 1473 | } 1474 | else if ($) { 1475 | return $(el).clone(true)[0]; 1476 | } 1477 | else { 1478 | return el.cloneNode(true); 1479 | } 1480 | } 1481 | 1482 | function _saveInputCheckedState(root) { 1483 | var inputs = root.getElementsByTagName('input'); 1484 | var idx = inputs.length; 1485 | 1486 | while (idx--) { 1487 | var el = inputs[idx]; 1488 | el.checked && savedInputChecked.push(el); 1489 | } 1490 | } 1491 | 1492 | function _nextTick(fn) { 1493 | return setTimeout(fn, 0); 1494 | } 1495 | 1496 | function _cancelNextTick(id) { 1497 | return clearTimeout(id); 1498 | } 1499 | 1500 | // Fixed #973: 1501 | _on(document, 'touchmove', function (evt) { 1502 | if (Sortable.active) { 1503 | evt.preventDefault(); 1504 | } 1505 | }); 1506 | 1507 | // Export utils 1508 | Sortable.utils = { 1509 | on: _on, 1510 | off: _off, 1511 | css: _css, 1512 | find: _find, 1513 | is: function (el, selector) { 1514 | return !!_closest(el, selector, el); 1515 | }, 1516 | extend: _extend, 1517 | throttle: _throttle, 1518 | closest: _closest, 1519 | toggleClass: _toggleClass, 1520 | clone: _clone, 1521 | index: _index, 1522 | nextTick: _nextTick, 1523 | cancelNextTick: _cancelNextTick 1524 | }; 1525 | 1526 | 1527 | /** 1528 | * Create sortable instance 1529 | * @param {HTMLElement} el 1530 | * @param {Object} [options] 1531 | */ 1532 | Sortable.create = function (el, options) { 1533 | return new Sortable(el, options); 1534 | }; 1535 | 1536 | 1537 | // Export 1538 | Sortable.version = '1.7.0'; 1539 | return Sortable; 1540 | }); 1541 | -------------------------------------------------------------------------------- /src/libs/iconv-lite-license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Alexander Shtuchkin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Swift Selection Search", 4 | "version": "3.48.0", 5 | 6 | "description": "Swiftly access your search engines in a popup panel when you select text in a webpage. Context search menu also included!", 7 | "homepage_url": "https://github.com/CanisLupus/swift-selection-search", 8 | 9 | "icons": { 10 | "48": "icons/icon48.png", 11 | "96": "icons/icon96.png" 12 | }, 13 | 14 | "background": { 15 | "page": "swift-selection-search.html" 16 | }, 17 | 18 | "applications": { 19 | "gecko": { 20 | "id": "jid1-KdTtiCj6wxVAFA@jetpack", 21 | "strict_min_version": "66.0" 22 | } 23 | }, 24 | 25 | "options_ui": { 26 | "browser_style": true, 27 | "page": "settings/settings.html", 28 | "open_in_tab": true 29 | }, 30 | 31 | "permissions": ["", "clipboardWrite", "contextMenus", "search", "storage", "webNavigation", "webRequest", "webRequestBlocking"], 32 | "optional_permissions": ["downloads", "tabs"], 33 | 34 | "commands": { 35 | "open-popup": { 36 | "suggested_key": { "default": "Ctrl+Shift+Space" }, 37 | "description": "Open the popup (if text is selected)" 38 | }, 39 | "toggle-auto-popup": { 40 | "suggested_key": { "default": "Ctrl+Shift+U" }, 41 | "description": "Switch popup opening behaviour between \"Auto\" and \"Keyboard-only\"" 42 | } 43 | }, 44 | 45 | "web_accessible_resources": ["res/*"] 46 | } 47 | -------------------------------------------------------------------------------- /src/res/msg-pages/sss-intro.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 52 | Welcome to SSS! 53 | 54 | 55 | 56 |
57 |
58 | 59 |

Swift Selection Search

60 |
61 | 62 |
63 |
64 |

Hey! Sorry for bothering you. I'm Swift Selection Search, but friends call me SSS.

65 | 66 |

Try selecting some text and my popup should appear. I'm in the context menu too!

67 | 68 |
    69 |
  • How to customize something?
    70 | Please go to the Firefox Add-ons menu (the puzzle piece icon!) and access my Options page.
  • 71 |
  • How to add new search engines? Or remove some?
    72 | You can do that in the Options page as well. :) Be sure to read the instructions at the top!
  • 73 |
  • Found a problem?
    74 | In the Options page please expand the "Instructions" section and check "Problems?".
  • 75 |
76 | 77 |

Thank you!
78 | SSS

79 |
80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /src/res/sss-engine-icons/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanisLupus/swift-selection-search/f541b5115b19df0088ad34400457c2c6446f726c/src/res/sss-engine-icons/copy.png -------------------------------------------------------------------------------- /src/res/sss-engine-icons/open-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanisLupus/swift-selection-search/f541b5115b19df0088ad34400457c2c6446f726c/src/res/sss-engine-icons/open-link.png -------------------------------------------------------------------------------- /src/res/sss-engine-icons/separator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanisLupus/swift-selection-search/f541b5115b19df0088ad34400457c2c6446f726c/src/res/sss-engine-icons/separator.png -------------------------------------------------------------------------------- /src/search-variable-modifications.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This file contains functions related to the modifications the user can apply to URL text "variables" 4 | like searchTerms, href, host, etc. The user can essentially "substring" a variable' value and do all kinds 5 | of text replacements on it, even using regex. 6 | 7 | This could be part of the background script's code, since it's only used there, but it's large and specific 8 | enough that it's better located in a different file and included in separate. 9 | 10 | */ 11 | 12 | namespace SearchVariables 13 | { 14 | export function modifySearchVariable(url: string, variableName: string, text: string, encode: boolean): string 15 | { 16 | text = text.trim(); 17 | 18 | let searchIndex: number = 0; 19 | const queryParts: string[] = []; 20 | 21 | while (true) 22 | { 23 | const variableModifications = getSearchVariableReplacements(url, variableName, searchIndex); 24 | 25 | if (variableModifications.searchVariableEndIndex == -1) { 26 | break; 27 | } 28 | 29 | queryParts.push(url.substring(searchIndex, variableModifications.searchVariableStartIndex)); 30 | 31 | let replacedText: string = replace(text, variableModifications.modifications); 32 | 33 | // encode chars after replacement (but only if the modifications DON'T include the disableURIEncoding function) 34 | if (encode && !variableModifications.containsDisableURIEncoding()) { 35 | replacedText = encodeURIComponent(replacedText); 36 | } 37 | 38 | queryParts.push(replacedText); 39 | searchIndex = variableModifications.searchVariableEndIndex; 40 | } 41 | 42 | queryParts.push(url.substring(searchIndex)); 43 | return queryParts.join(""); 44 | } 45 | 46 | abstract class SearchVariableModification 47 | { 48 | abstract apply(text: string): string; 49 | } 50 | 51 | export class SearchVariableSlice extends SearchVariableModification // exported for tests 52 | { 53 | constructor( 54 | public startIndex: number, 55 | public endIndex: number) 56 | { 57 | super(); 58 | } 59 | 60 | apply(text: string): string 61 | { 62 | let endIndex = this.endIndex; 63 | if (endIndex === null) { 64 | endIndex = text.length; 65 | } else if (endIndex < 0) { 66 | endIndex += text.length; 67 | } 68 | 69 | let startIndex = this.startIndex; 70 | if (startIndex === null) { 71 | startIndex = 0; 72 | } else if (startIndex < 0) { 73 | startIndex += text.length; 74 | if (endIndex === 0) { 75 | endIndex = text.length; 76 | } 77 | } 78 | 79 | try { 80 | return text.substring(startIndex, endIndex); 81 | } catch { 82 | return text; 83 | } 84 | } 85 | } 86 | 87 | export class SearchVariableReplacement extends SearchVariableModification // exported for tests 88 | { 89 | constructor( 90 | public source: string, 91 | public target: string) 92 | { 93 | super(); 94 | } 95 | 96 | apply(text: string): string 97 | { 98 | return text.split(this.source).join(this.target); // replace all occurrences 99 | } 100 | } 101 | 102 | export class SearchVariableRegexReplacement extends SearchVariableModification // exported for tests 103 | { 104 | constructor( 105 | public source: string, 106 | public flags: string, 107 | public target: string) 108 | { 109 | super(); 110 | } 111 | 112 | apply(text: string): string 113 | { 114 | try { 115 | const regex = new RegExp(this.source, this.flags); 116 | return text.replace(regex, this.target); // replace all occurrences 117 | } catch { 118 | return text; 119 | } 120 | } 121 | } 122 | 123 | export class SearchVariableRegexMatch extends SearchVariableModification // exported for tests 124 | { 125 | constructor( 126 | public source: string, 127 | public flags: string) 128 | { 129 | super(); 130 | } 131 | 132 | apply(text: string): string 133 | { 134 | try { 135 | const regex = new RegExp(this.source, this.flags); 136 | const match = text.match(regex) 137 | return match !== null ? match.join("") : text; 138 | } catch { 139 | return text; 140 | } 141 | } 142 | } 143 | 144 | export class SearchVariableFunction extends SearchVariableModification // exported for tests 145 | { 146 | constructor(public functionName: string) 147 | { 148 | super(); 149 | } 150 | 151 | apply(text: string): string 152 | { 153 | const name = this.functionName.toLowerCase(); 154 | switch (name) 155 | { 156 | case "lowercase": return text.toLowerCase(); 157 | case "uppercase": return text.toUpperCase(); 158 | case "encodeuricomponent": return encodeURIComponent(text); 159 | case "disableuriencoding": return text; // doesn't apply anything, only makes it so that the variable doesn't get encoded at the end 160 | default: return text; 161 | } 162 | } 163 | } 164 | 165 | export class SearchVariableModifications // exported for tests 166 | { 167 | constructor( 168 | public modifications: SearchVariableModification[], 169 | public searchVariableStartIndex: number, 170 | public searchVariableEndIndex: number) 171 | { 172 | } 173 | 174 | static createDefault(): SearchVariableModifications 175 | { 176 | return new SearchVariableModifications([], -1, -1); 177 | } 178 | 179 | containsDisableURIEncoding(): boolean 180 | { 181 | return this.modifications.find(mod => (mod instanceof SearchVariableFunction) && mod.functionName.toLowerCase() == "disableuriencoding") !== undefined; 182 | } 183 | } 184 | 185 | const enum SearchVariableParserState 186 | { 187 | EXPECTING_MODIFICATION_OR_END, // expecting a {}, (), or [] modification block (or the end of the variable name, like "searchTerms") 188 | IN_REPLACE, // inside a {} replacement block 189 | IN_REPLACE_REGEX, // inside a {/ /|} block, between the / / 190 | IN_REPLACE_REGEX_FLAGS, // inside a {/ /|} block, after the / / 191 | IN_REPLACE_SOURCE, // inside a {|} replacement block, before the |, and we know it's not a regex 192 | IN_REPLACE_TARGET, // inside a {|} replacement block, after the | 193 | IN_FUNCTION, // inside a () function block 194 | IN_RANGE_START, // inside a [] slice block 195 | IN_RANGE_END, // inside a [:] slice block, after the : 196 | } 197 | 198 | export function getSearchVariableReplacements(url: string, variableName: string, startIndexForIndexOf: number): SearchVariableModifications // exported for tests 199 | { 200 | const startString: string = "{" + variableName; // find things like {searchTerms, {href, etc 201 | 202 | // this is essentially a case insensitive "url.indexOf(startString, startIndexForIndexOf)" (JavaScript doesn't have it) 203 | var regex = new RegExp("\\" + startString, "i"); // slash to escape the {, "i" to be case insensitive 204 | let startIndex: number = url.substring(startIndexForIndexOf).search(regex); 205 | 206 | // if variable not found, quit 207 | if (startIndex === -1) { 208 | return SearchVariableModifications.createDefault(); 209 | } 210 | 211 | const modifications = SearchVariableModifications.createDefault(); 212 | 213 | startIndex += startIndexForIndexOf; 214 | 215 | let index: number = startIndex + startString.length; 216 | // if variable ends immediately with no replacements, quit 217 | if (url[index] == "}") { 218 | modifications.searchVariableStartIndex = startIndex; 219 | modifications.searchVariableEndIndex = index + 1; 220 | return modifications; 221 | } 222 | 223 | let state: SearchVariableParserState = SearchVariableParserState.EXPECTING_MODIFICATION_OR_END; 224 | 225 | let replacementSource: string; 226 | let replacementTarget: string; 227 | let isEscaped: boolean = false; 228 | 229 | let regexSource: string; 230 | let regexFlags: string; 231 | 232 | let functionName: string; 233 | 234 | let rangeStartIndexString: string; 235 | let rangeEndIndexString: string; 236 | 237 | for (; index < url.length; index++) 238 | { 239 | // get char 240 | const c: string = url[index]; 241 | 242 | switch (state) 243 | { 244 | case SearchVariableParserState.EXPECTING_MODIFICATION_OR_END: { 245 | if (c === "}") { 246 | modifications.searchVariableStartIndex = startIndex; 247 | modifications.searchVariableEndIndex = index + 1; 248 | return modifications; 249 | } else if (c === "{") { 250 | state = SearchVariableParserState.IN_REPLACE; 251 | replacementSource = ""; 252 | } else if (c === "[") { 253 | state = SearchVariableParserState.IN_RANGE_START; 254 | rangeStartIndexString = ""; 255 | } else if (c === "(") { 256 | state = SearchVariableParserState.IN_FUNCTION; 257 | functionName = ""; 258 | } else if (c !== " ") { 259 | return SearchVariableModifications.createDefault(); 260 | } 261 | break; 262 | } 263 | 264 | case SearchVariableParserState.IN_REPLACE: { 265 | if (index < url.length-2 && c === "r" && url[index+1] === "e" && url[index+2] === "/") { 266 | state = SearchVariableParserState.IN_REPLACE_REGEX; 267 | regexSource = ""; 268 | index += 2; 269 | } else { 270 | state = SearchVariableParserState.IN_REPLACE_SOURCE; 271 | index--; 272 | } 273 | break; 274 | } 275 | 276 | case SearchVariableParserState.IN_REPLACE_REGEX: { 277 | if (c === "/") { 278 | state = SearchVariableParserState.IN_REPLACE_REGEX_FLAGS; 279 | regexFlags = ""; 280 | } else { 281 | regexSource += c; 282 | } 283 | 284 | if (c === "\\" && index < url.length-1 && url[index+1] === "/") { 285 | regexSource += "/"; 286 | index += 1; 287 | } 288 | break; 289 | } 290 | 291 | case SearchVariableParserState.IN_REPLACE_REGEX_FLAGS: { 292 | if (c === "|") { 293 | state = SearchVariableParserState.IN_REPLACE_TARGET; 294 | replacementTarget = ""; 295 | } else if (c === "{") { 296 | return SearchVariableModifications.createDefault(); 297 | } else if (c === "}") { 298 | modifications.modifications.push(new SearchVariableRegexMatch(regexSource, regexFlags)); 299 | state = SearchVariableParserState.EXPECTING_MODIFICATION_OR_END; 300 | } else { 301 | regexFlags += c; 302 | } 303 | break; 304 | } 305 | 306 | case SearchVariableParserState.IN_REPLACE_SOURCE: { 307 | if (!isEscaped && c === "\\") { 308 | isEscaped = true; 309 | continue; 310 | } 311 | 312 | if (!isEscaped && c === "|") { 313 | state = SearchVariableParserState.IN_REPLACE_TARGET; 314 | replacementTarget = ""; 315 | } else if (!isEscaped && c === "{") { 316 | return SearchVariableModifications.createDefault(); 317 | } else if (!isEscaped && c === "}") { 318 | return SearchVariableModifications.createDefault(); 319 | } else { 320 | replacementSource += c; 321 | } 322 | break; 323 | } 324 | 325 | case SearchVariableParserState.IN_REPLACE_TARGET: { 326 | if (!isEscaped && c === "\\") { 327 | isEscaped = true; 328 | continue; 329 | } 330 | 331 | if (!isEscaped && c === "}") { 332 | if (regexSource && regexSource.length > 0) { 333 | modifications.modifications.push(new SearchVariableRegexReplacement(regexSource, regexFlags, replacementTarget)); 334 | } else { 335 | modifications.modifications.push(new SearchVariableReplacement(replacementSource, replacementTarget)); 336 | } 337 | state = SearchVariableParserState.EXPECTING_MODIFICATION_OR_END; 338 | } else if (!isEscaped && (c === "|" || c === "}")) { 339 | return SearchVariableModifications.createDefault(); 340 | } else { 341 | replacementTarget += c; 342 | } 343 | break; 344 | } 345 | 346 | case SearchVariableParserState.IN_FUNCTION: { 347 | if (c === ")") { 348 | modifications.modifications.push(new SearchVariableFunction(functionName)); 349 | state = SearchVariableParserState.EXPECTING_MODIFICATION_OR_END; 350 | } else { 351 | functionName += c; 352 | } 353 | break; 354 | } 355 | 356 | case SearchVariableParserState.IN_RANGE_START: { 357 | if (c === "]") { 358 | const rangeStartIndex = rangeStartIndexString.length > 0 ? Number(rangeStartIndexString) : NaN; 359 | if (isNaN(rangeStartIndex)) { 360 | return SearchVariableModifications.createDefault(); 361 | } else { 362 | modifications.modifications.push(new SearchVariableSlice(rangeStartIndex, rangeStartIndex + 1)); 363 | } 364 | 365 | state = SearchVariableParserState.EXPECTING_MODIFICATION_OR_END; 366 | } else if (c === ":") { 367 | state = SearchVariableParserState.IN_RANGE_END; 368 | rangeEndIndexString = ""; 369 | } else { 370 | rangeStartIndexString += c; 371 | } 372 | break; 373 | } 374 | 375 | case SearchVariableParserState.IN_RANGE_END: { 376 | if (c === "]") { 377 | state = SearchVariableParserState.EXPECTING_MODIFICATION_OR_END; 378 | 379 | const rangeStartIndex = rangeStartIndexString.length > 0 ? Number(rangeStartIndexString) : null; 380 | const rangeEndIndex = rangeEndIndexString.length > 0 ? Number(rangeEndIndexString) : null; 381 | 382 | if (rangeStartIndex === NaN || rangeEndIndex === NaN) { 383 | return SearchVariableModifications.createDefault(); 384 | } else { 385 | modifications.modifications.push(new SearchVariableSlice(rangeStartIndex, rangeEndIndex)); 386 | } 387 | } else { 388 | rangeEndIndexString += c; 389 | } 390 | break; 391 | } 392 | } 393 | 394 | if (isEscaped) { 395 | isEscaped = false; 396 | } 397 | } 398 | 399 | return SearchVariableModifications.createDefault(); 400 | } 401 | 402 | function replace(text: string, modifications: SearchVariableModification[]): string 403 | { 404 | for (let i = 0; i < modifications.length; i++) { 405 | const modification: SearchVariableModification = modifications[i]; 406 | text = modification.apply(text); 407 | } 408 | return text; 409 | } 410 | } -------------------------------------------------------------------------------- /src/swift-selection-search.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/shadow-dom-tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Title 7 | 8 | 23 | 24 | 25 |
26 |
27 |
28 | 29 | 67 | 68 | -------------------------------------------------------------------------------- /tests/tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Title 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/tests.ts: -------------------------------------------------------------------------------- 1 | class Test 2 | { 3 | constructor(public variableName: string, public url: string, public expectedModifications: SearchVariables.SearchVariableModifications, public example: ExampleSelection) { } 4 | } 5 | 6 | class ExampleSelection 7 | { 8 | constructor(public selectedText: string, public expectedResult: string) { } 9 | } 10 | 11 | function runTests() 12 | { 13 | const STM = SearchVariables.SearchVariableModifications; 14 | const Repl = SearchVariables.SearchVariableReplacement; 15 | const ReRepl = SearchVariables.SearchVariableRegexReplacement; 16 | const ReMatch = SearchVariables.SearchVariableRegexMatch; 17 | const Slice = SearchVariables.SearchVariableSlice; 18 | const Func = SearchVariables.SearchVariableFunction; 19 | const ES = ExampleSelection; 20 | const r = String.raw; 21 | 22 | const tests = [ 23 | // well-formed 24 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |+}{_|-}} site:{hostname}`, new STM([new Repl(" ", "+"), new Repl("_", "-")], 15, 38), new ES("a b_c_", "http://a.com?q=a+b-c- site:{hostname}") ), 25 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |+}{_*&|%%}{cenas|coiso}} site:{hostname}`, new STM([new Repl(" ", "+"), new Repl("_*&", "%%"), new Repl("cenas", "coiso")], 15, 55), new ES("a b_c_", "http://a.com?q=a b_c_ site:{hostname}") ), 26 | new Test("searchTerms", r`http://a.com?q={searchTerms}`, new STM([], 15, 28), new ES("a b_c_", "http://a.com?q=a b_c_") ), 27 | new Test("searchTerms", r`http://a.com?q={searchTerms}{searchTerms{a|b}{g|h}}{searchTerms{b|c}{h|i}}`, new STM([], 15, 28), new ES("a bgch", "http://a.com?q=a bgchb bhcha cgci") ), 28 | new Test("searchTerms", r`http://a.com?q={searchTerms{\||\\}{\{|a}}`, new STM([new Repl("|", "\\"), new Repl("{", "a")], 15, 41), new ES("a|b\\c{coisas}\\", "http://a.com?q=a\\b\\cacoisas}\\") ), 29 | new Test("searchTerms", r`http://a.com?q={searchTerms{\ |\+}{a|\\}}`, new STM([new Repl(" ", "+"), new Repl("a", "\\")], 15, 41), new ES("a | b\\c {coisas}\\", "http://a.com?q=\\+|+b\\c+{cois\\s}\\") ), 30 | new Test("searchTerms", r`http://a.com?q={searchTerms{\\|\{\|\}}}`, new STM([new Repl("\\", "{|}")], 15, 39), new ES("a | b\\c {coisas}\\", "http://a.com?q=a | b{|}c {coisas}{|}") ), 31 | new Test("searchTerms", r`http://a.com?q={searchTerms{|+}{_*&|%%}{cenas|coiso}} site:{hostname}`, new STM([new Repl("", "+"), new Repl("_*&", "%%"), new Repl("cenas", "coiso")], 15, 53), new ES("a b_c_", "http://a.com?q=a+ +b+_+c+_ site:{hostname}")), 32 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |}{_*&|%%}{cenas|coiso}} site:{hostname}`, new STM([new Repl(" ", ""), new Repl("_*&", "%%"), new Repl("cenas", "coiso")], 15, 53), new ES("a b_c_", "http://a.com?q=ab_c_ site:{hostname}") ), 33 | 34 | // malformed 35 | new Test("searchTerms", r`http://a.com?q={searchTerms |+}{_*&|%%}{cenas|coiso}} site:{hostname}`, new STM([], -1, -1), new ES("a b_c_", null)), 36 | new Test("searchTerms", r`http://a.com?q={searchTerms{ +}{_*&|%%}{cenas|coiso}} site:{hostname}`, new STM([], -1, -1), new ES("a b_c_", null)), 37 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |+{_*&|%%}{cenas|coiso}} site:{hostname}`, new STM([], -1, -1), new ES("a b_c_", null)), 38 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |+}_*&|%%}{cenas|coiso}} site:{hostname}`, new STM([], -1, -1), new ES("a b_c_", null)), 39 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |+}{_*&%%}{cenas|coiso}} site:{hostname}`, new STM([], -1, -1), new ES("a b_c_", null)), 40 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |+}{_*&|%%{cenas|coiso}} site:{hostname}`, new STM([], -1, -1), new ES("a b_c_", null)), 41 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |+}{_*&|%%}cenas|coiso}} site:{hostname}`, new STM([], -1, -1), new ES("a b_c_", null)), 42 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |+}{_*&|%%}{cenascoiso}} site:{hostname}`, new STM([], -1, -1), new ES("a b_c_", null)), 43 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |+}{_*&|%%}{cenas|coiso} site:{hostname}`, new STM([], -1, -1), new ES("a b_c_", null)), 44 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |+}{_*&|%%}{cenas|coiso}asdasda} site:{hostname}`, new STM([], -1, -1), new ES("a b_c_", null)), 45 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |+}{_*&|%%}{cenas|coiso} site:{hostname}`, new STM([], -1, -1), new ES("a b_c_", null)), 46 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |+}{_*&|%%}{cenas|coiso}`, new STM([], -1, -1), new ES("a b_c_", null)), 47 | 48 | // ranges 49 | new Test("searchTerms", r`http://a.com?q={searchTerms[0]}`, new STM([new Slice(0, 1)], 15, 31), new ES("a b_c_", "http://a.com?q=a")), 50 | new Test("searchTerms", r`http://a.com?q={searchTerms[1]}`, new STM([new Slice(1, 2)], 15, 31), new ES("a b_c_", "http://a.com?q= ")), 51 | new Test("searchTerms", r`http://a.com?q={searchTerms[1:]}`, new STM([new Slice(1, null)], 15, 32), new ES("a b_c_", "http://a.com?q= b_c_")), 52 | new Test("searchTerms", r`http://a.com?q={searchTerms[1:4]}`, new STM([new Slice(1, 4)], 15, 33), new ES("a b_c_", "http://a.com?q= b_")), 53 | new Test("searchTerms", r`http://a.com?q={searchTerms[1:10]}`, new STM([new Slice(1, 10)], 15, 34), new ES("a b_c_", "http://a.com?q= b_c_")), 54 | new Test("searchTerms", r`http://a.com?q={searchTerms[:4]}`, new STM([new Slice(null, 4)], 15, 32), new ES("a b_c_", "http://a.com?q=a b_")), 55 | new Test("searchTerms", r`http://a.com?q={searchTerms[:10]}`, new STM([new Slice(null, 10)], 15, 33), new ES("a b_c_", "http://a.com?q=a b_c_")), 56 | new Test("searchTerms", r`http://a.com?q={searchTerms[:]}`, new STM([new Slice(null, null)], 15, 31), new ES("a b_c_", "http://a.com?q=a b_c_")), 57 | new Test("searchTerms", r`http://a.com?q={searchTerms[5]}`, new STM([new Slice(5, 6)], 15, 31), new ES("a b_c_", "http://a.com?q=_")), 58 | new Test("searchTerms", r`http://a.com?q={searchTerms[5:6]}`, new STM([new Slice(5, 6)], 15, 33), new ES("a b_c_", "http://a.com?q=_")), 59 | new Test("searchTerms", r`http://a.com?q={searchTerms[5:7]}`, new STM([new Slice(5, 7)], 15, 33), new ES("a b_c_", "http://a.com?q=_")), 60 | new Test("searchTerms", r`http://a.com?q={searchTerms[6]}`, new STM([new Slice(6, 7)], 15, 31), new ES("a b_c_", "http://a.com?q=")), 61 | new Test("searchTerms", r`http://a.com?q={searchTerms[6:7]}`, new STM([new Slice(6, 7)], 15, 33), new ES("a b_c_", "http://a.com?q=")), 62 | new Test("searchTerms", r`http://a.com?q={searchTerms[6:10]}`, new STM([new Slice(6, 10)], 15, 34), new ES("a b_c_", "http://a.com?q=")), 63 | new Test("searchTerms", r`http://a.com?q={searchTerms[7]}`, new STM([new Slice(7, 8)], 15, 31), new ES("a b_c_", "http://a.com?q=")), 64 | new Test("searchTerms", r`http://a.com?q={searchTerms[7:10]}`, new STM([new Slice(7, 10)], 15, 34), new ES("a b_c_", "http://a.com?q=")), 65 | new Test("searchTerms", r`http://a.com?q={searchTerms[-1:]}`, new STM([new Slice(-1, null)], 15, 33), new ES("a b_c_", "http://a.com?q=_")), 66 | new Test("searchTerms", r`http://a.com?q={searchTerms[-2:]}`, new STM([new Slice(-2, null)], 15, 33), new ES("a b_c_", "http://a.com?q=c_")), 67 | new Test("searchTerms", r`http://a.com?q={searchTerms[-2:-2]}`, new STM([new Slice(-2, -2)], 15, 35), new ES("a b_c_", "http://a.com?q=")), 68 | new Test("searchTerms", r`http://a.com?q={searchTerms[-1]}`, new STM([new Slice(-1, 0)], 15, 32), new ES("a b_c_", "http://a.com?q=_")), 69 | new Test("searchTerms", r`http://a.com?q={searchTerms[-2]}`, new STM([new Slice(-2, -1)], 15, 32), new ES("a b_c_", "http://a.com?q=c")), 70 | new Test("searchTerms", r`http://a.com?q={searchTerms[-10]}`, new STM([new Slice(-10, -9)], 15, 33), new ES("a b_c_", "http://a.com?q=")), 71 | 72 | new Test("searchTerms", r`http://a.com?q={searchTerms[]}`, new STM([], -1, -1), new ES("a b_c_", null)), 73 | new Test("searchTerms", r`http://a.com?q={searchTerms[}]}`, new STM([], -1, -1), new ES("a b_c_", null)), 74 | new Test("searchTerms", r`http://a.com?q={searchTerms[1ab]}`, new STM([], -1, -1), new ES("a b_c_", null)), 75 | new Test("searchTerms", r`http://a.com?q={searchTerms[}`, new STM([], -1, -1), new ES("a b_c_", null)), 76 | 77 | // functions 78 | new Test("searchTerms", r`http://a.com?q={searchTerms(lowercase)}`, new STM([new Func("lowercase")], 15, 39), new ES("a B_c_", "http://a.com?q=a b_c_")), 79 | new Test("searchTerms", r`http://a.com?q={searchTerms(uppercase)}`, new STM([new Func("uppercase")], 15, 39), new ES("a b_c_", "http://a.com?q=A B_C_")), 80 | new Test("searchTerms", r`http://a.com?q={searchTerms(upperlol)}`, new STM([new Func("upperlol")], 15, 38), new ES("a b_c_", "http://a.com?q=a b_c_")), 81 | new Test("searchTerms", r`http://a.com?q={searchTerms({})}`, new STM([new Func("{}")], 15, 32), new ES("a b_c_", "http://a.com?q=a b_c_")), 82 | 83 | // mixed 84 | new Test("searchTerms", r`http://a.com?q={searchTerms[0](uppercase)}`, new STM([new Slice(0, 1), new Func("uppercase")], 15, 42), new ES("a b_c_", "http://a.com?q=A")), 85 | new Test("searchTerms", r`http://a.com?q={searchTerms(uppercase)[0]}`, new STM([new Func("uppercase"), new Slice(0, 1)], 15, 42), new ES("a b_c_", "http://a.com?q=A")), 86 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |_}{_|aaa}(uppercase){A|b}{c|12345}{C|}[:-2]}`, 87 | new STM([new Repl(" ", "_"), new Repl("_", "aaa"), new Func("uppercase"), new Repl("A", "b"), new Repl("c", "12345"), new Repl("C", ""), new Slice(null, -2)], 15, 74), 88 | new ES("a b_c_", "http://a.com?q=bbbbBbbbb") 89 | ), 90 | new Test("searchTerms", r`http://a.com?q={searchTerms{ |_}{_|aaa}(uppercase)[2:7]{B|b}}`, 91 | new STM([new Repl(" ", "_"), new Repl("_", "aaa"), new Func("uppercase"), new Slice(2, 7), new Repl("B", "b")], 15, 61), 92 | new ES("a b_c_", "http://a.com?q=AAbAA") 93 | ), 94 | 95 | // regex 96 | new Test("searchTerms", r`http://a.com?q={searchTerms{re/([^0-9a-z])/gi|$1abc}}`, new STM([new ReRepl("([^0-9a-z])", "gi", "$1abc")], 15, 53), new ES("a b_c_", "http://a.com?q=a abcb_abcc_abc")), 97 | new Test("searchTerms", r`http://a.com?q={searchTerms{re/[^0-9a-z]/gi}}`, new STM([new ReMatch("[^0-9a-z]", "gi")], 15, 45), new ES("a b_c_", "http://a.com?q= __")), 98 | new Test("searchTerms", r`http://a.com?q={searchTerms{re/\w/gi}}`, new STM([new ReMatch("\\w", "gi")], 15, 38), new ES("a b_c_", "http://a.com?q=ab_c_")), 99 | new Test("searchTerms", r`http://a.com?q={searchTerms{re/\//gi}}`, new STM([new ReMatch("\\/", "gi")], 15, 38), new ES("a/b/c", "http://a.com?q=//")), 100 | new Test("searchTerms", r`http://a.com?q={searchTerms{re/\///gi}}`, new STM([new ReMatch("\\/", "/gi")], 15, 39), new ES("a b_c_", "http://a.com?q=a b_c_")), 101 | new Test("searchTerms", r`http://a.com?q={searchTerms{r/\///gi}}`, new STM([], -1, -1), new ES("a b_c_", null)), 102 | new Test("searchTerms", r`http://a.com?q={searchTerms{re/(\w+)\s+(\w+)/gi|$2 $1}}`, new STM([new ReRepl("(\\w+)\\s+(\\w+)", "gi", "$2 $1")], 15, 55), new ES("John Smith ", "http://a.com?q=Smith John")), 103 | 104 | // non-searchTerms variables (actually just a copy of the "well-formed" tests above, but with another variable) 105 | new Test("href", r`http://a.com?q={href{ |+}{_|-}} site:{hostname}`, new STM([new Repl(" ", "+"), new Repl("_", "-")], 15, 31), new ES("a b_c_", "http://a.com?q=a+b-c- site:{hostname}") ), 106 | new Test("href", r`http://a.com?q={href{ |+}{_*&|%%}{cenas|coiso}} site:{hostname}`, new STM([new Repl(" ", "+"), new Repl("_*&", "%%"), new Repl("cenas", "coiso")], 15, 48), new ES("a b_c_", "http://a.com?q=a b_c_ site:{hostname}") ), 107 | new Test("href", r`http://a.com?q={href}`, new STM([], 15, 21), new ES("a b_c_", "http://a.com?q=a b_c_") ), 108 | new Test("href", r`http://a.com?q={href}{href{a|b}{g|h}}{href{b|c}{h|i}}`, new STM([], 15, 21), new ES("a bgch", "http://a.com?q=a bgchb bhcha cgci") ), 109 | new Test("href", r`http://a.com?q={href{\||\\}{\{|a}}`, new STM([new Repl("|", "\\"), new Repl("{", "a")], 15, 34), new ES("a|b\\c{coisas}\\", "http://a.com?q=a\\b\\cacoisas}\\") ), 110 | new Test("href", r`http://a.com?q={href{\ |\+}{a|\\}}`, new STM([new Repl(" ", "+"), new Repl("a", "\\")], 15, 34), new ES("a | b\\c {coisas}\\", "http://a.com?q=\\+|+b\\c+{cois\\s}\\") ), 111 | new Test("href", r`http://a.com?q={href{\\|\{\|\}}}`, new STM([new Repl("\\", "{|}")], 15, 32), new ES("a | b\\c {coisas}\\", "http://a.com?q=a | b{|}c {coisas}{|}") ), 112 | new Test("href", r`http://a.com?q={href{|+}{_*&|%%}{cenas|coiso}} site:{hostname}`, new STM([new Repl("", "+"), new Repl("_*&", "%%"), new Repl("cenas", "coiso")], 15, 46), new ES("a b_c_", "http://a.com?q=a+ +b+_+c+_ site:{hostname}")), 113 | new Test("href", r`http://a.com?q={href{ |}{_*&|%%}{cenas|coiso}} site:{hostname}`, new STM([new Repl(" ", ""), new Repl("_*&", "%%"), new Repl("cenas", "coiso")], 15, 46), new ES("a b_c_", "http://a.com?q=ab_c_ site:{hostname}") ), 114 | 115 | // test case insensitiveness 116 | new Test("searchTerms", r`http://a.com?q={searchterms{ |+}{_|-}} site:{hostname}`, new STM([new Repl(" ", "+"), new Repl("_", "-")], 15, 38), new ES("a b_c_", "http://a.com?q=a+b-c- site:{hostname}") ), 117 | new Test("searchTerms", r`http://a.com?q={SEARCHTERMS{ |+}{_*&|%%}{cenas|coiso}} site:{hostname}`, new STM([new Repl(" ", "+"), new Repl("_*&", "%%"), new Repl("cenas", "coiso")], 15, 55), new ES("a b_c_", "http://a.com?q=a b_c_ site:{hostname}") ), 118 | new Test("searchTerms", r`http://a.com?q={SEARCHterms}`, new STM([], 15, 28), new ES("a b_c_", "http://a.com?q=a b_c_") ), 119 | new Test("searchTerms", r`http://a.com?q={SeArChTeRmS}{searCHTErms{a|b}{g|h}}{searchTerms{b|c}{h|i}}`, new STM([], 15, 28), new ES("a bgch", "http://a.com?q=a bgchb bhcha cgci") ), 120 | new Test("searchTerms", r`http://a.com?q={SearchTerms{\||\\}{\{|a}}`, new STM([new Repl("|", "\\"), new Repl("{", "a")], 15, 41), new ES("a|b\\c{coisas}\\", "http://a.com?q=a\\b\\cacoisas}\\") ), 121 | new Test("searchTerms", r`http://a.com?q={sEaRcHtErMs{\ |\+}{a|\\}}`, new STM([new Repl(" ", "+"), new Repl("a", "\\")], 15, 41), new ES("a | b\\c {coisas}\\", "http://a.com?q=\\+|+b\\c+{cois\\s}\\") ), 122 | new Test("href", r`http://a.com?q={hREf{\\|\{\|\}}}`, new STM([new Repl("\\", "{|}")], 15, 32), new ES("a | b\\c {coisas}\\", "http://a.com?q=a | b{|}c {coisas}{|}") ), 123 | new Test("href", r`http://a.com?q={HREF{|+}{_*&|%%}{cenas|coiso}} site:{hostname}`, new STM([new Repl("", "+"), new Repl("_*&", "%%"), new Repl("cenas", "coiso")], 15, 46), new ES("a b_c_", "http://a.com?q=a+ +b+_+c+_ site:{hostname}")), 124 | new Test("href", r`http://a.com?q={hReF{ |}{_*&|%%}{cenas|coiso}} site:{hostname}`, new STM([new Repl(" ", ""), new Repl("_*&", "%%"), new Repl("cenas", "coiso")], 15, 46), new ES("a b_c_", "http://a.com?q=ab_c_ site:{hostname}") ), 125 | ]; 126 | 127 | for (const test of tests) 128 | { 129 | const modifications = SearchVariables.getSearchVariableReplacements(test.url, test.variableName, 0); 130 | const replacementsString = JSON.stringify(modifications.modifications); 131 | const expectedReplacementsString = JSON.stringify(test.expectedModifications.modifications); 132 | 133 | echo("///////////// " + JSON.stringify(test)); 134 | 135 | if (replacementsString !== expectedReplacementsString) { 136 | echo("FAIL\nreplacements\nexpected: " + expectedReplacementsString + "\nactual: " + replacementsString); 137 | } 138 | if (modifications.searchVariableStartIndex !== test.expectedModifications.searchVariableStartIndex) { 139 | echo("FAIL\nstartIndex\nexpected: " + test.expectedModifications.searchVariableStartIndex + "\nactual: " + modifications.searchVariableStartIndex); 140 | } 141 | if (modifications.searchVariableEndIndex !== test.expectedModifications.searchVariableEndIndex) { 142 | echo("FAIL\nendIndex\nexpected: " + test.expectedModifications.searchVariableEndIndex + "\nactual: " + modifications.searchVariableEndIndex); 143 | } 144 | 145 | const filteredUrl = SearchVariables.modifySearchVariable(test.url, test.variableName, test.example.selectedText, false); 146 | if (filteredUrl != (test.example.expectedResult !== null ? test.example.expectedResult : test.url)) { 147 | echo("FAIL\nfilteredUrl\nexpected: " + test.example.expectedResult + "\nactual: " + filteredUrl); 148 | } 149 | } 150 | } 151 | 152 | function echo(text: string) 153 | { 154 | document.body.appendChild(document.createTextNode(text)); 155 | document.body.appendChild(document.createElement("br")); 156 | console.log(text); 157 | } 158 | 159 | runTests(); 160 | -------------------------------------------------------------------------------- /tests/visual tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 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 | 119 | 120 | 121 | 122 |
123 |
124 | 125 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | // "outDir": "js-build", 5 | "inlineSourceMap": true, 6 | "typeRoots": ["node_modules/@types"], 7 | "newLine": "LF", 8 | "removeComments": true, 9 | } 10 | } --------------------------------------------------------------------------------