/*! * FilePondPluginFilePoster 2.5.1 * Licensed under MIT, https://opensource.org/licenses/MIT/ * Please visit https://pqina.nl/filepond/ for details. */ /* eslint-disable */ const IMAGE_SCALE_SPRING_PROPS = { type: 'spring', stiffness: 0.5, damping: 0.45, mass: 10 }; const createPosterView = _ => _.utils.createView({ name: 'file-poster', tag: 'div', ignoreRect: true, create: ({ root }) => { root.ref.image = document.createElement('img'); root.element.appendChild(root.ref.image); }, write: _.utils.createRoute({ DID_FILE_POSTER_LOAD: ({ root, props }) => { const { id } = props; // get item const item = root.query('GET_ITEM', { id: props.id }); if (!item) return; // get poster const poster = item.getMetadata('poster'); root.ref.image.src = poster; // let others know of our fabulous achievement (so the image can be faded in) root.dispatch('DID_FILE_POSTER_DRAW', { id }); } }), mixins: { styles: ['scaleX', 'scaleY', 'opacity'], animations: { scaleX: IMAGE_SCALE_SPRING_PROPS, scaleY: IMAGE_SCALE_SPRING_PROPS, opacity: { type: 'tween', duration: 750 } } } }); const applyTemplate = (source, target) => { // copy width and height target.width = source.width; target.height = source.height; // draw the template const ctx = target.getContext('2d'); ctx.drawImage(source, 0, 0); }; const createPosterOverlayView = fpAPI => fpAPI.utils.createView({ name: 'file-poster-overlay', tag: 'canvas', ignoreRect: true, create: ({ root, props }) => { applyTemplate(props.template, root.element); }, mixins: { styles: ['opacity'], animations: { opacity: { type: 'spring', mass: 25 } } } }); const getImageSize = (url, cb) => { let image = new Image(); image.onload = () => { const width = image.naturalWidth; const height = image.naturalHeight; image = null; cb(width, height); }; image.src = url; }; const easeInOutSine = t => -0.5 * (Math.cos(Math.PI * t) - 1); const addGradientSteps = ( gradient, color, alpha = 1, easeFn = easeInOutSine, steps = 10, offset = 0 ) => { const range = 1 - offset; const rgb = color.join(','); for (let i = 0; i <= steps; i++) { const p = i / steps; const stop = offset + range * p; gradient.addColorStop(stop, `rgba(${rgb}, ${easeFn(p) * alpha})`); } }; const MAX_WIDTH = 10; const MAX_HEIGHT = 10; const calculateAverageColor = image => { const scalar = Math.min(MAX_WIDTH / image.width, MAX_HEIGHT / image.height); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const width = (canvas.width = Math.ceil(image.width * scalar)); const height = (canvas.height = Math.ceil(image.height * scalar)); ctx.drawImage(image, 0, 0, width, height); let data = null; try { data = ctx.getImageData(0, 0, width, height).data; } catch (e) { return null; } const l = data.length; let r = 0; let g = 0; let b = 0; let i = 0; for (; i < l; i += 4) { r += data[i] * data[i]; g += data[i + 1] * data[i + 1]; b += data[i + 2] * data[i + 2]; } r = averageColor(r, l); g = averageColor(g, l); b = averageColor(b, l); return { r, g, b }; }; const averageColor = (c, l) => Math.floor(Math.sqrt(c / (l / 4))); const drawTemplate = (canvas, width, height, color, alphaTarget) => { canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); const horizontalCenter = width * 0.5; const grad = ctx.createRadialGradient( horizontalCenter, height + 110, height - 100, horizontalCenter, height + 110, height + 100 ); addGradientSteps(grad, color, alphaTarget, undefined, 8, 0.4); ctx.save(); ctx.translate(-width * 0.5, 0); ctx.scale(2, 1); ctx.fillStyle = grad; ctx.fillRect(0, 0, width, height); ctx.restore(); }; const hasNavigator = typeof navigator !== 'undefined'; const width = 500; const height = 200; const overlayTemplateShadow = hasNavigator && document.createElement('canvas'); const overlayTemplateError = hasNavigator && document.createElement('canvas'); const overlayTemplateSuccess = hasNavigator && document.createElement('canvas'); let itemShadowColor = [40, 40, 40]; let itemErrorColor = [196, 78, 71]; let itemSuccessColor = [54, 151, 99]; if (hasNavigator) { drawTemplate(overlayTemplateShadow, width, height, itemShadowColor, 0.85); drawTemplate(overlayTemplateError, width, height, itemErrorColor, 1); drawTemplate(overlayTemplateSuccess, width, height, itemSuccessColor, 1); } const loadImage = (url, crossOriginValue) => new Promise((resolve, reject) => { const img = new Image(); if (typeof crossOrigin === 'string') { img.crossOrigin = crossOriginValue; } img.onload = () => { resolve(img); }; img.onerror = e => { reject(e); }; img.src = url; }); const createPosterWrapperView = _ => { // create overlay view const overlay = createPosterOverlayView(_); /** * Write handler for when preview container has been created */ const didCreatePreviewContainer = ({ root, props }) => { const { id } = props; // we need to get the file data to determine the eventual image size const item = root.query('GET_ITEM', id); if (!item) return; // get url to file const fileURL = item.getMetadata('poster'); // image is now ready const previewImageLoaded = data => { // calculate average image color, is in try catch to circumvent any cors errors const averageColor = root.query( 'GET_FILE_POSTER_CALCULATE_AVERAGE_IMAGE_COLOR' ) ? calculateAverageColor(data) : null; item.setMetadata('color', averageColor, true); // the preview is now ready to be drawn root.dispatch('DID_FILE_POSTER_LOAD', { id, data }); }; // determine image size of this item getImageSize(fileURL, (width, height) => { // we can now scale the panel to the final size root.dispatch('DID_FILE_POSTER_CALCULATE_SIZE', { id, width, height }); // create fallback preview loadImage( fileURL, root.query('GET_FILE_POSTER_CROSS_ORIGIN_ATTRIBUTE_VALUE') ).then(previewImageLoaded); }); }; /** * Write handler for when the preview has been loaded */ const didLoadPreview = ({ root }) => { root.ref.overlayShadow.opacity = 1; }; /** * Write handler for when the preview image is ready to be animated */ const didDrawPreview = ({ root }) => { const { image } = root.ref; // reveal image image.scaleX = 1.0; image.scaleY = 1.0; image.opacity = 1; }; /** * Write handler for when the preview has been loaded */ const restoreOverlay = ({ root }) => { root.ref.overlayShadow.opacity = 1; root.ref.overlayError.opacity = 0; root.ref.overlaySuccess.opacity = 0; }; const didThrowError = ({ root }) => { root.ref.overlayShadow.opacity = 0.25; root.ref.overlayError.opacity = 1; }; const didCompleteProcessing = ({ root }) => { root.ref.overlayShadow.opacity = 0.25; root.ref.overlaySuccess.opacity = 1; }; /** * Constructor */ const create = ({ root, props }) => { // test if colors aren't default item overlay colors const itemShadowColorProp = root.query( 'GET_FILE_POSTER_ITEM_OVERLAY_SHADOW_COLOR' ); const itemErrorColorProp = root.query( 'GET_FILE_POSTER_ITEM_OVERLAY_ERROR_COLOR' ); const itemSuccessColorProp = root.query( 'GET_FILE_POSTER_ITEM_OVERLAY_SUCCESS_COLOR' ); if (itemShadowColorProp && itemShadowColorProp !== itemShadowColor) { itemShadowColor = itemShadowColorProp; drawTemplate(overlayTemplateShadow, width, height, itemShadowColor, 0.85); } if (itemErrorColorProp && itemErrorColorProp !== itemErrorColor) { itemErrorColor = itemErrorColorProp; drawTemplate(overlayTemplateError, width, height, itemErrorColor, 1); } if (itemSuccessColorProp && itemSuccessColorProp !== itemSuccessColor) { itemSuccessColor = itemSuccessColorProp; drawTemplate(overlayTemplateSuccess, width, height, itemSuccessColor, 1); } // image view const image = createPosterView(_); // append image presenter root.ref.image = root.appendChildView( root.createChildView(image, { id: props.id, scaleX: 1.25, scaleY: 1.25, opacity: 0 }) ); // image overlays root.ref.overlayShadow = root.appendChildView( root.createChildView(overlay, { template: overlayTemplateShadow, opacity: 0 }) ); root.ref.overlaySuccess = root.appendChildView( root.createChildView(overlay, { template: overlayTemplateSuccess, opacity: 0 }) ); root.ref.overlayError = root.appendChildView( root.createChildView(overlay, { template: overlayTemplateError, opacity: 0 }) ); }; return _.utils.createView({ name: 'file-poster-wrapper', create, write: _.utils.createRoute({ // image preview stated DID_FILE_POSTER_LOAD: didLoadPreview, DID_FILE_POSTER_DRAW: didDrawPreview, DID_FILE_POSTER_CONTAINER_CREATE: didCreatePreviewContainer, // file states DID_THROW_ITEM_LOAD_ERROR: didThrowError, DID_THROW_ITEM_PROCESSING_ERROR: didThrowError, DID_THROW_ITEM_INVALID: didThrowError, DID_COMPLETE_ITEM_PROCESSING: didCompleteProcessing, DID_START_ITEM_PROCESSING: restoreOverlay, DID_REVERT_ITEM_PROCESSING: restoreOverlay }) }); }; /** * File Poster Plugin */ const plugin = fpAPI => { const { addFilter, utils } = fpAPI; const { Type, createRoute } = utils; // filePosterView const filePosterView = createPosterWrapperView(fpAPI); // 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; // only hook up to item view and only if is enabled for this cropper if (!is('file') || !query('GET_ALLOW_FILE_POSTER')) return; // create the file poster plugin, but only do so if the item is an image const didLoadItem = ({ root, props }) => { updateItemPoster(root, props); }; const didUpdateItemMetadata = ({ root, props, action }) => { if (!/poster/.test(action.change.key)) return; updateItemPoster(root, props); }; const updateItemPoster = (root, props) => { const { id } = props; const item = query('GET_ITEM', id); // item could theoretically have been removed in the mean time if (!item || !item.getMetadata('poster') || item.archived) return; // don't update if is the same poster if (root.ref.previousPoster === item.getMetadata('poster')) return; root.ref.previousPoster = item.getMetadata('poster'); // test if is filtered if (!query('GET_FILE_POSTER_FILTER_ITEM')(item)) return; if (root.ref.filePoster) { view.removeChildView(root.ref.filePoster); } // set preview view root.ref.filePoster = view.appendChildView( view.createChildView(filePosterView, { id }) ); // now ready root.dispatch('DID_FILE_POSTER_CONTAINER_CREATE', { id }); }; const didCalculatePreviewSize = ({ root, action }) => { // no poster set if (!root.ref.filePoster) return; // remember dimensions root.ref.imageWidth = action.width; root.ref.imageHeight = action.height; root.ref.shouldUpdatePanelHeight = true; root.dispatch('KICK'); }; const getPosterHeight = ({ root }) => { let fixedPosterHeight = root.query('GET_FILE_POSTER_HEIGHT'); // if fixed height: return fixed immediately if (fixedPosterHeight) { return fixedPosterHeight; } const minPosterHeight = root.query('GET_FILE_POSTER_MIN_HEIGHT'); const maxPosterHeight = root.query('GET_FILE_POSTER_MAX_HEIGHT'); // if natural height is smaller than minHeight: return min height if (minPosterHeight && root.ref.imageHeight < minPosterHeight) { return minPosterHeight; } let height = root.rect.element.width * (root.ref.imageHeight / root.ref.imageWidth); if (minPosterHeight && height < minPosterHeight) { return minPosterHeight; } if (maxPosterHeight && height > maxPosterHeight) { return maxPosterHeight; } return height; }; // start writing view.registerWriter( createRoute( { DID_LOAD_ITEM: didLoadItem, DID_FILE_POSTER_CALCULATE_SIZE: didCalculatePreviewSize, DID_UPDATE_ITEM_METADATA: didUpdateItemMetadata }, ({ root, props }) => { // don't run without poster if (!root.ref.filePoster) return; // don't do anything while hidden if (root.rect.element.hidden) return; // should we redraw if (root.ref.shouldUpdatePanelHeight) { // time to resize the parent panel root.dispatch('DID_UPDATE_PANEL_HEIGHT', { id: props.id, height: getPosterHeight({ root }) }); // done! root.ref.shouldUpdatePanelHeight = false; } } ) ); }); // expose plugin return { options: { // Enable or disable file poster allowFilePoster: [true, Type.BOOLEAN], // Fixed preview height filePosterHeight: [null, Type.INT], // Min image height filePosterMinHeight: [null, Type.INT], // Max image height filePosterMaxHeight: [null, Type.INT], // filters file items to determine which are shown as poster filePosterFilterItem: [() => true, Type.FUNCTION], // Enables or disables reading average image color filePosterCalculateAverageImageColor: [false, Type.BOOLEAN], // Allows setting the value of the CORS attribute (null is don't set attribute) filePosterCrossOriginAttributeValue: ['Anonymous', Type.STRING], // Colors used for item overlay gradient filePosterItemOverlayShadowColor: [null, Type.ARRAY], filePosterItemOverlayErrorColor: [null, Type.ARRAY], filePosterItemOverlaySuccessColor: [null, Type.ARRAY] } }; }; // fire pluginloaded event if running in browser, this allows registering the plugin when using async script tags const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; if (isBrowser) { document.dispatchEvent( new CustomEvent('FilePond:pluginloaded', { detail: plugin }) ); } export default plugin;