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 ]; } 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("$", "i"), ""); 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; });




© 2015 - 2025 Weber Informatics LLC | Privacy Policy