diff options
| author | Shin'ya Ueoka <ueokande@i-beam.org> | 2017-08-22 22:00:42 +0900 | 
|---|---|---|
| committer | Shin'ya Ueoka <ueokande@i-beam.org> | 2017-08-22 22:00:42 +0900 | 
| commit | ab5fd9a336166cab75ff00e65afb4be991ff0765 (patch) | |
| tree | f2e933c927baf64cc393ea1a83ed57cc1fcc9bb2 /src | |
| parent | 13fb726332e9638cb3fafc477cf9fe641cb906ce (diff) | |
| parent | dcebe336281d068649927a8bb8d3c0403807ef01 (diff) | |
Merge branch 'follow-hints'
Diffstat (limited to 'src')
| -rw-r--r-- | src/background/key-queue.js | 2 | ||||
| -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 | ||||
| -rw-r--r-- | src/shared/actions.js | 4 | 
7 files changed, 215 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..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;    }  } 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) => { | 
