From 28dfae017e523a4b1528b7c510494ac51eba73ec Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sat, 23 May 2015 14:43:27 +0200 Subject: [PATCH] Draft for an improved AJAX-JS-API --- wcfsetup/install/files/js/WoltLab/WCF/Ajax.js | 258 +++-------------- .../files/js/WoltLab/WCF/Ajax/Request.js | 271 ++++++++++++++++++ .../js/WoltLab/WCF/Controller/Sitemap.js | 32 ++- .../install/files/js/WoltLab/WCF/UI/Dialog.js | 4 +- wcfsetup/install/files/js/require.config.js | 2 + 5 files changed, 332 insertions(+), 235 deletions(-) create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/Ajax/Request.js diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ajax.js b/wcfsetup/install/files/js/WoltLab/WCF/Ajax.js index 26df32ee33..9b1a14c29b 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Ajax.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ajax.js @@ -6,255 +6,73 @@ * @license GNU Lesser General Public License * @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} options request options + * @param {object} callbackObject callback object + * @param {object=} 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 = '

Stacktrace:

' + data.stacktrace + '

'; - else if (data.exceptionID) details = '

Exception ID: ' + data.exceptionID + '

'; - - message = data.message; - } - else { - message = xhr.responseText; - } - - if (!message || message === 'undefined') { - return; - } - - var html = '

' + message + '

' + details + '
'; - - 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} 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} 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 index 0000000000..d90c66088f --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ajax/Request.js @@ -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} 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} 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 = '

Stacktrace:

' + data.stacktrace + '

'; + else if (data.exceptionID) details = '

Exception ID: ' + data.exceptionID + '

'; + + message = data.message; + } + else { + message = xhr.responseText; + } + + if (!message || message === 'undefined') { + return; + } + + var html = '

' + message + '

' + details + '
'; + + 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; +}); diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Controller/Sitemap.js b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Sitemap.js index 4a4197ae0e..b950051e65 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Controller/Sitemap.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Sitemap.js @@ -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 } }); } diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js index e8c3fafbbd..1c67cf82e6 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js @@ -220,7 +220,7 @@ define( options.onShow(id); } - ChangeListener.trigger(); + DOMChangeListener.trigger(); }, /** @@ -259,7 +259,7 @@ define( } } - ChangeListener.trigger(); + DOMChangeListener.trigger(); }, /** diff --git a/wcfsetup/install/files/js/require.config.js b/wcfsetup/install/files/js/require.config.js index 412eb2f210..e199597822 100644 --- a/wcfsetup/install/files/js/require.config.js +++ b/wcfsetup/install/files/js/require.config.js @@ -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', -- 2.20.1