@anzusystems/common-admin 2.0.0-rc.1 → 2.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common-admin.js +188 -187
- package/dist/domains/dam/cropper/components/ACropper.js.map +1 -1
- package/dist/domains/dam/cropper/components/ACropper.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/domains/dam/cropper/components/ACropper.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/domains/dam/cropper/utils/cropperGeometry.js +1 -1
- package/dist/domains/dam/cropper/utils/cropperGeometry.js.map +1 -1
- package/dist/domains/dam/keyword/factory/KeywordFactory.js +2 -2
- package/dist/domains/dam/keyword/factory/KeywordFactory.js.map +1 -1
- package/dist/domains/filters/composables/filterFactory.d.ts +31 -2
- package/dist/domains/filters/composables/filterFactory.js +78 -67
- package/dist/domains/filters/composables/filterFactory.js.map +1 -1
- package/dist/domains/system/composables/breadcrumbs.d.ts +6 -6
- package/dist/domains/system/composables/breadcrumbs.js +7 -7
- package/dist/domains/system/composables/breadcrumbs.js.map +1 -1
- package/dist/domains/system/composables/recordActions.d.ts +41 -0
- package/dist/domains/system/composables/recordActions.js +33 -0
- package/dist/domains/system/composables/recordActions.js.map +1 -0
- package/dist/domains/system/composables/recordPage.d.ts +14 -4
- package/dist/domains/system/composables/recordPage.js +36 -34
- package/dist/domains/system/composables/recordPage.js.map +1 -1
- package/dist/lib.d.ts +3 -2
- package/dist/vendor/@cropper/element/dist/element.esm.raw.js +1 -1
- package/dist/vendor/@cropper/element/dist/element.esm.raw.js.map +1 -1
- package/dist/vendor/@cropper/element-canvas/dist/element-canvas.esm.raw.js +1 -1
- package/dist/vendor/@cropper/element-canvas/dist/element-canvas.esm.raw.js.map +1 -1
- package/dist/vendor/@cropper/element-crosshair/dist/element-crosshair.esm.raw.js +1 -1
- package/dist/vendor/@cropper/element-crosshair/dist/element-crosshair.esm.raw.js.map +1 -1
- package/dist/vendor/@cropper/element-grid/dist/element-grid.esm.raw.js +1 -1
- package/dist/vendor/@cropper/element-grid/dist/element-grid.esm.raw.js.map +1 -1
- package/dist/vendor/@cropper/element-handle/dist/element-handle.esm.raw.js +1 -1
- package/dist/vendor/@cropper/element-handle/dist/element-handle.esm.raw.js.map +1 -1
- package/dist/vendor/@cropper/element-image/dist/element-image.esm.raw.js +74 -58
- package/dist/vendor/@cropper/element-image/dist/element-image.esm.raw.js.map +1 -1
- package/dist/vendor/@cropper/element-selection/dist/element-selection.esm.raw.js +198 -52
- package/dist/vendor/@cropper/element-selection/dist/element-selection.esm.raw.js.map +1 -1
- package/dist/vendor/@cropper/element-shade/dist/element-shade.esm.raw.js +11 -4
- package/dist/vendor/@cropper/element-shade/dist/element-shade.esm.raw.js.map +1 -1
- package/dist/vendor/@cropper/element-viewer/dist/element-viewer.esm.raw.js +8 -8
- package/dist/vendor/@cropper/element-viewer/dist/element-viewer.esm.raw.js.map +1 -1
- package/dist/vendor/@cropper/utils/dist/utils.esm.raw.js +89 -46
- package/dist/vendor/@cropper/utils/dist/utils.esm.raw.js.map +1 -1
- package/dist/vendor/{cropperjs2 → cropperjs}/dist/cropper.esm.raw.js +11 -11
- package/dist/vendor/cropperjs/dist/cropper.esm.raw.js.map +1 -0
- package/package.json +10 -10
- package/src/eslint/adminConfig.mjs +2 -1
- package/src/eslint/oxlint-admin.json +3 -0
- package/dist/vendor/cropperjs2/dist/cropper.esm.raw.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ACropper.js","names":["_sfc_main","ACropper_vue_vue_type_script_setup_true_lang_default","ACropper_default"],"sources":["../../../../../src/domains/dam/cropper/components/ACropper.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'\nimport { useResizeObserver } from '@vueuse/core'\nimport {\n CropperCanvas,\n CropperCrosshair,\n CropperGrid,\n CropperHandle,\n CropperImage,\n CropperSelection,\n CropperShade,\n} from 'cropperjs2'\nimport {\n canvasSizeFor,\n clampRectInside,\n initialSelectionRect,\n isUsableAspectRatio,\n RESIZE_ANCHORS,\n rectsAreClose,\n type Rect,\n type Size,\n} from '@/domains/dam/cropper/utils/cropperGeometry'\nimport type { CropRect } from '@/domains/dam/cropper/utils/cropperTypes'\n\n/**\n * Image cropper built on cropper.js v2's custom elements, replacing the v1-based `ACropperjs`.\n *\n * It lives here, beside `DamAssetImageRoiSelect`, because that is the only thing that uses it and\n * nothing outside this library does. It is not exported: shaping it to one need rather than to a\n * general audience is what keeps it to its size.\n *\n * The crop is bound, not fetched: `v-model` carries it as fractions of the image, and `commit` says\n * the user has finished a gesture. There is no imperative surface — the one thing a host wants to do\n * with a cropper is know where the crop is, and a binding says that better than six methods did.\n *\n * The elements are assembled in script rather than in the template on purpose: a consumer of this\n * library does not configure `isCustomElement` in their own build, and setting element *properties*\n * instead of going through attribute strings avoids the coercion surprises (`NaN`, booleans,\n * fractional numbers) that come with the attribute round-trip.\n */\n\nconst props = withDefaults(\n defineProps<{\n src?: string\n /** The crop, as fractions of the image. Leave it null to start from the default box. */\n modelValue?: CropRect | null\n /** `NaN` (the default) leaves the crop free-form. */\n aspectRatio?: number\n containerStyle?: { [key: string]: string } | undefined\n /** Wash over everything outside the crop — the replacement for overriding `.cropper-modal`. */\n shadeColor?: string\n }>(),\n {\n src: '',\n modelValue: null,\n aspectRatio: NaN,\n containerStyle: undefined,\n shadeColor: 'rgba(0, 0, 0, 0.5)',\n }\n)\n\nconst emit = defineEmits<{\n (event: 'update:modelValue', crop: CropRect): void\n /** The user has finished a gesture; the crop is worth saving. */\n (event: 'commit', crop: CropRect): void\n (event: 'ready'): void\n}>()\n\n// cropper.js registers its elements as a side effect of loading its entry, and this package's\n// `sideEffects` lets the consumer's bundler drop that module once only the classes are used. `$define`\n// is a no-op for an element that is already registered.\nfor (const element of [\n CropperCanvas,\n CropperCrosshair,\n CropperGrid,\n CropperHandle,\n CropperImage,\n CropperSelection,\n CropperShade,\n]) {\n element.$define()\n}\n\n/**\n * Settings the one caller never varies, kept as constants rather than as props nobody passes.\n * Each of them is behaviour, not preference: no checkerboard behind a photograph, the wheel left to\n * the page, pinch zoom kept for tablets, and the minimum canvas cropper.js' own stylesheet declares.\n */\nconst AUTO_CROP_AREA = 0.8\nconst MIN_CONTAINER_WIDTH = 200\nconst MIN_CONTAINER_HEIGHT = 100\n\n// Matched to cropper.js v1's stylesheet so the two components are indistinguishable side by side.\nconst SELECTION_COLOR = 'rgba(51, 153, 255, 0.75)'\nconst FACE_COLOR = 'rgba(255, 255, 255, 0.1)'\nconst GRID_COLOR = 'rgba(238, 238, 238, 0.5)'\nconst CROSSHAIR_COLOR = 'rgba(238, 238, 238, 0.75)'\nconst RESIZE_ACTIONS = [\n 'n-resize',\n 'e-resize',\n 's-resize',\n 'w-resize',\n 'ne-resize',\n 'nw-resize',\n 'se-resize',\n 'sw-resize',\n]\n\ninterface CropperParts {\n canvas: CropperCanvas\n image: CropperImage\n selection: CropperSelection\n}\n\nconst hostEl = useTemplateRef<HTMLDivElement>('hostEl')\nconst loading = ref(true)\n\nlet parts: CropperParts | null = null\nlet natural: Size = { width: 0, height: 0 }\n/** The action cropper.js is currently performing, so a clamp knows which edge to hold still. */\nlet currentAction: string | null = null\n/** Guards the re-entrant `$change` a clamp performs from being clamped again. */\nlet clamping = false\n/** Guards the half-applied transforms `$center` emits between its move and its scale. */\nlet centring = false\n/** Invalidates an in-flight build when `src` changes or the component goes away mid-load. */\nlet buildToken = 0\n/** The host box the current layout was computed for — not merely the last one observed. */\nlet laidOut: Size = { width: 0, height: 0 }\n/** The last crop handed out, so a model written straight back is not mistaken for a new one. */\nlet lastEmitted: CropRect | null = null\n/** Watches the ancestors that clip the host; their height is part of the room it has. */\nlet ancestorObserver: ResizeObserver | null = null\n\nconst create = <T extends HTMLElement>(name: string): T => document.createElement(name) as T\n\n/** The image box in canvas coordinates. Read from layout so panning and zooming stay accounted for. */\nconst imageBounds = (): Rect => {\n if (!parts) return { x: 0, y: 0, width: 0, height: 0 }\n const canvasRect = parts.canvas.getBoundingClientRect()\n const imageRect = parts.image.getBoundingClientRect()\n return {\n x: imageRect.left - canvasRect.left,\n y: imageRect.top - canvasRect.top,\n width: imageRect.width,\n height: imageRect.height,\n }\n}\n\n/**\n * Where the crop is allowed to be: the image, but never outside the visible canvas. The two are the\n * same box until the image is zoomed past its frame, and then it is the frame that wins — cropper.js\n * v1 limited its crop box to the canvas and the container both, so a zoomed-in image never let the\n * crop wander off the part of it you could actually see.\n */\nconst constraintBounds = (): Rect => {\n if (!parts) return { x: 0, y: 0, width: 0, height: 0 }\n const image = imageBounds()\n const canvas = parts.canvas.getBoundingClientRect()\n const left = Math.max(image.x, 0)\n const top = Math.max(image.y, 0)\n const right = Math.min(image.x + image.width, canvas.width)\n const bottom = Math.min(image.y + image.height, canvas.height)\n return { x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) }\n}\n\nconst selectionRect = (): Rect => {\n if (!parts) return { x: 0, y: 0, width: 0, height: 0 }\n const { x, y, width, height } = parts.selection\n return { x, y, width, height }\n}\n\nconst applyRect = (rect: Rect) => {\n if (!parts) return\n clamping = true\n parts.selection.$change(rect.x, rect.y, rect.width, rect.height, props.aspectRatio, true)\n clamping = false\n}\n\n/**\n * Re-fits the image to the canvas. cropper.js only lets `$center` scale and move an image that is\n * marked scalable and translatable — it grants itself that permission while the image is still\n * loading, but not afterwards — so the flags are lifted for the call and put back straight after.\n */\nconst centerImage = () => {\n if (!parts) return\n const { image } = parts\n const { scalable, translatable } = image\n centring = true\n image.scalable = true\n image.translatable = true\n image.$center('contain')\n image.scalable = scalable\n image.translatable = translatable\n centring = false\n}\n\n/**\n * The crop a fresh picture starts with. Computed here rather than handed to cropper.js'\n * `initialCoverage`, whose deferred initialisation would land *after* a `ready` handler had already\n * positioned the crop. It is measured against the image rather than the canvas, which are the same\n * box for a source wider than its container and are not once the image sits centred between margins.\n */\nconst seedInitialCrop = () => {\n if (!parts) return\n const bounds = imageBounds()\n if (bounds.width <= 0 || bounds.height <= 0) return\n const initial = initialSelectionRect(bounds, props.aspectRatio, AUTO_CROP_AREA)\n applyRect({ ...initial, x: initial.x + bounds.x, y: initial.y + bounds.y })\n}\n\n/** Uses the bound crop if there is one, and the default box if there is not. */\nconst applyModelOrSeed = () => {\n const model = props.modelValue\n if (model && model.width > 0 && model.height > 0) applyCrop(model)\n else seedInitialCrop()\n emitCrop()\n}\n\n/** Pulls the selection back inside the image after something moved the image out from under it. */\nconst refit = () => {\n if (!parts || centring) return\n const current = selectionRect()\n if (current.width <= 0 || current.height <= 0) return\n const fitted = clampRectInside(current, constraintBounds(), props.aspectRatio)\n if (!rectsAreClose(current, fitted)) applyRect(fitted)\n}\n\nconst onSelectionChange = (event: Event) => {\n if (clamping || !parts) return\n const detail = (event as CustomEvent<Rect>).detail\n if (!detail) return\n // cropper.js reverses the action when a grip is dragged through the opposite edge, and it records\n // that on the canvas just before emitting this event — one move ahead of the `action` event the\n // tracking below sees. Prefer its own answer, and fall back if a future version stops publishing it.\n const liveAction = (parts.canvas as unknown as { $action?: unknown }).$action\n const action = typeof liveAction === 'string' && liveAction ? liveAction : currentAction\n const anchor = action ? RESIZE_ANCHORS[action] : undefined\n const clamped = clampRectInside(detail, constraintBounds(), props.aspectRatio, anchor)\n if (rectsAreClose(detail, clamped)) return\n // Rejecting the change and re-issuing the clamped one is what keeps the crop sliding along the\n // border instead of freezing there; the shade reads `defaultPrevented` and follows the new values.\n event.preventDefault()\n applyRect(clamped)\n}\n\nconst onActionStart = (event: Event) => {\n currentAction = (event as CustomEvent<{ action?: string }>).detail?.action ?? null\n}\n\nconst onAction = (event: Event) => {\n const action = (event as CustomEvent<{ action?: string }>).detail?.action\n // cropper.js turns a fresh \"select\" drag into the matching corner resize once it knows the\n // direction, and reports the new action on the next move — follow it so the anchor stays right.\n if (action) currentAction = action\n}\n\nconst onActionEnd = () => {\n currentAction = null\n emitCrop(true)\n}\n\n/**\n * The canvas must never see a wheel event: its own handler calls `preventDefault()` unconditionally,\n * which would swallow page scrolling over the picture the way cropper.js v1 never did. Pinching is\n * unaffected — that arrives as pointer events, and the picture stays scalable for it.\n */\nconst onWheelCapture = (event: WheelEvent) => {\n event.stopPropagation()\n}\n\n/**\n * cropper.js emits `transform` *before* it writes the new matrix to the element's style (it sets the\n * new transform to measure the result, puts the old one back, then emits and finally commits). Re-\n * fitting straight away would therefore measure the image as it was: after a zoom out the crop would\n * be judged against the larger, previous picture and left hanging over the edge of the new one.\n */\nconst onImageTransform = () => queueMicrotask(refit)\n\nconst teardown = () => {\n if (parts) {\n const { canvas, image, selection } = parts\n selection.removeEventListener('change', onSelectionChange)\n canvas.removeEventListener('actionstart', onActionStart)\n canvas.removeEventListener('action', onAction)\n canvas.removeEventListener('actionend', onActionEnd)\n image.removeEventListener('transform', onImageTransform)\n }\n // Every canvas, not just the one on `parts`: a build that was superseded while still waiting on\n // its image has already put one in the DOM and has nothing tracking it yet.\n hostEl.value?.querySelectorAll(CropperCanvas.$name).forEach((canvas) => {\n // Disconnecting a `cropper-image` drops its listeners but leaves its request running, and the\n // `$ready()` promise waiting on it keeps this component reachable until the server answers.\n canvas.querySelectorAll<CropperImage>(CropperImage.$name).forEach((image) => {\n if (image.$image) image.$image.src = ''\n })\n canvas.remove()\n })\n ancestorObserver?.disconnect()\n ancestorObserver = null\n parts = null\n natural = { width: 0, height: 0 }\n laidOut = { width: 0, height: 0 }\n currentAction = null\n}\n\n/**\n * How much of the host's height will actually be shown.\n *\n * The host's own `max-height` is the usual answer, and the one `DamAssetImageRoiSelect` relies on.\n * Ancestors count too when they clip with `overflow: hidden` — what they cut off cannot be scrolled\n * back into view, so it is no more reachable than the host's own overflow. An ancestor that\n * scrolls is left alone: there the rest of the picture is a scroll away, not gone.\n */\nconst clippingAncestorsOf = (host: HTMLElement): HTMLElement[] => {\n const found: HTMLElement[] = []\n let ancestor = host.parentElement\n while (ancestor && ancestor !== document.body && ancestor !== document.documentElement) {\n const { overflowY } = window.getComputedStyle(ancestor)\n if (overflowY === 'hidden' || overflowY === 'clip') found.push(ancestor)\n ancestor = ancestor.parentElement\n }\n return found\n}\n\nconst visibleHeightOf = (host: HTMLElement): number => {\n let available = host.clientHeight\n const hostTop = host.getBoundingClientRect().top\n\n clippingAncestorsOf(host).forEach((ancestor) => {\n const rect = ancestor.getBoundingClientRect()\n // From wherever the host starts being visible down to where the ancestor stops showing anything.\n // Taking the ancestor's own top into account matters when the host begins above it: what is\n // scrolled or positioned off the top is just as unreachable as what falls off the bottom.\n const room = rect.bottom - Math.max(rect.top, hostTop)\n if (room > 0) available = Math.min(available, room)\n })\n\n return available\n}\n\n/**\n * Sizes the canvas to the room the host actually gives it, in two passes because the second answer\n * depends on the first: the height a picture wants follows from its width, but a frame with a\n * `max-height` only reveals how much of that it will show once something is in it.\n *\n * cropper.js v1 did the same dance with its `<img>`, and the point of it is that its cropper never\n * overflowed: a tall picture in a short frame was scaled down to fit rather than cut off at the\n * bottom, where the part of the crop hidden by `overflow: hidden` could not be reached again.\n */\nconst sizeCanvas = (canvas: CropperCanvas, host: HTMLElement, availableWidth: number): Size => {\n const size = canvasSizeFor(natural, availableWidth, MIN_CONTAINER_WIDTH, MIN_CONTAINER_HEIGHT)\n canvas.style.width = `${size.width}px`\n canvas.style.height = `${size.height}px`\n\n const visibleHeight = visibleHeightOf(host)\n if (visibleHeight > 0 && visibleHeight < size.height) {\n size.height = Math.max(visibleHeight, MIN_CONTAINER_HEIGHT)\n canvas.style.height = `${size.height}px`\n }\n return size\n}\n\nconst buildSelectionChildren = (selection: CropperSelection) => {\n const grid = create<CropperGrid>(CropperGrid.$name)\n grid.rows = 3\n grid.columns = 3\n grid.covered = true\n grid.themeColor = GRID_COLOR\n grid.setAttribute('role', 'grid')\n selection.appendChild(grid)\n\n const crosshair = create<CropperCrosshair>(CropperCrosshair.$name)\n crosshair.centered = true\n crosshair.themeColor = CROSSHAIR_COLOR\n selection.appendChild(crosshair)\n\n const face = create<CropperHandle>(CropperHandle.$name)\n face.action = 'move'\n face.themeColor = FACE_COLOR\n selection.appendChild(face)\n\n RESIZE_ACTIONS.forEach((action) => {\n const handle = create<CropperHandle>(CropperHandle.$name)\n handle.action = action\n handle.themeColor = SELECTION_COLOR\n selection.appendChild(handle)\n })\n}\n\nconst build = async () => {\n const token = ++buildToken\n teardown()\n loading.value = true\n\n const host = hostEl.value\n if (!host || !props.src) {\n // Nothing to wait for: leaving the spinner up would make an empty cropper look like a stuck one.\n loading.value = false\n return\n }\n\n const canvas = create<CropperCanvas>(CropperCanvas.$name)\n canvas.background = false\n canvas.themeColor = SELECTION_COLOR\n canvas.style.display = 'block'\n sizeCanvas(canvas, host, host.clientWidth)\n\n const image = create<CropperImage>(CropperImage.$name)\n image.alt = ''\n image.initialFit = 'contain'\n image.rotatable = false\n image.skewable = false\n image.translatable = false\n image.scalable = true\n image.src = props.src\n\n const shade = create<CropperShade>(CropperShade.$name)\n shade.themeColor = props.shadeColor\n // Kept out of the way until the first crop box exists, or it would flash across the whole canvas.\n shade.hidden = true\n\n const selectHandle = create<CropperHandle>(CropperHandle.$name)\n selectHandle.action = 'select'\n selectHandle.plain = true\n\n const selection = create<CropperSelection>(CropperSelection.$name)\n selection.themeColor = SELECTION_COLOR\n selection.movable = true\n selection.resizable = true\n selection.outlined = true\n // Keep sub-pixel values: rounding to whole canvas pixels would drift the natural-pixel crop data\n // that callers persist, and cropper.js v1 did not round either.\n selection.precise = true\n selection.aspectRatio = props.aspectRatio\n buildSelectionChildren(selection)\n\n // Listeners go on before anything is connected, so the action in flight is already known by the\n // time the elements' own handlers react to it.\n canvas.addEventListener('actionstart', onActionStart)\n canvas.addEventListener('action', onAction)\n canvas.addEventListener('actionend', onActionEnd)\n image.addEventListener('transform', onImageTransform)\n selection.addEventListener('change', onSelectionChange)\n\n canvas.append(image, shade, selectHandle, selection)\n // The whole tree is attached in one move on purpose: each element wires itself up to its siblings\n // when it connects, and the shade in particular goes looking for the selection. Appending the\n // children one at a time leaves it looking at an empty canvas, and it never follows the crop.\n host.appendChild(canvas)\n // Second pass, now that there is something in the host for its own constraints to act on.\n sizeCanvas(canvas, host, host.clientWidth)\n\n let loaded: HTMLImageElement\n try {\n // The element runs the one request there is; its promise carries the picture that arrived.\n loaded = await image.$ready()\n } catch {\n // Mirrors cropper.js v1, which simply never became ready on a broken source.\n canvas.remove()\n if (token === buildToken) loading.value = false\n return\n }\n if (token !== buildToken) {\n canvas.remove()\n return\n }\n\n parts = { canvas, image, selection }\n // Only now is the picture's own size known, so the canvas gets its real dimensions here rather\n // than from a second request made ahead of time purely to measure it.\n natural = { width: loaded.naturalWidth, height: loaded.naturalHeight }\n sizeCanvas(canvas, host, host.clientWidth)\n\n // A prop that changed while the picture was loading found the watchers with no elements to write\n // to. Re-reading the two that can vary closes that window; it is idempotent otherwise.\n shade.themeColor = props.shadeColor\n selection.aspectRatio = props.aspectRatio\n\n centerImage()\n\n laidOut = hostBox(host)\n observeAncestors(host)\n applyModelOrSeed()\n\n loading.value = false\n emit('ready')\n}\n\n/** Where the crop sits, as fractions of the image. */\nconst currentCrop = (): CropRect | null => {\n if (!parts) return null\n const bounds = imageBounds()\n if (bounds.width <= 0 || bounds.height <= 0) return null\n const rect = selectionRect()\n return {\n x: (rect.x - bounds.x) / bounds.width,\n y: (rect.y - bounds.y) / bounds.height,\n width: rect.width / bounds.width,\n height: rect.height / bounds.height,\n }\n}\n\n/** Puts a crop given in fractions onto the canvas, clamped to the picture. */\nconst applyCrop = (crop: CropRect) => {\n const bounds = imageBounds()\n if (!parts || bounds.width <= 0 || bounds.height <= 0) return\n applyRect(\n clampRectInside(\n {\n x: bounds.x + crop.x * bounds.width,\n y: bounds.y + crop.y * bounds.height,\n width: crop.width * bounds.width,\n height: crop.height * bounds.height,\n },\n constraintBounds(),\n props.aspectRatio\n )\n )\n}\n\nconst cropsAreClose = (a: CropRect | null, b: CropRect | null): boolean =>\n a !== null &&\n b !== null &&\n Math.abs(a.x - b.x) < 0.0005 &&\n Math.abs(a.y - b.y) < 0.0005 &&\n Math.abs(a.width - b.width) < 0.0005 &&\n Math.abs(a.height - b.height) < 0.0005\n\n/**\n * Hands the crop back up. Also called after the component itself has adjusted one — a stored region\n * rarely matches the configured ratio, and the binding should say where the crop really is rather\n * than where it was asked to be.\n */\nconst emitCrop = (alsoCommit = false) => {\n const crop = currentCrop()\n if (!crop) return\n if (alsoCommit || !cropsAreClose(crop, lastEmitted)) {\n lastEmitted = crop\n emit('update:modelValue', crop)\n }\n if (alsoCommit) emit('commit', crop)\n}\n\n/** The room the host has: its width, and the height that will actually be shown. */\nconst hostBox = (host: HTMLElement): Size => ({ width: host.clientWidth, height: visibleHeightOf(host) })\n\n/** Re-fit the image to the new container while keeping the crop the user already chose. */\nconst relayout = () => {\n const host = hostEl.value\n if (!parts || !host) return\n // A crop of no size means the last layout had none either — fall back to the bound one, which is\n // what a cropper built inside a closed panel has been holding all along.\n const wanted = currentCrop() ?? props.modelValue\n\n const size = sizeCanvas(parts.canvas, host, host.clientWidth)\n laidOut = hostBox(host)\n // Re-read the ancestors as well: a cropper can be moved between frames, and the one clipping it\n // now is the one whose height matters.\n observeAncestors(host)\n centerImage()\n if (size.width <= 0 || size.height <= 0) return\n\n if (wanted && wanted.width > 0) applyCrop(wanted)\n else applyModelOrSeed()\n emitCrop()\n}\n\nconst onHostBoxChanged = () => {\n const host = hostEl.value\n if (!host) return\n const box = hostBox(host)\n if (box.width <= 0 || box.height <= 0) return\n // Compared against the box the current layout was built for, so a size seen while the cropper was\n // still loading cannot be mistaken for one that has already been laid out.\n if (box.width === laidOut.width && box.height === laidOut.height) return\n if (!parts) return\n // Deferred a frame: resizing the canvas from inside the observer's own callback is what makes\n // Chromium report an undelivered-notification loop.\n requestAnimationFrame(() => relayout())\n}\n\n// Height counts as much as width: the DAM sizes the cropper with `calc(100vh - 160px)`, so a\n// shorter window shrinks the frame without touching its width.\nuseResizeObserver(hostEl, onHostBoxChanged)\n\n/** An ancestor that clips can change how much it shows without the host's own box moving at all. */\nconst observeAncestors = (host: HTMLElement) => {\n ancestorObserver?.disconnect()\n const ancestors = clippingAncestorsOf(host)\n if (ancestors.length === 0) {\n ancestorObserver = null\n return\n }\n ancestorObserver = new ResizeObserver(onHostBoxChanged)\n ancestors.forEach((ancestor) => ancestorObserver?.observe(ancestor))\n}\n\nwatch(\n () => props.src,\n () => {\n build()\n }\n)\n\nwatch(\n () => props.modelValue,\n (model) => {\n // Ignore the echo of what this component just handed up, or the two would write to each other\n // for as long as the parent keeps binding it back.\n if (!parts || !model || cropsAreClose(model, lastEmitted)) return\n applyCrop(model)\n emitCrop()\n },\n { deep: true }\n)\n\nwatch(\n () => props.shadeColor,\n (shadeColor) => {\n const shade = parts?.canvas.querySelector<CropperShade>(CropperShade.$name)\n if (shade) shade.themeColor = shadeColor\n }\n)\n\nwatch(\n () => props.aspectRatio,\n (aspectRatio) => {\n if (!parts) return\n parts.selection.aspectRatio = aspectRatio\n if (!isUsableAspectRatio(aspectRatio)) return\n const current = selectionRect()\n applyRect(clampRectInside({ ...current, height: current.width / aspectRatio }, constraintBounds(), aspectRatio))\n }\n)\n\nonMounted(() => {\n build()\n})\n\nonBeforeUnmount(() => {\n buildToken += 1\n teardown()\n})\n</script>\n\n<template>\n <div class=\"a-cropper d-flex flex-column\">\n <div\n v-if=\"loading\"\n class=\"d-flex w-100 align-center justify-center\"\n >\n <VProgressCircular\n class=\"position-absolute\"\n indeterminate\n />\n </div>\n\n <div\n ref=\"hostEl\"\n class=\"a-cropper__container\"\n :style=\"containerStyle\"\n @wheel.capture=\"onWheelCapture\"\n />\n </div>\n</template>\n\n<style lang=\"scss\">\n// `cropper-crosshair` sizes itself in `em`, so without a size of its own the centre marker would\n// grow and shrink with whatever font the surrounding admin happens to use. 7px is the plus sign\n// cropper.js v1 drew through `.cropper-center`.\n.a-cropper cropper-crosshair {\n font-size: 7px;\n}\n\n// cropper.js v1 drew a `.cropper-line` along each edge of the crop box: a 5px band of the selection\n// colour at a tenth opacity, sitting 3px outside the border and 2px inside it. It is what made the\n// thin outline read as a grabbable edge, so it is drawn here too — as a pair of shadows rather than\n// four elements, which keeps it independent of how the v2 grips happen to be sized.\n.a-cropper cropper-selection[outlined] {\n box-shadow:\n inset 0 0 0 2px rgb(51 153 255 / 10%),\n 0 0 0 3px rgb(51 153 255 / 10%);\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"ACropper.js","names":["_sfc_main","ACropper_vue_vue_type_script_setup_true_lang_default","ACropper_default"],"sources":["../../../../../src/domains/dam/cropper/components/ACropper.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'\nimport { useResizeObserver } from '@vueuse/core'\nimport {\n CropperCanvas,\n CropperCrosshair,\n CropperGrid,\n CropperHandle,\n CropperImage,\n CropperSelection,\n CropperShade,\n} from 'cropperjs'\nimport {\n canvasSizeFor,\n clampRectInside,\n initialSelectionRect,\n isUsableAspectRatio,\n RESIZE_ANCHORS,\n rectsAreClose,\n type Rect,\n type Size,\n} from '@/domains/dam/cropper/utils/cropperGeometry'\nimport type { CropRect } from '@/domains/dam/cropper/utils/cropperTypes'\n\n/**\n * Image cropper built on cropper.js v2's custom elements, replacing the v1-based `ACropperjs`.\n *\n * It lives here, beside `DamAssetImageRoiSelect`, because that is the only thing that uses it and\n * nothing outside this library does. It is not exported: shaping it to one need rather than to a\n * general audience is what keeps it to its size.\n *\n * The crop is bound, not fetched: `v-model` carries it as fractions of the image, and `commit` says\n * the user has finished a gesture. There is no imperative surface — the one thing a host wants to do\n * with a cropper is know where the crop is, and a binding says that better than six methods did.\n *\n * The elements are assembled in script rather than in the template on purpose: a consumer of this\n * library does not configure `isCustomElement` in their own build, and setting element *properties*\n * instead of going through attribute strings avoids the coercion surprises (`NaN`, booleans,\n * fractional numbers) that come with the attribute round-trip.\n */\n\nconst props = withDefaults(\n defineProps<{\n src?: string\n /** The crop, as fractions of the image. Leave it null to start from the default box. */\n modelValue?: CropRect | null\n /** `NaN` (the default) leaves the crop free-form. */\n aspectRatio?: number\n containerStyle?: { [key: string]: string } | undefined\n /** Wash over everything outside the crop — the replacement for overriding `.cropper-modal`. */\n shadeColor?: string\n }>(),\n {\n src: '',\n modelValue: null,\n aspectRatio: NaN,\n containerStyle: undefined,\n shadeColor: 'rgba(0, 0, 0, 0.5)',\n }\n)\n\nconst emit = defineEmits<{\n (event: 'update:modelValue', crop: CropRect): void\n /** The user has finished a gesture; the crop is worth saving. */\n (event: 'commit', crop: CropRect): void\n (event: 'ready'): void\n}>()\n\n// cropper.js registers its elements as a side effect of loading its entry, and this package's\n// `sideEffects` lets the consumer's bundler drop that module once only the classes are used. `$define`\n// is a no-op for an element that is already registered.\nfor (const element of [\n CropperCanvas,\n CropperCrosshair,\n CropperGrid,\n CropperHandle,\n CropperImage,\n CropperSelection,\n CropperShade,\n]) {\n element.$define()\n}\n\n/**\n * Settings the one caller never varies, kept as constants rather than as props nobody passes.\n * Each of them is behaviour, not preference: no checkerboard behind a photograph, the wheel left to\n * the page, pinch zoom kept for tablets, and the minimum canvas cropper.js' own stylesheet declares.\n */\nconst AUTO_CROP_AREA = 0.8\nconst MIN_CONTAINER_WIDTH = 200\nconst MIN_CONTAINER_HEIGHT = 100\n\n// Matched to cropper.js v1's stylesheet so the two components are indistinguishable side by side.\nconst SELECTION_COLOR = 'rgba(51, 153, 255, 0.75)'\nconst FACE_COLOR = 'rgba(255, 255, 255, 0.1)'\nconst GRID_COLOR = 'rgba(238, 238, 238, 0.5)'\nconst CROSSHAIR_COLOR = 'rgba(238, 238, 238, 0.75)'\nconst RESIZE_ACTIONS = [\n 'n-resize',\n 'e-resize',\n 's-resize',\n 'w-resize',\n 'ne-resize',\n 'nw-resize',\n 'se-resize',\n 'sw-resize',\n]\n\ninterface CropperParts {\n canvas: CropperCanvas\n image: CropperImage\n selection: CropperSelection\n}\n\nconst hostEl = useTemplateRef<HTMLDivElement>('hostEl')\nconst loading = ref(true)\n\nlet parts: CropperParts | null = null\nlet natural: Size = { width: 0, height: 0 }\n/** The action cropper.js is currently performing, so a clamp knows which edge to hold still. */\nlet currentAction: string | null = null\n/** Guards the re-entrant `$change` a clamp performs from being clamped again. */\nlet clamping = false\n/** Guards the half-applied transforms `$center` emits between its move and its scale. */\nlet centring = false\n/** Invalidates an in-flight build when `src` changes or the component goes away mid-load. */\nlet buildToken = 0\n/** The host box the current layout was computed for — not merely the last one observed. */\nlet laidOut: Size = { width: 0, height: 0 }\n/** The last crop handed out, so a model written straight back is not mistaken for a new one. */\nlet lastEmitted: CropRect | null = null\n/** Watches the ancestors that clip the host; their height is part of the room it has. */\nlet ancestorObserver: ResizeObserver | null = null\n\nconst create = <T extends HTMLElement>(name: string): T => document.createElement(name) as T\n\n/** The image box in canvas coordinates. Read from layout so panning and zooming stay accounted for. */\nconst imageBounds = (): Rect => {\n if (!parts) return { x: 0, y: 0, width: 0, height: 0 }\n const canvasRect = parts.canvas.getBoundingClientRect()\n const imageRect = parts.image.getBoundingClientRect()\n return {\n x: imageRect.left - canvasRect.left,\n y: imageRect.top - canvasRect.top,\n width: imageRect.width,\n height: imageRect.height,\n }\n}\n\n/**\n * Where the crop is allowed to be: the image, but never outside the visible canvas. The two are the\n * same box until the image is zoomed past its frame, and then it is the frame that wins — cropper.js\n * v1 limited its crop box to the canvas and the container both, so a zoomed-in image never let the\n * crop wander off the part of it you could actually see.\n */\nconst constraintBounds = (): Rect => {\n if (!parts) return { x: 0, y: 0, width: 0, height: 0 }\n const image = imageBounds()\n const canvas = parts.canvas.getBoundingClientRect()\n const left = Math.max(image.x, 0)\n const top = Math.max(image.y, 0)\n const right = Math.min(image.x + image.width, canvas.width)\n const bottom = Math.min(image.y + image.height, canvas.height)\n return { x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) }\n}\n\nconst selectionRect = (): Rect => {\n if (!parts) return { x: 0, y: 0, width: 0, height: 0 }\n const { x, y, width, height } = parts.selection\n return { x, y, width, height }\n}\n\nconst applyRect = (rect: Rect) => {\n if (!parts) return\n clamping = true\n parts.selection.$change(rect.x, rect.y, rect.width, rect.height, props.aspectRatio, true)\n clamping = false\n}\n\n/**\n * Re-fits the image to the canvas. cropper.js only lets `$center` scale and move an image that is\n * marked scalable and translatable — it grants itself that permission while the image is still\n * loading, but not afterwards — so the flags are lifted for the call and put back straight after.\n */\nconst centerImage = () => {\n if (!parts) return\n const { image } = parts\n const { scalable, translatable } = image\n centring = true\n image.scalable = true\n image.translatable = true\n image.$center('contain')\n image.scalable = scalable\n image.translatable = translatable\n centring = false\n}\n\n/**\n * The crop a fresh picture starts with. Computed here rather than handed to cropper.js'\n * `initialCoverage`, whose deferred initialisation would land *after* a `ready` handler had already\n * positioned the crop. It is measured against the image rather than the canvas, which are the same\n * box for a source wider than its container and are not once the image sits centred between margins.\n */\nconst seedInitialCrop = () => {\n if (!parts) return\n const bounds = imageBounds()\n if (bounds.width <= 0 || bounds.height <= 0) return\n const initial = initialSelectionRect(bounds, props.aspectRatio, AUTO_CROP_AREA)\n applyRect({ ...initial, x: initial.x + bounds.x, y: initial.y + bounds.y })\n}\n\n/** Uses the bound crop if there is one, and the default box if there is not. */\nconst applyModelOrSeed = () => {\n const model = props.modelValue\n if (model && model.width > 0 && model.height > 0) applyCrop(model)\n else seedInitialCrop()\n emitCrop()\n}\n\n/** Pulls the selection back inside the image after something moved the image out from under it. */\nconst refit = () => {\n if (!parts || centring) return\n const current = selectionRect()\n if (current.width <= 0 || current.height <= 0) return\n const fitted = clampRectInside(current, constraintBounds(), props.aspectRatio)\n if (!rectsAreClose(current, fitted)) applyRect(fitted)\n}\n\nconst onSelectionChange = (event: Event) => {\n if (clamping || !parts) return\n const detail = (event as CustomEvent<Rect>).detail\n if (!detail) return\n // cropper.js reverses the action when a grip is dragged through the opposite edge, and it records\n // that on the canvas just before emitting this event — one move ahead of the `action` event the\n // tracking below sees. Prefer its own answer, and fall back if a future version stops publishing it.\n const liveAction = (parts.canvas as unknown as { $action?: unknown }).$action\n const action = typeof liveAction === 'string' && liveAction ? liveAction : currentAction\n const anchor = action ? RESIZE_ANCHORS[action] : undefined\n const clamped = clampRectInside(detail, constraintBounds(), props.aspectRatio, anchor)\n if (rectsAreClose(detail, clamped)) return\n // Rejecting the change and re-issuing the clamped one is what keeps the crop sliding along the\n // border instead of freezing there; the shade reads `defaultPrevented` and follows the new values.\n event.preventDefault()\n applyRect(clamped)\n}\n\nconst onActionStart = (event: Event) => {\n currentAction = (event as CustomEvent<{ action?: string }>).detail?.action ?? null\n}\n\nconst onAction = (event: Event) => {\n const action = (event as CustomEvent<{ action?: string }>).detail?.action\n // cropper.js turns a fresh \"select\" drag into the matching corner resize once it knows the\n // direction, and reports the new action on the next move — follow it so the anchor stays right.\n if (action) currentAction = action\n}\n\nconst onActionEnd = () => {\n currentAction = null\n emitCrop(true)\n}\n\n/**\n * The canvas must never see a wheel event: its own handler calls `preventDefault()` unconditionally,\n * which would swallow page scrolling over the picture the way cropper.js v1 never did. Pinching is\n * unaffected — that arrives as pointer events, and the picture stays scalable for it.\n */\nconst onWheelCapture = (event: WheelEvent) => {\n event.stopPropagation()\n}\n\n/**\n * cropper.js emits `transform` *before* it writes the new matrix to the element's style (it sets the\n * new transform to measure the result, puts the old one back, then emits and finally commits). Re-\n * fitting straight away would therefore measure the image as it was: after a zoom out the crop would\n * be judged against the larger, previous picture and left hanging over the edge of the new one.\n */\nconst onImageTransform = () => queueMicrotask(refit)\n\nconst teardown = () => {\n if (parts) {\n const { canvas, image, selection } = parts\n selection.removeEventListener('change', onSelectionChange)\n canvas.removeEventListener('actionstart', onActionStart)\n canvas.removeEventListener('action', onAction)\n canvas.removeEventListener('actionend', onActionEnd)\n image.removeEventListener('transform', onImageTransform)\n }\n // Every canvas, not just the one on `parts`: a build that was superseded while still waiting on\n // its image has already put one in the DOM and has nothing tracking it yet.\n hostEl.value?.querySelectorAll(CropperCanvas.$name).forEach((canvas) => {\n // Disconnecting a `cropper-image` drops its listeners but leaves its request running, and the\n // `$ready()` promise waiting on it keeps this component reachable until the server answers.\n canvas.querySelectorAll<CropperImage>(CropperImage.$name).forEach((image) => {\n if (image.$image) image.$image.src = ''\n })\n canvas.remove()\n })\n ancestorObserver?.disconnect()\n ancestorObserver = null\n parts = null\n natural = { width: 0, height: 0 }\n laidOut = { width: 0, height: 0 }\n currentAction = null\n}\n\n/**\n * How much of the host's height will actually be shown.\n *\n * The host's own `max-height` is the usual answer, and the one `DamAssetImageRoiSelect` relies on.\n * Ancestors count too when they clip with `overflow: hidden` — what they cut off cannot be scrolled\n * back into view, so it is no more reachable than the host's own overflow. An ancestor that\n * scrolls is left alone: there the rest of the picture is a scroll away, not gone.\n */\nconst clippingAncestorsOf = (host: HTMLElement): HTMLElement[] => {\n const found: HTMLElement[] = []\n let ancestor = host.parentElement\n while (ancestor && ancestor !== document.body && ancestor !== document.documentElement) {\n const { overflowY } = window.getComputedStyle(ancestor)\n if (overflowY === 'hidden' || overflowY === 'clip') found.push(ancestor)\n ancestor = ancestor.parentElement\n }\n return found\n}\n\nconst visibleHeightOf = (host: HTMLElement): number => {\n let available = host.clientHeight\n const hostTop = host.getBoundingClientRect().top\n\n clippingAncestorsOf(host).forEach((ancestor) => {\n const rect = ancestor.getBoundingClientRect()\n // From wherever the host starts being visible down to where the ancestor stops showing anything.\n // Taking the ancestor's own top into account matters when the host begins above it: what is\n // scrolled or positioned off the top is just as unreachable as what falls off the bottom.\n const room = rect.bottom - Math.max(rect.top, hostTop)\n if (room > 0) available = Math.min(available, room)\n })\n\n return available\n}\n\n/**\n * Sizes the canvas to the room the host actually gives it, in two passes because the second answer\n * depends on the first: the height a picture wants follows from its width, but a frame with a\n * `max-height` only reveals how much of that it will show once something is in it.\n *\n * cropper.js v1 did the same dance with its `<img>`, and the point of it is that its cropper never\n * overflowed: a tall picture in a short frame was scaled down to fit rather than cut off at the\n * bottom, where the part of the crop hidden by `overflow: hidden` could not be reached again.\n */\nconst sizeCanvas = (canvas: CropperCanvas, host: HTMLElement, availableWidth: number): Size => {\n const size = canvasSizeFor(natural, availableWidth, MIN_CONTAINER_WIDTH, MIN_CONTAINER_HEIGHT)\n canvas.style.width = `${size.width}px`\n canvas.style.height = `${size.height}px`\n\n const visibleHeight = visibleHeightOf(host)\n if (visibleHeight > 0 && visibleHeight < size.height) {\n size.height = Math.max(visibleHeight, MIN_CONTAINER_HEIGHT)\n canvas.style.height = `${size.height}px`\n }\n return size\n}\n\nconst buildSelectionChildren = (selection: CropperSelection) => {\n const grid = create<CropperGrid>(CropperGrid.$name)\n grid.rows = 3\n grid.columns = 3\n grid.covered = true\n grid.themeColor = GRID_COLOR\n grid.setAttribute('role', 'grid')\n selection.appendChild(grid)\n\n const crosshair = create<CropperCrosshair>(CropperCrosshair.$name)\n crosshair.centered = true\n crosshair.themeColor = CROSSHAIR_COLOR\n selection.appendChild(crosshair)\n\n const face = create<CropperHandle>(CropperHandle.$name)\n face.action = 'move'\n face.themeColor = FACE_COLOR\n selection.appendChild(face)\n\n RESIZE_ACTIONS.forEach((action) => {\n const handle = create<CropperHandle>(CropperHandle.$name)\n handle.action = action\n handle.themeColor = SELECTION_COLOR\n selection.appendChild(handle)\n })\n}\n\nconst build = async () => {\n const token = ++buildToken\n teardown()\n loading.value = true\n\n const host = hostEl.value\n if (!host || !props.src) {\n // Nothing to wait for: leaving the spinner up would make an empty cropper look like a stuck one.\n loading.value = false\n return\n }\n\n const canvas = create<CropperCanvas>(CropperCanvas.$name)\n canvas.background = false\n canvas.themeColor = SELECTION_COLOR\n canvas.style.display = 'block'\n sizeCanvas(canvas, host, host.clientWidth)\n\n const image = create<CropperImage>(CropperImage.$name)\n image.alt = ''\n image.initialFit = 'contain'\n image.rotatable = false\n image.skewable = false\n image.translatable = false\n image.scalable = true\n image.src = props.src\n\n const shade = create<CropperShade>(CropperShade.$name)\n shade.themeColor = props.shadeColor\n // Kept out of the way until the first crop box exists, or it would flash across the whole canvas.\n shade.hidden = true\n\n const selectHandle = create<CropperHandle>(CropperHandle.$name)\n selectHandle.action = 'select'\n selectHandle.plain = true\n\n const selection = create<CropperSelection>(CropperSelection.$name)\n selection.themeColor = SELECTION_COLOR\n selection.movable = true\n selection.resizable = true\n selection.outlined = true\n // Keep sub-pixel values: rounding to whole canvas pixels would drift the natural-pixel crop data\n // that callers persist, and cropper.js v1 did not round either.\n selection.precise = true\n selection.aspectRatio = props.aspectRatio\n buildSelectionChildren(selection)\n\n // Listeners go on before anything is connected, so the action in flight is already known by the\n // time the elements' own handlers react to it.\n canvas.addEventListener('actionstart', onActionStart)\n canvas.addEventListener('action', onAction)\n canvas.addEventListener('actionend', onActionEnd)\n image.addEventListener('transform', onImageTransform)\n selection.addEventListener('change', onSelectionChange)\n\n canvas.append(image, shade, selectHandle, selection)\n // The whole tree is attached in one move on purpose: each element wires itself up to its siblings\n // when it connects, and the shade in particular goes looking for the selection. Appending the\n // children one at a time leaves it looking at an empty canvas, and it never follows the crop.\n host.appendChild(canvas)\n // Second pass, now that there is something in the host for its own constraints to act on.\n sizeCanvas(canvas, host, host.clientWidth)\n\n let loaded: HTMLImageElement\n try {\n // The element runs the one request there is; its promise carries the picture that arrived.\n loaded = await image.$ready()\n } catch {\n // Mirrors cropper.js v1, which simply never became ready on a broken source.\n canvas.remove()\n if (token === buildToken) loading.value = false\n return\n }\n if (token !== buildToken) {\n canvas.remove()\n return\n }\n\n parts = { canvas, image, selection }\n // Only now is the picture's own size known, so the canvas gets its real dimensions here rather\n // than from a second request made ahead of time purely to measure it.\n natural = { width: loaded.naturalWidth, height: loaded.naturalHeight }\n sizeCanvas(canvas, host, host.clientWidth)\n\n // A prop that changed while the picture was loading found the watchers with no elements to write\n // to. Re-reading the two that can vary closes that window; it is idempotent otherwise.\n shade.themeColor = props.shadeColor\n selection.aspectRatio = props.aspectRatio\n\n centerImage()\n\n laidOut = hostBox(host)\n observeAncestors(host)\n applyModelOrSeed()\n\n loading.value = false\n emit('ready')\n}\n\n/** Where the crop sits, as fractions of the image. */\nconst currentCrop = (): CropRect | null => {\n if (!parts) return null\n const bounds = imageBounds()\n if (bounds.width <= 0 || bounds.height <= 0) return null\n const rect = selectionRect()\n return {\n x: (rect.x - bounds.x) / bounds.width,\n y: (rect.y - bounds.y) / bounds.height,\n width: rect.width / bounds.width,\n height: rect.height / bounds.height,\n }\n}\n\n/** Puts a crop given in fractions onto the canvas, clamped to the picture. */\nconst applyCrop = (crop: CropRect) => {\n const bounds = imageBounds()\n if (!parts || bounds.width <= 0 || bounds.height <= 0) return\n applyRect(\n clampRectInside(\n {\n x: bounds.x + crop.x * bounds.width,\n y: bounds.y + crop.y * bounds.height,\n width: crop.width * bounds.width,\n height: crop.height * bounds.height,\n },\n constraintBounds(),\n props.aspectRatio\n )\n )\n}\n\nconst cropsAreClose = (a: CropRect | null, b: CropRect | null): boolean =>\n a !== null &&\n b !== null &&\n Math.abs(a.x - b.x) < 0.0005 &&\n Math.abs(a.y - b.y) < 0.0005 &&\n Math.abs(a.width - b.width) < 0.0005 &&\n Math.abs(a.height - b.height) < 0.0005\n\n/**\n * Hands the crop back up. Also called after the component itself has adjusted one — a stored region\n * rarely matches the configured ratio, and the binding should say where the crop really is rather\n * than where it was asked to be.\n */\nconst emitCrop = (alsoCommit = false) => {\n const crop = currentCrop()\n if (!crop) return\n if (alsoCommit || !cropsAreClose(crop, lastEmitted)) {\n lastEmitted = crop\n emit('update:modelValue', crop)\n }\n if (alsoCommit) emit('commit', crop)\n}\n\n/** The room the host has: its width, and the height that will actually be shown. */\nconst hostBox = (host: HTMLElement): Size => ({ width: host.clientWidth, height: visibleHeightOf(host) })\n\n/** Re-fit the image to the new container while keeping the crop the user already chose. */\nconst relayout = () => {\n const host = hostEl.value\n if (!parts || !host) return\n // A crop of no size means the last layout had none either — fall back to the bound one, which is\n // what a cropper built inside a closed panel has been holding all along.\n const wanted = currentCrop() ?? props.modelValue\n\n const size = sizeCanvas(parts.canvas, host, host.clientWidth)\n laidOut = hostBox(host)\n // Re-read the ancestors as well: a cropper can be moved between frames, and the one clipping it\n // now is the one whose height matters.\n observeAncestors(host)\n centerImage()\n if (size.width <= 0 || size.height <= 0) return\n\n if (wanted && wanted.width > 0) applyCrop(wanted)\n else applyModelOrSeed()\n emitCrop()\n}\n\nconst onHostBoxChanged = () => {\n const host = hostEl.value\n if (!host) return\n const box = hostBox(host)\n if (box.width <= 0 || box.height <= 0) return\n // Compared against the box the current layout was built for, so a size seen while the cropper was\n // still loading cannot be mistaken for one that has already been laid out.\n if (box.width === laidOut.width && box.height === laidOut.height) return\n if (!parts) return\n // Deferred a frame: resizing the canvas from inside the observer's own callback is what makes\n // Chromium report an undelivered-notification loop.\n requestAnimationFrame(() => relayout())\n}\n\n// Height counts as much as width: the DAM sizes the cropper with `calc(100vh - 160px)`, so a\n// shorter window shrinks the frame without touching its width.\nuseResizeObserver(hostEl, onHostBoxChanged)\n\n/** An ancestor that clips can change how much it shows without the host's own box moving at all. */\nconst observeAncestors = (host: HTMLElement) => {\n ancestorObserver?.disconnect()\n const ancestors = clippingAncestorsOf(host)\n if (ancestors.length === 0) {\n ancestorObserver = null\n return\n }\n ancestorObserver = new ResizeObserver(onHostBoxChanged)\n ancestors.forEach((ancestor) => ancestorObserver?.observe(ancestor))\n}\n\nwatch(\n () => props.src,\n () => {\n build()\n }\n)\n\nwatch(\n () => props.modelValue,\n (model) => {\n // Ignore the echo of what this component just handed up, or the two would write to each other\n // for as long as the parent keeps binding it back.\n if (!parts || !model || cropsAreClose(model, lastEmitted)) return\n applyCrop(model)\n emitCrop()\n },\n { deep: true }\n)\n\nwatch(\n () => props.shadeColor,\n (shadeColor) => {\n const shade = parts?.canvas.querySelector<CropperShade>(CropperShade.$name)\n if (shade) shade.themeColor = shadeColor\n }\n)\n\nwatch(\n () => props.aspectRatio,\n (aspectRatio) => {\n if (!parts) return\n parts.selection.aspectRatio = aspectRatio\n if (!isUsableAspectRatio(aspectRatio)) return\n const current = selectionRect()\n applyRect(clampRectInside({ ...current, height: current.width / aspectRatio }, constraintBounds(), aspectRatio))\n }\n)\n\nonMounted(() => {\n build()\n})\n\nonBeforeUnmount(() => {\n buildToken += 1\n teardown()\n})\n</script>\n\n<template>\n <div class=\"a-cropper d-flex flex-column\">\n <div\n v-if=\"loading\"\n class=\"d-flex w-100 align-center justify-center\"\n >\n <VProgressCircular\n class=\"position-absolute\"\n indeterminate\n />\n </div>\n\n <div\n ref=\"hostEl\"\n class=\"a-cropper__container\"\n :style=\"containerStyle\"\n @wheel.capture=\"onWheelCapture\"\n />\n </div>\n</template>\n\n<style lang=\"scss\">\n// `cropper-crosshair` sizes itself in `em`, so without a size of its own the centre marker would\n// grow and shrink with whatever font the surrounding admin happens to use. 7px is the plus sign\n// cropper.js v1 drew through `.cropper-center`.\n.a-cropper cropper-crosshair {\n font-size: 7px;\n}\n\n// cropper.js v1 drew a `.cropper-line` along each edge of the crop box: a 5px band of the selection\n// colour at a tenth opacity, sitting 3px outside the border and 2px inside it. It is what made the\n// thin outline read as a grabbable edge, so it is drawn here too — as a pair of shadows rather than\n// four elements, which keeps it independent of how the v2 grips happen to be sized.\n.a-cropper cropper-selection[outlined] {\n box-shadow:\n inset 0 0 0 2px rgb(51 153 255 / 10%),\n 0 0 0 3px rgb(51 153 255 / 10%);\n}\n</style>\n"],"mappings":""}
|
|
@@ -5,7 +5,7 @@ import r from "../../../../vendor/@cropper/element-handle/dist/element-handle.es
|
|
|
5
5
|
import i from "../../../../vendor/@cropper/element-selection/dist/element-selection.esm.raw.js";
|
|
6
6
|
import a from "../../../../vendor/@cropper/element-grid/dist/element-grid.esm.raw.js";
|
|
7
7
|
import o from "../../../../vendor/@cropper/element-crosshair/dist/element-crosshair.esm.raw.js";
|
|
8
|
-
import "../../../../vendor/
|
|
8
|
+
import "../../../../vendor/cropperjs/dist/cropper.esm.raw.js";
|
|
9
9
|
import { RESIZE_ANCHORS as s, canvasSizeFor as c, clampRectInside as l, initialSelectionRect as u, isUsableAspectRatio as d, rectsAreClose as f } from "../utils/cropperGeometry.js";
|
|
10
10
|
import { createCommentVNode as ee, createElementBlock as p, createElementVNode as te, createVNode as ne, defineComponent as m, normalizeStyle as re, onBeforeUnmount as ie, onMounted as ae, openBlock as h, ref as oe, useTemplateRef as se, watch as g } from "vue";
|
|
11
11
|
import { useResizeObserver as ce } from "@vueuse/core";
|
package/dist/domains/dam/cropper/components/ACropper.vue_vue_type_script_setup_true_lang.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ACropper.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createElementBlock","_createVNode","_component_VProgressCircular","_createCommentVNode","_createElementVNode","_normalizeStyle","CropperCanvas","CropperImage","CropperShade","CropperHandle","CropperSelection","CropperGrid","CropperCrosshair","_hoisted_1","_hoisted_2","AUTO_CROP_AREA","MIN_CONTAINER_WIDTH","MIN_CONTAINER_HEIGHT","SELECTION_COLOR","FACE_COLOR","GRID_COLOR","CROSSHAIR_COLOR","ACropper_vue_vue_type_script_setup_true_lang_default","__emit","props","__props","emit","element","RESIZE_ACTIONS","hostEl","useTemplateRef","loading","ref","parts","natural","currentAction","clamping","centring","buildToken","laidOut","lastEmitted","ancestorObserver","create","name","imageBounds","canvasRect","imageRect","constraintBounds","image","canvas","left","top","right","bottom","selectionRect","applyRect","rect","centerImage","scalable","translatable","seedInitialCrop","bounds","initial","initialSelectionRect","applyModelOrSeed","model","applyCrop","emitCrop","refit","current","fitted","clampRectInside","rectsAreClose","onSelectionChange","detail","event","liveAction","action","anchor","RESIZE_ANCHORS","clamped","onActionStart","onAction","onActionEnd","onWheelCapture","onImageTransform","teardown","selection","clippingAncestorsOf","found","ancestor","host","overflowY","visibleHeightOf","available","hostTop","room","sizeCanvas","size","canvasSizeFor","availableWidth","visibleHeight","buildSelectionChildren","grid","crosshair","face","handle","build","token","shade","selectHandle","loaded","hostBox","observeAncestors","currentCrop","crop","cropsAreClose","a","b","alsoCommit","relayout","wanted","onHostBoxChanged","box","ancestors","watch","shadeColor","aspectRatio","isUsableAspectRatio","onMounted","onBeforeUnmount","openBlock","createElementBlock","createVNode","VProgressCircular","createCommentVNode","createElementVNode","normalizeStyle"],"sources":["../../../../../src/domains/dam/cropper/components/ACropper.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'\nimport { useResizeObserver } from '@vueuse/core'\nimport {\n CropperCanvas,\n CropperCrosshair,\n CropperGrid,\n CropperHandle,\n CropperImage,\n CropperSelection,\n CropperShade,\n} from 'cropperjs2'\nimport {\n canvasSizeFor,\n clampRectInside,\n initialSelectionRect,\n isUsableAspectRatio,\n RESIZE_ANCHORS,\n rectsAreClose,\n type Rect,\n type Size,\n} from '@/domains/dam/cropper/utils/cropperGeometry'\nimport type { CropRect } from '@/domains/dam/cropper/utils/cropperTypes'\n\n/**\n * Image cropper built on cropper.js v2's custom elements, replacing the v1-based `ACropperjs`.\n *\n * It lives here, beside `DamAssetImageRoiSelect`, because that is the only thing that uses it and\n * nothing outside this library does. It is not exported: shaping it to one need rather than to a\n * general audience is what keeps it to its size.\n *\n * The crop is bound, not fetched: `v-model` carries it as fractions of the image, and `commit` says\n * the user has finished a gesture. There is no imperative surface — the one thing a host wants to do\n * with a cropper is know where the crop is, and a binding says that better than six methods did.\n *\n * The elements are assembled in script rather than in the template on purpose: a consumer of this\n * library does not configure `isCustomElement` in their own build, and setting element *properties*\n * instead of going through attribute strings avoids the coercion surprises (`NaN`, booleans,\n * fractional numbers) that come with the attribute round-trip.\n */\n\nconst props = withDefaults(\n defineProps<{\n src?: string\n /** The crop, as fractions of the image. Leave it null to start from the default box. */\n modelValue?: CropRect | null\n /** `NaN` (the default) leaves the crop free-form. */\n aspectRatio?: number\n containerStyle?: { [key: string]: string } | undefined\n /** Wash over everything outside the crop — the replacement for overriding `.cropper-modal`. */\n shadeColor?: string\n }>(),\n {\n src: '',\n modelValue: null,\n aspectRatio: NaN,\n containerStyle: undefined,\n shadeColor: 'rgba(0, 0, 0, 0.5)',\n }\n)\n\nconst emit = defineEmits<{\n (event: 'update:modelValue', crop: CropRect): void\n /** The user has finished a gesture; the crop is worth saving. */\n (event: 'commit', crop: CropRect): void\n (event: 'ready'): void\n}>()\n\n// cropper.js registers its elements as a side effect of loading its entry, and this package's\n// `sideEffects` lets the consumer's bundler drop that module once only the classes are used. `$define`\n// is a no-op for an element that is already registered.\nfor (const element of [\n CropperCanvas,\n CropperCrosshair,\n CropperGrid,\n CropperHandle,\n CropperImage,\n CropperSelection,\n CropperShade,\n]) {\n element.$define()\n}\n\n/**\n * Settings the one caller never varies, kept as constants rather than as props nobody passes.\n * Each of them is behaviour, not preference: no checkerboard behind a photograph, the wheel left to\n * the page, pinch zoom kept for tablets, and the minimum canvas cropper.js' own stylesheet declares.\n */\nconst AUTO_CROP_AREA = 0.8\nconst MIN_CONTAINER_WIDTH = 200\nconst MIN_CONTAINER_HEIGHT = 100\n\n// Matched to cropper.js v1's stylesheet so the two components are indistinguishable side by side.\nconst SELECTION_COLOR = 'rgba(51, 153, 255, 0.75)'\nconst FACE_COLOR = 'rgba(255, 255, 255, 0.1)'\nconst GRID_COLOR = 'rgba(238, 238, 238, 0.5)'\nconst CROSSHAIR_COLOR = 'rgba(238, 238, 238, 0.75)'\nconst RESIZE_ACTIONS = [\n 'n-resize',\n 'e-resize',\n 's-resize',\n 'w-resize',\n 'ne-resize',\n 'nw-resize',\n 'se-resize',\n 'sw-resize',\n]\n\ninterface CropperParts {\n canvas: CropperCanvas\n image: CropperImage\n selection: CropperSelection\n}\n\nconst hostEl = useTemplateRef<HTMLDivElement>('hostEl')\nconst loading = ref(true)\n\nlet parts: CropperParts | null = null\nlet natural: Size = { width: 0, height: 0 }\n/** The action cropper.js is currently performing, so a clamp knows which edge to hold still. */\nlet currentAction: string | null = null\n/** Guards the re-entrant `$change` a clamp performs from being clamped again. */\nlet clamping = false\n/** Guards the half-applied transforms `$center` emits between its move and its scale. */\nlet centring = false\n/** Invalidates an in-flight build when `src` changes or the component goes away mid-load. */\nlet buildToken = 0\n/** The host box the current layout was computed for — not merely the last one observed. */\nlet laidOut: Size = { width: 0, height: 0 }\n/** The last crop handed out, so a model written straight back is not mistaken for a new one. */\nlet lastEmitted: CropRect | null = null\n/** Watches the ancestors that clip the host; their height is part of the room it has. */\nlet ancestorObserver: ResizeObserver | null = null\n\nconst create = <T extends HTMLElement>(name: string): T => document.createElement(name) as T\n\n/** The image box in canvas coordinates. Read from layout so panning and zooming stay accounted for. */\nconst imageBounds = (): Rect => {\n if (!parts) return { x: 0, y: 0, width: 0, height: 0 }\n const canvasRect = parts.canvas.getBoundingClientRect()\n const imageRect = parts.image.getBoundingClientRect()\n return {\n x: imageRect.left - canvasRect.left,\n y: imageRect.top - canvasRect.top,\n width: imageRect.width,\n height: imageRect.height,\n }\n}\n\n/**\n * Where the crop is allowed to be: the image, but never outside the visible canvas. The two are the\n * same box until the image is zoomed past its frame, and then it is the frame that wins — cropper.js\n * v1 limited its crop box to the canvas and the container both, so a zoomed-in image never let the\n * crop wander off the part of it you could actually see.\n */\nconst constraintBounds = (): Rect => {\n if (!parts) return { x: 0, y: 0, width: 0, height: 0 }\n const image = imageBounds()\n const canvas = parts.canvas.getBoundingClientRect()\n const left = Math.max(image.x, 0)\n const top = Math.max(image.y, 0)\n const right = Math.min(image.x + image.width, canvas.width)\n const bottom = Math.min(image.y + image.height, canvas.height)\n return { x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) }\n}\n\nconst selectionRect = (): Rect => {\n if (!parts) return { x: 0, y: 0, width: 0, height: 0 }\n const { x, y, width, height } = parts.selection\n return { x, y, width, height }\n}\n\nconst applyRect = (rect: Rect) => {\n if (!parts) return\n clamping = true\n parts.selection.$change(rect.x, rect.y, rect.width, rect.height, props.aspectRatio, true)\n clamping = false\n}\n\n/**\n * Re-fits the image to the canvas. cropper.js only lets `$center` scale and move an image that is\n * marked scalable and translatable — it grants itself that permission while the image is still\n * loading, but not afterwards — so the flags are lifted for the call and put back straight after.\n */\nconst centerImage = () => {\n if (!parts) return\n const { image } = parts\n const { scalable, translatable } = image\n centring = true\n image.scalable = true\n image.translatable = true\n image.$center('contain')\n image.scalable = scalable\n image.translatable = translatable\n centring = false\n}\n\n/**\n * The crop a fresh picture starts with. Computed here rather than handed to cropper.js'\n * `initialCoverage`, whose deferred initialisation would land *after* a `ready` handler had already\n * positioned the crop. It is measured against the image rather than the canvas, which are the same\n * box for a source wider than its container and are not once the image sits centred between margins.\n */\nconst seedInitialCrop = () => {\n if (!parts) return\n const bounds = imageBounds()\n if (bounds.width <= 0 || bounds.height <= 0) return\n const initial = initialSelectionRect(bounds, props.aspectRatio, AUTO_CROP_AREA)\n applyRect({ ...initial, x: initial.x + bounds.x, y: initial.y + bounds.y })\n}\n\n/** Uses the bound crop if there is one, and the default box if there is not. */\nconst applyModelOrSeed = () => {\n const model = props.modelValue\n if (model && model.width > 0 && model.height > 0) applyCrop(model)\n else seedInitialCrop()\n emitCrop()\n}\n\n/** Pulls the selection back inside the image after something moved the image out from under it. */\nconst refit = () => {\n if (!parts || centring) return\n const current = selectionRect()\n if (current.width <= 0 || current.height <= 0) return\n const fitted = clampRectInside(current, constraintBounds(), props.aspectRatio)\n if (!rectsAreClose(current, fitted)) applyRect(fitted)\n}\n\nconst onSelectionChange = (event: Event) => {\n if (clamping || !parts) return\n const detail = (event as CustomEvent<Rect>).detail\n if (!detail) return\n // cropper.js reverses the action when a grip is dragged through the opposite edge, and it records\n // that on the canvas just before emitting this event — one move ahead of the `action` event the\n // tracking below sees. Prefer its own answer, and fall back if a future version stops publishing it.\n const liveAction = (parts.canvas as unknown as { $action?: unknown }).$action\n const action = typeof liveAction === 'string' && liveAction ? liveAction : currentAction\n const anchor = action ? RESIZE_ANCHORS[action] : undefined\n const clamped = clampRectInside(detail, constraintBounds(), props.aspectRatio, anchor)\n if (rectsAreClose(detail, clamped)) return\n // Rejecting the change and re-issuing the clamped one is what keeps the crop sliding along the\n // border instead of freezing there; the shade reads `defaultPrevented` and follows the new values.\n event.preventDefault()\n applyRect(clamped)\n}\n\nconst onActionStart = (event: Event) => {\n currentAction = (event as CustomEvent<{ action?: string }>).detail?.action ?? null\n}\n\nconst onAction = (event: Event) => {\n const action = (event as CustomEvent<{ action?: string }>).detail?.action\n // cropper.js turns a fresh \"select\" drag into the matching corner resize once it knows the\n // direction, and reports the new action on the next move — follow it so the anchor stays right.\n if (action) currentAction = action\n}\n\nconst onActionEnd = () => {\n currentAction = null\n emitCrop(true)\n}\n\n/**\n * The canvas must never see a wheel event: its own handler calls `preventDefault()` unconditionally,\n * which would swallow page scrolling over the picture the way cropper.js v1 never did. Pinching is\n * unaffected — that arrives as pointer events, and the picture stays scalable for it.\n */\nconst onWheelCapture = (event: WheelEvent) => {\n event.stopPropagation()\n}\n\n/**\n * cropper.js emits `transform` *before* it writes the new matrix to the element's style (it sets the\n * new transform to measure the result, puts the old one back, then emits and finally commits). Re-\n * fitting straight away would therefore measure the image as it was: after a zoom out the crop would\n * be judged against the larger, previous picture and left hanging over the edge of the new one.\n */\nconst onImageTransform = () => queueMicrotask(refit)\n\nconst teardown = () => {\n if (parts) {\n const { canvas, image, selection } = parts\n selection.removeEventListener('change', onSelectionChange)\n canvas.removeEventListener('actionstart', onActionStart)\n canvas.removeEventListener('action', onAction)\n canvas.removeEventListener('actionend', onActionEnd)\n image.removeEventListener('transform', onImageTransform)\n }\n // Every canvas, not just the one on `parts`: a build that was superseded while still waiting on\n // its image has already put one in the DOM and has nothing tracking it yet.\n hostEl.value?.querySelectorAll(CropperCanvas.$name).forEach((canvas) => {\n // Disconnecting a `cropper-image` drops its listeners but leaves its request running, and the\n // `$ready()` promise waiting on it keeps this component reachable until the server answers.\n canvas.querySelectorAll<CropperImage>(CropperImage.$name).forEach((image) => {\n if (image.$image) image.$image.src = ''\n })\n canvas.remove()\n })\n ancestorObserver?.disconnect()\n ancestorObserver = null\n parts = null\n natural = { width: 0, height: 0 }\n laidOut = { width: 0, height: 0 }\n currentAction = null\n}\n\n/**\n * How much of the host's height will actually be shown.\n *\n * The host's own `max-height` is the usual answer, and the one `DamAssetImageRoiSelect` relies on.\n * Ancestors count too when they clip with `overflow: hidden` — what they cut off cannot be scrolled\n * back into view, so it is no more reachable than the host's own overflow. An ancestor that\n * scrolls is left alone: there the rest of the picture is a scroll away, not gone.\n */\nconst clippingAncestorsOf = (host: HTMLElement): HTMLElement[] => {\n const found: HTMLElement[] = []\n let ancestor = host.parentElement\n while (ancestor && ancestor !== document.body && ancestor !== document.documentElement) {\n const { overflowY } = window.getComputedStyle(ancestor)\n if (overflowY === 'hidden' || overflowY === 'clip') found.push(ancestor)\n ancestor = ancestor.parentElement\n }\n return found\n}\n\nconst visibleHeightOf = (host: HTMLElement): number => {\n let available = host.clientHeight\n const hostTop = host.getBoundingClientRect().top\n\n clippingAncestorsOf(host).forEach((ancestor) => {\n const rect = ancestor.getBoundingClientRect()\n // From wherever the host starts being visible down to where the ancestor stops showing anything.\n // Taking the ancestor's own top into account matters when the host begins above it: what is\n // scrolled or positioned off the top is just as unreachable as what falls off the bottom.\n const room = rect.bottom - Math.max(rect.top, hostTop)\n if (room > 0) available = Math.min(available, room)\n })\n\n return available\n}\n\n/**\n * Sizes the canvas to the room the host actually gives it, in two passes because the second answer\n * depends on the first: the height a picture wants follows from its width, but a frame with a\n * `max-height` only reveals how much of that it will show once something is in it.\n *\n * cropper.js v1 did the same dance with its `<img>`, and the point of it is that its cropper never\n * overflowed: a tall picture in a short frame was scaled down to fit rather than cut off at the\n * bottom, where the part of the crop hidden by `overflow: hidden` could not be reached again.\n */\nconst sizeCanvas = (canvas: CropperCanvas, host: HTMLElement, availableWidth: number): Size => {\n const size = canvasSizeFor(natural, availableWidth, MIN_CONTAINER_WIDTH, MIN_CONTAINER_HEIGHT)\n canvas.style.width = `${size.width}px`\n canvas.style.height = `${size.height}px`\n\n const visibleHeight = visibleHeightOf(host)\n if (visibleHeight > 0 && visibleHeight < size.height) {\n size.height = Math.max(visibleHeight, MIN_CONTAINER_HEIGHT)\n canvas.style.height = `${size.height}px`\n }\n return size\n}\n\nconst buildSelectionChildren = (selection: CropperSelection) => {\n const grid = create<CropperGrid>(CropperGrid.$name)\n grid.rows = 3\n grid.columns = 3\n grid.covered = true\n grid.themeColor = GRID_COLOR\n grid.setAttribute('role', 'grid')\n selection.appendChild(grid)\n\n const crosshair = create<CropperCrosshair>(CropperCrosshair.$name)\n crosshair.centered = true\n crosshair.themeColor = CROSSHAIR_COLOR\n selection.appendChild(crosshair)\n\n const face = create<CropperHandle>(CropperHandle.$name)\n face.action = 'move'\n face.themeColor = FACE_COLOR\n selection.appendChild(face)\n\n RESIZE_ACTIONS.forEach((action) => {\n const handle = create<CropperHandle>(CropperHandle.$name)\n handle.action = action\n handle.themeColor = SELECTION_COLOR\n selection.appendChild(handle)\n })\n}\n\nconst build = async () => {\n const token = ++buildToken\n teardown()\n loading.value = true\n\n const host = hostEl.value\n if (!host || !props.src) {\n // Nothing to wait for: leaving the spinner up would make an empty cropper look like a stuck one.\n loading.value = false\n return\n }\n\n const canvas = create<CropperCanvas>(CropperCanvas.$name)\n canvas.background = false\n canvas.themeColor = SELECTION_COLOR\n canvas.style.display = 'block'\n sizeCanvas(canvas, host, host.clientWidth)\n\n const image = create<CropperImage>(CropperImage.$name)\n image.alt = ''\n image.initialFit = 'contain'\n image.rotatable = false\n image.skewable = false\n image.translatable = false\n image.scalable = true\n image.src = props.src\n\n const shade = create<CropperShade>(CropperShade.$name)\n shade.themeColor = props.shadeColor\n // Kept out of the way until the first crop box exists, or it would flash across the whole canvas.\n shade.hidden = true\n\n const selectHandle = create<CropperHandle>(CropperHandle.$name)\n selectHandle.action = 'select'\n selectHandle.plain = true\n\n const selection = create<CropperSelection>(CropperSelection.$name)\n selection.themeColor = SELECTION_COLOR\n selection.movable = true\n selection.resizable = true\n selection.outlined = true\n // Keep sub-pixel values: rounding to whole canvas pixels would drift the natural-pixel crop data\n // that callers persist, and cropper.js v1 did not round either.\n selection.precise = true\n selection.aspectRatio = props.aspectRatio\n buildSelectionChildren(selection)\n\n // Listeners go on before anything is connected, so the action in flight is already known by the\n // time the elements' own handlers react to it.\n canvas.addEventListener('actionstart', onActionStart)\n canvas.addEventListener('action', onAction)\n canvas.addEventListener('actionend', onActionEnd)\n image.addEventListener('transform', onImageTransform)\n selection.addEventListener('change', onSelectionChange)\n\n canvas.append(image, shade, selectHandle, selection)\n // The whole tree is attached in one move on purpose: each element wires itself up to its siblings\n // when it connects, and the shade in particular goes looking for the selection. Appending the\n // children one at a time leaves it looking at an empty canvas, and it never follows the crop.\n host.appendChild(canvas)\n // Second pass, now that there is something in the host for its own constraints to act on.\n sizeCanvas(canvas, host, host.clientWidth)\n\n let loaded: HTMLImageElement\n try {\n // The element runs the one request there is; its promise carries the picture that arrived.\n loaded = await image.$ready()\n } catch {\n // Mirrors cropper.js v1, which simply never became ready on a broken source.\n canvas.remove()\n if (token === buildToken) loading.value = false\n return\n }\n if (token !== buildToken) {\n canvas.remove()\n return\n }\n\n parts = { canvas, image, selection }\n // Only now is the picture's own size known, so the canvas gets its real dimensions here rather\n // than from a second request made ahead of time purely to measure it.\n natural = { width: loaded.naturalWidth, height: loaded.naturalHeight }\n sizeCanvas(canvas, host, host.clientWidth)\n\n // A prop that changed while the picture was loading found the watchers with no elements to write\n // to. Re-reading the two that can vary closes that window; it is idempotent otherwise.\n shade.themeColor = props.shadeColor\n selection.aspectRatio = props.aspectRatio\n\n centerImage()\n\n laidOut = hostBox(host)\n observeAncestors(host)\n applyModelOrSeed()\n\n loading.value = false\n emit('ready')\n}\n\n/** Where the crop sits, as fractions of the image. */\nconst currentCrop = (): CropRect | null => {\n if (!parts) return null\n const bounds = imageBounds()\n if (bounds.width <= 0 || bounds.height <= 0) return null\n const rect = selectionRect()\n return {\n x: (rect.x - bounds.x) / bounds.width,\n y: (rect.y - bounds.y) / bounds.height,\n width: rect.width / bounds.width,\n height: rect.height / bounds.height,\n }\n}\n\n/** Puts a crop given in fractions onto the canvas, clamped to the picture. */\nconst applyCrop = (crop: CropRect) => {\n const bounds = imageBounds()\n if (!parts || bounds.width <= 0 || bounds.height <= 0) return\n applyRect(\n clampRectInside(\n {\n x: bounds.x + crop.x * bounds.width,\n y: bounds.y + crop.y * bounds.height,\n width: crop.width * bounds.width,\n height: crop.height * bounds.height,\n },\n constraintBounds(),\n props.aspectRatio\n )\n )\n}\n\nconst cropsAreClose = (a: CropRect | null, b: CropRect | null): boolean =>\n a !== null &&\n b !== null &&\n Math.abs(a.x - b.x) < 0.0005 &&\n Math.abs(a.y - b.y) < 0.0005 &&\n Math.abs(a.width - b.width) < 0.0005 &&\n Math.abs(a.height - b.height) < 0.0005\n\n/**\n * Hands the crop back up. Also called after the component itself has adjusted one — a stored region\n * rarely matches the configured ratio, and the binding should say where the crop really is rather\n * than where it was asked to be.\n */\nconst emitCrop = (alsoCommit = false) => {\n const crop = currentCrop()\n if (!crop) return\n if (alsoCommit || !cropsAreClose(crop, lastEmitted)) {\n lastEmitted = crop\n emit('update:modelValue', crop)\n }\n if (alsoCommit) emit('commit', crop)\n}\n\n/** The room the host has: its width, and the height that will actually be shown. */\nconst hostBox = (host: HTMLElement): Size => ({ width: host.clientWidth, height: visibleHeightOf(host) })\n\n/** Re-fit the image to the new container while keeping the crop the user already chose. */\nconst relayout = () => {\n const host = hostEl.value\n if (!parts || !host) return\n // A crop of no size means the last layout had none either — fall back to the bound one, which is\n // what a cropper built inside a closed panel has been holding all along.\n const wanted = currentCrop() ?? props.modelValue\n\n const size = sizeCanvas(parts.canvas, host, host.clientWidth)\n laidOut = hostBox(host)\n // Re-read the ancestors as well: a cropper can be moved between frames, and the one clipping it\n // now is the one whose height matters.\n observeAncestors(host)\n centerImage()\n if (size.width <= 0 || size.height <= 0) return\n\n if (wanted && wanted.width > 0) applyCrop(wanted)\n else applyModelOrSeed()\n emitCrop()\n}\n\nconst onHostBoxChanged = () => {\n const host = hostEl.value\n if (!host) return\n const box = hostBox(host)\n if (box.width <= 0 || box.height <= 0) return\n // Compared against the box the current layout was built for, so a size seen while the cropper was\n // still loading cannot be mistaken for one that has already been laid out.\n if (box.width === laidOut.width && box.height === laidOut.height) return\n if (!parts) return\n // Deferred a frame: resizing the canvas from inside the observer's own callback is what makes\n // Chromium report an undelivered-notification loop.\n requestAnimationFrame(() => relayout())\n}\n\n// Height counts as much as width: the DAM sizes the cropper with `calc(100vh - 160px)`, so a\n// shorter window shrinks the frame without touching its width.\nuseResizeObserver(hostEl, onHostBoxChanged)\n\n/** An ancestor that clips can change how much it shows without the host's own box moving at all. */\nconst observeAncestors = (host: HTMLElement) => {\n ancestorObserver?.disconnect()\n const ancestors = clippingAncestorsOf(host)\n if (ancestors.length === 0) {\n ancestorObserver = null\n return\n }\n ancestorObserver = new ResizeObserver(onHostBoxChanged)\n ancestors.forEach((ancestor) => ancestorObserver?.observe(ancestor))\n}\n\nwatch(\n () => props.src,\n () => {\n build()\n }\n)\n\nwatch(\n () => props.modelValue,\n (model) => {\n // Ignore the echo of what this component just handed up, or the two would write to each other\n // for as long as the parent keeps binding it back.\n if (!parts || !model || cropsAreClose(model, lastEmitted)) return\n applyCrop(model)\n emitCrop()\n },\n { deep: true }\n)\n\nwatch(\n () => props.shadeColor,\n (shadeColor) => {\n const shade = parts?.canvas.querySelector<CropperShade>(CropperShade.$name)\n if (shade) shade.themeColor = shadeColor\n }\n)\n\nwatch(\n () => props.aspectRatio,\n (aspectRatio) => {\n if (!parts) return\n parts.selection.aspectRatio = aspectRatio\n if (!isUsableAspectRatio(aspectRatio)) return\n const current = selectionRect()\n applyRect(clampRectInside({ ...current, height: current.width / aspectRatio }, constraintBounds(), aspectRatio))\n }\n)\n\nonMounted(() => {\n build()\n})\n\nonBeforeUnmount(() => {\n buildToken += 1\n teardown()\n})\n</script>\n\n<template>\n <div class=\"a-cropper d-flex flex-column\">\n <div\n v-if=\"loading\"\n class=\"d-flex w-100 align-center justify-center\"\n >\n <VProgressCircular\n class=\"position-absolute\"\n indeterminate\n />\n </div>\n\n <div\n ref=\"hostEl\"\n class=\"a-cropper__container\"\n :style=\"containerStyle\"\n @wheel.capture=\"onWheelCapture\"\n />\n </div>\n</template>\n\n<style lang=\"scss\">\n// `cropper-crosshair` sizes itself in `em`, so without a size of its own the centre marker would\n// grow and shrink with whatever font the surrounding admin happens to use. 7px is the plus sign\n// cropper.js v1 drew through `.cropper-center`.\n.a-cropper cropper-crosshair {\n font-size: 7px;\n}\n\n// cropper.js v1 drew a `.cropper-line` along each edge of the crop box: a 5px band of the selection\n// colour at a tenth opacity, sitting 3px outside the border and 2px inside it. It is what made the\n// thin outline read as a grabbable edge, so it is drawn here too — as a pair of shadows rather than\n// four elements, which keeps it independent of how the v2 grips happen to be sized.\n.a-cropper cropper-selection[outlined] {\n box-shadow:\n inset 0 0 0 2px rgb(51 153 255 / 10%),\n 0 0 0 3px rgb(51 153 255 / 10%);\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ACropper.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createElementBlock","_createVNode","_component_VProgressCircular","_createCommentVNode","_createElementVNode","_normalizeStyle","CropperCanvas","CropperImage","CropperShade","CropperHandle","CropperSelection","CropperGrid","CropperCrosshair","_hoisted_1","_hoisted_2","AUTO_CROP_AREA","MIN_CONTAINER_WIDTH","MIN_CONTAINER_HEIGHT","SELECTION_COLOR","FACE_COLOR","GRID_COLOR","CROSSHAIR_COLOR","ACropper_vue_vue_type_script_setup_true_lang_default","__emit","props","__props","emit","element","RESIZE_ACTIONS","hostEl","useTemplateRef","loading","ref","parts","natural","currentAction","clamping","centring","buildToken","laidOut","lastEmitted","ancestorObserver","create","name","imageBounds","canvasRect","imageRect","constraintBounds","image","canvas","left","top","right","bottom","selectionRect","applyRect","rect","centerImage","scalable","translatable","seedInitialCrop","bounds","initial","initialSelectionRect","applyModelOrSeed","model","applyCrop","emitCrop","refit","current","fitted","clampRectInside","rectsAreClose","onSelectionChange","detail","event","liveAction","action","anchor","RESIZE_ANCHORS","clamped","onActionStart","onAction","onActionEnd","onWheelCapture","onImageTransform","teardown","selection","clippingAncestorsOf","found","ancestor","host","overflowY","visibleHeightOf","available","hostTop","room","sizeCanvas","size","canvasSizeFor","availableWidth","visibleHeight","buildSelectionChildren","grid","crosshair","face","handle","build","token","shade","selectHandle","loaded","hostBox","observeAncestors","currentCrop","crop","cropsAreClose","a","b","alsoCommit","relayout","wanted","onHostBoxChanged","box","ancestors","watch","shadeColor","aspectRatio","isUsableAspectRatio","onMounted","onBeforeUnmount","openBlock","createElementBlock","createVNode","VProgressCircular","createCommentVNode","createElementVNode","normalizeStyle"],"sources":["../../../../../src/domains/dam/cropper/components/ACropper.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'\nimport { useResizeObserver } from '@vueuse/core'\nimport {\n CropperCanvas,\n CropperCrosshair,\n CropperGrid,\n CropperHandle,\n CropperImage,\n CropperSelection,\n CropperShade,\n} from 'cropperjs'\nimport {\n canvasSizeFor,\n clampRectInside,\n initialSelectionRect,\n isUsableAspectRatio,\n RESIZE_ANCHORS,\n rectsAreClose,\n type Rect,\n type Size,\n} from '@/domains/dam/cropper/utils/cropperGeometry'\nimport type { CropRect } from '@/domains/dam/cropper/utils/cropperTypes'\n\n/**\n * Image cropper built on cropper.js v2's custom elements, replacing the v1-based `ACropperjs`.\n *\n * It lives here, beside `DamAssetImageRoiSelect`, because that is the only thing that uses it and\n * nothing outside this library does. It is not exported: shaping it to one need rather than to a\n * general audience is what keeps it to its size.\n *\n * The crop is bound, not fetched: `v-model` carries it as fractions of the image, and `commit` says\n * the user has finished a gesture. There is no imperative surface — the one thing a host wants to do\n * with a cropper is know where the crop is, and a binding says that better than six methods did.\n *\n * The elements are assembled in script rather than in the template on purpose: a consumer of this\n * library does not configure `isCustomElement` in their own build, and setting element *properties*\n * instead of going through attribute strings avoids the coercion surprises (`NaN`, booleans,\n * fractional numbers) that come with the attribute round-trip.\n */\n\nconst props = withDefaults(\n defineProps<{\n src?: string\n /** The crop, as fractions of the image. Leave it null to start from the default box. */\n modelValue?: CropRect | null\n /** `NaN` (the default) leaves the crop free-form. */\n aspectRatio?: number\n containerStyle?: { [key: string]: string } | undefined\n /** Wash over everything outside the crop — the replacement for overriding `.cropper-modal`. */\n shadeColor?: string\n }>(),\n {\n src: '',\n modelValue: null,\n aspectRatio: NaN,\n containerStyle: undefined,\n shadeColor: 'rgba(0, 0, 0, 0.5)',\n }\n)\n\nconst emit = defineEmits<{\n (event: 'update:modelValue', crop: CropRect): void\n /** The user has finished a gesture; the crop is worth saving. */\n (event: 'commit', crop: CropRect): void\n (event: 'ready'): void\n}>()\n\n// cropper.js registers its elements as a side effect of loading its entry, and this package's\n// `sideEffects` lets the consumer's bundler drop that module once only the classes are used. `$define`\n// is a no-op for an element that is already registered.\nfor (const element of [\n CropperCanvas,\n CropperCrosshair,\n CropperGrid,\n CropperHandle,\n CropperImage,\n CropperSelection,\n CropperShade,\n]) {\n element.$define()\n}\n\n/**\n * Settings the one caller never varies, kept as constants rather than as props nobody passes.\n * Each of them is behaviour, not preference: no checkerboard behind a photograph, the wheel left to\n * the page, pinch zoom kept for tablets, and the minimum canvas cropper.js' own stylesheet declares.\n */\nconst AUTO_CROP_AREA = 0.8\nconst MIN_CONTAINER_WIDTH = 200\nconst MIN_CONTAINER_HEIGHT = 100\n\n// Matched to cropper.js v1's stylesheet so the two components are indistinguishable side by side.\nconst SELECTION_COLOR = 'rgba(51, 153, 255, 0.75)'\nconst FACE_COLOR = 'rgba(255, 255, 255, 0.1)'\nconst GRID_COLOR = 'rgba(238, 238, 238, 0.5)'\nconst CROSSHAIR_COLOR = 'rgba(238, 238, 238, 0.75)'\nconst RESIZE_ACTIONS = [\n 'n-resize',\n 'e-resize',\n 's-resize',\n 'w-resize',\n 'ne-resize',\n 'nw-resize',\n 'se-resize',\n 'sw-resize',\n]\n\ninterface CropperParts {\n canvas: CropperCanvas\n image: CropperImage\n selection: CropperSelection\n}\n\nconst hostEl = useTemplateRef<HTMLDivElement>('hostEl')\nconst loading = ref(true)\n\nlet parts: CropperParts | null = null\nlet natural: Size = { width: 0, height: 0 }\n/** The action cropper.js is currently performing, so a clamp knows which edge to hold still. */\nlet currentAction: string | null = null\n/** Guards the re-entrant `$change` a clamp performs from being clamped again. */\nlet clamping = false\n/** Guards the half-applied transforms `$center` emits between its move and its scale. */\nlet centring = false\n/** Invalidates an in-flight build when `src` changes or the component goes away mid-load. */\nlet buildToken = 0\n/** The host box the current layout was computed for — not merely the last one observed. */\nlet laidOut: Size = { width: 0, height: 0 }\n/** The last crop handed out, so a model written straight back is not mistaken for a new one. */\nlet lastEmitted: CropRect | null = null\n/** Watches the ancestors that clip the host; their height is part of the room it has. */\nlet ancestorObserver: ResizeObserver | null = null\n\nconst create = <T extends HTMLElement>(name: string): T => document.createElement(name) as T\n\n/** The image box in canvas coordinates. Read from layout so panning and zooming stay accounted for. */\nconst imageBounds = (): Rect => {\n if (!parts) return { x: 0, y: 0, width: 0, height: 0 }\n const canvasRect = parts.canvas.getBoundingClientRect()\n const imageRect = parts.image.getBoundingClientRect()\n return {\n x: imageRect.left - canvasRect.left,\n y: imageRect.top - canvasRect.top,\n width: imageRect.width,\n height: imageRect.height,\n }\n}\n\n/**\n * Where the crop is allowed to be: the image, but never outside the visible canvas. The two are the\n * same box until the image is zoomed past its frame, and then it is the frame that wins — cropper.js\n * v1 limited its crop box to the canvas and the container both, so a zoomed-in image never let the\n * crop wander off the part of it you could actually see.\n */\nconst constraintBounds = (): Rect => {\n if (!parts) return { x: 0, y: 0, width: 0, height: 0 }\n const image = imageBounds()\n const canvas = parts.canvas.getBoundingClientRect()\n const left = Math.max(image.x, 0)\n const top = Math.max(image.y, 0)\n const right = Math.min(image.x + image.width, canvas.width)\n const bottom = Math.min(image.y + image.height, canvas.height)\n return { x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) }\n}\n\nconst selectionRect = (): Rect => {\n if (!parts) return { x: 0, y: 0, width: 0, height: 0 }\n const { x, y, width, height } = parts.selection\n return { x, y, width, height }\n}\n\nconst applyRect = (rect: Rect) => {\n if (!parts) return\n clamping = true\n parts.selection.$change(rect.x, rect.y, rect.width, rect.height, props.aspectRatio, true)\n clamping = false\n}\n\n/**\n * Re-fits the image to the canvas. cropper.js only lets `$center` scale and move an image that is\n * marked scalable and translatable — it grants itself that permission while the image is still\n * loading, but not afterwards — so the flags are lifted for the call and put back straight after.\n */\nconst centerImage = () => {\n if (!parts) return\n const { image } = parts\n const { scalable, translatable } = image\n centring = true\n image.scalable = true\n image.translatable = true\n image.$center('contain')\n image.scalable = scalable\n image.translatable = translatable\n centring = false\n}\n\n/**\n * The crop a fresh picture starts with. Computed here rather than handed to cropper.js'\n * `initialCoverage`, whose deferred initialisation would land *after* a `ready` handler had already\n * positioned the crop. It is measured against the image rather than the canvas, which are the same\n * box for a source wider than its container and are not once the image sits centred between margins.\n */\nconst seedInitialCrop = () => {\n if (!parts) return\n const bounds = imageBounds()\n if (bounds.width <= 0 || bounds.height <= 0) return\n const initial = initialSelectionRect(bounds, props.aspectRatio, AUTO_CROP_AREA)\n applyRect({ ...initial, x: initial.x + bounds.x, y: initial.y + bounds.y })\n}\n\n/** Uses the bound crop if there is one, and the default box if there is not. */\nconst applyModelOrSeed = () => {\n const model = props.modelValue\n if (model && model.width > 0 && model.height > 0) applyCrop(model)\n else seedInitialCrop()\n emitCrop()\n}\n\n/** Pulls the selection back inside the image after something moved the image out from under it. */\nconst refit = () => {\n if (!parts || centring) return\n const current = selectionRect()\n if (current.width <= 0 || current.height <= 0) return\n const fitted = clampRectInside(current, constraintBounds(), props.aspectRatio)\n if (!rectsAreClose(current, fitted)) applyRect(fitted)\n}\n\nconst onSelectionChange = (event: Event) => {\n if (clamping || !parts) return\n const detail = (event as CustomEvent<Rect>).detail\n if (!detail) return\n // cropper.js reverses the action when a grip is dragged through the opposite edge, and it records\n // that on the canvas just before emitting this event — one move ahead of the `action` event the\n // tracking below sees. Prefer its own answer, and fall back if a future version stops publishing it.\n const liveAction = (parts.canvas as unknown as { $action?: unknown }).$action\n const action = typeof liveAction === 'string' && liveAction ? liveAction : currentAction\n const anchor = action ? RESIZE_ANCHORS[action] : undefined\n const clamped = clampRectInside(detail, constraintBounds(), props.aspectRatio, anchor)\n if (rectsAreClose(detail, clamped)) return\n // Rejecting the change and re-issuing the clamped one is what keeps the crop sliding along the\n // border instead of freezing there; the shade reads `defaultPrevented` and follows the new values.\n event.preventDefault()\n applyRect(clamped)\n}\n\nconst onActionStart = (event: Event) => {\n currentAction = (event as CustomEvent<{ action?: string }>).detail?.action ?? null\n}\n\nconst onAction = (event: Event) => {\n const action = (event as CustomEvent<{ action?: string }>).detail?.action\n // cropper.js turns a fresh \"select\" drag into the matching corner resize once it knows the\n // direction, and reports the new action on the next move — follow it so the anchor stays right.\n if (action) currentAction = action\n}\n\nconst onActionEnd = () => {\n currentAction = null\n emitCrop(true)\n}\n\n/**\n * The canvas must never see a wheel event: its own handler calls `preventDefault()` unconditionally,\n * which would swallow page scrolling over the picture the way cropper.js v1 never did. Pinching is\n * unaffected — that arrives as pointer events, and the picture stays scalable for it.\n */\nconst onWheelCapture = (event: WheelEvent) => {\n event.stopPropagation()\n}\n\n/**\n * cropper.js emits `transform` *before* it writes the new matrix to the element's style (it sets the\n * new transform to measure the result, puts the old one back, then emits and finally commits). Re-\n * fitting straight away would therefore measure the image as it was: after a zoom out the crop would\n * be judged against the larger, previous picture and left hanging over the edge of the new one.\n */\nconst onImageTransform = () => queueMicrotask(refit)\n\nconst teardown = () => {\n if (parts) {\n const { canvas, image, selection } = parts\n selection.removeEventListener('change', onSelectionChange)\n canvas.removeEventListener('actionstart', onActionStart)\n canvas.removeEventListener('action', onAction)\n canvas.removeEventListener('actionend', onActionEnd)\n image.removeEventListener('transform', onImageTransform)\n }\n // Every canvas, not just the one on `parts`: a build that was superseded while still waiting on\n // its image has already put one in the DOM and has nothing tracking it yet.\n hostEl.value?.querySelectorAll(CropperCanvas.$name).forEach((canvas) => {\n // Disconnecting a `cropper-image` drops its listeners but leaves its request running, and the\n // `$ready()` promise waiting on it keeps this component reachable until the server answers.\n canvas.querySelectorAll<CropperImage>(CropperImage.$name).forEach((image) => {\n if (image.$image) image.$image.src = ''\n })\n canvas.remove()\n })\n ancestorObserver?.disconnect()\n ancestorObserver = null\n parts = null\n natural = { width: 0, height: 0 }\n laidOut = { width: 0, height: 0 }\n currentAction = null\n}\n\n/**\n * How much of the host's height will actually be shown.\n *\n * The host's own `max-height` is the usual answer, and the one `DamAssetImageRoiSelect` relies on.\n * Ancestors count too when they clip with `overflow: hidden` — what they cut off cannot be scrolled\n * back into view, so it is no more reachable than the host's own overflow. An ancestor that\n * scrolls is left alone: there the rest of the picture is a scroll away, not gone.\n */\nconst clippingAncestorsOf = (host: HTMLElement): HTMLElement[] => {\n const found: HTMLElement[] = []\n let ancestor = host.parentElement\n while (ancestor && ancestor !== document.body && ancestor !== document.documentElement) {\n const { overflowY } = window.getComputedStyle(ancestor)\n if (overflowY === 'hidden' || overflowY === 'clip') found.push(ancestor)\n ancestor = ancestor.parentElement\n }\n return found\n}\n\nconst visibleHeightOf = (host: HTMLElement): number => {\n let available = host.clientHeight\n const hostTop = host.getBoundingClientRect().top\n\n clippingAncestorsOf(host).forEach((ancestor) => {\n const rect = ancestor.getBoundingClientRect()\n // From wherever the host starts being visible down to where the ancestor stops showing anything.\n // Taking the ancestor's own top into account matters when the host begins above it: what is\n // scrolled or positioned off the top is just as unreachable as what falls off the bottom.\n const room = rect.bottom - Math.max(rect.top, hostTop)\n if (room > 0) available = Math.min(available, room)\n })\n\n return available\n}\n\n/**\n * Sizes the canvas to the room the host actually gives it, in two passes because the second answer\n * depends on the first: the height a picture wants follows from its width, but a frame with a\n * `max-height` only reveals how much of that it will show once something is in it.\n *\n * cropper.js v1 did the same dance with its `<img>`, and the point of it is that its cropper never\n * overflowed: a tall picture in a short frame was scaled down to fit rather than cut off at the\n * bottom, where the part of the crop hidden by `overflow: hidden` could not be reached again.\n */\nconst sizeCanvas = (canvas: CropperCanvas, host: HTMLElement, availableWidth: number): Size => {\n const size = canvasSizeFor(natural, availableWidth, MIN_CONTAINER_WIDTH, MIN_CONTAINER_HEIGHT)\n canvas.style.width = `${size.width}px`\n canvas.style.height = `${size.height}px`\n\n const visibleHeight = visibleHeightOf(host)\n if (visibleHeight > 0 && visibleHeight < size.height) {\n size.height = Math.max(visibleHeight, MIN_CONTAINER_HEIGHT)\n canvas.style.height = `${size.height}px`\n }\n return size\n}\n\nconst buildSelectionChildren = (selection: CropperSelection) => {\n const grid = create<CropperGrid>(CropperGrid.$name)\n grid.rows = 3\n grid.columns = 3\n grid.covered = true\n grid.themeColor = GRID_COLOR\n grid.setAttribute('role', 'grid')\n selection.appendChild(grid)\n\n const crosshair = create<CropperCrosshair>(CropperCrosshair.$name)\n crosshair.centered = true\n crosshair.themeColor = CROSSHAIR_COLOR\n selection.appendChild(crosshair)\n\n const face = create<CropperHandle>(CropperHandle.$name)\n face.action = 'move'\n face.themeColor = FACE_COLOR\n selection.appendChild(face)\n\n RESIZE_ACTIONS.forEach((action) => {\n const handle = create<CropperHandle>(CropperHandle.$name)\n handle.action = action\n handle.themeColor = SELECTION_COLOR\n selection.appendChild(handle)\n })\n}\n\nconst build = async () => {\n const token = ++buildToken\n teardown()\n loading.value = true\n\n const host = hostEl.value\n if (!host || !props.src) {\n // Nothing to wait for: leaving the spinner up would make an empty cropper look like a stuck one.\n loading.value = false\n return\n }\n\n const canvas = create<CropperCanvas>(CropperCanvas.$name)\n canvas.background = false\n canvas.themeColor = SELECTION_COLOR\n canvas.style.display = 'block'\n sizeCanvas(canvas, host, host.clientWidth)\n\n const image = create<CropperImage>(CropperImage.$name)\n image.alt = ''\n image.initialFit = 'contain'\n image.rotatable = false\n image.skewable = false\n image.translatable = false\n image.scalable = true\n image.src = props.src\n\n const shade = create<CropperShade>(CropperShade.$name)\n shade.themeColor = props.shadeColor\n // Kept out of the way until the first crop box exists, or it would flash across the whole canvas.\n shade.hidden = true\n\n const selectHandle = create<CropperHandle>(CropperHandle.$name)\n selectHandle.action = 'select'\n selectHandle.plain = true\n\n const selection = create<CropperSelection>(CropperSelection.$name)\n selection.themeColor = SELECTION_COLOR\n selection.movable = true\n selection.resizable = true\n selection.outlined = true\n // Keep sub-pixel values: rounding to whole canvas pixels would drift the natural-pixel crop data\n // that callers persist, and cropper.js v1 did not round either.\n selection.precise = true\n selection.aspectRatio = props.aspectRatio\n buildSelectionChildren(selection)\n\n // Listeners go on before anything is connected, so the action in flight is already known by the\n // time the elements' own handlers react to it.\n canvas.addEventListener('actionstart', onActionStart)\n canvas.addEventListener('action', onAction)\n canvas.addEventListener('actionend', onActionEnd)\n image.addEventListener('transform', onImageTransform)\n selection.addEventListener('change', onSelectionChange)\n\n canvas.append(image, shade, selectHandle, selection)\n // The whole tree is attached in one move on purpose: each element wires itself up to its siblings\n // when it connects, and the shade in particular goes looking for the selection. Appending the\n // children one at a time leaves it looking at an empty canvas, and it never follows the crop.\n host.appendChild(canvas)\n // Second pass, now that there is something in the host for its own constraints to act on.\n sizeCanvas(canvas, host, host.clientWidth)\n\n let loaded: HTMLImageElement\n try {\n // The element runs the one request there is; its promise carries the picture that arrived.\n loaded = await image.$ready()\n } catch {\n // Mirrors cropper.js v1, which simply never became ready on a broken source.\n canvas.remove()\n if (token === buildToken) loading.value = false\n return\n }\n if (token !== buildToken) {\n canvas.remove()\n return\n }\n\n parts = { canvas, image, selection }\n // Only now is the picture's own size known, so the canvas gets its real dimensions here rather\n // than from a second request made ahead of time purely to measure it.\n natural = { width: loaded.naturalWidth, height: loaded.naturalHeight }\n sizeCanvas(canvas, host, host.clientWidth)\n\n // A prop that changed while the picture was loading found the watchers with no elements to write\n // to. Re-reading the two that can vary closes that window; it is idempotent otherwise.\n shade.themeColor = props.shadeColor\n selection.aspectRatio = props.aspectRatio\n\n centerImage()\n\n laidOut = hostBox(host)\n observeAncestors(host)\n applyModelOrSeed()\n\n loading.value = false\n emit('ready')\n}\n\n/** Where the crop sits, as fractions of the image. */\nconst currentCrop = (): CropRect | null => {\n if (!parts) return null\n const bounds = imageBounds()\n if (bounds.width <= 0 || bounds.height <= 0) return null\n const rect = selectionRect()\n return {\n x: (rect.x - bounds.x) / bounds.width,\n y: (rect.y - bounds.y) / bounds.height,\n width: rect.width / bounds.width,\n height: rect.height / bounds.height,\n }\n}\n\n/** Puts a crop given in fractions onto the canvas, clamped to the picture. */\nconst applyCrop = (crop: CropRect) => {\n const bounds = imageBounds()\n if (!parts || bounds.width <= 0 || bounds.height <= 0) return\n applyRect(\n clampRectInside(\n {\n x: bounds.x + crop.x * bounds.width,\n y: bounds.y + crop.y * bounds.height,\n width: crop.width * bounds.width,\n height: crop.height * bounds.height,\n },\n constraintBounds(),\n props.aspectRatio\n )\n )\n}\n\nconst cropsAreClose = (a: CropRect | null, b: CropRect | null): boolean =>\n a !== null &&\n b !== null &&\n Math.abs(a.x - b.x) < 0.0005 &&\n Math.abs(a.y - b.y) < 0.0005 &&\n Math.abs(a.width - b.width) < 0.0005 &&\n Math.abs(a.height - b.height) < 0.0005\n\n/**\n * Hands the crop back up. Also called after the component itself has adjusted one — a stored region\n * rarely matches the configured ratio, and the binding should say where the crop really is rather\n * than where it was asked to be.\n */\nconst emitCrop = (alsoCommit = false) => {\n const crop = currentCrop()\n if (!crop) return\n if (alsoCommit || !cropsAreClose(crop, lastEmitted)) {\n lastEmitted = crop\n emit('update:modelValue', crop)\n }\n if (alsoCommit) emit('commit', crop)\n}\n\n/** The room the host has: its width, and the height that will actually be shown. */\nconst hostBox = (host: HTMLElement): Size => ({ width: host.clientWidth, height: visibleHeightOf(host) })\n\n/** Re-fit the image to the new container while keeping the crop the user already chose. */\nconst relayout = () => {\n const host = hostEl.value\n if (!parts || !host) return\n // A crop of no size means the last layout had none either — fall back to the bound one, which is\n // what a cropper built inside a closed panel has been holding all along.\n const wanted = currentCrop() ?? props.modelValue\n\n const size = sizeCanvas(parts.canvas, host, host.clientWidth)\n laidOut = hostBox(host)\n // Re-read the ancestors as well: a cropper can be moved between frames, and the one clipping it\n // now is the one whose height matters.\n observeAncestors(host)\n centerImage()\n if (size.width <= 0 || size.height <= 0) return\n\n if (wanted && wanted.width > 0) applyCrop(wanted)\n else applyModelOrSeed()\n emitCrop()\n}\n\nconst onHostBoxChanged = () => {\n const host = hostEl.value\n if (!host) return\n const box = hostBox(host)\n if (box.width <= 0 || box.height <= 0) return\n // Compared against the box the current layout was built for, so a size seen while the cropper was\n // still loading cannot be mistaken for one that has already been laid out.\n if (box.width === laidOut.width && box.height === laidOut.height) return\n if (!parts) return\n // Deferred a frame: resizing the canvas from inside the observer's own callback is what makes\n // Chromium report an undelivered-notification loop.\n requestAnimationFrame(() => relayout())\n}\n\n// Height counts as much as width: the DAM sizes the cropper with `calc(100vh - 160px)`, so a\n// shorter window shrinks the frame without touching its width.\nuseResizeObserver(hostEl, onHostBoxChanged)\n\n/** An ancestor that clips can change how much it shows without the host's own box moving at all. */\nconst observeAncestors = (host: HTMLElement) => {\n ancestorObserver?.disconnect()\n const ancestors = clippingAncestorsOf(host)\n if (ancestors.length === 0) {\n ancestorObserver = null\n return\n }\n ancestorObserver = new ResizeObserver(onHostBoxChanged)\n ancestors.forEach((ancestor) => ancestorObserver?.observe(ancestor))\n}\n\nwatch(\n () => props.src,\n () => {\n build()\n }\n)\n\nwatch(\n () => props.modelValue,\n (model) => {\n // Ignore the echo of what this component just handed up, or the two would write to each other\n // for as long as the parent keeps binding it back.\n if (!parts || !model || cropsAreClose(model, lastEmitted)) return\n applyCrop(model)\n emitCrop()\n },\n { deep: true }\n)\n\nwatch(\n () => props.shadeColor,\n (shadeColor) => {\n const shade = parts?.canvas.querySelector<CropperShade>(CropperShade.$name)\n if (shade) shade.themeColor = shadeColor\n }\n)\n\nwatch(\n () => props.aspectRatio,\n (aspectRatio) => {\n if (!parts) return\n parts.selection.aspectRatio = aspectRatio\n if (!isUsableAspectRatio(aspectRatio)) return\n const current = selectionRect()\n applyRect(clampRectInside({ ...current, height: current.width / aspectRatio }, constraintBounds(), aspectRatio))\n }\n)\n\nonMounted(() => {\n build()\n})\n\nonBeforeUnmount(() => {\n buildToken += 1\n teardown()\n})\n</script>\n\n<template>\n <div class=\"a-cropper d-flex flex-column\">\n <div\n v-if=\"loading\"\n class=\"d-flex w-100 align-center justify-center\"\n >\n <VProgressCircular\n class=\"position-absolute\"\n indeterminate\n />\n </div>\n\n <div\n ref=\"hostEl\"\n class=\"a-cropper__container\"\n :style=\"containerStyle\"\n @wheel.capture=\"onWheelCapture\"\n />\n </div>\n</template>\n\n<style lang=\"scss\">\n// `cropper-crosshair` sizes itself in `em`, so without a size of its own the centre marker would\n// grow and shrink with whatever font the surrounding admin happens to use. 7px is the plus sign\n// cropper.js v1 drew through `.cropper-center`.\n.a-cropper cropper-crosshair {\n font-size: 7px;\n}\n\n// cropper.js v1 drew a `.cropper-line` along each edge of the crop box: a 5px band of the selection\n// colour at a tenth opacity, sitting 3px outside the border and 2px inside it. It is what made the\n// thin outline read as a grabbable edge, so it is drawn here too — as a pair of shadows rather than\n// four elements, which keeps it independent of how the v2 grips happen to be sized.\n.a-cropper cropper-selection[outlined] {\n box-shadow:\n inset 0 0 0 2px rgb(51 153 255 / 10%),\n 0 0 0 3px rgb(51 153 255 / 10%);\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;A,GAwFMgB,KAAiB,IACjBC,KAAsB,KACtBC,IAAuB,KAGvBC,IAAkB,4BAClBC,KAAa,4BACbC,KAAa,4BACbC,KAAkB,6B;;;;;;;;;;;;;;;EAvDxB,IAAMG,IAAQC,GAoBRC,IAAOH;EAUb,KAAK,IAAMI,KAAW;GACpBrB;GACAM;GACAD;GACAF;GACAF;GACAG;GACAF;EACF,GACE,EAAQ,QAAQ;EAiBlB,IAAMoB,KAAiB;GACrB;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF,GAQMC,IAASC,GAA+B,QAAQ,GAChDC,IAAUC,GAAI,EAAI,GAEpBC,IAA6B,MAC7BC,IAAgB;GAAE,OAAO;GAAG,QAAQ;EAAE,GAEtCC,IAA+B,MAE/BC,IAAW,IAEXC,IAAW,IAEXC,IAAa,GAEbC,IAAgB;GAAE,OAAO;GAAG,QAAQ;EAAE,GAEtCC,IAA+B,MAE/BC,IAA0C,MAExCC,KAAiC,MAAoB,SAAS,cAAcC,CAAI,GAGhFC,UAA0B;GAC9B,IAAI,CAACX,GAAO,OAAO;IAAE,GAAG;IAAG,GAAG;IAAG,OAAO;IAAG,QAAQ;GAAE;GACrD,IAAMY,IAAaZ,EAAM,OAAO,sBAAsB,GAChDa,IAAYb,EAAM,MAAM,sBAAsB;GACpD,OAAO;IACL,GAAGa,EAAU,OAAOD,EAAW;IAC/B,GAAGC,EAAU,MAAMD,EAAW;IAC9B,OAAOC,EAAU;IACjB,QAAQA,EAAU;GACpB;EACF,GAQMC,UAA+B;GACnC,IAAI,CAACd,GAAO,OAAO;IAAE,GAAG;IAAG,GAAG;IAAG,OAAO;IAAG,QAAQ;GAAE;GACrD,IAAMe,IAAQJ,EAAY,GACpBK,IAAShB,EAAM,OAAO,sBAAsB,GAC5CiB,IAAO,KAAK,IAAIF,EAAM,GAAG,CAAC,GAC1BG,IAAM,KAAK,IAAIH,EAAM,GAAG,CAAC,GACzBI,IAAQ,KAAK,IAAIJ,EAAM,IAAIA,EAAM,OAAOC,EAAO,KAAK,GACpDI,IAAS,KAAK,IAAIL,EAAM,IAAIA,EAAM,QAAQC,EAAO,MAAM;GAC7D,OAAO;IAAE,GAAGC;IAAM,GAAGC;IAAK,OAAO,KAAK,IAAI,GAAGC,IAAQF,CAAI;IAAG,QAAQ,KAAK,IAAI,GAAGG,IAASF,CAAG;GAAE;EAChG,GAEMG,UAA4B;GAChC,IAAI,CAACrB,GAAO,OAAO;IAAE,GAAG;IAAG,GAAG;IAAG,OAAO;IAAG,QAAQ;GAAE;GACrD,IAAM,EAAE,MAAG,MAAG,UAAO,cAAWA,EAAM;GACtC,OAAO;IAAE;IAAG;IAAG;IAAO;GAAO;EAC/B,GAEMsB,KAAa,MAAe;GAChC,AAAKtB,MACL,IAAW,IACXA,EAAM,UAAU,QAAQuB,EAAK,GAAGA,EAAK,GAAGA,EAAK,OAAOA,EAAK,QAAQhC,EAAM,aAAa,EAAI,GACxF,IAAW;EACb,GAOMiC,UAAoB;GACxB,IAAI,CAACxB,GAAO;GACZ,IAAM,EAAE,aAAUA,GACZ,EAAE,aAAU,oBAAiBe;GAOnC,AANA,IAAW,IACX,EAAM,WAAW,IACjB,EAAM,eAAe,IACrBA,EAAM,QAAQ,SAAS,GACvB,EAAM,WAAWU,GACjB,EAAM,eAAeC,GACrB,IAAW;EACb,GAQMC,WAAwB;GAC5B,IAAI,CAAC3B,GAAO;GACZ,IAAM4B,IAASjB,EAAY;GAC3B,IAAIiB,EAAO,SAAS,KAAKA,EAAO,UAAU,GAAG;GAC7C,IAAMC,IAAUC,EAAqBF,GAAQrC,EAAM,aAAaT,EAAc;GAC9E,EAAU;IAAE,GAAG+C;IAAS,GAAGA,EAAQ,IAAID,EAAO;IAAG,GAAGC,EAAQ,IAAID,EAAO;GAAE,CAAC;EAC5E,GAGMG,UAAyB;GAC7B,IAAMC,IAAQzC,EAAM;GAGpB,AAFIyC,KAASA,EAAM,QAAQ,KAAKA,EAAM,SAAS,IAAGC,EAAUD,CAAK,IAC5DL,GAAgB,GACrBO,EAAS;EACX,GAGMC,WAAc;GAClB,IAAI,CAACnC,KAASI,GAAU;GACxB,IAAMgC,IAAUf,EAAc;GAC9B,IAAIe,EAAQ,SAAS,KAAKA,EAAQ,UAAU,GAAG;GAC/C,IAAMC,IAASC,EAAgBF,GAAStB,EAAiB,GAAGvB,EAAM,WAAW;GAC7E,AAAKgD,EAAcH,GAASC,CAAM,KAAGf,EAAUe,CAAM;EACvD,GAEMG,KAAqB,MAAiB;GAC1C,IAAIrC,KAAY,CAACH,GAAO;GACxB,IAAMyC,IAAUC,EAA4B;GAC5C,IAAI,CAACD,GAAQ;GAIb,IAAME,IAAc3C,EAAM,OAA4C,SAChE4C,IAAS,OAAOD,KAAe,YAAYA,IAAaA,IAAazC,GACrE2C,IAASD,IAASE,EAAeF,KAAU,KAAA,GAC3CG,IAAUT,EAAgBG,GAAQ3B,EAAiB,GAAGvB,EAAM,aAAasD,CAAM;GACrF,AAAIN,EAAcE,GAAQM,CAAO,MAGjCL,EAAM,eAAe,GACrBpB,EAAUyB,CAAO;EACnB,GAEMC,KAAiB,MAAiB;GACtC,IAAiBN,EAA2C,QAAQ,UAAU;EAChF,GAEMO,KAAY,MAAiB;GACjC,IAAML,IAAUF,EAA2C,QAAQ;GAGnE,AAAIE,MAAQ,IAAgBA;EAC9B,GAEMM,UAAoB;GAExB,AADA,IAAgB,MAChBhB,EAAS,EAAI;EACf,GAOMiB,MAAkB,MAAsB;GAC5C,EAAM,gBAAgB;EACxB,GAQMC,UAAyB,eAAejB,EAAK,GAE7CkB,UAAiB;GACrB,IAAIrD,GAAO;IACT,IAAM,EAAE,WAAQ,UAAO,iBAAcA;IAKrC,AAJAsD,EAAU,oBAAoB,UAAUd,CAAiB,GACzDxB,EAAO,oBAAoB,eAAegC,CAAa,GACvDhC,EAAO,oBAAoB,UAAUiC,CAAQ,GAC7CjC,EAAO,oBAAoB,aAAakC,CAAW,GACnDnC,EAAM,oBAAoB,aAAaqC,CAAgB;GACzD;GAgBA,AAbAxD,EAAO,OAAO,iBAAiBvB,EAAc,KAAK,CAAC,CAAC,SAAS,MAAW;IAMtE,AAHA2C,EAAO,iBAA+B1C,EAAa,KAAK,CAAC,CAAC,SAAS,MAAU;KAC3E,AAAIyC,EAAM,WAAQ,EAAM,OAAO,MAAM;IACvC,CAAC,GACDC,EAAO,OAAO;GAChB,CAAC,GACDR,GAAkB,WAAW,GAC7B,IAAmB,MACnB,IAAQ,MACR,IAAU;IAAE,OAAO;IAAG,QAAQ;GAAE,GAChC,IAAU;IAAE,OAAO;IAAG,QAAQ;GAAE,GAChC,IAAgB;EAClB,GAUM+C,KAAuB,MAAqC;GAChE,IAAMC,IAAuB,CAAC,GAC1BC,IAAWC,EAAK;GACpB,OAAOD,KAAYA,MAAa,SAAS,QAAQA,MAAa,SAAS,kBAAiB;IACtF,IAAM,EAAE,iBAAc,OAAO,iBAAiBA,CAAQ;IAEtD,CADIE,MAAc,YAAYA,MAAc,WAAQH,EAAM,KAAKC,CAAQ,GACvE,IAAWA,EAAS;GACtB;GACA,OAAOD;EACT,GAEMI,KAAmB,MAA8B;GACrD,IAAIC,IAAYH,EAAK,cACfI,IAAUJ,EAAK,sBAAsB,CAAC,CAAC;GAW7C,OATAH,EAAoBG,CAAI,CAAC,CAAC,SAAS,MAAa;IAC9C,IAAMnC,IAAOkC,EAAS,sBAAsB,GAItCM,IAAOxC,EAAK,SAAS,KAAK,IAAIA,EAAK,KAAKuC,CAAO;IACrD,AAAIC,IAAO,MAAG,IAAY,KAAK,IAAIF,GAAWE,CAAI;GACpD,CAAC,GAEMF;EACT,GAWMG,KAAc,GAAuB,GAAmB,MAAiC;GAC7F,IAAMC,IAAOC,EAAcjE,GAASkE,GAAgBpF,IAAqBC,CAAoB;GAE7F,AADA,EAAO,MAAM,QAAQ,GAAGiF,EAAK,MAAM,KACnC,EAAO,MAAM,SAAS,GAAGA,EAAK,OAAO;GAErC,IAAMG,IAAgBR,EAAgBF,CAAI;GAK1C,OAJIU,IAAgB,KAAKA,IAAgBH,EAAK,WAC5C,EAAK,SAAS,KAAK,IAAIG,GAAepF,CAAoB,GAC1D,EAAO,MAAM,SAAS,GAAGiF,EAAK,OAAO,MAEhCA;EACT,GAEMI,MAA0B,MAAgC;GAC9D,IAAMC,IAAO7D,EAAoB/B,EAAY,KAAK;GAMlD,AALA,EAAK,OAAO,GACZ,EAAK,UAAU,GACf,EAAK,UAAU,IACf,EAAK,aAAaS,IAClBmF,EAAK,aAAa,QAAQ,MAAM,GAChChB,EAAU,YAAYgB,CAAI;GAE1B,IAAMC,IAAY9D,EAAyB9B,EAAiB,KAAK;GAGjE,AAFA,EAAU,WAAW,IACrB,EAAU,aAAaS,IACvBkE,EAAU,YAAYiB,CAAS;GAE/B,IAAMC,IAAO/D,EAAsBjC,EAAc,KAAK;GAKtD,AAJA,EAAK,SAAS,QACd,EAAK,aAAaU,IAClBoE,EAAU,YAAYkB,CAAI,GAE1B7E,GAAe,SAAS,MAAW;IACjC,IAAM8E,IAAShE,EAAsBjC,EAAc,KAAK;IAGxD,AAFA,EAAO,SAASoE,GAChB,EAAO,aAAa3D,GACpBqE,EAAU,YAAYmB,CAAM;GAC9B,CAAC;EACH,GAEMC,IAAQ,YAAY;GACxB,IAAMC,IAAQ,EAAEtE;GAEhB,AADAgD,EAAS,GACT,EAAQ,QAAQ;GAEhB,IAAMK,IAAO9D,EAAO;GACpB,IAAI,CAAC8D,KAAQ,CAACnE,EAAM,KAAK;IAEvB,EAAQ,QAAQ;IAChB;GACF;GAEA,IAAMyB,IAASP,EAAsBpC,EAAc,KAAK;GAIxD,AAHA,EAAO,aAAa,IACpB,EAAO,aAAaY,GACpB,EAAO,MAAM,UAAU,SACvB+E,EAAWhD,GAAQ0C,GAAMA,EAAK,WAAW;GAEzC,IAAM3C,IAAQN,EAAqBnC,EAAa,KAAK;GAOrD,AANA,EAAM,MAAM,IACZ,EAAM,aAAa,WACnB,EAAM,YAAY,IAClB,EAAM,WAAW,IACjB,EAAM,eAAe,IACrB,EAAM,WAAW,IACjB,EAAM,MAAMiB,EAAM;GAElB,IAAMqF,IAAQnE,EAAqBlC,EAAa,KAAK;GAGrD,AAFA,EAAM,aAAagB,EAAM,YAEzB,EAAM,SAAS;GAEf,IAAMsF,IAAepE,EAAsBjC,EAAc,KAAK;GAE9D,AADA,EAAa,SAAS,UACtB,EAAa,QAAQ;GAErB,IAAM8E,IAAY7C,EAAyBhC,EAAiB,KAAK;GAyBjE,AAxBA,EAAU,aAAaQ,GACvB,EAAU,UAAU,IACpB,EAAU,YAAY,IACtB,EAAU,WAAW,IAGrB,EAAU,UAAU,IACpB,EAAU,cAAcM,EAAM,aAC9B8E,GAAuBf,CAAS,GAIhCtC,EAAO,iBAAiB,eAAegC,CAAa,GACpDhC,EAAO,iBAAiB,UAAUiC,CAAQ,GAC1CjC,EAAO,iBAAiB,aAAakC,CAAW,GAChDnC,EAAM,iBAAiB,aAAaqC,CAAgB,GACpDE,EAAU,iBAAiB,UAAUd,CAAiB,GAEtDxB,EAAO,OAAOD,GAAO6D,GAAOC,GAAcvB,CAAS,GAInDI,EAAK,YAAY1C,CAAM,GAEvBgD,EAAWhD,GAAQ0C,GAAMA,EAAK,WAAW;GAEzC,IAAIoB;GACJ,IAAI;IAEF,IAAS,MAAM/D,EAAM,OAAO;GAC9B,QAAQ;IAGN,AADAC,EAAO,OAAO,GACV2D,MAAUtE,MAAY,EAAQ,QAAQ;IAC1C;GACF;GACA,AAAIsE,MAAUtE,KAKd,IAAQ;IAAE;IAAQ;IAAO;GAAU,GAGnC,IAAU;IAAE,OAAOyE,EAAO;IAAc,QAAQA,EAAO;GAAc,GACrEd,EAAWhD,GAAQ0C,GAAMA,EAAK,WAAW,GAIzC,EAAM,aAAanE,EAAM,YACzB,EAAU,cAAcA,EAAM,aAE9BiC,EAAY,GAEZ,IAAUuD,EAAQrB,CAAI,GACtBsB,EAAiBtB,CAAI,GACrB3B,EAAiB,GAEjB,EAAQ,QAAQ,IAChBtC,EAAK,OAAO,KAtBVuB,EAAO,OAAO;EAuBlB,GAGMiE,WAAqC;GACzC,IAAI,CAACjF,GAAO,OAAO;GACnB,IAAM4B,IAASjB,EAAY;GAC3B,IAAIiB,EAAO,SAAS,KAAKA,EAAO,UAAU,GAAG,OAAO;GACpD,IAAML,IAAOF,EAAc;GAC3B,OAAO;IACL,IAAIE,EAAK,IAAIK,EAAO,KAAKA,EAAO;IAChC,IAAIL,EAAK,IAAIK,EAAO,KAAKA,EAAO;IAChC,OAAOL,EAAK,QAAQK,EAAO;IAC3B,QAAQL,EAAK,SAASK,EAAO;GAC/B;EACF,GAGMK,KAAa,MAAmB;GACpC,IAAML,IAASjB,EAAY;GAC3B,AAAI,CAACX,KAAS4B,EAAO,SAAS,KAAKA,EAAO,UAAU,KACpDN,EACEgB,EACE;IACE,GAAGV,EAAO,IAAIsD,EAAK,IAAItD,EAAO;IAC9B,GAAGA,EAAO,IAAIsD,EAAK,IAAItD,EAAO;IAC9B,OAAOsD,EAAK,QAAQtD,EAAO;IAC3B,QAAQsD,EAAK,SAAStD,EAAO;GAC/B,GACAd,EAAiB,GACjBvB,EAAM,WACR,CACF;EACF,GAEM4F,MAAiB,GAAoB,MACzCC,MAAM,QACNC,MAAM,QACN,KAAK,IAAID,EAAE,IAAIC,EAAE,CAAC,IAAI,QACtB,KAAK,IAAID,EAAE,IAAIC,EAAE,CAAC,IAAI,QACtB,KAAK,IAAID,EAAE,QAAQC,EAAE,KAAK,IAAI,QAC9B,KAAK,IAAID,EAAE,SAASC,EAAE,MAAM,IAAI,MAO5BnD,KAAY,IAAa,OAAU;GACvC,IAAMgD,IAAOD,GAAY;GACzB,AAAKC,OACDI,KAAc,CAACH,GAAcD,GAAM3E,CAAW,OAChD,IAAc2E,GACdzF,EAAK,qBAAqByF,CAAI,IAE5BI,KAAY7F,EAAK,UAAUyF,CAAI;EACrC,GAGMH,KAAW,OAA6B;GAAE,OAAOrB,EAAK;GAAa,QAAQE,EAAgBF,CAAI;EAAE,IAGjG6B,WAAiB;GACrB,IAAM7B,IAAO9D,EAAO;GACpB,IAAI,CAACI,KAAS,CAAC0D,GAAM;GAGrB,IAAM8B,IAASP,GAAY,KAAK1F,EAAM,YAEhC0E,IAAOD,EAAWhE,EAAM,QAAQ0D,GAAMA,EAAK,WAAW;GAM5D,AALA,IAAUqB,EAAQrB,CAAI,GAGtBsB,EAAiBtB,CAAI,GACrBlC,EAAY,GACR,IAAK,SAAS,KAAKyC,EAAK,UAAU,OAElCuB,KAAUA,EAAO,QAAQ,IAAGvD,EAAUuD,CAAM,IAC3CzD,EAAiB,GACtBG,EAAS;EACX,GAEMuD,WAAyB;GAC7B,IAAM/B,IAAO9D,EAAO;GACpB,IAAI,CAAC8D,GAAM;GACX,IAAMgC,IAAMX,EAAQrB,CAAI;GACxB,AAAIgC,EAAI,SAAS,KAAKA,EAAI,UAAU,MAGhCA,EAAI,UAAUpF,EAAQ,SAASoF,EAAI,WAAWpF,EAAQ,WACrDN,KAGL,4BAA4BuF,GAAS,CAAC;EACxC;EAIA,GAAkB3F,GAAQ6F,EAAgB;EAG1C,IAAMT,KAAoB,MAAsB;GAC9C,GAAkB,WAAW;GAC7B,IAAMW,IAAYpC,EAAoBG,CAAI;GAC1C,AAAIiC,EAAU,WAAW,IACvB,IAAmB,QAGrB,IAAmB,IAAI,eAAeF,EAAgB,GACtDE,EAAU,SAAS,MAAanF,GAAkB,QAAQiD,CAAQ,CAAC;EACrE;E,OAEAmC,QACQrG,EAAM,WACN;GACJ,EAAM;EACR,CACF,GAEAqG,QACQrG,EAAM,aACX,MAAU;GAGT,AAAKS,KAAUgC,KAAS,IAAcA,GAAOzB,CAAW,MACxD0B,EAAUD,CAAK,GACfE,EAAS;EACX,GACA,EAAE,MAAM,GAAK,CACf,GAEA0D,QACQrG,EAAM,aACX,MAAe;GACd,IAAMqF,IAAQ5E,GAAO,OAAO,cAA4BzB,EAAa,KAAK;GAC1E,AAAIqG,MAAO,EAAM,aAAaiB;EAChC,CACF,GAEAD,QACQrG,EAAM,cACX,MAAgB;GAGf,IAFI,CAACS,MACL,EAAM,UAAU,cAAc8F,GAC1B,CAACC,EAAoBD,CAAW,IAAG;GACvC,IAAM1D,IAAUf,EAAc;GAC9B,EAAUiB,EAAgB;IAAE,GAAGF;IAAS,QAAQA,EAAQ,QAAQ0D;GAAY,GAAGhF,EAAiB,GAAGgF,CAAW,CAAC;EACjH,CACF,GAEAE,SAAgB;GACd,EAAM;EACR,CAAC,GAEDC,SAAsB;GAEpB,AADA,KAAc,GACd5C,EAAS;EACX,CAAC,I,UAICvF,EAAA,GAAAC,EAiBM,OAjBNa,IAiBM,CAfIkB,EAAA,SADRhC,EAAA,GAAAC,EAQM,OARNc,IAQM,CAJJb,GAGEC,IAAA;GAFA,OAAM;GACN,eAAA;E,oBAIJE,GAKE,OAAA;GAJI,SAAA;GAAJ,KAAIyB;GACJ,OAAM;GACL,OAAKxB,GAAEoB,EAAA,cAAc;GACN"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getAdjustedSizes as e } from "../../../../vendor/@cropper/utils/dist/utils.esm.raw.js";
|
|
2
|
-
import "../../../../vendor/
|
|
2
|
+
import "../../../../vendor/cropperjs/dist/cropper.esm.raw.js";
|
|
3
3
|
//#region src/domains/dam/cropper/utils/cropperGeometry.ts
|
|
4
4
|
var t = {
|
|
5
5
|
"n-resize": ["center", "end"],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cropperGeometry.js","names":["RESIZE_ANCHORS","isUsableAspectRatio","aspectRatio","clamp","value","min","max","fitAspectRatio","size","initialSelectionRect","fitted","bounds","width","coverage","height","coverAspectRatio","getAdjustedSizes","availableAlong","boundsEnd","boundsStart","boundsLength","side","start","length","placeAlong","newLength","clampRectInside","anchorX","anchorY","anchor","asked","rect","maxWidth","maxHeight","scale","x","y","rectsAreClose","a","b","epsilon","canvasSizeFor","availableWidth","minWidth","natural","minHeight","displayedHeight"],"sources":["../../../../../src/domains/dam/cropper/utils/cropperGeometry.ts"],"sourcesContent":["/**\n * The geometry behind `ACropper`: how big the canvas is, where the initial crop box lands, and how a\n * crop box is kept inside the image. cropper.js v1 did all of this internally and v2 does none of\n * it, so it has to live somewhere — and here, away from the DOM, it can be tested directly rather\n * than through a mounted component.\n */\nimport { getAdjustedSizes } from 'cropperjs2'\n\nexport interface Size {\n width: number\n height: number\n}\n\nexport interface Rect {\n x: number\n y: number\n width: number\n height: number\n}\n\n/**\n * Which edge of the selection stays put while the other one moves. Resizing from the west handle\n * pins the east edge, resizing from the north-east corner pins the south-west one, and the side\n * handles grow symmetrically around the perpendicular centre — this mirrors how cropper.js v2\n * computes a resize, and it is what lets a clamped selection stop at the image border instead of\n * sliding along it.\n *\n * `fit` is the anchor-less case (a programmatic `setData`, a plain move, a re-fit after the\n * container changed): the box may be as large as the image allows anywhere, and is slid back into\n * view afterwards, which is what cropper.js v1's `limitCropBox` did.\n */\nexport type AnchorSide = 'start' | 'center' | 'end' | 'fit'\n\nexport type Anchor = [AnchorSide, AnchorSide]\n\n/** Anchors per cropper.js action name. Anything missing is treated as a plain \"fit it in\" clamp. */\nexport const RESIZE_ANCHORS: Readonly<Record<string, Anchor>> = {\n 'n-resize': ['center', 'end'],\n 's-resize': ['center', 'start'],\n 'e-resize': ['start', 'center'],\n 'w-resize': ['end', 'center'],\n 'ne-resize': ['start', 'end'],\n 'nw-resize': ['end', 'end'],\n 'se-resize': ['start', 'start'],\n 'sw-resize': ['end', 'start'],\n // Drawing a brand new selection grows it away from the point the drag started at.\n select: ['start', 'start'],\n}\n\nexport const isUsableAspectRatio = (aspectRatio: number): boolean => Number.isFinite(aspectRatio) && aspectRatio > 0\n\nconst clamp = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max)\n\n/**\n * The largest box of `aspectRatio` that fits inside `size`. With no usable ratio the size is\n * returned untouched, which is how a free-form crop behaves.\n */\nexport const fitAspectRatio = (size: Size, aspectRatio: number): Size => {\n if (!isUsableAspectRatio(aspectRatio)) return { width: size.width, height: size.height }\n return size.height * aspectRatio > size.width\n ? { width: size.width, height: size.width / aspectRatio }\n : { width: size.height * aspectRatio, height: size.height }\n}\n\n/**\n * The box the crop starts out as: cropper.js v1's `autoCropArea` applied to the aspect-fitted\n * image, centred. Because `fitAspectRatio` scales linearly this is the same rectangle whether it is\n * computed in natural pixels or in displayed ones — which is why the crop data a user sees does not\n * depend on how wide their window happens to be.\n */\nexport const initialSelectionRect = (bounds: Size, aspectRatio: number, coverage: number): Rect => {\n const fitted = fitAspectRatio(bounds, aspectRatio)\n const width = fitted.width * coverage\n const height = fitted.height * coverage\n return {\n x: (bounds.width - width) / 2,\n y: (bounds.height - height) / 2,\n width,\n height,\n }\n}\n\n/**\n * Grows a box to the given ratio, by calling the very function cropper.js' `$change` will call on\n * it. Doing it here first means a rectangle is already the shape the library will make it *before*\n * it is measured against the image: a crop that names a new height keeps its old width through the\n * clamp otherwise, and cropper.js then widens it to suit the ratio — straight past the image edge.\n *\n * The guard is not redundant. `getAdjustedSizes` has no notion of a free-form crop and answers a\n * `NaN` ratio with a `NaN` dimension.\n */\nexport const coverAspectRatio = (size: Size, aspectRatio: number): Size =>\n isUsableAspectRatio(aspectRatio)\n ? getAdjustedSizes({ width: size.width, height: size.height, aspectRatio }, 'cover')\n : { width: size.width, height: size.height }\n\n/** How much room an axis has left once the anchored edge is held still. */\nconst availableAlong = (start: number, length: number, boundsStart: number, boundsLength: number, side: AnchorSide) => {\n const boundsEnd = boundsStart + boundsLength\n if (side === 'start') return Math.max(0, boundsEnd - Math.max(start, boundsStart))\n if (side === 'end') return Math.max(0, Math.min(start + length, boundsEnd) - boundsStart)\n // `center` and `fit` are both free to use the whole span; only where they land differs.\n return boundsLength\n}\n\n/** Where the axis ends up once its length is known and the anchored edge has to stay put. */\nconst placeAlong = (start: number, length: number, newLength: number, side: AnchorSide) => {\n if (side === 'end') return start + length - newLength\n if (side === 'center') return start + length / 2 - newLength / 2\n return start\n}\n\n/**\n * Keeps `rect` inside `bounds` the way cropper.js v1's `viewMode: 1` did: a selection that has\n * grown past an edge is shrunk rather than pushed, so the edge the user is not dragging stays where\n * they put it, and a selection that is merely out of position is slid back in whole.\n *\n * `anchor` says which edges the current gesture is holding still. Leaving it out (a programmatic\n * `setData`, the initial layout, a re-fit after a resize) caps the size at the bounds, keeps the\n * top-left corner it was given and slides the result into view — what v1's `limitCropBox` did.\n */\nexport const clampRectInside = (rect: Rect, bounds: Rect, aspectRatio: number, anchor?: Anchor): Rect => {\n const [anchorX, anchorY] = anchor ?? ['fit', 'fit']\n\n // Measure the rectangle cropper.js would end up with, not the one that was asked for. A rectangle\n // already in the right proportion — which is everything a drag produces — is untouched by this.\n const asked = { ...rect, ...coverAspectRatio(rect, aspectRatio) }\n\n const maxWidth = availableAlong(asked.x, asked.width, bounds.x, bounds.width, anchorX)\n const maxHeight = availableAlong(asked.y, asked.height, bounds.y, bounds.height, anchorY)\n\n let { width, height } = asked\n if (isUsableAspectRatio(aspectRatio)) {\n // Shrink both axes by the same factor, otherwise the crop silently changes shape at the border.\n const scale = Math.min(1, width > 0 ? maxWidth / width : 1, height > 0 ? maxHeight / height : 1)\n width *= scale\n height *= scale\n } else {\n width = Math.min(width, maxWidth)\n height = Math.min(height, maxHeight)\n }\n\n const x = placeAlong(asked.x, asked.width, width, anchorX)\n const y = placeAlong(asked.y, asked.height, height, anchorY)\n\n return {\n x: clamp(x, bounds.x, bounds.x + bounds.width - width),\n y: clamp(y, bounds.y, bounds.y + bounds.height - height),\n width,\n height,\n }\n}\n\nexport const rectsAreClose = (a: Rect, b: Rect, epsilon = 0.01): boolean =>\n Math.abs(a.x - b.x) < epsilon &&\n Math.abs(a.y - b.y) < epsilon &&\n Math.abs(a.width - b.width) < epsilon &&\n Math.abs(a.height - b.height) < epsilon\n\n/**\n * The box the cropper canvas takes. This is the frame, not the picture: cropper.js v1 let its\n * container fill the width it was given and then centred the image inside it, so a source narrower\n * than the container sat between two shaded margins rather than stretching to fill them. The height\n * comes from the image at its displayed size, which is its natural size unless that is wider than\n * the space available — v1 never enlarged a source either, its `<img>` being capped at `max-width`.\n *\n * The image is fitted into this box with `contain`, so image pixels and canvas pixels differ by one\n * uniform scale factor and an offset.\n */\nexport const canvasSizeFor = (natural: Size, availableWidth: number, minWidth: number, minHeight: number): Size => {\n const width = Math.max(availableWidth, minWidth)\n if (!(natural.width > 0) || !(natural.height > 0)) {\n return { width, height: minHeight }\n }\n const displayedWidth = Math.min(natural.width, availableWidth)\n const displayedHeight = (displayedWidth * natural.height) / natural.width\n return { width, height: Math.max(displayedHeight, minHeight) }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cropperGeometry.js","names":["RESIZE_ANCHORS","isUsableAspectRatio","aspectRatio","clamp","value","min","max","fitAspectRatio","size","initialSelectionRect","fitted","bounds","width","coverage","height","coverAspectRatio","getAdjustedSizes","availableAlong","boundsEnd","boundsStart","boundsLength","side","start","length","placeAlong","newLength","clampRectInside","anchorX","anchorY","anchor","asked","rect","maxWidth","maxHeight","scale","x","y","rectsAreClose","a","b","epsilon","canvasSizeFor","availableWidth","minWidth","natural","minHeight","displayedHeight"],"sources":["../../../../../src/domains/dam/cropper/utils/cropperGeometry.ts"],"sourcesContent":["/**\n * The geometry behind `ACropper`: how big the canvas is, where the initial crop box lands, and how a\n * crop box is kept inside the image. cropper.js v1 did all of this internally and v2 does none of\n * it, so it has to live somewhere — and here, away from the DOM, it can be tested directly rather\n * than through a mounted component.\n */\nimport { getAdjustedSizes } from 'cropperjs'\n\nexport interface Size {\n width: number\n height: number\n}\n\nexport interface Rect {\n x: number\n y: number\n width: number\n height: number\n}\n\n/**\n * Which edge of the selection stays put while the other one moves. Resizing from the west handle\n * pins the east edge, resizing from the north-east corner pins the south-west one, and the side\n * handles grow symmetrically around the perpendicular centre — this mirrors how cropper.js v2\n * computes a resize, and it is what lets a clamped selection stop at the image border instead of\n * sliding along it.\n *\n * `fit` is the anchor-less case (a programmatic `setData`, a plain move, a re-fit after the\n * container changed): the box may be as large as the image allows anywhere, and is slid back into\n * view afterwards, which is what cropper.js v1's `limitCropBox` did.\n */\nexport type AnchorSide = 'start' | 'center' | 'end' | 'fit'\n\nexport type Anchor = [AnchorSide, AnchorSide]\n\n/** Anchors per cropper.js action name. Anything missing is treated as a plain \"fit it in\" clamp. */\nexport const RESIZE_ANCHORS: Readonly<Record<string, Anchor>> = {\n 'n-resize': ['center', 'end'],\n 's-resize': ['center', 'start'],\n 'e-resize': ['start', 'center'],\n 'w-resize': ['end', 'center'],\n 'ne-resize': ['start', 'end'],\n 'nw-resize': ['end', 'end'],\n 'se-resize': ['start', 'start'],\n 'sw-resize': ['end', 'start'],\n // Drawing a brand new selection grows it away from the point the drag started at.\n select: ['start', 'start'],\n}\n\nexport const isUsableAspectRatio = (aspectRatio: number): boolean => Number.isFinite(aspectRatio) && aspectRatio > 0\n\nconst clamp = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max)\n\n/**\n * The largest box of `aspectRatio` that fits inside `size`. With no usable ratio the size is\n * returned untouched, which is how a free-form crop behaves.\n */\nexport const fitAspectRatio = (size: Size, aspectRatio: number): Size => {\n if (!isUsableAspectRatio(aspectRatio)) return { width: size.width, height: size.height }\n return size.height * aspectRatio > size.width\n ? { width: size.width, height: size.width / aspectRatio }\n : { width: size.height * aspectRatio, height: size.height }\n}\n\n/**\n * The box the crop starts out as: cropper.js v1's `autoCropArea` applied to the aspect-fitted\n * image, centred. Because `fitAspectRatio` scales linearly this is the same rectangle whether it is\n * computed in natural pixels or in displayed ones — which is why the crop data a user sees does not\n * depend on how wide their window happens to be.\n */\nexport const initialSelectionRect = (bounds: Size, aspectRatio: number, coverage: number): Rect => {\n const fitted = fitAspectRatio(bounds, aspectRatio)\n const width = fitted.width * coverage\n const height = fitted.height * coverage\n return {\n x: (bounds.width - width) / 2,\n y: (bounds.height - height) / 2,\n width,\n height,\n }\n}\n\n/**\n * Grows a box to the given ratio, by calling the very function cropper.js' `$change` will call on\n * it. Doing it here first means a rectangle is already the shape the library will make it *before*\n * it is measured against the image: a crop that names a new height keeps its old width through the\n * clamp otherwise, and cropper.js then widens it to suit the ratio — straight past the image edge.\n *\n * The guard is not redundant. `getAdjustedSizes` has no notion of a free-form crop and answers a\n * `NaN` ratio with a `NaN` dimension.\n */\nexport const coverAspectRatio = (size: Size, aspectRatio: number): Size =>\n isUsableAspectRatio(aspectRatio)\n ? getAdjustedSizes({ width: size.width, height: size.height, aspectRatio }, 'cover')\n : { width: size.width, height: size.height }\n\n/** How much room an axis has left once the anchored edge is held still. */\nconst availableAlong = (start: number, length: number, boundsStart: number, boundsLength: number, side: AnchorSide) => {\n const boundsEnd = boundsStart + boundsLength\n if (side === 'start') return Math.max(0, boundsEnd - Math.max(start, boundsStart))\n if (side === 'end') return Math.max(0, Math.min(start + length, boundsEnd) - boundsStart)\n // `center` and `fit` are both free to use the whole span; only where they land differs.\n return boundsLength\n}\n\n/** Where the axis ends up once its length is known and the anchored edge has to stay put. */\nconst placeAlong = (start: number, length: number, newLength: number, side: AnchorSide) => {\n if (side === 'end') return start + length - newLength\n if (side === 'center') return start + length / 2 - newLength / 2\n return start\n}\n\n/**\n * Keeps `rect` inside `bounds` the way cropper.js v1's `viewMode: 1` did: a selection that has\n * grown past an edge is shrunk rather than pushed, so the edge the user is not dragging stays where\n * they put it, and a selection that is merely out of position is slid back in whole.\n *\n * `anchor` says which edges the current gesture is holding still. Leaving it out (a programmatic\n * `setData`, the initial layout, a re-fit after a resize) caps the size at the bounds, keeps the\n * top-left corner it was given and slides the result into view — what v1's `limitCropBox` did.\n */\nexport const clampRectInside = (rect: Rect, bounds: Rect, aspectRatio: number, anchor?: Anchor): Rect => {\n const [anchorX, anchorY] = anchor ?? ['fit', 'fit']\n\n // Measure the rectangle cropper.js would end up with, not the one that was asked for. A rectangle\n // already in the right proportion — which is everything a drag produces — is untouched by this.\n const asked = { ...rect, ...coverAspectRatio(rect, aspectRatio) }\n\n const maxWidth = availableAlong(asked.x, asked.width, bounds.x, bounds.width, anchorX)\n const maxHeight = availableAlong(asked.y, asked.height, bounds.y, bounds.height, anchorY)\n\n let { width, height } = asked\n if (isUsableAspectRatio(aspectRatio)) {\n // Shrink both axes by the same factor, otherwise the crop silently changes shape at the border.\n const scale = Math.min(1, width > 0 ? maxWidth / width : 1, height > 0 ? maxHeight / height : 1)\n width *= scale\n height *= scale\n } else {\n width = Math.min(width, maxWidth)\n height = Math.min(height, maxHeight)\n }\n\n const x = placeAlong(asked.x, asked.width, width, anchorX)\n const y = placeAlong(asked.y, asked.height, height, anchorY)\n\n return {\n x: clamp(x, bounds.x, bounds.x + bounds.width - width),\n y: clamp(y, bounds.y, bounds.y + bounds.height - height),\n width,\n height,\n }\n}\n\nexport const rectsAreClose = (a: Rect, b: Rect, epsilon = 0.01): boolean =>\n Math.abs(a.x - b.x) < epsilon &&\n Math.abs(a.y - b.y) < epsilon &&\n Math.abs(a.width - b.width) < epsilon &&\n Math.abs(a.height - b.height) < epsilon\n\n/**\n * The box the cropper canvas takes. This is the frame, not the picture: cropper.js v1 let its\n * container fill the width it was given and then centred the image inside it, so a source narrower\n * than the container sat between two shaded margins rather than stretching to fill them. The height\n * comes from the image at its displayed size, which is its natural size unless that is wider than\n * the space available — v1 never enlarged a source either, its `<img>` being capped at `max-width`.\n *\n * The image is fitted into this box with `contain`, so image pixels and canvas pixels differ by one\n * uniform scale factor and an offset.\n */\nexport const canvasSizeFor = (natural: Size, availableWidth: number, minWidth: number, minHeight: number): Size => {\n const width = Math.max(availableWidth, minWidth)\n if (!(natural.width > 0) || !(natural.height > 0)) {\n return { width, height: minHeight }\n }\n const displayedWidth = Math.min(natural.width, availableWidth)\n const displayedHeight = (displayedWidth * natural.height) / natural.width\n return { width, height: Math.max(displayedHeight, minHeight) }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KeywordFactory.js","names":["useDamKeywordFactory","extSystemId","reviewed","dateTimeNow","ENTITY","SYSTEM_CORE_DAM"],"sources":["../../../../../src/domains/dam/keyword/factory/KeywordFactory.ts"],"sourcesContent":["import type { DamKeyword } from '@/domains/dam/keyword/types/DamKeyword'\nimport { dateTimeNow } from '@/shared/utils/datetime'\nimport { ENTITY } from '@/domains/dam/keyword/api/keywordApi'\nimport { SYSTEM_CORE_DAM } from '@/domains/dam/api/damConstants'\n\nexport function useDamKeywordFactory() {\n const createDefault = (extSystemId: number, reviewed?: boolean): DamKeyword => {\n return {\n id: '',\n name: '',\n extSystem: extSystemId,\n flags: {\n reviewed: reviewed ?? false,\n },\n createdAt: dateTimeNow(),\n modifiedAt: dateTimeNow(),\n createdBy:
|
|
1
|
+
{"version":3,"file":"KeywordFactory.js","names":["useDamKeywordFactory","extSystemId","reviewed","dateTimeNow","ENTITY","SYSTEM_CORE_DAM"],"sources":["../../../../../src/domains/dam/keyword/factory/KeywordFactory.ts"],"sourcesContent":["import type { DamKeyword } from '@/domains/dam/keyword/types/DamKeyword'\nimport { dateTimeNow } from '@/shared/utils/datetime'\nimport { ENTITY } from '@/domains/dam/keyword/api/keywordApi'\nimport { SYSTEM_CORE_DAM } from '@/domains/dam/api/damConstants'\n\nexport function useDamKeywordFactory() {\n const createDefault = (extSystemId: number, reviewed?: boolean): DamKeyword => {\n return {\n id: '',\n name: '',\n extSystem: extSystemId,\n flags: {\n reviewed: reviewed ?? false,\n },\n createdAt: dateTimeNow(),\n modifiedAt: dateTimeNow(),\n createdBy: null,\n modifiedBy: null,\n _resourceName: ENTITY,\n _system: SYSTEM_CORE_DAM,\n }\n }\n\n return {\n createDefault,\n }\n}\n"],"mappings":";;;;AAKA,SAAgBA,IAAuB;CAkBrC,OAAO,EACL,gBAlBqB,GAAqB,OACnC;EACL,IAAI;EACJ,MAAM;EACN,WAAWC;EACX,OAAO,EACL,UAAUC,KAAY,GACxB;EACA,WAAWC,EAAY;EACvB,YAAYA,EAAY;EACxB,WAAW;EACX,YAAY;EACZ,eAAeC;EACf,SAASC;CACX,GAKF;AACF"}
|
|
@@ -31,7 +31,7 @@ export declare function useFilterClearHelpers<F extends readonly MakeFilterOptio
|
|
|
31
31
|
clearAllFilterSelected: (filterData: FilterData<F>, filterConfig: FilterConfig<F>, filterSelected: Ref<Map<string, ValueObjectOption<string | number>[]>>) => void;
|
|
32
32
|
isClearable: (name: string, filterConfig: FilterConfig<F>) => boolean;
|
|
33
33
|
};
|
|
34
|
-
interface FilterHelpersMoreOptions {
|
|
34
|
+
export interface FilterHelpersMoreOptions {
|
|
35
35
|
storeFiltersLocalStorage: string | boolean;
|
|
36
36
|
populateUrlParams: boolean;
|
|
37
37
|
}
|
|
@@ -45,6 +45,36 @@ export declare function useFilterHelpers<F extends readonly MakeFilterOption<str
|
|
|
45
45
|
sortBy: DatatableSortBy;
|
|
46
46
|
} | null;
|
|
47
47
|
};
|
|
48
|
+
/**
|
|
49
|
+
* Provides the filter (`FilterConfigKey`, `FilterDataKey`) and `pagination` (`DatatablePaginationKey`) to the filter
|
|
50
|
+
* bar and `ADatatablePagination` below this component, and wires the list to them: `getList` is
|
|
51
|
+
* `fetchList(pagination, filterData, filterConfig)` debounced and resolves once that has (a refresh button can wait for
|
|
52
|
+
* it; a call that a newer one replaces within the debounce resolves at once, without a load of its own), submit, reset
|
|
53
|
+
* and sort go through `useFilterHelpers` (with `filterHelpersOptions`), and on mount the stored filter and the
|
|
54
|
+
* remembered page are restored before the first load.
|
|
55
|
+
*
|
|
56
|
+
* Only for a datatable that is the parent of its filter bar and does exactly this. Write the wiring out when the first
|
|
57
|
+
* load skips the stored filter, `getList` checks or adds anything, the sort is handled by hand, `fetchList` takes more
|
|
58
|
+
* than the list's three arguments, or a parent renders the filter bar (the parent provides then).
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* const { filterData, filterConfig } = useFaqListFilter()
|
|
62
|
+
* const { fetchList, listItems } = useFaqListActions()
|
|
63
|
+
* const { pagination } = usePagination(SORT_BY_ID)
|
|
64
|
+
* const { getList, sortByChange, submitFilterAction, resetFilterAction } = useDatatableList(
|
|
65
|
+
* filterData,
|
|
66
|
+
* filterConfig,
|
|
67
|
+
* pagination,
|
|
68
|
+
* fetchList
|
|
69
|
+
* )
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export declare function useDatatableList<F extends readonly MakeFilterOption<string>[] = readonly MakeFilterOption<string>[], R = unknown>(filterData: FilterData<F>, filterConfig: FilterConfig<F>, pagination: Ref<Pagination>, fetchList: (pagination: Ref<Pagination>, filterData: FilterData<F>, filterConfig: FilterConfig<F>) => R, filterHelpersOptions?: Partial<FilterHelpersMoreOptions>): {
|
|
73
|
+
getList: import('@vueuse/shared').UseDebounceFnReturn<() => R>;
|
|
74
|
+
sortByChange: () => void;
|
|
75
|
+
submitFilterAction: () => void;
|
|
76
|
+
resetFilterAction: () => void;
|
|
77
|
+
};
|
|
48
78
|
export type AllowedFilterValues = number | number[] | string | string[] | null | undefined | boolean;
|
|
49
79
|
export interface GeneralFilterOptions {
|
|
50
80
|
system?: string;
|
|
@@ -102,4 +132,3 @@ export type FilterConfig<F extends readonly MakeFilterOption<string>[] = readonl
|
|
|
102
132
|
export type FilterData<F extends readonly MakeFilterOption<string>[] = readonly MakeFilterOption<string>[]> = {
|
|
103
133
|
[P in F[number]['name']]: AllowedFilterValues;
|
|
104
134
|
};
|
|
105
|
-
export {};
|