Open In New Tab

登録したCSSセレクタに一致するリンクを新しいタブで開く

目前為 2014-12-20 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Open In New Tab
// @namespace    https://greasyfork.org/users/1009-kengo321
// @version      2
// @description  登録したCSSセレクタに一致するリンクを新しいタブで開く
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_openInTab
// @match        *://*/*
// @license      MIT
// @noframes
// ==/UserScript==

;(function() {
  'use strict'

  var Config = (function() {

    function compareLinkSelector(o1, o2) {
      var u1 = o1.url || '', u2 = o2.url || ''
      if (u1 < u2) return -1
      if (u1 > u2) return 1
      return 0
    }
    function setComputedHeight(win, elem) {
      elem.style.height = win.getComputedStyle(elem).height
    }

    function Config(doc) {
      this.doc = doc
      this.linkSelectors = Config.getLinkSelectors().sort(compareLinkSelector)
      this.updateUrlList()
      ;[['url-list', 'change', [
          this.updateSelectorList.bind(this),
          this.updateCaptureCheckbox.bind(this),
          this.updateDisabled.bind(this),
        ]],
        ['selector-list', 'change', this.updateDisabled.bind(this)],
        ['url-add-button', 'click', [
          this.addUrl.bind(this),
          this.updateDisabled.bind(this),
          this.updateSelectorList.bind(this),
          this.updateCaptureCheckbox.bind(this),
        ]],
        ['url-edit-button', 'click', this.editUrl.bind(this)],
        ['url-remove-button', 'click', [
          this.removeUrl.bind(this),
          this.updateDisabled.bind(this),
          this.updateSelectorList.bind(this),
          this.updateCaptureCheckbox.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.removeSelector.bind(this),
          this.updateDisabled.bind(this),
        ]],
        ['capture-checkbox', 'change', this.updateCapture.bind(this)],
        ['ok-button', 'click', [
          this.saveLinkSelectors.bind(this),
          LinkSelector.updateCallback,
          this.removeIFrame.bind(this),
        ]],
        ['cancel-button', 'click', this.removeIFrame.bind(this)],
      ].forEach(function(e) {
        ;[].concat(e[2]).forEach(function(callback) {
          doc.getElementById(e[0]).addEventListener(e[1], callback)
        })
      })
    }
    Config.srcdoc = '\
      <!DOCTYPE html>\
      <html><head><style>\
        body {\
          width: 32em;\
          margin: 0;\
          padding: 5px;\
        }\
        p { margin: 0; }\
        #url-list { width: 100%; }\
        #selector-list { width: 100%; }\
        #confirm-p { text-align: right; }\
        p.description { font-size: smaller; }\
      </style></head><body>\
        <fieldset>\
          <legend>対象ページのURL(前方一致)</legend>\
          <p><select id=url-list multiple size=10></select></p>\
          <p>\
            <button id=url-add-button type=button>追加</button>\
            <button id=url-edit-button type=button disabled>編集</button>\
            <button id=url-remove-button type=button disabled>削除</button>\
          </p>\
        </fieldset>\
        <fieldset id=selector-fieldset disabled>\
          <legend>新しいタブで開くリンクのCSSセレクタ</legend>\
          <p class=description>\
            何も登録していないときは、すべてのリンクが対象になります\
          </p>\
          <p><select id=selector-list multiple size=5></select></p>\
          <p>\
            <button id=selector-add-button type=button>追加</button>\
            <button id=selector-edit-button type=button>編集</button>\
            <button id=selector-remove-button type=button>削除</button>\
          </p>\
          <p>\
            <label>\
              <input type=checkbox id=capture-checkbox>\
              キャプチャフェーズを使用して、イベント伝播を中断する\
            </label>\
          </p>\
          <p class=description>\
            正しく動作しないときは、これを有効にして試してください\
          </p>\
        </fieldset>\
        <p id=confirm-p>\
          <button id=ok-button type=button>OK</button>\
          <button id=cancel-button type=button>キャンセル</button>\
        </p>\
      </body></html>\
    '
    Config.show = function(done) {
      var f = document.createElement('iframe')
      f.srcdoc = Config.srcdoc
      f.addEventListener('load', function() {
        var config = new Config(f.contentDocument)
        config.setIFrame(f)
        if (typeof(done) === 'function') done(config)
      })
      f.style.display = 'none'
      document.body.appendChild(f)
    }
    Config.getLinkSelectors = function() {
      return JSON.parse(GM_getValue('linkSelectors', '[]'))
    }
    Config.prototype.newOption = function(text) {
      var result = this.doc.createElement('option')
      result.textContent = text
      return result
    }
    Config.prototype.updateUrlList = function() {
      this.linkSelectors.forEach(function(s) {
        this.doc.getElementById('url-list').add(this.newOption(s.url))
      }, this)
    }
    Config.prototype.updateSelectorList = function() {
      this.clearSelectorList()
      var len = this.doc.getElementById('url-list').selectedOptions.length
      if (len !== 1) return
      var i = this.doc.getElementById('url-list').selectedIndex
      ;(this.linkSelectors[i].selectors || []).forEach(function(s) {
        this.doc.getElementById('selector-list').add(this.newOption(s))
      }, this)
    }
    Config.prototype.clearSelectorList = function() {
      var s = this.doc.getElementById('selector-list')
      while (s.hasChildNodes()) s.removeChild(s.firstChild)
    }
    Config.prototype.addUrl = function() {
      var r = prompt('', document.location.href)
      if (!r) return
      this.linkSelectors.push({url: r})
      var l = this.doc.getElementById('url-list')
      var o = this.newOption(r)
      l.add(o)
      l.selectedIndex = o.index
    }
    Config.prototype.editUrl = function() {
      var urlList = this.doc.getElementById('url-list')
      if (urlList.selectedOptions.length !== 1) return
      var i = urlList.selectedIndex
      var r = prompt('', this.linkSelectors[i].url)
      if (!r) return
      urlList.options[i].textContent = r
      this.linkSelectors[i].url = r
    }
    Config.prototype.removeUrl = function() {
      var urlList = this.doc.getElementById('url-list')
      var selectedOptions = [].slice.call(urlList.selectedOptions)
      var selectedIndices = selectedOptions.map(function(o) { return o.index })
      this.linkSelectors = this.linkSelectors.filter(function(s, i) {
        return selectedIndices.indexOf(i) === -1
      })
      selectedIndices.reverse().forEach(function(i) { urlList.remove(i) })
    }
    Config.prototype.getErrorIfInvalidSelector = function(selector) {
      try {
        this.doc.querySelector(selector)
        return null
      } catch (e) {
        return e
      }
    }
    Config.prototype.promptUntilValidSelector = function(defaultValue) {
      var selector = defaultValue || ''
      var error = null
      do {
        selector = prompt((error || '').toString(), selector)
        if (!selector) return null
      } while (error = this.getErrorIfInvalidSelector(selector))
      return selector
    }
    Config.prototype.addSelector = function() {
      var urlList = this.doc.getElementById('url-list')
      if (urlList.selectedOptions.length !== 1) return
      var r = this.promptUntilValidSelector()
      if (!r) return
      var s = this.linkSelectors[urlList.selectedIndex]
      ;(s.selectors || (s.selectors = [])).push(r)
      var selectorList = this.doc.getElementById('selector-list')
      var o = this.newOption(r)
      selectorList.add(o)
      selectorList.selectedIndex = o.index
    }
    Config.prototype.editSelector = function() {
      var selectorList = this.doc.getElementById('selector-list')
      if (selectorList.selectedOptions.length !== 1) return

      var selectorIndex = selectorList.selectedIndex
      var selector = selectorList.options[selectorIndex].textContent
      var r = this.promptUntilValidSelector(selector)
      if (!r) return

      var urlIndex = this.doc.getElementById('url-list').selectedIndex
      var linkSelector = this.linkSelectors[urlIndex]
      linkSelector.selectors[selectorIndex] = r
      selectorList.options[selectorIndex].textContent = r
    }
    Config.prototype.removeSelector = function() {
      var selectorList = this.doc.getElementById('selector-list')
      var selectedOptions = [].slice.call(selectorList.selectedOptions)
      if (!selectedOptions.length) return
      var selectedIndices = selectedOptions.map(function(o) { return o.index })
      var urlIndex = this.doc.getElementById('url-list').selectedIndex
      var linkSelector = this.linkSelectors[urlIndex]
      linkSelector.selectors = linkSelector.selectors.filter(function(s, i) {
        return selectedIndices.indexOf(i) === -1
      })
      selectedIndices.reverse().forEach(function(i) { selectorList.remove(i) })
    }
    Config.prototype.updateCaptureCheckbox = function() {
      var checkbox = this.doc.getElementById('capture-checkbox')
      var i = this.doc.getElementById('url-list').selectedIndex
      checkbox.checked = (i === -1 ? false : this.linkSelectors[i].capture)
    }
    Config.prototype.updateCapture = function() {
      var i = this.doc.getElementById('url-list').selectedIndex
      if (i === -1) return
      var checkbox = this.doc.getElementById('capture-checkbox')
      this.linkSelectors[i].capture = checkbox.checked
    }
    Config.prototype.updateDisabled = function() {
      var urlList = this.doc.getElementById('url-list')
      var urlSelectedOptLen = urlList.selectedOptions.length
      var selectorList = this.doc.getElementById('selector-list')
      var selectorSelectedOptLen = selectorList.selectedOptions.length
      ;[['url-edit-button', urlSelectedOptLen !== 1],
        ['url-remove-button', urlSelectedOptLen === 0],
        ['selector-fieldset', urlSelectedOptLen !== 1],
        ['selector-edit-button', selectorSelectedOptLen !== 1],
        ['selector-remove-button', selectorSelectedOptLen === 0],
      ].forEach(function(e) {
        this.doc.getElementById(e[0]).disabled = e[1]
      }, this)
    }
    Config.prototype.setIFrame = function(iframe) {
      this.iframe = iframe
      var s = iframe.style
      s.display = ''
      s.backgroundColor = 'white'
      s.position = 'absolute'
      s.zIndex = '9999'
      s.borderWidth = 'medium'
      s.borderStyle = 'solid'
      s.borderColor = 'black'
      // Firefox34.0+Greasemonkey2.3
      // iframeのサイズをそのコンテンツのサイズに変更しても
      // スクロールバーが非表示にならない
      // 一度、十分大きなサイズに変更して、スクロールバーを非表示にしてから
      // コンテンツのサイズに変更することで、これに対処する
      var v = iframe.ownerDocument.defaultView
      iframe.width = this.doc.body.offsetWidth * 2
      iframe.height = this.doc.documentElement.offsetHeight * 2
      var w = iframe.width = this.doc.body.offsetWidth
      var h = iframe.height = this.doc.documentElement.offsetHeight
      s.top = Math.max(v.innerHeight - h, 0) / 2 + v.pageYOffset + 'px'
      s.left = Math.max(v.innerWidth - w, 0) / 2 + v.pageXOffset + 'px'
      // Chrome39.0.2171.95m+Tampermonkey3.9.131
      // select要素のoption数が0から1以上、または1以上から0へ変更されたとき
      // そのselect要素の高さが変更される
      // select要素の高さを明示することで、これを防ぐ
      var w = iframe.contentWindow
      setComputedHeight(w, this.doc.getElementById('url-list'))
      setComputedHeight(w, this.doc.getElementById('selector-list'))
    }
    Config.prototype.removeIFrame = function() {
      var f = this.iframe
      if (f && f.parentNode) f.parentNode.removeChild(f)
    }
    Config.prototype.saveLinkSelectors = function() {
      GM_setValue('linkSelectors', JSON.stringify(this.linkSelectors))
    }
    return Config
  })()

  var LinkSelector = (function() {

    function isLeftMouseButtonWithoutModifierKeys(mouseEvent) {
      var e = mouseEvent
      return !(e.button || e.altKey || e.shiftKey || e.ctrlKey || e.metaKey)
    }
    function isOpenableLink(elem) {
      return ['A', 'AREA'].indexOf(elem.tagName) >= 0
          && elem.href
          && elem.protocol !== 'javascript:'
    }
    function getAncestorOpenableLink(descendant) {
      for (var p = descendant.parentNode; p; p = p.parentNode) {
        if (isOpenableLink(p)) return p
      }
      return null
    }

    function LinkSelector(o) {
      o = o || {}
      this.url = o.url || ''
      this.selectors = o.selectors || []
      this.capture = !!o.capture
    }
    LinkSelector.getInstances = function() {
      return Config.getLinkSelectors().map(function(o) {
        return new LinkSelector(o)
      })
    }
    LinkSelector.getLocatedInstances = function() {
      return LinkSelector.getInstances().filter(function(o) {
        return o.matchUrlForward(document.location.href)
      })
    }
    LinkSelector.addCallbackIfRequired = function() {
      var i = LinkSelector.getLocatedInstances()
      if (i.some(function(o) { return !o.capture })) {
        document.addEventListener('click', LinkSelector.callback, false)
      }
      if (i.some(function(o) { return o.capture })) {
        document.addEventListener('click', LinkSelector.callback, true)
      }
    }
    LinkSelector.updateCallback = function() {
      document.removeEventListener('click', LinkSelector.callback, false)
      document.removeEventListener('click', LinkSelector.callback, true)
      LinkSelector.addCallbackIfRequired()
    }
    LinkSelector.callback = function(mouseEvent) {
      var e = mouseEvent
      if (!isLeftMouseButtonWithoutModifierKeys(e)) return
      var l = isOpenableLink(e.target) ? e.target
                                       : getAncestorOpenableLink(e.target)
      if (!l) return
      var o = LinkSelector.getLocatedInstances()
      for (var i = 0; i < o.length; i++) {
        if (!o[i].openInTabIfMatch(l, e.eventPhase)) continue
        e.preventDefault()
        if (e.eventPhase === Event.CAPTURING_PHASE) e.stopPropagation()
        return
      }
    }
    LinkSelector.prototype.matchUrlForward = function(url) {
      return url.indexOf(this.url) === 0
    }
    LinkSelector.prototype.matchEventPhase = function(eventPhase) {
      return this.capture ? eventPhase === Event.CAPTURING_PHASE
                          : eventPhase === Event.BUBBLING_PHASE
    }
    LinkSelector.prototype.matchLink = function(link) {
      return !this.selectors.length
          || this.selectors.some(function(s) { return link.matches(s) })
    }
    LinkSelector.prototype.openInTabIfMatch = function(link, eventPhase) {
      if (this.matchEventPhase(eventPhase) && this.matchLink(link)) {
        GM_openInTab(link.href)
        return true
      }
      return false
    }
    return LinkSelector
  })()

  function main() {
    GM_registerMenuCommand('Open In New Tab 設定', Config.show)
    LinkSelector.addCallbackIfRequired()
  }

  main()
})()