<commit_before>export default class MetadataProvider {

    /**
     * Returns a cloned schema which is canonical, meaning that every 'entities', 'fields', 'layouts' and 'groups' are
     * arrays and not objects.
     * @param schema
     * @returns {*}
     */
    static canonizeSchema(schema) {
        if (!schema) throw Error('\'schema\' should be truthy');

        if(!schema.entities) {
            // when no entities are specified in the schema, the schema is considered to be in the SIMPLEST form, example:
            // {
            //     name: {
            //         type: 'string'
            //     },
            //     dateOfBirth: {
            //         type: 'string'
            //     }
            // }
            // OR...
            // [
            //     { name: 'name', type: 'string' },
            //     { name: 'dateOfBirth', type: 'date' },
            // ]
            // In this case, I'm just creating a 'default' entity
            return MetadataProvider.canonizeSchema({
                entities: [
                    {
                        name: 'default',
                        fields: schema
                    }
                ]
            });
        }

        schema = {...schema};
        this._canonizeArrays(schema, ["entities", "layouts", "groups"]);

        //schema.entities = MetadataProvider.canonizeArray(schema.entities);
        //_.each(schema.entities, entity => {
        //    entity.fields = MetadataProvider.canonizeArray(entity.fields);
        //    entity.layouts = MetadataProvider.canonizeArray(entity.layouts);
        //    _.each(entity.layouts, layout => {
        //        layout.fields = MetadataProvider.canonizeArray(layout.fields);
        //        layout.groups = MetadataProvider.canonizeArray(layout.groups);
        //        _.each(layout.groups, group => {
        //            group.fields = MetadataProvider.canonizeArray(group.fields);
        //        })
        //    });
        //});

        return schema;
    }

    static _canonizeArrays(dataArray, keys, id = 0) {
        dataArray[keys[id]] = MetadataProvider.canonizeArray(dataArray[keys[id]]);
        if(dataArray[keys[id]]) {
            dataArray[keys[id]].forEach(elem => {
                elem.fields = MetadataProvider.canonizeArray(elem.fields);
                if(id + 1 < keys.length)
                    this._canonizeArrays(elem, keys, id + 1);
            })
        }

    }
    /**
     * Ensures the object passed in is an array. If it is, it is returned, otherwise, this function
     * converts the target object into an array.
     * This is important to convert this:
     *     { fields: { dateOfBirth: { type: 'string' } } // this should be acceptable as a fields definition
     * into this:
     *     { fields: [{ name: 'dateOfBirth', type: 'string' }] } // this is the canonical field definition
     * @param obj
     */
    static canonizeArray(obj) {
        if (!obj) return obj; // this is so the canonizeSchema method doesn't have to check every property for undefined

        if (Array.isArray(obj))
            return obj;

        // let's create an array
        return Object.keys(obj).map((property) => {
            let isObject = obj[property] && typeof obj[property] === "object";
            if (!isObject)
                throw Error('cannot generate canonical array. Every field should be an object');

            return {name: property, ...obj[property]};
        });
    }

    /**
     * Validates a field metadata
     * @param metadata
     * @private
     */
    static validateFieldMetadata(metadata) {
        if (!metadata) throw Error('metadata should not be null or undefined');
        if (!metadata.name) throw Error('metadata\'s "name" property is required');
        if (!metadata.type) throw Error('metadata\'s "type" property is required');
    }

    /**
     * Gets a raw entity from the given schema. No processing.
     * @param schema
     * @param entityName
     * @returns {*}
     */
    static getEntity(schema, entityName) {
        if (!schema) throw Error('\'schema\' should be truthy');
        if (schema.entities === undefined || schema.entities === null || !schema.entities.length)
            throw Error('schema should have entities');

        let entity;

        if (entityName) {
            entity = schema.entities.find(e => e.name === entityName);
        } else {
            if(schema.entities.length != 1)
                throw Error('When an entityName is not specified, there must be one and only one entity');
            entity = schema.entities[0];
        }

        if (!entity) throw Error(`Could not find entity. Entity name: ${entityName}`);

        return entity;
    }

    /**
     * Gets a raw layout from the given entity. No processing.
     * @param entity
     * @param layoutName
     * @returns {*}
     */
    static getLayout(entity, layoutName) {
        if (!entity) throw Error('\'entity\' should be truthy');
        let layout;

        if (layoutName) {
            layout = entity.layouts ? entity.layouts.find(l => l.name === layoutName) : layout;
        } else {
            if(entity.layouts.length != 1)
                throw Error('When the layoutName is not specified, there must be one and only one layout');
            layout = entity.layouts[0];
        }

        if (!layout) throw Error(`Could not find layout. Layout name: ${layoutName}`);

        return layout;
    }

    /**
     * Gets a raw entity and a raw layout from the given schema.
     * @param schema
     * @param entityName
     * @param layoutName
     * @returns {{entity: *, layout: *}}
     */
    static getEntityAndLayout(schema, entityName, layoutName) {
        let entity = this.getEntity(schema, entityName);
        let layout = layoutName ? this.getLayout(entity, layoutName) : this.generateDefaultLayout(schema, entity);

        return {
            entity: entity,
            layout: layout
        }
    }

    /**
     * Internal method for merging entity and layout fields
     * @param schema
     * @param entity
     * @param layout
     * @param partialResult
     * @param callback
     * @return {*[]}
     */
    static getFieldsInternal(schema, entity, layout, partialResult, callback) {

        if (!schema) throw Error('\'schema\' should be truthy');
        if (!entity) throw Error('\'entity\' should be truthy');
        if (!layout.fields && !layout.groups) throw Error('A layout should have fields or groups');

        partialResult = partialResult || [];
        let thisGroupFields = [];

        if (layout.groups) {
            layout.groups.forEach(g => {
                thisGroupFields = [... new Set(thisGroupFields.concat(this.getFieldsInternal(schema, entity, g, partialResult, callback)))];
            });
        }

        if (layout.fields) {

            for (let i = 0; i < layout.fields.length; i++) {

                let groupField = layout.fields[i];
                let existingEntityProperty = entity.fields.find(field => field.name == groupField.name);

                let field = {...existingEntityProperty || {}, ...groupField};
                this.validateFieldMetadata(field);

                thisGroupFields.push(field);

                if (field.type == 'entity') {

                    if (!field.entityName) {
                        throw Error('when a field is of type \'entity\', it needs to specify an \'entityName\'')
                    }

                    let entityAndLayout = this.getEntityAndLayout(schema, field.entityName, field.layoutName);
                    field.layout = this.processLayout(schema, entityAndLayout.entity, entityAndLayout.layout);
                    field.fields = this.getFieldsInternal(schema, entityAndLayout.entity, entityAndLayout.layout, partialResult, callback);
                }

                if (field.type == 'array') {
                    if (!field.arrayType) {
                        throw Error('when a field is of type \'array\', it needs to specify an \'arrayType\'');
                    }

                    if (field.arrayType != 'entity') {
                        throw Error('only entity arrays are currently supported');
                    }

                    if (!field.entityType) {
                        throw Error('when a field is of type \'array\' and arrayType is \'entity\', it needs to specify an \'entityType\'');
                    }

                    let entityAndLayout = this.getEntityAndLayout(schema, field.entityType, field.layoutName);
                    field.layout = this.processLayout(schema, entityAndLayout.entity, entityAndLayout.layout);
                    field.fields = this.getFieldsInternal(schema, entityAndLayout.entity, entityAndLayout.layout, partialResult, callback);
                }

                if (callback) {
                    callback(field);
                }
            }
        }

        return [...new Set(partialResult.concat(thisGroupFields))];
    }

    /**
     * Merges the given field collection
     * @param schema
     * @param entity
     * @param layout
     * @param callback
     * @external https://github.com/gearz-lab/react-metaform/blob/master/docs-md/MetadataProvider.md
     */
    static getFields(schema, entity, layout, callback) {
        entity = typeof entity === 'string' ? this.getEntity(schema, entity) : entity;

        if (!layout) {
            layout = this.generateDefaultLayout(schema, entity);
        } else {
            layout = typeof layout === 'string' ? this.getLayout(entity, layout) : layout;
        }

        return this.getFieldsInternal(schema, entity, layout, undefined, callback);
    }

    /**
     * Creates a clone of the given layout-group that maintains only the hierarchy and the 'name' property
     * for fields
     * @param layoutGroup
     * @returns {Object}
     */
    static processLayoutGroup(layoutGroup) {
        if (!layoutGroup) throw Error('\'layoutGroup\' should be truthy');

        let layoutGroupClone = {};
        if (layoutGroup.fields) {
            layoutGroupClone.fields = [];

            for (let i = 0; i < layoutGroup.fields.length; i++) {
                layoutGroupClone.fields.push({name: layoutGroup.fields[i].name});
            }

        } else if (layoutGroup.groups) {
            layoutGroupClone.groups = [];

            for (let i = 0; i < layoutGroup.groups.length; i++) {
                layoutGroupClone.groups.push(this.processLayoutGroup(layoutGroup.groups[i]));
            }
        }

        // copying useful properties
        layoutGroupClone.orientation = layoutGroup.orientation;
        layoutGroupClone.component = layoutGroup.component;
        layoutGroupClone.title = layoutGroup.title;

        return layoutGroupClone;
    }

    static processLayout(schema, entity, layout) {
        entity = typeof entity === 'string' ? this.getEntity(schema, entity) : entity;
        layout = typeof layout === 'string' ? this.getLayout(entity, layout) : layout;

        return this.processLayoutGroup(layout);
    }

    /**
     * Generates a default layout for the given entity. Useful so it's not obligatory to implement layouts.
     * @param schema application schema
     * @param entity
     */
    static generateDefaultLayout(schema, entity) {
        entity = typeof entity === 'string' ? this.getEntity(schema, entity) : entity;
        return {
            name: `${entity.name}-default`,
            fields: entity.fields.map(f => {
                return {
                    name: f.name
                };
            })
        };
    }

    /**
     * Gets the field-list for Redux-Form
     * @param fieldMetadata
     * @param prefix
     */
    static getReduxFormFields(fieldMetadata, prefix) {
        if (!fieldMetadata) throw Error('fieldMetadata should be truthy');
        let result = [];

        fieldMetadata.forEach(f => {
            if (f.fields) {
                // if a field has fields, it's either an array or a complex object
                let fieldPrefix = f.type == 'array' ? `${f.name}[]` : f.name;
                let totalPrefix = prefix ? `${prefix}.${fieldPrefix}` : fieldPrefix;
                this.getReduxFormFields(f.fields, totalPrefix).map(f2 => result.push(f2));
            } else {
                result.push(prefix ? `${prefix}.${f.name}` : f.name)
            }
        });

        return result;
    }
};<commit_msg>Delete unneeded comments Added merge method to simplify read of MetadataProvider code<commit_after>export default class MetadataProvider {

    /**
     * Returns a cloned schema which is canonical, meaning that every 'entities', 'fields', 'layouts' and 'groups' are
     * arrays and not objects.
     * @param schema
     * @returns {*}
     */
    static canonizeSchema(schema) {
        if (!schema) throw Error('\'schema\' should be truthy');

        if(!schema.entities) {
            // when no entities are specified in the schema, the schema is considered to be in the SIMPLEST form, example:
            // {
            //     name: {
            //         type: 'string'
            //     },
            //     dateOfBirth: {
            //         type: 'string'
            //     }
            // }
            // OR...
            // [
            //     { name: 'name', type: 'string' },
            //     { name: 'dateOfBirth', type: 'date' },
            // ]
            // In this case, I'm just creating a 'default' entity
            return MetadataProvider.canonizeSchema({
                entities: [
                    {
                        name: 'default',
                        fields: schema
                    }
                ]
            });
        }

        schema = {...schema};
        this._canonizeArrays(schema, ["entities", "layouts", "groups"]);

        return schema;
    }

    static _canonizeArrays(dataArray, keys, id = 0) {
        dataArray[keys[id]] = MetadataProvider.canonizeArray(dataArray[keys[id]]);
        if(dataArray[keys[id]]) {
            dataArray[keys[id]].forEach(elem => {
                elem.fields = MetadataProvider.canonizeArray(elem.fields);
                if(id + 1 < keys.length)
                    this._canonizeArrays(elem, keys, id + 1);
            })
        }

    }
    /**
     * Ensures the object passed in is an array. If it is, it is returned, otherwise, this function
     * converts the target object into an array.
     * This is important to convert this:
     *     { fields: { dateOfBirth: { type: 'string' } } // this should be acceptable as a fields definition
     * into this:
     *     { fields: [{ name: 'dateOfBirth', type: 'string' }] } // this is the canonical field definition
     * @param obj
     */
    static canonizeArray(obj) {
        // this is so the canonizeSchema method doesn't have to check every property for undefined
        if (!obj) return obj;

        if (Array.isArray(obj))
            return obj;

        // let's create an array
        return Object.keys(obj).map((property) => {
            let isObject = obj[property] && typeof obj[property] === "object";
            if (!isObject)
                throw Error('cannot generate canonical array. Every field should be an object');

            return {name: property, ...obj[property]};
        });
    }

    /**
     * Validates a field metadata
     * @param metadata
     * @private
     */
    static validateFieldMetadata(metadata) {
        if (!metadata) throw Error('metadata should not be null or undefined');
        if (!metadata.name) throw Error('metadata\'s "name" property is required');
        if (!metadata.type) throw Error('metadata\'s "type" property is required');
    }

    /**
     * Gets a raw entity from the given schema. No processing.
     * @param schema
     * @param entityName
     * @returns {*}
     */
    static getEntity(schema, entityName) {
        if (!schema) throw Error('\'schema\' should be truthy');
        if (schema.entities === undefined || schema.entities === null || !schema.entities.length)
            throw Error('schema should have entities');

        let entity;

        if (entityName) {
            entity = schema.entities.find(e => e.name === entityName);
        } else {
            if(schema.entities.length != 1)
                throw Error('When an entityName is not specified, there must be one and only one entity');
            entity = schema.entities[0];
        }

        if (!entity) throw Error(`Could not find entity. Entity name: ${entityName}`);

        return entity;
    }

    /**
     * Gets a raw layout from the given entity. No processing.
     * @param entity
     * @param layoutName
     * @returns {*}
     */
    static getLayout(entity, layoutName) {
        if (!entity) throw Error('\'entity\' should be truthy');
        let layout;

        if (layoutName) {
            layout = entity.layouts ? entity.layouts.find(l => l.name === layoutName) : layout;
        } else {
            if(entity.layouts.length != 1)
                throw Error('When the layoutName is not specified, there must be one and only one layout');
            layout = entity.layouts[0];
        }

        if (!layout) throw Error(`Could not find layout. Layout name: ${layoutName}`);

        return layout;
    }

    /**
     * Gets a raw entity and a raw layout from the given schema.
     * @param schema
     * @param entityName
     * @param layoutName
     * @returns {{entity: *, layout: *}}
     */
    static getEntityAndLayout(schema, entityName, layoutName) {
        let entity = this.getEntity(schema, entityName);
        let layout = layoutName ? this.getLayout(entity, layoutName) : this.generateDefaultLayout(schema, entity);

        return {
            entity: entity,
            layout: layout
        }
    }

    /**
     * Internal method for merging entity and layout fields
     * @param schema
     * @param entity
     * @param layout
     * @param partialResult
     * @param callback
     * @return {*[]}
     */
    static getFieldsInternal(schema, entity, layout, partialResult, callback) {
        if (!schema) throw Error('\'schema\' should be truthy');
        if (!entity) throw Error('\'entity\' should be truthy');
        if (!layout.fields && !layout.groups) throw Error('A layout should have fields or groups');

        partialResult = partialResult || [];
        let thisGroupFields = [];

        if (layout.groups) {
            layout.groups.forEach(g => {
                let fieldsInternal = this.getFieldsInternal(schema, entity, g, partialResult, callback);
                thisGroupFields = this.merge(thisGroupFields, fieldsInternal);
            });
        }

        if (layout.fields) {
            for (let i = 0; i < layout.fields.length; i++) {

                let groupField = layout.fields[i];
                let existingEntityProperty = entity.fields.find(field => field.name == groupField.name);

                let field = {...existingEntityProperty || {}, ...groupField};
                this.validateFieldMetadata(field);

                thisGroupFields.push(field);

                if (field.type == 'entity') {

                    if (!field.entityName) {
                        throw Error('when a field is of type \'entity\', it needs to specify an \'entityName\'')
                    }

                    let entityAndLayout = this.getEntityAndLayout(schema, field.entityName, field.layoutName);
                    field.layout = this.processLayout(schema, entityAndLayout.entity, entityAndLayout.layout);
                    field.fields = this.getFieldsInternal(schema, entityAndLayout.entity, entityAndLayout.layout, partialResult, callback);
                }

                if (field.type == 'array') {
                    if (!field.arrayType) {
                        throw Error('when a field is of type \'array\', it needs to specify an \'arrayType\'');
                    }

                    if (field.arrayType != 'entity') {
                        throw Error('only entity arrays are currently supported');
                    }

                    if (!field.entityType) {
                        throw Error('when a field is of type \'array\' and arrayType is \'entity\', it needs to specify an \'entityType\'');
                    }

                    let entityAndLayout = this.getEntityAndLayout(schema, field.entityType, field.layoutName);
                    field.layout = this.processLayout(schema, entityAndLayout.entity, entityAndLayout.layout);
                    field.fields = this.getFieldsInternal(schema, entityAndLayout.entity, entityAndLayout.layout, partialResult, callback);
                }

                if (callback) {
                    callback(field);
                }
            }
        }

        return this.merge(partialResult, thisGroupFields);
    }

    /**
     * Merges the given field collection
     * @param schema
     * @param entity
     * @param layout
     * @param callback
     * @external https://github.com/gearz-lab/react-metaform/blob/master/docs-md/MetadataProvider.md
     */
    static getFields(schema, entity, layout, callback) {
        entity = typeof entity === 'string' ? this.getEntity(schema, entity) : entity;

        if (!layout) {
            layout = this.generateDefaultLayout(schema, entity);
        } else {
            layout = typeof layout === 'string' ? this.getLayout(entity, layout) : layout;
        }

        return this.getFieldsInternal(schema, entity, layout, undefined, callback);
    }

    /**
     * Creates a clone of the given layout-group that maintains only the hierarchy and the 'name' property
     * for fields
     * @param layoutGroup
     * @returns {Object}
     */
    static processLayoutGroup(layoutGroup) {
        if (!layoutGroup) throw Error('\'layoutGroup\' should be truthy');

        let layoutGroupClone = {};
        if (layoutGroup.fields) {
            layoutGroupClone.fields = [];

            for (let i = 0; i < layoutGroup.fields.length; i++) {
                layoutGroupClone.fields.push({name: layoutGroup.fields[i].name});
            }

        } else if (layoutGroup.groups) {
            layoutGroupClone.groups = [];

            for (let i = 0; i < layoutGroup.groups.length; i++) {
                layoutGroupClone.groups.push(this.processLayoutGroup(layoutGroup.groups[i]));
            }
        }

        // copying useful properties
        layoutGroupClone.orientation = layoutGroup.orientation;
        layoutGroupClone.component = layoutGroup.component;
        layoutGroupClone.title = layoutGroup.title;

        return layoutGroupClone;
    }

    static processLayout(schema, entity, layout) {
        entity = typeof entity === 'string' ? this.getEntity(schema, entity) : entity;
        layout = typeof layout === 'string' ? this.getLayout(entity, layout) : layout;

        return this.processLayoutGroup(layout);
    }

    /**
     * Generates a default layout for the given entity. Useful so it's not obligatory to implement layouts.
     * @param schema application schema
     * @param entity
     */
    static generateDefaultLayout(schema, entity) {
        entity = typeof entity === 'string' ? this.getEntity(schema, entity) : entity;
        return {
            name: `${entity.name}-default`,
            fields: entity.fields.map(f => {
                return {
                    name: f.name
                };
            })
        };
    }

    /**
     * Gets the field-list for Redux-Form
     * @param fieldMetadata
     * @param prefix
     */
    static getReduxFormFields(fieldMetadata, prefix) {
        if (!fieldMetadata) throw Error('fieldMetadata should be truthy');
        let result = [];

        fieldMetadata.forEach(f => {
            if (f.fields) {
                // if a field has fields, it's either an array or a complex object
                let fieldPrefix = f.type == 'array' ? `${f.name}[]` : f.name;
                let totalPrefix = prefix ? `${prefix}.${fieldPrefix}` : fieldPrefix;
                this.getReduxFormFields(f.fields, totalPrefix).map(f2 => result.push(f2));
            } else {
                result.push(prefix ? `${prefix}.${f.name}` : f.name)
            }
        });

        return result;
    }

    static merge(from, to) {
        if (!from) throw Error('from should be truthy');
        if (!to) throw Error('to should be truthy');

        return [...new Set(from.concat(to))];
    }
};<|endoftext|>


