ux-datagrid-sortModel.js

! ux-angularjs-datagrid v.1.4.11 (c) 2016, Obogo https://github.com/obogo/ux-angularjs-datagrid License: MIT.

(function (exports, global) {
if (typeof define === "function" && define.amd) {
  define(exports);
} else if (typeof module !== "undefined" && module.exports) {
  module.exports = exports;
} else {
  global.ux = exports;
}

exports.datagrid.events.ON_BEFORE_SORT = "datagrid:onBeforeSort";

exports.datagrid.events.ON_AFTER_SORT = "datagrid:onAfterSort";

exports.datagrid.events.ON_BEFORE_TOGGLE_SORT = "datagrid:onBeforeToggleSort";

exports.datagrid.events.ON_AFTER_TOGGLE_SORT = "datagrid:onAfterToggleSort";

exports.datagrid.events.CLEAR_SORTS = "datagrid:clearSorts";

exports.datagrid.events.CLEAR_ALL_SORTS = "datagrid:clearAllSorts";

angular.module("ux").service("sortStatesModel", [ "$location", "$rootScope", function($location, $rootScope) {


ColumnStates for sorting. Singleton. Keeps track of weather a column is sorted or not. It persists based on the path.

Type
Object ************************************************************************************
    exports.datagrid.sortStatesModel = function() {
        var api = exports.logWrapper("columnSortStatesModel", {}, "blue", function() {
            exports.util.apply($rootScope.$emit, $rootScope, arguments);
        }), sortOptions = {
            ASC: "asc",
            DESC: "desc",
            NONE: "none"

}, lastPath = "", states = {}, multipleStates = {}, ignoreParamsInPath = {},
The default of weather multiple states are allowed or not.

Type
Boolean
        defaultAllowMultipleStates = false;

Params
prop String
        function isPrivate(prop) {
            return prop.charAt(0) === "$";
        }

Get the whole url as a key to store the scroll value against.

        function getCurrentPath() {
            return $location.path();
        }

Return just the url without any GET/Search params.

        function getCurrentPathWithoutParams() {
            return api.getCurrentPath().split("?").shift();
        }

Returns
string {string}
        function getPath() {
            var path;
            if (api.getIgnoreParamsInPath()) {
                path = api.getCurrentPathWithoutParams();
            } else {
                path = api.getCurrentPath();
            }
            if (path !== lastPath) {
                api.log("getPath changed from %s to %s", lastPath, path);
                lastPath = path;
            }
            return path;
        }

Check for allowing of multipleStates on a per path basis or fall back on the defaultAllowMultipleStates

Returns
Boolean {Boolean}
        function getAllowMultipleStates() {
            var result = multipleStates[api.getPath()];
            if (result === true || result === false) {
                return result;
            }
            return defaultAllowMultipleStates;
        }

Set the allow multiple states on a per path basis.

Params
value Boolean
path String=
        function setAllowMultipleStates(value, path) {
            path = path || api.getPath();
            if (value !== undefined) {
                api.log("setAllowMultipleStates %s", value);
                multipleStates[path] = value === true;
            }
            return !!multipleStates[path];
        }

Return weather params are ignored in storing path values.

Returns
Boolean {Boolean}
        function getIgnoreParamsInPath() {
            return ignoreParamsInPath[api.getCurrentPathWithoutParams()] === true;
        }

Change if path is stored with or without params.

Params
value Boolean
        function setIgnoreParamsInPath(value) {
            api.log("setIgnoreParamsInPath %s", value);
            ignoreParamsInPath[api.getCurrentPathWithoutParams()] = value;
        }

See if there is a path that is registered or not.

Params
path String=
        function hasPathState(path) {
            path = path || api.getPath();
            return !!states[path];
        }

Params
path String=
Returns
*
        function getPathState(path) {
            path = path || api.getPath();
            if (!states[path]) {
                states[path] = {
                    $dirty: false,
                    $path: path,
                    $order: []
                };
            }
            return states[path];
        }

Override all of the path states. Any columnNames not passed are set to none if not allow multiples.

Params
pathState Object
        function setPathState(pathState) {
            var columnName, currentPathState = api.getPathState();
            api.log("setPathState %s to %s", currentPathState, pathState);
            api.setIgnoreParamsInPath(true);
            for (columnName in pathState) {
                if (exports.util.apply(Object.prototype.hasOwnProperty, pathState, [ columnName ]) && pathState[columnName] !== currentPathState[columnName] && !isPrivate(columnName)) {
                    api.setState(columnName, pathState[columnName], currentPathState);
                }
            }
        }

Returns the current state at the currentPath of the columnName. 'none', 'asc', 'desc'

Params
columnName
Returns
String {String}
        function getState(columnName) {
            var pathState = api.getPathState(api.getPath());
            if (pathState[columnName] === undefined) {
                pathState[columnName] = sortOptions.NONE;
            }
            return pathState[columnName];
        }

Set the state at the currentPath for that columnName.

Params
columnName String
state String
pathState Object=
        function setState(columnName, state, pathState) {
            var index, prevState;
            pathState = pathState || api.getPathState(api.getPath());
            if (!api.isPrivate(columnName) && pathState[columnName] !== state) {
                prevState = pathState[columnName];
                if (api.getAllowMultipleStates(pathState.$path)) {
                    index = pathState.$order.indexOf(columnName);
                    if (index !== -1) {
                        pathState.$order.splice(index, 1);
                    }
                }
                if (state !== sortOptions.NONE) {
                    if (!api.getAllowMultipleStates()) {
                        api.clear(pathState);
                    }
                    pathState.$order.push(columnName);
                }
                pathState[columnName] = state;
                if (!prevState && state === sortOptions.NONE) {
                    return;
                }
                api.dirtyState(pathState);
            }
        }

converts the path state into a string key. ex("description:asc|index:desc")

Params
pathState Object=
        function createKeyFromStates(pathState) {
            pathState = pathState || api.getPathState(api.getPath());
            var combo = {
                text: "",
                pathState: pathState
            };
            exports.each(pathState.$order, api.createKeyFromState, combo);
            return combo.text;
        }

converts the state into a key string. ex("description:asc")

Params
columnName
index
list
combo
        function createKeyFromState(columnName, index, list, combo) {
            if (!api.isPrivate(columnName)) {
                combo.text += (combo.text.length ? "|" : "") + columnName + ":" + combo.pathState[columnName];
            }
        }

Increment the columnName's state to the next in the list.

Params
columnName String
        function toggle(columnName) {
            var state = api.getState(columnName), nextState = api.getNextState(state);
            api.info("toggle %s from %s to %s", columnName, state, nextState);
            api.setState(columnName, nextState);
            api.dirtyState();
        }

Sets a state to dirty for the next lookup.

Params
pathState Object=
        function dirtyState(pathState) {
            pathState = pathState || api.getPathState(api.getPath());
            api.log("dirtyState %s", pathState);
            pathState.$dirty = true;
        }

clears the dirty state.

Params
pathState Object=
        function clearDirty(pathState) {
            pathState = pathState || api.getPathState(api.getPath());
            api.log("clearDirty %s", pathState);
            pathState.$dirty = false;
        }

calculate the next state would be given a currentState. Used for toggling states.

Params
state String
Returns
String {String}
        function getNextState(state) {
            var result = sortOptions.ASC;
            switch (state) {
              case sortOptions.NONE:
                result = sortOptions.ASC;
                break;

              case sortOptions.ASC:
                result = sortOptions.DESC;
                break;

              case sortOptions.DESC:
                result = sortOptions.NONE;
                break;
            }
            return result;
        }

Determines if a state is dirty or not.

Params
pathState String=
        function hasDirtySortState(pathState) {
            pathState = pathState || api.getPathState(api.getPath());
            return pathState.$dirty;
        }

Clears up undefined and null values.

Params
value
        function cleanSortValue(value) {

undefined and null should be compared as an empty string.

            var result = value === undefined || value === null ? "" : value;
            if (typeof result === "string") {
                return result.toLowerCase();
            }
            return result;
        }

Listed so it can be overridden for different locals. override by setting the method on ux.datagrid.sortStatesModel.

        function getLocale() {
            return "en";
        }

Compare the string values for sorting.

Params
a String
v String
        function sortValueCompare(a, b) {
            a = api.cleanSortValue(a);
            b = api.cleanSortValue(b);
            return a > b ? 1 : a < b ? -1 : 0;
        }

Params
a *
b *
        function sortNone(a, b) {
            return 0;
        }

Create an ASC sort wrapped on that property.

Params
property String
        function createAscSort(property) {
            return function asc(a, b) {
                var av = a[property], bv = b[property];
                return api.sortValueCompare(av, bv);
            };
        }

Create a DESC sort wrapped on that property.

Params
property String
        function createDescSort(property) {
            return function desc(a, b) {
                var av = a[property], bv = b[property];
                return -api.sortValueCompare(av, bv);
            };
        }

Clear the states for the currentPath.

Params
pathState Object=
        function clear(pathState) {
            var i;
            pathState = pathState || api.getPathState(api.getPath());
            pathState.$order.length = 0;
            for (i in pathState) {
                if (exports.util.apply(Object.prototype.hasOwnProperty, pathState, [ i ]) && !api.isPrivate(i) && pathState[i] !== sortOptions.NONE) {
                    pathState[i] = sortOptions.NONE;
                }
            }
        }

Clear all stored sorts.

        function clearAll() {
            states = {};
            multipleStates = {};
        }
        api.getPath = getPath;
        api.getCurrentPath = getCurrentPath;
        api.getCurrentPathWithoutParams = getCurrentPathWithoutParams;
        api.sortValueCompare = sortValueCompare;
        api.cleanSortValue = cleanSortValue;
        api.getNextState = getNextState;
        api.createKeyFromState = createKeyFromState;
        api.getAllowMultipleStates = getAllowMultipleStates;
        api.setAllowMultipleStates = setAllowMultipleStates;
        api.getIgnoreParamsInPath = getIgnoreParamsInPath;
        api.setIgnoreParamsInPath = setIgnoreParamsInPath;
        api.hasPathState = hasPathState;
        api.getPathState = getPathState;
        api.setPathState = setPathState;
        api.getState = getState;
        api.setState = setState;
        api.toggle = toggle;
        api.dirtyState = dirtyState;
        api.clearDirty = clearDirty;
        api.sortNone = sortNone;
        api.createAscSort = createAscSort;
        api.createDescSort = createDescSort;
        api.hasDirtySortState = hasDirtySortState;
        api.createKeyFromStates = createKeyFromStates;
        api.isPrivate = isPrivate;
        api.getLocale = getLocale;
        api.clear = clear;
        api.clearAll = clearAll;
        api.sortOptions = sortOptions;
        return api;
    }();
    return exports.datagrid.sortStatesModel;
} ]);


The sortModel addon is used to apply sorting to the contents of the grid. It stores the sorted state on the sortStatesModel to keep around until cleared.

Params
sortStatesModel Object
angular.module("ux").factory("sortModel", [ "sortStatesModel", function(sortStatesModel) {
    return [ "inst", function sortModel(inst) {

cache is the stored sort values. It needs to be cleared if the data changes.

        var result = exports.logWrapper("sortModel", {}, "blue", inst), sorts = {}, original, cache = {}, options = inst.options.sortModel || {}, lastSortResult;

add a column so that it's sort state can be toggled and used.

Params
name String
methods Object
        result.addSortColumn = function addSortColumn(name, methods) {
            sorts[name] = methods;
            var pathState = sortStatesModel.getPathState();
            pathState[name] = pathState[name] || sortStatesModel.sortOptions.NONE;
        };

Params
key String
        result.getCache = function getCache(key) {
            return cache[key];
        };

Stores the sort value locally so that if that sort is again performed it will not need to process.

Params
key String
value Array
        result.setCache = function setCache(key, value) {
            cache[key] = value;
        };

Apply the sorts to the array. Pull from cache if it exists.

Params
ary Array
sortOptions Object
clear Boolean=
        result.applySorts = function applySorts(ary, sortOptions, clear) {
            var pathStateRef = sortStatesModel.getPathState(), currentPathState = angular.copy(pathStateRef);
            if (sortOptions) {
                result.log("apply sortOptions");
                sortStatesModel.setPathState(sortOptions);
            }
            if (original !== ary || sortStatesModel.hasDirtySortState(pathStateRef)) {
                original = ary;
                if (!original) {
                    lastSortResult = original;
                    return lastSortResult;
                }
                if (clear) {
                    result.clear();
                }
                result.setCache("", original);

the original is always without any sort options.

                if (!result.$processing) {
                    result.$processing = true;
                    var key = sortStatesModel.createKeyFromStates(pathStateRef), event, pathState = angular.copy(pathStateRef);

clone so they cannot mess with the data directly.

                    result.info("applySorts %s", key);
                    event = inst.dispatch(exports.datagrid.events.ON_BEFORE_SORT, key, currentPathState, pathState);

prevent default on event to prevent sort.

                    if (!event.defaultPrevented) {
                        if (!result.getCache(key) || result.getCache(key).length !== original.length) {
                            result.log("  store sort %s", key);
                            result.setCache(key, original && original.slice(0) || []);

clone it

                            ux.each(pathState.$order, applyListSort, {
                                grouped: inst.grouped,
                                pathState: pathState,
                                ary: result.getCache(key)
                            });
                        } else {
                            result.log("  pull sort from cache");
                        }
                        lastSortResult = result.getCache(key);
                        sortStatesModel.clearDirty(pathStateRef);
                        if (options.enableCache === false) {
                            result.clear();
                        }
                    } else {

TODO: need to unit test this to make sure it works with async sort.

                        lastSortResult = original;
                    }
                    result.$processing = false;
                    inst.dispatch(exports.datagrid.events.ON_AFTER_SORT, key, pathState, currentPathState);
                }
            }
            return lastSortResult;
        };

Perform the sort dictated by the state.

Params
ary Array
columnName String
pathState Object
        function sortArray(ary, columnName, pathState) {
            var state = pathState[columnName];
            if (state && sorts[columnName]) {
                ux.util.array.sort(ary, sorts[columnName][state]);
            }
            return ary;
        }

Take into consideration for grouped data and apply the appropriate sorts for the array.

Params
columnName String
index Number
list Array
data Object
        function applyListSort(columnName, index, list, data) {
            var i, len;
            if (!options.groupSort && data.grouped && data.ary.length && data.ary[0].hasOwnProperty(data.grouped)) {
                len = data.ary.length;
                for (i = 0; i < len; i += 1) {
                    data.ary[i] = angular.extend({}, data.ary[i]);

shallow copy

                    data.ary[i][data.grouped] = sortArray(data.ary[i][data.grouped].slice(0), columnName, data.pathState);
                }
            } else {
                sortArray(data.ary, columnName, data.pathState);
            }
        }

Determine if that sort is already applied.

Params
name String
methodName String
        result.isApplied = function isApplied(name, methodName) {
            return sortStatesModel.getPathState()[name] === methodName;
        };

Get the current sort state of that column.

Params
name String
        result.getSortStateOf = function getSortStateOf(name) {
            return sortStatesModel.getPathState()[name];
        };

Enable or disable multiple state sorting.

Type
setAllowMultipleStates
        result.multipleSort = sortStatesModel.setAllowMultipleStates;

Get the sortKey for a state.

Type
Function createKeyFromStates
        result.getSortKey = sortStatesModel.createKeyFromStates;

Based on the options passed, automatically add sort columns for those and create states. If the states already exist apply those states on the render.

        function addSortsFromOptions() {
            var i, methods, alreadyHasState = sortStatesModel.hasPathState(), pathState = sortStatesModel.getPathState();
            options.sorts = options.sorts || inst.options.sorts;
            if (options.sorts) {
                for (i in options.sorts) {
                    if (typeof options.sorts[i] === "object") {
                        sortStatesModel.setState(i, options.sorts[i].value, pathState);

value is the default sort state.

                        methods = options.sorts[i];
                    } else {
                        if (!alreadyHasState) {
                            sortStatesModel.setState(i, options.sorts[i], pathState);
                        }
                        methods = {
                            asc: sortStatesModel.createAscSort(i),
                            desc: sortStatesModel.createDescSort(i),
                            none: sortStatesModel.sortNone
                        };
                    }
                    result.addSortColumn(i, methods);
                }
            }
        }

Sorts always toggle clockwise none -> asc -> desc.

Params
name String
        result.toggleSort = function toggleSort(name) {
            result.log("toggleSort %s", name);
            inst.dispatch(exports.datagrid.events.ON_BEFORE_TOGGLE_SORT, name);
            if (inst.creepRenderModel) {
                inst.creepRenderModel.stop();
            }
            sortStatesModel.toggle(name);
            result.applySorts(original);
            inst.dispatch(exports.datagrid.events.ON_AFTER_TOGGLE_SORT, name);
        };

Sets the sort value to asc, desc, or none

Params
name String
state String='none'
        result.setSortStateOf = function(name, state) {
            if (state === "none" || state === "asc" || state === "desc") {
                sortStatesModel.setState(name, state);
            }
        };

clear all cached sort values for this grid.

        result.clear = function clear() {
            cache = {};
        };

        result.destroy = function destroy() {
            result = null;
            lastSortResult = null;
            sorts = null;
            cache = null;
            original = null;
            inst.sortModel = null;
            inst = null;
        };
        inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.CLEAR_SORTS, exports.datagrid.sortStatesModel.clear));
        inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.CLEAR_ALL_SORTS, exports.datagrid.sortStatesModel.clearAll));
        inst.sortModel = result;
        addSortsFromOptions();
        return inst;
    } ];
} ]);
}(this.ux = this.ux || {}, function() {return this;}()));