From 355da18d3d6232620ebd7548f8d41b6f1af5aa08 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 20 Aug 2017 21:19:42 +0900 Subject: [PATCH] add follow fot a tags --- src/background/key-queue.js | 2 + src/content/follow.js | 110 ++++++++++++++++++++++++++++++++++++ src/content/index.js | 4 ++ src/shared/actions.js | 4 +- 4 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/content/follow.js diff --git a/src/background/key-queue.js b/src/background/key-queue.js index d753bc1..5693b36 100644 --- a/src/background/key-queue.js +++ b/src/background/key-queue.js @@ -13,6 +13,8 @@ const DEFAULT_KEYMAP = [ { keys: [{ code: KeyboardEvent.DOM_VK_U }], action: [ actions.TABS_REOPEN]}, { keys: [{ code: KeyboardEvent.DOM_VK_H }], action: [ actions.TABS_PREV, 1 ]}, { keys: [{ code: KeyboardEvent.DOM_VK_L }], action: [ actions.TABS_NEXT, 1 ]}, + { keys: [{ code: KeyboardEvent.DOM_VK_F }], action: [ actions.FOLLOW_START, false ]}, + { keys: [{ code: KeyboardEvent.DOM_VK_F, shift: true }], action: [ actions.FOLLOW_START, true ]}, ] export default class KeyQueue { diff --git a/src/content/follow.js b/src/content/follow.js new file mode 100644 index 0000000..c0b7a44 --- /dev/null +++ b/src/content/follow.js @@ -0,0 +1,110 @@ +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 = []; + + // TODO activate input elements and push button elements + 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.keyCode; + if (keyCode === KeyboardEvent.DOM_VK_ESCAPE) { + this.remove(); + return; + } else if (keyCode === KeyboardEvent.DOM_VK_ENTER || + keyCode === KeyboardEvent.DOM_VK_RETURN) { + this.openUrl(this.keys); + 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(); + } + + + let keysAsString = Follow.codeChars(this.keys); + let shown = Object.keys(this.hintElements).filter((key) => { + return key.startsWith(keysAsString); + }); + let hidden = Object.keys(this.hintElements).filter((key) => { + return !key.startsWith(keysAsString); + }); + if (shown.length == 0) { + this.remove(); + return; + } else if (shown.length == 1) { + this.openUrl(this.keys); + return; + } + + 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(); + }); + } + + openUrl(keys) { + let chars = Follow.codeChars(keys); + this.hintElements[chars].activate(); + } + + 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 codeChars(codes) { + const CHARCODE_ZERO = '0'.charCodeAt(0); + const CHARCODE_A = 'a'.charCodeAt(0); + + let chars = ''; + + for (let code of codes) { + if (KeyboardEvent.DOM_VK_0 <= code && code <= KeyboardEvent.DOM_VK_9) { + chars += String.fromCharCode(code - KeyboardEvent.DOM_VK_0 + CHARCODE_ZERO); + } else if (KeyboardEvent.DOM_VK_A <= code && code <= KeyboardEvent.DOM_VK_Z) { + chars += String.fromCharCode(code - KeyboardEvent.DOM_VK_A + CHARCODE_A); + } + } + return chars; + } + + static getTargetElements(doc) { + return doc.querySelectorAll('a') + } +} diff --git a/src/content/index.js b/src/content/index.js index 17ab308..78389fd 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -1,5 +1,6 @@ import * as scrolls from './scrolls'; import FooterLine from './footer-line'; +import Follow from './follow'; import * as actions from '../shared/actions'; var footer = null; @@ -52,6 +53,9 @@ const invokeEvent = (action) => { case actions.SCROLL_BOTTOM: scrolls.scrollBottom(window, action[1]); break; + case actions.FOLLOW_START: + new Follow(window.document, action[1] || false); + break; } } diff --git a/src/shared/actions.js b/src/shared/actions.js index be25d72..bb61dbc 100644 --- a/src/shared/actions.js +++ b/src/shared/actions.js @@ -8,6 +8,7 @@ export const SCROLL_UP = 'scroll.up'; export const SCROLL_DOWN = 'scroll.down'; export const SCROLL_TOP = 'scroll.top'; export const SCROLL_BOTTOM = 'scroll.bottom'; +export const FOLLOW_START = 'follow.start'; const BACKGROUND_ACTION_SET = new Set([ TABS_CLOSE, @@ -22,7 +23,8 @@ const CONTENT_ACTION_SET = new Set([ SCROLL_UP, SCROLL_DOWN, SCROLL_TOP, - SCROLL_BOTTOM + SCROLL_BOTTOM, + FOLLOW_START ]); export const isBackgroundAction = (action) => {