Custom Events for Scrolling Towards Element Margins Plugin

As per Nate's recommendation, I have rewritten the Custom Events for Scrolling Towards Element Margins YUI module to leverage the plugin system of YUI. This way you can add it to any existing widget, instead of running it as a standalone.

Getting ready

Here is the complete source code (ScrollActionerPlugin.js):

YUI.add('scroll_actioner_plugin', function(Y) {
  var BOUNDING_BOX = 'boundingBox',
  VIEWPORT_REGION = 'viewportRegion',

  Lang = Y.Lang;

  // A plugin class designed to animate Widget's show and hide methods.
  function ScrollActionerPlugin(config) {
    this.host = config.host;
    ScrollActionerPlugin.superclass.constructor.apply(this, arguments);
  }

  // Define Static properties NAME (to identify the class) and NS (to identify the namespace)
  ScrollActionerPlugin.NAME = 'scrollActionerPlugin';
  ScrollActionerPlugin.NS = 'scrap';
  ScrollActionerPlugin.EVT_BOTTOM_SCROLL = 'bottom_scroll';
  ScrollActionerPlugin.EVT_LEFT_SCROLL = 'left_scroll';
  ScrollActionerPlugin.EVT_RIGHT_SCROLL = 'right_scroll';
  ScrollActionerPlugin.EVT_TOP_SCROLL = 'top_scroll';

  // Attribute definitions for the plugin
  ScrollActionerPlugin.ATTRS = {
      activate: {
        validator: Lang.isBoolean,
        value: true
      },
      bottomMargin: {
        validator: Lang.isNumber,
        value: 200
      },
      leftMargin: {
        validator: Lang.isNumber,
        value: 200
      },
      rightMargin: {
        validator: Lang.isNumber,
        value: 200
      },
      topMargin: {
        validator: Lang.isNumber,
        value: 200
      }
  };

  // Extend Plugin.Base
  Y.extend(ScrollActionerPlugin, Y.Plugin.Base, {

    _lastScrollOffset: null,
    _lastScrollRegion: null,
    _scrollHandle: null,
    
    activate: function() {
      var that = this;

      if (that.host.get('rendered')) {
        if (! that._scrollHandle) {
          that.host.bindUI();
        }
      }
      else {
        that.host.render();
      }
    },
    _handleScroll: function() {
      var that = this,
      elBb = that.host.get(BOUNDING_BOX),
      aCurrScrollOffset = that.getScrollOffset(),
      aLastScrollOffset = that._lastScrollOffset,
      oRegion = elBb.get('region'),
      oVieportRegion = elBb.get(VIEWPORT_REGION),

      isScrollingDown = aLastScrollOffset[1] < aCurrScrollOffset[1],
      isScrollingLeft = aLastScrollOffset[0] > aCurrScrollOffset[0],
      isScrollingRight = aLastScrollOffset[0] < aCurrScrollOffset[0],
      isScrollingUp = aLastScrollOffset[1] > aCurrScrollOffset[1],

      iBottomScrollTrigger = oRegion.bottom - that.get('bottomMargin'),
      iLeftScrollTrigger = oRegion.left + that.get('leftMargin'),
      iRightScrollTrigger = oRegion.right - that.get('rightMargin'),
      iTopScrollTrigger = oRegion.top + that.get('topMargin');

      Y.log('isScrolling (Down|Left|Right|Up)=(' + isScrollingDown + '|' + isScrollingLeft + '|' + isScrollingRight + '|' + isScrollingUp + ')');
      Y.log('scrollTrigger (Down|Left|Right|Up)=(' + iBottomScrollTrigger + '|' + iLeftScrollTrigger + '|' + iRightScrollTrigger + '|' + iTopScrollTrigger + ')');

      if (isScrollingDown && iBottomScrollTrigger < oVieportRegion.bottom) {
        Y.log('Firing ScrollActionerPlugin bottom');
        that.fire(ScrollActionerPlugin.EVT_BOTTOM_SCROLL, oRegion, oVieportRegion);
      }

      if (isScrollingLeft && iLeftScrollTrigger > oVieportRegion.left) {
        Y.log('Firing ScrollActionerPlugin left');
        that.fire(ScrollActionerPlugin.EVT_LEFT_SCROLL, oRegion, oVieportRegion);
      }

      if (isScrollingRight && iRightScrollTrigger < oVieportRegion.right) {
        Y.log('Firing ScrollActionerPlugin right');
        that.fire(ScrollActionerPlugin.EVT_RIGHT_SCROLL, oRegion, oVieportRegion);
      }

      if (isScrollingUp && iTopScrollTrigger > oVieportRegion.top) {
        Y.log('Firing ScrollActionerPlugin top');
        that.fire(ScrollActionerPlugin.EVT_TOP_SCROLL, oRegion, oVieportRegion);
      }

      that._lastScrollOffset = aCurrScrollOffset;
    },
    deactivate: function() {
      var that = this;

      if (that._scrollHandle) {
        that._scrollHandle.detach();
        that._scrollHandle = null;
      }
    },
    bindUI: function() {
      var that = this,
      elDoc = that.host.get(BOUNDING_BOX).get('ownerDocument');

      that._scrollHandle = elDoc.on('scroll', Y.bind(that._handleScroll, that));
    },
    destructor: function() {
      this.deactivate();
    },
    getScrollOffset: function() {
      var elBb = this.host.get(BOUNDING_BOX);
      return [elBb.get('docScrollX'), elBb.get('docScrollY')];
    },
    initializer: function() {
      var that = this,
      elBb = that.host.get(BOUNDING_BOX);

      that._scrollHandle = null;
      that._lastScrollOffset = that.getScrollOffset();
      that._lastScrollRegion = elBb.get(VIEWPORT_REGION);

      that.afterHostMethod('bindUI', that.bindUI);

      if (that.get('activate')) {
        that.activate();
      }
    }

  });

  Y.ScrollActionerPlugin = ScrollActionerPlugin;
}, '', {requires: ['base','widget', 'node', 'plugin']});

