@wiajs/ui
Version:
wia app ui packages
464 lines (463 loc) • 16.6 kB
JavaScript
import { CLASS_CROP, CLASS_DISABLED, CLASS_HIDDEN, CLASS_MODAL, CLASS_MOVE, DATA_ACTION, DRAG_MODE_CROP, DRAG_MODE_MOVE, DRAG_MODE_NONE, NAMESPACE } from './constant';
import { getSize, getSourceCanvas, isUndefined, normalizeDecimalNumber } from './util';
export default {
// Show the crop box manually
crop () {
if (this.ready && !this.cropped && !this.disabled) {
this.cropped = true;
this.limitCropBox(true, true);
if (this.opt.modal) this.dragBox.addClass(CLASS_MODAL);
this.cropBox.removeClass(CLASS_HIDDEN);
this.setCropBoxData(this.initialCropBoxData);
}
return this;
},
// Reset the image and crop box to their initial states
reset () {
if (this.ready && !this.disabled) {
this.imageData = $.assign({}, this.initialImageData);
this.canvasData = $.assign({}, this.initialCanvasData);
this.cropBoxData = $.assign({}, this.initialCropBoxData);
this.renderCanvas();
if (this.cropped) {
this.renderCropBox();
}
}
return this;
},
// Clear the crop box
clear () {
if (this.cropped && !this.disabled) {
$.assign(this.cropBoxData, {
left: 0,
top: 0,
width: 0,
height: 0
});
this.cropped = false;
this.renderCropBox();
this.limitCanvas(true, true);
// Render canvas after crop box rendered
this.renderCanvas();
this.dragBox.removeClass(CLASS_MODAL);
this.cropBox.addClass(CLASS_HIDDEN);
}
return this;
},
/**
* Replace the image's src and rebuild the cropper
* @param {string} url - The new URL.
* @param {boolean} [hasSameSize] - Indicate if the new image has the same size as the old one.
* @returns {Cropper} this
*/ replace (url, hasSameSize = false) {
if (!this.disabled && url) {
if (this.isImg) {
this.img.src = url;
}
if (hasSameSize) {
this.url = url;
this.image.src = url;
if (this.ready) {
this.viewBoxImage.src = url;
$.forEach(this.previews, (n)=>{
n.getElementsByTagName('img')[0].src = url;
});
}
} else {
if (this.isImg) {
this.replaced = true;
}
this.opt.data = null;
this.uncreate();
this.load(url);
}
}
return this;
},
// Enable (unfreeze) the cropper
enable () {
if (this.ready && this.disabled) {
this.disabled = false;
this.cropper.removeClass(CLASS_DISABLED);
}
return this;
},
// Disable (freeze) the cropper
disable () {
if (this.ready && !this.disabled) {
this.disabled = true;
this.cropper.addClass(CLASS_DISABLED);
}
return this;
},
/**
* Destroy the cropper and remove the instance from the image
* @returns {Cropper} this
*/ destroy () {
const { img } = this;
if (!img[NAMESPACE]) {
return this;
}
img[NAMESPACE] = undefined;
if (this.isImg && this.replaced) {
img.src = this.originalUrl;
}
this.uncreate();
return this;
},
/**
* Move the canvas with relative offsets
* @param {number} offsetX - The relative offset distance on the x-axis.
* @param {number} [offsetY=offsetX] - The relative offset distance on the y-axis.
* @returns {Cropper} this
*/ move (offsetX, offsetY = offsetX) {
const { left, top } = this.canvasData;
return this.moveTo(isUndefined(offsetX) ? offsetX : left + Number(offsetX), isUndefined(offsetY) ? offsetY : top + Number(offsetY));
},
/**
* Move the canvas to an absolute point
* @param {number} x - The x-axis coordinate.
* @param {number} [y=x] - The y-axis coordinate.
* @returns {Cropper} this
*/ moveTo (x, y = x) {
const { canvasData } = this;
let changed = false;
x = Number(x);
y = Number(y);
if (this.ready && !this.disabled && this.opt.movable) {
if ($.isNumber(x)) {
canvasData.left = x;
changed = true;
}
if ($.isNumber(y)) {
canvasData.top = y;
changed = true;
}
if (changed) {
this.renderCanvas(true);
}
}
return this;
},
/**
* Get the cropped area position and size data (base on the original image)
* @param {boolean} [rounded=false] - Indicate if round the data values or not.
* @returns {Object} The result cropped data.
*/ getData (rounded = false) {
const { opt, imageData, canvasData, cropBoxData } = this;
let data = {};
if (this.ready && this.cropped) {
data = {
x: cropBoxData.left - canvasData.left,
y: cropBoxData.top - canvasData.top,
width: cropBoxData.width,
height: cropBoxData.height
};
const ratio = imageData.width / imageData.naturalWidth;
$.forEach(data, (n, i)=>{
data[i] = n / ratio;
});
data.ratio = ratio;
if (rounded) {
// In case rounding off leads to extra 1px in right or bottom border
// we should round the top-left corner and the dimension (#343).
const bottom = Math.round(data.y + data.height);
const right = Math.round(data.x + data.width);
data.x = Math.round(data.x);
data.y = Math.round(data.y);
data.width = right - data.x;
data.height = bottom - data.y;
}
} else {
data = {
x: 0,
y: 0,
width: 0,
height: 0
};
}
return data;
},
/**
* Set the cropped area position and size with new data
* @param {Object} data - The new data.
* @returns {Cropper} this
*/ setData (data) {
const { opt, imageData, canvasData } = this;
const cropBoxData = {};
if (this.ready && !this.disabled && $.isPlainObject(data)) {
const ratio = imageData.width / imageData.naturalWidth;
if ($.isNumber(data.x)) {
cropBoxData.left = data.x * ratio + canvasData.left;
}
if ($.isNumber(data.y)) {
cropBoxData.top = data.y * ratio + canvasData.top;
}
if ($.isNumber(data.width)) {
cropBoxData.width = data.width * ratio;
}
if ($.isNumber(data.height)) {
cropBoxData.height = data.height * ratio;
}
this.setCropBoxData(cropBoxData);
}
return this;
},
/**
* Get the container size data.
* @returns {Object} The result container data.
*/ getContainerData () {
return this.ready ? $.assign({}, this.containerData) : {};
},
/**
* Get the image position and size data.
* @returns {Object} The result image data.
*/ getImageData () {
return this.sized ? $.assign({}, this.imageData) : {};
},
/**
* Get the canvas position and size data.
* @returns {Object} The result canvas data.
*/ getCanvasData () {
const { canvasData } = this;
const data = {};
if (this.ready) {
$.forEach([
'left',
'top',
'width',
'height',
'naturalWidth',
'naturalHeight'
], (n)=>{
data[n] = canvasData[n];
});
}
return data;
},
/**
* Set the canvas position and size with new data.
* @param {Object} data - The new canvas data.
* @returns {Cropper} this
*/ setCanvasData (data) {
const { canvasData } = this;
const { aspectRatio } = canvasData;
if (this.ready && !this.disabled && $.isPlainObject(data)) {
if ($.isNumber(data.left)) {
canvasData.left = data.left;
}
if ($.isNumber(data.top)) {
canvasData.top = data.top;
}
if ($.isNumber(data.width)) {
canvasData.width = data.width;
canvasData.height = data.width / aspectRatio;
} else if ($.isNumber(data.height)) {
canvasData.height = data.height;
canvasData.width = data.height * aspectRatio;
}
this.renderCanvas(true);
}
return this;
},
/**
* Get the crop box position and size data.
* @returns {Object} The result crop box data.
*/ getCropBoxData () {
const { cropBoxData } = this;
let data;
if (this.ready && this.cropped) {
data = {
left: cropBoxData.left,
top: cropBoxData.top,
width: cropBoxData.width,
height: cropBoxData.height
};
}
return data || {};
},
/**
* Set the crop box position and size with new data.
* @param {Object} data - The new crop box data.
* @returns {Cropper} this
*/ setCropBoxData (data) {
const { cropBoxData } = this;
const { aspectRatio } = this.opt;
let widthChanged;
let heightChanged;
if (this.ready && this.cropped && !this.disabled && $.isPlainObject(data)) {
if ($.isNumber(data.left)) {
cropBoxData.left = data.left;
}
if ($.isNumber(data.top)) {
cropBoxData.top = data.top;
}
if ($.isNumber(data.width) && data.width !== cropBoxData.width) {
widthChanged = true;
cropBoxData.width = data.width;
}
if ($.isNumber(data.height) && data.height !== cropBoxData.height) {
heightChanged = true;
cropBoxData.height = data.height;
}
if (aspectRatio) {
if (widthChanged) {
cropBoxData.height = cropBoxData.width / aspectRatio;
} else if (heightChanged) {
cropBoxData.width = cropBoxData.height * aspectRatio;
}
}
this.renderCropBox();
}
return this;
},
/**
* Get a canvas drawn the cropped image.
* @param {Object} [opt={}] - The config opt.
* @returns {HTMLCanvasElement} - The result canvas.
*/ getCroppedCanvas (opt = {}) {
if (!this.ready || !window.HTMLCanvasElement) {
return null;
}
const { canvasData } = this;
const source = getSourceCanvas(this.image, this.imageData, canvasData, opt);
// Returns the source canvas if it is not cropped.
if (!this.cropped) {
return source;
}
let { x: initialX, y: initialY, width: initialWidth, height: initialHeight } = this.getData();
const ratio = source.width / Math.floor(canvasData.naturalWidth);
if (ratio !== 1) {
initialX *= ratio;
initialY *= ratio;
initialWidth *= ratio;
initialHeight *= ratio;
}
const aspect = initialWidth / initialHeight;
const maxSizes = getSize({
aspect,
width: opt.maxWidth || Infinity,
height: opt.maxHeight || Infinity
});
const minSizes = getSize({
aspect,
width: opt.minWidth || 0,
height: opt.minHeight || 0
}, 'cover');
let { width, height } = getSize({
aspect,
width: opt.width || (ratio !== 1 ? source.width : initialWidth),
height: opt.height || (ratio !== 1 ? source.height : initialHeight)
});
width = Math.min(maxSizes.width, Math.max(minSizes.width, width));
height = Math.min(maxSizes.height, Math.max(minSizes.height, height));
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = normalizeDecimalNumber(width);
canvas.height = normalizeDecimalNumber(height);
context.fillStyle = opt.fillColor || 'transparent';
context.fillRect(0, 0, width, height);
const { imageSmoothingEnabled = true, imageSmoothingQuality } = opt;
context.imageSmoothingEnabled = imageSmoothingEnabled;
if (imageSmoothingQuality) {
context.imageSmoothingQuality = imageSmoothingQuality;
}
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.drawImage
const sourceWidth = source.width;
const sourceHeight = source.height;
// Source canvas parameters
let srcX = initialX;
let srcY = initialY;
let srcWidth;
let srcHeight;
// Destination canvas parameters
let dstX;
let dstY;
let dstWidth;
let dstHeight;
if (srcX <= -initialWidth || srcX > sourceWidth) {
srcX = 0;
srcWidth = 0;
dstX = 0;
dstWidth = 0;
} else if (srcX <= 0) {
dstX = -srcX;
srcX = 0;
srcWidth = Math.min(sourceWidth, initialWidth + srcX);
dstWidth = srcWidth;
} else if (srcX <= sourceWidth) {
dstX = 0;
srcWidth = Math.min(initialWidth, sourceWidth - srcX);
dstWidth = srcWidth;
}
if (srcWidth <= 0 || srcY <= -initialHeight || srcY > sourceHeight) {
srcY = 0;
srcHeight = 0;
dstY = 0;
dstHeight = 0;
} else if (srcY <= 0) {
dstY = -srcY;
srcY = 0;
srcHeight = Math.min(sourceHeight, initialHeight + srcY);
dstHeight = srcHeight;
} else if (srcY <= sourceHeight) {
dstY = 0;
srcHeight = Math.min(initialHeight, sourceHeight - srcY);
dstHeight = srcHeight;
}
const params = [
srcX,
srcY,
srcWidth,
srcHeight
];
// Avoid "IndexSizeError"
if (dstWidth > 0 && dstHeight > 0) {
const scale = width / initialWidth;
params.push(dstX * scale, dstY * scale, dstWidth * scale, dstHeight * scale);
}
// All the numerical parameters should be integer for `drawImage`
// https://github.com/fengyuanchen/cropper/issues/476
context.drawImage(source, ...params.map((param)=>Math.floor(normalizeDecimalNumber(param))));
return canvas;
},
/**
* Change the aspect ratio of the crop box.
* @param {number} aspectRatio - The new aspect ratio.
* @returns {Cropper} this
*/ setAspectRatio (aspectRatio) {
const { opt } = this;
if (!this.disabled && !isUndefined(aspectRatio)) {
// 0 -> NaN
opt.aspectRatio = Math.max(0, aspectRatio) || NaN;
if (this.ready) {
this.initCropBox();
if (this.cropped) {
this.renderCropBox();
}
}
}
return this;
},
/**
* Change the drag mode.
* @param {string} mode - The new drag mode.
* @returns {Cropper} this
*/ setDragMode (mode) {
const { opt, dragBox, face } = this;
if (this.ready && !this.disabled) {
const croppable = mode === DRAG_MODE_CROP;
const movable = opt.movable && mode === DRAG_MODE_MOVE;
mode = croppable || movable ? mode : DRAG_MODE_NONE;
opt.dragMode = mode;
dragBox.data(DATA_ACTION, mode);
dragBox.toggleClass(CLASS_CROP, croppable).toggleClass(CLASS_MOVE, movable);
if (!opt.cropBoxMovable) {
// Sync drag mode to crop box when it is not movable
face.data(DATA_ACTION, mode);
face.toggleClass(CLASS_CROP, croppable).toggleClass(CLASS_MOVE, movable);
}
}
return this;
}
};