@brandup/ui-kit 1.0.38 → 1.0.39
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/package.json +1 -1
- package/source/index.ts +1 -0
- package/source/modal.less +94 -0
- package/source/modal.ts +114 -0
- package/source/styles.less +1 -1
package/package.json
CHANGED
package/source/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { UiKitMiddleware } from "./middleware";
|
|
2
2
|
export * from "./popup";
|
|
3
|
+
export { default as Modal, MODAL_CLASS, MODAL_OPENED_CLASS, MODAL_CLOSE_COMMAND, type ModalOptions } from "./modal";
|
|
3
4
|
export { IS_TOUCH_DEVICE } from "./utils/compatibility";
|
|
4
5
|
import "./styles.less";
|
|
5
6
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
@import (reference) "../vars.less";
|
|
2
|
+
|
|
3
|
+
// Модальное окно: затемнение на весь экран, окно по центру. Оформление берётся от popup —
|
|
4
|
+
// это та же всплывающая поверхность, только перекрывающая страницу.
|
|
5
|
+
|
|
6
|
+
:root {
|
|
7
|
+
--modal-fill: var(--popup-fill);
|
|
8
|
+
--modal-color: var(--popup-color);
|
|
9
|
+
--modal-border-radius: var(--popup-border-radius);
|
|
10
|
+
--modal-box-shadow: var(--popup-box-shadow);
|
|
11
|
+
--modal-backdrop: rgba(0, 0, 0, 0.45);
|
|
12
|
+
--modal-width: 480px;
|
|
13
|
+
--modal-padding: 20px;
|
|
14
|
+
--modal-z-index: 2000; // выше popup (1000): окно перекрывает и его
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.ui-modal {
|
|
18
|
+
position: fixed;
|
|
19
|
+
inset: 0;
|
|
20
|
+
z-index: var(--modal-z-index);
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
padding: 20px;
|
|
25
|
+
box-sizing: border-box;
|
|
26
|
+
|
|
27
|
+
& .modal-backdrop {
|
|
28
|
+
position: absolute;
|
|
29
|
+
inset: 0;
|
|
30
|
+
background-color: var(--modal-backdrop);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
& .modal-window {
|
|
34
|
+
position: relative; // над подложкой
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-flow: column nowrap;
|
|
37
|
+
width: 100%;
|
|
38
|
+
max-width: var(--modal-width);
|
|
39
|
+
max-height: 100%;
|
|
40
|
+
box-sizing: border-box;
|
|
41
|
+
background: var(--modal-fill);
|
|
42
|
+
color: var(--modal-color);
|
|
43
|
+
border-radius: var(--modal-border-radius);
|
|
44
|
+
box-shadow: var(--modal-box-shadow);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
& .modal-header {
|
|
48
|
+
flex: 0 0 auto;
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-flow: row nowrap;
|
|
51
|
+
align-items: center;
|
|
52
|
+
justify-content: space-between;
|
|
53
|
+
gap: 10px;
|
|
54
|
+
padding: var(--modal-padding);
|
|
55
|
+
padding-bottom: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
& .modal-title {
|
|
59
|
+
font-size: var(--h5-font-size);
|
|
60
|
+
font-weight: var(--h5-font-weight);
|
|
61
|
+
line-height: var(--h-line-height);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
& .modal-close {
|
|
65
|
+
flex: 0 0 auto;
|
|
66
|
+
margin-left: auto; // без заголовка крестик всё равно справа
|
|
67
|
+
width: 28px;
|
|
68
|
+
height: 28px;
|
|
69
|
+
padding: 0;
|
|
70
|
+
border: 0;
|
|
71
|
+
border-radius: 50%;
|
|
72
|
+
background: transparent;
|
|
73
|
+
color: inherit;
|
|
74
|
+
font-size: 22px;
|
|
75
|
+
line-height: 1;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
|
|
78
|
+
&:hover {
|
|
79
|
+
background-color: rgba(0, 0, 0, 0.06);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// тело прокручивается само, шапка остаётся на месте
|
|
84
|
+
& .modal-body {
|
|
85
|
+
flex: 1 1 auto;
|
|
86
|
+
padding: var(--modal-padding);
|
|
87
|
+
overflow-y: auto;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// страница под открытым окном не прокручивается
|
|
92
|
+
body.ui-modal-opened {
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
}
|
package/source/modal.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import "./modal.less"; // стили окна
|
|
2
|
+
|
|
3
|
+
import { DOM, UIElement } from "@brandup/ui";
|
|
4
|
+
|
|
5
|
+
export const MODAL_CLASS = "ui-modal";
|
|
6
|
+
export const MODAL_OPENED_CLASS = "ui-modal-opened"; // на <body>, чтобы страница не прокручивалась
|
|
7
|
+
export const MODAL_CLOSE_COMMAND = "ui-modal-close";
|
|
8
|
+
|
|
9
|
+
export interface ModalOptions {
|
|
10
|
+
/** Заголовок в шапке; пусто — шапка только с крестиком. */
|
|
11
|
+
title?: string | null;
|
|
12
|
+
/** Дополнительный класс на корне — под оформление конкретного окна. */
|
|
13
|
+
className?: string | null;
|
|
14
|
+
/** Закрывать по клику вне окна (по умолчанию да). */
|
|
15
|
+
closeOnBackdrop?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Базовое модальное окно: затемнение, шапка с заголовком и крестиком, тело.
|
|
20
|
+
*
|
|
21
|
+
* Наследник наполняет {@link body} в собственном конструкторе и живёт до {@link close}.
|
|
22
|
+
* Само окно не знает, что в нём показывают, — только рамку, слои и закрытие.
|
|
23
|
+
*
|
|
24
|
+
* Рендер содержимого намеренно не вызывается отсюда: до возврата из `super()` поля наследника
|
|
25
|
+
* ещё не объявлены, и с `useDefineForClassFields` их объявления затирают всё, что успел
|
|
26
|
+
* присвоить вызванный из конструктора метод.
|
|
27
|
+
*/
|
|
28
|
+
export default abstract class Modal extends UIElement {
|
|
29
|
+
private readonly __body: HTMLElement;
|
|
30
|
+
private readonly __onKeyDown: (e: KeyboardEvent) => void;
|
|
31
|
+
private __closed = false;
|
|
32
|
+
private __disposed = false;
|
|
33
|
+
|
|
34
|
+
get typeName(): string {
|
|
35
|
+
return "BrandUp.Modal";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
constructor(options: ModalOptions = {}) {
|
|
39
|
+
super();
|
|
40
|
+
|
|
41
|
+
const body = DOM.tag("div", { class: "modal-body" });
|
|
42
|
+
|
|
43
|
+
// закрытие — через систему команд, как у остальных контролов кита; крестик и подложка
|
|
44
|
+
// объявляют одну и ту же команду, обработчик находит её по ближайшему data-command
|
|
45
|
+
const root = DOM.tag("div", { class: [MODAL_CLASS].concat(options.className ? [options.className] : []) }, [
|
|
46
|
+
DOM.tag(
|
|
47
|
+
"div",
|
|
48
|
+
options.closeOnBackdrop === false
|
|
49
|
+
? { class: "modal-backdrop" }
|
|
50
|
+
: { class: "modal-backdrop", "data-command": MODAL_CLOSE_COMMAND }
|
|
51
|
+
),
|
|
52
|
+
DOM.tag("div", { class: "modal-window", role: "dialog", "aria-modal": "true" }, [
|
|
53
|
+
DOM.tag("div", { class: "modal-header" }, [
|
|
54
|
+
options.title ? DOM.tag("div", { class: "modal-title" }, options.title) : null,
|
|
55
|
+
DOM.tag(
|
|
56
|
+
"button",
|
|
57
|
+
{ type: "button", class: "modal-close", title: "Закрыть", "data-command": MODAL_CLOSE_COMMAND },
|
|
58
|
+
"×"
|
|
59
|
+
),
|
|
60
|
+
]),
|
|
61
|
+
body,
|
|
62
|
+
]),
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
this.__body = body;
|
|
66
|
+
this.setElement(root);
|
|
67
|
+
|
|
68
|
+
this.registerCommand(MODAL_CLOSE_COMMAND, () => this.close());
|
|
69
|
+
|
|
70
|
+
// Esc слушаем на документе: фокус может быть где угодно внутри окна
|
|
71
|
+
this.__onKeyDown = (e: KeyboardEvent) => {
|
|
72
|
+
if (e.key === "Escape") {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
this.close();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
document.addEventListener("keydown", this.__onKeyDown);
|
|
78
|
+
|
|
79
|
+
document.body.appendChild(root);
|
|
80
|
+
document.body.classList.add(MODAL_OPENED_CLASS);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Вызывается перед закрытием — наследнику отдать результат или прибрать за собой. */
|
|
84
|
+
protected onClose(): void {
|
|
85
|
+
// по умолчанию ничего
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected get body(): HTMLElement {
|
|
89
|
+
return this.__body;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
close(): void {
|
|
93
|
+
if (this.__closed) return; // закрыть могут и крестиком, и Esc, и подложкой
|
|
94
|
+
this.__closed = true;
|
|
95
|
+
|
|
96
|
+
this.onClose();
|
|
97
|
+
this.destroy();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override destroy(): void {
|
|
101
|
+
// Придти сюда могут дважды: из close() и следом от авто-уничтожения по удалению элемента
|
|
102
|
+
// из DOM (UIElement подписан на MutationObserver). Второй проход обращался бы к уже
|
|
103
|
+
// снятому элементу.
|
|
104
|
+
if (this.__disposed) return;
|
|
105
|
+
this.__disposed = true;
|
|
106
|
+
|
|
107
|
+
document.removeEventListener("keydown", this.__onKeyDown);
|
|
108
|
+
document.body.classList.remove(MODAL_OPENED_CLASS);
|
|
109
|
+
|
|
110
|
+
this.element?.remove();
|
|
111
|
+
|
|
112
|
+
super.destroy();
|
|
113
|
+
}
|
|
114
|
+
}
|
package/source/styles.less
CHANGED