<commit_before>/**
 This Source Code is licensed under the MIT license. If a copy of the
 MIT-license was not distributed with this file, You can obtain one at:
 http://opensource.org/licenses/mit-license.html.

 @author: Hans van den Akker (mysim1)
 @license MIT
 @copyright Bizboard, 2015

 */

import _                from 'lodash';
import FlexScrollView   from 'famous-flex/src/FlexScrollView.js';
import {Throttler}      from 'arva-utils/Throttler.js';

export class DataBoundScrollView extends FlexScrollView {

    constructor(OPTIONS = {}) {
        super(_.extend({
            autoPipeEvents: true,
            throttleDelay: 0, /* If set to 0, no delay is added in between adding items to the DataBoundScrollView. */
            dataSource: [],
            sortingDirection: 'ascending',
            flow: true,
            flowOptions: {
                spring: {               // spring-options used when transitioning between states
                    dampingRatio: 0.8,  // spring damping ratio
                    period: 1000        // duration of the animation
                },
                insertSpec: {           // render-spec used when inserting renderables
                    opacity: 0          // start opacity is 0, causing a fade-in effect,
                }
            }
        }, OPTIONS));

        this.isGrouped = this.options.groupBy != null;
        this.isDescending = this.options.sortingDirection === 'descending';
        this.throttler = new Throttler(this.options.throttleDelay, true, this);

        /* If no orderBy method is set, or it is a string field name, we set our own ordering method. */
        if (this.options.orderBy && typeof this.options.orderBy === 'string') {
            let fieldName = this.options.orderBy || 'id';
            this.options.orderBy = function (currentChild, compareChild) {
                if (this.isDescending) {
                    return currentChild[fieldName] < compareChild.data[fieldName];
                } else {
                    return currentChild[fieldName] > compareChild.data[fieldName];
                }
            }
        }

        /* If present in options.headerTemplate or options.placeholderTemplate, we build the header and placeholder elements. */
        this._addHeader();
        this._addPlaceholder();

        if (this.options.dataStore) {
            this._bindDataSource(this.options.dataStore);
        } else {
            console.log('No DataSource was set.');
        }
    }

    /**
     * Reloads the dataFilter option of the DataBoundScrollView, and verifies whether the items in the dataStore are allowed by the new filter.
     * It removes any currently visible items that aren't allowed anymore, and adds any non-visible ones that are allowed now.
     * @param {Function} newFilter New filter function to verify item visibility with.
     * @returns {void}
     */
    reloadFilter(newFilter) {
        this.options.dataFilter = newFilter;

        for (let entry of this.options.dataStore) {
            let surface = _.find(this._dataSource, (surface) => surface.dataId === entry.id);
            let alreadyExists = surface !== undefined;
            let result = newFilter(entry);

            if (result instanceof Promise) {
                result.then(function (shouldShow) { this._handleNewFilterResult(shouldShow, alreadyExists, entry); }.bind(this))
            } else {
                this._handleNewFilterResult(result, alreadyExists, entry);
            }
        }
    }

    _handleNewFilterResult(shouldShow, alreadyExists, entry) {
        if (shouldShow) {
            /* This entry should be in the view, add it if it doesn't exist yet. */
            if (!alreadyExists) {
                this._addItem(entry);
            }
        } else {
            /* This entry should not be in the view, remove if present. */
            if (alreadyExists) {
                this._removeItem(entry);
            }
        }
    }

    _findGroup(groupId) {
        return _.findIndex(this._dataSource, function (surface) {
            return surface.groupId === groupId;
        });
    }

    _findNextGroup(fromIndex) {
        let dslength = this._dataSource.length;
        for (let pos = fromIndex; pos < dslength; pos++) {
            if (this._dataSource[pos].groupId) {
                return pos;
            }
        }

        return this._dataSource.length;
    }

    _getGroupByValue(child) {
        let groupByValue = '';
        if (typeof this.options.groupBy === 'function') {
            groupByValue = this.options.groupBy(child);
        } else if (typeof this.options.groupBy === 'string') {
            groupByValue = this.options.groupBy;
        }
        return groupByValue;
    }

    _addGroupItem(groupByValue, insertIndex) {
        let newSurface = this.options.groupTemplate(groupByValue);
        newSurface.groupId = groupByValue;

        this.insert(insertIndex, newSurface);
    }

    _getInsertIndex(child, previousSiblingID = null) {
        /* By default, add item to the beginning if descending order, or at the end otherwise. */
        let firstIndex = this.header ? 1 : 0;
        let insertIndex = this.isDescending ? firstIndex : this._dataSource.length;

        /* If we have an orderBy function, find the index we should be inserting at. */
        if (this.options.orderBy && typeof this.options.orderBy === 'function') {
            let foundOrderedIndex = _.findIndex(this._dataSource, function (compareChild) {

                /* Ignore the header, placeholder, and group items. */
                if (!compareChild.isHeader && !compareChild.isPlaceholder && !compareChild.groupId) {
                    return this.options.orderBy(child, compareChild);
                }
                return false;

            }.bind(this));

            if (foundOrderedIndex !== -1) {
                insertIndex = foundOrderedIndex;
            }
        } else if (previousSiblingID) {
            /* We don't have an orderBy method, but do have a previousSiblingID we can use to find the correct insertion index. */
            let siblingIndex = _.findIndex(this._dataSource, (sibling) => sibling.dataId === previousSiblingID);
            if (siblingIndex !== -1) {
                insertIndex = siblingIndex + 1;
            }
        }

        if(this.isGrouped && insertIndex === firstIndex && this._dataSource[insertIndex] && this._dataSource[insertIndex].groupId
            && this._dataSource[insertIndex].groupId === this._getGroupByValue(child)) {
            /* We didn't get an insert index from the ordering method or a previous sibling ID, and our insert index is already occupied by
             * a group item that we need to insert after, so we'll move the index to after the group item. */
            insertIndex++;
        } else if (this.isGrouped && insertIndex > 0 && this._dataSource[insertIndex-1] && this._dataSource[insertIndex-1].groupId
            && this._dataSource[insertIndex-1].groupId !== this._getGroupByValue(child)) {
            /* The insert index is after a group item that this child does not belong to, so we'll have to decrement the index by one
             * to avoid the child being added under this group. */
            insertIndex--;
        }

        return insertIndex;
    }

    _addItem(child, previousSiblingID) {
        this._removePlaceholder();

        let insertIndex = this._getInsertIndex(child, previousSiblingID);

        /* If we're using groups, check if we need to insert a group item before this child. */
        if (this.isGrouped) {
            let groupByValue = this._getGroupByValue(child);
            let groupIndex = this._findGroup(groupByValue);
            if (groupIndex === -1) {
                /* No group of this value exists yet, so we'll need to create one. */
                this._addGroupItem(groupByValue, insertIndex);
                insertIndex++;
            }
        }

        let newSurface = this.options.itemTemplate(child);
        newSurface.dataId = child.id;
        newSurface.data = child;
        newSurface.on('click', function () {
            this._eventOutput.emit('child_click', {renderNode: newSurface, dataObject: child});
        }.bind(this));

        this.insert(insertIndex, newSurface);
    }

    _replaceItem(child) {
        let index = this._getDataSourceIndex(child.id);

        let newSurface = this.options.itemTemplate(child);
        newSurface.dataId = child.id;
        this.replace(index, newSurface);
    }

    _removeItem(child) {
        let index = _.findIndex(this._dataSource, function (surface) {
            return surface.dataId === child.id;
        });

        if (index > -1) {
            this.remove(index);
        }

        /* If we're using groups, check if we need to remove the group that this child belonged to. */
        if (this.isGrouped) {
            let groupByValue = this._getGroupByValue(child);
            let otherGroupChilds = _.findIndex(this._dataSource, function (otherChild) {
                return this._getGroupByValue(otherChild) === groupByValue;
            }.bind(this));

            if (otherGroupChilds === -1) {
                /* No more childs in this group, so let's remove the group. */
                let groupIndex = this._findGroup(groupByValue);
                if (groupIndex !== -1) {
                    this.remove(groupIndex);
                }
            }
        }

        /* The amount of items in the dataSource is subtracted with a header if present, to get the total amount of actual items in the scrollView. */
        let itemCount = this._dataSource.length - (this.header ? 1 : 0);
        if (itemCount === 0) {
            this._addPlaceholder();
        }
    }

    _moveItem(oldId, prevChildId = null) {

        let oldIndex = this._getDataSourceIndex(oldId);
        let previousSiblingIndex = this._getNextVisibleIndex(prevChildId);

        if (oldIndex !== previousSiblingIndex) {
            this.move(oldIndex, previousSiblingIndex);
        }
    }

    _addHeader() {
        if (this.options.headerTemplate && !this.header) {
            this.header = this.options.headerTemplate();
            this.header.isHeader = true;
            this.insert(0, this.header);
        }
    }

    _addPlaceholder() {
        if (this.options.placeholderTemplate && !this.placeholder) {
            let insertIndex = this.header ? 1 : 0;
            this.placeholder = this.options.placeholderTemplate();
            this.placeholder.dataId = this.placeholder.id = '_placeholder';
            this.placeholder.isPlaceholder = true;
            this.insert(insertIndex, this.placeholder);
        }
    }

    _removePlaceholder() {
        if (this.placeholder) {
            this._removeItem(this.placeholder);
            this.placeholder = null;
        }
    }

