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 &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().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    }