@oscarpalmer/toretto 0.41.0 → 0.42.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/dist/attribute/{get.d.mts → get.attribute.d.mts} +3 -2
- package/dist/attribute/{get.mjs → get.attribute.mjs} +4 -3
- package/dist/attribute/index.d.mts +3 -16
- package/dist/attribute/index.mjs +4 -7
- package/dist/attribute/{set.d.mts → set.attribute.d.mts} +4 -4
- package/dist/attribute/{set.mjs → set.attribute.mjs} +2 -2
- package/dist/create.d.mts +25 -0
- package/dist/create.mjs +17 -0
- package/dist/data.mjs +7 -7
- package/dist/event/delegation.mjs +8 -1
- package/dist/html/index.d.mts +23 -26
- package/dist/html/index.mjs +85 -18
- package/dist/html/sanitize.mjs +6 -5
- package/dist/index.d.mts +113 -52
- package/dist/index.mjs +510 -361
- package/dist/internal/attribute.d.mts +4 -3
- package/dist/internal/attribute.mjs +13 -23
- package/dist/internal/element-value.d.mts +2 -2
- package/dist/internal/element-value.mjs +4 -2
- package/dist/internal/get-value.mjs +1 -1
- package/dist/internal/property.d.mts +4 -0
- package/dist/internal/property.mjs +21 -0
- package/dist/property/get.property.d.mts +20 -0
- package/dist/property/get.property.mjs +35 -0
- package/dist/property/index.d.mts +3 -0
- package/dist/property/index.mjs +3 -0
- package/dist/property/set.property.d.mts +32 -0
- package/dist/property/set.property.mjs +34 -0
- package/dist/style.d.mts +12 -7
- package/dist/style.mjs +14 -18
- package/package.json +12 -5
- package/src/attribute/{get.ts → get.attribute.ts} +14 -3
- package/src/attribute/index.ts +10 -22
- package/src/attribute/{set.ts → set.attribute.ts} +9 -5
- package/src/create.ts +81 -0
- package/src/data.ts +16 -8
- package/src/event/delegation.ts +24 -3
- package/src/event/index.ts +9 -3
- package/src/find/index.ts +11 -3
- package/src/find/relative.ts +4 -0
- package/src/focusable.ts +10 -2
- package/src/html/index.ts +166 -58
- package/src/html/sanitize.ts +14 -11
- package/src/index.ts +2 -1
- package/src/internal/attribute.ts +23 -42
- package/src/internal/element-value.ts +11 -4
- package/src/internal/get-value.ts +8 -0
- package/src/internal/is.ts +4 -0
- package/src/internal/property.ts +42 -0
- package/src/is.ts +10 -2
- package/src/property/get.property.ts +73 -0
- package/src/property/index.ts +2 -0
- package/src/property/set.property.ts +102 -0
- package/src/style.ts +52 -27
- package/src/touch.ts +14 -2
package/src/create.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type {Primitive} from '@oscarpalmer/atoms/models';
|
|
2
|
+
import {setAttributes} from './attribute';
|
|
3
|
+
import {setStyles} from './style';
|
|
4
|
+
import {setProperties} from './property';
|
|
5
|
+
|
|
6
|
+
// #region Types
|
|
7
|
+
|
|
8
|
+
type Properties<Target extends Element> = {
|
|
9
|
+
[Property in keyof Target]?: Target[Property] extends Primitive ? Target[Property] : never;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type Styles = Partial<Record<keyof CSSStyleDeclaration, unknown>>;
|
|
13
|
+
|
|
14
|
+
// #endregion
|
|
15
|
+
|
|
16
|
+
// #region Functions
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates an HTML element with the specified tag name together with optional properties, attributes, and styles
|
|
20
|
+
* @param tag Tag name
|
|
21
|
+
* @param properties Element properties
|
|
22
|
+
* @param attributes Element attributes
|
|
23
|
+
* @param styles Element styles
|
|
24
|
+
* @returns Created element
|
|
25
|
+
*/
|
|
26
|
+
export function createElement<TagName extends keyof HTMLElementTagNameMap>(
|
|
27
|
+
tag: TagName,
|
|
28
|
+
properties?: Properties<HTMLElementTagNameMap[TagName]>,
|
|
29
|
+
attributes?: Record<string, unknown>,
|
|
30
|
+
styles?: Styles,
|
|
31
|
+
): HTMLElementTagNameMap[TagName];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates an HTML element with the specified tag name together with optional properties, attributes, and styles
|
|
35
|
+
* @param tag Tag name
|
|
36
|
+
* @param properties Element properties
|
|
37
|
+
* @param attributes Element attributes
|
|
38
|
+
* @param styles Element styles
|
|
39
|
+
* @returns Created element
|
|
40
|
+
*/
|
|
41
|
+
export function createElement(
|
|
42
|
+
tag: string,
|
|
43
|
+
properties?: Properties<HTMLElement>,
|
|
44
|
+
attributes?: Record<string, unknown>,
|
|
45
|
+
styles?: Styles,
|
|
46
|
+
): HTMLUnknownElement;
|
|
47
|
+
|
|
48
|
+
export function createElement(
|
|
49
|
+
tag: string,
|
|
50
|
+
properties?: Properties<HTMLElement>,
|
|
51
|
+
attributes?: Record<string, unknown>,
|
|
52
|
+
styles?: Styles,
|
|
53
|
+
): HTMLElement {
|
|
54
|
+
if (typeof tag !== 'string') {
|
|
55
|
+
throw new TypeError(MESSAGE);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const element = document.createElement(tag);
|
|
59
|
+
|
|
60
|
+
if (properties != null) {
|
|
61
|
+
setProperties(element, properties);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (attributes != null) {
|
|
65
|
+
setAttributes(element, attributes);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (styles != null) {
|
|
69
|
+
setStyles(element, styles);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return element;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// #endregion
|
|
76
|
+
|
|
77
|
+
// #region Variables
|
|
78
|
+
|
|
79
|
+
const MESSAGE = 'Tag name must be a string';
|
|
80
|
+
|
|
81
|
+
// #endregion
|
package/src/data.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type {PlainObject} from '@oscarpalmer/atoms/models';
|
|
2
2
|
import {parse} from '@oscarpalmer/atoms/string';
|
|
3
|
-
import {kebabCase} from '@oscarpalmer/atoms/string/case';
|
|
3
|
+
import {camelCase, kebabCase} from '@oscarpalmer/atoms/string/case';
|
|
4
4
|
import {setElementValues, updateElementValue} from './internal/element-value';
|
|
5
5
|
import {EXPRESSION_DATA_PREFIX} from './internal/get-value';
|
|
6
6
|
import {isHTMLOrSVGElement} from './internal/is';
|
|
7
7
|
|
|
8
|
+
// #region Functions
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Get a keyed data value from an element
|
|
10
12
|
* @param element Element to get data from
|
|
@@ -32,16 +34,16 @@ export function getData(element: Element, keys: string | string[], parseValues?:
|
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
const
|
|
37
|
+
const noParse = parseValues === false;
|
|
36
38
|
|
|
37
39
|
if (typeof keys === 'string') {
|
|
38
|
-
const value = element.dataset[keys];
|
|
40
|
+
const value = element.dataset[camelCase(keys)];
|
|
39
41
|
|
|
40
42
|
if (value === undefined) {
|
|
41
43
|
return;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
return
|
|
46
|
+
return noParse ? value : parse(value);
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
const {length} = keys;
|
|
@@ -50,12 +52,12 @@ export function getData(element: Element, keys: string | string[], parseValues?:
|
|
|
50
52
|
|
|
51
53
|
for (let index = 0; index < length; index += 1) {
|
|
52
54
|
const key = keys[index];
|
|
53
|
-
const value = element.dataset[key];
|
|
55
|
+
const value = element.dataset[camelCase(key)];
|
|
54
56
|
|
|
55
57
|
if (value == null) {
|
|
56
58
|
data[key] = undefined;
|
|
57
59
|
} else {
|
|
58
|
-
data[key] =
|
|
60
|
+
data[key] = noParse ? value : parse(value);
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -63,7 +65,7 @@ export function getData(element: Element, keys: string | string[], parseValues?:
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
function getName(original: string): string {
|
|
66
|
-
return `${ATTRIBUTE_DATA_PREFIX}${kebabCase(original
|
|
68
|
+
return `${ATTRIBUTE_DATA_PREFIX}${kebabCase(original.replace(EXPRESSION_DATA_PREFIX, ''))}`;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
/**
|
|
@@ -90,13 +92,19 @@ function updateDataAttribute(element: Element, key: string, value: unknown): voi
|
|
|
90
92
|
element,
|
|
91
93
|
getName(key),
|
|
92
94
|
value,
|
|
95
|
+
// oxlint-disable-next-line typescript/unbound-method: using .call in `updateElementValue`
|
|
93
96
|
element.setAttribute,
|
|
97
|
+
// oxlint-disable-next-line typescript/unbound-method: using .call in `updateElementValue`
|
|
94
98
|
element.removeAttribute,
|
|
95
99
|
false,
|
|
96
100
|
true,
|
|
97
101
|
);
|
|
98
102
|
}
|
|
99
103
|
|
|
100
|
-
//
|
|
104
|
+
// #endregion
|
|
105
|
+
|
|
106
|
+
// #region Variables
|
|
101
107
|
|
|
102
108
|
const ATTRIBUTE_DATA_PREFIX = 'data-';
|
|
109
|
+
|
|
110
|
+
// #endregion
|
package/src/event/delegation.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import {isEventTarget} from '../internal/is';
|
|
2
2
|
import type {CustomEventListener, RemovableEventListener} from '../models';
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// #region Types
|
|
5
5
|
|
|
6
6
|
export type EventTargetWithListeners = EventTarget &
|
|
7
7
|
Partial<{
|
|
8
8
|
[key: string]: Set<EventListener | CustomEventListener>;
|
|
9
9
|
}>;
|
|
10
10
|
|
|
11
|
+
// #endregion
|
|
12
|
+
|
|
13
|
+
// #region Functions
|
|
14
|
+
|
|
11
15
|
function addDelegatedHandler(doc: Document, type: string, name: string, passive: boolean): void {
|
|
12
16
|
if (DELEGATED.has(name)) {
|
|
13
17
|
return;
|
|
@@ -44,8 +48,21 @@ function delegatedEventHandler(this: boolean, event: Event): void {
|
|
|
44
48
|
const items = event.composedPath();
|
|
45
49
|
const {length} = items;
|
|
46
50
|
|
|
51
|
+
let cancelled = false;
|
|
47
52
|
let target = items[0];
|
|
48
53
|
|
|
54
|
+
// oxlint-disable-next-line typescript/unbound-method: using `.call` to ensure correct `this` context
|
|
55
|
+
const originalStopPropagation = event.stopPropagation;
|
|
56
|
+
|
|
57
|
+
event.stopPropagation = function () {
|
|
58
|
+
cancelled = true;
|
|
59
|
+
|
|
60
|
+
originalStopPropagation.call(event);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Event is one and the same for all listeners, so stopping propagation should work the same, regardless of stop type
|
|
64
|
+
event.stopImmediatePropagation = event.stopPropagation.bind(event);
|
|
65
|
+
|
|
49
66
|
Object.defineProperties(event, {
|
|
50
67
|
currentTarget: {
|
|
51
68
|
configurable: true,
|
|
@@ -72,7 +89,7 @@ function delegatedEventHandler(this: boolean, event: Event): void {
|
|
|
72
89
|
for (const listener of listeners) {
|
|
73
90
|
(listener as EventListener).call(item, event);
|
|
74
91
|
|
|
75
|
-
if (
|
|
92
|
+
if (cancelled) {
|
|
76
93
|
return;
|
|
77
94
|
}
|
|
78
95
|
}
|
|
@@ -115,7 +132,9 @@ export function removeDelegatedListener(
|
|
|
115
132
|
return true;
|
|
116
133
|
}
|
|
117
134
|
|
|
118
|
-
//
|
|
135
|
+
// #endregion
|
|
136
|
+
|
|
137
|
+
// #region Variables
|
|
119
138
|
|
|
120
139
|
const DELEGATED = new Set<string>();
|
|
121
140
|
|
|
@@ -153,3 +172,5 @@ const EVENT_TYPES: Set<string> = new Set([
|
|
|
153
172
|
const HANDLER_ACTIVE = delegatedEventHandler.bind(false);
|
|
154
173
|
|
|
155
174
|
const HANDLER_PASSIVE = delegatedEventHandler.bind(true);
|
|
175
|
+
|
|
176
|
+
// #endregion
|
package/src/event/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
removeDelegatedListener,
|
|
11
11
|
} from './delegation';
|
|
12
12
|
|
|
13
|
-
//
|
|
13
|
+
// #region Types
|
|
14
14
|
|
|
15
15
|
type EventOptions = {
|
|
16
16
|
capture: boolean;
|
|
@@ -19,7 +19,9 @@ type EventOptions = {
|
|
|
19
19
|
signal?: AbortSignal;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
//
|
|
22
|
+
// #endregion
|
|
23
|
+
|
|
24
|
+
// #region Functions
|
|
23
25
|
|
|
24
26
|
function createDispatchOptions(options: EventInit): EventInit {
|
|
25
27
|
return {
|
|
@@ -219,6 +221,10 @@ export function on(
|
|
|
219
221
|
};
|
|
220
222
|
}
|
|
221
223
|
|
|
222
|
-
//
|
|
224
|
+
// #endregion
|
|
225
|
+
|
|
226
|
+
// #region Variables
|
|
223
227
|
|
|
224
228
|
const PROPERTY_DETAIL = 'detail';
|
|
229
|
+
|
|
230
|
+
// #endregion
|
package/src/find/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type {PlainObject} from '@oscarpalmer/atoms/models';
|
|
2
2
|
import type {Selector} from '../models';
|
|
3
3
|
|
|
4
|
+
// #region Functions
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Find the first element that matches the tag name
|
|
6
8
|
* @param tagName Tag name of element to find
|
|
@@ -180,7 +182,9 @@ function isContext(value: unknown): boolean {
|
|
|
180
182
|
);
|
|
181
183
|
}
|
|
182
184
|
|
|
183
|
-
//
|
|
185
|
+
// #endregion
|
|
186
|
+
|
|
187
|
+
// #region Variables
|
|
184
188
|
|
|
185
189
|
const QUERY_SELECTOR_ALL = 'querySelectorAll';
|
|
186
190
|
|
|
@@ -194,7 +198,11 @@ const SUFFIX_HOVER = ':hover';
|
|
|
194
198
|
|
|
195
199
|
const TAG_HEAD = 'HEAD';
|
|
196
200
|
|
|
197
|
-
//
|
|
201
|
+
// #endregion
|
|
202
|
+
|
|
203
|
+
// #region Exports
|
|
198
204
|
|
|
199
|
-
export {findElement as $, findElements as $$};
|
|
200
205
|
export {findAncestor, findRelatives, getDistance} from './relative';
|
|
206
|
+
export {findElement as $, findElements as $$};
|
|
207
|
+
|
|
208
|
+
// #endregion
|
package/src/find/relative.ts
CHANGED
package/src/focusable.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Based on https://github.com/focus-trap/tabbable :-)
|
|
2
2
|
|
|
3
|
+
// #region Types
|
|
4
|
+
|
|
3
5
|
type ElementWithTabIndex = {
|
|
4
6
|
element: Element;
|
|
5
7
|
tabIndex: number;
|
|
@@ -9,7 +11,9 @@ type Filter = (item: ElementWithTabIndex) => boolean;
|
|
|
9
11
|
|
|
10
12
|
type InertElement = Element & {inert: boolean};
|
|
11
13
|
|
|
12
|
-
//
|
|
14
|
+
// #endregion
|
|
15
|
+
|
|
16
|
+
// #region Functions
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Get a list of focusable elements within a parent element
|
|
@@ -230,7 +234,9 @@ function isValidElement(element: Element, filters: Filter[], tabbable: boolean):
|
|
|
230
234
|
return !filters.some(filter => filter(item));
|
|
231
235
|
}
|
|
232
236
|
|
|
233
|
-
//
|
|
237
|
+
// #endregion
|
|
238
|
+
|
|
239
|
+
// #region Variables
|
|
234
240
|
|
|
235
241
|
const ATTRIBUTE_CONTENTEDITABLE = 'contenteditable';
|
|
236
242
|
|
|
@@ -294,3 +300,5 @@ const TABINDEX_BASE = 0;
|
|
|
294
300
|
const TABINDEX_DEFAULT = -1;
|
|
295
301
|
|
|
296
302
|
const TYPE_RADIO = 'radio';
|
|
303
|
+
|
|
304
|
+
// #endregion
|
package/src/html/index.ts
CHANGED
|
@@ -1,36 +1,9 @@
|
|
|
1
1
|
import {isPlainObject} from '@oscarpalmer/atoms/is';
|
|
2
|
+
import {SizedMap} from '@oscarpalmer/atoms/sized/map';
|
|
3
|
+
import {getString} from '@oscarpalmer/atoms/string';
|
|
2
4
|
import {sanitizeNodes} from './sanitize';
|
|
3
5
|
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
type Html = {
|
|
7
|
-
/**
|
|
8
|
-
* Create nodes from an HTML string or a template element
|
|
9
|
-
* @param value HTML string or id for a template element
|
|
10
|
-
* @param options Options for creating nodes
|
|
11
|
-
* @returns Created nodes
|
|
12
|
-
*/
|
|
13
|
-
(value: string, options?: HtmlOptions): Node[];
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Create nodes from a template element
|
|
17
|
-
* @param template Template element
|
|
18
|
-
* @param options Options for creating nodes
|
|
19
|
-
* @returns Created nodes
|
|
20
|
-
*/
|
|
21
|
-
(template: HTMLTemplateElement, options?: HtmlOptions): Node[];
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Clear cache of template elements
|
|
25
|
-
*/
|
|
26
|
-
clear(): void;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Remove cached template element for an HTML string or id
|
|
30
|
-
* @param template HTML string or id for a template element
|
|
31
|
-
*/
|
|
32
|
-
remove(template: string): void;
|
|
33
|
-
};
|
|
6
|
+
// #region Types
|
|
34
7
|
|
|
35
8
|
type HtmlOptions = {
|
|
36
9
|
/**
|
|
@@ -41,7 +14,14 @@ type HtmlOptions = {
|
|
|
41
14
|
|
|
42
15
|
type Options = Required<HtmlOptions>;
|
|
43
16
|
|
|
44
|
-
|
|
17
|
+
type Tagged = {
|
|
18
|
+
nodes: Node[];
|
|
19
|
+
template: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// #endregion
|
|
23
|
+
|
|
24
|
+
// #region Functions
|
|
45
25
|
|
|
46
26
|
function createHtml(value: string | HTMLTemplateElement): string {
|
|
47
27
|
const parsed = getParser().parseFromString(getHtml(value), PARSE_TYPE_HTML);
|
|
@@ -62,27 +42,41 @@ function createTemplate(
|
|
|
62
42
|
template.innerHTML = createHtml(value);
|
|
63
43
|
|
|
64
44
|
if (typeof value === 'string' && options.cache) {
|
|
65
|
-
templates
|
|
45
|
+
templates.set(value, template);
|
|
66
46
|
}
|
|
67
47
|
|
|
68
48
|
return template;
|
|
69
49
|
}
|
|
70
50
|
|
|
51
|
+
function getComment(index: number): string {
|
|
52
|
+
return COMMENT_TEMPLATE.replace(COMMENT_INDEX, String(index));
|
|
53
|
+
}
|
|
54
|
+
|
|
71
55
|
function getHtml(value: string | HTMLTemplateElement): string {
|
|
72
56
|
return `${TEMPORARY_ELEMENT}${typeof value === 'string' ? value : value.innerHTML}${TEMPORARY_ELEMENT}`;
|
|
73
57
|
}
|
|
74
58
|
|
|
75
|
-
function getNodes(value:
|
|
59
|
+
function getNodes(value: unknown, options: Options, nodes?: Node[]): Node[] {
|
|
76
60
|
if (typeof value !== 'string' && !(value instanceof HTMLTemplateElement)) {
|
|
77
61
|
return [];
|
|
78
62
|
}
|
|
79
63
|
|
|
80
64
|
const template = getTemplate(value, options);
|
|
81
65
|
|
|
82
|
-
|
|
66
|
+
if (template == null) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const cloned = [...template.content.cloneNode(true).childNodes];
|
|
71
|
+
|
|
72
|
+
if (nodes != null) {
|
|
73
|
+
replaceComments(cloned, nodes);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return cloned;
|
|
83
77
|
}
|
|
84
78
|
|
|
85
|
-
function getOptions(input?:
|
|
79
|
+
function getOptions(input?: unknown): Options {
|
|
86
80
|
const options = isPlainObject(input) ? input : {};
|
|
87
81
|
|
|
88
82
|
options.cache = typeof options.cache === 'boolean' ? options.cache : true;
|
|
@@ -96,6 +90,58 @@ function getParser(): DOMParser {
|
|
|
96
90
|
return parser;
|
|
97
91
|
}
|
|
98
92
|
|
|
93
|
+
function getTagged(strings: TemplateStringsArray, values: unknown[]): Tagged {
|
|
94
|
+
const tagged: Tagged = {
|
|
95
|
+
nodes: [],
|
|
96
|
+
template: '',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const stringsLength = strings.length;
|
|
100
|
+
|
|
101
|
+
let nodeIndex = 0;
|
|
102
|
+
|
|
103
|
+
for (let stringIndex = 0; stringIndex < stringsLength; stringIndex += 1) {
|
|
104
|
+
const value = values[stringIndex];
|
|
105
|
+
|
|
106
|
+
tagged.template += strings[stringIndex];
|
|
107
|
+
|
|
108
|
+
if (value instanceof Node) {
|
|
109
|
+
tagged.nodes.push(value);
|
|
110
|
+
|
|
111
|
+
tagged.template += getComment(nodeIndex);
|
|
112
|
+
|
|
113
|
+
nodeIndex += 1;
|
|
114
|
+
} else if (hasNodes(value)) {
|
|
115
|
+
const items = [...value];
|
|
116
|
+
const itemsLength = items.length;
|
|
117
|
+
|
|
118
|
+
for (let itemIndex = 0; itemIndex < itemsLength; itemIndex += 1) {
|
|
119
|
+
const item = items[itemIndex];
|
|
120
|
+
|
|
121
|
+
if (item instanceof Node) {
|
|
122
|
+
tagged.nodes.push(item);
|
|
123
|
+
|
|
124
|
+
tagged.template += getComment(nodeIndex);
|
|
125
|
+
|
|
126
|
+
nodeIndex += 1;
|
|
127
|
+
} else {
|
|
128
|
+
tagged.template += getString(item);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} else if (Array.isArray(value)) {
|
|
132
|
+
const valueLength = value.length;
|
|
133
|
+
|
|
134
|
+
for (let valueIndex = 0; valueIndex < valueLength; valueIndex += 1) {
|
|
135
|
+
tagged.template += getString(value[valueIndex]);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
tagged.template += getString(value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return tagged;
|
|
143
|
+
}
|
|
144
|
+
|
|
99
145
|
function getTemplate(
|
|
100
146
|
value: string | HTMLTemplateElement,
|
|
101
147
|
options: Options,
|
|
@@ -108,7 +154,7 @@ function getTemplate(
|
|
|
108
154
|
return;
|
|
109
155
|
}
|
|
110
156
|
|
|
111
|
-
let template = templates
|
|
157
|
+
let template = templates.get(value);
|
|
112
158
|
|
|
113
159
|
if (template != null) {
|
|
114
160
|
return template;
|
|
@@ -119,34 +165,87 @@ function getTemplate(
|
|
|
119
165
|
return createTemplate(element instanceof HTMLTemplateElement ? element : value, options);
|
|
120
166
|
}
|
|
121
167
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
168
|
+
function hasNodes(value: unknown): value is HTMLCollection | NodeList | Node[] {
|
|
169
|
+
if (value instanceof HTMLCollection || value instanceof NodeList) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return Array.isArray(value) && value.some(item => item instanceof Node);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Create nodes from a template string
|
|
178
|
+
* @returns Created nodes
|
|
179
|
+
*/
|
|
180
|
+
export function html(strings: TemplateStringsArray, ...values: unknown[]): Node[];
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Create nodes from an HTML string or a template element
|
|
184
|
+
* @param value HTML string or id for a template element
|
|
185
|
+
* @param options Options for creating nodes
|
|
186
|
+
* @returns Created nodes
|
|
187
|
+
*/
|
|
188
|
+
export function html(value: string, options?: HtmlOptions): Node[];
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Create nodes from a template element
|
|
192
|
+
* @param template Template element
|
|
193
|
+
* @param options Options for creating nodes
|
|
194
|
+
* @returns Created nodes
|
|
195
|
+
*/
|
|
196
|
+
export function html(template: HTMLTemplateElement, options?: HtmlOptions): Node[];
|
|
197
|
+
|
|
198
|
+
export function html(first: unknown, ...second: unknown[]): Node[] {
|
|
199
|
+
if (isTagged(first)) {
|
|
200
|
+
const tagged = getTagged(first, second);
|
|
201
|
+
|
|
202
|
+
return getNodes(tagged.template, getOptions(), tagged.nodes);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return getNodes(first, getOptions(second[0]));
|
|
206
|
+
}
|
|
125
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Clear cache of template elements
|
|
210
|
+
*/
|
|
126
211
|
html.clear = (): void => {
|
|
127
|
-
templates
|
|
212
|
+
templates.clear();
|
|
128
213
|
};
|
|
129
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Remove cached template element for an HTML string or id
|
|
217
|
+
* @param template HTML string or id for a template element
|
|
218
|
+
*/
|
|
130
219
|
html.remove = (template: string): void => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
220
|
+
templates.delete(template);
|
|
221
|
+
};
|
|
134
222
|
|
|
135
|
-
|
|
136
|
-
|
|
223
|
+
function isTagged(value: unknown): value is TemplateStringsArray {
|
|
224
|
+
return Array.isArray(value) && Array.isArray((value as unknown as TemplateStringsArray).raw);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function replaceComments(origin: NodeList | Node[], replacements: Node[]): void {
|
|
228
|
+
const nodes = [...origin];
|
|
229
|
+
const {length} = nodes;
|
|
230
|
+
|
|
231
|
+
for (let nodeIndex = 0; nodeIndex < length; nodeIndex += 1) {
|
|
232
|
+
const node = nodes[nodeIndex];
|
|
137
233
|
|
|
138
|
-
|
|
234
|
+
if (node instanceof Comment) {
|
|
235
|
+
const [, index] = EXPRESSION_COMMENT.exec(node.textContent) ?? [];
|
|
139
236
|
|
|
140
|
-
|
|
141
|
-
|
|
237
|
+
if (index != null) {
|
|
238
|
+
node.replaceWith(replacements[Number(index)]);
|
|
239
|
+
}
|
|
142
240
|
|
|
143
|
-
|
|
144
|
-
updated[key] = templates[key];
|
|
241
|
+
continue;
|
|
145
242
|
}
|
|
146
|
-
}
|
|
147
243
|
|
|
148
|
-
|
|
149
|
-
|
|
244
|
+
if (node.hasChildNodes()) {
|
|
245
|
+
replaceComments(node.childNodes, replacements);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
150
249
|
|
|
151
250
|
/**
|
|
152
251
|
* Sanitize one or more nodes, recursively
|
|
@@ -158,7 +257,15 @@ export function sanitize(value: Node | Node[]): Node[] {
|
|
|
158
257
|
return sanitizeNodes(Array.isArray(value) ? value : [value], 0);
|
|
159
258
|
}
|
|
160
259
|
|
|
161
|
-
//
|
|
260
|
+
// #endregion
|
|
261
|
+
|
|
262
|
+
// #region Variables
|
|
263
|
+
|
|
264
|
+
const COMMENT_INDEX = '<index>';
|
|
265
|
+
|
|
266
|
+
const COMMENT_TEMPLATE = `<!--toretto.node:${COMMENT_INDEX}-->`;
|
|
267
|
+
|
|
268
|
+
const EXPRESSION_COMMENT = /^toretto\.node:(\d+)$/;
|
|
162
269
|
|
|
163
270
|
const EXPRESSION_ID = /^[a-z][\w-]*$/i;
|
|
164
271
|
|
|
@@ -168,10 +275,11 @@ const TEMPLATE_TAG = 'template';
|
|
|
168
275
|
|
|
169
276
|
const TEMPORARY_ELEMENT = '<toretto-temporary></toretto-temporary>';
|
|
170
277
|
|
|
171
|
-
|
|
278
|
+
const templates = new SizedMap<string, HTMLTemplateElement>(128);
|
|
172
279
|
|
|
173
|
-
let
|
|
280
|
+
let parser: DOMParser;
|
|
174
281
|
|
|
175
|
-
//
|
|
282
|
+
// @ts-expect-error debug
|
|
283
|
+
window.templates = templates;
|
|
176
284
|
|
|
177
|
-
|
|
285
|
+
// #endregion
|
package/src/html/sanitize.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import {setAttribute} from '../attribute/set';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
_isInvalidBooleanAttribute,
|
|
6
|
-
} from '../internal/attribute';
|
|
1
|
+
import {setAttribute} from '../attribute/set.attribute';
|
|
2
|
+
import {_isBadAttribute, _isInvalidBooleanAttribute} from '../internal/attribute';
|
|
3
|
+
|
|
4
|
+
// #region Functions
|
|
7
5
|
|
|
8
6
|
function handleElement(element: Element, depth: number): void {
|
|
9
7
|
if (depth === 0) {
|
|
10
|
-
const removable = element.querySelectorAll(REMOVE_SELECTOR);
|
|
8
|
+
const removable = [...element.querySelectorAll(REMOVE_SELECTOR)];
|
|
9
|
+
const {length} = removable;
|
|
11
10
|
|
|
12
|
-
for (
|
|
13
|
-
|
|
11
|
+
for (let index = 0; index < length; index += 1) {
|
|
12
|
+
removable[index].remove();
|
|
14
13
|
}
|
|
15
14
|
}
|
|
16
15
|
|
|
@@ -49,7 +48,7 @@ export function sanitizeAttributes(element: Element, attributes: Attr[]): void {
|
|
|
49
48
|
for (let index = 0; index < length; index += 1) {
|
|
50
49
|
const {name, value} = attributes[index];
|
|
51
50
|
|
|
52
|
-
if (_isBadAttribute(name, value, false)
|
|
51
|
+
if (_isBadAttribute(name, value, false)) {
|
|
53
52
|
element.removeAttribute(name);
|
|
54
53
|
} else if (_isInvalidBooleanAttribute(name, value, false)) {
|
|
55
54
|
setAttribute(element, name, true);
|
|
@@ -103,8 +102,12 @@ export function sanitizeNodes(nodes: Node[], depth: number): Node[] {
|
|
|
103
102
|
return nodes;
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
//
|
|
105
|
+
// #endregion
|
|
106
|
+
|
|
107
|
+
// #region Variables
|
|
107
108
|
|
|
108
109
|
const COMMENT_HARMFUL = /<[/\w]/g;
|
|
109
110
|
|
|
110
111
|
const REMOVE_SELECTOR = 'script, toretto-temporary';
|
|
112
|
+
|
|
113
|
+
// #endregion
|