    _bindDataSource() {

        if (!this.options.dataStore || !this.options.itemTemplate) {
            console.log('Datasource and template should both be set.');
            return;
        }

        if (!this.options.template instanceof Function) {
            console.log('Template needs to be a function.');
            return;
        }

        this.options.dataStore.on('child_added', function (child, previousSiblingID) {

            if (this.options.dataFilter &&
                (typeof this.options.dataFilter === 'function')) {

                let result = this.options.dataFilter(child);

                if (result instanceof Promise) {
                    /* If the result is a Promise, show the item when that promise resolves. */
                    result.then((show) => { if (show) { this.throttler.add(() => { return this._addItem(child), previousSiblingID; }); } });
                } else if (result) {
                    /* The result is an item, so we can add it directly. */
                    this.throttler.add(() => { return this._addItem(child, previousSiblingID); });
                }
            } else {
                /* There is no dataFilter method, so we can add this child. */
                this.throttler.add(() => { return this._addItem(child, previousSiblingID); });
            }
        }.bind(this));

        this.options.dataStore.on('child_changed', function (child, previousSiblingID) {

            let changedItem = this._getDataSourceIndex(child.id);

            if (this._dataSource && changedItem < this._dataSource.length) {

                if (this.options.dataFilter &&
                    typeof this.options.dataFilter === 'function' && !this.options.dataFilter(child)) {
                    this._removeItem(child);
                } else {
                    if (changedItem === -1) {
                        this.throttler.add(() => { return this._addItem(child, previousSiblingID); });
                    } else {
                        this.throttler.add(() => { return this._replaceItem(child); });
                        this.throttler.add(() => { return this._moveItem(child.id, previousSiblingID); });
                    }
                }
            }
        }.bind(this));

        this.options.dataStore.on('child_moved', function (child, previousSibling) {
            let current = this._getDataSourceIndex(child.id);
            let previous = this._getDataSourceIndex(previousSibling);
            this.throttler.add(() => { return this._moveItem(current, previous); });
        }.bind(this));

        this.options.dataStore.on('child_removed', function (child) {
            this.throttler.add(() => { return this._removeItem(child); });
        }.bind(this));
    }

    _getDataSourceIndex(id) {
        return _.findIndex(this._dataSource, function (surface) {
            return surface.dataId === id;
        });
    }

    _getNextVisibleIndex(id) {

        let viewIndex = this._getDataSourceIndex(id);
        if (viewIndex === -1) {
            let modelIndex = _.findIndex(this.options.dataStore, function (model) {
                return model.id === id;
            });

            if (modelIndex === 0 || modelIndex === -1) {
                return this.isDescending ? this._dataSource ? this._dataSource.length - 1 : 0 : 0;
            } else {
                let nextModel = this.options.dataStore[this.isDescending ? modelIndex + 1 : modelIndex - 1];
                let nextIndex = this._getDataSourceIndex(nextModel.id);
                if (nextIndex > -1) {

                    return this.isDescending ? nextIndex === 0 ? 0 : nextIndex - 1 :
                           this._dataSource.length === nextIndex + 1 ? nextIndex : nextIndex + 1;
                } else {
                    return this._getNextVisibleIndex(nextModel.id);
                }
            }
        } else {
            return this.isDescending ? viewIndex === 0 ? 0 : viewIndex - 1 :
                   this._dataSource.length === viewIndex + 1 ? viewIndex : viewIndex + 1;
        }
    }
}<commit_msg>Added promises support for the child_changed events<commit_after>/**
 This Source Code is licensed under the MIT license. If a copy of the
 MIT-license was not distributed with this file, You can obtain one at:
 http://opensource.org/licenses/mit-license.html.

 @author: Hans van den Akker (mysim1)
 @license MIT
 @copyright Bizboard, 2015

 */

import _                from 'lodash';
import FlexScrollView   from 'famous-flex/src/FlexScrollView.js';
import {Throttler}      from 'arva-utils/Throttler.js';

export class DataBoundScrollView extends FlexScrollView {

    constructor(OPTIONS = {}) {
        super(_.extend({
            autoPipeEvents: true,
            throttleDelay: 0, /* If set to 0, no delay is added in between adding items to the DataBoundScrollView. */
            dataSource: [],
            sortingDirection: 'ascending',
            flow: true,
            flowOptions: {
                spring: {               // spring-options used when transitioning between states
                    dampingRatio: 0.8,  // spring damping ratio
                    period: 1000        // duration of the animation
                },
                insertSpec: {           // render-spec used when inserting renderables
                    opacity: 0          // start opacity is 0, causing a fade-in effect,
                }
            }
        }, OPTIONS));

        this.isGrouped = this.options.groupBy != null;
        this.isDescending = this.options.sortingDirection === 'descending';
        this.throttler = new Throttler(this.options.throttleDelay, true, this);

        /* If no orderBy method is set, or it is a string field name, we set our own ordering method. */
        if (this.options.orderBy && typeof this.options.orderBy === 'string') {
            let fieldName = this.options.orderBy || 'id';
            this.options.orderBy = function (currentChild, compareChild) {
                if (this.isDescending) {
                    return currentChild[fieldName] < compareChild.data[fieldName];
                } else {
                    return currentChild[fieldName] > compareChild.data[fieldName];
                }
            }
        }

        /* If present in options.headerTemplate or options.placeholderTemplate, we build the header and placeholder elements. */
        this._addHeader();
        this._addPlaceholder();

        if (this.options.dataStore) {
            this._bindDataSource(this.options.dataStore);
        } else {
            console.log('No DataSource was set.');
        }
    }

    /**
     * Reloads the dataFilter option of the DataBoundScrollView, and verifies whether the items in the dataStore are allowed by the new filter.
     * It removes any currently visible items that aren't allowed anymore, and adds any non-visible ones that are allowed now.
     * @param {Function} newFilter New filter function to verify item visibility with.
     * @returns {void}
     */
    reloadFilter(newFilter) {
        this.options.dataFilter = newFilter;

        for (let entry of this.options.dataStore) {
            let surface = _.find(this._dataSource, (surface) => surface.dataId === entry.id);
            let alreadyExists = surface !== undefined;
            let result = newFilter(entry);

            if (result instanceof Promise) {
                result.then(function (shouldShow) { this._handleNewFilterResult(shouldShow, alreadyExists, entry); }.bind(this))
            } else {
                this._handleNewFilterResult(result, alreadyExists, entry);
            }
        }
    }

    _handleNewFilterResult(shouldShow, alreadyExists, entry) {
        if (shouldShow) {
            /* This entry should be in the view, add it if it doesn't exist yet. */
            if (!alreadyExists) {
                this._addItem(entry);
            }
        } else {
            /* This entry should not be in the view, remove if present. */
            if (alreadyExists) {
                this._removeItem(entry);
            }
        }
    }

    _findGroup(groupId) {
        return _.findIndex(this._dataSource, function (surface) {
            return surface.groupId === groupId;
        });
    }

    _findNextGroup(fromIndex) {
        let dslength = this._dataSource.length;
        for (let pos = fromIndex; pos < dslength; pos++) {
            if (this._dataSource[pos].groupId) {
                return pos;
            }
        }

        return this._dataSource.length;
    }

    _getGroupByValue(child) {
        let groupByValue = '';
        if (typeof this.options.groupBy === 'function') {
            groupByValue = this.options.groupBy(child);
        } else if (typeof this.options.groupBy === 'string') {
            groupByValue = this.options.groupBy;
        }
        return groupByValue;
    }

    _addGroupItem(groupByValue, insertIndex) {
        let newSurface = this.options.groupTemplate(groupByValue);
        newSurface.groupId = groupByValue;

        this.insert(insertIndex, newSurface);
    }

    _getInsertIndex(child, previousSiblingID = null) {
        /* By default, add item to the beginning if descending order, or at the end otherwise. */
        let firstIndex = this.header ? 1 : 0;
        let insertIndex = this.isDescending ? firstIndex : this._dataSource.length;

        /* If we have an orderBy function, find the index we should be inserting at. */
        if (this.options.orderBy && typeof this.options.orderBy === 'function') {
            let foundOrderedIndex = _.findIndex(this._dataSource, function (compareChild) {

                /* Ignore the header, placeholder, and group items. */
                if (!compareChild.isHeader && !compareChild.isPlaceholder && !compareChild.groupId) {
                    return this.options.orderBy(child, compareChild);
                }
                return false;

            }.bind(this));

            if (foundOrderedIndex !== -1) {
                insertIndex = foundOrderedIndex;
            }
        } else if (previousSiblingID) {
            /* We don't have an orderBy method, but do have a previousSiblingID we can use to find the correct insertion index. */
            let siblingIndex = _.findIndex(this._dataSource, (sibling) => sibling.dataId === previousSiblingID);
            if (siblingIndex !== -1) {
                insertIndex = siblingIndex + 1;
            }
        }

        if(this.isGrouped && insertIndex === firstIndex && this._dataSource[insertIndex] && this._dataSource[insertIndex].groupId
            && this._dataSource[insertIndex].groupId === this._getGroupByValue(child)) {
            /* We didn't get an insert index from the ordering method or a previous sibling ID, and our insert index is already occupied by
             * a group item that we need to insert after, so we'll move the index to after the group item. */
            insertIndex++;
        } else if (this.isGrouped && insertIndex > 0 && this._dataSource[insertIndex-1] && this._dataSource[insertIndex-1].groupId
            && this._dataSource[insertIndex-1].groupId !== this._getGroupByValue(child)) {
            /* The insert index is after a group item that this child does not belong to, so we'll have to decrement the index by one
             * to avoid the child being added under this group. */
            insertIndex--;
        }

        return insertIndex;
    }

    _addItem(child, previousSiblingID) {
        this._removePlaceholder();

        let insertIndex = this._getInsertIndex(child, previousSiblingID);

        /* If we're using groups, check if we need to insert a group item before this child. */
        if (this.isGrouped) {
            let groupByValue = this._getGroupByValue(child);
            let groupIndex = this._findGroup(groupByValue);
            if (groupIndex === -1) {
                /* No group of this value exists yet, so we'll need to create one. */
                this._addGroupItem(groupByValue, insertIndex);
                insertIndex++;
            }
        }

        let newSurface = this.options.itemTemplate(child);
        newSurface.dataId = child.id;
        newSurface.data = child;
        newSurface.on('click', function () {
            this._eventOutput.emit('child_click', {renderNode: newSurface, dataObject: child});
        }.bind(this));

        this.insert(insertIndex, newSurface);
    }

    _replaceItem(child) {
        let index = this._getDataSourceIndex(child.id);

        let newSurface = this.options.itemTemplate(child);
        newSurface.dataId = child.id;
        this.replace(index, newSurface);
    }

