diff options
Diffstat (limited to 'src/console')
-rw-r--r-- | src/console/actions/console.js | 44 | ||||
-rw-r--r-- | src/console/actions/index.js | 9 | ||||
-rw-r--r-- | src/console/components/completion.js | 61 | ||||
-rw-r--r-- | src/console/components/console.js | 148 | ||||
-rw-r--r-- | src/console/index.html | 20 | ||||
-rw-r--r-- | src/console/index.js | 34 | ||||
-rw-r--r-- | src/console/reducers/index.js | 94 | ||||
-rw-r--r-- | src/console/site.scss | 92 |
8 files changed, 502 insertions, 0 deletions
diff --git a/src/console/actions/console.js b/src/console/actions/console.js new file mode 100644 index 0000000..01d9a9b --- /dev/null +++ b/src/console/actions/console.js @@ -0,0 +1,44 @@ +import actions from 'console/actions'; + +const showCommand = (text) => { + return { + type: actions.CONSOLE_SHOW_COMMAND, + text: text + }; +}; + +const showError = (text) => { + return { + type: actions.CONSOLE_SHOW_ERROR, + text: text + }; +}; + +const hide = () => { + return { + type: actions.CONSOLE_HIDE + }; +}; + +const setCompletions = (completions) => { + return { + type: actions.CONSOLE_SET_COMPLETIONS, + completions: completions + }; +}; + +const completionNext = () => { + return { + type: actions.CONSOLE_COMPLETION_NEXT, + }; +}; + +const completionPrev = () => { + return { + type: actions.CONSOLE_COMPLETION_PREV, + }; +}; + +export { + showCommand, showError, hide, setCompletions, completionNext, completionPrev +}; diff --git a/src/console/actions/index.js b/src/console/actions/index.js new file mode 100644 index 0000000..a5d03bc --- /dev/null +++ b/src/console/actions/index.js @@ -0,0 +1,9 @@ +export default { + // console commands + CONSOLE_SHOW_COMMAND: 'console.show.command', + CONSOLE_SET_COMPLETIONS: 'console.set.completions', + CONSOLE_SHOW_ERROR: 'console.show.error', + CONSOLE_HIDE: 'console.hide', + CONSOLE_COMPLETION_NEXT: 'console.completion.next', + CONSOLE_COMPLETION_PREV: 'console.completion.prev', +}; diff --git a/src/console/components/completion.js b/src/console/components/completion.js new file mode 100644 index 0000000..5033b5c --- /dev/null +++ b/src/console/components/completion.js @@ -0,0 +1,61 @@ +export default class Completion { + constructor(wrapper, store) { + this.wrapper = wrapper; + this.store = store; + this.prevState = {}; + } + + update() { + let state = this.store.getState(); + if (JSON.stringify(this.prevState) === JSON.stringify(state)) { + return; + } + + this.wrapper.innerHTML = ''; + + for (let i = 0; i < state.completions.length; ++i) { + let group = state.completions[i]; + let title = this.createCompletionTitle(group.name); + this.wrapper.append(title); + + for (let j = 0; j < group.items.length; ++j) { + let item = group.items[j]; + let li = this.createCompletionItem(item.icon, item.caption, item.url); + this.wrapper.append(li); + + if (i === state.groupSelection && j === state.itemSelection) { + li.classList.add('vimvixen-completion-selected'); + } + } + } + + this.prevState = state; + } + + createCompletionTitle(text) { + let doc = this.wrapper.ownerDocument; + let li = doc.createElement('li'); + li.className = 'vimvixen-console-completion-title'; + li.textContent = text; + return li; + } + + createCompletionItem(icon, caption, url) { + let doc = this.wrapper.ownerDocument; + + let captionEle = doc.createElement('span'); + captionEle.className = 'vimvixen-console-completion-item-caption'; + captionEle.textContent = caption; + + let urlEle = doc.createElement('span'); + urlEle.className = 'vimvixen-console-completion-item-url'; + urlEle.textContent = url; + + let li = doc.createElement('li'); + li.style.backgroundImage = 'url(' + icon + ')'; + li.className = 'vimvixen-console-completion-item'; + li.append(captionEle); + li.append(urlEle); + return li; + } +} diff --git a/src/console/components/console.js b/src/console/components/console.js new file mode 100644 index 0000000..9023d91 --- /dev/null +++ b/src/console/components/console.js @@ -0,0 +1,148 @@ +import messages from 'shared/messages'; +import * as consoleActions from 'console/actions/console'; + +export default class ConsoleComponent { + constructor(wrapper, store) { + this.wrapper = wrapper; + this.prevValue = ''; + this.prevState = {}; + this.completionOrigin = ''; + this.store = store; + + let doc = this.wrapper.ownerDocument; + let input = doc.querySelector('#vimvixen-console-command-input'); + input.addEventListener('blur', this.onBlur.bind(this)); + input.addEventListener('keydown', this.onKeyDown.bind(this)); + input.addEventListener('keyup', this.onKeyUp.bind(this)); + + this.hideCommand(); + this.hideError(); + } + + onBlur() { + return browser.runtime.sendMessage({ + type: messages.CONSOLE_BLURRED, + }); + } + + onKeyDown(e) { + let doc = this.wrapper.ownerDocument; + let input = doc.querySelector('#vimvixen-console-command-input'); + + switch (e.keyCode) { + case KeyboardEvent.DOM_VK_ESCAPE: + return input.blur(); + case KeyboardEvent.DOM_VK_RETURN: + return browser.runtime.sendMessage({ + type: messages.CONSOLE_ENTERED, + text: e.target.value + }).then(this.onBlur); + case KeyboardEvent.DOM_VK_TAB: + if (e.shiftKey) { + this.store.dispatch(consoleActions.completionPrev()); + } else { + this.store.dispatch(consoleActions.completionNext()); + } + e.stopPropagation(); + e.preventDefault(); + break; + } + } + + onKeyUp(e) { + if (e.keyCode === KeyboardEvent.DOM_VK_TAB) { + return; + } + if (e.target.value === this.prevValue) { + return; + } + + let doc = this.wrapper.ownerDocument; + let input = doc.querySelector('#vimvixen-console-command-input'); + this.completionOrigin = input.value; + + this.prevValue = e.target.value; + return browser.runtime.sendMessage({ + type: messages.CONSOLE_QUERY_COMPLETIONS, + text: e.target.value + }).then((completions) => { + this.store.dispatch(consoleActions.setCompletions(completions)); + }); + } + + update() { + let state = this.store.getState(); + if (!this.prevState.commandShown && state.commandShown) { + this.showCommand(state.commandText); + } else if (!state.commandShown) { + this.hideCommand(); + } + + if (state.errorShown) { + this.setErrorText(state.errorText); + this.showError(); + } else { + this.hideError(); + } + + if (state.groupSelection >= 0 && state.itemSelection >= 0) { + let group = state.completions[state.groupSelection]; + let item = group.items[state.itemSelection]; + this.setCommandValue(item.content); + } else if (state.completions.length > 0 && + JSON.stringify(this.prevState.completions) === + JSON.stringify(state.completions)) { + // Reset input only completion groups not changed (unselected an item in + // completion) in order to avoid to override previous input + this.setCommandCompletionOrigin(); + } + + this.prevState = state; + } + + showCommand(text) { + let doc = this.wrapper.ownerDocument; + let command = doc.querySelector('#vimvixen-console-command'); + let input = doc.querySelector('#vimvixen-console-command-input'); + + command.style.display = 'block'; + input.value = text; + input.focus(); + } + + hideCommand() { + let doc = this.wrapper.ownerDocument; + let command = doc.querySelector('#vimvixen-console-command'); + command.style.display = 'none'; + } + + setCommandValue(value) { + let doc = this.wrapper.ownerDocument; + let input = doc.querySelector('#vimvixen-console-command-input'); + input.value = value; + } + + setCommandCompletionOrigin() { + let doc = this.wrapper.ownerDocument; + let input = doc.querySelector('#vimvixen-console-command-input'); + input.value = this.completionOrigin; + } + + setErrorText(text) { + let doc = this.wrapper.ownerDocument; + let error = doc.querySelector('#vimvixen-console-error'); + error.textContent = text; + } + + showError() { + let doc = this.wrapper.ownerDocument; + let error = doc.querySelector('#vimvixen-console-error'); + error.style.display = 'block'; + } + + hideError() { + let doc = this.wrapper.ownerDocument; + let error = doc.querySelector('#vimvixen-console-error'); + error.style.display = 'none'; + } +} diff --git a/src/console/index.html b/src/console/index.html new file mode 100644 index 0000000..4222f12 --- /dev/null +++ b/src/console/index.html @@ -0,0 +1,20 @@ +<!doctype html> +<html> + <head> + <meta charset=utf-8 /> + <title>VimVixen console</title> + <script src='console.js'></script> + </head> + <body class='vimvixen-console'> + <p id='vimvixen-console-error' + class='vimvixen-console-error'></p> + <div id='vimvixen-console-command'> + <ul id='vimvixen-console-completion' class='vimvixen-console-completion'></ul> + <div class='vimvixen-console-command'> + <i class='vimvixen-console-command-prompt'></i><input + id='vimvixen-console-command-input' + class='vimvixen-console-command-input'></input> + </div> + </div> + </body> +</html> diff --git a/src/console/index.js b/src/console/index.js new file mode 100644 index 0000000..7396a96 --- /dev/null +++ b/src/console/index.js @@ -0,0 +1,34 @@ +import './site.scss'; +import messages from 'shared/messages'; +import CompletionComponent from 'console/components/completion'; +import ConsoleComponent from 'console/components/console'; +import reducers from 'console/reducers'; +import { createStore } from 'shared/store'; +import * as consoleActions from 'console/actions/console'; + +const store = createStore(reducers); +let completionComponent = null; +let consoleComponent = null; + +window.addEventListener('load', () => { + let wrapper = document.querySelector('#vimvixen-console-completion'); + completionComponent = new CompletionComponent(wrapper, store); + + consoleComponent = new ConsoleComponent(document.body, store); +}); + +store.subscribe(() => { + completionComponent.update(); + consoleComponent.update(); +}); + +browser.runtime.onMessage.addListener((action) => { + switch (action.type) { + case messages.CONSOLE_SHOW_COMMAND: + return store.dispatch(consoleActions.showCommand(action.command)); + case messages.CONSOLE_SHOW_ERROR: + return store.dispatch(consoleActions.showError(action.text)); + case messages.CONSOLE_HIDE: + return store.dispatch(consoleActions.hide(action.command)); + } +}); diff --git a/src/console/reducers/index.js b/src/console/reducers/index.js new file mode 100644 index 0000000..ee9c691 --- /dev/null +++ b/src/console/reducers/index.js @@ -0,0 +1,94 @@ +import actions from 'console/actions'; + +const defaultState = { + errorShown: false, + errorText: '', + commandShown: false, + commandText: '', + completions: [], + groupSelection: -1, + itemSelection: -1, +}; + +const nextSelection = (state) => { + if (state.groupSelection < 0) { + return [0, 0]; + } + + let group = state.completions[state.groupSelection]; + if (state.groupSelection + 1 >= state.completions.length && + state.itemSelection + 1 >= group.items.length) { + return [-1, -1]; + } + if (state.itemSelection + 1 >= group.items.length) { + return [state.groupSelection + 1, 0]; + } + return [state.groupSelection, state.itemSelection + 1]; +}; + +const prevSelection = (state) => { + if (state.groupSelection < 0) { + return [ + state.completions.length - 1, + state.completions[state.completions.length - 1].items.length - 1 + ]; + } + if (state.groupSelection === 0 && state.itemSelection === 0) { + return [-1, -1]; + } else if (state.itemSelection === 0) { + return [ + state.groupSelection - 1, + state.completions[state.groupSelection - 1].items.length - 1 + ]; + } + return [state.groupSelection, state.itemSelection - 1]; +}; + +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_SHOW_ERROR: + return Object.assign({}, state, { + errorText: action.text, + errorShown: true, + commandShown: false, + }); + case actions.CONSOLE_HIDE: + if (state.errorShown) { + // keep error message if shown + return state; + } + return Object.assign({}, state, { + errorShown: false, + commandShown: false + }); + case actions.CONSOLE_SET_COMPLETIONS: + return Object.assign({}, state, { + completions: action.completions, + groupSelection: -1, + itemSelection: -1, + }); + case actions.CONSOLE_COMPLETION_NEXT: { + let next = nextSelection(state); + return Object.assign({}, state, { + groupSelection: next[0], + itemSelection: next[1], + }); + } + case actions.CONSOLE_COMPLETION_PREV: { + let next = prevSelection(state); + return Object.assign({}, state, { + groupSelection: next[0], + itemSelection: next[1], + }); + } + default: + return state; + } +} diff --git a/src/console/site.scss b/src/console/site.scss new file mode 100644 index 0000000..5823dce --- /dev/null +++ b/src/console/site.scss @@ -0,0 +1,92 @@ +html, body, * { + margin: 0; + padding: 0; +} + +body { + position: absolute; + bottom: 0; + left: 0; + right: 0; + overflow: hidden; +} + +.vimvixen-console { + border-top: 1px solid gray; + bottom: 0; + margin: 0; + padding: 0; + + @mixin consoole-font { + font-style: normal; + font-family: monospace; + font-size: 12px; + line-height: 16px; + } + + &-completion { + background-color: white; + + @include consoole-font; + + &-title { + background-color: lightgray; + font-weight: bold; + margin: 0; + padding: 0; + } + + &-item { + padding-left: 1.5rem; + background-position: 0 center; + background-size: contain; + background-repeat: no-repeat; + white-space: nowrap; + + &.vimvixen-completion-selected { + background-color: yellow; + } + + &-caption { + display: inline-block; + width: 40%; + text-overflow: ellipsis; + overflow: hidden; + } + + &-url { + display: inline-block; + color: green; + width: 60%; + text-overflow: ellipsis; + overflow: hidden; + } + } + } + + &-error { + background-color: red; + font-weight: bold; + color: white; + + @include consoole-font; + } + + &-command { + background-color: white; + display: flex; + + &-prompt:before { + content: ':'; + + @include consoole-font; + } + + &-input { + border: none; + flex-grow: 1; + + @include consoole-font; + } + } +} |