@radix-ng/primitives 0.35.0 → 0.37.0

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 (50) hide show
  1. package/avatar/index.d.ts +1 -1
  2. package/avatar/src/avatar-context.token.d.ts +7 -0
  3. package/avatar/src/avatar-fallback.directive.d.ts +3 -9
  4. package/avatar/src/avatar-image.directive.d.ts +11 -11
  5. package/avatar/src/avatar-root.directive.d.ts +3 -6
  6. package/avatar/src/types.d.ts +2 -0
  7. package/checkbox/index.d.ts +1 -1
  8. package/checkbox/src/checkbox-button.directive.d.ts +1 -1
  9. package/checkbox/src/checkbox-indicator.directive.d.ts +1 -1
  10. package/checkbox/src/checkbox-input.directive.d.ts +1 -1
  11. package/checkbox/src/checkbox.directive.d.ts +3 -3
  12. package/checkbox/src/checkbox.token.d.ts +3 -3
  13. package/core/index.d.ts +2 -0
  14. package/core/src/clamp.d.ts +38 -0
  15. package/cropper/README.md +1 -0
  16. package/cropper/index.d.ts +15 -0
  17. package/cropper/src/cropper-context.token.d.ts +12 -0
  18. package/cropper/src/cropper-crop-area.directive.d.ts +6 -0
  19. package/cropper/src/cropper-description.directive.d.ts +6 -0
  20. package/cropper/src/cropper-image.component.d.ts +10 -0
  21. package/cropper/src/cropper-root.directive.d.ts +91 -0
  22. package/date-field/index.d.ts +9 -0
  23. package/fesm2022/radix-ng-primitives-avatar.mjs +82 -86
  24. package/fesm2022/radix-ng-primitives-avatar.mjs.map +1 -1
  25. package/fesm2022/radix-ng-primitives-checkbox.mjs +13 -16
  26. package/fesm2022/radix-ng-primitives-checkbox.mjs.map +1 -1
  27. package/fesm2022/radix-ng-primitives-core.mjs +84 -1
  28. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  29. package/fesm2022/radix-ng-primitives-cropper.mjs +680 -0
  30. package/fesm2022/radix-ng-primitives-cropper.mjs.map +1 -0
  31. package/fesm2022/radix-ng-primitives-date-field.mjs +16 -2
  32. package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
  33. package/fesm2022/radix-ng-primitives-number-field.mjs +502 -0
  34. package/fesm2022/radix-ng-primitives-number-field.mjs.map +1 -0
  35. package/fesm2022/radix-ng-primitives-stepper.mjs +1 -14
  36. package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
  37. package/hover-card/src/hover-card-root.directive.d.ts +4 -4
  38. package/number-field/README.md +1 -0
  39. package/number-field/index.d.ts +17 -0
  40. package/number-field/src/number-field-context.token.d.ts +24 -0
  41. package/number-field/src/number-field-decrement.directive.d.ts +23 -0
  42. package/number-field/src/number-field-increment.directive.d.ts +23 -0
  43. package/number-field/src/number-field-input.directive.d.ts +22 -0
  44. package/number-field/src/number-field-root.directive.d.ts +86 -0
  45. package/number-field/src/types.d.ts +1 -0
  46. package/number-field/src/utils.d.ts +18 -0
  47. package/package.json +9 -1
  48. package/popover/src/popover-root.directive.d.ts +4 -4
  49. package/tooltip/src/tooltip-root.directive.d.ts +4 -4
  50. /package/{stepper/src/utils → core/src}/getActiveElement.d.ts +0 -0