    _removeItem(child) {
        let index = _.findIndex(this._dataSource, function (surface) {
            return surface.dataId === child.id;
        });

        if (index > -1) {
            this.remove(index);
        }

        /* If we're using groups, check if we need to remove the group that this child belonged to. */
        if (this.isGrouped) {
            let groupByValue = this._getGroupByValue(child);
            let otherGroupChilds = _.findIndex(this._dataSource, function (otherChild) {
                return this._getGroupByValue(otherChild) === groupByValue;
            }.bind(this));

            if (otherGroupChilds === -1) {
                /* No more childs in this group, so let's remove the group. */
                let groupIndex = this._findGroup(groupByValue);
                if (groupIndex !== -1) {
                    this.remove(groupIndex);
                }
            }
        }

        /* The amount of items in the dataSource is subtracted with a header if present, to get the total amount of actual items in the scrollView. */
        let itemCount = this._dataSource.length - (this.header ? 1 : 0);
        if (itemCount === 0) {
            this._addPlaceholder();
        }
    }

    _moveItem(oldId, prevChildId = null) {

        let oldIndex = this._getDataSourceIndex(oldId);
        let previousSiblingIndex = this._getNextVisibleIndex(prevChildId);

        if (oldIndex !== previousSiblingIndex) {
            this.move(oldIndex, previousSiblingIndex);
        }
    }

    _addHeader() {
        if (this.options.headerTemplate && !this.header) {
            this.header = this.options.headerTemplate();
            this.header.isHeader = true;
            this.insert(0, this.header);
        }
    }

    _addPlaceholder() {
        if (this.options.placeholderTemplate && !this.placeholder) {
            let insertIndex = this.header ? 1 : 0;
            this.placeholder = this.options.placeholderTemplate();
            this.placeholder.dataId = this.placeholder.id = '_placeholder';
            this.placeholder.isPlaceholder = true;
            this.insert(insertIndex, this.placeholder);
        }
    }

    _removePlaceholder() {
        if (this.placeholder) {
            this._removeItem(this.placeholder);
            this.placeholder = null;
        }
    }

    _bindDataSource() {

        if (!this.options.dataStore || !this.options.itemTemplate) {
            console.log('Datasource and template should both be set.');
            return;
        }

        if (!this.options.template instanceof Function) {
            console.log('Template needs to be a function.');
            return;
        }

        this.options.dataStore.on('child_added', function (child, previousSiblingID) {

            if (this.options.dataFilter &&
                (typeof this.options.dataFilter === 'function')) {

                let result = this.options.dataFilter(child);

                if (result instanceof Promise) {
                    /* If the result is a Promise, show the item when that promise resolves. */
                    result.then((show) => { if (show) { this.throttler.add(() => { return this._addItem(child), previousSiblingID; }); } });
                } else if (result) {
                    /* The result is an item, so we can add it directly. */
                    this.throttler.add(() => { return this._addItem(child, previousSiblingID); });
                }
            } else {
                /* There is no dataFilter method, so we can add this child. */
                this.throttler.add(() => { return this._addItem(child, previousSiblingID); });
            }
        }.bind(this));

        this.options.dataStore.on('child_changed', function (child, previousSiblingID) {

            let result = this.options.dataFilter(child);

            if(result instanceof Promise){
                result.then(function(show) {
                    if(show) {
                        this.throttler.add(() => { return this._replaceItem(child); });
                        this.throttler.add(() => { return this._moveItem(child.id, previousSiblingID); })
                    } else {
                        this._removeItem(child);
                    }
                }.bind(this));
            }
            else if (this.options.dataFilter &&
                typeof this.options.dataFilter === 'function' && !result) {
                this._removeItem(child);
            } else {
                if (changedItem === -1) {
                    this.throttler.add(() => { return this._addItem(child, previousSiblingID); });
                } else {
                    this.throttler.add(() => { return this._replaceItem(child); });
                    this.throttler.add(() => { return this._moveItem(child.id, previousSiblingID); });
                }
            }
        }.bind(this));

        this.options.dataStore.on('child_moved', function (child, previousSibling) {
            let current = this._getDataSourceIndex(child.id);
            let previous = this._getDataSourceIndex(previousSibling);
            this.throttler.add(() => { return this._moveItem(current, previous); });
        }.bind(this));

        this.options.dataStore.on('child_removed', function (child) {
            this.throttler.add(() => { return this._removeItem(child); });
        }.bind(this));
    }

    _getDataSourceIndex(id) {
        return _.findIndex(this._dataSource, function (surface) {
            return surface.dataId === id;
        });
    }

    _getNextVisibleIndex(id) {

        let viewIndex = this._getDataSourceIndex(id);
        if (viewIndex === -1) {
            let modelIndex = _.findIndex(this.options.dataStore, function (model) {
                return model.id === id;
            });

            if (modelIndex === 0 || modelIndex === -1) {
                return this.isDescending ? this._dataSource ? this._dataSource.length - 1 : 0 : 0;
            } else {
                let nextModel = this.options.dataStore[this.isDescending ? modelIndex + 1 : modelIndex - 1];
                let nextIndex = this._getDataSourceIndex(nextModel.id);
                if (nextIndex > -1) {

                    return this.isDescending ? nextIndex === 0 ? 0 : nextIndex - 1 :
                           this._dataSource.length === nextIndex + 1 ? nextIndex : nextIndex + 1;
                } else {
                    return this._getNextVisibleIndex(nextModel.id);
                }
            }
        } else {
            return this.isDescending ? viewIndex === 0 ? 0 : viewIndex - 1 :
                   this._dataSource.length === viewIndex + 1 ? viewIndex : viewIndex + 1;
        }
    }
}<|endoftext|>


<commit_before>(function() {
    module.exports = CanvasDependencies;

    function CanvasDependencies() {

        var _cropResize     = this,
            _onChangeQueue  = [],
            _image          = new Image(),
            _source,
            _canvas,
            _canvasElement,
            _context;

        /*========================================================================
            PUBLIC
        ========================================================================*/
        this.cropArea       = {};
        this.cropArea.init  = init;

        /*========================================================================
            PRIVATE
        ========================================================================*/

        _image.onload       =   onCanvasSrcLoad;

        this.eventsQueue.subscribe('resize', window, cacheCanvasDimensions);

        function init(canvasElement, file){
            _canvasElement  =   canvasElement;
            _context        =   _canvasElement.getContext('2d');
            _cropResize.utils.fileToBase64(file, function (src) {
                _image.src  =   src;
            });
            return {
                onChange:storeOnChange,
                changeFile:changeFile
            };
        }

        function changeFile(file){
            _cropResize.utils.fileToBase64(file, function (src) {
                _image.src = src;
            });
        }

        function onCanvasSrcLoad(e){

            _source           = {};
            _canvas           = {};

            _source['width']  = this.width;
            _source['height'] = this.height;

            _canvas['width']  = _canvasElement.width;
            _canvas['height'] = _canvasElement.height;

            var canvasParams  = measureContext();

            _context.clearRect(0, 0, _canvas.width, _canvas.height);
            _context.drawImage(_image,
                canvasParams.sx,
                canvasParams.sy,
                canvasParams.sWidth,
                canvasParams.sHeight,
                canvasParams.dx,
                canvasParams.dy,
                canvasParams.dWidth,
                canvasParams.dHeight
            );
            cacheCanvasDimensions();
            notify();
        }

        function storeOnChange(fn){
            _onChangeQueue.push(fn);
        }

        function notify(){
            var totalSubscribers= _onChangeQueue.length;
            while(totalSubscribers--){
                if(_onChangeQueue[totalSubscribers] && typeof _onChangeQueue[totalSubscribers] === 'function'){
                    _onChangeQueue[totalSubscribers]({
                        source:_source,
                        canvas:_canvas,
                        context:_context
                    });
                }
            }
        }

        function cacheCanvasDimensions(){
            if(_canvasElement){
                var canvasBounding = _canvasElement.getBoundingClientRect();

                _canvas['top']      = canvasBounding.top;
                _canvas['left']     = canvasBounding.left;
                _canvas['cWidth']   = _canvasElement.offsetWidth;
                _canvas['cHeight']  = _canvasElement.offsetHeight;
            }
        }

        function measureContext(){
            var hRatio      =   _canvas.width  / _source.width,
                vRatio      =   _canvas.height  / _source.height,
                ratio       =   Math.min( hRatio, vRatio);

            return {
                sx:         0,
                sy:         0,
                sWidth:     _source.width,
                sHeight:    _source.height,
                dx:         (_canvas.width - _source.width * ratio ) / 2,
                dy:         (_canvas.height - _source.height * ratio ) / 2,
                dWidth:     _source.width * ratio,
                dHeight:    _source.height * ratio
            }
        }
    }
})();<commit_msg>Implemented change to improve the quality of loaded image in the crop area.<commit_after>(function() {
    module.exports = CanvasDependencies;

    function CanvasDependencies() {

        var _cropResize     = this,
            _onChangeQueue  = [],
            _image          = new Image(),
            _source,
            _canvas,
            _canvasElement,
            _context;

        /*========================================================================
            PUBLIC
        ========================================================================*/
        this.cropArea       = {};
        this.cropArea.init  = init;

        /*========================================================================
            PRIVATE
        ========================================================================*/

        _image.onload       =   onCanvasSrcLoad;

        this.eventsQueue.subscribe('resize', window, cacheCanvasDimensions);

        function init(canvasElement, file){
            _canvasElement  =   canvasElement;
            _context        =   _canvasElement.getContext('2d');
            _cropResize.utils.fileToBase64(file, function (src) {
                _image.src  =   src;
            });
            return {
                onChange    : storeOnChange,
                changeFile  : changeFile
            };
        }

        function changeFile(file){
            _cropResize.utils.fileToBase64(file, function (src) {
                _image.src = src;
            });
        }

        function onCanvasSrcLoad(e){
            //Without a set size on the canvas the image data context with render at it's default dimensions of 300x150.
            //CSS will then scale the scaled down image data up to the size that it calculates for the DOM.
            _canvasElement.width    = _canvasElement.offsetWidth;
            _canvasElement.height   = _canvasElement.offsetHeight;

            _source           = {};
            _canvas           = {};

            _source['width']  = this.width;
            _source['height'] = this.height;

            _canvas['width']  = _canvasElement.width;
            _canvas['height'] = _canvasElement.height;

            var canvasParams  = measureContext();

            _context.clearRect(0, 0, _canvas.width, _canvas.height);
            _context.drawImage(_image,
                canvasParams.sx,
                canvasParams.sy,
                canvasParams.sWidth,
                canvasParams.sHeight,
                canvasParams.dx,
                canvasParams.dy,
                canvasParams.dWidth,
                canvasParams.dHeight
            );
            cacheCanvasDimensions();
            notify();
        }

        function storeOnChange(fn){
            _onChangeQueue.push(fn);
        }

        function notify(){
            var totalSubscribers= _onChangeQueue.length;
            while(totalSubscribers--){
                if(_onChangeQueue[totalSubscribers] && typeof _onChangeQueue[totalSubscribers] === 'function'){
                    _onChangeQueue[totalSubscribers]({
                        source:_source,
                        canvas:_canvas,
                        context:_context
                    });
                }
            }
        }

        function cacheCanvasDimensions(){
            if(_canvasElement){
                var canvasBounding = _canvasElement.getBoundingClientRect();

                _canvas['top']      = canvasBounding.top;
                _canvas['left']     = canvasBounding.left;
                _canvas['cWidth']   = _canvasElement.offsetWidth;
                _canvas['cHeight']  = _canvasElement.offsetHeight;
            }
        }

        function measureContext(){
            var hRatio      =   _canvas.width  / _source.width,
                vRatio      =   _canvas.height  / _source.height,
                ratio       =   Math.min( hRatio, vRatio);

            return {
                sx:         0,
                sy:         0,
                sWidth:     _source.width,
                sHeight:    _source.height,
                dx:         (_canvas.width - _source.width * ratio ) / 2,
                dy:         (_canvas.height - _source.height * ratio ) / 2,
                dWidth:     _source.width * ratio,
                dHeight:    _source.height * ratio
            }
        }
    }
})();<|endoftext|>


