/*!
 * FilePond 4.30.4
 * 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.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
    allowRemove: [true, Type.BOOLEAN], // Allow user to remove a file
    allowProcess: [true, Type.BOOLEAN], // Allows user to process a file, when set to false, this removes the file upload button
    allowReorder: [false, Type.BOOLEAN], // Allow reordering of files
    allowDirectoriesOnly: [false, Type.BOOLEAN], // Allow only selecting directories with browse (no support for filtering dnd at this point)

    // Try store file if `server` not set
    storeAsFile: [false, Type.BOOLEAN],

    // 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 immediately on drop
    maxParallelUploads: [2, Type.INT], // Maximum files to upload in parallel
    allowMinimumUploadDuration: [true, Type.BOOLEAN], // if true uploads take at least 750 ms, this ensures the user sees the upload progress giving trust the upload actually happened

    // Chunks
    chunkUploads: [false, Type.BOOLEAN], // Enable chunked uploads
    chunkForce: [false, Type.BOOLEAN], // Force use of chunk uploads even for files smaller than chunk size
    chunkSize: [5000000, Type.INT], // Size of chunks (5MB default)
    chunkRetryDelays: [[500, 1000, 3000], Type.ARRAY], // Amount of times to retry upload of a chunk when it fails

    // The server api end points to use for uploading (see docs)
    server: [null, Type.SERVER_API],

    // File size calculations, can set to 1024, this is only used for display, properties use file size base 1000
    fileSizeBase: [1000, Type.INT],

    // Labels and status messages
    labelFileSizeBytes: ['bytes', Type.STRING],
    labelFileSizeKilobytes: ['KB', Type.STRING],
    labelFileSizeMegabytes: ['MB', Type.STRING],
    labelFileSizeGigabytes: ['GB', Type.STRING],

    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],
    oninitfile: [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],
    onreorderfiles: [null, Type.FUNCTION],

    // hooks
    beforeDropFile: [null, Type.FUNCTION],
    beforeAddFile: [null, Type.FUNCTION],
    beforeRemoveFile: [null, Type.FUNCTION],
    beforePrepareFile: [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],
    styleButtonRemoveItemAlign: [false, Type.BOOLEAN],

    // custom initial files array
    files: [[], Type.ARRAY],

    // show support by displaying credits
    credits: [['https://pqina.nl/', 'Powered by PQINA'], 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, // waiting
    ERROR: 2, // a file is in error state
    BUSY: 3, // busy processing or loading
    READY: 4, // all files uploaded
};

let res = null;
const canUpdateFileInput = () => {
    if (res === null) {
        try {
            const dataTransfer = new DataTransfer();
            dataTransfer.items.add(new File(['hello world'], 'This_Works.txt'));
            const el = document.createElement('input');
            el.setAttribute('type', 'file');
            el.files = dataTransfer.files;
            res = el.files.length === 1;
        } catch (err) {
            res = false;
        }
    }
    return res;
};

const ITEM_ERROR = [
    ItemStatus.LOAD_ERROR,
    ItemStatus.PROCESSING_ERROR,
    ItemStatus.PROCESSING_REVERT_ERROR,
];
const ITEM_BUSY = [
    ItemStatus.LOADING,
    ItemStatus.PROCESSING,
    ItemStatus.PROCESSING_QUEUED,
    ItemStatus.INIT,
];
const ITEM_READY = [ItemStatus.PROCESSING_COMPLETE];

const isItemInErrorState = item => ITEM_ERROR.includes(item.status);
const isItemInBusyState = item => ITEM_BUSY.includes(item.status);
const isItemInReadyState = item => ITEM_READY.includes(item.status);

const isAsync = state =>
    isObject(state.options.server) &&
    (isObject(state.options.server.process) || isFunction(state.options.server.process));

const queries = state => ({
    GET_STATUS: () => {
        const items = getActiveItems(state.items);

        const { EMPTY, ERROR, BUSY, IDLE, READY } = Status;

        if (items.length === 0) return EMPTY;

        if (items.some(isItemInErrorState)) return ERROR;

        if (items.some(isItemInBusyState)) return BUSY;

        if (items.some(isItemInReadyState)) return READY;

        return IDLE;
    },

    GET_ITEM: query => getItemByQuery(state.items, query),

    GET_ACTIVE_ITEM: query => getItemByQuery(getActiveItems(state.items), query),

    GET_ACTIVE_ITEMS: () => getActiveItems(state.items),

    GET_ITEMS: () => state.items,

    GET_ITEM_NAME: query => {
        const item = getItemByQuery(state.items, query);
        return item ? item.filename : null;
    },

    GET_ITEM_SIZE: query => {
        const item = getItemByQuery(state.items, query);
        return item ? item.fileSize : null;
    },

    GET_STYLES: () =>
        Object.keys(state.options)
            .filter(key => /^style/.test(key))
            .map(option => ({
                name: option,
                value: state.options[option],
            })),

    GET_PANEL_ASPECT_RATIO: () => {
        const isShapeCircle = /circle/.test(state.options.stylePanelLayout);
        const aspectRatio = isShapeCircle
            ? 1
            : getNumericAspectRatioFromString(state.options.stylePanelAspectRatio);
        return aspectRatio;
    },

    GET_ITEM_PANEL_ASPECT_RATIO: () => state.options.styleItemPanelAspectRatio,

    GET_ITEMS_BY_STATUS: status =>
        getActiveItems(state.items).filter(item => item.status === status),

    GET_TOTAL_ITEMS: () => getActiveItems(state.items).length,

    SHOULD_UPDATE_FILE_INPUT: () =>
        state.options.storeAsFile && canUpdateFileInput() && !isAsync(state),

    IS_ASYNC: () => isAsync(state),

    GET_FILE_SIZE_LABELS: query => ({
        labelBytes: query('GET_LABEL_FILE_SIZE_BYTES') || undefined,
        labelKilobytes: query('GET_LABEL_FILE_SIZE_KILOBYTES') || undefined,
        labelMegabytes: query('GET_LABEL_FILE_SIZE_MEGABYTES') || undefined,
        labelGigabytes: query('GET_LABEL_FILE_SIZE_GIGABYTES') || undefined,
    }),
});

const hasRoomForItem = state => {
    const count = getActiveItems(state.items).length;

    // if cannot have multiple items, to add one item it should currently not contain items
    if (!state.options.allowMultiple) {
        return count === 0;
    }

    // if allows multiple items, we check if a max item count has been set, if not, there's no limit
    const maxFileCount = state.options.maxFiles;
    if (maxFileCount === null) {
        return true;
    }

    // we check if the current count is smaller than the max count, if so, another file can still be added
    if (count < maxFileCount) {
        return true;
    }

    // no more room for another file
    return false;
};

const limit = (value, min, max) => Math.max(Math.min(max, value), min);

const arrayInsert = (arr, index, item) => arr.splice(index, 0, item);

const insertItem = (items, item, index) => {
    if (isEmpty(item)) {
        return null;
    }

    // if index is undefined, append
    if (typeof index === 'undefined') {
        items.push(item);
        return item;
    }

    // limit the index to the size of the items array
    index = limit(index, 0, items.length);

    // add item to array
    arrayInsert(items, index, item);

    // expose
    return item;
};

const isBase64DataURI = str =>
    /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*)\s*$/i.test(
        str
    );

const getFilenameFromURL = url =>
    url
        .split('/')
        .pop()
        .split('?')
        .shift();

const getExtensionFromFilename = name => name.split('.').pop();

const guesstimateExtension = type => {
    // if no extension supplied, exit here
    if (typeof type !== 'string') {
        return '';
    }

    // get subtype
    const subtype = type.split('/').pop();

    // is svg subtype
    if (/svg/.test(subtype)) {
        return 'svg';
    }

    if (/zip|compressed/.test(subtype)) {
        return 'zip';
    }

    if (/plain/.test(subtype)) {
        return 'txt';
    }

    if (/msword/.test(subtype)) {
        return 'doc';
    }

    // if is valid subtype
    if (/[a-z]+/.test(subtype)) {
        // always use jpg extension
        if (subtype === 'jpeg') {
            return 'jpg';
        }

        // return subtype
        return subtype;
    }

    return '';
};

const leftPad = (value, padding = '') => (padding + value).slice(-padding.length);

const getDateString = (date = new Date()) =>
    `${date.getFullYear()}-${leftPad(date.getMonth() + 1, '00')}-${leftPad(
        date.getDate(),
        '00'
    )}_${leftPad(date.getHours(), '00')}-${leftPad(date.getMinutes(), '00')}-${leftPad(
        date.getSeconds(),
        '00'
    )}`;

const getFileFromBlob = (blob, filename, type = null, extension = null) => {
    const file =
        typeof type === 'string'
            ? blob.slice(0, blob.size, type)
            : blob.slice(0, blob.size, blob.type);
    file.lastModifiedDate = new Date();

    // copy relative path
    if (blob._relativePath) file._relativePath = blob._relativePath;

    // if blob has name property, use as filename if no filename supplied
    if (!isString(filename)) {
        filename = getDateString();
    }

    // if filename supplied but no extension and filename has extension
    if (filename && extension === null && getExtensionFromFilename(filename)) {
        file.name = filename;
    } else {
        extension = extension || guesstimateExtension(file.type);
        file.name = filename + (extension ? '.' + extension : '');
    }

    return file;
};

const getBlobBuilder = () => {
    return (window.BlobBuilder =
        window.BlobBuilder ||
        window.WebKitBlobBuilder ||
        window.MozBlobBuilder ||
        window.MSBlobBuilder);
};

const createBlob = (arrayBuffer, mimeType) => {
    const BB = getBlobBuilder();

    if (BB) {
        const bb = new BB();
        bb.append(arrayBuffer);
        return bb.getBlob(mimeType);
    }

    return new Blob([arrayBuffer], {
        type: mimeType,
    });
};

const getBlobFromByteStringWithMimeType = (byteString, mimeType) => {
    const ab = new ArrayBuffer(byteString.length);
    const ia = new Uint8Array(ab);

    for (let i = 0; i < byteString.length; i++) {
        ia[i] = byteString.charCodeAt(i);
    }

    return createBlob(ab, mimeType);
};

const getMimeTypeFromBase64DataURI = dataURI => {
    return (/^data:(.+);/.exec(dataURI) || [])[1] || null;
};

const getBase64DataFromBase64DataURI = dataURI => {
    // get data part of string (remove data:image/jpeg...,)
    const data = dataURI.split(',')[1];

    // remove any whitespace as that causes InvalidCharacterError in IE
    return data.replace(/\s/g, '');
};

const getByteStringFromBase64DataURI = dataURI => {
    return atob(getBase64DataFromBase64DataURI(dataURI));
};

const getBlobFromBase64DataURI = dataURI => {
    const mimeType = getMimeTypeFromBase64DataURI(dataURI);
    const byteString = getByteStringFromBase64DataURI(dataURI);

    return getBlobFromByteStringWithMimeType(byteString, mimeType);
};

const getFileFromBase64DataURI = (dataURI, filename, extension) => {
    return getFileFromBlob(getBlobFromBase64DataURI(dataURI), filename, null, extension);
};

const getFileNameFromHeader = header => {
    // test if is content disposition header, if not exit
    if (!/^content-disposition:/i.test(header)) return null;

    // get filename parts
    const matches = header
        .split(/filename=|filename\*=.+''/)
        .splice(1)
        .map(name => name.trim().replace(/^["']|[;"']{0,2}$/g, ''))
        .filter(name => name.length);

    return matches.length ? decodeURI(matches[matches.length - 1]) : null;
};

const getFileSizeFromHeader = header => {
    if (/content-length:/i.test(header)) {
        const size = header.match(/[0-9]+/)[0];
        return size ? parseInt(size, 10) : null;
    }
    return null;
};

const getTranfserIdFromHeader = header => {
    if (/x-content-transfer-id:/i.test(header)) {
        const id = (header.split(':')[1] || '').trim();
        return id || null;
    }
    return null;
};

const getFileInfoFromHeaders = headers => {
    const info = {
        source: null,
        name: null,
        size: null,
    };

    const rows = headers.split('\n');
    for (let header of rows) {
        const name = getFileNameFromHeader(header);
        if (name) {
            info.name = name;
            continue;
        }

        const size = getFileSizeFromHeader(header);
        if (size) {
            info.size = size;
            continue;
        }

        const source = getTranfserIdFromHeader(header);
        if (source) {
            info.source = source;
            continue;
        }
    }

    return info;
};

const createFileLoader = fetchFn => {
    const state = {
        source: null,
        complete: false,
        progress: 0,
        size: null,
        timestamp: null,
        duration: 0,
        request: null,
    };

    const getProgress = () => state.progress;
    const abort = () => {
        if (state.request && state.request.abort) {
            state.request.abort();
        }
    };

    // load source
    const load = () => {
        // get quick reference
        const source = state.source;

        api.fire('init', source);

        // Load Files
        if (source instanceof File) {
            api.fire('load', source);
        } else if (source instanceof Blob) {
            // Load blobs, set default name to current date
            api.fire('load', getFileFromBlob(source, source.name));
        } else if (isBase64DataURI(source)) {
            // Load base 64, set default name to current date
            api.fire('load', getFileFromBase64DataURI(source));
        } else {
            // Deal as if is external URL, let's load it!
            loadURL(source);
        }
    };

    // loads a url
    const loadURL = url => {
        // is remote url and no fetch method supplied
        if (!fetchFn) {
            api.fire('error', {
                type: 'error',
                body: "Can't load URL",
                code: 400,
            });
            return;
        }

        // set request start
        state.timestamp = Date.now();

        // load file
        state.request = fetchFn(
            url,
            response => {
                // update duration
                state.duration = Date.now() - state.timestamp;

                // done!
                state.complete = true;

                // turn blob response into a file
                if (response instanceof Blob) {
                    response = getFileFromBlob(response, response.name || getFilenameFromURL(url));
                }

                api.fire(
                    'load',
                    // if has received blob, we go with blob, if no response, we return null
                    response instanceof Blob ? response : response ? response.body : null
                );
            },
            error => {
                api.fire(
                    'error',
                    typeof error === 'string'
                        ? {
                              type: 'error',
                              code: 0,
                              body: error,
                          }
                        : error
                );
            },
            (computable, current, total) => {
                // collected some meta data already
                if (total) {
                    state.size = total;
                }

                // update duration
                state.duration = Date.now() - state.timestamp;

                // if we can't compute progress, we're not going to fire progress events
                if (!computable) {
                    state.progress = null;
                    return;
                }

                // update progress percentage
                state.progress = current / total;

                // expose
                api.fire('progress', state.progress);
            },
            () => {
                api.fire('abort');
            },
            response => {
                const fileinfo = getFileInfoFromHeaders(
                    typeof response === 'string' ? response : response.headers
                );
                api.fire('meta', {
                    size: state.size || fileinfo.size,
                    filename: fileinfo.name,
                    source: fileinfo.source,
                });
            }
        );
    };

    const api = {
        ...on(),
        setSource: source => (state.source = source),
        getProgress, // file load progress
        abort, // abort file load
        load, // start load
    };

    return api;
};

const isGet = method => /GET|HEAD/.test(method);

const sendRequest = (data, url, options) => {
    const api = {
        onheaders: () => {},
        onprogress: () => {},
        onload: () => {},
        ontimeout: () => {},
        onerror: () => {},
        onabort: () => {},
        abort: () => {
            aborted = true;
            xhr.abort();
        },
    };

    // timeout identifier, only used when timeout is defined
    let aborted = false;
    let headersReceived = false;

    // set default options
    options = {
        method: 'POST',
        headers: {},
        withCredentials: false,
        ...options,
    };

    // encode url
    url = encodeURI(url);

    // if method is GET, add any received data to url

    if (isGet(options.method) && data) {
        url = `${url}${encodeURIComponent(typeof data === 'string' ? data : JSON.stringify(data))}`;
    }

    // create request
    const xhr = new XMLHttpRequest();

    // progress of load
    const process = isGet(options.method) ? xhr : xhr.upload;
    process.onprogress = e => {
        // no progress event when aborted ( onprogress is called once after abort() )
        if (aborted) {
            return;
        }

        api.onprogress(e.lengthComputable, e.loaded, e.total);
    };

    // tries to get header info to the app as fast as possible
    xhr.onreadystatechange = () => {
        // not interesting in these states ('unsent' and 'openend' as they don't give us any additional info)
        if (xhr.readyState < 2) {
            return;
        }

        // no server response
        if (xhr.readyState === 4 && xhr.status === 0) {
            return;
        }

        if (headersReceived) {
            return;
        }

        headersReceived = true;

        // we've probably received some useful data in response headers
        api.onheaders(xhr);
    };

    // load successful
    xhr.onload = () => {
        // is classified as valid response
        if (xhr.status >= 200 && xhr.status < 300) {
            api.onload(xhr);
        } else {
            api.onerror(xhr);
        }
    };

    // error during load
    xhr.onerror = () => api.onerror(xhr);

    // request aborted
    xhr.onabort = () => {
        aborted = true;
        api.onabort();
    };

    // request timeout
    xhr.ontimeout = () => api.ontimeout(xhr);

    // open up open up!
    xhr.open(options.method, url, true);

    // set timeout if defined (do it after open so IE11 plays ball)
    if (isInt(options.timeout)) {
        xhr.timeout = options.timeout;
    }

    // add headers
    Object.keys(options.headers).forEach(key => {
        const value = unescape(encodeURIComponent(options.headers[key]));
        xhr.setRequestHeader(key, value);
    });

    // set type of response
    if (options.responseType) {
        xhr.responseType = options.responseType;
    }

    // set credentials
    if (options.withCredentials) {
        xhr.withCredentials = true;
    }

    // let's send our data
    xhr.send(data);

    return api;
};

const createResponse = (type, code, body, headers) => ({
    type,
    code,
    body,
    headers,
});

const createTimeoutResponse = cb => xhr => {
    cb(createResponse('error', 0, 'Timeout', xhr.getAllResponseHeaders()));
};

const hasQS = str => /\?/.test(str);
const buildURL = (...parts) => {
    let url = '';
    parts.forEach(part => {
        url += hasQS(url) && hasQS(part) ? part.replace(/\?/, '&') : part;
    });
    return url;
};

const createFetchFunction = (apiUrl = '', action) => {
    // custom handler (should also handle file, load, error, progress and abort)
    if (typeof action === 'function') {
        return action;
    }

    // no action supplied
    if (!action || !isString(action.url)) {
        return null;
    }

    // set onload hanlder
    const onload = action.onload || (res => res);
    const onerror = action.onerror || (res => null);

    // internal handler
    return (url, load, error, progress, abort, headers) => {
        // do local or remote request based on if the url is external
        const request = sendRequest(url, buildURL(apiUrl, action.url), {
            ...action,
            responseType: 'blob',
        });

        request.onload = xhr => {
            // get headers
            const headers = xhr.getAllResponseHeaders();

            // get filename
            const filename = getFileInfoFromHeaders(headers).name || getFilenameFromURL(url);

            // create response
            load(
                createResponse(
                    'load',
                    xhr.status,
                    action.method === 'HEAD'
                        ? null
                        : getFileFromBlob(onload(xhr.response), filename),
                    headers
                )
            );
        };

        request.onerror = xhr => {
            error(
                createResponse(
                    'error',
                    xhr.status,
                    onerror(xhr.response) || xhr.statusText,
                    xhr.getAllResponseHeaders()
                )
            );
        };

        request.onheaders = xhr => {
            headers(createResponse('headers', xhr.status, null, xhr.getAllResponseHeaders()));
        };

        request.ontimeout = createTimeoutResponse(error);
        request.onprogress = progress;
        request.onabort = abort;

        // should return request
        return request;
    };
};

const ChunkStatus = {
    QUEUED: 0,
    COMPLETE: 1,
    PROCESSING: 2,
    ERROR: 3,
    WAITING: 4,
};

/*
function signature:
  (file, metadata, load, error, progress, abort, transfer, options) => {
    return {
    abort:() => {}
  }
}
*/

// apiUrl, action, name, file, metadata, load, error, progress, abort, transfer, options
const processFileChunked = (
    apiUrl,
    action,
    name,
    file,
    metadata,
    load,
    error,
    progress,
    abort,
    transfer,
    options
) => {
    // all chunks
    const chunks = [];
    const { chunkTransferId, chunkServer, chunkSize, chunkRetryDelays } = options;

    // default state
    const state = {
        serverId: chunkTransferId,
        aborted: false,
    };

    // set onload handlers
    const ondata = action.ondata || (fd => fd);
    const onload =
        action.onload ||
        ((xhr, method) =>
            method === 'HEAD' ? xhr.getResponseHeader('Upload-Offset') : xhr.response);
    const onerror = action.onerror || (res => null);

    // create server hook
    const requestTransferId = cb => {
        const formData = new FormData();

        // add metadata under same name
        if (isObject(metadata)) formData.append(name, JSON.stringify(metadata));

        const headers =
            typeof action.headers === 'function'
                ? action.headers(file, metadata)
                : {
                      ...action.headers,
                      'Upload-Length': file.size,
                  };

        const requestParams = {
            ...action,
            headers,
        };

        // send request object
        const request = sendRequest(ondata(formData), buildURL(apiUrl, action.url), requestParams);

        request.onload = xhr => cb(onload(xhr, requestParams.method));

        request.onerror = xhr =>
            error(
                createResponse(
                    'error',
                    xhr.status,
                    onerror(xhr.response) || xhr.statusText,
                    xhr.getAllResponseHeaders()
                )
            );

        request.ontimeout = createTimeoutResponse(error);
    };

    const requestTransferOffset = cb => {
        const requestUrl = buildURL(apiUrl, chunkServer.url, state.serverId);

        const headers =
            typeof action.headers === 'function'
                ? action.headers(state.serverId)
                : {
                      ...action.headers,
                  };

        const requestParams = {
            headers,
            method: 'HEAD',
        };

        const request = sendRequest(null, requestUrl, requestParams);

        request.onload = xhr => cb(onload(xhr, requestParams.method));

        request.onerror = xhr =>
            error(
                createResponse(
                    'error',
                    xhr.status,
                    onerror(xhr.response) || xhr.statusText,
                    xhr.getAllResponseHeaders()
                )
            );

        request.ontimeout = createTimeoutResponse(error);
    };

    // create chunks
    const lastChunkIndex = Math.floor(file.size / chunkSize);
    for (let i = 0; i <= lastChunkIndex; i++) {
        const offset = i * chunkSize;
        const data = file.slice(offset, offset + chunkSize, 'application/offset+octet-stream');
        chunks[i] = {
            index: i,
            size: data.size,
            offset,
            data,
            file,
            progress: 0,
            retries: [...chunkRetryDelays],
            status: ChunkStatus.QUEUED,
            error: null,
            request: null,
            timeout: null,
        };
    }

    const completeProcessingChunks = () => load(state.serverId);

    const canProcessChunk = chunk =>
        chunk.status === ChunkStatus.QUEUED || chunk.status === ChunkStatus.ERROR;

    const processChunk = chunk => {
        // processing is paused, wait here
        if (state.aborted) return;

        // get next chunk to process
        chunk = chunk || chunks.find(canProcessChunk);

        // no more chunks to process
        if (!chunk) {
            // all done?
            if (chunks.every(chunk => chunk.status === ChunkStatus.COMPLETE)) {
                completeProcessingChunks();
            }

            // no chunk to handle
            return;
        }

        // now processing this chunk
        chunk.status = ChunkStatus.PROCESSING;
        chunk.progress = null;

        // allow parsing of formdata
        const ondata = chunkServer.ondata || (fd => fd);
        const onerror = chunkServer.onerror || (res => null);

        // send request object
        const requestUrl = buildURL(apiUrl, chunkServer.url, state.serverId);

        const headers =
            typeof chunkServer.headers === 'function'
                ? chunkServer.headers(chunk)
                : {
                      ...chunkServer.headers,
                      'Content-Type': 'application/offset+octet-stream',
                      'Upload-Offset': chunk.offset,
                      'Upload-Length': file.size,
                      'Upload-Name': file.name,
                  };

        const request = (chunk.request = sendRequest(ondata(chunk.data), requestUrl, {
            ...chunkServer,
            headers,
        }));

        request.onload = () => {
            // done!
            chunk.status = ChunkStatus.COMPLETE;

            // remove request reference
            chunk.request = null;

            // start processing more chunks
            processChunks();
        };

        request.onprogress = (lengthComputable, loaded, total) => {
            chunk.progress = lengthComputable ? loaded : null;
            updateTotalProgress();
        };

        request.onerror = xhr => {
            chunk.status = ChunkStatus.ERROR;
            chunk.request = null;
            chunk.error = onerror(xhr.response) || xhr.statusText;
            if (!retryProcessChunk(chunk)) {
                error(
                    createResponse(
                        'error',
                        xhr.status,
                        onerror(xhr.response) || xhr.statusText,
                        xhr.getAllResponseHeaders()
                    )
                );
            }
        };

        request.ontimeout = xhr => {
            chunk.status = ChunkStatus.ERROR;
            chunk.request = null;
            if (!retryProcessChunk(chunk)) {
                createTimeoutResponse(error)(xhr);
            }
        };

        request.onabort = () => {
            chunk.status = ChunkStatus.QUEUED;
            chunk.request = null;
            abort();
        };
    };

    const retryProcessChunk = chunk => {
        // no more retries left
        if (chunk.retries.length === 0) return false;

        // new retry
        chunk.status = ChunkStatus.WAITING;
        clearTimeout(chunk.timeout);
        chunk.timeout = setTimeout(() => {
            processChunk(chunk);
        }, chunk.retries.shift());

        // we're going to retry
        return true;
    };

    const updateTotalProgress = () => {
        // calculate total progress fraction
        const totalBytesTransfered = chunks.reduce((p, chunk) => {
            if (p === null || chunk.progress === null) return null;
            return p + chunk.progress;
        }, 0);

        // can't compute progress
        if (totalBytesTransfered === null) return progress(false, 0, 0);

        // calculate progress values
        const totalSize = chunks.reduce((total, chunk) => total + chunk.size, 0);

        // can update progress indicator
        progress(true, totalBytesTransfered, totalSize);
    };

    // process new chunks
    const processChunks = () => {
        const totalProcessing = chunks.filter(chunk => chunk.status === ChunkStatus.PROCESSING)
            .length;
        if (totalProcessing >= 1) return;
        processChunk();
    };

    const abortChunks = () => {
        chunks.forEach(chunk => {
            clearTimeout(chunk.timeout);
            if (chunk.request) {
                chunk.request.abort();
            }
        });
    };

    // let's go!
    if (!state.serverId) {
        requestTransferId(serverId => {
            // stop here if aborted, might have happened in between request and callback
            if (state.aborted) return;

            // pass back to item so we can use it if something goes wrong
            transfer(serverId);

            // store internally
            state.serverId = serverId;
            processChunks();
        });
    } else {
        requestTransferOffset(offset => {
            // stop here if aborted, might have happened in between request and callback
            if (state.aborted) return;

            // mark chunks with lower offset as complete
            chunks
                .filter(chunk => chunk.offset < offset)
                .forEach(chunk => {
                    chunk.status = ChunkStatus.COMPLETE;
                    chunk.progress = chunk.size;
                });

            // continue processing
            processChunks();
        });
    }

    return {
        abort: () => {
            state.aborted = true;
            abortChunks();
        },
    };
};

