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

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

/* Copyright 2007, 2008, 2009, 2010, 2011 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 <form> 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. * * TAP5-1418: Added "type=image" so that they set the submitting element * correctly. */ $$("INPUT[type=submit]", "INPUT[type=image]").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 an info message on the console. */ info : function(message, substitutions) { Tapestry.invokeLogger(message, substitutions, Tapestry.Logging.info); }, /** 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 || Prototype.emptyFunction; 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 <a> or <form>) 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 <div>). 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 (!element) { Tapestry.error(Tapestry.Messages.missingZone, { id : zoneElement }); return null; } var manager = $T(element).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; } if (!Tapestry.buildUrl) { var l = window.location; Tapestry.buildUrl = l.protocol + "//" + l.host; } return Tapestry.buildUrl + 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); }, /** * 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; } }); /** Compatibility: set Tapestry.Initializer equal to T5.Initializer. */ Tapestry.Initializer = T5.Initializer; /** Container of functions that may be invoked by the Tapestry.init() function. */ T5 .extendInitializer({ /** 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 <form> or <a> 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 <select> * @param spec.zoneId * id of element to update when select is changed * @param spec.url * component event request URL */ linkSelectToZone : function(spec) { Tapestry.Initializer.updateZoneOnEvent("change", spec.selectId, spec.zoneId, spec.url); }, linkSubmit : function(spec) { Tapestry.replaceElementTagName(spec.clientId, "A"); $(spec.clientId).writeAttribute("href", "#"); $(spec.clientId).observeAction("click", function(event) { var form = $(spec.form); if (!spec.validate) form.skipValidation(); form.setSubmittingElement(this); form.performSubmit(event); }); }, /** * Used by other initializers to connect an element (either a link * or a form) to a zone. * * @param eventName * the event on the element to observe * @param element * the element to observe for events * @param zoneId * identified a Zone by its clientId. Alternately, the * special value '^' indicates that the Zone is a * container of the element (the first container with the * 't-zone' CSS class). * @param url * The request URL to be triggered when the event is * observed. Ultimately, a partial page update JSON * response will be passed to the Zone's ZoneManager. */ updateZoneOnEvent : function(eventName, element, zoneId, url) { element = $(element); $T(element).zoneUpdater = true; var zoneElement = zoneId == '^' ? $(element).up('.t-zone') : $(zoneId); if (!zoneElement) { Tapestry .error( "Could not find zone element '#{zoneId}' to update on #{eventName} of element '#{elementId}", { zoneId : zoneId, eventName : eventName, elementId : element.id }); return; } /* * Update the element with the id of zone div. This may be * changed dynamically on the client side. */ $T(element).zoneId = zoneElement.id; if (element.tagName == "FORM") { // Create the FEM if necessary. element.addClassName(Tapestry.PREVENT_SUBMISSION); /* * After the form is validated and prepared, this code will * process the form submission via an Ajax call. The * original submit event will have been cancelled. */ element .observe( Tapestry.FORM_PROCESS_SUBMIT_EVENT, function() { var zoneManager = Tapestry .findZoneManager(element); if (!zoneManager) return; var successHandler = function(transport) { zoneManager .processReply(transport.responseJSON); }; element.sendAjaxRequest(url, { parameters : { "t:zoneid" : zoneId }, onSuccess : successHandler }); }); return; } /* Otherwise, assume it's just an ordinary link or input field. */ element.observeAction(eventName, function(event) { element.fire(Tapestry.TRIGGER_ZONE_UPDATE_EVENT); }); element.observe(Tapestry.TRIGGER_ZONE_UPDATE_EVENT, function() { var zoneObject = Tapestry.findZoneManager(element); if (!zoneObject) return; /* * A hack related to allowing a Select to perform an Ajax * update of the page. */ var parameters = {}; if (element.tagName == "SELECT" && element.value) { parameters["t:selectvalue"] = element.value; } zoneObject.updateFromURL(url, parameters); }); }, /** * Sets up a Tapestry.FormEventManager for the form, and enables * events for validations. This is executed with * InitializationPriority.EARLY, to ensure that the FormEventManager * exists vefore any validations are added for fields within the * Form. * * @since 5.2.2 */ formEventManager : function(spec) { $T(spec.formId).formEventManager = new Tapestry.FormEventManager( spec); }, /** * Keys in the masterSpec are ids of field control elements. Value * is a list of validation specs. Each validation spec is a 2 or 3 * element array. */ validate : function(masterSpec) { $H(masterSpec) .each( function(pair) { var field = $(pair.key); /* * Force the creation of the field event * manager. */ $(field).getFieldEventManager(); $A(pair.value) .each( function(spec) { /* * Each pair value is an * array of specs, each * spec is a 2 or 3 * element array. * validator function * name, message, * optional constraint */ var name = spec[0]; var message = spec[1]; var constraint = spec[2]; var vfunc = Tapestry.Validator[name]; if (vfunc == undefined) { Tapestry .error( Tapestry.Messages.missingValidator, { name : name, fieldName : field.id }); return; } /* * Pass the extended * field, the provided * message, and the * constraint object to * the * Tapestry.Validator * function, so that it * can, typically, * invoke * field.addValidator(). */ try { vfunc.call(this, field, message, constraint); } catch (e) { Tapestry .error( Tapestry.Messages.invocationException, { fname : "Tapestry.Validator." + functionName, params : Object .toJSON([ field.id, message, constraint ]), exception : e }); } }); }); }, zone : function(spec) { new Tapestry.ZoneManager(spec); }, formFragment : function(spec) { var element = $(spec.element); var hidden = $(spec.element + "-hidden"); var form = $(hidden.form); function runAnimation(makeVisible) { var effect = makeVisible ? Tapestry.ElementEffect[spec.show] || Tapestry.ElementEffect.slidedown : Tapestry.ElementEffect[spec.hide] || Tapestry.ElementEffect.slideup; return effect(element); } element.observe(Tapestry.CHANGE_VISIBILITY_EVENT, function( event) { var makeVisible = event.memo.visible; if (makeVisible == element.visible()) return; runAnimation(makeVisible); }); element.observe(Tapestry.HIDE_AND_REMOVE_EVENT, function() { var effect = runAnimation(false); effect.options.afterFinish = function() { Tapestry.remove(element); }; }); if (!spec.alwaysSubmit) { form.observe(Tapestry.FORM_PREPARE_FOR_SUBMIT_EVENT, function() { /* * On a submission, if the fragment is not * visible, then disabled its form submission * data, so that no processing or validation * occurs on the server. */ hidden.disabled = !element.isDeepVisible(); }); } }, formInjector : function(spec) { new Tapestry.FormInjector(spec); }, /* * Links a FormFragment to a trigger (a radio or a checkbox), such * that changing the trigger will hide or show the FormFragment. * Care should be taken to render the page with the checkbox and the * FormFragment's visibility in agreement. */ linkTriggerToFormFragment : function(spec) { var trigger = $(spec.triggerId); var update = function() { var checked = trigger.checked; var makeVisible = checked == !spec.invert; $(spec.fragmentId).fire(Tapestry.CHANGE_VISIBILITY_EVENT, { visible : makeVisible }, true); } /* Let the event bubble up to the form level. */ if (trigger.type == "radio") { $(trigger.form).observe("click", update); return; } /* Normal trigger is a checkbox; listen just to it. */ trigger.observe("click", update); }, cancelButton : function(clientId) { /* * Set the form's skipValidation property and allow the event to * continue, which will ultimately submit the form. */ $(clientId).observeAction("click", function(event) { $(this.form).skipValidation(); $(this.form).setSubmittingElement(clientId); $(this.form).performSubmit(event); }); } }); /* * Collection of field based functions related to validation. Each function * takes a field, a message and an optional constraint value. Some functions are * related to Translators and work on the format event, other's are from * Validators and work on the validate event. */ Tapestry.Validator = { required : function(field, message) { $(field).getFieldEventManager().requiredCheck = function(value) { if ((Object.isString(value) && value.strip() == '') || value == null) $(field).showValidationMessage(message); }; }, /** Supplies a client-side numeric translator for the field. */ numericformat : function(field, message, isInteger) { $(field).getFieldEventManager().translator = function(input) { try { return Tapestry.formatLocalizedNumber(input, isInteger); } catch (e) { $(field).showValidationMessage(message); } }; }, minlength : function(field, message, length) { field.addValidator(function(value) { if (value.length < length) throw message; }); }, maxlength : function(field, message, maxlength) { field.addValidator(function(value) { if (value.length > maxlength) throw message; }); }, min : function(field, message, minValue) { field.addValidator(function(value) { if (value < minValue) throw message; }); }, max : function(field, message, maxValue) { field.addValidator(function(value) { if (value > maxValue) throw message; }); }, regexp : function(field, message, pattern) { var regexp = new RegExp(pattern); field.addValidator(function(value) { if (!regexp.test(value)) throw message; }); } }; Tapestry.ErrorPopup = Class.create({ /* * If the images associated with the error popup are overridden (by * overriding Tapestry's default.css stylesheet), then some of these values * may also need to be adjusted. */ BUBBLE_VERT_OFFSET : -34, BUBBLE_HORIZONTAL_OFFSET : -20, BUBBLE_WIDTH : "auto", BUBBLE_HEIGHT : "39px", initialize : function(field) { this.field = $(field); this.innerSpan = new Element("span"); this.outerDiv = $(new Element("div", { 'id' : this.field.id + ":errorpopup", 'class' : 't-error-popup' })).update(this.innerSpan).hide(); var body = $(document.body); body.insert({ bottom : this.outerDiv }); this.outerDiv.absolutize(); this.outerDiv.observe("click", function(event) { this.ignoreNextFocus = true; this.stopAnimation(); this.outerDiv.hide(); this.field.activate(); event.stop(); }.bindAsEventListener(this)); this.queue = { position : 'end', scope : this.field.id }; Event.observe(window, "resize", this.repositionBubble.bind(this)); document.observe(Tapestry.FOCUS_CHANGE_EVENT, function(event) { if (this.ignoreNextFocus) { this.ignoreNextFocus = false; return; } if (event.memo == this.field) { this.fadeIn(); return; } /* * If this field is not the focus field after a focus change, then * it's bubble, if visible, should fade out. This covers tabbing * from one form to another. */ this.fadeOut(); }.bind(this)); }, showMessage : function(message) { this.stopAnimation(); this.innerSpan.update(message); this.hasMessage = true; this.fadeIn(); }, repositionBubble : function() { var fieldPos = this.field.cumulativeOffset(); this.outerDiv.setStyle({ top : (fieldPos[1] + this.BUBBLE_VERT_OFFSET) + "px", left : (fieldPos[0] + this.BUBBLE_HORIZONTAL_OFFSET) + "px", width : this.BUBBLE_WIDTH, height : this.BUBBLE_HEIGHT }); }, fadeIn : function() { if (!this.hasMessage) return; this.repositionBubble(); if (this.animation) return; this.animation = new Effect.Appear(this.outerDiv, { queue : this.queue, afterFinish : function() { this.animation = null; if (this.field != Tapestry.currentFocusField) this.fadeOut(); }.bind(this) }); }, stopAnimation : function() { if (this.animation) this.animation.cancel(); this.animation = null; }, fadeOut : function() { if (this.animation) return; this.animation = new Effect.Fade(this.outerDiv, { queue : this.queue, afterFinish : function() { this.animation = null; }.bind(this) }); }, hide : function() { this.hasMessage = false; this.stopAnimation(); this.outerDiv.hide(); } }); Tapestry.FormEventManager = Class.create({ initialize : function(spec) { this.form = $(spec.formId); this.validateOnBlur = spec.validate.blur; this.validateOnSubmit = spec.validate.submit; this.form.onsubmit = this.handleSubmit.bindAsEventListener(this); }, /** * 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 element * id or element that is the cause of the submit (a Submit or * LinkSubmit) */ setSubmittingElement : function(element) { if (!this.submitHidden) { // skip if this is not a tapestry controlled form if (this.form.getInputs("hidden", "t:formdata").size() == 0) return; var hiddens = this.form.getInputs("hidden", "t:submit"); if (hiddens.size() == 0) { /** * Create a new hidden field directly after the first hidden * field in the form. */ var firstHidden = this.form.getInputs("hidden").first(); this.submitHidden = new Element("input", { type : "hidden", name : "t:submit" }); firstHidden.insert({ after : this.submitHidden }); } else this.submitHidden = hiddens.first(); } this.submitHidden.value = element == null ? null : $(element).id; }, handleSubmit : function(domevent) { /* * Necessary because we set the onsubmit property of the form, rather * than observing the event. But that's because we want to specfically * overwrite any other handlers. */ Event.extend(domevent); var t = $T(this.form); t.validationError = false; if (!t.skipValidation) { t.skipValidation = false; /* Let all the fields do their validations first. */ this.form.fire(Tapestry.FORM_VALIDATE_FIELDS_EVENT, this.form); /* * Allow observers to validate the form as a whole. The FormEvent * will be visible as event.memo. The Form will not be submitted if * event.result is set to false (it defaults to true). Still trying * to figure out what should get focus from this kind of event. */ if (!t.validationError) this.form.fire(Tapestry.FORM_VALIDATE_EVENT, this.form); if (t.validationError) { domevent.stop(); /* * Because the submission failed, the last submit element is * cleared, since the form may be submitted for some other * reason later. */ this.setSubmittingElement(null); return false; } } this.form.fire(Tapestry.FORM_PREPARE_FOR_SUBMIT_EVENT, this.form); /* * This flag can be set to prevent the form from submitting normally. * This is used for some Ajax cases where the form submission must run * via Ajax.Request. */ if (this.form.hasClassName(Tapestry.PREVENT_SUBMISSION)) { domevent.stop(); /* * Instead fire the event (a listener will then trigger the Ajax * submission). This is really a hook for the ZoneManager. */ this.form.fire(Tapestry.FORM_PROCESS_SUBMIT_EVENT); return false; } /* Validation is OK, not doing Ajax, continue as planned. */ return true; } }); Tapestry.FieldEventManager = Class.create({ initialize : function(field) { this.field = $(field); this.translator = Prototype.K; var fem = $(this.field.form).getFormEventManager(); if (fem.validateOnBlur) { document.observe(Tapestry.FOCUS_CHANGE_EVENT, function(event) { /* * If changing focus *within the same form* then perform * validation. Note that Tapestry.currentFocusField does not * change until after the FOCUS_CHANGE_EVENT notification. */ if (Tapestry.currentFocusField == this.field && this.field.form == event.memo.form) this.validateInput(); }.bindAsEventListener(this)); } if (fem.validateOnSubmit) { $(this.field.form).observe(Tapestry.FORM_VALIDATE_FIELDS_EVENT, this.validateInput.bindAsEventListener(this)); } }, getLabel : function() { if (!this.label) { var selector = "label[for='" + this.field.id + "']"; this.label = this.field.form.down(selector); } return this.label; }, getIcon : function() { if (!this.icon) { this.com = $(this.field.id + "_icon"); } return this.icon; }, /** * Removes validation decorations if present. Hides the ErrorPopup, if it * exists. */ removeDecorations : function() { this.field.removeClassName("t-error"); this.getLabel() && this.getLabel().removeClassName("t-error"); this.getIcon() && this.getIcon().hide(); if (this.errorPopup) this.errorPopup.hide(); }, /** * Show a validation error message, which will add decorations to the field * and it label, make the icon visible, and raise the field's * Tapestry.ErrorPopup to show the message. * * @param message * validation message to display */ showValidationMessage : function(message) { $T(this.field).validationError = true; $T(this.field.form).validationError = true; this.field.addClassName("t-error"); this.getLabel() && this.getLabel().addClassName("t-error"); var icon = this.getIcon(); if (icon && !icon.visible()) { new Effect.Appear(this.icon); } if (this.errorPopup == undefined) this.errorPopup = new Tapestry.ErrorPopup(this.field); this.errorPopup.showMessage(message); }, /** * Invoked when a form is submitted, or when leaving a field, to perform * field validations. Field validations are skipped for disabled fields. If * all validations are succesful, any decorations are removed. If any * validation fails, an error popup is raised for the field, to display the * validation error message. * * @return true if the field has a validation error */ validateInput : function() { if (this.field.disabled) return false; if (!this.field.isDeepVisible()) return false; var t = $T(this.field); var value = $F(this.field); t.validationError = false; if (this.requiredCheck) this.requiredCheck.call(this, value); /* * Don't try to validate blank values; if the field is required, that * error is already noted and presented to the user. */ if (!t.validationError && !(Object.isString(value) && value.blank())) { var translated = this.translator(value); /* * If Format went ok, perhaps do the other validations. */ if (!t.validationError) { this.field.fire(Tapestry.FIELD_VALIDATE_EVENT, { value : value, translated : translated }); } } /* Lastly, if no validation errors were found, remove the decorations. */ if (!t.validationError) this.field.removeDecorations(); return t.validationError; } }); /* * Wrappers around Prototype and Scriptaculous effects. All the functions of * this object should have all-lowercase names. The methods all return the * Effect object they create. */ Tapestry.ElementEffect = { /** Fades in the element. */ show : function(element) { return new Effect.Appear(element); }, /** The classic yellow background fade. */ highlight : function(element, color) { if (color) return new Effect.Highlight(element, { endcolor : color, restorecolor : color }); return new Effect.Highlight(element); }, /** Scrolls the content down. */ slidedown : function(element) { return new Effect.SlideDown(element); }, /** Slids the content back up (opposite of slidedown). */ slideup : function(element) { return new Effect.SlideUp(element); }, /** Fades the content out (opposite of show). */ fade : function(element) { return new Effect.Fade(element); } }; /** * Manages a <div> (or other element) for dynamic updates. * */ Tapestry.ZoneManager = Class.create({ /* * spec are the parameters for the Zone: trigger: required -- name or * instance of link. element: required -- name or instance of div element to * be shown, hidden and updated show: name of Tapestry.ElementEffect * function used to reveal the zone if hidden update: name of * Tapestry.ElementEffect function used to highlight the zone after it is * updated */ initialize : function(spec) { this.element = $(spec.element); this.showFunc = Tapestry.ElementEffect[spec.show] || Tapestry.ElementEffect.show; this.updateFunc = Tapestry.ElementEffect[spec.update] || Tapestry.ElementEffect.highlight; this.specParameters = spec.parameters; /* * TAP5-707: store the old background color of the element or take white * as a default */ this.endcolor = this.element.getStyle('background-color').parseColor( '#ffffff'); /* Link the div back to this zone. */ $T(this.element).zoneManager = this; /* * Look inside the managed element for another element with the CSS * class "t-zone-update". If present, then this is the element whose * content will be changed, rather then the entire zone's element. This * allows a Zone element to contain "wrapper" markup (borders and such). * Typically, such a Zone element will initially be invisible. The show * and update functions apply to the Zone element, not the update * element. */ var updates = this.element.select(".t-zone-update"); this.updateElement = updates.first() || this.element; }, /* * Updates the content of the div controlled by this Zone, then invokes the * show function (if not visible) or the update function (if visible) */ /** * Updates the zone's content, and invokes either the update function (to * highlight the change) or the show function (to reveal a hidden element). * Lastly, fires the Tapestry.ZONE_UPDATED_EVENT to let listeners know that * the zone was updated. * * @param content */ show : function(content) { Tapestry.purgeChildren(this.updateElement); this.updateElement.update(content); var func = this.element.visible() ? this.updateFunc : this.showFunc; func.call(this, this.element, this.endcolor); this.element.fire(Tapestry.ZONE_UPDATED_EVENT); }, /** * Invoked with a reply (i.e., transport.responseJSON), this updates the * managed element and processes any JavaScript in the reply. The response * should have a content key, and may have script, scripts and stylesheets * keys. * * @param reply * response in JSON format appropriate to a Tapestry.Zone */ processReply : function(reply) { Tapestry.loadScriptsInReply(reply, function() { /* * In a multi-zone update, the reply.content may be missing, in * which case, leave the curent content in place. TAP5-1177 */ reply.content != undefined && this.show(reply.content); /* * zones is an object of zone ids and zone content that will be * present in a multi-zone update response. */ reply.zones && Object.keys(reply.zones).each(function(zoneId) { var manager = Tapestry.findZoneManagerForZone(zoneId); if (manager) { var zoneContent = reply.zones[zoneId]; manager.show(zoneContent); } }); }.bind(this)); }, /** * Initiates an Ajax request to update this zone by sending a request to the * URL. Expects the correct JSON reply (wth keys content, etc.). * * @param URL * component event request URL * @param parameters * object containing additional key/value pairs (optional) */ updateFromURL : function(URL, parameters) { var finalParameters = $H({ "t:zoneid" : this.element.id }).update(this.specParameters); /* If parameters were supplied, merge them in with the zone id */ if (!Object.isUndefined(parameters)) finalParameters.update(parameters); Tapestry.ajaxRequest(URL, { parameters : finalParameters.toObject(), onSuccess : function(transport) { this.processReply(transport.responseJSON); }.bind(this) }); } }); Tapestry.FormInjector = Class.create({ initialize : function(spec) { this.element = $(spec.element); this.url = spec.url; this.below = spec.below; this.showFunc = Tapestry.ElementEffect[spec.show] || Tapestry.ElementEffect.highlight; this.element.trigger = function() { var successHandler = function(transport) { var reply = transport.responseJSON; /* * Clone the FormInjector element (usually a div) to create the * new element, that gets inserted before or after the * FormInjector's element. */ var newElement = new Element(this.element.tagName, { 'class' : this.element.className }); /* Insert the new element before or after the existing element. */ var param = {}; param[this.below ? "after" : "before"] = newElement; Tapestry.loadScriptsInReply(reply, function() { /* Add the new element with the downloaded content. */ this.element.insert(param); /* * Update the empty element with the content from the server */ newElement.update(reply.content); newElement.id = reply.elementId; /* * Add some animation to reveal it all. */ this.showFunc(newElement); }.bind(this)); }.bind(this); Tapestry.ajaxRequest(this.url, successHandler); return false; }.bind(this); } }); Tapestry.ScriptManager = { initialize : function() { /* * Check to see if document.scripts is supported; if not (for example, * FireFox), we can fake it. */ this.emulated = false; if (!document.scripts) { this.emulated = true; document.scripts = new Array(); $$('script').each(function(s) { document.scripts.push(s); }); } }, loadScript : function(scriptURL, callback) { /* IE needs the type="text/javascript" as well. */ var element = new Element('script', { src : scriptURL, type : 'text/javascript' }); $$("head").first().insert({ bottom : element }); if (this.emulated) document.scripts.push(element); if (Prototype.Browser.IE) { var loaded = false; element.onreadystatechange = function() { /* IE may fire either 'loaded' or 'complete', or possibly both. */ if (!loaded && this.readyState == 'loaded' || this.readyState == 'complete') { loaded = true; callback.call(this); } }; return; } /* Safari, Firefox, etc. are easier. */ element.onload = callback.bindAsEventListener(this); }, /** * Checks to see if the given collection (of <script> or <style> * elements) contains the given asset URL. * * @param collection * @param prop * property to check ('src' for script, 'href' to style). * @param assetURL * complete URL (i.e., with protocol, host and port) to the asset */ contains : function(collection, prop, assetURL) { return $A(collection).any( function(element) { var existing = element[prop]; if (!existing || existing.blank()) return false; var complete = Prototype.Browser.IE ? Tapestry .rebuildURL(existing) : existing; return complete == assetURL; }); return false; }, /** * Add scripts, as needed, to the document, then waits for them all to load, * and finally, calls the callback function. * * @param scripts * Array of scripts to load * @param callback * invoked after scripts are loaded */ addScripts : function(scripts, callback) { var scriptsToLoad = []; /* scripts may be null or undefined */ (scripts || []).each(function(s) { var assetURL = Tapestry.rebuildURL(s); if (Tapestry.ScriptManager.contains(document.scripts, "src", assetURL)) return; scriptsToLoad.push(assetURL); }); /* * Set it up last script to first script. The last script's callback is * the main callback (the code to execute after all scripts are loaded). * The 2nd to last script's callback loads the last script. Prototype's * Array.inject() is effectively the same as Clojure's reduce(). */ scriptsToLoad.reverse(); var topCallback = scriptsToLoad.inject(callback, function(nextCallback, scriptURL) { return function() { Tapestry.ScriptManager.loadScript(scriptURL, nextCallback); }; }); /* Kick it off with the callback that loads the first script. */ topCallback.call(); }, addStylesheets : function(stylesheets) { if (!stylesheets) return; var head = $$('head').first(); $(stylesheets).each( function(s) { var assetURL = Tapestry.rebuildURL(s.href); if (Tapestry.ScriptManager.contains(document.styleSheets, 'href', assetURL)) return; var element = new Element('link', { type : 'text/css', rel : 'stylesheet', href : assetURL }); /* * Careful about media types, some browser will break if it * ends up as 'null'. */ if (s.media != undefined) element.writeAttribute('media', s.media); head.insert({ bottom : element }); }); } }; /** * In the spirit of $(), $T() exists to access a hash of extra data about an * element. In release 5.1 and prior, a hash attached to the element by Tapestry * was returned. In 5.2, Prototype's storage object is returned, which is less * likely to cause memory leaks in IE. * * @deprecated With no specific replacement. To be removed after Tapestry 5.2. * @param element * an element instance or element id * @return object Prototype storage object for the element */ function $T(element) { return $(element).getStorage(); } Tapestry.onDOMLoaded(Tapestry.onDomLoadedCallback); /* Ajax code needs to know to do nothing after the window is unloaded. */ Event.observe(window, "beforeunload", function() { Tapestry.windowUnloaded = true; });




© 2015 - 2025 Weber Informatics LLC | Privacy Policy