@liwe3/webcomponents 1.1.0 → 1.1.10

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 (56) hide show
  1. package/dist/AIMarkdownEditor.d.ts +35 -0
  2. package/dist/AIMarkdownEditor.d.ts.map +1 -0
  3. package/dist/AIMarkdownEditor.js +412 -0
  4. package/dist/AIMarkdownEditor.js.map +1 -0
  5. package/dist/AITextEditor.d.ts +10 -0
  6. package/dist/AITextEditor.d.ts.map +1 -1
  7. package/dist/AITextEditor.js +63 -27
  8. package/dist/AITextEditor.js.map +1 -1
  9. package/dist/ButtonToolbar.d.ts +35 -0
  10. package/dist/ButtonToolbar.d.ts.map +1 -0
  11. package/dist/ButtonToolbar.js +220 -0
  12. package/dist/ButtonToolbar.js.map +1 -0
  13. package/dist/CheckList.d.ts +31 -0
  14. package/dist/CheckList.d.ts.map +1 -0
  15. package/dist/CheckList.js +336 -0
  16. package/dist/CheckList.js.map +1 -0
  17. package/dist/ChunkUploader.d.ts +22 -0
  18. package/dist/ChunkUploader.d.ts.map +1 -1
  19. package/dist/ChunkUploader.js +245 -103
  20. package/dist/ChunkUploader.js.map +1 -1
  21. package/dist/ComicBalloon.d.ts +82 -0
  22. package/dist/ComicBalloon.d.ts.map +1 -0
  23. package/dist/ComicBalloon.js +346 -0
  24. package/dist/ComicBalloon.js.map +1 -0
  25. package/dist/Dialog.d.ts +102 -0
  26. package/dist/Dialog.d.ts.map +1 -0
  27. package/dist/Dialog.js +299 -0
  28. package/dist/Dialog.js.map +1 -0
  29. package/dist/MarkdownPreview.d.ts +25 -0
  30. package/dist/MarkdownPreview.d.ts.map +1 -0
  31. package/dist/MarkdownPreview.js +147 -0
  32. package/dist/MarkdownPreview.js.map +1 -0
  33. package/dist/ResizableCropper.d.ts +158 -0
  34. package/dist/ResizableCropper.d.ts.map +1 -0
  35. package/dist/ResizableCropper.js +562 -0
  36. package/dist/ResizableCropper.js.map +1 -0
  37. package/dist/SmartSelect.d.ts +1 -0
  38. package/dist/SmartSelect.d.ts.map +1 -1
  39. package/dist/SmartSelect.js +45 -2
  40. package/dist/SmartSelect.js.map +1 -1
  41. package/dist/index.d.ts +16 -9
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +52 -29
  44. package/dist/index.js.map +1 -1
  45. package/package.json +33 -3
  46. package/src/AIMarkdownEditor.ts +568 -0
  47. package/src/AITextEditor.ts +97 -2
  48. package/src/ButtonToolbar.ts +302 -0
  49. package/src/CheckList.ts +438 -0
  50. package/src/ChunkUploader.ts +837 -623
  51. package/src/ComicBalloon.ts +709 -0
  52. package/src/Dialog.ts +510 -0
  53. package/src/MarkdownPreview.ts +213 -0
  54. package/src/ResizableCropper.ts +1099 -0
  55. package/src/SmartSelect.ts +48 -2
  56. package/src/index.ts +110 -47
