Source: winjscontrib.ui.datasourcemanager.js

/* 
 * WinJS Contrib v2.1.0.6
 * licensed under MIT license (see http://opensource.org/licenses/MIT)
 * sources available at https://github.com/gleborgne/winjscontrib
 */

var WinJSContrib = WinJSContrib || {};
WinJSContrib.UI = WinJSContrib.UI || {};

/** @namespace */
WinJSContrib.UI.DataSources = WinJSContrib.UI.DataSources || {};


(function (ds, gr) {
    "use strict";

    ds.DataSourceManager = WinJS.Class.mix(WinJS.Class.define(
    /** 
     * 
     * @class WinJSContrib.UI.DataSources.DataSourceManager
     * @classdesc helper class to manage an array with filter and grouping facilities and plug it into listview with or without semantic zoom. 
     * This is a low level helper object, it's likely that relying on {@link WinJSContrib.UI.SemanticListViews} will be easier.
     * @param {Object} options
     * @example
     * {@lang javascript}
     * var datamgr = new WinJSContrib.UI.DataSources.DataSourceManager({
     *    defaultGroupLimit: 12,
     *    groupKind: WinJSContrib.UI.DataSources.Grouping.byField,
     *    field: 'metadata.genre',
     *    listview: page.zoomedInList,
     *    zoomedOutListView: page.zoomedOutList
     * });
     * 
     */
    function (options) {
        this.groupedList = null;
        this.list = null;
        this.defaultGroupLimit = 0;
        this.filters = new WinJS.Binding.List();
        this.filters.onitemchanged = this.init.bind(this);
        this.filters.oniteminserted = this.init.bind(this);
        this.filters.onitemremoved = this.init.bind(this);
        this.apply(options);
    },
    /**
     * @lends WinJSContrib.UI.DataSources.DataSourceManager.prototype
     */
    {
        apply: function (options) {
            options = options || {};

            if (options.field)
                this.field = options.field;

            if (options.defaultGroupLimit) this.defaultGroupLimit = options.defaultGroupLimit;
            if (options.filter) this.filter = options.filter;
            if (options.groupKind) {
                if (options.groupKind != this.groupKind) {
                    this.groupKind = options.groupKind;
                }
                options = options.groupKind(options);
            }
            if (options.defaultGroupName) this.defaultGroupName = options.defaultGroupName;
            if (options.compareGroups) this.compareGroups = options.compareGroups;
            if (options.getGroupKey) this.getGroupKey = options.getGroupKey;
            if (options.getGroupData) this.getGroupData = options.getGroupData;
            if (options.listview) this.listview = options.listview;
            if (options.zoomedOutListView) this.zoomedOutListView = options.zoomedOutListView;
            if (options.items) this.items = options.items;

        },

        /**
         * path to the field to use for groupings
         * @field
         * @type string
         */
        field: {
            get: function () {
                return this._field;
            },
            set: function (val) {
                this._field = val;
            }
        },

        /**
         * no grouping if items.length < defaultGroupLimit
         * @field
         * @type number
         */
        defaultGroupLimit: {
            get: function () {
                return this._defaultGroupLimit;
            },
            set: function (val) {
                this._defaultGroupLimit = val;
            }
        },

        /**
         * groupKind grouping manager function
         * @field
         * @type function
         */
        groupKind: {
            get: function () {
                return this._groupKind;
            },
            set: function (val) {
                if (val != this._groupKind) {
                    this._groupKind = val;
                    this.apply({ groupKind: val });
                    this.init();
                }
                if (!val) {
                    this.compareGroups = null;
                    this.getGroupData = null;
                    this.getGroupKey = null;
                }
            }
        },

        /**
         * items listview
         * @field
         * @type WinJS.UI.ListView
         */
        listview: {
            get: function () {
                return this._listview;
            },
            set: function (val) {
                if (this._listview) {
                    this.detach();
                }

                this._listview = val;
            }
        },

        /**
         * listview for grouped items
         * @field
         * @type WinJS.UI.ListView
         */
        groupedList: {
            get: function () {
                return this._groupedList;
            },
            set: function (val) {
                this._groupedList = val;
            }
        },

        /**
         * item filter callback
         * @field
         * @type function
         */
        filter: {
            get: function () {
                return this._filter;
            },
            set: function (val) {
                if (this._filter) {
                    this.detach();
                }

                this._filter = val;
                if (val) {
                    this.init();
                }
            }
        },

        /**
         * data items
         * @field
         * @type Array
         */
        items: {
            get: function () {
                return this._items;
            },
            set: function (val) {
                if (this._items) {
                    this.detach();
                }
                this._items = val;
                if (val) {
                    this.init();
                }
                else {
                    this.list = null;
                    this.groupedList = null;
                    this.filteredlist = null;
                    this.detach();
                }
            }
        },

        _filterItems: function (item) {
            var ctrl = this;
            var res = function (item) { return true; };

            if (this.filter)
                res = function (item) { return ctrl.filter(item) };

            if (this.filters.length) {
                var addFilter = function (f, newFilter) {
                    return function (item) {
                        var tmp = f(item);
                        if (!tmp)
                            return false;

                        return newFilter(item);
                    }
                }

                for (var i = 0 ; i < this.filters.length ; i++) {
                    var filter = this.filters.getAt(i);
                    res = addFilter(res, filter);
                }
            }

            return res;
        },

        refresh: function () {
            this.init();
        },

        /**
         * initialise data
         */
        init: function (dontAttach) {
            var ctrl = this;
            this.detach();
            if (this.items) {
                this.list = new WinJS.Binding.List(this.items);

                if (this.filter || this.filters.length) {
                    var filterCallback = this._filterItems();
                    this.filteredlist = this.list.createFiltered(filterCallback);
                }
                else {
                    this.filteredlist = this.list;
                }

                if (this.getGroupKey && this.getGroupData && this.compareGroups) {
                    this.groupedList = this.filteredlist.createGrouped(this.getGroupKey.bind(this), this.getGroupData.bind(this), this.compareGroups.bind(this));

                    var g = ctrl.groupedList.groups;
                    for (var n in g._groupItems) {
                        console.log(n);
                        var grp = ctrl.groupedList._groupsProjection._groupItems[n];
                        if (grp && grp.groupSize !== undefined)
                            grp.data.groupCount = grp.groupSize;
                    }
                } else {
                    this.groupedList = null;
                }

                if (!dontAttach)
                    this.attach();
            }
        },

        /**
         * clean-up, initialise data and bind listviews to data
         * @param {Array} items
         */
        prepareItems: function (items) {
            this.detach();
            this._items = items;
            this.init(true);
        },

        /**
         * attach listviews to data
         */
        attach: function () {
            if ((this.groupedList || this.filteredlist) && this.listview) {
                if (this.groupedList && this.groupedList.length >= this.defaultGroupLimit) {
                    this.listview.itemDataSource = this.groupedList.dataSource;
                    this.listview.groupDataSource = this.groupedList.groups.dataSource;

                    if (this.zoomedOutListView)
                        this.zoomedOutListView.itemDataSource = this.groupedList.groups.dataSource;

                    var semzoom = this.parentSemanticZoom();
                    if (semzoom) {
                        semzoom.locked = false;
                    }
                }
                else if (this.filteredlist && (this.filteredlist.length >= this.defaultGroupLimit || !this.groupedList)) {
                    this.listview.itemDataSource = this.filteredlist.dataSource;
                    this.listview.groupDataSource = null;
                    var semzoom = this.parentSemanticZoom();
                    if (semzoom) {
                        semzoom.locked = true;
                    }
                }
                //else if (this.filteredlist && this.filteredlist.length) {
                //    this.listview.itemDataSource = this.filteredlist.dataSource;
                //    this.listview.groupDataSource = null;
                //    var semzoom = this.parentSemanticZoom();
                //    if (semzoom) {
                //        semzoom.locked = true;
                //    }
                //}
                else {
                    if (this.groupedList)
                        this.listview.itemDataSource = this.groupedList.dataSource;
                    this.listview.groupDataSource = null;
                    if (this.zoomedOutListView)
                        this.zoomedOutListView.itemDataSource = null;

                    var semzoom = this.parentSemanticZoom();
                    if (semzoom) {
                        semzoom.locked = true;
                    }
                }
            }
        },

        /**
         * get parent semantic zoom control
         * @returns {WinJS.UI.SemanticZoom}
         */
        parentSemanticZoom: function () {
            var parent = this.listview.element.parentElement;
            while (parent) {
                if (parent.classList.contains('win-semanticzoom'))
                    return parent.winControl;
                parent = parent.parentElement;
            }
        },

        /**
         * detach listviews from data
         */
        detach: function () {
            if (this.listview) {
                this.listview.itemDataSource = null;
                this.listview.groupDataSource = null;
                if (this.zoomedOutListView)
                    this.zoomedOutListView.itemDataSource = null;
            }
        }
    }), WinJS.UI.DOMEventMixin);

    WinJSContrib.UI.SemanticListViews = WinJS.Class.mix(WinJS.Class.define(
    /** 
     * 
     * @class WinJSContrib.UI.SemanticListViews
     * @classdesc Control wrapping semantic zoom and listviews with a {@link WinJSContrib.UI.DataSources.DataSourceManager}
     * @param {HTMLElement} element DOM element containing the control
     * @param {Object} options
     * 
     * @example {@lang xml}
     * <div id="semanticzoom" data-win-control="WinJSContrib.UI.SemanticListViews" data-win-options="{
     *      listview: { itemTemplate: select('#listItemTemplate'), groupHeaderTemplate : select('#groupItemTemplate')},
     *      zoomedOutListview: { itemTemplate: select('#semanticItemTemplate')},
     *      data: {
     *          defaultGroupLimit: 12,
     *          groupKind: WinJSContrib.UI.DataSources.Grouping.byField,
     *          field: 'metadata.genre',
     *          items: moviesSample
     *      }}">
     * </div>
     */
    function ctor(element, options) {
        this.element = element || document.createElement('DIV');
        options = options || {};
        this.element.winControl = this;
        this.element.classList.add('win-disposable');
        WinJS.UI.setOptions(this, options);
        this._initControl(options);
    },
    /**
     * @lends WinJSContrib.UI.SemanticListViews.prototype
     */
    {
        /**
         * semantic zoom control
         * @field
         * @type {WinJS.UI.SemanticZoom}
         */
        semanticZoom: {
            get: function () {
                return this._semanticZoom;
            },
            set: function (val) {
                this._semanticZoom = val;
            }
        },

        /**
         * items listview
         * @field
         * @type WinJS.UI.ListView 
         */
        listview: {
            get: function () {
                return this._listview;
            },
            set: function (val) {
                this._listview = val;
            }
        },

        /**
         * listview for zoomed out items (usually groups)
         * @field
         * @type WinJS.UI.ListView 
         */
        zoomedOutListview: {
            get: function () {
                return this._zoomedOutListview;
            },
            set: function (val) {
                this._zoomedOutListview = val;
            }
        },

        /**
         * dataManager datasource manager
         * @field
         * @type WinJSContrib.UI.DataSources.DataSourceManager 
         */
        dataManager: {
            get: function () {
                return this._dataManager;
            }
        },

        _initControl: function (options) {
            var semanticPlaceholder = document.createElement('DIV')
            semanticPlaceholder.style.width = '100%';
            semanticPlaceholder.style.height = '100%';

            var listviewPlaceholder = document.createElement('DIV');
            listviewPlaceholder.style.width = '100%';
            listviewPlaceholder.style.height = '100%';
            listviewPlaceholder.className = 'zoomedinlist';

            var zoomedOutListviewPlaceholder = document.createElement('DIV');
            zoomedOutListviewPlaceholder.style.width = '100%';
            zoomedOutListviewPlaceholder.style.height = '100%';
            zoomedOutListviewPlaceholder.className = 'zoomedoutlist';

            semanticPlaceholder.appendChild(listviewPlaceholder);
            semanticPlaceholder.appendChild(zoomedOutListviewPlaceholder);
            this.element.appendChild(semanticPlaceholder);

            this._listview = new WinJS.UI.ListView(listviewPlaceholder, options.listview);
            this._zoomedOutListview = new WinJS.UI.ListView(zoomedOutListviewPlaceholder, options.zoomedOutListview);

            this._semanticZoom = new WinJS.UI.SemanticZoom(semanticPlaceholder, options.semanticzoom);

            var dataOptions = options.data || {};
            dataOptions.listview = this._listview;
            dataOptions.zoomedOutListView = this._zoomedOutListview;
            this._dataManager = new ds.DataSourceManager(dataOptions);
        },

        /**
         * release control
         */
        dispose: function () {
            WinJS.Utilities.disposeSubTree(this.element);
        }
    }), WinJS.UI.DOMEventMixin, WinJS.Utilities.createEventProperties("myevent"));
})(WinJSContrib.UI.DataSources, WinJSContrib.UI.DataSources.Grouping);

