@operato/popup 7.1.30 → 7.1.32

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,617 +0,0 @@
1
- import '@material/web/icon/icon.js'
2
- import '@operato/input/ox-input-search.js'
3
-
4
- import { css, html, LitElement, PropertyValues } from 'lit'
5
- import { customElement, property, query } from 'lit/decorators.js'
6
- import { ifDefined } from 'lit/directives/if-defined.js'
7
-
8
- import { ScrollbarStyles } from '@operato/styles'
9
- import { isHandheldDevice } from '@operato/utils'
10
-
11
- function getPoint(e: Event): { x: number; y: number } | undefined {
12
- if ((e as MouseEvent).button == 0) {
13
- return {
14
- x: (e as MouseEvent).clientX,
15
- y: (e as MouseEvent).clientY
16
- }
17
- } else if ((e as TouchEvent).touches?.length == 1) {
18
- const touch = (e as TouchEvent).touches[0]
19
- return {
20
- x: touch.clientX,
21
- y: touch.clientY
22
- }
23
- } else {
24
- return
25
- }
26
- }
27
-
28
- /**
29
- * Custom element for creating floating overlays.
30
- * These overlays can have various properties like direction, size, title, and more.
31
- * They are often used for modal dialogs, pop-ups, and similar UI elements.
32
- */
33
- @customElement('ox-floating-overlay')
34
- export class OxFloatingOverlay extends LitElement {
35
- static styles = [
36
- ScrollbarStyles,
37
- css`
38
- /* for layout style */
39
- :host {
40
- position: relative;
41
- z-index: var(--z-index, 10);
42
- box-shadow: 2px 3px 10px 5px rgba(0, 0, 0, 0.15);
43
- }
44
-
45
- :host([hovering='edge']) {
46
- /* edge hovering 인 경우에는 상위 relative position 크기와 위치를 반영한다. */
47
- position: initial;
48
- }
49
-
50
- #backdrop {
51
- position: fixed;
52
- left: 0;
53
- top: 0;
54
-
55
- width: 100vw;
56
- height: 100vh;
57
- height: 100dvh;
58
-
59
- background-color: var(--md-sys-color-shadow, #000);
60
- opacity: 0.4;
61
- }
62
-
63
- [overlayed] {
64
- position: absolute;
65
-
66
- display: flex;
67
- flex-direction: column;
68
- overflow: hidden;
69
- }
70
-
71
- [overlayed][hovering='center'] {
72
- position: fixed;
73
-
74
- left: 50%;
75
- top: 50%;
76
- transform: translate(-50%, -50%);
77
-
78
- opacity: 0;
79
- border: 2px solid var(--md-sys-color-primary);
80
- }
81
-
82
- [overlayed][hovering='center'][opened] {
83
- opacity: 1;
84
- transition: opacity 0.3s ease-in;
85
- background-color: var(--md-sys-color-surface-container-lowest);
86
- }
87
-
88
- [hovering='center'] {
89
- width: var(--overlay-center-normal-width, 60%);
90
- height: var(--overlay-center-normal-height, 60%);
91
- }
92
-
93
- [hovering='center'][size='small'] {
94
- width: var(--overlay-center-small-width, 40%);
95
- height: var(--overlay-center-small-height, 40%);
96
- }
97
-
98
- [hovering='center'][size='large'] {
99
- width: var(--overlay-center-large-width, 100%);
100
- height: var(--overlay-center-large-height, 100%);
101
- }
102
-
103
- [header] {
104
- --help-icon-color: var(--md-sys-color-on-primary-container);
105
- --help-icon-hover-color: var(--md-sys-color-on-primary-container);
106
-
107
- pointer-events: initial;
108
- }
109
-
110
- [content] {
111
- flex: 1;
112
- overflow: hidden;
113
- }
114
-
115
- ::slotted(*) {
116
- box-sizing: border-box;
117
- pointer-events: initial;
118
- }
119
-
120
- [hovering='center'] [content] ::slotted(*) {
121
- width: 100%;
122
- height: 100%;
123
- }
124
- [direction='up'],
125
- [direction='down'] {
126
- width: 100%;
127
-
128
- max-height: 0;
129
- transition: max-height 0.7s ease-in;
130
- }
131
- [direction='up'] {
132
- bottom: 0;
133
- }
134
- [direction='down'] {
135
- top: 0;
136
- }
137
-
138
- [direction='up'][opened],
139
- [direction='down'][opened] {
140
- max-height: 100vh;
141
- max-height: 100dvh;
142
- }
143
-
144
- [settled][direction='down'] [content],
145
- [settled][direction='up'] [content] {
146
- overflow-y: auto;
147
- }
148
-
149
- [direction='left'],
150
- [direction='right'] {
151
- height: 100%;
152
-
153
- max-width: 0;
154
- transition: max-width 0.5s ease-in;
155
- }
156
- [direction='left'] {
157
- right: 0;
158
- }
159
- [direction='right'] {
160
- left: 0;
161
- }
162
-
163
- [direction='left'][opened],
164
- [direction='right'][opened] {
165
- max-width: 100vw;
166
- }
167
-
168
- [settled][direction='left'] [content],
169
- [settled][direction='right'] [content] {
170
- overflow-x: auto;
171
- }
172
-
173
- @media screen and (max-width: 460px) {
174
- [direction='up'],
175
- [direction='down'] {
176
- max-height: 100vh;
177
- max-height: 100dvh;
178
- }
179
-
180
- [direction='left'],
181
- [direction='right'] {
182
- max-width: 100vw;
183
- }
184
- }
185
- `,
186
- css`
187
- /* for header style */
188
- [header] {
189
- display: flex;
190
- flex-direction: row;
191
- align-items: center;
192
- background-color: var(--md-sys-color-primary);
193
- font-weight: var(--md-sys-typescale-label-large-weight, var(--md-ref-typeface-weight-medium, 500));
194
- color: var(--md-sys-color-on-primary);
195
- }
196
-
197
- slot[name='header'] {
198
- flex: 1;
199
-
200
- display: flex;
201
- flex-direction: row;
202
- align-items: center;
203
- justify-content: center;
204
- text-transform: capitalize;
205
- }
206
-
207
- [name='header']::slotted(*) {
208
- margin: 0 auto;
209
- }
210
-
211
- [name='header'] > h1 {
212
- font-size: var(--md-sys-typescale-title-medium-size, 1rem);
213
- margin-block: 0.4rem;
214
- }
215
-
216
- [historyback] {
217
- margin-left: var(--spacing-medium);
218
- margin-right: auto;
219
- }
220
-
221
- [close] {
222
- margin-left: auto;
223
- margin-right: var(--spacing-medium);
224
- }
225
-
226
- [historyback],
227
- [close] {
228
- display: none;
229
- }
230
-
231
- [closable][close] {
232
- display: block;
233
- cursor: pointer;
234
- }
235
-
236
- [search] ox-help-icon {
237
- color: var(--md-sys-color-on-secondary-container);
238
- }
239
-
240
- [search] {
241
- --help-icon-color: var(--md-sys-color-primary);
242
- --help-icon-hover-color: var(--md-sys-color-primary);
243
- --help-icon-opacity: 0.2;
244
- --help-icon-size: 20px;
245
-
246
- --md-icon-size: 20px;
247
-
248
- --input-search-padding: var(--spacing-medium);
249
- --input-search-focus-border-bottom: none;
250
- --input-search-font: normal 16px var(--theme-font);
251
-
252
- display: flex;
253
- justify-content: space-between;
254
- align-items: center;
255
-
256
- align-self: center;
257
- color: var(--md-sys-color-primary);
258
- background-color: var(--md-sys-color-on-primary);
259
- border-radius: 999em;
260
- padding: 0 var(--spacing-medium);
261
- }
262
-
263
- [search] > * {
264
- align-self: center;
265
- }
266
-
267
- ox-input-search {
268
- color: var(--md-sys-color-primary);
269
- background-color: var(--md-sys-color-on-primary);
270
- border-radius: var(--md-sys-shape-corner-full);
271
- }
272
-
273
- @media screen and (max-width: 460px) {
274
- [closable][historyback] {
275
- display: block;
276
- }
277
-
278
- [closable][close] {
279
- display: none;
280
- }
281
- }
282
- `
283
- ]
284
-
285
- /**
286
- * A boolean property that determines whether a backdrop should be displayed behind the overlay.
287
- * Backdrop provides a semi-transparent background that covers the entire screen when the overlay is open.
288
- */
289
- @property({ type: Boolean }) backdrop?: boolean = false
290
-
291
- /**
292
- * A string property that specifies the direction in which the overlay should appear.
293
- * Possible values are: 'up', 'down', 'left', or 'right'.
294
- */
295
- @property({ type: String }) direction?: 'up' | 'down' | 'left' | 'right'
296
-
297
- /**
298
- * A string property that reflects the hovering state of the overlay.
299
- * Possible values are: 'center', 'edge', or 'next'.
300
- */
301
- @property({ type: String, reflect: true }) hovering?: 'center' | 'edge' | 'next'
302
-
303
- /**
304
- * A string property that specifies the size of the overlay.
305
- * Possible values are: 'small', 'medium', or 'large'.
306
- */
307
- @property({ type: String }) size?: 'small' | 'medium' | 'large'
308
-
309
- /**
310
- * A string property that represents the name of the overlay.
311
- * This can be used for identification or other purposes.
312
- */
313
- @property({ type: String }) name?: string
314
-
315
- /**
316
- * A string property that sets the title of the overlay.
317
- * The title is typically displayed in the header of the overlay.
318
- */
319
- @property({ type: String }) title: string = ''
320
-
321
- /**
322
- * A boolean property that determines whether the overlay can be closed by the user.
323
- * If set to true, a close button will be displayed in the header.
324
- */
325
- @property({ type: Boolean }) closable?: boolean = false
326
-
327
- /**
328
- * An object property that can hold custom properties for the template of the overlay.
329
- * These properties can be used to customize the template's behavior.
330
- */
331
- @property({ type: Object }) templateProperties: any
332
-
333
- /**
334
- * An object property that can hold information related to help or assistance for the overlay.
335
- * This information may be used to provide additional guidance to users.
336
- */
337
- @property({ type: Object }) help: any
338
-
339
- /**
340
- * A boolean property that determines whether the overlay is considered historical.
341
- * Historical overlays may have specific behavior or interactions, such as navigating back in history.
342
- */
343
- @property({ type: Boolean }) historical?: boolean = false
344
-
345
- /**
346
- * A boolean property that determines whether the overlay can be moved by dragging.
347
- * If set to true, the overlay can be repositioned by dragging its header.
348
- */
349
- @property({ type: Boolean }) movable?: boolean = false
350
-
351
- /**
352
- * An object property that can hold information related to a search feature within the overlay.
353
- * It can include properties like 'value', 'handler', and 'placeholder'.
354
- */
355
- @property({ type: Object }) search?: {
356
- value?: string
357
- handler?: (instance: any, value: string) => void
358
- placeholder?: string
359
- }
360
-
361
- /**
362
- * An object property that can hold information related to a filter feature within the overlay.
363
- * It can include a 'handler' function for filtering content.
364
- */
365
- @property({ type: Object }) filter?: { handler?: (instance: any) => void }
366
-
367
- /**
368
- * A numeric property that specifies the z-index of the overlay.
369
- * The z-index determines the stacking order of the overlay in relation to other elements on the page.
370
- */
371
- @property({ type: Number, attribute: 'z-index' }) zIndex?: number
372
-
373
- private dragStart?: { x: number; y: number }
374
- private dragEndHandler = this.onDragEnd.bind(this) as EventListener
375
- private dragMoveHandler = this.onDragMove.bind(this) as EventListener
376
-
377
- @query('[overlayed]') overlayed!: HTMLDivElement
378
- @query('[content]') content!: HTMLDivElement
379
-
380
- render() {
381
- const direction = this.hovering == 'center' ? undefined : this.direction
382
-
383
- const { value = '', handler: searchHandler, placeholder = '', autofocus = true } = this.search || ({} as any)
384
- const { handler: filterHandler } = this.filter || ({} as any)
385
-
386
- const searchable = typeof searchHandler == 'function'
387
- const filterable = typeof filterHandler == 'function'
388
-
389
- return html`
390
- ${Boolean(this.backdrop)
391
- ? html`
392
- <div
393
- id="backdrop"
394
- ?hidden=${!this.backdrop}
395
- @click=${(e: Event) => this.onClose(e, true /* escape */)}
396
- ></div>
397
- `
398
- : html``}
399
-
400
- <div
401
- overlayed
402
- hovering=${this.hovering || 'center'}
403
- direction=${ifDefined(direction)}
404
- size=${this.size || 'medium'}
405
- @close-overlay=${(e: Event) => {
406
- e.stopPropagation()
407
- this.onClose(e)
408
- }}
409
- @transitionstart=${(e: Event) => {
410
- /* to hide scrollbar during transition */
411
- ;(e.target as HTMLElement).removeAttribute('settled')
412
- }}
413
- @transitionend=${(e: Event) => {
414
- ;(e.target as HTMLElement).setAttribute('settled', '')
415
- }}
416
- @click=${(e: MouseEvent) => {
417
- if (this.backdrop && e.target === this.content) {
418
- this.onClose(e, true /* escape */)
419
- }
420
- }}
421
- >
422
- <div
423
- header
424
- @mousedown=${this.onDragStart.bind(this)}
425
- @touchstart=${this.onDragStart.bind(this)}
426
- draggable="false"
427
- >
428
- <md-icon @click=${(e: Event) => this.onClose(e)} ?closable=${this.closable} historyback>arrow_back</md-icon>
429
- ${this.movable ? html`<md-icon>drag_indicator</md-icon>` : html``}
430
- <slot name="header">
431
- ${this.title || this.closable
432
- ? html`
433
- <h1>
434
- ${this.title || ''}&nbsp;${this.help
435
- ? html`<ox-help-icon .topic=${this.help}></ox-help-icon>`
436
- : html``}
437
- </h1>
438
- `
439
- : html``}
440
- ${searchable || filterable
441
- ? html`
442
- <div search>
443
- ${searchable
444
- ? html` <ox-input-search
445
- .placeholder=${placeholder}
446
- .value=${value || ''}
447
- ?autofocus=${autofocus}
448
- @change=${(e: Event) => {
449
- searchHandler(this.firstElementChild, (e.target as any).value)
450
- }}
451
- ></ox-input-search>`
452
- : html``}
453
- ${this.help && searchable ? html`<ox-help-icon .topic=${this.help}></ox-help-icon>` : html``}
454
- ${filterable
455
- ? html`<md-icon @click=${(e: MouseEvent) => filterHandler(this.firstElementChild)}>tune</md-icon>`
456
- : html``}
457
- </div>
458
- `
459
- : html``}
460
- ${this.help && !searchable && !this.title /* help only */
461
- ? html`<ox-help-icon .topic=${this.help}></ox-help-icon>`
462
- : html``}
463
- </slot>
464
- <md-icon @click=${(e: Event) => this.onClose(e)} ?closable=${this.closable} close>close</md-icon>
465
- </div>
466
-
467
- <div content>
468
- <slot> </slot>
469
- </div>
470
- </div>
471
- `
472
- }
473
-
474
- updated(changes: PropertyValues<this>) {
475
- if (changes.has('templateProperties') && this.templateProperties) {
476
- var template = this.firstElementChild
477
- if (template) {
478
- for (let prop in this.templateProperties) {
479
- //@ts-ignore
480
- template[prop] = this.templateProperties[prop]
481
- }
482
- }
483
- }
484
- }
485
-
486
- firstUpdated() {
487
- if (this.zIndex) {
488
- this.style.setProperty('--z-index', String(this.zIndex))
489
- }
490
-
491
- requestAnimationFrame(() => {
492
- /* transition(animation) 효과를 위해 'opened' 속성을 변화시킨다. */
493
- this.overlayed?.setAttribute('opened', 'true')
494
- })
495
- }
496
-
497
- connectedCallback(): void {
498
- super.connectedCallback()
499
-
500
- this.movable = this.movable && !isHandheldDevice()
501
-
502
- if (this.movable) {
503
- document.addEventListener('mouseup', this.dragEndHandler)
504
- document.addEventListener('touchend', this.dragEndHandler)
505
- document.addEventListener('touchcancel', this.dragEndHandler)
506
- document.addEventListener('mousemove', this.dragMoveHandler)
507
- document.addEventListener('touchmove', this.dragMoveHandler)
508
- }
509
- }
510
-
511
- disconnectedCallback() {
512
- document.dispatchEvent(
513
- new CustomEvent('overlay-closed', {
514
- detail: this.name
515
- })
516
- )
517
-
518
- if (this.movable) {
519
- document.removeEventListener('mouseup', this.dragEndHandler!)
520
- document.removeEventListener('touchend', this.dragEndHandler!)
521
- document.removeEventListener('touchcancel', this.dragEndHandler!)
522
- document.removeEventListener('mousemove', this.dragMoveHandler!)
523
- document.removeEventListener('touchmove', this.dragMoveHandler!)
524
- }
525
-
526
- super.disconnectedCallback()
527
- }
528
-
529
- onDragStart(e: Event) {
530
- if (!this.movable) {
531
- return
532
- }
533
-
534
- const point = getPoint(e)
535
-
536
- if (point) {
537
- this.dragStart = point
538
- e.stopPropagation()
539
- return false
540
- }
541
- }
542
-
543
- onDragMove(e: Event) {
544
- if (!this.movable || !this.dragStart) {
545
- return false
546
- }
547
-
548
- const point = getPoint(e)
549
-
550
- if (!point) {
551
- return false
552
- }
553
-
554
- e.stopPropagation()
555
- e.preventDefault()
556
-
557
- const dragStart = point
558
- var { x, y } = point
559
-
560
- x -= this.dragStart.x
561
- y -= this.dragStart.y
562
-
563
- this.dragStart = dragStart
564
-
565
- const overlayed = this.overlayed
566
-
567
- var boundingRect = overlayed.getBoundingClientRect()
568
-
569
- overlayed.style.left =
570
- Math.min(document.body.offsetWidth - 40, Math.max(40 - overlayed.offsetWidth, boundingRect.left + x)) + 'px'
571
- overlayed.style.top = Math.min(document.body.offsetHeight - 40, Math.max(0, boundingRect.top + y)) + 'px'
572
-
573
- overlayed.style.transform = 'initial'
574
-
575
- return false
576
- }
577
-
578
- onDragEnd(e: Event) {
579
- if (this.movable && this.dragStart) {
580
- e.stopPropagation()
581
- e.preventDefault()
582
-
583
- delete this.dragStart
584
- }
585
- }
586
-
587
- /**
588
- * A method that closes the overlay by removing it from its parent node in the DOM.
589
- * When called, this method removes the overlay element, effectively hiding it from the user interface.
590
- */
591
- close() {
592
- this.parentNode?.removeChild(this)
593
- }
594
-
595
- onClose(e: Event, escape?: boolean) {
596
- e.stopPropagation()
597
- /* 현재 overlay state를 확인해서, 자신이 포함하고 있는 템플릿인 경우에 history.back() 한다. */
598
-
599
- if (this.historical) {
600
- var state = history.state
601
- var overlay = (state || {}).overlay
602
-
603
- if (!overlay || overlay.name !== this.name) {
604
- return
605
- }
606
-
607
- /* Backdrop click 경우는 escape 시도라고 정의한다. overlay 속성이 escapable이 아닌 경우에는 동작하지 않는다. */
608
- if (escape && !overlay.escapable) {
609
- return true
610
- }
611
-
612
- history.back()
613
- } else {
614
- this.close()
615
- }
616
- }
617
- }