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 to do it

As with most problems, there are ...

HTML 5 Feature Detection Using Modernizr

HTML 5 is probably the most exciting improvement to come to the web, since the popularization of AJAX. However, as shown by last weeks article (HTML 5 Forms), only the most cutting edge browsers support many of the new HTML 5 features. To progressively enhance a site for HTML 5 enabled browsers, developers need a way to detect if the browser supports a desired HTML 5 feature. Enter Modernizr by Faruk Ates ...

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 ...

Mac Preference Radios For The Web

One of my favorite UI features of the Mac OS, both OSX and the iPhone, is the way it handles radio inputs. Instead of having small, hard to click little round circle (like the web), there are large buttons that are obviously grouped by use of a connecting bar. The best place to see this is in the system preferences on OSX. Todays article, introduces a widget that will do the same, by converting existing ...

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 ...

YUI 3 Radial Menu Gallery Component

The RadialMenu gallery component is finished and available on GitHub at http://github.com/mattsnider/yui3-gallery/tree/master/build/gallery-radialmenu. The latest version of the RadialMenu improves on the previous two articles by having the RadialMenu and RadailMenuPanel objects extend Overlay. By using the overlay component, RadialMenu can leverage the knowledge and experience that went into developing overlay, and we remove the need to mask the page with an invisible div.

Getting ready

You will need to include the following for ...