<commit_before>/* eslint-disable max-len */
import React from 'react'

export const LOGGED_IN_PROMOTIONS = [
  {
    username: 'thvnderkat',
    caption:
Ello is gifs & 3D.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/9606/ello-regular-79cae819.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967225/ello-hdpi-1f330db7.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967225/ello-xhdpi-1f330db7.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967225/ello-optimized-1f330db7.jpg' },
    },
  },
  {
    username: 'carolynedebellefeuille',
    caption:
Ello is photography & fashion.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/997491/ello-regular-905a9aca.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967221/ello-hdpi-a0acd15b.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967221/ello-xhdpi-a0acd15b.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967221/ello-optimized-a0acd15b.jpg' },
    },
  },
  {
    username: 'kevin_kuenstler',
    caption:
Ello is art & architecture.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/11636/ello-regular-762f2230.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967218/ello-hdpi-b266e1c3.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967218/ello-xhdpi-b266e1c3.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967218/ello-optimized-b266e1c3.jpg' },
    },
  },
  {
    username: 'gersoncabrera',
    caption:
Ello is graphic design & illustration.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/1577002/ello-regular-ad1b0111.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967212/ello-hdpi-7b559bc7.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967212/ello-xhdpi-7b559bc7.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967212/ello-optimized-7b559bc7.jpg' },
    },
  },
  {
    username: 'rosablue',
    caption:
Always ad-free.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/355/ello-regular-53b283da.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967210/ello-hdpi-ae5dc289.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967210/ello-xhdpi-ae5dc289.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967210/ello-optimized-ae5dc289.jpg' },
    },
  },
  {
    username: 'christofkessemeier',
    caption:
Ello is NSFW.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2095104/ello-regular-3e7ad015.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967208/ello-hdpi-3a4470d7.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967208/ello-xhdpi-3a4470d7.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967208/ello-optimized-3a4470d7.jpg' },
    },
  },
  {
    username: 'meganpoparda',
    caption:
Experience the promise of the internet.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2717655/ello-regular-8f8cb927.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967203/ello-hdpi-4ee72712.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967203/ello-xhdpi-4ee72712.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967203/ello-optimized-4ee72712.jpg' },
    },
  },
  {
    username: 'anthonyneildart',
    caption:
Supporting creators since 2014.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2106815/ello-regular-34edf269.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967202/ello-hdpi-fc66ca7c.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967202/ello-xhdpi-fc66ca7c.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967202/ello-optimized-fc66ca7c.jpg' },
    },
  },
  {
    username: 'matthieuvenot',
    caption:
Open source by default.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2733732/ello-regular-690b8e1f.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967197/ello-hdpi-804eed2a.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967197/ello-xhdpi-804eed2a.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967197/ello-optimized-804eed2a.jpg' },
    },
  },
  {
    username: 'pelocodo',
    caption:
Explore. Expand. Escape.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2811840/ello-regular-36b0f86c.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967195/ello-hdpi-48433a87.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967195/ello-xhdpi-48433a87.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967195/ello-optimized-48433a87.jpg' },
    },
  },
  {
    username: 'lukesiem',
    caption:
Explore. Expand. Escape.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/89825/ello-regular-a279784a.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967192/ello-hdpi-02b6fbed.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967192/ello-xhdpi-02b6fbed.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967192/ello-optimized-02b6fbed.jpg' },
    },
  },
  {
    username: 'frankpantera',
    caption:
Welcome to the future.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2044563/ello-regular-d510f0c1.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967186/ello-hdpi-186135a0.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967186/ello-xhdpi-186135a0.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967186/ello-optimized-186135a0.jpg' },
    },
  },
  {
    username: 'rvdy',
    caption:
Welcome to the future.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2657695/ello-regular-37fe91cd.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967182/ello-hdpi-508b551d.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967182/ello-xhdpi-508b551d.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967182/ello-optimized-508b551d.jpg' },
    },
  },
  {
    username: 'artmour',
    caption:
Humans over algorithms.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2758607/ello-regular-00579970.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967179/ello-hdpi-c606762a.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967179/ello-xhdpi-c606762a.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967179/ello-optimized-c606762a.jpg' },
    },
  },
  {
    username: 'taylortilcrisdale',
    caption:
Humans over algorithms.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2810346/ello-regular-b63ab05d.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967177/ello-hdpi-65da07c4.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967177/ello-xhdpi-65da07c4.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967177/ello-optimized-65da07c4.jpg' },
    },
  },
]
<commit_msg>Update username and broken avatar<commit_after>/* eslint-disable max-len */
import React from 'react'

export const LOGGED_IN_PROMOTIONS = [
  {
    username: 'thvnderkat',
    caption:
Ello is gifs & 3D.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/9606/ello-regular-79cae819.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967225/ello-hdpi-1f330db7.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967225/ello-xhdpi-1f330db7.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967225/ello-optimized-1f330db7.jpg' },
    },
  },
  {
    username: 'carolynedebellefeuille',
    caption:
Ello is photography & fashion.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/997491/ello-regular-905a9aca.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967221/ello-hdpi-a0acd15b.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967221/ello-xhdpi-a0acd15b.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967221/ello-optimized-a0acd15b.jpg' },
    },
  },
  {
    username: 'kevin_kuenstler',
    caption:
Ello is art & architecture.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/11636/ello-regular-762f2230.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967218/ello-hdpi-b266e1c3.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967218/ello-xhdpi-b266e1c3.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967218/ello-optimized-b266e1c3.jpg' },
    },
  },
  {
    username: 'gersoncabrera',
    caption:
Ello is graphic design & illustration.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/1577002/ello-regular-ad1b0111.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967212/ello-hdpi-7b559bc7.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967212/ello-xhdpi-7b559bc7.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967212/ello-optimized-7b559bc7.jpg' },
    },
  },
  {
    username: 'rosablue',
    caption:
Always ad-free.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/355/ello-regular-53b283da.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967210/ello-hdpi-ae5dc289.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967210/ello-xhdpi-ae5dc289.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967210/ello-optimized-ae5dc289.jpg' },
    },
  },
  {
    username: 'christofkessemeier',
    caption:
Ello is NSFW.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2095104/ello-regular-3e7ad015.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967208/ello-hdpi-3a4470d7.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967208/ello-xhdpi-3a4470d7.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967208/ello-optimized-3a4470d7.jpg' },
    },
  },
  {
    username: 'meganpoparda',
    caption:
Experience the promise of the internet.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2717655/ello-regular-8f8cb927.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967203/ello-hdpi-4ee72712.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967203/ello-xhdpi-4ee72712.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967203/ello-optimized-4ee72712.jpg' },
    },
  },
  {
    username: 'anthonyneildart',
    caption:
Supporting creators since 2014.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2106815/ello-regular-34edf269.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967202/ello-hdpi-fc66ca7c.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967202/ello-xhdpi-fc66ca7c.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967202/ello-optimized-fc66ca7c.jpg' },
    },
  },
  {
    username: 'matthieuvenot',
    caption:
Open source by default.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2733732/ello-regular-690b8e1f.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967197/ello-hdpi-804eed2a.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967197/ello-xhdpi-804eed2a.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967197/ello-optimized-804eed2a.jpg' },
    },
  },
  {
    username: 'pelocodo',
    caption:
Explore. Expand. Escape.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2811840/ello-regular-36b0f86c.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967195/ello-hdpi-48433a87.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967195/ello-xhdpi-48433a87.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967195/ello-optimized-48433a87.jpg' },
    },
  },
  {
    username: 'lukesiem',
    caption:
Explore. Expand. Escape.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/89825/ello-regular-a279784a.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967192/ello-hdpi-02b6fbed.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967192/ello-xhdpi-02b6fbed.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967192/ello-optimized-02b6fbed.jpg' },
    },
  },
  {
    username: 'dan_peterka',
    caption:
Welcome to the future.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2044563/ello-regular-dcb36af3.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967186/ello-hdpi-186135a0.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967186/ello-xhdpi-186135a0.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967186/ello-optimized-186135a0.jpg' },
    },
  },
  {
    username: 'rvdy',
    caption:
Welcome to the future.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2657695/ello-regular-37fe91cd.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967182/ello-hdpi-508b551d.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967182/ello-xhdpi-508b551d.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967182/ello-optimized-508b551d.jpg' },
    },
  },
  {
    username: 'artmour',
    caption:
Humans over algorithms.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2758607/ello-regular-00579970.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967179/ello-hdpi-c606762a.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967179/ello-xhdpi-c606762a.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967179/ello-optimized-c606762a.jpg' },
    },
  },
  {
    username: 'taylortilcrisdale',
    caption:
Humans over algorithms.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2810346/ello-regular-b63ab05d.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967177/ello-hdpi-65da07c4.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3967177/ello-xhdpi-65da07c4.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3967177/ello-optimized-65da07c4.jpg' },
    },
  },
]
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import { api } from '../../data'
import { Field, Submit, Base, Subheading } from '../components/Forms'
import Button from '../components/Button'
import LoadingAnimation from '../components/LoadingAnimation'
import theme, { colors, mx } from '../theme'
import { withFormik } from 'formik'
import yup from 'yup'
import fetch from 'unfetch'

