UNPKG

filepond

Version:

FilePond, Where files go to stretch their bits.

1,991 lines (1,667 loc) 221 kB
/*! * FilePond 4.3.7 * Licensed under MIT, https://opensource.org/licenses/MIT/ * Please visit https://pqina.nl/filepond/ for details. */ /* eslint-disable */ const isNode = value => value instanceof HTMLElement; const createStore = (initialState, queries = [], actions = []) => { // internal state const state = { ...initialState }; // contains all actions for next frame, is clear when actions are requested const actionQueue = []; const dispatchQueue = []; // returns a duplicate of the current state const getState = () => ({ ...state }); // returns a duplicate of the actions array and clears the actions array const processActionQueue = () => { // create copy of actions queue const queue = [...actionQueue]; // clear actions queue (we don't want no double actions) actionQueue.length = 0; return queue; }; // processes actions that might block the main UI thread const processDispatchQueue = () => { // create copy of actions queue const queue = [...dispatchQueue]; // clear actions queue (we don't want no double actions) dispatchQueue.length = 0; // now dispatch these actions queue.forEach(({ type, data }) => { dispatch(type, data); }); }; // adds a new action, calls its handler and const dispatch = (type, data, isBlocking) => { // is blocking action if (isBlocking) { dispatchQueue.push({ type, data }); return; } // if this action has a handler, handle the action if (actionHandlers[type]) { actionHandlers[type](data); } // now add action actionQueue.push({ type, data }); }; const query = (str, ...args) => queryHandles[str] ? queryHandles[str](...args) : null; const api = { getState, processActionQueue, processDispatchQueue, dispatch, query }; let queryHandles = {}; queries.forEach(query => { queryHandles = { ...query(state), ...queryHandles }; }); let actionHandlers = {}; actions.forEach(action => { actionHandlers = { ...action(dispatch, query, state), ...actionHandlers }; }); return api; }; const defineProperty = (obj, property, definition) => { if (typeof definition === 'function') { obj[property] = definition; return; } Object.defineProperty(obj, property, { ...definition }); }; const forin = (obj, cb) => { for (const key in obj) { if (!obj.hasOwnProperty(key)) { continue; } cb(key, obj[key]); } }; const createObject = definition => { const obj = {}; forin(definition, property => { defineProperty(obj, property, definition[property]); }); return obj; }; const attr = (node, name, value = null) => { if (value === null) { return node.getAttribute(name) || node.hasAttribute(name); } node.setAttribute(name, value); }; const ns = 'http://www.w3.org/2000/svg'; const svgElements = ['svg', 'path']; // only svg elements used const isSVGElement = tag => svgElements.includes(tag); const createElement = (tag, className, attributes = {}) => { if (typeof className === 'object') { attributes = className; className = null; } const element = isSVGElement(tag) ? document.createElementNS(ns, tag) : document.createElement(tag); if (className) { if (isSVGElement(tag)) { attr(element, 'class', className); } else { element.className = className; } } forin(attributes, (name, value) => { attr(element, name, value); }); return element; }; const appendChild = parent => (child, index) => { if (typeof index !== 'undefined' && parent.children[index]) { parent.insertBefore(child, parent.children[index]); } else { parent.appendChild(child); } }; const appendChildView = (parent, childViews) => (view, index) => { if (typeof index !== 'undefined') { childViews.splice(index, 0, view); } else { childViews.push(view); } return view; }; const removeChildView = (parent, childViews) => view => { // remove from child views childViews.splice(childViews.indexOf(view), 1); // remove the element if (view.element.parentNode) { parent.removeChild(view.element); } return view; }; const getViewRect = (elementRect, childViews, offset, scale) => { const left = offset[0] || elementRect.left; const top = offset[1] || elementRect.top; const right = left + elementRect.width; const bottom = top + elementRect.height * (scale[1] || 1); const rect = { // the rectangle of the element itself element: { ...elementRect }, // the rectangle of the element expanded to contain its children, does not include any margins inner: { left: elementRect.left, top: elementRect.top, right: elementRect.right, bottom: elementRect.bottom }, // the rectangle of the element expanded to contain its children including own margin and child margins // margins will be added after we've recalculated the size outer: { left, top, right, bottom } }; // expand rect to fit all child rectangles childViews .filter(childView => !childView.isRectIgnored()) .map(childView => childView.rect) .forEach(childViewRect => { expandRect(rect.inner, { ...childViewRect.inner }); expandRect(rect.outer, { ...childViewRect.outer }); }); // calculate inner width and height calculateRectSize(rect.inner); // append additional margin (top and left margins are included in top and left automatically) rect.outer.bottom += rect.element.marginBottom; rect.outer.right += rect.element.marginRight; // calculate outer width and height calculateRectSize(rect.outer); return rect; }; const expandRect = (parent, child) => { // adjust for parent offset child.top += parent.top; child.right += parent.left; child.bottom += parent.top; child.left += parent.left; if (child.bottom > parent.bottom) { parent.bottom = child.bottom; } if (child.right > parent.right) { parent.right = child.right; } }; const calculateRectSize = rect => { rect.width = rect.right - rect.left; rect.height = rect.bottom - rect.top; }; const isNumber = value => typeof value === 'number'; /** * Determines if position is at destination * @param position * @param destination * @param velocity * @param errorMargin * @returns {boolean} */ const thereYet = (position, destination, velocity, errorMargin = 0.001) => { return ( Math.abs(position - destination) < errorMargin && Math.abs(velocity) < errorMargin ); }; /** * Spring animation */ const spring = // default options ({ stiffness = 0.5, damping = 0.75, mass = 10 } = {}) => // method definition { let target = null; let position = null; let velocity = 0; let resting = false; // updates spring state const interpolate = () => { // in rest, don't animate if (resting) { return; } // need at least a target or position to do springy things if (!(isNumber(target) && isNumber(position))) { resting = true; velocity = 0; return; } // calculate spring force const f = -(position - target) * stiffness; // update velocity by adding force based on mass velocity += f / mass; // update position by adding velocity position += velocity; // slow down based on amount of damping velocity *= damping; // we've arrived if we're near target and our velocity is near zero if (thereYet(position, target, velocity)) { position = target; velocity = 0; resting = true; // we done api.onupdate(position); api.oncomplete(position); } else { // progress update api.onupdate(position); } }; /** * Set new target value * @param value */ const setTarget = value => { // if currently has no position, set target and position to this value if (isNumber(value) && !isNumber(position)) { position = value; } // next target value will not be animated to if (target === null) { target = value; position = value; } // let start moving to target target = value; // already at target if (position === target || typeof target === 'undefined') { // now resting as target is current position, stop moving resting = true; velocity = 0; // done! api.onupdate(position); api.oncomplete(position); return; } resting = false; }; // need 'api' to call onupdate callback const api = createObject({ interpolate, target: { set: setTarget, get: () => target }, resting: { get: () => resting }, onupdate: value => {}, oncomplete: value => {} }); return api; }; const easeLinear = t => t; const easeInOutQuad = t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t); const tween = // default values ({ duration = 500, easing = easeInOutQuad, delay = 0 } = {}) => // method definition { let start = null; let t; let p; let resting = true; let reverse = false; let target = null; const interpolate = ts => { if (resting || target === null) { return; } if (start === null) { start = ts; } if (ts - start < delay) { return; } t = ts - start - delay; if (t < duration) { p = t / duration; api.onupdate((t >= 0 ? easing(reverse ? 1 - p : p) : 0) * target); } else { t = 1; p = reverse ? 0 : 1; api.onupdate(p * target); api.oncomplete(p * target); resting = true; } }; // need 'api' to call onupdate callback const api = createObject({ interpolate, target: { get: () => (reverse ? 0 : target), set: value => { // is initial value if (target === null) { target = value; api.onupdate(value); api.oncomplete(value); return; } // want to tween to a smaller value and have a current value if (value < target) { target = 1; reverse = true; } else { // not tweening to a smaller value reverse = false; target = value; } // let's go! resting = false; start = null; } }, resting: { get: () => resting }, onupdate: value => {}, oncomplete: value => {} }); return api; }; const animator = { spring, tween }; /* { type: 'spring', stiffness: .5, damping: .75, mass: 10 }; { translation: { type: 'spring', ... }, ... } { translation: { x: { type: 'spring', ... } } } */ const createAnimator = (definition, category, property) => { // default is single definition // we check if transform is set, if so, we check if property is set const def = definition[category] && typeof definition[category][property] === 'object' ? definition[category][property] : definition[category] || definition; const type = typeof def === 'string' ? def : def.type; const props = typeof def === 'object' ? { ...def } : {}; return animator[type] ? animator[type](props) : null; }; const addGetSet = (keys, obj, props, overwrite = false) => { obj = Array.isArray(obj) ? obj : [obj]; obj.forEach(o => { keys.forEach(key => { let name = key; let getter = () => props[key]; let setter = value => (props[key] = value); if (typeof key === 'object') { name = key.key; getter = key.getter || getter; setter = key.setter || setter; } if (o[name] && !overwrite) { return; } o[name] = { get: getter, set: setter }; }); }); }; const isDefined = value => value != null; // add to state, // add getters and setters to internal and external api (if not set) // setup animators const animations = ({ mixinConfig, viewProps, viewInternalAPI, viewExternalAPI, viewState }) => { // initial properties const initialProps = { ...viewProps }; // list of all active animations const animations = []; // setup animators forin(mixinConfig, (property, animation) => { const animator = createAnimator(animation); if (!animator) { return; } // when the animator updates, update the view state value animator.onupdate = value => { viewProps[property] = value; }; // set animator target animator.target = initialProps[property]; // when value is set, set the animator target value const prop = { key: property, setter: value => { // if already at target, we done! if (animator.target === value) { return; } animator.target = value; }, getter: () => viewProps[property] }; // add getters and setters addGetSet([prop], [viewInternalAPI, viewExternalAPI], viewProps, true); // add it to the list for easy updating from the _write method animations.push(animator); }); // expose internal write api return { write: ts => { let resting = true; animations.forEach(animation => { if (!animation.resting) { resting = false; } animation.interpolate(ts); }); return resting; }, destroy: () => {} }; }; const addEvent = element => (type, fn) => { element.addEventListener(type, fn); }; const removeEvent = element => (type, fn) => { element.removeEventListener(type, fn); }; // mixin const listeners = ({ mixinConfig, viewProps, viewInternalAPI, viewExternalAPI, viewState, view }) => { const events = []; const add = addEvent(view.element); const remove = removeEvent(view.element); viewExternalAPI.on = (type, fn) => { events.push({ type, fn }); add(type, fn); }; viewExternalAPI.off = (type, fn) => { events.splice( events.findIndex(event => event.type === type && event.fn === fn), 1 ); remove(type, fn); }; return { write: () => { // not busy return true; }, destroy: () => { events.forEach(event => { remove(event.type, event.fn); }); } }; }; // add to external api and link to props const apis = ({ mixinConfig, viewProps, viewExternalAPI }) => { addGetSet(mixinConfig, viewExternalAPI, viewProps); }; // add to state, // add getters and setters to internal and external api (if not set) // set initial state based on props in viewProps // apply as transforms each frame const defaults = { opacity: 1, scaleX: 1, scaleY: 1, translateX: 0, translateY: 0, rotateX: 0, rotateY: 0, rotateZ: 0, originX: 0, originY: 0 }; const styles = ({ mixinConfig, viewProps, viewInternalAPI, viewExternalAPI, view }) => { // initial props const initialProps = { ...viewProps }; // current props const currentProps = {}; // we will add those properties to the external API and link them to the viewState addGetSet(mixinConfig, [viewInternalAPI, viewExternalAPI], viewProps); // override rect on internal and external rect getter so it takes in account transforms const getOffset = () => [ viewProps['translateX'] || 0, viewProps['translateY'] || 0 ]; const getScale = () => [viewProps['scaleX'] || 0, viewProps['scaleY'] || 0]; const getRect = () => view.rect ? getViewRect(view.rect, view.childViews, getOffset(), getScale()) : null; viewInternalAPI.rect = { get: getRect }; viewExternalAPI.rect = { get: getRect }; // apply view props mixinConfig.forEach(key => { viewProps[key] = typeof initialProps[key] === 'undefined' ? defaults[key] : initialProps[key]; }); // expose api return { write: () => { // see if props have changed if (!propsHaveChanged(currentProps, viewProps)) { return; } // moves element to correct position on screen applyStyles(view.element, viewProps); // store new transforms Object.assign(currentProps, { ...viewProps }); // no longer busy return true; }, destroy: () => {} }; }; const propsHaveChanged = (currentProps, newProps) => { // different amount of keys if (Object.keys(currentProps).length !== Object.keys(newProps).length) { return true; } // lets analyze the individual props for (const prop in newProps) { if (newProps[prop] !== currentProps[prop]) { return true; } } return false; }; const applyStyles = ( element, { opacity, perspective, translateX, translateY, scaleX, scaleY, rotateX, rotateY, rotateZ, originX, originY, width, height } ) => { let transforms = ''; let styles = ''; // handle transform origin if (isDefined(originX) || isDefined(originY)) { styles += `transform-origin: ${originX || 0}px ${originY || 0}px;`; } // transform order is relevant // 0. perspective if (isDefined(perspective)) { transforms += `perspective(${perspective}px) `; } // 1. translate if (isDefined(translateX) || isDefined(translateY)) { transforms += `translate3d(${translateX || 0}px, ${translateY || 0}px, 0) `; } // 2. scale if (isDefined(scaleX) || isDefined(scaleY)) { transforms += `scale3d(${isDefined(scaleX) ? scaleX : 1}, ${ isDefined(scaleY) ? scaleY : 1 }, 1) `; } // 3. rotate if (isDefined(rotateZ)) { transforms += `rotateZ(${rotateZ}rad) `; } if (isDefined(rotateX)) { transforms += `rotateX(${rotateX}rad) `; } if (isDefined(rotateY)) { transforms += `rotateY(${rotateY}rad) `; } // add transforms if (transforms.length) { styles += `transform:${transforms};`; } // add opacity if (isDefined(opacity)) { styles += `opacity:${opacity};`; // if we reach zero, we make the element inaccessible if (opacity === 0) { styles += `visibility:hidden;`; } // if we're below 100% opacity this element can't be clicked if (opacity < 1) { styles += `pointer-events:none;`; } } // add height if (isDefined(height)) { styles += `height:${height}px;`; } // add width if (isDefined(width)) { styles += `width:${width}px;`; } // apply styles const elementCurrentStyle = element.elementCurrentStyle || ''; // if new styles does not match current styles, lets update! if ( styles.length !== elementCurrentStyle.length || styles !== elementCurrentStyle ) { element.setAttribute('style', styles); // store current styles so we can compare them to new styles later on // _not_ getting the style attribute is faster element.elementCurrentStyle = styles; } }; const Mixins = { styles, listeners, animations, apis }; const updateRect = (rect = {}, element = {}, style = {}) => { if (!element.layoutCalculated) { rect.paddingTop = parseInt(style.paddingTop, 10) || 0; rect.marginTop = parseInt(style.marginTop, 10) || 0; rect.marginRight = parseInt(style.marginRight, 10) || 0; rect.marginBottom = parseInt(style.marginBottom, 10) || 0; rect.marginLeft = parseInt(style.marginLeft, 10) || 0; element.layoutCalculated = true; } rect.left = element.offsetLeft || 0; rect.top = element.offsetTop || 0; rect.width = element.offsetWidth || 0; rect.height = element.offsetHeight || 0; rect.right = rect.left + rect.width; rect.bottom = rect.top + rect.height; rect.scrollTop = element.scrollTop; rect.hidden = element.offsetParent === null; return rect; }; const createView = // default view definition ({ // element definition tag = 'div', name = null, attributes = {}, // view interaction read = () => {}, write = () => {}, create = () => {}, destroy = () => {}, // hooks filterFrameActionsForChild = (child, actions) => actions, didCreateView = () => {}, didWriteView = () => {}, // rect related ignoreRect = false, ignoreRectUpdate = false, // mixins mixins = [] } = {}) => ( // each view requires reference to store store, // specific properties for this view props = {} ) => { // root element should not be changed const element = createElement(tag, `filepond--${name}`, attributes); // style reference should also not be changed const style = window.getComputedStyle(element, null); // element rectangle const rect = updateRect(); let frameRect = null; // rest state let isResting = false; // pretty self explanatory const childViews = []; // loaded mixins const activeMixins = []; // references to created children const ref = {}; // state used for each instance const state = {}; // list of writers that will be called to update this view const writers = [ write // default writer ]; const readers = [ read // default reader ]; const destroyers = [ destroy // default destroy ]; // core view methods const getElement = () => element; const getChildViews = () => childViews.concat(); const getReference = () => ref; const createChildView = store => (view, props) => view(store, props); const getRect = () => { if (frameRect) { return frameRect; } frameRect = getViewRect(rect, childViews, [0, 0], [1, 1]); return frameRect; }; const getStyle = () => style; /** * Read data from DOM * @private */ const _read = () => { frameRect = null; // read child views childViews.forEach(child => child._read()); const shouldUpdate = !(ignoreRectUpdate && rect.width && rect.height); if (shouldUpdate) { updateRect(rect, element, style); } // readers const api = { root: internalAPI, props, rect }; readers.forEach(reader => reader(api)); }; /** * Write data to DOM * @private */ const _write = (ts, frameActions, shouldOptimize) => { // if no actions, we assume that the view is resting let resting = frameActions.length === 0; // writers writers.forEach(writer => { const writerResting = writer({ props, root: internalAPI, actions: frameActions, timestamp: ts, shouldOptimize }); if (writerResting === false) { resting = false; } }); // run mixins activeMixins.forEach(mixin => { // if one of the mixins is still busy after write operation, we are not resting const mixinResting = mixin.write(ts); if (mixinResting === false) { resting = false; } }); // updates child views that are currently attached to the DOM childViews .filter(child => !!child.element.parentNode) .forEach(child => { // if a child view is not resting, we are not resting const childResting = child._write( ts, filterFrameActionsForChild(child, frameActions), shouldOptimize ); if (!childResting) { resting = false; } }); // append new elements to DOM and update those childViews //.filter(child => !child.element.parentNode) .forEach((child, index) => { // skip if (child.element.parentNode) { return; } // append to DOM internalAPI.appendChild(child.element, index); // call read (need to know the size of these elements) child._read(); // re-call write child._write( ts, filterFrameActionsForChild(child, frameActions), shouldOptimize ); // we just added somthing to the dom, no rest resting = false; }); // update resting state isResting = resting; didWriteView({ props, root: internalAPI, actions: frameActions, timestamp: ts }); // let parent know if we are resting return resting; }; const _destroy = () => { activeMixins.forEach(mixin => mixin.destroy()); destroyers.forEach(destroyer => { destroyer({ root: internalAPI, props }); }); childViews.forEach(child => child._destroy()); }; // sharedAPI const sharedAPIDefinition = { element: { get: getElement }, style: { get: getStyle }, childViews: { get: getChildViews } }; // private API definition const internalAPIDefinition = { ...sharedAPIDefinition, rect: { get: getRect }, // access to custom children references ref: { get: getReference }, // dom modifiers is: needle => name === needle, appendChild: appendChild(element), createChildView: createChildView(store), linkView: view => { childViews.push(view); return view; }, unlinkView: view => { childViews.splice(childViews.indexOf(view), 1); }, appendChildView: appendChildView(element, childViews), removeChildView: removeChildView(element, childViews), registerWriter: writer => writers.push(writer), registerReader: reader => readers.push(reader), registerDestroyer: destroyer => destroyers.push(destroyer), invalidateLayout: () => (element.layoutCalculated = false), // access to data store dispatch: store.dispatch, query: store.query }; // public view API methods const externalAPIDefinition = { element: { get: getElement }, childViews: { get: getChildViews }, rect: { get: getRect }, resting: { get: () => isResting }, isRectIgnored: () => ignoreRect, _read, _write, _destroy }; // mixin API methods const mixinAPIDefinition = { ...sharedAPIDefinition, rect: { get: () => rect } }; // add mixin functionality Object.keys(mixins) .sort((a, b) => { // move styles to the back of the mixin list (so adjustments of other mixins are applied to the props correctly) if (a === 'styles') { return 1; } else if (b === 'styles') { return -1; } return 0; }) .forEach(key => { const mixinAPI = Mixins[key]({ mixinConfig: mixins[key], viewProps: props, viewState: state, viewInternalAPI: internalAPIDefinition, viewExternalAPI: externalAPIDefinition, view: createObject(mixinAPIDefinition) }); if (mixinAPI) { activeMixins.push(mixinAPI); } }); // construct private api const internalAPI = createObject(internalAPIDefinition); // create the view create({ root: internalAPI, props }); // append created child views to root node const childCount = element.children.length; // need to know the current child count so appending happens in correct order childViews.forEach((child, index) => { internalAPI.appendChild(child.element, childCount + index); }); // call did create didCreateView(internalAPI); // expose public api return createObject(externalAPIDefinition, props); }; const createPainter = (read, write, fps = 60) => { const name = '__framePainter'; // set global painter if (window[name]) { window[name].readers.push(read); window[name].writers.push(write); return; } window[name] = { readers: [read], writers: [write] }; const painter = window[name]; const interval = 1000 / fps; let last = null; let frame = null; const tick = ts => { // queue next tick frame = window.requestAnimationFrame(tick); // limit fps if (!last) { last = ts; } const delta = ts - last; if (delta <= interval) { // skip frame return; } // align next frame last = ts - (delta % interval); // update view painter.readers.forEach(read => read()); painter.writers.forEach(write => write(ts)); }; tick(performance.now()); return { pause: () => { window.cancelAnimationFrame(frame); } }; }; const createRoute = (routes, fn) => ({ root, props, actions = [], timestamp, shouldOptimize }) => { actions .filter(action => routes[action.type]) .forEach(action => routes[action.type]({ root, props, action: action.data, timestamp, shouldOptimize }) ); if (fn) { fn({ root, props, actions, timestamp, shouldOptimize }); } }; const insertBefore = (newNode, referenceNode) => referenceNode.parentNode.insertBefore(newNode, referenceNode); const insertAfter = (newNode, referenceNode) => { return referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling ); }; const isArray = value => Array.isArray(value); const isEmpty = value => value == null; const trim = str => str.trim(); const toString = value => '' + value; const toArray = (value, splitter = ',') => { if (isEmpty(value)) { return []; } if (isArray(value)) { return value; } return toString(value) .split(splitter) .map(trim) .filter(str => str.length); }; const isBoolean = value => typeof value === 'boolean'; const toBoolean = value => (isBoolean(value) ? value : value === 'true'); const isString = value => typeof value === 'string'; const toNumber = value => isNumber(value) ? value : isString(value) ? toString(value).replace(/[a-z]+/gi, '') : 0; const toInt = value => parseInt(toNumber(value), 10); const toFloat = value => parseFloat(toNumber(value)); const isInt = value => isNumber(value) && isFinite(value) && Math.floor(value) === value; const toBytes = value => { // is in bytes if (isInt(value)) { return value; } // is natural file size let naturalFileSize = toString(value).trim(); // if is value in megabytes if (/MB$/i.test(naturalFileSize)) { naturalFileSize = naturalFileSize.replace(/MB$i/, '').trim(); return toInt(naturalFileSize) * 1000 * 1000; } // if is value in kilobytes if (/KB/i.test(naturalFileSize)) { naturalFileSize = naturalFileSize.replace(/KB$i/, '').trim(); return toInt(naturalFileSize) * 1000; } return toInt(naturalFileSize); }; const isFunction = value => typeof value === 'function'; const toFunctionReference = string => { let ref = self; let levels = string.split('.'); let level = null; while ((level = levels.shift())) { ref = ref[level]; if (!ref) { return null; } } return ref; }; const methods = { process: 'POST', revert: 'DELETE', fetch: 'GET', restore: 'GET', load: 'GET' }; const createServerAPI = outline => { const api = {}; api.url = isString(outline) ? outline : outline.url || ''; api.timeout = outline.timeout ? parseInt(outline.timeout, 10) : 0; forin(methods, key => { api[key] = createAction(key, outline[key], methods[key], api.timeout); }); // special treatment for remove api.remove = outline.remove || null; return api; }; const createAction = (name, outline, method, timeout) => { // is explicitely set to null so disable if (outline === null) { return null; } // if is custom function, done! Dev handles everything. if (typeof outline === 'function') { return outline; } // build action object const action = { url: method === 'GET' ? `?${name}=` : '', method, headers: {}, withCredentials: false, timeout, onload: null, ondata: null, onerror: null }; // is a single url if (isString(outline)) { action.url = outline; return action; } // overwrite Object.assign(action, outline); // see if should reformat headers; if (isString(action.headers)) { const parts = action.headers.split(/:(.+)/); action.headers = { header: parts[0], value: parts[1] }; } // if is bool withCredentials action.withCredentials = toBoolean(action.withCredentials); return action; }; const toServerAPI = value => createServerAPI(value); const isNull = value => value === null; const isObject = value => typeof value === 'object' && value !== null; const isAPI = value => { return ( isObject(value) && isString(value.url) && isObject(value.process) && isObject(value.revert) && isObject(value.restore) && isObject(value.fetch) ); }; const getType = value => { if (isArray(value)) { return 'array'; } if (isNull(value)) { return 'null'; } if (isInt(value)) { return 'int'; } if (/^[0-9]+ ?(?:GB|MB|KB)$/gi.test(value)) { return 'bytes'; } if (isAPI(value)) { return 'api'; } return typeof value; }; const replaceSingleQuotes = str => str .replace(/{\s*'/g, '{"') .replace(/'\s*}/g, '"}') .replace(/'\s*:/g, '":') .replace(/:\s*'/g, ':"') .replace(/,\s*'/g, ',"') .replace(/'\s*,/g, '",'); const conversionTable = { array: toArray, boolean: toBoolean, int: value => (getType(value) === 'bytes' ? toBytes(value) : toInt(value)), float: toFloat, bytes: toBytes, string: value => (isFunction(value) ? value : toString(value)), serverapi: toServerAPI, object: value => { try { return JSON.parse(replaceSingleQuotes(value)); } catch (e) { return null; } }, function: value => toFunctionReference(value) }; const convertTo = (value, type) => conversionTable[type](value); const getValueByType = (newValue, defaultValue, valueType) => { // can always assign default value if (newValue === defaultValue) { return newValue; } // get the type of the new value let newValueType = getType(newValue); // is valid type? if (newValueType !== valueType) { // is string input, let's attempt to convert const convertedValue = convertTo(newValue, valueType); // what is the type now newValueType = getType(convertedValue); // no valid conversions found if (convertedValue === null) { throw `Trying to assign value with incorrect type to "${option}", allowed type: "${valueType}"`; } else { newValue = convertedValue; } } // assign new value return newValue; }; const createOption = (defaultValue, valueType) => { let currentValue = defaultValue; return { enumerable: true, get: () => currentValue, set: newValue => { currentValue = getValueByType(newValue, defaultValue, valueType); } }; }; const createOptions = options => { const obj = {}; forin(options, prop => { const optionDefinition = options[prop]; obj[prop] = createOption(optionDefinition[0], optionDefinition[1]); }); return createObject(obj); }; const createInitialState = options => ({ // model items: [], // timeout used for calling update items listUpdateTimeout: null, // queue of items waiting to be processed processingQueue: [], // options options: createOptions(options) }); const fromCamels = (string, separator = '-') => string .split(/(?=[A-Z])/) .map(part => part.toLowerCase()) .join(separator); const createOptionAPI = (store, options) => { const obj = {}; forin(options, key => { obj[key] = { get: () => store.getState().options[key], set: value => { store.dispatch(`SET_${fromCamels(key, '_').toUpperCase()}`, { value }); } }; }); return obj; }; const createOptionActions = options => (dispatch, query, state) => { const obj = {}; forin(options, key => { const name = fromCamels(key, '_').toUpperCase(); obj[`SET_${name}`] = action => { try { state.options[key] = action.value; } catch (e) { // nope, failed } // we successfully set the value of this option dispatch(`DID_SET_${name}`, { value: state.options[key] }); }; }); return obj; }; const createOptionQueries = options => state => { const obj = {}; forin(options, key => { obj[`GET_${fromCamels(key, '_').toUpperCase()}`] = action => state.options[key]; }); return obj; }; const InteractionMethod = { API: 1, DROP: 2, BROWSE: 3, PASTE: 4, NONE: 5 }; const getUniqueId = () => Math.random() .toString(36) .substr(2, 9); const arrayRemove = (arr, index) => arr.splice(index, 1); const on = () => { const listeners = []; const off = (event, cb) => { arrayRemove( listeners, listeners.findIndex( listener => listener.event === event && (listener.cb === cb || !cb) ) ); }; return { fire: (event, ...args) => { listeners .filter(listener => listener.event === event) .map(listener => listener.cb) .forEach(cb => { setTimeout(() => { cb(...args); }, 0); }); }, on: (event, cb) => { listeners.push({ event, cb }); }, onOnce: (event, cb) => { listeners.push({ event, cb: (...args) => { off(event, cb); cb(...args); } }); }, off }; }; const copyObjectPropertiesToObject = (src, target, excluded) => { Object.getOwnPropertyNames(src) .filter(property => !excluded.includes(property)) .forEach(key => Object.defineProperty( target, key, Object.getOwnPropertyDescriptor(src, key) ) ); }; const PRIVATE = [ 'fire', 'process', 'revert', 'load', 'on', 'off', 'onOnce', 'retryLoad', 'extend', 'archive', 'archived', 'release', 'released', 'requestProcessing', 'freeze' ]; const createItemAPI = item => { const api = {}; copyObjectPropertiesToObject(item, api, PRIVATE); return api; }; const removeReleasedItems = items => { items.forEach((item, index) => { if (item.released) { arrayRemove(items, index); } }); }; const ItemStatus = { INIT: 1, IDLE: 2, PROCESSING_QUEUED: 9, PROCESSING: 3, PROCESSING_COMPLETE: 5, PROCESSING_ERROR: 6, PROCESSING_REVERT_ERROR: 10, LOADING: 7, LOAD_ERROR: 8 }; const getNonNumeric = str => /[^0-9]+/.exec(str); const getDecimalSeparator = () => getNonNumeric((1.1).toLocaleString())[0]; const getThousandsSeparator = () => { // Added for browsers that do not return the thousands separator (happend on native browser Android 4.4.4) // We check against the normal toString output and if they're the same return a comma when decimal separator is a dot const decimalSeparator = getDecimalSeparator(); const thousandsStringWithSeparator = (1000.0).toLocaleString(); const thousandsStringWithoutSeparator = (1000.0).toString(); if (thousandsStringWithSeparator !== thousandsStringWithoutSeparator) { return getNonNumeric(thousandsStringWithSeparator)[0]; } return decimalSeparator === '.' ? ',' : '.'; }; const Type = { BOOLEAN: 'boolean', INT: 'int', STRING: 'string', ARRAY: 'array', OBJECT: 'object', FUNCTION: 'function', ACTION: 'action', SERVER_API: 'serverapi', REGEX: 'regex' }; // all registered filters const filters = []; // loops over matching filters and passes options to each filter, returning the mapped results const applyFilterChain = (key, value, utils) => new Promise((resolve, reject) => { // find matching filters for this key const matchingFilters = filters.filter(f => f.key === key).map(f => f.cb); // resolve now if (matchingFilters.length === 0) { resolve(value); return; } // first filter to kick things of const initialFilter = matchingFilters.shift(); // chain filters matchingFilters .reduce( // loop over promises passing value to next promise (current, next) => current.then(value => next(value, utils)), // call initial filter, will return a promise initialFilter(value, utils) // all executed ) .then(value => resolve(value)) .catch(error => reject(error)); }); const applyFilters = (key, value, utils) => filters.filter(f => f.key === key).map(f => f.cb(value, utils)); // adds a new filter to the list const addFilter = (key, cb) => filters.push({ key, cb }); const extendDefaultOptions = additionalOptions => Object.assign(defaultOptions, additionalOptions); const getOptions = () => ({ ...defaultOptions }); const setOptions = opts => { forin(opts, (key, value) => { // key does not exist, so this option cannot be set if (!defaultOptions[key]) { return; } defaultOptions[key][0] = getValueByType( value, defaultOptions[key][0], defaultOptions[key][1] ); }); }; // default options on app const defaultOptions = { // the id to add to the root element id: [null, Type.STRING], // input field name to use name: ['filepond', Type.STRING], // disable the field disabled: [false, Type.BOOLEAN], // classname to put on wrapper className: [null, Type.STRING], // is the field required required: [false, Type.BOOLEAN], // Allow media capture when value is set captureMethod: [null, Type.STRING], // - "camera", "microphone" or "camcorder", // - Does not work with multiple on apple devices // - If set, acceptedFileTypes must be made to match with media wildcard "image/*", "audio/*" or "video/*" // Feature toggles allowDrop: [true, Type.BOOLEAN], // Allow dropping of files allowBrowse: [true, Type.BOOLEAN], // Allow browsing the file system allowPaste: [true, Type.BOOLEAN], // Allow pasting files allowMultiple: [false, Type.BOOLEAN], // Allow multiple files (disabled by default, as multiple attribute is also required on input to allow multiple) allowReplace: [true, Type.BOOLEAN], // Allow dropping a file on other file to replace it (only works when multiple is set to false) allowRevert: [true, Type.BOOLEAN], // Allows user to revert file upload // Revert mode forceRevert: [false, Type.BOOLEAN], // Set to 'force' to require the file to be reverted before removal // Input requirements maxFiles: [null, Type.INT], // Max number of files checkValidity: [false, Type.BOOLEAN], // Enables custom validity messages // Where to put file itemInsertLocationFreedom: [true, Type.BOOLEAN], // Set to false to always add items to begin or end of list itemInsertLocation: ['before', Type.STRING], // Default index in list to add items that have been dropped at the top of the list itemInsertInterval: [75, Type.INT], // Drag 'n Drop related dropOnPage: [false, Type.BOOLEAN], // Allow dropping of files anywhere on page (prevents browser from opening file if dropped outside of Up) dropOnElement: [true, Type.BOOLEAN], // Drop needs to happen on element (set to false to also load drops outside of Up) dropValidation: [false, Type.BOOLEAN], // Enable or disable validating files on drop ignoredFiles: [['.ds_store', 'thumbs.db', 'desktop.ini'], Type.ARRAY], // Upload related instantUpload: [true, Type.BOOLEAN], // Should upload files immidiately on drop maxParallelUploads: [2, Type.INT], // Maximum files to upload in parallel // The server api end points to use for uploading (see docs) server: [null, Type.SERVER_API], // Labels and status messages labelDecimalSeparator: [getDecimalSeparator(), Type.STRING], // Default is locale separator labelThousandsSeparator: [getThousandsSeparator(), Type.STRING], // Default is locale separator labelIdle: [ 'Drag & Drop your files or <span class="filepond--label-action">Browse</span>', Type.STRING ], labelInvalidField: ['Field contains invalid files', Type.STRING], labelFileWaitingForSize: ['Waiting for size', Type.STRING], labelFileSizeNotAvailable: ['Size not available', Type.STRING], labelFileCountSingular: ['file in list', Type.STRING], labelFileCountPlural: ['files in list', Type.STRING], labelFileLoading: ['Loading', Type.STRING], labelFileAdded: ['Added', Type.STRING], // assistive only labelFileLoadError: ['Error during load', Type.STRING], labelFileRemoved: ['Removed', Type.STRING], // assistive only labelFileRemoveError: ['Error during remove', Type.STRING], labelFileProcessing: ['Uploading', Type.STRING], labelFileProcessingComplete: ['Upload complete', Type.STRING], labelFileProcessingAborted: ['Upload cancelled', Type.STRING], labelFileProcessingError: ['Error during upload', Type.STRING], labelFileProcessingRevertError: ['Error during revert', Type.STRING], labelTapToCancel: ['tap to cancel', Type.STRING], labelTapToRetry: ['tap to retry', Type.STRING], labelTapToUndo: ['tap to undo', Type.STRING], labelButtonRemoveItem: ['Remove', Type.STRING], labelButtonAbortItemLoad: ['Abort', Type.STRING], labelButtonRetryItemLoad: ['Retry', Type.STRING], labelButtonAbortItemProcessing: ['Cancel', Type.STRING], labelButtonUndoItemProcessing: ['Undo', Type.STRING], labelButtonRetryItemProcessing: ['Retry', Type.STRING], labelButtonProcessItem: ['Upload', Type.STRING], // make sure width and height plus viewpox are even numbers so icons are nicely centered iconRemove: [ '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M11.586 13l-2.293 2.293a1 1 0 0 0 1.414 1.414L13 14.414l2.293 2.293a1 1 0 0 0 1.414-1.414L14.414 13l2.293-2.293a1 1 0 0 0-1.414-1.414L13 11.586l-2.293-2.293a1 1 0 0 0-1.414 1.414L11.586 13z" fill="currentColor" fill-rule="nonzero"/></svg>', Type.STRING ], iconProcess: [ '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M14 10.414v3.585a1 1 0 0 1-2 0v-3.585l-1.293 1.293a1 1 0 0 1-1.414-1.415l3-3a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1-1.414 1.415L14 10.414zM9 18a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2H9z" fill="currentColor" fill-rule="evenodd"/></svg>', Type.STRING ], iconRetry: [ '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M10.81 9.185l-.038.02A4.997 4.997 0 0 0 8 13.683a5 5 0 0 0 5 5 5 5 0 0 0 5-5 1 1 0 0 1 2 0A7 7 0 1 1 9.722 7.496l-.842-.21a.999.999 0 1 1 .484-1.94l3.23.806c.535.133.86.675.73 1.21l-.804 3.233a.997.997 0 0 1-1.21.73.997.997 0 0 1-.73-1.21l.23-.928v-.002z" fill="currentColor" fill-rule="nonzero"/></svg>', Type.STRING ], iconUndo: [ '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M9.185 10.81l.02-.038A4.997 4.997 0 0 1 13.683 8a5 5 0 0 1 5 5 5 5 0 0 1-5 5 1 1 0 0 0 0 2A7 7 0 1 0 7.496 9.722l-.21-.842a.999.999 0 1 0-1.94.484l.806 3.23c.133.535.675.86 1.21.73l3.233-.803a.997.997 0 0 0 .73-1.21.997.997 0 0 0-1.21-.73l-.928.23-.002-.001z" fill="currentColor" fill-rule="nonzero"/></svg>', Type.STRING ], iconDone: [ '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M18.293 9.293a1 1 0 0 1 1.414 1.414l-7.002 7a1 1 0 0 1-1.414 0l-3.998-4a1 1 0 1 1 1.414-1.414L12 15.586l6.294-6.293z" fill="currentColor" fill-rule="nonzero"/></svg>', Type.STRING ], // event handlers oninit: [null, Type.FUNCTION], onwarning: [null, Type.FUNCTION], onerror: [null, Type.FUNCTION], onactivatefile: [null, Type.FUNCTION], onaddfilestart: [null, Type.FUNCTION], onaddfileprogress: [null, Type.FUNCTION], onaddfile: [null, Type.FUNCTION], onprocessfilestart: [null, Type.FUNCTION], onprocessfileprogress: [null, Type.FUNCTION], onprocessfileabort: [null, Type.FUNCTION], onprocessfilerevert: [null, Type.FUNCTION], onprocessfile: [null, Type.FUNCTION], onprocessfiles: [null, Type.FUNCTION], onremovefile: [null, Type.FUNCTION], onpreparefile: [null, Type.FUNCTION], onupdatefiles: [null, Type.FUNCTION], // hooks beforeAddFile: [null, Type.FUNCTION], beforeRemoveFile: [null, Type.FUNCTION], // styles stylePanelLayout: [null, Type.STRING], // null 'integrated', 'compact', 'circle' stylePanelAspectRatio: [null, Type.STRING], // null or '3:2' or 1 styleItemPanelAspectRatio: [null, Type.STRING], styleButtonRemoveItemPosition: ['left', Type.STRING], styleButtonProcessItemPosition: ['right', Type.STRING], styleLoadIndicatorPosition: ['right', Type.STRING], styleProgressIndicatorPosition: ['right', Type.STRING], // custom initial files array files: [[], Type.ARRAY] }; const getItemByQuery = (items, query) => { // just return first index if (isEmpty(query)) { return items[0] || null; } // query is index if (isInt(query)) { return items[query] || null; } // if query is item, get the id if (typeof query === 'object') { query = query.id; } // assume query is a string and return item by id return items.find(item => item.id === query) || null; }; const getNumericAspectRatioFromString = aspectRatio => { if (isEmpty(aspectRatio)) { return aspectRatio; } if (/:/.test(aspectRatio)) { const parts = aspectRatio.split(':'); return parts[1] / parts[0]; } return parseFloat(aspectRatio); }; const getActiveItems = items => items.filter(item => !item.archived); const Status = { EMPTY: 0, IDLE: 1, // wait