UNPKG

filepond

Version:

FilePond, Where files go to stretch their bits.

8,776 lines 224 kB
/*!
 * FilePond 4.6.0
 * Licensed under MIT, https://opensource.org/licenses/MIT/
 * Please visit https://pqina.nl/filepond/ for details.
 */

/* eslint-disable */

const isNode = value => value instanceof HTMLElement;

const createStore = (initialState, queries = [], actions = []) => {
  // internal state
  const state = {
    ...initialState
  };

  // contains all actions for next frame, is clear when actions are requested
  const actionQueue = [];
  const dispatchQueue = [];

  // returns a duplicate of the current state
  const getState = () => ({ ...state });

  // returns a duplicate of the actions array and clears the actions array
  const processActionQueue = () => {
    // create copy of actions queue
    const queue = [...actionQueue];

    // clear actions queue (we don't want no double actions)
    actionQueue.length = 0;

    return queue;
  };

  // processes actions that might block the main UI thread
  const processDispatchQueue = () => {
    // create copy of actions queue
    const queue = [...dispatchQueue];

    // clear actions queue (we don't want no double actions)
    dispatchQueue.length = 0;

    // now dispatch these actions
    queue.forEach(({ type, data }) => {
      dispatch(type, data);
    });
  };

  // adds a new action, calls its handler and
  const dispatch = (type, data, isBlocking) => {
    // is blocking action
    if (isBlocking) {
      dispatchQueue.push({
        type,
        data
      });
      return;
    }

    // if this action has a handler, handle the action
    if (actionHandlers[type]) {
      actionHandlers[type](data);
    }

    // now add action
    actionQueue.push({
      type,
      data
    });
  };

  const query = (str, ...args) =>
    queryHandles[str] ? queryHandles[str](...args) : null;

  const api = {
    getState,
    processActionQueue,
    processDispatchQueue,
    dispatch,
    query
  };

  let queryHandles = {};
  queries.forEach(query => {
    queryHandles = {
      ...query(state),
      ...queryHandles
    };
  });

  let actionHandlers = {};
  actions.forEach(action => {
    actionHandlers = {
      ...action(dispatch, query, state),
      ...actionHandlers
    };
  });

  return api;
};

const defineProperty = (obj, property, definition) => {
  if (typeof definition === 'function') {
    obj[property] = definition;
    return;
  }
  Object.defineProperty(obj, property, { ...definition });
};

const forin = (obj, cb) => {
  for (const key in obj) {
    if (!obj.hasOwnProperty(key)) {
      continue;
    }

    cb(key, obj[key]);
  }
};

const createObject = definition => {
  const obj = {};
  forin(definition, property => {
    defineProperty(obj, property, definition[property]);
  });
  return obj;
};

const attr = (node, name, value = null) => {
  if (value === null) {
    return node.getAttribute(name) || node.hasAttribute(name);
  }
  node.setAttribute(name, value);
};

const ns = 'http://www.w3.org/2000/svg';
const svgElements = ['svg', 'path']; // only svg elements used

const isSVGElement = tag => svgElements.includes(tag);

const createElement = (tag, className, attributes = {}) => {
  if (typeof className === 'object') {
    attributes = className;
    className = null;
  }
  const element = isSVGElement(tag)
    ? document.createElementNS(ns, tag)
    : document.createElement(tag);
  if (className) {
    if (isSVGElement(tag)) {
      attr(element, 'class', className);
    } else {
      element.className = className;
    }
  }
  forin(attributes, (name, value) => {
    attr(element, name, value);
  });
  return element;
};

const appendChild = parent => (child, index) => {
  if (typeof index !== 'undefined' && parent.children[index]) {
    parent.insertBefore(child, parent.children[index]);
  } else {
    parent.appendChild(child);
  }
};

const appendChildView = (parent, childViews) => (view, index) => {
  if (typeof index !== 'undefined') {
    childViews.splice(index, 0, view);
  } else {
    childViews.push(view);
  }

  return view;
};

const removeChildView = (parent, childViews) => view => {
  // remove from child views
  childViews.splice(childViews.indexOf(view), 1);

  // remove the element
  if (view.element.parentNode) {
    parent.removeChild(view.element);
  }

  return view;
};

const getViewRect = (elementRect, childViews, offset, scale) => {
  const left = offset[0] || elementRect.left;
  const top = offset[1] || elementRect.top;
  const right = left + elementRect.width;
  const bottom = top + elementRect.height * (scale[1] || 1);

  const rect = {
    // the rectangle of the element itself
    element: {
      ...elementRect
    },

    // the rectangle of the element expanded to contain its children, does not include any margins
    inner: {
      left: elementRect.left,
      top: elementRect.top,
      right: elementRect.right,
      bottom: elementRect.bottom
    },

    // the rectangle of the element expanded to contain its children including own margin and child margins
    // margins will be added after we've recalculated the size
    outer: {
      left,
      top,
      right,
      bottom
    }
  };

  // expand rect to fit all child rectangles
  childViews
    .filter(childView => !childView.isRectIgnored())
    .map(childView => childView.rect)
    .forEach(childViewRect => {
      expandRect(rect.inner, { ...childViewRect.inner });
      expandRect(rect.outer, { ...childViewRect.outer });
    });

  // calculate inner width and height
  calculateRectSize(rect.inner);

  // append additional margin (top and left margins are included in top and left automatically)
  rect.outer.bottom += rect.element.marginBottom;
  rect.outer.right += rect.element.marginRight;

  // calculate outer width and height
  calculateRectSize(rect.outer);

  return rect;
};

const expandRect = (parent, child) => {
  // adjust for parent offset
  child.top += parent.top;
  child.right += parent.left;
  child.bottom += parent.top;
  child.left += parent.left;

  if (child.bottom > parent.bottom) {
    parent.bottom = child.bottom;
  }

  if (child.right > parent.right) {
    parent.right = child.right;
  }
};

const calculateRectSize = rect => {
  rect.width = rect.right - rect.left;
  rect.height = rect.bottom - rect.top;
};

const isNumber = value => typeof value === 'number';

/**
 * Determines if position is at destination
 * @param position
 * @param destination
 * @param velocity
 * @param errorMargin
 * @returns {boolean}
 */
const thereYet = (position, destination, velocity, errorMargin = 0.001) => {
  return (
    Math.abs(position - destination) < errorMargin &&
    Math.abs(velocity) < errorMargin
  );
};