<commit_before>/**
 * autoTab.js
 * @version: v1.0.0
 * @author: Dennis Hernández
 **/
;(function(e){e.autoTab=function(){var t=e("[data-tab]").not("body"),n=null,r=[9,16,37,38,39,40],i=function(t){if(t.value.length<=parseInt(e(t).attr("data-length"))){t.value=t.value.substr(0,parseInt(e(t).attr("data-length")));return true}else{return false}},s=function(t){var n=e('[data-tab="'+t+'"]'),r=0,i;if(!n){return false}if(n.is("input, textarea")){i=n.get(0).value.length;if(n.setSelectionRange){n.setSelectionRange(r,i)}else if(n.createTextRange){var s=n.createTextRange();s.moveStart("character",r);s.moveEnd("character",i-n.value.length);s.select()}n.focus()}},o=function(t){if(e(this).is("body")){return}if(!t){t=window.event}var n=this;if(!e.inArray(t.keyCode,r)){if(n.value.length<n.maxLength){return false}}else{if(n.value.length<parseInt(e(n).attr("data-length"))){return false}else if(n.value.length===parseInt(e(n).attr("data-length"))){s(parseInt(e(n).attr("data-tab"))+1)}}};for(var u=0;u<t.length;u++){n=e(t[u]);n.keyup(o);if(n.is("input")){n.attr("maxlength",n.attr("data-length"));n.attr("autocomplete","off")}}t[0].focus()}})(jQuery);<commit_msg>v1.2..0 min<commit_after>/**
 * autoTab.js
 * @version: v1.2.0
 * @author: Dennis Hernández
 **/
