lupine.web 1.0.14

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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/jsx-runtime/index.js +14 -0
  4. package/jsx-runtime/package.json +16 -0
  5. package/jsx-runtime/src/index.d.ts +2 -0
  6. package/package.json +51 -0
  7. package/src/assets/themes/base-themes.ts +16 -0
  8. package/src/assets/themes/dark-themes.ts +85 -0
  9. package/src/assets/themes/index.ts +4 -0
  10. package/src/assets/themes/light-themes.ts +92 -0
  11. package/src/assets/themes/shared-themes.ts +50 -0
  12. package/src/components/button-push-animation.tsx +138 -0
  13. package/src/components/button.tsx +55 -0
  14. package/src/components/drag-refresh.tsx +110 -0
  15. package/src/components/editable-label.tsx +83 -0
  16. package/src/components/float-window.tsx +226 -0
  17. package/src/components/grid.tsx +18 -0
  18. package/src/components/html-var.tsx +41 -0
  19. package/src/components/index.ts +36 -0
  20. package/src/components/input-with-title.tsx +24 -0
  21. package/src/components/link-item.tsx +13 -0
  22. package/src/components/link-list.tsx +62 -0
  23. package/src/components/menu-bar.tsx +220 -0
  24. package/src/components/menu-item-props.tsx +10 -0
  25. package/src/components/menu-sidebar.tsx +289 -0
  26. package/src/components/message-box.tsx +44 -0
  27. package/src/components/meta-data.tsx +54 -0
  28. package/src/components/meta-description.tsx +19 -0
  29. package/src/components/meta-title.tsx +19 -0
  30. package/src/components/modal.tsx +29 -0
  31. package/src/components/notice-message.tsx +119 -0
  32. package/src/components/paging-link.tsx +100 -0
  33. package/src/components/panel.tsx +24 -0
  34. package/src/components/popup-menu.tsx +218 -0
  35. package/src/components/progress.tsx +91 -0
  36. package/src/components/redirect.tsx +19 -0
  37. package/src/components/resizable-splitter.tsx +129 -0
  38. package/src/components/select-with-title.tsx +37 -0
  39. package/src/components/spinner.tsx +100 -0
  40. package/src/components/svg.tsx +24 -0
  41. package/src/components/tabs.tsx +252 -0
  42. package/src/components/text-glow.tsx +36 -0
  43. package/src/components/text-wave.tsx +54 -0
  44. package/src/components/theme-selector.tsx +35 -0
  45. package/src/components/toggle-base.tsx +260 -0
  46. package/src/components/toggle-switch.tsx +156 -0
  47. package/src/core/bind-attributes.ts +58 -0
  48. package/src/core/bind-lang.ts +51 -0
  49. package/src/core/bind-links.ts +16 -0
  50. package/src/core/bind-ref.ts +33 -0
  51. package/src/core/bind-styles.ts +180 -0
  52. package/src/core/bind-theme.ts +51 -0
  53. package/src/core/camel-to-hyphens.ts +3 -0
  54. package/src/core/core.ts +179 -0
  55. package/src/core/index.ts +15 -0
  56. package/src/core/mount-components.ts +259 -0
  57. package/src/core/page-loaded-events.ts +16 -0
  58. package/src/core/page-router.ts +170 -0
  59. package/src/core/replace-innerhtml.ts +10 -0
  60. package/src/core/server-cookie.ts +22 -0
  61. package/src/core/web-version.ts +12 -0
  62. package/src/global.d.ts +66 -0
  63. package/src/index.ts +15 -0
  64. package/src/jsx.ts +1041 -0
  65. package/src/lib/date-utils.ts +317 -0
  66. package/src/lib/debug-watch.ts +31 -0
  67. package/src/lib/deep-merge.ts +37 -0
  68. package/src/lib/document-ready.ts +36 -0
  69. package/src/lib/dom/calculate-text-width.ts +13 -0
  70. package/src/lib/dom/cookie.ts +41 -0
  71. package/src/lib/dom/download-stream.ts +17 -0
  72. package/src/lib/dom/download.ts +12 -0
  73. package/src/lib/dom/index.ts +71 -0
  74. package/src/lib/dynamical-load.ts +138 -0
  75. package/src/lib/format-bytes.ts +11 -0
  76. package/src/lib/index.ts +17 -0
  77. package/src/lib/lite-dom.ts +227 -0
  78. package/src/lib/logger.ts +55 -0
  79. package/src/lib/message-hub.ts +105 -0
  80. package/src/lib/observable.ts +188 -0
  81. package/src/lib/promise-timeout.ts +1 -0
  82. package/src/lib/simple-storage.ts +40 -0
  83. package/src/lib/stop-propagation.ts +7 -0
  84. package/src/lib/unique-id.ts +39 -0
  85. package/src/lib/upload-file.ts +68 -0
  86. package/src/lib/web-env.ts +98 -0
  87. package/src/models/index.ts +2 -0
  88. package/src/models/simple-storage-props.ts +9 -0
  89. package/src/models/to-client-delivery-props.ts +8 -0
  90. package/src/types/css-styles.ts +814 -0
  91. package/src/types/css-types.ts +17 -0
  92. package/src/types/index.ts +6 -0
  93. package/src/types/media-query.ts +93 -0
  94. package/tsconfig.json +113 -0
