UNPKG

@xuda.io/xuda-widget-plugin-xuda-drive

Version:

Xuda Drive widget plugin

736 lines (713 loc) 28.7 kB
/*! * Pintura Image Editor 8.51.10 * (c) 2018-2023 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 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; }; let result$4 = null; var isBrowser = () => { if (result$4 === null) result$4 = typeof window !== 'undefined' && typeof window.document !== 'undefined'; return result$4; }; // @ts-ignore const supportsReplaceChildren = isBrowser() && !!Node.prototype.replaceChildren; const fn = supportsReplaceChildren ? // @ts-ignore (parent, newChildren) => parent.replaceChildren(newChildren) : (parent, newChildren) => { while (parent.lastChild) { parent.removeChild(parent.lastChild); } if (newChildren !== undefined) { parent.append(newChildren); } }; const container = isBrowser() && h('div', { class: 'PinturaMeasure', style: 'position:absolute;left:0;top:0;width:99999px;height:0;pointer-events:none;contain:strict;margin:0;padding:0;', }); let timeoutId; var appendForMeasuring = (element) => { // replace element children with this child fn(container, element); // append to DOM if not in it atm if (!container.parentNode) document.body.append(container); // auto detach from DOM after it isn't used for a little while clearTimeout(timeoutId); timeoutId = setTimeout(() => { container.remove(); }, 500); // return added element for measuring return element; }; let result$3 = null; var isSafari = () => { if (result$3 === null) result$3 = isBrowser() && /^((?!chrome|android).)*(safari|iphone|ipad)/i.test(navigator.userAgent); // added |iphone|ipad as safari is not present in WebView on React Native return result$3; }; 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()) { 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;`; appendForMeasuring(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.remove(); clearInterval(intervalId); resolve({ width, height }); }; imageElement.onerror = (err) => { clearInterval(intervalId); reject(err); }; const intervalId = setInterval(measure, 1); measure(); }); var getVideoElementSize = (element) => new Promise((resolve, reject) => { const done = () => { resolve({ width: element.videoWidth, height: element.videoHeight, }); }; if (element.readyState >= 1) return done(); element.onloadedmetadata = done; element.onerror = reject; }); var isString = (v) => typeof v === 'string'; var getAsVideoOrImageElement = (src) => new Promise((resolve) => { // undetermined or is video const elementSrc = (isString(src) ? src : URL.createObjectURL(src)); const createImage = () => { const image = new Image(); image.src = elementSrc; resolve(image); }; // we can already know it's an image if (src instanceof Blob && isImage(src)) return createImage(); // try video first const element = document.createElement('video'); element.preload = 'metadata'; element.onloadedmetadata = () => resolve(element); // when errors we switch to image element.onerror = createImage; element.src = elementSrc; }); var isVideoElement = (element) => element.nodeName === 'VIDEO'; // TODO: video rename function var getImageSize = async (src) => { // the image element we'll use to load the image let mediaElement; // if is not an image or video element, it must be a valid image or video source if (!src['src']) { mediaElement = await getAsVideoOrImageElement(src); } else { mediaElement = src; } let size; try { size = isVideoElement(mediaElement) ? await getVideoElementSize(mediaElement) : await getImageElementSize(mediaElement); } finally { isFile(src) && URL.revokeObjectURL(mediaElement.src); } return size; }; var isBlob = (v) => v instanceof Blob && !(v instanceof File); var noop = (...args) => { }; 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); canvas = undefined; } 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) { let canvas = h('canvas'); result$1 = !!getWebGLContext(canvas); releaseCanvas(canvas); canvas = undefined; } 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 createQueue = ({ parallel = 1, autoShift = true }) => { const queue = []; let jobs = 0; const next = () => { // done if (!queue.length) return api.oncomplete(); // get next item jobs++; const job = queue.shift(); // run job(() => { jobs--; // pick up next item if (jobs < parallel) runJobs(); }); }; const runJobs = () => { for (let i = 0; i < parallel - jobs; i++) { next(); } }; const api = { queue: (cb) => { // queue job queue.push(cb); // run item immidiately autoShift && runJobs(); }, runJobs, oncomplete: () => { }, }; return api; }; const renderQueue = createQueue({ parallel: 1 }); 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); }; // generate preview const getPosterTargetSize = (query, targetSize) => { const posterHeight = query('GET_FILE_POSTER_HEIGHT'); const maxPosterHeight = 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 createImagePoster = (query, item, done = () => { }) => { if (!item) return; const { imageProcessor, imageReader, imageWriter, editorOptions, imageState: imageBaseState, } = getEditorSafe(query('GET_IMAGE_EDITOR')); // need image processor to create image poster if (!imageProcessor) return; const [createImageReader, imageReaderOptions] = imageReader; const [createImageWriter = noop, imageWriterOptions] = imageWriter; const file = item.file; const imageState = item.getMetadata('imageState'); const targetSize = getPosterTargetSize(query, { width: 512, height: 512, }); const options = { ...editorOptions, imageReader: createImageReader(imageReaderOptions), imageWriter: createImageWriter({ // can optionally overwrite poster size ...(imageWriterOptions || {}), // limit memory so poster is created quicker canvasMemoryLimit: targetSize.width * targetSize.height * 2, }), imageState: { ...imageBaseState, ...imageState, }, }; renderQueue.queue((next) => { imageProcessor(file, options).then(({ dest }) => { item.setMetadata('poster', URL.createObjectURL(dest), true); next(); done(); }); }); }; // 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, 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; // tests if file item has poster const getItemByProps = (props) => { const { id } = props; const item = query('GET_ITEM', id); return item; }; const hasPoster = (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; }; // 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({ ...editorOptions, imageReader: createImageReader(imageReaderOptions), 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 }) => { // 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; // draw edit button next to file name updateEditButton(root, props); }; const updateEditButton = (root, props) => { // handle interactions if (!root.ref.handleEdit) { root.ref.handleEdit = (e) => { e.stopPropagation(); root.dispatch('EDIT_ITEM', { id: props.id }); }; } if (hasPoster(props)) { // remove current editButton if (root.ref.editButton && root.ref.editButton.parentNode) { root.ref.editButton.parentNode.removeChild(root.ref.editButton); } // 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); } // no poster else { // remove current button if (root.ref.buttonEditItem) { root.removeChildView(root.ref.buttonEditItem); } // 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') + '<span>edit</span>'; editButton.addEventListener('click', root.ref.handleEdit); filenameElement.appendChild(editButton); root.ref.editButton = editButton; } }; //#endregion const didUpdateItemMetadata = ({ root, props, action }) => { // handle image state updates if (/imageState/.test(action.change.key) && query('GET_ALLOW_FILE_POSTER')) return root.dispatch('REQUEST_CREATE_IMAGE_POSTER', { id: props.id }); // no change to poster, skip if (!/poster/.test(action.change.key)) return; // no editor allowed so no need to show the button 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: ({ root, props }) => createImagePoster(root.query, getItemByProps(props)), 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; } // 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) => { // test if has image poster yet, if not, create poster const prepare = () => { // queue for preparing renderQueue.queue((next) => { 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) .finally(next); }); }; if (query('GET_ALLOW_FILE_POSTER') && !item.getMetadata('poster')) { // create poster createImagePoster(query, item, prepare); } else { prepare(); } }); 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 a function is defined if (afterFn) return Promise.resolve(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: [ '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><path d="M8.5 17h1.586l7-7L15.5 8.414l-7 7V17zm-1.707-2.707l8-8a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-8 8A1 1 0 0 1 10.5 19h-3a1 1 0 0 1-1-1v-3a1 1 0 0 1 .293-.707z" fill="currentColor" fill-rule="nonzero"/></svg>', 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 { plugin as default };