Open In New Tab

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

  1. // ==UserScript==
  2. // @name Open In New Tab
  3. // @namespace https://gf.qytechs.cn/users/1009-kengo321
  4. // @version 8
  5. // @description 新しいタブで開くリンクをCSSセレクタで選べるようにする
  6. // @grant GM_registerMenuCommand
  7. // @grant GM_getValue
  8. // @grant GM_setValue
  9. // @grant GM_openInTab
  10. // @grant GM_setClipboard
  11. // @grant GM_info
  12. // @grant GM.getValue
  13. // @grant GM.setValue
  14. // @grant GM.openInTab
  15. // @grant GM.setClipboard
  16. // @grant GM.info
  17. // @match *://*/*
  18. // @license MIT
  19. // @noframes
  20. // @run-at document-start
  21. // ==/UserScript==
  22.  
  23. ;(function() {
  24. 'use strict'
  25.  
  26. if (window.self !== window.top) return
  27.  
  28. function getter(propName) {
  29. return function(o) { return o[propName] }
  30. }
  31. function not(func) {
  32. return function() { return !func.apply(null, arguments) }
  33. }
  34. const [gmGetValue, gmSetValue, gmOpenInTab, gmSetClipboard, gmInfo] =
  35. typeof GM_getValue === 'undefined'
  36. ? [GM.getValue, GM.setValue, GM.openInTab, GM.setClipboard, GM.info]
  37. : [GM_getValue, GM_setValue, GM_openInTab, GM_setClipboard, GM_info]
  38. async function gmGetLinkSelectors() {
  39. return JSON.parse(await gmGetValue('linkSelectors', '[]')).map(o => new LinkSelector(o))
  40. }
  41.  
  42. var Config = (function() {
  43.  
  44. function compareLinkSelector(o1, o2) {
  45. var m1 = o1.matchUrlForward(), m2 = o2.matchUrlForward()
  46. if (m1 && !m2) return -1
  47. if (!m1 && m2) return 1
  48. if (o1.url < o2.url) return -1
  49. if (o1.url > o2.url) return 1
  50. return 0
  51. }
  52. function setComputedHeight(win, elem) {
  53. elem.style.height = win.getComputedStyle(elem).height
  54. }
  55. function updateUrlOptionClass(option, linkSelector) {
  56. var p = linkSelector.matchUrlForward() ? 'add' : 'remove'
  57. option.classList[p]('matched')
  58. return option
  59. }
  60. function addAndSelectOption(selectElem, option) {
  61. selectElem.add(option)
  62. selectElem.selectedIndex = option.index
  63. }
  64. function getSelectedIndices(selectElem) {
  65. return [].map.call(selectElem.selectedOptions, getter('index'))
  66. }
  67. function removeSelectedOptions(selectElem) {
  68. ;[].slice.call(selectElem.selectedOptions).forEach(function(o) {
  69. o.parentNode.removeChild(o)
  70. })
  71. }
  72. function filterIndices(array, indices) {
  73. return array.filter(function(e, i) { return indices.indexOf(i) === -1 })
  74. }
  75. function optionAdder(selectElem, newOption) {
  76. return function(e) { selectElem.add(newOption(e)) }
  77. }
  78. function maxZIndex() {
  79. return '2147483647'
  80. }
  81.  
  82. function Config(doc) {
  83. this.doc = doc
  84. this.doc.getElementById('insert-p').hidden = (gmInfo.scriptHandler === 'Greasemonkey')
  85. this.addCallbacks()
  86. }
  87. Config.srcdoc = [
  88. '<!DOCTYPE html>',
  89. '<html><head><style>',
  90. ' html {',
  91. ' margin: 0 auto;',
  92. ' max-width: 50em;',
  93. ' height: 100%;',
  94. ' line-height: 1.5em;',
  95. ' }',
  96. ' body {',
  97. ' height: 100%;',
  98. ' margin: 0;',
  99. ' display: flex;',
  100. ' flex-direction: column;',
  101. ' justify-content: center;',
  102. ' }',
  103. ' #dialog {',
  104. ' overflow: auto;',
  105. ' padding: 8px;',
  106. ' background-color: white;',
  107. ' }',
  108. ' p { margin: 0; }',
  109. ' textarea { width: 100%; }',
  110. ' #url-list { width: 100%; }',
  111. ' #url-list option.matched { text-decoration: underline; }',
  112. ' #selector-list { width: 100%; }',
  113. ' #confirm-p { text-align: right; }',
  114. ' p.description { font-size: smaller; }',
  115. ' #import-export-container { display: none; }',
  116. ' #import-export-container.show { display: block; }',
  117. '</style></head><body><div id=dialog>',
  118. '<fieldset>',
  119. ' <legend>対象ページのURL(前方一致)</legend>',
  120. ' <p><select id=url-list multiple size=10></select></p>',
  121. ' <p>',
  122. ' <button id=url-add-button type=button>追加</button>',
  123. ' <button id=url-edit-button type=button disabled>編集</button>',
  124. ' <button id=url-remove-button type=button disabled>削除</button>',
  125. ' </p>',
  126. '</fieldset>',
  127. '<fieldset id=selector-fieldset disabled>',
  128. ' <legend>新しいタブで開くリンクのCSSセレクタ</legend>',
  129. ' <p class=description>',
  130. ' 何も登録していないときは、すべてのリンクが対象になります',
  131. ' </p>',
  132. ' <p><select id=selector-list multiple size=5></select></p>',
  133. ' <p>',
  134. ' <button id=selector-add-button type=button>追加</button>',
  135. ' <button id=selector-edit-button type=button>編集</button>',
  136. ' <button id=selector-remove-button type=button>削除</button>',
  137. ' </p>',
  138. ' <p>',
  139. ' <label>',
  140. ' <input type=checkbox id=capture-checkbox>',
  141. ' キャプチャフェーズを使用して、イベント伝播を中断する',
  142. ' </label>',
  143. ' </p>',
  144. ' <p class=description>',
  145. ' 正しく動作しないときは、これを有効にして試してください',
  146. ' </p>',
  147. '</fieldset>',
  148. '<p id=active-p><label>',
  149. ' <input id=active-checkbox type=checkbox>',
  150. ' 新しいタブを開いたとき、すぐにそのタブに切り替える',
  151. '</label></p>',
  152. '<p id=insert-p><label>',
  153. ' <input id=insert-checkbox type=checkbox>',
  154. ' 現在のタブの後ろに新しいタブを挿入する',
  155. '</label></p>',
  156. '<p>',
  157. ' インポート・エクスポート:',
  158. ' <small>',
  159. ' <label><input id=import-export-checkbox type=checkbox>表示</label>',
  160. ' </small>',
  161. '</p>',
  162. '<div id=import-export-container>',
  163. ' <p><textarea id=import-export-textarea rows=2></textarea></p>',
  164. ' <p>',
  165. ' <input id=import-button type=button value=インポート>',
  166. ' <input id=export-button type=button value=エクスポート>',
  167. ' </p>',
  168. ' <p><input id=export-to-clipboard-button type=button',
  169. ' value=クリップボードへエクスポート></p>',
  170. '</div>',
  171. '<p id=confirm-p>',
  172. ' <button id=ok-button type=button>OK</button>',
  173. ' <button id=cancel-button type=button>キャンセル</button>',
  174. '</p>',
  175. '</div></body></html>',
  176. ].join('\n')
  177. Config.show = function(done) {
  178. var background = document.createElement('div')
  179. background.style.backgroundColor = 'black'
  180. background.style.opacity = '0.5'
  181. background.style.zIndex = maxZIndex() - 1
  182. background.style.position = 'fixed'
  183. background.style.top = '0'
  184. background.style.left = '0'
  185. background.style.width = '100%'
  186. background.style.height = '100%'
  187. document.body.appendChild(background)
  188. var f = document.createElement('iframe')
  189. f.style.position = 'fixed'
  190. f.style.top = '0'
  191. f.style.left = '0'
  192. f.style.width = '100%'
  193. f.style.height = '100%'
  194. f.style.zIndex = maxZIndex()
  195. f.srcdoc = Config.srcdoc
  196. f.addEventListener('load', async function() {
  197. const linkSelectors = await gmGetLinkSelectors()
  198. Config.setLinkSelectors(linkSelectors)
  199. var config = new Config(f.contentDocument)
  200. config.linkSelectors = linkSelectors.sort(compareLinkSelector)
  201. config.updateUrlList()
  202. config.getActiveCheckbox().checked = await Config.isNewTabActivation()
  203. config.getInsertCheckbox().checked = await Config.isNewTabInsertion()
  204. config.setIFrame(f)
  205. config.background = background
  206. if (typeof(done) === 'function') done(config)
  207. })
  208. document.body.appendChild(f)
  209. }
  210. Config.getLinkSelectors = function() {
  211. return Config._linkSelectors
  212. }
  213. Config.setLinkSelectors = function(linkSelectors) {
  214. Config._linkSelectors = linkSelectors
  215. }
  216. Config.isNewTabActivation = function() {
  217. return gmGetValue('active', true)
  218. }
  219. Config.isNewTabInsertion = function() {
  220. return gmGetValue('insert', false)
  221. }
  222. Config.prototype.addCallbacks = function() {
  223. var doc = this.doc
  224. ;[['url-list', 'change', [
  225. this.updateSelectorList.bind(this),
  226. this.updateCaptureCheckbox.bind(this),
  227. this.updateDisabled.bind(this),
  228. ]],
  229. ['selector-list', 'change', this.updateDisabled.bind(this)],
  230. ['url-add-button', 'click', [
  231. this.addUrl.bind(this),
  232. this.updateDisabled.bind(this),
  233. this.updateSelectorList.bind(this),
  234. this.updateCaptureCheckbox.bind(this),
  235. ]],
  236. ['url-edit-button', 'click', this.editUrl.bind(this)],
  237. ['url-remove-button', 'click', [
  238. this.removeUrl.bind(this),
  239. this.updateDisabled.bind(this),
  240. this.updateSelectorList.bind(this),
  241. this.updateCaptureCheckbox.bind(this),
  242. ]],
  243. ['selector-add-button', 'click', [
  244. this.addSelector.bind(this),
  245. this.updateDisabled.bind(this),
  246. ]],
  247. ['selector-edit-button', 'click', this.editSelector.bind(this)],
  248. ['selector-remove-button', 'click', [
  249. this.removeSelector.bind(this),
  250. this.updateDisabled.bind(this),
  251. ]],
  252. ['capture-checkbox', 'change', this.updateCapture.bind(this)],
  253. ['ok-button', 'click', [
  254. this.save.bind(this),
  255. LinkSelector.updateCallback,
  256. this.removeIFrame.bind(this),
  257. ]],
  258. ['cancel-button', 'click', this.removeIFrame.bind(this)],
  259. [ 'import-export-checkbox',
  260. 'change',
  261. this.importExportCheckboxChanged.bind(this),
  262. ],
  263. [ 'export-to-clipboard-button',
  264. 'click',
  265. this.exportToClipboard.bind(this),
  266. ],
  267. ['import-button', 'click', [
  268. this.import.bind(this),
  269. this.updateSelectorList.bind(this),
  270. this.updateCaptureCheckbox.bind(this),
  271. this.updateDisabled.bind(this),
  272. ]],
  273. ['export-button', 'click', this.export.bind(this)],
  274. ].forEach(function(e) {
  275. ;[].concat(e[2]).forEach(function(callback) {
  276. doc.getElementById(e[0]).addEventListener(e[1], callback)
  277. })
  278. })
  279. }
  280. Config.prototype.getUrlList = function() {
  281. return this.doc.getElementById('url-list')
  282. }
  283. Config.prototype.getSelectorList = function() {
  284. return this.doc.getElementById('selector-list')
  285. }
  286. Config.prototype.getCaptureCheckbox = function() {
  287. return this.doc.getElementById('capture-checkbox')
  288. }
  289. Config.prototype.getActiveCheckbox = function() {
  290. return this.doc.getElementById('active-checkbox')
  291. }
  292. Config.prototype.getInsertCheckbox = function() {
  293. return this.doc.getElementById('insert-checkbox')
  294. }
  295. Config.prototype.getImpExpTextarea = function() {
  296. return this.doc.getElementById('import-export-textarea')
  297. }
  298. Config.prototype.newOption = function(text) {
  299. var result = this.doc.createElement('option')
  300. result.textContent = text
  301. return result
  302. }
  303. Config.prototype.newUrlOption = function(linkSelector) {
  304. return updateUrlOptionClass(this.newOption(linkSelector.url)
  305. , linkSelector)
  306. }
  307. Config.prototype.updateUrlList = function() {
  308. this.getUrlList().length = 0
  309. this.linkSelectors.forEach(optionAdder(this.getUrlList()
  310. , this.newUrlOption.bind(this)))
  311. }
  312. Config.prototype.getSelectedLinkSelector = function() {
  313. return this.linkSelectors[this.getUrlList().selectedIndex]
  314. }
  315. Config.prototype.updateSelectorList = function() {
  316. this.clearSelectorList()
  317. if (this.getUrlList().selectedOptions.length !== 1) return
  318. this.getSelectedLinkSelector()
  319. .selectors
  320. .forEach(optionAdder(this.getSelectorList()
  321. , this.newOption.bind(this)))
  322. }
  323. Config.prototype.clearSelectorList = function() {
  324. var s = this.getSelectorList()
  325. while (s.hasChildNodes()) s.removeChild(s.firstChild)
  326. }
  327. Config.prototype.addUrl = function() {
  328. var r = prompt('', document.location.href)
  329. if (!r) return
  330. var s = new LinkSelector({url: r})
  331. this.linkSelectors.push(s)
  332. addAndSelectOption(this.getUrlList(), this.newUrlOption(s))
  333. }
  334. Config.prototype.editUrl = function() {
  335. if (this.getUrlList().selectedOptions.length !== 1) return
  336. var r = prompt('', this.getSelectedLinkSelector().url)
  337. if (!r) return
  338. this.getUrlList().selectedOptions[0].textContent = r
  339. this.getSelectedLinkSelector().url = r
  340. updateUrlOptionClass(this.getUrlList().selectedOptions[0]
  341. , this.getSelectedLinkSelector())
  342. }
  343. Config.prototype.removeUrl = function() {
  344. this.linkSelectors = filterIndices(this.linkSelectors
  345. , getSelectedIndices(this.getUrlList()))
  346. removeSelectedOptions(this.getUrlList())
  347. }
  348. Config.prototype.getErrorIfInvalidSelector = function(selector) {
  349. try {
  350. this.doc.querySelector(selector)
  351. return null
  352. } catch (e) {
  353. return e
  354. }
  355. }
  356. Config.prototype.promptUntilValidSelector = function(defaultValue) {
  357. var selector = defaultValue || ''
  358. var error = null
  359. do {
  360. selector = prompt((error || '').toString(), selector)
  361. if (!selector) return null
  362. } while (error = this.getErrorIfInvalidSelector(selector))
  363. return selector
  364. }
  365. Config.prototype.addSelector = function() {
  366. if (this.getUrlList().selectedOptions.length !== 1) return
  367. var r = this.promptUntilValidSelector()
  368. if (!r) return
  369. this.getSelectedLinkSelector().selectors.push(r)
  370. addAndSelectOption(this.getSelectorList(), this.newOption(r))
  371. }
  372. Config.prototype.getSelectedSelector = function() {
  373. return this.getSelectorList().selectedOptions[0].textContent
  374. }
  375. Config.prototype.setSelectedSelector = function(selector) {
  376. var o = this.getSelectorList().selectedOptions[0]
  377. o.textContent = selector
  378. this.getSelectedLinkSelector().selectors[o.index] = selector
  379. }
  380. Config.prototype.editSelector = function() {
  381. if (this.getSelectorList().selectedOptions.length !== 1) return
  382. var r = this.promptUntilValidSelector(this.getSelectedSelector())
  383. if (!r) return
  384. this.setSelectedSelector(r)
  385. }
  386. Config.prototype.removeSelector = function() {
  387. var s = this.getSelectedLinkSelector()
  388. s.selectors = filterIndices(s.selectors
  389. , getSelectedIndices(this.getSelectorList()))
  390. removeSelectedOptions(this.getSelectorList())
  391. }
  392. Config.prototype.updateCaptureCheckbox = function() {
  393. var s = this.getSelectedLinkSelector()
  394. this.getCaptureCheckbox().checked = (s ? s.capture : false)
  395. }
  396. Config.prototype.updateCapture = function() {
  397. var s = this.getSelectedLinkSelector()
  398. if (s) s.capture = this.getCaptureCheckbox().checked
  399. }
  400. Config.prototype.updateDisabled = function() {
  401. var selectedUrlNum = this.getUrlList().selectedOptions.length
  402. var selectedSelectorNum = this.getSelectorList().selectedOptions.length
  403. ;[['url-edit-button', selectedUrlNum !== 1],
  404. ['url-remove-button', selectedUrlNum === 0],
  405. ['selector-fieldset', selectedUrlNum !== 1],
  406. ['selector-edit-button', selectedSelectorNum !== 1],
  407. ['selector-remove-button', selectedSelectorNum === 0],
  408. ].forEach(function(e) {
  409. this.doc.getElementById(e[0]).disabled = e[1]
  410. }, this)
  411. }
  412. Config.prototype.setIFrame = function(iframe) {
  413. this.iframe = iframe
  414. this.getUrlList().focus()
  415. }
  416. Config.prototype.removeIFrame = function() {
  417. var rm = function(e) {
  418. if (e && e.parentNode) e.parentNode.removeChild(e)
  419. }
  420. rm(this.iframe)
  421. rm(this.background)
  422. }
  423. Config.prototype.save = function() {
  424. gmSetValue('linkSelectors', JSON.stringify(this.linkSelectors))
  425. Config.setLinkSelectors(this.linkSelectors)
  426. gmSetValue('active', this.getActiveCheckbox().checked)
  427. gmSetValue('insert', this.getInsertCheckbox().checked)
  428. }
  429. Config.prototype.importExportCheckboxChanged = function() {
  430. var checkbox = this.doc.getElementById('import-export-checkbox')
  431. var container = this.doc.getElementById('import-export-container')
  432. container.classList[checkbox.checked ? 'add' : 'remove']('show')
  433. this.iframe.height = this.doc.documentElement.offsetHeight
  434. }
  435. Config.prototype.exportToClipboard = function() {
  436. gmSetClipboard(JSON.stringify(this.linkSelectors))
  437. }
  438. Config.prototype.import = function() {
  439. try {
  440. var ta = this.getImpExpTextarea()
  441. this.linkSelectors = JSON.parse(ta.value).map(function(o) {
  442. return new LinkSelector(o)
  443. })
  444. this.updateUrlList()
  445. ta.setCustomValidity('')
  446. } catch (e) {
  447. ta.setCustomValidity(e.toString())
  448. }
  449. }
  450. Config.prototype.export = function() {
  451. this.getImpExpTextarea().value = JSON.stringify(this.linkSelectors)
  452. }
  453. return Config
  454. })()
  455.  
  456. var LinkSelector = (function() {
  457.  
  458. function isLeftMouseButtonWithoutModifierKeys(mouseEvent) {
  459. var e = mouseEvent
  460. return !(e.button || e.altKey || e.shiftKey || e.ctrlKey || e.metaKey)
  461. }
  462. function isOpenableLink(elem) {
  463. return ['A', 'AREA'].indexOf(elem.tagName) >= 0
  464. && elem.href
  465. && elem.protocol !== 'javascript:'
  466. }
  467. function getAncestorOpenableLink(descendant) {
  468. for (var p = descendant.parentNode; p; p = p.parentNode) {
  469. if (isOpenableLink(p)) return p
  470. }
  471. return null
  472. }
  473. async function openInTab(url) {
  474. if (gmInfo.scriptHandler === 'Greasemonkey') {
  475. gmOpenInTab(url, !(await Config.isNewTabActivation()))
  476. } else {
  477. gmOpenInTab(url, {
  478. active: await Config.isNewTabActivation(),
  479. insert: await Config.isNewTabInsertion(),
  480. })
  481. }
  482. }
  483.  
  484. function LinkSelector(o) {
  485. o = o || {}
  486. this.url = o.url || ''
  487. this.selectors = o.selectors || []
  488. this.capture = !!o.capture
  489. }
  490. LinkSelector.getLocatedInstances = function() {
  491. return Config.getLinkSelectors().filter(s => s.matchUrlForward())
  492. }
  493. LinkSelector.addCallbackIfRequired = function() {
  494. var i = LinkSelector.getLocatedInstances()
  495. if (i.some(not(getter('capture')))) {
  496. document.addEventListener('click', LinkSelector.callback, false)
  497. }
  498. if (i.some(getter('capture'))) {
  499. document.addEventListener('click', LinkSelector.callback, true)
  500. }
  501. }
  502. LinkSelector.updateCallback = function() {
  503. document.removeEventListener('click', LinkSelector.callback, false)
  504. document.removeEventListener('click', LinkSelector.callback, true)
  505. LinkSelector.addCallbackIfRequired()
  506. }
  507. LinkSelector.callback = function(mouseEvent) {
  508. var e = mouseEvent
  509. if (!isLeftMouseButtonWithoutModifierKeys(e)) return
  510.  
  511. var link = isOpenableLink(e.target) ? e.target
  512. : getAncestorOpenableLink(e.target)
  513. if (!link) return
  514.  
  515. var opened = LinkSelector.getLocatedInstances()
  516. .some(s => s.openInTabIfMatch(link, e.eventPhase))
  517. if (!opened) return
  518.  
  519. e.preventDefault()
  520. if (e.eventPhase === Event.CAPTURING_PHASE) e.stopPropagation()
  521. }
  522. LinkSelector.prototype.matchUrlForward = function() {
  523. return document.location.href.indexOf(this.url) === 0
  524. }
  525. LinkSelector.prototype.matchEventPhase = function(eventPhase) {
  526. return this.capture ? eventPhase === Event.CAPTURING_PHASE
  527. : eventPhase === Event.BUBBLING_PHASE
  528. }
  529. LinkSelector.prototype.matchLink = function(link) {
  530. return !this.selectors.length
  531. || this.selectors.some(link.matches.bind(link))
  532. }
  533. LinkSelector.prototype.openInTabIfMatch = function(link, eventPhase) {
  534. if (this.matchEventPhase(eventPhase) && this.matchLink(link)) {
  535. openInTab(link.href)
  536. return true
  537. }
  538. return false
  539. }
  540. return LinkSelector
  541. })()
  542.  
  543. function addConfigButtonIfScriptPage() {
  544. if (!location.href.startsWith('https://gf.qytechs.cn/ja/scripts/5591-open-in-new-tab'))
  545. return
  546. const add = () => {
  547. const e = document.createElement('button')
  548. e.type = 'button'
  549. e.textContent = '設定'
  550. e.addEventListener('click', Config.show)
  551. document.querySelector('#script-info > header > h2').appendChild(e)
  552. }
  553. if (['interactive', 'complete'].includes(document.readyState))
  554. add()
  555. else
  556. document.addEventListener('DOMContentLoaded', add)
  557. }
  558. async function main() {
  559. Config.setLinkSelectors(await gmGetLinkSelectors())
  560. LinkSelector.addCallbackIfRequired()
  561. if (typeof GM_registerMenuCommand !== 'undefined')
  562. GM_registerMenuCommand('Open In New Tab 設定', Config.show)
  563. addConfigButtonIfScriptPage()
  564. }
  565.  
  566. main()
  567. })()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址