Giter VIP home page Giter VIP logo

material-refresh's Introduction

Material Refresh

High Performance

Mobile only

Google Material Design swipe (pull) to refresh.

It uses CSS3 and JavaScript depend on Zepto or jQuery.

Actually, it's easy to convert the dependent js library or just use the vanilla JavaScript.

It's high performancenot which not impact the structure of sites.

Types and preview

Type1: Above surface (default)

Type2: Below surface

Type3: Button action

Demo

Getting Started

Install it

Include material-refresh.min.js and material-refresh.min.css in your target html file.

<link rel='stylesheet' href='material-refresh.min.css'/>

<script src='material-refresh.min.js'></script>

Cause it is a plugin for Zepto or jQuery, so we also need to include Zepto or jQuery:

<script src='zepto.js'></script>

<!-- or include jQuery.js-->
<script src='jQuery.js'></script>

Usually, we will combine and compress all the css or js, depend on your needs.

You can also install it via Bower or npm:

bower install --save material-refresh
npm install --save material-refresh

Basic usage

Example for Type1: Above surface (default):

1.Instantiation:

mRefresh();

2.Finish the refresh and hide it:

mRefresh.resolve();

If you don't use this method, refesher will stop after the maxTime(Default: 6000ms).

Relations of three types

  • Type1 and Type2 can not use in the same time.
  • Type3 is depend on Type1 or Type2, cause it will determine the refresher position
  • Type3 and (Type1 or Type2) can use in the same time.

Advanced usage

Options

    // Default options 
     var opts = { 
         nav: '', //String, using for Type2 
         scrollEl: '', //String  
         onBegin: null, //Function 
         onEnd: null, //Function 
         top: '0px', //String 
         theme: 'mui-blue-theme', //String 
         index: 10001, //Number
         maxTime: 6000, //Number 
         freeze: false //Boolen 
     } 
    mRefresh(opts);
nav:

-- Using for turn into Type2, refresh body will below the nav surface

// Example
var opts = {
  nav: '#navMain'
}
scrollEl:

-- Custom scroll wrapper element, decide which elemnt will allow trigger refresh action.

-- Default:{ ios:document.body, android: document }

var opts = {
  scrollEl: '#mainWrapper'
}
onBegin: (Callback Function)

-- Trigger when the refresh body start to rotate because of the right gesture(swipe).

-- You can use this callback to pull ajax data or other action.

var opts = {
  onBegin: function(){
    alert('Begin to rotate');
    $.get('/whatevs.html', function(response){
        $('#someDom').append(response);
    });
  }
}
onEnd: (Callback Function)

-- Trigger when finished the refresh

-- Using like onBegin

top:

-- Set top of the refresher.

-- You can change its position finally by setting this option.

-- Default{ Type1 :'0px', Type2: depend on the height and top of the nav element }

var opts = {
  top: '50px'
}
theme: (Default: 'mui-blue-theme')

-- Set color or custom style of the refresher.

-- You can write your own style in css file by using the className like 'mui-somecolor-theme'

var opts = {
  theme: 'mui-red-theme'
}
index:

-- Set z-index of the refresher to change it in z-space.

-- Default { Type1: 10001, Type2: (the z-index of nav element) - 1}

var opts = {
  index: 99
}
maxTime: (Default: 6000ms)

-- Refresher will stop after the maxTime if you don't use mRefresh.resolve() to stop it.

-- You can change this maxTime to make it longer or shorter.

var opts = {
  maxTime: 2000
}
freeze: (Default: false)

-- The touch event of the refresher will not trigger if freeze is true.

-- You can use this option to prevent Type1 or Type2 and just allow Type3: button action

var opts = {
  freeze: true
}

Type1: Above surface

You can custom your own refresher like:

var opts = {
    maxTime: 3000,
    onBegin: function(){
      $.get('/whatevs.html', function(response){
        $('#someDom').append(response);
      });
    },
    onEnd: function(){
      alert('Finish the refresh');
    }
}

mRefresh(opts);

Type2: Below surface

Use Type2 by setting the option nav to the top of the elements:

// example
var opts = {
    nav: '#navMain',
    onBegin: function(){
      $.get('/whatevs.html', function(response){
        $('#someDom').append(response);
      });
    }
}

mRefresh(opts);

Then the refresher will below the surface of the navMain element.