/**
 * Spring animation
 */
const spring =
  // default options
  ({ stiffness = 0.5, damping = 0.75, mass = 10 } = {}) =>
    // method definition
    {
      let target = null;
      let position = null;
      let velocity = 0;
      let resting = false;

      // updates spring state
      const interpolate = () => {
        // in rest, don't animate
        if (resting) {
          return;
        }

        // need at least a target or position to do springy things
        if (!(isNumber(target) && isNumber(position))) {
          resting = true;
          velocity = 0;
          return;
        }

        // calculate spring force
        const f = -(position - target) * stiffness;

        // update velocity by adding force based on mass
        velocity += f / mass;

        // update position by adding velocity
        position += velocity;

        // slow down based on amount of damping
        velocity *= damping;

        // we've arrived if we're near target and our velocity is near zero
        if (thereYet(position, target, velocity)) {
          position = target;
          velocity = 0;
          resting = true;

          // we done
          api.onupdate(position);
          api.oncomplete(position);
        } else {
          // progress update
          api.onupdate(position);
        }
      };

      /**
       * Set new target value
       * @param value
       */
      const setTarget = value => {
        // if currently has no position, set target and position to this value
        if (isNumber(value) && !isNumber(position)) {
          position = value;
        }

        // next target value will not be animated to
        if (target === null) {
          target = value;
          position = value;
        }

        // let start moving to target
        target = value;

        // already at target
        if (position === target || typeof target === 'undefined') {
          // now resting as target is current position, stop moving
          resting = true;
          velocity = 0;

          // done!
          api.onupdate(position);
          api.oncomplete(position);

          return;
        }

        resting = false;
      };

      // need 'api' to call onupdate callback
      const api = createObject({
        interpolate,
        target: {
          set: setTarget,
          get: () => target
        },
        resting: {
          get: () => resting
        },
        onupdate: value => {},
        oncomplete: value => {}
      });

      return api;
    };

const easeLinear = t => t;
const easeInOutQuad = t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t);

const tween =
  // default values
  ({ duration = 500, easing = easeInOutQuad, delay = 0 } = {}) =>
    // method definition
    {
      let start = null;
      let t;
      let p;
      let resting = true;
      let reverse = false;
      let target = null;

      const interpolate = ts => {
        if (resting || target === null) {
          return;
        }

        if (start === null) {
          start = ts;
        }

        if (ts - start < delay) {
          return;
        }

        t = ts - start - delay;

        if (t < duration) {
          p = t / duration;
          api.onupdate((t >= 0 ? easing(reverse ? 1 - p : p) : 0) * target);
        } else {
          t = 1;
          p = reverse ? 0 : 1;
          api.onupdate(p * target);
          api.oncomplete(p * target);
          resting = true;
        }
      };

      // need 'api' to call onupdate callback
      const api = createObject({
        interpolate,
        target: {
          get: () => (reverse ? 0 : target),
          set: value => {
            // is initial value
            if (target === null) {
              target = value;
              api.onupdate(value);
              api.oncomplete(value);
              return;
            }

            // want to tween to a smaller value and have a current value
            if (value < target) {
              target = 1;
              reverse = true;
            } else {
              // not tweening to a smaller value
              reverse = false;
              target = value;
            }

            // let's go!
            resting = false;
            start = null;
          }
        },
        resting: {
          get: () => resting
        },
        onupdate: value => {},
        oncomplete: value => {}
      });

      return api;
    };

const animator = {
  spring,
  tween
};

/*
 { type: 'spring', stiffness: .5, damping: .75, mass: 10 };
 { translation: { type: 'spring', ... }, ... }
 { translation: { x: { type: 'spring', ... } } }
*/
const createAnimator = (definition, category, property) => {
  // default is single definition
  // we check if transform is set, if so, we check if property is set
  const def =
    definition[category] && typeof definition[category][property] === 'object'
      ? definition[category][property]
      : definition[category] || definition;

  const type = typeof def === 'string' ? def : def.type;
  const props = typeof def === 'object' ? { ...def } : {};

  return animator[type] ? animator[type](props) : null;
};

const addGetSet = (keys, obj, props, overwrite = false) => {
  obj = Array.isArray(obj) ? obj : [obj];
  obj.forEach(o => {
    keys.forEach(key => {
      let name = key;
      let getter = () => props[key];
      let setter = value => (props[key] = value);

      if (typeof key === 'object') {
        name = key.key;
        getter = key.getter || getter;
        setter = key.setter || setter;
      }

      if (o[name] && !overwrite) {
        return;
      }

      o[name] = {
        get: getter,
        set: setter
      };
    });
  });
};

const isDefined = value => value != null;

// add to state,
// add getters and setters to internal and external api (if not set)
// setup animators

const animations = ({
  mixinConfig,
  viewProps,
  viewInternalAPI,
  viewExternalAPI,
  viewState
}) => {
  // initial properties
  const initialProps = { ...viewProps };

  // list of all active animations
  const animations = [];

  // setup animators
  forin(mixinConfig, (property, animation) => {
    const animator = createAnimator(animation);
    if (!animator) {
      return;
    }

    // when the animator updates, update the view state value
    animator.onupdate = value => {
      viewProps[property] = value;
    };

    // set animator target
    animator.target = initialProps[property];

    // when value is set, set the animator target value
    const prop = {
      key: property,
      setter: value => {
        // if already at target, we done!
        if (animator.target === value) {
          return;
        }

        animator.target = value;
      },
      getter: () => viewProps[property]
    };

    // add getters and setters
    addGetSet([prop], [viewInternalAPI, viewExternalAPI], viewProps, true);

    // add it to the list for easy updating from the _write method
    animations.push(animator);
  });

  // expose internal write api
  return {
    write: ts => {
      let resting = true;
      animations.forEach(animation => {
        if (!animation.resting) {
          resting = false;
        }
        animation.interpolate(ts);
      });
      return resting;
    },
    destroy: () => {}
  };
};

const addEvent = element => (type, fn) => {
  element.addEventListener(type, fn);
};

const removeEvent = element => (type, fn) => {
  element.removeEventListener(type, fn);
};

