@nutin/cli 1.2.3 → 1.3.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/CHANGELOG.md +36 -1
- package/README.md +23 -15
- package/lib/index.mjs +7 -4
- package/lib/src/context-builder.mjs +35 -0
- package/lib/src/file-generator.mjs +26 -126
- package/lib/src/git-manager.mjs +1 -1
- package/lib/src/json-generator.mjs +31 -0
- package/lib/src/package-manager.mjs +21 -18
- package/lib/src/project-generator.mjs +53 -0
- package/lib/src/prompts.mjs +28 -15
- package/lib/src/template-compiler.mjs +1 -1
- package/lib/src/utils.mjs +9 -9
- package/lib/src/version.mjs +1 -0
- package/package.json +1 -1
- package/templates/base/.gitignore.hbs +1 -0
- package/templates/base/builder.config.js.hbs +7 -0
- package/templates/base/src/app/main.ts.hbs +2 -1
- package/templates/base/src/app/views/home/home.view.test.js.hbs +26 -0
- package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
- package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +1 -1
- package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +23 -20
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +2 -2
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +4 -4
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +26 -13
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +9 -9
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +40 -8
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +16 -16
- package/templates/base/src/core/base-classes/component/component.ts.hbs +12 -12
- package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +2 -2
- package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +2 -2
- package/templates/base/src/core/base-classes/service/service.ts.hbs +35 -12
- package/templates/base/src/core/base-classes/view/view.ts.hbs +6 -6
- package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +13 -13
- package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +18 -18
- package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +24 -24
- package/templates/base/src/core/services/http-client/http-client.ts.hbs +19 -19
- package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +6 -6
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +13 -13
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +22 -22
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +6 -5
- package/templates/base/src/core/services/router/router.ts.hbs +45 -45
- package/templates/base/src/core/services/store/store.ts.hbs +6 -6
- package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +6 -8
- package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +9 -11
- package/templates/base/src/core/utils/helpers/attributes.helper.ts.hbs +11 -5
- package/templates/base/src/libs/pipes/pipes.ts.hbs +1 -1
- package/templates/base/src/libs/popover/popover.ts.hbs +48 -48
- package/templates/base/src/libs/snackbar/snackbar.ts.hbs +1 -1
- package/templates/base/tools/builder/builder.js.hbs +9 -3
- package/templates/base/tools/builder/core/add-tags.js.hbs +46 -0
- package/templates/base/tools/builder/core/copy-static.js.hbs +35 -35
- package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -6
- package/templates/base/tools/builder/core/hash-files.js.hbs +52 -0
- package/templates/base/tools/builder/core/validate-html.js.hbs +12 -32
- package/templates/base/tools/utils/get-files-recursive.js.hbs +17 -8
- package/templates/features/deployHelper/tools/builder/core/compress-files.js.hbs +56 -0
- package/templates/{base → features/deployHelper}/tools/deployment/Dockerfile.hbs +12 -4
- package/templates/{base → features/deployHelper}/tools/deployment/nginx.conf.hbs +30 -2
- package/templates/features/i18n/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +4 -6
- package/templates/features/i18n/src/core/services/i18n/i18n.ts.hbs +38 -47
- package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +17 -0
- package/templates/features/template/src/app/views/home/home.view.html.hbs +1 -1
- package/templates/features/template/tools/builder/core/merge-templates.js.hbs +2 -2
- package/templates/features/testinNutin/testin-nutin/core/src/utils/test-queue.js.hbs +2 -2
- package/templates/features/testinNutin/testin-nutin/runner.js.hbs +1 -0
- package/templates/features/testinNutin/testin-nutin.config.js.hbs +2 -2
- package/templates/base/tools/deployment/gzip.conf.hbs +0 -19
- package/templates/features/i18n/src/app/views/home/locales/fr.json.hbs +0 -9
- /package/templates/{base → features/deployHelper}/.dockerignore.hbs +0 -0
package/lib/src/utils.mjs
CHANGED
|
@@ -5,14 +5,14 @@ import { print } from './print.mjs';
|
|
|
5
5
|
export const promiseExec = promisify(exec);
|
|
6
6
|
|
|
7
7
|
export function displaySuccessMessage(answers) {
|
|
8
|
-
const { projectName } = answers;
|
|
9
|
-
|
|
8
|
+
const { projectName, packageManager, deployHelper } = answers;
|
|
10
9
|
print.boldSuccess('\n🎉 Your project is ready!');
|
|
11
|
-
print.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
10
|
+
print.boldInfo(`Documentation on https://www.nutin.org`);
|
|
11
|
+
|
|
12
|
+
if (packageManager === 'pnpm' || packageManager === 'bun') {
|
|
13
|
+
if (deployHelper) print.boldError(`You'll need to modify the Dockerfile to support ${packageManager}`)
|
|
14
|
+
}
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
print.info('Next steps:');
|
|
17
|
+
print.info(` cd ${projectName} && ${packageManager} run serve\n`);
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const packageVersion = '1.3.0';
|
package/package.json
CHANGED
|
@@ -22,6 +22,7 @@ class App {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
private renderBaseLayout() {
|
|
25
|
+
// e
|
|
25
26
|
// document.body.prepend(this.header.render());
|
|
26
27
|
// document.body.append(this.footer.render());
|
|
27
28
|
}
|
|
@@ -36,5 +37,5 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
36
37
|
});
|
|
37
38
|
|
|
38
39
|
window.addEventListener('beforeunload', async () => {
|
|
39
|
-
Service.destroyAll();
|
|
40
|
+
await Service.destroyAll();
|
|
40
41
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { HomeView } from '#root/dist/src/app/views/index.js';
|
|
2
|
+
|
|
3
|
+
let view;
|
|
4
|
+
|
|
5
|
+
describe('HomeView', () => {
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
view = new HomeView();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should be defined', () => {
|
|
11
|
+
expect(view).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
{{#if i18n}}
|
|
14
|
+
it('should have title with i18n key', () => {
|
|
15
|
+
const h1 = view.element.querySelector('h1');
|
|
16
|
+
expect(h1).toBeDefined();
|
|
17
|
+
expect(h1.textContent).toBe('home.title');
|
|
18
|
+
});
|
|
19
|
+
{{else}}
|
|
20
|
+
it('should have title', () => {
|
|
21
|
+
const h1 = view.element.querySelector('h1');
|
|
22
|
+
expect(h1).toBeDefined();
|
|
23
|
+
expect(h1.textContent).toBe('{{projectName}}');
|
|
24
|
+
});
|
|
25
|
+
{{/if}}
|
|
26
|
+
})
|
|
@@ -4,7 +4,7 @@ const template = `{{#if template}}__TEMPLATE_PLACEHOLDER__{{else}}<div
|
|
|
4
4
|
{{#if stylinNutin}}
|
|
5
5
|
class="u-flex-center u-flex-column u-padd-y-large"
|
|
6
6
|
{{else}}
|
|
7
|
-
style="display:flex;justify-content:center;align-items:center;flex-direction:column"
|
|
7
|
+
style="display:flex;justify-content:center;align-items:center;flex-direction:column;padding-top:1rem;padding-bottom: 1rem;"
|
|
8
8
|
{{/if}}>
|
|
9
9
|
<h1
|
|
10
10
|
{{#if i18n}}
|
|
@@ -33,7 +33,7 @@ const template = `{{#if template}}__TEMPLATE_PLACEHOLDER__{{else}}<div
|
|
|
33
33
|
class="u-font-large u-padd-y-medium"
|
|
34
34
|
style="margin-top:-2rem;"
|
|
35
35
|
{{else}}
|
|
36
|
-
style="font-size:1.5rem;paddin-top:1rem;padding-bottom:1rem;margin-top
|
|
36
|
+
style="font-size:1.5rem;paddin-top:1rem;padding-bottom:1rem;margin-top:-2rem;"
|
|
37
37
|
{{/if}}
|
|
38
38
|
>{{#unless i18n}}You're all set !{{/unless}}</div>
|
|
39
39
|
<div
|
|
@@ -35,7 +35,7 @@ export class NotFoundView extends View {
|
|
|
35
35
|
return [
|
|
36
36
|
{
|
|
37
37
|
selector: 'home',
|
|
38
|
-
factory: (el) => new ButtonComponent(el, { {{#if i18n}}i18nKey: 'not-found.redirect'{{else}}textContent: 'Back to home'{{/if}}, callback: () => this.handleHome(), {{#if stylinNutin}}className: btnClass{{else}}
|
|
38
|
+
factory: (el) => new ButtonComponent(el, { {{#if i18n}}i18nKey: 'not-found.redirect'{{else}}textContent: 'Back to home'{{/if}}, callback: () => this.handleHome(), {{#if stylinNutin}}className: btnClass{{else}}style: btnStyles{{/if}} })
|
|
39
39
|
},
|
|
40
40
|
]
|
|
41
41
|
}
|
|
@@ -6,6 +6,7 @@ import { I18nHelper } from './helpers/i18n.helper.js';
|
|
|
6
6
|
import { PipeHelper } from './helpers/pipe.helper.js';
|
|
7
7
|
import { ChildrenHelper } from './helpers/children.helper.js';
|
|
8
8
|
import { CatalogHelper, CatalogConfig } from './helpers/catalog.helper.js';
|
|
9
|
+
import { TrustLevel } from './helpers/security.helper.js';
|
|
9
10
|
export { CatalogItemConfig } from './helpers/catalog.helper.js';
|
|
10
11
|
|
|
11
12
|
/**```typescript
|
|
@@ -24,6 +25,7 @@ export interface BaseComponentOptions {
|
|
|
24
25
|
template?: string;
|
|
25
26
|
mountTarget?: string | HTMLElement;
|
|
26
27
|
tagName?: keyof HTMLElementTagNameMap;
|
|
28
|
+
trustLevel?: TrustLevel;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export abstract class BaseComponent<T extends HTMLElement = HTMLElement> {
|
|
@@ -34,27 +36,28 @@ export abstract class BaseComponent<T extends HTMLElement = HTMLElement> {
|
|
|
34
36
|
constructor({
|
|
35
37
|
template = '',
|
|
36
38
|
mountTarget = '#app',
|
|
37
|
-
tagName = 'div'
|
|
39
|
+
tagName = 'div',
|
|
40
|
+
trustLevel
|
|
38
41
|
}: BaseComponentOptions) {
|
|
39
|
-
this.element = DomHelper.createElement<T>(tagName, template);
|
|
42
|
+
this.element = DomHelper.createElement<T>(tagName, template, trustLevel);
|
|
40
43
|
this.parseDataAttributes();
|
|
41
44
|
this.autoBindEvents();
|
|
42
45
|
DomHelper.mountElement(this.element, mountTarget);
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
I18nHelper.parseI18nAttributes(this.element);
|
|
48
|
-
{{/if}}
|
|
49
|
-
PipeHelper.parsePipeAttributes(this.element);
|
|
48
|
+
public childConfigs(): ComponentConfig[] {
|
|
49
|
+
return [];
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
EventHelper.
|
|
52
|
+
public destroy(): void {
|
|
53
|
+
EventHelper.destroyEvents(this.element, this.eventListeners);
|
|
54
|
+
ChildrenHelper.destroyChildren(this._children);
|
|
55
|
+
this.element.remove();
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
public render(): HTMLElement {
|
|
59
|
+
this.addChildren();
|
|
60
|
+
return this.element;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
/**
|
|
@@ -72,18 +75,18 @@ interface CatalogConfig {
|
|
|
72
75
|
return CatalogHelper.generateCatalog(config);
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
protected parseDataAttributes(): void {
|
|
79
|
+
{{#if i18n}}
|
|
80
|
+
I18nHelper.parseI18nAttributes(this.element);
|
|
81
|
+
{{/if}}
|
|
82
|
+
PipeHelper.parsePipeAttributes(this.element);
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
EventHelper.
|
|
81
|
-
ChildrenHelper.destroyChildren(this._children);
|
|
82
|
-
this.element.remove();
|
|
85
|
+
protected autoBindEvents(): void {
|
|
86
|
+
EventHelper.bindEvents(this, this.element, this.eventListeners);
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return this.element;
|
|
89
|
+
protected addChildren(): void {
|
|
90
|
+
ChildrenHelper.addChildren(this, this.element, this._children);
|
|
88
91
|
}
|
|
89
92
|
}
|
|
@@ -53,14 +53,14 @@ export class CatalogHelper {
|
|
|
53
53
|
return componentConfigs;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
private static createElements(index: number, config: CatalogConfig, container: HTMLElement) {
|
|
56
|
+
private static createElements(index: number, config: CatalogConfig, container: HTMLElement): void {
|
|
57
57
|
const el = document.createElement(config.elementTag || 'div');
|
|
58
58
|
el.setAttribute('data-component', `${config.elementName}-${index}`);
|
|
59
59
|
el.dataset.index = String(index);
|
|
60
60
|
container?.appendChild(el);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
private static pushConfig(index: number, componentConfigs: ComponentConfig[], config: CatalogConfig) {
|
|
63
|
+
private static pushConfig(index: number, componentConfigs: ComponentConfig[], config: CatalogConfig): void {
|
|
64
64
|
const item = config.array[index];
|
|
65
65
|
|
|
66
66
|
const configWithIndex: CatalogItemConfig = (item && typeof item === 'object')
|
|
@@ -18,11 +18,11 @@ export class ChildrenHelper {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
private static registerChild(child: BaseComponent, children: BaseComponent[]): void {
|
|
22
|
-
children.push(child);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
21
|
public static destroyChildren(children: BaseComponent[]): void {
|
|
26
22
|
children.forEach(child => child.destroy());
|
|
27
23
|
}
|
|
24
|
+
|
|
25
|
+
private static registerChild(child: BaseComponent, children: BaseComponent[]): void {
|
|
26
|
+
children.push(child);
|
|
27
|
+
}
|
|
28
28
|
}
|
|
@@ -1,28 +1,41 @@
|
|
|
1
|
+
import { SecurityHelper, TrustLevel } from "./security.helper.js";
|
|
2
|
+
|
|
3
|
+
interface DomElementConfig {
|
|
4
|
+
target: Element | null;
|
|
5
|
+
mountTarget: string | HTMLElement;
|
|
6
|
+
element: HTMLElement
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
export class DomHelper {
|
|
2
10
|
public static mountElement(element: HTMLElement, mountTarget: string | HTMLElement): void {
|
|
3
11
|
const target = typeof mountTarget === 'string'
|
|
4
12
|
? document.querySelector(mountTarget)
|
|
5
13
|
: mountTarget;
|
|
6
14
|
|
|
7
|
-
|
|
8
|
-
if (typeof mountTarget === 'string') {
|
|
9
|
-
// Append mode
|
|
10
|
-
target.appendChild(element);
|
|
11
|
-
} else {
|
|
12
|
-
// Replace placeholder mode
|
|
13
|
-
target.replaceWith(element);
|
|
14
|
-
}
|
|
15
|
-
} else {
|
|
16
|
-
console.warn('Invalid mount target for component:', mountTarget);
|
|
17
|
-
}
|
|
15
|
+
this.appendOrReplace({ target, element, mountTarget });
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
public static createElement<T extends HTMLElement>(
|
|
21
19
|
tagName: keyof HTMLElementTagNameMap,
|
|
22
|
-
template: string = ''
|
|
20
|
+
template: string = '',
|
|
21
|
+
trustLevel?: TrustLevel
|
|
23
22
|
): T {
|
|
24
23
|
const element = document.createElement(tagName) as T;
|
|
25
|
-
element.innerHTML = template;
|
|
24
|
+
element.innerHTML = SecurityHelper.sanitizeTemplate(template, trustLevel);
|
|
26
25
|
return element;
|
|
27
26
|
}
|
|
27
|
+
|
|
28
|
+
private static appendOrReplace(config: DomElementConfig) {
|
|
29
|
+
if (config.target instanceof HTMLElement) {
|
|
30
|
+
if (typeof config.mountTarget === 'string') {
|
|
31
|
+
// Append mode
|
|
32
|
+
config.target.appendChild(config.element);
|
|
33
|
+
} else {
|
|
34
|
+
// Replace placeholder mode
|
|
35
|
+
config.target.replaceWith(config.element);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
console.warn('Mount target not found:', config.mountTarget);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
28
41
|
}
|
|
@@ -23,6 +23,15 @@ export class EventHelper {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
public static destroyEvents(
|
|
27
|
+
element: HTMLElement,
|
|
28
|
+
eventListeners: Array<[string, EventListener]>
|
|
29
|
+
): void {
|
|
30
|
+
eventListeners.forEach(([event, listener]) => {
|
|
31
|
+
element.removeEventListener(event, listener);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
private static createBoundHandler(
|
|
27
36
|
el: Element,
|
|
28
37
|
component: BaseComponent,
|
|
@@ -45,13 +54,4 @@ export class EventHelper {
|
|
|
45
54
|
target.addEventListener(event, listener);
|
|
46
55
|
eventListeners.push([event, listener]);
|
|
47
56
|
}
|
|
48
|
-
|
|
49
|
-
public static destroyEvents(
|
|
50
|
-
element: HTMLElement,
|
|
51
|
-
eventListeners: Array<[string, EventListener]>
|
|
52
|
-
): void {
|
|
53
|
-
eventListeners.forEach(([event, listener]) => {
|
|
54
|
-
element.removeEventListener(event, listener);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
57
|
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
+
export type TrustLevel = 'strict' | 'normal' | 'trusted';
|
|
2
|
+
|
|
1
3
|
export class SecurityHelper {
|
|
2
|
-
public static
|
|
4
|
+
public static sanitizeTemplate(value: unknown, trustLevel: TrustLevel = 'normal'): string {
|
|
3
5
|
if (value === null || value === undefined) return '';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
.replace(/</g, "<")
|
|
8
|
-
.replace(/>/g, ">")
|
|
9
|
-
.replace(/"/g, """)
|
|
10
|
-
.replace(/'/g, "'");
|
|
6
|
+
let template = String(value);
|
|
7
|
+
|
|
8
|
+
return this.sanitizeInputTemplate(template, trustLevel);
|
|
11
9
|
}
|
|
12
10
|
|
|
13
11
|
public static sanitizeInputElement(
|
|
@@ -21,4 +19,38 @@ export class SecurityHelper {
|
|
|
21
19
|
}
|
|
22
20
|
return '';
|
|
23
21
|
}
|
|
22
|
+
|
|
23
|
+
public static escapeHtml(value: unknown): string {
|
|
24
|
+
if (value === null || value === undefined) return '';
|
|
25
|
+
const str = String(value);
|
|
26
|
+
return str
|
|
27
|
+
.replace(/&/g, "&")
|
|
28
|
+
.replace(/</g, "<")
|
|
29
|
+
.replace(/>/g, ">")
|
|
30
|
+
.replace(/"/g, """)
|
|
31
|
+
.replace(/'/g, "'");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private static sanitizeInputTemplate(template: string, trustLevel: TrustLevel): string {
|
|
35
|
+
// Trusted: no tamplate sanitization
|
|
36
|
+
if (trustLevel === 'trusted') {
|
|
37
|
+
return template;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Always remove scripts and inline event handlers
|
|
41
|
+
template = template
|
|
42
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
43
|
+
.replace(/\son\w+\s*=\s*["'][^"']*["']/gi, '');
|
|
44
|
+
|
|
45
|
+
// Strict: remove iframe, object, embed, href, data: protocol
|
|
46
|
+
if (trustLevel === 'strict') {
|
|
47
|
+
template = template
|
|
48
|
+
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
|
|
49
|
+
.replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, '')
|
|
50
|
+
.replace(/<embed\b[^>]*>/gi, '')
|
|
51
|
+
.replace(/href\s*=\s*["']javascript:[^"']*["']/gi, '')
|
|
52
|
+
.replace(/src\s*=\s*["']data:[^"']*["']/gi, '');
|
|
53
|
+
}
|
|
54
|
+
return template;
|
|
55
|
+
}
|
|
24
56
|
}
|
|
@@ -47,6 +47,22 @@ export class TokenHelper {
|
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** Register a new exact token like "@foo" */
|
|
51
|
+
public static registerCustomToken(
|
|
52
|
+
name: string,
|
|
53
|
+
resolver: (el: HTMLElement, ev: Event) => any
|
|
54
|
+
): void {
|
|
55
|
+
this.customResolvers[name] = resolver;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Register a new prefixed token like "@style:" */
|
|
59
|
+
public static registerPrefixedToken(
|
|
60
|
+
prefix: string,
|
|
61
|
+
resolver: (suffix: string, el: HTMLElement) => any
|
|
62
|
+
): void {
|
|
63
|
+
PREFIXED_TOKEN_RESOLVERS[prefix] = resolver;
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
private static resolveExact(token: string, el: HTMLElement, ev: Event): any | null {
|
|
51
67
|
const resolver = EXACT_TOKEN_RESOLVERS[token];
|
|
52
68
|
return resolver ? resolver(el, ev) : null;
|
|
@@ -80,20 +96,4 @@ export class TokenHelper {
|
|
|
80
96
|
}
|
|
81
97
|
return null;
|
|
82
98
|
}
|
|
83
|
-
|
|
84
|
-
/** Register a new exact token like "@foo" */
|
|
85
|
-
public static registerCustomToken(
|
|
86
|
-
name: string,
|
|
87
|
-
resolver: (el: HTMLElement, ev: Event) => any
|
|
88
|
-
) {
|
|
89
|
-
this.customResolvers[name] = resolver;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Register a new prefixed token like "@style:" */
|
|
93
|
-
public static registerPrefixedToken(
|
|
94
|
-
prefix: string,
|
|
95
|
-
resolver: (suffix: string, el: HTMLElement) => any
|
|
96
|
-
) {
|
|
97
|
-
PREFIXED_TOKEN_RESOLVERS[prefix] = resolver;
|
|
98
|
-
}
|
|
99
99
|
}
|
|
@@ -76,6 +76,18 @@ export abstract class Component<T extends HTMLElement = HTMLElement, K = any> ex
|
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
public getValues(): Record<string, string> {
|
|
80
|
+
return DataBindingHelper.getDataBindingValues(this.element);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public override render(): HTMLElement {
|
|
84
|
+
this.applyProps();
|
|
85
|
+
this.appendDynamicButtons();
|
|
86
|
+
this.autoBindEvents();
|
|
87
|
+
this.parseDataAttributes();
|
|
88
|
+
return super.render();
|
|
89
|
+
}
|
|
90
|
+
|
|
79
91
|
private applyProps(): void {
|
|
80
92
|
if (this.props.className) {
|
|
81
93
|
this.element.classList.add(this.props.className);
|
|
@@ -95,16 +107,4 @@ export abstract class Component<T extends HTMLElement = HTMLElement, K = any> ex
|
|
|
95
107
|
private applyDataBindings(): void {
|
|
96
108
|
DataBindingHelper.applyDataBindings(this.element, this.props);
|
|
97
109
|
}
|
|
98
|
-
|
|
99
|
-
public getValues(): Record<string, string> {
|
|
100
|
-
return DataBindingHelper.getDataBindingValues(this.element);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
public override render(): HTMLElement {
|
|
104
|
-
this.applyProps();
|
|
105
|
-
this.appendDynamicButtons();
|
|
106
|
-
this.autoBindEvents();
|
|
107
|
-
this.parseDataAttributes();
|
|
108
|
-
return super.render();
|
|
109
|
-
}
|
|
110
110
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export class ConfigHelper {
|
|
2
|
-
static normalize<T extends Record<string, any>>(
|
|
2
|
+
public static normalize<T extends Record<string, any>>(
|
|
3
3
|
config: T,
|
|
4
4
|
defaults: Partial<T>
|
|
5
5
|
): T {
|
|
@@ -9,7 +9,7 @@ export class ConfigHelper {
|
|
|
9
9
|
} as T;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
static normalizeStrings<T extends Record<string, any>>(
|
|
12
|
+
public static normalizeStrings<T extends Record<string, any>>(
|
|
13
13
|
config: T,
|
|
14
14
|
stringKeys: (keyof T)[]
|
|
15
15
|
): T {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ComponentProps } from "../component.js";
|
|
2
2
|
|
|
3
3
|
export class DataBindingHelper {
|
|
4
|
-
static applyDataBindings(element: HTMLElement, props: ComponentProps): void {
|
|
4
|
+
public static applyDataBindings(element: HTMLElement, props: ComponentProps): void {
|
|
5
5
|
const bindEls = element.querySelectorAll('[data-bind]');
|
|
6
6
|
bindEls.forEach(el => {
|
|
7
7
|
const key = el.getAttribute('data-bind');
|
|
@@ -18,7 +18,7 @@ export class DataBindingHelper {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
static getDataBindingValues(element: HTMLElement): Record<string, string> {
|
|
21
|
+
public static getDataBindingValues(element: HTMLElement): Record<string, string> {
|
|
22
22
|
const values: Record<string, string> = {};
|
|
23
23
|
element.querySelectorAll('[data-bind]').forEach(el => {
|
|
24
24
|
const key = el.getAttribute('data-bind');
|
|
@@ -15,16 +15,10 @@ export abstract class Service<T extends Service<T>> {
|
|
|
15
15
|
`${constructor.name} is a Service. Use ${constructor.name}.getInstance() instead.`
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
|
+
this.autoBindMethods();
|
|
18
19
|
window.addEventListener('beforeunload', this.dispose);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
/**
|
|
22
|
-
* For async and custom cleanup operations
|
|
23
|
-
*/
|
|
24
|
-
protected onDestroy<T extends Service<T>>(this: () => T): void {
|
|
25
|
-
console.log(`No onDestroy operations.`)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
22
|
public static createInstance<T extends Service<T>>(
|
|
29
23
|
constructor: new (...args: any[]) => T,
|
|
30
24
|
...args: any[]
|
|
@@ -62,11 +56,7 @@ export abstract class Service<T extends Service<T>> {
|
|
|
62
56
|
}
|
|
63
57
|
}
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
this._cleanupCallbacks.push(callback);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
public dispose = () => {
|
|
59
|
+
public dispose = (): void => {
|
|
70
60
|
this._cleanupCallbacks.forEach(fn => fn());
|
|
71
61
|
this._cleanupCallbacks = [];
|
|
72
62
|
Service._instances.delete(this.constructor);
|
|
@@ -81,6 +71,39 @@ export abstract class Service<T extends Service<T>> {
|
|
|
81
71
|
Service._instantiating.clear();
|
|
82
72
|
}
|
|
83
73
|
|
|
74
|
+
protected registerCleanup(callback: () => void): void {
|
|
75
|
+
this._cleanupCallbacks.push(callback);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* For async and custom cleanup operations
|
|
80
|
+
*/
|
|
81
|
+
protected onDestroy<T extends Service<T>>(this: () => T): void {
|
|
82
|
+
console.log(`No onDestroy operations.`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private autoBindMethods(): void {
|
|
86
|
+
let proto = Object.getPrototypeOf(this);
|
|
87
|
+
|
|
88
|
+
while (proto && proto !== Service.prototype) {
|
|
89
|
+
const propertyNames = Object.getOwnPropertyNames(proto);
|
|
90
|
+
|
|
91
|
+
for (const key of propertyNames) {
|
|
92
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
|
|
93
|
+
|
|
94
|
+
if (
|
|
95
|
+
key !== 'constructor' &&
|
|
96
|
+
descriptor &&
|
|
97
|
+
typeof descriptor.value === 'function'
|
|
98
|
+
) {
|
|
99
|
+
(this as any)[key] = descriptor.value.bind(this);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
proto = Object.getPrototypeOf(proto);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
84
107
|
/**
|
|
85
108
|
* Resets the Service instance without cleanup.
|
|
86
109
|
*/
|
|
@@ -18,12 +18,6 @@ export abstract class View<T extends HTMLElement = HTMLElement> extends BaseComp
|
|
|
18
18
|
this.viewName = viewName || this.getKebabCaseViewName();
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
private getKebabCaseViewName(): string {
|
|
22
|
-
const className = this.constructor.name;
|
|
23
|
-
const baseName = className.replace(/View$/, '');
|
|
24
|
-
return baseName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
21
|
/**
|
|
28
22
|
* Set route parameters for this view instance
|
|
29
23
|
* Called by the router when the view is instantiated
|
|
@@ -74,4 +68,10 @@ export abstract class View<T extends HTMLElement = HTMLElement> extends BaseComp
|
|
|
74
68
|
super.destroy();
|
|
75
69
|
this.onExit();
|
|
76
70
|
}
|
|
71
|
+
|
|
72
|
+
private getKebabCaseViewName(): string {
|
|
73
|
+
const className = this.constructor.name;
|
|
74
|
+
const baseName = className.replace(/View$/, '');
|
|
75
|
+
return baseName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
76
|
+
}
|
|
77
77
|
}
|
|
@@ -34,18 +34,6 @@ export class EventBus extends Service<EventBus> {
|
|
|
34
34
|
this.addHandler(event, callback, true);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
private addHandler<K extends EventKey>(
|
|
38
|
-
event: K,
|
|
39
|
-
callback: (data: EventMap[K]) => void,
|
|
40
|
-
once: boolean
|
|
41
|
-
) {
|
|
42
|
-
if (!this.handlers[event]) {
|
|
43
|
-
this.handlers[event] = [];
|
|
44
|
-
}
|
|
45
|
-
this.handlers[event]!.push(callback);
|
|
46
|
-
this._subscriptions.push({ event, callback, once });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
37
|
public emit<K extends EventKey>(event: K, data?: EventMap[K]): void {
|
|
50
38
|
const callbacks = this.handlers[event];
|
|
51
39
|
if (!callbacks) return;
|
|
@@ -80,7 +68,19 @@ export class EventBus extends Service<EventBus> {
|
|
|
80
68
|
);
|
|
81
69
|
}
|
|
82
70
|
|
|
83
|
-
private
|
|
71
|
+
private addHandler<K extends EventKey>(
|
|
72
|
+
event: K,
|
|
73
|
+
callback: (data: EventMap[K]) => void,
|
|
74
|
+
once: boolean
|
|
75
|
+
): void {
|
|
76
|
+
if (!this.handlers[event]) {
|
|
77
|
+
this.handlers[event] = [];
|
|
78
|
+
}
|
|
79
|
+
this.handlers[event]!.push(callback);
|
|
80
|
+
this._subscriptions.push({ event, callback, once });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private cleanupEventListeners = (): void => {
|
|
84
84
|
this._subscriptions.forEach(({ event, callback }) => {
|
|
85
85
|
this.off(event, callback);
|
|
86
86
|
});
|