solarite 0.3.1 → 0.4.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/src/Shell.js CHANGED
@@ -53,7 +53,7 @@ export default class Shell {
53
53
  //#ENDIF
54
54
 
55
55
  if (html.length === 1 && !html[0].match(/[<&]/)) {
56
- this.fragment = document.createTextNode(html[0]);
56
+ this.fragment = Globals.doc.createTextNode(html[0]);
57
57
  return;
58
58
  }
59
59
 
@@ -61,18 +61,18 @@ export default class Shell {
61
61
  // 1. Add placeholders
62
62
  let joinedHtml = Shell.addPlaceholders(html);
63
63
 
64
- let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
64
+ let template = Globals.doc.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
65
65
  if (joinedHtml)
66
66
  template.innerHTML = joinedHtml;
67
67
  else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
68
- template.content.append(document.createTextNode(''))
68
+ template.content.append(Globals.doc.createTextNode(''))
69
69
  this.fragment = template.content;
70
70
 
71
71
  // 2. Find placeholders
72
72
  let node;
73
73
  let toRemove = [];
74
74
  let placeholdersUsed = 0;
75
- const walker = document.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
75
+ const walker = Globals.doc.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
76
76
  while (node = walker.nextNode()) {
77
77
 
78
78
  // Remove previous after each iteration, so paths will still be calculated correctly.
@@ -108,10 +108,13 @@ export default class Shell {
108
108
  // Replace comment placeholders
109
109
  else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
110
110
 
111
+ if (node?.parentNode?.closest && node?.parentNode?.closest('[contenteditable]'))
112
+ throw new Error(`Contenteditable can't have expressions inside them. Use <div contenteditable value="\${...}"> instead.`);
113
+
111
114
  // Get or create nodeBefore.
112
115
  let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
113
116
  if (!nodeBefore) {
114
- nodeBefore = document.createComment('ExprPath:'+this.paths.length);
117
+ nodeBefore = Globals.doc.createComment('ExprPath:'+this.paths.length);
115
118
  node.parentNode.insertBefore(nodeBefore, node)
116
119
  }
117
120
  /*#IFDEV*/assert(nodeBefore);/*#ENDIF*/
@@ -136,9 +139,9 @@ export default class Shell {
136
139
  placeholdersUsed ++;
137
140
  }
138
141
 
142
+ // Comments become text nodes when inside textareas.
139
143
  else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
140
144
  throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
141
-
142
145
 
143
146
 
144
147
  // Sometimes users will comment out a block of html code that has expressions.
@@ -162,7 +165,7 @@ export default class Shell {
162
165
 
163
166
  let placeholders = [];
164
167
  for (let i = 0; i<parts.length; i++) {
165
- let current = document.createTextNode(parts[i]);
168
+ let current = Globals.doc.createTextNode(parts[i]);
166
169
  node.parentNode.insertBefore(current, node);
167
170
  if (i > 0)
168
171
  placeholders.push(current)
@@ -201,9 +204,9 @@ export default class Shell {
201
204
  path.nodeMarkerPath = getNodePath(path.nodeMarker)
202
205
 
203
206
  // Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
204
- if (path.type === ExprPathType.AttribValue && path.nodeMarker.nodeType === 1 &&
207
+ if ((path.type === ExprPathType.AttribValue || path.type === ExprPathType.Event) && path.nodeMarker.nodeType === 1 &&
205
208
  (path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
206
- path.type = ExprPathType.ComponentAttribValue;
209
+ path.isComponent = true;
207
210
  }
208
211
  }
209
212
 
package/src/Solarite.d.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  /**
2
- * Solarite JavasCript UI library.
2
+ * Solarite JavaScript UI library.
3
3
  * MIT License
4
4
  * https://vorticode.github.io/solarite/
5
5
  */
6
6
 
7
- export default function h(htmlStrings?: HTMLElement | string | string[] | Function | {render: Function}, ...exprs: any[]): Node | HTMLElement | Template | Function;
8
- export { default as h, default as r } from './h.js';
7
+ export default function h(htmlStrings?: HTMLElement | string | string[] | TemplateStringsArray | Function | {render: Function}, ...exprs: any[]): Node | HTMLElement | Template | Function;
8
+ export function toEl(htmlOrTemplate: string|Template|{render:()=>void}) : Node|HTMLElement|DocumentFragment;
9
+
10
+ // Deprecated:
11
+ export function t(html: string): Template;
12
+
9
13
 
10
14
  export const ArgType: {
11
15
  Bool: string;
@@ -44,7 +48,7 @@ export function delve(obj: object, path: string[], createVal?: any): any;
44
48
 
45
49
  // Globals object
46
50
  export const Globals: {
47
- componentArgsHash: WeakMap<any, any>;
51
+ //componentArgsHash: WeakMap<any, any>;
48
52
  connected: WeakSet<HTMLElement>;
49
53
  currentExprPath: any;
50
54
  div: HTMLDivElement;
package/src/Solarite.js CHANGED
@@ -1,16 +1,17 @@
1
- /**
2
- * Solarite JavasCript UI library.
3
- * MIT License
4
- * https://vorticode.github.io/solarite/
5
- */
6
-
1
+ /*
2
+ ┏┓ ┓ •
3
+ ┗┓┏┓┃┏┓┏┓┓╋▗▖
4
+ ┗┛┗┛┗┗┻╹ ╹╹┗
5
+ JavasCript UI library
6
+ @license MIT
7
+ @copyright Vorticode LLC
8
+ https://vorticode.github.io/solarite/ */
7
9
  import h from './h.js';
8
10
  export default h;
9
11
  export {default as h, default as r} from './h.js'; //Named exports for h() are deprecated.
10
- export {default as delve} from './delve.js';
11
12
  export {getArg, ArgType} from './getArg.js';
12
13
  export {default as Template} from './Template.js';
13
-
14
+ export {default as toEl} from './toEl.js';
14
15
 
15
16
 
16
17
  // Experimental:
@@ -28,6 +29,7 @@ const Solarite = new Proxy(createSolarite(), {
28
29
  }
29
30
  });
30
31
 
32
+
31
33
  /** @type {HTMLElement|Class} */
32
34
  export {Solarite}
33
35
  export {default as Globals} from './Globals.js';
package/src/Template.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {assert} from "./assert.js";
2
2
  import {getObjectHash, getObjectId} from "./hash.js";
3
3
  import Globals from "./Globals.js";
4
- import {RootNodeGroup} from "./NodeGroup.js";
4
+ import RootNodeGroup from "./RootNodeGroup.js";
5
+ import Util from "./Util.js";
5
6
 
6
7
  /**
7
8
  * The html strings and evaluated expressions from an html tagged template.
@@ -18,8 +19,7 @@ export default class Template {
18
19
  /** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
19
20
  hashedFields;
20
21
 
21
- /** @type {NodeGroup} */
22
- nodeGroup;
22
+ isText;
23
23
 
24
24
  /**
25
25
  *
@@ -124,6 +124,138 @@ export default class Template {
124
124
 
125
125
  return this.closeKey;
126
126
  }
127
+
128
+ /**
129
+ * @param tag {string}
130
+ * @param props {?Record<string, any>}
131
+ * @param children
132
+ * @returns {Template} */
133
+ static fromJsx(tag, props, children) {
134
+
135
+ // HTML void elements that must not have closing tags
136
+ const isVoid = selfClosingTags.has(tag.toLowerCase());
137
+
138
+ // Build htmlStrings/exprs so Shell can place placeholders in attribute values and child content.
139
+ let htmlStrings = [];
140
+ let templateExprs = [];
141
+
142
+ // Opening tag
143
+ let open = `<${tag}`;
144
+
145
+ // Attributes
146
+ if (props && typeof props === 'object') {
147
+ for (let name in props) {
148
+ let value = props[name];
149
+
150
+ // id and data-id are static in templates — never expressions
151
+ if (name === 'id' || name === 'data-id') {
152
+ // Write directly into the opening string with quotes
153
+ open += ` ${name}="${value}"`;
154
+ continue;
155
+ }
156
+
157
+ // Dynamic attribute value: functions are unquoted (e.g., onclick=${fn}), others quoted
158
+ if (typeof value === 'function') {
159
+ open += ` ${name}=`;
160
+ htmlStrings.push(open);
161
+ templateExprs.push(value);
162
+ // reset so subsequent attributes start fresh (e.g., ' title=')
163
+ open = ``;
164
+ }
165
+ else {
166
+ open += ` ${name}=`;
167
+ htmlStrings.push(open);
168
+ templateExprs.push(value);
169
+ // reset so subsequent attributes start fresh (e.g., ' title=')
170
+ open = ``;
171
+ }
172
+ }
173
+ }
174
+
175
+ // Finalize opening tag precisely to match tagged template splitting
176
+ if (!isVoid) {
177
+ const pushedAny = htmlStrings.length > 0;
178
+ // If nothing pushed yet (no dynamic attrs), push the entire open + '>'
179
+ if (!pushedAny)
180
+ htmlStrings.push(open + '>');
181
+ else {
182
+ // If we were in a quoted attr (open === '"'), then the string after expr is '">' ;
183
+ // Otherwise (function-valued attr), the string after expr is just '>'
184
+ htmlStrings.push(open === '"' ? '">' : '>');
185
+ }
186
+
187
+ for (let child of children)
188
+ addChild(child, htmlStrings, templateExprs);
189
+ }
190
+
191
+ // Closing tag (not for void tags)
192
+ if (!isVoid) {
193
+ // If we never emitted the '>' for the open tag (no children were added),
194
+ // then it was appended above before children. Now just add the closing tag to the last html segment.
195
+ let lastIdx = htmlStrings.length - 1;
196
+ htmlStrings[lastIdx] += `</${tag}>`;
197
+ }
198
+ else {
199
+ // Void element: ensure we emitted a trailing '>' segment
200
+ const pushedAny = htmlStrings.length > 0;
201
+ if (!pushedAny)
202
+ htmlStrings.push(open + '>');
203
+ else
204
+ htmlStrings.push('>');
205
+ }
206
+
207
+ // Ensure invariant
208
+ //assert(htmlStrings.length === templateExprs.length + 1);
209
+ //console.log([htmlStrings, templateExprs])
210
+ return new Template(htmlStrings, templateExprs);
211
+ }
212
+ }
213
+
214
+
215
+ const selfClosingTags = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
216
+
217
+
218
+ /**
219
+ * Add child Templates that were already created via h() and Template.fromJsx()
220
+ * @param template {Template}
221
+ * @param html {string[]}
222
+ * @param exprs {any[]} */
223
+ const addChild = (template, html, exprs) => {
224
+
225
+ if (Array.isArray(template)) {
226
+ for (let c of template)
227
+ addChild(c, html, exprs);
228
+ }
229
+ else {
230
+ let flatten = false;
231
+ if (template instanceof Template) {
232
+ // Heuristic to match tagged-template splitting:
233
+ // - Flatten if the child has expressions (so JSX can inline attribute/value placeholders like tagged literals would).
234
+ // - Also flatten void elements (e.g., <img>) so they inline like literals.
235
+ // - Otherwise, keep as a dynamic child placeholder to match cases where the tagged template used an expression child.
236
+ const childHasExprs = template.exprs.length > 0;
237
+ if (childHasExprs)
238
+ flatten = true;
239
+ else {
240
+ const m = (template.html[0] || '').match(/^<([a-zA-Z][\w:-]*)/);
241
+ const childTag = m ? m[1].toLowerCase() : '';
242
+ flatten = selfClosingTags.has(childTag);
243
+ }
244
+ }
245
+
246
+ if (flatten) {
247
+ // Flatten/interleave into current segment to match tagged template splitting
248
+ html[html.length - 1] += template.html[0];
249
+ for (let i = 0; i < template.exprs.length; i++) {
250
+ exprs.push(template.exprs[i]);
251
+ html.push(template.html[i + 1] ?? '');
252
+ }
253
+ } else {
254
+ // Keep as dynamic child
255
+ exprs.push(template);
256
+ html.push('');
257
+ }
258
+ }
127
259
  }
128
260
 
129
261
 
package/src/Util.js CHANGED
@@ -18,12 +18,25 @@ let Util = {
18
18
  return true; // the same.
19
19
  },
20
20
 
21
+ /**
22
+ * Convert HTMLElement attributes to an object.
23
+ * @param el {HTMLElement}
24
+ * @param ignore {?string} Optionally ignore this attribute.
25
+ * @return {Object} */
26
+ attribsToObject(el, ignore=null) {
27
+ let result = {};
28
+ for (let attrib of el.attributes)
29
+ if (attrib.name !== ignore)
30
+ result[Util.dashesToCamel(attrib.name)] = attrib.value;
31
+ return result;
32
+ },
33
+
21
34
  bindId(root, el) {
22
35
  let id = el.getAttribute('data-id') || el.getAttribute('id');
23
36
  if (id) { // If something hasn't removed the id.
24
37
 
25
38
  // Don't allow overwriting existing class properties if they already have a non-Node value.
26
- if (root[id] && !(root[id] instanceof Node))
39
+ if (root[id] && !(root[id]?.nodeType))
27
40
  throw new Error(`${root.constructor.name}.${id} already has a value. ` +
28
41
  `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
29
42
 
@@ -32,26 +45,50 @@ let Util = {
32
45
  },
33
46
 
34
47
  /**
48
+ * If the style tab has a global attribute:
49
+ * 1. Put it in the document head as <style data-style="tag-name">...</style>
50
+ * 2. Replace the :host {...} CSS selector as tag-name {...}.
51
+ * Otherwise keep it where it is and:
52
+ * 1. Add data-style="1" attribute to the root element.
53
+ * 2. Replace the :host {...} selector in the style as tag-name[data-style='1'] {...}
35
54
  * @param style {HTMLStyleElement}
36
55
  * @param root {HTMLElement} */
37
56
  bindStyles(style, root) {
38
- let styleId = root.getAttribute('data-style');
39
- if (!styleId) {
40
- // Keep track of one style id for each class.
41
- // TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
42
- if (!root.constructor.styleId)
43
- root.constructor.styleId = 1;
44
- styleId = root.constructor.styleId++;
45
-
46
- root.setAttribute('data-style', styleId)
57
+
58
+ let tagName = root.tagName.toLowerCase();
59
+ let styleId, attribSelector;
60
+
61
+ if (style.hasAttribute('global') || style.hasAttribute('data-global')) {
62
+ styleId = tagName;
63
+ attribSelector = '';
64
+ let doc = Globals.doc || root.ownerDocument || document;
65
+ if (!doc.head.querySelector(`style[data-style="${styleId}"]`)) {
66
+ doc.head.append(style)
67
+ style.setAttribute('data-style', styleId);
68
+ }
69
+ else // TODO: Make sure the style has no expressions.
70
+ style.remove(); // already in the head.
71
+ }
72
+ else {
73
+ let styleId = root.getAttribute('data-style');
74
+ if (!styleId) {
75
+ // Keep track of one style id for each class.
76
+ // TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
77
+ if (!root.constructor.styleId)
78
+ root.constructor.styleId = 1;
79
+ styleId = root.constructor.styleId++;
80
+
81
+ root.setAttribute('data-style', styleId);
82
+ }
83
+
84
+ attribSelector = `[data-style="${styleId}"]`;
47
85
  }
48
86
 
49
87
  // Replace ":host" with "tagName[data-style=...]" in the css.
50
- let tagName = root.tagName.toLowerCase();
51
88
  for (let child of style.childNodes) {
52
89
  if (child.nodeType === 3) {
53
90
  let oldText = child.textContent;
54
- let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}[data-style="${styleId}"]`)
91
+ let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}${attribSelector}`)
55
92
  if (oldText !== newText)
56
93
  child.textContent = newText;
57
94
  }
@@ -98,7 +135,6 @@ let Util = {
98
135
  return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
99
136
  },
100
137
 
101
-
102
138
  /**
103
139
  * A generator function that recursively traverses and flattens a value.
104
140
  *
@@ -139,7 +175,7 @@ let Util = {
139
175
 
140
176
  /**
141
177
  * Get the value of an input as the most appropriate JavaScript type.
142
- * @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
178
+ * @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLElement}
143
179
  * @return {string|string[]|number|[]|File[]|Date|boolean} */
144
180
  getInputValue(node) {
145
181
  // .type is a built-in DOM property
@@ -153,6 +189,8 @@ let Util = {
153
189
  return node.valueAsDate; // Date Object
154
190
  if (node.type === 'select-multiple') // <select multiple>
155
191
  return [...node.selectedOptions].map(option => option.value); // Array of Strings
192
+ if (node.hasAttribute('contenteditable'))
193
+ return node.innerHTML;
156
194
 
157
195
  return node.value; // String
158
196
  },
@@ -42,7 +42,7 @@ export default function createSolarite(extendsTag=null) {
42
42
 
43
43
  BaseClass = Globals.elementClasses[extendsTag];
44
44
  if (!BaseClass) { // TODO: Use Cache
45
- BaseClass = document.createElement(extendsTag).constructor;
45
+ BaseClass = Globals.doc.createElement(extendsTag).constructor;
46
46
  Globals.elementClasses[extendsTag] = BaseClass
47
47
  }
48
48
  }
@@ -103,7 +103,7 @@ export default function createSolarite(extendsTag=null) {
103
103
  this.innerHTML = html;
104
104
  }
105
105
  else
106
- this.modifications = r(this, html, options);
106
+ this.modifications = h(this, html, options);
107
107
  }
108
108
  })*/
109
109