diff options
Diffstat (limited to 'js')
29 files changed, 2878 insertions, 0 deletions
| diff --git a/js/classes/ElggEntity.js b/js/classes/ElggEntity.js new file mode 100644 index 000000000..9461a463f --- /dev/null +++ b/js/classes/ElggEntity.js @@ -0,0 +1,20 @@ +/** + * Create a new ElggEntity + *  + * @class Represents an ElggEntity + * @property {number} guid + * @property {string} type + * @property {string} subtype + * @property {number} owner_guid + * @property {number} site_guid + * @property {number} container_guid + * @property {number} access_id + * @property {number} time_created + * @property {number} time_updated + * @property {number} last_action + * @property {string} enabled + *  + */ +elgg.ElggEntity = function(o) { +	$.extend(this, o); +};
\ No newline at end of file diff --git a/js/classes/ElggPriorityList.js b/js/classes/ElggPriorityList.js new file mode 100644 index 000000000..b4cec5044 --- /dev/null +++ b/js/classes/ElggPriorityList.js @@ -0,0 +1,92 @@ +/** + * Priority lists allow you to create an indexed list that can be iterated through in a specific + * order. + */ +elgg.ElggPriorityList = function() { +	this.length = 0; +	this.priorities_ = []; +}; + +/** + * Inserts an element into the priority list at the priority specified. + * + * @param {Object} obj          The object to insert + * @param {Number} opt_priority An optional priority to insert at. + *  + * @return {Void} + */ +elgg.ElggPriorityList.prototype.insert = function(obj, opt_priority) { +	var priority = 500; +	if (arguments.length == 2 && opt_priority != undefined) { +		priority = parseInt(opt_priority, 10); +	} + +	priority = Math.max(priority, 0); + +	if (elgg.isUndefined(this.priorities_[priority])) { +		this.priorities_[priority] = []; +	} + +	this.priorities_[priority].push(obj); +	this.length++; +}; + +/** + * Iterates through each element in order. + * + * Unlike every, this ignores the return value of the callback. + * + * @param {Function} callback The callback function to pass each element through. See + *                            Array.prototype.every() for details. + * @return {Object} + */ +elgg.ElggPriorityList.prototype.forEach = function(callback) { +	elgg.assertTypeOf('function', callback); + +	var index = 0; + +	this.priorities_.forEach(function(elems) { +		elems.forEach(function(elem) { +			callback(elem, index++); +		}); +	}); + +	return this; +}; + +/** + * Iterates through each element in order. + * + * Unlike forEach, this returns the value of the callback and will break on false. + * + * @param {Function} callback The callback function to pass each element through. See + *                            Array.prototype.every() for details. + * @return {Object} + */ +elgg.ElggPriorityList.prototype.every = function(callback) { +	elgg.assertTypeOf('function', callback); + +	var index = 0; + +	return this.priorities_.every(function(elems) { +		return elems.every(function(elem) { +			return callback(elem, index++); +		}); +	}); +}; + +/** + * Removes an element from the priority list + * + * @param {Object} obj The object to remove. + * @return {Void} + */ +elgg.ElggPriorityList.prototype.remove = function(obj) { +	this.priorities_.forEach(function(elems) { +		var index; +		while ((index = elems.indexOf(obj)) !== -1) { +			elems.splice(index, 1); +			this.length--; +		} +	}); +};
\ No newline at end of file diff --git a/js/classes/ElggUser.js b/js/classes/ElggUser.js new file mode 100644 index 000000000..b8a976fba --- /dev/null +++ b/js/classes/ElggUser.js @@ -0,0 +1,28 @@ +/** + * Create a new ElggUser + * + * @param {Object} o + * @extends ElggEntity + * @class Represents an ElggUser + * @property {string} name + * @property {string} username + * @property {string} language + * @property {boolean} admin + */ +elgg.ElggUser = function(o) { +	elgg.ElggEntity.call(this, o); +}; + +elgg.inherit(elgg.ElggUser, elgg.ElggEntity); + +/** + * Is this user an admin? + * + * @warning The admin state of the user should be checked on the server for any + * actions taken that require admin privileges. + * + * @return {boolean} + */ +elgg.ElggUser.prototype.isAdmin = function() { +	return this.admin; +};
\ No newline at end of file diff --git a/js/lib/ajax.js b/js/lib/ajax.js new file mode 100644 index 000000000..b3f39cc42 --- /dev/null +++ b/js/lib/ajax.js @@ -0,0 +1,242 @@ +/*globals elgg, $*/ +elgg.provide('elgg.ajax'); + +/** + * @author Evan Winslow + * Provides a bunch of useful shortcut functions for making ajax calls + */ + +/** + * Wrapper function for jQuery.ajax which ensures that the url being called + * is relative to the elgg site root. + * + * You would most likely use elgg.get or elgg.post, rather than this function + * + * @param {string} url Optionally specify the url as the first argument + * @param {Object} options Optional. {@see jQuery#ajax} + * @return {XmlHttpRequest} + */ +elgg.ajax = function(url, options) { +	options = elgg.ajax.handleOptions(url, options); + +	options.url = elgg.normalize_url(options.url); +	return $.ajax(options); +}; +/** + * @const + */ +elgg.ajax.SUCCESS = 0; + +/** + * @const + */ +elgg.ajax.ERROR = -1; + +/** + * Handle optional arguments and return the resulting options object + * + * @param url + * @param options + * @return {Object} + * @private + */ +elgg.ajax.handleOptions = function(url, options) { +	var data_only = true, +		data, +		member; + +	//elgg.ajax('example/file.php', {...}); +	if (elgg.isString(url)) { +		options = options || {}; + +	//elgg.ajax({...}); +	} else { +		options = url || {}; +		url = options.url; +	} + +	//elgg.ajax('example/file.php', function() {...}); +	if (elgg.isFunction(options)) { +		data_only = false; +		options = {success: options}; +	} + +	//elgg.ajax('example/file.php', {data:{...}}); +	if (options.data) { +		data_only = false; +	} else { +		for (member in options) { +			//elgg.ajax('example/file.php', {callback:function(){...}}); +			if (elgg.isFunction(options[member])) { +				data_only = false; +			} +		} +	} + +	//elgg.ajax('example/file.php', {notdata:notfunc}); +	if (data_only) { +		data = options; +		options = {data: data}; +	} + +	if (url) { +		options.url = url; +	} + +	return options; +}; + +/** + * Wrapper function for elgg.ajax which forces the request type to 'get.' + * + * @param {string} url Optionally specify the url as the first argument + * @param {Object} options {@see jQuery#ajax} + * @return {XmlHttpRequest} + */ +elgg.get = function(url, options) { +	options = elgg.ajax.handleOptions(url, options); + +	options.type = 'get'; +	return elgg.ajax(options); +}; + +/** + * Wrapper function for elgg.get which forces the dataType to 'json.' + * + * @param {string} url Optionally specify the url as the first argument + * @param {Object} options {@see jQuery#ajax} + * @return {XmlHttpRequest} + */ +elgg.getJSON = function(url, options) { +	options = elgg.ajax.handleOptions(url, options); + +	options.dataType = 'json'; +	return elgg.get(options); +}; + +/** + * Wrapper function for elgg.ajax which forces the request type to 'post.' + * + * @param {string} url Optionally specify the url as the first argument + * @param {Object} options {@see jQuery#ajax} + * @return {XmlHttpRequest} + */ +elgg.post = function(url, options) { +	options = elgg.ajax.handleOptions(url, options); + +	options.type = 'post'; +	return elgg.ajax(options); +}; + +/** + * Perform an action via ajax + * + * @example Usage 1: + * At its simplest, only the action name is required (and anything more than the + * action name will be invalid). + * <pre> + * elgg.action('name/of/action'); + * </pre> + * + * The action can be relative to the current site ('name/of/action') or + * the full URL of the action ('http://elgg.org/action/name/of/action'). + * + * @example Usage 2: + * If you want to pass some data along with it, use the second parameter + * <pre> + * elgg.action('friend/add', { friend: some_guid }); + * </pre> + * + * @example Usage 3: + * Of course, you will have no control over what happens when the request + * completes if you do it like that, so there's also the most verbose method + * <pre> + * elgg.action('friend/add', { + *     data: { + *         friend: some_guid + *     }, + *     success: function(json) { + *         //do something + *     }, + * } + * </pre> + * You can pass any of your favorite $.ajax arguments into this second parameter. + * + * @note If you intend to use the second field in the "verbose" way, you must + * specify a callback method or the data parameter.  If you do not, elgg.action + * will think you mean to send the second parameter as data. + * + * @note You do not have to add security tokens to this request.  Elgg does that + * for you automatically. + * + * @see jQuery.ajax + * + * @param {String} action The action to call. + * @param {Object} options + * @return {XMLHttpRequest} + */ +elgg.action = function(action, options) { +	elgg.assertTypeOf('string', action); + +	// support shortcut and full URLs +	// this will mangle URLs that aren't elgg actions. +	// Use post, get, or ajax for those. +	if (action.indexOf('action/') < 0) { +		action = 'action/' + action; +	} + +	options = elgg.ajax.handleOptions(action, options); + +	// This is a misuse of elgg.security.addToken() because it is not always a +	// full query string with a ?. As such we need a special check for the tokens. +	if (!elgg.isString(options.data) || options.data.indexOf('__elgg_ts') == -1) { +		options.data = elgg.security.addToken(options.data); +	} +	options.dataType = 'json'; + +	//Always display system messages after actions +	var custom_success = options.success || elgg.nullFunction; +	options.success = function(json, two, three, four) { +		if (json && json.system_messages) { +			elgg.register_error(json.system_messages.error); +			elgg.system_message(json.system_messages.success); +		} + +		custom_success(json, two, three, four); +	}; + +	return elgg.post(options); +}; + +/** + * Make an API call + * + * @example Usage: + * <pre> + * elgg.api('system.api.list', { + *     success: function(data) { + *         console.log(data); + *     } + * }); + * </pre> + * + * @param {String} method The API method to be called + * @param {Object} options {@see jQuery#ajax} + * @return {XmlHttpRequest} + */ +elgg.api = function (method, options) { +	elgg.assertTypeOf('string', method); + +	var defaults = { +		dataType: 'json', +		data: {} +	}; + +	options = elgg.ajax.handleOptions(method, options); +	options = $.extend(defaults, options); + +	options.url = 'services/api/rest/' + options.dataType + '/'; +	options.data.method = method; + +	return elgg.ajax(options); +}; diff --git a/js/lib/configuration.js b/js/lib/configuration.js new file mode 100644 index 000000000..6e221c957 --- /dev/null +++ b/js/lib/configuration.js @@ -0,0 +1,10 @@ +elgg.provide('elgg.config'); + +/** + * Returns the current site URL + * + * @return {String} The site URL. + */ +elgg.get_site_url = function() { +	return elgg.config.wwwroot; +};
\ No newline at end of file diff --git a/js/lib/elgglib.js b/js/lib/elgglib.js new file mode 100644 index 000000000..a8e187f1d --- /dev/null +++ b/js/lib/elgglib.js @@ -0,0 +1,563 @@ +/** + * @namespace Singleton object for holding the Elgg javascript library + */ +var elgg = elgg || {}; + +/** + * Pointer to the global context + * + * @see elgg.require + * @see elgg.provide + */ +elgg.global = this; + +/** + * Convenience reference to an empty function. + * + * Save memory by not generating multiple empty functions. + */ +elgg.nullFunction = function() {}; + +/** + * This forces an inheriting class to implement the method or + * it will throw an error. + * + * @example + * AbstractClass.prototype.toBeImplemented = elgg.abstractMethod; + */ +elgg.abstractMethod = function() { +	throw new Error("Oops... you forgot to implement an abstract method!"); +}; + +/** + * Merges two or more objects together and returns the result. + */ +elgg.extend = jQuery.extend; + +/** + * Check if the value is an array. + * + * No sense in reinventing the wheel! + * + * @param {*} val + * + * @return boolean + */ +elgg.isArray = jQuery.isArray; + +/** + * Check if the value is a function. + * + * No sense in reinventing the wheel! + * + * @param {*} val + * + * @return boolean + */ +elgg.isFunction = jQuery.isFunction; + +/** + * Check if the value is a "plain" object (i.e., created by {} or new Object()) + * + * No sense in reinventing the wheel! + * + * @param {*} val + * + * @return boolean + */ +elgg.isPlainObject = jQuery.isPlainObject; + +/** + * Check if the value is a string + * + * @param {*} val + * + * @return boolean + */ +elgg.isString = function(val) { +	return typeof val === 'string'; +}; + +/** + * Check if the value is a number + * + * @param {*} val + * + * @return boolean + */ +elgg.isNumber = function(val) { +	return typeof val === 'number'; +}; + +/** + * Check if the value is an object + * + * @note This returns true for functions and arrays!  If you want to return true only + * for "plain" objects (created using {} or new Object()) use elgg.isPlainObject. + * + * @param {*} val + * + * @return boolean + */ +elgg.isObject = function(val) { +	return typeof val === 'object'; +}; + +/** + * Check if the value is undefined + * + * @param {*} val + * + * @return boolean + */ +elgg.isUndefined = function(val) { +	return val === undefined; +}; + +/** + * Check if the value is null + * + * @param {*} val + * + * @return boolean + */ +elgg.isNull = function(val) { +	return val === null; +}; + +/** + * Check if the value is either null or undefined + * + * @param {*} val + * + * @return boolean + */ +elgg.isNullOrUndefined = function(val) { +	return val == null; +}; + +/** + * Throw an exception of the type doesn't match + * + * @todo Might be more appropriate for debug mode only? + */ +elgg.assertTypeOf = function(type, val) { +	if (typeof val !== type) { +		throw new TypeError("Expecting param of " + +							arguments.caller + "to be a(n) " + type + "." + +							"  Was actually a(n) " + typeof val + "."); +	} +}; + +/** + * Throw an error if the required package isn't present + * + * @param {String} pkg The required package (e.g., 'elgg.package') + */ +elgg.require = function(pkg) { +	elgg.assertTypeOf('string', pkg); + +	var parts = pkg.split('.'), +		cur = elgg.global, +		part, i; + +	for (i = 0; i < parts.length; i += 1) { +		part = parts[i]; +		cur = cur[part]; +		if (elgg.isUndefined(cur)) { +			throw new Error("Missing package: " + pkg); +		} +	} +}; + +/** + * Generate the skeleton for a package. + * + * <pre> + * elgg.provide('elgg.package.subpackage'); + * </pre> + * + * is equivalent to + * + * <pre> + * elgg = elgg || {}; + * elgg.package = elgg.package || {}; + * elgg.package.subpackage = elgg.package.subpackage || {}; + * </pre> + * + * @example elgg.provide('elgg.config.translations') + * + * @param {string} pkg The package name. + */ +elgg.provide = function(pkg, opt_context) { +	elgg.assertTypeOf('string', pkg); + +	var parts = pkg.split('.'), +		context = opt_context || elgg.global, +		part, i; + + +	for (i = 0; i < parts.length; i += 1) { +		part = parts[i]; +		context[part] = context[part] || {}; +		context = context[part]; +	} +}; + +/** + * Inherit the prototype methods from one constructor into another. + * + * @example + * <pre> + * function ParentClass(a, b) { } + * + * ParentClass.prototype.foo = function(a) { alert(a); } + * + * function ChildClass(a, b, c) { + *     //equivalent of parent::__construct(a, b); in PHP + *     ParentClass.call(this, a, b); + * } + * + * elgg.inherit(ChildClass, ParentClass); + * + * var child = new ChildClass('a', 'b', 'see'); + * child.foo('boo!'); // alert('boo!'); + * </pre> + * + * @param {Function} Child Child class constructor. + * @param {Function} Parent Parent class constructor. + */ +elgg.inherit = function(Child, Parent) { +	Child.prototype = new Parent(); +	Child.prototype.constructor = Child; +}; + +/** + * Converts shorthand urls to absolute urls. + * + * If the url is already absolute or protocol-relative, no change is made. + * + * elgg.normalize_url('');                   // 'http://my.site.com/' + * elgg.normalize_url('dashboard');          // 'http://my.site.com/dashboard' + * elgg.normalize_url('http://google.com/'); // no change + * elgg.normalize_url('//google.com/');      // no change + * + * @param {String} url The url to normalize + * @return {String} The extended url + * @private + */ +elgg.normalize_url = function(url) { +	url = url || ''; +	elgg.assertTypeOf('string', url); + +	validated = (function(url) { +		url = elgg.parse_url(url); +		if (url.scheme){ +			url.scheme = url.scheme.toLowerCase(); +		} +		if (url.scheme == 'http' || url.scheme == 'https') { +			if (!url.host) { +				return false; +			} +			/* hostname labels may contain only alphanumeric characters, dots and hypens. */ +			if (!(new RegExp("^([a-zA-Z0-9][a-zA-Z0-9\\-\\.]*)$", "i")).test(url.host) || url.host.charAt(-1) == '.') { +				return false; +			} +		} +		/* some schemas allow the host to be empty */ +		if (!url.scheme || !url.host && url.scheme != 'mailto' && url.scheme != 'news' && url.scheme != 'file') { +			return false; +		} +		return true; +	})(url); + +	// all normal URLs including mailto: +	if (validated) {		 +		return url; +	} + +	// '//example.com' (Shortcut for protocol.) +	// '?query=test', #target +	else if ((new RegExp("^(\\#|\\?|//)", "i")).test(url)) { +		return url; +	} + +	// 'javascript:' +	else if (url.indexOf('javascript:') === 0 || url.indexOf('mailto:') === 0 ) { +		return url; +	} + +	// watch those double escapes in JS. + +	// 'install.php', 'install.php?step=step' +	else if ((new RegExp("^[^\/]*\\.php(\\?.*)?$", "i")).test(url)) { +		return elgg.config.wwwroot + url.ltrim('/'); +	} + +	// 'example.com', 'example.com/subpage' +	else if ((new RegExp("^[^/]*\\.", "i")).test(url)) { +		return 'http://' + url; +	} + +	// 'page/handler', 'mod/plugin/file.php' +	else { +		// trim off any leading / because the site URL is stored +		// with a trailing / +		return elgg.config.wwwroot + url.ltrim('/'); +	} +}; + +/** + * Displays system messages via javascript rather than php. + * + * @param {String} msgs The message we want to display + * @param {Number} delay The amount of time to display the message in milliseconds. Defaults to 6 seconds. + * @param {String} type The type of message (typically 'error' or 'message') + * @private + */ +elgg.system_messages = function(msgs, delay, type) { +	if (elgg.isUndefined(msgs)) { +		return; +	} + +	var classes = ['elgg-message'], +		messages_html = [], +		appendMessage = function(msg) { +			messages_html.push('<li class="' + classes.join(' ') + '"><p>' + msg + '</p></li>'); +		}, +		systemMessages = $('ul.elgg-system-messages'), +		i; + +	//validate delay.  Must be a positive integer. +	delay = parseInt(delay || 6000, 10); +	if (isNaN(delay) || delay <= 0) { +		delay = 6000; +	} + +	//Handle non-arrays +	if (!elgg.isArray(msgs)) { +		msgs = [msgs]; +	} + +	if (type === 'error') { +		classes.push('elgg-state-error'); +	} else { +		classes.push('elgg-state-success'); +	} + +	msgs.forEach(appendMessage); + +	if (type != 'error') { +		$(messages_html.join('')).appendTo(systemMessages) +			.animate({opacity: '1.0'}, delay).fadeOut('slow'); +	} else { +		$(messages_html.join('')).appendTo(systemMessages); +	} +}; + +/** + * Wrapper function for system_messages. Specifies "messages" as the type of message + * @param {String} msgs  The message to display + * @param {Number} delay How long to display the message (milliseconds) + */ +elgg.system_message = function(msgs, delay) { +	elgg.system_messages(msgs, delay, "message"); +}; + +/** + * Wrapper function for system_messages.  Specifies "errors" as the type of message + * @param {String} errors The error message to display + * @param {Number} delay  How long to dispaly the error message (milliseconds) + */ +elgg.register_error = function(errors, delay) { +	elgg.system_messages(errors, delay, "error"); +}; + +/** + * Meant to mimic the php forward() function by simply redirecting the + * user to another page. + * + * @param {String} url The url to forward to + */ +elgg.forward = function(url) { +	location.href = elgg.normalize_url(url); +}; + +/** + * Parse a URL into its parts. Mimicks http://php.net/parse_url + * + * @param {String} url       The URL to parse + * @param {Int}    component A component to return + * @param {Bool}   expand    Expand the query into an object? Else it's a string. + * + * @return {Object} The parsed URL + */ +elgg.parse_url = function(url, component, expand) { +	// Adapted from http://blog.stevenlevithan.com/archives/parseuri +	// which was release under the MIT +	// It was modified to fix mailto: and javascript: support. +	var +	expand = expand || false, +	component = component || false, +	 +	re_str = +		// scheme (and user@ testing) +		'^(?:(?![^:@]+:[^:@/]*@)([^:/?#.]+):)?(?://)?' +		// possibly a user[:password]@ +		+ '((?:(([^:@]*)(?::([^:@]*))?)?@)?' +		// host and port +		+ '([^:/?#]*)(?::(\\d*))?)' +		// path +		+ '(((/(?:[^?#](?![^?#/]*\\.[^?#/.]+(?:[?#]|$)))*/?)?([^?#/]*))' +		// query string +		+ '(?:\\?([^#]*))?' +		// fragment +		+ '(?:#(.*))?)', +	keys = { +			1: "scheme", +			4: "user", +			5: "pass", +			6: "host", +			7: "port", +			9: "path", +			12: "query", +			13: "fragment" +	}, +	results = {}; + +	if (url.indexOf('mailto:') === 0) { +		results['scheme'] = 'mailto'; +		results['path'] = url.replace('mailto:', ''); +		return results; +	} + +	if (url.indexOf('javascript:') === 0) { +		results['scheme'] = 'javascript'; +		results['path'] = url.replace('javascript:', ''); +		return results; +	} + +	var re = new RegExp(re_str); +	var matches = re.exec(url); + +	for (var i in keys) { +		if (matches[i]) { +			results[keys[i]] = matches[i]; +		} +	} + +	if (expand && typeof(results['query']) != 'undefined') { +		results['query'] = elgg.parse_str(results['query']); +	} + +	if (component) { +		if (typeof(results[component]) != 'undefined') { +			return results[component]; +		} else { +			return false; +		} +	} +	return results; +}; + +/** + * Returns an object with key/values of the parsed query string. + * + * @param  {String} string The string to parse + * @return {Object} The parsed object string + */ +elgg.parse_str = function(string) { +	var params = {}; +	var result, +		key, +		value, +		re = /([^&=]+)=?([^&]*)/g; + +	while (result = re.exec(string)) { +		key = decodeURIComponent(result[1].replace(/\+/g, ' ')); +		value = decodeURIComponent(result[2].replace(/\+/g, ' ')); +		params[key] = value; +	} +	 +	return params; +}; + +/** + * Returns a jQuery selector from a URL's fragement.  Defaults to expecting an ID. + * + * Examples: + *  http://elgg.org/download.php returns '' + *	http://elgg.org/download.php#id returns #id + *	http://elgg.org/download.php#.class-name return .class-name + *	http://elgg.org/download.php#a.class-name return a.class-name + * + * @param {String} url The URL + * @return {String} The selector + */ +elgg.getSelectorFromUrlFragment = function(url) { +	var fragment = url.split('#')[1]; + +	if (fragment) { +		// this is a .class or a tag.class +		if (fragment.indexOf('.') > -1) { +			return fragment; +		} + +		// this is an id +		else { +			return '#' + fragment; +		} +	} +	return ''; +}; + +/** + * Adds child to object[parent] array. + * + * @param {Object} object The object to add to + * @param {String} parent The parent array to add to. + * @param {Mixed}  value  The value + */ +elgg.push_to_object_array = function(object, parent, value) { +	elgg.assertTypeOf('object', object); +	elgg.assertTypeOf('string', parent); + +	if (!(object[parent] instanceof Array)) { +		object[parent] = [] +	} + +	if ($.inArray(value, object[parent]) < 0) { +		return object[parent].push(value); +	} + +	return false; +}; + +/** + * Tests if object[parent] contains child + * + * @param {Object} object The object to add to + * @param {String} parent The parent array to add to. + * @param {Mixed}  value  The value + */ +elgg.is_in_object_array = function(object, parent, value) { +	elgg.assertTypeOf('object', object); +	elgg.assertTypeOf('string', parent); + +	return typeof(object[parent]) != 'undefined' && $.inArray(value, object[parent]) >= 0; +}; + +/** + * Triggers the init hook when the library is ready + * + * Current requirements: + * - DOM is ready + * - languages loaded + * + */ +elgg.initWhenReady = function() { +	if (elgg.config.languageReady && elgg.config.domReady) { +		elgg.trigger_hook('init', 'system'); +		elgg.trigger_hook('ready', 'system'); +	} +}; diff --git a/js/lib/hooks.js b/js/lib/hooks.js new file mode 100644 index 000000000..5e1808e22 --- /dev/null +++ b/js/lib/hooks.js @@ -0,0 +1,173 @@ +/* + * Javascript hook interface + */ + +elgg.provide('elgg.config.hooks'); +elgg.provide('elgg.config.instant_hooks'); +elgg.provide('elgg.config.triggered_hooks'); + +/** + * Registers a hook handler with the event system. + * + * The special keyword "all" can be used for either the name or the type or both + * and means to call that handler for all of those hooks. + * + * Note that handlers registering for instant hooks will be executed immediately if the instant + * hook has been previously triggered. + * + * @param {String}   name     Name of the plugin hook to register for + * @param {String}   type     Type of the event to register for + * @param {Function} handler  Handle to call + * @param {Number}   priority Priority to call the event handler + * @return {Bool} + */ +elgg.register_hook_handler = function(name, type, handler, priority) { +	elgg.assertTypeOf('string', name); +	elgg.assertTypeOf('string', type); +	elgg.assertTypeOf('function', handler); + +	if (!name || !type) { +		return false; +	} + +	var priorities =  elgg.config.hooks; + +	elgg.provide(name + '.' + type, priorities); + +	if (!(priorities[name][type] instanceof elgg.ElggPriorityList)) { +		priorities[name][type] = new elgg.ElggPriorityList(); +	} + +	// call if instant and already triggered. +	if (elgg.is_instant_hook(name, type) && elgg.is_triggered_hook(name, type)) { +		handler(name, type, null, null); +	} + +	return priorities[name][type].insert(handler, priority); +}; + +/** + * Emits a hook. + * + * Loops through all registered hooks and calls the handler functions in order. + * Every handler function will always be called, regardless of the return value. + * + * @warning Handlers take the same 4 arguments in the same order as when calling this function. + * This is different from the PHP version! + * + * @note Instant hooks do not support params or values. + * + * Hooks are called in this order: + *	specifically registered (event_name and event_type match) + *	all names, specific type + *	specific name, all types + *	all names, all types + * + * @param {String} name   Name of the hook to emit + * @param {String} type   Type of the hook to emit + * @param {Object} params Optional parameters to pass to the handlers + * @param {Object} value  Initial value of the return. Can be mangled by handlers + * + * @return {Bool} + */ +elgg.trigger_hook = function(name, type, params, value) { +	elgg.assertTypeOf('string', name); +	elgg.assertTypeOf('string', type); + +	// mark as triggered +	elgg.set_triggered_hook(name, type); + +	// default to true if unpassed +	value = value || true; + +	var hooks = elgg.config.hooks, +		tempReturnValue = null, +		returnValue = value, +		callHookHandler = function(handler) { +			tempReturnValue = handler(name, type, params, value); +		}; + +	elgg.provide(name + '.' + type, hooks); +	elgg.provide('all.' + type, hooks); +	elgg.provide(name + '.all', hooks); +	elgg.provide('all.all', hooks); + +	var hooksList = []; +	 +	if (name != 'all' && type != 'all') { +		hooksList.push(hooks[name][type]); +	} + +	if (type != 'all') { +		hooksList.push(hooks['all'][type]); +	} + +	if (name != 'all') { +		hooksList.push(hooks[name]['all']); +	} + +	hooksList.push(hooks['all']['all']); + +	hooksList.every(function(handlers) { +		if (handlers instanceof elgg.ElggPriorityList) { +			handlers.forEach(callHookHandler); +		} +		return true; +	}); + +	return (tempReturnValue != null) ? tempReturnValue : returnValue; +}; + +/** + * Registers a hook as an instant hook. + * + * After being trigger once, registration of a handler to an instant hook will cause the + * handle to be executed immediately. + * + * @note Instant hooks must be triggered without params or defaults. Any params or default + * passed will *not* be passed to handlers executed upon registration. + * + * @param {String} name The hook name. + * @param {String} type The hook type. + * @return {Int} + */ +elgg.register_instant_hook = function(name, type) { +	elgg.assertTypeOf('string', name); +	elgg.assertTypeOf('string', type); + +	return elgg.push_to_object_array(elgg.config.instant_hooks, name, type); +}; + +/** + * Is this hook registered as an instant hook? + * + * @param {String} name The hook name. + * @param {String} type The hook type. + */ +elgg.is_instant_hook = function(name, type) { +	return elgg.is_in_object_array(elgg.config.instant_hooks, name, type); +}; + +/** + * Records that a hook has been triggered. + * + * @param {String} name The hook name. + * @param {String} type The hook type. + */ +elgg.set_triggered_hook = function(name, type) { +	return elgg.push_to_object_array(elgg.config.triggered_hooks, name, type); +}; + +/** + * Has this hook been triggered yet? + * + * @param {String} name The hook name. + * @param {String} type The hook type. + */ +elgg.is_triggered_hook = function(name, type) { +	return elgg.is_in_object_array(elgg.config.triggered_hooks, name, type); +}; + +elgg.register_instant_hook('init', 'system'); +elgg.register_instant_hook('ready', 'system'); +elgg.register_instant_hook('boot', 'system'); diff --git a/js/lib/languages.js b/js/lib/languages.js new file mode 100644 index 000000000..d218cbc4f --- /dev/null +++ b/js/lib/languages.js @@ -0,0 +1,96 @@ +/*globals vsprintf*/ +/** + * Provides language-related functionality + */ +elgg.provide('elgg.config.translations'); + +// default language - required by unit tests +elgg.config.language = 'en'; + +/** + * Analagous to the php version.  Merges translations for a + * given language into the current translations map. + */ +elgg.add_translation = function(lang, translations) { +	elgg.provide('elgg.config.translations.' + lang); + +	elgg.extend(elgg.config.translations[lang], translations); +}; + +/** + * Load the translations for the given language. + * + * If no language is specified, the default language is used. + * @param {string} language + * @return {XMLHttpRequest} + */ +elgg.reload_all_translations = function(language) { +	var lang = language || elgg.get_language(); + +	var url, options; +	url = 'ajax/view/js/languages'; +	options = {data: {language: lang}}; +    if (elgg.config.simplecache_enabled) { +        options.data.lc = elgg.config.lastcache; +    } + +	options['success'] = function(json) { +		elgg.add_translation(lang, json); +		elgg.config.languageReady = true; +		elgg.initWhenReady(); +	}; + +	elgg.getJSON(url, options); +}; + +/** + * Get the current language + * @return {String} + */ +elgg.get_language = function() { +	var user = elgg.get_logged_in_user_entity(); + +	if (user && user.language) { +		return user.language; +	} + +	return elgg.config.language; +}; + +/** + * Translates a string + * + * @param {String} key      The string to translate + * @param {Array}  argv     vsprintf support + * @param {String} language The language to display it in + * + * @return {String} The translation + */ +elgg.echo = function(key, argv, language) { +	//elgg.echo('str', 'en') +	if (elgg.isString(argv)) { +		language = argv; +		argv = []; +	} + +	//elgg.echo('str', [...], 'en') +	var translations = elgg.config.translations, +		dlang = elgg.get_language(), +		map; + +	language = language || dlang; +	argv = argv || []; + +	map = translations[language] || translations[dlang]; +	if (map && map[key]) { +		return vsprintf(map[key], argv); +	} + +	return key; +}; + +elgg.config.translations.init = function() { +	elgg.reload_all_translations(); +}; + +elgg.register_hook_handler('boot', 'system', elgg.config.translations.init);
\ No newline at end of file diff --git a/js/lib/pageowner.js b/js/lib/pageowner.js new file mode 100644 index 000000000..c695c41c3 --- /dev/null +++ b/js/lib/pageowner.js @@ -0,0 +1,18 @@ +/** + * Provides page owner and context functions + * + * @todo This is a stub. Page owners can't be fully implemented until + * the 4 types are finished. + */ + +/** + * @return {number} The GUID of the page owner entity or 0 for no owner + */ +elgg.get_page_owner_guid = function() { +	if (elgg.page_owner !== undefined) { +		return elgg.page_owner.guid; +	} else { +		return 0; +	} +}; + diff --git a/js/lib/prototypes.js b/js/lib/prototypes.js new file mode 100644 index 000000000..cb6184097 --- /dev/null +++ b/js/lib/prototypes.js @@ -0,0 +1,66 @@ +/** + * Interates through each element of an array and calls a callback function. + * The callback should accept the following arguments: + *	element - The current element + *	index	- The current index + * + * This is different to Array.forEach in that if the callback returns false, the loop returns + * immediately without processing the remaining elements. + * + *	@param {Function} callback + *	@return {Bool} + */ +if (!Array.prototype.every) { +	Array.prototype.every = function(callback) { +		var len = this.length, i; + +		for (i = 0; i < len; i++) { +			if (i in this && !callback.call(null, this[i], i)) { +				return false; +			} +		} + +		return true; +	}; +} + +/** + * Interates through each element of an array and calls callback a function. + * The callback should accept the following arguments: + *	element - The current element + *	index	- The current index + * + * This is different to Array.every in that the callback's return value is ignored and every + * element of the array will be parsed. + * + *	@param {Function} callback + *	@return {Void} + */ +if (!Array.prototype.forEach) { +	Array.prototype.forEach = function(callback) { +		var len = this.length, i; + +		for (i = 0; i < len; i++) { +			if (i in this) { +				callback.call(null, this[i], i); +			} +		} +	}; +} + +/** + * Left trim + * + * Removes a character from the left side of a string. + * @param {String} str The character to remove + * @return {String} + */ +if (!String.prototype.ltrim) { +	String.prototype.ltrim = function(str) { +		if (this.indexOf(str) === 0) { +			return this.substring(str.length); +		} else { +			return this; +		} +	}; +}
\ No newline at end of file diff --git a/js/lib/security.js b/js/lib/security.js new file mode 100644 index 000000000..9c12f8586 --- /dev/null +++ b/js/lib/security.js @@ -0,0 +1,107 @@ +/** + * Hold security-related data here + */ +elgg.provide('elgg.security'); + +elgg.security.token = {}; + +elgg.security.tokenRefreshFailed = false; + +elgg.security.tokenRefreshTimer = null; + +/** + * Sets the currently active security token and updates all forms and links on the current page. + * + * @param {Object} json The json representation of a token containing __elgg_ts and __elgg_token + * @return {Void} + */ +elgg.security.setToken = function(json) {	 +	//update the convenience object +	elgg.security.token = json; + +	//also update all forms +	$('[name=__elgg_ts]').val(json.__elgg_ts); +	$('[name=__elgg_token]').val(json.__elgg_token); + +	// also update all links that contain tokens and time stamps +	$('[href*="__elgg_ts"][href*="__elgg_token"]').each(function() { +		this.href = this.href +			.replace(/__elgg_ts=\d*/, '__elgg_ts=' + json.__elgg_ts) +			.replace(/__elgg_token=[0-9a-f]*/, '__elgg_token=' + json.__elgg_token); +	}); +}; + +/** + * Security tokens time out so we refresh those every so often. + *  + * @private + */ +elgg.security.refreshToken = function() { +	elgg.action('security/refreshtoken', function(data) { +		if (data && data.output.__elgg_ts && data.output.__elgg_token) { +			elgg.security.setToken(data.output); +		} else { +			clearInterval(elgg.security.tokenRefreshTimer); +		} +	}); +}; + + +/** + * Add elgg action tokens to an object, URL, or query string (with a ?). + * + * @param {Object|string} data + * @return {Object} The new data object including action tokens + * @private + */ +elgg.security.addToken = function(data) { + +	// 'http://example.com?data=sofar' +	if (elgg.isString(data)) { +		// is this a full URL, relative URL, or just the query string? +		var parts = elgg.parse_url(data), +			args = {}, +			base = ''; +		 +		if (parts['host'] == undefined) { +			if (data.indexOf('?') === 0) { +				// query string +				base = '?'; +				args = elgg.parse_str(parts['query']); +			} +		} else { +			// full or relative URL + +			if (parts['query'] != undefined) { +				// with query string +				args = elgg.parse_str(parts['query']); +			} +			var split = data.split('?'); +			base = split[0] + '?'; +		} +		args["__elgg_ts"] = elgg.security.token.__elgg_ts; +		args["__elgg_token"] = elgg.security.token.__elgg_token; + +		return base + jQuery.param(args); +	} + +	// no input!  acts like a getter +	if (elgg.isUndefined(data)) { +		return elgg.security.token; +	} + +	// {...} +	if (elgg.isPlainObject(data)) { +		return elgg.extend(data, elgg.security.token); +	} + +	// oops, don't recognize that! +	throw new TypeError("elgg.security.addToken not implemented for " + (typeof data) + "s"); +}; + +elgg.security.init = function() { +	// elgg.security.interval is set in the js/elgg PHP view. +	elgg.security.tokenRefreshTimer = setInterval(elgg.security.refreshToken, elgg.security.interval); +}; + +elgg.register_hook_handler('boot', 'system', elgg.security.init);
\ No newline at end of file diff --git a/js/lib/session.js b/js/lib/session.js new file mode 100644 index 000000000..a8d52733c --- /dev/null +++ b/js/lib/session.js @@ -0,0 +1,123 @@ +/** + * Provides session methods. + */ +elgg.provide('elgg.session'); + +/** + * Helper function for setting cookies + * @param {string} name + * @param {string} value + * @param {Object} options + *  + *  {number|Date} options[expires] + * 	{string} options[path] + * 	{string} options[domain] + * 	{boolean} options[secure] + *  + * @return {string|undefined} The value of the cookie, if only name is specified. Undefined if no value set + */ +elgg.session.cookie = function(name, value, options) { +	var cookies = [], cookie = [], i = 0, date, valid = true; +	 +	//elgg.session.cookie() +	if (elgg.isUndefined(name)) { +		return document.cookie; +	} +	 +	//elgg.session.cookie(name) +	if (elgg.isUndefined(value)) { +		if (document.cookie && document.cookie !== '') { +			cookies = document.cookie.split(';'); +			for (i = 0; i < cookies.length; i += 1) { +				cookie = jQuery.trim(cookies[i]).split('='); +				if (cookie[0] === name) { +					return decodeURIComponent(cookie[1]); +				} +			} +		} +		return undefined; +	} +	 +	// elgg.session.cookie(name, value[, opts]) +	options = options || {}; +	 +	if (elgg.isNull(value)) { +		value = ''; +		options.expires = -1; +	} +	 +	cookies.push(name + '=' + value); + +    if (options.expires) { +        if (elgg.isNumber(options.expires)) { +            date = new Date(); +            date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); +        } else if (options.expires.toUTCString) { +            date = options.expires; +        } + +        if (date) { +            cookies.push('expires=' + date.toUTCString()); +        } +    } +	 +	// CAUTION: Needed to parenthesize options.path and options.domain +	// in the following expressions, otherwise they evaluate to undefined +	// in the packed version for some reason. +	if (options.path) { +		cookies.push('path=' + (options.path)); +	} + +	if (options.domain) { +		cookies.push('domain=' + (options.domain)); +	} +	 +	if (options.secure) { +		cookies.push('secure'); +	} +	 +	document.cookie = cookies.join('; '); +}; + +/** + * Returns the object of the user logged in. + * + * @return {ElggUser} The logged in user + */ +elgg.get_logged_in_user_entity = function() { +	return elgg.session.user; +}; + +/** + * Returns the GUID of the logged in user or 0. + * + * @return {number} The GUID of the logged in user + */ +elgg.get_logged_in_user_guid = function() { +	var user = elgg.get_logged_in_user_entity(); +	return user ? user.guid : 0; +}; + +/** + * Returns if a user is logged in. + * + * @return {boolean} Whether there is a user logged in + */ +elgg.is_logged_in = function() { +	return (elgg.get_logged_in_user_entity() instanceof elgg.ElggUser); +}; + +/** + * Returns if the currently logged in user is an admin. + * + * @return {boolean} Whether there is an admin logged in + */ +elgg.is_admin_logged_in = function() { +	var user = elgg.get_logged_in_user_entity(); +	return (user instanceof elgg.ElggUser) && user.isAdmin(); +}; + +/** + * @deprecated Use elgg.session.cookie instead + */ +jQuery.cookie = elgg.session.cookie;
\ No newline at end of file diff --git a/js/lib/ui.autocomplete.js b/js/lib/ui.autocomplete.js new file mode 100644 index 000000000..46d72d146 --- /dev/null +++ b/js/lib/ui.autocomplete.js @@ -0,0 +1,14 @@ +/** + *  + */ +elgg.provide('elgg.autocomplete'); + +elgg.autocomplete.init = function() { +	$('.elgg-input-autocomplete').autocomplete({ +		source: elgg.autocomplete.url, //gets set by input/autocomplete view +		minLength: 2, +		html: "html" +	}) +}; + +elgg.register_hook_handler('init', 'system', elgg.autocomplete.init);
\ No newline at end of file diff --git a/js/lib/ui.avatar_cropper.js b/js/lib/ui.avatar_cropper.js new file mode 100644 index 000000000..fc32a0832 --- /dev/null +++ b/js/lib/ui.avatar_cropper.js @@ -0,0 +1,76 @@ +/** + * Avatar cropping + */ + +elgg.provide('elgg.avatarCropper'); + +/** + * Register the avatar cropper. + * + * If the hidden inputs have the coordinates from a previous cropping, begin + * the selection and preview with that displayed. + */ +elgg.avatarCropper.init = function() { +	var params = { +		selectionOpacity: 0, +		aspectRatio: '1:1', +		onSelectEnd: elgg.avatarCropper.selectChange, +		onSelectChange: elgg.avatarCropper.preview +	}; + +	if ($('input[name=x2]').val()) { +		params.x1 = $('input[name=x1]').val(); +		params.x2 = $('input[name=x2]').val(); +		params.y1 = $('input[name=y1]').val(); +		params.y2 = $('input[name=y2]').val(); +	} + +	$('#user-avatar-cropper').imgAreaSelect(params); + +	if ($('input[name=x2]').val()) { +		var ias = $('#user-avatar-cropper').imgAreaSelect({instance: true}); +		var selection = ias.getSelection(); +		elgg.avatarCropper.preview($('#user-avatar-cropper'), selection); +	} +}; + +/** + * Handler for changing select area. + * + * @param {Object} reference to the image + * @param {Object} imgareaselect selection object + * @return void + */ +elgg.avatarCropper.preview = function(img, selection) { +	// catch for the first click on the image +	if (selection.width == 0 || selection.height == 0) { +		return; +	} + +	var origWidth = $("#user-avatar-cropper").width(); +	var origHeight = $("#user-avatar-cropper").height(); +	var scaleX = 100 / selection.width; +	var scaleY = 100 / selection.height; +	$('#user-avatar-preview > img').css({ +		width: Math.round(scaleX * origWidth) + 'px', +		height: Math.round(scaleY * origHeight) + 'px', +		marginLeft: '-' + Math.round(scaleX * selection.x1) + 'px', +		marginTop: '-' + Math.round(scaleY * selection.y1) + 'px' +	}); +}; + +/** + * Handler for updating the form inputs after select ends + * + * @param {Object} reference to the image + * @param {Object} imgareaselect selection object + * @return void + */ +elgg.avatarCropper.selectChange = function(img, selection) { +	$('input[name=x1]').val(selection.x1); +	$('input[name=x2]').val(selection.x2); +	$('input[name=y1]').val(selection.y1); +	$('input[name=y2]').val(selection.y2); +}; + +elgg.register_hook_handler('init', 'system', elgg.avatarCropper.init);
\ No newline at end of file diff --git a/js/lib/ui.friends_picker.js b/js/lib/ui.friends_picker.js new file mode 100644 index 000000000..9257c40fc --- /dev/null +++ b/js/lib/ui.friends_picker.js @@ -0,0 +1,91 @@ +/* +	elgg friendsPicker plugin +	adapted from Niall Doherty's excellent Coda-Slider - http://www.ndoherty.com/coda-slider + */ + + +jQuery.fn.friendsPicker = function(iterator) { + +	var settings;  +	settings = $.extend({ easeFunc: "easeOutExpo", easeTime: 1000, toolTip: false }, settings); + +	return this.each(function() { + +		var container = $(this); +		container.addClass("friends-picker"); +		// set panelwidth manually as it's hidden initially - adjust this value for different themes/pagewidths  +		var panelWidth = 730; + +		// count the panels in the container +		var panelCount = container.find("div.panel").size(); +		// calculate the width of all the panels lined up end-to-end +		var friendsPicker_containerWidth = panelWidth*panelCount; +		// specify width for the friendsPicker_container +		container.find("div.friends-picker-container").css("width" , friendsPicker_containerWidth); + +		// global variables for container.each function below +		var friendsPickerNavigationWidth = 0; +		var currentPanel = 1; + +		// generate appropriate nav for each container +		container.each(function(i) { +			// generate Left and Right arrows +			$(this).before("<div class='friends-picker-navigation-l' id='friends-picker-navigation-l" + iterator + "'><a href='#'>Left</a><\/div>"); +			$(this).after("<div class='friends-picker-navigation-r' id='friends-picker-navigation-r" + iterator + "'><a href='#'>Right</a><\/div>"); + +			// generate a-z tabs +			$(this).before("<div class='friends-picker-navigation' id='friends-picker-navigation" + iterator + "'><ul><\/ul><\/div>"); +			$(this).find("div.panel").each(function(individualTabItemNumber) { +				$("div#friends-picker-navigation" + iterator + " ul").append("<li class='tab" + (individualTabItemNumber+1) + "'><a href='#" + (individualTabItemNumber+1) + "'>" + $(this).attr("title") + "<\/a><\/li>");		 +			}); + +			// tabs navigation +			$("div#friends-picker-navigation" + iterator + " a").each(function(individualTabItemNumber) { +				// calc friendsPickerNavigationWidth by summing width of each li +				friendsPickerNavigationWidth += $(this).parent().width(); +				// set-up individual tab clicks +				$(this).bind("click", function() { +					$(this).addClass("current").parent().parent().find("a").not($(this)).removeClass("current");  +					var distanceToMoveFriendsPicker_container = - (panelWidth*individualTabItemNumber); +					currentPanel = individualTabItemNumber + 1; +					$(this).parent().parent().parent().next().find("div.friends-picker-container").animate({ left: distanceToMoveFriendsPicker_container}, settings.easeTime, settings.easeFunc); +				}); +			}); + +			// Right arow click function +			$("div#friends-picker-navigation-r" + iterator + " a").click(function() { +				if (currentPanel == panelCount) { +					var distanceToMoveFriendsPicker_container = 0; +					currentPanel = 1;  +					$(this).parent().parent().find("div.friends-picker-navigation a.current").removeClass("current").parent().parent().find("a:eq(0)").addClass("current"); +				} else { +					var distanceToMoveFriendsPicker_container = - (panelWidth*currentPanel); +					currentPanel += 1; +					$(this).parent().parent().find("div.friends-picker-navigation a.current").removeClass("current").parent().next().find("a").addClass("current"); +				}; +				$(this).parent().parent().find("div.friends-picker-container").animate({ left: distanceToMoveFriendsPicker_container}, settings.easeTime, settings.easeFunc); +				return false; +			}); + +			// Left arrow click function +			$("div#friends-picker-navigation-l" + iterator + " a").click(function() { +				if (currentPanel == 1) { +					var distanceToMoveFriendsPicker_container = - (panelWidth*(panelCount - 1)); +					currentPanel = panelCount; +					$(this).parent().parent().find("div.friends-picker-navigation a.current").removeClass("current").parent().parent().find("li:last a").addClass("current"); +				} else { +					currentPanel -= 1; +					var distanceToMoveFriendsPicker_container = - (panelWidth*(currentPanel - 1)); +					$(this).parent().parent().find("div.friends-picker-navigation a.current").removeClass("current").parent().prev().find("a").addClass("current"); +				}; +				$(this).parent().parent().find("div.friends-picker-container").animate({ left: distanceToMoveFriendsPicker_container}, settings.easeTime, settings.easeFunc); +				return false; +			}); + +			// apply 'current' class to currently selected tab link +			$("div#friends-picker-navigation" + iterator + " a:eq(0)").addClass("current"); +		}); + +		$("div#friends-picker-navigation" + iterator).append("<br />");		 +	}); +};
\ No newline at end of file diff --git a/js/lib/ui.js b/js/lib/ui.js new file mode 100644 index 000000000..413078b4f --- /dev/null +++ b/js/lib/ui.js @@ -0,0 +1,293 @@ +elgg.provide('elgg.ui'); + +elgg.ui.init = function () { +	// add user hover menus +	elgg.ui.initHoverMenu(); + +	//if the user clicks a system message, make it disappear +	$('.elgg-system-messages li').live('click', function() { +		$(this).stop().fadeOut('fast'); +	}); + +	$('.elgg-system-messages li').animate({opacity: 0.9}, 6000); +	$('.elgg-system-messages li.elgg-state-success').fadeOut('slow'); + +	$('[rel=toggle]').live('click', elgg.ui.toggles); + +	$('[rel=popup]').live('click', elgg.ui.popupOpen); + +	$('.elgg-menu-page .elgg-menu-parent').live('click', elgg.ui.toggleMenu); + +	$('.elgg-requires-confirmation').live('click', elgg.ui.requiresConfirmation); + +	$('.elgg-autofocus').focus(); +}; + +/** + * Toggles an element based on clicking a separate element + * + * Use rel="toggle" on the toggler element + * Set the href to target the item you want to toggle (<a rel="toggle" href="#id-of-target">) + * + * @param {Object} event + * @return void + */ +elgg.ui.toggles = function(event) { +	event.preventDefault(); + +	// @todo might want to switch this to elgg.getSelectorFromUrlFragment(). +	var target = $(this).toggleClass('elgg-state-active').attr('href'); + +	$(target).slideToggle('medium'); +}; + +/** + * Pops up an element based on clicking a separate element + * + * Set the rel="popup" on the popper and set the href to target the + * item you want to toggle (<a rel="popup" href="#id-of-target">) + * + * This function emits the getOptions, ui.popup hook that plugins can register for to provide custom + * positioning for elements.  The handler is passed the following params: + *	targetSelector: The selector used to find the popup + *	target:         The popup jQuery element as found by the selector + *	source:         The jquery element whose click event initiated a popup. + * + * The return value of the function is used as the options object to .position(). + * Handles can also return false to abort the default behvior and override it with their own. + * + * @param {Object} event + * @return void + */ +elgg.ui.popupOpen = function(event) { +	event.preventDefault(); +	event.stopPropagation(); + +	var target = elgg.getSelectorFromUrlFragment($(this).toggleClass('elgg-state-active').attr('href')); +	var $target = $(target); + +	// emit a hook to allow plugins to position and control popups +	var params = { +		targetSelector: target, +		target: $target, +		source: $(this) +	}; + +	var options = { +		my: 'center top', +		at: 'center bottom', +		of: $(this), +		collision: 'fit fit' +	} + +	options = elgg.trigger_hook('getOptions', 'ui.popup', params, options); + +	// allow plugins to cancel event +	if (!options) { +		return; +	} + +	// hide if already open +	if ($target.is(':visible')) { +		$target.fadeOut(); +		$('body').die('click', elgg.ui.popupClose); +		return; +	} + +	$target.appendTo('body') +		.fadeIn() +		.position(options); + +	$('body') +		.die('click', elgg.ui.popupClose) +		.live('click', elgg.ui.popupClose); +}; + +/** + * Catches clicks that aren't in a popup and closes all popups. + */ +elgg.ui.popupClose = function(event) { +	$eventTarget = $(event.target); +	var inTarget = false; +	var $popups = $('[rel=popup]'); + +	// if the click event target isn't in a popup target, fade all of them out. +	$popups.each(function(i, e) { +		var target = elgg.getSelectorFromUrlFragment($(e).attr('href')) + ':visible'; +		var $target = $(target); + +		if (!$target.is(':visible')) { +			return; +		} + +		// didn't click inside the target +		if ($eventTarget.closest(target).length > 0) { +			inTarget = true; +			return false; +		} +	}); + +	if (!inTarget) { +		$popups.each(function(i, e) { +			var $e = $(e); +			var $target = $(elgg.getSelectorFromUrlFragment($e.attr('href')) + ':visible'); +			if ($target.length > 0) { +				$target.fadeOut(); +				$e.removeClass('elgg-state-active'); +			} +		}); + +		$('body').die('click', elgg.ui.popClose); +	} +}; + +/** + * Toggles a child menu when the parent is clicked + * + * @param {Object} event + * @return void + */ +elgg.ui.toggleMenu = function(event) { +	$(this).siblings().slideToggle('medium'); +	$(this).toggleClass('elgg-menu-closed elgg-menu-opened'); +	event.preventDefault(); +}; + +/** + * Initialize the hover menu + * + * @param {Object} parent + * @return void + */ +elgg.ui.initHoverMenu = function(parent) { +	if (!parent) { +		parent = document; +	} + +	// avatar image menu link +	$(parent).find(".elgg-avatar").live('mouseover', function() { +		$(this).children(".elgg-icon-hover-menu").show(); +	}) +	.live('mouseout', function() { +		$(this).children(".elgg-icon-hover-menu").hide(); +	}); + + +	// avatar contextual menu +	$(".elgg-avatar > .elgg-icon-hover-menu").live('click', function(e) { +		// check if we've attached the menu to this element already +		var $hovermenu = $(this).data('hovermenu') || null; + +		if (!$hovermenu) { +			$hovermenu = $(this).parent().find(".elgg-menu-hover"); +			$(this).data('hovermenu', $hovermenu); +		} + +		// close hovermenu if arrow is clicked & menu already open +		if ($hovermenu.css('display') == "block") { +			$hovermenu.fadeOut(); +		} else { +			$avatar = $(this).closest(".elgg-avatar"); + +			// @todo Use jQuery-ui position library instead -- much simpler +			var offset = $avatar.offset(); +			var top = $avatar.height() + offset.top + 'px'; +			var left = $avatar.width() - 15 + offset.left + 'px'; + +			$hovermenu.appendTo('body') +					.css('position', 'absolute') +					.css("top", top) +					.css("left", left) +					.fadeIn('normal'); +		} + +		// hide any other open hover menus +		$(".elgg-menu-hover:visible").not($hovermenu).fadeOut(); +	}); + +	// hide avatar menu when user clicks elsewhere +	$(document).click(function(event) { +		if ($(event.target).parents(".elgg-avatar").length == 0) { +			$(".elgg-menu-hover").fadeOut(); +		} +	}); +}; + +/** + * Calls a confirm() and prevents default if denied. + * + * @param {Object} e + * @return void + */ +elgg.ui.requiresConfirmation = function(e) { +	var confirmText = $(this).attr('rel') || elgg.echo('question:areyousure'); +	if (!confirm(confirmText)) { +		e.preventDefault(); +	} +}; + +/** + * Repositions the login popup + * + * @param {String} hook    'getOptions' + * @param {String} type    'ui.popup' + * @param {Object} params  An array of info about the target and source. + * @param {Object} options Options to pass to + * + * @return {Object} + */ +elgg.ui.loginHandler = function(hook, type, params, options) { +	if (params.target.attr('id') == 'login-dropdown-box') { +		options.my = 'right top'; +		options.at = 'right bottom'; +		return options; +	} +	return null; +}; + +/** + * Initialize the date picker + * + * Uses the class .elgg-input-date as the selector. + * + * If the class .elgg-input-timestamp is set on the input element, the onSelect + * method converts the date text to a unix timestamp in seconds. That value is + * stored in a hidden element indicated by the id on the input field. + * + * @return void + */ +elgg.ui.initDatePicker = function() { +	var loadDatePicker = function() { +		$('.elgg-input-date').datepicker({ +			// ISO-8601 +			dateFormat: 'yy-mm-dd', +			onSelect: function(dateText) { +				if ($(this).is('.elgg-input-timestamp')) { +					// convert to unix timestamp +					var dateParts = dateText.split("-"); +					var timestamp = Date.UTC(dateParts[0], dateParts[1] - 1, dateParts[2]); +					timestamp = timestamp / 1000; + +					var id = $(this).attr('id'); +					$('input[name="' + id + '"]').val(timestamp); +				} +			} +		}); +	}; +	 +	if ($('.elgg-input-date').length && elgg.get_language() == 'en') { +		loadDatePicker(); +	} else if ($('.elgg-input-date').length) { +		elgg.get({ +			url: elgg.config.wwwroot + 'vendors/jquery/i18n/jquery.ui.datepicker-'+ elgg.get_language() +'.js', +			dataType: "script", +			cache: true, +			success: loadDatePicker, +			error: loadDatePicker // english language is already loaded. +		}); +	} +}; + +elgg.register_hook_handler('init', 'system', elgg.ui.init); +elgg.register_hook_handler('init', 'system', elgg.ui.initDatePicker); +elgg.register_hook_handler('getOptions', 'ui.popup', elgg.ui.loginHandler); diff --git a/js/lib/ui.river.js b/js/lib/ui.river.js new file mode 100644 index 000000000..c103fabb3 --- /dev/null +++ b/js/lib/ui.river.js @@ -0,0 +1,14 @@ +elgg.provide('elgg.ui.river'); + +elgg.ui.river.init = function() { +	$('#elgg-river-selector').change(function() { +		var url = window.location.href; +		if (window.location.search.length) { +			url = url.substring(0, url.indexOf('?')); +		} +		url += '?' + $(this).val(); +		elgg.forward(url); +	}); +}; + +elgg.register_hook_handler('init', 'system', elgg.ui.river.init);
\ No newline at end of file diff --git a/js/lib/ui.userpicker.js b/js/lib/ui.userpicker.js new file mode 100644 index 000000000..669b84cdb --- /dev/null +++ b/js/lib/ui.userpicker.js @@ -0,0 +1,117 @@ +elgg.provide('elgg.userpicker'); + +/** + * Userpicker initialization + * + * The userpicker is an autocomplete library for selecting multiple users or + * friends. It works in concert with the view input/userpicker. + * + * @return void + */ +elgg.userpicker.init = function() { +	 +	// binding autocomplete. +	// doing this as an each so we can pass this to functions. +	$('.elgg-input-user-picker').each(function() { + +		$(this).autocomplete({ +			source: function(request, response) { + +				var params = elgg.userpicker.getSearchParams(this); +				 +				elgg.get('livesearch', { +					data: params, +					dataType: 'json', +					success: function(data) { +						response(data); +					} +				}); +			}, +			minLength: 2, +			html: "html", +			select: elgg.userpicker.addUser +		}) +	}); + +	$('.elgg-userpicker-remove').live('click', elgg.userpicker.removeUser); +}; + +/** + * Adds a user to the select user list + * + * elgg.userpicker.userList is defined in the input/userpicker view + * + * @param {Object} event + * @param {Object} ui    The object returned by the autocomplete endpoint + * @return void + */ +elgg.userpicker.addUser = function(event, ui) { +	var info = ui.item; + +	// do not allow users to be added multiple times +	if (!(info.guid in elgg.userpicker.userList)) { +		elgg.userpicker.userList[info.guid] = true; +		var users = $(this).siblings('.elgg-user-picker-list'); +		var li = '<input type="hidden" name="members[]" value="' + info.guid + '" />'; +		li += elgg.userpicker.viewUser(info); +		$('<li>').html(li).appendTo(users); +	} + +	$(this).val(''); +	event.preventDefault(); +}; + +/** + * Remove a user from the selected user list + * + * @param {Object} event + * @return void + */ +elgg.userpicker.removeUser = function(event) { +	var item = $(this).closest('.elgg-user-picker-list > li'); +	 +	var guid = item.find('[name="members[]"]').val(); +	delete elgg.userpicker.userList[guid]; + +	item.remove(); +	event.preventDefault(); +}; + +/** + * Render the list item for insertion into the selected user list + * + * The html in this method has to remain synced with the input/userpicker view + * + * @param {Object} info  The object returned by the autocomplete endpoint + * @return string + */ +elgg.userpicker.viewUser = function(info) { + +	var deleteLink = "<a href='#' class='elgg-userpicker-remove'>X</a>"; + +	var html = "<div class='elgg-image-block'>"; +	html += "<div class='elgg-image'>" + info.icon + "</div>"; +	html += "<div class='elgg-image-alt'>" + deleteLink + "</div>"; +	html += "<div class='elgg-body'>" + info.name + "</div>"; +	html += "</div>"; +	 +	return html; +}; + +/** + * Get the parameters to use for autocomplete + * + * This grabs the value of the friends checkbox. + * + * @param {Object} obj  Object for the autocomplete callback + * @return Object + */ +elgg.userpicker.getSearchParams = function(obj) { +	if (obj.element.parent('.elgg-user-picker').find('input[name=match_on]').attr('checked')) { +		return {'match_on[]': 'friends', 'term' : obj.term}; +	} else { +		return {'match_on[]': 'users', 'term' : obj.term}; +	} +}; + +elgg.register_hook_handler('init', 'system', elgg.userpicker.init); diff --git a/js/lib/ui.widgets.js b/js/lib/ui.widgets.js new file mode 100644 index 000000000..26020bb4b --- /dev/null +++ b/js/lib/ui.widgets.js @@ -0,0 +1,209 @@ +elgg.provide('elgg.ui.widgets'); + +/** + * Widgets initialization + * + * @return void + */ +elgg.ui.widgets.init = function() { + +	// widget layout? +	if ($(".elgg-widgets").length == 0) { +		return; +	} + +	$(".elgg-widgets").sortable({ +		items:                'div.elgg-module-widget.elgg-state-draggable', +		connectWith:          '.elgg-widgets', +		handle:               '.elgg-widget-handle', +		forcePlaceholderSize: true, +		placeholder:          'elgg-widget-placeholder', +		opacity:              0.8, +		revert:               500, +		stop:                 elgg.ui.widgets.move +	}); + +	$('.elgg-widgets-add-panel li.elgg-state-available').click(elgg.ui.widgets.add); + +	$('a.elgg-widget-delete-button').live('click', elgg.ui.widgets.remove); +	$('.elgg-widget-edit > form ').live('submit', elgg.ui.widgets.saveSettings); +	$('a.elgg-widget-collapse-button').live('click', elgg.ui.widgets.collapseToggle); + +	elgg.ui.widgets.setMinHeight(".elgg-widgets"); +}; + +/** + * Adds a new widget + * + * Makes Ajax call to persist new widget and inserts the widget html + * + * @param {Object} event + * @return void + */ +elgg.ui.widgets.add = function(event) { +	// elgg-widget-type-<type> +	var type = $(this).attr('id'); +	type = type.substr(type.indexOf('elgg-widget-type-') + "elgg-widget-type-".length); + +	// if multiple instances not allow, disable this widget type add button +	var multiple = $(this).attr('class').indexOf('elgg-widget-multiple') != -1; +	if (multiple == false) { +		$(this).addClass('elgg-state-unavailable'); +		$(this).removeClass('elgg-state-available'); +		$(this).unbind('click', elgg.ui.widgets.add); +	} + +	elgg.action('widgets/add', { +		data: { +			handler: type, +			owner_guid: elgg.get_page_owner_guid(), +			context: $("input[name='widget_context']").val(), +			show_access: $("input[name='show_access']").val(), +			default_widgets: $("input[name='default_widgets']").val() || 0 +		}, +		success: function(json) { +			$('#elgg-widget-col-1').prepend(json.output); +		} +	}); +	event.preventDefault(); +}; + +/** + * Persist the widget's new position + * + * @param {Object} event + * @param {Object} ui + * + * @return void + */ +elgg.ui.widgets.move = function(event, ui) { + +	// elgg-widget-<guid> +	var guidString = ui.item.attr('id'); +	guidString = guidString.substr(guidString.indexOf('elgg-widget-') + "elgg-widget-".length); + +	// elgg-widget-col-<column> +	var col = ui.item.parent().attr('id'); +	col = col.substr(col.indexOf('elgg-widget-col-') + "elgg-widget-col-".length); + +	elgg.action('widgets/move', { +		data: { +			widget_guid: guidString, +			column: col, +			position: ui.item.index() +		} +	}); + +	// @hack fixes jquery-ui/opera bug where draggable elements jump +	ui.item.css('top', 0); +	ui.item.css('left', 0); +}; + +/** + * Removes a widget from the layout + * + * Event callback the uses Ajax to delete the widget and removes its HTML + * + * @param {Object} event + * @return void + */ +elgg.ui.widgets.remove = function(event) { +	if (confirm(elgg.echo('deleteconfirm')) == false) { +		event.preventDefault(); +		return; +	} +	 +	var $widget = $(this).closest('.elgg-module-widget'); + +	// if widget type is single instance type, enable the add buton +	var type = $widget.attr('class'); +	// elgg-widget-instance-<type> +	type = type.substr(type.indexOf('elgg-widget-instance-') + "elgg-widget-instance-".length); +	$button = $('#elgg-widget-type-' + type); +	var multiple = $button.attr('class').indexOf('elgg-widget-multiple') != -1; +	if (multiple == false) { +		$button.addClass('elgg-state-available'); +		$button.removeClass('elgg-state-unavailable'); +		$button.unbind('click', elgg.ui.widgets.add); // make sure we don't bind twice +		$button.click(elgg.ui.widgets.add); +	} + +	$widget.remove(); + +	// delete the widget through ajax +	elgg.action($(this).attr('href')); + +	event.preventDefault(); +}; + +/** + * Toggle the collapse state of the widget + * + * @param {Object} event + * @return void + */ +elgg.ui.widgets.collapseToggle = function(event) { +	$(this).toggleClass('elgg-widget-collapsed'); +	$(this).parent().parent().find('.elgg-body').slideToggle('medium'); +	event.preventDefault(); +}; + +/** + * Save a widget's settings + * + * Uses Ajax to save the settings and updates the HTML. + * + * @param {Object} event + * @return void + */ +elgg.ui.widgets.saveSettings = function(event) { +	$(this).parent().slideToggle('medium'); +	var $widgetContent = $(this).parent().parent().children('.elgg-widget-content'); + +	// stick the ajax loader in there +	var $loader = $('#elgg-widget-loader').clone(); +	$loader.attr('id', '#elgg-widget-active-loader'); +	$loader.removeClass('hidden'); +	$widgetContent.html($loader); + +	var default_widgets = $("input[name='default_widgets']").val() || 0; +	if (default_widgets) { +		$(this).append('<input type="hidden" name="default_widgets" value="1">'); +	} + +	elgg.action('widgets/save', { +		data: $(this).serialize(), +		success: function(json) { +			$widgetContent.html(json.output); +		} +	}); +	event.preventDefault(); +}; + +/** + * Set the min-height so that all widget column bottoms are the same + * + * This addresses the issue of trying to drag a widget into a column that does + * not have any widgets or many fewer widgets than other columns. + * + * @param {String} selector + * @return void + */ +elgg.ui.widgets.setMinHeight = function(selector) { +	var maxBottom = 0; +	$(selector).each(function() { +		var bottom = parseInt($(this).offset().top + $(this).height()); +		if (bottom > maxBottom) { +			maxBottom = bottom; +		} +	}) +	$(selector).each(function() { +		var bottom = parseInt($(this).offset().top + $(this).height()); +		if (bottom < maxBottom) { +			var newMinHeight = parseInt($(this).height() + (maxBottom - bottom)); +			$(this).css('min-height', newMinHeight + 'px'); +		} +	}) +}; + +elgg.register_hook_handler('init', 'system', elgg.ui.widgets.init); diff --git a/js/tests/ElggAjaxOptionsTest.js b/js/tests/ElggAjaxOptionsTest.js new file mode 100644 index 000000000..a6b40d439 --- /dev/null +++ b/js/tests/ElggAjaxOptionsTest.js @@ -0,0 +1,61 @@ +/** + * Tests elgg.ajax.handleOptions() with all of the possible valid inputs + */ +ElggAjaxOptionsTest = TestCase("ElggAjaxOptionsTest"); + +ElggAjaxOptionsTest.prototype.testHandleOptionsAcceptsNoArgs = function() { +	assertNotUndefined(elgg.ajax.handleOptions()); +}; + +ElggAjaxOptionsTest.prototype.testHandleOptionsAcceptsUrl = function() { +	var url = 'url', +		result = elgg.ajax.handleOptions(url); +	 +	assertEquals(url, result.url); +}; + +ElggAjaxOptionsTest.prototype.testHandleOptionsAcceptsDataOnly = function() { +	var options = {}, +		result = elgg.ajax.handleOptions(options); +	 +	assertEquals(options, result.data); +}; + +ElggAjaxOptionsTest.prototype.testHandleOptionsAcceptsOptions = function() { +	var options = {data:{arg:1}}, +		result = elgg.ajax.handleOptions(options); +	 +	assertEquals(options, result); +	 +	function func() {} +	options = {success: func}; +	result = elgg.ajax.handleOptions(options); +	 +	assertEquals(options, result); +}; + +ElggAjaxOptionsTest.prototype.testHandleOptionsAcceptsUrlThenDataOnly = function() { +	var url = 'url', +		options = {arg:1}, +		result = elgg.ajax.handleOptions(url, options); +	 +	assertEquals(url, result.url); +	assertEquals(options, result.data); +}; + +ElggAjaxOptionsTest.prototype.testHandleOptionsAcceptsUrlThenSuccessOnly = function() { +	var url = 'url', +	result = elgg.ajax.handleOptions(url, elgg.nullFunction); +	 +	assertEquals(url, result.url); +	assertEquals(elgg.nullFunction, result.success); +}; + +ElggAjaxOptionsTest.prototype.testHandleOptionsAcceptsUrlThenOptions = function() { +	var url = 'url', +	options = {data:{arg:1}}, +	result = elgg.ajax.handleOptions(url, options); +	 +	assertEquals(url, result.url); +	assertEquals(options.data, result.data); +};
\ No newline at end of file diff --git a/js/tests/ElggAjaxTest.js b/js/tests/ElggAjaxTest.js new file mode 100644 index 000000000..a683415fc --- /dev/null +++ b/js/tests/ElggAjaxTest.js @@ -0,0 +1,59 @@ +/** + * Makes sure that each of the helper ajax functions ends up calling $.ajax + * with the right options. + */ +ElggAjaxTest = TestCase("ElggAjaxTest"); + +ElggAjaxTest.prototype.setUp = function() { +	 +	this.wwwroot = elgg.config.wwwroot; +	this.ajax = $.ajax; +	 +	elgg.config.wwwroot = 'http://www.elgg.org/'; +	 +	$.ajax = function(options) { +		return options; +	}; +}; + +ElggAjaxTest.prototype.tearDown = function() { +	$.ajax = this.ajax; +	elgg.config.wwwroot = this.wwwroot; +}; + +ElggAjaxTest.prototype.testElggAjax = function() { +	assertEquals(elgg.config.wwwroot, elgg.ajax().url); +}; + +ElggAjaxTest.prototype.testElggGet = function() { +	assertEquals('get', elgg.get().type); +}; + +ElggAjaxTest.prototype.testElggGetJSON = function() { +	assertEquals('json', elgg.getJSON().dataType); +}; + +ElggAjaxTest.prototype.testElggPost = function() { +	assertEquals('post', elgg.post().type); +}; + +ElggAjaxTest.prototype.testElggAction = function() { +	assertException(function() { elgg.action(); }); +	assertException(function() { elgg.action({}); }); +	 +	var result = elgg.action('action'); +	assertEquals('post', result.type); +	assertEquals('json', result.dataType); +	assertEquals(elgg.config.wwwroot + 'action/action', result.url); +	assertEquals(elgg.security.token.__elgg_ts, result.data.__elgg_ts); +}; + +ElggAjaxTest.prototype.testElggAPI = function() { +	assertException(function() { elgg.api(); }); +	assertException(function() { elgg.api({}); }); +	 +	var result = elgg.api('method'); +	assertEquals('json', result.dataType); +	assertEquals('method', result.data.method); +	assertEquals(elgg.config.wwwroot + 'services/api/rest/json/', result.url); +}; diff --git a/js/tests/ElggHooksTest.js b/js/tests/ElggHooksTest.js new file mode 100644 index 000000000..e7a2440e7 --- /dev/null +++ b/js/tests/ElggHooksTest.js @@ -0,0 +1,28 @@ +ElggHooksTest = TestCase("ElggHooksTest"); + +ElggHooksTest.prototype.setUp = function() { +	elgg.config.hooks = {}; +	elgg.provide('elgg.config.hooks.all.all'); +}; + +ElggHooksTest.prototype.testHookHandlersMustBeFunctions = function () { +	assertException(function() { elgg.register_hook_handler('str', 'str', 'oops'); }); +}; + +ElggHooksTest.prototype.testReturnValueDefaultsToTrue = function () { +	assertTrue(elgg.trigger_hook('fee', 'fum')); + +	elgg.register_hook_handler('fee', 'fum', elgg.nullFunction); +	assertTrue(elgg.trigger_hook('fee', 'fum')); +}; + +ElggHooksTest.prototype.testCanGlomHooksWithAll = function () { +	elgg.register_hook_handler('all', 'bar', elgg.abstractMethod); +	assertException("all,bar", function() { elgg.trigger_hook('foo', 'bar'); }); + +	elgg.register_hook_handler('foo', 'all', elgg.abstractMethod); +	assertException("foo,all", function() { elgg.trigger_hook('foo', 'baz'); }); + +	elgg.register_hook_handler('all', 'all', elgg.abstractMethod); +	assertException("all,all", function() { elgg.trigger_hook('pinky', 'winky'); }); +};
\ No newline at end of file diff --git a/js/tests/ElggLanguagesTest.js b/js/tests/ElggLanguagesTest.js new file mode 100644 index 000000000..9186ff5bb --- /dev/null +++ b/js/tests/ElggLanguagesTest.js @@ -0,0 +1,45 @@ +ElggLanguagesTest = TestCase("ElggLanguagesTest"); + +ElggLanguagesTest.prototype.setUp = function() { +	this.ajax = $.ajax; +	 +	//Immediately execute some dummy "returned" javascript instead of sending +	//an actual ajax request +	$.ajax = function(settings) { +		var lang = settings.data.language; +		elgg.config.translations[lang] = {'language':lang}; +	}; +}; + +ElggLanguagesTest.prototype.tearDown = function() { +	$.ajax = this.ajax; +	 +	//clear translations +	elgg.config.translations['en'] = undefined; +	elgg.config.translations['aa'] = undefined; +}; + +ElggLanguagesTest.prototype.testLoadTranslations = function() { +	assertUndefined(elgg.config.translations['en']); +	assertUndefined(elgg.config.translations['aa']); +	 +	elgg.reload_all_translations(); +	elgg.reload_all_translations('aa'); +	 +	assertNotUndefined(elgg.config.translations['en']['language']); +	assertNotUndefined(elgg.config.translations['aa']['language']); +}; + +ElggLanguagesTest.prototype.testElggEchoTranslates = function() { +	elgg.reload_all_translations('en'); +	elgg.reload_all_translations('aa'); +	 +	assertEquals('en', elgg.echo('language')); +	assertEquals('aa', elgg.echo('language', 'aa')); +}; + +ElggLanguagesTest.prototype.testElggEchoFallsBackToDefaultLanguage = function() { +	elgg.reload_all_translations('en'); +	assertEquals('en', elgg.echo('language', 'aa')); +}; + diff --git a/js/tests/ElggLibTest.js b/js/tests/ElggLibTest.js new file mode 100644 index 000000000..bd39e7fb3 --- /dev/null +++ b/js/tests/ElggLibTest.js @@ -0,0 +1,140 @@ +/** + * Test basic elgg library functions + */ +ElggLibTest = TestCase("ElggLibTest"); + +ElggLibTest.prototype.testGlobal = function() { +	assertTrue(window === elgg.global); +}; + +ElggLibTest.prototype.testAssertTypeOf = function() { +	[//Valid inputs +	    ['string', ''], +        ['object', {}], +        ['boolean', true], +        ['boolean', false], +        ['undefined', undefined], +        ['number', 0], +        ['function', elgg.nullFunction] +    ].forEach(function(args) { +		assertNoException(function() { +			elgg.assertTypeOf.apply(undefined, args); +		}); +	}); + +	[//Invalid inputs +        ['function', {}], +        ['object', elgg.nullFunction] +    ].forEach(function() { +		assertException(function(args) { +			elgg.assertTypeOf.apply(undefined, args); +		}); +	}); +}; + +ElggLibTest.prototype.testProvideDoesntClobber = function() { +	elgg.provide('foo.bar.baz'); + +	foo.bar.baz.oof = "test"; + +	elgg.provide('foo.bar.baz'); + +	assertEquals("test", foo.bar.baz.oof); +}; + +/** + * Try requiring bogus input + */ +ElggLibTest.prototype.testRequire = function () { +	assertException(function(){ elgg.require(''); }); +	assertException(function(){ elgg.require('garbage'); }); +	assertException(function(){ elgg.require('gar.ba.ge'); }); + +	assertNoException(function(){ +		elgg.require('jQuery'); +		elgg.require('elgg'); +		elgg.require('elgg.config'); +		elgg.require('elgg.security'); +	}); +}; + +ElggLibTest.prototype.testInherit = function () { +	function Base() {} +	function Child() {} + +	elgg.inherit(Child, Base); + +	assertInstanceOf(Base, new Child()); +	assertEquals(Child, Child.prototype.constructor); +}; + +ElggLibTest.prototype.testNormalizeUrl = function() { +	elgg.config.wwwroot = "http://elgg.org/"; + +	[ +		['', elgg.config.wwwroot], +		['test', elgg.config.wwwroot + 'test'], +		['http://example.com', 'http://example.com'], +		['https://example.com', 'https://example.com'], +		['http://example-time.com', 'http://example-time.com'], +		['//example.com', '//example.com'], +		['mod/my_plugin/graphics/image.jpg', elgg.config.wwwroot + 'mod/my_plugin/graphics/image.jpg'], + +		['ftp://example.com/file', 'ftp://example.com/file'], +		['mailto:brett@elgg.org', 'mailto:brett@elgg.org'], +		['javascript:alert("test")', 'javascript:alert("test")'], +		['app://endpoint', 'app://endpoint'], + +		['example.com', 'http://example.com'], +		['example.com/subpage', 'http://example.com/subpage'], + +		['page/handler', elgg.config.wwwroot + 'page/handler'], +		['page/handler?p=v&p2=v2', elgg.config.wwwroot + 'page/handler?p=v&p2=v2'], +		['mod/plugin/file.php', elgg.config.wwwroot + 'mod/plugin/file.php'], +		['mod/plugin/file.php?p=v&p2=v2', elgg.config.wwwroot + 'mod/plugin/file.php?p=v&p2=v2'], +		['rootfile.php', elgg.config.wwwroot + 'rootfile.php'], +		['rootfile.php?p=v&p2=v2', elgg.config.wwwroot + 'rootfile.php?p=v&p2=v2'], + +		['/page/handler', elgg.config.wwwroot + 'page/handler'], +		['/page/handler?p=v&p2=v2', elgg.config.wwwroot + 'page/handler?p=v&p2=v2'], +		['/mod/plugin/file.php', elgg.config.wwwroot + 'mod/plugin/file.php'], +		['/mod/plugin/file.php?p=v&p2=v2', elgg.config.wwwroot + 'mod/plugin/file.php?p=v&p2=v2'], +		['/rootfile.php', elgg.config.wwwroot + 'rootfile.php'], +		['/rootfile.php?p=v&p2=v2', elgg.config.wwwroot + 'rootfile.php?p=v&p2=v2'] + +	].forEach(function(args) { +		assertEquals(args[1], elgg.normalize_url(args[0])); +	}); +}; + +ElggLibTest.prototype.testParseUrl = function() { + +	[ +		["http://www.elgg.org/test/", {'scheme': 'http', 'host': 'www.elgg.org', 'path': '/test/'}], +		["https://www.elgg.org/test/", {'scheme': 'https', 'host': 'www.elgg.org', 'path': '/test/'}], +		["ftp://www.elgg.org/test/", {'scheme': 'ftp', 'host': 'www.elgg.org', 'path': '/test/'}], +		["http://elgg.org/test?val1=one&val2=two", {'scheme': 'http', 'host': 'elgg.org', 'path': '/test', 'query': 'val1=one&val2=two'}], +		["http://elgg.org:8080/", {'scheme': 'http', 'host': 'elgg.org', 'port': 8080, 'path': '/'}], +		["http://elgg.org/test#there", {'scheme': 'http', 'host': 'elgg.org', 'path': '/test', 'fragment': 'there'}], +		 +		["test?val=one", {'host': 'test', 'query': 'val=one'}], +		["?val=one", {'query': 'val=one'}], + +		["mailto:joe@elgg.org", {'scheme': 'mailto', 'path': 'joe@elgg.org'}], +		["javascript:load()", {'scheme': 'javascript', 'path': 'load()'}] + +	].forEach(function(args) { +		assertEquals(args[1], elgg.parse_url(args[0])); +	}); +}; + +ElggLibTest.prototype.testParseStr = function() { + +	[ +		["A+%2B+B=A+%2B+B", {"A + B": "A + B"}] + +	].forEach(function(args) { +		assertEquals(args[1], elgg.parse_str(args[0])); +	}); +}; + diff --git a/js/tests/ElggPriorityListTest.js b/js/tests/ElggPriorityListTest.js new file mode 100644 index 000000000..2329a8490 --- /dev/null +++ b/js/tests/ElggPriorityListTest.js @@ -0,0 +1,47 @@ +ElggPriorityListTest = TestCase("ElggPriorityListTest"); + +ElggPriorityListTest.prototype.setUp = function() { +	this.list = new elgg.ElggPriorityList(); +}; + +ElggPriorityListTest.prototype.tearDown = function() { +	this.list = null; +}; + +ElggPriorityListTest.prototype.testInsert = function() { +	this.list.insert('foo'); + +	assertEquals('foo', this.list.priorities_[500][0]); + +	this.list.insert('bar', 501); + +	assertEquals('bar', this.list.priorities_[501][0]); +}; + +ElggPriorityListTest.prototype.testInsertRespectsPriority = function() { +	var values = [5, 4, 3, 2, 1, 0]; + +	for (var i in values) { +		this.list.insert(values[i], values[i]); +	} + +	this.list.forEach(function(elem, idx) { +		assertEquals(elem, idx); +	}) +}; + +ElggPriorityListTest.prototype.testInsertHandlesDuplicatePriorities = function() { +	values = [0, 1, 2, 3, 4, 5, 6, 7, 8 , 9]; + +	for (var i in values) { +		this.list.insert(values[i], values[i]/3); +	} + +	this.list.forEach(function(elem, idx) { +		assertEquals(elem, idx); +	}); +}; + +ElggPriorityListTest.prototype.testEveryDefaultsToTrue = function() { +	assertTrue(this.list.every(function() {})); +};
\ No newline at end of file diff --git a/js/tests/ElggSecurityTest.js b/js/tests/ElggSecurityTest.js new file mode 100644 index 000000000..107c0adbd --- /dev/null +++ b/js/tests/ElggSecurityTest.js @@ -0,0 +1,75 @@ +ElggSecurityTest = TestCase("ElggSecurityTest"); + +ElggSecurityTest.prototype.setUp = function() { +	//fill with fake, but reasonable, values for testing +	this.ts = elgg.security.token.__elgg_ts = 12345; +	this.token = elgg.security.token.__elgg_token = 'abcdef'; +}; + +ElggSecurityTest.prototype.testAddTokenAcceptsUndefined = function() { +	var input, +		expected = { +			__elgg_ts: this.ts, +			__elgg_token: this.token +		}; +	 +	assertEquals(expected, elgg.security.addToken(input)); +}; + +ElggSecurityTest.prototype.testAddTokenAcceptsObject = function() { +	var input = {}, +		expected = { +			__elgg_ts: this.ts, +			__elgg_token: this.token +		}; +	 +	assertEquals(expected, elgg.security.addToken(input)); +}; + +ElggSecurityTest.prototype.testAddTokenAcceptsRelativeUrl = function() { +	var input, +		str = "__elgg_ts=" + this.ts + "&__elgg_token=" + this.token; + +	input = "test"; +	assertEquals(input + '?' + str, elgg.security.addToken(input)); +}; + +ElggSecurityTest.prototype.testAddTokenAcceptsFullUrl = function() { +	var input, +		str = "__elgg_ts=" + this.ts + "&__elgg_token=" + this.token; + +	input = "http://elgg.org/"; +	assertEquals(input + '?' + str, elgg.security.addToken(input)); +}; + +ElggSecurityTest.prototype.testAddTokenAcceptsQueryString = function() { +	var input, +		str = "__elgg_ts=" + this.ts + "&__elgg_token=" + this.token; + +	input = "?data=sofar"; +	assertEquals(input + '&' + str, elgg.security.addToken(input)); + +	input = "test?data=sofar"; +	assertEquals(input + '&' + str, elgg.security.addToken(input)); + +	input = "http://elgg.org/?data=sofar"; +	assertEquals(input + '&' + str, elgg.security.addToken(input)); +}; + +ElggSecurityTest.prototype.testAddTokenAlreadyAdded = function() { +	var input, +		str = "__elgg_ts=" + this.ts + "&__elgg_token=" + this.token; + +	input = "http://elgg.org/?" + str + "&data=sofar"; +	assertEquals(input, elgg.security.addToken(input)); +}; + +ElggSecurityTest.prototype.testSetTokenSetsElggSecurityToken = function() { +	var json = { +		__elgg_ts: 4567, +		__elgg_token: 'abcdef' +	}; +	 +	elgg.security.setToken(json); +	assertEquals(json, elgg.security.token); +}; diff --git a/js/tests/ElggSessionTest.js b/js/tests/ElggSessionTest.js new file mode 100644 index 000000000..5ff8ca13e --- /dev/null +++ b/js/tests/ElggSessionTest.js @@ -0,0 +1,36 @@ +ElggSessionTest = TestCase("ElggSessionTest"); + +ElggSessionTest.prototype.testGetCookie = function() { +	assertEquals(document.cookie, elgg.session.cookie()); +}; + +ElggSessionTest.prototype.testGetCookieKey = function() { +	document.cookie = "name=value"; +	assertEquals('value', elgg.session.cookie('name')); +	 +	document.cookie = "name=value2"; +	assertEquals('value2', elgg.session.cookie('name')); +	 +	document.cookie = "name=value"; +	document.cookie = "name2=value2"; +	assertEquals('value', elgg.session.cookie('name')); +	assertEquals('value2', elgg.session.cookie('name2')); +}; + +ElggSessionTest.prototype.testSetCookieKey = function() { +	elgg.session.cookie('name', 'value'); +	assertEquals('value', elgg.session.cookie('name')); + +	elgg.session.cookie('name', 'value2'); +	assertEquals('value2', elgg.session.cookie('name')); +	 +	elgg.session.cookie('name', 'value'); +	elgg.session.cookie('name2', 'value2'); +	assertEquals('value', elgg.session.cookie('name')); +	assertEquals('value2', elgg.session.cookie('name2')); +	 +	elgg.session.cookie('name', null); +	elgg.session.cookie('name2', null); +	assertUndefined(elgg.session.cookie('name')); +	assertUndefined(elgg.session.cookie('name2')); +};
\ No newline at end of file diff --git a/js/tests/README b/js/tests/README new file mode 100644 index 000000000..f43c0c89d --- /dev/null +++ b/js/tests/README @@ -0,0 +1,25 @@ +Elgg JavaScript Unit Tests +-------------------------- + +Introduction +============ +Elgg uses js-test-driver to run its unit tests. Instructions on obtaining, +configuring, and using it are at http://code.google.com/p/js-test-driver/. It +supports running the test in multiple browsers and debugging using web browser +based debuggers. Visit its wiki at the Google Code site for more information. + + +Sample Usage +============ + 1. Put jar file in the base directory of Elgg + 2. Run the server: java -jar JsTestDriver-1.3.5.jar --port 4224 + 3. Point a web browser at http://localhost:4224 + 4. Click "Capture this browser" + 5. Run the tests: java -jar JsTestDriver-1.3.5.jar --config js/tests/jsTestDriver.conf --basePath . --tests all + + +Configuration Hints +=================== + * The port when running the server must be the same as listed in the  +   configuration file. If that port is being used, change the configuration file. + * The basePath must be the base directory of Elgg.
\ No newline at end of file diff --git a/js/tests/jsTestDriver.conf b/js/tests/jsTestDriver.conf new file mode 100644 index 000000000..cc0b5d373 --- /dev/null +++ b/js/tests/jsTestDriver.conf @@ -0,0 +1,10 @@ +server: http://localhost:4224 + +load: + - vendors/jquery/jquery-1.6.4.min.js + - vendors/sprintf.js + - js/lib/elgglib.js + - js/lib/hooks.js + - js/classes/*.js + - js/lib/*.js + - js/tests/*.js
\ No newline at end of file | 
