<commit_before>/*! © 2012 Korx Limited */
/*
---
name: Korx.Cycler

version: 0.1

description: Korx.Cycler is a versatile slider/slideshow/carousel MooTools plugin. It uses CSS 3 transitions with a fallback to Fx.Morph for older browsers.

license:
  - MIT-style

authors:
  - Michael Bird <michael.bird@korx.com>

requires:
  - core/1.4:Core
  - core/1.4:Array
  - core/1.4:Function
  - core/1.4:Event
  - core/1.4:Class
  - core/1.4:Class.Extras
  - core/1.4:Element
  - core/1.4:Element.Style
  - core/1.4:Element.Event
  - core/1.4:Fx.Morph

provides:
  - Korx.Cycler

...
*/

Element.implement({

    addTransitionEndEvent: function(fn){
        ['webkitTransitionEnd', 'MSTransitionEnd', 'oTransitionEnd', 'transitionend'].each(function(event){
            if (this.addEventListener) {
                this.addEventListener(event, fn, false);
            } else {
                this.attachEvent(event, fn);
            }
        }, this);
        return this;
    },

    removeTransitionEndEvent: function(fn){
        ['webkitTransitionEnd', 'MSTransitionEnd', 'oTransitionEnd', 'transitionend'].each(function(event){
            if (this.removeEventListener) {
                this.removeEventListener(event, fn, false);
            } else {
                this.detachEvent(event, fn);
            }
        }, this);
        return this;
    },

    setPrefixedStyles: function(styles){
        for (var style in styles) this.setPrefixedStyle(style, styles[style]);
        return this;
    },

    setPrefixedStyle: function(property, value){
        if ((property.length >= 10 && property.slice(0, 10) == 'transition') || (property.length >= 9 && property.slice(0, 9) == 'transform')) {
            ['-webkit-', '-moz-', '-ms-', '-o-', ''].each(function(prefix){
                this.setStyle(prefix + property.hyphenate(), value);
            }, this);
        } else {
            this.setStyle(property, value);
        }
        return this;
    },

    removePrefixedStyles: function(){
        return this.removePrefixedStyle(arguments);
    },

    removePrefixedStyle: function(){
        Array.flatten(arguments).each(function(style){
            this.setPrefixedStyle(style, null);
        }, this);
        return this;
    }

});

if (!window.Korx) window.Korx = {};

Korx.Cycler = new Class({

    Implements: [Events, Options],

    options: {/*
        onPlay: function(thisElement){},
        onStop: function(thisElement){},
        onPause: function(thisElement){},
        onMove: function(thisElement, event){},*/
        onReady: function(thisElement){
            this.play();
        },
        onStep: function(thisElement){
            this.move(1);
        },
        onTap: function(thisElement, event){
            this.pause();
        },
        onSwipe: function(thisElement, event){
            this.pause();
            switch (event.direction) {
                case 'up':
                case 'left':
                    this.move(-1);
                    break;
                case 'down':
                case 'right':
                    this.move(1);
                    break;
            }
        },
        duration: 5000,
        appear: {
            onStart: function(){
                this.element.setStyle('display', null);
            },
            duration: 500,
            transition: 'quad:in:out',
            timingFunction: 'ease-in-out'
        },
        disappear: {
            onComplete: function(){
                this.element.setStyle('display', 'none');
            },
            duration: 500,
            transition: 'quad:in:out',
            timingFunction: 'ease-in-out'
        },
        origin: {
            css: {
                zIndex: 3
            },
            js: {
                zIndex: 3
            }
        },
        current: {
            css: {
                zIndex: 1
            },
            js: {
                zIndex: 1
            }
        },
        destination: {
            css: {
                zIndex: 2
            },
            js: {
                zIndex: 2
            }
        },
        swipeTime: 500,
        swipeDistance: 50
    },

    initialize: function(element, items, options){
        this.setOptions(options);

        this.element = document.id(element);
        this.items = [];
        this.css = false;
        this.ready = false;
        this.current = 0;
        this.stepper = null;

        this.addItems($$(document.id(items) || items));

        // Detect swipe
        this.element.addEvent('touchstart', function(e){
            // calculate the swipe distance based on the viewport scale
            var distance = this.options.swipeDistance;
            if (screen.width && screen.height && window.innerWidth && window.innerHeight) {
                if (!window.orientation) {
                    window.orientation = 0;
                }
                switch (window.orientation) {
                    case 0:
                    case 180:
                        // portrait
                        distance = distance * (window.innerWidth / screen.width);
                        break;
                    case 90:
                    case -90:
                    case 270:
                        // landscape
                        distance = distance * (window.innerHeight / screen.height);
                        break;
                }
            }
            // setup position vars
            var origin = null;
            var destination = null;
            // set the origin position
            if (e.touches) {
                if (e.touches.length == 1) {
                    origin = {
                        x: e.touches[0].clientX,
                        y: e.touches[0].clientY,
                        t: new Date().getTime()
                    };
                }
            } else {
                origin = {
                    x: e.clientX,
                    y: e.clientY,
                    t: new Date().getTime()
                };
            }
            if (origin != null) {
                // listen to move events
                var moveListener = function(e){
                    // set the destination position
                    if (e.touches) {
                        if (e.touches.length == 1) {
                            destination = {
                                x: e.touches[0].clientX,
                                y: e.touches[0].clientY,
                                t: new Date().getTime()
                            };
                        } else {
                            destination = null;
                        }
                    } else {
                        destination = {
                            x: e.clientX,
                            y: e.clientY,
                            t: new Date().getTime()
                        };
                    }
                    // make sure we're in the timeframe and distance tolerance
                    if (origin != null && destination != null && destination.t - origin.t < this.options.swipeTime) {
                        // prevent default action
                        e.preventDefault();
                    } else {
                        // remove listeners
                        this.element.removeEvent('touchmove', moveListener);
                        this.element.removeEvent('touchend', endListener);
                        this.element.removeEvent('touchcancel', cancelListener);
                    }
                }.bind(this);
                this.element.addEvent('touchmove', moveListener);
                // listen to end events
                var endListener = function(e){
                    if (origin != null && destination != null && destination.t - origin.t < this.options.swipeTime) {
                        // prevent default action
                        e.preventDefault();
                        // get the difference in the origin and destination x postitions
                        var dx = destination.x - origin.x;
                        // get the difference in the origin and destination y postitions
                        var dy = destination.y - origin.y;
                        // check of the delta is more than the minimum swipe distance
                        var direction = null;
                        if (dx < -distance && -distance < dy < distance) {
                            direction = 'left';
                        } else if (dx > distance && -distance < dy < distance) {
                            direction = 'right';
                        } else if (dy < -distance && -distance < dx < distance) {
                            direction = 'up';
                        } else if (dy > distance && -distance < dx < distance) {
                            direction = 'down';
                        }
                        // fire a swipe event if there was a direction or a tap if there wasnt
                        if (direction != null) {
                            this.fireEvent('swipe', [this.element, {
                                origin: origin,
                                destination: destination,
                                direction: direction
                            }]);
                        } else {
                            this.fireEvent('tap', [this.element, {
                                origin: origin,
                                destination: destination
                            }]);
                        }
                    }
                    // remove listeners
                    this.element.removeEvent('touchmove', moveListener);
                    this.element.removeEvent('touchend', endListener);
                    this.element.removeEvent('touchcancel', cancelListener);
                }.bind(this);
                this.element.addEvent('touchend', endListener);
                // listen to cancel events
                var cancelListener = function(e){
                    // remove listeners
                    this.element.removeEvent('touchmove', moveListener);
                    this.element.removeEvent('touchend', endListener);
                    this.element.removeEvent('touchcancel', cancelListener);
                }.bind(this);
                this.element.addEvent('touchcancel', cancelListener);
            }
        }.bind(this));

        // test for CSS transition support
        this.useJs();
        var testListener = function(e){
            this.useCss();
        }.bind(this);
        var test = new Element('div').setPrefixedStyles({
            display: 'block',
            position: 'absolute',
            top: -1,
            left: -1,
            width: 1,
            height: 1,
            transition: 'top 1ms ease'
        }).addTransitionEndEvent(testListener).inject(document.id(document.body));
        // wait for events to be applied
        (function() {
            // move the test element
            test.setPrefixedStyle('top', -2);
            // give the event time to fire
            (function() {
                // get rid of the test element
                test.removeTransitionEndEvent(testListener).destroy();
                // reset the elements
                this.reset();
                // let the world know we're ready for action
                this.ready = true;
                this.fireEvent('ready', this.element);
            }).delay(50, this);
        }).delay(10, this);
    },

    useCss: function(){
        this.css = true;
        if (this.stepper != null) {
            this.stop().play();
        } else {
            this.stop();
        }
        return this;
    },

    useJs: function(){
        this.css = false;
        if (this.stepper != null) {
            this.stop().play();
        } else {
            this.stop();
        }
        return this;
    },

    addItems: function(){
        Array.flatten(arguments).each(function(item){
            this.items.include(item);
        }, this);
        return this;
    },

    removeItems: function(){
        return $$(Array.flatten(arguments).map(function(item){
            this.items.erase(item);
            return item;
        }, this));
    },

    reset: function(){
        if (this.items.length < 1 || this.current >= this.items.length) return this;

        // remove all existing styles and then setup the current item styles
        this.items.each(function(item){
            item.removePrefixedStyles(['transition-property', 'transition-duration', 'transition-timing-function'].append(Object.keys(this.options.origin.js)).append(Object.keys(this.options.origin.css))).setPrefixedStyles(this.css ? this.options.origin.css : this.options.origin.js).setStyle('display', 'none').get('morph').cancel();
        }, this);
        this.items[this.current].setPrefixedStyles(this.css ? this.options.current.css : this.options.current.js).setStyle('display', null);

        return this;
    },

    play: function(){
        if (!this.ready || this.items.length < 1) return this;

        // create the stepper to automcatically move
        clearInterval(this.stepper);
        this.stepper = (function(){
            this.fireEvent('step', this.element);
        }).periodical(this.options.duration, this);
        this.fireEvent('play', this.element);
        return this;
    },

    stop: function(){
        clearInterval(this.stepper);
        this.stepper = null;
        this.reset();
        this.fireEvent('stop', this.element);
        return this;
    },

    pause: function(){
        clearInterval(this.stepper);
        this.stepper = null;
        this.fireEvent('pause', this.element);
        return this;
    },

    move: function(delta){
        if (!this.ready || this.items.length < 1 || delta == 0) return this;

        // get the remainder if the delta is bigger than the items array
        remainder = delta % this.items.length;
        var previous = this.current;
        var next = previous + remainder;
        // make sure the next index isn't out of bounds
        if (next > this.items.length - 1) {
            next = next - this.items.length;
        } else if (next < 0) {
            next = next + this.items.length;
        }

        // get the next and previous items
        var nextItem = this.items[next];
        var previousItem = this.items[previous];

        // fire the move event
        this.fireEvent('move', [this.element, {
            delta: delta,
            next: nextItem,
            previous: previousItem
        }]);

        // update the current item index
        this.current = next;

        if (this.css) {

            // transition next item to current style
            this.transition(nextItem, (delta > 0) ? this.options.origin.css : this.options.destination.css, this.options.current.css, this.options.appear);
            // transition previous item to destination style
            this.transition(previousItem, null, (delta > 0) ? this.options.destination.css : this.options.origin.css, this.options.disappear);

        } else {

            // set next item origin style
            nextItem.setPrefixedStyles((delta > 0) ? this.options.origin.js : this.options.destination.js);
            // morph next item to current style
            nextItem.set('morph', this.options.appear).morph(this.options.current.js);
            // morph previous item to destination style
            previousItem.set('morph', this.options.disappear).morph((delta > 0) ? this.options.destination.js : this.options.origin.js);

        }

        return this;
    },

    transition: function(element, origin, destination, options){
        if (element.retrieve('queue', []).length > 0) {
            element.store('queue', element.retrieve('queue', []).push({origin: origin, destination: destination, options: options}));
        } else {
            // don't transition to the origin styles
            if (origin != null) {
                element.setPrefixedStyles({
                    transitionProperty: 'none'
                });
            }
            // wait for styles to be applied
            (function(){
                // set element origin style
                if (origin != null) {
                    element.setPrefixedStyles(origin);
                }
                // fire start event
                if (typeOf(options.onStart) == 'function') {
                    options.onStart.call({element: element, options: options});
                }
                // wait for styles to be applied
                (function(){
                    // start using transitions for the element
                    element.setPrefixedStyles({
                        transitionProperty: 'all',
                        transitionDuration: options.duration+'ms',
                        transitionTimingFunction: options.timingFunction
                    });
                    // stop using transitions when the element finishes transitioning
                    var transitionEndEventListener = function(e){
                        if (e.target == element) {
                            element.removeTransitionEndEvent(transitionEndEventListener);
                            // fire complete event
                            if (typeOf(options.onComplete) == 'function') {
                                options.onComplete.call({element: element, options: options});
                            }
                            // action next in queue
                            if (element.retrieve('queue', []).length > 0) {
                                var queue = element.retrieve('queue', []).shift();
                                this.transition(element, queue.origin, queue.destination, queue.options);
                            } else {
                                element.setPrefixedStyles({
                                    transitionProperty: 'none'
                                });
                            }
                        }
                    }.bind(this);
                    element.addTransitionEndEvent(transitionEndEventListener);
                    // set element destination style
                    element.setPrefixedStyles(destination);
                }).delay(10, this);
            }).delay(10, this);
        }
    }

});<commit_msg>Fixed zIndex order<commit_after>/*! © 2012 Korx Limited */
/*
---
name: Korx.Cycler

version: 0.1

description: Korx.Cycler is a versatile slider/slideshow/carousel MooTools plugin. It uses CSS 3 transitions with a fallback to Fx.Morph for older browsers.

license:
  - MIT-style

authors:
  - Michael Bird <michael.bird@korx.com>

requires:
  - core/1.4:Core
  - core/1.4:Array
  - core/1.4:Function
  - core/1.4:Event
  - core/1.4:Class
  - core/1.4:Class.Extras
  - core/1.4:Element
  - core/1.4:Element.Style
  - core/1.4:Element.Event
  - core/1.4:Fx.Morph

provides:
  - Korx.Cycler

...
*/

