Skip to content

Commit

Permalink
Merge pull request #180 from ator-dev/toolbar-isolated
Browse files Browse the repository at this point in the history
Put toolbar in shadow root, create StyleManager
  • Loading branch information
ator-dev authored Sep 4, 2024
2 parents c2a81d5 + 681d52f commit f5d78a2
Show file tree
Hide file tree
Showing 18 changed files with 540 additions and 296 deletions.
58 changes: 15 additions & 43 deletions src/content.mts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { sendBackgroundMessage } from "/dist/modules/messaging/background.mjs";
import { type MatchMode, MatchTerm, termEquals, TermTokens, TermPatterns } from "/dist/modules/match-term.mjs";
import { EleID } from "/dist/modules/common.mjs";
import { type AbstractEngineManager, EngineManager } from "/dist/modules/highlight/engine-manager.mjs";
import * as Stylesheet from "/dist/modules/interface/stylesheet.mjs";
import { Style } from "/dist/modules/style.mjs";
import { type AbstractToolbar, type ControlButtonName } from "/dist/modules/interface/toolbar.mjs";
import { Toolbar } from "/dist/modules/interface/toolbars/toolbar.mjs";
import { assert, itemsMatch } from "/dist/modules/common.mjs";
Expand All @@ -34,8 +34,8 @@ type ControlsInfo = {
highlightsShown: boolean
barCollapsed: boolean
termsOnHold: ReadonlyArray<MatchTerm>
barControlsShown: ConfigValues["barControlsShown"]
barLook: ConfigValues["barLook"]
barControlsShown: Readonly<ConfigValues["barControlsShown"]>
barLook: Readonly<ConfigValues["barLook"]>
matchMode: Readonly<MatchMode>
}

