lupine.web 1.0.31 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lupine.web",
3
- "version": "1.0.31",
3
+ "version": "1.1.0",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -1,8 +1,5 @@
1
1
  import { VNode } from '../jsx';
2
- import { bindAttributes } from './bind-attributes';
3
- import { bindLinks } from './bind-links';
4
2
  import { mountInnerComponent, mountOuterComponent } from './mount-component';
5
- import { renderComponent } from './render-component';
6
3
  import { replaceInnerhtml } from './replace-innerhtml';
7
4
 
8
5
  export const bindRef = (type: any, newProps: any, el: Element) => {
@@ -29,10 +26,16 @@ export const bindRef = (type: any, newProps: any, el: Element) => {
29
26
  * @returns Element
30
27
  */
31
28
  newProps['ref'].$ = (selector: string) => {
32
- return el.querySelector(`[${id}] ` + selector);
29
+ if (selector.startsWith('&')) {
30
+ return el.querySelector(`.${id}${selector.substring(1).replace(/&/g, id)}`);
31
+ }
32
+ return el.querySelector(`.${id} ${selector.replace(/&/g, id)}`);
33
33
  };
34
34
  newProps['ref'].$all = (selector: string) => {
35
- return el.querySelectorAll(`[${id}] ` + selector);
35
+ if (selector.startsWith('&')) {
36
+ return el.querySelectorAll(`.${id}${selector.substring(1).replace(/&/g, id)}`);
37
+ }
38
+ return el.querySelectorAll(`.${id} ${selector.replace(/&/g, id)}`);
36
39
  };
37
40
 
38
41
  newProps['ref'].mountInnerComponent = async (content: string | VNode<any>) => {
@@ -36,20 +36,32 @@ const updateOneBlock = (css: string[], cssTemp: string[], className: string, med
36
36
  }
37
37
  };
38
38
 
39
- export const processStyle = (className: string, style: CssProps, mediaQuery?: string): string[] => {
39
+ const processStyleSub = (
40
+ topUniqueClassName: string,
41
+ classSelector: string,
42
+ style: CssProps,
43
+ mediaQuery?: string
44
+ ): string[] => {
45
+ const outClassName = classSelector
46
+ .split(',')
47
+ .map((key0) => key0.trim())
48
+ .map((key0) => {
49
+ return (key0.startsWith('&') ? `${classSelector}${key0.substring(1)}` : key0).replace(/&/g, topUniqueClassName);
50
+ })
51
+ .join(',');
40
52
  const css: string[] = [];
41
53
  const cssTemp: string[] = [];
42
54
  for (let i in style) {
43
55
  const value = style[i];
44
56
  if (value === null || typeof value !== 'object') {
45
57
  if (value !== '' && typeof value !== 'undefined') {
46
- if (!className) {
58
+ if (!classSelector) {
47
59
  console.warn(`No className is defined for: ${camelToHyphens(i)}:${value};`);
48
60
  }
49
61
  cssTemp.push(`${camelToHyphens(i)}:${value};`);
50
62
  }
51
63
  } else {
52
- updateOneBlock(css, cssTemp, className, mediaQuery);
64
+ updateOneBlock(css, cssTemp, outClassName, mediaQuery);
53
65
 
54
66
  if (i.startsWith('@keyframes')) {
55
67
  const cssText = Object.keys(value)
@@ -57,17 +69,22 @@ export const processStyle = (className: string, style: CssProps, mediaQuery?: st
57
69
  .join('');
58
70
  css.push(`${i}{${cssText}}`);
59
71
  } else if (i.startsWith('@media')) {
60
- const ret = processStyle(className, value, i);
72
+ const ret = processStyleSub(topUniqueClassName, classSelector, value, i);
61
73
  css.push(...ret);
62
74
  } else {
63
75
  // '&:hover, &.open': {
64
- // '>.d1, .d2': {
65
- // },
76
+ // '>.d1, .d2': {...},
66
77
  // }, ==>
67
78
  // &:hover >.d1, &:hover >.d2, &.open >.d1, &.open .d2
68
- const newClassName = !className
79
+
80
+ // '.aa': {
81
+ // '&:hover': {...},
82
+ // '.bb': {...},
83
+ // }, ==>
84
+ // .aa:hover, .aa .bb
85
+ const newClassSelector = !classSelector
69
86
  ? i
70
- : className
87
+ : classSelector
71
88
  .split(',')
72
89
  .map((key0) => key0.trim())
73
90
  .map((key0) => {
@@ -77,25 +94,31 @@ export const processStyle = (className: string, style: CssProps, mediaQuery?: st
77
94
  .map((key) => {
78
95
  // not needed to "+" as them share same parents?
79
96
  // return key.split('+').map(key2 => key2.startsWith('&') ? key0 + key2.substring(1) : key0 + ' ' + key2).join('+');
80
- return key.startsWith('&') ? key0 + key.substring(1) : key0 + ' ' + key;
97
+ // return key.startsWith('&') ? key0 + key.substring(1) : key0 + ' ' + key;
98
+ const newKey = key.startsWith('&') ? key0 + key.substring(1) : key0 + ' ' + key;
99
+ return newKey.replace(/&/g, topUniqueClassName);
81
100
  })
82
101
  .join(',');
83
102
  })
84
103
  .join(',');
85
- const ret = processStyle(newClassName, value, mediaQuery);
104
+ const ret = processStyleSub(topUniqueClassName, newClassSelector, value, mediaQuery);
86
105
  css.push(...ret);
87
106
  }
88
107
  }
89
108
  }
90
- updateOneBlock(css, cssTemp, className, mediaQuery);
109
+ updateOneBlock(css, cssTemp, outClassName, mediaQuery);
91
110
  return css;
92
111
  };
112
+ // topUniqueClassName is used to replace '&' in className, and '.' + topUniqueClassName is used as selector in styles ".xxx {}"
113
+ export const processStyle = (topUniqueClassName: string, style: CssProps): string[] => {
114
+ return processStyleSub(topUniqueClassName, topUniqueClassName ? `.${topUniqueClassName}` : '', style);
115
+ };
93
116
 
94
117
  // mount-components has the same name `sty-`
95
- export const updateStyles = (selector: string, style: CssProps) => {
96
- const el = selector && document.querySelector(selector);
118
+ export const updateStyles = (topUniqueClassName: string, style: CssProps) => {
119
+ const el = topUniqueClassName && document.querySelector(`.${topUniqueClassName}`);
97
120
  if (el) {
98
- const cssText = processStyle(selector, style).join('');
121
+ const cssText = processStyle(topUniqueClassName, style).join('');
99
122
  // if the first child is style, then update it
100
123
  if (el.firstChild && el.firstChild.nodeName === 'STYLE') {
101
124
  (el.firstChild as any).innerHTML = cssText;
@@ -106,7 +129,7 @@ export const updateStyles = (selector: string, style: CssProps) => {
106
129
  el.prepend(style);
107
130
  }
108
131
  } else {
109
- console.warn(`Can't find "${selector}" to update styles.`);
132
+ console.warn(`Can't find "${topUniqueClassName}" to update styles.`);
110
133
  }
111
134
  };
112
135
 
@@ -120,21 +143,36 @@ const updateCssDom = (uniqueStyleId: string, cssText: string, cssDom: HTMLElemen
120
143
  };
121
144
 
122
145
  /*
146
+ If selectors in GlobalStyles have '&' at top level, then classSelector is needed, otherwise classSelector can be ''
147
+ This is ok:
148
+ {
149
+ '.aa':{
150
+ '&:hover': {...}
151
+ }
152
+ }
153
+ This needs classSelector:
154
+ {
155
+ 'color': 'red', // will need and be put under .topUniqueClassName
156
+ '&:hover': {...} // & will be replaced by .topUniqueClassName
157
+ }
123
158
  Global styles including theme will not be updated once it's created.
124
- topClassName is a className or a tag name.
159
+ topUniqueClassName is a className or a tag name.
160
+ classSelector is a selector used in styles ".xxx {}"
125
161
  For example, it can be like this for all elements:
126
162
  html { ... } or :root { ... }
163
+
164
+ For themes like [data-theme="dark" i], the topUniqueClassName should be empty
127
165
  */
128
166
  const _globalStyle = new Map();
129
- export const bindGlobalStyles = (uniqueStyleId: string, topClassName: string, style: CssProps, forceUpdate = false) => {
167
+ export const bindGlobalStyles = (topUniqueClassName: string, style: CssProps, forceUpdate = false, isTheme = false) => {
130
168
  if (typeof document !== 'undefined') {
131
- let cssDom = document.getElementById(`sty-${uniqueStyleId}`);
169
+ let cssDom = document.getElementById(`sty-${topUniqueClassName}`);
132
170
  if (forceUpdate || !cssDom) {
133
- updateCssDom(uniqueStyleId, processStyle(topClassName, style).join(''), cssDom);
171
+ updateCssDom(topUniqueClassName, processStyle(isTheme ? '' : topUniqueClassName, style).join(''), cssDom);
134
172
  }
135
- } else if (!_globalStyle.has(uniqueStyleId) || forceUpdate) {
173
+ } else if (!_globalStyle.has(topUniqueClassName) || forceUpdate) {
136
174
  // don't overwrite it to have the same behavior as in the Browser
137
- _globalStyle.set(uniqueStyleId, { topClassName, style });
175
+ _globalStyle.set(topUniqueClassName, { topUniqueClassName, style });
138
176
  }
139
177
  };
140
178
 
@@ -143,7 +181,11 @@ const generateThemeStyles = () => {
143
181
  const themeCss = [];
144
182
  for (let themeName in currentTheme.themes) {
145
183
  // i is for case-insensitive
146
- themeCss.push(...processStyle(`[data-theme="${themeName}" i]`, currentTheme.themes[themeName]));
184
+ themeCss.push(
185
+ ...processStyle('', {
186
+ [`[data-theme="${themeName}" i]`]: currentTheme.themes[themeName],
187
+ })
188
+ );
147
189
  }
148
190
  return themeCss.join('\n');
149
191
  };
@@ -159,7 +201,7 @@ if (typeof document !== 'undefined') {
159
201
  });
160
202
  }
161
203
 
162
- // 不能清空,在index.tsx中加载的只会被加载一次,清空了就没有了
204
+ // can't clear global styles,because in index.tsx it is only loaded once, clear it it will be gone
163
205
  // const clearGlobalStyles = () => {
164
206
  // // reset unique id
165
207
  // _globalStyle.clear();
@@ -169,10 +211,10 @@ if (typeof document !== 'undefined') {
169
211
  export const generateAllGlobalStyles = () => {
170
212
  const result = [];
171
213
 
172
- result.push(`<style id="sty-theme">${generateThemeStyles()}</style>`);
214
+ result.push(`<style id="sty-${themeCookieName}">${generateThemeStyles()}</style>`);
173
215
 
174
- for (let [uniqueStyleId, { topClassName, style }] of _globalStyle) {
175
- const cssText = processStyle(topClassName, style).join('');
216
+ for (let [uniqueStyleId, { topUniqueClassName, style }] of _globalStyle) {
217
+ const cssText = processStyle(topUniqueClassName, style).join('');
176
218
  result.push(`<style id="sty-${uniqueStyleId}">${cssText}</style>`);
177
219
  }
178
220
 
@@ -11,7 +11,7 @@ export const domUniqueId = uniqueIdGenerator('l'); // l means label
11
11
  // domUniqueId(true);
12
12
  // });
13
13
 
14
- function renderChildren(html: string[], children: any) {
14
+ function renderChildren(html: string[], children: any, uniqueClassName?: string) {
15
15
  if (typeof children === 'string') {
16
16
  html.push(children);
17
17
  } else if (children === false || children === null || typeof children === 'undefined') {
@@ -22,10 +22,10 @@ function renderChildren(html: string[], children: any) {
22
22
  } else if (Array.isArray(children)) {
23
23
  for (let i = 0; i < children.length; i++) {
24
24
  const item = children[i];
25
- renderChildren(html, item);
25
+ renderChildren(html, item, uniqueClassName);
26
26
  }
27
27
  } else if (children.type && children.props) {
28
- renderComponent(children.type, children.props);
28
+ renderComponent(children.type, children.props, uniqueClassName);
29
29
  html.push(...children.props._html);
30
30
  children.props._html.length = 0;
31
31
  } else {
@@ -57,16 +57,16 @@ const genUniqueId = (props: any) => {
57
57
  return props._id;
58
58
  };
59
59
  // data-refid will be assigned with a ref.id
60
- function renderAttribute(type: any, props: any, jsxNodes: any) {
60
+ function renderAttribute(type: any, props: any, jsxNodes: any, uniqueClassName?: string) {
61
61
  const html = [];
62
62
  // data-refid is used for nested components like this:
63
63
  // <div class='class-name' ref={ref} ...>...
64
64
  // <div data-refid={ref}>
65
65
  // then data-refid can be located:
66
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
- }
67
+ // if (props['data-refid'] && props['data-refid'].id) {
68
+ // props['data-refid'] = props['data-refid'].id;
69
+ // }
70
70
  for (let i in props) {
71
71
  if (i === 'ref') {
72
72
  if (props[i]) {
@@ -103,11 +103,15 @@ function renderAttribute(type: any, props: any, jsxNodes: any) {
103
103
  html.push(`${i}="${props[i]}"`);
104
104
  }
105
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}"`);
106
+ let classNameList = props[i].split(' ').filter((item: string) => item && item !== '');
107
+ if ((props['css'] || props['ref']) && !classNameList.includes(props._id)) {
108
+ // add as the first
109
+ classNameList.unshift(props._id);
110
+ }
111
+ if (uniqueClassName) {
112
+ classNameList = classNameList.map((item: string) => item.replace(/&/g, uniqueClassName));
113
+ }
114
+ html.push(`class="${classNameList.join(' ')}"`);
111
115
  } else if (i !== 'dangerouslySetInnerHTML') {
112
116
  html.push(`${i}="${props[i]}"`);
113
117
  }
@@ -120,28 +124,12 @@ function renderAttribute(type: any, props: any, jsxNodes: any) {
120
124
  return html.join(' ');
121
125
  }
122
126
 
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
127
  // The result has only one element
140
- export const renderComponent = (type: any, props: any) => {
128
+ export const renderComponent = (type: any, props: any, uniqueClassName?: string) => {
141
129
  // logger.log("==================renderComponent", type);
142
130
  if (Array.isArray(props)) {
143
131
  const jsxNodes = { type: 'Fragment', props: { children: props } } as any;
144
- renderComponent(jsxNodes.type, jsxNodes.props);
132
+ renderComponent(jsxNodes.type, jsxNodes.props, uniqueClassName);
145
133
  return;
146
134
  }
147
135
 
@@ -159,7 +147,7 @@ export const renderComponent = (type: any, props: any) => {
159
147
  // }
160
148
  // logger.log('==========props._result', props._result);
161
149
  if (typeof props._result.type === 'function') {
162
- renderComponent(props._result.type, props._result.props);
150
+ renderComponent(props._result.type, props._result.props, uniqueClassName);
163
151
  if (props._result.props._html) {
164
152
  props._html.push(...props._result.props._html);
165
153
  props._result.props._html.length = 0;
@@ -174,7 +162,17 @@ export const renderComponent = (type: any, props: any) => {
174
162
  console.log('renderComponent', newType, newProps);
175
163
  }
176
164
  if (typeof newType === 'string') {
177
- const attrs = renderAttribute(newType, newProps, { type, props });
165
+ if (newProps._id) {
166
+ console.warn('This component reference is used more than once and will have binding issues: ', newProps);
167
+ }
168
+ let newUniqueClassName = uniqueClassName;
169
+ if (newProps['css'] || newProps['ref']) {
170
+ newUniqueClassName = genUniqueId(newProps);
171
+ if (!newProps['class'] && !newProps['className']) {
172
+ newProps['class'] = newUniqueClassName;
173
+ }
174
+ }
175
+ const attrs = renderAttribute(newType, newProps, { type, props }, newUniqueClassName);
178
176
  if (selfClosingTags.includes(newType.toLowerCase())) {
179
177
  // for Fragment, only needs this tag when Ref is assigned
180
178
  if (newType !== 'Fragment' || newProps.ref) {
@@ -189,8 +187,8 @@ export const renderComponent = (type: any, props: any) => {
189
187
  }
190
188
 
191
189
  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
190
+ const cssText = processStyle(newUniqueClassName!, newProps['css']).join('');
191
+ props._html.push(`<style id="sty-${newUniqueClassName}">${cssText}</style>`); // sty means style, and updateStyles has the same name
194
192
  }
195
193
 
196
194
  if (newProps.children) {
@@ -198,7 +196,7 @@ export const renderComponent = (type: any, props: any) => {
198
196
  // assignLabels(newProps._lb, newProps.children);
199
197
  // }
200
198
 
201
- renderChildren(props._html, newProps.children);
199
+ renderChildren(props._html, newProps.children, newUniqueClassName);
202
200
  } else if (newProps['dangerouslySetInnerHTML']) {
203
201
  props._html.push(newProps['dangerouslySetInnerHTML']);
204
202
  } else {
@@ -210,7 +208,7 @@ export const renderComponent = (type: any, props: any) => {
210
208
  }
211
209
  }
212
210
  } else if (newType.name === 'Fragment') {
213
- renderChildren(props._html, newProps.children);
211
+ renderChildren(props._html, newProps.children, uniqueClassName);
214
212
  } else {
215
213
  logger.warn('Unknown type: ', type, props, newType, newProps);
216
214
  }
@@ -25,9 +25,10 @@
25
25
 
26
26
  export function uniqueIdGenerator(preKey: string) {
27
27
  let count = 0;
28
- let lastKey = Math.round(new Date().getTime() / 1000).toString(36);
28
+ const baseTime = Math.round(Date.now() / 1000);
29
+ let lastKey = '';
29
30
  return function (): string {
30
- const key = Math.round(new Date().getTime() / 1000).toString(36);
31
+ const key = Math.round(Date.now() / 1000 - baseTime).toString(36);
31
32
  if (key !== lastKey) {
32
33
  count = 0;
33
34
  lastKey = key;