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