diff --git a/e107_handlers/e107_class.php b/e107_handlers/e107_class.php index 59cf5deab..b54b48e4d 100644 --- a/e107_handlers/e107_class.php +++ b/e107_handlers/e107_class.php @@ -156,6 +156,7 @@ class e107 'e_admin_request' => '{e_HANDLER}admin_ui.php', 'e_admin_response' => '{e_HANDLER}admin_ui.php', 'e_admin_ui' => '{e_HANDLER}admin_ui.php', + 'e_ajax_class' => '{e_HANDLER}e_ajax_class.php', 'e_array' => '{e_HANDLER}core_functions.php', // Old ArrayStorage. 'e_bbcode' => '{e_HANDLER}bbcode_handler.php', 'e_bb_base' => '{e_HANDLER}bbcode_handler.php', @@ -1655,6 +1656,16 @@ class e107 return self::getSingleton('eMessage', true); } + /** + * Retrieve ajax singleton object + * + * @return e_ajax_class + */ + public static function getAjax() + { + return self::getSingleton('e_ajax_class', true); + } + /** * Retrieve Library Manager singleton object (internal use only. Use e107::library()) * diff --git a/e107_handlers/e_ajax_class.php b/e107_handlers/e_ajax_class.php new file mode 100644 index 000000000..39bdfe506 --- /dev/null +++ b/e107_handlers/e_ajax_class.php @@ -0,0 +1,286 @@ +commandSettings(array('foo' => 'bar')); + * // Remove 'disabled' attribute from the '#object-1' element. + * $commands[] = $ajax->commandInvoke('#object-1', 'removeAttr', array('disabled')); + * // Insert HTML content into the '#object-1' element. + * $commands[] = $ajax->commandInsert('#object-1', 'html', 'some html content'); + * + * // This method returns with data in JSON format. It sets the header for + * // JavaScript output. + * $ajax->response($commands); + * @endcode + */ +class e_ajax_class +{ + + /** + * Constructor. + * Use {@link getInstance()}, direct instantiating is not possible for signleton + * objects. + */ + public function __construct() + { + } + + /** + * @return void + */ + protected function _init() + { + } + + /** + * Cloning is not allowed. + */ + private function __clone() + { + } + + /** + * Returns data in JSON format. + * + * This function should be used for JavaScript callback functions returning + * data in JSON format. It sets the header for JavaScript output. + * + * @param $var + * (optional) If set, the variable will be converted to JSON and output. + */ + public function response($var = null) + { + // We are returning JSON, so tell the browser. + header('Content-Type: application/json'); + + if(isset($var)) + { + echo $this->render($var); + } + } + + /** + * Renders a commands array into JSON. + * + * @param array $commands + * A list of macro commands generated by the use of e107::ajax()->command* + * methods. + * + * @return string + */ + public function render($commands = array()) + { + $tp = e107::getParser(); + return $tp->toJSON($commands); + } + + /** + * Creates an Ajax 'alert' command. + * + * The 'alert' command instructs the client to display a JavaScript alert + * dialog box. + * + * @param $text + * The message string to display to the user. + * + * @return array + * An array suitable for use with the e107::ajax()->render() function. + */ + public function commandAlert($text) + { + return array( + 'command' => 'alert', + 'text' => $text, + ); + } + + /** + * Creates an Ajax 'insert' command. + * + * This command instructs the client to insert the given HTML. + * + * @param $target + * A jQuery target selector. + * @param $method + * Selected method fo DOM manipulation: + * 'replaceWith', 'append', 'prepend', 'before', 'after', 'html' + * @param $html + * The data to use with the jQuery method. + * + * @return array + * An array suitable for use with the e107::ajax()->render() function. + */ + public function commandInsert($target, $method, $html) + { + return array( + 'command' => 'insert', + 'method' => $method, + 'target' => $target, + 'data' => $html, + ); + } + + /** + * Creates an Ajax 'remove' command. + * + * The 'remove' command instructs the client to use jQuery's remove() method + * to remove each of elements matched by the given target, and everything + * within them. + * + * @param $target + * A jQuery selector string. + * + * @return array + * An array suitable for use with the e107::ajax()->render() function. + * + * @see http://docs.jquery.com/Manipulation/remove#expr + */ + public function commandRemove($target) + { + return array( + 'command' => 'remove', + 'target' => $target, + ); + } + + /** + * Creates an Ajax 'css' command. + * + * The 'css' command will instruct the client to use the jQuery css() method + * to apply the CSS arguments to elements matched by the given target. + * + * @param $target + * A jQuery selector string. + * @param $argument + * An array of key/value pairs to set in the CSS for the target. + * + * @return array + * An array suitable for use with the e107::ajax()->render() function. + * + * @see http://docs.jquery.com/CSS/css#properties + */ + public function commandCSS($target, $argument) + { + return array( + 'command' => 'css', + 'target' => $target, + 'argument' => $argument, + ); + } + + /** + * Creates an Ajax 'settings' command. + * + * The 'settings' command instructs the client to extend e107.settings with + * the given array. + * + * @param $settings + * An array of key/value pairs to add to the settings. This will be utilized + * for all commands after this if they do not include their own settings + * array. + * + * @return array + * An array suitable for use with the e107::ajax()->render() function. + */ + public function commandSettings($settings) + { + return array( + 'command' => 'settings', + 'settings' => $settings, + ); + } + + /** + * Creates an Ajax 'data' command. + * + * The 'data' command instructs the client to attach the name=value pair of + * data to the target via jQuery's data cache. + * + * @param $target + * A jQuery selector string. + * @param $name + * The name or key (in the key value pair) of the data attached to this + * target. + * @param $value + * The value of the data. Not just limited to strings can be any format. + * + * @return array + * An array suitable for use with the e107::ajax()->render() function. + * + * @see http://docs.jquery.com/Core/data#namevalue + */ + public function commandData($target, $name, $value) + { + return array( + 'command' => 'data', + 'target' => $target, + 'name' => $name, + 'value' => $value, + ); + } + + /** + * Creates an Ajax 'invoke' command. + * + * The 'invoke' command will instruct the client to invoke the given jQuery + * method with the supplied arguments on the elements matched by the given + * target. Intended for simple jQuery commands, such as attr(), addClass(), + * removeClass(), toggleClass(), etc. + * + * @param $target + * A jQuery selector string. + * @param $method + * The jQuery method to invoke. + * @param $arguments + * (optional) A list of arguments to the jQuery $method, if any. + * + * @return array + * An array suitable for use with the e107::ajax()->render() function. + */ + public function commandInvoke($target, $method, array $arguments = array()) + { + return array( + 'command' => 'invoke', + 'target' => $target, + 'method' => $method, + 'arguments' => $arguments, + ); + } + +} \ No newline at end of file diff --git a/e107_web/js/core/all.jquery.js b/e107_web/js/core/all.jquery.js index 434101af6..6e85c635a 100644 --- a/e107_web/js/core/all.jquery.js +++ b/e107_web/js/core/all.jquery.js @@ -117,208 +117,50 @@ var e107 = e107 || {'settings': {}, 'behaviors': {}}; }); /** - * Behavior to attach a click event to links with .e-ajax class. - * - * @type {{attach: Function}} + * Attaches the AJAX behavior to each AJAX form/page elements. E107 uses + * this behavior to enhance form/page elements with .e-ajax class. */ - e107.behaviors.eAjaxLink = { + e107.behaviors.eAJAX = { attach: function (context, settings) { - $(context).find('a.e-ajax').once('e-ajax-link').each(function () + $(context).find('.e-ajax').once('e-ajax').each(function () { - $(this).click(function () - { - // Old way - href='myscript.php#id-to-target - var href = $(this).attr("href"); - // Target container for result. - var target = $(this).attr("data-target"); - // Image to show loading. - var loading = $(this).attr('data-loading'); - // If this is a navigation controller, e.g. pager... - var nav = $(this).attr('data-nav-inc'); - // Method: 'replaceWith', 'append', 'prepend', 'before', 'after', 'html' (default). - var method = $(this).attr('data-method'); + var $this = $(this); + var event = $this.attr('data-event') || e107.callbacks.getDefaultEventHandler($this); - if(nav != null) + $this.on(event, function () + { + var $element = $(this); + + var ajaxOptions = { + // URL for Ajax request. + url: $element.attr('data-src'), + // Ajax type: POST or GET. + type: $element.attr('data-ajax-type'), + // Target container for result. + target: $element.attr("data-target"), + // Method: 'replaceWith', 'append', 'prepend', 'before', 'after', 'html' (default). + method: $element.attr('data-method'), + // Image to show loading. + loading: $element.attr('data-loading'), + // If this is a navigation controller, e.g. pager. + nav: $element.attr('data-nav-inc'), + // Old way - href='myscript.php#id-to-target. + href: $element.attr("href") + }; + + // If this is a navigation controller, e.g. pager. + if(ajaxOptions.nav != null) { // Modify data-src value for next/prev. 'from=' e107.callbacks.eNav(this, '.e-ajax'); + // Update URL for Ajax request. + ajaxOptions.url = $element.attr('data-src'); + // Set Ajax type to "GET". + ajaxOptions.type = 'GET'; } - // URL for Ajax request. - var handler = $(this).attr("data-src"); - - var $target = $("#" + target); - var html = null; // Ajax result. - var $loadingImage = null; - - // TODO: set default loading icon? - if(loading != null) - { - $loadingImage = $(""); - $(this).after($loadingImage); - } - - if(target == null || handler == null) // Old way - href='myscript.php#id-to-target - { - if(href != null) - { - var tmp = href.split('#'); - var id = tmp[1]; - - if(handler == null) - { - handler = tmp[0]; - } - - if(target == null) - { - $target = $('#' + id); - } - } - } - - $.ajax({ - type: 'GET', - url: handler, - complete: function () - { - if($loadingImage) - { - $loadingImage.remove(); - } - }, - success: function (data) - { - switch(method) - { - case 'replaceWith': - html = $.parseHTML(data); - $target.replaceWith(html); - break; - - case 'append': - html = $.parseHTML(data); - $target.append(html); - break; - - case 'prepend': - html = $.parseHTML(data); - $target.prepend(html); - break; - - case 'before': - html = $.parseHTML(data); - $target.before(html); - break; - - case 'after': - html = $.parseHTML(data); - $target.after(html); - break; - - case 'html': - default: - $target.html(data).hide().show("slow"); - break; - } - - // Attach all registered behaviors to the new content. - e107.attachBehaviors(); - } - }); - - return false; - }); - }); - } - }; - - /** - * Behavior to attach a change event to selects with .e-ajax class. - * - * @type {{attach: Function}} - */ - e107.behaviors.eAjaxSelect = { - attach: function (context, settings) - { - $(context).find('select.e-ajax').once('e-ajax-select').each(function () - { - $(this).on('change', function () - { - var form = $(this).closest("form").attr('id'); - - // Target container for result. - var target = $(this).attr("data-target"); - // Image to show loading. - var loading = $(this).attr('data-loading'); - // URL for Ajax request. - var handler = $(this).attr('data-src'); - // Method: 'replaceWith', 'append', 'prepend', 'before', 'after', 'html' (default). - var method = $(this).attr('data-method'); - - var data = $('#' + form).serialize(); - var $target = $("#" + target); - var html = null; - var $loadingImage = null; - - // TODO: set default loading icon? - if(loading != null) - { - $loadingImage = $(""); - $(this).after($loadingImage); - } - - $.ajax({ - type: 'post', - url: handler, - data: data, - complete: function () - { - if($loadingImage) - { - $loadingImage.remove(); - } - }, - success: function (data) - { - switch(method) - { - case 'replaceWith': - html = $.parseHTML(data); - $target.replaceWith(html); - break; - - case 'append': - html = $.parseHTML(data); - $target.append(html); - break; - - case 'prepend': - html = $.parseHTML(data); - $target.prepend(html); - break; - - case 'before': - html = $.parseHTML(data); - $target.before(html); - break; - - case 'after': - html = $.parseHTML(data); - $target.after(html); - break; - - case 'html': - default: - $target.html(data).hide().show("slow"); - break; - } - - // Attach all registered behaviors to the new content. - e107.attachBehaviors(); - } - }); + e107.callbacks.ajaxRequestHandler($element, ajaxOptions); return false; }); @@ -452,6 +294,285 @@ var e107 = e107 || {'settings': {}, 'behaviors': {}}; } }; + /** + * Get a reasonable default event handler for a (jQuery) element. + * + * @param $element + * JQuery element. + */ + e107.callbacks.getDefaultEventHandler = function ($element) + { + var event = 'click'; // Default event handler. + var tag = $element.prop("tagName").toLowerCase(); + + if(tag == 'input') + { + var type = $element.attr('type').toLowerCase(); + + switch(type) + { + case 'submit': + case 'button': + // Pressing the ENTER key within a textfield triggers the click event of + // the form's first submit button. Triggering Ajax in this situation + // leads to problems, like breaking autocomplete textfields, so we bind + // to mousedown instead of click. + event = 'mousedown'; + break; + + case 'radio': + case 'checkbox': + event = 'change'; + break; + + // text, number, password, date, datetime, datetime-local, month, week, time, + // email, search, tel, url, color, range + default: + event = 'blur'; + break; + } + } + else + { + switch(tag) + { + case 'button': + // Pressing the ENTER key within a textfield triggers the click event of + // the form's first submit button. Triggering Ajax in this situation + // leads to problems, like breaking autocomplete textfields, so we bind + // to mousedown instead of click. + event = 'mousedown'; + break; + + case 'select': + event = 'change'; + break; + + case 'textarea': + event = 'blur'; + break; + } + } + + return event; + }; + + /** + * Handler fo Ajax requests. + * + * @param $element + * JQuery element which fired the event. + * @param options + * An object with Ajax request options. + */ + e107.callbacks.ajaxRequestHandler = function ($element, options) + { + var $loadingImage = null; + + // Loading image. + if(options.loading != null) + { + $loadingImage = $(options.loading); + $element.after($loadingImage); + } + + // Old way - href='myscript.php#id-to-target. + if(options.target == null || options.url == null) + { + if(options.href != null) + { + var tmp = options.href.split('#'); + var id = tmp[1]; + + if(options.url == null) + { + options.url = tmp[0]; + } + + if(options.target == null) + { + options.target = id; + } + } + } + + // BC. + if(options.target && options.target.charAt(0) != "#" && options.target.charAt(0) != ".") + { + options.target = "#" + options.target; + } + + var form = $element.closest("form").attr('id'); + var data = $('#' + form).serialize(); + + $.ajax({ + type: options.type || 'POST', + url: options.url, + data: data || '', + complete: function () + { + if($loadingImage) + { + $loadingImage.remove(); + } + }, + success: function (response) + { + var $target = $(options.target); + var jsonObject = response; + + if(typeof response == 'string') + { + try + { + jsonObject = $.parseJSON(response); + } catch(e) + { + // Not JSON. + } + } + + if(typeof jsonObject == 'object') + { + // If result is JSON. + e107.callbacks.ajaxJsonResponseHandler($target, options, jsonObject); + } + else + { + // If result is a simple text/html. + e107.callbacks.ajaxResponseHandler($target, options, response); + } + } + }); + }; + + /** + * Handler for JSON responses. Provides a series of commands that the server + * can request the client perform. + * + * @param $target + * JQuery (target) object. + * @param options + * Object with options for Ajax request. + * @param commands + * JSON object with commands. + */ + e107.callbacks.ajaxJsonResponseHandler = function ($target, options, commands) + { + $(commands).each(function () + { + var command = this; + // Get target selector from the response. If it is not there, default to our presets. + var $newtarget = command.target ? $(command.target) : $target; + + switch(command.command) + { + // Command to insert new content into the DOM. + case 'insert': + var newOptions = options; + newOptions.method = command.method; + e107.callbacks.ajaxResponseHandler($newtarget, newOptions, command.data); + break; + + // Command to remove a chunk from the page. + case 'remove': + e107.detachBehaviors($(command.target)); + $(command.target).remove(); + break; + + // Command to provide an alert. + case 'alert': + alert(command.text, command.title); + break; + + // Command to provide the jQuery css() function. + case 'css': + $(command.target).css(command.arguments); + break; + + // Command to set the settings that will be used for other commands in this response. + case 'settings': + if(typeof command.settings == 'object') + { + $.extend(true, e107.settings, command.settings); + } + break; + + // Command to attach data using jQuery's data API. + case 'data': + $(command.target).data(command.name, command.value); + break; + + // Command to apply a jQuery method. + case 'invoke': + var $element = $(command.target); + $element[command.method].apply($element, command.arguments); + break; + } + }); + }; + + /** + * Handler for text/html responses. Inserting new content into the DOM. + * + * @param $target + * JQuery (target) object. + * @param options + * An object with Ajax request options. + * @param data + * Text/HTML content. + */ + e107.callbacks.ajaxResponseHandler = function ($target, options, data) + { + var html = null; + + // If removing content from the wrapper, detach behaviors first. + switch(options.method) + { + case 'html': + case 'replaceWith': + e107.detachBehaviors($target); + break; + } + + // Inserting content. + switch(options.method) + { + case 'replaceWith': + html = $.parseHTML(data); + $target.replaceWith(html); + break; + + case 'append': + html = $.parseHTML(data); + $target.append(html); + break; + + case 'prepend': + html = $.parseHTML(data); + $target.prepend(html); + break; + + case 'before': + html = $.parseHTML(data); + $target.before(html); + break; + + case 'after': + html = $.parseHTML(data); + $target.after(html); + break; + + case 'html': + default: + $target.html(data).hide().show("slow"); + break; + } + + // Attach all registered behaviors to the new content. + e107.attachBehaviors(); + }; + })(jQuery); $.ajaxSetup({