<commit_before>import PropTypes from 'prop-types';
import React, { Component } from 'react';
import warning from 'warning';
import { createCountUpInstance } from './common';

class CountUp extends Component {
  static propTypes = {
    decimal: PropTypes.string,
    decimals: PropTypes.number,
    delay: PropTypes.number,
    easingFn: PropTypes.func,
    end: PropTypes.number.isRequired,
    formattingFn: PropTypes.func,
    onEnd: PropTypes.func,
    onStart: PropTypes.func,
    prefix: PropTypes.string,
    redraw: PropTypes.bool,
    separator: PropTypes.string,
    start: PropTypes.number,
    startOnMount: PropTypes.bool,
    suffix: PropTypes.string,
    style: PropTypes.object,
    useEasing: PropTypes.bool,
    preserveValue: PropTypes.bool,
  };

  static defaultProps = {
    decimal: '.',
    decimals: 0,
    delay: null,
    duration: null,
    easingFn: null,
    formattingFn: null,
    onEnd: () => {},
    onPauseResume: () => {},
    onReset: () => {},
    onStart: () => {},
    onUpdate: () => {},
    prefix: '',
    redraw: false,
    separator: '',
    start: 0,
    startOnMount: true,
    suffix: '',
    style: undefined,
    useEasing: true,
    preserveValue: false,
  };

  componentDidMount() {
    const { children, delay } = this.props;
    this.instance = this.createInstance();

    // Don't invoke start if component is used as a render prop
    if (typeof children === 'function' && delay !== 0) return;

    // Otherwise just start immediately
    this.start();
  }

  checkProps = (updatedProps) => {
    const {
      start,
      suffix,
      prefix,
      redraw,
      duration,
      separator,
      decimals,
      decimal,
      className,
    } = this.props;

    const hasPropsChanged =
      duration !== updatedProps.duration ||
      start !== updatedProps.start ||
      suffix !== updatedProps.suffix ||
      prefix !== updatedProps.prefix ||
      separator !== updatedProps.separator ||
      decimals !== updatedProps.decimals ||
      decimal !== updatedProps.decimal ||
      className !== updatedProps.className;

    return hasPropsChanged || redraw;
  };

  shouldComponentUpdate(nextProps) {
    const { end } = this.props;
    return this.checkProps(nextProps) || end !== nextProps.end;
  }

  componentDidUpdate(prevProps) {
    // If duration, suffix, prefix, separator or start has changed
    // there's no way to update the values.
    // So we need to re-create the CountUp instance in order to
    // restart it.
    const { end, preserveValue } = this.props;

    if (this.checkProps(prevProps)) {
      this.instance.reset();
      this.instance = this.createInstance();
      this.start();
    }

    // Only end value has changed, so reset and and re-animate with the updated
    // end value.
    if (end !== prevProps.end) {
      if (!preserveValue) {
        this.instance.reset();
      }
      this.instance.update(end);
    }
  }

  componentWillUnmount() {
    if (this.timeoutId) {
      clearTimeout(this.timeoutId);
    }
    this.instance.reset();
  }

  createInstance = () => {
    if (typeof this.props.children === 'function') {
      // Warn when user didn't use containerRef at all
      warning(
        this.containerRef.current &&
          (this.containerRef.current instanceof HTMLElement ||
            this.containerRef.current instanceof SVGTextElement ||
            this.containerRef.current instanceof SVGTSpanElement),
        `Couldn't find attached element to hook the CountUp instance into! Try to attach "containerRef" from the render prop to a an HTMLElement, eg. <span ref={containerRef} />.`,
      );
    }
    return createCountUpInstance(this.containerRef.current, this.props);
  };

  pauseResume = () => {
    const { reset, restart: start, update } = this;
    const { onPauseResume } = this.props;

    this.instance.pauseResume();

    onPauseResume({ reset, start, update });
  };

  reset = () => {
    const { pauseResume, restart: start, update } = this;
    const { onReset } = this.props;

    this.instance.reset();

    onReset({ pauseResume, start, update });
  };

  restart = () => {
    this.reset();
    this.start();
  };

  start = () => {
    const { pauseResume, reset, restart: start, update } = this;
    const { delay, onEnd, onStart } = this.props;
    const run = () =>
      this.instance.start(() => onEnd({ pauseResume, reset, start, update }));

    // Delay start if delay prop is properly set
    if (delay > 0) {
      this.timeoutId = setTimeout(run, delay * 1000);
    } else {
      run();
    }

    onStart({ pauseResume, reset, update });
  };

  update = (newEnd) => {
    const { pauseResume, reset, restart: start } = this;
    const { onUpdate } = this.props;

    this.instance.update(newEnd);

    onUpdate({ pauseResume, reset, start });
  };

  containerRef = React.createRef();

  render() {
    const { children, className, style } = this.props;
    const { containerRef, pauseResume, reset, restart, update } = this;

    if (typeof children === 'function') {
      return children({
        countUpRef: containerRef,
        pauseResume,
        reset,
        start: restart,
        update,
      });
    }

    return ;
  }
}

