Greasy Fork is available in English.

Focus Navigator

タブキーによるフォーカス移動の対象と順番をCSSセレクタで設定できるようにする

// ==UserScript==
// @name        Focus Navigator
// @namespace   https://greasyfork.org/users/1009-kengo321
// @description タブキーによるフォーカス移動の対象と順番をCSSセレクタで設定できるようにする
// @match       *://*/*
// @version     5
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_registerMenuCommand
// @grant       GM_setClipboard
// @grant       GM.getValue
// @grant       GM.setValue
// @grant       GM.setClipboard
// @license     MIT License
// @noframes
// @run-at      document-start
// ==/UserScript==

;(function() {
  'use strict'

  const [gmGetValue, gmSetValue, gmSetClipboard] =
    typeof GM_getValue === 'undefined'
    ? [GM.getValue, GM.setValue, GM.setClipboard]
    : [GM_getValue, GM_setValue, GM_setClipboard]

  var Config = (function() {

    var byId = function(id) {
      return function() { return this._doc.getElementById(id) }
    }
    var addOption = function(selectElem) {
      return function(optionText) {
        var o = selectElem.ownerDocument.createElement('option')
        o.textContent = optionText
        selectElem.appendChild(o)
      }
    }
    var isValidSelector = function(selector) {
      try {
        document.querySelector(selector)
        return true
      } catch (e) {
        return false
      }
    }
    var matchForward = function(s1, s2) {
      return s1.indexOf(s2) === 0
    }
    var comparator = function(url) {
      return function(o1, o2) {
        var matched1 = matchForward(url, o1.url)
        var matched2 = matchForward(url, o2.url)
        if (matched1 && !matched2) return -1
        if (!matched1 && matched2) return 1
        if (o1.url < o2.url) return -1
        if (o1.url > o2.url) return 1
        return 0
      }
    }
    var maxZIndex = function() {
      return '2147483647'
    }
    var maxFrameWidth = function() {
      return 600
    }
    var selectedIndices = function(selectElem) {
      return [].map.call(selectElem.selectedOptions, function(o) {
        return o.index
      })
    }
    var removeAt = function(array, indices) {
      return array.filter(function(e, i) { return indices.indexOf(i) === -1 })
    }
    var removeSelectedOptions = function(selectElem) {
      ;[].slice.call(selectElem.selectedOptions).forEach(function(o) {
        o.parentNode.removeChild(o)
      })
    }
    var moveSelectedSelector = function(op) {
      return function() {
        var l = this._selectorList()
        var i = l.selectedIndex
        var t = l.options[i].textContent
        l.options[i].textContent = l.options[op(i)].textContent
        l.options[op(i)].textContent = t
        l.selectedIndex = op(i)
        var d = this._data[this._urlList().selectedIndex]
        var s = d.selectors[i]
        d.selectors[i] = d.selectors[op(i)]
        d.selectors[op(i)] = s
      }
    }

    var Config = function(doc) {
      this._doc = doc
      this._data = Config.focusSelectors().sort(comparator(this._topUrl()))
      this._updateUrlList()
      this._styleMatchedUrl()
      this._addAllCallbacks()
      this._urlList().focus()
    }
    Config.prototype._urlList = byId('url-list')
    Config.prototype._urlEditButton = byId('url-edit-button')
    Config.prototype._urlRemoveButton = byId('url-remove-button')
    Config.prototype._selectorList = byId('selector-list')
    Config.prototype._selectorAddButton = byId('selector-add-button')
    Config.prototype._selectorEditButton = byId('selector-edit-button')
    Config.prototype._selectorRemoveButton = byId('selector-remove-button')
    Config.prototype._selectorUpButton = byId('selector-up-button')
    Config.prototype._selectorDownButton = byId('selector-down-button')
    Config.prototype._scrollPositionFieldSet = byId('scroll-position-fieldset')
    Config.prototype._coordinateCheckbox = byId('coordinate-checkbox')
    Config.prototype._lowerSpaceInput = byId('lower-space-input')
    Config.prototype._upperSpaceInput = byId('upper-space-input')
    Config.prototype._customCssTextArea = byId('custom-css-textarea')
    Config.prototype._frame = function() {
      return this._doc.defaultView.frameElement
    }
    Config.prototype._topUrl = function() {
      return this._frame().ownerDocument.location.href
    }
    Config.prototype._updateUrlList = function() {
      this._urlList().length = 0
      this._data.map(function(s) { return s.url })
        .forEach(addOption(this._urlList()))
    }
    Config.prototype._styleMatchedUrl = function() {
      var url = this._topUrl()
      this._data.map(function(d) {
        return matchForward(url, d.url)
      }).forEach(function(matched, i) {
        this._urlList().options[i]
          .classList[matched ? 'add' : 'remove']('matched')
      }, this)
    }
    Config.prototype._setComputedHeight = function(elem) {
      elem.style.height = this._doc.defaultView.getComputedStyle(elem).height
    }
    Config.prototype._addCallbacks = function(id, type, callbacks) {
      var target = this._doc.getElementById(id)
      callbacks.forEach(function(callback) {
        target.addEventListener(type, callback)
      })
    }
    Config.prototype._addAllCallbacks = function() {
      ;[['url-list', 'change', [
          this.updateValueBySelectedUrl.bind(this),
          this.updateDisabled.bind(this)
        ]],
        ['selector-list', 'change', [this.updateDisabled.bind(this)]],
        ['url-add-button', 'click', [
          this.addUrl.bind(this),
          this.updateValueBySelectedUrl.bind(this),
          this.updateDisabled.bind(this),
        ]],
        ['url-edit-button', 'click', [this.editUrl.bind(this)]],
        ['url-remove-button', 'click', [
          this.removeSelectedUrls.bind(this),
          this.updateValueBySelectedUrl.bind(this),
          this.updateDisabled.bind(this),
        ]],
        ['selector-add-button', 'click', [
          this.addSelector.bind(this),
          this.updateDisabled.bind(this),
        ]],
        ['selector-edit-button', 'click', [
          this.editSelector.bind(this),
        ]],
        ['selector-remove-button', 'click', [
          this.removeSelectedSelectors.bind(this),
          this.updateDisabled.bind(this),
        ]],
        ['selector-up-button', 'click', [
          this.upSelectedSelector.bind(this),
          this.updateDisabled.bind(this),
        ]],
        ['selector-down-button', 'click', [
          this.downSelectedSelector.bind(this),
          this.updateDisabled.bind(this),
        ]],
        ['coordinate-checkbox', 'change', [
          this.coordinateCheckboxChanged.bind(this),
        ]],
        ['lower-space-input', 'input', [
          this.lowerSpaceInputChanged.bind(this),
        ]],
        ['upper-space-input', 'input', [
          this.upperSpaceInputChanged.bind(this),
        ]],
        ['custom-css-textarea', 'input', [
          this.cssTextAreaChanged.bind(this),
        ]],
        ['import-export-checkbox', 'change', [
          this.impExpCheckboxChanged.bind(this),
        ]],
        ['export-button', 'click', [this.export.bind(this)]],
        ['import-button', 'click', [
          this.import.bind(this),
          this.updateValueBySelectedUrl.bind(this),
          this.updateDisabled.bind(this),
        ]],
        ['ok-button', 'click', [
          this.save.bind(this),
          FocusSelector.update.bind(null, this._frame().ownerDocument),
          this.removeFrameFromParent.bind(this),
        ]],
        ['cancel-button', 'click', [this.removeFrameFromParent.bind(this)]],
      ].forEach(this._addCallbacks.apply.bind(this._addCallbacks, this))
    }
    Config.prototype._updateSelectorList = function() {
      this._selectorList().length = 0
      if (this._urlList().selectedOptions.length !== 1) return
      var i = this._urlList().selectedIndex
      ;(this._data[i].selectors || []).forEach(addOption(this._selectorList()))
    }
    Config.prototype._updateScrollPositionElements = function() {
      if (this._urlList().selectedOptions.length === 1) {
        var d = this._data[this._urlList().selectedIndex]
        this._coordinateCheckbox().checked = d.coordinated
        this._lowerSpaceInput().value = d.lowerSpace || 0
        this._upperSpaceInput().value = d.upperSpace || 0
      } else {
        this._coordinateCheckbox().checked = false
        this._lowerSpaceInput().value = 0
        this._upperSpaceInput().value = 0
      }
    }
    Config.prototype._updateCustomCssTextArea = function() {
      if (this._urlList().selectedOptions.length === 1) {
        var d = this._data[this._urlList().selectedIndex]
        this._customCssTextArea().value = d.cssText || ''
      } else {
        this._customCssTextArea().value = ''
      }
    }
    Config.prototype.updateValueBySelectedUrl = function() {
      this._updateSelectorList()
      this._updateScrollPositionElements()
      this._updateCustomCssTextArea()
    }
    Config.prototype._updateDisabledBySelectedUrl = function() {
      var len = this._urlList().selectedOptions.length
      this._urlEditButton().disabled = len !== 1
      this._urlRemoveButton().disabled = len === 0
      this._selectorList().disabled = len !== 1
      this._selectorAddButton().disabled = len !== 1
      this._coordinateCheckbox().disabled = len !== 1
      this._scrollPositionFieldSet().disabled =
        !(len === 1 && this._coordinateCheckbox().checked)
      this._customCssTextArea().disabled = len !== 1
    }
    Config.prototype._updateDisabledBySelectedSelector = function() {
      var list = this._selectorList()
      var len = list.selectedOptions.length
      this._selectorEditButton().disabled = len !== 1
      this._selectorRemoveButton().disabled = len === 0
      var top = list.selectedIndex === 0
      this._selectorUpButton().disabled = len !== 1 || top
      var last = list.selectedIndex === list.length - 1
      this._selectorDownButton().disabled = len !== 1 || last
    }
    Config.prototype.updateDisabled = function() {
      this._updateDisabledBySelectedUrl()
      this._updateDisabledBySelectedSelector()
    }
    Config.prototype.addUrl = function() {
      var url = ''
      do {
        url = prompt(url ? '"' + url + '"は登録済みです。' : ''
                   , url || this._topUrl())
        if (url === null) return
      } while (this._data.some(function(d) { return d.url === url}))
      this._data.push({url: url})
      addOption(this._urlList())(url)
      this._urlList().selectedIndex = this._urlList().length - 1
      this._styleMatchedUrl()
    }
    Config.prototype.editUrl = function() {
      var i = this._urlList().selectedIndex
      var data  = this._data[i]
      var url = ''
      do {
        url = prompt(url ? '"' + url + '"は登録済みです。' : ''
                   , url || data.url)
        if (url === null) return
      } while (this._data.some(function(d) { return d.url === url}))
      data.url = url
      this._urlList().options[i].textContent = url
      this._styleMatchedUrl()
    }
    Config.prototype.save = function() {
      gmSetValue('focusSelectors', JSON.stringify(this._data))
      Config.focusSelectors.set(this._data)
    }
    Config.prototype.removeSelectedUrls = function() {
      this._data = removeAt(this._data, selectedIndices(this._urlList()))
      removeSelectedOptions(this._urlList())
    }
    Config.prototype.addSelector = function() {
      var selector = ''
      do {
        selector = prompt(selector ? '構文エラー' : '', selector)
        if (selector === null) return
      } while (!isValidSelector(selector))
      addOption(this._selectorList())(selector)
      var d = this._data[this._urlList().selectedIndex]
      d.selectors = (d.selectors || []).concat(selector)
      this._selectorList().selectedIndex = this._selectorList().length - 1
    }
    Config.prototype.editSelector = function() {
      var d = this._data[this._urlList().selectedIndex]
      var i = this._selectorList().selectedIndex
      var current = d.selectors[i]
      var selector = ''
      do {
        selector = prompt(selector ? '構文エラー' : '', selector || current)
        if (selector === null) return
      } while (!isValidSelector(selector))
      d.selectors[i] = selector
      this._selectorList().options[i].textContent = selector
    }
    Config.prototype.removeSelectedSelectors = function() {
      var d = this._data[this._urlList().selectedIndex]
      d.selectors = removeAt(d.selectors
                           , selectedIndices(this._selectorList()))
      removeSelectedOptions(this._selectorList())
    }
    Config.prototype.upSelectedSelector =
      moveSelectedSelector(function(v) { return v - 1 })
    Config.prototype.downSelectedSelector =
      moveSelectedSelector(function(v) { return v + 1 })
    Config.prototype.coordinateCheckboxChanged = function() {
      var checked = this._coordinateCheckbox().checked
      this._data[this._urlList().selectedIndex].coordinated = checked
      this._scrollPositionFieldSet().disabled = !checked
    }
    Config.prototype.lowerSpaceInputChanged = function() {
      if (this._lowerSpaceInput().validity.valid) {
        this._data[this._urlList().selectedIndex].lowerSpace =
          this._lowerSpaceInput().valueAsNumber
      }
    }
    Config.prototype.upperSpaceInputChanged = function() {
      if (this._upperSpaceInput().validity.valid) {
        this._data[this._urlList().selectedIndex].upperSpace =
          this._upperSpaceInput().valueAsNumber
      }
    }
    Config.prototype.cssTextAreaChanged = function() {
      this._data[this._urlList().selectedIndex].cssText =
        this._customCssTextArea().value.trim()
    }
    Config.prototype.removeFrameFromParent = function() {
      this._frame().parentNode.removeChild(this._frame())
      var b = this.background
      if (b && b.parentNode) b.parentNode.removeChild(b)
    }
    Config.prototype.impExpCheckboxChanged = function() {
      var checkbox = this._doc.getElementById('import-export-checkbox')
      var container = this._doc.getElementById('import-export-container')
      container.classList[checkbox.checked ? 'add' : 'remove']('show')
    }
    Config.prototype.export = function() {
      gmSetClipboard(JSON.stringify(this._data))
    }
    Config.prototype.import = function() {
      try {
        var ta = this._doc.getElementById('import-textarea')
        this._data = JSON.parse(ta.value)
        this._updateUrlList()
        this._styleMatchedUrl()
        ta.setCustomValidity('')
      } catch (e) {
        ta.setCustomValidity(e.toString())
      }
    }
    Config.focusSelectors = function() {
      return Config._focusSelectors
    }
    Config.focusSelectors.set = function(focusSelectors) {
      Config._focusSelectors = focusSelectors
    }
    Config.show = function(doc) {
      var background = doc.createElement('div')
      background.style.backgroundColor = 'black'
      background.style.opacity = '0.5'
      background.style.zIndex = maxZIndex() - 1
      background.style.position = 'fixed'
      background.style.top = '0'
      background.style.left = '0'
      background.style.width = '100%'
      background.style.height = '100%'
      doc.body.appendChild(background)
      var f = doc.createElement('iframe')
      f.style.position = 'fixed'
      f.style.top = '0'
      f.style.left = '0'
      f.style.width = '100%'
      f.style.height = '100%'
      f.style.zIndex = maxZIndex()
      f.srcdoc = Config.srcdoc
      f.addEventListener('load', async function() {
        Config.focusSelectors.set(JSON.parse(await gmGetValue('focusSelectors', '[]')))
        var config = new Config(f.contentDocument)
        config.background = background
      })
      doc.body.appendChild(f)
    }
    Config.srcdoc = [
      '<!doctype html><html><head><style>',
      '  html {',
      '    margin: 0 auto;',
      '    max-width: 50em;',
      '    height: 100%;',
      '    line-height: 1.5em;',
      '  }',
      '  body {',
      '    height: 100%;',
      '    margin: 0;',
      '    display: flex;',
      '    flex-direction: column;',
      '    justify-content: center;',
      '  }',
      '  #dialog {',
      '    overflow: auto;',
      '    padding: 8px;',
      '    background-color: white;',
      '  }',
      '  p { margin: 0; }',
      '  select { width: 100%; }',
      '  textarea { width: 100%; }',
      '  .label-p, #scroll-position-fieldset, #ok-cancel-p {',
      '    margin-top: 10px;',
      '  }',
      '  #custom-css-desc { line-height: 1.1em; }',
      '  #ok-cancel-p { text-align: right; }',
      '  #url-list .matched { text-decoration: underline; }',
      '  #import-export-container { display: none; }',
      '  #import-export-container.show { display: block; }',
      '</style></head><body><div id=dialog>',
      '<p><label for=url-list>対象ページのURL一覧(前方一致):</label></p>',
      '<p><select id=url-list size=10 multiple></select></p>',
      '<p>',
      '  <input id=url-add-button type=button value=追加>',
      '  <input id=url-edit-button type=button value=編集 disabled>',
      '  <input id=url-remove-button type=button value=削除 disabled>',
      '</p>',
      '<p><small>',
      '  複数一致するときは、一番長いURLの設定を使用します。',
      '</small></p>',
      '<p class=label-p><label for=selector-list>',
      '  フォーカス対象と移動順のCSSセレクタ一覧:',
      '</label></p>',
      '<p><select id=selector-list size=5 multiple disabled></select></p>',
      '<p>',
      '  <input id=selector-add-button type=button value=追加 disabled>',
      '  <input id=selector-edit-button type=button value=編集 disabled>',
      '  <input id=selector-remove-button type=button value=削除 disabled>',
      '  <input id=selector-up-button type=button value=上へ disabled>',
      '  <input id=selector-down-button type=button value=下へ disabled>',
      '</p>',
      '<fieldset id=scroll-position-fieldset disabled>',
      '  <legend><label>',
      '    <input id=coordinate-checkbox type=checkbox disabled>',
      '    スクロールの位置を調整する',
      '  </label></legend>',
      '  <p><label>',
      '    表示領域の上とフォーカスとの最小間隔:',
      '    <input id=upper-space-input type=number value=0 required>',
      '  </label></p>',
      '  <p><label>',
      '    表示領域の下とフォーカスとの最小間隔:',
      '    <input id=lower-space-input type=number value=0 required>',
      '  </label></p>',
      '</fieldset>',
      '<p class=label-p>',
      '  <label for=custom-css-textarea>カスタムCSS:</label>',
      '</p>',
      '<p><textarea id=custom-css-textarea rows=3 disabled></textarea></p>',
      '<p id=custom-css-desc><small>',
      '  この入力欄の内容を持つstyle要素をhead要素の末尾に追加します。何も入力していないときは追加しません。フォーカスのアウトラインが非表示のときに利用してください。',
      '</small></p>',
      '<p class=label-p>',
      '  インポート・エクスポート:',
      '  <small>',
      '    <label><input id=import-export-checkbox type=checkbox>表示</label>',
      '  </small>',
      '</p>',
      '<div id=import-export-container>',
      '  <p><textarea id=import-textarea rows=2></textarea></p>',
      '  <p><input id=import-button type=button value=インポート></p>',
      '  <p><input id=export-button type=button',
      '    value=クリップボードへエクスポート></p>',
      '</div>',
      '<p id=ok-cancel-p>',
      '  <input id=ok-button type=button value=OK>',
      '  <input id=cancel-button type=button value=キャンセル>',
      '</p>',
      '</div></body></html>',
    ].join('\n')
    return Config
  })()

  var FocusSelector = (function() {

    var tabKeyCode = 9
    var isTabKey = function(e) {
      return e.which === tabKeyCode && !(e.ctrlKey || e.metaKey || e.altKey)
    }
    var ring = function(array, start, reverse) {
      var a = array.slice(start).concat(array.slice(0, start))
      return reverse ? a.reverse() : a
    }
    var matchedFocusSelectors = function(doc) {
      return Config.focusSelectors()
        .map(FocusSelector.new)
        .filter(function(focusSelector) {
          return focusSelector._matchUrlForward(doc.location.href)
        })
    }
    var longestUrlFocusSelector = function(doc) {
      var s = matchedFocusSelectors(doc)
      if (!s.length) return
      return s.reduce(function(previous, current) {
        return previous._hasLongerUrl(current) ? previous : current
      })
    }
    var focusNext = function(requestFocus) {
      return function(keyDownEvent) {
        if (!isTabKey(keyDownEvent)) return

        var d = keyDownEvent.target.ownerDocument
        var focusSelector = longestUrlFocusSelector(d)
        if (!focusSelector) return
        var selected = focusSelector._querySelectors(d)
        if (!selected.length) return

        var i = selected.indexOf(d.activeElement)
        var start = keyDownEvent.shiftKey ? Math.max(i, 0)
                                          : (i + 1) % selected.length
        var r = ring(selected, start, keyDownEvent.shiftKey)
        requestFocus(keyDownEvent
                   , r.some.bind(r, focusSelector._focus.bind(focusSelector)))
      }
    }

    var FocusSelector = function(o) {
      this._url = o.url
      this._selectors = o.selectors || []
      this._coordinated = Boolean(o.coordinated)
      this._lowerSpace = o.lowerSpace || 0
      this._upperSpace = o.upperSpace || 0
      this._cssText = o.cssText
    }
    FocusSelector.prototype._matchUrlForward = function(url) {
      return url.indexOf(this._url) === 0
    }
    FocusSelector.prototype._hasLongerUrl = function(other) {
      return this._url.length >= other._url.length
    }
    FocusSelector.prototype._querySelectors = function(doc) {
      return this._selectors.reduce(function(selected, selector) {
        return selected.concat.apply(selected, doc.querySelectorAll(selector))
      }, [])
    }
    FocusSelector.prototype._focus = function(elem) {
      var preFocus = elem.getBoundingClientRect()
      elem.focus()
      var result = elem.ownerDocument.activeElement === elem
      if (result && this._coordinated) {
        this._coordinateScroll(elem, preFocus)
      }
      return result
    }
    FocusSelector.prototype._coordinateScroll = function(elem, preFocus) {
      var win = elem.ownerDocument.defaultView
      var postFocus = elem.getBoundingClientRect()
      if (preFocus.bottom > win.innerHeight - this._lowerSpace) {
        var y = win.innerHeight - postFocus.bottom - this._lowerSpace
        win.scrollBy(0, -y)
      } else if (preFocus.top < this._upperSpace) {
        win.scrollBy(0, postFocus.top - this._upperSpace)
      }
    }
    FocusSelector.prototype._hasCssText = function() {
      return Boolean(this._cssText)
    }
    FocusSelector.prototype._createStyleElem = function(doc) {
      var result = doc.createElement('style')
      result.id = 'focus-navigator-style'
      result.textContent = this._cssText
      return result
    }
    FocusSelector.new = function(o) {
      return new FocusSelector(o)
    }
    FocusSelector.addCallbackIfRequired = function(doc) {
      if (matchedFocusSelectors(doc).length) {
        doc.addEventListener('keydown', FocusSelector.firstCallback)
      }
    }
    // Firefox36.0 + Greasemonkey2.3
    // タブキーの keydown イベントをすべてキャンセルして
    // デフォルトの処理を一度も実行させなかった場合、
    // フォーカスされた要素のアウトラインが表示されない。
    // これの対策として、フォーカス処理をあとのイベントループで実行させて、
    // 最初のイベントだけキャンセルせずにデフォルトの処理をさせることで、
    // アウトラインを表示。
    FocusSelector.firstCallback = focusNext(function(event, focus) {
      setTimeout(focus, 0)
      var d = event.target.ownerDocument
      d.removeEventListener('keydown', FocusSelector.firstCallback)
      d.addEventListener('keydown', FocusSelector.callback)
    })
    FocusSelector.callback = focusNext(function(event, focus) {
      if (focus()) event.preventDefault()
    })
    FocusSelector.addStyleElemIfRequired = function(doc) {
      var s = longestUrlFocusSelector(doc)
      if (s && s._hasCssText()) {
        if (doc.head) {
          doc.head.appendChild(s._createStyleElem(doc))
        } else {
          doc.addEventListener('DOMContentLoaded', () => {
            doc.head.appendChild(s._createStyleElem(doc))
          })
        }
      }
    }
    FocusSelector.update = function(doc) {
      doc.removeEventListener('keydown', FocusSelector.firstCallback)
      doc.removeEventListener('keydown', FocusSelector.callback)
      var style = doc.getElementById('focus-navigator-style')
      if (style) style.parentNode.removeChild(style)
      FocusSelector.addCallbackIfRequired(doc)
      FocusSelector.addStyleElemIfRequired(doc)
    }
    return FocusSelector
  })()

  function addConfigButtonIfScriptPage() {
    if (!location.href.startsWith('https://greasyfork.org/ja/scripts/8736-focus-navigator'))
      return
    const add = () => {
      const e = document.createElement('button')
      e.type = 'button'
      e.textContent = '設定'
      e.addEventListener('click', Config.show.bind(Config, document))
      document.querySelector('#script-info > header > h2').appendChild(e)
    }
    if (['interactive', 'complete'].includes(document.readyState))
      add()
    else
      document.addEventListener('DOMContentLoaded', add)
  }

  async function main() {
    Config.focusSelectors.set(JSON.parse(await gmGetValue('focusSelectors', '[]')))
    FocusSelector.addCallbackIfRequired(document)
    FocusSelector.addStyleElemIfRequired(document)
    if (typeof GM_registerMenuCommand !== 'undefined') {
      GM_registerMenuCommand('Focus Navigator 設定'
                           , Config.show.bind(Config, document))
    }
    addConfigButtonIfScriptPage()
  }

  main()
})()