ngwr 14.5.1 → 14.7.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 (41) hide show
  1. package/alert/styles/_index.scss +14 -3
  2. package/avatar/styles/_index.scss +90 -0
  3. package/cascader/styles/_rules.scss +0 -4
  4. package/collapse/styles/_index.scss +0 -1
  5. package/fesm2022/ngwr-avatar.mjs +76 -8
  6. package/fesm2022/ngwr-avatar.mjs.map +1 -1
  7. package/fesm2022/ngwr-cascader.mjs +2 -2
  8. package/fesm2022/ngwr-cascader.mjs.map +1 -1
  9. package/fesm2022/ngwr-image-cropper-testing.mjs +34 -3
  10. package/fesm2022/ngwr-image-cropper-testing.mjs.map +1 -1
  11. package/fesm2022/ngwr-image-cropper.mjs +245 -65
  12. package/fesm2022/ngwr-image-cropper.mjs.map +1 -1
  13. package/fesm2022/ngwr-lightbox.mjs +2 -2
  14. package/fesm2022/ngwr-lightbox.mjs.map +1 -1
  15. package/fesm2022/ngwr-pagination.mjs +1 -1
  16. package/fesm2022/ngwr-pagination.mjs.map +1 -1
  17. package/fesm2022/ngwr-schema-form.mjs +1 -1
  18. package/fesm2022/ngwr-schema-form.mjs.map +1 -1
  19. package/fesm2022/ngwr-select-testing.mjs +21 -2
  20. package/fesm2022/ngwr-select-testing.mjs.map +1 -1
  21. package/fesm2022/ngwr-select.mjs +299 -50
  22. package/fesm2022/ngwr-select.mjs.map +1 -1
  23. package/fesm2022/ngwr-version.mjs +1 -1
  24. package/fesm2022/ngwr-version.mjs.map +1 -1
  25. package/image-cropper/styles/_index.scss +7 -0
  26. package/input/styles/_index.scss +8 -2
  27. package/lightbox/styles/_rules.scss +7 -1
  28. package/llms-full.txt +4 -4
  29. package/package.json +1 -1
  30. package/schematics/use/symbol-map.json +1 -0
  31. package/select/styles/_index.scss +50 -4
  32. package/skills/ngwr/references/catalog.md +1 -1
  33. package/table/styles/_index.scss +55 -0
  34. package/toast/styles/_index.scss +9 -3
  35. package/tree/styles/_index.scss +0 -3
  36. package/types/ngwr-avatar.d.ts +51 -4
  37. package/types/ngwr-image-cropper-testing.d.ts +25 -3
  38. package/types/ngwr-image-cropper.d.ts +136 -2
  39. package/types/ngwr-select-testing.d.ts +18 -1
  40. package/types/ngwr-select.d.ts +238 -10
  41. package/types/ngwr-version.d.ts +1 -1
@@ -2,7 +2,7 @@ import { coerceNumberProperty } from '@angular/cdk/coercion';
2
2
  import * as i0 from '@angular/core';
3
3
  import { signal, input, output, viewChild, inject, DestroyRef, computed, effect, ViewEncapsulation, Component } from '@angular/core';
4
4
  import { useI18nText } from 'ngwr/i18n';
5
- import { randomId, clamp } from 'ngwr/utils';
5
+ import { clamp, randomId } from 'ngwr/utils';
6
6
 
7
7
  /**
8
8
  * @license
@@ -10,6 +10,17 @@ import { randomId, clamp } from 'ngwr/utils';
10
10
  * Use of this source code is governed by an MIT-style license that can be
11
11
  * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
12
12
  */
13
+ /**
14
+ * `[maxOutputSize]`'s transform. Anything that is not a positive, finite number —
15
+ * `null`, an empty attribute, `0`, a typo — means "no cap" rather than a 1px export:
16
+ * a bad config should cost resolution nobody asked to lose, not the whole image.
17
+ */
18
+ function coerceMaxOutputSize(value) {
19
+ if (value === null || value === undefined || value === '')
20
+ return null;
21
+ const n = coerceNumberProperty(value, Number.NaN);
22
+ return Number.isFinite(n) && n > 0 ? n : null;
23
+ }
13
24
  /**
14
25
  * Stands in for the consumer input `useI18nText` expects. None of this
15
26
  * component's three strings has a `*Label` input behind it — the catalog is the
@@ -19,6 +30,67 @@ const NO_OVERRIDE = signal(null).asReadonly();
19
30
  /** Keyboard step in display pixels, and what Shift promotes it to. */
20
31
  const KEY_STEP = 1;
21
32
  const KEY_STEP_COARSE = 10;
