├── .eslintignore ├── .eslintrc ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── icons ├── wesisho128.png ├── wesisho16.png └── wesisho48.png ├── package.json ├── src ├── app.ts ├── background │ └── index.ts ├── css │ └── wesisho.global.css ├── logger.ts ├── manifest.json ├── options │ ├── index.html │ ├── index.tsx │ ├── shortcut-item.tsx │ ├── style.css │ ├── style.css.d.ts │ ├── web-shortcut-group.tsx │ └── web-shortcuts.tsx ├── popup │ ├── index.html │ ├── index.tsx │ ├── style.css │ └── style.css.d.ts ├── shortcut.ts ├── types │ └── css-selector-generator │ │ └── index.d.ts ├── url-handler.ts └── utils.ts ├── tsconfig.json ├── webpack ├── config.js ├── webpack.config.debug.js ├── webpack.config.dev.js └── webpack.config.prod.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack/* 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint", 6 | "react", 7 | "jsx-a11y", 8 | "import" 9 | ], 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/eslint-recommended", 13 | "plugin:@typescript-eslint/recommended" 14 | ], 15 | "globals": { 16 | "$": true, 17 | "document": true, 18 | "window": true, 19 | "top": true, 20 | "KeyboardEvent": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | build/ 4 | key.pem 5 | *.log 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.18.1 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pratyush Verma 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 | WeSiSho 2 | === 3 | 4 | **We**b **Si**tes **Sho**rtcuts is a chrome extension for adding shortcuts and replaying click actions inside websites. 5 | 6 | Save a location on a website with a sequence. Later, press the leader(default `,`) and the sequence to access the location. Each page have their own sequence, i.e. same sequence keys defined for `github` and `medium` can perform different tasks, if specified. 7 | 8 | `h` is reserved for home. So pressing ` h` will take you to the home page of the currently opened website. 9 | 10 | ![Imgur](https://i.imgur.com/MeHnkde.gif) 11 | 12 | Record complex click actions, like logging or opening settings, and save them to replay them later by just using few keys. 13 | 14 | ![Play recorded sequence](https://i.imgur.com/HuI5MsL.gif) 15 | 16 | To build from source use, 17 | 18 | > yarn prod 19 | -------------------------------------------------------------------------------- /icons/wesisho128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-v/WeSiSho/3842064fb5adb75b51bdd4092760da135db22945/icons/wesisho128.png -------------------------------------------------------------------------------- /icons/wesisho16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-v/WeSiSho/3842064fb5adb75b51bdd4092760da135db22945/icons/wesisho16.png -------------------------------------------------------------------------------- /icons/wesisho48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-v/WeSiSho/3842064fb5adb75b51bdd4092760da135db22945/icons/wesisho48.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WeSiSho", 3 | "version": "1.1.0", 4 | "description": "A chrome extension for adding shortcuts inside websites", 5 | "scripts": { 6 | "clean": "rm -rf build", 7 | "debug": "mkdir -p ./build/debug && webpack --config ./webpack/webpack.config.debug.js", 8 | "dev": "mkdir -p ./build/dev && webpack --config ./webpack/webpack.config.dev.js", 9 | "lint": "eslint ./src --ext .ts", 10 | "prod": "mkdir -p ./build/prod && webpack --config ./webpack/webpack.config.prod.js", 11 | "crx": "npm run prod && cd build && crx pack prod -p ../key.pem -o WeSiSho-$npm_package_version.crx" 12 | }, 13 | "author": "p-v", 14 | "license": "MIT", 15 | "dependencies": { 16 | "css-selector-generator": "^2.1.1", 17 | "react": "^16.2.0", 18 | "react-dom": "^16.13.1", 19 | "sweetalert2": "^6.6.9" 20 | }, 21 | "devDependencies": { 22 | "@types/chrome": "^0.0.116", 23 | "@types/react": "^16.9.38", 24 | "@types/react-dom": "^16.9.8", 25 | "@typescript-eslint/eslint-plugin": "^3.3.0", 26 | "@typescript-eslint/parser": "^3.3.0", 27 | "copy-webpack-plugin": "^4.5.0", 28 | "css-loader": "^3.6.0", 29 | "eslint": "^7.3.0", 30 | "eslint-config-airbnb": "^12.0.0", 31 | "eslint-plugin-import": "^2.0.1", 32 | "eslint-plugin-jsx-a11y": "^2.2.3", 33 | "eslint-plugin-react": "^6.4.1", 34 | "file-loader": "^1.1.11", 35 | "html-webpack-plugin": "^4.3.0", 36 | "lodash": "^4.17.19", 37 | "style-loader": "^0.20.2", 38 | "ts-loader": "^7.0.5", 39 | "typescript": "^3.9.5", 40 | "uglifyjs-webpack-plugin": "^1.2.4", 41 | "webpack": "^4.43.0", 42 | "webpack-cli": "^3.3.12" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import getCssSelector from 'css-selector-generator'; 2 | import Handler from './url-handler'; 3 | import Logger from './logger'; 4 | 5 | interface KeyIndex { 6 | [key: string]: boolean; 7 | } 8 | 9 | const KEY_TIMEOUT = 1000; // 2 seconds 10 | 11 | /* 12 | * Get leader key and timeout from chrome storage. 13 | * key Defaults to ',' 14 | * timeout Defaults to '1000' 15 | */ 16 | let leaderKey = ','; 17 | let leaderTimeout = KEY_TIMEOUT; 18 | chrome.storage.local.get(['leader_key', 'key_timeout'], (res) => { 19 | leaderKey = res.leader_key || ','; 20 | leaderTimeout = res.key_timeout || KEY_TIMEOUT; 21 | }); 22 | 23 | 24 | let leaderTimeoutId: number; 25 | let sequenceTimeoutId: number; 26 | let isListeningForKeyPresses = false; 27 | let sequence: number[] = []; // The sequence of characters entered after pressing leader 28 | const handled: KeyIndex = {}; 29 | 30 | window.addEventListener('keyup', (e) => { 31 | if (handled[e.key]) { 32 | e.preventDefault(); 33 | e.stopImmediatePropagation(); 34 | delete handled[e.key]; 35 | } 36 | }); 37 | 38 | 39 | window.addEventListener('keydown', (e) => { 40 | const activeElement = document.activeElement as HTMLElement; 41 | if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable) return; 42 | 43 | if (!isListeningForKeyPresses && e.key === leaderKey) { 44 | handled[e.key] = true; 45 | e.preventDefault(); 46 | e.stopImmediatePropagation(); 47 | 48 | // Now listening for sequence 49 | isListeningForKeyPresses = true; 50 | sequence = []; 51 | 52 | Logger.log('Leader timeout started for 1 secs'); 53 | 54 | leaderTimeoutId = setTimeout(() => { 55 | Logger.log('Leader timed out'); 56 | isListeningForKeyPresses = false; 57 | }, leaderTimeout); 58 | } else if (isListeningForKeyPresses && e.keyCode !== null) { 59 | handled[e.key] = true; 60 | e.preventDefault(); 61 | e.stopImmediatePropagation(); 62 | Logger.log('Leader and sequence timeout cleared'); 63 | clearTimeout(leaderTimeoutId); 64 | if (sequenceTimeoutId) { 65 | clearTimeout(sequenceTimeoutId); 66 | } 67 | sequence.push(e.keyCode); 68 | isListeningForKeyPresses = true; 69 | 70 | sequenceTimeoutId = setTimeout(() => { 71 | Logger.log('Sequence timed out'); 72 | isListeningForKeyPresses = false; 73 | const charSequence = sequence.map(code => String.fromCharCode(code).toLowerCase()); 74 | const key = charSequence.join(''); 75 | chrome.runtime.sendMessage({ key, shift: false }); 76 | }, leaderTimeout); 77 | } 78 | }, true); 79 | 80 | 81 | chrome.runtime.onMessage.addListener(Handler.onUrlReceive); 82 | 83 | document.addEventListener('mousedown', (event) => { 84 | if (event.button === 0) { 85 | const clickedEl = event.target as HTMLElement; 86 | chrome.storage.local.get(['recording', 'activeRecording'], (res) => { 87 | const isRecording = res && res.recording; 88 | if (isRecording) { 89 | const selector = getCssSelector(clickedEl); 90 | 91 | const activeRecording = (res && res.activeRecording) || []; 92 | chrome.storage.local.set({ activeRecording: [...activeRecording, selector] }); 93 | } 94 | }); 95 | } 96 | }, true); 97 | -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessage.addListener((request, sender) => { 2 | if (request.type === 'execute') { 3 | const { sequenceIndex, sequence } = request; 4 | if (sequenceIndex !== -1 && sequenceIndex < sequence.length) { 5 | setTimeout(() => { 6 | if (sender.tab && sender.tab.id) { 7 | chrome.tabs.sendMessage(sender.tab.id, { 8 | action: 'execute_command', 9 | sequenceIndex, 10 | sequence, 11 | }); 12 | } 13 | }, sequenceIndex === 0 ? 0 : 1000); 14 | } 15 | } else { 16 | chrome.tabs.query({ 17 | active: true, 18 | lastFocusedWindow: true, 19 | }, (tabs) => { 20 | const url = tabs[0].url; 21 | if (tabs[0].id) { 22 | chrome.tabs.sendMessage(tabs[0].id, { 23 | url, 24 | action: 'perform', 25 | key: request.key, 26 | }); 27 | } 28 | }); 29 | } 30 | }); 31 | 32 | // Reset `recording` flag on tag change 33 | chrome.tabs.onActivated.addListener(() => { 34 | chrome.storage.local.set({ 35 | recording: false, 36 | activeRecording: [], 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/css/wesisho.global.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #292826; 3 | } 4 | textarea:focus, input:focus{ 5 | outline: none; 6 | } 7 | 8 | input { 9 | border: 1px solid #292826; 10 | padding: 5px; 11 | border-radius: 4px; 12 | font-family: Arial, Helvetica, sans-serif; 13 | } 14 | 15 | textarea { 16 | min-height: 50px; 17 | border: 1px solid #292826; 18 | padding: 5px; 19 | resize: none; 20 | border-radius: 4px; 21 | font-family: Arial, Helvetica, sans-serif; 22 | } 23 | 24 | button { 25 | opacity: 1; 26 | border: none; 27 | background: #f9d342; 28 | color: #292826; 29 | border-radius: 2px; 30 | padding: 5px; 31 | min-width: 100px; 32 | } 33 | 34 | button:hover { 35 | opacity: 0.75; 36 | } 37 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | const DEBUG = true; 2 | const log = (msg: string): void => { 3 | if (DEBUG) { 4 | console.log(msg); 5 | } 6 | }; 7 | 8 | export default { log }; 9 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "WeSiSho", 4 | "description": "A chrome extension for adding shortcuts inside websites", 5 | "version": "2.0", 6 | 7 | "browser_action": { 8 | "default_popup": "html/popup.html" 9 | }, 10 | "permissions": ["activeTab", "storage", "tabs"], 11 | "content_scripts": [ 12 | { 13 | "matches": ["http://*/*", "https://*/*"], 14 | "css": ["css/sweetalert2.min.css"], 15 | "js": ["js/app.js"], 16 | "run_at": "document_start" 17 | } 18 | ], 19 | "background": { 20 | "scripts": ["js/background.js"] 21 | }, 22 | "options_ui": { 23 | "page": "html/options.html", 24 | "chrome_style": true, 25 | "open_in_tab": true 26 | }, 27 | "icons": { 28 | "16": "icons/wesisho16.png", 29 | "48": "icons/wesisho48.png", 30 | "128": "icons/wesisho128.png" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/options/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {createRef} from 'react'; 2 | import { render } from 'react-dom'; 3 | import Style from './style.css' 4 | import WebShortcuts from './web-shortcuts'; 5 | import { showErrorMessage, showSuccessMessage } from '../utils'; 6 | 7 | const INVALID_KEY = 'Please enter a valid key'; 8 | const LEADER_SAVED = 'Leader key saved'; 9 | const LEADER_TIMEOUT_SAVED = 'Key timeout saved'; 10 | 11 | const DEFAULT_TIMEOUT = 1000; 12 | 13 | type OptionsState = { 14 | }; 15 | 16 | class Main extends React.Component { 17 | private leaderInput = createRef(); 18 | private timeoutSelector = createRef(); 19 | 20 | constructor(props: unknown) { 21 | super(props); 22 | this.onLeaderSave = this.onLeaderSave.bind(this); 23 | this.onLeaderTimeoutChange = this.onLeaderTimeoutChange.bind(this); 24 | } 25 | 26 | componentDidMount() { 27 | // configure leader key 28 | chrome.storage.local.get(['leader_key', 'key_timeout'], (res) => { 29 | if (res) { 30 | const leaderNode = this.leaderInput.current; 31 | const timeoutNode = this.timeoutSelector.current; 32 | 33 | if (leaderNode) { 34 | leaderNode.value = res.leader_key || ','; 35 | } 36 | 37 | if (timeoutNode) { 38 | timeoutNode.value = res.key_timeout || DEFAULT_TIMEOUT; 39 | } 40 | } 41 | }); 42 | } 43 | 44 | onLeaderSave() { 45 | // get leader value 46 | const leaderNode = this.leaderInput.current; 47 | const leaderVal: string|null = leaderNode && leaderNode.value; 48 | 49 | if (leaderVal) { 50 | chrome.storage.local.set({ leader_key: leaderVal }, () => { 51 | showSuccessMessage(LEADER_SAVED); 52 | }); 53 | } else { 54 | showErrorMessage(INVALID_KEY); 55 | chrome.storage.local.remove('leader_key', () => { 56 | const error = chrome.runtime.lastError; 57 | if (error) { 58 | console.error(error); 59 | } 60 | }); 61 | } 62 | } 63 | 64 | onLeaderTimeoutChange(e: React.FormEvent) { 65 | const target = e.target as HTMLSelectElement; 66 | const value = target.value; 67 | chrome.storage.local.set({ key_timeout: Number(value) }, () => { 68 | showSuccessMessage(LEADER_TIMEOUT_SAVED); 69 | }); 70 | } 71 | 72 | render() { 73 | return ( 74 |
75 |