How to do it…

Here is a sample code that listens for the bottom margin:

YUI({
	modules: {
		'scroll_actioner_plugin': {
			fullpath: '/assets/js/yahoo-3-ext/ScrollActionerPlugin.js',
			requires: ['base','widget', 'node', 'plugin']
		}
	}
}).use('event', 'dom', 'node', 'widget', 'scroll_actioner_plugin', function(Y) {
	Y.on('load', function() {
		var elDoc = Y.one('#doc'),
		oScrollActioner = new Y.Widget({boundingBox: elDoc});

		oScrollActioner.plug(Y.ScrollActionerPlugin, {});

		oScrollActioner.scrap.on(Y.ScrollActionerPlugin.EVT_BOTTOM_SCROLL, function() {
			// turn off until DOM response is complete (usually AJAX request or loading new elements)
			oScrollActioner.scrap.deactivate();

			// slight delay for effect
			setTimeout(function() {
				var value = elDoc.get('region').height;
				elDoc.setStyle('height', value + 100);

				// turn on when DOM response is complete
				oScrollActioner.scrap.activate();
			}, 1000);
		});
	});
});

How it works…

Under the hood, this code works exactly the same as the standalone version. The key difference is how developers work with the plugin. First you create a Widget instance. Then you plugin the ScrollActionerPlugin into that Widget instance. This will attach the scrap property directly on the Widget instance, which is a reference to the instantiated plugin. You now need to subscribe to scroll events on the plugin object, instead of the widget object. Additionally, the activate and deactivate functions are attached to the plugin instance as well.

Overall the plugin code has changed little. The few differences are because it no longer uses the Y.Base.create function, and it references the host object for finding the bounding box.

There’s more…

I put together a demo page so you can try it out.