@nuralyui/modal 0.0.3 → 0.0.4

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.
@@ -1,202 +1,448 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2023 Nuraly, Laabidi Aymen
4
+ * SPDX-License-Identifier: MIT
5
+ */
1
6
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
7
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
8
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
9
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
10
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
11
  };
7
- /* eslint-disable @typescript-eslint/no-explicit-any */
8
- import { css, html, LitElement } from 'lit';
9
- import { property, state } from 'lit/decorators.js';
12
+ import { html, LitElement, nothing } from 'lit';
13
+ import { customElement, property, state } from 'lit/decorators.js';
14
+ import { classMap } from 'lit/directives/class-map.js';
10
15
  import { styleMap } from 'lit/directives/style-map.js';
11
- export class ModalComponent extends LitElement {
16
+ import { ModalSize, ModalPosition, ModalAnimation, ModalBackdrop, EMPTY_STRING } from './modal.types.js';
17
+ import { styles } from './modal.style.js';
18
+ import { NuralyUIBaseMixin } from '../../shared/base-mixin.js';
19
+ import { ModalManager } from './modal-manager.js';
20
+ // Import icon component
21
+ import '../icon/icon.component.js';
22
+ import '../button/button.component.js';
23
+ // Import controllers
24
+ import { ModalDragController, ModalKeyboardController } from './controllers/index.js';
25
+ /**
26
+ * Versatile modal component with multiple sizes, animations, and enhanced functionality.
27
+ *
28
+ * @example
29
+ * ```html
30
+ * <!-- Simple usage -->
31
+ * <nr-modal open title="My Modal">
32
+ * <p>Modal content goes here</p>
33
+ * </nr-modal>
34
+ *
35
+ * <!-- With custom configuration -->
36
+ * <nr-modal
37
+ * open
38
+ * size="large"
39
+ * position="top"
40
+ * animation="zoom"
41
+ * backdrop="static"
42
+ * draggable>
43
+ * <div slot="header">
44
+ * <nr-icon name="info"></nr-icon>
45
+ * <span>Custom Header</span>
46
+ * </div>
47
+ * <p>Modal content</p>
48
+ * <div slot="footer">
49
+ * <nr-button type="secondary">Cancel</nr-button>
50
+ * <nr-button type="primary">OK</nr-button>
51
+ * </div>
52
+ * </nr-modal>
53
+ * ```
54
+ *
55
+ * @fires modal-open - Modal opened
56
+ * @fires modal-close - Modal closed
57
+ * @fires modal-before-close - Before modal closes (cancelable)
58
+ * @fires modal-after-open - After modal opens
59
+ * @fires modal-escape - Escape key pressed
60
+ *
61
+ * @slot default - Modal body content
62
+ * @slot header - Custom header content
63
+ * @slot footer - Custom footer content
64
+ */
65
+ let NrModalElement = class NrModalElement extends NuralyUIBaseMixin(LitElement) {
12
66
  constructor() {
13
67
  super(...arguments);
14
- this.label = '';
68
+ /** Whether the modal is open */
69
+ this.open = false;
70
+ /** Modal size (small, medium, large, xl) */
71
+ this.size = ModalSize.Medium;
72
+ /** Modal position (center, top, bottom) */
73
+ this.position = ModalPosition.Center;
74
+ /** Animation type */
75
+ this.animation = ModalAnimation.Fade;
76
+ /** Backdrop behavior */
77
+ this.backdrop = ModalBackdrop.Closable;
78
+ /** Whether the modal can be closed */
79
+ this.closable = true;
80
+ /** Whether the modal can be dragged */
81
+ this.modalDraggable = false;
82
+ /** Whether the modal is resizable */
83
+ this.resizable = false;
84
+ /** Whether the modal is fullscreen */
85
+ this.fullscreen = false;
86
+ /** Modal title */
87
+ this.modalTitle = EMPTY_STRING;
88
+ /** Show close button in header */
89
+ this.showCloseButton = true;
90
+ /** Header icon */
91
+ this.headerIcon = EMPTY_STRING;
92
+ /** Z-index for the modal */
93
+ this.zIndex = 1000;
94
+ /** Custom width */
95
+ this.width = EMPTY_STRING;
96
+ /** Custom height */
97
+ this.height = EMPTY_STRING;
98
+ /** Dragging state */
99
+ this.isDragging = false;
100
+ /** Current X offset for dragging */
15
101
  this.offsetX = 0;
102
+ /** Current Y offset for dragging */
16
103
  this.offsetY = 0;
17
- this.isDragging = false;
18
- this.initialX = 0;
19
- this.initialY = 0;
104
+ /** Animation state */
105
+ this.animationState = 'closed';
106
+ /** Previous focus element */
107
+ this.previousActiveElement = null;
108
+ this.requiredComponents = ['nr-icon', 'nr-button'];
109
+ // Controllers
110
+ this.dragController = new ModalDragController(this);
111
+ this.keyboardController = new ModalKeyboardController(this);
112
+ this.handleBackdropClick = (event) => {
113
+ // Only allow backdrop close if this is the top modal and backdrop is closable
114
+ if (this.backdrop === ModalBackdrop.Closable &&
115
+ event.target === event.currentTarget &&
116
+ ModalManager.handleBackdropClick(this)) {
117
+ this.closeModal();
118
+ }
119
+ };
20
120
  }
21
121
  connectedCallback() {
22
122
  super.connectedCallback();
23
- // this.addEventListener('mousedown', this.startDrag);
24
- document.addEventListener('mousemove', this.drag.bind(this));
25
- // this.addEventListener('mouseup', this.stopDrag);
26
- document.addEventListener('keydown', this.handleKeyDown.bind(this));
27
- // document.addEventListener('click', this.handleOutsideClick.bind(this));
123
+ this.validateDependencies();
28
124
  }
29
125
  disconnectedCallback() {
30
126
  super.disconnectedCallback();
31
- // this.removeEventListener('mousedown', this.startDrag);
32
- document.removeEventListener('mousemove', this.drag);
33
- // this.removeEventListener('mouseup', this.stopDrag);
34
- document.removeEventListener('keydown', this.handleKeyDown.bind(this));
35
- document.removeEventListener('click', this.handleOutsideClick.bind(this));
36
- }
37
- startDrag(event) {
38
- if (event.target instanceof HTMLElement) {
39
- this.isDragging = true;
40
- this.initialX = event.clientX - this.offsetX;
41
- this.initialY = event.clientY - this.offsetY;
42
- event.preventDefault();
127
+ // Restore focus when modal is destroyed
128
+ if (this.previousActiveElement instanceof HTMLElement) {
129
+ this.previousActiveElement.focus();
43
130
  }
44
131
  }
45
- drag(event) {
46
- if (this.isDragging) {
47
- this.offsetX = event.clientX - this.initialX;
48
- this.offsetY = event.clientY - this.initialY;
49
- this.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px)`;
50
- event.preventDefault();
51
- }
52
- }
53
- handleKeyDown(event) {
54
- if (event.key === 'Escape') {
55
- this.closeModal();
56
- event.preventDefault();
132
+ willUpdate(changedProperties) {
133
+ super.willUpdate(changedProperties);
134
+ if (changedProperties.has('open')) {
135
+ if (this.open) {
136
+ this.handleOpen();
137
+ }
138
+ else {
139
+ this.handleClose();
140
+ }
57
141
  }
58
142
  }
59
- handleOutsideClick(event) {
60
- if (this.isOpen && !this.isChildDialog(event.target)) {
61
- this.closeModal();
62
- }
143
+ handleOpen() {
144
+ // Register with modal manager and get z-index
145
+ const assignedZIndex = ModalManager.openModal(this);
146
+ this.zIndex = assignedZIndex;
147
+ // Set animation state to opening
148
+ this.animationState = 'opening';
149
+ // Dispatch before open event
150
+ this.dispatchEvent(new CustomEvent('modal-open', {
151
+ bubbles: true,
152
+ detail: { modal: this, stackDepth: ModalManager.getStackDepth() }
153
+ }));
154
+ // Wait for DOM update, then start JavaScript animation
155
+ this.updateComplete.then(() => {
156
+ this.startOpenAnimation();
157
+ });
63
158
  }
64
- isChildDialog(target) {
65
- let element = target.parentElement;
66
- while (element !== null) {
67
- if (element.tagName === 'DIALOG') {
68
- return true;
159
+ startOpenAnimation() {
160
+ var _a, _b;
161
+ const modalElement = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.modal');
162
+ const backdropElement = (_b = this.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.modal-backdrop');
163
+ if (!modalElement || !backdropElement)
164
+ return;
165
+ // Get animation keyframes based on animation type
166
+ const { modalKeyframes } = this.getAnimationKeyframes();
167
+ // Start animations
168
+ const modalAnimation = modalElement.animate(modalKeyframes, {
169
+ duration: 300,
170
+ easing: 'ease',
171
+ fill: 'forwards'
172
+ });
173
+ // When animation completes
174
+ modalAnimation.addEventListener('finish', () => {
175
+ this.animationState = 'open';
176
+ // Only focus if this is the top modal
177
+ if (ModalManager.isTopModal(this)) {
178
+ this.keyboardController.focusFirstElement();
69
179
  }
70
- element = element.parentElement;
180
+ // Dispatch after open event
181
+ this.dispatchEvent(new CustomEvent('modal-after-open', {
182
+ bubbles: true,
183
+ detail: { modal: this, stackDepth: ModalManager.getStackDepth() }
184
+ }));
185
+ });
186
+ }
187
+ getAnimationKeyframes() {
188
+ const backdropKeyframes = [
189
+ { opacity: 0 },
190
+ { opacity: 1 }
191
+ ];
192
+ let modalKeyframes;
193
+ switch (this.animation) {
194
+ case 'fade':
195
+ modalKeyframes = [
196
+ { opacity: 0, transform: 'scale(0.9)' },
197
+ { opacity: 1, transform: 'scale(1)' }
198
+ ];
199
+ break;
200
+ case 'zoom':
201
+ modalKeyframes = [
202
+ { opacity: 0, transform: 'scale(0.7)' },
203
+ { opacity: 1, transform: 'scale(1)' }
204
+ ];
205
+ break;
206
+ case 'slide-up':
207
+ modalKeyframes = [
208
+ { opacity: 0, transform: 'translateY(20px)' },
209
+ { opacity: 1, transform: 'translateY(0)' }
210
+ ];
211
+ break;
212
+ case 'slide-down':
213
+ modalKeyframes = [
214
+ { opacity: 0, transform: 'translateY(-20px)' },
215
+ { opacity: 1, transform: 'translateY(0)' }
216
+ ];
217
+ break;
218
+ default:
219
+ modalKeyframes = [
220
+ { opacity: 0, transform: 'scale(0.9)' },
221
+ { opacity: 1, transform: 'scale(1)' }
222
+ ];
71
223
  }
72
- return false;
224
+ return { modalKeyframes, backdropKeyframes };
225
+ }
226
+ handleClose() {
227
+ this.animationState = 'closing';
228
+ // Unregister from modal manager
229
+ ModalManager.closeModal(this);
230
+ // Reset drag position
231
+ this.dragController.resetPosition();
232
+ // Wait for animation to complete
233
+ setTimeout(() => {
234
+ this.animationState = 'closed';
235
+ }, 300);
236
+ }
237
+ /**
238
+ * Opens the modal
239
+ */
240
+ openModal() {
241
+ this.open = true;
73
242
  }
243
+ /**
244
+ * Closes the modal
245
+ */
74
246
  closeModal() {
75
- this.isOpen = false;
76
- this.offsetX = 0;
77
- this.offsetY = 0;
78
- this.style.transform = 'none';
79
- this.dispatchEvent(new CustomEvent('close'));
80
- }
81
- adjustDialogPosition() {
82
- var _a;
83
- if (this.isOpen) {
84
- const dialog = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.modal');
85
- if (dialog) {
86
- const contentHeight = dialog.clientHeight;
87
- const windowHeight = window.innerHeight;
88
- const dialogHeight = Math.min(contentHeight + 40, windowHeight - 40);
89
- const topPosition = Math.max((windowHeight - dialogHeight) / 2, 0);
90
- dialog.style.top = `${topPosition}px`;
247
+ if (!this.closable)
248
+ return;
249
+ // Dispatch before close event (cancelable)
250
+ const beforeCloseEvent = new CustomEvent('modal-before-close', {
251
+ bubbles: true,
252
+ cancelable: true,
253
+ detail: {
254
+ modal: this,
255
+ cancel: () => beforeCloseEvent.preventDefault()
91
256
  }
257
+ });
258
+ const dispatched = this.dispatchEvent(beforeCloseEvent);
259
+ // Only close if event wasn't cancelled
260
+ if (dispatched) {
261
+ this.open = false;
262
+ // Dispatch close event
263
+ this.dispatchEvent(new CustomEvent('modal-close', {
264
+ bubbles: true,
265
+ detail: { modal: this }
266
+ }));
92
267
  }
93
268
  }
94
- updated(changedProperties) {
95
- super.updated(changedProperties);
96
- if (changedProperties.has('isOpen')) {
97
- if (this.isOpen) {
98
- // The dialog has been opened
99
- this.adjustDialogPosition();
100
- }
269
+ getBackdropClasses() {
270
+ return {
271
+ 'modal-backdrop': true,
272
+ 'modal-backdrop--hidden': !this.open,
273
+ [`modal-backdrop--position-${this.position}`]: true
274
+ };
275
+ }
276
+ getModalClasses() {
277
+ return {
278
+ 'modal': true,
279
+ [`modal--size-${this.size}`]: !this.fullscreen,
280
+ 'modal--fullscreen': this.fullscreen,
281
+ [`modal--animation-${this.animation}`]: this.animationState === 'opening' || this.animationState === 'open',
282
+ 'modal--dragging': this.isDragging,
283
+ 'modal--resizable': this.resizable
284
+ };
285
+ }
286
+ getModalStyles() {
287
+ const styles = {};
288
+ if (this.zIndex) {
289
+ styles['--nuraly-z-modal-backdrop'] = this.zIndex.toString();
290
+ }
291
+ if (this.width) {
292
+ styles.width = this.width;
101
293
  }
294
+ if (this.height) {
295
+ styles.height = this.height;
296
+ }
297
+ return styles;
102
298
  }
103
- stopDrag() {
104
- this.isDragging = false;
299
+ renderHeader() {
300
+ const hasCustomHeader = this.querySelector('[slot="header"]');
301
+ const hasTitle = this.modalTitle || this.headerIcon;
302
+ if (!hasCustomHeader && !hasTitle && !this.showCloseButton) {
303
+ return nothing;
304
+ }
305
+ return html `
306
+ <div class="modal-header ${this.modalDraggable ? 'modal-header--draggable' : ''}">
307
+ ${hasCustomHeader ? html `
308
+ <div class="modal-header-content">
309
+ <slot name="header"></slot>
310
+ </div>
311
+ ` : hasTitle ? html `
312
+ <div class="modal-header-content">
313
+ ${this.headerIcon ? html `
314
+ <nr-icon class="modal-header-icon" name="${this.headerIcon}"></nr-icon>
315
+ ` : nothing}
316
+ ${this.modalTitle ? html `
317
+ <h2 class="modal-title">${this.modalTitle}</h2>
318
+ ` : nothing}
319
+ </div>
320
+ ` : nothing}
321
+
322
+ ${this.showCloseButton ? html `
323
+ <button
324
+ class="modal-close-button"
325
+ @click=${this.closeModal}
326
+ aria-label="Close modal"
327
+ type="button">
328
+ <nr-icon class="modal-close-icon" name="close"></nr-icon>
329
+ </button>
330
+ ` : nothing}
331
+ </div>
332
+ `;
105
333
  }
106
- render() {
107
- const backdropStyles = {
108
- display: this.isOpen ? 'block' : 'none',
109
- };
110
- const modalStyles = {
111
- transform: `translate(${this.offsetX}px, ${this.offsetY}px)`,
112
- };
334
+ renderFooter() {
335
+ const hasCustomFooter = this.querySelector('[slot="footer"]');
336
+ if (!hasCustomFooter) {
337
+ return nothing;
338
+ }
113
339
  return html `
114
- <div class="backdrop" style=${styleMap(backdropStyles)}></div>
115
-
116
- <dialog class="modal" ?open="${this.isOpen}" style=${styleMap(modalStyles)}>
117
- <hy-icon
118
- class="close-icon"
119
- name="window-close"
120
- style="float: right;"
121
- @click=${() => (this.closeModal())}
122
- ></hy-icon>
123
- <h2 class="dialog-label" @mousedown=${this.startDrag} @mouseup=${this.stopDrag}>${this.label}</h2>
124
- <slot></slot>
340
+ <div class="modal-footer">
125
341
  <slot name="footer"></slot>
126
- </dialog>
342
+ </div>
127
343
  `;
128
344
  }
129
- }
130
- ModalComponent.styles = css `
131
- :host {
132
- font-size: 16px;
133
- }
134
- .modal {
135
- position: fixed;
136
- z-index: 9999;
137
- background-color: #ffffff;
138
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
139
- }
140
-
141
- dialog {
142
- border: 0;
143
- box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12),
144
- 0 9px 28px 8px rgba(0, 0, 0, 0.05);
145
- min-width: 400px;
146
- min-height: 200px;
147
- }
148
-
149
- .dialog-label {
150
- cursor: move;
151
- }
152
-
153
- .backdrop {
154
- position: fixed;
155
- top: 0;
156
- left: 0;
157
- width: 100%;
158
- height: 100%;
159
- background-color: rgba(0, 0, 0, 0.3);
160
- z-index: 9998;
161
- }
162
-
163
- .close-icon {
164
- cursor: pointer;
165
- }
166
- ::slotted([slot='footer']) {
167
- /* Styles applied specifically to slotted elements with slot="custom-slot" */
168
- bottom: 0;
169
- position: absolute;
170
- text-align: end;
171
- margin-bottom: 10px;
172
- width : 93%;
173
- }
174
-
175
- ::slotted([slot='content']) {
176
- padding: 0px 24px 20px 24px;
177
- }
178
- @media (prefers-color-scheme: dark) {
179
- .modal {
180
- background-color: rgb(74, 74, 74);
181
- color: #ffffff;
182
- }
183
- .backdrop {
184
- background-color: rgba(0, 0, 0, 0.3);
185
- }
186
- }
187
-
188
- `;
345
+ updated() {
346
+ this.updateDataTheme();
347
+ }
348
+ updateDataTheme() {
349
+ if (!this.closest('[data-theme]')) {
350
+ this.setAttribute('data-theme', this.currentTheme);
351
+ }
352
+ }
353
+ render() {
354
+ if (!this.open && this.animationState === 'closed') {
355
+ return nothing;
356
+ }
357
+ return html `
358
+ <div
359
+ class=${classMap(this.getBackdropClasses())}
360
+ @click=${this.handleBackdropClick}
361
+ style=${styleMap(this.getModalStyles())}>
362
+
363
+ <div
364
+ class=${classMap(this.getModalClasses())}
365
+ role="dialog"
366
+ aria-modal="true"
367
+ aria-labelledby=${this.modalTitle ? 'modal-title' : nothing}
368
+ tabindex="-1">
369
+
370
+ ${this.renderHeader()}
371
+
372
+ <div class="modal-body">
373
+ <slot></slot>
374
+ </div>
375
+
376
+ ${this.renderFooter()}
377
+
378
+ ${this.resizable ? html `
379
+ <div class="resize-handle"></div>
380
+ ` : nothing}
381
+ </div>
382
+ </div>
383
+ `;
384
+ }
385
+ };
386
+ NrModalElement.styles = styles;
387
+ __decorate([
388
+ property({ type: Boolean, reflect: true })
389
+ ], NrModalElement.prototype, "open", void 0);
390
+ __decorate([
391
+ property({ type: String })
392
+ ], NrModalElement.prototype, "size", void 0);
393
+ __decorate([
394
+ property({ type: String })
395
+ ], NrModalElement.prototype, "position", void 0);
396
+ __decorate([
397
+ property({ type: String })
398
+ ], NrModalElement.prototype, "animation", void 0);
399
+ __decorate([
400
+ property({ type: String })
401
+ ], NrModalElement.prototype, "backdrop", void 0);
189
402
  __decorate([
190
403
  property({ type: Boolean })
191
- ], ModalComponent.prototype, "isOpen", void 0);
404
+ ], NrModalElement.prototype, "closable", void 0);
405
+ __decorate([
406
+ property({ type: Boolean })
407
+ ], NrModalElement.prototype, "modalDraggable", void 0);
408
+ __decorate([
409
+ property({ type: Boolean })
410
+ ], NrModalElement.prototype, "resizable", void 0);
411
+ __decorate([
412
+ property({ type: Boolean })
413
+ ], NrModalElement.prototype, "fullscreen", void 0);
414
+ __decorate([
415
+ property({ type: String })
416
+ ], NrModalElement.prototype, "modalTitle", void 0);
417
+ __decorate([
418
+ property({ type: Boolean, attribute: 'show-close-button' })
419
+ ], NrModalElement.prototype, "showCloseButton", void 0);
192
420
  __decorate([
193
421
  property({ type: String })
194
- ], ModalComponent.prototype, "label", void 0);
422
+ ], NrModalElement.prototype, "headerIcon", void 0);
423
+ __decorate([
424
+ property({ type: Number })
425
+ ], NrModalElement.prototype, "zIndex", void 0);
426
+ __decorate([
427
+ property({ type: String })
428
+ ], NrModalElement.prototype, "width", void 0);
429
+ __decorate([
430
+ property({ type: String })
431
+ ], NrModalElement.prototype, "height", void 0);
195
432
  __decorate([
196
433
  state()
197
- ], ModalComponent.prototype, "offsetX", void 0);
434
+ ], NrModalElement.prototype, "isDragging", void 0);
435
+ __decorate([
436
+ property({ type: Number })
437
+ ], NrModalElement.prototype, "offsetX", void 0);
438
+ __decorate([
439
+ property({ type: Number })
440
+ ], NrModalElement.prototype, "offsetY", void 0);
198
441
  __decorate([
199
442
  state()
200
- ], ModalComponent.prototype, "offsetY", void 0);
201
- customElements.define('modal-component', ModalComponent);
443
+ ], NrModalElement.prototype, "animationState", void 0);
444
+ NrModalElement = __decorate([
445
+ customElement('nr-modal')
446
+ ], NrModalElement);
447
+ export { NrModalElement };
202
448
  //# sourceMappingURL=modal.component.js.map