;(function(e){e.autoTab=function(t){var n={autoFocus:false},t=e.extend(n,t),r=e("[data-tab]").not("body"),i=null,s=[9,16,37,38,39,40],o=function(t){if(t.value.length<=parseInt(e(t).attr("data-length"))){t.value=t.value.substr(0,parseInt(e(t).attr("data-length")));return true}else{return false}},u=function(e){var t=f(e),n=0,r;if(!t){return false}if(t.is("input, textarea")){r=t.get(0).value.length;if(t.setSelectionRange){t.setSelectionRange(n,r)}else if(t.createTextRange){var i=t.createTextRange();i.moveStart("character",n);i.moveEnd("character",r-t.value.length);i.select()}t.focus()}},a=function(t){if(e(this).is("body")){return}if(!t){t=window.event}var n=this;if(!e.inArray(t.keyCode,s)){if(n.value.length<n.maxLength){return false}}else{if(n.value.length<parseInt(e(n).attr("data-length"))){return false}else if(n.value.length===parseInt(e(n).attr("data-length"))){u(parseInt(e(n).attr("data-tab"))+1)}}},f=function(t){return e('[data-tab="'+t+'"]')},l=function(){for(var n=0;n<r.length;n++){i=e(r[n]);i.keyup(a);if(i.is("input")){i.attr("maxlength",i.attr("data-length"));i.attr("autocomplete","off")}}if(t.autoFocus){f(0).focus()}};l()}})(jQuery);<|endoftext|>


<commit_before>/**
 * Created by felix on 19.05.16.
 */
import { expect } from 'chai';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
import _ from 'underscore';

require('sinon-as-promised');
require('../../../lib/helpers');

let MinutesCollection = {
    find: sinon.stub(),
    findOne: sinon.stub()
};

class MeteorError {}

let Meteor = {
    call: sinon.stub(),
    callPromise: sinon.stub().resolves(true),
    Error: MeteorError
};

let PromisedMethods = {};

let isCurrentUserModeratorStub = sinon.stub();
let updateLastMinutesDateStub = sinon.stub();
let updateLastMinutesDateAsyncStub = sinon.stub().resolves(true);
let MeetingSeries = function(seriesId) {
    this._id = seriesId;
    this.isCurrentUserModerator = isCurrentUserModeratorStub;
    this.updateLastMinutesDate = updateLastMinutesDateStub;
    this.updateLastMinutesDateAsync = updateLastMinutesDateAsyncStub;
};

let topicGetOpenActionItemsStub = sinon.stub().returns([]);
let Topic = function () {
    this.getOpenActionItems = topicGetOpenActionItemsStub;
};
Topic.hasOpenActionItem = () => { return false; };

let ActionItem = function (topic, doc) {
    this._parentTopic = topic;
    this._infoItemDoc = doc;
};

const {
    Minutes
    } = proxyquire('../../../imports/minutes', {
    'meteor/meteor': { Meteor, '@noCallThru': true},
    './collections/minutes_private': { MinutesCollection, '@noCallThru': true},
    './collections/workflow_private': { null, '@noCallThru': true},
    './helpers/promisedMethods': { PromisedMethods, '@noCallThru': true},
    './meetingseries': { MeetingSeries, '@noCallThru': true},
    './topic': { Topic, '@noCallThru': true},
    './actionitem': { ActionItem, '@noCallThru': true},
    'meteor/underscore': { _, '@noCallThru': true}
});

