diff --git a/src/actions/background.js b/src/actions/background.js deleted file mode 100644 index 40b901b..0000000 --- a/src/actions/background.js +++ /dev/null @@ -1,11 +0,0 @@ -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 index c983278..03f1e83 100644 --- a/src/actions/command.js +++ b/src/actions/command.js @@ -1,4 +1,5 @@ -import actions from '../actions'; +import * as tabs from '../background/tabs'; +import * as consoleActions from './console'; const normalizeUrl = (string) => { try { @@ -8,28 +9,76 @@ const normalizeUrl = (string) => { } } -export function exec(line) { - let name = line.split(' ')[0]; - let remaining = line.replace(name + ' ', ''); +const openCommand = (url) => { + return browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { + if (tabs.length > 0) { + return browser.tabs.update(tabs[0].id, { url: url }); + } + }); +} +const tabopenCommand = (url) => { + return browser.tabs.create({ url: url }); +} + +const bufferCommand = (keywords) => { + return browser.tabs.query({ active: true, currentWindow: true }).then((tabss) => { + if (tabss.length > 0) { + if (isNaN(keywords)) { + return tabs.selectByKeyword(tabss[0], keywords); + } else { + let index = parseInt(keywords, 10) - 1; + return tabs.selectAt(index); + } + } + }); +} + +const doCommand = (name, remaining) => { switch (name) { case 'open': // TODO use search engined and pass keywords to them - return { - type: actions.COMMAND_OPEN_URL, - url: normalizeUrl(remaining) - }; + return openCommand(normalizeUrl(remaining)); case 'tabopen': - return { - type: actions.COMMAND_TABOPEN_URL, - url: normalizeUrl(remaining) - }; + return tabopenCommand(normalizeUrl(remaining)); case 'b': case 'buffer': - return { - type: actions.COMMAND_BUFFER, - keywords: remaining - }; + return bufferCommand(remaining); } throw new Error(name + ' command is not defined'); } + +const getCompletions = (command, keywords) => { + switch (command) { + case '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 + } + }); + return [{ + name: "Buffers", + items: items + }]; + }); + } + return Promise.resolve([]); +}; + +export function exec(line) { + let name = line.split(' ')[0]; + let remaining = line.replace(name + ' ', ''); + return doCommand(name, remaining).then(() => { + return consoleActions.hide(); + }); +} + +export function complete(line) { + let command = line.split(' ', 1)[0]; + let keywords = line.replace(command + ' ', ''); + return getCompletions(command, keywords).then(consoleActions.setCompletions); +} diff --git a/src/actions/index.js b/src/actions/index.js index 63d5f6f..977b3c2 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -5,36 +5,7 @@ export default { 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/operation.js b/src/actions/operation.js new file mode 100644 index 0000000..e589b89 --- /dev/null +++ b/src/actions/operation.js @@ -0,0 +1,43 @@ +import operations from '../operations'; +import messages from '../messages'; +import * as consoleActions from './console'; +import * as tabs from '../background/tabs'; +import * as zooms from '../background/zooms'; + +export function exec(operation, tab) { + switch (operation.type) { + case operations.TABS_CLOSE: + return tabs.closeTab(tab.id); + case operations.TABS_REOPEN: + return tabs.reopenTab(); + case operations.TABS_PREV: + return tabs.selectPrevTab(tab.index, operation.count); + case operations.TABS_NEXT: + return tabs.selectNextTab(tab.index, operation.count); + case operations.TABS_RELOAD: + return tabs.reload(tab, operation.cache); + case operations.ZOOM_IN: + return zooms.zoomIn(); + case operations.ZOOM_OUT: + return zooms.zoomOut(); + case operations.ZOOM_NEUTRAL: + return zooms.neutral(); + case operations.COMMAND_OPEN: + return consoleActions.showCommand(''); + case operations.COMMAND_TABS_OPEN: + if (operations.alter) { + // alter url + return consoleActions.showCommand('open ' + tab.url); + } else { + return consoleActions.showCommand('open '); + } + case operations.COMMAND_BUFFER: + return consoleActions.showCommand('buffer '); + default: + return browser.tabs.sendMessage(tab.id, { + type: messages.CONTENT_OPERATION, + operation + }); + } +} + diff --git a/src/background/index.js b/src/background/index.js index e72cab0..ef1b881 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -1,45 +1,75 @@ 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'; +import * as operationActions from '../actions/operation'; +import * as commandActions from '../actions/command'; +import * as consoleActions from '../actions/console'; +import reducers from '../reducers'; +import messages from '../messages'; +import * as store from '../store' -let inputState = inputReducers(undefined, {}); +let prevInput = []; +const backgroundStore = store.createStore(reducers, (e, sender) => { + console.error('Vim-Vixen:', e); + if (sender) { + backgroundStore.dispatch(consoleActions.showError(e.message), sender); + } +}); +backgroundStore.subscribe((sender) => { + let currentInput = backgroundStore.getState().input + if (JSON.stringify(prevInput) === JSON.stringify(currentInput)) { + return + } + prevInput = currentInput; -const keyQueueChanged = (sender, prevState, state) => { - if (state.keys.length === 0) { - return Promise.resolve(); + if (currentInput.keys.length === 0) { + return; + } + if (sender) { + return keyQueueChanged(backgroundStore.getState(), sender); } +}); +backgroundStore.subscribe((sender) => { + if (sender) { + return browser.tabs.sendMessage(sender.tab.id, { + type: messages.STATE_UPDATE, + state: backgroundStore.getState() + }); + } +}); - let prefix = keys.asKeymapChars(state.keys); +const keyQueueChanged = (state, sender) => { + let prefix = keys.asKeymapChars(state.input.keys); let matched = Object.keys(keys.defaultKeymap).filter((keys) => { return keys.startsWith(prefix); }); if (matched.length == 0) { - return handleMessage(inputActions.clearKeys(), sender); + backgroundStore.dispatch(inputActions.clearKeys(), sender); + return Promise.resolve(); } else if (matched.length > 1 || matched.length === 1 && prefix !== matched[0]) { 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); - }); - }); + backgroundStore.dispatch(operationActions.exec(action, sender.tab), sender); + backgroundStore.dispatch(inputActions.clearKeys(), sender); }; -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); - } - return backgroundReducers(undefined, action, sender).then(() => { - return commandReducer(undefined, action, sender).then(() => { - return browser.tabs.sendMessage(sender.tab.id, action); - }); - }); -}; +const handleMessage = (message, sender) => { + switch (message.type) { + case messages.KEYDOWN: + return backgroundStore.dispatch(inputActions.keyPress(message.code, message.ctrl), sender); + case messages.CONSOLE_BLURRED: + return backgroundStore.dispatch(consoleActions.hide(), sender); + case messages.CONSOLE_ENTERED: + return backgroundStore.dispatch(commandActions.exec(message.text), sender); + case messages.CONSOLE_CHANGEED: + return backgroundStore.dispatch(commandActions.complete(message.text), sender); + } +} -browser.runtime.onMessage.addListener(handleMessage); +browser.runtime.onMessage.addListener((message, sender) => { + try { + handleMessage(message, sender); + } catch (e) { + backgroundStore.dispatch(consoleActions.showError(e.message), sender); + } +}); diff --git a/src/background/keys.js b/src/background/keys.js index 0ce53fa..8d75aba 100644 --- a/src/background/keys.js +++ b/src/background/keys.js @@ -1,35 +1,35 @@ -import actions from '../actions'; +import operations from '../operations'; 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 }, + ':': { type: operations.COMMAND_OPEN }, + 'o': { type: operations.COMMAND_TABS_OPEN, alter: false }, + 'O': { type: operations.COMMAND_TABS_OPEN, alter: true }, + 'b': { type: operations.COMMAND_BUFFER }, + 'k': { type: operations.SCROLL_LINES, count: -1 }, + 'j': { type: operations.SCROLL_LINES, count: 1 }, + '': { type: operations.SCROLL_LINES, count: -1 }, + '': { type: operations.SCROLL_LINES, count: 1 }, + '': { type: operations.SCROLL_PAGES, count: -0.5 }, + '': { type: operations.SCROLL_PAGES, count: 0.5 }, + '': { type: operations.SCROLL_PAGES, count: -1 }, + '': { type: operations.SCROLL_PAGES, count: 1 }, + 'gg': { type: operations.SCROLL_TOP }, + 'G': { type: operations.SCROLL_BOTTOM }, + '0': { type: operations.SCROLL_LEFT }, + '$': { type: operations.SCROLL_RIGHT }, + 'd': { type: operations.TABS_CLOSE }, + 'u': { type: operations.TABS_REOPEN }, + 'h': { type: operations.TABS_PREV, count: 1 }, + 'l': { type: operations.TABS_NEXT, count: 1 }, + 'r': { type: operations.TABS_RELOAD, cache: false }, + 'R': { type: operations.TABS_RELOAD, cache: true }, + 'zi': { type: operations.ZOOM_IN }, + 'zo': { type: operations.ZOOM_OUT }, + 'zz': { type: operations.ZOOM_NEUTRAL }, + 'f': { type: operations.FOLLOW_START, newTab: false }, + 'F': { type: operations.FOLLOW_START, newTab: true }, + 'H': { type: operations.HISTORY_PREV }, + 'L': { type: operations.HISTORY_NEXT }, } const asKeymapChars = (keys) => { diff --git a/src/console/console.js b/src/console/console.js index 25ab36d..d1547b8 100644 --- a/src/console/console.js +++ b/src/console/console.js @@ -1,18 +1,17 @@ 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 consoleReducer from '../reducers/console'; +import messages from '../messages'; // TODO consider object-oriented var prevValue = ""; var completion = null; var completionOrigin = ""; -let state = consoleReducer(undefined, {}); +var prevState = {}; const handleBlur = () => { - return browser.runtime.sendMessage(consoleActions.hide()); + return browser.runtime.sendMessage({ + type: messages.CONSOLE_BLURRED, + }); }; const completeNext = () => { @@ -52,7 +51,10 @@ const handleKeydown = (e) => { case KeyboardEvent.DOM_VK_ESCAPE: return input.blur(); case KeyboardEvent.DOM_VK_RETURN: - return browser.runtime.sendMessage(commandActions.exec(e.target.value)); + return browser.runtime.sendMessage({ + type: messages.CONSOLE_ENTERED, + text: e.target.value + }); case KeyboardEvent.DOM_VK_TAB: if (e.shiftKey) { completePrev(); @@ -73,9 +75,10 @@ const handleKeyup = (e) => { return; } prevValue = e.target.value; - return browser.runtime.sendMessage( - backgroundActions.requestCompletions(e.target.value) - ); + return browser.runtime.sendMessage({ + type: messages.CONSOLE_CHANGEED, + text: e.target.value + }); }; window.addEventListener('load', () => { @@ -147,7 +150,7 @@ const updateCompletions = (completions) => { completionOrigin = input.value.split(' ')[0]; } -const update = (prevState, state) => { +const update = (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'); @@ -156,25 +159,26 @@ const update = (prevState, state) => { error.textContent = state.errorText; command.style.display = state.commandShown ? 'block' : 'none'; - if (!prevState.commandShown && state.commandShown) { - // setup input on firstly shown + if (state.commandShown && !prevState.commandShown) { input.value = state.commandText; input.focus(); } - if (JSON.stringify(state.completions) !== JSON.stringify(prevState.completions)) { updateCompletions(state.completions); } + + prevState = state; } browser.runtime.onMessage.addListener((action) => { - let nextState = consoleReducer(state, action); - if (JSON.stringify(nextState) !== JSON.stringify(state)) { - update(state, nextState); - state = nextState; + if (action.type === messages.STATE_UPDATE) { + return update(action.state.console); } }); window.addEventListener('load', () => { - update({}, state); + let error = window.document.querySelector('#vimvixen-console-error'); + let command = window.document.querySelector('#vimvixen-console-command'); + error.style.display = 'none'; + command.style.display = 'none'; }); diff --git a/src/console/frames.js b/src/console/frames.js index 0b6f3e2..ec1e38c 100644 --- a/src/console/frames.js +++ b/src/console/frames.js @@ -1,5 +1,4 @@ import './console-frame.scss'; -import * as consoleActions from '../actions/console'; const initialize = (doc) => { let iframe = doc.createElement('iframe'); @@ -11,17 +10,9 @@ const initialize = (doc) => { 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 }; +export { initialize, blur }; diff --git a/src/content/index.js b/src/content/index.js index 12d079f..5d3735c 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -1,40 +1,61 @@ 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'; +import * as scrolls from '../content/scrolls'; +import * as histories from '../content/histories'; +import Follow from '../content/follow'; +import operations from '../operations'; +import messages from '../messages'; consoleFrames.initialize(window.document); -browser.runtime.onMessage.addListener((action) => { - contentReducer(undefined, action); - return Promise.resolve(); -}); - window.addEventListener("keypress", (e) => { if (e.target instanceof HTMLInputElement) { return; } - browser.runtime.sendMessage(inputActions.keyPress(e.which, e.ctrlKey)) - .catch((err) => { - console.error("Vim Vixen:", err); - return consoleFrames.showError(err.message); - }); + browser.runtime.sendMessage({ + type: messages.KEYDOWN, + code: e.which, + ctrl: e.ctrl + }); }); +const execOperation = (operation) => { + switch (operation.type) { + case operations.SCROLL_LINES: + return scrolls.scrollLines(window, operation.count); + case operations.SCROLL_PAGES: + return scrolls.scrollPages(window, operation.count); + case operations.SCROLL_TOP: + return scrolls.scrollTop(window); + case operations.SCROLL_BOTTOM: + return scrolls.scrollBottom(window); + case operations.SCROLL_LEFT: + return scrolls.scrollLeft(window); + case operations.SCROLL_RIGHT: + return scrolls.scrollRight(window); + case operations.FOLLOW_START: + return new Follow(window.document, operation.newTab); + case operations.HISTORY_PREV: + return histories.prev(window); + case operations.HISTORY_NEXT: + return histories.next(window); + } +} + +const update = (state) => { + if (!state.console.commandShown) { + window.focus(); + consoleFrames.blur(window.document); + } +} + browser.runtime.onMessage.addListener((action) => { switch (action.type) { - case actions.CONSOLE_HIDE: - window.focus(); - return consoleFrames.blur(window.document); - case 'vimvixen.command.enter': - return browser.runtime.sendMessage({ - type: 'event.cmd.enter', - text: action.value - }).catch((err) => { - console.error("Vim Vixen:", err); - return consoleFrames.showError(err.message); - }); + case messages.STATE_UPDATE: + return update(action.state); + case messages.CONTENT_OPERATION: + execOperation(action.operation); + return Promise.resolve(); default: return Promise.resolve(); } diff --git a/src/messages/index.js b/src/messages/index.js new file mode 100644 index 0000000..3bdecca --- /dev/null +++ b/src/messages/index.js @@ -0,0 +1,10 @@ +export default { + STATE_UPDATE: 'state.update', + CONTENT_OPERATION: 'content.operation', + + CONSOLE_BLURRED: 'console.blured', + CONSOLE_ENTERED: 'console.entered', + CONSOLE_CHANGEED: 'console.changed', + + KEYDOWN: 'keydown' +}; diff --git a/src/operations/index.js b/src/operations/index.js new file mode 100644 index 0000000..05c7d78 --- /dev/null +++ b/src/operations/index.js @@ -0,0 +1,26 @@ +export default { + // command + COMMAND_OPEN: 'cmd.open', + COMMAND_TABS_OPEN: 'cmd.tabs.open', + COMMAND_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', + + // background + 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', +} diff --git a/src/reducers/background.js b/src/reducers/background.js deleted file mode 100644 index d7d7860..0000000 --- a/src/reducers/background.js +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 7e03593..0000000 --- a/src/reducers/command.js +++ /dev/null @@ -1,24 +0,0 @@ -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 index 3303802..27ccdc9 100644 --- a/src/reducers/console.js +++ b/src/reducers/console.js @@ -28,10 +28,13 @@ export default function reducer(state = defaultState, action = {}) { commandShown: false, }); case actions.CONSOLE_HIDE: + if (state.errorShown) { + // keep error message if shown + return state; + } return Object.assign({}, state, { errorShown: false, commandShown: false - }); default: return state; diff --git a/src/reducers/content.js b/src/reducers/content.js deleted file mode 100644 index bcf1160..0000000 --- a/src/reducers/content.js +++ /dev/null @@ -1,48 +0,0 @@ -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/index.js b/src/reducers/index.js new file mode 100644 index 0000000..83a9a56 --- /dev/null +++ b/src/reducers/index.js @@ -0,0 +1,14 @@ +import inputReducer from '../reducers/input'; +import consoleReducer from '../reducers/console'; + +const defaultState = { + input: inputReducer(undefined, {}), + console: consoleReducer(undefined, {}) +}; + +export default function reducer(state = defaultState, action = {}) { + return Object.assign({}, state, { + input: inputReducer(state.input, action), + console: consoleReducer(state.console, action) + }); +} diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..2d08296 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,51 @@ +class Store { + constructor(reducer, catcher) { + this.reducer = reducer; + this.catcher = catcher; + this.subscribers = []; + try { + this.state = this.reducer(undefined, {}); + } catch (e) { + catcher(e); + } + } + + dispatch(action, sender) { + if (action instanceof Promise) { + action.then((a) => { + this.transitNext(a, sender); + }).catch((e) => { + this.catcher(e, sender); + }); + } else { + try { + this.transitNext(action, sender); + } catch (e) { + this.catcher(e, sender); + } + } + return action + } + + getState() { + return this.state; + } + + subscribe(callback) { + this.subscribers.push(callback); + } + + transitNext(action, sender) { + let newState = this.reducer(this.state, action); + if (JSON.stringify(this.state) !== JSON.stringify(newState)) { + this.state = newState; + this.subscribers.forEach(f => f(sender)); + } + } +} + +const empty = () => {}; + +export function createStore(reducer, catcher = empty) { + return new Store(reducer, catcher); +} diff --git a/test/actions/background.test.js b/test/actions/background.test.js deleted file mode 100644 index a3203ee..0000000 --- a/test/actions/background.test.js +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 01a67f2..0000000 --- a/test/actions/command.test.js +++ /dev/null @@ -1,51 +0,0 @@ -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/store/index.test.js b/test/store/index.test.js new file mode 100644 index 0000000..e19d50e --- /dev/null +++ b/test/store/index.test.js @@ -0,0 +1,111 @@ +import { expect } from "chai"; +import { createStore } from '../../src/store'; + +describe("Store class", () => { + const reducer = (state, action) => { + if (state == undefined) { + return 0; + } + return state + action; + }; + + describe("#dispatch", () => { + it('transit status by immediate action', () => { + let store = createStore(reducer); + store.dispatch(10); + expect(store.getState()).to.equal(10); + + store.dispatch(-20); + expect(store.getState()).to.equal(-10); + }); + + it('returns next state by immediate action', () => { + let store = createStore(reducer); + let dispatchedAction = store.dispatch(11); + expect(dispatchedAction).to.equal(11); + }); + + it('transit status by Promise action', () => { + let store = createStore(reducer); + let p1 = Promise.resolve(10); + + return store.dispatch(p1).then(() => { + expect(store.getState()).to.equal(10); + }).then(() => { + store.dispatch(Promise.resolve(-20)); + }).then(() => { + expect(store.getState()).to.equal(-10); + }); + }); + + it('returns next state by promise action', () => { + let store = createStore(reducer); + let dispatchedAction = store.dispatch(Promise.resolve(11)); + return dispatchedAction.then((value) => { + expect(value).to.equal(11); + }); + }); + }); + + describe("#subscribe", () => { + it('invoke callback', (done) => { + let store = createStore(reducer); + store.subscribe(() => { + expect(store.getState()).to.equal(15); + done(); + }); + store.dispatch(15); + }); + + it('propagate sender object', (done) => { + let store = createStore(reducer); + store.subscribe((sender) => { + expect(sender).to.equal('sender'); + done(); + }); + store.dispatch(15, 'sender'); + }); + }) + + describe("catcher", () => { + it('catch an error in reducer on initializing by immediate action', (done) => { + let store = createStore(() => { + throw new Error(); + }, (e) => { + expect(e).to.be.an('error'); + done(); + }); + }); + + it('catch an error in reducer on initializing by immediate action', (done) => { + let store = createStore((state, action) => { + if (state === undefined) return 0; + throw new Error(); + }, (e) => { + expect(e).to.be.an('error'); + done(); + }); + store.dispatch(20); + }); + + it('catch an error in reducer on initializing by promise action', (done) => { + let store = createStore((state, action) => { + if (state === undefined) return 0; + throw new Error(); + }, (e) => { + expect(e).to.be.an('error'); + done(); + }); + store.dispatch(Promise.resolve(20)); + }); + + it('catch an error in promise action', (done) => { + let store = createStore((state, action) => 0, (e) => { + expect(e).to.be.an('error'); + done(); + }); + store.dispatch(new Promise(() => { throw new Error() })); + }); + }) +}); +