diff --git a/manifest.json b/manifest.json index 4c43b64..39d14f4 100644 --- a/manifest.json +++ b/manifest.json @@ -40,5 +40,11 @@ ], "options_ui": { "page": "build/settings.html" + }, + "browser_action": { + "default_icon": { + "32": "resources/enabled_32x32.png" + }, + "default_title": "Vim Vixen" } } diff --git a/resources/disabled_32x32.png b/resources/disabled_32x32.png new file mode 100644 index 0000000..d3b88db Binary files /dev/null and b/resources/disabled_32x32.png differ diff --git a/resources/enabled_32x32.png b/resources/enabled_32x32.png new file mode 100644 index 0000000..eaf2862 Binary files /dev/null and b/resources/enabled_32x32.png differ diff --git a/src/background/actions/command.js b/src/background/actions/command.js index 4c52bca..2f7305a 100644 --- a/src/background/actions/command.js +++ b/src/background/actions/command.js @@ -1,5 +1,5 @@ import actions from '../actions'; -import * as tabs from 'background/tabs'; +import * as tabs from '../shared/tabs'; import * as parsers from 'shared/commands/parsers'; import * as properties from 'shared/settings/properties'; diff --git a/src/background/actions/index.js b/src/background/actions/index.js index 2bdaaf2..3833389 100644 --- a/src/background/actions/index.js +++ b/src/background/actions/index.js @@ -5,4 +5,7 @@ export default { // Find FIND_SET_KEYWORD: 'find.set.keyword', + + // Tab + TAB_SELECTED: 'tab.selected', }; diff --git a/src/background/actions/operation.js b/src/background/actions/operation.js deleted file mode 100644 index 10c366f..0000000 --- a/src/background/actions/operation.js +++ /dev/null @@ -1,92 +0,0 @@ -import operations from 'shared/operations'; -import messages from 'shared/messages'; -import * as tabs from 'background/tabs'; -import * as zooms from 'background/zooms'; - -const sendConsoleShowCommand = (tab, command) => { - return browser.tabs.sendMessage(tab.id, { - type: messages.CONSOLE_SHOW_COMMAND, - command, - }); -}; - -// This switch statement is only gonna get longer as more -// features are added, so disable complexity check -/* eslint-disable complexity */ -const exec = (operation, tab) => { - switch (operation.type) { - case operations.TAB_CLOSE: - return tabs.closeTab(tab.id); - case operations.TAB_CLOSE_FORCE: - return tabs.closeTabForce(tab.id); - case operations.TAB_REOPEN: - return tabs.reopenTab(); - case operations.TAB_PREV: - return tabs.selectPrevTab(tab.index, operation.count); - case operations.TAB_NEXT: - return tabs.selectNextTab(tab.index, operation.count); - case operations.TAB_FIRST: - return tabs.selectFirstTab(); - case operations.TAB_LAST: - return tabs.selectLastTab(); - case operations.TAB_PREV_SEL: - return tabs.selectPrevSelTab(); - case operations.TAB_RELOAD: - return tabs.reload(tab, operation.cache); - case operations.TAB_PIN: - return tabs.updateTabPinned(tab, true); - case operations.TAB_UNPIN: - return tabs.updateTabPinned(tab, false); - case operations.TAB_TOGGLE_PINNED: - return tabs.toggleTabPinned(tab); - case operations.TAB_DUPLICATE: - return tabs.duplicate(tab.id); - case operations.ZOOM_IN: - return zooms.zoomIn(); - case operations.ZOOM_OUT: - return zooms.zoomOut(); - case operations.ZOOM_NEUTRAL: - return zooms.neutral(); - case operations.COMMAND_SHOW: - return sendConsoleShowCommand(tab, ''); - case operations.COMMAND_SHOW_OPEN: - if (operation.alter) { - // alter url - return sendConsoleShowCommand(tab, 'open ' + tab.url); - } - return sendConsoleShowCommand(tab, 'open '); - case operations.COMMAND_SHOW_TABOPEN: - if (operation.alter) { - // alter url - return sendConsoleShowCommand(tab, 'tabopen ' + tab.url); - } - return sendConsoleShowCommand(tab, 'tabopen '); - case operations.COMMAND_SHOW_WINOPEN: - if (operation.alter) { - // alter url - return sendConsoleShowCommand(tab, 'winopen ' + tab.url); - } - return sendConsoleShowCommand(tab, 'winopen '); - case operations.COMMAND_SHOW_BUFFER: - return sendConsoleShowCommand(tab, 'buffer '); - case operations.FIND_START: - return browser.tabs.sendMessage(tab.id, { - type: messages.CONSOLE_SHOW_FIND - }); - case operations.CANCEL: - return browser.tabs.sendMessage(tab.id, { - type: messages.CONSOLE_HIDE, - }); - case operations.PAGE_SOURCE: - return browser.tabs.create({ - url: 'view-source:' + tab.url, - index: tab.index + 1, - openerTabId: tab.id, - }); - default: - return Promise.resolve(); - } -}; -/* eslint-enable complexity */ - -export { exec }; diff --git a/src/background/actions/tab.js b/src/background/actions/tab.js index 3c642fd..0d439fd 100644 --- a/src/background/actions/tab.js +++ b/src/background/actions/tab.js @@ -1,3 +1,5 @@ +import actions from './index'; + const openNewTab = (url, openerTabId, background = false, adjacent = false) => { if (adjacent) { return browser.tabs.query({ @@ -18,4 +20,11 @@ const openToTab = (url, tab) => { return browser.tabs.update(tab.id, { url: url }); }; -export { openNewTab, openToTab }; +const selected = (tabId) => { + return { + type: actions.TAB_SELECTED, + tabId, + }; +}; + +export { openNewTab, openToTab, selected }; diff --git a/src/background/components/background.js b/src/background/components/background.js index fae3fbb..e13424b 100644 --- a/src/background/components/background.js +++ b/src/background/components/background.js @@ -1,10 +1,9 @@ import messages from 'shared/messages'; -import * as operationActions from 'background/actions/operation'; import * as commandActions from 'background/actions/command'; import * as settingActions from 'background/actions/setting'; import * as findActions from 'background/actions/find'; import * as tabActions from 'background/actions/tab'; -import * as commands from 'shared/commands'; +import * as completions from '../shared/completions'; export default class BackgroundComponent { constructor(store) { @@ -27,10 +26,6 @@ export default class BackgroundComponent { let find = this.store.getState().find; switch (message.type) { - case messages.BACKGROUND_OPERATION: - return this.store.dispatch( - operationActions.exec(message.operation, sender.tab), - sender); case messages.OPEN_URL: if (message.newTab) { let action = tabActions.openNewTab( @@ -49,7 +44,7 @@ export default class BackgroundComponent { case messages.SETTINGS_QUERY: return Promise.resolve(this.store.getState().setting.value); case messages.CONSOLE_QUERY_COMPLETIONS: - return commands.complete(message.text, settings.value); + return completions.complete(message.text, settings.value); case messages.SETTINGS_RELOAD: this.store.dispatch(settingActions.load()); return this.broadcastSettingsChanged(); diff --git a/src/background/components/indicator.js b/src/background/components/indicator.js new file mode 100644 index 0000000..cceb119 --- /dev/null +++ b/src/background/components/indicator.js @@ -0,0 +1,45 @@ +import * as indicators from '../shared/indicators'; +import messages from 'shared/messages'; + +export default class IndicatorComponent { + constructor(store) { + this.store = store; + + messages.onMessage(this.onMessage.bind(this)); + + browser.browserAction.onClicked.addListener(this.onClicked); + browser.tabs.onActivated.addListener((info) => { + return browser.tabs.query({ currentWindow: true }).then(() => { + return this.onTabActivated(info); + }); + }); + } + + onTabActivated(info) { + return browser.tabs.sendMessage(info.tabId, { + type: messages.ADDON_ENABLED_QUERY, + }).then((resp) => { + return this.updateIndicator(resp.enabled); + }); + } + + onClicked(tab) { + browser.tabs.sendMessage(tab.id, { + type: messages.ADDON_TOGGLE_ENABLED, + }); + } + + onMessage(message) { + switch (message.type) { + case messages.ADDON_ENABLED_RESPONSE: + return this.updateIndicator(message.enabled); + } + } + + updateIndicator(enabled) { + if (enabled) { + return indicators.enable(); + } + return indicators.disable(); + } +} diff --git a/src/background/components/operation.js b/src/background/components/operation.js new file mode 100644 index 0000000..9a0b4e1 --- /dev/null +++ b/src/background/components/operation.js @@ -0,0 +1,118 @@ +import messages from 'shared/messages'; +import operations from 'shared/operations'; +import * as tabs from '../shared//tabs'; +import * as zooms from '../shared/zooms'; + +export default class BackgroundComponent { + constructor(store) { + this.store = store; + + browser.runtime.onMessage.addListener((message, sender) => { + try { + return this.onMessage(message, sender); + } catch (e) { + return browser.tabs.sendMessage(sender.tab.id, { + type: messages.CONSOLE_SHOW_ERROR, + text: e.message, + }); + } + }); + } + + onMessage(message, sender) { + switch (message.type) { + case messages.BACKGROUND_OPERATION: + return this.store.dispatch( + this.exec(message.operation, sender.tab), + sender); + } + } + + // eslint-disable-next-line complexity + exec(operation, tab) { + let tabState = this.store.getState().tab; + + switch (operation.type) { + case operations.TAB_CLOSE: + return tabs.closeTab(tab.id); + case operations.TAB_CLOSE_FORCE: + return tabs.closeTabForce(tab.id); + case operations.TAB_REOPEN: + return tabs.reopenTab(); + case operations.TAB_PREV: + return tabs.selectPrevTab(tab.index, operation.count); + case operations.TAB_NEXT: + return tabs.selectNextTab(tab.index, operation.count); + case operations.TAB_FIRST: + return tabs.selectFirstTab(); + case operations.TAB_LAST: + return tabs.selectLastTab(); + case operations.TAB_PREV_SEL: + if (tabState.previousSelected > 0) { + return tabs.selectTab(tabState.previousSelected); + } + break; + case operations.TAB_RELOAD: + return tabs.reload(tab, operation.cache); + case operations.TAB_PIN: + return tabs.updateTabPinned(tab, true); + case operations.TAB_UNPIN: + return tabs.updateTabPinned(tab, false); + case operations.TAB_TOGGLE_PINNED: + return tabs.toggleTabPinned(tab); + case operations.TAB_DUPLICATE: + return tabs.duplicate(tab.id); + case operations.ZOOM_IN: + return zooms.zoomIn(); + case operations.ZOOM_OUT: + return zooms.zoomOut(); + case operations.ZOOM_NEUTRAL: + return zooms.neutral(); + case operations.COMMAND_SHOW: + return this.sendConsoleShowCommand(tab, ''); + case operations.COMMAND_SHOW_OPEN: + if (operation.alter) { + // alter url + return this.sendConsoleShowCommand(tab, 'open ' + tab.url); + } + return this.sendConsoleShowCommand(tab, 'open '); + case operations.COMMAND_SHOW_TABOPEN: + if (operation.alter) { + // alter url + return this.sendConsoleShowCommand(tab, 'tabopen ' + tab.url); + } + return this.sendConsoleShowCommand(tab, 'tabopen '); + case operations.COMMAND_SHOW_WINOPEN: + if (operation.alter) { + // alter url + return this.sendConsoleShowCommand(tab, 'winopen ' + tab.url); + } + return this.sendConsoleShowCommand(tab, 'winopen '); + case operations.COMMAND_SHOW_BUFFER: + return this.sendConsoleShowCommand(tab, 'buffer '); + case operations.FIND_START: + return browser.tabs.sendMessage(tab.id, { + type: messages.CONSOLE_SHOW_FIND + }); + case operations.CANCEL: + return browser.tabs.sendMessage(tab.id, { + type: messages.CONSOLE_HIDE, + }); + case operations.PAGE_SOURCE: + return browser.tabs.create({ + url: 'view-source:' + tab.url, + index: tab.index + 1, + openerTabId: tab.id, + }); + default: + return Promise.resolve(); + } + } + + sendConsoleShowCommand(tab, command) { + return browser.tabs.sendMessage(tab.id, { + type: messages.CONSOLE_SHOW_COMMAND, + command, + }); + } +} diff --git a/src/background/components/tab.js b/src/background/components/tab.js new file mode 100644 index 0000000..b273546 --- /dev/null +++ b/src/background/components/tab.js @@ -0,0 +1,17 @@ +import * as tabActions from '../actions/tab'; + +export default class TabComponent { + constructor(store) { + this.store = store; + + browser.tabs.onActivated.addListener((info) => { + return browser.tabs.query({ currentWindow: true }).then(() => { + return this.onTabActivated(info); + }); + }); + } + + onTabActivated(info) { + return this.store.dispatch(tabActions.selected(info.tabId)); + } +} diff --git a/src/background/index.js b/src/background/index.js index ff27796..3f1013c 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -1,6 +1,9 @@ import * as settingActions from 'background/actions/setting'; import messages from 'shared/messages'; import BackgroundComponent from 'background/components/background'; +import OperationComponent from 'background/components/operation'; +import TabComponent from 'background/components/tab'; +import IndicatorComponent from 'background/components/indicator'; import reducers from 'background/reducers'; import { createStore } from 'shared/store'; import * as versions from 'shared/versions'; @@ -14,8 +17,13 @@ const store = createStore(reducers, (e, sender) => { }); } }); -// eslint-disable-next-line no-unused-vars + +/* eslint-disable no-unused-vars */ const backgroundComponent = new BackgroundComponent(store); +const operationComponent = new OperationComponent(store); +const tabComponent = new TabComponent(store); +const indicatorComponent = new IndicatorComponent(store); +/* eslint-enable no-unused-vars */ store.dispatch(settingActions.load()); diff --git a/src/background/reducers/index.js b/src/background/reducers/index.js index 63ff0f8..5729f0a 100644 --- a/src/background/reducers/index.js +++ b/src/background/reducers/index.js @@ -1,15 +1,18 @@ import settingReducer from './setting'; import findReducer from './find'; +import tabReducer from './tab'; // Make setting reducer instead of re-use const defaultState = { setting: settingReducer(undefined, {}), find: findReducer(undefined, {}), + tab: tabReducer(undefined, {}), }; export default function reducer(state = defaultState, action = {}) { return Object.assign({}, state, { setting: settingReducer(state.setting, action), find: findReducer(state.find, action), + tab: tabReducer(state.tab, action), }); } diff --git a/src/background/reducers/tab.js b/src/background/reducers/tab.js new file mode 100644 index 0000000..e0cdf32 --- /dev/null +++ b/src/background/reducers/tab.js @@ -0,0 +1,19 @@ +import actions from 'background/actions'; + +const defaultState = { + previousSelected: -1, + currentSelected: -1, +}; + +export default function reducer(state = defaultState, action = {}) { + switch (action.type) { + case actions.TAB_SELECTED: + return { + previousSelected: state.currentSelected, + currentSelected: action.tabId, + }; + default: + return state; + } +} + diff --git a/src/background/histories.js b/src/background/shared/completions/histories.js similarity index 100% rename from src/background/histories.js rename to src/background/shared/completions/histories.js diff --git a/src/shared/commands/complete.js b/src/background/shared/completions/index.js similarity index 94% rename from src/shared/commands/complete.js rename to src/background/shared/completions/index.js index 0bdbab8..73b7b27 100644 --- a/src/shared/commands/complete.js +++ b/src/background/shared/completions/index.js @@ -1,5 +1,5 @@ -import * as tabs from 'background/tabs'; -import * as histories from 'background/histories'; +import * as tabs from './tabs'; +import * as histories from './histories'; const getOpenCompletions = (command, keywords, searchConfig) => { return histories.getCompletions(keywords).then((pages) => { @@ -81,4 +81,4 @@ const complete = (line, settings) => { return getCompletions(line, settings); }; -export default complete; +export { complete }; diff --git a/src/background/shared/completions/tabs.js b/src/background/shared/completions/tabs.js new file mode 100644 index 0000000..5edddca --- /dev/null +++ b/src/background/shared/completions/tabs.js @@ -0,0 +1,10 @@ +const getCompletions = (keyword) => { + return browser.tabs.query({ currentWindow: true }).then((tabs) => { + let matched = tabs.filter((t) => { + return t.url.includes(keyword) || t.title && t.title.includes(keyword); + }); + return matched; + }); +}; + +export { getCompletions }; diff --git a/src/background/shared/indicators.js b/src/background/shared/indicators.js new file mode 100644 index 0000000..74002c4 --- /dev/null +++ b/src/background/shared/indicators.js @@ -0,0 +1,13 @@ +const enable = () => { + return browser.browserAction.setIcon({ + path: 'resources/enabled_32x32.png', + }); +}; + +const disable = () => { + return browser.browserAction.setIcon({ + path: 'resources/disabled_32x32.png', + }); +}; + +export { enable, disable }; diff --git a/src/background/tabs.js b/src/background/shared/tabs.js similarity index 81% rename from src/background/tabs.js rename to src/background/shared/tabs.js index e939870..f1dcc73 100644 --- a/src/background/tabs.js +++ b/src/background/shared/tabs.js @@ -1,13 +1,3 @@ -let prevSelTab = 1; -let currSelTab = 1; - -browser.tabs.onActivated.addListener((activeInfo) => { - return browser.tabs.query({ currentWindow: true }).then(() => { - prevSelTab = currSelTab; - currSelTab = activeInfo.tabId; - }); -}); - const closeTab = (id) => { return browser.tabs.get(id).then((tab) => { if (!tab.pinned) { @@ -66,15 +56,6 @@ const selectByKeyword = (current, keyword) => { }); }; -const getCompletions = (keyword) => { - return browser.tabs.query({ currentWindow: true }).then((tabs) => { - let matched = tabs.filter((t) => { - return t.url.includes(keyword) || t.title && t.title.includes(keyword); - }); - return matched; - }); -}; - const selectPrevTab = (current, count) => { return browser.tabs.query({ currentWindow: true }).then((tabs) => { if (tabs.length < 2) { @@ -111,8 +92,8 @@ const selectLastTab = () => { }); }; -const selectPrevSelTab = () => { - return browser.tabs.update(prevSelTab, { active: true }); +const selectTab = (id) => { + return browser.tabs.update(id, { active: true }); }; const reload = (current, cache) => { @@ -139,7 +120,7 @@ const duplicate = (id) => { export { closeTab, closeTabForce, reopenTab, selectAt, selectByKeyword, - getCompletions, selectPrevTab, selectNextTab, selectFirstTab, - selectLastTab, selectPrevSelTab, reload, updateTabPinned, + selectPrevTab, selectNextTab, selectFirstTab, + selectLastTab, selectTab, reload, updateTabPinned, toggleTabPinned, duplicate }; diff --git a/src/background/zooms.js b/src/background/shared/zooms.js similarity index 100% rename from src/background/zooms.js rename to src/background/shared/zooms.js diff --git a/src/content/components/common/index.js b/src/content/components/common/index.js index 565632c..9b7b083 100644 --- a/src/content/components/common/index.js +++ b/src/content/components/common/index.js @@ -3,6 +3,7 @@ import KeymapperComponent from './keymapper'; import FollowComponent from './follow'; import * as settingActions from 'content/actions/setting'; import messages from 'shared/messages'; +import * as addonActions from '../../actions/addon'; export default class Common { constructor(win, store) { @@ -14,16 +15,32 @@ export default class Common { input.onKey(key => keymapper.key(key)); this.store = store; + this.prevEnabled = undefined; this.reloadSettings(); messages.onMessage(this.onMessage.bind(this)); + store.subscribe(() => this.update()); } onMessage(message) { switch (message.type) { case messages.SETTINGS_CHANGED: - this.reloadSettings(); + return this.reloadSettings(); + case messages.ADDON_TOGGLE_ENABLED: + return this.store.dispatch(addonActions.toggleEnabled()); + } + } + + update() { + let enabled = this.store.getState().addon.enabled; + if (enabled !== this.prevEnabled) { + this.prevEnabled = enabled; + + browser.runtime.sendMessage({ + type: messages.ADDON_ENABLED_RESPONSE, + enabled, + }); } } diff --git a/src/content/components/top-content/index.js b/src/content/components/top-content/index.js index cf21ec4..a0d0480 100644 --- a/src/content/components/top-content/index.js +++ b/src/content/components/top-content/index.js @@ -44,15 +44,24 @@ export default class TopContent { .some(regex => regex.test(partial)); if (matched) { this.store.dispatch(addonActions.disable()); + } else { + this.store.dispatch(addonActions.enable()); } } onMessage(message) { + let addonState = this.store.getState().addon; + switch (message.type) { case messages.CONSOLE_UNFOCUS: this.win.focus(); consoleFrames.blur(window.document); return Promise.resolve(); + case messages.ADDON_ENABLED_QUERY: + return Promise.resolve({ + type: messages.ADDON_ENABLED_RESPONSE, + enabled: addonState.enabled, + }); } } } diff --git a/src/shared/commands/index.js b/src/shared/commands/index.js deleted file mode 100644 index 78cb4df..0000000 --- a/src/shared/commands/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import complete from './complete'; - -export { complete }; diff --git a/src/shared/messages.js b/src/shared/messages.js index a404658..1f9c816 100644 --- a/src/shared/messages.js +++ b/src/shared/messages.js @@ -48,6 +48,10 @@ export default { FIND_GET_KEYWORD: 'find.get.keyword', FIND_SET_KEYWORD: 'find.set.keyword', + ADDON_ENABLED_QUERY: 'addon.enabled.query', + ADDON_ENABLED_RESPONSE: 'addon.enabled.response', + ADDON_TOGGLE_ENABLED: 'addon.toggle.enabled', + OPEN_URL: 'open.url', SETTINGS_RELOAD: 'settings.reload', diff --git a/test/background/actions/tab.test.js b/test/background/actions/tab.test.js new file mode 100644 index 0000000..ab57374 --- /dev/null +++ b/test/background/actions/tab.test.js @@ -0,0 +1,13 @@ +import actions from 'background/actions'; +import * as tabActions from 'background/actions/tab'; + +describe("tab actions", () => { + describe("selected", () => { + it('create TAB_SELECTED action', () => { + let action = tabActions.selected(123); + expect(action.type).to.equal(actions.TAB_SELECTED); + expect(action.tabId).to.equal(123); + }); + }); +}); + diff --git a/test/background/reducers/tab.test.js b/test/background/reducers/tab.test.js new file mode 100644 index 0000000..09fa8a7 --- /dev/null +++ b/test/background/reducers/tab.test.js @@ -0,0 +1,22 @@ +import actions from 'background/actions'; +import tabReducer from 'background/reducers/tab'; + +describe("tab reducer", () => { + it('return the initial state', () => { + let state = tabReducer(undefined, {}); + expect(state.previousSelected).to.equal(-1); + expect(state.currentSelected).to.equal(-1); + }); + + it('return next state for TAB_SELECTED', () => { + let state = undefined; + + state = tabReducer(state, { type: actions.TAB_SELECTED, tabId: 123 }); + expect(state.previousSelected).to.equal(-1); + expect(state.currentSelected).to.equal(123); + + state = tabReducer(state, { type: actions.TAB_SELECTED, tabId: 456 }); + expect(state.previousSelected).to.equal(123); + expect(state.currentSelected).to.equal(456); + }); +});