export default CountUp;
<commit_msg>Check props formattingFn (#508)<commit_after>import PropTypes from 'prop-types';
import React, { Component } from 'react';
import warning from 'warning';
import { createCountUpInstance } from './common';

class CountUp extends Component {
  static propTypes = {
    decimal: PropTypes.string,
    decimals: PropTypes.number,
    delay: PropTypes.number,
    easingFn: PropTypes.func,
    end: PropTypes.number.isRequired,
    formattingFn: PropTypes.func,
    onEnd: PropTypes.func,
    onStart: PropTypes.func,
    prefix: PropTypes.string,
    redraw: PropTypes.bool,
    separator: PropTypes.string,
    start: PropTypes.number,
    startOnMount: PropTypes.bool,
    suffix: PropTypes.string,
    style: PropTypes.object,
    useEasing: PropTypes.bool,
    preserveValue: PropTypes.bool,
  };

  static defaultProps = {
    decimal: '.',
    decimals: 0,
    delay: null,
    duration: null,
    easingFn: null,
    formattingFn: null,
    onEnd: () => {},
    onPauseResume: () => {},
    onReset: () => {},
    onStart: () => {},
    onUpdate: () => {},
    prefix: '',
    redraw: false,
    separator: '',
    start: 0,
    startOnMount: true,
    suffix: '',
    style: undefined,
    useEasing: true,
    preserveValue: false,
  };

  componentDidMount() {
    const { children, delay } = this.props;
    this.instance = this.createInstance();

    // Don't invoke start if component is used as a render prop
    if (typeof children === 'function' && delay !== 0) return;

    // Otherwise just start immediately
    this.start();
  }

  checkProps = (updatedProps) => {
    const {
      start,
      suffix,
      prefix,
      redraw,
      duration,
      separator,
      decimals,
      decimal,
      className,
      formattingFn
    } = this.props;

    const hasPropsChanged =
      duration !== updatedProps.duration ||
      start !== updatedProps.start ||
      suffix !== updatedProps.suffix ||
      prefix !== updatedProps.prefix ||
      separator !== updatedProps.separator ||
      decimals !== updatedProps.decimals ||
      decimal !== updatedProps.decimal ||
      className !== updatedProps.className ||
      formattingFn !== updatedProps.formattingFn;

    return hasPropsChanged || redraw;
  };

  shouldComponentUpdate(nextProps) {
    const { end } = this.props;
    return this.checkProps(nextProps) || end !== nextProps.end;
  }

  componentDidUpdate(prevProps) {
    // If duration, suffix, prefix, separator or start has changed
    // there's no way to update the values.
    // So we need to re-create the CountUp instance in order to
    // restart it.
    const { end, preserveValue } = this.props;

    if (this.checkProps(prevProps)) {
      this.instance.reset();
      this.instance = this.createInstance();
      this.start();
    }

    // Only end value has changed, so reset and and re-animate with the updated
    // end value.
    if (end !== prevProps.end) {
      if (!preserveValue) {
        this.instance.reset();
      }
      this.instance.update(end);
    }
  }

  componentWillUnmount() {
    if (this.timeoutId) {
      clearTimeout(this.timeoutId);
    }
    this.instance.reset();
  }

  createInstance = () => {
    if (typeof this.props.children === 'function') {
      // Warn when user didn't use containerRef at all
      warning(
        this.containerRef.current &&
          (this.containerRef.current instanceof HTMLElement ||
            this.containerRef.current instanceof SVGTextElement ||
            this.containerRef.current instanceof SVGTSpanElement),
        `Couldn't find attached element to hook the CountUp instance into! Try to attach "containerRef" from the render prop to a an HTMLElement, eg. <span ref={containerRef} />.`,
      );
    }
    return createCountUpInstance(this.containerRef.current, this.props);
  };

  pauseResume = () => {
    const { reset, restart: start, update } = this;
    const { onPauseResume } = this.props;

    this.instance.pauseResume();

    onPauseResume({ reset, start, update });
  };

  reset = () => {
    const { pauseResume, restart: start, update } = this;
    const { onReset } = this.props;

    this.instance.reset();

    onReset({ pauseResume, start, update });
  };

  restart = () => {
    this.reset();
    this.start();
  };

  start = () => {
    const { pauseResume, reset, restart: start, update } = this;
    const { delay, onEnd, onStart } = this.props;
    const run = () =>
      this.instance.start(() => onEnd({ pauseResume, reset, start, update }));

    // Delay start if delay prop is properly set
    if (delay > 0) {
      this.timeoutId = setTimeout(run, delay * 1000);
    } else {
      run();
    }

    onStart({ pauseResume, reset, update });
  };

  update = (newEnd) => {
    const { pauseResume, reset, restart: start } = this;
    const { onUpdate } = this.props;

    this.instance.update(newEnd);

    onUpdate({ pauseResume, reset, start });
  };

  containerRef = React.createRef();

  render() {
    const { children, className, style } = this.props;
    const { containerRef, pauseResume, reset, restart, update } = this;

    if (typeof children === 'function') {
      return children({
        countUpRef: containerRef,
        pauseResume,
        reset,
        start: restart,
        update,
      });
    }

    return ;
  }
}

export default CountUp;
<|endoftext|>


<commit_before>var elementClass = require('element-class');
var dirtyClean = require('../dirty-clean');
var util = require('../util');
var rangeUtil = require('../range-util');
var passThrough = require('../pass-through');
var capitalize = require('capitalize');

module.exports = function () {

    var grid = {};

    //the order here matters because some of these depend on each other
    grid.eventLoop = require('../event-loop')(grid);
    grid.decorators = require('../decorators')(grid);
    grid.cellClasses = require('../cell-classes')(grid);
    grid.rowModel = require('../row-model')(grid);
    grid.colModel = require('../col-model')(grid);
    grid.dataModel = require('../simple-data-model')(grid);
    grid.virtualPixelCellModel = require('../virtual-pixel-cell-model')(grid);
    grid.cellScrollModel = require('../cell-scroll-model')(grid);
    grid.cellMouseModel = require('../cell-mouse-model')(grid);

    grid.viewPort = require('../view-port')(grid);
    grid.viewLayer = require('../view-layer')(grid);

    //things with logic that also register decorators (slightly less core than the other models)
    grid.navigationModel = require('../navigation-model')(grid);
    grid.pixelScrollModel = require('../pixel-scroll-model')(grid);
    grid.colResize = require('../col-resize')(grid);
    grid.colReorder = require('../col-reorder')(grid);
    grid.showHiddenCols = require('../show-hidden-cols')(grid);
    grid.copyPaste = require('../copy-paste')(grid);

    var drawRequested = false;
    grid.requestDraw = function () {
        if (!grid.eventLoop.isRunning) {
            grid.viewLayer.draw();
        } else {
            drawRequested = true;
        }
    };

    grid.eventLoop.bind('grid-draw', function () {
        drawRequested = false;
    });

    grid.eventLoop.addExitListener(function () {
        if (drawRequested) {
            grid.viewLayer.draw();
        }
    });

    function setupTextareaForContainer(textarea, container) {
        textarea.addEventListener('focus', function () {
            if (container) {
                elementClass(container).add('focus');
            }
            textarea.select();
            grid.focused = true;
            grid.eventLoop.fire('grid-focus');
        });

        textarea.addEventListener('blur', function () {
            if (container) {
                elementClass(container).remove('focus');
            }
            grid.focused = false;
            grid.eventLoop.fire('grid-blur');
        });

        container.appendChild(textarea);
        if (!container.getAttribute('tabIndex')) {
            container.tabIndex = -1;
        }
        container.addEventListener('focus', function () {
            if (textarea) {
                textarea.focus();
            }
        });
    }

    function createFocusTextArea() {
        var textarea = document.createElement('div');
        textarea.setAttribute('dts', 'grid-textarea');
        textarea.setAttribute('contenteditable', 'true');
        util.position(textarea, 0, 0, 0, 0);
        textarea.style.background = 'transparent';
        textarea.style.color = 'transparent';
        textarea.style.border = 'none';
        textarea.style.boxShadow = 'none';
        textarea.style.cursor = 'default';
        textarea.classList.add('grid-textarea');
        textarea.select = function () {
            var range = document.createRange();
            range.selectNodeContents(textarea);
            window.getSelection().removeAllRanges();
            window.getSelection().addRange(range);
        };

        textarea.setAttribute('ondragstart', 'return false;');
        Object.defineProperty(textarea, 'value', {
            get: function () {
                return textarea.innerText;
            },
            set: function (val) {
                textarea.innerText = val;
            }
        });
        return textarea;
    }

    var spaces = ['virtual', 'data', 'view'];

    function iterateRange() {
        //expects to be called with the space as its this
        var args = rangeUtil.getArgs(arguments);
        var range = args.range;
        var rowFn = args.rowFn;
        var cellFn = args.cellFn;
        var rowResult;
        for (var r = range.top; r < range.top + range.height; r = this.row.next(r)) {
            rowResult = undefined;
            if (rowFn) {
                rowResult = rowFn(r);
            }
            for (var c = range.left; c < range.left + range.width; c = this.col.next(c)) {
                if (cellFn) {
                    cellFn(r, c, rowResult);
                }
            }
        }
    }

    function iterateWhileHidden(step, start) {
        step = step || 1;
        for (var i = start + step; i < this.count() && i >= 0; i += step) {
            if (!this.get(i).hidden) {
                return i;
            }
        }
    }

    function addToDimension(dim, spaceName, getter) {
        //convert whatever space to virtual and use the row or col virtual getter
        dim.get = function (idx) {
            return getter(this.toVirtual(idx));
        }.bind(dim);
        dim.next = iterateWhileHidden.bind(dim, 1);
        dim.prev = iterateWhileHidden.bind(dim, -1);
        dim.clamp = function (idx) {
            return util.clamp(idx, 0, this.count() - 1);
        }.bind(dim);
        dim.indexes = function () {
            var opts;
            opts = arguments[0];
            opts = opts || {};
            opts.from = opts.from || 0;
            var count = this.count();
            opts.to = opts.to + 1 || (opts.length && opts.length + opts.from) || count;
            var indexes = [];
            for (var idx = Math.max(opts.from, 0); idx < Math.min(opts.to, count); idx = opts.reverse ? this.prev(idx) : this.next(idx)) {
                indexes.push(idx);
            }
            return indexes;
        };

        dim.iterate = function () {
            var opts;
            var fn;
            if (arguments.length === 2) {
                opts = arguments[0];
                fn = arguments[1];
            } else {
                fn = arguments[0];
            }
            dim.indexes(opts).forEach(function (idx) {
                fn(idx);
            });
        };

        //have data to data be passthrough for example
        dim['to' + capitalize(spaceName)] = passThrough;

        return dim;
    }

    function addToSpace(spaceName) {
        var space = grid[spaceName];
        space.iterate = iterateRange.bind(space);
        addToDimension(space.col, spaceName, function (idx) {
            return grid.colModel.get(idx);
        });
        addToDimension(space.row, spaceName, function (idx) {
            return grid.rowModel.get(idx);
        });
        space.up = space.row.prev;
        space.down = space.row.next;
        space.left = space.col.prev;
        space.right = space.col.next;
    }

    grid.data = {
        col: {
            toVirtual: function (dataCol) {
                return grid.colModel.toVirtual(dataCol);
            },
            toView: function (dataCol) {
                return grid.virtual.col.toView(this.toVirtual(dataCol));
            },
            count: function () {
                return grid.colModel.length();
            }
        },
        row: {
            toVirtual: function (dataRow) {
                return grid.rowModel.toVirtual(dataRow);
            },
            toView: function (dataRow) {
                return grid.virtual.row.toView(this.toVirtual(dataRow));
            },
            count: function () {
                return grid.rowModel.length();
            }
        }
    };
    addToSpace('data');

    grid.virtual = {
        col: {
            toData: function (virtualCol) {
                return grid.colModel.toData(virtualCol);
            },
            toView: function (virtualCol) {
                return grid.viewPort.toRealCol(virtualCol);
            },
            count: function () {
                return grid.colModel.length(true);
            }
        },
        row: {
            toData: function (virtualRow) {
                return grid.rowModel.toData(virtualRow);
            },
            toView: function (virtualRow) {
                return grid.viewPort.toRealRow(virtualRow);
            },
            count: function () {
                return grid.rowModel.length(true);
            }
        }
    };
    addToSpace('virtual');

    grid.view = {
        col: {
            toData: function (viewCol) {
                return grid.virtual.col.toData(this.toVirtual(viewCol));
            },
            toVirtual: function (viewCol) {
                return grid.viewPort.toVirtualCol(viewCol);
            },
            count: function () {
                return grid.viewPort.cols();
            }
        },
        row: {
            toData: function (viewRow) {
                return grid.virtual.row.toData(this.toVirtual(viewRow));
            },
            toVirtual: function (viewRow) {
                return grid.viewPort.toVirtualRow(viewRow);
            },
            count: function () {
                return grid.viewPort.rows();
            }
        }
    };
    addToSpace('view');

    var timeouts = [];
    grid.timeout = function(){
        var id = setTimeout.apply(window, arguments);
        timeouts.push(id);
        return id;
    };
    var intervals = [];
    grid.interval = function(){
        var id = setInterval.apply(window, arguments);
        intervals.push(id);
        return id;
    };

    grid.eventLoop.bind('grid-destroy', function () {
        intervals.forEach(function (id) {
            clearInterval(id);
        });

        timeouts.forEach(function (id) {
            clearTimeout(id);
        });
    });

    grid.build = function (container) {
        setupTextareaForContainer(grid.textarea, container);
        grid.viewPort.sizeToContainer(container);
        grid.viewLayer.build(container);
        grid.eventLoop.setContainer(container);
    };

    grid.makeDirtyClean = function () {
        return dirtyClean(grid);
    };

    grid.textarea = createFocusTextArea();

    return grid;
};<commit_msg>remove ff scroll bars<commit_after>var elementClass = require('element-class');
var dirtyClean = require('../dirty-clean');
var util = require('../util');
var rangeUtil = require('../range-util');
var passThrough = require('../pass-through');
var capitalize = require('capitalize');

module.exports = function() {

    var grid = {};

    //the order here matters because some of these depend on each other
    grid.eventLoop = require('../event-loop')(grid);
    grid.decorators = require('../decorators')(grid);
    grid.cellClasses = require('../cell-classes')(grid);
    grid.rowModel = require('../row-model')(grid);
    grid.colModel = require('../col-model')(grid);
    grid.dataModel = require('../simple-data-model')(grid);
    grid.virtualPixelCellModel = require('../virtual-pixel-cell-model')(grid);
    grid.cellScrollModel = require('../cell-scroll-model')(grid);
    grid.cellMouseModel = require('../cell-mouse-model')(grid);

    grid.viewPort = require('../view-port')(grid);
    grid.viewLayer = require('../view-layer')(grid);

    //things with logic that also register decorators (slightly less core than the other models)
    grid.navigationModel = require('../navigation-model')(grid);
    grid.pixelScrollModel = require('../pixel-scroll-model')(grid);
    grid.colResize = require('../col-resize')(grid);
    grid.colReorder = require('../col-reorder')(grid);
    grid.showHiddenCols = require('../show-hidden-cols')(grid);
    grid.copyPaste = require('../copy-paste')(grid);

    var drawRequested = false;
    grid.requestDraw = function() {
        if (!grid.eventLoop.isRunning) {
            grid.viewLayer.draw();
        } else {
            drawRequested = true;
        }
    };

    grid.eventLoop.bind('grid-draw', function() {
        drawRequested = false;
    });

    grid.eventLoop.addExitListener(function() {
        if (drawRequested) {
            grid.viewLayer.draw();
        }
    });

    function setupTextareaForContainer(textarea, container) {
        textarea.addEventListener('focus', function() {
            if (container) {
                elementClass(container).add('focus');
            }
            textarea.select();
            grid.focused = true;
            grid.eventLoop.fire('grid-focus');
        });

        textarea.addEventListener('blur', function() {
            if (container) {
                elementClass(container).remove('focus');
            }
            grid.focused = false;
            grid.eventLoop.fire('grid-blur');
        });

        container.appendChild(textarea);
        if (!container.getAttribute('tabIndex')) {
            container.tabIndex = -1;
        }
        container.addEventListener('focus', function() {
            if (textarea) {
                textarea.focus();
            }
        });
    }

    function createFocusTextArea() {
        var textarea = document.createElement('div');
        textarea.setAttribute('dts', 'grid-textarea');
        textarea.setAttribute('contenteditable', 'true');
        util.position(textarea, 0, 0, 0, 0);
        textarea.style.background = 'transparent';
        textarea.style.color = 'transparent';
        textarea.style.border = 'none';
        textarea.style.boxShadow = 'none';
        textarea.style.cursor = 'default';
        textarea.classList.add('grid-textarea');
        textarea.select = function() {
            var range = document.createRange();
            range.selectNodeContents(textarea);
            window.getSelection().removeAllRanges();
            window.getSelection().addRange(range);
        };

        textarea.setAttribute('ondragstart', 'return false;');
        Object.defineProperty(textarea, 'value', {
            get: function() {
                return textarea.innerText;
            },
            set: function(val) {
                textarea.innerText = val;
            }
        });
        return textarea;
    }

    var spaces = ['virtual', 'data', 'view'];

    function iterateRange() {
        //expects to be called with the space as its this
        var args = rangeUtil.getArgs(arguments);
        var range = args.range;
        var rowFn = args.rowFn;
        var cellFn = args.cellFn;
        var rowResult;
        for (var r = range.top; r < range.top + range.height; r = this.row.next(r)) {
            rowResult = undefined;
            if (rowFn) {
                rowResult = rowFn(r);
            }
            for (var c = range.left; c < range.left + range.width; c = this.col.next(c)) {
                if (cellFn) {
                    cellFn(r, c, rowResult);
                }
            }
        }
    }

    function iterateWhileHidden(step, start) {
        step = step || 1;
        for (var i = start + step; i < this.count() && i >= 0; i += step) {
            if (!this.get(i).hidden) {
                return i;
            }
        }
    }

    function addToDimension(dim, spaceName, getter) {
        //convert whatever space to virtual and use the row or col virtual getter
        dim.get = function(idx) {
            return getter(this.toVirtual(idx));
        }.bind(dim);
        dim.next = iterateWhileHidden.bind(dim, 1);
        dim.prev = iterateWhileHidden.bind(dim, -1);
        dim.clamp = function(idx) {
            return util.clamp(idx, 0, this.count() - 1);
        }.bind(dim);
        dim.indexes = function() {
            var opts;
            opts = arguments[0];
            opts = opts || {};
            opts.from = opts.from || 0;
            var count = this.count();
            opts.to = opts.to + 1 || (opts.length && opts.length + opts.from) || count;
            var indexes = [];
            for (var idx = Math.max(opts.from, 0); idx < Math.min(opts.to, count); idx = opts.reverse ? this.prev(idx) : this.next(idx)) {
                indexes.push(idx);
            }
            return indexes;
        };

        dim.iterate = function() {
            var opts;
            var fn;
            if (arguments.length === 2) {
                opts = arguments[0];
                fn = arguments[1];
            } else {
                fn = arguments[0];
            }
            dim.indexes(opts).forEach(function(idx) {
                fn(idx);
            });
        };

        //have data to data be passthrough for example
        dim['to' + capitalize(spaceName)] = passThrough;

        return dim;
    }

    function addToSpace(spaceName) {
        var space = grid[spaceName];
        space.iterate = iterateRange.bind(space);
        addToDimension(space.col, spaceName, function(idx) {
            return grid.colModel.get(idx);
        });
        addToDimension(space.row, spaceName, function(idx) {
            return grid.rowModel.get(idx);
        });
        space.up = space.row.prev;
        space.down = space.row.next;
        space.left = space.col.prev;
        space.right = space.col.next;
    }

    grid.data = {
        col: {
            toVirtual: function(dataCol) {
                return grid.colModel.toVirtual(dataCol);
            },
            toView: function(dataCol) {
                return grid.virtual.col.toView(this.toVirtual(dataCol));
            },
            count: function() {
                return grid.colModel.length();
            }
        },
        row: {
            toVirtual: function(dataRow) {
                return grid.rowModel.toVirtual(dataRow);
            },
            toView: function(dataRow) {
                return grid.virtual.row.toView(this.toVirtual(dataRow));
            },
            count: function() {
                return grid.rowModel.length();
            }
        }
    };
    addToSpace('data');

    grid.virtual = {
        col: {
            toData: function(virtualCol) {
                return grid.colModel.toData(virtualCol);
            },
            toView: function(virtualCol) {
                return grid.viewPort.toRealCol(virtualCol);
            },
            count: function() {
                return grid.colModel.length(true);
            }
        },
        row: {
            toData: function(virtualRow) {
                return grid.rowModel.toData(virtualRow);
            },
            toView: function(virtualRow) {
                return grid.viewPort.toRealRow(virtualRow);
            },
            count: function() {
                return grid.rowModel.length(true);
            }
        }
    };
    addToSpace('virtual');

    grid.view = {
        col: {
            toData: function(viewCol) {
                return grid.virtual.col.toData(this.toVirtual(viewCol));
            },
            toVirtual: function(viewCol) {
                return grid.viewPort.toVirtualCol(viewCol);
            },
            count: function() {
                return grid.viewPort.cols();
            }
        },
        row: {
            toData: function(viewRow) {
                return grid.virtual.row.toData(this.toVirtual(viewRow));
            },
            toVirtual: function(viewRow) {
                return grid.viewPort.toVirtualRow(viewRow);
            },
            count: function() {
                return grid.viewPort.rows();
            }
        }
    };
    addToSpace('view');

    var timeouts = [];
    grid.timeout = function() {
        var id = setTimeout.apply(window, arguments);
        timeouts.push(id);
        return id;
    };
    var intervals = [];
    grid.interval = function() {
        var id = setInterval.apply(window, arguments);
        intervals.push(id);
        return id;
    };

    grid.eventLoop.bind('grid-destroy', function() {
        intervals.forEach(function(id) {
            clearInterval(id);
        });

        timeouts.forEach(function(id) {
            clearTimeout(id);
        });
    });

    grid.build = function(container) {
        setupTextareaForContainer(grid.textarea, container);
        grid.viewPort.sizeToContainer(container);
        grid.viewLayer.build(container);
        grid.eventLoop.setContainer(container);
        container.style.overflow = 'hidden';
    };

    grid.makeDirtyClean = function() {
        return dirtyClean(grid);
    };

    grid.textarea = createFocusTextArea();

    return grid;
};<|endoftext|>


<commit_before>'use strict';

/**
 * The maximum expiry date a cookie can have.
 *
 * @private
 *
 * @static
 *
 * @final
 *
 * @type {Date}
 *
 * @memberof Crumble
 */
const MAXIMUM_EXPIRY_DATE = new Date('Fri, 31 Dec 9999 23:59:59 GMT');

// --------------------------------------------------------

/**
 * Escapes a string so it can be placed in a regular expression.
 *
 * @static
 *
 * @private
 *
 * @return {String} The escaped string.
 *
 * @param {String} string The string to escape.
 *
 * @memberof Crumble
 */
function escapeForRegularExpression (string)
{
	return string.replace(/([.*+?^=!:${}()/|[\]\\])/g, '\\$1');
}

// --------------------------------------------------------

/**
 * Encodes a string; treating it as a RFC 6265 compliant cookie name.
 *
 * @static
 *
 * @private
 *
 * @return {String} The encoded string.
 *
 * @param {String} string The string to encode as a cookie name.
 *
 * @memberof Crumble
 */
function encodeAsCookieName (string)
{
	return encodeURIComponent(string)
		.replace(/\(/g, '%28')
		.replace(/\)/g, '%29')
		.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent);
}