if (WinJSContrib.UI.WebComponents) {
    WinJSContrib.UI.WebComponents.register('mcn-semanticlistviews', WinJSContrib.UI.SemanticListViews, {
        properties: [],
        controls: {
            "listview": WinJS.UI.ListView,
            "zoomedOutListview": WinJS.UI.ListView,
            "semanticZoom": WinJS.UI.SemanticZoom
        },
        map: {
            "DEFAULTGROUPLIMIT": { attribute: 'defaultGroupLimit', property: '_dataManager.defaultGroupLimit', resolve: true },
            "GROUPKIND": { attribute: 'groupKind', property: '_dataManager.groupKind', resolve: true },
            "FIELD": { attribute: 'field', property: '_dataManager.field', resolve: true },
            "ITEMS": { attribute: 'items', property: '_dataManager.items', resolve: true },
        }
    });
}

/** 
 * Custom grouping settings for {@link WinJSContrib.UI.DataSources.DataSourceManager}
 * @namespace 
 * @example
 * var datamgr = new WinJSContrib.UI.DataSources.DataSourceManager({
 *    defaultGroupLimit: 12,
 *    groupKind: WinJSContrib.UI.DataSources.Grouping.byField,
 *    field: 'metadata.genre',
 *    listview: page.zoomedInList,
 *    zoomedOutListView: page.zoomedOutList
 * });
 */