/*
function signature:
  (file, metadata, load, error, progress, abort) => {
    return {
    abort:() => {}
  }
}
*/
const createFileProcessorFunction = (apiUrl, action, name, options) => (
    file,
    metadata,
    load,
    error,
    progress,
    abort,
    transfer
) => {
    // no file received
    if (!file) return;

    // if was passed a file, and we can chunk it, exit here
    const canChunkUpload = options.chunkUploads;
    const shouldChunkUpload = canChunkUpload && file.size > options.chunkSize;
    const willChunkUpload = canChunkUpload && (shouldChunkUpload || options.chunkForce);
    if (file instanceof Blob && willChunkUpload)
        return processFileChunked(
            apiUrl,
            action,
            name,
            file,
            metadata,
            load,
            error,
            progress,
            abort,
            transfer,
            options
        );

    // set handlers
    const ondata = action.ondata || (fd => fd);
    const onload = action.onload || (res => res);
    const onerror = action.onerror || (res => null);

    const headers =
        typeof action.headers === 'function'
            ? action.headers(file, metadata) || {}
            : {
                  ...action.headers,
              };

    const requestParams = {
        ...action,
        headers,
    };

    // create formdata object
    var formData = new FormData();

    // add metadata under same name
    if (isObject(metadata)) {
        formData.append(name, JSON.stringify(metadata));
    }

    // Turn into an array of objects so no matter what the input, we can handle it the same way
    (file instanceof Blob ? [{ name: null, file }] : file).forEach(item => {
        formData.append(
            name,
            item.file,
            item.name === null ? item.file.name : `${item.name}${item.file.name}`
        );
    });

    // send request object
    const request = sendRequest(ondata(formData), buildURL(apiUrl, action.url), requestParams);
    request.onload = xhr => {
        load(createResponse('load', xhr.status, onload(xhr.response), xhr.getAllResponseHeaders()));
    };

    request.onerror = xhr => {
        error(
            createResponse(
                'error',
                xhr.status,
                onerror(xhr.response) || xhr.statusText,
                xhr.getAllResponseHeaders()
            )
        );
    };

    request.ontimeout = createTimeoutResponse(error);
    request.onprogress = progress;
    request.onabort = abort;

    // should return request
    return request;
};

const createProcessorFunction = (apiUrl = '', action, name, options) => {
    // custom handler (should also handle file, load, error, progress and abort)
    if (typeof action === 'function') return (...params) => action(name, ...params, options);

    // no action supplied
    if (!action || !isString(action.url)) return null;

    // internal handler
    return createFileProcessorFunction(apiUrl, action, name, options);
};

/*
 function signature:
 (uniqueFileId, load, error) => { }
 */
const createRevertFunction = (apiUrl = '', action) => {
    // is custom implementation
    if (typeof action === 'function') {
        return action;
    }

    // no action supplied, return stub function, interface will work, but file won't be removed
    if (!action || !isString(action.url)) {
        return (uniqueFileId, load) => load();
    }

    // set onload hanlder
    const onload = action.onload || (res => res);
    const onerror = action.onerror || (res => null);

    // internal implementation
    return (uniqueFileId, load, error) => {
        const request = sendRequest(
            uniqueFileId,
            apiUrl + action.url,
            action // contains method, headers and withCredentials properties
        );
        request.onload = xhr => {
            load(
                createResponse(
                    'load',
                    xhr.status,
                    onload(xhr.response),
                    xhr.getAllResponseHeaders()
                )
            );
        };

        request.onerror = xhr => {
            error(
                createResponse(
                    'error',
                    xhr.status,
                    onerror(xhr.response) || xhr.statusText,
                    xhr.getAllResponseHeaders()
                )
            );
        };

        request.ontimeout = createTimeoutResponse(error);

        return request;
    };
};

const getRandomNumber = (min = 0, max = 1) => min + Math.random() * (max - min);

const createPerceivedPerformanceUpdater = (
    cb,
    duration = 1000,
    offset = 0,
    tickMin = 25,
    tickMax = 250
) => {
    let timeout = null;
    const start = Date.now();

    const tick = () => {
        let runtime = Date.now() - start;
        let delay = getRandomNumber(tickMin, tickMax);

        if (runtime + delay > duration) {
            delay = runtime + delay - duration;
        }

        let progress = runtime / duration;
        if (progress >= 1 || document.hidden) {
            cb(1);
            return;
        }

        cb(progress);

        timeout = setTimeout(tick, delay);
    };

    if (duration > 0) tick();

    return {
        clear: () => {
            clearTimeout(timeout);
        },
    };
};

const createFileProcessor = (processFn, options) => {
    const state = {
        complete: false,
        perceivedProgress: 0,
        perceivedPerformanceUpdater: null,
        progress: null,
        timestamp: null,
        perceivedDuration: 0,
        duration: 0,
        request: null,
        response: null,
    };

    const { allowMinimumUploadDuration } = options;

    const process = (file, metadata) => {
        const progressFn = () => {
            // we've not yet started the real download, stop here
            // the request might not go through, for instance, there might be some server trouble
            // if state.progress is null, the server does not allow computing progress and we show the spinner instead
            if (state.duration === 0 || state.progress === null) return;

            // as we're now processing, fire the progress event
            api.fire('progress', api.getProgress());
        };

        const completeFn = () => {
            state.complete = true;
            api.fire('load-perceived', state.response.body);
        };

        // let's start processing
        api.fire('start');

        // set request start
        state.timestamp = Date.now();

        // create perceived performance progress indicator
        state.perceivedPerformanceUpdater = createPerceivedPerformanceUpdater(
            progress => {
                state.perceivedProgress = progress;
                state.perceivedDuration = Date.now() - state.timestamp;

                progressFn();

                // if fake progress is done, and a response has been received,
                // and we've not yet called the complete method
                if (state.response && state.perceivedProgress === 1 && !state.complete) {
                    // we done!
                    completeFn();
                }
            },
            // random delay as in a list of files you start noticing
            // files uploading at the exact same speed
            allowMinimumUploadDuration ? getRandomNumber(750, 1500) : 0
        );

        // remember request so we can abort it later
        state.request = processFn(
            // the file to process
            file,

            // the metadata to send along
            metadata,

            // callbacks (load, error, progress, abort, transfer)
            // load expects the body to be a server id if
            // you want to make use of revert
            response => {
                // we put the response in state so we can access
                // it outside of this method
                state.response = isObject(response)
                    ? response
                    : {
                          type: 'load',
                          code: 200,
                          body: `${response}`,
                          headers: {},
                      };

                // update duration
                state.duration = Date.now() - state.timestamp;

                // force progress to 1 as we're now done
                state.progress = 1;

                // actual load is done let's share results
                api.fire('load', state.response.body);

                // we are really done
                // if perceived progress is 1 ( wait for perceived progress to complete )
                // or if server does not support progress ( null )
                if (
                    !allowMinimumUploadDuration ||
                    (allowMinimumUploadDuration && state.perceivedProgress === 1)
                ) {
                    completeFn();
                }
            },

            // error is expected to be an object with type, code, body
            error => {
                // cancel updater
                state.perceivedPerformanceUpdater.clear();

                // update others about this error
                api.fire(
                    'error',
                    isObject(error)
                        ? error
                        : {
                              type: 'error',
                              code: 0,
                              body: `${error}`,
                          }
                );
            },

            // actual processing progress
            (computable, current, total) => {
                // update actual duration
                state.duration = Date.now() - state.timestamp;

                // update actual progress
                state.progress = computable ? current / total : null;

                progressFn();
            },

            // abort does not expect a value
            () => {
                // stop updater
                state.perceivedPerformanceUpdater.clear();

                // fire the abort event so we can switch visuals
                api.fire('abort', state.response ? state.response.body : null);
            },

            // register the id for this transfer
            transferId => {
                api.fire('transfer', transferId);
            }
        );
    };

    const abort = () => {
        // no request running, can't abort
        if (!state.request) return;

        // stop updater
        state.perceivedPerformanceUpdater.clear();

        // abort actual request
        if (state.request.abort) state.request.abort();

        // if has response object, we've completed the request
        state.complete = true;
    };

    const reset = () => {
        abort();
        state.complete = false;
        state.perceivedProgress = 0;
        state.progress = 0;
        state.timestamp = null;
        state.perceivedDuration = 0;
        state.duration = 0;
        state.request = null;
        state.response = null;
    };

    const getProgress = allowMinimumUploadDuration
        ? () => (state.progress ? Math.min(state.progress, state.perceivedProgress) : null)
        : () => state.progress || null;

    const getDuration = allowMinimumUploadDuration
        ? () => Math.min(state.duration, state.perceivedDuration)
        : () => state.duration;

    const api = {
        ...on(),
        process, // start processing file
        abort, // abort active process request
        getProgress,
        getDuration,
        reset,
    };

    return api;
};

const getFilenameWithoutExtension = name => name.substring(0, name.lastIndexOf('.')) || name;

const createFileStub = source => {
    let data = [source.name, source.size, source.type];

    // is blob or base64, then we need to set the name
    if (source instanceof Blob || isBase64DataURI(source)) {
        data[0] = source.name || getDateString();
    } else if (isBase64DataURI(source)) {
        // if is base64 data uri we need to determine the average size and type
        data[1] = source.length;
        data[2] = getMimeTypeFromBase64DataURI(source);
    } else if (isString(source)) {
        // url
        data[0] = getFilenameFromURL(source);
        data[1] = 0;
        data[2] = 'application/octet-stream';
    }

    return {
        name: data[0],
        size: data[1],
        type: data[2],
    };
};

const isFile = value => !!(value instanceof File || (value instanceof Blob && value.name));

const deepCloneObject = src => {
    if (!isObject(src)) return src;
    const target = isArray(src) ? [] : {};
    for (const key in src) {
        if (!src.hasOwnProperty(key)) continue;
        const v = src[key];
        target[key] = v && isObject(v) ? deepCloneObject(v) : v;
    }
    return target;
};

const createItem = (origin = null, serverFileReference = null, file = null) => {
    // unique id for this item, is used to identify the item across views
    const id = getUniqueId();

    /**
     * Internal item state
     */
    const state = {
        // is archived
        archived: false,

        // if is frozen, no longer fires events
        frozen: false,

        // removed from view
        released: false,

        // original source
        source: null,

        // file model reference
        file,

        // id of file on server
        serverFileReference,

        // id of file transfer on server
        transferId: null,

        // is aborted
        processingAborted: false,

        // current item status
        status: serverFileReference ? ItemStatus.PROCESSING_COMPLETE : ItemStatus.INIT,

        // active processes
        activeLoader: null,
        activeProcessor: null,
    };

    // callback used when abort processing is called to link back to the resolve method
    let abortProcessingRequestComplete = null;

    /**
     * Externally added item metadata
     */
    const metadata = {};

    // item data
    const setStatus = status => (state.status = status);

    // fire event unless the item has been archived
    const fire = (event, ...params) => {
        if (state.released || state.frozen) return;
        api.fire(event, ...params);
    };

    // file data
    const getFileExtension = () => getExtensionFromFilename(state.file.name);
    const getFileType = () => state.file.type;
    const getFileSize = () => state.file.size;
    const getFile = () => state.file;

    //
    // logic to load a file
    //
    const load = (source, loader, onload) => {
        // remember the original item source
        state.source = source;

        // source is known
        api.fireSync('init');

        // file stub is already there
        if (state.file) {
            api.fireSync('load-skip');
            return;
        }

        // set a stub file object while loading the actual data
        state.file = createFileStub(source);

        // starts loading
        loader.on('init', () => {
            fire('load-init');
        });

        // we'eve received a size indication, let's update the stub
        loader.on('meta', meta => {
            // set size of file stub
            state.file.size = meta.size;

            // set name of file stub
            state.file.filename = meta.filename;

            // if has received source, we done
            if (meta.source) {
                origin = FileOrigin.LIMBO;
                state.serverFileReference = meta.source;
                state.status = ItemStatus.PROCESSING_COMPLETE;
            }

            // size has been updated
            fire('load-meta');
        });

        // the file is now loading we need to update the progress indicators
        loader.on('progress', progress => {
            setStatus(ItemStatus.LOADING);

            fire('load-progress', progress);
        });

        // an error was thrown while loading the file, we need to switch to error state
        loader.on('error', error => {
            setStatus(ItemStatus.LOAD_ERROR);

            fire('load-request-error', error);
        });

        // user or another process aborted the file load (cannot retry)
        loader.on('abort', () => {
            setStatus(ItemStatus.INIT);
            fire('load-abort');
        });

        // done loading
        loader.on('load', file => {
            // as we've now loaded the file the loader is no longer required
            state.activeLoader = null;

            // called when file has loaded succesfully
            const success = result => {
                // set (possibly) transformed file
                state.file = isFile(result) ? result : state.file;

                // file received
                if (origin === FileOrigin.LIMBO && state.serverFileReference) {
                    setStatus(ItemStatus.PROCESSING_COMPLETE);
                } else {
                    setStatus(ItemStatus.IDLE);
                }

                fire('load');
            };

            const error = result => {
                // set original file
                state.file = file;
                fire('load-meta');

                setStatus(ItemStatus.LOAD_ERROR);
                fire('load-file-error', result);
            };

            // if we already have a server file reference, we don't need to call the onload method
            if (state.serverFileReference) {
                success(file);
                return;
            }

            // no server id, let's give this file the full treatment
            onload(file, success, error);
        });

        // set loader source data
        loader.setSource(source);

        // set as active loader
        state.activeLoader = loader;

        // load the source data
        loader.load();
    };

    const retryLoad = () => {
        if (!state.activeLoader) {
            return;
        }
        state.activeLoader.load();
    };

    const abortLoad = () => {
        if (state.activeLoader) {
            state.activeLoader.abort();
            return;
        }
        setStatus(ItemStatus.INIT);
        fire('load-abort');
    };

    //
    // logic to process a file
    //
    const process = (processor, onprocess) => {
        // processing was aborted
        if (state.processingAborted) {
            state.processingAborted = false;
            return;
        }

        // now processing
        setStatus(ItemStatus.PROCESSING);

        // reset abort callback
        abortProcessingRequestComplete = null;

        // if no file loaded we'll wait for the load event
        if (!(state.file instanceof Blob)) {
            api.on('load', () => {
                process(processor, onprocess);
            });
            return;
        }

        // setup processor
        processor.on('load', serverFileReference => {
            // need this id to be able to revert the upload
            state.transferId = null;
            state.serverFileReference = serverFileReference;
        });

        // register transfer id
        processor.on('transfer', transferId => {
            // need this id to be able to revert the upload
            state.transferId = transferId;
        });

        processor.on('load-perceived', serverFileReference => {
            // no longer required
            state.activeProcessor = null;

            // need this id to be able to rever the upload
            state.transferId = null;
            state.serverFileReference = serverFileReference;

            setStatus(ItemStatus.PROCESSING_COMPLETE);
            fire('process-complete', serverFileReference);
        });

        processor.on('start', () => {
            fire('process-start');
        });

        processor.on('error', error => {
            state.activeProcessor = null;
            setStatus(ItemStatus.PROCESSING_ERROR);
            fire('process-error', error);
        });

        processor.on('abort', serverFileReference => {
            state.activeProcessor = null;

            // if file was uploaded but processing was cancelled during perceived processor time store file reference
            state.serverFileReference = serverFileReference;

            setStatus(ItemStatus.IDLE);
            fire('process-abort');

            // has timeout so doesn't interfere with remove action
            if (abortProcessingRequestComplete) {
                abortProcessingRequestComplete();
            }
        });

        processor.on('progress', progress => {
            fire('process-progress', progress);
        });

        // when successfully transformed
        const success = file => {
            // if was archived in the mean time, don't process
            if (state.archived) return;

            // process file!
            processor.process(file, { ...metadata });
        };

        // something went wrong during transform phase
        const error = console.error;

        // start processing the file
        onprocess(state.file, success, error);

        // set as active processor
        state.activeProcessor = processor;
    };

    const requestProcessing = () => {
        state.processingAborted = false;
        setStatus(ItemStatus.PROCESSING_QUEUED);
    };

    const abortProcessing = () =>
        new Promise(resolve => {
            if (!state.activeProcessor) {
                state.processingAborted = true;

                setStatus(ItemStatus.IDLE);
                fire('process-abort');

                resolve();
                return;
            }

            abortProcessingRequestComplete = () => {
                resolve();
            };

            state.activeProcessor.abort();
        });

    //
    // logic to revert a processed file
    //
    const revert = (revertFileUpload, forceRevert) =>
        new Promise((resolve, reject) => {
            // a completed upload will have a serverFileReference, a failed chunked upload where
            // getting a serverId succeeded but >=0 chunks have been uploaded will have transferId set
            const serverTransferId =
                state.serverFileReference !== null ? state.serverFileReference : state.transferId;

            // cannot revert without a server id for this process
            if (serverTransferId === null) {
                resolve();
                return;
            }

            // revert the upload (fire and forget)
            revertFileUpload(
                serverTransferId,
                () => {
                    // reset file server id and transfer id as now it's not available on the server
                    state.serverFileReference = null;
                    state.transferId = null;
                    resolve();
                },
                error => {
                    // don't set error state when reverting is optional, it will always resolve
                    if (!forceRevert) {
                        resolve();
                        return;
                    }

                    // oh no errors
                    setStatus(ItemStatus.PROCESSING_REVERT_ERROR);
                    fire('process-revert-error');
                    reject(error);
                }
            );

            // fire event
            setStatus(ItemStatus.IDLE);
            fire('process-revert');
        });

    // exposed methods
    const setMetadata = (key, value, silent) => {
        const keys = key.split('.');
        const root = keys[0];
        const last = keys.pop();
        let data = metadata;
        keys.forEach(key => (data = data[key]));

        // compare old value against new value, if they're the same, we're not updating
        if (JSON.stringify(data[last]) === JSON.stringify(value)) return;

        // update value
        data[last] = value;

        // fire update
        fire('metadata-update', {
            key: root,
            value: metadata[root],
            silent,
        });
    };

    const getMetadata = key => deepCloneObject(key ? metadata[key] : metadata);

    const api = {
        id: { get: () => id },
        origin: { get: () => origin, set: value => (origin = value) },
        serverId: { get: () => state.serverFileReference },
        transferId: { get: () => state.transferId },
        status: { get: () => state.status },
        filename: { get: () => state.file.name },
        filenameWithoutExtension: { get: () => getFilenameWithoutExtension(state.file.name) },
        fileExtension: { get: getFileExtension },
        fileType: { get: getFileType },
        fileSize: { get: getFileSize },
        file: { get: getFile },
        relativePath: { get: () => state.file._relativePath },

        source: { get: () => state.source },

        getMetadata,
        setMetadata: (key, value, silent) => {
            if (isObject(key)) {
                const data = key;
                Object.keys(data).forEach(key => {
                    setMetadata(key, data[key], value);
                });
                return key;
            }
            setMetadata(key, value, silent);
            return value;
        },

        extend: (name, handler) => (itemAPI[name] = handler),

        abortLoad,
        retryLoad,
        requestProcessing,
        abortProcessing,

        load,
        process,
        revert,

        ...on(),

        freeze: () => (state.frozen = true),

        release: () => (state.released = true),
        released: { get: () => state.released },

        archive: () => (state.archived = true),
        archived: { get: () => state.archived },
    };

    // create it here instead of returning it instantly so we can extend it later
    const itemAPI = createObject(api);

    return itemAPI;
};

const getItemIndexByQuery = (items, query) => {
    // just return first index
    if (isEmpty(query)) {
        return 0;
    }

    // invalid queries
    if (!isString(query)) {
        return -1;
    }

    // return item by id (or -1 if not found)
    return items.findIndex(item => item.id === query);
};

const getItemById = (items, itemId) => {
    const index = getItemIndexByQuery(items, itemId);
    if (index < 0) {
        return;
    }
    return items[index] || null;
};

const fetchBlob = (url, load, error, progress, abort, headers) => {
    const request = sendRequest(null, url, {
        method: 'GET',
        responseType: 'blob',
    });

    request.onload = xhr => {
        // get headers
        const headers = xhr.getAllResponseHeaders();

        // get filename
        const filename = getFileInfoFromHeaders(headers).name || getFilenameFromURL(url);

        // create response
        load(createResponse('load', xhr.status, getFileFromBlob(xhr.response, filename), headers));
    };

    request.onerror = xhr => {
        error(createResponse('error', xhr.status, xhr.statusText, xhr.getAllResponseHeaders()));
    };

    request.onheaders = xhr => {
        headers(createResponse('headers', xhr.status, null, xhr.getAllResponseHeaders()));
    };

    request.ontimeout = createTimeoutResponse(error);
    request.onprogress = progress;
    request.onabort = abort;

    // should return request
    return request;
};