// --------------------------------------------------------

/**
 * Encodes a string; treating it as a RFC 6265 compliant cookie value.
 *
 * @static
 *
 * @private
 *
 * @return {String} The encoded string.
 *
 * @param {String} string The string to encode as a cookie value.
 *
 * @memberof Crumble
 */
function encodeAsCookieValue (string)
{
	return encodeURIComponent(string)
		.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
}

// --------------------------------------------------------

/**
 * Determines whether a cookie exists in a plate of cookies like `document.cookie`.
 *
 * Example usage:
 *
 * ```
 * Crumble.hasCookie(document.cookie, 'cookie');
 * ```
 *
 * @static
 *
 * @return {Boolean} `true` if the cookie exists, otherwise `false`.
 *
 * @param {String} plate The plate of cookies to search. Will usually be the value of `document.cookie`.
 * @param {String} name  The name of the cookie to search the plate for.
 *
 * @memberof Crumble
 */
function hasCookie (plate, name)
{
	let safeCookieName = escapeForRegularExpression(
		encodeAsCookieName(name)
	);

	return new RegExp('(?:^|.*;)\\s*' + safeCookieName + '\\s*\\=').test(plate);
}

// --------------------------------------------------------

/**
 * Reads the value of a cookie from a plate of cookies like `document.cookie`.
 *
 * Example usage:
 *
 * ```
 * Crumble.getCookie(document.cookie, 'cookie');
 * ```
 *
 * @static
 *
 * @return {String} The decoded value of the cookie or `null` if the cookie doesn't exist.
 *
 * @param {String} plate The plate of cookies to search. Will usually be the value of `document.cookie`.
 * @param {String} name  The name of the cookie to read from the plate.
 *
 * @memberof Crumble
 */
function getCookie (plate, name)
{
	let safeCookieName = escapeForRegularExpression(
		encodeAsCookieName(name)
	);

	let cookie = new RegExp('(?:(?:^|.*;)\\s*' + safeCookieName + '\\s*\\=\\s*(.*?)(?:;|$))').exec(plate);

	if (cookie === null)
	{
		return null;
	}

	return decodeURIComponent(cookie[1]);
}

// --------------------------------------------------------

/**
 * Creates a string that will set a cookie when assigned to a plate like `document.cookie`.
 *
 * Example usage:
 *
 * ```
 * document.cookie = Crumble.setCookie(
 * {
 *    name   : "name",
 *    value  : "value",
 *    domain : "a.domain.com",
 *    path   : "/an/example/path",
 *    secure : false
 * });
 * ```
 *
 * Alternatively you can separate the value from the rest of the crumbs, like so:
 *
 * ```
 * document.cookie = Crumble.setCookie(
 * {
 *    name   : "name",
 *    domain : "a.domain.com",
 *    path   : "/an/example/path",
 *    secure : false
 *
 * }, "value");
 * ```
 *
 * This can be useful when the cookie value is the variable and the other crumbs are fixed.
 *
 * @static
 *
 * @return {String} A string that will set a cookie.
 *
 * @param {Object}             crumbs                          The crumbs that make the cookie.
 * @param {String}             crumbs.name                     The name of the cookie.
 * @param {String}             [crumbs.value = null]           The value of the cookie.
 * @param {Number}             [crumbs.age]                    The duration (in milliseconds) of which the cookie can live. When omitted and no `expires` crumb is provided, the cookie will expire at the end of the session. This takes precedence over the `expires` crumb.
 * @param {Date|String|Number} [crumbs.expires]                The expiry date of the cookie. When omitted and no `age` crumb is provided, the cookie will expire at the end of the session.
 * @param {String}             [crumbs.path]                   The path of which the cookie will be created. Defaults to the current path.
 * @param {String}             [crumbs.domain]                 The (sub)domain of which the cookie will be created. Defaults to the current domain.
 * @param {Boolean}            [crumbs.secure = false]         Indicates whether the cookie should only be passed over HTTPS connections.
 * @param {Boolean}            [crumbs.firstPartyOnly = false] Indicates whether the cookie should only be sent in a first-party context. This is subject to client support.
 * @param {String}             [value = crumbs.value]          The value of the cookie. This takes precedence over the `value` crumb.
 *
 * @throws {TypeError} When the `name` crumb is `null` or not provided.
 * @throws {TypeError} When the `age` crumb is not a valid number.
 * @throws {TypeError} When the `expires` crumb does not represent a valid date.
 */
function setCookie (crumbs, value = crumbs.value)
{
	let { name = null, age, expires, path, domain, secure = false, firstPartyOnly = false } = crumbs;

	if (name === null)
	{
		throw new TypeError('The cookie name must be provided.');
	}

	let cookie = encodeAsCookieName(name) + '=';

	// Value.
	if (typeof value !== 'undefined' && value !== null)
	{
		cookie += encodeAsCookieValue(value);
	}

	// Path.
	if (path)
	{
		cookie += ';path=' + path;
	}

	// Domain.
	if (domain)
	{
		cookie += ';domain=' + domain;
	}

	// Age.
	if (age)
	{
		if (typeof age !== 'number' || isNaN(age))
		{
			throw new TypeError('The cookie age must be a valid number.');
		}

		if (age === Infinity)
		{
			age = MAXIMUM_EXPIRY_DATE.getTime() - Date.now();
		}

		expires = new Date();

		expires.setTime(
			expires.getTime() + age
		);

		// HTTP 1.1.
		cookie += ';max-age=' + (age / 1000);

		// HTTP 1.0.
		cookie += ';expires=' + expires.toUTCString();
	}

	// Expires.
	else if (expires)
	{
		expires = (expires === Infinity) ? MAXIMUM_EXPIRY_DATE : new Date(expires);

		if (expires.toString() === 'Invalid Date')
		{
			throw new TypeError('The cookie expiry must represent a valid date.');
		}
		else
		{
			age = expires.getTime() - Date.now();

			// HTTP 1.1.
			cookie += ';max-age=' + (age / 1000);

			// HTTP 1.0.
			cookie += ';expires=' + expires.toUTCString();
		}
	}

	// Secure.
	if (secure)
	{
		cookie += ';secure';
	}

	// First-Party-Only.
	if (firstPartyOnly)
	{
		cookie += ';first-party-only';
	}

	return cookie;
}

// --------------------------------------------------------

/**
 * Creates a string that will remove a cookie when assigned to a plate like `document.cookie`.
 *
 * Example usage:
 *
 * ```
 * document.cookie = Crumble.removeCookie(
 * {
 *    name   : "name",
 *    domain : "a.domain.com",
 *    path   : "/an/example/path",
 *    secure : false
 * });
 * ```
 *
 * @static
 *
 * @return {String} A string that will remove a cookie.
 *
 * @param {Object}  crumbs                          The crumbs of the cookie to remove.
 * @param {String}  crumbs.name                     The name of the cookie.
 * @param {String}  [crumbs.path]                   The path of which the cookie will be removed from. Defaults to the current path.
 * @param {String}  [crumbs.domain]                 The (sub)domain of which the cookie will be removed from. Defaults to the current domain.
 * @param {Boolean} [crumbs.secure = false]         Indicates whether the cookie should only be removed over HTTPS connections.
 * @param {Boolean} [crumbs.firstPartyOnly = false] Indicates whether the cookie should only be sent in a first-party context. This is subject to client support.
 *
 * @throws {Error} When the `name` crumb is `null` or not provided.
 */