describe('Minutes', function () {

    let minutesDoc, minute;

    beforeEach(function () {
        minutesDoc = {
            meetingSeries_id: 'AaBbCc01',
            _id: 'AaBbCc02',
            date: '2016-05-06',
            createdAt: new Date(),
            topics: [],
            isFinalized: false,
            participants: '',
            agenda: ''
        };

        minute = new Minutes(minutesDoc);
    });

    afterEach(function () {
        MinutesCollection.find.reset();
        MinutesCollection.findOne.reset();
        Meteor.call.reset();
        Meteor.callPromise.reset();
        isCurrentUserModeratorStub.reset();
        updateLastMinutesDateStub.reset();
        topicGetOpenActionItemsStub.reset();
    });

    describe('#constructor', function () {

        it('sets the properties correctly', function () {
            expect(JSON.stringify(minute)).to.equal(JSON.stringify(minutesDoc));
        });

        it('fetches the minute from the database if the id was given', function() {

            new Minutes(minutesDoc._id);
            expect(MinutesCollection.findOne.calledOnce, "findOne should be called once").to.be.true;
            expect(MinutesCollection.findOne.calledWith(minutesDoc._id), "findOne should be called with the id").to.be.true;
        });

        it('throws exception if constructor will be called without any arguments', function () {
            let exceptionThrown;
            try {
                new Minutes();
                exceptionThrown = false;
            } catch (e) {
                exceptionThrown = (e instanceof MeteorError);
            }

            expect(exceptionThrown).to.be.true;
        });

    });

    describe('find', function () {

        it('#find', function () {
            Minutes.find("myArg");
            expect(MinutesCollection.find.calledOnce, "find-Method should be called once").to.be.true;
            expect(MinutesCollection.find.calledWithExactly("myArg"), "arguments should be passed").to.be.true;
        });

        it('#findOne', function () {
            Minutes.findOne("myArg");
            expect(MinutesCollection.findOne.calledOnce, "findOne-Method should be called once").to.be.true;
            expect(MinutesCollection.findOne.calledWithExactly("myArg"), "arguments should be passed").to.be.true;
        });

        describe('#findAllIn', function () {

            let minIdArray;
            let limit;

            beforeEach(function () {
                minIdArray = ['1', '2'];
                limit = 3;
            });

            it('calls the find-Method of the Collection', function () {
                Minutes.findAllIn(minIdArray, limit);
                expect(MinutesCollection.find.calledOnce, "find-Method should be called once").to.be.true;
            });

            it('sets the id selector correctly', function () {
                Minutes.findAllIn(minIdArray, limit);
                let selector = MinutesCollection.find.getCall(0).args[0];
                expect(selector, "Selector has the property _id").to.have.ownProperty('_id');
                expect(selector._id, '_id-selector has propery $in').to.have.ownProperty('$in');
                expect(selector._id.$in, 'idArray should be passed').to.deep.equal(minIdArray);
            });

            it('sets the option correctly (sort, no limit)', function () {
                let expectedOption = { sort: {date: -1} };
                Minutes.findAllIn(minIdArray);
                let options = MinutesCollection.find.getCall(0).args[1];
                expect(options).to.deep.equal(expectedOption);
            });

            it('sets the option correctly (sort and limit)', function () {
                let expectedOption = { sort: {date: -1}, limit: limit };
                Minutes.findAllIn(minIdArray, limit);
                let options = MinutesCollection.find.getCall(0).args[1];
                expect(options).to.deep.equal(expectedOption);
            });

        });

    });

    describe('#remove', function () {

        it('calls the meteor method minutes.remove', function () {
            Minutes.remove(minute._id);
            expect(Meteor.callPromise.calledOnce).to.be.true;
        });

        it('sends the minutes id to the meteor method minutes.remove', function () {
            Minutes.remove(minute._id);
            expect(Meteor.callPromise.calledWithExactly('workflow.removeMinute', minute._id)).to.be.true;
        });

    });

    describe('#syncVisibility', function () {

        let visibleForArray, parentSeriesId;

        beforeEach(function () {
            visibleForArray = [
                '1',
                '2'
            ];
            parentSeriesId = minute.meetingSeries_id;
        });

        it('calls the meteor method minutes.syncVisibility', function () {
            Minutes.syncVisibility(parentSeriesId, visibleForArray);
            expect(Meteor.callPromise.calledOnce).to.be.true;
        });

        it('sends the parentSeriesId and the visibleFor-array to the meteor method minutes.syncVisibility', function () {
            Minutes.syncVisibility(parentSeriesId, visibleForArray);
            expect(Meteor.callPromise.calledWithExactly('minutes.syncVisibility', parentSeriesId, visibleForArray)).to.be.true;
        });

    });

    describe('#update', function () {

        let updateDocPart;

        beforeEach(function () {
            updateDocPart = {
                date: '2016-05-07'
            }
        });

        it('calls the meteor method minutes.update', function () {
            minute.update(updateDocPart);
            expect(Meteor.callPromise.calledOnce).to.be.true;
        });

        it('sends the doc part and the minutes id to the meteor method minutes.update', function () {
            minute.update(updateDocPart);
            let sentObj = JSON.parse(JSON.stringify(updateDocPart));
            sentObj._id = minute._id;
            expect(Meteor.callPromise.calledWithExactly('minutes.update', sentObj, undefined)).to.be.true;
        });

        it('updates the changed property of the minute object', async function (done) {
            await minute.update(updateDocPart);
            expect(minute.date).to.equal(updateDocPart.date);
            done();
        });

    });

    describe('#save', function () {

        it('calls the meteor method minutes.insert if a new minute will be saved', function () {
            delete minute._id;
            minute.save();
            expect(Meteor.call.calledOnce).to.be.true;
        });

        it('uses the workflow.addMinutes method to save a new minutes document', function () {
            delete minute._id;
            minute.save();
            expect(Meteor.call.calledWithExactly('workflow.addMinutes', minute, undefined, undefined)).to.be.true;
        });

        it('sets the createdAt-property if it is not set', function () {
            delete minute._id;
            delete minute.createdAt;
            minute.save();
            expect(minute).to.have.ownProperty('createdAt');
        });

        it('calls the meteor method minutes.update if a existing minute will be saved', function () {
            minute.save();
            expect(Meteor.call.calledOnce).to.be.true;
        });

        it('sends the minutes object to the meteor method minutes.update', function () {
            minute.save();
            expect(Meteor.call.calledWithExactly('minutes.update', minute)).to.be.true;
        });

    });

    it('#parentMeetingSeries', function () {
        let parentSeries = minute.parentMeetingSeries();
        expect(parentSeries instanceof MeetingSeries, "result should be an instance of MeetingSeries").to.be.true;
        expect(parentSeries._id, "created meeting series object should have the correct series id").to.equal(minute.meetingSeries_id);
    });

    it('#parentMeetingSeriesID', function () {
        expect(minute.parentMeetingSeriesID()).to.equal(minute.meetingSeries_id);
    });

    describe('topic related methods', function () {

        let topic1, topic2, topic3, topic4;

        beforeEach(function () {
            topic1 = {
                _id: "01",
                subject: "firstTopic",
                isNew: true,
                isOpen: true
            };
            topic2 = {
                _id: "02",
                subject: "2ndTopic",
                isNew: true,
                isOpen: false
            };
            topic3 = {
                _id: "03",
                subject: "3rdTopic",
                isNew: false,
                isOpen: true
            };
            topic4 = {
                _id: "04",
                subject: "4thTopic",
                isNew: false,
                isOpen: false
            };
            minute.topics.push(topic1);
            minute.topics.push(topic2);
            minute.topics.push(topic3);
            minute.topics.push(topic4);
        });

        describe('#findTopic', function () {

            it('finds the correct topic identified by its id', function () {
                expect(minute.findTopic(topic1._id)).to.deep.equal(topic1);
            });

            it('returns undefined if topic was not found', function () {
                expect(minute.findTopic('unknownId')).to.be.undefined;
            });

        });

        describe('#removeTopic', function () {

            it('removes the topic from the topics array', function () {
                let oldLength = minute.topics.length;
                minute.removeTopic(topic1._id);
                expect(minute.topics).to.have.length(oldLength-1);
            });

            it('calls the meteor method minutes.update', function () {
                minute.removeTopic(topic1._id);
                expect(Meteor.callPromise.calledOnce).to.be.true;
            });

        });

        describe('#getNewTopics', function () {

            it('returns the correct amount of topics', function () {
                expect(minute.getNewTopics()).to.have.length(2);
            });

            it('returns only new topics', function () {
                let newTopics = minute.getNewTopics();
                newTopics.forEach(topic => {
                    expect(topic.isNew, "isNew-flag should be set").to.be.true;
                });
            });

        });

        describe('#getOldClosedTopics', function () {

            it('returns the correct amount of topics', function () {
                expect(minute.getOldClosedTopics()).to.have.length(1);
            });

            it('returns only old and closed topics', function () {
                let oldClosedTopics = minute.getOldClosedTopics();
                oldClosedTopics.forEach(topic => {
                    expect(
                        topic.isNew && topic.isOpen,
                        "isNew and isOpen flag should both not set"
                    ).to.be.false;
                });
            });

        });

        describe('#getOpenActionItems', function () {

            it('calls the getOpenActionItems method for each topic', function () {
                minute.getOpenActionItems();
                expect(topicGetOpenActionItemsStub.callCount).to.equal(minute.topics.length);
            });

            it('concatenates all results of each getOpenActionItems-call', function () {
                topicGetOpenActionItemsStub.returns([5,7]);
                expect(minute.getOpenActionItems()).to.have.length(minute.topics.length * 2);

            });

        });

    });

    describe('#upsertTopic', function () {

        let topicDoc;

        beforeEach(function () {
            topicDoc = {
                subject: "myTopic"
            }
        });

        it('adds a new topic to the topic array', function () {
            let oldLength = minute.topics.length;
            minute.upsertTopic(topicDoc);
            expect(minute.topics).to.have.length(oldLength+1);
        });

        it('generates a id for a brand new topic', function() {
            minute.upsertTopic(topicDoc);
            expect(minute.topics[0]._id).to.not.be.empty;
        });

        it('adds a new topic which already has a id', function () {
            topicDoc._id = "myId";
            minute.upsertTopic(topicDoc);
            expect(minute.topics, "size of the topic array should be increased by one").to.have.length(1);
            expect(minute.topics[0]._id, "the id should not have changed").to.equal(topicDoc._id);
        });

        it('updates an existing topic correctly', function () {
            topicDoc._id = "myId";
            minute.upsertTopic(topicDoc);
            topicDoc.subject = "changedSubject";
            minute.upsertTopic(topicDoc);
            expect(minute.topics, "update an existing topic should not change the size of the topics array").to.have.length(1);
            expect(minute.topics[0].subject, "the subject should have been updated").to.equal(topicDoc.subject);
        });

        it('calls the meteor method minutes.update', function () {
            minute.upsertTopic(topicDoc);
            expect(Meteor.callPromise.calledOnce).to.be.true;
        });

        it('sends the minutes id and the topic doc to the meteor method minutes.addTopic', function () {
            minute.upsertTopic(topicDoc);
            let callArgs = Meteor.callPromise.getCall(0).args;
            expect(callArgs[0], "first argument should be the name of the meteor method", 'minutes.addTopic');
            let sentDoc = callArgs[1];
            expect(callArgs[1], 'minutes id should be sent to the meteor method').to.equal(minutesDoc._id);
            expect(callArgs[2], 'topic-doc should be sent to the meteor method').to.equal(topicDoc);
        });

    });

    describe('#finalize', function () {

        it('calls the meteor method workflow.finalizeMinute', function() {
            minute.finalize();

            expect(Meteor.callPromise.calledOnce).to.be.true;
        });

        it('sends the id to the meteor method workflow.finalizeMinute', function () {
            minute.finalize();

            expect(Meteor.callPromise.calledWith('workflow.finalizeMinute', minutesDoc._id)).to.be.true;
        });

    });

    describe('#unfinalize', function () {

        it('calls the meteor method workflow.unfinalizeMinute', function() {
            minute.unfinalize();

            expect(Meteor.callPromise.calledOnce).to.be.true;
        });

        it('sends the id to the meteor method workflow.unfinalizeMinute', function () {
            minute.unfinalize();

            expect(Meteor.callPromise.calledWithExactly('workflow.unfinalizeMinute', minutesDoc._id)).to.be.true;
        });

    });

    it('#isCurrentUserModerator', function () {
        minute.isCurrentUserModerator();

        expect(isCurrentUserModeratorStub.calledOnce).to.be.true;
    });

});<commit_msg>Fix unit test<commit_after>/**
 * Created by felix on 19.05.16.
 */
import { expect } from 'chai';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
import _ from 'underscore';

require('sinon-as-promised');
require('../../../lib/helpers');

let MinutesCollection = {
    find: sinon.stub(),
    findOne: sinon.stub()
};

class MeteorError {}

let Meteor = {
    call: sinon.stub(),
    callPromise: sinon.stub().resolves(true),
    Error: MeteorError
};

let PromisedMethods = {};

