您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
タブキーによるフォーカス移動の対象と順番をCSSセレクタで設定できるようにする
// ==UserScript== // @name Focus Navigator // @namespace https://gf.qytechs.cn/users/1009-kengo321 // @description タブキーによるフォーカス移動の対象と順番をCSSセレクタで設定できるようにする // @match *://*/* // @version 5 // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_setClipboard // @grant GM.getValue // @grant GM.setValue // @grant GM.setClipboard // @license MIT License // @noframes // @run-at document-start // ==/UserScript== ;(function() { 'use strict' const [gmGetValue, gmSetValue, gmSetClipboard] = typeof GM_getValue === 'undefined' ? [GM.getValue, GM.setValue, GM.setClipboard] : [GM_getValue, GM_setValue, GM_setClipboard] var Config = (function() { var byId = function(id) { return function() { return this._doc.getElementById(id) } } var addOption = function(selectElem) { return function(optionText) { var o = selectElem.ownerDocument.createElement('option') o.textContent = optionText selectElem.appendChild(o) } } var isValidSelector = function(selector) { try { document.querySelector(selector) return true } catch (e) { return false } } var matchForward = function(s1, s2) { return s1.indexOf(s2) === 0 } var comparator = function(url) { return function(o1, o2) { var matched1 = matchForward(url, o1.url) var matched2 = matchForward(url, o2.url) if (matched1 && !matched2) return -1 if (!matched1 && matched2) return 1 if (o1.url < o2.url) return -1 if (o1.url > o2.url) return 1 return 0 } } var maxZIndex = function() { return '2147483647' } var maxFrameWidth = function() { return 600 } var selectedIndices = function(selectElem) { return [].map.call(selectElem.selectedOptions, function(o) { return o.index }) } var removeAt = function(array, indices) { return array.filter(function(e, i) { return indices.indexOf(i) === -1 }) } var removeSelectedOptions = function(selectElem) { ;[].slice.call(selectElem.selectedOptions).forEach(function(o) { o.parentNode.removeChild(o) }) } var moveSelectedSelector = function(op) { return function() { var l = this._selectorList() var i = l.selectedIndex var t = l.options[i].textContent l.options[i].textContent = l.options[op(i)].textContent l.options[op(i)].textContent = t l.selectedIndex = op(i) var d = this._data[this._urlList().selectedIndex] var s = d.selectors[i] d.selectors[i] = d.selectors[op(i)] d.selectors[op(i)] = s } } var Config = function(doc) { this._doc = doc this._data = Config.focusSelectors().sort(comparator(this._topUrl())) this._updateUrlList() this._styleMatchedUrl() this._addAllCallbacks() this._urlList().focus() } Config.prototype._urlList = byId('url-list') Config.prototype._urlEditButton = byId('url-edit-button') Config.prototype._urlRemoveButton = byId('url-remove-button') Config.prototype._selectorList = byId('selector-list') Config.prototype._selectorAddButton = byId('selector-add-button') Config.prototype._selectorEditButton = byId('selector-edit-button') Config.prototype._selectorRemoveButton = byId('selector-remove-button') Config.prototype._selectorUpButton = byId('selector-up-button') Config.prototype._selectorDownButton = byId('selector-down-button') Config.prototype._scrollPositionFieldSet = byId('scroll-position-fieldset') Config.prototype._coordinateCheckbox = byId('coordinate-checkbox') Config.prototype._lowerSpaceInput = byId('lower-space-input') Config.prototype._upperSpaceInput = byId('upper-space-input') Config.prototype._customCssTextArea = byId('custom-css-textarea') Config.prototype._frame = function() { return this._doc.defaultView.frameElement } Config.prototype._topUrl = function() { return this._frame().ownerDocument.location.href } Config.prototype._updateUrlList = function() { this._urlList().length = 0 this._data.map(function(s) { return s.url }) .forEach(addOption(this._urlList())) } Config.prototype._styleMatchedUrl = function() { var url = this._topUrl() this._data.map(function(d) { return matchForward(url, d.url) }).forEach(function(matched, i) { this._urlList().options[i] .classList[matched ? 'add' : 'remove']('matched') }, this) } Config.prototype._setComputedHeight = function(elem) { elem.style.height = this._doc.defaultView.getComputedStyle(elem).height } Config.prototype._addCallbacks = function(id, type, callbacks) { var target = this._doc.getElementById(id) callbacks.forEach(function(callback) { target.addEventListener(type, callback) }) } Config.prototype._addAllCallbacks = function() { ;[['url-list', 'change', [ this.updateValueBySelectedUrl.bind(this), this.updateDisabled.bind(this) ]], ['selector-list', 'change', [this.updateDisabled.bind(this)]], ['url-add-button', 'click', [ this.addUrl.bind(this), this.updateValueBySelectedUrl.bind(this), this.updateDisabled.bind(this), ]], ['url-edit-button', 'click', [this.editUrl.bind(this)]], ['url-remove-button', 'click', [ this.removeSelectedUrls.bind(this), this.updateValueBySelectedUrl.bind(this), this.updateDisabled.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.removeSelectedSelectors.bind(this), this.updateDisabled.bind(this), ]], ['selector-up-button', 'click', [ this.upSelectedSelector.bind(this), this.updateDisabled.bind(this), ]], ['selector-down-button', 'click', [ this.downSelectedSelector.bind(this), this.updateDisabled.bind(this), ]], ['coordinate-checkbox', 'change', [ this.coordinateCheckboxChanged.bind(this), ]], ['lower-space-input', 'input', [ this.lowerSpaceInputChanged.bind(this), ]], ['upper-space-input', 'input', [ this.upperSpaceInputChanged.bind(this), ]], ['custom-css-textarea', 'input', [ this.cssTextAreaChanged.bind(this), ]], ['import-export-checkbox', 'change', [ this.impExpCheckboxChanged.bind(this), ]], ['export-button', 'click', [this.export.bind(this)]], ['import-button', 'click', [ this.import.bind(this), this.updateValueBySelectedUrl.bind(this), this.updateDisabled.bind(this), ]], ['ok-button', 'click', [ this.save.bind(this), FocusSelector.update.bind(null, this._frame().ownerDocument), this.removeFrameFromParent.bind(this), ]], ['cancel-button', 'click', [this.removeFrameFromParent.bind(this)]], ].forEach(this._addCallbacks.apply.bind(this._addCallbacks, this)) } Config.prototype._updateSelectorList = function() { this._selectorList().length = 0 if (this._urlList().selectedOptions.length !== 1) return var i = this._urlList().selectedIndex ;(this._data[i].selectors || []).forEach(addOption(this._selectorList())) } Config.prototype._updateScrollPositionElements = function() { if (this._urlList().selectedOptions.length === 1) { var d = this._data[this._urlList().selectedIndex] this._coordinateCheckbox().checked = d.coordinated this._lowerSpaceInput().value = d.lowerSpace || 0 this._upperSpaceInput().value = d.upperSpace || 0 } else { this._coordinateCheckbox().checked = false this._lowerSpaceInput().value = 0 this._upperSpaceInput().value = 0 } } Config.prototype._updateCustomCssTextArea = function() { if (this._urlList().selectedOptions.length === 1) { var d = this._data[this._urlList().selectedIndex] this._customCssTextArea().value = d.cssText || '' } else { this._customCssTextArea().value = '' } } Config.prototype.updateValueBySelectedUrl = function() { this._updateSelectorList() this._updateScrollPositionElements() this._updateCustomCssTextArea() } Config.prototype._updateDisabledBySelectedUrl = function() { var len = this._urlList().selectedOptions.length this._urlEditButton().disabled = len !== 1 this._urlRemoveButton().disabled = len === 0 this._selectorList().disabled = len !== 1 this._selectorAddButton().disabled = len !== 1 this._coordinateCheckbox().disabled = len !== 1 this._scrollPositionFieldSet().disabled = !(len === 1 && this._coordinateCheckbox().checked) this._customCssTextArea().disabled = len !== 1 } Config.prototype._updateDisabledBySelectedSelector = function() { var list = this._selectorList() var len = list.selectedOptions.length this._selectorEditButton().disabled = len !== 1 this._selectorRemoveButton().disabled = len === 0 var top = list.selectedIndex === 0 this._selectorUpButton().disabled = len !== 1 || top var last = list.selectedIndex === list.length - 1 this._selectorDownButton().disabled = len !== 1 || last } Config.prototype.updateDisabled = function() { this._updateDisabledBySelectedUrl() this._updateDisabledBySelectedSelector() } Config.prototype.addUrl = function() { var url = '' do { url = prompt(url ? '"' + url + '"は登録済みです。' : '' , url || this._topUrl()) if (url === null) return } while (this._data.some(function(d) { return d.url === url})) this._data.push({url: url}) addOption(this._urlList())(url) this._urlList().selectedIndex = this._urlList().length - 1 this._styleMatchedUrl() } Config.prototype.editUrl = function() { var i = this._urlList().selectedIndex var data = this._data[i] var url = '' do { url = prompt(url ? '"' + url + '"は登録済みです。' : '' , url || data.url) if (url === null) return } while (this._data.some(function(d) { return d.url === url})) data.url = url this._urlList().options[i].textContent = url this._styleMatchedUrl() } Config.prototype.save = function() { gmSetValue('focusSelectors', JSON.stringify(this._data)) Config.focusSelectors.set(this._data) } Config.prototype.removeSelectedUrls = function() { this._data = removeAt(this._data, selectedIndices(this._urlList())) removeSelectedOptions(this._urlList()) } Config.prototype.addSelector = function() { var selector = '' do { selector = prompt(selector ? '構文エラー' : '', selector) if (selector === null) return } while (!isValidSelector(selector)) addOption(this._selectorList())(selector) var d = this._data[this._urlList().selectedIndex] d.selectors = (d.selectors || []).concat(selector) this._selectorList().selectedIndex = this._selectorList().length - 1 } Config.prototype.editSelector = function() { var d = this._data[this._urlList().selectedIndex] var i = this._selectorList().selectedIndex var current = d.selectors[i] var selector = '' do { selector = prompt(selector ? '構文エラー' : '', selector || current) if (selector === null) return } while (!isValidSelector(selector)) d.selectors[i] = selector this._selectorList().options[i].textContent = selector } Config.prototype.removeSelectedSelectors = function() { var d = this._data[this._urlList().selectedIndex] d.selectors = removeAt(d.selectors , selectedIndices(this._selectorList())) removeSelectedOptions(this._selectorList()) } Config.prototype.upSelectedSelector = moveSelectedSelector(function(v) { return v - 1 }) Config.prototype.downSelectedSelector = moveSelectedSelector(function(v) { return v + 1 }) Config.prototype.coordinateCheckboxChanged = function() { var checked = this._coordinateCheckbox().checked this._data[this._urlList().selectedIndex].coordinated = checked this._scrollPositionFieldSet().disabled = !checked } Config.prototype.lowerSpaceInputChanged = function() { if (this._lowerSpaceInput().validity.valid) { this._data[this._urlList().selectedIndex].lowerSpace = this._lowerSpaceInput().valueAsNumber } } Config.prototype.upperSpaceInputChanged = function() { if (this._upperSpaceInput().validity.valid) { this._data[this._urlList().selectedIndex].upperSpace = this._upperSpaceInput().valueAsNumber } } Config.prototype.cssTextAreaChanged = function() { this._data[this._urlList().selectedIndex].cssText = this._customCssTextArea().value.trim() } Config.prototype.removeFrameFromParent = function() { this._frame().parentNode.removeChild(this._frame()) var b = this.background if (b && b.parentNode) b.parentNode.removeChild(b) } Config.prototype.impExpCheckboxChanged = function() { var checkbox = this._doc.getElementById('import-export-checkbox') var container = this._doc.getElementById('import-export-container') container.classList[checkbox.checked ? 'add' : 'remove']('show') } Config.prototype.export = function() { gmSetClipboard(JSON.stringify(this._data)) } Config.prototype.import = function() { try { var ta = this._doc.getElementById('import-textarea') this._data = JSON.parse(ta.value) this._updateUrlList() this._styleMatchedUrl() ta.setCustomValidity('') } catch (e) { ta.setCustomValidity(e.toString()) } } Config.focusSelectors = function() { return Config._focusSelectors } Config.focusSelectors.set = function(focusSelectors) { Config._focusSelectors = focusSelectors } Config.show = function(doc) { var background = doc.createElement('div') background.style.backgroundColor = 'black' background.style.opacity = '0.5' background.style.zIndex = maxZIndex() - 1 background.style.position = 'fixed' background.style.top = '0' background.style.left = '0' background.style.width = '100%' background.style.height = '100%' doc.body.appendChild(background) var f = doc.createElement('iframe') f.style.position = 'fixed' f.style.top = '0' f.style.left = '0' f.style.width = '100%' f.style.height = '100%' f.style.zIndex = maxZIndex() f.srcdoc = Config.srcdoc f.addEventListener('load', async function() { Config.focusSelectors.set(JSON.parse(await gmGetValue('focusSelectors', '[]'))) var config = new Config(f.contentDocument) config.background = background }) doc.body.appendChild(f) } Config.srcdoc = [ '<!doctype html><html><head><style>', ' html {', ' margin: 0 auto;', ' max-width: 50em;', ' height: 100%;', ' line-height: 1.5em;', ' }', ' body {', ' height: 100%;', ' margin: 0;', ' display: flex;', ' flex-direction: column;', ' justify-content: center;', ' }', ' #dialog {', ' overflow: auto;', ' padding: 8px;', ' background-color: white;', ' }', ' p { margin: 0; }', ' select { width: 100%; }', ' textarea { width: 100%; }', ' .label-p, #scroll-position-fieldset, #ok-cancel-p {', ' margin-top: 10px;', ' }', ' #custom-css-desc { line-height: 1.1em; }', ' #ok-cancel-p { text-align: right; }', ' #url-list .matched { text-decoration: underline; }', ' #import-export-container { display: none; }', ' #import-export-container.show { display: block; }', '</style></head><body><div id=dialog>', '<p><label for=url-list>対象ページのURL一覧(前方一致):</label></p>', '<p><select id=url-list size=10 multiple></select></p>', '<p>', ' <input id=url-add-button type=button value=追加>', ' <input id=url-edit-button type=button value=編集 disabled>', ' <input id=url-remove-button type=button value=削除 disabled>', '</p>', '<p><small>', ' 複数一致するときは、一番長いURLの設定を使用します。', '</small></p>', '<p class=label-p><label for=selector-list>', ' フォーカス対象と移動順のCSSセレクタ一覧:', '</label></p>', '<p><select id=selector-list size=5 multiple disabled></select></p>', '<p>', ' <input id=selector-add-button type=button value=追加 disabled>', ' <input id=selector-edit-button type=button value=編集 disabled>', ' <input id=selector-remove-button type=button value=削除 disabled>', ' <input id=selector-up-button type=button value=上へ disabled>', ' <input id=selector-down-button type=button value=下へ disabled>', '</p>', '<fieldset id=scroll-position-fieldset disabled>', ' <legend><label>', ' <input id=coordinate-checkbox type=checkbox disabled>', ' スクロールの位置を調整する', ' </label></legend>', ' <p><label>', ' 表示領域の上とフォーカスとの最小間隔:', ' <input id=upper-space-input type=number value=0 required>', ' </label></p>', ' <p><label>', ' 表示領域の下とフォーカスとの最小間隔:', ' <input id=lower-space-input type=number value=0 required>', ' </label></p>', '</fieldset>', '<p class=label-p>', ' <label for=custom-css-textarea>カスタムCSS:</label>', '</p>', '<p><textarea id=custom-css-textarea rows=3 disabled></textarea></p>', '<p id=custom-css-desc><small>', ' この入力欄の内容を持つstyle要素をhead要素の末尾に追加します。何も入力していないときは追加しません。フォーカスのアウトラインが非表示のときに利用してください。', '</small></p>', '<p class=label-p>', ' インポート・エクスポート:', ' <small>', ' <label><input id=import-export-checkbox type=checkbox>表示</label>', ' </small>', '</p>', '<div id=import-export-container>', ' <p><textarea id=import-textarea rows=2></textarea></p>', ' <p><input id=import-button type=button value=インポート></p>', ' <p><input id=export-button type=button', ' value=クリップボードへエクスポート></p>', '</div>', '<p id=ok-cancel-p>', ' <input id=ok-button type=button value=OK>', ' <input id=cancel-button type=button value=キャンセル>', '</p>', '</div></body></html>', ].join('\n') return Config })() var FocusSelector = (function() { var tabKeyCode = 9 var isTabKey = function(e) { return e.which === tabKeyCode && !(e.ctrlKey || e.metaKey || e.altKey) } var ring = function(array, start, reverse) { var a = array.slice(start).concat(array.slice(0, start)) return reverse ? a.reverse() : a } var matchedFocusSelectors = function(doc) { return Config.focusSelectors() .map(FocusSelector.new) .filter(function(focusSelector) { return focusSelector._matchUrlForward(doc.location.href) }) } var longestUrlFocusSelector = function(doc) { var s = matchedFocusSelectors(doc) if (!s.length) return return s.reduce(function(previous, current) { return previous._hasLongerUrl(current) ? previous : current }) } var focusNext = function(requestFocus) { return function(keyDownEvent) { if (!isTabKey(keyDownEvent)) return var d = keyDownEvent.target.ownerDocument var focusSelector = longestUrlFocusSelector(d) if (!focusSelector) return var selected = focusSelector._querySelectors(d) if (!selected.length) return var i = selected.indexOf(d.activeElement) var start = keyDownEvent.shiftKey ? Math.max(i, 0) : (i + 1) % selected.length var r = ring(selected, start, keyDownEvent.shiftKey) requestFocus(keyDownEvent , r.some.bind(r, focusSelector._focus.bind(focusSelector))) } } var FocusSelector = function(o) { this._url = o.url this._selectors = o.selectors || [] this._coordinated = Boolean(o.coordinated) this._lowerSpace = o.lowerSpace || 0 this._upperSpace = o.upperSpace || 0 this._cssText = o.cssText } FocusSelector.prototype._matchUrlForward = function(url) { return url.indexOf(this._url) === 0 } FocusSelector.prototype._hasLongerUrl = function(other) { return this._url.length >= other._url.length } FocusSelector.prototype._querySelectors = function(doc) { return this._selectors.reduce(function(selected, selector) { return selected.concat.apply(selected, doc.querySelectorAll(selector)) }, []) } FocusSelector.prototype._focus = function(elem) { var preFocus = elem.getBoundingClientRect() elem.focus() var result = elem.ownerDocument.activeElement === elem if (result && this._coordinated) { this._coordinateScroll(elem, preFocus) } return result } FocusSelector.prototype._coordinateScroll = function(elem, preFocus) { var win = elem.ownerDocument.defaultView var postFocus = elem.getBoundingClientRect() if (preFocus.bottom > win.innerHeight - this._lowerSpace) { var y = win.innerHeight - postFocus.bottom - this._lowerSpace win.scrollBy(0, -y) } else if (preFocus.top < this._upperSpace) { win.scrollBy(0, postFocus.top - this._upperSpace) } } FocusSelector.prototype._hasCssText = function() { return Boolean(this._cssText) } FocusSelector.prototype._createStyleElem = function(doc) { var result = doc.createElement('style') result.id = 'focus-navigator-style' result.textContent = this._cssText return result } FocusSelector.new = function(o) { return new FocusSelector(o) } FocusSelector.addCallbackIfRequired = function(doc) { if (matchedFocusSelectors(doc).length) { doc.addEventListener('keydown', FocusSelector.firstCallback) } } // Firefox36.0 + Greasemonkey2.3 // タブキーの keydown イベントをすべてキャンセルして // デフォルトの処理を一度も実行させなかった場合、 // フォーカスされた要素のアウトラインが表示されない。 // これの対策として、フォーカス処理をあとのイベントループで実行させて、 // 最初のイベントだけキャンセルせずにデフォルトの処理をさせることで、 // アウトラインを表示。 FocusSelector.firstCallback = focusNext(function(event, focus) { setTimeout(focus, 0) var d = event.target.ownerDocument d.removeEventListener('keydown', FocusSelector.firstCallback) d.addEventListener('keydown', FocusSelector.callback) }) FocusSelector.callback = focusNext(function(event, focus) { if (focus()) event.preventDefault() }) FocusSelector.addStyleElemIfRequired = function(doc) { var s = longestUrlFocusSelector(doc) if (s && s._hasCssText()) { if (doc.head) { doc.head.appendChild(s._createStyleElem(doc)) } else { doc.addEventListener('DOMContentLoaded', () => { doc.head.appendChild(s._createStyleElem(doc)) }) } } } FocusSelector.update = function(doc) { doc.removeEventListener('keydown', FocusSelector.firstCallback) doc.removeEventListener('keydown', FocusSelector.callback) var style = doc.getElementById('focus-navigator-style') if (style) style.parentNode.removeChild(style) FocusSelector.addCallbackIfRequired(doc) FocusSelector.addStyleElemIfRequired(doc) } return FocusSelector })() function addConfigButtonIfScriptPage() { if (!location.href.startsWith('https://gf.qytechs.cn/ja/scripts/8736-focus-navigator')) return const add = () => { const e = document.createElement('button') e.type = 'button' e.textContent = '設定' e.addEventListener('click', Config.show.bind(Config, document)) document.querySelector('#script-info > header > h2').appendChild(e) } if (['interactive', 'complete'].includes(document.readyState)) add() else document.addEventListener('DOMContentLoaded', add) } async function main() { Config.focusSelectors.set(JSON.parse(await gmGetValue('focusSelectors', '[]'))) FocusSelector.addCallbackIfRequired(document) FocusSelector.addStyleElemIfRequired(document) if (typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand('Focus Navigator 設定' , Config.show.bind(Config, document)) } addConfigButtonIfScriptPage() } main() })()
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址