新しいタブで開くリンクをCSSセレクタで選べるようにする
当前为
// ==UserScript==
// @name Open In New Tab
// @namespace https://greasyfork.org/users/1009-kengo321
// @version 3
// @description 新しいタブで開くリンクをCSSセレクタで選べるようにする
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_openInTab
// @match *://*/*
// @license MIT
// @noframes
// ==/UserScript==
;(function() {
'use strict'
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) }
}
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 Config(doc) {
this.doc = doc
this.linkSelectors = Config.getLinkSelectors().sort(compareLinkSelector)
this.updateUrlList()
this.addCallbacks()
}
Config.srcdoc = '\
<!DOCTYPE html>\
<html><head><style>\
body {\
margin: 0;\
padding: 5px;\
}\
p { margin: 0; }\
#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; }\
</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', '[]')).map(function(o) {
return new LinkSelector(o)
})
}
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.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.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.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.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 = '9999'
s.borderWidth = 'medium'
s.borderStyle = 'solid'
s.borderColor = 'black'
s.boxSizing = 'content-box'
var w = iframe.width = 600
var h = iframe.height = this.doc.documentElement.offsetHeight
var v = iframe.ownerDocument.defaultView
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.getUrlList())
setComputedHeight(w, this.getSelectorList())
}
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.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)) {
GM_openInTab(link.href)
return true
}
return false
}
return LinkSelector
})()
function main() {
GM_registerMenuCommand('Open In New Tab 設定', Config.show)
LinkSelector.addCallbackIfRequired()
}
main()
})()