All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.tapestry5.tapestry.js Maven / Gradle / Ivy

Go to download

Central module for Tapestry, containing interfaces to the Java Servlet API and all core services and components.

There is a newer version: 5.8.6
Show newest version
/* Copyright 2007, 2008, 2009, 2010 The Apache Software Foundation
 *
 * Licensed 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.
 */

var Tapestry = {

	/**
	 * Event that allows observers to perform cross-form validation after
	 * individual fields have performed their validation. The form element is
	 * passed as the event memo. Observers may set the validationError property
	 * of the Form's Tapestry object to true (which will prevent form
	 * submission).
	 */
	FORM_VALIDATE_EVENT : "tapestry:formvalidate",

	/**
	 * Event fired just before the form submits, to allow observers to make
	 * final preparations for the submission, such as updating hidden form
	 * fields. The form element is passed as the event memo.
	 */
	FORM_PREPARE_FOR_SUBMIT_EVENT : "tapestry:formprepareforsubmit",

	/**
	 * Form event fired after prepare.
	 */
	FORM_PROCESS_SUBMIT_EVENT : "tapestry:formprocesssubmit",

	/**
	 * Event, fired on a field element, to cause observers to validate the
	 * input. Passes a memo object with two keys: "value" (the raw input value)
	 * and "translated" (the parsed value, usually meaning a number parsed from
	 * a string). Observers may invoke Element.showValidationMessage() to
	 * identify that the field is in error (and decorate the field and show a
	 * popup error message).
	 */
	FIELD_VALIDATE_EVENT : "tapestry:fieldvalidate",

	/**
	 * Event notification, on a form object, that is used to trigger validation
	 * on all fields within the form (observed by each field's
	 * Tapestry.FieldEventManager).
	 */
	FORM_VALIDATE_FIELDS_EVENT : "tapestry:validatefields",

	/**
	 * Event, fired on the document object, which identifies the current focus
	 * input element.
	 */
	FOCUS_CHANGE_EVENT : "tapestry:focuschange",

	/** Event, fired on a zone element when the zone is updated with new content. */
	ZONE_UPDATED_EVENT : "tapestry:zoneupdated",

	/**
	 * Event fired on a form fragment element to change the visibility of the
	 * fragment. The event memo object includes a key, visible, that should be
	 * true or false.
	 */
	CHANGE_VISIBILITY_EVENT : "tapestry:changevisibility",

	/**
	 * Event fired on a form fragment element to hide the element and remove it
	 * from the DOM.
	 */
	HIDE_AND_REMOVE_EVENT : "tapestry:hideandremove",

	/**
	 * Event fired on a link or submit to request that it request that the
	 * correct ZoneManager update from a provided URL.
	 */
	TRIGGER_ZONE_UPDATE_EVENT : "tapestry:triggerzoneupdate",

	/** Event used when intercepting and canceling the normal click event. */
	ACTION_EVENT : "tapestry:action",

	/** When false, the default, the Tapestry.debug() function will be a no-op. */
	DEBUG_ENABLED : false,

	/** Time, in seconds, that console messages are visible. */
	CONSOLE_DURATION : 10,

	/**
	 * CSS Class added to a 
element that directs Tapestry to prevent * normal (HTTP POST) form submission, in favor of Ajax (XmlHttpRequest) * submission. */ PREVENT_SUBMISSION : "t-prevent-submission", /** Initially, false, set to true once the page is fully loaded. */ pageLoaded : false, /** * Invoked from onclick event handlers built into links and forms. Raises a * dialog if the page is not yet fully loaded. */ waitForPage : function(event) { if (Tapestry.pageLoaded) return true; Event.extend(event || window.event).stop(); var body = $(document.body); /* * The overlay is stretched to cover the full screen (including * scrolling areas) and is used to fade out the background ... and * prevent keypresses (its z-order helps there). */ var overlay = new Element("div", { 'class' : 't-dialog-overlay' }); overlay.setOpacity(0.0); body.insert( { top : overlay }); new Effect.Appear(overlay, { duration : 0.2, from : 0.0 }); var messageDiv = new Element("div", { 'class' : 't-page-loading-banner' }).update(Tapestry.Messages.pageIsLoading); overlay.insert( { top : messageDiv }); var hideDialog = function() { new Effect.Fade(overlay, { duration : 0.2, afterFinish : function() { Tapestry.remove(overlay); } }); }; document.observe("dom:loaded", hideDialog); /* A rare race condition. */ if (Tapestry.pageLoaded) { hideDialog.call(null); return true; } else { return false; } }, /** * Adds a callback function that will be invoked when the DOM is loaded * (which occurs *before* window.onload, which has to wait for images and * such to load first. This simply observes the dom:loaded event on the * document object (support for which is provided by Prototype). */ onDOMLoaded : function(callback) { document.observe("dom:loaded", callback); }, /** * Find all elements marked with the "t-invisible" CSS class and hide()s * them, so that Prototype's visible() method operates correctly. In * addition, finds form control elements and adds additional listeners to * them to support form field input validation. * *

* This is invoked when the DOM is first loaded, and AGAIN whenever dynamic * content is loaded via the Zone mechanism. */ onDomLoadedCallback : function() { Tapestry.pageLoaded = true; Tapestry.ScriptManager.initialize(); $$(".t-invisible").each(function(element) { element.hide(); element.removeClassName("t-invisible"); }); /* * Adds a focus observer that fades all error popups except for the * field in question. */ $$("INPUT", "SELECT", "TEXTAREA").each(function(element) { /* * Due to Ajax, we may execute the callback multiple times, and we * don't want to add multiple listeners to the same element. */ var t = $T(element); if (!t.observingFocusChange) { element.observe("focus", function() { if (element != Tapestry.currentFocusField) { document.fire(Tapestry.FOCUS_CHANGE_EVENT, element); Tapestry.currentFocusField = element; } }); t.observingFocusChange = true; } }); /* * When a submit element is clicked, record the name of the element into * the associated form. This is necessary for some Ajax processing, see * TAPESTRY-2324. */ $$("INPUT[type=submit]").each(function(element) { var t = $T(element); if (!t.trackingClicks) { element.observe("click", function() { $(element.form).setSubmittingElement(element); }); t.trackingClicks = true; } }); }, /* * Generalized initialize function for Tapestry, used to help minimize the * amount of JavaScript for the page by removing redundancies such as * repeated Object and method names. The spec is a hash whose keys are the * names of methods of the Tapestry.Initializer object. The value is an * array of arrays. The outer arrays represent invocations of the method. * The inner array are the parameters for each invocation. As an * optimization, the inner value may not be an array but instead a single * value. */ init : function(spec) { $H(spec).each(function(pair) { var functionName = pair.key; var initf = Tapestry.Initializer[functionName]; if (initf == undefined) { Tapestry.error(Tapestry.Messages.missingInitializer, { name : functionName }); return; } pair.value.each(function(parameterList) { if (!Object.isArray(parameterList)) { parameterList = [ parameterList ]; } try { initf.apply(this, parameterList); } catch (e) { Tapestry.error(Tapestry.Messages.invocationException, { fname : "Tapestry.Initializer." + functionName, params : Object.toJSON(parameterList), exception : e }); } }); }); }, /** Formats and displays an error message on the console. */ error : function(message, substitutions) { Tapestry.invokeLogger(message, substitutions, Tapestry.Logging.error); }, /** Formats and displays a warning on the console. */ warn : function(message, substitutions) { Tapestry.invokeLogger(message, substitutions, Tapestry.Logging.warn); }, /** Formats and displays a debug message on the console. */ debug : function(message, substitutions) { Tapestry.invokeLogger(message, substitutions, Tapestry.Logging.debug); }, invokeLogger : function(message, substitutions, loggingFunction) { if (substitutions != undefined) message = message.interpolate(substitutions); loggingFunction.call(this, message); }, /** * Passed the JSON content of a Tapestry partial markup response, extracts * the script and stylesheet information. JavaScript libraries and * stylesheets are loaded, then the callback is invoked. All three keys are * optional: *

*
redirectURL
*
URL to redirect to (in which case, the callback is not invoked)
*
inits
*
Defines a set of calls to Tapestry.init() to perform initialization * after the DOM has been updated.
*
stylesheets
*
Array of hashes, each hash has key href and optional key media
* * @param reply * JSON response object from the server * @param callback * function invoked after the scripts have all loaded * (presumably, to update the DOM) */ loadScriptsInReply : function(reply, callback) { var redirectURL = reply.redirectURL; if (redirectURL) { window.location.href = redirectURL; /* Don't bother loading scripts or invoking the callback. */ return; } Tapestry.ScriptManager.addStylesheets(reply.stylesheets); Tapestry.ScriptManager.addScripts(reply.scripts, function() { /* Let the caller do its thing first (i.e., modify the DOM). */ callback.call(this); /* And handle the scripts after the DOM is updated. */ Tapestry.executeInits(reply.inits); }); }, /** * Called from Tapestry.loadScriptsInReply to load any initializations from * the Ajax partial page render response. Calls * Tapestry.onDomLoadedCallback() last. This logic must be deferred until * after the DOM is fully updated, as initialization often refer to DOM * elements. * * @param initializations * array of parameters to pass to Tapestry.init(), one invocation * per element (may be null) */ executeInits : function(initializations) { $A(initializations).each(function(spec) { Tapestry.init(spec); }); Tapestry.onDomLoadedCallback(); }, /** * Default function for handling a communication error during an Ajax * request. */ ajaxExceptionHander : function(response, exception) { Tapestry.error(Tapestry.Messages.communicationFailed + exception); Tapestry.debug(Tapestry.Messages.ajaxFailure + exception, response); }, /** * Default function for handling Ajax-related failures. */ ajaxFailureHandler : function(response) { var rawMessage = response.getHeader("X-Tapestry-ErrorMessage"); var message = unescape(rawMessage).escapeHTML(); Tapestry.error(Tapestry.Messages.communicationFailed + message); Tapestry.debug(Tapestry.Messages.ajaxFailure + message, response); }, /** * Processes a typical Ajax request for a URL. In the simple case, a success * handler is provided (as options). In a more complex case, an options * object is provided, with keys as per Ajax.Request. The onSuccess key will * be overwritten, and defaults for onException and onFailure will be * provided. The handler should take up-to two parameters: the * XMLHttpRequest object itself, and the JSON Response (from the X-JSON * response header, usually null). * * @param url * of Ajax request * @param options * either a success handler * @return the Ajax.Request object */ ajaxRequest : function(url, options) { if (Object.isFunction(options)) { return Tapestry.ajaxRequest(url, { onSuccess : options }); } var successHandler = options.onSuccess; var finalOptions = $H( { onException : Tapestry.ajaxExceptionHandler, onFailure : Tapestry.ajaxFailureHandler }).update(options).update( { onSuccess : function(response, jsonResponse) { /* * When the page is unloaded, pending Ajax requests appear to * terminate as successful (but with no reply value). Since * we're trying to navigate to a new page anyway, we just ignore * those false success callbacks. We have a listener for the * window's "beforeunload" event that sets this flag. */ if (Tapestry.windowUnloaded) return; /* * Prototype treats status == 0 as success, even though it seems * to mean the server didn't respond. */ if (!response.getStatus() || !response.request.success()) { Tapestry.error(Tapestry.Messages.ajaxRequestUnsuccessful); return; } try { /* Re-invoke the success handler, capturing any exceptions. */ successHandler.call(this, response, jsonResponse); } catch (e) { finalOptions.onException.call(this, ajaxRequest, e); } } }); var ajaxRequest = new Ajax.Request(url, finalOptions.toObject()); return ajaxRequest; }, /** * Obtains the Tapestry.ZoneManager object associated with a triggering * element (an or ) configured to update a zone. Writes errors to * the AjaxConsole if the zone and ZoneManager can not be resolved. * * @param element * triggering element (id or instance) * @return Tapestry.ZoneManager instance for updated zone, or null if not * found. */ findZoneManager : function(element) { var zoneId = $T(element).zoneId; return Tapestry.findZoneManagerForZone(zoneId); }, /** * Obtains the Tapestry.ZoneManager object associated with a zone element * (usually a
). Writes errors to the Ajax console if the element or * manager can not be resolved. * * @param zoneElement * zone element (id or instance) * @return Tapestry.ZoneManager instance for zone, or null if not found */ findZoneManagerForZone : function(zoneElement) { var element = $(zoneElement); if (!zoneElement) { Tapestry.error(Tapestry.Messages.missingZone, { id : zoneElement }); return null; } var manager = $T(zoneElement).zoneManager; if (!manager) { Tapestry.error(Tapestry.Messages.noZoneManager, element); return null; } return manager; }, /** * Used to reconstruct a complete URL from a path that is (or may be) * relative to window.location. This is used when determining if a * JavaScript library or CSS stylesheet has already been loaded. Recognizes * complete URLs (which are returned unchanged), otherwise the URLs are * expected to be absolute paths. * * @param path * @return complete URL as string */ rebuildURL : function(path) { if (path.match(/^https?:/)) { return path; } if (!path.startsWith("/")) { Tapestry.error(Tapestry.Messages.pathDoesNotStartWithSlash, { path : path }); return path; } var l = window.location; return l.protocol + "//" + l.host + path; }, stripToLastSlash : function(URL) { var slashx = URL.lastIndexOf("/"); return URL.substring(0, slashx + 1); }, /** * Convert a user-provided localized number to an ordinary number (not a * string). Removes seperators and leading/trailing whitespace. Disallows * the decimal point if isInteger is true. * * @param number * string provided by user * @param isInteger * if true, disallow decimal point */ formatLocalizedNumber : function(number, isInteger) { /* * We convert from localized string to a canonical string, stripping out * group seperators (normally commas). If isInteger is true, we don't * allow a decimal point. */ var minus = Tapestry.decimalFormatSymbols.minusSign; var grouping = Tapestry.decimalFormatSymbols.groupingSeparator; var decimal = Tapestry.decimalFormatSymbols.decimalSeparator; var canonical = ""; number.strip().toArray().each(function(ch) { if (ch == minus) { canonical += "-"; return; } if (ch == grouping) { return; } if (ch == decimal) { if (isInteger) throw Tapestry.Messages.notAnInteger; ch = "."; } else if (ch < "0" || ch > "9") throw Tapestry.Messages.invalidCharacter; canonical += ch; }); return Number(canonical); }, /** * Marks a number of script libraries as loaded; this is used with virtual * scripts (which combine multiple actual scripts). This is necessary so * that subsequent Ajax requests do not load scripts that have already been * loaded * * @param scripts * array of script paths */ markScriptLibrariesLoaded : function(scripts) { $(scripts).each(function(script) { var complete = Tapestry.rebuildURL(script); Tapestry.ScriptManager.virtualScripts.push(complete); }); }, /** * Creates a clone of the indicated element, but with the alternate tag * name. Attributes of the original node are copied to the new node. Tag * names should be all upper-case. The content of the original element is * copied to the new element and the original element is removed. Event * observers on the original element will be lost. * * @param element * element or element id * @since 5.2.0 */ replaceElementTagName : function(element, newTagName) { element = $(element); var tag = element.tagName; /* outerHTML is IE only; this simulates it on any browser. */ var dummy = document.createElement('html'); dummy.appendChild(element.cloneNode(true)); var outerHTML = dummy.innerHTML; var replaceHTML = outerHTML.replace(new RegExp("^<" + tag, "i"), "<" + newTagName).replace(new RegExp("$", "i"), ""); element.insert( { before : replaceHTML }); Tapestry.remove(element); }, /** * Removes an element and all of its direct and indirect children. The * element is first purged, to ensure that Internet Explorer doesn't leak * memory if event handlers associated with the element (or its children) * have references back to the element. * * @since 5.2.0 */ remove : function(element) { Tapestry.purge(element); Element.remove(element); }, /** * Purges the element of any event handlers (necessary in IE to ensure that * memory leaks do not occur, and harmless in other browsers). The element * is purged, then any children of the element are purged. */ purge : function(element) { /* Adapted from http://javascript.crockford.com/memory/leak.html */ var attrs = element.attributes; if (attrs) { var i, name; for (i = attrs.length - 1; i >= 0; i--) { if (attrs[i]) { name = attrs[i].name; /* Looking for onclick, etc. */ if (typeof element[name] == 'function') { element[name] = null; } } } } /* Get rid of any Prototype event handlers as well. */ Event.stopObserving(element); Tapestry.purgeChildren(element); }, /** * Invokes purge() on all the children of the element. */ purgeChildren : function(element) { var children = element.childNodes; if (children) { var l = children.length, i, child; for (i = 0; i < l; i++) { var child = children[i]; /* Just purge element nodes, not text, etc. */ if (child.nodeType == 1) Tapestry.purge(children[i]); } } } }; Element.addMethods( { /** * Works upward from the element, checking to see if the element is visible. * Returns false if it finds an invisible container. Returns true if it * makes it as far as a (visible) FORM element. * * Note that this only applies to the CSS definition of visible; it doesn't * check that the element is scrolled into view. * * @param element * to search up from * @return true if visible (and containers visible), false if it or * container are not visible */ isDeepVisible : function(element) { var current = $(element); while (true) { if (!current.visible()) return false; if (current.tagName == "FORM") break; current = $(current.parentNode); } return true; }, /** * Observes an event and turns it into a Tapestry.ACTION_EVENT. The original * event is stopped. The original event object is passed as the memo when * the action event is fired. This allows the logic for clicking an element * to be separated from the logic for processing that click event, which is * often useful when the click logic needs to be intercepted, or when the * action logic needs to be triggered outside the context of a DOM event. * * $T(element).hasAction will be true after invoking this method. * * @param element * to observe events from * @param eventName * name of event to observer, typically "click" * @param handler * function to be invoked; it will be registered as a observer of * the Tapestry.ACTION_EVENT. */ observeAction : function(element, eventName, handler) { element.observe(eventName, function(event) { event.stop(); element.fire(Tapestry.ACTION_EVENT, event); }); element.observe(Tapestry.ACTION_EVENT, handler); $T(element).hasAction = true; } }); Element .addMethods( 'FORM', { /** * Gets the Tapestry.FormEventManager for the form. * * @param form * form element */ getFormEventManager : function(form) { form = $(form); var manager = $T(form).formEventManager; if (manager == undefined) { throw "No Tapestry.FormEventManager object has been created for form '#{id}'." .interpolate(form); } return manager; }, /** * Identifies in the form what is the cause of the * submission. The element's id is stored into the t:submit * hidden field (created as needed). * * @param form * to update * @param element * id or element that is the cause of the submit * (a Submit or LinkSubmit) */ setSubmittingElement : function(form, element) { form.getFormEventManager() .setSubmittingElement(element); }, /** * Turns off client validation for the next submission of * the form. */ skipValidation : function(form) { $T(form).skipValidation = true; }, /** * Programmatically perform a submit, invoking the onsubmit * event handler (if present) before calling form.submit(). */ performSubmit : function(form, event) { if (form.onsubmit == undefined || form.onsubmit.call(window.document, event)) { form.submit(); } }, /** * Sends an Ajax request to the Form's action. This * encapsulates a few things, such as a default onFailure * handler, and working around bugs/features in Prototype * concerning how submit buttons are processed. * * @param form * used to define the data to be sent in the * request * @param options * standard Prototype Ajax Options * @return Ajax.Request the Ajax.Request created for the * request */ sendAjaxRequest : function(form, url, options) { form = $(form); /* * Generally, options should not be null or missing, * because otherwise there's no way to provide any * callbacks! */ options = Object.clone(options || {}); /* * Find the elements, skipping over any submit buttons. * This works around bugs in Prototype 1.6.0.2. */ var elements = form.getElements().reject(function(e) { return e.tagName == "INPUT" && e.type == "submit"; }); var hash = Form.serializeElements(elements, true); /* * Copy the parameters in, overwriting field values, * because Prototype 1.6.0.2 does not. */ Object.extend(hash, options.parameters); options.parameters = hash; /* * Ajax.Request will convert the hash into a query * string and post it. */ return Tapestry.ajaxRequest(url, options); } }); Element.addMethods( [ 'INPUT', 'SELECT', 'TEXTAREA' ], { /** * Invoked on a form element (INPUT, SELECT, etc.), gets or creates the * Tapestry.FieldEventManager for that field. * * @param field * field element */ getFieldEventManager : function(field) { field = $(field); var t = $T(field); var manager = t.fieldEventManager; if (manager == undefined) { manager = new Tapestry.FieldEventManager(field); t.fieldEventManager = manager; } return manager; }, /** * Obtains the Tapestry.FieldEventManager and asks it to show the validation * message. Sets the validationError property of the elements tapestry * object to true. * * @param element * @param message * to display */ showValidationMessage : function(element, message) { element = $(element); element.getFieldEventManager().showValidationMessage(message); return element; }, /** * Removes any validation decorations on the field, and hides the error * popup (if any) for the field. */ removeDecorations : function(element) { $(element).getFieldEventManager().removeDecorations(); return element; }, /** * Adds a standard validator for the element, an observer of * Tapestry.FIELD_VALIDATE_EVENT. The validator function will be passed the * current field value and should throw an error message if the field's * value is not valid. * * @param element * field element to validate * @param validator * function to be passed the field value */ addValidator : function(element, validator) { element.observe(Tapestry.FIELD_VALIDATE_EVENT, function(event) { try { validator.call(this, event.memo.translated); } catch (message) { element.showValidationMessage(message); } }); return element; } }); /** Container of functions that may be invoked by the Tapestry.init() function. */ Tapestry.Initializer = { /** Make the given field the active field (focus on the field). */ activate : function(id) { $(id).activate(); }, /** * evalScript is a synonym for the JavaScript eval function. It is used in * Ajax requests to handle any setup code that does not fit into a standard * Tapestry.Initializer call. */ evalScript : eval, ajaxFormLoop : function(spec) { var rowInjector = $(spec.rowInjector); $(spec.addRowTriggers).each(function(triggerId) { $(triggerId).observeAction("click", function(event) { $(rowInjector).trigger(); }); }); }, formLoopRemoveLink : function(spec) { var link = $(spec.link); var fragmentId = spec.fragment; link.observeAction("click", function(event) { var successHandler = function(transport) { var container = $(fragmentId); var effect = Tapestry.ElementEffect.fade(container); effect.options.afterFinish = function() { Tapestry.remove(container); } }; Tapestry.ajaxRequest(spec.url, successHandler); }); }, /** * Convert a form or link into a trigger of an Ajax update that updates the * indicated Zone. * * @param spec.linkId * id or instance of or element * @param spec.zoneId * id of the element to update when link clicked or form * submitted * @param spec.url * absolute component event request URL */ linkZone : function(spec) { Tapestry.Initializer.updateZoneOnEvent("click", spec.linkId, spec.zoneId, spec.url); }, /** * Converts a link into an Ajax update of a Zone. The url includes the * information to reconnect with the server-side Form. * * @param spec.selectId * id or instance of