├── .gitignore
├── README.markdown
├── keysearch.safariextension
├── Info.plist
├── Settings.plist
├── global.html
├── index.css
├── index.html
├── toolbar.png
└── toolbar@2x.png
├── package.json
├── resources
└── toolbar.idraw
├── src
├── actions
│ ├── index.js
│ └── rules.js
├── components
│ ├── Button.jsx
│ ├── Rule.jsx
│ ├── TokenInput.jsx
│ └── index.js
├── constants
│ └── actionTypes.js
├── containers
│ ├── App.jsx
│ ├── Root.jsx
│ └── RulesApp.jsx
├── global.js
├── index.jsx
├── middleware
│ └── api.js
├── reducers
│ ├── index.js
│ └── rules.js
├── routes.js
├── store.js
├── store
│ └── configureStore.js
├── styles
│ ├── components.scss
│ ├── components
│ │ ├── _button.scss
│ │ ├── _rule.scss
│ │ ├── _rules-app.scss
│ │ └── _token-input.scss
│ └── index.scss
└── utils.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | keysearch.safariextension/*.js
3 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | # DEPRECATED: Safari no longer supports extensions that modify the behaviour of the address bar. Consider using a separate tool, such as Alfred: https://www.alfredapp.com
2 |
3 | ---
4 |
5 | # KeySearch
6 |
7 | **_A keyword search extension for Safari._**
8 |
9 | **[Download](http://www.macosxtips.co.uk/keysearch)**
10 |
11 | ## Introduction
12 |
13 | **_KeySearch_ adds a new way of searching the Internet to Safari.** It uses short keywords to allow you to search any site you want, right from the toolbar.
14 |
15 | While browsing the web, just press ^S (or click the toolbar button) to bring up the KeySearch bar. Type a keyword, then your search query, and hit ⏎ , and KeySearch will find the site you want to search and perform the search for you.
16 |
17 | No more going to Google first!
18 |
19 | ## Examples
20 |
21 | KeySearch comes with plenty of keywords that are ready for you to use.
22 |
23 | **Want to see videos of cute kittens?** `youtube cute kittens`
24 |
25 | **Forgotten who directed *Star Trek*?** `imdb Star Trek 2009`
26 |
27 | **Need directions for a trip?** `map New York to LA`
28 |
29 | ## Creating Keywords
30 |
31 | To create a new keyword, just do the following:
32 |
33 | - right-click on any search box in a web page
34 | - choose “Create keyword for this search”
35 | - type in a keyword
36 |
37 | …And let KeySearch take care of the rest.
38 |
39 | You can keep track of all your keywords by clicking “Edit Keywords” in the KeySearch bar. Here, you can manually create new keywords, add specific keyboard shortcuts for each keyword, and delete (or temporarily disable) keywords.
40 |
41 | ## Customisations
42 |
43 | There are a few different ways you can search using Keysearch:
44 |
45 | - Click the toolbar button to bring up the popover search
46 |
47 | - Use a keyboard shortcut to bring up the popover search (configurable in settings)
48 |
49 | - Use a keyboard shortcut to search using a semi-transparent “HUD” over the current web page
50 |
51 | - Search using the address bar (or “omnibar” in Safari 6). The keyboard shortcut ⌘L can be used to activate the address bar.
52 |
53 | ## Sites that don’t have search
54 |
55 | Some sites don’t have their own search engines, but you can still use KeySearch to search them.
56 |
57 | Instead of a keyword, just type `>example.com query` to search any site using Google Site Search. If you want to search the site that is currently open, just use `> query`.
58 |
59 | ## Faster Bookmarks
60 |
61 | Keywords aren’t just great for search. You can assign them to any web address you want, and use them as a quick way to access your favourite sites. Use `yt` to take you to YouTube, or `fb` to take you to Facebook.
62 |
--------------------------------------------------------------------------------
/keysearch.safariextension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Author
6 | Matt Swain
7 | Builder Version
8 | 12602.3.3
9 | CFBundleDisplayName
10 | KeySearch
11 | CFBundleIdentifier
12 | com.matt-swain.keysearch
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleShortVersionString
16 | 3.0.0
17 | CFBundleVersion
18 | 3.0.0
19 | Chrome
20 |
21 | Context Menu Items
22 |
23 |
24 | Command
25 | keySearchContext
26 | Identifier
27 | keySearchContext
28 | Title
29 | Create a keyword for this search
30 |
31 |
32 | Global Page
33 | global.html
34 | Toolbar Items
35 |
36 |
37 | Command
38 | keySearchButton
39 | Identifier
40 | keySearchButton
41 | Image
42 | toolbar.png
43 | Label
44 | KeySearch
45 | Palette Label
46 | KeySearch
47 | Tool Tip
48 | KeySearch
49 |
50 |
51 |
52 | Description
53 | Use keywords to quickly search any site from the address bar
54 | DeveloperIdentifier
55 | 6M853ET88Q
56 | ExtensionInfoDictionaryVersion
57 | 1.0
58 | Permissions
59 |
60 | Website Access
61 |
62 | Include Secure Pages
63 |
64 | Level
65 | None
66 |
67 |
68 | Update Manifest URL
69 | http://matt-swain.com/keysearch/keysearch.plist
70 | Website
71 | http://matt-swain.com/keysearch
72 |
73 |
74 |
--------------------------------------------------------------------------------
/keysearch.safariextension/Settings.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DefaultValue
7 | false
8 | Key
9 | openSettings
10 | Title
11 | Click here to open the KeySearch settings page and edit your rules. You can also access the settings page by clicking on the KeySearch toolbar button.
12 | Type
13 | CheckBox
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/keysearch.safariextension/global.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/keysearch.safariextension/index.css:
--------------------------------------------------------------------------------
1 | .button {
2 | background-color: #fff;
3 | font-size: 14px;
4 | border: none;
5 | border-radius: 4px;
6 | cursor: pointer;
7 | color: #444;
8 | margin: 0;
9 | padding: 2px 6px 3px 6px;
10 | transition: background-color 200ms linear, color 200ms linear; }
11 | .button__icon {
12 | height: 1.4em;
13 | width: 1.4em; }
14 | .button__text {
15 | vertical-align: middle;
16 | line-height: 1.4em; }
17 | .button:hover {
18 | color: #fff;
19 | background-color: #444; }
20 |
21 | .rules-app__add {
22 | margin: 10px 5px 0 0;
23 | float: left;
24 | color: #00bc00; }
25 | .rules-app__add:hover {
26 | background-color: #00bc00; }
27 |
28 | .rules-app__import {
29 | margin: 10px 5px 0 0;
30 | float: left; }
31 |
32 | .rules-app__export {
33 | margin: 10px 5px 0 0;
34 | float: left; }
35 |
36 | .rule {
37 | padding: 20px 0; }
38 | .rule.is-disabled {
39 | opacity: 0.5; }
40 | .rule__header {
41 | display: flex; }
42 | .rule__body {
43 | position: relative;
44 | display: flex; }
45 | .rule__name {
46 | margin: 0 5px 5px 0;
47 | flex: 1 80%; }
48 | .rule__enable {
49 | margin: 2px 0 8px 5px; }
50 | .rule__delete {
51 | margin: 2px 0 8px 5px;
52 | color: #d40000; }
53 | .rule__delete:hover {
54 | background-color: #d40000; }
55 | .rule__key {
56 | position: relative;
57 | flex: 1 25%;
58 | padding: 8px;
59 | background: #e9e9e9;
60 | border-top-left-radius: 5px;
61 | border-bottom-left-radius: 5px; }
62 | .rule__key .is-token {
63 | background: #e9e9e9; }
64 | .rule__key:after {
65 | content: '';
66 | display: block;
67 | right: -46px;
68 | position: absolute;
69 | top: 0px;
70 | width: 0;
71 | height: 0;
72 | border-left: 23px solid #e9e9e9;
73 | border-right: 23px solid transparent;
74 | border-top: 23px solid transparent;
75 | border-bottom: 23px solid transparent; }
76 | .rule__url {
77 | flex: 1 75%;
78 | padding: 8px 8px 8px 31px;
79 | background: #f4f4f4;
80 | border-top-right-radius: 5px;
81 | border-bottom-right-radius: 5px; }
82 | .rule__url .is-token {
83 | background: #f4f4f4; }
84 | .rule__chevron::before {
85 | border-style: solid;
86 | border-width: 0.25em 0.25em 0 0;
87 | content: '';
88 | display: inline-block;
89 | height: 0.65em;
90 | width: 0.65em;
91 | position: relative;
92 | top: 1em;
93 | transform: rotate(45deg);
94 | vertical-align: top;
95 | left: 0; }
96 |
97 | .token-input {
98 | position: relative;
99 | height: 30px;
100 | color: #444; }
101 | .token-input__input {
102 | background: transparent;
103 | outline: none;
104 | border: 1px solid transparent;
105 | position: absolute;
106 | margin: 0;
107 | top: 0;
108 | bottom: 0;
109 | left: 0;
110 | right: 0;
111 | font-size: inherit;
112 | font-weight: inherit;
113 | color: inherit;
114 | line-height: 1;
115 | padding: 8px 4px; }
116 | .token-input__input:hover {
117 | border: 1px solid #ddd; }
118 | .token-input__input:focus {
119 | border: 1px solid #498feb; }
120 | .token-input__overlay {
121 | pointer-events: none;
122 | color: transparent;
123 | position: absolute;
124 | top: 0;
125 | bottom: 0;
126 | left: 0;
127 | right: 0;
128 | line-height: 1;
129 | padding: 7px 4px;
130 | white-space: pre;
131 | overflow: hidden;
132 | border: 1px solid transparent; }
133 | .token-input__overlay.is-focused {
134 | opacity: 0; }
135 | .token-input__span.is-token {
136 | padding: 0 4px; }
137 | .token-input__span.is-token > span {
138 | border-radius: 3px;
139 | border: 2px solid #498feb;
140 | padding: 1px 5px 2px;
141 | color: #498feb; }
142 |
143 | html {
144 | box-sizing: border-box; }
145 |
146 | body {
147 | margin: 0;
148 | padding: 0 10px;
149 | font-family: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
150 | font-size: 14px;
151 | line-height: 1.5;
152 | color: #444; }
153 |
154 | h2 {
155 | font-weight: normal;
156 | font-size: 24px; }
157 |
158 | .container {
159 | max-width: 970px;
160 | margin: 0 auto 50px; }
161 |
162 | .row:after {
163 | content: '';
164 | display: block;
165 | clear: both; }
166 |
167 | .navbar {
168 | height: 58px;
169 | border-bottom: 2px solid #444; }
170 | .navbar__brand {
171 | float: left;
172 | margin: 20px 0 10px 0;
173 | line-height: 28px; }
174 | .navbar__nav {
175 | float: right;
176 | padding: 0;
177 | margin: 20px 0 0 0;
178 | list-style: none; }
179 | .navbar__item {
180 | float: left;
181 | position: relative;
182 | display: block; }
183 | .navbar__link {
184 | line-height: 38px;
185 | padding: 20px 5px 10px 5px;
186 | font-weight: bold;
187 | text-decoration: none;
188 | font-size: 16px;
189 | border-bottom: 2px solid transparent; }
190 | .navbar__link.active {
191 | color: #498feb;
192 | border-bottom: 2px solid #498feb; }
193 |
--------------------------------------------------------------------------------
/keysearch.safariextension/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | KeySearch
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/keysearch.safariextension/toolbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcs07/KeySearch/eca6152c719a0435630979b852f3933f39fbdaf6/keysearch.safariextension/toolbar.png
--------------------------------------------------------------------------------
/keysearch.safariextension/toolbar@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcs07/KeySearch/eca6152c719a0435630979b852f3933f39fbdaf6/keysearch.safariextension/toolbar@2x.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "KeySearch",
3 | "version": "3.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "NODE_ENV=development webpack --progress --colors --watch --devtool eval-source-map",
7 | "build": "NODE_ENV=production webpack -p --config ./webpack.config.js"
8 | },
9 | "devDependencies": {
10 | "babel-loader": "^6.2.8",
11 | "babel-plugin-transform-runtime": "^6.15.0",
12 | "babel-preset-es2015": "^6.18.0",
13 | "babel-preset-react": "^6.16.0",
14 | "babel-preset-stage-0": "^6.16.0",
15 | "bem-cn": "^2.1.3",
16 | "css-loader": "^0.26.0",
17 | "extract-text-webpack-plugin": "^2.0.0-beta.4",
18 | "file-loader": "^0.9.0",
19 | "file-saver": "^1.3.3",
20 | "json-loader": "^0.5.4",
21 | "lodash": "^4.17.2",
22 | "node-sass": "^3.13.0",
23 | "node-uuid": "^1.4.7",
24 | "react": "^15.4.1",
25 | "react-dom": "^15.4.1",
26 | "react-dropzone": "^3.7.3",
27 | "react-icons": "^2.2.1",
28 | "react-loader": "^2.4.0",
29 | "react-redux": "^4.4.6",
30 | "react-router": "^3.0.0",
31 | "react-router-redux": "^4.0.7",
32 | "redux": "^3.6.0",
33 | "redux-logger": "^2.7.4",
34 | "redux-thunk": "^2.1.0",
35 | "sass-loader": "^4.0.2",
36 | "semver": "^5.3.0",
37 | "strip-loader": "^0.1.2",
38 | "style-loader": "^0.13.1",
39 | "velocity-react": "^1.1.11",
40 | "webpack": "^2.1.0-beta.27"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/resources/toolbar.idraw:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcs07/KeySearch/eca6152c719a0435630979b852f3933f39fbdaf6/resources/toolbar.idraw
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export {loadRules, loadRule, createRule, updateRule, deleteRule} from './rules'
2 |
--------------------------------------------------------------------------------
/src/actions/rules.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/actionTypes'
2 | import {CALL_API} from '../middleware/api'
3 |
4 |
5 | export function loadRules() {
6 | return {
7 | [CALL_API]: {
8 | types: [types.RULES_REQUEST, types.RULES_SUCCESS, types.RULES_FAILURE],
9 | method: 'all'
10 | }
11 | }
12 | }
13 |
14 |
15 | export function loadRule(id) {
16 | return {
17 | [CALL_API]: {
18 | types: [types.RULE_REQUEST, types.RULE_SUCCESS, types.RULE_FAILURE],
19 | method: 'get',
20 | args: [id]
21 | }
22 | }
23 | }
24 |
25 |
26 | export function createRule(rule) {
27 | return {
28 | [CALL_API]: {
29 | types: [types.CREATE_RULE_REQUEST, types.CREATE_RULE_SUCCESS, types.CREATE_RULE_FAILURE],
30 | method: 'create',
31 | args: [rule]
32 | }
33 | }
34 | }
35 |
36 |
37 | export function updateRule(rule) {
38 | return {
39 | [CALL_API]: {
40 | types: [types.UPDATE_RULE_REQUEST, types.UPDATE_RULE_SUCCESS, types.UPDATE_RULE_FAILURE],
41 | method: 'update',
42 | args: [rule]
43 | }
44 | }
45 | }
46 |
47 |
48 | export function deleteRule(id) {
49 | return {
50 | [CALL_API]: {
51 | types: [types.DELETE_RULE_REQUEST, types.DELETE_RULE_SUCCESS, types.DELETE_RULE_FAILURE],
52 | method: 'delete',
53 | args: [id]
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import block from 'bem-cn'
3 | import MdAdd from 'react-icons/lib/md/add'
4 | import MdClear from 'react-icons/lib/md/clear'
5 | import MdPause from 'react-icons/lib/md/pause'
6 | import MdPlayArrow from 'react-icons/lib/md/play-arrow'
7 | import MdDelete from 'react-icons/lib/md/delete'
8 | import MdFileUpload from 'react-icons/lib/md/file-upload'
9 | import MdFileDownload from 'react-icons/lib/md/file-download'
10 |
11 |
12 | export default class Button extends React.Component {
13 |
14 | render() {
15 | let {icon, className, children, ...props} = this.props
16 | let b = block('button')
17 | let iconEl = null
18 | switch (icon) {
19 | case 'add': iconEl = MdAdd
20 | break
21 | case 'clear': iconEl = MdClear
22 | break
23 | case 'pause': iconEl = MdPause
24 | break
25 | case 'play': iconEl = MdPlayArrow
26 | break
27 | case 'delete': iconEl = MdDelete
28 | break
29 | case 'export': iconEl = MdFileDownload
30 | break
31 | case 'import': iconEl = MdFileUpload
32 | break
33 | }
34 | return (
35 |
36 | {React.createElement(iconEl, {className: b('icon')})}
37 | {children}
38 |
39 | )
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/Rule.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import block from 'bem-cn'
3 | import Button from './Button'
4 | import TokenInput from './TokenInput'
5 |
6 |
7 | export default class Rule extends React.Component {
8 |
9 | handleKeyChange(value) {
10 | if (this.props.data.key !== value) {
11 | console.log(`Rule: Changing key ${this.props.data.key} => ${value}`)
12 | let data = Object.assign(this.props.data, {key: value})
13 | this.props.onUpdate(data)
14 | }
15 | }
16 |
17 | handleUrlChange(value) {
18 | if (this.props.data.url !== value) {
19 | console.log(`Rule: Changing url ${this.props.data.url} => ${value}`)
20 | let data = Object.assign(this.props.data, {url: value})
21 | this.props.onUpdate(data)
22 | }
23 | }
24 |
25 | handleNameChange(value) {
26 | if (this.props.data.name !== value) {
27 | console.log(`Rule: Changing name ${this.props.data.name} => ${value}`)
28 | let data = Object.assign(this.props.data, {name: value})
29 | this.props.onUpdate(data)
30 | }
31 | }
32 |
33 | handleEnable(e) {
34 | console.log(`Rule: Changing enabled ${this.props.data.enabled} => ${!this.props.data.enabled}`)
35 | let data = Object.assign(this.props.data, {enabled: !this.props.data.enabled})
36 | this.props.onUpdate(data)
37 | }
38 |
39 | handleDelete(e) {
40 | console.log(`Rule: Removing ${this.props.data.name} (${this.props.data.id})`)
41 | this.props.onDelete(this.props.data.id)
42 | }
43 |
44 | render() {
45 | let {data} = this.props
46 | let b = block('rule')
47 | let enableText = data.enabled ? 'Disable' : 'Enable'
48 | let enableIcon = data.enabled ? 'pause' : 'play'
49 | return (
50 |
51 |
52 |
53 | {enableText}
54 | Delete
55 |
56 |
60 |
61 | )
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/TokenInput.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import block from 'bem-cn'
3 |
4 | export default class TokenInput extends React.Component {
5 |
6 | constructor(props) {
7 | super(props)
8 | this.state = {currentValue: this.props.value, focused: false}
9 | }
10 |
11 | // handleClick(e) {
12 | // console.log('TokenInput clicked, focusing...')
13 | // if (!this.state.focused) {
14 | // this.refs.input.focus()
15 | // }
16 | // }
17 |
18 | handleChange(e) {
19 | this.setState({currentValue: e.target.value})
20 | }
21 |
22 | handleFocus(e) {
23 | console.log('TokenInput handleFocus')
24 | this.setState({focused: true})
25 | }
26 |
27 | handleBlur(e) {
28 | console.log(`TokenInput blur: ${e.target.value}`)
29 | this.props.onChange(e.target.value)
30 | this.setState({focused: false})
31 | }
32 |
33 | handleKeyDown(e) {
34 | // Tab, Return
35 | if ([9, 13].indexOf(e.keyCode) !== -1) {
36 | e.preventDefault()
37 | this.refs.input.blur()
38 | }
39 | }
40 |
41 | renderSpan(value, token) {
42 | let className = this.props.noTokens ? block('token-input')('span') : block('token-input')('span').state({token})
43 | return (
44 |
45 | {value}
46 |
47 | )
48 | }
49 |
50 | render() {
51 | let {currentValue, focused} = this.state
52 | let {value, placeholder} = this.props
53 | let b = block('token-input')
54 |
55 | // Insert spans in place of tokens
56 | let spans = []
57 | let re = /\{\{.+?\}\}/g
58 | let lastIndex = 0
59 | let match
60 | while ((match = re.exec(value)) !== null) {
61 | spans.push(this.renderSpan(value.substring(lastIndex, match.index), false))
62 | spans.push(this.renderSpan(value.substring(match.index + 2, match.index + match[0].length - 2), true))
63 | lastIndex = match.index + match[0].length
64 | }
65 | spans.push(this.renderSpan(value.substring(lastIndex), false))
66 | return (
67 |
81 | )
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export Button from './Button'
2 | export Rule from './Rule'
3 | export TokenInput from './TokenInput'
4 |
--------------------------------------------------------------------------------
/src/constants/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const RULES_REQUEST = 'RULES_REQUEST'
2 | export const RULES_SUCCESS = 'RULES_SUCCESS'
3 | export const RULES_FAILURE = 'RULES_FAILURE'
4 |
5 | export const RULE_REQUEST = 'RULE_REQUEST'
6 | export const RULE_SUCCESS = 'RULE_SUCCESS'
7 | export const RULE_FAILURE = 'RULE_FAILURE'
8 |
9 | export const CREATE_RULE_REQUEST = 'CREATE_RULE_REQUEST'
10 | export const CREATE_RULE_SUCCESS = 'CREATE_RULE_SUCCESS'
11 | export const CREATE_RULE_FAILURE = 'CREATE_RULE_FAILURE'
12 |
13 | export const UPDATE_RULE_REQUEST = 'UPDATE_RULE_REQUEST'
14 | export const UPDATE_RULE_SUCCESS = 'UPDATE_RULE_SUCCESS'
15 | export const UPDATE_RULE_FAILURE = 'UPDATE_RULE_FAILURE'
16 |
17 | export const DELETE_RULE_REQUEST = 'DELETE_RULE_REQUEST'
18 | export const DELETE_RULE_SUCCESS = 'DELETE_RULE_SUCCESS'
19 | export const DELETE_RULE_FAILURE = 'DELETE_RULE_FAILURE'
20 |
--------------------------------------------------------------------------------
/src/containers/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Link} from 'react-router'
3 | import block from 'bem-cn'
4 |
5 |
6 | export default class App extends React.Component {
7 | render() {
8 | let h = block('navbar')
9 | return (
10 |
11 |
12 |
KeySearch
13 |
14 |
15 | My Rules
16 |
17 |
18 |
19 | {this.props.children}
20 |
21 | )
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/containers/Root.jsx:
--------------------------------------------------------------------------------
1 | import React, {PropTypes} from 'react'
2 | import {Provider} from 'react-redux'
3 | import {Router} from 'react-router'
4 | import routes from '../routes'
5 |
6 |
7 | export default class Root extends React.Component {
8 |
9 | static propTypes = {
10 | store: PropTypes.object.isRequired,
11 | history: PropTypes.object.isRequired
12 | }
13 |
14 | render() {
15 | let {store, history} = this.props
16 | return (
17 |
18 |
19 |
20 |
21 |
22 | )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/containers/RulesApp.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {bindActionCreators} from 'redux'
3 | import {connect} from 'react-redux'
4 | import {VelocityTransitionGroup} from 'velocity-react'
5 | import * as actionCreators from '../actions'
6 | import {Button, Rule, TokenInput} from '../components'
7 | import block from 'bem-cn'
8 | import FileSaver from 'file-saver'
9 | import Dropzone from 'react-dropzone'
10 |
11 |
12 | class RulesApp extends React.Component {
13 |
14 | componentDidMount() {
15 | this.props.actions.loadRules()
16 | }
17 |
18 | handleAdd(e) {
19 | console.log('RulesApp handleAdd')
20 | this.props.actions.createRule({name: '', key: '', url: '', enabled: true})
21 | }
22 |
23 | handleImport(files, rejected) {
24 | console.log('RulesApp handleImport')
25 | if (files.length > 0) {
26 | console.log('Files: ', files);
27 | let reader = new FileReader()
28 | reader.onload = (function(createRule) {
29 | return function(e) {
30 | let rules = JSON.parse(e.target.result)
31 | console.log(rules)
32 | for (let rule of rules) {
33 | createRule(rule)
34 | }
35 | }
36 | })(this.props.actions.createRule)
37 | reader.readAsText(files[0]);
38 | }
39 | }
40 |
41 | handleExport(e) {
42 | console.log('RulesApp handleExport')
43 | console.log(this.props.rulesList.rules)
44 | // No ideal configuration here for now...
45 | // application/octet-stream => downloads, "Unknown" filename
46 | // application/json => displays with incorrect encoding, "Unknown.json" default name in save dialog
47 | // application/json;charset=utf-8 => fails completely
48 | // text/plain;charset=utf-8 => displays with correct encoding, "Unknown.css" default name in save dialog
49 | let blob = new Blob([JSON.stringify(this.props.rulesList.rules, null, 2)], {type: 'text/plain;charset=utf-8'})
50 | FileSaver.saveAs(blob, 'keysearch.json')
51 | }
52 |
53 | render() {
54 | let {rulesList, actions} = this.props
55 | let rows = !rulesList.rules ? [] : rulesList.rules.map((rule, i) => )
61 | let b = block('rules-app')
62 | return (
63 |
64 |
65 | Create New Rule
66 |
67 | Import Rules
68 |
69 | Export Rules
70 |
71 | {rulesList.loading &&
Loading rules...
}
72 | {rulesList.rules != null && rulesList.rules.length === 0 &&
You have no rules!
}
73 | {rulesList.rules != null &&
74 |
75 | {rows}
76 |
77 | }
78 |
79 | )
80 | }
81 | }
82 |
83 |
84 | const mapStateToProps = (state) => ({
85 | rulesList: state.rules.rulesList,
86 | // updateRule: state.rules.updateRule,
87 | // deleteRule: state.rules.deleteRule
88 | })
89 |
90 |
91 | const mapDispatchToProps = (dispatch) => ({
92 | actions: bindActionCreators(actionCreators, dispatch)
93 | })
94 |
95 |
96 | export default connect(mapStateToProps, mapDispatchToProps)(RulesApp)
97 |
--------------------------------------------------------------------------------
/src/global.js:
--------------------------------------------------------------------------------
1 | import semver from 'semver'
2 | import store from './store'
3 | const isInteger = require('lodash/isInteger')
4 |
5 |
6 | // Abbreviations
7 | const app = safari.application
8 | const ext = safari.extension
9 |
10 |
11 | // Safari Application event listeners
12 | app.addEventListener('command', performCommand, false)
13 | app.addEventListener('validate', validateCommand, false)
14 | app.addEventListener('beforeSearch', handleBeforeSearch, false)
15 | app.addEventListener('message', handleMessage, false)
16 | ext.settings.addEventListener('change', handleSettingChanged, false);
17 |
18 |
19 | // Debug logging
20 | console.log('KeySearch global page loaded')
21 | console.log('======')
22 | for (let k of store) {
23 | console.log(k)
24 | }
25 | console.log('======')
26 |
27 |
28 | // Runs once per browser session
29 | function init() {
30 | if (!('version' in ext.settings)) {
31 | // New installation
32 | console.log('New KeySearch installation. Doing initial setup...')
33 | // Initialize the KeySearch store with some example keywords
34 | store.addPresets()
35 | } else if (isInteger(ext.settings.version)) {
36 | // Upgrade older KeySearch where version was stored as an integer
37 | console.log('Upgrading KeySearch from version ' + ext.settings.version)
38 | store.upgrade()
39 | }
40 | ext.settings.version = '3.0.0'
41 | }
42 |
43 |
44 | // Accept search input via the address bar
45 | function handleBeforeSearch(e) {
46 | console.log('beforeSearch')
47 | console.log(e)
48 | let textEntered = e.query
49 | if (textEntered) {
50 | let urls = parseQuery(textEntered)
51 | if (urls.length > 0) {
52 | // Set the first URL as the target of the search event
53 | e.preventDefault()
54 | e.target.url = urls.shift()
55 | for (let url of urls) {
56 | // Open any additional URLs in background tabs
57 | app.activeBrowserWindow.openTab('background').url = url
58 | }
59 | }
60 | }
61 | }
62 |
63 |
64 | // Convert the entered text into URL(s) according to rules
65 | function parseQuery(textEntered) {
66 | console.log('Parsing input: ' + textEntered)
67 | const token_re = /\{\{(.*?)\}\}/g
68 | let urls = []
69 | for (let rule of store) {
70 | // Get each token in key
71 | let tokens = rule.key.match(token_re)
72 | // Replace tokens with (.+?) and compile to regex
73 | let query_re = new RegExp('^' + rule.key.replace(token_re, '(.+?)') + '$')
74 | let queries = textEntered.match(query_re)
75 | if (queries) {
76 | // Replace tokens in URL with corresponding query
77 | let url = rule.url
78 | queries.shift() // Remove first element (entire text entered)
79 | for (let i = 0; i < queries.length; i++) {
80 | url = url.split(tokens[i]).join(queries[i])
81 | }
82 | console.log('URL: ' + url)
83 | urls.push(url)
84 | }
85 | }
86 | return urls
87 | }
88 |
89 |
90 | // Open settings page
91 | function openSettings() {
92 | console.log('Opening KeySearch settings page')
93 | app.activeBrowserWindow.openTab('foreground').url = ext.baseURI + 'index.html'
94 | }
95 |
96 |
97 | // Perform commands
98 | function performCommand(e) {
99 | console.log('Command: ' + e.command)
100 | if (e.command === 'keySearchButton') {
101 | console.log('Toolbar button pressed')
102 | openSettings()
103 | } else if (e.command === 'keySearchContext') {
104 | console.log('Context menu item chosen')
105 | store.set(e.userInfo)
106 | openSettings()
107 | }
108 | }
109 |
110 |
111 | // Validate whether the context menu item should be shown
112 | function validateCommand(e) {
113 | if (e.userInfo && e.userInfo.url == 'noUrl') {
114 | e.target.disabled = true
115 | } else {
116 | e.target.disabled = false
117 | }
118 | }
119 |
120 |
121 | // Handle messages from injected script
122 | function handleMessage(e) {
123 | if (e.name === 'storeRequest') {
124 | console.log('global storeRequest')
125 | console.log(e)
126 | // Perform operation on keyword store and respond with result message
127 | let {id, method, args} = e.message
128 | let response = store[method].apply(store, args)
129 | e.target.page.dispatchMessage('storeResponse', {id, response})
130 | }
131 | }
132 |
133 |
134 | // Handle settings changes
135 | function handleSettingChanged(e) {
136 | console.log(e)
137 | if (e.key === 'openSettings') {
138 | openSettings()
139 | }
140 | }
141 |
142 |
143 | // Run init function when global page is loaded
144 | init()
145 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import {hashHistory} from 'react-router'
4 | import {syncHistoryWithStore} from 'react-router-redux'
5 | import Root from './containers/Root'
6 | import configureStore from './store/configureStore'
7 | import './styles/index.scss'
8 |
9 | const store = configureStore()
10 | const history = syncHistoryWithStore(hashHistory, store)
11 |
12 | ReactDOM.render(
13 | ,
14 | document.getElementById('root')
15 | )
16 |
--------------------------------------------------------------------------------
/src/middleware/api.js:
--------------------------------------------------------------------------------
1 | class StoreClient {
2 |
3 | constructor() {
4 | this.callbacks = {}
5 |
6 | // Call the corresponding Promise callback
7 | safari.self.addEventListener('message', (e) => {
8 | if (e.name === 'storeResponse') {
9 | console.log('StoreClient storeResponse')
10 | let {id, response} = e.message
11 | let cb = this.callbacks[id]
12 | if (cb) {
13 | delete this.callbacks[id]
14 | cb.fulfill(response)
15 | // TODO: Call cb.reject(data) if there is an error
16 | }
17 | }
18 | }, false)
19 | }
20 |
21 | request(method, args) {
22 | // Return Promise that is fulfilled by the response message from the global page
23 | return new Promise((fulfill, reject) => {
24 | let id = Math.random()
25 | this.callbacks[id] = {fulfill, reject}
26 | safari.self.tab.dispatchMessage('storeRequest', {id, method, args})
27 | })
28 | }
29 |
30 | }
31 |
32 | const storeClient = new StoreClient()
33 |
34 |
35 | export const CALL_API = Symbol('Call API')
36 |
37 |
38 | export default store => next => action => {
39 | const callAPI = action[CALL_API]
40 | if (typeof callAPI === 'undefined') {
41 | return next(action)
42 | }
43 |
44 | function actionWith(data) {
45 | const finalAction = Object.assign({}, action, data)
46 | delete finalAction[CALL_API]
47 | return finalAction
48 | }
49 |
50 | let {method, args, types} = callAPI
51 | const [requestType, successType, errorType] = types
52 | next(actionWith({payload: args, type: requestType}))
53 | return storeClient.request(method, args).then(
54 | response => next(actionWith({payload: response, type: successType})),
55 | error => next(actionWith({payload: error, type: errorType}))
56 | )
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import {routerReducer as routing} from 'react-router-redux'
2 | import {combineReducers} from 'redux'
3 | import rules from './rules'
4 |
5 |
6 | const rootReducer = combineReducers({
7 | routing,
8 | rules
9 | })
10 |
11 |
12 | export default rootReducer
13 |
--------------------------------------------------------------------------------
/src/reducers/rules.js:
--------------------------------------------------------------------------------
1 | import {createReducer} from '../utils'
2 | import * as types from '../constants/actionTypes'
3 |
4 | const initialState = {
5 | rulesList: {rules: null, error: null, loading: false},
6 | newRule: {error: null, loading: false},
7 | updateRule: {error: null, loading: false},
8 | deleteRule: {error: null, loading: false}
9 | }
10 |
11 | export default createReducer(initialState, {
12 | [types.RULES_REQUEST]: (state, payload) => {
13 | return {...state, rulesList: {rules: state.rulesList.rules, error: null, loading: true}}
14 | },
15 | [types.RULES_SUCCESS]: (state, payload) => {
16 | return {...state, rulesList: {rules: payload, error: null, loading: false}}
17 | },
18 | [types.RULES_FAILURE]: (state, payload) => {
19 | return {...state, rulesList: {rules: null, error: payload, loading: false}}
20 | },
21 | [types.CREATE_RULE_REQUEST]: (state, payload) => {
22 | return {...state, newRule: {error: null, loading: true}}
23 | },
24 | [types.CREATE_RULE_SUCCESS]: (state, payload) => {
25 | // Add the rule in the local state
26 | let rulesList = Object.assign({}, state.rulesList)
27 | rulesList.rules.unshift(payload)
28 | return {...state, rulesList, newRule: {error: null, loading: false}}
29 | },
30 | [types.CREATE_RULE_FAILURE]: (state, payload) => {
31 | return {...state, newRule: {error: payload, loading: false}}
32 | },
33 | [types.UPDATE_RULE_REQUEST]: (state, payload) => {
34 | return {...state, updateRule: {error: null, loading: true}}
35 | },
36 | [types.UPDATE_RULE_SUCCESS]: (state, payload) => {
37 | // Update the rule in the local state
38 | let rulesList = Object.assign({}, state.rulesList)
39 | for(var i = 0; i < rulesList.rules.length; i++) {
40 | if (rulesList.rules[i].id === payload.id) {
41 | rulesList.rules[i] = payload
42 | break
43 | }
44 | }
45 | return {...state, rulesList, updateRule: {error: null, loading: false}}
46 | },
47 | [types.UPDATE_RULE_FAILURE]: (state, payload) => {
48 | return {...state, updateRule: {error: payload, loading: false}}
49 | },
50 | [types.DELETE_RULE_REQUEST]: (state, payload) => {
51 | return {...state, deleteRule: {error: null, loading: true}}
52 | },
53 | [types.DELETE_RULE_SUCCESS]: (state, payload) => {
54 | // Remove the deleted rule from the local state
55 | let rulesList = Object.assign({}, state.rulesList)
56 | for(var i = 0; i < rulesList.rules.length; i++) {
57 | if (rulesList.rules[i].id === payload) {
58 | rulesList.rules.splice(i, 1)
59 | break
60 | }
61 | }
62 | return {...state, rulesList, deleteRule: {error: null, loading: false}}
63 | },
64 | [types.DELETE_RULE_FAILURE]: (state, payload) => {
65 | return {...state, deleteRule: {error: payload, loading: false}}
66 | }
67 | })
68 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Route, IndexRedirect} from 'react-router'
3 | import App from './containers/App'
4 | import RulesApp from './containers/RulesApp'
5 |
6 |
7 | export default
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import uuid from 'node-uuid'
2 |
3 | // KeySearch data store
4 |
5 | // This makes use of the Safari extension settings API to store the necessary data for each saved rule.
6 | // Data is stored with ids starting with 'rule_' as a rudimentary namespace to distinguish from any other
7 | // KeySearch settings.
8 |
9 |
10 | const presets = [
11 | {key: 'wiki {{query}}', name: 'Wikipedia', url: 'http://en.wikipedia.org/w/index.php?search={{query}}', enabled: true},
12 | {key: '! {{url}}', name: 'Open URL', url: 'http://{{url}}', enabled: true},
13 | {key: 'amazon {{query}}', name: 'Amazon', url: 'http://www.amazon.com/s/ref=nb_sb_noss?field-keywords={{query}}&url=search-alias%3Daps&tag=moxt-20', enabled: true},
14 | {key: 'image {{query}}', name: 'Google Images', url: 'http://www.google.com/images?q={{query}}', enabled: true},
15 | {key: 'gmail {{query}}', name: 'GMail', url: 'https://mail.google.com/mail/?shva=1#search/{{query}}', enabled: true},
16 | {key: 'map {{where}}', name: 'Google Maps', url: 'http://maps.google.com/maps?q={{where}}', enabled: true},
17 | {key: 'imdb {{query}}', name: 'IMDb', url: 'http://www.imdb.com/find?s=all&q={{query}}', enabled: true},
18 | {key: 'youtube {{query}}', name: 'YouTube', url: 'http://www.youtube.com/results?search_query={{query}}', enabled: true},
19 | {key: 'fb {{query}}', name: 'FaceBook', url: 'https://www.facebook.com/search.php?q={{query}}', enabled: true},
20 | {key: 'r {{query}}', name: 'reddit', url: 'http://www.reddit.com/search?q={{query}}', enabled: true}
21 | ]
22 |
23 |
24 | class Store {
25 |
26 | // Initialize the store with example rules on first run
27 | addPresets() {
28 | for (let preset of presets) {
29 | console.log(`Adding preset rule ${preset.key}`)
30 | this.create(preset)
31 | }
32 | }
33 |
34 | // Get the stored data for a rule
35 | get(id) {
36 | console.log('Getting rule')
37 | let jsonString = safari.extension.settings.getItem('rule_' + id)
38 | if (jsonString) {
39 | let data = JSON.parse(jsonString)
40 | return data
41 | }
42 | }
43 |
44 | // Set the stored data for a new rule
45 | create(data) {
46 | console.log('Creating rule')
47 | data.created = Date.now()
48 | data.updated = data.created
49 | data.id = data.created + '_' + uuid.v4()
50 | let jsonString = JSON.stringify(data)
51 | safari.extension.settings.setItem('rule_' + data.id, jsonString)
52 | return this.get(data.id)
53 | }
54 |
55 | // Update stored data for an existing rule
56 | update(data) {
57 | console.log('Updating rule')
58 | data.updated = Date.now()
59 | let jsonString = JSON.stringify(data)
60 | safari.extension.settings.setItem('rule_' + data.id, jsonString)
61 | return this.get(data.id)
62 | }
63 |
64 | // Remove stored data for a rule
65 | delete(id) {
66 | console.log('Deleting rule')
67 | safari.extension.settings.removeItem('rule_' + id)
68 | return id
69 | }
70 |
71 | all() {
72 | console.log('Getting all rules')
73 | return Array.from(this)
74 | }
75 |
76 | // Iterator for rule data
77 | [Symbol.iterator]() {
78 | let i = 0
79 | let ids = null
80 | return {
81 | next: () => {
82 | if (ids === null) {
83 | ids = []
84 | for (let id in safari.extension.settings) {
85 | if (id.substr(0, 5) == 'rule_') {
86 | ids.push(id.substr(5))
87 | }
88 | }
89 | ids.sort().reverse()
90 | }
91 | if (i >= ids.length) {
92 | return {done: true}
93 | } else {
94 | return {done: false, value: this.get(ids[i++])}
95 | }
96 | }
97 | }
98 | }
99 |
100 | // Upgrade data store from old versions of KeySearch
101 | upgrade() {
102 | for (let id in safari.extension.settings) {
103 | if (id.substr(0, 2) == '__') {
104 | let jsonString = safari.extension.settings.getItem(id)
105 | if (jsonString) {
106 | let data = JSON.parse(jsonString)
107 | data.key = data.keyword + ' {{query}}'
108 | data.url = data.url.replace('@@@', '{{query}}')
109 | delete data.keyword
110 | this.create(data)
111 | safari.extension.settings.removeItem(id)
112 | }
113 | }
114 | }
115 | }
116 | }
117 |
118 | export default new Store()
119 |
--------------------------------------------------------------------------------
/src/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import {createStore, applyMiddleware, compose} from 'redux'
2 | import thunk from 'redux-thunk'
3 | // import createLogger from 'redux-logger'
4 | import api from '../middleware/api'
5 | import rootReducer from '../reducers'
6 |
7 | export default function configureStore(initialState) {
8 | let middleware = [thunk, api]
9 | if (process.env.NODE_ENV !== 'production') {
10 | let createLogger = require('redux-logger')
11 | middleware = [...middleware, createLogger()]
12 | }
13 | return createStore(rootReducer, initialState, applyMiddleware(...middleware))
14 | }
15 |
--------------------------------------------------------------------------------
/src/styles/components.scss:
--------------------------------------------------------------------------------
1 | @import "components/button.scss";
2 | @import "components/rules-app.scss";
3 | @import "components/rule.scss";
4 | @import "components/token-input.scss";
5 |
--------------------------------------------------------------------------------
/src/styles/components/_button.scss:
--------------------------------------------------------------------------------
1 | .button {
2 | background-color: #fff; //#f4f4f4;
3 | font-size: 14px;
4 | border: none;
5 | border-radius: 4px;
6 | cursor: pointer;
7 | color: #444;
8 | margin: 0;
9 | padding: 2px 6px 3px 6px;
10 | transition: background-color 200ms linear, color 200ms linear;
11 |
12 | &__icon {
13 | height: 1.4em;
14 | width: 1.4em;
15 | }
16 |
17 | &__text {
18 | vertical-align: middle;
19 | line-height: 1.4em;
20 | }
21 |
22 | &:hover {
23 | color: #fff;
24 | background-color: #444;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/styles/components/_rule.scss:
--------------------------------------------------------------------------------
1 | .rule {
2 | // border-bottom: 1px solid #d1d3d4;
3 | padding: 20px 0;
4 |
5 | &.is-disabled {
6 | opacity: 0.5;
7 | }
8 |
9 | &__header {
10 | display: flex;
11 | }
12 |
13 | &__body {
14 | position: relative;
15 | display: flex;
16 | }
17 |
18 | &__name {
19 | margin: 0 5px 5px 0;
20 | flex: 1 80%;
21 | }
22 |
23 | &__enable {
24 | margin: 2px 0 8px 5px;
25 | }
26 |
27 | &__delete {
28 | margin: 2px 0 8px 5px;
29 | color: #d40000;
30 | // background-color: #f9f9f9;
31 | &:hover {
32 | // color: #fff;
33 | background-color: #d40000;
34 | }
35 | }
36 |
37 | &__key {
38 | position: relative;
39 | flex: 1 25%;
40 | padding: 8px;
41 | background: #e9e9e9;
42 | border-top-left-radius: 5px;
43 | border-bottom-left-radius: 5px;
44 |
45 | & .is-token {
46 | background: #e9e9e9;
47 | }
48 |
49 | &:after {
50 | content: '';
51 | display: block;
52 | right: -46px;
53 | position: absolute;
54 | top: 0px;
55 | width: 0;
56 | height: 0;
57 | border-left: 23px solid #e9e9e9;
58 | border-right: 23px solid transparent;
59 | border-top: 23px solid transparent;
60 | border-bottom: 23px solid transparent;
61 | }
62 | }
63 |
64 | &__url {
65 | flex: 1 75%;
66 | padding: 8px 8px 8px 31px;
67 | background: #f4f4f4;
68 | border-top-right-radius: 5px;
69 | border-bottom-right-radius: 5px;
70 |
71 | & .is-token {
72 | background: #f4f4f4;
73 | }
74 | }
75 |
76 | &__chevron {
77 | &::before {
78 | border-style: solid;
79 | border-width: 0.25em 0.25em 0 0;
80 | content: '';
81 | display: inline-block;
82 | height: 0.65em;
83 | width: 0.65em;
84 | position: relative;
85 | top: 1em;
86 | transform: rotate(45deg);
87 | vertical-align: top;
88 | left: 0;
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/styles/components/_rules-app.scss:
--------------------------------------------------------------------------------
1 | .rules-app {
2 | &__add {
3 | margin: 10px 5px 0 0;
4 | float: left;
5 | color: #00bc00;
6 |
7 | &:hover {
8 | background-color: #00bc00;
9 | }
10 | }
11 |
12 | &__import {
13 | margin: 10px 5px 0 0;
14 | float: left;
15 | }
16 |
17 | &__export {
18 | margin: 10px 5px 0 0;
19 | float: left;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/styles/components/_token-input.scss:
--------------------------------------------------------------------------------
1 | .token-input {
2 | position: relative;
3 | height: 30px;
4 | color: #444;
5 |
6 | &__input {
7 | background: transparent;
8 | outline: none;
9 | border: 1px solid transparent;
10 | position: absolute;
11 | margin: 0;
12 | top: 0;
13 | bottom: 0;
14 | left: 0;
15 | right: 0;
16 | font-size: inherit;
17 | font-weight: inherit;
18 | color: inherit;
19 | line-height: 1;
20 | padding: 8px 4px;
21 |
22 | &:hover {
23 | border: 1px solid #ddd;
24 | }
25 |
26 | &:focus {
27 | border: 1px solid #498feb;
28 | }
29 |
30 | }
31 |
32 | &__overlay {
33 | pointer-events: none;
34 | color: transparent;
35 | position: absolute;
36 | top: 0;
37 | bottom: 0;
38 | left: 0;
39 | right: 0;
40 | line-height: 1;
41 | padding: 7px 4px;
42 | white-space: pre;
43 | overflow: hidden;
44 | border: 1px solid transparent;
45 |
46 | &.is-focused {
47 | opacity: 0;
48 | }
49 | }
50 |
51 | &__span {
52 |
53 | &.is-token {
54 | padding: 0 4px;
55 |
56 | &>span {
57 | border-radius: 3px;
58 | border: 2px solid #498feb;
59 | padding: 1px 5px 2px;
60 | color: #498feb;
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import "./components";
2 |
3 | html {
4 | box-sizing: border-box;
5 | }
6 |
7 | body {
8 | margin: 0;
9 | padding: 0 10px;
10 | font-family: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
11 | font-size: 14px;
12 | line-height: 1.5;
13 | color: #444;
14 | }
15 |
16 | h2 {
17 | font-weight: normal;
18 | font-size: 24px;
19 | }
20 |
21 | .container {
22 | max-width: 970px;
23 | margin: 0 auto 50px;
24 | }
25 |
26 | .row:after {
27 | content: '';
28 | display: block;
29 | clear: both;
30 | }
31 |
32 | .navbar {
33 | height: 58px;
34 | border-bottom: 2px solid #444;
35 |
36 | &__brand {
37 | float: left;
38 | // color: #498feb;
39 | margin: 20px 0 10px 0;
40 | line-height: 28px;
41 | }
42 |
43 | &__nav {
44 | float: right;
45 | padding: 0;
46 | margin: 20px 0 0 0;
47 | list-style: none;
48 | }
49 |
50 | &__item {
51 | float: left;
52 | position: relative;
53 | display: block;
54 | }
55 |
56 | &__link {
57 | line-height: 38px;
58 | padding: 20px 5px 10px 5px;
59 | font-weight: bold;
60 | text-decoration: none;
61 | font-size: 16px;
62 | border-bottom: 2px solid transparent;
63 | }
64 |
65 | &__link.active {
66 | color: #498feb;
67 | border-bottom: 2px solid #498feb;
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export function createReducer(initialState, reducerMap) {
2 | return (state = initialState, action) => {
3 | const reducer = reducerMap[action.type]
4 | return reducer ? reducer(state, action.payload) : state
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 |
2 | const webpack = require('webpack');
3 | const {resolve} = require('path');
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
5 |
6 | const extensionPath = resolve(__dirname, 'keysearch.safariextension');
7 | const isProduction = process.env.NODE_ENV === 'production'
8 |
9 | var jsLoaders = [{
10 | loader: 'babel-loader',
11 | options: {
12 | presets: [['es2015', {modules: false}], 'stage-0', 'react'],
13 | plugins: ['transform-runtime']
14 | }
15 | }]
16 |
17 | if (isProduction) {
18 | jsLoaders.push({
19 | loader: 'strip-loader',
20 | options: {
21 | strip: ['console.log']
22 | }
23 | })
24 | }
25 |
26 | module.exports = {
27 | entry: {
28 | global: './src/global.js',
29 | index: './src/index.jsx'
30 | },
31 | output: {
32 | path: extensionPath,
33 | filename: '[name].js'
34 | },
35 | plugins: [
36 | new webpack.NoErrorsPlugin(),
37 | new ExtractTextPlugin('[name].css')
38 | ],
39 | resolve: {
40 | extensions: ['.js', '.jsx']
41 | },
42 | module: {
43 | rules: [
44 | {
45 | test: /\.scss$/,
46 | loader: ExtractTextPlugin.extract({
47 | fallbackLoader: ['style-loader'],
48 | loader: ['css-loader', 'sass-loader']
49 | })
50 | },
51 | {
52 | test: /\.jsx?$/,
53 | exclude: /node_modules/,
54 | use: jsLoaders
55 | },
56 | {
57 | test: /\.(jpg|png|svg|ttf|eot|woff2?)$/,
58 | loader: 'file-loader',
59 | options: {
60 | name: '[path][name].[ext]'
61 | }
62 | },
63 | {test: /\.json$/, loader: 'json-loader'}
64 | ]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------