function removeCookie ({ name, path, domain, secure, firstPartyOnly })
{
	return setCookie({
		name, path, domain, secure, firstPartyOnly, age : -3600000
	});
}

// --------------------------------------------------------

module.exports = { hasCookie, getCookie, setCookie, removeCookie };
<commit_msg>Fixed some documentation. The value crumb no longer defaults to null.<commit_after>'use strict';

/**
 * The maximum expiry date a cookie can have.
 *
 * @private
 *
 * @static
 *
 * @final
 *
 * @type {Date}
 *
 * @memberof Crumble
 */
const MAXIMUM_EXPIRY_DATE = new Date('Fri, 31 Dec 9999 23:59:59 GMT');

// --------------------------------------------------------

/**
 * Escapes a string so it can be placed in a regular expression.
 *
 * @static
 *
 * @private
 *
 * @return {String} The escaped string.
 *
 * @param {String} string The string to escape.
 *
 * @memberof Crumble
 */
function escapeForRegularExpression (string)
{
	return string.replace(/([.*+?^=!:${}()/|[\]\\])/g, '\\$1');
}

// --------------------------------------------------------

/**
 * Encodes a string; treating it as a RFC 6265 compliant cookie name.
 *
 * @static
 *
 * @private
 *
 * @return {String} The encoded string.
 *
 * @param {String} string The string to encode as a cookie name.
 *
 * @memberof Crumble
 */
function encodeAsCookieName (string)
{
	return encodeURIComponent(string)
		.replace(/\(/g, '%28')
		.replace(/\)/g, '%29')
		.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent);
}

// --------------------------------------------------------

/**
 * Encodes a string; treating it as a RFC 6265 compliant cookie value.
 *
 * @static
 *
 * @private
 *
 * @return {String} The encoded string.
 *
 * @param {String} string The string to encode as a cookie value.
 *
 * @memberof Crumble
 */
function encodeAsCookieValue (string)
{
	return encodeURIComponent(string)
		.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
}

// --------------------------------------------------------

/**
 * Determines whether a cookie exists in a plate of cookies like `document.cookie`.
 *
 * Example usage:
 *
 * ```
 * Crumble.hasCookie(document.cookie, 'cookie');
 * ```
 *
 * @static
 *
 * @return {Boolean} `true` if the cookie exists, otherwise `false`.
 *
 * @param {String} plate The plate of cookies to search. Will usually be the value of `document.cookie`.
 * @param {String} name  The name of the cookie to search the plate for.
 *
 * @memberof Crumble
 */
function hasCookie (plate, name)
{
	let safeCookieName = escapeForRegularExpression(
		encodeAsCookieName(name)
	);

	return new RegExp('(?:^|.*;)\\s*' + safeCookieName + '\\s*\\=').test(plate);
}

// --------------------------------------------------------

/**
 * Reads the value of a cookie from a plate of cookies like `document.cookie`.
 *
 * Example usage:
 *
 * ```
 * Crumble.getCookie(document.cookie, 'cookie');
 * ```
 *
 * @static
 *
 * @return {String} The decoded value of the cookie or `null` if the cookie doesn't exist.
 *
 * @param {String} plate The plate of cookies to search. Will usually be the value of `document.cookie`.
 * @param {String} name  The name of the cookie to read from the plate.
 *
 * @memberof Crumble
 */
function getCookie (plate, name)
{
	let safeCookieName = escapeForRegularExpression(
		encodeAsCookieName(name)
	);

	let cookie = new RegExp('(?:(?:^|.*;)\\s*' + safeCookieName + '\\s*\\=\\s*(.*?)(?:;|$))').exec(plate);

	if (cookie === null)
	{
		return null;
	}

	return decodeURIComponent(cookie[1]);
}

// --------------------------------------------------------

/**
 * Creates a string that will set a cookie when assigned to a plate like `document.cookie`.
 *
 * Example usage:
 *
 * ```
 * document.cookie = Crumble.setCookie(
 * {
 *    name   : "name",
 *    value  : "value",
 *    domain : "a.domain.com",
 *    path   : "/an/example/path",
 *    secure : false
 * });
 * ```
 *
 * Alternatively you can separate the value from the rest of the crumbs, like so:
 *
 * ```
 * document.cookie = Crumble.setCookie(
 * {
 *    name   : "name",
 *    domain : "a.domain.com",
 *    path   : "/an/example/path",
 *    secure : false
 *
 * }, "value");
 * ```
 *
 * This can be useful when the cookie value is the variable and the other crumbs are fixed.
 *
 * @static
 *
 * @return {String} A string that will set a cookie.
 *
 * @param {Object}             crumbs                          The crumbs that make the cookie.
 * @param {String}             crumbs.name                     The name of the cookie.
 * @param {String}             [crumbs.value]                  The value of the cookie.
 * @param {Number}             [crumbs.age]                    The duration (in milliseconds) of which the cookie can live. When omitted and no `expires` crumb is provided, the cookie will expire at the end of the session. This takes precedence over the `expires` crumb.
 * @param {Date|String|Number} [crumbs.expires]                The expiry date of the cookie. When omitted and no `age` crumb is provided, the cookie will expire at the end of the session.
 * @param {String}             [crumbs.path]                   The path of which the cookie will be created. Defaults to the current path.
 * @param {String}             [crumbs.domain]                 The (sub)domain of which the cookie will be created. Defaults to the current domain.
 * @param {Boolean}            [crumbs.secure = false]         Indicates whether the cookie should only be passed over HTTPS connections.
 * @param {Boolean}            [crumbs.firstPartyOnly = false] Indicates whether the cookie should only be sent in a first-party context. This is subject to client support.
 * @param {String}             [value]                         The value of the cookie. This takes precedence over the `value` crumb.
 *
 * @throws {TypeError} When the `name` crumb is `null` or not provided.
 * @throws {TypeError} When the `age` crumb is not a valid number.
 * @throws {TypeError} When the `expires` crumb does not represent a valid date.
 */
function setCookie (crumbs, value = crumbs.value)
{
	let { name = null, age, expires, path, domain, secure = false, firstPartyOnly = false } = crumbs;

	if (name === null)
	{
		throw new TypeError('The cookie name must be provided.');
	}

	let cookie = encodeAsCookieName(name) + '=';

	// Value.
	if (typeof value !== 'undefined' && value !== null)
	{
		cookie += encodeAsCookieValue(value);
	}

	// Path.
	if (path)
	{
		cookie += ';path=' + path;
	}

	// Domain.
	if (domain)
	{
		cookie += ';domain=' + domain;
	}

	// Age.
	if (age)
	{
		if (typeof age !== 'number' || isNaN(age))
		{
			throw new TypeError('The cookie age must be a valid number.');
		}

		if (age === Infinity)
		{
			age = MAXIMUM_EXPIRY_DATE.getTime() - Date.now();
		}

		expires = new Date();

		expires.setTime(
			expires.getTime() + age
		);

		// HTTP 1.1.
		cookie += ';max-age=' + (age / 1000);

		// HTTP 1.0.
		cookie += ';expires=' + expires.toUTCString();
	}

	// Expires.
	else if (expires)
	{
		expires = (expires === Infinity) ? MAXIMUM_EXPIRY_DATE : new Date(expires);

		if (expires.toString() === 'Invalid Date')
		{
			throw new TypeError('The cookie expiry must represent a valid date.');
		}
		else
		{
			age = expires.getTime() - Date.now();

			// HTTP 1.1.
			cookie += ';max-age=' + (age / 1000);

			// HTTP 1.0.
			cookie += ';expires=' + expires.toUTCString();
		}
	}

	// Secure.
	if (secure)
	{
		cookie += ';secure';
	}

	// First-Party-Only.
	if (firstPartyOnly)
	{
		cookie += ';first-party-only';
	}

	return cookie;
}

// --------------------------------------------------------

/**
 * Creates a string that will remove a cookie when assigned to a plate like `document.cookie`.
 *
 * Example usage:
 *
 * ```
 * document.cookie = Crumble.removeCookie(
 * {
 *    name   : "name",
 *    domain : "a.domain.com",
 *    path   : "/an/example/path",
 *    secure : false
 * });
 * ```
 *
 * @static
 *
 * @return {String} A string that will remove a cookie.
 *
 * @param {Object}  crumbs                          The crumbs of the cookie to remove.
 * @param {String}  crumbs.name                     The name of the cookie.
 * @param {String}  [crumbs.path]                   The path of which the cookie will be removed from. Defaults to the current path.
 * @param {String}  [crumbs.domain]                 The (sub)domain of which the cookie will be removed from. Defaults to the current domain.
 * @param {Boolean} [crumbs.secure = false]         Indicates whether the cookie should only be removed over HTTPS connections.
 * @param {Boolean} [crumbs.firstPartyOnly = false] Indicates whether the cookie should only be sent in a first-party context. This is subject to client support.
 *
 * @throws {Error} When the `name` crumb is `null` or not provided.
 */
function removeCookie ({ name, path, domain, secure, firstPartyOnly })
{
	return setCookie({
		name, path, domain, secure, firstPartyOnly, age : -3600000
	});
}

// --------------------------------------------------------

module.exports = { hasCookie, getCookie, setCookie, removeCookie };
<|endoftext|>


<commit_before>/**
 * horizontal-pager
 *
 * A small, fast, no-dep, horizontal pager.
 * Features:
 *   1.  Horizontal swipes navigate to next/prev page.
 *   2.  Uses requestAnimationFrame aligned (decoupled) animations.
 *   3.  Vertical scrolling supported.
 *   4.  Can start at any page index.
 *   5.  Edge resistance.
 *   6.  Track finger when down, then ease out animation.
 *   7.  Passive event listeners where possible.
 *   8.  When finger up and navigation certain to complete, calls `willComplete`
 *       callback (optional).
 *   9.  Optional `done` callback for notification after navigation complete.
 *   10. Css class identifies scroll level items (pages).
 * Missing:
 *   1.  No continuous option (last-wraps-to-first or vice-versa).
 *   2.  No direct navigate ability (goto directly to page N).
 *
 * Copyright (c) 2017 Alex Grant (@localnerve), LocalNerve LLC
 * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
 */
/* global document, setTimeout, requestAnimationFrame, cancelAnimationFrame */

class HorizontalPager {
  /**
   * HorizontalPager constructor.
   * @public
   *
   * @param {Object} options - HorizontalScroller options.
   * @param {String} options.targetClass - The class that identifies the scroll
   * target. Must be supplied.
   * @param {Number} [options.startIndex] - Which scroll target to show initially.
   * Defaults to 0.
   * @param {Number} [options.scrollThreshold] - Less than 1, a decimal
   * percentage beyond which a touch will cause a complete scroll.
   * Defaults to 0.35.
   * @param {Number} [options.doneThreshold] - The translateX pixel value below
   * which to stop animations. Defaults to 1 (Will not animate below 1px).
   * @param {Function} [options.done] - A function to call after a scroll has
   * completed.
   * @param {Function} [options.willComplete] - A function to call when a scroll
   * will complete very soon.
   */
  constructor (options) {
    this.opts = Object.assign({}, {
      targetClass: '',
      startIndex: 0,
      doneThreshold: 1,
      scrollThreshold: 0.35
    }, options);

    this.onStart = this.onStart.bind(this);
    this.onMove = this.onMove.bind(this);
    this.onEnd = this.onEnd.bind(this);
    this.update = this.update.bind(this);

    this.targetIndex = this.opts.startIndex;
    this.dataId = 'data-hpid';
    this.initialized = false;
    this.destroyed = false;
    this.targetRect = null;
    this.target = null;
    this.nextSib = null;
    this.prevSib = null;
    this.touching = false;
    this.startX = 0;
    this.startY = 0;
    this.currentX = 0;
    this.translateX = 0;
    this.targetX = 0;
    this.atEdge = false;
    this.willCompleteCalled = false;
    this.isVertical = undefined;
    this.targets = [];
    this.rafs = [];
  }

