ux-datagrid-expandRows.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.COLLAPSE_ROW = "datagrid:collapseRow";

exports.datagrid.events.EXPAND_ROW = "datagrid:expandRow";

exports.datagrid.events.TOGGLE_ROW = "datagrid:toggleRow";

exports.datagrid.events.ROW_TRANSITION_COMPLETE = "datagrid:rowTransitionComplete";

exports.datagrid.events.COLLAPSE_ALL_EXPANDED_ROWS = "datagrid:collapseAllExpandedRows";

exports.datagrid.options.expandRows = [];

exports.datagrid.options.expandRows.autoClose = true;

exports.datagrid.options.expandRows.scrollOnExpand = true;

angular.module("ux").factory("expandRows", function() {

TODO: on change row template. This needs to collapse the row.

    return [ "inst", function(inst) {
        var intv, result = exports.logWrapper("expandRows", {}, "green", inst), lastGetIndex, cache = {}, opened = {}, opening = false, states = {
            opened: "opened",
            closed: "closed"
        }, superGetTemplateHeight = inst.templateModel.getTemplateHeight, // transition end lookup.
        dummyStyle = document.createElement("div").style, vendor = function() {
            var vendors = "t,webkitT,MozT,msT,OT".split(","), t, i = 0, l = vendors.length;
            for (;i < l; i++) {
                t = vendors[i] + "ransform";
                if (t in dummyStyle) {
                    return vendors[i].substr(0, vendors[i].length - 1);
                }
            }
            return false;
        }(), TRNEND_EV = function() {
            if (vendor === false) return false;
            var transitionEnd = {
                "": "transitionend",
                webkit: "webkitTransitionEnd",
                Moz: "transitionend",
                O: "oTransitionEnd",
                ms: "MSTransitionEnd"
            };
            return transitionEnd[vendor];
        }();
        dummyStyle = null;
        function getIndex(itemOrIndex) {
            lastGetIndex = typeof itemOrIndex === "number" ? itemOrIndex : inst.getNormalizedIndex(itemOrIndex, lastGetIndex);
            return lastGetIndex;
        }
        function setup(item) {
            item.template = item.template || inst.templateModel.defaultName;
            if (!item.cls && !item.style && !item.swap) {
                inst.throwError("expandRows will not work without an cls|style|swap property");
            }
            cache[item.template] = item;
        }
        function setupTemplates() {
            ux.each(inst.options.expandRows, setup);
        }
        function getState(itemOrIndex) {
            var index = getIndex(itemOrIndex);
            return opened[index] ? states.opened : states.closed;
        }
        function toggle(itemOrIndex) {
            if (getState(itemOrIndex) === states.closed) {
                expand(itemOrIndex);
            } else {
                collapse(itemOrIndex);
            }
        }
        function expand(itemOrIndex) {
            var index = getIndex(itemOrIndex);
            if (getState(index) === states.closed) {
                result.log("expand %s", itemOrIndex);

prevent multi-finger expand rows.

                if (inst.options.expandRows.autoClose && opening) {
                    return;
                }
                opening = true;
                autoClose([ index ], true);
                setState(index, states.opened);
            }
        }
        function collapse(itemOrIndex, immediate) {
            var index = getIndex(itemOrIndex);
            if (getState(index) === states.opened) {
                result.log("collapse %s", itemOrIndex);
                setState(index, states.closed, immediate);
            }
        }
        function autoClose(omitIndexes, immediate) {
            if (inst.options.expandRows.autoClose) {
                closeAll(omitIndexes, false, immediate);
            }
        }
        function closeAll(omitIndexes, silent, immediate) {
            exports.each(opened, function(cacheItemData, index) {
                var intIndex = parseInt(index, 10);
                if (!omitIndexes || inst.rowsLength > intIndex && omitIndexes.indexOf(intIndex) === -1) {
                    if (silent) {
                        collapse(intIndex, true);
                        delete opened[index];
                    } else {
                        collapse(intIndex, immediate);
                    }
                }
            });
        }
        function setState(index, state, immediate) {
            var template = inst.templateModel.getTemplate(inst.data[index]), elm, tpl, swapTpl;
            if (cache[template.name]) {
                elm = inst.getExistingRow(index);
                if (!elm || !elm.scope()) {

we must be closing a row out of view. possibly destroyed.

                    delete opened[index];
                    return;
                }
                elm.scope().$state = state;
                tpl = cache[template.name];
                if (tpl.transition !== false) {
                    elm[0].addEventListener(TRNEND_EV, onTransitionEnd);
                }
                if (tpl.style) {
                    if (!tpl.reverse) {
                        tpl.reverse = makeReverseStyle(elm, tpl.style);
                    }
                    elm.css(state === states.opened ? tpl.style : tpl.reverse);
                }
                if (tpl.swap && tpl.state !== state) {
                    swapTpl = cache[tpl.swap];
                    swapTpl.cls = swapTpl.cls || "";
                    inst.templateModel.setTemplate(index, tpl.swap, [ swapTpl.cls ]);
                    elm = inst.getRowElm(index);
                } else if (tpl.cls) {
                    elm[state === states.opened ? "addClass" : "removeClass"](tpl.cls);
                    elm.addClass("animating");
                }

we need to wait for the heights to update before updating positions.

                var evt = {
                    target: elm[0],
                    index: index,
                    state: state
                };
                if (immediate) {
                    onTransitionEnd(evt, immediate);
                } else {
                    inst.flow.add(onTransitionEnd, [ evt ], 0);
                }
            } else {
                inst.throwError("unable to toggle template. cls for template '" + template.name + "' was not set.");
            }
        }
        function makeReverseStyle(elm, style) {
            var params = {
                elm: elm,
                style: style,
                reverse: {}
            };
            ux.each(style, reverseStyle, params);
            return params.reverse;
        }
        function reverseStyle(value, key, list, params) {
            params.reverse[key] = params.elm.css(key);
        }
        function onTransitionEnd(event, immediate) {
            var elm, s, index, state;
            if (exports.util.apply(Object.prototype.hasOwnProperty, event, [ "index" ])) {
                elm = inst.getRowElm(event.index);
                index = event.index;
                state = event.state;
            } else {
                elm = angular.element(event.target);
            }
            s = elm.scope();
            if (state && s) {
                s.$index = index;
                s.$state = state;
            } else if (s) {
                index = s.$index;
                state = s.$state;
            }
            elm[0].removeEventListener(TRNEND_EV, onTransitionEnd);
            elm.removeClass("animating");
            if (state === states.opened) {
                opened[index] = {
                    index: index,
                    height: parseInt(elm[0].offsetHeight || 0, 10)
                };
                if (isNaN(opened[index].height)) {
                    inst.throwError("Invalid Height");
                }
            } else {
                delete opened[index];
            }
            inst.updateHeights(index);

if opening and collapsing a row at the same time, we don't want to do this twice.

            if (immediate) {
                opening = false;
            } else {

we told the heights to update. Give time for them to change then fire the event.

                inst.flow.add(function() {
                    if (inst.options.expandRows.scrollOnExpand) {
                        inst.scrollModel.scrollIntoView(index, true);
                    }
                    inst.dispatch(exports.datagrid.events.ROW_TRANSITION_COMPLETE);
                    opening = false;
                    if (inst.options.expandRows.scrollOnExpand) {
                        inst.flow.add(function() {

check for last row. On expansion it needs to scroll down.

                            if (state === states.opened && index === inst.data.length - 1 && inst.getViewportHeight() < inst.getContentHeight()) {
                                inst.scrollModel.scrollToBottom(true);
                            }
                        }, [], 0);
                    }
                }, [], 0);
            }
        }
        function isExpanded(itemOrIndex) {
            var index = getIndex(itemOrIndex);
            return !!opened[index];
        }
        function getTemplateHeight(item) {
            var index = getIndex(item);
            if (opened[index]) {
                result.log("  expandRow %s to height %s", index, opened[index].height);
                return opened[index].height;
            }
            return superGetTemplateHeight(item);
        }
        function destroy() {
            result = null;
            cache = null;
            opened = null;
            states = null;
        }

override the getTemplateHeight to return the result with the expanded height.

        inst.templateModel.getTemplateHeight = getTemplateHeight;
        result.states = states;
        result.getIndex = getIndex;
        result.toggle = function(itemOrIndex) {
            inst.flow.add(toggle, [ itemOrIndex ]);
        };
        result.expand = function(itemOrIndex) {
            inst.flow.add(expand, [ itemOrIndex ]);
        };
        result.collapse = function(itemOrIndex) {
            inst.flow.add(collapse, [ itemOrIndex ]);
        };
        result.isExpanded = isExpanded;
        result.destroy = destroy;
        inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.ON_READY, setupTemplates));
        inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.EXPAND_ROW, function(event, itemOrIndex) {
            result.expand(itemOrIndex);
        }));
        inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.COLLAPSE_ROW, function(event, itemOrIndex) {
            result.collapse(itemOrIndex);
        }));
        inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.TOGGLE_ROW, function(event, itemOrIndex) {
            result.toggle(itemOrIndex);
        }));
        inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.ON_ROW_COMPILE, function(event, $s, el) {
            if (opened[$s.$index]) {
                var template = inst.templateModel.getTemplate(inst.data[$s.$index]), tpl = cache[template.name];
                el[0].classList.add(tpl.cls);
            }
        }));
        inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.COLLAPSE_ALL_EXPANDED_ROWS, function(event, silent) {
            closeAll(null, silent);
        }));
        if (exports.datagrid.events.ON_BEFORE_TOGGLE_SORT) {
            inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.ON_BEFORE_TOGGLE_SORT, function(event) {
                closeAll();
            }));
        }
        inst.unwatchers.push(inst.scope.$on(exports.datagrid.events.ON_BEFORE_DATA_CHANGE, function(event) {
            closeAll(null, true);
        }));
        inst.expandRows = result;
        return inst;
    } ];
});
}(this.ux = this.ux || {}, function() {return this;}()));