const id = () => {
  const pairs = window.location.search.split(/\?|&/)
  for (var i = 0; i < pairs.length; i++) {
    let pair = pairs[i]
    let k = pair.split('=')[0]
    let v = pair.split('=')[1]
    if (k === 'id') {
      return parseInt(v)
    }
  }
}
const authToken = () => (
  window.localStorage.getItem('authToken')
)
const formToObj = form => {
  var obj = {}
  const formData = new FormData(form)
  formData.forEach((v, k) => {
    obj[k] = v
  })
  return obj
}
const InnerForm = ({
  values,
  errors,
  touched,
  handleChange,
  handleBlur,
  handleSubmit,
  isSubmitting,
  saveDraft = (e) => {
    const draftButton = e.target
    draftButton.value = 'Saving...'
    draftButton.disabled = true
    const data = formToObj(e.target.form)
    fetch(`${api}/v1/club_applications/${id()}`, {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${authToken()}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    })
      .then(res => (res.json()))
      .then(json => {})
      .catch(e => {alert(e)})
  },
  inviteLeader = () => {
    const leaderInvite = window.document.querySelector('#leader_invite')
    const data = {email: leaderInvite.value}
    const schema = yup.object().shape({
      email: yup.string().required().email()
    })

    schema
      .validate(data)
      .then(data => {
        fetch(`${api}/v1/club_applications/${id()}/add_applicant`, {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${authToken()}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(data)
        })
          .then(res => (res.json()))
          .then(json => {
            alert(`Invite sent to ${data.email}`)
            leaderInvite.value = ''
          })
          .catch(e => {alert(e)})
      })
      .catch(e => {alert(e)})
  }
}) => (
  <Base is="form" onSubmit={handleSubmit}>
    <Subheading>School</Subheading>
    <Field name="high_school_name"
           label="Name of high school"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.high_school_name}
           error={touched.high_school_name && errors.high_school_name}
    />
    <Field name="high_school_url"
           label="Link to your high school's website, if any"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.high_school_url}
           error={touched.high_school_url && errors.high_school_url}
    />
    <Field name="high_school_type"
           label="Type of school"
           type="select"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.high_school_type || 'select'}
           error={touched.high_school_type && errors.high_school_type}
    >
      <option disabled value="select">Select One</option>
      <option value="0">Public</option>
      <option value="1">Private</option>
      <option value="2">Charter</option>
    </Field>
    <Field name="high_school_address"
           label="High school's full address"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.high_school_address}
           error={touched.high_school_address && errors.high_school_address}
           type="textarea" />
    <Subheading>Leaders</Subheading>

      {values.applicant_profiles.map((profile, index) => (

{profile.applicant.email}

      ))}

    <Field label="Email address of co-leader" id="leader_invite" />
    <Button onClick={inviteLeader}>Add co-leader</Button>
    <Field name="leaders_video_url"
           label="Please enter the URL of a 1 minute unlisted (not private) YouTube video introducing the leaders"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.leaders_video_url}
           error={touched.leaders_video_url && errors.leaders_video_url}
    />
    <Field name="leaders_interesting_project"
           label="Please tell us about an interesting project, preferably outside of class, that two or more of you created together. Include URLs if possible."
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.leaders_interesting_project}
           error={touched.leaders_interesting_project && errors.leaders_interesting_project}
           type="textarea" />
    <Field name="leaders_team_origin_story"
           label="How long have you known your other club leaders and how did you meet?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.leaders_team_origin_story}
           error={touched.leaders_team_origin_story && errors.leaders_team_origin_story}
           type="textarea" />
    <Subheading>Progress</Subheading>
    <Field name="progress_general"
           label="How far along are you in starting your club?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.progress_general}
           error={touched.progress_general && errors.progress_general}
           type="textarea" />
    <Field name="progress_student_interest"
           label="Have you already polled for interest at your school? Are students interested? If you've already had meetings, how many people came?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.progress_student_interest}
           error={touched.progress_student_interest && errors.progress_student_interest}
           type="textarea" />
    <Field name="progress_meeting_yet"
           label="Have you begun meeting yet? We encourage you to not begin meeting until we accept you."
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.progress_meeting_yet}
           error={touched.progress_meeting_yet && errors.progress_meeting_yet}
           type="textarea" />
    <Subheading>Idea</Subheading>
    <Field name="idea_why"
           label="Why did you decide to start a Hack Club? Have you ran anything like a Hack Club before? Why do you think the club is going to work?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.idea_why}
           error={touched.idea_why && errors.idea_why}
           type="textarea" />
    <Field name="idea_other_coding_clubs"
           label="Has your school had coding clubs before? What's going to be new about your Hack Club?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.idea_other_coding_clubs}
           error={touched.idea_other_coding_clubs && errors.idea_other_coding_clubs}
           type="textarea" />
    <Field name="idea_other_general_clubs"
           label="What other clubs exist at your school? Why will you be just as successful, if not more, than them?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.idea_other_general_clubs}
           error={touched.idea_other_general_clubs && errors.idea_other_general_clubs}
           type="textarea" />
    <Subheading>Formation</Subheading>
    <Field name="formation_registered"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.formation_registered}
           error={touched.formation_registered && errors.formation_registered}
           label="Have you already registerd your club with your school?"
    />
    <Field name="formation_misc"
           label="Please provide any other relevant information about the structure or formation of the club."
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.formation_misc}
           error={touched.formation_misc && errors.formation_misc}
           type="textarea" />
    <Subheading>Other</Subheading>
    <Field name="other_surprising_or_amusing_discovery"
           label="What is something surprising or amusing you discovered?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.other_surprising_or_amusing_discovery}
           error={touched.other_surprising_or_amusing_discovery && errors.other_surprising_or_amusing_discovery}
           type="textarea" />
    <Subheading>Curious</Subheading>
    <Field name="curious_what_convinced"
           label="What convinced you to start a Hack Club?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.curious_what_convinced}
           error={touched.curious_what_convinced && errors.curious_what_convinced}
           type="textarea" />
    <Field name="curious_how_did_hear"
           label="How did you hear about Hack Club?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.curious_how_did_hear}
           error={touched.curious_how_did_hear && errors.curious_how_did_hear}
           type="textarea" />
    <Submit
      onClick={saveDraft}
      value="Save as draft"
    />

    {/* TODO: Add a submit button that submits instead of saving a draft */}
    {/* <Submit
        disabled={isSubmitting || !isValid}
        onClick={handleSubmit}
        value="Submit"
        /> */}
  </Base>
)

const ClubForm = withFormik({
  mapPropsToValues: params => ( {...params} ),
  enableReinitialize: true,
  displayName: 'ClubForm',
  handleSubmit: (values, {props, setSubmitting, setErrors}) => {
    // this will submit as a final draft
  }
})(InnerForm)

export default class extends Component {
  constructor(props) {
    super(props)

    this.state = {
      status: 'loading',
      formFields: null
    }
  }

  componentDidMount() {
    fetch(`${api}/v1/club_applications/${id()}`, {
      method: 'GET',
      headers: { 'Authorization': `Bearer ${authToken()}`, },
    })
      .then(res => {
        if (res.ok) {
          return res.json()
        } else {
          throw res
        }})
      .then(json => {
        this.setState({
          status: 'loaded',
          formFields: json
        })
      })
      .catch(e => {alert(e)})
  }

  render() {
    const { status, formFields } = this.state

    if (status === 'loading' || formFields === null) {
      return(<LoadingAnimation />)
    } else {
      return(<ClubForm {...formFields} />)
    }
  }
}
<commit_msg>Save club application draft button is enabled after saving<commit_after>import React, { Component } from 'react'
import { api } from '../../data'
import { Field, Submit, Base, Subheading } from '../components/Forms'
import Button from '../components/Button'
import LoadingAnimation from '../components/LoadingAnimation'
import theme, { colors, mx } from '../theme'
import { withFormik } from 'formik'
import yup from 'yup'
import fetch from 'unfetch'

