
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 ];
}
initf.apply(this, parameterList);
});
});
},
/** 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. This function is a no-op unless Tapestry.DEBUG_ENABLED is true
* (which will be the case when the application is running in development mode).
*/
debug: function (message, substitutions) {
if (Tapestry.DEBUG_ENABLED) {
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.
*/
ajaxExceptionHandler: function (response, exception) {
Tapestry.error(Tapestry.Messages.communicationFailed + exception);
Tapestry.debug(Tapestry.Messages.ajaxFailure + exception, response);
// This covers just FireFox and Opera:
var trace = exception.stack || exception.stacktrace;
if (exception.stack) { Tapestry.debug(exception.stack); }
throw exception;
},
/**
* 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);
var contentType = response.getResponseHeader("content-type")
var isHTML = contentType && (contentType.split(';')[0] === "text/html");
if (isHTML) {
T5.ajax.showExceptionDialog(response.responseText)
}
},
/**
* 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 && 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 && response.responseText.blank())
return;
/*
* Prototype treats status == 0 as success, even though it seems
* to mean the server didn't respond.
*/
if (!response.getStatus() || !response.request.success()) {
finalOptions.get('onFailure').call(this, response);
return;
}
try {
/* Re-invoke the success handler, capturing any exceptions. */
successHandler.call(this, response, jsonResponse);
} catch (e) {
finalOptions.get('onException').call(this, response, 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("" + tag + ">$", "i"),
"" + newTagName + ">");
element.insert({
before: replaceHTML
});
T5.dom.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
* @deprecated Since 5.3, use T5.dom.remove() instead
*/
remove: T5.dom.remove,
/** @deprecated Since 5.3, use T5.dom.purgeChildren instead */
purgeChildren: T5.dom.purgeChildren
};
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
* @param options
* Optional map of options. Only used key currently is "bound" which should be a javascript function name
* that determines whether the current element bounds the search. The default is to stop the search when
* the
* @return true if visible (and containers visible), false if it or
* container are not visible
*/
isDeepVisible: function (element, options) {
var current = $(element);
var boundFunc = (options && options.bound) || function (el) {
return el.tagName == "FORM"
};
while (true) {
if (!current.visible())
return false;
if (boundFunc(current))
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) {
// A crude check to see if it is a Tapestry form.
if ($(form).getInputs("hidden", "t:formdata").size() > 0) {
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.initializers. */
Tapestry.Initializer = T5.initializers;
/** Container of functions that may be invoked by the Tapestry.init() function. */
T5.extendInitializers({
/** 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", "#");
if (spec.mode == "cancel") {
$(spec.clientId).writeAttribute("name", "cancel");
}
$(spec.clientId).observeAction("click", function (event) {
var form = $(spec.form);
if (spec.mode != "normal") {
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().
*/
vfunc.call(this, field, message, constraint);
});
});
},
zone: function (spec) {
new Tapestry.ZoneManager(spec);
},
formInjector: function (spec) {
new Tapestry.FormInjector(spec);
},
/**
* Invoked on a submit element to indicate that it forces form to submit as a cancel (bypassing client-side validation
* and most server-side processing).
* @param clientId of submit element
*/
enableBypassValidation: 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 ((T5._.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",
IE_FADE_TIME: 500,
initialize: function (field) {
this.field = $(field);
// The UI elements (outerDiv and friends) are created by the first call to setMessage().
this.outerDiv = null;
},
/**
* Invoked once, from setMessage(), to create the outerDiv and innerSpan elements, as well as necessary listeners
* (to hide the popup if clicked), and reposition the popup as necessary when the window resizes.
*/
createUI: function () {
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
* its bubble, if visible, should fade out. This covers tabbing
* from one form to another.
*/
this.fadeOut();
}.bind(this));
},
showMessage: function (message) {
if (this.outerDiv == null) {
this.createUI();
}
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;
if (Prototype.Browser.IE) {
var _ = T5._;
this.outerDiv.show();
var bound = _.bind(this.hideIfNotFocused, this);
_.delay(bound, this.IE_FADE_TIME);
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)
});
},
/** Used in IE to hide the field if not the focus field. */
hideIfNotFocused: function () {
if (this.outerDiv != null && this.field != Tapestry.currentFocusField) {
this.outerDiv.hide();
}
},
stopAnimation: function () {
if (this.animation)
this.animation.cancel();
this.animation = null;
},
fadeOut: function () {
if (this.animation || this.outerDiv == null)
return;
if (Prototype.Browser.IE) {
var div = this.outerDiv;
T5._.delay(function () {
div.hide();
}, this.IE_FADE_TIME);
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 && 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 : Object.toJSON([$(element).id, $(element).name]);
},
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.icon = $(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 && !(T5._.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);
},
none: function (element) {
return 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);
},
rebuildURLIfIE: Prototype.Browser.IE ? Tapestry.rebuildURL : T5._.identity,
/**
* 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 _ = T5._;
var loaded = _(document.scripts).chain().pluck("src").without("").map(this.rebuildURLIfIE).value();
var self = this;
var topCallback = _(scripts).chain().map(Tapestry.rebuildURL).difference(loaded).reverse().reduce(
function (nextCallback, scriptURL) {
return function () {
self.loadScript(scriptURL, nextCallback);
}
}, callback).value();
// Kick if off with the callback that loads the first script:
topCallback.call(this);
},
/**
* Adds stylesheets to the document. Each element in stylesheets is an object with two keys: href (the URL to the CSS file) and
* (optionally) media.
* @param stylesheets
*/
addStylesheets: function (stylesheets) {
if (!stylesheets)
return;
var _ = T5._;
var loaded = _(document.styleSheets).chain().pluck("href").without("").without(null).map(this.rebuildURLIfIE).value();
var toLoad = _(stylesheets).chain().map(
function (ss) {
ss.href = Tapestry.rebuildURL(ss.href);
return ss;
}).reject(
function (ss) {
return _(loaded).contains(ss.href);
}).value();
var head = $$('head').first();
var insertionPoint = head.down("link[rel='stylesheet t-ajax-insertion-point']");
_(toLoad).each(function (ss) {
var element = new Element('link', { type: 'text/css', rel: 'stylesheet', href: ss.href });
if (ss.media) {
element.writeAttribute('media', ss.media);
}
if (insertionPoint) {
insertionPoint.insert({ before: element });
}
else {
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;
});