filepond
Version:
FilePond, Where files go to stretch their bits.
1,828 lines (1,516 loc) • 296 kB
JavaScript
/*!
* FilePond 4.32.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 (should never block if document is hidden)
if (isBlocking && !document.hidden) {
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 IS_BROWSER = (() =>
typeof window !== 'undefined' && typeof window.document !== 'undefined')();
const isBrowser = () => IS_BROWSER;
const testElement = isBrowser() ? createElement('svg') : {};
const getChildCount =
'children' in testElement ? el => el.children.length : el => el.childNodes.length;
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 = (ts, skipToEndState) => {
// 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) || skipToEndState) {
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, skipToEndState) => {
if (resting || target === null) return;
if (start === null) {
start = ts;
}
if (ts - start < delay) return;
t = ts - start - delay;
if (t >= duration || skipToEndState) {
t = 1;
p = reverse ? 0 : 1;
api.onupdate(p * target);
api.oncomplete(p * target);
resting = true;
} else {
p = t / duration;
api.onupdate((t >= 0 ? easing(reverse ? 1 - p : p) : 0) * target);
}
};
// 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,
};
});
});
};
// add to state,
// add getters and setters to internal and external api (if not set)
// setup animators
const animations = ({ mixinConfig, viewProps, viewInternalAPI, viewExternalAPI }) => {
// 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 skipToEndState = document.hidden;
let resting = true;
animations.forEach(animation => {
if (!animation.resting) resting = false;
animation.interpolate(ts, skipToEndState);
});
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);
};
const isDefined = value => value != null;
// 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.style.cssText = styles;
// store current styles so we can compare them to new styles later on
// _not_ getting the style value 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 = getChildCount(element); // 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);
};
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 id = null;
let requestTick = null;
let cancelTick = null;
const setTimerType = () => {
if (document.hidden) {
requestTick = () => window.setTimeout(() => tick(performance.now()), interval);
cancelTick = () => window.clearTimeout(id);
} else {
requestTick = () => window.requestAnimationFrame(tick);
cancelTick = () => window.cancelAnimationFrame(id);
}
};
document.addEventListener('visibilitychange', () => {
if (cancelTick) cancelTick();
setTimerType();
tick(performance.now());
});
const tick = ts => {
// queue next tick
id = requestTick(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));
};
setTimerType();
tick(performance.now());
return {
pause: () => {
cancelTick(id);
},
};
};
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, base = 1000) => {
// 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) * base * base;
}
// if is value in kilobytes
if (/KB/i.test(naturalFileSize)) {
naturalFileSize = naturalFileSize.replace(/KB$i/, '').trim();
return toInt(naturalFileSize) * base;
}
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',
patch: 'PATCH',
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;
api.headers = outline.headers ? outline.headers : {};
forin(methods, key => {
api[key] = createAction(key, outline[key], methods[key], api.timeout, api.headers);
});
// remove process if no url or process on outline
api.process = outline.process || isString(outline) || outline.url ? api.process : null;
// special treatment for remove
api.remove = outline.remove || null;
// remove generic headers from api object
delete api.headers;
return api;
};
const createAction = (name, outline, method, timeout, headers) => {
// 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' || method === 'PATCH' ? `?${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)),
number: toFloat,
float: toFloat,
bytes: toBytes,
string: value => (isFunction(value) ? value : toString(value)),
function: value => toFunctionReference(value),
serverapi: toServerAPI,
object: value => {
try {
return JSON.parse(replaceSingleQuotes(value));
} catch (e) {
return null;
}
},
};
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,
// timeout used for stacking metadata updates
itemUpdateTimeout: 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)
.substring(2, 11);
const arrayRemove = (arr, index) => arr.splice(index, 1);
const run = (cb, sync) => {
if (sync) {
cb();
} else if (document.hidden) {
Promise.resolve(1).then(cb);
} else {
setTimeout(cb, 0);
}
};
const on = () => {
const listeners = [];
const off = (event, cb) => {
arrayRemove(
listeners,
listeners.findIndex(listener => listener.event === event && (listener.cb === cb || !cb))
);
};
const fire = (event, args, sync) => {
listeners
.filter(listener => listener.event === event)
.map(listener => listener.cb)
.forEach(cb => run(() => cb(...args), sync));
};
return {
fireSync: (event, ...args) => {
fire(event, args, true);
},
fire: (event, ...args) => {
fire(event, args, false);
},
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 FileOrigin = {
INPUT: 1,
LIMBO: 2,
LOCAL: 3,
};
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',
NUMBER: 'number',
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/*"
// sync `acceptedFileTypes` property with `accept` attribute
allowSyncAcceptAttribute: [true, Type.BOOLEAN],
// 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.BOOLEA