Open In New Tab

新しいタブで開くリンクをCSSセレクタで選べるようにする

Stan na 10-04-2015. Zobacz najnowsza wersja.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

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.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         Open In New Tab
// @namespace    https://greasyfork.org/users/1009-kengo321
// @version      5
// @description  新しいタブで開くリンクをCSSセレクタで選べるようにする
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_openInTab
// @grant        GM_setClipboard
// @grant        GM_info
// @match        *://*/*
// @license      MIT
// @noframes
// ==/UserScript==

;(function() {
  'use strict'

  if (window.self !== window.top) return

  function getter(propName) {
    return function(o) { return o[propName] }
  }
  function not(func) {
    return function() { return !func.apply(null, arguments) }
  }
  function invoker(methodName) {
    var args = [].concat.apply([], arguments).slice(1)
    return function(o) { return o[methodName].apply(o, args) }
  }
  function isTampermonkey() {
    return GM_info.scriptHandler === 'Tampermonkey'
  }

  var Config = (function() {

    function compareLinkSelector(o1, o2) {
      var m1 = o1.matchUrlForward(), m2 = o2.matchUrlForward()
      if (m1 && !m2) return -1
      if (!m1 && m2) return 1
      if (o1.url < o2.url) return -1
      if (o1.url > o2.url) return 1
      return 0
    }
    function setComputedHeight(win, elem) {
      elem.style.height = win.getComputedStyle(elem).height
    }
    function updateUrlOptionClass(option, linkSelector) {
      var p = linkSelector.matchUrlForward() ? 'add' : 'remove'
      option.classList[p]('matched')
      return option
    }
    function addAndSelectOption(selectElem, option) {
      selectElem.add(option)
      selectElem.selectedIndex = option.index
    }
    function getSelectedIndices(selectElem) {
      return [].map.call(selectElem.selectedOptions, getter('index'))
    }
    function removeSelectedOptions(selectElem) {
      ;[].slice.call(selectElem.selectedOptions).forEach(function(o) {
        o.parentNode.removeChild(o)
      })
    }
    function filterIndices(array, indices) {
      return array.filter(function(e, i) { return indices.indexOf(i) === -1 })
    }
    function optionAdder(selectElem, newOption) {
      return function(e) { selectElem.add(newOption(e)) }
    }
    function maxZIndex() {
      return '2147483647'
    }

    function Config(doc) {
      this.doc = doc
      this.linkSelectors = Config.getLinkSelectors().sort(compareLinkSelector)
      this.setTampermonkeySettingVisible()
      this.updateUrlList()
      this.getActiveCheckbox().checked = Config.isNewTabActivation()
      this.getInsertCheckbox().checked = Config.isNewTabInsertion()
      this.doc.getElementById('export-to-clipboard-button').hidden =
        typeof(GM_setClipboard) === 'undefined'
      this.addCallbacks()
    }
    Config.srcdoc = [
      '<!DOCTYPE html>',
      '<html><head><style>',
      '  p { margin: 0; }',
      '  textarea { width: 100%; }',
      '  #url-list { width: 100%; }',
      '  #url-list option.matched { text-decoration: underline; }',
      '  #selector-list { width: 100%; }',
      '  #confirm-p { text-align: right; }',
      '  p.description { font-size: smaller; }',
      '  #import-export-container { display: none; }',
      '  #import-export-container.show { display: block; }',
      '</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=active-p><label>',
      '  <input id=active-checkbox type=checkbox>',
      '  新しいタブを開いたとき、すぐにそのタブに切り替える',
      '</label></p>',
      '<p id=insert-p><label>',
      '  <input id=insert-checkbox type=checkbox>',
      '  現在のタブの後ろに新しいタブを挿入する',
      '</label></p>',
      '<p>',
      '  インポート・エクスポート:',
      '  <small>',
      '    <label><input id=import-export-checkbox type=checkbox>表示</label>',
      '  </small>',
      '</p>',
      '<div id=import-export-container>',
      '  <p><textarea id=import-export-textarea rows=2></textarea></p>',
      '  <p>',
      '    <input id=import-button type=button value=インポート>',
      '    <input id=export-button type=button value=エクスポート>',
      '  </p>',
      '  <p><input id=export-to-clipboard-button type=button',
      '    value=クリップボードへエクスポート></p>',
      '</div>',
      '<p id=confirm-p>',
      '  <button id=ok-button type=button>OK</button>',
      '  <button id=cancel-button type=button>キャンセル</button>',
      '</p>',
      '</body></html>',
    ].join('\n')
    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', '[]')).map(function(o) {
        return new LinkSelector(o)
      })
    }
    Config.isNewTabActivation = function() {
      return GM_getValue('active', true)
    }
    Config.isNewTabInsertion = function() {
      return GM_getValue('insert', false)
    }
    Config.prototype.addCallbacks = function() {
      var doc = this.doc
      ;[['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.save.bind(this),
          LinkSelector.updateCallback,
          this.removeIFrame.bind(this),
        ]],
        ['cancel-button', 'click', this.removeIFrame.bind(this)],
        [ 'import-export-checkbox',
          'change',
          this.importExportCheckboxChanged.bind(this),
        ],
        [ 'export-to-clipboard-button',
          'click',
          this.export2clipboard.bind(this),
        ],
        ['import-button', 'click', [
          this.import.bind(this),
          this.updateSelectorList.bind(this),
          this.updateCaptureCheckbox.bind(this),
          this.updateDisabled.bind(this),
        ]],
        ['export-button', 'click', this.export.bind(this)],
      ].forEach(function(e) {
        ;[].concat(e[2]).forEach(function(callback) {
          doc.getElementById(e[0]).addEventListener(e[1], callback)
        })
      })
    }
    Config.prototype.getUrlList = function() {
      return this.doc.getElementById('url-list')
    }
    Config.prototype.getSelectorList = function() {
      return this.doc.getElementById('selector-list')
    }
    Config.prototype.getCaptureCheckbox = function() {
      return this.doc.getElementById('capture-checkbox')
    }
    Config.prototype.getActiveCheckbox = function() {
      return this.doc.getElementById('active-checkbox')
    }
    Config.prototype.getInsertCheckbox = function() {
      return this.doc.getElementById('insert-checkbox')
    }
    Config.prototype.getImpExpTextarea = function() {
      return this.doc.getElementById('import-export-textarea')
    }
    Config.prototype.setTampermonkeySettingVisible = function() {
      this.doc.getElementById('active-p').hidden = !isTampermonkey()
      this.doc.getElementById('insert-p').hidden = !isTampermonkey()
    }
    Config.prototype.newOption = function(text) {
      var result = this.doc.createElement('option')
      result.textContent = text
      return result
    }
    Config.prototype.newUrlOption = function(linkSelector) {
      return updateUrlOptionClass(this.newOption(linkSelector.url)
                                , linkSelector)
    }
    Config.prototype.updateUrlList = function() {
      this.getUrlList().length = 0
      this.linkSelectors.forEach(optionAdder(this.getUrlList()
                                           , this.newUrlOption.bind(this)))
    }
    Config.prototype.getSelectedLinkSelector = function() {
      return this.linkSelectors[this.getUrlList().selectedIndex]
    }
    Config.prototype.updateSelectorList = function() {
      this.clearSelectorList()
      if (this.getUrlList().selectedOptions.length !== 1) return
      this.getSelectedLinkSelector()
        .selectors
        .forEach(optionAdder(this.getSelectorList()
                           , this.newOption.bind(this)))
    }
    Config.prototype.clearSelectorList = function() {
      var s = this.getSelectorList()
      while (s.hasChildNodes()) s.removeChild(s.firstChild)
    }
    Config.prototype.addUrl = function() {
      var r = prompt('', document.location.href)
      if (!r) return
      var s = new LinkSelector({url: r})
      this.linkSelectors.push(s)
      addAndSelectOption(this.getUrlList(), this.newUrlOption(s))
    }
    Config.prototype.editUrl = function() {
      if (this.getUrlList().selectedOptions.length !== 1) return
      var r = prompt('', this.getSelectedLinkSelector().url)
      if (!r) return
      this.getUrlList().selectedOptions[0].textContent = r
      this.getSelectedLinkSelector().url = r
      updateUrlOptionClass(this.getUrlList().selectedOptions[0]
                         , this.getSelectedLinkSelector())
    }
    Config.prototype.removeUrl = function() {
      this.linkSelectors = filterIndices(this.linkSelectors
                                       , getSelectedIndices(this.getUrlList()))
      removeSelectedOptions(this.getUrlList())
    }
    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() {
      if (this.getUrlList().selectedOptions.length !== 1) return
      var r = this.promptUntilValidSelector()
      if (!r) return
      this.getSelectedLinkSelector().selectors.push(r)
      addAndSelectOption(this.getSelectorList(), this.newOption(r))
    }
    Config.prototype.getSelectedSelector = function() {
      return this.getSelectorList().selectedOptions[0].textContent
    }
    Config.prototype.setSelectedSelector = function(selector) {
      var o = this.getSelectorList().selectedOptions[0]
      o.textContent = selector
      this.getSelectedLinkSelector().selectors[o.index] = selector
    }
    Config.prototype.editSelector = function() {
      if (this.getSelectorList().selectedOptions.length !== 1) return
      var r = this.promptUntilValidSelector(this.getSelectedSelector())
      if (!r) return
      this.setSelectedSelector(r)
    }
    Config.prototype.removeSelector = function() {
      var s = this.getSelectedLinkSelector()
      s.selectors = filterIndices(s.selectors
                                , getSelectedIndices(this.getSelectorList()))
      removeSelectedOptions(this.getSelectorList())
    }
    Config.prototype.updateCaptureCheckbox = function() {
      var s = this.getSelectedLinkSelector()
      this.getCaptureCheckbox().checked = (s ? s.capture : false)
    }
    Config.prototype.updateCapture = function() {
      var s = this.getSelectedLinkSelector()
      if (s) s.capture = this.getCaptureCheckbox().checked
    }
    Config.prototype.updateDisabled = function() {
      var selectedUrlNum = this.getUrlList().selectedOptions.length
      var selectedSelectorNum = this.getSelectorList().selectedOptions.length
      ;[['url-edit-button', selectedUrlNum !== 1],
        ['url-remove-button', selectedUrlNum === 0],
        ['selector-fieldset', selectedUrlNum !== 1],
        ['selector-edit-button', selectedSelectorNum !== 1],
        ['selector-remove-button', selectedSelectorNum === 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 = maxZIndex()
      s.borderWidth = 'medium'
      s.borderStyle = 'solid'
      s.borderColor = 'black'
      s.boxSizing = 'content-box'
      var v = iframe.ownerDocument.defaultView
      var maxWidth = 600
      var w = iframe.width = Math.min(maxWidth, v.innerWidth)
      var h = iframe.height = this.doc.documentElement.offsetHeight
      s.top = v.pageYOffset + 'px'
      s.left = (v.innerWidth - w) / 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.getUrlList())
      setComputedHeight(w, this.getSelectorList())
      this.getUrlList().focus()
    }
    Config.prototype.removeIFrame = function() {
      var f = this.iframe
      if (f && f.parentNode) f.parentNode.removeChild(f)
    }
    Config.prototype.save = function() {
      GM_setValue('linkSelectors', JSON.stringify(this.linkSelectors))
      if (isTampermonkey()) {
        GM_setValue('active', this.getActiveCheckbox().checked)
        GM_setValue('insert', this.getInsertCheckbox().checked)
      }
    }
    Config.prototype.importExportCheckboxChanged = function() {
      var checkbox = this.doc.getElementById('import-export-checkbox')
      var container = this.doc.getElementById('import-export-container')
      container.classList[checkbox.checked ? 'add' : 'remove']('show')
      this.iframe.height = this.doc.documentElement.offsetHeight
    }
    Config.prototype.export2clipboard = function() {
      GM_setClipboard(JSON.stringify(this.linkSelectors))
    }
    Config.prototype.import = function() {
      try {
        var ta = this.getImpExpTextarea()
        this.linkSelectors = JSON.parse(ta.value).map(function(o) {
          return new LinkSelector(o)
        })
        this.updateUrlList()
        ta.setCustomValidity('')
      } catch (e) {
        ta.setCustomValidity(e.toString())
      }
    }
    Config.prototype.export = function() {
      this.getImpExpTextarea().value = 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 openInTab(url) {
      if (isTampermonkey()) {
        GM_openInTab(url, {
          active: Config.isNewTabActivation(),
          insert: Config.isNewTabInsertion(),
        })
      } else {
        GM_openInTab(url)
      }
    }

    function LinkSelector(o) {
      o = o || {}
      this.url = o.url || ''
      this.selectors = o.selectors || []
      this.capture = !!o.capture
    }
    LinkSelector.getLocatedInstances = function() {
      return Config.getLinkSelectors().filter(invoker('matchUrlForward'))
    }
    LinkSelector.addCallbackIfRequired = function() {
      var i = LinkSelector.getLocatedInstances()
      if (i.some(not(getter('capture')))) {
        document.addEventListener('click', LinkSelector.callback, false)
      }
      if (i.some(getter('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 link = isOpenableLink(e.target) ? e.target
                                          : getAncestorOpenableLink(e.target)
      if (!link) return

      var opened = LinkSelector.getLocatedInstances()
        .some(invoker('openInTabIfMatch', link, e.eventPhase))
      if (!opened) return

      e.preventDefault()
      if (e.eventPhase === Event.CAPTURING_PHASE) e.stopPropagation()
    }
    LinkSelector.prototype.matchUrlForward = function() {
      return document.location.href.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(link.matches.bind(link))
    }
    LinkSelector.prototype.openInTabIfMatch = function(link, eventPhase) {
      if (this.matchEventPhase(eventPhase) && this.matchLink(link)) {
        openInTab(link.href)
        return true
      }
      return false
    }
    return LinkSelector
  })()

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

  main()
})()