@cropper/element-image
Version:
A custom image element for the Cropper.
561 lines (554 loc) • 24.9 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@cropper/element'), require('@cropper/utils')) :
typeof define === 'function' && define.amd ? define(['@cropper/element', '@cropper/utils'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.CropperImage = factory(global.CropperElement, global.CropperUtils));
})(this, (function (CropperElement, utils) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var CropperElement__default = /*#__PURE__*/_interopDefaultLegacy(CropperElement);
var style = `:host{display:inline-block}img{display:block;height:100%;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}`;
const canvasCache = new WeakMap();
const NATIVE_ATTRIBUTES = [
'alt',
'crossorigin',
'decoding',
'importance',
'loading',
'referrerpolicy',
'sizes',
'src',
'srcset',
];
class CropperImage extends CropperElement__default["default"] {
constructor() {
super(...arguments);
this.$matrix = [1, 0, 0, 1, 0, 0];
this.$onLoad = null;
this.$onCanvasAction = null;
this.$onCanvasActionEnd = null;
this.$onCanvasActionStart = null;
this.$actionStartTarget = null;
this.$style = style;
this.$image = new Image();
this.initialCenterSize = 'contain';
this.rotatable = false;
this.scalable = false;
this.skewable = false;
this.slottable = false;
this.translatable = false;
}
set $canvas(element) {
canvasCache.set(this, element);
}
get $canvas() {
return canvasCache.get(this);
}
static get observedAttributes() {
return super.observedAttributes.concat(NATIVE_ATTRIBUTES, [
'initial-center-size',
'rotatable',
'scalable',
'skewable',
'translatable',
]);
}
attributeChangedCallback(name, oldValue, newValue) {
if (Object.is(newValue, oldValue)) {
return;
}
super.attributeChangedCallback(name, oldValue, newValue);
// Inherits the native attributes
if (NATIVE_ATTRIBUTES.includes(name)) {
this.$image.setAttribute(name, newValue);
}
}
$propertyChangedCallback(name, oldValue, newValue) {
if (Object.is(newValue, oldValue)) {
return;
}
super.$propertyChangedCallback(name, oldValue, newValue);
switch (name) {
case 'initialCenterSize':
this.$nextTick(() => {
this.$center(newValue);
});
break;
}
}
connectedCallback() {
super.connectedCallback();
const { $image } = this;
const $canvas = this.closest(this.$getTagNameOf(utils.CROPPER_CANVAS));
if ($canvas) {
this.$canvas = $canvas;
this.$setStyles({
// Make it a block element to avoid side effects (#1074).
display: 'block',
position: 'absolute',
});
this.$onCanvasActionStart = (event) => {
var _a, _b;
this.$actionStartTarget = (_b = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.relatedEvent) === null || _b === void 0 ? void 0 : _b.target;
};
this.$onCanvasActionEnd = () => {
this.$actionStartTarget = null;
};
this.$onCanvasAction = this.$handleAction.bind(this);
utils.on($canvas, utils.EVENT_ACTION_START, this.$onCanvasActionStart);
utils.on($canvas, utils.EVENT_ACTION_END, this.$onCanvasActionEnd);
utils.on($canvas, utils.EVENT_ACTION, this.$onCanvasAction);
}
this.$onLoad = this.$handleLoad.bind(this);
utils.on($image, utils.EVENT_LOAD, this.$onLoad);
this.$getShadowRoot().appendChild($image);
}
disconnectedCallback() {
const { $image, $canvas } = this;
if ($canvas) {
if (this.$onCanvasActionStart) {
utils.off($canvas, utils.EVENT_ACTION_START, this.$onCanvasActionStart);
this.$onCanvasActionStart = null;
}
if (this.$onCanvasActionEnd) {
utils.off($canvas, utils.EVENT_ACTION_END, this.$onCanvasActionEnd);
this.$onCanvasActionEnd = null;
}
if (this.$onCanvasAction) {
utils.off($canvas, utils.EVENT_ACTION, this.$onCanvasAction);
this.$onCanvasAction = null;
}
}
if ($image && this.$onLoad) {
utils.off($image, utils.EVENT_LOAD, this.$onLoad);
this.$onLoad = null;
}
this.$getShadowRoot().removeChild($image);
super.disconnectedCallback();
}
$handleLoad() {
const { $image } = this;
this.$setStyles({
width: $image.naturalWidth,
height: $image.naturalHeight,
});
if (this.$canvas) {
this.$center(this.initialCenterSize);
}
}
$handleAction(event) {
if (this.hidden || !(this.rotatable || this.scalable || this.translatable)) {
return;
}
const { $canvas } = this;
const { detail } = event;
if (detail) {
const { relatedEvent } = detail;
let { action } = detail;
if (action === utils.ACTION_TRANSFORM && (!this.rotatable || !this.scalable)) {
if (this.rotatable) {
action = utils.ACTION_ROTATE;
}
else if (this.scalable) {
action = utils.ACTION_SCALE;
}
else {
action = utils.ACTION_NONE;
}
}
switch (action) {
case utils.ACTION_MOVE:
if (this.translatable) {
let $selection = null;
if (relatedEvent) {
$selection = relatedEvent.target.closest(this.$getTagNameOf(utils.CROPPER_SELECTION));
}
if (!$selection) {
$selection = $canvas.querySelector(this.$getTagNameOf(utils.CROPPER_SELECTION));
}
if ($selection && $selection.multiple && !$selection.active) {
$selection = $canvas.querySelector(`${this.$getTagNameOf(utils.CROPPER_SELECTION)}[active]`);
}
if (!$selection || $selection.hidden || !$selection.movable || $selection.dynamic
|| !(this.$actionStartTarget && $selection.contains(this.$actionStartTarget))) {
this.$move(detail.endX - detail.startX, detail.endY - detail.startY);
}
}
break;
case utils.ACTION_ROTATE:
if (this.rotatable) {
if (relatedEvent) {
const { x, y } = this.getBoundingClientRect();
this.$rotate(detail.rotate, relatedEvent.clientX - x, relatedEvent.clientY - y);
}
else {
this.$rotate(detail.rotate);
}
}
break;
case utils.ACTION_SCALE:
if (this.scalable) {
if (relatedEvent) {
const $selection = relatedEvent.target.closest(this.$getTagNameOf(utils.CROPPER_SELECTION));
if (!$selection
|| !$selection.zoomable
|| ($selection.zoomable && $selection.dynamic)) {
const { x, y } = this.getBoundingClientRect();
this.$zoom(detail.scale, relatedEvent.clientX - x, relatedEvent.clientY - y);
}
}
else {
this.$zoom(detail.scale);
}
}
break;
case utils.ACTION_TRANSFORM:
if (this.rotatable && this.scalable) {
const { rotate } = detail;
let { scale } = detail;
if (scale < 0) {
scale = 1 / (1 - scale);
}
else {
scale += 1;
}
const cos = Math.cos(rotate);
const sin = Math.sin(rotate);
const [scaleX, skewY, skewX, scaleY] = [
cos * scale,
sin * scale,
-sin * scale,
cos * scale,
];
if (relatedEvent) {
const clientRect = this.getBoundingClientRect();
const x = relatedEvent.clientX - clientRect.x;
const y = relatedEvent.clientY - clientRect.y;
const [a, b, c, d] = this.$matrix;
const originX = clientRect.width / 2;
const originY = clientRect.height / 2;
const moveX = x - originX;
const moveY = y - originY;
const translateX = ((moveX * d) - (c * moveY)) / ((a * d) - (c * b));
const translateY = ((moveY * a) - (b * moveX)) / ((a * d) - (c * b));
/**
* Equals to
* this.$rotate(rotate, x, y);
* this.$scale(scale, x, y);
*/
this.$transform(scaleX, skewY, skewX, scaleY, translateX * (1 - scaleX) + translateY * skewX, translateY * (1 - scaleY) + translateX * skewY);
}
else {
/**
* Equals to
* this.$rotate(rotate);
* this.$scale(scale);
*/
this.$transform(scaleX, skewY, skewX, scaleY, 0, 0);
}
}
break;
}
}
}
/**
* Defers the callback to execute after successfully loading the image.
* @param {Function} [callback] The callback to execute after successfully loading the image.
* @returns {Promise} Returns a promise that resolves to the image element.
*/
$ready(callback) {
const { $image } = this;
const promise = new Promise((resolve, reject) => {
const error = new Error('Failed to load the image source');
if ($image.complete) {
if ($image.naturalWidth > 0 && $image.naturalHeight > 0) {
resolve($image);
}
else {
reject(error);
}
}
else {
const onLoad = () => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
utils.off($image, utils.EVENT_ERROR, onError);
resolve($image);
};
const onError = () => {
utils.off($image, utils.EVENT_LOAD, onLoad);
reject(error);
};
utils.once($image, utils.EVENT_LOAD, onLoad);
utils.once($image, utils.EVENT_ERROR, onError);
}
});
if (utils.isFunction(callback)) {
promise.then((image) => {
callback(image);
return image;
});
}
return promise;
}
/**
* Aligns the image to the center of its parent element.
* @param {string} [size] The size of the image.
* @returns {CropperImage} Returns `this` for chaining.
*/
$center(size) {
const { parentElement } = this;
if (!parentElement) {
return this;
}
const container = parentElement.getBoundingClientRect();
const containerWidth = container.width;
const containerHeight = container.height;
const { x, y, width, height, } = this.getBoundingClientRect();
const startX = x + (width / 2);
const startY = y + (height / 2);
const endX = container.x + (containerWidth / 2);
const endY = container.y + (containerHeight / 2);
this.$move(endX - startX, endY - startY);
if (size && (width !== containerWidth || height !== containerHeight)) {
const scaleX = containerWidth / width;
const scaleY = containerHeight / height;
switch (size) {
case 'cover':
this.$scale(Math.max(scaleX, scaleY));
break;
case 'contain':
this.$scale(Math.min(scaleX, scaleY));
break;
}
}
return this;
}
/**
* Moves the image.
* @param {number} x The moving distance in the horizontal direction.
* @param {number} [y] The moving distance in the vertical direction.
* @returns {CropperImage} Returns `this` for chaining.
*/
$move(x, y = x) {
if (this.translatable && utils.isNumber(x) && utils.isNumber(y)) {
const [a, b, c, d] = this.$matrix;
const e = ((x * d) - (c * y)) / ((a * d) - (c * b));
const f = ((y * a) - (b * x)) / ((a * d) - (c * b));
this.$translate(e, f);
}
return this;
}
/**
* Moves the image to a specific position.
* @param {number} x The new position in the horizontal direction.
* @param {number} [y] The new position in the vertical direction.
* @returns {CropperImage} Returns `this` for chaining.
*/
$moveTo(x, y = x) {
if (this.translatable && utils.isNumber(x) && utils.isNumber(y)) {
const [a, b, c, d] = this.$matrix;
const e = ((x * d) - (c * y)) / ((a * d) - (c * b));
const f = ((y * a) - (b * x)) / ((a * d) - (c * b));
this.$setTransform(a, b, c, d, e, f);
}
return this;
}
/**
* Rotates the image.
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/rotate}
* {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/rotate}
* @param {number|string} angle The rotation angle (in radians).
* @param {number} [x] The rotation origin in the horizontal, defaults to the center of the image.
* @param {number} [y] The rotation origin in the vertical, defaults to the center of the image.
* @returns {CropperImage} Returns `this` for chaining.
*/
$rotate(angle, x, y) {
if (this.rotatable) {
const radian = utils.toAngleInRadian(angle);
const cos = Math.cos(radian);
const sin = Math.sin(radian);
const [scaleX, skewY, skewX, scaleY] = [cos, sin, -sin, cos];
if (utils.isNumber(x) && utils.isNumber(y)) {
const [a, b, c, d] = this.$matrix;
const { width, height } = this.getBoundingClientRect();
const originX = width / 2;
const originY = height / 2;
const moveX = x - originX;
const moveY = y - originY;
const translateX = ((moveX * d) - (c * moveY)) / ((a * d) - (c * b));
const translateY = ((moveY * a) - (b * moveX)) / ((a * d) - (c * b));
/**
* Equals to
* this.$translate(translateX, translateX);
* this.$rotate(angle);
* this.$translate(-translateX, -translateX);
*/
this.$transform(scaleX, skewY, skewX, scaleY, translateX * (1 - scaleX) - translateY * skewX, translateY * (1 - scaleY) - translateX * skewY);
}
else {
this.$transform(scaleX, skewY, skewX, scaleY, 0, 0);
}
}
return this;
}
/**
* Zooms the image.
* @param {number} scale The zoom factor. Positive numbers for zooming in, and negative numbers for zooming out.
* @param {number} [x] The zoom origin in the horizontal, defaults to the center of the image.
* @param {number} [y] The zoom origin in the vertical, defaults to the center of the image.
* @returns {CropperImage} Returns `this` for chaining.
*/
$zoom(scale, x, y) {
if (!this.scalable || scale === 0) {
return this;
}
if (scale < 0) {
scale = 1 / (1 - scale);
}
else {
scale += 1;
}
if (utils.isNumber(x) && utils.isNumber(y)) {
const [a, b, c, d] = this.$matrix;
const { width, height } = this.getBoundingClientRect();
const originX = width / 2;
const originY = height / 2;
const moveX = x - originX;
const moveY = y - originY;
const translateX = ((moveX * d) - (c * moveY)) / ((a * d) - (c * b));
const translateY = ((moveY * a) - (b * moveX)) / ((a * d) - (c * b));
/**
* Equals to
* this.$translate(translateX, translateX);
* this.$scale(scale);
* this.$translate(-translateX, -translateX);
*/
this.$transform(scale, 0, 0, scale, translateX * (1 - scale), translateY * (1 - scale));
}
else {
this.$scale(scale);
}
return this;
}
/**
* Scales the image.
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/scale}
* {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/scale}
* @param {number} x The scaling factor in the horizontal direction.
* @param {number} [y] The scaling factor in the vertical direction.
* @returns {CropperImage} Returns `this` for chaining.
*/
$scale(x, y = x) {
if (this.scalable) {
this.$transform(x, 0, 0, y, 0, 0);
}
return this;
}
/**
* Skews the image.
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/skew}
* {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/transform}
* @param {number|string} x The skewing angle in the horizontal direction.
* @param {number|string} [y] The skewing angle in the vertical direction.
* @returns {CropperImage} Returns `this` for chaining.
*/
$skew(x, y = 0) {
if (this.skewable) {
const radianX = utils.toAngleInRadian(x);
const radianY = utils.toAngleInRadian(y);
this.$transform(1, Math.tan(radianY), Math.tan(radianX), 1, 0, 0);
}
return this;
}
/**
* Translates the image.
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/translate}
* {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/translate}
* @param {number} x The translating distance in the horizontal direction.
* @param {number} [y] The translating distance in the vertical direction.
* @returns {CropperImage} Returns `this` for chaining.
*/
$translate(x, y = x) {
if (this.translatable && utils.isNumber(x) && utils.isNumber(y)) {
this.$transform(1, 0, 0, 1, x, y);
}
return this;
}
/**
* Transforms the image.
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix}
* {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/transform}
* @param {number} a The scaling factor in the horizontal direction.
* @param {number} b The skewing angle in the vertical direction.
* @param {number} c The skewing angle in the horizontal direction.
* @param {number} d The scaling factor in the vertical direction.
* @param {number} e The translating distance in the horizontal direction.
* @param {number} f The translating distance in the vertical direction.
* @returns {CropperImage} Returns `this` for chaining.
*/
$transform(a, b, c, d, e, f) {
if (utils.isNumber(a)
&& utils.isNumber(b)
&& utils.isNumber(c)
&& utils.isNumber(d)
&& utils.isNumber(e)
&& utils.isNumber(f)) {
return this.$setTransform(utils.multiplyMatrices(this.$matrix, [a, b, c, d, e, f]));
}
return this;
}
/**
* Resets (overrides) the current transform to the specific identity matrix.
* {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setTransform}
* @param {number|Array} a The scaling factor in the horizontal direction.
* @param {number} b The skewing angle in the vertical direction.
* @param {number} c The skewing angle in the horizontal direction.
* @param {number} d The scaling factor in the vertical direction.
* @param {number} e The translating distance in the horizontal direction.
* @param {number} f The translating distance in the vertical direction.
* @returns {CropperImage} Returns `this` for chaining.
*/
$setTransform(a, b, c, d, e, f) {
if (this.rotatable || this.scalable || this.skewable || this.translatable) {
if (Array.isArray(a)) {
[a, b, c, d, e, f] = a;
}
if (utils.isNumber(a)
&& utils.isNumber(b)
&& utils.isNumber(c)
&& utils.isNumber(d)
&& utils.isNumber(e)
&& utils.isNumber(f)) {
const oldMatrix = [...this.$matrix];
const newMatrix = [a, b, c, d, e, f];
if (this.$emit(utils.EVENT_TRANSFORM, {
matrix: newMatrix,
oldMatrix,
}) === false) {
return this;
}
this.$matrix = newMatrix;
this.style.transform = `matrix(${newMatrix.join(', ')})`;
}
}
return this;
}
/**
* Retrieves the current transformation matrix being applied to the element.
* {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getTransform}
* @returns {Array} Returns the readonly transformation matrix.
*/
$getTransform() {
return this.$matrix.slice();
}
/**
* Resets the current transform to the initial identity matrix.
* {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/resetTransform}
* @returns {CropperImage} Returns `this` for chaining.
*/
$resetTransform() {
return this.$setTransform([1, 0, 0, 1, 0, 0]);
}
}
CropperImage.$name = utils.CROPPER_IMAGE;
CropperImage.$version = '2.0.0';
return CropperImage;
}));