@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.
- package/dist/AIMarkdownEditor.d.ts +35 -0
- package/dist/AIMarkdownEditor.d.ts.map +1 -0
- package/dist/AIMarkdownEditor.js +412 -0
- package/dist/AIMarkdownEditor.js.map +1 -0
- package/dist/AITextEditor.d.ts +10 -0
- package/dist/AITextEditor.d.ts.map +1 -1
- package/dist/AITextEditor.js +63 -27
- package/dist/AITextEditor.js.map +1 -1
- package/dist/ButtonToolbar.d.ts +35 -0
- package/dist/ButtonToolbar.d.ts.map +1 -0
- package/dist/ButtonToolbar.js +220 -0
- package/dist/ButtonToolbar.js.map +1 -0
- package/dist/CheckList.d.ts +31 -0
- package/dist/CheckList.d.ts.map +1 -0
- package/dist/CheckList.js +336 -0
- package/dist/CheckList.js.map +1 -0
- package/dist/ChunkUploader.d.ts +22 -0
- package/dist/ChunkUploader.d.ts.map +1 -1
- package/dist/ChunkUploader.js +245 -103
- package/dist/ChunkUploader.js.map +1 -1
- package/dist/ComicBalloon.d.ts +82 -0
- package/dist/ComicBalloon.d.ts.map +1 -0
- package/dist/ComicBalloon.js +346 -0
- package/dist/ComicBalloon.js.map +1 -0
- package/dist/Dialog.d.ts +102 -0
- package/dist/Dialog.d.ts.map +1 -0
- package/dist/Dialog.js +299 -0
- package/dist/Dialog.js.map +1 -0
- package/dist/MarkdownPreview.d.ts +25 -0
- package/dist/MarkdownPreview.d.ts.map +1 -0
- package/dist/MarkdownPreview.js +147 -0
- package/dist/MarkdownPreview.js.map +1 -0
- package/dist/ResizableCropper.d.ts +158 -0
- package/dist/ResizableCropper.d.ts.map +1 -0
- package/dist/ResizableCropper.js +562 -0
- package/dist/ResizableCropper.js.map +1 -0
- package/dist/SmartSelect.d.ts +1 -0
- package/dist/SmartSelect.d.ts.map +1 -1
- package/dist/SmartSelect.js +45 -2
- package/dist/SmartSelect.js.map +1 -1
- package/dist/index.d.ts +16 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +52 -29
- package/dist/index.js.map +1 -1
- package/package.json +33 -3
- package/src/AIMarkdownEditor.ts +568 -0
- package/src/AITextEditor.ts +97 -2
- package/src/ButtonToolbar.ts +302 -0
- package/src/CheckList.ts +438 -0
- package/src/ChunkUploader.ts +837 -623
- package/src/ComicBalloon.ts +709 -0
- package/src/Dialog.ts +510 -0
- package/src/MarkdownPreview.ts +213 -0
- package/src/ResizableCropper.ts +1099 -0
- package/src/SmartSelect.ts +48 -2
- package/src/index.ts +110 -47
|
@@ -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();
|