truecropper
Version:
A vanilla JavaScript image cropper that's lightweight, awesome, and has absolutely zero dependencies.
1,425 lines • 57.7 kB
JavaScript
var U = Object.defineProperty;
var F = (n, t, e) => t in n ? U(n, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : n[t] = e;
var r = (n, t, e) => F(n, typeof t != "symbol" ? t + "" : t, e);
const w = "truecropper", u = {
base: w,
img: `${w}__image`,
background: `${w}__background`,
new: `${w}__new-selection`,
selection: `${w}__selection`,
handle: `${w}__handle`,
hanleds: `${w}__handles`,
valueX: `${w}X`,
valueY: `${w}Y`,
valueWidth: `${w}Width`,
valueHeight: `${w}Height`,
valueStatus: `${w}Status`,
epsilon: 0.05
}, _ = {
/**
* Error when the target element is not found.
*/
elementNotFound: { text: "Unable to find element", id: 0 },
/**
* Error when the image source is not provided.
*/
srcEmpty: { text: "Image src not provided", id: 1 },
/**
* Error when the parent element does not contain the required <div> element.
*/
parentNotContainDiv: { text: "Parent element can be exists", id: 2 }
};
class v extends Error {
/**
* Creates an instance of TrueCropperHtmlError.
*
* @param key - The key corresponding to a predefined error message.
*/
constructor(e) {
const i = _[e];
super(i.text);
/**
* Additional error data.
*/
r(this, "data");
/**
* The unique identifier for the error message.
*/
r(this, "messageId");
Object.setPrototypeOf(this, v.prototype), this.name = "TrueCropperHtmlError", this.data = {}, this.messageId = i.id;
}
}
class x extends Error {
/**
* Creates an instance of TrueCropperImageError.
*
* @param message - The error message.
* @param data - Additional data associated with the image error.
* @param messageId - A unique identifier for the error message.
*/
constructor(e, i, s) {
super(e);
/**
* Additional data related to the image error.
*/
r(this, "data");
/**
* A unique identifier for the error message.
*/
r(this, "messageId");
Object.setPrototypeOf(this, x.prototype), this.name = "TrueCropperImageError", this.data = {
target: i.target,
targetCoordinates: i.coordinates ? { ...i.coordinates } : void 0,
targetSize: { ...i.targetSize },
source: i.source,
sourceSize: { ...i.sourceSize }
}, this.messageId = s;
}
/**
* Creates a new TrueCropperImageError instance for a start size issue.
*
* @param target - The target element identifier.
* @param coordinates - The coordinates related to the error.
* @param targetSize - The dimensions of the target element.
* @param source - The source element identifier.
* @param sourceSize - The dimensions of the source element.
* @returns A new instance of TrueCropperImageError.
*/
static startSize(e, i, s, h, o) {
const a = `The ${e} (${i.x}x${i.y}:${s.width}x${s.height}) exceeds the ${h} (${o.width}x${o.height})`, l = {
target: e,
coordinates: i,
targetSize: s,
source: h,
sourceSize: o
};
return new this(a, l, 6);
}
/**
* Creates a new TrueCropperImageError instance for a size issue.
*
* @param target - The target element identifier.
* @param targetSize - The dimensions of the target element.
* @param source - The source element identifier.
* @param sourceSize - The dimensions of the source element.
* @returns A new instance of TrueCropperImageError.
*/
static size(e, i, s, h) {
const o = `The ${e} (${i.width}x${i.height}) exceeds the ${s} (${h.width}x${h.height})`, a = {
target: e,
coordinates: void 0,
targetSize: i,
source: s,
sourceSize: h
};
return new this(o, a, 7);
}
}
class d extends Error {
/**
* Creates an instance of TrueCropperOptionsError.
*
* @param message - The error message.
* @param data - Additional error data.
* @param messageId - A unique identifier for the error message.
*/
constructor(e, i, s = 0) {
super(e);
/**
* Additional data associated with the options error.
*/
r(this, "data");
/**
* A unique identifier for the error message.
*/
r(this, "messageId");
Object.setPrototypeOf(this, d.prototype), this.name = "TrueCropperOptionsError", this.data = i, this.messageId = s;
}
/**
* Factory method for creating an options error related to aspect ratio mismatch.
*
* @param name - The name of the property or dimension with the aspect ratio issue.
* @param calculatedAspectRatio - The calculated aspect ratio based on dimensions.
* @param aspectRatio - The expected aspect ratio.
* @param epsilon - The tolerance value for aspect ratio differences.
* @returns A new instance of TrueCropperOptionsError with aspect ratio error details.
*/
static aspectRatio(e, i, s, h) {
const o = `The specified aspect ratio (${s}) and calculated ${e} dimensions (width/height = ${i}) are greater than (${h}). This might be due to a rounding error on the server side or incorrect minimum sizes.`;
return new this(o, { name: e }, 5);
}
static widthIsNull(e) {
const i = `The width of (${e}) is null`;
return new this(i, { name: e }, 8);
}
static heightIsNull(e) {
const i = `The height of (${e}) is null`;
return new this(i, { name: e }, 9);
}
static badSizeOfPercent(e) {
const i = `The percent values of (${e}) > 100`;
return new this(i, { name: e }, 10);
}
/**
* Factory method for creating a generic options error.
*
* @param name - The name of the option.
* @param object - The expected or disallowed object description.
* @param positive - If true, indicates the option must be the specified object; if false, indicates it must not be.
* @returns A new instance of TrueCropperOptionsError with generic error details.
*/
static new(e, i, s = !0) {
const h = s ? 3 : 4, o = s ? `${e} must be ${i}` : `${e} must not be ${i}`;
return new this(o, { name: e, object: i }, h);
}
}
const q = (n) => {
let t = null;
if (typeof n == "string") {
if (t = document.querySelector(n), t === null)
throw new v("elementNotFound");
} else
t = n;
if (!(t instanceof HTMLImageElement))
throw new v("srcEmpty");
let e = t.parentElement;
if (!e)
throw new v("parentNotContainDiv");
return e.classList.contains(u.base) || (e = null), [t, e];
}, M = (n, t = void 0) => {
const e = document.createElement("div");
return e.className = n, t && t.appendChild(e), e;
}, A = (n, t) => {
if (t.savedCoordinate < 0)
return { flipped: !1, coordinate: null, size: null, point: 0.5 };
const e = n < t.savedCoordinate, i = t.left !== e, s = t.savedCoordinate, h = Math.abs(t.savedCoordinate - n), o = Number(e);
return {
flipped: i,
coordinate: s,
size: h,
point: o
};
}, Z = (n, t, e) => {
const i = A(n.x, t), s = A(n.y, e);
return {
flipped: { x: i.flipped, y: s.flipped },
newBox: {
coordinates: { x: i.coordinate, y: s.coordinate },
size: { width: i.size, height: s.size },
points: { x: i.point, y: s.point }
}
};
}, G = (n, t, e, i, s) => {
const h = (m, p, z) => z === "relative" ? m * s[p] : z === "percent" ? m >= 1 ? i[p] * (m / 100) : i[p] * m : m, o = {
width: h(t.width, "width", t.unit),
height: h(t.height, "height", t.unit)
}, a = {
width: h(e.width, "width", e.unit),
height: h(e.height, "height", e.unit)
}, l = {
x: h(n.x, "width", n.unit),
y: h(n.y, "height", n.unit)
}, c = {
width: h(n.width, "width", n.unit),
height: h(n.height, "height", n.unit)
};
return { coordinates: l, size: c, minSize: o, maxSize: a };
}, J = (n, t, e, i, s, h) => {
const o = I(
n.minSize,
{ width: 1, height: 1 },
e
);
let a = I(n.maxSize, t, e), l = I(n.size, t, e);
a = tt(a, t, e);
let c = n.coordinates;
if (s) {
const m = et(
c,
l,
o,
a,
t,
e,
h.x,
h.y
);
c = m.coordinates, l = m.size;
}
return { coordinates: c, size: l, minSize: o, maxSize: a, imgProps: t, aspectRatio: e, epsilon: i };
}, K = ({
coordinates: n,
minSize: t,
maxSize: e,
size: i,
imgProps: s
}) => {
const h = (o, a, l, c) => {
if (o.width > a.width || o.height > a.height)
throw x.size(l, o, c, a);
};
if (h(t, s, "minSize", "imageSize"), h(t, e, "minSize", "maxSize"), h(t, i, "minSize", "startSize"), n.x + i.width > s.width || n.y + i.height > s.height)
throw x.startSize(
"startSize",
n,
i,
"imageSize",
s
);
}, B = ({
size: n,
minSize: t,
maxSize: e,
aspectRatio: i
}) => {
const s = { ...n };
return e && (s.width > e.width && (s.width = e.width, s.height = i ? e.width / i : s.height), s.height > e.height && (s.width = i ? e.height * i : s.width, s.height = e.height)), t && (s.width < t.width && (s.width = t.width, s.height = i ? t.width / i : s.height), s.height < t.height && (s.width = i ? t.height * i : s.width, s.height = t.height)), s;
}, W = (n, t, e) => {
const i = n * t;
return { width: i, height: i / e };
}, j = (n, t, e) => {
const i = n * t;
return { width: i * e, height: i };
}, Q = (n, t, e) => {
let i = { ...n.size };
if (e === 0)
return i;
const s = n.isMultiAxis ? i.height * e >= i.width : n.isVerticalMovement, h = n.points.x === 1 || n.points.x === 0 ? 1 : 2, o = n.points.y === 1 || n.points.y === 0 ? 1 : 2;
if (s) {
const a = i.height;
i = { width: a * e, height: a };
} else {
const a = i.width;
i = { width: a, height: a / e };
}
return n.coordinates.x + i.width * (1 - n.points.x) > t.width && (i = W(
t.width - n.coordinates.x,
h,
e
)), n.coordinates.y + i.height * (1 - n.points.y) > t.height && (i = j(
t.height - n.coordinates.y,
o,
e
)), n.coordinates.x - i.width * n.points.x < 0 && (i = W(n.coordinates.x, h, e)), n.coordinates.y - i.height * n.points.y < 0 && (i = j(n.coordinates.y, o, e)), i;
}, I = (n, t, e) => {
const i = { ...n };
return e && !i.width && !i.height && (e > 1 ? i.height = t.height : i.width = t.width), i.width || (i.width = e ? i.height * e : t.width), i.height || (i.height = e ? i.width / e : t.height), i;
}, tt = (n, t, e) => {
let i = { ...n };
return e && (i.width > i.height * e ? i.width = i.height * e : i.height = i.width / e), i = B({
size: i,
maxSize: t,
aspectRatio: e
}), i;
}, et = (n, t, e, i, s, h, o, a) => {
const l = { ...t }, c = { ...n }, m = Math.min(i.width, s.width - n.x), p = Math.min(i.height, s.height - n.y), z = B({
size: l,
maxSize: { width: m, height: p },
minSize: e,
aspectRatio: h
});
return l.width = z.width, l.height = z.height, c.x = o ? (s.width - l.width) / 2 : n.x, c.y = a ? (s.height - l.height) / 2 : n.y, { coordinates: c, size: l };
};
class it {
/**
* Creates a new Box instance.
* @constructor
* @param {TrueCropperBoxInitConfig} - Initialization parameters.
*/
constructor({
coordinates: t,
size: e,
minSize: i,
maxSize: s,
imgProps: h,
aspectRatio: o,
epsilon: a
}) {
r(this, "coordinates");
r(this, "size");
r(this, "minSize");
r(this, "maxSize");
r(this, "imgSize");
r(this, "aspectRatio");
r(this, "epsilon");
this.coordinates = { ...t }, this.size = { ...e }, this.minSize = { ...i }, this.maxSize = { ...s }, this.imgSize = { ...h }, this.aspectRatio = o, this.epsilon = a;
}
/**
* Sets the value of coordinates and size properties based on the provided BoxProps object.
* @param {TrueCropperBoxProps} box - The BoxProps object containing x, y, width, and height properties.
* @returns {void}
*/
setValue(t) {
return t.width < this.minSize.width || t.height < this.minSize.height ? { ok: !1, message: "Crop region is smaller than the minimum allowed size." } : t.width > this.maxSize.width || t.height > this.maxSize.height ? { ok: !1, message: "Crop region exceeds the maximum allowed size." } : this.aspectRatio && t.width / t.height - this.aspectRatio > this.epsilon ? { ok: !1, message: "Crop region does not match the required aspect ratio." } : t.x < 0 || t.x > this.imgSize.width || t.y < 0 || t.y > this.imgSize.height ? { ok: !1, message: "Crop region is positioned outside the image boundaries." } : t.x + t.width > this.imgSize.width || t.y + t.height > this.imgSize.height ? { ok: !1, message: "Crop region extends beyond the image boundaries." } : (this.coordinates = { x: t.x, y: t.y }, this.size = { width: t.width, height: t.height }, { ok: !0, message: "success" });
}
/**
* Moves the box to the specified coordinates within the boundaries of the image.
* @param {TrueCropperCoordinates} coordinates - The new x and y coordinates for the box.
* @returns {void}
*/
move(t) {
this.coordinates.x = Math.min(
Math.max(t.x, 0),
this.imgSize.width - this.size.width
), this.coordinates.y = Math.min(
Math.max(t.y, 0),
this.imgSize.height - this.size.height
);
}
/**
* Resizes the box to a new size.
* @param {TrueCropperSize} size - The new size for the box.
* @param {TrueCropperPoints} points - The relative points for resizing.
* @returns {void}
*/
resize(t, e) {
if (e.x < 0 || e.x > 1 || e.y < 0 || e.y > 1)
return { ok: !1, message: "Point coordinates must be within the range of 0 to 1." };
const i = this.coordinates.x + this.size.width * e.x, s = this.coordinates.y + this.size.height * e.y, h = i - t.width * e.x, o = s - t.height * e.y;
return this.setValue({ x: h, y: o, width: t.width, height: t.height });
}
/**
* Scales the box by a factor and relative points.
* @param {number} factor - The scaling factor.
* @param {TrueCropperPoints} points - The relative points for scaling.
* @returns {void}
*/
scale(t, e) {
const i = this.size.width * t, s = this.size.height * t;
return this.resize({ width: i, height: s }, e);
}
/**
* Retrieves the current dimensions of the box.
* @returns {TrueCropperSize} The width and height of the box.
*/
getBoxSize() {
return { ...this.imgSize };
}
/**
* Retrieves the current coordinates of the box.
* @returns {TrueCropperCoordinates} The current x and y coordinates of the box.
*/
getCoourdinates() {
return { x: this.coordinates.x, y: this.coordinates.y };
}
/**
* Retrieves the current box.
* @returns {TrueCropperBoxProps} The current x and y coordinates, width, and height of the box.
*/
getValue() {
return {
x: this.coordinates.x,
y: this.coordinates.y,
width: this.size.width,
height: this.size.height
};
}
/**
* Retrieves the current real(natural) value of the box including coordinates, width, and height.
* @returns {TrueCropperBoxProps} The current x and y coordinates, width, and height of the box.
*/
getValueReal() {
return this.getValue();
}
/**
* Retrieves the current value of the box relative to a specified width and height.
* @param {TrueCropperSize} size - The width and height for calculating relative values.
* @returns {TrueCropperBoxProps} The current x and y coordinates, width, and height of the box relative to the specified width and height.
*/
getValueRelative({ width: t, height: e }) {
return {
x: this.coordinates.x * t,
y: this.coordinates.y * e,
width: this.size.width * t,
height: this.size.height * e
};
}
/**
* Retrieves the current value of the box as a percentage of the image size.
* @returns {TrueCropperBoxProps} The current x and y coordinates, width, and height of the box as a percentage of the image size.
*/
getValuePercent() {
return {
x: this.coordinates.x / this.imgSize.width * 100,
y: this.coordinates.y / this.imgSize.height * 100,
width: this.size.width / this.imgSize.width * 100,
height: this.size.height / this.imgSize.height * 100
};
}
/**
* Calculates the coordinates of the opposite corner of the box based on relative points.
* @param {TrueCropperPoints} points - The relative points determining the opposite corner.
* @returns {TrueCropperCoordinates} The calculated x and y coordinates of the opposite corner.
*/
getOppositeCornerCoordinates(t) {
const e = t.x === 0.5 ? -1 : this.coordinates.x + this.size.width * (1 - t.x), i = t.y === 0.5 ? -1 : this.coordinates.y + this.size.height * (1 - t.y);
return { x: e, y: i };
}
/**
* Prepares and applies new size and coordinates for the box based on the provided data.
* @param {TrueCropperNullableBoxData} newBox - The new box data to apply.
* @returns {boolean} Returns true if the new size and coordinates were successfully applied, false otherwise.
*/
prepareAndApplyNewSizeAndCoordinates(t) {
const e = this.prepareSizeAndCoordinates(t);
if (e.size.width === 0 || e.size.height === 0)
return !1;
const i = this.adjustAndCalculateSize(e), s = this.adjustAndCalculateCoordinate(
e.coordinates,
i,
e.points
);
return s.x < 0 || s.x + i.width > this.imgSize.width || s.y < 0 || s.y + i.height > this.imgSize.height ? !1 : (this.size = i, this.coordinates = s, !0);
}
/**
* Prepares and calculates the size and coordinates for the new box based on the provided data.
* @param {TrueCropperNullableBoxData} newBox - The new box data to calculate size and coordinates for.
* @returns {TrueCropperDragData} An object containing the calculated size, coordinates, and other relevant properties.
*/
prepareSizeAndCoordinates(t) {
const e = {
width: t.size.width ?? this.size.width,
height: t.size.height ?? this.size.height
}, i = {
x: t.coordinates.x ?? this.coordinates.x + this.size.width / 2,
y: t.coordinates.y ?? this.coordinates.y + this.size.height / 2
}, s = t.coordinates.y !== null, h = s && t.coordinates.x !== null;
return {
size: e,
coordinates: i,
isVerticalMovement: s,
isMultiAxis: h,
points: t.points
};
}
/**
* Adjusts and calculates the size based on aspect ratio and constraints for the new box.
* @param {TrueCropperDragData} data - The data containing coordinates, size, and other parameters for adjustment.
* @returns {TrueCropperSize} The adjusted size within the constraints of aspect ratio, min size, and max size.
*/
adjustAndCalculateSize(t) {
const e = Q(t, this.imgSize, this.aspectRatio);
return B({
size: e,
minSize: this.minSize,
maxSize: this.maxSize,
aspectRatio: this.aspectRatio
});
}
/**
* Adjusts and calculates the new coordinates based on the input coordinates, size, and points.
* @param {TrueCropperCoordinates} coordinates - The original coordinates.
* @param {TrueCropperSize} size - The size to adjust the coordinates.
* @param {TrueCropperPoints} points - The points to calculate the adjustment.
* @returns {TrueCropperCoordinates} The adjusted coordinates based on the size and points.
*/
adjustAndCalculateCoordinate(t, e, i) {
return {
x: t.x - e.width * i.x,
y: t.y - e.height * i.y
};
}
}
function st(n) {
n.addEventListener("touchstart", k), n.addEventListener("touchend", k), n.addEventListener("touchmove", k);
}
function k(n) {
n.preventDefault();
const t = n, e = t.changedTouches[0];
e.target.dispatchEvent(
new MouseEvent(nt(t.type), {
bubbles: !0,
cancelable: !0,
view: window,
clientX: e.clientX,
clientY: e.clientY,
screenX: e.screenX,
screenY: e.screenY
})
);
}
function nt(n) {
switch (n) {
case "touchstart":
return "mousedown";
case "touchmove":
return "mousemove";
default:
return "mouseup";
}
}
class ht {
/**
* Creates an instance of Background.
*
* @param parent - The parent HTMLDivElement where the background elements will be appended.
* @param className - The base CSS class name for the background elements.
*/
constructor(t, e) {
r(this, "nested", []);
for (let i = 0; i < 4; i++) {
const s = M(`${e}-${i}`, t);
this.nested.push(s);
}
}
/**
* Hides the background elements by setting their display style to "none".
*/
hide() {
for (const t of this.nested)
t.style.display = "none";
}
/**
* Displays the background elements by setting their display style to "block".
*/
show() {
for (const t of this.nested)
t.style.display = "block";
}
/**
* Removes the background elements from the DOM.
*/
destroy() {
for (const t of this.nested)
t.remove();
}
/**
* Transforms the background elements based on the provided crop box.
*
* @param box - An object representing the crop box, including its x and y coordinates and dimensions.
*/
transform(t) {
const e = t.x + t.width, i = t.y + t.height;
this.nested[0].style.height = `${t.y}px`, this.nested[0].style.left = `${t.x}px`, this.nested[0].style.right = `calc(100% - ${t.width}px - ${t.x}px)`, this.nested[1].style.left = `${e}px`, this.nested[2].style.left = `${t.x}px`, this.nested[2].style.right = `calc(100% - ${t.width}px - ${t.x}px)`, this.nested[2].style.top = `${i}px`, this.nested[3].style.width = `${t.x}px`;
}
}
class ot {
/**
* Creates a new NewSelection instance.
*
* @param parent - The parent HTMLDivElement where the new selection element is appended.
* @param className - The CSS class name for styling the new selection element.
* @param eventBus - A callback function for communicating events (e.g., creating a new box).
* @param enable - Determines whether the new selection functionality is enabled.
*/
constructor(t, e, i, s) {
/**
* Callback function to communicate events to the parent.
*/
r(this, "eventBus");
/**
* The container element for the new selection.
*/
r(this, "el");
/**
* The starting mouse coordinates when a new selection is initiated.
*/
r(this, "startMouse", { mouseX: 0, mouseY: 0 });
/**
* Flag indicating whether a new crop box has been created.
*/
r(this, "newBoxCreated", !1);
/**
* Reference to the mousedown event listener.
*/
r(this, "listener");
this.eventBus = i, this.el = M(e, t), s ? (this.listener = this.mouseEvent(), this.el.addEventListener("mousedown", this.listener)) : this.hide();
}
/**
* Hides the new selection element.
*/
hide() {
this.el.style.display = "none";
}
/**
* Shows the new selection element.
*/
show() {
this.el.style.display = "block";
}
/**
* Removes the new selection element from the DOM and cleans up event listeners.
*/
destroy() {
this.listener && this.el.removeEventListener("mousedown", this.listener), this.el.remove();
}
/**
* Creates and returns a mousedown event handler that initiates the new selection process.
*
* When the user presses the mouse button down, mousemove and mouseup listeners are attached
* to track the selection process.
*
* @returns A mousedown event handler function.
*/
mouseEvent() {
const t = (s) => {
s.stopPropagation(), document.addEventListener("mousemove", e), document.addEventListener("mouseup", i), this.startMouse = { mouseX: s.clientX, mouseY: s.clientY }, this.newBoxCreated = !1;
}, e = (s) => {
if (s.stopPropagation(), this.newBoxCreated) {
const h = { x: s.clientX, y: s.clientY };
this.eventBus({ type: "handlemove", data: h });
} else
this.tryToCreateNewBox(s.clientX, s.clientY);
}, i = (s) => {
s.stopPropagation(), document.removeEventListener("mousemove", e), document.removeEventListener("mouseup", i), this.newBoxCreated && this.eventBus({ type: "handleend" });
};
return t;
}
/**
* Attempts to create a new crop box based on the current mouse coordinates.
*
* This method calculates the new crop box dimensions from the starting mouse position
* and the current mouse position, then notifies the parent via the event bus.
*
* @param mouseX - The current x-coordinate of the mouse.
* @param mouseY - The current y-coordinate of the mouse.
*/
tryToCreateNewBox(t, e) {
if (t === this.startMouse.mouseX || e === this.startMouse.mouseY)
return;
const i = t < this.startMouse.mouseX, s = e < this.startMouse.mouseY, [h, o] = i ? [t, this.startMouse.mouseX - t] : [this.startMouse.mouseX, t - this.startMouse.mouseX], [a, l] = s ? [e, this.startMouse.mouseY - e] : [this.startMouse.mouseY, e - this.startMouse.mouseY], c = {
coordinates: { x: h, y: a },
size: { width: o, height: l },
leftMovable: i,
topMovable: s
};
this.newBoxCreated = this.eventBus({ type: "createnewbox", data: c });
}
}
class rt {
/**
* Creates a new Selection instance.
*
* @param parent - The parent HTMLDivElement to which the selection element is appended.
* @param className - The CSS class name assigned to the selection element.
* @param eventBus - A callback to emit events related to selection interactions.
* @param enable - Determines if the selection element should be interactive.
*/
constructor(t, e, i, s) {
/**
* Callback function to handle events emitted by the selection component.
*/
r(this, "eventBus");
/**
* The DOM element representing the selection area.
*/
r(this, "el");
/**
* Indicates whether the selection is interactive.
*/
r(this, "enable");
/**
* Reference to the mousedown event listener.
*/
r(this, "listener");
this.eventBus = i, this.el = M(e, t), this.enable = s, s ? (this.listener = this.mouseEvent(), this.el.addEventListener("mousedown", this.listener)) : this.el.style.cursor = "default";
}
/**
* Transforms the selection element to match the specified crop box dimensions.
*
* @param box - An object containing the x, y coordinates and width, height dimensions.
*/
transform(t) {
this.el.style.transform = `translate(${t.x}px, ${t.y}px)`, this.el.style.width = `${t.width}px`, this.el.style.height = `${t.height}px`;
}
/**
* Hides the selection element.
*/
hide() {
this.el.style.display = "none", this.el.style.cursor = "default";
}
/**
* Shows the selection element.
*/
show() {
this.el.style.display = "block", this.el.style.cursor = "move";
}
/**
* Destroys the selection element by removing it from the DOM and cleaning up event listeners.
*/
destroy() {
this.listener && this.el.removeEventListener("mousedown", this.listener), this.el.remove();
}
/**
* Creates and returns a mousedown event handler for the selection element.
*
* This handler attaches mousemove and mouseup listeners to the document to enable
* dragging of the selection element. It emits corresponding events via the event bus.
*
* @returns A mousedown event handler function.
*/
mouseEvent() {
const t = (s) => {
if (s.stopPropagation(), !this.enable)
return;
document.addEventListener("mousemove", e), document.addEventListener("mouseup", i);
const h = { x: s.clientX, y: s.clientY };
this.eventBus({ type: "regionstart", data: h });
}, e = (s) => {
s.stopPropagation();
const h = { x: s.clientX, y: s.clientY };
this.eventBus({ type: "regionmove", data: h });
}, i = (s) => {
s.stopPropagation(), document.removeEventListener("mousemove", e), document.removeEventListener("mouseup", i);
const h = { x: s.clientX, y: s.clientY };
this.eventBus({ type: "regionend", data: h });
};
return t;
}
}
const at = ["real", "relative", "percent"];
var y = /* @__PURE__ */ ((n) => (n.Waiting = "waiting", n.Ready = "ready", n.Reloading = "reloading", n.Error = "error", n))(y || {});
const lt = u.base;
function dt(n) {
return n.charAt(0).toUpperCase() + n.slice(1);
}
function f(n) {
return n == null;
}
function g(n, t, e, i = !1, s = !1) {
if (f(t))
return e;
if (typeof t != "number")
throw d.new(n, "number");
if (Number.isNaN(t))
throw d.new(n, "NaN", !1);
if (i ? t < 0 : t <= 0)
throw d.new(n, "positive");
if (!s && t > 0 && t < 1)
throw d.new(n, "fractional");
return t;
}
function E(n, t, e) {
if (f(t))
return e;
if (typeof t != "boolean")
throw d.new(n, "boolean");
return t;
}
function b(n, t, e) {
if (f(t))
return e;
if (typeof t != "string" || !at.includes(t))
throw d.new(n, "SizeUnit");
return t;
}
const ct = (n, t) => {
var h, o, a, l, c, m, p, z, R, D, T, L, X, H, Y, O;
const e = t || {};
if (typeof e != "object" || e === null)
throw d.new("options", "object");
const i = (P, V) => {
const S = n[`${lt}${dt(P)}`];
if (!S)
return V;
const C = S.toLowerCase();
return C === "null" || C === "undefined" || C === "nil" ? V : S.trim().length !== 0 && !Number.isNaN(Number(S)) ? Number(S) : C === "true" ? !0 : C === "false" ? !1 : S;
}, s = {
aspectRatio: i("aspectRatio", e.aspectRatio),
epsilon: i("epsilon", e.epsilon),
allowFlip: i("allowFlip", e.allowFlip),
allowNewSelection: i("allowNewSelection", e.allowNewSelection),
allowMove: i("allowMove", e.allowMove),
allowResize: i("allowResize", e.allowResize),
returnMode: i("returnMode", e.returnMode),
minSize: {
width: i("minSizeWidth", (h = e.minSize) == null ? void 0 : h.width),
height: i("minSizeHeight", (o = e.minSize) == null ? void 0 : o.height),
unit: i("minSizeUnit", (a = e.minSize) == null ? void 0 : a.unit)
},
maxSize: {
width: i("maxSizeWidth", (l = e.maxSize) == null ? void 0 : l.width),
height: i("maxSizeHeight", (c = e.maxSize) == null ? void 0 : c.height),
unit: i("maxSizeUnit", (m = e.maxSize) == null ? void 0 : m.unit)
},
startSize: {
x: i("startSizeX", (p = e.startSize) == null ? void 0 : p.x),
y: i("startSizeY", (z = e.startSize) == null ? void 0 : z.y),
width: i("startSizeWidth", (R = e.startSize) == null ? void 0 : R.width),
height: i("startSizeHeight", (D = e.startSize) == null ? void 0 : D.height),
unit: i("startSizeUnit", (T = e.startSize) == null ? void 0 : T.unit)
},
defaultSize: {
x: i("defaultSizeX", (L = e.defaultSize) == null ? void 0 : L.x),
y: i("defaultSizeY", (X = e.defaultSize) == null ? void 0 : X.y),
width: i("defaultSizeWidth", (H = e.defaultSize) == null ? void 0 : H.width),
height: i("defaultSizeHeight", (Y = e.defaultSize) == null ? void 0 : Y.height),
unit: i("defaultSizeUnit", (O = e.defaultSize) == null ? void 0 : O.unit)
}
};
return f(s.startSize.x) && f(s.startSize.y) && f(s.startSize.width) && f(s.startSize.height) && (s.startSize = s.defaultSize), s;
}, $ = (n, t, e) => Math.abs(n - t) < e, ut = (n) => {
var a;
const t = g("aspectRatio", n.aspectRatio, 0, !1, !0), e = g("epsilon", n.epsilon, u.epsilon, !0, !0), i = {
width: g("minSizeWidth", n.minSize.width, 0),
height: g("minSizeHeight", n.minSize.height, 0),
unit: b("minSizeUnit", (a = n.minSize) == null ? void 0 : a.unit, "real")
}, s = {
width: g("maxSizeWidth", n.maxSize.width, 0),
height: g("maxSizeHeight", n.maxSize.height, 0),
unit: b("maxSizeUnit", n.maxSize.unit, "real")
}, h = {
x: g("startSizeX", n.startSize.x, 0, !0),
y: g("startSizeY", n.startSize.y, 0, !0),
width: g("startSizeWidth", n.startSize.width, 0),
height: g("startSizeHeight", n.startSize.height, 0),
unit: b("startSizeUnit", n.startSize.unit, "real"),
centeredX: f(n.startSize.x),
centeredY: f(n.startSize.y),
allowChange: !1
};
h.allowChange = h.width === 0 && h.height === 0;
const o = {
x: g("defaultSizeX", n.defaultSize.x, 0, !0),
y: g("defaultSizeY", n.defaultSize.y, 0, !0),
width: g("defaultSizeWidth", n.defaultSize.width, 0),
height: g("defaultSizeHeight", n.defaultSize.height, 0),
unit: b("defaultSizeUnit", n.defaultSize.unit, "real"),
centeredX: f(n.defaultSize.x),
centeredY: f(n.defaultSize.y),
allowChange: !1
};
if (o.allowChange = o.width === 0 && o.height === 0, t) {
if (i.width && i.height) {
const l = i.width / i.height;
if (!$(l, t, e))
throw d.aspectRatio(
"minimum",
l,
t,
e
);
}
if (o.width && o.height) {
const l = o.width / o.height;
if (!$(l, t, e))
throw d.aspectRatio(
"defaultSize",
l,
t,
e
);
}
if (h.width && h.height) {
const l = h.width / h.height;
if (!$(l, t, e))
throw d.aspectRatio(
"startSize",
l,
t,
e
);
}
}
if (!h.centeredX && h.width === 0)
throw d.widthIsNull("firstInitSize");
if (!h.centeredY && h.height === 0)
throw d.heightIsNull("firstInitSize");
if (!o.centeredX && o.width === 0)
throw d.widthIsNull("startSize");
if (!o.centeredY && o.height === 0)
throw d.heightIsNull("startSize");
if (o.unit === "percent" && (o.x + o.width > 100 || o.y + o.height > 100))
throw d.badSizeOfPercent("startSize");
if (h.unit === "percent" && (h.x + h.width > 100 || h.y + h.height > 100))
throw d.badSizeOfPercent("firstInitSize");
if (i.unit === "percent" && (i.width > 100 || i.height > 100))
throw d.badSizeOfPercent("minSize");
if (s.unit === "percent" && (s.width > 100 || s.height > 100))
throw d.badSizeOfPercent("maxSize");
return {
aspectRatio: t,
epsilon: e,
allowFlip: E("allowFlip", n.allowFlip, !0),
allowNewSelection: E("allowNewSelection", n.allowNewSelection, !0),
allowMove: E("allowMove", n.allowMove, !0),
allowResize: E("allowResize", n.allowResize, !0),
returnMode: b("returnMode", n.returnMode, "real"),
minSize: i,
maxSize: s,
firstInitSize: h,
startSize: o
};
};
class gt {
/**
* Creates an instance of the Handle.
*
* @param parent - The parent HTMLDivElement to which the handle element is appended.
* @param className - The CSS class name to assign to the handle element.
* @param item - The handle configuration object, including its position and cursor style.
* @param eventBus - A callback function to handle events emitted by the handle.
* @param enable - Determines whether the handle is enabled.
*/
constructor(t, e, i, s, h) {
/**
* The normalized position of the handle (values between 0 and 1).
*/
r(this, "position");
/**
* Event bus function used to emit handle events.
*/
r(this, "eventBus");
/**
* The HTML element representing the handle.
*/
r(this, "el");
/**
* Flag indicating whether the handle is enabled.
*/
r(this, "enable");
/**
* The event listener function for handling mouse events.
*/
r(this, "listener");
this.position = i.position, this.eventBus = s, this.enable = h, this.el = M(e, t), this.el.style.cursor = i.cursor, h ? (this.listener = this.mouseEvent(), this.el.addEventListener("mousedown", this.listener)) : this.hide();
}
/**
* Displays the handle element.
*/
show() {
this.el.style.display = "block";
}
/**
* Hides the handle element.
*/
hide() {
this.el.style.display = "none";
}
/**
* Destroys the handle by removing event listeners and detaching it from the DOM.
*/
destroy() {
this.listener && this.el.removeEventListener("mousedown", this.listener), this.el.remove();
}
/**
* Transforms the handle's position based on the provided crop box properties.
*
* @param box - The crop box properties (x, y, width, height).
*/
transform(t) {
const e = this.el.offsetWidth, i = this.el.offsetHeight, s = t.x + t.width * this.position.x - e / 2, h = t.y + t.height * this.position.y - i / 2;
this.el.style.transform = `translate(${s}px, ${h}px)`;
}
/**
* Retrieves data associated with the handle.
*
* @returns An object containing the handle's normalized position.
*/
getData() {
return {
points: { ...this.position }
};
}
/**
* Creates and returns a mouse event handler for the handle.
*
* This function attaches mousemove and mouseup listeners to the document when a mousedown event is detected.
*
* @returns The mousedown event handler function.
*/
mouseEvent() {
const t = (s) => {
if (s.stopPropagation(), !this.enable)
return;
document.addEventListener("mousemove", e), document.addEventListener("mouseup", i);
const h = this.getData();
this.eventBus({ type: "handlestart", data: h });
}, e = (s) => {
s.stopPropagation();
const h = { x: s.clientX, y: s.clientY };
this.eventBus({ type: "handlemove", data: h });
}, i = (s) => {
s.stopPropagation(), document.removeEventListener("mousemove", e), document.removeEventListener("mouseup", i), this.eventBus({ type: "handleend" });
};
return t;
}
}
const mt = [
{ position: { x: 0, y: 0 }, cursor: "nw-resize" },
{ position: { x: 0.5, y: 0 }, cursor: "n-resize" },
{ position: { x: 1, y: 0 }, cursor: "ne-resize" },
{ position: { x: 1, y: 0.5 }, cursor: "e-resize" },
{ position: { x: 1, y: 1 }, cursor: "se-resize" },
{ position: { x: 0.5, y: 1 }, cursor: "s-resize" },
{ position: { x: 0, y: 1 }, cursor: "sw-resize" },
{ position: { x: 0, y: 0.5 }, cursor: "w-resize" }
];
class wt {
/**
* Creates a new instance of the Handles collection.
*
* @param parent - The parent HTMLDivElement to which the handles container is appended.
* @param className - The CSS class name for the handles container.
* @param eventBus - A callback function to handle events emitted by the handles.
* @param enable - Determines whether the handles are enabled for user interaction.
* @param handleClassName - The CSS class name for individual handle elements.
*/
constructor(t, e, i, s, h) {
/**
* The container element for the handles.
*/
r(this, "el");
/**
* Array of individual handle instances.
*/
r(this, "handles", []);
this.el = M(e, t);
for (const o of mt) {
const a = new gt(
this.el,
h,
o,
i,
s
);
this.handles.push(a);
}
}
/**
* Hides all the handles by setting their display style to "none".
*/
hide() {
for (const t of this.handles)
t.hide();
}
/**
* Shows all the handles by setting their display style to "block".
*/
show() {
for (const t of this.handles)
t.show();
}
/**
* Destroys all handles by removing them from the DOM.
*/
destroy() {
for (const t of this.handles)
t.destroy();
this.el.remove();
}
/**
* Transforms (repositions) all handles based on the provided crop box dimensions.
*
* @param box - An object representing the crop box properties (x, y, width, height).
*/
transform(t) {
for (const e of this.handles)
e.transform(t);
}
/**
* Retrieves a handle based on the movability of the crop box edges.
*
* @param leftMovable - Indicates whether the left edge of the crop box is movable.
* @param topMovable - Indicates whether the top edge of the crop box is movable.
* @returns The handle corresponding to the specified movability configuration.
*/
handleByMovableType(t, e) {
return t ? e ? this.handles[0] : this.handles[6] : e ? this.handles[2] : this.handles[4];
}
}
const N = { width: 0, height: 0 };
class pt {
constructor(t, e) {
r(this, "replaceDOM", !1);
r(this, "htmlContainer");
r(this, "htmlImg");
r(this, "options");
r(this, "newSelection");
r(this, "selection");
r(this, "handles");
r(this, "background");
r(this, "box");
r(this, "currentMove");
r(this, "activeHandle");
r(this, "real", N);
r(this, "relative", N);
r(this, "ratio", N);
r(this, "firstInit", !0);
r(this, "isDomCreated", !1);
r(this, "status", y.Waiting);
r(this, "eventBus", this.event.bind(this));
r(this, "observer");
r(this, "preventDoubleLoad");
r(this, "callbacks", {
onInitialize: void 0,
onCropStart: void 0,
onCropChange: void 0,
onCropEnd: void 0,
onError: void 0
});
try {
this.parseCallbackFunctions(e);
const [i, s] = q(t);
this.htmlImg = i, s ? this.htmlContainer = s : this.replaceDOM = !0, this.changeStatus(y.Waiting);
const h = ct(this.htmlImg.dataset, e);
this.options = ut(h), this.initializeCropper();
} catch (i) {
if (i instanceof v || i instanceof d)
this.onErrorCallback(i);
else
throw i;
}
}
getImagePreview() {
if (this.status !== "ready" || !this.htmlImg.complete || this.htmlImg.naturalWidth === 0 || this.htmlImg.naturalHeight === 0)
return null;
const t = this.getValue("real");
if (!t || t.width <= 0 || t.height <= 0 || t.x < 0 || t.y < 0 || t.x + t.width > this.htmlImg.naturalWidth || t.y + t.height > this.htmlImg.naturalHeight)
return null;
const e = document.createElement("canvas"), i = window.devicePixelRatio || 1;
e.width = t.width * i, e.height = t.height * i, e.style.width = `${t.width}px`, e.style.height = `${t.height}px`;
const s = e.getContext("2d");
return s ? (s.scale(i, i), s.drawImage(
this.htmlImg,
t.x,
t.y,
t.width,
t.height,
0,
0,
t.width,
t.height
), e) : null;
}
/**
* Changes the image src.
* @param {String} src
*/
setImage(t) {
t && t.length !== 0 && (this.firstInit = !1, this.htmlImg.src = t);
}
/**
* Resets the crop region to the initial settings.
*/
reset() {
try {
this.firstInit = !1, this.destroy(), this.initializeCropper();
} catch (t) {
if (t instanceof v || t instanceof d || t instanceof x)
this.onErrorCallback(t);
else
throw t;
}
}
/**
* Destroy the TrueCropper instance and replace with the original element.
*/
destroy() {
this.isDomCreated && (this.observer.unobserve(this.htmlImg), this.newSelection.destroy(), this.handles.destroy(), this.selection.destroy(), this.background.destroy(), this.replaceDOM && this.htmlContainer.parentElement && this.htmlContainer.parentElement.replaceChild(
this.htmlImg,
this.htmlContainer
)), this.isDomCreated = !1;
}
/**
* Moves the crop region to a specified coordinate.
* @param {TrueCropperCoordinates} coordinates
*/
moveTo(t, e = void 0) {
if (typeof t != "object" || !t || typeof t.x != "number" || typeof t.y != "number")
return;
const i = this.coordinatesToReal(t, e);
this.box.move(i), this.redraw(), this.onCropEndCallback();
}
/**
* Resizes the crop region to a specified width and height.
* @param {SiTrueCropperSizeze} size
* @param {TrueCropperPoints} points
*/
resizeTo(t, e = { x: 0.5, y: 0.5 }, i = void 0) {
if (typeof t != "object")
return { ok: !1, message: "Size must be provided as an Size object." };
if (!t || typeof t.width != "number" || typeof t.height != "number")
return { ok: !1, message: "Size object must have numeric 'width' and 'height' properties." };
if (typeof e != "object")
return { ok: !1, message: "Points must be provided as an Points object." };
if (!e || typeof e.x != "number" || typeof e.y != "number")
return { ok: !1, message: "Points object must have numeric 'x' and 'y' properties." };
const s = this.sizeToReal(t, i);
this.box.resize(s, e), this.redraw(), this.onCropEndCallback();
}
/**
* Scale the crop region by a factor.
* @param {Number} factor
* @param {TrueCropperPoints} points
*/
scaleBy(t, e = { x: 0.5, y: 0.5 }) {
if (typeof t != "number")
return { ok: !1, message: "factor must be provided as numeric." };
const i = this.box.scale(t, e);
return i.ok && (this.redraw(), this.onCropEndCallback()), i;
}
/**
* Sets the value of a box.
* @param {TrueCropperBoxProps} box - The box object containing properties to set.
* @public
*/
setValue(t, e = void 0) {
if (typeof t != "object")
return { ok: !1, message: "Size must be provided as an BoxProps object." };
if (!t || typeof t.x != "number" || typeof t.y != "number" || typeof t.width != "number" || typeof t.height != "number")
return { ok: !1, message: "BoxProps object must have numeric 'x', 'y', 'width' and 'height' properties." };
const i = this.boxToReal(t, e), s = this.box.setValue(i);
return s.ok && (this.redraw(), this.onCropEndCallback()), s;
}
/**
* Get the value of the crop region.
* @param {TrueCropperSizeUnit | undefined} mode - The mode of return value type. If null, defaults to the return mode set in returnMode options.
* @returns {number} - The value of the crop region.
*/
getValue(t = void 0) {
const e = t || this.options.returnMode, s = e === "relative" ? this.box.getValueRelative(this.ratio) : e === "percent" ? this.box.getValuePercent() : this.box.getValueReal();
return {
x: Math.round(s.x),
y: Math.round(s.y),
width: Math.round(s.width),
height: Math.round(s.height)
};
}
/**
* Retrieves the image properties.
* @returns {real: TrueCropperSize, relative: TrueCropperSize} An object containing the real and relative properties.
* @public
*/
getImageProps() {
return { real: this.real, relative: this.relative };
}
/**
* Retrieves the status of the instance.
* @returns {TrueCropperStatus} The status of the instance.
*/
getStatus() {
return this.status;
}
/**
* Handles the callback when after initialization.
*/
onInitializeCallback() {
this.callbacks.onInitialize && this.callbacks.onInitialize(this, this.getValue());
}
/**
* Handles the callback when cropping starts.
*/
onCropStartCallback() {
this.callbacks.onCropStart && this.callbacks.onCropStart(this, this.getValue());
}
/**
* Handles the callback when cropping is in progress.
*/
onCropChangeCallback() {
this.callbacks.onCropChange && this.callbacks.onCropChange(this, this.getValue());
}
/**
* Handles the callback when cropping ends.
*/
onCropEndCallback() {
const t = this.getValue();
this.setDatasetCropValues(t), this.callbacks.onCropEnd && this.callbacks.onCropEnd(this, t);
}
/**
* Handles errors encountered during operations.
* @param {TrueCropperHtmlError | TrueCropperImageError | TrueCropperOptionsError} error - The error object containing information about the error.
*/
onErrorCallback(t) {
this.changeStatus(y.Error);
const e = {
name: t.name,
message: t.message,
messageId: t.messageId,
data: t.data
};
if (this.destroy(), this.callbacks.onError)
this.callbacks.onError(this, e);
else
throw t;
}
/** ==============
*
*
* Private methods
*
*
* ==============
*/
initializeObserver() {
this.observer = new ResizeObserver((t) => {
for (const e of t) {
const i = e.target;
i === this.htmlImg && i.complete && i.width !== 0 && (this.updateRelativeSize(), this.redraw());
}
});
}
initializeCropper() {
this.initializeObserver(), this.htmlImg.src && this.htmlImg.complete && this.htmlImg.width !== 0 && this.htmlImg.height !== 0 && (this.preventDoubleLoad = this.htmlImg.src, this.initialize()), this.htmlImg.onload = () => {
!this.htmlImg.src || this.preventDoubleLoad === this.htmlImg.src || (this.preventDoubleLoad = void 0, this.changeStatus(
this.status === y.Waiting ? y.Waiting : y.Reloading
), this.observer.unobserve(this.htmlImg), this.initialize());
};
}
initialize() {
try {
this.createDOM(), this.calcContainerProps(), this.updateRelativeSize(), this.createNewBox(), this.onInitializeCallback(), this.observer.observe(this.htmlImg), this.changeStatus(y.Ready), this.onCropEndCallback();
} catch (t) {
if (t instanceof x)
this.onErrorCallback(t);
else
throw t;
}
}
createDOM() {
if (this.isDomCreated)
return;
this.replaceDOM && (this.htmlContainer = document.createElement("div"), this.htmlContainer.classList.add(u.base), this.htmlImg.parentElement && this.htmlImg.parentElement.replaceChild(
this.htmlContainer,
this.htmlImg
), this.htmlContainer.appendChild(this.htmlImg));
const t = this.htmlContainer;
st(t), this.htmlImg.classList.add(u.img), this.background = new ht(t, u.background), this.newSelection = new ot(
t,
u.new,
this.eventBus,
this.options.allowNewSelection
), this.selection = new rt(
t,
u.selection,
this.eventBus,
this.options.allowMove
), this.handles = new wt(
t,
u.hanleds,
this.eventBus,
this.options.allowResize,
u.handle
), this.isDomCreated = !0;
}
calcContainerProps() {
this.real = {
width: this.htmlImg.naturalWidth,
height: this.htmlImg.naturalHeight
};
}
createNewBox() {
let t = this.options.startSize;
this.firstInit && (this.firstInit = !1, t = this.options.firstInitSize);
const e = {
x: t.centeredX,
y: t.centeredX
}, i = t.allowChange, s = G(
t,
this.options.minSize,
this.options.maxSize,
this.real,
this.ratio
), h = J(
s,
this.real,
this.options.aspectRatio,
this.options.epsilon,
i,
e
);
K(h), this.box = new it(h);
}
updateRelativeSize() {
const { width: t, height: e } = this.htmlImg.getBoundingClientRect();
this.htmlImg.offsetWidth === 0 || this.htmlImg.offsetHeight === 0 ? this.relative = { wid