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 |