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/RootNodeGroup.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import NodeGroup from './NodeGroup.js';
|
|
2
2
|
import Globals from './Globals.js';
|
|
3
|
+
import Util from './Util.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Has these properties not present on NodeGroup, assigned by instantiate():
|
|
6
7
|
* They're not declared as fields because subclass field initializers run after the
|
|
7
8
|
* super constructor and would overwrite the assigned values.
|
|
8
|
-
* @property {HTMLElement}
|
|
9
|
-
* @property {?object}
|
|
9
|
+
* @property {HTMLElement} rootEl - Root node at the top of the hierarchy.
|
|
10
|
+
* @property {?object} renderOptions - RenderOptions */
|
|
10
11
|
export default class RootNodeGroup extends NodeGroup {
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -15,19 +16,19 @@ export default class RootNodeGroup extends NodeGroup {
|
|
|
15
16
|
* Called by the NodeGroup constructor. */
|
|
16
17
|
instantiate(shell, shellFragment, el, options) {
|
|
17
18
|
let startingPathDepth = 0;
|
|
18
|
-
this.
|
|
19
|
+
this.renderOptions = options;
|
|
19
20
|
if (shellFragment instanceof Text) {
|
|
20
21
|
if (!el)
|
|
21
|
-
throw new Error('
|
|
22
|
+
throw new Error('Text node needs an element.');
|
|
22
23
|
|
|
23
|
-
this.
|
|
24
|
+
this.rootEl = el;
|
|
24
25
|
if (shellFragment.nodeValue.length)
|
|
25
|
-
this.
|
|
26
|
+
this.rootEl.append(shellFragment);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
else {
|
|
29
30
|
if (el) {
|
|
30
|
-
this.
|
|
31
|
+
this.rootEl = el;
|
|
31
32
|
|
|
32
33
|
// Save slot
|
|
33
34
|
// 1. Globals.currentSlotChildren is set if this is called via PathToComponent.applyComponent() calls render()
|
|
@@ -39,13 +40,13 @@ export default class RootNodeGroup extends NodeGroup {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
// If el should replace the root node of the fragment.
|
|
42
|
-
if (isReplaceEl(shellFragment, this.
|
|
43
|
-
this.
|
|
43
|
+
if (isReplaceEl(shellFragment, this.rootEl.tagName)) {
|
|
44
|
+
this.rootEl.append(...shellFragment.children[0].childNodes);
|
|
44
45
|
|
|
45
46
|
// Copy attributes
|
|
46
47
|
for (let attrib of shellFragment.children[0].attributes)
|
|
47
|
-
if (!this.
|
|
48
|
-
this.
|
|
48
|
+
if (!this.rootEl.hasAttribute(attrib.name))
|
|
49
|
+
this.rootEl.setAttribute(attrib.name, attrib.value);
|
|
49
50
|
|
|
50
51
|
// Go one level deeper into all of shell's paths.
|
|
51
52
|
startingPathDepth = 1;
|
|
@@ -54,7 +55,7 @@ export default class RootNodeGroup extends NodeGroup {
|
|
|
54
55
|
else {
|
|
55
56
|
let isEmpty = shellFragment.childNodes.length === 1 && shellFragment.childNodes[0].nodeType === 3 && shellFragment.childNodes[0].textContent === '';
|
|
56
57
|
if (!isEmpty)
|
|
57
|
-
this.
|
|
58
|
+
this.rootEl.append(...shellFragment.childNodes);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
|
|
@@ -80,34 +81,26 @@ export default class RootNodeGroup extends NodeGroup {
|
|
|
80
81
|
|
|
81
82
|
// Instantiate as a standalone element.
|
|
82
83
|
else {
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
// Trimming the whitespace and comment nodes off both ends leaves a list of exactly
|
|
85
|
+
// one node only when the fragment has exactly one node worth keeping, which is the
|
|
86
|
+
// question being asked here.
|
|
87
|
+
let relevantNodes = Util.trimEmptyNodes(shellFragment.childNodes);
|
|
88
|
+
let onlyChild = relevantNodes.length === 1 ? relevantNodes[0] : null;
|
|
89
|
+
this.rootEl = onlyChild || shellFragment; // We return the whole fragment when calling h() with a collection of nodes.
|
|
85
90
|
if (onlyChild)
|
|
86
91
|
startingPathDepth = 1;
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
this.setPathsFromFragment(this.
|
|
90
|
-
this.activateEmbeds(this.
|
|
94
|
+
this.setPathsFromFragment(this.rootEl, shell, startingPathDepth);
|
|
95
|
+
this.activateEmbeds(this.rootEl, shell, startingPathDepth);
|
|
91
96
|
}
|
|
92
|
-
this.startNode = this.endNode = this.
|
|
97
|
+
this.startNode = this.endNode = this.rootEl;
|
|
93
98
|
|
|
94
|
-
Globals.rootNodeGroups.set(this.
|
|
99
|
+
Globals.rootNodeGroups.set(this.rootEl, this);
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
|
|
99
|
-
function getSingleEl(fragment) {
|
|
100
|
-
let nonempty = [];
|
|
101
|
-
for (let n of fragment.childNodes) {
|
|
102
|
-
if (n.nodeType === 1 || n.nodeType === 3 && n.textContent.trim().length) {
|
|
103
|
-
if (nonempty.length)
|
|
104
|
-
return null;
|
|
105
|
-
nonempty.push(n);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return nonempty[0];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
104
|
/**
|
|
112
105
|
* Does the fragment have one child that's an element matching the tagname of el?
|
|
113
106
|
* @param fragment {DocumentFragment}
|
package/src/Selector.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A key-scoped selection that updates only the rows it actually affects.
|
|
3
|
+
*
|
|
4
|
+
* Rendering a list normally means calling render() and letting the reconciler decide what
|
|
5
|
+
* changed. That is the right default, but it is a poor fit for a selection: moving a
|
|
6
|
+
* highlight from one row of a thousand to another changes two attributes, and asking the
|
|
7
|
+
* reconciler about it means walking the whole list to discover that fact.
|
|
8
|
+
*
|
|
9
|
+
* A Selector short-circuits that. when() hands each row one of exactly two objects — the
|
|
10
|
+
* selected one or the unselected one — and set() reaches the two rows that change through
|
|
11
|
+
* the list they were rendered into, writing their attributes directly with no render() call.
|
|
12
|
+
*
|
|
13
|
+
* This is the same primitive as Solid's createSelector, adapted to a library that has no
|
|
14
|
+
* signals: the list, not a subscription, is what carries the binding.
|
|
15
|
+
*
|
|
16
|
+
* Because set() locates a row by its key, **the rows must be keyed** — the row template needs
|
|
17
|
+
* a key=${...} attribute. set() throws on an unkeyed list rather than silently doing nothing.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The value an attribute is bound to. There are only ever **two** of these per Selector,
|
|
22
|
+
* both built in its constructor: one standing for "this row is the selected one" and one for
|
|
23
|
+
* "this row is not". when() returns whichever of the two the row's key calls for.
|
|
24
|
+
*
|
|
25
|
+
* Two singletons rather than one object per key is what makes a selector free to create. A
|
|
26
|
+
* row of a freshly-drawn list with nothing selected gets the unselected singleton, whose
|
|
27
|
+
* value is the off value, so there is no allocation, no map entry and no DOM call — only the
|
|
28
|
+
* two stores that record where the list lives. It also sharpens the re-render skip: a row's
|
|
29
|
+
* expression changes identity exactly when its selectedness changes, so
|
|
30
|
+
* NodeGroup.rewriteStamp() rewrites the rows that gained or lost the selection and no others.
|
|
31
|
+
*/
|
|
32
|
+
export class SelectorRef {
|
|
33
|
+
|
|
34
|
+
/** @type {Selector} */
|
|
35
|
+
selector;
|
|
36
|
+
|
|
37
|
+
/** @type {boolean} True on the singleton that stands for the selected row. */
|
|
38
|
+
selected;
|
|
39
|
+
|
|
40
|
+
constructor(selector, selected) {
|
|
41
|
+
this.selector = selector;
|
|
42
|
+
this.selected = selected;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @return {*} The value this ref currently stands for. */
|
|
46
|
+
value() {
|
|
47
|
+
let s = this.selector;
|
|
48
|
+
return this.selected ? s.onValue : s.offValue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Write this ref's value to an element's attribute, and tell the selector where the list
|
|
53
|
+
* is so that a later set() can find any row in it.
|
|
54
|
+
*
|
|
55
|
+
* Called by PathToAttribValue when the ref appears as an attribute expression. It runs
|
|
56
|
+
* once per row per render, so it is deliberately nothing but two stores and a write that
|
|
57
|
+
* the common case skips.
|
|
58
|
+
*
|
|
59
|
+
* @param node {Node} The element carrying the attribute.
|
|
60
|
+
* @param attribName {string}
|
|
61
|
+
* @param parentNg {NodeGroup} The row this attribute belongs to. */
|
|
62
|
+
bind(node, attribName, parentNg) {
|
|
63
|
+
// set() writes through the row's own root element, so an attribute anywhere deeper
|
|
64
|
+
// would be found at bind time and then written somewhere else at set() time. Catching
|
|
65
|
+
// it here turns a silently misplaced attribute into a clear message. It SHIPS: it is not
|
|
66
|
+
// in a debug-strip block, and it must not be, because the failure it catches is silent.
|
|
67
|
+
if (parentNg.startNode !== node)
|
|
68
|
+
throw new Error(`Solarite: a selector must be on the row's root element.`);
|
|
69
|
+
|
|
70
|
+
let s = this.selector;
|
|
71
|
+
s.attribName = attribName;
|
|
72
|
+
s.path = parentNg.parentPath;
|
|
73
|
+
|
|
74
|
+
let v = this.selected ? s.onValue : s.offValue;
|
|
75
|
+
|
|
76
|
+
// Matches PathToAttribValue.applySingle: an empty or falsy value leaves no attribute
|
|
77
|
+
// behind, so a selector never adds markup a hand-written implementation wouldn't have.
|
|
78
|
+
if (v === '' || v === false || v === null || v === undefined) {
|
|
79
|
+
// A just-cloned row provably carries no attribute of this name yet, so the
|
|
80
|
+
// removeAttribute — a DOM call for every row of the list — can be skipped.
|
|
81
|
+
if (parentNg.firstApply !== true)
|
|
82
|
+
node.removeAttribute(attribName);
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
node.setAttribute(attribName, v);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Created by h.selector(). Holds one selected key.
|
|
91
|
+
*
|
|
92
|
+
* Only attribute expressions can bind a selector; using one as element content throws,
|
|
93
|
+
* because writing text through this path would need bookkeeping the two-node fast case
|
|
94
|
+
* doesn't want.
|
|
95
|
+
*
|
|
96
|
+
* The selector keeps **no per-row state at all** — no map of keys, nothing to sweep, and
|
|
97
|
+
* nothing that could pin a removed row's element in memory. All it remembers is which
|
|
98
|
+
* attribute it drives and which list it was rendered into.
|
|
99
|
+
*/
|
|
100
|
+
export default class Selector {
|
|
101
|
+
|
|
102
|
+
/** @type {*} The selected key, or null. */
|
|
103
|
+
#key = null;
|
|
104
|
+
|
|
105
|
+
/** @type {SelectorRef} Returned by when() for the row whose key is selected. */
|
|
106
|
+
#on = new SelectorRef(this, true);
|
|
107
|
+
|
|
108
|
+
/** @type {SelectorRef} Returned by when() for every other row. */
|
|
109
|
+
#off = new SelectorRef(this, false);
|
|
110
|
+
|
|
111
|
+
/** @type {*} Value the bound attribute takes for the selected key. Held here rather than
|
|
112
|
+
* on each ref, so the two refs stay interchangeable between call sites. */
|
|
113
|
+
onValue;
|
|
114
|
+
|
|
115
|
+
/** @type {*} Value it takes for every other key. */
|
|
116
|
+
offValue = '';
|
|
117
|
+
|
|
118
|
+
/** @type {?string} The attribute this selector drives, learned when a row binds. */
|
|
119
|
+
attribName = null;
|
|
120
|
+
|
|
121
|
+
/** @type {?PathToNodes} The list this selector's rows were rendered into, learned when a
|
|
122
|
+
* row binds. set() asks it for the NodeGroup holding a given key. */
|
|
123
|
+
path = null;
|
|
124
|
+
|
|
125
|
+
/** @param key {*} The initially selected key. */
|
|
126
|
+
constructor(key = null) {
|
|
127
|
+
this.#key = key;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @return {*} The selected key. */
|
|
131
|
+
get key() {
|
|
132
|
+
return this.#key;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Bind an attribute to whether key is the selected one.
|
|
137
|
+
*
|
|
138
|
+
* h`<tr key=${row.id} class=${sel.when(row.id, 'danger')}>`
|
|
139
|
+
*
|
|
140
|
+
* @param key {*} This row's key.
|
|
141
|
+
* @param on {*} Value the attribute takes when key is selected.
|
|
142
|
+
* @param off {*} Value it takes otherwise. '' removes the attribute.
|
|
143
|
+
* @return {SelectorRef} */
|
|
144
|
+
when(key, on, off = '') {
|
|
145
|
+
this.onValue = on;
|
|
146
|
+
this.offValue = off;
|
|
147
|
+
return key === this.#key ? this.#on : this.#off;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Move the selection. Writes at most two attributes — the row losing the selection and
|
|
152
|
+
* the row gaining it — and touches nothing else. There is no render() call.
|
|
153
|
+
* @param key {*} The newly selected key, or null for none. */
|
|
154
|
+
set(key) {
|
|
155
|
+
let old = this.#key;
|
|
156
|
+
if (old === key)
|
|
157
|
+
return;
|
|
158
|
+
this.#key = key;
|
|
159
|
+
|
|
160
|
+
// Nothing has rendered a row yet, so there is no list to write into. The new key
|
|
161
|
+
// still takes effect: rows drawn later come up already carrying the attribute.
|
|
162
|
+
if (this.path === null)
|
|
163
|
+
return;
|
|
164
|
+
|
|
165
|
+
this.#write(old, this.offValue);
|
|
166
|
+
this.#write(key, this.onValue);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Find the row holding key and give its root element the value v.
|
|
171
|
+
* @param key {*}
|
|
172
|
+
* @param v {*} */
|
|
173
|
+
#write(key, v) {
|
|
174
|
+
if (key === null || key === undefined)
|
|
175
|
+
return;
|
|
176
|
+
|
|
177
|
+
let ngs = this.path.nodeGroups;
|
|
178
|
+
if (ngs === null || ngs.length === 0)
|
|
179
|
+
return;
|
|
180
|
+
|
|
181
|
+
if (ngs[0].key === undefined)
|
|
182
|
+
throw new Error('Solarite: a selector must be on a keyed list, as key=${...}.');
|
|
183
|
+
|
|
184
|
+
// A linear scan over the rows. The list is walked only when the selection actually
|
|
185
|
+
// moves — twice per user click, not once per row per render — so a thousand pointer
|
|
186
|
+
// comparisons here cost far less than the per-row index that would avoid them.
|
|
187
|
+
let ng = null;
|
|
188
|
+
for (let i = 0; i < ngs.length; i++)
|
|
189
|
+
if (ngs[i].key === key) {
|
|
190
|
+
ng = ngs[i];
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
if (ng === null)
|
|
194
|
+
return;
|
|
195
|
+
|
|
196
|
+
// The selector owns an attribute on the row's own root element, which for a
|
|
197
|
+
// single-root row template is exactly the NodeGroup's startNode.
|
|
198
|
+
let node = ng.startNode;
|
|
199
|
+
if (node === null || node.nodeType !== 1)
|
|
200
|
+
return;
|
|
201
|
+
|
|
202
|
+
if (v === '' || v === false || v === null || v === undefined)
|
|
203
|
+
node.removeAttribute(this.attribName);
|
|
204
|
+
else
|
|
205
|
+
node.setAttribute(this.attribName, v);
|
|
206
|
+
}
|
|
207
|
+
}
|