@cropper/element-handle
Version:
A custom handle element for the Cropper.
351 lines (343 loc) • 15 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.CropperHandle = factory());
})(this, (function () { 'use strict';
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_HANDLE = `${NAMESPACE}-handle`;
const ACTION_NONE = 'none';
/**
* 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;
}
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 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';
var style = `:host{background-color:var(--theme-color);display:block}:host([action=move]),:host([action=select]){height:100%;left:0;position:absolute;top:0;width:100%}:host([action=move]){cursor:move}:host([action=select]){cursor:crosshair}:host([action$=-resize]){background-color:transparent;height:15px;position:absolute;width:15px}:host([action$=-resize]):after{background-color:var(--theme-color);content:"";display:block;height:5px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:5px}:host([action=n-resize]),:host([action=s-resize]){cursor:ns-resize;left:50%;transform:translateX(-50%);width:100%}:host([action=n-resize]){top:-8px}:host([action=s-resize]){bottom:-8px}:host([action=e-resize]),:host([action=w-resize]){cursor:ew-resize;height:100%;top:50%;transform:translateY(-50%)}:host([action=e-resize]){right:-8px}:host([action=w-resize]){left:-8px}:host([action=ne-resize]){cursor:nesw-resize;right:-8px;top:-8px}:host([action=nw-resize]){cursor:nwse-resize;left:-8px;top:-8px}:host([action=se-resize]){bottom:-8px;cursor:nwse-resize;right:-8px}:host([action=se-resize]):after{height:15px;width:15px} (pointer:coarse){:host([action=se-resize]):after{height:10px;width:10px}} (pointer:fine){:host([action=se-resize]):after{height:5px;width:5px}}:host([action=sw-resize]){bottom:-8px;cursor:nesw-resize;left:-8px}:host([plain]){background-color:transparent}`;
class CropperHandle extends CropperElement {
constructor() {
super(...arguments);
this.$onCanvasCropEnd = null;
this.$onCanvasCropStart = null;
this.$style = style;
this.action = ACTION_NONE;
this.plain = false;
this.slottable = false;
this.themeColor = 'rgba(51, 153, 255, 0.5)';
}
static get observedAttributes() {
return super.observedAttributes.concat([
'action',
'plain',
]);
}
}
CropperHandle.$name = CROPPER_HANDLE;
CropperHandle.$version = '2.0.0';
return CropperHandle;
}));