diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/follow.js | 210 | 
1 files changed, 210 insertions, 0 deletions
diff --git a/src/components/follow.js b/src/components/follow.js new file mode 100644 index 0000000..4fe4c58 --- /dev/null +++ b/src/components/follow.js @@ -0,0 +1,210 @@ +import * as followActions from '../actions/follow'; +import messages from '../content/messages'; +import Hint from '../content/hint'; +import HintKeyProducer from '../content/hint-key-producer'; + +const DEFAULT_HINT_CHARSET = 'abcdefghijklmnopqrstuvwxyz'; + +const availableKey = (keyCode) => { +  return ( +    KeyboardEvent.DOM_VK_0 <= keyCode && keyCode <= KeyboardEvent.DOM_VK_9 || +    KeyboardEvent.DOM_VK_A <= keyCode && keyCode <= KeyboardEvent.DOM_VK_Z +  ); +}; + +const isNumericKey = (code) => { +  return KeyboardEvent.DOM_VK_0 <= code && code <= KeyboardEvent.DOM_VK_9; +}; + +const isAlphabeticKey = (code) => { +  return KeyboardEvent.DOM_VK_A <= code && code <= KeyboardEvent.DOM_VK_Z; +}; + +const inWindow = (window, element) => { +  let { +    top, left, bottom, right +  } = element.getBoundingClientRect(); +  return ( +    top >= 0 && left >= 0 && +    bottom <= (window.innerHeight || document.documentElement.clientHeight) && +    right <= (window.innerWidth || document.documentElement.clientWidth) +  ); +}; + +export default class FollowComponent { +  constructor(wrapper, store) { +    this.wrapper = wrapper; +    this.store = store; +    this.hintElements = {}; +    this.state = {}; + +    let doc = wrapper.ownerDocument; +    doc.addEventListener('keydown', this.onKeyDown.bind(this)); +  } + +  update() { +    let prevState = this.state; +    this.state = this.store.getState(); +    if (!prevState.enabled && this.state.enabled) { +      this.create(); +    } else if (prevState.enabled && !this.state.enabled) { +      this.remove(); +    } else if (JSON.stringify(prevState.keys) !== +      JSON.stringify(this.state.keys)) { +      this.updateHints(); +    } +  } + +  onKeyDown(e) { +    if (!this.state.enabled) { +      return; +    } + +    let { keyCode } = e; +    switch (keyCode) { +    case KeyboardEvent.DOM_VK_ENTER: +    case KeyboardEvent.DOM_VK_RETURN: +      this.activate(this.hintElements[ +        FollowComponent.codeChars(this.state.keys)].target); +      return; +    case KeyboardEvent.DOM_VK_ESCAPE: +      this.store.dispatch(followActions.disable()); +      return; +    case KeyboardEvent.DOM_VK_BACK_SPACE: +    case KeyboardEvent.DOM_VK_DELETE: +      this.store.dispatch(followActions.backspace()); +      break; +    default: +      if (availableKey(keyCode)) { +        this.store.dispatch(followActions.keyPress(keyCode)); +      } +      break; +    } + +    e.stopPropagation(); +    e.preventDefault(); +  } + +  updateHints() { +    let chars = FollowComponent.codeChars(this.state.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.activate(this.hintElements[chars].target); +      this.remove(); +    } + +    shown.forEach((key) => { +      this.hintElements[key].show(); +    }); +    hidden.forEach((key) => { +      this.hintElements[key].hide(); +    }); +  } + +  activate(element) { +    switch (element.tagName.toLowerCase()) { +    case 'a': +      if (this.state.newTab) { +        // getAttribute() to avoid to resolve absolute path +        let href = element.getAttribute('href'); + +        // eslint-disable-next-line no-script-url +        if (!href || href === '#' || href.startsWith('javascript:')) { +          return; +        } +        return browser.runtime.sendMessage({ +          type: messages.OPEN_URL, +          url: element.href, +          newTab: this.state.newTab, +        }); +      } +      if (element.href.startsWith('http://') || +        element.href.startsWith('https://') || +        element.href.startsWith('ftp://')) { +        return browser.runtime.sendMessage({ +          type: messages.OPEN_URL, +          url: element.href, +          newTab: this.state.newTab, +        }); +      } +      return element.click(); +    case 'input': +      switch (element.type) { +      case 'file': +      case 'checkbox': +      case 'radio': +      case 'submit': +      case 'reset': +      case 'button': +      case 'image': +      case 'color': +        return element.click(); +      default: +        return element.focus(); +      } +    case 'textarea': +      return element.focus(); +    case 'button': +      return element.click(); +    } +  } + +  create() { +    let doc = this.wrapper.ownerDocument; +    let elements = FollowComponent.getTargetElements(doc); +    let producer = new HintKeyProducer(DEFAULT_HINT_CHARSET); +    let hintElements = {}; +    Array.prototype.forEach.call(elements, (ele) => { +      let keys = producer.produce(); +      let hint = new Hint(ele, keys); +      hintElements[keys] = hint; +    }); +    this.hintElements = hintElements; +  } + +  remove() { +    let hintElements = this.hintElements; +    Object.keys(this.hintElements).forEach((key) => { +      hintElements[key].remove(); +    }); +  } + +  static codeChars(codes) { +    const CHARCODE_ZERO = '0'.charCodeAt(0); +    const CHARCODE_A = 'a'.charCodeAt(0); + +    let chars = ''; + +    for (let code of codes) { +      if (isNumericKey(code)) { +        chars += String.fromCharCode( +          code - KeyboardEvent.DOM_VK_0 + CHARCODE_ZERO); +      } else if (isAlphabeticKey(code)) { +        chars += String.fromCharCode( +          code - KeyboardEvent.DOM_VK_A + CHARCODE_A); +      } +    } +    return chars; +  } + +  static getTargetElements(doc) { +    let all = doc.querySelectorAll('a,button,input,textarea'); +    let filtered = Array.prototype.filter.call(all, (element) => { +      let style = window.getComputedStyle(element); +      return style.display !== 'none' && +        style.visibility !== 'hidden' && +        element.type !== 'hidden' && +        element.offsetHeight > 0 && +        inWindow(window, element); +    }); +    return filtered; +  } +}  | 
