@fusion.dev/helix-core 0.218.0 → 0.220.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +1 -1
- package/dist/controllers/popover-controller/popover-controller.d.ts +27 -0
- package/dist/controllers/popover-controller/popover-controller.js +57 -0
- package/dist/controllers/toolbar-controller/toolbar-controller.js +5 -3
- package/dist/events/details.d.ts +7 -0
- package/dist/events/events.d.ts +6 -2
- package/dist/events/events.js +5 -1
- package/dist/events/found.d.ts +14 -0
- package/dist/events/found.js +11 -0
- package/dist/events/names.js +2 -2
- package/dist/events/stop.d.ts +12 -0
- package/dist/events/stop.js +6 -0
- package/dist/helix-element/helix-element.d.ts +19 -0
- package/dist/helix-element/helix-element.js +7 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/mixins/mixin-viewport/mixin-viewport.js +15 -7
- package/dist/utils/audit-children/audit-children-off.d.ts +2 -0
- package/dist/utils/audit-children/audit-children-off.js +1 -0
- package/dist/utils/audit-children/audit-children.d.ts +18 -3
- package/dist/utils/audit-children/audit-children.js +42 -9
- package/dist/utils/find-text/find-text.d.ts +101 -0
- package/dist/utils/find-text/find-text.js +330 -0
- package/dist/utils/toolbar-around/toolbar-around.d.ts +2 -1
- package/dist/utils/toolbar-around/toolbar-around.js +20 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long, in milliseconds, the page goes without loading a resource or
|
|
3
|
+
* registering a Helix family before a child still undefined counts as missing.
|
|
4
|
+
*
|
|
5
|
+
* **An app that imports its families with `import()` registers them after
|
|
6
|
+
* the page has loaded**, each as its chunk arrives, so a container can be
|
|
7
|
+
* defined and painted while the chunk holding its children is still in
|
|
8
|
+
* flight. Measured in the Hartley desktop app on October 14, 2026: Command was
|
|
9
|
+
* defined 90 ms before Option, and its families arrived over 230 ms.
|
|
10
|
+
*/
|
|
11
|
+
export declare const QUIET = 500;
|
|
1
12
|
/**
|
|
2
13
|
* Reports a child from another family that a container holds and nobody
|
|
3
14
|
* registered, naming the entry to import.
|
|
@@ -9,8 +20,10 @@
|
|
|
9
20
|
* This says so, once for each container tag and child tag on the page, with
|
|
10
21
|
* the line to add.
|
|
11
22
|
*
|
|
12
|
-
* Asked once the page has loaded and painted
|
|
13
|
-
*
|
|
23
|
+
* Asked once the page has loaded and painted. A child still undefined then
|
|
24
|
+
* is reported once the page has loaded and registered nothing for `QUIET`
|
|
25
|
+
* milliseconds and the tag is still undefined, so an entry a later module or
|
|
26
|
+
* a later chunk imports is not taken for a missing one.
|
|
14
27
|
*
|
|
15
28
|
* **In a development build only**, as `missing-name` is: every container would
|
|
16
29
|
* pay for a sentence meant for the person writing the page. Core imports it as
|
|
@@ -40,6 +53,8 @@ export declare const auditChildren: (host: HTMLElement, part: string, entry: str
|
|
|
40
53
|
* `attribute` is the one the page set, `entry` the entry after the tier's
|
|
41
54
|
* package, such as `menu/filter`, and `consequence` what the missing entry
|
|
42
55
|
* costs, after "so". `missing` is asked once the page has loaded and painted,
|
|
43
|
-
*
|
|
56
|
+
* and again once the page has loaded and registered nothing for `QUIET`
|
|
57
|
+
* milliseconds, so an entry a later module or a later chunk imports is not
|
|
58
|
+
* taken for a missing one.
|
|
44
59
|
*/
|
|
45
60
|
export declare const auditEntry: (host: HTMLElement, attribute: string, entry: string, consequence: string, missing: () => boolean) => void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { definedTags } from '../custom-element/custom-element.js';
|
|
1
2
|
import { howToRegister } from '../how-to-register/how-to-register.js';
|
|
2
3
|
import { reportRuntime } from '../report-runtime/report-runtime.js';
|
|
3
4
|
const reported = new Set();
|
|
@@ -12,6 +13,27 @@ const afterPaint = (check) => {
|
|
|
12
13
|
window.addEventListener('load', frame, { once: true });
|
|
13
14
|
}
|
|
14
15
|
};
|
|
16
|
+
export const QUIET = 500;
|
|
17
|
+
const activity = () => definedTags().length + (performance.getEntriesByType?.('resource').length ?? 0);
|
|
18
|
+
const whenQuiet = () => new Promise((resolve) => {
|
|
19
|
+
let seen = activity();
|
|
20
|
+
let periods = 0;
|
|
21
|
+
const wait = () => {
|
|
22
|
+
setTimeout(() => {
|
|
23
|
+
const now = activity();
|
|
24
|
+
periods += 1;
|
|
25
|
+
if (now === seen || periods >= 20) {
|
|
26
|
+
resolve();
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
seen = now;
|
|
30
|
+
wait();
|
|
31
|
+
}
|
|
32
|
+
}, QUIET);
|
|
33
|
+
};
|
|
34
|
+
wait();
|
|
35
|
+
});
|
|
36
|
+
const settled = (tag) => Promise.race([customElements.whenDefined(tag), whenQuiet()]);
|
|
15
37
|
export const auditChildren = (host, part, entry, consequence, drawn = false) => {
|
|
16
38
|
if (typeof window === 'undefined') {
|
|
17
39
|
return;
|
|
@@ -22,11 +44,16 @@ export const auditChildren = (host, part, entry, consequence, drawn = false) =>
|
|
|
22
44
|
if (reported.has(key)) {
|
|
23
45
|
return;
|
|
24
46
|
}
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
47
|
+
const fine = () => reported.has(key) ||
|
|
48
|
+
!host.isConnected ||
|
|
49
|
+
customElements.get(tag) !== undefined ||
|
|
50
|
+
(!drawn && host.querySelector(tag) === null);
|
|
51
|
+
const check = async () => {
|
|
52
|
+
if (fine()) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await settled(tag);
|
|
56
|
+
if (fine()) {
|
|
30
57
|
return;
|
|
31
58
|
}
|
|
32
59
|
reported.add(key);
|
|
@@ -36,7 +63,7 @@ export const auditChildren = (host, part, entry, consequence, drawn = false) =>
|
|
|
36
63
|
tag: host.localName,
|
|
37
64
|
});
|
|
38
65
|
};
|
|
39
|
-
afterPaint(check);
|
|
66
|
+
afterPaint(() => void check());
|
|
40
67
|
};
|
|
41
68
|
export const auditEntry = (host, attribute, entry, consequence, missing) => {
|
|
42
69
|
if (typeof window === 'undefined') {
|
|
@@ -46,8 +73,13 @@ export const auditEntry = (host, attribute, entry, consequence, missing) => {
|
|
|
46
73
|
if (reported.has(key)) {
|
|
47
74
|
return;
|
|
48
75
|
}
|
|
49
|
-
|
|
50
|
-
|
|
76
|
+
const fine = () => reported.has(key) || !host.isConnected || !missing();
|
|
77
|
+
const check = async () => {
|
|
78
|
+
if (fine()) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await whenQuiet();
|
|
82
|
+
if (fine()) {
|
|
51
83
|
return;
|
|
52
84
|
}
|
|
53
85
|
reported.add(key);
|
|
@@ -58,5 +90,6 @@ export const auditEntry = (host, attribute, entry, consequence, missing) => {
|
|
|
58
90
|
message: `<${host.localName}> has \`${attribute}\` and its entry is not imported, so ${consequence}. Import '${pkg}/${entry}'.`,
|
|
59
91
|
tag: host.localName,
|
|
60
92
|
});
|
|
61
|
-
}
|
|
93
|
+
};
|
|
94
|
+
afterPaint(() => void check());
|
|
62
95
|
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding words in what a page shows, as ranges over its text, and painting
|
|
3
|
+
* them with the Custom Highlight API: the query every finder takes, and the
|
|
4
|
+
* finder Find uses for the content its `for` names.
|
|
5
|
+
*
|
|
6
|
+
* **Painted, and nothing is inserted.** The text is the page's own light DOM,
|
|
7
|
+
* often written by React or by another Helix family, and a `<mark>` wrapped
|
|
8
|
+
* around a match would be undone by the next render or would break it, as
|
|
9
|
+
* `filter-marks` explains for a filtered list.
|
|
10
|
+
*/
|
|
11
|
+
/** What every match is painted as, in `CSS.highlights` and in `base.css`. */
|
|
12
|
+
export declare const FIND_MATCH = "helix-find-match";
|
|
13
|
+
/** What the current match is painted as, over the others. */
|
|
14
|
+
export declare const FIND_CURRENT = "helix-find-current";
|
|
15
|
+
/** The search Find hands a finder. */
|
|
16
|
+
export interface FindQuery {
|
|
17
|
+
/** Whether case has to match, so "Error" does not find "error". */
|
|
18
|
+
caseSensitive: boolean;
|
|
19
|
+
/** Whether `search` is a regular expression, in JavaScript's syntax. */
|
|
20
|
+
regex: boolean;
|
|
21
|
+
/** What Replace puts in place of a match. */
|
|
22
|
+
replace: string;
|
|
23
|
+
/** What the reader typed. */
|
|
24
|
+
search: string;
|
|
25
|
+
/** Whether a match has to be a whole word. */
|
|
26
|
+
wholeWord: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Where a search stands: how many matches, and which is current, counting from 0, or -1 for none. */
|
|
29
|
+
export interface FindResult {
|
|
30
|
+
count: number;
|
|
31
|
+
index: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Which match a search makes current. `first` stays on the current match
|
|
35
|
+
* where it still matches, as while the reader types, and otherwise takes the
|
|
36
|
+
* next one from there. `next` and `previous` move one match either way, and
|
|
37
|
+
* wrap around at the ends.
|
|
38
|
+
*/
|
|
39
|
+
export type FindDirection = 'first' | 'next' | 'previous';
|
|
40
|
+
/**
|
|
41
|
+
* Whatever searches content for Find: the text finder Find makes for the
|
|
42
|
+
* content `for` names, or one a frame or a page attaches, as Terminal's
|
|
43
|
+
* emulator and Code Editor's engine are.
|
|
44
|
+
*/
|
|
45
|
+
export interface Finder {
|
|
46
|
+
/** Takes away the marks a search drew, as Find closes or its field empties. */
|
|
47
|
+
end(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Finds `query`, marks every match, and makes one current by `direction`.
|
|
50
|
+
* Returns where the search stands, or `undefined` from a finder that reports
|
|
51
|
+
* it later through the sink `attach` returned, as one that searches in a
|
|
52
|
+
* worker does.
|
|
53
|
+
*/
|
|
54
|
+
find(query: FindQuery, direction: FindDirection): FindResult | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Replaces the current match, or every match with `all`, and returns where
|
|
57
|
+
* the search stands after. Find offers replacing only to a finder that has
|
|
58
|
+
* this.
|
|
59
|
+
*/
|
|
60
|
+
replace?(query: FindQuery, all: boolean): FindResult | undefined;
|
|
61
|
+
}
|
|
62
|
+
/** The pattern a query reads as, or nothing for an empty search or a regular expression that cannot be read. */
|
|
63
|
+
export declare const findPattern: (query: FindQuery) => RegExp | undefined;
|
|
64
|
+
/** Where a query matches in a text, as `[from, to]` offsets, with no empty match. */
|
|
65
|
+
export declare const findRanges: (text: string, query: FindQuery) => Array<[number, number]>;
|
|
66
|
+
/**
|
|
67
|
+
* Every place the text under `root` matches `query`, as ranges in the order
|
|
68
|
+
* they come. A match may run across the elements inside one block, as
|
|
69
|
+
* "**bold** words" does, and never from one block into the next.
|
|
70
|
+
*/
|
|
71
|
+
export declare const findTextRanges: (root: Element, query: FindQuery, skip?: string) => Range[];
|
|
72
|
+
/** What `textFinder` takes beside its root. */
|
|
73
|
+
export interface TextFinderOptions {
|
|
74
|
+
/**
|
|
75
|
+
* Called with where the search stands once the content has changed under
|
|
76
|
+
* it, as a log that keeps writing does. A search that `find` returned is
|
|
77
|
+
* not reported again here.
|
|
78
|
+
*/
|
|
79
|
+
found?: ((result: FindResult) => void) | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Elements under the root whose text is not searched, as a selector, such
|
|
82
|
+
* as line numbers or the buttons beside each message.
|
|
83
|
+
*/
|
|
84
|
+
skip?: string | undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A finder over the text a reader sees under `root`, in its light DOM and
|
|
88
|
+
* what is slotted there: Find's own, for the content its `for` names.
|
|
89
|
+
*
|
|
90
|
+
* Every match is painted as `helix-find-match`, and the current one as
|
|
91
|
+
* `helix-find-current`, by the Custom Highlight API. The first search starts
|
|
92
|
+
* at the first match in sight. The current match is scrolled into the middle
|
|
93
|
+
* of its scroller unless it is already in sight, and what hides it until found
|
|
94
|
+
* is opened first. While a search is open, a change to the content searches
|
|
95
|
+
* again once it has been still for 150ms, staying on the current match, and
|
|
96
|
+
* reports through `found`.
|
|
97
|
+
*
|
|
98
|
+
* Nothing is painted where the API is missing, as on a server or in a test
|
|
99
|
+
* DOM, and the count is still right there.
|
|
100
|
+
*/
|
|
101
|
+
export declare const textFinder: (root: Element, options?: TextFinderOptions) => Finder;
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
export const FIND_MATCH = 'helix-find-match';
|
|
2
|
+
export const FIND_CURRENT = 'helix-find-current';
|
|
3
|
+
export const findPattern = (query) => {
|
|
4
|
+
if (query.search === '') {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
const source = query.regex ? query.search : query.search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8
|
+
try {
|
|
9
|
+
return new RegExp(query.wholeWord ? `\\b(?:${source})\\b` : source, query.caseSensitive ? 'gu' : 'giu');
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export const findRanges = (text, query) => {
|
|
16
|
+
const pattern = findPattern(query);
|
|
17
|
+
if (!pattern) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
return [...text.matchAll(pattern)]
|
|
21
|
+
.filter((match) => match[0] !== '')
|
|
22
|
+
.map((match) => [match.index, match.index + match[0].length]);
|
|
23
|
+
};
|
|
24
|
+
const NEVER = 'script, style, template, noscript, textarea, select, [inert], [aria-hidden="true"], [hidden]:not([hidden="until-found"])';
|
|
25
|
+
const BETWEEN = '\u0000';
|
|
26
|
+
const flatParent = (node) => {
|
|
27
|
+
const slot = node.assignedSlot;
|
|
28
|
+
if (slot) {
|
|
29
|
+
return slot;
|
|
30
|
+
}
|
|
31
|
+
const parent = node.parentNode;
|
|
32
|
+
if (parent?.nodeType === Node.ELEMENT_NODE) {
|
|
33
|
+
return parent;
|
|
34
|
+
}
|
|
35
|
+
return parent !== null && 'host' in parent ? parent.host : null;
|
|
36
|
+
};
|
|
37
|
+
const revealers = (node) => {
|
|
38
|
+
const found = [];
|
|
39
|
+
let below = node;
|
|
40
|
+
for (let element = flatParent(node); element !== null; element = flatParent(element)) {
|
|
41
|
+
const closed = element.localName === 'details' &&
|
|
42
|
+
!element.open &&
|
|
43
|
+
!(below instanceof Element && below.localName === 'summary');
|
|
44
|
+
if (closed || element.getAttribute('hidden') === 'until-found') {
|
|
45
|
+
found.unshift(element);
|
|
46
|
+
}
|
|
47
|
+
below = element;
|
|
48
|
+
}
|
|
49
|
+
return found;
|
|
50
|
+
};
|
|
51
|
+
const seen = (element) => {
|
|
52
|
+
if (typeof element.checkVisibility !== 'function') {
|
|
53
|
+
for (let at = element; at !== null; at = flatParent(at)) {
|
|
54
|
+
const hidden = at.getAttribute('hidden');
|
|
55
|
+
if (hidden !== null && hidden !== 'until-found') {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (element.checkVisibility({ visibilityProperty: true })) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
const outer = revealers(element)[0];
|
|
65
|
+
return (outer && flatParent(outer)?.checkVisibility({ visibilityProperty: true })) ?? false;
|
|
66
|
+
};
|
|
67
|
+
const INLINE = /^(?:inline|contents|ruby|$)/;
|
|
68
|
+
const blockOf = (text, root, inline) => {
|
|
69
|
+
const view = root.ownerDocument.defaultView;
|
|
70
|
+
for (let element = text.parentElement; element !== null && element !== root; element = element.parentElement) {
|
|
71
|
+
let flows = inline.get(element);
|
|
72
|
+
if (flows === undefined) {
|
|
73
|
+
flows = view ? INLINE.test(view.getComputedStyle(element).display) : false;
|
|
74
|
+
inline.set(element, flows);
|
|
75
|
+
}
|
|
76
|
+
if (!flows) {
|
|
77
|
+
return element;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return root;
|
|
81
|
+
};
|
|
82
|
+
const textsOf = (root, skip) => {
|
|
83
|
+
const nodes = [];
|
|
84
|
+
let joined = '';
|
|
85
|
+
if (root.closest(NEVER)) {
|
|
86
|
+
return { joined, nodes };
|
|
87
|
+
}
|
|
88
|
+
const never = skip === '' ? NEVER : `${NEVER}, ${skip}`;
|
|
89
|
+
const shown = new Map();
|
|
90
|
+
const inline = new Map();
|
|
91
|
+
const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, {
|
|
92
|
+
acceptNode: (node) => {
|
|
93
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
94
|
+
const element = node;
|
|
95
|
+
if (element.matches(never)) {
|
|
96
|
+
return NodeFilter.FILTER_REJECT;
|
|
97
|
+
}
|
|
98
|
+
return element.localName === 'br' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
|
99
|
+
}
|
|
100
|
+
const parent = node.parentElement;
|
|
101
|
+
if (parent === null || node.nodeValue === '') {
|
|
102
|
+
return NodeFilter.FILTER_REJECT;
|
|
103
|
+
}
|
|
104
|
+
let visible = shown.get(parent);
|
|
105
|
+
if (visible === undefined) {
|
|
106
|
+
visible = seen(parent);
|
|
107
|
+
shown.set(parent, visible);
|
|
108
|
+
}
|
|
109
|
+
return visible ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
let block;
|
|
113
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
114
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
115
|
+
joined += BETWEEN;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const text = node;
|
|
119
|
+
const here = blockOf(text, root, inline);
|
|
120
|
+
if (block !== undefined && here !== block) {
|
|
121
|
+
joined += BETWEEN;
|
|
122
|
+
}
|
|
123
|
+
block = here;
|
|
124
|
+
nodes.push([text, joined.length]);
|
|
125
|
+
joined += text.data;
|
|
126
|
+
}
|
|
127
|
+
return { joined, nodes };
|
|
128
|
+
};
|
|
129
|
+
const placed = (nodes, at, end) => {
|
|
130
|
+
let low = 0;
|
|
131
|
+
let high = nodes.length - 1;
|
|
132
|
+
while (low < high) {
|
|
133
|
+
const middle = Math.ceil((low + high) / 2);
|
|
134
|
+
const from = nodes[middle]?.[1] ?? 0;
|
|
135
|
+
if (from < at || (!end && from === at)) {
|
|
136
|
+
low = middle;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
high = middle - 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const [node, from] = nodes[low];
|
|
143
|
+
return [node, at - from];
|
|
144
|
+
};
|
|
145
|
+
export const findTextRanges = (root, query, skip = '') => {
|
|
146
|
+
if (findPattern(query) === undefined) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
const { joined, nodes } = textsOf(root, skip);
|
|
150
|
+
return findRanges(joined, query)
|
|
151
|
+
.filter(([from, to]) => !joined.slice(from, to).includes(BETWEEN))
|
|
152
|
+
.map(([from, to]) => {
|
|
153
|
+
const range = root.ownerDocument.createRange();
|
|
154
|
+
range.setStart(...placed(nodes, from, false));
|
|
155
|
+
range.setEnd(...placed(nodes, to, true));
|
|
156
|
+
return range;
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
const painted = new Map();
|
|
160
|
+
const paint = (owner, ranges, current) => {
|
|
161
|
+
const registry = globalThis.CSS?.highlights;
|
|
162
|
+
if (registry === undefined) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (ranges.length === 0) {
|
|
166
|
+
painted.delete(owner);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
painted.set(owner, { current, others: ranges.filter((range) => range !== current) });
|
|
170
|
+
}
|
|
171
|
+
const entries = [...painted.values()];
|
|
172
|
+
const layers = [
|
|
173
|
+
[FIND_MATCH, entries.flatMap((entry) => entry.others), 0],
|
|
174
|
+
[FIND_CURRENT, entries.flatMap((entry) => (entry.current ? [entry.current] : [])), 1],
|
|
175
|
+
];
|
|
176
|
+
for (const [name, ranges, priority] of layers) {
|
|
177
|
+
if (ranges.length === 0) {
|
|
178
|
+
registry.delete(name);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const highlight = new Highlight(...ranges);
|
|
182
|
+
highlight.priority = priority;
|
|
183
|
+
registry.set(name, highlight);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const against = (range, anchor) => {
|
|
188
|
+
try {
|
|
189
|
+
return range.compareBoundaryPoints(Range.START_TO_START, anchor);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const firstInView = (ranges, root) => {
|
|
196
|
+
const top = Math.max(0, root.getBoundingClientRect().top);
|
|
197
|
+
const at = ranges.findIndex((range) => typeof range.getBoundingClientRect === 'function' &&
|
|
198
|
+
range.getBoundingClientRect().bottom > top);
|
|
199
|
+
return Math.max(at, 0);
|
|
200
|
+
};
|
|
201
|
+
const pick = (ranges, anchor, direction, root) => {
|
|
202
|
+
if (ranges.length === 0) {
|
|
203
|
+
return -1;
|
|
204
|
+
}
|
|
205
|
+
if (anchor === undefined) {
|
|
206
|
+
const first = firstInView(ranges, root);
|
|
207
|
+
return direction === 'previous' ? (first - 1 + ranges.length) % ranges.length : first;
|
|
208
|
+
}
|
|
209
|
+
const index = direction === 'previous'
|
|
210
|
+
? ranges.findLastIndex((range) => against(range, anchor) < 0)
|
|
211
|
+
: ranges.findIndex((range) => direction === 'next' ? against(range, anchor) > 0 : against(range, anchor) >= 0);
|
|
212
|
+
if (index !== -1) {
|
|
213
|
+
return index;
|
|
214
|
+
}
|
|
215
|
+
return direction === 'previous' ? ranges.length - 1 : 0;
|
|
216
|
+
};
|
|
217
|
+
const scrollerOf = (element) => {
|
|
218
|
+
const view = element.ownerDocument.defaultView;
|
|
219
|
+
for (let at = flatParent(element); at !== null; at = flatParent(at)) {
|
|
220
|
+
if (at === element.ownerDocument.documentElement || at === element.ownerDocument.body) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const overflow = view?.getComputedStyle(at).overflowY ?? '';
|
|
224
|
+
if (/auto|scroll/.test(overflow) && at.scrollHeight > at.clientHeight) {
|
|
225
|
+
return at;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
};
|
|
230
|
+
const bringIntoView = (range) => {
|
|
231
|
+
const element = range.startContainer.parentElement;
|
|
232
|
+
if (element === null || typeof range.getBoundingClientRect !== 'function') {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const scroller = scrollerOf(element);
|
|
236
|
+
const box = scroller?.getBoundingClientRect() ?? {
|
|
237
|
+
bottom: innerHeight,
|
|
238
|
+
left: 0,
|
|
239
|
+
right: innerWidth,
|
|
240
|
+
top: 0,
|
|
241
|
+
};
|
|
242
|
+
const rect = range.getBoundingClientRect();
|
|
243
|
+
if (rect.top >= box.top &&
|
|
244
|
+
rect.bottom <= box.bottom &&
|
|
245
|
+
rect.left >= box.left &&
|
|
246
|
+
rect.right <= box.right) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
element.scrollIntoView?.({ behavior: 'instant', block: 'center', inline: 'nearest' });
|
|
250
|
+
const after = range.getBoundingClientRect();
|
|
251
|
+
if (scroller && (after.top < box.top || after.bottom > box.bottom)) {
|
|
252
|
+
scroller.scrollTop += after.top + after.height / 2 - (box.top + (box.bottom - box.top) / 2);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
const reveal = (range) => {
|
|
256
|
+
const hiding = revealers(range.startContainer);
|
|
257
|
+
for (const element of hiding) {
|
|
258
|
+
if (element.localName === 'details') {
|
|
259
|
+
;
|
|
260
|
+
element.open = true;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
element.dispatchEvent(new Event('beforematch', { bubbles: true }));
|
|
264
|
+
if (element.getAttribute('hidden') === 'until-found') {
|
|
265
|
+
element.removeAttribute('hidden');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return hiding.length > 0;
|
|
270
|
+
};
|
|
271
|
+
const SETTLE = 150;
|
|
272
|
+
export const textFinder = (root, options = {}) => {
|
|
273
|
+
const owner = {};
|
|
274
|
+
let ranges = [];
|
|
275
|
+
let current;
|
|
276
|
+
let query;
|
|
277
|
+
let observer;
|
|
278
|
+
let settling;
|
|
279
|
+
const search = (direction, show) => {
|
|
280
|
+
ranges = query ? findTextRanges(root, query, options.skip ?? '') : [];
|
|
281
|
+
const index = pick(ranges, current, direction, root);
|
|
282
|
+
current = ranges[index];
|
|
283
|
+
paint(owner, ranges, current);
|
|
284
|
+
const shown = current;
|
|
285
|
+
if (shown && show) {
|
|
286
|
+
if (reveal(shown)) {
|
|
287
|
+
setTimeout(() => bringIntoView(shown));
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
bringIntoView(shown);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return { count: ranges.length, index };
|
|
294
|
+
};
|
|
295
|
+
const watch = () => {
|
|
296
|
+
if (observer !== undefined || typeof MutationObserver === 'undefined') {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
observer = new MutationObserver(() => {
|
|
300
|
+
clearTimeout(settling);
|
|
301
|
+
settling = setTimeout(() => {
|
|
302
|
+
if (query) {
|
|
303
|
+
options.found?.(search('first', false));
|
|
304
|
+
}
|
|
305
|
+
}, SETTLE);
|
|
306
|
+
});
|
|
307
|
+
observer.observe(root, {
|
|
308
|
+
attributeFilter: ['aria-hidden', 'hidden', 'inert'],
|
|
309
|
+
characterData: true,
|
|
310
|
+
childList: true,
|
|
311
|
+
subtree: true,
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
return {
|
|
315
|
+
end() {
|
|
316
|
+
observer?.disconnect();
|
|
317
|
+
observer = undefined;
|
|
318
|
+
clearTimeout(settling);
|
|
319
|
+
query = undefined;
|
|
320
|
+
ranges = [];
|
|
321
|
+
current = undefined;
|
|
322
|
+
paint(owner, ranges, current);
|
|
323
|
+
},
|
|
324
|
+
find(next, direction) {
|
|
325
|
+
query = next;
|
|
326
|
+
watch();
|
|
327
|
+
return search(direction, true);
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
};
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* import Toolbar's class; Toolbar adds its own class for a third tag.
|
|
9
9
|
*
|
|
10
10
|
* `closest` stops at a shadow root, which is what a slotted child wants: its
|
|
11
|
-
* toolbar is its light-DOM parent or above.
|
|
11
|
+
* toolbar is its light-DOM parent or above. It also stops at a panel, so a row
|
|
12
|
+
* in a menu opened from a toolbar is in no toolbar.
|
|
12
13
|
*/
|
|
13
14
|
export declare const toolbarAround: (element: Element) => Element | null;
|
|
14
15
|
/**
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
const TOOLBAR = '[role="toolbar"], helix-toolbar, helix-headless-toolbar';
|
|
2
|
-
|
|
2
|
+
const PANEL = [
|
|
3
|
+
'helix-dialog',
|
|
4
|
+
'helix-drawer',
|
|
5
|
+
'helix-headless-dialog',
|
|
6
|
+
'helix-headless-drawer',
|
|
7
|
+
'helix-headless-megamenu',
|
|
8
|
+
'helix-headless-menu',
|
|
9
|
+
'helix-headless-popover',
|
|
10
|
+
'helix-megamenu',
|
|
11
|
+
'helix-menu',
|
|
12
|
+
'helix-popover',
|
|
13
|
+
'[popover]',
|
|
14
|
+
'[role="dialog"]',
|
|
15
|
+
'[role="menu"]',
|
|
16
|
+
'dialog',
|
|
17
|
+
].join(', ');
|
|
18
|
+
export const toolbarAround = (element) => {
|
|
19
|
+
const found = element.parentElement?.closest(`${TOOLBAR}, ${PANEL}`);
|
|
20
|
+
return found?.matches(TOOLBAR) ? found : null;
|
|
21
|
+
};
|
|
3
22
|
export const toolbarAxis = (toolbar) => (toolbar.getAttribute('orientation') ?? toolbar.getAttribute('aria-orientation')) === 'vertical'
|
|
4
23
|
? 'vertical'
|
|
5
24
|
: 'horizontal';
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.220.0';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-core",
|
|
3
3
|
"description": "Helix element primitives: base classes, mixins, decorators, utilities.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.220.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -81,12 +81,12 @@
|
|
|
81
81
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
82
82
|
"dependencies": {
|
|
83
83
|
"@floating-ui/dom": "^1.8.0",
|
|
84
|
-
"@fusion.dev/helix-icons": "0.
|
|
84
|
+
"@fusion.dev/helix-icons": "0.220.0",
|
|
85
85
|
"tslib": "^2.8.1"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
|
-
"@fusion.dev/helix-cem": "0.
|
|
89
|
-
"@fusion.dev/helix-testing": "0.
|
|
88
|
+
"@fusion.dev/helix-cem": "0.220.0",
|
|
89
|
+
"@fusion.dev/helix-testing": "0.220.0",
|
|
90
90
|
"@lit-labs/ssr": "^4.1.0",
|
|
91
91
|
"lit": "^3.2.1"
|
|
92
92
|
},
|