const getDomainFromURL = url => {
    if (url.indexOf('//') === 0) {
        url = location.protocol + url;
    }
    return url
        .toLowerCase()
        .replace('blob:', '')
        .replace(/([a-z])?:\/\//, '$1')
        .split('/')[0];
};

const isExternalURL = url =>
    (url.indexOf(':') > -1 || url.indexOf('//') > -1) &&
    getDomainFromURL(location.href) !== getDomainFromURL(url);

const dynamicLabel = label => (...params) => (isFunction(label) ? label(...params) : label);

const isMockItem = item => !isFile(item.file);

const listUpdated = (dispatch, state) => {
    clearTimeout(state.listUpdateTimeout);
    state.listUpdateTimeout = setTimeout(() => {
        dispatch('DID_UPDATE_ITEMS', { items: getActiveItems(state.items) });
    }, 0);
};

const optionalPromise = (fn, ...params) =>
    new Promise(resolve => {
        if (!fn) {
            return resolve(true);
        }

        const result = fn(...params);

        if (result == null) {
            return resolve(true);
        }

        if (typeof result === 'boolean') {
            return resolve(result);
        }

        if (typeof result.then === 'function') {
            result.then(resolve);
        }
    });

const sortItems = (state, compare) => {
    state.items.sort((a, b) => compare(createItemAPI(a), createItemAPI(b)));
};

// returns item based on state
const getItemByQueryFromState = (state, itemHandler) => ({
    query,
    success = () => {},
    failure = () => {},
    ...options
} = {}) => {
    const item = getItemByQuery(state.items, query);
    if (!item) {
        failure({
            error: createResponse('error', 0, 'Item not found'),
            file: null,
        });
        return;
    }
    itemHandler(item, success, failure, options || {});
};

const actions = (dispatch, query, state) => ({
    /**
     * Aborts all ongoing processes
     */
    ABORT_ALL: () => {
        getActiveItems(state.items).forEach(item => {
            item.freeze();
            item.abortLoad();
            item.abortProcessing();
        });
    },

    /**
     * Sets initial files
     */
    DID_SET_FILES: ({ value = [] }) => {
        // map values to file objects
        const files = value.map(file => ({
            source: file.source ? file.source : file,
            options: file.options,
        }));

        // loop over files, if file is in list, leave it be, if not, remove
        // test if items should be moved
        let activeItems = getActiveItems(state.items);

        activeItems.forEach(item => {
            // if item not is in new value, remove
            if (!files.find(file => file.source === item.source || file.source === item.file)) {
                dispatch('REMOVE_ITEM', { query: item, remove: false });
            }
        });

        // add new files
        activeItems = getActiveItems(state.items);
        files.forEach((file, index) => {
            // if file is already in list
            if (activeItems.find(item => item.source === file.source || item.file === file.source))
                return;

            // not in list, add
            dispatch('ADD_ITEM', {
                ...file,
                interactionMethod: InteractionMethod.NONE,
                index,
            });
        });
    },

    DID_UPDATE_ITEM_METADATA: ({ id, action, change }) => {
        // don't do anything
        if (change.silent) return;

        // if is called multiple times in close succession we combined all calls together to save resources
        clearTimeout(state.itemUpdateTimeout);
        state.itemUpdateTimeout = setTimeout(() => {
            const item = getItemById(state.items, id);

            // only revert and attempt to upload when we're uploading to a server
            if (!query('IS_ASYNC')) {
                // should we update the output data
                applyFilterChain('SHOULD_PREPARE_OUTPUT', false, {
                    item,
                    query,
                    action,
                    change,
                }).then(shouldPrepareOutput => {
                    // plugins determined the output data should be prepared (or not), can be adjusted with beforePrepareOutput hook
                    const beforePrepareFile = query('GET_BEFORE_PREPARE_FILE');
                    if (beforePrepareFile)
                        shouldPrepareOutput = beforePrepareFile(item, shouldPrepareOutput);

                    if (!shouldPrepareOutput) return;

                    dispatch(
                        'REQUEST_PREPARE_OUTPUT',
                        {
                            query: id,
                            item,
                            success: file => {
                                dispatch('DID_PREPARE_OUTPUT', { id, file });
                            },
                        },
                        true
                    );
                });

                return;
            }

            // if is local item we need to enable upload button so change can be propagated to server
            if (item.origin === FileOrigin.LOCAL) {
                dispatch('DID_LOAD_ITEM', {
                    id: item.id,
                    error: null,
                    serverFileReference: item.source,
                });
            }

            // for async scenarios
            const upload = () => {
                // we push this forward a bit so the interface is updated correctly
                setTimeout(() => {
                    dispatch('REQUEST_ITEM_PROCESSING', { query: id });
                }, 32);
            };

            const revert = doUpload => {
                item.revert(
                    createRevertFunction(state.options.server.url, state.options.server.revert),
                    query('GET_FORCE_REVERT')
                )
                    .then(doUpload ? upload : () => {})
                    .catch(() => {});
            };

            const abort = doUpload => {
                item.abortProcessing().then(doUpload ? upload : () => {});
            };

            // if we should re-upload the file immediately
            if (item.status === ItemStatus.PROCESSING_COMPLETE) {
                return revert(state.options.instantUpload);
            }

            // if currently uploading, cancel upload
            if (item.status === ItemStatus.PROCESSING) {
                return abort(state.options.instantUpload);
            }

            if (state.options.instantUpload) {
                upload();
            }
        }, 0);
    },

    MOVE_ITEM: ({ query, index }) => {
        const item = getItemByQuery(state.items, query);
        if (!item) return;
        const currentIndex = state.items.indexOf(item);
        index = limit(index, 0, state.items.length - 1);
        if (currentIndex === index) return;
        state.items.splice(index, 0, state.items.splice(currentIndex, 1)[0]);
    },

    SORT: ({ compare }) => {
        sortItems(state, compare);
        dispatch('DID_SORT_ITEMS', {
            items: query('GET_ACTIVE_ITEMS'),
        });
    },

    ADD_ITEMS: ({ items, index, interactionMethod, success = () => {}, failure = () => {} }) => {
        let currentIndex = index;

        if (index === -1 || typeof index === 'undefined') {
            const insertLocation = query('GET_ITEM_INSERT_LOCATION');
            const totalItems = query('GET_TOTAL_ITEMS');
            currentIndex = insertLocation === 'before' ? 0 : totalItems;
        }

        const ignoredFiles = query('GET_IGNORED_FILES');
        const isValidFile = source =>
            isFile(source) ? !ignoredFiles.includes(source.name.toLowerCase()) : !isEmpty(source);
        const validItems = items.filter(isValidFile);

        const promises = validItems.map(
            source =>
                new Promise((resolve, reject) => {
                    dispatch('ADD_ITEM', {
                        interactionMethod,
                        source: source.source || source,
                        success: resolve,
                        failure: reject,
                        index: currentIndex++,
                        options: source.options || {},
                    });
                })
        );

        Promise.all(promises)
            .then(success)
            .catch(failure);
    },

    /**
     * @param source
     * @param index
     * @param interactionMethod
     */
    ADD_ITEM: ({
        source,
        index = -1,
        interactionMethod,
        success = () => {},
        failure = () => {},
        options = {},
    }) => {
        // if no source supplied
        if (isEmpty(source)) {
            failure({
                error: createResponse('error', 0, 'No source'),
                file: null,
            });
            return;
        }

        // filter out invalid file items, used to filter dropped directory contents
        if (isFile(source) && state.options.ignoredFiles.includes(source.name.toLowerCase())) {
            // fail silently
            return;
        }

        // test if there's still room in the list of files
        if (!hasRoomForItem(state)) {
            // if multiple allowed, we can't replace
            // or if only a single item is allowed but we're not allowed to replace it we exit
            if (
                state.options.allowMultiple ||
                (!state.options.allowMultiple && !state.options.allowReplace)
            ) {
                const error = createResponse('warning', 0, 'Max files');

                dispatch('DID_THROW_MAX_FILES', {
                    source,
                    error,
                });

                failure({ error, file: null });

                return;
            }

            // let's replace the item
            // id of first item we're about to remove
            const item = getActiveItems(state.items)[0];

            // if has been processed remove it from the server as well
            if (
                item.status === ItemStatus.PROCESSING_COMPLETE ||
                item.status === ItemStatus.PROCESSING_REVERT_ERROR
            ) {
                const forceRevert = query('GET_FORCE_REVERT');
                item.revert(
                    createRevertFunction(state.options.server.url, state.options.server.revert),
                    forceRevert
                )
                    .then(() => {
                        if (!forceRevert) return;

                        // try to add now
                        dispatch('ADD_ITEM', {
                            source,
                            index,
                            interactionMethod,
                            success,
                            failure,
                            options,
                        });
                    })
                    .catch(() => {}); // no need to handle this catch state for now

                if (forceRevert) return;
            }

            // remove first item as it will be replaced by this item
            dispatch('REMOVE_ITEM', { query: item.id });
        }

        // where did the file originate
        const origin =
            options.type === 'local'
                ? FileOrigin.LOCAL
                : options.type === 'limbo'
                ? FileOrigin.LIMBO
                : FileOrigin.INPUT;

        // create a new blank item
        const item = createItem(
            // where did this file come from
            origin,

            // an input file never has a server file reference
            origin === FileOrigin.INPUT ? null : source,

            // file mock data, if defined
            options.file
        );

        // set initial meta data
        Object.keys(options.metadata || {}).forEach(key => {
            item.setMetadata(key, options.metadata[key]);
        });

        // created the item, let plugins add methods
        applyFilters('DID_CREATE_ITEM', item, { query, dispatch });

        // where to insert new items
        const itemInsertLocation = query('GET_ITEM_INSERT_LOCATION');

        // adjust index if is not allowed to pick location
        if (!state.options.itemInsertLocationFreedom) {
            index = itemInsertLocation === 'before' ? -1 : state.items.length;
        }

        // add item to list
        insertItem(state.items, item, index);

        // sort items in list
        if (isFunction(itemInsertLocation) && source) {
            sortItems(state, itemInsertLocation);
        }

        // get a quick reference to the item id
        const id = item.id;

        // observe item events
        item.on('init', () => {
            dispatch('DID_INIT_ITEM', { id });
        });

        item.on('load-init', () => {
            dispatch('DID_START_ITEM_LOAD', { id });
        });

        item.on('load-meta', () => {
            dispatch('DID_UPDATE_ITEM_META', { id });
        });

        item.on('load-progress', progress => {
            dispatch('DID_UPDATE_ITEM_LOAD_PROGRESS', { id, progress });
        });

        item.on('load-request-error', error => {
            const mainStatus = dynamicLabel(state.options.labelFileLoadError)(error);

            // is client error, no way to recover
            if (error.code >= 400 && error.code < 500) {
                dispatch('DID_THROW_ITEM_INVALID', {
                    id,
                    error,
                    status: {
                        main: mainStatus,
                        sub: `${error.code} (${error.body})`,
                    },
                });

                // reject the file so can be dealt with through API
                failure({ error, file: createItemAPI(item) });
                return;
            }

            // is possible server error, so might be possible to retry
            dispatch('DID_THROW_ITEM_LOAD_ERROR', {
                id,
                error,
                status: {
                    main: mainStatus,
                    sub: state.options.labelTapToRetry,
                },
            });
        });

        item.on('load-file-error', error => {
            dispatch('DID_THROW_ITEM_INVALID', {
                id,
                error: error.status,
                status: error.status,
            });
            failure({ error: error.status, file: createItemAPI(item) });
        });

        item.on('load-abort', () => {
            dispatch('REMOVE_ITEM', { query: id });
        });

        item.on('load-skip', () => {
            dispatch('COMPLETE_LOAD_ITEM', {
                query: id,
                item,
                data: {
                    source,
                    success,
                },
            });
        });

        item.on('load', () => {
            const handleAdd = shouldAdd => {
                // no should not add this file
                if (!shouldAdd) {
                    dispatch('REMOVE_ITEM', {
                        query: id,
                    });
                    return;
                }

                // now interested in metadata updates
                item.on('metadata-update', change => {
                    dispatch('DID_UPDATE_ITEM_METADATA', { id, change });
                });

                // let plugins decide if the output data should be prepared at this point
                // means we'll do this and wait for idle state
                applyFilterChain('SHOULD_PREPARE_OUTPUT', false, { item, query }).then(
                    shouldPrepareOutput => {
                        // plugins determined the output data should be prepared (or not), can be adjusted with beforePrepareOutput hook
                        const beforePrepareFile = query('GET_BEFORE_PREPARE_FILE');
                        if (beforePrepareFile)
                            shouldPrepareOutput = beforePrepareFile(item, shouldPrepareOutput);

                        const loadComplete = () => {
                            dispatch('COMPLETE_LOAD_ITEM', {
                                query: id,
                                item,
                                data: {
                                    source,
                                    success,
                                },
                            });

                            listUpdated(dispatch, state);
                        };

                        // exit
                        if (shouldPrepareOutput) {
                            // wait for idle state and then run PREPARE_OUTPUT
                            dispatch(
                                'REQUEST_PREPARE_OUTPUT',
                                {
                                    query: id,
                                    item,
                                    success: file => {
                                        dispatch('DID_PREPARE_OUTPUT', { id, file });
                                        loadComplete();
                                    },
                                },
                                true
                            );

                            return;
                        }

                        loadComplete();
                    }
                );
            };

            // item loaded, allow plugins to
            // - read data (quickly)
            // - add metadata
            applyFilterChain('DID_LOAD_ITEM', item, { query, dispatch })
                .then(() => {
                    optionalPromise(query('GET_BEFORE_ADD_FILE'), createItemAPI(item)).then(
                        handleAdd
                    );
                })
                .catch(e => {
                    if (!e || !e.error || !e.status) return handleAdd(false);
                    dispatch('DID_THROW_ITEM_INVALID', {
                        id,
                        error: e.error,
                        status: e.status,
                    });
                });
        });

        item.on('process-start', () => {
            dispatch('DID_START_ITEM_PROCESSING', { id });
        });

        item.on('process-progress', progress => {
            dispatch('DID_UPDATE_ITEM_PROCESS_PROGRESS', { id, progress });
        });

        item.on('process-error', error => {
            dispatch('DID_THROW_ITEM_PROCESSING_ERROR', {
                id,
                error,
                status: {
                    main: dynamicLabel(state.options.labelFileProcessingError)(error),
                    sub: state.options.labelTapToRetry,
                },
            });
        });

        item.on('process-revert-error', error => {
            dispatch('DID_THROW_ITEM_PROCESSING_REVERT_ERROR', {
                id,
                error,
                status: {
                    main: dynamicLabel(state.options.labelFileProcessingRevertError)(error),
                    sub: state.options.labelTapToRetry,
                },
            });
        });

        item.on('process-complete', serverFileReference => {
            dispatch('DID_COMPLETE_ITEM_PROCESSING', {
                id,
                error: null,
                serverFileReference,
            });
            dispatch('DID_DEFINE_VALUE', { id, value: serverFileReference });
        });

        item.on('process-abort', () => {
            dispatch('DID_ABORT_ITEM_PROCESSING', { id });
        });

        item.on('process-revert', () => {
            dispatch('DID_REVERT_ITEM_PROCESSING', { id });
            dispatch('DID_DEFINE_VALUE', { id, value: null });
        });

        // let view know the item has been inserted
        dispatch('DID_ADD_ITEM', { id, index, interactionMethod });

        listUpdated(dispatch, state);

        // start loading the source
        const { url, load, restore, fetch } = state.options.server || {};

        item.load(
            source,

            // this creates a function that loads the file based on the type of file (string, base64, blob, file) and location of file (local, remote, limbo)
            createFileLoader(
                origin === FileOrigin.INPUT
                    ? // input, if is remote, see if should use custom fetch, else use default fetchBlob
                      isString(source) && isExternalURL(source)
                        ? fetch
                            ? createFetchFunction(url, fetch)
                            : fetchBlob // remote url
                        : fetchBlob // try to fetch url
                    : // limbo or local
                    origin === FileOrigin.LIMBO
                    ? createFetchFunction(url, restore) // limbo
                    : createFetchFunction(url, load) // local
            ),

            // called when the file is loaded so it can be piped through the filters
            (file, success, error) => {
                // let's process the file
                applyFilterChain('LOAD_FILE', file, { query })
                    .then(success)
                    .catch(error);
            }
        );
    },

    REQUEST_PREPARE_OUTPUT: ({ item, success, failure = () => {} }) => {
        // error response if item archived
        const err = {
            error: createResponse('error', 0, 'Item not found'),
            file: null,
        };

        // don't handle archived items, an item could have been archived (load aborted) while waiting to be prepared
        if (item.archived) return failure(err);

        // allow plugins to alter the file data
        applyFilterChain('PREPARE_OUTPUT', item.file, { query, item }).then(result => {
            applyFilterChain('COMPLETE_PREPARE_OUTPUT', result, { query, item }).then(result => {
                // don't handle archived items, an item could have been archived (load aborted) while being prepared
                if (item.archived) return failure(err);

                // we done!
                success(result);
            });
        });
    },

    COMPLETE_LOAD_ITEM: ({ item, data }) => {
        const { success, source } = data;

        // sort items in list
        const itemInsertLocation = query('GET_ITEM_INSERT_LOCATION');
        if (isFunction(itemInsertLocation) && source) {
            sortItems(state, itemInsertLocation);
        }

        // let interface know the item has loaded
        dispatch('DID_LOAD_ITEM', {
            id: item.id,
            error: null,
            serverFileReference: item.origin === FileOrigin.INPUT ? null : source,
        });

        // item has been successfully loaded and added to the
        // list of items so can now be safely returned for use
        success(createItemAPI(item));

        // if this is a local server file we need to show a different state
        if (item.origin === FileOrigin.LOCAL) {
            dispatch('DID_LOAD_LOCAL_ITEM', { id: item.id });
            return;
        }

        // if is a temp server file we prevent async upload call here (as the file is already on the server)
        if (item.origin === FileOrigin.LIMBO) {
            dispatch('DID_COMPLETE_ITEM_PROCESSING', {
                id: item.id,
                error: null,
                serverFileReference: source,
            });

            dispatch('DID_DEFINE_VALUE', {
                id: item.id,
                value: item.serverId || source,
            });
            return;
        }

        // id we are allowed to upload the file immediately, lets do it
        if (query('IS_ASYNC') && state.options.instantUpload) {
            dispatch('REQUEST_ITEM_PROCESSING', { query: item.id });
        }
    },

    RETRY_ITEM_LOAD: getItemByQueryFromState(state, item => {
        // try loading the source one more time
        item.retryLoad();
    }),

    REQUEST_ITEM_PREPARE: getItemByQueryFromState(state, (item, success, failure) => {
        dispatch(
            'REQUEST_PREPARE_OUTPUT',
            {
                query: item.id,
                item,
                success: file => {
                    dispatch('DID_PREPARE_OUTPUT', { id: item.id, file });
                    success({
                        file: item,
                        output: file,
                    });
                },
                failure,
            },
            true
        );
    }),

    REQUEST_ITEM_PROCESSING: getItemByQueryFromState(state, (item, success, failure) => {
        // cannot be queued (or is already queued)
        const itemCanBeQueuedForProcessing =
            // waiting for something
            item.status === ItemStatus.IDLE ||
            // processing went wrong earlier
            item.status === ItemStatus.PROCESSING_ERROR;

        // not ready to be processed
        if (!itemCanBeQueuedForProcessing) {
            const processNow = () =>
                dispatch('REQUEST_ITEM_PROCESSING', { query: item, success, failure });

            const process = () => (document.hidden ? processNow() : setTimeout(processNow, 32));

            // if already done processing or tried to revert but didn't work, try again
            if (
                item.status === ItemStatus.PROCESSING_COMPLETE ||
                item.status === ItemStatus.PROCESSING_REVERT_ERROR
            ) {
                item.revert(
                    createRevertFunction(state.options.server.url, state.options.server.revert),
                    query('GET_FORCE_REVERT')
                )
                    .then(process)
                    .catch(() => {}); // don't continue with processing if something went wrong
            } else if (item.status === ItemStatus.PROCESSING) {
                item.abortProcessing().then(process);
            }

            return;
        }

        // already queued for processing
        if (item.status === ItemStatus.PROCESSING_QUEUED) return;

        item.requestProcessing();

        dispatch('DID_REQUEST_ITEM_PROCESSING', { id: item.id });

        dispatch('PROCESS_ITEM', { query: item, success, failure }, true);
    }),

    PROCESS_ITEM: getItemByQueryFromState(state, (item, success, failure) => {
        const maxParallelUploads = query('GET_MAX_PARALLEL_UPLOADS');
        const totalCurrentUploads = query('GET_ITEMS_BY_STATUS', ItemStatus.PROCESSING).length;

        // queue and wait till queue is freed up
        if (totalCurrentUploads === maxParallelUploads) {
            // queue for later processing
            state.processingQueue.push({
                id: item.id,
                success,
                failure,
            });

            // stop it!
            return;
        }

        // if was not queued or is already processing exit here
        if (item.status === ItemStatus.PROCESSING) return;

        const processNext = () => {
            // process queueud items
            const queueEntry = state.processingQueue.shift();

            // no items left
            if (!queueEntry) return;

            // get item reference
            const { id, success, failure } = queueEntry;
            const itemReference = getItemByQuery(state.items, id);

            // if item was archived while in queue, jump to next
            if (!itemReference || itemReference.archived) {
                processNext();
                return;
            }

            // process queued item
            dispatch('PROCESS_ITEM', { query: id, success, failure }, true);
        };

        // we done function
        item.onOnce('process-complete', () => {
            success(createItemAPI(item));
            processNext();

            // if origin is local, and we're instant uploading, trigger remove of original
            // as revert will remove file from list
            const server = state.options.server;
            const instantUpload = state.options.instantUpload;
            if (instantUpload && item.origin === FileOrigin.LOCAL && isFunction(server.remove)) {
                const noop = () => {};
                item.origin = FileOrigin.LIMBO;
                state.options.server.remove(item.source, noop, noop);
            }

            // All items processed? No errors?
            const allItemsProcessed =
                query('GET_ITEMS_BY_STATUS', ItemStatus.PROCESSING_COMPLETE).length ===
                state.items.length;
            if (allItemsProcessed) {
                dispatch('DID_COMPLETE_ITEM_PROCESSING_ALL');
            }
        });

        // we error function
        item.onOnce('process-error', error => {
            failure({ error, file: createItemAPI(item) });
            processNext();
        });

        // start file processing
        const options = state.options;
        item.process(
            createFileProcessor(
                createProcessorFunction(options.server.url, options.server.process, options.name, {
                    chunkTransferId: item.transferId,
                    chunkServer: options.server.patch,
                    chunkUploads: options.chunkUploads,
                    chunkForce: options.chunkForce,
                    chunkSize: options.chunkSize,
                    chunkRetryDelays: options.chunkRetryDelays,
                }),
                {
                    allowMinimumUploadDuration: query('GET_ALLOW_MINIMUM_UPLOAD_DURATION'),
                }
            ),
            // called when the file is about to be processed so it can be piped through the transform filters
            (file, success, error) => {
                // allow plugins to alter the file data
                applyFilterChain('PREPARE_OUTPUT', file, { query, item })
                    .then(file => {
                        dispatch('DID_PREPARE_OUTPUT', { id: item.id, file });

                        success(file);
                    })
                    .catch(error);
            }
        );
    }),

    RETRY_ITEM_PROCESSING: getItemByQueryFromState(state, item => {
        dispatch('REQUEST_ITEM_PROCESSING', { query: item });
    }),

    REQUEST_REMOVE_ITEM: getItemByQueryFromState(state, item => {
        optionalPromise(query('GET_BEFORE_REMOVE_FILE'), createItemAPI(item)).then(shouldRemove => {
            if (!shouldRemove) {
                return;
            }
            dispatch('REMOVE_ITEM', { query: item });
        });
    }),

    RELEASE_ITEM: getItemByQueryFromState(state, item => {
        item.release();
    }),

    REMOVE_ITEM: getItemByQueryFromState(state, (item, success, failure, options) => {
        const removeFromView = () => {
            // get id reference
            const id = item.id;

            // archive the item, this does not remove it from the list
            getItemById(state.items, id).archive();

            // tell the view the item has been removed
            dispatch('DID_REMOVE_ITEM', { error: null, id, item });

            // now the list has been modified
            listUpdated(dispatch, state);

            // correctly removed
            success(createItemAPI(item));
        };

        // if this is a local file and the `server.remove` function has been configured,
        // send source there so dev can remove file from server
        const server = state.options.server;
        if (
            item.origin === FileOrigin.LOCAL &&
            server &&
            isFunction(server.remove) &&
            options.remove !== false
        ) {
            dispatch('DID_START_ITEM_REMOVE', { id: item.id });

            server.remove(
                item.source,
                () => removeFromView(),
                status => {
                    dispatch('DID_THROW_ITEM_REMOVE_ERROR', {
                        id: item.id,
                        error: createResponse('error', 0, status, null),
                        status: {
                            main: dynamicLabel(state.options.labelFileRemoveError)(status),
                            sub: state.options.labelTapToRetry,
                        },
                    });
                }
            );
        } else {
            // if is requesting revert and can revert need to call revert handler (not calling request_ because that would also trigger beforeRemoveHook)
            if (
                (options.revert && item.origin !== FileOrigin.LOCAL && item.serverId !== null) ||
                // if chunked uploads are enabled and we're uploading in chunks for this specific file
                // or if the file isn't big enough for chunked uploads but chunkForce is set then call
                // revert before removing from the view...
                (state.options.chunkUploads && item.file.size > state.options.chunkSize) ||
                (state.options.chunkUploads && state.options.chunkForce)
            ) {
                item.revert(
                    createRevertFunction(state.options.server.url, state.options.server.revert),
                    query('GET_FORCE_REVERT')
                );
            }

            // can now safely remove from view
            removeFromView();
        }
    }),

    ABORT_ITEM_LOAD: getItemByQueryFromState(state, item => {
        item.abortLoad();
    }),

    ABORT_ITEM_PROCESSING: getItemByQueryFromState(state, item => {
        // test if is already processed
        if (item.serverId) {
            dispatch('REVERT_ITEM_PROCESSING', { id: item.id });
            return;
        }

        // abort
        item.abortProcessing().then(() => {
            const shouldRemove = state.options.instantUpload;
            if (shouldRemove) {
                dispatch('REMOVE_ITEM', { query: item.id });
            }
        });
    }),

    REQUEST_REVERT_ITEM_PROCESSING: getItemByQueryFromState(state, item => {
        // not instant uploading, revert immediately
        if (!state.options.instantUpload) {
            dispatch('REVERT_ITEM_PROCESSING', { query: item });
            return;
        }

        // if we're instant uploading the file will also be removed if we revert,
        // so if a before remove file hook is defined we need to run it now
        const handleRevert = shouldRevert => {
            if (!shouldRevert) return;
            dispatch('REVERT_ITEM_PROCESSING', { query: item });
        };

        const fn = query('GET_BEFORE_REMOVE_FILE');
        if (!fn) {
            return handleRevert(true);
        }

        const requestRemoveResult = fn(createItemAPI(item));
        if (requestRemoveResult == null) {
            // undefined or null
            return handleRevert(true);
        }

        if (typeof requestRemoveResult === 'boolean') {
            return handleRevert(requestRemoveResult);
        }

        if (typeof requestRemoveResult.then === 'function') {
            requestRemoveResult.then(handleRevert);
        }
    }),

    REVERT_ITEM_PROCESSING: getItemByQueryFromState(state, item => {
        item.revert(
            createRevertFunction(state.options.server.url, state.options.server.revert),
            query('GET_FORCE_REVERT')
        )
            .then(() => {
                const shouldRemove = state.options.instantUpload || isMockItem(item);
                if (shouldRemove) {
                    dispatch('REMOVE_ITEM', { query: item.id });
                }
            })
            .catch(() => {});
    }),

    SET_OPTIONS: ({ options }) => {
        // get all keys passed
        const optionKeys = Object.keys(options);

        // get prioritized keyed to include (remove once not in options object)
        const prioritizedOptionKeys = PrioritizedOptions.filter(key => optionKeys.includes(key));

        // order the keys, prioritized first, then rest
        const orderedOptionKeys = [
            // add prioritized first if passed to options, else remove
            ...prioritizedOptionKeys,

            // prevent duplicate keys
            ...Object.keys(options).filter(key => !prioritizedOptionKeys.includes(key)),
        ];

        // dispatch set event for each option
        orderedOptionKeys.forEach(key => {
            dispatch(`SET_${fromCamels(key, '_').toUpperCase()}`, {
                value: options[key],
            });
        });
    },
});

const PrioritizedOptions = [
    'server', // must be processed before "files"
];

const formatFilename = name => name;

const createElement$1 = tagName => {
    return document.createElement(tagName);
};

const text = (node, value) => {
    let textNode = node.childNodes[0];
    if (!textNode) {
        textNode = document.createTextNode(value);
        node.appendChild(textNode);
    } else if (value !== textNode.nodeValue) {
        textNode.nodeValue = value;
    }
};

const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => {
    const angleInRadians = (((angleInDegrees % 360) - 90) * Math.PI) / 180.0;
    return {
        x: centerX + radius * Math.cos(angleInRadians),
        y: centerY + radius * Math.sin(angleInRadians),
    };
};

const describeArc = (x, y, radius, startAngle, endAngle, arcSweep) => {
    const start = polarToCartesian(x, y, radius, endAngle);
    const end = polarToCartesian(x, y, radius, startAngle);
    return ['M', start.x, start.y, 'A', radius, radius, 0, arcSweep, 0, end.x, end.y].join(' ');
};

const percentageArc = (x, y, radius, from, to) => {
    let arcSweep = 1;
    if (to > from && to - from <= 0.5) {
        arcSweep = 0;
    }
    if (from > to && from - to >= 0.5) {
        arcSweep = 0;
    }
    return describeArc(
        x,
        y,
        radius,
        Math.min(0.9999, from) * 360,
        Math.min(0.9999, to) * 360,
        arcSweep
    );
};

const create = ({ root, props }) => {
    // start at 0
    props.spin = false;
    props.progress = 0;
    props.opacity = 0;

    // svg
    const svg = createElement('svg');
    root.ref.path = createElement('path', {
        'stroke-width': 2,
        'stroke-linecap': 'round',
    });
    svg.appendChild(root.ref.path);

    root.ref.svg = svg;

    root.appendChild(svg);
};

const write = ({ root, props }) => {
    if (props.opacity === 0) {
        return;
    }

    if (props.align) {
        root.element.dataset.align = props.align;
    }

    // get width of stroke
    const ringStrokeWidth = parseInt(attr(root.ref.path, 'stroke-width'), 10);

    // calculate size of ring
    const size = root.rect.element.width * 0.5;

    // ring state
    let ringFrom = 0;
    let ringTo = 0;

    // now in busy mode
    if (props.spin) {
        ringFrom = 0;
        ringTo = 0.5;
    } else {
        ringFrom = 0;
        ringTo = props.progress;
    }

    // get arc path
    const coordinates = percentageArc(size, size, size - ringStrokeWidth, ringFrom, ringTo);

    // update progress bar
    attr(root.ref.path, 'd', coordinates);

    // hide while contains 0 value
    attr(root.ref.path, 'stroke-opacity', props.spin || props.progress > 0 ? 1 : 0);
};

const progressIndicator = createView({
    tag: 'div',
    name: 'progress-indicator',
    ignoreRectUpdate: true,
    ignoreRect: true,
    create,
    write,
    mixins: {
        apis: ['progress', 'spin', 'align'],
        styles: ['opacity'],
        animations: {
            opacity: { type: 'tween', duration: 500 },
            progress: {
                type: 'spring',
                stiffness: 0.95,
                damping: 0.65,
                mass: 10,
            },
        },
    },
});

const create$1 = ({ root, props }) => {
    root.element.innerHTML = (props.icon || '') + `<span>${props.label}</span>`;

    props.isDisabled = false;
};

const write$1 = ({ root, props }) => {
    const { isDisabled } = props;
    const shouldDisable = root.query('GET_DISABLED') || props.opacity === 0;

    if (shouldDisable && !isDisabled) {
        props.isDisabled = true;
        attr(root.element, 'disabled', 'disabled');
    } else if (!shouldDisable && isDisabled) {
        props.isDisabled = false;
        root.element.removeAttribute('disabled');
    }
};

const fileActionButton = createView({
    tag: 'button',
    attributes: {
        type: 'button',
    },
    ignoreRect: true,
    ignoreRectUpdate: true,
    name: 'file-action-button',
    mixins: {
        apis: ['label'],
        styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity'],
        animations: {
            scaleX: 'spring',
            scaleY: 'spring',
            translateX: 'spring',
            translateY: 'spring',
            opacity: { type: 'tween', duration: 250 },
        },
        listeners: true,
    },
    create: create$1,
    write: write$1,
});

const toNaturalFileSize = (bytes, decimalSeparator = '.', base = 1000, options = {}) => {
    const {
        labelBytes = 'bytes',
        labelKilobytes = 'KB',
        labelMegabytes = 'MB',
        labelGigabytes = 'GB',
    } = options;

    // no negative byte sizes
    bytes = Math.round(Math.abs(bytes));

    const KB = base;
    const MB = base * base;
    const GB = base * base * base;

    // just bytes
    if (bytes < KB) {
        return `${bytes} ${labelBytes}`;
    }

    // kilobytes
    if (bytes < MB) {
        return `${Math.floor(bytes / KB)} ${labelKilobytes}`;
    }

    // megabytes
    if (bytes < GB) {
        return `${removeDecimalsWhenZero(bytes / MB, 1, decimalSeparator)} ${labelMegabytes}`;
    }

    // gigabytes
    return `${removeDecimalsWhenZero(bytes / GB, 2, decimalSeparator)} ${labelGigabytes}`;
};

const removeDecimalsWhenZero = (value, decimalCount, separator) => {
    return value
        .toFixed(decimalCount)
        .split('.')
        .filter(part => part !== '0')
        .join(separator);
};

const create$2 = ({ root, props }) => {
    // filename
    const fileName = createElement$1('span');
    fileName.className = 'filepond--file-info-main';
    // hide for screenreaders
    // the file is contained in a fieldset with legend that contains the filename
    // no need to read it twice
    attr(fileName, 'aria-hidden', 'true');
    root.appendChild(fileName);
    root.ref.fileName = fileName;

    // filesize
    const fileSize = createElement$1('span');
    fileSize.className = 'filepond--file-info-sub';
    root.appendChild(fileSize);
    root.ref.fileSize = fileSize;

    // set initial values
    text(fileSize, root.query('GET_LABEL_FILE_WAITING_FOR_SIZE'));
    text(fileName, formatFilename(root.query('GET_ITEM_NAME', props.id)));
};

const updateFile = ({ root, props }) => {
    text(
        root.ref.fileSize,
        toNaturalFileSize(
            root.query('GET_ITEM_SIZE', props.id),
            '.',
            root.query('GET_FILE_SIZE_BASE'),
            root.query('GET_FILE_SIZE_LABELS', root.query)
        )
    );
    text(root.ref.fileName, formatFilename(root.query('GET_ITEM_NAME', props.id)));
};

const updateFileSizeOnError = ({ root, props }) => {
    // if size is available don't fallback to unknown size message
    if (isInt(root.query('GET_ITEM_SIZE', props.id))) {
        updateFile({ root, props });
        return;
    }

    text(root.ref.fileSize, root.query('GET_LABEL_FILE_SIZE_NOT_AVAILABLE'));
};

const fileInfo = createView({
    name: 'file-info',
    ignoreRect: true,
    ignoreRectUpdate: true,
    write: createRoute({
        DID_LOAD_ITEM: updateFile,
        DID_UPDATE_ITEM_META: updateFile,
        DID_THROW_ITEM_LOAD_ERROR: updateFileSizeOnError,
        DID_THROW_ITEM_INVALID: updateFileSizeOnError,
    }),
    didCreateView: root => {
        applyFilters('CREATE_VIEW', { ...root, view: root });
    },
    create: create$2,
    mixins: {
        styles: ['translateX', 'translateY'],
        animations: {
            translateX: 'spring',
            translateY: 'spring',
        },
    },
});

const toPercentage = value => Math.round(value * 100);

const create$3 = ({ root }) => {
    // main status
    const main = createElement$1('span');
    main.className = 'filepond--file-status-main';
    root.appendChild(main);
    root.ref.main = main;

    // sub status
    const sub = createElement$1('span');
    sub.className = 'filepond--file-status-sub';
    root.appendChild(sub);
    root.ref.sub = sub;

    didSetItemLoadProgress({ root, action: { progress: null } });
};

const didSetItemLoadProgress = ({ root, action }) => {
    const title =
        action.progress === null
            ? root.query('GET_LABEL_FILE_LOADING')
            : `${root.query('GET_LABEL_FILE_LOADING')} ${toPercentage(action.progress)}%`;
    text(root.ref.main, title);
    text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL'));
};

const didSetItemProcessProgress = ({ root, action }) => {
    const title =
        action.progress === null
            ? root.query('GET_LABEL_FILE_PROCESSING')
            : `${root.query('GET_LABEL_FILE_PROCESSING')} ${toPercentage(action.progress)}%`;
    text(root.ref.main, title);
    text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL'));
};

const didRequestItemProcessing = ({ root }) => {
    text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING'));
    text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL'));
};

const didAbortItemProcessing = ({ root }) => {
    text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING_ABORTED'));
    text(root.ref.sub, root.query('GET_LABEL_TAP_TO_RETRY'));
};

const didCompleteItemProcessing = ({ root }) => {
    text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING_COMPLETE'));
    text(root.ref.sub, root.query('GET_LABEL_TAP_TO_UNDO'));
};

const clear = ({ root }) => {
    text(root.ref.main, '');
    text(root.ref.sub, '');
};

const error = ({ root, action }) => {
    text(root.ref.main, action.status.main);
    text(root.ref.sub, action.status.sub);
};

const fileStatus = createView({
    name: 'file-status',
    ignoreRect: true,
    ignoreRectUpdate: true,
    write: createRoute({
        DID_LOAD_ITEM: clear,
        DID_REVERT_ITEM_PROCESSING: clear,
        DID_REQUEST_ITEM_PROCESSING: didRequestItemProcessing,
        DID_ABORT_ITEM_PROCESSING: didAbortItemProcessing,
        DID_COMPLETE_ITEM_PROCESSING: didCompleteItemProcessing,
        DID_UPDATE_ITEM_PROCESS_PROGRESS: didSetItemProcessProgress,
        DID_UPDATE_ITEM_LOAD_PROGRESS: didSetItemLoadProgress,
        DID_THROW_ITEM_LOAD_ERROR: error,
        DID_THROW_ITEM_INVALID: error,
        DID_THROW_ITEM_PROCESSING_ERROR: error,
        DID_THROW_ITEM_PROCESSING_REVERT_ERROR: error,
        DID_THROW_ITEM_REMOVE_ERROR: error,
    }),
    didCreateView: root => {
        applyFilters('CREATE_VIEW', { ...root, view: root });
    },
    create: create$3,
    mixins: {
        styles: ['translateX', 'translateY', 'opacity'],
        animations: {
            opacity: { type: 'tween', duration: 250 },
            translateX: 'spring',
            translateY: 'spring',
        },
    },
});

/**
 * Button definitions for the file view
 */

const Buttons = {
    AbortItemLoad: {
        label: 'GET_LABEL_BUTTON_ABORT_ITEM_LOAD',
        action: 'ABORT_ITEM_LOAD',
        className: 'filepond--action-abort-item-load',
        align: 'LOAD_INDICATOR_POSITION', // right
    },
    RetryItemLoad: {
        label: 'GET_LABEL_BUTTON_RETRY_ITEM_LOAD',
        action: 'RETRY_ITEM_LOAD',
        icon: 'GET_ICON_RETRY',
        className: 'filepond--action-retry-item-load',
        align: 'BUTTON_PROCESS_ITEM_POSITION', // right
    },
    RemoveItem: {
        label: 'GET_LABEL_BUTTON_REMOVE_ITEM',
        action: 'REQUEST_REMOVE_ITEM',
        icon: 'GET_ICON_REMOVE',
        className: 'filepond--action-remove-item',
        align: 'BUTTON_REMOVE_ITEM_POSITION', // left
    },
    ProcessItem: {
        label: 'GET_LABEL_BUTTON_PROCESS_ITEM',
        action: 'REQUEST_ITEM_PROCESSING',
        icon: 'GET_ICON_PROCESS',
        className: 'filepond--action-process-item',
        align: 'BUTTON_PROCESS_ITEM_POSITION', // right
    },
    AbortItemProcessing: {
        label: 'GET_LABEL_BUTTON_ABORT_ITEM_PROCESSING',
        action: 'ABORT_ITEM_PROCESSING',
        className: 'filepond--action-abort-item-processing',
        align: 'BUTTON_PROCESS_ITEM_POSITION', // right
    },
    RetryItemProcessing: {
        label: 'GET_LABEL_BUTTON_RETRY_ITEM_PROCESSING',
        action: 'RETRY_ITEM_PROCESSING',
        icon: 'GET_ICON_RETRY',
        className: 'filepond--action-retry-item-processing',
        align: 'BUTTON_PROCESS_ITEM_POSITION', // right
    },
    RevertItemProcessing: {
        label: 'GET_LABEL_BUTTON_UNDO_ITEM_PROCESSING',
        action: 'REQUEST_REVERT_ITEM_PROCESSING',
        icon: 'GET_ICON_UNDO',
        className: 'filepond--action-revert-item-processing',
        align: 'BUTTON_PROCESS_ITEM_POSITION', // right
    },
};

// make a list of buttons, we can then remove buttons from this list if they're disabled
const ButtonKeys = [];
forin(Buttons, key => {
    ButtonKeys.push(key);
});

const calculateFileInfoOffset = root => {
    if (getRemoveIndicatorAligment(root) === 'right') return 0;
    const buttonRect = root.ref.buttonRemoveItem.rect.element;
    return buttonRect.hidden ? null : buttonRect.width + buttonRect.left;
};

const calculateButtonWidth = root => {
    const buttonRect = root.ref.buttonAbortItemLoad.rect.element;
    return buttonRect.width;
};

// Force on full pixels so text stays crips
const calculateFileVerticalCenterOffset = root =>
    Math.floor(root.ref.buttonRemoveItem.rect.element.height / 4);
const calculateFileHorizontalCenterOffset = root =>
    Math.floor(root.ref.buttonRemoveItem.rect.element.left / 2);

const getLoadIndicatorAlignment = root => root.query('GET_STYLE_LOAD_INDICATOR_POSITION');
const getProcessIndicatorAlignment = root => root.query('GET_STYLE_PROGRESS_INDICATOR_POSITION');
const getRemoveIndicatorAligment = root => root.query('GET_STYLE_BUTTON_REMOVE_ITEM_POSITION');

const DefaultStyle = {
    buttonAbortItemLoad: { opacity: 0 },
    buttonRetryItemLoad: { opacity: 0 },
    buttonRemoveItem: { opacity: 0 },
    buttonProcessItem: { opacity: 0 },
    buttonAbortItemProcessing: { opacity: 0 },
    buttonRetryItemProcessing: { opacity: 0 },
    buttonRevertItemProcessing: { opacity: 0 },
    loadProgressIndicator: { opacity: 0, align: getLoadIndicatorAlignment },
    processProgressIndicator: { opacity: 0, align: getProcessIndicatorAlignment },
    processingCompleteIndicator: { opacity: 0, scaleX: 0.75, scaleY: 0.75 },
    info: { translateX: 0, translateY: 0, opacity: 0 },
    status: { translateX: 0, translateY: 0, opacity: 0 },
};

const IdleStyle = {
    buttonRemoveItem: { opacity: 1 },
    buttonProcessItem: { opacity: 1 },
    info: { translateX: calculateFileInfoOffset },
    status: { translateX: calculateFileInfoOffset },
};

const ProcessingStyle = {
    buttonAbortItemProcessing: { opacity: 1 },
    processProgressIndicator: { opacity: 1 },
    status: { opacity: 1 },
};

const StyleMap = {
    DID_THROW_ITEM_INVALID: {
        buttonRemoveItem: { opacity: 1 },
        info: { translateX: calculateFileInfoOffset },
        status: { translateX: calculateFileInfoOffset, opacity: 1 },
    },
    DID_START_ITEM_LOAD: {
        buttonAbortItemLoad: { opacity: 1 },
        loadProgressIndicator: { opacity: 1 },
        status: { opacity: 1 },
    },
    DID_THROW_ITEM_LOAD_ERROR: {
        buttonRetryItemLoad: { opacity: 1 },
        buttonRemoveItem: { opacity: 1 },
        info: { translateX: calculateFileInfoOffset },
        status: { opacity: 1 },
    },
    DID_START_ITEM_REMOVE: {
        processProgressIndicator: { opacity: 1, align: getRemoveIndicatorAligment },
        info: { translateX: calculateFileInfoOffset },
        status: { opacity: 0 },
    },
    DID_THROW_ITEM_REMOVE_ERROR: {
        processProgressIndicator: { opacity: 0, align: getRemoveIndicatorAligment },
        buttonRemoveItem: { opacity: 1 },
        info: { translateX: calculateFileInfoOffset },
        status: { opacity: 1, translateX: calculateFileInfoOffset },
    },
    DID_LOAD_ITEM: IdleStyle,
    DID_LOAD_LOCAL_ITEM: {
        buttonRemoveItem: { opacity: 1 },
        info: { translateX: calculateFileInfoOffset },
        status: { translateX: calculateFileInfoOffset },
    },
    DID_START_ITEM_PROCESSING: ProcessingStyle,
    DID_REQUEST_ITEM_PROCESSING: ProcessingStyle,
    DID_UPDATE_ITEM_PROCESS_PROGRESS: ProcessingStyle,
    DID_COMPLETE_ITEM_PROCESSING: {
        buttonRevertItemProcessing: { opacity: 1 },
        info: { opacity: 1 },
        status: { opacity: 1 },
    },
    DID_THROW_ITEM_PROCESSING_ERROR: {
        buttonRemoveItem: { opacity: 1 },
        buttonRetryItemProcessing: { opacity: 1 },
        status: { opacity: 1 },
        info: { translateX: calculateFileInfoOffset },
    },
    DID_THROW_ITEM_PROCESSING_REVERT_ERROR: {
        buttonRevertItemProcessing: { opacity: 1 },
        status: { opacity: 1 },
        info: { opacity: 1 },
    },
    DID_ABORT_ITEM_PROCESSING: {
        buttonRemoveItem: { opacity: 1 },
        buttonProcessItem: { opacity: 1 },
        info: { translateX: calculateFileInfoOffset },
        status: { opacity: 1 },
    },
    DID_REVERT_ITEM_PROCESSING: IdleStyle,
};

// complete indicator view
const processingCompleteIndicatorView = createView({
    create: ({ root }) => {
        root.element.innerHTML = root.query('GET_ICON_DONE');
    },
    name: 'processing-complete-indicator',
    ignoreRect: true,
    mixins: {
        styles: ['scaleX', 'scaleY', 'opacity'],
        animations: {
            scaleX: 'spring',
            scaleY: 'spring',
            opacity: { type: 'tween', duration: 250 },
        },
    },
});

/**
 * Creates the file view
 */
const create$4 = ({ root, props }) => {
    // copy Buttons object
    const LocalButtons = Object.keys(Buttons).reduce((prev, curr) => {
        prev[curr] = { ...Buttons[curr] };
        return prev;
    }, {});

    const { id } = props;

    // allow reverting upload
    const allowRevert = root.query('GET_ALLOW_REVERT');

    // allow remove file
    const allowRemove = root.query('GET_ALLOW_REMOVE');

    // allow processing upload
    const allowProcess = root.query('GET_ALLOW_PROCESS');

    // is instant uploading, need this to determine the icon of the undo button
    const instantUpload = root.query('GET_INSTANT_UPLOAD');

    // is async set up
    const isAsync = root.query('IS_ASYNC');

    // should align remove item buttons
    const alignRemoveItemButton = root.query('GET_STYLE_BUTTON_REMOVE_ITEM_ALIGN');

    // enabled buttons array
    let buttonFilter;
    if (isAsync) {
        if (allowProcess && !allowRevert) {
            // only remove revert button
            buttonFilter = key => !/RevertItemProcessing/.test(key);
        } else if (!allowProcess && allowRevert) {
            // only remove process button
            buttonFilter = key => !/ProcessItem|RetryItemProcessing|AbortItemProcessing/.test(key);
        } else if (!allowProcess && !allowRevert) {
            // remove all process buttons
            buttonFilter = key => !/Process/.test(key);
        }
    } else {
        // no process controls available
        buttonFilter = key => !/Process/.test(key);
    }

    const enabledButtons = buttonFilter ? ButtonKeys.filter(buttonFilter) : ButtonKeys.concat();

    // update icon and label for revert button when instant uploading
    if (instantUpload && allowRevert) {
        LocalButtons['RevertItemProcessing'].label = 'GET_LABEL_BUTTON_REMOVE_ITEM';
        LocalButtons['RevertItemProcessing'].icon = 'GET_ICON_REMOVE';
    }

    // remove last button (revert) if not allowed
    if (isAsync && !allowRevert) {
        const map = StyleMap['DID_COMPLETE_ITEM_PROCESSING'];
        map.info.translateX = calculateFileHorizontalCenterOffset;
        map.info.translateY = calculateFileVerticalCenterOffset;
        map.status.translateY = calculateFileVerticalCenterOffset;
        map.processingCompleteIndicator = { opacity: 1, scaleX: 1, scaleY: 1 };
    }

    // should align center
    if (isAsync && !allowProcess) {
        [
            'DID_START_ITEM_PROCESSING',
            'DID_REQUEST_ITEM_PROCESSING',
            'DID_UPDATE_ITEM_PROCESS_PROGRESS',
            'DID_THROW_ITEM_PROCESSING_ERROR',
        ].forEach(key => {
            StyleMap[key].status.translateY = calculateFileVerticalCenterOffset;
        });
        StyleMap['DID_THROW_ITEM_PROCESSING_ERROR'].status.translateX = calculateButtonWidth;
    }

    // move remove button to right
    if (alignRemoveItemButton && allowRevert) {
        LocalButtons['RevertItemProcessing'].align = 'BUTTON_REMOVE_ITEM_POSITION';
        const map = StyleMap['DID_COMPLETE_ITEM_PROCESSING'];
        map.info.translateX = calculateFileInfoOffset;
        map.status.translateY = calculateFileVerticalCenterOffset;
        map.processingCompleteIndicator = { opacity: 1, scaleX: 1, scaleY: 1 };
    }

    // show/hide RemoveItem button
    if (!allowRemove) {
        LocalButtons['RemoveItem'].disabled = true;
    }

    // create the button views
    forin(LocalButtons, (key, definition) => {
        // create button
        const buttonView = root.createChildView(fileActionButton, {
            label: root.query(definition.label),
            icon: root.query(definition.icon),
            opacity: 0,
        });

        // should be appended?
        if (enabledButtons.includes(key)) {
            root.appendChildView(buttonView);
        }

        // toggle
        if (definition.disabled) {
            buttonView.element.setAttribute('disabled', 'disabled');
            buttonView.element.setAttribute('hidden', 'hidden');
        }

        // add position attribute
        buttonView.element.dataset.align = root.query(`GET_STYLE_${definition.align}`);

        // add class
        buttonView.element.classList.add(definition.className);

        // handle interactions
        buttonView.on('click', e => {
            e.stopPropagation();
            if (definition.disabled) return;
            root.dispatch(definition.action, { query: id });
        });

        // set reference
        root.ref[`button${key}`] = buttonView;
    });

    // checkmark
    root.ref.processingCompleteIndicator = root.appendChildView(
        root.createChildView(processingCompleteIndicatorView)
    );
    root.ref.processingCompleteIndicator.element.dataset.align = root.query(
        `GET_STYLE_BUTTON_PROCESS_ITEM_POSITION`
    );

    // create file info view
    root.ref.info = root.appendChildView(root.createChildView(fileInfo, { id }));

    // create file status view
    root.ref.status = root.appendChildView(root.createChildView(fileStatus, { id }));

    // add progress indicators
    const loadIndicatorView = root.appendChildView(
        root.createChildView(progressIndicator, {
            opacity: 0,
            align: root.query(`GET_STYLE_LOAD_INDICATOR_POSITION`),
        })
    );
    loadIndicatorView.element.classList.add('filepond--load-indicator');
    root.ref.loadProgressIndicator = loadIndicatorView;

    const progressIndicatorView = root.appendChildView(
        root.createChildView(progressIndicator, {
            opacity: 0,
            align: root.query(`GET_STYLE_PROGRESS_INDICATOR_POSITION`),
        })
    );
    progressIndicatorView.element.classList.add('filepond--process-indicator');
    root.ref.processProgressIndicator = progressIndicatorView;

    // current active styles
    root.ref.activeStyles = [];
};

const write$2 = ({ root, actions, props }) => {
    // route actions
    route({ root, actions, props });

    // select last state change action
    let action = actions
        .concat()
        .filter(action => /^DID_/.test(action.type))
        .reverse()
        .find(action => StyleMap[action.type]);

    // a new action happened, let's get the matching styles
    if (action) {
        // define new active styles
        root.ref.activeStyles = [];

        const stylesToApply = StyleMap[action.type];
        forin(DefaultStyle, (name, defaultStyles) => {
            // get reference to control
            const control = root.ref[name];

            // loop over all styles for this control
            forin(defaultStyles, (key, defaultValue) => {
                const value =
                    stylesToApply[name] && typeof stylesToApply[name][key] !== 'undefined'
                        ? stylesToApply[name][key]
                        : defaultValue;
                root.ref.activeStyles.push({ control, key, value });
            });
        });
    }

    // apply active styles to element
    root.ref.activeStyles.forEach(({ control, key, value }) => {
        control[key] = typeof value === 'function' ? value(root) : value;
    });
};

const route = createRoute({
    DID_SET_LABEL_BUTTON_ABORT_ITEM_PROCESSING: ({ root, action }) => {
        root.ref.buttonAbortItemProcessing.label = action.value;
    },
    DID_SET_LABEL_BUTTON_ABORT_ITEM_LOAD: ({ root, action }) => {
        root.ref.buttonAbortItemLoad.label = action.value;
    },
    DID_SET_LABEL_BUTTON_ABORT_ITEM_REMOVAL: ({ root, action }) => {
        root.ref.buttonAbortItemRemoval.label = action.value;
    },
    DID_REQUEST_ITEM_PROCESSING: ({ root }) => {
        root.ref.processProgressIndicator.spin = true;
        root.ref.processProgressIndicator.progress = 0;
    },
    DID_START_ITEM_LOAD: ({ root }) => {
        root.ref.loadProgressIndicator.spin = true;
        root.ref.loadProgressIndicator.progress = 0;
    },
    DID_START_ITEM_REMOVE: ({ root }) => {
        root.ref.processProgressIndicator.spin = true;
        root.ref.processProgressIndicator.progress = 0;
    },
    DID_UPDATE_ITEM_LOAD_PROGRESS: ({ root, action }) => {
        root.ref.loadProgressIndicator.spin = false;
        root.ref.loadProgressIndicator.progress = action.progress;
    },
    DID_UPDATE_ITEM_PROCESS_PROGRESS: ({ root, action }) => {
        root.ref.processProgressIndicator.spin = false;
        root.ref.processProgressIndicator.progress = action.progress;
    },
});

const file = createView({
    create: create$4,
    write: write$2,
    didCreateView: root => {
        applyFilters('CREATE_VIEW', { ...root, view: root });
    },
    name: 'file',
});

/**
 * Creates the file view
 */
const create$5 = ({ root, props }) => {
    // filename
    root.ref.fileName = createElement$1('legend');
    root.appendChild(root.ref.fileName);

    // file appended
    root.ref.file = root.appendChildView(root.createChildView(file, { id: props.id }));

    // data has moved to data.js
    root.ref.data = false;
};

/**
 * Data storage
 */
const didLoadItem = ({ root, props }) => {
    // updates the legend of the fieldset so screenreaders can better group buttons
    text(root.ref.fileName, formatFilename(root.query('GET_ITEM_NAME', props.id)));
};

const fileWrapper = createView({
    create: create$5,
    ignoreRect: true,
    write: createRoute({
        DID_LOAD_ITEM: didLoadItem,
    }),
    didCreateView: root => {
        applyFilters('CREATE_VIEW', { ...root, view: root });
    },
    tag: 'fieldset',
    name: 'file-wrapper',
});

const PANEL_SPRING_PROPS = { type: 'spring', damping: 0.6, mass: 7 };

const create$6 = ({ root, props }) => {
    [
        {
            name: 'top',
        },
        {
            name: 'center',
            props: {
                translateY: null,
                scaleY: null,
            },
            mixins: {
                animations: {
                    scaleY: PANEL_SPRING_PROPS,
                },
                styles: ['translateY', 'scaleY'],
            },
        },
        {
            name: 'bottom',
            props: {
                translateY: null,
            },
            mixins: {
                animations: {
                    translateY: PANEL_SPRING_PROPS,
                },
                styles: ['translateY'],
            },
        },
    ].forEach(section => {
        createSection(root, section, props.name);
    });

    root.element.classList.add(`filepond--${props.name}`);

    root.ref.scalable = null;
};

const createSection = (root, section, className) => {
    const viewConstructor = createView({
        name: `panel-${section.name} filepond--${className}`,
        mixins: section.mixins,
        ignoreRectUpdate: true,
    });

    const view = root.createChildView(viewConstructor, section.props);

    root.ref[section.name] = root.appendChildView(view);
};

const write$3 = ({ root, props }) => {
    // update scalable state
    if (root.ref.scalable === null || props.scalable !== root.ref.scalable) {
        root.ref.scalable = isBoolean(props.scalable) ? props.scalable : true;
        root.element.dataset.scalable = root.ref.scalable;
    }

    // no height, can't set
    if (!props.height) return;

    // get child rects
    const topRect = root.ref.top.rect.element;
    const bottomRect = root.ref.bottom.rect.element;

    // make sure height never is smaller than bottom and top seciton heights combined (will probably never happen, but who knows)
    const height = Math.max(topRect.height + bottomRect.height, props.height);

    // offset center part
    root.ref.center.translateY = topRect.height;

    // scale center part
    // use math ceil to prevent transparent lines because of rounding errors
    root.ref.center.scaleY = (height - topRect.height - bottomRect.height) / 100;

    // offset bottom part
    root.ref.bottom.translateY = height - bottomRect.height;
};

const panel = createView({
    name: 'panel',
    read: ({ root, props }) => (props.heightCurrent = root.ref.bottom.translateY),
    write: write$3,
    create: create$6,
    ignoreRect: true,
    mixins: {
        apis: ['height', 'heightCurrent', 'scalable'],
    },
});

const createDragHelper = items => {
    const itemIds = items.map(item => item.id);
    let prevIndex = undefined;
    return {
        setIndex: index => {
            prevIndex = index;
        },
        getIndex: () => prevIndex,
        getItemIndex: item => itemIds.indexOf(item.id),
    };
};

const ITEM_TRANSLATE_SPRING = {
    type: 'spring',
    stiffness: 0.75,
    damping: 0.45,
    mass: 10,
};

const ITEM_SCALE_SPRING = 'spring';

const StateMap = {
    DID_START_ITEM_LOAD: 'busy',
    DID_UPDATE_ITEM_LOAD_PROGRESS: 'loading',
    DID_THROW_ITEM_INVALID: 'load-invalid',
    DID_THROW_ITEM_LOAD_ERROR: 'load-error',
    DID_LOAD_ITEM: 'idle',
    DID_THROW_ITEM_REMOVE_ERROR: 'remove-error',
    DID_START_ITEM_REMOVE: 'busy',
    DID_START_ITEM_PROCESSING: 'busy processing',
    DID_REQUEST_ITEM_PROCESSING: 'busy processing',
    DID_UPDATE_ITEM_PROCESS_PROGRESS: 'processing',
    DID_COMPLETE_ITEM_PROCESSING: 'processing-complete',
    DID_THROW_ITEM_PROCESSING_ERROR: 'processing-error',
    DID_THROW_ITEM_PROCESSING_REVERT_ERROR: 'processing-revert-error',
    DID_ABORT_ITEM_PROCESSING: 'cancelled',
    DID_REVERT_ITEM_PROCESSING: 'idle',
};

/**
 * Creates the file view
 */
const create$7 = ({ root, props }) => {
    // select
    root.ref.handleClick = e => root.dispatch('DID_ACTIVATE_ITEM', { id: props.id });

    // set id
    root.element.id = `filepond--item-${props.id}`;
    root.element.addEventListener('click', root.ref.handleClick);

    // file view
    root.ref.container = root.appendChildView(root.createChildView(fileWrapper, { id: props.id }));

    // file panel
    root.ref.panel = root.appendChildView(root.createChildView(panel, { name: 'item-panel' }));

    // default start height
    root.ref.panel.height = null;

    // by default not marked for removal
    props.markedForRemoval = false;

    // if not allowed to reorder file items, exit here
    if (!root.query('GET_ALLOW_REORDER')) return;

    // set to idle so shows grab cursor
    root.element.dataset.dragState = 'idle';

    const grab = e => {
        if (!e.isPrimary) return;

        let removedActivateListener = false;

        const origin = {
            x: e.pageX,
            y: e.pageY,
        };

        props.dragOrigin = {
            x: root.translateX,
            y: root.translateY,
        };

        props.dragCenter = {
            x: e.offsetX,
            y: e.offsetY,
        };

        const dragState = createDragHelper(root.query('GET_ACTIVE_ITEMS'));

        root.dispatch('DID_GRAB_ITEM', { id: props.id, dragState });

        const drag = e => {
            if (!e.isPrimary) return;

            e.stopPropagation();
            e.preventDefault();

            props.dragOffset = {
                x: e.pageX - origin.x,
                y: e.pageY - origin.y,
            };

            // if dragged stop listening to clicks, will re-add when done dragging
            const dist =
                props.dragOffset.x * props.dragOffset.x + props.dragOffset.y * props.dragOffset.y;
            if (dist > 16 && !removedActivateListener) {
                removedActivateListener = true;
                root.element.removeEventListener('click', root.ref.handleClick);
            }

            root.dispatch('DID_DRAG_ITEM', { id: props.id, dragState });
        };

        const drop = e => {
            if (!e.isPrimary) return;

            document.removeEventListener('pointermove', drag);
            document.removeEventListener('pointerup', drop);

            props.dragOffset = {
                x: e.pageX - origin.x,
                y: e.pageY - origin.y,
            };

            root.dispatch('DID_DROP_ITEM', { id: props.id, dragState });

            // start listening to clicks again
            if (removedActivateListener) {
                setTimeout(() => root.element.addEventListener('click', root.ref.handleClick), 0);
            }
        };

        document.addEventListener('pointermove', drag);
        document.addEventListener('pointerup', drop);
    };

    root.element.addEventListener('pointerdown', grab);
};

const route$1 = createRoute({
    DID_UPDATE_PANEL_HEIGHT: ({ root, action }) => {
        root.height = action.height;
    },
});

const write$4 = createRoute(
    {
        DID_GRAB_ITEM: ({ root, props }) => {
            props.dragOrigin = {
                x: root.translateX,
                y: root.translateY,
            };
        },
        DID_DRAG_ITEM: ({ root }) => {
            root.element.dataset.dragState = 'drag';
        },
        DID_DROP_ITEM: ({ root, props }) => {
            props.dragOffset = null;
            props.dragOrigin = null;
            root.element.dataset.dragState = 'drop';
        },
    },
    ({ root, actions, props, shouldOptimize }) => {
        if (root.element.dataset.dragState === 'drop') {
            if (root.scaleX <= 1) {
                root.element.dataset.dragState = 'idle';
            }
        }

        // select last state change action
        let action = actions
            .concat()
            .filter(action => /^DID_/.test(action.type))
            .reverse()
            .find(action => StateMap[action.type]);

        // no need to set same state twice
        if (action && action.type !== props.currentState) {
            // set current state
            props.currentState = action.type;

            // set state
            root.element.dataset.filepondItemState = StateMap[props.currentState] || '';
        }

        // route actions
        const aspectRatio =
            root.query('GET_ITEM_PANEL_ASPECT_RATIO') || root.query('GET_PANEL_ASPECT_RATIO');
        if (!aspectRatio) {
            route$1({ root, actions, props });
            if (!root.height && root.ref.container.rect.element.height > 0) {
                root.height = root.ref.container.rect.element.height;
            }
        } else if (!shouldOptimize) {
            root.height = root.rect.element.width * aspectRatio;
        }

        // sync panel height with item height
        if (shouldOptimize) {
            root.ref.panel.height = null;
        }

        root.ref.panel.height = root.height;
    }
);

const item = createView({
    create: create$7,
    write: write$4,
    destroy: ({ root, props }) => {
        root.element.removeEventListener('click', root.ref.handleClick);
        root.dispatch('RELEASE_ITEM', { query: props.id });
    },
    tag: 'li',
    name: 'item',
    mixins: {
        apis: [
            'id',
            'interactionMethod',
            'markedForRemoval',
            'spawnDate',
            'dragCenter',
            'dragOrigin',
            'dragOffset',
        ],
        styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity', 'height'],
        animations: {
            scaleX: ITEM_SCALE_SPRING,
            scaleY: ITEM_SCALE_SPRING,
            translateX: ITEM_TRANSLATE_SPRING,
            translateY: ITEM_TRANSLATE_SPRING,
            opacity: { type: 'tween', duration: 150 },
        },
    },
});

var getItemsPerRow = (horizontalSpace, itemWidth) => {
    // add one pixel leeway, when using percentages for item width total items can be 1.99 per row

    return Math.max(1, Math.floor((horizontalSpace + 1) / itemWidth));
};

const getItemIndexByPosition = (view, children, positionInView) => {
    if (!positionInView) return;

    const horizontalSpace = view.rect.element.width;
    // const children = view.childViews;
    const l = children.length;
    let last = null;

    // -1, don't move items to accomodate (either add to top or bottom)
    if (l === 0 || positionInView.top < children[0].rect.element.top) return -1;

    // let's get the item width
    const item = children[0];
    const itemRect = item.rect.element;
    const itemHorizontalMargin = itemRect.marginLeft + itemRect.marginRight;
    const itemWidth = itemRect.width + itemHorizontalMargin;
    const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth);

    // stack
    if (itemsPerRow === 1) {
        for (let index = 0; index < l; index++) {
            const child = children[index];
            const childMid = child.rect.outer.top + child.rect.element.height * 0.5;
            if (positionInView.top < childMid) {
                return index;
            }
        }
        return l;
    }

    // grid
    const itemVerticalMargin = itemRect.marginTop + itemRect.marginBottom;
    const itemHeight = itemRect.height + itemVerticalMargin;
    for (let index = 0; index < l; index++) {
        const indexX = index % itemsPerRow;
        const indexY = Math.floor(index / itemsPerRow);

        const offsetX = indexX * itemWidth;
        const offsetY = indexY * itemHeight;

        const itemTop = offsetY - itemRect.marginTop;
        const itemRight = offsetX + itemWidth;
        const itemBottom = offsetY + itemHeight + itemRect.marginBottom;

        if (positionInView.top < itemBottom && positionInView.top > itemTop) {
            if (positionInView.left < itemRight) {
                return index;
            } else if (index !== l - 1) {
                last = index;
            } else {
                last = null;
            }
        }
    }

    if (last !== null) {
        return last;
    }

    return l;
};

