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