let isCurrentUserModeratorStub = sinon.stub();
let updateLastMinutesDateStub = sinon.stub();
let updateLastMinutesDateAsyncStub = sinon.stub().resolves(true);
let MeetingSeries = function(seriesId) {
    this._id = seriesId;
    this.isCurrentUserModerator = isCurrentUserModeratorStub;
    this.updateLastMinutesDate = updateLastMinutesDateStub;
    this.updateLastMinutesDateAsync = updateLastMinutesDateAsyncStub;
};

let topicGetOpenActionItemsStub = sinon.stub().returns([]);
let Topic = function () {
    this.getOpenActionItems = topicGetOpenActionItemsStub;
};
Topic.hasOpenActionItem = () => { return false; };

let ActionItem = function (topic, doc) {
    this._parentTopic = topic;
    this._infoItemDoc = doc;
};

const {
    Minutes
    } = proxyquire('../../../imports/minutes', {
    'meteor/meteor': { Meteor, '@noCallThru': true},
    './collections/minutes_private': { MinutesCollection, '@noCallThru': true},
    './collections/workflow_private': { null, '@noCallThru': true},
    './helpers/promisedMethods': { PromisedMethods, '@noCallThru': true},
    './meetingseries': { MeetingSeries, '@noCallThru': true},
    './topic': { Topic, '@noCallThru': true},
    './actionitem': { ActionItem, '@noCallThru': true},
    'meteor/underscore': { _, '@noCallThru': true}
});

describe('Minutes', function () {

    let minutesDoc, minute;

    beforeEach(function () {
        minutesDoc = {
            meetingSeries_id: 'AaBbCc01',
            _id: 'AaBbCc02',
            date: '2016-05-06',
            createdAt: new Date(),
            topics: [],
            isFinalized: false,
            participants: '',
            agenda: ''
        };

        minute = new Minutes(minutesDoc);
    });

    afterEach(function () {
        MinutesCollection.find.reset();
        MinutesCollection.findOne.reset();
        Meteor.call.reset();
        Meteor.callPromise.reset();
        isCurrentUserModeratorStub.reset();
        updateLastMinutesDateStub.reset();
        topicGetOpenActionItemsStub.reset();
    });

    describe('#constructor', function () {

        it('sets the properties correctly', function () {
            expect(JSON.stringify(minute)).to.equal(JSON.stringify(minutesDoc));
        });

        it('fetches the minute from the database if the id was given', function() {

            new Minutes(minutesDoc._id);
            expect(MinutesCollection.findOne.calledOnce, "findOne should be called once").to.be.true;
            expect(MinutesCollection.findOne.calledWith(minutesDoc._id), "findOne should be called with the id").to.be.true;
        });

        it('throws exception if constructor will be called without any arguments', function () {
            let exceptionThrown;
            try {
                new Minutes();
                exceptionThrown = false;
            } catch (e) {
                exceptionThrown = (e instanceof MeteorError);
            }

            expect(exceptionThrown).to.be.true;
        });

    });

    describe('find', function () {

        it('#find', function () {
            Minutes.find("myArg");
            expect(MinutesCollection.find.calledOnce, "find-Method should be called once").to.be.true;
            expect(MinutesCollection.find.calledWithExactly("myArg"), "arguments should be passed").to.be.true;
        });

        it('#findOne', function () {
            Minutes.findOne("myArg");
            expect(MinutesCollection.findOne.calledOnce, "findOne-Method should be called once").to.be.true;
            expect(MinutesCollection.findOne.calledWithExactly("myArg"), "arguments should be passed").to.be.true;
        });

        describe('#findAllIn', function () {

            let minIdArray;
            let limit;

            beforeEach(function () {
                minIdArray = ['1', '2'];
                limit = 3;
            });

            it('calls the find-Method of the Collection', function () {
                Minutes.findAllIn(minIdArray, limit);
                expect(MinutesCollection.find.calledOnce, "find-Method should be called once").to.be.true;
            });

            it('sets the id selector correctly', function () {
                Minutes.findAllIn(minIdArray, limit);
                let selector = MinutesCollection.find.getCall(0).args[0];
                expect(selector, "Selector has the property _id").to.have.ownProperty('_id');
                expect(selector._id, '_id-selector has propery $in').to.have.ownProperty('$in');
                expect(selector._id.$in, 'idArray should be passed').to.deep.equal(minIdArray);
            });

            it('sets the option correctly (sort, no limit)', function () {
                let expectedOption = { sort: {date: -1} };
                Minutes.findAllIn(minIdArray);
                let options = MinutesCollection.find.getCall(0).args[1];
                expect(options).to.deep.equal(expectedOption);
            });

            it('sets the option correctly (sort and limit)', function () {
                let expectedOption = { sort: {date: -1}, limit: limit };
                Minutes.findAllIn(minIdArray, limit);
                let options = MinutesCollection.find.getCall(0).args[1];
                expect(options).to.deep.equal(expectedOption);
            });

        });

    });

    describe('#remove', function () {

        it('calls the meteor method minutes.remove', function () {
            Minutes.remove(minute._id);
            expect(Meteor.callPromise.calledOnce).to.be.true;
        });

        it('sends the minutes id to the meteor method minutes.remove', function () {
            Minutes.remove(minute._id);
            expect(Meteor.callPromise.calledWithExactly('workflow.removeMinute', minute._id)).to.be.true;
        });

    });

    describe('#syncVisibility', function () {

        let visibleForArray, parentSeriesId;

        beforeEach(function () {
            visibleForArray = [
                '1',
                '2'
            ];
            parentSeriesId = minute.meetingSeries_id;
        });

        it('calls the meteor method minutes.syncVisibility', function () {
            Minutes.syncVisibility(parentSeriesId, visibleForArray);
            expect(Meteor.callPromise.calledOnce).to.be.true;
        });

        it('sends the parentSeriesId and the visibleFor-array to the meteor method minutes.syncVisibility', function () {
            Minutes.syncVisibility(parentSeriesId, visibleForArray);
            expect(Meteor.callPromise.calledWithExactly('minutes.syncVisibility', parentSeriesId, visibleForArray)).to.be.true;
        });

    });

    describe('#update', function () {

        let updateDocPart;

        beforeEach(function () {
            updateDocPart = {
                date: '2016-05-07'
            }
        });

        it('calls the meteor method minutes.update', function () {
            minute.update(updateDocPart);
            expect(Meteor.callPromise.calledOnce).to.be.true;
        });

        it('sends the doc part and the minutes id to the meteor method minutes.update', function () {
            minute.update(updateDocPart);
            let sentObj = JSON.parse(JSON.stringify(updateDocPart));
            sentObj._id = minute._id;
            expect(Meteor.callPromise.calledWithExactly('minutes.update', sentObj, undefined)).to.be.true;
        });

        it('updates the changed property of the minute object', async function () {
            await minute.update(updateDocPart);
            expect(minute.date).to.equal(updateDocPart.date);
        });

    });

    describe('#save', function () {

        it('calls the meteor method minutes.insert if a new minute will be saved', function () {
            delete minute._id;
            minute.save();
            expect(Meteor.call.calledOnce).to.be.true;
        });

        it('uses the workflow.addMinutes method to save a new minutes document', function () {
            delete minute._id;
            minute.save();
            expect(Meteor.call.calledWithExactly('workflow.addMinutes', minute, undefined, undefined)).to.be.true;
        });

        it('sets the createdAt-property if it is not set', function () {
            delete minute._id;
            delete minute.createdAt;
            minute.save();
            expect(minute).to.have.ownProperty('createdAt');
        });

        it('calls the meteor method minutes.update if a existing minute will be saved', function () {
            minute.save();
            expect(Meteor.call.calledOnce).to.be.true;
        });

        it('sends the minutes object to the meteor method minutes.update', function () {
            minute.save();
            expect(Meteor.call.calledWithExactly('minutes.update', minute)).to.be.true;
        });

    });

    it('#parentMeetingSeries', function () {
        let parentSeries = minute.parentMeetingSeries();
        expect(parentSeries instanceof MeetingSeries, "result should be an instance of MeetingSeries").to.be.true;
        expect(parentSeries._id, "created meeting series object should have the correct series id").to.equal(minute.meetingSeries_id);
    });

    it('#parentMeetingSeriesID', function () {
        expect(minute.parentMeetingSeriesID()).to.equal(minute.meetingSeries_id);
    });

    describe('topic related methods', function () {

        let topic1, topic2, topic3, topic4;

        beforeEach(function () {
            topic1 = {
                _id: "01",
                subject: "firstTopic",
                isNew: true,
                isOpen: true
            };
            topic2 = {
                _id: "02",
                subject: "2ndTopic",
                isNew: true,
                isOpen: false
            };
            topic3 = {
                _id: "03",
                subject: "3rdTopic",
                isNew: false,
                isOpen: true
            };
            topic4 = {
                _id: "04",
                subject: "4thTopic",
                isNew: false,
                isOpen: false
            };
            minute.topics.push(topic1);
            minute.topics.push(topic2);
            minute.topics.push(topic3);
            minute.topics.push(topic4);
        });

        describe('#findTopic', function () {

            it('finds the correct topic identified by its id', function () {
                expect(minute.findTopic(topic1._id)).to.deep.equal(topic1);
            });

            it('returns undefined if topic was not found', function () {
                expect(minute.findTopic('unknownId')).to.be.undefined;
            });

        });

        describe('#removeTopic', function () {

            it('removes the topic from the topics array', function () {
                let oldLength = minute.topics.length;
                minute.removeTopic(topic1._id);
                expect(minute.topics).to.have.length(oldLength-1);
            });

            it('calls the meteor method minutes.update', function () {
                minute.removeTopic(topic1._id);
                expect(Meteor.callPromise.calledOnce).to.be.true;
            });

        });

        describe('#getNewTopics', function () {

            it('returns the correct amount of topics', function () {
                expect(minute.getNewTopics()).to.have.length(2);
            });

            it('returns only new topics', function () {
                let newTopics = minute.getNewTopics();
                newTopics.forEach(topic => {
                    expect(topic.isNew, "isNew-flag should be set").to.be.true;
                });
            });

        });

        describe('#getOldClosedTopics', function () {

            it('returns the correct amount of topics', function () {
                expect(minute.getOldClosedTopics()).to.have.length(1);
            });

            it('returns only old and closed topics', function () {
                let oldClosedTopics = minute.getOldClosedTopics();
                oldClosedTopics.forEach(topic => {
                    expect(
                        topic.isNew && topic.isOpen,
                        "isNew and isOpen flag should both not set"
                    ).to.be.false;
                });
            });

        });

        describe('#getOpenActionItems', function () {

            it('calls the getOpenActionItems method for each topic', function () {
                minute.getOpenActionItems();
                expect(topicGetOpenActionItemsStub.callCount).to.equal(minute.topics.length);
            });

            it('concatenates all results of each getOpenActionItems-call', function () {
                topicGetOpenActionItemsStub.returns([5,7]);
                expect(minute.getOpenActionItems()).to.have.length(minute.topics.length * 2);

            });

        });

    });

    describe('#upsertTopic', function () {

        let topicDoc;

        beforeEach(function () {
            topicDoc = {
                subject: "myTopic"
            }
        });

        it('adds a new topic to the topic array', function () {
            let oldLength = minute.topics.length;
            minute.upsertTopic(topicDoc);
            expect(minute.topics).to.have.length(oldLength+1);
        });

        it('generates a id for a brand new topic', function() {
            minute.upsertTopic(topicDoc);
            expect(minute.topics[0]._id).to.not.be.empty;
        });

        it('adds a new topic which already has a id', function () {
            topicDoc._id = "myId";
            minute.upsertTopic(topicDoc);
            expect(minute.topics, "size of the topic array should be increased by one").to.have.length(1);
            expect(minute.topics[0]._id, "the id should not have changed").to.equal(topicDoc._id);
        });

        it('updates an existing topic correctly', function () {
            topicDoc._id = "myId";
            minute.upsertTopic(topicDoc);
            topicDoc.subject = "changedSubject";
            minute.upsertTopic(topicDoc);
            expect(minute.topics, "update an existing topic should not change the size of the topics array").to.have.length(1);
            expect(minute.topics[0].subject, "the subject should have been updated").to.equal(topicDoc.subject);
        });

        it('calls the meteor method minutes.update', function () {
            minute.upsertTopic(topicDoc);
            expect(Meteor.callPromise.calledOnce).to.be.true;
        });

        it('sends the minutes id and the topic doc to the meteor method minutes.addTopic', function () {
            minute.upsertTopic(topicDoc);
            let callArgs = Meteor.callPromise.getCall(0).args;
            expect(callArgs[0], "first argument should be the name of the meteor method", 'minutes.addTopic');
            let sentDoc = callArgs[1];
            expect(callArgs[1], 'minutes id should be sent to the meteor method').to.equal(minutesDoc._id);
            expect(callArgs[2], 'topic-doc should be sent to the meteor method').to.equal(topicDoc);
        });

    });

    describe('#finalize', function () {

        it('calls the meteor method workflow.finalizeMinute', function() {
            minute.finalize();

            expect(Meteor.callPromise.calledOnce).to.be.true;
        });

        it('sends the id to the meteor method workflow.finalizeMinute', function () {
            minute.finalize();

            expect(Meteor.callPromise.calledWith('workflow.finalizeMinute', minutesDoc._id)).to.be.true;
        });

    });

    describe('#unfinalize', function () {

        it('calls the meteor method workflow.unfinalizeMinute', function() {
            minute.unfinalize();

            expect(Meteor.callPromise.calledOnce).to.be.true;
        });

        it('sends the id to the meteor method workflow.unfinalizeMinute', function () {
            minute.unfinalize();

            expect(Meteor.callPromise.calledWithExactly('workflow.unfinalizeMinute', minutesDoc._id)).to.be.true;
        });

    });

    it('#isCurrentUserModerator', function () {
        minute.isCurrentUserModerator();

        expect(isCurrentUserModeratorStub.calledOnce).to.be.true;
    });

});<|endoftext|>