33
+ const NO_SIDES = { left: 0, top: 0, right: 0, bottom: 0 };
34
+ /**
35
+ * The rect a resize gesture produces: a pure function of the rect the gesture started
36
+ * from and the TOTAL pointer offset, so the same pointer position always gives the same
37
+ * rect however the pointer got there.
38
+ *
39
+ * The anchor never moves. It is the edge or corner opposite the handle — for `se` the
40
+ * top-left corner, for `e` the west edge's line. The old version clamped `x`, `y`, `w`
41
+ * and `h` one at a time after deriving the second axis, so once the derived axis ran
42
+ * into the image edge the rect slid AWAY from its anchor and, past the far bound, off
43
+ * the image entirely.
44
+ *
45
+ * With a ratio the size is the largest along that ratio which fits from the anchor —
46
+ * the smaller of the width-limited and height-limited sizes — and no smaller than both
47
+ * minimums allow. Where the image cannot fit the minimum from this anchor, the bound
48
+ * wins: a crop past the edge of the image is worse than one under the minimum.
49
+ *
50
+ * An edge handle with a ratio also sizes the axis it does not drive, and that axis has
51
+ * the image's WHOLE extent to grow into: the rect stays centred on its old midline where
52
+ * it can and slides along that axis only as far as it must to stay on the image. Capping
53
+ * the size at twice the distance from the midline to the nearer side instead would leave
54
+ * a square crop resting against the top edge unable to widen at all.
55
+ */
56
+ function resizeFrom(r, handle, dx, dy, bounds, min, ratio) {
57
+ const east = handle.includes('e');
58
+ const west = handle.includes('w');
59
+ const south = handle.includes('s');
60
+ const north = handle.includes('n');
61
+ const right = r.x + r.w;
62
+ const bottom = r.y + r.h;
63
+ if (!ratio || ratio <= 0) {
64
+ let { x, y, w, h } = r;
65
+ if (east)
66
+ w = clamp(r.w + dx, min.w, bounds.w - r.x);
67
+ if (west) {
68
+ w = clamp(r.w - dx, min.w, right);
69
+ x = right - w;
70
+ }
71
+ if (south)
72
+ h = clamp(r.h + dy, min.h, bounds.h - r.y);
73
+ if (north) {
74
+ h = clamp(r.h - dy, min.h, bottom);
75
+ y = bottom - h;
76
+ }
77
+ return { x, y, w, h };
78
+ }
79
+ // Room from the anchor to the image edge on each axis the handle drives; the whole
80
+ // image on an axis it does not.
81
+ const roomW = east ? bounds.w - r.x : west ? right : bounds.w;
82
+ const roomH = south ? bounds.h - r.y : north ? bottom : bounds.h;
83
+ // Corners follow the horizontal delta, as they always have; `n` / `s` the vertical one.
84
+ const wanted = east ? r.w + dx : west ? r.w - dx : (south ? r.h + dy : r.h - dy) * ratio;
85
+ const w = clamp(wanted, Math.max(min.w, min.h * ratio), Math.min(roomW, roomH * ratio));
86
+ const h = w / ratio;
87
+ return {
88
+ x: east ? r.x : west ? right - w : clamp(r.x + r.w / 2 - w / 2, 0, bounds.w - w),
89
+ y: south ? r.y : north ? bottom - h : clamp(r.y + r.h / 2 - h / 2, 0, bounds.h - h),
90
+ w,
91
+ h,
92
+ };
93
+ }
22
94
  /**
23
95
  * Image crop UI. Pass `[src]` (URL, `File`, or `Blob`); the user drags
24
96
  * the crop window or any of its eight handles. Optionally lock the crop
@@ -31,12 +103,18 @@ const KEY_STEP_COARSE = 10;
31
103
  * freshly-rendered `Blob` of the cropped region. For one-off reads use
32
104
  * `toBlob()` / `toDataUrl()`.
33
105
  *
106
+ * A source the browser cannot decode fires `(loadError)` once, sets `status()` to
107
+ * `'failed'` and puts `wr-image-cropper--failed` on the host. The component renders
108
+ * no message of its own for it — what went wrong and what to do next is the host's
109
+ * to say.
110
+ *
34
111
  * @example
35
112
  * ```html
36
113
  * <wr-image-cropper
37
114
  * [src]="file"
38
115
  * [aspectRatio]="1"
39
116
  * (cropped)="onBlob($event)"
117
+ * (loadError)="onUnreadable($event)"
40
118
  * />
41
119
  * ```
42
120
  *
@@ -59,8 +137,23 @@ class WrImageCropper {
59
137
  /** JPEG / WebP quality for `(cropped)` in [0, 1]. @default 0.92 */
60
138
  outputQuality = input(0.92, /* @ts-ignore */
61
139
  ...(ngDevMode ? [{ debugName: "outputQuality" }] : /* istanbul ignore next */ []));
140
+ /**
141
+ * Longest side of the exported image, in pixels — for `(cropped)`, `toBlob()` and
142
+ * `toDataUrl()` alike. A crop larger than this is scaled down to fit, keeping its
143
+ * aspect ratio; a smaller one is exported as it is, never upscaled. The crop itself
144
+ * — `cropRect()`, in source pixels — is not affected. `null` (or anything that is
145
+ * not a positive number) exports at the source image's own resolution.
146
+ * @default null
147
+ */
148
+ maxOutputSize = input(null, { ...(ngDevMode ? { debugName: "maxOutputSize" } : /* istanbul ignore next */ {}), transform: coerceMaxOutputSize });
62
149
  /** Emits a Blob after each drag end. */
