const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined'; const WINDOW = IS_BROWSER ? window : {}; IS_BROWSER ? 'ontouchstart' in WINDOW.document.documentElement : false; const NAMESPACE = 'cropper'; const CROPPER_CANVAS = `${NAMESPACE}-canvas`; const CROPPER_IMAGE = `${NAMESPACE}-image`; const CROPPER_SELECTION = `${NAMESPACE}-selection`; const CROPPER_VIEWER = `${NAMESPACE}-viewer`; const EVENT_LOAD = 'load'; const EVENT_CHANGE = 'change'; const EVENT_TRANSFORM = 'transform'; /** * Check if the given value is not a number. */ const isNaN = Number.isNaN || WINDOW.isNaN; /** * Check if the given value is a number. * @param {*} value The value to check. * @returns {boolean} Returns `true` if the given value is a number, else `false`. */ function isNumber(value) { return typeof value === 'number' && !isNaN(value); } /** * Check if the given value is undefined. * @param {*} value The value to check. * @returns {boolean} Returns `true` if the given value is undefined, else `false`. */ function isUndefined(value) { return typeof value === 'undefined'; } /** * Check if the given value is an object. * @param {*} value - The value to check. * @returns {boolean} Returns `true` if the given value is an object, else `false`. */ function isObject(value) { return typeof value === 'object' && value !== null; } /** * Check if the given node is an element. * @param {*} node The node to check. * @returns {boolean} Returns `true` if the given node is an element; otherwise, `false`. */ function isElement(node) { return typeof node === 'object' && node !== null && node.nodeType === 1; } const REGEXP_CAMEL_CASE = /([a-z\d])([A-Z])/g; /** * Transform the given string from camelCase to kebab-case. * @param {string} value The value to transform. * @returns {string} Returns the transformed value. */ function toKebabCase(value) { return String(value).replace(REGEXP_CAMEL_CASE, '$1-$2').toLowerCase(); } const REGEXP_KEBAB_CASE = /-[A-z\d]/g; /** * Transform the given string from kebab-case to camelCase. * @param {string} value The value to transform. * @returns {string} Returns the transformed value. */ function toCamelCase(value) { return value.replace(REGEXP_KEBAB_CASE, (substring) => substring.slice(1).toUpperCase()); } const REGEXP_SPACES = /\s\s*/; /** * Remove event listener from the event target. * {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener} * @param {EventTarget} target The target of the event. * @param {string} types The types of the event. * @param {EventListenerOrEventListenerObject} listener The listener of the event. * @param {EventListenerOptions} [options] The options specify characteristics about the event listener. */ function off(target, types, listener, options) { types.trim().split(REGEXP_SPACES).forEach((type) => { target.removeEventListener(type, listener, options); }); } /** * Add event listener to the event target. * {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener} * @param {EventTarget} target The target of the event. * @param {string} types The types of the event. * @param {EventListenerOrEventListenerObject} listener The listener of the event. * @param {AddEventListenerOptions} [options] The options specify characteristics about the event listener. */ function on(target, types, listener, options) { types.trim().split(REGEXP_SPACES).forEach((type) => { target.addEventListener(type, listener, options); }); } const defaultEventOptions = { bubbles: true, cancelable: true, composed: true, }; /** * Dispatch event on the event target. * {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent} * @param {EventTarget} target The target of the event. * @param {string} type The name of the event. * @param {*} [detail] The data passed when initializing the event. * @param {CustomEventInit} [options] The other event options. * @returns {boolean} Returns the result value. */ function emit(target, type, detail, options) { return target.dispatchEvent(new CustomEvent(type, Object.assign(Object.assign(Object.assign({}, defaultEventOptions), { detail }), options))); } const resolvedPromise = Promise.resolve(); /** * Defers the callback to be executed after the next DOM update cycle. * @param {*} [context] The `this` context. * @param {Function} [callback] The callback to execute after the next DOM update cycle. * @returns {Promise} A promise that resolves to nothing. */ function nextTick(context, callback) { return callback ? resolvedPromise.then(context ? callback.bind(context) : callback) : resolvedPromise; } var style$1 = `:host([hidden]){display:none!important}`; const REGEXP_SUFFIX = /left|top|width|height/i; const DEFAULT_SHADOW_ROOT_MODE = 'open'; const shadowRoots = new WeakMap(); const styleSheets = new WeakMap(); const tagNames = new Map(); const supportsAdoptedStyleSheets = WINDOW.document && Array.isArray(WINDOW.document.adoptedStyleSheets) && 'replaceSync' in WINDOW.CSSStyleSheet.prototype; class CropperElement extends HTMLElement { get $sharedStyle() { return `${this.themeColor ? `:host{--theme-color: ${this.themeColor};}` : ''}${style$1}`; } constructor() { var _a, _b; super(); this.shadowRootMode = DEFAULT_SHADOW_ROOT_MODE; this.slottable = true; const name = (_b = (_a = Object.getPrototypeOf(this)) === null || _a === void 0 ? void 0 : _a.constructor) === null || _b === void 0 ? void 0 : _b.$name; if (name) { tagNames.set(name, this.tagName.toLowerCase()); } } static get observedAttributes() { return [ 'shadow-root-mode', 'slottable', 'theme-color', ]; } // Convert attribute to property attributeChangedCallback(name, oldValue, newValue) { if (Object.is(newValue, oldValue)) { return; } const propertyName = toCamelCase(name); const oldPropertyValue = this[propertyName]; let newPropertyValue = newValue; switch (typeof oldPropertyValue) { case 'boolean': newPropertyValue = newValue !== null && newValue !== 'false'; break; case 'number': newPropertyValue = Number(newValue); break; } this[propertyName] = newPropertyValue; switch (name) { case 'theme-color': { const styleSheet = styleSheets.get(this); const styles = this.$sharedStyle; if (styleSheet && styles) { if (supportsAdoptedStyleSheets) { styleSheet.replaceSync(styles); } else { styleSheet.textContent = styles; } } break; } } } // Convert property to attribute $propertyChangedCallback(name, oldValue, newValue) { if (Object.is(newValue, oldValue)) { return; } name = toKebabCase(name); switch (typeof newValue) { case 'boolean': if (newValue === true) { if (!this.hasAttribute(name)) { this.setAttribute(name, ''); } } else { this.removeAttribute(name); } break; case 'number': if (isNaN(newValue)) { newValue = ''; } else { newValue = String(newValue); } // Fall through // case 'string': // eslint-disable-next-line no-fallthrough default: if (newValue) { if (this.getAttribute(name) !== newValue) { this.setAttribute(name, newValue); } } else { this.removeAttribute(name); } } } connectedCallback() { // Observe properties after observed attributes Object.getPrototypeOf(this).constructor.observedAttributes.forEach((attribute) => { const property = toCamelCase(attribute); let value = this[property]; if (!isUndefined(value)) { this.$propertyChangedCallback(property, undefined, value); } Object.defineProperty(this, property, { enumerable: true, configurable: true, get() { return value; }, set(newValue) { const oldValue = value; value = newValue; this.$propertyChangedCallback(property, oldValue, newValue); }, }); }); const shadow = this.attachShadow({ mode: this.shadowRootMode || DEFAULT_SHADOW_ROOT_MODE, }); if (!this.shadowRoot) { shadowRoots.set(this, shadow); } styleSheets.set(this, this.$addStyles(this.$sharedStyle)); if (this.$style) { this.$addStyles(this.$style); } if (this.$template) { const template = document.createElement('template'); template.innerHTML = this.$template; shadow.appendChild(template.content); } if (this.slottable) { const slot = document.createElement('slot'); shadow.appendChild(slot); } } disconnectedCallback() { if (styleSheets.has(this)) { styleSheets.delete(this); } if (shadowRoots.has(this)) { shadowRoots.delete(this); } } // eslint-disable-next-line class-methods-use-this $getTagNameOf(name) { var _a; return (_a = tagNames.get(name)) !== null && _a !== void 0 ? _a : name; } $setStyles(properties) { Object.keys(properties).forEach((property) => { let value = properties[property]; if (isNumber(value)) { if (value !== 0 && REGEXP_SUFFIX.test(property)) { value = `${value}px`; } else { value = String(value); } } this.style[property] = value; }); return this; } /** * Outputs the shadow root of the element. * @returns {ShadowRoot} Returns the shadow root. */ $getShadowRoot() { return this.shadowRoot || shadowRoots.get(this); } /** * Adds styles to the shadow root. * @param {string} styles The styles to add. * @returns {CSSStyleSheet|HTMLStyleElement} Returns the generated style sheet. */ $addStyles(styles) { let styleSheet; const shadow = this.$getShadowRoot(); if (supportsAdoptedStyleSheets) { styleSheet = new CSSStyleSheet(); styleSheet.replaceSync(styles); shadow.adoptedStyleSheets = shadow.adoptedStyleSheets.concat(styleSheet); } else { styleSheet = document.createElement('style'); styleSheet.textContent = styles; shadow.appendChild(styleSheet); } return styleSheet; } /** * Dispatches an event at the element. * @param {string} type The name of the event. * @param {*} [detail] The data passed when initializing the event. * @param {CustomEventInit} [options] The other event options. * @returns {boolean} Returns the result value. */ $emit(type, detail, options) { return emit(this, type, detail, options); } /** * Defers the callback to be executed after the next DOM update cycle. * @param {Function} [callback] The callback to execute after the next DOM update cycle. * @returns {Promise} A promise that resolves to nothing. */ $nextTick(callback) { return nextTick(this, callback); } /** * Defines the constructor as a new custom element. * {@link https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define} * @param {string|object} [name] The element name. * @param {object} [options] The element definition options. */ static $define(name, options) { if (isObject(name)) { options = name; name = ''; } if (!name) { name = this.$name || this.name; } name = toKebabCase(name); if (IS_BROWSER && WINDOW.customElements && !WINDOW.customElements.get(name)) { customElements.define(name, this, options); } } } CropperElement.$version = '2.0.0-rc.2'; var style = `:host{display:block;height:100%;overflow:hidden;position:relative;width:100%}`; const canvasCache = new WeakMap(); const imageCache = new WeakMap(); const selectionCache = new WeakMap(); const sourceImageCache = new WeakMap(); const RESIZE_BOTH = 'both'; const RESIZE_HORIZONTAL = 'horizontal'; const RESIZE_VERTICAL = 'vertical'; const RESIZE_NONE = 'none'; class CropperViewer extends CropperElement { constructor() { super(...arguments); this.$onSelectionChange = null; this.$onSourceImageLoad = null; this.$onSourceImageTransform = null; this.$scale = 1; this.$style = style; this.resize = RESIZE_VERTICAL; this.selection = ''; this.slottable = false; } set $image(element) { imageCache.set(this, element); } get $image() { return imageCache.get(this); } set $sourceImage(element) { sourceImageCache.set(this, element); } get $sourceImage() { return sourceImageCache.get(this); } set $canvas(element) { canvasCache.set(this, element); } get $canvas() { return canvasCache.get(this); } set $selection(element) { selectionCache.set(this, element); } get $selection() { return selectionCache.get(this); } static get observedAttributes() { return super.observedAttributes.concat([ 'resize', 'selection', ]); } connectedCallback() { super.connectedCallback(); let $selection = null; if (this.selection) { $selection = this.ownerDocument.querySelector(this.selection); } else { $selection = this.closest(this.$getTagNameOf(CROPPER_SELECTION)); } if (isElement($selection)) { this.$selection = $selection; this.$onSelectionChange = this.$handleSelectionChange.bind(this); on($selection, EVENT_CHANGE, this.$onSelectionChange); const $canvas = $selection.closest(this.$getTagNameOf(CROPPER_CANVAS)); if ($canvas) { this.$canvas = $canvas; const $sourceImage = $canvas.querySelector(this.$getTagNameOf(CROPPER_IMAGE)); if ($sourceImage) { this.$sourceImage = $sourceImage; this.$image = $sourceImage.cloneNode(true); this.$getShadowRoot().appendChild(this.$image); this.$onSourceImageLoad = this.$handleSourceImageLoad.bind(this); this.$onSourceImageTransform = this.$handleSourceImageTransform.bind(this); on($sourceImage.$image, EVENT_LOAD, this.$onSourceImageLoad); on($sourceImage, EVENT_TRANSFORM, this.$onSourceImageTransform); } } this.$render(); } } disconnectedCallback() { const { $selection, $sourceImage } = this; if ($selection && this.$onSelectionChange) { off($selection, EVENT_CHANGE, this.$onSelectionChange); this.$onSelectionChange = null; } if ($sourceImage && this.$onSourceImageLoad) { off($sourceImage.$image, EVENT_LOAD, this.$onSourceImageLoad); this.$onSourceImageLoad = null; } if ($sourceImage && this.$onSourceImageTransform) { off($sourceImage, EVENT_TRANSFORM, this.$onSourceImageTransform); this.$onSourceImageTransform = null; } super.disconnectedCallback(); } $handleSelectionChange(event) { this.$render(event.detail); } $handleSourceImageLoad() { const { $image, $sourceImage } = this; const oldSrc = $image.getAttribute('src'); const newSrc = $sourceImage.getAttribute('src'); if (newSrc && newSrc !== oldSrc) { $image.setAttribute('src', newSrc); $image.$ready(() => { setTimeout(() => { this.$render(); }, 50); }); } } $handleSourceImageTransform(event) { this.$render(undefined, event.detail.matrix); } $render(selection, matrix) { const { $canvas, $selection } = this; if (!selection && !$selection.hidden) { selection = $selection; } if (!selection || (selection.x === 0 && selection.y === 0 && selection.width === 0 && selection.height === 0)) { selection = { x: 0, y: 0, width: $canvas.offsetWidth, height: $canvas.offsetHeight, }; } const { x, y, width, height, } = selection; const styles = {}; const { clientWidth, clientHeight } = this; let newWidth = clientWidth; let newHeight = clientHeight; let scale = NaN; switch (this.resize) { case RESIZE_BOTH: scale = 1; newWidth = width; newHeight = height; styles.width = width; styles.height = height; break; case RESIZE_HORIZONTAL: scale = height > 0 ? clientHeight / height : 0; newWidth = width * scale; styles.width = newWidth; break; case RESIZE_VERTICAL: scale = width > 0 ? clientWidth / width : 0; newHeight = height * scale; styles.height = newHeight; break; case RESIZE_NONE: default: if (clientWidth > 0) { scale = width > 0 ? clientWidth / width : 0; } else if (clientHeight > 0) { scale = height > 0 ? clientHeight / height : 0; } } this.$scale = scale; this.$setStyles(styles); if (this.$sourceImage) { this.$transformImageByOffset(matrix !== null && matrix !== void 0 ? matrix : this.$sourceImage.$getTransform(), -x, -y); } } $transformImageByOffset(matrix, x, y) { const { $image, $scale, $sourceImage, } = this; if ($sourceImage && $image && $scale >= 0) { const [a, b, c, d, e, f] = matrix; const translateX = ((x * d) - (c * y)) / ((a * d) - (c * b)); const translateY = ((y * a) - (b * x)) / ((a * d) - (c * b)); const newE = a * translateX + c * translateY + e; const newF = b * translateX + d * translateY + f; $image.$ready((image) => { this.$setStyles.call($image, { width: image.naturalWidth * $scale, height: image.naturalHeight * $scale, }); }); $image.$setTransform(a, b, c, d, newE * $scale, newF * $scale); } } } CropperViewer.$name = CROPPER_VIEWER; CropperViewer.$version = '2.0.0-rc.2'; export { RESIZE_BOTH, RESIZE_HORIZONTAL, RESIZE_NONE, RESIZE_VERTICAL, CropperViewer as default };