separate console

This commit is contained in:
Shin'ya Ueoka 2017-10-08 14:18:12 +09:00
parent 22db12f2a3
commit 541449b1fc
13 changed files with 38 additions and 40 deletions

View file

@ -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
};

View file

@ -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',
};

View file

@ -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;
}
}

View file

@ -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';
}
}

20
src/console/index.html Normal file
View file

@ -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>

34
src/console/index.js Normal file
View file

@ -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 '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));
}
});

View file

@ -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;
}
}

92
src/console/site.scss Normal file
View file

@ -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;
}
}
}