63
150
  cropped = output();
151
+ /**
152
+ * Fires once when the browser cannot decode the current `src` — a format it does
153
+ * not support, a broken file, a URL that fails. Nothing is rendered for it; show
154
+ * your own message. A new `src` clears the failure.
155
+ */
156
+ loadError = output();
64
157
  imgEl = viewChild.required('img', /* @ts-ignore */
65
158
  ...(ngDevMode ? [{ debugName: "imgEl" }] : /* istanbul ignore next */ []));
66
159
  /** Resolved object URL for `src` (so File / Blob render in `<img>`). */
@@ -96,7 +189,21 @@ class WrImageCropper {
96
189
  active = null;
97
190
  startPointer = { x: 0, y: 0 };
98
191
  startRect = { x: 0, y: 0, w: 0, h: 0 };
192
+ /** The element the live drag set pointer capture on, so the end releases that one. */
193
+ captureTarget = null;
194
+ /** The pointer the live drag belongs to. Every other pointer's events are ignored. */
195
+ pointerId = null;
196
+ /** How far past each side of the crop the live resize was pressed — see `onPointerMove`. */
197
+ overhang = NO_SIDES;
99
198
  destroyRef = inject(DestroyRef);
199
+ statusState = signal('empty', /* @ts-ignore */
200
+ ...(ngDevMode ? [{ debugName: "statusState" }] : /* istanbul ignore next */ []));
201
+ /**
202
+ * Where the source stands: `'empty'`, `'loading'`, `'loaded'` or `'failed'`. Reset
203
+ * to `'loading'` (or `'empty'`) whenever `src` changes, so a failure never outlives
204
+ * the source that caused it.
205
+ */
206
+ status = this.statusState.asReadonly();
100
207
  /** Resolved crop rect in natural (source) pixel coordinates. */
101
208
  cropRect = computed(() => {
102
209
  const display = this.display();
@@ -132,8 +239,13 @@ class WrImageCropper {
132
239
  this.resetGeometry();
133
240
  if (!src) {
134
241
  this.objectUrl.set(null);
242
+ this.statusState.set('empty');
135
243
  return;
136
244
  }
245
+ // A new source starts over: a failure belongs to the source that caused it, and
246
+ // left in place it would mark a perfectly good next image as broken until the
247
+ // browser answered for that one too.
248
+ this.statusState.set('loading');
137
249
  if (typeof src === 'string') {
138
250
  this.objectUrl.set(src);
139
251
  }
@@ -175,11 +287,38 @@ class WrImageCropper {
175
287
  w: img.naturalWidth || display.w,
176
288
  h: img.naturalHeight || display.h,
177
289
  };
290
+ this.statusState.set('loaded');
178
291
  this.natural.set(natural);
179
292
  this.display.set(display);
180
293
  this.cropDisplay.set(this.initialCrop(display));
181
294
  this.watchResize(img);
182
295
  }
296
+ /**
297
+ * The `<img>` could not decode its source.
298
+ *
299
+ * Before this handler existed a failed decode left the component exactly as it
300
+ * looks while loading — an image with no size, no crop window, nothing emitted —
301
+ * so a host had no way to tell "still decoding" from "never going to", and a user
302
+ * who picked a `.heic` the browser cannot read saw an empty box and no reason.
303
+ *
304
+ * Once per source: `status` is reset by a new `src` and by nothing else, so a
305
+ * second `error` for the same source says nothing new and is not re-emitted.
306
+ */
307
+ onImageError() {
308
+ const url = this.objectUrl();
309
+ if (!url || this.statusState() === 'failed')
310
+ return;
311
+ this.statusState.set('failed');
312
+ this.resetGeometry();
313
+ const src = this.src();
314
+ const isBlob = typeof src !== 'string' && src !== null;
315
+ this.loadError.emit({
316
+ url,
317
+ name: typeof File !== 'undefined' && src instanceof File ? src.name : null,
318
+ type: isBlob ? src.type : null,
319
+ size: isBlob ? src.size : null,
320
+ });
321
+ }
183
322
  /**
184
323
  * Keep `display` on the box the image is actually rendered at.
185
324
  *
@@ -258,27 +397,98 @@ class WrImageCropper {
258
397
  event.preventDefault();
259
398
  event.stopPropagation();
260
399
  this.active = handle;
400
+ this.pointerId = event.pointerId;
261
401
  this.startPointer = { x: event.clientX, y: event.clientY };
262
402
  this.startRect = { ...this.cropDisplay() };
263
- event.currentTarget.setPointerCapture(event.pointerId);
403
+ this.captureTarget = event.currentTarget;
404
+ this.captureTarget.setPointerCapture(event.pointerId);
405
+ this.overhang = handle === 'move' ? NO_SIDES : this.pastCrop(event);
264
406
  }
407
+ /**
408
+ * A resize ENDS when the pointer leaves the image, exactly as `pointerup` would end it:
409
+ * the move that carried it out is applied — clamped to the image like any other — and
410
+ * then the drag is over. Nothing after that resumes it or changes the crop, pointer
411
+ * back over the image or not, until a new `pointerdown`. A move drag is not affected:
412
+ * its size is fixed, so it simply rests against the edge.
413
+ *
414
+ * The move that leaves is applied rather than dropped because pointer moves are
415
+ * SAMPLES. Dropped, a quick drag toward a corner would stop wherever its last sample
416
+ * inside happened to fall — a single move from a handle to past the image would change
417
+ * nothing at all — and even a slow one would stop a fraction of a pixel short of the
418
+ * edge. A drag that leaves across one edge still ends there: heading for a corner at a
419
+ * slant that crosses the bottom first, the crop's right edge stays where it was then.
420
+ *
421
+ * "The image" is the `<img>`'s own box — the bounds `applyMove` / `applyResize` clamp
422
+ * the crop to — with its edges counting as inside, since the crop may reach them. The
423
+ * box is re-measured on every move and compared with `clientX` / `clientY`: both are
424
+ * viewport coordinates, so a scroll mid-drag cannot put them out of step. The moves
425
+ * outside reach this handler at all only because the drag holds pointer capture.
426
+ *
427
+ * One allowance, fixed at `pointerdown`: the box is widened on each side by however far
428
+ * past that side of the CROP the press landed — at most as far as the pressed handle
429
+ * reaches past it. Half of an edge handle, and three quarters of a corner handle, lies
430
+ * outside the crop, so a press there already has the pointer ahead of the edge it drags.
431
+ * Measured without the allowance, that pointer would leave the image while the crop was
432
+ * still short of the image's edge by the same distance, with nothing able to close the
433
+ * gap; and a handle resting on the image's edge, pressed on its outer part, would begin
434
+ * its drag already outside. A press on a handle's inner part widens nothing.
435
+ */
265
436
  onPointerMove(event) {
266
- if (!this.active)
437
+ if (!this.active || event.pointerId !== this.pointerId)
267
438
  return;
268
439
  const dx = event.clientX - this.startPointer.x;
269
440
  const dy = event.clientY - this.startPointer.y;
270
- if (this.active === 'move')
441
+ if (this.active === 'move') {
271
442
  this.applyMove(dx, dy);
272
- else
273
- this.applyResize(this.active, dx, dy);
443
+ return;
444
+ }
445
+ this.applyResize(this.active, dx, dy);
446
+ if (!this.withinImage(event))
447
+ this.onPointerUp(event);
274
448
  }
449
+ /**
450
+ * Ends the drag — for `pointerup`, `pointercancel`, a capture lost to something else,
451
+ * and a resize whose pointer left the image. Once per drag: the capture released here
452
+ * fires `lostpointercapture` afterwards, and by then there is nothing left to end.
453
+ *
454
+ * Only for the pointer that started it. A second finger is refused a drag of its own at
455
+ * `pointerdown`, but its moves and its lifting reach these handlers all the same, and
456
+ * would otherwise drive or end the first finger's drag.
457
+ */
275
458
  onPointerUp(event) {
276
- if (!this.active)
459
+ if (!this.active || event.pointerId !== this.pointerId)
277
460
  return;
278
- event.currentTarget.releasePointerCapture(event.pointerId);
461
+ const target = this.captureTarget;
279
462
  this.active = null;
463
+ this.pointerId = null;
464
+ this.captureTarget = null;
465
+ this.overhang = NO_SIDES;
466
+ // Asked first: a capture that is already gone has nothing to release, and releasing
467
+ // one for a pointer that is no longer active throws.
468
+ if (target?.hasPointerCapture(event.pointerId))
469
+ target.releasePointerCapture(event.pointerId);
280
470
  void this.emitCropped();
281
471
  }
472
+ /** How far past each side of the crop window the pointer is, in CSS pixels. */
473
+ pastCrop(event) {
474
+ const box = this.imgEl().nativeElement.getBoundingClientRect();
475
+ const c = this.startRect;
476
+ return {
477
+ left: Math.max(0, box.left + c.x - event.clientX),
478
+ top: Math.max(0, box.top + c.y - event.clientY),
479
+ right: Math.max(0, event.clientX - (box.left + c.x + c.w)),
480
+ bottom: Math.max(0, event.clientY - (box.top + c.y + c.h)),
481
+ };
482
+ }
483
+ /** Whether the pointer is on the image, edges and the press's `overhang` included. */
484
+ withinImage(event) {
485
+ const box = this.imgEl().nativeElement.getBoundingClientRect();
486
+ const o = this.overhang;
487
+ return (event.clientX >= box.left - o.left &&
488
+ event.clientX <= box.right + o.right &&
489
+ event.clientY >= box.top - o.top &&
490
+ event.clientY <= box.bottom + o.bottom);
491
+ }
282
492
  // Keyboard
283
493
  /** Whether a key has moved the crop since the last `keyup`. */
284
494
  keyboardDirty = false;
@@ -356,59 +566,8 @@ class WrImageCropper {
356
566
  this.cropDisplay.set(next);
357
567
  }
358
568
  applyResize(handle, dx, dy) {
359
- const display = this.display();
360
569
  const min = { w: this.minWidth(), h: this.minHeight() };
361
- const ratio = this.aspectRatio();
362
- const r = this.startRect;
363
- let x = r.x;
364
- let y = r.y;
365
- let w = r.w;
366
- let h = r.h;
367
- if (handle.includes('e'))
368
- w = clamp(r.w + dx, min.w, display.w - r.x);
369
- if (handle.includes('w')) {
370
- const nextW = clamp(r.w - dx, min.w, r.x + r.w);
371
- x = r.x + (r.w - nextW);
372
- w = nextW;
373
- }
374
- if (handle.includes('s'))
375
- h = clamp(r.h + dy, min.h, display.h - r.y);
376
- if (handle.includes('n')) {
377
- const nextH = clamp(r.h - dy, min.h, r.y + r.h);
378
- y = r.y + (r.h - nextH);
379
- h = nextH;
380
- }
381
- if (ratio && ratio > 0) {
382
- // Lock the orthogonal axis to maintain ratio. For corner handles,
383
- // pick whichever delta drives the larger change so we don't bounce.
384
- if (handle === 'e' || handle === 'w') {
385
- const newH = w / ratio;
386
- const dh = newH - r.h;
387
- if (handle === 'e' || handle === 'w') {
388
- y = r.y - dh / 2;
389
- h = newH;
390
- }
391
- }
392
- else if (handle === 'n' || handle === 's') {
393
- const newW = h * ratio;
394
- const dw = newW - r.w;
395
- x = r.x - dw / 2;
396
- w = newW;
397
- }
398
- else {
399
- // Corner — drive by width, then derive height.
400
- const newH = w / ratio;
401
- if (handle.includes('n'))
402
- y = r.y + r.h - newH;
403
- h = newH;
404
- }
405
- // Re-clamp after ratio adjustment so we never spill out of the canvas.
406
- x = clamp(x, 0, display.w - w);
407
- y = clamp(y, 0, display.h - h);
408
- w = clamp(w, min.w, display.w - x);
409
- h = clamp(h, min.h, display.h - y);
410
- }
411
- this.cropDisplay.set({ x, y, w, h });
570
+ this.cropDisplay.set(resizeFrom(this.startRect, handle, dx, dy, this.display(), min, this.aspectRatio()));
412
571
  }
413
572
  // Public API
414
573
  /** Render the current crop as a Blob. */
@@ -432,12 +591,30 @@ class WrImageCropper {
432
591
  await this.emitCropped();
433
592
  }
434
593
  // Internals
594
+ /**
595
+ * The exported image's pixel size: the crop in source pixels, scaled down so its
596
+ * longer side fits `maxOutputSize`. `Math.min(1, …)` is the never-upscale rule — a
597
+ * crop already inside the cap keeps its own size, which is also the whole of the
598
+ * uncapped path.
599
+ */
600
+ outputSize(c) {
601
+ const cap = this.maxOutputSize();
602
+ const longest = Math.max(c.width, c.height);
603
+ const scale = cap === null || longest === 0 ? 1 : Math.min(1, cap / longest);
604
+ return {
605
+ width: Math.max(1, Math.round(c.width * scale)),
606
+ height: Math.max(1, Math.round(c.height * scale)),
607
+ };
608
+ }
435
609
  toCanvas() {
436
610
  const img = this.imgEl().nativeElement;
437
611
  const c = this.cropRect();
438
612
  const canvas = document.createElement('canvas');
439
- canvas.width = Math.max(1, c.width);
440
- canvas.height = Math.max(1, c.height);
613
+ const size = this.outputSize(c);
614
+ canvas.width = size.width;
615
+ canvas.height = size.height;
616
+ // The SOURCE rect stays the whole crop and the destination is the canvas, so a
617
+ // capped export is the same region drawn smaller rather than a smaller region.
441
618
  const ctx = canvas.getContext('2d');
442
619
  if (ctx)
443
620
  ctx.drawImage(img, c.x, c.y, c.width, c.height, 0, 0, canvas.width, canvas.height);
@@ -453,12 +630,15 @@ class WrImageCropper {
453
630
  }
454
631
  }
455
632
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrImageCropper, deps: [], target: i0.ɵɵFactoryTarget.Component });
456
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: WrImageCropper, isStandalone: true, selector: "wr-image-cropper", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, outputType: { classPropertyName: "outputType", publicName: "outputType", isSignal: true, isRequired: false, transformFunction: null }, outputQuality: { classPropertyName: "outputQuality", publicName: "outputQuality", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cropped: "cropped" }, host: { classAttribute: "wr-image-cropper" }, viewQueries: [{ propertyName: "imgEl", first: true, predicate: ["img"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (objectUrl(); as url) {\n <div class=\"wr-image-cropper__canvas\">\n <img #img class=\"wr-image-cropper__image\" [src]=\"url\" alt=\"\" draggable=\"false\" (load)=\"onImageLoad()\" />\n\n @if (display().w > 0) {\n <!-- Semi-transparent backdrop with a cut-out for the crop window. -->\n <div\n class=\"wr-image-cropper__backdrop\"\n [style.clip-path]=\"`polygon(0 0, 100% 0, 100% 100%, 0 100%, 0 ${cropDisplay().y}px, ${cropDisplay().x}px ${cropDisplay().y}px, ${cropDisplay().x}px ${cropDisplay().y + cropDisplay().h}px, ${cropDisplay().x + cropDisplay().w}px ${cropDisplay().y + cropDisplay().h}px, ${cropDisplay().x + cropDisplay().w}px ${cropDisplay().y}px, 0 ${cropDisplay().y}px)`\"\n ></div>\n\n <!--\n Crop window \u2014 drag to move, with eight resize handles, and the ONE\n keyboard control the component has (see `onWindowKeydown`). The eight\n handles stay pointer-only and `aria-hidden`: eight extra tab stops per\n cropper would be hostile. The keys resize from the east and south edges\n only, so a west or north drag comes out as a resize plus a move rather\n than as one gesture.\n\n `role=\"group\"` rather than no role at all, because `aria-label` is\n prohibited on a roleless `<div>` and this needs a name; and rather than\n `role=\"application\"`, which would switch the reader out of its own\n browse mode for the whole subtree \u2014 far more than one rectangle is worth.\n -->\n <div\n class=\"wr-image-cropper__window\"\n role=\"group\"\n tabindex=\"0\"\n [attr.aria-label]=\"resolvedWindowLabel()\"\n [attr.aria-describedby]=\"helpId\"\n [style.left.px]=\"cropDisplay().x\"\n [style.top.px]=\"cropDisplay().y\"\n [style.width.px]=\"cropDisplay().w\"\n [style.height.px]=\"cropDisplay().h\"\n (pointerdown)=\"onPointerDown('move', $event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (keydown)=\"onWindowKeydown($event)\"\n (keyup)=\"onWindowKeyup()\"\n >\n @for (h of handles; track h) {\n <span\n aria-hidden=\"true\"\n [class]=\"`wr-image-cropper__handle wr-image-cropper__handle--${h}`\"\n (pointerdown)=\"onPointerDown(h, $event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n ></span>\n }\n </div>\n\n <p class=\"wr-image-cropper__sr-only\" [id]=\"helpId\">{{ resolvedKeyHelp() }}</p>\n\n <!--\n The crop rect in SOURCE pixels, announced after a keyboard change only \u2014\n a pointer drag already gives sighted feedback and would flood the region,\n and the initial crop is not news. Deliberately bare numbers: a sentence\n here would be a translated string the catalog does not carry, and the\n reader has just been told what the four numbers mean by `helpId`.\n -->\n <p class=\"wr-image-cropper__sr-only\" role=\"status\">{{ announcement() }}</p>\n }\n </div>\n} @else {\n <div class=\"wr-image-cropper__empty\">{{ emptyText() }}</div>\n}\n", encapsulation: i0.ViewEncapsulation.None });
633
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: WrImageCropper, isStandalone: true, selector: "wr-image-cropper", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, outputType: { classPropertyName: "outputType", publicName: "outputType", isSignal: true, isRequired: false, transformFunction: null }, outputQuality: { classPropertyName: "outputQuality", publicName: "outputQuality", isSignal: true, isRequired: false, transformFunction: null }, maxOutputSize: { classPropertyName: "maxOutputSize", publicName: "maxOutputSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cropped: "cropped", loadError: "loadError" }, host: { properties: { "class.wr-image-cropper--failed": "status() === 'failed'" }, classAttribute: "wr-image-cropper" }, viewQueries: [{ propertyName: "imgEl", first: true, predicate: ["img"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (objectUrl(); as url) {\n <div class=\"wr-image-cropper__canvas\">\n <img\n #img\n class=\"wr-image-cropper__image\"\n [src]=\"url\"\n alt=\"\"\n draggable=\"false\"\n (load)=\"onImageLoad()\"\n (error)=\"onImageError()\"\n />\n\n @if (display().w > 0) {\n <!-- Semi-transparent backdrop with a cut-out for the crop window. -->\n <div\n class=\"wr-image-cropper__backdrop\"\n [style.clip-path]=\"`polygon(0 0, 100% 0, 100% 100%, 0 100%, 0 ${cropDisplay().y}px, ${cropDisplay().x}px ${cropDisplay().y}px, ${cropDisplay().x}px ${cropDisplay().y + cropDisplay().h}px, ${cropDisplay().x + cropDisplay().w}px ${cropDisplay().y + cropDisplay().h}px, ${cropDisplay().x + cropDisplay().w}px ${cropDisplay().y}px, 0 ${cropDisplay().y}px)`\"\n ></div>\n\n <!--\n Crop window \u2014 drag to move, with eight resize handles, and the ONE\n keyboard control the component has (see `onWindowKeydown`). The eight\n handles stay pointer-only and `aria-hidden`: eight extra tab stops per\n cropper would be hostile. The keys resize from the east and south edges\n only, so a west or north drag comes out as a resize plus a move rather\n than as one gesture.\n\n `role=\"group\"` rather than no role at all, because `aria-label` is\n prohibited on a roleless `<div>` and this needs a name; and rather than\n `role=\"application\"`, which would switch the reader out of its own\n browse mode for the whole subtree \u2014 far more than one rectangle is worth.\n -->\n <div\n class=\"wr-image-cropper__window\"\n role=\"group\"\n tabindex=\"0\"\n [attr.aria-label]=\"resolvedWindowLabel()\"\n [attr.aria-describedby]=\"helpId\"\n [style.left.px]=\"cropDisplay().x\"\n [style.top.px]=\"cropDisplay().y\"\n [style.width.px]=\"cropDisplay().w\"\n [style.height.px]=\"cropDisplay().h\"\n (pointerdown)=\"onPointerDown('move', $event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (lostpointercapture)=\"onPointerUp($event)\"\n (keydown)=\"onWindowKeydown($event)\"\n (keyup)=\"onWindowKeyup()\"\n >\n @for (h of handles; track h) {\n <span\n aria-hidden=\"true\"\n [class]=\"`wr-image-cropper__handle wr-image-cropper__handle--${h}`\"\n (pointerdown)=\"onPointerDown(h, $event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (lostpointercapture)=\"onPointerUp($event)\"\n ></span>\n }\n </div>\n\n <p class=\"wr-image-cropper__sr-only\" [id]=\"helpId\">{{ resolvedKeyHelp() }}</p>\n\n <!--\n The crop rect in SOURCE pixels, announced after a keyboard change only \u2014\n a pointer drag already gives sighted feedback and would flood the region,\n and the initial crop is not news. Deliberately bare numbers: a sentence\n here would be a translated string the catalog does not carry, and the\n reader has just been told what the four numbers mean by `helpId`.\n -->\n <p class=\"wr-image-cropper__sr-only\" role=\"status\">{{ announcement() }}</p>\n }\n </div>\n} @else {\n <div class=\"wr-image-cropper__empty\">{{ emptyText() }}</div>\n}\n", encapsulation: i0.ViewEncapsulation.None });
457
634
  }
458
635
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrImageCropper, decorators: [{
459
636
  type: Component,
460
- args: [{ selector: 'wr-image-cropper', encapsulation: ViewEncapsulation.None, host: { class: 'wr-image-cropper' }, template: "@if (objectUrl(); as url) {\n <div class=\"wr-image-cropper__canvas\">\n <img #img class=\"wr-image-cropper__image\" [src]=\"url\" alt=\"\" draggable=\"false\" (load)=\"onImageLoad()\" />\n\n @if (display().w > 0) {\n <!-- Semi-transparent backdrop with a cut-out for the crop window. -->\n <div\n class=\"wr-image-cropper__backdrop\"\n [style.clip-path]=\"`polygon(0 0, 100% 0, 100% 100%, 0 100%, 0 ${cropDisplay().y}px, ${cropDisplay().x}px ${cropDisplay().y}px, ${cropDisplay().x}px ${cropDisplay().y + cropDisplay().h}px, ${cropDisplay().x + cropDisplay().w}px ${cropDisplay().y + cropDisplay().h}px, ${cropDisplay().x + cropDisplay().w}px ${cropDisplay().y}px, 0 ${cropDisplay().y}px)`\"\n ></div>\n\n <!--\n Crop window \u2014 drag to move, with eight resize handles, and the ONE\n keyboard control the component has (see `onWindowKeydown`). The eight\n handles stay pointer-only and `aria-hidden`: eight extra tab stops per\n cropper would be hostile. The keys resize from the east and south edges\n only, so a west or north drag comes out as a resize plus a move rather\n than as one gesture.\n\n `role=\"group\"` rather than no role at all, because `aria-label` is\n prohibited on a roleless `<div>` and this needs a name; and rather than\n `role=\"application\"`, which would switch the reader out of its own\n browse mode for the whole subtree \u2014 far more than one rectangle is worth.\n -->\n <div\n class=\"wr-image-cropper__window\"\n role=\"group\"\n tabindex=\"0\"\n [attr.aria-label]=\"resolvedWindowLabel()\"\n [attr.aria-describedby]=\"helpId\"\n [style.left.px]=\"cropDisplay().x\"\n [style.top.px]=\"cropDisplay().y\"\n [style.width.px]=\"cropDisplay().w\"\n [style.height.px]=\"cropDisplay().h\"\n (pointerdown)=\"onPointerDown('move', $event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (keydown)=\"onWindowKeydown($event)\"\n (keyup)=\"onWindowKeyup()\"\n >\n @for (h of handles; track h) {\n <span\n aria-hidden=\"true\"\n [class]=\"`wr-image-cropper__handle wr-image-cropper__handle--${h}`\"\n (pointerdown)=\"onPointerDown(h, $event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n ></span>\n }\n </div>\n\n <p class=\"wr-image-cropper__sr-only\" [id]=\"helpId\">{{ resolvedKeyHelp() }}</p>\n\n <!--\n The crop rect in SOURCE pixels, announced after a keyboard change only \u2014\n a pointer drag already gives sighted feedback and would flood the region,\n and the initial crop is not news. Deliberately bare numbers: a sentence\n here would be a translated string the catalog does not carry, and the\n reader has just been told what the four numbers mean by `helpId`.\n -->\n <p class=\"wr-image-cropper__sr-only\" role=\"status\">{{ announcement() }}</p>\n }\n </div>\n} @else {\n <div class=\"wr-image-cropper__empty\">{{ emptyText() }}</div>\n}\n" }]
461
- }], ctorParameters: () => [], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], minHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "minHeight", required: false }] }], outputType: [{ type: i0.Input, args: [{ isSignal: true, alias: "outputType", required: false }] }], outputQuality: [{ type: i0.Input, args: [{ isSignal: true, alias: "outputQuality", required: false }] }], cropped: [{ type: i0.Output, args: ["cropped"] }], imgEl: [{ type: i0.ViewChild, args: ['img', { isSignal: true }] }] } });
637
+ args: [{ selector: 'wr-image-cropper', encapsulation: ViewEncapsulation.None, host: {
638
+ class: 'wr-image-cropper',
639
+ '[class.wr-image-cropper--failed]': "status() === 'failed'",
640
+ }, template: "@if (objectUrl(); as url) {\n <div class=\"wr-image-cropper__canvas\">\n <img\n #img\n class=\"wr-image-cropper__image\"\n [src]=\"url\"\n alt=\"\"\n draggable=\"false\"\n (load)=\"onImageLoad()\"\n (error)=\"onImageError()\"\n />\n\n @if (display().w > 0) {\n <!-- Semi-transparent backdrop with a cut-out for the crop window. -->\n <div\n class=\"wr-image-cropper__backdrop\"\n [style.clip-path]=\"`polygon(0 0, 100% 0, 100% 100%, 0 100%, 0 ${cropDisplay().y}px, ${cropDisplay().x}px ${cropDisplay().y}px, ${cropDisplay().x}px ${cropDisplay().y + cropDisplay().h}px, ${cropDisplay().x + cropDisplay().w}px ${cropDisplay().y + cropDisplay().h}px, ${cropDisplay().x + cropDisplay().w}px ${cropDisplay().y}px, 0 ${cropDisplay().y}px)`\"\n ></div>\n\n <!--\n Crop window \u2014 drag to move, with eight resize handles, and the ONE\n keyboard control the component has (see `onWindowKeydown`). The eight\n handles stay pointer-only and `aria-hidden`: eight extra tab stops per\n cropper would be hostile. The keys resize from the east and south edges\n only, so a west or north drag comes out as a resize plus a move rather\n than as one gesture.\n\n `role=\"group\"` rather than no role at all, because `aria-label` is\n prohibited on a roleless `<div>` and this needs a name; and rather than\n `role=\"application\"`, which would switch the reader out of its own\n browse mode for the whole subtree \u2014 far more than one rectangle is worth.\n -->\n <div\n class=\"wr-image-cropper__window\"\n role=\"group\"\n tabindex=\"0\"\n [attr.aria-label]=\"resolvedWindowLabel()\"\n [attr.aria-describedby]=\"helpId\"\n [style.left.px]=\"cropDisplay().x\"\n [style.top.px]=\"cropDisplay().y\"\n [style.width.px]=\"cropDisplay().w\"\n [style.height.px]=\"cropDisplay().h\"\n (pointerdown)=\"onPointerDown('move', $event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (lostpointercapture)=\"onPointerUp($event)\"\n (keydown)=\"onWindowKeydown($event)\"\n (keyup)=\"onWindowKeyup()\"\n >\n @for (h of handles; track h) {\n <span\n aria-hidden=\"true\"\n [class]=\"`wr-image-cropper__handle wr-image-cropper__handle--${h}`\"\n (pointerdown)=\"onPointerDown(h, $event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (lostpointercapture)=\"onPointerUp($event)\"\n ></span>\n }\n </div>\n\n <p class=\"wr-image-cropper__sr-only\" [id]=\"helpId\">{{ resolvedKeyHelp() }}</p>\n\n <!--\n The crop rect in SOURCE pixels, announced after a keyboard change only \u2014\n a pointer drag already gives sighted feedback and would flood the region,\n and the initial crop is not news. Deliberately bare numbers: a sentence\n here would be a translated string the catalog does not carry, and the\n reader has just been told what the four numbers mean by `helpId`.\n -->\n <p class=\"wr-image-cropper__sr-only\" role=\"status\">{{ announcement() }}</p>\n }\n </div>\n} @else {\n <div class=\"wr-image-cropper__empty\">{{ emptyText() }}</div>\n}\n" }]
641
+ }], ctorParameters: () => [], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], minHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "minHeight", required: false }] }], outputType: [{ type: i0.Input, args: [{ isSignal: true, alias: "outputType", required: false }] }], outputQuality: [{ type: i0.Input, args: [{ isSignal: true, alias: "outputQuality", required: false }] }], maxOutputSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxOutputSize", required: false }] }], cropped: [{ type: i0.Output, args: ["cropped"] }], loadError: [{ type: i0.Output, args: ["loadError"] }], imgEl: [{ type: i0.ViewChild, args: ['img', { isSignal: true }] }] } });
462
642
 
463
643
  /**
464
644
  * Generated bundle index. Do not edit.