@wiajs/ui
Version:
wia app ui packages
403 lines (402 loc) • 15.8 kB
JavaScript
/** @jsx-x jsx */ /** @jsxImportSource @wiajs/core */ import { jsx as _jsx, jsxs as _jsxs } from "@wiajs/core/jsx-runtime";
import { Event } from '@wiajs/core';
import { getExif, getRotate, urlToBuf, bufToUrl } from '@wiajs/lib/img/util';
// @ts-ignore
import * as css from './index.less';
import DEFAULTS from './default';
import render from './render';
import preview from './preview';
import events from './event';
import handlers from './handler';
import change from './change';
import methods from './method';
import { ACTION_ALL, CLASS_HIDDEN, CLASS_HIDE, CLASS_INVISIBLE, CLASS_MOVE, DATA_ACTION, EVENT_READY, MIME_TYPE_JPEG, NAMESPACE, REGEXP_DATA_URL, REGEXP_DATA_URL_JPEG, REGEXP_TAG_NAME, WINDOW } from './constant';
import { addTimestamp, isCrossOriginURL } from './util';
const AnotherCropper = WINDOW.Cropper;
export default class Cropper extends Event {
/**
* Create a new Cropper.
* @param {Element} el - The target element for cropping.
* @param {Object} [opt={}] - The configuration opt.
*/ constructor(app, opt = {}){
super(opt, [
app
]);
this.el = opt.el;
this.el.addClass(`${css.wiaui_cropper}`) // 添加样式scope
;
this.img = this.el.findNode('img').dom;
if (!this.img || !REGEXP_TAG_NAME.test(this.img.tagName)) {
throw new Error('The img argument is required and must be an <img> or <canvas> element.');
}
this.opt = {
...DEFAULTS,
...opt
};
this.cropped = false;
this.disabled = false;
this.pointers = {};
this.ready = false;
this.reloading = false;
this.replaced = false;
this.sized = false;
this.sizing = false;
this.init();
}
init() {
const { img } = this;
const tagName = img.tagName.toLowerCase();
let url;
if (img[NAMESPACE]) {
return;
}
img[NAMESPACE] = this;
if (tagName === 'img') {
this.isImg = true;
// e.g.: "img/picture.jpg"
url = img.getAttribute('src') || '';
this.originalUrl = url;
// Stop when it's a blank image
if (!url) {
return;
}
// e.g.: "https://example.com/img/picture.jpg"
url = img.src;
} else if (tagName === 'canvas' && window.HTMLCanvasElement) {
url = img.toDataURL();
}
this.load(url);
}
load(url) {
if (!url) {
return;
}
this.url = url;
this.imageData = {};
const { img, opt } = this;
// Only IE10+ supports Typed Arrays
if (!opt.checkOrientation || !window.ArrayBuffer) {
this.clone();
return;
}
// Detect the mime type of the image directly if it is a Data URL
if (REGEXP_DATA_URL.test(url)) {
// Read ArrayBuffer from Data URL of JPEG images directly for better performance
if (REGEXP_DATA_URL_JPEG.test(url)) {
this.read(urlToBuf(url));
} else {
// Only a JPEG image may contains Exif Orientation information,
// the rest types of Data URLs are not necessary to check orientation at all.
this.clone();
}
return;
}
// 1. Detect the mime type of the image by a XMLHttpRequest.
// 2. Load the image as ArrayBuffer for reading orientation if its a JPEG image.
const xhr = new XMLHttpRequest();
const clone = this.clone.bind(this);
this.reloading = true;
this.xhr = xhr;
// 1. Cross origin requests are only supported for protocol schemes:
// http, https, data, chrome, chrome-extension.
// 2. Access to XMLHttpRequest from a Data URL will be blocked by CORS policy
// in some browsers as IE11 and Safari.
xhr.onabort = clone;
xhr.onerror = clone;
xhr.ontimeout = clone;
xhr.onprogress = ()=>{
// Abort the request directly if it not a JPEG image for better performance
if (xhr.getResponseHeader('content-type') !== MIME_TYPE_JPEG) {
xhr.abort();
}
};
xhr.onload = ()=>{
this.read(xhr.response);
};
xhr.onloadend = ()=>{
this.reloading = false;
this.xhr = null;
};
// Bust cache when there is a "crossOrigin" property to avoid browser cache error
if (opt.checkCrossOrigin && isCrossOriginURL(url) && img.crossOrigin) {
url = addTimestamp(url);
}
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.withCredentials = img.crossOrigin === 'use-credentials';
xhr.send();
}
read(arrayBuffer) {
const { opt, imageData } = this;
// Reset the orientation value to its default value 1
// as some iOS browsers will render image with its orientation
const { orientation } = getExif(arrayBuffer, true);
let rotate = 0;
let scaleX = 1;
let scaleY = 1;
if (orientation > 1) {
// Generate a new URL which has the default orientation value
this.url = bufToUrl(arrayBuffer, MIME_TYPE_JPEG);
({ rotate, scaleX, scaleY } = getRotate(orientation));
}
if (opt.rotatable) imageData.rotate = rotate;
this.clone();
}
clone() {
const { img, url } = this;
let { crossOrigin } = img;
let crossOriginUrl = url;
if (this.opt.checkCrossOrigin && isCrossOriginURL(url)) {
if (!crossOrigin) {
crossOrigin = 'anonymous';
}
// Bust cache when there is not a "crossOrigin" property (#519)
crossOriginUrl = addTimestamp(url);
}
this.crossOrigin = crossOrigin;
this.crossOriginUrl = crossOriginUrl;
const image = document.createElement('img');
if (crossOrigin) {
image.crossOrigin = crossOrigin;
}
image.src = crossOriginUrl || url;
image.alt = img.alt || 'The image to crop';
this.image = image;
image.onload = this.start.bind(this);
image.onerror = this.stop.bind(this);
$(image).addClass(CLASS_HIDE);
img.parentNode.insertBefore(image, img.nextSibling);
}
start() {
const { image } = this;
image.onload = null;
image.onerror = null;
this.sizing = true;
// Match all browsers that use WebKit as the layout engine in iOS devices,
// such as Safari for iOS, Chrome for iOS, and in-app browsers.
const isIOSWebKit = WINDOW.navigator && /(?:iPad|iPhone|iPod).*?AppleWebKit/i.test(WINDOW.navigator.userAgent);
const done = (naturalWidth, naturalHeight)=>{
$.assign(this.imageData, {
naturalWidth,
naturalHeight,
aspectRatio: naturalWidth / naturalHeight
});
this.initialImageData = $.assign({}, this.imageData);
this.sizing = false;
this.sized = true;
this.build();
};
// Most modern browsers (excepts iOS WebKit)
if (image.naturalWidth && !isIOSWebKit) {
done(image.naturalWidth, image.naturalHeight);
return;
}
const sizingImage = document.createElement('img');
const body = document.body || document.documentElement;
this.sizingImage = sizingImage;
sizingImage.onload = ()=>{
done(sizingImage.width, sizingImage.height);
if (!isIOSWebKit) {
body.removeChild(sizingImage);
}
};
sizingImage.src = image.src;
// iOS WebKit will convert the image automatically
// with its orientation once append it into DOM (#279)
if (!isIOSWebKit) {
sizingImage.style.cssText = 'left:0;' + 'max-height:none!important;' + 'max-width:none!important;' + 'min-height:0!important;' + 'min-width:0!important;' + 'opacity:0;' + 'position:absolute;' + 'top:0;' + 'z-index:-1;';
body.appendChild(sizingImage);
}
}
stop() {
const { image } = this;
image.onload = null;
image.onerror = null;
image.remove();
this.image = null;
}
build() {
if (!this.sized || this.ready) {
return;
}
const { img, opt, image } = this;
// Create cropper elements
const container = img.parentNode;
const div = document.createElement('div');
div.innerHTML = /*#__PURE__*/ _jsxs("div", {
class: css.container,
"touch-action": "none",
children: [
/*#__PURE__*/ _jsx("div", {
class: css.wrapbox,
children: /*#__PURE__*/ _jsx("div", {
class: css.canvas
})
}),
/*#__PURE__*/ _jsx("div", {
class: css.dragbox
}),
/*#__PURE__*/ _jsxs("div", {
class: css.cropbox,
children: [
/*#__PURE__*/ _jsx("span", {
class: css.viewbox
}),
/*#__PURE__*/ _jsx("span", {
class: `${css.dashed} ${css.dashed_h}`
}),
/*#__PURE__*/ _jsx("span", {
class: `${css.dashed} ${css.dashed_v}`
}),
/*#__PURE__*/ _jsx("span", {
class: css.center
}),
/*#__PURE__*/ _jsx("span", {
class: css.face
}),
/*#__PURE__*/ _jsx("span", {
class: `${css.line} ${css.line_e}`,
"data-cropper-action": "e"
}),
/*#__PURE__*/ _jsx("span", {
class: `${css.line} ${css.line_n}`,
"data-cropper-action": "n"
}),
/*#__PURE__*/ _jsx("span", {
class: `${css.line} ${css.line_w}`,
"data-cropper-action": "w"
}),
/*#__PURE__*/ _jsx("span", {
class: `${css.line} ${css.line_s}`,
"data-cropper-action": "s"
}),
/*#__PURE__*/ _jsx("span", {
class: `${css.point} ${css.point_e}`,
"data-cropper-action": "e"
}),
/*#__PURE__*/ _jsx("span", {
class: `${css.point} ${css.point_n}`,
"data-cropper-action": "n"
}),
/*#__PURE__*/ _jsx("span", {
class: `${css.point} ${css.point_w}`,
"data-cropper-action": "w"
}),
/*#__PURE__*/ _jsx("span", {
class: `${css.point} ${css.point_s}`,
"data-cropper-action": "s"
}),
/*#__PURE__*/ _jsx("span", {
class: `${css.point} ${css.point_ne}`,
"data-cropper-action": "ne"
}),
/*#__PURE__*/ _jsx("span", {
class: `${css.point} ${css.point_nw}`,
"data-cropper-action": "nw"
}),
/*#__PURE__*/ _jsx("span", {
class: `${css.point} ${css.point_sw}`,
"data-cropper-action": "sw"
}),
/*#__PURE__*/ _jsx("span", {
class: `${css.point} ${css.point_se}`,
"data-cropper-action": "se"
})
]
})
]
});
const cropper = $(div).class(`${css.container}`);
const canvas = cropper.class(`${css.canvas}`);
const dragBox = cropper.class(`${css.dragbox}`);
const cropBox = $(cropper).class(`${css.cropbox}`);
const face = cropBox.class(`${css.face}`);
this.container = container;
this.cropper = cropper;
this.canvas = canvas;
this.dragBox = dragBox;
this.cropBox = cropBox;
this.viewBox = cropper.class(`${css.viewbox}`);
this.face = face;
canvas.append(image);
// Hide the original image
$(img).addClass(CLASS_HIDDEN);
// Inserts the cropper after to the current image
cropper.insertAfter(img);
// Show the image if is hidden
if (!this.isImg) {
$(image).removeClass(CLASS_HIDE);
}
this.initPreview();
this.bind();
opt.initialAspectRatio = Math.max(0, opt.initialAspectRatio) || NaN;
opt.aspectRatio = Math.max(0, opt.aspectRatio) || NaN;
opt.viewMode = Math.max(0, Math.min(3, Math.round(opt.viewMode))) || 0;
cropBox.addClass(CLASS_HIDDEN);
if (!opt.guides) cropBox.class(`${css.dashed}`).addClass(CLASS_HIDDEN);
if (!opt.center) cropBox.class(`${css.center}`).addClass(CLASS_HIDDEN);
if (opt.background) cropper.addClass(`${css.bg}`);
if (!opt.highlight) face.addClass(CLASS_INVISIBLE);
if (opt.cropBoxMovable) {
face.addClass(CLASS_MOVE);
face.data(DATA_ACTION, ACTION_ALL);
}
if (!opt.cropBoxResizable) {
cropBox.class(`${css.line}`).addClass(CLASS_HIDDEN);
cropBox.class(`${css.point}`).addClass(CLASS_HIDDEN);
}
this.render();
this.ready = true;
this.setDragMode(opt.dragMode);
if (opt.autoCrop) {
this.crop();
}
this.setData(opt.data);
if ($.isFunction(opt.ready)) {
$(img).once(EVENT_READY, opt.ready);
}
// 触发组件事件
this.emit(`local::${EVENT_READY} cropper${EVENT_READY}`, img);
// dispatchEvent(el, EVENT_READY);
}
unbuild() {
if (!this.ready) {
return;
}
this.ready = false;
this.unbind();
this.resetPreview();
this.cropper.remove();
$(this.img).removeClass(CLASS_HIDDEN);
}
uncreate() {
if (this.ready) {
this.unbuild();
this.ready = false;
this.cropped = false;
} else if (this.sizing) {
this.sizingImage.onload = null;
this.sizing = false;
this.sized = false;
} else if (this.reloading) {
this.xhr.onabort = null;
this.xhr.abort();
} else if (this.image) {
this.stop();
}
}
/**
* Get the no conflict cropper class.
* @returns {Cropper} The cropper class.
*/ static noConflict() {
window.Cropper = AnotherCropper;
return Cropper;
}
/**
* Change the default opt.
* @param {Object} opt - The new default opt.
*/ static setDefaults(opt) {
$.assign(DEFAULTS, $.isPlainObject(opt) && opt);
}
}
$.assign(Cropper.prototype, render, preview, events, handlers, change, methods);