diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d390a04..2f672d1 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -197,6 +197,14 @@ "message": "Hide Find and Replace Pane Toggle Button", "description": "Text displayed beside the hide find and replace toggle button option." }, + "extension_option_scroll_markers_text": { + "message": "Show Scroll Markers", + "description": "Text displayed beside the scroll markers toggle option." + }, + "extension_option_scroll_markers_title": { + "message": "If enabled, scroll markers will appear on the right edge of the page indicating the position of all matches in the document.", + "description": "Tooltip text for the scroll markers option description icon." + }, "search_option_max_results_text": { "message": "Max Highlighted Results", "description": "Text displayed above the max highlighted results option." diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 46d2109..f2b12fa 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -197,6 +197,14 @@ "message": "検索と置換ボタンを非表示", "description": "Text displayed beside the hide find and replace toggle button option." }, + "extension_option_scroll_markers_text": { + "message": "スクロールマーカーを表示", + "description": "Text displayed beside the scroll markers toggle option." + }, + "extension_option_scroll_markers_title": { + "message": "有効時:すべての一致箇所の位置を示すスクロールマーカーがページ右端に表示されます", + "description": "Tooltip text for the scroll markers option description icon." + }, "search_option_max_results_text": { "message": "最大ハイライト数", "description": "Text displayed above the max highlighted results option." diff --git a/_locales/zh/messages.json b/_locales/zh/messages.json index 7bce042..a470262 100644 --- a/_locales/zh/messages.json +++ b/_locales/zh/messages.json @@ -197,6 +197,14 @@ "message": "隐藏“查找并替换”按钮", "description": "Text displayed beside the hide find and replace toggle button option." }, + "extension_option_scroll_markers_text": { + "message": "显示滚动标记", + "description": "Text displayed beside the scroll markers toggle option." + }, + "extension_option_scroll_markers_title": { + "message": "若启用,滚动标记将出现在页面的右边缘,指示文档中所有匹配项的位置。", + "description": "Tooltip text for the scroll markers option description icon." + }, "search_option_max_results_text": { "message": "最大高亮数量", "description": "Text displayed above the max highlighted results option." diff --git a/content/highlighter.js b/content/highlighter.js index 25f2d08..10660c0 100644 --- a/content/highlighter.js +++ b/content/highlighter.js @@ -18,6 +18,10 @@ Find.register('Content.Highlighter', function(self) { * @param {object} options - The search and highlight options * */ self.highlightAll = function(occurrenceMap, regex, options) { + if (options && options.scroll_markers) { + Find.Content.ScrollbarHighlightMaker.init(options); + } + const tags = { occIndex: null, maxIndex: null, @@ -185,6 +189,10 @@ Find.register('Content.Highlighter', function(self) { inMatch = charMap[key].matched; matchGroup.text += tags.openingMarkup; } + + if (options && options.scroll_markers) { + Find.Content.ScrollbarHighlightMaker.addOccurrence(occIndex, document.getElementById(matchGroup.groupUUID)); + } } else { if (inMatch) { inMatch = charMap[key].matched; @@ -217,6 +225,11 @@ Find.register('Content.Highlighter', function(self) { } } } + + if (options && options.scroll_markers) { + Find.Content.ScrollbarHighlightMaker.mount(); + Find.Content.ScrollbarHighlightMaker.createMarkers(); + } }; /** @@ -261,6 +274,10 @@ Find.register('Content.Highlighter', function(self) { window.scrollBy(0, -100); } } + + if (options.scroll_markers) { + Find.Content.ScrollbarHighlightMaker.setActive(index); + } }; /** @@ -331,6 +348,8 @@ Find.register('Content.Highlighter', function(self) { * @private * */ self.restore = function() { + Find.Content.ScrollbarHighlightMaker.destroy(); + let classes = [indexHighlight, allHighlight]; for (let classIndex = 0; classIndex < classes.length; classIndex++) { let els = Array.from(document.querySelectorAll('.' + classes[classIndex])); @@ -365,4 +384,4 @@ Find.register('Content.Highlighter', function(self) { return true; } -}); \ No newline at end of file +}); diff --git a/content/scrollbar.css b/content/scrollbar.css new file mode 100644 index 0000000..4f92937 --- /dev/null +++ b/content/scrollbar.css @@ -0,0 +1,87 @@ +/* + * Static presentational styles for the ScrollbarHighlightMaker overlay. + * + * IMPORTANT: this file must never contain the native-scrollbar suppression + * rules (::-webkit-scrollbar, scrollbar-width). Those rules are injected by + * content/scrollbar.js only while the overlay is mounted, and removed on + * destroy(). This file is loaded on every page at all times via the + * extension manifest, so anything placed here is permanently active + * regardless of whether find is in use. It is safe because every selector + * below only matches find-ext-* IDs/classes that only exist in the DOM + * while the overlay is mounted. + * + * Marker background colors are intentionally NOT set here, since they come + * from user-selected highlight color options and are applied inline by + * content/scrollbar.js at runtime. + */ + +#find-ext-scrollbar-track { + position: fixed; + top: 0; + right: 0; + height: 100vh; + z-index: 2147483647; + pointer-events: auto; + background: #f1f1f1; + box-sizing: border-box; + overflow: hidden; +} + +@media (prefers-color-scheme: dark) { + #find-ext-scrollbar-track { + background: #2b2b2b; + } +} + +#find-ext-scroll-thumb { + position: absolute; + right: 0; + width: 100%; + min-height: 30px; + background: #aaaaaa; + border-radius: 3px; + cursor: pointer; + box-sizing: border-box; + transition: background 0.15s; +} + +@media (prefers-color-scheme: dark) { + #find-ext-scroll-thumb { + background: #6b6b6b; + } +} + +#find-ext-scroll-thumb:hover { + background: #888888; +} + +#find-ext-scroll-marker-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.find-ext-scroll-marker { + display: block; + position: absolute; + left: 0; + right: 0; + width: 100%; + height: 4px; + min-height: 4px; + opacity: 0.85; + z-index: 2; + box-sizing: border-box; + pointer-events: none; + margin: 0; + padding: 0; + border: none; + border-radius: 1px; +} + +.find-ext-scroll-marker.find-ext-marker-active { + z-index: 3; +} diff --git a/content/scrollbar.js b/content/scrollbar.js new file mode 100644 index 0000000..b5b6374 --- /dev/null +++ b/content/scrollbar.js @@ -0,0 +1,309 @@ +'use strict'; + +/** + * Create the ScrollbarHighlightMaker namespace. This component renders a + * custom "fake" scrollbar overlay on the right edge of the page, with + * markers indicating the position of search occurrences in the document. + * + * Native browser scrollbars cannot be styled or overlaid directly by an + * extension, so this component hides the native scrollbar for the duration + * of the search and replaces it with a fully interactive substitute + * (draggable thumb, click-to-jump track) plus one marker per occurrence. + * */ +Find.register('Content.ScrollbarHighlightMaker', function (self) { + + const FALLBACK_SCROLLBAR_WIDTH = 13; + const MIN_THUMB_HEIGHT = 30; + const MARKER_HEIGHT = 4; + const FRAME_INTERVAL = 1000 / 60; // ~60 FPS + + let options = null; + let globalStyle = null; + let overlay = null; + let track = null; + let thumb = null; + let markerContainer = null; + let currentScrollY = 0; + let docInvisibleHeight = 0; + let scrollListener = null; + let scrollTicking = false; + + let occTopPositionMap = new Map(); + + /** + * Compute the native scrollbar width for this page. Falls back to a + * reasonable default when OS-level overlay scrollbars are in use, since + * those report a width of 0. + * + * @private + * @returns {number} The scrollbar width, in pixels + * */ + function getScrollbarWidth() { + const w = window.innerWidth - document.documentElement.clientWidth; + return w > 0 ? w : FALLBACK_SCROLLBAR_WIDTH; + } + + /** + * Build and inject the overlay DOM structure (native scrollbar + * suppression style, track, thumb, and marker container) into the page. + * + * The scrollbar-suppression rules are injected here as a JS-managed + *