@@ -0,0 +1,680 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, Directive, input, computed, Component, ElementRef, numberAttribute, output, signal, afterNextRender, effect, NgModule } from '@angular/core';
3
+ import { clamp, provideToken } from '@radix-ng/primitives/core';
4
+
5
+ const CROPPER_ROOT_CONTEXT = new InjectionToken('CROPPER_ROOT_CONTEXT');
6
+ function injectCropperRootContext() {
7
+ return inject(CROPPER_ROOT_CONTEXT);
8
+ }
9
+
10
+ class RdxCropperCropAreaDirective {
11
+ constructor() {
12
+ this.rootContext = injectCropperRootContext();
13
+ }
14
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxCropperCropAreaDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
15
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.4", type: RdxCropperCropAreaDirective, isStandalone: true, selector: "[rdxCropperCropArea]", host: { properties: { "style": "rootContext.getCropAreaStyle()" } }, ngImport: i0 }); }
16
+ }
17
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxCropperCropAreaDirective, decorators: [{
18
+ type: Directive,
19
+ args: [{
20
+ selector: '[rdxCropperCropArea]',
21
+ host: {
22
+ '[style]': 'rootContext.getCropAreaStyle()'
23
+ }
24
+ }]
25
+ }] });
26
+
27
+ class RdxCropperDescriptionDirective {
28
+ constructor() {
29
+ this.rootContext = injectCropperRootContext();
30
+ }
31
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxCropperDescriptionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
32
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.4", type: RdxCropperDescriptionDirective, isStandalone: true, selector: "[rdxCropperDescription]", host: { properties: { "attr.id": "rootContext.descriptionId()" } }, ngImport: i0 }); }
33
+ }
34
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxCropperDescriptionDirective, decorators: [{
35
+ type: Directive,
36
+ args: [{
37
+ selector: '[rdxCropperDescription]',
38
+ host: {
39
+ '[attr.id]': 'rootContext.descriptionId()'
40
+ }
41
+ }]
42
+ }] });
43
+
44
+ class RdxCropperImageComponent {
45
+ constructor() {
46
+ this.rootContext = injectCropperRootContext();
47
+ this.imgClass = input();
48
+ this.imgStyles = input();
49
+ this.imgClasses = computed(() => this.imgClass());
50
+ this.imgStyless = computed(() => this.imgStyles());
51
+ }
52
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxCropperImageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
53
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.4", type: RdxCropperImageComponent, isStandalone: true, selector: "[rdxCropperImage]", inputs: { imgClass: { classPropertyName: "imgClass", publicName: "imgClass", isSignal: true, isRequired: false, transformFunction: null }, imgStyles: { classPropertyName: "imgStyles", publicName: "imgStyles", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style": "rootContext.getImageWrapperStyle()" } }, ngImport: i0, template: `
54
+ <img
55
+ [class]="imgClasses()"
56
+ [src]="rootContext.getImageProps()['src']"
57
+ [alt]="rootContext.getImageProps()['alt']"
58
+ [draggable]="rootContext.getImageProps()['draggable']"
59
+ [style]="imgStyless()"
60
+ aria-hidden="true"
61
+ />
62
+ `, isInline: true }); }
63
+ }
64
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxCropperImageComponent, decorators: [{
65
+ type: Component,
66
+ args: [{
67
+ selector: '[rdxCropperImage]',
68
+ host: {
69
+ '[style]': 'rootContext.getImageWrapperStyle()'
70
+ },
71
+ template: `
72
+ <img
73
+ [class]="imgClasses()"
74
+ [src]="rootContext.getImageProps()['src']"
75
+ [alt]="rootContext.getImageProps()['alt']"
76
+ [draggable]="rootContext.getImageProps()['draggable']"
77
+ [style]="imgStyless()"
78
+ aria-hidden="true"
79
+ />
80
+ `
81
+ }]
82
+ }] });
83
+
84
+ class RdxCropperRootDirective {
85
+ constructor() {
86
+ this.elementRef = inject((ElementRef));
87
+ this.CROPPER_DESC_WARN_MESSAGE = `Warning: \`Cropper\` requires a description element for accessibility.`;
88
+ this.image = input.required();
89
+ this.cropPadding = input(25, { transform: numberAttribute });
90
+ this.aspectRatio = input(1, { transform: numberAttribute });
91
+ this.minZoom = input(1, { transform: numberAttribute });
92
+ this.maxZoom = input(3, { transform: numberAttribute });
93
+ this.zoomSensitivity = input(0.005, { transform: numberAttribute });
94
+ this.keyboardStep = input(10, { transform: numberAttribute });
95
+ this.zoom = input(undefined, { transform: numberAttribute });
96
+ this.onCropChange = output();
97
+ this.onZoomChange = output();
98
+ // State signals
99
+ this.imgWidth = signal(null);
100
+ this.imgHeight = signal(null);
101
+ this.cropAreaWidth = signal(0);
102
+ this.cropAreaHeight = signal(0);
103
+ this.imageWrapperWidth = signal(0);
104
+ this.imageWrapperHeight = signal(0);
105
+ this.offsetX = signal(0);
106
+ this.offsetY = signal(0);
107
+ this.internalZoom = signal(this.minZoom());
108
+ this.isDragging = signal(false);
109
+ this.descriptionId = signal(`cropper-${Math.random().toString(36).substring(2, 9)}`);
110
+ this.isZoomControlled = computed(() => this.zoom() !== undefined);
111
+ this.effectiveZoom = computed(() => (this.isZoomControlled() ? this.zoom() : this.internalZoom()));
112
+ this.zoomValueText = computed(() => {
113
+ const zoomPercent = this.effectiveZoom() * 100;
114
+ return `Zoom: ${zoomPercent.toFixed(0)}%`;
115
+ });
116
+ // Refs
117
+ this.dragStartPoint = signal({ x: 0, y: 0 });
118
+ this.dragStartOffset = signal({ x: 0, y: 0 });
119
+ this.latestRestrictedOffset = signal({ x: 0, y: 0 });
120
+ this.latestZoom = signal(this.minZoom());
121
+ this.isInitialSetupDone = signal(false);
122
+ this.initialPinchDistance = signal(0);
123
+ this.initialPinchZoom = signal(1);
124
+ this.isPinching = signal(false);
125
+ this.hasWarned = signal(false);
126
+ afterNextRender(() => {
127
+ this.initializeContainerDimensions();
128
+ });
129
+ this.setupImageLoadEffect();
130
+ this.setupDimensionsEffects();
131
+ this.setupCropCalculationEffect();
132
+ this.setupAccessibilityWarningEffect();
133
+ this.setupEventListenersEffect();
134
+ effect(() => {
135
+ this.latestZoom.set(this.effectiveZoom());
136
+ });
137
+ }
138
+ updateZoom(newZoomValue) {
139
+ const clampedZoom = clamp(newZoomValue, this.minZoom(), this.maxZoom());
140
+ this.onZoomChange.emit(clampedZoom);
141
+ if (!this.isZoomControlled()) {
142
+ this.internalZoom.set(clampedZoom);
143
+ }
144
+ return clampedZoom;
145
+ }
146
+ initializeContainerDimensions() {
147
+ const element = this.elementRef.nativeElement;
148
+ if (element && element.clientWidth > 0 && element.clientHeight > 0) {
149
+ this.cropAreaWidth.set(Math.max(0, element.clientWidth - this.cropPadding() * 2));
150
+ this.cropAreaHeight.set(Math.max(0, element.clientHeight - this.cropPadding() * 2));
151
+ }
152
+ }
153
+ setupImageLoadEffect() {
154
+ effect(() => {
155
+ const image = this.image();
156
+ this.offsetX.set(0);
157
+ this.offsetY.set(0);
158
+ if (!this.isZoomControlled()) {
159
+ this.internalZoom.set(this.minZoom());
160
+ }
161
+ this.isInitialSetupDone.set(false);
162
+ if (!image) {
163
+ this.imgWidth.set(null);
164
+ this.imgHeight.set(null);
165
+ return;
166
+ }
167
+ let isMounted = true;
168
+ const img = new Image();
169
+ img.onload = () => {
170
+ if (isMounted) {
171
+ this.imgWidth.set(img.naturalWidth);
172
+ this.imgHeight.set(img.naturalHeight);
173
+ }
174
+ };
175
+ img.onerror = () => {
176
+ if (isMounted) {
177
+ this.imgWidth.set(null);
178
+ this.imgHeight.set(null);
179
+ }
180
+ };
181
+ img.src = image;
182
+ return () => {
183
+ isMounted = false;
184
+ };
185
+ });
186
+ }
187
+ setupDimensionsEffects() {
188
+ effect(() => {
189
+ const element = this.elementRef.nativeElement;
190
+ if (!element)
191
+ return;
192
+ const updateDimensions = (width, height) => {
193
+ if (width <= 0 || height <= 0) {
194
+ this.cropAreaWidth.set(0);
195
+ this.cropAreaHeight.set(0);
196
+ return;
197
+ }
198
+ const maxPossibleWidth = Math.max(0, width - this.cropPadding() * 2);
199
+ const maxPossibleHeight = Math.max(0, height - this.cropPadding() * 2);
200
+ let targetCropW, targetCropH;
201
+ if (maxPossibleWidth / this.aspectRatio() >= maxPossibleHeight) {
202
+ targetCropH = maxPossibleHeight;
203
+ targetCropW = targetCropH * this.aspectRatio();
204
+ }
205
+ else {
206
+ targetCropW = maxPossibleWidth;
207
+ targetCropH = targetCropW / this.aspectRatio();
208
+ }
209
+ this.cropAreaWidth.set(targetCropW);
210
+ this.cropAreaHeight.set(targetCropH);
211
+ };
212
+ const observer = new ResizeObserver((entries) => {
213
+ for (const entry of entries) {
214
+ const { width, height } = entry.contentRect;
215
+ if (width > 0 && height > 0)
216
+ updateDimensions(width, height);
217
+ }
218
+ });
219
+ observer.observe(element);
220
+ const initialWidth = element.clientWidth;
221
+ const initialHeight = element.clientHeight;
222
+ if (initialWidth > 0 && initialHeight > 0) {
223
+ updateDimensions(initialWidth, initialHeight);
224
+ }
225
+ return () => observer.disconnect();
226
+ });
227
+ // Update image wrapper dimensions when crop area or image dimensions change
228
+ effect(() => {
229
+ const cropW = this.cropAreaWidth();
230
+ const cropH = this.cropAreaHeight();
231
+ const imgW = this.imgWidth();
232
+ const imgH = this.imgHeight();
233
+ if (cropW <= 0 || cropH <= 0 || !imgW || !imgH) {
234
+ this.imageWrapperWidth.set(0);
235
+ this.imageWrapperHeight.set(0);
236
+ return;
237
+ }
238
+ const naturalAspect = imgW / imgH;
239
+ const cropAspect = cropW / cropH;
240
+ let targetWrapperWidth, targetWrapperHeight;
241
+ if (naturalAspect >= cropAspect) {
242
+ targetWrapperHeight = cropH;
243
+ targetWrapperWidth = targetWrapperHeight * naturalAspect;
244
+ }
245
+ else {
246
+ targetWrapperWidth = cropW;
247
+ targetWrapperHeight = targetWrapperWidth / naturalAspect;
248
+ }
249
+ this.imageWrapperWidth.set(targetWrapperWidth);
250
+ this.imageWrapperHeight.set(targetWrapperHeight);
251
+ });
252
+ }
253
+ restrictOffset(dragOffsetX, dragOffsetY, currentZoom) {
254
+ const wrapperW = this.imageWrapperWidth();
255
+ const wrapperH = this.imageWrapperHeight();
256
+ const cropW = this.cropAreaWidth();
257
+ const cropH = this.cropAreaHeight();
258
+ if (wrapperW <= 0 || wrapperH <= 0 || cropW <= 0 || cropH <= 0) {
259
+ return { x: 0, y: 0 };
260
+ }
261
+ const effectiveWrapperWidth = wrapperW * currentZoom;
262
+ const effectiveWrapperHeight = wrapperH * currentZoom;
263
+ const maxDragX = Math.max(0, (effectiveWrapperWidth - cropW) / 2);
264
+ const maxDragY = Math.max(0, (effectiveWrapperHeight - cropH) / 2);
265
+ return {
266
+ x: clamp(dragOffsetX, -maxDragX, maxDragX),
267
+ y: clamp(dragOffsetY, -maxDragY, maxDragY)
268
+ };
269
+ }
270
+ calculateCropData(finalOffsetX, finalOffsetY, finalZoom) {
271
+ const currentOffsetX = finalOffsetX ?? this.latestRestrictedOffset().x;
272
+ const currentOffsetY = finalOffsetY ?? this.latestRestrictedOffset().y;
273
+ const currentZoom = finalZoom ?? this.effectiveZoom();
274
+ const imgW = this.imgWidth();
275
+ const imgH = this.imgHeight();
276
+ const wrapperW = this.imageWrapperWidth();
277
+ const wrapperH = this.imageWrapperHeight();
278
+ const cropW = this.cropAreaWidth();
279
+ const cropH = this.cropAreaHeight();
280
+ if (!imgW || !imgH || wrapperW <= 0 || wrapperH <= 0 || cropW <= 0 || cropH <= 0) {
281
+ return null;
282
+ }
283
+ const scaledWrapperWidth = wrapperW * currentZoom;
284
+ const scaledWrapperHeight = wrapperH * currentZoom;
285
+ const topLeftOffsetX = currentOffsetX + (cropW - scaledWrapperWidth) / 2;
286
+ const topLeftOffsetY = currentOffsetY + (cropH - scaledWrapperHeight) / 2;
287
+ const baseScale = imgW / wrapperW;
288
+ if (isNaN(baseScale) || baseScale === 0) {
289
+ return null;
290
+ }
291
+ const sx = (-topLeftOffsetX * baseScale) / currentZoom;
292
+ const sy = (-topLeftOffsetY * baseScale) / currentZoom;
293
+ const sWidth = (cropW * baseScale) / currentZoom;
294
+ const sHeight = (cropH * baseScale) / currentZoom;
295
+ const finalX = clamp(Math.round(sx), 0, imgW);
296
+ const finalY = clamp(Math.round(sy), 0, imgH);
297
+ const finalWidth = clamp(Math.round(sWidth), 0, imgW - finalX);
298
+ const finalHeight = clamp(Math.round(sHeight), 0, imgH - finalY);
299
+ if (finalWidth <= 0 || finalHeight <= 0) {
300
+ return null;
301
+ }
302
+ return { x: finalX, y: finalY, width: finalWidth, height: finalHeight };
303
+ }
304
+ setupCropCalculationEffect() {
305
+ effect(() => {
306
+ const wrapperW = this.imageWrapperWidth();
307
+ const wrapperH = this.imageWrapperHeight();
308
+ const cropW = this.cropAreaWidth();
309
+ const cropH = this.cropAreaHeight();
310
+ const currentZoom = this.effectiveZoom();
311
+ if (wrapperW > 0 && wrapperH > 0 && cropW > 0 && cropH > 0) {
312
+ if (!this.isInitialSetupDone()) {
313
+ const restrictedInitial = this.restrictOffset(0, 0, currentZoom);
314
+ this.offsetX.set(restrictedInitial.x);
315
+ this.offsetY.set(restrictedInitial.y);
316
+ if (!this.isZoomControlled()) {
317
+ this.internalZoom.set(currentZoom);
318
+ }
319
+ this.dragStartOffset.set(restrictedInitial);
320
+ this.latestRestrictedOffset.set(restrictedInitial);
321
+ this.latestZoom.set(currentZoom);
322
+ this.onCropChange.emit(this.calculateCropData(restrictedInitial.x, restrictedInitial.y, currentZoom));
323
+ this.isInitialSetupDone.set(true);
324
+ }
325
+ else {
326
+ const currentX = this.latestRestrictedOffset().x;
327
+ const currentY = this.latestRestrictedOffset().y;
328
+ const restrictedCurrent = this.restrictOffset(currentX, currentY, currentZoom);
329
+ if (restrictedCurrent.x !== currentX || restrictedCurrent.y !== currentY) {
330
+ this.offsetX.set(restrictedCurrent.x);
331
+ this.offsetY.set(restrictedCurrent.y);
332
+ this.latestRestrictedOffset.set(restrictedCurrent);
333
+ this.dragStartOffset.set(restrictedCurrent);
334
+ }
335
+ this.onCropChange.emit(this.calculateCropData(restrictedCurrent.x, restrictedCurrent.y, currentZoom));
336
+ }
337
+ }
338
+ else {
339
+ this.isInitialSetupDone.set(false);
340
+ this.offsetX.set(0);
341
+ this.offsetY.set(0);
342
+ if (!this.isZoomControlled()) {
343
+ this.internalZoom.set(this.minZoom());
344
+ }
345
+ this.dragStartOffset.set({ x: 0, y: 0 });
346
+ this.latestRestrictedOffset.set({ x: 0, y: 0 });
347
+ this.latestZoom.set(currentZoom);
348
+ this.onCropChange.emit(null);
349
+ }
350
+ });
351
+ }
352
+ setupAccessibilityWarningEffect() {
353
+ effect(() => {
354
+ const checkTimeout = setTimeout(() => {
355
+ if (this.elementRef.nativeElement && !this.hasWarned()) {
356
+ const hasDescription = document.getElementById(this.descriptionId());
357
+ if (!hasDescription) {
358
+ console.warn(this.CROPPER_DESC_WARN_MESSAGE);
359
+ this.hasWarned.set(true);
360
+ }
361
+ }
362
+ }, 100);
363
+ return () => clearTimeout(checkTimeout);
364
+ });
365
+ }
366
+ setupEventListenersEffect() {
367
+ effect((onCleanup) => {
368
+ const node = this.elementRef.nativeElement;
369
+ if (!node)
370
+ return;
371
+ const options = { passive: false };
372
+ const wheelHandler = (e) => this.handleWheel(e);
373
+ const touchStartHandler = (e) => this.handleTouchStart(e);
374
+ const touchMoveHandler = (e) => this.handleTouchMove(e);
375
+ const touchEndHandler = (e) => this.handleTouchEnd(e);
376
+ node.addEventListener('wheel', wheelHandler, options);
377
+ node.addEventListener('touchstart', touchStartHandler, options);
378
+ node.addEventListener('touchmove', touchMoveHandler, options);
379
+ node.addEventListener('touchend', touchEndHandler, options);
380
+ node.addEventListener('touchcancel', touchEndHandler, options);
381
+ onCleanup(() => {
382
+ node.removeEventListener('wheel', wheelHandler, options);
383
+ node.removeEventListener('touchstart', touchStartHandler, options);
384
+ node.removeEventListener('touchmove', touchMoveHandler, options);
385
+ node.removeEventListener('touchend', touchEndHandler, options);
386
+ node.removeEventListener('touchcancel', touchEndHandler, options);
387
+ });
388
+ });
389
+ }
390
+ handleInteractionEnd() {
391
+ const finalData = this.calculateCropData(this.latestRestrictedOffset().x, this.latestRestrictedOffset().y, this.effectiveZoom());
392
+ this.onCropChange.emit(finalData);
393
+ }
394
+ /**
395
+ * @ignore
396
+ */
397
+ onMouseDown(e) {
398
+ if (e.button !== 0)
399
+ return;
400
+ e.preventDefault();
401
+ this.isDragging.set(true);
402
+ this.isPinching.set(false);
403
+ this.dragStartPoint.set({ x: e.clientX, y: e.clientY });
404
+ this.dragStartOffset.set(this.latestRestrictedOffset());
405
+ const handleMouseMove = (ev) => {
406
+ const deltaX = ev.clientX - this.dragStartPoint().x;
407
+ const deltaY = ev.clientY - this.dragStartPoint().y;
408
+ const targetOffsetX = this.dragStartOffset().x + deltaX;
409
+ const targetOffsetY = this.dragStartOffset().y + deltaY;
410
+ const restricted = this.restrictOffset(targetOffsetX, targetOffsetY, this.effectiveZoom());
411
+ this.latestRestrictedOffset.set(restricted);
412
+ this.offsetX.set(restricted.x);
413
+ this.offsetY.set(restricted.y);
414
+ };
415
+ const handleMouseUp = () => {
416
+ this.isDragging.set(false);
417
+ window.removeEventListener('mousemove', handleMouseMove);
418
+ window.removeEventListener('mouseup', handleMouseUp);
419
+ this.handleInteractionEnd();
420
+ };
421
+ window.addEventListener('mousemove', handleMouseMove);
422
+ window.addEventListener('mouseup', handleMouseUp);
423
+ }
424
+ handleWheel(e) {
425
+ e.preventDefault();
426
+ e.stopPropagation();
427
+ if (!this.elementRef.nativeElement || this.imageWrapperWidth() <= 0 || this.imageWrapperHeight() <= 0)
428
+ return;
429
+ const currentZoom = this.latestZoom();
430
+ const currentOffsetX = this.latestRestrictedOffset().x;
431
+ const currentOffsetY = this.latestRestrictedOffset().y;
432
+ const delta = e.deltaY * -this.zoomSensitivity();
433
+ const targetZoom = currentZoom + delta;
434
+ if (clamp(targetZoom, this.minZoom(), this.maxZoom()) === currentZoom)
435
+ return;
436
+ const rect = this.elementRef.nativeElement.getBoundingClientRect();
437
+ const pointerX = e.clientX - rect.left - rect.width / 2;
438
+ const pointerY = e.clientY - rect.top - rect.height / 2;
439
+ const imagePointX = (pointerX - currentOffsetX) / currentZoom;
440
+ const imagePointY = (pointerY - currentOffsetY) / currentZoom;
441
+ const finalNewZoom = this.updateZoom(targetZoom);
442
+ const newOffsetX = pointerX - imagePointX * finalNewZoom;
443
+ const newOffsetY = pointerY - imagePointY * finalNewZoom;
444
+ const restrictedNewOffset = this.restrictOffset(newOffsetX, newOffsetY, finalNewZoom);
445
+ this.offsetX.set(restrictedNewOffset.x);
446
+ this.offsetY.set(restrictedNewOffset.y);
447
+ this.latestRestrictedOffset.set(restrictedNewOffset);
448
+ const finalData = this.calculateCropData(restrictedNewOffset.x, restrictedNewOffset.y, finalNewZoom);
449
+ this.onCropChange.emit(finalData);
450
+ }
451
+ getPinchDistance(touches) {
452
+ return Math.sqrt(Math.pow(touches[1].clientX - touches[0].clientX, 2) + Math.pow(touches[1].clientY - touches[0].clientY, 2));
453
+ }
454
+ getPinchCenter(touches) {
455
+ return {
456
+ x: (touches[0].clientX + touches[1].clientX) / 2,
457
+ y: (touches[0].clientY + touches[1].clientY) / 2
458
+ };
459
+ }
460
+ handleTouchStart(e) {
461
+ if (this.imageWrapperWidth() <= 0 || this.imageWrapperHeight() <= 0)
462
+ return;
463
+ e.preventDefault();
464
+ const touches = e.touches;
465
+ if (touches.length === 1) {
466
+ this.isDragging.set(true);
467
+ this.isPinching.set(false);
468
+ this.dragStartPoint.set({ x: touches[0].clientX, y: touches[0].clientY });
469
+ this.dragStartOffset.set(this.latestRestrictedOffset());
470
+ }
471
+ else if (touches.length === 2) {
472
+ this.isDragging.set(false);
473
+ this.isPinching.set(true);
474
+ this.initialPinchDistance.set(this.getPinchDistance(touches));
475
+ this.initialPinchZoom.set(this.latestZoom());
476
+ this.dragStartOffset.set(this.latestRestrictedOffset());
477
+ }
478
+ }
479
+ handleTouchMove(e) {
480
+ if (this.imageWrapperWidth() <= 0 || this.imageWrapperHeight() <= 0)
481
+ return;
482
+ e.preventDefault();
483
+ const touches = e.touches;
484
+ if (touches.length === 1 && this.isDragging() && !this.isPinching()) {
485
+ const deltaX = touches[0].clientX - this.dragStartPoint().x;
486
+ const deltaY = touches[0].clientY - this.dragStartPoint().y;
487
+ const targetOffsetX = this.dragStartOffset().x + deltaX;
488
+ const targetOffsetY = this.dragStartOffset().y + deltaY;
489
+ const restricted = this.restrictOffset(targetOffsetX, targetOffsetY, this.effectiveZoom());
490
+ this.latestRestrictedOffset.set(restricted);
491
+ this.offsetX.set(restricted.x);
492
+ this.offsetY.set(restricted.y);
493
+ }
494
+ else if (touches.length === 2 && this.isPinching()) {
495
+ const currentPinchDistance = this.getPinchDistance(touches);
496
+ const scale = currentPinchDistance / this.initialPinchDistance();
497
+ const currentZoom = this.initialPinchZoom();
498
+ const targetZoom = currentZoom * scale;
499
+ if (clamp(targetZoom, this.minZoom(), this.maxZoom()) === this.latestZoom())
500
+ return;
501
+ const pinchCenter = this.getPinchCenter(touches);
502
+ const rect = this.elementRef.nativeElement.getBoundingClientRect();
503
+ const pinchCenterX = pinchCenter.x - rect.left - rect.width / 2;
504
+ const pinchCenterY = pinchCenter.y - rect.top - rect.height / 2;
505
+ const currentOffsetX = this.dragStartOffset().x;
506
+ const currentOffsetY = this.dragStartOffset().y;
507
+ const imagePointX = (pinchCenterX - currentOffsetX) / currentZoom;
508
+ const imagePointY = (pinchCenterY - currentOffsetY) / currentZoom;
509
+ const finalNewZoom = this.updateZoom(targetZoom);
510
+ const newOffsetX = pinchCenterX - imagePointX * finalNewZoom;
511
+ const newOffsetY = pinchCenterY - imagePointY * finalNewZoom;
512
+ const restrictedNewOffset = this.restrictOffset(newOffsetX, newOffsetY, finalNewZoom);
513
+ this.offsetX.set(restrictedNewOffset.x);
514
+ this.offsetY.set(restrictedNewOffset.y);
515
+ this.latestRestrictedOffset.set(restrictedNewOffset);
516
+ const finalData = this.calculateCropData(restrictedNewOffset.x, restrictedNewOffset.y, finalNewZoom);
517
+ this.onCropChange.emit(finalData);
518
+ }
519
+ }
520
+ handleTouchEnd(e) {
521
+ e.preventDefault();
522
+ const touches = e.touches;
523
+ if (this.isPinching() && touches.length < 2) {
524
+ this.isPinching.set(false);
525
+ if (touches.length === 1) {
526
+ this.isDragging.set(true);
527
+ this.dragStartPoint.set({ x: touches[0].clientX, y: touches[0].clientY });
528
+ this.dragStartOffset.set(this.latestRestrictedOffset());
529
+ }
530
+ else {
531
+ this.isDragging.set(false);
532
+ this.handleInteractionEnd();
533
+ }
534
+ }
535
+ else if (this.isDragging() && touches.length === 0) {
536
+ this.isDragging.set(false);
537
+ this.handleInteractionEnd();
538
+ }
539
+ }
540
+ /**
541
+ * @ignore
542
+ */
543
+ onKeyDown(e) {
544
+ if (this.imageWrapperWidth() <= 0)
545
+ return;
546
+ let targetOffsetX = this.latestRestrictedOffset().x;
547
+ let targetOffsetY = this.latestRestrictedOffset().y;
548
+ let moved = false;
549
+ switch (e.key) {
550
+ case 'ArrowUp':
551
+ targetOffsetY += this.keyboardStep();
552
+ moved = true;
553
+ break;
554
+ case 'ArrowDown':
555
+ targetOffsetY -= this.keyboardStep();
556
+ moved = true;
557
+ break;
558
+ case 'ArrowLeft':
559
+ targetOffsetX += this.keyboardStep();
560
+ moved = true;
561
+ break;
562
+ case 'ArrowRight':
563
+ targetOffsetX -= this.keyboardStep();
564
+ moved = true;
565
+ break;
566
+ default:
567
+ return;
568
+ }
569
+ if (moved) {
570
+ e.preventDefault();
571
+ const restricted = this.restrictOffset(targetOffsetX, targetOffsetY, this.effectiveZoom());
572
+ if (restricted.x !== this.latestRestrictedOffset().x || restricted.y !== this.latestRestrictedOffset().y) {
573
+ this.latestRestrictedOffset.set(restricted);
574
+ this.offsetX.set(restricted.x);
575
+ this.offsetY.set(restricted.y);
576
+ }
577
+ }
578
+ }
579
+ /**
580
+ * @ignore
581
+ */
582
+ onKeyUp(e) {
583
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
584
+ this.handleInteractionEnd();
585
+ }
586
+ }
587
+ /**
588
+ * @ignore
589
+ */
590
+ getImageProps() {
591
+ return {
592
+ src: this.image(),
593
+ alt: 'Image being cropped',
594
+ draggable: false,
595
+ 'aria-hidden': true
596
+ };
597
+ }
598
+ /**
599
+ * @ignore
600
+ */
601
+ getImageWrapperStyle() {
602
+ const wrapperW = this.imageWrapperWidth();
603
+ const wrapperH = this.imageWrapperHeight();
604
+ const offsetX = this.offsetX();
605
+ const offsetY = this.offsetY();
606
+ const zoom = this.effectiveZoom();
607
+ return {
608
+ width: `${wrapperW}px`,
609
+ height: `${wrapperH}px`,
610
+ transform: `translate3d(${offsetX}px, ${offsetY}px, 0px) scale(${zoom})`,
611
+ position: 'absolute',
612
+ left: `calc(50% - ${wrapperW / 2}px)`,
613
+ top: `calc(50% - ${wrapperH / 2}px)`,
614
+ willChange: 'transform'
615
+ };
616
+ }
617
+ /**
618
+ * @ignore
619
+ */
620
+ getCropAreaStyle() {
621
+ return {
622
+ width: `${this.cropAreaWidth()}px`,
623
+ height: `${this.cropAreaHeight()}px`
624
+ };
625
+ }
626
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxCropperRootDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
627
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.4", type: RdxCropperRootDirective, isStandalone: true, selector: "[rdxCropperRoot]", inputs: { image: { classPropertyName: "image", publicName: "image", isSignal: true, isRequired: true, transformFunction: null }, cropPadding: { classPropertyName: "cropPadding", publicName: "cropPadding", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, minZoom: { classPropertyName: "minZoom", publicName: "minZoom", isSignal: true, isRequired: false, transformFunction: null }, maxZoom: { classPropertyName: "maxZoom", publicName: "maxZoom", isSignal: true, isRequired: false, transformFunction: null }, zoomSensitivity: { classPropertyName: "zoomSensitivity", publicName: "zoomSensitivity", isSignal: true, isRequired: false, transformFunction: null }, keyboardStep: { classPropertyName: "keyboardStep", publicName: "keyboardStep", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onCropChange: "onCropChange", onZoomChange: "onZoomChange" }, host: { attributes: { "tabindex": "0", "role": "application" }, listeners: { "mousedown": "onMouseDown($event)", "keydown": "onKeyDown($event)", "keyup": "onKeyUp($event)" }, properties: { "attr.aria-label": "\"Interactive image cropper\"", "attr.aria-describedby": "descriptionId()", "attr.aria-valuemin": "minZoom()", "attr.aria-valuemax": "maxZoom()", "attr.aria-valuenow": "effectiveZoom()", "attr.aria-valuetext": "zoomValueText()" } }, providers: [provideToken(CROPPER_ROOT_CONTEXT, RdxCropperRootDirective)], ngImport: i0 }); }
628
+ }
629
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxCropperRootDirective, decorators: [{
630
+ type: Directive,
631
+ args: [{
632
+ selector: '[rdxCropperRoot]',
633
+ providers: [provideToken(CROPPER_ROOT_CONTEXT, RdxCropperRootDirective)],
634
+ host: {
635
+ '[attr.aria-label]': '"Interactive image cropper"',
636
+ '[attr.aria-describedby]': 'descriptionId()',
637
+ '[attr.aria-valuemin]': 'minZoom()',
638
+ '[attr.aria-valuemax]': 'maxZoom()',
639
+ '[attr.aria-valuenow]': 'effectiveZoom()',
640
+ '[attr.aria-valuetext]': 'zoomValueText()',
641
+ tabindex: '0',
642
+ role: 'application',
643
+ '(mousedown)': 'onMouseDown($event)',
644
+ '(keydown)': 'onKeyDown($event)',
645
+ '(keyup)': 'onKeyUp($event)'
646
+ }
647
+ }]
648
+ }], ctorParameters: () => [] });
649
+
650
+ const _imports = [
651
+ RdxCropperRootDirective,
652
+ RdxCropperImageComponent,
653
+ RdxCropperCropAreaDirective,
654
+ RdxCropperDescriptionDirective
655
+ ];
656
+ class RdxCropperModule {
657
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxCropperModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
658
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.4", ngImport: i0, type: RdxCropperModule, imports: [RdxCropperRootDirective,
659
+ RdxCropperImageComponent,
660
+ RdxCropperCropAreaDirective,
661
+ RdxCropperDescriptionDirective], exports: [RdxCropperRootDirective,
662
+ RdxCropperImageComponent,
663
+ RdxCropperCropAreaDirective,
664
+ RdxCropperDescriptionDirective] }); }
665
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxCropperModule }); }
666
+ }
667
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxCropperModule, decorators: [{
668
+ type: NgModule,
669
+ args: [{
670
+ imports: [..._imports],
671
+ exports: [..._imports]
672
+ }]
673
+ }] });
674
+
675
+ /**
676
+ * Generated bundle index. Do not edit.
677
+ */
678
+
679
+ export { CROPPER_ROOT_CONTEXT, RdxCropperCropAreaDirective, RdxCropperDescriptionDirective, RdxCropperImageComponent, RdxCropperModule, RdxCropperRootDirective, injectCropperRootContext };
680
+ //# sourceMappingURL=radix-ng-primitives-cropper.mjs.map