@@ -0,0 +1,180 @@
1
+ import { CssProps } from '../jsx';
2
+ import { getCurrentTheme, themeCookieName } from './bind-theme';
3
+ import { camelToHyphens } from './camel-to-hyphens';
4
+ // import { bindPageResetEvent } from './page-reset-events';
5
+ import { bindPageLoadedEvent } from './page-loaded-events';
6
+
7
+ const wrapCss = (className: string, cssText: string, mediaQuery?: string) => {
8
+ // if (!className) {
9
+ // console.warn(`No class name is provided for ${cssText}`);
10
+ // }
11
+ let cssTextWrap = className ? `${className}{${cssText}}` : cssText;
12
+ if (mediaQuery) {
13
+ cssTextWrap = `${mediaQuery}{${cssTextWrap}}`;
14
+ }
15
+ return cssTextWrap;
16
+ };
17
+
18
+ const processStyleValue = (style: CssProps) => {
19
+ return Object.keys(style)
20
+ .map((key) => key.trim())
21
+ .map((key) => {
22
+ const noOutput =
23
+ (style[key] != null && typeof style[key] === 'object') ||
24
+ typeof style[key] === 'undefined' ||
25
+ style[key] === '';
26
+ return noOutput ? '' : `${camelToHyphens(key)}:${style[key]};`;
27
+ })
28
+ .join('');
29
+ };
30
+
31
+ const updateOneBlock = (css: string[], cssTemp: string[], className: string, mediaQuery?: string) => {
32
+ if (cssTemp.length > 0) {
33
+ const cssText = wrapCss(className, cssTemp.join(''), mediaQuery);
34
+ css.push(cssText);
35
+ cssTemp.length = 0;
36
+ }
37
+ };
38
+
39
+ export const processStyle = (className: string, style: CssProps, mediaQuery?: string): string[] => {
40
+ const css: string[] = [];
41
+ const cssTemp: string[] = [];
42
+ for (let i in style) {
43
+ const value = style[i];
44
+ if (value === null || typeof value !== 'object') {
45
+ if (value !== '' && typeof value !== 'undefined') {
46
+ if (!className) {
47
+ console.warn(`No className is defined for: ${camelToHyphens(i)}:${value};`);
48
+ }
49
+ cssTemp.push(`${camelToHyphens(i)}:${value};`);
50
+ }
51
+ } else {
52
+ updateOneBlock(css, cssTemp, className, mediaQuery);
53
+
54
+ if (i.startsWith('@keyframes')) {
55
+ const cssText = Object.keys(value)
56
+ .map((stageKey) => stageKey + '{' + processStyleValue(value[stageKey] as CssProps) + '}')
57
+ .join('');
58
+ css.push(`${i}{${cssText}}`);
59
+ } else if (i.startsWith('@media')) {
60
+ const ret = processStyle(className, value, i);
61
+ css.push(...ret);
62
+ } else {
63
+ // '&:hover, &.open': {
64
+ // '>.d1, .d2': {
65
+ // },
66
+ // }, ==>
67
+ // &:hover >.d1, &:hover >.d2, &.open >.d1, &.open .d2
68
+ const newClassName = !className
69
+ ? i
70
+ : className
71
+ .split(',')
72
+ .map((key0) => key0.trim())
73
+ .map((key0) => {
74
+ return i
75
+ .split(',')
76
+ .map((key) => key.trim())
77
+ .map((key) => {
78
+ // not needed to "+" as them share same parents?
79
+ // return key.split('+').map(key2 => key2.startsWith('&') ? key0 + key2.substring(1) : key0 + ' ' + key2).join('+');
80
+ return key.startsWith('&') ? key0 + key.substring(1) : key0 + ' ' + key;
81
+ })
82
+ .join(',');
83
+ })
84
+ .join(',');
85
+ const ret = processStyle(newClassName, value, mediaQuery);
86
+ css.push(...ret);
87
+ }
88
+ }
89
+ }
90
+ updateOneBlock(css, cssTemp, className, mediaQuery);
91
+ return css;
92
+ };
93
+
94
+ // mount-components has the same name `sty-`
95
+ export const updateStyles = (selector: string, style: CssProps) => {
96
+ const el = selector && document.querySelector(selector);
97
+ if (el) {
98
+ const cssText = processStyle(selector, style).join('');
99
+ // if the first child is style, then update it
100
+ if (el.firstChild && el.firstChild.nodeName === 'STYLE') {
101
+ (el.firstChild as any).innerHTML = cssText;
102
+ } else {
103
+ const style = document.createElement('style');
104
+ style.innerHTML = cssText;
105
+ // style.id = `sty-${selector}`; // sty means style, this is different from mount-components.renderComponent
106
+ el.prepend(style);
107
+ }
108
+ } else {
109
+ console.warn(`Can't find "${selector}" to update styles.`);
110
+ }
111
+ };
112
+
113
+ const updateCssDom = (uniqueStyleId: string, cssText: string, cssDom: HTMLElement | null) => {
114
+ if (!cssDom) {
115
+ cssDom = document.createElement('style');
116
+ cssDom.id = `sty-${uniqueStyleId}`;
117
+ document.head.appendChild(cssDom);
118
+ }
119
+ cssDom.innerText = cssText;
120
+ };
121
+
122
+ /*
123
+ Global styles including theme will not be updated once it's created.
124
+ topClassName is a className or a tag name.
125
+ For example, it can be like this for all elements:
126
+ html { ... } or :root { ... }
127
+ */
128
+ const _globalStyle = new Map();
129
+ export const bindGlobalStyles = (uniqueStyleId: string, topClassName: string, style: CssProps, forceUpdate = false) => {
130
+ if (typeof document !== 'undefined') {
131
+ let cssDom = document.getElementById(`sty-${uniqueStyleId}`);
132
+ if (forceUpdate || !cssDom) {
133
+ updateCssDom(uniqueStyleId, processStyle(topClassName, style).join(''), cssDom);
134
+ }
135
+ } else if (!_globalStyle.has(uniqueStyleId) || forceUpdate) {
136
+ // don't overwrite it to have the same behavior as in the Browser
137
+ _globalStyle.set(uniqueStyleId, { topClassName, style });
138
+ }
139
+ };
140
+
141
+ const generateThemeStyles = () => {
142
+ const currentTheme = getCurrentTheme();
143
+ const themeCss = [];
144
+ for (let themeName in currentTheme.themes) {
145
+ // i is for case-insensitive
146
+ themeCss.push(...processStyle(`[data-theme="${themeName}" i]`, currentTheme.themes[themeName]));
147
+ }
148
+ return themeCss.join('\n');
149
+ };
150
+
151
+ if (typeof document !== 'undefined') {
152
+ // Update theme in Browser when no SSR
153
+ bindPageLoadedEvent(() => {
154
+ const uniqueStyleId = themeCookieName;
155
+ let cssDom = document.getElementById(`sty-${uniqueStyleId}`);
156
+ if (!cssDom) {
157
+ updateCssDom(uniqueStyleId, generateThemeStyles(), cssDom);
158
+ }
159
+ });
160
+ }
161
+
162
+ const clearGlobalStyles = () => {
163
+ // reset unique id
164
+ _globalStyle.clear();
165
+ };
166
+ // bindPageResetEvent(clearGlobalStyles);
167
+
168
+ export const generateAllGlobalStyles = () => {
169
+ const result = [];
170
+
171
+ result.push(`<style id="sty-theme">${generateThemeStyles()}</style>`);
172
+
173
+ for (let [uniqueStyleId, { topClassName, style }] of _globalStyle) {
174
+ const cssText = processStyle(topClassName, style).join('');
175
+ result.push(`<style id="sty-${uniqueStyleId}">${cssText}</style>`);
176
+ }
177
+
178
+ clearGlobalStyles();
179
+ return result.join('');
180
+ };
@@ -0,0 +1,51 @@
1
+ import { CssProps } from '../jsx';
2
+ import { DomUtils } from '../lib';
3
+ import { getEitherCookie } from './server-cookie';
4
+
5
+ // theme doesn't need to reset, theme name is stored in cookie
6
+
7
+ export const defaultThemeName = 'light';
8
+ export const themeCookieName = 'theme';
9
+ export const updateThemeEventName = 'updateTheme';
10
+ export const themeAttributeName = 'data-theme';
11
+ const _themeCfg: any = { defaultTheme: defaultThemeName, themes: {} };
12
+ export const bindTheme = (defaultTheme: string, themes: { [key: string]: CssProps }) => {
13
+ _themeCfg.defaultTheme = defaultTheme;
14
+ _themeCfg.themes = themes;
15
+
16
+ // set to cookie
17
+ getCurrentTheme();
18
+ };
19
+
20
+ export const getCurrentTheme = () => {
21
+ let themeName = getEitherCookie(themeCookieName) as string;
22
+ if (!themeName || !_themeCfg.themes[themeName]) {
23
+ themeName = _themeCfg.defaultTheme;
24
+ if (typeof window !== 'undefined') {
25
+ DomUtils.setCookie(themeCookieName, _themeCfg.defaultTheme);
26
+ }
27
+ }
28
+ return { themeName, themes: _themeCfg.themes as { [key: string]: CssProps } };
29
+ };
30
+
31
+ export const updateTheme = (themeName: string) => {
32
+ // Theme is only updated in Browser
33
+ _themeCfg.defaultTheme = themeName;
34
+ if (typeof window === 'undefined') {
35
+ return;
36
+ }
37
+
38
+ DomUtils.setCookie(themeCookieName, themeName);
39
+ document.documentElement.setAttribute(themeAttributeName, themeName);
40
+
41
+ // update theme for all iframe
42
+ const allIframe = document.querySelectorAll('iframe');
43
+ for (let i = 0; i < allIframe.length; i++) {
44
+ if (allIframe[i].contentWindow && allIframe[i].contentWindow!.top === window) {
45
+ allIframe[i].contentWindow!.document.documentElement.setAttribute(themeAttributeName, themeName);
46
+ }
47
+ }
48
+
49
+ const event = new CustomEvent(updateThemeEventName, { detail: themeName });
50
+ window.dispatchEvent(event);
51
+ };
@@ -0,0 +1,3 @@
1
+ export const camelToHyphens = function (name: string) {
2
+ return name.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
3
+ };
@@ -0,0 +1,179 @@
1
+ import { getMetaDataObject, getMetaDataTags, getMetaTitle } from '../components';
2
+ import { VNode } from '../jsx';
3
+ import { initWebEnv, initWebSetting } from '../lib';
4
+ import { Logger } from '../lib/logger';
5
+ import { generateAllGlobalStyles } from './bind-styles';
6
+ import { defaultThemeName, getCurrentTheme, updateTheme } from './bind-theme';
7
+ import { mountComponents } from './mount-components';
8
+ // import { callPageResetEvent } from './page-reset-events';
9
+ import { PageRouter } from './page-router';
10
+ import { callPageLoadedEvent } from './page-loaded-events';
11
+ import { initServerCookies } from './server-cookie';
12
+ import { IToClientDelivery } from '../models/to-client-delivery-props';
13
+
14
+ export type JsonKeyValue = {
15
+ [key: string]: string | number | boolean | null | undefined | JsonKeyValue | JsonKeyValue[];
16
+ };
17
+ export type JsonObject =
18
+ | JsonKeyValue[]
19
+ | {
20
+ [key: string]: string | number | boolean | null | undefined | JsonKeyValue | JsonKeyValue[];
21
+ };
22
+
23
+ export type RenderPageFunctionsType = {
24
+ fetchData: (url: string, postBody?: string | JsonObject, returnRawResponse?: boolean) => Promise<any>;
25
+ [key: string]: Function;
26
+ };
27
+ export interface PageProps {
28
+ url: string;
29
+ // urlSections: string[];
30
+ query: { [key: string]: string };
31
+ urlParameters: { [key: string]: string };
32
+ renderPageFunctions: RenderPageFunctionsType;
33
+ }
34
+
35
+ export type PageResultType = {
36
+ content: string;
37
+ title: string;
38
+ metaData: string;
39
+ themeName: string;
40
+ globalCss: string;
41
+ };
42
+ export type _LupineJs = {
43
+ generatePage: (props: any, toClientDelivery: IToClientDelivery) => Promise<PageResultType>;
44
+ renderPageFunctions: RenderPageFunctionsType;
45
+ router: PageRouter | ((props: PageProps) => Promise<VNode<any>>);
46
+ renderPageProps: PageProps;
47
+ };
48
+
49
+ const logger = new Logger('core');
50
+ export const _lupineJs: _LupineJs = {} as _LupineJs;
51
+
52
+ // for SSR, it exports _lupineJs function for the server to call
53
+ if (typeof exports !== 'undefined') {
54
+ // ignore esbuild's warnings:
55
+ // The CommonJS "exports" variable is treated as a global variable in an ECMAScript module and may not work as expected [commonjs-variable-in-esm]
56
+ exports._lupineJs = () => {
57
+ return _lupineJs;
58
+ };
59
+ }
60
+
61
+ // this should be called by the FE and also by the server side to set fetchData and others for client and server side rendering.
62
+ // And the RenderPageFunctionsType will be passed to call (generate) a page through PageProps
63
+ export const bindRenderPageFunctions = (calls: RenderPageFunctionsType) => {
64
+ _lupineJs.renderPageFunctions = calls || {};
65
+ };
66
+ // export const getRenderPageFunctions = (): RenderPageFunctionsType => {
67
+ // return globalThis._lupineJs.renderPageFunctions;
68
+ // }
69
+ // this is only used inside the core
70
+ const setRenderPageProps = (props: PageProps) => {
71
+ _lupineJs.renderPageProps = props;
72
+ };
73
+ // this is used by the code to get url info when it's executed in the FE or in the server side.
74
+ export const getRenderPageProps = (): PageProps => {
75
+ return _lupineJs.renderPageProps;
76
+ };
77
+
78
+ export const bindRouter = (router: PageRouter | ((props: PageProps) => Promise<VNode<any>>)) => {
79
+ _lupineJs.router = router;
80
+ };
81
+
82
+ export const isFrontEnd = () => {
83
+ return typeof window === 'object' && typeof document === 'object';
84
+ };
85
+
86
+ const renderTargetPage = async (props: PageProps, renderPartPage: boolean) => {
87
+ if (_lupineJs.router instanceof PageRouter) {
88
+ return _lupineJs.router.handleRoute(props.url, props, renderPartPage);
89
+ }
90
+ return await _lupineJs.router(props);
91
+ };
92
+
93
+ // this is called by server side for SSR (server-side-rendering)
94
+ export const generatePage = async (props: PageProps, toClientDelivery: IToClientDelivery): Promise<PageResultType> => {
95
+ setRenderPageProps(props);
96
+
97
+ initWebEnv(toClientDelivery.getWebEnv());
98
+ initWebSetting(toClientDelivery.getWebSetting());
99
+ initServerCookies(toClientDelivery.getServerCookie());
100
+ // callPageResetEvent();
101
+ callPageLoadedEvent();
102
+
103
+ const jsxNodes = await renderTargetPage(props, false);
104
+ if (!jsxNodes || !jsxNodes.props) {
105
+ return {
106
+ content: `Unexpected url: ${props.url}`,
107
+ title: '',
108
+ metaData: '',
109
+ globalCss: '',
110
+ themeName: defaultThemeName,
111
+ };
112
+ }
113
+
114
+ await mountComponents(null, jsxNodes);
115
+ const currentTheme = getCurrentTheme();
116
+ const cssText = generateAllGlobalStyles();
117
+ const content = jsxNodes.props._html.join('');
118
+
119
+ return {
120
+ content,
121
+ title: getMetaTitle(),
122
+ metaData: getMetaDataTags(),
123
+ globalCss: cssText,
124
+ themeName: currentTheme.themeName,
125
+ };
126
+ };
127
+ _lupineJs.generatePage = generatePage;
128
+
129
+ let _pageInitialized = false;
130
+ // this is called in the FE when the document is loaded
131
+ export const initializePage = async (newUrl?: string) => {
132
+ const currentPageInitialized = _pageInitialized;
133
+ _pageInitialized = true;
134
+ logger.log('initializePage: ', newUrl);
135
+ if (newUrl) {
136
+ window.history.pushState({ urlPath: newUrl }, '', newUrl);
137
+ // prevents browser from storing history with each change:
138
+ // window.history.replaceState({ html: '', pageTitle: newUrl }, '', newUrl);
139
+ }
140
+ const splitUrl = newUrl ? newUrl.split('?') : [];
141
+ const url = splitUrl[0] || document.location.pathname;
142
+ const queryString = splitUrl[1] || document.location.search;
143
+
144
+ const props: PageProps = {
145
+ url,
146
+ // urlSections: url.split('/').filter((i) => !!i),
147
+ query: Object.fromEntries(new URLSearchParams(queryString)), // new URLSearchParams(queryString),
148
+ urlParameters: {},
149
+ renderPageFunctions: _lupineJs.renderPageFunctions,
150
+ };
151
+
152
+ setRenderPageProps(props);
153
+ // !currentPageInitialized && callPageResetEvent();
154
+ !currentPageInitialized && callPageLoadedEvent();
155
+
156
+ const jsxNodes = await renderTargetPage(props, currentPageInitialized);
157
+ if (jsxNodes === null) return;
158
+ if (!jsxNodes || !jsxNodes.props) {
159
+ document.querySelector('.lupine-root')!.innerHTML = `Unexpected url: ${url}`;
160
+ return;
161
+ }
162
+
163
+ // generateAllGlobalStyles will be updated directly in Browser
164
+ await mountComponents('.lupine-root', jsxNodes);
165
+ updateTheme(getCurrentTheme().themeName);
166
+
167
+ // title
168
+ document.title = getMetaTitle();
169
+ const metaData = getMetaDataObject();
170
+ // meta data?
171
+ };
172
+ if (typeof window !== 'undefined') {
173
+ addEventListener('popstate', (event) => {
174
+ initializePage();
175
+ });
176
+ addEventListener('load', (event) => {
177
+ initializePage();
178
+ });
179
+ }
@@ -0,0 +1,15 @@
1
+ export * from './bind-attributes';
2
+ export * from './bind-lang';
3
+ export * from './bind-links';
4
+ export * from './bind-ref';
5
+ export * from './bind-styles';
6
+ export * from './bind-theme';
7
+ export * from './core';
8
+ export * from './camel-to-hyphens';
9
+ export * from './mount-components';
10
+ export * from './page-loaded-events';
11
+ export * from './page-router';
12
+ export * from './replace-innerhtml';
13
+ export * from './server-cookie';
14
+ export * from './web-version';
15
+
@@ -0,0 +1,259 @@
1
+ import { bindAttributes } from './bind-attributes';
2
+ import { bindLinks } from './bind-links';
3
+ import { VNode } from '../jsx';
4
+ import { Logger } from '../lib/logger';
5
+ import { uniqueIdGenerator } from '../lib/unique-id';
6
+ import { processStyle } from './bind-styles';
7
+ // import { bindPageResetEvent } from './page-reset-events';
8
+ 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
+ };
214
+
215
+ export const mountComponents = async (selector: string | null | Element, jsxNodes: VNode<any>) => {
216
+ renderComponent(jsxNodes.type, jsxNodes.props);
217
+ const el = selector && (typeof selector === 'string' ? document.querySelector(selector) : selector);
218
+ 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
+ // call unload before releace innerHTML
228
+ // el.innerHTML = jsxNodes.props._html.join('');
229
+ await replaceInnerhtml(el, jsxNodes.props._html.join(''));
230
+
231
+ // if (firstDom && firstDom.tagName === 'STYLE') {
232
+ // el.insertBefore(firstDom, el.firstChild);
233
+ // }
234
+ bindAttributes(el, jsxNodes.type, jsxNodes.props);
235
+ bindLinks(el);
236
+ }
237
+ };
238
+
239
+ // suggest to use HtmlVar.
240
+ export const mountSelfComponents = async (selector: string | null | Element, jsxNodes: VNode<any>) => {
241
+ renderComponent(jsxNodes.type, jsxNodes.props);
242
+ let el = selector && (typeof selector === 'string' ? document.querySelector(selector) : selector);
243
+ if (el) {
244
+ const parentNode = el.parentElement;
245
+ // Can't do outerHTML directly because it will lose attributes
246
+ const template = document.createElement('template');
247
+ // template.innerHTML = jsxNodes.props._html.join("");
248
+ // call unload before releace innerHTML
249
+ await replaceInnerhtml(template, jsxNodes.props._html.join(''));
250
+
251
+ // renderComponent should only have one element
252
+ template.content.children.length > 1 &&
253
+ console.error('renderComponent should only have one element: ', template.content.children.length);
254
+ el.replaceWith(template.content.firstChild as Element);
255
+ el = parentNode as Element;
256
+ bindAttributes(el, jsxNodes.type, jsxNodes.props);
257
+ bindLinks(el);
258
+ }
259
+ };