@liwe3/webcomponents 1.0.14 → 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 (85) 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 +183 -0
  6. package/dist/AITextEditor.d.ts.map +1 -0
  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 +125 -0
  18. package/dist/ChunkUploader.d.ts.map +1 -0
  19. package/dist/ChunkUploader.js +756 -0
  20. package/dist/ChunkUploader.js.map +1 -0
  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/ContainerBox.d.ts +112 -0
  26. package/dist/ContainerBox.d.ts.map +1 -0
  27. package/dist/ContainerBox.js +359 -0
  28. package/dist/ContainerBox.js.map +1 -0
  29. package/dist/DateSelector.d.ts +103 -0
  30. package/dist/DateSelector.d.ts.map +1 -0
  31. package/dist/Dialog.d.ts +102 -0
  32. package/dist/Dialog.d.ts.map +1 -0
  33. package/dist/Dialog.js +299 -0
  34. package/dist/Dialog.js.map +1 -0
  35. package/dist/Drawer.d.ts +63 -0
  36. package/dist/Drawer.d.ts.map +1 -0
  37. package/dist/Drawer.js +340 -0
  38. package/dist/Drawer.js.map +1 -0
  39. package/dist/ImageView.d.ts +42 -0
  40. package/dist/ImageView.d.ts.map +1 -0
  41. package/dist/ImageView.js +209 -0
  42. package/dist/ImageView.js.map +1 -0
  43. package/dist/MarkdownPreview.d.ts +25 -0
  44. package/dist/MarkdownPreview.d.ts.map +1 -0
  45. package/dist/MarkdownPreview.js +147 -0
  46. package/dist/MarkdownPreview.js.map +1 -0
  47. package/dist/PopoverMenu.d.ts +103 -0
  48. package/dist/PopoverMenu.d.ts.map +1 -0
  49. package/dist/ResizableCropper.d.ts +158 -0
  50. package/dist/ResizableCropper.d.ts.map +1 -0
  51. package/dist/ResizableCropper.js +562 -0
  52. package/dist/ResizableCropper.js.map +1 -0
  53. package/dist/SmartSelect.d.ts +100 -0
  54. package/dist/SmartSelect.d.ts.map +1 -0
  55. package/dist/SmartSelect.js +45 -2
  56. package/dist/SmartSelect.js.map +1 -1
  57. package/dist/Toast.d.ts +127 -0
  58. package/dist/Toast.d.ts.map +1 -0
  59. package/dist/Toast.js +79 -49
  60. package/dist/Toast.js.map +1 -1
  61. package/dist/TreeView.d.ts +84 -0
  62. package/dist/TreeView.d.ts.map +1 -0
  63. package/dist/TreeView.js +478 -0
  64. package/dist/TreeView.js.map +1 -0
  65. package/dist/index.d.ts +23 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +51 -14
  68. package/dist/index.js.map +1 -1
  69. package/package.json +60 -5
  70. package/src/AIMarkdownEditor.ts +568 -0
  71. package/src/AITextEditor.ts +97 -2
  72. package/src/ButtonToolbar.ts +302 -0
  73. package/src/CheckList.ts +438 -0
  74. package/src/ChunkUploader.ts +1135 -0
  75. package/src/ComicBalloon.ts +709 -0
  76. package/src/ContainerBox.ts +570 -0
  77. package/src/Dialog.ts +510 -0
  78. package/src/Drawer.ts +435 -0
  79. package/src/ImageView.ts +265 -0
  80. package/src/MarkdownPreview.ts +213 -0
  81. package/src/ResizableCropper.ts +1099 -0
  82. package/src/SmartSelect.ts +48 -2
  83. package/src/Toast.ts +96 -32
  84. package/src/TreeView.ts +673 -0
  85. package/src/index.ts +129 -27
