@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
|
@@ -4,20 +4,13 @@ import { {{#if i18n}}I18nService, {{/if}}View } from "../../../index.js";
|
|
|
4
4
|
* Navigation - handles path normalization and history management
|
|
5
5
|
*/
|
|
6
6
|
export class NavigationManager {
|
|
7
|
-
static normalizePath(path: string): string {
|
|
8
|
-
const
|
|
7
|
+
public static normalizePath(path: string): string {
|
|
8
|
+
const collapsed = path.replace(/\/\/+/g, '/');
|
|
9
|
+
const url = new URL(collapsed, window.location.origin);
|
|
9
10
|
return (url.pathname || '/').replace(/\/+$/, '') || '/';
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
static
|
|
13
|
-
pushState: boolean,
|
|
14
|
-
currentPath: string,
|
|
15
|
-
normalizedPath: string
|
|
16
|
-
): boolean {
|
|
17
|
-
return pushState && currentPath !== normalizedPath;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
static updateHistory(
|
|
13
|
+
public static updateHistory(
|
|
21
14
|
normalizedPath: string,
|
|
22
15
|
currentPath: string,
|
|
23
16
|
pushState: boolean
|
|
@@ -27,7 +20,7 @@ export class NavigationManager {
|
|
|
27
20
|
}
|
|
28
21
|
}
|
|
29
22
|
|
|
30
|
-
static updateMetaContent(currentView: View): void {
|
|
23
|
+
public static updateMetaContent(currentView: View): void {
|
|
31
24
|
if (!currentView.shouldUpdateMetaContent()) return;
|
|
32
25
|
|
|
33
26
|
const strippedName = currentView.viewName.replace('-view', '');
|
|
@@ -44,11 +37,11 @@ export class NavigationManager {
|
|
|
44
37
|
{{/if}}
|
|
45
38
|
}
|
|
46
39
|
|
|
47
|
-
static getCurrentPath(): string {
|
|
40
|
+
public static getCurrentPath(): string {
|
|
48
41
|
return this.normalizePath(window.location.pathname);
|
|
49
42
|
}
|
|
50
43
|
|
|
51
|
-
static matchPattern(pattern: string, path: string): Record<string, string> | null {
|
|
44
|
+
public static matchPattern(pattern: string, path: string): Record<string, string> | null {
|
|
52
45
|
const paramNames: string[] = [];
|
|
53
46
|
|
|
54
47
|
const regexPattern = pattern
|
|
@@ -75,4 +68,12 @@ export class NavigationManager {
|
|
|
75
68
|
});
|
|
76
69
|
return params;
|
|
77
70
|
}
|
|
71
|
+
|
|
72
|
+
private static shouldPushState(
|
|
73
|
+
pushState: boolean,
|
|
74
|
+
currentPath: string,
|
|
75
|
+
normalizedPath: string
|
|
76
|
+
): boolean {
|
|
77
|
+
return pushState && currentPath !== normalizedPath;
|
|
78
|
+
}
|
|
78
79
|
}
|
|
@@ -25,31 +25,11 @@ export type RouteConfig = (() => View) | {
|
|
|
25
25
|
export type Routes = Record<string, RouteConfig>;
|
|
26
26
|
|
|
27
27
|
export class RouteGuardsManager {
|
|
28
|
-
static getViewConstructor(routeConfig: RouteConfig): () => View {
|
|
28
|
+
public static getViewConstructor(routeConfig: RouteConfig): () => View {
|
|
29
29
|
return typeof routeConfig === 'function' ? routeConfig : routeConfig.view;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
static
|
|
33
|
-
return typeof routeConfig === 'function' ? [] : (routeConfig.guards || []);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Runs all guards in sequence with route parameters and returns the result
|
|
38
|
-
*/
|
|
39
|
-
static async runGuards(
|
|
40
|
-
guards: RouteGuard[],
|
|
41
|
-
params: Record<string, string>
|
|
42
|
-
): Promise<boolean | string> {
|
|
43
|
-
for (const guard of guards) {
|
|
44
|
-
const result = await guard(params);
|
|
45
|
-
if (result !== true) {
|
|
46
|
-
return result; // false or redirect path
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
static async processRouteGuards(
|
|
32
|
+
public static async processRouteGuards(
|
|
53
33
|
routeConfig: RouteConfig,
|
|
54
34
|
targetPath: string,
|
|
55
35
|
params: Record<string, string> = {}
|
|
@@ -71,4 +51,24 @@ export class RouteGuardsManager {
|
|
|
71
51
|
const viewConstructor = this.getViewConstructor(routeConfig);
|
|
72
52
|
return { allowed: true, viewConstructor };
|
|
73
53
|
}
|
|
54
|
+
|
|
55
|
+
private static getRouteGuards(routeConfig: RouteConfig): RouteGuard[] {
|
|
56
|
+
return typeof routeConfig === 'function' ? [] : (routeConfig.guards || []);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Runs all guards in sequence with route parameters and returns the result
|
|
61
|
+
*/
|
|
62
|
+
private static async runGuards(
|
|
63
|
+
guards: RouteGuard[],
|
|
64
|
+
params: Record<string, string>
|
|
65
|
+
): Promise<boolean | string> {
|
|
66
|
+
for (const guard of guards) {
|
|
67
|
+
const result = await guard(params);
|
|
68
|
+
if (result !== true) {
|
|
69
|
+
return result; // false or redirect path
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
74
|
}
|
|
@@ -5,7 +5,7 @@ import { View, AppEventBus } from '../../../index.js';
|
|
|
5
5
|
*/
|
|
6
6
|
export class ViewRenderManager {
|
|
7
7
|
{{#if transition}}
|
|
8
|
-
static renderAnimatedView(view: View): void {
|
|
8
|
+
public static renderAnimatedView(view: View): void {
|
|
9
9
|
const container = document.getElementById('app');
|
|
10
10
|
const el = view.render();
|
|
11
11
|
el.classList.add('app-view-transition');
|
|
@@ -17,7 +17,7 @@ export class ViewRenderManager {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
{{/if}}
|
|
20
|
-
static async transitionOutCurrentView(currentView: View | null): Promise<null> {
|
|
20
|
+
public static async transitionOutCurrentView(currentView: View | null): Promise<null> {
|
|
21
21
|
if (!currentView) return null;
|
|
22
22
|
{{#if transition}}
|
|
23
23
|
const container = document.getElementById('app');
|
|
@@ -33,7 +33,7 @@ export class ViewRenderManager {
|
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
static renderNewView(
|
|
36
|
+
public static renderNewView(
|
|
37
37
|
viewConstructor: () => View,
|
|
38
38
|
params: Record<string, string> = {}
|
|
39
39
|
): View {
|
|
@@ -49,43 +49,11 @@ export class ViewRenderManager {
|
|
|
49
49
|
view.render();
|
|
50
50
|
|
|
51
51
|
{{/if}}
|
|
52
|
-
this.cleanupOptionalContent();
|
|
53
|
-
|
|
54
52
|
this.emitEvent('view-mount', view.viewName);
|
|
55
53
|
return view;
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
static
|
|
59
|
-
const isEmpty = (el: HTMLElement): boolean => {
|
|
60
|
-
const attrName = el.dataset.optional?.trim();
|
|
61
|
-
|
|
62
|
-
if (attrName && attrName !== "") {
|
|
63
|
-
const attrValue = el.getAttribute(attrName);
|
|
64
|
-
return !attrValue || attrValue.trim() === "" || attrValue === "undefined";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (el instanceof HTMLImageElement) {
|
|
68
|
-
return !el.src || el.src.trim() === "";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
72
|
-
return !el.value?.trim();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (el instanceof HTMLMediaElement || el instanceof HTMLSourceElement) {
|
|
76
|
-
return !el.getAttribute("src");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const content = el.textContent?.trim();
|
|
80
|
-
return !content || content === "undefined";
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
document.querySelectorAll<HTMLElement>("[data-optional]").forEach(el => {
|
|
84
|
-
if (isEmpty(el)) el.remove();
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
static emitEvent(event: EventKey, viewName: string): void {
|
|
56
|
+
private static emitEvent(event: EventKey, viewName: string): void {
|
|
89
57
|
AppEventBus.emit(event, viewName);
|
|
90
58
|
}
|
|
91
59
|
{{#if transition}}
|
|
@@ -5,7 +5,6 @@ import { NavigationManager } from './helpers/navigation-manager.helper.js';
|
|
|
5
5
|
|
|
6
6
|
// centralized export
|
|
7
7
|
export { Routes, RouteGuard } from './helpers/route-guard-manager.helper.js';
|
|
8
|
-
export { ViewRenderManager } from './helpers/view-render-manager.helper.js';
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* IRouter is a type alias for the instance of Router, not a true interface
|
|
@@ -26,6 +25,51 @@ class Router {
|
|
|
26
25
|
this.navigate(NavigationManager.getCurrentPath());
|
|
27
26
|
}
|
|
28
27
|
|
|
28
|
+
public async reload(): Promise<void> {
|
|
29
|
+
const currentRoute = NavigationManager.getCurrentPath();
|
|
30
|
+
await this.navigate(currentRoute, false);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public async navigate(path: string | '', pushState: boolean = true): Promise<void> {
|
|
34
|
+
const normalizedPath = NavigationManager.normalizePath(path);
|
|
35
|
+
const currentPath = NavigationManager.getCurrentPath();
|
|
36
|
+
|
|
37
|
+
// Try to match the route with parameters
|
|
38
|
+
const routeMatch = this.matchRoute(normalizedPath);
|
|
39
|
+
|
|
40
|
+
if (!routeMatch) {
|
|
41
|
+
await this.handleNotFound(normalizedPath, currentPath, pushState);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const guardResult = await this.handleGuards(
|
|
46
|
+
normalizedPath,
|
|
47
|
+
routeMatch.route,
|
|
48
|
+
routeMatch.params,
|
|
49
|
+
pushState
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (!guardResult) return;
|
|
53
|
+
|
|
54
|
+
this._currentView = await ViewRenderManager.transitionOutCurrentView(this._currentView);
|
|
55
|
+
this._currentParams = routeMatch.params;
|
|
56
|
+
this._currentView = ViewRenderManager.renderNewView(
|
|
57
|
+
guardResult.viewConstructor!,
|
|
58
|
+
routeMatch.params
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
NavigationManager.updateMetaContent(this._currentView);
|
|
62
|
+
NavigationManager.updateHistory(normalizedPath, currentPath, pushState);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public getCurrentParams(): Record<string, string> {
|
|
66
|
+
return { ...this._currentParams };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public getParam(key: string): string | undefined {
|
|
70
|
+
return this._currentParams[key];
|
|
71
|
+
}
|
|
72
|
+
|
|
29
73
|
private initializeEventListeners(): void {
|
|
30
74
|
window.addEventListener('popstate', () => this.handlePopState());
|
|
31
75
|
AppEventBus.subscribe('navigate', (path: string) => this.navigate(path));
|
|
@@ -95,51 +139,6 @@ class Router {
|
|
|
95
139
|
|
|
96
140
|
return guardResult;
|
|
97
141
|
}
|
|
98
|
-
|
|
99
|
-
public async reload(): Promise<void> {
|
|
100
|
-
const currentRoute = NavigationManager.getCurrentPath();
|
|
101
|
-
await this.navigate(currentRoute, false);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
public async navigate(path: string | '', pushState: boolean = true): Promise<void> {
|
|
105
|
-
const normalizedPath = NavigationManager.normalizePath(path);
|
|
106
|
-
const currentPath = NavigationManager.getCurrentPath();
|
|
107
|
-
|
|
108
|
-
// Try to match the route with parameters
|
|
109
|
-
const routeMatch = this.matchRoute(normalizedPath);
|
|
110
|
-
|
|
111
|
-
if (!routeMatch) {
|
|
112
|
-
await this.handleNotFound(normalizedPath, currentPath, pushState);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const guardResult = await this.handleGuards(
|
|
117
|
-
normalizedPath,
|
|
118
|
-
routeMatch.route,
|
|
119
|
-
routeMatch.params,
|
|
120
|
-
pushState
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
if (!guardResult) return;
|
|
124
|
-
|
|
125
|
-
this._currentView = await ViewRenderManager.transitionOutCurrentView(this._currentView);
|
|
126
|
-
this._currentParams = routeMatch.params;
|
|
127
|
-
this._currentView = ViewRenderManager.renderNewView(
|
|
128
|
-
guardResult.viewConstructor!,
|
|
129
|
-
routeMatch.params
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
NavigationManager.updateMetaContent(this._currentView);
|
|
133
|
-
NavigationManager.updateHistory(normalizedPath, currentPath, pushState);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
public getCurrentParams(): Record<string, string> {
|
|
137
|
-
return { ...this._currentParams };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
public getParam(key: string): string | undefined {
|
|
141
|
-
return this._currentParams[key];
|
|
142
|
-
}
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
export const AppRouter = (routes: Routes) => new Router(routes);
|
|
@@ -10,10 +10,6 @@ export class Store extends Service<Store> {
|
|
|
10
10
|
this.registerCleanup(this.clear);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
protected onDestroy(): void {
|
|
14
|
-
this.clear();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
13
|
public set<T = any>(key: string, value: T): void {
|
|
18
14
|
this._state[key] = value;
|
|
19
15
|
AppEventBus.emit(`store:${key}`, value);
|
|
@@ -27,12 +23,16 @@ export class Store extends Service<Store> {
|
|
|
27
23
|
AppEventBus.subscribe(`store:${key}`, callback);
|
|
28
24
|
}
|
|
29
25
|
|
|
26
|
+
public unsubscribe<T = any>(key: string, callback: (value: T) => void): void {
|
|
27
|
+
AppEventBus.off(`store:${key}`, callback);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
30
|
public clear(): void {
|
|
31
31
|
this._state = {};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
protected onDestroy(): void {
|
|
35
|
+
this.clear();
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -3,9 +3,9 @@ import { AttributesHelper, IAttributesConfig } from "../helpers/attributes.helpe
|
|
|
3
3
|
/**
|
|
4
4
|
* ```typescript
|
|
5
5
|
interface IAttributesConfig {
|
|
6
|
-
{{#if i18n}}
|
|
6
|
+
{{#if i18n}}
|
|
7
7
|
i18nKey?: string;
|
|
8
|
-
{{/if}}
|
|
8
|
+
{{/if}}
|
|
9
9
|
textContent?: string;
|
|
10
10
|
className?: string;
|
|
11
11
|
style?: string;
|
|
@@ -42,30 +42,22 @@ export class AnchorManager {
|
|
|
42
42
|
target.appendChild(container);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
private createAnchorContainer() {
|
|
46
|
-
const container = document.createElement('
|
|
45
|
+
private createAnchorContainer(): HTMLSpanElement {
|
|
46
|
+
const container = document.createElement('span');
|
|
47
47
|
const anchor = this.createAnchor();
|
|
48
|
-
|
|
49
48
|
container.appendChild(anchor);
|
|
50
|
-
|
|
51
49
|
return container;
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
private createAnchor(): HTMLElement {
|
|
55
53
|
const anchor = document.createElement('a');
|
|
56
|
-
|
|
57
54
|
this.setAnchorRef(anchor);
|
|
58
|
-
|
|
59
|
-
AttributesHelper.setContent(anchor, this.config);
|
|
60
|
-
AttributesHelper.setStyle(anchor, this.config);
|
|
61
|
-
AttributesHelper.setPipes(anchor, this.config);
|
|
62
|
-
|
|
55
|
+
AttributesHelper.setAttributes(anchor, this.config);
|
|
63
56
|
this.handleClick(anchor);
|
|
64
|
-
|
|
65
57
|
return anchor;
|
|
66
58
|
}
|
|
67
59
|
|
|
68
|
-
private setAnchorRef(anchor: HTMLAnchorElement) {
|
|
60
|
+
private setAnchorRef(anchor: HTMLAnchorElement): void {
|
|
69
61
|
anchor.setAttribute('href', this.config.href);
|
|
70
62
|
if (this.config.target) {
|
|
71
63
|
anchor.setAttribute('target', this.config.target);
|
|
@@ -81,7 +73,41 @@ export class AnchorManager {
|
|
|
81
73
|
anchor?.addEventListener('click', (e) => {
|
|
82
74
|
e.preventDefault();
|
|
83
75
|
const target = document.getElementById(id);
|
|
84
|
-
target
|
|
76
|
+
this.scrollToTarget(target);
|
|
77
|
+
this.accessibilityFeatures(target, id);
|
|
85
78
|
});
|
|
86
79
|
}
|
|
80
|
+
|
|
81
|
+
private scrollToTarget(target: HTMLElement | null): void {
|
|
82
|
+
const targetPosition = target?.offsetTop;
|
|
83
|
+
window.scrollTo({
|
|
84
|
+
top: targetPosition,
|
|
85
|
+
behavior: 'smooth'
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private accessibilityFeatures(target: HTMLElement | null, id: string) {
|
|
90
|
+
this.setFocus(target);
|
|
91
|
+
this.setAriaLive(target, id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private setFocus(target: HTMLElement | null): void {
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
target?.setAttribute('tabindex', '-1');
|
|
97
|
+
target?.focus({ preventScroll: true });
|
|
98
|
+
// Remove tabindex after focus
|
|
99
|
+
target?.addEventListener('blur', () => {
|
|
100
|
+
target?.removeAttribute('tabindex');
|
|
101
|
+
}, { once: true });
|
|
102
|
+
}, 100);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private setAriaLive(target: HTMLElement | null, id: string): void {
|
|
106
|
+
const announcement = document.createElement('div');
|
|
107
|
+
announcement.setAttribute('aria-live', 'polite');
|
|
108
|
+
announcement.className = 'sr-only';
|
|
109
|
+
announcement.textContent = `Navigated to ${ target?.textContent || id }`;
|
|
110
|
+
document.body.appendChild(announcement);
|
|
111
|
+
setTimeout(() => document.body.removeChild(announcement), 1000);
|
|
112
|
+
}
|
|
87
113
|
}
|
|
@@ -2,6 +2,7 @@ import { AttributesHelper, IAttributesConfig } from "../helpers/attributes.helpe
|
|
|
2
2
|
|
|
3
3
|
export interface BaseButton extends IAttributesConfig {
|
|
4
4
|
callback: () => void;
|
|
5
|
+
label?: string;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export interface ButtonContainerOptions {
|
|
@@ -22,10 +23,6 @@ export interface ButtonContainerOptions {
|
|
|
22
23
|
pipes?: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
interface BaseButton extends IAttributesConfig {
|
|
26
|
-
callback: () => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
26
|
interface ButtonContainerOptions {
|
|
30
27
|
containerClassName?: string;
|
|
31
28
|
containerStyles?: string;
|
|
@@ -61,47 +58,71 @@ export class ButtonManager {
|
|
|
61
58
|
container.style = this.containerOptions.containerStyles || '';
|
|
62
59
|
|
|
63
60
|
this.buttons.forEach((config, index) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
if (config.label) {
|
|
62
|
+
return this.createButtonWithCheckbox(config, index, container);
|
|
63
|
+
} else {
|
|
64
|
+
const button = this.createButton(config, index);
|
|
65
|
+
container.appendChild(button);
|
|
66
|
+
this.bindButtonCallback(config, index);
|
|
67
|
+
}
|
|
67
68
|
});
|
|
68
69
|
|
|
69
70
|
return container;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
public appendTo(target: HTMLElement): void {
|
|
74
|
+
const container = this.createButtonContainer();
|
|
75
|
+
if (container) {
|
|
76
|
+
target.appendChild(container);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private createButtonWithCheckbox(config: BaseButton, index: number, container: HTMLElement): HTMLElement {
|
|
81
|
+
if (!config.label) return container;
|
|
82
|
+
const checkbox = document.createElement('input');
|
|
83
|
+
checkbox.setAttribute('type', 'checkbox');
|
|
84
|
+
checkbox.setAttribute('id', config.label);
|
|
85
|
+
checkbox.hidden = true;
|
|
86
|
+
container.appendChild(checkbox);
|
|
87
|
+
|
|
88
|
+
const label = document.createElement('label');
|
|
89
|
+
label.setAttribute('for', config.label);
|
|
90
|
+
|
|
91
|
+
const button = this.createButton(config, index);
|
|
92
|
+
label.appendChild(button);
|
|
93
|
+
|
|
94
|
+
container.appendChild(label);
|
|
95
|
+
this.bindButtonCallback(config, index, checkbox);
|
|
96
|
+
return container;
|
|
97
|
+
}
|
|
98
|
+
|
|
72
99
|
private createButton(config: BaseButton, index: number): HTMLElement {
|
|
73
100
|
const button = document.createElement('button');
|
|
74
101
|
|
|
75
|
-
AttributesHelper.
|
|
76
|
-
AttributesHelper.setStyle(button, config);
|
|
77
|
-
AttributesHelper.setPipes(button, config);
|
|
102
|
+
AttributesHelper.setAttributes(button, config);
|
|
78
103
|
|
|
79
104
|
this.setDataEvent(button, index);
|
|
80
105
|
|
|
81
106
|
return button;
|
|
82
107
|
}
|
|
83
108
|
|
|
84
|
-
private setDataEvent(button: HTMLButtonElement, index: number) {
|
|
109
|
+
private setDataEvent(button: HTMLButtonElement, index: number): void {
|
|
85
110
|
button.setAttribute('data-event', `click:onButtonClick_${index}`);
|
|
86
111
|
}
|
|
87
112
|
|
|
88
|
-
private bindButtonCallback(config: BaseButton, index: number): void {
|
|
113
|
+
private bindButtonCallback(config: BaseButton, index: number, checkbox?: HTMLInputElement): void {
|
|
89
114
|
const methodName = `onButtonClick_${index}`;
|
|
90
115
|
|
|
91
116
|
this.component[methodName] = () => {
|
|
92
117
|
if (typeof config.callback === 'function') {
|
|
93
118
|
config.callback();
|
|
119
|
+
if (checkbox) {
|
|
120
|
+
checkbox.checked = !checkbox.checked;
|
|
121
|
+
}
|
|
94
122
|
}
|
|
95
123
|
};
|
|
96
124
|
}
|
|
97
125
|
|
|
98
|
-
public appendTo(target: HTMLElement): void {
|
|
99
|
-
const container = this.createButtonContainer();
|
|
100
|
-
if (container) {
|
|
101
|
-
target.appendChild(container);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
126
|
/** Note : doesn't track container
|
|
106
127
|
*/
|
|
107
128
|
public updateButtons(newButtons: BaseButton[]): void {
|
|
@@ -10,21 +10,27 @@ export interface IAttributesConfig {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export class AttributesHelper {
|
|
13
|
-
static
|
|
13
|
+
public static setAttributes(element: HTMLElement, config: IAttributesConfig): void {
|
|
14
|
+
this.setContent(element, config);
|
|
15
|
+
this.setPipes(element, config);
|
|
16
|
+
this.setStyle(element, config);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private static setContent(element: HTMLElement, config: IAttributesConfig): void {
|
|
14
20
|
element.textContent = config.textContent || '';
|
|
15
|
-
|
|
21
|
+
{{#if i18n}}
|
|
16
22
|
if (config.i18nKey) {
|
|
17
23
|
element.setAttribute('data-i18n', config.i18nKey);
|
|
18
24
|
}
|
|
19
|
-
|
|
25
|
+
{{/if}}
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
static setPipes(element: HTMLElement, config: IAttributesConfig) {
|
|
28
|
+
private static setPipes(element: HTMLElement, config: IAttributesConfig): void {
|
|
23
29
|
if (!config.pipes) return;
|
|
24
30
|
element.setAttribute('data-pipe', config.pipes);
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
static setStyle(element: HTMLElement, config: IAttributesConfig) {
|
|
33
|
+
private static setStyle(element: HTMLElement, config: IAttributesConfig): void {
|
|
28
34
|
const classes = [
|
|
29
35
|
config.className
|
|
30
36
|
].filter(Boolean).join(' ');
|