// mixin
const listeners = ({
  mixinConfig,
  viewProps,
  viewInternalAPI,
  viewExternalAPI,
  viewState,
  view
}) => {
  const events = [];

  const add = addEvent(view.element);
  const remove = removeEvent(view.element);

  viewExternalAPI.on = (type, fn) => {
    events.push({
      type,
      fn
    });
    add(type, fn);
  };

  viewExternalAPI.off = (type, fn) => {
    events.splice(
      events.findIndex(event => event.type === type && event.fn === fn),
      1
    );
    remove(type, fn);
  };

  return {
    write: () => {
      // not busy
      return true;
    },
    destroy: () => {
      events.forEach(event => {
        remove(event.type, event.fn);
      });
    }
  };
};

// add to external api and link to props

const apis = ({ mixinConfig, viewProps, viewExternalAPI }) => {
  addGetSet(mixinConfig, viewExternalAPI, viewProps);
};

// add to state,
// add getters and setters to internal and external api (if not set)
// set initial state based on props in viewProps
// apply as transforms each frame

const defaults = {
  opacity: 1,
  scaleX: 1,
  scaleY: 1,
  translateX: 0,
  translateY: 0,
  rotateX: 0,
  rotateY: 0,
  rotateZ: 0,
  originX: 0,
  originY: 0
};

const styles = ({
  mixinConfig,
  viewProps,
  viewInternalAPI,
  viewExternalAPI,
  view
}) => {
  // initial props
  const initialProps = { ...viewProps };

  // current props
  const currentProps = {};

  // we will add those properties to the external API and link them to the viewState
  addGetSet(mixinConfig, [viewInternalAPI, viewExternalAPI], viewProps);

  // override rect on internal and external rect getter so it takes in account transforms
  const getOffset = () => [
    viewProps['translateX'] || 0,
    viewProps['translateY'] || 0
  ];
  const getScale = () => [viewProps['scaleX'] || 0, viewProps['scaleY'] || 0];
  const getRect = () =>
    view.rect
      ? getViewRect(view.rect, view.childViews, getOffset(), getScale())
      : null;
  viewInternalAPI.rect = { get: getRect };
  viewExternalAPI.rect = { get: getRect };

  // apply view props
  mixinConfig.forEach(key => {
    viewProps[key] =
      typeof initialProps[key] === 'undefined'
        ? defaults[key]
        : initialProps[key];
  });

  // expose api
  return {
    write: () => {
      // see if props have changed
      if (!propsHaveChanged(currentProps, viewProps)) {
        return;
      }

      // moves element to correct position on screen
      applyStyles(view.element, viewProps);

      // store new transforms
      Object.assign(currentProps, { ...viewProps });

      // no longer busy
      return true;
    },
    destroy: () => {}
  };
};

const propsHaveChanged = (currentProps, newProps) => {
  // different amount of keys
  if (Object.keys(currentProps).length !== Object.keys(newProps).length) {
    return true;
  }

  // lets analyze the individual props
  for (const prop in newProps) {
    if (newProps[prop] !== currentProps[prop]) {
      return true;
    }
  }

  return false;
};

const applyStyles = (
  element,
  {
    opacity,
    perspective,
    translateX,
    translateY,
    scaleX,
    scaleY,
    rotateX,
    rotateY,
    rotateZ,
    originX,
    originY,
    width,
    height
  }
) => {
  let transforms = '';
  let styles = '';

  // handle transform origin
  if (isDefined(originX) || isDefined(originY)) {
    styles += `transform-origin: ${originX || 0}px ${originY || 0}px;`;
  }

  // transform order is relevant
  // 0. perspective
  if (isDefined(perspective)) {
    transforms += `perspective(${perspective}px) `;
  }

  // 1. translate
  if (isDefined(translateX) || isDefined(translateY)) {
    transforms += `translate3d(${translateX || 0}px, ${translateY || 0}px, 0) `;
  }

  // 2. scale
  if (isDefined(scaleX) || isDefined(scaleY)) {
    transforms += `scale3d(${isDefined(scaleX) ? scaleX : 1}, ${
      isDefined(scaleY) ? scaleY : 1
    }, 1) `;
  }

  // 3. rotate
  if (isDefined(rotateZ)) {
    transforms += `rotateZ(${rotateZ}rad) `;
  }

  if (isDefined(rotateX)) {
    transforms += `rotateX(${rotateX}rad) `;
  }

  if (isDefined(rotateY)) {
    transforms += `rotateY(${rotateY}rad) `;
  }

  // add transforms
  if (transforms.length) {
    styles += `transform:${transforms};`;
  }

  // add opacity
  if (isDefined(opacity)) {
    styles += `opacity:${opacity};`;

    // if we reach zero, we make the element inaccessible
    if (opacity === 0) {
      styles += `visibility:hidden;`;
    }

    // if we're below 100% opacity this element can't be clicked
    if (opacity < 1) {
      styles += `pointer-events:none;`;
    }
  }

  // add height
  if (isDefined(height)) {
    styles += `height:${height}px;`;
  }

  // add width
  if (isDefined(width)) {
    styles += `width:${width}px;`;
  }

  // apply styles
  const elementCurrentStyle = element.elementCurrentStyle || '';

  // if new styles does not match current styles, lets update!
  if (
    styles.length !== elementCurrentStyle.length ||
    styles !== elementCurrentStyle
  ) {
    element.setAttribute('style', styles);
    // store current styles so we can compare them to new styles later on
    // _not_ getting the style attribute is faster
    element.elementCurrentStyle = styles;
  }
};

const Mixins = {
  styles,
  listeners,
  animations,
  apis
};

const updateRect = (rect = {}, element = {}, style = {}) => {
  if (!element.layoutCalculated) {
    rect.paddingTop = parseInt(style.paddingTop, 10) || 0;
    rect.marginTop = parseInt(style.marginTop, 10) || 0;
    rect.marginRight = parseInt(style.marginRight, 10) || 0;
    rect.marginBottom = parseInt(style.marginBottom, 10) || 0;
    rect.marginLeft = parseInt(style.marginLeft, 10) || 0;
    element.layoutCalculated = true;
  }

  rect.left = element.offsetLeft || 0;
  rect.top = element.offsetTop || 0;
  rect.width = element.offsetWidth || 0;
  rect.height = element.offsetHeight || 0;

  rect.right = rect.left + rect.width;
  rect.bottom = rect.top + rect.height;

  rect.scrollTop = element.scrollTop;

  rect.hidden = element.offsetParent === null;

  return rect;
};