Element.implement({

    addTransitionEndEvent: function(fn){
        ['webkitTransitionEnd', 'MSTransitionEnd', 'oTransitionEnd', 'transitionend'].each(function(event){
            if (this.addEventListener) {
                this.addEventListener(event, fn, false);
            } else {
                this.attachEvent(event, fn);
            }
        }, this);
        return this;
    },

    removeTransitionEndEvent: function(fn){
        ['webkitTransitionEnd', 'MSTransitionEnd', 'oTransitionEnd', 'transitionend'].each(function(event){
            if (this.removeEventListener) {
                this.removeEventListener(event, fn, false);
            } else {
                this.detachEvent(event, fn);
            }
        }, this);
        return this;
    },

    setPrefixedStyles: function(styles){
        for (var style in styles) this.setPrefixedStyle(style, styles[style]);
        return this;
    },

    setPrefixedStyle: function(property, value){
        if ((property.length >= 10 && property.slice(0, 10) == 'transition') || (property.length >= 9 && property.slice(0, 9) == 'transform')) {
            ['-webkit-', '-moz-', '-ms-', '-o-', ''].each(function(prefix){
                this.setStyle(prefix + property.hyphenate(), value);
            }, this);
        } else {
            this.setStyle(property, value);
        }
        return this;
    },

    removePrefixedStyles: function(){
        return this.removePrefixedStyle(arguments);
    },

    removePrefixedStyle: function(){
        Array.flatten(arguments).each(function(style){
            this.setPrefixedStyle(style, null);
        }, this);
        return this;
    }

});

if (!window.Korx) window.Korx = {};

