001 package org.gwtbootstrap3.client.ui.base.form;
002
003 import java.util.ArrayList;
004 import java.util.List;
005
006 import org.gwtbootstrap3.client.ui.constants.Attributes;
007 import org.gwtbootstrap3.client.ui.form.validator.HasValidators;
008
009 import com.google.gwt.core.client.GWT;
010 import com.google.gwt.core.client.Scheduler;
011 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
012 import com.google.gwt.dom.client.Document;
013 import com.google.gwt.dom.client.Element;
014 import com.google.gwt.dom.client.FormElement;
015 import com.google.gwt.event.shared.EventHandler;
016 import com.google.gwt.event.shared.GwtEvent;
017 import com.google.gwt.event.shared.HandlerRegistration;
018 import com.google.gwt.safehtml.client.SafeHtmlTemplates;
019 import com.google.gwt.safehtml.shared.SafeHtml;
020 import com.google.gwt.safehtml.shared.SafeUri;
021 import com.google.gwt.user.client.Event;
022 import com.google.gwt.user.client.ui.FormPanel;
023 import com.google.gwt.user.client.ui.HasWidgets;
024 import com.google.gwt.user.client.ui.NamedFrame;
025 import com.google.gwt.user.client.ui.RootPanel;
026 import com.google.gwt.user.client.ui.Widget;
027 import com.google.gwt.user.client.ui.impl.FormPanelImpl;
028 import com.google.gwt.user.client.ui.impl.FormPanelImplHost;
029
030 /*
031 * #%L
032 * GwtBootstrap3
033 * %%
034 * Copyright (C) 2013 GwtBootstrap3
035 * %%
036 * Licensed under the Apache License, Version 2.0 (the "License");
037 * you may not use this file except in compliance with the License.
038 * You may obtain a copy of the License at
039 *
040 * http://www.apache.org/licenses/LICENSE-2.0
041 *
042 * Unless required by applicable law or agreed to in writing, software
043 * distributed under the License is distributed on an "AS IS" BASIS,
044 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
045 * See the License for the specific language governing permissions and
046 * limitations under the License.
047 * #L%
048 */
049
050 /**
051 * @author Sven Jacobs
052 * @author Steven Jardine
053 */
054 public abstract class AbstractForm extends FormElementContainer implements
055 FormPanelImplHost {
056
057 /**
058 * Fired when a form has been submitted successfully.
059 */
060 public static class SubmitCompleteEvent extends
061 GwtEvent<SubmitCompleteHandler> {
062
063 /**
064 * The event type.
065 */
066 private static Type<SubmitCompleteHandler> TYPE;
067
068 /**
069 * Handler hook.
070 *
071 * @return the handler hook
072 */
073 public static Type<SubmitCompleteHandler> getType() {
074 if (TYPE == null) {
075 TYPE = new Type<SubmitCompleteHandler>();
076 }
077 return TYPE;
078 }
079
080 private final String resultHtml;
081
082 /**
083 * Create a submit complete event.
084 *
085 * @param resultsHtml
086 * 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
103 * submitting a 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 SubmitCompleteEvent} events.
117 */
118 public interface SubmitCompleteHandler extends EventHandler {
119
120 /**
121 * Fired when a form has been submitted successfully.
122 *
123 * @param event
124 * the event
125 */
126 void onSubmitComplete(SubmitCompleteEvent event);
127 }
128
129 /**
130 * Fired when the form is submitted.
131 */
132 public static class SubmitEvent extends GwtEvent<SubmitHandler> {
133
134 /**
135 * The event type.
136 */
137 private static Type<SubmitHandler> TYPE;
138
139 /**
140 * Handler hook.
141 *
142 * @return the handler hook
143 */
144 public static Type<SubmitHandler> getType() {
145 if (TYPE == null) {
146 TYPE = new Type<SubmitHandler>();
147 }
148 return TYPE;
149 }
150
151 private boolean canceled = false;
152
153 /**
154 * Cancel the form submit. Firing this will prevent a subsequent
155 * {@link SubmitCompleteEvent} from being fired.
156 */
157 public void cancel() {
158 this.canceled = true;
159 }
160
161 @Override
162 public final Type<SubmitHandler> getAssociatedType() {
163 return getType();
164 }
165
166 /**
167 * Gets whether this form submit will be canceled.
168 *
169 * @return <code>true</code> if the form submit will be canceled
170 */
171 public boolean isCanceled() {
172 return canceled;
173 }
174
175 @Override
176 protected void dispatch(SubmitHandler handler) {
177 handler.onSubmit(this);
178 }
179
180 /**
181 * This method is used for legacy support and should be removed when
182 * {@link FormPanel.SubmitHandler} is removed.
183 *
184 * @deprecated Use {@link FormPanel.SubmitEvent#cancel()} instead
185 */
186 @Deprecated
187 void setCanceled(boolean canceled) {
188 this.canceled = canceled;
189 }
190 }
191
192 /**
193 * Handler for {@link FormPanel.SubmitEvent} events.
194 */
195 public interface SubmitHandler extends EventHandler {
196
197 /**
198 * Fired when the form is submitted.
199 *
200 * <p>
201 * The FormPanel must <em>not</em> be detached (i.e. removed from its
202 * parent or otherwise disconnected from a {@link RootPanel}) until the
203 * submission is complete. Otherwise, notification of submission will
204 * fail.
205 * </p>
206 *
207 * @param event
208 * the event
209 */
210 void onSubmit(SubmitEvent event);
211 }
212
213 interface IFrameTemplate extends SafeHtmlTemplates {
214
215 static final IFrameTemplate INSTANCE = GWT.create(IFrameTemplate.class);
216
217 @Template("<iframe src=\"javascript:''\" name='{0}' tabindex='-1' "
218 + "style='position:absolute;width:0;height:0;border:0'>")
219 SafeHtml get(String name);
220 }
221
222 private static final String FORM = "form";
223 private static int formId = 0;
224 private static final FormPanelImpl impl = GWT.create(FormPanelImpl.class);
225
226 private String frameName;
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().length() == 0 : "Cannot create target iframe if the form's target is already set.";
260
261 // We use the module name as part of the unique ID to ensure that
262 // ids are
263 // unique across modules.
264 frameName = "FormPanel_" + GWT.getModuleName() + "_" + (++formId);
265 setTarget(frameName);
266
267 sinkEvents(Event.ONLOAD);
268 }
269 }
270
271 @Override
272 protected void onAttach() {
273 super.onAttach();
274
275 if (frameName != null) {
276 // Create and attach a hidden iframe to the body element.
277 createFrame();
278 Document.get().getBody().appendChild(synthesizedFrame);
279 }
280 // Hook up the underlying iframe's onLoad event when attached to the
281 // DOM.
282 // Making this connection only when attached avoids memory-leak issues.
283 // The FormPanel cannot use the built-in GWT event-handling mechanism
284 // because there is no standard onLoad event on iframes that works
285 // across
286 // browsers.
287 impl.hookEvents(synthesizedFrame, getElement(), this);
288 }
289
290 @Override
291 protected void onDetach() {
292 super.onDetach();
293
294 // Unhook the iframe's onLoad when detached.
295 impl.unhookEvents(synthesizedFrame, getElement());
296
297 if (synthesizedFrame != null) {
298 // And remove it from the document.
299 Document.get().getBody().removeChild(synthesizedFrame);
300 synthesizedFrame = null;
301 }
302 }
303
304 @Override
305 public boolean onFormSubmit() {
306 return onFormSubmitImpl();
307 }
308
309 @Override
310 public void onFrameLoad() {
311 onFrameLoadImpl();
312 }
313
314 /**
315 * Adds a {@link SubmitCompleteEvent} handler.
316 *
317 * @param handler
318 * the handler
319 * @return the handler registration used to remove the handler
320 */
321 public HandlerRegistration addSubmitCompleteHandler(
322 SubmitCompleteHandler handler) {
323 return addHandler(handler, SubmitCompleteEvent.getType());
324 }
325
326 /**
327 * Adds a {@link SubmitEvent} handler.
328 *
329 * @param handler
330 * the handler
331 * @return the handler registration used to remove the handler
332 */
333 public HandlerRegistration addSubmitHandler(SubmitHandler handler) {
334 return addHandler(handler, 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 url
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
432 impl.submit(getElement(), synthesizedFrame);
433 }
434
435 /**
436 * Resets form
437 */
438 public void reset() {
439 impl.reset(getElement());
440 for (HasValidators<?> child : getChildrenWithValidators(this)) {
441 child.reset();
442 }
443 }
444
445
446 private void createFrame() {
447 // Attach a hidden IFrame to the form. This is the target iframe to
448 // which the form will be submitted. We have to create the iframe using
449 // innerHTML, because setting an iframe's 'name' property dynamically
450 // doesn't work on most browsers.
451 Element dummy = Document.get().createDivElement();
452 dummy.setInnerSafeHtml(IFrameTemplate.INSTANCE.get(frameName));
453
454 synthesizedFrame = dummy.getFirstChildElement();
455 }
456
457 /**
458 * Fire a {@link FormPanel.SubmitEvent}.
459 *
460 * @return true to continue, false if canceled
461 */
462 private boolean fireSubmitEvent() {
463 FormPanel.SubmitEvent event = new FormPanel.SubmitEvent();
464 fireEvent(event);
465 return !event.isCanceled();
466 }
467
468 FormElement getFormElement() {
469 return FormElement.as(getElement());
470 }
471
472 /**
473 * Returns true if the form is submitted, false if canceled.
474 */
475 private boolean onFormSubmitImpl() {
476 return fireSubmitEvent();
477 }
478
479 private void onFrameLoadImpl() {
480 // Fire onComplete events in a deferred command. This is necessary
481 // because clients that detach the form panel when submission is
482 // complete can cause some browsers (i.e. Mozilla) to go into an
483 // 'infinite loading' state. See issue 916.
484 Scheduler.get().scheduleDeferred(new ScheduledCommand() {
485
486 @Override
487 public void execute() {
488 fireEvent(new SubmitCompleteEvent(impl
489 .getContents(synthesizedFrame)));
490 }
491 });
492 }
493
494 private void setTarget(String target) {
495 getFormElement().setTarget(target);
496 }
497
498 /**
499 * @return true if the child input elements are all valid.
500 */
501 public boolean validate() {
502 return validate(true);
503 }
504
505 /**
506 * @return true if the child input elements are all valid.
507 */
508 public boolean validate(boolean show) {
509 boolean result = true;
510 for (HasValidators<?> child : getChildrenWithValidators(this)) {
511 result &= child.validate(show);
512 }
513 return result;
514 }
515
516 /**
517 * Get this forms child input elements with validators.
518 *
519 * @param widget the widget
520 * @return the children with validators
521 */
522 protected List<HasValidators<?>> getChildrenWithValidators(Widget widget) {
523 List<HasValidators<?>> result = new ArrayList<HasValidators<?>>();
524 if (widget != null) {
525 if (widget instanceof HasValidators<?>) {
526 result.add((HasValidators<?>) widget);
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 }