const createView =
  // default view definition
  ({
    // element definition
    tag = 'div',
    name = null,
    attributes = {},

    // view interaction
    read = () => {},
    write = () => {},
    create = () => {},
    destroy = () => {},

    // hooks
    filterFrameActionsForChild = (child, actions) => actions,
    didCreateView = () => {},
    didWriteView = () => {},

    // rect related
    ignoreRect = false,
    ignoreRectUpdate = false,

    // mixins
    mixins = []
  } = {}) => (
    // each view requires reference to store
    store,
    // specific properties for this view
    props = {}
  ) => {
    // root element should not be changed
    const element = createElement(tag, `filepond--${name}`, attributes);

    // style reference should also not be changed
    const style = window.getComputedStyle(element, null);

    // element rectangle
    const rect = updateRect();
    let frameRect = null;

    // rest state
    let isResting = false;

    // pretty self explanatory
    const childViews = [];

    // loaded mixins
    const activeMixins = [];

    // references to created children
    const ref = {};

    // state used for each instance
    const state = {};

    // list of writers that will be called to update this view
    const writers = [
      write // default writer
    ];

    const readers = [
      read // default reader
    ];

    const destroyers = [
      destroy // default destroy
    ];

    // core view methods
    const getElement = () => element;
    const getChildViews = () => childViews.concat();
    const getReference = () => ref;
    const createChildView = store => (view, props) => view(store, props);
    const getRect = () => {
      if (frameRect) {
        return frameRect;
      }
      frameRect = getViewRect(rect, childViews, [0, 0], [1, 1]);
      return frameRect;
    };
    const getStyle = () => style;

    /**
     * Read data from DOM
     * @private
     */
    const _read = () => {
      frameRect = null;

      // read child views
      childViews.forEach(child => child._read());

      const shouldUpdate = !(ignoreRectUpdate && rect.width && rect.height);
      if (shouldUpdate) {
        updateRect(rect, element, style);
      }

      // readers
      const api = { root: internalAPI, props, rect };
      readers.forEach(reader => reader(api));
    };

    /**
     * Write data to DOM
     * @private
     */
    const _write = (ts, frameActions, shouldOptimize) => {
      // if no actions, we assume that the view is resting
      let resting = frameActions.length === 0;

      // writers
      writers.forEach(writer => {
        const writerResting = writer({
          props,
          root: internalAPI,
          actions: frameActions,
          timestamp: ts,
          shouldOptimize
        });
        if (writerResting === false) {
          resting = false;
        }
      });

      // run mixins
      activeMixins.forEach(mixin => {
        // if one of the mixins is still busy after write operation, we are not resting
        const mixinResting = mixin.write(ts);
        if (mixinResting === false) {
          resting = false;
        }
      });

      // updates child views that are currently attached to the DOM
      childViews
        .filter(child => !!child.element.parentNode)
        .forEach(child => {
          // if a child view is not resting, we are not resting
          const childResting = child._write(
            ts,
            filterFrameActionsForChild(child, frameActions),
            shouldOptimize
          );
          if (!childResting) {
            resting = false;
          }
        });

      // append new elements to DOM and update those
      childViews
        //.filter(child => !child.element.parentNode)
        .forEach((child, index) => {
          // skip
          if (child.element.parentNode) {
            return;
          }

          // append to DOM
          internalAPI.appendChild(child.element, index);

          // call read (need to know the size of these elements)
          child._read();

          // re-call write
          child._write(
            ts,
            filterFrameActionsForChild(child, frameActions),
            shouldOptimize
          );

          // we just added somthing to the dom, no rest
          resting = false;
        });

      // update resting state
      isResting = resting;

      didWriteView({
        props,
        root: internalAPI,
        actions: frameActions,
        timestamp: ts
      });

      // let parent know if we are resting
      return resting;
    };

    const _destroy = () => {
      activeMixins.forEach(mixin => mixin.destroy());
      destroyers.forEach(destroyer => {
        destroyer({ root: internalAPI, props });
      });
      childViews.forEach(child => child._destroy());
    };

    // sharedAPI
    const sharedAPIDefinition = {
      element: {
        get: getElement
      },
      style: {
        get: getStyle
      },
      childViews: {
        get: getChildViews
      }
    };

    // private API definition
    const internalAPIDefinition = {
      ...sharedAPIDefinition,
      rect: {
        get: getRect
      },

      // access to custom children references
      ref: {
        get: getReference
      },

      // dom modifiers
      is: needle => name === needle,
      appendChild: appendChild(element),
      createChildView: createChildView(store),
      linkView: view => {
        childViews.push(view);
        return view;
      },
      unlinkView: view => {
        childViews.splice(childViews.indexOf(view), 1);
      },
      appendChildView: appendChildView(element, childViews),
      removeChildView: removeChildView(element, childViews),
      registerWriter: writer => writers.push(writer),
      registerReader: reader => readers.push(reader),
      registerDestroyer: destroyer => destroyers.push(destroyer),
      invalidateLayout: () => (element.layoutCalculated = false),

      // access to data store
      dispatch: store.dispatch,
      query: store.query
    };

    // public view API methods
    const externalAPIDefinition = {
      element: {
        get: getElement
      },
      childViews: {
        get: getChildViews
      },
      rect: {
        get: getRect
      },
      resting: {
        get: () => isResting
      },
      isRectIgnored: () => ignoreRect,
      _read,
      _write,
      _destroy
    };

    // mixin API methods
    const mixinAPIDefinition = {
      ...sharedAPIDefinition,
      rect: {
        get: () => rect
      }
    };

    // add mixin functionality
    Object.keys(mixins)
      .sort((a, b) => {
        // move styles to the back of the mixin list (so adjustments of other mixins are applied to the props correctly)
        if (a === 'styles') {
          return 1;
        } else if (b === 'styles') {
          return -1;
        }
        return 0;
      })
      .forEach(key => {
        const mixinAPI = Mixins[key]({
          mixinConfig: mixins[key],
          viewProps: props,
          viewState: state,
          viewInternalAPI: internalAPIDefinition,
          viewExternalAPI: externalAPIDefinition,
          view: createObject(mixinAPIDefinition)
        });

        if (mixinAPI) {
          activeMixins.push(mixinAPI);
        }
      });

    // construct private api
    const internalAPI = createObject(internalAPIDefinition);

    // create the view
    create({
      root: internalAPI,
      props
    });

    // append created child views to root node
    const childCount = element.children.length; // need to know the current child count so appending happens in correct order
    childViews.forEach((child, index) => {
      internalAPI.appendChild(child.element, childCount + index);
    });

    // call did create
    didCreateView(internalAPI);

    // expose public api
    return createObject(externalAPIDefinition, props);
  };