const dropAreaDimensions = {
    height: 0,
    width: 0,
    get getHeight() {
        return this.height;
    },
    set setHeight(val) {
        if (this.height === 0 || val === 0) this.height = val;
    },
    get getWidth() {
        return this.width;
    },
    set setWidth(val) {
        if (this.width === 0 || val === 0) this.width = val;
    },
    setDimensions: function(height, width) {
        if (this.height === 0 || height === 0) this.height = height;
        if (this.width === 0 || width === 0) this.width = width;
    },
};

const create$8 = ({ root }) => {
    // need to set role to list as otherwise it won't be read as a list by VoiceOver
    attr(root.element, 'role', 'list');

    root.ref.lastItemSpanwDate = Date.now();
};

/**
 * Inserts a new item
 * @param root
 * @param action
 */
const addItemView = ({ root, action }) => {
    const { id, index, interactionMethod } = action;

    root.ref.addIndex = index;

    const now = Date.now();
    let spawnDate = now;
    let opacity = 1;

    if (interactionMethod !== InteractionMethod.NONE) {
        opacity = 0;
        const cooldown = root.query('GET_ITEM_INSERT_INTERVAL');
        const dist = now - root.ref.lastItemSpanwDate;
        spawnDate = dist < cooldown ? now + (cooldown - dist) : now;
    }

    root.ref.lastItemSpanwDate = spawnDate;

    root.appendChildView(
        root.createChildView(
            // view type
            item,

            // props
            {
                spawnDate,
                id,
                opacity,
                interactionMethod,
            }
        ),
        index
    );
};