Korx.Cycler = new Class({

    Implements: [Events, Options],

    options: {/*
        onPlay: function(thisElement){},
        onStop: function(thisElement){},
        onPause: function(thisElement){},
        onMove: function(thisElement, event){},*/
        onReady: function(thisElement){
            this.play();
        },
        onStep: function(thisElement){
            this.move(1);
        },
        onTap: function(thisElement, event){
            this.pause();
        },
        onSwipe: function(thisElement, event){
            this.pause();
            switch (event.direction) {
                case 'up':
                case 'left':
                    this.move(-1);
                    break;
                case 'down':
                case 'right':
                    this.move(1);
                    break;
            }
        },
        duration: 5000,
        appear: {
            onStart: function(){
                this.element.setStyle('display', null);
            },
            duration: 500,
            transition: 'quad:in:out',
            timingFunction: 'ease-in-out'
        },
        disappear: {
            onComplete: function(){
                this.element.setStyle('display', 'none');
            },
            duration: 500,
            transition: 'quad:in:out',
            timingFunction: 'ease-in-out'
        },
        origin: {
            css: {
                zIndex: 3
            },
            js: {
                zIndex: 3
            }
        },
        current: {
            css: {
                zIndex: 2
            },
            js: {
                zIndex: 2
            }
        },
        destination: {
            css: {
                zIndex: 1
            },
            js: {
                zIndex: 1
            }
        },
        swipeTime: 500,
        swipeDistance: 50
    },

    initialize: function(element, items, options){
        this.setOptions(options);

        this.element = document.id(element);
        this.items = [];
        this.css = false;
        this.ready = false;
        this.current = 0;
        this.stepper = null;

        this.addItems($$(document.id(items) || items));

        // Detect swipe
        this.element.addEvent('touchstart', function(e){
            // calculate the swipe distance based on the viewport scale
            var distance = this.options.swipeDistance;
            if (screen.width && screen.height && window.innerWidth && window.innerHeight) {
                if (!window.orientation) {
                    window.orientation = 0;
                }
                switch (window.orientation) {
                    case 0:
                    case 180:
                        // portrait
                        distance = distance * (window.innerWidth / screen.width);
                        break;
                    case 90:
                    case -90:
                    case 270:
                        // landscape
                        distance = distance * (window.innerHeight / screen.height);
                        break;
                }
            }
            // setup position vars
            var origin = null;
            var destination = null;
            // set the origin position
            if (e.touches) {
                if (e.touches.length == 1) {
                    origin = {
                        x: e.touches[0].clientX,
                        y: e.touches[0].clientY,
                        t: new Date().getTime()
                    };
                }
            } else {
                origin = {
                    x: e.clientX,
                    y: e.clientY,
                    t: new Date().getTime()
                };
            }
            if (origin != null) {
                // listen to move events
                var moveListener = function(e){
                    // set the destination position
                    if (e.touches) {
                        if (e.touches.length == 1) {
                            destination = {
                                x: e.touches[0].clientX,
                                y: e.touches[0].clientY,
                                t: new Date().getTime()
                            };
                        } else {
                            destination = null;
                        }
                    } else {
                        destination = {
                            x: e.clientX,
                            y: e.clientY,
                            t: new Date().getTime()
                        };
                    }
                    // make sure we're in the timeframe and distance tolerance
                    if (origin != null && destination != null && destination.t - origin.t < this.options.swipeTime) {
                        // prevent default action
                        e.preventDefault();
                    } else {
                        // remove listeners
                        this.element.removeEvent('touchmove', moveListener);
                        this.element.removeEvent('touchend', endListener);
                        this.element.removeEvent('touchcancel', cancelListener);
                    }
                }.bind(this);
                this.element.addEvent('touchmove', moveListener);
                // listen to end events
                var endListener = function(e){
                    if (origin != null && destination != null && destination.t - origin.t < this.options.swipeTime) {
                        // prevent default action
                        e.preventDefault();
                        // get the difference in the origin and destination x postitions
                        var dx = destination.x - origin.x;
                        // get the difference in the origin and destination y postitions
                        var dy = destination.y - origin.y;
                        // check of the delta is more than the minimum swipe distance
                        var direction = null;
                        if (dx < -distance && -distance < dy < distance) {
                            direction = 'left';
                        } else if (dx > distance && -distance < dy < distance) {
                            direction = 'right';
                        } else if (dy < -distance && -distance < dx < distance) {
                            direction = 'up';
                        } else if (dy > distance && -distance < dx < distance) {
                            direction = 'down';
                        }
                        // fire a swipe event if there was a direction or a tap if there wasnt
                        if (direction != null) {
                            this.fireEvent('swipe', [this.element, {
                                origin: origin,
                                destination: destination,
                                direction: direction
                            }]);
                        } else {
                            this.fireEvent('tap', [this.element, {
                                origin: origin,
                                destination: destination
                            }]);
                        }
                    }
                    // remove listeners
                    this.element.removeEvent('touchmove', moveListener);
                    this.element.removeEvent('touchend', endListener);
                    this.element.removeEvent('touchcancel', cancelListener);
                }.bind(this);
                this.element.addEvent('touchend', endListener);
                // listen to cancel events
                var cancelListener = function(e){
                    // remove listeners
                    this.element.removeEvent('touchmove', moveListener);
                    this.element.removeEvent('touchend', endListener);
                    this.element.removeEvent('touchcancel', cancelListener);
                }.bind(this);
                this.element.addEvent('touchcancel', cancelListener);
            }
        }.bind(this));

        // test for CSS transition support
        this.useJs();
        var testListener = function(e){
            this.useCss();
        }.bind(this);
        var test = new Element('div').setPrefixedStyles({
            display: 'block',
            position: 'absolute',
            top: -1,
            left: -1,
            width: 1,
            height: 1,
            transition: 'top 1ms ease'
        }).addTransitionEndEvent(testListener).inject(document.id(document.body));
        // wait for events to be applied
        (function() {
            // move the test element
            test.setPrefixedStyle('top', -2);
            // give the event time to fire
            (function() {
                // get rid of the test element
                test.removeTransitionEndEvent(testListener).destroy();
                // reset the elements
                this.reset();
                // let the world know we're ready for action
                this.ready = true;
                this.fireEvent('ready', this.element);
            }).delay(50, this);
        }).delay(10, this);
    },

    useCss: function(){
        this.css = true;
        if (this.stepper != null) {
            this.stop().play();
        } else {
            this.stop();
        }
        return this;
    },

    useJs: function(){
        this.css = false;
        if (this.stepper != null) {
            this.stop().play();
        } else {
            this.stop();
        }
        return this;
    },

    addItems: function(){
        Array.flatten(arguments).each(function(item){
            this.items.include(item);
        }, this);
        return this;
    },

    removeItems: function(){
        return $$(Array.flatten(arguments).map(function(item){
            this.items.erase(item);
            return item;
        }, this));
    },

    reset: function(){
        if (this.items.length < 1 || this.current >= this.items.length) return this;

        // remove all existing styles and then setup the current item styles
        this.items.each(function(item){
            item.removePrefixedStyles(['transition-property', 'transition-duration', 'transition-timing-function'].append(Object.keys(this.options.origin.js)).append(Object.keys(this.options.origin.css))).setPrefixedStyles(this.css ? this.options.origin.css : this.options.origin.js).setStyle('display', 'none').get('morph').cancel();
        }, this);
        this.items[this.current].setPrefixedStyles(this.css ? this.options.current.css : this.options.current.js).setStyle('display', null);

        return this;
    },

    play: function(){
        if (!this.ready || this.items.length < 1) return this;

        // create the stepper to automcatically move
        clearInterval(this.stepper);
        this.stepper = (function(){
            this.fireEvent('step', this.element);
        }).periodical(this.options.duration, this);
        this.fireEvent('play', this.element);
        return this;
    },

    stop: function(){
        clearInterval(this.stepper);
        this.stepper = null;
        this.reset();
        this.fireEvent('stop', this.element);
        return this;
    },

    pause: function(){
        clearInterval(this.stepper);
        this.stepper = null;
        this.fireEvent('pause', this.element);
        return this;
    },

    move: function(delta){
        if (!this.ready || this.items.length < 1 || delta == 0) return this;

        // get the remainder if the delta is bigger than the items array
        remainder = delta % this.items.length;
        var previous = this.current;
        var next = previous + remainder;
        // make sure the next index isn't out of bounds
        if (next > this.items.length - 1) {
            next = next - this.items.length;
        } else if (next < 0) {
            next = next + this.items.length;
        }

        // get the next and previous items
        var nextItem = this.items[next];
        var previousItem = this.items[previous];

        // fire the move event
        this.fireEvent('move', [this.element, {
            delta: delta,
            next: nextItem,
            previous: previousItem
        }]);

        // update the current item index
        this.current = next;

        if (this.css) {

            // transition next item to current style
            this.transition(nextItem, (delta > 0) ? this.options.origin.css : this.options.destination.css, this.options.current.css, this.options.appear);
            // transition previous item to destination style
            this.transition(previousItem, null, (delta > 0) ? this.options.destination.css : this.options.origin.css, this.options.disappear);

        } else {

            // set next item origin style
            nextItem.setPrefixedStyles((delta > 0) ? this.options.origin.js : this.options.destination.js);
            // morph next item to current style
            nextItem.set('morph', this.options.appear).morph(this.options.current.js);
            // morph previous item to destination style
            previousItem.set('morph', this.options.disappear).morph((delta > 0) ? this.options.destination.js : this.options.origin.js);

        }

        return this;
    },

    transition: function(element, origin, destination, options){
        if (element.retrieve('queue', []).length > 0) {
            element.store('queue', element.retrieve('queue', []).push({origin: origin, destination: destination, options: options}));
        } else {
            // don't transition to the origin styles
            if (origin != null) {
                element.setPrefixedStyles({
                    transitionProperty: 'none'
                });
            }
            // wait for styles to be applied
            (function(){
                // set element origin style
                if (origin != null) {
                    element.setPrefixedStyles(origin);
                }
                // fire start event
                if (typeOf(options.onStart) == 'function') {
                    options.onStart.call({element: element, options: options});
                }
                // wait for styles to be applied
                (function(){
                    // start using transitions for the element
                    element.setPrefixedStyles({
                        transitionProperty: 'all',
                        transitionDuration: options.duration+'ms',
                        transitionTimingFunction: options.timingFunction
                    });
                    // stop using transitions when the element finishes transitioning
                    var transitionEndEventListener = function(e){
                        if (e.target == element) {
                            element.removeTransitionEndEvent(transitionEndEventListener);
                            // fire complete event
                            if (typeOf(options.onComplete) == 'function') {
                                options.onComplete.call({element: element, options: options});
                            }
                            // action next in queue
                            if (element.retrieve('queue', []).length > 0) {
                                var queue = element.retrieve('queue', []).shift();
                                this.transition(element, queue.origin, queue.destination, queue.options);
                            } else {
                                element.setPrefixedStyles({
                                    transitionProperty: 'none'
                                });
                            }
                        }
                    }.bind(this);
                    element.addTransitionEndEvent(transitionEndEventListener);
                    // set element destination style
                    element.setPrefixedStyles(destination);
                }).delay(10, this);
            }).delay(10, this);
        }
    }

});<|endoftext|>


