/*! * Pintura Image Editor 8.13.1 * (c) 2018-2021 PQINA Inc. - All Rights Reserved * License: https://pqina.nl/pintura/license/ */ /* eslint-disable */ var isFile = (v) => v instanceof File; var isImage = (file) => /^image/.test(file.type); var isString = (v) => typeof v === 'string'; let isSafari = null; var isSafari$1 = () => { if (isSafari === null) isSafari = isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); return isSafari; }; var getImageElementSize = (imageElement) => new Promise((resolve, reject) => { let shouldAutoRemove = false; // test if image is attached to DOM, if not attached, attach so measurement is correct on Safari if (!imageElement.parentNode && isSafari$1()) { shouldAutoRemove = true; // has width 0 and height 0 to prevent rendering very big SVGs (without width and height) that will for one frame overflow the window and show a scrollbar imageElement.style.cssText = `position:absolute;visibility:hidden;pointer-events:none;left:0;top:0;width:0;height:0;`; document.body.appendChild(imageElement); } // start testing size const measure = () => { const width = imageElement.naturalWidth; const height = imageElement.naturalHeight; const hasSize = width && height; if (!hasSize) return; // clean up image if was attached for measuring if (shouldAutoRemove) imageElement.parentNode.removeChild(imageElement); clearInterval(intervalId); resolve({ width, height }); }; imageElement.onerror = (err) => { clearInterval(intervalId); reject(err); }; const intervalId = setInterval(measure, 1); measure(); }); var getImageSize = async (image) => { // the image element we'll use to load the image let imageElement = image; // if is not an image element, it must be a valid image source if (!imageElement.src) { imageElement = new Image(); imageElement.src = isString(image) ? image : URL.createObjectURL(image); } let size; try { size = await getImageElementSize(imageElement); } finally { isFile(image) && URL.revokeObjectURL(imageElement.src); } return size; }; var isBlob = (v) => v instanceof Blob && !(v instanceof File); var noop = (...args) => { }; let result$3 = null; var isBrowser = () => { if (result$3 === null) result$3 = typeof window !== 'undefined' && typeof window.document !== 'undefined'; return result$3; }; var h = (name, attributes, children = []) => { const el = document.createElement(name); // @ts-ignore const descriptors = Object.getOwnPropertyDescriptors(el.__proto__); for (const key in attributes) { if (key === 'style') { el.style.cssText = attributes[key]; } else if ((descriptors[key] && descriptors[key].set) || /textContent|innerHTML/.test(key) || typeof attributes[key] === 'function') { el[key] = attributes[key]; } else { el.setAttribute(key, attributes[key]); } } children.forEach((child) => el.appendChild(child)); return el; }; var releaseCanvas = (canvas) => { canvas.width = 1; canvas.height = 1; const ctx = canvas.getContext('2d'); ctx && ctx.clearRect(0, 0, 1, 1); }; let result$2 = null; var supportsWebGL2 = () => { if (result$2 === null) { if ('WebGL2RenderingContext' in window) { let canvas; try { canvas = h('canvas'); result$2 = !!canvas.getContext('webgl2'); } catch (err) { result$2 = false; } canvas && releaseCanvas(canvas); } else { result$2 = false; } } return result$2; }; var getWebGLContext = (canvas, attrs) => { if (supportsWebGL2()) return canvas.getContext('webgl2', attrs); return (canvas.getContext('webgl', attrs) || canvas.getContext('experimental-webgl', attrs)); }; let result$1 = null; var supportsWebGL = () => { if (result$1 === null) { const canvas = h('canvas'); result$1 = !!getWebGLContext(canvas); releaseCanvas(canvas); } return result$1; }; const isOperaMini = () => Object.prototype.toString.call(window['operamini']) === '[object OperaMini]'; const hasPromises = () => 'Promise' in window; const hasCreateObjectURL = () => 'URL' in window && 'createObjectURL' in window.URL; const hasVisibility = () => 'visibilityState' in document; const hasTiming = () => 'performance' in window; // iOS 8.x const hasFileConstructor = () => 'File' in window; // excludes IE11 let result = null; var isModernBrowser = () => { if (result === null) result = isBrowser() && // Can't run on Opera Mini due to lack of everything !isOperaMini() && // Require these APIs to feature detect a modern browser hasVisibility() && hasPromises() && hasFileConstructor() && hasCreateObjectURL() && hasTiming(); return result; }; /** * Image Edit Proxy Plugin */ const plugin = (_) => { const { addFilter, utils, views } = _; const { Type, createRoute } = utils; const { fileActionButton } = views; const getEditorSafe = (editor) => (editor === null ? {} : editor); addFilter('SHOULD_REMOVE_ON_REVERT', (shouldRemove, { item, query }) => new Promise((resolve) => { const { file } = item; // if this file is editable it shouldn't be removed immidiately even when instant uploading const canEdit = query('GET_ALLOW_IMAGE_EDITOR') && query('GET_IMAGE_EDITOR_ALLOW_EDIT') && query('GET_IMAGE_EDITOR_SUPPORT_EDIT') && query('GET_IMAGE_EDITOR_SUPPORT_IMAGE')(file); // if the file cannot be edited it should be removed on revert resolve(!canEdit); })); // open editor when loading a new item addFilter('DID_LOAD_ITEM', (item, { query, dispatch }) => new Promise((resolve, reject) => { // if is temp or local file if (item.origin > 1) { resolve(item); return; } // get file reference const { file } = item; if (!query('GET_ALLOW_IMAGE_EDITOR') || !query('GET_IMAGE_EDITOR_INSTANT_EDIT' )) return resolve(item); // exit if this is not an image if (!query('GET_IMAGE_EDITOR_SUPPORT_IMAGE')(file)) return resolve(item); // request editing of a file const requestEdit = () => { if (!editRequestQueue.length) return; const { item, resolve, reject } = editRequestQueue[0]; dispatch('EDIT_ITEM', { id: item.id, handleEditorResponse: createEditorResponseHandler(item, resolve, reject), }); }; // is called when the user confirms editing the file const createEditorResponseHandler = (item, resolve, reject) => (userDidConfirm) => { // remove item editRequestQueue.shift(); // handle item if (userDidConfirm) { resolve(item); } else { reject(item); } // TODO: Fix, should not be needed to kick the internal loop in case no processes are running dispatch('KICK'); // handle next item! requestEdit(); }; queueEditRequest({ item, resolve, reject }); if (editRequestQueue.length === 1) { requestEdit(); } })); // extend item methods addFilter('DID_CREATE_ITEM', (item, { query, dispatch }) => { item.extend('edit', () => { dispatch('EDIT_ITEM', { id: item.id }); }); }); const editRequestQueue = []; const queueEditRequest = (editRequest) => { editRequestQueue.push(editRequest); return editRequest; }; const couldTransformFile = (query) => { // no editor defined, then exit const { imageProcessor, imageReader, imageWriter } = getEditorSafe(query('GET_IMAGE_EDITOR')); return (query('GET_IMAGE_EDITOR_WRITE_IMAGE') && query('GET_IMAGE_EDITOR_SUPPORT_WRITE_IMAGE') && imageProcessor && imageReader && imageWriter); }; // called for each view that is created right after the 'create' method addFilter('CREATE_VIEW', (viewAPI) => { // get reference to created view const { is, view, query } = viewAPI; if (!query('GET_ALLOW_IMAGE_EDITOR')) return; if (!query('GET_IMAGE_EDITOR_SUPPORT_WRITE_IMAGE')) return; const supportsFilePoster = query('GET_ALLOW_FILE_POSTER'); // only run for either the file or the file info panel const shouldExtendView = (is('file-info') && !supportsFilePoster) || (is('file') && supportsFilePoster); if (!shouldExtendView) return; // no editor defined, then exit const { createEditor, imageProcessor, imageReader, imageWriter, editorOptions, legacyDataToImageState, imageState: imageBaseState, } = getEditorSafe(query('GET_IMAGE_EDITOR')); if (!imageReader || !imageWriter || !editorOptions || !editorOptions.locale) return; // remove default image reader and writer if set delete editorOptions.imageReader; delete editorOptions.imageWriter; const [createImageReader, imageReaderOptions] = imageReader; const [createImageWriter = noop, imageWriterOptions] = imageWriter; // tests if file item has poster const getItemByProps = (props) => { const { id } = props; const item = query('GET_ITEM', id); return item; }; const hasPoster = (root, props) => { if (!query('GET_ALLOW_FILE_POSTER')) return false; const item = getItemByProps(props); if (!item) return; // test if is filtered if (!query('GET_FILE_POSTER_FILTER_ITEM')(item)) return false; const poster = item.getMetadata('poster'); return !!poster; }; // generate preview const getPosterTargetSize = (root, targetSize) => { const posterHeight = root.query('GET_FILE_POSTER_HEIGHT'); const maxPosterHeight = root.query('GET_FILE_POSTER_MAX_HEIGHT'); if (posterHeight) { targetSize.width = posterHeight * 2; targetSize.height = posterHeight * 2; } else if (maxPosterHeight) { targetSize.width = maxPosterHeight * 2; targetSize.height = maxPosterHeight * 2; } return targetSize; }; const createEditorOptions = (root) => { const targetSize = getPosterTargetSize(root, { width: 512, height: 512, }); return { ...editorOptions, imageReader: createImageReader(imageReaderOptions), imageWriter: createImageWriter({ // poster size targetSize, // can optionally overwrite poster size ...(imageWriterOptions || {}), }), }; }; const createImagePoster = ({ root, props }) => { // need image processor to create image poster if (!imageProcessor) return; const item = getItemByProps(props); if (!item) return; const file = item.file; const imageState = item.getMetadata('imageState'); const options = { ...createEditorOptions(root), imageState: { ...imageBaseState, ...imageState, }, }; imageProcessor(file, options).then(({ dest }) => { item.setMetadata('poster', URL.createObjectURL(dest), true); }); }; // opens the editor, if it does not already exist, it creates the editor const openImageEditor = ({ root, props, action }) => { const { handleEditorResponse } = action; // get item const item = getItemByProps(props); // file to open const file = item.file; // open the editor (sets editor properties and imageState property) const editor = createEditor({ ...createEditorOptions(root), src: file, }); // when the image has loaded, update the editor editor.on('load', ({ size }) => { // get current image edit state let imageState = item.getMetadata('imageState'); imageState = legacyDataToImageState ? legacyDataToImageState(editor, size, imageState) : imageState; // update editor view based on image edit state editor.imageState = { ...imageBaseState, ...imageState, }; }); editor.on('process', ({ imageState, dest }) => { // if already has post URL, try to revoke const poster = item.getMetadata('poster'); poster && URL.revokeObjectURL(item.getMetadata('poster')); // store state, two seperate actions because we want to trigger preparefile when setting `imageState` item.setMetadata('poster', URL.createObjectURL(dest)); item.setMetadata('imageState', imageState); // used in instant edit mode if (!handleEditorResponse) return; handleEditorResponse(true); }); editor.on('close', () => { // used in instant edit mode if (!handleEditorResponse) return; handleEditorResponse(false); }); }; //#region view const didLoadItem = ({ root, props }) => { const { id } = props; // try to access item const item = query('GET_ITEM', id); if (!item) return; // get the file object const file = item.file; // exit if this is not an image if (!query('GET_IMAGE_EDITOR_SUPPORT_IMAGE')(file)) return; if (query('GET_ALLOW_FILE_POSTER') && !item.getMetadata('poster')) { root.dispatch('REQUEST_CREATE_IMAGE_POSTER', { id }); } if (!query('GET_IMAGE_EDITOR_ALLOW_EDIT') || !query('GET_IMAGE_EDITOR_SUPPORT_EDIT')) return; // handle interactions root.ref.handleEdit = (e) => { e.stopPropagation(); root.dispatch('EDIT_ITEM', { id }); }; updateEditButton(root, props); }; const updateEditButton = (root, props) => { root.ref.buttonEditItem && root.removeChildView(root.ref.buttonEditItem); if (root.ref.editButton && root.ref.editButton.parentNode) { root.ref.editButton.parentNode.removeChild(root.ref.editButton); } if (hasPoster(root, props)) { // add edit button to preview const buttonView = view.createChildView(fileActionButton, { label: 'edit', icon: query('GET_IMAGE_EDITOR_ICON_EDIT'), opacity: 0, }); // edit item classname buttonView.element.classList.add('filepond--action-edit-item'); buttonView.element.dataset.align = query('GET_STYLE_IMAGE_EDITOR_BUTTON_EDIT_ITEM_POSITION'); buttonView.on('click', root.ref.handleEdit); root.ref.buttonEditItem = view.appendChildView(buttonView); } else { // view is file info const filenameElement = view.element.querySelector('.filepond--file-info-main'); const editButton = document.createElement('button'); editButton.className = 'filepond--action-edit-item-alt'; editButton.innerHTML = query('GET_IMAGE_EDITOR_ICON_EDIT') + 'edit'; editButton.addEventListener('click', root.ref.handleEdit); filenameElement.appendChild(editButton); root.ref.editButton = editButton; } }; const didUpdateItemMetadata = ({ root, props, action }) => { if (!/poster/.test(action.change.key)) return; if (!query('GET_IMAGE_EDITOR_ALLOW_EDIT') || !query('GET_IMAGE_EDITOR_SUPPORT_EDIT')) return; updateEditButton(root, props); }; view.registerDestroyer(({ root }) => { if (root.ref.buttonEditItem) { root.ref.buttonEditItem.off('click', root.ref.handleEdit); } if (root.ref.editButton) { root.ref.editButton.removeEventListener('click', root.ref.handleEdit); } }); const routes = { EDIT_ITEM: openImageEditor, DID_LOAD_ITEM: didLoadItem, DID_UPDATE_ITEM_METADATA: didUpdateItemMetadata, DID_REMOVE_ITEM: ({ props }) => { const { id } = props; const item = query('GET_ITEM', id); if (!item) return; const poster = item.getMetadata('poster'); poster && URL.revokeObjectURL(poster); }, REQUEST_CREATE_IMAGE_POSTER: createImagePoster, DID_FILE_POSTER_LOAD: undefined, }; if (supportsFilePoster) { // view is file const didPosterUpdate = ({ root }) => { if (!root.ref.buttonEditItem) return; root.ref.buttonEditItem.opacity = 1; }; routes.DID_FILE_POSTER_LOAD = didPosterUpdate; } //#endregion // start writing view.registerWriter(createRoute(routes)); }); //#region write image addFilter('SHOULD_PREPARE_OUTPUT', (shouldPrepareOutput, { query, change, item }) => new Promise((resolve) => { if (!query('GET_IMAGE_EDITOR_SUPPORT_IMAGE')(item.file)) return resolve(false); if (change && !/imageState/.test(change.key)) return resolve(false); resolve(!query('IS_ASYNC')); })); const shouldTransformFile = (query, file, item) => new Promise((resolve) => { // invalid item if (!couldTransformFile(query) || item.archived || (!isFile(file) && !isBlob(file)) || !query('GET_IMAGE_EDITOR_SUPPORT_IMAGE')(file)) { return resolve(false); } // if size can't be read this browser doesn't support image getImageSize(file) .then(() => { const fn = query('GET_IMAGE_TRANSFORM_IMAGE_FILTER'); if (fn) { const filterResult = fn(file); if (typeof filterResult === 'boolean') { return resolve(filterResult); } if (typeof filterResult.then === 'function') { return filterResult.then(resolve); } } resolve(true); }) .catch(() => { resolve(false); }); }); // subscribe to file transformations addFilter('PREPARE_OUTPUT', (file, { query, item }) => { const writeOutputImage = (file) => new Promise((resolve, reject) => { const imageState = item.getMetadata('imageState'); // no editor defined, then exit const { imageProcessor, imageReader, imageWriter, editorOptions, imageState: imageBaseState, } = getEditorSafe(query('GET_IMAGE_EDITOR')); if (!imageProcessor || !imageReader || !imageWriter || !editorOptions) return; const [createImageReader, imageReaderOptions] = imageReader; const [createImageWriter = noop, imageWriterOptions] = imageWriter; imageProcessor(file, { ...editorOptions, imageReader: createImageReader(imageReaderOptions), imageWriter: createImageWriter(imageWriterOptions), imageState: { ...imageBaseState, ...imageState, }, }) .then(resolve) .catch(reject); }); return new Promise((resolve) => { shouldTransformFile(query, file, item).then((shouldWrite) => { if (!shouldWrite) return resolve(file); writeOutputImage(file).then((res) => { const afterFn = query('GET_IMAGE_EDITOR_AFTER_WRITE_IMAGE'); if (afterFn) return afterFn(res).then(resolve); // @ts-ignore resolve(res.dest); }); }); }); }); //#endregion // Expose plugin options return { options: { // enable or disable image editing allowImageEditor: [true, Type.BOOLEAN], // open editor when image is dropped imageEditorInstantEdit: [false, Type.BOOLEAN], // allow editing imageEditorAllowEdit: [true, Type.BOOLEAN], // cannot edit if no WebGL or is <=IE11 imageEditorSupportEdit: [ isBrowser() && isModernBrowser() && supportsWebGL(), Type.BOOLEAN, ], // receives file and should return true if can edit imageEditorSupportImage: [isImage, Type.FUNCTION], // cannot write if is <= IE11 imageEditorSupportWriteImage: [isModernBrowser(), Type.BOOLEAN], // should output image imageEditorWriteImage: [true, Type.BOOLEAN], // receives written image and can return single or more images imageEditorAfterWriteImage: [undefined, Type.FUNCTION], // editor object imageEditor: [null, Type.OBJECT], // the icon to use for the edit button imageEditorIconEdit: [ '', Type.STRING, ], // location of processing button styleImageEditorButtonEditItemPosition: ['bottom center', Type.STRING], }, }; }; // fire pluginloaded event if running in browser, this allows registering the plugin when using async script tags if (isBrowser()) document.dispatchEvent(new CustomEvent('FilePond:pluginloaded', { detail: plugin })); export default plugin;