@@ -0,0 +1,709 @@
1
+ /**
2
+ * ComicBalloon Web Component
3
+ * A customizable comic balloon with different styles and draggable handler
4
+ */
5
+
6
+ export enum BalloonType {
7
+ TALK = 'talk',
8
+ CLOUD = 'cloud',
9
+ WHISPER = 'whisper',
10
+ RECTANGLE = 'rectangle'
11
+ }
12
+
13
+ export type HandlerPosition = {
14
+ x: number;
15
+ y: number;
16
+ };
17
+
18
+ export interface IComicBalloon extends HTMLElement {
19
+ type: BalloonType;
20
+ textContent: string;
21
+ handlerPosition: HandlerPosition;
22
+ updateHandlerPosition( position: HandlerPosition ): void;
23
+ getHTML(): string;
24
+ }
25
+
26
+ export type ContentChangeEvent = CustomEvent<{
27
+ newContent: string;
28
+ balloonType: BalloonType;
29
+ }>;
30
+
31
+ export type HandlerMoveEvent = CustomEvent<{
32
+ finalPosition: HandlerPosition;
33
+ balloonType: BalloonType;
34
+ }>;
35
+
36
+ export type ResizeEvent = CustomEvent<{
37
+ width: number;
38
+ height: number;
39
+ balloonType: BalloonType;
40
+ }>;
41
+
42
+ export class ComicBalloonElement extends HTMLElement implements IComicBalloon {
43
+ declare shadowRoot: ShadowRoot;
44
+ private _type: BalloonType = BalloonType.TALK;
45
+ private _handlerPosition: HandlerPosition = { x: 50, y: 100 };
46
+ private isDragging: boolean = false;
47
+ private isResizing: boolean = false;
48
+ private dragStartOffset: { x: number; y: number } = { x: 0, y: 0 };
49
+ private resizeStartPos: { x: number; y: number } = { x: 0, y: 0 };
50
+ private contentEditableElement?: HTMLDivElement;
51
+ private handler?: HTMLElement;
52
+ private resizeHandle?: HTMLElement;
53
+ private balloon?: HTMLElement;
54
+
55
+ static get observedAttributes(): string[] {
56
+ return [ 'type', 'text' ];
57
+ }
58
+
59
+ constructor() {
60
+ super();
61
+ this.attachShadow( { mode: 'open' } );
62
+ }
63
+
64
+ connectedCallback(): void {
65
+ this.render();
66
+ this.setupEventListeners();
67
+ }
68
+
69
+ disconnectedCallback(): void {
70
+ this.removeEventListeners();
71
+ }
72
+
73
+ attributeChangedCallback( name: string, oldValue: string, newValue: string ): void {
74
+ if ( oldValue === newValue ) return;
75
+
76
+ if ( name === 'type' ) {
77
+ this._type = ( newValue as BalloonType ) || BalloonType.TALK;
78
+ this.updateBalloonStyle();
79
+ } else if ( name === 'text' ) {
80
+ if ( this.contentEditableElement ) {
81
+ this.contentEditableElement.textContent = newValue;
82
+ }
83
+ }
84
+ }
85
+
86
+ get type(): BalloonType {
87
+ return this._type;
88
+ }
89
+
90
+ set type( value: BalloonType ) {
91
+ this._type = value;
92
+ this.setAttribute( 'type', value );
93
+ }
94
+
95
+ get handlerPosition(): HandlerPosition {
96
+ return { ...this._handlerPosition };
97
+ }
98
+
99
+ set handlerPosition( value: HandlerPosition ) {
100
+ this._handlerPosition = { ...value };
101
+ this.updateHandlerVisual();
102
+ }
103
+
104
+ updateHandlerPosition( position: HandlerPosition ): void {
105
+ this.handlerPosition = position;
106
+ }
107
+
108
+ getHTML(): string {
109
+ return this.outerHTML;
110
+ }
111
+
112
+ private render(): void {
113
+ this.shadowRoot.innerHTML = `
114
+ <style>${this.getStyles()}</style>
115
+ <div class="comic-balloon-container">
116
+ <svg class="balloon-svg" xmlns="http://www.w3.org/2000/svg">
117
+ <g class="cloud-shape"></g>
118
+ </svg>
119
+ <div class="balloon ${this._type}">
120
+ <div class="content" contenteditable="true" role="textbox" aria-label="Balloon text">
121
+ ${this.getAttribute( 'text' ) || 'Type your text here...'}
122
+ </div>
123
+ <div class="resize-handle" role="button" aria-label="Resize balloon"></div>
124
+ </div>
125
+ <div class="handler ${this._type}" role="button" aria-label="Drag to reposition pointer"></div>
126
+ </div>
127
+ `;
128
+
129
+ this.contentEditableElement = this.shadowRoot.querySelector( '.content' ) as HTMLDivElement;
130
+ this.handler = this.shadowRoot.querySelector( '.handler' ) as HTMLElement;
131
+ this.resizeHandle = this.shadowRoot.querySelector( '.resize-handle' ) as HTMLElement;
132
+ this.balloon = this.shadowRoot.querySelector( '.balloon' ) as HTMLElement;
133
+
134
+ if ( this._type === BalloonType.CLOUD ) {
135
+ this.updateCloudShape();
136
+ }
137
+ this.updateHandlerVisual();
138
+ }
139
+
140
+ private getStyles(): string {
141
+ return `
142
+ :host {
143
+ display: inline-block;
144
+ position: relative;
145
+ width: 300px;
146
+ min-height: 150px;
147
+ overflow: visible;
148
+ }
149
+
150
+ .comic-balloon-container {
151
+ position: relative;
152
+ width: 100%;
153
+ height: 100%;
154
+ padding: 50px;
155
+ margin: -50px;
156
+ }
157
+
158
+ .balloon-svg {
159
+ position: absolute;
160
+ top: 0;
161
+ left: 0;
162
+ width: 100%;
163
+ height: 100%;
164
+ pointer-events: none;
165
+ z-index: 1;
166
+ overflow: visible;
167
+ }
168
+
169
+ .balloon {
170
+ position: relative;
171
+ padding: 20px;
172
+ background: white;
173
+ border: 3px solid #000;
174
+ min-height: 100px;
175
+ z-index: 2;
176
+ margin: 50px;
177
+ width: calc(100% - 100px);
178
+ height: calc(100% - 100px);
179
+ box-sizing: border-box;
180
+ }
181
+
182
+ .balloon.talk {
183
+ border-radius: 25px;
184
+ }
185
+
186
+ .balloon.cloud {
187
+ border: none;
188
+ background: transparent;
189
+ position: relative;
190
+ }
191
+
192
+ .balloon.cloud .content {
193
+ position: relative;
194
+ z-index: 10;
195
+ }
196
+
197
+ .balloon.whisper {
198
+ border-radius: 25px;
199
+ border: 3px dashed #000;
200
+ }
201
+
202
+ .balloon.rectangle {
203
+ border-radius: 5px;
204
+ }
205
+
206
+ .content {
207
+ outline: none;
208
+ min-height: 60px;
209
+ font-family: 'Comic Sans MS', cursive, sans-serif;
210
+ font-size: 16px;
211
+ line-height: 1.4;
212
+ color: #000;
213
+ display: flex;
214
+ align-items: center;
215
+ justify-content: center;
216
+ text-align: center;
217
+ }
218
+
219
+ .content:empty:before {
220
+ content: attr(aria-label);
221
+ color: #999;
222
+ }
223
+
224
+ .handler {
225
+ position: absolute;
226
+ width: 20px;
227
+ height: 20px;
228
+ cursor: move;
229
+ z-index: 3;
230
+ user-select: none;
231
+ background: rgba(0, 0, 0, 0.1);
232
+ border-radius: 50%;
233
+ border: 2px solid rgba(0, 0, 0, 0.3);
234
+ }
235
+
236
+ .handler:hover {
237
+ background: rgba(0, 0, 0, 0.2);
238
+ border-color: rgba(0, 0, 0, 0.5);
239
+ }
240
+
241
+ .handler.dragging {
242
+ cursor: grabbing;
243
+ background: rgba(0, 0, 0, 0.3);
244
+ }
245
+
246
+ .resize-handle {
247
+ position: absolute;
248
+ bottom: 0;
249
+ right: 0;
250
+ width: 20px;
251
+ height: 20px;
252
+ cursor: nwse-resize;
253
+ z-index: 4;
254
+ background: linear-gradient(135deg, transparent 50%, rgba(102, 126, 234, 0.5) 50%);
255
+ border-bottom-right-radius: inherit;
256
+ }
257
+
258
+ .resize-handle:hover {
259
+ background: linear-gradient(135deg, transparent 50%, rgba(102, 126, 234, 0.8) 50%);
260
+ }
261
+
262
+ .resize-handle.resizing {
263
+ background: linear-gradient(135deg, transparent 50%, rgba(102, 126, 234, 1) 50%);
264
+ }
265
+ `;
266
+ }
267
+
268
+ private updateBalloonStyle(): void {
269
+ if ( !this.balloon || !this.handler ) return;
270
+
271
+ this.balloon.className = `balloon ${this._type}`;
272
+ this.handler.className = `handler ${this._type}`;
273
+
274
+ if ( this._type === BalloonType.CLOUD ) {
275
+ this.updateCloudShape();
276
+ }
277
+
278
+ this.updateHandlerVisual();
279
+ }
280
+
281
+ private updateCloudShape(): void {
282
+ if ( !this.balloon ) return;
283
+
284
+ const cloudGroup = this.shadowRoot.querySelector( '.cloud-shape' ) as SVGGElement;
285
+ if ( !cloudGroup ) return;
286
+
287
+ const rect = this.balloon.getBoundingClientRect();
288
+ const containerRect = this.shadowRoot.querySelector( '.comic-balloon-container' )?.getBoundingClientRect();
289
+ if ( !containerRect ) return;
290
+
291
+ const x = rect.left - containerRect.left;
292
+ const y = rect.top - containerRect.top;
293
+ const w = rect.width;
294
+ const h = rect.height;
295
+
296
+ cloudGroup.innerHTML = '';
297
+
298
+ const centerX = x + w / 2;
299
+ const centerY = y + h / 2;
300
+
301
+ const numScallops = 14;
302
+ const radiusX = w / 2.5;
303
+ const radiusY = h / 2.5;
304
+
305
+ let pathData = '';
306
+
307
+ for ( let i = 0; i <= numScallops; i++ ) {
308
+ const angle = ( i / numScallops ) * Math.PI * 2 - Math.PI / 2;
309
+ const nextAngle = ( ( i + 1 ) / numScallops ) * Math.PI * 2 - Math.PI / 2;
310
+
311
+ const px = centerX + Math.cos( angle ) * radiusX;
312
+ const py = centerY + Math.sin( angle ) * radiusY;
313
+
314
+ const nx = centerX + Math.cos( nextAngle ) * radiusX;
315
+ const ny = centerY + Math.sin( nextAngle ) * radiusY;
316
+
317
+ // Chord midpoint
318
+ const mx = ( px + nx ) / 2;
319
+ const my = ( py + ny ) / 2;
320
+
321
+ // Chord length
322
+ const dx = nx - px;
323
+ const dy = ny - py;
324
+ const chordLen = Math.sqrt( dx * dx + dy * dy );
325
+
326
+ // Vector from center to midpoint
327
+ const vx = mx - centerX;
328
+ const vy = my - centerY;
329
+ const vLen = Math.sqrt( vx * vx + vy * vy );
330
+
331
+ // Normalized vector
332
+ const nx_vec = vx / vLen;
333
+ const ny_vec = vy / vLen;
334
+
335
+ // Control point distance
336
+ const scallopHeight = chordLen * 0.8;
337
+
338
+ const cx = mx + nx_vec * scallopHeight;
339
+ const cy = my + ny_vec * scallopHeight;
340
+
341
+ if ( i === 0 ) {
342
+ pathData += `M ${px} ${py} `;
343
+ }
344
+
345
+ pathData += `Q ${cx} ${cy} ${nx} ${ny} `;
346
+ }
347
+
348
+ pathData += 'Z';
349
+
350
+ const path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
351
+ path.setAttribute( 'd', pathData );
352
+ path.setAttribute( 'fill', 'white' );
353
+ path.setAttribute( 'stroke', '#000' );
354
+ path.setAttribute( 'stroke-width', '3' );
355
+ path.setAttribute( 'stroke-linejoin', 'round' );
356
+
357
+ cloudGroup.appendChild( path );
358
+ }
359
+
360
+ private updateHandlerVisual(): void {
361
+ if ( !this.handler || !this.balloon ) return;
362
+
363
+ const relativeX = this._handlerPosition.x;
364
+ const relativeY = this._handlerPosition.y;
365
+
366
+ this.handler.style.left = `${relativeX - 10}px`;
367
+ this.handler.style.top = `${relativeY - 10}px`;
368
+
369
+ if ( this._type === BalloonType.CLOUD ) {
370
+ this.updateCloudShape();
371
+ }
372
+
373
+ this.updateSVGPointer();
374
+ }
375
+
376
+ private updateSVGPointer(): void {
377
+ const svg = this.shadowRoot.querySelector( '.balloon-svg' ) as SVGElement;
378
+ if ( !svg || !this.balloon ) return;
379
+
380
+ const balloonRect = this.balloon.getBoundingClientRect();
381
+ const containerRect = svg.getBoundingClientRect();
382
+
383
+ let pointerGroup = svg.querySelector( '.pointer-group' );
384
+ if ( !pointerGroup ) {
385
+ pointerGroup = document.createElementNS( 'http://www.w3.org/2000/svg', 'g' );
386
+ pointerGroup.setAttribute( 'class', 'pointer-group' );
387
+ svg.appendChild( pointerGroup );
388
+ }
389
+
390
+ pointerGroup.innerHTML = '';
391
+
392
+ const balloonLeft = balloonRect.left - containerRect.left;
393
+ const balloonTop = balloonRect.top - containerRect.top;
394
+ const balloonCenterX = balloonLeft + balloonRect.width / 2;
395
+ const balloonCenterY = balloonTop + balloonRect.height / 2;
396
+
397
+ const handlerX = this._handlerPosition.x;
398
+ const handlerY = this._handlerPosition.y;
399
+
400
+ const angle = Math.atan2( handlerY - balloonCenterY, handlerX - balloonCenterX );
401
+
402
+ const balloonRadiusX = balloonRect.width / 2;
403
+ const balloonRadiusY = balloonRect.height / 2;
404
+
405
+ const edgeX = balloonCenterX + Math.cos( angle ) * balloonRadiusX * 0.85;
406
+ const edgeY = balloonCenterY + Math.sin( angle ) * balloonRadiusY * 0.85;
407
+
408
+ if ( this._type === BalloonType.TALK || this._type === BalloonType.RECTANGLE ) {
409
+ this.drawTalkPointer( pointerGroup as SVGGElement, edgeX, edgeY, handlerX, handlerY );
410
+ } else if ( this._type === BalloonType.CLOUD ) {
411
+ this.drawCloudPointer( pointerGroup as SVGGElement, edgeX, edgeY, handlerX, handlerY );
412
+ } else if ( this._type === BalloonType.WHISPER ) {
413
+ this.drawWhisperPointer( pointerGroup as SVGGElement, edgeX, edgeY, handlerX, handlerY );
414
+ }
415
+ }
416
+
417
+ private drawTalkPointer( group: SVGGElement, edgeX: number, edgeY: number, handlerX: number, handlerY: number ): void {
418
+ const angle = Math.atan2( handlerY - edgeY, handlerX - edgeX );
419
+ const perpAngle = angle + Math.PI / 2;
420
+
421
+ const baseWidth = 20;
422
+
423
+ const base1X = edgeX + Math.cos( perpAngle ) * baseWidth / 2;
424
+ const base1Y = edgeY + Math.sin( perpAngle ) * baseWidth / 2;
425
+ const base2X = edgeX - Math.cos( perpAngle ) * baseWidth / 2;
426
+ const base2Y = edgeY - Math.sin( perpAngle ) * baseWidth / 2;
427
+ const tipX = handlerX;
428
+ const tipY = handlerY;
429
+
430
+ const path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
431
+ const d = `M ${base1X} ${base1Y} L ${tipX} ${tipY} L ${base2X} ${base2Y} Z`;
432
+ path.setAttribute( 'd', d );
433
+ path.setAttribute( 'fill', 'white' );
434
+ path.setAttribute( 'stroke', '#000' );
435
+ path.setAttribute( 'stroke-width', '3' );
436
+ path.setAttribute( 'stroke-linejoin', 'round' );
437
+
438
+ group.appendChild( path );
439
+ }
440
+
441
+ private drawCloudPointer( group: SVGGElement, edgeX: number, edgeY: number, handlerX: number, handlerY: number ): void {
442
+ const dx = handlerX - edgeX;
443
+ const dy = handlerY - edgeY;
444
+
445
+ const numBubbles = 3;
446
+ for ( let i = 0; i < numBubbles; i++ ) {
447
+ const t = ( i + 1 ) / ( numBubbles + 1 );
448
+ const x = edgeX + dx * t;
449
+ const y = edgeY + dy * t;
450
+ const radius = 8 - i * 2;
451
+
452
+ const circle = document.createElementNS( 'http://www.w3.org/2000/svg', 'circle' );
453
+ circle.setAttribute( 'cx', x.toString() );
454
+ circle.setAttribute( 'cy', y.toString() );
455
+ circle.setAttribute( 'r', radius.toString() );
456
+ circle.setAttribute( 'fill', 'white' );
457
+ circle.setAttribute( 'stroke', '#000' );
458
+ circle.setAttribute( 'stroke-width', '2' );
459
+
460
+ group.appendChild( circle );
461
+ }
462
+ }
463
+
464
+ private drawWhisperPointer( group: SVGGElement, edgeX: number, edgeY: number, handlerX: number, handlerY: number ): void {
465
+ const angle = Math.atan2( handlerY - edgeY, handlerX - edgeX );
466
+ const perpAngle = angle + Math.PI / 2;
467
+
468
+ const baseWidth = 20;
469
+
470
+ const base1X = edgeX + Math.cos( perpAngle ) * baseWidth / 2;
471
+ const base1Y = edgeY + Math.sin( perpAngle ) * baseWidth / 2;
472
+ const base2X = edgeX - Math.cos( perpAngle ) * baseWidth / 2;
473
+ const base2Y = edgeY - Math.sin( perpAngle ) * baseWidth / 2;
474
+ const tipX = handlerX;
475
+ const tipY = handlerY;
476
+
477
+ const path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
478
+ const d = `M ${base1X} ${base1Y} L ${tipX} ${tipY} L ${base2X} ${base2Y}`;
479
+ path.setAttribute( 'd', d );
480
+ path.setAttribute( 'fill', 'none' );
481
+ path.setAttribute( 'stroke', '#000' );
482
+ path.setAttribute( 'stroke-width', '3' );
483
+ path.setAttribute( 'stroke-dasharray', '10 5' );
484
+ path.setAttribute( 'stroke-linejoin', 'round' );
485
+ path.setAttribute( 'stroke-linecap', 'round' );
486
+
487
+ group.appendChild( path );
488
+ }
489
+
490
+ private setupEventListeners(): void {
491
+ if ( !this.handler ) return;
492
+
493
+ this.handler.addEventListener( 'mousedown', this.handleMouseDown.bind( this ) );
494
+ this.handler.addEventListener( 'touchstart', this.handleTouchStart.bind( this ), { passive: false } );
495
+
496
+ if ( this.resizeHandle ) {
497
+ this.resizeHandle.addEventListener( 'mousedown', this.handleResizeMouseDown.bind( this ) );
498
+ this.resizeHandle.addEventListener( 'touchstart', this.handleResizeTouchStart.bind( this ), { passive: false } );
499
+ }
500
+
501
+ if ( this.contentEditableElement ) {
502
+ this.contentEditableElement.addEventListener( 'blur', this.handleContentBlur.bind( this ) );
503
+ }
504
+ }
505
+
506
+ private removeEventListeners(): void {
507
+ if ( !this.handler ) return;
508
+
509
+ this.handler.removeEventListener( 'mousedown', this.handleMouseDown.bind( this ) );
510
+ this.handler.removeEventListener( 'touchstart', this.handleTouchStart.bind( this ) );
511
+
512
+ if ( this.resizeHandle ) {
513
+ this.resizeHandle.removeEventListener( 'mousedown', this.handleResizeMouseDown.bind( this ) );
514
+ this.resizeHandle.removeEventListener( 'touchstart', this.handleResizeTouchStart.bind( this ) );
515
+ }
516
+
517
+ if ( this.contentEditableElement ) {
518
+ this.contentEditableElement.removeEventListener( 'blur', this.handleContentBlur.bind( this ) );
519
+ }
520
+ }
521
+
522
+ private handleMouseDown( e: MouseEvent ): void {
523
+ e.preventDefault();
524
+ this.startDrag( e.clientX, e.clientY );
525
+
526
+ const handleMouseMove = ( e: MouseEvent ) => this.handleDrag( e.clientX, e.clientY );
527
+ const handleMouseUp = () => {
528
+ this.stopDrag();
529
+ document.removeEventListener( 'mousemove', handleMouseMove );
530
+ document.removeEventListener( 'mouseup', handleMouseUp );
531
+ };
532
+
533
+ document.addEventListener( 'mousemove', handleMouseMove );
534
+ document.addEventListener( 'mouseup', handleMouseUp );
535
+ }
536
+
537
+ private handleTouchStart( e: TouchEvent ): void {
538
+ e.preventDefault();
539
+ const touch = e.touches[0];
540
+ this.startDrag( touch.clientX, touch.clientY );
541
+
542
+ const handleTouchMove = ( e: TouchEvent ) => {
543
+ const touch = e.touches[0];
544
+ this.handleDrag( touch.clientX, touch.clientY );
545
+ };
546
+ const handleTouchEnd = () => {
547
+ this.stopDrag();
548
+ document.removeEventListener( 'touchmove', handleTouchMove );
549
+ document.removeEventListener( 'touchend', handleTouchEnd );
550
+ };
551
+
552
+ document.addEventListener( 'touchmove', handleTouchMove, { passive: false } );
553
+ document.addEventListener( 'touchend', handleTouchEnd );
554
+ }
555
+
556
+ private startDrag( clientX: number, clientY: number ): void {
557
+ this.isDragging = true;
558
+ if ( this.handler ) {
559
+ this.handler.classList.add( 'dragging' );
560
+ }
561
+
562
+ const containerRect = this.shadowRoot.querySelector( '.comic-balloon-container' )?.getBoundingClientRect();
563
+ if ( containerRect ) {
564
+ this.dragStartOffset = {
565
+ x: clientX - containerRect.left - this._handlerPosition.x,
566
+ y: clientY - containerRect.top - this._handlerPosition.y
567
+ };
568
+ }
569
+ }
570
+
571
+ private handleDrag( clientX: number, clientY: number ): void {
572
+ if ( !this.isDragging ) return;
573
+
574
+ const containerRect = this.shadowRoot.querySelector( '.comic-balloon-container' )?.getBoundingClientRect();
575
+ if ( !containerRect ) return;
576
+
577
+ let newX = clientX - containerRect.left - this.dragStartOffset.x;
578
+ let newY = clientY - containerRect.top - this.dragStartOffset.y;
579
+
580
+ newX = Math.max( -50, Math.min( containerRect.width + 50, newX ) );
581
+ newY = Math.max( -50, Math.min( containerRect.height + 50, newY ) );
582
+
583
+ this._handlerPosition = { x: newX, y: newY };
584
+ this.updateHandlerVisual();
585
+ }
586
+
587
+ private stopDrag(): void {
588
+ this.isDragging = false;
589
+ if ( this.handler ) {
590
+ this.handler.classList.remove( 'dragging' );
591
+ }
592
+
593
+ this.dispatchEvent( new CustomEvent<HandlerMoveEvent['detail']>( 'handler-move', {
594
+ detail: {
595
+ finalPosition: { ...this._handlerPosition },
596
+ balloonType: this._type
597
+ },
598
+ bubbles: true,
599
+ composed: true
600
+ } ) );
601
+ }
602
+
603
+ private handleContentBlur(): void {
604
+ if ( !this.contentEditableElement ) return;
605
+
606
+ this.dispatchEvent( new CustomEvent<ContentChangeEvent['detail']>( 'balloon-content-change', {
607
+ detail: {
608
+ newContent: this.contentEditableElement.textContent || '',
609
+ balloonType: this._type
610
+ },
611
+ bubbles: true,
612
+ composed: true
613
+ } ) );
614
+ }
615
+
616
+ private handleResizeMouseDown( e: MouseEvent ): void {
617
+ e.preventDefault();
618
+ e.stopPropagation();
619
+ this.startResize( e.clientX, e.clientY );
620
+
621
+ const handleMouseMove = ( e: MouseEvent ) => this.handleResize( e.clientX, e.clientY );
622
+ const handleMouseUp = () => {
623
+ this.stopResize();
624
+ document.removeEventListener( 'mousemove', handleMouseMove );
625
+ document.removeEventListener( 'mouseup', handleMouseUp );
626
+ };
627
+
628
+ document.addEventListener( 'mousemove', handleMouseMove );
629
+ document.addEventListener( 'mouseup', handleMouseUp );
630
+ }
631
+
632
+ private handleResizeTouchStart( e: TouchEvent ): void {
633
+ e.preventDefault();
634
+ e.stopPropagation();
635
+ const touch = e.touches[0];
636
+ this.startResize( touch.clientX, touch.clientY );
637
+
638
+ const handleTouchMove = ( e: TouchEvent ) => {
639
+ const touch = e.touches[0];
640
+ this.handleResize( touch.clientX, touch.clientY );
641
+ };
642
+ const handleTouchEnd = () => {
643
+ this.stopResize();
644
+ document.removeEventListener( 'touchmove', handleTouchMove );
645
+ document.removeEventListener( 'touchend', handleTouchEnd );
646
+ };
647
+
648
+ document.addEventListener( 'touchmove', handleTouchMove, { passive: false } );
649
+ document.addEventListener( 'touchend', handleTouchEnd );
650
+ }
651
+
652
+ private startResize( clientX: number, clientY: number ): void {
653
+ if ( !this.balloon || !this.resizeHandle ) return;
654
+
655
+ this.isResizing = true;
656
+ this.resizeHandle.classList.add( 'resizing' );
657
+
658
+ this.resizeStartPos = {
659
+ x: clientX,
660
+ y: clientY
661
+ };
662
+ }
663
+
664
+ private handleResize( clientX: number, clientY: number ): void {
665
+ if ( !this.isResizing ) return;
666
+
667
+ const deltaX = clientX - this.resizeStartPos.x;
668
+ const deltaY = clientY - this.resizeStartPos.y;
669
+
670
+ const hostRect = this.getBoundingClientRect();
671
+ const newWidth = Math.max( 150, hostRect.width + deltaX );
672
+ const newHeight = Math.max( 100, hostRect.height + deltaY );
673
+
674
+ this.style.width = `${newWidth}px`;
675
+ this.style.height = `${newHeight}px`;
676
+
677
+ this.resizeStartPos = {
678
+ x: clientX,
679
+ y: clientY
680
+ };
681
+
682
+ this.updateHandlerVisual();
683
+ }
684
+
685
+ private stopResize(): void {
686
+ if ( !this.isResizing || !this.balloon || !this.resizeHandle ) return;
687
+
688
+ this.isResizing = false;
689
+ this.resizeHandle.classList.remove( 'resizing' );
690
+
691
+ const rect = this.balloon.getBoundingClientRect();
692
+
693
+ this.dispatchEvent( new CustomEvent<ResizeEvent['detail']>( 'balloon-resize', {
694
+ detail: {
695
+ width: rect.width,
696
+ height: rect.height,
697
+ balloonType: this._type
698
+ },
699
+ bubbles: true,
700
+ composed: true
701
+ } ) );
702
+ }
703
+ }
704
+
705
+ export const defineComicBalloon = (): void => {
706
+ if ( typeof window !== 'undefined' && !customElements.get( 'comic-balloon' ) ) {
707
+ customElements.define( 'comic-balloon', ComicBalloonElement );
708
+ }
709
+ };