  /**
   * Wireup events.
   * @private
   */
  addEventListeners () {
    document.addEventListener('touchstart', this.onStart, {
      passive: true
    });
    document.addEventListener('mousedown', this.onStart, {
      passive: true
    });
    document.addEventListener('touchmove', this.onMove);
    document.addEventListener('mousemove', this.onMove);
    document.addEventListener('touchend', this.onEnd, {
      passive: true
    });
    document.addEventListener('mouseup', this.onEnd, {
      passive: true
    });
  }

  /**
   * Setup the scroll targets (pages).
   * @private
   */
  setupTargets () {
    let style;
    let i;
    const targetClass = this.opts.targetClass;
    const startIndex = this.opts.startIndex;

    this.targets = document.querySelectorAll(`.${targetClass}`);

    for (i = 0; i < this.targets.length; i++) {
      this.targets[i].setAttribute(this.dataId, i);
      style = this.targets[i].style;

      style.position = (i === startIndex) ? 'static' : 'absolute';
      style.transform = `translate3d(${((i - startIndex) * 100)}%, 0px, 0px)`;
      style.display = 'block';
      style.top = 0;
      style.left = 0;
      style.width = '100%';
    }
  }

  /**
   * Cancel all pending animation frames and set animation done state.
   * @private
   *
   * @param {Object} [newTarget] - The new, next target for paging.
   * If supplied, directs style resets to happen. Will not reset styles for
   * the next target.
   */
  resetAnimations (newTarget) {
    this.rafs.forEach(raf => cancelAnimationFrame(raf));
    this.rafs.length = 0;

    if (newTarget) {
      const newId = newTarget.getAttribute(this.dataId);
      const resetStyle = {
        position: 'absolute',
        willChange: 'initial'
      };

      const nextStyle =
        (this.nextSib && this.nextSib.getAttribute(this.dataId) !== newId) ?
          this.nextSib.style : {};
      const prevStyle =
        (this.prevSib && this.prevSib.getAttribute(this.dataId) !== newId) ?
          this.prevSib.style : {};
      const targetStyle =
        (this.target && this.target.getAttribute(this.dataId) !== newId) ?
          this.target.style : {};

      Object.assign(nextStyle, resetStyle);
      Object.assign(prevStyle, resetStyle);
      Object.assign(targetStyle, resetStyle);
    }
  }

  /**
   * Create percentage translateX value.
   * If done, make sure the value is off by 100% magnitude.
   * NOTE: This presumes all targets are the same width.
   * @private
   *
   * @param {Boolean} done - True if done, and adjustment required.
   * Otherwise, pass the value through.
   * @param {Number} value - The value to adjust, if required.
   */
  transX (done, value) {
    if (done && Math.abs(value) < this.targetRect.width) {
      return (100 * (value < 0 ? -1 : 1));
    }
    return (value / this.targetRect.width) * 100;
  }

  /**
   * Check if scroll animation done, stop if it is.
   * @private
   *
   * @param {Boolean} targetDone - True if the scroll target is done, false otherwise.
   * @param {Boolean} nextDone - True if the next page is done, false otherwise.
   * @param {Boolean} prevDone - True if the prev page is done, false otherwise.
   */
  slideDone (targetDone, nextDone, prevDone) {
    if (nextDone || prevDone) {
      const direction = nextDone ? 1 : -1;
      const nextStyle = this.nextSib ? this.nextSib.style : {};
      const prevStyle = this.prevSib ? this.prevSib.style : {};

      this.target.style.position = 'absolute';
      this.target.style.willChange = 'initial';

      if (nextDone) {
        nextStyle.position = 'static';
        prevStyle.willChange = 'initial';
      } else {
        prevStyle.position = 'static';
        nextStyle.willChange = 'initial';
      }

      if (typeof this.opts.done === 'function') {
        setTimeout(this.opts.done, 0, direction);
      }
    }

    if (targetDone || nextDone || prevDone) {
      this.target = null;
    }
  }

  /**
   * touchstart handler, passive.
   * @private
   *
   * @param {Object} evt - The TouchEvent object.
   */
  onStart (evt) {
    const newTarget = this.targets[this.targetIndex];
    if (!newTarget) {
      return;
    }

    this.resetAnimations(newTarget);

    this.target = newTarget;
    this.nextSib = newTarget.nextElementSibling;
    this.prevSib = newTarget.previousElementSibling;

    this.targetRect = this.target.getBoundingClientRect();
    this.startX = evt.pageX || evt.touches[0].pageX;
    this.startY = evt.pageY || evt.touches[0].pageY;
    this.currentX = this.startX;
    this.touching = true;
    this.willCompleteCalled = false;
    this.isVertical = undefined;
    this.atEdge = false;

    this.target.style.willChange = 'transform';
    this.target.style.position = 'static';

    if (this.nextSib) {
      this.nextSib.style.willChange = 'transform';
    }
    if (this.prevSib) {
      this.prevSib.style.willChange = 'transform';
    }

    this.rafs.push(requestAnimationFrame(this.update));
  }

  /**
   * touchmove handler.
   * @private
   *
   * @param {Object} evt - The TouchEvent object.
   */
  onMove (evt) {
    if (!this.target) {
      return;
    }

    this.currentX = evt.pageX || evt.touches[0].pageX;

    if (typeof this.isVertical === 'undefined') {
      this.isVertical = (
        Math.abs(this.currentX - this.startX) <
          Math.abs((evt.pageY || evt.touches[0].pageY) - this.startY)
      );
    }

    if (this.isVertical) {
      this.currentX = this.startX;
    } else {
      evt.preventDefault();
    }
  }

  /**
   * touchend handler.
   * @private
   */
  onEnd () {
    if (!this.target) {
      return;
    }

    const translateX = this.currentX - this.startX;
    const threshold = this.targetRect.width * this.opts.scrollThreshold;

    this.targetX = 0;

    if (Math.abs(translateX) > threshold && !this.atEdge) {
      const moveLeft = translateX > 0;
      const direction = moveLeft ? -1 : 1;

      this.targetX = moveLeft ?
        this.targetRect.width : -this.targetRect.width;

      this.targetIndex += direction;

      if (!this.willCompleteCalled) {
        this.willCompleteCalled = true;
        if (typeof this.opts.willComplete === 'function') {
          setTimeout(this.opts.willComplete, 0, direction);
        }
      }
    }

    this.touching = false;
  }

  /**
   * RAF handler.
   * @private
   */
  update () {
    if (!this.target) {
      return;
    }

    this.rafs.push(requestAnimationFrame(this.update));

    // Calc translateX units. If not touching, ease out.
    if (this.touching) {
      this.translateX = this.currentX - this.startX;

      // Detect edge, add resistance and limit
      this.atEdge = (!this.prevSib && this.translateX > 0) ||
        (!this.nextSib && this.translateX < 0);
      if (this.atEdge) {
        this.translateX = this.translateX / (
          (Math.abs(this.translateX) / this.targetRect.width) + 1
        );
      }
    } else {
      this.translateX += (this.targetX - this.translateX) / 4;
    }
    const nextX = (this.translateX + this.targetRect.width).toFixed(6);
    const prevX = (this.translateX - this.targetRect.width).toFixed(6);

    // Detect animation done
    const targetDone = this.isVertical || (
      !this.touching && Math.abs(this.translateX) < this.opts.doneThreshold
    );
    const nextDone = Math.abs(nextX) < this.opts.doneThreshold;
    const prevDone = Math.abs(prevX) < this.opts.doneThreshold;

    // Update transform translateX
    this.target.style.transform = `translateX(${
      (targetDone ? 0 : this.transX(nextDone || prevDone, this.translateX))
    }%)`;
    if (this.nextSib) {
      this.nextSib.style.transform = `translateX(${
        (nextDone ? 0 : this.transX(targetDone, nextX))
      }%)`;
    }
    if (this.prevSib) {
      this.prevSib.style.transform = `translateX(${
        (prevDone ? 0 : this.transX(targetDone, prevX))
      }%)`;
    }

    // If slide done, cleanup
    this.slideDone(targetDone, nextDone, prevDone);
  }

  /**
   * Call to start browser event handling, and render targets into position.
   * Requires browser.
   * @public
   */
  initialize () {
    if (!this.initialized) {
      this.initialized = true;
      this.addEventListeners();
      this.setupTargets();
    }
  }

  /**
   * Call to cleanup, stop animations, and events.
   * Requires browser.
   * @public
   */
  destroy () {
    if (this.initialized && !this.destroyed) {
      this.destroyed = true;
      this.resetAnimations();
      document.removeEventListener('touchstart', this.onStart);
      document.removeEventListener('mousedown', this.onStart);
      document.removeEventListener('touchmove', this.onMove);
      document.removeEventListener('mousemove', this.onMove);
      document.removeEventListener('touchend', this.onEnd);
      document.removeEventListener('mouseup', this.onEnd);
    }
  }
}

/**
 * Create the public interface for HorizontalPager.
 * @see HorizontalPager constructor for options details.
 *
 * @param {Object} options - HorizontalPager options.
 * @param {String} options.targetClass - classname to identify pager targets.
 * @returns {Object} The public interface for HorizontalPager.
 */
export default function createHorizontalPager (options) {
  const horizontalPager = new HorizontalPager(options);

  return {
    initialize: horizontalPager.initialize.bind(horizontalPager),
    destroy: horizontalPager.destroy.bind(horizontalPager)
  };
}
<commit_msg>refactor for better raf-onstart overlap handling<commit_after>/**
 * horizontal-pager
 *
 * A small, fast, no-dep, horizontal pager.
 * Features:
 *   1.  Horizontal swipes navigate to next/prev page.
 *   2.  Uses requestAnimationFrame aligned (decoupled) animations.
 *   3.  Vertical scrolling supported.
 *   4.  Can start at any page index.
 *   5.  Edge resistance.
 *   6.  Track finger when down, then ease out animation.
 *   7.  Passive event listeners where possible.
 *   8.  When finger up and navigation certain to complete, calls `willComplete`
 *       callback (optional).
 *   9.  Optional `done` callback for notification after navigation complete.
 *   10. Css class identifies scroll level items (pages).
 * Missing:
 *   1.  No continuous option (last-wraps-to-first or vice-versa).
 *   2.  No direct navigate ability (goto directly to page N).
 *
 * Copyright (c) 2017 Alex Grant (@localnerve), LocalNerve LLC
 * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
 */
/* global document, setTimeout, requestAnimationFrame, cancelAnimationFrame */

class HorizontalPager {
  /**
   * HorizontalPager constructor.
   * @public
   *
   * @param {Object} options - HorizontalScroller options.
   * @param {String} options.targetClass - The class that identifies the scroll
   * target. Must be supplied.
   * @param {Number} [options.startIndex] - Which scroll target to show initially.
   * Defaults to 0.
   * @param {Number} [options.scrollThreshold] - Less than 1, a decimal
   * percentage beyond which a touch will cause a complete scroll.
   * Defaults to 0.35.
   * @param {Number} [options.doneThreshold] - The translateX pixel value below
   * which to stop animations. Defaults to 1 (Will not animate below 1px).
   * @param {Function} [options.done] - A function to call after a scroll has
   * completed.
   * @param {Function} [options.willComplete] - A function to call when a scroll
   * will complete very soon.
   */
  constructor (options) {
    this.opts = Object.assign({}, {
      targetClass: '',
      startIndex: 0,
      doneThreshold: 1,
      scrollThreshold: 0.35
    }, options);

    this.onStart = this.onStart.bind(this);
    this.onMove = this.onMove.bind(this);
    this.onEnd = this.onEnd.bind(this);
    this.update = this.update.bind(this);

    this.targetIndex = this.opts.startIndex;
    this.dataId = 'data-hpid';
    this.initialized = false;
    this.destroyed = false;
    this.targetRect = null;
    this.target = null;
    this.nextSib = null;
    this.prevSib = null;
    this.touching = false;
    this.startX = 0;
    this.startY = 0;
    this.currentX = 0;
    this.translateX = 0;
    this.targetX = 0;
    this.atEdge = false;
    this.willCompleteCalled = false;
    this.isVertical = undefined;
    this.targets = [];
    this.rafs = [];
  }

