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
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;
50
51 private boolean hideOnBlur = true;
52
53 private HandlerManager handlerManager;
54 private ApplicationConnection connection;
55
56
57
58
59
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
109 if (shouldHideBubble()) {
110 closeBubble();
111 }
112
113 if (event.getTypeInt() == Event.ONMOUSEWHEEL || event.getTypeInt() == Event.ONMOUSEDOWN) {
114 closeBubble();
115 }
116
117
118
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
203
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
223
224
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
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);
304 super.ac = connection;
305 setStylePrimaryName(CLASSNAME + "-bubble");
306 setZIndex(Z_INDEX_BASE);
307 helpHtml = new HTML();
308 setWidget(helpHtml);
309
310 super.hide();
311 }
312
313 public void updateStyleNames(String styleNames) {
314 StringBuffer styleBuf = new StringBuffer();
315
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
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
375 if (Document.get().getClientWidth() < 1312) {
376 return Placement.LEFT;
377 }
378
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
419
420 Style style = getElement().getStyle();
421 style.clearMarginLeft();
422 style.clearMarginTop();
423 }
424 }
425 }