@nutin/cli 1.2.3 → 1.3.1
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 +70 -1
- package/README.md +25 -18
- package/lib/index.mjs +23 -20
- package/lib/src/context-builder.mjs +40 -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/print.mjs +1 -0
- package/lib/src/project-generator.mjs +53 -0
- package/lib/src/prompts.mjs +16 -75
- 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 +8 -0
- package/templates/base/src/app/globals.d.ts.hbs +12 -8
- 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 +46 -26
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +36 -22
- 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 +60 -13
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +16 -17
- 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 +20 -24
- package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +19 -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 +15 -14
- 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 +4 -36
- package/templates/base/src/core/services/router/router.ts.hbs +45 -46
- 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 +41 -15
- package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +40 -19
- 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/helpers/focus-trap.helper.ts.hbs +197 -0
- package/templates/base/src/libs/popover/helpers/popover-dom.helper.ts.hbs +75 -0
- package/templates/base/src/libs/popover/popover.ts.hbs +89 -96
- package/templates/base/src/libs/snackbar/snackbar.ts.hbs +7 -4
- package/templates/base/src/styles/base/_variables.scss.hbs +1 -0
- package/templates/base/src/styles/core/_anchors.scss.hbs +10 -0
- package/templates/base/src/styles/core/_index.scss.hbs +6 -0
- package/templates/base/src/styles/core/_popover.scss.hbs +96 -0
- package/templates/base/src/styles/core/_snackbar.scss.hbs +50 -0
- package/templates/base/src/styles/main.scss.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 +76 -52
- package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +31 -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/src/styles/core/_libs.scss.hbs +0 -132
- 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/prompts.mjs
CHANGED
|
@@ -1,103 +1,44 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
|
+
import { defaults } from './context-builder.mjs';
|
|
2
3
|
|
|
3
4
|
export async function promptUser(initialName, cliOptions = {}) {
|
|
5
|
+
|
|
4
6
|
let projectName = initialName;
|
|
5
7
|
if (!projectName) {
|
|
6
|
-
const
|
|
8
|
+
const nameInput = await inquirer.prompt([
|
|
7
9
|
{
|
|
8
10
|
type: 'input',
|
|
9
11
|
name: 'projectName',
|
|
10
12
|
message: 'Project name:',
|
|
11
|
-
default:
|
|
12
|
-
validate: validateProjectName
|
|
13
|
-
}
|
|
13
|
+
default: defaults.projectName,
|
|
14
|
+
validate: validateProjectName,
|
|
15
|
+
},
|
|
14
16
|
]);
|
|
15
|
-
projectName =
|
|
17
|
+
projectName = nameInput.projectName;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
let packageManager = cliOptions.packageManager;
|
|
19
21
|
if (!packageManager) {
|
|
20
|
-
const
|
|
22
|
+
const pmPreference = await inquirer.prompt([
|
|
21
23
|
{
|
|
22
24
|
type: 'list',
|
|
23
25
|
name: 'packageManager',
|
|
24
26
|
message: 'Package manager:',
|
|
25
27
|
choices: ['npm', 'yarn', 'pnpm', 'bun'],
|
|
26
|
-
default:
|
|
27
|
-
}
|
|
28
|
+
default: defaults.packageManager,
|
|
29
|
+
},
|
|
28
30
|
]);
|
|
29
|
-
packageManager =
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (cliOptions.defaults) {
|
|
33
|
-
return {
|
|
34
|
-
projectName,
|
|
35
|
-
packageManager,
|
|
36
|
-
stylinNutin: cliOptions.stylinNutin || true,
|
|
37
|
-
template: cliOptions.template || true,
|
|
38
|
-
i18n: cliOptions.i18n || true,
|
|
39
|
-
testinNutin: cliOptions.testinNutin || false,
|
|
40
|
-
transition: cliOptions.transitions || false
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const questions = [];
|
|
45
|
-
|
|
46
|
-
if (cliOptions.stylinNutin === undefined) {
|
|
47
|
-
questions.push({
|
|
48
|
-
type: 'confirm',
|
|
49
|
-
name: 'stylinNutin',
|
|
50
|
-
message: 'Use built-in SCSS utility classes ?',
|
|
51
|
-
default: true
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (cliOptions.template === undefined) {
|
|
56
|
-
questions.push({
|
|
57
|
-
type: 'confirm',
|
|
58
|
-
name: 'template',
|
|
59
|
-
message: 'Use external templates ?',
|
|
60
|
-
default: true
|
|
61
|
-
});
|
|
31
|
+
packageManager = pmPreference.packageManager;
|
|
62
32
|
}
|
|
63
33
|
|
|
64
|
-
if (cliOptions.i18n === undefined) {
|
|
65
|
-
questions.push({
|
|
66
|
-
type: 'confirm',
|
|
67
|
-
name: 'i18n',
|
|
68
|
-
message: 'Use i18n with json-based content ?',
|
|
69
|
-
default: true
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (cliOptions.transition === undefined) {
|
|
74
|
-
questions.push({
|
|
75
|
-
type: 'confirm',
|
|
76
|
-
name: 'transition',
|
|
77
|
-
message: 'Use animated view transitions?',
|
|
78
|
-
default: false
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (cliOptions.testinNutin === undefined) {
|
|
83
|
-
questions.push({
|
|
84
|
-
type: 'confirm',
|
|
85
|
-
name: 'testinNutin',
|
|
86
|
-
message: 'Use testin-nutin toolkit?',
|
|
87
|
-
default: true
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
|
|
92
|
-
|
|
93
34
|
return {
|
|
94
35
|
projectName,
|
|
95
36
|
packageManager,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
37
|
+
preset: cliOptions.preset,
|
|
38
|
+
i18n: cliOptions.i18n,
|
|
39
|
+
deployHelper: cliOptions.deployHelper,
|
|
40
|
+
testinNutin: cliOptions.testinNutin,
|
|
41
|
+
transition: cliOptions.transition,
|
|
101
42
|
};
|
|
102
43
|
}
|
|
103
44
|
|
|
@@ -36,7 +36,7 @@ export class TemplateCompiler {
|
|
|
36
36
|
|
|
37
37
|
Handlebars.registerHelper('hasAnyFeature', function(options) {
|
|
38
38
|
const context = options.data.root;
|
|
39
|
-
return context.i18n || context.template || context.
|
|
39
|
+
return context.i18n || context.template || context.testinNutin || context.stylinNutin
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
|
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.1';
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ export default {
|
|
|
3
3
|
bundle: true,
|
|
4
4
|
minify: true,
|
|
5
5
|
sourcemap: false,
|
|
6
|
+
drop: ['console', 'debugger'],
|
|
6
7
|
platform: 'browser',
|
|
7
8
|
format: 'esm',
|
|
8
9
|
target: ['es2015'],
|
|
@@ -24,5 +25,12 @@ export default {
|
|
|
24
25
|
{{/if}}
|
|
25
26
|
], // in styles/
|
|
26
27
|
},
|
|
28
|
+
{{#if deployHelper}}
|
|
29
|
+
compression: {
|
|
30
|
+
gzip: true,
|
|
31
|
+
// BROTLI OPTIONAL
|
|
32
|
+
// brotli: false,
|
|
33
|
+
},
|
|
34
|
+
{{/if}}
|
|
27
35
|
verbose: false,
|
|
28
36
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
declare type CoreEventMap = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
'navigate': string;
|
|
3
|
+
'reload': string;
|
|
4
|
+
'view-mount': string;
|
|
5
|
+
'view-render': string;
|
|
6
|
+
'view-unmount': string;
|
|
7
|
+
'track-pageview': { page: string };
|
|
8
|
+
'popover-opened': string;
|
|
9
|
+
'popover-close': string;
|
|
10
|
+
{{#if i18n}}
|
|
11
|
+
'language-changed': string;
|
|
12
|
+
{{/if}}
|
|
13
|
+
// Add other events and their payload types here
|
|
10
14
|
};
|
|
11
15
|
|
|
12
16
|
declare type StoreEventMap = {[ K in `store:${string}` ]: any; };
|
|
@@ -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,60 +6,67 @@ 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
|
-
|
|
9
|
+
import { Component, ComponentOptions, AppEventBus } from '../../index.js';
|
|
10
|
+
import { SecurityHelper, TrustLevel } from './helpers/security.helper.js';
|
|
11
|
+
export { CatalogItemConfig, CatalogConfig, CatalogItemPrimitive } from './helpers/catalog.helper.js';
|
|
10
12
|
|
|
11
13
|
/**```typescript
|
|
12
14
|
* export interface ComponentConfig {
|
|
13
15
|
* selector: string;
|
|
14
|
-
* factory: (element: HTMLElement) =>
|
|
16
|
+
* factory: (element: HTMLElement) => Component;
|
|
15
17
|
* }
|
|
16
18
|
* ```
|
|
17
19
|
*/
|
|
18
20
|
export interface ComponentConfig {
|
|
19
21
|
selector: string;
|
|
20
|
-
factory: (element: HTMLElement) =>
|
|
22
|
+
factory: (element: HTMLElement) => Component;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export interface BaseComponentOptions {
|
|
24
26
|
template?: string;
|
|
25
27
|
mountTarget?: string | HTMLElement;
|
|
26
28
|
tagName?: keyof HTMLElementTagNameMap;
|
|
29
|
+
trustLevel?: TrustLevel;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export abstract class BaseComponent<T extends HTMLElement = HTMLElement> {
|
|
30
33
|
private _children: BaseComponent[] = [];
|
|
31
34
|
protected element: T;
|
|
32
|
-
protected eventListeners: Array<[string, EventListener]> = [];
|
|
35
|
+
protected eventListeners: Array<[EventTarget, string, EventListener]> = [];
|
|
36
|
+
private _options: ComponentOptions;
|
|
33
37
|
|
|
34
38
|
constructor({
|
|
35
39
|
template = '',
|
|
36
40
|
mountTarget = '#app',
|
|
37
|
-
tagName = 'div'
|
|
41
|
+
tagName = 'div',
|
|
42
|
+
trustLevel
|
|
38
43
|
}: BaseComponentOptions) {
|
|
39
|
-
this.element = DomHelper.createElement<T>(tagName, template);
|
|
44
|
+
this.element = DomHelper.createElement<T>(tagName, template, trustLevel);
|
|
45
|
+
this._options = { template, mountTarget, tagName, trustLevel };
|
|
40
46
|
this.parseDataAttributes();
|
|
41
47
|
this.autoBindEvents();
|
|
42
48
|
DomHelper.mountElement(this.element, mountTarget);
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
I18nHelper.parseI18nAttributes(this.element);
|
|
48
|
-
{{/if}}
|
|
49
|
-
PipeHelper.parsePipeAttributes(this.element);
|
|
51
|
+
public childConfigs(): ComponentConfig[] {
|
|
52
|
+
return [];
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
EventHelper.
|
|
55
|
+
public destroy(): void {
|
|
56
|
+
EventHelper.destroyEvents(this.eventListeners);
|
|
57
|
+
ChildrenHelper.destroyChildren(this._children);
|
|
58
|
+
this.element.remove();
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
public render(): HTMLElement {
|
|
62
|
+
this.addChildren();
|
|
63
|
+
DomHelper.cleanupOptionalContent();
|
|
64
|
+
return this.element;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/**
|
|
61
68
|
* ```typescript
|
|
62
|
-
interface CatalogConfig {
|
|
69
|
+
interface CatalogConfig extends ComponentOptions {
|
|
63
70
|
array: CatalogItemConfig[];
|
|
64
71
|
elementName: string;
|
|
65
72
|
elementTag?: keyof HTMLElementTagNameMap;
|
|
@@ -69,21 +76,34 @@ interface CatalogConfig {
|
|
|
69
76
|
```
|
|
70
77
|
*/
|
|
71
78
|
protected catalogConfig(config: CatalogConfig): ComponentConfig[] {
|
|
72
|
-
return CatalogHelper.generateCatalog(config);
|
|
79
|
+
return CatalogHelper.generateCatalog(config, this.element);
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
protected parseDataAttributes(): void {
|
|
83
|
+
{{#if i18n}}
|
|
84
|
+
I18nHelper.parseI18nAttributes(this.element);
|
|
85
|
+
{{/if}}
|
|
86
|
+
PipeHelper.parsePipeAttributes(this.element);
|
|
77
87
|
}
|
|
78
88
|
|
|
79
|
-
|
|
80
|
-
EventHelper.destroyEvents(this.
|
|
81
|
-
|
|
82
|
-
this.element.
|
|
89
|
+
protected autoBindEvents(): void {
|
|
90
|
+
EventHelper.destroyEvents(this.eventListeners);
|
|
91
|
+
this.eventListeners.length = 0;
|
|
92
|
+
EventHelper.bindEvents(this, this.element, this.eventListeners);
|
|
83
93
|
}
|
|
84
94
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
protected addChildren(): void {
|
|
96
|
+
ChildrenHelper.addChildren(this, this.element, this._children);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
protected forceRender(): void {
|
|
100
|
+
this.element.innerHTML = SecurityHelper.sanitizeTemplate(this._options.template, this._options.trustLevel);
|
|
101
|
+
this.render();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
protected listenToRenderEvents(events: EventKey[], force: boolean = false): void {
|
|
105
|
+
events.forEach((event: EventKey) => {
|
|
106
|
+
AppEventBus.subscribe(event, () => force ? this.forceRender() : this.render());
|
|
107
|
+
})
|
|
88
108
|
}
|
|
89
109
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Component, ComponentConfig, ComponentOptions } from "../../index.js";
|
|
2
2
|
|
|
3
|
-
export interface CatalogConfig {
|
|
3
|
+
export interface CatalogConfig extends ComponentOptions {
|
|
4
4
|
array: CatalogItemConfig[];
|
|
5
5
|
elementName: string;
|
|
6
6
|
elementTag?: keyof HTMLElementTagNameMap;
|
|
7
7
|
selector: string;
|
|
8
|
-
component: new (el: HTMLElement, data: any) =>
|
|
8
|
+
component: new (el: HTMLElement, data: any, props?: any) => Component;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export interface CatalogItemBase {
|
|
@@ -23,12 +23,12 @@ export type CatalogItemConfig<T = any> =
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* ```typescript
|
|
26
|
-
interface CatalogConfig {
|
|
26
|
+
interface CatalogConfig extends ComponentOptions {
|
|
27
27
|
array: CatalogItemConfig[];
|
|
28
28
|
elementName: string;
|
|
29
29
|
elementTag?: keyof HTMLElementTagNameMap;
|
|
30
30
|
selector: string;
|
|
31
|
-
component: new (el: HTMLElement, data: any) =>
|
|
31
|
+
component: new (el: HTMLElement, data: any) => Component;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
type CatalogItemConfig<T = any> =
|
|
@@ -36,42 +36,56 @@ type CatalogItemConfig<T = any> =
|
|
|
36
36
|
```
|
|
37
37
|
*/
|
|
38
38
|
export class CatalogHelper {
|
|
39
|
-
public static generateCatalog(config: CatalogConfig): ComponentConfig[] {
|
|
39
|
+
public static generateCatalog(config: CatalogConfig, scope: Element = document.documentElement): ComponentConfig[] {
|
|
40
40
|
if (!config.array || config.array.length < 1) return [];
|
|
41
41
|
|
|
42
|
-
const
|
|
42
|
+
const componentConfigs: ComponentConfig[] = [];
|
|
43
|
+
const containers = scope.querySelectorAll(`[data-catalog="${config.selector}"]`);
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
containers.forEach((container) => {
|
|
46
|
+
if (!container || !(container instanceof HTMLElement) || (container.firstElementChild)) return;
|
|
47
|
+
componentConfigs.push(...this.getComponentConfigArray(config, container))
|
|
48
|
+
})
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
return componentConfigs;
|
|
51
|
+
}
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
private static getComponentConfigArray(config: CatalogConfig, container: HTMLElement): ComponentConfig[] {
|
|
54
|
+
const componentConfigs: ComponentConfig[] = [];
|
|
55
|
+
for (let i = 0; i < config.array.length; i++) {
|
|
49
56
|
this.createElements(i, config, container);
|
|
50
57
|
this.pushConfig(i, componentConfigs, config);
|
|
51
58
|
}
|
|
52
|
-
|
|
53
59
|
return componentConfigs;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
private static createElements(index: number, config: CatalogConfig, container: HTMLElement) {
|
|
57
|
-
const
|
|
62
|
+
private static createElements(index: number, config: CatalogConfig, container: HTMLElement): void {
|
|
63
|
+
const wrapper = document.createElement(config.elementTag || 'div');
|
|
64
|
+
const el = document.createElement('div');
|
|
65
|
+
wrapper.appendChild(el);
|
|
58
66
|
el.setAttribute('data-component', `${config.elementName}-${index}`);
|
|
59
|
-
|
|
60
|
-
container?.appendChild(
|
|
67
|
+
wrapper.dataset.index = String(index);
|
|
68
|
+
container?.appendChild(wrapper);
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
private static pushConfig(index: number, componentConfigs: ComponentConfig[], config: CatalogConfig) {
|
|
64
|
-
const
|
|
71
|
+
private static pushConfig(index: number, componentConfigs: ComponentConfig[], config: CatalogConfig): void {
|
|
72
|
+
const configWithIndex = this.getConfigWithIndex(config, index)
|
|
73
|
+
|
|
74
|
+
const { props, defaults, normalizeKeys } = config;
|
|
75
|
+
const options = { ...props, ...defaults, ...normalizeKeys };
|
|
65
76
|
|
|
66
|
-
const configWithIndex: CatalogItemConfig = (item && typeof item === 'object')
|
|
67
|
-
? { ...(item as object), index: index }
|
|
68
|
-
: { value: item, index: index };
|
|
69
|
-
|
|
70
77
|
componentConfigs.push(
|
|
71
78
|
{
|
|
72
79
|
selector: `${config.elementName}-${index}`,
|
|
73
|
-
factory: (el) => new config.component(el, configWithIndex)
|
|
80
|
+
factory: (el) => new config.component(el, configWithIndex, options),
|
|
74
81
|
},
|
|
75
82
|
)
|
|
76
83
|
}
|
|
84
|
+
|
|
85
|
+
private static getConfigWithIndex(config: CatalogConfig, index: number): CatalogItemConfig {
|
|
86
|
+
const item = config.array[index];
|
|
87
|
+
return (item && typeof item === 'object')
|
|
88
|
+
? { ...(item as object), index: index }
|
|
89
|
+
: { value: item, index: index };
|
|
90
|
+
}
|
|
77
91
|
}
|
|
@@ -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,75 @@
|
|
|
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
|
+
public static cleanupOptionalContent(): void {
|
|
29
|
+
const isEmpty = (el: HTMLElement): boolean => {
|
|
30
|
+
if (el instanceof HTMLImageElement) {
|
|
31
|
+
return !el.src || el.src.trim() === "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
35
|
+
return !el.value?.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (el instanceof HTMLMediaElement || el instanceof HTMLSourceElement) {
|
|
39
|
+
return !el.getAttribute("src");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const content = el.textContent?.trim();
|
|
43
|
+
return !content || content === "undefined";
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const isValueUndefined = (el: HTMLElement): boolean => {
|
|
47
|
+
const value = el.getAttribute("data-optional");
|
|
48
|
+
if (!value) return false;
|
|
49
|
+
return (
|
|
50
|
+
value.trim() === '' ||
|
|
51
|
+
value === "undefined" ||
|
|
52
|
+
value === "null"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
document.querySelectorAll<HTMLElement>("[data-optional]").forEach(el => {
|
|
57
|
+
if (isValueUndefined(el) || isEmpty(el)) el.remove();
|
|
58
|
+
el.removeAttribute("data-optional");
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private static appendOrReplace(config: DomElementConfig) {
|
|
63
|
+
if (config.target instanceof HTMLElement) {
|
|
64
|
+
if (typeof config.mountTarget === 'string') {
|
|
65
|
+
// Append mode
|
|
66
|
+
config.target.appendChild(config.element);
|
|
67
|
+
} else {
|
|
68
|
+
// Replace placeholder mode
|
|
69
|
+
config.target.replaceWith(config.element);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
console.warn('Mount target not found:', config.mountTarget);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
28
75
|
}
|