@radix-ng/primitives 0.36.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.
- package/avatar/index.d.ts +1 -1
- package/avatar/src/avatar-context.token.d.ts +7 -0
- package/avatar/src/avatar-fallback.directive.d.ts +3 -9
- package/avatar/src/avatar-image.directive.d.ts +11 -11
- package/avatar/src/avatar-root.directive.d.ts +3 -6
- package/avatar/src/types.d.ts +2 -0
- package/checkbox/index.d.ts +1 -1
- package/checkbox/src/checkbox-button.directive.d.ts +1 -1
- package/checkbox/src/checkbox-indicator.directive.d.ts +1 -1
- package/checkbox/src/checkbox-input.directive.d.ts +1 -1
- package/checkbox/src/checkbox.directive.d.ts +3 -3
- package/checkbox/src/checkbox.token.d.ts +3 -3
- package/cropper/README.md +1 -0
- package/cropper/index.d.ts +15 -0
- package/cropper/src/cropper-context.token.d.ts +12 -0
- package/cropper/src/cropper-crop-area.directive.d.ts +6 -0
- package/cropper/src/cropper-description.directive.d.ts +6 -0
- package/cropper/src/cropper-image.component.d.ts +10 -0
- package/cropper/src/cropper-root.directive.d.ts +91 -0
- package/fesm2022/radix-ng-primitives-avatar.mjs +82 -86
- package/fesm2022/radix-ng-primitives-avatar.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-checkbox.mjs +13 -16
- package/fesm2022/radix-ng-primitives-checkbox.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-cropper.mjs +680 -0
- package/fesm2022/radix-ng-primitives-cropper.mjs.map +1 -0
- package/fesm2022/radix-ng-primitives-stepper.mjs +1 -14
- package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
- package/hover-card/src/hover-card-root.directive.d.ts +4 -4
- package/package.json +5 -1
- package/popover/src/popover-root.directive.d.ts +4 -4
- package/tooltip/src/tooltip-root.directive.d.ts +4 -4
- package/stepper/src/utils/getActiveElement.d.ts +0 -1
@@ -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
|