filepond
Version:
FilePond, Where files go to stretch their bits.
1,991 lines (1,667 loc) • 221 kB
JavaScript
/*!
* 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