View Javadoc
1   package org.vaadin.jonatan.contexthelp.widgetset.client.ui;
2   
3   import com.google.gwt.core.client.Scheduler;
4   import com.google.gwt.dom.client.Document;
5   import com.google.gwt.dom.client.Element;
6   import com.google.gwt.dom.client.EventTarget;
7   import com.google.gwt.dom.client.Node;
8   import com.google.gwt.dom.client.NodeList;
9   import com.google.gwt.dom.client.Style;
10  import com.google.gwt.dom.client.Style.Unit;
11  import com.google.gwt.event.dom.client.KeyCodes;
12  import com.google.gwt.event.shared.GwtEvent;
13  import com.google.gwt.event.shared.HandlerManager;
14  import com.google.gwt.event.shared.HandlerRegistration;
15  import com.google.gwt.event.shared.HasHandlers;
16  import com.google.gwt.user.client.DOM;
17  import com.google.gwt.user.client.Event;
18  import com.google.gwt.user.client.Event.NativePreviewEvent;
19  import com.google.gwt.user.client.Event.NativePreviewHandler;
20  import com.google.gwt.user.client.Timer;
21  import com.google.gwt.user.client.ui.HTML;
22  import com.vaadin.client.ApplicationConnection;
23  import com.vaadin.client.Util;
24  import com.vaadin.client.ui.VOverlay;
25  import org.vaadin.jonatan.contexthelp.widgetset.client.ui.ContextHelpEvent.BubbleHiddenEvent;
26  import org.vaadin.jonatan.contexthelp.widgetset.client.ui.ContextHelpEvent.BubbleHiddenHandler;
27  import org.vaadin.jonatan.contexthelp.widgetset.client.ui.ContextHelpEvent.BubbleMovedEvent;
28  import org.vaadin.jonatan.contexthelp.widgetset.client.ui.ContextHelpEvent.BubbleMovedHandler;
29  import org.vaadin.jonatan.contexthelp.widgetset.client.ui.ContextHelpEvent.BubbleShownEvent;
30  import org.vaadin.jonatan.contexthelp.widgetset.client.ui.ContextHelpEvent.BubbleShownHandler;
31  
32  public class VContextHelp implements NativePreviewHandler, HasHandlers {
33  
34      private static final int SCROLL_UPDATER_INTERVAL = 100;
35  
36      /**
37       * Set the CSS class name to allow styling.
38       */
39      public static final String CLASSNAME = "v-contexthelp";
40  
41      private boolean followFocus = false;
42  
43      private boolean hidden = true;
44  
45      private final HelpBubble bubble;
46  
47      private final Timer scrollUpdater;
48  
49      private int helpKeyCode = 112; // F1 by default
50  
51      private boolean hideOnBlur = true;
52  
53      private HandlerManager handlerManager;
54      private ApplicationConnection connection;
55  
56  
57      /**
58       * The constructor should first call super() to initialize the component and
59       * then handle any initialization relevant to Vaadin.
60       */
61      public VContextHelp(ApplicationConnection connection) {
62          super();
63          this.connection = connection;
64  
65          handlerManager = new HandlerManager(this);
66  
67          Event.addNativePreviewHandler(this);
68          suppressHelpForIE();
69  
70          bubble = new HelpBubble();
71          scrollUpdater = new Timer() {
72              public void run() {
73                  bubble.updatePositionIfNeeded();
74              }
75          };
76      }
77  
78      public HandlerRegistration addBubbleShownHandler(BubbleShownHandler handler) {
79          return handlerManager.addHandler(BubbleShownEvent.TYPE, handler);
80      }
81  
82      public HandlerRegistration addBubbleHiddenHandler(BubbleHiddenHandler handler) {
83          return handlerManager.addHandler(BubbleHiddenEvent.TYPE, handler);
84      }
85  
86      public HandlerRegistration addBubbleMovedHandler(BubbleMovedHandler handler) {
87          return handlerManager.addHandler(BubbleMovedEvent.TYPE, handler);
88      }
89  
90      private void fireBubbleShownEvent(String componentId, String helpHtml) {
91          handlerManager.fireEvent(new BubbleShownEvent(componentId, helpHtml));
92      }
93  
94      private void fireBubbleHiddenEvent() {
95          handlerManager.fireEvent(new BubbleHiddenEvent());
96      }
97  
98      private void fireBubbleMovedEvent(String componentId) {
99          handlerManager.fireEvent(new BubbleMovedEvent(componentId));
100     }
101 
102     @Override
103     public void fireEvent(GwtEvent<?> event) {
104         handlerManager.fireEvent(event);
105     }
106 
107     public void onPreviewNativeEvent(NativePreviewEvent event) {
108         // Hide if the element has disappeared (views changed)
109         if (shouldHideBubble()) {
110             closeBubble();
111         }
112 //        if (!isAttached()) {
113 //            return;
114 //        }
115         if (isFollowFocus()) {
116             if (isFocusMovingEvent(event)) {
117                 openBubble();
118             }
119         } else {
120             if (isHelpKeyPressed(event)) {
121                 openBubble();
122                 event.cancel();
123             } else if (shouldHideOnEvent(event)) {
124                 closeBubble();
125             }
126         }
127     }
128 
129     private boolean shouldHideOnEvent(NativePreviewEvent event) {
130         Element targetElement = null;
131         EventTarget target = event.getNativeEvent().getEventTarget();
132         if (Element.is(target)) {
133             targetElement = Element.as(target);
134         }
135         return hideOnBlur && bubble.isShowing()
136                 && targetElement != null
137                 && !bubble.getElement().isOrHasChild(targetElement)
138                 && isFocusMovingEvent(event)
139                 && !bubble.helpElement.isOrHasChild(targetElement);
140     }
141 
142     private boolean shouldHideBubble() {
143         if (!hideOnBlur && bubble != null && bubble.helpElement != null) {
144             return bubble.helpElement.getAbsoluteLeft() < 0
145                     || bubble.helpElement.getAbsoluteTop() < 0
146                     || !Document.get().getBody().isOrHasChild(bubble.helpElement)
147                     || "hidden".equalsIgnoreCase(bubble.helpElement.getStyle().getVisibility())
148                     || "none".equalsIgnoreCase(bubble.helpElement.getStyle().getDisplay());
149         }
150         return false;
151     }
152 
153     private void openBubble() {
154         scrollUpdater.cancel();
155         scrollUpdater.scheduleRepeating(SCROLL_UPDATER_INTERVAL);
156         setHidden(false);
157         fireBubbleMovedEvent(getHelpElement().getId());
158     }
159 
160     private void closeBubble() {
161         scrollUpdater.cancel();
162         setHidden(true);
163         bubble.hide();
164     }
165 
166     public void showHelpBubble(String componentId, String helpText, Placement placement) {
167         bubble.showHelpBubble(componentId, helpText, placement);
168     }
169 
170     public void hideHelpBubble() {
171         closeBubble();
172     }
173 
174     private boolean isFocusMovingEvent(NativePreviewEvent event) {
175         return isMouseUp(event) || isTabUp(event);
176     }
177 
178     private boolean isMouseUp(NativePreviewEvent event) {
179         return event.getTypeInt() == Event.ONMOUSEUP;
180     }
181 
182     private boolean isTabUp(NativePreviewEvent event) {
183         return (event.getTypeInt() == Event.ONKEYUP
184                 && event.getNativeEvent().getKeyCode() == KeyCodes.KEY_TAB);
185     }
186 
187     private boolean isHelpKeyPressed(NativePreviewEvent event) {
188         return event.getTypeInt() == Event.ONKEYDOWN && event.getNativeEvent().getKeyCode() == helpKeyCode;
189     }
190 
191     private boolean isKeyDownOrClick(NativePreviewEvent event) {
192         return event.getTypeInt() == Event.ONKEYDOWN || event.getTypeInt() == Event.ONCLICK;
193     }
194 
195     public native void suppressHelpForIE()
196     /*-{
197         $doc.onhelp = function () {
198             return false;
199         }
200     }-*/;
201 
202     private Element findHelpElement(String id) {
203         if (id == null || id.length() == 0) {
204             return null;
205         }
206         Element helpElement = DOM.getElementById(id);
207         if (helpElement != null) {
208             Element contentElement = findContentElement(helpElement);
209             if (contentElement != null) {
210                 return contentElement;
211             }
212         }
213         return helpElement;
214     }
215 
216     private Element findContentElement(Element helpElement) {
217         // check whether helpElement has a child element with
218         // class="v-XXXX-content" and if this is the case, use the content
219         // element for the position calculations below.
220         NodeList<Node> children = helpElement.getChildNodes();
221         for (int i = 0; i < children.getLength(); i++) {
222             if (children.getItem(i).getNodeType() == Node.ELEMENT_NODE) {
223                 Element e = Element.as(children.getItem(i));
224                 if (e.getClassName().contains("content")) {
225                     return e;
226                 }
227             }
228         }
229         return null;
230     }
231 
232     public static native Element getFocusedElement()
233     /*-{
234         return $doc.activeElement;
235     }-*/;
236 
237     private Element getHelpElement() {
238         Element focused = getFocusedElement();
239         return findFirstElementInHierarchyWithId(focused);
240     }
241 
242     private static Element findFirstElementInHierarchyWithId(Element focused) {
243         Element elementWithId = focused;
244         while (("".equals(elementWithId.getId()) || elementWithId.getId() == null
245                 || elementWithId.getId().startsWith("gwt-uid"))
246                 && elementWithId.getParentElement() != null) {
247             elementWithId = elementWithId.getParentElement();
248         }
249         return elementWithId;
250     }
251 
252 
253     public boolean isHidden() {
254         return hidden;
255     }
256 
257     public void setHidden(boolean hidden) {
258         this.hidden = hidden;
259     }
260 
261     public boolean isFollowFocus() {
262         return followFocus;
263     }
264 
265     public void setFollowFocus(boolean followFocus) {
266         this.followFocus = followFocus;
267     }
268 
269     public boolean isHideOnBlur() {
270         return hideOnBlur;
271     }
272 
273     public void setHideOnBlur(boolean hideOnBlur) {
274         this.hideOnBlur = hideOnBlur;
275     }
276 
277     public int getHelpKeyCode() {
278         return helpKeyCode;
279     }
280 
281     public void setHelpKeyCode(int helpKeyCode) {
282         this.helpKeyCode = helpKeyCode;
283     }
284 
285     private class HelpBubble extends VOverlay {
286         private static final int Z_INDEX_BASE = 90000;
287 
288         private final HTML helpHtml;
289 
290         private Element helpElement;
291 
292         private int elementTop;
293         private int elementLeft;
294 
295         private Placement placement;
296 
297         public HelpBubble() {
298             super(false, false); // autoHide, modal
299             super.ac = connection;
300             setStylePrimaryName(CLASSNAME + "-bubble");
301             setZIndex(Z_INDEX_BASE);
302             helpHtml = new HTML();
303             setWidget(helpHtml);
304             // Make sure we are hidden (bypassing event triggering)
305             super.hide();
306         }
307 
308         public void updateStyleNames(String styleNames) {
309             StringBuffer styleBuf = new StringBuffer();
310             // Copied from ApplicationConnection.updateComponent
311             if (styleNames != null && !"".equals(styleNames)) {
312                 final String[] styles = styleNames.split(" ");
313                 for (int i = 0; i < styles.length; i++) {
314                     styleBuf.append(" ");
315                     styleBuf.append(CLASSNAME + "-bubble");
316                     styleBuf.append("-");
317                     styleBuf.append(styles[i]);
318                     styleBuf.append(" ");
319                     styleBuf.append(styles[i]);
320                 }
321                 addStyleName(styleBuf.toString());
322             }
323         }
324 
325         public void setHelpText(String helpText) {
326             helpHtml.setHTML(helpText);
327             helpHtml.setStyleName("helpText");
328         }
329 
330         public void showHelpBubble(String componentId, String helpText, Placement placement) {
331             this.placement = placement;
332             helpElement = findHelpElement(componentId);
333             if (helpElement != null) {
334                 show();
335                 setHelpText(helpText);
336                 calculateAndSetPopupPosition();
337             }
338             fireBubbleShownEvent(componentId, helpText);
339         }
340 
341         @Override
342         public void hide() {
343             super.hide();
344             fireBubbleHiddenEvent();
345         }
346 
347         public void updatePositionIfNeeded() {
348             if (isAttached() && !hidden && helpElement != null) {
349                 if (elementLeft != helpElement.getAbsoluteLeft() || elementTop != helpElement.getAbsoluteTop()) {
350                     calculateAndSetPopupPosition();
351                 }
352             }
353         }
354 
355         private void calculateAndSetPopupPosition() {
356             // Save the current position for checking whether the element has moved == scrolled
357             elementLeft = helpElement.getAbsoluteLeft();
358             elementTop = helpElement.getAbsoluteTop();
359 
360             Placement finalPlacement = placement;
361             if (placement == Placement.AUTO) {
362                 finalPlacement = findDefaultPlacement();
363             }
364             updatePopupStyleForPlacement(finalPlacement);
365             setPopupPosition(Math.max(0, getLeft(finalPlacement)), Math.max(0, getTop(finalPlacement)));
366         }
367 
368         private Placement findDefaultPlacement() {
369             // Would the popup go too far to the right?
370             if (Document.get().getClientWidth() < 1312) {
371                 return Placement.LEFT;
372             }
373             // By default, place the popup to the right of the field
374             return Placement.RIGHT;
375         }
376 
377         private void updatePopupStyleForPlacement(Placement placement) {
378             for (Placement p : Placement.values()) {
379                 removeStyleName(p.name().toLowerCase());
380             }
381             addStyleName(placement.name().toLowerCase());
382         }
383 
384         private int getLeft(Placement placement) {
385             switch (placement) {
386                 case RIGHT:
387                     return helpElement.getAbsoluteLeft() + (helpElement.getOffsetWidth() + 12);
388                 case LEFT:
389                     return helpElement.getAbsoluteLeft() - (bubble.getOffsetWidth() + 12);
390                 case ABOVE:
391                 case BELOW:
392                     return helpElement.getAbsoluteLeft() + helpElement.getOffsetWidth() / 2 - bubble.getOffsetWidth() / 2;
393             }
394             return 0;
395         }
396 
397         public int getTop(Placement placement) {
398             switch (placement) {
399                 case RIGHT:
400                 case LEFT:
401                     return helpElement.getAbsoluteTop() + helpElement.getOffsetHeight() / 2 - bubble.getOffsetHeight() / 2;
402                 case ABOVE:
403                     return helpElement.getAbsoluteTop() - bubble.getOffsetHeight();
404                 case BELOW:
405                     return helpElement.getAbsoluteTop() + helpElement.getOffsetHeight();
406             }
407             return 0;
408         }
409 
410         @Override
411         public void setPopupPosition(int left, int top) {
412             super.setPopupPosition(left, top);
413             // Remove the margin styles, that VOverlay forces on the element,
414             // in order to be able to move the entire bubble with margins.
415             Style style = getElement().getStyle();
416             style.clearMarginLeft();
417             style.clearMarginTop();
418         }
419     }
420 }