WeSiSho Options

76 |
77 | 78 | 79 | 80 | 83 | 92 | 97 | 98 | 99 | 102 | 115 | 116 | 117 |
81 |

Configure Leader Key:

82 |
84 | 91 | 93 | 96 |
100 |

{'Key timeout:'}

101 |
103 | 114 |
118 | 119 |
120 | ); 121 | } 122 | 123 | } 124 | 125 | render( 126 |
, 127 | document.getElementById('root') 128 | ); 129 | -------------------------------------------------------------------------------- /src/options/shortcut-item.tsx: -------------------------------------------------------------------------------- 1 | import React, {createRef} from 'react'; 2 | 3 | type ShortcutItemState = { 4 | showSaveIcon: boolean; 5 | } 6 | 7 | type ShortcutItemProps = { 8 | base: string; 9 | description: string; 10 | shortcut: string; 11 | title: string; 12 | onUpdateClick: (a: string, b: string, c: string) => void; 13 | onRemoveClick: (a: string, b: string) => void; 14 | } 15 | 16 | export default class ShortcutItem extends React.Component { 17 | private shortcutInput = createRef(); 18 | 19 | constructor(props: ShortcutItemProps) { 20 | super(props); 21 | this.state = { 22 | showSaveIcon: false, 23 | }; 24 | this.onChange = this.onChange.bind(this); 25 | this.onSave = this.onSave.bind(this); 26 | } 27 | 28 | onChange() { 29 | if (!this.state.showSaveIcon) { 30 | this.setState({ showSaveIcon: true }); 31 | } 32 | } 33 | 34 | onSave() { 35 | if (this.shortcutInput.current) { 36 | this.props.onUpdateClick(this.props.base, this.props.shortcut, 37 | this.shortcutInput.current.value); 38 | } 39 | } 40 | 41 | render() { 42 | const { shortcut, title, description, base, onRemoveClick } = this.props; 43 | const { showSaveIcon } = this.state; 44 | 45 | const displayTitle = description ? `${description} (${title})` : title; 46 | return ( 47 |
48 |
{displayTitle}
49 | 57 | { showSaveIcon && } 58 | 59 |
60 | ); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/options/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 15px; 3 | } 4 | 5 | .sites { 6 | font-size: 14px; 7 | cursor: pointer; 8 | user-select: none; 9 | } 10 | 11 | .leader-input { 12 | width: 100px; 13 | } 14 | 15 | .shortcuts-root { 16 | margin-top: 40px; 17 | } 18 | 19 | .arrow-up { 20 | border-bottom: 4px solid black; 21 | } 22 | 23 | .arrow-down { 24 | border-top: 4px solid black; 25 | } 26 | 27 | .arrow-up, .arrow-down { 28 | border-left: 4px solid transparent; 29 | border-right: 4px solid transparent; 30 | display: inline-block; 31 | width: 0; 32 | height: 0; 33 | margin-left: 10px; 34 | margin-bottom: 2px; 35 | } 36 | 37 | .group-title > * { 38 | display: inline-block; 39 | } 40 | -------------------------------------------------------------------------------- /src/options/style.css.d.ts: -------------------------------------------------------------------------------- 1 | export const sites: string; 2 | export const arrowUp: string; 3 | export const arrowDown: string; 4 | export const groupTitle: string; 5 | export const shortcutsRoot: string; 6 | export const leaderInput: string; 7 | -------------------------------------------------------------------------------- /src/options/web-shortcut-group.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { arrowUp, arrowDown, groupTitle } from './style.css'; 3 | import ShortcutItem from './shortcut-item'; 4 | 5 | type Shortcut = { 6 | url: string; 7 | description: string; 8 | } 9 | 10 | export interface ShortcutIndex { 11 | [key: string]: Shortcut; 12 | } 13 | 14 | type WebShortcutGroupProps = { 15 | base: string; 16 | shortcuts: ShortcutIndex; 17 | onRemoveClick: (a: string, b: string) => void 18 | onUpdateClick: (a: string, b: string, c: string) => void 19 | } 20 | 21 | type WebShortcutGroupState = { 22 | expanded: boolean; 23 | } 24 | 25 | export default class WebShortcutGroup extends React.Component { 26 | 27 | constructor(props: WebShortcutGroupProps) { 28 | super(props); 29 | this.state = { 30 | expanded: false, 31 | }; 32 | this.toggleItem = this.toggleItem.bind(this); 33 | } 34 | 35 | toggleItem() { 36 | this.setState({ expanded: !this.state.expanded }); 37 | } 38 | 39 | render() { 40 | const { base, shortcuts, onRemoveClick, onUpdateClick } = this.props; 41 | const { expanded } = this.state; 42 | 43 | const rows = Object.keys(shortcuts) 44 | .map(shortcut => 45 | 54 | ); 55 | 56 | return ( 57 |
58 |
59 |

{base}

60 |
61 |
62 |
63 | {expanded && rows} 64 |
65 | ); 66 | } 67 | 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/options/web-shortcuts.tsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import swal from 'sweetalert2'; 3 | import WebShortcutGroup from './web-shortcut-group'; 4 | import { shortcutsRoot } from './style.css'; 5 | import { showConfirmationMessage } from '../utils'; 6 | 7 | const WESISHO = 'WeSiSho'; 8 | const SETTINGS_SAVED = 'Settings Saved'; 9 | 10 | export interface Shortcut { 11 | url: string; 12 | description: string; 13 | } 14 | 15 | export interface ShortcutIndex { 16 | [key: string]: Shortcut; 17 | } 18 | 19 | export interface SiteIndex { 20 | [key: string]: ShortcutIndex; 21 | } 22 | 23 | type WebShortcutsProps = { 24 | 25 | } 26 | 27 | type WebShortcutsState = { 28 | showLoader: boolean; 29 | shortcuts: SiteIndex; 30 | } 31 | 32 | export default class WebShortcuts extends Component { 33 | private _isMounted = false; 34 | 35 | constructor(props: WebShortcutsProps) { 36 | super(props); 37 | this.state = { 38 | showLoader: true, 39 | shortcuts: {}, 40 | }; 41 | this.onRemoveClick = this.onRemoveClick.bind(this); 42 | this.onUpdateClick = this.onUpdateClick.bind(this); 43 | } 44 | 45 | componentDidMount(): void { 46 | this._isMounted = true; 47 | chrome.storage.local.get('shortcuts', (result) => { 48 | const shortcuts = result.shortcuts; 49 | if (this._isMounted) { 50 | this.setState({ shortcuts, showLoader: false }); 51 | } 52 | }); 53 | } 54 | 55 | componentWillUnmount(): void { 56 | this._isMounted = false; 57 | } 58 | 59 | updateShortcut(key: string, prevShortcut: string, newShortcut: string, newShortcuts: SiteIndex): void { 60 | newShortcuts[key][newShortcut] = newShortcuts[key][prevShortcut]; 61 | delete newShortcuts[key][prevShortcut]; 62 | 63 | this.setState({ shortcuts: newShortcuts }); 64 | 65 | chrome.storage.local.get('shortcuts', (result) => { 66 | const storageShortcuts = result.shortcuts; 67 | storageShortcuts[key][newShortcut] = storageShortcuts[key][prevShortcut]; 68 | delete storageShortcuts[key][prevShortcut]; 69 | // update local storage 70 | chrome.storage.local.set({ shortcuts: storageShortcuts }, () => { 71 | // Notify that we saved. 72 | swal(WESISHO, SETTINGS_SAVED, 'success'); 73 | }); 74 | }); 75 | } 76 | 77 | onUpdateClick(key: string, prevShortcut: string, newShortcut: string): void { 78 | const { shortcuts } = this.state; 79 | 80 | const newShortcuts = { 81 | ...shortcuts, 82 | }; 83 | 84 | // Check if shortcut already exist 85 | if (newShortcuts[key][newShortcut]) { 86 | showConfirmationMessage('Shortcut already exist', 'Do you want to replace it?', () => { 87 | delete newShortcuts[key][newShortcut]; 88 | this.updateShortcut(key, prevShortcut, newShortcut, newShortcuts); 89 | }); 90 | } 91 | 92 | this.updateShortcut(key, prevShortcut, newShortcut, newShortcuts); 93 | } 94 | 95 | onRemoveClick(key: string, shortcut: string): void { 96 | const { shortcuts } = this.state; 97 | 98 | const newShortcuts = { 99 | ...shortcuts, 100 | }; 101 | 102 | let deleteKey = false; 103 | 104 | if (newShortcuts[key] && Object.values(newShortcuts[key]).length === 1) { 105 | delete newShortcuts[key]; 106 | deleteKey = true; 107 | } else { 108 | delete newShortcuts[key][shortcut]; 109 | } 110 | 111 | this.setState({ shortcuts: newShortcuts }); 112 | 113 | chrome.storage.local.get('shortcuts', (result) => { 114 | const storageShortcuts = result.shortcuts; 115 | if (deleteKey) { 116 | delete storageShortcuts[key]; 117 | } else { 118 | delete storageShortcuts[key][shortcut]; 119 | } 120 | 121 | // update local storage 122 | chrome.storage.local.set({ shortcuts: storageShortcuts }, () => { 123 | // Notify that we saved. 124 | swal(WESISHO, SETTINGS_SAVED, 'success'); 125 | }); 126 | }); 127 | } 128 | 129 | render() { 130 | const { showLoader, shortcuts } = this.state; 131 | if (showLoader) { 132 | return (

Loading...

); 133 | } 134 | 135 | if (!shortcuts || Object.keys(shortcuts).length === 0) { 136 | return (

{'No web shortcuts set so far'}

); 137 | } 138 | 139 | const rows = Object.keys(shortcuts) 140 | .map(shortcut => 141 | 148 | ); 149 | 150 | return ( 151 |
152 |

Configure Shortcuts:

153 | {rows} 154 |
155 | ); 156 | } 157 | 158 | } 159 | 160 | -------------------------------------------------------------------------------- /src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/popup/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {Component, createRef} from 'react'; 2 | import { render } from 'react-dom'; 3 | import '../css/wesisho.global.css'; 4 | import { shortcutInput, shortcutDescription, saveButton, optionContainer, cancelButton } from './style.css'; 5 | 6 | const SHORTCUT_EMPTY = 'Oops! Looks like no shortcut has been set'; 7 | 8 | type PopupState = { 9 | addShortcut: boolean; 10 | createShortcut: boolean; 11 | recording: boolean; 12 | selectors: string[]; 13 | refUrl: string; 14 | } 15 | 16 | class Main extends Component { 17 | 18 | private refUrlInput = createRef(); 19 | private shortcutInput = createRef(); 20 | private shortcutDescInput = createRef(); 21 | private _isMounted = false; 22 | 23 | constructor(props: unknown) { 24 | super(props); 25 | this.state = { 26 | addShortcut: false, 27 | createShortcut: false, 28 | recording: false, 29 | selectors: [], 30 | refUrl: '', 31 | }; 32 | this.onAddButtonClick = this.onAddButtonClick.bind(this); 33 | this.onSaveShortcutClick = this.onSaveShortcutClick.bind(this); 34 | this.onSaveRecordingClick = this.onSaveRecordingClick.bind(this); 35 | this.onRecordClick = this.onRecordClick.bind(this); 36 | this.onCreateShortcutClick = this.onCreateShortcutClick.bind(this); 37 | this.onCancelSaveShortcutClick = this.onCancelSaveShortcutClick.bind(this); 38 | this.onRefUrlChange = this.onRefUrlChange.bind(this); 39 | } 40 | 41 | componentDidMount() { 42 | this._isMounted = true; 43 | chrome.storage.local.get('recording', (res) => { 44 | if (this._isMounted && res) { 45 | this.setState({ recording: res.recording }); 46 | } 47 | }); 48 | } 49 | 50 | componentWillUnmount() { 51 | this._isMounted = false; 52 | } 53 | 54 | onAddButtonClick() { 55 | this.setState({ addShortcut: true }); 56 | } 57 | 58 | 59 | onSaveShortcutClick() { 60 | this.setState({ addShortcut: false }); 61 | 62 | if (!this.shortcutInput.current || !this.shortcutDescInput.current) { 63 | return; 64 | } 65 | 66 | const keyValue = this.shortcutInput.current.value; 67 | const description = this.shortcutDescInput.current.value; 68 | 69 | if (!keyValue) { 70 | chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => { 71 | if (tabs[0].id) { 72 | chrome.tabs.sendMessage(tabs[0].id, { action: 'error', message: SHORTCUT_EMPTY }); 73 | } 74 | }); 75 | return; 76 | } 77 | 78 | window.close(); // Close extension popup 79 | 80 | chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => { 81 | const url = tabs[0].url; 82 | const key = keyValue; 83 | if (tabs[0].id) { 84 | chrome.tabs.sendMessage(tabs[0].id, { action: 'save_url', url, key, description }); 85 | } 86 | }); 87 | } 88 | 89 | onSaveRecordingClick() { 90 | if (!this.shortcutInput.current || !this.shortcutDescInput.current) { 91 | return; 92 | } 93 | const key = this.shortcutInput.current.value; 94 | const description = this.shortcutDescInput.current.value; 95 | 96 | if (!key) { 97 | chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => { 98 | if (tabs[0].id) { 99 | chrome.tabs.sendMessage(tabs[0].id, { action: 'error', message: SHORTCUT_EMPTY }); 100 | } 101 | }); 102 | return; 103 | } 104 | 105 | const { refUrl, selectors } = this.state; 106 | 107 | this.setState({ 108 | addShortcut: false, 109 | createShortcut: false, 110 | recording: false, 111 | selectors: [], 112 | refUrl: '', 113 | }); 114 | 115 | window.close(); // Close extension popup 116 | 117 | chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => { 118 | if (tabs[0].id) { 119 | chrome.tabs.sendMessage(tabs[0].id, { action: 'save_recording', key, description, refUrl, selectors }); 120 | } 121 | }); 122 | } 123 | 124 | onRecordClick() { 125 | this.setState({ recording: !this.state.recording }); 126 | 127 | if (!this.state.recording) { 128 | chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => { 129 | const url = tabs[0].url; 130 | chrome.storage.local.set({ recording: true, refUrl: url, activeRecording: [] }); 131 | }); 132 | } else { 133 | // Fetch the active recording and append the current selector 134 | chrome.storage.local.get(['activeRecording', 'refUrl'], (recRes) => { 135 | // Clear recording 136 | chrome.storage.local.set({ recording: false, activeRecording: [] }); 137 | 138 | const activeRecording = recRes && recRes.activeRecording; 139 | if (activeRecording && activeRecording.length > 0) { 140 | this.setState({ selectors: activeRecording, refUrl: recRes.refUrl }); 141 | } 142 | }); 143 | } 144 | } 145 | 146 | onCreateShortcutClick() { 147 | this.setState({ createShortcut: true }); 148 | } 149 | 150 | onCancelSaveShortcutClick() { 151 | this.setState({ createShortcut: false, selectors: [], refUrl: '' }); 152 | } 153 | 154 | onRefUrlChange(e: React.FormEvent) { 155 | const target = e.target as HTMLInputElement; 156 | this.setState({ refUrl: target.value }); 157 | } 158 | 159 | renderView() { 160 | const { createShortcut, recording, selectors, refUrl } = this.state; 161 | 162 | if (selectors.length > 0) { 163 | const selectorRows = selectors.map((selector, idx) => 164 |
{selector}
165 | ); 166 | 167 | return ( 168 |
169 |

{'Save following sequence?'}

170 | {selectorRows} 171 | 180 | 187 |