<commit_before>// @flow
import { SRSpacedRepetition } from '../SRSpacedRepetition'

import moment from 'moment'
import expect, { createSpy, spyOn, isSpy } from 'expect'

export const processDataForList = (studyTasks) => {
  /**
   * 1. get only information needed for section list, in this case task name and the date
   * 2. Group tasks that belong to the same day
   * 3. sort day groups
   * 4. sort tasks inside day groups
   */
  const taskNameAndDateArray = studyTasks.map(taskNameAndDate)
  const tasksGroupedByDate = groupTasksByDate(taskNameAndDateArray)
  const sortedByDateArray = sortByDate(tasksGroupedByDate)
  const listReadyArray = prepareArrayForSectionList(sortedByDateArray)
  return listReadyArray
}

const taskNameAndDate = (singleTask) => {
    const { taskName, dates, srs } = singleTask
    const { easinessFactor, interval, repetition } = srs
    const srsData = new SRSpacedRepetition(easinessFactor, interval, repetition)
    const nextDueDate = srsData.nextDate(dates[0])
    return {
      "taskName": taskName, "nextDate": nextDueDate
    }
}

// TODO: rename to addTaskToArrayMatchingDate
// TODO: change parameter to task: {date: string, taskName: string}
const addTaskNameToArrayMatchingDate = (array: Array<object>, date: string, taskNameToAdd: string) => {
  var returnArray = [...array]
  const dateObject = new Date(date)
  var foundExistingDateGroup = false

  // search if resultArray already has object with that date
  array.forEach((item) => {
    const singleTask = item
    const { dateWithoutTime, tasksWithTimes } = singleTask
    const dateWithoutTimeObject = new Date(dateWithoutTime)

    // compare dates, if dates match the day, then group
    if(dateWithoutTimeObject.toDateString() == dateObject.toDateString()) { // adds new tasks to existing date group
      singleTask['tasksWithTimes'] = [...tasksWithTimes, {taskName: taskNameToAdd, taskDate: date}]
      foundExistingDateGroup = true
      return
    }
  })

  if (returnArray.length == 0 || !foundExistingDateGroup) {
    returnArray = [...returnArray, {dateWithoutTime: dateObject.toDateString(), tasksWithTimes: [{taskName: taskNameToAdd, taskDate: date}]}]
  }

  return returnArray
}

const groupTasksByDate = (taskWithDatesArray) => {
  var resultArray = []

  taskWithDatesArray.forEach((item) => {
    const { taskName, nextDate } = item
    resultArray = addTaskNameToArrayMatchingDate(resultArray, nextDate, taskName)
  })

  return resultArray
}

const sortByDate = (tasksGroupedByDateArray) => {
  const sortedByDateGroupArray = sortDateGroup(tasksGroupedByDateArray)
  const sortedTasksByDateArray = sortTasksByDate(sortedByDateGroupArray)
  return sortedTasksByDateArray
}

const sortDateGroup = (array: Array<{dateWithoutTime: string, tasksWithTimes: Array<{taskName: string, taskDate: string}>}>) => {
  return array.sort((a,b) => {
    return new Date(a.dateWithoutTime) - new Date(b.dateWithoutTime)
  })
}

const sortTasksByDate = (array: Array<{dateWithoutTime: string, tasksWithTimes: Array<{taskName: string, taskDate: string}>}>) => {
  var arrayCopy = [...array]

  arrayCopy.forEach((item) => {
    const { tasksWithTimes } = item
    arrayCopy['tasksWithTimes'] = tasksWithTimes.sort((a,b) => {
      return new Date(a.taskDate) - new Date(b.taskDate) // earlier time first
    })
  })

  return arrayCopy
}

//Array<{title: string, data: string}>
const prepareArrayForSectionList = (array: Array<{dateWithoutTime: string, tasksWithTimes: Array<{taskName: string, taskDate: string}>}>) => {

  var resultArray = []

  array.forEach((item) => {
    const { dateWithoutTime, tasksWithTimes } = item
    const formattedTitle = formatDateForTitle(dateWithoutTime)

    var dataArray = []
    tasksWithTimes.forEach((taskItem) => {
      const { taskName } = taskItem
      dataArray = [...dataArray, taskName]
    })

    resultArray = [...resultArray, {title: formattedTitle, data: dataArray}]
  })

  return resultArray
}

const formatDateForTitle = (date: string) => {
  const dateObject = new Date(date),
    month = moment(dateObject).format('MMMM')
  return dateObject.getDate() + " " + month
}

/////////////////////////////////////////////
/////////////////////////////////////////////
/////////////////////////////////////////////

const testTaskNameAndDate = () => {
  const beforeState = {
    "id": "5srtvsvev",
    "taskName": "ませんか",
    "notes": "efefasdad",
    "dates": ["July 17, 2017 11:13:00"],
    "ratingHistory": [],
    "srs": {
      "easinessFactor": 2.5,
      "interval": 0,
      "repetition": 0
    }
  }

  const expectedState = {
    id: '5srtvsvev',
    taskName: 'ませんか',
    notes: 'efefasdad',
    nextDate: new Date('July 17, 2017 11:13:00').toString()
  }

  expect(taskNameAndDate(beforeState)).toEqual(expectedState)
}
testTaskNameAndDate()

const testSortDateGroup = () => {
  const beforeState = [
    {dateWithoutTime: 'Wed Jul 19 2017', tasksWithTimes: []},
    {dateWithoutTime: 'Sat Jul 29 2017', tasksWithTimes: []},
    {dateWithoutTime: 'Tue Jul 25 2017', tasksWithTimes: []},
    {dateWithoutTime: 'Thu Jul 20 2017', tasksWithTimes: []}
  ]
  const expectedState = [
    {dateWithoutTime: 'Wed Jul 19 2017', tasksWithTimes: []},
    {dateWithoutTime: 'Thu Jul 20 2017', tasksWithTimes: []},
    {dateWithoutTime: 'Tue Jul 25 2017', tasksWithTimes: []},
    {dateWithoutTime: 'Sat Jul 29 2017', tasksWithTimes: []},
  ]
  expect(sortDateGroup(beforeState)).toEqual(expectedState)
}
testSortDateGroup()

