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 &lt;form&gt; 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 &lt;iframe&gt; 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}