Open In New Tab

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

As of 2014-12-20. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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()
})()