aboutsummaryrefslogtreecommitdiff
path: root/src/console
diff options
context:
space:
mode:
Diffstat (limited to 'src/console')
-rw-r--r--src/console/actions/console.js44
-rw-r--r--src/console/actions/index.js9
-rw-r--r--src/console/components/completion.js61
-rw-r--r--src/console/components/console.js148
-rw-r--r--src/console/index.html20
-rw-r--r--src/console/index.js34
-rw-r--r--src/console/reducers/index.js94
-rw-r--r--src/console/site.scss92
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;
+ }
+ }
+}