const testSortTasksByDate = () => {
  const beforeState = [
    {dateWithoutTime: 'Wed Jul 19 2017', tasksWithTimes: [
      {taskName: 'san', taskDate: 'July 19, 2017 11:14:00'},
      {taskName: 'ni', taskDate: 'July 19, 2017 22:23:00'},
      {taskName: 'ichi', taskDate: 'July 19, 2017 08:34:00'},
      {taskName: 'yon', taskDate: 'July 19, 2017 17:35:00'}
    ]},
    {dateWithoutTime: 'Thu Jul 20 2017', tasksWithTimes: [
      {taskName: 'go', taskDate: 'July 20, 2017 17:13:00'},
      {taskName: 'roku', taskDate: 'July 20, 2017 11:13:00'}
    ]}
  ]
  const expectedState = [
    {dateWithoutTime: 'Wed Jul 19 2017', tasksWithTimes: [
      {taskName: 'ichi', taskDate: 'July 19, 2017 08:34:00'},
      {taskName: 'san', taskDate: 'July 19, 2017 11:14:00'},
      {taskName: 'yon', taskDate: 'July 19, 2017 17:35:00'},
      {taskName: 'ni', taskDate: 'July 19, 2017 22:23:00'}
    ]},
    {dateWithoutTime: 'Thu Jul 20 2017', tasksWithTimes: [
      {taskName: 'roku', taskDate: 'July 20, 2017 11:13:00'},
      {taskName: 'go', taskDate: 'July 20, 2017 17:13:00'}
    ]}
  ]
  expect(sortTasksByDate(beforeState)).toEqual(expectedState)
}
testSortTasksByDate()

const testFormatDateForTitle = () => {
  expect(formatDateForTitle('July 19, 2017 08:34:00')).toEqual('19 July')
  expect(formatDateForTitle('July 4, 2017')).toEqual('4 July')
}
testFormatDateForTitle()

const testPrepareArrayForSectionList = () => {
  const beforeState = [
    {dateWithoutTime: 'Wed Jul 19 2017', tasksWithTimes: [
      {taskName: 'ichi', taskDate: 'July 19, 2017 08:34:00'},
      {taskName: 'san', taskDate: 'July 19, 2017 11:14:00'},
      {taskName: 'yon', taskDate: 'July 19, 2017 17:35:00'},
      {taskName: 'ni', taskDate: 'July 19, 2017 22:23:00'}
    ]},
    {dateWithoutTime: 'Thu Jul 20 2017', tasksWithTimes: [
      {taskName: 'roku', taskDate: 'July 20, 2017 11:13:00'},
      {taskName: 'go', taskDate: 'July 20, 2017 17:13:00'}
    ]}
  ]
  const expectedState = [
    {title: '19 July', data: [
      'ichi',
      'san',
      'yon',
      'ni',
    ]},
    {title: '20 July', data: [
      'roku',
      'go',
    ]}
  ]
  expect(prepareArrayForSectionList(beforeState)).toEqual(expectedState)
}
testPrepareArrayForSectionList()

const testAddTaskNameToArrayMatchingDateWithEmptyArray = () => {
  const beforeState = []
  const afterState = addTaskNameToArrayMatchingDate(beforeState, 'July 19, 2017 11:13:00', 'hello')
  const expectedState = [{dateWithoutTime: 'Wed Jul 19 2017', tasksWithTimes: [{taskName: 'hello', taskDate: 'July 19, 2017 11:13:00'}]}]
  expect(afterState).toEqual(expectedState)
}
const testAddTaskNameToArrayMatchingDateWithDifferentDate = () => {
  const beforeState = [{dateWithoutTime: 'Wed Jul 19 2017', tasksWithTimes: [{taskName: 'hello', taskDate: 'July 19, 2017 11:13:00'}]}]
  const afterState = addTaskNameToArrayMatchingDate(beforeState, 'July 20, 2017 11:13:00', 'bye')
  const expectedState = [
    {dateWithoutTime: 'Wed Jul 19 2017', tasksWithTimes: [{taskName: 'hello', taskDate: 'July 19, 2017 11:13:00'}]},
    {dateWithoutTime: 'Thu Jul 20 2017', tasksWithTimes: [{taskName: 'bye', taskDate: 'July 20, 2017 11:13:00'}]}
  ]
  expect(afterState).toEqual(expectedState)
}
const testAddTaskNameToArrayMatchingDateWithSameDate = () => {
  const beforeState = [{dateWithoutTime: 'Wed Jul 19 2017', tasksWithTimes: [{taskName: 'hello', taskDate: 'July 19, 2017 11:13:00'}]}]
  const afterState = addTaskNameToArrayMatchingDate(beforeState, 'July 19, 2017 13:13:00', 'hola')
  const expectedState = [
    {dateWithoutTime: 'Wed Jul 19 2017', tasksWithTimes:
    [
      {taskName: 'hello', taskDate: 'July 19, 2017 11:13:00'},
      {taskName: 'hola', taskDate: 'July 19, 2017 13:13:00'}
    ]}]
  expect(afterState).toEqual(expectedState)
}

const testAddTaskNameToArrayMatchingDateWithSameDateAtEndOfArray = () => {
  const beforeState = [
    {dateWithoutTime: 'Wed Jul 19 2017', tasksWithTimes: [
      {taskName: 'hello', taskDate: 'July 19, 2017 11:13:00'}
    ]},
    {dateWithoutTime: 'Fri Jul 20 2017', tasksWithTimes: [
      {taskName: 'TGIF', taskDate: 'July 20, 2017 11:13:00'}
    ]}
  ]
  const afterState = addTaskNameToArrayMatchingDate(beforeState, 'July 20, 2017 13:13:00', 'Leeroy Jenkins')
  const expectedState = [
    {dateWithoutTime: 'Wed Jul 19 2017', tasksWithTimes: [
      {taskName: 'hello', taskDate: 'July 19, 2017 11:13:00'}
    ]},
    {dateWithoutTime: 'Fri Jul 20 2017', tasksWithTimes: [
      {taskName: 'TGIF', taskDate: 'July 20, 2017 11:13:00'},
      {taskName: 'Leeroy Jenkins', taskDate: 'July 20, 2017 13:13:00'}
    ]}
  ]
  expect(afterState).toEqual(expectedState)
}

testAddTaskNameToArrayMatchingDateWithEmptyArray()
testAddTaskNameToArrayMatchingDateWithDifferentDate()
testAddTaskNameToArrayMatchingDateWithSameDate()
testAddTaskNameToArrayMatchingDateWithSameDateAtEndOfArray()
<commit_msg>Updated SRDataPresenter to pass more item properties.<commit_after>// @flow
import { SRSpacedRepetition } from '../SRSpacedRepetition'

import moment from 'moment'
import expect, { createSpy, spyOn, isSpy } from 'expect'

//Array<{title: string, data: string}>
export const processDataForList = (studyTasks) => {
  /**
   * 1. take object that has _at least_ the properties we need { id, taskName, notes, dates, srs }
   * 2. Group tasks that belong to the same day
   * 3. sort day groups
   * 4. sort tasks inside day groups
   */
  const itemArray = studyTasks.map(itemWithCalculatedDueDate)
  const itemsGroupedByDate = groupItemsByDate(itemArray)
  const sortedByDateArray = sortByDate(itemsGroupedByDate)
  const listReadyArray = prepareArrayForSectionList(sortedByDateArray)
  return listReadyArray
}

const itemWithCalculatedDueDate = (singleTask) => {
    const { id, taskName, notes, dates, srs } = singleTask
    const { easinessFactor, interval, repetition } = srs
    const srsData = new SRSpacedRepetition(easinessFactor, interval, repetition)
    const nextDueDate = srsData.nextDate(dates[0])
    return {
      id: id,
      taskName: taskName,
      notes: notes,
      date: nextDueDate
    }
}

const groupItemsByDate = (itemArray) => {
  var resultArray = []

  itemArray.forEach((item) => {
    const { id, taskName, notes, date } = item
    resultArray = addItemToArrayMatchingDate(resultArray, item)
  })

  return resultArray
}

const addItemToArrayMatchingDate = (array, item) => {
  var arrayCopy = [...array]
  const { id, taskName, notes, date } = item
  const dateObject = new Date(date)
  var foundExistingDateGroup = false

  // search if resultArray already has object with that date
  arrayCopy.forEach((section) => {
    const { dateGroup, items } = section
    const dateWithoutTimeObject = new Date(dateGroup)

    // compare dates, if dates match the day, then group
    if(dateWithoutTimeObject.toDateString() == dateObject.toDateString()) { // adds new tasks to existing date group
      section.items = [...items, {id: id, taskName: taskName, notes: notes, date: date}]
      foundExistingDateGroup = true
      return
    }
  })

  // creates new date group
  if (arrayCopy.length == 0 || !foundExistingDateGroup) {
    arrayCopy = [...arrayCopy, {
      dateGroup: dateObject.toDateString(),
      items: [{id: id, taskName: taskName, notes: notes, date: date}]
    }]
  }

  return arrayCopy
}