@@ -0,0 +1,1099 @@
1
+ /**
2
+ * ResizableCropper Web Component
3
+ * A container that wraps a single child element with drag-to-scale (resizing) and drag-to-crop (panning) capabilities
4
+ */
5
+
6
+ export interface ResizableCropperState {
7
+ width : number;
8
+ height : number;
9
+ minWidth : number;
10
+ minHeight : number;
11
+ aspectRatio : string | null;
12
+ contentElement : HTMLElement | null;
13
+ contentLeft : number;
14
+ contentTop : number;
15
+ rotation : number;
16
+ wrapperLeft : number;
17
+ wrapperTop : number;
18
+ }
19
+
20
+ export interface ResizableCropperValues {
21
+ wrapperWidth : number;
22
+ wrapperHeight : number;
23
+ wrapperLeft : number;
24
+ wrapperTop : number;
25
+ contentWidth : number;
26
+ contentHeight : number;
27
+ contentLeft : number;
28
+ contentTop : number;
29
+ zoom : number;
30
+ rotation : number;
31
+ }
32
+
33
+ export interface ResizableCropperComponentState {
34
+ mode : 'transform' | 'crop';
35
+ disabled : boolean;
36
+ allowCrop : boolean;
37
+ allowResize : boolean;
38
+ allowRotate : boolean;
39
+ allowDrag : boolean;
40
+ minWidth : number;
41
+ minHeight : number;
42
+ aspectRatio : string | null;
43
+ values : ResizableCropperValues;
44
+ }
45
+
46
+ export interface ResizableCropEventDetail {
47
+ width : number;
48
+ height : number;
49
+ wrapperLeft : number;
50
+ wrapperTop : number;
51
+ contentLeft : number;
52
+ contentTop : number;
53
+ action : 'scale' | 'crop' | 'pan' | 'rotate' | 'move';
54
+ rotation? : number;
55
+ handle? : string;
56
+ }
57
+
58
+ export class ResizableCropperElement extends HTMLElement {
59
+ declare shadowRoot : ShadowRoot;
60
+
61
+ private state : ResizableCropperState = {
62
+ width: 200,
63
+ height: 150,
64
+ minWidth: 50,
65
+ minHeight: 50,
66
+ aspectRatio: null,
67
+ contentElement: null,
68
+ contentLeft: 0,
69
+ contentTop: 0,
70
+ rotation: 0,
71
+ wrapperLeft: 0,
72
+ wrapperTop: 0,
73
+ };
74
+
75
+ private wrapper! : HTMLElement;
76
+ private contentSlot! : HTMLSlotElement;
77
+ private handlesContainer! : HTMLElement;
78
+
79
+ private isDragging = false;
80
+ private dragAction : 'scale' | 'crop' | 'pan' | 'rotate' | 'move' | null = null;
81
+ private dragHandle : string | null = null;
82
+ private dragStartX = 0;
83
+ private dragStartY = 0;
84
+ private dragStartWidth = 0;
85
+ private dragStartHeight = 0;
86
+ private dragStartContentLeft = 0;
87
+ private dragStartContentTop = 0;
88
+ private dragStartContentWidth = 0;
89
+ private dragStartContentHeight = 0;
90
+ private initialContentWidth = 0;
91
+ private initialContentHeight = 0;
92
+ private _dragStartRotation = 0;
93
+ private _dragStartPointerAngle = 0;
94
+ private _dragRotateCenterX = 0;
95
+ private _dragRotateCenterY = 0;
96
+ private _interactionMode : 'transform' | 'crop' = 'transform';
97
+ private _dragPointerOffsetX = 0;
98
+ private _dragPointerOffsetY = 0;
99
+ private _dragMoveContainer : HTMLElement | null = null;
100
+
101
+ constructor () {
102
+ super();
103
+ this.attachShadow( { mode: 'open' } );
104
+ this.render();
105
+ }
106
+
107
+ static get observedAttributes () : string[] {
108
+ return [ 'width', 'height', 'min-width', 'min-height', 'aspect-ratio', 'disabled', 'allow-crop', 'allow-resize', 'allow-rotate', 'allow-drag' ];
109
+ }
110
+
111
+ attributeChangedCallback ( name : string, oldValue : string | null, newValue : string | null ) : void {
112
+ if ( oldValue === newValue ) return;
113
+
114
+ switch ( name ) {
115
+ case 'width':
116
+ this.state.width = parseFloat( newValue || '200' );
117
+ this.updateWrapperDimensions();
118
+ break;
119
+ case 'height':
120
+ this.state.height = parseFloat( newValue || '150' );
121
+ this.updateWrapperDimensions();
122
+ break;
123
+ case 'min-width':
124
+ this.state.minWidth = parseFloat( newValue || '50' );
125
+ break;
126
+ case 'min-height':
127
+ this.state.minHeight = parseFloat( newValue || '50' );
128
+ break;
129
+ case 'aspect-ratio':
130
+ this.state.aspectRatio = newValue;
131
+ break;
132
+ case 'allow-crop':
133
+ case 'allow-resize':
134
+ case 'allow-rotate':
135
+ case 'allow-drag':
136
+ this.updateHandlesVisibility();
137
+ this._applyInteractionModeUI();
138
+ break;
139
+ }
140
+ }
141
+
142
+ connectedCallback () : void {
143
+ this.wrapper = this.shadowRoot.querySelector( '#wrapper' )!;
144
+ this.shadowRoot.querySelector( '#clipper' )!;
145
+ this.contentSlot = this.shadowRoot.querySelector( 'slot' )!;
146
+ this.handlesContainer = this.shadowRoot.querySelector( '#handles-container' )!;
147
+
148
+ this.updateWrapperDimensions();
149
+ this.updateHandlesVisibility();
150
+ this._applyInteractionModeUI();
151
+ this._syncWrapperPositionFromLayout( this._getContainerForMove() );
152
+ this.bindEvents();
153
+
154
+ this.contentSlot.addEventListener( 'slotchange', () => {
155
+ this.updateContentElement();
156
+ } );
157
+
158
+ this.updateContentElement();
159
+ }
160
+
161
+ disconnectedCallback () : void {
162
+ this.unbindEvents();
163
+ }
164
+
165
+ private _applyInteractionModeUI () : void {
166
+ if ( !this.wrapper ) return;
167
+ this.wrapper.style.cursor = this._interactionMode === 'transform' && this.allowDrag && !this.disabled ? 'move' : '';
168
+ }
169
+
170
+ private _setInteractionMode ( mode : 'transform' | 'crop' ) : void {
171
+ if ( this._interactionMode === mode ) return;
172
+ this._interactionMode = mode;
173
+ this.updateHandlesVisibility();
174
+ this._applyInteractionModeUI();
175
+ }
176
+
177
+ private updateContentElement () : void {
178
+ const nodes = this.contentSlot.assignedElements();
179
+ if ( nodes.length > 0 ) {
180
+ this.state.contentElement = nodes[0] as HTMLElement;
181
+ this.state.contentElement.style.position = 'absolute';
182
+ this.state.contentElement.style.left = `${this.state.contentLeft}px`;
183
+ this.state.contentElement.style.top = `${this.state.contentTop}px`;
184
+
185
+ // Get initial content dimensions
186
+ if ( this.state.contentElement instanceof HTMLImageElement ) {
187
+ if ( this.state.contentElement.complete ) {
188
+ this.initializeContentSize();
189
+ } else {
190
+ this.state.contentElement.addEventListener( 'load', () => {
191
+ this.initializeContentSize();
192
+ }, { once: true } );
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ private initializeContentSize () : void {
199
+ if ( !this.state.contentElement ) return;
200
+ const width = this.state.contentElement.offsetWidth;
201
+ const height = this.state.contentElement.offsetHeight;
202
+ if ( width > 0 ) {
203
+ this.state.contentElement.style.width = `${width}px`;
204
+ this.state.contentElement.style.height = 'auto';
205
+ this.initialContentWidth = width;
206
+ this.initialContentHeight = height;
207
+ }
208
+ }
209
+
210
+ private updateWrapperDimensions () : void {
211
+ if ( !this.wrapper ) return;
212
+ this.wrapper.style.width = `${this.state.width}px`;
213
+ this.wrapper.style.height = `${this.state.height}px`;
214
+ this._applyWrapperTransform();
215
+ }
216
+
217
+ private _applyWrapperTransform () : void {
218
+ if ( !this.wrapper ) return;
219
+ this.wrapper.style.transformOrigin = 'center center';
220
+ this.wrapper.style.transform = `rotate(${this.state.rotation}deg)`;
221
+ }
222
+
223
+ private updateHandlesVisibility () : void {
224
+ if ( !this.handlesContainer ) return;
225
+
226
+ const scaleHandle = this.handlesContainer.querySelector( '[data-action="scale"]' ) as HTMLElement;
227
+ const rotateHandle = this.handlesContainer.querySelector( '[data-action="rotate"]' ) as HTMLElement;
228
+ const cropHandles = this.handlesContainer.querySelectorAll( '[data-action="crop"]' );
229
+
230
+ if ( scaleHandle ) {
231
+ scaleHandle.style.display = this.allowResize ? '' : 'none';
232
+ }
233
+
234
+ if ( rotateHandle ) {
235
+ rotateHandle.style.display = this.allowRotate ? '' : 'none';
236
+ }
237
+
238
+ cropHandles.forEach( ( handle ) => {
239
+ ( handle as HTMLElement ).style.display = this.allowCrop && this._interactionMode === 'crop' ? '' : 'none';
240
+ } );
241
+ }
242
+
243
+ private bindEvents () : void {
244
+ this.handlesContainer.addEventListener( 'pointerdown', this.handlePointerDown );
245
+ this.wrapper.addEventListener( 'pointerdown', this.handleWrapperPointerDown );
246
+ this.wrapper.addEventListener( 'dblclick', this.handleWrapperDoubleClick );
247
+ document.addEventListener( 'pointerdown', this.handleDocumentPointerDown, true );
248
+
249
+ // Add listener for dragging the content/image itself
250
+ this.contentSlot.addEventListener( 'slotchange', () => {
251
+ const elements = this.contentSlot.assignedElements();
252
+ if ( elements.length > 0 ) {
253
+ const content = elements[0] as HTMLElement;
254
+ content.addEventListener( 'pointerdown', this.handleContentPointerDown );
255
+ }
256
+ } );
257
+ }
258
+
259
+ private unbindEvents () : void {
260
+ this.handlesContainer.removeEventListener( 'pointerdown', this.handlePointerDown );
261
+ this.wrapper.removeEventListener( 'pointerdown', this.handleWrapperPointerDown );
262
+ this.wrapper.removeEventListener( 'dblclick', this.handleWrapperDoubleClick );
263
+ document.removeEventListener( 'pointerdown', this.handleDocumentPointerDown, true );
264
+
265
+ const elements = this.contentSlot.assignedElements();
266
+ if ( elements.length > 0 ) {
267
+ const content = elements[0] as HTMLElement;
268
+ content.removeEventListener( 'pointerdown', this.handleContentPointerDown );
269
+ }
270
+ }
271
+
272
+ private handleWrapperDoubleClick = ( event : MouseEvent ) : void => {
273
+ if ( this.disabled ) return;
274
+ event.preventDefault();
275
+ event.stopPropagation();
276
+ this._setInteractionMode( 'crop' );
277
+ };
278
+
279
+ private handleDocumentPointerDown = ( event : PointerEvent ) : void => {
280
+ if ( this._interactionMode !== 'crop' ) return;
281
+ const path = event.composedPath();
282
+ if ( path.includes( this ) ) return;
283
+ this._setInteractionMode( 'transform' );
284
+ };
285
+
286
+ private _getContainerForMove () : HTMLElement {
287
+ return this.parentElement || ( this.offsetParent as HTMLElement ) || document.body;
288
+ }
289
+
290
+ private _ensureContainerPositionedForMove ( container : HTMLElement ) : void {
291
+ const computed = window.getComputedStyle( container );
292
+ if ( computed.position !== 'static' ) return;
293
+ container.style.position = 'relative';
294
+ }
295
+
296
+ private _syncWrapperPositionFromLayout ( container : HTMLElement ) : void {
297
+ const containerRect = container.getBoundingClientRect();
298
+ const hostRect = this.getBoundingClientRect();
299
+ const scrollLeft = container.scrollLeft;
300
+ const scrollTop = container.scrollTop;
301
+ const originLeft = containerRect.left + container.clientLeft;
302
+ const originTop = containerRect.top + container.clientTop;
303
+ this.state.wrapperLeft = hostRect.left - originLeft + scrollLeft;
304
+ this.state.wrapperTop = hostRect.top - originTop + scrollTop;
305
+ }
306
+
307
+ private _ensureAbsolutePositionForMove ( container : HTMLElement ) : void {
308
+ this._ensureContainerPositionedForMove( container );
309
+ this._syncWrapperPositionFromLayout( container );
310
+ this.style.position = 'absolute';
311
+ this.style.left = `${this.state.wrapperLeft}px`;
312
+ this.style.top = `${this.state.wrapperTop}px`;
313
+ this.style.touchAction = 'none';
314
+ }
315
+
316
+ private handleWrapperPointerDown = ( event : PointerEvent ) : void => {
317
+ if ( this.disabled ) return;
318
+ if ( !this.allowDrag ) return;
319
+ if ( this._interactionMode !== 'transform' ) return;
320
+
321
+ const target = event.target as HTMLElement;
322
+ if ( target.closest( '[data-action]' ) ) return;
323
+
324
+ // In transform mode, dragging anywhere moves the whole component
325
+ event.preventDefault();
326
+ event.stopPropagation();
327
+
328
+ const container = this._getContainerForMove();
329
+ this._dragMoveContainer = container;
330
+ this._ensureAbsolutePositionForMove( container );
331
+ this._syncWrapperPositionFromLayout( container );
332
+
333
+ const hostRect = this.getBoundingClientRect();
334
+ this._dragPointerOffsetX = event.clientX - hostRect.left;
335
+ this._dragPointerOffsetY = event.clientY - hostRect.top;
336
+
337
+ this.isDragging = true;
338
+ this.dragAction = 'move';
339
+ this.dragHandle = null;
340
+ this.dragStartX = event.clientX;
341
+ this.dragStartY = event.clientY;
342
+
343
+ document.addEventListener( 'pointermove', this.handlePointerMove );
344
+ document.addEventListener( 'pointerup', this.handlePointerUp );
345
+ this.dispatchEvent( new CustomEvent( 'rcw:move-start', { detail: { action: 'move' } } ) );
346
+ };
347
+
348
+ private handlePointerDown = ( event : PointerEvent ) : void => {
349
+ if ( this.disabled ) return;
350
+
351
+ const target = event.target as HTMLElement;
352
+ const handle = target.closest( '[data-action]' ) as HTMLElement;
353
+ if ( !handle ) return;
354
+
355
+ event.preventDefault();
356
+ event.stopPropagation();
357
+
358
+ this.dragAction = handle.dataset.action as 'scale' | 'crop' | 'rotate';
359
+ if ( this.dragAction === 'crop' && this._interactionMode !== 'crop' ) return;
360
+ if ( this.dragAction === 'rotate' && !this.allowRotate ) return;
361
+ this.isDragging = true;
362
+ this.dragHandle = handle.dataset.corner || handle.dataset.side || null;
363
+ this.dragStartX = event.clientX;
364
+ this.dragStartY = event.clientY;
365
+ this.dragStartWidth = this.state.width;
366
+ this.dragStartHeight = this.state.height;
367
+ this.dragStartContentLeft = this.state.contentLeft;
368
+ this.dragStartContentTop = this.state.contentTop;
369
+
370
+ // Get current content dimensions (ignore transforms)
371
+ if ( this.state.contentElement ) {
372
+ this.dragStartContentWidth = this.state.contentElement.offsetWidth;
373
+ this.dragStartContentHeight = this.state.contentElement.offsetHeight;
374
+ }
375
+
376
+ if ( this.dragAction === 'rotate' ) {
377
+ const wrapperRect = this.wrapper.getBoundingClientRect();
378
+ this._dragRotateCenterX = wrapperRect.left + wrapperRect.width / 2;
379
+ this._dragRotateCenterY = wrapperRect.top + wrapperRect.height / 2;
380
+ this._dragStartPointerAngle = Math.atan2( event.clientY - this._dragRotateCenterY, event.clientX - this._dragRotateCenterX );
381
+ this._dragStartRotation = this.state.rotation;
382
+ }
383
+
384
+ document.addEventListener( 'pointermove', this.handlePointerMove );
385
+ document.addEventListener( 'pointerup', this.handlePointerUp );
386
+
387
+ const eventName = this.dragAction === 'scale'
388
+ ? 'rcw:scale-start'
389
+ : this.dragAction === 'crop'
390
+ ? 'rcw:crop-start'
391
+ : 'rcw:rotate-start';
392
+ this.dispatchEvent(
393
+ new CustomEvent( eventName, {
394
+ detail: { action: this.dragAction, handle: this.dragHandle },
395
+ } ),
396
+ );
397
+ };
398
+
399
+ private handleContentPointerDown = ( event : PointerEvent ) : void => {
400
+ if ( this.disabled ) return;
401
+ if ( this._interactionMode !== 'crop' ) return;
402
+
403
+ // Only allow panning if content is larger than wrapper
404
+ if ( !this.state.contentElement ) return;
405
+
406
+ // Check if content is larger than wrapper in either dimension (ignore transforms)
407
+ const canPan = this.state.contentElement.offsetWidth > this.state.width || this.state.contentElement.offsetHeight > this.state.height;
408
+ if ( !canPan ) return;
409
+
410
+ event.preventDefault();
411
+ event.stopPropagation();
412
+
413
+ this.isDragging = true;
414
+ this.dragAction = 'pan';
415
+ this.dragHandle = null;
416
+ this.dragStartX = event.clientX;
417
+ this.dragStartY = event.clientY;
418
+ this.dragStartContentLeft = this.state.contentLeft;
419
+ this.dragStartContentTop = this.state.contentTop;
420
+
421
+ document.addEventListener( 'pointermove', this.handlePointerMove );
422
+ document.addEventListener( 'pointerup', this.handlePointerUp );
423
+
424
+ this.dispatchEvent(
425
+ new CustomEvent( 'rcw:pan-start', {
426
+ detail: { action: 'pan' },
427
+ } ),
428
+ );
429
+ };
430
+
431
+ private handlePointerMove = ( event : PointerEvent ) : void => {
432
+ if ( !this.isDragging ) return;
433
+ if ( this.dragAction === 'move' ) {
434
+ const container = this._dragMoveContainer || this._getContainerForMove();
435
+ const containerRect = container.getBoundingClientRect();
436
+ const originLeft = containerRect.left + container.clientLeft;
437
+ const originTop = containerRect.top + container.clientTop;
438
+ this.state.wrapperLeft = event.clientX - originLeft + container.scrollLeft - this._dragPointerOffsetX;
439
+ this.state.wrapperTop = event.clientY - originTop + container.scrollTop - this._dragPointerOffsetY;
440
+ this.style.left = `${this.state.wrapperLeft}px`;
441
+ this.style.top = `${this.state.wrapperTop}px`;
442
+ this.dispatchChange( 'move' );
443
+ return;
444
+ }
445
+ if ( this.dragAction === 'rotate' ) {
446
+ this.handleRotate( event.clientX, event.clientY );
447
+ this.dispatchChange( 'rotate' );
448
+ return;
449
+ }
450
+
451
+ const deltaX = event.clientX - this.dragStartX;
452
+ const deltaY = event.clientY - this.dragStartY;
453
+
454
+ if ( this.dragAction === 'scale' ) {
455
+ this.handleScale( deltaX, deltaY, this.dragHandle! );
456
+ } else if ( this.dragAction === 'crop' ) {
457
+ this.handleCrop( deltaX, deltaY, this.dragHandle! );
458
+ } else if ( this.dragAction === 'pan' ) {
459
+ this.handlePan( deltaX, deltaY );
460
+ }
461
+
462
+ this.dispatchChange( this.dragAction! );
463
+ };
464
+ private handleRotate ( pointerX : number, pointerY : number ) : void {
465
+ const pointerAngle = Math.atan2( pointerY - this._dragRotateCenterY, pointerX - this._dragRotateCenterX );
466
+ const deltaAngle = pointerAngle - this._dragStartPointerAngle;
467
+ const deltaDegrees = deltaAngle * ( 180 / Math.PI );
468
+ this.state.rotation = this._dragStartRotation + deltaDegrees;
469
+ this._applyWrapperTransform();
470
+ }
471
+
472
+ private handlePointerUp = () : void => {
473
+ if ( !this.isDragging ) return;
474
+
475
+ this.isDragging = false;
476
+ document.removeEventListener( 'pointermove', this.handlePointerMove );
477
+ document.removeEventListener( 'pointerup', this.handlePointerUp );
478
+
479
+ if ( this.dragAction ) {
480
+ this.dispatchChange( this.dragAction );
481
+ }
482
+
483
+ this.dragAction = null;
484
+ this.dragHandle = null;
485
+ this._dragMoveContainer = null;
486
+ };
487
+
488
+ private handleScale ( deltaX : number, deltaY : number, handleCorner : string ) : void {
489
+ if ( !this.state.contentElement ) return;
490
+
491
+ let newWidth = this.dragStartWidth;
492
+ let newHeight = this.dragStartHeight;
493
+
494
+ // Scale both the wrapper AND the content together
495
+ switch ( handleCorner ) {
496
+ case 'br': // Bottom-right
497
+ newWidth = this.dragStartWidth + deltaX;
498
+ newHeight = this.dragStartHeight + deltaY;
499
+ break;
500
+ case 'bl': // Bottom-left
501
+ newWidth = this.dragStartWidth - deltaX;
502
+ newHeight = this.dragStartHeight + deltaY;
503
+ break;
504
+ case 'tr': // Top-right
505
+ newWidth = this.dragStartWidth + deltaX;
506
+ newHeight = this.dragStartHeight - deltaY;
507
+ break;
508
+ case 'tl': // Top-left
509
+ newWidth = this.dragStartWidth - deltaX;
510
+ newHeight = this.dragStartHeight - deltaY;
511
+ break;
512
+ }
513
+
514
+ // Apply minimum constraints
515
+ newWidth = Math.max( this.state.minWidth, newWidth );
516
+ newHeight = Math.max( this.state.minHeight, newHeight );
517
+
518
+ // Apply aspect ratio if set
519
+ if ( this.state.aspectRatio ) {
520
+ const ratio = this.parseAspectRatio( this.state.aspectRatio );
521
+ if ( ratio ) {
522
+ const widthBasedHeight = newWidth / ratio;
523
+ const heightBasedWidth = newHeight * ratio;
524
+
525
+ if ( Math.abs( newWidth - this.dragStartWidth ) > Math.abs( newHeight - this.dragStartHeight ) ) {
526
+ newHeight = widthBasedHeight;
527
+ } else {
528
+ newWidth = heightBasedWidth;
529
+ }
530
+
531
+ newWidth = Math.max( this.state.minWidth, newWidth );
532
+ newHeight = Math.max( this.state.minHeight, newHeight );
533
+ }
534
+ }
535
+
536
+ // Calculate scale ratio to maintain proportional resizing
537
+ const scaleX = newWidth / this.dragStartWidth;
538
+ const scaleY = newHeight / this.dragStartHeight;
539
+ const scale = Math.min( scaleX, scaleY ); // Use uniform scale
540
+
541
+ // Update wrapper size
542
+ this.state.width = newWidth;
543
+ this.state.height = newHeight;
544
+ this.updateWrapperDimensions();
545
+
546
+ // Update content size proportionally
547
+ const newContentWidth = this.dragStartContentWidth * scale;
548
+ const newContentHeight = this.dragStartContentHeight * scale;
549
+
550
+ this.state.contentElement.style.width = `${newContentWidth}px`;
551
+ this.state.contentElement.style.height = `${newContentHeight}px`;
552
+
553
+ // Adjust content position proportionally to keep it centered relative to wrapper
554
+ this.state.contentLeft = this.dragStartContentLeft * scale;
555
+ this.state.contentTop = this.dragStartContentTop * scale;
556
+
557
+ // Clamp content position after scaling
558
+ this.clampContentPosition();
559
+ }
560
+
561
+ private handleCrop ( deltaX : number, deltaY : number, side : string ) : void {
562
+ // Crop resizes the wrapper (visible area), not the content
563
+ let newWidth = this.dragStartWidth;
564
+ let newHeight = this.dragStartHeight;
565
+
566
+ switch ( side ) {
567
+ case 'l': // Left - resize wrapper from left
568
+ newWidth = this.dragStartWidth - deltaX;
569
+ break;
570
+ case 'r': // Right - resize wrapper from right
571
+ newWidth = this.dragStartWidth + deltaX;
572
+ break;
573
+ case 't': // Top - resize wrapper from top
574
+ newHeight = this.dragStartHeight - deltaY;
575
+ break;
576
+ case 'b': // Bottom - resize wrapper from bottom
577
+ newHeight = this.dragStartHeight + deltaY;
578
+ break;
579
+ }
580
+
581
+ // Apply minimum constraints
582
+ newWidth = Math.max( this.state.minWidth, newWidth );
583
+ newHeight = Math.max( this.state.minHeight, newHeight );
584
+
585
+ // Apply aspect ratio if set
586
+ if ( this.state.aspectRatio ) {
587
+ const ratio = this.parseAspectRatio( this.state.aspectRatio );
588
+ if ( ratio ) {
589
+ const widthBasedHeight = newWidth / ratio;
590
+ const heightBasedWidth = newHeight * ratio;
591
+
592
+ if ( side === 'l' || side === 'r' ) {
593
+ newHeight = widthBasedHeight;
594
+ } else {
595
+ newWidth = heightBasedWidth;
596
+ }
597
+
598
+ newWidth = Math.max( this.state.minWidth, newWidth );
599
+ newHeight = Math.max( this.state.minHeight, newHeight );
600
+ }
601
+ }
602
+
603
+ this.state.width = newWidth;
604
+ this.state.height = newHeight;
605
+ this.updateWrapperDimensions();
606
+
607
+ // When cropping from left or top, adjust content position
608
+ if ( side === 'l' ) {
609
+ const widthDiff = newWidth - this.dragStartWidth;
610
+ this.state.contentLeft = this.dragStartContentLeft - widthDiff;
611
+ } else if ( side === 't' ) {
612
+ const heightDiff = newHeight - this.dragStartHeight;
613
+ this.state.contentTop = this.dragStartContentTop - heightDiff;
614
+ }
615
+
616
+ // Clamp content position
617
+ this.clampContentPosition();
618
+ }
619
+
620
+ private clampContentPosition () : void {
621
+ if ( !this.state.contentElement ) return;
622
+
623
+ const contentWidth = this.state.contentElement.offsetWidth;
624
+ const contentHeight = this.state.contentElement.offsetHeight;
625
+ const minLeft = Math.min( 0, this.state.width - contentWidth );
626
+ const minTop = Math.min( 0, this.state.height - contentHeight );
627
+
628
+ this.state.contentLeft = Math.max( minLeft, Math.min( 0, this.state.contentLeft ) );
629
+ this.state.contentTop = Math.max( minTop, Math.min( 0, this.state.contentTop ) );
630
+
631
+ this.state.contentElement.style.left = `${this.state.contentLeft}px`;
632
+ this.state.contentElement.style.top = `${this.state.contentTop}px`;
633
+ }
634
+
635
+ private handlePan ( deltaX : number, deltaY : number ) : void {
636
+ if ( !this.state.contentElement ) return;
637
+
638
+ // Pan the content by moving its position
639
+ this.state.contentLeft = this.dragStartContentLeft + deltaX;
640
+ this.state.contentTop = this.dragStartContentTop + deltaY;
641
+
642
+ // Clamp to prevent blank areas
643
+ this.clampContentPosition();
644
+ }
645
+
646
+ private parseAspectRatio ( ratio : string ) : number | null {
647
+ const parts = ratio.split( '/' );
648
+ if ( parts.length === 2 ) {
649
+ const width = parseFloat( parts[0] );
650
+ const height = parseFloat( parts[1] );
651
+ if ( !isNaN( width ) && !isNaN( height ) && height !== 0 ) {
652
+ return width / height;
653
+ }
654
+ }
655
+ return null;
656
+ }
657
+
658
+ private dispatchChange ( action : 'scale' | 'crop' | 'pan' | 'rotate' | 'move' ) : void {
659
+ const detail : ResizableCropEventDetail = {
660
+ width: this.state.width,
661
+ height: this.state.height,
662
+ wrapperLeft: this.state.wrapperLeft,
663
+ wrapperTop: this.state.wrapperTop,
664
+ contentLeft: this.state.contentLeft,
665
+ contentTop: this.state.contentTop,
666
+ action,
667
+ rotation: this.state.rotation,
668
+ handle: this.dragHandle || undefined,
669
+ };
670
+
671
+ this.dispatchEvent( new CustomEvent( 'rcw:change', { detail } ) );
672
+
673
+ // Dispatch the onChange event with full values
674
+ this.dispatchOnChange();
675
+ }
676
+
677
+ private dispatchOnChange () : void {
678
+ const values = this.getValues();
679
+ this.dispatchEvent(
680
+ new CustomEvent( 'change', {
681
+ detail: values,
682
+ bubbles: true,
683
+ composed: true,
684
+ } ),
685
+ );
686
+ }
687
+
688
+ private render () : void {
689
+ this.shadowRoot.innerHTML = `
690
+ <style>
691
+ :host {
692
+ display: inline-block;
693
+ position: relative;
694
+ }
695
+
696
+ #wrapper {
697
+ position: relative;
698
+ border: 2px solid #007bff;
699
+ box-sizing: border-box;
700
+ background: rgba(0, 123, 255, 0.05);
701
+ }
702
+
703
+ #clipper {
704
+ position: absolute;
705
+ top: 0;
706
+ left: 0;
707
+ right: 0;
708
+ bottom: 0;
709
+ overflow: hidden;
710
+ }
711
+
712
+ ::slotted(*) {
713
+ position: absolute;
714
+ max-width: none;
715
+ max-height: none;
716
+ cursor: grab;
717
+ }
718
+
719
+ ::slotted(*:active) {
720
+ cursor: grabbing;
721
+ }
722
+
723
+ #handles-container {
724
+ position: absolute;
725
+ top: 0;
726
+ left: 0;
727
+ right: 0;
728
+ bottom: 0;
729
+ pointer-events: none;
730
+ }
731
+
732
+ .handle {
733
+ position: absolute;
734
+ background: white;
735
+ border: 2px solid #007bff;
736
+ pointer-events: auto;
737
+ touch-action: none;
738
+ z-index: 1000;
739
+ }
740
+
741
+ .handle.rotate {
742
+ width: 10px;
743
+ height: 10px;
744
+ border-radius: 50%;
745
+ top: -18px;
746
+ left: 50%;
747
+ transform: translateX(-50%);
748
+ cursor: grab;
749
+ }
750
+
751
+ .handle.rotate:active {
752
+ cursor: grabbing;
753
+ }
754
+
755
+ .handle.scale {
756
+ width: 12px;
757
+ height: 12px;
758
+ border-radius: 50%;
759
+ cursor: nwse-resize;
760
+ }
761
+
762
+ .handle.scale.tl {
763
+ top: -6px;
764
+ left: -6px;
765
+ cursor: nwse-resize;
766
+ }
767
+
768
+ .handle.scale.tr {
769
+ top: -6px;
770
+ right: -6px;
771
+ cursor: nesw-resize;
772
+ }
773
+
774
+ .handle.scale.bl {
775
+ bottom: -6px;
776
+ left: -6px;
777
+ cursor: nesw-resize;
778
+ }
779
+
780
+ .handle.scale.br {
781
+ bottom: -6px;
782
+ right: -6px;
783
+ cursor: nwse-resize;
784
+ }
785
+
786
+ .handle.crop {
787
+ background: #007bff;
788
+ cursor: move;
789
+ }
790
+
791
+ .handle.crop.t {
792
+ top: -2px;
793
+ left: 50%;
794
+ transform: translateX(-50%);
795
+ width: 40px;
796
+ height: 4px;
797
+ cursor: ns-resize;
798
+ }
799
+
800
+ .handle.crop.b {
801
+ bottom: -2px;
802
+ left: 50%;
803
+ transform: translateX(-50%);
804
+ width: 40px;
805
+ height: 4px;
806
+ cursor: ns-resize;
807
+ }
808
+
809
+ .handle.crop.l {
810
+ left: -2px;
811
+ top: 50%;
812
+ transform: translateY(-50%);
813
+ width: 4px;
814
+ height: 40px;
815
+ cursor: ew-resize;
816
+ }
817
+
818
+ .handle.crop.r {
819
+ right: -2px;
820
+ top: 50%;
821
+ transform: translateY(-50%);
822
+ width: 4px;
823
+ height: 40px;
824
+ cursor: ew-resize;
825
+ }
826
+
827
+ :host([disabled]) #wrapper {
828
+ opacity: 0.6;
829
+ cursor: not-allowed;
830
+ }
831
+
832
+ :host([disabled]) .handle {
833
+ display: none;
834
+ }
835
+ </style>
836
+
837
+ <div id="wrapper">
838
+ <div id="clipper">
839
+ <slot></slot>
840
+ </div>
841
+
842
+ <div id="handles-container">
843
+ <!-- Rotate handle (top-center) -->
844
+ <div class="handle rotate" data-action="rotate"></div>
845
+
846
+ <!-- Scale handle (only bottom-right corner) -->
847
+ <div class="handle scale br" data-action="scale" data-corner="br"></div>
848
+
849
+ <!-- Crop handles (only right and bottom) -->
850
+ <div class="handle crop b" data-action="crop" data-side="b"></div>
851
+ <div class="handle crop r" data-action="crop" data-side="r"></div>
852
+ </div>
853
+ </div>
854
+ `;
855
+ }
856
+
857
+ // Public API - Getters and Setters
858
+ get width () : number {
859
+ return this.state.width;
860
+ }
861
+
862
+ set width ( value : number ) {
863
+ this.setAttribute( 'width', String( value ) );
864
+ }
865
+
866
+ get height () : number {
867
+ return this.state.height;
868
+ }
869
+
870
+ set height ( value : number ) {
871
+ this.setAttribute( 'height', String( value ) );
872
+ }
873
+
874
+ get minWidth () : number {
875
+ return this.state.minWidth;
876
+ }
877
+
878
+ set minWidth ( value : number ) {
879
+ this.setAttribute( 'min-width', String( value ) );
880
+ }
881
+
882
+ get minHeight () : number {
883
+ return this.state.minHeight;
884
+ }
885
+
886
+ set minHeight ( value : number ) {
887
+ this.setAttribute( 'min-height', String( value ) );
888
+ }
889
+
890
+ get aspectRatio () : string | null {
891
+ return this.state.aspectRatio;
892
+ }
893
+
894
+ set aspectRatio ( value : string | null ) {
895
+ if ( value ) {
896
+ this.setAttribute( 'aspect-ratio', value );
897
+ } else {
898
+ this.removeAttribute( 'aspect-ratio' );
899
+ }
900
+ }
901
+
902
+ get disabled () : boolean {
903
+ return this.hasAttribute( 'disabled' );
904
+ }
905
+
906
+ set disabled ( value : boolean ) {
907
+ if ( value ) {
908
+ this.setAttribute( 'disabled', '' );
909
+ } else {
910
+ this.removeAttribute( 'disabled' );
911
+ }
912
+ }
913
+
914
+ get allowCrop () : boolean {
915
+ return this.hasAttribute( 'allow-crop' ) ? this.getAttribute( 'allow-crop' ) !== 'false' : true;
916
+ }
917
+
918
+ set allowCrop ( value : boolean ) {
919
+ if ( value ) {
920
+ this.setAttribute( 'allow-crop', 'true' );
921
+ } else {
922
+ this.setAttribute( 'allow-crop', 'false' );
923
+ }
924
+ }
925
+
926
+ get allowResize () : boolean {
927
+ return this.hasAttribute( 'allow-resize' ) ? this.getAttribute( 'allow-resize' ) !== 'false' : true;
928
+ }
929
+
930
+ set allowResize ( value : boolean ) {
931
+ if ( value ) {
932
+ this.setAttribute( 'allow-resize', 'true' );
933
+ } else {
934
+ this.setAttribute( 'allow-resize', 'false' );
935
+ }
936
+ }
937
+
938
+ get allowRotate () : boolean {
939
+ return this.hasAttribute( 'allow-rotate' ) ? this.getAttribute( 'allow-rotate' ) !== 'false' : true;
940
+ }
941
+
942
+ set allowRotate ( value : boolean ) {
943
+ if ( value ) {
944
+ this.setAttribute( 'allow-rotate', 'true' );
945
+ } else {
946
+ this.setAttribute( 'allow-rotate', 'false' );
947
+ }
948
+ }
949
+
950
+ get allowDrag () : boolean {
951
+ return this.hasAttribute( 'allow-drag' ) ? this.getAttribute( 'allow-drag' ) !== 'false' : true;
952
+ }
953
+
954
+ set allowDrag ( value : boolean ) {
955
+ if ( value ) {
956
+ this.setAttribute( 'allow-drag', 'true' );
957
+ } else {
958
+ this.setAttribute( 'allow-drag', 'false' );
959
+ }
960
+ }
961
+
962
+ /**
963
+ * Gets the current values including wrapper size, content size, position, and zoom level
964
+ */
965
+ public getValues () : ResizableCropperValues {
966
+ // Keep wrapperLeft/wrapperTop in sync even when not moved yet.
967
+ this._syncWrapperPositionFromLayout( this._getContainerForMove() );
968
+
969
+ const contentWidth = this.state.contentElement?.offsetWidth || 0;
970
+ const contentHeight = this.state.contentElement?.offsetHeight || 0;
971
+
972
+ // Calculate zoom based on current content size vs initial size
973
+ const zoom = this.initialContentWidth > 0 ? contentWidth / this.initialContentWidth : 1;
974
+
975
+ return {
976
+ wrapperWidth: this.state.width,
977
+ wrapperHeight: this.state.height,
978
+ wrapperLeft: this.state.wrapperLeft,
979
+ wrapperTop: this.state.wrapperTop,
980
+ contentWidth,
981
+ contentHeight,
982
+ contentLeft: this.state.contentLeft,
983
+ contentTop: this.state.contentTop,
984
+ zoom,
985
+ rotation: this.state.rotation,
986
+ };
987
+ }
988
+
989
+ /**
990
+ * Sets the values to reproduce size, zoom and pan
991
+ * @param values - The values to set
992
+ */
993
+ public setValues ( values : Partial<ResizableCropperValues> ) : void {
994
+ // Set wrapper size
995
+ if ( values.wrapperWidth !== undefined ) {
996
+ this.state.width = values.wrapperWidth;
997
+ }
998
+ if ( values.wrapperHeight !== undefined ) {
999
+ this.state.height = values.wrapperHeight;
1000
+ }
1001
+ this.updateWrapperDimensions();
1002
+
1003
+ if ( values.wrapperLeft !== undefined || values.wrapperTop !== undefined ) {
1004
+ const container = this._getContainerForMove();
1005
+ this._ensureAbsolutePositionForMove( container );
1006
+ if ( values.wrapperLeft !== undefined ) this.state.wrapperLeft = values.wrapperLeft;
1007
+ if ( values.wrapperTop !== undefined ) this.state.wrapperTop = values.wrapperTop;
1008
+ this.style.left = `${this.state.wrapperLeft}px`;
1009
+ this.style.top = `${this.state.wrapperTop}px`;
1010
+ }
1011
+
1012
+ if ( values.rotation !== undefined ) {
1013
+ this.state.rotation = values.rotation;
1014
+ this._applyWrapperTransform();
1015
+ }
1016
+
1017
+ if ( !this.state.contentElement ) {
1018
+ this.dispatchOnChange();
1019
+ return;
1020
+ }
1021
+
1022
+ // Set content size based on zoom or explicit dimensions
1023
+ if ( values.zoom !== undefined && this.initialContentWidth > 0 ) {
1024
+ const newContentWidth = this.initialContentWidth * values.zoom;
1025
+ const newContentHeight = this.initialContentHeight * values.zoom;
1026
+ this.state.contentElement.style.width = `${newContentWidth}px`;
1027
+ this.state.contentElement.style.height = `${newContentHeight}px`;
1028
+ } else if ( values.contentWidth !== undefined ) {
1029
+ this.state.contentElement.style.width = `${values.contentWidth}px`;
1030
+ if ( values.contentHeight !== undefined ) {
1031
+ this.state.contentElement.style.height = `${values.contentHeight}px`;
1032
+ } else {
1033
+ this.state.contentElement.style.height = 'auto';
1034
+ }
1035
+ }
1036
+
1037
+ // Set content position
1038
+ if ( values.contentLeft !== undefined ) {
1039
+ this.state.contentLeft = values.contentLeft;
1040
+ }
1041
+ if ( values.contentTop !== undefined ) {
1042
+ this.state.contentTop = values.contentTop;
1043
+ }
1044
+
1045
+ // Apply position and clamp
1046
+ this.clampContentPosition();
1047
+
1048
+ // Dispatch change event
1049
+ this.dispatchOnChange();
1050
+ }
1051
+
1052
+ /**
1053
+ * Gets a serializable state snapshot of the component including flags, constraints, mode,
1054
+ * and the current transform/crop values.
1055
+ */
1056
+ public getState () : ResizableCropperComponentState {
1057
+ return {
1058
+ mode: this._interactionMode,
1059
+ disabled: this.disabled,
1060
+ allowCrop: this.allowCrop,
1061
+ allowResize: this.allowResize,
1062
+ allowRotate: this.allowRotate,
1063
+ allowDrag: this.allowDrag,
1064
+ minWidth: this.minWidth,
1065
+ minHeight: this.minHeight,
1066
+ aspectRatio: this.aspectRatio,
1067
+ values: this.getValues(),
1068
+ };
1069
+ }
1070
+
1071
+ /**
1072
+ * Restores a state snapshot produced by getState().
1073
+ * @param state - State to restore
1074
+ */
1075
+ public setState ( state : Partial<ResizableCropperComponentState> ) : void {
1076
+ if ( state.disabled !== undefined ) this.disabled = state.disabled;
1077
+ if ( state.allowCrop !== undefined ) this.allowCrop = state.allowCrop;
1078
+ if ( state.allowResize !== undefined ) this.allowResize = state.allowResize;
1079
+ if ( state.allowRotate !== undefined ) this.allowRotate = state.allowRotate;
1080
+ if ( state.allowDrag !== undefined ) this.allowDrag = state.allowDrag;
1081
+ if ( state.minWidth !== undefined ) this.minWidth = state.minWidth;
1082
+ if ( state.minHeight !== undefined ) this.minHeight = state.minHeight;
1083
+ if ( state.aspectRatio !== undefined ) this.aspectRatio = state.aspectRatio;
1084
+ if ( state.mode !== undefined ) this._setInteractionMode( state.mode );
1085
+ if ( state.values ) this.setValues( state.values );
1086
+ }
1087
+ }
1088
+
1089
+ /**
1090
+ * Conditionally defines the custom element if in a browser environment.
1091
+ */
1092
+ export const defineResizableCropper = ( tagName : string = 'liwe3-resizable-cropper' ) : void => {
1093
+ if ( typeof window !== 'undefined' && !window.customElements.get( tagName ) ) {
1094
+ customElements.define( tagName, ResizableCropperElement );
1095
+ }
1096
+ };
1097
+
1098
+ // Auto-register with default tag name
1099
+ defineResizableCropper();