webcimes-modal 1.0.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.
@@ -0,0 +1,502 @@
1
+ /**
2
+ * Copyright (c) 2023 WebCimes - RICHARD Florian (https://webcimes.com)
3
+ * MIT License - https://choosealicense.com/licenses/mit/
4
+ * Date: 2023-03-25
5
+ */
6
+
7
+ "use strict";
8
+
9
+ /**
10
+ * Options
11
+ */
12
+ interface Options {
13
+ /** set a specific id on the modal. default "null" */
14
+ setId: string | null;
15
+ /** set a specific class on the modal, default "null" */
16
+ setClass: string | null;
17
+ /** width (specify unit), default "auto" */
18
+ width: string;
19
+ /** height (specify unit), default "auto" */
20
+ height: string;
21
+ /** html for title, default "null" */
22
+ titleHtml: string | null;
23
+ /** html for body, default "null" */
24
+ bodyHtml: string | null;
25
+ /** html for cancel button, default "null" */
26
+ buttonCancelHtml: string | null;
27
+ /** html for confirm button, default "null" */
28
+ buttonConfirmHtml: string | null;
29
+ /** close modal after trigger cancel button, default "true" */
30
+ closeOnCancelButton: boolean;
31
+ /** close modal after trigger confirm button, default "true" */
32
+ closeOnConfirmButton: boolean;
33
+ /** show close button, default "true" */
34
+ showCloseButton: boolean;
35
+ /** allow the modal to close when clicked outside, default "true" */
36
+ allowCloseOutside: boolean;
37
+ /** ability to move modal, default "true" */
38
+ allowMovement: boolean;
39
+ /** if allowMovement is set to "true", ability to move modal from header, default "true" */
40
+ moveFromHeader: boolean;
41
+ /** if allowMovement is set to "true", ability to move modal from body, default "false" */
42
+ moveFromBody: boolean;
43
+ /** if allowMovement is set to "true", ability to move modal from footer, default "true" */
44
+ moveFromFooter: boolean;
45
+ /** keep header sticky (visible) when scrolling, default "true" */
46
+ stickyHeader: boolean;
47
+ /** keep footer sticky (visible) when scrolling, default "true" */
48
+ stickyFooter: boolean;
49
+ /** add extra css style to modal, default null */
50
+ style: string | null;
51
+ /** "animDropDown" or "animFadeIn" for show animation, default "animDropDown" */
52
+ animationOnShow: "animDropDown" | "animFadeIn";
53
+ /** "animDropUp" or "animFadeOut" for destroy animation, default "animDropUp" */
54
+ animationOnDestroy: "animDropUp" | "animFadeOut";
55
+ /** animation duration in ms, default "500" */
56
+ animationDuration: number;
57
+ /** callback before show modal */
58
+ beforeShow: () => void;
59
+ /** callback after show modal */
60
+ afterShow: () => void;
61
+ /** callback before destroy modal */
62
+ beforeDestroy: () => void;
63
+ /** callback after destroy modal */
64
+ afterDestroy: () => void;
65
+ /** callback after triggering cancel button */
66
+ onCancelButton: () => void;
67
+ /** callback after triggering confirm button */
68
+ onConfirmButton: () => void;
69
+ }
70
+
71
+ /**
72
+ * Class WebcimesModal
73
+ */
74
+ export class WebcimesModal
75
+ {
76
+ private webcimesModals: HTMLElement;
77
+ private modal: HTMLElement;
78
+ private options: Options;
79
+ private eventCancelButton: () => void = () => {
80
+ // Callback on cancel button
81
+ if(typeof this.options.onCancelButton === 'function')
82
+ {
83
+ this.options.onCancelButton();
84
+ }
85
+ };
86
+ private eventConfirmButton: () => void = () => {
87
+ // Callback on confirm button
88
+ if(typeof this.options.onConfirmButton === 'function')
89
+ {
90
+ this.options.onConfirmButton();
91
+ }
92
+ };
93
+ private eventClickOutside: (e: Event) => void = (e) => {
94
+ if(e.target == this.webcimesModals)
95
+ {
96
+ if(this.options.allowCloseOutside)
97
+ {
98
+ // Destroy modal
99
+ this.destroy();
100
+ }
101
+ else
102
+ {
103
+ // Add animation for show modal who can't be close
104
+ this.modal.classList.add("animGrowShrink");
105
+
106
+ // Delete animation after the animation delay
107
+ setTimeout(() => {
108
+ this.modal.classList.remove("animGrowShrink");
109
+ }, this.options.animationDuration);
110
+ }
111
+ }
112
+ };
113
+ private eventClickCloseButton: () => void = () => {
114
+ // Destroy modal
115
+ this.destroy();
116
+ };
117
+ private eventDragModalOnTop: (e: Event) => void = (e) => {
118
+ // Only if target is not close button (for bug in chrome)
119
+ if(!(<HTMLElement>e.target).closest(".close"))
120
+ {
121
+ // If multiple modal, and modal not already on top (no next sibling), we place the current modal on the top
122
+ if(document.querySelectorAll(".modal").length > 1 && this.modal.nextElementSibling !== null)
123
+ {
124
+ let oldScrollTop = this.modal.scrollTop;
125
+ this.webcimesModals.insertAdjacentElement("beforeend", this.modal);
126
+ this.modal.scrollTop = oldScrollTop;
127
+ }
128
+ }
129
+ };
130
+ private position: {x: number, y: number};
131
+ private offset: {x: number, y: number};
132
+ private isDragging: boolean = false;
133
+ private moveFromElements: HTMLElement[] = [];
134
+ private eventDragStart: (e: Event) => void = (e) => {
135
+ // Start drag only if it's not a button
136
+ if(!(<HTMLElement>e.target).closest("button"))
137
+ {
138
+ this.isDragging = true;
139
+
140
+ // Mouse
141
+ if((<MouseEvent>e).clientX)
142
+ {
143
+ this.offset = {
144
+ x: this.modal.offsetLeft - (<MouseEvent>e).clientX,
145
+ y: this.modal.offsetTop - (<MouseEvent>e).clientY
146
+ };
147
+ }
148
+ // Touch device (use the first touch only)
149
+ else if((<TouchEvent>e).touches)
150
+ {
151
+ this.offset = {
152
+ x: this.modal.offsetLeft - (<TouchEvent>e).touches[0].clientX,
153
+ y: this.modal.offsetTop - (<TouchEvent>e).touches[0].clientY
154
+ };
155
+ }
156
+ }
157
+ };
158
+ private eventMove: (e: Event) => void = (e) => {
159
+ if(this.isDragging)
160
+ {
161
+ // Mouse
162
+ if((<MouseEvent>e).clientX)
163
+ {
164
+ this.position = {
165
+ x: (<MouseEvent>e).clientX,
166
+ y: (<MouseEvent>e).clientY
167
+ };
168
+ }
169
+ // Touch device (use the first touch only)
170
+ else if((<TouchEvent>e).touches)
171
+ {
172
+ this.position = {
173
+ x: (<TouchEvent>e).touches[0].clientX,
174
+ y: (<TouchEvent>e).touches[0].clientY
175
+ };
176
+ }
177
+ this.modal.style.left = (this.position.x + this.offset.x)+'px';
178
+ this.modal.style.top = (this.position.y + this.offset.y)+'px';
179
+ }
180
+ };
181
+ private eventDragStop: () => void = () => {
182
+ this.isDragging = false;
183
+ };
184
+ private eventPreventSelectText: (e: Event) => void = (e) => {
185
+ if(this.isDragging)
186
+ {
187
+ e.preventDefault();
188
+ }
189
+ };
190
+
191
+ /**
192
+ * Create modal
193
+ * @param {Object} options
194
+ * @param {string | null} options.setId - set a specific id on the modal. default "null"
195
+ * @param {string | null} options.setClass - set a specific class on the modal, default "null"
196
+ * @param {string} options.width - width (specify unit), default "auto"
197
+ * @param {string} options.height - height (specify unit), default "auto"
198
+ * @param {string | null} options.titleHtml - html for title, default "null"
199
+ * @param {string | null} options.bodyHtml - html for body, default "null"
200
+ * @param {string | null} options.buttonCancelHtml - html for cancel button, default "null"
201
+ * @param {string | null} options.buttonConfirmHtml - html for confirm button, default "null"
202
+ * @param {boolean} options.closeOnCancelButton - close modal after trigger cancel button, default "true"
203
+ * @param {boolean} options.closeOnConfirmButton - close modal after trigger confirm button, default "true"
204
+ * @param {boolean} options.showCloseButton - show close button, default "true"
205
+ * @param {boolean} options.allowCloseOutside - allow the modal to close when clicked outside, default "true"
206
+ * @param {boolean} options.allowMovement - ability to move modal, default "true"
207
+ * @param {boolean} options.moveFromHeader - if allowMovement is set to "true", ability to move modal from header, default "true"
208
+ * @param {boolean} options.moveFromBody - if allowMovement is set to "true", ability to move modal from body, default "false"
209
+ * @param {boolean} options.moveFromFooter - if allowMovement is set to "true", ability to move modal from footer, default "true"
210
+ * @param {boolean} options.stickyHeader - keep header sticky (visible) when scrolling, default "true"
211
+ * @param {boolean} options.stickyFooter - keep footer sticky (visible) when scrolling, default "true"
212
+ * @param {string | null} options.style - add extra css style to modal, default null
213
+ * @param {"animDropDown" | "animFadeIn"} options.animationOnShow - "animDropDown" or "animFadeIn" for show animation, default "animDropDown"
214
+ * @param {"animDropUp" | "animFadeOut"} options.animationOnDestroy - "animDropUp" or "animFadeOut" for destroy animation, default "animDropUp"
215
+ * @param {number} options.animationDuration - animation duration in ms, default "500"
216
+ * @param {() => void} options.beforeShow - callback before show modal
217
+ * @param {() => void} options.afterShow - callback after show modal
218
+ * @param {() => void} options.beforeDestroy - callback before destroy modal
219
+ * @param {() => void} options.afterDestroy - callback after destroy modal
220
+ * @param {() => void} options.onCancelButton - callback after triggering cancel button
221
+ * @param {() => void} options.onConfirmButton - callback after triggering confirm button
222
+ */
223
+ constructor(options: Options)
224
+ {
225
+ // Defaults
226
+ const defaults: Options = {
227
+ setId: null,
228
+ setClass: null,
229
+ width: 'auto',
230
+ height: 'auto',
231
+ titleHtml: null,
232
+ bodyHtml: null,
233
+ buttonCancelHtml: null,
234
+ buttonConfirmHtml: null,
235
+ closeOnCancelButton: true,
236
+ closeOnConfirmButton: true,
237
+ showCloseButton: true,
238
+ allowCloseOutside: true,
239
+ allowMovement: true,
240
+ moveFromHeader: true,
241
+ moveFromBody: false,
242
+ moveFromFooter: true,
243
+ stickyHeader: true,
244
+ stickyFooter: true,
245
+ style: null,
246
+ animationOnShow: 'animDropDown',
247
+ animationOnDestroy: 'animDropUp',
248
+ animationDuration: 500,
249
+ beforeShow: () => {},
250
+ afterShow: () => {},
251
+ beforeDestroy: () => {},
252
+ afterDestroy: () => {},
253
+ onCancelButton: () => {},
254
+ onConfirmButton: () => {},
255
+ }
256
+ this.options = {...defaults, ...options};
257
+
258
+ // Call init method
259
+ this.init();
260
+ }
261
+
262
+ /**
263
+ * Init modal
264
+ */
265
+ init()
266
+ {
267
+ // Create webcimesModals
268
+ if(!document.querySelector(".webcimesModals"))
269
+ {
270
+ // Create webcimesModals
271
+ document.body.insertAdjacentHTML("beforeend", '<div class="webcimesModals animFadeIn"></div>');
272
+ this.webcimesModals = <HTMLElement>document.querySelector(".webcimesModals");
273
+
274
+ // Set animation duration for webcimesModals
275
+ this.webcimesModals.style.setProperty("animation-duration", this.options.animationDuration+"ms");
276
+
277
+ // Delete enter animation after animation delay
278
+ setTimeout(() => {
279
+ this.webcimesModals.classList.remove("animFadeIn");
280
+ }, this.options.animationDuration);
281
+ }
282
+ else
283
+ {
284
+ this.webcimesModals = <HTMLElement>document.querySelector(".webcimesModals");
285
+ }
286
+
287
+ // Callback before show modal
288
+ if(typeof this.options.beforeShow === 'function')
289
+ {
290
+ this.options.beforeShow();
291
+ }
292
+
293
+ // Create modal
294
+ this.webcimesModals.insertAdjacentHTML("beforeend",
295
+ `<div class="modal `+(this.options.setClass?this.options.setClass:'')+` `+this.options.animationOnShow+`" `+(this.options.setClass?'id="'+this.options.setId+'"':'')+`>
296
+ `+(this.options.titleHtml||this.options.showCloseButton?
297
+ `<div class="modalHeader `+(this.options.stickyHeader?'sticky':'')+` `+(this.options.moveFromHeader?'movable':'')+`">
298
+ `+(this.options.titleHtml?'<div class="title">'+this.options.titleHtml+'</div>':'')+`
299
+ `+(this.options.showCloseButton?'<button class="close"></button>':'')+`
300
+ </div>`
301
+ :'')+`
302
+ `+(this.options.bodyHtml?
303
+ `<div class="modalBody `+(this.options.moveFromBody?'movable':'')+`">
304
+ `+this.options.bodyHtml+`
305
+ </div>`
306
+ :'')+`
307
+ `+(this.options.buttonCancelHtml||this.options.buttonConfirmHtml?
308
+ `<div class="modalFooter `+(this.options.stickyFooter?'sticky':'')+` `+(this.options.moveFromFooter?'movable':'')+`">
309
+ `+(this.options.buttonCancelHtml?'<button class="cancel '+(this.options.closeOnCancelButton?'close':'')+'">'+this.options.buttonCancelHtml+'</button>':'')+`
310
+ `+(this.options.buttonConfirmHtml?'<button class="confirm '+(this.options.closeOnConfirmButton?'close':'')+'">'+this.options.buttonConfirmHtml+'</button>':'')+`
311
+ </div>`
312
+ :'')+`
313
+ </div>`
314
+ );
315
+ this.modal = <HTMLElement>this.webcimesModals.lastElementChild;
316
+
317
+ // Set animation duration for modal
318
+ this.modal.style.setProperty("animation-duration", this.options.animationDuration+"ms");
319
+
320
+ // Delete animation of enter after the animation delay
321
+ setTimeout(() => {
322
+ this.modal.classList.remove(this.options.animationOnShow);
323
+
324
+ // Callback after show modal
325
+ if(typeof this.options.afterShow === 'function')
326
+ {
327
+ this.options.afterShow();
328
+ }
329
+ }, this.options.animationDuration);
330
+
331
+ // Width of modal
332
+ this.modal.style.setProperty("max-width", "90%");
333
+ if(this.options.width != "auto" && this.options.width)
334
+ {
335
+ this.modal.style.setProperty("width", this.options.width);
336
+ }
337
+ else
338
+ {
339
+ // "max-content" is for keep size in "auto" and for maximum to max-width
340
+ this.modal.style.setProperty("width", "max-content");
341
+ }
342
+
343
+ // Height of modal
344
+ this.modal.style.setProperty("max-height", "90%");
345
+ if(this.options.height != "auto" && this.options.height)
346
+ {
347
+ this.modal.style.setProperty("height", this.options.height);
348
+ }
349
+ else
350
+ {
351
+ // "max-content" is for keep size in "auto" and for maximum to max-height
352
+ this.modal.style.setProperty("height", "max-content");
353
+ }
354
+
355
+ // Style
356
+ if(this.options.style)
357
+ {
358
+ let oldStyle = this.modal.getAttribute("style");
359
+ this.modal.setAttribute("style", oldStyle+this.options.style);
360
+ }
361
+
362
+ // Event on cancel button
363
+ if(this.options.buttonCancelHtml)
364
+ {
365
+ this.modal.querySelector(".cancel")?.addEventListener("click", this.eventCancelButton);
366
+ }
367
+
368
+ // Event on confirm button
369
+ if(this.options.buttonConfirmHtml)
370
+ {
371
+ this.modal.querySelector(".confirm")?.addEventListener("click", this.eventConfirmButton);
372
+ }
373
+
374
+ // Event click outside (on webcimesModals)
375
+ this.webcimesModals.addEventListener("click", this.eventClickOutside);
376
+
377
+ // Event close modal when click on close button
378
+ this.modal.querySelectorAll(".close").forEach((el) => {
379
+ el.addEventListener("click", this.eventClickCloseButton);
380
+ });
381
+
382
+ // Place selected modal on top
383
+ ['mousedown', 'touchstart'].forEach((typeEvent) => {
384
+ this.modal.addEventListener(typeEvent, this.eventDragModalOnTop);
385
+ });
386
+
387
+ // Move modal
388
+ if(this.options.allowMovement && (this.options.moveFromHeader || this.options.moveFromBody || this.options.moveFromFooter))
389
+ {
390
+ if(this.options.moveFromHeader && this.modal.querySelector(".modalHeader"))
391
+ {
392
+ this.moveFromElements.push(<HTMLElement>this.modal.querySelector(".modalHeader"));
393
+ }
394
+ if(this.options.moveFromBody && this.modal.querySelector(".modalBody"))
395
+ {
396
+ this.moveFromElements.push(<HTMLElement>this.modal.querySelector(".modalBody"));
397
+ }
398
+ if(this.options.moveFromFooter && this.modal.querySelector(".modalFooter"))
399
+ {
400
+ this.moveFromElements.push(<HTMLElement>this.modal.querySelector(".modalFooter"));
401
+ }
402
+
403
+ ['mousedown', 'touchstart'].forEach((typeEvent) => {
404
+ this.moveFromElements.forEach((el) => {
405
+ el.addEventListener(typeEvent, this.eventDragStart);
406
+ });
407
+ });
408
+
409
+ ['mousemove', 'touchmove'].forEach((typeEvent) => {
410
+ document.addEventListener(typeEvent, this.eventMove);
411
+ });
412
+
413
+ ['mouseup', 'touchend'].forEach((typeEvent) => {
414
+ document.addEventListener(typeEvent, this.eventDragStop);
415
+ });
416
+
417
+ document.addEventListener("selectstart", this.eventPreventSelectText);
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Destroy modal
423
+ */
424
+ destroy()
425
+ {
426
+ // If modal is not already destroying
427
+ if(!this.modal.getAttribute("data-destroying"))
428
+ {
429
+ // Callback before destroy modal
430
+ if(typeof this.options.beforeDestroy === 'function')
431
+ {
432
+ this.options.beforeDestroy();
433
+ }
434
+
435
+ // Close webcimesModals (according the number of modal not already destroying)
436
+ if(document.querySelectorAll(".modal:not([data-destroying])").length == 1)
437
+ {
438
+ this.webcimesModals.classList.add("animFadeOut");
439
+ }
440
+
441
+ // Close modal
442
+ this.modal.setAttribute("data-destroying", "1");
443
+ this.modal.classList.add(this.options.animationOnDestroy);
444
+
445
+ // Destroy all events from modal and remove webcimesModals or modal after animation duration
446
+ setTimeout(() => {
447
+ if(typeof this.modal !== 'undefined')
448
+ {
449
+ // Destroy all events from modal
450
+
451
+ if(this.options.buttonCancelHtml)
452
+ {
453
+ this.modal.querySelector(".cancel")?.removeEventListener("click", this.eventCancelButton);
454
+ }
455
+
456
+ if(this.options.buttonConfirmHtml)
457
+ {
458
+ this.modal.querySelector(".confirm")?.removeEventListener("click", this.eventConfirmButton);
459
+ }
460
+
461
+ this.webcimesModals.removeEventListener("click", this.eventClickOutside);
462
+
463
+ this.modal.querySelectorAll(".close").forEach((el) => {
464
+ el.removeEventListener("click", this.eventClickCloseButton);
465
+ });
466
+
467
+ ['mousedown', 'touchstart'].forEach((typeEvent) => {
468
+ this.modal.removeEventListener(typeEvent, this.eventDragModalOnTop);
469
+ });
470
+
471
+ if(this.options.allowMovement && (this.options.moveFromHeader || this.options.moveFromBody || this.options.moveFromFooter))
472
+ {
473
+ ['mousedown', 'touchstart'].forEach((typeEvent) => {
474
+ this.moveFromElements.forEach((el) => {
475
+ el.removeEventListener(typeEvent, this.eventDragStart);
476
+ });
477
+ });
478
+
479
+ ['mousemove', 'touchmove'].forEach((typeEvent) => {
480
+ document.removeEventListener(typeEvent, this.eventMove);
481
+ });
482
+
483
+ ['mouseup', 'touchend'].forEach((typeEvent) => {
484
+ document.removeEventListener(typeEvent, this.eventDragStop);
485
+ });
486
+
487
+ document.removeEventListener("selectstart", this.eventPreventSelectText);
488
+ }
489
+
490
+ // Remove webcimesModals or modal according the number of modal
491
+ (document.querySelectorAll(".modal").length>1?this.modal:this.webcimesModals).remove();
492
+ }
493
+
494
+ // Callback after destroy modal
495
+ if(typeof this.options.afterDestroy === 'function')
496
+ {
497
+ this.options.afterDestroy();
498
+ }
499
+ }, this.options.animationDuration);
500
+ }
501
+ }
502
+ }