aboutsummaryrefslogtreecommitdiff
path: root/src/content/follow.js
blob: d678351e286eb4072629e3fc23f9fae79d436901 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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) {
    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;
  }
}