solarite 0.7.0 → 0.8.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/Solarite-debug.js +5193 -4114
- package/dist/Solarite.js +4951 -3878
- package/dist/Solarite.min.js +2 -2
- package/dist/jsx-dev-runtime.js +3 -0
- package/dist/jsx-runtime.js +86 -0
- package/package.json +11 -10
- package/readme.md +8 -8
- package/src/MappedList.js +34 -0
- package/src/MultiValueMap.js +4 -4
- package/src/NodeGroup.js +205 -119
- package/src/Path.js +52 -53
- package/src/PathToAttribValue.js +182 -101
- package/src/PathToAttribs.js +10 -18
- package/src/PathToComponent.js +43 -19
- package/src/PathToEvent.js +18 -12
- package/src/PathToKey.js +0 -6
- package/src/PathToNodes.js +695 -155
- package/src/RootNodeGroup.js +23 -30
- package/src/Selector.js +207 -0
- package/src/Shell.js +276 -117
- package/src/Solarite.d.ts +89 -13
- package/src/Solarite.js +38 -25
- package/src/Template.js +20 -7
- package/src/Util.js +75 -60
- package/src/assert.js +1 -1
- package/src/assignAttributes.js +20 -19
- package/src/h.js +64 -37
- package/src/jsx.js +4 -7
- package/src/toEl.js +1 -1
- package/src/HtmlParser.js +0 -91
- package/src/PathToComment.js +0 -8
package/src/Solarite.d.ts
CHANGED
|
@@ -10,14 +10,17 @@ export interface RenderOptions {
|
|
|
10
10
|
ids?: boolean;
|
|
11
11
|
render?: boolean;
|
|
12
12
|
|
|
13
|
-
/** Defaults to true: bubbling events (click, input, etc.) dispatch from one
|
|
14
|
-
*
|
|
15
|
-
* of large lists. Pass false to bind every event directly, or an
|
|
16
|
-
* the listed event names.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
13
|
+
/** Defaults to true: bubbling events (click, input, etc.) dispatch from one listener on
|
|
14
|
+
* the component's root element instead of addEventListener per element - much faster
|
|
15
|
+
* creation and teardown of large lists. Pass false to bind every event directly, or an
|
|
16
|
+
* array to delegate only the listed event names. Pass 'document' to also register the
|
|
17
|
+
* dispatcher on the document, so handlers keep firing on nodes that get re-parented
|
|
18
|
+
* outside the component (e.g. a toolbar a dock parks in its own chrome). Non-bubbling
|
|
19
|
+
* events always bind directly. Note: delegated handlers run when the event bubbles to
|
|
20
|
+
* the root (or document), so stopPropagation() in a manually added listener on an element
|
|
21
|
+
* in between suppresses them, and such manual listeners fire first. A programmatically
|
|
22
|
+
* dispatched non-bubbling event won't reach delegated handlers. */
|
|
23
|
+
eventDelegation?: boolean | string[] | 'document';
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
/**
|
|
@@ -37,11 +40,17 @@ declare function h(): (htmlStrings: TemplateStringsArray, ...exprs: any[]) => No
|
|
|
37
40
|
|
|
38
41
|
declare namespace h {
|
|
39
42
|
/** Render a list, reusing each item's DOM while the item is the SAME object; replace an item
|
|
40
|
-
* (don't mutate it) to re-render it. fn runs only for
|
|
41
|
-
|
|
43
|
+
* (don't mutate it) to re-render it. fn runs only for rows the reconciler can't match.
|
|
44
|
+
* No deps: identity is the dep. Returns a MappedList, not an array — put it straight into
|
|
45
|
+
* a template expression; it's iterable if you really need the templates. */
|
|
46
|
+
function map<T>(items:T[], fn:(item:T) => Template): MappedList<T>;
|
|
42
47
|
|
|
43
48
|
/** Alias of h.map with a name that flags the immutability contract at the call site. */
|
|
44
|
-
function immutableMap<T>(items:T[], fn:(item:T) => Template):
|
|
49
|
+
function immutableMap<T>(items:T[], fn:(item:T) => Template): MappedList<T>;
|
|
50
|
+
|
|
51
|
+
/** A selection that writes only the two rows it affects, with no render() call. Bind with
|
|
52
|
+
* when() as a whole attribute value; move it with set(). */
|
|
53
|
+
function selector<K = any>(key?: K | null): Selector<K>;
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
/** Tagged template literal for SVG markup and SVG child fragments. */
|
|
@@ -54,7 +63,26 @@ export {h, svg};
|
|
|
54
63
|
/**
|
|
55
64
|
* Solarite provides more features if your web component extends Solarite instead of HTMLElement. */
|
|
56
65
|
export class Solarite extends HTMLElement {
|
|
57
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Fill in and fix up the attribs object a component's constructor receives, so the component
|
|
68
|
+
* can then copy those values onto its own fields, e.g. with ObjectUtil.assign(this, attribs).
|
|
69
|
+
*
|
|
70
|
+
* 1. If attribs is an empty object, fill it with the attributes on the DOM element.
|
|
71
|
+
* This happens when the browser creates the element from plain html, because then nothing
|
|
72
|
+
* calls the constructor with arguments. Attribute names convert from dash-case to
|
|
73
|
+
* camelCase, and `${...}` values are parsed from JSON.
|
|
74
|
+
* 2. If types is given, convert attribs values from strings to those types. Attribute values
|
|
75
|
+
* written as literal text always arrive as strings, whether from plain html or from an h()
|
|
76
|
+
* template. types maps a field name to Number, Boolean, String, Date, or any function
|
|
77
|
+
* taking the string and returning a value. Boolean is true for every string except
|
|
78
|
+
* 'false' and '0', so a bare attribute like `<select-box-3 editable>` becomes true.
|
|
79
|
+
* Values that are already not strings, like a `${true}` template expression, are left alone.
|
|
80
|
+
*
|
|
81
|
+
* This runs before the subclass initializes its fields and renders, so converted values are
|
|
82
|
+
* right the first time, even for fields that change what render() builds. This constructor
|
|
83
|
+
* can't copy attribs onto fields itself, because subclass field initializers run after it
|
|
84
|
+
* finishes and would overwrite them; that's why the subclass does the final assign. */
|
|
85
|
+
constructor(attribs?: Record<string, any> | null, types?: Record<string, Function> | null);
|
|
58
86
|
render(attribs?: Record<string, any>, changed?:boolean): void;
|
|
59
87
|
renderFirstTime(): void;
|
|
60
88
|
connectedCallback(): void;
|
|
@@ -88,6 +116,54 @@ export function getEventBinding(node: Node, key: string): EventBinding | undefin
|
|
|
88
116
|
* and '0', so a bare attribute reads as true. Field names in `ignore` are skipped. */
|
|
89
117
|
export function assignAttributes(dest: HTMLElement, types?: Record<string, Function>, ignore?: string[]): void;
|
|
90
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Convert an attribute string with the given converter: Number, Boolean, String, Date,
|
|
121
|
+
* or any function taking the string and returning a value. Boolean is true for any string
|
|
122
|
+
* except 'false' and '0', so a bare attribute reads as true. No converter returns the
|
|
123
|
+
* string unchanged. */
|
|
124
|
+
export function convertType(value: string, type?: Function): any;
|
|
125
|
+
|
|
126
|
+
/** What h.map() returns: the source items plus the callback that builds one item's Template,
|
|
127
|
+
* so the reconciler can recognize an unchanged row by the item it was built from. Iterating it
|
|
128
|
+
* yields the Templates, building each one, for code that needs an array. */
|
|
129
|
+
export class MappedList<T = any> {
|
|
130
|
+
items: T[];
|
|
131
|
+
fn: (item: T) => Template;
|
|
132
|
+
constructor(items: T[], fn: (item: T) => Template);
|
|
133
|
+
[Symbol.iterator](): IterableIterator<Template>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** What h.selector() returns. Holds one selected key; when() binds an attribute to whether a
|
|
137
|
+
* row's key is that one, and set() moves the selection by writing only the rows that change. */
|
|
138
|
+
export class Selector<K = any> {
|
|
139
|
+
constructor(key?: K | null);
|
|
140
|
+
|
|
141
|
+
/** The selected key, or null. */
|
|
142
|
+
readonly key: K | null;
|
|
143
|
+
|
|
144
|
+
/** Bind an attribute to whether key is the selected one. Must supply the WHOLE attribute
|
|
145
|
+
* value, on the row's own root element, and the rows must be keyed — each of those throws
|
|
146
|
+
* otherwise. An off value of '' (the default) leaves no attribute rather than an empty one. */
|
|
147
|
+
when(key: K, on: any, off?: any): SelectorRef<K>;
|
|
148
|
+
|
|
149
|
+
/** Move the selection, writing at most two attributes and calling no render().
|
|
150
|
+
* Pass null to deselect. */
|
|
151
|
+
set(key: K | null): void;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** What when() returns. A Selector owns exactly TWO of these — one meaning "this row is the
|
|
155
|
+
* selected one" and one meaning "it isn't" — rather than one per key, so drawing a row costs no
|
|
156
|
+
* allocation. The stable identity is also what lets an unchanged row skip its write on a
|
|
157
|
+
* re-render: a row's expression changes identity exactly when its selectedness does. */
|
|
158
|
+
export class SelectorRef<K = any> {
|
|
159
|
+
readonly selector: Selector<K>;
|
|
160
|
+
|
|
161
|
+
/** True for the selector's "selected" singleton, false for its "unselected" one. */
|
|
162
|
+
readonly selected: boolean;
|
|
163
|
+
|
|
164
|
+
value(): any;
|
|
165
|
+
}
|
|
166
|
+
|
|
91
167
|
export class Template {
|
|
92
168
|
exprs: any[];
|
|
93
169
|
html: string[];
|
|
@@ -126,7 +202,7 @@ export const SolariteUtil: {
|
|
|
126
202
|
bindStyles(style: HTMLStyleElement, root: HTMLElement): void;
|
|
127
203
|
camelToDashes(str: string): string;
|
|
128
204
|
dashesToCamel(str: string): string;
|
|
129
|
-
defineClass(Class: typeof HTMLElement, tagName?: string | null):
|
|
205
|
+
defineClass(Class: typeof HTMLElement, tagName?: string | null): string;
|
|
130
206
|
isIterable(obj: any): boolean;
|
|
131
207
|
trimEmptyNodes(nodes: NodeList | Node[]): Node[];
|
|
132
208
|
[key: string]: any;
|
package/src/Solarite.js
CHANGED
|
@@ -7,15 +7,24 @@ JavasCript UI library
|
|
|
7
7
|
@copyright Vorticode LLC
|
|
8
8
|
https://vorticode.github.io/solarite/ */
|
|
9
9
|
import h from './h.js';
|
|
10
|
+
import {convertType} from './assignAttributes.js';
|
|
10
11
|
export default h;
|
|
11
12
|
export {default as delve} from './delve.js';
|
|
12
13
|
export {default as Template} from './Template.js';
|
|
14
|
+
export {default as MappedList} from './MappedList.js';
|
|
15
|
+
export {default as Selector, SelectorRef} from './Selector.js';
|
|
13
16
|
export {default as toEl} from './toEl.js';
|
|
14
|
-
export {assignAttributes} from './assignAttributes.js';
|
|
17
|
+
export {assignAttributes, convertType} from './assignAttributes.js';
|
|
15
18
|
export {svg} from './h.js';
|
|
16
19
|
export {getEventBinding} from './PathToAttribValue.js';
|
|
17
20
|
export {Fragment} from './jsx.js';
|
|
18
21
|
|
|
22
|
+
// Internals the JSX runtime needs, exported so dist/jsx-runtime.js can be built as a SEPARATE
|
|
23
|
+
// module that shares this bundle rather than bundling its own copy. They must be shared, not
|
|
24
|
+
// duplicated: PathToAttribs tests `instanceof JsxAttr`, which fails across two copies of the
|
|
25
|
+
// class. Not part of the documented API — jsx-runtime is the supported entry point.
|
|
26
|
+
export {jsxToTemplate as internalJsxToTemplate, JsxAttr as InternalJsxAttr} from './jsx.js';
|
|
27
|
+
|
|
19
28
|
// Experimental:
|
|
20
29
|
//--------------
|
|
21
30
|
export {default as Globals} from './Globals.js';
|
|
@@ -25,7 +34,7 @@ export {default as SolariteUtil} from './Util.js';
|
|
|
25
34
|
//--------------
|
|
26
35
|
export {default as h} from './h.js'; // Named exports for h() are deprecated.
|
|
27
36
|
|
|
28
|
-
//
|
|
37
|
+
// NodeGroup and Shell are internal; tests import them directly from their modules.
|
|
29
38
|
|
|
30
39
|
|
|
31
40
|
|
|
@@ -65,42 +74,46 @@ let HTMLElementAutoDefine = new Proxy(HTMLElement, {
|
|
|
65
74
|
export class Solarite extends HTMLElementAutoDefine {
|
|
66
75
|
|
|
67
76
|
/**
|
|
68
|
-
*
|
|
69
|
-
|
|
77
|
+
* Fill in and fix up the attribs object a component's constructor receives, so the component
|
|
78
|
+
* can then copy those values onto its own fields, e.g. with ObjectUtil.assign(this, attribs).
|
|
79
|
+
*
|
|
80
|
+
* 1. If attribs is an empty object, fill it with the attributes on the DOM element.
|
|
81
|
+
* This happens when the browser creates the element from plain html, because then nothing
|
|
82
|
+
* calls the constructor with arguments. Attribute names convert from dash-case to
|
|
83
|
+
* camelCase, and `${...}` values are parsed from JSON.
|
|
84
|
+
* 2. If types is given, convert attribs values from strings to those types. Attribute values
|
|
85
|
+
* written as literal text always arrive as strings, whether from plain html or from an h()
|
|
86
|
+
* template. types maps a field name to Number, Boolean, String, Date, or any function
|
|
87
|
+
* taking the string and returning a value. Boolean is true for every string except
|
|
88
|
+
* 'false' and '0', so a bare attribute like `<select-box-3 editable>` becomes true.
|
|
89
|
+
* Values that are already not strings, like a `${true}` template expression, are left alone.
|
|
90
|
+
*
|
|
91
|
+
* This runs before the subclass initializes its fields and renders, so converted values are
|
|
92
|
+
* right the first time, even for fields that change what render() builds. This constructor
|
|
93
|
+
* can't copy attribs onto fields itself, because subclass field initializers run after it
|
|
94
|
+
* finishes and would overwrite them; that's why the subclass does the final assign.
|
|
95
|
+
* @param attribs {?Record<string, any>}
|
|
96
|
+
* @param types {?Record<string, Function>} */
|
|
97
|
+
constructor(attribs=null, types=null) {
|
|
70
98
|
super();
|
|
71
99
|
|
|
72
100
|
if (attribs) {
|
|
73
101
|
if (typeof attribs !== 'object')
|
|
74
|
-
throw new Error('First argument
|
|
102
|
+
throw new Error('First argument must be an object.');
|
|
75
103
|
|
|
76
104
|
// 1. Populate attribs if it's an empty object.
|
|
77
|
-
if (
|
|
105
|
+
if (!Object.keys(attribs).length) {
|
|
78
106
|
let attribs2 = Solarite.getAttribs(this);
|
|
79
107
|
for (let name in attribs2) {
|
|
80
108
|
attribs[name] = attribs2[name];
|
|
81
109
|
}
|
|
82
110
|
}
|
|
83
111
|
|
|
84
|
-
// 2.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// const descriptor = Object.getOwnPropertyDescriptor(this, name);
|
|
89
|
-
// if (!descriptor || descriptor.writable || descriptor.set)
|
|
90
|
-
// this[name] = attribs[name];
|
|
91
|
-
// }
|
|
92
|
-
//}
|
|
112
|
+
// 2. Convert string values to the types the component declares.
|
|
113
|
+
for (let name in types || {})
|
|
114
|
+
if (typeof attribs[name] === 'string')
|
|
115
|
+
attribs[name] = convertType(attribs[name], types[name]);
|
|
93
116
|
}
|
|
94
|
-
|
|
95
|
-
// 3. Wrap render function so it always provides the attribs argument.
|
|
96
|
-
// Disabled because this gives us strings for attribute values when we call render manually.
|
|
97
|
-
// Instead of values given from ${...} expressions.
|
|
98
|
-
// let originalRender = this.render;
|
|
99
|
-
// this.render = (attribs, changed=true) => {
|
|
100
|
-
// if (!attribs) // If we have to look up the attribs, we don't know if they changed or not.
|
|
101
|
-
// attribs = Solarite.getAttribs(this);
|
|
102
|
-
// originalRender.call(this, attribs, changed);
|
|
103
|
-
// }
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
'render'() {
|
package/src/Template.js
CHANGED
|
@@ -53,7 +53,7 @@ export default class Template {
|
|
|
53
53
|
|
|
54
54
|
//this.trace = new Error().stack.split(/\n/g)
|
|
55
55
|
|
|
56
|
-
//#
|
|
56
|
+
//#IFDEBUG
|
|
57
57
|
assert(Array.isArray(htmlStrings))
|
|
58
58
|
assert(Array.isArray(exprs))
|
|
59
59
|
|
|
@@ -78,8 +78,12 @@ export default class Template {
|
|
|
78
78
|
if (!ng) {
|
|
79
79
|
ng = new RootNodeGroup(this, null, el, options);
|
|
80
80
|
if (!el) // null if it's a standalone elment.
|
|
81
|
-
el = ng.
|
|
82
|
-
|
|
81
|
+
el = ng.getRootEl();
|
|
82
|
+
|
|
83
|
+
// RootNodeGroup.instantiate() ends by registering itself under its own rootEl, which
|
|
84
|
+
// is the element we were given, or -- when we were given none -- the very element
|
|
85
|
+
// getRootEl() just handed back. Registering it a second time here stored the same
|
|
86
|
+
// group under the same key.
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
// Make sure the expresion count matches match the Path "hole" count.
|
|
@@ -94,8 +98,13 @@ export default class Template {
|
|
|
94
98
|
// If we didn't just create it, we need to render it.
|
|
95
99
|
if (this.html?.length === 1 && !this.html[0]) // An empty string.
|
|
96
100
|
el.innerHTML = ''; // Fast path for empty component.
|
|
97
|
-
else
|
|
98
|
-
|
|
101
|
+
else {
|
|
102
|
+
// A component renders the same template every time, so hand over the expressions it
|
|
103
|
+
// applied last time; paths that can prove an unchanged expression is a no-op skip.
|
|
104
|
+
let last = ng.template;
|
|
105
|
+
ng.applyExprs(this.exprs, true, last !== this && last.html === this.html ? last.exprs : null);
|
|
106
|
+
ng.template = this;
|
|
107
|
+
}
|
|
99
108
|
|
|
100
109
|
return el;
|
|
101
110
|
}
|
|
@@ -125,9 +134,13 @@ export default class Template {
|
|
|
125
134
|
export function templatesSame(a, b) {
|
|
126
135
|
if (a.html === b.html && a.svgMode === b.svgMode) {
|
|
127
136
|
let ae = a.exprs, be = b.exprs;
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
// Most expressions are identical between renders, so test that here rather than paying
|
|
138
|
+
// a call into exprSame() to learn it.
|
|
139
|
+
for (let i=0; i<ae.length; i++) {
|
|
140
|
+
let x = ae[i], y = be[i];
|
|
141
|
+
if (x !== y && !exprSame(x, y))
|
|
130
142
|
return false;
|
|
143
|
+
}
|
|
131
144
|
return true;
|
|
132
145
|
}
|
|
133
146
|
|
package/src/Util.js
CHANGED
|
@@ -40,13 +40,15 @@ let Util = {
|
|
|
40
40
|
// Don't clobber a non-element value. For a simple (non-nested) id this covers two cases:
|
|
41
41
|
// an inherited/built-in property like `title` or `style`, or an own property that already
|
|
42
42
|
// holds a non-Node value. A previously-bound element (a Node) is fine to re-assign.
|
|
43
|
+
// This can only fail on a mistake in the component's own template, so a developer meets it
|
|
44
|
+
// the first time the component renders and never again at runtime. It nonetheless SHIPS,
|
|
45
|
+
// and deliberately: debug-strip blocks are removed from dist/Solarite.js, which is what
|
|
46
|
+
// npm serves, so hiding it there would delete it for everyone, not only for production.
|
|
43
47
|
if (!id.includes('.')) {
|
|
44
48
|
let existing = root[id];
|
|
45
49
|
let isInherited = (id in root) && !Object.hasOwn(root, id);
|
|
46
50
|
if (!existing?.nodeType && (existing != null || isInherited))
|
|
47
|
-
throw new Error(
|
|
48
|
-
`<${el.tagName.toLowerCase()} id="${id}"> because it would clobber an existing ` +
|
|
49
|
-
`${isInherited ? 'built-in ' : ''}property. Rename the id or the property.`);
|
|
51
|
+
throw new Error(`Solarite: id="${id}" would overwrite an existing ${root.constructor.name} property.`);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
delve(root, id.split(/\./g), el);
|
|
@@ -65,29 +67,29 @@ let Util = {
|
|
|
65
67
|
bindStyles(style, root) {
|
|
66
68
|
|
|
67
69
|
let tagName = root.tagName.toLowerCase();
|
|
68
|
-
|
|
70
|
+
|
|
71
|
+
// A global style is scoped by tag name alone, so it needs no attribute in the selector.
|
|
72
|
+
let attribSelector = '';
|
|
69
73
|
|
|
70
74
|
if (style.hasAttribute('global') || style.hasAttribute('data-global')) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!doc.head.querySelector(`style[data-style="${styleId}"]`)) {
|
|
75
|
-
doc.head.append(style)
|
|
76
|
-
style.setAttribute('data-style', styleId);
|
|
77
|
-
}
|
|
78
|
-
else // TODO: Make sure the style has no expressions.
|
|
75
|
+
let head = Globals.doc.head;
|
|
76
|
+
if (head.querySelector(`style[data-style="${tagName}"]`))
|
|
77
|
+
// TODO: Make sure the style has no expressions.
|
|
79
78
|
style.remove(); // already in the head.
|
|
79
|
+
else {
|
|
80
|
+
head.append(style)
|
|
81
|
+
style.setAttribute('data-style', tagName);
|
|
82
|
+
}
|
|
80
83
|
}
|
|
81
84
|
else {
|
|
82
85
|
let styleId = root.getAttribute('data-style');
|
|
83
86
|
if (!styleId) {
|
|
84
|
-
// Keep track of one style id for each class.
|
|
87
|
+
// Keep track of one style id for each class. Reading the static walks up to a parent
|
|
88
|
+
// class's counter if this class has never been styled, but the assignment always lands
|
|
89
|
+
// on this class, so each class then counts on from where its parent left off.
|
|
85
90
|
// TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
styleId = root.constructor.styleId++;
|
|
89
|
-
|
|
90
|
-
root.setAttribute('data-style', styleId);
|
|
91
|
+
let Class = root.constructor;
|
|
92
|
+
root.setAttribute('data-style', styleId = Class.styleId = (Class.styleId || 0) + 1);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
attribSelector = `[data-style="${styleId}"]`;
|
|
@@ -97,7 +99,19 @@ let Util = {
|
|
|
97
99
|
for (let child of style.childNodes) {
|
|
98
100
|
if (child.nodeType === 3) {
|
|
99
101
|
let oldText = child.textContent;
|
|
100
|
-
|
|
102
|
+
|
|
103
|
+
// One pass rewrites both forms of the selector:
|
|
104
|
+
// 1. The functional form ':host(X)' — the host element when it also matches X — unwraps
|
|
105
|
+
// so X sits right after the scoped name: tag[data-style="1"]X. X may hold one
|
|
106
|
+
// nested group like ':not(.open)'; deeper parentheses can't be paired by a regex,
|
|
107
|
+
// so such an X is left as written rather than half-rewritten into a selector the
|
|
108
|
+
// browser would discard silently.
|
|
109
|
+
// 2. Plain ':host'. The lookahead turns down longer names (':host-context') and '(',
|
|
110
|
+
// which only follows ':host' when alternative 1 already gave up on it, and accepts
|
|
111
|
+
// the end of the text node, where an expression may have split a dynamic style.
|
|
112
|
+
let newText = oldText.replace(
|
|
113
|
+
/:host(?:\(((?:[^()]|\([^()]*\))*)\)|(?![-a-z0-9_(]))/gi,
|
|
114
|
+
`${tagName}${attribSelector}$1`);
|
|
101
115
|
if (oldText !== newText)
|
|
102
116
|
child.textContent = newText;
|
|
103
117
|
}
|
|
@@ -118,17 +132,15 @@ let Util = {
|
|
|
118
132
|
* 'UIForm' => 'ui-form'
|
|
119
133
|
* 'A100' => 'a-100' */
|
|
120
134
|
camelToDashes(str) {
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// Convert all the remaining capital letters to lowercase.
|
|
131
|
-
return str.toLowerCase();
|
|
135
|
+
// One pass finds all three dash positions. Each alternative matches only the character
|
|
136
|
+
// *before* the boundary and uses a lookahead for what follows, so the following character
|
|
137
|
+
// is never consumed and can still start the next boundary. That's what lets the three
|
|
138
|
+
// rules interleave in a single scan the way three sequential replaces used to:
|
|
139
|
+
// 1. a lowercase letter or digit before a capital ('ProperName').
|
|
140
|
+
// 2. a capital before a capital+lowercase pair, i.e. the last capital of a run ('HTMLElement').
|
|
141
|
+
// 3. a letter before a digit ('A100').
|
|
142
|
+
// '$&-' appends the dash after the matched character, then everything folds to lowercase.
|
|
143
|
+
return str.replace(/[a-z0-9](?=[A-Z])|[A-Z](?=[A-Z][a-z])|[a-zA-Z](?=\d)/g, '$&-').toLowerCase();
|
|
132
144
|
},
|
|
133
145
|
|
|
134
146
|
/**
|
|
@@ -144,13 +156,24 @@ let Util = {
|
|
|
144
156
|
return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
|
145
157
|
},
|
|
146
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Register Class as a custom element, unless it's registered already.
|
|
161
|
+
* @param Class {typeof HTMLElement}
|
|
162
|
+
* @param tagName {?string} Name to register under. Defaults to the dashed form of the class name.
|
|
163
|
+
* @return {string} The tag name Class is registered under, whether we just registered it or it
|
|
164
|
+
* was already in the registry under some other name. Callers that emit markup for the class
|
|
165
|
+
* use this instead of re-deriving the name, which guesses wrong for any class registered
|
|
166
|
+
* under a name that isn't camelToDashes(Class.name). */
|
|
147
167
|
defineClass(Class, tagName) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
168
|
+
let defined = customElements[getName](Class);
|
|
169
|
+
if (defined) // Previously defined.
|
|
170
|
+
return defined;
|
|
171
|
+
|
|
172
|
+
tagName = tagName || Util.camelToDashes(Class.name)
|
|
173
|
+
if (!tagName.includes('-')) // Browsers require that web components always have a dash in the name.
|
|
174
|
+
tagName += '-element';
|
|
175
|
+
customElements[define](tagName, Class)
|
|
176
|
+
return tagName;
|
|
154
177
|
},
|
|
155
178
|
|
|
156
179
|
/**
|
|
@@ -177,8 +200,8 @@ let Util = {
|
|
|
177
200
|
return node.value; // String
|
|
178
201
|
},
|
|
179
202
|
|
|
180
|
-
isEvent(
|
|
181
|
-
return
|
|
203
|
+
isEvent(attribName) {
|
|
204
|
+
return attribName.startsWith('on') && attribName in Globals.div;
|
|
182
205
|
},
|
|
183
206
|
|
|
184
207
|
/**
|
|
@@ -215,16 +238,13 @@ let Util = {
|
|
|
215
238
|
* @returns {Object} */
|
|
216
239
|
splitAttribs(str) {
|
|
217
240
|
let result = {};
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
value = (value || '').replace(/^(['"])(.*)\1$/, '$2'); // trim value quotes if they match.
|
|
226
|
-
result[name] = value;
|
|
227
|
-
}
|
|
241
|
+
|
|
242
|
+
// One scan collects every name and its value. The value is optional so a boolean attribute
|
|
243
|
+
// written on its own ('disabled') still lands in the result with an empty value, and the three
|
|
244
|
+
// value alternatives capture *inside* the quotes so no separate quote-trimming pass is needed.
|
|
245
|
+
// Whatever doesn't look like an attribute name is skipped rather than becoming a bogus key.
|
|
246
|
+
(str + '').replace(/([\w-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g,
|
|
247
|
+
(_, name, dq, sq, bare) => result[name] = dq ?? sq ?? bare ?? '');
|
|
228
248
|
|
|
229
249
|
return result;
|
|
230
250
|
},
|
|
@@ -262,19 +282,14 @@ let Util = {
|
|
|
262
282
|
* @param nodes {Node[]|NodeList}
|
|
263
283
|
* @returns {Node[]} */
|
|
264
284
|
trimEmptyNodes(nodes) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// Convert nodeList to an array for easier manipulation
|
|
270
|
-
const result = [...nodes]
|
|
285
|
+
// nodeType 1 is an element and 3 is a text node; the literals are what Node.ELEMENT_NODE
|
|
286
|
+
// and Node.TEXT_NODE are defined as, and they cost a fraction of the bytes.
|
|
287
|
+
let isEmpty = node => node.nodeType !== 1 && (node.nodeType !== 3 || !node.textContent.trim());
|
|
271
288
|
|
|
272
|
-
//
|
|
273
|
-
while (result.length
|
|
289
|
+
let result = [...nodes]; // A NodeList can't shift() or pop().
|
|
290
|
+
while (result.length && isEmpty(result[0]))
|
|
274
291
|
result.shift();
|
|
275
|
-
|
|
276
|
-
// Trim from the end
|
|
277
|
-
while (result.length > 0 && shouldTrimNode(result[result.length - 1]))
|
|
292
|
+
while (result.length && isEmpty(result[result.length - 1]))
|
|
278
293
|
result.pop();
|
|
279
294
|
|
|
280
295
|
return result;
|
|
@@ -292,7 +307,7 @@ export default Util;
|
|
|
292
307
|
|
|
293
308
|
|
|
294
309
|
// For debugging only
|
|
295
|
-
//#
|
|
310
|
+
//#IFDEBUG
|
|
296
311
|
export function setIndent(items, level=1) {
|
|
297
312
|
if (typeof items === 'string')
|
|
298
313
|
items = items.split(/\r?\n/g)
|
package/src/assert.js
CHANGED
package/src/assignAttributes.js
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
/*
|
|
2
|
-
┏┓ ┓ •
|
|
3
|
-
┗┓┏┓┃┏┓┏┓┓╋▗▖
|
|
4
|
-
┗┛┗┛┗┗┻╹ ╹╹┗
|
|
5
|
-
JavaScript UI library
|
|
6
|
-
@license MIT
|
|
7
|
-
@copyright Vorticode LLC
|
|
8
|
-
https://vorticode.github.io/solarite/ */
|
|
9
|
-
|
|
10
1
|
import Util from "./Util.js";
|
|
11
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Convert an attribute string with the given converter: Number, Boolean, String, Date,
|
|
5
|
+
* or any function taking the string and returning a value. Boolean is true for any string
|
|
6
|
+
* except 'false' and '0', so a bare attribute like `<my-timer auto-start>` reads as true.
|
|
7
|
+
* Date uses new Date(value). No converter returns the string unchanged. */
|
|
8
|
+
export function convertType(value, type) {
|
|
9
|
+
if (type === Date)
|
|
10
|
+
return new Date(value);
|
|
11
|
+
if (type === Boolean)
|
|
12
|
+
return !['false', '0'].includes(value);
|
|
13
|
+
// Number and String need no cases of their own: they're plain functions, so the custom
|
|
14
|
+
// branch below calls them correctly. Date and Boolean are the ones that can't fall through
|
|
15
|
+
// (Date without `new` returns a string; Boolean('false') is true).
|
|
16
|
+
if (type) // Number, String, or a custom string=>value function
|
|
17
|
+
return type(value);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
/**
|
|
13
22
|
* Read an element's html attributes onto fields that already exist on the element.
|
|
14
23
|
* Typically called from a web component constructor to support plain-html instantiation
|
|
@@ -43,16 +52,8 @@ export function assignAttributes(dest, types={}, ignore=[]) {
|
|
|
43
52
|
dest[name] = JSON.parse(value.slice(2, -1));
|
|
44
53
|
|
|
45
54
|
// 2. Cast the string with the converter named in `types`, if any.
|
|
46
|
-
else if (type
|
|
47
|
-
dest[name] =
|
|
48
|
-
else if (type === Boolean)
|
|
49
|
-
dest[name] = !['false', '0'].includes(value);
|
|
50
|
-
else if (type === Number)
|
|
51
|
-
dest[name] = Number(value);
|
|
52
|
-
else if (type === String)
|
|
53
|
-
dest[name] = String(value);
|
|
54
|
-
else if (type) // custom string=>value function
|
|
55
|
-
dest[name] = type(value);
|
|
55
|
+
else if (type)
|
|
56
|
+
dest[name] = convertType(value, type);
|
|
56
57
|
|
|
57
58
|
// 3. No converter named: assign the raw string. But an empty value over a function/object
|
|
58
59
|
// field is just the serialization residue of a template expression (functions render as
|