@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.
Files changed (80) hide show
  1. package/CHANGELOG.md +70 -1
  2. package/README.md +25 -18
  3. package/lib/index.mjs +23 -20
  4. package/lib/src/context-builder.mjs +40 -0
  5. package/lib/src/file-generator.mjs +26 -126
  6. package/lib/src/git-manager.mjs +1 -1
  7. package/lib/src/json-generator.mjs +31 -0
  8. package/lib/src/package-manager.mjs +21 -18
  9. package/lib/src/print.mjs +1 -0
  10. package/lib/src/project-generator.mjs +53 -0
  11. package/lib/src/prompts.mjs +16 -75
  12. package/lib/src/template-compiler.mjs +1 -1
  13. package/lib/src/utils.mjs +9 -9
  14. package/lib/src/version.mjs +1 -0
  15. package/package.json +1 -1
  16. package/templates/base/.gitignore.hbs +1 -0
  17. package/templates/base/builder.config.js.hbs +8 -0
  18. package/templates/base/src/app/globals.d.ts.hbs +12 -8
  19. package/templates/base/src/app/main.ts.hbs +2 -1
  20. package/templates/base/src/app/views/home/home.view.test.js.hbs +26 -0
  21. package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
  22. package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +1 -1
  23. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +46 -26
  24. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +36 -22
  25. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +4 -4
  26. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +60 -13
  27. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +16 -17
  28. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +40 -8
  29. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +16 -16
  30. package/templates/base/src/core/base-classes/component/component.ts.hbs +20 -24
  31. package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +19 -2
  32. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +2 -2
  33. package/templates/base/src/core/base-classes/service/service.ts.hbs +35 -12
  34. package/templates/base/src/core/base-classes/view/view.ts.hbs +6 -6
  35. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +13 -13
  36. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +18 -18
  37. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +24 -24
  38. package/templates/base/src/core/services/http-client/http-client.ts.hbs +19 -19
  39. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +6 -6
  40. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +15 -14
  41. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +22 -22
  42. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +4 -36
  43. package/templates/base/src/core/services/router/router.ts.hbs +45 -46
  44. package/templates/base/src/core/services/store/store.ts.hbs +6 -6
  45. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +41 -15
  46. package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +40 -19
  47. package/templates/base/src/core/utils/helpers/attributes.helper.ts.hbs +11 -5
  48. package/templates/base/src/libs/pipes/pipes.ts.hbs +1 -1
  49. package/templates/base/src/libs/popover/helpers/focus-trap.helper.ts.hbs +197 -0
  50. package/templates/base/src/libs/popover/helpers/popover-dom.helper.ts.hbs +75 -0
  51. package/templates/base/src/libs/popover/popover.ts.hbs +89 -96
  52. package/templates/base/src/libs/snackbar/snackbar.ts.hbs +7 -4
  53. package/templates/base/src/styles/base/_variables.scss.hbs +1 -0
  54. package/templates/base/src/styles/core/_anchors.scss.hbs +10 -0
  55. package/templates/base/src/styles/core/_index.scss.hbs +6 -0
  56. package/templates/base/src/styles/core/_popover.scss.hbs +96 -0
  57. package/templates/base/src/styles/core/_snackbar.scss.hbs +50 -0
  58. package/templates/base/src/styles/main.scss.hbs +1 -1
  59. package/templates/base/tools/builder/builder.js.hbs +9 -3
  60. package/templates/base/tools/builder/core/add-tags.js.hbs +46 -0
  61. package/templates/base/tools/builder/core/copy-static.js.hbs +35 -35
  62. package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -6
  63. package/templates/base/tools/builder/core/hash-files.js.hbs +52 -0
  64. package/templates/base/tools/builder/core/validate-html.js.hbs +12 -32
  65. package/templates/base/tools/utils/get-files-recursive.js.hbs +17 -8
  66. package/templates/features/deployHelper/tools/builder/core/compress-files.js.hbs +56 -0
  67. package/templates/{base → features/deployHelper}/tools/deployment/Dockerfile.hbs +12 -4
  68. package/templates/{base → features/deployHelper}/tools/deployment/nginx.conf.hbs +30 -2
  69. package/templates/features/i18n/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +4 -6
  70. package/templates/features/i18n/src/core/services/i18n/i18n.ts.hbs +76 -52
  71. package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +31 -0
  72. package/templates/features/template/src/app/views/home/home.view.html.hbs +1 -1
  73. package/templates/features/template/tools/builder/core/merge-templates.js.hbs +2 -2
  74. package/templates/features/testinNutin/testin-nutin/core/src/utils/test-queue.js.hbs +2 -2
  75. package/templates/features/testinNutin/testin-nutin/runner.js.hbs +1 -0
  76. package/templates/features/testinNutin/testin-nutin.config.js.hbs +2 -2
  77. package/templates/base/src/styles/core/_libs.scss.hbs +0 -132
  78. package/templates/base/tools/deployment/gzip.conf.hbs +0 -19
  79. package/templates/features/i18n/src/app/views/home/locales/fr.json.hbs +0 -9
  80. /package/templates/{base → features/deployHelper}/.dockerignore.hbs +0 -0
