@web-atoms/core 2.2.50 → 2.2.51

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.
@@ -0,0 +1,456 @@
1
+ import Bind from "../../core/Bind";
2
+ import { BindableProperty } from "../../core/BindableProperty";
3
+ import XNode from "../../core/XNode";
4
+ import sleep from "../../core/sleep";
5
+ import { IClassOf, IDisposable, IRect } from "../../core/types";
6
+ import styled from "../../style/styled";
7
+ import { AtomControl } from "../controls/AtomControl";
8
+ import { ChildEnumerator } from "../core/AtomUI";
9
+ import type PopupService from "./PopupService";
10
+ import type { IDialogOptions } from "./PopupService";
11
+
12
+ let popupService: typeof PopupService;
13
+
14
+ const loadPopupService = async () => {
15
+ if (popupService) {
16
+ return popupService;
17
+ }
18
+ return popupService = (await (import("./PopupService"))).default;
19
+ };
20
+
21
+ styled.css `
22
+ position: absolute;
23
+ border: solid 1px lightgray;
24
+ border-radius: 5px;
25
+ background-color: white;
26
+ top: 50%;
27
+ left: 50%;
28
+ transform: translate(-50%, -50%);
29
+ box-shadow: 0 0 20px 1px rgba(0 0 0 / 75%);
30
+
31
+ display: grid;
32
+ align-items: center;
33
+ justify-items: center;
34
+ grid-template-rows: auto auto 1fr auto;
35
+ grid-template-columns: auto 1fr auto;
36
+ opacity: 0;
37
+ transition: opacity 0.3s cubic-bezier(0.55, 0.09, 0.97, 0.32) ;
38
+
39
+ &[data-ready=true] {
40
+ opacity: 1;
41
+ }
42
+ &[data-dragging=true] {
43
+ opacity: 0.5;
44
+ }
45
+ & > [data-window-element=icon] {
46
+ grid-row: 1;
47
+ grid-column: 1;
48
+ }
49
+ & > [data-window-element=title] {
50
+ grid-row: 1;
51
+ grid-column: 2;
52
+ font-size: medium;
53
+ overflow: hidden;
54
+ white-space: nowrap;
55
+ text-overflow: ellipsis;
56
+ cursor: move;
57
+ padding: var(--spacing, 5px);
58
+ }
59
+ & > [data-window-element=close-button] {
60
+ grid-row: 1;
61
+ grid-column: 3;
62
+ }
63
+ & > [data-window-element=action-bar] {
64
+ grid-row: 1;
65
+ grid-column: 1 / span 3;
66
+ align-self: stretch;
67
+ justify-self: stretch;
68
+ background-color: var(--accent-color, rgba(211, 211, 211, 0.2))
69
+ }
70
+ & > [data-window-element=header] {
71
+ margin-top: 5px;
72
+ grid-row: 2;
73
+ grid-column: 1 / span 3;
74
+ }
75
+ & > [data-window-element=content] {
76
+ margin-top: 5px;
77
+ grid-row: 3;
78
+ grid-column: 1 / span 3;
79
+ position: relative;
80
+ }
81
+ & > [data-window-element=footer] {
82
+ margin-top: 5px;
83
+ grid-row: 3;
84
+ grid-column: 1 / span 3;
85
+ margin-bottom: 5px;
86
+ }
87
+
88
+ `.installGlobal("[data-popup-window=popup=window]")
89
+
90
+ export default class PopupWindow extends AtomControl {
91
+
92
+ public static async showWindow<T>(options?: IDialogOptions): Promise<T>;
93
+ public static async showWindow<T>(window: IClassOf<PopupWindow>, options?: IDialogOptions): Promise<T>;
94
+ public static async showWindow<T>(
95
+ window: IClassOf<PopupWindow> | IDialogOptions,
96
+ options?: IDialogOptions): Promise<T> {
97
+ if (arguments.length <= 1) {
98
+ options = arguments[0];
99
+ window = this;
100
+ }
101
+ // this will force lastTarget to be set
102
+ await sleep(1);
103
+ const PS = await loadPopupService();
104
+ return PS.showWindow<T>(PS.lastTarget, window as any, options);
105
+ }
106
+
107
+ public static async showModal<T>(options?: IDialogOptions): Promise<T>;
108
+ public static async showModal<T>(window: IClassOf<PopupWindow>, options?: IDialogOptions): Promise<T>;
109
+ public static async showModal<T>(
110
+ window: IClassOf<PopupWindow> | IDialogOptions,
111
+ options?: IDialogOptions): Promise<T> {
112
+ if (arguments.length <= 1) {
113
+ options = arguments[0];
114
+ window = this;
115
+ }
116
+ options ??= {};
117
+ options.modal ??= true;
118
+ // this will force lastTarget to be set
119
+ await sleep(1);
120
+ const PS = await loadPopupService();
121
+ return PS.showWindow<T>(PS.lastTarget, window as any, options);
122
+ }
123
+
124
+
125
+ @BindableProperty
126
+ public title?: string;
127
+
128
+ public viewModelTitle?: string;
129
+
130
+ public close: (r?) => void;
131
+
132
+ public cancel: (r?) => void;
133
+
134
+ public titleRenderer: () => XNode;
135
+
136
+ public closeButtonRenderer: () => XNode;
137
+
138
+ public footerRenderer: () => XNode;
139
+
140
+ public headerRenderer: () => XNode;
141
+
142
+ public iconRenderer: () => XNode;
143
+
144
+ public actionBarRenderer: () => XNode;
145
+
146
+ @BindableProperty
147
+ public closeWarning: string;
148
+
149
+ public onPropertyChanged(name) {
150
+ super.onPropertyChanged(name);
151
+ switch (name as keyof PopupWindow) {
152
+ case "iconRenderer":
153
+ this.recreate(name, "icon");
154
+ break;
155
+ case "actionBarRenderer":
156
+ this.recreate(name, "action-bar");
157
+ break;
158
+ case "footerRenderer":
159
+ this.recreate(name, "footer");
160
+ break;
161
+ case "titleRenderer":
162
+ this.recreate(name, "title");
163
+ break;
164
+ case "headerRenderer":
165
+ this.recreate(name, "header");
166
+ break;
167
+ }
168
+ }
169
+
170
+ protected init(): any {
171
+ // do nothing...
172
+ }
173
+
174
+ protected async requestCancel() {
175
+ if (this.closeWarning) {
176
+ if (!await ConfirmPopup.confirm({
177
+ message : this.closeWarning
178
+ })) {
179
+ return;
180
+ }
181
+ }
182
+ this.cancel();
183
+ }
184
+
185
+ protected recreate(renderer, name): HTMLElement {
186
+ const node = this[renderer]?.() ?? undefined;
187
+ for (const e of ChildEnumerator.enumerate(this.element)) {
188
+ if (e.dataset.pageElement === name) {
189
+ this.dispose(e);
190
+ e.remove();
191
+ break;
192
+ }
193
+ }
194
+ if (node) {
195
+ const na = node.attributes ??= {};
196
+ na["data-window-element"] = name;
197
+ super.render(<div>{node}</div>);
198
+ return this.element.querySelector(`[data-window-element="${name}"]`);
199
+ }
200
+ return null;
201
+ }
202
+
203
+ protected preCreate(): void {
204
+ this.title = null;
205
+ this.viewModelTitle = null;
206
+ const handler = (e: KeyboardEvent) => {
207
+ if (e.key === "Escape") {
208
+ this.app.runAsync(() => this.requestCancel());
209
+ e.preventDefault();
210
+ return;
211
+ }
212
+ };
213
+ this.bindEvent(this.element, "keydown", handler);
214
+ // document.body.addEventListener("keydown", handler);
215
+ // this.registerDisposable({
216
+ // dispose() {
217
+ // document.body.removeEventListener("keydown", handler);
218
+ // }
219
+ // });
220
+ this.element.dataset.popupWindow = "popup-window";
221
+
222
+ this.runAfterInit(() => this.app.runAsync(() => this.init()));
223
+
224
+ setTimeout((p) => {
225
+ p.dataset.ready = "true";
226
+ }, 10, this.element);
227
+ }
228
+
229
+ protected render(node: XNode, e?: any, creator?: any): void {
230
+ this.render = super.render;
231
+ const titleContent = this.titleRenderer?.() ?? <span
232
+ class="title-text" text={Bind.oneWay(() => this.title || this.viewModelTitle)}/>;
233
+ const closeButton = this.closeButtonRenderer?.() ?? <button
234
+ class="popup-close-button"
235
+ text="x"
236
+ eventClick={Bind.event(() => this.requestCancel())}/>;
237
+ const a = node.attributes ??= {};
238
+ a["data-window-content"] = "window-content";
239
+ a["data-window-element"] = "content";
240
+ titleContent["data-window-element"] = "title";
241
+ closeButton["data-window-element"] = "close";
242
+ const extracted = this.extractControlProperties(node);
243
+ super.render(<div
244
+ viewModelTitle={Bind.oneWay(() => this.viewModel.title)}
245
+ { ... extracted }>
246
+ <div data-window-element="action-bar"></div>
247
+ { titleContent }
248
+ { closeButton }
249
+ { node }
250
+ </div>);
251
+
252
+ this.runAfterInit(() => {
253
+ if (!this.element) {
254
+ return;
255
+ }
256
+ const host = this.element.getElementsByClassName("title-host")[0];
257
+ this.setupDragging(host as HTMLElement);
258
+ // this.element may become null if it was immediately
259
+ // closed, very rare case, but possible if
260
+ // supplied cancelToken was cancelled
261
+ const anyAutofocus = this.element.querySelector(`*[autofocus]`);
262
+ if (!anyAutofocus) {
263
+ const windowContent = this.element.querySelector("[data-window-content]");
264
+ if (windowContent) {
265
+ const firstInput = windowContent.querySelector("input,button,a") as HTMLInputElement;
266
+ if (firstInput) {
267
+ firstInput.focus();
268
+ return;
269
+ }
270
+ }
271
+
272
+ const cb = this.element.querySelector(".popup-close-button") as HTMLButtonElement;
273
+ if (cb) {
274
+ cb.focus();
275
+ }
276
+ return;
277
+ }
278
+ });
279
+ }
280
+
281
+ protected setupDragging(tp: HTMLElement): void {
282
+ this.bindEvent(tp, "mousedown", (startEvent: MouseEvent) => {
283
+ if ((startEvent.target as HTMLElement).tagName === "BUTTON") {
284
+ return;
285
+ }
286
+ startEvent.preventDefault();
287
+ const disposables: IDisposable[] = [];
288
+ // const offset = AtomUI.screenOffset(tp);
289
+ const element = this.element;
290
+ const offset = { x: element.offsetLeft, y: element.offsetTop };
291
+ if (element.style.transform !== "none") {
292
+ offset.x -= element.offsetWidth / 2;
293
+ offset.y -= element.offsetHeight / 2;
294
+ element.style.left = offset.x + "px";
295
+ element.style.top = offset.y + "px";
296
+ element.style.transform = "none";
297
+ }
298
+ this.element.dataset.dragging = "true";
299
+ const rect: IRect = { x: startEvent.clientX, y: startEvent.clientY };
300
+ const cursor = tp.style.cursor;
301
+ tp.style.cursor = "move";
302
+ disposables.push(this.bindEvent(document.body, "mousemove", (moveEvent: MouseEvent) => {
303
+ const { clientX, clientY } = moveEvent;
304
+ const dx = clientX - rect.x;
305
+ const dy = clientY - rect.y;
306
+
307
+ const finalX = offset.x + dx;
308
+ const finalY = offset.y + dy;
309
+ if (finalX < 5 || finalY < 5) {
310
+ return;
311
+ }
312
+
313
+ offset.x = finalX;
314
+ offset.y = finalY;
315
+
316
+ this.element.style.left = offset.x + "px";
317
+ this.element.style.top = offset.y + "px";
318
+
319
+ rect.x = clientX;
320
+ rect.y = clientY;
321
+ }));
322
+ disposables.push(this.bindEvent(document.body, "mouseup", (endEvent: MouseEvent) => {
323
+ tp.style.cursor = cursor;
324
+ this.element.removeAttribute("data-dragging");
325
+ for (const iterator of disposables) {
326
+ iterator.dispose();
327
+ }
328
+ }));
329
+ });
330
+ }
331
+
332
+
333
+ }
334
+
335
+ // @ts-ignore
336
+ delete PopupWindow.prototype.init;
337
+
338
+ styled.css `
339
+ & > .buttons[data-window-element=footer] > button{
340
+ border-radius: 9999px;
341
+ padding-left: 10px;
342
+ padding-right: 10px;
343
+ border-width: 1px;
344
+ border-color: transparent;
345
+ margin: 5px;
346
+ margin-right: 5px;
347
+ }
348
+ & > .buttons > .yes {
349
+ background-color: lightgreen;
350
+ color: white;
351
+ }
352
+ & > .buttons > .no {
353
+ background-color: red;
354
+ color: white;
355
+ }
356
+ & > .buttons > .cancel {
357
+ background-color: gray;
358
+ color: white;
359
+ }
360
+ `.installGlobal("[data-confirm-popup=confirm-popup]")
361
+
362
+ export class ConfirmPopup extends PopupWindow {
363
+
364
+ public static async confirm({
365
+ message,
366
+ title = "Confirm",
367
+ yesLabel = "Yes",
368
+ noLabel = "No",
369
+ cancelLabel = null
370
+ }): Promise<boolean> {
371
+ const PS = await loadPopupService();
372
+ return PS.confirm({ title, message, yesLabel, noLabel, cancelLabel});
373
+ }
374
+
375
+ public message: string;
376
+
377
+ public messageRenderer: () => XNode;
378
+
379
+ public yesLabel: string;
380
+
381
+ public noLabel: string;
382
+
383
+ public cancelLabel: string;
384
+
385
+ protected preCreate(): void {
386
+ super.preCreate();
387
+ this.yesLabel = "Yes";
388
+ this.noLabel = "No";
389
+ this.cancelLabel = null;
390
+
391
+ this.element.dataset.confirmPopup = "confirm-popup";
392
+
393
+ this.footerRenderer = () => <div>
394
+ <button
395
+ class="yes"
396
+ autofocus={true}
397
+ text={Bind.oneWay(() => this.yesLabel)}
398
+ eventClick={() => this.close(true)}
399
+ style-display={Bind.oneWay(() => !!this.yesLabel)}
400
+ />
401
+ <button
402
+ class="no"
403
+ text={Bind.oneWay(() => this.noLabel)}
404
+ eventClick={() => this.close(false)}
405
+ style-display={Bind.oneWay(() => !!this.noLabel)}
406
+ />
407
+ <button
408
+ class="cancel"
409
+ text={Bind.oneWay(() => this.cancelLabel)}
410
+ eventClick={() => this.requestCancel()}
411
+ style-display={Bind.oneWay(() => !!this.cancelLabel)}
412
+ />
413
+ </div>;
414
+
415
+ this.closeButtonRenderer = () => <div/>;
416
+ }
417
+
418
+ protected requestCancel(): Promise<void> {
419
+ this.cancel();
420
+ return Promise.resolve();
421
+ }
422
+
423
+ // protected render(node: XNode, e?: any, creator?: any) {
424
+ // this.render = super.render;
425
+ // this.element.dataset.confirmPopup = "confirm-popup";
426
+ // this.closeButtonRenderer = () => <div/>;
427
+ // const extracted = this.extractControlProperties(node);
428
+ // const na = node.attributes ??= {};
429
+ // na["data-element"] = "message";
430
+ // super.render(<div { ... extracted }>
431
+ // { node }
432
+ // <div data-element="buttons">
433
+ // <button
434
+ // class="yes"
435
+ // autofocus={true}
436
+ // text={Bind.oneWay(() => this.yesLabel)}
437
+ // eventClick={() => this.close(true)}
438
+ // style-display={Bind.oneWay(() => !!this.yesLabel)}
439
+ // />
440
+ // <button
441
+ // class="no"
442
+ // text={Bind.oneWay(() => this.noLabel)}
443
+ // eventClick={() => this.close(false)}
444
+ // style-display={Bind.oneWay(() => !!this.noLabel)}
445
+ // />
446
+ // <button
447
+ // class="cancel"
448
+ // text={Bind.oneWay(() => this.cancelLabel)}
449
+ // eventClick={() => this.requestCancel()}
450
+ // style-display={Bind.oneWay(() => !!this.cancelLabel)}
451
+ // />
452
+ // </div>
453
+ // </div>);
454
+ // }
455
+
456
+ }