const createPainter = (read, write, fps = 60) => {
  const name = '__framePainter';

  // set global painter
  if (window[name]) {
    window[name].readers.push(read);
    window[name].writers.push(write);
    return;
  }

  window[name] = {
    readers: [read],
    writers: [write]
  };

  const painter = window[name];

  const interval = 1000 / fps;
  let last = null;
  let 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 => {
  // is in bytes
  if (isInt(value)) {
    return value;
  }

  // is natural file size
  let naturalFileSize = toString(value).trim();

  // if is value in megabytes
  if (/MB$/i.test(naturalFileSize)) {
    naturalFileSize = naturalFileSize.replace(/MB$i/, '').trim();
    return toInt(naturalFileSize) * 1000 * 1000;
  }

  // if is value in kilobytes
  if (/KB/i.test(naturalFileSize)) {
    naturalFileSize = naturalFileSize.replace(/KB$i/, '').trim();
    return toInt(naturalFileSize) * 1000;
  }

  return toInt(naturalFileSize);
};

const isFunction = value => typeof value === 'function';

const toFunctionReference = string => {
  let ref = self;
  let levels = string.split('.');
  let level = null;
  while ((level = levels.shift())) {
    ref = ref[level];
    if (!ref) {
      return null;
    }
  }
  return ref;
};

const methods = {
  process: 'POST',
  revert: 'DELETE',
  fetch: 'GET',
  restore: 'GET',
  load: 'GET'
};

const createServerAPI = outline => {
  const api = {};

  api.url = isString(outline) ? outline : outline.url || '';
  api.timeout = outline.timeout ? parseInt(outline.timeout, 10) : 0;
  api.headers = outline.headers ? outline.headers : {};

  forin(methods, key => {
    api[key] = createAction(
      key,
      outline[key],
      methods[key],
      api.timeout,
      api.headers
    );
  });

  // 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' ? `?${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)
    .substr(2, 9);

const arrayRemove = (arr, index) => arr.splice(index, 1);

const on = () => {
  const listeners = [];
  const off = (event, cb) => {
    arrayRemove(
      listeners,
      listeners.findIndex(
        listener => listener.event === event && (listener.cb === cb || !cb)
      )
    );
  };
  return {
    fire: (event, ...args) => {
      listeners
        .filter(listener => listener.event === event)
        .map(listener => listener.cb)
        .forEach(cb => {
          setTimeout(() => {
            cb(...args);
          }, 0);
        });
    },
    on: (event, cb) => {
      listeners.push({ event, cb });
    },
    onOnce: (event, cb) => {
      listeners.push({
        event,
        cb: (...args) => {
          off(event, cb);
          cb(...args);
        }
      });
    },
    off
  };
};

const copyObjectPropertiesToObject = (src, target, excluded) => {
  Object.getOwnPropertyNames(src)
    .filter(property => !excluded.includes(property))
    .forEach(key =>
      Object.defineProperty(
        target,
        key,
        Object.getOwnPropertyDescriptor(src, key)
      )
    );
};

const PRIVATE = [
  'fire',
  'process',
  'revert',
  'load',
  'on',
  'off',
  'onOnce',
  'retryLoad',
  'extend',
  'archive',
  'archived',
  'release',
  'released',
  'requestProcessing',
  'freeze'
];

const createItemAPI = item => {
  const api = {};
  copyObjectPropertiesToObject(item, api, PRIVATE);
  return api;
};

const removeReleasedItems = items => {
  items.forEach((item, index) => {
    if (item.released) {
      arrayRemove(items, index);
    }
  });
};

const ItemStatus = {
  INIT: 1,
  IDLE: 2,
  PROCESSING_QUEUED: 9,
  PROCESSING: 3,
  PROCESSING_COMPLETE: 5,
  PROCESSING_ERROR: 6,
  PROCESSING_REVERT_ERROR: 10,
  LOADING: 7,
  LOAD_ERROR: 8
};

const 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/*"

  // Feature toggles
  allowDrop: [true, Type.BOOLEAN], // Allow dropping of files
  allowBrowse: [true, Type.BOOLEAN], // Allow browsing the file system
  allowPaste: [true, Type.BOOLEAN], // Allow pasting files
  allowMultiple: [false, Type.BOOLEAN], // Allow multiple files (disabled by default, as multiple attribute is also required on input to allow multiple)
  allowReplace: [true, Type.BOOLEAN], // Allow dropping a file on other file to replace it (only works when multiple is set to false)
  allowRevert: [true, Type.BOOLEAN], // Allows user to revert file upload

  // Revert mode
  forceRevert: [false, Type.BOOLEAN], // Set to 'force' to require the file to be reverted before removal

  // Input requirements
  maxFiles: [null, Type.INT], // Max number of files
  checkValidity: [false, Type.BOOLEAN], // Enables custom validity messages

  // Where to put file
  itemInsertLocationFreedom: [true, Type.BOOLEAN], // Set to false to always add items to begin or end of list
  itemInsertLocation: ['before', Type.STRING], // Default index in list to add items that have been dropped at the top of the list
  itemInsertInterval: [75, Type.INT],

  // Drag 'n Drop related
  dropOnPage: [false, Type.BOOLEAN], // Allow dropping of files anywhere on page (prevents browser from opening file if dropped outside of Up)
  dropOnElement: [true, Type.BOOLEAN], // Drop needs to happen on element (set to false to also load drops outside of Up)
  dropValidation: [false, Type.BOOLEAN], // Enable or disable validating files on drop
  ignoredFiles: [['.ds_store', 'thumbs.db', 'desktop.ini'], Type.ARRAY],

  // Upload related
  instantUpload: [true, Type.BOOLEAN], // Should upload files immidiately on drop
  maxParallelUploads: [2, Type.INT], // Maximum files to upload in parallel

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

  // Labels and status messages
  labelDecimalSeparator: [getDecimalSeparator(), Type.STRING], // Default is locale separator
  labelThousandsSeparator: [getThousandsSeparator(), Type.STRING], // Default is locale separator

  labelIdle: [
    'Drag & Drop your files or <span class="filepond--label-action">Browse</span>',
    Type.STRING
  ],
  labelInvalidField: ['Field contains invalid files', Type.STRING],
  labelFileWaitingForSize: ['Waiting for size', Type.STRING],
  labelFileSizeNotAvailable: ['Size not available', Type.STRING],
  labelFileCountSingular: ['file in list', Type.STRING],
  labelFileCountPlural: ['files in list', Type.STRING],
  labelFileLoading: ['Loading', Type.STRING],
  labelFileAdded: ['Added', Type.STRING], // assistive only
  labelFileLoadError: ['Error during load', Type.STRING],
  labelFileRemoved: ['Removed', Type.STRING], // assistive only
  labelFileRemoveError: ['Error during remove', Type.STRING],
  labelFileProcessing: ['Uploading', Type.STRING],
  labelFileProcessingComplete: ['Upload complete', Type.STRING],
  labelFileProcessingAborted: ['Upload cancelled', Type.STRING],
  labelFileProcessingError: ['Error during upload', Type.STRING],
  labelFileProcessingRevertError: ['Error during revert', Type.STRING],

  labelTapToCancel: ['tap to cancel', Type.STRING],
  labelTapToRetry: ['tap to retry', Type.STRING],
  labelTapToUndo: ['tap to undo', Type.STRING],

  labelButtonRemoveItem: ['Remove', Type.STRING],
  labelButtonAbortItemLoad: ['Abort', Type.STRING],
  labelButtonRetryItemLoad: ['Retry', Type.STRING],
  labelButtonAbortItemProcessing: ['Cancel', Type.STRING],
  labelButtonUndoItemProcessing: ['Undo', Type.STRING],
  labelButtonRetryItemProcessing: ['Retry', Type.STRING],
  labelButtonProcessItem: ['Upload', Type.STRING],

  // make sure width and height plus viewpox are even numbers so icons are nicely centered
  iconRemove: [
    '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M11.586 13l-2.293 2.293a1 1 0 0 0 1.414 1.414L13 14.414l2.293 2.293a1 1 0 0 0 1.414-1.414L14.414 13l2.293-2.293a1 1 0 0 0-1.414-1.414L13 11.586l-2.293-2.293a1 1 0 0 0-1.414 1.414L11.586 13z" fill="currentColor" fill-rule="nonzero"/></svg>',
    Type.STRING
  ],
  iconProcess: [
    '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M14 10.414v3.585a1 1 0 0 1-2 0v-3.585l-1.293 1.293a1 1 0 0 1-1.414-1.415l3-3a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1-1.414 1.415L14 10.414zM9 18a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2H9z" fill="currentColor" fill-rule="evenodd"/></svg>',
    Type.STRING
  ],
  iconRetry: [
    '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M10.81 9.185l-.038.02A4.997 4.997 0 0 0 8 13.683a5 5 0 0 0 5 5 5 5 0 0 0 5-5 1 1 0 0 1 2 0A7 7 0 1 1 9.722 7.496l-.842-.21a.999.999 0 1 1 .484-1.94l3.23.806c.535.133.86.675.73 1.21l-.804 3.233a.997.997 0 0 1-1.21.73.997.997 0 0 1-.73-1.21l.23-.928v-.002z" fill="currentColor" fill-rule="nonzero"/></svg>',
    Type.STRING
  ],
  iconUndo: [
    '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M9.185 10.81l.02-.038A4.997 4.997 0 0 1 13.683 8a5 5 0 0 1 5 5 5 5 0 0 1-5 5 1 1 0 0 0 0 2A7 7 0 1 0 7.496 9.722l-.21-.842a.999.999 0 1 0-1.94.484l.806 3.23c.133.535.675.86 1.21.73l3.233-.803a.997.997 0 0 0 .73-1.21.997.997 0 0 0-1.21-.73l-.928.23-.002-.001z" fill="currentColor" fill-rule="nonzero"/></svg>',
    Type.STRING
  ],
  iconDone: [
    '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><path d="M18.293 9.293a1 1 0 0 1 1.414 1.414l-7.002 7a1 1 0 0 1-1.414 0l-3.998-4a1 1 0 1 1 1.414-1.414L12 15.586l6.294-6.293z" fill="currentColor" fill-rule="nonzero"/></svg>',
    Type.STRING
  ],

  // event handlers
  oninit: [null, Type.FUNCTION],
  onwarning: [null, Type.FUNCTION],
  onerror: [null, Type.FUNCTION],
  onactivatefile: [null, Type.FUNCTION],
  onaddfilestart: [null, Type.FUNCTION],
  onaddfileprogress: [null, Type.FUNCTION],
  onaddfile: [null, Type.FUNCTION],
  onprocessfilestart: [null, Type.FUNCTION],
  onprocessfileprogress: [null, Type.FUNCTION],
  onprocessfileabort: [null, Type.FUNCTION],
  onprocessfilerevert: [null, Type.FUNCTION],
  onprocessfile: [null, Type.FUNCTION],
  onprocessfiles: [null, Type.FUNCTION],
  onremovefile: [null, Type.FUNCTION],
  onpreparefile: [null, Type.FUNCTION],
  onupdatefiles: [null, Type.FUNCTION],

  // hooks
  beforeDropFile: [null, Type.FUNCTION],
  beforeAddFile: [null, Type.FUNCTION],
  beforeRemoveFile: [null, Type.FUNCTION],

  // styles
  stylePanelLayout: [null, Type.STRING], // null 'integrated', 'compact', 'circle'
  stylePanelAspectRatio: [null, Type.STRING], // null or '3:2' or 1
  styleItemPanelAspectRatio: [null, Type.STRING],
  styleButtonRemoveItemPosition: ['left', Type.STRING],
  styleButtonProcessItemPosition: ['right', Type.STRING],
  styleLoadIndicatorPosition: ['right', Type.STRING],
  styleProgressIndicatorPosition: ['right', Type.STRING],

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

const getItemByQuery = (items, query) => {
  // just return first index
  if (isEmpty(query)) {
    return items[0] || null;
  }

  // query is index
  if (isInt(query)) {
    return items[query] || null;
  }

  // if query is item, get the id
  if (typeof query === 'object') {
    query = query.id;
  }

  // assume query is a string and return item by id
  return items.find(item => item.id === query) || null;
};

const getNumericAspectRatioFromString = aspectRatio => {
  if (isEmpty(aspectRatio)) {
    return aspectRatio;
  }
  if (/:/.test(aspectRatio)) {
    const parts = aspectRatio.split(':');
    return parts[1] / parts[0];
  }
  return parseFloat(aspectRatio);
};

const getActiveItems = items => items.filter(item => !item.archived);

const Status = {
  EMPTY: 0,
  IDLE: 1, // waiting
  ERROR: 2, // a file is in error state
  BUSY: 3, // busy processing or loading
  READY: 4 // all files uploaded
};

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 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: query => getActiveItems(state.items),

  GET_ITEMS: query => 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,

  IS_ASYNC: () =>
    isObject(state.options.server) &&
    (isObject(state.options.server.process) ||
      isFunction(state.options.server.process))
});

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();

  // 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', response instanceof Blob ? response : response.body);
      },
      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 => {
    xhr.setRequestHeader(key, options.headers[key]);
  });

  // 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 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, 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,
          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;
  };
};

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

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

  // internal handler
  return (file, metadata, load, error, progress, abort) => {
    // set onload hanlder
    const ondata = action.ondata || (fd => fd);
    const onload = action.onload || (res => res);
    const onerror = action.onerror || (res => null);

    // no file received
    if (!file) return;

    // 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), apiUrl + action.url, action);
    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;
  };
};

/*
 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) {
      cb(1);
      return;
    }

    cb(progress);

    timeout = setTimeout(tick, delay);
  };

  tick();

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

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

  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
      getRandomNumber(750, 1500)
    );

    // 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)
      // 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 (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);
      }
    );
  };

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

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

    // abort actual request
    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 = () =>
    state.progress ? Math.min(state.progress, state.perceivedProgress) : null;
  const getDuration = () => Math.min(state.duration, state.perceivedDuration);

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

  return api;
};

const getFilenameWithoutExtension = name =>
  name.substr(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,

    // 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;

    // file stub is already there
    if (state.file) {
      fire('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) => {
    // 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.serverFileReference = serverFileReference;
    });

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

      // need this id to be able to rever the upload
      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 = result => {};

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

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

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

  const abortProcessing = () =>
    new Promise(resolve => {
      if (!state.activeProcessor) {
        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) => {
      // cannot revert without a server id for this process
      if (state.serverFileReference === null) {
        resolve();
        return;
      }

      // revert the upload (fire and forget)
      revertFileUpload(
        state.serverFileReference,
        () => {
          // reset file server id as now it's no available on the server
          state.serverFileReference = 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;

    // don't fire update
    if (silent) return;

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

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

  const api = {
    id: { get: () => id },
    origin: { get: () => origin },
    serverId: { get: () => state.serverFileReference },
    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 },

    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 fetchLocal = (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 = () => {}
} = {}) => {
  const item = getItemByQuery(state.items, query);
  if (!item) {
    failure({
      error: createResponse('error', 0, 'Item not found'),
      file: null
    });
    return;
  }
  itemHandler(item, success, failure);
};

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 });
      }
    });

    // 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 }) => {
    // 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 }).then(
          shouldPrepareOutput => {
            if (!shouldPrepareOutput) {
              return;
            }
            dispatch(
              'REQUEST_PREPARE_OUTPUT',
              {
                query: id,
                item,
                ready: file => {
                  dispatch('DID_PREPARE_OUTPUT', { id, file });
                }
              },
              true
            );
          }
        );

        return;
      }

      // 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 immidiately
      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);
  },

  SORT: ({ compare }) => {
    sortItems(state, compare);
  },

  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('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 => {
            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,
                  ready: 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(() => {
          handleAdd(false);
        });
    });

    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
      });
    });

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

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

    // 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
            isString(source) && isExternalURL(source)
            ? createFetchFunction(url, fetch) // remote url
            : fetchLocal // local 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, ready }) => {
    // don't handle archived items, an item could have been archived (load aborted) while waiting to be prepared
    if (item.archived) return;

    // 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;

          // we done!
          ready(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
      });
      return;
    }

    // id we are allowed to upload the file immidiately, 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_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 process = () => {
          setTimeout(() => {
            dispatch('REQUEST_ITEM_PROCESSING', {
              query: item,
              success,
              failure
            });
          }, 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();

      // 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
    item.process(
      createFileProcessor(
        createProcessorFunction(
          state.options.server.url,
          state.options.server.process,
          state.options.name
        )
      ),
      // 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) => {
    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)
    ) {
      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 {
      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 immidiately
    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 }) => {
    forin(options, (key, value) => {
      dispatch(`SET_${fromCamels(key, '_').toUpperCase()}`, { value });
    });
  }
});

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 = '.') => {
  // nope, no negative byte sizes
  bytes = Math.round(Math.abs(bytes));

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

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

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

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

const KB = 1000;
const MB = 1000000;
const GB = 1000000000;

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))
  );
  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))) {
    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, props }) => {
  // 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 => {
  const buttonRect = root.ref.buttonRemoveItem.rect.element;
  return buttonRect.hidden ? null : buttonRect.width + buttonRect.left;
};

// 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 }) => {
  const { id } = props;

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

  // 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');

  // enabled buttons array
  const enabledButtons = isAsync
    ? ButtonKeys.concat()
    : ButtonKeys.filter(key => !/Process/.test(key));

  // remove last button (revert) if not allowed
  if (isAsync && !allowRevert) {
    enabledButtons.splice(-1, 1);
    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 };
  }

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

  // create the button views
  forin(Buttons, (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);
    }

    // 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();
      root.dispatch(definition.action, { query: id });
    });

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

  // 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 })
  );

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

  // 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 })
  );

  // create data container
  const dataContainer = createElement$1('input');
  dataContainer.type = 'hidden';
  dataContainer.name = root.query('GET_NAME');
  dataContainer.disabled = root.query('GET_DISABLED');
  root.ref.data = dataContainer;
  root.appendChild(dataContainer);
};

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

/**
 * Data storage
 */
const didLoadItem = ({ root, action, props }) => {
  root.ref.data.value = action.serverFileReference;

  // 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 didRemoveItem = ({ root }) => {
  root.ref.data.removeAttribute('value');
};

const didCompleteItemProcessing$1 = ({ root, action }) => {
  root.ref.data.value = action.serverFileReference;
};

const didRevertItemProcessing = ({ root }) => {
  root.ref.data.removeAttribute('value');
};

const fileWrapper = createView({
  create: create$5,
  ignoreRect: true,
  write: createRoute({
    DID_SET_DISABLED: didSetDisabled,
    DID_LOAD_ITEM: didLoadItem,
    DID_REMOVE_ITEM: didRemoveItem,
    DID_COMPLETE_ITEM_PROCESSING: didCompleteItemProcessing$1,
    DID_REVERT_ITEM_PROCESSING: didRevertItemProcessing
  }),
  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',
  write: write$3,
  create: create$6,
  ignoreRect: true,
  mixins: {
    apis: ['height', 'scalable']
  }
});

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

const ITEM_SCALE_SPRING = 'spring';

/**
 * Creates the file view
 */
const create$7 = ({ root, props }) => {
  // select
  root.ref.handleClick = () =>
    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;
};

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',
  DID_REQUEST_ITEM_PROCESSING: 'busy',
  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'
};

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

const write$4 = ({ root, actions, props, shouldOptimize }) => {
  // 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'],
    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 }
    }
  }
});

