Draft for an improved AJAX-JS-API
authorAlexander Ebert <ebert@woltlab.com>
Sat, 23 May 2015 12:43:27 +0000 (14:43 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 23 May 2015 12:43:27 +0000 (14:43 +0200)
wcfsetup/install/files/js/WoltLab/WCF/Ajax.js
wcfsetup/install/files/js/WoltLab/WCF/Ajax/Request.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Controller/Sitemap.js
wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js
wcfsetup/install/files/js/require.config.js

index 26df32ee33ad95378ad0e76a66e22e7d14fb26d2..9b1a14c29b0975fefd2b48d68c463ae0a0d19827 100644 (file)
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @module     WoltLab/WCF/Ajax
  */
-define(['Core', 'Language', 'DOM/ChangeListener', 'DOM/Util', 'UI/Dialog', 'WoltLab/WCF/Ajax/Status'], function(Core, Language, DOMChangeListener, DOMUtil, UIDialog, AjaxStatus) {
+define(['AjaxRequest', 'Core', 'ObjectMap'], function(AjaxRequest, Core, ObjectMap) {
        "use strict";
        
-       var _didInit = false;
-       var _ignoreAllErrors = false;
+       var _requests = new ObjectMap();
        
        /**
         * @constructor
         */
-       function Ajax(options) {
-               this._options = {};
-               this._previousXhr = null;
-               this._xhr = null;
-               
-               this._init(options);
-       };
+       function Ajax() {};
        Ajax.prototype = {
                /**
-                * Initializes the request options.
+                * Shorthand function to perform a request against the WCF-API.
                 * 
-                * @param       {object<string, *>}     options         request options
+                * @param       {object}                callbackObject  callback object
+                * @param       {object<string, *>=}    data            request data
+                * @return      {AjaxRequest}
                 */
-               _init: function(options) {
-                       this._options = Core.extend({
-                               // request data
-                               data: {},
-                               type: 'POST',
-                               url: '',
+               api: function(callbackObject, data) {
+                       var request = _requests.get(callbackObject);
+                       if (request !== undefined) {
+                               data = data || {};
                                
-                               // behavior
-                               autoAbort: false,
-                               ignoreError: false,
-                               silent: false,
+                               request.setData(data || {});
+                               request.sendRequest();
                                
-                               // callbacks
-                               failure: null,
-                               finalize: null,
-                               success: null
-                       }, options);
-                       
-                       this._options.url = Core.convertLegacyUrl(this._options.url);
-                       
-                       if (_didInit === false) {
-                               _didInit = true;
-                               
-                               window.addEventListener('beforeunload', function() { _ignoreAllErrors = true; });
-                       }
-               },
-               
-               /**
-                * Dispatches a request, optionally aborting a currently active request.
-                * 
-                * @param       {boolean}       abortPrevious   abort currently active request
-                */
-               sendRequest: function(abortPrevious) {
-                       if (abortPrevious === true || this._options.autoAbort) {
-                               this.abortPrevious();
+                               return request;
                        }
                        
-                       if (!this._options.silent) {
-                               AjaxStatus.show();
+                       if (typeof callbackObject._ajaxSetup !== 'function') {
+                               throw new TypeError("Callback object must implement at least _ajaxSetup().");
                        }
                        
-                       if (this._xhr instanceof XMLHttpRequest) {
-                               this._previousXhr = this._xhr;
+                       var options = callbackObject._ajaxSetup();
+                       if (typeof data === 'object') {
+                               options.data = Core.extend(data, options.data);
                        }
                        
-                       this._xhr = new XMLHttpRequest();
-                       this._xhr.open(this._options.type, this._options.url, true);
-                       this._xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
-                       this._xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+                       options.pinData = true;
+                       options.callbackObject = callbackObject;
                        
-                       var self = this;
-                       this._xhr.onload = function() {
-                               if (this.readyState === XMLHttpRequest.DONE) {
-                                       if (this.status >= 200 && this.status < 300 || this.status === 304) {
-                                               self._success(this);
-                                       }
-                                       else {
-                                               self._failure(this);
-                                       }
-                               }
-                       };
-                       this._xhr.onerror = function() {
-                               self._failure(this);
-                       };
+                       if (!options.url) options.url = 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN;
                        
-                       if (this._options.type === 'POST') {
-                               var data = this._options.data;
-                               if (typeof data === 'object') {
-                                       data = Core.serialize(data);
-                               }
-                               
-                               this._xhr.send(data);
-                       }
-                       else {
-                               this._xhr.send();
-                       }
-               },
-               
-               /**
-                * Aborts a previous request.
-                */
-               abortPrevious: function() {
-                       if (this._previousXhr === null) {
-                               return;
-                       }
+                       request = new AjaxRequest(options);
+                       request.sendRequest();
                        
-                       this._previousXhr.abort();
-                       this._previousXhr = null;
+                       _requests.set(callbackObject, request);
                        
-                       if (!this._options.silent) {
-                               AjaxStatus.hide();
-                       }
+                       return request;
                },
                
                /**
-                * Sets a specific option.
-                * 
-                * Do not call this method, it exists for compatibility with WCF.Action.Proxy
-                * and will be removed at some point without further notice.
+                * Shorthand function to perform a single request against the WCF-API.
                 * 
-                * @deprecated  2.2
-                * 
-                * @param       {string}        key     option name
-                * @param       {*}             value   option value
-                */
-               setOption: function(key, value) {
-                       this._options[key] = value;
-               },
-               
-               /**
-                * Handles a successful request.
-                * 
-                * @param       {XMLHttpRequest}        xhr     request object
-                */
-               _success: function(xhr) {
-                       if (!this._options.silent) {
-                               AjaxStatus.hide();
-                       }
-                       
-                       if (typeof this._options.success === 'function') {
-                               var data = xhr.response;
-                               if (xhr.responseType === 'json') {
-                                       // trim HTML before processing, see http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring
-                                       if (data.returnValues !== undefined && data.returnValues.template !== undefined) {
-                                               data.returnValues.template = data.returnValues.template.trim();
-                                       }
-                               }
-                               
-                               this._options.success(data, xhr.responseText, xhr);
-                       }
-                       
-                       this._finalize();
-               },
-               
-               /**
-                * Handles failed requests, this can be both a successful request with
-                * a non-success status code or an entirely failed request.
-                * 
-                * @param       {XMLHttpRequest}        xhr     request object
-                */
-               _failure: function (xhr) {
-                       if (_ignoreAllErrors) {
-                               return;
-                       }
-                       
-                       if (!this._options.silent) {
-                               AjaxStatus.hide();
-                       }
-                       
-                       var data = null;
-                       try {
-                               data = JSON.parse(xhr.responseText);
-                       }
-                       catch (e) {}
-                       
-                       var showError = true;
-                       if (typeof this._options.failure === 'function') {
-                               showError = this._options.failure(data, xhr);
-                       }
-                       
-                       if (this._options.ignoreError !== true && showError !== false) {
-                               var details = '';
-                               var message = '';
-                               
-                               if (data !== null) {
-                                       if (data.stacktrace) details = '<br /><p>Stacktrace:</p><p>' + data.stacktrace + '</p>';
-                                       else if (data.exceptionID) details = '<br /><p>Exception ID: <code>' + data.exceptionID + '</code></p>';
-                                       
-                                       message = data.message;
-                               }
-                               else {
-                                       message = xhr.responseText;
-                               }
-                               
-                               if (!message || message === 'undefined') {
-                                       return;
-                               }
-                               
-                               var html = '<div class="ajaxDebugMessage"><p>' + message + '</p>' + details + '</div>';
-                               
-                               UIDialog.open(DOMUtil.getUniqueId(), html, {
-                                       title: Language.get('wcf.global.error.title')
-                               });
-                       }
-                       
-                       this._finalize();
-               },
-               
-               /**
-                * Finalizes a request.
+                * Please use `Ajax.api` if you're about to repeatedly send requests because this
+                * method will spawn an new and rather expensive `AjaxRequest` with each call.
+                *  
+                * @param       {object<string, *>}     options         request options
                 */
-               _finalize: function() {
-                       if (typeof this._options.finalize === 'function') {
-                               this._options.finalize(this._xhr);
-                       }
-                       
-                       this._previousXhr = null;
+               apiOnce: function(options) {
+                       options.pinData = false;
+                       options.callbackObject = null;
+                       if (!options.url) options.url = 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN;
                        
-                       DOMChangeListener.trigger();
-                       
-                       // fix anchor tags generated through WCF::getAnchor()
-                       var links = document.querySelectorAll('a[href*="#"]');
-                       for (var i = 0, length = links.length; i < length; i++) {
-                               var link = links[i];
-                               var href = link.getAttribute('href');
-                               if (href.indexOf('AJAXProxy') !== -1 || href.indexOf('ajax-proxy') !== -1) {
-                                       href = href.substr(href.indexOf('#'));
-                                       link.setAttribute('href', document.location.toString().replace(/#.*/, '') + href);
-                               }
-                       }
+                       var request = new AjaxRequest(options);
+                       request.sendRequest();
                }
        };
        
-       /**
-        * Shorthand function to perform a request agains the WCF-API.
-        * 
-        * @param       {object<string, *>}     options         request options
-        * @return      {Ajax}
-        */
-       Ajax.api = function(options) {
-               if (!options.url) options.url = 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN + SID_ARG_2ND;
-               
-               var obj = new Ajax(options);
-               obj.sendRequest();
-               
-               return obj;
-       };
-       
-       return Ajax;
+       return new Ajax();
 });
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ajax/Request.js b/wcfsetup/install/files/js/WoltLab/WCF/Ajax/Request.js
new file mode 100644 (file)
index 0000000..d90c660
--- /dev/null
@@ -0,0 +1,271 @@
+define(['Core', 'Language', 'DOM/ChangeListener', 'DOM/Util', 'UI/Dialog', 'WoltLab/WCF/Ajax/Status'], function(Core, Language, DOMChangeListener, DOMUtil, UIDialog, AjaxStatus) {
+       "use strict";
+       
+       var _didInit = false;
+       var _ignoreAllErrors = false;
+       
+       function AjaxRequest(options) {
+               this._data = null;
+               this._options = {};
+               this._previousXhr = null;
+               this._xhr = null;
+               
+               this._init(options);
+       };
+       AjaxRequest.prototype = {
+               /**
+                * Initializes the request options.
+                * 
+                * @param       {object<string, *>}     options         request options
+                */
+               _init: function(options) {
+                       this._options = Core.extend({
+                               // request data
+                               data: {},
+                               type: 'POST',
+                               url: '',
+                               
+                               // behavior
+                               autoAbort: false,
+                               ignoreError: false,
+                               pinData: false,
+                               silent: false,
+                               
+                               // callbacks
+                               failure: null,
+                               finalize: null,
+                               success: null,
+                               
+                               callbackObject: null
+                       }, options);
+                       
+                       this._options.url = Core.convertLegacyUrl(this._options.url);
+                       
+                       if (this._options.pinData) {
+                               this._data = Core.extend({}, this._options.data);
+                       }
+                       
+                       if (this._options.callbackObject !== null) {
+                               this._options.failure = (typeof this._options.callbackObject._ajaxFailure === 'function') ? this._options.callbackObject._ajaxFailure.bind(this._options.callbackObject) : null;
+                               this._options.finalize = (typeof this._options.callbackObject._ajaxFinalize === 'function') ? this._options.callbackObject._ajaxFinalize.bind(this._options.callbackObject) : null;
+                               this._options.success = (typeof this._options.callbackObject._ajaxSuccess === 'function') ? this._options.callbackObject._ajaxSuccess.bind(this._options.callbackObject) : null;
+                       }
+                       
+                       if (_didInit === false) {
+                               _didInit = true;
+                               
+                               window.addEventListener('beforeunload', function() { _ignoreAllErrors = true; });
+                       }
+               },
+               
+               /**
+                * Dispatches a request, optionally aborting a currently active request.
+                * 
+                * @param       {boolean}       abortPrevious   abort currently active request
+                */
+               sendRequest: function(abortPrevious) {
+                       if (abortPrevious === true || this._options.autoAbort) {
+                               this.abortPrevious();
+                       }
+                       
+                       if (!this._options.silent) {
+                               AjaxStatus.show();
+                       }
+                       
+                       if (this._xhr instanceof XMLHttpRequest) {
+                               this._previousXhr = this._xhr;
+                       }
+                       
+                       this._xhr = new XMLHttpRequest();
+                       this._xhr.open(this._options.type, this._options.url, true);
+                       this._xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
+                       this._xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+                       
+                       var self = this;
+                       this._xhr.onload = function() {
+                               if (this.readyState === XMLHttpRequest.DONE) {
+                                       if (this.status >= 200 && this.status < 300 || this.status === 304) {
+                                               self._success(this);
+                                       }
+                                       else {
+                                               self._failure(this);
+                                       }
+                               }
+                       };
+                       this._xhr.onerror = function() {
+                               self._failure(this);
+                       };
+                       
+                       if (this._options.type === 'POST') {
+                               var data = this._options.data;
+                               if (typeof data === 'object') {
+                                       data = Core.serialize(data);
+                               }
+                               
+                               this._xhr.send(data);
+                       }
+                       else {
+                               this._xhr.send();
+                       }
+               },
+               
+               /**
+                * Aborts a previous request.
+                */
+               abortPrevious: function() {
+                       if (this._previousXhr === null) {
+                               return;
+                       }
+                       
+                       this._previousXhr.abort();
+                       this._previousXhr = null;
+                       
+                       if (!this._options.silent) {
+                               AjaxStatus.hide();
+                       }
+               },
+               
+               /**
+                * Sets a specific option.
+                * 
+                * Do not call this method, it exists for compatibility with WCF.Action.Proxy
+                * and will be removed at some point without further notice.
+                * 
+                * @deprecated  2.2
+                * 
+                * @param       {string}        key     option name
+                * @param       {*}             value   option value
+                */
+               setOption: function(key, value) {
+                       this._options[key] = value;
+               },
+               
+               /**
+                * Sets request data while honoring pinned data from setup callback.
+                * 
+                * @param       {object<string, *>}     data    request data
+                */
+               setData: function(data) {
+                       if (this._data !== null) {
+                               data = Core.extend(this._data, data);
+                       }
+                       
+                       this._options.data = data;
+               },
+               
+               /**
+                * Handles a successful request.
+                * 
+                * @param       {XMLHttpRequest}        xhr     request object
+                */
+               _success: function(xhr) {
+                       if (!this._options.silent) {
+                               AjaxStatus.hide();
+                       }
+                       
+                       if (typeof this._options.success === 'function') {
+                               var data = null;
+                               if (xhr.getResponseHeader('Content-Type') === 'application/json') {
+                                       try {
+                                               data = JSON.parse(xhr.responseText);
+                                       }
+                                       catch (e) {
+                                               // invalid JSON
+                                               this._failure(xhr);
+                                               
+                                               return;
+                                       }
+                                       
+                                       // trim HTML before processing, see http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring
+                                       if (data.returnValues !== undefined && data.returnValues.template !== undefined) {
+                                               data.returnValues.template = data.returnValues.template.trim();
+                                       }
+                               }
+                               
+                               this._options.success(data, xhr.responseText, xhr);
+                       }
+                       
+                       this._finalize();
+               },
+               
+               /**
+                * Handles failed requests, this can be both a successful request with
+                * a non-success status code or an entirely failed request.
+                * 
+                * @param       {XMLHttpRequest}        xhr     request object
+                */
+               _failure: function (xhr) {
+                       if (_ignoreAllErrors) {
+                               return;
+                       }
+                       
+                       if (!this._options.silent) {
+                               AjaxStatus.hide();
+                       }
+                       
+                       var data = null;
+                       try {
+                               data = JSON.parse(xhr.responseText);
+                       }
+                       catch (e) {}
+                       
+                       var showError = true;
+                       if (typeof this._options.failure === 'function') {
+                               showError = this._options.failure(data, xhr);
+                       }
+                       
+                       if (this._options.ignoreError !== true && showError !== false) {
+                               var details = '';
+                               var message = '';
+                               
+                               if (data !== null) {
+                                       if (data.stacktrace) details = '<br /><p>Stacktrace:</p><p>' + data.stacktrace + '</p>';
+                                       else if (data.exceptionID) details = '<br /><p>Exception ID: <code>' + data.exceptionID + '</code></p>';
+                                       
+                                       message = data.message;
+                               }
+                               else {
+                                       message = xhr.responseText;
+                               }
+                               
+                               if (!message || message === 'undefined') {
+                                       return;
+                               }
+                               
+                               var html = '<div class="ajaxDebugMessage"><p>' + message + '</p>' + details + '</div>';
+                               
+                               UIDialog.open(DOMUtil.getUniqueId(), html, {
+                                       title: Language.get('wcf.global.error.title')
+                               });
+                       }
+                       
+                       this._finalize();
+               },
+               
+               /**
+                * Finalizes a request.
+                */
+               _finalize: function() {
+                       if (typeof this._options.finalize === 'function') {
+                               this._options.finalize(this._xhr);
+                       }
+                       
+                       this._previousXhr = null;
+                       
+                       DOMChangeListener.trigger();
+                       
+                       // fix anchor tags generated through WCF::getAnchor()
+                       var links = document.querySelectorAll('a[href*="#"]');
+                       for (var i = 0, length = links.length; i < length; i++) {
+                               var link = links[i];
+                               var href = link.getAttribute('href');
+                               if (href.indexOf('AJAXProxy') !== -1 || href.indexOf('ajax-proxy') !== -1) {
+                                       href = href.substr(href.indexOf('#'));
+                                       link.setAttribute('href', document.location.toString().replace(/#.*/, '') + href);
+                               }
+                       }
+               }
+       };
+       
+       return AjaxRequest;
+});
index 4a4197ae0e733040fdf527120f8936412719a6f0..b950051e65ac85e48a4245ff8ee89fb276b57638 100644 (file)
@@ -33,7 +33,7 @@ define(['Ajax', 'EventHandler', 'Language', 'DOM/Util', 'UI/Dialog', 'UI/TabMenu
                        event.preventDefault();
                        
                        if (UIDialog.getDialog('sitemapDialog') === undefined) {
-                               Ajax.api({
+                               Ajax.apiOnce({
                                        data: {
                                                actionName: 'getSitemap',
                                                className: 'wcf\\data\\sitemap\\SitemapAction'
@@ -60,6 +60,21 @@ define(['Ajax', 'EventHandler', 'Language', 'DOM/Util', 'UI/Dialog', 'UI/TabMenu
                        }
                },
                
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       actionName: 'getSitemap',
+                                       className: 'wcf\\data\\sitemap\\SitemapAction'
+                               }
+                       };
+               },
+               
+               _ajaxSuccess: function(data) {
+                       _cache.push(data.returnValues.sitemapName);
+                       
+                       document.getElementById('sitemap_' + data.returnValues.sitemapName).innerHTML = data.returnValues.template;
+               },
+               
                /**
                 * Callback for tab links, lazy loads content.
                 * 
@@ -69,18 +84,9 @@ define(['Ajax', 'EventHandler', 'Language', 'DOM/Util', 'UI/Dialog', 'UI/TabMenu
                        var name = tabData.active.getAttribute('data-name').replace(/^sitemap_/, '');
                        
                        if (_cache.indexOf(name) === -1) {
-                               Ajax.api({
-                                       data: {
-                                               actionName: 'getSitemap',
-                                               className: 'wcf\\data\\sitemap\\SitemapAction',
-                                               parameters: {
-                                                       sitemapName: name
-                                               }
-                                       },
-                                       success: function(data) {
-                                               _cache.push(data.returnValues.sitemapName);
-                                               
-                                               document.getElementById('sitemap_' + data.returnValues.sitemapName).innerHTML = data.returnValues.template;
+                               Ajax.api(this, {
+                                       parameters: {
+                                               sitemapName: name
                                        }
                                });
                        }
index e8c3fafbbd2b359e366f383febccdcfe7a4a7b15..1c67cf82e69e288519665b449a4a0a6703b808fa 100644 (file)
@@ -220,7 +220,7 @@ define(
                                options.onShow(id);
                        }
                        
-                       ChangeListener.trigger();
+                       DOMChangeListener.trigger();
                },
                
                /**
@@ -259,7 +259,7 @@ define(
                                }
                        }
                        
-                       ChangeListener.trigger();
+                       DOMChangeListener.trigger();
                },
                
                /**
index 412eb2f210e193800040db8ff64cd1456e756acc..e1995978224706f58de38e57f32fad7422c585c0 100644 (file)
@@ -6,6 +6,7 @@ requirejs.config({
        map: {
                '*': {
                        'Ajax': 'WoltLab/WCF/Ajax',
+                       'AjaxRequest': 'WoltLab/WCF/Ajax/Request',
                        'CallbackList': 'WoltLab/WCF/CallbackList',
                        'Core': 'WoltLab/WCF/Core',
                        'Dictionary': 'WoltLab/WCF/Dictionary',
@@ -15,6 +16,7 @@ requirejs.config({
                        'Environment': 'WoltLab/WCF/Environment',
                        'EventHandler': 'WoltLab/WCF/Event/Handler',
                        'Language': 'WoltLab/WCF/Language',
+                       'ObjectMap': 'WoltLab/WCF/ObjectMap',
                        'UI/Alignment': 'WoltLab/WCF/UI/Alignment',
                        'UI/Dialog': 'WoltLab/WCF/UI/Dialog',
                        'UI/SimpleDropdown': 'WoltLab/WCF/UI/Dropdown/Simple',