diff --git a/src/actions/completion.js b/src/actions/completion.js new file mode 100644 index 0000000..1ffb025 --- /dev/null +++ b/src/actions/completion.js @@ -0,0 +1,22 @@ +import actions from '../actions'; + +const setItems = (groups) => { + return { + type: actions.COMPLETION_SET_ITEMS, + groups, + }; +}; + +const selectNext = () => { + return { + type: actions.COMPLETION_SELECT_NEXT + }; +}; + +const selectPrev = () => { + return { + type: actions.COMPLETION_SELECT_PREV + }; +}; + +export { setItems, selectNext, selectPrev }; diff --git a/src/actions/index.js b/src/actions/index.js index 7b79864..2aa28fa 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -9,4 +9,9 @@ export default { INPUT_KEY_PRESS: 'input.key,press', INPUT_CLEAR_KEYS: 'input.clear.keys', INPUT_SET_KEYMAPS: 'input.set,keymaps', + + // Completion + COMPLETION_SET_ITEMS: 'completion.set.items', + COMPLETION_SELECT_NEXT: 'completions.select.next', + COMPLETION_SELECT_PREV: 'completions.select.prev' }; diff --git a/src/components/completion.js b/src/components/completion.js new file mode 100644 index 0000000..e6ee0cb --- /dev/null +++ b/src/components/completion.js @@ -0,0 +1,55 @@ +export default class Completion { + constructor(wrapper, store) { + this.wrapper = wrapper; + this.store = store; + } + + update() { + let state = this.store.getState(); + + this.wrapper.innerHTML = ''; + + for (let i = 0; i < state.groups.length; ++i) { + let group = state.groups[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'); + } + } + } + } + + 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/pages/completion.js b/src/pages/completion.js deleted file mode 100644 index 4c69afb..0000000 --- a/src/pages/completion.js +++ /dev/null @@ -1,27 +0,0 @@ -export default class Completion { - constructor(completions) { - if (typeof completions.length !== 'number') { - throw new TypeError('completions does not have a length in number'); - } - this.completions = completions; - this.index = 0; - } - - prev() { - let length = this.completions.length; - if (length === 0) { - return null; - } - this.index = (this.index + length - 1) % length; - return this.completions[this.index]; - } - - next() { - if (this.completions.length === 0) { - return null; - } - let item = this.completions[this.index]; - this.index = (this.index + 1) % this.completions.length; - return item; - } -} diff --git a/src/pages/console.js b/src/pages/console.js index 31f2643..40e713e 100644 --- a/src/pages/console.js +++ b/src/pages/console.js @@ -1,61 +1,41 @@ import './console.scss'; -import Completion from './completion'; import messages from '../content/messages'; +import CompletionComponent from '../components/completion'; +import completionReducer from '../reducers/completion'; +import * as store from '../store'; +import * as completionActions from '../actions/completion'; + +const completionStore = store.createStore(completionReducer); +let completionComponent = null; + +window.addEventListener('load', () => { + let wrapper = document.querySelector('#vimvixen-console-completion'); + completionComponent = new CompletionComponent(wrapper, completionStore); +}); // TODO consider object-oriented let prevValue = ''; -let completion = null; let completionOrigin = ''; let prevState = {}; -const handleBlur = () => { - return browser.runtime.sendMessage({ - type: messages.CONSOLE_BLURRED, - }); -}; - -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 completeNext = () => { - if (!completion) { - return; - } - let item = completion.next(); - if (!item) { - return; - } +completionStore.subscribe(() => { + completionComponent.update(); + let state = completionStore.getState(); let input = window.document.querySelector('#vimvixen-console-command-input'); - input.value = completionOrigin + ' ' + item[0].content; - selectCompletion(item[1]); -}; - -const completePrev = () => { - if (!completion) { - return; + if (state.groupSelection >= 0) { + let item = state.groups[state.groupSelection].items[state.itemSelection]; + input.value = completionOrigin + ' ' + item.content; + } else if (state.groups.length > 0) { + input.value = completionOrigin + ' '; } - let item = completion.prev(); - if (!item) { - return; - } - - let input = window.document.querySelector('#vimvixen-console-command-input'); - input.value = completionOrigin + ' ' + item[0].content; +}); - selectCompletion(item[1]); +const handleBlur = () => { + return browser.runtime.sendMessage({ + type: messages.CONSOLE_BLURRED, + }); }; const handleKeydown = (e) => { @@ -71,9 +51,9 @@ const handleKeydown = (e) => { }); case KeyboardEvent.DOM_VK_TAB: if (e.shiftKey) { - completePrev(); + completionStore.dispatch(completionActions.selectPrev()); } else { - completeNext(); + completionStore.dispatch(completionActions.selectNext()); } e.stopPropagation(); e.preventDefault(); @@ -102,52 +82,10 @@ window.addEventListener('load', () => { input.addEventListener('keyup', handleKeyup); }); -const createCompletionTitle = (text) => { - let li = document.createElement('li'); - li.className = 'vimvixen-console-completion-title'; - li.textContent = text; - return li; -}; - -const createCompletionItem = (icon, caption, url) => { - let captionEle = document.createElement('span'); - captionEle.className = 'vimvixen-console-completion-item-caption'; - captionEle.textContent = caption; - - let urlEle = document.createElement('span'); - urlEle.className = 'vimvixen-console-completion-item-url'; - urlEle.textContent = url; - - let li = document.createElement('li'); - li.style.backgroundImage = 'url(' + icon + ')'; - li.className = 'vimvixen-console-completion-item'; - li.append(captionEle); - li.append(urlEle); - return li; -}; - 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); - completionsContainer.append(title); + completionStore.dispatch(completionActions.setItems(completions)); - for (let item of group.items) { - let li = createCompletionItem(item.icon, item.caption, item.url); - 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]; }; diff --git a/src/reducers/completion.js b/src/reducers/completion.js new file mode 100644 index 0000000..a706988 --- /dev/null +++ b/src/reducers/completion.js @@ -0,0 +1,61 @@ +import actions from '../actions'; + +const defaultState = { + groupSelection: -1, + itemSelection: -1, + groups: [], +}; + +const nextSelection = (state) => { + if (state.groupSelection < 0) { + return [0, 0]; + } + + let group = state.groups[state.groupSelection]; + if (state.groupSelection + 1 >= state.groups.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 [0, 0]; + } + if (state.groupSelection === 0 && state.itemSelection === 0) { + return [-1, -1]; + } else if (state.itemSelection === 0) { + return [ + state.groupSelection - 1, + state.groups[state.groupSelection - 1].items.length - 1 + ]; + } + return [state.groupSelection, state.itemSelection - 1]; +}; + +export default function reducer(state = defaultState, action = {}) { + switch (action.type) { + case actions.COMPLETION_SET_ITEMS: + return Object.assign({}, state, { + groups: action.groups + }); + case actions.COMPLETION_SELECT_NEXT: { + let next = nextSelection(state); + return Object.assign({}, state, { + groupSelection: next[0], + itemSelection: next[1], + }); + } + case actions.COMPLETION_SELECT_PREV: { + let next = prevSelection(state); + return Object.assign({}, state, { + groupSelection: next[0], + itemSelection: next[1], + }); + } + } +} diff --git a/test/pages/completion.test.js b/test/pages/completion.test.js deleted file mode 100644 index 28dd9a7..0000000 --- a/test/pages/completion.test.js +++ /dev/null @@ -1,48 +0,0 @@ -import { expect } from "chai"; -import Completion from '../../src/pages/completion'; - -describe('Completion class', () => { - describe('#constructor', () => { - it('creates new object by iterable items', () => { - new Completion([1,2,3,4,5]); - new Completion([]); - new Completion('hello'); - new Completion(''); - }); - - it('creates new object by iterable items', () => { - expect(() => new Completion({ key: 'value' })).to.throw(TypeError); - expect(() => new Completion(12345)).to.throw(TypeError); - }); - }); - - describe('#next', () => { - it('complete next items', () => { - let completion = new Completion([3, 4, 5]); - expect(completion.next()).to.equal(3); - expect(completion.next()).to.equal(4); - expect(completion.next()).to.equal(5); - expect(completion.next()).to.equal(3); - }); - - it('returns null when empty completions', () => { - let completion = new Completion([]); - expect(completion.next()).to.be.null; - }); - }); - - describe('#prev', () => { - it('complete prev items', () => { - let completion = new Completion([3, 4, 5]); - expect(completion.prev()).to.equal(5); - expect(completion.prev()).to.equal(4); - expect(completion.prev()).to.equal(3); - expect(completion.prev()).to.equal(5); - }); - - it('returns null when empty completions', () => { - let completion = new Completion([]); - expect(completion.prev()).to.be.null; - }); - }); -});