const moveItem = (item, x, y, vx = 0, vy = 1) => {
    // set to null to remove animation while dragging
    if (item.dragOffset) {
        item.translateX = null;
        item.translateY = null;
        item.translateX = item.dragOrigin.x + item.dragOffset.x;
        item.translateY = item.dragOrigin.y + item.dragOffset.y;
        item.scaleX = 1.025;
        item.scaleY = 1.025;
    } else {
        item.translateX = x;
        item.translateY = y;

        if (Date.now() > item.spawnDate) {
            // reveal element
            if (item.opacity === 0) {
                introItemView(item, x, y, vx, vy);
            }

            // make sure is default scale every frame
            item.scaleX = 1;
            item.scaleY = 1;
            item.opacity = 1;
        }
    }
};

const introItemView = (item, x, y, vx, vy) => {
    if (item.interactionMethod === InteractionMethod.NONE) {
        item.translateX = null;
        item.translateX = x;
        item.translateY = null;
        item.translateY = y;
    } else if (item.interactionMethod === InteractionMethod.DROP) {
        item.translateX = null;
        item.translateX = x - vx * 20;

        item.translateY = null;
        item.translateY = y - vy * 10;

        item.scaleX = 0.8;
        item.scaleY = 0.8;
    } else if (item.interactionMethod === InteractionMethod.BROWSE) {
        item.translateY = null;
        item.translateY = y - 30;
    } else if (item.interactionMethod === InteractionMethod.API) {
        item.translateX = null;
        item.translateX = x - 30;
        item.translateY = null;
    }
};

/**
 * Removes an existing item
 * @param root
 * @param action
 */
const removeItemView = ({ root, action }) => {
    const { id } = action;

    // get the view matching the given id
    const view = root.childViews.find(child => child.id === id);

    // if no view found, exit
    if (!view) {
        return;
    }

    // animate view out of view
    view.scaleX = 0.9;
    view.scaleY = 0.9;
    view.opacity = 0;

    // mark for removal
    view.markedForRemoval = true;
};

const getItemHeight = child =>
    child.rect.element.height +
    child.rect.element.marginBottom * 0.5 +
    child.rect.element.marginTop * 0.5;
const getItemWidth = child =>
    child.rect.element.width +
    child.rect.element.marginLeft * 0.5 +
    child.rect.element.marginRight * 0.5;

const dragItem = ({ root, action }) => {
    const { id, dragState } = action;

    // reference to item
    const item = root.query('GET_ITEM', { id });

    // get the view matching the given id
    const view = root.childViews.find(child => child.id === id);

    const numItems = root.childViews.length;
    const oldIndex = dragState.getItemIndex(item);

    // if no view found, exit
    if (!view) return;

    const dragPosition = {
        x: view.dragOrigin.x + view.dragOffset.x + view.dragCenter.x,
        y: view.dragOrigin.y + view.dragOffset.y + view.dragCenter.y,
    };

    // get drag area dimensions
    const dragHeight = getItemHeight(view);
    const dragWidth = getItemWidth(view);

    // get rows and columns (There will always be at least one row and one column if a file is present)
    let cols = Math.floor(root.rect.outer.width / dragWidth);
    if (cols > numItems) cols = numItems;

    // rows are used to find when we have left the preview area bounding box
    const rows = Math.floor(numItems / cols + 1);

    dropAreaDimensions.setHeight = dragHeight * rows;
    dropAreaDimensions.setWidth = dragWidth * cols;

    // get new index of dragged item
    var location = {
        y: Math.floor(dragPosition.y / dragHeight),
        x: Math.floor(dragPosition.x / dragWidth),
        getGridIndex: function getGridIndex() {
            if (
                dragPosition.y > dropAreaDimensions.getHeight ||
                dragPosition.y < 0 ||
                dragPosition.x > dropAreaDimensions.getWidth ||
                dragPosition.x < 0
            )
                return oldIndex;
            return this.y * cols + this.x;
        },
        getColIndex: function getColIndex() {
            const items = root.query('GET_ACTIVE_ITEMS');
            const visibleChildren = root.childViews.filter(child => child.rect.element.height);
            const children = items.map(item =>
                visibleChildren.find(childView => childView.id === item.id)
            );
            const currentIndex = children.findIndex(child => child === view);
            const dragHeight = getItemHeight(view);
            const l = children.length;
            let idx = l;
            let childHeight = 0;
            let childBottom = 0;
            let childTop = 0;
            for (let i = 0; i < l; i++) {
                childHeight = getItemHeight(children[i]);
                childTop = childBottom;
                childBottom = childTop + childHeight;
                if (dragPosition.y < childBottom) {
                    if (currentIndex > i) {
                        if (dragPosition.y < childTop + dragHeight) {
                            idx = i;
                            break;
                        }
                        continue;
                    }
                    idx = i;
                    break;
                }
            }
            return idx;
        },
    };

    // get new index
    const index = cols > 1 ? location.getGridIndex() : location.getColIndex();
    root.dispatch('MOVE_ITEM', { query: view, index });

    // if the index of the item changed, dispatch reorder action
    const currentIndex = dragState.getIndex();

    if (currentIndex === undefined || currentIndex !== index) {
        dragState.setIndex(index);

        if (currentIndex === undefined) return;

        root.dispatch('DID_REORDER_ITEMS', {
            items: root.query('GET_ACTIVE_ITEMS'),
            origin: oldIndex,
            target: index,
        });
    }
};

/**
 * Setup action routes
 */
const route$2 = createRoute({
    DID_ADD_ITEM: addItemView,
    DID_REMOVE_ITEM: removeItemView,
    DID_DRAG_ITEM: dragItem,
});

/**
 * Write to view
 * @param root
 * @param actions
 * @param props
 */
const write$5 = ({ root, props, actions, shouldOptimize }) => {
    // route actions
    route$2({ root, props, actions });

    const { dragCoordinates } = props;

    // available space on horizontal axis
    const horizontalSpace = root.rect.element.width;

    // only draw children that have dimensions
    const visibleChildren = root.childViews.filter(child => child.rect.element.height);

    // sort based on current active items
    const children = root
        .query('GET_ACTIVE_ITEMS')
        .map(item => visibleChildren.find(child => child.id === item.id))
        .filter(item => item);

    // get index
    const dragIndex = dragCoordinates
        ? getItemIndexByPosition(root, children, dragCoordinates)
        : null;

    // add index is used to reserve the dropped/added item index till the actual item is rendered
    const addIndex = root.ref.addIndex || null;

    // add index no longer needed till possibly next draw
    root.ref.addIndex = null;

    let dragIndexOffset = 0;
    let removeIndexOffset = 0;
    let addIndexOffset = 0;

    if (children.length === 0) return;

    const childRect = children[0].rect.element;
    const itemVerticalMargin = childRect.marginTop + childRect.marginBottom;
    const itemHorizontalMargin = childRect.marginLeft + childRect.marginRight;
    const itemWidth = childRect.width + itemHorizontalMargin;
    const itemHeight = childRect.height + itemVerticalMargin;
    const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth);

    // stack
    if (itemsPerRow === 1) {
        let offsetY = 0;
        let dragOffset = 0;

        children.forEach((child, index) => {
            if (dragIndex) {
                let dist = index - dragIndex;
                if (dist === -2) {
                    dragOffset = -itemVerticalMargin * 0.25;
                } else if (dist === -1) {
                    dragOffset = -itemVerticalMargin * 0.75;
                } else if (dist === 0) {
                    dragOffset = itemVerticalMargin * 0.75;
                } else if (dist === 1) {
                    dragOffset = itemVerticalMargin * 0.25;
                } else {
                    dragOffset = 0;
                }
            }

            if (shouldOptimize) {
                child.translateX = null;
                child.translateY = null;
            }

            if (!child.markedForRemoval) {
                moveItem(child, 0, offsetY + dragOffset);
            }

            let itemHeight = child.rect.element.height + itemVerticalMargin;

            let visualHeight = itemHeight * (child.markedForRemoval ? child.opacity : 1);

            offsetY += visualHeight;
        });
    }
    // grid
    else {
        let prevX = 0;
        let prevY = 0;

        children.forEach((child, index) => {
            if (index === dragIndex) {
                dragIndexOffset = 1;
            }

            if (index === addIndex) {
                addIndexOffset += 1;
            }

            if (child.markedForRemoval && child.opacity < 0.5) {
                removeIndexOffset -= 1;
            }

            const visualIndex = index + addIndexOffset + dragIndexOffset + removeIndexOffset;

            const indexX = visualIndex % itemsPerRow;
            const indexY = Math.floor(visualIndex / itemsPerRow);

            const offsetX = indexX * itemWidth;
            const offsetY = indexY * itemHeight;

            const vectorX = Math.sign(offsetX - prevX);
            const vectorY = Math.sign(offsetY - prevY);

            prevX = offsetX;
            prevY = offsetY;

            if (child.markedForRemoval) return;

            if (shouldOptimize) {
                child.translateX = null;
                child.translateY = null;
            }

            moveItem(child, offsetX, offsetY, vectorX, vectorY);
        });
    }
};

/**
 * Filters actions that are meant specifically for a certain child of the list
 * @param child
 * @param actions
 */
const filterSetItemActions = (child, actions) =>
    actions.filter(action => {
        // if action has an id, filter out actions that don't have this child id
        if (action.data && action.data.id) {
            return child.id === action.data.id;
        }

        // allow all other actions
        return true;
    });

const list = createView({
    create: create$8,
    write: write$5,
    tag: 'ul',
    name: 'list',
    didWriteView: ({ root }) => {
        root.childViews
            .filter(view => view.markedForRemoval && view.opacity === 0 && view.resting)
            .forEach(view => {
                view._destroy();
                root.removeChildView(view);
            });
    },
    filterFrameActionsForChild: filterSetItemActions,
    mixins: {
        apis: ['dragCoordinates'],
    },
});

const create$9 = ({ root, props }) => {
    root.ref.list = root.appendChildView(root.createChildView(list));
    props.dragCoordinates = null;
    props.overflowing = false;
};

const storeDragCoordinates = ({ root, props, action }) => {
    if (!root.query('GET_ITEM_INSERT_LOCATION_FREEDOM')) return;
    props.dragCoordinates = {
        left: action.position.scopeLeft - root.ref.list.rect.element.left,
        top:
            action.position.scopeTop -
            (root.rect.outer.top + root.rect.element.marginTop + root.rect.element.scrollTop),
    };
};

const clearDragCoordinates = ({ props }) => {
    props.dragCoordinates = null;
};

const route$3 = createRoute({
    DID_DRAG: storeDragCoordinates,
    DID_END_DRAG: clearDragCoordinates,
});

const write$6 = ({ root, props, actions }) => {
    // route actions
    route$3({ root, props, actions });

    // current drag position
    root.ref.list.dragCoordinates = props.dragCoordinates;

    // if currently overflowing but no longer received overflow
    if (props.overflowing && !props.overflow) {
        props.overflowing = false;

        // reset overflow state
        root.element.dataset.state = '';
        root.height = null;
    }

    // if is not overflowing currently but does receive overflow value
    if (props.overflow) {
        const newHeight = Math.round(props.overflow);
        if (newHeight !== root.height) {
            props.overflowing = true;
            root.element.dataset.state = 'overflow';
            root.height = newHeight;
        }
    }
};

