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