diff --git a/src/content.mts b/src/content.mts index 308804b..a74038a 100644 --- a/src/content.mts +++ b/src/content.mts @@ -128,17 +128,6 @@ const startHighlighting = ( ); }; -/** - * Inserts a uniquely identified CSS stylesheet to perform all extension styling. - */ -const styleElementsInsert = () => { - if (!document.getElementById(EleID.DRAW_CONTAINER)) { - const container = document.createElement("div"); - container.id = EleID.DRAW_CONTAINER; - document.body.insertAdjacentElement("afterend", container); - } -}; - // TODO decompose this horrible generator function /** * Returns a generator function to consume individual command objects and produce their desired effect. @@ -254,7 +243,7 @@ interface TermAppender { } const termsOld: ReadonlyArray = [ ...terms ]; terms = termsNew; - styleManager.updateStyle(terms, termTokens, hues, highlighter); + styleManager.updateStyle(terms, termTokens, hues); updateToolbar(termsOld, terms, null, getToolbar(), commands); // Give the interface a chance to redraw before performing highlighting. setTimeout(() => { @@ -270,7 +259,7 @@ interface TermAppender { } else { terms = terms.slice(0, termIndex).concat(terms.slice(termIndex + 1)); } - styleManager.updateStyle(terms, termTokens, hues, highlighter); + styleManager.updateStyle(terms, termTokens, hues); updateToolbar(termsOld, terms, { term, termIndex }, getToolbar(), commands); // Give the interface a chance to redraw before performing highlighting. setTimeout(() => { @@ -280,7 +269,7 @@ interface TermAppender { appendTerm: term => { const termsOld: ReadonlyArray = [ ...terms ]; terms = terms.concat(term); - styleManager.updateStyle(terms, termTokens, hues, highlighter); + styleManager.updateStyle(terms, termTokens, hues); updateToolbar(termsOld, terms, { term, termIndex: termsOld.length }, getToolbar(), commands); // Give the interface a chance to redraw before performing highlighting. setTimeout(() => { @@ -351,7 +340,6 @@ interface TermAppender { if (queuingPromise) { await queuingPromise; } - styleElementsInsert(); if (message.highlighter !== undefined) { highlighter.removeEngine(); highlighter.signalPaintEngineMethod(message.highlighter.paintEngine.method); @@ -441,7 +429,8 @@ interface TermAppender { if (messageQueue.length === 1) { sendBackgroundMessage({ initializationGet: true }).then(initMessage => { if (!initMessage) { - assert(false, "not initialized, so highlighting remains inactive", "no init response was received"); + assert(false, "not initialized, so highlighting remains inactive", + "no init response was received"); return; } const initialize = () => { diff --git a/src/modules/common.mts b/src/modules/common.mts index a13616b..91c5b80 100644 --- a/src/modules/common.mts +++ b/src/modules/common.mts @@ -117,14 +117,11 @@ const compatibility = new Compatibility(); const [ Z_INDEX_MIN, Z_INDEX_MAX ] = [ -(2**31), 2**31 - 1 ]; enum EleID { - STYLE_PAINT = "markmysearch--style-paint", - STYLE_PAINT_SPECIAL = "markmysearch--style-paint-special", BAR = "markmysearch--bar", MARKER_GUTTER = "markmysearch--markers", DRAW_CONTAINER = "markmysearch--draw-container", - DRAW_ELEMENT = "markmysearch--draw", - ELEMENT_CONTAINER_SPECIAL = "markmysearch--element-container-special", - INPUT = "markmysearch--input", + DRAW_ELEMENT = "markmysearch--draw-", + INPUT = "markmysearch--input-", } enum EleClass { diff --git a/src/modules/highlight/engine-manager.mts b/src/modules/highlight/engine-manager.mts index 3ec26ee..fb68127 100644 --- a/src/modules/highlight/engine-manager.mts +++ b/src/modules/highlight/engine-manager.mts @@ -67,18 +67,6 @@ class EngineManager implements AbstractEngineManager { this.#updateTermStatus = updateTermStatus; } - readonly getCSS = { - misc: (): string => ( - this.#engineData?.engine.getCSS.misc() ?? "" - ), - termHighlights: (): string => ( - this.#engineData?.engine.getCSS.termHighlights() ?? "" - ), - termHighlight: (terms: ReadonlyArray, hues: ReadonlyArray, termIndex: number): string => ( - this.#engineData?.engine.getCSS.termHighlight(terms, hues, termIndex) ?? "" - ), - }; - getTermBackgroundStyle (colorA: string, colorB: string, cycle: number): string { return this.#engineData?.engine.getTermBackgroundStyle(colorA, colorB, cycle) ?? ""; } @@ -127,7 +115,7 @@ class EngineManager implements AbstractEngineManager { async setEngine (preference: Engine) { const highlighting = this.#highlighting; if (highlighting && this.#engineData) { - this.#engineData.engine.endHighlighting(); + this.#engineData.engine.deactivate(); } this.#engineData = await this.constructAndLinkEngineData(compatibility.highlighting.engineToUse(preference)); } @@ -232,7 +220,7 @@ class EngineManager implements AbstractEngineManager { removeEngine () { if (this.#highlighting && this.#engineData) { - this.#engineData.engine.endHighlighting(); + this.#engineData.engine.deactivate(); } this.#engineData = null; } diff --git a/src/modules/highlight/engine.mts b/src/modules/highlight/engine.mts index 22d12bd..178360e 100644 --- a/src/modules/highlight/engine.mts +++ b/src/modules/highlight/engine.mts @@ -16,6 +16,8 @@ interface AbstractEngine extends Highlighter { readonly terms: RContainer>; readonly hues: RContainer>; + readonly deactivate: () => void + readonly addHighlightingUpdatedListener: (listener: Generator) => void readonly getHighlightedElements: () => Iterable @@ -24,21 +26,9 @@ interface AbstractEngine extends Highlighter { interface Highlighter extends HighlighterCSSInterface, HighlightingInterface {} interface HighlighterCSSInterface { - readonly getCSS: EngineCSS - readonly getTermBackgroundStyle: (colorA: string, colorB: string, cycle: number) => string } -type EngineCSS = Readonly<{ - misc: () => string - termHighlights: () => string - termHighlight: ( - terms: ReadonlyArray, - hues: ReadonlyArray, - termIndex: number, - ) => string -}> - interface HighlightingInterface { /** * Removes previous highlighting, then highlights the document using the terms supplied. @@ -59,6 +49,6 @@ interface HighlightingInterface { export type { AbstractEngine, Highlighter, - HighlighterCSSInterface, EngineCSS, + HighlighterCSSInterface, HighlightingInterface, }; diff --git a/src/modules/highlight/engines/element.mts b/src/modules/highlight/engines/element.mts index 2ef8c67..05c89f2 100644 --- a/src/modules/highlight/engines/element.mts +++ b/src/modules/highlight/engines/element.mts @@ -10,7 +10,9 @@ import type { FlowMutationObserver } from "/dist/modules/highlight/flow-mutation import { highlightTags } from "/dist/modules/highlight/highlight-tags.mjs"; import * as TermCSS from "/dist/modules/highlight/term-css.mjs"; import type { MatchTerm, TermTokens, TermPatterns } from "/dist/modules/match-term.mjs"; -import { EleID, EleClass, AtRuleID, elementsPurgeClass, getTermClass, createContainer } from "/dist/modules/common.mjs"; +import { StyleManager } from "/dist/modules/style-manager.mjs"; +import { HTMLStylesheet } from "/dist/modules/stylesheets/html.mjs"; +import { EleID, EleClass, elementsPurgeClass, getTermClass, createContainer } from "/dist/modules/common.mjs"; class ElementEngine implements AbstractTreeEditEngine { readonly class = "ELEMENT"; @@ -23,6 +25,9 @@ class ElementEngine implements AbstractTreeEditEngine { readonly #elementsJustHighlighted = new Set(); + readonly #styleManager = new StyleManager(new HTMLStylesheet(document.head)); + readonly #termStyleManagerMap = new Map>>(); + readonly terms = createContainer>([]); readonly hues = createContainer>([]); @@ -32,6 +37,14 @@ class ElementEngine implements AbstractTreeEditEngine { ) { this.#termTokens = termTokens; this.#termPatterns = termPatterns; + this.#styleManager.setStyle(` +${HIGHLIGHT_TAG} { + font: inherit !important; + border-radius: 2px !important; + visibility: visible !important; +} +` + ); { const rejectSelector = Array.from(highlightTags.reject).join(", "); const elements = new Set(); @@ -108,33 +121,9 @@ class ElementEngine implements AbstractTreeEditEngine { } } - readonly getCSS = { - misc: () => "", - termHighlights: () => { - return (` -${HIGHLIGHT_TAG} { - font: inherit; - border-radius: 2px; - visibility: visible; -} -.${EleClass.FOCUS_CONTAINER} { - animation: ${AtRuleID.FLASH} 1s; -}` - ); - }, - termHighlight: (terms: ReadonlyArray, hues: ReadonlyArray, termIndex: number) => { - const term = terms[termIndex]; - const hue = hues[termIndex % hues.length]; - const cycle = Math.floor(termIndex / hues.length); - return (` -#${EleID.BAR} ~ body .${EleClass.FOCUS_CONTAINER} ${HIGHLIGHT_TAG}.${getTermClass(term, this.#termTokens)}, -#${EleID.BAR}.${EleClass.HIGHLIGHTS_SHOWN} ~ body ${HIGHLIGHT_TAG}.${getTermClass(term, this.#termTokens)} { - background: ${this.getTermBackgroundStyle(`hsl(${hue} 100% 60% / 0.4)`, `hsl(${hue} 100% 88% / 0.4)`, cycle)}; - box-shadow: 0 0 0 1px hsl(${hue} 100% 20% / 0.35); -}` - ); - }, - }; + deactivate () { + this.endHighlighting(); + } readonly getTermBackgroundStyle = TermCSS.getDiagonalStyle; @@ -144,10 +133,14 @@ ${HIGHLIGHT_TAG} { termsToPurge: ReadonlyArray, hues: ReadonlyArray, ) { + // Clean up. this.#flowMutations.unobserveMutations(); this.undoHighlights(termsToPurge); + this.removeTermStyles(); + // MAIN this.terms.assign(terms); this.hues.assign(hues); + this.addTermStyles(terms, hues); this.generateTermHighlightsUnderNode(termsToHighlight, document.body); this.#flowMutations.observeMutations(); } @@ -155,6 +148,7 @@ ${HIGHLIGHT_TAG} { endHighlighting () { this.#flowMutations.unobserveMutations(); this.undoHighlights(); + this.removeTermStyles(); } /** @@ -179,6 +173,37 @@ ${HIGHLIGHT_TAG} { elementsPurgeClass(EleClass.FOCUS, root); } + addTermStyles (terms: ReadonlyArray, hues: ReadonlyArray) { + for (let i = 0; i < terms.length; i++) { + const styleManager = new StyleManager(new HTMLStylesheet(document.head)); + styleManager.setStyle(this.getTermCSS(terms, hues, i)); + this.#termStyleManagerMap.set(terms[i], styleManager); + } + } + + removeTermStyles () { + for (const styleManager of this.#termStyleManagerMap.values()) { + styleManager.deactivate(); + } + this.#termStyleManagerMap.clear(); + } + + getTermCSS (terms: ReadonlyArray, hues: ReadonlyArray, termIndex: number) { + const term = terms[termIndex]; + const hue = hues[termIndex % hues.length]; + const cycle = Math.floor(termIndex / hues.length); + return ` +#${EleID.BAR} ~ body .${EleClass.FOCUS_CONTAINER} ${HIGHLIGHT_TAG}.${getTermClass(term, this.#termTokens)}, +#${EleID.BAR}.${EleClass.HIGHLIGHTS_SHOWN} ~ body ${HIGHLIGHT_TAG}.${getTermClass(term, this.#termTokens)} { + background: ${this.getTermBackgroundStyle( + `hsl(${hue} 100% 60% / 0.4)`, `hsl(${hue} 100% 88% / 0.4)`, cycle, + )} !important; + box-shadow: 0 0 0 1px hsl(${hue} 100% 20% / 0.35) !important; +} +` + ; + } + getHighlightedElements (): Iterable { return this.getHighlightedElementsForTerms(this.terms.current); } diff --git a/src/modules/highlight/engines/highlight.mts b/src/modules/highlight/engines/highlight.mts index 6560985..fd5d23d 100644 --- a/src/modules/highlight/engines/highlight.mts +++ b/src/modules/highlight/engines/highlight.mts @@ -8,7 +8,9 @@ import type { AbstractTreeCacheEngine } from "/dist/modules/highlight/models/tre import type { AbstractFlowTracker, Flow, Span } from "/dist/modules/highlight/models/tree-cache/flow-tracker.mjs"; import { FlowTracker } from "/dist/modules/highlight/models/tree-cache/flow-trackers/flow-tracker.mjs"; import * as TermCSS from "/dist/modules/highlight/term-css.mjs"; -import type { MatchTerm, TermTokens, TermPatterns } from "/dist/modules/match-term.mjs"; +import { MatchTerm, type TermTokens, type TermPatterns } from "/dist/modules/match-term.mjs"; +import { StyleManager } from "/dist/modules/style-manager.mjs"; +import { HTMLStylesheet } from "/dist/modules/stylesheets/html.mjs"; import { EleID, EleClass, createContainer, type AllReadonly } from "/dist/modules/common.mjs"; type HighlightStyle = Readonly<{ @@ -32,6 +34,8 @@ class HighlightEngine implements AbstractTreeCacheEngine { readonly #highlightingUpdatedListeners = new Set(); + readonly #termStyleManagerMap = new Map>>(); + readonly terms = createContainer>([]); readonly hues = createContainer>([]); @@ -70,29 +74,9 @@ class HighlightEngine implements AbstractTreeCacheEngine { this.#elementFlowsMap = this.#flowTracker.getElementFlowsMap(); } - readonly getCSS = { - misc: () => "", - termHighlights: () => "", - termHighlight: (terms: ReadonlyArray, hues: ReadonlyArray, termIndex: number) => { - const term = terms[termIndex]; - const hue = hues[termIndex % hues.length]; - const cycle = Math.floor(termIndex / hues.length); - const { - opacity, - lineThickness, - lineStyle, - textColor, - } = HighlightEngine.hueCycleStyles[Math.min(cycle, HighlightEngine.hueCycleStyles.length - 1)]; - return (` -#${EleID.BAR}.${EleClass.HIGHLIGHTS_SHOWN} ~ body ::highlight(${getName(this.#termTokens.get(term))}) { - background-color: hsl(${hue} 70% 70% / ${opacity}); - ${textColor ? `color: ${textColor};` : ""} - ${lineThickness ? `text-decoration: ${lineThickness}px hsl(${hue} 100% 35%) ${lineStyle} underline;` : ""} - ${lineThickness ? `text-decoration-skip-ink: none;` : ""} -}` - ); - }, - }; + deactivate () { + this.endHighlighting(); + } readonly getTermBackgroundStyle = TermCSS.getFlatStyle; @@ -105,9 +89,11 @@ class HighlightEngine implements AbstractTreeCacheEngine { // Clean up. this.#flowTracker.unobserveMutations(); this.undoHighlights(termsToPurge); + this.removeTermStyles(); // MAIN this.terms.assign(terms); this.hues.assign(hues); + this.addTermStyles(terms, hues); for (const term of terms) { this.#highlights.set(this.#termTokens.get(term), new ExtendedHighlight()); } @@ -118,6 +104,7 @@ class HighlightEngine implements AbstractTreeCacheEngine { endHighlighting () { this.#flowTracker.unobserveMutations(); this.undoHighlights(); + this.removeTermStyles(); } undoHighlights (terms?: ReadonlyArray) { @@ -131,6 +118,42 @@ class HighlightEngine implements AbstractTreeCacheEngine { } } + addTermStyles (terms: ReadonlyArray, hues: ReadonlyArray) { + for (let i = 0; i < terms.length; i++) { + const styleManager = new StyleManager(new HTMLStylesheet(document.head)); + styleManager.setStyle(this.getTermCSS(terms, hues, i)); + this.#termStyleManagerMap.set(terms[i], styleManager); + } + } + + removeTermStyles () { + for (const styleManager of this.#termStyleManagerMap.values()) { + styleManager.deactivate(); + } + this.#termStyleManagerMap.clear(); + } + + getTermCSS (terms: ReadonlyArray, hues: ReadonlyArray, termIndex: number) { + const term = terms[termIndex]; + const hue = hues[termIndex % hues.length]; + const cycle = Math.floor(termIndex / hues.length); + const { + opacity, + lineThickness, + lineStyle, + textColor, + } = HighlightEngine.hueCycleStyles[Math.min(cycle, HighlightEngine.hueCycleStyles.length - 1)]; + return (` +#${EleID.BAR}.${EleClass.HIGHLIGHTS_SHOWN} ~ body ::highlight(${getName(this.#termTokens.get(term))}) { + background-color: hsl(${hue} 70% 70% / ${opacity}) !important; + ${textColor ? `color: ${textColor} !important;` : ""} + ${lineThickness ? `text-decoration: ${lineThickness}px hsl(${hue} 100% 35%) ${lineStyle} underline !important;` : ""} + ${lineThickness ? `text-decoration-skip-ink: none !important;` : ""} +} +` + ); + } + getElementFlowsMap (): AllReadonly>> { return this.#elementFlowsMap; } diff --git a/src/modules/highlight/engines/paint.mts b/src/modules/highlight/engines/paint.mts index d8c2a38..6bdd125 100644 --- a/src/modules/highlight/engines/paint.mts +++ b/src/modules/highlight/engines/paint.mts @@ -9,11 +9,12 @@ import { getBoxesOwned } from "/dist/modules/highlight/engines/paint/boxes.mjs"; import type { AbstractTreeCacheEngine } from "/dist/modules/highlight/models/tree-cache.mjs"; import type { AbstractFlowTracker, Flow, Span } from "/dist/modules/highlight/models/tree-cache/flow-tracker.mjs"; import { FlowTracker } from "/dist/modules/highlight/models/tree-cache/flow-trackers/flow-tracker.mjs"; -import type { EngineCSS } from "/dist/modules/highlight/engine.mjs"; import { highlightTags } from "/dist/modules/highlight/highlight-tags.mjs"; import * as TermCSS from "/dist/modules/highlight/term-css.mjs"; import type { MatchTerm, TermTokens, TermPatterns } from "/dist/modules/match-term.mjs"; -import { EleID, createContainer, type PaintEngineMethod, type AllReadonly } from "/dist/modules/common.mjs"; +import { StyleManager } from "/dist/modules/style-manager.mjs"; +import { HTMLStylesheet } from "/dist/modules/stylesheets/html.mjs"; +import { createContainer, type PaintEngineMethod, type AllReadonly } from "/dist/modules/common.mjs"; type Box = Readonly<{ token: string @@ -60,6 +61,8 @@ class PaintEngine implements AbstractTreeCacheEngine, HighlightingStyleObserver readonly #highlightingStyleRuleDeletedListeners = new Set(); readonly #highlightingAppliedListeners = new Set(); + readonly #styleManager = new StyleManager(new HTMLStylesheet(document.head)); + readonly terms = createContainer>([]); readonly hues = createContainer>([]); @@ -111,7 +114,6 @@ class PaintEngine implements AbstractTreeCacheEngine, HighlightingStyleObserver }} })(); this.#method = method; - this.getCSS = method.getCSS; { const visibilityObserver = new IntersectionObserver(entries => { for (const entry of entries) { @@ -181,7 +183,11 @@ class PaintEngine implements AbstractTreeCacheEngine, HighlightingStyleObserver }} } - readonly getCSS: EngineCSS; + deactivate () { + this.endHighlighting(); + this.#method.deactivate(); + this.#styleManager.deactivate(); + } readonly getTermBackgroundStyle = TermCSS.getHorizontalStyle; @@ -197,6 +203,7 @@ class PaintEngine implements AbstractTreeCacheEngine, HighlightingStyleObserver // MAIN this.terms.assign(terms); this.hues.assign(hues); + this.#method.startHighlighting(terms, termsToHighlight, termsToPurge, hues); this.#flowTracker.generateHighlightSpansFor(terms, document.body); this.#flowTracker.observeMutations(); // TODO how are the currently-visible elements known and hence highlighted (when the engine has not been watching them)? @@ -215,6 +222,7 @@ class PaintEngine implements AbstractTreeCacheEngine, HighlightingStyleObserver this.unobserveVisibilityChanges(); this.#flowTracker.unobserveMutations(); this.#flowTracker.removeHighlightSpansFor(); + this.#method.endHighlighting(); // FIXME this should really be applied automatically and judiciously, and the stylesheet should be cleaned up with it for (const element of document.body.querySelectorAll("[markmysearch-h_id]")) { element.removeAttribute("markmysearch-h_id"); @@ -285,8 +293,7 @@ class PaintEngine implements AbstractTreeCacheEngine, HighlightingStyleObserver for (const listener of this.#highlightingAppliedListeners) { listener(this.#elementStyleRuleMap.keys()); } - const style = document.getElementById(EleID.STYLE_PAINT) as HTMLStyleElement; // NEXT 2 - style.textContent = Array.from(this.#elementStyleRuleMap.values()).join("\n"); + this.#styleManager.setStyle(Array.from(this.#elementStyleRuleMap.values()).join("\n")); } getElementFlowsMap (): AllReadonly>> { diff --git a/src/modules/highlight/engines/paint/method.mts b/src/modules/highlight/engines/paint/method.mts index d371be0..c28f824 100644 --- a/src/modules/highlight/engines/paint/method.mts +++ b/src/modules/highlight/engines/paint/method.mts @@ -6,13 +6,21 @@ import type { Box } from "/dist/modules/highlight/engines/paint.mjs"; import type { Highlightables } from "/dist/modules/highlight/engines/paint/highlightables.mjs"; -import type { EngineCSS } from "/dist/modules/highlight/engine.mjs"; import type { MatchTerm } from "/dist/modules/match-term.mjs"; interface AbstractMethod { - readonly highlightables?: Highlightables + readonly deactivate: () => void + + readonly startHighlighting: ( + terms: ReadonlyArray, + termsToHighlight: ReadonlyArray, + termsToPurge: ReadonlyArray, + hues: ReadonlyArray, + ) => void + + readonly endHighlighting: () => void - readonly getCSS: EngineCSS + readonly highlightables?: Highlightables /** * Gets a CSS rule to style all elements as per the enabled PAINT variant. diff --git a/src/modules/highlight/engines/paint/methods/element.mts b/src/modules/highlight/engines/paint/methods/element.mts index 8bcd84e..ec58034 100644 --- a/src/modules/highlight/engines/paint/methods/element.mts +++ b/src/modules/highlight/engines/paint/methods/element.mts @@ -7,9 +7,10 @@ import type { AbstractMethod } from "/dist/modules/highlight/engines/paint/method.mjs"; import { getBoxesOwned } from "/dist/modules/highlight/engines/paint/boxes.mjs"; import type { HighlightingStyleObserver, Flow, Span, Box } from "/dist/modules/highlight/engines/paint.mjs"; -import type { EngineCSS } from "/dist/modules/highlight/engine.mjs"; import * as TermCSS from "/dist/modules/highlight/term-css.mjs"; -import type { MatchTerm, TermTokens } from "/dist/modules/match-term.mjs"; +import { MatchTerm, type TermTokens } from "/dist/modules/match-term.mjs"; +import { StyleManager } from "/dist/modules/style-manager.mjs"; +import { HTMLStylesheet } from "/dist/modules/stylesheets/html.mjs"; import type { AllReadonly } from "/dist/modules/common.mjs"; import { Z_INDEX_MIN, EleID, EleClass, getTermClass, getTermTokenClass } from "/dist/modules/common.mjs"; @@ -20,6 +21,9 @@ class ElementMethod implements AbstractMethod { readonly #spanBoxesMap: AllReadonly>>; readonly #elementHighlightingIdMap: AllReadonly>; + readonly #styleManager = new StyleManager(new HTMLStylesheet(document.head)); + readonly #termStyleManagerMap = new Map>>(); + readonly #drawContainersParent: HTMLElement; readonly #elementDrawContainerMap = new Map(); constructor ( @@ -33,6 +37,32 @@ class ElementMethod implements AbstractMethod { this.#elementFlowsMap = elementFlowsMap; this.#spanBoxesMap = spanBoxesMap; this.#elementHighlightingIdMap = elementHighlightingIdMap; + this.#styleManager.setStyle(` +#${EleID.DRAW_CONTAINER} { + & { + position: fixed !important; + width: 100% !important; + height: 100% !important; + top: 100% !important; + z-index: ${Z_INDEX_MIN} !important; + } + & > * { + position: fixed !important; + width: 100% !important; + height: 100% !important; + } +} + +#${EleID.BAR}.${EleClass.HIGHLIGHTS_SHOWN} ~ #${EleID.DRAW_CONTAINER} .${EleClass.TERM} { + outline: 2px solid hsl(0 0% 0% / 0.1) !important; + outline-offset: -2px !important; + border-radius: 2px !important; +} +` + ); + this.#drawContainersParent = document.createElement("div"); + this.#drawContainersParent.id = EleID.DRAW_CONTAINER; + document.body.insertAdjacentElement("afterend", this.#drawContainersParent); const newlyStyledElements = new Set(); const newlyUnstyledElements = new Set(); styleObserver.addHighlightingStyleRuleChangedListener(element => { @@ -42,7 +72,6 @@ class ElementMethod implements AbstractMethod { newlyUnstyledElements.add(element); }); styleObserver.addHighlightingAppliedListener(() => { - const parent = document.getElementById(EleID.DRAW_CONTAINER)!; for (const element of newlyUnstyledElements) { this.#elementDrawContainerMap.get(element)?.remove(); this.#elementDrawContainerMap.delete(element); @@ -53,57 +82,61 @@ class ElementMethod implements AbstractMethod { const container = this.getDrawElementContainer(element); if (container) { this.#elementDrawContainerMap.set(element, container); - parent.appendChild(container); + this.#drawContainersParent.appendChild(container); } } }); } - readonly getCSS: EngineCSS = { - misc: () => { - return (` -#${EleID.DRAW_CONTAINER} { -& { - position: fixed; - width: 100%; - height: 100%; - top: 100%; - z-index: ${Z_INDEX_MIN}; -} -& > * { - position: fixed; - width: 100%; - height: 100%; -} -} + deactivate () { + this.endHighlighting(); + this.#drawContainersParent.remove(); + this.#styleManager.deactivate(); + } -#${EleID.BAR}.${EleClass.HIGHLIGHTS_SHOWN} ~ #${EleID.DRAW_CONTAINER} .${EleClass.TERM} { -outline: 2px solid hsl(0 0% 0% / 0.1); -outline-offset: -2px; -border-radius: 2px; -}` - ); - }, - termHighlights: () => "", - termHighlight: (terms: ReadonlyArray, hues: ReadonlyArray, termIndex: number) => { - const term = terms[termIndex]; - const hue = hues[termIndex % hues.length]; - const cycle = Math.floor(termIndex / hues.length); - const selector = `#${EleID.BAR}.${EleClass.HIGHLIGHTS_SHOWN} ~ #${EleID.DRAW_CONTAINER} .${ - getTermClass(term, this.#termTokens) - }`; - const backgroundStyle = TermCSS.getHorizontalStyle( - `hsl(${hue} 100% 60% / 0.4)`, - `hsl(${hue} 100% 88% / 0.4)`, - cycle, - ); - return`${selector} { background: ${backgroundStyle}; }`; - }, - }; + startHighlighting ( + terms: ReadonlyArray, + termsToHighlight: ReadonlyArray, + termsToPurge: ReadonlyArray, + hues: ReadonlyArray, + ) { + this.removeTermStyles(); + for (let i = 0; i < terms.length; i++) { + const styleManager = new StyleManager(new HTMLStylesheet(document.head)); + styleManager.setStyle(this.getTermCSS(terms, hues, i)); + this.#termStyleManagerMap.set(terms[i], styleManager); + } + } + + endHighlighting () { + this.removeTermStyles(); + } + + removeTermStyles () { + for (const styleManager of this.#termStyleManagerMap.values()) { + styleManager.deactivate(); + } + this.#termStyleManagerMap.clear(); + } + + getTermCSS (terms: ReadonlyArray, hues: ReadonlyArray, termIndex: number): string { + const term = terms[termIndex]; + const hue = hues[termIndex % hues.length]; + const cycle = Math.floor(termIndex / hues.length); + const selector = `#${EleID.BAR}.${EleClass.HIGHLIGHTS_SHOWN} ~ #${EleID.DRAW_CONTAINER} .${ + getTermClass(term, this.#termTokens) + }`; + const backgroundStyle = TermCSS.getHorizontalStyle( + `hsl(${hue} 100% 60% / 0.4)`, + `hsl(${hue} 100% 88% / 0.4)`, + cycle, + ); + return`${selector} { background: ${backgroundStyle} !important; }`; + } constructHighlightStyleRule (highlightingId: number) { return `body [markmysearch-h_id="${highlightingId}"] { background-image: -moz-element(#${ - EleID.DRAW_ELEMENT + "-" + highlightingId.toString() + EleID.DRAW_ELEMENT + highlightingId.toString() }) !important; background-repeat: no-repeat !important; }`; } @@ -124,7 +157,7 @@ border-radius: 2px; return null; } const container = document.createElement("div"); - container.id = EleID.DRAW_ELEMENT + "-" + highlightingId.toString(); + container.id = EleID.DRAW_ELEMENT + highlightingId.toString(); let boxRightmost = boxes[0]; let boxDownmost = boxes[0]; for (const box of boxes) { diff --git a/src/modules/highlight/engines/paint/methods/paint.mts b/src/modules/highlight/engines/paint/methods/paint.mts index d8c763e..f0a119f 100644 --- a/src/modules/highlight/engines/paint/methods/paint.mts +++ b/src/modules/highlight/engines/paint/methods/paint.mts @@ -7,8 +7,9 @@ import type { AbstractMethod } from "/dist/modules/highlight/engines/paint/method.mjs"; import type { Box } from "/dist/modules/highlight/engines/paint.mjs"; import type { Highlightables } from "/dist/modules/highlight/engines/paint/highlightables.mjs"; -import type { EngineCSS } from "/dist/modules/highlight/engine.mjs"; import type { MatchTerm, TermTokens } from "/dist/modules/match-term.mjs"; +import { StyleManager } from "/dist/modules/style-manager.mjs"; +import { HTMLStylesheet } from "/dist/modules/stylesheets/html.mjs"; import { EleID, EleClass } from "/dist/modules/common.mjs"; type TermSelectorStyles = Record, + termsToHighlight: ReadonlyArray, + termsToPurge: ReadonlyArray, + hues: ReadonlyArray, + ) { + this.#styleManager.setStyle(this.getTermsCSS(terms, hues)); + } + + endHighlighting () {} + + getTermsCSS (terms: ReadonlyArray, hues: ReadonlyArray) { + const styles: TermSelectorStyles = {}; + for (let i = 0; i < terms.length; i++) { + styles[this.#termTokens.get(terms[i])] = { + hue: hues[i % hues.length], + cycle: Math.floor(i / hues.length), + }; + } + return (` +#${EleID.BAR}.${EleClass.HIGHLIGHTS_SHOWN} ~ body [markmysearch-h_id] { + & [markmysearch-h_beneath] { + background-color: transparent !important; + } + & { + background-image: paint(markmysearch-highlights) !important; + --markmysearch-styles: ${JSON.stringify(styles)}; + } + & > :not([markmysearch-h_id]) { + --markmysearch-styles: unset; + --markmysearch-boxes: unset; + } +}` + ); + } + readonly highlightables: Highlightables = { isElementHighlightable (element: HTMLElement) { return !element.closest("a"); @@ -49,35 +93,6 @@ class PaintMethod implements AbstractMethod { }, }; - readonly getCSS: EngineCSS = { - misc: () => "", - termHighlights: () => "", - termHighlight: (terms: ReadonlyArray, hues: ReadonlyArray) => { - const styles: TermSelectorStyles = {}; - for (let i = 0; i < terms.length; i++) { - styles[this.#termTokens.get(terms[i])] = { - hue: hues[i % hues.length], - cycle: Math.floor(i / hues.length), - }; - } - return (` -#${EleID.BAR}.${EleClass.HIGHLIGHTS_SHOWN} ~ body [markmysearch-h_id] { -& [markmysearch-h_beneath] { - background-color: transparent; -} -& { - background-image: paint(markmysearch-highlights); - --markmysearch-styles: ${JSON.stringify(styles)}; -} -& > :not([markmysearch-h_id]) { - --markmysearch-styles: unset; - --markmysearch-boxes: unset; -} -}` - ); - }, - }; - constructHighlightStyleRule (highlightingId: number, boxes: ReadonlyArray) { return `body [markmysearch-h_id="${highlightingId}"] { --markmysearch-boxes: ${ JSON.stringify(boxes) diff --git a/src/modules/highlight/engines/paint/methods/url.mts b/src/modules/highlight/engines/paint/methods/url.mts index 92a3bbb..e229249 100644 --- a/src/modules/highlight/engines/paint/methods/url.mts +++ b/src/modules/highlight/engines/paint/methods/url.mts @@ -6,7 +6,6 @@ import type { AbstractMethod } from "/dist/modules/highlight/engines/paint/method.mjs"; import type { Box } from "/dist/modules/highlight/engines/paint.mjs"; -import type { EngineCSS } from "/dist/modules/highlight/engine.mjs"; import type { MatchTerm, TermTokens } from "/dist/modules/match-term.mjs"; import { EleID, EleClass } from "/dist/modules/common.mjs"; @@ -17,11 +16,11 @@ class UrlMethod implements AbstractMethod { this.#termTokens = termTokens; } - readonly getCSS: EngineCSS = { - misc: () => "", - termHighlights: () => "", - termHighlight: () => "", - }; + deactivate () {} + + startHighlighting () {} + + endHighlighting () {} constructHighlightStyleRule ( highlightingId: number, diff --git a/src/modules/highlight/models/tree-cache/term-markers/term-marker.mts b/src/modules/highlight/models/tree-cache/term-markers/term-marker.mts index 91d6f42..d462104 100644 --- a/src/modules/highlight/models/tree-cache/term-markers/term-marker.mts +++ b/src/modules/highlight/models/tree-cache/term-markers/term-marker.mts @@ -16,12 +16,21 @@ class TermMarker implements AbstractTermMarker { readonly #elementFlowsMap: AllReadonly>>; + readonly #scrollGutter: HTMLElement; + constructor ( termTokens: TermTokens, elementFlowsMap: AllReadonly>>, ) { this.#termTokens = termTokens; this.#elementFlowsMap = elementFlowsMap; + this.#scrollGutter = document.createElement("div"); + this.#scrollGutter.id = EleID.MARKER_GUTTER; + document.body.insertAdjacentElement("afterend", this.#scrollGutter); + } + + deactivate () { + this.#scrollGutter.remove(); } insert ( @@ -30,7 +39,6 @@ class TermMarker implements AbstractTermMarker { highlightedElements: Iterable, ) { const termsSet = new Set(terms); - const gutter = document.getElementById(EleID.MARKER_GUTTER)!; let markersHtml = ""; for (const element of highlightedElements) if (this.#elementFlowsMap.has(element)) { const highlightedTerms = new Set((this.#elementFlowsMap.get(element) ?? []).flatMap(flow => @@ -44,8 +52,8 @@ class TermMarker implements AbstractTermMarker { }" top="${yRelative}" style="top: ${yRelative * 100}%; padding-left: ${i * 5}px; z-index: ${i * -1}">`) .join(""); } - gutter.replaceChildren(); // Removes children, since inner HTML replacement does not for some reason - gutter.innerHTML = markersHtml; + this.#scrollGutter.replaceChildren(); // Removes children, since inner HTML replacement does not for some reason + this.#scrollGutter.innerHTML = markersHtml; } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/modules/highlight/models/tree-edit/term-markers/term-marker.mts b/src/modules/highlight/models/tree-edit/term-markers/term-marker.mts index dd4b5a1..d3d8c53 100644 --- a/src/modules/highlight/models/tree-edit/term-markers/term-marker.mts +++ b/src/modules/highlight/models/tree-edit/term-markers/term-marker.mts @@ -8,16 +8,24 @@ import type { AbstractTermMarker } from "/dist/modules/highlight/term-marker.mjs import { getContainerBlock } from "/dist/modules/highlight/container-blocks.mjs"; import type { MatchTerm, TermTokens } from "/dist/modules/match-term.mjs"; import { - EleID, EleClass, - getElementYRelative, elementsPurgeClass, + EleID, EleClass, getElementYRelative, elementsPurgeClass, getTermClass, getTermClassToken, } from "/dist/modules/common.mjs"; class TermMarker implements AbstractTermMarker { readonly #termTokens: TermTokens; + + readonly #scrollGutter: HTMLElement; constructor (termTokens: TermTokens) { this.#termTokens = termTokens; + this.#scrollGutter = document.createElement("div"); + this.#scrollGutter.id = EleID.MARKER_GUTTER; + document.body.insertAdjacentElement("afterend", this.#scrollGutter); + } + + deactivate () { + this.#scrollGutter.remove(); } insert ( @@ -26,7 +34,6 @@ class TermMarker implements AbstractTermMarker { highlightedElements: Iterable, ) { const regexMatchTermSelector = new RegExp(`\\b${EleClass.TERM}(?:-\\w+)+\\b`); - const gutter = document.getElementById(EleID.MARKER_GUTTER)!; const containersInfo: Array<{ container: HTMLElement termsAdded: Set @@ -56,16 +63,15 @@ class TermMarker implements AbstractTermMarker { } markersHtml += `
`; } - gutter.replaceChildren(); // Removes children, since inner HTML replacement does not for some reason - gutter.innerHTML = markersHtml; + this.#scrollGutter.replaceChildren(); // Removes children, since inner HTML replacement does not for some reason + this.#scrollGutter.innerHTML = markersHtml; } raise (term: MatchTerm | null, container: HTMLElement) { - const scrollMarkerGutter = document.getElementById(EleID.MARKER_GUTTER)!; - elementsPurgeClass(EleClass.FOCUS, scrollMarkerGutter); + elementsPurgeClass(EleClass.FOCUS, this.#scrollGutter); [6, 5, 4, 3, 2].some(precisionFactor => { const precision = 10**precisionFactor; - const scrollMarker = scrollMarkerGutter.querySelector( + const scrollMarker = this.#scrollGutter.querySelector( `${term ? `.${getTermClass(term, this.#termTokens)}` : ""}[top^="${ Math.trunc(getElementYRelative(container) * precision) / precision }"]` diff --git a/src/modules/highlight/special-engines/paint.mts b/src/modules/highlight/special-engines/paint.mts index 5e3bf3d..e8433b0 100644 --- a/src/modules/highlight/special-engines/paint.mts +++ b/src/modules/highlight/special-engines/paint.mts @@ -9,6 +9,8 @@ import type { Box } from "/dist/modules/highlight/engines/paint.mjs"; import { UrlMethod } from "/dist/modules/highlight/engines/paint/methods/url.mjs"; import { type BaseFlow, type BaseSpan, matchInText } from "/dist/modules/highlight/matcher.mjs"; import type { MatchTerm, TermTokens, TermPatterns } from "/dist/modules/match-term.mjs"; +import { StyleManager } from "/dist/modules/style-manager.mjs"; +import { HTMLStylesheet } from "/dist/modules/stylesheets/html.mjs"; import { EleID, EleClass, getElementTagsSet } from "/dist/modules/common.mjs"; type Flow = BaseFlow @@ -22,42 +24,52 @@ type HighlightContext = keyof typeof contextCSS type StyleRulesInfo = Record class PaintSpecialEngine implements AbstractSpecialEngine { - readonly termTokens: TermTokens; - readonly termPatterns: TermPatterns; + readonly #termTokens: TermTokens; + readonly #termPatterns: TermPatterns; - readonly method: UrlMethod; - terms: ReadonlyArray = []; - hues: ReadonlyArray = []; - readonly styleRules: StyleRulesInfo = { hovered: "", focused: "" }; + readonly #method: UrlMethod; + readonly #styleRules: StyleRulesInfo = { hovered: "", focused: "" }; - readonly elementsInfo = new Map unknown, set: (value: unknown) => unknown }> - }>(); + //readonly #elementsInfo = new Map unknown, set: (value: unknown) => unknown }> + //}>(); + + readonly #styleManager = new StyleManager(new HTMLStylesheet(document.head)); + readonly #elementContainer: HTMLElement; + + #terms: ReadonlyArray = []; + #hues: ReadonlyArray = []; constructor (termTokens: TermTokens, termPatterns: TermPatterns) { - this.termTokens = termTokens; - this.termPatterns = termPatterns; - this.method = new UrlMethod(termTokens); + this.#termTokens = termTokens; + this.#termPatterns = termPatterns; + this.#method = new UrlMethod(termTokens); + this.#elementContainer = document.createElement("div"); + document.body.insertAdjacentElement("afterend", this.#elementContainer); + } + + deactivate () { + this.endHighlighting(); + this.#styleManager.deactivate(); + this.#elementContainer.remove(); } startHighlighting (terms: ReadonlyArray, hues: ReadonlyArray) { // Clean up. this.endHighlighting(); // MAIN - this.terms = terms; - this.hues = hues; - this.insertHelperElements(); + this.#terms = terms; + this.#hues = hues; window.addEventListener("focusin", this.onFocusIn); window.addEventListener("pointerover", this.onHover); window.addEventListener("input", this.onInput); } endHighlighting () { - this.terms = []; + this.#terms = []; window.removeEventListener("focusin", this.onFocusIn); window.removeEventListener("pointerover", this.onHover); window.removeEventListener("input", this.onInput); - this.removeHelperElements(); } handledTags: Array = [ "input", "textarea" ]; @@ -65,23 +77,6 @@ class PaintSpecialEngine implements AbstractSpecialEngine { handledTagSet = getElementTagsSet(this.handledTags); // Handle