@web-atoms/core 2.2.49 → 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.
- package/dist/core/Command.js +1 -1
- package/dist/core/Command.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/web/services/PopupService.d.ts +4 -20
- package/dist/web/services/PopupService.d.ts.map +1 -1
- package/dist/web/services/PopupService.js +6 -224
- package/dist/web/services/PopupService.js.map +1 -1
- package/dist/web/services/PopupWindow.d.ts +45 -0
- package/dist/web/services/PopupWindow.d.ts.map +1 -0
- package/dist/web/services/PopupWindow.js +378 -0
- package/dist/web/services/PopupWindow.js.map +1 -0
- package/package.json +1 -1
- package/src/core/Command.ts +1 -1
- package/src/web/services/PopupService.tsx +350 -346
- package/src/web/services/PopupWindow.tsx +456 -0
|
@@ -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
|
+
}
|