@nutin/cli 1.3.0 → 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 +34 -0
- package/README.md +8 -9
- package/lib/index.mjs +18 -18
- package/lib/src/context-builder.mjs +21 -16
- package/lib/src/print.mjs +1 -0
- package/lib/src/project-generator.mjs +5 -5
- package/lib/src/prompts.mjs +13 -85
- package/lib/src/version.mjs +1 -1
- package/package.json +1 -1
- package/templates/base/builder.config.js.hbs +1 -0
- package/templates/base/src/app/globals.d.ts.hbs +12 -8
- package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +25 -8
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +34 -20
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +34 -0
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +11 -12
- package/templates/base/src/core/base-classes/component/component.ts.hbs +13 -17
- package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +17 -0
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +2 -1
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +0 -33
- package/templates/base/src/core/services/router/router.ts.hbs +0 -1
- package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +35 -7
- package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +31 -8
- 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 +84 -91
- package/templates/base/src/libs/snackbar/snackbar.ts.hbs +6 -3
- 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/features/i18n/src/core/services/i18n/i18n.ts.hbs +41 -8
- package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +14 -0
- package/templates/base/src/styles/core/_libs.scss.hbs +0 -132
|
@@ -3,9 +3,9 @@ import { TokenHelper } from './token.helper.js';
|
|
|
3
3
|
|
|
4
4
|
export class EventHelper {
|
|
5
5
|
public static bindEvents(
|
|
6
|
-
component: BaseComponent,
|
|
7
|
-
element: HTMLElement,
|
|
8
|
-
eventListeners: Array<[string, EventListener]>
|
|
6
|
+
component: BaseComponent,
|
|
7
|
+
element: HTMLElement,
|
|
8
|
+
eventListeners: Array<[EventTarget, string, EventListener]>
|
|
9
9
|
): void {
|
|
10
10
|
element.querySelectorAll('[data-event]').forEach(el => {
|
|
11
11
|
const parts = el.getAttribute('data-event')!.split(':');
|
|
@@ -24,11 +24,10 @@ export class EventHelper {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
public static destroyEvents(
|
|
27
|
-
|
|
28
|
-
eventListeners: Array<[string, EventListener]>
|
|
27
|
+
eventListeners: Array<[EventTarget, string, EventListener]>
|
|
29
28
|
): void {
|
|
30
|
-
eventListeners.forEach(([event, listener]) => {
|
|
31
|
-
|
|
29
|
+
eventListeners.forEach(([target, event, listener]) => {
|
|
30
|
+
target.removeEventListener(event, listener);
|
|
32
31
|
});
|
|
33
32
|
}
|
|
34
33
|
|
|
@@ -46,12 +45,12 @@ export class EventHelper {
|
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
private static addEvent(
|
|
49
|
-
target: EventTarget,
|
|
50
|
-
event: string,
|
|
51
|
-
listener: EventListener,
|
|
52
|
-
eventListeners: Array<[string, EventListener]>
|
|
48
|
+
target: EventTarget,
|
|
49
|
+
event: string,
|
|
50
|
+
listener: EventListener,
|
|
51
|
+
eventListeners: Array<[EventTarget, string, EventListener]>
|
|
53
52
|
): void {
|
|
54
53
|
target.addEventListener(event, listener);
|
|
55
|
-
eventListeners.push([event, listener]);
|
|
54
|
+
eventListeners.push([target, event, listener]);
|
|
56
55
|
}
|
|
57
56
|
}
|
|
@@ -56,24 +56,14 @@ export abstract class Component<T extends HTMLElement = HTMLElement, K = any> ex
|
|
|
56
56
|
normalizeKeys = [],
|
|
57
57
|
defaults = {},
|
|
58
58
|
}: ComponentOptions) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const template = templateFn(normalizedConfig);
|
|
63
|
-
|
|
59
|
+
const template = ConfigHelper.createNormalizedTemplate({
|
|
60
|
+
config, defaults, normalizeKeys, templateFn,
|
|
61
|
+
});
|
|
64
62
|
super({template, mountTarget, tagName});
|
|
65
63
|
|
|
66
|
-
this.config = normalizeKeys
|
|
67
|
-
? ConfigHelper.normalizeStrings(config, normalizeKeys)
|
|
68
|
-
: config ?? ({} as T);
|
|
69
|
-
|
|
64
|
+
this.config = ConfigHelper.setConfigValue(config, normalizeKeys);
|
|
70
65
|
this.props = props;
|
|
71
|
-
|
|
72
|
-
this.buttonManager = new ButtonManager(
|
|
73
|
-
this,
|
|
74
|
-
this.props.buttons,
|
|
75
|
-
{ containerClassName: 'component-buttons' }
|
|
76
|
-
);
|
|
66
|
+
this.buttonManager = this.createButtonManager();
|
|
77
67
|
}
|
|
78
68
|
|
|
79
69
|
public getValues(): Record<string, string> {
|
|
@@ -88,15 +78,21 @@ export abstract class Component<T extends HTMLElement = HTMLElement, K = any> ex
|
|
|
88
78
|
return super.render();
|
|
89
79
|
}
|
|
90
80
|
|
|
81
|
+
private createButtonManager(): ButtonManager {
|
|
82
|
+
return new ButtonManager(
|
|
83
|
+
this,
|
|
84
|
+
this.props.buttons,
|
|
85
|
+
{ containerClassName: 'component-buttons' }
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
91
89
|
private applyProps(): void {
|
|
92
90
|
if (this.props.className) {
|
|
93
91
|
this.element.classList.add(this.props.className);
|
|
94
92
|
}
|
|
95
|
-
|
|
96
93
|
if (this.props.style) {
|
|
97
94
|
this.element.style = this.props.style;
|
|
98
95
|
}
|
|
99
|
-
|
|
100
96
|
this.applyDataBindings();
|
|
101
97
|
}
|
|
102
98
|
|
|
@@ -23,4 +23,21 @@ export class ConfigHelper {
|
|
|
23
23
|
|
|
24
24
|
return normalized;
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
public static createNormalizedTemplate<T extends Record<string, any>>({
|
|
28
|
+
config,
|
|
29
|
+
defaults = {},
|
|
30
|
+
normalizeKeys = [],
|
|
31
|
+
templateFn = () => ''
|
|
32
|
+
}: T) {
|
|
33
|
+
let normalizedConfig = ConfigHelper.normalize(config, defaults);
|
|
34
|
+
normalizedConfig = ConfigHelper.normalizeStrings(normalizedConfig, normalizeKeys);
|
|
35
|
+
return templateFn(normalizedConfig);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public static setConfigValue<T extends Record<string, any>>(config: T, normalizeKeys: (keyof T)[]): T {
|
|
39
|
+
if (normalizeKeys) return ConfigHelper.normalizeStrings(config, normalizeKeys) as T;
|
|
40
|
+
else if (config) return config as T
|
|
41
|
+
else return ({} as T);
|
|
42
|
+
}
|
|
26
43
|
}
|
|
@@ -5,7 +5,8 @@ import { {{#if i18n}}I18nService, {{/if}}View } from "../../../index.js";
|
|
|
5
5
|
*/
|
|
6
6
|
export class NavigationManager {
|
|
7
7
|
public static normalizePath(path: string): string {
|
|
8
|
-
const
|
|
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
|
|
|
@@ -49,43 +49,10 @@ export class ViewRenderManager {
|
|
|
49
49
|
view.render();
|
|
50
50
|
|
|
51
51
|
{{/if}}
|
|
52
|
-
|
|
53
|
-
this.cleanupOptionalContent();
|
|
54
|
-
|
|
55
52
|
this.emitEvent('view-mount', view.viewName);
|
|
56
53
|
return view;
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
public static cleanupOptionalContent(): void {
|
|
60
|
-
const isEmpty = (el: HTMLElement): boolean => {
|
|
61
|
-
const attrName = el.dataset.optional?.trim();
|
|
62
|
-
|
|
63
|
-
if (attrName && attrName !== "") {
|
|
64
|
-
const attrValue = el.getAttribute(attrName);
|
|
65
|
-
return !attrValue || attrValue.trim() === "" || attrValue === "undefined";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (el instanceof HTMLImageElement) {
|
|
69
|
-
return !el.src || el.src.trim() === "";
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
73
|
-
return !el.value?.trim();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (el instanceof HTMLMediaElement || el instanceof HTMLSourceElement) {
|
|
77
|
-
return !el.getAttribute("src");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const content = el.textContent?.trim();
|
|
81
|
-
return !content || content === "undefined";
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
document.querySelectorAll<HTMLElement>("[data-optional]").forEach(el => {
|
|
85
|
-
if (isEmpty(el)) el.remove();
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
56
|
private static emitEvent(event: EventKey, viewName: string): void {
|
|
90
57
|
AppEventBus.emit(event, viewName);
|
|
91
58
|
}
|
|
@@ -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
|
|
@@ -45,21 +45,15 @@ export class AnchorManager {
|
|
|
45
45
|
private createAnchorContainer(): HTMLSpanElement {
|
|
46
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
55
|
AttributesHelper.setAttributes(anchor, this.config);
|
|
60
|
-
|
|
61
56
|
this.handleClick(anchor);
|
|
62
|
-
|
|
63
57
|
return anchor;
|
|
64
58
|
}
|
|
65
59
|
|
|
@@ -79,7 +73,41 @@ export class AnchorManager {
|
|
|
79
73
|
anchor?.addEventListener('click', (e) => {
|
|
80
74
|
e.preventDefault();
|
|
81
75
|
const target = document.getElementById(id);
|
|
82
|
-
target
|
|
76
|
+
this.scrollToTarget(target);
|
|
77
|
+
this.accessibilityFeatures(target, id);
|
|
83
78
|
});
|
|
84
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
|
+
}
|
|
85
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,9 +58,13 @@ 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;
|
|
@@ -76,6 +77,25 @@ export class ButtonManager {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
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
|
+
|
|
79
99
|
private createButton(config: BaseButton, index: number): HTMLElement {
|
|
80
100
|
const button = document.createElement('button');
|
|
81
101
|
|
|
@@ -90,12 +110,15 @@ export class ButtonManager {
|
|
|
90
110
|
button.setAttribute('data-event', `click:onButtonClick_${index}`);
|
|
91
111
|
}
|
|
92
112
|
|
|
93
|
-
private bindButtonCallback(config: BaseButton, index: number): void {
|
|
113
|
+
private bindButtonCallback(config: BaseButton, index: number, checkbox?: HTMLInputElement): void {
|
|
94
114
|
const methodName = `onButtonClick_${index}`;
|
|
95
115
|
|
|
96
116
|
this.component[methodName] = () => {
|
|
97
117
|
if (typeof config.callback === 'function') {
|
|
98
118
|
config.callback();
|
|
119
|
+
if (checkbox) {
|
|
120
|
+
checkbox.checked = !checkbox.checked;
|
|
121
|
+
}
|
|
99
122
|
}
|
|
100
123
|
};
|
|
101
124
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { AppEventBus } from "../../../core/index.js";
|
|
2
|
+
|
|
3
|
+
type KeyboardEventHandler = (event: KeyboardEvent) => void;
|
|
4
|
+
|
|
5
|
+
export interface IFocusTrapOptions {
|
|
6
|
+
escapeDeactivates?: boolean;
|
|
7
|
+
clickOutsideDeactivates?: boolean;
|
|
8
|
+
returnFocusOnDeactivate?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IFocusTrapHelperParams {
|
|
12
|
+
container: HTMLElement;
|
|
13
|
+
overlay: HTMLElement;
|
|
14
|
+
options?: IFocusTrapOptions;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class FocusTrapHelper {
|
|
18
|
+
private _container: HTMLElement;
|
|
19
|
+
private _overlay: HTMLElement;
|
|
20
|
+
private _previousActiveElement: HTMLElement | null | undefined = null;
|
|
21
|
+
private _focusableElements: NodeListOf<HTMLElement> | null | undefined = null;
|
|
22
|
+
private _firstFocusableElement: HTMLElement | null | undefined = null;
|
|
23
|
+
private _lastFocusableElement: HTMLElement | null | undefined = null;
|
|
24
|
+
private _boundKeyDown: KeyboardEventHandler;
|
|
25
|
+
private _options: IFocusTrapOptions;
|
|
26
|
+
private _isActive: boolean = false;
|
|
27
|
+
|
|
28
|
+
constructor({container, overlay, options = {}}: IFocusTrapHelperParams) {
|
|
29
|
+
this._container = container;
|
|
30
|
+
this._overlay = overlay;
|
|
31
|
+
this._options = {
|
|
32
|
+
escapeDeactivates: true,
|
|
33
|
+
clickOutsideDeactivates: true,
|
|
34
|
+
returnFocusOnDeactivate: true,
|
|
35
|
+
...options
|
|
36
|
+
};
|
|
37
|
+
this._boundKeyDown = this._handleKeyDown.bind(this);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private _getFocusableElements(): NodeListOf<HTMLElement> {
|
|
41
|
+
return this._container.querySelectorAll(
|
|
42
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
43
|
+
) as NodeListOf<HTMLElement>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public activate(): void {
|
|
47
|
+
if (this._isActive) return;
|
|
48
|
+
|
|
49
|
+
this._previousActiveElement = document.activeElement as HTMLElement;
|
|
50
|
+
|
|
51
|
+
this._focusableElements = this._getFocusableElements();
|
|
52
|
+
|
|
53
|
+
if (this._focusableElements.length > 0) {
|
|
54
|
+
this._firstFocusableElement = this._focusableElements[0];
|
|
55
|
+
this._lastFocusableElement = this._focusableElements[this._focusableElements.length - 1];
|
|
56
|
+
|
|
57
|
+
this._firstFocusableElement?.focus();
|
|
58
|
+
} else {
|
|
59
|
+
this._container.setAttribute('tabindex', '-1');
|
|
60
|
+
this._container.focus();
|
|
61
|
+
this._firstFocusableElement = this._container;
|
|
62
|
+
this._lastFocusableElement = this._container;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this._container.addEventListener('keydown', this._boundKeyDown);
|
|
66
|
+
|
|
67
|
+
document.addEventListener('click', this._handleOutsideClick.bind(this));
|
|
68
|
+
|
|
69
|
+
this._disableOutsideTabIndex();
|
|
70
|
+
|
|
71
|
+
this._isActive = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public deactivate(): void {
|
|
75
|
+
if (!this._isActive) return;
|
|
76
|
+
|
|
77
|
+
this._container.removeEventListener('keydown', this._boundKeyDown);
|
|
78
|
+
document.removeEventListener('click', this._handleOutsideClick.bind(this));
|
|
79
|
+
|
|
80
|
+
this._enableOutsideTabIndex();
|
|
81
|
+
|
|
82
|
+
if (this._container.getAttribute('tabindex') === '-1') {
|
|
83
|
+
this._container.removeAttribute('tabindex');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (this._options.returnFocusOnDeactivate &&
|
|
87
|
+
this._previousActiveElement &&
|
|
88
|
+
this._previousActiveElement.focus
|
|
89
|
+
) {
|
|
90
|
+
this._previousActiveElement.focus();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this._focusableElements = null;
|
|
94
|
+
this._firstFocusableElement = null;
|
|
95
|
+
this._lastFocusableElement = null;
|
|
96
|
+
this._previousActiveElement = null;
|
|
97
|
+
|
|
98
|
+
this._isActive = false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public getIsActive(): boolean {
|
|
102
|
+
return this._isActive;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public updateFocusableElements(): void {
|
|
106
|
+
if (!this._isActive) return;
|
|
107
|
+
|
|
108
|
+
this._focusableElements = this._getFocusableElements();
|
|
109
|
+
|
|
110
|
+
if (this._focusableElements.length > 0) {
|
|
111
|
+
this._firstFocusableElement = this._focusableElements[0];
|
|
112
|
+
this._lastFocusableElement = this._focusableElements[this._focusableElements.length - 1];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private _disableOutsideTabIndex(): void {
|
|
117
|
+
const allElements = document.querySelectorAll('body *') as NodeListOf<HTMLElement>;
|
|
118
|
+
|
|
119
|
+
allElements.forEach((el: HTMLElement) => {
|
|
120
|
+
if (!this._container.contains(el)) {
|
|
121
|
+
const currentTabIndex = el.getAttribute('tabindex');
|
|
122
|
+
if (currentTabIndex !== '-1') {
|
|
123
|
+
const originalTabIndex = currentTabIndex ||
|
|
124
|
+
(el.tabIndex !== -1 ? el.tabIndex.toString() : null);
|
|
125
|
+
|
|
126
|
+
if (originalTabIndex !== null) {
|
|
127
|
+
el.setAttribute('data-original-tabindex', originalTabIndex);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
el.setAttribute('tabindex', '-1');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private _enableOutsideTabIndex(): void {
|
|
137
|
+
const allElements = document.querySelectorAll(
|
|
138
|
+
'[data-original-tabindex]'
|
|
139
|
+
) as NodeListOf<HTMLElement>;
|
|
140
|
+
|
|
141
|
+
allElements.forEach((el: HTMLElement) => {
|
|
142
|
+
const originalTabIndex = el.getAttribute('data-original-tabindex');
|
|
143
|
+
|
|
144
|
+
if (originalTabIndex === null) {
|
|
145
|
+
el.removeAttribute('tabindex');
|
|
146
|
+
} else if (originalTabIndex === '0') {
|
|
147
|
+
el.removeAttribute('tabindex');
|
|
148
|
+
} else {
|
|
149
|
+
el.setAttribute('tabindex', originalTabIndex);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
el.removeAttribute('data-original-tabindex');
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private _handleKeyDown(event: KeyboardEvent): void {
|
|
157
|
+
switch (event.key) {
|
|
158
|
+
case 'Tab':
|
|
159
|
+
this._handleTabKey(event);
|
|
160
|
+
break;
|
|
161
|
+
case 'Escape':
|
|
162
|
+
if (this._options.escapeDeactivates) {
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
AppEventBus.emit('popover-close');
|
|
165
|
+
this.deactivate();
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private _handleTabKey(event: KeyboardEvent): void {
|
|
172
|
+
if (!this._focusableElements || this._focusableElements.length === 0) return;
|
|
173
|
+
|
|
174
|
+
if (event.shiftKey) {
|
|
175
|
+
if (document.activeElement === this._firstFocusableElement) {
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
this._lastFocusableElement?.focus();
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
if (document.activeElement === this._lastFocusableElement) {
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
this._firstFocusableElement?.focus();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private _handleOutsideClick(event: MouseEvent): void {
|
|
188
|
+
if (!this._options.clickOutsideDeactivates) return;
|
|
189
|
+
this._overlay.addEventListener('click', (e: MouseEvent) => {
|
|
190
|
+
if (e.target === this._overlay){
|
|
191
|
+
event.stopPropagation();
|
|
192
|
+
AppEventBus.emit('popover-close');
|
|
193
|
+
this.deactivate();;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ButtonManager } from "src/core/index.js";
|
|
2
|
+
|
|
3
|
+
export interface IPopoverDomElements {
|
|
4
|
+
wrapper: HTMLElement;
|
|
5
|
+
overlay: HTMLElement;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class PopoverDomHelper {
|
|
9
|
+
public static createDomElements(
|
|
10
|
+
content: HTMLElement,
|
|
11
|
+
buttonManager: ButtonManager
|
|
12
|
+
): IPopoverDomElements {
|
|
13
|
+
const overlay = this.createOverlay();
|
|
14
|
+
const wrapper = this.createWrapper();
|
|
15
|
+
content = this.createContent(content);
|
|
16
|
+
|
|
17
|
+
this.appendCloseButton(content);
|
|
18
|
+
this.appendFooterButtons(content, buttonManager);
|
|
19
|
+
|
|
20
|
+
wrapper.appendChild(content);
|
|
21
|
+
overlay.appendChild(wrapper);
|
|
22
|
+
document.body.appendChild(overlay);
|
|
23
|
+
|
|
24
|
+
return { wrapper, overlay };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public static removeDomElements(overlay: HTMLElement | null, onRemoved?: () => void): null {
|
|
28
|
+
if (overlay) {
|
|
29
|
+
const wrapper = overlay.querySelector('.popover-wrapper');
|
|
30
|
+
overlay.classList.remove('show');
|
|
31
|
+
wrapper?.classList.remove('show');
|
|
32
|
+
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
wrapper?.remove();
|
|
35
|
+
overlay?.remove();
|
|
36
|
+
onRemoved?.();
|
|
37
|
+
}, 300); // match transition duration
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private static createOverlay(): HTMLElement {
|
|
43
|
+
const overlay = document.createElement('div');
|
|
44
|
+
overlay.className = 'popover-overlay';
|
|
45
|
+
return overlay;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private static createWrapper(): HTMLElement {
|
|
49
|
+
const wrapper = document.createElement('div');
|
|
50
|
+
wrapper.className = 'popover-wrapper';
|
|
51
|
+
return wrapper;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private static createContent(content: HTMLElement): HTMLElement {
|
|
55
|
+
content.classList.add('popover-content');
|
|
56
|
+
return content;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private static appendCloseButton(content: HTMLElement): void {
|
|
60
|
+
const closeButton = document.createElement('button');
|
|
61
|
+
closeButton.className = 'popover-close-button';
|
|
62
|
+
closeButton.innerHTML = '×';
|
|
63
|
+
closeButton.setAttribute('aria-label', 'Close');
|
|
64
|
+
// bind method onCloseClick
|
|
65
|
+
closeButton.setAttribute('data-event', 'click:onCloseClick');
|
|
66
|
+
content.prepend(closeButton);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private static appendFooterButtons(
|
|
70
|
+
content: HTMLElement,
|
|
71
|
+
buttonManager: ButtonManager
|
|
72
|
+
): void {
|
|
73
|
+
buttonManager.appendTo(content);
|
|
74
|
+
}
|
|
75
|
+
}
|