diff options
| author | Sem <sembrestels@riseup.net> | 2011-11-18 07:32:27 +0100 | 
|---|---|---|
| committer | Sem <sembrestels@riseup.net> | 2011-11-18 07:32:27 +0100 | 
| commit | e53d410129701ea1c9d19529afa493f11b5f5b70 (patch) | |
| tree | d9963b24bf8932654b4a47e36602c75975e50dba /js | |
| parent | 377da25d2965c64941f83baae119fc970ec60982 (diff) | |
| parent | 08a962c98e2923724f8013d6eaae89101243752a (diff) | |
| download | elgg-e53d410129701ea1c9d19529afa493f11b5f5b70.tar.gz elgg-e53d410129701ea1c9d19529afa493f11b5f5b70.tar.bz2 | |
Merge github.com:Elgg/Elgg
Conflicts:
	engine/lib/input.php
Diffstat (limited to 'js')
| -rw-r--r-- | js/classes/ElggPriorityList.js | 7 | ||||
| -rw-r--r-- | js/classes/ElggUser.js | 16 | ||||
| -rw-r--r-- | js/lib/ajax.js | 6 | ||||
| -rw-r--r-- | js/lib/autocomplete.js | 39 | ||||
| -rw-r--r-- | js/lib/configuration.js | 2 | ||||
| -rw-r--r-- | js/lib/elgglib.js | 187 | ||||
| -rw-r--r-- | js/lib/hooks.js | 77 | ||||
| -rw-r--r-- | js/lib/languages.js | 2 | ||||
| -rw-r--r-- | js/lib/security.js | 31 | ||||
| -rw-r--r-- | js/lib/ui.autocomplete.js | 14 | ||||
| -rw-r--r-- | js/lib/ui.avatar_cropper.js | 76 | ||||
| -rw-r--r-- | js/lib/ui.friends_picker.js (renamed from js/lib/friends_picker.js) | 0 | ||||
| -rw-r--r-- | js/lib/ui.js | 92 | ||||
| -rw-r--r-- | js/lib/ui.userpicker.js | 117 | ||||
| -rw-r--r-- | js/lib/ui.widgets.js | 12 | ||||
| -rw-r--r-- | js/lib/userpicker.js | 84 | ||||
| -rw-r--r-- | js/tests/ElggLanguagesTest.js | 2 | ||||
| -rw-r--r-- | js/tests/ElggLibTest.js | 59 | ||||
| -rw-r--r-- | js/tests/ElggPriorityListTest.js | 6 | ||||
| -rw-r--r-- | js/tests/ElggSecurityTest.js | 44 | ||||
| -rw-r--r-- | js/tests/README | 24 | ||||
| -rw-r--r-- | js/tests/jsTestDriver.conf | 5 | 
22 files changed, 667 insertions, 235 deletions
| diff --git a/js/classes/ElggPriorityList.js b/js/classes/ElggPriorityList.js index 831342f21..b4cec5044 100644 --- a/js/classes/ElggPriorityList.js +++ b/js/classes/ElggPriorityList.js @@ -16,7 +16,10 @@ elgg.ElggPriorityList = function() {   * @return {Void}   */  elgg.ElggPriorityList.prototype.insert = function(obj, opt_priority) { -	var priority = parseInt(opt_priority || 500, 10); +	var priority = 500; +	if (arguments.length == 2 && opt_priority != undefined) { +		priority = parseInt(opt_priority, 10); +	}  	priority = Math.max(priority, 0); @@ -31,7 +34,7 @@ elgg.ElggPriorityList.prototype.insert = function(obj, opt_priority) {  /**   * Iterates through each element in order.   * -* Unlike every, this ignores the return value of the callback. + * 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. diff --git a/js/classes/ElggUser.js b/js/classes/ElggUser.js index 8a7a8b7eb..b8a976fba 100644 --- a/js/classes/ElggUser.js +++ b/js/classes/ElggUser.js @@ -6,9 +6,23 @@   * @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);
\ No newline at end of file +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 index 6f6ae052f..b3f39cc42 100644 --- a/js/lib/ajax.js +++ b/js/lib/ajax.js @@ -187,7 +187,11 @@ elgg.action = function(action, options) {  	options = elgg.ajax.handleOptions(action, options); -	options.data = elgg.security.addToken(options.data); +	// 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 diff --git a/js/lib/autocomplete.js b/js/lib/autocomplete.js deleted file mode 100644 index 917326d4f..000000000 --- a/js/lib/autocomplete.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - *  - */ -elgg.provide('elgg.autocomplete'); - -elgg.autocomplete.init = function() { -	$('.elgg-input-autocomplete').autocomplete({ -		source: elgg.autocomplete.url, //gets set by input/autocomplete -		minLength: 1, -		select: function(event, ui) { -			var item = ui.item; -			$(this).val(item.name); -	 -			var hidden = $(this).next(); -			hidden.val(item.guid); -		} -	}) -	 -	//@todo This seems convoluted -	.data("autocomplete")._renderItem = function(ul, item) { -		switch (item.type) { -			case 'user': -			case 'group': -				r = item.icon + item.name + ' - ' + item.desc; -				break; - -			default: -				r = item.name + ' - ' + item.desc; -				break; -		} -		 -		return $("<li/>") -			.data("item.autocomplete", item) -			.append(r) -			.appendTo(ul); -	}; -}; - -elgg.register_hook_handler('init', 'system', elgg.autocomplete.init);
\ No newline at end of file diff --git a/js/lib/configuration.js b/js/lib/configuration.js index f724a2f01..6e221c957 100644 --- a/js/lib/configuration.js +++ b/js/lib/configuration.js @@ -7,4 +7,4 @@ elgg.provide('elgg.config');   */  elgg.get_site_url = function() {  	return elgg.config.wwwroot; -}
\ No newline at end of file +};
\ No newline at end of file diff --git a/js/lib/elgglib.js b/js/lib/elgglib.js index f2545fb6c..81209ebd0 100644 --- a/js/lib/elgglib.js +++ b/js/lib/elgglib.js @@ -224,8 +224,8 @@ elgg.provide = function(pkg, opt_context) {   * child.foo('boo!'); // alert('boo!');   * </pre>   * - * @param {Function} childCtor Child class. - * @param {Function} parentCtor Parent class. + * @param {Function} Child Child class constructor. + * @param {Function} Parent Parent class constructor.   */  elgg.inherit = function(Child, Parent) {  	Child.prototype = new Parent(); @@ -250,8 +250,35 @@ elgg.normalize_url = function(url) {  	url = url || '';  	elgg.assertTypeOf('string', url); -	// jslint complains if you use /regexp/ shorthand here... ?!?! -	if ((new RegExp("^(https?:)?//", "i")).test(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;  	} @@ -353,6 +380,105 @@ elgg.forward = function(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]) +		value = decodeURIComponent(result[2]) +		params[key] = value; +	} +	 +	return params; +}; + +/**   * Returns a jQuery selector from a URL's fragement.  Defaults to expecting an ID.   *   * Examples: @@ -379,4 +505,55 @@ elgg.getSelectorFromUrlFragment = function(url) {  		}  	}  	return ''; -};
\ No newline at end of file +}; + +/** + * 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 index ab3a8a224..5e1808e22 100644 --- a/js/lib/hooks.js +++ b/js/lib/hooks.js @@ -3,13 +3,18 @@   */  elgg.provide('elgg.config.hooks'); +elgg.provide('elgg.config.instant_hooks'); +elgg.provide('elgg.config.triggered_hooks');  /** - * Registers an hook handler with the event system. + * 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 @@ -33,6 +38,11 @@ elgg.register_hook_handler = function(name, type, handler, priority) {  		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);  }; @@ -43,7 +53,9 @@ elgg.register_hook_handler = function(name, type, handler, priority) {   * 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 to the PHP version! + * 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) @@ -62,6 +74,9 @@ 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; @@ -100,5 +115,59 @@ elgg.trigger_hook = function(name, type, params, value) {  		return true;  	}); -	return (tempReturnValue !== null) ? tempReturnValue : returnValue; -};
\ No newline at end of file +	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 index 4cfe84968..ae7ba63e2 100644 --- a/js/lib/languages.js +++ b/js/lib/languages.js @@ -32,6 +32,8 @@ elgg.reload_all_translations = function(language) {  		},  		success: function(json) {  			elgg.add_translation(lang, json); +			elgg.config.languageReady = true; +			elgg.initWhenReady();  		}  	});  }; diff --git a/js/lib/security.js b/js/lib/security.js index 486347b88..61aa1cfcd 100644 --- a/js/lib/security.js +++ b/js/lib/security.js @@ -60,7 +60,7 @@ elgg.security.refreshToken = function() {  /** - * Add elgg action tokens to an object or string (assumed to be url data) + * 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 @@ -70,14 +70,31 @@ elgg.security.addToken = function(data) {  	// 'http://example.com?data=sofar'  	if (elgg.isString(data)) { -		var args = []; -		if (data) { -			args.push(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.push("__elgg_ts=" + elgg.security.token.__elgg_ts); -		args.push("__elgg_token=" + elgg.security.token.__elgg_token); +		args["__elgg_ts"] = elgg.security.token.__elgg_ts; +		args["__elgg_token"] = elgg.security.token.__elgg_token; -		return args.join('&'); +		return base + jQuery.param(args);  	}  	// no input!  acts like a getter 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/friends_picker.js b/js/lib/ui.friends_picker.js index 9257c40fc..9257c40fc 100644 --- a/js/lib/friends_picker.js +++ b/js/lib/ui.friends_picker.js diff --git a/js/lib/ui.js b/js/lib/ui.js index 46e418e8b..c26cbe389 100644 --- a/js/lib/ui.js +++ b/js/lib/ui.js @@ -1,6 +1,7 @@  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 @@ -13,23 +14,14 @@ elgg.ui.init = function () {  	$('[rel=toggle]').live('click', elgg.ui.toggles); -	$('[rel=popup]').live('click', elgg.ui.popsUp); +	$('[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); -	if ($('.elgg-input-date').length) { -		elgg.ui.initDatePicker(); -	} - -	// fix for ie7 CSS issue on menu dropdown -	// open the menu when you hover over it, close when you click off of it. -	// @todo This should be possible with CSS. Anyone want to tame the beast, go for it. -	if ($.browser.msie && $.browser.version <= 7) { -		$('.elgg-menu-site > .elgg-more').live('mouseenter', elgg.ui.ie7MenuFixMouseEnter) -	} -} +	$('.elgg-autofocus').focus(); +};  /**   * Toggles an element based on clicking a separate element @@ -47,7 +39,7 @@ elgg.ui.toggles = function(event) {  	var target = $(this).toggleClass('elgg-state-active').attr('href');  	$(target).slideToggle('medium'); -} +};  /**   * Pops up an element based on clicking a separate element @@ -67,7 +59,7 @@ elgg.ui.toggles = function(event) {   * @param {Object} event   * @return void   */ -elgg.ui.popsUp = function(event) { +elgg.ui.popupOpen = function(event) {  	event.preventDefault();  	event.stopPropagation(); @@ -109,7 +101,7 @@ elgg.ui.popsUp = function(event) {  	$('body')  		.die('click', elgg.ui.popupClose)  		.live('click', elgg.ui.popupClose); -} +};  /**   * Catches clicks that aren't in a popup and closes all popups. @@ -147,7 +139,7 @@ elgg.ui.popupClose = function(event) {  		$('body').die('click', elgg.ui.popClose);  	} -} +};  /**   * Toggles a child menu when the parent is clicked @@ -159,7 +151,7 @@ elgg.ui.toggleMenu = function(event) {  	$(this).siblings().slideToggle('medium');  	$(this).toggleClass('elgg-menu-closed elgg-menu-opened');  	event.preventDefault(); -} +};  /**   * Initialize the hover menu @@ -187,7 +179,7 @@ elgg.ui.initHoverMenu = function(parent) {  		var $hovermenu = $(this).data('hovermenu') || null;  		if (!$hovermenu) { -			var $hovermenu = $(this).parent().find(".elgg-menu-hover"); +			$hovermenu = $(this).parent().find(".elgg-menu-hover");  			$(this).data('hovermenu', $hovermenu);  		} @@ -219,7 +211,7 @@ elgg.ui.initHoverMenu = function(parent) {  			$(".elgg-menu-hover").fadeOut();  		}  	}); -} +};  /**   * Calls a confirm() and prevents default if denied. @@ -244,7 +236,7 @@ elgg.ui.requiresConfirmation = function(e) {   *   * @return {Object}   */ -elgg.ui.LoginHandler = function(hook, type, params, options) { +elgg.ui.loginHandler = function(hook, type, params, options) {  	if (params.target.attr('id') == 'login-dropdown-box') {  		options.my = 'right top';  		options.at = 'right bottom'; @@ -265,49 +257,25 @@ elgg.ui.LoginHandler = function(hook, type, params, options) {   * @return void   */  elgg.ui.initDatePicker = 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 date = $.datepicker.parseDate('yy-mm-dd', dateText); -				var timestamp = $.datepicker.formatDate('@', date); -				timestamp = timestamp / 1000; - -				var id = $(this).attr('id'); -				$('input[name="' + id + '"]').val(timestamp); +	if ($('.elgg-input-date').length) { +		$('.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); +				}  			} -		} -	}); -} - -/** - * IE 7 doesn't like our site menu system CSS, so open it with JS. - */ -elgg.ui.ie7MenuFixMouseEnter = function() { -	$('.elgg-menu-site .elgg-menu-site-more').css('display', 'block'); -	$('.elgg-menu-site .elgg-more > a') -		.css('background-color', 'white') -		.css('color', '#555') - -	$body = $('body'); -	if (!$body.data('hasIe7Clear')) { -		$body.live('click', elgg.ui.ie7MenuClear); -		$body.data('hasIe7Clear', true); +		});  	} -	 -} - -/** - * Close the menu when clicking on the body - */ -elgg.ui.ie7MenuClear = function() { -	$('.elgg-menu-site .elgg-menu-site-more').css('display', 'none'); -	$('.elgg-menu-site .elgg-more > a') -		.css('background-color', 'transparent') -		.css('color', 'white') -} +};  elgg.register_hook_handler('init', 'system', elgg.ui.init); -elgg.register_hook_handler('getOptions', 'ui.popup', elgg.ui.LoginHandler);
\ No newline at end of file +elgg.register_hook_handler('init', 'system', elgg.ui.initDatePicker); +elgg.register_hook_handler('getOptions', 'ui.popup', elgg.ui.loginHandler);
\ 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..8287ba91c --- /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.siblings('[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);
\ No newline at end of file diff --git a/js/lib/ui.widgets.js b/js/lib/ui.widgets.js index fb256672a..6435d2147 100644 --- a/js/lib/ui.widgets.js +++ b/js/lib/ui.widgets.js @@ -65,7 +65,7 @@ elgg.ui.widgets.add = function(event) {  		}  	});  	event.preventDefault(); -} +};  /**   * Persist the widget's new position @@ -96,7 +96,7 @@ elgg.ui.widgets.move = function(event, ui) {  	// @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 @@ -134,7 +134,7 @@ elgg.ui.widgets.remove = function(event) {  		}  	});  	event.preventDefault(); -} +};  /**   * Toggle the collapse state of the widget @@ -146,7 +146,7 @@ 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 @@ -178,7 +178,7 @@ elgg.ui.widgets.saveSettings = function(event) {  		}  	});  	event.preventDefault(); -} +};  /**   * Make all elements have the same min-height @@ -197,6 +197,6 @@ elgg.ui.widgets.equalHeight = function(selector) {  		}  	})  	$(selector).css('min-height', maxHeight); -} +};  elgg.register_hook_handler('init', 'system', elgg.ui.widgets.init); diff --git a/js/lib/userpicker.js b/js/lib/userpicker.js deleted file mode 100644 index 826bf21a0..000000000 --- a/js/lib/userpicker.js +++ /dev/null @@ -1,84 +0,0 @@ -elgg.provide('elgg.userpicker'); - -elgg.userpicker.init = function() { -	// binding autocomplete. -	// doing this as an each so we can pass this to functions. -	$('.elgg-input-user-picker').each(function() { -		 -		var _this = this; -		 -		$(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, -			select: elgg.userpicker.addUser -		}) -		 -		//@todo This seems convoluted -		.data("autocomplete")._renderItem = elgg.userpicker.formatItem; -	}); -}; - -elgg.userpicker.formatItem = function(ul, item) { -	switch (item.type) { -		case 'user': -		case 'group': -			r = item.icon + item.name + ' - ' + item.desc; -			break; - -		default: -			r = item.name + ' - ' + item.desc; -			break; -	} -	 -	return $("<li/>") -		.data("item.autocomplete", item) -		.append(r) -		.appendTo(ul); -}; - -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 picker = $(this).closest('.elgg-user-picker'); -		var users = picker.find('.elgg-user-picker-entries'); -		var internalName = users.find('[type=hidden]').attr('name'); -		 -		// not sure why formatted isn't. -		var formatted = elgg.userpicker.formatItem(data); - -		// add guid as hidden input and to list. -		var li = formatted + ' <div class="delete-button"><a onclick="elgg.userpicker.removeUser(this, ' + info.guid + ')"><strong>X</strong></a></div>' -		+ '<input type="hidden" name="' + internalName + '" value="' + info.guid + '" />'; -		$('<li>').html(li).appendTo(users); - -		$(this).val(''); -	} -}; - -elgg.userpicker.removeUser = function(link, guid) { -	$(link).closest('.elgg-user-picker-entries > li').remove(); -}; - -elgg.userpicker.getSearchParams = function(e) { -	if ($(e).closest('.elgg-user-picker').find('[name=match_on]').attr('checked')) { -		return {'match_on[]': 'friends', 'term' : e.term}; -	} else { -		return {'match_on[]': 'users', 'term' : e.term}; -	} -} - -elgg.register_hook_handler('init', 'system', elgg.userpicker.init);
\ No newline at end of file diff --git a/js/tests/ElggLanguagesTest.js b/js/tests/ElggLanguagesTest.js index 1f66fc35b..9186ff5bb 100644 --- a/js/tests/ElggLanguagesTest.js +++ b/js/tests/ElggLanguagesTest.js @@ -6,7 +6,7 @@ ElggLanguagesTest.prototype.setUp = function() {  	//Immediately execute some dummy "returned" javascript instead of sending  	//an actual ajax request  	$.ajax = function(settings) { -		var lang = settings.data.js.split('/')[1]; +		var lang = settings.data.language;  		elgg.config.translations[lang] = {'language':lang};  	};  }; diff --git a/js/tests/ElggLibTest.js b/js/tests/ElggLibTest.js index dd0267c5c..a29ebf743 100644 --- a/js/tests/ElggLibTest.js +++ b/js/tests/ElggLibTest.js @@ -72,13 +72,58 @@ ElggLibTest.prototype.testNormalizeUrl = function() {  	elgg.config.wwwroot = "http://elgg.org/";  	[ -	    ['', elgg.config.wwwroot], -	    ['test', elgg.config.wwwroot + 'test'], -	    ['http://google.com', 'http://google.com'], -	    ['//example.com', '//example.com'], -	    ['/page', elgg.config.wwwroot + 'page'], -	    ['mod/plugin/index.php', elgg.config.wwwroot + 'mod/plugin/index.php'], +		['', 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'], + +		['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]));  	}); -};
\ No newline at end of file +}; + +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])); +	}); +}; + diff --git a/js/tests/ElggPriorityListTest.js b/js/tests/ElggPriorityListTest.js index 2549e0ee0..2329a8490 100644 --- a/js/tests/ElggPriorityListTest.js +++ b/js/tests/ElggPriorityListTest.js @@ -15,7 +15,7 @@ ElggPriorityListTest.prototype.testInsert = function() {  	this.list.insert('bar', 501); -	assertEquals('foo', this.list.priorities_[501][0]); +	assertEquals('bar', this.list.priorities_[501][0]);  };  ElggPriorityListTest.prototype.testInsertRespectsPriority = function() { @@ -25,9 +25,9 @@ ElggPriorityListTest.prototype.testInsertRespectsPriority = function() {  		this.list.insert(values[i], values[i]);  	} -	this.list.forEach(function(elem, idx)) { +	this.list.forEach(function(elem, idx) {  		assertEquals(elem, idx); -	} +	})  };  ElggPriorityListTest.prototype.testInsertHandlesDuplicatePriorities = function() { diff --git a/js/tests/ElggSecurityTest.js b/js/tests/ElggSecurityTest.js index f1111168f..107c0adbd 100644 --- a/js/tests/ElggSecurityTest.js +++ b/js/tests/ElggSecurityTest.js @@ -26,16 +26,42 @@ ElggSecurityTest.prototype.testAddTokenAcceptsObject = function() {  	assertEquals(expected, elgg.security.addToken(input));  }; -ElggSecurityTest.prototype.testAddTokenAcceptsString = function() { +ElggSecurityTest.prototype.testAddTokenAcceptsRelativeUrl = function() {  	var input,  		str = "__elgg_ts=" + this.ts + "&__elgg_token=" + this.token; -	 -	input = ""; -	assertEquals(str, elgg.security.addToken(input)); -	 -	input = "data=sofar"; -	assertEquals(input+'&'+str, elgg.security.addToken(input)); -	 + +	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() { @@ -47,5 +73,3 @@ ElggSecurityTest.prototype.testSetTokenSetsElggSecurityToken = function() {  	elgg.security.setToken(json);  	assertEquals(json, elgg.security.token);  }; - - diff --git a/js/tests/README b/js/tests/README new file mode 100644 index 000000000..4f86b27c6 --- /dev/null +++ b/js/tests/README @@ -0,0 +1,24 @@ +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.3d.jar --port 4224 + 3. Point a web browser at http://localhost:4224 + 4. Run the tests: java -jar JsTestDriver-1.3.3d.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 index 1bb06e811..cc0b5d373 100644 --- a/js/tests/jsTestDriver.conf +++ b/js/tests/jsTestDriver.conf @@ -1,9 +1,10 @@ -server: http://localhost:42442 +server: http://localhost:4224  load: - - vendors/jquery/jquery-1.4.2.min.js + - 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 | 
