@operato/board 0.2.15

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.
Files changed (69) hide show
  1. package/.storybook/main.js +3 -0
  2. package/.storybook/server.mjs +8 -0
  3. package/CHANGELOG.md +22 -0
  4. package/LICENSE +21 -0
  5. package/README.md +95 -0
  6. package/custom-elements.json +1377 -0
  7. package/demo/index-player.html +101 -0
  8. package/demo/index-viewer.html +101 -0
  9. package/demo/index.html +101 -0
  10. package/dist/src/index.d.ts +2 -0
  11. package/dist/src/index.js +3 -0
  12. package/dist/src/index.js.map +1 -0
  13. package/dist/src/ox-board-player copy.d.ts +39 -0
  14. package/dist/src/ox-board-player copy.js +258 -0
  15. package/dist/src/ox-board-player copy.js.map +1 -0
  16. package/dist/src/ox-board-player-control.d.ts +39 -0
  17. package/dist/src/ox-board-player-control.js +390 -0
  18. package/dist/src/ox-board-player-control.js.map +1 -0
  19. package/dist/src/ox-board-player-style.d.ts +1 -0
  20. package/dist/src/ox-board-player-style.js +200 -0
  21. package/dist/src/ox-board-player-style.js.map +1 -0
  22. package/dist/src/ox-board-player.d.ts +39 -0
  23. package/dist/src/ox-board-player.js +284 -0
  24. package/dist/src/ox-board-player.js.map +1 -0
  25. package/dist/src/ox-board-viewer.d.ts +45 -0
  26. package/dist/src/ox-board-viewer.js +491 -0
  27. package/dist/src/ox-board-viewer.js.map +1 -0
  28. package/dist/src/ox-board-wrapper.d.ts +1 -0
  29. package/dist/src/ox-board-wrapper.js +88 -0
  30. package/dist/src/ox-board-wrapper.js.map +1 -0
  31. package/dist/src/player/board-player-carousel.d.ts +1 -0
  32. package/dist/src/player/board-player-carousel.js +205 -0
  33. package/dist/src/player/board-player-carousel.js.map +1 -0
  34. package/dist/src/player/board-player-grid.d.ts +1 -0
  35. package/dist/src/player/board-player-grid.js +78 -0
  36. package/dist/src/player/board-player-grid.js.map +1 -0
  37. package/dist/src/utils/fullscreen.d.ts +14 -0
  38. package/dist/src/utils/fullscreen.js +69 -0
  39. package/dist/src/utils/fullscreen.js.map +1 -0
  40. package/dist/src/utils/os.d.ts +20 -0
  41. package/dist/src/utils/os.js +41 -0
  42. package/dist/src/utils/os.js.map +1 -0
  43. package/dist/src/utils/swipe-listener.d.ts +10 -0
  44. package/dist/src/utils/swipe-listener.js +257 -0
  45. package/dist/src/utils/swipe-listener.js.map +1 -0
  46. package/dist/stories/index.stories.d.ts +33 -0
  47. package/dist/stories/index.stories.js +33 -0
  48. package/dist/stories/index.stories.js.map +1 -0
  49. package/dist/test/board-viewer.test.d.ts +1 -0
  50. package/dist/test/board-viewer.test.js +24 -0
  51. package/dist/test/board-viewer.test.js.map +1 -0
  52. package/dist/tsconfig.tsbuildinfo +1 -0
  53. package/package.json +76 -0
  54. package/src/index.ts +2 -0
  55. package/src/ox-board-player-style.ts +200 -0
  56. package/src/ox-board-player.ts +292 -0
  57. package/src/ox-board-viewer.ts +534 -0
  58. package/src/ox-board-wrapper.ts +93 -0
  59. package/src/player/board-player-carousel.ts +197 -0
  60. package/src/player/board-player-grid.ts +78 -0
  61. package/src/utils/fullscreen.ts +82 -0
  62. package/src/utils/os.ts +41 -0
  63. package/src/utils/swipe-listener.ts +290 -0
  64. package/src/utils/things-scene.d.ts +1 -0
  65. package/stories/index.stories.ts +52 -0
  66. package/test/board-viewer.test.ts +35 -0
  67. package/tsconfig.json +22 -0
  68. package/web-dev-server.config.mjs +28 -0
  69. package/web-test-runner.config.mjs +29 -0