const getItemIndexByPosition = (view, 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 = Math.round(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 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) => {
  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;
};

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

/**
 * 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;

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

  // 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);

  // 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 = Math.round(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}`);

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

    // extract files
    const files = Array.from(root.element.files);

    // 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 }) => {
  attrToggle(
    root.element,
    'accept',
    !!action.value,
    action.value ? action.value.join(',') : ''
  );
};

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

const toggleDisabled = ({ root, action }) => {
  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_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 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 apps = ['rtf', 'pdf', 'json'];
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] || null;
};

const requestDataTransferItems = dataTransfer =>
  new Promise((resolve, reject) => {
    // try to get links from transfer, if found we'll exit immidiately (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(returendFileGroups => {
      // flatten groups
      const files = [];
      returendFileGroups.forEach(group => {
        files.push.apply(files, group);
      });

      // done (filter out empty files)!
      resolve(files.filter(file => file));
    });
  });

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);
      return;
    }

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

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

    // the total entries to read
    let totalFilesFound = 0;

    // the recursive function
    const readEntries = dirEntry => {
      const directoryReader = dirEntry.createReader();
      directoryReader.readEntries(entries => {
        entries.forEach(entry => {
          // recursively read more directories
          if (entry.isDirectory) {
            readEntries(entry);
          } else {
            // read as file
            totalFilesFound++;
            entry.file(file => {
              files.push(correctMissingFileType(file));

              if (totalFilesFound === files.length) {
                resolve(files);
              }
            });
          }
        });
      });
    };

    // go!
    readEntries(entry);
  });

const correctMissingFileType = file => {
  if (file.type.length) return file;
  const date = file.lastModifiedDate;
  const name = file.name;
  file = file.slice(
    0,
    file.size,
    guesstimateMimeType(getExtensionFromFilename(file.name))
  );
  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;

      const allowsTransfer = allowdrop(items);

      // no transfer for this client
      if (!allowsTransfer) {
        onexit(eventPosition(e));
        return;
      }

      // if we're filtering on element we need to be over the element to drop
      if (filterElement && !isEventTarget(e, element)) {
        return;
      }

      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 => {
  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$c = ({ 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$c,
  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 create$d = ({ 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(' ').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 })
  );

  // 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 = [];
};

const write$8 = ({ 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.substr(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);
  }
};

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 children = itemList.childViews;

  // no children, done!
  if (children.length === 0) return { visual, bounds };

  const horizontalSpace = itemList.rect.element.width;
  const dragIndex = getItemIndexByPosition(
    itemList,
    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 = Math.round(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) {
    return true;
  }

  // limit max items to one if not allowed to drop multiple items
  maxItems = allowMultiple ? maxItems : allowReplace ? maxItems : 1;

  // 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, position) => {
  const itemList = list.childViews[0];
  return getItemIndexByPosition(itemList, {
    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 => {
        // these files don't fit so stop here
        if (exceedsMaxFiles(root, items)) return false;

        // 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) => {
      root.dispatch('ADD_ITEMS', {
        items,
        index: getDragIndex(root.ref.list, 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 => {
          // these files don't fit so stop here
          if (exceedsMaxFiles(root, items)) return false;

          // add items!
          root.dispatch('ADD_ITEMS', {
            items,
            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 => {
      root.dispatch('ADD_ITEMS', {
        items,
        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;
    }
  }
});

const root = createView({
  name: 'root',
  read: ({ root }) => {
    if (root.ref.measure) {
      root.ref.measureHeight = root.ref.measure.offsetHeight;
    }
  },
  create: create$d,
  write: write$8,
  destroy: ({ root }) => {
    if (root.ref.paster) {
      root.ref.paster.destroy();
    }
    if (root.ref.hopper) {
      root.ref.hopper.destroy();
    }
  },
  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;
    }

    return event;
  };

  const eventRoutes = {
    DID_DESTROY: createEvent('destroy'),

    DID_INIT: createEvent('init'),

    DID_THROW_MAX_FILES: createEvent('warning'),

    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')
  };

  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.forEach(action => {
      if (!eventRoutes[action.type]) {
        return;
      }
      const routes = eventRoutes[action.type];
      (Array.isArray(routes) ? routes : [routes]).forEach(route => {
        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 addFile = (source, options = {}) =>
    new Promise((resolve, reject) => {
      addFiles([{ source, options }], { index: options.index })
        .then(items => resolve(items && items[0]))
        .catch(reject);
    });

  const removeFile = query => {
    // request item removal
    store.dispatch('REMOVE_ITEM', { 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 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;
    const files = getFiles();

    if (!queries.length) {
      return Promise.all(files.map(removeFile));
    }

    // 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(removeFile);
  };

  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,

    /**
     * Removes a file by its name
     * @param query { string, number, null  }
     */
    removeFile,

    /**
     * Returns all files (wrapped in public api)
     */
    getFiles,

    /**
     * Starts uploading all files
     */
    processFiles,

    /**
     * Clears all files from the files list
     */
    removeFiles,

    /**
     * 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',

    // 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 isBrowser = () =>
  typeof window !== 'undefined' && typeof window.document !== 'undefined';

const supported = (() => {
  // Runs immidiately 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();

  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$e = 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$e,
          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$e = (...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$e(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$e as create,
  destroy,
  find,
  getOptions$1 as getOptions,
  parse,
  registerPlugin,
  setOptions$1 as setOptions,
  supported
};