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

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,8 @@ 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-delete=${() => this.deleteSelectedShape()}
89
+ @tap-save=${() => {
102
90
  this.dispatchEvent(
103
91
  new CustomEvent('shapes-changed', {
104
92
  detail: this.shapes,
@@ -112,40 +100,36 @@ export class OxImageMarker extends BaseImageMarker {
112
100
  }
113
101
 
114
102
  protected onSvgMouseDown(event: MouseEvent) {
115
- const svg = this.svg
116
-
117
103
  this.selectedShape = null
118
104
  this.shapes = this.shapes.map(s => {
119
105
  return { ...s, selected: false }
120
106
  })
121
107
 
122
- this.selectedShape = null
123
108
  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)
109
+ this.startPanning(event)
133
110
  } else if (this.currentMode === 'draw') {
134
111
  this.startDrawing(event)
135
- svg.addEventListener('mousemove', this.onMouseMove)
136
- svg.addEventListener('mouseup', this.onMouseUp)
137
112
  }
138
113
  }
139
114
 
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)
115
+ protected onSvgTouchStart = (event: TouchEvent) => {
116
+ if (event.touches.length === 1) {
117
+ this.selectedShape = null
118
+ this.shapes = this.shapes.map(s => {
119
+ return { ...s, selected: false }
120
+ })
121
+
122
+ if (this.currentMode === 'move') {
123
+ this.startPanning(event)
124
+ } else if (this.currentMode === 'draw') {
125
+ this.startDrawing(event)
126
+ }
127
+ } else if (event.touches.length === 2) {
128
+ this.startPinchZooming(event)
145
129
  }
146
130
  }
147
131
 
148
- private startDrawing(event: MouseEvent) {
132
+ private startDrawing(event: MouseEvent | TouchEvent) {
149
133
  const { x, y } = this.getSvgPoint(event)
150
134
 
151
135
  if (this.currentShapeType === 'text') {
@@ -165,6 +149,14 @@ export class OxImageMarker extends BaseImageMarker {
165
149
  this.startY = y
166
150
  this.drawing = true
167
151
  }
152
+
153
+ if ('touches' in event) {
154
+ this.svg.addEventListener('touchmove', this.onDrawingMouseMove)
155
+ this.svg.addEventListener('touchend', this.onDrawingMouseUp)
156
+ } else {
157
+ this.svg.addEventListener('mousemove', this.onDrawingMouseMove)
158
+ this.svg.addEventListener('mouseup', this.onDrawingMouseUp)
159
+ }
168
160
  }
169
161
 
170
162
  private setMode(mode: ImageMarkerMode, shapeType: ShapeType) {
@@ -172,8 +164,10 @@ export class OxImageMarker extends BaseImageMarker {
172
164
  this.currentShapeType = shapeType
173
165
  }
174
166
 
175
- private onMouseMove = (event: MouseEvent) => {
176
- if (!this.drawing) return
167
+ private onDrawingMouseMove = (event: MouseEvent | TouchEvent) => {
168
+ if (!this.drawing) {
169
+ return
170
+ }
177
171
 
178
172
  const { x, y } = this.getSvgPoint(event)
179
173
 
@@ -273,11 +267,18 @@ export class OxImageMarker extends BaseImageMarker {
273
267
  }
274
268
  }
275
269
 
276
- private onMouseUp = () => {
270
+ private onDrawingMouseUp = (event: MouseEvent | TouchEvent) => {
277
271
  this.drawing = false
272
+
278
273
  const svg = this.svg as SVGSVGElement
279
- svg.removeEventListener('mousemove', this.onMouseMove)
280
- svg.removeEventListener('mouseup', this.onMouseUp)
274
+
275
+ if ('touches' in event) {
276
+ svg.removeEventListener('touchmove', this.onDrawingMouseMove)
277
+ svg.removeEventListener('touchend', this.onDrawingMouseUp)
278
+ } else {
279
+ svg.removeEventListener('mousemove', this.onDrawingMouseMove)
280
+ svg.removeEventListener('mouseup', this.onDrawingMouseUp)
281
+ }
281
282
 
282
283
  if (this.selectedShape) {
283
284
  this.selectedShape.selected = true
@@ -286,12 +287,15 @@ export class OxImageMarker extends BaseImageMarker {
286
287
  this.requestUpdate()
287
288
  }
288
289
 
289
- private getSvgPoint(event: MouseEvent) {
290
+ private getSvgPoint(event: MouseEvent | TouchEvent) {
291
+ const { x, y } = this.getPointFromEvent(event)
292
+
290
293
  const svg = this.svg
291
294
  const point = svg.createSVGPoint()
292
- point.x = event.clientX
293
- point.y = event.clientY
295
+ point.x = x
296
+ point.y = y
294
297
  const ctm = svg.getScreenCTM()!.inverse()
298
+
295
299
  return point.matrixTransform(ctm)
296
300
  }
297
301
 
@@ -312,6 +316,7 @@ export class OxImageMarker extends BaseImageMarker {
312
316
  fill="${commonAttrs.stroke}"
313
317
  style="cursor: ${commonAttrs.cursor}; font-size: 16px;"
314
318
  @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
319
+ @touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
315
320
  @dblclick="${(e: MouseEvent) => this.onTextDoubleClick(e, shape)}"
316
321
  >${shape.text}</text>
317
322
  `
@@ -328,6 +333,7 @@ export class OxImageMarker extends BaseImageMarker {
328
333
  fill="${commonAttrs.fill}"
329
334
  style="cursor: ${commonAttrs.cursor}"
330
335
  @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
336
+ @touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
331
337
  ></rect>
332
338
  ${shape.selected ? this.renderResizeHandles(shape) : ''}
333
339
  `
@@ -343,6 +349,7 @@ export class OxImageMarker extends BaseImageMarker {
343
349
  fill="${commonAttrs.fill}"
344
350
  style="cursor: ${commonAttrs.cursor}"
345
351
  @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
352
+ @touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
346
353
  ></circle>
347
354
  ${shape.selected ? this.renderResizeHandles(shape) : ''}
348
355
  `
@@ -356,6 +363,7 @@ export class OxImageMarker extends BaseImageMarker {
356
363
  fill="${commonAttrs.fill}"
357
364
  style="cursor: ${commonAttrs.cursor}"
358
365
  @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
366
+ @touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
359
367
  ></polygon>
360
368
  ${shape.selected ? this.renderResizeHandles(shape) : ''}
361
369
  `
@@ -371,6 +379,7 @@ export class OxImageMarker extends BaseImageMarker {
371
379
  stroke-width="${commonAttrs['stroke-width']}"
372
380
  style="cursor: ${commonAttrs.cursor}"
373
381
  @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
382
+ @touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
374
383
  ></line>
375
384
  ${shape.selected ? this.renderResizeHandles(shape) : ''}
376
385
  `
@@ -392,6 +401,7 @@ export class OxImageMarker extends BaseImageMarker {
392
401
  marker-end="url(#arrowhead-${shape.id})"
393
402
  style="cursor: ${commonAttrs.cursor}"
394
403
  @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
404
+ @touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
395
405
  ></line>
396
406
  ${shape.selected ? this.renderResizeHandles(shape) : ''}
397
407
  `
@@ -401,7 +411,7 @@ export class OxImageMarker extends BaseImageMarker {
401
411
  }
402
412
  }
403
413
 
404
- private onShapeMouseDown(event: MouseEvent, shape: Shape) {
414
+ private onShapeMouseDown(event: MouseEvent | TouchEvent, shape: Shape) {
405
415
  event.stopPropagation()
406
416
 
407
417
  this.shapes = this.shapes.map(s => ({
@@ -438,12 +448,20 @@ export class OxImageMarker extends BaseImageMarker {
438
448
  }
439
449
 
440
450
  const svg = this.svg as SVGSVGElement
441
- svg.addEventListener('mousemove', this.onShapeMouseMove)
442
- svg.addEventListener('mouseup', this.onShapeMouseUp)
451
+
452
+ if ('touches' in event) {
453
+ svg.addEventListener('touchmove', this.onShapeMouseMove)
454
+ svg.addEventListener('touchend', this.onShapeMouseUp)
455
+ } else {
456
+ svg.addEventListener('mousemove', this.onShapeMouseMove)
457
+ svg.addEventListener('mouseup', this.onShapeMouseUp)
458
+ }
443
459
  }
444
460
 
445
- private onShapeMouseMove = (event: MouseEvent) => {
446
- if (!this.selectedShape) return
461
+ private onShapeMouseMove = (event: MouseEvent | TouchEvent) => {
462
+ if (!this.selectedShape) {
463
+ return
464
+ }
447
465
 
448
466
  const { x, y } = this.getSvgPoint(event)
449
467
 
@@ -481,21 +499,16 @@ export class OxImageMarker extends BaseImageMarker {
481
499
  this.requestUpdate()
482
500
  }
483
501
 
484
- private onShapeMouseUp = () => {
502
+ private onShapeMouseUp = (event: MouseEvent | TouchEvent) => {
485
503
  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)
494
504
 
495
- const scaleX = vbWidth / this.svg.clientWidth
496
- const scaleY = vbHeight / this.svg.clientHeight
497
-
498
- return Math.max(scaleX, scaleY)
505
+ if ('touches' in event) {
506
+ svg.removeEventListener('touchmove', this.onShapeMouseMove)
507
+ svg.removeEventListener('touchend', this.onShapeMouseUp)
508
+ } else {
509
+ svg.removeEventListener('mousemove', this.onShapeMouseMove)
510
+ svg.removeEventListener('mouseup', this.onShapeMouseUp)
511
+ }
499
512
  }
500
513
 
501
514
  private renderResizeHandles(shape: Shape) {
@@ -549,6 +562,7 @@ export class OxImageMarker extends BaseImageMarker {
549
562
  class="resize-handle"
550
563
  style="cursor: ${handle.cursor};"
551
564
  @mousedown="${(e: MouseEvent) => this.onHandleMouseDown(e, shape, handle)}"
565
+ @touchstart="${(e: TouchEvent) => this.onHandleMouseDown(e, shape, handle)}"
552
566
  ></rect>
553
567
  </g>
554
568
  `
@@ -556,27 +570,73 @@ export class OxImageMarker extends BaseImageMarker {
556
570
  `
557
571
  }
558
572
 
559
- private onHandleMouseDown(event: MouseEvent, shape: Shape, handle: any) {
573
+ private onHandleMouseDown(event: MouseEvent | TouchEvent, shape: Shape, handle: any) {
560
574
  event.stopPropagation()
575
+
561
576
  this.selectedShape = shape
562
577
  this.offsetX = handle.x
563
578
  this.offsetY = handle.y
564
579
  const svg = this.svg as SVGSVGElement
565
- svg.addEventListener('mousemove', this.onHandleMouseMove)
566
- svg.addEventListener('mouseup', this.onHandleMouseUp)
580
+
581
+ if ('touches' in event) {
582
+ svg.addEventListener('touchmove', this.onHandleMouseMove)
583
+ svg.addEventListener('touchend', this.onHandleMouseUp)
584
+ } else {
585
+ svg.addEventListener('mousemove', this.onHandleMouseMove)
586
+ svg.addEventListener('mouseup', this.onHandleMouseUp)
587
+ }
567
588
  }
568
589
 
569
- private onHandleMouseMove = (event: MouseEvent) => {
570
- if (!this.selectedShape) return
590
+ private onHandleMouseMove = (event: MouseEvent | TouchEvent) => {
591
+ if (!this.selectedShape) {
592
+ return
593
+ }
594
+
571
595
  const { x, y } = this.getSvgPoint(event)
572
596
 
573
597
  switch (this.selectedShape.type) {
574
- case 'text':
598
+ // case 'text':
599
+ // this.selectedShape.width = Math.abs(x - this.selectedShape.x!)
600
+ // this.selectedShape.height = Math.abs(y - this.selectedShape.y!)
601
+ // if (x < this.selectedShape.x!) this.selectedShape.x = x
602
+ // if (y < this.selectedShape.y!) this.selectedShape.y = y
603
+ // break
575
604
  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
605
+ const startX = this.selectedShape.x!
606
+ const startY = this.selectedShape.y!
607
+ const width = this.selectedShape.width!
608
+ const height = this.selectedShape.height!
609
+
610
+ // 핸들의 좌표에 따라 너비 및 높이 조정
611
+ if (this.offsetX === startX && this.offsetY === startY) {
612
+ // 상좌 핸들 (왼쪽 위)
613
+ this.selectedShape.x = x
614
+ this.selectedShape.y = y
615
+ this.selectedShape.width = width + (startX - x)
616
+ this.selectedShape.height = height + (startY - y)
617
+ } else if (this.offsetX === startX + width && this.offsetY === startY) {
618
+ // 상우 핸들 (오른쪽 위)
619
+ this.selectedShape.y = y
620
+ this.selectedShape.width = Math.abs(x - startX)
621
+ this.selectedShape.height = height + (startY - y)
622
+ } else if (this.offsetX === startX && this.offsetY === startY + height) {
623
+ // 하좌 핸들 (왼쪽 아래)
624
+ this.selectedShape.x = x
625
+ this.selectedShape.width = width + (startX - x)
626
+ this.selectedShape.height = Math.abs(y - startY)
627
+ } else if (this.offsetX === startX + width && this.offsetY === startY + height) {
628
+ // 하우 핸들 (오른쪽 아래)
629
+ this.selectedShape.width = Math.abs(x - startX)
630
+ this.selectedShape.height = Math.abs(y - startY)
631
+ }
632
+
633
+ // 최소 너비 및 높이 유지
634
+ this.selectedShape.width = Math.max(10, this.selectedShape.width!)
635
+ this.selectedShape.height = Math.max(10, this.selectedShape.height!)
636
+
637
+ this.offsetX = x
638
+ this.offsetY = y
639
+
580
640
  break
581
641
  case 'circle':
582
642
  const dx = x - this.selectedShape.cx!
@@ -610,10 +670,17 @@ export class OxImageMarker extends BaseImageMarker {
610
670
  this.requestUpdate()
611
671
  }
612
672
 
613
- private onHandleMouseUp = () => {
673
+ private onHandleMouseUp = (event: MouseEvent | TouchEvent) => {
614
674
  const svg = this.svg as SVGSVGElement
615
- svg.removeEventListener('mousemove', this.onHandleMouseMove)
616
- svg.removeEventListener('mouseup', this.onHandleMouseUp)
675
+
676
+ if ('touches' in event) {
677
+ svg.removeEventListener('touchmove', this.onHandleMouseMove)
678
+ svg.removeEventListener('touchend', this.onHandleMouseUp)
679
+ } else {
680
+ svg.removeEventListener('mousemove', this.onHandleMouseMove)
681
+ svg.removeEventListener('mouseup', this.onHandleMouseUp)
682
+ }
683
+
617
684
  this.selectedShape = null
618
685
  }
619
686
 
@@ -740,4 +807,26 @@ export class OxImageMarker extends BaseImageMarker {
740
807
 
741
808
  return context.measureText(text).width
742
809
  }
810
+
811
+ private onFocus = () => {
812
+ this.addEventListener('keydown', this.handleKeyDown)
813
+ }
814
+
815
+ private onBlur = () => {
816
+ this.removeEventListener('keydown', this.handleKeyDown)
817
+ }
818
+
819
+ private handleKeyDown = (event: KeyboardEvent) => {
820
+ if ((event.key === 'Delete' || event.key === 'Backspace') && this.selectedShape) {
821
+ this.deleteSelectedShape()
822
+ }
823
+ }
824
+
825
+ private deleteSelectedShape() {
826
+ if (this.selectedShape) {
827
+ this.shapes = this.shapes.filter(s => s.id !== this.selectedShape!.id)
828
+ this.selectedShape = null
829
+ this.requestUpdate()
830
+ }
831
+ }
743
832
  }
@@ -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;