@@ -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 nameAnswer = await inquirer.prompt([
8
+ const nameInput = await inquirer.prompt([
7
9
  {
8
10
  type: 'input',
9
11
  name: 'projectName',
10
12
  message: 'Project name:',
11
- default: 'my-nutin-app',
12
- validate: validateProjectName
13
- }
13
+ default: defaults.projectName,
14
+ validate: validateProjectName,
15
+ },
14
16
  ]);
15
- projectName = nameAnswer.projectName;
17
+ projectName = nameInput.projectName;
16
18
  }
17
19
 
18
20
  let packageManager = cliOptions.packageManager;
19
21
  if (!packageManager) {
20
- const pmAnswer = await inquirer.prompt([
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: 'npm'
27
- }
28
+ default: defaults.packageManager,
29
+ },
28
30
  ]);
29
- packageManager = pmAnswer.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
- template: cliOptions.template ?? answers.template,
97
- stylinNutin: cliOptions.stylinNutin ?? answers.stylinNutin,
98
- i18n: cliOptions.i18n ?? answers.i18n,
99
- transition: cliOptions.transition ?? answers.transition,
100
- testinNutin: cliOptions.testinNutin ?? answers.testinNutin
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.testing
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.info('\nNext steps:');
12
- print.info(` cd ${projectName} && npm run serve`);
13
- print.boldInfo(`\nDocumentation on https://www.nutin.org`);
14
-
15
- print.success('\nHappy coding! 🚀\n');
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
- export const packageVersion = '1.2.1';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nutin/cli",
3
- "version": "1.2.3",
3
+ "version": "1.3.1",
4
4
  "description": "Build SPAs by understanding SPAs. A minimal frontend toolkit crafted for learning and lean applications.",
5
5
  "preferGlobal": true,
6
6
  "main": "lib/index.mjs",
@@ -6,6 +6,7 @@ pnpm-debug.log*
6
6
  .yarn-integrity
7
7
 
8
8
  /dist/
9
+ /dist-build/
9
10
 
10
11
  .DS_Store
11
12
  .DS_Store?
@@ -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
- 'navigate': string;
3
- 'reload': string;
4
- 'view-mount': string;
5
- 'view-render': string;
6
- 'view-unmount': string;
7
- 'track-pageview': { page: string };
8
-
9
- // Add other events and their payload types here
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:2rem;"
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}}styles: btnStyles{{/if}} })
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
- export { CatalogItemConfig } from './helpers/catalog.helper.js';
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) => BaseComponent;
16
+ * factory: (element: HTMLElement) => Component;
15
17
  * }
16
18
  * ```
17
19
  */
18
20
  export interface ComponentConfig {
19
21
  selector: string;
20
- factory: (element: HTMLElement) => BaseComponent;
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
- protected parseDataAttributes(): void {
46
- {{#if i18n}}
47
- I18nHelper.parseI18nAttributes(this.element);
48
- {{/if}}
49
- PipeHelper.parsePipeAttributes(this.element);
51
+ public childConfigs(): ComponentConfig[] {
52
+ return [];
50
53
  }
51
54
 
52
- protected autoBindEvents(): void {
53
- EventHelper.bindEvents(this, this.element, this.eventListeners);
55
+ public destroy(): void {
56
+ EventHelper.destroyEvents(this.eventListeners);
57
+ ChildrenHelper.destroyChildren(this._children);
58
+ this.element.remove();
54
59
  }
55
60
 
56
- protected addChildren(): void {
57
- ChildrenHelper.addChildren(this, this.element, this._children);
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
- public childConfigs(): ComponentConfig[] {
76
- return [];
82
+ protected parseDataAttributes(): void {
83
+ {{#if i18n}}
84
+ I18nHelper.parseI18nAttributes(this.element);
85
+ {{/if}}
86
+ PipeHelper.parsePipeAttributes(this.element);
77
87
  }
78
88
 
79
- public destroy(): void {
80
- EventHelper.destroyEvents(this.element, this.eventListeners);
81
- ChildrenHelper.destroyChildren(this._children);
82
- this.element.remove();
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
- public render(): HTMLElement {
86
- this.addChildren();
87
- return this.element;
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 { BaseComponent, ComponentConfig } from "../base-component.js";
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) => BaseComponent;
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) => BaseComponent;
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 container = document.querySelector(`[data-catalog="${config.selector}"]`);
42
+ const componentConfigs: ComponentConfig[] = [];
43
+ const containers = scope.querySelectorAll(`[data-catalog="${config.selector}"]`);
43
44
 
44
- if (!container || !(container instanceof HTMLElement)) return [];
45
+ containers.forEach((container) => {
46
+ if (!container || !(container instanceof HTMLElement) || (container.firstElementChild)) return;
47
+ componentConfigs.push(...this.getComponentConfigArray(config, container))
48
+ })
45
49
 
46
- const componentConfigs: ComponentConfig[] = [];
50
+ return componentConfigs;
51
+ }
47
52
 
48
- for (let i = 0; i < config.array.length; i++) {
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 el = document.createElement(config.elementTag || 'div');
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
- el.dataset.index = String(index);
60
- container?.appendChild(el);
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 item = config.array[index];
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
- if (target instanceof HTMLElement) {
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
  }