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