API Docs for: 1.0.0
Show:

File: src/gallery-scrollintoview/js/ScrollIntoView.js

"use strict";

/**
 * @module gallery-scrollintoview
 */

var scrollBodyParent = (YUI.Env.UA.gecko > 0 || YUI.Env.UA.opera > 0);

/**
 * Only scrolls if the object is not currently visible.
 * 
 * This requires that all scrollable elements have position:relative.
 * Otherwise, this algorithm will skip over them with unpredictable
 * results.
 * 
 * @main gallery-scrollintoview
 * @class Node~scrollIntoView
 */

function scrollContainer(node)
{
	var info = node.scrollIntoViewData;
	while (1)
	{
		var hit_top = (info.a.offsetParent === null);

		var a = Y.one(info.a),
			b = (a.getDOMNode() === Y.config.doc.body),
			w = b ? Y.DOM.winWidth() : info.a.clientWidth,
			h = b ? Y.DOM.winHeight() : info.a.clientHeight;
		if (info.a.scrollWidth - a.horizMarginBorderPadding() > w ||
			info.a.scrollHeight - a.vertMarginBorderPadding() > h)
		{
			break;
		}
		else if (hit_top)
		{
			node.fire('scrollIntoViewFinished');
			node.scrollIntoViewData = null;
			return false;
		}

		info.r.move(info.a.offsetLeft - info.a.scrollLeft, info.a.offsetTop - info.a.scrollTop);
		info.a = info.a.offsetParent || info.a.parentNode;
	}

	var scrollX = (hit_top ? Y.config.doc.documentElement.scrollLeft || Y.config.doc.body.scrollLeft : info.a.scrollLeft);
	var scrollY = (hit_top ? Y.config.doc.documentElement.scrollTop || Y.config.doc.body.scrollTop : info.a.scrollTop);

	var d =
	{
		top:    scrollY,
		bottom: scrollY + (hit_top ? Y.DOM.winHeight() : info.a.clientHeight),
		left:   scrollX,
		right:  scrollX + (hit_top ? Y.DOM.winWidth() : info.a.clientWidth)
	};

	if (hit_top && info.margin)
	{
		d.top    += info.margin.top    || 0;
		d.bottom -= info.margin.bottom || 0;
		d.left   += info.margin.left   || 0;
		d.right  -= info.margin.right  || 0;
	}

	var dy = 0;
	if (a.getStyle('overflowY') == 'hidden')
	{
		// don't scroll
	}
	else if (info.r.top < d.top)
	{
		dy = info.r.top - d.top;
	}
	else if (info.r.bottom > d.bottom)
	{
		dy = Math.min(info.r.bottom - d.bottom, info.r.top - d.top);
	}

	var dx = 0;
	if (a.getStyle('overflowX') == 'hidden')
	{
		// don't scroll
	}
	else if (info.r.left < d.left)
	{
		dx = info.r.left - d.left;
	}
	else if (info.r.right > d.right)
	{
		dx = Math.min(info.r.right - d.right, info.r.left - d.left);
	}

	if (hit_top)
	{
		if (dx || dy)
		{
			if (info.anim)
			{
				var body = Y.one('body');
				if (scrollBodyParent)
				{
					body = body.get('parentNode');
				}

				info.anim.setAttrs(
				{
					node: body,
					to:
					{
						scrollLeft: body.get('scrollLeft') + dx,
						scrollTop:  body.get('scrollTop') + dy
					}
				});

				info.anim.once('end', function()
				{
					node.fire('scrollIntoViewFinished');
				});
				info.anim.run();
			}
			else
			{
				window.scrollBy(dx, dy);
				node.fire('scrollIntoViewFinished');
			}
		}
		else if (info.anim)
		{
			Y.later(0, null, function()
			{
				node.fire('scrollIntoViewFinished');
			});
		}

		node.scrollIntoViewData = null;
		return false;
	}

	if (dx || dy)
	{
		if (info.anim)
		{
			info.anim.setAttrs(
			{
				node: Y.one(info.a),
				to:
				{
					scrollLeft: info.a.scrollLeft + dx,
					scrollTop:  info.a.scrollTop + dy
				}
			});

			info.anim.once('end', function()
			{
				info.r.move(info.a.offsetLeft - info.a.scrollLeft, info.a.offsetTop - info.a.scrollTop);
				info.a = info.a.offsetParent;
				scrollContainer(node);
			});
			info.anim.run();
		}
		else
		{
			info.a.scrollLeft += dx;
			info.a.scrollTop  += dy;

			info.r.move(info.a.offsetLeft - info.a.scrollLeft, info.a.offsetTop - info.a.scrollTop);
			info.a = info.a.offsetParent;
		}
	}
	else if (info.anim)
	{
		Y.later(0, null, function()
		{
			info.r.move(info.a.offsetLeft - info.a.scrollLeft, info.a.offsetTop - info.a.scrollTop);
			info.a = info.a.offsetParent;
			scrollContainer(node);
		});
	}
	else
	{
		info.r.move(info.a.offsetLeft - info.a.scrollLeft, info.a.offsetTop - info.a.scrollTop);
		info.a = info.a.offsetParent;
	}

	return true;
}

/**
 * To receive notification of when the animation finishes, subscribe to the
 * event `scrollIntoViewFinished` on the Y.Node instance.  For consistency,
 * this event always fires, even without animation.
 * 
 * If you modify the ancestor chain while the animation is running, the
 * results will be unpredictable.
 *
 * @method scrollIntoView
 * @param [config] configuration
 * @param [config.anim] true to use default duration and easing, or object
 * @param [config.anim.duration] duration of animation
 * @param [config.anim.easing] easing used during animation
 * @param [config.margin] viewport margins, to avoid ending up under an element with `position:fixed`
 * @param [config.margin.top] top margin (px)
 * @param [config.margin.bottom] bottom margin (px)
 * @param [config.margin.left] left margin (px)
 * @param [config.margin.right] right margin (px)
 * @chainable
 */
Y.Node.prototype.scrollIntoView = function(config)
{
	if (this.scrollIntoViewData)
	{
		return this;
	}

	var ancestor = this.get('offsetParent');
	if (!ancestor)
	{
		return this;
	}

	var r =
	{
		top:    this.get('offsetTop'),
		bottom: this.get('offsetTop') + this.get('offsetHeight'),
		left:   this.get('offsetLeft'),
		right:  this.get('offsetLeft') + this.get('offsetWidth')
	};

	r.move = function(
		/* int */	dx,
		/* int */	dy)
	{
		this.top    += dy;
		this.bottom += dy;
		this.left   += dx;
		this.right  += dx;
	};

	this.scrollIntoViewData =
	{
		a: ancestor.getDOMNode(),
		r: r
	}

	config                         = config || {};
	this.scrollIntoViewData.margin = config.margin;

	if (config.anim)
	{
		var anim_config = {};
		if (Y.Lang.isObject(config.anim))
		{
			Y.mix(anim_config, config.anim);
		}
		this.scrollIntoViewData.anim = new Y.Anim(anim_config);

		scrollContainer(this);
	}
	else
	{
		while (scrollContainer(this))
		{
			// do it again
		}
	}

	return this;
};