const sortByDate = (itemsGroupedByDateArray) => {
  const sortedByDateGroupArray = sortDateGroup(itemsGroupedByDateArray)
  const sortedTasksByDateArray = sortTasksByDate(sortedByDateGroupArray)
  return sortedTasksByDateArray
}

const sortDateGroup = (array) => {
  return array.sort((a,b) => {
    return new Date(a.dateGroup) - new Date(b.dateGroup)
  })
}

const sortTasksByDate = (array) => {
  var arrayCopy = [...array]

  arrayCopy.forEach((section) => {
    const { items } = section
    arrayCopy.items = items.sort((a,b) => {
      return new Date(a.date) - new Date(b.date) // earlier time first
    })
  })

  return arrayCopy
}

const prepareArrayForSectionList = (array) => {
  var resultArray = []

  array.forEach((section) => {
    const { dateGroup, items } = section
    const formattedTitle = formatDateForTitle(dateGroup)

    var dataArray = []
    items.forEach((taskItem) => {
      const { id, taskName, notes } = taskItem
      dataArray = [...dataArray, {id: id, taskName: taskName, notes: notes}]
    })

    resultArray = [...resultArray, {title: formattedTitle, data: dataArray}]
  })

  return resultArray
}

const formatDateForTitle = (date: string) => {
  const dateObject = new Date(date),
    month = moment(dateObject).format('MMMM')
  return dateObject.getDate() + " " + month
}

/////////////////////////////////////////////
/////////////////////////////////////////////
/////////////////////////////////////////////

const testTaskNameAndDate = () => {
  const beforeState = {
    "id": "5srtvsvev",
    "taskName": "ませんか",
    "notes": "efefasdad",
    "dates": ["July 17, 2017 11:13:00"],
    "ratingHistory": [],
    "srs": {
      "easinessFactor": 2.5,
      "interval": 0,
      "repetition": 0
    }
  }

  const expectedState = {
    id: '5srtvsvev',
    taskName: 'ませんか',
    notes: 'efefasdad',
    date: new Date('July 17, 2017 11:13:00').toString()
  }

  expect(itemWithCalculatedDueDate(beforeState)).toEqual(expectedState)
}
testTaskNameAndDate()

const testGroupItemsByDate = () => {
  const beforeState = [
    { "id": "5srtvsvev",
      "taskName": "ませんか",
      "notes": "",
      "date": "July 17, 2017 11:13:00",
    },
    { "id": "vtsrvtrtv",
      "taskName": "ましょう・ましょうか",
      "notes": "",
      "date": "July 17, 2017 11:13:00",
    },
    { "id": "jdb56srv5",
      "taskName": "てはいけません",
      "notes": "",
      "date": "July 16, 2017 10:13:00",
    },
    { "id": "S7SM6ND6BVD",
      "taskName": "何も",
      "notes": "",
      "date": "July 16, 2017 11:13:00",
    },
    { "id": "D6UD6MU",
      "taskName": "どこかに・どこにも",
      "notes": "",
      "date": "July 18, 2017 11:13:00",
    },
  ]

  const expectedState = [

  ]

  expect(groupItemsByDate(beforeState)).toEqual(expectedState)
}
// testGroupItemsByDate()

const testSortDateGroup = () => {
  const beforeState = [
    {dateGroup: 'Wed Jul 19 2017', items: []},
    {dateGroup: 'Sat Jul 29 2017', items: []},
    {dateGroup: 'Tue Jul 25 2017', items: []},
    {dateGroup: 'Thu Jul 20 2017', items: []}
  ]
  const expectedState = [
    {dateGroup: 'Wed Jul 19 2017', items: []},
    {dateGroup: 'Thu Jul 20 2017', items: []},
    {dateGroup: 'Tue Jul 25 2017', items: []},
    {dateGroup: 'Sat Jul 29 2017', items: []},
  ]
  expect(sortDateGroup(beforeState)).toEqual(expectedState)
}
testSortDateGroup()

const testSortTasksByDate = () => {
  const beforeState = [
    {dateGroup: 'Wed Jul 19 2017', items: [
      {taskName: 'san', date: 'July 19, 2017 11:14:00'},
      {taskName: 'ni', date: 'July 19, 2017 22:23:00'},
      {taskName: 'ichi', date: 'July 19, 2017 08:34:00'},
      {taskName: 'yon', date: 'July 19, 2017 17:35:00'}
    ]},
    {dateGroup: 'Thu Jul 20 2017', items: [
      {taskName: 'go', date: 'July 20, 2017 17:13:00'},
      {taskName: 'roku', date: 'July 20, 2017 11:13:00'}
    ]}
  ]
  const expectedState = [
    {dateGroup: 'Wed Jul 19 2017', items: [
      {taskName: 'ichi', date: 'July 19, 2017 08:34:00'},
      {taskName: 'san', date: 'July 19, 2017 11:14:00'},
      {taskName: 'yon', date: 'July 19, 2017 17:35:00'},
      {taskName: 'ni', date: 'July 19, 2017 22:23:00'}
    ]},
    {dateGroup: 'Thu Jul 20 2017', items: [
      {taskName: 'roku', date: 'July 20, 2017 11:13:00'},
      {taskName: 'go', date: 'July 20, 2017 17:13:00'}
    ]}
  ]
  expect(sortTasksByDate(beforeState)).toEqual(expectedState)
}
testSortTasksByDate()

const testFormatDateForTitle = () => {
  expect(formatDateForTitle('July 19, 2017 08:34:00')).toEqual('19 July')
  expect(formatDateForTitle('July 4, 2017')).toEqual('4 July')
}
testFormatDateForTitle()

const testPrepareArrayForSectionList = () => {
  const beforeState = [
    {dateGroup: 'Wed Jul 19 2017', items: [
      {taskName: 'ichi', date: 'July 19, 2017 08:34:00'},
      {taskName: 'san', date: 'July 19, 2017 11:14:00'},
      {taskName: 'yon', date: 'July 19, 2017 17:35:00'},
      {taskName: 'ni', date: 'July 19, 2017 22:23:00'}
    ]},
    {dateGroup: 'Thu Jul 20 2017', items: [
      {taskName: 'roku', date: 'July 20, 2017 11:13:00'},
      {taskName: 'go', date: 'July 20, 2017 17:13:00'}
    ]}
  ]
  const expectedState = [
    {title: '19 July', data: [
      'ichi',
      'san',
      'yon',
      'ni',
    ]},
    {title: '20 July', data: [
      'roku',
      'go',
    ]}
  ]
  expect(prepareArrayForSectionList(beforeState)).toEqual(expectedState)
}
// testPrepareArrayForSectionList()

const testAddTaskNameToArrayMatchingDateWithEmptyArray = () => {
  const beforeState = []
  const item =  {
    id: 'ss4bcrsc',
    taskName: 'hello',
    notes: 'TiV7A!a]x',
    date: 'July 19, 2017 11:13:00'
  }
  const expectedState = [{
    dateGroup: 'Wed Jul 19 2017',
    items: [{
      id: 'ss4bcrsc',
      taskName: 'hello',
      notes: 'TiV7A!a]x',
      date: 'July 19, 2017 11:13:00'
    }]
  }]
  expect(addItemToArrayMatchingDate(beforeState, item)).toEqual(expectedState)
}
testAddTaskNameToArrayMatchingDateWithEmptyArray()

const testAddTaskNameToArrayMatchingDateWithDifferentDate = () => {
  const beforeState = [{
    dateGroup: 'Wed Jul 19 2017',
    items: [{
      id: 'ss4bcrsc',
      taskName: 'hello',
      notes: 'TiV7A!a]x',
      date: 'July 19, 2017 11:13:00'
    }]
  }]
  const item = {
    id: 'Q$Kv&6lvzFp%tAh',
    taskName: 'Dv%^002T8',
    notes: 'uY3!2&HGLg6dJ]rq*[uE',
    date: 'July 25, 2017 11:13:00'
  }
  const expectedState = [{
    dateGroup: 'Wed Jul 19 2017',
    items: [{
      id: 'ss4bcrsc',
      taskName: 'hello',
      notes: 'TiV7A!a]x',
      date: 'July 19, 2017 11:13:00'
    }]
  },
  {
    dateGroup: 'Tue Jul 25 2017',
    items: [{
      id: 'Q$Kv&6lvzFp%tAh',
      taskName: 'Dv%^002T8',
      notes: 'uY3!2&HGLg6dJ]rq*[uE',
      date: 'July 25, 2017 11:13:00'
    }]
  }]
  expect(addItemToArrayMatchingDate(beforeState, item)).toEqual(expectedState)
}
testAddTaskNameToArrayMatchingDateWithDifferentDate()

