├── .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 | [](https://apps.apple.com/us/app/vimari/id1480933944?ls=1&mt=12)
7 |
8 | 
9 | 
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 | [](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 |
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 |
52 |
--------------------------------------------------------------------------------
/assets/logo.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/televator-apps/vimari/0d31b6af58f8779fb1896d566f11d8ed586e5fb8/assets/logo.sketch
--------------------------------------------------------------------------------
/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------