diff options
Diffstat (limited to 'src/content')
-rw-r--r-- | src/content/follow.js | 123 | ||||
-rw-r--r-- | src/content/hint-key-producer.js | 33 | ||||
-rw-r--r-- | src/content/hint.css | 7 | ||||
-rw-r--r-- | src/content/hint.js | 43 | ||||
-rw-r--r-- | src/content/index.js | 4 |
5 files changed, 210 insertions, 0 deletions
diff --git a/src/content/follow.js b/src/content/follow.js new file mode 100644 index 0000000..ffa16b9 --- /dev/null +++ b/src/content/follow.js @@ -0,0 +1,123 @@ +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) { + let chars = Follow.codeChars(this.keys); + this.hintElements[chars].activate(); + 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(); + } + + 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.hintElements[chars].activate(); + } + + 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(); + }); + } + + 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) { + let all = doc.querySelectorAll('a'); + let filtered = Array.prototype.filter.call(all, (e) => { + return Follow.isVisibleElement(e); + }); + return filtered; + } + + static isVisibleElement(element) { + var style = window.getComputedStyle(element); + if (style.display === 'none') { + return false; + } else if (style.visibility === 'hidden') { + return false; + } + return true; + } +} diff --git a/src/content/hint-key-producer.js b/src/content/hint-key-producer.js new file mode 100644 index 0000000..8064afb --- /dev/null +++ b/src/content/hint-key-producer.js @@ -0,0 +1,33 @@ +export default class HintKeyProducer { + constructor(charset) { + if (charset.length === 0) { + throw new TypeError('charset is empty'); + } + + this.charset = charset; + this.counter = []; + } + + produce() { + this.increment(); + + return this.counter.map((x) => this.charset[x]).join(''); + } + + increment() { + let max = this.charset.length - 1; + if (this.counter.every((x) => x == max)) { + this.counter = new Array(this.counter.length + 1).fill(0); + return; + } + + this.counter.reverse(); + let len = this.charset.length; + let num = this.counter.reduce((x,y,index) => x + y * (len ** index)) + 1; + for (let i = 0; i < this.counter.length; ++i) { + this.counter[i] = num % len; + num = ~~(num / len); + } + this.counter.reverse(); + } +} diff --git a/src/content/hint.css b/src/content/hint.css new file mode 100644 index 0000000..a0f1233 --- /dev/null +++ b/src/content/hint.css @@ -0,0 +1,7 @@ +.vimvixen-hint { + background-color: yellow; + border: 1px solid gold; + font-weight: bold; + position: absolute; + text-transform: uppercase; +} diff --git a/src/content/hint.js b/src/content/hint.js new file mode 100644 index 0000000..fabf725 --- /dev/null +++ b/src/content/hint.js @@ -0,0 +1,43 @@ +import './hint.css'; + +export default class Hint { + constructor(target, tag) { + if (!(document.body instanceof HTMLElement)) { + throw new TypeError('target is not an HTMLElement'); + } + + this.target = target; + + let doc = target.ownerDocument + let { top, left } = target.getBoundingClientRect(); + let scrollX = window.scrollX; + let scrollY = window.scrollY; + + this.element = doc.createElement('span'); + this.element.className = 'vimvixen-hint'; + this.element.textContent = tag; + this.element.style.left = left + scrollX + 'px'; + this.element.style.top = top + scrollY + 'px'; + + this.show(); + doc.body.append(this.element); + } + + show() { + this.element.style.display = 'inline'; + } + + hide() { + this.element.style.display = 'none'; + } + + remove() { + this.element.remove(); + } + + activate() { + if (this.target.tagName.toLowerCase() === 'a') { + this.target.click(); + } + } +} 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; } } |