const testAddTaskNameToArrayMatchingDateWithSameDate = () => {
  const beforeState = [{dateGroup: 'Wed Jul 19 2017', items: [{taskName: 'hello', date: 'July 19, 2017 11:13:00'}]}]
  const afterState = addItemToArrayMatchingDate(beforeState, 'July 19, 2017 13:13:00', 'hola')
  const expectedState = [
    {dateGroup: 'Wed Jul 19 2017', items:
    [
      {taskName: 'hello', date: 'July 19, 2017 11:13:00'},
      {taskName: 'hola', date: 'July 19, 2017 13:13:00'}
    ]}]
  expect(afterState).toEqual(expectedState)
}
// testAddTaskNameToArrayMatchingDateWithSameDate()

const testAddTaskNameToArrayMatchingDateWithSameDateAtEndOfArray = () => {
  const beforeState = [
    {dateGroup: 'Wed Jul 19 2017', items: [
      {taskName: 'hello', date: 'July 19, 2017 11:13:00'}
    ]},
    {dateGroup: 'Fri Jul 20 2017', items: [
      {taskName: 'TGIF', date: 'July 20, 2017 11:13:00'}
    ]}
  ]
  const afterState = addItemToArrayMatchingDate(beforeState, 'July 20, 2017 13:13:00', 'Leeroy Jenkins')
  const expectedState = [
    {dateGroup: 'Wed Jul 19 2017', items: [
      {taskName: 'hello', date: 'July 19, 2017 11:13:00'}
    ]},
    {dateGroup: 'Fri Jul 20 2017', items: [
      {taskName: 'TGIF', date: 'July 20, 2017 11:13:00'},
      {taskName: 'Leeroy Jenkins', date: 'July 20, 2017 13:13:00'}
    ]}
  ]
  expect(afterState).toEqual(expectedState)
}
// testAddTaskNameToArrayMatchingDateWithSameDateAtEndOfArray()
<|endoftext|>


<commit_before><commit_msg>Update closure-example.js<commit_after><|endoftext|>


<commit_before>import Component from '../../common/Component';
import SubtreeRetainer from '../../common/utils/SubtreeRetainer';
import Dropdown from '../../common/components/Dropdown';
import PostControls from '../utils/PostControls';
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';

/**
 * The `Post` component displays a single post. The basic post template just
 * includes a controls dropdown; subclasses must implement `content` and `attrs`
 * methods.
 *
 * ### Props
 *
 * - `post`
 *
 * @abstract
 */
export default class Post extends Component {
  init() {
    this.loading = false;

    /**
     * Set up a subtree retainer so that the post will not be redrawn
     * unless new data comes in.
     *
     * @type {SubtreeRetainer}
     */
    this.subtree = new SubtreeRetainer(
      () => this.props.post.freshness,
      () => {
        const user = this.props.post.user();
        return user && user.freshness;
      },
      () => this.controlsOpen
    );
  }

  view() {
    const attrs = this.attrs();

    attrs.className = this.classes(attrs.className).join(' ');

    return (

        {this.subtree.retain() || (() => {
          const controls = PostControls.controls(this.props.post, this).toArray();

          return (

              {this.content()}

                  {listItems(this.actionItems().toArray())}
                  {controls.length ?

                    <Dropdown
                      className="Post-controls"
                      buttonClassName="Button Button--icon Button--flat"
                      menuClassName="Dropdown-menu--right"
                      icon="fas fa-ellipsis-h"
                      onshow={() => this.$('.Post-actions').addClass('open')}
                      onhide={() => this.$('.Post-actions').removeClass('open')}>
                      {controls}
                    </Dropdown>

 : ''}

{listItems(this.footerItems().toArray())}

          );
        })()}

    );
  }

  config(isInitialized) {
    const $actions = this.$('.Post-actions');
    const $controls = this.$('.Post-controls');

    $actions.toggleClass('open', $controls.hasClass('open'));
  }

  /**
   * Get attributes for the post element.
   *
   * @return {Object}
   */
  attrs() {
    return {};
  }

  /**
   * Get the post's content.
   *
   * @return {Array}
   */
  content() {
    return [];
  }

  /**
   * Get the post's classes.
   *
   * @param string classes
   * @returns {string[]}
   */
  classes(existing) {
    let classes = (existing || '').split(' ').concat(['Post']);

    if (this.loading) {
      classes.push('Post--loading');
    }

    if (this.props.post.user() === app.session.user) {
      classes.push('Post--by-actor');
    }

    if (app.current.discussion && app.current.discussion.attribute('startUserId') == this.props.post.user().id()) {
      classes.push('Post--by-start-user')
    }

    return classes;
  }

  /**
   * Build an item list for the post's actions.
   *
   * @return {ItemList}
   */
  actionItems() {
    return new ItemList();
  }

  /**
   * Build an item list for the post's footer.
   *
   * @return {ItemList}
   */
  footerItems() {
    return new ItemList();
  }
}
<commit_msg>fixed an issue with Post--by-start-user for discussions that contain posts of deleted users<commit_after>import Component from '../../common/Component';
import SubtreeRetainer from '../../common/utils/SubtreeRetainer';
import Dropdown from '../../common/components/Dropdown';
import PostControls from '../utils/PostControls';
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';

/**
 * The `Post` component displays a single post. The basic post template just
 * includes a controls dropdown; subclasses must implement `content` and `attrs`
 * methods.
 *
 * ### Props
 *
 * - `post`
 *
 * @abstract
 */
export default class Post extends Component {
  init() {
    this.loading = false;

    /**
     * Set up a subtree retainer so that the post will not be redrawn
     * unless new data comes in.
     *
     * @type {SubtreeRetainer}
     */
    this.subtree = new SubtreeRetainer(
      () => this.props.post.freshness,
      () => {
        const user = this.props.post.user();
        return user && user.freshness;
      },
      () => this.controlsOpen
    );
  }

  view() {
    const attrs = this.attrs();

    attrs.className = this.classes(attrs.className).join(' ');

    return (

        {this.subtree.retain() || (() => {
          const controls = PostControls.controls(this.props.post, this).toArray();

          return (

              {this.content()}

                  {listItems(this.actionItems().toArray())}
                  {controls.length ?

                    <Dropdown
                      className="Post-controls"
                      buttonClassName="Button Button--icon Button--flat"
                      menuClassName="Dropdown-menu--right"
                      icon="fas fa-ellipsis-h"
                      onshow={() => this.$('.Post-actions').addClass('open')}
                      onhide={() => this.$('.Post-actions').removeClass('open')}>
                      {controls}
                    </Dropdown>

 : ''}

{listItems(this.footerItems().toArray())}

          );
        })()}

    );
  }

  config(isInitialized) {
    const $actions = this.$('.Post-actions');
    const $controls = this.$('.Post-controls');

    $actions.toggleClass('open', $controls.hasClass('open'));
  }

  /**
   * Get attributes for the post element.
   *
   * @return {Object}
   */
  attrs() {
    return {};
  }

  /**
   * Get the post's content.
   *
   * @return {Array}
   */
  content() {
    return [];
  }

  /**
   * Get the post's classes.
   *
   * @param string classes
   * @returns {string[]}
   */
  classes(existing) {
    let classes = (existing || '').split(' ').concat(['Post']);

    const user = this.props.post.user();

    if (this.loading) {
      classes.push('Post--loading');
    }

    if (user && user === app.session.user) {
      classes.push('Post--by-actor');
    }

    if (user && app.current.discussion && app.current.discussion.attribute('startUserId') == user.id()) {
      classes.push('Post--by-start-user')
    }

    return classes;
  }

  /**
   * Build an item list for the post's actions.
   *
   * @return {ItemList}
   */
  actionItems() {
    return new ItemList();
  }

  /**
   * Build an item list for the post's footer.
   *
   * @return {ItemList}
   */
  footerItems() {
    return new ItemList();
  }
}
<|endoftext|>


<commit_before>'use strict'

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

const log       = require ('ololog')
    , ansi      = require ('ansicolor').nice
    , chai      = require ('chai')
    , expect    = chai.expect
    , assert    = chai.assert

/*  ------------------------------------------------------------------------ */

