View Javadoc
1   /**
2    * This file Copyright (c) 2013-2018 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.connector.FormConnector;
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.user.client.DOM;
45  import com.google.gwt.user.client.ui.Widget;
46  import com.googlecode.mgwt.dom.client.event.touch.TouchEndEvent;
47  import com.googlecode.mgwt.dom.client.event.touch.TouchEndHandler;
48  import com.googlecode.mgwt.ui.client.widget.touch.TouchDelegate;
49  import com.vaadin.client.ComponentConnector;
50  import com.vaadin.client.ComputedStyle;
51  import com.vaadin.client.LayoutManager;
52  import com.vaadin.client.ServerConnector;
53  import com.vaadin.client.Util;
54  import com.vaadin.client.communication.StateChangeEvent;
55  import com.vaadin.client.extensions.AbstractExtensionConnector;
56  import com.vaadin.client.ui.layout.ElementResizeEvent;
57  import com.vaadin.client.ui.layout.ElementResizeListener;
58  import com.vaadin.client.ui.ui.UIConnector;
59  import com.vaadin.shared.ui.Connect;
60  
61  /**
62   * Client-side connector for {@link info.magnolia.ui.vaadin.richtext.TextAreaStretcher}.
63   */
64  @Connect(TextAreaStretcher.class)
65  public class TextAreaStretcherConnector extends AbstractExtensionConnector {
66  
67      public static final String STRETCHER_BASE = "textarea-stretcher";
68      public static final String STRETCHED = "stretched";
69      public static final String COLLAPSED = "collapsed";
70      public static final String CKEDITOR_TOOLBOX = ".cke_top";
71      public static final String TEXTAREA_STRETCHED = "textarea-stretched";
72      public static final String RICH_TEXT_STYLE_NAME = "rich-text";
73      public static final String SIMPLE_STYLE_NAME = "simple";
74      public static final int DELAY_MS = 500;
75  
76      private static final int UNCALCULATED_SIZE = -1;
77      private static final int TOP = 0;
78      private static final int RIGHT = 1;
79      private static final int BOTTOM = 2;
80      private static final int LEFT = 3;
81  
82      private Widget form;
83      private Widget dialog;
84      private Widget textWidget;
85      private Element stretchControl = DOM.createDiv();
86      private ComputedStyle dialogComputedStyle;
87  
88      // Border, padding, margin property for dialog-wrapper, dialog-header, dialog-content class, contain values for top, right, bottom, left.
89      private int[] dialogWrapperBorder;
90      private int[] dialogHeaderPadding;
91      private int[] dialogContentMargin;
92      private int[] dialogMargin;
93      private int[] dialogPadding;
94      private String formHeight;
95      private int dialogHeaderOuterHeight = UNCALCULATED_SIZE;
96      private int dialogFooterOuterHeight = UNCALCULATED_SIZE;
97      private int dialogDesErrorOuterHeight = UNCALCULATED_SIZE;
98      private boolean isOverlay = false;
99      private boolean isRichTextEditor = false;
100 
101     private StateChangeEvent.StateChangeHandler textAreaSizeHandler = stateChangeEvent -> {
102         if (isFormVisible()) {
103             adjustTextAreaAndFormSizeToScreen();
104         }
105     };
106 
107     private WindowResizeListener windowResizeListener = new WindowResizeListener();
108 
109     private ElementResizeListener formResizeListener = e -> doElementResize();
110 
111     private StateChangeEvent.StateChangeHandler formStateChangeHandler = new StateChangeEvent.StateChangeHandler() {
112         @Override
113         public void onStateChanged(StateChangeEvent stateChangeEvent) {
114             if (stateChangeEvent.hasPropertyChanged("descriptionsVisible") || stateChangeEvent.hasPropertyChanged("errorAmount")) {
115                 if (!getState().isCollapsed) {
116                     unregisterSizeChangeListeners();
117                     getRpcProxy(TextAreaStretcherServerRpc.class).toggle(textWidget.getOffsetWidth(), textWidget.getOffsetHeight());
118                 }
119             }
120         }
121     };
122 
123     @Override
124     public void onStateChanged(StateChangeEvent stateChangeEvent) {
125         super.onStateChanged(stateChangeEvent);
126         if (stateChangeEvent.hasPropertyChanged("isCollapsed")) {
127             updateSize();
128             if (!getState().isCollapsed) {
129                 registerSizeChangeListeners();
130             }
131         }
132     }
133 
134     @Override
135     public ComponentConnector getParent() {
136         return (ComponentConnector) super.getParent();
137     }
138 
139     @Override
140     protected void extend(ServerConnector target) {
141         this.textWidget = ((ComponentConnector) target).getWidget();
142         this.isRichTextEditor = target instanceof RichTextConnector;
143         this.stretchControl.setClassName(STRETCHER_BASE);
144 
145         ServerConnector potentialFormParent = getParent();
146         while (potentialFormParent != null && !(potentialFormParent instanceof FormConnector)) {
147             potentialFormParent = potentialFormParent.getParent();
148         }
149 
150         if (potentialFormParent == null) {
151             return;
152         }
153 
154         final ComponentConnector formConnector = (ComponentConnector) potentialFormParent;
155         textWidget.addAttachHandler(attachEvent -> {
156             if (attachEvent.isAttached()) {
157                 initFormView(formConnector.getWidget());
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     @Override
187     public TextAreaStretcherState getState() {
188         return (TextAreaStretcherState) super.getState();
189     }
190 
191     private void appendStretcher(Element rootElement) {
192         rootElement.getStyle().setPosition(Style.Position.RELATIVE);
193         rootElement.getParentElement().insertAfter(stretchControl, rootElement);
194         Widget parent = textWidget.getParent();
195         TouchDelegate touchDelegate = new TouchDelegate(parent);
196         touchDelegate.addTouchEndHandler(new TouchEndHandler() {
197             @Override
198             public void onTouchEnd(TouchEndEvent event) {
199                 Element target = event.getNativeEvent().getEventTarget().cast();
200                 if (stretchControl.isOrHasChild(target)) {
201                     if (!getState().isCollapsed) {
202                         unregisterSizeChangeListeners();
203                     }
204                     getRpcProxy(TextAreaStretcherServerRpc.class).toggle(textWidget.getOffsetWidth(), textWidget.getOffsetHeight());
205 
206                 }
207             }
208         });
209     }
210 
211     private void registerSizeChangeListeners() {
212         final LayoutManager lm = getParent().getLayoutManager();
213         final UIConnector ui = getConnection().getUIConnector();
214         getParent().addStateChangeHandler(textAreaSizeHandler);
215         lm.addElementResizeListener(ui.getWidget().getElement(), windowResizeListener);
216 
217         final ComponentConnector formConnector = Util.findConnectorFor(this.form);
218         if (formConnector != null) {
219             formConnector.getLayoutManager().addElementResizeListener(this.form.getElement(), formResizeListener);
220             formConnector.addStateChangeHandler(formStateChangeHandler);
221         }
222     }
223 
224     private void updateSize() {
225         if (!isFormVisible()) {
226             return;
227         }
228 
229         if (!getState().isCollapsed) {
230             stretchControl.replaceClassName("icon-open-fullscreen-2", "icon-close-fullscreen-2");
231             stretchControl.replaceClassName(COLLAPSED, STRETCHED);
232             form.asWidget().addStyleName("textarea-stretched");
233 
234             Style style = textWidget.getElement().getStyle();
235             style.setPosition(Style.Position.ABSOLUTE);
236             style.setZIndex(5);
237 
238             int top = calculateTextWidgetTop();
239             int left = calculateTextWidgetLeft();
240 
241             style.setLeft(left, Style.Unit.PX);
242             style.setTop(top, Style.Unit.PX);
243 
244             adjustTextAreaAndFormSizeToScreen();
245 
246             if (!isRichTextEditor) {
247                 setStretchControlPosition(top, left);
248             }
249 
250             hideOtherStretchers();
251         } else {
252             stretchControl.replaceClassName(STRETCHED, COLLAPSED);
253             stretchControl.replaceClassName("icon-close-fullscreen-2", "icon-open-fullscreen-2");
254             form.asWidget().removeStyleName(TEXTAREA_STRETCHED);
255             // Restore form height
256             form.setHeight(formHeight);
257             clearTraces();
258         }
259     }
260 
261     @Override
262     public void onUnregister() {
263         super.onUnregister();
264         clearTraces();
265     }
266 
267     private void setStretchControlPosition(int top, int left) {
268         stretchControl.getStyle().setTop(top + 5, Style.Unit.PX);
269         stretchControl.getStyle().setLeft(left + textWidget.getOffsetWidth() - stretchControl.getOffsetWidth() - 5, Style.Unit.PX);
270     }
271 
272     private int calculateTextWidgetTop() {
273         int top = getDialogHeaderOuterHeight() + getDialogDesErrorOuterHeight();
274         return isOverlay ? top + getDialogMargin()[TOP] + getDialogPadding()[TOP] : top + getDialogHeaderPadding()[TOP] + getDialogHeaderPadding()[BOTTOM];
275     }
276 
277     private int calculateTextWidgetLeft() {
278         return isOverlay ? getDialogPadding()[LEFT] : form.getAbsoluteLeft();
279     }
280 
281     private void hideOtherStretchers() {
282         JQueryWrapper.select("." + STRETCHER_BASE).setCss("display", "none");
283         this.stretchControl.getStyle().setDisplay(Style.Display.BLOCK);
284     }
285 
286     private void clearTraces() {
287         Style style = textWidget.getElement().getStyle();
288         style.clearLeft();
289         style.clearTop();
290         style.clearPosition();
291         style.clearZIndex();
292 
293         stretchControl.getStyle().clearTop();
294         stretchControl.getStyle().clearLeft();
295         stretchControl.getStyle().clearDisplay();
296 
297         JQueryWrapper.select("." + STRETCHER_BASE).setCss("display", "");
298     }
299 
300     private void unregisterSizeChangeListeners() {
301         final LayoutManager lm = getParent().getLayoutManager();
302         final UIConnector ui = getConnection().getUIConnector();
303         if (ui != null) {
304             getParent().removeStateChangeHandler(textAreaSizeHandler);
305             lm.removeElementResizeListener(ui.getWidget().getElement(), windowResizeListener);
306         }
307 
308         final ComponentConnector formConnector = Util.findConnectorFor(this.form);
309         if (formConnector != null) {
310             formConnector.getLayoutManager().removeElementResizeListener(this.form.getElement(), formResizeListener);
311             formConnector.removeStateChangeHandler(formStateChangeHandler);
312         }
313     }
314 
315     private void checkOverlay() {
316         Widget it = this.dialog.asWidget();
317         while (it != null && !isOverlay) {
318             it = it.getParent();
319             this.isOverlay = it instanceof OverlayWidget;
320         }
321     }
322 
323     private void initDialog() {
324         this.dialog = form.getParent();
325     }
326 
327     private void initFormView(Widget formWidget) {
328         this.form = formWidget;
329         // Keep original form's height
330         this.formHeight = this.form.getElement().getStyle().getHeight();
331     }
332 
333     private void adjustTextAreaAndFormSizeToScreen() {
334         int formHeight = calculateFormHeight();
335         form.setHeight(formHeight + "px");
336         textWidget.setWidth((form.getOffsetWidth() + getDialogWrapperBorder()[RIGHT] + getDialogWrapperBorder()[LEFT]) + "px");
337         textWidget.setHeight(formHeight + "px");
338     }
339 
340     private int calculateFormHeight() {
341         int formHeight = dialog.getOffsetHeight();
342         formHeight -= getDialogHeaderOuterHeight() + getDialogFooterOuterHeight();
343         formHeight -= getDialogMargin()[TOP];
344         formHeight -= getDialogContentMargin()[TOP] - getDialogContentMargin()[BOTTOM];
345         formHeight -= getDialogDesErrorOuterHeight();
346 
347         return formHeight;
348     }
349 
350     private void doElementResize() {
351         if (isRichTextEditor) {
352             Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
353                 @Override
354                 public void execute() {
355                     updateSize();
356                 }
357             });
358         } else {
359             updateSize();
360         }
361     }
362 
363     private int getElementsOuterHeight(String... elementClasses) {
364         int elementsOuterHeight = 0;
365         for (String clazzName : elementClasses) {
366             Element element = JQueryWrapper.select(dialog.asWidget()).find(clazzName).get(0);
367             if (element != null) {
368                 int margin[] = new ComputedStyle(element).getMargin();
369                 elementsOuterHeight += element.getOffsetHeight() + margin[TOP] + margin[BOTTOM];
370             }
371         }
372         return elementsOuterHeight;
373     }
374 
375     private int[] getDialogHeaderPadding() {
376         if (dialogHeaderPadding == null) {
377             Element headerElement = JQueryWrapper.select(dialog.asWidget()).find(".dialog-header").get(0);
378             if (headerElement != null) {
379                 dialogHeaderPadding = new ComputedStyle(headerElement).getPadding();
380             } else {
381                 dialogHeaderPadding = new int[]{0, 0, 0, 0};
382             }
383         }
384 
385         return dialogHeaderPadding;
386     }
387 
388     private int[] getDialogContentMargin() {
389         if (dialogContentMargin == null) {
390             Element contentElement = JQueryWrapper.select(dialog.asWidget()).find(".dialog-content").get(0);
391             if (contentElement != null) {
392                 dialogContentMargin = new ComputedStyle(contentElement).getMargin();
393             } else {
394                 dialogContentMargin = new int[]{0, 0, 0, 0};
395             }
396         }
397 
398         return dialogContentMargin;
399     }
400 
401     private int[] getDialogPadding() {
402         if (dialogPadding == null) {
403             dialogPadding = getDialogComputedStyle().getPadding();
404         }
405 
406         return dialogPadding;
407     }
408 
409     private int[] getDialogMargin() {
410         if (dialogMargin == null) {
411             dialogMargin = getDialogComputedStyle().getMargin();
412         }
413 
414         return dialogMargin;
415     }
416 
417     private int[] getDialogWrapperBorder() {
418         if (dialogWrapperBorder == null) {
419             Element dialogWrapperElement = JQueryWrapper.select(dialog.asWidget()).find(".dialog-wrapper").get(0);
420             if (dialogWrapperElement != null) {
421                 dialogWrapperBorder = new ComputedStyle(dialogWrapperElement).getBorder();
422             } else {
423                 dialogWrapperBorder = new int[]{0, 0, 0, 0};
424             }
425         }
426 
427         return dialogWrapperBorder;
428     }
429 
430     private int getDialogFooterOuterHeight() {
431         if (dialogFooterOuterHeight == UNCALCULATED_SIZE) {
432             dialogFooterOuterHeight = getElementsOuterHeight(".dialog-footer");
433         }
434 
435         return dialogFooterOuterHeight;
436     }
437 
438     private int getDialogHeaderOuterHeight() {
439         if (dialogHeaderOuterHeight == UNCALCULATED_SIZE) {
440             dialogHeaderOuterHeight = getElementsOuterHeight(".dialog-header");
441         }
442 
443         return dialogHeaderOuterHeight;
444     }
445 
446     private int getDialogDesErrorOuterHeight() {
447         if (dialogDesErrorOuterHeight == UNCALCULATED_SIZE) {
448             dialogDesErrorOuterHeight = getElementsOuterHeight(".dialog-description", ".dialog-error");
449         }
450 
451         return dialogDesErrorOuterHeight;
452     }
453 
454     private ComputedStyle getDialogComputedStyle() {
455         if (dialogComputedStyle == null) {
456             dialogComputedStyle = new ComputedStyle(dialog.getElement());
457         }
458 
459         return dialogComputedStyle;
460     }
461 
462     private boolean isFormVisible() {
463         return JQueryWrapper.select(form.asWidget()).is(":visible");
464     }
465 
466     private class WindowResizeListener implements ElementResizeListener {
467         @Override
468         public void onElementResize(ElementResizeEvent e) {
469             doElementResize();
470         }
471     }
472 
473 }