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