From 2164b7909c758996174a3f1cc5d7e600d0abeeb7 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Sat, 20 Dec 2025 23:44:55 -0500 Subject: [PATCH 01/53] Clean up (es6 optimizations, memory leak on listeners) --- src/cropt.ts | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/cropt.ts b/src/cropt.ts index eb24c7e..b670164 100644 --- a/src/cropt.ts +++ b/src/cropt.ts @@ -34,9 +34,10 @@ class TransformOrigin { this.y = 0; return; } - var css = el.style.transformOrigin.split(" "); - this.x = parseFloat(css[0]); - this.y = parseFloat(css[1]); + + const [x, y] = el.style.transformOrigin.split(" "); + this.x = parseFloat(x) || 0; + this.y = parseFloat(y) || 0; } toString() { @@ -45,7 +46,7 @@ class TransformOrigin { } function debounce(func: T, wait: number) { - let timer = 0; + let timer: number | undefined; return (...args: any) => { clearTimeout(timer); timer = setTimeout(() => func(...args), wait); @@ -60,7 +61,7 @@ function setZoomerVal(value: number, zoomer: HTMLInputElement) { } function loadImage(src: string): Promise { - var img = new Image(); + const img = new Image(); return new Promise(function (resolve, reject) { img.onload = () => { @@ -143,12 +144,14 @@ export class Cropt { }, zoomerInputClass: "cr-slider", }; - #boundZoom: number | null = null; + #boundZoom: number | undefined = undefined; #scale = 1; #keyDownHandler: ((ev: KeyboardEvent) => void) | null = null; + #zoomInputHandler: (() => void) | null = null; + #wheelHandler: ((ev: WheelEvent) => void) | null = null; #updateOverlayDebounced = debounce(() => { this.#updateOverlay(); - }, 200); + }, 100); constructor(element: HTMLElement, options: RecursivePartial) { if (element.classList.contains("cropt-container")) { @@ -159,7 +162,7 @@ export class Cropt { options.viewport = { ...this.options.viewport, ...options.viewport }; } - this.options = { ...this.options, ...(options as CroptOptions) }; + this.options = { ...this.options, ...options } as CroptOptions; this.element = element; this.element.classList.add("cropt-container"); @@ -194,7 +197,7 @@ export class Cropt { * Bind an image from an src string. * Returns a Promise which resolves when the image has been loaded and state is initialized. */ - bind(src: string, zoom: number | null = null) { + bind(src: string, zoom: number | undefined = undefined) { if (!src) { throw new Error("src cannot be empty"); } @@ -283,11 +286,12 @@ export class Cropt { const curWidth = this.options.viewport.width; const curHeight = this.options.viewport.height; - if (options.viewport) { - options.viewport = { ...this.options.viewport, ...options.viewport }; - } + // if (options.viewport) { + // options.viewport = { ...this.options.viewport, ...options.viewport }; + // } - this.options = structuredClone({ ...this.options, ...(options as CroptOptions) }); + // changed: removed structuredClone: slow, and would fail passing functions in options + this.options = { ...this.options, ...options } as CroptOptions; this.#setOptionsCss(); if ( @@ -300,7 +304,7 @@ export class Cropt { setZoom(value: number) { setZoomerVal(value, this.elements.zoomer); - var event = new Event("input"); + const event = new Event("input"); this.elements.zoomer.dispatchEvent(event); // triggers this.#onZoom call } @@ -308,6 +312,12 @@ export class Cropt { if (this.#keyDownHandler) { document.removeEventListener("keydown", this.#keyDownHandler); } + if (this.#zoomInputHandler) { + this.elements.zoomer.removeEventListener("input", this.#zoomInputHandler); + } + if (this.#wheelHandler) { + this.elements.boundary.removeEventListener("wheel", this.#wheelHandler); + } this.element.removeChild(this.elements.boundary); this.element.classList.remove("cropt-container"); this.element.removeChild(this.elements.zoomerWrap); @@ -448,10 +458,7 @@ export class Cropt { if (pEventCache.length === 2) { let touch1 = pEventCache[0]; let touch2 = pEventCache[1]; - let dist = Math.sqrt( - (touch1.pageX - touch2.pageX) * (touch1.pageX - touch2.pageX) + - (touch1.pageY - touch2.pageY) * (touch1.pageY - touch2.pageY), - ); + let dist = Math.hypot(touch1.pageX - touch2.pageX, touch1.pageY - touch2.pageY); if (origPinchDistance === 0) { origPinchDistance = dist / this.#scale; @@ -550,6 +557,8 @@ export class Cropt { this.elements.zoomer.addEventListener("input", change); this.elements.boundary.addEventListener("wheel", scroll); + this.#zoomInputHandler = change; + this.#wheelHandler = scroll; } #onZoom() { @@ -682,7 +691,7 @@ export class Cropt { this.elements.zoomer.max = maxZoom.toFixed(3); let zoom = this.#boundZoom; - if (zoom === null) { + if (zoom === undefined) { const bData = this.elements.boundary.getBoundingClientRect(); zoom = Math.max(bData.width / img.naturalWidth, bData.height / img.naturalHeight); } From 9105a9032b3efef0af9a98182a49f54453bc82be Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Sun, 21 Dec 2025 01:03:40 -0500 Subject: [PATCH 02/53] Added resize bar functionality --- src/cropt.css | 38 +++++++++++++ src/cropt.ts | 149 +++++++++++++++++++++++++++++++++++++++++++++++--- src/demo.ts | 1 + 3 files changed, 181 insertions(+), 7 deletions(-) diff --git a/src/cropt.css b/src/cropt.css index eb8eb35..a3b1669 100644 --- a/src/cropt.css +++ b/src/cropt.css @@ -55,3 +55,41 @@ .cropt-container .cr-slider { width: 100%; } + +.cropt-container .cr-control { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.cropt-container .cr-resize-handle { + z-index: 100 !important; + position: absolute; + width: 44px !important; + height: 44px !important; + touch-action: none; + pointer-events: auto; + display: flex; + align-items: center; + justify-content: center; +} + +.cropt-container .cr-resize-handle-grabber { + width: 10px; + height: 10px; + background: white; + border: 1px solid rgba(0, 0, 0, 0.3); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + pointer-events: none; +} + +.cropt-container .cr-resize-handle-right { + cursor: ew-resize; +} + +.cropt-container .cr-resize-handle-bottom { + cursor: ns-resize; +} \ No newline at end of file diff --git a/src/cropt.ts b/src/cropt.ts index b670164..57460ca 100644 --- a/src/cropt.ts +++ b/src/cropt.ts @@ -34,10 +34,9 @@ class TransformOrigin { this.y = 0; return; } - - const [x, y] = el.style.transformOrigin.split(" "); - this.x = parseFloat(x) || 0; - this.y = parseFloat(y) || 0; + const css = el.style.transformOrigin.split(" "); + this.x = parseFloat(css[0]); + this.y = parseFloat(css[1]); } toString() { @@ -78,6 +77,9 @@ function getInitialElements() { viewport: document.createElement("div"), preview: document.createElement("img"), overlay: document.createElement("div"), + controls: document.createElement("div"), + resizeHandleRight: document.createElement("div"), + resizeHandleBottom: document.createElement("div"), zoomerWrap: document.createElement("div"), zoomer: document.createElement("input"), }; @@ -116,6 +118,7 @@ export interface CroptOptions { borderRadius: string; }; zoomerInputClass: string; + resizeBars?: boolean; } interface CropPoints { @@ -132,6 +135,9 @@ export class Cropt { viewport: HTMLDivElement; preview: HTMLImageElement; overlay: HTMLDivElement; + controls: HTMLDivElement; + resizeHandleRight: HTMLDivElement; + resizeHandleBottom: HTMLDivElement; zoomerWrap: HTMLDivElement; zoomer: HTMLInputElement; }; @@ -143,6 +149,7 @@ export class Cropt { borderRadius: "0px", }, zoomerInputClass: "cr-slider", + resizeBars: false, }; #boundZoom: number | undefined = undefined; #scale = 1; @@ -162,6 +169,7 @@ export class Cropt { options.viewport = { ...this.options.viewport, ...options.viewport }; } + // changed: removed structuredClone: slow, and would fail passing functions in options this.options = { ...this.options, ...options } as CroptOptions; this.element = element; this.element.classList.add("cropt-container"); @@ -171,6 +179,7 @@ export class Cropt { this.elements.boundary.classList.add("cr-boundary"); this.elements.viewport.classList.add("cr-viewport"); this.elements.overlay.classList.add("cr-overlay"); + this.elements.controls.classList.add("cr-controls"); this.elements.viewport.setAttribute("tabindex", "0"); this.#setPreviewAttributes(this.elements.preview); @@ -178,6 +187,8 @@ export class Cropt { this.elements.boundary.appendChild(this.elements.preview); this.elements.boundary.appendChild(this.elements.viewport); this.elements.boundary.appendChild(this.elements.overlay); + this.elements.boundary.appendChild(this.elements.controls); + this.#setupControlOverlay(); this.elements.zoomer.type = "range"; this.elements.zoomer.step = "0.001"; @@ -286,9 +297,9 @@ export class Cropt { const curWidth = this.options.viewport.width; const curHeight = this.options.viewport.height; - // if (options.viewport) { - // options.viewport = { ...this.options.viewport, ...options.viewport }; - // } + if (options.viewport) { + options.viewport = { ...this.options.viewport, ...options.viewport }; + } // changed: removed structuredClone: slow, and would fail passing functions in options this.options = { ...this.options, ...options } as CroptOptions; @@ -330,6 +341,130 @@ export class Cropt { viewport.style.borderRadius = this.options.viewport.borderRadius; viewport.style.width = this.options.viewport.width + "px"; viewport.style.height = this.options.viewport.height + "px"; + + this.#updateControlHandlePositions(); // move controls overlayed (when necessary) + } + + #setupControlOverlay() { + // currently only resize, if off, nothing to do + if( !this.options.resizeBars ) return + + const { resizeHandleRight, resizeHandleBottom } = this.elements; + + // Style right handle - 44px touch zone with 10px visual indicator + resizeHandleRight.classList.add("cr-resize-handle","cr-resize-handle-right"); + const resizeHandleRightGrabber = document.createElement("div"); + resizeHandleRightGrabber.classList.add("cr-resize-handle-grabber"); + resizeHandleRight.appendChild(resizeHandleRightGrabber); + + // Style bottom handle - 44px touch zone with 10px visual indicator + resizeHandleBottom.classList.add("cr-resize-handle","cr-resize-handle-bottom") + const resizeHandleBottomGrabber = document.createElement("div"); + resizeHandleBottomGrabber.classList.add("cr-resize-handle-grabber"); + resizeHandleBottom.appendChild(resizeHandleBottomGrabber); + + // Append to controls layer (sibling to viewport and overlay) + this.elements.controls.appendChild(resizeHandleRight); + this.elements.controls.appendChild(resizeHandleBottom); + + // Initialize resize handlers + this.#initControlHandlers(); + this.#updateControlHandlePositions(); + } + + #updateControlHandlePositions() { + if( !this.options.resizeBars ) return; + + const { resizeHandleRight, resizeHandleBottom, viewport, boundary } = this.elements; + const width = this.options.viewport.width; + const height = this.options.viewport.height; + const handleSize = 44; // Touch zone size + + // Get viewport position relative to boundary + const vpRect = viewport.getBoundingClientRect(); + const boundRect = boundary.getBoundingClientRect(); + const vpLeft = vpRect.left - boundRect.left; + const vpTop = vpRect.top - boundRect.top; + + // Position right handle (middle of right edge of viewport) + // Center the 44px handle on the edge + resizeHandleRight.style.left = `${vpLeft + width - handleSize / 2}px`; + resizeHandleRight.style.top = `${vpTop + height / 2 - handleSize / 2}px`; + + // Position bottom handle (middle of bottom edge of viewport) + // Center the 44px handle on the edge + resizeHandleBottom.style.left = `${vpLeft + width / 2 - handleSize / 2}px`; + resizeHandleBottom.style.top = `${vpTop + height - handleSize / 2}px`; + } + + #initControlHandlers() { + const MIN_SIZE = 50; + + // Right handle - adjusts width + let rightStartX = 0; + let rightStartWidth = 0; + + const rightPointerMove = (ev: PointerEvent) => { + ev.preventDefault(); + const deltaX = ev.pageX - rightStartX; + const maxWidth = Math.floor(this.elements.boundary.clientWidth*0.95); + const newWidth = Math.min(maxWidth, Math.max(MIN_SIZE, rightStartWidth + deltaX)); + + this.options.viewport.width = newWidth; + this.#setOptionsCss(); + }; + + const rightPointerUp = () => { + document.removeEventListener("pointermove", rightPointerMove); + document.removeEventListener("pointerup", rightPointerUp); + }; + + const rightPointerDown = (ev: PointerEvent) => { + if (ev.button !== 0) return; // Only left mouse button + ev.preventDefault(); + ev.stopPropagation(); + + rightStartX = ev.pageX; + rightStartWidth = this.options.viewport.width; + + document.addEventListener("pointermove", rightPointerMove); + document.addEventListener("pointerup", rightPointerUp); + }; + + this.elements.resizeHandleRight.addEventListener("pointerdown", rightPointerDown); + + // Bottom handle - adjusts height + let bottomStartY = 0; + let bottomStartHeight = 0; + + const bottomPointerMove = (ev: PointerEvent) => { + ev.preventDefault(); + const deltaY = ev.pageY - bottomStartY; + const maxHeight = Math.floor(this.elements.boundary.clientHeight*0.95); + const newHeight = Math.min(maxHeight, Math.max(MIN_SIZE, bottomStartHeight + deltaY)); + + this.options.viewport.height = newHeight; + this.#setOptionsCss(); + }; + + const bottomPointerUp = () => { + document.removeEventListener("pointermove", bottomPointerMove); + document.removeEventListener("pointerup", bottomPointerUp); + }; + + const bottomPointerDown = (ev: PointerEvent) => { + if (ev.button !== 0) return; // Only left mouse button + ev.preventDefault(); + ev.stopPropagation(); + + bottomStartY = ev.pageY; + bottomStartHeight = this.options.viewport.height; + + document.addEventListener("pointermove", bottomPointerMove); + document.addEventListener("pointerup", bottomPointerUp); + }; + + this.elements.resizeHandleBottom.addEventListener("pointerdown", bottomPointerDown); } #getUnscaledCanvas(p: CropPoints) { diff --git a/src/demo.ts b/src/demo.ts index 0b3126c..0688df2 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -38,6 +38,7 @@ let options: CroptOptions = { }, mouseWheelZoom: "on", zoomerInputClass: "form-range", + resizeBars: true, }; function getCode() { From 694dead1e2b70510875a98539354bd9756b7f09b Mon Sep 17 00:00:00 2001 From: MindFlow Date: Sun, 21 Dec 2025 01:23:22 -0500 Subject: [PATCH 03/53] Update README with new 'resizeBars' option Added new option 'resizeBars' to show resize handles for viewport adjustment. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 86b9c26..bd1aa7d 100755 --- a/README.md +++ b/README.md @@ -50,6 +50,13 @@ Default value: `"cr-slider"` Optionally set a different class on the zoom range input to customize styling (e.g. set to `"form-range"` when using Bootstrap). +### `resizeBars` + +Type: `boolean` +Default value: `false` + +Optionally to show resize handles (grab-bars) to adjust the viewport width/height. + ## Methods ### `bind(src: string, zoom: number | null = null): Promise` @@ -112,3 +119,4 @@ Cropt should also work in any other modern browser using an engine based on Geck ## License MIT + From d6c3ae701bde86c9e27f166ba296067685e8e979 Mon Sep 17 00:00:00 2001 From: MindFlow Date: Sun, 21 Dec 2025 01:29:40 -0500 Subject: [PATCH 04/53] Update README with enhancements and clarifications Added a note about Croppie Resize handles and updated the installation and usage sections for clarity. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bd1aa7d..5774caf 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Cropt - lightweight JavaScript image cropper -Originally based on [Foliotek/Croppie](https://github.com/Foliotek/Croppie), but rewritten as a modern ES module with a simpler API, higher quality image scaling, and numerous other improvements. +Originally based on [Foliotek/Croppie](https://github.com/Foliotek/Croppie), but rewritten as a modern ES module with a simpler API, higher quality image scaling, and numerous other improvements. Brought Croppie Resize handles to life by [Filipe Laborde](https://github.com/mindflowgo/). ## Installation @@ -120,3 +120,4 @@ Cropt should also work in any other modern browser using an engine based on Geck MIT + From 9546f42b897e4e564996c5299fe7f82d5219da92 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Sun, 21 Dec 2025 11:51:50 -0500 Subject: [PATCH 05/53] Added getCropInfo() to read crop details --- src/cropt.css | 2 +- src/cropt.ts | 30 ++++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/cropt.css b/src/cropt.css index a3b1669..589c9e4 100644 --- a/src/cropt.css +++ b/src/cropt.css @@ -86,7 +86,7 @@ pointer-events: none; } -.cropt-container .cr-resize-handle-right { +.cropt-container .cr-resize-handle-right { cursor: ew-resize; } diff --git a/src/cropt.ts b/src/cropt.ts index 57460ca..5c25dbf 100644 --- a/src/cropt.ts +++ b/src/cropt.ts @@ -34,9 +34,9 @@ class TransformOrigin { this.y = 0; return; } - const css = el.style.transformOrigin.split(" "); - this.x = parseFloat(css[0]); - this.y = parseFloat(css[1]); + const [x, y] = el.style.transformOrigin.split(" "); + this.x = parseFloat(x) || 0; + this.y = parseFloat(y) || 0; } toString() { @@ -243,6 +243,24 @@ export class Cropt { return Math.round(Math.max(0, pos / this.#scale)); } + /** + * Returns: + * image { left, top, right, bottom }: the viewport rectangle mapped to ORIGINAL image + * scale: user zoom used + * viewport: the active viewport size + borderRadius used (in case it's system adjusted) + */ + getCropInfo() { + return { + image: this.#getPoints(), + scale: this.#scale, + viewport: { + width: Math.round(this.options.viewport.width), + height: Math.round(this.options.viewport.height), + borderRadius: parseInt(this.options.viewport.borderRadius), + } + } + } + /** * Returns a Promise resolving to an HTMLCanvasElement object for the cropped image. * If size is specified, the image will be scaled with its longest side set to size. @@ -403,7 +421,7 @@ export class Cropt { // Right handle - adjusts width let rightStartX = 0; let rightStartWidth = 0; - + const rightPointerMove = (ev: PointerEvent) => { ev.preventDefault(); const deltaX = ev.pageX - rightStartX; @@ -690,10 +708,10 @@ export class Cropt { this.setZoom(this.#scale + delta * this.#scale); }; - this.elements.zoomer.addEventListener("input", change); - this.elements.boundary.addEventListener("wheel", scroll); this.#zoomInputHandler = change; this.#wheelHandler = scroll; + this.elements.zoomer.addEventListener("input", this.#zoomInputHandler); + this.elements.boundary.addEventListener("wheel", this.#wheelHandler); } #onZoom() { From c4889005ea5922ccad7ee4c758530b8b0baf8965 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Mon, 22 Dec 2025 02:15:30 -0500 Subject: [PATCH 06/53] Added get() method, and bind-set to restore the image placement --- CONTRIBUTIONS.md | 7 +++++++ README.md | 15 +++++++++++--- src/cropt.ts | 51 +++++++++++++++++++++++++++++++++++++----------- src/demo.ts | 18 ++++++++++------- 4 files changed, 70 insertions(+), 21 deletions(-) create mode 100644 CONTRIBUTIONS.md diff --git a/CONTRIBUTIONS.md b/CONTRIBUTIONS.md new file mode 100644 index 0000000..25b876d --- /dev/null +++ b/CONTRIBUTIONS.md @@ -0,0 +1,7 @@ +We thank our contributors for helping make Cropt even better: + +[Foliotek/Croppie](https://github.com/Foliotek/Croppie) + Originally based on Croppie, but rewritten as a modern ES + +[Filipe Laborde](https://github.com/mindflowgo/) + - Croppie Resize handle grabbers & get()/set() for restoring zoom/image placement \ No newline at end of file diff --git a/README.md b/README.md index 5774caf..c5d60f8 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Cropt - lightweight JavaScript image cropper -Originally based on [Foliotek/Croppie](https://github.com/Foliotek/Croppie), but rewritten as a modern ES module with a simpler API, higher quality image scaling, and numerous other improvements. Brought Croppie Resize handles to life by [Filipe Laborde](https://github.com/mindflowgo/). +Originally based on [Foliotek/Croppie](https://github.com/Foliotek/Croppie), but rewritten as a modern ES module with a simpler API, higher quality image scaling, and numerous other improvements. ## Installation @@ -59,9 +59,9 @@ Optionally to show resize handles (grab-bars) to adjust the viewport width/heigh ## Methods -### `bind(src: string, zoom: number | null = null): Promise` +### `bind(src: string, set: number | { transform, viewport }): Promise` -Takes an image URL as the first argument, and an optional initial zoom value. Returns a `Promise` which resolves when the image has been loaded and state is initialized. +Takes an image URL as the first argument, and an optional initial zoom value OR {transform, viewport} object to restore image placement in viewport. Returns a `Promise` which resolves when the image has been loaded and state is initialized. ### `destroy(): void` @@ -79,6 +79,15 @@ Returns a `Promise` resolving to an `HTMLCanvasElement` object for the cropped i Returns a Promise resolving to a `Blob` object for the cropped image. If `size` is specified, the cropped image will be scaled with its longest side set to this value. The `type` and `quality` parameters are passed directly to the corresponding [HTMLCanvasElement.toBlob()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob) method parameters. +### `get(): { crop: { left, top, right, bottom }, transform: { x, y, scale, origin: {x, y}}, viewport: { width, height, borderRadius } }` +Returns information about the current crop state (all `number`s): + +- crop: Crop coordinates on the original image (left, top, right, bottom in pixels) +- transform: Information for re-placement of image within viewport +- viewport: Final viewport dimensions and styling (width, height, borderRadius) + +Useful for server-side cropping or saving user selections. + ### `setOptions(options: CroptOptions): void` Allows options to be dynamically changed on an existing Cropt instance. diff --git a/src/cropt.ts b/src/cropt.ts index 5c25dbf..a0adfda 100644 --- a/src/cropt.ts +++ b/src/cropt.ts @@ -206,18 +206,33 @@ export class Cropt { /** * Bind an image from an src string. + * Passing in set transform/viewport parameters will restore to those * Returns a Promise which resolves when the image has been loaded and state is initialized. */ - bind(src: string, zoom: number | undefined = undefined) { + bind(src: string, set?: number | { + transform: { x: number; y: number; scale: number, origin: { x: number; y: number } }; + viewport: { width: number; height: number; borderRadius: string }; + }) { + if (!src) { throw new Error("src cannot be empty"); } - this.#boundZoom = zoom; + if (typeof(set) !== 'object') { + this.#boundZoom = set; + } return loadImage(src).then((img) => { this.#replaceImage(img); - this.#updatePropertiesFromImage(); + + if (typeof(set) === 'object') { + this.setOptions({ viewport: set.viewport }); + + // defer restore to next frame (after layout) + setTimeout(() => this.#restoreTransform(set.transform), 0); + } else { + this.#updatePropertiesFromImage(); + } }); } @@ -243,24 +258,38 @@ export class Cropt { return Math.round(Math.max(0, pos / this.#scale)); } + #restoreTransform(transform: { x: number; y: number; scale: number, origin: { x: number; y: number } }) { + const scale = transform.scale; + this.#scale = scale; + this.elements.zoomer.value = scale.toFixed(3); + + this.elements.preview.style.transform = new Transform(transform.x, transform.y, scale).toString() + this.elements.preview.style.transformOrigin = `${transform.origin.x}px ${transform.origin.y}px`; + + this.#updateOverlay(); + } + /** * Returns: - * image { left, top, right, bottom }: the viewport rectangle mapped to ORIGINAL image - * scale: user zoom used + * crop { left, top, right, bottom }: the crop rectangle for image cropping outside Cropt + * transform: adjustments to re-create placement of image in viewport (ex. continue editing) * viewport: the active viewport size + borderRadius used (in case it's system adjusted) */ - getCropInfo() { + get() { return { - image: this.#getPoints(), - scale: this.#scale, + crop: this.#getPoints(), + transform: { + ...Transform.parse(this.elements.preview), + origin: new TransformOrigin(this.elements.preview), + }, viewport: { width: Math.round(this.options.viewport.width), height: Math.round(this.options.viewport.height), - borderRadius: parseInt(this.options.viewport.borderRadius), + borderRadius: this.options.viewport.borderRadius, } - } + }; } - + /** * Returns a Promise resolving to an HTMLCanvasElement object for the cropped image. * If size is specified, the image will be scaled with its longest side set to size. diff --git a/src/demo.ts b/src/demo.ts index 0688df2..8237159 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -18,11 +18,11 @@ function popupResult(src: string, borderRadius: string) { let photos = [ "girl-piano.jpg", - "hiker.jpg", - "kitten.jpg", - "red-panda.webp", - "toucan.jpg", - "woman-dog.jpg", + // "hiker.jpg", + // "kitten.jpg", + // "red-panda.webp", + // "toucan.jpg", + // "woman-dog.jpg", ]; const cropElId = "crop-demo"; @@ -77,13 +77,17 @@ function setCode() { getElById("code-el").innerHTML = hljs.highlight(code, { language: "javascript" }).value; } -function demoMain() { +async function demoMain() { const cropEl = getElById(cropElId); const resultBtn = getElById(resultBtnId); + const set = {transform:{x:-736.431,y:-1298.56,scale:0.585,origin:{x:897.296,y:1458.98}},viewport:{width:220,height:220,borderRadius:'13%'}}; const cropt = new Cropt(cropEl, options); - cropt.bind(photoSrc); + cropt.bind(photoSrc, set); resultBtn.onclick = function () { + const restoreSet = cropt.get(); + console.log( `Image parameters [cropt.get()]:`, JSON.stringify(restoreSet) ); + cropt.toCanvas(outputSize).then(function (canvas) { popupResult(canvas.toDataURL(), cropt.options.viewport.borderRadius); }); From 65bc6980977b6f647e1bce2555993cf22a765298 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Mon, 22 Dec 2025 02:23:31 -0500 Subject: [PATCH 07/53] Reverted demo; left get()/set there but comented out. --- src/demo.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/demo.ts b/src/demo.ts index 8237159..da0358a 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -18,11 +18,11 @@ function popupResult(src: string, borderRadius: string) { let photos = [ "girl-piano.jpg", - // "hiker.jpg", - // "kitten.jpg", - // "red-panda.webp", - // "toucan.jpg", - // "woman-dog.jpg", + "hiker.jpg", + "kitten.jpg", + "red-panda.webp", + "toucan.jpg", + "woman-dog.jpg", ]; const cropElId = "crop-demo"; @@ -77,7 +77,7 @@ function setCode() { getElById("code-el").innerHTML = hljs.highlight(code, { language: "javascript" }).value; } -async function demoMain() { +function demoMain() { const cropEl = getElById(cropElId); const resultBtn = getElById(resultBtnId); const set = {transform:{x:-736.431,y:-1298.56,scale:0.585,origin:{x:897.296,y:1458.98}},viewport:{width:220,height:220,borderRadius:'13%'}}; From ffb1abc98072ccc796aa01e11182684bf253f4a1 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Mon, 22 Dec 2025 02:25:18 -0500 Subject: [PATCH 08/53] Reverted demo; left get()/set there but comented out. --- src/demo.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/demo.ts b/src/demo.ts index da0358a..2dffcac 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -80,9 +80,11 @@ function setCode() { function demoMain() { const cropEl = getElById(cropElId); const resultBtn = getElById(resultBtnId); - const set = {transform:{x:-736.431,y:-1298.56,scale:0.585,origin:{x:897.296,y:1458.98}},viewport:{width:220,height:220,borderRadius:'13%'}}; const cropt = new Cropt(cropEl, options); - cropt.bind(photoSrc, set); + cropt.bind(photoSrc); + // If wanting to pass in preset image-transform/viewport; do this way: + // const set = {transform:{x:-736.431,y:-1298.56,scale:0.585,origin:{x:897.296,y:1458.98}},viewport:{width:220,height:220,borderRadius:'13%'}}; + // cropt.bind(photoSrc, set); resultBtn.onclick = function () { const restoreSet = cropt.get(); From 44c25e42d6599fcd62a97fca14513f05d0df8b5a Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Mon, 22 Dec 2025 21:52:39 -0500 Subject: [PATCH 09/53] Optimized transform management. --- src/cropt.ts | 186 ++++++++++++++++++++++++--------------------------- 1 file changed, 87 insertions(+), 99 deletions(-) diff --git a/src/cropt.ts b/src/cropt.ts index a0adfda..e3a2617 100644 --- a/src/cropt.ts +++ b/src/cropt.ts @@ -1,49 +1,3 @@ -class Transform { - x: number; - y: number; - scale: number; - - constructor(x: number, y: number, scale: number) { - this.x = x; - this.y = y; - this.scale = scale; - } - - toString() { - return `translate(${this.x}px, ${this.y}px) scale(${this.scale})`; - } - - static parse(img: HTMLImageElement) { - const values = img.style.transform.split(") "); - const translate = values[0].substring("translate".length + 1).split(","); - const scale = values.length > 1 ? values[1].substring(6) : "1"; - const x = translate.length > 1 ? translate[0] : "0"; - const y = translate.length > 1 ? translate[1] : "0"; - - return new Transform(parseFloat(x), parseFloat(y), parseFloat(scale)); - } -} - -class TransformOrigin { - x: number; - y: number; - - constructor(el?: HTMLImageElement) { - if (!el || !el.style.transformOrigin) { - this.x = 0; - this.y = 0; - return; - } - const [x, y] = el.style.transformOrigin.split(" "); - this.x = parseFloat(x) || 0; - this.y = parseFloat(y) || 0; - } - - toString() { - return this.x + "px " + this.y + "px"; - } -} - function debounce(func: T, wait: number) { let timer: number | undefined; return (...args: any) => { @@ -98,7 +52,7 @@ function getArrowKeyDeltas(key: string): [number, number] { } function clampDelta(innerDiff: number, delta: number, outerDiff: number) { - return Math.max(Math.min(innerDiff, delta), outerDiff); + return Math.round(Math.max(Math.min(innerDiff, delta), outerDiff)); } function canvasSupportsWebP() { @@ -229,7 +183,11 @@ export class Cropt { this.setOptions({ viewport: set.viewport }); // defer restore to next frame (after layout) - setTimeout(() => this.#restoreTransform(set.transform), 0); + setTimeout(() => { + this.#updateZoomLimits(true); + this.setZoom(set.transform.scale); + this.#previewTransform(set.transform) + }, 0); } else { this.#updatePropertiesFromImage(); } @@ -258,17 +216,6 @@ export class Cropt { return Math.round(Math.max(0, pos / this.#scale)); } - #restoreTransform(transform: { x: number; y: number; scale: number, origin: { x: number; y: number } }) { - const scale = transform.scale; - this.#scale = scale; - this.elements.zoomer.value = scale.toFixed(3); - - this.elements.preview.style.transform = new Transform(transform.x, transform.y, scale).toString() - this.elements.preview.style.transformOrigin = `${transform.origin.x}px ${transform.origin.y}px`; - - this.#updateOverlay(); - } - /** * Returns: * crop { left, top, right, bottom }: the crop rectangle for image cropping outside Cropt @@ -278,10 +225,7 @@ export class Cropt { get() { return { crop: this.#getPoints(), - transform: { - ...Transform.parse(this.elements.preview), - origin: new TransformOrigin(this.elements.preview), - }, + transform: this.#previewTransform(), viewport: { width: Math.round(this.options.viewport.width), height: Math.round(this.options.viewport.height), @@ -382,6 +326,59 @@ export class Cropt { this.elements = getInitialElements(); } + // adjust preview tranform styles (& transformOrigin) + #previewTransform(data?: {x?: number, y?: number, scale?: number, rotate?: number, origin?: {x?: number, y?: number}}): {x: number, y: number, scale: number, rotate: number, origin: {x: number, y: number}} { + const el = this.elements.preview + + const parseOrigin = (): {x: number, y: number} => { + const [oxStr, oyStr] = (el.style.transformOrigin || '0px 0px').split(' '); + return { + x: parseFloat(oxStr) || 0, + y: parseFloat(oyStr) || 0, + }; + }; + + // apply the data given + if (data !== undefined) { + const { x = 0, y = 0, scale = 1, rotate = 0 } = data; + el.style.transform = `translate(${x}px, ${y}px) scale(${scale}) rotate(${rotate}deg)`; + + // Only set transformOrigin if provided + if (data.origin !== undefined) { + const ox = data.origin.x ?? 0; + const oy = data.origin.y ?? 0; + el.style.transformOrigin = `${ox}px ${oy}px`; + } + + return {x, y, scale, rotate, origin: parseOrigin()}; + } + + // no data so PARSE current element and pass out + const str = el.style.transform || ''; + let x = 0, y = 0, scale = 1, rotate = 0; + + for (const action of ['translate', 'scale', 'rotate']) { + const regex = new RegExp(`${action}\s*\\(([^)]+)\\)`); + const match = str.match(regex); + + if (match) { + const value = match[1].trim(); + + if (action === 'translate') { + const [xStr, yStr] = value.split(',').map(v => v.trim()); + x = Math.round(parseFloat(xStr)) || 0; + y = yStr !== undefined ? (Math.round(parseFloat(yStr)) || 0) : x; + } else if (action === 'scale') { + scale = parseFloat(value) || 1; + } else if (action === 'rotate') { + rotate = parseFloat(value.replace('deg','')) || 0; + } + } + } + + return {x, y, scale, rotate, origin: parseOrigin()}; + } + #setOptionsCss() { this.elements.zoomer.className = this.options.zoomerInputClass; const viewport = this.elements.viewport; @@ -610,7 +607,7 @@ export class Cropt { #assignTransformCoordinates(deltaX: number, deltaY: number) { const imgRect = this.elements.preview.getBoundingClientRect(); const vpRect = this.elements.viewport.getBoundingClientRect(); - const transform = Transform.parse(this.elements.preview); + const transform = this.#previewTransform(); transform.y += clampDelta(vpRect.top - imgRect.top, deltaY, vpRect.bottom - imgRect.bottom); transform.x += clampDelta(vpRect.left - imgRect.left, deltaX, vpRect.right - imgRect.right); @@ -744,43 +741,36 @@ export class Cropt { } #onZoom() { - const transform = Transform.parse(this.elements.preview); - const origin = new TransformOrigin(this.elements.preview); - - let applyCss = () => { - this.elements.preview.style.transform = transform.toString(); - this.elements.preview.style.transformOrigin = origin.toString(); - }; - + const transform = this.#previewTransform() this.#scale = parseFloat(this.elements.zoomer.value); transform.scale = this.#scale; - applyCss(); - + // this.#previewTransform(transform) + const boundaries = this.#getVirtualBoundaries(); const transBoundaries = boundaries.translate; const oBoundaries = boundaries.origin; if (transform.x >= transBoundaries.maxX) { - origin.x = oBoundaries.minX; + transform.origin.x = oBoundaries.minX; transform.x = transBoundaries.maxX; } if (transform.x <= transBoundaries.minX) { - origin.x = oBoundaries.maxX; + transform.origin.x = oBoundaries.maxX; transform.x = transBoundaries.minX; } if (transform.y >= transBoundaries.maxY) { - origin.y = oBoundaries.minY; + transform.origin.y = oBoundaries.minY; transform.y = transBoundaries.maxY; } if (transform.y <= transBoundaries.minY) { - origin.y = oBoundaries.maxY; + transform.origin.y = oBoundaries.maxY; transform.y = transBoundaries.minY; } - applyCss(); + this.#previewTransform(transform); this.#updateOverlayDebounced(); } @@ -823,40 +813,36 @@ export class Cropt { return; } - const preview = this.elements.preview; - const transformReset = new Transform(0, 0, 1); - preview.style.transform = transformReset.toString(); - preview.style.transformOrigin = new TransformOrigin().toString(); - + // resets values to calculate zoom limits + const transformReset = {x: 0, y: 0, scale: 1, rotate: 0, origin: {x: 0, y: 0}} + this.#previewTransform(transformReset); this.#updateZoomLimits(); - transformReset.scale = this.#scale; - preview.style.transform = transformReset.toString(); - preview.style.transformOrigin = new TransformOrigin().toString(); + transformReset.scale = this.#scale + this.#previewTransform(transformReset); this.#centerImage(); this.#updateOverlay(); } - #updateCenterPoint(transform: Transform) { + #updateCenterPoint(transform: {x: number, y: number, scale: number, rotate?: number, origin?: {x: number, y: number}}) { const vpData = this.elements.viewport.getBoundingClientRect(); const data = this.elements.preview.getBoundingClientRect(); - const curPos = new TransformOrigin(this.elements.preview); + const { origin } = this.#previewTransform() const top = vpData.top - data.top + vpData.height / 2; const left = vpData.left - data.left + vpData.width / 2; const center = { - x: left / this.#scale, - y: top / this.#scale, + x: Math.round(left / this.#scale), + y: Math.round(top / this.#scale), }; - transform.x -= (center.x - curPos.x) * (1 - this.#scale); - transform.y -= (center.y - curPos.y) * (1 - this.#scale); + transform.x = Math.round(transform.x - (center.x - (origin.x ?? 0)) * (1 - this.#scale)); + transform.y = Math.round(transform.y - (center.y - (origin.y ?? 0)) * (1 - this.#scale)); - this.elements.preview.style.transform = transform.toString(); - this.elements.preview.style.transformOrigin = center.x + "px " + center.y + "px"; + this.#previewTransform({ ...transform, origin: center }) } - #updateZoomLimits() { + #updateZoomLimits(skipCenter = false) { const img = this.elements.preview; const vpData = this.elements.viewport.getBoundingClientRect(); const minZoom = Math.max( @@ -871,6 +857,8 @@ export class Cropt { this.elements.zoomer.min = minZoom.toFixed(3); this.elements.zoomer.max = maxZoom.toFixed(3); + if (skipCenter) return; + let zoom = this.#boundZoom; if (zoom === undefined) { @@ -880,7 +868,7 @@ export class Cropt { this.setZoom(zoom); } - + #centerImage() { const imgDim = this.elements.preview.getBoundingClientRect(); const vpDim = this.elements.viewport.getBoundingClientRect(); @@ -891,6 +879,6 @@ export class Cropt { const x = vpLeft - (imgDim.width - vpDim.width) / 2; const y = vpTop - (imgDim.height - vpDim.height) / 2; - this.#updateCenterPoint(new Transform(x, y, this.#scale)); + this.#updateCenterPoint({x, y, scale: this.#scale}); } -} +} \ No newline at end of file From c7782aed74e09734c94f92bdf4773178807eba46 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Mon, 22 Dec 2025 22:22:58 -0500 Subject: [PATCH 10/53] Small fixes for final release with resizeBrs. --- src/cropt.ts | 18 +++++++++++------- src/demo.ts | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/cropt.ts b/src/cropt.ts index e3a2617..231430e 100644 --- a/src/cropt.ts +++ b/src/cropt.ts @@ -105,6 +105,7 @@ export class Cropt { zoomerInputClass: "cr-slider", resizeBars: false, }; + #boundZoom: number | undefined = undefined; #scale = 1; #keyDownHandler: ((ev: KeyboardEvent) => void) | null = null; @@ -184,12 +185,15 @@ export class Cropt { // defer restore to next frame (after layout) setTimeout(() => { - this.#updateZoomLimits(true); + // const transformReset = {x: 0, y: 0, scale: 1, rotate: 0, origin: {x: 0, y: 0}} + // this.#previewTransform(transformReset); + this.#updateZoomLimits(true); // skipping center this.setZoom(set.transform.scale); this.#previewTransform(set.transform) + this.#updateOverlay(); }, 0); } else { - this.#updatePropertiesFromImage(); + this.#initPropertiesFromImage(); } }); } @@ -281,7 +285,7 @@ export class Cropt { } refresh() { - this.#updatePropertiesFromImage(); + this.#initPropertiesFromImage(); } setOptions(options: RecursivePartial) { @@ -808,7 +812,7 @@ export class Cropt { overlay.style.left = `${imgData.left - boundRect.left}px`; } - #updatePropertiesFromImage() { + #initPropertiesFromImage() { if (!this.#isVisible()) { return; } @@ -818,13 +822,13 @@ export class Cropt { this.#previewTransform(transformReset); this.#updateZoomLimits(); - transformReset.scale = this.#scale + transformReset.scale = this.#scale; this.#previewTransform(transformReset); this.#centerImage(); this.#updateOverlay(); } - #updateCenterPoint(transform: {x: number, y: number, scale: number, rotate?: number, origin?: {x: number, y: number}}) { + #updateCenterPoint(transform: {x: number, y: number, scale: number, rotate: number, origin?: {x: number, y: number}}) { const vpData = this.elements.viewport.getBoundingClientRect(); const data = this.elements.preview.getBoundingClientRect(); const { origin } = this.#previewTransform() @@ -879,6 +883,6 @@ export class Cropt { const x = vpLeft - (imgDim.width - vpDim.width) / 2; const y = vpTop - (imgDim.height - vpDim.height) / 2; - this.#updateCenterPoint({x, y, scale: this.#scale}); + this.#updateCenterPoint({x, y, scale: this.#scale, rotate: 0}); } } \ No newline at end of file diff --git a/src/demo.ts b/src/demo.ts index 2dffcac..66f5aab 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -83,7 +83,7 @@ function demoMain() { const cropt = new Cropt(cropEl, options); cropt.bind(photoSrc); // If wanting to pass in preset image-transform/viewport; do this way: - // const set = {transform:{x:-736.431,y:-1298.56,scale:0.585,origin:{x:897.296,y:1458.98}},viewport:{width:220,height:220,borderRadius:'13%'}}; + // const set = {"transform":{"x":-857,"y":-752,"scale":0.685,"rotate":0,"origin":{"x":1017.26,"y":911}},"viewport":{"width":252,"height":128,"borderRadius":"33%"}} // cropt.bind(photoSrc, set); resultBtn.onclick = function () { From 3267976c83655c5e138f77603fbc916e77bcecb0 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Mon, 22 Dec 2025 23:03:06 -0500 Subject: [PATCH 11/53] Switched eventListener cleanup to AbortController() --- src/cropt.ts | 43 +++++++++++++++---------------------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/src/cropt.ts b/src/cropt.ts index 231430e..f45d0a3 100644 --- a/src/cropt.ts +++ b/src/cropt.ts @@ -108,9 +108,7 @@ export class Cropt { #boundZoom: number | undefined = undefined; #scale = 1; - #keyDownHandler: ((ev: KeyboardEvent) => void) | null = null; - #zoomInputHandler: (() => void) | null = null; - #wheelHandler: ((ev: WheelEvent) => void) | null = null; + #abortController = new AbortController(); #updateOverlayDebounced = debounce(() => { this.#updateOverlay(); }, 100); @@ -315,15 +313,7 @@ export class Cropt { } destroy() { - if (this.#keyDownHandler) { - document.removeEventListener("keydown", this.#keyDownHandler); - } - if (this.#zoomInputHandler) { - this.elements.zoomer.removeEventListener("input", this.#zoomInputHandler); - } - if (this.#wheelHandler) { - this.elements.boundary.removeEventListener("wheel", this.#wheelHandler); - } + this.#abortController.abort(); this.element.removeChild(this.elements.boundary); this.element.classList.remove("cropt-container"); this.element.removeChild(this.elements.zoomerWrap); @@ -475,11 +465,11 @@ export class Cropt { rightStartX = ev.pageX; rightStartWidth = this.options.viewport.width; - document.addEventListener("pointermove", rightPointerMove); - document.addEventListener("pointerup", rightPointerUp); + document.addEventListener("pointermove", rightPointerMove, { signal: this.#abortController.signal }); + document.addEventListener("pointerup", rightPointerUp, { signal: this.#abortController.signal }); }; - this.elements.resizeHandleRight.addEventListener("pointerdown", rightPointerDown); + this.elements.resizeHandleRight.addEventListener("pointerdown", rightPointerDown, { signal: this.#abortController.signal }); // Bottom handle - adjusts height let bottomStartY = 0; @@ -508,11 +498,11 @@ export class Cropt { bottomStartY = ev.pageY; bottomStartHeight = this.options.viewport.height; - document.addEventListener("pointermove", bottomPointerMove); - document.addEventListener("pointerup", bottomPointerUp); + document.addEventListener("pointermove", bottomPointerMove, { signal: this.#abortController.signal }); + document.addEventListener("pointerup", bottomPointerUp, { signal: this.#abortController.signal }); }; - this.elements.resizeHandleBottom.addEventListener("pointerdown", bottomPointerDown); + this.elements.resizeHandleBottom.addEventListener("pointerdown", bottomPointerDown, { signal: this.#abortController.signal }); } #getUnscaledCanvas(p: CropPoints) { @@ -692,9 +682,9 @@ export class Cropt { originalY = ev.pageY; this.#setDragState(true, this.elements.preview); - this.elements.overlay.addEventListener("pointermove", pointerMove); - this.elements.overlay.addEventListener("pointerup", pointerUp); - this.elements.overlay.addEventListener("pointerout", pointerUp); + this.elements.overlay.addEventListener("pointermove", pointerMove, { signal: this.#abortController.signal }); + this.elements.overlay.addEventListener("pointerup", pointerUp, { signal: this.#abortController.signal }); + this.elements.overlay.addEventListener("pointerout", pointerUp, { signal: this.#abortController.signal }); }; let keyDown = (ev: KeyboardEvent) => { @@ -714,9 +704,8 @@ export class Cropt { } }; - this.elements.overlay.addEventListener("pointerdown", pointerDown); - document.addEventListener("keydown", keyDown); - this.#keyDownHandler = keyDown; + this.elements.overlay.addEventListener("pointerdown", pointerDown, { signal: this.#abortController.signal }); + document.addEventListener("keydown", keyDown, { signal: this.#abortController.signal }); } #initializeZoom() { @@ -738,10 +727,8 @@ export class Cropt { this.setZoom(this.#scale + delta * this.#scale); }; - this.#zoomInputHandler = change; - this.#wheelHandler = scroll; - this.elements.zoomer.addEventListener("input", this.#zoomInputHandler); - this.elements.boundary.addEventListener("wheel", this.#wheelHandler); + this.elements.zoomer.addEventListener("input", change, { signal: this.#abortController.signal }); + this.elements.boundary.addEventListener("wheel", scroll, { signal: this.#abortController.signal }); } #onZoom() { From f2779772e660ef59fdf7271f9f3377b6c42a656c Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Tue, 23 Dec 2025 01:30:39 -0500 Subject: [PATCH 12/53] Updated demo code to demonstrate presets & resizeBars. Multi-tab demo page now. --- demo/index.html | 99 +++++++++-------- src/demo.ts | 281 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 253 insertions(+), 127 deletions(-) diff --git a/demo/index.html b/demo/index.html index a089832..2a81e3b 100644 --- a/demo/index.html +++ b/demo/index.html @@ -69,29 +69,53 @@

- - + + View Readme

-
+
+
+

Basic with external viewport adjusting:

+
+
+
+

Cropping from restored configuration:

+
+
+
+

Rotation and resize handles enabled:

+
+
+
+
+ +
+
@@ -99,17 +123,8 @@

Demo

@@ -117,43 +132,25 @@

Demo

-
+
+                        
+                    
-
+
+
+
- - + +
- +
- +
diff --git a/src/demo.ts b/src/demo.ts index 66f5aab..cf511a5 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -1,4 +1,4 @@ -import { Cropt, type CroptOptions } from "./cropt.js"; +import { Cropt } from "./cropt.js"; declare var hljs: any; declare var bootstrap: any; @@ -25,38 +25,104 @@ let photos = [ "woman-dog.jpg", ]; -const cropElId = "crop-demo"; -const resultBtnId = "result-btn"; const outputSize = 500; -let photoSrc = "photos/" + photos[Math.floor(Math.random() * photos.length)]; -let options: CroptOptions = { - viewport: { - width: 220, - height: 220, - borderRadius: "50%", +interface DemoConfig { + id: string; + options: { + viewport: { width: number; height: number; borderRadius: string }; + mouseWheelZoom?: "off" | "on" | "ctrl"; + zoomerInputClass?: string; + resizeBars?: boolean; + }; + preset: null | { + transform: { x: number; y: number; scale: number; rotate: number; origin: { x: number; y: number } }; + viewport: { width: number; height: number; borderRadius: string }; + }; + hideControls: boolean; + notes: string; + getRandomImage: () => string; +} + +const demoConfigs: Record = { + demo1: { + id: "crop-demo", + options: { + viewport: { width: 220, height: 220, borderRadius: "50%" }, + mouseWheelZoom: "on", + zoomerInputClass: "form-range", + }, + preset: null, + hideControls: false, + notes: '', + getRandomImage: () => "photos/" + photos[Math.floor(Math.random() * photos.length)] + }, + demo2: { + id: "crop-demo-2", + options: { + viewport: { width: 252, height: 128, borderRadius: "0%" }, // Fixed: can't be 0x0 + }, + preset: { + "transform": { + "x": -857, + "y": -752, + "scale": 0.685, + "rotate": 0, + "origin": { "x": 1017.26, "y": 911 } + }, + "viewport": { + "width": 252, + "height": 128, + "borderRadius": "33%" + } + }, + hideControls: true, + notes: "Passing a previously captured:
    cropt.get()
restores viewport " + +"and image position. See the
    cropt.bind()
section. Look in the console.log for output from cropt.get().", + getRandomImage: () => "photos/kitten.jpg" }, - mouseWheelZoom: "on", - zoomerInputClass: "form-range", - resizeBars: true, + demo3: { + id: "crop-demo-3", + options: { + viewport: { width: 160, height: 220, borderRadius: "30%" }, + resizeBars: true, + }, + preset: null, + hideControls: true, + notes: "Note the grab bars on the viewport, you can manually adjust the sizing of viewport.", + getRandomImage: () => "photos/" + photos[Math.floor(Math.random() * photos.length)] + } }; +let activeDemo = 'demo1'; // Initialize with default +let cropt: Cropt | null = null; + function getCode() { - const optionStr = JSON.stringify(options, undefined, 4); + const config = demoConfigs[activeDemo]; + const optionStr = JSON.stringify(config.options, undefined, 4); + const imgSrc = config.getRandomImage(); + let bindPreset = ''; + if (config.preset) { + bindPreset = `\n// Using a preset here:\nconst preset = ${JSON.stringify(config.preset)}` + + `\n// Pass to bind():`; + } return `import { Cropt } from "cropt"; -const cropEl = document.getElementById("${cropElId}"); -const resultBtn = document.getElementById("${resultBtnId}"); +const cropEl = document.getElementById("${config.id}"); +const resultBtn = document.getElementById("result-btn"); const cropt = new Cropt(cropEl, ${optionStr}); - -cropt.bind("${photoSrc}"); - -resultBtn.addEventListener("click", () => { +${bindPreset} +cropt.bind("${imgSrc}"${bindPreset ? ', preset' : ''}); + +resultBtn.addEventListener("click", () => {${bindPreset ? ` + // Read the crop & viewport details this way... + const cropAndViewportInfo = cropt.get(); + console.log(\`Image parameters [cropt.get()]:\`) + console.log( JSON.stringify(cropAndViewportInfo) );\n` : ''} cropt.toCanvas(${outputSize}).then((canvas) => { let url = canvas.toDataURL(); - // Data URL can be set as the src of an image element. // Display in modal dialog. }); });`; @@ -64,11 +130,7 @@ resultBtn.addEventListener("click", () => { function getElById(elementId: string) { const el = document.getElementById(elementId); - - if (el === null) { - throw new Error(`${elementId} is null`); - } - + if (el === null) throw new Error(`${elementId} is null`); return el; } @@ -77,83 +139,150 @@ function setCode() { getElById("code-el").innerHTML = hljs.highlight(code, { language: "javascript" }).value; } -function demoMain() { - const cropEl = getElById(cropElId); - const resultBtn = getElById(resultBtnId); - const cropt = new Cropt(cropEl, options); - cropt.bind(photoSrc); - // If wanting to pass in preset image-transform/viewport; do this way: - // const set = {"transform":{"x":-857,"y":-752,"scale":0.685,"rotate":0,"origin":{"x":1017.26,"y":911}},"viewport":{"width":252,"height":128,"borderRadius":"33%"}} - // cropt.bind(photoSrc, set); - - resultBtn.onclick = function () { - const restoreSet = cropt.get(); - console.log( `Image parameters [cropt.get()]:`, JSON.stringify(restoreSet) ); - - cropt.toCanvas(outputSize).then(function (canvas) { - popupResult(canvas.toDataURL(), cropt.options.viewport.borderRadius); - }); - }; +function setupControls() { + const config = demoConfigs[activeDemo]; + const controlsContainer = document.getElementById('controls'); + const notesContainer = document.getElementById('notes'); + if (controlsContainer) controlsContainer.classList.toggle('d-none', config.hideControls); + if (notesContainer) { + notesContainer.classList.toggle('d-none', !config.hideControls); + notesContainer.innerHTML = config.notes; + } - const borderRadiusRange = getElById("borderRadiusRange") as HTMLInputElement; - borderRadiusRange.value = parseInt(options.viewport.borderRadius).toString(); + if (!config.hideControls) { + const borderRadiusRange = getElById("borderRadiusRange") as HTMLInputElement; + borderRadiusRange.value = parseInt(config.options.viewport.borderRadius).toString(); + + const widthRange = getElById("widthRange") as HTMLInputElement; + widthRange.value = config.options.viewport.width.toString(); + + const heightRange = getElById("heightRange") as HTMLInputElement; + heightRange.value = config.options.viewport.height.toString(); + + if (config.options.mouseWheelZoom) { + const mouseWheelSelect = getElById("mouseWheelSelect") as HTMLSelectElement; + mouseWheelSelect.value = config.options.mouseWheelZoom; + } + } +} - borderRadiusRange.oninput = function (ev) { - options.viewport.borderRadius = borderRadiusRange.value + "%"; +function bindControlEvents() { + const config = demoConfigs[activeDemo]; + if (config.hideControls) return; + + const borderRadiusRange = getElById("borderRadiusRange") as HTMLInputElement; + borderRadiusRange.oninput = () => { + const activeConfig = demoConfigs[activeDemo]; + activeConfig.options.viewport.borderRadius = borderRadiusRange.value + "%"; setCode(); - cropt.setOptions(options); + cropt?.setOptions(activeConfig.options); }; - + const widthRange = getElById("widthRange") as HTMLInputElement; - widthRange.value = options.viewport.width.toString(); - - widthRange.oninput = function (ev) { - options.viewport.width = +widthRange.value; + widthRange.oninput = () => { + const activeConfig = demoConfigs[activeDemo]; + activeConfig.options.viewport.width = +widthRange.value; setCode(); - cropt.setOptions(options); + cropt?.setOptions(activeConfig.options); }; - + const heightRange = getElById("heightRange") as HTMLInputElement; - heightRange.value = options.viewport.height.toString(); - - heightRange.oninput = function (ev) { - options.viewport.height = +heightRange.value; + heightRange.oninput = () => { + const activeConfig = demoConfigs[activeDemo]; + activeConfig.options.viewport.height = +heightRange.value; setCode(); - cropt.setOptions(options); + cropt?.setOptions(activeConfig.options); }; - + const mouseWheelSelect = getElById("mouseWheelSelect") as HTMLSelectElement; - mouseWheelSelect.value = options.mouseWheelZoom; - - mouseWheelSelect.onchange = function (ev) { - options.mouseWheelZoom = mouseWheelSelect.value as "on" | "off" | "ctrl"; + mouseWheelSelect.onchange = () => { + const activeConfig = demoConfigs[activeDemo]; + const value = mouseWheelSelect.value as "on" | "off" | "ctrl"; + activeConfig.options.mouseWheelZoom = value; setCode(); - cropt.setOptions(options); + cropt?.setOptions(activeConfig.options); }; +} +function bindFileUpload() { const fileInput = getElById("imgFile") as HTMLInputElement; fileInput.value = ""; - - fileInput.onchange = function () { - if (fileInput.files && fileInput.files[0]) { + + fileInput.onchange = () => { + if (fileInput.files?.[0]) { const file = fileInput.files[0]; - photoSrc = file.name; - setCode(); const reader = new FileReader(); - + reader.onload = (e) => { - if (typeof e.target?.result === "string") { - cropt.bind(e.target.result).then(() => { - console.log("upload bind complete"); - }); + const result = e.target?.result; + if (typeof result === "string" && cropt) { + cropt.bind(result); } }; - + reader.readAsDataURL(file); } }; +} +function initializeDemo(demoKey: string) { + const config = demoConfigs[demoKey]; + const cropEl = getElById(config.id); + const imgSrc = config.getRandomImage(); + + // Destroy existing instance if present + if (cropt) { + cropt.destroy(); + } + + // Create new instance + cropt = new Cropt(cropEl, config.options); + + if (config.preset) { + cropt.bind(imgSrc, config.preset); + } else { + cropt.bind(imgSrc); + } +} + +function demoMain() { + // Initial setup + initializeDemo('demo1'); + setupControls(); setCode(); + bindControlEvents(); + bindFileUpload(); + + // Setup result button + const resultBtn = getElById("result-btn"); + resultBtn.onclick = () => { + if (!cropt) return; + const cropAndViewportInfo = cropt.get(); + console.log(`Image parameters [cropt.get()]:`, JSON.stringify(cropAndViewportInfo)); + + cropt.toCanvas(outputSize).then((canvas: HTMLCanvasElement) => { + if (cropt) popupResult(canvas.toDataURL(), cropt.options.viewport.borderRadius); + }); + }; + + // Setup tab switching + document.querySelectorAll('[data-bs-toggle="tab"]').forEach(tab => { + tab.addEventListener('shown.bs.tab', (event) => { + const target = (event.target as HTMLElement).dataset.bsTarget?.substring(1); + + if (target && activeDemo !== target) { + activeDemo = target; + initializeDemo(target); + setupControls(); + setCode(); + if (target === 'demo1') { + bindControlEvents(); + bindFileUpload(); + } + } + }); + }); } demoMain(); + From a0469cf1f9a9a35260c060ed849844e37693d080 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Tue, 23 Dec 2025 01:46:35 -0500 Subject: [PATCH 13/53] Cleanup of demo code --- demo/index.html | 6 +++--- src/demo.ts | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/demo/index.html b/demo/index.html index 2a81e3b..1773e3f 100644 --- a/demo/index.html +++ b/demo/index.html @@ -83,7 +83,7 @@

-

Cropping from restored configuration:

+

Cropping from restored preset:

@@ -105,7 +105,7 @@

@@ -123,8 +159,17 @@

Code

@@ -132,25 +177,46 @@

Code

-
+                    
                         
                     
-
-
+
- - + +
- +
- +
diff --git a/src/cropt.css b/src/cropt.css index 25bb021..ef9ecfe 100644 --- a/src/cropt.css +++ b/src/cropt.css @@ -105,4 +105,3 @@ background: transparent; cursor: pointer; } - diff --git a/src/cropt.ts b/src/cropt.ts index 525b3a2..a56f951 100644 --- a/src/cropt.ts +++ b/src/cropt.ts @@ -111,7 +111,7 @@ export class Cropt { resizeBars: false, enableRotate: false, }; - + #boundZoom: number | undefined = undefined; #scale = 1; #rotation = 0; @@ -155,7 +155,7 @@ export class Cropt { this.elements.zoomer.value = "1"; this.elements.zoomer.setAttribute("aria-label", "zoom"); - if( this.options.enableRotate ) { + if (this.options.enableRotate) { this.elements.rotateLeft.type = "button"; this.elements.rotateLeft.innerHTML = "↺"; this.elements.rotateLeft.setAttribute("aria-label", "rotate left"); @@ -170,7 +170,7 @@ export class Cropt { this.elements.toolBar.appendChild(this.elements.rotateRight); } this.elements.toolBar.appendChild(this.elements.zoomer); - + this.element.appendChild(this.elements.boundary); this.element.appendChild(this.elements.toolBar); @@ -185,23 +185,33 @@ export class Cropt { * Passing in preset transform/viewport parameters will restore to those * Returns a Promise which resolves when the image has been loaded and state is initialized. */ - bind(src: string, preset?: number | { - transform: { x: number; y: number; scale: number, rotate: number, origin: { x: number; y: number } }; - viewport: { width: number; height: number; borderRadius: string }; - }) { - + bind( + src: string, + preset?: + | number + | { + transform: { + x: number; + y: number; + scale: number; + rotate: number; + origin: { x: number; y: number }; + }; + viewport: { width: number; height: number; borderRadius: string }; + }, + ) { if (!src) { throw new Error("src cannot be empty"); } - if (typeof(preset) !== 'object') { + if (typeof preset !== "object") { this.#boundZoom = preset; } return loadImage(src).then(async (img) => { this.#replaceImage(img); - if (typeof(preset) === 'object') { + if (typeof preset === "object") { this.setOptions({ viewport: preset.viewport }); // defer restore to next frame (after layout) @@ -260,10 +270,10 @@ export class Cropt { width: Math.round(this.options.viewport.width), height: Math.round(this.options.viewport.height), borderRadius: this.options.viewport.borderRadius, - } + }, }; } - + /** * Returns a Promise resolving to an HTMLCanvasElement object for the cropped image. * If size is specified, the image will be scaled with its longest side set to size. @@ -341,13 +351,13 @@ export class Cropt { } async setRotation(degrees: number) { - if( degrees === undefined ) return; + if (degrees === undefined) return; // Normalize to 0, 90, 180, 270 const normalizedDegrees = ((degrees % 360) + 360) % 360; const deltaRotation = normalizedDegrees - this.#rotation; - if( deltaRotation === 0 ) return; // No change + if (deltaRotation === 0) return; // No change this.#rotation = normalizedDegrees; @@ -363,12 +373,12 @@ export class Cropt { // For 90 or 270 degree rotations, swap width and height const isRotated90 = Math.abs(degrees % 180) === 90; - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); canvas.width = isRotated90 ? bitmap.height : bitmap.width; canvas.height = isRotated90 ? bitmap.width : bitmap.height; - const ctx = canvas.getContext('2d'); - if (!ctx) throw new Error('Could not get canvas context'); + const ctx = canvas.getContext("2d"); + if (!ctx) throw new Error("Could not get canvas context"); // Rotate and draw ctx.translate(canvas.width / 2, canvas.height / 2); @@ -378,11 +388,14 @@ export class Cropt { // Convert to blob and update img src const blob = await new Promise((resolve, reject) => { - canvas.toBlob((b) => b ? resolve(b) : reject(new Error('Failed to create blob')), 'image/png'); + canvas.toBlob( + (b) => (b ? resolve(b) : reject(new Error("Failed to create blob"))), + "image/png", + ); }); // Revoke old URL and set new one - if (img.src.startsWith('blob:')) URL.revokeObjectURL(img.src); + if (img.src.startsWith("blob:")) URL.revokeObjectURL(img.src); img.src = URL.createObjectURL(blob); await img.decode(); // Wait for decode without onload event @@ -392,7 +405,7 @@ export class Cropt { this.#abortController.abort(); // Clean up blob URL if it exists - if (this.elements.preview.src.startsWith('blob:')) { + if (this.elements.preview.src.startsWith("blob:")) { URL.revokeObjectURL(this.elements.preview.src); } @@ -404,11 +417,16 @@ export class Cropt { // adjust preview tranform styles (& transformOrigin) // NOTE: rotate is handled by physically rotating the image, not CSS transform - #previewTransform(data?: {x?: number, y?: number, scale?: number, origin?: {x?: number, y?: number}}): {x: number, y: number, scale: number, rotate: number, origin: {x: number, y: number}} { - const el = this.elements.preview + #previewTransform(data?: { + x?: number; + y?: number; + scale?: number; + origin?: { x?: number; y?: number }; + }): { x: number; y: number; scale: number; rotate: number; origin: { x: number; y: number } } { + const el = this.elements.preview; - const parseOrigin = (): {x: number, y: number} => { - const [oxStr, oyStr] = (el.style.transformOrigin || '0px 0px').split(' '); + const parseOrigin = (): { x: number; y: number } => { + const [oxStr, oyStr] = (el.style.transformOrigin || "0px 0px").split(" "); return { x: parseFloat(oxStr) || 0, y: parseFloat(oyStr) || 0, @@ -427,31 +445,33 @@ export class Cropt { el.style.transformOrigin = `${ox}px ${oy}px`; } - return {x, y, scale, rotate: this.#rotation, origin: parseOrigin()}; + return { x, y, scale, rotate: this.#rotation, origin: parseOrigin() }; } // no data so PARSE current element and pass out - const str = el.style.transform || ''; - let x = 0, y = 0, scale = 1; + const str = el.style.transform || ""; + let x = 0, + y = 0, + scale = 1; - for (const action of ['translate', 'scale']) { + for (const action of ["translate", "scale"]) { const regex = new RegExp(`${action}\s*\\(([^)]+)\\)`); const match = str.match(regex); if (match) { const value = match[1].trim(); - if (action === 'translate') { - const [xStr, yStr] = value.split(',').map(v => v.trim()); + if (action === "translate") { + const [xStr, yStr] = value.split(",").map((v) => v.trim()); x = Math.round(parseFloat(xStr)) || 0; - y = yStr !== undefined ? (Math.round(parseFloat(yStr)) || 0) : x; - } else if (action === 'scale') { + y = yStr !== undefined ? Math.round(parseFloat(yStr)) || 0 : x; + } else if (action === "scale") { scale = parseFloat(value) || 1; } } } - return {x, y, scale, rotate: this.#rotation, origin: parseOrigin()}; + return { x, y, scale, rotate: this.#rotation, origin: parseOrigin() }; } #setOptionsCss() { @@ -466,18 +486,18 @@ export class Cropt { #setupControlOverlay() { // currently only resize, if off, nothing to do - if( !this.options.resizeBars ) return + if (!this.options.resizeBars) return; const { resizeHandleRight, resizeHandleBottom } = this.elements; // Style right handle - 44px touch zone with 10px visual indicator - resizeHandleRight.classList.add("cr-resize-handle","cr-resize-handle-right"); + resizeHandleRight.classList.add("cr-resize-handle", "cr-resize-handle-right"); const resizeHandleRightGrabber = document.createElement("div"); resizeHandleRightGrabber.classList.add("cr-resize-handle-grabber"); resizeHandleRight.appendChild(resizeHandleRightGrabber); // Style bottom handle - 44px touch zone with 10px visual indicator - resizeHandleBottom.classList.add("cr-resize-handle","cr-resize-handle-bottom") + resizeHandleBottom.classList.add("cr-resize-handle", "cr-resize-handle-bottom"); const resizeHandleBottomGrabber = document.createElement("div"); resizeHandleBottomGrabber.classList.add("cr-resize-handle-grabber"); resizeHandleBottom.appendChild(resizeHandleBottomGrabber); @@ -492,7 +512,7 @@ export class Cropt { } #updateControlHandlePositions() { - if( !this.options.resizeBars ) return; + if (!this.options.resizeBars) return; const { resizeHandleRight, resizeHandleBottom, viewport, boundary } = this.elements; const width = this.options.viewport.width; @@ -526,7 +546,7 @@ export class Cropt { const rightPointerMove = (ev: PointerEvent) => { ev.preventDefault(); const deltaX = ev.pageX - rightStartX; - const maxWidth = Math.floor(this.elements.boundary.clientWidth*0.95); + const maxWidth = Math.floor(this.elements.boundary.clientWidth * 0.95); const newWidth = Math.min(maxWidth, Math.max(MIN_SIZE, rightStartWidth + deltaX)); this.options.viewport.width = newWidth; @@ -546,11 +566,17 @@ export class Cropt { rightStartX = ev.pageX; rightStartWidth = this.options.viewport.width; - document.addEventListener("pointermove", rightPointerMove, { signal: this.#abortController.signal }); - document.addEventListener("pointerup", rightPointerUp, { signal: this.#abortController.signal }); + document.addEventListener("pointermove", rightPointerMove, { + signal: this.#abortController.signal, + }); + document.addEventListener("pointerup", rightPointerUp, { + signal: this.#abortController.signal, + }); }; - this.elements.resizeHandleRight.addEventListener("pointerdown", rightPointerDown, { signal: this.#abortController.signal }); + this.elements.resizeHandleRight.addEventListener("pointerdown", rightPointerDown, { + signal: this.#abortController.signal, + }); // Bottom handle - adjusts height let bottomStartY = 0; @@ -559,7 +585,7 @@ export class Cropt { const bottomPointerMove = (ev: PointerEvent) => { ev.preventDefault(); const deltaY = ev.pageY - bottomStartY; - const maxHeight = Math.floor(this.elements.boundary.clientHeight*0.95); + const maxHeight = Math.floor(this.elements.boundary.clientHeight * 0.95); const newHeight = Math.min(maxHeight, Math.max(MIN_SIZE, bottomStartHeight + deltaY)); this.options.viewport.height = newHeight; @@ -579,11 +605,17 @@ export class Cropt { bottomStartY = ev.pageY; bottomStartHeight = this.options.viewport.height; - document.addEventListener("pointermove", bottomPointerMove, { signal: this.#abortController.signal }); - document.addEventListener("pointerup", bottomPointerUp, { signal: this.#abortController.signal }); + document.addEventListener("pointermove", bottomPointerMove, { + signal: this.#abortController.signal, + }); + document.addEventListener("pointerup", bottomPointerUp, { + signal: this.#abortController.signal, + }); }; - this.elements.resizeHandleBottom.addEventListener("pointerdown", bottomPointerDown, { signal: this.#abortController.signal }); + this.elements.resizeHandleBottom.addEventListener("pointerdown", bottomPointerDown, { + signal: this.#abortController.signal, + }); } #getUnscaledCanvas(p: CropPoints) { @@ -763,9 +795,15 @@ export class Cropt { originalY = ev.pageY; this.#setDragState(true, this.elements.preview); - this.elements.overlay.addEventListener("pointermove", pointerMove, { signal: this.#abortController.signal }); - this.elements.overlay.addEventListener("pointerup", pointerUp, { signal: this.#abortController.signal }); - this.elements.overlay.addEventListener("pointerout", pointerUp, { signal: this.#abortController.signal }); + this.elements.overlay.addEventListener("pointermove", pointerMove, { + signal: this.#abortController.signal, + }); + this.elements.overlay.addEventListener("pointerup", pointerUp, { + signal: this.#abortController.signal, + }); + this.elements.overlay.addEventListener("pointerout", pointerUp, { + signal: this.#abortController.signal, + }); }; let keyDown = (ev: KeyboardEvent) => { @@ -785,7 +823,9 @@ export class Cropt { } }; - this.elements.overlay.addEventListener("pointerdown", pointerDown, { signal: this.#abortController.signal }); + this.elements.overlay.addEventListener("pointerdown", pointerDown, { + signal: this.#abortController.signal, + }); document.addEventListener("keydown", keyDown, { signal: this.#abortController.signal }); } @@ -808,16 +848,20 @@ export class Cropt { this.setZoom(this.#scale + delta * this.#scale); }; - this.elements.zoomer.addEventListener("input", change, { signal: this.#abortController.signal }); - this.elements.boundary.addEventListener("wheel", scroll, { signal: this.#abortController.signal }); + this.elements.zoomer.addEventListener("input", change, { + signal: this.#abortController.signal, + }); + this.elements.boundary.addEventListener("wheel", scroll, { + signal: this.#abortController.signal, + }); } #onZoom() { - const transform = this.#previewTransform() + const transform = this.#previewTransform(); this.#scale = parseFloat(this.elements.zoomer.value); transform.scale = this.#scale; // this.#previewTransform(transform) - + const boundaries = this.#getVirtualBoundaries(); const transBoundaries = boundaries.translate; const oBoundaries = boundaries.origin; @@ -847,12 +891,18 @@ export class Cropt { } #initializeRotate() { - if( !this.options.enableRotate ) return + if (!this.options.enableRotate) return; - this.elements.rotateLeft.addEventListener("click", - () => this.setRotation(this.#rotation - 90), { signal: this.#abortController.signal }); - this.elements.rotateRight.addEventListener("click", - () => this.setRotation(this.#rotation + 90), { signal: this.#abortController.signal }); + this.elements.rotateLeft.addEventListener( + "click", + () => this.setRotation(this.#rotation - 90), + { signal: this.#abortController.signal }, + ); + this.elements.rotateRight.addEventListener( + "click", + () => this.setRotation(this.#rotation + 90), + { signal: this.#abortController.signal }, + ); } #replaceImage(img: HTMLImageElement) { @@ -895,7 +945,7 @@ export class Cropt { } // resets values to calculate zoom limits - const transformReset = {x: 0, y: 0, scale: 1, origin: {x: 0, y: 0}} + const transformReset = { x: 0, y: 0, scale: 1, origin: { x: 0, y: 0 } }; this.#previewTransform(transformReset); this.#updateZoomLimits(); @@ -905,10 +955,15 @@ export class Cropt { this.#updateOverlay(); } - #updateCenterPoint(transform: {x: number, y: number, scale: number, origin?: {x: number, y: number}}) { + #updateCenterPoint(transform: { + x: number; + y: number; + scale: number; + origin?: { x: number; y: number }; + }) { const vpData = this.elements.viewport.getBoundingClientRect(); const data = this.elements.preview.getBoundingClientRect(); - const { origin } = this.#previewTransform() + const { origin } = this.#previewTransform(); const top = vpData.top - data.top + vpData.height / 2; const left = vpData.left - data.left + vpData.width / 2; @@ -920,7 +975,7 @@ export class Cropt { transform.x = Math.round(transform.x - (center.x - (origin.x ?? 0)) * (1 - this.#scale)); transform.y = Math.round(transform.y - (center.y - (origin.y ?? 0)) * (1 - this.#scale)); - this.#previewTransform({ ...transform, origin: center }) + this.#previewTransform({ ...transform, origin: center }); } #updateZoomLimits(skipCenter = false) { @@ -949,7 +1004,7 @@ export class Cropt { this.setZoom(zoom); } - + #centerImage() { const imgDim = this.elements.preview.getBoundingClientRect(); const vpDim = this.elements.viewport.getBoundingClientRect(); @@ -960,6 +1015,6 @@ export class Cropt { const x = vpLeft - (imgDim.width - vpDim.width) / 2; const y = vpTop - (imgDim.height - vpDim.height) / 2; - this.#updateCenterPoint({x, y, scale: this.#scale}); + this.#updateCenterPoint({ x, y, scale: this.#scale }); } -} \ No newline at end of file +} diff --git a/src/demo.ts b/src/demo.ts index 20e772d..7662344 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -37,7 +37,13 @@ interface DemoConfig { enableRotate?: boolean; }; preset: null | { - transform: { x: number; y: number; scale: number; rotate: number; origin: { x: number; y: number } }; + transform: { + x: number; + y: number; + scale: number; + rotate: number; + origin: { x: number; y: number }; + }; viewport: { width: number; height: number; borderRadius: string }; }; hideControls: boolean; @@ -55,8 +61,8 @@ const demoConfigs: Record = { }, preset: null, hideControls: false, - notes: '', - getRandomImage: () => "photos/" + photos[Math.floor(Math.random() * photos.length)] + notes: "", + getRandomImage: () => "photos/" + photos[Math.floor(Math.random() * photos.length)], }, demo2: { id: "crop-demo-2", @@ -64,23 +70,24 @@ const demoConfigs: Record = { viewport: { width: 1, height: 1, borderRadius: "0%" }, }, preset: { - "transform": { - "x": -857, - "y": -752, - "scale": 0.685, - "rotate": 0, - "origin": { "x": 1017.26, "y": 911 } + transform: { + x: -857, + y: -752, + scale: 0.685, + rotate: 0, + origin: { x: 1017.26, y: 911 }, + }, + viewport: { + width: 252, + height: 128, + borderRadius: "33%", }, - "viewport": { - "width": 252, - "height": 128, - "borderRadius": "33%" - } }, hideControls: true, - notes: "Passing a previously captured:
    cropt.get()
restores viewport " - +"and image position. See the
    cropt.bind()
section. Look in the console.log for output from cropt.get().", - getRandomImage: () => "photos/kitten.jpg" + notes: + "Passing a previously captured:
    cropt.get()
restores viewport " + + "and image position. See the
    cropt.bind()
section. Look in the console.log for output from cropt.get().", + getRandomImage: () => "photos/kitten.jpg", }, demo3: { id: "crop-demo-3", @@ -91,41 +98,47 @@ const demoConfigs: Record = { }, preset: null, hideControls: true, - notes: "Note the grab bars on the viewport, you can manually adjust the sizing of viewport." - +"

Note for rotation, if you are using the crop coordinates, you must rotate " - +"the image FIRST, then the crop coordinates apply.", - getRandomImage: () => "photos/" + photos[Math.floor(Math.random() * photos.length)] - } + notes: + "Note the grab bars on the viewport, you can manually adjust the sizing of viewport." + + "

Note for rotation, if you are using the crop coordinates, you must rotate " + + "the image FIRST, then the crop coordinates apply.", + getRandomImage: () => "photos/" + photos[Math.floor(Math.random() * photos.length)], + }, }; -let activeDemo = 'demo1'; +let activeDemo = "demo1"; let cropt: Cropt | null = null; let config = demoConfigs[activeDemo]; let cropEl: HTMLElement | null = null; -let imgSrc = ''; +let imgSrc = ""; function getCode() { const optionStr = JSON.stringify(config.options, undefined, 4); - let bindPreset = ''; + let bindPreset = ""; if (config.preset) { - bindPreset = `\n// Using a preset here (ignoring initial viewport setup):\nconst preset = ${JSON.stringify(config.preset)}` - + `\n// Pass to bind():`; + bindPreset = + `\n// Using a preset here (ignoring initial viewport setup):\nconst preset = ${JSON.stringify(config.preset)}` + + `\n// Pass to bind():`; } - return `import { Cropt } from "cropt"; + return `import { Cropt } from "cropt"; // npm install cropt const cropEl = document.getElementById("${config.id}"); const resultBtn = document.getElementById("result-btn"); const cropt = new Cropt(cropEl, ${optionStr}); ${bindPreset} -cropt.bind("${imgSrc}"${bindPreset ? ', preset' : ''}); +cropt.bind("${imgSrc}"${bindPreset ? ", preset" : ""}); -resultBtn.addEventListener("click", () => {${bindPreset ? ` +resultBtn.addEventListener("click", () => {${ + bindPreset + ? ` // Read the crop & viewport details this way... const cropAndViewportInfo = cropt.get(); console.log(\`Image parameters [cropt.get()]:\`) - console.log( JSON.stringify(cropAndViewportInfo) );\n` : ''} + console.log( JSON.stringify(cropAndViewportInfo) );\n` + : "" + } cropt.toCanvas(${outputSize}).then((canvas) => { let url = canvas.toDataURL(); // Display in modal dialog. @@ -146,24 +159,24 @@ function setCode() { function setupControls() { const config = demoConfigs[activeDemo]; - const controlsContainer = document.getElementById('controls'); - const notesContainer = document.getElementById('notes'); - if (controlsContainer) controlsContainer.classList.toggle('d-none', config.hideControls); + const controlsContainer = document.getElementById("controls"); + const notesContainer = document.getElementById("notes"); + if (controlsContainer) controlsContainer.classList.toggle("d-none", config.hideControls); if (notesContainer) { - notesContainer.classList.toggle('d-none', !config.hideControls); + notesContainer.classList.toggle("d-none", !config.hideControls); notesContainer.innerHTML = config.notes; } if (!config.hideControls) { const borderRadiusRange = getElById("borderRadiusRange") as HTMLInputElement; borderRadiusRange.value = parseInt(config.options.viewport.borderRadius).toString(); - + const widthRange = getElById("widthRange") as HTMLInputElement; widthRange.value = config.options.viewport.width.toString(); - + const heightRange = getElById("heightRange") as HTMLInputElement; heightRange.value = config.options.viewport.height.toString(); - + if (config.options.mouseWheelZoom) { const mouseWheelSelect = getElById("mouseWheelSelect") as HTMLSelectElement; mouseWheelSelect.value = config.options.mouseWheelZoom; @@ -174,7 +187,7 @@ function setupControls() { function bindControlEvents() { const config = demoConfigs[activeDemo]; if (config.hideControls) return; - + const borderRadiusRange = getElById("borderRadiusRange") as HTMLInputElement; borderRadiusRange.oninput = () => { const activeConfig = demoConfigs[activeDemo]; @@ -182,7 +195,7 @@ function bindControlEvents() { setCode(); cropt?.setOptions(activeConfig.options); }; - + const widthRange = getElById("widthRange") as HTMLInputElement; widthRange.oninput = () => { const activeConfig = demoConfigs[activeDemo]; @@ -190,7 +203,7 @@ function bindControlEvents() { setCode(); cropt?.setOptions(activeConfig.options); }; - + const heightRange = getElById("heightRange") as HTMLInputElement; heightRange.oninput = () => { const activeConfig = demoConfigs[activeDemo]; @@ -198,7 +211,7 @@ function bindControlEvents() { setCode(); cropt?.setOptions(activeConfig.options); }; - + const mouseWheelSelect = getElById("mouseWheelSelect") as HTMLSelectElement; mouseWheelSelect.onchange = () => { const activeConfig = demoConfigs[activeDemo]; @@ -212,19 +225,19 @@ function bindControlEvents() { function bindFileUpload() { const fileInput = getElById("imgFile") as HTMLInputElement; fileInput.value = ""; - + fileInput.onchange = () => { if (fileInput.files?.[0]) { const file = fileInput.files[0]; const reader = new FileReader(); - + reader.onload = (e) => { const result = e.target?.result; if (typeof result === "string" && cropt) { cropt.bind(result); } }; - + reader.readAsDataURL(file); } }; @@ -234,16 +247,16 @@ function initializeDemo(demoKey: string) { config = demoConfigs[demoKey]; cropEl = getElById(config.id); imgSrc = config.getRandomImage(); - + // Destroy existing instance if present if (cropt) { cropt.destroy(); - console.log( `Destroyed prior cropt instance;`); + console.log(`Destroyed prior cropt instance;`); } - + // Create new instance cropt = new Cropt(cropEl, config.options); - console.log( `Initialized new Cropt() instance (${demoKey}).` ); + console.log(`Initialized new Cropt() instance (${demoKey}).`); if (config.preset) { cropt.bind(imgSrc, config.preset); @@ -254,7 +267,7 @@ function initializeDemo(demoKey: string) { function demoMain() { // Initial setup - initializeDemo('demo1'); + initializeDemo("demo1"); setupControls(); setCode(); bindControlEvents(); @@ -266,23 +279,23 @@ function demoMain() { if (!cropt) return; const cropAndViewportInfo = cropt.get(); console.log(`Image parameters [cropt.get()]:`, JSON.stringify(cropAndViewportInfo)); - + cropt.toCanvas(outputSize).then((canvas: HTMLCanvasElement) => { if (cropt) popupResult(canvas.toDataURL(), cropt.options.viewport.borderRadius); }); }; - + // Setup tab switching - document.querySelectorAll('[data-bs-toggle="tab"]').forEach(tab => { - tab.addEventListener('shown.bs.tab', (event) => { + document.querySelectorAll('[data-bs-toggle="tab"]').forEach((tab) => { + tab.addEventListener("shown.bs.tab", (event) => { const target = (event.target as HTMLElement).dataset.bsTarget?.substring(1); - + if (target && activeDemo !== target) { activeDemo = target; initializeDemo(target); setupControls(); setCode(); - if (activeDemo === 'demo1') { + if (activeDemo === "demo1") { bindControlEvents(); bindFileUpload(); } @@ -292,4 +305,3 @@ function demoMain() { } demoMain(); - From 43324afa1f2fddeda6932d412b77f0c1492736c8 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Tue, 23 Dec 2025 12:25:28 -0500 Subject: [PATCH 19/53] Added options to disable zoom slider, revised option names --- README.md | 21 +++++++++++++ demo/index.html | 2 +- src/cropt.ts | 82 ++++++++++++++++++++++++++----------------------- src/demo.ts | 8 +++-- 4 files changed, 72 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 57bd70a..4c227cc 100755 --- a/README.md +++ b/README.md @@ -57,6 +57,20 @@ Default value: `"cr-slider"` Optionally set a different class on the zoom range input to customize styling (e.g. set to `"form-range"` when using Bootstrap). +### `enableZoomSlider` + +Type: `boolean` +Default value: `true` + +Toggle if hiding the zoom slider. + +### `enableKeypress` + +Type: `boolean` +Default value: `true` + +Toggle if allow listening for keyboard arrow keys for moving image. + ### `resizeBars` Type: `boolean` @@ -64,6 +78,13 @@ Default value: `false` Optionally to show resize handles (grab-bars) to adjust the viewport width/height. +### `enableRotateBtns` + +Type: `boolean` +Default value: `false` + +Toggle if showing rotation buttons beside the zoom slider bar. If both are off (enableZoomSlider and this), the toolbar is hidden. + ## Methods ### `bind(src: string, preset: number | { transform, viewport }): Promise` diff --git a/demo/index.html b/demo/index.html index 802544b..6301794 100644 --- a/demo/index.html +++ b/demo/index.html @@ -61,7 +61,7 @@

- Cropt v1.0 + Cropt v2.0

Cropt is a modern, lightweight image cropper with zero dependencies. diff --git a/src/cropt.ts b/src/cropt.ts index a56f951..05aa19c 100644 --- a/src/cropt.ts +++ b/src/cropt.ts @@ -74,10 +74,11 @@ export interface CroptOptions { borderRadius: string; }; zoomerInputClass: string; - resizeBars?: boolean; - enableRotate?: boolean; + enableZoomSlider?: boolean; // show the physical slider (pinch zoom will work regardless) + enableKeypress?: boolean; // listen to arrow keys + resizeBars?: boolean; // allow on-picture resize bars + enableRotateBtns?: boolean; // passing in rotation will work regardless, but no btns will be visible } - interface CropPoints { left: number; top: number; @@ -108,8 +109,10 @@ export class Cropt { borderRadius: "0px", }, zoomerInputClass: "cr-slider", + enableZoomSlider: true, + enableKeypress: true, resizeBars: false, - enableRotate: false, + enableRotateBtns: false, }; #boundZoom: number | undefined = undefined; @@ -150,12 +153,7 @@ export class Cropt { this.elements.boundary.appendChild(this.elements.controls); this.#setupControlOverlay(); - this.elements.zoomer.type = "range"; - this.elements.zoomer.step = "0.001"; - this.elements.zoomer.value = "1"; - this.elements.zoomer.setAttribute("aria-label", "zoom"); - - if (this.options.enableRotate) { + if (this.options.enableRotateBtns) { this.elements.rotateLeft.type = "button"; this.elements.rotateLeft.innerHTML = "↺"; this.elements.rotateLeft.setAttribute("aria-label", "rotate left"); @@ -169,7 +167,14 @@ export class Cropt { this.elements.toolBar.appendChild(this.elements.rotateLeft); this.elements.toolBar.appendChild(this.elements.rotateRight); } - this.elements.toolBar.appendChild(this.elements.zoomer); + + if (this.options.enableZoomSlider) { + this.elements.zoomer.type = "range"; + this.elements.zoomer.step = "0.001"; + this.elements.zoomer.value = "1"; + this.elements.zoomer.setAttribute("aria-label", "zoom"); + this.elements.toolBar.appendChild(this.elements.zoomer); + } this.element.appendChild(this.elements.boundary); this.element.appendChild(this.elements.toolBar); @@ -235,6 +240,8 @@ export class Cropt { } #getPoints() { + const getPoint = (pos: number) => Math.round(Math.max(0, pos / this.#scale)); + const imgData = this.elements.preview.getBoundingClientRect(); const vpData = this.elements.viewport.getBoundingClientRect(); const oWidth = this.elements.viewport.offsetWidth; @@ -245,17 +252,13 @@ export class Cropt { const top = vpData.top - imgData.top; return { - left: this.#getPoint(left), - top: this.#getPoint(top), - right: this.#getPoint(left + oWidth + widthDiff), - bottom: this.#getPoint(top + oHeight + heightDiff), + left: getPoint(left), + top: getPoint(top), + right: getPoint(left + oWidth + widthDiff), + bottom: getPoint(top + oHeight + heightDiff), }; } - #getPoint(pos: number) { - return Math.round(Math.max(0, pos / this.#scale)); - } - /** * Returns: * crop { left, top, right, bottom }: the crop rectangle for image cropping outside Cropt @@ -806,27 +809,30 @@ export class Cropt { }); }; - let keyDown = (ev: KeyboardEvent) => { - if (document.activeElement !== this.elements.viewport) { - return; - } - - if (ev.shiftKey && (ev.key === "ArrowUp" || ev.key === "ArrowDown")) { - ev.preventDefault(); - let zoomVal = parseFloat(this.elements.zoomer.value); - let stepVal = ev.key === "ArrowUp" ? 0.01 : -0.01; - this.setZoom(zoomVal + stepVal); - } else if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(ev.key)) { - ev.preventDefault(); - let [deltaX, deltaY] = getArrowKeyDeltas(ev.key); - this.#assignTransformCoordinates(deltaX, deltaY); - } - }; - this.elements.overlay.addEventListener("pointerdown", pointerDown, { signal: this.#abortController.signal, }); - document.addEventListener("keydown", keyDown, { signal: this.#abortController.signal }); + + if (this.options.enableKeypress) { + let keyDown = (ev: KeyboardEvent) => { + if (document.activeElement !== this.elements.viewport) { + return; + } + + if (ev.shiftKey && (ev.key === "ArrowUp" || ev.key === "ArrowDown")) { + ev.preventDefault(); + let zoomVal = parseFloat(this.elements.zoomer.value); + let stepVal = ev.key === "ArrowUp" ? 0.01 : -0.01; + this.setZoom(zoomVal + stepVal); + } else if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(ev.key)) { + ev.preventDefault(); + let [deltaX, deltaY] = getArrowKeyDeltas(ev.key); + this.#assignTransformCoordinates(deltaX, deltaY); + } + }; + + document.addEventListener("keydown", keyDown, { signal: this.#abortController.signal }); + } } #initializeZoom() { @@ -891,7 +897,7 @@ export class Cropt { } #initializeRotate() { - if (!this.options.enableRotate) return; + if (!this.options.enableRotateBtns) return; this.elements.rotateLeft.addEventListener( "click", diff --git a/src/demo.ts b/src/demo.ts index 7662344..e61cc05 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -33,8 +33,10 @@ interface DemoConfig { viewport: { width: number; height: number; borderRadius: string }; mouseWheelZoom?: "off" | "on" | "ctrl"; zoomerInputClass?: string; + enableZoomSlider?: boolean; + enableKeypress?: boolean; resizeBars?: boolean; - enableRotate?: boolean; + enableRotateBtns?: boolean; }; preset: null | { transform: { @@ -68,6 +70,8 @@ const demoConfigs: Record = { id: "crop-demo-2", options: { viewport: { width: 1, height: 1, borderRadius: "0%" }, + enableZoomSlider: false, + enableKeypress: false, }, preset: { transform: { @@ -94,7 +98,7 @@ const demoConfigs: Record = { options: { viewport: { width: 160, height: 220, borderRadius: "7%" }, resizeBars: true, - enableRotate: true, + enableRotateBtns: true, }, preset: null, hideControls: true, From 7906d5e26a201926c03c8a362cd9e24fa53947e0 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Tue, 23 Dec 2025 12:58:44 -0500 Subject: [PATCH 20/53] Fixed up the keyboard navigation code and keyboard enablement option --- src/cropt.ts | 10 ++++++++-- src/demo.ts | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/cropt.ts b/src/cropt.ts index 05aa19c..ace6604 100644 --- a/src/cropt.ts +++ b/src/cropt.ts @@ -110,7 +110,7 @@ export class Cropt { }, zoomerInputClass: "cr-slider", enableZoomSlider: true, - enableKeypress: true, + enableKeypress: false, resizeBars: false, enableRotateBtns: false, }; @@ -815,7 +815,13 @@ export class Cropt { if (this.options.enableKeypress) { let keyDown = (ev: KeyboardEvent) => { - if (document.activeElement !== this.elements.viewport) { + // for user-input fields we skip + if ( + document.activeElement && + ["INPUT", "TEXTAREA", "SELECT", "BUTTON"].includes( + document.activeElement.nodeName, + ) + ) { return; } diff --git a/src/demo.ts b/src/demo.ts index e61cc05..ae6d991 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -60,6 +60,7 @@ const demoConfigs: Record = { viewport: { width: 220, height: 220, borderRadius: "50%" }, mouseWheelZoom: "on", zoomerInputClass: "form-range", + enableKeypress: true, }, preset: null, hideControls: false, @@ -71,7 +72,6 @@ const demoConfigs: Record = { options: { viewport: { width: 1, height: 1, borderRadius: "0%" }, enableZoomSlider: false, - enableKeypress: false, }, preset: { transform: { From 94c0271db1d22d4a4594e9834d1bca658932ddcf Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Tue, 23 Dec 2025 13:45:43 -0500 Subject: [PATCH 21/53] Include a CONTRIBUTING.md file --- CONTRIBUTING.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cf20654 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,75 @@ +# Contributing to Cropt +First off, thank you for considering contributing to Cropt! It's people like you that make Cropt such a great tool. + +## Acknowledgements +We thank our contributors for helping make Cropt even better: + +[Foliotek/Croppie](https://github.com/Foliotek/Croppie) + Originally based on Croppie, but rewritten as a modern ES6 + +[Filipe Laborde](https://github.com/mindflowgo/) + - Croppie Resize handle grabbers & get()/set() for restoring zoom/image placement + +## Quickstart +First, this is a community project, so the developers and contributors appreciate properly prepared contributions and help by others. Please review the code before making changes to keep with the format of the existing code. + +### Pull Requests +Fork the repository on GitHub + +Clone your fork locally: + +```bash +git clone https://github.com/YOUR-USERNAME/cropt.git +cd cropt +npm install +``` + +### Coding Standards +We use TypeScript for type safety. Please run your changes and submits through Prettier before submitting PR. + +``` +npm run format +``` + +Write self-documenting code with clear variable/function names; Add comments for complex logic. + +### Project Structure +cropt/ +├── src/ # Source code +│ ├── cropt.ts # Main Cropt class +│ └── demo.ts # Demo application +├── dist/ # Compiled output +├── docs/ # Documentation +├── tests/ # Test files +├── package.json +└── README.md # new options & methods document here! + +### Building and Testing +Test your changes thoroughly. Please try to keep existing behaviour and methods so it will be backwards compatible. + +```bash +npm run prepare +npm start +``` + +### Documentation +Keep documentation up to date with code changes + +Update README.md when adding new features + +Add JSDoc comments for public APIs + +Update demo examples when features warrant. Create another tab in the Demo page to showcase them. + +### Reporting Issues +When reporting issues, please include: +- Clear description of the problem +- Steps to reproduce the issue +- Expected behavior vs Actual behavior +- Screenshots if applicable +- Browser/OS information + +### Feature Requests +We welcome feature requests! Please provide links to other croppers that showcase the feature or screenshots to help us understand. Give us the use-case so we can understand better. + +And finally, consider writing the feature yourself. Use AI to help! diff --git a/README.md b/README.md index 4c227cc..e7ea2de 100755 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Toggle if hiding the zoom slider. Type: `boolean` Default value: `true` -Toggle if allow listening for keyboard arrow keys for moving image. +Toggle if allow listening for keyboard arrow keys for moving image. Will ignore if active element is a user input one (input box, text area, button). ### `resizeBars` From 00fb593bd382e59caa7e1af0b76600d09af84d59 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Tue, 23 Dec 2025 13:49:54 -0500 Subject: [PATCH 22/53] Updating changelog + contributor --- CHANGELOG.md | 6 ++++++ CONTRIBUTIONS.md | 7 ------- 2 files changed, 6 insertions(+), 7 deletions(-) delete mode 100644 CONTRIBUTIONS.md diff --git a/CHANGELOG.md b/CHANGELOG.md index af59c9c..aa89fa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.0.0] - 2025-12-23 +- Extended feature set: resizeBars, rotation. +- Fixed features (keyboard image movement) +- Added get() method to get viewport/crop info +- Added preset parameter for bind() to pass in viewport/transform options to restore crop arrangement + ## [1.0.0] - 2024-12-01 ### Changed - Replaced `viewport.type` option with `viewport.borderRadius`. diff --git a/CONTRIBUTIONS.md b/CONTRIBUTIONS.md deleted file mode 100644 index 25b876d..0000000 --- a/CONTRIBUTIONS.md +++ /dev/null @@ -1,7 +0,0 @@ -We thank our contributors for helping make Cropt even better: - -[Foliotek/Croppie](https://github.com/Foliotek/Croppie) - Originally based on Croppie, but rewritten as a modern ES - -[Filipe Laborde](https://github.com/mindflowgo/) - - Croppie Resize handle grabbers & get()/set() for restoring zoom/image placement \ No newline at end of file From 7dd52d59b305719d555a54a36add3afdc4f2467a Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Thu, 25 Dec 2025 01:52:52 -0500 Subject: [PATCH 23/53] Transparency handling added; bind() loads blob now (much faster) --- CONTRIBUTING.md | 15 +++--- src/cropt.ts | 132 ++++++++++++++++++++++++++++++------------------ src/demo.ts | 13 ++--- 3 files changed, 94 insertions(+), 66 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf20654..832a643 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ We thank our contributors for helping make Cropt even better: Originally based on Croppie, but rewritten as a modern ES6 [Filipe Laborde](https://github.com/mindflowgo/) - - Croppie Resize handle grabbers & get()/set() for restoring zoom/image placement + - Croppie-like resize handle grabbers, rotation, get() + preset restoring... all in under 300 lines ## Quickstart First, this is a community project, so the developers and contributors appreciate properly prepared contributions and help by others. Please review the code before making changes to keep with the format of the existing code. @@ -22,6 +22,8 @@ Clone your fork locally: git clone https://github.com/YOUR-USERNAME/cropt.git cd cropt npm install +npm run prepare +npm start ``` ### Coding Standards @@ -36,11 +38,12 @@ Write self-documenting code with clear variable/function names; Add comments for ### Project Structure cropt/ ├── src/ # Source code -│ ├── cropt.ts # Main Cropt class -│ └── demo.ts # Demo application -├── dist/ # Compiled output -├── docs/ # Documentation -├── tests/ # Test files +| ├── cropt.css # Cropt CSS +| ├── cropt.ts # Cropt code +│ └── demo.ts # Demo javascript +├── demo/ # Demo assets (index.html, photos, styles) +├── docs/ # Documentation (future?) +├── tests/ # Test files (future?) ├── package.json └── README.md # new options & methods document here! diff --git a/src/cropt.ts b/src/cropt.ts index ace6604..a698d93 100644 --- a/src/cropt.ts +++ b/src/cropt.ts @@ -13,15 +13,27 @@ function setZoomerVal(value: number, zoomer: HTMLInputElement) { zoomer.value = Math.max(zMin, Math.min(zMax, value)).toFixed(3); } -function loadImage(src: string): Promise { - const img = new Image(); +function loadImage(src: string | Blob): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + + const cleanup = () => { + if (img.src.startsWith('blob:')) { + URL.revokeObjectURL(img.src); + } + }; - return new Promise(function (resolve, reject) { img.onload = () => { + cleanup(); resolve(img); }; - img.onerror = reject; - img.src = src; + + img.onerror = (ev) => { + cleanup(); + reject(new Error(`Failed to load image from ${typeof src === 'string' ? 'URL' : 'Blob'}`)); + }; + + img.src = typeof src === 'string' ? src : URL.createObjectURL(src); }); } @@ -118,6 +130,8 @@ export class Cropt { #boundZoom: number | undefined = undefined; #scale = 1; #rotation = 0; + // when displaying in UI or experting to jpeg, use this color for transparency sections + #transparencyColor = '#fff'; #abortController = new AbortController(); #updateOverlayDebounced = debounce(() => { this.#updateOverlay(); @@ -179,7 +193,7 @@ export class Cropt { this.element.appendChild(this.elements.boundary); this.element.appendChild(this.elements.toolBar); - this.#setOptionsCss(); + this.#setViewportCss(); this.#initDraggable(); this.#initializeZoom(); this.#initializeRotate(); @@ -191,7 +205,7 @@ export class Cropt { * Returns a Promise which resolves when the image has been loaded and state is initialized. */ bind( - src: string, + src: string | Blob, preset?: | number | { @@ -214,7 +228,7 @@ export class Cropt { } return loadImage(src).then(async (img) => { - this.#replaceImage(img); + this.#replaceImage(img); // force-replace image node (prevents caching, etc) if (typeof preset === "object") { this.setOptions({ viewport: preset.viewport }); @@ -230,7 +244,7 @@ export class Cropt { // Then apply scale and position this.setZoom(preset.transform.scale); - this.#previewTransform(preset.transform); + this.#transformPreview(preset.transform); this.#updateOverlay(); }, 0); } else { @@ -268,7 +282,7 @@ export class Cropt { get() { return { crop: this.#getPoints(), - transform: this.#previewTransform(), + transform: this.#transformPreview(), viewport: { width: Math.round(this.options.viewport.width), height: Math.round(this.options.viewport.height), @@ -280,14 +294,16 @@ export class Cropt { /** * Returns a Promise resolving to an HTMLCanvasElement object for the cropped image. * If size is specified, the image will be scaled with its longest side set to size. + * Otherwise (size = null), full-size cropped area is returned */ - toCanvas(size: number | null = null) { + toCanvas(size: number | null = null, type: string = '') { const vpRect = this.elements.viewport.getBoundingClientRect(); const ratio = vpRect.width / vpRect.height; const points = this.#getPoints(); let width = points.right - points.left; let height = points.bottom - points.top; + // resize only if size passed in if (size !== null) { if (ratio > 1) { width = size; @@ -298,7 +314,7 @@ export class Cropt { } } - return Promise.resolve(this.#getCanvas(points, width, height)); + return Promise.resolve(this.#getCanvas(points, width, height, type)); } toBlob(size: number | null = null, type = "image/webp", quality = 1): Promise { @@ -307,7 +323,7 @@ export class Cropt { } return new Promise((resolve, reject) => { - this.toCanvas(size).then((canvas) => { + this.toCanvas(size,type).then((canvas) => { canvas.toBlob( (blob) => { if (blob === null) { @@ -337,7 +353,7 @@ export class Cropt { // changed: removed structuredClone: slow, and would fail passing functions in options this.options = { ...this.options, ...options } as CroptOptions; - this.#setOptionsCss(); + this.#setViewportCss(); if ( this.options.viewport.width !== curWidth || @@ -420,7 +436,7 @@ export class Cropt { // adjust preview tranform styles (& transformOrigin) // NOTE: rotate is handled by physically rotating the image, not CSS transform - #previewTransform(data?: { + #transformPreview(data?: { x?: number; y?: number; scale?: number; @@ -477,14 +493,15 @@ export class Cropt { return { x, y, scale, rotate: this.#rotation, origin: parseOrigin() }; } - #setOptionsCss() { + #setViewportCss() { this.elements.zoomer.className = this.options.zoomerInputClass; const viewport = this.elements.viewport; viewport.style.borderRadius = this.options.viewport.borderRadius; viewport.style.width = this.options.viewport.width + "px"; viewport.style.height = this.options.viewport.height + "px"; - this.#updateControlHandlePositions(); // move controls overlayed (when necessary) + // whenever viewport changes - need to move controls overlayed! + this.#updateControlHandlePositions(); } #setupControlOverlay() { @@ -511,7 +528,8 @@ export class Cropt { // Initialize resize handlers this.#initControlHandlers(); - this.#updateControlHandlePositions(); + // Hack - delay setup for UI layout to finalize (for larger images esp) + setTimeout( ()=> this.#updateControlHandlePositions(), 200 ); } #updateControlHandlePositions() { @@ -553,7 +571,7 @@ export class Cropt { const newWidth = Math.min(maxWidth, Math.max(MIN_SIZE, rightStartWidth + deltaX)); this.options.viewport.width = newWidth; - this.#setOptionsCss(); + this.#setViewportCss(); }; const rightPointerUp = () => { @@ -592,7 +610,7 @@ export class Cropt { const newHeight = Math.min(maxHeight, Math.max(MIN_SIZE, bottomStartHeight + deltaY)); this.options.viewport.height = newHeight; - this.#setOptionsCss(); + this.#setViewportCss(); }; const bottomPointerUp = () => { @@ -638,8 +656,11 @@ export class Cropt { return canvas; } - - #getCanvas(points: CropPoints, width: number, height: number) { + + #getCanvas(points: CropPoints, width: number, height: number, type: string) { + // cannot draw from a canvas into itself while resizing — it causes visual corruption + // ping-pong oc -> buffer -> oc .... + console.time('getCanvas') const oc = this.#getUnscaledCanvas(points); const octx = oc.getContext("2d"); const buffer = document.createElement("canvas"); @@ -652,35 +673,44 @@ export class Cropt { if (ctx === null || octx === null || bctx === null) { throw new Error("Canvas context cannot be null"); } - - let cur = { + + let to = { width: oc.width, height: oc.height, }; - while (cur.width * 0.5 > canvas.width) { + while (to.width > canvas.width * 2) { // step down size by one half for smooth scaling - let curWidth = cur.width; - let curHeight = cur.height; + let w = to.width; + let h = to.height; - cur = { - width: Math.floor(cur.width * 0.5), - height: Math.floor(cur.height * 0.5), + to = { + width: Math.floor(to.width / 2), + height: Math.floor(to.height / 2), }; // write oc to buffer - buffer.width = curWidth; - buffer.height = curHeight; + buffer.width = w; + buffer.height = h; bctx.clearRect(0, 0, buffer.width, buffer.height); bctx.drawImage(oc, 0, 0); // clear oc - octx.clearRect(0, 0, curWidth, curHeight); + octx.clearRect(0, 0, w, h); - octx.drawImage(buffer, 0, 0, curWidth, curHeight, 0, 0, cur.width, cur.height); + octx.drawImage(buffer, 0, 0, w, h, 0, 0, to.width, to.height); } - ctx.drawImage(oc, 0, 0, cur.width, cur.height, 0, 0, canvas.width, canvas.height); + // if jpeg we fill with transparencyColor in case the canvas has alpha + if (type === 'image/jpeg') { + ctx.fillStyle = this.#transparencyColor; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + ctx.drawImage(oc, 0, 0, to.width, to.height, 0, 0, canvas.width, canvas.height); + + oc.width = oc.height = 0; // ios hint to free + buffer.width = buffer.height = 0; + console.timeEnd('getCanvas') return canvas; } @@ -717,7 +747,7 @@ export class Cropt { #assignTransformCoordinates(deltaX: number, deltaY: number) { const imgRect = this.elements.preview.getBoundingClientRect(); const vpRect = this.elements.viewport.getBoundingClientRect(); - const transform = this.#previewTransform(); + const transform = this.#transformPreview(); transform.y += clampDelta(vpRect.top - imgRect.top, deltaY, vpRect.bottom - imgRect.bottom); transform.x += clampDelta(vpRect.left - imgRect.left, deltaX, vpRect.right - imgRect.right); @@ -842,15 +872,17 @@ export class Cropt { } #initializeZoom() { - const change = () => { - this.#onZoom(); - }; + this.elements.zoomer.addEventListener("input", ()=>this.#onZoom(), { + signal: this.#abortController.signal, + }); + + if (this.options.mouseWheelZoom === "off") return; const scroll = (ev: WheelEvent) => { const optionVal = this.options.mouseWheelZoom; let delta = 0; - if (optionVal === "off" || (optionVal === "ctrl" && !ev.ctrlKey)) { + if (optionVal === "ctrl" && !ev.ctrlKey) { return; } else if (ev.deltaY) { delta = (ev.deltaY * -1) / 2000; @@ -860,19 +892,16 @@ export class Cropt { this.setZoom(this.#scale + delta * this.#scale); }; - this.elements.zoomer.addEventListener("input", change, { - signal: this.#abortController.signal, - }); this.elements.boundary.addEventListener("wheel", scroll, { signal: this.#abortController.signal, }); } #onZoom() { - const transform = this.#previewTransform(); + const transform = this.#transformPreview(); this.#scale = parseFloat(this.elements.zoomer.value); transform.scale = this.#scale; - // this.#previewTransform(transform) + // this.#transformPreview(transform) const boundaries = this.#getVirtualBoundaries(); const transBoundaries = boundaries.translate; @@ -898,7 +927,7 @@ export class Cropt { transform.y = transBoundaries.minY; } - this.#previewTransform(transform); + this.#transformPreview(transform); this.#updateOverlayDebounced(); } @@ -919,6 +948,8 @@ export class Cropt { #replaceImage(img: HTMLImageElement) { this.#setPreviewAttributes(img); + + // replace whole child node with new one - prevents caching issues, attach listeners, etc. if (this.elements.preview.parentNode) { this.elements.preview.parentNode.replaceChild(img, this.elements.preview); } @@ -927,6 +958,7 @@ export class Cropt { #setPreviewAttributes(preview: HTMLImageElement) { preview.classList.add("cr-image"); + preview.style.background = this.#transparencyColor; // if transparency want this color to show preview.setAttribute("alt", "preview"); this.#setDragState(false, preview); } @@ -958,11 +990,11 @@ export class Cropt { // resets values to calculate zoom limits const transformReset = { x: 0, y: 0, scale: 1, origin: { x: 0, y: 0 } }; - this.#previewTransform(transformReset); + this.#transformPreview(transformReset); this.#updateZoomLimits(); transformReset.scale = this.#scale; - this.#previewTransform(transformReset); + this.#transformPreview(transformReset); this.#centerImage(); this.#updateOverlay(); } @@ -975,7 +1007,7 @@ export class Cropt { }) { const vpData = this.elements.viewport.getBoundingClientRect(); const data = this.elements.preview.getBoundingClientRect(); - const { origin } = this.#previewTransform(); + const { origin } = this.#transformPreview(); const top = vpData.top - data.top + vpData.height / 2; const left = vpData.left - data.left + vpData.width / 2; @@ -987,7 +1019,7 @@ export class Cropt { transform.x = Math.round(transform.x - (center.x - (origin.x ?? 0)) * (1 - this.#scale)); transform.y = Math.round(transform.y - (center.y - (origin.y ?? 0)) * (1 - this.#scale)); - this.#previewTransform({ ...transform, origin: center }); + this.#transformPreview({ ...transform, origin: center }); } #updateZoomLimits(skipCenter = false) { diff --git a/src/demo.ts b/src/demo.ts index ae6d991..22c6f55 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -233,16 +233,9 @@ function bindFileUpload() { fileInput.onchange = () => { if (fileInput.files?.[0]) { const file = fileInput.files[0]; - const reader = new FileReader(); - - reader.onload = (e) => { - const result = e.target?.result; - if (typeof result === "string" && cropt) { - cropt.bind(result); - } - }; - - reader.readAsDataURL(file); + if (cropt) { + cropt.bind(file); + } } }; } From 7799e185585f15766d40fdc99e0d5ef4bc47b982 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Thu, 25 Dec 2025 02:08:06 -0500 Subject: [PATCH 24/53] Reverted unnecessary change in loadImage() --- src/cropt.ts | 20 ++++---------------- src/demo.ts | 5 ++++- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/cropt.ts b/src/cropt.ts index a698d93..59b757b 100644 --- a/src/cropt.ts +++ b/src/cropt.ts @@ -13,27 +13,15 @@ function setZoomerVal(value: number, zoomer: HTMLInputElement) { zoomer.value = Math.max(zMin, Math.min(zMax, value)).toFixed(3); } -function loadImage(src: string | Blob): Promise { +function loadImage(src: string): Promise { return new Promise((resolve, reject) => { const img = new Image(); - const cleanup = () => { - if (img.src.startsWith('blob:')) { - URL.revokeObjectURL(img.src); - } - }; - img.onload = () => { - cleanup(); resolve(img); }; - - img.onerror = (ev) => { - cleanup(); - reject(new Error(`Failed to load image from ${typeof src === 'string' ? 'URL' : 'Blob'}`)); - }; - - img.src = typeof src === 'string' ? src : URL.createObjectURL(src); + img.onerror = reject; + img.src = src; }); } @@ -205,7 +193,7 @@ export class Cropt { * Returns a Promise which resolves when the image has been loaded and state is initialized. */ bind( - src: string | Blob, + src: string, preset?: | number | { diff --git a/src/demo.ts b/src/demo.ts index 22c6f55..0df9709 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -234,7 +234,10 @@ function bindFileUpload() { if (fileInput.files?.[0]) { const file = fileInput.files[0]; if (cropt) { - cropt.bind(file); + if (imgSrc.startsWith('blob')) URL.revokeObjectURL(imgSrc) + imgSrc = URL.createObjectURL(file) + cropt.bind(imgSrc); + setCode(); } } }; From 536a2603e733a76b7751b55d4d205e7370e45ddf Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Tue, 30 Dec 2025 02:25:26 -0500 Subject: [PATCH 25/53] Adjusting tooling for minified versions. --- .gitignore | 2 + .npmignore | 4 + Gruntfile.cjs | 105 +- README.md | 4 +- demo/index.html | 13 +- package-lock.json | 2581 --------------------------------------------- package.json | 35 +- src/cropt.ts | 281 ++--- src/demo.ts | 27 +- tsconfig.json | 1 + 10 files changed, 284 insertions(+), 2769 deletions(-) create mode 100644 .npmignore delete mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index 03e416e..0f8b550 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ node_modules/ demo/build/ src/*.js src/*.d.ts +dist/ +package-lock.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..9d56eb2 --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +src/ +demo/styles.scss +Gruntfile.cjs +tsconfig.json \ No newline at end of file diff --git a/Gruntfile.cjs b/Gruntfile.cjs index 06d8896..84ccc3d 100644 --- a/Gruntfile.cjs +++ b/Gruntfile.cjs @@ -1,36 +1,79 @@ module.exports = function (grunt) { - grunt.initConfig({ - copy: { - dist: { - files: [ - { - expand: true, - cwd: "node_modules/bootstrap/dist/js", - src: "bootstrap.bundle.min.*", - dest: "demo/build", - }, - { - expand: true, - cwd: "src", - src: ["cropt.js", "cropt.css", "demo.js"], - dest: "demo/build", - }, - ], - }, + grunt.initConfig({ + + clean: { + demo: ["demo/build"] + }, + + terser: { + dist: { + options: { + compress: true, + mangle: true, + sourceMap: true }, - sass: { - dist: { - options: { - style: "expanded", - }, - files: { - "demo/build/bs-custom.css": "demo/styles.scss", - }, - }, + files: [{ + expand: true, + cwd: "dist", + src: ["*.js", "!*.min.js"], + dest: "dist", + ext: ".min.js" + }] + } + }, + + cssmin: { + dist: { + options: { + sourceMap: true }, - }); + files: { + "dist/cropt.min.css": "src/cropt.css" + } + } + }, + + sass: { + demo: { + options: { style: "expanded" }, + files: { + "demo/build/bs-custom.css": "demo/styles.scss" + } + } + }, + + copy: { + demo: { + files: [ + { + expand: true, + cwd: "node_modules/bootstrap/dist/js", + src: "bootstrap.bundle.min.*", + dest: "demo/build" + }, + { + expand: true, + cwd: "dist", + src: ["*.min.js", "*.min.css", "*.d.ts"], + dest: "demo/build" + } + ] + } + } + + }); + + grunt.loadNpmTasks("grunt-contrib-clean"); + grunt.loadNpmTasks("grunt-terser"); + grunt.loadNpmTasks("grunt-contrib-cssmin"); + grunt.loadNpmTasks("grunt-contrib-sass"); + grunt.loadNpmTasks("grunt-contrib-copy"); - grunt.loadNpmTasks("grunt-contrib-copy"); - grunt.loadNpmTasks("grunt-contrib-sass"); - grunt.registerTask("default", ["copy", "sass"]); + grunt.registerTask("default", [ + "clean", + "terser", + "cssmin", + "sass", + "copy" + ]); }; diff --git a/README.md b/README.md index e7ea2de..66a4af7 100755 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Originally based on [Foliotek/Croppie](https://github.com/Foliotek/Croppie), but ## Installation ``` -npm install cropt +npm install cropt2 ``` ## Running Demo @@ -17,7 +17,7 @@ npm start ## Usage -1. Include the `src/cropt.css` stylesheet on your page. +1. Include the `cropt.min.css` stylesheet on your page. 2. Add a `div` element to your HTML to hold the Cropt instance. 3. Import Cropt and bind it to an image: diff --git a/demo/index.html b/demo/index.html index 6301794..eb0b705 100644 --- a/demo/index.html +++ b/demo/index.html @@ -7,11 +7,11 @@ - - + + - + @@ -66,9 +66,12 @@

Cropt is a modern, lightweight image cropper with zero dependencies.

+

+ V2.0 built on the awesome work by DevTheorem (v1.0). +

- + Result

- + diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index f0dd309..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2581 +0,0 @@ -{ - "name": "cropt", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "cropt", - "version": "1.0.0", - "license": "MIT", - "devDependencies": { - "bootstrap": "^5.3.3", - "gh-pages": "^6.2.0", - "grunt": "^1.6.1", - "grunt-cli": "^1.5.0", - "grunt-contrib-copy": "^1.0.0", - "grunt-contrib-sass": "^2.0.0", - "http-server": "^14.1.1", - "prettier": "^3.4.1", - "sass": "^1.77.6", - "typescript": "^5.7.2" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "dev": true, - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-slice": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dev": true, - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha512-ENwblkFQpqqia6b++zLD/KUWafYlVY/UNnAp7oz7LY7E924wmpye416wBOmvv/HMWzl8gL1kJlfvId/1Dg176w==", - "dev": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/corser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", - "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/dargs": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-6.1.0.tgz", - "integrity": "sha512-5dVBvpBLBnPwSsYXqfybFyehMmC/EenKEcf23AhCTgTf48JFBbmJKqoZBsERDnjL0FyiVTYWdFsRfTLHxLyKdQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/email-addresses": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", - "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", - "dev": true - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eventemitter2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==", - "dev": true - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", - "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-sync-cmp": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz", - "integrity": "sha512-0k45oWBokCqh2MOexeYKpyqmGKG+8mQ2Wd8iawx+uWd/weWJQAZ6SoPybagdCI4xFisag8iAR77WPm4h3pTfxA==", - "dev": true - }, - "node_modules/filename-reserved-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", - "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/filenamify": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", - "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", - "dev": true, - "dependencies": { - "filename-reserved-regex": "^2.0.0", - "strip-outer": "^1.0.1", - "trim-repeated": "^1.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/findup-sync": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", - "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", - "dev": true, - "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.3", - "micromatch": "^4.0.4", - "resolve-dir": "^1.0.1" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", - "dev": true, - "dependencies": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", - "dev": true, - "dependencies": { - "for-in": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/getobject": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.2.tgz", - "integrity": "sha512-2zblDBaFcb3rB4rF77XVnuINOE2h2k/OnqXAiy0IrTxUfV1iFp3la33oAQVY9pCpWU268WFYVt2t71hlMuLsOg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/gh-pages": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.2.0.tgz", - "integrity": "sha512-HMXJ8th9u5wRXaZCnLcs/d3oVvCHiZkaP5KQExQljYGwJjQbSPyTdHe/Gc1IvYUR/rWiZLxNobIqfoMHKTKjHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "^3.2.4", - "commander": "^11.0.0", - "email-addresses": "^5.0.0", - "filenamify": "^4.3.0", - "find-cache-dir": "^3.3.1", - "fs-extra": "^11.1.1", - "globby": "^11.1.0" - }, - "bin": { - "gh-pages": "bin/gh-pages.js", - "gh-pages-clean": "bin/gh-pages-clean.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "dependencies": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", - "dev": true, - "dependencies": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/grunt": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz", - "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==", - "dev": true, - "dependencies": { - "dateformat": "~4.6.2", - "eventemitter2": "~0.4.13", - "exit": "~0.1.2", - "findup-sync": "~5.0.0", - "glob": "~7.1.6", - "grunt-cli": "~1.4.3", - "grunt-known-options": "~2.0.0", - "grunt-legacy-log": "~3.0.0", - "grunt-legacy-util": "~2.0.1", - "iconv-lite": "~0.6.3", - "js-yaml": "~3.14.0", - "minimatch": "~3.0.4", - "nopt": "~3.0.6" - }, - "bin": { - "grunt": "bin/grunt" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/grunt-cli": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.5.0.tgz", - "integrity": "sha512-rILKAFoU0dzlf22SUfDtq2R1fosChXXlJM5j7wI6uoW8gwmXDXzbUvirlKZSYCdXl3LXFbR+8xyS+WFo+b6vlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "grunt-known-options": "~2.0.0", - "interpret": "~1.1.0", - "liftup": "~3.0.1", - "nopt": "~5.0.0", - "v8flags": "^4.0.1" - }, - "bin": { - "grunt": "bin/grunt" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/grunt-cli/node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/grunt-contrib-copy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz", - "integrity": "sha512-gFRFUB0ZbLcjKb67Magz1yOHGBkyU6uL29hiEW1tdQ9gQt72NuMKIy/kS6dsCbV0cZ0maNCb0s6y+uT1FKU7jA==", - "dev": true, - "dependencies": { - "chalk": "^1.1.1", - "file-sync-cmp": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-sass": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-sass/-/grunt-contrib-sass-2.0.0.tgz", - "integrity": "sha512-RxZ3dlZZTX4YBPu2zMu84NPYgJ2AYAlIdEqlBaixNVyLNbgvJBGUr5Gi0ec6IiOQbt/I/z7uZVN9HsRxgznIRw==", - "dev": true, - "dependencies": { - "async": "^2.6.1", - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "dargs": "^6.0.0", - "which": "^1.3.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/grunt-contrib-sass/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/grunt-contrib-sass/node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/grunt-contrib-sass/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/grunt-contrib-sass/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/grunt-known-options": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz", - "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-legacy-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz", - "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==", - "dev": true, - "dependencies": { - "colors": "~1.1.2", - "grunt-legacy-log-utils": "~2.1.0", - "hooker": "~0.2.3", - "lodash": "~4.17.19" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/grunt-legacy-log-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz", - "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==", - "dev": true, - "dependencies": { - "chalk": "~4.1.0", - "lodash": "~4.17.19" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/grunt-legacy-log-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/grunt-legacy-log-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/grunt-legacy-log-utils/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/grunt-legacy-log-utils/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/grunt-legacy-log-utils/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/grunt-legacy-log-utils/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/grunt-legacy-util": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz", - "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==", - "dev": true, - "dependencies": { - "async": "~3.2.0", - "exit": "~0.1.2", - "getobject": "~1.0.0", - "hooker": "~0.2.3", - "lodash": "~4.17.21", - "underscore.string": "~3.3.5", - "which": "~2.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/grunt-legacy-util/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/grunt/node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/grunt/node_modules/grunt-cli": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz", - "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "grunt-known-options": "~2.0.0", - "interpret": "~1.1.0", - "liftup": "~3.0.1", - "nopt": "~4.0.1", - "v8flags": "~3.2.0" - }, - "bin": { - "grunt": "bin/grunt" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/grunt/node_modules/grunt-cli/node_modules/nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "1", - "osenv": "^0.1.4" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/grunt/node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/grunt/node_modules/v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "dependencies": { - "parse-passwd": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/hooker": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "dev": true, - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-server": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", - "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", - "dev": true, - "dependencies": { - "basic-auth": "^2.0.1", - "chalk": "^4.1.2", - "corser": "^2.0.1", - "he": "^1.2.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy": "^1.18.1", - "mime": "^1.6.0", - "minimist": "^1.2.6", - "opener": "^1.5.1", - "portfinder": "^1.0.28", - "secure-compare": "3.0.1", - "union": "~0.5.0", - "url-join": "^4.0.1" - }, - "bin": { - "http-server": "bin/http-server" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/http-server/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/http-server/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/http-server/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/http-server/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/http-server/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/http-server/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immutable": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", - "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", - "dev": true - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA==", - "dev": true - }, - "node_modules/is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "dependencies": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", - "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", - "dev": true, - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, - "dependencies": { - "is-unc-path": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "dependencies": { - "unc-path-regex": "^0.1.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/liftup": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz", - "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==", - "dev": true, - "dependencies": { - "extend": "^3.0.2", - "findup-sync": "^4.0.0", - "fined": "^1.2.0", - "flagged-respawn": "^1.0.1", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.1", - "rechoir": "^0.7.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/liftup/node_modules/findup-sync": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", - "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", - "dev": true, - "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^4.0.2", - "resolve-dir": "^1.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node_modules/nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", - "dev": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", - "dev": true, - "dependencies": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", - "dev": true, - "dependencies": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "dev": true, - "bin": { - "opener": "bin/opener-bin.js" - } - }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", - "dev": true, - "dependencies": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", - "dev": true, - "dependencies": { - "path-root-regex": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/portfinder": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", - "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", - "dev": true, - "dependencies": { - "async": "^2.6.4", - "debug": "^3.2.7", - "mkdirp": "^0.5.6" - }, - "engines": { - "node": ">= 0.12.0" - } - }, - "node_modules/portfinder/node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/prettier": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", - "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/qs": { - "version": "6.12.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", - "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/rechoir": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", - "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", - "dev": true, - "dependencies": { - "resolve": "^1.9.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", - "dev": true, - "dependencies": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/sass": { - "version": "1.77.6", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", - "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", - "dev": true, - "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/secure-compare": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", - "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", - "dev": true - }, - "node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-outer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/trim-repeated": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/underscore.string": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", - "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", - "dev": true, - "dependencies": { - "sprintf-js": "^1.1.1", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/underscore.string/node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true - }, - "node_modules/union": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", - "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", - "dev": true, - "dependencies": { - "qs": "^6.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "dev": true - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/v8flags": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", - "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dev": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - } - } -} diff --git a/package.json b/package.json index 84f86d0..c80eb55 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,39 @@ { - "name": "cropt", - "version": "1.0.0", - "description": "A lightweight JavaScript image cropper", + "name": "cropt2", + "version": "2.0.0", + "description": "A lightweight but powerful JavaScript image cropper", + "main": "dist/cropt.min.js", + "style": "dist/cropt.min.css", "files": [ - "src/cropt.*" - ], - "types": "./src/cropt.d.ts", + "dist/cropt.min.js", + "dist/cropt.min.js.map", + "dist/cropt.min.css", + "dist/cropt.min.css.map", + "LICENSE", + "README.md" + ], "type": "module", - "main": "./src/cropt.js", "devDependencies": { "bootstrap": "^5.3.3", "gh-pages": "^6.2.0", "grunt": "^1.6.1", "grunt-cli": "^1.5.0", + "grunt-contrib-clean": "^2.0.1", "grunt-contrib-copy": "^1.0.0", + "grunt-contrib-cssmin": "^5.0.0", "grunt-contrib-sass": "^2.0.0", + "grunt-postcss": "^0.9.0", + "grunt-terser": "^2.0.0", "http-server": "^14.1.1", + "postcss": "^8.5.6", + "postcss-cli": "^11.0.1", "prettier": "^3.4.1", "sass": "^1.77.6", + "terser": "^5.44.1", "typescript": "^5.7.2" }, "scripts": { + "build": "tsc && grunt", "deploy": "gh-pages -d ./demo", "prepare": "tsc && grunt --gruntfile Gruntfile.cjs", "format": "prettier . --write", @@ -28,7 +41,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/devtheorem/cropt.git" + "url": "git+https://github.com/mindflowgo/cropt.git" }, "keywords": [ "crop", @@ -36,12 +49,12 @@ "image", "cropping" ], - "author": "Theodore Brown ", + "author": "Filipe Laborde + Theodore Brown ", "license": "MIT", "bugs": { - "url": "https://github.com/devtheorem/cropt/issues" + "url": "https://github.com/mindflowgo/cropt/issues" }, - "homepage": "http://devtheorem.github.io/cropt/", + "homepage": "http://mindflowgo.github.io/cropt/", "cspell": { "words": [ "Cropt", diff --git a/src/cropt.ts b/src/cropt.ts index 59b757b..56c0829 100644 --- a/src/cropt.ts +++ b/src/cropt.ts @@ -1,3 +1,10 @@ +declare global { + interface Blob { + width?: number; + height?: number; + } +} + function debounce(func: T, wait: number) { let timer: number | undefined; return (...args: any) => { @@ -6,13 +13,6 @@ function debounce(func: T, wait: number) { }; } -function setZoomerVal(value: number, zoomer: HTMLInputElement) { - const zMin = parseFloat(zoomer.min); - const zMax = parseFloat(zoomer.max); - - zoomer.value = Math.max(zMin, Math.min(zMax, value)).toFixed(3); -} - function loadImage(src: string): Promise { return new Promise((resolve, reject) => { const img = new Image(); @@ -41,15 +41,13 @@ function getInitialElements() { }; } -function getArrowKeyDeltas(key: string): [number, number] { - if (key === "ArrowLeft") { - return [2, 0]; - } else if (key === "ArrowUp") { - return [0, 2]; - } else if (key === "ArrowRight") { - return [-2, 0]; - } else { - return [0, -2]; +function getArrowKeyDeltas(key: string): [number, number] | null { + switch (key) { + case "ArrowLeft": return [2, 0]; + case "ArrowUp": return [0, 2]; + case "ArrowRight": return [-2, 0]; + case "ArrowDown": return [0, -2]; + default: return null; } } @@ -78,12 +76,15 @@ export interface CroptOptions { enableKeypress?: boolean; // listen to arrow keys resizeBars?: boolean; // allow on-picture resize bars enableRotateBtns?: boolean; // passing in rotation will work regardless, but no btns will be visible + transparencyColor?: string; // what color to show behind transparency colors (or if converting to jpeg) } interface CropPoints { left: number; top: number; right: number; bottom: number; + width: number; + height: number; } export class Cropt { @@ -114,10 +115,14 @@ export class Cropt { resizeBars: false, enableRotateBtns: false, }; + // active settings + #maxZoom = 0.85; // up to 1 possible + #viewportMinWidth = 50; // for adjustable, min viewport width + #viewportMinHeight = 50; - #boundZoom: number | undefined = undefined; #scale = 1; #rotation = 0; + #clearRotationBlob = false; // when we create intermittent blobs (ex. rotate), must clear // when displaying in UI or experting to jpeg, use this color for transparency sections #transparencyColor = '#fff'; #abortController = new AbortController(); @@ -136,6 +141,7 @@ export class Cropt { // changed: removed structuredClone: slow, and would fail passing functions in options this.options = { ...this.options, ...options } as CroptOptions; + if( this.options.transparencyColor ) this.#transparencyColor = this.options.transparencyColor; this.element = element; this.element.classList.add("cropt-container"); @@ -170,10 +176,12 @@ export class Cropt { this.elements.toolBar.appendChild(this.elements.rotateRight); } - if (this.options.enableZoomSlider) { - this.elements.zoomer.type = "range"; - this.elements.zoomer.step = "0.001"; - this.elements.zoomer.value = "1"; + this.elements.zoomer.type = "range"; + this.elements.zoomer.step = "0.001"; + this.elements.zoomer.value = "1"; + // zooming can happen with pinch, however when slider enabled, it is visible, otherwise unseen + if (this.options.enableZoomSlider) { + this.elements.zoomer.className = this.options.zoomerInputClass; this.elements.zoomer.setAttribute("aria-label", "zoom"); this.elements.toolBar.appendChild(this.elements.zoomer); } @@ -211,10 +219,6 @@ export class Cropt { throw new Error("src cannot be empty"); } - if (typeof preset !== "object") { - this.#boundZoom = preset; - } - return loadImage(src).then(async (img) => { this.#replaceImage(img); // force-replace image node (prevents caching, etc) @@ -227,16 +231,17 @@ export class Cropt { if (preset.transform.rotate) { await this.setRotation(preset.transform.rotate); } + const zoom = preset.transform.scale; + this.#updateZoomLimits(zoom); - this.#updateZoomLimits(true); // skipping center - - // Then apply scale and position - this.setZoom(preset.transform.scale); + // Finally, override to custom preview positioning this.#transformPreview(preset.transform); this.#updateOverlay(); }, 0); } else { - this.#initPropertiesFromImage(); + // passed-in number, so simply zoom level + const zoom = preset; + this.#initPropertiesFromImage(zoom); } }); } @@ -258,18 +263,49 @@ export class Cropt { top: getPoint(top), right: getPoint(left + oWidth + widthDiff), bottom: getPoint(top + oHeight + heightDiff), + width: getPoint(oWidth + widthDiff), + height: getPoint(oHeight + heightDiff), }; } /** * Returns: - * crop { left, top, right, bottom }: the crop rectangle for image cropping outside Cropt + * crop { x, y, width, height }: the crop rectangle for image cropping outside Cropt * transform: adjustments to re-create placement of image in viewport (ex. continue editing) * viewport: the active viewport size + borderRadius used (in case it's system adjusted) */ get() { + const p = this.#getPoints(); + let crop = { + x: p.left, + y: p.top, + width: p.width, + height: p.height, + }; + + // adjust (x,y) depending on rotation to match orientation AFTER rotation + const origW = this.elements.preview.naturalWidth; + const origH = this.elements.preview.naturalHeight; + if (this.#rotation === 90 || this.#rotation === 270) { + // 90 degrees so switch W x H -> H x W, then adjust coords + crop.width = p.height; + crop.height = p.width; + if (this.#rotation === 90) { + crop.x = p.top; + crop.y = origW - p.left - p.width; + } else { + crop.x = origH - p.top - p.height; + crop.y = p.left; + } + } else if( this.#rotation === 180 ) { + crop.x = origW - p.left - p.width; + crop.y = origH - p.top - p.height; + } + crop.x = Math.max(0,crop.x); // sometimes calcs -1 or -3, clamp it to 0. + crop.y = Math.max(0,crop.y); + return { - crop: this.#getPoints(), + crop, transform: this.#transformPreview(), viewport: { width: Math.round(this.options.viewport.width), @@ -281,28 +317,32 @@ export class Cropt { /** * Returns a Promise resolving to an HTMLCanvasElement object for the cropped image. - * If size is specified, the image will be scaled with its longest side set to size. - * Otherwise (size = null), full-size cropped area is returned + * If size is POSTIVE: the image will be SCALED with its longest side set to size. + * If size is NEGATIVE: it will only SHRINK it if it exceeds size (never enlarge it) + * Otherwise (size = null), actual-size cropped area is returned */ toCanvas(size: number | null = null, type: string = '') { - const vpRect = this.elements.viewport.getBoundingClientRect(); - const ratio = vpRect.width / vpRect.height; const points = this.#getPoints(); - let width = points.right - points.left; - let height = points.bottom - points.top; + const shrinkOnly = size && size < 0; + if( size && shrinkOnly ) size = -size; // make positive + let finalWidth = points.width; + let finalHeight = points.height; + + // resize only if size passed in (if negative only shrink if sides exceed final) + if (size && (!shrinkOnly || finalWidth > size || finalHeight > size)) { + const vpRect = this.elements.viewport.getBoundingClientRect(); + const ratio = vpRect.width / vpRect.height; - // resize only if size passed in - if (size !== null) { if (ratio > 1) { - width = size; - height = size / ratio; + finalWidth = size; + finalHeight = size / ratio; } else { - height = size; - width = size * ratio; + finalHeight = size; + finalWidth = size * ratio; } } - return Promise.resolve(this.#getCanvas(points, width, height, type)); + return Promise.resolve(this.#getCanvas(points, finalWidth, finalHeight, type)); } toBlob(size: number | null = null, type = "image/webp", quality = 1): Promise { @@ -317,6 +357,9 @@ export class Cropt { if (blob === null) { reject("Canvas blob is null"); } else { + // add in this meta-data to blob + blob.width = canvas.width; + blob.height = canvas.height; resolve(blob); } }, @@ -334,27 +377,27 @@ export class Cropt { setOptions(options: RecursivePartial) { const curWidth = this.options.viewport.width; const curHeight = this.options.viewport.height; + const viewport = this.options.viewport; if (options.viewport) { - options.viewport = { ...this.options.viewport, ...options.viewport }; + options.viewport = { ...viewport, ...options.viewport }; } - // changed: removed structuredClone: slow, and would fail passing functions in options this.options = { ...this.options, ...options } as CroptOptions; this.#setViewportCss(); - if ( - this.options.viewport.width !== curWidth || - this.options.viewport.height !== curHeight - ) { - this.#updateZoomLimits(); - } + // if viewport dimensions unchanged, don't change zoom + if (viewport.width === curWidth && viewport.height === curHeight) return; + + this.#updateZoomLimits(); } setZoom(value: number) { - setZoomerVal(value, this.elements.zoomer); - const event = new Event("input"); - this.elements.zoomer.dispatchEvent(event); // triggers this.#onZoom call + const zoomer = this.elements.zoomer; + const zMin = parseFloat(zoomer.min); + const zMax = parseFloat(zoomer.max); + zoomer.value = Math.max(zMin, Math.min(zMax, value)).toFixed(3); + this.#onZoom(); } async setRotation(degrees: number) { @@ -397,24 +440,23 @@ export class Cropt { const blob = await new Promise((resolve, reject) => { canvas.toBlob( (b) => (b ? resolve(b) : reject(new Error("Failed to create blob"))), - "image/png", + // "image/png", + "image/webp", 1 ); }); - // Revoke old URL and set new one - if (img.src.startsWith("blob:")) URL.revokeObjectURL(img.src); - + if (this.#clearRotationBlob) URL.revokeObjectURL(img.src); img.src = URL.createObjectURL(blob); await img.decode(); // Wait for decode without onload event + // set a flag so we delete this blob if changing src or destroy + this.#clearRotationBlob = true; } destroy() { this.#abortController.abort(); - // Clean up blob URL if it exists - if (this.elements.preview.src.startsWith("blob:")) { - URL.revokeObjectURL(this.elements.preview.src); - } + // Clean up blob URL if we created it internally (ex. for rotation) + if (this.#clearRotationBlob) URL.revokeObjectURL(this.elements.preview.src); this.element.removeChild(this.elements.boundary); this.element.classList.remove("cropt-container"); @@ -423,7 +465,7 @@ export class Cropt { } // adjust preview tranform styles (& transformOrigin) - // NOTE: rotate is handled by physically rotating the image, not CSS transform + // NOTE: rotate is handled by physically rotating the image (saves many recalculations) #transformPreview(data?: { x?: number; y?: number; @@ -482,7 +524,6 @@ export class Cropt { } #setViewportCss() { - this.elements.zoomer.className = this.options.zoomerInputClass; const viewport = this.elements.viewport; viewport.style.borderRadius = this.options.viewport.borderRadius; viewport.style.width = this.options.viewport.width + "px"; @@ -546,8 +587,6 @@ export class Cropt { } #initControlHandlers() { - const MIN_SIZE = 50; - // Right handle - adjusts width let rightStartX = 0; let rightStartWidth = 0; @@ -556,7 +595,7 @@ export class Cropt { ev.preventDefault(); const deltaX = ev.pageX - rightStartX; const maxWidth = Math.floor(this.elements.boundary.clientWidth * 0.95); - const newWidth = Math.min(maxWidth, Math.max(MIN_SIZE, rightStartWidth + deltaX)); + const newWidth = Math.min(maxWidth, Math.max(this.#viewportMinWidth, rightStartWidth + deltaX)); this.options.viewport.width = newWidth; this.#setViewportCss(); @@ -595,7 +634,7 @@ export class Cropt { ev.preventDefault(); const deltaY = ev.pageY - bottomStartY; const maxHeight = Math.floor(this.elements.boundary.clientHeight * 0.95); - const newHeight = Math.min(maxHeight, Math.max(MIN_SIZE, bottomStartHeight + deltaY)); + const newHeight = Math.min(maxHeight, Math.max(this.#viewportMinHeight, bottomStartHeight + deltaY)); this.options.viewport.height = newHeight; this.#setViewportCss(); @@ -628,8 +667,6 @@ export class Cropt { } #getUnscaledCanvas(p: CropPoints) { - const sWidth = p.right - p.left; - const sHeight = p.bottom - p.top; const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); @@ -637,15 +674,15 @@ export class Cropt { throw new Error("Canvas context cannot be null"); } - canvas.width = sWidth; - canvas.height = sHeight; + canvas.width = p.width; + canvas.height = p.height; const el = this.elements.preview; - ctx.drawImage(el, p.left, p.top, sWidth, sHeight, 0, 0, canvas.width, canvas.height); + ctx.drawImage(el, p.left, p.top, p.width, p.height, 0, 0, canvas.width, canvas.height); return canvas; } - #getCanvas(points: CropPoints, width: number, height: number, type: string) { + #getCanvas(points: CropPoints, finalWidth: number, finalHeight: number, type: string) { // cannot draw from a canvas into itself while resizing — it causes visual corruption // ping-pong oc -> buffer -> oc .... console.time('getCanvas') @@ -655,8 +692,8 @@ export class Cropt { const bctx = buffer.getContext("2d"); const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); - canvas.width = width; - canvas.height = height; + canvas.width = finalWidth; + canvas.height = finalHeight; if (ctx === null || octx === null || bctx === null) { throw new Error("Canvas context cannot be null"); @@ -668,24 +705,22 @@ export class Cropt { }; while (to.width > canvas.width * 2) { - // step down size by one half for smooth scaling let w = to.width; let h = to.height; - to = { - width: Math.floor(to.width / 2), - height: Math.floor(to.height / 2), - }; - - // write oc to buffer + // buffer: copy oc (oc -> buffer) buffer.width = w; buffer.height = h; bctx.clearRect(0, 0, buffer.width, buffer.height); bctx.drawImage(oc, 0, 0); - // clear oc + to = { + width: Math.floor(w / 2), + height: Math.floor(h / 2), + }; + // clear oc octx.clearRect(0, 0, w, h); - + // oc: copy 1/2-size buffer (buffer -> oc/2) -- half-size for smooth scaling octx.drawImage(buffer, 0, 0, w, h, 0, 0, to.width, to.height); } @@ -831,39 +866,37 @@ export class Cropt { signal: this.#abortController.signal, }); - if (this.options.enableKeypress) { - let keyDown = (ev: KeyboardEvent) => { - // for user-input fields we skip - if ( - document.activeElement && - ["INPUT", "TEXTAREA", "SELECT", "BUTTON"].includes( - document.activeElement.nodeName, - ) - ) { - return; - } + if (!this.options.enableKeypress) return; - if (ev.shiftKey && (ev.key === "ArrowUp" || ev.key === "ArrowDown")) { - ev.preventDefault(); - let zoomVal = parseFloat(this.elements.zoomer.value); - let stepVal = ev.key === "ArrowUp" ? 0.01 : -0.01; - this.setZoom(zoomVal + stepVal); - } else if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(ev.key)) { - ev.preventDefault(); - let [deltaX, deltaY] = getArrowKeyDeltas(ev.key); - this.#assignTransformCoordinates(deltaX, deltaY); - } - }; + const keyDown = (ev: KeyboardEvent) => { + // for user-input fields we skip + if ( document.activeElement && + ["INPUT", "TEXTAREA", "SELECT", "BUTTON"] + .includes(document.activeElement.nodeName)) return; - document.addEventListener("keydown", keyDown, { signal: this.#abortController.signal }); - } + const deltaXY = getArrowKeyDeltas(ev.key); + if (deltaXY === null) return; // only care about arrow keys + + if (ev.shiftKey && deltaXY[1]) { + ev.preventDefault(); + const zoomVal = parseFloat(this.elements.zoomer.value); + this.setZoom(zoomVal + deltaXY[1]*0.005); // +/-2 *.005 = +/-0.01 + } else { + ev.preventDefault(); + this.#assignTransformCoordinates(deltaXY[0], deltaXY[1]); + } + }; + + document.addEventListener("keydown", keyDown, { signal: this.#abortController.signal }); } #initializeZoom() { - this.elements.zoomer.addEventListener("input", ()=>this.#onZoom(), { - signal: this.#abortController.signal, - }); - + if (this.options.enableZoomSlider) { + this.elements.zoomer.addEventListener("input", ()=>this.#onZoom(), { + signal: this.#abortController.signal, + }); + } + if (this.options.mouseWheelZoom === "off") return; const scroll = (ev: WheelEvent) => { @@ -889,7 +922,6 @@ export class Cropt { const transform = this.#transformPreview(); this.#scale = parseFloat(this.elements.zoomer.value); transform.scale = this.#scale; - // this.#transformPreview(transform) const boundaries = this.#getVirtualBoundaries(); const transBoundaries = boundaries.translate; @@ -971,15 +1003,13 @@ export class Cropt { overlay.style.left = `${imgData.left - boundRect.left}px`; } - #initPropertiesFromImage() { - if (!this.#isVisible()) { - return; - } + #initPropertiesFromImage(zoom: number | null = null) { + if (!this.#isVisible()) return; // resets values to calculate zoom limits const transformReset = { x: 0, y: 0, scale: 1, origin: { x: 0, y: 0 } }; this.#transformPreview(transformReset); - this.#updateZoomLimits(); + this.#updateZoomLimits(zoom); transformReset.scale = this.#scale; this.#transformPreview(transformReset); @@ -1010,7 +1040,7 @@ export class Cropt { this.#transformPreview({ ...transform, origin: center }); } - #updateZoomLimits(skipCenter = false) { + #updateZoomLimits(zoom: number | null = null) { const img = this.elements.preview; const vpData = this.elements.viewport.getBoundingClientRect(); const minZoom = Math.max( @@ -1018,18 +1048,15 @@ export class Cropt { vpData.height / img.naturalHeight, ); - let maxZoom = 0.85; + let maxZoom = this.#maxZoom; if (minZoom >= maxZoom) { maxZoom += minZoom; } this.elements.zoomer.min = minZoom.toFixed(3); this.elements.zoomer.max = maxZoom.toFixed(3); - if (skipCenter) return; - - let zoom = this.#boundZoom; - if (zoom === undefined) { + if (zoom === null) { const bData = this.elements.boundary.getBoundingClientRect(); zoom = Math.max(bData.width / img.naturalWidth, bData.height / img.naturalHeight); } diff --git a/src/demo.ts b/src/demo.ts index 0df9709..4f878de 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -11,7 +11,7 @@ function popupResult(src: string, borderRadius: string) { if (bodyEl === null) { throw new Error("bodyEl is null"); } - + bodyEl.innerHTML = ``; resultModal.show(); } @@ -25,6 +25,7 @@ let photos = [ "woman-dog.jpg", ]; +// Get Result output size const outputSize = 500; interface DemoConfig { @@ -104,8 +105,7 @@ const demoConfigs: Record = { hideControls: true, notes: "Note the grab bars on the viewport, you can manually adjust the sizing of viewport." + - "

Note for rotation, if you are using the crop coordinates, you must rotate " + - "the image FIRST, then the crop coordinates apply.", + "

If picture is rotated, crop coordinates correspond to rotated canvas.", getRandomImage: () => "photos/" + photos[Math.floor(Math.random() * photos.length)], }, }; @@ -134,7 +134,7 @@ const cropt = new Cropt(cropEl, ${optionStr}); ${bindPreset} cropt.bind("${imgSrc}"${bindPreset ? ", preset" : ""}); -resultBtn.addEventListener("click", () => {${ +resultBtn.addEventListener("click", async () => {${ bindPreset ? ` // Read the crop & viewport details this way... @@ -143,10 +143,10 @@ resultBtn.addEventListener("click", () => {${ console.log( JSON.stringify(cropAndViewportInfo) );\n` : "" } - cropt.toCanvas(${outputSize}).then((canvas) => { - let url = canvas.toDataURL(); - // Display in modal dialog. - }); + const canvas = await cropt.toCanvas(${outputSize}) + let url = canvas.toDataURL(); // or canvas.toBlob(); + // Now can display in image: img.src = url + // Display in modal dialog. });`; } @@ -275,14 +275,17 @@ function demoMain() { // Setup result button const resultBtn = getElById("result-btn"); - resultBtn.onclick = () => { + resultBtn.onclick = async () => { if (!cropt) return; const cropAndViewportInfo = cropt.get(); console.log(`Image parameters [cropt.get()]:`, JSON.stringify(cropAndViewportInfo)); - cropt.toCanvas(outputSize).then((canvas: HTMLCanvasElement) => { - if (cropt) popupResult(canvas.toDataURL(), cropt.options.viewport.borderRadius); - }); + // const canvas = await cropt.toCanvas(outputSize) + // if (cropt && canvas) popupResult(canvas.toDataURL(), cropt.options.viewport.borderRadius); + // or using blobs better if big images + const cropBlob = await cropt.toBlob(outputSize); + console.log(`- returned Blob:`, cropBlob); + if( cropt && cropBlob ) popupResult(URL.createObjectURL(cropBlob), cropt.options.viewport.borderRadius); }; // Setup tab switching diff --git a/tsconfig.json b/tsconfig.json index a0d8a02..51083b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "outDir": "dist", "module": "ES2022", "target": "ES2022", "noUnusedLocals": true, From bd9755db06f9a0e563b414b4ba0e829a8e2e5fb1 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Wed, 31 Dec 2025 00:15:43 -0500 Subject: [PATCH 26/53] Fixes to make proper cross-platform package. CDN-compatible now. Minified. --- Gruntfile.cjs | 79 -------------- README.md | 28 +++-- demo/basic.html | 52 ++++++++++ src/demo.ts => demo/demo.js | 201 +++++++++++++++++------------------- demo/index.html | 49 ++++++--- package.json | 121 +++++++++++----------- package.mjs | 13 +++ src/cropt.ts | 7 +- tsconfig.json | 2 +- 9 files changed, 275 insertions(+), 277 deletions(-) delete mode 100644 Gruntfile.cjs create mode 100644 demo/basic.html rename src/demo.ts => demo/demo.js (65%) create mode 100644 package.mjs diff --git a/Gruntfile.cjs b/Gruntfile.cjs deleted file mode 100644 index 84ccc3d..0000000 --- a/Gruntfile.cjs +++ /dev/null @@ -1,79 +0,0 @@ -module.exports = function (grunt) { - grunt.initConfig({ - - clean: { - demo: ["demo/build"] - }, - - terser: { - dist: { - options: { - compress: true, - mangle: true, - sourceMap: true - }, - files: [{ - expand: true, - cwd: "dist", - src: ["*.js", "!*.min.js"], - dest: "dist", - ext: ".min.js" - }] - } - }, - - cssmin: { - dist: { - options: { - sourceMap: true - }, - files: { - "dist/cropt.min.css": "src/cropt.css" - } - } - }, - - sass: { - demo: { - options: { style: "expanded" }, - files: { - "demo/build/bs-custom.css": "demo/styles.scss" - } - } - }, - - copy: { - demo: { - files: [ - { - expand: true, - cwd: "node_modules/bootstrap/dist/js", - src: "bootstrap.bundle.min.*", - dest: "demo/build" - }, - { - expand: true, - cwd: "dist", - src: ["*.min.js", "*.min.css", "*.d.ts"], - dest: "demo/build" - } - ] - } - } - - }); - - grunt.loadNpmTasks("grunt-contrib-clean"); - grunt.loadNpmTasks("grunt-terser"); - grunt.loadNpmTasks("grunt-contrib-cssmin"); - grunt.loadNpmTasks("grunt-contrib-sass"); - grunt.loadNpmTasks("grunt-contrib-copy"); - - grunt.registerTask("default", [ - "clean", - "terser", - "cssmin", - "sass", - "copy" - ]); -}; diff --git a/README.md b/README.md index 66a4af7..50f08cd 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ -# Cropt - lightweight JavaScript image cropper +# Cropt v2 - lightweight JavaScript image cropper +[Github](https://github.com/mindflowgo/cropt/) + +Originally based on [Foliotek/Croppie](https://github.com/Foliotek/Croppie), but rewritten as a modern ES module with a simpler API, higher quality image scaling, and numerous other improvements by +[Devtheorem](https://devtheorem.github.io/cropt/). + +It was extensively enhanced (but backwards compatible with v1) to include adjustable viewport, rotation, +keyboard handling, and various optimizations and bug fixes. And packed to work as browser install, commonJS, +esm package, etc. -Originally based on [Foliotek/Croppie](https://github.com/Foliotek/Croppie), but rewritten as a modern ES module with a simpler API, higher quality image scaling, and numerous other improvements. ## Installation @@ -11,7 +18,7 @@ npm install cropt2 ## Running Demo ``` -npm run prepare +npm run build npm start ``` @@ -130,17 +137,7 @@ Set a rotation factor (0, 90, 180, 270) to the image. ## Visibility and binding -Cropt is dependent on its container being visible when the bind method is called. This can be an issue when your component is inside a modal that isn't shown. Consider the Bootstrap modal, for example: - -```javascript -const cropEl = document.getElementById('my-cropt'); -const c = new Cropt(cropEl, opts); -const myModal = document.getElementById('my-modal'); - -myModal.addEventListener('shown.bs.modal', () => { - c.bind("my/image.jpg"); -}); -``` +Cropt is dependent on its container being **visible** when the bind method is called. This can be an issue when your component is inside a modal or block that isn't shown (ex. style = display:none). If you have issues getting the correct result, and your Cropt instance is shown inside a modal, try taking it out of the modal and see if the issue persists. If not, make sure that your bind method is called after the modal finishes opening. @@ -154,11 +151,10 @@ Cropt is tested in the following browsers: * Safari * Chrome * Edge +* Mobile Safari Cropt should also work in any other modern browser using an engine based on Gecko, WebKit, or Chromium. ## License MIT - - diff --git a/demo/basic.html b/demo/basic.html new file mode 100644 index 0000000..5f79889 --- /dev/null +++ b/demo/basic.html @@ -0,0 +1,52 @@ + + + + + + + +

Basic Demo

+

This demo attempts to show the bare minimum needed... basically just the npm packages in header + and some javascript to wire-up the image. +

+ + + + + +
+
+
+ + + +
+ + + + + + \ No newline at end of file diff --git a/src/demo.ts b/demo/demo.js similarity index 65% rename from src/demo.ts rename to demo/demo.js index 4f878de..75ff6dd 100644 --- a/src/demo.ts +++ b/demo/demo.js @@ -1,19 +1,52 @@ -import { Cropt } from "./cropt.js"; +// install locally in a project, ex: npm i cropt2 +// import Cropt from "cropt2"; + +// access via CDN like this (for modules note 'esm') +import Cropt from 'https://unpkg.com/cropt2@latest/dist/cropt.esm.min.js'; + +/* bootstrap tabs + modal -------------------------------------- */ +document.addEventListener('click', e => { + const btn = e.target.closest('[data-bs-toggle="tab"]'); + if (!btn) return; + e.preventDefault(); + + const nav = btn.closest('.nav'); + const target = document.querySelector(btn.dataset.bsTarget); + + nav.querySelectorAll('.nav-link').forEach(b => b.classList.remove('active')); + document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('show','active')); + + btn.classList.add('active'); + target.classList.add('show','active'); + + // initialize this demo when tab changed! + demoSwitch( target.id ) +}); + +document.getElementById('resultModalClose').addEventListener('click', + ()=>closeModal(getElById("resultModal")) ) + +function openModal(modal) { + modal.classList.add('show'); + modal.style.display = 'block'; + document.body.classList.add('modal-open'); +} + +function closeModal(modal) { + modal.classList.remove('show'); + modal.style.display = 'none'; + document.body.classList.remove('modal-open'); +} -declare var hljs: any; -declare var bootstrap: any; +/* DEMO Code --------------------------------------------------- */ -function popupResult(src: string, borderRadius: string) { - const resultModal = new bootstrap.Modal(getElById("resultModal")); +function popupResult(src, borderRadius) { const imgStyle = `max-width: min(100%, 320px); max-height: 320px; border-radius: ${borderRadius};`; const bodyEl = document.querySelector("#resultModal .modal-body"); - - if (bodyEl === null) { - throw new Error("bodyEl is null"); - } - + if (bodyEl === null) throw new Error("Broken modal, can't display!"); bodyEl.innerHTML = ``; - resultModal.show(); + // now lets show it! + openModal(getElById("resultModal")); } let photos = [ @@ -28,33 +61,7 @@ let photos = [ // Get Result output size const outputSize = 500; -interface DemoConfig { - id: string; - options: { - viewport: { width: number; height: number; borderRadius: string }; - mouseWheelZoom?: "off" | "on" | "ctrl"; - zoomerInputClass?: string; - enableZoomSlider?: boolean; - enableKeypress?: boolean; - resizeBars?: boolean; - enableRotateBtns?: boolean; - }; - preset: null | { - transform: { - x: number; - y: number; - scale: number; - rotate: number; - origin: { x: number; y: number }; - }; - viewport: { width: number; height: number; borderRadius: string }; - }; - hideControls: boolean; - notes: string; - getRandomImage: () => string; -} - -const demoConfigs: Record = { +const demoConfigs = { demo1: { id: "crop-demo", options: { @@ -89,8 +96,7 @@ const demoConfigs: Record = { }, }, hideControls: true, - notes: - "Passing a previously captured:
    cropt.get()
restores viewport " + + notes: "Passing a previously captured:
    cropt.get()
restores viewport " + "and image position. See the
    cropt.bind()
section. Look in the console.log for output from cropt.get().", getRandomImage: () => "photos/kitten.jpg", }, @@ -103,17 +109,16 @@ const demoConfigs: Record = { }, preset: null, hideControls: true, - notes: - "Note the grab bars on the viewport, you can manually adjust the sizing of viewport." + + notes: "Note the grab bars on the viewport, you can manually adjust the sizing of viewport." + "

If picture is rotated, crop coordinates correspond to rotated canvas.", getRandomImage: () => "photos/" + photos[Math.floor(Math.random() * photos.length)], }, }; let activeDemo = "demo1"; -let cropt: Cropt | null = null; +let cropt = null; let config = demoConfigs[activeDemo]; -let cropEl: HTMLElement | null = null; +let cropEl = null; let imgSrc = ""; function getCode() { @@ -122,10 +127,10 @@ function getCode() { if (config.preset) { bindPreset = `\n// Using a preset here (ignoring initial viewport setup):\nconst preset = ${JSON.stringify(config.preset)}` + - `\n// Pass to bind():`; + `\n// Pass to bind():`; } - - return `import { Cropt } from "cropt"; // npm install cropt + return `// import Cropt from "cropt2"; // npm install cropt2 +import Cropt from 'https://unpkg.com/cropt2@latest/dist/cropt.min.js'; //direct const cropEl = document.getElementById("${config.id}"); const resultBtn = document.getElementById("result-btn"); @@ -134,15 +139,13 @@ const cropt = new Cropt(cropEl, ${optionStr}); ${bindPreset} cropt.bind("${imgSrc}"${bindPreset ? ", preset" : ""}); -resultBtn.addEventListener("click", async () => {${ - bindPreset - ? ` +resultBtn.addEventListener("click", async () => {${bindPreset + ? ` // Read the crop & viewport details this way... const cropAndViewportInfo = cropt.get(); console.log(\`Image parameters [cropt.get()]:\`) console.log( JSON.stringify(cropAndViewportInfo) );\n` - : "" - } + : ""} const canvas = await cropt.toCanvas(${outputSize}) let url = canvas.toDataURL(); // or canvas.toBlob(); // Now can display in image: img.src = url @@ -150,9 +153,10 @@ resultBtn.addEventListener("click", async () => {${ });`; } -function getElById(elementId: string) { +function getElById(elementId) { const el = document.getElementById(elementId); - if (el === null) throw new Error(`${elementId} is null`); + if (el === null) + throw new Error(`${elementId} is null`); return el; } @@ -165,24 +169,21 @@ function setupControls() { const config = demoConfigs[activeDemo]; const controlsContainer = document.getElementById("controls"); const notesContainer = document.getElementById("notes"); - if (controlsContainer) controlsContainer.classList.toggle("d-none", config.hideControls); + if (controlsContainer) + controlsContainer.classList.toggle("d-none", config.hideControls); if (notesContainer) { notesContainer.classList.toggle("d-none", !config.hideControls); notesContainer.innerHTML = config.notes; } - if (!config.hideControls) { - const borderRadiusRange = getElById("borderRadiusRange") as HTMLInputElement; + const borderRadiusRange = getElById("borderRadiusRange"); borderRadiusRange.value = parseInt(config.options.viewport.borderRadius).toString(); - - const widthRange = getElById("widthRange") as HTMLInputElement; + const widthRange = getElById("widthRange"); widthRange.value = config.options.viewport.width.toString(); - - const heightRange = getElById("heightRange") as HTMLInputElement; + const heightRange = getElById("heightRange"); heightRange.value = config.options.viewport.height.toString(); - if (config.options.mouseWheelZoom) { - const mouseWheelSelect = getElById("mouseWheelSelect") as HTMLSelectElement; + const mouseWheelSelect = getElById("mouseWheelSelect"); mouseWheelSelect.value = config.options.mouseWheelZoom; } } @@ -190,36 +191,33 @@ function setupControls() { function bindControlEvents() { const config = demoConfigs[activeDemo]; - if (config.hideControls) return; - - const borderRadiusRange = getElById("borderRadiusRange") as HTMLInputElement; + if (config.hideControls) + return; + const borderRadiusRange = getElById("borderRadiusRange"); borderRadiusRange.oninput = () => { const activeConfig = demoConfigs[activeDemo]; activeConfig.options.viewport.borderRadius = borderRadiusRange.value + "%"; setCode(); cropt?.setOptions(activeConfig.options); }; - - const widthRange = getElById("widthRange") as HTMLInputElement; + const widthRange = getElById("widthRange"); widthRange.oninput = () => { const activeConfig = demoConfigs[activeDemo]; activeConfig.options.viewport.width = Math.round(+widthRange.value); setCode(); cropt?.setOptions(activeConfig.options); }; - - const heightRange = getElById("heightRange") as HTMLInputElement; + const heightRange = getElById("heightRange"); heightRange.oninput = () => { const activeConfig = demoConfigs[activeDemo]; activeConfig.options.viewport.height = Math.round(+heightRange.value); setCode(); cropt?.setOptions(activeConfig.options); }; - - const mouseWheelSelect = getElById("mouseWheelSelect") as HTMLSelectElement; + const mouseWheelSelect = getElById("mouseWheelSelect"); mouseWheelSelect.onchange = () => { const activeConfig = demoConfigs[activeDemo]; - const value = mouseWheelSelect.value as "on" | "off" | "ctrl"; + const value = mouseWheelSelect.value; activeConfig.options.mouseWheelZoom = value; setCode(); cropt?.setOptions(activeConfig.options); @@ -227,15 +225,15 @@ function bindControlEvents() { } function bindFileUpload() { - const fileInput = getElById("imgFile") as HTMLInputElement; + const fileInput = getElById("imgFile"); fileInput.value = ""; - fileInput.onchange = () => { if (fileInput.files?.[0]) { const file = fileInput.files[0]; if (cropt) { - if (imgSrc.startsWith('blob')) URL.revokeObjectURL(imgSrc) - imgSrc = URL.createObjectURL(file) + if (imgSrc.startsWith('blob')) + URL.revokeObjectURL(imgSrc); + imgSrc = URL.createObjectURL(file); cropt.bind(imgSrc); setCode(); } @@ -243,28 +241,40 @@ function bindFileUpload() { }; } -function initializeDemo(demoKey: string) { +function initializeDemo(demoKey) { config = demoConfigs[demoKey]; cropEl = getElById(config.id); imgSrc = config.getRandomImage(); - // Destroy existing instance if present if (cropt) { cropt.destroy(); console.log(`Destroyed prior cropt instance;`); } - // Create new instance cropt = new Cropt(cropEl, config.options); console.log(`Initialized new Cropt() instance (${demoKey}).`); - if (config.preset) { cropt.bind(imgSrc, config.preset); - } else { + } + else { cropt.bind(imgSrc); } } +function demoSwitch( demoKey ){ + activeDemo = demoKey; + initializeDemo(activeDemo); + setupControls(); + setCode(); + document.getElementById('html-el').classList.add('d-none') + // only demo 1 binds these thigs + if (activeDemo === "demo1") { + bindControlEvents(); + bindFileUpload(); + document.getElementById('html-el').classList.remove('d-none') + } +} + function demoMain() { // Initial setup initializeDemo("demo1"); @@ -272,39 +282,20 @@ function demoMain() { setCode(); bindControlEvents(); bindFileUpload(); - // Setup result button const resultBtn = getElById("result-btn"); resultBtn.onclick = async () => { - if (!cropt) return; + if (!cropt) + return; const cropAndViewportInfo = cropt.get(); console.log(`Image parameters [cropt.get()]:`, JSON.stringify(cropAndViewportInfo)); - // const canvas = await cropt.toCanvas(outputSize) // if (cropt && canvas) popupResult(canvas.toDataURL(), cropt.options.viewport.borderRadius); // or using blobs better if big images const cropBlob = await cropt.toBlob(outputSize); console.log(`- returned Blob:`, cropBlob); - if( cropt && cropBlob ) popupResult(URL.createObjectURL(cropBlob), cropt.options.viewport.borderRadius); + if (cropt && cropBlob) + popupResult(URL.createObjectURL(cropBlob), cropt.options.viewport.borderRadius); }; - - // Setup tab switching - document.querySelectorAll('[data-bs-toggle="tab"]').forEach((tab) => { - tab.addEventListener("shown.bs.tab", (event) => { - const target = (event.target as HTMLElement).dataset.bsTarget?.substring(1); - - if (target && activeDemo !== target) { - activeDemo = target; - initializeDemo(target); - setupControls(); - setCode(); - if (activeDemo === "demo1") { - bindControlEvents(); - bindFileUpload(); - } - } - }); - }); } - demoMain(); diff --git a/demo/index.html b/demo/index.html index eb0b705..7a6b04c 100644 --- a/demo/index.html +++ b/demo/index.html @@ -1,9 +1,10 @@ - + - Cropt - lightweight image cropper + Cropt v2 - lightweight image cropper + @@ -11,9 +12,7 @@ - - - + + + \ No newline at end of file From 9c76b5030df3678ae44092e73f5c66da3e83e354 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Thu, 1 Jan 2026 21:23:20 -0500 Subject: [PATCH 37/53] Added react & svelte component wrappers --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c8eeacb..dc98743 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cropt2", - "version": "2.0.6", + "version": "2.0.7", "description": "A lightweight but powerful JavaScript image cropper", "keywords": [ "cropping", From 3ba5b55f04fd384bb09b6a5265b3d657b13da7e2 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Thu, 1 Jan 2026 21:27:11 -0500 Subject: [PATCH 38/53] Cropt svelte component updates --- src/svelte/Cropt.svelte | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/svelte/Cropt.svelte b/src/svelte/Cropt.svelte index de3a19b..5ce3a3a 100644 --- a/src/svelte/Cropt.svelte +++ b/src/svelte/Cropt.svelte @@ -1,20 +1,22 @@ - + @@ -76,8 +65,7 @@

- - + Cropt v2.0.11

diff --git a/package.json b/package.json index 1c324ee..4b7fb63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cropt2", - "version": "2.0.11", + "version": "2.0.14", "description": "A lightweight but powerful JavaScript image cropper", "keywords": [ "cropping", diff --git a/src/cropt.css b/src/cropt.css index ef9ecfe..fe8694b 100644 --- a/src/cropt.css +++ b/src/cropt.css @@ -20,12 +20,12 @@ overflow: hidden; margin: 0 auto; z-index: 1; - height: 320px; + width: 100%; + height: calc(100% - var(--cropt-toolbar, 0px)); } -.cropt-container .cr-boundary, .cropt-container .cr-toolbar-wrap { - width: 320px; + height: calc(var(--cropt-toolbar, 4px) - 8px); } .cropt-container .cr-viewport { diff --git a/src/cropt.ts b/src/cropt.ts index ebfc0f2..980c82a 100644 --- a/src/cropt.ts +++ b/src/cropt.ts @@ -189,6 +189,12 @@ class Cropt { this.element.appendChild(this.elements.boundary); this.element.appendChild(this.elements.toolBar); + if (this.elements.toolBar.childNodes.length ) { + // there's something in toolbar, so show it, and adjust height of picture-box + this.element.style.setProperty('--cropt-toolbar', '32px'); + } else { + this.element.style.setProperty('--cropt-toolbar', '0px'); + } this.#setViewportCss(); this.#initDraggable(); From 12ecca956b790ba8811067ebaf5afa1d725baa39 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Fri, 2 Jan 2026 03:44:44 -0500 Subject: [PATCH 43/53] Added more documentation; css tweaks. --- CHANGELOG.md | 3 + CONTRIBUTING.md | 11 +- docs/index.html | 206 ++++++++++++++++++- docs/readme.html | 526 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- src/cropt.css | 3 +- 6 files changed, 743 insertions(+), 8 deletions(-) create mode 100644 docs/readme.html diff --git a/CHANGELOG.md b/CHANGELOG.md index eaa8a1d..39b1501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # Changelog +## [2.0.15] - 2026-01-02 +- Included more documentation, small fixes and CSS tweaks. + ## [2.0.4] - 2025-12-31 - Extended package options for use in browser, commonjs, etc. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cad2e9f..d2d5d7c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,8 +7,11 @@ We thank our contributors for helping make Cropt even better: [Foliotek/Croppie](https://github.com/Foliotek/Croppie) Originally based on Croppie, but rewritten as a modern ES6 +[Theodore Brown](https://github.com/devtheorem/) + - Simplified Croppie into this streamline solution. + [Filipe Laborde](https://github.com/mindflowgo/) - - Croppie-like resize handle grabbers, rotation, get() + preset restoring, and improved packaging. + - Enhanced Cropt with Croppie-like resize handle grabbers, rotation, get() + preset restoring, and improved packaging. ## Quickstart First, this is a community project, so the developers and contributors appreciate properly prepared contributions and help by others. Please review the code before making changes to keep with the format of the existing code. @@ -41,17 +44,17 @@ cropt/ | ├── cropt.css # Cropt CSS | ├── cropt.ts # Cropt code │ └── demo.ts # Demo javascript -├── demo/ # Demo assets (index.html, photos, styles) -├── docs/ # Documentation (future?) +├── docs/ # Documentation & Demos ├── tests/ # Test files (future?) ├── package.json +├── package.mjs # Builder script └── README.md # new options & methods document here! ### Building and Testing Test your changes thoroughly. Please try to keep existing behaviour and methods so it will be backwards compatible. ```bash -npm run prepare +npm run build npm start ``` diff --git a/docs/index.html b/docs/index.html index c65459a..bc64625 100644 --- a/docs/index.html +++ b/docs/index.html @@ -13,7 +13,8 @@ - + +

- Cropt v2.0.11 + Cropt v2.0.14

Cropt is a modern, lightweight image cropper with zero dependencies. @@ -258,6 +293,127 @@

Code

+ +
+

Why Cropt?

+

Built for developers who need powerful yet small footprint cropper. Built using modern javascript constructs.

+ +
+ +
+
+
+
+
+ +
+

Ultra Flexible

+
+

+ Minimal footprint, zero external dependencies, minimal DOM-manipulation for easy integration. +

+
+
+
+ +
+
+
+
+
+ +
+

 No Dependencies

+
+

+ Pure vanilla JavaScript, ready-shipped in a flexible npm package for use anywhere. +

+
+
+
+ +
+
+
+
+
+ +
+

 Component Ready

+
+

+ Full TypeScript support, and comes with sample components to use various frameworks. +

+
+
+
+
+
+ +
+

Let's Go!

+

If you've reviewed the above examples, you're ready to use it: it's that simple! Load up your code editor and let's install it.

+ +
+ +
+
+
+ NPM + +
+
+
npm install cropt2
+
+
+
+ + +
+
+
+ YARN + +
+
+
yarn add cropt2
+
+
+
+
+ +
+
+ USAGE EXAMPLE + +
+
+
import Cropt from 'cropt2';
+
+const container = document.getElementById("container-id");
+const image_url = "https://raw.githubusercontent.com/mindflowgo/cropt/refs/heads/master/docs/assets/photos/kitten.jpg";
+
+const cropt = new Cropt(container);
+cropt.bind(image_url)
+
+console.log( `Crop area: `, cropt.get() )
+
+
+ +
+ + + + + + +

+ + + + diff --git a/docs/readme.html b/docs/readme.html new file mode 100644 index 0000000..3055dc1 --- /dev/null +++ b/docs/readme.html @@ -0,0 +1,526 @@ + + + + + + Cropt v2 - lightweight image cropper + + + + + + + + + + + + + + + + + + + + + + +
+
+

+ + Cropt Readme +

+

+ Cropt is a modern, lightweight image cropper with zero dependencies. +

+
+
+ + +
+

Is Cropt For Me?

+

+ Cropt is a clean ES6 class with clearly structured methods, perfect for cropping avatars, + thumbnails, or any user-uploaded images. It uses a powerful resizing algorithm to prevent + artifacts when shrinking large images, and works seamlessly on mobile and desktop. +

+ Key Features: viewport resizing, rotation, pinch/scroll zoom, keyboard navigation, + and export to Canvas or Blob — all in under 10KB minified. +

+
+ +
+

Installing

+

Install Cropt as a package, or our Github repo.

+ +
+
+
+
+ NPM + +
+
+
npm install cropt2
+
+
+
+ +
+
+
+ YARN + +
+
+
yarn add cropt2
+
+
+
+
+ +
+
+ RECAP: BASIC USAGE + +
+
+
import Cropt from 'cropt2';
+
+const container = document.getElementById('demo');
+const cropt = new Cropt(container);
+cropt.bind('image.jpg');
+
+
+
+ + +
+

Crop Container

+

+ Cropt will fit into the container you place it in. If your container has no height by default, + nothing will appear, so please make sure you specify a minimum height for your container. +

+
+ + +
+

Options

+

+ Pass these as the second argument to the Cropt constructor. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionTypeDefaultDescription
mouseWheelZoom"off" | "on" | "ctrl""on"Control zoom via mouse wheel. Use "ctrl" to require Ctrl key.
viewport{ width, height, borderRadius }{ width: 220, height: 220, borderRadius: "0px" }Size and shape of crop area. Use borderRadius: "50%" for circle.
zoomerInputClassstring"cr-slider"Custom class for the zoom slider (e.g., Bootstrap’s "form-range").
enableZoomSliderbooleantrueShow/hide the zoom slider.
enableKeypressbooleantrueEnable arrow keys to move image (ignored in input fields).
resizeBarsbooleanfalseShow resize handles to adjust viewport size.
enableRotateBtnsbooleanfalseShow rotation buttons (↺/↻) in toolbar.
+
+
+ + + +
+

Methods

+

+ Methods available on a Cropt instance. +

+ +
+ + +
+

+ +

+
+
+

Loads an image from URL. Optionally pass an initial zoom level (number) or a full preset object to restore a previous state.

+
+
cropt.bind('image.jpg', 1.2); // zoom to 1.2x
+// OR
+cropt.bind('image.jpg', {
+  transform: { x: 10, y: 20, scale: 1.5, rotate: 90 },
+  viewport: { width: 250, height: 250, borderRadius: '8px' }
+});
+
+

→ Resolves when image is loaded and UI is ready.

+
+
+
+ + +
+

+ +

+
+
+

Cleans up the instance and removes all DOM elements.

+
+
cropt.destroy();
+
+

→ No return value. Safe to call multiple times.

+
+
+
+ + +
+

+ +

+
+
+

Recalculates layout. Call this if Cropt was hidden during initialization (e.g., in a modal).

+
+
// After modal opens
+modal.addEventListener('shown.bs.modal', () => {
+  cropt.refresh();
+});
+
+

→ No return value.

+
+
+
+ + +
+

+ +

+
+
+

Returns a canvas of the cropped image. If size is provided, the longest side is scaled to that value.

+
+
const canvas = await cropt.toCanvas(500);
+document.body.appendChild(canvas);
+
+

→ Returns Promise<HTMLCanvasElement>.

+
+
+
+ + +
+

+ +

+
+
+

Exports cropped image as Blob. Supports image/webp, image/jpeg, etc. Falls back to JPEG if WebP is unsupported with quality < 1.

+
+
const blob = await cropt.toBlob(800, 'image/jpeg', 0.8);
+const url = URL.createObjectURL(blob);
+img.src = url;
+
+

→ Returns Promise<Blob> with width and height properties.

+
+
+
+ + +
+

+ +

+
+
+

Returns current crop state. Use for saving/restoring sessions or server-side cropping.

+
+
const data = cropt.get();
+console.log(data.crop);      // { x, y, width, height }
+console.log(data.transform); // { x, y, scale, rotate, origin }
+console.log(data.viewport);  // { width, height, borderRadius }
+
+

→ Returns plain object with crop coordinates, transform, and viewport info.

+
+
+
+ + +
+

+ +

+
+
+

Update options dynamically (e.g., change viewport size or enable rotation).

+
+
cropt.setOptions({
+  viewport: { width: 300, height: 300, borderRadius: '50%' },
+  enableRotateBtns: true
+});
+
+

→ No return value. Re-renders UI if needed.

+
+
+
+ + +
+

+ +

+
+
+

Programatically set zoom level (0–1 range, clamped to Cropt’s min/max).

+
+
cropt.setZoom(0.8);
+
+

→ No return value.

+
+
+
+ + +
+

+ +

+
+
+

Rotate image by 0, 90, 180, or 270 degrees.

+
+
await cropt.setRotation(90);
+
+

→ Returns Promise<void> (waits for image rotation to complete).

+
+
+
+ +
+
+ + +
+ +
+ + + + + + \ No newline at end of file diff --git a/package.json b/package.json index 4b7fb63..10695e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cropt2", - "version": "2.0.14", + "version": "2.0.15", "description": "A lightweight but powerful JavaScript image cropper", "keywords": [ "cropping", diff --git a/src/cropt.css b/src/cropt.css index fe8694b..28c4f4d 100644 --- a/src/cropt.css +++ b/src/cropt.css @@ -18,10 +18,11 @@ .cropt-container .cr-boundary { position: relative; overflow: hidden; - margin: 0 auto; z-index: 1; width: 100%; height: calc(100% - var(--cropt-toolbar, 0px)); + min-width: 100px; + min-height: 100px; } .cropt-container .cr-toolbar-wrap { From 573ae44dff4ab4d1fc502399da57247971b3d9e0 Mon Sep 17 00:00:00 2001 From: Filipe Laborde Date: Fri, 2 Jan 2026 03:52:28 -0500 Subject: [PATCH 44/53] Updated readme --- docs/index.html | 6 +++--- docs/readme.html | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/index.html b/docs/index.html index bc64625..aea8da0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -101,7 +101,7 @@

- Cropt v2.0.14 + Cropt v2.0.15

Cropt is a modern, lightweight image cropper with zero dependencies. @@ -111,7 +111,7 @@

-