const id = () => {
  const pairs = window.location.search.split(/\?|&/)
  for (var i = 0; i < pairs.length; i++) {
    let pair = pairs[i]
    let k = pair.split('=')[0]
    let v = pair.split('=')[1]
    if (k === 'id') {
      return parseInt(v)
    }
  }
}
const authToken = () => (
  window.localStorage.getItem('authToken')
)
const formToObj = form => {
  var obj = {}
  const formData = new FormData(form)
  formData.forEach((v, k) => {
    obj[k] = v
  })
  return obj
}
const InnerForm = ({
  values,
  errors,
  touched,
  handleChange,
  handleBlur,
  handleSubmit,
  isSubmitting,
  saveDraft = (e) => {
    const draftButton = e.target
    draftButton.value = 'Saving...'
    draftButton.disabled = true
    const data = formToObj(e.target.form)
    fetch(`${api}/v1/club_applications/${id()}`, {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${authToken()}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    })
      .then(res => (res.json()))
      .then(json => {
        draftButton.value = 'Save as draft'
        draftButton.disabled = false
      })
      .catch(e => {
        alert(e)
        draftButton.value = 'Save as draft'
        draftButton.disabled = false
      })
  },
  inviteLeader = () => {
    const leaderInvite = window.document.querySelector('#leader_invite')
    const data = {email: leaderInvite.value}
    const schema = yup.object().shape({
      email: yup.string().required().email()
    })

    schema
      .validate(data)
      .then(data => {
        fetch(`${api}/v1/club_applications/${id()}/add_applicant`, {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${authToken()}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(data)
        })
          .then(res => (res.json()))
          .then(json => {
            alert(`Invite sent to ${data.email}`)
            leaderInvite.value = ''
          })
          .catch(e => {alert(e)})
      })
      .catch(e => {alert(e)})
  }
}) => (
  <Base is="form" onSubmit={handleSubmit}>
    <Subheading>School</Subheading>
    <Field name="high_school_name"
           label="Name of high school"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.high_school_name}
           error={touched.high_school_name && errors.high_school_name}
    />
    <Field name="high_school_url"
           label="Link to your high school's website, if any"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.high_school_url}
           error={touched.high_school_url && errors.high_school_url}
    />
    <Field name="high_school_type"
           label="Type of school"
           type="select"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.high_school_type || 'select'}
           error={touched.high_school_type && errors.high_school_type}
    >
      <option disabled value="select">Select One</option>
      <option value="0">Public</option>
      <option value="1">Private</option>
      <option value="2">Charter</option>
    </Field>
    <Field name="high_school_address"
           label="High school's full address"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.high_school_address}
           error={touched.high_school_address && errors.high_school_address}
           type="textarea" />
    <Subheading>Leaders</Subheading>

      {values.applicant_profiles.map((profile, index) => (

{profile.applicant.email}

      ))}

    <Field label="Email address of co-leader" id="leader_invite" />
    <Button onClick={inviteLeader}>Add co-leader</Button>
    <Field name="leaders_video_url"
           label="Please enter the URL of a 1 minute unlisted (not private) YouTube video introducing the leaders"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.leaders_video_url}
           error={touched.leaders_video_url && errors.leaders_video_url}
    />
    <Field name="leaders_interesting_project"
           label="Please tell us about an interesting project, preferably outside of class, that two or more of you created together. Include URLs if possible."
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.leaders_interesting_project}
           error={touched.leaders_interesting_project && errors.leaders_interesting_project}
           type="textarea" />
    <Field name="leaders_team_origin_story"
           label="How long have you known your other club leaders and how did you meet?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.leaders_team_origin_story}
           error={touched.leaders_team_origin_story && errors.leaders_team_origin_story}
           type="textarea" />
    <Subheading>Progress</Subheading>
    <Field name="progress_general"
           label="How far along are you in starting your club?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.progress_general}
           error={touched.progress_general && errors.progress_general}
           type="textarea" />
    <Field name="progress_student_interest"
           label="Have you already polled for interest at your school? Are students interested? If you've already had meetings, how many people came?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.progress_student_interest}
           error={touched.progress_student_interest && errors.progress_student_interest}
           type="textarea" />
    <Field name="progress_meeting_yet"
           label="Have you begun meeting yet? We encourage you to not begin meeting until we accept you."
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.progress_meeting_yet}
           error={touched.progress_meeting_yet && errors.progress_meeting_yet}
           type="textarea" />
    <Subheading>Idea</Subheading>
    <Field name="idea_why"
           label="Why did you decide to start a Hack Club? Have you ran anything like a Hack Club before? Why do you think the club is going to work?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.idea_why}
           error={touched.idea_why && errors.idea_why}
           type="textarea" />
    <Field name="idea_other_coding_clubs"
           label="Has your school had coding clubs before? What's going to be new about your Hack Club?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.idea_other_coding_clubs}
           error={touched.idea_other_coding_clubs && errors.idea_other_coding_clubs}
           type="textarea" />
    <Field name="idea_other_general_clubs"
           label="What other clubs exist at your school? Why will you be just as successful, if not more, than them?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.idea_other_general_clubs}
           error={touched.idea_other_general_clubs && errors.idea_other_general_clubs}
           type="textarea" />
    <Subheading>Formation</Subheading>
    <Field name="formation_registered"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.formation_registered}
           error={touched.formation_registered && errors.formation_registered}
           label="Have you already registerd your club with your school?"
    />
    <Field name="formation_misc"
           label="Please provide any other relevant information about the structure or formation of the club."
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.formation_misc}
           error={touched.formation_misc && errors.formation_misc}
           type="textarea" />
    <Subheading>Other</Subheading>
    <Field name="other_surprising_or_amusing_discovery"
           label="What is something surprising or amusing you discovered?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.other_surprising_or_amusing_discovery}
           error={touched.other_surprising_or_amusing_discovery && errors.other_surprising_or_amusing_discovery}
           type="textarea" />
    <Subheading>Curious</Subheading>
    <Field name="curious_what_convinced"
           label="What convinced you to start a Hack Club?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.curious_what_convinced}
           error={touched.curious_what_convinced && errors.curious_what_convinced}
           type="textarea" />
    <Field name="curious_how_did_hear"
           label="How did you hear about Hack Club?"
           onChange={handleChange}
           onBlur={handleBlur}
           value={values.curious_how_did_hear}
           error={touched.curious_how_did_hear && errors.curious_how_did_hear}
           type="textarea" />
    <Submit
      onClick={saveDraft}
      value="Save as draft"
    />

    {/* TODO: Add a submit button that submits instead of saving a draft */}
    {/* <Submit
        disabled={isSubmitting || !isValid}
        onClick={handleSubmit}
        value="Submit"
        /> */}
  </Base>
)

const ClubForm = withFormik({
  mapPropsToValues: params => ( {...params} ),
  enableReinitialize: true,
  displayName: 'ClubForm',
  handleSubmit: (values, {props, setSubmitting, setErrors}) => {
    // this will submit as a final draft
  }
})(InnerForm)

export default class extends Component {
  constructor(props) {
    super(props)

    this.state = {
      status: 'loading',
      formFields: null
    }
  }

  componentDidMount() {
    fetch(`${api}/v1/club_applications/${id()}`, {
      method: 'GET',
      headers: { 'Authorization': `Bearer ${authToken()}`, },
    })
      .then(res => {
        if (res.ok) {
          return res.json()
        } else {
          throw res
        }})
      .then(json => {
        this.setState({
          status: 'loaded',
          formFields: json
        })
      })
      .catch(e => {alert(e)})
  }

  render() {
    const { status, formFields } = this.state

    if (status === 'loading' || formFields === null) {
      return(<LoadingAnimation />)
    } else {
      return(<ClubForm {...formFields} />)
    }
  }
}
<|endoftext|>


<commit_before><commit_msg>[Practise] remove cookbook<commit_after><|endoftext|>


<commit_before>/*jslint nomen: true, vars: true */
/*global interstate,esprima,able,uid,console */