WinJSContrib.UI.DataSources.Grouping = WinJSContrib.UI.DataSources.Grouping || {};

(function (ds, gr) {
    "use strict";

    /**
     * alphabetic grouping, use "options.field" to define the property used for grouping
     * @param {Object} options grouping options
     * @returns {Object} group manager
     */
    WinJSContrib.UI.DataSources.Grouping.alphabetic = WinJS.Utilities.markSupportedForProcessing(function (options) {
        options = options || {};
        options.defaultGroupName = options.defaultGroupName || '#';

        options.compareGroups = options.compareGroups || function (left, right) {
            return left.toUpperCase().charCodeAt(0) - right.toUpperCase().charCodeAt(0);
        }

        options.getGroupKey = options.getGroupKey || function (dataItem) {
            if (!dataItem)
                return this.defaultGroupName;

            var val = WinJSContrib.Utils.readProperty(dataItem, this.field);
            if (!val || !val[0])
                return this.defaultGroupName;

            var key = val[0].toUpperCase();
            return key;
        }

        options.getGroupData = options.getGroupData || function (dataItem) {
            var val = '#';
            if (dataItem)
                val = WinJSContrib.Utils.readProperty(dataItem, this.field);
            if (!val || !val[0])
                val = this.defaultGroupName;

            var key = val[0].toUpperCase();
            return {
                title: key
            };
        }
        return options;
    });

    /**
     * grouping by a string field on items, use "options.field" to define the property used for grouping
     * @param {Object} options grouping options
     * @returns {Object} group manager
     */
    WinJSContrib.UI.DataSources.Grouping.byField = WinJS.Utilities.markSupportedForProcessing(function (options) {
        options.defaultGroupName = options.defaultGroupName || '#';

        options.compareGroups = options.compareGroups || function (a, b) {
            if (a > b)
                return 1;
            if (a < b)
                return -1;

            return 0;
        }

        options.getGroupKey = options.getGroupKey || function (dataItem) {
            if (!dataItem)
                return options.defaultGroupNam;

            var val = WinJSContrib.Utils.readProperty(dataItem, this.field);
            if (!val)
                return this.defaultGroupName;
            var key = val.toString();
            if (typeof val !== 'string' && (val.length != null && val.length != undefined)) {
                key = val.join(", ");
            }
            
            if (key.trim().length == 0)
                key = options.defaultGroupName;

            key = key.toUpperCase();

            return key || options.defaultGroupName;
        }

        options.getGroupData = options.getGroupData || function (dataItem) {
            var val = '#';
            if (dataItem)
                val = WinJSContrib.Utils.readProperty(dataItem, this.field);
            if (!val)
                val = this.defaultGroupName;

            var key = val.toString();
            if (typeof val !== 'string' && (val.length != null && val.length != undefined)) {
                key = val.join(", ");
            }
            
            if (key.trim().length == 0)
                key = options.defaultGroupName;

            return {
                title: key || options.defaultGroupName
            };
        }

        return options;
    });

    /**
     * grouping by year from a date field, use "options.field" to define the property used for grouping
     * @param {Object} options grouping options
     * @returns {Object} group manager
     */
    WinJSContrib.UI.DataSources.Grouping.byYear = WinJS.Utilities.markSupportedForProcessing(function (options) {
        options.defaultGroupName = options.defaultGroupName || '#';

        options.compareGroups = options.compareGroups || function (a, b) {
            if (a > b)
                return 1;
            if (a < b)
                return -1;

            return 0;
        }

        options.getGroupKey = options.getGroupKey || function (dataItem) {
            if (!dataItem)
                return this.defaultGroupName;

            var val = WinJSContrib.Utils.readProperty(dataItem, this.field);
            if (!val)
                return this.defaultGroupName;

            var key = new Date(new Date(new Date(val).setMonth(0, 1)).setHours(0, 0, 0, 0));
            return key.toISOString();
        }

        options.getGroupData = options.getGroupData || function (dataItem) {
            var val = '#';
            if (dataItem)
                val = WinJSContrib.Utils.readProperty(dataItem, this.field);
            if (!val)
                val = this.defaultGroupName;

            var key = new Date(new Date(new Date(val).setMonth(0, 1)).setHours(0, 0, 0, 0));
            return {
                title: key.getFullYear().toString(),
                date: key
            };
        }

        return options;
    });

    /**
     * grouping by month from a date field, use "options.field" to define the property used for grouping
     * @param {Object} options grouping options
     * @returns {Object} group manager
     */
    WinJSContrib.UI.DataSources.Grouping.byMonth = WinJS.Utilities.markSupportedForProcessing(function (options) {
        options.defaultGroupName = options.defaultGroupName || '#';

        options.compareGroups = options.compareGroups || function (a, b) {
            if (a > b)
                return 1;
            if (a < b)
                return -1;

            return 0;
        }

        options.getGroupKey = options.getGroupKey || function (dataItem) {
            if (!dataItem)
                return this.defaultGroupName;

            var val = WinJSContrib.Utils.readProperty(dataItem, this.field);
            if (!val)
                return this.defaultGroupName;

            var key = new Date(new Date(new Date(val).setDate(1)).setHours(0, 0, 0, 0));
            return key.toISOString();
        }

        options.getGroupData = options.getGroupData || function (dataItem) {
            var val = '#';
            if (dataItem)
                val = WinJSContrib.Utils.readProperty(dataItem, this.field);
            if (!val)
                val = this.defaultGroupName;

            var key = new Date(new Date(new Date(val).setDate(1)).setHours(0, 0, 0, 0));
            return {
                title: WinJSContrib.Utils.pad2(key.getMonth() + 1) + '-' + key.getFullYear().toString(),
                date: key
            };
        }

        return options;
    });

    /**
     * grouping by day from a date field, use "options.field" to define the property used for grouping
     * @param {Object} options grouping options
     * @returns {Object} group manager
     */
    WinJSContrib.UI.DataSources.Grouping.byDay = WinJS.Utilities.markSupportedForProcessing(function (options) {
        options.defaultGroupName = options.defaultGroupName || '#';

        options.compareGroups = options.compareGroups || function (a, b) {
            if (a > b)
                return 1;
            if (a < b)
                return -1;

            return 0;
        }

        options.getGroupKey = options.getGroupKey || function (dataItem) {
            if (!dataItem)
                return this.defaultGroupName;

            var val = WinJSContrib.Utils.readProperty(dataItem, this.field);
            if (!val)
                return this.defaultGroupName;

            var key = new Date(new Date(val).setHours(0, 0, 0, 0));
            return key.toISOString();
        }

        options.getGroupData = options.getGroupData || function (dataItem) {
            var val = '#';
            if (dataItem)
                val = WinJSContrib.Utils.readProperty(dataItem, this.field);
            if (!val)
                val = this.defaultGroupName;

            var key = new Date(new Date(val).setHours(0, 0, 0, 0));
            return {
                title: WinJSContrib.Utils.pad2(key.getMonth() + 1) + '-' + WinJSContrib.Utils.pad2(key.getDate()) + '-' + key.getFullYear().toString(),
                date: key
            };
        }

        return options;
    });

})(WinJSContrib.UI.DataSources, WinJSContrib.UI.DataSources.Grouping);