Fix #1933: make :find work on :viewsource and :reader

This is quite a big piece of work that moves :viewsource out
into its own tab and iframe, because Firefox can't find text
the old way. Then, we fix an iframe bug where :find was not
working across iframes, such as on :reader
This commit is contained in:
Oliver Blanthorn 2026-07-23 13:24:08 +02:00
parent 8dd6e14550
commit e7993f216c
No known key found for this signature in database
GPG key ID: 2BB8C36BB504BFF3
5 changed files with 48 additions and 64 deletions

View file

@ -49,7 +49,7 @@ class FindHighlight extends HTMLSpanElement {
}
static fromFindApi(found, allTextNode: Text[]) {
const range = document.createRange()
const range = allTextNode[0].ownerDocument.createRange()
range.setStart(allTextNode[found.startTextNodePos], found.startOffset)
range.setEnd(allTextNode[found.endTextNodePos], found.endOffset)
if (range.getClientRects().length < 1)
@ -146,6 +146,7 @@ class FindHighlight extends HTMLSpanElement {
queryInRange(selector: string): HTMLElement | null {
const range = this.range
const rangeEndNode = range.endContainer
if (range.startContainer.ownerDocument !== document) return null
// start and end of range is always text node because fromFindApi()
@ -186,6 +187,7 @@ customElements.define("find-highlight", FindHighlight, { extends: "span" })
const HIGHLIGHT_NAME = "tridactyl-find-highlight"
const ACTIVE_HIGHLIGHT_NAME = "tridactyl-find-highlight-active"
let nativeHighlights: { normal: Highlight; active: Highlight }
let nativeRegistry = CSS.highlights
function isHighlightVisible(highlight: FindHighlight) {
return DOM.isVisible(nativeHighlights ? highlight.range : highlight)
@ -199,18 +201,18 @@ function setNativeFocus(range: Range, active: boolean) {
function clearNativeHighlights() {
if (!nativeHighlights) return
if (CSS.highlights.get(HIGHLIGHT_NAME) === nativeHighlights.normal)
CSS.highlights.delete(HIGHLIGHT_NAME)
if (CSS.highlights.get(ACTIVE_HIGHLIGHT_NAME) === nativeHighlights.active)
CSS.highlights.delete(ACTIVE_HIGHLIGHT_NAME)
if (nativeRegistry.get(HIGHLIGHT_NAME) === nativeHighlights.normal)
nativeRegistry.delete(HIGHLIGHT_NAME)
if (nativeRegistry.get(ACTIVE_HIGHLIGHT_NAME) === nativeHighlights.active)
nativeRegistry.delete(ACTIVE_HIGHLIGHT_NAME)
nativeHighlights = undefined
}
function highlightsDrawn() {
if (!nativeHighlights) return !!host?.firstChild
return (
CSS.highlights.get(HIGHLIGHT_NAME) === nativeHighlights.normal &&
CSS.highlights.get(ACTIVE_HIGHLIGHT_NAME) === nativeHighlights.active &&
nativeRegistry.get(HIGHLIGHT_NAME) === nativeHighlights.normal &&
nativeRegistry.get(ACTIVE_HIGHLIGHT_NAME) === nativeHighlights.active &&
nativeHighlights.normal.size + nativeHighlights.active.size ===
lastHighlights.length
)
@ -260,25 +262,25 @@ export async function jumpToMatch(searchQuery, option) {
lastHighlights = []
removeHighlighting()
// We need to grab all text nodes in order to find the corresponding element
const walker = document.createTreeWalker(
document,
NodeFilter.SHOW_TEXT,
null,
)
const nodes = []
let node
do {
node = walker.nextNode()
nodes.push(node)
} while (node)
const documents = [document]
for (const frame of DOM.getAllDocumentFrames())
if (frame.contentDocument) documents.push(frame.contentDocument)
const nodeSets = documents.map(doc => {
const walker = doc.createTreeWalker(doc, NodeFilter.SHOW_TEXT)
const nodes = []
while (walker.nextNode()) nodes.push(walker.currentNode)
return nodes
})
for (let i = 0; i < results.count; ++i) {
const range = results.rangeData[i]
try {
const high = FindHighlight.fromFindApi(range, nodes)
lastHighlights.push(high)
} catch (_) {} // Inaccessible range, eg cross-origin iframe - ignore
for (const nodes of nodeSets) {
try {
const high = FindHighlight.fromFindApi(range, nodes)
lastHighlights.push(high)
break
} catch (_) {} // Inaccessible range, eg cross-origin iframe - ignore
}
}
if (lastHighlights.length < 1) {
throw new Error("Pattern not found: " + searchQuery)
@ -307,13 +309,16 @@ export async function jumpToMatch(searchQuery, option) {
function drawHighlights(highlights) {
if (NATIVE_HIGHLIGHTS) {
const normal = new Highlight()
const doc = highlights[0].range.startContainer.ownerDocument
const win: any = doc.defaultView
const normal = new win.Highlight()
highlights.forEach(highlight => normal.add(highlight.nativeRange))
const active = new Highlight()
const active = new win.Highlight()
nativeRegistry = win.CSS.highlights
normal.priority = 2147483646
active.priority = 2147483647
CSS.highlights.set(HIGHLIGHT_NAME, normal)
CSS.highlights.set(ACTIVE_HIGHLIGHT_NAME, active)
nativeRegistry.set(HIGHLIGHT_NAME, normal)
nativeRegistry.set(ACTIVE_HIGHLIGHT_NAME, active)
nativeHighlights = { normal, active }
return
}

View file

@ -1754,18 +1754,6 @@ export async function url2args() {
return UrlUtil.searchUrlToArgs(document.location.href, await config.getAsync("searchurls"))
}
/** @hidden */
//#content_helper
let sourceElement: Element
/** @hidden */
//#content_helper
function removeSource() {
if (sourceElement) {
sourceElement.remove()
sourceElement = undefined
}
}
/** Display the (HTML) source of the current page.
Behaviour can be changed by the 'viewsource' setting.
@ -1775,27 +1763,16 @@ function removeSource() {
Otherwise, the source of the current document will be displayed.
*/
//#content
export function viewsource(url = "") {
export async function viewsource(url = "") {
if (window.location.href.includes("static/reader.html?source#")) return tabclose()
if (url === "") url = window.location.href
if (config.get("viewsource") === "default") {
window.location.href = "view-source:" + url
return
}
if (!sourceElement) {
sourceElement = CommandLineContent.executeWithoutCommandLine(() => {
const pre = document.createElement("pre")
pre.id = "TridactylViewsourceElement"
pre.className = "cleanslate " + config.get("theme")
pre.innerText = document.documentElement.innerHTML
document.documentElement.appendChild(pre)
window.addEventListener("popstate", removeSource)
return pre
})
} else {
sourceElement.parentNode.removeChild(sourceElement)
sourceElement = undefined
window.removeEventListener("popstate", removeSource)
}
const pre = document.createElement("pre")
pre.textContent = CommandLineContent.executeWithoutCommandLine(() => document.documentElement.innerHTML)
return tabopen(await readerurl({ content: pre.outerHTML, link: url, source: true }))
}
/**
@ -6583,11 +6560,13 @@ import { Readability } from "@mozilla/readability"
* @hidden
*/
//#content_helper
export async function readerurl() {
document.querySelectorAll(".TridactylStatusIndicator").forEach(ind => ind.parentNode.removeChild(ind))
const article = new Readability(document.cloneNode(true) as any as Document).parse()
article["link"] = window.location.href
article["favicon"] = (await ownTab()).favIconUrl
export async function readerurl(article: any = undefined) {
if (!article) {
document.querySelectorAll(".TridactylStatusIndicator").forEach(ind => ind.parentNode.removeChild(ind))
article = new Readability(document.cloneNode(true) as any as Document).parse()
article["link"] = window.location.href
article["favicon"] = (await ownTab()).favIconUrl
}
let hash = ""
const article_encoded = btoa(encodeURIComponent(JSON.stringify(article)))
if (!(await browserBg.windows.getCurrent()).incognito) {
@ -6597,7 +6576,7 @@ export async function readerurl() {
} else {
hash = article_encoded
}
return browser.runtime.getURL("static/reader.html#" + hash)
return browser.runtime.getURL(`static/reader.html${article.source ? "?source" : ""}#${hash}`)
}
/**

View file

@ -289,7 +289,7 @@ export function isVisible(thing: Element | Range) {
return false
}
if (thing instanceof Range) return true
if ("startContainer" in thing) return true
const element = thing
// remove elements that are barely within the viewport, tiny, or invisible

View file

@ -83,7 +83,7 @@ async function updatePage() {
: staticThemes.includes(theme)
? `@import url("${browser.runtime.getURL("static/themes/" + theme + "/" + theme + ".css")}");`
: (await config.getAsync("customthemes", theme)) || ""
readerContent.srcdoc = `<!doctype html><html lang="en" class="TridactylOwnNamespace"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${readerCsp}"><link rel="stylesheet" href="${browser.runtime.getURL("static/css/reader.css")}"><style>${themeCss}</style></head><body id="tridactyl-reader" dir="auto">${headerHtml}<main>${article.content}</main></body></html>`
readerContent.srcdoc = `<!doctype html><html lang="en" class="TridactylOwnNamespace"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${readerCsp}"><link rel="stylesheet" href="${browser.runtime.getURL("static/css/" + (article.source ? "viewsource.css" : "reader.css"))}"><link rel="stylesheet" href="${browser.runtime.getURL("static/css/content.css")}"><style>${themeCss}</style></head><body id="tridactyl-reader" dir="auto">${headerHtml}<main>${article.content}</main></body></html>`
if (article.link !== undefined) {
const link =
(document.getElementById("tricanonlink") as HTMLLinkElement) ??

View file

@ -1,6 +1,6 @@
@import url("../themes/auto/auto.css");
#TridactylViewsourceElement {
pre {
position: absolute !important;
/* This is the z-index of hint.css and content.css -1 */
z-index: 2147483646 !important;