(function (ist) {
	"use strict";
	var cjs = ist.cjs,
		_ = ist._;

	// mouse
	ist.createMouseObject = function() {
		var clientX = cjs(0),
			clientY = cjs(0),
			pageX = cjs(0),
			pageY = cjs(0),
			screenX = cjs(0),
			screenY = cjs(0),
			move_listener = function(event) {
				cjs.wait();
				clientX.set(event.clientX);
				clientY.set(event.clientY);
				pageX.set(event.pageX);
				pageY.set(event.pageY);
				screenX.set(event.screenX);
				screenY.set(event.screenY);
				cjs.signal();
			},
			addMouseListeners = function() {
				window.addEventListener("mousemove", move_listener);
			},
			removeMouseListeners = function() {
				window.removeEventListener("mousemove", move_listener);
			},
			destroy = function(silent) {
				cjs.wait();

				removeMouseListeners();
				clientX.destroy(silent);
				clientY.destroy(silent);
				pageX.destroy(silent);
				pageY.destroy(silent);
				screenX.destroy(silent);
				screenY.destroy(silent);

				cjs.signal();
			},
			device_mouse = new ist.Dict({has_protos: false, value: {
					x: pageX,
					y: pageY,
					clientX: clientX,
					clientY: clientY,
					pageX: pageX,
					pageY: pageY,
					screenX: screenX,
					screenY: screenY
				}
			});
		device_mouse.destroy = function() {
			ist.Dict.prototype.destroy.apply(this, arguments);
			destroy();
		};
		device_mouse.__is_mouse_device__ = true;
		addMouseListeners();
		return device_mouse;
	};
	ist.register_serializable_type("ist_device_mouse",
		function (x) {
			return x.__is_mouse_device__;
		},
		function () {
			return {};
		},
		function (obj) {
			return ist.createMouseObject();
		});

	// keyboard
	ist.createKeyboardObject = function() {
		var key_codes = {
				shift: 16,
				ctrl: 17,
				alt: 18,
				cmd_left: 91,
				cmd_right: 93
			},
			interesting_keycodes = {},
			keyboard_info = {};

		_.each(key_codes, function(code, name) {
			keyboard_info[name] = cjs(false);
			interesting_keycodes[code] = name;
		});

		var keydown_listener = function(event) {
				var keyCode = event.keyCode,
					name = interesting_keycodes[keyCode];
				if(name) {
					keyboard_info[name].set(true);
					event.preventDefault();
				}
			},
			keyup_listener = function(event) {
				var keyCode = event.keyCode,
					name = interesting_keycodes[keyCode];

				if(interesting_keycodes[keyCode]) {
					keyboard_info[name].set(false);
					event.preventDefault();
				}
			},
			addKeyboardListeners = function() {
				window.addEventListener("keydown", keydown_listener);
				window.addEventListener("keyup", keyup_listener);
			},
			removeKeyboardListeners = function() {
				window.removeEventListener("keydown", keydown_listener);
				window.removeEventListener("keyup", keyup_listener);
			},
			destroy = function(silent) {
				cjs.wait();

				removeKeyboardListeners();

				shift.destroy(silent);
				ctrl.destroy(silent);
				alt.destroy(silent);
				cmd_left.destroy(silent);
				cmd_right.destroy(silent);
				cmd.destroy(silent);

				cjs.signal();
			},
			shift = keyboard_info.shift,
			ctrl = keyboard_info.ctrl,
			alt = keyboard_info.alt,
			cmd_left = keyboard_info.cmd_left,
			cmd_right = keyboard_info.cmd_right,
			cmd = cmd_left.or(cmd_right),
			device_keyboard = new ist.Dict({has_protos: false, value: {
					shift: shift,
					ctrl: ctrl,
					alt: alt,
					cmd_left: cmd_left,
					cmd_right: cmd_right,
					cmd: cmd
				}
			});
		device_keyboard.destroy = function() {
			ist.Dict.prototype.destroy.apply(this, arguments);
			destroy();
		};
		device_keyboard.__is_keyboard_device__ = true;
		addKeyboardListeners();
		return device_keyboard;
	};
	ist.register_serializable_type("ist_device_keyboard",
		function (x) {
			return x.__is_keyboard_device__;
		},
		function () {
			return {};
		},
		function (obj) {
			return ist.createKeyboardObject();
		});

	// touches
	ist.createTouchscreenObject = function() {
		var touch_props = ["identifier",
							"screenX", "screenY",
							"clientX", "clientY",
							"pageX", "pageY",
							"radiusX", "radiusY",
							"rotationAngle"],
			touch_infos = {};

		_.each(touch_props, function(prop_name) {
			touch_infos[prop_name] = cjs([]);
		});

		var touch_starts = {},
			touch_ends = {},
			touches = {},
			touch_start_listener = function(event) {
				cjs.wait();

				_.each(event.changedTouches, function(ct) {
					var identifier = ct.identifier,
						touch, touch_start_obj;

					if(touches.hasOwnProperty(identifier)) {
						var index = touch_infos.identifier.indexOf(identifier);

						touch = touches[identifier];

						if(index >= 0) {
							_.each(touch_props, function(prop_name) {
								var arr_constraint = touch_infos[prop_name],
									val = ct[prop_name];

								touch[prop_name].set(val);
								arr_constraint.set(i, val);
							});
						} else {
							console.error("Could not find touch");
						}
					} else {
						touch_start_obj = {};
						touch = {};

						_.each(touch_props, function(prop_name) {
							var arr_constraint = touch_infos[prop_name],
								val = ct[prop_name];

							arr_constraint.push(val);

							touch[prop_name] = cjs(val);
							touch_start_obj[prop_name] = val;
						});

						touches[identifier] = touch;
						touch_starts[identifier] = touch_start_obj;
						touch_ends[identifier] = false;
					}
				});

				event.preventDefault();
				cjs.signal();
			},
			touch_move_listener = function(event) {
				cjs.wait();

				_.each(event.changedTouches, function(ct) {
					var identifier = ct.identifier,
						touch_start_obj = {};

					if(touches.hasOwnProperty(identifier)) {
						var index = touch_infos.identifier.indexOf(identifier),
							touch = touches[identifier];

						if(index >= 0) {
							_.each(touch_props, function(prop_name) {
								var arr_constraint = touch_infos[prop_name],
									val = ct[prop_name];

								arr_constraint.set(i, val);
								touch[prop_name].set(val);
							});
						}
					} else {
						console.error("Could not find changed touch");
					}
				});

				event.preventDefault();
				cjs.signal();
			},
			touch_end_listener = function(event) {
				cjs.wait();

				_.each(event.changedTouches, function(ct) {
					var identifier = ct.identifier;

					if(touch_starts.hasOwnProperty(identifier)) {
						var index = touch_infos.identifier.indexOf(identifier);
						if(index >= 0) {
							_.each(touch_props, function(prop_name) {
								var arr_constraint = touch_infos[prop_name],
									val = ct[prop_name];

								arr_constraint.set(i, val);
							});
						}
					} else {
						console.error("Could not find ended touch");
					}
				});

				event.preventDefault();
				cjs.signal();
			},
			addTouchListeners = function() {
				window.addEventListener("touchstart", touch_start_listener);
				window.addEventListener("touchmove", touch_move_listener);
				window.addEventListener("touchend", touch_end_listener);
			},
			removeTouchListeners = function() {
				window.removeEventListener("touchstart", touch_start_listener);
				window.removeEventListener("touchmove", touch_move_listener);
				window.removeEventListener("touchend", touch_end_listener);
			},
			destroy = function(silent) {
				cjs.wait();

				removeTouchListeners();
				touch_count.destroy(silent);

				cjs.signal();
			},
			touch_count = cjs(function() {
				return touch_infos.identifiers.length();
			}),
			device_touchscreen = new ist.Dict({has_protos: false, value: {
					finger_count: touch_count
				}
			});
		device_touchscreen.destroy = function() {
			ist.Dict.prototype.destroy.apply(this, arguments);
			destroy();
		};
		device_touchscreen.__is_touchscreen_device__ = true;
		addTouchListeners();
		return device_touchscreen;
	};
	ist.register_serializable_type("ist_device_touchscreen",
		function (x) {
			return x.__is_touchscreen_device__;
		},
		function () {
			return {};
		},
		function (obj) {
			return ist.createTouchscreenObject();
		});

	// accelorometer
	ist.createAccelorometerObject = function() {
		var x = cjs(0),
			y = cjs(0),
			z = cjs(0),
			motion_listener = function(event) {
				cjs.wait();
				x.set(event.accelerationIncludingGravity.x);
				y.set(event.accelerationIncludingGravity.y);
				z.set(event.accelerationIncludingGravity.z);
				cjs.signal();
			},
			addAccelorometerListeners = function() {
				window.addEventListener("devicemotion", motion_listener);
			},
			removeAccelorometerListeners = function() {
				window.removeEventListener("devicemotion", motion_listener);
			},
			destroy = function(silent) {
				cjs.wait();
				removeAccelorometerListeners();
				x.destroy(silent);
				y.destroy(silent);
				z.destroy(silent);
				cjs.signal();
			},
			device_accelorometer = new ist.Dict({has_protos: false, value: {
					x: x,
					y: y,
					z: z
				}
			});
		device_accelorometer.destroy = function() {
			ist.Dict.prototype.destroy.apply(this, arguments);
			destroy();
		};
		device_accelorometer.__is_accelorometer_device__ = true;
		addAccelorometerListeners();
		return device_accelorometer;
	};
	ist.register_serializable_type("ist_device_accelorometer",
		function (x) {
			return x.__is_accelorometer_device__;
		},
		function () {
			return {};
		},
		function (obj) {
			return ist.createAccelorometerObject();
		});

	// gyroscope
	ist.createGyroscopeObject = function() {
		var alpha = cjs(0),
			beta = cjs(0),
			gamma = cjs(0),
			heading = cjs(0),
			accuracy = cjs(0),
			motion_listener = function(event) {
				cjs.wait();
				alpha.set(event.alpha);
				beta.set(event.beta);
				gamma.set(event.gamma);
				heading.set(event.heading);
				accuracy.set(event.accuracy);
				cjs.signal();
			},
			addGyroscopeListeners = function() {
				window.addEventListener("deviceorientation", motion_listener);
			},
			removeGyroscopeListeners = function() {
				window.removeEventListener("deviceorientation", motion_listener);
			},
			destroy = function(silent) {
				cjs.wait();
				removeGyroscopeListeners();
				alpha.destroy(silent);
				beta.destroy(silent);
				gamma.destroy(silent);
				heading.destroy(silent);
				accuracy.destroy(silent);
				cjs.signal();
			},
			device_gyroscope = new ist.Dict({has_protos: false, value: {
					alpha: alpha,
					beta: beta,
					gamma: gamma,
					heading: heading,
					accuracy: accuracy
				}
			});
		device_gyroscope.destroy = function() {
			ist.Dict.prototype.destroy.apply(this, arguments);
			destroy();
		};
		device_gyroscope.__is_gyroscope_device__ = true;
		addGyroscopeListeners();
		return device_gyroscope;
	};
	ist.register_serializable_type("ist_device_gyroscope",
		function (x) {
			return x.__is_gyroscope_device__;
		},
		function () {
			return {};
		},
		function (obj) {
			return ist.createGyroscopeObject();
		});

}(interstate));
<commit_msg>Added width and height<commit_after>/*jslint nomen: true, vars: true */
/*global interstate,esprima,able,uid,console */