@@ -0,0 +1,534 @@
1
+ import '@material/mwc-fab'
2
+ import '@material/mwc-icon'
3
+
4
+ import { LitElement, PropertyValues, css, html } from 'lit'
5
+ import { customElement, property, query, state } from 'lit/decorators.js'
6
+
7
+ import { create } from '@hatiolab/things-scene'
8
+ import { isIOS } from './utils/os'
9
+ import { togglefullscreen } from './utils/fullscreen'
10
+
11
+ @customElement('ox-board-viewer')
12
+ export class BoardViewer extends LitElement {
13
+ static styles = css`
14
+ :host {
15
+ display: flex;
16
+ flex-direction: column;
17
+
18
+ position: relative;
19
+
20
+ width: 100%; /* 전체화면보기를 위해서 필요함. */
21
+ overflow: hidden;
22
+ }
23
+
24
+ #target {
25
+ flex: 1;
26
+
27
+ width: 100%; /* 전체화면보기를 위해서 필요함. */
28
+ height: 100%;
29
+
30
+ outline: 0;
31
+ }
32
+
33
+ /* navigation buttons */
34
+ mwc-icon {
35
+ z-index: 10;
36
+ position: absolute;
37
+ top: 45%;
38
+ min-width: 50px;
39
+ width: 50px;
40
+ height: 50px;
41
+ margin: 0;
42
+ padding: 0;
43
+ color: #fff;
44
+ user-select: none;
45
+
46
+ --mdc-icon-size: 3em;
47
+ }
48
+
49
+ mwc-icon[hidden] {
50
+ display: none;
51
+ }
52
+
53
+ mwc-icon:hover {
54
+ background-color: rgba(0, 0, 0, 0.2);
55
+ border-radius: 50%;
56
+ }
57
+
58
+ #prev {
59
+ left: 5px;
60
+ }
61
+
62
+ #next {
63
+ right: 5px;
64
+ }
65
+
66
+ #fullscreen {
67
+ position: absolute;
68
+ bottom: 15px;
69
+ right: 16px;
70
+ }
71
+
72
+ /* for scroller */
73
+ ::-webkit-scrollbar {
74
+ width: 5px;
75
+ height: 5px;
76
+ }
77
+ ::-webkit-scrollbar-track {
78
+ background-color: transparent;
79
+ }
80
+ ::-webkit-scrollbar-thumb {
81
+ background-color: rgba(0, 0, 0, 0.2);
82
+ }
83
+ ::-webkit-scrollbar-thumb:hover {
84
+ background-color: #aa866a;
85
+ }
86
+
87
+ [hidden] {
88
+ display: none;
89
+ }
90
+
91
+ @media print {
92
+ mwc-fab,
93
+ mwc-icon {
94
+ display: none;
95
+ }
96
+ }
97
+ `
98
+
99
+ @property({ type: String }) baseUrl = ''
100
+ @property({ type: Object }) board: any = {}
101
+ @property({ type: Object }) provider: any = null
102
+ @property({ type: Object }) data = {}
103
+
104
+ @property({ type: Boolean, reflect: true, attribute: 'hide-fullscreen' }) hideFullscreen = false
105
+ @property({ type: Boolean, reflect: true, attribute: 'hide-navigation' }) hideNavigation = false
106
+
107
+ @state() scene: any = null
108
+ @state() forward: Array<any> = []
109
+ @state() backward: Array<any> = []
110
+
111
+ @state() _oldtarget?: HTMLElement
112
+ @state() _fade_animations?: Array<Animation>
113
+
114
+ @query('#target') target!: HTMLElement
115
+ @query('#prev') prev!: HTMLElement
116
+ @query('#next') next!: HTMLElement
117
+ @query('#fullscreen') fullscreen!: HTMLElement
118
+
119
+ render() {
120
+ var fullscreen =
121
+ !isIOS() && !this.hideFullscreen
122
+ ? html`
123
+ <mwc-fab
124
+ id="fullscreen"
125
+ .icon=${document.fullscreenElement ? 'fullscreen_exit' : 'fullscreen'}
126
+ @click=${(e: Event) => this.onTapFullscreen()}
127
+ @mouseover=${(e: Event) => this.transientShowButtons(true)}
128
+ @mouseout=${(e: Event) => this.transientShowButtons()}
129
+ title="fullscreen"
130
+ ></mwc-fab>
131
+ `
132
+ : html``
133
+
134
+ var prev = !this.hideNavigation
135
+ ? html`
136
+ <mwc-icon
137
+ id="prev"
138
+ @click=${(e: Event) => this.onTapPrev()}
139
+ @mouseover=${(e: Event) => this.transientShowButtons(true)}
140
+ @mouseout=${(e: Event) => this.transientShowButtons()}
141
+ hidden
142
+ >keyboard_arrow_left</mwc-icon
143
+ >
144
+ `
145
+ : html``
146
+
147
+ var next = !this.hideNavigation
148
+ ? html`
149
+ <mwc-icon
150
+ id="next"
151
+ @click=${(e: Event) => this.onTapNext()}
152
+ @mouseover=${(e: Event) => this.transientShowButtons(true)}
153
+ @mouseout=${(e: Event) => this.transientShowButtons()}
154
+ hidden
155
+ >keyboard_arrow_right</mwc-icon
156
+ >
157
+ `
158
+ : html``
159
+
160
+ return html`
161
+ ${prev}
162
+
163
+ <div
164
+ id="target"
165
+ @touchstart=${(e: Event) => this.transientShowButtons()}
166
+ @mousemove=${(e: Event) => this.transientShowButtons()}
167
+ ></div>
168
+
169
+ ${next} ${fullscreen}
170
+ `
171
+ }
172
+
173
+ firstUpdated() {
174
+ window.addEventListener('resize', () => {
175
+ this.scene && this.scene.fit()
176
+ })
177
+
178
+ this.renderRoot.addEventListener(
179
+ 'close-scene',
180
+ e => {
181
+ e.preventDefault()
182
+ this.onTapPrev()
183
+ },
184
+ false
185
+ )
186
+ }
187
+
188
+ updated(changes: PropertyValues<this>) {
189
+ if (changes.has('board')) {
190
+ if (this.board && this.board.id) {
191
+ this.initScene()
192
+ } else {
193
+ this.closeScene()
194
+ }
195
+ }
196
+
197
+ if (changes.has('data') && this.scene && this.data) {
198
+ this.scene.data = this.data
199
+ }
200
+ }
201
+
202
+ initScene() {
203
+ if (!this.board || !this.board.id) return
204
+
205
+ var scene = create({
206
+ model: {
207
+ ...this.board.model
208
+ },
209
+ mode: 0,
210
+ refProvider: this.provider
211
+ })
212
+
213
+ if (this.baseUrl) {
214
+ scene.baseUrl = this.baseUrl
215
+ }
216
+
217
+ this.provider.add(this.board.id, scene)
218
+
219
+ this.showScene(this.board.id)
220
+
221
+ /* provider.add 시에 추가된 레퍼런스 카운트를 다운시켜주어야 함 */
222
+ scene.release()
223
+ }
224
+
225
+ closeScene() {
226
+ if (this.scene) {
227
+ this.unbindSceneEvents(this.scene)
228
+
229
+ this.scene.target = null
230
+ this.scene.release()
231
+
232
+ delete this.scene
233
+ }
234
+
235
+ // delete queued scenes
236
+ this.forward.forEach(scene => scene.release())
237
+ this.forward = []
238
+
239
+ this.backward.forEach(scene => scene.release())
240
+ this.backward = []
241
+ }
242
+
243
+ releaseScene() {
244
+ if (this.scene) {
245
+ this.unbindSceneEvents(this.scene)
246
+
247
+ this.scene.target = null
248
+ this.scene.release()
249
+
250
+ delete this.scene
251
+
252
+ // delete queued scenes
253
+ this.forward.forEach(scene => {
254
+ scene.release()
255
+ })
256
+ this.forward = []
257
+
258
+ this.backward.forEach(scene => {
259
+ scene.release()
260
+ })
261
+ this.backward = []
262
+
263
+ this.transientShowButtons()
264
+ }
265
+ }
266
+
267
+ setupScene(scene: any) {
268
+ this.scene = scene
269
+
270
+ /* scene의 기존 target을 보관한다. */
271
+ this._oldtarget = this.scene.target
272
+
273
+ this.scene.fit(this.board.model.fitMode)
274
+ this.scene.target = this.target
275
+
276
+ if (this.data) {
277
+ this.scene.data = this.data
278
+ }
279
+
280
+ this.bindSceneEvents()
281
+
282
+ this.transientShowButtons()
283
+ }
284
+
285
+ async showScene(boardId: string, bindingData?: any) {
286
+ if (!boardId) return
287
+
288
+ try {
289
+ var scene = await this.provider.get(boardId, true)
290
+
291
+ var old_scene = this.scene
292
+ this.scene = scene
293
+
294
+ if (scene.target === this.target) {
295
+ scene.release()
296
+ return
297
+ }
298
+
299
+ if (old_scene) {
300
+ /* old scene을 backward에 보관한다. */
301
+ this.unbindSceneEvents(old_scene)
302
+ /* 원래의 target에 되돌린다. */
303
+ old_scene.target = this._oldtarget
304
+ this.backward.push(old_scene)
305
+ }
306
+
307
+ this.forward.forEach(scene => {
308
+ scene.release()
309
+ })
310
+
311
+ /* forward를 비운다. */
312
+ this.forward = []
313
+
314
+ this.setupScene(scene)
315
+
316
+ if (bindingData) {
317
+ scene.data = bindingData
318
+ }
319
+ } catch (e) {
320
+ console.error(e)
321
+ }
322
+ }
323
+
324
+ bindSceneEvents() {
325
+ this.scene.on('goto', this.onLinkGoto, this)
326
+ this.scene.on('link-open', this.onLinkOpen, this)
327
+ this.scene.on('link-move', this.onLinkMove, this)
328
+ this.scene.on('route-page', this.onRoutePage, this)
329
+ }
330
+
331
+ unbindSceneEvents(scene: any) {
332
+ scene.off('goto', this.onLinkGoto, this)
333
+ scene.off('link-open', this.onLinkOpen, this)
334
+ scene.off('link-move', this.onLinkMove, this)
335
+ scene.off('route-page', this.onRoutePage, this)
336
+ }
337
+
338
+ transientShowButtons(stop?: boolean) {
339
+ var buttons = []
340
+ !this.hideNavigation && buttons.push(this.next, this.prev)
341
+ !this.hideFullscreen && buttons.push(this.fullscreen)
342
+
343
+ if (buttons.length == 0) {
344
+ return
345
+ }
346
+
347
+ if (!this._fade_animations) {
348
+ this._fade_animations = buttons
349
+ .filter(button => button)
350
+ .map(button => {
351
+ let animation = button.animate(
352
+ [
353
+ {
354
+ opacity: 1,
355
+ easing: 'ease-in'
356
+ },
357
+ { opacity: 0 }
358
+ ],
359
+ { delay: 1000, duration: 2000 }
360
+ )
361
+
362
+ animation.onfinish = () => {
363
+ button.setAttribute('hidden', '')
364
+ }
365
+
366
+ return animation
367
+ })
368
+ }
369
+
370
+ this.forward.length <= 0 ? this.next.setAttribute('hidden', '') : this.next.removeAttribute('hidden')
371
+ this.backward.length <= 0 ? this.prev.setAttribute('hidden', '') : this.prev.removeAttribute('hidden')
372
+ this.fullscreen && this.fullscreen.removeAttribute('hidden')
373
+
374
+ this._fade_animations.forEach(animation => {
375
+ animation.cancel()
376
+ if (stop) return
377
+
378
+ animation.play()
379
+ })
380
+ }
381
+
382
+ /* event handlers */
383
+
384
+ onTapNext() {
385
+ var scene = this.forward.pop()
386
+ if (!scene) return
387
+
388
+ if (this.scene) {
389
+ this.scene.target = null
390
+ /* 원래의 target에 되돌린다. */
391
+ this.scene.target = this._oldtarget
392
+ this.unbindSceneEvents(this.scene)
393
+ this.backward.push(this.scene)
394
+ }
395
+
396
+ this.setupScene(scene)
397
+ }
398
+
399
+ onTapPrev() {
400
+ var scene = this.backward.pop()
401
+ if (!scene) return
402
+
403
+ if (this.scene) {
404
+ this.scene.target = null
405
+ /* 원래의 target에 되돌린다. */
406
+ this.scene.target = this._oldtarget
407
+ this.unbindSceneEvents(this.scene)
408
+ this.forward.push(this.scene)
409
+ }
410
+
411
+ this.setupScene(scene)
412
+ }
413
+
414
+ onTapFullscreen() {
415
+ togglefullscreen(
416
+ this,
417
+ () => {
418
+ this.requestUpdate()
419
+ },
420
+ () => {
421
+ this.requestUpdate()
422
+ }
423
+ )
424
+ }
425
+
426
+ onLinkGoto(targetBoardId: string, value: any, fromComponent: any) {
427
+ this.showScene(targetBoardId, fromComponent.data)
428
+ }
429
+
430
+ onLinkOpen(url: string, value: any, fromComponent: any) {
431
+ if (!url) return
432
+
433
+ try {
434
+ window.open(url)
435
+ } catch (ex) {
436
+ document.dispatchEvent(
437
+ new CustomEvent('notify', {
438
+ detail: {
439
+ level: 'error',
440
+ message: ex,
441
+ ex
442
+ }
443
+ })
444
+ )
445
+ }
446
+ }
447
+
448
+ onLinkMove(url: string, value: any, fromComponent: any) {
449
+ if (!url) return
450
+
451
+ location.href = url
452
+ }
453
+
454
+ onRoutePage(page: string) {
455
+ if (!page) {
456
+ return
457
+ }
458
+
459
+ history.pushState({}, '', page)
460
+ window.dispatchEvent(new Event('popstate'))
461
+ }
462
+
463
+ async getSceneImageData(base64 = false) {
464
+ if (!this.scene) {
465
+ return
466
+ }
467
+
468
+ var { width, height } = this.scene.model
469
+ var pixelRatio = window.devicePixelRatio
470
+
471
+ // 1. Scene의 바운드에 근거하여, 오프스크린 캔바스를 만든다.
472
+ var canvas = document.createElement('canvas')
473
+ canvas.width = Number(width)
474
+ canvas.height = Number(height)
475
+
476
+ var root = this.scene.root
477
+ // 2. 모델레이어의 원래 위치와 스케일을 저장한다.
478
+ var translate = root.get('translate')
479
+ var scale = root.get('scale')
480
+
481
+ // 3. 위치와 스케일 기본 설정.
482
+ root.set('translate', { x: 0, y: 0 })
483
+ root.set('scale', { x: 1 / pixelRatio, y: 1 / pixelRatio })
484
+
485
+ // 4. 오프스크린 캔바스의 Context2D를 구한뒤, 모델레이어를 그 위에 그린다.
486
+ var context = canvas.getContext('2d')
487
+
488
+ root.draw(context)
489
+
490
+ root.set('translate', translate)
491
+ root.set('scale', scale)
492
+
493
+ var data = base64 ? canvas.toDataURL() : context!.getImageData(0, 0, width, height).data
494
+
495
+ return {
496
+ width,
497
+ height,
498
+ data
499
+ }
500
+ }
501
+
502
+ async printTrick(image: string) {
503
+ var viewTarget: HTMLElement | null = null
504
+ var printTarget: HTMLImageElement | null = null
505
+
506
+ if (!image) {
507
+ image = (await this.getSceneImageData(true))?.data as string
508
+ }
509
+
510
+ printTarget = document.createElement('img')
511
+ printTarget.id = 'target'
512
+ printTarget.src = image
513
+ printTarget.style.width = '100%'
514
+ printTarget.style.height = '100%'
515
+
516
+ const x = (mql: MediaQueryListEvent) => {
517
+ if (mql.matches) {
518
+ if (!viewTarget) {
519
+ viewTarget = this.shadowRoot!.getElementById('target')
520
+ this.renderRoot.replaceChild(printTarget!, viewTarget!)
521
+ }
522
+ } else {
523
+ this.renderRoot.replaceChild(viewTarget!, printTarget!)
524
+ printTarget!.remove()
525
+ mediaQueryList.removeEventListener('change', x)
526
+ }
527
+ }
528
+
529
+ if (window.matchMedia) {
530
+ var mediaQueryList = window.matchMedia('print')
531
+ mediaQueryList.addEventListener('change', x)
532
+ }
533
+ }
534
+ }
@@ -0,0 +1,93 @@
1
+ import { LitElement, PropertyValues, css, html } from 'lit'
2
+ import { customElement, property, query, state } from 'lit/decorators.js'
3
+
4
+ /**
5
+ * @class BoardWrapper
6
+ *
7
+ * @description scene provider로부터 제공받은 scene의 reference count control을 담당하며, resize 이벤트 발생시 scene의 사이즈를 fit 시키는 역할을 담당한다.
8
+ */
9
+ @customElement('ox-board-wrapper')
10
+ class BoardWrapper extends LitElement {
11
+ static styles = [
12
+ css`
13
+ :host {
14
+ position: relative;
15
+ }
16
+
17
+ #target {
18
+ display: block;
19
+ width: 100%;
20
+ height: 100%;
21
+ outline: 0;
22
+ }
23
+ `
24
+ ]
25
+
26
+ @property({ type: String }) sceneId!: string
27
+ @property({ type: Object }) provider!: any
28
+
29
+ @state() _scene: any
30
+
31
+ @query('#target') _targetEl!: HTMLElement
32
+
33
+ render() {
34
+ return html` <div id="target"></div> `
35
+ }
36
+
37
+ connectedCallback() {
38
+ super.connectedCallback()
39
+
40
+ window.addEventListener('resize', () => {
41
+ requestAnimationFrame(() => {
42
+ if (this._scene) {
43
+ this._scene.resize()
44
+
45
+ if (this.offsetWidth) {
46
+ this._scene.fit()
47
+ }
48
+ }
49
+ })
50
+ })
51
+ }
52
+
53
+ disconnectedCallback() {
54
+ super.disconnectedCallback()
55
+
56
+ this._releaseRef()
57
+ }
58
+
59
+ updated(changes: PropertyValues<this>) {
60
+ changes.has('sceneId') && this._onSceneIdChanged()
61
+ }
62
+
63
+ _releaseRef() {
64
+ if (this._scene) {
65
+ this._scene.target = null
66
+ this._scene.release()
67
+ delete this._scene
68
+ }
69
+ }
70
+
71
+ _onSceneIdChanged() {
72
+ if (!this.provider) return
73
+ this._releaseRef()
74
+
75
+ if (!this.sceneId) return
76
+
77
+ this.provider.get(this.sceneId, true).then(
78
+ (scene: any) => {
79
+ this._scene = scene
80
+ this._scene.target = this._targetEl
81
+
82
+ /* 이 컴포넌트의 폭이 값을 가지고 있으면 - 화면상에 자리를 잡고 보여지고 있음을 의미한다.
83
+ * 이 때는 정상적으로 그려주고,
84
+ * 그렇지 않으면, 다음 Resize Handling시에 처리하도록 한다.
85
+ */
86
+ if (this._scene.target.offsetWidth) {
87
+ this._scene.fit()
88
+ }
89
+ },
90
+ (e: any) => {}
91
+ )
92
+ }
93
+ }