* @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();
});
--- /dev/null
+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;
+});
event.preventDefault();
if (UIDialog.getDialog('sitemapDialog') === undefined) {
- Ajax.api({
+ Ajax.apiOnce({
data: {
actionName: 'getSitemap',
className: 'wcf\\data\\sitemap\\SitemapAction'
}
},
+ _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.
*
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
}
});
}
options.onShow(id);
}
- ChangeListener.trigger();
+ DOMChangeListener.trigger();
},
/**
}
}
- ChangeListener.trigger();
+ DOMChangeListener.trigger();
},
/**
map: {
'*': {
'Ajax': 'WoltLab/WCF/Ajax',
+ 'AjaxRequest': 'WoltLab/WCF/Ajax/Request',
'CallbackList': 'WoltLab/WCF/CallbackList',
'Core': 'WoltLab/WCF/Core',
'Dictionary': 'WoltLab/WCF/Dictionary',
'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',