ux-datagrid-iosScroll.js

uxDatagrid v.0.4.0-alpha (c) 2014, WebUX https://github.com/webux/ux-angularjs-datagrid License: MIT.

(function(exports, global){

exports.datagrid.events.BEFORE_VIRTUAL_SCROLL_START = "virtualScroll:beforeScrollStart";

exports.datagrid.events.VIRTUAL_SCROLL_TOP = "virtualScroll:top";

exports.datagrid.events.VIRTUAL_SCROLL_BOTTOM = "virtualScroll:bottom";

exports.datagrid.events.ON_VIRTUAL_SCROLL_UPDATE = "virtualScroll:onUpdate";


For simulating the scroll in IOS that doesn't have a smooth scroll natively using transform3d.

exports.datagrid.VirtualScroll = function VirtualScroll(scope, element, vals, updateValuesCallback, renderCallback) {
    var friction = .9, stopThreshold = .01, moved = false, transitionDuration = 100, result = exports.logWrapper("VirtualScroll", {}, "redOrange", function() {
        scope.$emit.apply(scope, arguments);
    }), _x = 0, _y = 0, max, top = 0, bottom = 0, enabled = true, doubleTapTimer, offTouchEnd, touchStart = "touchstart", touchEnd = "touchend", touchMove = "touchmove", touchCancel = "touchCancel", values = angular.extend({
        scrollingStopIntv: null,
        scroll: 0,
        speed: 0,
        absSpeed: 0,
        touchDown: false
    }, vals);
    result.scope = scope;
    result.element = element;
    result.content = element.children();
    function setup() {
        element.css({
            overflow: "hidden"
        });
        if (scope.datagrid && element === scope.datagrid.element) {
            scope.$on(exports.datagrid.events.ON_TOUCH_DOWN, onTouchStartNg);
        } else {
            result.content.bind(touchStart, onTouchStart);
        }
        result.content.css({
            transitionTimingFunction: "ease-out"
        });
    }
    function wait(method, time) {
        var args = exports.util.array.toArray(arguments), intv;
        args.splice(0, 2);
        if (result.async) {
            intv = setTimeout(function() {
                method.apply(null, args);
            }, time);
        } else {
            method.apply(this, args);
        }
        return intv;
    }
    function clearIntv() {
        clearTimeout(values.scrollingStopIntv);
        values.scrollingStopIntv = 0;
    }
    function stopEvent(e) {
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
    }
    function onTouchStartNg(evtType, event) {
        onTouchStart(event);
    }
    function onTouchStart(e) {
        if (!enabled) {
            return;
        }
        result.log("onTouchStart");
        clearIntv();
        updateValuesCallback(result.getValues());
        var touches = e.touches || e.originalEvent.touches;
        result.dispatch(exports.datagrid.events.BEFORE_VIRTUAL_SCROLL_START);
        if (touches[0] && touches[0].target && touches[0].target.tagName.match(/input|textarea|select/i)) {
            return;
        }
        max = element[0].scrollHeight;
        var touch = touches[0];
        _x = touch.pageX;
        _y = touch.pageY;
        values.touchDown = true;
        moved = false;
        stopEvent(e);
        stopCreep();
        updateBottom();
        addTouchEnd();
    }
    function waitToStop() {
        clearIntv();
        values.scrollingStopIntv = wait(stop, transitionDuration);
    }
    function stop() {
        clearIntv();
        result.onScrollingStop();
    }
    function stopCreep() {
        if (scope.datagrid) {
            scope.datagrid.creepRenderModel.stop();
        }
    }
    function updateBottom() {
        bottom = getHeight(result.content) - element[0].offsetHeight;
    }
    function getHeight(elms) {
        var totals = {
            height: 0
        };
        ux.each(elms, onGetHeight, totals);
        return totals.height;
    }
    function onGetHeight(item, index, list, params) {
        params.height += item.offsetHeight;
    }
    result.enable = function enable(value, speed) {
        if (value !== undefined) {
            enabled = !!value;
            if (!enabled) {
                removeTouchEnd();
            } else if (enabled) {
                updateBottom();
                values.touchDown = false;
                _y = element[0].offsetTop + element[0].offsetHeight * .5;
                if (speed) {
                    values.speed = speed;
                    values.absSpeed = Math.abs(speed);
                }
                clearIntv();
                values.scrollingStopIntv = wait(applyFriction);
            }
        }
        return enabled;
    };
    function addTouchEnd() {
        result.log("addTouchEnd");
        ux.each(result.content, function(el) {
            el.addEventListener(touchMove, onTouchMove, true);
            if (scope.datagrid && element === scope.datagrid.element) {
                result.log("  listen for ON_TOUCH_UP");
                offTouchEnd = scope.$on(exports.datagrid.events.ON_TOUCH_UP, onTouchEndNg);
            } else {
                result.log("  listen for touchend");
                el.addEventListener(touchEnd, onTouchEnd, true);
                el.addEventListener(touchCancel, onTouchEnd, true);
            }
        });
    }
    function removeTouchEnd() {
        result.log("removeTouchEnd");
        ux.each(result.content, function(el) {
            el.removeEventListener(touchMove, onTouchMove, true);
            if (offTouchEnd) {
                result.log("  remove ON_TOUCH_UP");
                offTouchEnd();
                offTouchEnd = null;
            } else {
                result.log("  remove touchend");
                el.removeEventListener(touchEnd, onTouchEnd, true);
                el.removeEventListener(touchCancel, onTouchEnd, true);
            }
        });
    }
    function onTouchEndNg(evtStr, event) {
        onTouchEnd(event);
    }
    function onTouchEnd(event) {
        result.log("onTouchEnd");
        if (enabled) {
            values.touchDown = false;
            stopEvent(event);
            if (!moved) {
                fireClick(event);
            }
            removeTouchEnd();
            applyFriction();
        }
    }
    function onTouchMove(event) {
        if (enabled) {
            moved = true;
            stopEvent(event);
            var touch = event.changedTouches[0];
            updateScroll(touch.pageX, touch.pageY);
        }
    }
    function updateScrollValues(x, y) {
        var deltaX = _x + x, deltaY = _y - y;
        if (values.scroll + deltaY <= 0) {
            if (values.scroll) {
                result.dispatch(exports.datagrid.events.VIRTUAL_SCROLL_TOP, result, deltaY);
            }
            values.speed = -values.scroll;
            values.absSpeed = Math.abs(values.speed);
            values.scroll = deltaY = 0;
        } else if (values.scroll + deltaY >= bottom) {
            if (values.scroll !== bottom) {
                result.dispatch(exports.datagrid.events.VIRTUAL_SCROLL_BOTTOM, result, deltaY);
            }
            values.scroll = bottom;
            values.speed = 0;
            values.absSpeed = 0;
        } else {
            values.speed = deltaY;
            values.scroll += deltaY;

result.cap(values.scroll + deltaY);

            values.absSpeed = Math.abs(deltaY);
        }
        _y = y;
        _x = x;
    }
    function updateScroll(x, y) {
        updateScrollValues(x, y);
        if (!values.touchDown) {
            clearIntv();
            values.scrollingStopIntv = wait(applyFriction);
        }
        render();
    }
    function fireClick(e) {
        result.log("fireClick");
        var point = e.changedTouches ? e.changedTouches[0] : e, target, ev;
        clearTimeout(doubleTapTimer);

TODO: doubleTapTimer needs to be configurable to make clicks fire faster.

        doubleTapTimer = wait(function() {
            doubleTapTimer = null;

Find the last touched element

            target = point.target;
            while (target.nodeType != 1) target = target.parentNode;
            if (target.tagName != "SELECT" && target.tagName != "INPUT" && target.tagName != "TEXTAREA") {
                ev = document.createEvent("MouseEvents");
                ev.initMouseEvent("click", true, true, e.view, 1, point.screenX, point.screenY, point.clientX, point.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, 0, null);
                ev._fake = true;
                target.dispatchEvent(ev);
            }
        }, 1);
    }
    result.dispatch = function dispatch() {
        scope.$emit.apply(scope, arguments);
    };
    result.cap = function cap(value) {
        var v = Math.abs(value);
        v = v > bottom ? bottom : v < top ? top : v;
        return value < 0 ? -v : v;
    };
    function applyFriction() {
        if (values.absSpeed >= stopThreshold) {
            var startScroll = values.scroll, dist, transDuration;
            while (values.absSpeed >= stopThreshold) {
                updateScrollValues(_x, _y - values.speed * friction);
            }
            dist = Math.abs(values.scroll - startScroll);
            transDuration = dist > transitionDuration ? dist : transitionDuration;
            render(transDuration);
            values.scrollingStopIntv = wait(applyFriction, transDuration);
            _startX = _endX = _startY = _endY = _startTime = _endTime = 0;
        } else {
            waitToStop();
        }
    }
    function render(tranDuration) {
        var value = element[0].scrollTop - values.scroll;
        result.content[0].style.transitionDuration = tranDuration ? tranDuration + "ms" : 0;
        result.content[0].style.webkitTransform = "translate3d(0px, " + value + "px, 0px)";
        if (!tranDuration) {
            onVirtualScrollUpdate();
        } else {
            setTimeout(onVirtualScrollUpdate, tranDuration);
        }
    }
    function onVirtualScrollUpdate() {
        result.dispatch(exports.datagrid.events.ON_VIRTUAL_SCROLL_UPDATE);
    }
    result.getScroll = function getScroll() {
        return values.scroll;
    };
    result.setScroll = function setScroll(value) {
        values.scroll = value;
        if (scope.datagrid) {
            scope.datagrid.values.scroll = value;
        }
    };
    result.scrollToBottom = function(immediately) {
        updateBottom();
        result.scrollTo(bottom, immediately);
    };
    result.getValues = function getValues() {
        return values;
    };
    result.clear = function clearRender() {
        result.content.css({
            webkitTransform: ""
        });
    };
    result.async = true;

Scroll to the numeric value.

Params
value
immediately Boolean=
    result.scrollTo = function scrollTo(value, immediately) {
        render();
        renderCallback(value, immediately);
    };
    result.scrollIntoView = scope.datagrid ? scope.datagrid.scrollModel.scrollIntoView : function() {};

When it stops render.

    result.onScrollingStop = function onScrollingStop() {
        result.scrollTo(values.scroll, true);
    };
    result.destroy = function() {
        removeTouchEnd();
        element.unbind(touchStart, onTouchStart);
        result.destroyLogger();
        result = null;
        values = null;
    };
    result.setup = setup;
    return result;
};

/*global ux */

we want to override the default scrolling if it is an IOS device.

angular.module("ux").factory("iosScroll", function() {
    return function iosScroll(inst) {

do not let escape if in unit tests. exp.flow.async is false in unit tests.

        var vScroll, originalScrollModel = inst.scrollModel;
        if (!exports.datagrid.isIOS) {
            return inst;
        }
        vScroll = new ux.datagrid.VirtualScroll(inst.scope, inst.element, inst.values, function updateValues(values) {
            inst.values.scroll = values.scroll;
            inst.values.speed = values.speed;
            inst.values.absSpeed = values.absSpeed;
        }, function render(value, immediately) {
            inst.values.scroll = value;
            if (immediately) {
                originalScrollModel.onScrollingStop();
            } else {
                originalScrollModel.waitForStop();
            }
        });
        inst.scope.$on(ux.datagrid.events.ON_READY, function() {
            vScroll.content = inst.getContent();
            vScroll.setup();
            originalScrollModel.removeScrollListener();
        });
        vScroll.scrollToIndex = originalScrollModel.scrollToIndex;
        vScroll.scrollToItem = originalScrollModel.scrollToItem;
        inst.scrollModel = vScroll;
        return inst;
    };
});
}(this.ux = this.ux || {}, function() {return this;}()));