diff --git a/src/actions/background.js b/src/actions/background.js new file mode 100644 index 0000000..40b901b --- /dev/null +++ b/src/actions/background.js @@ -0,0 +1,11 @@ +import actions from '../actions'; + +export function requestCompletions(line) { + let command = line.split(' ', 1)[0]; + let keywords = line.replace(command + ' ', ''); + return { + type: actions.BACKGROUND_REQUEST_COMPLETIONS, + command, + keywords + }; +} diff --git a/src/actions/command.js b/src/actions/command.js new file mode 100644 index 0000000..c983278 --- /dev/null +++ b/src/actions/command.js @@ -0,0 +1,35 @@ +import actions from '../actions'; + +const normalizeUrl = (string) => { + try { + return new URL(string).href + } catch (e) { + return 'http://' + string; + } +} + +export function exec(line) { + let name = line.split(' ')[0]; + let remaining = line.replace(name + ' ', ''); + + switch (name) { + case 'open': + // TODO use search engined and pass keywords to them + return { + type: actions.COMMAND_OPEN_URL, + url: normalizeUrl(remaining) + }; + case 'tabopen': + return { + type: actions.COMMAND_TABOPEN_URL, + url: normalizeUrl(remaining) + }; + case 'b': + case 'buffer': + return { + type: actions.COMMAND_BUFFER, + keywords: remaining + }; + } + throw new Error(name + ' command is not defined'); +} diff --git a/src/actions/console.js b/src/actions/console.js new file mode 100644 index 0000000..99a46e8 --- /dev/null +++ b/src/actions/console.js @@ -0,0 +1,28 @@ +import actions from '../actions'; + +export function showCommand(text) { + return { + type: actions.CONSOLE_SHOW_COMMAND, + text: text + }; +} + +export function setCompletions(completions) { + return { + type: actions.CONSOLE_SET_COMPLETIONS, + completions: completions + }; +} + +export function showError(text) { + return { + type: actions.CONSOLE_SHOW_ERROR, + text: text + }; +} + +export function hide() { + return { + type: actions.CONSOLE_HIDE + }; +} diff --git a/src/actions/index.js b/src/actions/index.js new file mode 100644 index 0000000..63d5f6f --- /dev/null +++ b/src/actions/index.js @@ -0,0 +1,40 @@ +export default { + // console commands + CONSOLE_SHOW_COMMAND: 'vimvixen.console.show.command', + CONSOLE_SET_COMPLETIONS: 'vimvixen.console.set.completions', + CONSOLE_SHOW_ERROR: 'vimvixen.console.show.error', + CONSOLE_HIDE: 'vimvixen.console.hide', + + // Background commands + BACKGROUND_REQUEST_COMPLETIONS: 'vimvixen.background.request.completions', + TABS_CLOSE: 'tabs.close', + TABS_REOPEN: 'tabs.reopen', + TABS_PREV: 'tabs.prev', + TABS_NEXT: 'tabs.next', + TABS_RELOAD: 'tabs.reload', + ZOOM_IN: 'zoom.in', + ZOOM_OUT: 'zoom.out', + ZOOM_NEUTRAL: 'zoom.neutral', + + // content commands + CMD_OPEN: 'cmd.open', + CMD_TABS_OPEN: 'cmd.tabs.open', + CMD_BUFFER: 'cmd.buffer', + SCROLL_LINES: 'scroll.lines', + SCROLL_PAGES: 'scroll.pages', + SCROLL_TOP: 'scroll.top', + SCROLL_BOTTOM: 'scroll.bottom', + SCROLL_LEFT: 'scroll.left', + SCROLL_RIGHT: 'scroll.right', + FOLLOW_START: 'follow.start', + HISTORY_PREV: 'history.prev', + HISTORY_NEXT: 'history.next', + + // User input + INPUT_KEY_PRESS: 'input.key,press', + INPUT_CLEAR_KEYS: 'input.clear.keys', + + COMMAND_OPEN_URL: 'command.open.url', + COMMAND_TABOPEN_URL: 'command.tabopen.url', + COMMAND_BUFFER: 'command.buffer', +}; diff --git a/src/actions/input.js b/src/actions/input.js new file mode 100644 index 0000000..c72b9e0 --- /dev/null +++ b/src/actions/input.js @@ -0,0 +1,15 @@ +import actions from '../actions'; + +export function keyPress(code, ctrl) { + return { + type: actions.INPUT_KEY_PRESS, + code, + ctrl + }; +} + +export function clearKeys() { + return { + type: actions.INPUT_CLEAR_KEYS + } +} diff --git a/src/background/index.js b/src/background/index.js index 8913a83..e72cab0 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -1,103 +1,45 @@ -import * as actions from '../shared/actions'; -import * as tabs from './tabs'; -import * as zooms from './zooms'; -import KeyQueue from './key-queue'; +import * as keys from './keys'; +import * as inputActions from '../actions/input'; +import backgroundReducers from '../reducers/background'; +import commandReducer from '../reducers/command'; +import inputReducers from '../reducers/input'; -const queue = new KeyQueue(); +let inputState = inputReducers(undefined, {}); -const keyPressHandle = (request, sender) => { - let action = queue.push({ - code: request.code, - ctrl: request.ctrl - }); - if (!action) { +const keyQueueChanged = (sender, prevState, state) => { + if (state.keys.length === 0) { return Promise.resolve(); } - if (actions.isBackgroundAction(action[0])) { - return doBackgroundAction(sender, action); - } else if (actions.isContentAction(action[0])) { - return Promise.resolve({ - type: 'response.action', - action: action - }); + let prefix = keys.asKeymapChars(state.keys); + let matched = Object.keys(keys.defaultKeymap).filter((keys) => { + return keys.startsWith(prefix); + }); + if (matched.length == 0) { + return handleMessage(inputActions.clearKeys(), sender); + } else if (matched.length > 1 || matched.length === 1 && prefix !== matched[0]) { + return Promise.resolve(); } - return Promise.resolve(); + let action = keys.defaultKeymap[matched]; + return handleMessage(inputActions.clearKeys(), sender).then(() => { + return backgroundReducers(undefined, action, sender).then(() => { + return browser.tabs.sendMessage(sender.tab.id, action); + }); + }); }; -const doBackgroundAction = (sender, action) => { - switch(action[0]) { - case actions.TABS_CLOSE: - return tabs.closeTab(sender.tab.id); - case actions.TABS_REOPEN: - return tabs.reopenTab(); - case actions.TABS_PREV: - return tabs.selectPrevTab(sender.tab.index, actions[1] || 1); - case actions.TABS_NEXT: - return tabs.selectNextTab(sender.tab.index, actions[1] || 1); - case actions.TABS_RELOAD: - return tabs.reload(sender.tab, actions[1] || false); - case actions.ZOOM_IN: - return zooms.zoomIn(); - case actions.ZOOM_OUT: - return zooms.zoomOut(); - case actions.ZOOM_NEUTRAL: - return zooms.neutral(); - } - return Promise.resolve(); -} - -const normalizeUrl = (string) => { - try { - return new URL(string).href - } catch (e) { - return 'http://' + string; +const handleMessage = (action, sender) => { + let nextInputState = inputReducers(inputState, action); + if (JSON.stringify(nextInputState) !== JSON.stringify(inputState)) { + let prevState = inputState; + inputState = nextInputState; + return keyQueueChanged(sender, prevState, inputState); } -} - -const cmdBuffer = (sender, arg) => { - if (isNaN(arg)) { - return tabs.selectByKeyword(sender.tab, arg); - } else { - let index = parseInt(arg, 10) - 1; - return tabs.selectAt(index); - } -} - -const cmdEnterHandle = (request, sender) => { - let words = request.text.split(' ').filter((s) => s.length > 0); - switch (words[0]) { - case 'open': - return browser.tabs.update(sender.tab.id, { url: normalizeUrl(words[1]) }); - case 'tabopen': - return browser.tabs.create({ url: normalizeUrl(words[1]) }); - case 'b': - case 'buffer': - return cmdBuffer(sender, words[1]); - } - throw new Error(words[0] + ' command is not defined'); + return backgroundReducers(undefined, action, sender).then(() => { + return commandReducer(undefined, action, sender).then(() => { + return browser.tabs.sendMessage(sender.tab.id, action); + }); + }); }; -browser.runtime.onMessage.addListener((request, sender) => { - switch (request.type) { - case 'event.keypress': - return keyPressHandle(request, sender); - case 'event.cmd.enter': - return cmdEnterHandle(request, sender); - case 'event.cmd.tabs.completion': - return tabs.getCompletions(request.text).then((tabs) => { - let items = tabs.map((tab) => { - return { - caption: tab.title, - content: tab.title, - url: tab.url, - icon: tab.favIconUrl - } - }); - return { - name: "Buffers", - items: items - }; - }); - } -}); +browser.runtime.onMessage.addListener(handleMessage); diff --git a/src/background/key-queue.js b/src/background/key-queue.js deleted file mode 100644 index d7f0984..0000000 --- a/src/background/key-queue.js +++ /dev/null @@ -1,82 +0,0 @@ -import * as actions from '../shared/actions'; - -const DEFAULT_KEYMAP = { - ':': [ actions.CMD_OPEN ], - 'o': [ actions.CMD_TABS_OPEN, false ], - 'O': [ actions.CMD_TABS_OPEN, true ], - 'b': [ actions.CMD_BUFFER ], - 'k': [ actions.SCROLL_LINES, -1 ], - 'j': [ actions.SCROLL_LINES, 1 ], - '': [ actions.SCROLL_LINES, -1 ], - '': [ actions.SCROLL_LINES, 1 ], - '': [ actions.SCROLL_PAGES, -0.5 ], - '': [ actions.SCROLL_PAGES, 0.5 ], - '': [ actions.SCROLL_PAGES, -1 ], - '': [ actions.SCROLL_PAGES, 1 ], - 'gg': [ actions.SCROLL_TOP ], - 'G': [ actions.SCROLL_BOTTOM ], - '0': [ actions.SCROLL_LEFT ], - '$': [ actions.SCROLL_RIGHT ], - 'd': [ actions.TABS_CLOSE ], - 'u': [ actions.TABS_REOPEN], - 'h': [ actions.TABS_PREV, 1 ], - 'l': [ actions.TABS_NEXT, 1 ], - 'r': [ actions.TABS_RELOAD, false ], - 'R': [ actions.TABS_RELOAD, true ], - 'zi': [ actions.ZOOM_IN ], - 'zo': [ actions.ZOOM_OUT ], - 'zz': [ actions.ZOOM_NEUTRAL], - 'f': [ actions.FOLLOW_START, false ], - 'F': [ actions.FOLLOW_START, true ], - 'H': [ actions.HISTORY_PREV ], - 'L': [ actions.HISTORY_NEXT ], -} - -export default class KeyQueue { - - constructor(keymap = DEFAULT_KEYMAP) { - this.data = []; - this.keymap = keymap; - } - - push(key) { - this.data.push(key); - - let current = this.asKeymapChars(); - let filtered = Object.keys(this.keymap).filter((keys) => { - return keys.startsWith(current); - }); - - if (filtered.length == 0) { - this.data = []; - return null; - } else if (filtered.length === 1 && current === filtered[0]) { - let action = this.keymap[filtered[0]]; - this.data = []; - return action; - } - return null; - } - - asKeymapChars() { - return this.data.map((k) => { - let c = String.fromCharCode(k.code); - if (k.ctrl) { - return ''; - } else { - return c - } - }).join(''); - } - - asCaretChars() { - return this.data.map((k) => { - let c = String.fromCharCode(k.code); - if (k.ctrl) { - return '^' + c.toUpperCase(); - } else { - return c; - } - }).join(''); - } -} diff --git a/src/background/keys.js b/src/background/keys.js index 2fd00a2..0ce53fa 100644 --- a/src/background/keys.js +++ b/src/background/keys.js @@ -1,28 +1,57 @@ -const identifyKey = (key1, key2) => { - return (key1.code === key2.code) && - ((key1.shift || false) === (key2.shift || false)) && - ((key1.ctrl || false) === (key2.ctrl || false)) && - ((key1.alt || false) === (key2.alt || false)) && - ((key1.meta || false) === (key2.meta || false)); -}; +import actions from '../actions'; -const hasPrefix = (keys, prefix) => { - if (keys.length < prefix.length) { - return false; - } - for (let i = 0; i < prefix.length; ++i) { - if (!identifyKey(keys[i], prefix[i])) { - return false; +const defaultKeymap = { + ':': { type: actions.CMD_OPEN }, + 'o': { type: actions.CMD_TABS_OPEN, alter: false }, + 'O': { type: actions.CMD_TABS_OPEN, alter: true }, + 'b': { type: actions.CMD_BUFFER }, + 'k': { type: actions.SCROLL_LINES, count: -1 }, + 'j': { type: actions.SCROLL_LINES, count: 1 }, + '': { type: actions.SCROLL_LINES, count: -1 }, + '': { type: actions.SCROLL_LINES, count: 1 }, + '': { type: actions.SCROLL_PAGES, count: -0.5 }, + '': { type: actions.SCROLL_PAGES, count: 0.5 }, + '': { type: actions.SCROLL_PAGES, count: -1 }, + '': { type: actions.SCROLL_PAGES, count: 1 }, + 'gg': { type: actions.SCROLL_TOP }, + 'G': { type: actions.SCROLL_BOTTOM }, + '0': { type: actions.SCROLL_LEFT }, + '$': { type: actions.SCROLL_RIGHT }, + 'd': { type: actions.TABS_CLOSE }, + 'u': { type: actions.TABS_REOPEN }, + 'h': { type: actions.TABS_PREV, count: 1 }, + 'l': { type: actions.TABS_NEXT, count: 1 }, + 'r': { type: actions.TABS_RELOAD, cache: false }, + 'R': { type: actions.TABS_RELOAD, cache: true }, + 'zi': { type: actions.ZOOM_IN }, + 'zo': { type: actions.ZOOM_OUT }, + 'zz': { type: actions.ZOOM_NEUTRAL }, + 'f': { type: actions.FOLLOW_START, newTab: false }, + 'F': { type: actions.FOLLOW_START, newTab: true }, + 'H': { type: actions.HISTORY_PREV }, + 'L': { type: actions.HISTORY_NEXT }, +} + +const asKeymapChars = (keys) => { + return keys.map((k) => { + let c = String.fromCharCode(k.code); + if (k.ctrl) { + return ''; + } else { + return c } - } - return true; + }).join(''); } -const identifyKeys = (keys1, keys2) => { - if (keys1.length !== keys2.length) { - return false; - } - return hasPrefix(keys1, keys2); +const asCaretChars = (keys) => { + return keys.map((k) => { + let c = String.fromCharCode(k.code); + if (k.ctrl) { + return '^' + c.toUpperCase(); + } else { + return c; + } + }).join(''); } -export { identifyKey, identifyKeys, hasPrefix }; +export { defaultKeymap, asKeymapChars, asCaretChars }; diff --git a/src/background/tabs.js b/src/background/tabs.js index 111bbd9..bd69b4b 100644 --- a/src/background/tabs.js +++ b/src/background/tabs.js @@ -59,7 +59,7 @@ const getCompletions = (keyword) => { }; const selectPrevTab = (current, count) => { - return browser.tabs.query({ currentWindow: true }, (tabs) => { + return browser.tabs.query({ currentWindow: true }).then((tabs) => { if (tabs.length < 2) { return; } @@ -70,7 +70,7 @@ const selectPrevTab = (current, count) => { }; const selectNextTab = (current, count) => { - return browser.tabs.query({ currentWindow: true }, (tabs) => { + return browser.tabs.query({ currentWindow: true }).then((tabs) => { if (tabs.length < 2) { return; } diff --git a/src/console/console-frame.js b/src/console/console-frame.js deleted file mode 100644 index e6bf3f5..0000000 --- a/src/console/console-frame.js +++ /dev/null @@ -1,61 +0,0 @@ -import './console-frame.scss'; -import * as messages from '../shared/messages'; - -export default class ConsoleFrame { - constructor(win) { - let element = window.document.createElement('iframe'); - element.src = browser.runtime.getURL('build/console.html'); - element.className = 'vimvixen-console-frame'; - win.document.body.append(element); - - this.element = element; - - this.errorShown = true; - - this.hide(); - } - - showCommand(text) { - this.showFrame(); - - let message = { - type: 'vimvixen.console.show.command', - text: text - }; - messages.send(this.element.contentWindow, message); - this.errorShown = false; - } - - showError(text) { - this.showFrame(); - - let message = { - type: 'vimvixen.console.show.error', - text: text - }; - messages.send(this.element.contentWindow, message); - this.errorShown = true; - this.element.blur(); - } - - showFrame() { - this.element.style.display = 'block'; - } - - hide() { - this.element.style.display = 'none'; - this.element.blur(); - this.errorShown = false; - } - - isErrorShown() { - return this.element.style.display === 'block' && this.errorShown; - } - - setCompletions(completions) { - messages.send(this.element.contentWindow, { - type: 'vimvixen.console.set.completions', - completions: completions - }); - } -} diff --git a/src/console/console.js b/src/console/console.js index c32eca0..25ab36d 100644 --- a/src/console/console.js +++ b/src/console/console.js @@ -1,36 +1,18 @@ import './console.scss'; +import * as backgroundActions from '../actions/background'; +import * as consoleActions from '../actions/console'; +import * as commandActions from '../actions/command'; import Completion from './completion'; -import * as messages from '../shared/messages'; - -const parent = window.parent; +import consoleReducer from '../reducers/console'; // TODO consider object-oriented var prevValue = ""; var completion = null; var completionOrigin = ""; - -const blurMessage = () => { - return { - type: 'vimvixen.command.blur' - }; -}; - -const keydownMessage = (input) => { - return { - type: 'vimvixen.command.enter', - value: input.value - }; -}; - -const keyupMessage = (input) => { - return { - type: 'vimvixen.command.change', - value: input.value - }; -}; +let state = consoleReducer(undefined, {}); const handleBlur = () => { - messages.send(parent, blurMessage()); + return browser.runtime.sendMessage(consoleActions.hide()); }; const completeNext = () => { @@ -64,13 +46,13 @@ const completePrev = () => { } const handleKeydown = (e) => { + let input = window.document.querySelector('#vimvixen-console-command-input'); + switch(e.keyCode) { case KeyboardEvent.DOM_VK_ESCAPE: - messages.send(parent, blurMessage()); - break; + return input.blur(); case KeyboardEvent.DOM_VK_RETURN: - messages.send(parent, keydownMessage(e.target)); - break; + return browser.runtime.sendMessage(commandActions.exec(e.target.value)); case KeyboardEvent.DOM_VK_TAB: if (e.shiftKey) { completePrev(); @@ -90,8 +72,10 @@ const handleKeyup = (e) => { if (e.target.value === prevValue) { return; } - messages.send(parent, keyupMessage(e.target)); prevValue = e.target.value; + return browser.runtime.sendMessage( + backgroundActions.requestCompletions(e.target.value) + ); }; window.addEventListener('load', () => { @@ -101,35 +85,6 @@ window.addEventListener('load', () => { input.addEventListener('keyup', handleKeyup); }); -const showCommand = (text) => { - let command = window.document.querySelector('#vimvixen-console-command'); - command.style.display = 'block'; - - let error = window.document.querySelector('#vimvixen-console-error'); - error.style.display = 'none'; - - let input = window.document.querySelector('#vimvixen-console-command-input'); - input.value = text; - input.focus(); - - completion = null; - let container = window.document.querySelector('#vimvixen-console-completion'); - container.innerHTML = ''; - messages.send(parent, keyupMessage(input)); -} - -const showError = (text) => { - let error = window.document.querySelector('#vimvixen-console-error'); - error.textContent = text; - error.style.display = 'block'; - - let command = window.document.querySelector('#vimvixen-console-command'); - command.style.display = 'none'; - - let completion = window.document.querySelector('#vimvixen-console-completion'); - completion.style.display = 'none'; -} - const createCompletionTitle = (text) => { let li = document.createElement('li'); li.className = 'vimvixen-console-completion-title'; @@ -154,55 +109,72 @@ const createCompletionItem = (icon, caption, url) => { return li; } -const setCompletions = (completions) => { +const selectCompletion = (target) => { let container = window.document.querySelector('#vimvixen-console-completion'); - container.style.display = 'block'; - container.innerHTML = ''; + Array.prototype.forEach.call(container.children, (ele) => { + if (!ele.classList.contains('vimvixen-console-completion-item')) { + return; + } + if (ele === target) { + ele.classList.add('vimvixen-completion-selected'); + } else { + ele.classList.remove('vimvixen-completion-selected'); + } + }); +}; + +const updateCompletions = (completions) => { + let completionsContainer = window.document.querySelector('#vimvixen-console-completion'); + let input = window.document.querySelector('#vimvixen-console-command-input'); + + completionsContainer.innerHTML = ''; let pairs = []; for (let group of completions) { let title = createCompletionTitle(group.name); - container.append(title); + completionsContainer.append(title); for (let item of group.items) { let li = createCompletionItem(item.icon, item.caption, item.url); - container.append(li); + completionsContainer.append(li); pairs.push([item, li]); } } completion = new Completion(pairs); - - let input = window.document.querySelector('#vimvixen-console-command-input'); completionOrigin = input.value.split(' ')[0]; } -const selectCompletion = (target) => { - let container = window.document.querySelector('#vimvixen-console-completion'); - Array.prototype.forEach.call(container.children, (ele) => { - if (!ele.classList.contains('vimvixen-console-completion-item')) { - return; - } - if (ele === target) { - ele.classList.add('vimvixen-completion-selected'); - } else { - ele.classList.remove('vimvixen-completion-selected'); - } - }); -}; +const update = (prevState, state) => { + let error = window.document.querySelector('#vimvixen-console-error'); + let command = window.document.querySelector('#vimvixen-console-command'); + let input = window.document.querySelector('#vimvixen-console-command-input'); -messages.receive(window, (message) => { - switch (message.type) { - case 'vimvixen.console.show.command': - showCommand(message.text); - break; - case 'vimvixen.console.show.error': - showError(message.text); - break; - case 'vimvixen.console.set.completions': - setCompletions(message.completions); - break; + error.style.display = state.errorShown ? 'block' : 'none'; + error.textContent = state.errorText; + + command.style.display = state.commandShown ? 'block' : 'none'; + if (!prevState.commandShown && state.commandShown) { + // setup input on firstly shown + input.value = state.commandText; + input.focus(); } + + if (JSON.stringify(state.completions) !== JSON.stringify(prevState.completions)) { + updateCompletions(state.completions); + } +} + +browser.runtime.onMessage.addListener((action) => { + let nextState = consoleReducer(state, action); + if (JSON.stringify(nextState) !== JSON.stringify(state)) { + update(state, nextState); + state = nextState; + } +}); + +window.addEventListener('load', () => { + update({}, state); }); diff --git a/src/console/frames.js b/src/console/frames.js new file mode 100644 index 0000000..0b6f3e2 --- /dev/null +++ b/src/console/frames.js @@ -0,0 +1,27 @@ +import './console-frame.scss'; +import * as consoleActions from '../actions/console'; + +const initialize = (doc) => { + let iframe = doc.createElement('iframe'); + iframe.src = browser.runtime.getURL('build/console.html'); + iframe.id = 'vimvixen-console-frame'; + iframe.className = 'vimvixen-console-frame'; + doc.body.append(iframe); + + return iframe; +} + +const showCommand = (text) => { + return browser.runtime.sendMessage(consoleActions.showCommand(text)); +}; + +const showError = (text) => { + return browser.runtime.sendMessage(consoleActions.showError(text)); +} + +const blur = (doc) => { + let iframe = doc.getElementById('vimvixen-console-frame'); + iframe.blur(); +} + +export { initialize, showCommand, showError, blur }; diff --git a/src/content/index.js b/src/content/index.js index fdc7e89..12d079f 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -1,129 +1,41 @@ -import * as scrolls from './scrolls'; -import * as histories from './histories'; -import * as actions from '../shared/actions'; -import * as messages from '../shared/messages'; -import ConsoleFrame from '../console/console-frame'; -import Follow from './follow'; +import '../console/console-frame.scss'; +import * as inputActions from '../actions/input'; +import * as consoleFrames from '../console/frames'; +import actions from '../actions'; +import contentReducer from '../reducers/content'; -let vvConsole = new ConsoleFrame(window); +consoleFrames.initialize(window.document); -const doAction = (action) => { - if (typeof action === 'undefined' || action === null) { - return; - } - - switch (action[0]) { - case actions.CMD_OPEN: - vvConsole.showCommand(''); - break; - case actions.CMD_TABS_OPEN: - if (action[1] || false) { - // alter url - vvConsole.showCommand('open ' + window.location.href); - } else { - vvConsole.showCommand('open '); - } - break; - case actions.CMD_BUFFER: - vvConsole.showCommand('buffer '); - break; - case actions.SCROLL_LINES: - scrolls.scrollLines(window, action[1]); - break; - case actions.SCROLL_PAGES: - scrolls.scrollPages(window, action[1]); - break; - case actions.SCROLL_TOP: - scrolls.scrollTop(window); - break; - case actions.SCROLL_BOTTOM: - scrolls.scrollBottom(window); - break; - case actions.SCROLL_LEFT: - scrolls.scrollLeft(window); - break; - case actions.SCROLL_RIGHT: - scrolls.scrollRight(window); - break; - case actions.FOLLOW_START: - new Follow(window.document, action[1] || false); - break; - case actions.HISTORY_PREV: - histories.prev(window); - break; - case actions.HISTORY_NEXT: - histories.next(window); - break; - } -} - -const handleResponse = (response) => { - if (!response) { - return; - } - - switch(response.type) { - case 'response.action': - doAction(response.action); - break; - } -}; +browser.runtime.onMessage.addListener((action) => { + contentReducer(undefined, action); + return Promise.resolve(); +}); window.addEventListener("keypress", (e) => { if (e.target instanceof HTMLInputElement) { return; } - - let request = { - type: 'event.keypress', - code: e.which, - ctrl: e.ctrlKey, - } - - browser.runtime.sendMessage(request) - .then(handleResponse) + browser.runtime.sendMessage(inputActions.keyPress(e.which, e.ctrlKey)) .catch((err) => { console.error("Vim Vixen:", err); - vvConsole.showError(err.message); + return consoleFrames.showError(err.message); }); }); -const doCompletion = (line) => { - if (line.startsWith('buffer ')) { - let keyword = line.replace('buffer ', ''); - - browser.runtime.sendMessage({ - type: 'event.cmd.tabs.completion', - text: keyword - }).then((completions) => { - vvConsole.setCompletions([completions]); - }).catch((err) => { - console.error("Vim Vixen:", err); - vvConsole.showError(err.message); - }); - } -}; - -messages.receive(window, (message) => { - switch (message.type) { - case 'vimvixen.command.blur': - if (!vvConsole.isErrorShown()) { - vvConsole.hide(); - } - break; +browser.runtime.onMessage.addListener((action) => { + switch (action.type) { + case actions.CONSOLE_HIDE: + window.focus(); + return consoleFrames.blur(window.document); case 'vimvixen.command.enter': - browser.runtime.sendMessage({ + return browser.runtime.sendMessage({ type: 'event.cmd.enter', - text: message.value + text: action.value }).catch((err) => { console.error("Vim Vixen:", err); - vvConsole.showError(err.message); + return consoleFrames.showError(err.message); }); - break; - case 'vimvixen.command.change': - doCompletion(message.value); - break; default: - return; + return Promise.resolve(); } }); diff --git a/src/reducers/background.js b/src/reducers/background.js new file mode 100644 index 0000000..d7d7860 --- /dev/null +++ b/src/reducers/background.js @@ -0,0 +1,53 @@ +import * as tabs from '../background/tabs'; +import * as zooms from '../background/zooms'; +import * as consoleActions from '../actions/console'; +import actions from '../actions'; + +const doCompletion = (command, keywords, sender) => { + if (command === 'buffer') { + return tabs.getCompletions(keywords).then((tabs) => { + let items = tabs.map((tab) => { + return { + caption: tab.title, + content: tab.title, + url: tab.url, + icon: tab.favIconUrl + } + }); + let completions = { + name: "Buffers", + items: items + }; + return browser.tabs.sendMessage( + sender, + consoleActions.setCompletions([completions])); + }); + } + return Promise.resolve(); +}; + +export default function reducer(state, action = {}, sender) { + // TODO hide sender object + switch (action.type) { + case actions.BACKGROUND_REQUEST_COMPLETIONS: + return doCompletion(action.command, action.keywords, sender.tab.id); + case actions.TABS_CLOSE: + return tabs.closeTab(sender.tab.id); + case actions.TABS_REOPEN: + return tabs.reopenTab(); + case actions.TABS_PREV: + return tabs.selectPrevTab(sender.tab.index, action.count); + case actions.TABS_NEXT: + return tabs.selectNextTab(sender.tab.index, action.count); + case actions.TABS_RELOAD: + return tabs.reload(sender.tab, action.cache); + case actions.ZOOM_IN: + return zooms.zoomIn(); + case actions.ZOOM_OUT: + return zooms.zoomOut(); + case actions.ZOOM_NEUTRAL: + return zooms.neutral(); + default: + return Promise.resolve(); + } +} diff --git a/src/reducers/command.js b/src/reducers/command.js new file mode 100644 index 0000000..7e03593 --- /dev/null +++ b/src/reducers/command.js @@ -0,0 +1,24 @@ +import * as tabs from '../background/tabs'; +import actions from '../actions'; + +const cmdBuffer = (sender, arg) => { + if (isNaN(arg)) { + return tabs.selectByKeyword(sender.tab, arg); + } else { + let index = parseInt(arg, 10) - 1; + return tabs.selectAt(index); + } +} + +export default function reducer(state, action, sender) { + switch (action.type) { + case actions.COMMAND_OPEN_URL: + return browser.tabs.update(sender.tab.id, { url: action.url }); + case actions.COMMAND_TABOPEN_URL: + return browser.tabs.create({ url: action.url }); + case actions.COMMAND_BUFFER: + return cmdBuffer(sender, action.keywords); + default: + return Promise.resolve(); + } +} diff --git a/src/reducers/console.js b/src/reducers/console.js new file mode 100644 index 0000000..3303802 --- /dev/null +++ b/src/reducers/console.js @@ -0,0 +1,39 @@ +import actions from '../actions'; + +const defaultState = { + errorShown: false, + errorText: '', + commandShown: false, + commandText: '', + completions: [], +}; + +export default function reducer(state = defaultState, action = {}) { + switch (action.type) { + case actions.CONSOLE_SHOW_COMMAND: + return Object.assign({}, state, { + commandShown: true, + commandText: action.text, + errorShown: false, + completions: [] + }); + case actions.CONSOLE_SET_COMPLETIONS: + return Object.assign({}, state, { + completions: action.completions + }); + case actions.CONSOLE_SHOW_ERROR: + return Object.assign({}, state, { + errorText: action.text, + errorShown: true, + commandShown: false, + }); + case actions.CONSOLE_HIDE: + return Object.assign({}, state, { + errorShown: false, + commandShown: false + + }); + default: + return state; + } +} diff --git a/src/reducers/content.js b/src/reducers/content.js new file mode 100644 index 0000000..bcf1160 --- /dev/null +++ b/src/reducers/content.js @@ -0,0 +1,48 @@ +import * as consoleFrames from '../console/frames'; +import * as histories from '../content/histories'; +import * as scrolls from '../content/scrolls'; +import Follow from '../content/follow'; +import actions from '../actions'; + +export default function reducer(state, action = {}) { + switch (action.type) { + case actions.CMD_OPEN: + return consoleFrames.showCommand(''); + case actions.CMD_TABS_OPEN: + if (action.alter) { + // alter url + return consoleFrames.showCommand('open ' + window.location.href); + } else { + return consoleFrames.showCommand('open '); + } + case actions.CMD_BUFFER: + return consoleFrames.showCommand('buffer '); + case actions.SCROLL_LINES: + scrolls.scrollLines(window, action.count); + break; + case actions.SCROLL_PAGES: + scrolls.scrollPages(window, action.count); + break; + case actions.SCROLL_TOP: + scrolls.scrollTop(window); + break; + case actions.SCROLL_BOTTOM: + scrolls.scrollBottom(window); + break; + case actions.SCROLL_LEFT: + scrolls.scrollLeft(window); + break; + case actions.SCROLL_RIGHT: + scrolls.scrollRight(window); + break; + case actions.FOLLOW_START: + new Follow(window.document, action.newTab); + break; + case actions.HISTORY_PREV: + histories.prev(window); + break; + case actions.HISTORY_NEXT: + histories.next(window); + break; + } +} diff --git a/src/reducers/input.js b/src/reducers/input.js new file mode 100644 index 0000000..25ff1a3 --- /dev/null +++ b/src/reducers/input.js @@ -0,0 +1,23 @@ +import actions from '../actions'; + +const defaultState = { + keys: [], +}; + +export default function reducer(state = defaultState, action = {}) { + switch (action.type) { + case actions.INPUT_KEY_PRESS: + return Object.assign({}, state, { + keys: state.keys.concat([{ + code: action.code, + ctrl: action.ctrl + }]) + }); + case actions.INPUT_CLEAR_KEYS: + return Object.assign({}, state, { + keys: [], + }); + default: + return state; + } +} diff --git a/src/shared/actions.js b/src/shared/actions.js deleted file mode 100644 index 7151dd1..0000000 --- a/src/shared/actions.js +++ /dev/null @@ -1,54 +0,0 @@ -export const CMD_OPEN = 'cmd.open'; -export const CMD_TABS_OPEN = 'cmd.tabs.open'; -export const CMD_BUFFER = 'cmd.buffer'; -export const TABS_CLOSE = 'tabs.close'; -export const TABS_REOPEN = 'tabs.reopen'; -export const TABS_PREV = 'tabs.prev'; -export const TABS_NEXT = 'tabs.next'; -export const TABS_RELOAD = 'tabs.reload'; -export const SCROLL_LINES = 'scroll.lines'; -export const SCROLL_PAGES = 'scroll.pages'; -export const SCROLL_TOP = 'scroll.top'; -export const SCROLL_BOTTOM = 'scroll.bottom'; -export const SCROLL_LEFT= 'scroll.left'; -export const SCROLL_RIGHT= 'scroll.right'; -export const FOLLOW_START = 'follow.start'; -export const HISTORY_PREV = 'history.prev'; -export const HISTORY_NEXT = 'history.next'; -export const ZOOM_IN = 'zoom.in'; -export const ZOOM_OUT = 'zoom.out'; -export const ZOOM_NEUTRAL = 'zoom.neutral'; - -const BACKGROUND_ACTION_SET = new Set([ - TABS_CLOSE, - TABS_REOPEN, - TABS_PREV, - TABS_NEXT, - TABS_RELOAD, - ZOOM_IN, - ZOOM_OUT, - ZOOM_NEUTRAL -]); - -const CONTENT_ACTION_SET = new Set([ - CMD_OPEN, - CMD_TABS_OPEN, - CMD_BUFFER, - SCROLL_LINES, - SCROLL_PAGES, - SCROLL_TOP, - SCROLL_BOTTOM, - SCROLL_LEFT, - SCROLL_RIGHT, - FOLLOW_START, - HISTORY_PREV, - HISTORY_NEXT -]); - -export const isBackgroundAction = (action) => { - return BACKGROUND_ACTION_SET.has(action); -}; - -export const isContentAction = (action) => { - return CONTENT_ACTION_SET.has(action); -}; diff --git a/src/shared/messages.js b/src/shared/messages.js deleted file mode 100644 index 517fc4c..0000000 --- a/src/shared/messages.js +++ /dev/null @@ -1,19 +0,0 @@ -const receive = (win, callback) => { - win.addEventListener('message', (e) => { - let message; - try { - message = JSON.parse(e.data); - } catch (e) { - // ignore message posted by author of web page - return; - } - - callback(message); - }) -} - -const send = (win, message) => { - win.postMessage(JSON.stringify(message), '*'); -} - -export { receive, send }; diff --git a/test/actions/background.test.js b/test/actions/background.test.js new file mode 100644 index 0000000..a3203ee --- /dev/null +++ b/test/actions/background.test.js @@ -0,0 +1,14 @@ +import { expect } from "chai"; +import actions from '../../src/actions'; +import * as backgroundActions from '../../src/actions/background'; + +describe("background actions", () => { + describe("requestCompletions", () => { + it('create BACKGROUND_REQUEST_COMPLETIONS action', () => { + let action = backgroundActions.requestCompletions('buffer hoge fuga'); + expect(action.type).to.equal(actions.BACKGROUND_REQUEST_COMPLETIONS); + expect(action.command).to.equal('buffer'); + expect(action.keywords).to.equal('hoge fuga'); + }); + }); +}); diff --git a/test/actions/command.test.js b/test/actions/command.test.js new file mode 100644 index 0000000..01a67f2 --- /dev/null +++ b/test/actions/command.test.js @@ -0,0 +1,51 @@ +import { expect } from "chai"; +import actions from '../../src/actions'; +import * as commandActions from '../../src/actions/command'; + +describe("command actions", () => { + describe("exec", () => { + context("open command", () => { + it('create COMMAND_OPEN_URL acion with a full url', () => { + let action = commandActions.exec("open https://github.com/") + expect(action.type).to.equal(actions.COMMAND_OPEN_URL); + expect(action.url).to.equal('https://github.com/'); + }); + + it('create COMMAND_OPEN_URL acion with a domain name', () => { + let action = commandActions.exec("open github.com") + expect(action.type).to.equal(actions.COMMAND_OPEN_URL); + expect(action.url).to.equal('http://github.com'); + }); + }); + + context("tabopen command", () => { + it('create COMMAND_TABOPEN_URL acion with a full url', () => { + let action = commandActions.exec("tabopen https://github.com/") + expect(action.type).to.equal(actions.COMMAND_TABOPEN_URL); + expect(action.url).to.equal('https://github.com/'); + }); + + it('create COMMAND_TABOPEN_URL acion with a domain name', () => { + let action = commandActions.exec("tabopen github.com") + expect(action.type).to.equal(actions.COMMAND_TABOPEN_URL); + expect(action.url).to.equal('http://github.com'); + }); + }); + + context("buffer command", () => { + it('create COMMAND_BUFFER acion with a keywords', () => { + let action = commandActions.exec("buffer foo bar") + expect(action.type).to.equal(actions.COMMAND_BUFFER); + expect(action.keywords).to.equal('foo bar'); + }); + }); + + context("b command", () => { + it('create COMMAND_BUFFER acion with a keywords', () => { + let action = commandActions.exec("b foo bar") + expect(action.type).to.equal(actions.COMMAND_BUFFER); + expect(action.keywords).to.equal('foo bar'); + }); + }); + }); +}); diff --git a/test/actions/console.test.js b/test/actions/console.test.js new file mode 100644 index 0000000..512ee40 --- /dev/null +++ b/test/actions/console.test.js @@ -0,0 +1,37 @@ +import { expect } from "chai"; +import actions from '../../src/actions'; +import * as consoleActions from '../../src/actions/console'; + +describe("console actions", () => { + describe("showCommand", () => { + it('create CONSOLE_SHOW_COMMAND action', () => { + let action = consoleActions.showCommand('hello'); + expect(action.type).to.equal(actions.CONSOLE_SHOW_COMMAND); + expect(action.text).to.equal('hello'); + }); + }); + + describe("setCompletions", () => { + it('create CONSOLE_SET_COMPLETIONS action', () => { + let action = consoleActions.setCompletions([1,2,3]); + expect(action.type).to.equal(actions.CONSOLE_SET_COMPLETIONS); + expect(action.completions).to.deep.equal([1, 2, 3]); + }); + }); + + describe("showError", () => { + it('create CONSOLE_SHOW_ERROR action', () => { + let action = consoleActions.showError('an error'); + expect(action.type).to.equal(actions.CONSOLE_SHOW_ERROR); + expect(action.text).to.equal('an error'); + }); + }); + + describe("hide", () => { + it('create CONSOLE_HIDE action', () => { + let action = consoleActions.hide(); + expect(action.type).to.equal(actions.CONSOLE_HIDE); + }); + }); +}); + diff --git a/test/actions/input.test.js b/test/actions/input.test.js new file mode 100644 index 0000000..9ec6de4 --- /dev/null +++ b/test/actions/input.test.js @@ -0,0 +1,21 @@ +import { expect } from "chai"; +import actions from '../../src/actions'; +import * as inputActions from '../../src/actions/input'; + +describe("input actions", () => { + describe("keyPress", () => { + it('create INPUT_KEY_PRESS action', () => { + let action = inputActions.keyPress(123, true); + expect(action.type).to.equal(actions.INPUT_KEY_PRESS); + expect(action.code).to.equal(123); + expect(action.ctrl).to.be.true; + }); + }); + + describe("clearKeys", () => { + it('create INPUT_CLEAR_KEYSaction', () => { + let action = inputActions.clearKeys(); + expect(action.type).to.equal(actions.INPUT_CLEAR_KEYS); + }); + }); +}); diff --git a/test/background/key-queue.test.js b/test/background/key-queue.test.js deleted file mode 100644 index ac43228..0000000 --- a/test/background/key-queue.test.js +++ /dev/null @@ -1,50 +0,0 @@ -import { expect } from "chai"; -import KeyQueue from '../../src/background/key-queue'; - -describe("keyQueue class", () => { - const KEYMAP = { - 'gGG': [], - 'gg': [ 'scroll.top' ], - }; - - const g = 'g'.charCodeAt(0); - const G = 'G'.charCodeAt(0); - const x = 'x'.charCodeAt(0); - - describe("#push", () => { - it("returns matched action", () => { - let queue = new KeyQueue(KEYMAP); - queue.push({ code: g }); - let action = queue.push({ code: g }); - - expect(action).to.deep.equal([ 'scroll.top' ]); - }); - - it("returns null on no actions matched", () => { - let queue = new KeyQueue(KEYMAP); - queue.push({ code: g }); - let action = queue.push({ code: G }); - - expect(action).to.be.null; - expect(queue.asKeymapChars()).to.be.empty; - }); - }); - - describe('#asKeymapChars', () => { - let queue = new KeyQueue(KEYMAP); - queue.push({ code: g }); - queue.push({ code: x, ctrl: true }); - queue.push({ code: G }); - - expect(queue.asKeymapChars()).to.equal('gG'); - }); - - describe('#asCaretChars', () => { - let queue = new KeyQueue(KEYMAP); - queue.push({ code: g }); - queue.push({ code: x, ctrl: true }); - queue.push({ code: G }); - - expect(queue.asCaretChars()).to.equal('g^XG'); - }); -}); diff --git a/test/background/keys.test.js b/test/background/keys.test.js index da9d430..2cb9a3a 100644 --- a/test/background/keys.test.js +++ b/test/background/keys.test.js @@ -1,55 +1,31 @@ import { expect } from "chai"; -import { identifyKey, identifyKeys, hasPrefix } from '../../src/background/keys'; +import * as keys from '../../src/background/keys'; -describe('keys', () => { - describe('#identifyKey', () => { - it('return true if key matched', () => { - expect(identifyKey( - { code: 100 }, - { code: 100 })).to.be.true; - expect(identifyKey( - { code: 100, shift: true, ctrl: true }, - { code: 100, shift: true, ctrl: true })).to.be.true; - expect(identifyKey( - { code: 100, shift: false, ctrl: false }, - { code: 100 })).to.be.true; - }); +describe("keys", () => { + const KEYMAP = { + 'gGG': [], + 'gg': { type: 'scroll.top' }, + }; - it('return false if key not matched', () => { - expect(identifyKey( - { code: 100 }, - { code: 101 })).to.be.false; - expect(identifyKey( - { code: 100, shift: true, ctrl: true }, - { code: 100, shift: true })).to.be.false; - }); - }); - - describe('#identifyKeys', () => { - it ('return true if keys matched', () => { - let keys = [{ code: 100 }, { code: 101, ctrl: false}]; - let prefix = [{ code: 100, ctrl: false }, { code: 101 }]; - expect(hasPrefix(keys, prefix)).to.be.true; - }); + const g = 'g'.charCodeAt(0); + const G = 'G'.charCodeAt(0); + const x = 'x'.charCodeAt(0); - it ('return false if keys matched', () => { - let keys = [{ code: 100 }, { code: 101, ctrl: true }]; - let prefix = [{ code: 100 }, { code: 101 }]; - expect(hasPrefix(keys, prefix)).to.be.false; - }); + describe('#asKeymapChars', () => { + let keySequence = [ + { code: g }, + { code: x, ctrl: true }, + { code: G } + ]; + expect(keys.asKeymapChars(keySequence)).to.equal('gG'); }); - describe('#hasPrefix', () => { - it ('return true if prefix matched', () => { - let keys = [{ code: 100 }, { code: 101 }, { code: 102 }]; - let prefix = [{ code: 100 }, { code: 101 }]; - expect(hasPrefix(keys, prefix)).to.be.true; - }); - - it ('return false if prefix not matched', () => { - let keys = [{ code: 100 }, { code: 101 }, { code: 102 }]; - let prefix = [{ code: 102 }]; - expect(hasPrefix(keys, prefix)).to.be.false; - }); + describe('#asCaretChars', () => { + let keySequence = [ + { code: g }, + { code: x, ctrl: true }, + { code: G } + ]; + expect(keys.asCaretChars(keySequence)).to.equal('g^XG'); }); }); diff --git a/test/reducers/console.test.js b/test/reducers/console.test.js new file mode 100644 index 0000000..9820a08 --- /dev/null +++ b/test/reducers/console.test.js @@ -0,0 +1,43 @@ +import { expect } from "chai"; +import actions from '../../src/actions'; +import consoleReducer from '../../src/reducers/console'; + +describe("console reducer", () => { + it('return the initial state', () => { + let state = consoleReducer(undefined, {}); + expect(state).to.have.property('errorShown', false); + expect(state).to.have.property('errorText', ''); + expect(state).to.have.property('commandShown', false); + expect(state).to.have.property('commandText', ''); + expect(state).to.have.deep.property('completions', []); + }); + + it('return next state for CONSOLE_SHOW_COMMAND', () => { + let action = { type: actions.CONSOLE_SHOW_COMMAND, text: 'open ' }; + let state = consoleReducer({}, action); + expect(state).to.have.property('commandShown', true); + expect(state).to.have.property('commandText', 'open '); + expect(state).to.have.property('errorShown', false); + }); + + it('return next state for CONSOLE_SET_COMPLETIONS', () => { + let action = { type: actions.CONSOLE_SET_COMPLETIONS, completions: [1, 2, 3] }; + let state = consoleReducer({}, action); + expect(state).to.have.deep.property('completions', [1, 2, 3]); + }); + + it('return next state for CONSOLE_SHOW_ERROR', () => { + let action = { type: actions.CONSOLE_SHOW_ERROR, text: 'an error' }; + let state = consoleReducer({}, action); + expect(state).to.have.property('errorShown', true); + expect(state).to.have.property('errorText', 'an error'); + expect(state).to.have.property('commandShown', false); + }); + + it('return next state for CONSOLE_HIDE', () => { + let action = { type: actions.CONSOLE_HIDE }; + let state = consoleReducer({}, action); + expect(state).to.have.property('errorShown', false); + expect(state).to.have.property('commandShown', false); + }); +}); diff --git a/test/reducers/input.test.js b/test/reducers/input.test.js new file mode 100644 index 0000000..d7a0855 --- /dev/null +++ b/test/reducers/input.test.js @@ -0,0 +1,34 @@ +import { expect } from "chai"; +import actions from '../../src/actions'; +import inputReducer from '../../src/reducers/input'; + +describe("input reducer", () => { + it('return the initial state', () => { + let state = inputReducer(undefined, {}); + expect(state).to.have.deep.property('keys', []); + }); + + it('return next state for INPUT_KEY_PRESS', () => { + let action = { type: actions.INPUT_KEY_PRESS, code: 123, ctrl: true }; + let state = inputReducer(undefined, action); + expect(state).to.have.deep.property('keys', [{ code: 123, ctrl: true }]); + + action = { type: actions.INPUT_KEY_PRESS, code: 456, ctrl: false }; + state = inputReducer(state, action); + expect(state).to.have.deep.property('keys', [ + { code: 123, ctrl: true }, + { code: 456, ctrl: false } + ]); + }); + + it('return next state for INPUT_CLEAR_KEYS', () => { + let action = { type: actions.INPUT_CLEAR_KEYS }; + let state = inputReducer({ + keys: [ + { code: 123, ctrl: true }, + { code: 456, ctrl: false } + ] + }, action); + expect(state).to.have.deep.property('keys', []); + }); +}); diff --git a/test/shared/messages.test.js b/test/shared/messages.test.js deleted file mode 100644 index 0ebaf1a..0000000 --- a/test/shared/messages.test.js +++ /dev/null @@ -1,25 +0,0 @@ -import { expect } from "chai"; -import * as messages from '../../src/shared/messages'; - -describe('messages', () => { - describe('#receive', () => { - it('received a message', (done) => { - messages.receive(window, (message) => { - expect(message).to.deep.equal({ type: 'vimvixen.test' }); - done(); - }); - window.postMessage(JSON.stringify({ type: 'vimvixen.test' }), '*'); - }); - }); - - describe('#send', () => { - it('sends a message', (done) => { - window.addEventListener('message', (e) => { - let json = JSON.parse(e.data); - expect(json).to.deep.equal({ type: 'vimvixen.test' }); - done(); - }); - messages.send(window, { type: 'vimvixen.test' }); - }); - }); -});