<commit_before>/*
    - FARSIGHT -
    JavaScript viewport framework - enables you cool scrolling animations and much more...

    @version: v1.0.0
    @author: Nikola Stamatovic Stamat <nikola@otilito.com>
*/

//TODO: terminate farsight function, unbind all the events
//TODO: change the custom function system to merge CSS transform into one - make a separate custom CSS function

//TODO: Percentage of dissapearance when passing through the left or top border of the viewport if enabled
//TODO: Think about cases where both vertical and horizontal scrolling is enabled

var farsight = farsight || {};

farsight.Viewport = function Viewport(o) {
    var self = this;

    this.width = null;
    this.height = null;
    this.x = null;
    this.y = null;
    this.bottom = null;
    this.right = null;
    this.callbacks = {};
    this.xp = 0; //percentage x
    this.yp = 0; //percentage y
    this.xd = 0; //delta x
    this.yd = 0; //delta y
    this.xo = 0; //old x
    this.yo = 0; //old y

    this.vonly = false; //vertical only - saves calculation time
    this.honly = false; //horizontal only - saves calculation time

    this.viewport = window;
    this.default_viewport = true;
    this.pane = {};
    this.pane.elem = $('body');
    this.pane.width = 0;
    this.pane.height = 0;

    var autoincrement = 0; //autoincrement for callback naming

    this.bind = function(name, callback) {
        if (!name) {
            name = 'fn' + autoincrement;
        }
        this.callbacks[name] = callback;
        autoincrement++;
    };

    this.unbind = function(name) {
        delete this.callbacks[name];
    };

    this.calculate = function() {

        if (this.default_viewport) {
            this.width = this.viewport.innerWidth;
            this.height = this.viewport.innerHeight;

            this.xo = this.x === null ? this.viewport.scrollX : this.x;
            this.yo = this.y === null ? this.viewport.scrollY : this.y;

            this.y = this.viewport.scrollY;
            this.x = this.viewport.scrollX;
        } else {
            this.width = this.viewport.width();
            this.height = this.viewport.height();

            this.xo = this.x === null ? this.viewport.scrollLeft() : this.x;
            this.yo = this.y === null ? this.viewport.scrollTop() : this.y;

            this.y = this.viewport.scrollTop();
            this.x = this.viewport.scrollLeft();
        }

        if (!this.honly) {
            this.yd = this.y - this.yo;
            this.bottom = this.y + this.height;
            this.pane.height = this.pane.elem.height();

            var suby = this.pane.height - this.height;
            if (suby <= 0) {
                this.yp = 0;
            } else {
                this.yp = this.y * 100 / suby;
            }
        }

        if (!this.vonly) {
            this.xd = this.x - this.xo;
            this.right = this.x + this.width;
            this.pane.width = this.pane.elem.width();

            var subx = this.pane.width - this.width;

            if (subx <= 0) {
                this.xp = 0;
            } else {
                this.xp = this.x * 100 / subx;
            }
        }
    };

    var scrollTimer = null;

    this.onscroll = function(force) {
        this.calculate();

        for (var key in this.callbacks) {
            this.callbacks[key](this, force);
        }
    };

    farsight._utils.extend(this, o, false, false);

    var __init__ = function() {
        //if the viewport is not window
        if (!self.viewport.hasOwnProperty('innerWidth')) {
            self.default_viewport = false;
        }

        window.onresize = function() {
            self.calculate();
        };

        var scrollfn = function() {
            self.onscroll();

            //scroll end fix
            if (scrollTimer) {
              clearTimeout(scrollTimer);
            }
            scrollTimer = setTimeout(function(){
                self.onscroll(true);
            }, 100);
        };

        if (self.default_viewport) {
            self.onscroll();
            self.viewport.onscroll = scrollfn;
        } else {
            self.calculate();
            self.viewport.scroll(scrollfn);
        }
    }
    __init__();
}

