| # Copyright 2012, 2013 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. |
| |
| # ## t5/core/forms |
| # |
| # Defines handlers for HTML forms and HTML field elements, specifically to control input validation. |
| |
| define ["./events", "./dom", "underscore"], |
| (events, dom, _) -> |
| |
| # Meta-data name that indicates the next submission should skip validation (typically, because |
| # the form was submitted by a "cancel" button). |
| SKIP_VALIDATION = "t5:skip-validation" |
| |
| clearSubmittingHidden = (form) -> |
| hidden = form.findFirst "[name='t:submit']" |
| |
| # Clear if found |
| hidden and hidden.value null |
| |
| form.meta SKIP_VALIDATION, null |
| |
| return |
| |
| setSubmittingHidden = (form, submitter) -> |
| |
| mode = submitter.attr "data-submit-mode" |
| isCancel = mode is "cancel" |
| if mode and mode isnt "normal" |
| form.meta SKIP_VALIDATION, true |
| |
| hidden = form.findFirst "[name='t:submit']" |
| |
| unless hidden |
| firstHidden = form.findFirst "input[type=hidden]" |
| hidden = dom.create "input", type: "hidden", name: "t:submit" |
| firstHidden.insertBefore hidden |
| |
| # TODO: Research why we need id and name and get rid of one if possible. |
| name = if isCancel then "cancel" else submitter.element.name |
| # Not going to drag in all of json2 just for this one purpose, but even |
| # so, I'd like to get rid of this. Prototype includes Object.toJSON(), but jQuery |
| # is curiously absent an equivalent. |
| hidden.value "[\"#{submitter.element.id}\",\"#{name}\"]" |
| |
| return |
| |
| # Passed the element wrapper for a form element, returns a map of all the values |
| # for all non-disabled fields (including hidden fields, select, textarea). This is primarily |
| # used when assembling an Ajax request for a form submission. |
| gatherParameters = (form) -> |
| result = {} |
| |
| fields = form.find "input, select, textarea" |
| |
| _.each fields, (field) -> |
| return if field.attr "disabled" |
| |
| type = field.element.type |
| |
| # Ignore types file and submit; file doesn't make sense for Ajax, and submit |
| # is handled by keeping a hidden field active with the data Tapestry needs |
| # on the server. |
| return if type is "file" or type is "submit" |
| |
| return if (type is "checkbox" or type is "radio") and field.checked() is false |
| |
| value = field.value() |
| |
| return if value is null |
| |
| name = field.element.name |
| |
| # Many modern UIs create name-less elements on the fly (e.g., Backbone); these may be mixed |
| # in with normal elements managed by Tapestry but should be ignored (not sent to the server in a POST |
| # or Ajax update). |
| return if name is "" |
| |
| existing = result[name] |
| |
| if _.isArray existing |
| existing.push value |
| return |
| |
| if existing |
| result[name] = [existing, value] |
| return |
| |
| result[name] = value |
| |
| return result |
| |
| |
| defaultValidateAndSubmit = -> |
| |
| where = -> "processing form submission" |
| |
| try |
| |
| if ((@attr "data-validate") is "submit") and |
| (not @meta SKIP_VALIDATION) |
| |
| @meta SKIP_VALIDATION, null |
| |
| hasError = false |
| focusField = null |
| |
| for field in @find "[data-validation]" |
| memo = {} |
| where = -> "triggering #{events.field.inputValidation} event on #{field.toString()}" |
| field.trigger events.field.inputValidation, memo |
| |
| if memo.error |
| hasError = true |
| focusField = field unless focusField |
| |
| # Only do form validation if all individual field validation |
| # was successful. |
| unless hasError |
| memo = {} |
| where = -> "trigging cross-form validation event" |
| @trigger events.form.validate, memo |
| |
| hasError = memo.error |
| |
| if hasError |
| clearSubmittingHidden this |
| |
| # If a specific field has been identified as the source of the validation error, then |
| # focus on it. |
| focusField.focus() if focusField |
| |
| # Trigger an event to inform that form validation results in error |
| where = -> "triggering validation in error event" |
| @trigger events.form.validateInError |
| |
| # Cancel the original submit event when there's an error |
| return false |
| |
| # Allow certain types of elements to do last-moment set up. Basically, this is for |
| # FormFragment, or similar, to make their hidden field enabled or disabled to match |
| # their UI's visible/hidden status. This is assumed to work or throw an exception; there |
| # is no memo. |
| where = -> "triggering #{events.form.prepareForSubmit} event (after validation)" |
| |
| @trigger events.form.prepareForSubmit |
| |
| catch error |
| console.error "Form validiation/submit error `#{error.toString()}', in form #{this.toString()}, #{where()}" |
| console.error error |
| return false |
| |
| # Otherwise, the event is good, there are no validation problems, let the normal processing commence. |
| # Possibly, the document event handler provided by the t5/core/zone module will intercept form submission if this |
| # is an Ajax submission. |
| return |
| |
| dom.onDocument "submit", "form", defaultValidateAndSubmit |
| |
| # On any click on a submit or image, update the containing form to indicate that the element |
| # was responsible for the eventual submit; this is very important to Ajax updates, otherwise the |
| # information about which control triggered the submit gets lost. |
| dom.onDocument "click", "input[type=submit], input[type=image]", -> |
| setSubmittingHidden (dom @element.form), this |
| return |
| |
| # Support for link submits. `data-submit-mode` will be non-null, possibly "cancel". |
| # Update the hidden field, but also cancel the default behavior for the click. |
| dom.onDocument "click", "a[data-submit-mode]", -> |
| form = @findParent "form" |
| |
| unless form |
| console.error "Submitting link element not contained inside a form element." |
| return false |
| |
| setSubmittingHidden form, @closest "a[data-submit-mode]" |
| |
| # Now the ugly part; if we just invoke submit() on the form, it does not trigger |
| # the form's "submit" event, which we need. |
| |
| form.trigger "submit" |
| |
| # And cancel the default behavior for the original click event |
| return false |
| |
| exports = |
| gatherParameters: gatherParameters |
| |
| setSubmittingElement: setSubmittingHidden |
| |
| # Sets a flag on the form to indicate that client-side validation should be bypassed. |
| # This is typically associated with submit buttons that "cancel" the form. |
| skipValidation: (form) -> |
| form.meta SKIP_VALIDATION, true |