View Javadoc
1   /**
2    * This file Copyright (c) 2013-2017 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.vaadin.gwt.client.richtext;
35  
36  import info.magnolia.ui.vaadin.gwt.client.dialog.widget.OverlayWidget;
37  import info.magnolia.ui.vaadin.gwt.client.form.widget.FormView;
38  import info.magnolia.ui.vaadin.gwt.client.jquerywrapper.JQueryWrapper;
39  import info.magnolia.ui.vaadin.richtext.TextAreaStretcher;
40  
41  import com.google.gwt.core.client.Scheduler;
42  import com.google.gwt.dom.client.Element;
43  import com.google.gwt.dom.client.Style;
44  import com.google.gwt.event.logical.shared.AttachEvent;
45  import com.google.gwt.user.client.DOM;
46  import com.google.gwt.user.client.ui.Widget;
47  import com.googlecode.mgwt.dom.client.event.touch.TouchEndEvent;
48  import com.googlecode.mgwt.dom.client.event.touch.TouchEndHandler;
49  import com.googlecode.mgwt.ui.client.widget.touch.TouchDelegate;
50  import com.vaadin.client.ComponentConnector;
51  import com.vaadin.client.ComputedStyle;
52  import com.vaadin.client.LayoutManager;
53  import com.vaadin.client.ServerConnector;
54  import com.vaadin.client.Util;
55  import com.vaadin.client.communication.StateChangeEvent;
56  import com.vaadin.client.extensions.AbstractExtensionConnector;
57  import com.vaadin.client.ui.layout.ElementResizeEvent;
58  import com.vaadin.client.ui.layout.ElementResizeListener;
59  import com.vaadin.client.ui.ui.UIConnector;
60  import com.vaadin.shared.ui.Connect;
61  
62  /**
63   * Client-side connector for {@link info.magnolia.ui.vaadin.richtext.TextAreaStretcher}.
64   */
65  @Connect(TextAreaStretcher.class)
66  public class TextAreaStretcherConnector extends AbstractExtensionConnector {
67  
68      public static final String STRETCHER_BASE = "textarea-stretcher";
69      public static final String STRETCHED = "stretched";
70      public static final String COLLAPSED = "collapsed";
71      public static final String CKEDITOR_TOOLBOX = ".cke_top";
72      public static final String TEXTAREA_STRETCHED = "textarea-stretched";
73      public static final String RICH_TEXT_STYLE_NAME = "rich-text";
74      public static final String SIMPLE_STYLE_NAME = "simple";
75      public static final int DELAY_MS = 500;
76  
77      private static final int UNCALCULATED_SIZE = -1;
78      private static final int TOP = 0;
79      private static final int RIGHT = 1;
80      private static final int BOTTOM = 2;
81      private static final int LEFT = 3;
82  
83      private Widget form;
84      private Widget dialog;
85      private Widget textWidget;
86      private Element stretchControl = DOM.createDiv();
87      private ComputedStyle dialogComputedStyle;
88  
89      // Border, padding, margin property for dialog-wrapper, dialog-header, dialog-content class, contain values for top, right, bottom, left.
90      private int[] dialogWrapperBorder;
91      private int[] dialogHeaderPadding;
92      private int[] dialogContentMargin;
93      private int[] dialogMargin;
94      private int[] dialogPadding;
95      private String formHeight;
96      private int dialogHeaderOuterHeight = UNCALCULATED_SIZE;
97      private int dialogFooterOuterHeight = UNCALCULATED_SIZE;
98      private int dialogDesErrorOuterHeight = UNCALCULATED_SIZE;
99      private boolean isOverlay = false;
100     private boolean isRichTextEditor = false;
101 
102     private StateChangeEvent.StateChangeHandler textAreaSizeHandler = new StateChangeEvent.StateChangeHandler() {
103         @Override
104         public void onStateChanged(StateChangeEvent stateChangeEvent) {
105             if (isFormVisible()) {
106                 adjustTextAreaAndFormSizeToScreen();
107             }
108         }
109     };
110 
111     private WindowResizeListener windowResizeListener = new WindowResizeListener();
112 
113     private ElementResizeListener formResizeListener = new ElementResizeListener() {
114         @Override
115         public void onElementResize(ElementResizeEvent e) {
116             doElementResize();
117         }
118     };
119 
120     private StateChangeEvent.StateChangeHandler formStateChangeHandler = new StateChangeEvent.StateChangeHandler() {
121         @Override
122         public void onStateChanged(StateChangeEvent stateChangeEvent) {
123             if (stateChangeEvent.hasPropertyChanged("descriptionsVisible") || stateChangeEvent.hasPropertyChanged("errorAmount")) {
124                 if (!getState().isCollapsed) {
125                     unregisterSizeChangeListeners();
126                     getRpcProxy(TextAreaStretcherServerRpc.class).toggle(textWidget.getOffsetWidth(), textWidget.getOffsetHeight());
127                 }
128             }
129         }
130     };
131 
132     @Override
133     public void onStateChanged(StateChangeEvent stateChangeEvent) {
134         super.onStateChanged(stateChangeEvent);
135         if (stateChangeEvent.hasPropertyChanged("isCollapsed")) {
136             updateSize();
137             if (!getState().isCollapsed) {
138                 registerSizeChangeListeners();
139             }
140         }
141     }
142 
143     @Override
144     public ComponentConnector getParent() {
145         return (ComponentConnector) super.getParent();
146     }
147 
148     @Override
149     protected void extend(ServerConnector target) {
150         this.textWidget = ((ComponentConnector) target).getWidget();
151         this.isRichTextEditor = target instanceof RichTextConnector;
152         this.stretchControl.setClassName(STRETCHER_BASE);
153         textWidget.addAttachHandler(new AttachEvent.Handler() {
154             @Override
155             public void onAttachOrDetach(AttachEvent attachEvent) {
156                 if (attachEvent.isAttached()) {
157                     initFormView();
158                     initDialog();
159                     checkOverlay();
160                     if (!isRichTextEditor) {
161                         appendStretcher(textWidget.getElement());
162                         stretchControl.addClassName(SIMPLE_STYLE_NAME);
163                     } else {
164                         Scheduler.get().scheduleFixedDelay(new Scheduler.RepeatingCommand() {
165                             private int repeats = 0;
166 
167                             @Override
168                             public boolean execute() {
169                                 repeats++;
170                                 isRichTextEditor = true;
171                                 final Element toolbox = JQueryWrapper.select(textWidget).find(CKEDITOR_TOOLBOX).get(0);
172                                 if (toolbox != null) {
173                                     appendStretcher(toolbox);
174                                     stretchControl.addClassName(RICH_TEXT_STYLE_NAME);
175                                 }
176                                 return toolbox == null && repeats < 5;
177                             }
178                         }, DELAY_MS);
179                     }
180                 } else {
181                     clearTraces();
182                 }
183             }
184         });
185     }
186 
187     @Override
188     public TextAreaStretcherState getState() {
189         return (TextAreaStretcherState) super.getState();
190     }
191 
192     private void appendStretcher(Element rootElement) {
193         rootElement.getStyle().setPosition(Style.Position.RELATIVE);
194         rootElement.getParentElement().insertAfter(stretchControl, rootElement);
195         Widget parent = textWidget.getParent();
196         TouchDelegate touchDelegate = new TouchDelegate(parent);
197         touchDelegate.addTouchEndHandler(new TouchEndHandler() {
198             @Override
199             public void onTouchEnd(TouchEndEvent event) {
200                 Element target = event.getNativeEvent().getEventTarget().cast();
201                 if (stretchControl.isOrHasChild(target)) {
202                     if (!getState().isCollapsed) {
203                         unregisterSizeChangeListeners();
204                     }
205                     getRpcProxy(TextAreaStretcherServerRpc.class).toggle(textWidget.getOffsetWidth(), textWidget.getOffsetHeight());
206 
207                 }
208             }
209         });
210     }
211 
212     private void registerSizeChangeListeners() {
213         final LayoutManager lm = getParent().getLayoutManager();
214         final UIConnector ui = getConnection().getUIConnector();
215         getParent().addStateChangeHandler(textAreaSizeHandler);
216         lm.addElementResizeListener(ui.getWidget().getElement(), windowResizeListener);
217 
218         final ComponentConnector formConnector = Util.findConnectorFor(this.form);
219         if (formConnector != null) {
220             formConnector.getLayoutManager().addElementResizeListener(this.form.getElement(), formResizeListener);
221             formConnector.addStateChangeHandler(formStateChangeHandler);
222         }
223     }
224 
225     private void updateSize() {
226         if (!isFormVisible()) {
227             return;
228         }
229 
230         if (!getState().isCollapsed) {
231             stretchControl.replaceClassName("icon-open-fullscreen-2", "icon-close-fullscreen-2");
232             stretchControl.replaceClassName(COLLAPSED, STRETCHED);
233             form.asWidget().addStyleName("textarea-stretched");
234 
235             Style style = textWidget.getElement().getStyle();
236             style.setPosition(Style.Position.ABSOLUTE);
237             style.setZIndex(5);
238 
239             int top = calculateTextWidgetTop();
240             int left = calculateTextWidgetLeft();
241 
242             style.setLeft(left, Style.Unit.PX);
243             style.setTop(top, Style.Unit.PX);
244 
245             adjustTextAreaAndFormSizeToScreen();
246 
247             if (!isRichTextEditor) {
248                 setStretchControlPosition(top, left);
249             }
250 
251             hideOtherStretchers();
252         } else {
253             stretchControl.replaceClassName(STRETCHED, COLLAPSED);
254             stretchControl.replaceClassName("icon-close-fullscreen-2", "icon-open-fullscreen-2");
255             form.asWidget().removeStyleName(TEXTAREA_STRETCHED);
256             // Restore form height
257             form.setHeight(formHeight);
258             clearTraces();
259         }
260     }
261 
262     @Override
263     public void onUnregister() {
264         super.onUnregister();
265         clearTraces();
266     }
267 
268     private void setStretchControlPosition(int top, int left) {
269         stretchControl.getStyle().setTop(top + 5, Style.Unit.PX);
270         stretchControl.getStyle().setLeft(left + textWidget.getOffsetWidth() - stretchControl.getOffsetWidth() - 5, Style.Unit.PX);
271     }
272 
273     private int calculateTextWidgetTop() {
274         int top = getDialogHeaderOuterHeight() + getDialogDesErrorOuterHeight();
275         return isOverlay ? top + getDialogMargin()[TOP] + getDialogPadding()[TOP] : top + getDialogHeaderPadding()[TOP] + getDialogHeaderPadding()[BOTTOM];
276     }
277 
278     private int calculateTextWidgetLeft() {
279         return isOverlay ? getDialogPadding()[LEFT] : form.getAbsoluteLeft();
280     }
281 
282     private void hideOtherStretchers() {
283         JQueryWrapper.select("." + STRETCHER_BASE).setCss("display", "none");
284         this.stretchControl.getStyle().setDisplay(Style.Display.BLOCK);
285     }
286 
287     private void clearTraces() {
288         Style style = textWidget.getElement().getStyle();
289         style.clearLeft();
290         style.clearTop();
291         style.clearPosition();
292         style.clearZIndex();
293 
294         stretchControl.getStyle().clearTop();
295         stretchControl.getStyle().clearLeft();
296         stretchControl.getStyle().clearDisplay();
297 
298         JQueryWrapper.select("." + STRETCHER_BASE).setCss("display", "");
299     }
300 
301     private void unregisterSizeChangeListeners() {
302         final LayoutManager lm = getParent().getLayoutManager();
303         final UIConnector ui = getConnection().getUIConnector();
304         if (ui != null) {
305             getParent().removeStateChangeHandler(textAreaSizeHandler);
306             lm.removeElementResizeListener(ui.getWidget().getElement(), windowResizeListener);
307         }
308 
309         final ComponentConnector formConnector = Util.findConnectorFor(this.form);
310         if (formConnector != null) {
311             formConnector.getLayoutManager().removeElementResizeListener(this.form.getElement(), formResizeListener);
312             formConnector.removeStateChangeHandler(formStateChangeHandler);
313         }
314     }
315 
316     private void checkOverlay() {
317         Widget it = this.dialog.asWidget();
318         while (it != null && !isOverlay) {
319             it = it.getParent();
320             this.isOverlay = it instanceof OverlayWidget;
321         }
322     }
323 
324     private void initDialog() {
325         this.dialog = form.getParent();
326     }
327 
328     private void initFormView() {
329         Widget it = textWidget;
330         while (it != null && !(it instanceof FormView)) {
331             it = it.getParent();
332         }
333         this.form = (it instanceof FormView) ? it : null;
334         // Keep original form's height
335         this.formHeight = this.form.getElement().getStyle().getHeight();
336     }
337 
338     private void adjustTextAreaAndFormSizeToScreen() {
339         int formHeight = calculateFormHeight();
340         form.setHeight(formHeight + "px");
341         textWidget.setWidth((form.getOffsetWidth() + getDialogWrapperBorder()[RIGHT] + getDialogWrapperBorder()[LEFT]) + "px");
342         textWidget.setHeight(formHeight + "px");
343     }
344 
345     private int calculateFormHeight() {
346         int formHeight = dialog.getOffsetHeight();
347         formHeight -= getDialogHeaderOuterHeight() + getDialogFooterOuterHeight();
348         formHeight -= getDialogMargin()[TOP];
349         formHeight -= getDialogContentMargin()[TOP] - getDialogContentMargin()[BOTTOM];
350         formHeight -= getDialogDesErrorOuterHeight();
351 
352         return formHeight;
353     }
354 
355     private void doElementResize() {
356         if (isRichTextEditor) {
357             Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
358                 @Override
359                 public void execute() {
360                     updateSize();
361                 }
362             });
363         } else {
364             updateSize();
365         }
366     }
367 
368     private int getElementsOuterHeight(String... elementClasses) {
369         int elementsOuterHeight = 0;
370         for (String clazzName : elementClasses) {
371             Element element = JQueryWrapper.select(dialog.asWidget()).find(clazzName).get(0);
372             if (element != null) {
373                 int margin[] = new ComputedStyle(element).getMargin();
374                 elementsOuterHeight += element.getOffsetHeight() + margin[TOP] + margin[BOTTOM];
375             }
376         }
377         return elementsOuterHeight;
378     }
379 
380     private int[] getDialogHeaderPadding() {
381         if (dialogHeaderPadding == null) {
382             Element headerElement = JQueryWrapper.select(dialog.asWidget()).find(".dialog-header").get(0);
383             if (headerElement != null) {
384                 dialogHeaderPadding = new ComputedStyle(headerElement).getPadding();
385             } else {
386                 dialogHeaderPadding = new int[]{0, 0, 0, 0};
387             }
388         }
389 
390         return dialogHeaderPadding;
391     }
392 
393     private int[] getDialogContentMargin() {
394         if (dialogContentMargin == null) {
395             Element contentElement = JQueryWrapper.select(dialog.asWidget()).find(".dialog-content").get(0);
396             if (contentElement != null) {
397                 dialogContentMargin = new ComputedStyle(contentElement).getMargin();
398             } else {
399                 dialogContentMargin = new int[]{0, 0, 0, 0};
400             }
401         }
402 
403         return dialogContentMargin;
404     }
405 
406     private int[] getDialogPadding() {
407         if (dialogPadding == null) {
408             dialogPadding = getDialogComputedStyle().getPadding();
409         }
410 
411         return dialogPadding;
412     }
413 
414     private int[] getDialogMargin() {
415         if (dialogMargin == null) {
416             dialogMargin = getDialogComputedStyle().getMargin();
417         }
418 
419         return dialogMargin;
420     }
421 
422     private int[] getDialogWrapperBorder() {
423         if (dialogWrapperBorder == null) {
424             Element dialogWrapperElement = JQueryWrapper.select(dialog.asWidget()).find(".dialog-wrapper").get(0);
425             if (dialogWrapperElement != null) {
426                 dialogWrapperBorder = new ComputedStyle(dialogWrapperElement).getBorder();
427             } else {
428                 dialogWrapperBorder = new int[]{0, 0, 0, 0};
429             }
430         }
431 
432         return dialogWrapperBorder;
433     }
434 
435     private int getDialogFooterOuterHeight() {
436         if (dialogFooterOuterHeight == UNCALCULATED_SIZE) {
437             dialogFooterOuterHeight = getElementsOuterHeight(".dialog-footer");
438         }
439 
440         return dialogFooterOuterHeight;
441     }
442 
443     private int getDialogHeaderOuterHeight() {
444         if (dialogHeaderOuterHeight == UNCALCULATED_SIZE) {
445             dialogHeaderOuterHeight = getElementsOuterHeight(".dialog-header");
446         }
447 
448         return dialogHeaderOuterHeight;
449     }
450 
451     private int getDialogDesErrorOuterHeight() {
452         if (dialogDesErrorOuterHeight == UNCALCULATED_SIZE) {
453             dialogDesErrorOuterHeight = getElementsOuterHeight(".dialog-description", ".dialog-error");
454         }
455 
456         return dialogDesErrorOuterHeight;
457     }
458 
459     private ComputedStyle getDialogComputedStyle() {
460         if (dialogComputedStyle == null) {
461             dialogComputedStyle = new ComputedStyle(dialog.getElement());
462         }
463 
464         return dialogComputedStyle;
465     }
466 
467     private boolean isFormVisible() {
468         return JQueryWrapper.select(form.asWidget()).is(":visible");
469     }
470 
471     private class WindowResizeListener implements ElementResizeListener {
472         @Override
473         public void onElementResize(ElementResizeEvent e) {
474             doElementResize();
475         }
476     }
477 
478 }