farsight.ActiveElement = function ActiveElement(elem, viewport, o, auto_init) {
    this.width = null;
    this.height = null;
    this.x = null;
    this.y = null;
    this.xp = 0; //percentage visible
    this.yp = 0; //percentage visible
    this.element = $(elem);
    this.bottom = null;
    this.right = null;
    this.viewport = viewport;
    this.callback = null;
    this.multiple_callbacks = false;
    this.once = false;
    this.vdirection = 1;
    this.hdirection = 1;

    this.oldy = 0; //start position y
    this.oldx = 0; //start position x

    this.animation = null;
    this.duration = null;
    this.delay = null;
    this.count = false;

    this.disappear = false; //https://youtu.be/nYSDC3cHoZs

    this.target = null; //can be used in scroll callbacks, represents a target DOM element cache
    this.data = {};

    this.auto_init = auto_init === undefined;

    var autoincrement = 0;

    this.bind = function(name, callback) {
        if (!this.multiple_callbacks && this.callback !== null) {
            this.callback = {'0': this.callback};
        }
        if (!name) {
            name = 'fn' + autoincrement;
        }
        this.multiple_callbacks =  true;
        this.callback = this.callback || {};
        this.callback[name] = callback;
        autoincrement++;
    };

    this.unbind = function(name) {
        delete this.callback[name];
    };

    this.update = function(force) {
        if (!this.callback) {
            return;
        }

        this.width = this.element.outerWidth();
        this.height = this.element.outerHeight();
        var off = {};
        if (this.viewport.default_viewport) {
            off = this.element.offset();
            this.y = off.top;
            this.x = off.left;
        } else {
            off = this.element.position();
            this.y = this.viewport.y + off.top;
            this.x = this.viewport.x + off.left;
        }

        if (!this.viewport.honly) {
            this.bottom = this.y + this.height;
            if (this.viewport.bottom < this.y) {
                this.yp = 0;
                this.vdirection = 1;
            } else if (this.disappear && this.viewport.y < this.bottom && this.viewport.y > this.y) {
                this.yp = 1 - (this.viewport.y - this.y) / this.height;
                this.vdirection = -1;
            } else if (this.disappear && this.viewport.y > this.bottom) {
                this.yp = 0;
                this.vdirection = -1;
            } else if (this.viewport.bottom >= this.bottom) {
                this.yp = 1;
                this.vdirection = 1;
            } else {
                this.yp = (this.viewport.bottom-this.y) / this.height;
                this.vdirection = 1;
            }
        }

        if (!this.viewport.vonly) {
            this.right = this.x + this.width;

            if (this.viewport.right < this.x) {
                this.xp = 0;
                this.hdirection = 1;
            } else if (this.disappear && this.viewport.x < this.right && this.viewport.x > this.x) {
                this.xp = 1 - (this.viewport.x - this.x) / this.width;
                this.hdirection = -1;
            } else if (this.disappear && this.viewport.x > this.right) {
                this.xp = 0;
                this.hdirection = -1;
            } else if (this.viewport.right >= this.right) {
                this.xp = 1;
                this.hdirection = 1;
            } else {
                this.xp = (this.viewport.right-this.x) / this.width;
                this.hdirection = 1;
            }
        }

        if ((force && !this.once) || (this.xp > 0 && this.xp <= 1 && this.yp > 0 && this.yp <= 1)) {
            if (this.multiple_callbacks) {
                for (var k in this.callback) {
                    this.callback[k](this);
                }
            } else {
                this.callback(this);
            }

            if (this.once) {
                this.callback = null;
            }
        }
    }

    farsight._utils.extend(this, o, false, false);

    var self = this;
    var __init__ = function() {

        if (o.hasOwnProperty('callback')) {
            self.multiple_callbacks = typeof self.callback !== 'function';
        }

        if (o.hasOwnProperty('animation')) {
            self.once = true;
            self.disappear = true;
            self.callback = function() {
                self.element.addClass('animated '+ self.animation);
            };
            self.multiple_callbacks = false;
        }

        if (self.duration) {
            self.element.css('animation-duration', self.duration);
        }

        if (self.delay) {
            self.element.css('animation-delay', self.delay);
        }

        if (self.count) {
            self.element.css('animation-iteration-count', self.count);
        }
        if (self.auto_init) {
            self.update();
        }

        var name = typeof elem === 'string' ? elem : undefined;
        self.viewport.bind(name, function(viewport, force) {
            self.update(force);
        });
    }
    __init__();
}

//utils from stamat/ivartech
farsight._utils = {};
farsight._utils.getPropertyByNamespace = function(str, root, del) {
    del = del || '.';
	var parts = typeof str === 'string' ? str.split(del) : str;
	var current = root || window;
	for (var i = 0; i < parts.length; i++) {
		if (current.hasOwnProperty(parts[i])) {
			current = current[parts[i]];
		} else {
			return;
		}
	}
	return current;
};

farsight._utils.namespace = function(str, root, del) {
    del = del || '.';
	var parts = typeof str === 'string' ? str.split(del) : str;
	var current = root || window;
	for(var i = 0; i < parts.length; i++) {
		if (!current.hasOwnProperty(parts[i])) {
			current[parts[i]] = {};
        }
		current = current[parts[i]];
	};
	return current;
};

//deep extend
//if_not_exists can be undefined, true, false, if it's false it will only extend the object if it has the property of the extender, if it's true it extends only if the property doesn't exist or it's null
farsight._utils.extend = function(o1, o2, deep, if_not_exists) {
	for (var i in o2) {
        var state = true;
        if (if_not_exists !== undefined ) {
            if (if_not_exists === false) {
                state = o1.hasOwnProperty(i);
            } else {
                state = !(o1[i] !== undefined && o1[i] !== null && if_not_exists);
            }
        }

        if (deep && typeof o1[i] === 'object' && typeof o2[i] === 'object') {
            farsight._utils.extend(o1[i], o2[i], if_not_exists);
        }

		if (state) {
			o1[i] = o2[i];
		}
	}
	return o1;
};

// attrs should be an object with names and desired format
farsight._utils.parseAttrs = function(elem, attrs) {
    var res = {};
    var $elem = $(elem);

    var parsers = {};
    parsers['boolean'] = function(v) {
        return v === '' || v === 'true';
    };

    parsers['string'] = function(v) {
        return v;
    };

    parsers['function'] = function(v) {
        var fns = v.split(/,\s?/g);
        if (fns.length === 1) {
            return farsight._utils.getPropertyByNamespace(fns[0]);
        }
        var r = {};
        for (var i = 0; i < fns.length; i++) {
            r[fns[i]] = farsight._utils.getPropertyByNamespace(fns[i]);
        }

        return r;
    };

    parsers['number'] = function(v) {
        return parseFloat(v);
    };

    parsers['selector'] = function(v) {
        return $(v);
    };

    parsers['json'] = function(v) {
        return JSON.parse(v);
    };

    for (var k in attrs) {
        var attr = $elem.attr(k);

        if (attr || attr === '') {
            var nk = k.replace(/^fs\-/ig, '');
            nk = nk.replace(/\-/g, '_');
            res[nk] = parsers[attrs[k]](attr);
        }
    }

    return res;
};

function Farsight(o) {
    this.elements = [];
    this.selector = '.fs';
    this.viewport = window;
    this.pane = $('body');
    this.vonly = false;
    this.honly = false;
    this.viewport_instance = null;

    this.getViewport = function() {
        return this.viewport_instance;
    };

    var attrs = {
        'fs-animation': 'string',
        'fs-callback': 'function',
        'fs-duration': 'string',
        'fs-delay': 'string',
        'fs-count': 'string',
        'fs-target': 'selector',
        'fs-disappear': 'boolean'
    };

    farsight._utils.extend(this, o, false, false);

    var self = this;
    var __init__ = function() {

        self.viewport_instance = new farsight.Viewport({vonly: self.vonly, honly: self.honly, viewport: self.viewport, pane: {elem: self.pane}});

        var $elems = $(self.selector);
        for (var i = 0; i < $elems.length; i++) {
            var $elem = $($elems[i]);
            var o = farsight._utils.parseAttrs($elem, attrs);

            var ae = new farsight.ActiveElement($elem, self.viewport_instance, o, false);
            self.elements.push(ae);

            function executePreFn(fpath, ae) {
                var pts = fpath.split('.');
                pts.splice(pts.length-1, 0, '_pre');

                var pre_fn = farsight._utils.getPropertyByNamespace(pts);
                if (pre_fn) {
                    pre_fn(ae);
                }
            }

            if (o && o.hasOwnProperty('callback')) {
                if (typeof o['callback'] === 'function') {
                    executePreFn(ae.element.attr('fs-callback'), ae);
                } else {
                    for (var k  in o['callback']) {
                        executePreFn(k, ae);
                    }
                }
            }
            ae.update();
        }
    }
    __init__();
}
<commit_msg>... and version number<commit_after>/*
    - FARSIGHT -
    JavaScript viewport framework - enables you cool scrolling animations and much more...

    @version: v1.0.1
    @author: Nikola Stamatovic Stamat <nikola@otilito.com>
*/

//TODO: terminate farsight function, unbind all the events
//TODO: change the custom function system to merge CSS transform into one - make a separate custom CSS function

//TODO: Percentage of dissapearance when passing through the left or top border of the viewport if enabled
//TODO: Think about cases where both vertical and horizontal scrolling is enabled

var farsight = farsight || {};

farsight.Viewport = function Viewport(o) {
    var self = this;

    this.width = null;
    this.height = null;
    this.x = null;
    this.y = null;
    this.bottom = null;
    this.right = null;
    this.callbacks = {};
    this.xp = 0; //percentage x
    this.yp = 0; //percentage y
    this.xd = 0; //delta x
    this.yd = 0; //delta y
    this.xo = 0; //old x
    this.yo = 0; //old y

    this.vonly = false; //vertical only - saves calculation time
    this.honly = false; //horizontal only - saves calculation time

    this.viewport = window;
    this.default_viewport = true;
    this.pane = {};
    this.pane.elem = $('body');
    this.pane.width = 0;
    this.pane.height = 0;

    var autoincrement = 0; //autoincrement for callback naming

    this.bind = function(name, callback) {
        if (!name) {
            name = 'fn' + autoincrement;
        }
        this.callbacks[name] = callback;
        autoincrement++;
    };

    this.unbind = function(name) {
        delete this.callbacks[name];
    };

    this.calculate = function() {

        if (this.default_viewport) {
            this.width = this.viewport.innerWidth;
            this.height = this.viewport.innerHeight;

            this.xo = this.x === null ? this.viewport.scrollX : this.x;
            this.yo = this.y === null ? this.viewport.scrollY : this.y;

            this.y = this.viewport.scrollY;
            this.x = this.viewport.scrollX;
        } else {
            this.width = this.viewport.width();
            this.height = this.viewport.height();

            this.xo = this.x === null ? this.viewport.scrollLeft() : this.x;
            this.yo = this.y === null ? this.viewport.scrollTop() : this.y;

            this.y = this.viewport.scrollTop();
            this.x = this.viewport.scrollLeft();
        }

        if (!this.honly) {
            this.yd = this.y - this.yo;
            this.bottom = this.y + this.height;
            this.pane.height = this.pane.elem.height();

            var suby = this.pane.height - this.height;
            if (suby <= 0) {
                this.yp = 0;
            } else {
                this.yp = this.y * 100 / suby;
            }
        }

        if (!this.vonly) {
            this.xd = this.x - this.xo;
            this.right = this.x + this.width;
            this.pane.width = this.pane.elem.width();

            var subx = this.pane.width - this.width;

            if (subx <= 0) {
                this.xp = 0;
            } else {
                this.xp = this.x * 100 / subx;
            }
        }
    };

    var scrollTimer = null;

    this.onscroll = function(force) {
        this.calculate();

        for (var key in this.callbacks) {
            this.callbacks[key](this, force);
        }
    };

    farsight._utils.extend(this, o, false, false);

    var __init__ = function() {
        //if the viewport is not window
        if (!self.viewport.hasOwnProperty('innerWidth')) {
            self.default_viewport = false;
        }

        window.onresize = function() {
            self.calculate();
        };

        var scrollfn = function() {
            self.onscroll();

            //scroll end fix
            if (scrollTimer) {
              clearTimeout(scrollTimer);
            }
            scrollTimer = setTimeout(function(){
                self.onscroll(true);
            }, 100);
        };

        if (self.default_viewport) {
            self.onscroll();
            self.viewport.onscroll = scrollfn;
        } else {
            self.calculate();
            self.viewport.scroll(scrollfn);
        }
    }
    __init__();
}

