Jump To …

jquery.fracs-0.11.js

/*! jQuery.fracs 0.11 - //larsjung.de/fracs - MIT License */

(function (window, document, $) {
'use strict';
/*jslint browser: true, confusion: true, nomen: true, regexp: true, vars: true, white: true */
/*global jQuery */

Some often used references.

	var $window = $(window),
		$document = $(document),
		extend = $.extend,
		isFn = $.isFunction,
		slice = [].slice,
		mathMax = Math.max,
		mathMin = Math.min,
		mathRound = Math.round,
		isTypeOf = function (obj, type) {

			return typeof obj === type;
		},
		isInstanceOf = function (obj, type) {

			return obj instanceof type;
		},
		isHTMLElement = function (obj) {

			return obj && obj.nodeType;
		},
		getHTMLElement = function (obj) {

			return isHTMLElement(obj) ? obj : (isInstanceOf(obj, $) ? obj[0] : undefined);
		},
		reduce = function (elements, fn, current) {

			$.each(elements, function (idx, element) {

				current = fn.call(element, current, idx, element);
			});
			return current;
		},
		getId = (function () {

			var ids = {},
				nextId = 1;

			return function (element) {

				if (!element) {
					return 0;
				}
				if (!ids[element]) {
					ids[element] = nextId;
					nextId += 1;
				}
				return ids[element];
			};
		}()),
		equal = function (obj1, obj2, props) {

			var i, l, prop;

			if (obj1 === obj2) {
				return true;
			}
			if (!obj1 || !obj2 || obj1.constructor !== obj2.constructor) {
				return false;
			}
			for (i = 0, l = props.length; i < l; i += 1) {
				prop = props[i];
				if (obj1[prop] && isFn(obj1[prop].equals) && !obj1[prop].equals(obj2[prop])) {
					return false;
				}
				if (obj1[prop] !== obj2[prop]) {
					return false;
				}
			}
			return true;
		};

Objects

Rect

Holds the position and dimensions of a rectangle. The position might be relative to document, viewport or element space.

	var Rect = function (left, top, width, height) {

Top left corner of the rectangle rounded to integers.

		this.left = mathRound(left);
		this.top = mathRound(top);

Dimensions rounded to integers.

		this.width = mathRound(width);
		this.height = mathRound(height);

Bottom right corner of the rectangle.

		this.right = this.left + this.width;
		this.bottom = this.top + this.height;
	};

Prototype

	extend(Rect.prototype, {

Checks if this instance equals that in position and dimensions.

		equals: function (that) {

			return equal(this, that, ['left', 'top', 'width', 'height']);
		},

Returns the area of this rectangle.

		area: function () {

			return this.width * this.height;
		},

Returns a new Rect representig this rect relative to rect.

		relativeTo: function (rect) {

			return new Rect(this.left - rect.left, this.top - rect.top, this.width, this.height);
		},

Returns a new rectangle representing the intersection of this instance and rect. If there is no intersection the return value is null.

		intersection: function (rect) {

			if (!isInstanceOf(rect, Rect)) {
				return null;
			}

			var left = mathMax(this.left, rect.left),
				right = mathMin(this.right, rect.right),
				top = mathMax(this.top, rect.top),
				bottom = mathMin(this.bottom, rect.bottom),
				width = right - left,
				height = bottom - top;

			return (width >= 0 && height >= 0) ? new Rect(left, top, width, height) : null;
		},

Returns a new rectangle representing the smallest rectangle containing this instance and rect.

		envelope: function (rect) {

			if (!isInstanceOf(rect, Rect)) {
				return this;
			}

			var left = mathMin(this.left, rect.left),
				right = mathMax(this.right, rect.right),
				top = mathMin(this.top, rect.top),
				bottom = mathMax(this.bottom, rect.bottom),
				width = right - left,
				height = bottom - top;

			return new Rect(left, top, width, height);
		}
	});

Static methods

	extend(Rect, {

Returns a new instance of Rect representing the content of the specified element. Since the coordinates are in content space the left and top values are always set to 0. If inDocSpace is true the rect gets returned in document space.

		ofContent: function (element, inContentSpace) {

			if (!element || element === document || element === window) {
				return new Rect(0, 0, $document.width(), $document.height());
			}

			if (inContentSpace) {
				return new Rect(0, 0, element.scrollWidth, element.scrollHeight);
			} else {
				return new Rect(element.offsetLeft - element.scrollLeft, element.offsetTop - element.scrollTop, element.scrollWidth, element.scrollHeight);
			}
		},

Returns a new instance of Rect representing the viewport of the specified element. If inDocSpace is true the rect gets returned in document space instead of content space.

		ofViewport: function (element, inContentSpace) {

			if (!element || element === document || element === window) {
				return new Rect($window.scrollLeft(), $window.scrollTop(), $window.width(), $window.height());
			}

			if (inContentSpace) {
				return new Rect(element.scrollLeft, element.scrollTop, element.clientWidth, element.clientHeight);
			} else {
				return new Rect(element.offsetLeft, element.offsetTop, element.clientWidth, element.clientHeight);
			}
		},

Returns a new instance of Rect representing a given HTMLElement. The dimensions respect padding and border widths. If the element is invisible (as determined by jQuery) the return value is null.

		ofElement: function (element) {

			var $element = $(element);
			if (!$element.is(':visible')) {
				return null;
			}

			var offset = $element.offset();
			return new Rect(offset.left, offset.top, $element.outerWidth(), $element.outerHeight());
		}
	});

Fractions

The heart of the library. Creates and holds the fractions data for the two specified rects. viewport defaults to Rect.ofViewport().

	var Fractions = function (visible, viewport, possible, rects) {

		this.visible = visible || 0;
		this.viewport = viewport || 0;
		this.possible = possible || 0;
		this.rects = (rects && extend({}, rects)) || null;
	};

Prototype

	extend(Fractions.prototype, {

Checks if this instance equals that in all attributes.

		equals: function (that) {

			return this.fracsEqual(that) && this.rectsEqual(that);
		},

Checks if this instance equals that in all fraction attributes.

		fracsEqual: function (that) {

			return equal(this, that, ['visible', 'viewport', 'possible']);
		},

Checks if this instance equals that in all rectangle attributes.

		rectsEqual: function (that) {

			return equal(this.rects, that.rects, ['document', 'element', 'viewport']);
		}
	});

Static methods

	extend(Fractions, {

		of: function (rect, viewport) {

			var intersection, intersectionArea, possibleArea;

			rect = (isHTMLElement(rect) && Rect.ofElement(rect)) || rect;
			viewport = (isHTMLElement(viewport) && Rect.ofViewport(viewport)) || viewport || Rect.ofViewport();

			intersection = rect.intersection(viewport);
			if (!intersection) {
				return new Fractions();
			}

			intersectionArea = intersection.area();
			possibleArea = mathMin(rect.width, viewport.width) * mathMin(rect.height, viewport.height);
			return new Fractions(
				intersectionArea / rect.area(),
				intersectionArea / viewport.area(),
				intersectionArea / possibleArea,
				{
					document: intersection,
					element: intersection.relativeTo(rect),
					viewport: intersection.relativeTo(viewport)
				}
			);
		}
	});

Group

	var Group = function (elements, viewport) {

		this.els = elements;
		this.viewport = viewport;
	};

Helpers

Accepted values for property parameters below.

	var rectProps = ['width', 'height', 'left', 'right', 'top', 'bottom'],
		fracsProps = ['possible', 'visible', 'viewport'],

Returns the specified property for HTMLElement element or 0 if property is invalid.

		getValue = function (element, viewport, property) {

			var obj;

			if ($.inArray(property, rectProps) >= 0) {
				obj = Rect.ofElement(element);
			} else if ($.inArray(property, fracsProps) >= 0) {
				obj = Fractions.of(element, viewport);
			}
			return obj ? obj[property] : 0;
		},

Sorting functions.

		sortAscending = function (entry1, entry2) {

			return entry1.val - entry2.val;
		},
		sortDescending = function (entry1, entry2) {

			return entry2.val - entry1.val;
		};

Prototype

	extend(Group.prototype, {

Returns a sorted list of objects {el: HTMLElement, val: Number} for the specified property. descending defaults to false.

		sorted: function (property, descending) {

			var viewport = this.viewport;

			return $.map(this.els, function (element) {

						return {
							el: element,
							val: getValue(element, viewport, property)
						};
					})
					.sort(descending ? sortDescending : sortAscending);
		},

Returns the first element of the sorted list returned by sorted above, or null if this list is empty.

		best: function (property, descending) {

			return this.els.length ? this.sorted(property, descending)[0] : null;
		}
	});

ScrollState

	var ScrollState = function (element) {

		var content = Rect.ofContent(element, true),
			viewport = Rect.ofViewport(element, true),
			w = content.width - viewport.width,
			h = content.height - viewport.height;

		this.content = content;
		this.viewport = viewport;
		this.width = w <= 0 ? null : viewport.left / w;
		this.height = h <= 0 ? null : viewport.top / h;
		this.left = viewport.left;
		this.top = viewport.top;
		this.right = content.right - viewport.right;
		this.bottom = content.bottom - viewport.bottom;
	};

Prototype

	extend(ScrollState.prototype, {

Checks if this instance equals that.

		equals: function (that) {

			return equal(this, that, ['width', 'height', 'left', 'top', 'right', 'bottom', 'content', 'viewport']);
		}
	});

Viewport

	var Viewport = function (element) {

		this.el = element || window;
	};

Prototype

	extend(Viewport.prototype, {

Checks if this instance equals that.

		equals: function (that) {

			return equal(this, that, ['el']);
		},

		scrollState: function () {

			return new ScrollState(this.el);
		},

		scrollTo: function (left, top, duration) {

			var $el = this.el === window ? $('html,body') : $(this.el);

			left = left || 0;
			top = top || 0;
			duration = isNaN(duration) ? 1000 : duration;

			$el.stop(true).animate({scrollLeft: left, scrollTop: top}, duration);
		},

		scroll: function (left, top, duration) {

			var $el = this.el === window ? $window : $(this.el);

			left = left || 0;
			top = top || 0;

			this.scrollTo($el.scrollLeft() + left, $el.scrollTop() + top, duration);
		},

		scrollToRect: function (rect, paddingLeft, paddingTop, duration) {

			paddingLeft = paddingLeft || 0;
			paddingTop = paddingTop || 0;

			this.scrollTo(rect.left - paddingLeft, rect.top - paddingTop, duration);
		},

		scrollToElement: function (element, paddingLeft, paddingTop, duration) {

			var rect = Rect.ofElement(element).relativeTo(Rect.ofContent(this.el));

			this.scrollToRect(rect, paddingLeft, paddingTop, duration);
		}
	});

Callbacks

callbacks mix-in

Expects context: HTMLElement and updatedValue: function.

	var callbacksMixIn = {

Initial setup.

		init: function () {

			this.callbacks = $.Callbacks('memory unique');
			this.currVal = null;
			this.prevVal = null;

A proxy to make check bindable to events.

			this.checkProxy = $.proxy(this.check, this);

			this.autoCheck();
		},

Adds a new callback function.

		bind: function (callback) {

			this.callbacks.add(callback);
		},

Removes a previously added callback function.

		unbind: function (callback) {

			if (callback) {
				this.callbacks.remove(callback);
			} else {
				this.callbacks.empty();
			}
		},

Triggers all callbacks with the current values.

		trigger: function () {

			this.callbacks.fireWith(this.context, [this.currVal, this.prevVal]);
		},

Checks if value changed, updates attributes currVal and prevVal accordingly and triggers the callbacks. Returns true if value changed, otherwise false.

		check: function (event) {

			var value = this.updatedValue(event);

			if (value === undefined) {
				return false;
			}

			this.prevVal = this.currVal;
			this.currVal = value;
			this.trigger();
			return true;
		},

Auto-check configuration.

		$autoTarget: $window,
		autoEvents: 'load resize scroll',

Enables/disables automated checking for changes on the specified window events.

		autoCheck: function (on) {

			this.$autoTarget[on === false ? 'off' : 'on'](this.autoEvents, this.checkProxy);
		}
	};

FracsCallbacks

	var FracsCallbacks = function (element, viewport) {

		this.context = element;
		this.viewport = viewport;
		this.init();
	};

Prototype

	extend(FracsCallbacks.prototype, callbacksMixIn, {
		updatedValue: function () {

			var value = Fractions.of(this.context, this.viewport);

			if (!this.currVal || !this.currVal.equals(value)) {
				return value;
			}
		}
	});

GroupCallbacks

	var GroupCallbacks = function (elements, viewport, property, descending) {

		this.context = new Group(elements, viewport);
		this.property = property;
		this.descending = descending;
		this.init();
	};

Prototype

	extend(GroupCallbacks.prototype, callbacksMixIn, {
		updatedValue: function () {

			var best = this.context.best(this.property, this.descending);

			if (best) {
				best = best.val > 0 ? best.el : null;
				if (this.currVal !== best) {
					return best;
				}
			}
		}
	});

ScrollStateCallbacks

	var ScrollStateCallbacks = function (element) {

		if (!element || element === window || element === document) {
			this.context = window;
		} else {
			this.context = element;
			this.$autoTarget = $(element);
		}
		this.init();
	};

Prototype

	extend(ScrollStateCallbacks.prototype, callbacksMixIn, {
		updatedValue: function () {

			var value = new ScrollState(this.context);

			if (!this.currVal || !this.currVal.equals(value)) {
				return value;
			}
		}
	});

modplug 0.7

Use http://larsjung.de/modplug to attach the plug-in to jQuery.

	var modplug = function (namespace, options) {

Save the initial settings.

		var settings = extend({}, options),

Helper function to apply default methods.

			applyMethod = function (obj, args, methodName, methods) {

If methodName is a function apply it to get the actual method name.

				methodName = isFn(methodName) ? methodName.apply(obj, args) : methodName;

If method exists then apply it and return the result ...

				if (isFn(methods[methodName])) {
					return methods[methodName].apply(obj, args);
				}

... otherwise raise an error.

				$.error('Method "' + methodName + '" does not exist on jQuery.' + namespace);
			},

This function gets exposed as $.<namespace>.

			statics = function () {

Try to apply a default method.

				return applyMethod(this, slice.call(arguments), settings.defaultStatic, statics);
			},

This function gets exposed as $(selector).<namespace>.

			methods = function (method) {

If method exists then apply it ...

				if (isFn(methods[method])) {
					return methods[method].apply(this, slice.call(arguments, 1));
				}

... otherwise try to apply a default method.

				return applyMethod(this, slice.call(arguments), settings.defaultMethod, methods);
			},

Adds/overwrites plug-in methods. This function gets exposed as $.<namespace>.modplug to make the plug-in extendable.

			plug = function (options) {

				if (options) {
					extend(statics, options.statics);
					extend(methods, options.methods);
				}

Make sure that $.<namespace>.modplug points to this function after adding new methods.

				statics.modplug = plug;
			};

Save objects or methods previously registered to the desired namespace. They are available via $.<namespace>.modplug.prev.

		plug.prev = {
			statics: $[namespace],
			methods: $.fn[namespace]
		};

Init the plug-in by adding the specified statics and methods.

		plug(options);

Register the plug-in.

		$[namespace] = statics;
		$.fn[namespace] = methods;
	};

Register the plug-in

The namespace used to register the plug-in and to attach data to elements.

	var namespace = 'fracs';

The methods are sorted in alphabetical order. All methods that do not provide a return value will return this to enable method chaining.

	modplug(namespace, {

Static methods

These methods are accessible via $.fracs.<methodname>.

		statics: {

Build version.

			version: '0.11',

Publish object constructors (for testing).

			Rect: Rect,
			Fractions: Fractions,
			Group: Group,
			ScrollState: ScrollState,
			Viewport: Viewport,
			FracsCallbacks: FracsCallbacks,
			GroupCallbacks: GroupCallbacks,
			ScrollStateCallbacks: ScrollStateCallbacks,

fracs

This is the default method. So instead of calling $.fracs.fracs(...) simply call $.fracs(...).

Returns the fractions for a given Rect and viewport, viewport defaults to $.fracs.viewport().

 $.fracs(rect: Rect, [viewport: Rect]): Fractions
			fracs: function (rect, viewport) {

				return Fractions.of(rect, viewport);
			}
		},

Instance methods

These methods are accessible via $(selector).fracs('<methodname>', ...).

		methods: {

'content'

Returns the content rect of the first selected element in content space. If no element is selected it returns the document rect.

 .fracs('content'): Rect
			content: function (inContentSpace) {

				return this.length ? Rect.ofContent(this[0], inContentSpace) : null;
			},

'envelope'

Returns the smallest rectangle that containes all selected elements.

 .fracs('envelope'): Rect
			envelope: function () {

				return reduce(this, function (current) {

					var rect = Rect.ofElement(this);
					return current ? current.envelope(rect) : rect;
				});
			},

'fracs'

This is the default method. So the first parameter 'fracs' can be omitted.

Returns the fractions for the first selected element.

 .fracs(): Fractions

Binds a callback function that will be invoked if fractions have changed after a window resize or window scroll event.

 .fracs(callback(fracs: Fractions, prevFracs: Fractions)): jQuery

Unbinds the specified callback function.

 .fracs('unbind', callback): jQuery

Unbinds all callback functions.

 .fracs('unbind'): jQuery

Checks if fractions changed and if so invokes all bound callback functions.

 .fracs('check'): jQuery
			fracs: function (action, callback, viewport) {

				if (!isTypeOf(action, 'string')) {
					viewport = callback;
					callback = action;
					action = null;
				}
				if (!isFn(callback)) {
					viewport = callback;
					callback = null;
				}
				viewport = getHTMLElement(viewport);

				var ns = namespace + '.fracs.' + getId(viewport);

				if (action === 'unbind') {
					return this.each(function () {

						var cbs = $(this).data(ns);

						if (cbs) {
							cbs.unbind(callback);
						}
					});
				} else if (action === 'check') {
					return this.each(function () {

						var cbs = $(this).data(ns);

						if (cbs) {
							cbs.check();
						}
					});
				} else if (isFn(callback)) {
					return this.each(function () {

						var $this = $(this),
							cbs = $this.data(ns);

						if (!cbs) {
							cbs = new FracsCallbacks(this, viewport);
							$this.data(ns, cbs);
						}
						cbs.bind(callback);
					});
				}

				return this.length ? Fractions.of(this[0], viewport) : null;
			},

'intersection'

Returns the greatest rectangle that is contained in all selected elements.

 .fracs('intersection'): Rect
			intersection: function () {

				return reduce(this, function (current) {

					var rect = Rect.ofElement(this);
					return current ? current.intersection(rect) : rect;
				});
			},

'max'

Reduces the set of selected elements to those with the maximum value of the specified property. Valid values for property are possible, visible, viewport, width, height, left, right, top, bottom.

 .fracs('max', property: String): jQuery

Binds a callback function to the set of selected elements that gets triggert whenever the element with the highest value of the specified property changes.

 .fracs('max', property: String, callback(best: Element, prevBest: Element)): jQuery
			max: function (property, callback, viewport) {

				if (!isFn(callback)) {
					viewport = callback;
					callback = null;
				}
				viewport = getHTMLElement(viewport);

				if (callback) {
					new GroupCallbacks(this, viewport, property, true).bind(callback);
					return this;
				}

				return this.pushStack(new Group(this, viewport).best(property, true).el);
			},

'min'

Reduces the set of selected elements to those with the minimum value of the specified property. Valid values for property are possible, visible, viewport, width, height, left, right, top, bottom.

 .fracs('min', property: String): jQuery

Binds a callback function to the set of selected elements that gets triggert whenever the element with the lowest value of the specified property changes.

 .fracs('min', property: String, callback(best: Element, prevBest: Element)): jQuery
			min: function (property, callback, viewport) {

				if (!isFn(callback)) {
					viewport = callback;
					callback = null;
				}
				viewport = getHTMLElement(viewport);

				if (callback) {
					new GroupCallbacks(this, viewport, property).bind(callback);
					return this;
				}

				return this.pushStack(new Group(this, viewport).best(property).el);
			},

'rect'

Returns the dimensions for the first selected element in document space.

 .fracs('rect'): Rect
			rect: function () {

				return this.length ? Rect.ofElement(this[0]) : null;
			},

'scrollState'

Returns the current scroll state for the first selected element.

 .fracs('scrollState'): ScrollState

Binds a callback function that will be invoked if scroll state has changed after a resize or scroll event.

 .fracs('scrollState', callback(scrollState: scrollState, prevScrollState: scrollState)): jQuery

Unbinds the specified callback function.

 .fracs('scrollState', 'unbind', callback): jQuery

Unbinds all callback functions.

 .fracs('scrollState', 'unbind'): jQuery

Checks if scroll state changed and if so invokes all bound callback functions.

 .fracs('scrollState', 'check'): jQuery
			scrollState: function (action, callback) {

				var ns = namespace + '.scrollState';

				if (!isTypeOf(action, 'string')) {
					callback = action;
					action = null;
				}

				if (action === 'unbind') {
					return this.each(function () {

						var cbs = $(this).data(ns);

						if (cbs) {
							cbs.unbind(callback);
						}
					});
				} else if (action === 'check') {
					return this.each(function () {

						var cbs = $(this).data(ns);

						if (cbs) {
							cbs.check();
						}
					});
				} else if (isFn(callback)) {
					return this.each(function () {

						var $this = $(this),
							cbs = $this.data(ns);

						if (!cbs) {
							cbs = new ScrollStateCallbacks(this);
							$this.data(ns, cbs);
						}
						cbs.bind(callback);
					});
				}

				return this.length ? new ScrollState(this[0]) : null;
			},

'scroll'

Scrolls the selected elements relative to its current position, padding defaults to 0, duration to 1000.

 .fracs('scroll', element: HTMLElement/jQuery, [paddingLeft: int,] [paddingTop: int,] [duration: int]): jQuery
			scroll: function (left, top, duration) {

				return this.each(function () {

					new Viewport(this).scroll(left, top, duration);
				});
			},

'scrollTo'

Scrolls the selected elements to the specified element or an absolute position, padding defaults to 0, duration to 1000.

 .fracs('scrollTo', element: HTMLElement/jQuery, [paddingLeft: int,] [paddingTop: int,] [duration: int]): jQuery
 .fracs('scrollTo', [left: int,] [top: int,] [duration: int]): jQuery
			scrollTo: function (element, paddingLeft, paddingTop, duration) {

				if ($.isNumeric(element)) {
					duration = paddingTop;
					paddingTop = paddingLeft;
					paddingLeft = element;
					element = null;
				}

				element = getHTMLElement(element);

				return this.each(function () {

					if (element) {
						new Viewport(this).scrollToElement(element, paddingLeft, paddingTop, duration);
					} else {
						new Viewport(this).scrollTo(paddingLeft, paddingTop, duration);
					}
				});
			},

'scrollToThis'

Scrolls the viewport (window) to the first selected element in the specified time, padding defaults to 0, duration to 1000.

 .fracs('scrollToThis', [paddingLeft: int,] [paddingTop: int,] [duration: int,] [viewport: HTMLElement/jQuery]): jQuery
			scrollToThis: function (paddingLeft, paddingTop, duration, viewport) {

				viewport = new Viewport(getHTMLElement(viewport));

				viewport.scrollToElement(this[0], paddingLeft, paddingTop, duration);
				return this;
			},

'softLink'

Converts all selected page intern links <a href="#..."> into soft links. Uses scrollTo to scroll to the location.

 .fracs('softLink', [paddingLeft: int,] [paddingTop: int,] [duration: int,] [viewport: HTMLElement/jQuery]): jQuery
			softLink: function (paddingLeft, paddingTop, duration, viewport) {

				viewport = new Viewport(getHTMLElement(viewport));

				return this.filter('a[href^=#]').each(function () {
					var $a = $(this);
					$a.on('click', function () {
						viewport.scrollToElement($($a.attr('href'))[0], paddingLeft, paddingTop, duration);
					});
				});
			},

'sort'

Sorts the set of selected elements by the specified property. Valid values for property are possible, visible, viewport, width, height, left, right, top, bottom. The default sort order is descending.

 .fracs('sort', property: String, [ascending: boolean]): jQuery
			sort: function (property, ascending, viewport) {

				if (!isTypeOf(ascending, 'boolean')) {
					viewport = ascending;
					ascending = null;
				}
				viewport = getHTMLElement(viewport);

				return this.pushStack($.map(new Group(this, viewport).sorted(property, !ascending), function (entry) {
					return entry.el;
				}));
			},

'viewport'

Returns the current viewport of the first selected element in content space. If no element is selected it returns the document's viewport.

 .fracs('viewport'): Rect
			viewport: function (inContentSpace) {

				return this.length ? Rect.ofViewport(this[0], inContentSpace) : null;
			}
		},

Defaults

		defaultStatic: 'fracs',
		defaultMethod: 'fracs'
	});

}(window, document, jQuery));