Open In New Tab

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

Versión del día 20/12/2014. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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