This is an automated email from the ASF dual-hosted git repository. mgrigorov pushed a commit to branch feature/WICKET-6681-vanilla-wicket-ajax in repository https://gitbox.apache.org/repos/asf/wicket.git
commit 3680062ba4209525f3bff87724b6534b228428e5 Author: Martin Tzvetanov Grigorov <[email protected]> AuthorDate: Sat Jun 29 18:39:34 2019 +0300 WICKET-6681 Implement Wicket Ajax with plain JavaScript APIs, i.e. without jQuery Copy/paste wicket-aja-jquery.js and remove usage of: #extend(), #hide(), #show(), #on(), #off(), #isWindow(), #isFunction(), #bind() --- .../ajax/WicketAjaxVanillaResourceReference.java | 44 + .../wicket/ajax/res/js/wicket-ajax-vanilla.js | 2586 ++++++++++++++++++++ .../wicket/devutils/debugbar/wicket-debugbar.js | 29 +- 3 files changed, 2645 insertions(+), 14 deletions(-) diff --git a/wicket-core/src/main/java/org/apache/wicket/ajax/WicketAjaxVanillaResourceReference.java b/wicket-core/src/main/java/org/apache/wicket/ajax/WicketAjaxVanillaResourceReference.java new file mode 100644 index 0000000..703d55a --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/ajax/WicketAjaxVanillaResourceReference.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.ajax; + +import org.apache.wicket.request.resource.JavaScriptResourceReference; + +/** + * A resource reference to an implementation of Wicket Ajax JavaScript APIs + * with vanilla JavaScript APIs + */ +public class WicketAjaxVanillaResourceReference extends JavaScriptResourceReference +{ + private static final long serialVersionUID = 1L; + + private static final WicketAjaxVanillaResourceReference INSTANCE = new WicketAjaxVanillaResourceReference(); + + /** + * @return the singleton INSTANCE + */ + public static WicketAjaxVanillaResourceReference get() + { + return INSTANCE; + } + + private WicketAjaxVanillaResourceReference() + { + super(AbstractDefaultAjaxBehavior.class, "res/js/wicket-ajax-vanilla.js"); + } + +} diff --git a/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-vanilla.js b/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-vanilla.js new file mode 100644 index 0000000..c25b74c --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-vanilla.js @@ -0,0 +1,2586 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*global DOMParser: true, console: true */ + +/* + * Wicket Ajax Support + * + * @author Igor Vaynberg + * @author Matej Knopp + */ + +;(function (undefined) { + + 'use strict'; + + if (typeof(Wicket) === 'undefined') { + window.Wicket = {}; + } + + if (typeof(Wicket.Head) === 'object') { + return; + } + + var isUndef = function (target) { + return (typeof(target) === 'undefined' || target === null); + }; + + /** + * A safe getter for Wicket's Ajax base URL. + * If the value is not defined or is empty string then + * return '.' (current folder) as base URL. + * Used for request header and parameter + */ + var getAjaxBaseUrl = function () { + var baseUrl = Wicket.Ajax.baseUrl || '.'; + return baseUrl; + }; + + /** + * Converts a NodeList to an Array + * + * @param nodeList The NodeList to convert + * @returns {Array} The array with document nodes + */ + var nodeListToArray = function (nodeList) { + var arr = [], + nodeId; + if (nodeList && nodeList.length) { + for (nodeId = 0; nodeId < nodeList.length; nodeId++) { + arr.push(nodeList.item(nodeId)); + } + } + return arr; + }; + + /** + * Functions executer takes array of functions and executes them. + * The functions are executed one by one as far as the return value is FunctionsExecuter.DONE. + * If the return value is FunctionsExecuter.ASYNC or undefined then the execution of + * the functions will be resumed once the `notify` callback function is called. + * This is needed because header contributions need to do asynchronous download of JS and/or CSS + * and they have to let next function to run only after the download. + * After the FunctionsExecuter is initialized, the start methods triggers the first function. + * + * @param functions {Array} - an array of functions to execute + */ + var FunctionsExecuter = function (functions) { + + this.functions = functions; + + /** + * The index of the currently executed function + * @type {number} + */ + this.current = 0; + + /** + * Tracks the depth of the call stack when `notify` is used for + * asynchronous notification that a function execution has finished. + * Should be reset to 0 when at some point to avoid problems like + * "too much recursion". The reset may break the atomicity by allowing + * another instance of FunctionsExecuter to run its functions + * @type {number} + */ + this.depth = 0; // we need to limit call stack depth + + this.processNext = function () { + if (this.current < this.functions.length) { + var f, run; + + f = this.functions[this.current]; + run = function () { + try { + var n = this.notify.bind(this); + return f(n); + } + catch (e) { + Wicket.Log.error("FunctionsExecuter.processNext:", e); + return FunctionsExecuter.FAIL; + } + }; + run = run.bind(this); + this.current++; + + if (this.depth > FunctionsExecuter.DEPTH_LIMIT) { + // to prevent stack overflow (see WICKET-4675) + this.depth = 0; + window.setTimeout(run, 1); + } else { + var retValue = run(); + if (isUndef(retValue) || retValue === FunctionsExecuter.ASYNC) { + this.depth++; + } + return retValue; + } + } + }; + + this.start = function () { + var retValue = FunctionsExecuter.DONE; + while (retValue === FunctionsExecuter.DONE) { + retValue = this.processNext(); + } + }; + + this.notify = function () { + this.start(); + }; + }; + + var isWindow = function(obj) { + return obj && obj.window === window + }; + + var isFunction = function (item) { + if (typeof item === 'function') { + return true; + } + var type = Object.prototype.toString.call(item); + return type === '[object Function]' || type === '[object GeneratorFunction]'; + } + + /** + * Response that should be used by a function when it finishes successfully + * in synchronous manner + * @type {number} + */ + FunctionsExecuter.DONE = 1; + + /** + * Response that should be used by a function when it finishes abnormally + * in synchronous manner + * @type {number} + */ + FunctionsExecuter.FAIL = 2; + + /** + * Response that may be used by a function when it executes asynchronous + * code and must wait `notify()` to be executed. + * @type {number} + */ + FunctionsExecuter.ASYNC = 3; + + /** + * An artificial number used as a limit of the call stack depth to avoid + * problems like "too much recursion" in the browser. + * The depth is not easy to be calculated because the memory used by the + * stack depends on many factors + * @type {number} + */ + FunctionsExecuter.DEPTH_LIMIT = 1000; + + // Pass in the objects to merge as arguments. + // For a deep extend, set the first argument to `true`. + var extend = function () { + + // Variables + var extended = {}; + var deep = false; + var i = 0; + var length = arguments.length; + + // Check if a deep merge + if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) { + deep = arguments[0]; + i++; + } + + // Merge the object into the extended object + var merge = function (obj) { + for ( var prop in obj ) { + if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) { + // If deep merge and property is an object, merge properties + if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) { + extended[prop] = extend( true, extended[prop], obj[prop] ); + } else { + extended[prop] = obj[prop]; + } + } + } + }; + + // Loop through each object and conduct a merge + for ( ; i < length; i++ ) { + var obj = arguments[i]; + merge(obj); + } + + return extended; + }; + + // API start + + Wicket.Class = { + create: function () { + return function () { + this.initialize.apply(this, arguments); + }; + } + }; + + /** + * Logging functionality. + */ + Wicket.Log = { + + enabled: false, + + log: function () { + if (Wicket.Log.enabled && typeof(console) !== "undefined" && typeof(console.log) === 'function') { + console.log.apply(console, arguments); + } + }, + + debug: function () { + if (Wicket.Log.enabled && typeof(console) !== "undefined" && typeof(console.debug) === 'function') { + console.debug.apply(console, arguments); + } + }, + + info: function () { + if (Wicket.Log.enabled && typeof(console) !== "undefined" && typeof(console.info) === 'function') { + console.info.apply(console, arguments); + } + }, + + warn: function () { + if (Wicket.Log.enabled && typeof(console) !== "undefined" && typeof(console.warn) === 'function') { + console.warn.apply(console, arguments); + } + }, + + error: function () { + if (Wicket.Log.enabled && typeof(console) !== "undefined" && typeof(console.error) === 'function') { + console.error.apply(console, arguments); + } + } + }; + + /** + * Channel management + * + * Wicket Ajax requests are organized in channels. A channel maintain the order of + * requests and determines, what should happen when a request is fired while another + * one is being processed. The default behavior (stack) puts the all subsequent requests + * in a queue, while the drop behavior limits queue size to one, so only the most + * recent of subsequent requests is executed. + * The name of channel determines the policy. E.g. channel with name foochannel|s is + * a stack channel, while barchannel|d is a drop channel. + * + * The Channel class is supposed to be used through the ChannelManager. + */ + Wicket.Channel = Wicket.Class.create(); + + Wicket.Channel.prototype = { + initialize: function (name) { + name = name || '0|s'; + var res = name.match(/^([^|]+)\|(d|s|a)$/); + if (isUndef(res)) { + this.name = '0'; // '0' is the default channel name + this.type = 's'; // default to stack/queue + } + else { + this.name = res[1]; + this.type = res[2]; + } + this.callbacks = []; + this.busy = false; + }, + + schedule: function (callback) { + if (this.busy === false) { + this.busy = true; + try { + return callback(); + } catch (exception) { + this.busy = false; + Wicket.Log.error("An error occurred while executing Ajax request:", exception); + } + } else { + var busyChannel = "Channel '"+ this.name+"' is busy"; + if (this.type === 's') { // stack/queue + Wicket.Log.info("%s - scheduling the callback to be executed when the previous request finish.", busyChannel); + this.callbacks.push(callback); + } + else if (this.type === 'd') { // drop + Wicket.Log.info("%s - dropping all previous scheduled callbacks and scheduling a new one to be executed when the current request finish.", busyChannel); + this.callbacks = []; + this.callbacks.push(callback); + } else if (this.type === 'a') { // active + Wicket.Log.info("%s - ignoring the Ajax call because there is a running request.", busyChannel); + } + return null; + } + }, + + done: function () { + var callback = null; + + if (this.callbacks.length > 0) { + callback = this.callbacks.shift(); + } + + if (callback !== null && typeof(callback) !== "undefined") { + Wicket.Log.info("Calling postponed function..."); + // we can't call the callback from this call-stack + // therefore we set it on timer event + window.setTimeout(callback, 1); + } else { + this.busy = false; + } + } + }; + + /** + * Channel manager maintains a map of channels. + */ + Wicket.ChannelManager = Wicket.Class.create(); + + Wicket.ChannelManager.prototype = { + initialize: function () { + this.channels = {}; + }, + + // Schedules the callback to channel with given name. + schedule: function (channel, callback) { + var parsed = new Wicket.Channel(channel); + var c = this.channels[parsed.name]; + if (isUndef(c)) { + c = parsed; + this.channels[c.name] = c; + } else { + c.type = parsed.type; + } + return c.schedule(callback); + }, + + // Tells the ChannelManager that the current callback in channel with given name + // has finished processing and another scheduled callback can be executed (if any). + done: function (channel) { + var parsed = new Wicket.Channel(channel); + var c = this.channels[parsed.name]; + if (!isUndef(c)) { + c.done(); + if (!c.busy) { + delete this.channels[parsed.name]; + } + } + } + }; + + Wicket.ChannelManager.FunctionsExecuter = FunctionsExecuter; + + /** + * The Ajax.Request class encapsulates a XmlHttpRequest. + */ + Wicket.Ajax = {}; + + /** + * Ajax call fires a Wicket Ajax request and processes the response. + * The response can contain + * - javascript that should be invoked + * - body of components being replaced + * - header contributions of components + * - a redirect location + */ + Wicket.Ajax.Call = Wicket.Class.create(); + + Wicket.Ajax.Call.prototype = { + + initialize: function() {}, + + /** + * Initializes the default values for Ajax request attributes. + * The defaults are not set at the server side to save some bytes + * for the network transfer + * + * @param attrs {Object} - the ajax request attributes to enrich + * @private + */ + _initializeDefaults: function (attrs) { + + // (ajax channel) + if (typeof(attrs.ch) !== 'string') { + attrs.ch = '0|s'; + } + + // (wicketAjaxResponse) be default the Ajax result should be processed for <ajax-response> + if (typeof(attrs.wr) !== 'boolean') { + attrs.wr = true; + } + + // (dataType) by default we expect XML responses from the Ajax behaviors + if (typeof(attrs.dt) !== 'string') { + attrs.dt = 'xml'; + } + + if (typeof(attrs.m) !== 'string') { + attrs.m = 'GET'; + } + + if (attrs.async !== false) { + attrs.async = true; + } + + if (isNaN(parseFloat(attrs.rt)) || !isFinite(attrs.rt)) { + attrs.rt = 0; + } + + if (attrs.pd !== true) { + attrs.pd = false; + } + + if (!attrs.sp) { + attrs.sp = "bubble"; + } + + if (!attrs.sr) { + attrs.sr = false; + } + }, + + /** + * Extracts the HTML element that "caused" this Ajax call. + * An Ajax call is usually caused by JavaScript event but maybe be also + * caused by manual usage of the JS API.. + * + * @param attrs {Object} - the ajax request attributes + * @return {HTMLElement} - the DOM element + * @private + */ + _getTarget: function (attrs) { + var target; + if (attrs.event) { + target = attrs.event.target; + } else if (!isWindow(attrs.c)) { + target = Wicket.$(attrs.c); + } else { + target = window; + } + return target; + }, + + /** + * A helper function that executes an array of handlers (before, success, failure) + * + * @param handlers {Array[Function]} - the handlers to execute + * @private + */ + _executeHandlers: function (handlers) { + if (Array.isArray(handlers)) { + + // cut the handlers argument + var args = Array.prototype.slice.call(arguments).slice(1); + + // assumes that the Ajax attributes is always the first argument + var attrs = args[0]; + var that = this._getTarget(attrs); + + for (var i = 0; i < handlers.length; i++) { + var handler = handlers[i]; + if (isFunction(handler)) { + handler.apply(that, args); + } else { + new Function(handler).apply(that, args); + } + } + } + }, + + /** + * Converts an object (hash) to an array suitable for consumption + * by jQuery.param() + * + * @param {Object} parameters - the object to convert to an array of + * name -> value pairs. + * @see jQuery.param + * @see jQuery.serializeArray + * @private + */ + _asParamArray: function(parameters) { + var result = [], + value, + name; + if (Array.isArray(parameters)) { + result = parameters; + } + else if (jQuery.isPlainObject(parameters)) { + for (name in parameters) { + if (name && parameters.hasOwnProperty(name)) { + value = parameters[name]; + result.push({name: name, value: value}); + } + } + } + + for (var i = 0; i < result.length; i++) { + if (result[i] === null) { + result.splice(i, 1); + i--; + } + } + + return result; + }, + + /** + * Executes all functions to calculate any dynamic extra parameters + * + * @param attrs The Ajax request attributes + * @returns {String} A query string snippet with any calculated request + * parameters. An empty string if there are no dynamic parameters in attrs + * @private + */ + _calculateDynamicParameters: function(attrs) { + var deps = attrs.dep, + params = []; + + for (var i = 0; i < deps.length; i++) { + var dep = deps[i], + extraParam; + if (isFunction(dep)) { + extraParam = dep(attrs); + } else { + extraParam = new Function('attrs', dep)(attrs); + } + extraParam = this._asParamArray(extraParam); + params = params.concat(extraParam); + } + return params; + }, + + /** + * Executes or schedules for execution #doAjax() + * + * @param {Object} attrs - the Ajax request attributes configured at the server side + */ + ajax: function (attrs) { + this._initializeDefaults(attrs); + + var res = Wicket.channelManager.schedule(attrs.ch, Wicket.bind(function () { + this.doAjax(attrs); + }, this)); + return res !== null ? res: true; + }, + + /** + * Is an element still present for Ajax requests. + */ + _isPresent: function(id) { + if (isUndef(id)) { + // no id so no check whether present + return true; + } + + var element = Wicket.$(id); + if (isUndef(element)) { + // not present + return false; + } + + // present if no attributes at all or not a placeholder + return (!element.hasAttribute || !element.hasAttribute('data-wicket-placeholder')); + }, + + /** + * Handles execution of Ajax calls. + * + * @param {Object} attrs - the Ajax request attributes configured at the server side + */ + doAjax: function (attrs) { + + var + // the headers to use for each Ajax request + headers = { + 'Wicket-Ajax': 'true', + 'Wicket-Ajax-BaseURL': getAjaxBaseUrl() + }, + + url = attrs.u, + + // the request (extra) parameters + data = this._asParamArray(attrs.ep), + + self = this, + + // the precondition to use if there are no explicit ones + defaultPrecondition = [ function (attributes) { + return self._isPresent(attributes.c) && self._isPresent(attributes.f); + }], + + // a context that brings the common data for the success/fialure/complete handlers + context = { + attrs: attrs, + + // initialize the array for steps (closures that execute each action) + steps: [] + }, + we = Wicket.Event, + topic = we.Topic; + + if (Wicket.Focus.lastFocusId) { + // WICKET-6568 might contain non-ASCII + headers["Wicket-FocusedElementId"] = Wicket.Form.encode(Wicket.Focus.lastFocusId); + } + + self._executeHandlers(attrs.bh, attrs); + we.publish(topic.AJAX_CALL_BEFORE, attrs); + + var preconditions = attrs.pre || []; + preconditions = defaultPrecondition.concat(preconditions); + if (Array.isArray(preconditions)) { + + var that = this._getTarget(attrs); + + for (var p = 0; p < preconditions.length; p++) { + + var precondition = preconditions[p]; + var result; + if (isFunction(precondition)) { + result = precondition.call(that, attrs); + } else { + result = new Function(precondition).call(that, attrs); + } + if (result === false) { + Wicket.Log.info("Ajax request stopped because of precondition check, url: %s", attrs.u); + self.done(attrs); + return false; + } + } + } + + we.publish(topic.AJAX_CALL_PRECONDITION, attrs); + + if (attrs.f) { + // serialize the form with id == attrs.f + var form = Wicket.$(attrs.f); + data = data.concat(Wicket.Form.serializeForm(form)); + + // set the submitting component input name + if (attrs.sc) { + var scName = attrs.sc; + data = data.concat({name: scName, value: 1}); + } + } else if (attrs.c && !isWindow(attrs.c)) { + // serialize just the form component with id == attrs.c + var el = Wicket.$(attrs.c); + data = data.concat(Wicket.Form.serializeElement(el, attrs.sr)); + } + + // collect the dynamic extra parameters + if (Array.isArray(attrs.dep)) { + var dynamicData = this._calculateDynamicParameters(attrs); + if (attrs.m.toLowerCase() === 'post') { + data = data.concat(dynamicData); + } else { + var separator = url.indexOf('?') > -1 ? '&' : '?'; + url = url + separator + jQuery.param(dynamicData); + } + } + + var wwwFormUrlEncoded; // undefined is jQuery's default + if (attrs.mp) { + try { + var formData = new FormData(); + for (var i = 0; i < data.length; i++) { + formData.append(data[i].name, data[i].value || ""); + } + + data = formData; + wwwFormUrlEncoded = false; + } catch (exception) { + Wicket.Log.error("Ajax multipart not supported:", exception); + } + } + + Wicket.Log.info("Executing Ajax request"); + Wicket.Log.debug(attrs); + + // execute the request + var jqXHR = jQuery.ajax({ + url: url, + type: attrs.m, + context: self, + processData: wwwFormUrlEncoded, + contentType: wwwFormUrlEncoded, + + beforeSend: function (jqXHR, settings) { + self._executeHandlers(attrs.bsh, attrs, jqXHR, settings); + we.publish(topic.AJAX_CALL_BEFORE_SEND, attrs, jqXHR, settings); + + if (attrs.i) { + // show the indicator + Wicket.DOM.showIncrementally(attrs.i); + } + }, + data: data, + dataType: attrs.dt, + async: attrs.async, + timeout: attrs.rt, + cache: false, + headers: headers, + success: function(data, textStatus, jqXHR) { + if (attrs.wr) { + self.processAjaxResponse(data, textStatus, jqXHR, context); + } else { + self._executeHandlers(attrs.sh, attrs, jqXHR, data, textStatus); + we.publish(topic.AJAX_CALL_SUCCESS, attrs, jqXHR, data, textStatus); + } + }, + error: function(jqXHR, textStatus, errorMessage) { + if (jqXHR.status === 301 && jqXHR.getResponseHeader('Ajax-Location')) { + self.processAjaxResponse(data, textStatus, jqXHR, context); + } else { + self.failure(context, jqXHR, errorMessage, textStatus); + } + }, + complete: function (jqXHR, textStatus) { + + context.steps.push(function (notify) { + if (attrs.i && context.isRedirecting !== true) { + Wicket.DOM.hideIncrementally(attrs.i); + } + + self._executeHandlers(attrs.coh, attrs, jqXHR, textStatus); + we.publish(topic.AJAX_CALL_COMPLETE, attrs, jqXHR, textStatus); + + self.done(attrs); + return FunctionsExecuter.DONE; + }.bind(self)); + + var executer = new FunctionsExecuter(context.steps); + executer.start(); + } + }); + + // execute after handlers right after the Ajax request is fired + self._executeHandlers(attrs.ah, attrs); + we.publish(topic.AJAX_CALL_AFTER, attrs); + + return jqXHR; + }, + + /** + * Method that processes a manually supplied <ajax-response>. + * + * @param data {XmlDocument} - the <ajax-response> XML document + */ + process: function(data) { + var context = { + attrs: {}, + steps: [] + }; + var xmlDocument = Wicket.Xml.parse(data); + this.loadedCallback(xmlDocument, context); + var executer = new FunctionsExecuter(context.steps); + executer.start(); + }, + + /** + * Method that processes the <ajax-response> in the context of an XMLHttpRequest. + * + * @param data {XmlDocument} - the <ajax-response> XML document + * @param textStatus {String} - the response status as text (e.g. 'success', 'parsererror', etc.) + * @param jqXHR {Object} - the jQuery wrapper around XMLHttpRequest + * @param context {Object} - the request context with the Ajax request attributes and the FunctionExecuter's steps + */ + processAjaxResponse: function (data, textStatus, jqXHR, context) { + + if (jqXHR.readyState === 4) { + + // first try to get the redirect header + var redirectUrl; + try { + redirectUrl = jqXHR.getResponseHeader('Ajax-Location'); + } catch (ignore) { // might happen in older mozilla + } + + // the redirect header was set, go to new url + if (typeof(redirectUrl) !== "undefined" && redirectUrl !== null && redirectUrl !== "") { + + // In case the page isn't really redirected. For example say the redirect is to an octet-stream. + // A file download popup will appear but the page in the browser won't change. + this.success(context); + + var withScheme = /^[a-z][a-z0-9+.-]*:\/\//; // checks whether the string starts with a scheme + + // support/check for non-relative redirectUrl like as provided and needed in a portlet context + if (redirectUrl.charAt(0) === '/' || withScheme.test(redirectUrl)) { + context.isRedirecting = true; + Wicket.Ajax.redirect(redirectUrl); + } + else { + var urlDepth = 0; + while (redirectUrl.substring(0, 3) === "../") { + urlDepth++; + redirectUrl = redirectUrl.substring(3); + } + // Make this a string. + var calculatedRedirect = window.location.pathname; + while (urlDepth > -1) { + urlDepth--; + var i = calculatedRedirect.lastIndexOf("/"); + if (i > -1) { + calculatedRedirect = calculatedRedirect.substring(0, i); + } + } + calculatedRedirect += "/" + redirectUrl; + + context.isRedirecting = true; + Wicket.Ajax.redirect(calculatedRedirect); + } + } + else { + // no redirect, just regular response + Wicket.Log.info("Received ajax response (%s characters)", jqXHR.responseText.length); + Wicket.Log.debug(jqXHR.responseXML); + + // invoke the loaded callback with an xml document + return this.loadedCallback(data, context); + } + } + }, + + // Processes the response + loadedCallback: function (envelope, context) { + // To process the response, we go through the xml document and add a function for every action (step). + // After this is done, a FunctionExecuter object asynchronously executes these functions. + // The asynchronous execution is necessary, because some steps might involve loading external javascript, + // which must be asynchronous, so that it doesn't block the browser, but we also have to maintain + // the order in which scripts are loaded and we have to delay the next steps until the script is + // loaded. + try { + var root = envelope.getElementsByTagName("ajax-response")[0]; + + // the root element must be <ajax-response + if (isUndef(root) || root.tagName !== "ajax-response") { + this.failure(context, null, "Could not find root <ajax-response> element", null); + return; + } + + var steps = context.steps; + + // go through the ajax response and execute all priority-invocations first + for (var i = 0; i < root.childNodes.length; ++i) { + var childNode = root.childNodes[i]; + if (childNode.tagName === "header-contribution") { + this.processHeaderContribution(context, childNode); + } else if (childNode.tagName === "priority-evaluate") { + this.processEvaluation(context, childNode); + } + } + + // go through the ajax response and for every action (component, js evaluation, header contribution) + // ad the proper closure to steps + var stepIndexOfLastReplacedComponent = -1; + for (var c = 0; c < root.childNodes.length; ++c) { + var node = root.childNodes[c]; + + if (node.tagName === "component") { + if (stepIndexOfLastReplacedComponent === -1) { + this.processFocusedComponentMark(context); + } + stepIndexOfLastReplacedComponent = steps.length; + this.processComponent(context, node); + } else if (node.tagName === "evaluate") { + this.processEvaluation(context, node); + } else if (node.tagName === "redirect") { + this.processRedirect(context, node); + } + + } + if (stepIndexOfLastReplacedComponent !== -1) { + this.processFocusedComponentReplaceCheck(steps, stepIndexOfLastReplacedComponent); + } + + // add the last step, which should trigger the success call the done method on request + this.success(context); + + } catch (exception) { + this.failure(context, null, exception, null); + } + }, + + // Adds a closure to steps that should be invoked after all other steps have been successfully executed + success: function (context) { + context.steps.push(function (notify) { + Wicket.Log.info("Response processed successfully."); + + var attrs = context.attrs; + this._executeHandlers(attrs.sh, attrs, null, null, 'success'); + Wicket.Event.publish(Wicket.Event.Topic.AJAX_CALL_SUCCESS, attrs, null, null, 'success'); + + Wicket.Focus.requestFocus(); + + // continue to next step (which should make the processing stop, as success should be the final step) + return FunctionsExecuter.DONE; + }.bind(this)); + }, + + // On ajax request failure + failure: function (context, jqXHR, errorMessage, textStatus) { + context.steps.push(function (notify) { + if (errorMessage) { + Wicket.Log.error("Wicket.Ajax.Call.failure: Error while parsing response: %s", errorMessage); + } + var attrs = context.attrs; + this._executeHandlers(attrs.fh, attrs, jqXHR, errorMessage, textStatus); + Wicket.Event.publish(Wicket.Event.Topic.AJAX_CALL_FAILURE, attrs, jqXHR, errorMessage, textStatus); + + return FunctionsExecuter.DONE; + }.bind(this)); + }, + + done: function (attrs) { + this._executeHandlers(attrs.dh, attrs); + Wicket.Event.publish(Wicket.Event.Topic.AJAX_CALL_DONE, attrs); + + Wicket.channelManager.done(attrs.ch); + }, + + // Adds a closure that replaces a component + processComponent: function (context, node) { + context.steps.push(function (notify) { + // get the component id + var compId = node.getAttribute("id"); + + // get existing component + var element = Wicket.$(compId); + + if (isUndef(element)) { + Wicket.Log.error("Wicket.Ajax.Call.processComponent: Component with id '%s' was not found while trying to perform markup update. " + + "Make sure you called component.setOutputMarkupId(true) on the component whose markup you are trying to update.", compId); + } else { + var text = Wicket.DOM.text(node); + + // replace the component + Wicket.DOM.replace(element, text); + } + // continue to next step + return FunctionsExecuter.DONE; + }); + }, + + /** + * Adds a closure that evaluates javascript code. + * @param context {Object} - the object that brings the executer's steps and the attributes + * @param node {XmlElement} - the <[priority-]evaluate> element with the script to evaluate + */ + processEvaluation: function (context, node) { + + // used to match evaluation scripts which manually call FunctionsExecuter's notify() when ready + var scriptWithIdentifierR = new RegExp("\\(function\\(\\)\\{([a-zA-Z_]\\w*)\\|((.|\\n)*)?\\}\\)\\(\\);$"); + + /** + * A regex used to split the text in (priority-)evaluate elements in the Ajax response + * when there are scripts which require manual call of 'FunctionExecutor#notify()' + * @type {RegExp} + */ + var scriptSplitterR = new RegExp("\\(function\\(\\)\\{[\\s\\S]*?}\\)\\(\\);", 'gi'); + + // get the javascript body + var text = Wicket.DOM.text(node); + + // aliases to improve performance + var steps = context.steps; + var log = Wicket.Log; + + var evaluateWithManualNotify = function (parameters, body) { + return function(notify) { + var toExecute = "(function(" + parameters + ") {" + body + "})"; + + try { + // do the evaluation in global scope + var f = window.eval(toExecute); + f(notify); + } catch (exception) { + log.error("Wicket.Ajax.Call.processEvaluation: Exception evaluating javascript: %s", text, exception); + } + return FunctionsExecuter.ASYNC; + }; + }; + + var evaluate = function (script) { + return function(notify) { + // just evaluate the javascript + try { + // do the evaluation in global scope + window.eval(script); + } catch (exception) { + log.error("Ajax.Call.processEvaluation: Exception evaluating javascript: %s", text, exception); + } + // continue to next step + return FunctionsExecuter.DONE; + }; + }; + + // test if the javascript is in form of identifier|code + // if it is, we allow for letting the javascript decide when the rest of processing will continue + // by invoking identifier();. This allows usage of some asynchronous/deferred logic before the next script + // See WICKET-5039 + if (scriptWithIdentifierR.test(text)) { + var scripts = []; + var scr; + while ( (scr = scriptSplitterR.exec(text) ) !== null ) { + scripts.push(scr[0]); + } + + for (var s = 0; s < scripts.length; s++) { + var script = scripts[s]; + if (script) { + var scriptWithIdentifier = script.match(scriptWithIdentifierR); + if (scriptWithIdentifier) { + steps.push(evaluateWithManualNotify(scriptWithIdentifier[1], scriptWithIdentifier[2])); + } + else { + steps.push(evaluate(script)); + } + } + } + } else { + steps.push(evaluate(text)); + } + }, + + // Adds a closure that processes a header contribution + processHeaderContribution: function (context, node) { + var c = Wicket.Head.Contributor; + c.processContribution(context, node); + }, + + // Adds a closure that processes a redirect + processRedirect: function (context, node) { + var text = Wicket.DOM.text(node); + Wicket.Log.info("Redirecting to: %s", text); + context.isRedirecting = true; + Wicket.Ajax.redirect(text); + }, + + // mark the focused component so that we know if it has been replaced by response + processFocusedComponentMark: function (context) { + context.steps.push(function (notify) { + Wicket.Focus.markFocusedComponent(); + + // continue to next step + return FunctionsExecuter.DONE; + }); + }, + + // detect if the focused component was replaced + processFocusedComponentReplaceCheck: function (steps, lastReplaceComponentStep) { + // add this step imediately after all components have been replaced + steps.splice(lastReplaceComponentStep + 1, 0, function (notify) { + Wicket.Focus.checkFocusedComponentReplaced(); + + // continue to next step + return FunctionsExecuter.DONE; + }); + } + }; + + + /** + * Throttler's purpose is to make sure that ajax requests wont be fired too often. + */ + Wicket.ThrottlerEntry = Wicket.Class.create(); + + Wicket.ThrottlerEntry.prototype = { + initialize: function (func) { + this.func = func; + this.timestamp = new Date().getTime(); + this.timeoutVar = undefined; + }, + + getTimestamp: function () { + return this.timestamp; + }, + + getFunc: function () { + return this.func; + }, + + setFunc: function (func) { + this.func = func; + }, + + getTimeoutVar: function () { + return this.timeoutVar; + }, + + setTimeoutVar: function (timeoutVar) { + this.timeoutVar = timeoutVar; + } + }; + + Wicket.Throttler = Wicket.Class.create(); + + // declare it as static so that it can be shared between Throttler instances + Wicket.Throttler.entries = []; + + Wicket.Throttler.prototype = { + + /* "postponeTimerOnUpdate" is an optional parameter. If it is set to true, then the timer is + reset each time the throttle function gets called. Use this behaviour if you want something + to happen at X milliseconds after the *last* call to throttle. + If the parameter is not set, or set to false, then the timer is not reset. */ + initialize: function (postponeTimerOnUpdate) { + this.postponeTimerOnUpdate = postponeTimerOnUpdate; + }, + + throttle: function (id, millis, func) { + var entries = Wicket.Throttler.entries; + var entry = entries[id]; + var me = this; + if (typeof(entry) === 'undefined') { + entry = new Wicket.ThrottlerEntry(func); + entry.setTimeoutVar(window.setTimeout(function() { me.execute(id); }, millis)); + entries[id] = entry; + } else { + entry.setFunc(func); + if (this.postponeTimerOnUpdate) + { + window.clearTimeout(entry.getTimeoutVar()); + entry.setTimeoutVar(window.setTimeout(function() { me.execute(id); }, millis)); + } + } + }, + + execute: function (id) { + var entries = Wicket.Throttler.entries; + var entry = entries[id]; + if (typeof(entry) !== 'undefined') { + var func = entry.getFunc(); + entries[id] = undefined; + return func(); + } + } + }; + + Wicket = extend(true, Wicket, { + + channelManager: new Wicket.ChannelManager(), + + throttler: new Wicket.Throttler(), + + $: function (arg) { + return Wicket.DOM.get(arg); + }, + + /** + * returns if the element belongs to current document + * if the argument is not element, function returns true + */ + $$: function (element) { + return Wicket.DOM.inDoc(element); + }, + + /** + * Merges two objects. Values of the second will overwrite values of the first. + * + * @param {Object} object1 - the first object to merge + * @param {Object} object2 - the second object to merge + * @return {Object} a new object with the values of object1 and object2 + */ + merge: function(object1, object2) { + return extend({}, object1, object2); + }, + + /** + * Takes a function and returns a new one that will always have a particular context, i.e. 'this' will be the passed context. + * + * @param {Function} fn - the function which context will be set + * @param {Object} context - the new context for the function + * @return {Function} the original function with the changed context + */ + bind: function(fn, context) { + return fn.bind(context); + }, + + Xml: { + parse: function (text) { + var parser = new DOMParser(); + + var xmlDocument = parser.parseFromString(text, "text/xml"); + + return xmlDocument; + } + }, + + /** + * Form serialization + * + * To post a form using Ajax Wicket first needs to serialize it, which means composing a string + * from form elments names and values. The string will then be set as body of POST request. + */ + + Form: { + encode: function (text) { + if (window.encodeURIComponent) { + return window.encodeURIComponent(text); + } else { + return window.escape(text); + } + }, + + /** + * Serializes HTMLFormSelectElement to URL encoded key=value string. + * + * @param select {HTMLFormSelectElement} - the form element to serialize + * @return an object of key -> value pair where 'value' can be an array of Strings if the select is .multiple, + * or empty object if the form element is disabled. + */ + serializeSelect: function (select){ + var result = []; + if (select) { + var $select = jQuery(select); + if ($select.length > 0 && $select.prop('disabled') === false) { + var name = $select.prop('name'); + var values = $select.val(); + if (Array.isArray(values)) { + for (var v = 0; v < values.length; v++) { + var value = values[v]; + result.push( { name: name, value: value } ); + } + } else { + result.push( { name: name, value: values } ); + } + } + } + return result; + }, + + /** + * Serializes a form element to an array with a single element - an object + * with two keys - <em>name</em> and <em>value</em>. + * + * Example: [{"name": "searchTerm", "value": "abc"}]. + * + * Note: this function intentionally ignores image and submit inputs. + * + * @param input {HtmlFormElement} - the form element to serialize + * @return the URL encoded key=value pair or empty string if the form element is disabled. + */ + serializeInput: function (input) { + var result = []; + if (input && input.type) { + var $input = jQuery(input); + + if (input.type === 'file') { + for (var f = 0; f < input.files.length; f++) { + result.push({"name" : input.name, "value" : input.files[f]}); + } + } else if (!(input.type === 'image' || input.type === 'submit')) { + result = $input.serializeArray(); + } + } + return result; + }, + + /** + * A hash of HTML form element to exclude from serialization + * As key the element's id is being used. + * As value - the string "true". + */ + excludeFromAjaxSerialization: { + }, + + /** + * Serializes a form element by checking its type and delegating the work to + * a more specific function. + * + * The form element will be ignored if it is registered as excluded in + * <em>Wicket.Form.excludeFromAjaxSerialization</em> + * + * @param element {HTMLFormElement} - the form element to serialize. E.g. HTMLInputElement + * @param serializeRecursively {Boolean} - a flag indicating whether to collect (submit) the + * name/value pairs for all HTML form elements children of the HTML element with + * the JavaScript listener + * @return An array with a single element - an object with two keys - <em>name</em> and <em>value</em>. + */ + serializeElement: function(element, serializeRecursively) { + + if (!element) { + return []; + } + else if (typeof(element) === 'string') { + element = Wicket.$(element); + } + + if (Wicket.Form.excludeFromAjaxSerialization && element.id && Wicket.Form.excludeFromAjaxSerialization[element.id] === "true") { + return []; + } + + var tag = element.tagName.toLowerCase(); + if (tag === "select") { + return Wicket.Form.serializeSelect(element); + } else if (tag === "input" || tag === "textarea") { + return Wicket.Form.serializeInput(element); + } else { + var result = []; + if (serializeRecursively) { + var elements = nodeListToArray(element.getElementsByTagName("input")); + elements = elements.concat(nodeListToArray(element.getElementsByTagName("select"))); + elements = elements.concat(nodeListToArray(element.getElementsByTagName("textarea"))); + + for (var i = 0; i < elements.length; ++i) { + var el = elements[i]; + if (el.name && el.name !== "") { + result = result.concat(Wicket.Form.serializeElement(el, serializeRecursively)); + } + } + } + return result; + } + }, + + serializeForm: function (form) { + var result = [], + elements; + + if (form) { + if (form.tagName.toLowerCase() === 'form') { + elements = form.elements; + } else { + do { + form = form.parentNode; + } while (form.tagName.toLowerCase() !== "form" && form.tagName.toLowerCase() !== "body"); + + elements = nodeListToArray(form.getElementsByTagName("input")); + elements = elements.concat(nodeListToArray(form.getElementsByTagName("select"))); + elements = elements.concat(nodeListToArray(form.getElementsByTagName("textarea"))); + } + } + + for (var i = 0; i < elements.length; ++i) { + var el = elements[i]; + if (el.name && el.name !== "") { + result = result.concat(Wicket.Form.serializeElement(el, false)); + } + } + return result; + }, + + serialize: function (element, dontTryToFindRootForm) { + if (typeof(element) === 'string') { + element = Wicket.$(element); + } + + if (element.tagName.toLowerCase() === "form") { + return Wicket.Form.serializeForm(element); + } else { + // try to find a form in DOM parents + var elementBck = element; + + if (dontTryToFindRootForm !== true) { + do { + element = element.parentNode; + } while(element.tagName.toLowerCase() !== "form" && element.tagName.toLowerCase() !== "body"); + } + + if (element.tagName.toLowerCase() === "form"){ + return Wicket.Form.serializeForm(element); + } else { + // there is not form in dom hierarchy + // simulate it + var form = document.createElement("form"); + var parent = elementBck.parentNode; + + parent.replaceChild(form, elementBck); + form.appendChild(elementBck); + var result = Wicket.Form.serializeForm(form); + parent.replaceChild(elementBck, form); + + return result; + } + } + } + }, + + /** + * DOM nodes serialization functionality + * + * The purpose of these methods is to return a string representation + * of the DOM tree. + */ + DOM: { + + /** + * Shows an element + * @param {HTMLElement | String} e The HTML element (or its id) to show + * @param {String} display The value of CSS display property to use, + * e.g. 'block', 'inline'. Optional + */ + show: function (e, display) { + e = Wicket.$(e); + if (e !== null) { + if (isUndef(display)) { + // no explicit 'display' value is requested so + // use jQuery. It has special logic to decide which is the + // best value for an HTMLElement + jQuery(e).show(); + } else { + e.style.display = display; + } + } + }, + + /** hides an element */ + hide: function (e) { + e = Wicket.$(e); + if (e !== null) { + jQuery(e).hide(); + } + }, + + /** call-counting implementation of Wicket.DOM.show() */ + showIncrementally: function (e) { + e = Wicket.$(e); + if (e === null) { + return; + } + var count = e.getAttribute("showIncrementallyCount"); + count = parseInt(isUndef(count) ? 0 : count, 10); + if (count >= 0) { + Wicket.DOM.show(e); + } + e.setAttribute("showIncrementallyCount", count + 1); + }, + + /** call-counting implementation of Wicket.DOM.hide() */ + hideIncrementally: function(e) { + e = Wicket.$(e); + if (e === null) { + return; + } + var count = e.getAttribute("showIncrementallyCount"); + count = parseInt(isUndef(count) ? 0 : count - 1, 10); + if (count <= 0) { + Wicket.DOM.hide(e); + } + e.setAttribute("showIncrementallyCount", count); + }, + + get: function (arg) { + if (isUndef(arg)) { + return null; + } + if (arguments.length > 1) { + var e = []; + for (var i = 0; i < arguments.length; i++) { + e.push(Wicket.DOM.get(arguments[i])); + } + return e; + } else if (typeof arg === 'string') { + return document.getElementById(arg); + } else { + return arg; + } + }, + + /** + * returns if the element belongs to current document + * if the argument is not element, function returns true + */ + inDoc: function (element) { + if (isWindow(element)) { + return true; + } + if (typeof(element) === "string") { + element = Wicket.$(element); + } + if (isUndef(element) || isUndef(element.tagName)) { + return false; + } + + var id = element.getAttribute('id'); + if (isUndef(id) || id === "") { + return element.ownerDocument === document; + } + else { + return document.getElementById(id) === element; + } + }, + + /** + * A cross-browser method that replaces the markup of an element. The behavior + * is similar to calling element.outerHtml=text in internet explorer. However + * this method also takes care of executing javascripts within the markup on + * browsers that don't do that automatically. + * Also this method takes care of replacing table elements (tbody, tr, td, thead) + * on browser where it's not supported when using outerHTML (IE). + * + * This method sends notifications to all subscribers for channels with names + * '/dom/node/removing' with the element that is going to be replaced and + * '/dom/node/added' with the newly created element (the replacement). + * + * Note: the 'to be replaced' element must have an 'id' attribute + */ + replace: function (element, text) { + + var we = Wicket.Event; + var topic = we.Topic; + + we.publish(topic.DOM_NODE_REMOVING, element); + + if (element.tagName.toLowerCase() === "title") { + // match the text between the tags + var titleText = />(.*?)</.exec(text)[1]; + document.title = titleText; + return; + } else { + // jQuery 1.9+ expects '<' as the very first character in text + var cleanedText = jQuery.trim(text); + + var $newElement = jQuery(cleanedText); + jQuery(element).replaceWith($newElement); + } + + var newElement = Wicket.$(element.id); + if (newElement) { + we.publish(topic.DOM_NODE_ADDED, newElement); + } + }, + + // Method for serializing DOM nodes to string + // original taken from Tacos (http://tacoscomponents.jot.com) + serializeNodeChildren: function (node) { + if (isUndef(node)) { + return ""; + } + var result = []; + + if (node.childNodes.length > 0) { + for (var i = 0; i < node.childNodes.length; i++) { + var thisNode = node.childNodes[i]; + switch (thisNode.nodeType) { + case 1: // ELEMENT_NODE + case 5: // ENTITY_REFERENCE_NODE + result.push(this.serializeNode(thisNode)); + break; + case 8: // COMMENT + result.push("<!--"); + result.push(thisNode.nodeValue); + result.push("-->"); + break; + case 4: // CDATA_SECTION_NODE + result.push("<![CDATA["); + result.push(thisNode.nodeValue); + result.push("]]>"); + break; + case 3: // TEXT_NODE + case 2: // ATTRIBUTE_NODE + result.push(thisNode.nodeValue); + break; + default: + break; + } + } + } else { + result.push(node.textContent || node.text); + } + return result.join(""); + }, + + serializeNode: function (node){ + if (isUndef(node)) { + return ""; + } + var result = []; + result.push("<"); + result.push(node.nodeName); + + if (node.attributes && node.attributes.length > 0) { + + for (var i = 0; i < node.attributes.length; i++) { + // serialize the attribute only if it has meaningful value that is not inherited + if (node.attributes[i].nodeValue && node.attributes[i].specified) { + result.push(" "); + result.push(node.attributes[i].name); + result.push("=\""); + result.push(node.attributes[i].value); + result.push("\""); + } + } + } + + result.push(">"); + result.push(Wicket.DOM.serializeNodeChildren(node)); + result.push("</"); + result.push(node.nodeName); + result.push(">"); + return result.join(""); + }, + + // Utility function that determines whether given element is part of the current document + containsElement: function (element) { + var id = element.getAttribute("id"); + if (id) { + return Wicket.$(id) !== null; + } + else { + return false; + } + }, + + /** + * Reads the text from the node's children nodes. + * Used instead of jQuery.text() because it is very slow in IE10/11. + * WICKET-5132, WICKET-5510 + * @param node {DOMElement} the root node + */ + text: function (node) { + if (isUndef(node)) { + return ""; + } + + var result = []; + + if (node.childNodes.length > 0) { + for (var i = 0; i < node.childNodes.length; i++) { + var thisNode = node.childNodes[i]; + switch (thisNode.nodeType) { + case 1: // ELEMENT_NODE + case 5: // ENTITY_REFERENCE_NODE + result.push(this.text(thisNode)); + break; + case 3: // TEXT_NODE + case 4: // CDATA_SECTION_NODE + result.push(thisNode.nodeValue); + break; + default: + break; + } + } + } else { + result.push(node.textContent || node.text); + } + + return result.join(""); + } + }, + + /** + * The Ajax class handles low level details of creating XmlHttpRequest objects, + * as well as registering and execution of pre-call, post-call and failure handlers. + */ + Ajax: { + + Call: Wicket.Ajax.Call, + + /** + * Aborts the default event if attributes request it + * + * @param {Object} attrs - the Ajax request attributes configured at the server side + */ + _handleEventCancelation: function(attrs) { + var evt = attrs.event; + if (evt) { + if (attrs.pd) { + try { + evt.preventDefault(); + } catch (ignore) { + // WICKET-4986 + // jquery fails 'member not found' with calls on busy channel + } + } + + if (attrs.sp === "stop") { + Wicket.Event.stop(evt); + } else if (attrs.sp === "stopImmediate") { + Wicket.Event.stop(evt, true); + } + } + }, + + get: function (attrs) { + + attrs.m = 'GET'; + + return Wicket.Ajax.ajax(attrs); + }, + + post: function (attrs) { + + attrs.m = 'POST'; + + return Wicket.Ajax.ajax(attrs); + }, + + ajax: function(attrs) { + + attrs.c = attrs.c || window; + attrs.e = attrs.e || [ 'domready' ]; + + if (!Array.isArray(attrs.e)) { + attrs.e = [ attrs.e ]; + } + + Array.prototype.forEach.call(attrs.e, function (evt) { + Wicket.Event.add(attrs.c, evt, function (jqEvent, data) { + var call = new Wicket.Ajax.Call(); + var attributes = extend({}, attrs); + + if (evt !== "domready") { + attributes.event = Wicket.Event.fix(jqEvent); + if (data) { + attributes.event.extraData = data; + } + } + + call._executeHandlers(attributes.ih, attributes); + Wicket.Event.publish(Wicket.Event.Topic.AJAX_CALL_INIT, attributes); + + var throttlingSettings = attributes.tr; + if (throttlingSettings) { + var postponeTimerOnUpdate = throttlingSettings.p || false; + var throttler = new Wicket.Throttler(postponeTimerOnUpdate); + throttler.throttle(throttlingSettings.id, throttlingSettings.d, + Wicket.bind(function () { + call.ajax(attributes); + }, this)); + } + else { + call.ajax(attributes); + } + if (evt !== "domready") { + Wicket.Ajax._handleEventCancelation(attributes); + } + }, null, attrs.sel); + }); + }, + + process: function(data) { + var call = new Wicket.Ajax.Call(); + call.process(data); + }, + + /** + * An abstraction over native window.location.replace() to be able to suppress it for unit tests + * + * @param url The url to redirect to + */ + redirect: function(url) { + window.location = url; + } + }, + + /** + * Header contribution allows component to include custom javascript and stylesheet. + * + * Header contributor takes the code component would render to page head and + * interprets it just as browser would when loading a page. + * That means loading external javascripts and stylesheets, executing inline + * javascript and aplying inline styles. + * + * Header contributor also filters duplicate entries, so that it doesn't load/process + * resources that have been loaded. + * For inline styles and javascript, element id is used to filter out duplicate entries. + * For stylesheet and javascript references, url is used for filtering. + */ + Head: { + Contributor: { + + // Parses the header contribution element (returns a DOM tree with the contribution) + parse: function (headerNode) { + // the header contribution is stored as CDATA section in the header-contribution element, + // we need to parse it since each header contribution needs to be treated separately + + // get the header contribution text and unescape it if necessary + var text = Wicket.DOM.text(headerNode); + + // build a DOM tree of the contribution + var xmldoc = Wicket.Xml.parse(text); + return xmldoc; + }, + + // checks whether the passed node is the special "parsererror" + // created by DOMParser if there is a error in XML parsing + // TODO: move out of the API section + _checkParserError: function (node) { + var result = false; + + if (!isUndef(node.tagName) && node.tagName.toLowerCase() === "parsererror") { + Wicket.Log.error("Error in parsing: %s", node.textContent); + result = true; + } + return result; + }, + + // Processes the parsed header contribution + processContribution: function (context, headerNode) { + var xmldoc = this.parse(headerNode); + var rootNode = xmldoc.documentElement; + + // Firefox and Opera reports the error in the documentElement + if (this._checkParserError(rootNode)) { + return; + } + + // go through the individual elements and process them according to their type + for (var i = 0; i < rootNode.childNodes.length; i++) { + var node = rootNode.childNodes[i]; + + // Chromium reports the error as a child node + if (this._checkParserError(node)) { + return; + } + + if (!isUndef(node.tagName)) { + var name = node.tagName.toLowerCase(); + + // it is possible that a reference is surrounded by a <wicket:link + // in that case, we need to find the inner element + if (name === "wicket:link") { + for (var j = 0; j < node.childNodes.length; ++j) { + var childNode = node.childNodes[j]; + // try to find a regular node inside wicket:link + if (childNode.nodeType === 1) { + node = childNode; + name = node.tagName.toLowerCase(); + break; + } + } + } + + // process the element + if (name === "link") { + this.processLink(context, node); + } else if (name === "script") { + this.processScript(context, node); + } else if (name === "style") { + this.processStyle(context, node); + } else if (name === "meta") { + this.processMeta(context, node); + } + } else if (node.nodeType === 8) { // comment type + this.processComment(context, node); + } + } + }, + + // Process an external stylesheet element + processLink: function (context, node) { + context.steps.push(function (notify) { + var res = Wicket.Head.containsElement(node, "href"); + var oldNode = res.oldNode; + if (res.contains) { + // an element with same href attribute is in document, skip it + return FunctionsExecuter.DONE; + } else if (oldNode) { + // remove another external element with the same id but different href + oldNode.parentNode.removeChild(oldNode); + } + + // create link element + var css = Wicket.Head.createElement("link"); + + // copy supplied attributes only. + var attributes = jQuery(node).prop("attributes"); + var $css = jQuery(css); + Array.prototype.forEach.call(attributes, function(attr) { + $css.attr(attr.name, attr.value); + }); + + // add element to head + Wicket.Head.addElement(css); + + // cross browser way to check when the css is loaded + // taken from http://www.backalleycoder.com/2011/03/20/link-tag-css-stylesheet-load-event/ + // this makes a second GET request to the css but it gets it either from the cache or + // downloads just the first several bytes and realizes that the MIME is wrong and ignores the rest + var img = document.createElement('img'); + var notifyCalled = false; + img.onerror = function () { + if (!notifyCalled) { + notifyCalled = true; + notify(); + } + }; + img.src = css.href; + if (img.complete) { + if (!notifyCalled) { + notifyCalled = true; + notify(); + } + } + + return FunctionsExecuter.ASYNC; + }); + }, + + // Process an inline style element + processStyle: function (context, node) { + context.steps.push(function (notify) { + // if element with same id is already in document, skip it + if (Wicket.DOM.containsElement(node)) { + return FunctionsExecuter.DONE; + } + // serialize the style to string + var content = Wicket.DOM.serializeNodeChildren(node); + + // create style element + var style = Wicket.Head.createElement("style"); + + // copy id attribute + style.id = node.getAttribute("id"); + + var textNode = document.createTextNode(content); + style.appendChild(textNode); + + Wicket.Head.addElement(style); + + // continue to next step + return FunctionsExecuter.DONE; + }); + }, + + // Process a script element (both inline and external) + processScript: function (context, node) { + context.steps.push(function (notify) { + + if (!node.getAttribute("src") && Wicket.DOM.containsElement(node)) { + // if an inline element with same id is already in document, skip it + return FunctionsExecuter.DONE; + } else { + var res = Wicket.Head.containsElement(node, "src"); + var oldNode = res.oldNode; + if (res.contains) { + // an element with same src attribute is in document, skip it + return FunctionsExecuter.DONE; + } else if (oldNode) { + // remove another external element with the same id but different src + oldNode.parentNode.removeChild(oldNode); + } + } + + // determine whether it is external javascript (has src attribute set) + var src = node.getAttribute("src"); + + if (src !== null && src !== "") { + + // convert the XML node to DOM node + var scriptDomNode = document.createElement("script"); + + var attrs = node.attributes; + for (var a = 0; a < attrs.length; a++) { + var attr = attrs[a]; + scriptDomNode[attr.name] = attr.value; + } + + var onScriptReady = function () { + notify(); + }; + + // first check for feature support + if (typeof(scriptDomNode.onload) !== 'undefined') { + scriptDomNode.onload = onScriptReady; + } else if (typeof(scriptDomNode.onreadystatechange) !== 'undefined') { + scriptDomNode.onreadystatechange = function () { + if (scriptDomNode.readyState === 'loaded' || scriptDomNode.readyState === 'complete') { + onScriptReady(); + } + }; + } else { + // as a final resort notify after the current function execution + window.setTimeout(onScriptReady, 10); + } + + Wicket.Head.addElement(scriptDomNode); + + return FunctionsExecuter.ASYNC; + } else { + // serialize the element content to string + var text = Wicket.DOM.serializeNodeChildren(node); + // get rid of prefix and suffix, they are not eval-d correctly + text = text.replace(/^\n\/\*<!\[CDATA\[\*\/\n/, ""); + text = text.replace(/\n\/\*\]\]>\*\/\n$/, ""); + + var id = node.getAttribute("id"); + var type = node.getAttribute("type"); + + if (typeof(id) === "string" && id.length > 0) { + // add javascript to document head + Wicket.Head.addJavascript(text, id, "", type); + } else { + try { + // do the evaluation in global scope + window.eval(text); + } catch (e) { + Wicket.Log.error("Wicket.Head.Contributor.processScript: %s", text, e); + } + } + + // continue to next step + return FunctionsExecuter.DONE; + } + }); + }, + + processMeta: function (context, node) { + context.steps.push(function (notify) { + var meta = Wicket.Head.createElement("meta"), + $meta = jQuery(meta), + attrs = jQuery(node).prop("attributes"), + name = node.getAttribute("name"); + + if(name) { + jQuery('meta[name="' + name + '"]').remove(); + } + Array.prototype.forEach.call(attrs, function(attr) { + $meta.attr(attr.name, attr.value); + }); + + Wicket.Head.addElement(meta); + + return FunctionsExecuter.DONE; + }); + }, + + // process (conditional) comments + processComment: function (context, node) { + context.steps.push(function (notify) { + var comment = document.createComment(node.nodeValue); + Wicket.Head.addElement(comment); + return FunctionsExecuter.DONE; + }); + } + }, + + // Creates an element in document + createElement: function (name) { + if (isUndef(name) || name === '') { + Wicket.Log.error('Cannot create an element without a name'); + return; + } + return document.createElement(name); + }, + + // Adds the element to page head + addElement: function (element) { + var headItems = document.querySelector('head meta[name="wicket.header.items"]'); + if (headItems) { + headItems.parentNode.insertBefore(element, headItems); + } else { + var head = document.querySelector("head"); + + if (head) { + head.appendChild(element); + } + } + }, + + // Returns true, if the page head contains element that has attribute with + // name mandatoryAttribute same as the given element and their names match. + // + // e.g. Wicket.Head.containsElement(myElement, "src") return true, if there + // is an element in head that is of same type as myElement, and whose src + // attribute is same as myElement.src. + containsElement: function (element, mandatoryAttribute) { + var attr = element.getAttribute(mandatoryAttribute); + if (isUndef(attr) || attr === "") { + return { + contains: false + }; + } + + var elementTagName = element.tagName.toLowerCase(); + var elementId = element.getAttribute("id"); + var head = document.getElementsByTagName("head")[0]; + + if (elementTagName === "script") { + head = document; + } + + var nodes = head.getElementsByTagName(elementTagName); + + for (var i = 0; i < nodes.length; ++i) { + var node = nodes[i]; + + // check node names and mandatory attribute values + // we also have to check for attribute name that is suffixed by "_". + // this is necessary for filtering script references + if (node.tagName.toLowerCase() === elementTagName) { + + var loadedUrl = node.getAttribute(mandatoryAttribute); + var loadedUrl_ = node.getAttribute(mandatoryAttribute+"_"); + if (loadedUrl === attr || loadedUrl_ === attr) { + return { + contains: true + }; + } else if (elementId && elementId === node.getAttribute("id")) { + return { + contains: false, + oldNode: node + }; + } + } + } + return { + contains: false + }; + }, + + // Adds a javascript element to page header. + // The fakeSrc attribute is used to filter out duplicate javascript references. + // External javascripts are loaded using xmlhttprequest. Then a javascript element is created and the + // javascript body is used as text for the element. For javascript references, wicket uses the src + // attribute to filter out duplicates. However, since we set the body of the element, we can't assign + // also a src value. Therefore we put the url to the src_ (notice the underscore) attribute. + // Wicket.Head.containsElement is aware of that and takes also the underscored attributes into account. + addJavascript: function (content, id, fakeSrc, type) { + var script = Wicket.Head.createElement("script"); + if (id) { + script.id = id; + } + + // WICKET-5047: encloses the content with a try...catch... block if the content is javascript + // content is considered javascript if mime-type is empty (html5's default) or is 'text/javascript' + if (!type || type.toLowerCase() === "text/javascript") { + type = "text/javascript"; + content = 'try{'+content+'}catch(e){Wicket.Log.error(e);}'; + } + + script.setAttribute("src_", fakeSrc); + script.setAttribute("type", type); + + // set the javascript as element content + if (null === script.canHaveChildren || script.canHaveChildren) { + var textNode = document.createTextNode(content); + script.appendChild(textNode); + } else { + script.text = content; + } + Wicket.Head.addElement(script); + }, + + // Goes through all script elements contained by the element and add them to head + addJavascripts: function (element, contentFilter) { + function add(element) { + var src = element.getAttribute("src"); + var type = element.getAttribute("type"); + + // if it is a reference, just add it to head + if (src !== null && src.length > 0) { + var e = document.createElement("script"); + if (type) { + e.setAttribute("type",type); + } + e.setAttribute("src", src); + Wicket.Head.addElement(e); + } else { + var content = Wicket.DOM.serializeNodeChildren(element); + if (isUndef(content) || content === "") { + content = element.text; + } + + if (typeof(contentFilter) === "function") { + content = contentFilter(content); + } + + Wicket.Head.addJavascript(content, element.id, "", type); + } + } + if (typeof(element) !== "undefined" && + typeof(element.tagName) !== "undefined" && + element.tagName.toLowerCase() === "script") { + add(element); + } else { + // we need to check if there are any children, because Safari + // aborts when the element is a text node + if (element.childNodes.length > 0) { + var scripts = element.getElementsByTagName("script"); + for (var i = 0; i < scripts.length; ++i) { + add(scripts[i]); + } + } + } + } + }, + + // FOCUS FUNCTIONS + + Focus: { + lastFocusId : "", + refocusLastFocusedComponentAfterResponse : false, + focusSetFromServer : false, + + focusin: function (event) { + event = Wicket.Event.fix(event); + + var target = event.target; + if (target) { + var WF = Wicket.Focus; + WF.refocusLastFocusedComponentAfterResponse = false; + var id = target.id; + WF.lastFocusId = id; + Wicket.Log.info("focus set on '%s'", id); + } + }, + + focusout: function (event) { + event = Wicket.Event.fix(event); + + var target = event.target; + var WF = Wicket.Focus; + if (target && WF.lastFocusId === target.id) { + var id = target.id; + if (WF.refocusLastFocusedComponentAfterResponse) { + // replaced components seem to blur when replaced only on Safari - so do not modify lastFocusId so it gets refocused + Wicket.Log.info("focus removed from '%s' but ignored because of component replacement", id); + } else { + WF.lastFocusId = null; + Wicket.Log.info("focus removed from '%s'", id); + } + } + }, + + getFocusedElement: function () { + var lastFocusId = Wicket.Focus.lastFocusId; + if (lastFocusId) { + var focusedElement = Wicket.$(lastFocusId); + Wicket.Log.info("returned focused element:", focusedElement); + return focusedElement; + } + }, + + setFocusOnId: function (id) { + var WF = Wicket.Focus; + if (id) { + WF.refocusLastFocusedComponentAfterResponse = true; + WF.focusSetFromServer = true; + WF.lastFocusId = id; + Wicket.Log.info("focus set on '%s' from server side", id); + } else { + WF.refocusLastFocusedComponentAfterResponse = false; + Wicket.Log.info("refocus focused component after request stopped from server side"); + } + }, + + // mark the focused component so that we know if it has been replaced or not by response + markFocusedComponent: function () { + var WF = Wicket.Focus; + var focusedElement = WF.getFocusedElement(); + if (focusedElement) { + // create a property of the focused element that would not remain there if component is replaced + focusedElement.wasFocusedBeforeComponentReplacements = true; + WF.refocusLastFocusedComponentAfterResponse = true; + WF.focusSetFromServer = false; + } else { + WF.refocusLastFocusedComponentAfterResponse = false; + } + }, + + // detect if the focused component was replaced + checkFocusedComponentReplaced: function () { + var WF = Wicket.Focus; + if (WF.refocusLastFocusedComponentAfterResponse) { + var focusedElement = WF.getFocusedElement(); + if (focusedElement) { + if (typeof(focusedElement.wasFocusedBeforeComponentReplacements) !== "undefined") { + // focus component was not replaced - no need to refocus it + WF.refocusLastFocusedComponentAfterResponse = false; + } + } else { + // focused component dissapeared completely - no use to try to refocus it + WF.refocusLastFocusedComponentAfterResponse = false; + WF.lastFocusId = ""; + } + } + }, + + requestFocus: function() { + // if the focused component is replaced by the ajax response, a re-focus might be needed + // (if focus was not changed from server) but if not, and the focus component should + // remain the same, do not re-focus - fixes problem on IE6 for combos that have + // the popup open (refocusing closes popup) + var WF = Wicket.Focus; + if (WF.refocusLastFocusedComponentAfterResponse && WF.lastFocusId) { + var toFocus = Wicket.$(WF.lastFocusId); + + if (toFocus) { + Wicket.Log.info("Calling focus on '%s'", WF.lastFocusId); + + var safeFocus = function() { + try { + toFocus.focus(); + } catch (ignore) { + // WICKET-6209 IE fails if toFocus is disabled + } + }; + + if (WF.focusSetFromServer) { + // WICKET-5858 + window.setTimeout(safeFocus, 0); + } else { + // avoid loops like - onfocus triggering an event the modifies the tag => refocus => the event is triggered again + var temp = toFocus.onfocus; + toFocus.onfocus = null; + + // IE needs setTimeout (it seems not to call onfocus sync. when focus() is called + window.setTimeout(function () { safeFocus(); toFocus.onfocus = temp; }, 0); + } + } else { + WF.lastFocusId = ""; + Wicket.Log.info("Couldn't set focus on element with id '%s' because it is not in the page anymore", WF.lastFocusId); + } + } else if (WF.refocusLastFocusedComponentAfterResponse) { + Wicket.Log.info("last focus id was not set"); + } else { + Wicket.Log.info("refocus last focused component not needed/allowed"); + } + Wicket.Focus.refocusLastFocusedComponentAfterResponse = false; + } + }, + + /** + * Manages the functionality needed by AbstractAjaxTimerBehavior and its subclasses + */ + Timer: { + /** + * Schedules a timer + * @param {string} timerId - the identifier for the timer + * @param {function} f - the JavaScript function to execute after the timeout + * @param {number} delay - the timeout + */ + 'set': function(timerId, f, delay) { + if (typeof(Wicket.TimerHandles) === 'undefined') { + Wicket.TimerHandles = {}; + } + + Wicket.Timer.clear(timerId); + Wicket.TimerHandles[timerId] = setTimeout(function() { + Wicket.Timer.clear(timerId); + f(); + }, delay); + }, + + /** + * Clears a timer by its id + * @param {string} timerId - the identifier of the timer + */ + clear: function(timerId) { + if (Wicket.TimerHandles && Wicket.TimerHandles[timerId]) { + clearTimeout(Wicket.TimerHandles[timerId]); + delete Wicket.TimerHandles[timerId]; + } + }, + + /** + * Clear all remaining timers. + */ + clearAll: function() { + var WTH = Wicket.TimerHandles; + if (WTH) { + for (var th in WTH) { + if (WTH.hasOwnProperty(th)) { + Wicket.Timer.clear(th); + } + } + } + } + }, + + /** + * Events related code + * Based on code from Mootools (http://mootools.net) + */ + Event: { + idCounter: 0, + + getId: function (element) { + var $el = jQuery(element), + id = $el.prop("id"); + + if (typeof(id) === "string" && id.length > 0) { + return id; + } else { + id = "wicket-generated-id-" + Wicket.Event.idCounter++; + $el.prop("id", id); + return id; + } + }, + + keyCode: function (evt) { + return Wicket.Event.fix(evt).keyCode; + }, + + /** + * Prevent event from bubbling up in the element hierarchy. + * @param evt {Event} - the event to stop + * @param immediate {Boolean} - true if the event should not be handled by other listeners registered + * on the same HTML element. Optional + */ + stop: function (evt, immediate) { + evt = Wicket.Event.fix(evt); + if (immediate) { + evt.stopImmediatePropagation(); + } else { + evt.stopPropagation(); + } + return evt; + }, + + /** + * If no event is given as argument (IE), window.event is returned. + */ + fix: function (evt) { + return jQuery.event.fix(evt || window.event); + }, + + fire: function (element, event) { + jQuery(element).trigger(event); + }, + + /** + * Binds an event listener for an element + * + * Also supports the special 'domready' event on window. + * 'domready' is event fired when the DOM is complete, but + * before loading external resources (images, scripts, ...) + * + * @param element {HTMLElement | Window} The host HTML element + * @param type {String} The type of the DOM event + * @param fn {Function} The event handler to unbind + * @param data {Object} Extra data for the event + * @param selector {String} A selector string to filter the descendants of the selected + * elements that trigger the event. If the selector is null or omitted, + * the event is always triggered when it reaches the selected element. + */ + add: function (element, type, fn, data, selector) { + if (type === 'domready') { + if (document.readyState !== 'loading') { + fn(); + } else { + document.addEventListener('DOMContentLoaded', fn); + } + } else if (type === 'load' && isWindow(element)) { + if (document.readyState !== 'loading') { + fn(); + } else { + document.addEventListener('load', fn); + } + } else { + var el = element; + if (typeof(element) === 'string') { + el = Wicket.$(element); + } + + if (!el && Wicket.Log) { + Wicket.Log.error("Cannot bind a listener for event '%s' because the element is not in the DOM", type, element); + } + + // FIXME: how to pass sub-selector and data to the native impl ? + Wicket.$(el).addEventListener(type, fn, selector, data); + } + return element; + }, + + /** + * Unbinds an event listener for an element + * + * @param element {HTMLElement} The host HTML element + * @param type {String} The type of the DOM event + * @param fn {Function} The event handler to unbind + */ + remove: function (element, type, fn) { + Wicket.$(element).removeEventListener(type, fn); + }, + + /** + * Adds a subscriber for the passed topic. + * + * @param topic {String} - the channel name for which this subscriber will be notified + * If '*' then it will be notified for all topics + * @param subscriber {Function} - the callback to call when an event with this type is published + */ + subscribe: function (topic, subscriber) { + if (topic) { + document.addEventListener(topic, subscriber); + } + }, + + /** + * Un-subscribes a subscriber from a topic. + * @param topic {String} - the topic name. + * @param subscriber {Function} - the handler to un-subscribe. + */ + unsubscribe: function(topic, subscriber) { + if (topic) { + if (subscriber) { + document.removeEventListener(topic, subscriber); + } else { + document.removeEventListener(topic); + } + } + }, + + /** + * Sends a notification to all subscribers for the given topic. + * Subscribers for topic '*' receive the actual topic as first parameter, + * otherwise the topic is not passed to subscribers which listen for specific + * event types. + * + * @param topic {String} - the channel name for which all subscribers will be notified. + */ + publish: function (topic) { + if (topic) { + // cut the topic argument + var args = Array.prototype.slice.call(arguments).slice(1); + + document.dispatchEvent(new CustomEvent(topic, {detail: args})); + document.dispatchEvent(new CustomEvent('*', {detail: args})); + } + }, + + /** + * The names of the topics on which Wicket notifies + */ + Topic: { + DOM_NODE_REMOVING : '/dom/node/removing', + DOM_NODE_ADDED : '/dom/node/added', + AJAX_CALL_INIT : '/ajax/call/init', + AJAX_CALL_BEFORE : '/ajax/call/before', + AJAX_CALL_PRECONDITION : '/ajax/call/precondition', + AJAX_CALL_BEFORE_SEND : '/ajax/call/beforeSend', + AJAX_CALL_SUCCESS : '/ajax/call/success', + AJAX_CALL_COMPLETE : '/ajax/call/complete', + AJAX_CALL_AFTER : '/ajax/call/after', + AJAX_CALL_FAILURE : '/ajax/call/failure', + AJAX_CALL_DONE : '/ajax/call/done', + AJAX_HANDLERS_BOUND : '/ajax/handlers/bound' + } + } + }); + + // MISC FUNCTIONS + + /** + * Track focused element. + */ + Wicket.Event.add(window, 'focusin', Wicket.Focus.focusin); + Wicket.Event.add(window, 'focusout', Wicket.Focus.focusout); + + /** + * Clear any scheduled Ajax timers when leaving the current page + */ + Wicket.Event.add(window, "unload", function() { + Wicket.Timer.clearAll(); + }); + +})(); diff --git a/wicket-devutils/src/main/java/org/apache/wicket/devutils/debugbar/wicket-debugbar.js b/wicket-devutils/src/main/java/org/apache/wicket/devutils/debugbar/wicket-debugbar.js index 4d70320..da65e0f 100644 --- a/wicket-devutils/src/main/java/org/apache/wicket/devutils/debugbar/wicket-debugbar.js +++ b/wicket-devutils/src/main/java/org/apache/wicket/devutils/debugbar/wicket-debugbar.js @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -;(function (jQuery, undefined) { +;(function (undefined) { 'use strict'; @@ -51,32 +51,33 @@ } } - jQuery('#wicketDebugBarCollapse').on("click", function() { - var content = jQuery('#wicketDebugBarContents'); + Wicket.Event.add('wicketDebugBarCollapse', 'click', function() { + var content = Wicket.$('wicketDebugBarContents'); setExpandedCookie(!content.is(':visible')); content.toggle(400); }); - jQuery('#wicketDebugBarRemove').on("click", function() { - var bar = jQuery('#wicketDebugBar'); + Wicket.Event.add('wicketDebugBarRemove', 'click', function() { + var bar = Wicket.$('wicketDebugBar'); setExpandedCookie(!bar.is(':visible')); - bar.hide(); + Wicket.DOM.hide(bar); }); // determine state and set it if (getExpandedCookie() === 'false') { - jQuery('#wicketDebugBarContents').hide(); + Wicket.DOM.hide(Wicket.$('wicketDebugBarContents')); } var original = Wicket.Log.error; Wicket.Log.error = function() { original.apply(Wicket.Log, arguments); - - jQuery('#wicketDebugBar') - .addClass('wicketDebugBarError') - .one('animationend', function() { - jQuery(this).removeClass('wicketDebugBarError'); - }); + + var el = Wicket.$('wicketDebugBar'); + el.classList.add('wicketDebugBarError') + el.addEventListener('animationend', function() { + el.removeEventListener('animationend'); + el.classList.remove('wicketDebugBarError'); + }); }; }; -})(jQuery); +})();
