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.

Custom Events for Scrolling Towards Margins

There is an emerging UI pattern, where additional content is loaded when the user scrolls to the bottom of an element or page. This can be seen on the Facebook newsfeed, or many applications on touch-driven mobile devices. Today's article describes a YUI 3 widget that we use on Votizen.com, which will fire an event when the user scrolls to the border of an element.

Getting ready

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

Simplified Text Replacement in HTML

JavaScript developers frequently have to replace parts of text on a webpage. Sometimes it is a hassle, as there are multiple small pieces of text that need to be replaced inside of a larger element (such as numbers or madlibs). Today's code snippet finds each var node inside of a parent element and returns an object with a substitute function. This function simplifies replacing the content inside the var elements.

Getting started…

Create HTML markup ...

Sharing Static Data Between YUI 3 Instances

YUI 3 does an excellent job of sandboxing instances so that you cannot accidentally contaminate objects or data. However, sometimes you need to share data between instances, such as if you have a global click handler that dispatches events. Otherwise, each time you include the module, the global data or event is duplicated. Therefore, we need a good way to occasionally share data between instances.

How do it…

As with most problems, there are ...

Updated YUI3 for YUI2

For those of you who are using my YUI 3 for YUI 2 emulation code, I want to share with you the latest, and perhaps final version. This version is very similar to what we are now using on Mint.com, where I have been successfully swapping back and forth between the mock YUI 3 and the real YUI 3. http://yui-ext-mvc.googlecode.com/svn/trunk/assets/js/yahoo-ext/yui3foryui2.js

Notes

  • Y.Base.create has been added
  • use YUI.add instead of YUI().add
  • Y.NodeList is ...

Adding Find() to NodeList in YUI3

In YUI 3, when searching an array for a value, you have to include the collection module, as the Y.Array.find() function is not built into Y.Array. This is a little annoying, because 9 out of 10 times, I include the collection module, it is just for the Y.Array.find() function. Additionally, because the function isnt built into the core library, it is not used or added to the core features, like NodeList. Yet, I believe having ...

Mocking YUI 3 in YUI 2

At Mint.com we have wanted for some time to upgrade to YUI 3. However, since YUI 3 is not backwards compatible with YUI 2, it is hard to justify taking the time to port a large codebase. To alleviate the cost of upgrading, we have decided on a two step approach. During the first phase we will convert our YUI 2 code to use YUI 3 syntax, then in the second phase when we ...

IE 8 Compatibility Issue with YUI 2 Selector Component

I realized recently, in my work on Mint.com, that the YAHOO.util.Selector.query function does not work properly in IE 8 compatibility mode when performing a search against the class and for attributes. The issue is that previous versions of IE defined these attributes names as className and htmlFor, while IE 8 changed them to follow the standard. And a faulty if statement in the selector component does not properly use the legacy names when developers ...

Augmenting YUI3 Module Objects

In YUI 2 augmenting a library class is as simple as appending functions to the static object or modifying the prototype of a non-static object. Augmenting objects in YUI 3 is almost as easy, differing only in the augmentation having to be applied inside of a YUI().add() function. Although, augmenting objects is not difficult, it feels complicated until you become used to the YUI 3 way of coding. This article will cover how to augment ...

YUI Cookie Storage Engine

In the YUI Storage Utility article, we covered the new utility method added into YUI 2.8 for storing large amounts of data client-side using JavaScript. The utility uses a variety of engines to make this possible, and was written to be easily extensible, so developers can write and add their own engines. This article will walk through how to create a simple engine using browser cookies. For starters, I do not recommending using this ...