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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/jsx-runtime/index.js +14 -0
- package/jsx-runtime/package.json +16 -0
- package/jsx-runtime/src/index.d.ts +2 -0
- package/package.json +51 -0
- package/src/assets/themes/base-themes.ts +16 -0
- package/src/assets/themes/dark-themes.ts +85 -0
- package/src/assets/themes/index.ts +4 -0
- package/src/assets/themes/light-themes.ts +92 -0
- package/src/assets/themes/shared-themes.ts +50 -0
- package/src/components/button-push-animation.tsx +138 -0
- package/src/components/button.tsx +55 -0
- package/src/components/drag-refresh.tsx +110 -0
- package/src/components/editable-label.tsx +83 -0
- package/src/components/float-window.tsx +226 -0
- package/src/components/grid.tsx +18 -0
- package/src/components/html-var.tsx +41 -0
- package/src/components/index.ts +36 -0
- package/src/components/input-with-title.tsx +24 -0
- package/src/components/link-item.tsx +13 -0
- package/src/components/link-list.tsx +62 -0
- package/src/components/menu-bar.tsx +220 -0
- package/src/components/menu-item-props.tsx +10 -0
- package/src/components/menu-sidebar.tsx +289 -0
- package/src/components/message-box.tsx +44 -0
- package/src/components/meta-data.tsx +54 -0
- package/src/components/meta-description.tsx +19 -0
- package/src/components/meta-title.tsx +19 -0
- package/src/components/modal.tsx +29 -0
- package/src/components/notice-message.tsx +119 -0
- package/src/components/paging-link.tsx +100 -0
- package/src/components/panel.tsx +24 -0
- package/src/components/popup-menu.tsx +218 -0
- package/src/components/progress.tsx +91 -0
- package/src/components/redirect.tsx +19 -0
- package/src/components/resizable-splitter.tsx +129 -0
- package/src/components/select-with-title.tsx +37 -0
- package/src/components/spinner.tsx +100 -0
- package/src/components/svg.tsx +24 -0
- package/src/components/tabs.tsx +252 -0
- package/src/components/text-glow.tsx +36 -0
- package/src/components/text-wave.tsx +54 -0
- package/src/components/theme-selector.tsx +35 -0
- package/src/components/toggle-base.tsx +260 -0
- package/src/components/toggle-switch.tsx +156 -0
- package/src/core/bind-attributes.ts +58 -0
- package/src/core/bind-lang.ts +51 -0
- package/src/core/bind-links.ts +16 -0
- package/src/core/bind-ref.ts +33 -0
- package/src/core/bind-styles.ts +180 -0
- package/src/core/bind-theme.ts +51 -0
- package/src/core/camel-to-hyphens.ts +3 -0
- package/src/core/core.ts +179 -0
- package/src/core/index.ts +15 -0
- package/src/core/mount-components.ts +259 -0
- package/src/core/page-loaded-events.ts +16 -0
- package/src/core/page-router.ts +170 -0
- package/src/core/replace-innerhtml.ts +10 -0
- package/src/core/server-cookie.ts +22 -0
- package/src/core/web-version.ts +12 -0
- package/src/global.d.ts +66 -0
- package/src/index.ts +15 -0
- package/src/jsx.ts +1041 -0
- package/src/lib/date-utils.ts +317 -0
- package/src/lib/debug-watch.ts +31 -0
- package/src/lib/deep-merge.ts +37 -0
- package/src/lib/document-ready.ts +36 -0
- package/src/lib/dom/calculate-text-width.ts +13 -0
- package/src/lib/dom/cookie.ts +41 -0
- package/src/lib/dom/download-stream.ts +17 -0
- package/src/lib/dom/download.ts +12 -0
- package/src/lib/dom/index.ts +71 -0
- package/src/lib/dynamical-load.ts +138 -0
- package/src/lib/format-bytes.ts +11 -0
- package/src/lib/index.ts +17 -0
- package/src/lib/lite-dom.ts +227 -0
- package/src/lib/logger.ts +55 -0
- package/src/lib/message-hub.ts +105 -0
- package/src/lib/observable.ts +188 -0
- package/src/lib/promise-timeout.ts +1 -0
- package/src/lib/simple-storage.ts +40 -0
- package/src/lib/stop-propagation.ts +7 -0
- package/src/lib/unique-id.ts +39 -0
- package/src/lib/upload-file.ts +68 -0
- package/src/lib/web-env.ts +98 -0
- package/src/models/index.ts +2 -0
- package/src/models/simple-storage-props.ts +9 -0
- package/src/models/to-client-delivery-props.ts +8 -0
- package/src/types/css-styles.ts +814 -0
- package/src/types/css-types.ts +17 -0
- package/src/types/index.ts +6 -0
- package/src/types/media-query.ts +93 -0
- 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
|
+
};
|
package/src/core/core.ts
ADDED
|
@@ -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
|
+
};
|