const listScroller = createView({
    create: create$9,
    write: write$6,
    name: 'list-scroller',
    mixins: {
        apis: ['overflow', 'dragCoordinates'],
        styles: ['height', 'translateY'],
        animations: {
            translateY: 'spring',
        },
    },
});

const attrToggle = (element, name, state, enabledValue = '') => {
    if (state) {
        attr(element, name, enabledValue);
    } else {
        element.removeAttribute(name);
    }
};

const resetFileInput = input => {
    // no value, no need to reset
    if (!input || input.value === '') {
        return;
    }

    try {
        // for modern browsers
        input.value = '';
    } catch (err) {}

    // for IE10
    if (input.value) {
        // quickly append input to temp form and reset form
        const form = createElement$1('form');
        const parentNode = input.parentNode;
        const ref = input.nextSibling;
        form.appendChild(input);
        form.reset();

        // re-inject input where it originally was
        if (ref) {
            parentNode.insertBefore(input, ref);
        } else {
            parentNode.appendChild(input);
        }
    }
};

const create$a = ({ root, props }) => {
    // set id so can be referenced from outside labels
    root.element.id = `filepond--browser-${props.id}`;

    // set name of element (is removed when a value is set)
    attr(root.element, 'name', root.query('GET_NAME'));

    // we have to link this element to the status element
    attr(root.element, 'aria-controls', `filepond--assistant-${props.id}`);

    // set label, we use labelled by as otherwise the screenreader does not read the "browse" text in the label (as it has tabindex: 0)
    attr(root.element, 'aria-labelledby', `filepond--drop-label-${props.id}`);

    // set configurable props
    setAcceptedFileTypes({ root, action: { value: root.query('GET_ACCEPTED_FILE_TYPES') } });
    toggleAllowMultiple({ root, action: { value: root.query('GET_ALLOW_MULTIPLE') } });
    toggleDirectoryFilter({ root, action: { value: root.query('GET_ALLOW_DIRECTORIES_ONLY') } });
    toggleDisabled({ root });
    toggleRequired({ root, action: { value: root.query('GET_REQUIRED') } });
    setCaptureMethod({ root, action: { value: root.query('GET_CAPTURE_METHOD') } });

    // handle changes to the input field
    root.ref.handleChange = e => {
        if (!root.element.value) {
            return;
        }

        // extract files and move value of webkitRelativePath path to _relativePath
        const files = Array.from(root.element.files).map(file => {
            file._relativePath = file.webkitRelativePath;
            return file;
        });

        // we add a little delay so the OS file select window can move out of the way before we add our file
        setTimeout(() => {
            // load files
            props.onload(files);

            // reset input, it's just for exposing a method to drop files, should not retain any state
            resetFileInput(root.element);
        }, 250);
    };

    root.element.addEventListener('change', root.ref.handleChange);
};

const setAcceptedFileTypes = ({ root, action }) => {
    if (!root.query('GET_ALLOW_SYNC_ACCEPT_ATTRIBUTE')) return;
    attrToggle(root.element, 'accept', !!action.value, action.value ? action.value.join(',') : '');
};

const toggleAllowMultiple = ({ root, action }) => {
    attrToggle(root.element, 'multiple', action.value);
};

const toggleDirectoryFilter = ({ root, action }) => {
    attrToggle(root.element, 'webkitdirectory', action.value);
};

const toggleDisabled = ({ root }) => {
    const isDisabled = root.query('GET_DISABLED');
    const doesAllowBrowse = root.query('GET_ALLOW_BROWSE');
    const disableField = isDisabled || !doesAllowBrowse;
    attrToggle(root.element, 'disabled', disableField);
};

const toggleRequired = ({ root, action }) => {
    // want to remove required, always possible
    if (!action.value) {
        attrToggle(root.element, 'required', false);
    }
    // if want to make required, only possible when zero items
    else if (root.query('GET_TOTAL_ITEMS') === 0) {
        attrToggle(root.element, 'required', true);
    }
};

const setCaptureMethod = ({ root, action }) => {
    attrToggle(root.element, 'capture', !!action.value, action.value === true ? '' : action.value);
};

const updateRequiredStatus = ({ root }) => {
    const { element } = root;
    // always remove the required attribute when more than zero items
    if (root.query('GET_TOTAL_ITEMS') > 0) {
        attrToggle(element, 'required', false);
        attrToggle(element, 'name', false);
    } else {
        // add name attribute
        attrToggle(element, 'name', true, root.query('GET_NAME'));

        // remove any validation messages
        const shouldCheckValidity = root.query('GET_CHECK_VALIDITY');
        if (shouldCheckValidity) {
            element.setCustomValidity('');
        }

        // we only add required if the field has been deemed required
        if (root.query('GET_REQUIRED')) {
            attrToggle(element, 'required', true);
        }
    }
};

const updateFieldValidityStatus = ({ root }) => {
    const shouldCheckValidity = root.query('GET_CHECK_VALIDITY');
    if (!shouldCheckValidity) return;
    root.element.setCustomValidity(root.query('GET_LABEL_INVALID_FIELD'));
};

const browser = createView({
    tag: 'input',
    name: 'browser',
    ignoreRect: true,
    ignoreRectUpdate: true,
    attributes: {
        type: 'file',
    },
    create: create$a,
    destroy: ({ root }) => {
        root.element.removeEventListener('change', root.ref.handleChange);
    },
    write: createRoute({
        DID_LOAD_ITEM: updateRequiredStatus,
        DID_REMOVE_ITEM: updateRequiredStatus,
        DID_THROW_ITEM_INVALID: updateFieldValidityStatus,

        DID_SET_DISABLED: toggleDisabled,
        DID_SET_ALLOW_BROWSE: toggleDisabled,
        DID_SET_ALLOW_DIRECTORIES_ONLY: toggleDirectoryFilter,
        DID_SET_ALLOW_MULTIPLE: toggleAllowMultiple,
        DID_SET_ACCEPTED_FILE_TYPES: setAcceptedFileTypes,
        DID_SET_CAPTURE_METHOD: setCaptureMethod,
        DID_SET_REQUIRED: toggleRequired,
    }),
});

const Key = {
    ENTER: 13,
    SPACE: 32,
};

const create$b = ({ root, props }) => {
    // create the label and link it to the file browser
    const label = createElement$1('label');
    attr(label, 'for', `filepond--browser-${props.id}`);

    // use for labeling file input (aria-labelledby on file input)
    attr(label, 'id', `filepond--drop-label-${props.id}`);

    // hide the label for screenreaders, the input element will read the contents of the label when it's focussed. If we don't set aria-hidden the screenreader will also navigate the contents of the label separately from the input.
    attr(label, 'aria-hidden', 'true');

    // handle keys
    root.ref.handleKeyDown = e => {
        const isActivationKey = e.keyCode === Key.ENTER || e.keyCode === Key.SPACE;
        if (!isActivationKey) return;
        // stops from triggering the element a second time
        e.preventDefault();

        // click link (will then in turn activate file input)
        root.ref.label.click();
    };

    root.ref.handleClick = e => {
        const isLabelClick = e.target === label || label.contains(e.target);

        // don't want to click twice
        if (isLabelClick) return;

        // click link (will then in turn activate file input)
        root.ref.label.click();
    };

    // attach events
    label.addEventListener('keydown', root.ref.handleKeyDown);
    root.element.addEventListener('click', root.ref.handleClick);

    // update
    updateLabelValue(label, props.caption);

    // add!
    root.appendChild(label);
    root.ref.label = label;
};

const updateLabelValue = (label, value) => {
    label.innerHTML = value;
    const clickable = label.querySelector('.filepond--label-action');
    if (clickable) {
        attr(clickable, 'tabindex', '0');
    }
    return value;
};

const dropLabel = createView({
    name: 'drop-label',
    ignoreRect: true,
    create: create$b,
    destroy: ({ root }) => {
        root.ref.label.addEventListener('keydown', root.ref.handleKeyDown);
        root.element.removeEventListener('click', root.ref.handleClick);
    },
    write: createRoute({
        DID_SET_LABEL_IDLE: ({ root, action }) => {
            updateLabelValue(root.ref.label, action.value);
        },
    }),
    mixins: {
        styles: ['opacity', 'translateX', 'translateY'],
        animations: {
            opacity: { type: 'tween', duration: 150 },
            translateX: 'spring',
            translateY: 'spring',
        },
    },
});

const blob = createView({
    name: 'drip-blob',
    ignoreRect: true,
    mixins: {
        styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity'],
        animations: {
            scaleX: 'spring',
            scaleY: 'spring',
            translateX: 'spring',
            translateY: 'spring',
            opacity: { type: 'tween', duration: 250 },
        },
    },
});

const addBlob = ({ root }) => {
    const centerX = root.rect.element.width * 0.5;
    const centerY = root.rect.element.height * 0.5;

    root.ref.blob = root.appendChildView(
        root.createChildView(blob, {
            opacity: 0,
            scaleX: 2.5,
            scaleY: 2.5,
            translateX: centerX,
            translateY: centerY,
        })
    );
};

const moveBlob = ({ root, action }) => {
    if (!root.ref.blob) {
        addBlob({ root });
        return;
    }

    root.ref.blob.translateX = action.position.scopeLeft;
    root.ref.blob.translateY = action.position.scopeTop;
    root.ref.blob.scaleX = 1;
    root.ref.blob.scaleY = 1;
    root.ref.blob.opacity = 1;
};

const hideBlob = ({ root }) => {
    if (!root.ref.blob) {
        return;
    }
    root.ref.blob.opacity = 0;
};

const explodeBlob = ({ root }) => {
    if (!root.ref.blob) {
        return;
    }
    root.ref.blob.scaleX = 2.5;
    root.ref.blob.scaleY = 2.5;
    root.ref.blob.opacity = 0;
};

const write$7 = ({ root, props, actions }) => {
    route$4({ root, props, actions });

    const { blob } = root.ref;

    if (actions.length === 0 && blob && blob.opacity === 0) {
        root.removeChildView(blob);
        root.ref.blob = null;
    }
};

const route$4 = createRoute({
    DID_DRAG: moveBlob,
    DID_DROP: explodeBlob,
    DID_END_DRAG: hideBlob,
});

const drip = createView({
    ignoreRect: true,
    ignoreRectUpdate: true,
    name: 'drip',
    write: write$7,
});

const setInputFiles = (element, files) => {
    try {
        // Create a DataTransfer instance and add a newly created file
        const dataTransfer = new DataTransfer();
        files.forEach(file => {
            if (file instanceof File) {
                dataTransfer.items.add(file);
            } else {
                dataTransfer.items.add(
                    new File([file], file.name, {
                        type: file.type,
                    })
                );
            }
        });

        // Assign the DataTransfer files list to the file input
        element.files = dataTransfer.files;
    } catch (err) {
        return false;
    }
    return true;
};

const create$c = ({ root }) => (root.ref.fields = {});

const getField = (root, id) => root.ref.fields[id];

const syncFieldPositionsWithItems = root => {
    root.query('GET_ACTIVE_ITEMS').forEach(item => {
        if (!root.ref.fields[item.id]) return;
        root.element.appendChild(root.ref.fields[item.id]);
    });
};

const didReorderItems = ({ root }) => syncFieldPositionsWithItems(root);

const didAddItem = ({ root, action }) => {
    const fileItem = root.query('GET_ITEM', action.id);
    const isLocalFile = fileItem.origin === FileOrigin.LOCAL;
    const shouldUseFileInput = !isLocalFile && root.query('SHOULD_UPDATE_FILE_INPUT');
    const dataContainer = createElement$1('input');
    dataContainer.type = shouldUseFileInput ? 'file' : 'hidden';
    dataContainer.name = root.query('GET_NAME');
    dataContainer.disabled = root.query('GET_DISABLED');
    root.ref.fields[action.id] = dataContainer;
    syncFieldPositionsWithItems(root);
};

const didLoadItem$1 = ({ root, action }) => {
    const field = getField(root, action.id);
    if (!field) return;

    // store server ref in hidden input
    if (action.serverFileReference !== null) field.value = action.serverFileReference;

    // store file item in file input
    if (!root.query('SHOULD_UPDATE_FILE_INPUT')) return;

    const fileItem = root.query('GET_ITEM', action.id);
    setInputFiles(field, [fileItem.file]);
};

const didPrepareOutput = ({ root, action }) => {
    // this timeout pushes the handler after 'load'
    if (!root.query('SHOULD_UPDATE_FILE_INPUT')) return;
    setTimeout(() => {
        const field = getField(root, action.id);
        if (!field) return;
        setInputFiles(field, [action.file]);
    }, 0);
};

const didSetDisabled = ({ root }) => {
    root.element.disabled = root.query('GET_DISABLED');
};

const didRemoveItem = ({ root, action }) => {
    const field = getField(root, action.id);
    if (!field) return;
    if (field.parentNode) field.parentNode.removeChild(field);
    delete root.ref.fields[action.id];
};

// only runs for server files (so doesn't deal with file input)
const didDefineValue = ({ root, action }) => {
    const field = getField(root, action.id);
    if (!field) return;
    if (action.value === null) {
        // clear field value
        field.removeAttribute('value');
    } else {
        // set field value
        field.value = action.value;
    }
    syncFieldPositionsWithItems(root);
};

const write$8 = createRoute({
    DID_SET_DISABLED: didSetDisabled,
    DID_ADD_ITEM: didAddItem,
    DID_LOAD_ITEM: didLoadItem$1,
    DID_REMOVE_ITEM: didRemoveItem,
    DID_DEFINE_VALUE: didDefineValue,
    DID_PREPARE_OUTPUT: didPrepareOutput,
    DID_REORDER_ITEMS: didReorderItems,
    DID_SORT_ITEMS: didReorderItems,
});

const data = createView({
    tag: 'fieldset',
    name: 'data',
    create: create$c,
    write: write$8,
    ignoreRect: true,
});

const getRootNode = element => ('getRootNode' in element ? element.getRootNode() : document);

const images = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'tiff'];
const text$1 = ['css', 'csv', 'html', 'txt'];
const map = {
    zip: 'zip|compressed',
    epub: 'application/epub+zip',
};

const guesstimateMimeType = (extension = '') => {
    extension = extension.toLowerCase();
    if (images.includes(extension)) {
        return (
            'image/' + (extension === 'jpg' ? 'jpeg' : extension === 'svg' ? 'svg+xml' : extension)
        );
    }
    if (text$1.includes(extension)) {
        return 'text/' + extension;
    }

    return map[extension] || '';
};

const requestDataTransferItems = dataTransfer =>
    new Promise((resolve, reject) => {
        // try to get links from transfer, if found we'll exit immediately (unless a file is in the dataTransfer as well, this is because Firefox could represent the file as a URL and a file object at the same time)
        const links = getLinks(dataTransfer);
        if (links.length && !hasFiles(dataTransfer)) {
            return resolve(links);
        }
        // try to get files from the transfer
        getFiles(dataTransfer).then(resolve);
    });

/**
 * Test if datatransfer has files
 */
const hasFiles = dataTransfer => {
    if (dataTransfer.files) return dataTransfer.files.length > 0;
    return false;
};

/**
 * Extracts files from a DataTransfer object
 */
const getFiles = dataTransfer =>
    new Promise((resolve, reject) => {
        // get the transfer items as promises
        const promisedFiles = (dataTransfer.items ? Array.from(dataTransfer.items) : [])

            // only keep file system items (files and directories)
            .filter(item => isFileSystemItem(item))

            // map each item to promise
            .map(item => getFilesFromItem(item));

        // if is empty, see if we can extract some info from the files property as a fallback
        if (!promisedFiles.length) {
            // TODO: test for directories (should not be allowed)
            // Use FileReader, problem is that the files property gets lost in the process
            resolve(dataTransfer.files ? Array.from(dataTransfer.files) : []);
            return;
        }

        // done!
        Promise.all(promisedFiles)
            .then(returnedFileGroups => {
                // flatten groups
                const files = [];
                returnedFileGroups.forEach(group => {
                    files.push.apply(files, group);
                });

                // done (filter out empty files)!
                resolve(
                    files
                        .filter(file => file)
                        .map(file => {
                            if (!file._relativePath) file._relativePath = file.webkitRelativePath;
                            return file;
                        })
                );
            })
            .catch(console.error);
    });

const isFileSystemItem = item => {
    if (isEntry(item)) {
        const entry = getAsEntry(item);
        if (entry) {
            return entry.isFile || entry.isDirectory;
        }
    }
    return item.kind === 'file';
};

const getFilesFromItem = item =>
    new Promise((resolve, reject) => {
        if (isDirectoryEntry(item)) {
            getFilesInDirectory(getAsEntry(item))
                .then(resolve)
                .catch(reject);
            return;
        }

        resolve([item.getAsFile()]);
    });

const getFilesInDirectory = entry =>
    new Promise((resolve, reject) => {
        const files = [];

        // the total entries to read
        let dirCounter = 0;
        let fileCounter = 0;

        const resolveIfDone = () => {
            if (fileCounter === 0 && dirCounter === 0) {
                resolve(files);
            }
        };

        // the recursive function
        const readEntries = dirEntry => {
            dirCounter++;

            const directoryReader = dirEntry.createReader();

            // directories are returned in batches, we need to process all batches before we're done
            const readBatch = () => {
                directoryReader.readEntries(entries => {
                    if (entries.length === 0) {
                        dirCounter--;
                        resolveIfDone();
                        return;
                    }

                    entries.forEach(entry => {
                        // recursively read more directories
                        if (entry.isDirectory) {
                            readEntries(entry);
                        } else {
                            // read as file
                            fileCounter++;

                            entry.file(file => {
                                const correctedFile = correctMissingFileType(file);
                                if (entry.fullPath) correctedFile._relativePath = entry.fullPath;
                                files.push(correctedFile);
                                fileCounter--;
                                resolveIfDone();
                            });
                        }
                    });

                    // try to get next batch of files
                    readBatch();
                }, reject);
            };

            // read first batch of files
            readBatch();
        };

        // go!
        readEntries(entry);
    });

const correctMissingFileType = file => {
    if (file.type.length) return file;
    const date = file.lastModifiedDate;
    const name = file.name;
    const type = guesstimateMimeType(getExtensionFromFilename(file.name));
    if (!type.length) return file;
    file = file.slice(0, file.size, type);
    file.name = name;
    file.lastModifiedDate = date;
    return file;
};

const isDirectoryEntry = item => isEntry(item) && (getAsEntry(item) || {}).isDirectory;

const isEntry = item => 'webkitGetAsEntry' in item;

const getAsEntry = item => item.webkitGetAsEntry();

/**
 * Extracts links from a DataTransfer object
 */
const getLinks = dataTransfer => {
    let links = [];
    try {
        // look in meta data property
        links = getLinksFromTransferMetaData(dataTransfer);
        if (links.length) {
            return links;
        }
        links = getLinksFromTransferURLData(dataTransfer);
    } catch (e) {
        // nope nope nope (probably IE trouble)
    }
    return links;
};

const getLinksFromTransferURLData = dataTransfer => {
    let data = dataTransfer.getData('url');
    if (typeof data === 'string' && data.length) {
        return [data];
    }
    return [];
};

const getLinksFromTransferMetaData = dataTransfer => {
    let data = dataTransfer.getData('text/html');
    if (typeof data === 'string' && data.length) {
        const matches = data.match(/src\s*=\s*"(.+?)"/);
        if (matches) {
            return [matches[1]];
        }
    }
    return [];
};

const dragNDropObservers = [];

const eventPosition = e => ({
    pageLeft: e.pageX,
    pageTop: e.pageY,
    scopeLeft: e.offsetX || e.layerX,
    scopeTop: e.offsetY || e.layerY,
});

const createDragNDropClient = (element, scopeToObserve, filterElement) => {
    const observer = getDragNDropObserver(scopeToObserve);

    const client = {
        element,
        filterElement,
        state: null,
        ondrop: () => {},
        onenter: () => {},
        ondrag: () => {},
        onexit: () => {},
        onload: () => {},
        allowdrop: () => {},
    };

    client.destroy = observer.addListener(client);

    return client;
};

const getDragNDropObserver = element => {
    // see if already exists, if so, return
    const observer = dragNDropObservers.find(item => item.element === element);
    if (observer) {
        return observer;
    }

    // create new observer, does not yet exist for this element
    const newObserver = createDragNDropObserver(element);
    dragNDropObservers.push(newObserver);
    return newObserver;
};

const createDragNDropObserver = element => {
    const clients = [];

    const routes = {
        dragenter,
        dragover,
        dragleave,
        drop,
    };

    const handlers = {};

    forin(routes, (event, createHandler) => {
        handlers[event] = createHandler(element, clients);
        element.addEventListener(event, handlers[event], false);
    });

    const observer = {
        element,
        addListener: client => {
            // add as client
            clients.push(client);

            // return removeListener function
            return () => {
                // remove client
                clients.splice(clients.indexOf(client), 1);

                // if no more clients, clean up observer
                if (clients.length === 0) {
                    dragNDropObservers.splice(dragNDropObservers.indexOf(observer), 1);

                    forin(routes, event => {
                        element.removeEventListener(event, handlers[event], false);
                    });
                }
            };
        },
    };

    return observer;
};

const elementFromPoint = (root, point) => {
    if (!('elementFromPoint' in root)) {
        root = document;
    }
    return root.elementFromPoint(point.x, point.y);
};

const isEventTarget = (e, target) => {
    // get root
    const root = getRootNode(target);

    // get element at position
    // if root is not actual shadow DOM and does not have elementFromPoint method, use the one on document
    const elementAtPosition = elementFromPoint(root, {
        x: e.pageX - window.pageXOffset,
        y: e.pageY - window.pageYOffset,
    });

    // test if target is the element or if one of its children is
    return elementAtPosition === target || target.contains(elementAtPosition);
};

let initialTarget = null;

const setDropEffect = (dataTransfer, effect) => {
    // is in try catch as IE11 will throw error if not
    try {
        dataTransfer.dropEffect = effect;
    } catch (e) {}
};

const dragenter = (root, clients) => e => {
    e.preventDefault();

    initialTarget = e.target;

    clients.forEach(client => {
        const { element, onenter } = client;

        if (isEventTarget(e, element)) {
            client.state = 'enter';

            // fire enter event
            onenter(eventPosition(e));
        }
    });
};

const dragover = (root, clients) => e => {
    e.preventDefault();

    const dataTransfer = e.dataTransfer;

    requestDataTransferItems(dataTransfer).then(items => {
        let overDropTarget = false;

        clients.some(client => {
            const { filterElement, element, onenter, onexit, ondrag, allowdrop } = client;

            // by default we can drop
            setDropEffect(dataTransfer, 'copy');

            // allow transfer of these items
            const allowsTransfer = allowdrop(items);

            // only used when can be dropped on page
            if (!allowsTransfer) {
                setDropEffect(dataTransfer, 'none');
                return;
            }

            // targetting this client
            if (isEventTarget(e, element)) {
                overDropTarget = true;

                // had no previous state, means we are entering this client
                if (client.state === null) {
                    client.state = 'enter';
                    onenter(eventPosition(e));
                    return;
                }

                // now over element (no matter if it allows the drop or not)
                client.state = 'over';

                // needs to allow transfer
                if (filterElement && !allowsTransfer) {
                    setDropEffect(dataTransfer, 'none');
                    return;
                }

                // dragging
                ondrag(eventPosition(e));
            } else {
                // should be over an element to drop
                if (filterElement && !overDropTarget) {
                    setDropEffect(dataTransfer, 'none');
                }

                // might have just left this client?
                if (client.state) {
                    client.state = null;
                    onexit(eventPosition(e));
                }
            }
        });
    });
};

const drop = (root, clients) => e => {
    e.preventDefault();

    const dataTransfer = e.dataTransfer;

    requestDataTransferItems(dataTransfer).then(items => {
        clients.forEach(client => {
            const { filterElement, element, ondrop, onexit, allowdrop } = client;

            client.state = null;

            // if we're filtering on element we need to be over the element to drop
            if (filterElement && !isEventTarget(e, element)) return;

            // no transfer for this client
            if (!allowdrop(items)) return onexit(eventPosition(e));

            // we can drop these items on this client
            ondrop(eventPosition(e), items);
        });
    });
};

const dragleave = (root, clients) => e => {
    if (initialTarget !== e.target) {
        return;
    }

    clients.forEach(client => {
        const { onexit } = client;

        client.state = null;

        onexit(eventPosition(e));
    });
};

const createHopper = (scope, validateItems, options) => {
    // is now hopper scope
    scope.classList.add('filepond--hopper');

    // shortcuts
    const { catchesDropsOnPage, requiresDropOnElement, filterItems = items => items } = options;

    // create a dnd client
    const client = createDragNDropClient(
        scope,
        catchesDropsOnPage ? document.documentElement : scope,
        requiresDropOnElement
    );

    // current client state
    let lastState = '';
    let currentState = '';

    // determines if a file may be dropped
    client.allowdrop = items => {
        // TODO: if we can, throw error to indicate the items cannot by dropped

        return validateItems(filterItems(items));
    };

    client.ondrop = (position, items) => {
        const filteredItems = filterItems(items);

        if (!validateItems(filteredItems)) {
            api.ondragend(position);
            return;
        }

        currentState = 'drag-drop';

        api.onload(filteredItems, position);
    };

    client.ondrag = position => {
        api.ondrag(position);
    };

    client.onenter = position => {
        currentState = 'drag-over';

        api.ondragstart(position);
    };

    client.onexit = position => {
        currentState = 'drag-exit';

        api.ondragend(position);
    };

    const api = {
        updateHopperState: () => {
            if (lastState !== currentState) {
                scope.dataset.hopperState = currentState;
                lastState = currentState;
            }
        },
        onload: () => {},
        ondragstart: () => {},
        ondrag: () => {},
        ondragend: () => {},
        destroy: () => {
            // destroy client
            client.destroy();
        },
    };

    return api;
};

let listening = false;
const listeners$1 = [];

