lupine.web 1.0.26 → 1.0.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lupine.web",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -1,5 +1,11 @@
1
1
  // import { mountSelfComponents, renderComponent } from "./mount-components";
2
2
 
3
+ import { VNode } from "../jsx";
4
+ import { bindAttributes } from "./bind-attributes";
5
+ import { bindLinks } from "./bind-links";
6
+ import { renderComponents } from "./render-components";
7
+ import { replaceInnerhtml } from "./replace-innerhtml";
8
+
3
9
  export const bindRef = (type: any, newProps: any, el: Element) => {
4
10
  // console.log('========', newProps, el);
5
11
  const id = newProps._id;
@@ -7,6 +13,7 @@ export const bindRef = (type: any, newProps: any, el: Element) => {
7
13
  newProps['ref'].current = el;
8
14
  if (newProps['ref'].onLoad) {
9
15
  // setTimeout(() => newProps['ref'].onLoad(el), 0);
16
+ // Promise.resolve().then(fn) will be called in the end of current event loop (quicker than setTimeout)
10
17
  const defer = Promise.prototype.then.bind(Promise.resolve());
11
18
  defer(() => newProps['ref'].onLoad(el));
12
19
  }
@@ -30,4 +37,16 @@ export const bindRef = (type: any, newProps: any, el: Element) => {
30
37
  newProps['ref'].$all = (selector: string) => {
31
38
  return el.querySelectorAll(`[${id}] ` + selector);
32
39
  };
40
+
41
+ newProps['ref'].loadContent = async (content: string | VNode<any>) => {
42
+ if (typeof content === 'object' && content.type && content.props) {
43
+ // await mountComponents(el, content);
44
+ renderComponents(content.type, content.props);
45
+ await replaceInnerhtml(el, content.props._html.join(''));
46
+ bindAttributes(el, content.type, content.props);
47
+ bindLinks(el);
48
+ } else {
49
+ await replaceInnerhtml(el, content as string);
50
+ }
51
+ };
33
52
  };
@@ -1,265 +1,42 @@
1
1
  import { bindAttributes } from './bind-attributes';
2
2
  import { bindLinks } from './bind-links';
3
3
  import { VNode } from '../jsx';
4
- import { Logger } from '../lib/logger';
5
- import { uniqueIdGenerator } from '../lib/unique-id';
6
- import { processStyle } from './bind-styles';
4
+ // import { Logger } from '../lib/logger';
7
5
  // import { bindPageResetEvent } from './page-reset-events';
8
6
  import { replaceInnerhtml } from './replace-innerhtml';
9
- import { camelToHyphens } from './camel-to-hyphens';
10
-
11
- const logger = new Logger('mount-components');
12
- export const domUniqueId = uniqueIdGenerator('l'); // l means label
13
- // bindPageResetEvent(() => {
14
- // // reset unique id
15
- // domUniqueId(true);
16
- // });
17
-
18
- function renderChildren(html: string[], children: any) {
19
- if (typeof children === 'string') {
20
- html.push(children);
21
- } else if (children === false || children === null || typeof children === 'undefined') {
22
- // add nothing
23
- } else if (typeof children === 'number' || typeof children === 'boolean') {
24
- // true will be added
25
- html.push(children.toString());
26
- } else if (Array.isArray(children)) {
27
- for (let i = 0; i < children.length; i++) {
28
- const item = children[i];
29
- renderChildren(html, item);
30
- }
31
- } else if (children.type && children.props) {
32
- renderComponent(children.type, children.props);
33
- html.push(...children.props._html);
34
- children.props._html.length = 0;
35
- } else {
36
- logger.warn('Unexpected', children);
37
- }
38
- }
39
-
40
- const selfClosingTags = [
41
- 'area',
42
- 'base',
43
- 'br',
44
- 'col',
45
- 'embed',
46
- 'hr',
47
- 'img',
48
- 'input',
49
- 'link',
50
- 'meta',
51
- 'param',
52
- 'source',
53
- 'track',
54
- 'wbr',
55
- ];
56
-
57
- const genUniqueId = (props: any) => {
58
- if (!props._id) {
59
- props._id = domUniqueId();
60
- }
61
- return props._id;
62
- };
63
- // data-refid will be assigned with a ref.id
64
- function renderAttribute(type: any, props: any, jsxNodes: any) {
65
- const html = [];
66
- // data-refid is used for nested components like this:
67
- // <div class='class-name' ref={ref} ...>...
68
- // <div data-refid={ref}>
69
- // then data-refid can be located:
70
- // ref.$(`.class-name[data-refid=${ref.id}]`)
71
- if (props['data-refid'] && props['data-refid'].id) {
72
- props['data-refid'] = props['data-refid'].id;
73
- }
74
- for (let i in props) {
75
- if (i === 'ref') {
76
- if (props[i]) {
77
- props[i].id = genUniqueId(props);
78
- html.push('data-ref');
79
- }
80
- } else if (!['children', 'key', '_result', '_html', '_id'].includes(i)) {
81
- //, "_lb"
82
- // , "value", "checked"
83
- // style is a string, in-line style
84
- if (i === 'style') {
85
- if (typeof props[i] === 'object') {
86
- let attrs = `${i}="`;
87
- for (let j in props[i]) {
88
- attrs += `${camelToHyphens(j)}:${props[i][j]};`;
89
- }
90
- attrs += `"`;
91
- html.push(attrs);
92
- } else {
93
- html.push(`${i}="${props[i]}"`);
94
- }
95
- } else if (i === 'css') {
96
- // css is a <style> tag, and is the first element in html
97
- genUniqueId(props);
98
- // props._lb = props._id;
99
- } else if (i[0] === 'o' && i[1] === 'n') {
100
- genUniqueId(props);
101
- } else if (i === 'defaultChecked') {
102
- if (props[i] === true || props[i] === 'checked') {
103
- html.push(`checked="true"`);
104
- }
105
- } else if (i === 'readonly' || i === 'disabled' || i === 'selected' || i === 'checked') {
106
- if (props[i] !== undefined && props[i] !== false && props[i] !== 'false') {
107
- html.push(`${i}="${props[i]}"`);
108
- }
109
- } else if (i === 'class' || i === 'className') {
110
- const className = props[i]
111
- .split(' ')
112
- .filter((item: string) => item && item !== '')
113
- .join(' ');
114
- html.push(`class="${className}"`);
115
- } else if (i !== 'dangerouslySetInnerHTML') {
116
- html.push(`${i}="${props[i]}"`);
117
- }
118
- }
119
- }
120
- if (props._id) {
121
- // tag id will be after all attributes
122
- html.push(props._id);
123
- }
124
- return html.join(' ');
125
- }
126
-
127
- // assign the same label to all children
128
- // function assignLabels(label: string, children: any) {
129
- // if (Array.isArray(children)) {
130
- // for (let i = 0; i < children.length; i++) {
131
- // const item = children[i];
132
- // if (Array.isArray(item) || (item && item.type && item.props)) {
133
- // assignLabels(label, item);
134
- // }
135
- // }
136
- // } else if (children.type && children.props) {
137
- // if (typeof children.type === 'string') {
138
- // children.props._lb = label;
139
- // }
140
- // }
141
- // }
142
-
143
- // The result has only one element
144
- const renderComponent = (type: any, props: any) => {
145
- // logger.log("==================renderComponent", type);
146
- if (Array.isArray(props)) {
147
- const jsxNodes = { type: 'Fragment', props: { children: props } } as any;
148
- return renderComponent(jsxNodes.type, jsxNodes.props);
149
- }
150
-
151
- props._html = [];
152
- if (typeof type === 'function') {
153
- props._result = type.call(null, props);
154
- if (props._result === null || props._result === undefined || props._result === false) {
155
- // placeholder for sub components
156
- props._result = { type: 'Fragment', props };
157
- }
158
- if (props._fragment_ref && props._result && props._result.props) {
159
- // pass the ref to the sub Fragment tag
160
- props._result.props.ref = props._fragment_ref;
161
- props._result.props._id = genUniqueId(props._result.props);
162
- }
163
- // logger.log('==========props._result', props._result);
164
- if (typeof props._result.type === 'function') {
165
- renderComponent(props._result.type, props._result.props);
166
- if (props._result.props._html) {
167
- props._html.push(...props._result.props._html);
168
- props._result.props._html.length = 0;
169
- }
170
- // function component doesn't have any attributes
171
- return;
172
- }
173
- }
174
- const newType = (props._result && props._result.type) || type;
175
- const newProps = (props._result && props._result.props) || props;
176
- if (newType === 'div' && newProps.class === 'answer-box') {
177
- console.log('renderComponent', newType, newProps);
178
- }
179
- if (typeof newType === 'string') {
180
- const attrs = renderAttribute(newType, newProps, { type, props });
181
- if (selfClosingTags.includes(newType.toLowerCase())) {
182
- props._html.push(`<${newType}${attrs ? ' ' : ''}${attrs} />`);
183
- if (newProps['css']) {
184
- console.warn(`ClosingTag [${newType}] doesn't support 'css', please use 'style' instead.`);
185
- }
186
- } else {
187
- props._html.push(`<${newType}${attrs ? ' ' : ''}${attrs}>`);
188
-
189
- if (newProps['css']) {
190
- const cssText = processStyle(`[${newProps._id}]`, newProps['css']).join('');
191
- props._html.push(`<style id="sty-${newProps._id}">${cssText}</style>`); // sty means style, and updateStyles has the same name
192
- }
193
-
194
- if (newProps.children) {
195
- // if (newProps._lb) {
196
- // assignLabels(newProps._lb, newProps.children);
197
- // }
198
-
199
- renderChildren(props._html, newProps.children);
200
- } else if (newProps['dangerouslySetInnerHTML']) {
201
- props._html.push(newProps['dangerouslySetInnerHTML']);
202
- } else {
203
- // single element
204
- }
205
-
206
- props._html.push(`</${newType}>`);
207
- }
208
- } else if (newType.name === 'Fragment') {
209
- renderChildren(props._html, newProps.children);
210
- } else {
211
- logger.warn('Unknown type: ', type, props, newType, newProps);
212
- }
213
- };
7
+ import { renderComponents } from './render-components';
214
8
 
9
+ // const logger = new Logger('mount-components');
215
10
  export const mountComponents = async (selector: string | null | Element, jsxNodes: VNode<any>) => {
216
- renderComponent(jsxNodes.type, jsxNodes.props);
11
+ renderComponents(jsxNodes.type, jsxNodes.props);
217
12
  const el = selector && (typeof selector === 'string' ? document.querySelector(selector) : selector);
218
13
  if (el) {
219
- // the parent node shouldn't have any styles
220
- // el.replaceChildren(...);
221
- // // keep <style id="sty-${newProps._id}">...</style>
222
- // const firstDom = el.firstChild as Element;
223
- // if (firstDom && firstDom.tagName === 'STYLE') {
224
- // firstDom.parentNode?.removeChild(firstDom);
225
- // }
226
-
227
14
  // call unload before releace innerHTML
228
- // el.innerHTML = jsxNodes.props._html.join('');
229
- // remove <Fragment> and </Fragment>
230
- const _html = jsxNodes.props._html;
231
- if (_html[0] === '<Fragment>' && _html[_html.length - 1] === '</Fragment>') {
232
- _html.shift();
233
- _html.pop();
234
- }
235
15
  await replaceInnerhtml(el, jsxNodes.props._html.join(''));
236
16
 
237
- // if (firstDom && firstDom.tagName === 'STYLE') {
238
- // el.insertBefore(firstDom, el.firstChild);
239
- // }
240
17
  bindAttributes(el, jsxNodes.type, jsxNodes.props);
241
18
  bindLinks(el);
242
19
  }
243
20
  };
244
21
 
245
- // suggest to use HtmlVar.
246
- export const mountSelfComponents = async (selector: string | null | Element, jsxNodes: VNode<any>) => {
247
- renderComponent(jsxNodes.type, jsxNodes.props);
248
- let el = selector && (typeof selector === 'string' ? document.querySelector(selector) : selector);
249
- if (el) {
250
- const parentNode = el.parentElement;
251
- // Can't do outerHTML directly because it will lose attributes
252
- const template = document.createElement('template');
253
- // template.innerHTML = jsxNodes.props._html.join("");
254
- // call unload before releace innerHTML
255
- await replaceInnerhtml(template, jsxNodes.props._html.join(''));
256
-
257
- // renderComponent should only have one element
258
- template.content.children.length > 1 &&
259
- console.error('renderComponent should only have one element: ', template.content.children.length);
260
- el.replaceWith(template.content.firstChild as Element);
261
- el = parentNode as Element;
262
- bindAttributes(el, jsxNodes.type, jsxNodes.props);
263
- bindLinks(el);
264
- }
265
- };
22
+ // // suggest to use HtmlVar.
23
+ // export const mountSelfComponents = async (selector: string | null | Element, jsxNodes: VNode<any>) => {
24
+ // renderComponents(jsxNodes.type, jsxNodes.props);
25
+ // let el = selector && (typeof selector === 'string' ? document.querySelector(selector) : selector);
26
+ // if (el) {
27
+ // const parentNode = el.parentElement;
28
+ // // Can't do outerHTML directly because it will lose attributes
29
+ // const template = document.createElement('template');
30
+ // // template.innerHTML = jsxNodes.props._html.join("");
31
+ // // call unload before releace innerHTML
32
+ // await replaceInnerhtml(template, jsxNodes.props._html.join(''));
33
+
34
+ // // renderComponent should only have one element
35
+ // template.content.children.length > 1 &&
36
+ // console.error('renderComponent should only have one element: ', template.content.children.length);
37
+ // el.replaceWith(template.content.firstChild as Element);
38
+ // el = parentNode as Element;
39
+ // bindAttributes(el, jsxNodes.type, jsxNodes.props);
40
+ // bindLinks(el);
41
+ // }
42
+ // };
@@ -0,0 +1,217 @@
1
+ import { Logger } from '../lib/logger';
2
+ import { uniqueIdGenerator } from '../lib/unique-id';
3
+ import { processStyle } from './bind-styles';
4
+ // import { bindPageResetEvent } from './page-reset-events';
5
+ import { camelToHyphens } from './camel-to-hyphens';
6
+
7
+ const logger = new Logger('render-components');
8
+ export const domUniqueId = uniqueIdGenerator('l'); // l means label
9
+ // bindPageResetEvent(() => {
10
+ // // reset unique id
11
+ // domUniqueId(true);
12
+ // });
13
+
14
+ function renderChildren(html: string[], children: any) {
15
+ if (typeof children === 'string') {
16
+ html.push(children);
17
+ } else if (children === false || children === null || typeof children === 'undefined') {
18
+ // add nothing
19
+ } else if (typeof children === 'number' || typeof children === 'boolean') {
20
+ // true will be added
21
+ html.push(children.toString());
22
+ } else if (Array.isArray(children)) {
23
+ for (let i = 0; i < children.length; i++) {
24
+ const item = children[i];
25
+ renderChildren(html, item);
26
+ }
27
+ } else if (children.type && children.props) {
28
+ renderComponents(children.type, children.props);
29
+ html.push(...children.props._html);
30
+ children.props._html.length = 0;
31
+ } else {
32
+ logger.warn('Unexpected', children);
33
+ }
34
+ }
35
+
36
+ const selfClosingTags = [
37
+ 'area',
38
+ 'base',
39
+ 'br',
40
+ 'col',
41
+ 'embed',
42
+ 'hr',
43
+ 'img',
44
+ 'input',
45
+ 'link',
46
+ 'meta',
47
+ 'param',
48
+ 'source',
49
+ 'track',
50
+ 'wbr',
51
+ ];
52
+
53
+ const genUniqueId = (props: any) => {
54
+ if (!props._id) {
55
+ props._id = domUniqueId();
56
+ }
57
+ return props._id;
58
+ };
59
+ // data-refid will be assigned with a ref.id
60
+ function renderAttribute(type: any, props: any, jsxNodes: any) {
61
+ const html = [];
62
+ // data-refid is used for nested components like this:
63
+ // <div class='class-name' ref={ref} ...>...
64
+ // <div data-refid={ref}>
65
+ // then data-refid can be located:
66
+ // ref.$(`.class-name[data-refid=${ref.id}]`)
67
+ if (props['data-refid'] && props['data-refid'].id) {
68
+ props['data-refid'] = props['data-refid'].id;
69
+ }
70
+ for (let i in props) {
71
+ if (i === 'ref') {
72
+ if (props[i]) {
73
+ props[i].id = genUniqueId(props);
74
+ html.push('data-ref');
75
+ }
76
+ } else if (!['children', 'key', '_result', '_html', '_id'].includes(i)) {
77
+ //, "_lb"
78
+ // , "value", "checked"
79
+ // style is a string, in-line style
80
+ if (i === 'style') {
81
+ if (typeof props[i] === 'object') {
82
+ let attrs = `${i}="`;
83
+ for (let j in props[i]) {
84
+ attrs += `${camelToHyphens(j)}:${props[i][j]};`;
85
+ }
86
+ attrs += `"`;
87
+ html.push(attrs);
88
+ } else {
89
+ html.push(`${i}="${props[i]}"`);
90
+ }
91
+ } else if (i === 'css') {
92
+ // css is a <style> tag, and is the first element in html
93
+ genUniqueId(props);
94
+ // props._lb = props._id;
95
+ } else if (i[0] === 'o' && i[1] === 'n') {
96
+ genUniqueId(props);
97
+ } else if (i === 'defaultChecked') {
98
+ if (props[i] === true || props[i] === 'checked') {
99
+ html.push(`checked="true"`);
100
+ }
101
+ } else if (i === 'readonly' || i === 'disabled' || i === 'selected' || i === 'checked') {
102
+ if (props[i] !== undefined && props[i] !== false && props[i] !== 'false') {
103
+ html.push(`${i}="${props[i]}"`);
104
+ }
105
+ } else if (i === 'class' || i === 'className') {
106
+ const className = props[i]
107
+ .split(' ')
108
+ .filter((item: string) => item && item !== '')
109
+ .join(' ');
110
+ html.push(`class="${className}"`);
111
+ } else if (i !== 'dangerouslySetInnerHTML') {
112
+ html.push(`${i}="${props[i]}"`);
113
+ }
114
+ }
115
+ }
116
+ if (props._id) {
117
+ // tag id will be after all attributes
118
+ html.push(props._id);
119
+ }
120
+ return html.join(' ');
121
+ }
122
+
123
+ // assign the same label to all children
124
+ // function assignLabels(label: string, children: any) {
125
+ // if (Array.isArray(children)) {
126
+ // for (let i = 0; i < children.length; i++) {
127
+ // const item = children[i];
128
+ // if (Array.isArray(item) || (item && item.type && item.props)) {
129
+ // assignLabels(label, item);
130
+ // }
131
+ // }
132
+ // } else if (children.type && children.props) {
133
+ // if (typeof children.type === 'string') {
134
+ // children.props._lb = label;
135
+ // }
136
+ // }
137
+ // }
138
+
139
+ // The result has only one element
140
+ export const renderComponents = (type: any, props: any) => {
141
+ // logger.log("==================renderComponent", type);
142
+ if (Array.isArray(props)) {
143
+ const jsxNodes = { type: 'Fragment', props: { children: props } } as any;
144
+ renderComponents(jsxNodes.type, jsxNodes.props);
145
+ return;
146
+ }
147
+
148
+ props._html = [];
149
+ if (typeof type === 'function') {
150
+ props._result = type.call(null, props);
151
+ if (props._result === null || props._result === undefined || props._result === false) {
152
+ // placeholder for sub components
153
+ props._result = { type: 'Fragment', props };
154
+ }
155
+ // if (props._fragment_ref && props._result && props._result.props) {
156
+ // // pass the ref to the sub Fragment tag
157
+ // props._result.props.ref = props._fragment_ref;
158
+ // props._result.props._id = genUniqueId(props._result.props);
159
+ // }
160
+ // logger.log('==========props._result', props._result);
161
+ if (typeof props._result.type === 'function') {
162
+ renderComponents(props._result.type, props._result.props);
163
+ if (props._result.props._html) {
164
+ props._html.push(...props._result.props._html);
165
+ props._result.props._html.length = 0;
166
+ }
167
+ // function component doesn't have any attributes
168
+ return;
169
+ }
170
+ }
171
+ const newType = (props._result && props._result.type) || type;
172
+ const newProps = (props._result && props._result.props) || props;
173
+ if (newType === 'div' && newProps.class === 'answer-box') {
174
+ console.log('renderComponent', newType, newProps);
175
+ }
176
+ if (typeof newType === 'string') {
177
+ const attrs = renderAttribute(newType, newProps, { type, props });
178
+ if (selfClosingTags.includes(newType.toLowerCase())) {
179
+ // for Fragment, only needs this tag when Ref is assigned
180
+ if (newType !== 'Fragment' || newProps.ref) {
181
+ props._html.push(`<${newType}${attrs ? ' ' : ''}${attrs} />`);
182
+ }
183
+ if (newProps['css']) {
184
+ console.warn(`ClosingTag [${newType}] doesn't support 'css', please use 'style' instead.`);
185
+ }
186
+ } else {
187
+ if (newType !== 'Fragment' || newProps.ref) {
188
+ props._html.push(`<${newType}${attrs ? ' ' : ''}${attrs}>`);
189
+ }
190
+
191
+ if (newProps['css']) {
192
+ const cssText = processStyle(`[${newProps._id}]`, newProps['css']).join('');
193
+ props._html.push(`<style id="sty-${newProps._id}">${cssText}</style>`); // sty means style, and updateStyles has the same name
194
+ }
195
+
196
+ if (newProps.children) {
197
+ // if (newProps._lb) {
198
+ // assignLabels(newProps._lb, newProps.children);
199
+ // }
200
+
201
+ renderChildren(props._html, newProps.children);
202
+ } else if (newProps['dangerouslySetInnerHTML']) {
203
+ props._html.push(newProps['dangerouslySetInnerHTML']);
204
+ } else {
205
+ // single element
206
+ }
207
+
208
+ if (newType !== 'Fragment' || newProps.ref) {
209
+ props._html.push(`</${newType}>`);
210
+ }
211
+ }
212
+ } else if (newType.name === 'Fragment') {
213
+ renderChildren(props._html, newProps.children);
214
+ } else {
215
+ logger.warn('Unknown type: ', type, props, newType, newProps);
216
+ }
217
+ };
@@ -1,4 +1,10 @@
1
1
  export const replaceInnerhtml = async (el: Element, newHtml: string) => {
2
+ // keep <style id="sty-${newProps._id}">...</style>
3
+ const firstDom = el.firstChild as Element;
4
+ if (firstDom && firstDom.tagName === 'STYLE') {
5
+ firstDom.parentNode?.removeChild(firstDom);
6
+ }
7
+
2
8
  const promises: Promise<void>[] = [];
3
9
  el.querySelectorAll('[data-ref]').forEach((child: any) => {
4
10
  if (child._lj && child._lj.onUnload) {
@@ -7,4 +13,8 @@ export const replaceInnerhtml = async (el: Element, newHtml: string) => {
7
13
  });
8
14
  await Promise.all(promises);
9
15
  el.innerHTML = newHtml;
16
+
17
+ if (firstDom && firstDom.tagName === 'STYLE') {
18
+ el.insertBefore(firstDom, el.firstChild);
19
+ }
10
20
  };
package/src/jsx.ts CHANGED
@@ -25,6 +25,7 @@ export type RefProps = {
25
25
  onUnload?: (el: Element) => Promise<void>;
26
26
  $?: any; // (selector: string) => undefined | Element,
27
27
  $all?: any; // (selector: string) => undefined | Element,
28
+ loadContent?: (content: string | VNode<any>) => Promise<void>;
28
29
  };
29
30
 
30
31
  export interface ClassAttributes<T> extends Attributes {
package/src/lib/cookie.ts CHANGED
@@ -39,3 +39,6 @@ export const clearCookie = (name: string, path?: string, domain?: string, secure
39
39
  (domain ? ';domain=' + domain : '') +
40
40
  (secure ? ';secure' : '');
41
41
  };
42
+
43
+ // convinent but not good for tree-shaking
44
+ export const cookie = { set: setCookie, get: getCookie, clear: clearCookie };