  /**
   * Wireup events.
   * @private
   */
  addEventListeners () {
    document.addEventListener('touchstart', this.onStart, {
      passive: true
    });
    document.addEventListener('mousedown', this.onStart, {
      passive: true
    });
    document.addEventListener('touchmove', this.onMove);
    document.addEventListener('mousemove', this.onMove);
    document.addEventListener('touchend', this.onEnd, {
      passive: true
    });
    document.addEventListener('mouseup', this.onEnd, {
      passive: true
    });
  }

  /**
   * Setup the scroll targets (pages).
   * @private
   */
  setupTargets () {
    let style;
    let i;
    const targetClass = this.opts.targetClass;
    const startIndex = this.opts.startIndex;

    this.targets = document.querySelectorAll(`.${targetClass}`);

    for (i = 0; i < this.targets.length; i++) {
      this.targets[i].setAttribute(this.dataId, i);
      style = this.targets[i].style;

      style.position = (i === startIndex) ? 'static' : 'absolute';
      style.transform = `translate3d(${((i - startIndex) * 100)}%, 0px, 0px)`;
      style.display = 'block';
      style.top = 0;
      style.left = 0;
      style.width = '100%';
    }
  }

  /**
   * Cancel all pending animation frames and set animation done state.
   * @private
   *
   * @param {Object} [newTarget] - The new, next target for paging.
   * If supplied, directs style resets to happen. Will not reset styles for
   * the next target.
   */
  resetAnimations (newTarget) {
    this.rafs.forEach(raf => cancelAnimationFrame(raf));
    this.rafs.length = 0;

    if (newTarget) {
      const { willComplete } = this.endStatus();

      if (!willComplete) {
        const newId = newTarget.getAttribute(this.dataId);
        const resetStyle = {
          position: 'absolute',
          willChange: 'initial'
        };

        const nextStyle =
          (this.nextSib && this.nextSib.getAttribute(this.dataId) !== newId) ?
            this.nextSib.style : {};
        const prevStyle =
          (this.prevSib && this.prevSib.getAttribute(this.dataId) !== newId) ?
            this.prevSib.style : {};
        const targetStyle =
          (this.target && this.target.getAttribute(this.dataId) !== newId) ?
            this.target.style : {};

        Object.assign(nextStyle, resetStyle);
        Object.assign(prevStyle, resetStyle);
        Object.assign(targetStyle, resetStyle);
      }
    }
  }

  /**
   * Create percentage translateX value.
   * If done, make sure the value is off by 100% magnitude.
   * NOTE: This presumes all targets are the same width.
   * @private
   *
   * @param {Boolean} done - True if done, and adjustment required.
   * Otherwise, pass the value through.
   * @param {Number} value - The value to adjust, if required.
   */
  transX (done, value) {
    if (done && Math.abs(value) < this.targetRect.width) {
      return (100 * (value < 0 ? -1 : 1));
    }
    return (value / this.targetRect.width) * 100;
  }

  /**
   * Calculate the touchend status values.
   *
   * @returns {Object} containing willComplete boolean and directional boolean.
   */
  endStatus () {
    const threshold = this.targetRect.width * this.opts.scrollThreshold;
    const translateX = this.currentX - this.startX;
    const willComplete = Math.abs(translateX) > threshold && !this.atEdge;
    const movingLeft = translateX > 0; // true if moving left to prev, swiping right.

    return {
      willComplete,
      movingLeft
    };
  }

  /**
   * Check if scroll animation done, stop if it is.
   * @private
   *
   * @param {Boolean} targetDone - True if the scroll target is done, false otherwise.
   * @param {Boolean} nextDone - True if the next page is done, false otherwise.
   * @param {Boolean} prevDone - True if the prev page is done, false otherwise.
   */
  slideDone (targetDone, nextDone, prevDone) {
    if (nextDone || prevDone) {
      const direction = nextDone ? 1 : -1;
      const nextStyle = this.nextSib ? this.nextSib.style : {};
      const prevStyle = this.prevSib ? this.prevSib.style : {};

      this.target.style.position = 'absolute';
      this.target.style.willChange = 'initial';

      if (nextDone) {
        nextStyle.position = 'static';
        prevStyle.willChange = 'initial';
      } else {
        prevStyle.position = 'static';
        nextStyle.willChange = 'initial';
      }

      if (typeof this.opts.done === 'function') {
        setTimeout(this.opts.done, 0, direction);
      }
    }

    if (targetDone || nextDone || prevDone) {
      this.target = null;
    }
  }

  /**
   * touchstart handler, passive.
   * @private
   *
   * @param {Object} evt - The TouchEvent object.
   */
  onStart (evt) {
    const newTarget = this.targets[this.targetIndex];
    if (!newTarget) {
      return;
    }

    this.targetRect = newTarget.getBoundingClientRect();
    this.startX = evt.pageX || evt.touches[0].pageX;
    this.startY = evt.pageY || evt.touches[0].pageY;
    this.currentX = this.startX;

    this.resetAnimations(newTarget);

    this.target = newTarget;
    this.nextSib = newTarget.nextElementSibling;
    this.prevSib = newTarget.previousElementSibling;

    this.touching = true;
    this.willCompleteCalled = false;
    this.isVertical = undefined;
    this.atEdge = false;

    this.target.style.willChange = 'transform';
    this.target.style.position = 'static';

    if (this.nextSib) {
      this.nextSib.style.willChange = 'transform';
    }
    if (this.prevSib) {
      this.prevSib.style.willChange = 'transform';
    }

    this.rafs.push(requestAnimationFrame(this.update));
  }

  /**
   * touchmove handler.
   * @private
   *
   * @param {Object} evt - The TouchEvent object.
   */
  onMove (evt) {
    if (!this.target) {
      return;
    }

    this.currentX = evt.pageX || evt.touches[0].pageX;

    if (typeof this.isVertical === 'undefined') {
      this.isVertical = (
        Math.abs(this.currentX - this.startX) <
          Math.abs((evt.pageY || evt.touches[0].pageY) - this.startY)
      );
    }

    if (this.isVertical) {
      this.currentX = this.startX;
    } else {
      evt.preventDefault();
    }
  }

  /**
   * touchend handler.
   * @private
   */
  onEnd () {
    if (!this.target) {
      return;
    }

    const { willComplete, movingLeft } = this.endStatus();

    this.targetX = 0;

    if (willComplete) {
      const direction = movingLeft ? -1 : 1;

      this.targetX = movingLeft ?
        this.targetRect.width : -this.targetRect.width;

      this.targetIndex += direction;

      if (!this.willCompleteCalled) {
        this.willCompleteCalled = true;
        if (typeof this.opts.willComplete === 'function') {
          setTimeout(this.opts.willComplete, 0, direction);
        }
      }
    }

    this.touching = false;
  }

  /**
   * RAF handler.
   * @private
   */
  update () {
    if (!this.target) {
      return;
    }

    this.rafs.push(requestAnimationFrame(this.update));

    // Calc translateX units. If not touching, ease out.
    if (this.touching) {
      this.translateX = this.currentX - this.startX;

      // Detect edge, add resistance and limit
      this.atEdge = (!this.prevSib && this.translateX > 0) ||
        (!this.nextSib && this.translateX < 0);
      if (this.atEdge) {
        this.translateX = this.translateX / (
          (Math.abs(this.translateX) / this.targetRect.width) + 1
        );
      }
    } else {
      this.translateX += (this.targetX - this.translateX) / 4;
    }
    const nextX = (this.translateX + this.targetRect.width).toFixed(6);
    const prevX = (this.translateX - this.targetRect.width).toFixed(6);

    // Detect animation done
    const targetDone = this.isVertical || (
      !this.touching && Math.abs(this.translateX) < this.opts.doneThreshold
    );
    const nextDone = Math.abs(nextX) < this.opts.doneThreshold;
    const prevDone = Math.abs(prevX) < this.opts.doneThreshold;

    // Update transform translateX
    this.target.style.transform = `translateX(${
      (targetDone ? 0 : this.transX(nextDone || prevDone, this.translateX))
    }%)`;
    if (this.nextSib) {
      this.nextSib.style.transform = `translateX(${
        (nextDone ? 0 : this.transX(targetDone, nextX))
      }%)`;
    }
    if (this.prevSib) {
      this.prevSib.style.transform = `translateX(${
        (prevDone ? 0 : this.transX(targetDone, prevX))
      }%)`;
    }

    // If slide done, cleanup
    this.slideDone(targetDone, nextDone, prevDone);
  }

  /**
   * Call to start browser event handling, and render targets into position.
   * Requires browser.
   * @public
   */
  initialize () {
    if (!this.initialized) {
      this.initialized = true;
      this.addEventListeners();
      this.setupTargets();
    }
  }

  /**
   * Call to cleanup, stop animations, and events.
   * Requires browser.
   * @public
   */
  destroy () {
    if (this.initialized && !this.destroyed) {
      this.destroyed = true;
      this.resetAnimations();
      document.removeEventListener('touchstart', this.onStart);
      document.removeEventListener('mousedown', this.onStart);
      document.removeEventListener('touchmove', this.onMove);
      document.removeEventListener('mousemove', this.onMove);
      document.removeEventListener('touchend', this.onEnd);
      document.removeEventListener('mouseup', this.onEnd);
    }
  }
}

/**
 * Create the public interface for HorizontalPager.
 * @see HorizontalPager constructor for options details.
 *
 * @param {Object} options - HorizontalPager options.
 * @param {String} options.targetClass - classname to identify pager targets.
 * @returns {Object} The public interface for HorizontalPager.
 */
export default function createHorizontalPager (options) {
  const horizontalPager = new HorizontalPager(options);

  return {
    initialize: horizontalPager.initialize.bind(horizontalPager),
    destroy: horizontalPager.destroy.bind(horizontalPager)
  };
}
<|endoftext|>


<commit_before>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2016
 */

import { NavigationActions, StackActions } from 'react-navigation';
import { UIDatabase } from '../database';
import Settings from '../settings/MobileAppSettings';
import { createRecord } from '../database/utilities/index';
import { navStrings } from '../localization/index';
import { SETTINGS_KEYS } from '../settings/index';
import { getCurrentRouteName } from './selectors';
import { ROUTES } from './constants';

/**
 * Navigation Action Creators.
 *
 * This file contains actions related to navigation which can be dispatched
 * to the redux store.
 *
 * Actions should return either a plain object or a thunk for side effects.
 *
 * NavigationActions are consists of action creators supplied by react-navigation.
 *
 * NavigationActions.navigate() - accepts an object as the payload, which should
 * have the fields:
 *
 * - `routeName` (See: pages/index - PAGES - keys are routeNames)
 * - `params` (See: Pages/pageContainer and pages/index FINALISABLE_PAGES for requirements)
 *
 */

/**
 * Pushes the Customer Invoices route onto the main navigation stack.
 */
export const gotoCustomerInvoices = () =>
  NavigationActions.navigate({
    routeName: ROUTES.CUSTOMER_INVOICES,
    params: {
      title: navStrings.customer_invoices,
    },
  });

/**
 * Pushes the Customer Requisitions route onto the main navigation stack.
 */
export const gotoCustomerRequisitions = () =>
  NavigationActions.navigate({
    routeName: ROUTES.CUSTOMER_REQUISITIONS,
    params: {
      title: navStrings.customer_requisitions,
    },
  });

/**
 * Pushes the Supplier Invoices route onto the main navigation stack.
 */
export const gotoSupplierInvoices = () =>
  NavigationActions.navigate({
    routeName: ROUTES.SUPPLIER_INVOICES,
    params: {
      title: navStrings.supplier_invoices,
    },
  });

/**
 * Pushes the Supplier Requisitions route onto the main navigation stack.
 */