const handlePaste = e => {
    // if is pasting in input or textarea and the target is outside of a filepond scope, ignore
    const activeEl = document.activeElement;
    if (activeEl && /textarea|input/i.test(activeEl.nodeName)) {
        // test textarea or input is contained in filepond root
        let inScope = false;
        let element = activeEl;
        while (element !== document.body) {
            if (element.classList.contains('filepond--root')) {
                inScope = true;
                break;
            }
            element = element.parentNode;
        }

        if (!inScope) return;
    }

    requestDataTransferItems(e.clipboardData).then(files => {
        // no files received
        if (!files.length) {
            return;
        }

        // notify listeners of received files
        listeners$1.forEach(listener => listener(files));
    });
};

const listen = cb => {
    // can't add twice
    if (listeners$1.includes(cb)) {
        return;
    }

    // add initial listener
    listeners$1.push(cb);

    // setup paste listener for entire page
    if (listening) {
        return;
    }

    listening = true;
    document.addEventListener('paste', handlePaste);
};

const unlisten = listener => {
    arrayRemove(listeners$1, listeners$1.indexOf(listener));

    // clean up
    if (listeners$1.length === 0) {
        document.removeEventListener('paste', handlePaste);
        listening = false;
    }
};

const createPaster = () => {
    const cb = files => {
        api.onload(files);
    };

    const api = {
        destroy: () => {
            unlisten(cb);
        },
        onload: () => {},
    };

    listen(cb);

    return api;
};

/**
 * Creates the file view
 */
const create$d = ({ root, props }) => {
    root.element.id = `filepond--assistant-${props.id}`;
    attr(root.element, 'role', 'status');
    attr(root.element, 'aria-live', 'polite');
    attr(root.element, 'aria-relevant', 'additions');
};

let addFilesNotificationTimeout = null;
let notificationClearTimeout = null;

const filenames = [];

const assist = (root, message) => {
    root.element.textContent = message;
};

const clear$1 = root => {
    root.element.textContent = '';
};

const listModified = (root, filename, label) => {
    const total = root.query('GET_TOTAL_ITEMS');
    assist(
        root,
        `${label} ${filename}, ${total} ${
            total === 1
                ? root.query('GET_LABEL_FILE_COUNT_SINGULAR')
                : root.query('GET_LABEL_FILE_COUNT_PLURAL')
        }`
    );

    // clear group after set amount of time so the status is not read twice
    clearTimeout(notificationClearTimeout);
    notificationClearTimeout = setTimeout(() => {
        clear$1(root);
    }, 1500);
};

const isUsingFilePond = root => root.element.parentNode.contains(document.activeElement);

const itemAdded = ({ root, action }) => {
    if (!isUsingFilePond(root)) {
        return;
    }

    root.element.textContent = '';
    const item = root.query('GET_ITEM', action.id);
    filenames.push(item.filename);

    clearTimeout(addFilesNotificationTimeout);
    addFilesNotificationTimeout = setTimeout(() => {
        listModified(root, filenames.join(', '), root.query('GET_LABEL_FILE_ADDED'));
        filenames.length = 0;
    }, 750);
};

const itemRemoved = ({ root, action }) => {
    if (!isUsingFilePond(root)) {
        return;
    }

    const item = action.item;
    listModified(root, item.filename, root.query('GET_LABEL_FILE_REMOVED'));
};

const itemProcessed = ({ root, action }) => {
    // will also notify the user when FilePond is not being used, as the user might be occupied with other activities while uploading a file

    const item = root.query('GET_ITEM', action.id);
    const filename = item.filename;
    const label = root.query('GET_LABEL_FILE_PROCESSING_COMPLETE');

    assist(root, `${filename} ${label}`);
};

const itemProcessedUndo = ({ root, action }) => {
    const item = root.query('GET_ITEM', action.id);
    const filename = item.filename;
    const label = root.query('GET_LABEL_FILE_PROCESSING_ABORTED');

    assist(root, `${filename} ${label}`);
};

const itemError = ({ root, action }) => {
    const item = root.query('GET_ITEM', action.id);
    const filename = item.filename;

    // will also notify the user when FilePond is not being used, as the user might be occupied with other activities while uploading a file

    assist(root, `${action.status.main} ${filename} ${action.status.sub}`);
};

const assistant = createView({
    create: create$d,
    ignoreRect: true,
    ignoreRectUpdate: true,
    write: createRoute({
        DID_LOAD_ITEM: itemAdded,
        DID_REMOVE_ITEM: itemRemoved,
        DID_COMPLETE_ITEM_PROCESSING: itemProcessed,

        DID_ABORT_ITEM_PROCESSING: itemProcessedUndo,
        DID_REVERT_ITEM_PROCESSING: itemProcessedUndo,

        DID_THROW_ITEM_REMOVE_ERROR: itemError,
        DID_THROW_ITEM_LOAD_ERROR: itemError,
        DID_THROW_ITEM_INVALID: itemError,
        DID_THROW_ITEM_PROCESSING_ERROR: itemError,
    }),
    tag: 'span',
    name: 'assistant',
});

const toCamels = (string, separator = '-') =>
    string.replace(new RegExp(`${separator}.`, 'g'), sub => sub.charAt(1).toUpperCase());

const debounce = (func, interval = 16, immidiateOnly = true) => {
    let last = Date.now();
    let timeout = null;

    return (...args) => {
        clearTimeout(timeout);

        const dist = Date.now() - last;

        const fn = () => {
            last = Date.now();
            func(...args);
        };

        if (dist < interval) {
            // we need to delay by the difference between interval and dist
            // for example: if distance is 10 ms and interval is 16 ms,
            // we need to wait an additional 6ms before calling the function)
            if (!immidiateOnly) {
                timeout = setTimeout(fn, interval - dist);
            }
        } else {
            // go!
            fn();
        }
    };
};

const MAX_FILES_LIMIT = 1000000;

const prevent = e => e.preventDefault();

const create$e = ({ root, props }) => {
    // Add id
    const id = root.query('GET_ID');
    if (id) {
        root.element.id = id;
    }

    // Add className
    const className = root.query('GET_CLASS_NAME');
    if (className) {
        className
            .split(' ')
            .filter(name => name.length)
            .forEach(name => {
                root.element.classList.add(name);
            });
    }

    // Field label
    root.ref.label = root.appendChildView(
        root.createChildView(dropLabel, {
            ...props,
            translateY: null,
            caption: root.query('GET_LABEL_IDLE'),
        })
    );

    // List of items
    root.ref.list = root.appendChildView(root.createChildView(listScroller, { translateY: null }));

    // Background panel
    root.ref.panel = root.appendChildView(root.createChildView(panel, { name: 'panel-root' }));

    // Assistant notifies assistive tech when content changes
    root.ref.assistant = root.appendChildView(root.createChildView(assistant, { ...props }));

    // Data
    root.ref.data = root.appendChildView(root.createChildView(data, { ...props }));

    // Measure (tests if fixed height was set)
    // DOCTYPE needs to be set for this to work
    root.ref.measure = createElement$1('div');
    root.ref.measure.style.height = '100%';
    root.element.appendChild(root.ref.measure);

    // information on the root height or fixed height status
    root.ref.bounds = null;

    // apply initial style properties
    root.query('GET_STYLES')
        .filter(style => !isEmpty(style.value))
        .map(({ name, value }) => {
            root.element.dataset[name] = value;
        });

    // determine if width changed
    root.ref.widthPrevious = null;
    root.ref.widthUpdated = debounce(() => {
        root.ref.updateHistory = [];
        root.dispatch('DID_RESIZE_ROOT');
    }, 250);

    // history of updates
    root.ref.previousAspectRatio = null;
    root.ref.updateHistory = [];

    // prevent scrolling and zooming on iOS (only if supports pointer events, for then we can enable reorder)
    const canHover = window.matchMedia('(pointer: fine) and (hover: hover)').matches;
    const hasPointerEvents = 'PointerEvent' in window;
    if (root.query('GET_ALLOW_REORDER') && hasPointerEvents && !canHover) {
        root.element.addEventListener('touchmove', prevent, { passive: false });
        root.element.addEventListener('gesturestart', prevent);
    }

    // add credits
    const credits = root.query('GET_CREDITS');
    const hasCredits = credits.length === 2;
    if (hasCredits) {
        const frag = document.createElement('a');
        frag.className = 'filepond--credits';
        frag.setAttribute('aria-hidden', 'true');
        frag.href = credits[0];
        frag.tabindex = -1;
        frag.target = '_blank';
        frag.rel = 'noopener noreferrer';
        frag.textContent = credits[1];
        root.element.appendChild(frag);
        root.ref.credits = frag;
    }
};

const write$9 = ({ root, props, actions }) => {
    // route actions
    route$5({ root, props, actions });

    // apply style properties
    actions
        .filter(action => /^DID_SET_STYLE_/.test(action.type))
        .filter(action => !isEmpty(action.data.value))
        .map(({ type, data }) => {
            const name = toCamels(type.substring(8).toLowerCase(), '_');
            root.element.dataset[name] = data.value;
            root.invalidateLayout();
        });

    if (root.rect.element.hidden) return;

    if (root.rect.element.width !== root.ref.widthPrevious) {
        root.ref.widthPrevious = root.rect.element.width;
        root.ref.widthUpdated();
    }

    // get box bounds, we do this only once
    let bounds = root.ref.bounds;
    if (!bounds) {
        bounds = root.ref.bounds = calculateRootBoundingBoxHeight(root);

        // destroy measure element
        root.element.removeChild(root.ref.measure);
        root.ref.measure = null;
    }

    // get quick references to various high level parts of the upload tool
    const { hopper, label, list, panel } = root.ref;

    // sets correct state to hopper scope
    if (hopper) {
        hopper.updateHopperState();
    }

    // bool to indicate if we're full or not
    const aspectRatio = root.query('GET_PANEL_ASPECT_RATIO');
    const isMultiItem = root.query('GET_ALLOW_MULTIPLE');
    const totalItems = root.query('GET_TOTAL_ITEMS');
    const maxItems = isMultiItem ? root.query('GET_MAX_FILES') || MAX_FILES_LIMIT : 1;
    const atMaxCapacity = totalItems === maxItems;

    // action used to add item
    const addAction = actions.find(action => action.type === 'DID_ADD_ITEM');

    // if reached max capacity and we've just reached it
    if (atMaxCapacity && addAction) {
        // get interaction type
        const interactionMethod = addAction.data.interactionMethod;

        // hide label
        label.opacity = 0;

        if (isMultiItem) {
            label.translateY = -40;
        } else {
            if (interactionMethod === InteractionMethod.API) {
                label.translateX = 40;
            } else if (interactionMethod === InteractionMethod.BROWSE) {
                label.translateY = 40;
            } else {
                label.translateY = 30;
            }
        }
    } else if (!atMaxCapacity) {
        label.opacity = 1;
        label.translateX = 0;
        label.translateY = 0;
    }

    const listItemMargin = calculateListItemMargin(root);

    const listHeight = calculateListHeight(root);

    const labelHeight = label.rect.element.height;
    const currentLabelHeight = !isMultiItem || atMaxCapacity ? 0 : labelHeight;

    const listMarginTop = atMaxCapacity ? list.rect.element.marginTop : 0;
    const listMarginBottom = totalItems === 0 ? 0 : list.rect.element.marginBottom;

    const visualHeight = currentLabelHeight + listMarginTop + listHeight.visual + listMarginBottom;
    const boundsHeight = currentLabelHeight + listMarginTop + listHeight.bounds + listMarginBottom;

    // link list to label bottom position
    list.translateY =
        Math.max(0, currentLabelHeight - list.rect.element.marginTop) - listItemMargin.top;

    if (aspectRatio) {
        // fixed aspect ratio

        // calculate height based on width
        const width = root.rect.element.width;
        const height = width * aspectRatio;

        // clear history if aspect ratio has changed
        if (aspectRatio !== root.ref.previousAspectRatio) {
            root.ref.previousAspectRatio = aspectRatio;
            root.ref.updateHistory = [];
        }

        // remember this width
        const history = root.ref.updateHistory;
        history.push(width);

        const MAX_BOUNCES = 2;
        if (history.length > MAX_BOUNCES * 2) {
            const l = history.length;
            const bottom = l - 10;
            let bounces = 0;
            for (let i = l; i >= bottom; i--) {
                if (history[i] === history[i - 2]) {
                    bounces++;
                }

                if (bounces >= MAX_BOUNCES) {
                    // dont adjust height
                    return;
                }
            }
        }

        // fix height of panel so it adheres to aspect ratio
        panel.scalable = false;
        panel.height = height;

        // available height for list
        const listAvailableHeight =
            // the height of the panel minus the label height
            height -
            currentLabelHeight -
            // the room we leave open between the end of the list and the panel bottom
            (listMarginBottom - listItemMargin.bottom) -
            // if we're full we need to leave some room between the top of the panel and the list
            (atMaxCapacity ? listMarginTop : 0);

        if (listHeight.visual > listAvailableHeight) {
            list.overflow = listAvailableHeight;
        } else {
            list.overflow = null;
        }

        // set container bounds (so pushes siblings downwards)
        root.height = height;
    } else if (bounds.fixedHeight) {
        // fixed height

        // fix height of panel
        panel.scalable = false;

        // available height for list
        const listAvailableHeight =
            // the height of the panel minus the label height
            bounds.fixedHeight -
            currentLabelHeight -
            // the room we leave open between the end of the list and the panel bottom
            (listMarginBottom - listItemMargin.bottom) -
            // if we're full we need to leave some room between the top of the panel and the list
            (atMaxCapacity ? listMarginTop : 0);

        // set list height
        if (listHeight.visual > listAvailableHeight) {
            list.overflow = listAvailableHeight;
        } else {
            list.overflow = null;
        }

        // no need to set container bounds as these are handles by CSS fixed height
    } else if (bounds.cappedHeight) {
        // max-height

        // not a fixed height panel
        const isCappedHeight = visualHeight >= bounds.cappedHeight;
        const panelHeight = Math.min(bounds.cappedHeight, visualHeight);
        panel.scalable = true;
        panel.height = isCappedHeight
            ? panelHeight
            : panelHeight - listItemMargin.top - listItemMargin.bottom;

        // available height for list
        const listAvailableHeight =
            // the height of the panel minus the label height
            panelHeight -
            currentLabelHeight -
            // the room we leave open between the end of the list and the panel bottom
            (listMarginBottom - listItemMargin.bottom) -
            // if we're full we need to leave some room between the top of the panel and the list
            (atMaxCapacity ? listMarginTop : 0);

        // set list height (if is overflowing)
        if (visualHeight > bounds.cappedHeight && listHeight.visual > listAvailableHeight) {
            list.overflow = listAvailableHeight;
        } else {
            list.overflow = null;
        }

        // set container bounds (so pushes siblings downwards)
        root.height = Math.min(
            bounds.cappedHeight,
            boundsHeight - listItemMargin.top - listItemMargin.bottom
        );
    } else {
        // flexible height

        // not a fixed height panel
        const itemMargin = totalItems > 0 ? listItemMargin.top + listItemMargin.bottom : 0;
        panel.scalable = true;
        panel.height = Math.max(labelHeight, visualHeight - itemMargin);

        // set container bounds (so pushes siblings downwards)
        root.height = Math.max(labelHeight, boundsHeight - itemMargin);
    }

    // move credits to bottom
    if (root.ref.credits && panel.heightCurrent)
        root.ref.credits.style.transform = `translateY(${panel.heightCurrent}px)`;
};

const calculateListItemMargin = root => {
    const item = root.ref.list.childViews[0].childViews[0];
    return item
        ? {
              top: item.rect.element.marginTop,
              bottom: item.rect.element.marginBottom,
          }
        : {
              top: 0,
              bottom: 0,
          };
};

const calculateListHeight = root => {
    let visual = 0;
    let bounds = 0;

    // get file list reference
    const scrollList = root.ref.list;
    const itemList = scrollList.childViews[0];
    const visibleChildren = itemList.childViews.filter(child => child.rect.element.height);
    const children = root
        .query('GET_ACTIVE_ITEMS')
        .map(item => visibleChildren.find(child => child.id === item.id))
        .filter(item => item);

    // no children, done!
    if (children.length === 0) return { visual, bounds };

    const horizontalSpace = itemList.rect.element.width;
    const dragIndex = getItemIndexByPosition(itemList, children, scrollList.dragCoordinates);

    const childRect = children[0].rect.element;

    const itemVerticalMargin = childRect.marginTop + childRect.marginBottom;
    const itemHorizontalMargin = childRect.marginLeft + childRect.marginRight;

    const itemWidth = childRect.width + itemHorizontalMargin;
    const itemHeight = childRect.height + itemVerticalMargin;

    const newItem = typeof dragIndex !== 'undefined' && dragIndex >= 0 ? 1 : 0;
    const removedItem = children.find(child => child.markedForRemoval && child.opacity < 0.45)
        ? -1
        : 0;
    const verticalItemCount = children.length + newItem + removedItem;
    const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth);

    // stack
    if (itemsPerRow === 1) {
        children.forEach(item => {
            const height = item.rect.element.height + itemVerticalMargin;
            bounds += height;
            visual += height * item.opacity;
        });
    }
    // grid
    else {
        bounds = Math.ceil(verticalItemCount / itemsPerRow) * itemHeight;
        visual = bounds;
    }

    return { visual, bounds };
};

const calculateRootBoundingBoxHeight = root => {
    const height = root.ref.measureHeight || null;
    const cappedHeight = parseInt(root.style.maxHeight, 10) || null;
    const fixedHeight = height === 0 ? null : height;

    return {
        cappedHeight,
        fixedHeight,
    };
};

const exceedsMaxFiles = (root, items) => {
    const allowReplace = root.query('GET_ALLOW_REPLACE');
    const allowMultiple = root.query('GET_ALLOW_MULTIPLE');
    const totalItems = root.query('GET_TOTAL_ITEMS');
    let maxItems = root.query('GET_MAX_FILES');

    // total amount of items being dragged
    const totalBrowseItems = items.length;

    // if does not allow multiple items and dragging more than one item
    if (!allowMultiple && totalBrowseItems > 1) {
        root.dispatch('DID_THROW_MAX_FILES', {
            source: items,
            error: createResponse('warning', 0, 'Max files'),
        });
        return true;
    }

    // limit max items to one if not allowed to drop multiple items
    maxItems = allowMultiple ? maxItems : 1;

    if (!allowMultiple && allowReplace) {
        // There is only one item, so there is room to replace or add an item
        return false;
    }

    // no more room?
    const hasMaxItems = isInt(maxItems);
    if (hasMaxItems && totalItems + totalBrowseItems > maxItems) {
        root.dispatch('DID_THROW_MAX_FILES', {
            source: items,
            error: createResponse('warning', 0, 'Max files'),
        });
        return true;
    }

    return false;
};

const getDragIndex = (list, children, position) => {
    const itemList = list.childViews[0];
    return getItemIndexByPosition(itemList, children, {
        left: position.scopeLeft - itemList.rect.element.left,
        top:
            position.scopeTop -
            (list.rect.outer.top + list.rect.element.marginTop + list.rect.element.scrollTop),
    });
};

/**
 * Enable or disable file drop functionality
 */
const toggleDrop = root => {
    const isAllowed = root.query('GET_ALLOW_DROP');
    const isDisabled = root.query('GET_DISABLED');
    const enabled = isAllowed && !isDisabled;
    if (enabled && !root.ref.hopper) {
        const hopper = createHopper(
            root.element,
            items => {
                // allow quick validation of dropped items
                const beforeDropFile = root.query('GET_BEFORE_DROP_FILE') || (() => true);

                // all items should be validated by all filters as valid
                const dropValidation = root.query('GET_DROP_VALIDATION');
                return dropValidation
                    ? items.every(
                          item =>
                              applyFilters('ALLOW_HOPPER_ITEM', item, {
                                  query: root.query,
                              }).every(result => result === true) && beforeDropFile(item)
                      )
                    : true;
            },
            {
                filterItems: items => {
                    const ignoredFiles = root.query('GET_IGNORED_FILES');
                    return items.filter(item => {
                        if (isFile(item)) {
                            return !ignoredFiles.includes(item.name.toLowerCase());
                        }
                        return true;
                    });
                },
                catchesDropsOnPage: root.query('GET_DROP_ON_PAGE'),
                requiresDropOnElement: root.query('GET_DROP_ON_ELEMENT'),
            }
        );

        hopper.onload = (items, position) => {
            // get item children elements and sort based on list sort
            const list = root.ref.list.childViews[0];
            const visibleChildren = list.childViews.filter(child => child.rect.element.height);
            const children = root
                .query('GET_ACTIVE_ITEMS')
                .map(item => visibleChildren.find(child => child.id === item.id))
                .filter(item => item);

            applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch }).then(queue => {
                // these files don't fit so stop here
                if (exceedsMaxFiles(root, queue)) return false;

                // go
                root.dispatch('ADD_ITEMS', {
                    items: queue,
                    index: getDragIndex(root.ref.list, children, position),
                    interactionMethod: InteractionMethod.DROP,
                });
            });

            root.dispatch('DID_DROP', { position });

            root.dispatch('DID_END_DRAG', { position });
        };

        hopper.ondragstart = position => {
            root.dispatch('DID_START_DRAG', { position });
        };

        hopper.ondrag = debounce(position => {
            root.dispatch('DID_DRAG', { position });
        });

        hopper.ondragend = position => {
            root.dispatch('DID_END_DRAG', { position });
        };

        root.ref.hopper = hopper;

        root.ref.drip = root.appendChildView(root.createChildView(drip));
    } else if (!enabled && root.ref.hopper) {
        root.ref.hopper.destroy();
        root.ref.hopper = null;
        root.removeChildView(root.ref.drip);
    }
};

/**
 * Enable or disable browse functionality
 */
const toggleBrowse = (root, props) => {
    const isAllowed = root.query('GET_ALLOW_BROWSE');
    const isDisabled = root.query('GET_DISABLED');
    const enabled = isAllowed && !isDisabled;
    if (enabled && !root.ref.browser) {
        root.ref.browser = root.appendChildView(
            root.createChildView(browser, {
                ...props,
                onload: items => {
                    applyFilterChain('ADD_ITEMS', items, {
                        dispatch: root.dispatch,
                    }).then(queue => {
                        // these files don't fit so stop here
                        if (exceedsMaxFiles(root, queue)) return false;

                        // add items!
                        root.dispatch('ADD_ITEMS', {
                            items: queue,
                            index: -1,
                            interactionMethod: InteractionMethod.BROWSE,
                        });
                    });
                },
            }),
            0
        );
    } else if (!enabled && root.ref.browser) {
        root.removeChildView(root.ref.browser);
        root.ref.browser = null;
    }
};

/**
 * Enable or disable paste functionality
 */
const togglePaste = root => {
    const isAllowed = root.query('GET_ALLOW_PASTE');
    const isDisabled = root.query('GET_DISABLED');
    const enabled = isAllowed && !isDisabled;
    if (enabled && !root.ref.paster) {
        root.ref.paster = createPaster();
        root.ref.paster.onload = items => {
            applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch }).then(queue => {
                // these files don't fit so stop here
                if (exceedsMaxFiles(root, queue)) return false;

                // add items!
                root.dispatch('ADD_ITEMS', {
                    items: queue,
                    index: -1,
                    interactionMethod: InteractionMethod.PASTE,
                });
            });
        };
    } else if (!enabled && root.ref.paster) {
        root.ref.paster.destroy();
        root.ref.paster = null;
    }
};

/**
 * Route actions
 */
const route$5 = createRoute({
    DID_SET_ALLOW_BROWSE: ({ root, props }) => {
        toggleBrowse(root, props);
    },
    DID_SET_ALLOW_DROP: ({ root }) => {
        toggleDrop(root);
    },
    DID_SET_ALLOW_PASTE: ({ root }) => {
        togglePaste(root);
    },
    DID_SET_DISABLED: ({ root, props }) => {
        toggleDrop(root);
        togglePaste(root);
        toggleBrowse(root, props);
        const isDisabled = root.query('GET_DISABLED');
        if (isDisabled) {
            root.element.dataset.disabled = 'disabled';
        } else {
            // delete root.element.dataset.disabled; <= this does not work on iOS 10
            root.element.removeAttribute('data-disabled');
        }
    },
});

const root = createView({
    name: 'root',
    read: ({ root }) => {
        if (root.ref.measure) {
            root.ref.measureHeight = root.ref.measure.offsetHeight;
        }
    },
    create: create$e,
    write: write$9,
    destroy: ({ root }) => {
        if (root.ref.paster) {
            root.ref.paster.destroy();
        }
        if (root.ref.hopper) {
            root.ref.hopper.destroy();
        }
        root.element.removeEventListener('touchmove', prevent);
        root.element.removeEventListener('gesturestart', prevent);
    },
    mixins: {
        styles: ['height'],
    },
});