module.exports = (exchange, ticker, method, symbol) => {

    const format = {
        'symbol':       'ETH/BTC',
        'info':          {},
        'timestamp':     1234567890,
        'datetime':     '2017-09-01T00:00:00',
        'high':          1.234, // highest price
        'low':           1.234, // lowest price
        'bid':           1.234, // current best bid (buy) price
        'bidVolume':     1.234, // current best bid (buy) amount (may be missing or undefined)
        'ask':           1.234, // current best ask (sell) price
        'askVolume':     1.234, // current best ask (sell) amount (may be missing or undefined)
        'vwap':          1.234, // volume weighed average price
        'open':          1.234, // opening price
        'close':         1.234, // price of last trade (closing price for current period)
        'last':          1.234, // same as `close`, duplicated for convenience
        'previousClose': 1.234, // closing price for the previous period
        'change':        1.234, // absolute change, `last - open`
        'percentage':    1.234, // relative change, `(change/open) * 100`
        'average':       1.234, // average price, `(last + open) / 2`
        'baseVolume':    1.234, // volume of base currency
        'quoteVolume':   1.234, // volume of quote currency
    }

    expect (ticker).to.have.all.keys (format)

    const keys = [ 'datetime', 'timestamp', 'high', 'low', 'bid', 'ask', 'baseVolume', 'quoteVolume', 'vwap' ]

    assert (!('first' in ticker), '`first` field leftover in ' + exchange.id)
    assert (ticker['last'] === ticker['close'], '`last` != `close` in ' + exchange.id)

    // log (ticker)

    keys.forEach (key => assert (key in ticker))

    const { high, low, vwap, baseVolume, quoteVolume } = ticker

    // this assert breaks QuadrigaCX sometimes... still investigating
    // if (vwap)
    //     assert (vwap >= low && vwap <= high)

    /*
    if (baseVolume && quoteVolume && high && low) {
        assert (quoteVolume >= baseVolume * low) // this assertion breaks therock
        assert (quoteVolume <= baseVolume * high)
    }
    */

    if (baseVolume && vwap)
        assert (quoteVolume)

    if (quoteVolume && vwap)
        assert (baseVolume)

    log (symbol.green, 'ticker',
        ticker['datetime'],
        ... (keys.map (key =>
            key + ': ' + ticker[key])))

    if ((exchange.id !== 'coinmarketcap') && (exchange.id !== 'xbtce'))
        if (ticker['bid'] && ticker['ask'])
            assert (ticker['bid'] <= ticker['ask'])
}<commit_msg>less logging in test.ticker.js<commit_after>'use strict'

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

const log       = require ('ololog')
    , ansi      = require ('ansicolor').nice
    , chai      = require ('chai')
    , expect    = chai.expect
    , assert    = chai.assert

/*  ------------------------------------------------------------------------ */

module.exports = (exchange, ticker, method, symbol) => {

    const format = {
        'symbol':       'ETH/BTC',
        'info':          {},
        'timestamp':     1234567890,
        'datetime':     '2017-09-01T00:00:00',
        'high':          1.234, // highest price
        'low':           1.234, // lowest price
        'bid':           1.234, // current best bid (buy) price
        'bidVolume':     1.234, // current best bid (buy) amount (may be missing or undefined)
        'ask':           1.234, // current best ask (sell) price
        'askVolume':     1.234, // current best ask (sell) amount (may be missing or undefined)
        'vwap':          1.234, // volume weighed average price
        'open':          1.234, // opening price
        'close':         1.234, // price of last trade (closing price for current period)
        'last':          1.234, // same as `close`, duplicated for convenience
        'previousClose': 1.234, // closing price for the previous period
        'change':        1.234, // absolute change, `last - open`
        'percentage':    1.234, // relative change, `(change/open) * 100`
        'average':       1.234, // average price, `(last + open) / 2`
        'baseVolume':    1.234, // volume of base currency
        'quoteVolume':   1.234, // volume of quote currency
    }

    expect (ticker).to.have.all.keys (format)

    const keys = [ 'datetime', 'timestamp', 'high', 'low', 'bid', 'ask', 'baseVolume', 'quoteVolume', 'vwap' ]

    assert (!('first' in ticker), '`first` field leftover in ' + exchange.id)
    assert (ticker['last'] === ticker['close'], '`last` != `close` in ' + exchange.id)

    // log (ticker)

    keys.forEach (key => assert (key in ticker))

    const { high, low, vwap, baseVolume, quoteVolume } = ticker

    // this assert breaks QuadrigaCX sometimes... still investigating
    // if (vwap)
    //     assert (vwap >= low && vwap <= high)

    /*
    if (baseVolume && quoteVolume && high && low) {
        assert (quoteVolume >= baseVolume * low) // this assertion breaks therock
        assert (quoteVolume <= baseVolume * high)
    }
    */

    if (baseVolume && vwap)
        assert (quoteVolume)

    if (quoteVolume && vwap)
        assert (baseVolume)

    // log (symbol.green, 'ticker',
    //     ticker['datetime'],
    //     ... (keys.map (key =>
    //         key + ': ' + ticker[key])))

    if ((exchange.id !== 'coinmarketcap') && (exchange.id !== 'xbtce'))
        if (ticker['bid'] && ticker['ask'])
            assert (ticker['bid'] <= ticker['ask'])
}<|endoftext|>


<commit_before>(function () {
    'use strict';

    (function () {
        // requestAnimationFrame polyfill

        var lastTime = 0;

        ['moz', 'webkit', 'o'].forEach(function (vendorPrefix) {
            if (window.requestAnimationFrame) {
                return;
            }
            window.requestAnimationFrame = window[vendorPrefix + 'RequestAnimationFrame'];
            window.cancelAnimationFrame = window[vendorPrefix + 'CancelAnimationFrame'];
        });

        if (!window.requestAnimationFrame) {
            window.requestAnimationFrame = function (callback) {
                var currTime = Date.now(); // not supported in IE<9!
                var timeToCall = Math.max(0, 16 - (currTime - lastTime));
                var id = window.setTimeout(function () {
                        callback(currTime + timeToCall);
                    },
                    timeToCall);
                lastTime = currTime + timeToCall;
                return id;
            };

            window.cancelAnimationFrame = function (id) {
                clearTimeout(id);
            };
        }
    })();
})();
<commit_msg>requestAnimationFrame polyfill update<commit_after>(function () {
    'use strict';

    (function () {
        // requestAnimationFrame polyfill

        var lastTime = 0;

        ['webkit', 'moz', 'o'].forEach(function (vendorPrefix) {
            if (window.requestAnimationFrame) {
                return;
            }
            window.requestAnimationFrame = window[vendorPrefix + 'RequestAnimationFrame'];
            window.cancelAnimationFrame = window[vendorPrefix + 'CancelAnimationFrame'];
        });

        if (!window.requestAnimationFrame) {
            window.requestAnimationFrame = function (callback) {
                var currTime = Date.now(); // not supported in IE<9!
                var timeToCall = Math.max(0, 16 - (currTime - lastTime));
                var id = window.setTimeout(function () {
                        callback(currTime + timeToCall);
                    },
                    timeToCall);
                lastTime = currTime + timeToCall;
                return id;
            };

            window.cancelAnimationFrame = function (id) {
                clearTimeout(id);
            };
        }
    })();
})();
<|endoftext|>


<commit_before>/**
 * Js-coding-standards
 *
 * @author      Robert Rossmann <rr.rossmann@me.com>
 * @copyright   2016 STRV
 * @license     http://choosealicense.com/licenses/bsd-3-clause  BSD-3-Clause License
 */

'use strict'

module.exports = {

  extends: [
    './recommended.js',
    './accessibility.js',
  ],

  rules: {
    'operator-linebreak': 0,
    'require-jsdoc': 0,
  },

  plugins: [
    'react',
  ],

  // Configures the react plugin to treat some rules with regard to this specific React.js version
  settings: {
    react: {
      version: '15.5',
    },
  },
}
<commit_msg>Remove unused require-jsdoc override from react env<commit_after>/**
 * Js-coding-standards
 *
 * @author      Robert Rossmann <rr.rossmann@me.com>
 * @copyright   2016 STRV
 * @license     http://choosealicense.com/licenses/bsd-3-clause  BSD-3-Clause License
 */

'use strict'

module.exports = {

  extends: [
    './recommended.js',
    './accessibility.js',
  ],

  rules: {
    'operator-linebreak': 0,
  },

  plugins: [
    'react',
  ],

  // Configures the react plugin to treat some rules with regard to this specific React.js version
  settings: {
    react: {
      version: '15.5',
    },
  },
}
<|endoftext|>