export const gotoSupplierRequisitions = () =>
  NavigationActions.navigate({
    routeName: ROUTES.SUPPLIER_REQUISITIONS,
    params: {
      title: navStrings.supplier_requisitions,
    },
  });

/**
 * Pushes the Stocktakes route onto the main navigation stack.
 */
export const gotoStocktakes = () =>
  NavigationActions.navigate({
    routeName: ROUTES.STOCKTAKES,
    params: {
      title: navStrings.stocktakes,
    },
  });

/**
 * Pushes the Stock route onto the main navigation stack.
 */
export const gotoStock = () =>
  NavigationActions.navigate({
    routeName: ROUTES.STOCK,
    params: {
      title: navStrings.current_stock,
    },
  });

/**
 * Action creator for navigating to the SupplierRequisition screen.
 *
 * @param {Object} requisition The requisition to pass to the next screen.
 */
export const gotoStocktakeManagePage = (stocktakeName, stocktake) =>
  NavigationActions.navigate({
    routeName: ROUTES.STOCKTAKE_MANAGER,
    params: {
      title: stocktake ? navStrings.manage_stocktake : navStrings.new_stocktake,
      stocktakeName,
      stocktake,
    },
  });

/**
 * Navigate to the StocktakeEditPage.
 *
 * @param {Object} stocktake  The requisition to navigate to.
 */
export const gotoStocktakeEditPage = stocktake => (dispatch, getState) => {
  const { nav } = getState();
  const currentRouteName = getCurrentRouteName(nav);

  const hasNegativeAdjustmentReasons = UIDatabase.objects('NegativeAdjustmentReason').length > 0;
  const hasPositiveAdjustmentReasons = UIDatabase.objects('PositiveAdjustmentReason').length > 0;
  const usesReasons = hasNegativeAdjustmentReasons && hasPositiveAdjustmentReasons;

  // If navigating from the stocktakesPage, go straight to the StocktakeEditPage. Otherwise,
  // replace the current page as the user is coming from StocktakeManagePage.
  const navigationActionCreator =
    currentRouteName === ROUTES.STOCKTAKES ? NavigationActions.navigate : StackActions.replace;

  const navigationParameters = {
    routeName: usesReasons ? ROUTES.STOCKTAKE_EDITOR_WITH_REASONS : ROUTES.STOCKTAKE_EDITOR,
    params: { title: navStrings.stocktake, stocktake },
  };

  dispatch(navigationActionCreator(navigationParameters));
};

/**
 * Action creator for navigating to a customer invoice. Ensures the CI is at least
 * confirmed before navigating as if this is not enforced, it is possible for
 * a particular item being issued across multiple invoices in larger quantities
 * than are available.
 *
 * @param {Object} transaction The CI to navigate to.
 * @param {Object} dispatch    Redux dispatch method.
 */
export const gotoCustomerInvoice = transaction => dispatch => {
  const { isConfirmed, isFinalised } = transaction;

  // Customer invoices are generally created with the status confirmed. This handles unexpected
  // cases of an incoming sycned invoice with status 'nw' or 'sg'.
  if (!isConfirmed && !isFinalised) {
    UIDatabase.write(() => {
      transaction.confirm(UIDatabase);
      UIDatabase.save('Transaction', transaction);
    });
  }

  const navigationAction = NavigationActions.navigate({
    routeName: ROUTES.CUSTOMER_INVOICE,
    params: {
      title: `${navStrings.invoice} ${transaction.serialNumber}`,
      transaction,
    },
  });

  dispatch(navigationAction);
};

/**
 * Action creator for navigating to a SupplierInvoice. Ensures the SI is finalised, if
 * confirmed before navigating. This should not occur, but if this is not enforced, a
 * user can reduce the amount of stock in a SI which has been used in a CI, which causes
 * inventory adjustments instantly.
 *
 * @param {Object} transaction The SI to navigate to.
 * @param {Object} dispatch    Redux dispatch method.
 */
export const gotoSupplierInvoice = transaction => dispatch => {
  const { isConfirmed } = transaction;

  // Supplier invoices are `new` or `finalised`. Ensure any `confirmed` invoices are
  // `finalised` before navigating.
  if (isConfirmed) {
    UIDatabase.write(() => {
      transaction.finalise(UIDatabase);
      UIDatabase.save('Transaction', transaction);
    });
  }

  const navigationAction = NavigationActions.navigate({
    routeName: ROUTES.SUPPLIER_INVOICE,
    params: {
      title: `${navStrings.invoice} ${transaction.serialNumber}`,
      transaction,
    },
  });

  dispatch(navigationAction);
};

/**
 * Navigate to the SupplierRequisitionPage.
 *
 * @param {Object} requisition  SupplierRequisition to navigate to.
 */
export const gotoSupplierRequisition = requisition =>
  NavigationActions.navigate({
    routeName: !requisition.program
      ? ROUTES.SUPPLIER_REQUISITION
      : ROUTES.SUPPLIER_REQUISITION_WITH_PROGRAM,
    params: {
      title: `${navStrings.requisition} ${requisition.serialNumber}`,
      requisition,
    },
  });

/**
 * Navigate to the CustomerRequisitionPage.
 *
 * @param {Object} requisition  Customer requisition to navigate to.
 */
export const gotoCustomerRequisition = requisition =>
  NavigationActions.navigate({
    routeName: ROUTES.CUSTOMER_REQUISITION,
    params: {
      title: `${navStrings.requisition} ${requisition.serialNumber}`,
      requisition,
    },
  });

/**
 * Action creator for creating, and navigating to a Supplier Requsition.
 * Requisition is created by a thunk initially.
 *
 * @param {String} CurrentUser The currently logged in user.
 * @param {Object} requisitionParameters Parameters for the to-be-created object.
 * RequisitionParameters can be any fields in Requisition.js to pass to createRecord.
 */
export const createSupplierRequisition = ({
  currentUser,
  ...requisitionParameters
}) => dispatch => {
  // Fetch this stores custom data to find if this store has customized
  // monthsLeadTime.
  const customData = Settings.get(SETTINGS_KEYS.THIS_STORE_CUSTOM_DATA);

  // CustomData is a stringified JSON object.
  const parsedCustomData = customData ? JSON.parse(customData) : '';

  // Months lead time has an effect on daysToSupply for a requisition.
  const monthsLeadTime = parsedCustomData.monthsLeadTime
    ? Number(customData.monthsLeadTime.data)
    : 0;

  // Create the requisition. If a program was supplied, add items from that
  // program, otherwise just navigate to it.
  let requisition;
  UIDatabase.write(() => {
    requisition = createRecord(UIDatabase, 'Requisition', currentUser, {
      ...requisitionParameters,
      monthsLeadTime,
    });

    if (requisition.program) requisition.addItemsFromProgram(UIDatabase);
  });

  dispatch(gotoSupplierRequisition(requisition));
};

/**
 * Creates a stocktake and replaces the current route in the StackNavigator which
 * would be a StocktakeManagePage, with a StocktakeEditPage, with the newly
 * created Stocktake.
 *
 * @param {Object} StocktakeParameters  Parameters for the stocktake to create.
 */
export const createStocktake = ({ currentUser, stocktakeName, program, itemIds }) => dispatch => {
  let stocktake;

  UIDatabase.write(() => {
    stocktake = createRecord(UIDatabase, 'Stocktake', currentUser, stocktakeName, program);
    if (program) stocktake.addItemsFromProgram(UIDatabase);
    else if (itemIds) stocktake.setItemsByID(UIDatabase, itemIds);
  });

  // If creating a Stocktake with a program or with a list of itemIds, navigate to
  // the StocktakeEditPage. Otherwise, navigate to the StocktakeManagePage.
  return program || itemIds
    ? dispatch(gotoStocktakeEditPage(stocktake))
    : dispatch(gotoStocktakeManagePage(stocktakeName, stocktake));
};

/**
 * Action creator which first creates a customer invoice, and then navigates to it
 * for editing.
 *
 * @param {Object} otherParty     The other party of the invoice (Customer)
 * @param {Object} currentUser    The currently logged in user.
 */
export const createCustomerInvoice = (otherParty, currentUser) => dispatch => {
  let newCustomerInvoice;
  UIDatabase.write(() => {
    newCustomerInvoice = createRecord(UIDatabase, 'CustomerInvoice', otherParty, currentUser);
  });

  dispatch(gotoCustomerInvoice(newCustomerInvoice));
};

/**
 * Action creator which first creates a supplier invoice, and then navigates to it
 * for editing.
 *
 * @param {Object} otherParty     The other party of the invoice (Supplier)
 * @param {Object} currentUser    The currently logged in user.
 */
export const createSupplierInvoice = (otherParty, currentUser) => dispatch => {
  let newCustomerInvoice;
  UIDatabase.write(() => {
    newCustomerInvoice = createRecord(UIDatabase, 'SupplierInvoice', otherParty, currentUser);
  });

  dispatch(gotoSupplierInvoice(newCustomerInvoice));
};

/**
 * Updates a Stocktake with the passed array of itemIDs and navigates to a
 * StocktakeEditPage.
 *
 * @param {Object} stocktake realm Stocktake object
 * @param {Array}  itemIds   Array of item id strings that should be the new
 *                           Items in the stocktake.
 * @param {String} name   Name of the stocktake for updating. Cannot update to remove completely.
 */
export const updateStocktake = (stocktake, itemIds, name = '') => dispatch => {
  UIDatabase.write(() => {
    if (name) stocktake.name = name;

    stocktake.setItemsByID(UIDatabase, itemIds);
    UIDatabase.save('Stocktake', stocktake);
  });

  dispatch(gotoStocktakeEditPage(stocktake));
};
<commit_msg>Add Realm Explorer navigation action<commit_after>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2016
 */

import { NavigationActions, StackActions } from 'react-navigation';
import { UIDatabase } from '../database';
import Settings from '../settings/MobileAppSettings';
import { createRecord } from '../database/utilities/index';
import { navStrings } from '../localization/index';
import { SETTINGS_KEYS } from '../settings/index';
import { getCurrentRouteName } from './selectors';
import { ROUTES } from './constants';

/**
 * Navigation Action Creators.
 *
 * This file contains actions related to navigation which can be dispatched
 * to the redux store.
 *
 * Actions should return either a plain object or a thunk for side effects.
 *
 * NavigationActions are consists of action creators supplied by react-navigation.
 *
 * NavigationActions.navigate() - accepts an object as the payload, which should
 * have the fields:
 *
 * - `routeName` (See: pages/index - PAGES - keys are routeNames)
 * - `params` (See: Pages/pageContainer and pages/index FINALISABLE_PAGES for requirements)
 *
 */

/**
 * Pushes the Realm explorer route onto the main navigation stack.
 */
export const gotoRealmExplorer = () =>
  NavigationActions.navigate({
    routeName: ROUTES.REALM_EXPLORER,
    params: {
      title: 'Database Contents',
    },
  });

/**
 * Pushes the Customer Invoices route onto the main navigation stack.
 */
export const gotoCustomerInvoices = () =>
  NavigationActions.navigate({
    routeName: ROUTES.CUSTOMER_INVOICES,
    params: {
      title: navStrings.customer_invoices,
    },
  });

/**
 * Pushes the Customer Requisitions route onto the main navigation stack.
 */
export const gotoCustomerRequisitions = () =>
  NavigationActions.navigate({
    routeName: ROUTES.CUSTOMER_REQUISITIONS,
    params: {
      title: navStrings.customer_requisitions,
    },
  });

/**
 * Pushes the Supplier Invoices route onto the main navigation stack.
 */
export const gotoSupplierInvoices = () =>
  NavigationActions.navigate({
    routeName: ROUTES.SUPPLIER_INVOICES,
    params: {
      title: navStrings.supplier_invoices,
    },
  });

/**
 * Pushes the Supplier Requisitions route onto the main navigation stack.
 */
export const gotoSupplierRequisitions = () =>
  NavigationActions.navigate({
    routeName: ROUTES.SUPPLIER_REQUISITIONS,
    params: {
      title: navStrings.supplier_requisitions,
    },
  });

