UNPKG

tinymce

Version:

Web based JavaScript HTML WYSIWYG editor control.

1,319 lines (1,294 loc) 64.3 kB
/** * TinyMCE version 7.9.0 (2025-05-15) */ (function () { 'use strict'; var global$4 = tinymce.util.Tools.resolve('tinymce.PluginManager'); /* eslint-disable @typescript-eslint/no-wrapper-object-types */ const getPrototypeOf = Object.getPrototypeOf; const hasProto = (v, constructor, predicate) => { var _a; if (predicate(v, constructor.prototype)) { return true; } else { // String-based fallback time return ((_a = v.constructor) === null || _a === void 0 ? void 0 : _a.name) === constructor.name; } }; const typeOf = (x) => { const t = typeof x; if (x === null) { return 'null'; } else if (t === 'object' && Array.isArray(x)) { return 'array'; } else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) { return 'string'; } else { return t; } }; const isType = (type) => (value) => typeOf(value) === type; const isSimpleType = (type) => (value) => typeof value === type; const eq = (t) => (a) => t === a; const is = (value, constructor) => isObject(value) && hasProto(value, constructor, (o, proto) => getPrototypeOf(o) === proto); const isString = isType('string'); const isObject = isType('object'); const isPlainObject = (value) => is(value, Object); const isArray = isType('array'); const isNull = eq(null); const isBoolean = isSimpleType('boolean'); const isNullable = (a) => a === null || a === undefined; const isNonNullable = (a) => !isNullable(a); const isFunction = isSimpleType('function'); const isNumber = isSimpleType('number'); const isArrayOf = (value, pred) => { if (isArray(value)) { for (let i = 0, len = value.length; i < len; ++i) { if (!(pred(value[i]))) { return false; } } return true; } return false; }; const noop = () => { }; /** * The `Optional` type represents a value (of any type) that potentially does * not exist. Any `Optional<T>` can either be a `Some<T>` (in which case the * value does exist) or a `None` (in which case the value does not exist). This * module defines a whole lot of FP-inspired utility functions for dealing with * `Optional` objects. * * Comparison with null or undefined: * - We don't get fancy null coalescing operators with `Optional` * - We do get fancy helper functions with `Optional` * - `Optional` support nesting, and allow for the type to still be nullable (or * another `Optional`) * - There is no option to turn off strict-optional-checks like there is for * strict-null-checks */ class Optional { // The internal representation has a `tag` and a `value`, but both are // private: able to be console.logged, but not able to be accessed by code constructor(tag, value) { this.tag = tag; this.value = value; } // --- Identities --- /** * Creates a new `Optional<T>` that **does** contain a value. */ static some(value) { return new Optional(true, value); } /** * Create a new `Optional<T>` that **does not** contain a value. `T` can be * any type because we don't actually have a `T`. */ static none() { return Optional.singletonNone; } /** * Perform a transform on an `Optional` type. Regardless of whether this * `Optional` contains a value or not, `fold` will return a value of type `U`. * If this `Optional` does not contain a value, the `U` will be created by * calling `onNone`. If this `Optional` does contain a value, the `U` will be * created by calling `onSome`. * * For the FP enthusiasts in the room, this function: * 1. Could be used to implement all of the functions below * 2. Forms a catamorphism */ fold(onNone, onSome) { if (this.tag) { return onSome(this.value); } else { return onNone(); } } /** * Determine if this `Optional` object contains a value. */ isSome() { return this.tag; } /** * Determine if this `Optional` object **does not** contain a value. */ isNone() { return !this.tag; } // --- Functor (name stolen from Haskell / maths) --- /** * Perform a transform on an `Optional` object, **if** there is a value. If * you provide a function to turn a T into a U, this is the function you use * to turn an `Optional<T>` into an `Optional<U>`. If this **does** contain * a value then the output will also contain a value (that value being the * output of `mapper(this.value)`), and if this **does not** contain a value * then neither will the output. */ map(mapper) { if (this.tag) { return Optional.some(mapper(this.value)); } else { return Optional.none(); } } // --- Monad (name stolen from Haskell / maths) --- /** * Perform a transform on an `Optional` object, **if** there is a value. * Unlike `map`, here the transform itself also returns an `Optional`. */ bind(binder) { if (this.tag) { return binder(this.value); } else { return Optional.none(); } } // --- Traversable (name stolen from Haskell / maths) --- /** * For a given predicate, this function finds out if there **exists** a value * inside this `Optional` object that meets the predicate. In practice, this * means that for `Optional`s that do not contain a value it returns false (as * no predicate-meeting value exists). */ exists(predicate) { return this.tag && predicate(this.value); } /** * For a given predicate, this function finds out if **all** the values inside * this `Optional` object meet the predicate. In practice, this means that * for `Optional`s that do not contain a value it returns true (as all 0 * objects do meet the predicate). */ forall(predicate) { return !this.tag || predicate(this.value); } filter(predicate) { if (!this.tag || predicate(this.value)) { return this; } else { return Optional.none(); } } // --- Getters --- /** * Get the value out of the inside of the `Optional` object, using a default * `replacement` value if the provided `Optional` object does not contain a * value. */ getOr(replacement) { return this.tag ? this.value : replacement; } /** * Get the value out of the inside of the `Optional` object, using a default * `replacement` value if the provided `Optional` object does not contain a * value. Unlike `getOr`, in this method the `replacement` object is also * `Optional` - meaning that this method will always return an `Optional`. */ or(replacement) { return this.tag ? this : replacement; } /** * Get the value out of the inside of the `Optional` object, using a default * `replacement` value if the provided `Optional` object does not contain a * value. Unlike `getOr`, in this method the `replacement` value is * "thunked" - that is to say that you don't pass a value to `getOrThunk`, you * pass a function which (if called) will **return** the `value` you want to * use. */ getOrThunk(thunk) { return this.tag ? this.value : thunk(); } /** * Get the value out of the inside of the `Optional` object, using a default * `replacement` value if the provided Optional object does not contain a * value. * * Unlike `or`, in this method the `replacement` value is "thunked" - that is * to say that you don't pass a value to `orThunk`, you pass a function which * (if called) will **return** the `value` you want to use. * * Unlike `getOrThunk`, in this method the `replacement` value is also * `Optional`, meaning that this method will always return an `Optional`. */ orThunk(thunk) { return this.tag ? this : thunk(); } /** * Get the value out of the inside of the `Optional` object, throwing an * exception if the provided `Optional` object does not contain a value. * * WARNING: * You should only be using this function if you know that the `Optional` * object **is not** empty (otherwise you're throwing exceptions in production * code, which is bad). * * In tests this is more acceptable. * * Prefer other methods to this, such as `.each`. */ getOrDie(message) { if (!this.tag) { throw new Error(message !== null && message !== void 0 ? message : 'Called getOrDie on None'); } else { return this.value; } } // --- Interop with null and undefined --- /** * Creates an `Optional` value from a nullable (or undefined-able) input. * Null, or undefined, is converted to `None`, and anything else is converted * to `Some`. */ static from(value) { return isNonNullable(value) ? Optional.some(value) : Optional.none(); } /** * Converts an `Optional` to a nullable type, by getting the value if it * exists, or returning `null` if it does not. */ getOrNull() { return this.tag ? this.value : null; } /** * Converts an `Optional` to an undefined-able type, by getting the value if * it exists, or returning `undefined` if it does not. */ getOrUndefined() { return this.value; } // --- Utilities --- /** * If the `Optional` contains a value, perform an action on that value. * Unlike the rest of the methods on this type, `.each` has side-effects. If * you want to transform an `Optional<T>` **into** something, then this is not * the method for you. If you want to use an `Optional<T>` to **do** * something, then this is the method for you - provided you're okay with not * doing anything in the case where the `Optional` doesn't have a value inside * it. If you're not sure whether your use-case fits into transforming * **into** something or **doing** something, check whether it has a return * value. If it does, you should be performing a transform. */ each(worker) { if (this.tag) { worker(this.value); } } /** * Turn the `Optional` object into an array that contains all of the values * stored inside the `Optional`. In practice, this means the output will have * either 0 or 1 elements. */ toArray() { return this.tag ? [this.value] : []; } /** * Turn the `Optional` object into a string for debugging or printing. Not * recommended for production code, but good for debugging. Also note that * these days an `Optional` object can be logged to the console directly, and * its inner value (if it exists) will be visible. */ toString() { return this.tag ? `some(${this.value})` : 'none()'; } } // Sneaky optimisation: every instance of Optional.none is identical, so just // reuse the same object Optional.singletonNone = new Optional(false); /* eslint-disable @typescript-eslint/unbound-method */ const nativeSlice = Array.prototype.slice; const nativePush = Array.prototype.push; const flatten = (xs) => { // Note, this is possible because push supports multiple arguments: // http://jsperf.com/concat-push/6 // Note that in the past, concat() would silently work (very slowly) for array-like objects. // With this change it will throw an error. const r = []; for (let i = 0, len = xs.length; i < len; ++i) { // Ensure that each value is an array itself if (!isArray(xs[i])) { throw new Error('Arr.flatten item ' + i + ' was not an array, input: ' + xs); } nativePush.apply(r, xs[i]); } return r; }; const get = (xs, i) => i >= 0 && i < xs.length ? Optional.some(xs[i]) : Optional.none(); const head = (xs) => get(xs, 0); isFunction(Array.from) ? Array.from : (x) => nativeSlice.call(x); const findMap = (arr, f) => { for (let i = 0; i < arr.length; i++) { const r = f(arr[i], i); if (r.isSome()) { return r; } } return Optional.none(); }; // There are many variations of Object iteration that are faster than the 'for-in' style: // http://jsperf.com/object-keys-iteration/107 // // Use the native keys if it is available (IE9+), otherwise fall back to manually filtering const keys = Object.keys; // eslint-disable-next-line @typescript-eslint/unbound-method const hasOwnProperty = Object.hasOwnProperty; const each = (obj, f) => { const props = keys(obj); for (let k = 0, len = props.length; k < len; k++) { const i = props[k]; const x = obj[i]; f(x, i); } }; const objAcc = (r) => (x, i) => { r[i] = x; }; const internalFilter = (obj, pred, onTrue, onFalse) => { each(obj, (x, i) => { (pred(x, i) ? onTrue : onFalse)(x, i); }); }; const filter = (obj, pred) => { const t = {}; internalFilter(obj, pred, objAcc(t), noop); return t; }; const has = (obj, key) => hasOwnProperty.call(obj, key); const hasNonNullableKey = (obj, key) => has(obj, key) && obj[key] !== undefined && obj[key] !== null; const deep = (old, nu) => { const bothObjects = isPlainObject(old) && isPlainObject(nu); return bothObjects ? deepMerge(old, nu) : nu; }; const baseMerge = (merger) => { return (...objects) => { if (objects.length === 0) { throw new Error(`Can't merge zero objects`); } const ret = {}; for (let j = 0; j < objects.length; j++) { const curObject = objects[j]; for (const key in curObject) { if (has(curObject, key)) { ret[key] = merger(ret[key], curObject[key]); } } } return ret; }; }; const deepMerge = baseMerge(deep); const isNotEmpty = (s) => s.length > 0; const fromHtml = (html, scope) => { const doc = scope || document; const div = doc.createElement('div'); div.innerHTML = html; if (!div.hasChildNodes() || div.childNodes.length > 1) { const message = 'HTML does not have a single root node'; // eslint-disable-next-line no-console console.error(message, html); throw new Error(message); } return fromDom(div.childNodes[0]); }; const fromTag = (tag, scope) => { const doc = scope || document; const node = doc.createElement(tag); return fromDom(node); }; const fromText = (text, scope) => { const doc = scope || document; const node = doc.createTextNode(text); return fromDom(node); }; const fromDom = (node) => { // TODO: Consider removing this check, but left atm for safety if (node === null || node === undefined) { throw new Error('Node cannot be null or undefined'); } return { dom: node }; }; const fromPoint = (docElm, x, y) => Optional.from(docElm.dom.elementFromPoint(x, y)).map(fromDom); // tslint:disable-next-line:variable-name const SugarElement = { fromHtml, fromTag, fromText, fromDom, fromPoint }; const rawSet = (dom, key, value) => { /* * JQuery coerced everything to a string, and silently did nothing on text node/null/undefined. * * We fail on those invalid cases, only allowing numbers and booleans. */ if (isString(value) || isBoolean(value) || isNumber(value)) { dom.setAttribute(key, value + ''); } else { // eslint-disable-next-line no-console console.error('Invalid call to Attribute.set. Key ', key, ':: Value ', value, ':: Element ', dom); throw new Error('Attribute value was not simple'); } }; const set = (element, key, value) => { rawSet(element.dom, key, value); }; const remove = (element, key) => { element.dom.removeAttribute(key); }; var global$3 = tinymce.util.Tools.resolve('tinymce.dom.DOMUtils'); var global$2 = tinymce.util.Tools.resolve('tinymce.util.URI'); const option = (name) => (editor) => editor.options.get(name); const register$2 = (editor) => { const registerOption = editor.options.register; registerOption('image_dimensions', { processor: 'boolean', default: true }); registerOption('image_advtab', { processor: 'boolean', default: false }); registerOption('image_uploadtab', { processor: 'boolean', default: true }); registerOption('image_prepend_url', { processor: 'string', default: '' }); registerOption('image_class_list', { processor: 'object[]' }); registerOption('image_description', { processor: 'boolean', default: true }); registerOption('image_title', { processor: 'boolean', default: false }); registerOption('image_caption', { processor: 'boolean', default: false }); registerOption('image_list', { processor: (value) => { const valid = value === false || isString(value) || isArrayOf(value, isObject) || isFunction(value); return valid ? { value, valid } : { valid: false, message: 'Must be false, a string, an array or a function.' }; }, default: false }); }; const hasDimensions = option('image_dimensions'); const hasAdvTab = option('image_advtab'); const hasUploadTab = option('image_uploadtab'); const getPrependUrl = option('image_prepend_url'); const getClassList = option('image_class_list'); const hasDescription = option('image_description'); const hasImageTitle = option('image_title'); const hasImageCaption = option('image_caption'); const getImageList = option('image_list'); const showAccessibilityOptions = option('a11y_advanced_options'); const isAutomaticUploadsEnabled = option('automatic_uploads'); const hasUploadUrl = (editor) => isNotEmpty(editor.options.get('images_upload_url')); const hasUploadHandler = (editor) => isNonNullable(editor.options.get('images_upload_handler')); // TODO: Figure out if these would ever be something other than numbers. This was added in: #TINY-1350 const parseIntAndGetMax = (val1, val2) => Math.max(parseInt(val1, 10), parseInt(val2, 10)); const getImageSize = (url) => new Promise((callback) => { const img = document.createElement('img'); const done = (dimensions) => { if (img.parentNode) { img.parentNode.removeChild(img); } callback(dimensions); }; img.addEventListener('load', () => { const width = parseIntAndGetMax(img.width, img.clientWidth); const height = parseIntAndGetMax(img.height, img.clientHeight); const dimensions = { width, height }; done(Promise.resolve(dimensions)); }); img.addEventListener('error', () => { done(Promise.reject(`Failed to get image dimensions for: ${url}`)); }); const style = img.style; style.visibility = 'hidden'; style.position = 'fixed'; style.bottom = style.left = '0px'; style.width = style.height = 'auto'; document.body.appendChild(img); img.src = url; }); const removePixelSuffix = (value) => { if (value) { value = value.replace(/px$/, ''); } return value; }; const addPixelSuffix = (value) => { if (value.length > 0 && /^[0-9]+$/.test(value)) { value += 'px'; } return value; }; const mergeMargins = (css) => { if (css.margin) { const splitMargin = String(css.margin).split(' '); switch (splitMargin.length) { case 1: // margin: toprightbottomleft; css['margin-top'] = css['margin-top'] || splitMargin[0]; css['margin-right'] = css['margin-right'] || splitMargin[0]; css['margin-bottom'] = css['margin-bottom'] || splitMargin[0]; css['margin-left'] = css['margin-left'] || splitMargin[0]; break; case 2: // margin: topbottom rightleft; css['margin-top'] = css['margin-top'] || splitMargin[0]; css['margin-right'] = css['margin-right'] || splitMargin[1]; css['margin-bottom'] = css['margin-bottom'] || splitMargin[0]; css['margin-left'] = css['margin-left'] || splitMargin[1]; break; case 3: // margin: top rightleft bottom; css['margin-top'] = css['margin-top'] || splitMargin[0]; css['margin-right'] = css['margin-right'] || splitMargin[1]; css['margin-bottom'] = css['margin-bottom'] || splitMargin[2]; css['margin-left'] = css['margin-left'] || splitMargin[1]; break; case 4: // margin: top right bottom left; css['margin-top'] = css['margin-top'] || splitMargin[0]; css['margin-right'] = css['margin-right'] || splitMargin[1]; css['margin-bottom'] = css['margin-bottom'] || splitMargin[2]; css['margin-left'] = css['margin-left'] || splitMargin[3]; } delete css.margin; } return css; }; // TODO: Input on this callback should really be validated const createImageList = (editor, callback) => { const imageList = getImageList(editor); if (isString(imageList)) { // eslint-disable-next-line @typescript-eslint/no-floating-promises fetch(imageList) .then((res) => { if (res.ok) { // eslint-disable-next-line @typescript-eslint/no-floating-promises res.json().then(callback); } }); } else if (isFunction(imageList)) { imageList(callback); } else { callback(imageList); } }; const waitLoadImage = (editor, data, imgElm) => { const selectImage = () => { imgElm.onload = imgElm.onerror = null; if (editor.selection) { editor.selection.select(imgElm); editor.nodeChanged(); } }; imgElm.onload = () => { if (!data.width && !data.height && hasDimensions(editor)) { editor.dom.setAttribs(imgElm, { width: String(imgElm.clientWidth), height: String(imgElm.clientHeight) }); } selectImage(); }; imgElm.onerror = selectImage; }; const blobToDataUri = (blob) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { resolve(reader.result); }; reader.onerror = () => { var _a; reject((_a = reader.error) === null || _a === void 0 ? void 0 : _a.message); }; reader.readAsDataURL(blob); }); const isPlaceholderImage = (imgElm) => imgElm.nodeName === 'IMG' && (imgElm.hasAttribute('data-mce-object') || imgElm.hasAttribute('data-mce-placeholder')); const isSafeImageUrl = (editor, src) => { const getOption = editor.options.get; return global$2.isDomSafe(src, 'img', { allow_html_data_urls: getOption('allow_html_data_urls'), allow_script_urls: getOption('allow_script_urls'), allow_svg_data_urls: getOption('allow_svg_data_urls') }); }; const DOM = global$3.DOM; const getHspace = (image) => { if (image.style.marginLeft && image.style.marginRight && image.style.marginLeft === image.style.marginRight) { return removePixelSuffix(image.style.marginLeft); } else { return ''; } }; const getVspace = (image) => { if (image.style.marginTop && image.style.marginBottom && image.style.marginTop === image.style.marginBottom) { return removePixelSuffix(image.style.marginTop); } else { return ''; } }; const getBorder = (image) => { if (image.style.borderWidth) { return removePixelSuffix(image.style.borderWidth); } else { return ''; } }; const getAttrib = (image, name) => { var _a; if (image.hasAttribute(name)) { return (_a = image.getAttribute(name)) !== null && _a !== void 0 ? _a : ''; } else { return ''; } }; const hasCaption = (image) => image.parentNode !== null && image.parentNode.nodeName === 'FIGURE'; const updateAttrib = (image, name, value) => { if (value === '' || value === null) { image.removeAttribute(name); } else { image.setAttribute(name, value); } }; const wrapInFigure = (image) => { const figureElm = DOM.create('figure', { class: 'image' }); DOM.insertAfter(figureElm, image); figureElm.appendChild(image); figureElm.appendChild(DOM.create('figcaption', { contentEditable: 'true' }, 'Caption')); figureElm.contentEditable = 'false'; }; const removeFigure = (image) => { const figureElm = image.parentNode; if (isNonNullable(figureElm)) { DOM.insertAfter(image, figureElm); DOM.remove(figureElm); } }; const toggleCaption = (image) => { if (hasCaption(image)) { removeFigure(image); } else { wrapInFigure(image); } }; const normalizeStyle = (image, normalizeCss) => { const attrValue = image.getAttribute('style'); const value = normalizeCss(attrValue !== null ? attrValue : ''); if (value.length > 0) { image.setAttribute('style', value); image.setAttribute('data-mce-style', value); } else { image.removeAttribute('style'); } }; const setSize = (name, normalizeCss) => (image, name, value) => { const styles = image.style; if (styles[name]) { styles[name] = addPixelSuffix(value); normalizeStyle(image, normalizeCss); } else { updateAttrib(image, name, value); } }; const getSize = (image, name) => { if (image.style[name]) { return removePixelSuffix(image.style[name]); } else { return getAttrib(image, name); } }; const setHspace = (image, value) => { const pxValue = addPixelSuffix(value); image.style.marginLeft = pxValue; image.style.marginRight = pxValue; }; const setVspace = (image, value) => { const pxValue = addPixelSuffix(value); image.style.marginTop = pxValue; image.style.marginBottom = pxValue; }; const setBorder = (image, value) => { const pxValue = addPixelSuffix(value); image.style.borderWidth = pxValue; }; const setBorderStyle = (image, value) => { image.style.borderStyle = value; }; const getBorderStyle = (image) => { var _a; return (_a = image.style.borderStyle) !== null && _a !== void 0 ? _a : ''; }; const isFigure = (elm) => isNonNullable(elm) && elm.nodeName === 'FIGURE'; const isImage = (elm) => elm.nodeName === 'IMG'; const getIsDecorative = (image) => DOM.getAttrib(image, 'alt').length === 0 && DOM.getAttrib(image, 'role') === 'presentation'; const getAlt = (image) => { if (getIsDecorative(image)) { return ''; } else { return getAttrib(image, 'alt'); } }; const defaultData = () => ({ src: '', alt: '', title: '', width: '', height: '', class: '', style: '', caption: false, hspace: '', vspace: '', border: '', borderStyle: '', isDecorative: false }); const getStyleValue = (normalizeCss, data) => { var _a; const image = document.createElement('img'); updateAttrib(image, 'style', data.style); if (getHspace(image) || data.hspace !== '') { setHspace(image, data.hspace); } if (getVspace(image) || data.vspace !== '') { setVspace(image, data.vspace); } if (getBorder(image) || data.border !== '') { setBorder(image, data.border); } if (getBorderStyle(image) || data.borderStyle !== '') { setBorderStyle(image, data.borderStyle); } return normalizeCss((_a = image.getAttribute('style')) !== null && _a !== void 0 ? _a : ''); }; const create = (normalizeCss, data) => { const image = document.createElement('img'); write(normalizeCss, { ...data, caption: false }, image); // Always set alt even if data.alt is an empty string setAlt(image, data.alt, data.isDecorative); if (data.caption) { const figure = DOM.create('figure', { class: 'image' }); figure.appendChild(image); figure.appendChild(DOM.create('figcaption', { contentEditable: 'true' }, 'Caption')); figure.contentEditable = 'false'; return figure; } else { return image; } }; const read = (normalizeCss, image) => ({ src: getAttrib(image, 'src'), alt: getAlt(image), title: getAttrib(image, 'title'), width: getSize(image, 'width'), height: getSize(image, 'height'), class: getAttrib(image, 'class'), style: normalizeCss(getAttrib(image, 'style')), caption: hasCaption(image), hspace: getHspace(image), vspace: getVspace(image), border: getBorder(image), borderStyle: getBorderStyle(image), isDecorative: getIsDecorative(image) }); const updateProp = (image, oldData, newData, name, set) => { if (newData[name] !== oldData[name]) { set(image, name, String(newData[name])); } }; const setAlt = (image, alt, isDecorative) => { if (isDecorative) { DOM.setAttrib(image, 'role', 'presentation'); // unfortunately can't set "" attr value with domutils const sugarImage = SugarElement.fromDom(image); set(sugarImage, 'alt', ''); } else { if (isNull(alt)) { const sugarImage = SugarElement.fromDom(image); remove(sugarImage, 'alt'); } else { // unfortunately can't set "" attr value with domutils const sugarImage = SugarElement.fromDom(image); set(sugarImage, 'alt', alt); } if (DOM.getAttrib(image, 'role') === 'presentation') { DOM.setAttrib(image, 'role', ''); } } }; const updateAlt = (image, oldData, newData) => { if (newData.alt !== oldData.alt || newData.isDecorative !== oldData.isDecorative) { setAlt(image, newData.alt, newData.isDecorative); } }; const normalized = (set, normalizeCss) => (image, name, value) => { set(image, value); normalizeStyle(image, normalizeCss); }; const write = (normalizeCss, newData, image) => { const oldData = read(normalizeCss, image); updateProp(image, oldData, newData, 'caption', (image, _name, _value) => toggleCaption(image)); updateProp(image, oldData, newData, 'src', updateAttrib); updateProp(image, oldData, newData, 'title', updateAttrib); updateProp(image, oldData, newData, 'width', setSize('width', normalizeCss)); updateProp(image, oldData, newData, 'height', setSize('height', normalizeCss)); updateProp(image, oldData, newData, 'class', updateAttrib); updateProp(image, oldData, newData, 'style', normalized((image, value) => updateAttrib(image, 'style', value), normalizeCss)); updateProp(image, oldData, newData, 'hspace', normalized(setHspace, normalizeCss)); updateProp(image, oldData, newData, 'vspace', normalized(setVspace, normalizeCss)); updateProp(image, oldData, newData, 'border', normalized(setBorder, normalizeCss)); updateProp(image, oldData, newData, 'borderStyle', normalized(setBorderStyle, normalizeCss)); updateAlt(image, oldData, newData); }; const normalizeCss$1 = (editor, cssText) => { const css = editor.dom.styles.parse(cssText); const mergedCss = mergeMargins(css); const compressed = editor.dom.styles.parse(editor.dom.styles.serialize(mergedCss)); return editor.dom.styles.serialize(compressed); }; const getSelectedImage = (editor) => { const imgElm = editor.selection.getNode(); const figureElm = editor.dom.getParent(imgElm, 'figure.image'); if (figureElm) { return editor.dom.select('img', figureElm)[0]; } if (imgElm && (imgElm.nodeName !== 'IMG' || isPlaceholderImage(imgElm))) { return null; } return imgElm; }; const splitTextBlock = (editor, figure) => { var _a; const dom = editor.dom; const textBlockElements = filter(editor.schema.getTextBlockElements(), (_, parentElm) => !editor.schema.isValidChild(parentElm, 'figure')); const textBlock = dom.getParent(figure.parentNode, (node) => hasNonNullableKey(textBlockElements, node.nodeName), editor.getBody()); if (textBlock) { return (_a = dom.split(textBlock, figure)) !== null && _a !== void 0 ? _a : figure; } else { return figure; } }; const readImageDataFromSelection = (editor) => { const image = getSelectedImage(editor); return image ? read((css) => normalizeCss$1(editor, css), image) : defaultData(); }; const insertImageAtCaret = (editor, data) => { const elm = create((css) => normalizeCss$1(editor, css), data); editor.dom.setAttrib(elm, 'data-mce-id', '__mcenew'); editor.focus(); editor.selection.setContent(elm.outerHTML); const insertedElm = editor.dom.select('*[data-mce-id="__mcenew"]')[0]; editor.dom.setAttrib(insertedElm, 'data-mce-id', null); if (isFigure(insertedElm)) { const figure = splitTextBlock(editor, insertedElm); editor.selection.select(figure); } else { editor.selection.select(insertedElm); } }; const syncSrcAttr = (editor, image) => { editor.dom.setAttrib(image, 'src', image.getAttribute('src')); }; const deleteImage = (editor, image) => { if (image) { const elm = editor.dom.is(image.parentNode, 'figure.image') ? image.parentNode : image; editor.dom.remove(elm); editor.focus(); editor.nodeChanged(); if (editor.dom.isEmpty(editor.getBody())) { editor.setContent(''); editor.selection.setCursorLocation(); } } }; const writeImageDataToSelection = (editor, data) => { const image = getSelectedImage(editor); if (image) { write((css) => normalizeCss$1(editor, css), data, image); syncSrcAttr(editor, image); if (isFigure(image.parentNode)) { editor.dom.setStyle(image, 'float', ''); const figure = image.parentNode; splitTextBlock(editor, figure); editor.selection.select(image.parentNode); } else { editor.selection.select(image); waitLoadImage(editor, data, image); } } }; const sanitizeImageData = (editor, data) => { // Sanitize the URL const src = data.src; return { ...data, src: isSafeImageUrl(editor, src) ? src : '' }; }; const insertOrUpdateImage = (editor, partialData) => { const image = getSelectedImage(editor); if (image) { const selectedImageData = read((css) => normalizeCss$1(editor, css), image); const data = { ...selectedImageData, ...partialData }; const sanitizedData = sanitizeImageData(editor, data); if (data.src) { writeImageDataToSelection(editor, sanitizedData); } else { deleteImage(editor, image); } } else if (partialData.src) { insertImageAtCaret(editor, { ...defaultData(), ...partialData }); } }; var global$1 = tinymce.util.Tools.resolve('tinymce.util.ImageUploader'); var global = tinymce.util.Tools.resolve('tinymce.util.Tools'); const getValue = (item) => isString(item.value) ? item.value : ''; const getText = (item) => { if (isString(item.text)) { return item.text; } else if (isString(item.title)) { return item.title; } else { return ''; } }; const sanitizeList = (list, extractValue) => { const out = []; global.each(list, (item) => { const text = getText(item); if (item.menu !== undefined) { const items = sanitizeList(item.menu, extractValue); out.push({ text, items }); // list group } else { const value = extractValue(item); out.push({ text, value }); // list value } }); return out; }; const sanitizer = (extractor = getValue) => (list) => { if (list) { return Optional.from(list).map((list) => sanitizeList(list, extractor)); } else { return Optional.none(); } }; const sanitize = (list) => sanitizer(getValue)(list); const isGroup = (item) => has(item, 'items'); const findEntryDelegate = (list, value) => findMap(list, (item) => { if (isGroup(item)) { return findEntryDelegate(item.items, value); } else if (item.value === value) { return Optional.some(item); } else { return Optional.none(); } }); const findEntry = (optList, value) => optList.bind((list) => findEntryDelegate(list, value)); const ListUtils = { sanitizer, sanitize, findEntry }; const makeTab$2 = (_info) => ({ title: 'Advanced', name: 'advanced', items: [ { type: 'grid', columns: 2, items: [ { type: 'input', label: 'Vertical space', name: 'vspace', inputMode: 'numeric' }, { type: 'input', label: 'Horizontal space', name: 'hspace', inputMode: 'numeric' }, { type: 'input', label: 'Border width', name: 'border', inputMode: 'numeric' }, { type: 'listbox', name: 'borderstyle', label: 'Border style', items: [ { text: 'Select...', value: '' }, { text: 'Solid', value: 'solid' }, { text: 'Dotted', value: 'dotted' }, { text: 'Dashed', value: 'dashed' }, { text: 'Double', value: 'double' }, { text: 'Groove', value: 'groove' }, { text: 'Ridge', value: 'ridge' }, { text: 'Inset', value: 'inset' }, { text: 'Outset', value: 'outset' }, { text: 'None', value: 'none' }, { text: 'Hidden', value: 'hidden' } ] } ] } ] }); const AdvTab = { makeTab: makeTab$2 }; const collect = (editor) => { const urlListSanitizer = ListUtils.sanitizer((item) => editor.convertURL(item.value || item.url || '', 'src')); const futureImageList = new Promise((completer) => { createImageList(editor, (imageList) => { completer(urlListSanitizer(imageList).map((items) => flatten([ [{ text: 'None', value: '' }], items ]))); }); }); const classList = ListUtils.sanitize(getClassList(editor)); const hasAdvTab$1 = hasAdvTab(editor); const hasUploadTab$1 = hasUploadTab(editor); const hasUploadUrl$1 = hasUploadUrl(editor); const hasUploadHandler$1 = hasUploadHandler(editor); const image = readImageDataFromSelection(editor); const hasDescription$1 = hasDescription(editor); const hasImageTitle$1 = hasImageTitle(editor); const hasDimensions$1 = hasDimensions(editor); const hasImageCaption$1 = hasImageCaption(editor); const hasAccessibilityOptions = showAccessibilityOptions(editor); const automaticUploads = isAutomaticUploadsEnabled(editor); const prependURL = Optional.some(getPrependUrl(editor)).filter((preUrl) => isString(preUrl) && preUrl.length > 0); return futureImageList.then((imageList) => ({ image, imageList, classList, hasAdvTab: hasAdvTab$1, hasUploadTab: hasUploadTab$1, hasUploadUrl: hasUploadUrl$1, hasUploadHandler: hasUploadHandler$1, hasDescription: hasDescription$1, hasImageTitle: hasImageTitle$1, hasDimensions: hasDimensions$1, hasImageCaption: hasImageCaption$1, prependURL, hasAccessibilityOptions, automaticUploads })); }; const makeItems = (info) => { const imageUrl = { name: 'src', type: 'urlinput', filetype: 'image', label: 'Source', picker_text: 'Browse files' }; const imageList = info.imageList.map((items) => ({ name: 'images', type: 'listbox', label: 'Image list', items })); const imageDescription = { name: 'alt', type: 'input', label: 'Alternative description', enabled: !(info.hasAccessibilityOptions && info.image.isDecorative) }; const imageTitle = { name: 'title', type: 'input', label: 'Image title' }; const imageDimensions = { name: 'dimensions', type: 'sizeinput' }; const isDecorative = { type: 'label', label: 'Accessibility', items: [{ name: 'isDecorative', type: 'checkbox', label: 'Image is decorative' }] }; // TODO: the original listbox supported styled items but bridge does not seem to support this const classList = info.classList.map((items) => ({ name: 'classes', type: 'listbox', label: 'Class', items })); const caption = { type: 'label', label: 'Caption', items: [ { type: 'checkbox', name: 'caption', label: 'Show caption' } ] }; const getDialogContainerType = (useColumns) => useColumns ? { type: 'grid', columns: 2 } : { type: 'panel' }; return flatten([ [imageUrl], imageList.toArray(), info.hasAccessibilityOptions && info.hasDescription ? [isDecorative] : [], info.hasDescription ? [imageDescription] : [], info.hasImageTitle ? [imageTitle] : [], info.hasDimensions ? [imageDimensions] : [], [{ ...getDialogContainerType(info.classList.isSome() && info.hasImageCaption), items: flatten([ classList.toArray(), info.hasImageCaption ? [caption] : [] ]) }] ]); }; const makeTab$1 = (info) => ({ title: 'General', name: 'general', items: makeItems(info) }); const MainTab = { makeTab: makeTab$1, makeItems }; const makeTab = (_info) => { const items = [ { type: 'dropzone', name: 'fileinput' } ]; return { title: 'Upload', name: 'upload', items }; }; const UploadTab = { makeTab }; const createState = (info) => ({ prevImage: ListUtils.findEntry(info.imageList, info.image.src), prevAlt: info.image.alt, open: true }); const fromImageData = (image) => ({ src: { value: image.src, meta: {} }, images: image.src, alt: image.alt, title: image.title, dimensions: { width: image.width, height: image.height }, classes: image.class, caption: image.caption, style: image.style, vspace: image.vspace, border: image.border, hspace: image.hspace, borderstyle: image.borderStyle, fileinput: [], isDecorative: image.isDecorative }); const toImageData = (data, removeEmptyAlt) => ({ src: data.src.value, alt: (data.alt === null || data.alt.length === 0) && removeEmptyAlt ? null : data.alt, title: data.title, width: data.dimensions.width, height: data.dimensions.height, class: data.classes, style: data.style, caption: data.caption, hspace: data.hspace, vspace: data.vspace, border: data.border, borderStyle: data.borderstyle, isDecorative: data.isDecorative }); const addPrependUrl2 = (info, srcURL) => { // Add the prependURL if (!/^(?:[a-zA-Z]+:)?\/\//.test(srcURL)) { return info.prependURL.bind((prependUrl) => { if (srcURL.substring(0, prependUrl.length) !== prependUrl) { return Optional.some(prependUrl + srcURL); } return Optional.none(); }); } return Optional.none(); }; const addPrependUrl = (info, api) => { const data = api.getData(); addPrependUrl2(info, data.src.value).each((srcURL) => { api.setData({ src: { value: srcURL, meta: data.src.meta } }); }); }; const formFillFromMeta2 = (info, data, meta) => { if (info.hasDescription && isString(meta.alt)) { data.alt = meta.alt; } if (info.hasAccessibilityOptions) { data.isDecorative = meta.isDecor