mount-observer 0.1.4 → 0.1.6
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/DefineCustomElementHandler.js +37 -3
- package/DefineCustomElementHandler.ts +43 -4
- package/ElementMountExtension.js +49 -0
- package/ElementMountExtension.ts +65 -0
- package/Events.js +18 -29
- package/Events.ts +7 -16
- package/EvtRt.js +14 -14
- package/EvtRt.ts +15 -15
- package/MountObserver.js +240 -111
- package/MountObserver.ts +283 -131
- package/README.md +667 -447
- package/emitEvents.js +13 -13
- package/emitEvents.ts +14 -14
- package/getRootRegistryContainer.js +49 -0
- package/getRootRegistryContainer.ts +56 -0
- package/index.js +4 -4
- package/index.ts +4 -5
- package/loadImports.js +1 -1
- package/loadImports.ts +1 -1
- package/mediaQuery.js +5 -5
- package/mediaQuery.ts +7 -7
- package/package.json +5 -9
- package/playwright.config.ts +8 -8
- package/types.d.ts +22 -46
- package/{whereOutside.js → withScopePerimeter.js} +1 -1
- package/{whereOutside.ts → withScopePerimeter.ts} +1 -1
- package/attrChanges.js +0 -70
- package/attrChanges.ts +0 -90
- package/attrCoordinates.js +0 -93
- package/attrCoordinates.ts +0 -122
- package/whereAttr.js +0 -174
- package/whereAttr.ts +0 -221
package/attrChanges.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import type { AttrChange, MountInit } from './types.d.ts';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Checks for attribute changes on a mounted element.
|
|
5
|
-
* This module is dynamically loaded only when whereAttr is configured.
|
|
6
|
-
*/
|
|
7
|
-
export function checkAttrChanges(
|
|
8
|
-
element: Element,
|
|
9
|
-
mountInit: MountInit,
|
|
10
|
-
buildAttrCoordinateMapFn: (whereAttr: any, isCustomElement: boolean) => any,
|
|
11
|
-
elementAttrStates: WeakMap<Element, Map<string, string | null>>,
|
|
12
|
-
elementOnceAttrs: WeakMap<Element, Set<string>>
|
|
13
|
-
): AttrChange[] {
|
|
14
|
-
if (!mountInit.whereAttr || !buildAttrCoordinateMapFn) {
|
|
15
|
-
return [];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const isCustomElement = element.tagName.toLowerCase().includes('-');
|
|
19
|
-
const attrCoordMap = buildAttrCoordinateMapFn(mountInit.whereAttr, isCustomElement);
|
|
20
|
-
|
|
21
|
-
// Get or create the attribute state for this element
|
|
22
|
-
let attrState = elementAttrStates.get(element);
|
|
23
|
-
if (!attrState) {
|
|
24
|
-
attrState = new Map<string, string | null>();
|
|
25
|
-
elementAttrStates.set(element, attrState);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const changes: AttrChange[] = [];
|
|
29
|
-
const currentAttrs = new Set<string>();
|
|
30
|
-
|
|
31
|
-
// Check all possible attributes from the coordinate map
|
|
32
|
-
for (const attrName of Object.keys(attrCoordMap)) {
|
|
33
|
-
const coordinate = attrCoordMap[attrName];
|
|
34
|
-
const currentValue = element.getAttribute(attrName);
|
|
35
|
-
const previousValue = attrState.get(attrName);
|
|
36
|
-
|
|
37
|
-
if (currentValue !== null) {
|
|
38
|
-
currentAttrs.add(attrName);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Check if this attribute has "once: true" in its map entry
|
|
42
|
-
const mapEntry = mountInit.map?.[coordinate] || null;
|
|
43
|
-
const isOnce = mapEntry?.once === true;
|
|
44
|
-
|
|
45
|
-
// If "once" is true, check if we've already seen this attribute
|
|
46
|
-
if (isOnce) {
|
|
47
|
-
let onceAttrs = elementOnceAttrs.get(element);
|
|
48
|
-
if (!onceAttrs) {
|
|
49
|
-
onceAttrs = new Set<string>();
|
|
50
|
-
elementOnceAttrs.set(element, onceAttrs);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// If we've already seen this attribute, skip it
|
|
54
|
-
if (onceAttrs.has(attrName)) {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Mark this attribute as seen if it currently has a value
|
|
59
|
-
if (currentValue !== null) {
|
|
60
|
-
onceAttrs.add(attrName);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Include if: currently has value OR previously had value but now removed
|
|
65
|
-
if (currentValue !== null || (previousValue !== undefined && currentValue === null)) {
|
|
66
|
-
// Check if value changed
|
|
67
|
-
if (currentValue !== previousValue) {
|
|
68
|
-
const attrNode = currentValue !== null ? element.getAttributeNode(attrName) : null;
|
|
69
|
-
|
|
70
|
-
changes.push({
|
|
71
|
-
value: currentValue,
|
|
72
|
-
attrNode,
|
|
73
|
-
mapEntry,
|
|
74
|
-
attrName,
|
|
75
|
-
coordinate,
|
|
76
|
-
element
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// Update state
|
|
80
|
-
if (currentValue !== null) {
|
|
81
|
-
attrState.set(attrName, currentValue);
|
|
82
|
-
} else {
|
|
83
|
-
attrState.delete(attrName);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return changes;
|
|
90
|
-
}
|
package/attrCoordinates.js
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builds a map of attribute names to their coordinates based on whereAttr config
|
|
3
|
-
*/
|
|
4
|
-
export function buildAttrCoordinateMap(whereAttr, isCustomElement) {
|
|
5
|
-
const map = {};
|
|
6
|
-
const rootPrefixes = isCustomElement
|
|
7
|
-
? (whereAttr.hasCERootIn || [])
|
|
8
|
-
: (whereAttr.hasBuiltInRootIn || []);
|
|
9
|
-
// Parse base attribute for custom delimiter
|
|
10
|
-
const { delimiter: baseDelimiter, name: baseName } = parseDelimiter(whereAttr.hasBase);
|
|
11
|
-
// Build attribute names for each prefix
|
|
12
|
-
for (const prefix of rootPrefixes) {
|
|
13
|
-
const baseAttrName = buildAttributeName(prefix, baseName, baseDelimiter);
|
|
14
|
-
// If no branches specified, just the base attribute
|
|
15
|
-
if (!whereAttr.hasBranchIn || whereAttr.hasBranchIn.length === 0) {
|
|
16
|
-
map[baseAttrName] = '0';
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
// Process each branch
|
|
20
|
-
for (let i = 0; i < whereAttr.hasBranchIn.length; i++) {
|
|
21
|
-
const branch = whereAttr.hasBranchIn[i];
|
|
22
|
-
if (branch === '') {
|
|
23
|
-
// Empty string means base attribute alone is valid
|
|
24
|
-
map[baseAttrName] = '0';
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
if (typeof branch === 'object') {
|
|
28
|
-
// Process branch object
|
|
29
|
-
processBranch(branch, baseAttrName, String(i), map);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return map;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Recursively processes a branch object to build attribute-coordinate mappings
|
|
37
|
-
*/
|
|
38
|
-
function processBranch(branch, parentAttrName, parentCoordinate, map) {
|
|
39
|
-
for (const [key, subBranches] of Object.entries(branch)) {
|
|
40
|
-
const { delimiter, name } = parseDelimiter(key);
|
|
41
|
-
const attrName = parentAttrName + delimiter + name;
|
|
42
|
-
// Process sub-branches
|
|
43
|
-
if (!subBranches || subBranches.length === 0) {
|
|
44
|
-
map[attrName] = parentCoordinate;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
for (let i = 0; i < subBranches.length; i++) {
|
|
48
|
-
const subBranch = subBranches[i];
|
|
49
|
-
const coordinate = `${parentCoordinate}.${i}`;
|
|
50
|
-
if (subBranch === '') {
|
|
51
|
-
// Empty string means this level alone is valid
|
|
52
|
-
map[attrName] = parentCoordinate;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if (typeof subBranch === 'string') {
|
|
56
|
-
// Simple string sub-branch
|
|
57
|
-
const { delimiter: subDelimiter, name: subName } = parseDelimiter(subBranch);
|
|
58
|
-
const subAttrName = attrName + subDelimiter + subName;
|
|
59
|
-
map[subAttrName] = coordinate;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (typeof subBranch === 'object') {
|
|
63
|
-
// Nested object - recursively process
|
|
64
|
-
processBranch(subBranch, attrName, coordinate, map);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Parses a key to extract custom delimiter and name
|
|
71
|
-
*/
|
|
72
|
-
function parseDelimiter(key) {
|
|
73
|
-
const match = key.match(/^\[(.+?)\](.+)$/);
|
|
74
|
-
if (match) {
|
|
75
|
-
return {
|
|
76
|
-
delimiter: match[1],
|
|
77
|
-
name: match[2]
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
delimiter: '-',
|
|
82
|
-
name: key
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Builds the full attribute name from prefix, base name, and delimiter
|
|
87
|
-
*/
|
|
88
|
-
function buildAttributeName(prefix, baseName, delimiter) {
|
|
89
|
-
if (prefix === '') {
|
|
90
|
-
return baseName;
|
|
91
|
-
}
|
|
92
|
-
return prefix + delimiter + baseName;
|
|
93
|
-
}
|
package/attrCoordinates.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { WhereAttr, BranchValue } from './types.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Represents a mapping from attribute name to coordinate
|
|
5
|
-
*/
|
|
6
|
-
export interface AttrCoordinateMap {
|
|
7
|
-
[attrName: string]: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Builds a map of attribute names to their coordinates based on whereAttr config
|
|
12
|
-
*/
|
|
13
|
-
export function buildAttrCoordinateMap(whereAttr: WhereAttr, isCustomElement: boolean): AttrCoordinateMap {
|
|
14
|
-
const map: AttrCoordinateMap = {};
|
|
15
|
-
const rootPrefixes = isCustomElement
|
|
16
|
-
? (whereAttr.hasCERootIn || [])
|
|
17
|
-
: (whereAttr.hasBuiltInRootIn || []);
|
|
18
|
-
|
|
19
|
-
// Parse base attribute for custom delimiter
|
|
20
|
-
const { delimiter: baseDelimiter, name: baseName } = parseDelimiter(whereAttr.hasBase);
|
|
21
|
-
|
|
22
|
-
// Build attribute names for each prefix
|
|
23
|
-
for (const prefix of rootPrefixes) {
|
|
24
|
-
const baseAttrName = buildAttributeName(prefix, baseName, baseDelimiter);
|
|
25
|
-
|
|
26
|
-
// If no branches specified, just the base attribute
|
|
27
|
-
if (!whereAttr.hasBranchIn || whereAttr.hasBranchIn.length === 0) {
|
|
28
|
-
map[baseAttrName] = '0';
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Process each branch
|
|
33
|
-
for (let i = 0; i < whereAttr.hasBranchIn.length; i++) {
|
|
34
|
-
const branch = whereAttr.hasBranchIn[i];
|
|
35
|
-
|
|
36
|
-
if (branch === '') {
|
|
37
|
-
// Empty string means base attribute alone is valid
|
|
38
|
-
map[baseAttrName] = '0';
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (typeof branch === 'object') {
|
|
43
|
-
// Process branch object
|
|
44
|
-
processBranch(branch, baseAttrName, String(i), map);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return map;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Recursively processes a branch object to build attribute-coordinate mappings
|
|
54
|
-
*/
|
|
55
|
-
function processBranch(
|
|
56
|
-
branch: { [key: string]: BranchValue[] },
|
|
57
|
-
parentAttrName: string,
|
|
58
|
-
parentCoordinate: string,
|
|
59
|
-
map: AttrCoordinateMap
|
|
60
|
-
): void {
|
|
61
|
-
for (const [key, subBranches] of Object.entries(branch)) {
|
|
62
|
-
const { delimiter, name } = parseDelimiter(key);
|
|
63
|
-
const attrName = parentAttrName + delimiter + name;
|
|
64
|
-
|
|
65
|
-
// Process sub-branches
|
|
66
|
-
if (!subBranches || subBranches.length === 0) {
|
|
67
|
-
map[attrName] = parentCoordinate;
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
for (let i = 0; i < subBranches.length; i++) {
|
|
72
|
-
const subBranch = subBranches[i];
|
|
73
|
-
const coordinate = `${parentCoordinate}.${i}`;
|
|
74
|
-
|
|
75
|
-
if (subBranch === '') {
|
|
76
|
-
// Empty string means this level alone is valid
|
|
77
|
-
map[attrName] = parentCoordinate;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (typeof subBranch === 'string') {
|
|
82
|
-
// Simple string sub-branch
|
|
83
|
-
const { delimiter: subDelimiter, name: subName } = parseDelimiter(subBranch);
|
|
84
|
-
const subAttrName = attrName + subDelimiter + subName;
|
|
85
|
-
map[subAttrName] = coordinate;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (typeof subBranch === 'object') {
|
|
90
|
-
// Nested object - recursively process
|
|
91
|
-
processBranch(subBranch, attrName, coordinate, map);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Parses a key to extract custom delimiter and name
|
|
99
|
-
*/
|
|
100
|
-
function parseDelimiter(key: string): { delimiter: string; name: string } {
|
|
101
|
-
const match = key.match(/^\[(.+?)\](.+)$/);
|
|
102
|
-
if (match) {
|
|
103
|
-
return {
|
|
104
|
-
delimiter: match[1],
|
|
105
|
-
name: match[2]
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
return {
|
|
109
|
-
delimiter: '-',
|
|
110
|
-
name: key
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Builds the full attribute name from prefix, base name, and delimiter
|
|
116
|
-
*/
|
|
117
|
-
function buildAttributeName(prefix: string, baseName: string, delimiter: string): string {
|
|
118
|
-
if (prefix === '') {
|
|
119
|
-
return baseName;
|
|
120
|
-
}
|
|
121
|
-
return prefix + delimiter + baseName;
|
|
122
|
-
}
|
package/whereAttr.js
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
const selectorCache = new WeakMap();
|
|
2
|
-
/**
|
|
3
|
-
* Checks if an element matches the whereAttr configuration using CSS selector matching
|
|
4
|
-
*/
|
|
5
|
-
export function matchesWhereAttr(element, whereAttr) {
|
|
6
|
-
// Get or build the CSS selectors for this whereAttr config
|
|
7
|
-
let selectors = selectorCache.get(whereAttr);
|
|
8
|
-
if (!selectors) {
|
|
9
|
-
selectors = {
|
|
10
|
-
builtIn: buildWhereAttrSelector(false, whereAttr),
|
|
11
|
-
custom: buildWhereAttrSelector(true, whereAttr)
|
|
12
|
-
};
|
|
13
|
-
selectorCache.set(whereAttr, selectors);
|
|
14
|
-
}
|
|
15
|
-
// Determine which selector to use based on element type
|
|
16
|
-
const isCustomElement = element.tagName.toLowerCase().includes('-');
|
|
17
|
-
const selector = isCustomElement ? selectors.custom : selectors.builtIn;
|
|
18
|
-
// Use native CSS matching - optimized in Chrome/Blink
|
|
19
|
-
return element.matches(selector);
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Builds a CSS selector string from the whereAttr configuration
|
|
23
|
-
* @param isCustomElement - Whether to build selector for custom elements (true) or built-in elements (false)
|
|
24
|
-
*/
|
|
25
|
-
function buildWhereAttrSelector(isCustomElement, whereAttr) {
|
|
26
|
-
const rootPrefixes = isCustomElement
|
|
27
|
-
? (whereAttr.hasCERootIn || [])
|
|
28
|
-
: (whereAttr.hasBuiltInRootIn || []);
|
|
29
|
-
// Parse base attribute for custom delimiter
|
|
30
|
-
const { delimiter: baseDelimiter, name: baseName } = parseDelimiter(whereAttr.hasBase);
|
|
31
|
-
const selectors = [];
|
|
32
|
-
// Build selectors for each valid prefix
|
|
33
|
-
for (const prefix of rootPrefixes) {
|
|
34
|
-
const baseAttrName = buildAttributeName(prefix, baseName, baseDelimiter);
|
|
35
|
-
const escapedBaseAttr = escapeAttributeName(baseAttrName);
|
|
36
|
-
// If no branches specified, just having the base attribute is enough
|
|
37
|
-
if (!whereAttr.hasBranchIn || whereAttr.hasBranchIn.length === 0) {
|
|
38
|
-
selectors.push(`[${escapedBaseAttr}]`);
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
// Build selectors for each branch combination
|
|
42
|
-
for (const branch of whereAttr.hasBranchIn) {
|
|
43
|
-
if (branch === '') {
|
|
44
|
-
// Empty string means base attribute alone is valid (no branch attributes)
|
|
45
|
-
// This requires base attr AND none of the branch attrs
|
|
46
|
-
// For CSS, we can only check for base attr presence
|
|
47
|
-
// The "no branch attrs" check needs to be done separately
|
|
48
|
-
selectors.push(`[${escapedBaseAttr}]`);
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
if (typeof branch === 'object') {
|
|
52
|
-
// Build selectors for each branch path
|
|
53
|
-
for (const [key, subBranches] of Object.entries(branch)) {
|
|
54
|
-
const branchSelectors = buildBranchSelectors(baseAttrName, key, subBranches);
|
|
55
|
-
selectors.push(...branchSelectors);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
// Join all selectors with comma (OR logic)
|
|
61
|
-
return selectors.join(',');
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Builds CSS selectors for a branch and its sub-branches
|
|
65
|
-
*/
|
|
66
|
-
function buildBranchSelectors(baseAttrName, branchKey, subBranches) {
|
|
67
|
-
const { delimiter: branchDelimiter, name: branchName } = parseDelimiter(branchKey);
|
|
68
|
-
const branchAttrName = baseAttrName + branchDelimiter + branchName;
|
|
69
|
-
const escapedBranchAttr = escapeAttributeName(branchAttrName);
|
|
70
|
-
const selectors = [];
|
|
71
|
-
// If no sub-branches specified, just the branch attribute itself
|
|
72
|
-
if (!subBranches || subBranches.length === 0) {
|
|
73
|
-
selectors.push(`[${escapedBranchAttr}]`);
|
|
74
|
-
return selectors;
|
|
75
|
-
}
|
|
76
|
-
// Build selectors for each sub-branch - they form an OR condition
|
|
77
|
-
for (const subBranch of subBranches) {
|
|
78
|
-
if (subBranch === '') {
|
|
79
|
-
// Empty string means this branch level alone is valid
|
|
80
|
-
selectors.push(`[${escapedBranchAttr}]`);
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
if (typeof subBranch === 'string') {
|
|
84
|
-
// Simple string sub-branch - build the full path
|
|
85
|
-
const { delimiter: subDelimiter, name: subName } = parseDelimiter(subBranch);
|
|
86
|
-
const subAttrName = branchAttrName + subDelimiter + subName;
|
|
87
|
-
const escapedSubAttr = escapeAttributeName(subAttrName);
|
|
88
|
-
selectors.push(`[${escapedSubAttr}]`);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (typeof subBranch === 'object') {
|
|
92
|
-
// Nested object sub-branch - recursively build deeper paths
|
|
93
|
-
for (const [key, nestedBranches] of Object.entries(subBranch)) {
|
|
94
|
-
const nestedSelectors = buildNestedBranchSelectors(branchAttrName, key, nestedBranches);
|
|
95
|
-
selectors.push(...nestedSelectors);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return selectors;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Builds CSS selectors for nested branches recursively
|
|
103
|
-
*/
|
|
104
|
-
function buildNestedBranchSelectors(parentAttrName, branchKey, subBranches) {
|
|
105
|
-
const { delimiter, name } = parseDelimiter(branchKey);
|
|
106
|
-
const attrName = parentAttrName + delimiter + name;
|
|
107
|
-
const escapedAttr = escapeAttributeName(attrName);
|
|
108
|
-
const selectors = [];
|
|
109
|
-
// If no sub-branches specified, just this attribute
|
|
110
|
-
if (!subBranches || subBranches.length === 0) {
|
|
111
|
-
selectors.push(`[${escapedAttr}]`);
|
|
112
|
-
return selectors;
|
|
113
|
-
}
|
|
114
|
-
// Build selectors for each sub-branch - they form an OR condition
|
|
115
|
-
for (const subBranch of subBranches) {
|
|
116
|
-
if (subBranch === '') {
|
|
117
|
-
// Empty string means this level alone is valid
|
|
118
|
-
selectors.push(`[${escapedAttr}]`);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
if (typeof subBranch === 'string') {
|
|
122
|
-
// Simple string sub-branch - build the full path
|
|
123
|
-
const { delimiter: subDelimiter, name: subName } = parseDelimiter(subBranch);
|
|
124
|
-
const subAttrName = attrName + subDelimiter + subName;
|
|
125
|
-
const escapedSubAttr = escapeAttributeName(subAttrName);
|
|
126
|
-
selectors.push(`[${escapedSubAttr}]`);
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (typeof subBranch === 'object') {
|
|
130
|
-
// Nested object - recursively build deeper paths
|
|
131
|
-
for (const [key, nestedBranches] of Object.entries(subBranch)) {
|
|
132
|
-
const nestedSelectors = buildNestedBranchSelectors(attrName, key, nestedBranches);
|
|
133
|
-
selectors.push(...nestedSelectors);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return selectors;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Escapes special characters in attribute names for CSS selectors
|
|
141
|
-
* Uses CSS.escape() API which handles all special characters including :
|
|
142
|
-
*/
|
|
143
|
-
function escapeAttributeName(attrName) {
|
|
144
|
-
// CSS.escape() is available in all modern browsers
|
|
145
|
-
// It properly escapes special characters like : . [ ] etc.
|
|
146
|
-
return CSS.escape(attrName);
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Parses a key to extract custom delimiter and name
|
|
150
|
-
* Format: [delimiter]name
|
|
151
|
-
* Example: "[_]my-custom" returns { delimiter: "_", name: "my-custom" }
|
|
152
|
-
*/
|
|
153
|
-
function parseDelimiter(key) {
|
|
154
|
-
const match = key.match(/^\[(.+?)\](.+)$/);
|
|
155
|
-
if (match) {
|
|
156
|
-
return {
|
|
157
|
-
delimiter: match[1],
|
|
158
|
-
name: match[2]
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
return {
|
|
162
|
-
delimiter: '-',
|
|
163
|
-
name: key
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Builds the full attribute name from prefix, base name, and delimiter
|
|
168
|
-
*/
|
|
169
|
-
function buildAttributeName(prefix, baseName, delimiter) {
|
|
170
|
-
if (prefix === '') {
|
|
171
|
-
return baseName;
|
|
172
|
-
}
|
|
173
|
-
return prefix + delimiter + baseName;
|
|
174
|
-
}
|
package/whereAttr.ts
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { WhereAttr } from './types.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Cache for compiled CSS selectors to avoid rebuilding them on every check
|
|
5
|
-
* Stores both built-in and custom element selectors
|
|
6
|
-
*/
|
|
7
|
-
interface CachedSelectors {
|
|
8
|
-
builtIn: string;
|
|
9
|
-
custom: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const selectorCache = new WeakMap<WhereAttr, CachedSelectors>();
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Checks if an element matches the whereAttr configuration using CSS selector matching
|
|
16
|
-
*/
|
|
17
|
-
export function matchesWhereAttr(element: Element, whereAttr: WhereAttr): boolean {
|
|
18
|
-
// Get or build the CSS selectors for this whereAttr config
|
|
19
|
-
let selectors = selectorCache.get(whereAttr);
|
|
20
|
-
if (!selectors) {
|
|
21
|
-
selectors = {
|
|
22
|
-
builtIn: buildWhereAttrSelector(false, whereAttr),
|
|
23
|
-
custom: buildWhereAttrSelector(true, whereAttr)
|
|
24
|
-
};
|
|
25
|
-
selectorCache.set(whereAttr, selectors);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Determine which selector to use based on element type
|
|
29
|
-
const isCustomElement = element.tagName.toLowerCase().includes('-');
|
|
30
|
-
const selector = isCustomElement ? selectors.custom : selectors.builtIn;
|
|
31
|
-
|
|
32
|
-
// Use native CSS matching - optimized in Chrome/Blink
|
|
33
|
-
return element.matches(selector);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Builds a CSS selector string from the whereAttr configuration
|
|
38
|
-
* @param isCustomElement - Whether to build selector for custom elements (true) or built-in elements (false)
|
|
39
|
-
*/
|
|
40
|
-
function buildWhereAttrSelector(isCustomElement: boolean, whereAttr: WhereAttr): string {
|
|
41
|
-
const rootPrefixes = isCustomElement
|
|
42
|
-
? (whereAttr.hasCERootIn || [])
|
|
43
|
-
: (whereAttr.hasBuiltInRootIn || []);
|
|
44
|
-
|
|
45
|
-
// Parse base attribute for custom delimiter
|
|
46
|
-
const { delimiter: baseDelimiter, name: baseName } = parseDelimiter(whereAttr.hasBase);
|
|
47
|
-
|
|
48
|
-
const selectors: string[] = [];
|
|
49
|
-
|
|
50
|
-
// Build selectors for each valid prefix
|
|
51
|
-
for (const prefix of rootPrefixes) {
|
|
52
|
-
const baseAttrName = buildAttributeName(prefix, baseName, baseDelimiter);
|
|
53
|
-
const escapedBaseAttr = escapeAttributeName(baseAttrName);
|
|
54
|
-
|
|
55
|
-
// If no branches specified, just having the base attribute is enough
|
|
56
|
-
if (!whereAttr.hasBranchIn || whereAttr.hasBranchIn.length === 0) {
|
|
57
|
-
selectors.push(`[${escapedBaseAttr}]`);
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Build selectors for each branch combination
|
|
62
|
-
for (const branch of whereAttr.hasBranchIn) {
|
|
63
|
-
if (branch === '') {
|
|
64
|
-
// Empty string means base attribute alone is valid (no branch attributes)
|
|
65
|
-
// This requires base attr AND none of the branch attrs
|
|
66
|
-
// For CSS, we can only check for base attr presence
|
|
67
|
-
// The "no branch attrs" check needs to be done separately
|
|
68
|
-
selectors.push(`[${escapedBaseAttr}]`);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (typeof branch === 'object') {
|
|
73
|
-
// Build selectors for each branch path
|
|
74
|
-
for (const [key, subBranches] of Object.entries(branch)) {
|
|
75
|
-
const branchSelectors = buildBranchSelectors(baseAttrName, key, subBranches);
|
|
76
|
-
selectors.push(...branchSelectors);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Join all selectors with comma (OR logic)
|
|
83
|
-
return selectors.join(',');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Builds CSS selectors for a branch and its sub-branches
|
|
88
|
-
*/
|
|
89
|
-
function buildBranchSelectors(
|
|
90
|
-
baseAttrName: string,
|
|
91
|
-
branchKey: string,
|
|
92
|
-
subBranches: any[]
|
|
93
|
-
): string[] {
|
|
94
|
-
const { delimiter: branchDelimiter, name: branchName } = parseDelimiter(branchKey);
|
|
95
|
-
const branchAttrName = baseAttrName + branchDelimiter + branchName;
|
|
96
|
-
const escapedBranchAttr = escapeAttributeName(branchAttrName);
|
|
97
|
-
|
|
98
|
-
const selectors: string[] = [];
|
|
99
|
-
|
|
100
|
-
// If no sub-branches specified, just the branch attribute itself
|
|
101
|
-
if (!subBranches || subBranches.length === 0) {
|
|
102
|
-
selectors.push(`[${escapedBranchAttr}]`);
|
|
103
|
-
return selectors;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Build selectors for each sub-branch - they form an OR condition
|
|
107
|
-
for (const subBranch of subBranches) {
|
|
108
|
-
if (subBranch === '') {
|
|
109
|
-
// Empty string means this branch level alone is valid
|
|
110
|
-
selectors.push(`[${escapedBranchAttr}]`);
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (typeof subBranch === 'string') {
|
|
115
|
-
// Simple string sub-branch - build the full path
|
|
116
|
-
const { delimiter: subDelimiter, name: subName } = parseDelimiter(subBranch);
|
|
117
|
-
const subAttrName = branchAttrName + subDelimiter + subName;
|
|
118
|
-
const escapedSubAttr = escapeAttributeName(subAttrName);
|
|
119
|
-
selectors.push(`[${escapedSubAttr}]`);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (typeof subBranch === 'object') {
|
|
124
|
-
// Nested object sub-branch - recursively build deeper paths
|
|
125
|
-
for (const [key, nestedBranches] of Object.entries(subBranch)) {
|
|
126
|
-
const nestedSelectors = buildNestedBranchSelectors(branchAttrName, key, nestedBranches as any[]);
|
|
127
|
-
selectors.push(...nestedSelectors);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return selectors;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Builds CSS selectors for nested branches recursively
|
|
137
|
-
*/
|
|
138
|
-
function buildNestedBranchSelectors(
|
|
139
|
-
parentAttrName: string,
|
|
140
|
-
branchKey: string,
|
|
141
|
-
subBranches: any[]
|
|
142
|
-
): string[] {
|
|
143
|
-
const { delimiter, name } = parseDelimiter(branchKey);
|
|
144
|
-
const attrName = parentAttrName + delimiter + name;
|
|
145
|
-
const escapedAttr = escapeAttributeName(attrName);
|
|
146
|
-
|
|
147
|
-
const selectors: string[] = [];
|
|
148
|
-
|
|
149
|
-
// If no sub-branches specified, just this attribute
|
|
150
|
-
if (!subBranches || subBranches.length === 0) {
|
|
151
|
-
selectors.push(`[${escapedAttr}]`);
|
|
152
|
-
return selectors;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Build selectors for each sub-branch - they form an OR condition
|
|
156
|
-
for (const subBranch of subBranches) {
|
|
157
|
-
if (subBranch === '') {
|
|
158
|
-
// Empty string means this level alone is valid
|
|
159
|
-
selectors.push(`[${escapedAttr}]`);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (typeof subBranch === 'string') {
|
|
164
|
-
// Simple string sub-branch - build the full path
|
|
165
|
-
const { delimiter: subDelimiter, name: subName } = parseDelimiter(subBranch);
|
|
166
|
-
const subAttrName = attrName + subDelimiter + subName;
|
|
167
|
-
const escapedSubAttr = escapeAttributeName(subAttrName);
|
|
168
|
-
selectors.push(`[${escapedSubAttr}]`);
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (typeof subBranch === 'object') {
|
|
173
|
-
// Nested object - recursively build deeper paths
|
|
174
|
-
for (const [key, nestedBranches] of Object.entries(subBranch)) {
|
|
175
|
-
const nestedSelectors = buildNestedBranchSelectors(attrName, key, nestedBranches as any[]);
|
|
176
|
-
selectors.push(...nestedSelectors);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return selectors;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Escapes special characters in attribute names for CSS selectors
|
|
186
|
-
* Uses CSS.escape() API which handles all special characters including :
|
|
187
|
-
*/
|
|
188
|
-
function escapeAttributeName(attrName: string): string {
|
|
189
|
-
// CSS.escape() is available in all modern browsers
|
|
190
|
-
// It properly escapes special characters like : . [ ] etc.
|
|
191
|
-
return CSS.escape(attrName);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Parses a key to extract custom delimiter and name
|
|
196
|
-
* Format: [delimiter]name
|
|
197
|
-
* Example: "[_]my-custom" returns { delimiter: "_", name: "my-custom" }
|
|
198
|
-
*/
|
|
199
|
-
function parseDelimiter(key: string): { delimiter: string; name: string } {
|
|
200
|
-
const match = key.match(/^\[(.+?)\](.+)$/);
|
|
201
|
-
if (match) {
|
|
202
|
-
return {
|
|
203
|
-
delimiter: match[1],
|
|
204
|
-
name: match[2]
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
return {
|
|
208
|
-
delimiter: '-',
|
|
209
|
-
name: key
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Builds the full attribute name from prefix, base name, and delimiter
|
|
215
|
-
*/
|
|
216
|
-
function buildAttributeName(prefix: string, baseName: string, delimiter: string): string {
|
|
217
|
-
if (prefix === '') {
|
|
218
|
-
return baseName;
|
|
219
|
-
}
|
|
220
|
-
return prefix + delimiter + baseName;
|
|
221
|
-
}
|