/**
 * Pushes the Stocktakes route onto the main navigation stack.
 */
export const gotoStocktakes = () =>
  NavigationActions.navigate({
    routeName: ROUTES.STOCKTAKES,
    params: {
      title: navStrings.stocktakes,
    },
  });

/**
 * Pushes the Stock route onto the main navigation stack.
 */
export const gotoStock = () =>
  NavigationActions.navigate({
    routeName: ROUTES.STOCK,
    params: {
      title: navStrings.current_stock,
    },
  });

/**
 * Action creator for navigating to the SupplierRequisition screen.
 *
 * @param {Object} requisition The requisition to pass to the next screen.
 */
export const gotoStocktakeManagePage = (stocktakeName, stocktake) =>
  NavigationActions.navigate({
    routeName: ROUTES.STOCKTAKE_MANAGER,
    params: {
      title: stocktake ? navStrings.manage_stocktake : navStrings.new_stocktake,
      stocktakeName,
      stocktake,
    },
  });

/**
 * Navigate to the StocktakeEditPage.
 *
 * @param {Object} stocktake  The requisition to navigate to.
 */
export const gotoStocktakeEditPage = stocktake => (dispatch, getState) => {
  const { nav } = getState();
  const currentRouteName = getCurrentRouteName(nav);

  const hasNegativeAdjustmentReasons = UIDatabase.objects('NegativeAdjustmentReason').length > 0;
  const hasPositiveAdjustmentReasons = UIDatabase.objects('PositiveAdjustmentReason').length > 0;
  const usesReasons = hasNegativeAdjustmentReasons && hasPositiveAdjustmentReasons;

  // If navigating from the stocktakesPage, go straight to the StocktakeEditPage. Otherwise,
  // replace the current page as the user is coming from StocktakeManagePage.
  const navigationActionCreator =
    currentRouteName === ROUTES.STOCKTAKES ? NavigationActions.navigate : StackActions.replace;

  const navigationParameters = {
    routeName: usesReasons ? ROUTES.STOCKTAKE_EDITOR_WITH_REASONS : ROUTES.STOCKTAKE_EDITOR,
    params: { title: navStrings.stocktake, stocktake },
  };

  dispatch(navigationActionCreator(navigationParameters));
};

/**
 * Action creator for navigating to a customer invoice. Ensures the CI is at least
 * confirmed before navigating as if this is not enforced, it is possible for
 * a particular item being issued across multiple invoices in larger quantities
 * than are available.
 *
 * @param {Object} transaction The CI to navigate to.
 * @param {Object} dispatch    Redux dispatch method.
 */
export const gotoCustomerInvoice = transaction => dispatch => {
  const { isConfirmed, isFinalised } = transaction;

  // Customer invoices are generally created with the status confirmed. This handles unexpected
  // cases of an incoming sycned invoice with status 'nw' or 'sg'.
  if (!isConfirmed && !isFinalised) {
    UIDatabase.write(() => {
      transaction.confirm(UIDatabase);
      UIDatabase.save('Transaction', transaction);
    });
  }

  const navigationAction = NavigationActions.navigate({
    routeName: ROUTES.CUSTOMER_INVOICE,
    params: {
      title: `${navStrings.invoice} ${transaction.serialNumber}`,
      transaction,
    },
  });

  dispatch(navigationAction);
};

/**
 * Action creator for navigating to a SupplierInvoice. Ensures the SI is finalised, if
 * confirmed before navigating. This should not occur, but if this is not enforced, a
 * user can reduce the amount of stock in a SI which has been used in a CI, which causes
 * inventory adjustments instantly.
 *
 * @param {Object} transaction The SI to navigate to.
 * @param {Object} dispatch    Redux dispatch method.
 */
export const gotoSupplierInvoice = transaction => dispatch => {
  const { isConfirmed } = transaction;

  // Supplier invoices are `new` or `finalised`. Ensure any `confirmed` invoices are
  // `finalised` before navigating.
  if (isConfirmed) {
    UIDatabase.write(() => {
      transaction.finalise(UIDatabase);
      UIDatabase.save('Transaction', transaction);
    });
  }

  const navigationAction = NavigationActions.navigate({
    routeName: ROUTES.SUPPLIER_INVOICE,
    params: {
      title: `${navStrings.invoice} ${transaction.serialNumber}`,
      transaction,
    },
  });

  dispatch(navigationAction);
};

/**
 * Navigate to the SupplierRequisitionPage.
 *
 * @param {Object} requisition  SupplierRequisition to navigate to.
 */
export const gotoSupplierRequisition = requisition =>
  NavigationActions.navigate({
    routeName: !requisition.program
      ? ROUTES.SUPPLIER_REQUISITION
      : ROUTES.SUPPLIER_REQUISITION_WITH_PROGRAM,
    params: {
      title: `${navStrings.requisition} ${requisition.serialNumber}`,
      requisition,
    },
  });

/**
 * Navigate to the CustomerRequisitionPage.
 *
 * @param {Object} requisition  Customer requisition to navigate to.
 */
export const gotoCustomerRequisition = requisition =>
  NavigationActions.navigate({
    routeName: ROUTES.CUSTOMER_REQUISITION,
    params: {
      title: `${navStrings.requisition} ${requisition.serialNumber}`,
      requisition,
    },
  });

/**
 * Action creator for creating, and navigating to a Supplier Requsition.
 * Requisition is created by a thunk initially.
 *
 * @param {String} CurrentUser The currently logged in user.
 * @param {Object} requisitionParameters Parameters for the to-be-created object.
 * RequisitionParameters can be any fields in Requisition.js to pass to createRecord.
 */
export const createSupplierRequisition = ({
  currentUser,
  ...requisitionParameters
}) => dispatch => {
  // Fetch this stores custom data to find if this store has customized
  // monthsLeadTime.
  const customData = Settings.get(SETTINGS_KEYS.THIS_STORE_CUSTOM_DATA);

  // CustomData is a stringified JSON object.
  const parsedCustomData = customData ? JSON.parse(customData) : '';

  // Months lead time has an effect on daysToSupply for a requisition.
  const monthsLeadTime = parsedCustomData.monthsLeadTime
    ? Number(customData.monthsLeadTime.data)
    : 0;

  // Create the requisition. If a program was supplied, add items from that
  // program, otherwise just navigate to it.
  let requisition;
  UIDatabase.write(() => {
    requisition = createRecord(UIDatabase, 'Requisition', currentUser, {
      ...requisitionParameters,
      monthsLeadTime,
    });

    if (requisition.program) requisition.addItemsFromProgram(UIDatabase);
  });

  dispatch(gotoSupplierRequisition(requisition));
};

/**
 * Creates a stocktake and replaces the current route in the StackNavigator which
 * would be a StocktakeManagePage, with a StocktakeEditPage, with the newly
 * created Stocktake.
 *
 * @param {Object} StocktakeParameters  Parameters for the stocktake to create.
 */
export const createStocktake = ({ currentUser, stocktakeName, program, itemIds }) => dispatch => {
  let stocktake;

  UIDatabase.write(() => {
    stocktake = createRecord(UIDatabase, 'Stocktake', currentUser, stocktakeName, program);
    if (program) stocktake.addItemsFromProgram(UIDatabase);
    else if (itemIds) stocktake.setItemsByID(UIDatabase, itemIds);
  });

  // If creating a Stocktake with a program or with a list of itemIds, navigate to
  // the StocktakeEditPage. Otherwise, navigate to the StocktakeManagePage.
  return program || itemIds
    ? dispatch(gotoStocktakeEditPage(stocktake))
    : dispatch(gotoStocktakeManagePage(stocktakeName, stocktake));
};

/**
 * Action creator which first creates a customer invoice, and then navigates to it
 * for editing.
 *
 * @param {Object} otherParty     The other party of the invoice (Customer)
 * @param {Object} currentUser    The currently logged in user.
 */
export const createCustomerInvoice = (otherParty, currentUser) => dispatch => {
  let newCustomerInvoice;
  UIDatabase.write(() => {
    newCustomerInvoice = createRecord(UIDatabase, 'CustomerInvoice', otherParty, currentUser);
  });

  dispatch(gotoCustomerInvoice(newCustomerInvoice));
};

/**
 * Action creator which first creates a supplier invoice, and then navigates to it
 * for editing.
 *
 * @param {Object} otherParty     The other party of the invoice (Supplier)
 * @param {Object} currentUser    The currently logged in user.
 */
export const createSupplierInvoice = (otherParty, currentUser) => dispatch => {
  let newCustomerInvoice;
  UIDatabase.write(() => {
    newCustomerInvoice = createRecord(UIDatabase, 'SupplierInvoice', otherParty, currentUser);
  });

  dispatch(gotoSupplierInvoice(newCustomerInvoice));
};

/**
 * Updates a Stocktake with the passed array of itemIDs and navigates to a
 * StocktakeEditPage.
 *
 * @param {Object} stocktake realm Stocktake object
 * @param {Array}  itemIds   Array of item id strings that should be the new
 *                           Items in the stocktake.
 * @param {String} name   Name of the stocktake for updating. Cannot update to remove completely.
 */
export const updateStocktake = (stocktake, itemIds, name = '') => dispatch => {
  UIDatabase.write(() => {
    if (name) stocktake.name = name;

    stocktake.setItemsByID(UIDatabase, itemIds);
    UIDatabase.save('Stocktake', stocktake);
  });

  dispatch(gotoStocktakeEditPage(stocktake));
};
<|endoftext|>


<commit_before>function getParamNames(func) {
  var
    STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
    ARGUMENT_NAMES = /([^\s,]+)/g,
    fnStr = func.toString().replace(STRIP_COMMENTS, ''),
    result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);

  if(result === null) {
     result = [];
   }

  return result;
}

var Factory = function(Model, config) {
  var configDefaults = {
    endpoints: {
      prefix: '',
      perPage: 20,
      list: { method: 'GET', path: '' },
      find: { method: 'GET', path: '' },
      create: { method: 'POST', path: '' },
      update: { method: 'PUT', path: '' },
      del: { method: 'DEL', path: '' }
    }
  };

  // ...
};

Factory.prototype._$dump = function() {
  // ...
};

Factory.prototype._$registerListeners = function(model) {
  // ...
};

Factory.prototype.$create = function() {
  // ...
};

Factory.prototype.$find = function(id) {
  // ...
};

Factory.prototype.$findSync = function(id) {
  // ...
};

Factory.prototype.$list = function(page, perPage) {
  // ...
};

Factory.prototype.$listSync = function(page, offset, perPage) {
  // ...
};

Factory.prototype.$map = function(idList) {
  // ...
};

Factory.prototype.$mapSync = function(idList) {
  // ...
};<commit_msg>renamed argument names<commit_after>function getParamNames(func) {
  var
    STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
    ARGUMENT_NAMES = /([^\s,]+)/g,
    fnStr = func.toString().replace(STRIP_COMMENTS, ''),
    result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);

  if(result === null) {
     result = [];
   }

  return result;
}

var Factory = function(Model, config) {
  var configDefaults = {
    endpoints: {
      prefix: '',
      perPage: 20,
      list: { method: 'GET', path: '' },
      find: { method: 'GET', path: '' },
      create: { method: 'POST', path: '' },
      update: { method: 'PUT', path: '' },
      del: { method: 'DEL', path: '' }
    }
  };

  // ...
};

Factory.prototype._$dump = function() {
  // ...
};

Factory.prototype._$registerListeners = function(model) {
  // ...
};

Factory.prototype.$create = function() {
  // ...
};

Factory.prototype.$find = function(id) {
  // ...
};

Factory.prototype.$findSync = function(id) {
  // ...
};

Factory.prototype.$list = function(page, perPage) {
  // ...
};

Factory.prototype.$listSync = function(page, perPage) {
  // ...
};

Factory.prototype.$map = function(ids) {
  // ...
};

Factory.prototype.$mapSync = function(ids) {
  // ...
};<|endoftext|>