(function (ist) {
	"use strict";
	var cjs = ist.cjs,
		_ = ist._;

	// mouse
	ist.createMouseObject = function() {
		var clientX = cjs(0),
			clientY = cjs(0),
			pageX = cjs(0),
			pageY = cjs(0),
			screenX = cjs(0),
			screenY = cjs(0),
			move_listener = function(event) {
				cjs.wait();
				clientX.set(event.clientX);
				clientY.set(event.clientY);
				pageX.set(event.pageX);
				pageY.set(event.pageY);
				screenX.set(event.screenX);
				screenY.set(event.screenY);
				cjs.signal();
			},
			addMouseListeners = function() {
				window.addEventListener("mousemove", move_listener);
			},
			removeMouseListeners = function() {
				window.removeEventListener("mousemove", move_listener);
			},
			destroy = function(silent) {
				cjs.wait();

				removeMouseListeners();
				clientX.destroy(silent);
				clientY.destroy(silent);
				pageX.destroy(silent);
				pageY.destroy(silent);
				screenX.destroy(silent);
				screenY.destroy(silent);

				cjs.signal();
			},
			device_mouse = new ist.Dict({has_protos: false, value: {
					x: pageX,
					y: pageY,
					clientX: clientX,
					clientY: clientY,
					pageX: pageX,
					pageY: pageY,
					screenX: screenX,
					screenY: screenY
				}
			});
		device_mouse.destroy = function() {
			ist.Dict.prototype.destroy.apply(this, arguments);
			destroy();
		};
		device_mouse.__is_mouse_device__ = true;
		addMouseListeners();
		return device_mouse;
	};
	ist.register_serializable_type("ist_device_mouse",
		function (x) {
			return x.__is_mouse_device__;
		},
		function () {
			return {};
		},
		function (obj) {
			return ist.createMouseObject();
		});

	// keyboard
	ist.createKeyboardObject = function() {
		var key_codes = {
				shift: 16,
				ctrl: 17,
				alt: 18,
				cmd_left: 91,
				cmd_right: 93
			},
			interesting_keycodes = {},
			keyboard_info = {};

		_.each(key_codes, function(code, name) {
			keyboard_info[name] = cjs(false);
			interesting_keycodes[code] = name;
		});

		var keydown_listener = function(event) {
				var keyCode = event.keyCode,
					name = interesting_keycodes[keyCode];
				if(name) {
					keyboard_info[name].set(true);
					event.preventDefault();
				}
			},
			keyup_listener = function(event) {
				var keyCode = event.keyCode,
					name = interesting_keycodes[keyCode];

				if(interesting_keycodes[keyCode]) {
					keyboard_info[name].set(false);
					event.preventDefault();
				}
			},
			addKeyboardListeners = function() {
				window.addEventListener("keydown", keydown_listener);
				window.addEventListener("keyup", keyup_listener);
			},
			removeKeyboardListeners = function() {
				window.removeEventListener("keydown", keydown_listener);
				window.removeEventListener("keyup", keyup_listener);
			},
			destroy = function(silent) {
				cjs.wait();

				removeKeyboardListeners();

				shift.destroy(silent);
				ctrl.destroy(silent);
				alt.destroy(silent);
				cmd_left.destroy(silent);
				cmd_right.destroy(silent);
				cmd.destroy(silent);

				cjs.signal();
			},
			shift = keyboard_info.shift,
			ctrl = keyboard_info.ctrl,
			alt = keyboard_info.alt,
			cmd_left = keyboard_info.cmd_left,
			cmd_right = keyboard_info.cmd_right,
			cmd = cmd_left.or(cmd_right),
			device_keyboard = new ist.Dict({has_protos: false, value: {
					shift: shift,
					ctrl: ctrl,
					alt: alt,
					cmd_left: cmd_left,
					cmd_right: cmd_right,
					cmd: cmd
				}
			});
		device_keyboard.destroy = function() {
			ist.Dict.prototype.destroy.apply(this, arguments);
			destroy();
		};
		device_keyboard.__is_keyboard_device__ = true;
		addKeyboardListeners();
		return device_keyboard;
	};
	ist.register_serializable_type("ist_device_keyboard",
		function (x) {
			return x.__is_keyboard_device__;
		},
		function () {
			return {};
		},
		function (obj) {
			return ist.createKeyboardObject();
		});

	// touches
	ist.createTouchscreenObject = function() {
		var touch_props = ["identifier",
							"screenX", "screenY",
							"clientX", "clientY",
							"pageX", "pageY",
							"radiusX", "radiusY",
							"rotationAngle"],
			touch_infos = {};

		_.each(touch_props, function(prop_name) {
			touch_infos[prop_name] = cjs([]);
		});

		var touch_starts = {},
			touch_ends = {},
			touches = {},
			touch_start_listener = function(event) {
				cjs.wait();

				_.each(event.changedTouches, function(ct) {
					var identifier = ct.identifier,
						touch, touch_start_obj;

					if(touches.hasOwnProperty(identifier)) {
						var index = touch_infos.identifier.indexOf(identifier);

						touch = touches[identifier];

						if(index >= 0) {
							_.each(touch_props, function(prop_name) {
								var arr_constraint = touch_infos[prop_name],
									val = ct[prop_name];

								touch[prop_name].set(val);
								arr_constraint.set(i, val);
							});
						} else {
							console.error("Could not find touch");
						}
					} else {
						touch_start_obj = {};
						touch = {};

						_.each(touch_props, function(prop_name) {
							var arr_constraint = touch_infos[prop_name],
								val = ct[prop_name];

							arr_constraint.push(val);

							touch[prop_name] = cjs(val);
							touch_start_obj[prop_name] = val;
						});

						touches[identifier] = touch;
						touch_starts[identifier] = touch_start_obj;
						touch_ends[identifier] = false;
					}
				});

				event.preventDefault();
				cjs.signal();
			},
			touch_move_listener = function(event) {
				cjs.wait();

				_.each(event.changedTouches, function(ct) {
					var identifier = ct.identifier,
						touch_start_obj = {};

					if(touches.hasOwnProperty(identifier)) {
						var index = touch_infos.identifier.indexOf(identifier),
							touch = touches[identifier];

						if(index >= 0) {
							_.each(touch_props, function(prop_name) {
								var arr_constraint = touch_infos[prop_name],
									val = ct[prop_name];

								arr_constraint.set(i, val);
								touch[prop_name].set(val);
							});
						}
					} else {
						console.error("Could not find changed touch");
					}
				});

				event.preventDefault();
				cjs.signal();
			},
			touch_end_listener = function(event) {
				cjs.wait();

				_.each(event.changedTouches, function(ct) {
					var identifier = ct.identifier;

					if(touch_starts.hasOwnProperty(identifier)) {
						var index = touch_infos.identifier.indexOf(identifier);
						if(index >= 0) {
							_.each(touch_props, function(prop_name) {
								var arr_constraint = touch_infos[prop_name],
									val = ct[prop_name];

								arr_constraint.set(i, val);
							});
						}
					} else {
						console.error("Could not find ended touch");
					}
				});

				event.preventDefault();
				cjs.signal();
			},
			addTouchListeners = function() {
				window.addEventListener("touchstart", touch_start_listener);
				window.addEventListener("touchmove", touch_move_listener);
				window.addEventListener("touchend", touch_end_listener);
			},
			removeTouchListeners = function() {
				window.removeEventListener("touchstart", touch_start_listener);
				window.removeEventListener("touchmove", touch_move_listener);
				window.removeEventListener("touchend", touch_end_listener);
			},
			destroy = function(silent) {
				cjs.wait();

				removeTouchListeners();
				touch_count.destroy(silent);

				cjs.signal();
			},
			touch_count = cjs(function() {
				return touch_infos.identifiers.length();
			}),
			device_touchscreen = new ist.Dict({has_protos: false, value: {
					finger_count: touch_count
				}
			});
		device_touchscreen.destroy = function() {
			ist.Dict.prototype.destroy.apply(this, arguments);
			destroy();
		};
		device_touchscreen.__is_touchscreen_device__ = true;
		addTouchListeners();
		return device_touchscreen;
	};
	ist.register_serializable_type("ist_device_touchscreen",
		function (x) {
			return x.__is_touchscreen_device__;
		},
		function () {
			return {};
		},
		function (obj) {
			return ist.createTouchscreenObject();
		});

	// accelorometer
	ist.createAccelorometerObject = function() {
		var x = cjs(0),
			y = cjs(0),
			z = cjs(0),
			motion_listener = function(event) {
				cjs.wait();
				x.set(event.accelerationIncludingGravity.x);
				y.set(event.accelerationIncludingGravity.y);
				z.set(event.accelerationIncludingGravity.z);
				cjs.signal();
			},
			addAccelorometerListeners = function() {
				window.addEventListener("devicemotion", motion_listener);
			},
			removeAccelorometerListeners = function() {
				window.removeEventListener("devicemotion", motion_listener);
			},
			destroy = function(silent) {
				cjs.wait();
				removeAccelorometerListeners();
				x.destroy(silent);
				y.destroy(silent);
				z.destroy(silent);
				cjs.signal();
			},
			device_accelorometer = new ist.Dict({has_protos: false, value: {
					x: x,
					y: y,
					z: z
				}
			});
		device_accelorometer.destroy = function() {
			ist.Dict.prototype.destroy.apply(this, arguments);
			destroy();
		};
		device_accelorometer.__is_accelorometer_device__ = true;
		addAccelorometerListeners();
		return device_accelorometer;
	};
	ist.register_serializable_type("ist_device_accelorometer",
		function (x) {
			return x.__is_accelorometer_device__;
		},
		function () {
			return {};
		},
		function (obj) {
			return ist.createAccelorometerObject();
		});

	// gyroscope
	ist.createGyroscopeObject = function() {
		var alpha = cjs(0),
			beta = cjs(0),
			gamma = cjs(0),
			heading = cjs(0),
			accuracy = cjs(0),
			motion_listener = function(event) {
				cjs.wait();
				alpha.set(event.alpha);
				beta.set(event.beta);
				gamma.set(event.gamma);
				heading.set(event.heading);
				accuracy.set(event.accuracy);
				cjs.signal();
			},
			addGyroscopeListeners = function() {
				window.addEventListener("deviceorientation", motion_listener);
			},
			removeGyroscopeListeners = function() {
				window.removeEventListener("deviceorientation", motion_listener);
			},
			destroy = function(silent) {
				cjs.wait();
				removeGyroscopeListeners();
				alpha.destroy(silent);
				beta.destroy(silent);
				gamma.destroy(silent);
				heading.destroy(silent);
				accuracy.destroy(silent);
				cjs.signal();
			},
			device_gyroscope = new ist.Dict({has_protos: false, value: {
					alpha: alpha,
					beta: beta,
					gamma: gamma,
					heading: heading,
					accuracy: accuracy
				}
			});
		device_gyroscope.destroy = function() {
			ist.Dict.prototype.destroy.apply(this, arguments);
			destroy();
		};
		device_gyroscope.__is_gyroscope_device__ = true;
		addGyroscopeListeners();
		return device_gyroscope;
	};
	ist.register_serializable_type("ist_device_gyroscope",
		function (x) {
			return x.__is_gyroscope_device__;
		},
		function () {
			return {};
		},
		function (obj) {
			return ist.createGyroscopeObject();
		});

	// gyroscope
	ist.getWidthHeight = function() {
		var width = cjs(window.innerWidth),
			height = cjs(window.innerHeight),
			resize_listener = function(event) {
				cjs.wait();
				width.set(window.innerWidth);
				height.set(window.innerHeight);
				cjs.signal();
			},
			addListeners = function() {
				window.addEventListener("resize", resize_listener);
			},
			removeListeners = function() {
				window.removeEventListener("resize", resize_listener);
			},
			destroy = function(silent) {
				cjs.wait();
				removeListeners();
				width.destroy(silent);
				height.destroy(silent);
				cjs.signal();
			},
			device_gyroscope = new ist.Dict({has_protos: false, value: {
					alpha: alpha,
					beta: beta,
					gamma: gamma,
					heading: heading,
					accuracy: accuracy
				}
			});
		device_gyroscope.destroy = function() {
			ist.Dict.prototype.destroy.apply(this, arguments);
			destroy();
		};
		addListeners();
		return device_gyroscope;
	};

}(interstate));
<|endoftext|>