Mar
9
2012

MooTools Roar Mac Style Notification System

OK, so in the beginning… way back when… I was one of those programmers that thought JS was just a toy and nothing more… boy oh boy was I missing the boat!

One of the craftsman that influenced me the most was Harald Kirschner of http://digitarald.de the guy is amazing. I have used many of his mooTools classes on numerous projects and more than that it gave me a point of reference when I started writing my own classes.

Today I needed Roar for a project, however I have moved on to mooTools 1.4+ so I am please to release the mooTools 1.4+ compatible code base (mooTools compatibility mode NOT required).

Oh and by the way, am I glad I jumped on that Boat, my life has not been the same over the last 5 years!

/**
 * Roar - Notifications
 *
 * Inspired by Growl
 *
 * @version		1.0.2
 *
 * @license		MIT-style license
 * @author		Harald Kirschner <mail [at] digitarald.de>
 * @copyright	Author
 * @update		Tim Wickstrom
 * @date		03/19/2012
 */

var Roar = new Class({

	Implements: [Options, Events, Chain],

	options: {
		duration: 10000,
		position: 'lowerLeft',
		container: null,
		bodyFx: {},
		itemFx: {},
		margin: {x: 0, y: 46},
		offset: 10,
		className: 'roar',
		adjust: false
		/****, Available Events
			onShow: function(){},
			onHide:  function(){},
			onRender:  function(){}
		****/
	},

	initialize: function(options) {
		this.setOptions(options);
		this.items = [];
		this.container = document.id(this.options.container) || document;
	},

	alert: function(title, message, options) {
		var items = [new Element('h3', { 'html': title ? title.clean() : '' })];
		if (message) items.push(new Element('p', {'html': message.clean() }));
		return this.inject(items, typeOf(options)=='object'?options:{});
	},

	inject: function(elements, options) {
		if (!this.body) this.render();
		
		var offset = [-this.options.offset, 0];
		var last = this.items.getLast();
		if (last) {
			offset[0] = last.retrieve('roar:offset');
			offset[1] = offset[0] + last.offsetHeight + this.options.offset;
		}
		var to = {'opacity': 1};
		to[this.align.y] = offset;
		var item = new Element('div', {
			'class': options.className || this.options.className,
			styles: {
				'opacity': 0
			}
		}).adopt(
			new Element('div', {
				'class': 'roar-bg',
				'opacity': 0.7
			}),
			elements
		);

		item.setStyle(this.align.x, 0).store('roar:offset', offset[1]).set('morph', Object.merge({
			link: 'cancel',
			transition: Fx.Transitions.Back.easeOut
		}, this.options.itemFx));

		var remove = this.remove.create({
			bind: this,
			arguments: [item],
			delay: 10
		});
		
		this.items.push(item.addEvent('click', remove));

		if (this.options.duration) {
			var over = false;
			var trigger = (function() {
				trigger = null;
				if (!over) remove();
			}).delay(this.options.duration);
			item.addEvents({
				mouseover: function() {
					over = true;
				},
				mouseout: function() {
					over = false;
					if (!trigger) remove();
				}
			});
		}
		this.show(item,this.body,to)
		return this.fireEvent('show', [item, this.items.length]);
	},

	show: function(i,b,t) {
		i.inject(b).morph(t);
	},

	remove: function(item) {
		var index = this.items.indexOf(item);
		if (index == -1) return this;
		this.items.splice(index, 1);
		item.removeEvents();
		var to = {opacity: 0};
		to[this.align.y] = item.getStyle(this.align.y).toInt() - item.offsetHeight - this.options.offset;
		item.set('morph', Object.merge(
			{
				onComplete: function() {
					item.destroy();
				}
			},
			item.get('morph')
		)).morph(to);
		if(this.options.adjust) {
			this.adjust(index, item.offsetHeight)
		}
		return this.fireEvent('hide', [item, this.items.length]).callChain(item);
	},
	
	adjust: function(index, offset) {
		this.adjusting = true;
		Array.each(this.items,function(el, i) {
			if(i>=index) {
				var to = {};
				to[this.align.y] = el.getStyle(this.align.y).toInt() - offset - this.options.offset;
				to.onComplete = function() {
					if(this.items.length == i){
						this.adjusting = false;
					}
				}
				el.store('roar:offset', to[this.align.y]).morph(to);
			}
		}.bind(this));
	},
	
	empty: function() {
		while (this.items.length) this.remove(this.items[0]);
		return this;
	},

	render: function() {
		this.adjusting = false;
		this.position = this.options.position;
		if (typeOf(this.position) == 'string') {
			var position = {x: 'center', y: 'center'};
			this.align = {x: 'left', y: 'top'};
			if ((/left|west/i).test(this.position)) position.x = 'left';
			else if ((/right|east/i).test(this.position)) this.align.x = position.x = 'right';
			if ((/upper|top|north/i).test(this.position)) position.y = 'top';
			else if ((/bottom|lower|south/i).test(this.position)) this.align.y = position.y = 'bottom';
			this.position = position;
		}
		this.body = new Element('div', {'class': 'roar-body'}).inject(document.body);
		
		this.moveTo = this.body.setStyles.bind(this.body);
		this.reposition();
		if (this.options.bodyFx) {
			var morph = new Fx.Morph(this.body, Object.merge({
				unit: 'px',
				chain: 'cancel',
				transition: Fx.Transitions.Circ.easeOut
			}, this.options.bodyFx));
			this.moveTo = morph.start.bind(morph);
		}
		var repos = this.reposition.bind(this);
		window.addEvents({
			scroll: repos,
			resize: repos
		});
		this.fireEvent('render', this.body);
	},

	reposition: function() {
		var max = document.getCoordinates(), scroll = document.getScroll(), margin = this.options.margin;
		max.left += scroll.x;
		max.right += scroll.x;
		max.top += scroll.y;
		max.bottom += scroll.y;
		var rel = (typeOf(this.container) == 'element') ? this.container.getCoordinates() : max;
		this.moveTo({
			left: (this.position.x == 'right')
				? (Math.min(rel.right, max.right) - margin.x)
				: (Math.max(rel.left, max.left) + margin.x),
			top: (this.position.y == 'bottom')
				? (Math.min(rel.bottom, max.bottom) - margin.y)
				: (Math.max(rel.top, max.top) + margin.y)
		});
	}

});

Documentation can still be found at http://digitarald.de/project/remooz/

I have added an experimental feature that is still in testing. A method called adjust. Its purpose is to slide all open roars back to place when one gets removed. Again this is still being tested but feel free to play with it and send any feedback.

Thanks

Leave a comment

Show/Hide Footer Actions

Status: Available for your project.