Source: ui/PullToRefresh/P2RComponent.js

// Copyright 2014 Startup Kitchen. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.provide('tart.ui.P2RComponent');
goog.require('goog.async.Throttle');
goog.require('tart.ui.P2RComponentModel');
goog.require('tart.ui.DlgComponent');



/**
 * P2RComponent is a small component which checks the scroll position of a given DOM element, and if it's in
 * an appropriate position, fires a REFRESH event for the parent component to act upon. When the parent component is
 * done with refreshing, it should reset this P2RComponent with the reset() method.
 *
 * @constructor
 * @extends {tart.ui.DlgComponent}
 *
 * @param {Element=} opt_el Optional element to track its scroll.
 */
tart.ui.P2RComponent = function(opt_el) {
    this.model = new tart.ui.P2RComponentModel();
    this.model.setParentEventTarget(this);
    goog.base(this);

    this.EventType = this.model.EventType;

    if (opt_el) this.register(opt_el);
};
goog.inherits(tart.ui.P2RComponent, tart.ui.DlgComponent);


/**
 * Threshold value for the release action. Releases after this threshold will trigger a refresh.
 *
 * @type {number}
 */
tart.ui.P2RComponent.prototype.threshold = 135;


/**
 * Height of this component. This setting is used to offset the scroll view while refreshing.
 *
 * @type {number}
 */
tart.ui.P2RComponent.prototype.height = 96;


/**
 * Start position of the arrow. This is adjusted for a spring-like effect.
 *
 * @type {number}
 */
tart.ui.P2RComponent.prototype.arrowOffset = -40;


/**
 * @override
 */
tart.ui.P2RComponent.prototype.bindModelEvents = function() {
    goog.events.listen(this.model, this.model.EventType.SHOULD_REFRESH, this.onShouldRefresh,
        false, this);
};


/**
 * Triggered when the components decides a refresh action. This method should be overridden
 * to, for example, display a spinner animation during refresh.
 */
tart.ui.P2RComponent.prototype.onShouldRefresh = function() {
    this.el.style.webkitTransform = 'translateY(' + this.height + 'px)';
    this.el.style.webkitTransition = '300ms ease-out';

    this.getChild(this.mappings.SPINNER)[0].style.visibility = 'visible';
    this.getChild(this.mappings.ARROW)[0].style.visibility = 'hidden';
};


/**
 * @override
 */
tart.ui.P2RComponent.prototype.render = function(opt_base, opt_index) {
    goog.base(this, 'render', opt_base, opt_index);

    this.rendered = true;

    if (!this.el) this.register(this.getElement().parentNode);
};


/**
 * Resets the component state to default. This should be used to signal the end of refreshing so that this component
 * can again check for pull to refresh.
 */
tart.ui.P2RComponent.prototype.reset = function() {
    if (this.el) this.el.style.webkitTransform = 'translateY(0)';

    var spinner = this.getChild(this.mappings.SPINNER);
    var arrow = this.getChild(this.mappings.ARROW);

    if (spinner) spinner[0].style.visibility = 'hidden';

    setTimeout(function() {
        if (arrow) arrow[0].style.visibility = 'visible';
    }, 500);

    this.model.reset();
};


/**
 * Registers an element to track its scroll. This can be used for lazily introducing an element to track.
 *
 * @param {Node} el Element to track.
 */
tart.ui.P2RComponent.prototype.register = function(el) {
    this.reset();

    this.el = el;

    goog.events.unlistenByKey(this.scrollListener);
    goog.events.unlistenByKey(this.releaseListener);

    this.scrollListener = goog.events.listen(el, goog.events.EventType.SCROLL, this.onScroll, false, this);
    this.releaseListener = goog.events.listen(document.body, goog.events.EventType.TOUCHEND,
        this.onRelease_, false, this);
};


/**
 * Scroll event handler for pull to refresh.
 *
 * @param {goog.events.BrowserEvent} e Scroll event object.
 */
tart.ui.P2RComponent.prototype.onScroll = function(e) {
    this.checkShouldRefresh(e);

    var rot = 0,
        scroll = -e.target.scrollTop,
        pos = this.arrowOffset + Math.pow(scroll, 0.75),
        rotationThreshold = this.threshold - 60;

    if (scroll >= rotationThreshold)
        rot = Math.min(180, (scroll - rotationThreshold) * 3);

    this.getChild(this.mappings.ARROW)[0].style.webkitTransform = 'translateY(' + pos + 'px) rotate(' + rot + 'deg)';
};


/**
 * Fires when the user lifts her finger to finish the scroll.
 * If the user scrolled enough, inform the model to refresh
 *
 * @private
 */
tart.ui.P2RComponent.prototype.onRelease_ = function() {
    if (this.el.scrollTop < -this.threshold)
        this.model.refresh();
};


/**
 * If in an appropriate state, checks if the scroll position is right and if so triggers a refresh event.
 *
 * @param {goog.events.BrowserEvent} e Scroll event object.
 */
tart.ui.P2RComponent.prototype.checkShouldRefresh = function(e) {
    this.model.triggerShouldCheckState();
};


/**
 * @override
 */
tart.ui.P2RComponent.prototype.templates_base = function() {
    return '<div id="' + this.getId() + '" class="pull-to-refresh">' +
            '<div class="pull-to-refresh-arrow"></div>' +
            '<div class="pull-to-refresh-spinner"></div>' +
        '</div>';
};


/**
 * @override
 */
tart.ui.P2RComponent.prototype.disposeInternal = function() {
    this.model.dispose();
    goog.events.unlistenByKey(this.scrollListener);

    goog.dom.removeNode(this.getElement());
    goog.base(this, 'disposeInternal');
};


/**
 * @enum {string}
 */
tart.ui.P2RComponent.prototype.mappings = {
    ARROW: '.pull-to-refresh-arrow',
    SPINNER: '.pull-to-refresh-spinner'
};