farsight.ActiveElement = function ActiveElement(elem, viewport, o, auto_init) {
    this.width = null;
    this.height = null;
    this.x = null;
    this.y = null;
    this.xp = 0; //percentage visible
    this.yp = 0; //percentage visible
    this.element = $(elem);
    this.bottom = null;
    this.right = null;
    this.viewport = viewport;
    this.callback = null;
    this.multiple_callbacks = false;
    this.once = false;
    this.vdirection = 1;
    this.hdirection = 1;

    this.oldy = 0; //start position y
    this.oldx = 0; //start position x

    this.animation = null;
    this.duration = null;
    this.delay = null;
    this.count = false;

    this.disappear = false; //https://youtu.be/nYSDC3cHoZs

    this.target = null; //can be used in scroll callbacks, represents a target DOM element cache
    this.data = {};

    this.auto_init = auto_init === undefined;

    var autoincrement = 0;

    this.bind = function(name, callback) {
        if (!this.multiple_callbacks && this.callback !== null) {
            this.callback = {'0': this.callback};
        }
        if (!name) {
            name = 'fn' + autoincrement;
        }
        this.multiple_callbacks =  true;
        this.callback = this.callback || {};
        this.callback[name] = callback;
        autoincrement++;
    };

    this.unbind = function(name) {
        delete this.callback[name];
    };

    this.update = function(force) {
        if (!this.callback) {
            return;
        }

        this.width = this.element.outerWidth();
        this.height = this.element.outerHeight();
        var off = {};
        if (this.viewport.default_viewport) {
            off = this.element.offset();
            this.y = off.top;
            this.x = off.left;
        } else {
            off = this.element.position();
            this.y = this.viewport.y + off.top;
            this.x = this.viewport.x + off.left;
        }

        if (!this.viewport.honly) {
            this.bottom = this.y + this.height;
            if (this.viewport.bottom < this.y) {
                this.yp = 0;
                this.vdirection = 1;
            } else if (this.disappear && this.viewport.y < this.bottom && this.viewport.y > this.y) {
                this.yp = 1 - (this.viewport.y - this.y) / this.height;
                this.vdirection = -1;
            } else if (this.disappear && this.viewport.y > this.bottom) {
                this.yp = 0;
                this.vdirection = -1;
            } else if (this.viewport.bottom >= this.bottom) {
                this.yp = 1;
                this.vdirection = 1;
            } else {
                this.yp = (this.viewport.bottom-this.y) / this.height;
                this.vdirection = 1;
            }
        }

        if (!this.viewport.vonly) {
            this.right = this.x + this.width;

            if (this.viewport.right < this.x) {
                this.xp = 0;
                this.hdirection = 1;
            } else if (this.disappear && this.viewport.x < this.right && this.viewport.x > this.x) {
                this.xp = 1 - (this.viewport.x - this.x) / this.width;
                this.hdirection = -1;
            } else if (this.disappear && this.viewport.x > this.right) {
                this.xp = 0;
                this.hdirection = -1;
            } else if (this.viewport.right >= this.right) {
                this.xp = 1;
                this.hdirection = 1;
            } else {
                this.xp = (this.viewport.right-this.x) / this.width;
                this.hdirection = 1;
            }
        }

        if ((force && !this.once) || (this.xp > 0 && this.xp <= 1 && this.yp > 0 && this.yp <= 1)) {
            if (this.multiple_callbacks) {
                for (var k in this.callback) {
                    this.callback[k](this);
                }
            } else {
                this.callback(this);
            }

            if (this.once) {
                this.callback = null;
            }
        }
    }

    farsight._utils.extend(this, o, false, false);

    var self = this;
    var __init__ = function() {

        if (o.hasOwnProperty('callback')) {
            self.multiple_callbacks = typeof self.callback !== 'function';
        }

        if (o.hasOwnProperty('animation')) {
            self.once = true;
            self.disappear = true;
            self.callback = function() {
                self.element.addClass('animated '+ self.animation);
            };
            self.multiple_callbacks = false;
        }

        if (self.duration) {
            self.element.css('animation-duration', self.duration);
        }

        if (self.delay) {
            self.element.css('animation-delay', self.delay);
        }

        if (self.count) {
            self.element.css('animation-iteration-count', self.count);
        }
        if (self.auto_init) {
            self.update();
        }

        var name = typeof elem === 'string' ? elem : undefined;
        self.viewport.bind(name, function(viewport, force) {
            self.update(force);
        });
    }
    __init__();
}

//utils from stamat/ivartech
farsight._utils = {};
farsight._utils.getPropertyByNamespace = function(str, root, del) {
    del = del || '.';
	var parts = typeof str === 'string' ? str.split(del) : str;
	var current = root || window;
	for (var i = 0; i < parts.length; i++) {
		if (current.hasOwnProperty(parts[i])) {
			current = current[parts[i]];
		} else {
			return;
		}
	}
	return current;
};

farsight._utils.namespace = function(str, root, del) {
    del = del || '.';
	var parts = typeof str === 'string' ? str.split(del) : str;
	var current = root || window;
	for(var i = 0; i < parts.length; i++) {
		if (!current.hasOwnProperty(parts[i])) {
			current[parts[i]] = {};
        }
		current = current[parts[i]];
	};
	return current;
};

//deep extend
//if_not_exists can be undefined, true, false, if it's false it will only extend the object if it has the property of the extender, if it's true it extends only if the property doesn't exist or it's null
farsight._utils.extend = function(o1, o2, deep, if_not_exists) {
	for (var i in o2) {
        var state = true;
        if (if_not_exists !== undefined ) {
            if (if_not_exists === false) {
                state = o1.hasOwnProperty(i);
            } else {
                state = !(o1[i] !== undefined && o1[i] !== null && if_not_exists);
            }
        }

        if (deep && typeof o1[i] === 'object' && typeof o2[i] === 'object') {
            farsight._utils.extend(o1[i], o2[i], if_not_exists);
        }

		if (state) {
			o1[i] = o2[i];
		}
	}
	return o1;
};

// attrs should be an object with names and desired format
farsight._utils.parseAttrs = function(elem, attrs) {
    var res = {};
    var $elem = $(elem);

    var parsers = {};
    parsers['boolean'] = function(v) {
        return v === '' || v === 'true';
    };

    parsers['string'] = function(v) {
        return v;
    };

    parsers['function'] = function(v) {
        var fns = v.split(/,\s?/g);
        if (fns.length === 1) {
            return farsight._utils.getPropertyByNamespace(fns[0]);
        }
        var r = {};
        for (var i = 0; i < fns.length; i++) {
            r[fns[i]] = farsight._utils.getPropertyByNamespace(fns[i]);
        }

        return r;
    };

    parsers['number'] = function(v) {
        return parseFloat(v);
    };

    parsers['selector'] = function(v) {
        return $(v);
    };

    parsers['json'] = function(v) {
        return JSON.parse(v);
    };

    for (var k in attrs) {
        var attr = $elem.attr(k);

        if (attr || attr === '') {
            var nk = k.replace(/^fs\-/ig, '');
            nk = nk.replace(/\-/g, '_');
            res[nk] = parsers[attrs[k]](attr);
        }
    }

    return res;
};

function Farsight(o) {
    this.elements = [];
    this.selector = '.fs';
    this.viewport = window;
    this.pane = $('body');
    this.vonly = false;
    this.honly = false;
    this.viewport_instance = null;

    this.getViewport = function() {
        return this.viewport_instance;
    };

    var attrs = {
        'fs-animation': 'string',
        'fs-callback': 'function',
        'fs-duration': 'string',
        'fs-delay': 'string',
        'fs-count': 'string',
        'fs-target': 'selector',
        'fs-disappear': 'boolean'
    };

    farsight._utils.extend(this, o, false, false);

    var self = this;
    var __init__ = function() {

        self.viewport_instance = new farsight.Viewport({vonly: self.vonly, honly: self.honly, viewport: self.viewport, pane: {elem: self.pane}});

        var $elems = $(self.selector);
        for (var i = 0; i < $elems.length; i++) {
            var $elem = $($elems[i]);
            var o = farsight._utils.parseAttrs($elem, attrs);

            var ae = new farsight.ActiveElement($elem, self.viewport_instance, o, false);
            self.elements.push(ae);

            function executePreFn(fpath, ae) {
                var pts = fpath.split('.');
                pts.splice(pts.length-1, 0, '_pre');

                var pre_fn = farsight._utils.getPropertyByNamespace(pts);
                if (pre_fn) {
                    pre_fn(ae);
                }
            }

            if (o && o.hasOwnProperty('callback')) {
                if (typeof o['callback'] === 'function') {
                    executePreFn(ae.element.attr('fs-callback'), ae);
                } else {
                    for (var k  in o['callback']) {
                        executePreFn(k, ae);
                    }
                }
            }
            ae.update();
        }
    }
    __init__();
}
<|endoftext|>