Expand All @@ -45,9 +45,10 @@ type ControlsInfo = {
* @returns `true` if focus was changed (i.e. it was in the toolbar), `false` otherwise.
*/
const focusReturnToDocument = (): boolean => {
const activeElement = document.activeElement;
if (activeElement instanceof HTMLInputElement && activeElement.closest(`#${EleID.BAR}`)) {
activeElement.blur();
const focus = document.activeElement;
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
if (focus instanceof HTMLElement && focus.id === EleID.BAR) {
focus.blur();
return true;
}
return false;
Expand Down Expand Up @@ -108,7 +109,7 @@ const updateToolbar = (
toolbar.replaceTerms(terms, commands);
}
toolbar.updateControlVisibility("replaceTerms");
toolbar.insertIntoDocument();
toolbar.insertAdjacentTo(document.body, "beforebegin");
};

const startHighlighting = (
Expand All @@ -117,12 +118,8 @@ const startHighlighting = (
highlighter: AbstractEngineManager,
hues: ReadonlyArray<number>,
) => {
const termsToHighlight: ReadonlyArray<MatchTerm> = terms.filter(term =>
!termsOld.find(termOld => termEquals(term, termOld))
);
const termsToPurge: ReadonlyArray<MatchTerm> = termsOld.filter(term =>
!terms.find(termOld => termEquals(term, termOld))
);
const termsToHighlight: ReadonlyArray<MatchTerm> = terms.filter(term => !termsOld.includes(term));
const termsToPurge: ReadonlyArray<MatchTerm> = termsOld.filter(termOld => !terms.includes(termOld));
highlighter.startHighlighting(
terms,
termsToHighlight,
Expand All @@ -135,36 +132,13 @@ const startHighlighting = (
* Inserts a uniquely identified CSS stylesheet to perform all extension styling.
*/
const styleElementsInsert = () => {
if (!document.getElementById(EleID.STYLE)) {
const style = document.createElement("style");
style.id = EleID.STYLE;
document.head.appendChild(style);
}
if (!document.getElementById(EleID.STYLE_PAINT)) {
const style = document.createElement("style");
style.id = EleID.STYLE_PAINT;
document.head.appendChild(style);
}
if (!document.getElementById(EleID.DRAW_CONTAINER)) {
const container = document.createElement("div");
container.id = EleID.DRAW_CONTAINER;
document.body.insertAdjacentElement("afterend", container);
}
};

const styleElementsCleanup = () => {
const style = document.getElementById(EleID.STYLE);
if (style && style.textContent !== "") {
style.textContent = "";
}
const stylePaint = document.getElementById(EleID.STYLE_PAINT);
if (stylePaint instanceof HTMLStyleElement && stylePaint.sheet) {
while (stylePaint.sheet.cssRules.length) {
stylePaint.sheet.deleteRule(0);
}
}
};

// TODO decompose this horrible generator function
/**
* Returns a generator function to consume individual command objects and produce their desired effect.
Expand Down Expand Up @@ -236,8 +210,6 @@ interface TermAppender<Async = true> {
}

(() => {
// Can't remove controls because a script may be left behind from the last install, and start producing unhandled errors. FIXME
//controlsRemove();
const commands: BrowserCommands = [];
let terms: ReadonlyArray<MatchTerm> = [];
let hues: ReadonlyArray<number> = [];
Expand Down Expand Up @@ -274,14 +246,15 @@ interface TermAppender<Async = true> {
};
const updateTermStatus = (term: MatchTerm) => getToolbarOrNull()?.updateTermStatus(term);
const highlighter: AbstractEngineManager = new EngineManager(updateTermStatus, termTokens, termPatterns);
const styleManager = new Style();
const termSetterInternal: TermSetter<false> = {
setTerms: termsNew => {
if (itemsMatch(terms, termsNew, termEquals)) {
return;
}
const termsOld: ReadonlyArray<MatchTerm> = [ ...terms ];
terms = termsNew;
Stylesheet.fillContent(terms, termTokens, hues, controlsInfo.barLook, highlighter);
styleManager.updateStyle(terms, termTokens, hues, highlighter);
updateToolbar(termsOld, terms, null, getToolbar(), commands);
// Give the interface a chance to redraw before performing highlighting.
setTimeout(() => {
Expand All @@ -297,7 +270,7 @@ interface TermAppender<Async = true> {
} else {
terms = terms.slice(0, termIndex).concat(terms.slice(termIndex + 1));
}
Stylesheet.fillContent(terms, termTokens, hues, controlsInfo.barLook, highlighter);
styleManager.updateStyle(terms, termTokens, hues, highlighter);
updateToolbar(termsOld, terms, { term, termIndex }, getToolbar(), commands);
// Give the interface a chance to redraw before performing highlighting.
setTimeout(() => {
Expand All @@ -307,7 +280,7 @@ interface TermAppender<Async = true> {
appendTerm: term => {
const termsOld: ReadonlyArray<MatchTerm> = [ ...terms ];
terms = terms.concat(term);
Stylesheet.fillContent(terms, termTokens, hues, controlsInfo.barLook, highlighter);
styleManager.updateStyle(terms, termTokens, hues, highlighter);
updateToolbar(termsOld, terms, { term, termIndex: termsOld.length }, getToolbar(), commands);
// Give the interface a chance to redraw before performing highlighting.
setTimeout(() => {
Expand Down Expand Up @@ -343,7 +316,7 @@ interface TermAppender<Async = true> {
getToolbar: () => {
if (!toolbar) {
toolbar = new Toolbar([],
commands, hues,
hues, commands,
controlsInfo,
termSetter, doPhrasesMatchTerms,
termTokens, highlighter,
Expand Down Expand Up @@ -426,7 +399,6 @@ interface TermAppender<Async = true> {
highlighter.endHighlighting();
terms = [];
getToolbarOrNull()?.remove();
styleElementsCleanup();
}
if (message.terms) {
// TODO make sure same MatchTerm objects are used for terms which are equivalent
Expand Down
84 changes: 23 additions & 61 deletions src/modules/common.mts
Original file line number Diff line number Diff line change
Expand Up @@ -116,69 +116,31 @@ const compatibility = new Compatibility();

const [ Z_INDEX_MIN, Z_INDEX_MAX ] = [ -(2**31), 2**31 - 1 ];

const EleID = (() => {
const wrap = (name: string) => "markmysearch--" + name;
return {
STYLE: wrap("style"),
STYLE_PAINT: wrap("style-paint"),
STYLE_PAINT_SPECIAL: wrap("style-paint-special"),
BAR: wrap("bar"),
BAR_LEFT: wrap("bar-left"),
BAR_TERMS: wrap("bar-terms"),
BAR_RIGHT: wrap("bar-right"),
MARKER_GUTTER: wrap("markers"),
DRAW_CONTAINER: wrap("draw-container"),
DRAW_ELEMENT: wrap("draw"),
ELEMENT_CONTAINER_SPECIAL: wrap("element-container-special"),
INPUT: wrap("input"),
} as const;
})();
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",
}

const EleClass = (() => {
const wrap = (name: string) => "mms--" + name;
return {
HIGHLIGHTS_SHOWN: wrap("highlights-shown"),
BAR_HIDDEN: wrap("bar-hidden"),
BAR_NO_AUTOFOCUS: wrap("bar-no-autofocus"),
CONTROL: wrap("control"),
CONTROL_PAD: wrap("control-pad"),
CONTROL_INPUT: wrap("control-input"),
CONTROL_CONTENT: wrap("control-content"),
CONTROL_BUTTON: wrap("control-button"),
CONTROL_REVEAL: wrap("control-reveal"),
CONTROL_EDIT: wrap("control-edit"),
OPTION_LIST: wrap("options"),
OPTION: wrap("option"),
OPTION_LIST_PULLDOWN: wrap("options-pulldown"),
TERM: wrap("term"),
FOCUS: wrap("focus"),
FOCUS_CONTAINER: wrap("focus-contain"),
FOCUS_REVERT: wrap("focus-revert"),
REMOVE: wrap("remove"),
DISABLED: wrap("disabled"),
LAST_FOCUSED: wrap("last-focused"),
MENU_OPEN: wrap("menu-open"),
COLLAPSED: wrap("collapsed"),
UNCOLLAPSIBLE: wrap("collapsed-impossible"),
MATCH_REGEX: wrap("match-regex"),
MATCH_CASE: wrap("match-case"),
MATCH_STEM: wrap("match-stem"),
MATCH_WHOLE: wrap("match-whole"),
MATCH_DIACRITICS: wrap("match-diacritics"),
PRIMARY: wrap("primary"),
SECONDARY: wrap("secondary"),
BAR_CONTROLS: wrap("bar-controls"),
} as const;
})();
enum EleClass {
HIGHLIGHTS_SHOWN = "mms--highlights-shown",
TERM = "mms--term",
FOCUS = "mms--focus",
FOCUS_CONTAINER = "mms--focus-contain",
FOCUS_REVERT = "mms--focus-revert",
REMOVE = "mms--remove",
}

const AtRuleID = (() => {
const wrap = (name: string) => "markmysearch--" + name;
return {
FLASH: wrap("flash"),
MARKER_ON: wrap("marker-on"),
MARKER_OFF: wrap("marker-off"),
} as const;
})();
enum AtRuleID {
FLASH = "markmysearch--flash",
MARKER_ON = "markmysearch--marker-on",
MARKER_OFF = "markmysearch--marker-off",
}

/**
* Transforms an array of lowercase element tags into a set of lowercase and uppercase tags.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ class TermWalker implements AbstractTermWalker {
const nodeBegin = reverse ? getNodeFinal(document.body) : document.body;
const nodeSelected = getSelection()?.anchorNode;
const nodeFocused = document.activeElement
? (document.activeElement === document.body || document.activeElement.closest(`${EleID.BAR}`))
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
? (document.activeElement === document.body || document.activeElement.id === EleID.BAR)
? null
: document.activeElement
: null;
Expand Down
24 changes: 13 additions & 11 deletions src/modules/highlight/models/tree-edit/term-walkers/term-walker.mts
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,17 @@ class TermWalker implements AbstractTermWalker {
const focusContainer = document.body
.getElementsByClassName(EleClass.FOCUS_CONTAINER)[0] as HTMLElement;
const selection = document.getSelection();
const activeElement = document.activeElement;
if (activeElement instanceof HTMLInputElement && activeElement.closest(`#${EleID.BAR}`)) {
activeElement.blur();
const focus = document.activeElement;
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
if (focus instanceof HTMLElement && focus.id === EleID.BAR) {
focus.blur();
}
const selectionFocus = selection && (!activeElement
|| activeElement === document.body || !document.body.contains(activeElement)
|| activeElement === focusBase || activeElement.contains(focusContainer)
const selectionFocus = selection && (!focus
|| focus === document.body || !document.body.contains(focus)
|| focus === focusBase || focus.contains(focusContainer)
)
? selection.focusNode
: activeElement ?? document.body;
: focus ?? document.body;
if (focusBase) {
focusBase.classList.remove(EleClass.FOCUS);
elementsPurgeClass(EleClass.FOCUS_CONTAINER);
Expand Down Expand Up @@ -183,17 +184,18 @@ class TermWalker implements AbstractTermWalker {
elementsPurgeClass(EleClass.FOCUS_CONTAINER);
elementsPurgeClass(EleClass.FOCUS);
const selection = getSelection();
const bar = document.getElementById(EleID.BAR);
if (!selection || !bar) {
if (!selection) {
return null;
}
if (document.activeElement instanceof HTMLElement && bar.contains(document.activeElement)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
if (document.activeElement instanceof HTMLElement && document.activeElement.id === EleID.BAR) {
document.activeElement.blur();
}
const nodeBegin = reverse ? getNodeFinal(document.body) : document.body;
const nodeSelected = reverse ? selection.anchorNode : selection.focusNode;
const nodeFocused = document.activeElement instanceof HTMLElement
? (document.activeElement === document.body || bar.contains(document.activeElement))
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
? (document.activeElement === document.body || document.activeElement.id === EleID.BAR)
? null
: document.activeElement
: null;
Expand Down
8 changes: 6 additions & 2 deletions src/modules/interface/toolbar.mts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@ interface AbstractToolbar {
readonly updateControlVisibility: (controlName: ControlButtonName) => void

/**
* Inserts the toolbar and appropriate controls.
* Inserts the toolbar as a child or sibling of an element.
* @param element The element into which to insert the toolbar.
* @param position The target position, adjacent to the element provided.
* "beforebegin" / "afterend" = previous/next sibling;
* "afterbegin" / "beforeend" = first/last child.
*/
readonly insertIntoDocument: () => void
readonly insertAdjacentTo: (element: HTMLElement, position: InsertPosition) => void

/**
* Removes the toolbar and appropriate controls.
Expand Down
40 changes: 38 additions & 2 deletions src/modules/interface/toolbar/common.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import type { ControlButtonName } from "/dist/modules/interface/toolbar.mjs";
import { type CommandInfo, parseCommand } from "/dist/modules/commands.mjs";
import type { MatchMode } from "/dist/modules/match-term.mjs";
import { EleID, EleClass } from "/dist/modules/common.mjs";
import { EleID as CommonEleID } from "/dist/modules/common.mjs";
import type { ControlsInfo } from "/dist/content.mjs";
import { getIdSequential } from "/dist/modules/common.mjs";

Expand All @@ -21,6 +21,41 @@ type ControlFocusArea = (
| "options_menu"
)

enum EleID {
BAR = "bar",
BAR_LEFT = "bar-left",
BAR_TERMS = "bar-terms",
BAR_RIGHT = "bar-right",
}

enum EleClass {
BAR_HIDDEN = "bar-hidden",
BAR_NO_AUTOFOCUS = "bar-no-autofocus",
CONTROL = "control",
CONTROL_PAD = "control-pad",
CONTROL_INPUT = "control-input",
CONTROL_CONTENT = "control-content",
CONTROL_BUTTON = "control-button",
CONTROL_REVEAL = "control-reveal",
CONTROL_EDIT = "control-edit",
OPTION_LIST = "options",
OPTION = "option",
OPTION_LIST_PULLDOWN = "options-pulldown",
DISABLED = "disabled",
LAST_FOCUSED = "last-focused",
MENU_OPEN = "menu-open",
COLLAPSED = "collapsed",
UNCOLLAPSIBLE = "collapsed-impossible",
MATCH_REGEX = "match-regex",
MATCH_CASE = "match-case",
MATCH_STEM = "match-stem",
MATCH_WHOLE = "match-whole",
MATCH_DIACRITICS = "match-diacritics",
PRIMARY = "primary",
SECONDARY = "secondary",
BAR_CONTROLS = "bar-controls",
}

/**
* Extracts assigned shortcut strings from browser commands.
* @param commands Commands as returned by the browser.
Expand Down Expand Up @@ -64,7 +99,7 @@ const applyMatchModeToClassList = (
classListToggle(EleClass.MATCH_DIACRITICS, matchMode.diacritics);
};

const getInputIdSequential = () => EleID.INPUT + "-" + getIdSequential.next().value.toString();
const getInputIdSequential = () => CommonEleID.INPUT + "-" + getIdSequential.next().value.toString();

const getControlClass = (controlName: ControlButtonName) => EleClass.CONTROL + "-" + controlName;

Expand All @@ -76,6 +111,7 @@ export {
type BarLook,
type BrowserCommands,
type ControlFocusArea,
EleID, EleClass,
getTermCommands,
getMatchModeOptionClass, getMatchModeFromClassList, applyMatchModeToClassList,
getInputIdSequential,
Expand Down
Loading

0 comments on commit f5d78a2

Please sign in to comment.