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