follow as redux

This commit is contained in:
Shin'ya Ueoka 2017-10-02 21:35:52 +09:00
parent 6f857e2c81
commit 0a7ae631cd
8 changed files with 297 additions and 208 deletions

View file

@ -1,149 +0,0 @@
import Hint from './hint';
import HintKeyProducer from './hint-key-producer';
const DEFAULT_HINT_CHARSET = 'abcdefghijklmnopqrstuvwxyz';
export default class Follow {
constructor(doc) {
this.doc = doc;
this.hintElements = {};
this.keys = [];
this.onActivatedCallbacks = [];
let links = Follow.getTargetElements(doc);
this.addHints(links);
this.boundKeydown = this.handleKeydown.bind(this);
doc.addEventListener('keydown', this.boundKeydown);
}
addHints(elements) {
let producer = new HintKeyProducer(DEFAULT_HINT_CHARSET);
Array.prototype.forEach.call(elements, (ele) => {
let keys = producer.produce();
let hint = new Hint(ele, keys);
this.hintElements[keys] = hint;
});
}
handleKeydown(e) {
let { keyCode } = e;
if (keyCode === KeyboardEvent.DOM_VK_ESCAPE) {
this.remove();
return;
} else if (keyCode === KeyboardEvent.DOM_VK_ENTER ||
keyCode === KeyboardEvent.DOM_VK_RETURN) {
let chars = Follow.codeChars(this.keys);
this.activate(this.hintElements[chars].target);
return;
} else if (Follow.availableKey(keyCode)) {
this.keys.push(keyCode);
} else if (keyCode === KeyboardEvent.DOM_VK_BACK_SPACE ||
keyCode === KeyboardEvent.DOM_VK_DELETE) {
this.keys.pop();
}
e.stopPropagation();
e.preventDefault();
this.refreshKeys();
}
refreshKeys() {
let chars = Follow.codeChars(this.keys);
let shown = Object.keys(this.hintElements).filter((key) => {
return key.startsWith(chars);
});
let hidden = Object.keys(this.hintElements).filter((key) => {
return !key.startsWith(chars);
});
if (shown.length === 0) {
this.remove();
return;
} else if (shown.length === 1) {
this.remove();
this.activate(this.hintElements[chars].target);
}
shown.forEach((key) => {
this.hintElements[key].show();
});
hidden.forEach((key) => {
this.hintElements[key].hide();
});
}
remove() {
this.doc.removeEventListener('keydown', this.boundKeydown);
Object.keys(this.hintElements).forEach((key) => {
this.hintElements[key].remove();
});
}
activate(element) {
this.onActivatedCallbacks.forEach(f => f(element));
}
onActivated(f) {
this.onActivatedCallbacks.push(f);
}
static availableKey(keyCode) {
return (
KeyboardEvent.DOM_VK_0 <= keyCode && keyCode <= KeyboardEvent.DOM_VK_9 ||
KeyboardEvent.DOM_VK_A <= keyCode && keyCode <= KeyboardEvent.DOM_VK_Z
);
}
static isNumericKey(code) {
return KeyboardEvent.DOM_VK_0 <= code && code <= KeyboardEvent.DOM_VK_9;
}
static isAlphabeticKey(code) {
return KeyboardEvent.DOM_VK_A <= code && code <= KeyboardEvent.DOM_VK_Z;
}
static codeChars(codes) {
const CHARCODE_ZERO = '0'.charCodeAt(0);
const CHARCODE_A = 'a'.charCodeAt(0);
let chars = '';
for (let code of codes) {
if (Follow.isNumericKey(code)) {
chars += String.fromCharCode(
code - KeyboardEvent.DOM_VK_0 + CHARCODE_ZERO);
} else if (Follow.isAlphabeticKey(code)) {
chars += String.fromCharCode(
code - KeyboardEvent.DOM_VK_A + CHARCODE_A);
}
}
return chars;
}
static inWindow(window, element) {
let {
top, left, bottom, right
} = element.getBoundingClientRect();
return (
top >= 0 && left >= 0 &&
bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
static getTargetElements(doc) {
let all = doc.querySelectorAll('a,button,input,textarea');
let filtered = Array.prototype.filter.call(all, (element) => {
let style = window.getComputedStyle(element);
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
element.type !== 'hidden' &&
element.offsetHeight > 0 &&
Follow.inWindow(window, element);
});
return filtered;
}
}

View file

@ -2,63 +2,25 @@ import './console-frame.scss';
import * as consoleFrames from './console-frames';
import * as scrolls from '../content/scrolls';
import * as navigates from '../content/navigates';
import Follow from '../content/follow';
import * as followActions from '../actions/follow';
import * as store from '../store';
import FollowComponent from '../components/follow';
import followReducer from '../reducers/follow';
import operations from '../operations';
import messages from './messages';
const followStore = store.createStore(followReducer);
const followComponent = new FollowComponent(window.document.body, followStore);
followStore.subscribe(() => {
try {
followComponent.update();
} catch (e) {
console.error(e);
}
});
consoleFrames.initialize(window.document);
const startFollows = (newTab) => {
let follow = new Follow(window.document);
follow.onActivated((element) => {
switch (element.tagName.toLowerCase()) {
case 'a':
if (newTab) {
// getAttribute() to avoid to resolve absolute path
let href = element.getAttribute('href');
// eslint-disable-next-line no-script-url
if (!href || href === '#' || href.startsWith('javascript:')) {
return;
}
return browser.runtime.sendMessage({
type: messages.OPEN_URL,
url: element.href,
newTab
});
}
if (element.href.startsWith('http://') ||
element.href.startsWith('https://') ||
element.href.startsWith('ftp://')) {
return browser.runtime.sendMessage({
type: messages.OPEN_URL,
url: element.href,
newTab
});
}
return element.click();
case 'input':
switch (element.type) {
case 'file':
case 'checkbox':
case 'radio':
case 'submit':
case 'reset':
case 'button':
case 'image':
case 'color':
return element.click();
default:
return element.focus();
}
case 'textarea':
return element.focus();
case 'button':
return element.click();
}
});
};
window.addEventListener('keypress', (e) => {
if (e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
@ -90,7 +52,7 @@ const execOperation = (operation) => {
case operations.SCROLL_END:
return scrolls.scrollRight(window);
case operations.FOLLOW_START:
return startFollows(operation.newTab);
return followStore.dispatch(followActions.enable(false));
case operations.NAVIGATE_HISTORY_PREV:
return navigates.historyPrev(window);
case operations.NAVIGATE_HISTORY_NEXT: