diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/actions/follow.js | 29 | ||||
| -rw-r--r-- | src/actions/index.js | 6 | ||||
| -rw-r--r-- | src/components/follow.js | 210 | ||||
| -rw-r--r-- | src/content/follow.js | 149 | ||||
| -rw-r--r-- | src/content/index.js | 68 | ||||
| -rw-r--r-- | src/reducers/follow.js | 31 | 
6 files changed, 291 insertions, 202 deletions
| diff --git a/src/actions/follow.js b/src/actions/follow.js new file mode 100644 index 0000000..7ab689e --- /dev/null +++ b/src/actions/follow.js @@ -0,0 +1,29 @@ +import actions from '../actions'; + +const enable = (newTab) => { +  return { +    type: actions.FOLLOW_ENABLE, +    newTab, +  }; +}; + +const disable = () => { +  return { +    type: actions.FOLLOW_DISABLE, +  }; +}; + +const keyPress = (key) => { +  return { +    type: actions.FOLLOW_KEY_PRESS, +    key: key +  }; +}; + +const backspace = () => { +  return { +    type: actions.FOLLOW_BACKSPACE, +  }; +}; + +export { enable, disable, keyPress, backspace }; diff --git a/src/actions/index.js b/src/actions/index.js index 63c36d2..4e8d4a7 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -17,4 +17,10 @@ export default {    // Settings    SETTING_SET_SETTINGS: 'setting.set.settings', + +  // Follow +  FOLLOW_ENABLE: 'follow.enable', +  FOLLOW_DISABLE: 'follow.disable', +  FOLLOW_KEY_PRESS: 'follow.key.press', +  FOLLOW_BACKSPACE: 'follow.backspace',  }; 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; +  } +} diff --git a/src/content/follow.js b/src/content/follow.js deleted file mode 100644 index b1d2f5c..0000000 --- a/src/content/follow.js +++ /dev/null @@ -1,149 +0,0 @@ -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 = []; -    this.onActivatedCallbacks = []; - -    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; -    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.activate(this.hintElements[chars].target); -      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(); -    } - -    e.stopPropagation(); -    e.preventDefault(); - -    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.activate(this.hintElements[chars].target); -    } - -    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(); -    }); -  } - -  activate(element) { -    this.onActivatedCallbacks.forEach(f => f(element)); -  } - -  onActivated(f) { -    this.onActivatedCallbacks.push(f); -  } - -  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 isNumericKey(code) { -    return KeyboardEvent.DOM_VK_0 <= code && code <= KeyboardEvent.DOM_VK_9; -  } - -  static isAlphabeticKey(code) { -    return KeyboardEvent.DOM_VK_A <= code && code <= 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 (Follow.isNumericKey(code)) { -        chars += String.fromCharCode( -          code - KeyboardEvent.DOM_VK_0 + CHARCODE_ZERO); -      } else if (Follow.isAlphabeticKey(code)) { -        chars += String.fromCharCode( -          code - KeyboardEvent.DOM_VK_A + CHARCODE_A); -      } -    } -    return chars; -  } - -  static 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) -    ); -  } - -  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 && -        Follow.inWindow(window, element); -    }); -    return filtered; -  } -} diff --git a/src/content/index.js b/src/content/index.js index 2e64af2..0dbc8c1 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -2,62 +2,24 @@ import './console-frame.scss';  import * as consoleFrames from './console-frames';  import * as scrolls from '../content/scrolls';  import * as navigates from '../content/navigates'; -import Follow from '../content/follow'; +import * as followActions from '../actions/follow'; +import * as store from '../store'; +import FollowComponent from '../components/follow'; +import followReducer from '../reducers/follow';  import operations from '../operations';  import messages from './messages'; -consoleFrames.initialize(window.document); - -const startFollows = (newTab) => { -  let follow = new Follow(window.document); -  follow.onActivated((element) => { -    switch (element.tagName.toLowerCase()) { -    case 'a': -      if (newTab) { -        // getAttribute() to avoid to resolve absolute path -        let href = element.getAttribute('href'); +const followStore = store.createStore(followReducer); +const followComponent = new FollowComponent(window.document.body, followStore); +followStore.subscribe(() => { +  try { +    followComponent.update(); +  } catch (e) { +    console.error(e); +  } +}); -        // 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 -        }); -      } -      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 -        }); -      } -      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(); -    } -  }); -}; +consoleFrames.initialize(window.document);  window.addEventListener('keypress', (e) => {    if (e.target instanceof HTMLInputElement || @@ -90,7 +52,7 @@ const execOperation = (operation) => {    case operations.SCROLL_END:      return scrolls.scrollRight(window);    case operations.FOLLOW_START: -    return startFollows(operation.newTab); +    return followStore.dispatch(followActions.enable(false));    case operations.NAVIGATE_HISTORY_PREV:      return navigates.historyPrev(window);    case operations.NAVIGATE_HISTORY_NEXT: diff --git a/src/reducers/follow.js b/src/reducers/follow.js new file mode 100644 index 0000000..136b367 --- /dev/null +++ b/src/reducers/follow.js @@ -0,0 +1,31 @@ +import actions from '../actions'; + +const defaultState = { +  enabled: false, +  newTab: false, +  keys: [], +}; + +export default function reducer(state = defaultState, action = {}) { +  switch (action.type) { +  case actions.FOLLOW_ENABLE: +    return Object.assign({}, state, { +      enabled: true, +      newTab: action.newTab, +    }); +  case actions.FOLLOW_DISABLE: +    return Object.assign({}, state, { +      enabled: false, +    }); +  case actions.FOLLOW_KEY_PRESS: +    return Object.assign({}, state, { +      keys: state.keys.concat([action.key]), +    }); +  case actions.FOLLOW_BACKSPACE: +    return Object.assign({}, state, { +      keys: state.keys.slice(0, -1), +    }); +  default: +    return state; +  } +} | 
