aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/background/key-queue.js2
-rw-r--r--src/content/follow.js110
-rw-r--r--src/content/index.js4
-rw-r--r--src/shared/actions.js4
4 files changed, 119 insertions, 1 deletions
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) => {