001package org.gwtbootstrap3.client.ui.base.form; 002 003import java.util.ArrayList; 004import java.util.List; 005 006import org.gwtbootstrap3.client.ui.constants.Attributes; 007import org.gwtbootstrap3.client.ui.form.validator.HasValidators; 008 009import com.google.gwt.core.client.GWT; 010import com.google.gwt.core.client.Scheduler; 011import com.google.gwt.core.client.Scheduler.ScheduledCommand; 012import com.google.gwt.dom.client.Document; 013import com.google.gwt.dom.client.Element; 014import com.google.gwt.dom.client.FormElement; 015import com.google.gwt.event.dom.client.KeyCodes; 016import com.google.gwt.event.dom.client.KeyPressEvent; 017import com.google.gwt.event.dom.client.KeyPressHandler; 018import com.google.gwt.event.shared.EventHandler; 019import com.google.gwt.event.shared.GwtEvent; 020import com.google.gwt.event.shared.HandlerRegistration; 021import com.google.gwt.safehtml.client.SafeHtmlTemplates; 022import com.google.gwt.safehtml.shared.SafeHtml; 023import com.google.gwt.safehtml.shared.SafeUri; 024import com.google.gwt.user.client.Event; 025import com.google.gwt.user.client.ui.HasOneWidget; 026import com.google.gwt.user.client.ui.HasWidgets; 027import com.google.gwt.user.client.ui.NamedFrame; 028import com.google.gwt.user.client.ui.RootPanel; 029import com.google.gwt.user.client.ui.Widget; 030import com.google.gwt.user.client.ui.impl.FormPanelImpl; 031import com.google.gwt.user.client.ui.impl.FormPanelImplHost; 032 033/* 034 * #%L 035 * GwtBootstrap3 036 * %% 037 * Copyright (C) 2013 GwtBootstrap3 038 * %% 039 * Licensed under the Apache License, Version 2.0 (the "License"); 040 * you may not use this file except in compliance with the License. 041 * You may obtain a copy of the License at 042 * 043 * http://www.apache.org/licenses/LICENSE-2.0 044 * 045 * Unless required by applicable law or agreed to in writing, software 046 * distributed under the License is distributed on an "AS IS" BASIS, 047 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 048 * See the License for the specific language governing permissions and 049 * limitations under the License. 050 * #L% 051 */ 052 053/** 054 * @author Sven Jacobs 055 * @author Steven Jardine 056 */ 057public abstract class AbstractForm extends FormElementContainer implements FormPanelImplHost { 058 059 /** 060 * Fired when a form has been submitted successfully. 061 */ 062 public static class SubmitCompleteEvent extends GwtEvent<SubmitCompleteHandler> { 063 064 /** 065 * The event type. 066 */ 067 private static Type<SubmitCompleteHandler> TYPE; 068 069 /** 070 * Handler hook. 071 * 072 * @return the handler hook 073 */ 074 public static Type<SubmitCompleteHandler> getType() { 075 if (TYPE == null) { 076 TYPE = new Type<SubmitCompleteHandler>(); 077 } 078 return TYPE; 079 } 080 081 private String resultHtml; 082 083 /** 084 * Create a submit complete event. 085 * 086 * @param resultsHtml the results from submitting the form 087 */ 088 protected SubmitCompleteEvent(String resultsHtml) { 089 this.resultHtml = resultsHtml; 090 } 091 092 @Override 093 public final Type<SubmitCompleteHandler> getAssociatedType() { 094 return getType(); 095 } 096 097 /** 098 * Gets the result text of the form submission. 099 * 100 * @return the result html, or <code>null</code> if there was an error 101 * reading it 102 * @tip The result html can be <code>null</code> as a result of submitting a 103 * form to a different domain. 104 */ 105 public String getResults() { 106 return resultHtml; 107 } 108 109 @Override 110 protected void dispatch(SubmitCompleteHandler handler) { 111 handler.onSubmitComplete(this); 112 } 113 } 114 115 /** 116 * Handler for {@link AbstractForm.SubmitCompleteEvent} events. 117 */ 118 public interface SubmitCompleteHandler extends EventHandler { 119 120 /** 121 * Fired when a form has been submitted successfully. 122 * 123 * @param event the event 124 */ 125 void onSubmitComplete(AbstractForm.SubmitCompleteEvent event); 126 } 127 128 /** 129 * Fired when the form is submitted. 130 */ 131 public static class SubmitEvent extends GwtEvent<SubmitHandler> { 132 133 /** 134 * The event type. 135 */ 136 private static Type<SubmitHandler> TYPE; 137 138 /** 139 * Handler hook. 140 * 141 * @return the handler hook 142 */ 143 public static Type<SubmitHandler> getType() { 144 if (TYPE == null) { 145 TYPE = new Type<SubmitHandler>(); 146 } 147 return TYPE; 148 } 149 150 private boolean canceled = false; 151 152 /** 153 * Cancel the form submit. Firing this will prevent a subsequent 154 * {@link AbstractForm.SubmitCompleteEvent} from being fired. 155 */ 156 public void cancel() { 157 this.canceled = true; 158 } 159 160 @Override 161 public final Type<AbstractForm.SubmitHandler> getAssociatedType() { 162 return getType(); 163 } 164 165 /** 166 * Gets whether this form submit will be canceled. 167 * 168 * @return <code>true</code> if the form submit will be canceled 169 */ 170 public boolean isCanceled() { 171 return canceled; 172 } 173 174 @Override 175 protected void dispatch(AbstractForm.SubmitHandler handler) { 176 handler.onSubmit(this); 177 } 178 179 /** 180 * This method is used for legacy support and should be removed when 181 * {@link SubmitCompleteHandler} is removed. 182 * 183 * @deprecated Use {@link AbstractForm.SubmitEvent#cancel()} instead 184 */ 185 @Deprecated 186 void setCanceled(boolean canceled) { 187 this.canceled = canceled; 188 } 189 } 190 191 /** 192 * Handler for {@link AbstractForm.SubmitEvent} events. 193 */ 194 public interface SubmitHandler extends EventHandler { 195 196 /** 197 * Fired when the form is submitted. 198 * 199 * <p> 200 * The AbstractForm must <em>not</em> be detached (i.e. removed from its parent 201 * or otherwise disconnected from a {@link RootPanel}) until the submission 202 * is complete. Otherwise, notification of submission will fail. 203 * </p> 204 * 205 * @param event the event 206 */ 207 void onSubmit(AbstractForm.SubmitEvent event); 208 } 209 210 interface IFrameTemplate extends SafeHtmlTemplates { 211 212 static final IFrameTemplate INSTANCE = GWT.create(IFrameTemplate.class); 213 214 @Template("<iframe src=\"javascript:''\" name='{0}' tabindex='-1' title='Form submit helper frame'" 215 + "style='position:absolute;width:0;height:0;border:0'>") 216 SafeHtml get(String name); 217 } 218 219 private static final String FORM = "form"; 220 221 private static int formId = 0; 222 223 private static final FormPanelImpl impl = GWT.create(FormPanelImpl.class); 224 225 private String frameName; 226 227 private Element synthesizedFrame; 228 229 public AbstractForm() { 230 this(true); 231 } 232 233 public AbstractForm(boolean createIFrame) { 234 this(Document.get().createFormElement(), createIFrame); 235 getElement().setAttribute(Attributes.ROLE, FORM); 236 } 237 238 /** 239 * This constructor may be used by subclasses to explicitly use an existing 240 * element. This element must be a <form> element. 241 * <p> 242 * If the createIFrame parameter is set to <code>true</code>, then the 243 * wrapped form's target attribute will be set to a hidden iframe. If not, 244 * the form's target will be left alone, and the FormSubmitComplete event 245 * will not be fired. 246 * </p> 247 * 248 * @param element 249 * the element to be used 250 * @param createIFrame 251 * <code>true</code> to create an <iframe> element that 252 * will be targeted by this form 253 */ 254 protected AbstractForm(Element element, boolean createIFrame) { 255 setElement(element); 256 FormElement.as(element); 257 258 if (createIFrame) { 259 assert getTarget() == null || getTarget().trim() 260 .length() == 0 : "Cannot create target iframe if the form's target is already set."; 261 262 // We use the module name as part of the unique ID to ensure that 263 // ids are 264 // unique across modules. 265 frameName = "GWTBootstrap3_AbstractForm_" + GWT.getModuleName() + "_" + (++formId); 266 setTarget(frameName); 267 268 sinkEvents(Event.ONLOAD); 269 } 270 } 271 272 @Override 273 protected void onAttach() { 274 super.onAttach(); 275 276 if (frameName != null) { 277 // Create and attach a hidden iframe to the body element. 278 createFrame(); 279 Document.get().getBody().appendChild(synthesizedFrame); 280 } 281 // Hook up the underlying iframe's onLoad event when attached to the 282 // DOM. 283 // Making this connection only when attached avoids memory-leak issues. 284 // The AbstractForm cannot use the built-in GWT event-handling mechanism 285 // because there is no standard onLoad event on iframes that works 286 // across 287 // browsers. 288 impl.hookEvents(synthesizedFrame, getElement(), this); 289 } 290 291 @Override 292 protected void onDetach() { 293 super.onDetach(); 294 295 // Unhook the iframe's onLoad when detached. 296 impl.unhookEvents(synthesizedFrame, getElement()); 297 298 if (synthesizedFrame != null) { 299 // And remove it from the document. 300 Document.get().getBody().removeChild(synthesizedFrame); 301 synthesizedFrame = null; 302 } 303 } 304 305 @Override 306 public boolean onFormSubmit() { 307 return onFormSubmitImpl(); 308 } 309 310 @Override 311 public void onFrameLoad() { 312 onFrameLoadImpl(); 313 } 314 315 /** 316 * Adds a {@link SubmitCompleteEvent} handler. 317 * 318 * @param handler 319 * the handler 320 * @return the handler registration used to remove the handler 321 */ 322 public HandlerRegistration addSubmitCompleteHandler(SubmitCompleteHandler handler) { 323 return addHandler(handler, SubmitCompleteEvent.getType()); 324 } 325 326 /** 327 * Adds a {@link AbstractForm.SubmitEvent} handler. 328 * 329 * @param handler 330 * the handler 331 * @return the handler registration used to remove the handler 332 */ 333 public HandlerRegistration addSubmitHandler(AbstractForm.SubmitHandler handler) { 334 return addHandler(handler, AbstractForm.SubmitEvent.getType()); 335 } 336 337 /** 338 * Gets the 'action' associated with this form. This is the URL to which it 339 * will be submitted. 340 * 341 * @return the form's action 342 */ 343 public String getAction() { 344 return getFormElement().getAction(); 345 } 346 347 /** 348 * Sets the 'action' associated with this form. This is the URL to which it 349 * will be submitted. 350 * 351 * @param action 352 * the form's action 353 */ 354 public void setAction(final String action) { 355 getFormElement().setAction(action); 356 } 357 358 /** 359 * Sets the 'action' associated with this form. This is the URL to which it 360 * will be submitted. 361 * 362 * @param url 363 * the form's action 364 */ 365 public void setAction(SafeUri url) { 366 getFormElement().setAction(url); 367 } 368 369 /** 370 * Gets the HTTP method used for submitting this form. This should be either 371 * {@link #METHOD_GET} or {@link #METHOD_POST}. 372 * 373 * @return the form's method 374 */ 375 public String getMethod() { 376 return getFormElement().getMethod(); 377 } 378 379 /** 380 * Sets the HTTP method used for submitting this form. This should be either 381 * {@link #METHOD_GET} or {@link #METHOD_POST}. 382 * 383 * @param method 384 * the form's method 385 */ 386 public void setMethod(final String method) { 387 getFormElement().setMethod(method); 388 } 389 390 /** 391 * Gets the form's 'target'. This is the name of the {@link NamedFrame} that 392 * will receive the results of submission, or <code>null</code> if none has 393 * been specified. 394 * 395 * @return the form's target. 396 */ 397 public String getTarget() { 398 return getFormElement().getTarget(); 399 } 400 401 /** 402 * Gets the encoding used for submitting this form. This should be either 403 * {@link #ENCODING_MULTIPART} or {@link #ENCODING_URLENCODED}. 404 * 405 * @return the form's encoding 406 */ 407 public String getEncoding() { 408 return impl.getEncoding(getElement()); 409 } 410 411 /** 412 * Sets the encoding used for submitting this form. This should be either 413 * {@link #ENCODING_MULTIPART} or {@link #ENCODING_URLENCODED}. 414 * 415 * @param encodingType 416 * the form's encoding 417 */ 418 public void setEncoding(String encodingType) { 419 impl.setEncoding(getElement(), encodingType); 420 } 421 422 /** 423 * Submits form 424 */ 425 public void submit() { 426 // Fire the onSubmit event, because javascript's form.submit() does not 427 // fire the built-in onsubmit event. 428 if (!fireSubmitEvent()) { 429 return; 430 } 431 impl.submit(getElement(), synthesizedFrame); 432 } 433 434 /** 435 * Resets form 436 */ 437 public void reset() { 438 impl.reset(getElement()); 439 for (HasValidators<?> child : getChildrenWithValidators(this)) { 440 child.reset(); 441 } 442 } 443 444 private void createFrame() { 445 // Attach a hidden IFrame to the form. This is the target iframe to 446 // which the form will be submitted. We have to create the iframe using 447 // innerHTML, because setting an iframe's 'name' property dynamically 448 // doesn't work on most browsers. 449 Element dummy = Document.get().createDivElement(); 450 dummy.setInnerSafeHtml(IFrameTemplate.INSTANCE.get(frameName)); 451 452 synthesizedFrame = dummy.getFirstChildElement(); 453 } 454 455 /** 456 * Fire a {@link AbstractForm.SubmitEvent}. 457 * 458 * @return true to continue, false if canceled 459 */ 460 private boolean fireSubmitEvent() { 461 AbstractForm.SubmitEvent event = new AbstractForm.SubmitEvent(); 462 fireEvent(event); 463 return !event.isCanceled(); 464 } 465 466 FormElement getFormElement() { 467 return FormElement.as(getElement()); 468 } 469 470 /** 471 * Returns true if the form is submitted, false if canceled. 472 */ 473 private boolean onFormSubmitImpl() { 474 return fireSubmitEvent(); 475 } 476 477 private void onFrameLoadImpl() { 478 // Fire onComplete events in a deferred command. This is necessary 479 // because clients that detach the form panel when submission is 480 // complete can cause some browsers (i.e. Mozilla) to go into an 481 // 'infinite loading' state. See issue 916. 482 Scheduler.get().scheduleDeferred(new ScheduledCommand() { 483 484 @Override 485 public void execute() { 486 fireEvent(new SubmitCompleteEvent(impl.getContents(synthesizedFrame))); 487 } 488 }); 489 } 490 491 private void setTarget(String target) { 492 getFormElement().setTarget(target); 493 } 494 495 /** 496 * @return true if the child input elements are all valid. 497 */ 498 public boolean validate() { 499 return validate(true); 500 } 501 502 /** 503 * @return true if the child input elements are all valid. 504 */ 505 public boolean validate(boolean show) { 506 boolean result = true; 507 for (HasValidators<?> child : getChildrenWithValidators(this)) { 508 result &= child.validate(show); 509 } 510 return result; 511 } 512 513 /** 514 * Get this forms child input elements with validators. 515 * 516 * @param widget the widget 517 * @return the children with validators 518 */ 519 protected List<HasValidators<?>> getChildrenWithValidators(Widget widget) { 520 List<HasValidators<?>> result = new ArrayList<HasValidators<?>>(); 521 if (widget != null) { 522 if (widget instanceof HasValidators<?>) { 523 result.add((HasValidators<?>) widget); 524 } 525 if (widget instanceof HasOneWidget) { 526 result.addAll(getChildrenWithValidators(((HasOneWidget) widget).getWidget())); 527 } 528 if (widget instanceof HasWidgets) { 529 for (Widget child : (HasWidgets) widget) { 530 result.addAll(getChildrenWithValidators(child)); 531 } 532 } 533 } 534 return result; 535 } 536 537 private HandlerRegistration submitOnEnterRegistration = null; 538 539 public void setSubmitOnEnter(boolean submitOnEnter) { 540 if (submitOnEnter) { 541 if (submitOnEnterRegistration == null) 542 submitOnEnterRegistration = addDomHandler(new KeyPressHandler() { 543 @Override 544 public void onKeyPress(KeyPressEvent event) { 545 if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) { 546 if (validate()) { 547 fireSubmitEvent(); 548 } 549 } 550 } 551 }, KeyPressEvent.getType()); 552 } else if (submitOnEnterRegistration != null) { 553 submitOnEnterRegistration.removeHandler(); 554 submitOnEnterRegistration = null; 555 } 556 } 557 558 public boolean isSubmitOnEnter() { 559 return submitOnEnterRegistration != null; 560 } 561 562}