├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── nodejs.yml ├── .gitignore ├── .swift-version ├── CHANGELOG.md ├── DEVELOPERS.md ├── LICENSE ├── Makefile ├── README.md ├── Vimari Extension ├── Base.lproj │ └── SafariExtensionViewController.xib ├── ConfigurationModel.swift ├── Info.plist ├── SafariExtensionHandler.swift ├── SafariExtensionViewController.swift ├── ToolbarItemIcon.pdf ├── Vimari_Extension.entitlements ├── css │ └── injected.css ├── js │ ├── SafariExtensionCommunicator.js │ ├── injected.js │ ├── keyboard-utils.js │ ├── lib │ │ ├── mousetrap.js │ │ ├── svim-scripts.js │ │ └── vimium-scripts.js │ ├── link-hints.js │ └── mocks.js └── json │ └── defaultSettings.json ├── Vimari.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Vimari ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ ├── 1024.png │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 256.png │ │ ├── 32.png │ │ ├── 512.png │ │ ├── 64.png │ │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Credits.rtf ├── Info.plist ├── ViewController.swift └── Vimari.entitlements ├── assets ├── Download_on_the_Mac_App_Store_Badge_US.svg ├── logo.sketch ├── logo.svg ├── screenshot.png ├── screenshot.psd └── vimlogo.svg ├── docs └── safari_12.md ├── jest.config.js ├── package-lock.json ├── package.json └── tests └── vimari.spec.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | 37 | 38 | - macOS version: 39 | - Safari version: 40 | - Vimari version: 41 | 42 | **Additional context** 43 | Add any other context about the problem here. 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: make all test 21 | run: | 22 | make all test 23 | env: 24 | CI: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | # Xcode 4 | ## Build generated 5 | build/ 6 | DerivedData/ 7 | 8 | ## Various settings 9 | xcuserdata/ 10 | 11 | ## Obj-C/Swift specific 12 | *.hmap 13 | *.ipa 14 | *.dSYM.zip 15 | *.dSYM 16 | 17 | ## Playgrounds 18 | timeline.xctimeline 19 | playground.xcworkspace 20 | 21 | .vscode/ 22 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ------------- 3 | 4 | ### 2.1.1 5 | 6 | * Rebuild for Apple Silicon 7 | * Fixes excludedUrls handling 8 | 9 | ### 2.1.0 10 | * Add `transparentBindings` setting that allows the use of non-bound keys in normal mode ([#188](https://github.com/televator-apps/vimari/issues/188)). 11 | * Remove eager link hint triggering [#190](https://github.com/televator-apps/vimari/issues/190) 12 | * Use `window.open` for `openNewTab` action [#189](https://github.com/televator-apps/vimari/issues/189) 13 | * Add user customisation (based on the work of @nieldm [#163](https://github.com/televator-apps/vimari/pull/163)). 14 | * Update Vimari interface to allow users access to their configuration. 15 | * Remove `closeTabReverse` action. 16 | * Normal mode now isolates keybindings from the underlying website, this means that to interact with the underlying website you need to enter insert mode. 17 | * You can enter insert mode by pressing i and exit the mode by pressing esc. Activating either mode will display the HUD. 18 | * In insert mode Vimari keybindings are disabled (except for esc which brings you back to normal mode) allowing you to interact with the underlying website. 19 | * Add `goToFirstInput` action on g i by default (by [isundaylee](https://github.com/isundaylee)). 20 | * Add smooth scrolling (based on sVim implementation). 21 | 22 | ### 2.0.3 (2019-09-26) 23 | 24 | * Fix newTabHintToggle to use shift+f instead of F 25 | * Implement forward tab and backward tab commands. 26 | * Close tab with x is now implemented. Note that this relies on Safari's default behaviour to choose whether to switch to the left or right tab after closing the current tab. 27 | 28 | ### 2.0.2 (2019-09-23) 29 | 30 | * Release a signed, notarized App and Safari App Extension 31 | * Reverse link hints, so nearby links have different hints [#77](https://github.com/televator-apps/vimari/issues/77) 32 | * Hide non-matching link hints [#79](https://github.com/televator-apps/vimari/issues/79) 33 | * Show state of extension in main application 34 | 35 | ### 2.0.0 (14/7/2018) 36 | * vimari now exists as a Safari App Extension, making it compatible with Safari 37 | version 12 38 | 39 | ### 1.13.0 (16/8/2018) 40 | * New fresh icon 41 | * Removed shift as default modifier key 42 | * 't' now opens new tab 43 | * HUD now looks nicer 44 | * Open link in new tab now works (bugfix) 45 | * Excluded URL doesn't need to be exact anymore (bugfix) 46 | 47 | ### 1.2 - 1.12 skipped 48 | 49 | ### 1.1 (31/07/2011) 50 | * Updated to work with the new version of Safari on lion 51 | * Removed history forward / back 52 | * Changed directory structure to make it more developer friendly 53 | 54 | ### 1.0 (21/11/2010) 55 | * Changed the way vimari modifier keys work. ESC key depricated. Now use CTRL-modifierkey. 56 | 57 | ### 0.4 (17/11/2010) 58 | * First BETA release ! 59 | * Press ESC to enter a permanent state of 'non' insert mode. Clicking on any input then exits insert mode. This fixes several issues with google and facebook. 60 | 61 | ### 0.3 (16/11/2010) 62 | * Moved the extension startup code to be loaded before the browser page. Events can now be intercepted before they are passed to the browser page. 63 | * Created a manifest file, this allows automatic updates to take place. 64 | * Added insert mode. If the selected node can accept an input, the extension is disabled. This functionality still needs some work. 65 | * Ported the HUD from vimium. The hud displays information along the bottom of the screen. The hud has been ported but is not used for very much at the moment. 66 | 67 | ### 0.2 (14/11/2010) 68 | * Pressing ESC now removes focus from any input fields and activates modifiers 69 | 70 | ### 0.1 (14/11/1020) 71 | * First alpa release of vimari. Added basic features but still very buggy. 72 | -------------------------------------------------------------------------------- /DEVELOPERS.md: -------------------------------------------------------------------------------- 1 | # Developers 2 | 3 | ## Setup 4 | 5 | ### Local setup 6 | 7 | 1. Clone the repository: 8 | ```bash 9 | git clone git@github.com:televator-apps/vimari.git 10 | ``` 11 | 2. Open `Vimari.xcodeproj` with Xcode. 12 | 3. [Set your signing team](https://help.apple.com/xcode/mac/current/#/dev23aab79b4) for both targets (Vimari and Vimari Extension). 13 | 4. Run the project (+R). 14 | 15 | You might have to reload the website you had open for the changes to take effect. Also check your configuration file as it currently does not get upgraded automatically. 16 | 17 | ### Linting & Formatting 18 | 19 | Code linting and formatting will be implemented in [#193](https://github.com/televator-apps/vimari/pull/193). 20 | 21 | ## Contributing 22 | 23 | If you'd like to contribute to the development of Vimari you can help us out through several means: 24 | 25 | 1. Create bug reports for issues you encounter, or look trough existing bug reports and try to reproduce their problems. 26 | 2. Try out the latest beta version (if there is one) and report issues back to us. 27 | 3. Contribute ideas, if you'd like something to be added to Vimari you can create an issue describing exactly what you have in mind. Together we can help form the idea and get it into Vimari. 28 | 4. Contribute code, if you find a bug or issue that you think you can help us solve you are more than welcome to do so. 29 | 30 | ### Contributing Code 31 | 32 | If you want to contribute to Vimari through coding you have to start by selecting an issue to work on. If you'd like to contribute something new, make an issue first to discuss the idea. 33 | 34 | You can fork the Vimari source code and make the changes to implement your feature or solve a bug. Once finished you can create a pull request back into the Vimari repository where it can be reviewed. 35 | 36 | After a successful review your code will be merged with the master branch and released to Vimari users in the next release. Pretty cool! 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Phil Crosby, Ilya Sukhar. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test test-watch 2 | 3 | NPM=$(shell which npm) 4 | NPM_BIN=$(shell npm bin) 5 | 6 | all: deps 7 | 8 | deps: 9 | @$(NPM) install 10 | 11 | test: 12 | @$(NPM_BIN)/jest tests 13 | 14 | test-watch: 15 | @$(NPM_BIN)/jest --watch tests 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Vimari 4 | _Keyboard Shortcuts extension for Safari_ 5 | 6 | [![Download on the Mac App Store](assets/Download_on_the_Mac_App_Store_Badge_US.svg)](https://apps.apple.com/us/app/vimari/id1480933944?ls=1&mt=12) 7 | 8 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/televator-apps/vimari) 9 | ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/televator-apps/vimari?include_prereleases&label=pre%20release) 10 | 11 | Vimari is a Safari extension that provides vim style keyboard based navigation. 12 | This lets you control Safari from your keyboard instead of having to use your mouse to open links, scroll, etc. 13 | The code is heavily based on [vimium](https://github.com/philc/vimium), a 14 | Chrome extension that provides much more extensive features. 15 | 16 | Vimari attempts to provide a lightweight port of vimium to Safari, taking the 17 | best components of vimium and adapting them to Safari. 18 | 19 | 20 | 21 | ## Releases 22 | 23 | ### Safari 12 and above 24 | 25 | [![Download on the Mac App Store](assets/Download_on_the_Mac_App_Store_Badge_US.svg)](https://apps.apple.com/us/app/vimari/id1480933944?ls=1&mt=12) 26 | 27 | ### Safari 11 and below (DEPRECATED) 28 | - [v1.13](https://github.com/guyht/vimari/releases/tag/v1.13) 29 | - [v1.12](https://github.com/guyht/vimari/releases/tag/v1.12) 30 | - [v1.11](https://github.com/guyht/vimari/releases/tag/v1.11) 31 | - [v1.10](https://github.com/guyht/vimari/releases/tag/v1.10) 32 | - [v1.9](https://github.com/guyht/vimari/releases/tag/v1.9) 33 | 34 | ## Installation 35 | 36 | ### Safari 12 and above (macOS Mojave or above) 37 | 38 | #### Mac App Store 39 | 40 | 1. [Download Vimari](https://apps.apple.com/us/app/vimari/id1480933944?ls=1&mt=12) for free from the Mac App Store 41 | 2. Launch Vimari.app 42 | 3. Click "Open in Safari Extensions Preferences", Safari's Extension Preferences should open 43 | 4. Make sure that the checkbox for the Vimari extension is ticked 44 | 5. Go back to Vimari.app and press the reload button to check the status of the app. If it says "Enabled" then it is ready. 45 | 6. You may need to relaunch Safari for the extension to work 46 | 47 | #### Prebuilt binaries 48 | 49 | 1. Download the [latest version](https://github.com/guyht/vimari/releases/latest) of Vimari 50 | 2. Unzip it 51 | 3. Move it to your `/Applications` folder 52 | 4. Launch Vimari.app 53 | 5. Click "Open in Safari Extensions Preferences...", Safari's Extension Preferences should open 54 | 6. Make sure that the checkbox for the Vimari extension is ticked 55 | 7. Go back to Vimari.app and press the reload button to check the status of the app. If it says "Enabled" then it is ready. 56 | 8. You may need to relaunch Safari for the extension to work 57 | 58 | 59 | 60 | ### Safari 11 (legacy Safari Extension method) 61 | 62 | [Download the Vimari 1.13](https://github.com/guyht/vimari/releases/tag/v1.13) and double-click 63 | the file. 64 | 65 | ## Usage 66 | 67 | ### Settings 68 | **Modifier** - Modifier key to hold down with your action key. If 69 | you leave it blank you don't need to hold down anything (default 70 | setting). 71 | 72 | **Excluded URLs** - Comma separated list of website URLs you don't want 73 | to use vimari with. To exclude GitHub for example, provide the value 74 | `github.com` or `http://github.com`. It's smart and should handle all 75 | possible domain cases. 76 | 77 | **Link Hint Characters** - Allowed characters to be used when generating 78 | link shortcuts. 79 | 80 | **Extra detection by cursor style** - Detect clickable links by looking 81 | for HTML elements having cursor style set to "pointer". 82 | 83 | **Scroll Size** - How much each scroll will move on the page. 84 | 85 | `Vimari v2.1+` 86 | 87 | **Smooth Scroll** - Scroll smoothly through the page. 88 | 89 | **Normal vs Insert mode** - Isolate website keybindings from the 90 | Vimari keybindings. In normal mode you can use the Vimari keybindings 91 | while in insert mode you can use the websites own keybindings. 92 | 93 | **Transparent Bindings** - Full keybinding isolation might not 94 | be your style, instead the transparent bindings setting (when enabled) 95 | allows you to use all **non-Vimari-bound** keys to interact with the web 96 | page as if you were in insert mode. 97 | 98 | **Multiple Bindings** - You can bind multiple keybindings to a Vimari 99 | action. This is done by specifying an array of bindings in the 100 | configuration file, like so: `"goToPageTop": ["g g", "shift+k"]`. 101 | 102 | 103 | ### Keyboard Bindings 104 | 105 | These bindings are the ones set by default, however you are able to change them in the settings. 106 | 107 | #### In-page navigation 108 | f Toggle links 109 | F Toggle links (open link in new tab) 110 | k Scroll up 111 | j Scroll down 112 | h Scroll left 113 | l Scroll right 114 | u Scroll up half page 115 | d Scroll down half page 116 | g g Go to top of page 117 | G Go to bottom of page 118 | g i Go to first input 119 | 120 | #### Page/Tab navigation 121 | H History back 122 | L History forward 123 | r Reload 124 | w Next tab 125 | q Previous tab 126 | x Close current tab 127 | t Open new tab 128 | 129 | `Vimari v2.1+` 130 | 131 | #### Vimari Modes 132 | i Enter insert mode 133 | ESC Enter normal mode 134 | CTRL+[ Enter normal mode 135 | 136 | ### Tips & Tricks 137 | 138 | Vimari is built as a Safari Extension, this poses some limits on what is possible through the extension. However default Safari shortcuts can help you keep your hands at the keyboard. Some helpful ones are listed here: 139 | 140 | - **Focus URL Bar** l - This is a feature not available in Vimari, it is also helpful where extensions are not loaded (for example on `topsites://`). By focusing the URL Bar you can go to a website where the extension is loaded. 141 | 142 | - **Reader mode** R - Currently Vimari does not support entering Reader mode (due to API limitations), also navigation inside reader mode (for example using j or k) is not supported. 143 | 144 | - **Re-open last closed tab** T - Allows you to reopen a recently closed tab. 145 | 146 | ## License 147 | 148 | Copyright (C) 2011 Guy Halford-Thompson. See [LICENSE](LICENSE) for details. 149 | -------------------------------------------------------------------------------- /Vimari Extension/Base.lproj/SafariExtensionViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Vimari Extension/ConfigurationModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationModel.swift 3 | // Vimari Extension 4 | // 5 | // Created by Daniel Mendez on 12/15/19. 6 | // Copyright © 2019 net.televator. All rights reserved. 7 | // 8 | 9 | protocol ConfigurationModelProtocol { 10 | func editConfigFile() throws 11 | func resetConfigFile() throws 12 | func getDefaultSettings() throws -> [String: Any] 13 | func getUserSettings() throws -> [String : Any] 14 | } 15 | 16 | import Foundation 17 | import SafariServices 18 | 19 | class ConfigurationModel: ConfigurationModelProtocol { 20 | 21 | private enum Constant { 22 | static let settingsFileName = "defaultSettings" 23 | static let userSettingsFileName = "userSettings" 24 | static let defaultEditor = "TextEdit" 25 | } 26 | 27 | let userSettingsUrl: URL = FileManager.documentDirectoryURL 28 | .appendingPathComponent(Constant.userSettingsFileName) 29 | .appendingPathExtension("json") 30 | 31 | func editConfigFile() throws { 32 | let settingsFilePath = try findOrCreateUserSettings() 33 | NSWorkspace.shared.openFile( 34 | settingsFilePath, 35 | withApplication: Constant.defaultEditor 36 | ) 37 | } 38 | 39 | func resetConfigFile() throws { 40 | let settingsFilePath = try overwriteUserSettings() 41 | NSWorkspace.shared.openFile( 42 | settingsFilePath, 43 | withApplication: Constant.defaultEditor 44 | ) 45 | } 46 | 47 | func getDefaultSettings() throws -> [String : Any] { 48 | return try loadSettings(fromFile: Constant.settingsFileName) 49 | } 50 | 51 | func getUserSettings() throws -> [String : Any] { 52 | let userFilePath = try findOrCreateUserSettings() 53 | let urlSettingsFile = URL(fileURLWithPath: userFilePath) 54 | let settingsData = try Data(contentsOf: urlSettingsFile) 55 | return try settingsData.toJSONObject() 56 | } 57 | 58 | private func loadSettings(fromFile file: String) throws -> [String : Any] { 59 | let settingsData = try Bundle.main.getJSONData(from: file) 60 | return try settingsData.toJSONObject() 61 | } 62 | 63 | private func findOrCreateUserSettings() throws -> String { 64 | let url = userSettingsUrl 65 | let urlString = url.path 66 | if FileManager.default.fileExists(atPath: urlString) { 67 | return urlString 68 | } 69 | let data = try Bundle.main.getJSONData(from: Constant.settingsFileName) 70 | try data.write(to: url) 71 | return urlString 72 | } 73 | 74 | private func overwriteUserSettings() throws -> String { 75 | let url = userSettingsUrl 76 | let urlString = userSettingsUrl.path 77 | let data = try Bundle.main.getJSONData(from: Constant.settingsFileName) 78 | try data.write(to: url) 79 | return urlString 80 | } 81 | } 82 | 83 | enum DataError: Error { 84 | case unableToParse 85 | case notFound 86 | } 87 | 88 | private extension Data { 89 | func toJSONObject() throws -> [String: Any] { 90 | let serialized = try JSONSerialization.jsonObject(with: self, options: []) 91 | guard let result = serialized as? [String: Any] else { 92 | throw DataError.unableToParse 93 | } 94 | return result 95 | } 96 | } 97 | 98 | private extension Bundle { 99 | func getJSONPath(for file: String) throws -> String { 100 | guard let result = self.path(forResource: file, ofType: ".json") else { 101 | throw DataError.notFound 102 | } 103 | return result 104 | } 105 | 106 | func getJSONData(from file: String) throws -> Data { 107 | let settingsPath = try self.getJSONPath(for: file) 108 | let urlSettingsFile = URL(fileURLWithPath: settingsPath) 109 | return try Data(contentsOf: urlSettingsFile) 110 | } 111 | } 112 | 113 | private extension FileManager { 114 | static var documentDirectoryURL: URL { 115 | let documentDirectoryURL = try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false) 116 | return documentDirectoryURL 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Vimari Extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Vimari 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSExtension 26 | 27 | NSExtensionPointIdentifier 28 | com.apple.Safari.extension 29 | NSExtensionPrincipalClass 30 | $(PRODUCT_MODULE_NAME).SafariExtensionHandler 31 | SFSafariContentScript 32 | 33 | 34 | Script 35 | svim-scripts.js 36 | 37 | 38 | Script 39 | SafariExtensionCommunicator.js 40 | 41 | 42 | Script 43 | keyboard-utils.js 44 | 45 | 46 | Script 47 | vimium-scripts.js 48 | 49 | 50 | Script 51 | link-hints.js 52 | 53 | 54 | Script 55 | mousetrap.js 56 | 57 | 58 | Script 59 | injected.js 60 | 61 | 62 | SFSafariStyleSheet 63 | 64 | 65 | Style Sheet 66 | injected.css 67 | 68 | 69 | SFSafariToolbarItem 70 | 71 | Action 72 | Command 73 | Identifier 74 | Button 75 | Image 76 | ToolbarItemIcon.pdf 77 | Label 78 | Vimari Settings 79 | 80 | SFSafariWebsiteAccess 81 | 82 | Level 83 | All 84 | 85 | 86 | NSHumanReadableCopyright 87 | Copyright © 2019 Televator, Guy Halford-Thompson, Simon Egersand, Phil Crosby, Ilya Sukhar, and other contributors. MIT Licensed 88 | NSHumanReadableDescription 89 | Adds Vim keybindings to Safari. Vimari is a port of Vimium. 90 | 91 | 92 | -------------------------------------------------------------------------------- /Vimari Extension/SafariExtensionHandler.swift: -------------------------------------------------------------------------------- 1 | import SafariServices 2 | 3 | enum ActionType: String { 4 | case openLinkInTab 5 | case tabForward 6 | case tabBackward 7 | case closeTab 8 | case updateSettings 9 | } 10 | 11 | enum InputAction: String { 12 | case openSettings 13 | case resetSettings 14 | } 15 | 16 | enum TabDirection: String { 17 | case forward 18 | case backward 19 | } 20 | 21 | class SafariExtensionHandler: SFSafariExtensionHandler { 22 | 23 | private enum Constant { 24 | static let mainAppName = "Vimari" 25 | static let newTabPageURL = "https://duckduckgo.com" //Try it :D 26 | } 27 | 28 | let configuration: ConfigurationModelProtocol = ConfigurationModel() 29 | 30 | //MARK: Overrides 31 | 32 | // This method handles messages from the Vimari App (located /Vimari in the repository) 33 | override func messageReceivedFromContainingApp(withName messageName: String, userInfo: [String : Any]? = nil) { 34 | do { 35 | switch InputAction(rawValue: messageName) { 36 | case .openSettings: 37 | try configuration.editConfigFile() 38 | case .resetSettings: 39 | try configuration.resetConfigFile() 40 | case .none: 41 | NSLog("Input not supported " + messageName) 42 | } 43 | } catch { 44 | NSLog(error.localizedDescription) 45 | } 46 | 47 | } 48 | 49 | // This method handles messages from the extension (in the browser page) 50 | override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String: Any]?) { 51 | NSLog("Received message: \(messageName)") 52 | switch ActionType(rawValue: messageName) { 53 | case .openLinkInTab: 54 | let url = URL(string: userInfo?["url"] as! String) 55 | openInNewTab(url: url!) 56 | case .tabForward: 57 | changeTab(withDirection: .forward, from: page) 58 | case .tabBackward: 59 | changeTab(withDirection: .backward, from: page) 60 | case .closeTab: 61 | closeTab(from: page) 62 | case .updateSettings: 63 | updateSettings(page: page) 64 | case .none: 65 | NSLog("Received message with unsupported type: \(messageName)") 66 | } 67 | } 68 | 69 | override func toolbarItemClicked(in _: SFSafariWindow) { 70 | // This method will be called when your toolbar item is clicked. 71 | NSLog("The extension's toolbar item was clicked") 72 | NSWorkspace.shared.launchApplication(Constant.mainAppName) 73 | } 74 | 75 | override func validateToolbarItem(in _: SFSafariWindow, validationHandler: @escaping ((Bool, String) -> Void)) { 76 | // This is called when Safari's state changed in some way that would require the extension's toolbar item to be validated again. 77 | validationHandler(true, "") 78 | } 79 | 80 | override func popoverViewController() -> SFSafariExtensionViewController { 81 | return SafariExtensionViewController.shared 82 | } 83 | 84 | // MARK: Tabs Methods 85 | 86 | private func openInNewTab(url: URL) { 87 | SFSafariApplication.getActiveWindow { activeWindow in 88 | activeWindow?.openTab(with: url, makeActiveIfPossible: false, completionHandler: { _ in 89 | // Perform some action here after the page loads 90 | }) 91 | } 92 | } 93 | 94 | private func changeTab(withDirection direction: TabDirection, from page: SFSafariPage, completionHandler: (() -> Void)? = nil ) { 95 | page.getContainingTab() { currentTab in 96 | // Using .currentWindow instead of .containingWindow, this prevents the window being nil in the case of a pinned tab. 97 | self.currentWindow(from: page) { window in 98 | window?.getAllTabs() { tabs in 99 | tabs.forEach { tab in NSLog(tab.description) } 100 | if let currentIndex = tabs.firstIndex(of: currentTab) { 101 | let indexStep = direction == TabDirection.forward ? 1 : -1 102 | 103 | // Wrap around the ends with a modulus operator. 104 | // % calculates the remainder, not the modulus, so we need a 105 | // custom function. 106 | let newIndex = mod(currentIndex + indexStep, tabs.count) 107 | 108 | tabs[newIndex].activate(completionHandler: completionHandler ?? {}) 109 | 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | /** 117 | Returns the containing window of a SFSafariPage, if not available default to the current active window. 118 | */ 119 | private func currentWindow(from page: SFSafariPage, completionHandler: @escaping ((SFSafariWindow?) -> Void)) { 120 | page.getContainingTab() { $0.getContainingWindow() { window in 121 | if window != nil { 122 | return completionHandler(window) 123 | } else { 124 | SFSafariApplication.getActiveWindow() { window in 125 | return completionHandler(window) 126 | } 127 | } 128 | }} 129 | } 130 | 131 | private func closeTab(from page: SFSafariPage) { 132 | page.getContainingTab { 133 | tab in 134 | tab.close() 135 | } 136 | } 137 | 138 | // MARK: Settings 139 | 140 | private func getSetting(_ settingKey: String) -> Any? { 141 | do { 142 | let settings = try configuration.getUserSettings() 143 | return settings[settingKey] 144 | } catch { 145 | NSLog("Was not able to retrieve the user settings\n\(error.localizedDescription)") 146 | return nil 147 | } 148 | } 149 | 150 | private func updateSettings(page: SFSafariPage) { 151 | do { 152 | let settings: [String: Any] 153 | if let userSettings = try? configuration.getUserSettings() { 154 | settings = userSettings 155 | } else { 156 | settings = try configuration.getDefaultSettings() 157 | } 158 | page.dispatch(settings: settings) 159 | } catch { 160 | NSLog(error.localizedDescription) 161 | } 162 | } 163 | 164 | private func fallbackSettings(page: SFSafariPage) { 165 | do { 166 | let settings = try configuration.getUserSettings() 167 | page.dispatch(settings: settings) 168 | } catch { 169 | NSLog(error.localizedDescription) 170 | } 171 | } 172 | } 173 | 174 | // MARK: Helpers 175 | 176 | private func mod(_ a: Int, _ n: Int) -> Int { 177 | // https://stackoverflow.com/questions/41180292/negative-number-modulo-in-swift 178 | precondition(n > 0, "modulus must be positive") 179 | let r = a % n 180 | return r >= 0 ? r : r + n 181 | } 182 | 183 | private extension SFSafariPage { 184 | func dispatch(settings: [String: Any]) { 185 | self.dispatchMessageToScript( 186 | withName: "updateSettingsEvent", 187 | userInfo: settings 188 | ) 189 | } 190 | } 191 | 192 | private extension SFSafariApplication { 193 | static func getActivePage(completionHandler: @escaping (SFSafariPage?) -> Void) { 194 | SFSafariApplication.getActiveWindow { 195 | $0?.getActiveTab { 196 | $0?.getActivePage(completionHandler: completionHandler) 197 | } 198 | } 199 | } 200 | } 201 | 202 | -------------------------------------------------------------------------------- /Vimari Extension/SafariExtensionViewController.swift: -------------------------------------------------------------------------------- 1 | import SafariServices 2 | 3 | class SafariExtensionViewController: SFSafariExtensionViewController { 4 | static let shared: SafariExtensionViewController = { 5 | let shared = SafariExtensionViewController() 6 | shared.preferredContentSize = NSSize(width: 320, height: 240) 7 | return shared 8 | }() 9 | } 10 | -------------------------------------------------------------------------------- /Vimari Extension/ToolbarItemIcon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/televator-apps/vimari/0d31b6af58f8779fb1896d566f11d8ed586e5fb8/Vimari Extension/ToolbarItemIcon.pdf -------------------------------------------------------------------------------- /Vimari Extension/Vimari_Extension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Vimari Extension/css/injected.css: -------------------------------------------------------------------------------- 1 | .vimiumReset { 2 | background: none; 3 | border: none; 4 | bottom: auto; 5 | box-shadow: none; 6 | color: black; 7 | cursor: auto; 8 | display: inline; 9 | float: none; 10 | font-family : "Helvetica Neue", "Helvetica", "Arial", sans-serif; 11 | font-size: inherit; 12 | font-style: normal; 13 | font-variant: normal; 14 | font-weight: normal; 15 | height: auto; 16 | left: auto; 17 | letter-spacing: 0; 18 | line-height: 100%; 19 | margin: 0; 20 | max-height: none; 21 | max-width: none; 22 | min-height: 0; 23 | min-width: 0; 24 | opacity: 1; 25 | padding: 0; 26 | position: static; 27 | right: auto; 28 | text-align: left; 29 | text-decoration: none; 30 | text-indent: 0; 31 | text-shadow: none; 32 | text-transform: none; 33 | top: auto; 34 | vertical-align: baseline; 35 | white-space: normal; 36 | width: auto; 37 | z-index: 2147483647; /* Maximum value in Safari */ 38 | } 39 | 40 | div.internalVimiumHintMarker { 41 | position: absolute !important; 42 | display: block; 43 | top: -1px; 44 | left: -1px; 45 | white-space: nowrap !important; 46 | overflow: hidden !important; 47 | font-size: 11px !important; 48 | padding: 2px 3px !important; 49 | background-color: #feda31 !important; 50 | border: 0 !important; 51 | border-radius: 2px !important; 52 | box-shadow: inset 0 -2px 0 #b39922 !important; 53 | } 54 | 55 | div.internalVimiumHintMarker span { 56 | color: #4a400e; 57 | font-family: Helvetica, Arial, sans-serif; 58 | font-weight: bold; 59 | } 60 | 61 | div.internalVimiumHintMarker > .matchingCharacter { 62 | color: #dcbc2a; 63 | } 64 | 65 | .vimiumHUD, .vimiumHUD * { 66 | line-height: 100%; 67 | font-size: 11px; 68 | font-weight: normal; 69 | } 70 | 71 | .vimiumHUD { 72 | position: fixed; 73 | bottom: 0px; 74 | left: 40px; 75 | color: black; 76 | max-width: 400px; 77 | min-width: 150px; 78 | text-align: center; 79 | background-color: #ebebeb; 80 | padding: 3px 3px 5px 3px; 81 | border: 1px solid #b3b3b3; 82 | border-bottom: none; 83 | border-radius: 4px 4px 0 0; 84 | font-family: Lucida Grande, Arial, Sans; 85 | /* One less than vimium's hint markers, so link hints can be shown e.g. for the panel's close button. */ 86 | z-index: 99999998; 87 | text-shadow: 0px 1px 2px #FFF; 88 | line-height: 1.0; 89 | opacity: 0; 90 | } 91 | 92 | .vimiumHUD a, .vimiumHUD a:hover { 93 | background: transparent; 94 | color: blue; 95 | text-decoration: underline; 96 | } 97 | 98 | .vimiumHUD a.close-button { 99 | float:right; 100 | font-family:courier new; 101 | font-weight:bold; 102 | color:#9C9A9A; 103 | text-decoration:none; 104 | padding-left:10px; 105 | margin-top:-1px; 106 | font-size:14px; 107 | } 108 | 109 | .vimiumHUD a.close-button:hover { 110 | color:#333333; 111 | cursor:default; 112 | -webkit-user-select:none; 113 | } 114 | 115 | -------------------------------------------------------------------------------- /Vimari Extension/js/SafariExtensionCommunicator.js: -------------------------------------------------------------------------------- 1 | var SafariExtensionCommunicator = (function (msgHandler) { 2 | 'use strict' 3 | var publicAPI = {} 4 | 5 | // Connect the provided message handler to the received messages. 6 | safari.self.addEventListener("message", msgHandler) 7 | 8 | var sendMessage = function(msgName) { 9 | safari.extension.dispatchMessage(msgName) 10 | } 11 | 12 | publicAPI.requestSettingsUpdate = function() { 13 | sendMessage("updateSettings") 14 | } 15 | publicAPI.requestTabForward = function() { 16 | sendMessage("tabForward") 17 | } 18 | publicAPI.requestTabBackward = function() { 19 | sendMessage("tabBackward") 20 | } 21 | publicAPI.requestCloseTab = function () { 22 | sendMessage("closeTab") 23 | } 24 | 25 | // Return only the public methods. 26 | return publicAPI; 27 | }); 28 | -------------------------------------------------------------------------------- /Vimari Extension/js/injected.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Vimari injected script. 3 | * 4 | * This script is called before the requested page is loaded. This allows us 5 | * to intercept events before they are passed to the requested pages code and 6 | * therefore we can stop certain pages (google) stealing the focus. 7 | */ 8 | 9 | 10 | /* 11 | * Global vars 12 | * 13 | * topWindow - true if top window, false if iframe 14 | * settings - stores user settings 15 | * currentZoomLevel - required for vimium scripts to run correctly 16 | * linkHintCss - required from vimium scripts 17 | * extensionActive - is the extension currently enabled (should only be true when tab is active) 18 | * shiftKeyToggle - is shift key currently toggled 19 | */ 20 | 21 | var topWindow = (window.top === window), 22 | settings = {}, 23 | currentZoomLevel = 100, 24 | linkHintCss = {}, 25 | extensionActive = true, 26 | insertMode = false, 27 | shiftKeyToggle = false, 28 | hudDuration = 5000, 29 | extensionCommunicator = SafariExtensionCommunicator(messageHandler); 30 | 31 | var actionMap = { 32 | 'hintToggle' : function() { 33 | HUD.showForDuration('Open link in current tab', hudDuration); 34 | activateLinkHintsMode(false, false); }, 35 | 36 | 'newTabHintToggle' : function() { 37 | HUD.showForDuration('Open link in new tab', hudDuration); 38 | activateLinkHintsMode(true, false); }, 39 | 40 | 'tabForward': 41 | function() { extensionCommunicator.requestTabForward(); }, 42 | 43 | 'tabBack': 44 | function() { extensionCommunicator.requestTabBackward() }, 45 | 46 | 'scrollDown': 47 | function() { customScrollBy(0, settings.scrollSize); }, 48 | 49 | 'scrollUp': 50 | function() { customScrollBy(0, -settings.scrollSize); }, 51 | 52 | 'scrollLeft': 53 | function() { customScrollBy(-settings.scrollSize, 0); }, 54 | 55 | 'scrollRight': 56 | function() { customScrollBy(settings.scrollSize, 0); }, 57 | 58 | 'goBack': 59 | function() { window.history.back(); }, 60 | 61 | 'goForward': 62 | function() { window.history.forward(); }, 63 | 64 | 'reload': 65 | function() { window.location.reload(); }, 66 | 67 | 'openTab': 68 | function() { window.open(settings.openTabUrl); }, 69 | 70 | 'closeTab': 71 | function() { extensionCommunicator.requestCloseTab(); }, 72 | 73 | 'duplicateTab': 74 | function() { window.open(window.location.href); }, 75 | 76 | 'scrollDownHalfPage': 77 | function() { customScrollBy(0, window.innerHeight / 2); }, 78 | 79 | 'scrollUpHalfPage': 80 | function() { customScrollBy(0, window.innerHeight / -2); }, 81 | 82 | 'goToPageBottom': 83 | function() { customScrollBy(0, document.body.scrollHeight); }, 84 | 85 | 'goToPageTop': 86 | function() { customScrollBy(0, -document.body.scrollHeight); }, 87 | 88 | 'goToFirstInput': 89 | function() { goToFirstInput(); } 90 | }; 91 | 92 | // Inspiration and general algorithm taken from sVim. 93 | function goToFirstInput() { 94 | var inputs = document.querySelectorAll('input,textarea'); 95 | 96 | var bestInput = null; 97 | var bestInViewInput = null; 98 | 99 | inputs.forEach(function(input) { 100 | // Skip if hidden or disabled 101 | if ((input.offsetParent === null) || 102 | input.disabled || 103 | (input.getAttribute('type') === 'hidden') || 104 | (getComputedStyle(input).visibility === 'hidden') || 105 | (input.getAttribute('display') === 'none')) { 106 | return; 107 | } 108 | 109 | // Skip things that are not actual inputs 110 | if ((input.localName !== 'textarea') && 111 | (input.localName !== 'input') && 112 | (input.getAttribute('contenteditable') !== 'true')) { 113 | return; 114 | } 115 | 116 | // Skip non-text inputs 117 | if (/button|radio|file|image|checkbox|submit/i.test(input.getAttribute('type'))) { 118 | return; 119 | } 120 | 121 | var inputRect = input.getClientRects()[0]; 122 | var isInView = (inputRect.top >= -inputRect.height) && 123 | (inputRect.top <= window.innerHeight) && 124 | (inputRect.left >= -inputRect.width) && 125 | (inputRect.left <= window.innerWidth); 126 | 127 | if (bestInput === null) { 128 | bestInput = input; 129 | } 130 | 131 | if (isInView && (bestInViewInput === null)) { 132 | bestInViewInput = input; 133 | } 134 | }); 135 | 136 | var inputToFocus = bestInViewInput || bestInput; 137 | if (inputToFocus !== null) { 138 | inputToFocus.focus(); 139 | } 140 | } 141 | 142 | // Meant to be overridden, but still has to be copy/pasted from the original... 143 | Mousetrap.prototype.stopCallback = function(e, element, combo) { 144 | // Escape key is special, no need to stop. Vimari-specific. 145 | if (combo === 'esc' || combo === 'ctrl+[') { return false; } 146 | 147 | // Preserve the behavior of allowing ex. ctrl-j in an input 148 | if (settings.modifier) { return false; } 149 | 150 | // if the element has the class "mousetrap" then no need to stop 151 | if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { 152 | return false; 153 | } 154 | 155 | var tagName = element.tagName; 156 | var contentIsEditable = (element.contentEditable && element.contentEditable === 'true'); 157 | 158 | // stop for input, select, and textarea 159 | return tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA' || contentIsEditable; 160 | }; 161 | 162 | // Set up key codes to event handlers 163 | function bindKeyCodesToActions(settings) { 164 | // Only add if topWindow... not iframe 165 | Mousetrap.reset(); 166 | if (topWindow) { 167 | Mousetrap.bind('esc', enterNormalMode); 168 | Mousetrap.bind('ctrl+[', enterNormalMode); 169 | Mousetrap.bind('i', enterInsertMode); 170 | for (var actionName in actionMap) { 171 | if (actionMap.hasOwnProperty(actionName)) { 172 | var keyCode = getKeyCode(actionName); 173 | Mousetrap.bind(keyCode, executeAction(actionName), 'keydown'); 174 | } 175 | } 176 | } 177 | } 178 | 179 | function enterNormalMode() { 180 | // Clear input focus 181 | document.activeElement.blur(); 182 | 183 | // Clear link hints (if any) 184 | deactivateLinkHintsMode(); 185 | 186 | 187 | if (insertMode === false) { 188 | return // We are already in normal mode. 189 | } 190 | 191 | // Re-enable if in insert mode 192 | insertMode = false; 193 | HUD.showForDuration('Normal Mode', hudDuration); 194 | 195 | Mousetrap.bind('i', enterInsertMode); 196 | } 197 | 198 | // Calling it 'insert mode', but it's really just a user-triggered 199 | // off switch for the actions. 200 | function enterInsertMode() { 201 | if (insertMode === true) { 202 | return // We are already in insert mode. 203 | } 204 | insertMode = true; 205 | HUD.showForDuration('Insert Mode', hudDuration); 206 | Mousetrap.unbind('i'); 207 | } 208 | 209 | function executeAction(actionName) { 210 | return function() { 211 | // don't do anything if we're not supposed to 212 | if (linkHintsModeActivated || !extensionActive || insertMode) 213 | return; 214 | 215 | //Call the action function 216 | actionMap[actionName](); 217 | 218 | // Tell mousetrap to stop propagation 219 | return false; 220 | } 221 | } 222 | 223 | function unbindKeyCodes() { 224 | Mousetrap.reset(); 225 | document.removeEventListener("keydown", stopSitePropagation); 226 | } 227 | 228 | // Returns all keys bound in the settings. 229 | function boundKeys() { 230 | const splitBinding = s => s.split(/\+| /i) 231 | var bindings = Object.values(settings.bindings) 232 | // Split multi-key bindings. 233 | .flatMap(s => { 234 | if (typeof s === "string" || s instanceof String) { 235 | return splitBinding(s) 236 | } else if (Array.isArray(s)) { 237 | return s.flatMap(splitBinding) 238 | } 239 | }) 240 | 241 | // Manually add the modifier, i, esc, and ctr+[. 242 | bindings.push(settings.modifier) 243 | bindings.push("i") 244 | bindings.push("Escape") 245 | bindings.push("Control") 246 | bindings.push("[") 247 | 248 | // Use a set to remove duplicates. 249 | return new Set(bindings) 250 | } 251 | 252 | // Stops propagation of keyboard events in normal mode. Adding this 253 | // callback to the document using the useCapture flag allows us to 254 | // prevent custom key behaviour implemented by the underlying website. 255 | function stopSitePropagation() { 256 | return function (e) { 257 | if (insertMode) { 258 | // Never stop propagation in insert mode. 259 | return 260 | } 261 | 262 | if (settings.transparentBindings === true) { 263 | if (boundKeys().has(e.key) && !isActiveElementEditable()) { 264 | // If we are in normal mode with transparentBindings enabled we 265 | // should only stop propagation in an editable element or if the 266 | // key is bound to a Vimari action. 267 | e.stopPropagation() 268 | } 269 | } else if (!isActiveElementEditable()) { 270 | e.stopPropagation() 271 | } 272 | } 273 | } 274 | 275 | // Check whether the current active element is editable. 276 | function isActiveElementEditable() { 277 | const el = document.activeElement; 278 | return (el != null && isEditable(el)) 279 | } 280 | 281 | 282 | // Adds an optional modifier to the configured key code for the action 283 | function getKeyCode(actionName) { 284 | if (settings === undefined) { 285 | return '' 286 | } 287 | 288 | var keyCode = settings["bindings"][actionName]; 289 | const addModifier = s => { 290 | if (settings.modifier && settings.modifier.length > 0) { 291 | return `${settings.modifier}+${s}` 292 | } else { 293 | return s 294 | } 295 | } 296 | 297 | if (Array.isArray(keyCode)) { 298 | return keyCode.map(addModifier) 299 | } else { 300 | return addModifier(keyCode) 301 | } 302 | } 303 | 304 | 305 | /* 306 | * Adds the given CSS to the page. 307 | * This function is required by vimium but depracated for vimari as the 308 | * css is pre loaded into the page. 309 | */ 310 | function addCssToPage(css) { 311 | return; 312 | } 313 | 314 | 315 | /* 316 | * Input or text elements are considered focusable and able to receive their own keyboard events, 317 | * and will enter enter mode if focused. Also note that the "contentEditable" attribute can be set on 318 | * any element which makes it a rich text editor, like the notes on jjot.com. 319 | * Note: we used to discriminate for text-only inputs, but this is not accurate since all input fields 320 | * can be controlled via the keyboard, particularly SELECT combo boxes. 321 | */ 322 | function isEditable(target) { 323 | if (target.getAttribute("contentEditable") === "true") 324 | return true; 325 | var focusableInputs = ["input", "textarea", "select", "button"]; 326 | return focusableInputs.indexOf(target.tagName.toLowerCase()) >= 0; 327 | } 328 | 329 | 330 | /* 331 | * Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically 332 | * unfocused. 333 | */ 334 | function isEmbed(element) { return ["EMBED", "OBJECT"].indexOf(element.tagName) > 0; } 335 | 336 | 337 | // ========================== 338 | // Message handling functions 339 | // ========================== 340 | 341 | function messageHandler(event){ 342 | if (event.name == "updateSettingsEvent") { 343 | setSettings(event.message); 344 | } 345 | } 346 | 347 | /* 348 | * Callback to pass settings to injected script 349 | */ 350 | function setSettings(msg) { 351 | settings = msg; 352 | activateExtension(settings); 353 | } 354 | 355 | function activateExtension(settings) { 356 | if ((typeof settings != "undefined") && 357 | isExcludedUrl(settings.excludedUrls, document.URL)) { 358 | return; 359 | } 360 | 361 | // Stop keydown propagation 362 | document.addEventListener("keydown", stopSitePropagation(), true); 363 | bindKeyCodesToActions(settings); 364 | } 365 | 366 | function isExcludedUrl(storedExcludedUrls, currentUrl) { 367 | if (!storedExcludedUrls.length) { 368 | return false; 369 | } 370 | 371 | var excludedUrls, regexp, url, formattedUrl, _i, _len; 372 | excludedUrls = storedExcludedUrls.split(","); 373 | for (_i = 0, _len = excludedUrls.length; _i < _len; _i++) { 374 | url = excludedUrls[_i]; 375 | formattedUrl = stripProtocolAndWww(url); 376 | formattedUrl = formattedUrl.toLowerCase().trim(); 377 | regexp = new RegExp('((.*)?(' + formattedUrl + ')+(.*))'); 378 | if (currentUrl.toLowerCase().match(regexp)) { 379 | return true; 380 | } 381 | } 382 | return false; 383 | } 384 | 385 | // These formations removes the protocol and www so that 386 | // the regexp can catch less AND more specific excluded 387 | // domains than the current URL. 388 | function stripProtocolAndWww(url) { 389 | url = url.replace('http://', ''); 390 | url = url.replace('https://', ''); 391 | if (url.startsWith('www.')) { 392 | url = url.slice(4); 393 | } 394 | 395 | return url; 396 | } 397 | 398 | // Add event listener 399 | function inIframe () { 400 | try { 401 | return window.self !== window.top; 402 | } 403 | catch (e) { 404 | return true; 405 | } 406 | } 407 | 408 | if(!inIframe()){ 409 | extensionCommunicator.requestSettingsUpdate() 410 | } 411 | 412 | // Export to make it testable 413 | window.isExcludedUrl = isExcludedUrl; 414 | window.stripProtocolAndWww = stripProtocolAndWww; 415 | -------------------------------------------------------------------------------- /Vimari Extension/js/keyboard-utils.js: -------------------------------------------------------------------------------- 1 | var keyCodes = { ESC: 27, backspace: 8, deleteKey: 46, enter: 13, space: 32, shiftKey: 16, f1: 112, f12: 123}; 2 | var keyNames = { 37: "left", 38: "up", 39: "right", 40: "down" }; 3 | 4 | // This is a mapping of the incorrect keyIdentifiers generated by Webkit on Windows during keydown events to 5 | // the correct identifiers, which are correctly generated on Mac. We require this mapping to properly handle 6 | // these keys on Windows. See https://bugs.webkit.org/show_bug.cgi?id=19906 for more details. 7 | var keyIdentifierCorrectionMap = { 8 | "U+00C0": ["U+0060", "U+007E"], // `~ 9 | "U+00BD": ["U+002D", "U+005F"], // -_ 10 | "U+00BB": ["U+003D", "U+002B"], // =+ 11 | "U+00DB": ["U+005B", "U+007B"], // [{ 12 | "U+00DD": ["U+005D", "U+007D"], // ]} 13 | "U+00DC": ["U+005C", "U+007C"], // \| 14 | "U+00BA": ["U+003B", "U+003A"], // ;: 15 | "U+00DE": ["U+0027", "U+0022"], // '" 16 | "U+00BC": ["U+002C", "U+003C"], // ,< 17 | "U+00BE": ["U+002E", "U+003E"], // .> 18 | "U+00BF": ["U+002F", "U+003F"] // /? 19 | }; 20 | 21 | var platform; 22 | if (navigator.userAgent.indexOf("Mac") !== -1) 23 | platform = "Mac"; 24 | else if (navigator.userAgent.indexOf("Linux") !== -1) 25 | platform = "Linux"; 26 | else 27 | platform = "Windows"; 28 | 29 | function getKeyChar(event) { 30 | // Not a letter 31 | if (event.keyIdentifier.slice(0, 2) !== "U+") { 32 | // Named key 33 | if (keyNames[event.keyCode]) { 34 | return keyNames[event.keyCode]; 35 | } 36 | // F-key 37 | if (event.keyCode >= keyCodes.f1 && event.keyCode <= keyCodes.f12) { 38 | return "f" + (1 + event.keyCode - keyCodes.f1); 39 | } 40 | return ""; 41 | } 42 | var keyIdentifier = event.keyIdentifier; 43 | // On Windows, the keyIdentifiers for non-letter keys are incorrect. See 44 | // https://bugs.webkit.org/show_bug.cgi?id=19906 for more details. 45 | if ((platform === "Windows" || platform === "Linux") && keyIdentifierCorrectionMap[keyIdentifier]) { 46 | correctedIdentifiers = keyIdentifierCorrectionMap[keyIdentifier]; 47 | keyIdentifier = event.shiftKey ? correctedIdentifiers[0] : correctedIdentifiers[1]; 48 | } 49 | var unicodeKeyInHex = "0x" + keyIdentifier.substring(2); 50 | return String.fromCharCode(parseInt(unicodeKeyInHex)).toLowerCase(); 51 | } 52 | 53 | function isPrimaryModifierKey(event) { 54 | if (platform === "Mac") 55 | return event.metaKey; 56 | else 57 | return event.ctrlKey; 58 | } 59 | 60 | function isEscape(event) { 61 | return event.keyCode === keyCodes.ESC || 62 | (event.ctrlKey && getKeyChar(event) === '['); // c-[ is mapped to ESC in Vim by default. 63 | } 64 | -------------------------------------------------------------------------------- /Vimari Extension/js/lib/mousetrap.js: -------------------------------------------------------------------------------- 1 | // Custom behaviour for Vimari implemented at line 185. Be aware of this 2 | // when upgrading mousetrap. 3 | 4 | /*global define:false */ 5 | /** 6 | * Copyright 2012-2017 Craig Campbell 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | * 20 | * Mousetrap is a simple keyboard shortcut library for Javascript with 21 | * no external dependencies 22 | * 23 | * @version 1.6.5 24 | * @url craig.is/killing/mice 25 | */ 26 | (function(window, document, undefined) { 27 | 28 | // Check if mousetrap is used inside browser, if not, return 29 | if (!window) { 30 | return; 31 | } 32 | 33 | /** 34 | * mapping of special keycodes to their corresponding keys 35 | * 36 | * everything in this dictionary cannot use keypress events 37 | * so it has to be here to map to the correct keycodes for 38 | * keyup/keydown events 39 | * 40 | * @type {Object} 41 | */ 42 | var _MAP = { 43 | 8: 'backspace', 44 | 9: 'tab', 45 | 13: 'enter', 46 | 16: 'shift', 47 | 17: 'ctrl', 48 | 18: 'alt', 49 | 20: 'capslock', 50 | 27: 'esc', 51 | 32: 'space', 52 | 33: 'pageup', 53 | 34: 'pagedown', 54 | 35: 'end', 55 | 36: 'home', 56 | 37: 'left', 57 | 38: 'up', 58 | 39: 'right', 59 | 40: 'down', 60 | 45: 'ins', 61 | 46: 'del', 62 | 91: 'meta', 63 | 93: 'meta', 64 | 224: 'meta' 65 | }; 66 | 67 | /** 68 | * mapping for special characters so they can support 69 | * 70 | * this dictionary is only used incase you want to bind a 71 | * keyup or keydown event to one of these keys 72 | * 73 | * @type {Object} 74 | */ 75 | var _KEYCODE_MAP = { 76 | 106: '*', 77 | 107: '+', 78 | 109: '-', 79 | 110: '.', 80 | 111 : '/', 81 | 186: ';', 82 | 187: '=', 83 | 188: ',', 84 | 189: '-', 85 | 190: '.', 86 | 191: '/', 87 | 192: '`', 88 | 219: '[', 89 | 220: '\\', 90 | 221: ']', 91 | 222: '\'' 92 | }; 93 | 94 | /** 95 | * this is a mapping of keys that require shift on a US keypad 96 | * back to the non shift equivelents 97 | * 98 | * this is so you can use keyup events with these keys 99 | * 100 | * note that this will only work reliably on US keyboards 101 | * 102 | * @type {Object} 103 | */ 104 | var _SHIFT_MAP = { 105 | '~': '`', 106 | '!': '1', 107 | '@': '2', 108 | '#': '3', 109 | '$': '4', 110 | '%': '5', 111 | '^': '6', 112 | '&': '7', 113 | '*': '8', 114 | '(': '9', 115 | ')': '0', 116 | '_': '-', 117 | '+': '=', 118 | ':': ';', 119 | '\"': '\'', 120 | '<': ',', 121 | '>': '.', 122 | '?': '/', 123 | '|': '\\' 124 | }; 125 | 126 | /** 127 | * this is a list of special strings you can use to map 128 | * to modifier keys when you specify your keyboard shortcuts 129 | * 130 | * @type {Object} 131 | */ 132 | var _SPECIAL_ALIASES = { 133 | 'option': 'alt', 134 | 'command': 'meta', 135 | 'return': 'enter', 136 | 'escape': 'esc', 137 | 'plus': '+', 138 | 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl' 139 | }; 140 | 141 | /** 142 | * variable to store the flipped version of _MAP from above 143 | * needed to check if we should use keypress or not when no action 144 | * is specified 145 | * 146 | * @type {Object|undefined} 147 | */ 148 | var _REVERSE_MAP; 149 | 150 | /** 151 | * loop through the f keys, f1 to f19 and add them to the map 152 | * programatically 153 | */ 154 | for (var i = 1; i < 20; ++i) { 155 | _MAP[111 + i] = 'f' + i; 156 | } 157 | 158 | /** 159 | * loop through to map numbers on the numeric keypad 160 | */ 161 | for (i = 0; i <= 9; ++i) { 162 | 163 | // This needs to use a string cause otherwise since 0 is falsey 164 | // mousetrap will never fire for numpad 0 pressed as part of a keydown 165 | // event. 166 | // 167 | // @see https://github.com/ccampbell/mousetrap/pull/258 168 | _MAP[i + 96] = i.toString(); 169 | } 170 | 171 | /** 172 | * cross browser add event method 173 | * 174 | * @param {Element|HTMLDocument} object 175 | * @param {string} type 176 | * @param {Function} callback 177 | * @returns void 178 | */ 179 | function _addEvent(object, type, callback) { 180 | if (object.addEventListener) { 181 | // VIMARI CUSTOMISATION: 182 | // We set the useCapture to true such that events are handled before 183 | // being dispatched to any EventTarget beneath it in the DOM tree. 184 | object.addEventListener(type, callback, true); 185 | return; 186 | } 187 | 188 | object.attachEvent('on' + type, callback); 189 | } 190 | 191 | /** 192 | * takes the event and returns the key character 193 | * 194 | * @param {Event} e 195 | * @return {string} 196 | */ 197 | function _characterFromEvent(e) { 198 | 199 | // for keypress events we should return the character as is 200 | if (e.type == 'keypress') { 201 | var character = String.fromCharCode(e.which); 202 | 203 | // if the shift key is not pressed then it is safe to assume 204 | // that we want the character to be lowercase. this means if 205 | // you accidentally have caps lock on then your key bindings 206 | // will continue to work 207 | // 208 | // the only side effect that might not be desired is if you 209 | // bind something like 'A' cause you want to trigger an 210 | // event when capital A is pressed caps lock will no longer 211 | // trigger the event. shift+a will though. 212 | if (!e.shiftKey) { 213 | character = character.toLowerCase(); 214 | } 215 | 216 | return character; 217 | } 218 | 219 | // for non keypress events the special maps are needed 220 | if (_MAP[e.which]) { 221 | return _MAP[e.which]; 222 | } 223 | 224 | if (_KEYCODE_MAP[e.which]) { 225 | return _KEYCODE_MAP[e.which]; 226 | } 227 | 228 | // if it is not in the special map 229 | 230 | // with keydown and keyup events the character seems to always 231 | // come in as an uppercase character whether you are pressing shift 232 | // or not. we should make sure it is always lowercase for comparisons 233 | return String.fromCharCode(e.which).toLowerCase(); 234 | } 235 | 236 | /** 237 | * checks if two arrays are equal 238 | * 239 | * @param {Array} modifiers1 240 | * @param {Array} modifiers2 241 | * @returns {boolean} 242 | */ 243 | function _modifiersMatch(modifiers1, modifiers2) { 244 | return modifiers1.sort().join(',') === modifiers2.sort().join(','); 245 | } 246 | 247 | /** 248 | * takes a key event and figures out what the modifiers are 249 | * 250 | * @param {Event} e 251 | * @returns {Array} 252 | */ 253 | function _eventModifiers(e) { 254 | var modifiers = []; 255 | 256 | if (e.shiftKey) { 257 | modifiers.push('shift'); 258 | } 259 | 260 | if (e.altKey) { 261 | modifiers.push('alt'); 262 | } 263 | 264 | if (e.ctrlKey) { 265 | modifiers.push('ctrl'); 266 | } 267 | 268 | if (e.metaKey) { 269 | modifiers.push('meta'); 270 | } 271 | 272 | return modifiers; 273 | } 274 | 275 | /** 276 | * prevents default for this event 277 | * 278 | * @param {Event} e 279 | * @returns void 280 | */ 281 | function _preventDefault(e) { 282 | if (e.preventDefault) { 283 | e.preventDefault(); 284 | return; 285 | } 286 | 287 | e.returnValue = false; 288 | } 289 | 290 | /** 291 | * stops propogation for this event 292 | * 293 | * @param {Event} e 294 | * @returns void 295 | */ 296 | function _stopPropagation(e) { 297 | if (e.stopPropagation) { 298 | e.stopPropagation(); 299 | return; 300 | } 301 | 302 | e.cancelBubble = true; 303 | } 304 | 305 | /** 306 | * determines if the keycode specified is a modifier key or not 307 | * 308 | * @param {string} key 309 | * @returns {boolean} 310 | */ 311 | function _isModifier(key) { 312 | return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta'; 313 | } 314 | 315 | /** 316 | * reverses the map lookup so that we can look for specific keys 317 | * to see what can and can't use keypress 318 | * 319 | * @return {Object} 320 | */ 321 | function _getReverseMap() { 322 | if (!_REVERSE_MAP) { 323 | _REVERSE_MAP = {}; 324 | for (var key in _MAP) { 325 | 326 | // pull out the numeric keypad from here cause keypress should 327 | // be able to detect the keys from the character 328 | if (key > 95 && key < 112) { 329 | continue; 330 | } 331 | 332 | if (_MAP.hasOwnProperty(key)) { 333 | _REVERSE_MAP[_MAP[key]] = key; 334 | } 335 | } 336 | } 337 | return _REVERSE_MAP; 338 | } 339 | 340 | /** 341 | * picks the best action based on the key combination 342 | * 343 | * @param {string} key - character for key 344 | * @param {Array} modifiers 345 | * @param {string=} action passed in 346 | */ 347 | function _pickBestAction(key, modifiers, action) { 348 | 349 | // if no action was picked in we should try to pick the one 350 | // that we think would work best for this key 351 | if (!action) { 352 | action = _getReverseMap()[key] ? 'keydown' : 'keypress'; 353 | } 354 | 355 | // modifier keys don't work as expected with keypress, 356 | // switch to keydown 357 | if (action == 'keypress' && modifiers.length) { 358 | action = 'keydown'; 359 | } 360 | 361 | return action; 362 | } 363 | 364 | /** 365 | * Converts from a string key combination to an array 366 | * 367 | * @param {string} combination like "command+shift+l" 368 | * @return {Array} 369 | */ 370 | function _keysFromString(combination) { 371 | if (combination === '+') { 372 | return ['+']; 373 | } 374 | 375 | combination = combination.replace(/\+{2}/g, '+plus'); 376 | return combination.split('+'); 377 | } 378 | 379 | /** 380 | * Gets info for a specific key combination 381 | * 382 | * @param {string} combination key combination ("command+s" or "a" or "*") 383 | * @param {string=} action 384 | * @returns {Object} 385 | */ 386 | function _getKeyInfo(combination, action) { 387 | var keys; 388 | var key; 389 | var i; 390 | var modifiers = []; 391 | 392 | // take the keys from this pattern and figure out what the actual 393 | // pattern is all about 394 | keys = _keysFromString(combination); 395 | 396 | for (i = 0; i < keys.length; ++i) { 397 | key = keys[i]; 398 | 399 | // normalize key names 400 | if (_SPECIAL_ALIASES[key]) { 401 | key = _SPECIAL_ALIASES[key]; 402 | } 403 | 404 | // if this is not a keypress event then we should 405 | // be smart about using shift keys 406 | // this will only work for US keyboards however 407 | if (action && action != 'keypress' && _SHIFT_MAP[key]) { 408 | key = _SHIFT_MAP[key]; 409 | modifiers.push('shift'); 410 | } 411 | 412 | // if this key is a modifier then add it to the list of modifiers 413 | if (_isModifier(key)) { 414 | modifiers.push(key); 415 | } 416 | } 417 | 418 | // depending on what the key combination is 419 | // we will try to pick the best event for it 420 | action = _pickBestAction(key, modifiers, action); 421 | 422 | return { 423 | key: key, 424 | modifiers: modifiers, 425 | action: action 426 | }; 427 | } 428 | 429 | function _belongsTo(element, ancestor) { 430 | if (element === null || element === document) { 431 | return false; 432 | } 433 | 434 | if (element === ancestor) { 435 | return true; 436 | } 437 | 438 | return _belongsTo(element.parentNode, ancestor); 439 | } 440 | 441 | function Mousetrap(targetElement) { 442 | var self = this; 443 | 444 | targetElement = targetElement || document; 445 | 446 | if (!(self instanceof Mousetrap)) { 447 | return new Mousetrap(targetElement); 448 | } 449 | 450 | /** 451 | * element to attach key events to 452 | * 453 | * @type {Element} 454 | */ 455 | self.target = targetElement; 456 | 457 | /** 458 | * a list of all the callbacks setup via Mousetrap.bind() 459 | * 460 | * @type {Object} 461 | */ 462 | self._callbacks = {}; 463 | 464 | /** 465 | * direct map of string combinations to callbacks used for trigger() 466 | * 467 | * @type {Object} 468 | */ 469 | self._directMap = {}; 470 | 471 | /** 472 | * keeps track of what level each sequence is at since multiple 473 | * sequences can start out with the same sequence 474 | * 475 | * @type {Object} 476 | */ 477 | var _sequenceLevels = {}; 478 | 479 | /** 480 | * variable to store the setTimeout call 481 | * 482 | * @type {null|number} 483 | */ 484 | var _resetTimer; 485 | 486 | /** 487 | * temporary state where we will ignore the next keyup 488 | * 489 | * @type {boolean|string} 490 | */ 491 | var _ignoreNextKeyup = false; 492 | 493 | /** 494 | * temporary state where we will ignore the next keypress 495 | * 496 | * @type {boolean} 497 | */ 498 | var _ignoreNextKeypress = false; 499 | 500 | /** 501 | * are we currently inside of a sequence? 502 | * type of action ("keyup" or "keydown" or "keypress") or false 503 | * 504 | * @type {boolean|string} 505 | */ 506 | var _nextExpectedAction = false; 507 | 508 | /** 509 | * resets all sequence counters except for the ones passed in 510 | * 511 | * @param {Object} doNotReset 512 | * @returns void 513 | */ 514 | function _resetSequences(doNotReset) { 515 | doNotReset = doNotReset || {}; 516 | 517 | var activeSequences = false, 518 | key; 519 | 520 | for (key in _sequenceLevels) { 521 | if (doNotReset[key]) { 522 | activeSequences = true; 523 | continue; 524 | } 525 | _sequenceLevels[key] = 0; 526 | } 527 | 528 | if (!activeSequences) { 529 | _nextExpectedAction = false; 530 | } 531 | } 532 | 533 | /** 534 | * finds all callbacks that match based on the keycode, modifiers, 535 | * and action 536 | * 537 | * @param {string} character 538 | * @param {Array} modifiers 539 | * @param {Event|Object} e 540 | * @param {string=} sequenceName - name of the sequence we are looking for 541 | * @param {string=} combination 542 | * @param {number=} level 543 | * @returns {Array} 544 | */ 545 | function _getMatches(character, modifiers, e, sequenceName, combination, level) { 546 | var i; 547 | var callback; 548 | var matches = []; 549 | var action = e.type; 550 | 551 | // if there are no events related to this keycode 552 | if (!self._callbacks[character]) { 553 | return []; 554 | } 555 | 556 | // if a modifier key is coming up on its own we should allow it 557 | if (action == 'keyup' && _isModifier(character)) { 558 | modifiers = [character]; 559 | } 560 | 561 | // loop through all callbacks for the key that was pressed 562 | // and see if any of them match 563 | for (i = 0; i < self._callbacks[character].length; ++i) { 564 | callback = self._callbacks[character][i]; 565 | 566 | // if a sequence name is not specified, but this is a sequence at 567 | // the wrong level then move onto the next match 568 | if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) { 569 | continue; 570 | } 571 | 572 | // if the action we are looking for doesn't match the action we got 573 | // then we should keep going 574 | if (action != callback.action) { 575 | continue; 576 | } 577 | 578 | // if this is a keypress event and the meta key and control key 579 | // are not pressed that means that we need to only look at the 580 | // character, otherwise check the modifiers as well 581 | // 582 | // chrome will not fire a keypress if meta or control is down 583 | // safari will fire a keypress if meta or meta+shift is down 584 | // firefox will fire a keypress if meta or control is down 585 | if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) { 586 | 587 | // when you bind a combination or sequence a second time it 588 | // should overwrite the first one. if a sequenceName or 589 | // combination is specified in this call it does just that 590 | // 591 | // @todo make deleting its own method? 592 | var deleteCombo = !sequenceName && callback.combo == combination; 593 | var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level; 594 | if (deleteCombo || deleteSequence) { 595 | self._callbacks[character].splice(i, 1); 596 | } 597 | 598 | matches.push(callback); 599 | } 600 | } 601 | 602 | return matches; 603 | } 604 | 605 | /** 606 | * actually calls the callback function 607 | * 608 | * if your callback function returns false this will use the jquery 609 | * convention - prevent default and stop propogation on the event 610 | * 611 | * @param {Function} callback 612 | * @param {Event} e 613 | * @returns void 614 | */ 615 | function _fireCallback(callback, e, combo, sequence) { 616 | 617 | // if this event should not happen stop here 618 | if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) { 619 | return; 620 | } 621 | 622 | if (callback(e, combo) === false) { 623 | _preventDefault(e); 624 | _stopPropagation(e); 625 | } 626 | } 627 | 628 | /** 629 | * handles a character key event 630 | * 631 | * @param {string} character 632 | * @param {Array} modifiers 633 | * @param {Event} e 634 | * @returns void 635 | */ 636 | self._handleKey = function(character, modifiers, e) { 637 | var callbacks = _getMatches(character, modifiers, e); 638 | var i; 639 | var doNotReset = {}; 640 | var maxLevel = 0; 641 | var processedSequenceCallback = false; 642 | 643 | // Calculate the maxLevel for sequences so we can only execute the longest callback sequence 644 | for (i = 0; i < callbacks.length; ++i) { 645 | if (callbacks[i].seq) { 646 | maxLevel = Math.max(maxLevel, callbacks[i].level); 647 | } 648 | } 649 | 650 | // loop through matching callbacks for this key event 651 | for (i = 0; i < callbacks.length; ++i) { 652 | 653 | // fire for all sequence callbacks 654 | // this is because if for example you have multiple sequences 655 | // bound such as "g i" and "g t" they both need to fire the 656 | // callback for matching g cause otherwise you can only ever 657 | // match the first one 658 | if (callbacks[i].seq) { 659 | 660 | // only fire callbacks for the maxLevel to prevent 661 | // subsequences from also firing 662 | // 663 | // for example 'a option b' should not cause 'option b' to fire 664 | // even though 'option b' is part of the other sequence 665 | // 666 | // any sequences that do not match here will be discarded 667 | // below by the _resetSequences call 668 | if (callbacks[i].level != maxLevel) { 669 | continue; 670 | } 671 | 672 | processedSequenceCallback = true; 673 | 674 | // keep a list of which sequences were matches for later 675 | doNotReset[callbacks[i].seq] = 1; 676 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq); 677 | continue; 678 | } 679 | 680 | // if there were no sequence matches but we are still here 681 | // that means this is a regular match so we should fire that 682 | if (!processedSequenceCallback) { 683 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo); 684 | } 685 | } 686 | 687 | // if the key you pressed matches the type of sequence without 688 | // being a modifier (ie "keyup" or "keypress") then we should 689 | // reset all sequences that were not matched by this event 690 | // 691 | // this is so, for example, if you have the sequence "h a t" and you 692 | // type "h e a r t" it does not match. in this case the "e" will 693 | // cause the sequence to reset 694 | // 695 | // modifier keys are ignored because you can have a sequence 696 | // that contains modifiers such as "enter ctrl+space" and in most 697 | // cases the modifier key will be pressed before the next key 698 | // 699 | // also if you have a sequence such as "ctrl+b a" then pressing the 700 | // "b" key will trigger a "keypress" and a "keydown" 701 | // 702 | // the "keydown" is expected when there is a modifier, but the 703 | // "keypress" ends up matching the _nextExpectedAction since it occurs 704 | // after and that causes the sequence to reset 705 | // 706 | // we ignore keypresses in a sequence that directly follow a keydown 707 | // for the same character 708 | var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress; 709 | if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) { 710 | _resetSequences(doNotReset); 711 | } 712 | 713 | _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown'; 714 | }; 715 | 716 | /** 717 | * handles a keydown event 718 | * 719 | * @param {Event} e 720 | * @returns void 721 | */ 722 | function _handleKeyEvent(e) { 723 | 724 | // normalize e.which for key events 725 | // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion 726 | if (typeof e.which !== 'number') { 727 | e.which = e.keyCode; 728 | } 729 | 730 | var character = _characterFromEvent(e); 731 | 732 | // no character found then stop 733 | if (!character) { 734 | return; 735 | } 736 | 737 | // need to use === for the character check because the character can be 0 738 | if (e.type == 'keyup' && _ignoreNextKeyup === character) { 739 | _ignoreNextKeyup = false; 740 | return; 741 | } 742 | 743 | self.handleKey(character, _eventModifiers(e), e); 744 | } 745 | 746 | /** 747 | * called to set a 1 second timeout on the specified sequence 748 | * 749 | * this is so after each key press in the sequence you have 1 second 750 | * to press the next key before you have to start over 751 | * 752 | * @returns void 753 | */ 754 | function _resetSequenceTimer() { 755 | clearTimeout(_resetTimer); 756 | _resetTimer = setTimeout(_resetSequences, 1000); 757 | } 758 | 759 | /** 760 | * binds a key sequence to an event 761 | * 762 | * @param {string} combo - combo specified in bind call 763 | * @param {Array} keys 764 | * @param {Function} callback 765 | * @param {string=} action 766 | * @returns void 767 | */ 768 | function _bindSequence(combo, keys, callback, action) { 769 | 770 | // start off by adding a sequence level record for this combination 771 | // and setting the level to 0 772 | _sequenceLevels[combo] = 0; 773 | 774 | /** 775 | * callback to increase the sequence level for this sequence and reset 776 | * all other sequences that were active 777 | * 778 | * @param {string} nextAction 779 | * @returns {Function} 780 | */ 781 | function _increaseSequence(nextAction) { 782 | return function() { 783 | _nextExpectedAction = nextAction; 784 | ++_sequenceLevels[combo]; 785 | _resetSequenceTimer(); 786 | }; 787 | } 788 | 789 | /** 790 | * wraps the specified callback inside of another function in order 791 | * to reset all sequence counters as soon as this sequence is done 792 | * 793 | * @param {Event} e 794 | * @returns void 795 | */ 796 | function _callbackAndReset(e) { 797 | _fireCallback(callback, e, combo); 798 | 799 | // we should ignore the next key up if the action is key down 800 | // or keypress. this is so if you finish a sequence and 801 | // release the key the final key will not trigger a keyup 802 | if (action !== 'keyup') { 803 | _ignoreNextKeyup = _characterFromEvent(e); 804 | } 805 | 806 | // weird race condition if a sequence ends with the key 807 | // another sequence begins with 808 | setTimeout(_resetSequences, 10); 809 | } 810 | 811 | // loop through keys one at a time and bind the appropriate callback 812 | // function. for any key leading up to the final one it should 813 | // increase the sequence. after the final, it should reset all sequences 814 | // 815 | // if an action is specified in the original bind call then that will 816 | // be used throughout. otherwise we will pass the action that the 817 | // next key in the sequence should match. this allows a sequence 818 | // to mix and match keypress and keydown events depending on which 819 | // ones are better suited to the key provided 820 | for (var i = 0; i < keys.length; ++i) { 821 | var isFinal = i + 1 === keys.length; 822 | var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action); 823 | _bindSingle(keys[i], wrappedCallback, action, combo, i); 824 | } 825 | } 826 | 827 | /** 828 | * binds a single keyboard combination 829 | * 830 | * @param {string} combination 831 | * @param {Function} callback 832 | * @param {string=} action 833 | * @param {string=} sequenceName - name of sequence if part of sequence 834 | * @param {number=} level - what part of the sequence the command is 835 | * @returns void 836 | */ 837 | function _bindSingle(combination, callback, action, sequenceName, level) { 838 | 839 | // store a direct mapped reference for use with Mousetrap.trigger 840 | self._directMap[combination + ':' + action] = callback; 841 | 842 | // make sure multiple spaces in a row become a single space 843 | combination = combination.replace(/\s+/g, ' '); 844 | 845 | var sequence = combination.split(' '); 846 | var info; 847 | 848 | // if this pattern is a sequence of keys then run through this method 849 | // to reprocess each pattern one key at a time 850 | if (sequence.length > 1) { 851 | _bindSequence(combination, sequence, callback, action); 852 | return; 853 | } 854 | 855 | info = _getKeyInfo(combination, action); 856 | 857 | // make sure to initialize array if this is the first time 858 | // a callback is added for this key 859 | self._callbacks[info.key] = self._callbacks[info.key] || []; 860 | 861 | // remove an existing match if there is one 862 | _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level); 863 | 864 | // add this call back to the array 865 | // if it is a sequence put it at the beginning 866 | // if not put it at the end 867 | // 868 | // this is important because the way these are processed expects 869 | // the sequence ones to come first 870 | self._callbacks[info.key][sequenceName ? 'unshift' : 'push']({ 871 | callback: callback, 872 | modifiers: info.modifiers, 873 | action: info.action, 874 | seq: sequenceName, 875 | level: level, 876 | combo: combination 877 | }); 878 | } 879 | 880 | /** 881 | * binds multiple combinations to the same callback 882 | * 883 | * @param {Array} combinations 884 | * @param {Function} callback 885 | * @param {string|undefined} action 886 | * @returns void 887 | */ 888 | self._bindMultiple = function(combinations, callback, action) { 889 | for (var i = 0; i < combinations.length; ++i) { 890 | _bindSingle(combinations[i], callback, action); 891 | } 892 | }; 893 | 894 | // start! 895 | _addEvent(targetElement, 'keypress', _handleKeyEvent); 896 | _addEvent(targetElement, 'keydown', _handleKeyEvent); 897 | _addEvent(targetElement, 'keyup', _handleKeyEvent); 898 | } 899 | 900 | /** 901 | * binds an event to mousetrap 902 | * 903 | * can be a single key, a combination of keys separated with +, 904 | * an array of keys, or a sequence of keys separated by spaces 905 | * 906 | * be sure to list the modifier keys first to make sure that the 907 | * correct key ends up getting bound (the last key in the pattern) 908 | * 909 | * @param {string|Array} keys 910 | * @param {Function} callback 911 | * @param {string=} action - 'keypress', 'keydown', or 'keyup' 912 | * @returns void 913 | */ 914 | Mousetrap.prototype.bind = function(keys, callback, action) { 915 | var self = this; 916 | keys = keys instanceof Array ? keys : [keys]; 917 | self._bindMultiple.call(self, keys, callback, action); 918 | return self; 919 | }; 920 | 921 | /** 922 | * unbinds an event to mousetrap 923 | * 924 | * the unbinding sets the callback function of the specified key combo 925 | * to an empty function and deletes the corresponding key in the 926 | * _directMap dict. 927 | * 928 | * TODO: actually remove this from the _callbacks dictionary instead 929 | * of binding an empty function 930 | * 931 | * the keycombo+action has to be exactly the same as 932 | * it was defined in the bind method 933 | * 934 | * @param {string|Array} keys 935 | * @param {string} action 936 | * @returns void 937 | */ 938 | Mousetrap.prototype.unbind = function(keys, action) { 939 | var self = this; 940 | return self.bind.call(self, keys, function() {}, action); 941 | }; 942 | 943 | /** 944 | * triggers an event that has already been bound 945 | * 946 | * @param {string} keys 947 | * @param {string=} action 948 | * @returns void 949 | */ 950 | Mousetrap.prototype.trigger = function(keys, action) { 951 | var self = this; 952 | if (self._directMap[keys + ':' + action]) { 953 | self._directMap[keys + ':' + action]({}, keys); 954 | } 955 | return self; 956 | }; 957 | 958 | /** 959 | * resets the library back to its initial state. this is useful 960 | * if you want to clear out the current keyboard shortcuts and bind 961 | * new ones - for example if you switch to another page 962 | * 963 | * @returns void 964 | */ 965 | Mousetrap.prototype.reset = function() { 966 | var self = this; 967 | self._callbacks = {}; 968 | self._directMap = {}; 969 | return self; 970 | }; 971 | 972 | /** 973 | * should we stop this event before firing off callbacks 974 | * 975 | * @param {Event} e 976 | * @param {Element} element 977 | * @return {boolean} 978 | */ 979 | Mousetrap.prototype.stopCallback = function(e, element) { 980 | var self = this; 981 | 982 | // if the element has the class "mousetrap" then no need to stop 983 | if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { 984 | return false; 985 | } 986 | 987 | if (_belongsTo(element, self.target)) { 988 | return false; 989 | } 990 | 991 | // Events originating from a shadow DOM are re-targetted and `e.target` is the shadow host, 992 | // not the initial event target in the shadow tree. Note that not all events cross the 993 | // shadow boundary. 994 | // For shadow trees with `mode: 'open'`, the initial event target is the first element in 995 | // the event’s composed path. For shadow trees with `mode: 'closed'`, the initial event 996 | // target cannot be obtained. 997 | if ('composedPath' in e && typeof e.composedPath === 'function') { 998 | // For open shadow trees, update `element` so that the following check works. 999 | var initialEventTarget = e.composedPath()[0]; 1000 | if (initialEventTarget !== e.target) { 1001 | element = initialEventTarget; 1002 | } 1003 | } 1004 | 1005 | // stop for input, select, and textarea 1006 | return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable; 1007 | }; 1008 | 1009 | /** 1010 | * exposes _handleKey publicly so it can be overwritten by extensions 1011 | */ 1012 | Mousetrap.prototype.handleKey = function() { 1013 | var self = this; 1014 | return self._handleKey.apply(self, arguments); 1015 | }; 1016 | 1017 | /** 1018 | * allow custom key mappings 1019 | */ 1020 | Mousetrap.addKeycodes = function(object) { 1021 | for (var key in object) { 1022 | if (object.hasOwnProperty(key)) { 1023 | _MAP[key] = object[key]; 1024 | } 1025 | } 1026 | _REVERSE_MAP = null; 1027 | }; 1028 | 1029 | /** 1030 | * Init the global mousetrap functions 1031 | * 1032 | * This method is needed to allow the global mousetrap functions to work 1033 | * now that mousetrap is a constructor function. 1034 | */ 1035 | Mousetrap.init = function() { 1036 | var documentMousetrap = Mousetrap(document); 1037 | for (var method in documentMousetrap) { 1038 | if (method.charAt(0) !== '_') { 1039 | Mousetrap[method] = (function(method) { 1040 | return function() { 1041 | return documentMousetrap[method].apply(documentMousetrap, arguments); 1042 | }; 1043 | } (method)); 1044 | } 1045 | } 1046 | }; 1047 | 1048 | Mousetrap.init(); 1049 | 1050 | // expose mousetrap to the global object 1051 | window.Mousetrap = Mousetrap; 1052 | 1053 | // expose as a common js module 1054 | if (typeof module !== 'undefined' && module.exports) { 1055 | module.exports = Mousetrap; 1056 | } 1057 | 1058 | // expose mousetrap as an AMD module 1059 | if (typeof define === 'function' && define.amd) { 1060 | define(function() { 1061 | return Mousetrap; 1062 | }); 1063 | } 1064 | }) (typeof window !== 'undefined' ? window : null, typeof window !== 'undefined' ? document : null); 1065 | -------------------------------------------------------------------------------- /Vimari Extension/js/lib/svim-scripts.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Code in this file is taken from sVim, it has been adjusted to 3 | * work with Vimari. 4 | * Assumes global variable: settings. 5 | */ 6 | 7 | let animationFrame = null; 8 | 9 | function customScrollBy(x, y) { 10 | // If smooth scroll is off then use regular scroll 11 | if (settings == undefined || settings.smoothScroll === undefined || !settings.smoothScroll) { 12 | window.scrollBy(x, y); 13 | return; 14 | } 15 | window.cancelAnimationFrame(animationFrame); 16 | 17 | // Smooth scroll 18 | let i = 0; 19 | let delta = 0; 20 | 21 | // Ease function 22 | function easeOutExpo(t, b, c, d) { 23 | return c * (-Math.pow(2, -10 * t / d) + 1) + b; 24 | } 25 | 26 | // Animate the scroll 27 | function animLoop() { 28 | const toScroll = Math.round(easeOutExpo(i, 0, y, settings.scrollDuration) - delta); 29 | if (toScroll !== 0) { 30 | if (y) { 31 | window.scrollBy(0, toScroll); 32 | } else { 33 | window.scrollBy(toScroll, 0); 34 | } 35 | } 36 | 37 | if (i < this.settings.scrollDuration) { 38 | animationFrame = window.requestAnimationFrame(animLoop); 39 | } 40 | 41 | delta = easeOutExpo(i, 0, (x || y), settings.scrollDuration); 42 | i += 1; 43 | } 44 | 45 | // Start scroll 46 | animLoop(); 47 | } 48 | -------------------------------------------------------------------------------- /Vimari Extension/js/lib/vimium-scripts.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Code in this file is taken directly from vimium 3 | */ 4 | 5 | 6 | /* 7 | * A heads-up-display (HUD) for showing Vimium page operations. 8 | * Note: you cannot interact with the HUD until document.body is available. 9 | */ 10 | HUD = { 11 | _tweenId: -1, 12 | _displayElement: null, 13 | _upgradeNotificationElement: null, 14 | 15 | showForDuration: function(text, duration) { 16 | HUD.show(text); 17 | HUD._showForDurationTimerId = setTimeout(function() { HUD.hide(); }, duration); 18 | }, 19 | 20 | show: function(text) { 21 | clearTimeout(HUD._showForDurationTimerId); 22 | HUD.displayElement().innerHTML = text; 23 | clearInterval(HUD._tweenId); 24 | HUD._tweenId = Tween.fade(HUD.displayElement(), 1.0, 150); 25 | HUD.displayElement().style.display = ""; 26 | }, 27 | 28 | onUpdateLinkClicked: function(event) { 29 | HUD.hideUpgradeNotification(); 30 | chrome.extension.sendRequest({ handler: "upgradeNotificationClosed" }); 31 | }, 32 | 33 | hideUpgradeNotification: function(clickEvent) { 34 | Tween.fade(HUD.upgradeNotificationElement(), 0, 150, 35 | function() { HUD.upgradeNotificationElement().style.display = "none"; }); 36 | }, 37 | 38 | updatePageZoomLevel: function(pageZoomLevel) { 39 | // Since the chrome HUD does not scale with the page's zoom level, neither will this HUD. 40 | var inverseZoomLevel = (100.0 / pageZoomLevel) * 100; 41 | if (HUD._displayElement) 42 | HUD.displayElement().style.zoom = inverseZoomLevel + "%"; 43 | if (HUD._upgradeNotificationElement) 44 | HUD.upgradeNotificationElement().style.zoom = inverseZoomLevel + "%"; 45 | }, 46 | 47 | /* 48 | * Retrieves the HUD HTML element. 49 | */ 50 | displayElement: function() { 51 | if (!HUD._displayElement) { 52 | HUD._displayElement = HUD.createHudElement(); 53 | HUD.updatePageZoomLevel(currentZoomLevel); 54 | } 55 | return HUD._displayElement; 56 | }, 57 | 58 | createHudElement: function() { 59 | var element = document.createElement("div"); 60 | element.className = "vimiumHUD"; 61 | document.body.appendChild(element); 62 | return element; 63 | }, 64 | 65 | hide: function() { 66 | clearInterval(HUD._tweenId); 67 | HUD._tweenId = Tween.fade(HUD.displayElement(), 0, 150, 68 | function() { HUD.displayElement().style.display = "none"; }); 69 | }, 70 | 71 | isReady: function() { return document.body != null; } 72 | }; 73 | 74 | 75 | 76 | 77 | Tween = { 78 | /* 79 | * Fades an element's alpha. Returns a timer ID which can be used to stop the tween via clearInterval. 80 | */ 81 | fade: function(element, toAlpha, duration, onComplete) { 82 | var state = {}; 83 | state.duration = duration; 84 | state.startTime = (new Date()).getTime(); 85 | state.from = parseInt(element.style.opacity) || 0; 86 | state.to = toAlpha; 87 | state.onUpdate = function(value) { 88 | element.style.opacity = value; 89 | if (value == state.to && onComplete) 90 | onComplete(); 91 | }; 92 | state.timerId = setInterval(function() { Tween.performTweenStep(state); }, 50); 93 | return state.timerId; 94 | }, 95 | 96 | performTweenStep: function(state) { 97 | var elapsed = (new Date()).getTime() - state.startTime; 98 | if (elapsed >= state.duration) { 99 | clearInterval(state.timerId); 100 | state.onUpdate(state.to); 101 | } else { 102 | var value = (elapsed / state.duration) * (state.to - state.from) + state.from; 103 | state.onUpdate(value); 104 | } 105 | } 106 | }; 107 | 108 | 109 | -------------------------------------------------------------------------------- /Vimari Extension/js/link-hints.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This implements link hinting. Typing "F" will enter link-hinting mode, where all clickable items on 3 | * the page have a hint marker displayed containing a sequence of letters. Typing those letters will select 4 | * a link. 5 | * 6 | * The characters we use to show link hints are a user-configurable option. By default they're the home row. 7 | * The CSS which is used on the link hints is also a configurable option. 8 | */ 9 | 10 | var hintMarkers = []; 11 | var hintMarkerContainingDiv = null; 12 | // The characters that were typed in while in "link hints" mode. 13 | var hintKeystrokeQueue = []; 14 | var linkHintsModeActivated = false; 15 | var shouldOpenLinkHintInNewTab = false; 16 | var shouldOpenLinkHintWithQueue = false; 17 | // Whether link hint's "open in current/new tab" setting is currently toggled 18 | var openLinkModeToggle = false; 19 | // Whether we have added to the page the CSS needed to display link hints. 20 | var linkHintsCssAdded = false; 21 | 22 | // We need this as a top-level function because our command system doesn't yet support arguments. 23 | function activateLinkHintsModeToOpenInNewTab() { activateLinkHintsMode(true, false); } 24 | 25 | function activateLinkHintsModeWithQueue() { activateLinkHintsMode(true, true); } 26 | 27 | function activateLinkHintsMode(openInNewTab, withQueue) { 28 | if (!linkHintsCssAdded) 29 | addCssToPage(linkHintCss); // linkHintCss is declared by vimiumFrontend.js 30 | linkHintCssAdded = true; 31 | linkHintsModeActivated = true; 32 | setOpenLinkMode(openInNewTab, withQueue); 33 | buildLinkHints(); 34 | document.addEventListener("keydown", onKeyDownInLinkHintsMode, true); 35 | document.addEventListener("keyup", onKeyUpInLinkHintsMode, true); 36 | } 37 | 38 | function setOpenLinkMode(openInNewTab, withQueue) { 39 | shouldOpenLinkHintInNewTab = openInNewTab; 40 | shouldOpenLinkHintWithQueue = withQueue; 41 | return; 42 | } 43 | 44 | /* 45 | * Builds and displays link hints for every visible clickable item on the page. 46 | */ 47 | function buildLinkHints() { 48 | var visibleElements = getVisibleClickableElements(); 49 | 50 | // Initialize the number used to generate the character hints to be as many digits as we need to 51 | // highlight all the links on the page; we don't want some link hints to have more chars than others. 52 | var digitsNeeded = Math.ceil(logXOfBase(visibleElements.length, settings.linkHintCharacters.length)); 53 | var linkHintNumber = 0; 54 | for (var i = 0; i < visibleElements.length; i++) { 55 | hintMarkers.push(createMarkerFor(visibleElements[i], linkHintNumber, digitsNeeded)); 56 | linkHintNumber++; 57 | } 58 | // Note(philc): Append these markers as top level children instead of as child nodes to the link itself, 59 | // because some clickable elements cannot contain children, e.g. submit buttons. This has the caveat 60 | // that if you scroll the page and the link has position=fixed, the marker will not stay fixed. 61 | // Also note that adding these nodes to document.body all at once is significantly faster than one-by-one. 62 | hintMarkerContainingDiv = document.createElement("div"); 63 | hintMarkerContainingDiv.id = "vimiumHintMarkerContainer"; 64 | hintMarkerContainingDiv.className = "vimiumReset"; 65 | for (var i = 0; i < hintMarkers.length; i++) 66 | hintMarkerContainingDiv.appendChild(hintMarkers[i]); 67 | document.body.appendChild(hintMarkerContainingDiv); 68 | } 69 | 70 | function logXOfBase(x, base) { return Math.log(x) / Math.log(base); } 71 | 72 | /* 73 | * Returns all clickable elements that are not hidden and are in the current viewport. 74 | * We prune invisible elements partly for performance reasons, but moreso it's to decrease the number 75 | * of digits needed to enumerate all of the links on screen. 76 | */ 77 | function getVisibleClickableElements() { 78 | // Get all clickable elements. 79 | var elements = getClickableElements(); 80 | 81 | // Get those that are visible too. 82 | var visibleElements = []; 83 | 84 | for (var i = 0; i < elements.length; i++) { 85 | var element = elements[i]; 86 | 87 | var selectedRect = getFirstVisibleRect(element); 88 | if (selectedRect) { 89 | visibleElements.push(selectedRect); 90 | } 91 | } 92 | 93 | return visibleElements; 94 | } 95 | 96 | function getClickableElements() { 97 | var elements = document.getElementsByTagName('*'); 98 | var clickableElements = []; 99 | for (var i = 0; i < elements.length; i++) { 100 | var element = elements[i]; 101 | if (isClickable(element)) 102 | clickableElements.push(element); 103 | } 104 | return clickableElements; 105 | } 106 | 107 | function isClickable(element) { 108 | var name = element.nodeName.toLowerCase(); 109 | var role = element.getAttribute('role'); 110 | 111 | return ( 112 | // normal html elements that can be clicked 113 | name === 'a' || 114 | name === 'button' || 115 | name === 'input' && element.getAttribute('type') !== 'hidden' || 116 | name === 'select' || 117 | name === 'textarea' || 118 | // elements having an ARIA role implying clickability 119 | // (see http://www.w3.org/TR/wai-aria/roles#widget_roles) 120 | role === 'button' || 121 | role === 'checkbox' || 122 | role === 'combobox' || 123 | role === 'link' || 124 | role === 'menuitem' || 125 | role === 'menuitemcheckbox' || 126 | role === 'menuitemradio' || 127 | role === 'radio' || 128 | role === 'tab' || 129 | role === 'textbox' || 130 | // other ways by which we can know an element is clickable 131 | element.hasAttribute('onclick') || 132 | settings.detectByCursorStyle && window.getComputedStyle(element).cursor === 'pointer' && 133 | (!element.parentNode || 134 | window.getComputedStyle(element.parentNode).cursor !== 'pointer') 135 | ); 136 | } 137 | 138 | /* 139 | * Get firs visible rect under an element. 140 | * 141 | * Inline elements can have more than one rect. 142 | * Block elemens only have one rect. 143 | * So, in general, add element's first visible rect, if any. 144 | * If element does not have any visible rect, 145 | * it can still be wrapping other visible children. 146 | * So, in that case, recurse to get the first visible rect 147 | * of the first child that has one. 148 | */ 149 | function getFirstVisibleRect(element) { 150 | // find visible clientRect of element itself 151 | var clientRects = element.getClientRects(); 152 | for (var i = 0; i < clientRects.length; i++) { 153 | var clientRect = clientRects[i]; 154 | if (isVisible(element, clientRect)) { 155 | return {element: element, rect: clientRect}; 156 | } 157 | } 158 | // Only iterate over elements with a children property. This is mainly to 159 | // avoid issues with SVG elements, as Safari doesn't expose a children 160 | // property on them. 161 | if (element.children) { 162 | // find visible clientRect of child 163 | for (var j = 0; j < element.children.length; j++) { 164 | var childClientRect = getFirstVisibleRect(element.children[j]); 165 | if (childClientRect) { 166 | return childClientRect; 167 | } 168 | } 169 | } 170 | return null; 171 | } 172 | 173 | /* 174 | * Returns true if element is visible. 175 | */ 176 | function isVisible(element, clientRect) { 177 | // Exclude links which have just a few pixels on screen, because the link hints won't show for them anyway. 178 | var zoomFactor = currentZoomLevel / 100.0; 179 | if (!clientRect || clientRect.top < 0 || clientRect.top * zoomFactor >= window.innerHeight - 4 || 180 | clientRect.left < 0 || clientRect.left * zoomFactor >= window.innerWidth - 4) 181 | return false; 182 | 183 | if (clientRect.width < 3 || clientRect.height < 3) 184 | return false; 185 | 186 | // eliminate invisible elements (see test_harnesses/visibility_test.html) 187 | var computedStyle = window.getComputedStyle(element, null); 188 | if (computedStyle.getPropertyValue('visibility') !== 'visible' || 189 | computedStyle.getPropertyValue('display') === 'none') 190 | return false; 191 | 192 | // Eliminate elements hidden by another overlapping element. 193 | // To do that, get topmost element at some offset from upper-left corner of clientRect 194 | // and check whether it is the element itself or one of its descendants. 195 | // The offset is needed to account for coordinates truncation and elements with rounded borders. 196 | // 197 | // Coordinates truncation occcurs when using zoom. In that case, clientRect coords should be float, 198 | // but we get integers instead. That makes so that elementFromPoint(clientRect.left, clientRect.top) 199 | // sometimes returns an element different from the one clientRect was obtained from. 200 | // So we introduce an offset to make sure elementFromPoint hits the right element. 201 | // 202 | // For elements with a rounded topleft border, the upper left corner lies outside the element. 203 | // Then, we need an offset to get to the point nearest to the upper left corner, but within border. 204 | var coordTruncationOffset = 2, // A value of 1 has been observed not to be enough, 205 | // so we heuristically choose 2, which seems to work well. 206 | // We know a value of 2 is still safe (lies within the element) because, 207 | // from the code above, widht & height are >= 3. 208 | radius = parseFloat(computedStyle.borderTopLeftRadius), 209 | roundedBorderOffset = Math.ceil(radius * (1 - Math.sin(Math.PI / 4))), 210 | offset = Math.max(coordTruncationOffset, roundedBorderOffset); 211 | if (offset >= clientRect.width || offset >= clientRect.height) 212 | return false; 213 | var el = document.elementFromPoint(clientRect.left + offset, clientRect.top + offset); 214 | while (el && el !== element) 215 | el = el.parentNode; 216 | if (!el) 217 | return false; 218 | 219 | return true; 220 | } 221 | 222 | function onKeyDownInLinkHintsMode(event) { 223 | console.log("-- key down pressed --") 224 | if (event.keyCode === keyCodes.shiftKey && !openLinkModeToggle) { 225 | // Toggle whether to open link in a new or current tab. 226 | setOpenLinkMode(!shouldOpenLinkHintInNewTab, shouldOpenLinkHintWithQueue); 227 | openLinkModeToggle = true; 228 | } 229 | 230 | var keyChar = getKeyChar(event); 231 | if (!keyChar) 232 | return; 233 | 234 | // TODO(philc): Ignore keys that have modifiers. 235 | if (isEscape(event)) { 236 | deactivateLinkHintsMode(); 237 | } else if (event.keyCode === keyCodes.backspace || event.keyCode === keyCodes.deleteKey) { 238 | if (hintKeystrokeQueue.length === 0) { 239 | deactivateLinkHintsMode(); 240 | } else { 241 | hintKeystrokeQueue.pop(); 242 | updateLinkHints(); 243 | } 244 | } else if (settings.linkHintCharacters.indexOf(keyChar) >= 0) { 245 | hintKeystrokeQueue.push(keyChar); 246 | updateLinkHints(); 247 | } else { 248 | return; 249 | } 250 | 251 | event.stopPropagation(); 252 | event.preventDefault(); 253 | } 254 | 255 | function onKeyUpInLinkHintsMode(event) { 256 | if (event.keyCode === keyCodes.shiftKey && openLinkModeToggle) { 257 | // Revert toggle on whether to open link in new or current tab. 258 | setOpenLinkMode(!shouldOpenLinkHintInNewTab, shouldOpenLinkHintWithQueue); 259 | openLinkModeToggle = false; 260 | } 261 | event.stopPropagation(); 262 | event.preventDefault(); 263 | } 264 | 265 | /* 266 | * Updates the visibility of link hints on screen based on the keystrokes typed 267 | * thus far. If the provided keystrokes match exactly with one LinkHint, click 268 | * on that link and exit link hints mode. 269 | */ 270 | function updateLinkHints() { 271 | var hintStringLength = hintMarkers[0].getAttribute("hintString").length 272 | var matchString = hintKeystrokeQueue.join(""); 273 | var linksMatched = highlightLinkMatches(matchString); 274 | if (linksMatched.length === 0) { 275 | deactivateLinkHintsMode(); 276 | } else if (linksMatched.length === 1 && matchString.length === hintStringLength) { 277 | var matchedLink = linksMatched[0]; 278 | if (isSelectable(matchedLink)) { 279 | matchedLink.focus(); 280 | // When focusing a textbox, put the selection caret at the end of the textbox's contents. 281 | matchedLink.setSelectionRange(matchedLink.value.length, matchedLink.value.length); 282 | deactivateLinkHintsMode(); 283 | } else { 284 | // When we're opening the link in the current tab, don't navigate to the selected link immediately; 285 | // we want to give the user some feedback depicting which link they've selected by focusing it. 286 | if (shouldOpenLinkHintWithQueue) { 287 | simulateClick(matchedLink, false); 288 | resetLinkHintsMode(); 289 | } else if (shouldOpenLinkHintInNewTab) { 290 | simulateClick(matchedLink, true); 291 | matchedLink.focus(); 292 | deactivateLinkHintsMode(); 293 | } else { 294 | setTimeout(function() { simulateClick(matchedLink, false); }, 400); 295 | matchedLink.focus(); 296 | deactivateLinkHintsMode(); 297 | } 298 | } 299 | } 300 | } 301 | 302 | /* 303 | * Selectable means the element has a text caret; this is not the same as "focusable". 304 | */ 305 | function isSelectable(element) { 306 | var selectableTypes = ["search", "text", "password"]; 307 | return (element.tagName === "INPUT" && selectableTypes.indexOf(element.type) >= 0) || 308 | element.tagName === "TEXTAREA"; 309 | } 310 | 311 | /* 312 | * Hides link hints which do not match the given search string. To allow the backspace key to work, this 313 | * will also show link hints which do match but were previously hidden. 314 | */ 315 | function highlightLinkMatches(searchString) { 316 | var linksMatched = []; 317 | for (var i = 0; i < hintMarkers.length; i++) { 318 | var linkMarker = hintMarkers[i]; 319 | if (linkMarker.getAttribute("hintString").indexOf(searchString) === 0) { 320 | if (linkMarker.style.display === "none") 321 | linkMarker.style.display = ""; 322 | for (var j = 0; j < linkMarker.childNodes.length; j++) 323 | linkMarker.childNodes[j].className = (j >= searchString.length) ? "" : "matchingCharacter"; 324 | linksMatched.push(linkMarker.clickableItem); 325 | } else { 326 | linkMarker.style.display = "none"; 327 | } 328 | } 329 | return linksMatched; 330 | } 331 | 332 | /* 333 | * Converts a number like "8" into a hint string like "JK". This is used to sequentially generate all of 334 | * the hint text. The hint string will be "padded with zeroes" to ensure its length is equal to numHintDigits. 335 | */ 336 | function numberToHintString(number, numHintDigits) { 337 | var base = settings.linkHintCharacters.length; 338 | var hintString = []; 339 | var remainder = 0; 340 | do { 341 | remainder = number % base; 342 | hintString.unshift(settings.linkHintCharacters[remainder]); 343 | number -= remainder; 344 | number /= Math.floor(base); 345 | } while (number > 0); 346 | 347 | // Pad the hint string we're returning so that it matches numHintDigits. 348 | var hintStringLength = hintString.length; 349 | for (var i = 0; i < numHintDigits - hintStringLength; i++) 350 | hintString.unshift(settings.linkHintCharacters[0]); 351 | return hintString.reverse().join(""); 352 | } 353 | 354 | function simulateClick(link, openInNewTab) { 355 | if (openInNewTab) { 356 | console.log("-- Open link in new tab --"); 357 | safari.extension.dispatchMessage("openLinkInTab", { url: link.href }); 358 | } else { 359 | link.click(); 360 | } 361 | 362 | // If clicking the link doesn't take you to a new page 363 | // the focus should not stay on the link, hence calling blur() 364 | link.blur(); 365 | } 366 | 367 | function deactivateLinkHintsMode() { 368 | if (hintMarkerContainingDiv) 369 | hintMarkerContainingDiv.parentNode.removeChild(hintMarkerContainingDiv); 370 | hintMarkerContainingDiv = null; 371 | hintMarkers = []; 372 | hintKeystrokeQueue = []; 373 | document.removeEventListener("keydown", onKeyDownInLinkHintsMode, true); 374 | document.removeEventListener("keyup", onKeyUpInLinkHintsMode, true); 375 | linkHintsModeActivated = false; 376 | } 377 | 378 | function resetLinkHintsMode() { 379 | deactivateLinkHintsMode(); 380 | activateLinkHintsModeWithQueue(); 381 | } 382 | 383 | /* 384 | * Creates a link marker for the given link. 385 | */ 386 | function createMarkerFor(link, linkHintNumber, linkHintDigits) { 387 | var hintString = numberToHintString(linkHintNumber, linkHintDigits); 388 | var marker = document.createElement("div"); 389 | marker.className = "internalVimiumHintMarker vimiumReset"; 390 | var innerHTML = []; 391 | // Make each hint character a span, so that we can highlight the typed characters as you type them. 392 | for (var i = 0; i < hintString.length; i++) 393 | innerHTML.push('' + hintString[i].toUpperCase() + ''); 394 | marker.innerHTML = innerHTML.join(""); 395 | marker.setAttribute("hintString", hintString); 396 | 397 | // Note: this call will be expensive if we modify the DOM in between calls. 398 | var clientRect = link.rect; 399 | // The coordinates given by the window do not have the zoom factor included since the zoom is set only on 400 | // the document node. 401 | var zoomFactor = currentZoomLevel / 100.0; 402 | marker.style.left = clientRect.left + window.scrollX / zoomFactor + "px"; 403 | marker.style.top = clientRect.top + window.scrollY / zoomFactor + "px"; 404 | 405 | marker.clickableItem = link.element; 406 | return marker; 407 | } 408 | -------------------------------------------------------------------------------- /Vimari Extension/js/mocks.js: -------------------------------------------------------------------------------- 1 | var safari = { 2 | self: { 3 | tab: { 4 | dispatchMessage: function () {}, 5 | }, 6 | addEventListener: function () {}, 7 | } 8 | }; 9 | window.safari = safari; 10 | 11 | global.SafariExtensionCommunicator = function () { 12 | return { 13 | requestSettingsUpdate: function () {}, 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /Vimari Extension/json/defaultSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "excludedUrls": "", 3 | "linkHintCharacters": "asdfjklqwerzxc", 4 | "detectByCursorStyle": false, 5 | "scrollSize": 150, 6 | "openTabUrl": "https://duckduckgo.com/", 7 | "modifier": "", 8 | "smoothScroll": true, 9 | "scrollDuration": 25, 10 | "transparentBindings": true, 11 | "bindings": { 12 | "hintToggle": "f", 13 | "newTabHintToggle": "shift+f", 14 | "scrollUp": "k", 15 | "scrollDown": "j", 16 | "scrollLeft": "h", 17 | "scrollRight": "l", 18 | "scrollUpHalfPage": "u", 19 | "scrollDownHalfPage": "d", 20 | "goToPageTop": "g g", 21 | "goToPageBottom": "shift+g", 22 | "goToFirstInput": "g i", 23 | "goBack": "shift+h", 24 | "goForward": "shift+l", 25 | "reload": "r", 26 | "tabForward": "w", 27 | "tabBack": "q", 28 | "closeTab": "x", 29 | "openTab": "t", 30 | "duplicateTab": "y t" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Vimari.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 659033F124E2B77400432D0E /* svim-scripts.js in Resources */ = {isa = PBXBuildFile; fileRef = 659033F024E2B77400432D0E /* svim-scripts.js */; }; 11 | 65E444F324CC3A1B008EA1DC /* SafariExtensionCommunicator.js in Resources */ = {isa = PBXBuildFile; fileRef = 65E444F224CC3A1B008EA1DC /* SafariExtensionCommunicator.js */; }; 12 | B1E3C17023A65ED400A56807 /* ConfigurationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E3C16F23A65ED400A56807 /* ConfigurationModel.swift */; }; 13 | B1FD3B9923A588DE00677A52 /* defaultSettings.json in Resources */ = {isa = PBXBuildFile; fileRef = B1FD3B9823A588DE00677A52 /* defaultSettings.json */; }; 14 | E320D0662337FC9800F2C3A4 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = E320D0652337FC9800F2C3A4 /* Credits.rtf */; }; 15 | E320D06823397C5C00F2C3A4 /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = E320D06723397C5C00F2C3A4 /* CHANGELOG.md */; }; 16 | E380F24C2331806400640547 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E380F24B2331806400640547 /* AppDelegate.swift */; }; 17 | E380F24F2331806400640547 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E380F24D2331806400640547 /* Main.storyboard */; }; 18 | E380F2512331806400640547 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E380F2502331806400640547 /* ViewController.swift */; }; 19 | E380F2532331806500640547 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E380F2522331806500640547 /* Assets.xcassets */; }; 20 | E380F25A2331806500640547 /* Vimari Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E380F2592331806500640547 /* Vimari Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 21 | E380F25F2331806500640547 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E380F25E2331806500640547 /* Cocoa.framework */; }; 22 | E380F2622331806500640547 /* SafariExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E380F2612331806500640547 /* SafariExtensionHandler.swift */; }; 23 | E380F2642331806500640547 /* SafariExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E380F2632331806500640547 /* SafariExtensionViewController.swift */; }; 24 | E380F2672331806500640547 /* SafariExtensionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E380F2652331806500640547 /* SafariExtensionViewController.xib */; }; 25 | E380F26C2331806500640547 /* ToolbarItemIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = E380F26B2331806500640547 /* ToolbarItemIcon.pdf */; }; 26 | E380F283233183EF00640547 /* link-hints.js in Resources */ = {isa = PBXBuildFile; fileRef = E380F278233183EE00640547 /* link-hints.js */; }; 27 | E380F285233183EF00640547 /* injected.js in Resources */ = {isa = PBXBuildFile; fileRef = E380F27A233183EE00640547 /* injected.js */; }; 28 | E380F286233183EF00640547 /* mocks.js in Resources */ = {isa = PBXBuildFile; fileRef = E380F27B233183EE00640547 /* mocks.js */; }; 29 | E380F287233183EF00640547 /* keyboard-utils.js in Resources */ = {isa = PBXBuildFile; fileRef = E380F27C233183EE00640547 /* keyboard-utils.js */; }; 30 | E380F288233183EF00640547 /* mousetrap.js in Resources */ = {isa = PBXBuildFile; fileRef = E380F27E233183EE00640547 /* mousetrap.js */; }; 31 | E380F289233183EF00640547 /* vimium-scripts.js in Resources */ = {isa = PBXBuildFile; fileRef = E380F27F233183EE00640547 /* vimium-scripts.js */; }; 32 | E380F28B233183EF00640547 /* injected.css in Resources */ = {isa = PBXBuildFile; fileRef = E380F282233183EE00640547 /* injected.css */; }; 33 | /* End PBXBuildFile section */ 34 | 35 | /* Begin PBXContainerItemProxy section */ 36 | E380F25B2331806500640547 /* PBXContainerItemProxy */ = { 37 | isa = PBXContainerItemProxy; 38 | containerPortal = E380F23E2331806400640547 /* Project object */; 39 | proxyType = 1; 40 | remoteGlobalIDString = E380F2582331806500640547; 41 | remoteInfo = "Vimari Extension"; 42 | }; 43 | /* End PBXContainerItemProxy section */ 44 | 45 | /* Begin PBXCopyFilesBuildPhase section */ 46 | E380F2732331806500640547 /* Embed App Extensions */ = { 47 | isa = PBXCopyFilesBuildPhase; 48 | buildActionMask = 2147483647; 49 | dstPath = ""; 50 | dstSubfolderSpec = 13; 51 | files = ( 52 | E380F25A2331806500640547 /* Vimari Extension.appex in Embed App Extensions */, 53 | ); 54 | name = "Embed App Extensions"; 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | /* End PBXCopyFilesBuildPhase section */ 58 | 59 | /* Begin PBXFileReference section */ 60 | 659033F024E2B77400432D0E /* svim-scripts.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "svim-scripts.js"; sourceTree = ""; }; 61 | 65E444F224CC3A1B008EA1DC /* SafariExtensionCommunicator.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = SafariExtensionCommunicator.js; sourceTree = ""; }; 62 | B1E3C16F23A65ED400A56807 /* ConfigurationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationModel.swift; sourceTree = ""; }; 63 | B1FD3B9823A588DE00677A52 /* defaultSettings.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = defaultSettings.json; sourceTree = ""; }; 64 | E320D0652337FC9800F2C3A4 /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 65 | E320D06723397C5C00F2C3A4 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 66 | E380F2462331806400640547 /* Vimari.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Vimari.app; sourceTree = BUILT_PRODUCTS_DIR; }; 67 | E380F2492331806400640547 /* Vimari.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Vimari.entitlements; sourceTree = ""; }; 68 | E380F24B2331806400640547 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 69 | E380F24E2331806400640547 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 70 | E380F2502331806400640547 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 71 | E380F2522331806500640547 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 72 | E380F2542331806500640547 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 73 | E380F2592331806500640547 /* Vimari Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Vimari Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 74 | E380F25E2331806500640547 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 75 | E380F2612331806500640547 /* SafariExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionHandler.swift; sourceTree = ""; }; 76 | E380F2632331806500640547 /* SafariExtensionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionViewController.swift; sourceTree = ""; }; 77 | E380F2662331806500640547 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/SafariExtensionViewController.xib; sourceTree = ""; }; 78 | E380F2682331806500640547 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 79 | E380F26B2331806500640547 /* ToolbarItemIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = ToolbarItemIcon.pdf; sourceTree = ""; }; 80 | E380F26D2331806500640547 /* Vimari_Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Vimari_Extension.entitlements; sourceTree = ""; }; 81 | E380F278233183EE00640547 /* link-hints.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "link-hints.js"; sourceTree = ""; }; 82 | E380F27A233183EE00640547 /* injected.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = injected.js; sourceTree = ""; }; 83 | E380F27B233183EE00640547 /* mocks.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = mocks.js; sourceTree = ""; }; 84 | E380F27C233183EE00640547 /* keyboard-utils.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "keyboard-utils.js"; sourceTree = ""; }; 85 | E380F27E233183EE00640547 /* mousetrap.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = mousetrap.js; sourceTree = ""; }; 86 | E380F27F233183EE00640547 /* vimium-scripts.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "vimium-scripts.js"; sourceTree = ""; }; 87 | E380F282233183EE00640547 /* injected.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = injected.css; sourceTree = ""; }; 88 | /* End PBXFileReference section */ 89 | 90 | /* Begin PBXFrameworksBuildPhase section */ 91 | E380F2432331806400640547 /* Frameworks */ = { 92 | isa = PBXFrameworksBuildPhase; 93 | buildActionMask = 2147483647; 94 | files = ( 95 | ); 96 | runOnlyForDeploymentPostprocessing = 0; 97 | }; 98 | E380F2562331806500640547 /* Frameworks */ = { 99 | isa = PBXFrameworksBuildPhase; 100 | buildActionMask = 2147483647; 101 | files = ( 102 | E380F25F2331806500640547 /* Cocoa.framework in Frameworks */, 103 | ); 104 | runOnlyForDeploymentPostprocessing = 0; 105 | }; 106 | /* End PBXFrameworksBuildPhase section */ 107 | 108 | /* Begin PBXGroup section */ 109 | B1FD3B9723A588DE00677A52 /* json */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | B1FD3B9823A588DE00677A52 /* defaultSettings.json */, 113 | ); 114 | path = json; 115 | sourceTree = ""; 116 | }; 117 | E380F23D2331806300640547 = { 118 | isa = PBXGroup; 119 | children = ( 120 | E320D06723397C5C00F2C3A4 /* CHANGELOG.md */, 121 | E380F2482331806400640547 /* Vimari */, 122 | E380F2602331806500640547 /* Vimari Extension */, 123 | E380F25D2331806500640547 /* Frameworks */, 124 | E380F2472331806400640547 /* Products */, 125 | ); 126 | sourceTree = ""; 127 | }; 128 | E380F2472331806400640547 /* Products */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | E380F2462331806400640547 /* Vimari.app */, 132 | E380F2592331806500640547 /* Vimari Extension.appex */, 133 | ); 134 | name = Products; 135 | sourceTree = ""; 136 | }; 137 | E380F2482331806400640547 /* Vimari */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | E380F2492331806400640547 /* Vimari.entitlements */, 141 | E380F24B2331806400640547 /* AppDelegate.swift */, 142 | E320D0652337FC9800F2C3A4 /* Credits.rtf */, 143 | E380F24D2331806400640547 /* Main.storyboard */, 144 | E380F2502331806400640547 /* ViewController.swift */, 145 | E380F2522331806500640547 /* Assets.xcassets */, 146 | E380F2542331806500640547 /* Info.plist */, 147 | ); 148 | path = Vimari; 149 | sourceTree = ""; 150 | }; 151 | E380F25D2331806500640547 /* Frameworks */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | E380F25E2331806500640547 /* Cocoa.framework */, 155 | ); 156 | name = Frameworks; 157 | sourceTree = ""; 158 | }; 159 | E380F2602331806500640547 /* Vimari Extension */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | E380F2612331806500640547 /* SafariExtensionHandler.swift */, 163 | B1E3C16F23A65ED400A56807 /* ConfigurationModel.swift */, 164 | E380F2632331806500640547 /* SafariExtensionViewController.swift */, 165 | E380F2652331806500640547 /* SafariExtensionViewController.xib */, 166 | B1FD3B9723A588DE00677A52 /* json */, 167 | E380F281233183EE00640547 /* css */, 168 | E380F277233183EE00640547 /* js */, 169 | E380F2682331806500640547 /* Info.plist */, 170 | E380F26B2331806500640547 /* ToolbarItemIcon.pdf */, 171 | E380F26D2331806500640547 /* Vimari_Extension.entitlements */, 172 | ); 173 | path = "Vimari Extension"; 174 | sourceTree = ""; 175 | }; 176 | E380F277233183EE00640547 /* js */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | E380F278233183EE00640547 /* link-hints.js */, 180 | E380F27A233183EE00640547 /* injected.js */, 181 | E380F27B233183EE00640547 /* mocks.js */, 182 | E380F27C233183EE00640547 /* keyboard-utils.js */, 183 | E380F27D233183EE00640547 /* lib */, 184 | 65E444F224CC3A1B008EA1DC /* SafariExtensionCommunicator.js */, 185 | ); 186 | path = js; 187 | sourceTree = ""; 188 | }; 189 | E380F27D233183EE00640547 /* lib */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | 659033F024E2B77400432D0E /* svim-scripts.js */, 193 | E380F27E233183EE00640547 /* mousetrap.js */, 194 | E380F27F233183EE00640547 /* vimium-scripts.js */, 195 | ); 196 | path = lib; 197 | sourceTree = ""; 198 | }; 199 | E380F281233183EE00640547 /* css */ = { 200 | isa = PBXGroup; 201 | children = ( 202 | E380F282233183EE00640547 /* injected.css */, 203 | ); 204 | path = css; 205 | sourceTree = ""; 206 | }; 207 | /* End PBXGroup section */ 208 | 209 | /* Begin PBXNativeTarget section */ 210 | E380F2452331806400640547 /* Vimari */ = { 211 | isa = PBXNativeTarget; 212 | buildConfigurationList = E380F2742331806500640547 /* Build configuration list for PBXNativeTarget "Vimari" */; 213 | buildPhases = ( 214 | E380F2422331806400640547 /* Sources */, 215 | E380F2432331806400640547 /* Frameworks */, 216 | E380F2442331806400640547 /* Resources */, 217 | E380F2732331806500640547 /* Embed App Extensions */, 218 | ); 219 | buildRules = ( 220 | ); 221 | dependencies = ( 222 | E380F25C2331806500640547 /* PBXTargetDependency */, 223 | ); 224 | name = Vimari; 225 | productName = Vimari; 226 | productReference = E380F2462331806400640547 /* Vimari.app */; 227 | productType = "com.apple.product-type.application"; 228 | }; 229 | E380F2582331806500640547 /* Vimari Extension */ = { 230 | isa = PBXNativeTarget; 231 | buildConfigurationList = E380F2702331806500640547 /* Build configuration list for PBXNativeTarget "Vimari Extension" */; 232 | buildPhases = ( 233 | E380F2552331806500640547 /* Sources */, 234 | E380F2562331806500640547 /* Frameworks */, 235 | E380F2572331806500640547 /* Resources */, 236 | ); 237 | buildRules = ( 238 | ); 239 | dependencies = ( 240 | ); 241 | name = "Vimari Extension"; 242 | productName = "Vimari Extension"; 243 | productReference = E380F2592331806500640547 /* Vimari Extension.appex */; 244 | productType = "com.apple.product-type.app-extension"; 245 | }; 246 | /* End PBXNativeTarget section */ 247 | 248 | /* Begin PBXProject section */ 249 | E380F23E2331806400640547 /* Project object */ = { 250 | isa = PBXProject; 251 | attributes = { 252 | LastSwiftUpdateCheck = 1100; 253 | LastUpgradeCheck = 1230; 254 | ORGANIZATIONNAME = net.televator; 255 | TargetAttributes = { 256 | E380F2452331806400640547 = { 257 | CreatedOnToolsVersion = 11.0; 258 | }; 259 | E380F2582331806500640547 = { 260 | CreatedOnToolsVersion = 11.0; 261 | }; 262 | }; 263 | }; 264 | buildConfigurationList = E380F2412331806400640547 /* Build configuration list for PBXProject "Vimari" */; 265 | compatibilityVersion = "Xcode 9.3"; 266 | developmentRegion = en; 267 | hasScannedForEncodings = 0; 268 | knownRegions = ( 269 | en, 270 | Base, 271 | ); 272 | mainGroup = E380F23D2331806300640547; 273 | productRefGroup = E380F2472331806400640547 /* Products */; 274 | projectDirPath = ""; 275 | projectRoot = ""; 276 | targets = ( 277 | E380F2452331806400640547 /* Vimari */, 278 | E380F2582331806500640547 /* Vimari Extension */, 279 | ); 280 | }; 281 | /* End PBXProject section */ 282 | 283 | /* Begin PBXResourcesBuildPhase section */ 284 | E380F2442331806400640547 /* Resources */ = { 285 | isa = PBXResourcesBuildPhase; 286 | buildActionMask = 2147483647; 287 | files = ( 288 | E320D06823397C5C00F2C3A4 /* CHANGELOG.md in Resources */, 289 | E380F2532331806500640547 /* Assets.xcassets in Resources */, 290 | E380F24F2331806400640547 /* Main.storyboard in Resources */, 291 | E320D0662337FC9800F2C3A4 /* Credits.rtf in Resources */, 292 | ); 293 | runOnlyForDeploymentPostprocessing = 0; 294 | }; 295 | E380F2572331806500640547 /* Resources */ = { 296 | isa = PBXResourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | E380F26C2331806500640547 /* ToolbarItemIcon.pdf in Resources */, 300 | B1FD3B9923A588DE00677A52 /* defaultSettings.json in Resources */, 301 | 659033F124E2B77400432D0E /* svim-scripts.js in Resources */, 302 | 65E444F324CC3A1B008EA1DC /* SafariExtensionCommunicator.js in Resources */, 303 | E380F28B233183EF00640547 /* injected.css in Resources */, 304 | E380F285233183EF00640547 /* injected.js in Resources */, 305 | E380F287233183EF00640547 /* keyboard-utils.js in Resources */, 306 | E380F289233183EF00640547 /* vimium-scripts.js in Resources */, 307 | E380F2672331806500640547 /* SafariExtensionViewController.xib in Resources */, 308 | E380F286233183EF00640547 /* mocks.js in Resources */, 309 | E380F283233183EF00640547 /* link-hints.js in Resources */, 310 | E380F288233183EF00640547 /* mousetrap.js in Resources */, 311 | ); 312 | runOnlyForDeploymentPostprocessing = 0; 313 | }; 314 | /* End PBXResourcesBuildPhase section */ 315 | 316 | /* Begin PBXSourcesBuildPhase section */ 317 | E380F2422331806400640547 /* Sources */ = { 318 | isa = PBXSourcesBuildPhase; 319 | buildActionMask = 2147483647; 320 | files = ( 321 | E380F2512331806400640547 /* ViewController.swift in Sources */, 322 | E380F24C2331806400640547 /* AppDelegate.swift in Sources */, 323 | ); 324 | runOnlyForDeploymentPostprocessing = 0; 325 | }; 326 | E380F2552331806500640547 /* Sources */ = { 327 | isa = PBXSourcesBuildPhase; 328 | buildActionMask = 2147483647; 329 | files = ( 330 | E380F2642331806500640547 /* SafariExtensionViewController.swift in Sources */, 331 | E380F2622331806500640547 /* SafariExtensionHandler.swift in Sources */, 332 | B1E3C17023A65ED400A56807 /* ConfigurationModel.swift in Sources */, 333 | ); 334 | runOnlyForDeploymentPostprocessing = 0; 335 | }; 336 | /* End PBXSourcesBuildPhase section */ 337 | 338 | /* Begin PBXTargetDependency section */ 339 | E380F25C2331806500640547 /* PBXTargetDependency */ = { 340 | isa = PBXTargetDependency; 341 | target = E380F2582331806500640547 /* Vimari Extension */; 342 | targetProxy = E380F25B2331806500640547 /* PBXContainerItemProxy */; 343 | }; 344 | /* End PBXTargetDependency section */ 345 | 346 | /* Begin PBXVariantGroup section */ 347 | E380F24D2331806400640547 /* Main.storyboard */ = { 348 | isa = PBXVariantGroup; 349 | children = ( 350 | E380F24E2331806400640547 /* Base */, 351 | ); 352 | name = Main.storyboard; 353 | sourceTree = ""; 354 | }; 355 | E380F2652331806500640547 /* SafariExtensionViewController.xib */ = { 356 | isa = PBXVariantGroup; 357 | children = ( 358 | E380F2662331806500640547 /* Base */, 359 | ); 360 | name = SafariExtensionViewController.xib; 361 | sourceTree = ""; 362 | }; 363 | /* End PBXVariantGroup section */ 364 | 365 | /* Begin XCBuildConfiguration section */ 366 | E380F26E2331806500640547 /* Debug */ = { 367 | isa = XCBuildConfiguration; 368 | buildSettings = { 369 | ALWAYS_SEARCH_USER_PATHS = NO; 370 | CLANG_ANALYZER_NONNULL = YES; 371 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 372 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 373 | CLANG_CXX_LIBRARY = "libc++"; 374 | CLANG_ENABLE_MODULES = YES; 375 | CLANG_ENABLE_OBJC_ARC = YES; 376 | CLANG_ENABLE_OBJC_WEAK = YES; 377 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 378 | CLANG_WARN_BOOL_CONVERSION = YES; 379 | CLANG_WARN_COMMA = YES; 380 | CLANG_WARN_CONSTANT_CONVERSION = YES; 381 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 382 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 383 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 384 | CLANG_WARN_EMPTY_BODY = YES; 385 | CLANG_WARN_ENUM_CONVERSION = YES; 386 | CLANG_WARN_INFINITE_RECURSION = YES; 387 | CLANG_WARN_INT_CONVERSION = YES; 388 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 389 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 390 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 391 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 392 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 393 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 394 | CLANG_WARN_STRICT_PROTOTYPES = YES; 395 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 396 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 397 | CLANG_WARN_UNREACHABLE_CODE = YES; 398 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 399 | COPY_PHASE_STRIP = NO; 400 | DEBUG_INFORMATION_FORMAT = dwarf; 401 | ENABLE_STRICT_OBJC_MSGSEND = YES; 402 | ENABLE_TESTABILITY = YES; 403 | GCC_C_LANGUAGE_STANDARD = gnu11; 404 | GCC_DYNAMIC_NO_PIC = NO; 405 | GCC_NO_COMMON_BLOCKS = YES; 406 | GCC_OPTIMIZATION_LEVEL = 0; 407 | GCC_PREPROCESSOR_DEFINITIONS = ( 408 | "DEBUG=1", 409 | "$(inherited)", 410 | ); 411 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 412 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 413 | GCC_WARN_UNDECLARED_SELECTOR = YES; 414 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 415 | GCC_WARN_UNUSED_FUNCTION = YES; 416 | GCC_WARN_UNUSED_VARIABLE = YES; 417 | MACOSX_DEPLOYMENT_TARGET = 10.12; 418 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 419 | MTL_FAST_MATH = YES; 420 | ONLY_ACTIVE_ARCH = YES; 421 | SDKROOT = macosx; 422 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 423 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 424 | }; 425 | name = Debug; 426 | }; 427 | E380F26F2331806500640547 /* Release */ = { 428 | isa = XCBuildConfiguration; 429 | buildSettings = { 430 | ALWAYS_SEARCH_USER_PATHS = NO; 431 | CLANG_ANALYZER_NONNULL = YES; 432 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 433 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 434 | CLANG_CXX_LIBRARY = "libc++"; 435 | CLANG_ENABLE_MODULES = YES; 436 | CLANG_ENABLE_OBJC_ARC = YES; 437 | CLANG_ENABLE_OBJC_WEAK = YES; 438 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 439 | CLANG_WARN_BOOL_CONVERSION = YES; 440 | CLANG_WARN_COMMA = YES; 441 | CLANG_WARN_CONSTANT_CONVERSION = YES; 442 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 443 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 444 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 445 | CLANG_WARN_EMPTY_BODY = YES; 446 | CLANG_WARN_ENUM_CONVERSION = YES; 447 | CLANG_WARN_INFINITE_RECURSION = YES; 448 | CLANG_WARN_INT_CONVERSION = YES; 449 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 450 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 451 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 452 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 453 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 454 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 455 | CLANG_WARN_STRICT_PROTOTYPES = YES; 456 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 457 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 458 | CLANG_WARN_UNREACHABLE_CODE = YES; 459 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 460 | COPY_PHASE_STRIP = NO; 461 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 462 | ENABLE_NS_ASSERTIONS = NO; 463 | ENABLE_STRICT_OBJC_MSGSEND = YES; 464 | GCC_C_LANGUAGE_STANDARD = gnu11; 465 | GCC_NO_COMMON_BLOCKS = YES; 466 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 467 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 468 | GCC_WARN_UNDECLARED_SELECTOR = YES; 469 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 470 | GCC_WARN_UNUSED_FUNCTION = YES; 471 | GCC_WARN_UNUSED_VARIABLE = YES; 472 | MACOSX_DEPLOYMENT_TARGET = 10.12; 473 | MTL_ENABLE_DEBUG_INFO = NO; 474 | MTL_FAST_MATH = YES; 475 | SDKROOT = macosx; 476 | SWIFT_COMPILATION_MODE = wholemodule; 477 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 478 | }; 479 | name = Release; 480 | }; 481 | E380F2712331806500640547 /* Debug */ = { 482 | isa = XCBuildConfiguration; 483 | buildSettings = { 484 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 485 | CODE_SIGN_ENTITLEMENTS = "Vimari Extension/Vimari_Extension.entitlements"; 486 | CODE_SIGN_IDENTITY = "Mac Developer"; 487 | CODE_SIGN_STYLE = Automatic; 488 | CURRENT_PROJECT_VERSION = 10; 489 | DEVELOPMENT_TEAM = Y48UDGWSSQ; 490 | ENABLE_HARDENED_RUNTIME = YES; 491 | INFOPLIST_FILE = "Vimari Extension/Info.plist"; 492 | LD_RUNPATH_SEARCH_PATHS = ( 493 | "$(inherited)", 494 | "@executable_path/../Frameworks", 495 | "@executable_path/../../../../Frameworks", 496 | ); 497 | MARKETING_VERSION = 2.1.1; 498 | PRODUCT_BUNDLE_IDENTIFIER = net.televator.Vimari.SafariExtension; 499 | PRODUCT_NAME = "$(TARGET_NAME)"; 500 | PROVISIONING_PROFILE_SPECIFIER = ""; 501 | SKIP_INSTALL = YES; 502 | SWIFT_VERSION = 5.0; 503 | }; 504 | name = Debug; 505 | }; 506 | E380F2722331806500640547 /* Release */ = { 507 | isa = XCBuildConfiguration; 508 | buildSettings = { 509 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 510 | CODE_SIGN_ENTITLEMENTS = "Vimari Extension/Vimari_Extension.entitlements"; 511 | CODE_SIGN_IDENTITY = "Mac Developer"; 512 | CODE_SIGN_STYLE = Automatic; 513 | CURRENT_PROJECT_VERSION = 10; 514 | DEVELOPMENT_TEAM = Y48UDGWSSQ; 515 | ENABLE_HARDENED_RUNTIME = YES; 516 | INFOPLIST_FILE = "Vimari Extension/Info.plist"; 517 | LD_RUNPATH_SEARCH_PATHS = ( 518 | "$(inherited)", 519 | "@executable_path/../Frameworks", 520 | "@executable_path/../../../../Frameworks", 521 | ); 522 | MARKETING_VERSION = 2.1.1; 523 | PRODUCT_BUNDLE_IDENTIFIER = net.televator.Vimari.SafariExtension; 524 | PRODUCT_NAME = "$(TARGET_NAME)"; 525 | PROVISIONING_PROFILE_SPECIFIER = ""; 526 | SKIP_INSTALL = YES; 527 | SWIFT_VERSION = 5.0; 528 | }; 529 | name = Release; 530 | }; 531 | E380F2752331806500640547 /* Debug */ = { 532 | isa = XCBuildConfiguration; 533 | buildSettings = { 534 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 535 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 536 | CODE_SIGN_ENTITLEMENTS = Vimari/Vimari.entitlements; 537 | CODE_SIGN_IDENTITY = "Mac Developer"; 538 | CODE_SIGN_STYLE = Automatic; 539 | COMBINE_HIDPI_IMAGES = YES; 540 | CURRENT_PROJECT_VERSION = 10; 541 | DEVELOPMENT_TEAM = Y48UDGWSSQ; 542 | ENABLE_HARDENED_RUNTIME = YES; 543 | INFOPLIST_FILE = Vimari/Info.plist; 544 | LD_RUNPATH_SEARCH_PATHS = ( 545 | "$(inherited)", 546 | "@executable_path/../Frameworks", 547 | ); 548 | MARKETING_VERSION = 2.1.1; 549 | PRODUCT_BUNDLE_IDENTIFIER = net.televator.Vimari; 550 | PRODUCT_NAME = "$(TARGET_NAME)"; 551 | PROVISIONING_PROFILE_SPECIFIER = ""; 552 | SWIFT_VERSION = 5.0; 553 | }; 554 | name = Debug; 555 | }; 556 | E380F2762331806500640547 /* Release */ = { 557 | isa = XCBuildConfiguration; 558 | buildSettings = { 559 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 560 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 561 | CODE_SIGN_ENTITLEMENTS = Vimari/Vimari.entitlements; 562 | CODE_SIGN_IDENTITY = "Mac Developer"; 563 | CODE_SIGN_STYLE = Automatic; 564 | COMBINE_HIDPI_IMAGES = YES; 565 | CURRENT_PROJECT_VERSION = 10; 566 | DEVELOPMENT_TEAM = Y48UDGWSSQ; 567 | ENABLE_HARDENED_RUNTIME = YES; 568 | INFOPLIST_FILE = Vimari/Info.plist; 569 | LD_RUNPATH_SEARCH_PATHS = ( 570 | "$(inherited)", 571 | "@executable_path/../Frameworks", 572 | ); 573 | MARKETING_VERSION = 2.1.1; 574 | PRODUCT_BUNDLE_IDENTIFIER = net.televator.Vimari; 575 | PRODUCT_NAME = "$(TARGET_NAME)"; 576 | PROVISIONING_PROFILE_SPECIFIER = ""; 577 | SWIFT_VERSION = 5.0; 578 | }; 579 | name = Release; 580 | }; 581 | /* End XCBuildConfiguration section */ 582 | 583 | /* Begin XCConfigurationList section */ 584 | E380F2412331806400640547 /* Build configuration list for PBXProject "Vimari" */ = { 585 | isa = XCConfigurationList; 586 | buildConfigurations = ( 587 | E380F26E2331806500640547 /* Debug */, 588 | E380F26F2331806500640547 /* Release */, 589 | ); 590 | defaultConfigurationIsVisible = 0; 591 | defaultConfigurationName = Release; 592 | }; 593 | E380F2702331806500640547 /* Build configuration list for PBXNativeTarget "Vimari Extension" */ = { 594 | isa = XCConfigurationList; 595 | buildConfigurations = ( 596 | E380F2712331806500640547 /* Debug */, 597 | E380F2722331806500640547 /* Release */, 598 | ); 599 | defaultConfigurationIsVisible = 0; 600 | defaultConfigurationName = Release; 601 | }; 602 | E380F2742331806500640547 /* Build configuration list for PBXNativeTarget "Vimari" */ = { 603 | isa = XCConfigurationList; 604 | buildConfigurations = ( 605 | E380F2752331806500640547 /* Debug */, 606 | E380F2762331806500640547 /* Release */, 607 | ); 608 | defaultConfigurationIsVisible = 0; 609 | defaultConfigurationName = Release; 610 | }; 611 | /* End XCConfigurationList section */ 612 | }; 613 | rootObject = E380F23E2331806400640547 /* Project object */; 614 | } 615 | -------------------------------------------------------------------------------- /Vimari.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Vimari.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Vimari/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | @NSApplicationMain 4 | class AppDelegate: NSObject, NSApplicationDelegate { 5 | func applicationDidFinishLaunching(_: Notification) { 6 | // Insert code here to initialize your application 7 | } 8 | 9 | func applicationWillTerminate(_: Notification) { 10 | // Insert code here to tear down your application 11 | } 12 | 13 | func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { 14 | return true 15 | } 16 | 17 | @IBAction func openHelpUrl(_ sender: Any) { 18 | NSWorkspace.shared.open(URL(string: "https://github.com/televator-apps/vimari#usage")!) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Vimari/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/televator-apps/vimari/0d31b6af58f8779fb1896d566f11d8ed586e5fb8/Vimari/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Vimari/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/televator-apps/vimari/0d31b6af58f8779fb1896d566f11d8ed586e5fb8/Vimari/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /Vimari/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/televator-apps/vimari/0d31b6af58f8779fb1896d566f11d8ed586e5fb8/Vimari/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /Vimari/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/televator-apps/vimari/0d31b6af58f8779fb1896d566f11d8ed586e5fb8/Vimari/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /Vimari/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/televator-apps/vimari/0d31b6af58f8779fb1896d566f11d8ed586e5fb8/Vimari/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /Vimari/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/televator-apps/vimari/0d31b6af58f8779fb1896d566f11d8ed586e5fb8/Vimari/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /Vimari/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/televator-apps/vimari/0d31b6af58f8779fb1896d566f11d8ed586e5fb8/Vimari/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /Vimari/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} -------------------------------------------------------------------------------- /Vimari/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 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 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | -------------------------------------------------------------------------------- /Vimari/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1671\cocoasubrtf600 2 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 6 | \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 7 | 8 | \f0\fs24 \cf0 Full source is available at {\field{\*\fldinst{HYPERLINK "https://github.com/televator-apps/vimari"}}{\fldrslt github.com/televator-apps/vimari}}. Product details are at {\field{\*\fldinst{HYPERLINK "https://televator.net/vimari/"}}{\fldrslt televator.net/vimari}}. Vimari is heavily derived from {\field{\*\fldinst{HYPERLINK "https://vimium.github.io"}}{\fldrslt Vimium}}. } -------------------------------------------------------------------------------- /Vimari/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | ITSAppUsesNonExemptEncryption 24 | 25 | LSApplicationCategoryType 26 | public.app-category.productivity 27 | LSMinimumSystemVersion 28 | $(MACOSX_DEPLOYMENT_TARGET) 29 | NSHumanReadableCopyright 30 | Copyright © 2019 Televator, Guy Halford-Thompson, Simon Egersand, Phil Crosby, Ilya Sukhar, and other contributors. MIT Licensed 31 | NSMainStoryboardFile 32 | Main 33 | NSPrincipalClass 34 | NSApplication 35 | NSSupportsAutomaticTermination 36 | 37 | NSSupportsSuddenTermination 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Vimari/ViewController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import SafariServices.SFSafariApplication 3 | import OSLog 4 | 5 | class ViewController: NSViewController { 6 | @IBOutlet var extensionStatus: NSTextField! 7 | @IBOutlet var spinner: NSProgressIndicator! 8 | 9 | private enum Constant { 10 | static let extensionIdentifier = "net.televator.Vimari.SafariExtension" 11 | static let openSettings = "openSettings" 12 | static let resetSettings = "resetSettings" 13 | } 14 | 15 | func refreshExtensionStatus() { 16 | NSLog("Refreshing extension status") 17 | spinner.startAnimation(self) 18 | extensionStatus.stringValue = "Checking extension status" 19 | 20 | if SFSafariServicesAvailable() { 21 | SFSafariExtensionManager.getStateOfSafariExtension( 22 | withIdentifier: Constant.extensionIdentifier) { 23 | state, error in 24 | print("State", state as Any, "Error", error as Any, state?.isEnabled as Any) 25 | 26 | DispatchQueue.main.async { 27 | // TODO: handle this getting updated in the Safari preferences too. 28 | if let state = state { 29 | if state.isEnabled { 30 | self.extensionStatus.stringValue = "Enabled" 31 | } else { 32 | self.extensionStatus.stringValue = "Disabled" 33 | } 34 | } 35 | if let error = error { 36 | NSLog("Error", error.localizedDescription) 37 | self.extensionStatus.stringValue = error.localizedDescription 38 | } 39 | self.spinner.stopAnimation(self) 40 | } 41 | } 42 | } else { 43 | NSLog("SFSafariServices not available") 44 | extensionStatus.stringValue = "Unavailable, Vimari requires Safari 10 or greater." 45 | spinner.stopAnimation(self) 46 | } 47 | } 48 | 49 | @IBAction func refreshButton(_: NSButton) { 50 | refreshExtensionStatus() 51 | } 52 | 53 | override func viewDidLoad() { 54 | super.viewDidLoad() 55 | 56 | refreshExtensionStatus() 57 | } 58 | 59 | @IBAction func openSafariExtensionPreferences(_: AnyObject?) { 60 | SFSafariApplication.showPreferencesForExtension( 61 | withIdentifier: Constant.extensionIdentifier) { error in 62 | if let _ = error { 63 | // Insert code to inform the user that something went wrong. 64 | } 65 | } 66 | } 67 | 68 | @IBAction func openSettingsAction(_ sender: Any) { 69 | dispatchOpenSettings() 70 | } 71 | 72 | @IBAction func resetSettingsAction(_ sender: Any) { 73 | dispatchResetSettings() 74 | } 75 | 76 | func dispatchOpenSettings() { 77 | SFSafariApplication.dispatchMessage( 78 | withName: Constant.openSettings, 79 | toExtensionWithIdentifier: Constant.extensionIdentifier, 80 | userInfo: nil) { (error) in 81 | if let error = error { 82 | print(error.localizedDescription) 83 | } 84 | } 85 | } 86 | 87 | func dispatchResetSettings() { 88 | SFSafariApplication.dispatchMessage( 89 | withName: Constant.resetSettings, 90 | toExtensionWithIdentifier: Constant.extensionIdentifier, 91 | userInfo: nil) { (error) in 92 | if let error = error { 93 | print(error.localizedDescription) 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Vimari/Vimari.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/Download_on_the_Mac_App_Store_Badge_US.svg: -------------------------------------------------------------------------------- 1 | 2 | Download_on_the_Mac_App_Store_Badge_US-UK_RGB_blk_092917 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /assets/logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/televator-apps/vimari/0d31b6af58f8779fb1896d566f11d8ed586e5fb8/assets/logo.sketch -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logo 5 | Created with Sketch. 6 | 7 | 25 | 26 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/televator-apps/vimari/0d31b6af58f8779fb1896d566f11d8ed586e5fb8/assets/screenshot.png -------------------------------------------------------------------------------- /assets/screenshot.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/televator-apps/vimari/0d31b6af58f8779fb1896d566f11d8ed586e5fb8/assets/screenshot.psd -------------------------------------------------------------------------------- /assets/vimlogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 14 | 15 | 17 | image/svg+xml 18 | 20 | 21 | 22 | 23 | 26 | 29 | 33 | 37 | 41 | 45 | 49 | 53 | 57 | 61 | 66 | 70 | 74 | 78 | 82 | 87 | 91 | 95 | 98 | 102 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /docs/safari_12.md: -------------------------------------------------------------------------------- 1 | # Installation notes for Safari version 12 and 13 2 | 3 | A new version of macOS is being released, macOS Mojave, and it's expected to 4 | have a stable release out September or October of 2018. With that new version 5 | comes Safari 12, and a [completely new way of dealing with browser 6 | extensions](https://developer.apple.com/documentation/safariservices/safari_app_extensions). 7 | [We have had some issues](./crowdfunding.md) related to releasing new version 8 | of this extension, but they are now fixed and it's possible to install a version 9 | of vimari for Safari 12. 10 | 11 | ## How to install 12 | **Note: We are currently working on improving this installation flow, as well 13 | as the extension itself. Because vimari now has to be released as a _Safari 14 | App Extension_ instead of a _Safari Extension_ it requires some fundamental 15 | changes to the code. We can't guarantee that all the features work in 16 | this version. It's a learning process for us so bare with us.** 17 | 18 | 1. Clone this repo 19 | ```sh 20 | $ git clone git@github.com:guyht/vimari.git 21 | ``` 22 | 2. Open the Swift project located at `/Vimari.xcodeproj` in Xcode 23 | 3. Configure the Signing settings for both the `vimari` and `extension` targets 24 | to use your information rather than the Vimari team's (see [this SO answer](https://stackoverflow.com/questions/39754341/none-of-your-accounts-are-a-member-code-signing-errors-after-upgrading-to-xcode) 25 | for more information). 26 | 4. If you want different settings than the default ones, make your changes in 27 | `settings.js`. You can always come back later to change the settings again. 28 | 5. Build and run the project (`⌘ + r`) 29 | 6. An empty GUI box will show up - ignore it (we'll fix it later). Go to 30 | Safari and open up settings (`⌘ + ,`). Go to _Extensions_ and you should 31 | see **vimari** in the list of extensions. Enable it. 32 | 7. You may now press stop in Xcode and close Xcode. The extension will be 33 | available even if you restart Safari. 34 | 35 | This was tested on High Sierra with Safari Technology Preview (version 12). Let 36 | us know if something is not working for you. 37 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: [ 3 | "./Vimari Extension/js/mocks.js", 4 | "./Vimari Extension/json/defaultSettings.json", 5 | "./Vimari Extension/js/lib/mousetrap.js", 6 | "./Vimari Extension/js/injected.js" 7 | ] 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vimari", 3 | "author": "Guy Halford-Thompson", 4 | "license": "MIT", 5 | "homepage": "https://github.com/guyht/vimari", 6 | "devDependencies": { 7 | "expect.js": "^0.3.1", 8 | "jest": "^24.9.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/vimari.spec.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js'); 2 | 3 | describe('isExcludedUrl', () => { 4 | const isExcludedUrl = window.isExcludedUrl; 5 | 6 | it('returns true on same exact domain', () => { 7 | const excludedUrl = 'specific-domain.com'; 8 | const currentUrl = excludedUrl; 9 | expect(isExcludedUrl(excludedUrl, currentUrl)).to.be.ok(); 10 | }); 11 | 12 | it('returns true on duplicate domains', () => { 13 | const excludedUrls = 'specific-domain.com,specific-domain.com'; 14 | const currentUrl = 'specific-domain.com'; 15 | expect(isExcludedUrl(excludedUrls, currentUrl)).to.be.ok(); 16 | }); 17 | 18 | it('returns true if any domain match', () => { 19 | const excludedUrls = 'different-domain.com,specific-domain.com'; 20 | const currentUrl = 'specific-domain.com'; 21 | expect(isExcludedUrl(excludedUrls, currentUrl)).to.be.ok(); 22 | }); 23 | 24 | it('returns true on comma separated domains', () => { 25 | const excludedUrls = 'specific-domain.com,different-domain.com'; 26 | const currentUrl = 'specific-domain.com'; 27 | expect(isExcludedUrl(excludedUrls, currentUrl)).to.be.ok(); 28 | }); 29 | 30 | it('returns false on different domain', () => { 31 | const excludedUrl = 'www.different-domain.com'; 32 | const currentUrl = 'specific-domain.com'; 33 | expect(isExcludedUrl(excludedUrl, currentUrl)).to.not.be.ok(); 34 | }); 35 | 36 | it('returns false if no domains match', () => { 37 | const excludedUrls = 'www.different-domain.com,www.different-domain-2.com'; 38 | const currentUrl = 'specific-domain.com'; 39 | expect(isExcludedUrl(excludedUrls, currentUrl)).to.not.be.ok(); 40 | }); 41 | 42 | it('returns false on space separated domains', () => { 43 | const excludedUrls = 'specific-domain.com different-domain.com'; 44 | const currentUrl = 'specific-domain.com'; 45 | expect(isExcludedUrl(excludedUrls, currentUrl)).to.not.be.ok(); 46 | }); 47 | 48 | it('returns true on string added in front of current URL', () => { 49 | const excludedUrl = 'specific-domain.com'; 50 | const currentUrl = 'http://specific-domain.com'; 51 | expect(isExcludedUrl(excludedUrl, currentUrl)).to.be.ok(); 52 | }); 53 | 54 | it('returns true on string appended to current URL', () => { 55 | const excludedUrl = 'specific-domain.com'; 56 | const currentUrl = 'specific-domain.com/arbitrary-string'; 57 | expect(isExcludedUrl(excludedUrl, currentUrl)).to.be.ok(); 58 | }); 59 | 60 | it('returns true on string added on both sides of current URL', () => { 61 | const excludedUrl = 'specific-domain.com'; 62 | const currentUrl = 'http://specific-domain.com/arbitrary-string'; 63 | expect(isExcludedUrl(excludedUrl, currentUrl)).to.be.ok(); 64 | }); 65 | 66 | it('returns true if current URL is less specific than excluded domain', () => { 67 | let excludedUrl = 'http://specific-domain.com'; 68 | let currentUrl = 'specific-domain.com'; 69 | expect(isExcludedUrl(excludedUrl, currentUrl)).to.be.ok(); 70 | 71 | excludedUrl = 'http://www.specific-domain.com'; 72 | currentUrl = 'specific-domain.com'; 73 | expect(isExcludedUrl(excludedUrl, currentUrl)).to.be.ok(); 74 | }); 75 | 76 | it('returns true if current URL with appended string is less specific than excluded domain', () => { 77 | const excludedUrl = 'http://specific-domain.com'; 78 | const currentUrl = 'specific-domain.com/arbitrary-string'; 79 | expect(isExcludedUrl(excludedUrl, currentUrl)).to.be.ok(); 80 | }); 81 | 82 | it('returns true even though cases doesn\'t match', () => { 83 | let excludedUrl = 'SPECIFIC-DOMAIN.com'; 84 | let currentUrl = 'specific-domain.com'; 85 | expect(isExcludedUrl(excludedUrl, currentUrl)).to.be.ok(); 86 | 87 | excludedUrl = 'specific-domain.com'; 88 | currentUrl = 'SPECIFIC-DOMAIN.com'; 89 | expect(isExcludedUrl(excludedUrl, currentUrl)).to.be.ok(); 90 | }); 91 | }); 92 | 93 | describe('stripProtocolAndWww', () => { 94 | const stripProtocolAndWww = window.stripProtocolAndWww; 95 | 96 | it('strips http', () => { 97 | const url = 'http://specific-domain.com'; 98 | expect(stripProtocolAndWww(url)).to.equal('specific-domain.com'); 99 | }); 100 | 101 | it('strips https', () => { 102 | const url = 'https://specific-domain.com'; 103 | expect(stripProtocolAndWww(url)).to.equal('specific-domain.com'); 104 | }); 105 | 106 | it('strips www', () => { 107 | const url = 'www.specific-domain.com'; 108 | expect(stripProtocolAndWww(url)).to.equal('specific-domain.com'); 109 | }); 110 | 111 | it('strips http and www', () => { 112 | const url = 'http://www.specific-domain.com'; 113 | expect(stripProtocolAndWww(url)).to.equal('specific-domain.com'); 114 | }); 115 | 116 | it('strips https and www', () => { 117 | const url = 'https://www.specific-domain.com'; 118 | expect(stripProtocolAndWww(url)).to.equal('specific-domain.com'); 119 | }); 120 | }); 121 | --------------------------------------------------------------------------------