// creates the app
const createApp = (initialOptions = {}) => {
    // let element
    let originalElement = null;

    // get default options
    const defaultOptions = getOptions();

    // create the data store, this will contain all our app info
    const store = createStore(
        // initial state (should be serializable)
        createInitialState(defaultOptions),

        // queries
        [queries, createOptionQueries(defaultOptions)],

        // action handlers
        [actions, createOptionActions(defaultOptions)]
    );

    // set initial options
    store.dispatch('SET_OPTIONS', { options: initialOptions });

    // kick thread if visibility changes
    const visibilityHandler = () => {
        if (document.hidden) return;
        store.dispatch('KICK');
    };
    document.addEventListener('visibilitychange', visibilityHandler);

    // re-render on window resize start and finish
    let resizeDoneTimer = null;
    let isResizing = false;
    let isResizingHorizontally = false;
    let initialWindowWidth = null;
    let currentWindowWidth = null;
    const resizeHandler = () => {
        if (!isResizing) {
            isResizing = true;
        }
        clearTimeout(resizeDoneTimer);
        resizeDoneTimer = setTimeout(() => {
            isResizing = false;
            initialWindowWidth = null;
            currentWindowWidth = null;
            if (isResizingHorizontally) {
                isResizingHorizontally = false;
                store.dispatch('DID_STOP_RESIZE');
            }
        }, 500);
    };
    window.addEventListener('resize', resizeHandler);

    // render initial view
    const view = root(store, { id: getUniqueId() });

    //
    // PRIVATE API -------------------------------------------------------------------------------------
    //
    let isResting = false;
    let isHidden = false;

    const readWriteApi = {
        // necessary for update loop

        /**
         * Reads from dom (never call manually)
         * @private
         */
        _read: () => {
            // test if we're resizing horizontally
            // TODO: see if we can optimize this by measuring root rect
            if (isResizing) {
                currentWindowWidth = window.innerWidth;
                if (!initialWindowWidth) {
                    initialWindowWidth = currentWindowWidth;
                }

                if (!isResizingHorizontally && currentWindowWidth !== initialWindowWidth) {
                    store.dispatch('DID_START_RESIZE');
                    isResizingHorizontally = true;
                }
            }

            if (isHidden && isResting) {
                // test if is no longer hidden
                isResting = view.element.offsetParent === null;
            }

            // if resting, no need to read as numbers will still all be correct
            if (isResting) return;

            // read view data
            view._read();

            // if is hidden we need to know so we exit rest mode when revealed
            isHidden = view.rect.element.hidden;
        },

        /**
         * Writes to dom (never call manually)
         * @private
         */
        _write: ts => {
            // get all actions from store
            const actions = store
                .processActionQueue()

                // filter out set actions (these will automatically trigger DID_SET)
                .filter(action => !/^SET_/.test(action.type));

            // if was idling and no actions stop here
            if (isResting && !actions.length) return;

            // some actions might trigger events
            routeActionsToEvents(actions);

            // update the view
            isResting = view._write(ts, actions, isResizingHorizontally);

            // will clean up all archived items
            removeReleasedItems(store.query('GET_ITEMS'));

            // now idling
            if (isResting) {
                store.processDispatchQueue();
            }
        },
    };

    //
    // EXPOSE EVENTS -------------------------------------------------------------------------------------
    //
    const createEvent = name => data => {
        // create default event
        const event = {
            type: name,
        };

        // no data to add
        if (!data) {
            return event;
        }

        // copy relevant props
        if (data.hasOwnProperty('error')) {
            event.error = data.error ? { ...data.error } : null;
        }

        if (data.status) {
            event.status = { ...data.status };
        }

        if (data.file) {
            event.output = data.file;
        }

        // only source is available, else add item if possible
        if (data.source) {
            event.file = data.source;
        } else if (data.item || data.id) {
            const item = data.item ? data.item : store.query('GET_ITEM', data.id);
            event.file = item ? createItemAPI(item) : null;
        }

        // map all items in a possible items array
        if (data.items) {
            event.items = data.items.map(createItemAPI);
        }

        // if this is a progress event add the progress amount
        if (/progress/.test(name)) {
            event.progress = data.progress;
        }

        // copy relevant props
        if (data.hasOwnProperty('origin') && data.hasOwnProperty('target')) {
            event.origin = data.origin;
            event.target = data.target;
        }

        return event;
    };

    const eventRoutes = {
        DID_DESTROY: createEvent('destroy'),

        DID_INIT: createEvent('init'),

        DID_THROW_MAX_FILES: createEvent('warning'),

        DID_INIT_ITEM: createEvent('initfile'),
        DID_START_ITEM_LOAD: createEvent('addfilestart'),
        DID_UPDATE_ITEM_LOAD_PROGRESS: createEvent('addfileprogress'),
        DID_LOAD_ITEM: createEvent('addfile'),

        DID_THROW_ITEM_INVALID: [createEvent('error'), createEvent('addfile')],

        DID_THROW_ITEM_LOAD_ERROR: [createEvent('error'), createEvent('addfile')],

        DID_THROW_ITEM_REMOVE_ERROR: [createEvent('error'), createEvent('removefile')],

        DID_PREPARE_OUTPUT: createEvent('preparefile'),

        DID_START_ITEM_PROCESSING: createEvent('processfilestart'),
        DID_UPDATE_ITEM_PROCESS_PROGRESS: createEvent('processfileprogress'),
        DID_ABORT_ITEM_PROCESSING: createEvent('processfileabort'),
        DID_COMPLETE_ITEM_PROCESSING: createEvent('processfile'),
        DID_COMPLETE_ITEM_PROCESSING_ALL: createEvent('processfiles'),
        DID_REVERT_ITEM_PROCESSING: createEvent('processfilerevert'),

        DID_THROW_ITEM_PROCESSING_ERROR: [createEvent('error'), createEvent('processfile')],

        DID_REMOVE_ITEM: createEvent('removefile'),

        DID_UPDATE_ITEMS: createEvent('updatefiles'),

        DID_ACTIVATE_ITEM: createEvent('activatefile'),

        DID_REORDER_ITEMS: createEvent('reorderfiles'),
    };

    const exposeEvent = event => {
        // create event object to be dispatched
        const detail = { pond: exports, ...event };
        delete detail.type;
        view.element.dispatchEvent(
            new CustomEvent(`FilePond:${event.type}`, {
                // event info
                detail,

                // event behaviour
                bubbles: true,
                cancelable: true,
                composed: true, // triggers listeners outside of shadow root
            })
        );

        // event object to params used for `on()` event handlers and callbacks `oninit()`
        const params = [];

        // if is possible error event, make it the first param
        if (event.hasOwnProperty('error')) {
            params.push(event.error);
        }

        // file is always section
        if (event.hasOwnProperty('file')) {
            params.push(event.file);
        }

        // append other props
        const filtered = ['type', 'error', 'file'];
        Object.keys(event)
            .filter(key => !filtered.includes(key))
            .forEach(key => params.push(event[key]));

        // on(type, () => { })
        exports.fire(event.type, ...params);

        // oninit = () => {}
        const handler = store.query(`GET_ON${event.type.toUpperCase()}`);
        if (handler) {
            handler(...params);
        }
    };

    const routeActionsToEvents = actions => {
        if (!actions.length) return;
        actions
            .filter(action => eventRoutes[action.type])
            .forEach(action => {
                const routes = eventRoutes[action.type];
                (Array.isArray(routes) ? routes : [routes]).forEach(route => {
                    // this isn't fantastic, but because of the stacking of settimeouts plugins can handle the did_load before the did_init
                    if (action.type === 'DID_INIT_ITEM') {
                        exposeEvent(route(action.data));
                    } else {
                        setTimeout(() => {
                            exposeEvent(route(action.data));
                        }, 0);
                    }
                });
            });
    };

    //
    // PUBLIC API -------------------------------------------------------------------------------------
    //
    const setOptions = options => store.dispatch('SET_OPTIONS', { options });

    const getFile = query => store.query('GET_ACTIVE_ITEM', query);

    const prepareFile = query =>
        new Promise((resolve, reject) => {
            store.dispatch('REQUEST_ITEM_PREPARE', {
                query,
                success: item => {
                    resolve(item);
                },
                failure: error => {
                    reject(error);
                },
            });
        });

    const addFile = (source, options = {}) =>
        new Promise((resolve, reject) => {
            addFiles([{ source, options }], { index: options.index })
                .then(items => resolve(items && items[0]))
                .catch(reject);
        });

    const isFilePondFile = obj => obj.file && obj.id;

    const removeFile = (query, options) => {
        // if only passed options
        if (typeof query === 'object' && !isFilePondFile(query) && !options) {
            options = query;
            query = undefined;
        }

        // request item removal
        store.dispatch('REMOVE_ITEM', { ...options, query });

        // see if item has been removed
        return store.query('GET_ACTIVE_ITEM', query) === null;
    };

    const addFiles = (...args) =>
        new Promise((resolve, reject) => {
            const sources = [];
            const options = {};

            // user passed a sources array
            if (isArray(args[0])) {
                sources.push.apply(sources, args[0]);
                Object.assign(options, args[1] || {});
            } else {
                // user passed sources as arguments, last one might be options object
                const lastArgument = args[args.length - 1];
                if (typeof lastArgument === 'object' && !(lastArgument instanceof Blob)) {
                    Object.assign(options, args.pop());
                }

                // add rest to sources
                sources.push(...args);
            }

            store.dispatch('ADD_ITEMS', {
                items: sources,
                index: options.index,
                interactionMethod: InteractionMethod.API,
                success: resolve,
                failure: reject,
            });
        });

    const getFiles = () => store.query('GET_ACTIVE_ITEMS');

    const processFile = query =>
        new Promise((resolve, reject) => {
            store.dispatch('REQUEST_ITEM_PROCESSING', {
                query,
                success: item => {
                    resolve(item);
                },
                failure: error => {
                    reject(error);
                },
            });
        });

    const prepareFiles = (...args) => {
        const queries = Array.isArray(args[0]) ? args[0] : args;
        const items = queries.length ? queries : getFiles();
        return Promise.all(items.map(prepareFile));
    };

    const processFiles = (...args) => {
        const queries = Array.isArray(args[0]) ? args[0] : args;
        if (!queries.length) {
            const files = getFiles().filter(
                item =>
                    !(item.status === ItemStatus.IDLE && item.origin === FileOrigin.LOCAL) &&
                    item.status !== ItemStatus.PROCESSING &&
                    item.status !== ItemStatus.PROCESSING_COMPLETE &&
                    item.status !== ItemStatus.PROCESSING_REVERT_ERROR
            );
            return Promise.all(files.map(processFile));
        }
        return Promise.all(queries.map(processFile));
    };

    const removeFiles = (...args) => {
        const queries = Array.isArray(args[0]) ? args[0] : args;

        let options;
        if (typeof queries[queries.length - 1] === 'object') {
            options = queries.pop();
        } else if (Array.isArray(args[0])) {
            options = args[1];
        }

        const files = getFiles();

        if (!queries.length) return Promise.all(files.map(file => removeFile(file, options)));

        // when removing by index the indexes shift after each file removal so we need to convert indexes to ids
        const mappedQueries = queries
            .map(query => (isNumber(query) ? (files[query] ? files[query].id : null) : query))
            .filter(query => query);

        return mappedQueries.map(q => removeFile(q, options));
    };

    const exports = {
        // supports events
        ...on(),

        // inject private api methods
        ...readWriteApi,

        // inject all getters and setters
        ...createOptionAPI(store, defaultOptions),

        /**
         * Override options defined in options object
         * @param options
         */
        setOptions,

        /**
         * Load the given file
         * @param source - the source of the file (either a File, base64 data uri or url)
         * @param options - object, { index: 0 }
         */
        addFile,

        /**
         * Load the given files
         * @param sources - the sources of the files to load
         * @param options - object, { index: 0 }
         */
        addFiles,

        /**
         * Returns the file objects matching the given query
         * @param query { string, number, null }
         */
        getFile,

        /**
         * Upload file with given name
         * @param query { string, number, null  }
         */
        processFile,

        /**
         * Request prepare output for file with given name
         * @param query { string, number, null  }
         */
        prepareFile,

        /**
         * Removes a file by its name
         * @param query { string, number, null  }
         */
        removeFile,

        /**
         * Moves a file to a new location in the files list
         */
        moveFile: (query, index) => store.dispatch('MOVE_ITEM', { query, index }),

        /**
         * Returns all files (wrapped in public api)
         */
        getFiles,

        /**
         * Starts uploading all files
         */
        processFiles,

        /**
         * Clears all files from the files list
         */
        removeFiles,

        /**
         * Starts preparing output of all files
         */
        prepareFiles,

        /**
         * Sort list of files
         */
        sort: compare => store.dispatch('SORT', { compare }),

        /**
         * Browse the file system for a file
         */
        browse: () => {
            // needs to be trigger directly as user action needs to be traceable (is not traceable in requestAnimationFrame)
            var input = view.element.querySelector('input[type=file]');
            if (input) {
                input.click();
            }
        },

        /**
         * Destroys the app
         */
        destroy: () => {
            // request destruction
            exports.fire('destroy', view.element);

            // stop active processes (file uploads, fetches, stuff like that)
            // loop over items and depending on states call abort for ongoing processes
            store.dispatch('ABORT_ALL');

            // destroy view
            view._destroy();

            // stop listening to resize
            window.removeEventListener('resize', resizeHandler);

            // stop listening to the visiblitychange event
            document.removeEventListener('visibilitychange', visibilityHandler);

            // dispatch destroy
            store.dispatch('DID_DESTROY');
        },

        /**
         * Inserts the plugin before the target element
         */
        insertBefore: element => insertBefore(view.element, element),

        /**
         * Inserts the plugin after the target element
         */
        insertAfter: element => insertAfter(view.element, element),

        /**
         * Appends the plugin to the target element
         */
        appendTo: element => element.appendChild(view.element),

        /**
         * Replaces an element with the app
         */
        replaceElement: element => {
            // insert the app before the element
            insertBefore(view.element, element);

            // remove the original element
            element.parentNode.removeChild(element);

            // remember original element
            originalElement = element;
        },

        /**
         * Restores the original element
         */
        restoreElement: () => {
            if (!originalElement) {
                return; // no element to restore
            }

            // restore original element
            insertAfter(originalElement, view.element);

            // remove our element
            view.element.parentNode.removeChild(view.element);

            // remove reference
            originalElement = null;
        },

        /**
         * Returns true if the app root is attached to given element
         * @param element
         */
        isAttachedTo: element => view.element === element || originalElement === element,

        /**
         * Returns the root element
         */
        element: {
            get: () => view.element,
        },

        /**
         * Returns the current pond status
         */
        status: {
            get: () => store.query('GET_STATUS'),
        },
    };

    // Done!
    store.dispatch('DID_INIT');

    // create actual api object
    return createObject(exports);
};

const createAppObject = (customOptions = {}) => {
    // default options
    const defaultOptions = {};
    forin(getOptions(), (key, value) => {
        defaultOptions[key] = value[0];
    });

    // set app options
    const app = createApp({
        // default options
        ...defaultOptions,

        // custom options
        ...customOptions,
    });

    // return the plugin instance
    return app;
};

const lowerCaseFirstLetter = string => string.charAt(0).toLowerCase() + string.slice(1);

const attributeNameToPropertyName = attributeName => toCamels(attributeName.replace(/^data-/, ''));

const mapObject = (object, propertyMap) => {
    // remove unwanted
    forin(propertyMap, (selector, mapping) => {
        forin(object, (property, value) => {
            // create regexp shortcut
            const selectorRegExp = new RegExp(selector);

            // tests if
            const matches = selectorRegExp.test(property);

            // no match, skip
            if (!matches) {
                return;
            }

            // if there's a mapping, the original property is always removed
            delete object[property];

            // should only remove, we done!
            if (mapping === false) {
                return;
            }

            // move value to new property
            if (isString(mapping)) {
                object[mapping] = value;
                return;
            }

            // move to group
            const group = mapping.group;
            if (isObject(mapping) && !object[group]) {
                object[group] = {};
            }

            object[group][lowerCaseFirstLetter(property.replace(selectorRegExp, ''))] = value;
        });

        // do submapping
        if (mapping.mapping) {
            mapObject(object[mapping.group], mapping.mapping);
        }
    });
};

const getAttributesAsObject = (node, attributeMapping = {}) => {
    // turn attributes into object
    const attributes = [];
    forin(node.attributes, index => {
        attributes.push(node.attributes[index]);
    });

    const output = attributes
        .filter(attribute => attribute.name)
        .reduce((obj, attribute) => {
            const value = attr(node, attribute.name);

            obj[attributeNameToPropertyName(attribute.name)] =
                value === attribute.name ? true : value;
            return obj;
        }, {});

    // do mapping of object properties
    mapObject(output, attributeMapping);

    return output;
};

const createAppAtElement = (element, options = {}) => {
    // how attributes of the input element are mapped to the options for the plugin
    const attributeMapping = {
        // translate to other name
        '^class$': 'className',
        '^multiple$': 'allowMultiple',
        '^capture$': 'captureMethod',
        '^webkitdirectory$': 'allowDirectoriesOnly',

        // group under single property
        '^server': {
            group: 'server',
            mapping: {
                '^process': {
                    group: 'process',
                },
                '^revert': {
                    group: 'revert',
                },
                '^fetch': {
                    group: 'fetch',
                },
                '^restore': {
                    group: 'restore',
                },
                '^load': {
                    group: 'load',
                },
            },
        },

        // don't include in object
        '^type$': false,
        '^files$': false,
    };

    // add additional option translators
    applyFilters('SET_ATTRIBUTE_TO_OPTION_MAP', attributeMapping);

    // create final options object by setting options object and then overriding options supplied on element
    const mergedOptions = {
        ...options,
    };

    const attributeOptions = getAttributesAsObject(
        element.nodeName === 'FIELDSET' ? element.querySelector('input[type=file]') : element,
        attributeMapping
    );

    // merge with options object
    Object.keys(attributeOptions).forEach(key => {
        if (isObject(attributeOptions[key])) {
            if (!isObject(mergedOptions[key])) {
                mergedOptions[key] = {};
            }
            Object.assign(mergedOptions[key], attributeOptions[key]);
        } else {
            mergedOptions[key] = attributeOptions[key];
        }
    });

    // if parent is a fieldset, get files from parent by selecting all input fields that are not file upload fields
    // these will then be automatically set to the initial files
    mergedOptions.files = (options.files || []).concat(
        Array.from(element.querySelectorAll('input:not([type=file])')).map(input => ({
            source: input.value,
            options: {
                type: input.dataset.type,
            },
        }))
    );

    // build plugin
    const app = createAppObject(mergedOptions);

    // add already selected files
    if (element.files) {
        Array.from(element.files).forEach(file => {
            app.addFile(file);
        });
    }

    // replace the target element
    app.replaceElement(element);

    // expose
    return app;
};

// if an element is passed, we create the instance at that element, if not, we just create an up object
const createApp$1 = (...args) =>
    isNode(args[0]) ? createAppAtElement(...args) : createAppObject(...args);

const PRIVATE_METHODS = ['fire', '_read', '_write'];

const createAppAPI = app => {
    const api = {};

    copyObjectPropertiesToObject(app, api, PRIVATE_METHODS);

    return api;
};

/**
 * Replaces placeholders in given string with replacements
 * @param string - "Foo {bar}""
 * @param replacements - { "bar": 10 }
 */
const replaceInString = (string, replacements) =>
    string.replace(/(?:{([a-zA-Z]+)})/g, (match, group) => replacements[group]);

const createWorker = fn => {
    const workerBlob = new Blob(['(', fn.toString(), ')()'], {
        type: 'application/javascript',
    });
    const workerURL = URL.createObjectURL(workerBlob);
    const worker = new Worker(workerURL);

    return {
        transfer: (message, cb) => {},
        post: (message, cb, transferList) => {
            const id = getUniqueId();

            worker.onmessage = e => {
                if (e.data.id === id) {
                    cb(e.data.message);
                }
            };

            worker.postMessage(
                {
                    id,
                    message,
                },
                transferList
            );
        },
        terminate: () => {
            worker.terminate();
            URL.revokeObjectURL(workerURL);
        },
    };
};

const loadImage = url =>
    new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => {
            resolve(img);
        };
        img.onerror = e => {
            reject(e);
        };
        img.src = url;
    });

const renameFile = (file, name) => {
    const renamedFile = file.slice(0, file.size, file.type);
    renamedFile.lastModifiedDate = file.lastModifiedDate;
    renamedFile.name = name;
    return renamedFile;
};

const copyFile = file => renameFile(file, file.name);

// already registered plugins (can't register twice)
const registeredPlugins = [];

// pass utils to plugin
const createAppPlugin = plugin => {
    // already registered
    if (registeredPlugins.includes(plugin)) {
        return;
    }

    // remember this plugin
    registeredPlugins.push(plugin);

    // setup!
    const pluginOutline = plugin({
        addFilter,
        utils: {
            Type,
            forin,
            isString,
            isFile,
            toNaturalFileSize,
            replaceInString,
            getExtensionFromFilename,
            getFilenameWithoutExtension,
            guesstimateMimeType,
            getFileFromBlob,
            getFilenameFromURL,
            createRoute,
            createWorker,
            createView,
            createItemAPI,
            loadImage,
            copyFile,
            renameFile,
            createBlob,
            applyFilterChain,
            text,
            getNumericAspectRatioFromString,
        },
        views: {
            fileActionButton,
        },
    });

    // add plugin options to default options
    extendDefaultOptions(pluginOutline.options);
};

// feature detection used by supported() method
const isOperaMini = () => Object.prototype.toString.call(window.operamini) === '[object OperaMini]';
const hasPromises = () => 'Promise' in window;
const hasBlobSlice = () => 'slice' in Blob.prototype;
const hasCreateObjectURL = () => 'URL' in window && 'createObjectURL' in window.URL;
const hasVisibility = () => 'visibilityState' in document;
const hasTiming = () => 'performance' in window; // iOS 8.x
const hasCSSSupports = () => 'supports' in (window.CSS || {}); // use to detect Safari 9+
const isIE11 = () => /MSIE|Trident/.test(window.navigator.userAgent);

const supported = (() => {
    // Runs immediately and then remembers result for subsequent calls
    const isSupported =
        // Has to be a browser
        isBrowser() &&
        // Can't run on Opera Mini due to lack of everything
        !isOperaMini() &&
        // Require these APIs to feature detect a modern browser
        hasVisibility() &&
        hasPromises() &&
        hasBlobSlice() &&
        hasCreateObjectURL() &&
        hasTiming() &&
        // doesn't need CSSSupports but is a good way to detect Safari 9+ (we do want to support IE11 though)
        (hasCSSSupports() || isIE11());

    return () => isSupported;
})();

/**
 * Plugin internal state (over all instances)
 */
const state = {
    // active app instances, used to redraw the apps and to find the later
    apps: [],
};

// plugin name
const name = 'filepond';

/**
 * Public Plugin methods
 */
const fn = () => {};
let Status$1 = {};
let FileStatus = {};
let FileOrigin$1 = {};
let OptionTypes = {};
let create$f = fn;
let destroy = fn;
let parse = fn;
let find = fn;
let registerPlugin = fn;
let getOptions$1 = fn;
let setOptions$1 = fn;

// if not supported, no API
if (supported()) {
    // start painter and fire load event
    createPainter(
        () => {
            state.apps.forEach(app => app._read());
        },
        ts => {
            state.apps.forEach(app => app._write(ts));
        }
    );

    // fire loaded event so we know when FilePond is available
    const dispatch = () => {
        // let others know we have area ready
        document.dispatchEvent(
            new CustomEvent('FilePond:loaded', {
                detail: {
                    supported,
                    create: create$f,
                    destroy,
                    parse,
                    find,
                    registerPlugin,
                    setOptions: setOptions$1,
                },
            })
        );

        // clean up event
        document.removeEventListener('DOMContentLoaded', dispatch);
    };

    if (document.readyState !== 'loading') {
        // move to back of execution queue, FilePond should have been exported by then
        setTimeout(() => dispatch(), 0);
    } else {
        document.addEventListener('DOMContentLoaded', dispatch);
    }

    // updates the OptionTypes object based on the current options
    const updateOptionTypes = () =>
        forin(getOptions(), (key, value) => {
            OptionTypes[key] = value[1];
        });

    Status$1 = { ...Status };
    FileOrigin$1 = { ...FileOrigin };
    FileStatus = { ...ItemStatus };

    OptionTypes = {};
    updateOptionTypes();

    // create method, creates apps and adds them to the app array
    create$f = (...args) => {
        const app = createApp$1(...args);
        app.on('destroy', destroy);
        state.apps.push(app);
        return createAppAPI(app);
    };

    // destroys apps and removes them from the app array
    destroy = hook => {
        // returns true if the app was destroyed successfully
        const indexToRemove = state.apps.findIndex(app => app.isAttachedTo(hook));
        if (indexToRemove >= 0) {
            // remove from apps
            const app = state.apps.splice(indexToRemove, 1)[0];

            // restore original dom element
            app.restoreElement();

            return true;
        }

        return false;
    };

    // parses the given context for plugins (does not include the context element itself)
    parse = context => {
        // get all possible hooks
        const matchedHooks = Array.from(context.querySelectorAll(`.${name}`));

        // filter out already active hooks
        const newHooks = matchedHooks.filter(
            newHook => !state.apps.find(app => app.isAttachedTo(newHook))
        );

        // create new instance for each hook
        return newHooks.map(hook => create$f(hook));
    };

    // returns an app based on the given element hook
    find = hook => {
        const app = state.apps.find(app => app.isAttachedTo(hook));
        if (!app) {
            return null;
        }
        return createAppAPI(app);
    };

    // adds a plugin extension
    registerPlugin = (...plugins) => {
        // register plugins
        plugins.forEach(createAppPlugin);

        // update OptionTypes, each plugin might have extended the default options
        updateOptionTypes();
    };

    getOptions$1 = () => {
        const opts = {};
        forin(getOptions(), (key, value) => {
            opts[key] = value[0];
        });
        return opts;
    };

    setOptions$1 = opts => {
        if (isObject(opts)) {
            // update existing plugins
            state.apps.forEach(app => {
                app.setOptions(opts);
            });

            // override defaults
            setOptions(opts);
        }

        // return new options
        return getOptions$1();
    };
}

export {
    FileOrigin$1 as FileOrigin,
    FileStatus,
    OptionTypes,
    Status$1 as Status,
    create$f as create,
    destroy,
    find,
    getOptions$1 as getOptions,
    parse,
    registerPlugin,
    setOptions$1 as setOptions,
    supported,
};