Type3: Button action

If you had inited the refresher,you can bind the DOM event by using:

$('#buttonAction').on('tap', function(){
  mRefresh.refresh();
});

When you click the buttonAction element, the refresher will show.

If you want to get some callback when start or stop to refresh,by using onBegin or onEnd:

$('#buttonAction').on('tap', function(){
  var refreshOpts = {
    onBegin: function(){
      // Do something
      $.get('/whatevs.html', function(response){
        $('#someDom').append(response);
      });
    },
    onEnd: function(){
      alert('Finish!')
    }
  }
  mRefresh.refresh(refreshOpts);
});

If you want not to trigger Type1 or Type2, and just need Type3.

var opts = {
  freeze: true
}
mRefresh(opts);

$('#buttonAction').on('tap', function(){
  mRefresh.refresh();
});

Or

mRefresh();
mRefresh.unbindEvents();

$('#buttonAction').on('tap', function(){
  mRefresh.refresh();
});

Browser support

Android 3 +

iOS 5 +

Resource

Google Material Design

License

MIT © Gctang

material-refresh's People

Contributors

lightningtgc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

material-refresh's Issues

Angular / Angular Material

Hi, is it possible to get this working with angular / angular material? I've been trying to fiddle with it but not winning

Unexpected behavior when pulling, fixed code inside

The way the pull down worked was a bit too particular. I relaxed the requirements a bit and I also changed the movement animation to use transform: translate instead of .animate. It should feel more fluid to the user.

I modified it quickly for my app, don't have too much time to clean it up, you may find some junk variables and console.logs in there. I mainly edited, touchStart, touchMove, touchEnd, and moveCircle.

/**
 * Google Material Design Swipe To Refresh.   
 * By Gctang(https://github.com/lightningtgc)
 *
 * Three types of refresh:
 * 1. Above or coplanar with another surface
 * 2. Below another surface in z-space. 
 * 3. Button action refresh
 *
 */

