@operato/image-marker 8.0.0-alpha.18 → 8.0.0-alpha.23

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.
@@ -52,28 +52,15 @@ export class OxImageMarker extends BaseImageMarker {
52
52
 
53
53
  connectedCallback() {
54
54
  super.connectedCallback()
55
- // 키다운 이벤트 리스너 추가
56
- window.addEventListener('keydown', this.handleKeyDown)
55
+ this.setAttribute('tabindex', '0')
56
+ this.addEventListener('focus', this.onFocus)
57
+ this.addEventListener('blur', this.onBlur)
57
58
  }
58
59
 
59
60
  disconnectedCallback() {
60
61
  super.disconnectedCallback()
61
- // 키다운 이벤트 리스너 제거
62
- window.removeEventListener('keydown', this.handleKeyDown)
63
- }
64
-
65
- private handleKeyDown = (event: KeyboardEvent) => {
66
- if ((event.key === 'Delete' || event.key === 'Backspace') && this.selectedShape) {
67
- this.deleteSelectedShape()
68
- }
69
- }
70
-
71
- private deleteSelectedShape() {
72
- if (this.selectedShape) {
73
- this.shapes = this.shapes.filter(s => s.id !== this.selectedShape!.id)
74
- this.selectedShape = null
75
- this.requestUpdate()
76
- }
62
+ this.removeEventListener('focus', this.onFocus)
63
+ this.removeEventListener('blur', this.onBlur)
77
64
  }
78
65
 
79
66
  render() {
@@ -81,8 +68,9 @@ export class OxImageMarker extends BaseImageMarker {
81
68
  <div class="container">
82
69
  <svg
83
70
  @mousedown=${this.onSvgMouseDown}
84
- @wheel=${this.onWheel}
85
- @dblclick=${this.onDoubleClick}
71
+ @touchstart=${this.onSvgTouchStart}
72
+ @wheel=${this.onSvgWheel}
73
+ @dblclick=${this.onSvgDoubleClick}
86
74
  viewBox=${this.viewBox}
87
75
  >
88
76
  <image href=${this.imageUrl} @load=${this.onImageLoad}></image>
@@ -97,8 +85,9 @@ export class OxImageMarker extends BaseImageMarker {
97
85
  const { mode, shapeType } = e.detail
98
86
  this.setMode(mode, shapeType)
99
87
  }}
100
- @clicked-delete=${() => this.deleteSelectedShape()}
101
- @clicked-save=${() => {
88
+ @tap-fit-size=${() => this.resetZoom()}
89
+ @tap-delete=${() => this.deleteSelectedShape()}
90
+ @tap-save=${() => {
102
91
  this.dispatchEvent(
103
92
  new CustomEvent('shapes-changed', {
104
93
  detail: this.shapes,
@@ -112,40 +101,44 @@ export class OxImageMarker extends BaseImageMarker {
112
101
  }
113
102
 
114
103
  protected onSvgMouseDown(event: MouseEvent) {
115
- const svg = this.svg
104
+ event.preventDefault()
116
105
 
117
106
  this.selectedShape = null
118
107
  this.shapes = this.shapes.map(s => {
119
108
  return { ...s, selected: false }
120
109
  })
121
110
 
122
- this.selectedShape = null
123
111
  if (this.currentMode === 'move') {
124
- this.isPanning = true
125
- this.panStart = { x: event.clientX, y: event.clientY }
126
- svg.classList.add('dragging')
127
- svg.addEventListener('mousemove', this.onPanMouseMove)
128
- svg.addEventListener('mouseup', this.onPanMouseUp)
129
- } else if (this.currentMode === 'select') {
130
- this.startShapeDragging(event)
131
- svg.addEventListener('mousemove', this.onShapeMouseMove)
132
- svg.addEventListener('mouseup', this.onShapeMouseUp)
112
+ this.startPanning(event)
133
113
  } else if (this.currentMode === 'draw') {
134
114
  this.startDrawing(event)
135
- svg.addEventListener('mousemove', this.onMouseMove)
136
- svg.addEventListener('mouseup', this.onMouseUp)
137
115
  }
138
116
  }
139
117
 
140
- private startShapeDragging(event: MouseEvent) {
141
- const { x, y } = this.getSvgPoint(event)
142
- if (this.selectedShape) {
143
- this.offsetX = x - (this.selectedShape.x || this.selectedShape.cx || 0)
144
- this.offsetY = y - (this.selectedShape.y || this.selectedShape.cy || 0)
118
+ protected onSvgTouchStart = (event: TouchEvent) => {
119
+ event.preventDefault()
120
+
121
+ if (this.isDoubleTouch(event)) {
122
+ this.resetZoom()
123
+ }
124
+
125
+ if (event.touches.length === 1) {
126
+ this.selectedShape = null
127
+ this.shapes = this.shapes.map(s => {
128
+ return { ...s, selected: false }
129
+ })
130
+
131
+ if (this.currentMode === 'move') {
132
+ this.startPanning(event)
133
+ } else if (this.currentMode === 'draw') {
134
+ this.startDrawing(event)
135
+ }
136
+ } else if (event.touches.length === 2) {
137
+ this.startPinchZooming(event)
145
138
  }
146
139
  }
147
140
 
148
- private startDrawing(event: MouseEvent) {
141
+ private startDrawing(event: MouseEvent | TouchEvent) {
149
142
  const { x, y } = this.getSvgPoint(event)
150
143
 
151
144
  if (this.currentShapeType === 'text') {
@@ -165,6 +158,14 @@ export class OxImageMarker extends BaseImageMarker {
165
158
  this.startY = y
166
159
  this.drawing = true
167
160
  }
161
+
162
+ if ('touches' in event) {
163
+ this.svg.addEventListener('touchmove', this.onDrawingMouseMove)
164
+ this.svg.addEventListener('touchend', this.onDrawingMouseUp)
165
+ } else {
166
+ this.svg.addEventListener('mousemove', this.onDrawingMouseMove)
167
+ this.svg.addEventListener('mouseup', this.onDrawingMouseUp)
168
+ }
168
169
  }
169
170
 
170
171
  private setMode(mode: ImageMarkerMode, shapeType: ShapeType) {
@@ -172,8 +173,12 @@ export class OxImageMarker extends BaseImageMarker {
172
173
  this.currentShapeType = shapeType
173
174
  }
174
175
 
175
- private onMouseMove = (event: MouseEvent) => {
176
- if (!this.drawing) return
176
+ private onDrawingMouseMove = (event: MouseEvent | TouchEvent) => {
177
+ if (!this.drawing) {
178
+ return
179
+ }
180
+
181
+ event.preventDefault()
177
182
 
178
183
  const { x, y } = this.getSvgPoint(event)
179
184
 
@@ -273,11 +278,20 @@ export class OxImageMarker extends BaseImageMarker {
273
278
  }
274
279
  }
275
280
 
276
- private onMouseUp = () => {
281
+ private onDrawingMouseUp = (event: MouseEvent | TouchEvent) => {
282
+ event.preventDefault()
283
+
277
284
  this.drawing = false
285
+
278
286
  const svg = this.svg as SVGSVGElement
279
- svg.removeEventListener('mousemove', this.onMouseMove)
280
- svg.removeEventListener('mouseup', this.onMouseUp)
287
+
288
+ if ('touches' in event) {
289
+ svg.removeEventListener('touchmove', this.onDrawingMouseMove)
290
+ svg.removeEventListener('touchend', this.onDrawingMouseUp)
291
+ } else {
292
+ svg.removeEventListener('mousemove', this.onDrawingMouseMove)
293
+ svg.removeEventListener('mouseup', this.onDrawingMouseUp)
294
+ }
281
295
 
282
296
  if (this.selectedShape) {
283
297
  this.selectedShape.selected = true
@@ -286,12 +300,15 @@ export class OxImageMarker extends BaseImageMarker {
286
300
  this.requestUpdate()
287
301
  }
288
302
 
289
- private getSvgPoint(event: MouseEvent) {
303
+ private getSvgPoint(event: MouseEvent | TouchEvent) {
304
+ const { x, y } = this.getPointFromEvent(event)
305
+
290
306
  const svg = this.svg
291
307
  const point = svg.createSVGPoint()
292
- point.x = event.clientX
293
- point.y = event.clientY
308
+ point.x = x
309
+ point.y = y
294
310
  const ctm = svg.getScreenCTM()!.inverse()
311
+
295
312
  return point.matrixTransform(ctm)
296
313
  }
297
314
 
@@ -312,6 +329,7 @@ export class OxImageMarker extends BaseImageMarker {
312
329
  fill="${commonAttrs.stroke}"
313
330
  style="cursor: ${commonAttrs.cursor}; font-size: 16px;"
314
331
  @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
332
+ @touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
315
333
  @dblclick="${(e: MouseEvent) => this.onTextDoubleClick(e, shape)}"
316
334
  >${shape.text}</text>
317
335
  `
@@ -328,6 +346,7 @@ export class OxImageMarker extends BaseImageMarker {
328
346
  fill="${commonAttrs.fill}"
329
347
  style="cursor: ${commonAttrs.cursor}"
330
348
  @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
349
+ @touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
331
350
  ></rect>
332
351
  ${shape.selected ? this.renderResizeHandles(shape) : ''}
333
352
  `
@@ -343,6 +362,7 @@ export class OxImageMarker extends BaseImageMarker {
343
362
  fill="${commonAttrs.fill}"
344
363
  style="cursor: ${commonAttrs.cursor}"
345
364
  @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
365
+ @touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
346
366
  ></circle>
347
367
  ${shape.selected ? this.renderResizeHandles(shape) : ''}
348
368
  `
@@ -356,6 +376,7 @@ export class OxImageMarker extends BaseImageMarker {
356
376
  fill="${commonAttrs.fill}"
357
377
  style="cursor: ${commonAttrs.cursor}"
358
378
  @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
379
+ @touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
359
380
  ></polygon>
360
381
  ${shape.selected ? this.renderResizeHandles(shape) : ''}
361
382
  `
@@ -371,6 +392,7 @@ export class OxImageMarker extends BaseImageMarker {
371
392
  stroke-width="${commonAttrs['stroke-width']}"
372
393
  style="cursor: ${commonAttrs.cursor}"
373
394
  @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
395
+ @touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
374
396
  ></line>
375
397
  ${shape.selected ? this.renderResizeHandles(shape) : ''}
376
398
  `
@@ -392,6 +414,7 @@ export class OxImageMarker extends BaseImageMarker {
392
414
  marker-end="url(#arrowhead-${shape.id})"
393
415
  style="cursor: ${commonAttrs.cursor}"
394
416
  @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
417
+ @touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
395
418
  ></line>
396
419
  ${shape.selected ? this.renderResizeHandles(shape) : ''}
397
420
  `
@@ -401,8 +424,9 @@ export class OxImageMarker extends BaseImageMarker {
401
424
  }
402
425
  }
403
426
 
404
- private onShapeMouseDown(event: MouseEvent, shape: Shape) {
427
+ private onShapeMouseDown(event: MouseEvent | TouchEvent, shape: Shape) {
405
428
  event.stopPropagation()
429
+ event.preventDefault()
406
430
 
407
431
  this.shapes = this.shapes.map(s => ({
408
432
  ...s,
@@ -438,12 +462,22 @@ export class OxImageMarker extends BaseImageMarker {
438
462
  }
439
463
 
440
464
  const svg = this.svg as SVGSVGElement
441
- svg.addEventListener('mousemove', this.onShapeMouseMove)
442
- svg.addEventListener('mouseup', this.onShapeMouseUp)
465
+
466
+ if ('touches' in event) {
467
+ svg.addEventListener('touchmove', this.onShapeMouseMove)
468
+ svg.addEventListener('touchend', this.onShapeMouseUp)
469
+ } else {
470
+ svg.addEventListener('mousemove', this.onShapeMouseMove)
471
+ svg.addEventListener('mouseup', this.onShapeMouseUp)
472
+ }
443
473
  }
444
474
 
445
- private onShapeMouseMove = (event: MouseEvent) => {
446
- if (!this.selectedShape) return
475
+ private onShapeMouseMove = (event: MouseEvent | TouchEvent) => {
476
+ if (!this.selectedShape) {
477
+ return
478
+ }
479
+
480
+ event.preventDefault()
447
481
 
448
482
  const { x, y } = this.getSvgPoint(event)
449
483
 
@@ -481,21 +515,18 @@ export class OxImageMarker extends BaseImageMarker {
481
515
  this.requestUpdate()
482
516
  }
483
517
 
484
- private onShapeMouseUp = () => {
485
- const svg = this.svg as SVGSVGElement
486
- svg.removeEventListener('mousemove', this.onShapeMouseMove)
487
- svg.removeEventListener('mouseup', this.onShapeMouseUp)
488
- }
489
-
490
- private getScale(): number {
491
- if (!this.svg) return 1
492
-
493
- const [vbWidth, vbHeight] = this.viewBox.split(' ').map(Number)
518
+ private onShapeMouseUp = (event: MouseEvent | TouchEvent) => {
519
+ event.preventDefault()
494
520
 
495
- const scaleX = vbWidth / this.svg.clientWidth
496
- const scaleY = vbHeight / this.svg.clientHeight
521
+ const svg = this.svg as SVGSVGElement
497
522
 
498
- return Math.max(scaleX, scaleY)
523
+ if ('touches' in event) {
524
+ svg.removeEventListener('touchmove', this.onShapeMouseMove)
525
+ svg.removeEventListener('touchend', this.onShapeMouseUp)
526
+ } else {
527
+ svg.removeEventListener('mousemove', this.onShapeMouseMove)
528
+ svg.removeEventListener('mouseup', this.onShapeMouseUp)
529
+ }
499
530
  }
500
531
 
501
532
  private renderResizeHandles(shape: Shape) {
@@ -549,6 +580,7 @@ export class OxImageMarker extends BaseImageMarker {
549
580
  class="resize-handle"
550
581
  style="cursor: ${handle.cursor};"
551
582
  @mousedown="${(e: MouseEvent) => this.onHandleMouseDown(e, shape, handle)}"
583
+ @touchstart="${(e: TouchEvent) => this.onHandleMouseDown(e, shape, handle)}"
552
584
  ></rect>
553
585
  </g>
554
586
  `
@@ -556,27 +588,76 @@ export class OxImageMarker extends BaseImageMarker {
556
588
  `
557
589
  }
558
590
 
559
- private onHandleMouseDown(event: MouseEvent, shape: Shape, handle: any) {
591
+ private onHandleMouseDown(event: MouseEvent | TouchEvent, shape: Shape, handle: any) {
560
592
  event.stopPropagation()
593
+ event.preventDefault()
594
+
561
595
  this.selectedShape = shape
562
596
  this.offsetX = handle.x
563
597
  this.offsetY = handle.y
564
598
  const svg = this.svg as SVGSVGElement
565
- svg.addEventListener('mousemove', this.onHandleMouseMove)
566
- svg.addEventListener('mouseup', this.onHandleMouseUp)
599
+
600
+ if ('touches' in event) {
601
+ svg.addEventListener('touchmove', this.onHandleMouseMove)
602
+ svg.addEventListener('touchend', this.onHandleMouseUp)
603
+ } else {
604
+ svg.addEventListener('mousemove', this.onHandleMouseMove)
605
+ svg.addEventListener('mouseup', this.onHandleMouseUp)
606
+ }
567
607
  }
568
608
 
569
- private onHandleMouseMove = (event: MouseEvent) => {
570
- if (!this.selectedShape) return
609
+ private onHandleMouseMove = (event: MouseEvent | TouchEvent) => {
610
+ if (!this.selectedShape) {
611
+ return
612
+ }
613
+
614
+ event.preventDefault()
615
+
571
616
  const { x, y } = this.getSvgPoint(event)
572
617
 
573
618
  switch (this.selectedShape.type) {
574
- case 'text':
619
+ // case 'text':
620
+ // this.selectedShape.width = Math.abs(x - this.selectedShape.x!)
621
+ // this.selectedShape.height = Math.abs(y - this.selectedShape.y!)
622
+ // if (x < this.selectedShape.x!) this.selectedShape.x = x
623
+ // if (y < this.selectedShape.y!) this.selectedShape.y = y
624
+ // break
575
625
  case 'rectangle':
576
- this.selectedShape.width = Math.abs(x - this.selectedShape.x!)
577
- this.selectedShape.height = Math.abs(y - this.selectedShape.y!)
578
- if (x < this.selectedShape.x!) this.selectedShape.x = x
579
- if (y < this.selectedShape.y!) this.selectedShape.y = y
626
+ const startX = this.selectedShape.x!
627
+ const startY = this.selectedShape.y!
628
+ const width = this.selectedShape.width!
629
+ const height = this.selectedShape.height!
630
+
631
+ // 핸들의 좌표에 따라 너비 및 높이 조정
632
+ if (this.offsetX === startX && this.offsetY === startY) {
633
+ // 상좌 핸들 (왼쪽 위)
634
+ this.selectedShape.x = x
635
+ this.selectedShape.y = y
636
+ this.selectedShape.width = width + (startX - x)
637
+ this.selectedShape.height = height + (startY - y)
638
+ } else if (this.offsetX === startX + width && this.offsetY === startY) {
639
+ // 상우 핸들 (오른쪽 위)
640
+ this.selectedShape.y = y
641
+ this.selectedShape.width = Math.abs(x - startX)
642
+ this.selectedShape.height = height + (startY - y)
643
+ } else if (this.offsetX === startX && this.offsetY === startY + height) {
644
+ // 하좌 핸들 (왼쪽 아래)
645
+ this.selectedShape.x = x
646
+ this.selectedShape.width = width + (startX - x)
647
+ this.selectedShape.height = Math.abs(y - startY)
648
+ } else if (this.offsetX === startX + width && this.offsetY === startY + height) {
649
+ // 하우 핸들 (오른쪽 아래)
650
+ this.selectedShape.width = Math.abs(x - startX)
651
+ this.selectedShape.height = Math.abs(y - startY)
652
+ }
653
+
654
+ // 최소 너비 및 높이 유지
655
+ this.selectedShape.width = Math.max(10, this.selectedShape.width!)
656
+ this.selectedShape.height = Math.max(10, this.selectedShape.height!)
657
+
658
+ this.offsetX = x
659
+ this.offsetY = y
660
+
580
661
  break
581
662
  case 'circle':
582
663
  const dx = x - this.selectedShape.cx!
@@ -610,15 +691,25 @@ export class OxImageMarker extends BaseImageMarker {
610
691
  this.requestUpdate()
611
692
  }
612
693
 
613
- private onHandleMouseUp = () => {
694
+ private onHandleMouseUp = (event: MouseEvent | TouchEvent) => {
695
+ event.preventDefault()
696
+
614
697
  const svg = this.svg as SVGSVGElement
615
- svg.removeEventListener('mousemove', this.onHandleMouseMove)
616
- svg.removeEventListener('mouseup', this.onHandleMouseUp)
698
+
699
+ if ('touches' in event) {
700
+ svg.removeEventListener('touchmove', this.onHandleMouseMove)
701
+ svg.removeEventListener('touchend', this.onHandleMouseUp)
702
+ } else {
703
+ svg.removeEventListener('mousemove', this.onHandleMouseMove)
704
+ svg.removeEventListener('mouseup', this.onHandleMouseUp)
705
+ }
706
+
617
707
  this.selectedShape = null
618
708
  }
619
709
 
620
710
  private onTextDoubleClick(event: MouseEvent, shape: Shape) {
621
711
  event.stopPropagation()
712
+ event.preventDefault()
622
713
 
623
714
  const textElement = event.target as SVGTextElement
624
715
 
@@ -740,4 +831,26 @@ export class OxImageMarker extends BaseImageMarker {
740
831
 
741
832
  return context.measureText(text).width
742
833
  }
834
+
835
+ private onFocus = () => {
836
+ this.addEventListener('keydown', this.handleKeyDown)
837
+ }
838
+
839
+ private onBlur = () => {
840
+ this.removeEventListener('keydown', this.handleKeyDown)
841
+ }
842
+
843
+ private handleKeyDown = (event: KeyboardEvent) => {
844
+ if ((event.key === 'Delete' || event.key === 'Backspace') && this.selectedShape) {
845
+ this.deleteSelectedShape()
846
+ }
847
+ }
848
+
849
+ private deleteSelectedShape() {
850
+ if (this.selectedShape) {
851
+ this.shapes = this.shapes.filter(s => s.id !== this.selectedShape!.id)
852
+ this.selectedShape = null
853
+ this.requestUpdate()
854
+ }
855
+ }
743
856
  }
@@ -55,6 +55,22 @@ const Template: Story<ArgTypes> = ({ imageUrl, shapes = [] }: ArgTypes) => html`
55
55
  </style>
56
56
 
57
57
  <style>
58
+ html,
59
+ body {
60
+ margin: 0;
61
+ padding: 0;
62
+ overflow: hidden;
63
+
64
+ overscroll-behavior-y: none;
65
+
66
+ /* This is a font-stack that tries to use the system-default sans-serifs first */
67
+ font-family: Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
68
+ line-height: 1.5;
69
+ -webkit-font-smoothing: antialiased;
70
+
71
+ accent-color: var(--md-sys-color-primary);
72
+ }
73
+
58
74
  body {
59
75
  width: 100%;
60
76
  height: 800px;