;(function($){
    var $scrollEl = $(document.body);
    var $refreshMain, $spinnerWrapper, $arrowWrapper, $arrowMain;
    var scrollEl = document.body;

    var noShowClass = 'mui-refresh-noshow';
    var mainAnimatClass = 'mui-refresh-main-animat';
    var blueThemeClass = 'mui-blue-theme';

    var isShowLoading = false;
    var isStoping = false;
    var isBtnAction = false;

    var NUM_POS_START_Y = -85;
    var NUM_POS_TARGET_Y = 0; // Where to stop
    var NUM_POS_MAX_Y = 120;   // Max position for the moving distance
    var NUM_POS_MIN_Y = -25;  // Min position for the moving distance
    var NUM_NAV_TARGET_ADDY = 20; // For custom nav bar

    var touchCurrentY;
    var touchStartY = 0;
    var customNavTop = 0;
    var verticalThreshold = 15;
    var maxRotateTime = 6000; //Max time to stop rotate
    var basePosY = 60;

    var onBegin = null;
    var onBtnBegin= null;
    var onEnd = null;
    var onBtnEnd = null;
    var stopAnimatTimeout = null;

    var refreshNav = '';

    var lastTime = new Date().getTime();

    var isIOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);

    var tmpl = '<div id="muiRefresh" class="mui-refresh-main">\
        <div class="mui-refresh-wrapper ">\
            <div class="mui-arrow-wrapper">\
                <div class="mui-arrow-main"></div>\
            </div>\
            <div class="mui-spinner-wrapper" style="display:none;">\
                <div class="mui-spinner-main" >\
                    <div class="mui-spinner-left">\
                        <div class="mui-half-circle"></div>\
                    </div>\
                    <div class="mui-spinner-right">\
                        <div class="mui-half-circle"></div>\
                    </div>\
                </div>\
            </div>\
        </div>\
    </div>';

    // Defined the object to improve performance
    var touchPos = {
        top: 0,
        x1: 0,
        x2: 0,
        y1: 0,
        y2: 0
    }

    // Default options 
    /* var opts = { */
    /*     scrollEl: '', //String  */
    /*     nav: '', //String */
    /*     top: '0px', //String */
    /*     theme: '', //String */
    /*     index: 10001, //Number*/
    /*     maxTime: 3000, //Number */
    /*     freeze: false, //Boolen */
    /*     onBegin: null, //Function */
    /*     onEnd: null //Function */
    /* } */


    /* Known issue: 
     * 1. iOS feature when scrolling ,animation will stop  
     * 2. Animation display issue in anfroid like miui小米
     *
     *
     * TODO list:
     * 1. Using translate and scale together to replace top 
     * 2. Optimize circle rotate animation
     */

    // Main function to init the refresh style
    function mRefresh(options) {
        options = options || {};

        scrollEl = options.scrollEl ? options.scrollEl :
                        isIOS ? scrollEl : document;
        $scrollEl = $(scrollEl);

        // extend options
        onBegin = options.onBegin;
        onEnd = options.onEnd;
        maxRotateTime = options.maxTime || maxRotateTime;
        refreshNav = options.nav || refreshNav;

        if ($('#muirefresh').length === 0) {
            renderTmpl();
        }

        $refreshMain = $('#muiRefresh');
        $spinnerWrapper = $('.mui-spinner-wrapper', $refreshMain);
        $arrowWrapper = $('.mui-arrow-wrapper', $refreshMain);
        $arrowMain = $('.mui-arrow-main', $refreshMain);

        // Custom nav bar 
        if (!isDefaultType()) {
            $refreshMain.addClass('mui-refresh-nav');
            basePosY = $(refreshNav).height() + 20;
            if($(refreshNav).offset()){
                customNavTop = $(refreshNav).offset().top;
                // Handle position fix
                if($(refreshNav).css('position') !== 'fixed'){
                    basePosY += customNavTop;
                }
                // Set the first Y position
                $refreshMain.css('-webkit-transform', 'translate3d(0px,'+customNavTop+'px,0px)');
               // $refreshMain.css('top', customNavTop + 'px');
            }

            //Set z-index to make sure ablow the nav bar
            var navIndex = $(refreshNav).css('z-index');
            $refreshMain.css('z-index', navIndex - 1);
        }

        //Set custom z-index
        if(options.index){
            $refreshMain.css('z-index', ~~options.index);
        }

        //Set custom top, to change the position
        if(options.top){
            $refreshMain.css('-webkit-transform', 'translate3d(0px,'+options.top+'px,0px)');
            //$refreshMain.css('top', options.top);
        }

        // Extract theme 
        if (options.theme) {
            $refreshMain.addClass(options.theme);
        } else {
            $refreshMain.addClass(blueThemeClass);
        }

        // Add Animation Class
        $refreshMain.addClass(mainAnimatClass);

        if(!options.freeze){
            bindEvents();
        }
    }

    // Public Methods

    // Finish loading
    mRefresh.resolve = function() {
        if(!isStoping && stopAnimatTimeout){
            clearTimeout(stopAnimatTimeout);
            stopAnimatTimeout = null;

            recoverRefresh();
        }
    }

    // Destory refresh
    mRefresh.destroy = function(){
        unbindEvents();
        $refreshMain.remove();

    }

    // Type3: Button action refresh
    mRefresh.refresh = function(opt) {
        // Do rotate
        if(!isShowLoading){
            var realTargetPos = basePosY + NUM_POS_TARGET_Y - 20;
            isShowLoading = true;
            isBtnAction = true;

            opt = opt || {};
            onBtnBegin = opt.onBegin;
            onBtnEnd = opt.onEnd;

            if (!isDefaultType()) {
                realTargetPos = realTargetPos + NUM_NAV_TARGET_ADDY;
            }

            // Handle freeze
            $refreshMain.show();
            //Romove animat time
            $refreshMain.removeClass(mainAnimatClass);
            // move to target position
            $refreshMain.css('-webkit-transform', 'translate3d(0px,'+realTargetPos+'px,0px)');
           // $refreshMain.css('top', realTargetPos + 'px');
            // make it small
            $refreshMain.css('-webkit-transform', 'scale(' + 0.01  + ')');

            setTimeout(doRotate, 60);
        }
    }

    // Unbind touch events,for freeze type1 and type2
    mRefresh.unbindEvents = function(){
        unbindEvents();
    }

    mRefresh.bindEvents = function(){
        bindEvents();
    }

    // Render html template
    function renderTmpl(){
        document.body.insertAdjacentHTML('beforeend', tmpl);
    }


    function touchStart(e){
        if(isIOS && scrollEl == document.body){
            touchPos.top = window.scrollY;
        }else if(scrollEl != document){
           scollPos = $(scrollEl).scrollTop();
        } else {
            touchPos.top = (document.documentElement || document.body.parentNode || document.body).scrollTop;
        }

        if (touchPos.top > 0 || isShowLoading) {
            return;
        }

        //touchCurrentY = basePosY + NUM_POS_START_Y;
        hasStarted = false;

        // Fix jQuery touch event detect
        e = e.originalEvent || e;

        if (e.touches[0]) {
            //touchPos.x1 = e.touches[0].pageX;
            //touchStartY = touchPos.y1 = e.touches[0].pageY;
            //touchCurrentY = e.touches[0].pageY;
            //previousY = currentY;
        }
    }

    var hasStarted = false;
    var startY = 0;
    var previousY = 0;
    var currentY = 0;
    var movePct = .25;
    var scollPos = 0;
    var distanceY = 0;

    function touchMove(e){
        var thisTouch;//, distanceY;
        var now = new Date().getTime();

        e = e.originalEvent || e;

        scrollPos = $(scrollEl).scrollTop();
        var maxHeight = $(scrollEl).height();

        if ( isShowLoading || !e.touches || e.touches.length !== 1) {
            // Just allow one finger
            return;
        }

        thisTouch = e.touches[0];

        touchCurrentY = thisTouch.pageY;
        distanceY = (touchCurrentY - touchStartY);

        console.log("Start="+touchStartY+", Current="+touchCurrentY+", Dist="+distanceY);
        if( !hasStarted && scrollPos < (verticalThreshold/2) ) {
            //if( distanceY > verticalThreshold ) {
            if( !hasStarted ) {
                hasStarted = true;  
                touchStartY = touchCurrentY;
            }
            touchStartY = touchCurrentY;
            $refreshMain.show();
            distanceY = 0;
            console.log("HIT THRESHOLD");
            //}
        }

        if( hasStarted && distanceY > verticalThreshold) {
            e.preventDefault();  

            // Some android phone
            // Throttle, aviod jitter 
            if (now - lastTime < 90) {
                return;
            }

            console.log("MOVING CIRCLE, DISTANCE = " + distanceY);
            moveCircle(distanceY);
        }


        /*
        touchPos.x2 = thisTouch.pageX;
        touchPos.y2 = thisTouch.pageY;

        // Distance for pageY change
        distanceY = touchPos.y2 - touchPos.y1;

        if (touchPos.y2 - touchStartY + verticalThreshold > 0) {
            e.preventDefault();  

            // Some android phone
            // Throttle, aviod jitter 
            if (now - lastTime < 90) {
                return;
            }

            if (touchCurrentY < basePosY - customNavTop + NUM_POS_MAX_Y) {
                touchCurrentY += distanceY ;
                moveCircle(touchCurrentY);
            } else {
                // Move over the max position will do the rotate
                doRotate();
                return;
            }

        }
        */
        // y1 always is the current pageY
        touchPos.y1 = thisTouch.pageY;
        lastTime = now;
    }

    function touchEnd(e){

        hasStarted = false;

        if (scrollPos > 0 || isShowLoading) {
            return;
        }
        e.preventDefault();

        if (distanceY >= (maxDistance - verticalThreshold)) {
            // Should move over the min position
            doRotate();

        } else {
            backToStart();
        }
    }

    /**
     * backToStart
     * Return to start position
     */
    function backToStart() {
        var realStartPos = basePosY + NUM_POS_START_Y;
        if ( isDefaultType() ) {
            $refreshMain.css('-webkit-transform', 'translate3d(0px,'+realStartPos+'px,0px)');
           // $refreshMain.css('top', realStartPos + 'px');
            $refreshMain.css('-webkit-transform', 'scale(' + 0  + ')');
        } else {
            // Distance must greater than NUM_POS_MIN_Y
            $refreshMain.css('-webkit-transform', 'translate3d(0px,'+customNavTop+'px,0px)');
            //$refreshMain.css('top', customNavTop + 'px');
            /* $refreshMain.css('-webkit-transform', 'translateY(' + realStartPos + 'px)'); */
        }
        setTimeout(function(){
            // Handle button action
            if(!isShowLoading){
                $refreshMain.css('opacity', 0);
                $refreshMain.hide();
            }
        }, 300);
    }

    /**
     * moveCircle
     * touchmove change the circle style
     *
     * @param {number} y
     */
    var maxDistance = 110;
    function moveCircle(y){
        if( y > maxDistance ) 
            y = maxDistance;

        var scaleRate = maxDistance/4;
        var scalePer = y / scaleRate > 1 ? 1 : y / scaleRate < 0 ? 0 : y / scaleRate;
        var currMoveY = basePosY + NUM_POS_START_Y + y;

        if (isDefaultType()) {
            // Small to Big
            $refreshMain.css('-webkit-transform', 'scale(' + scalePer  + ')');
        }
        /* $refreshMain.css('-webkit-transform', 'translateY('+ y + 'px)'); */

        $refreshMain.css('opacity', scalePer);
        // Change the position
        $refreshMain.css('-webkit-transform', 'translate3d(0px,'+currMoveY+'px,0px)');//currMoveY + 'px');
        $arrowMain.css('-webkit-transform', 'rotate(' + -(y * 3) + 'deg)');
        /* $arrowMain.css('transform', 'rotate(' + -(y * 3) + 'deg)'); */ 

    }

    /**
     * doRotate
     * Rotate the circle,and you can stop it by `mRefresh.resolve()`
     * or it wil stop within the time: `maxRotateTime`
     */
    function doRotate(){
        isShowLoading = true;
        // Do button action callback
        if (isBtnAction && typeof onBtnBegin === 'function') {
            onBtnBegin();
        } else if (typeof onBegin === 'function') {
            // Do onBegin callback
            onBegin();
        }

        // Make sure display entirely
        $refreshMain.css('opacity', 1);

        if (!isBtnAction) { 
            var realTargetPos = basePosY + NUM_POS_TARGET_Y - 20;
            if (!isDefaultType()) {
                realTargetPos = realTargetPos + NUM_NAV_TARGET_ADDY;
            }
            $refreshMain.css('-webkit-transform', 'translate3d(0px,'+realTargetPos+'px,0px)');
            //$refreshMain.css('top', realTargetPos + 'px');
            /* $refreshMain.css('-webkit-transform', 'translateY(' + realTargetPos + 'px)'); */
        } else {
            $refreshMain.addClass(mainAnimatClass);
            $refreshMain.css('-webkit-transform', 'scale(' + 1  + ')');
        }

        $arrowWrapper.hide();

        // Start animation
        $spinnerWrapper.show();

        // Timeout to stop animation
        stopAnimatTimeout = setTimeout(recoverRefresh, maxRotateTime);
    }

    /**
     * Recover Refresh
     * Hide the circle 
     */
    function recoverRefresh(){
        // For aviod resolve
        isStoping = true;

        // Stop animation 
        $refreshMain.addClass(noShowClass);

        $spinnerWrapper.hide();

        setTimeout(function(){
            $refreshMain.removeClass(noShowClass);
            $refreshMain.hide();

            backToStart();

            $arrowWrapper.show();

            isShowLoading = false;
            isStoping = false;

            if (isBtnAction && typeof onBtnEnd === 'function') {
                onBtnEnd();
            } else if (typeof onEnd === 'function') {
                onEnd();
            }

            isBtnAction = false;

        }, 500);
    }

    /**
     * isDefaultType
     * Check is type1: Above surface
     *
     * @return {Boolen}
     */
    function isDefaultType() {
       return $(refreshNav).length === 0;
    }

    function bindEvents() {
        $scrollEl.on('touchstart', touchStart);
        $scrollEl.on('touchmove', touchMove);
        $scrollEl.on('touchend', touchEnd);
    }

    function unbindEvents() {
        $scrollEl.off('touchstart', touchStart);
        $scrollEl.off('touchmove', touchMove);
        $scrollEl.off('touchend', touchEnd);
    }

    window.mRefresh = mRefresh;

})(window.Zepto || window.jQuery);

touchEnd freezes inputs

Hello,

The line
e.preventDefault();
in touchEnd method prevent inputs from being focused on some phones (not working on some Android phones, while on other there is no problem).

Not sure how to fix that. Just commented for now.
Thanks

click's when on top not working

When page scrolled to top .. clicks not firing , it's going as touch event not a click event and when you scroll 1 px down all click fire normal

not same behavior like google

i like it very much , great work! , but its still buggy und dont trigger at short fast swipes like googles loader does.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.