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 final int escapeKeyCode = 27;
52
53 private boolean hideOnBlur = true;
54
55 private HandlerManager handlerManager;
56 private ApplicationConnection connection;
57
58
59
60
61
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
111 if (shouldHideBubble()) {
112 closeBubble();
113 }
114
115 if (event.getTypeInt() == Event.ONMOUSEWHEEL || event.getTypeInt() == Event.ONMOUSEDOWN || event.getNativeEvent().getKeyCode() == escapeKeyCode) {
116 closeBubble();
117 }
118
119
120
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
205
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
225
226
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
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);
306 super.ac = connection;
307 setStylePrimaryName(CLASSNAME + "-bubble");
308 setZIndex(Z_INDEX_BASE);
309 helpHtml = new HTML();
310 setWidget(helpHtml);
311
312 super.hide();
313 }
314
315 public void updateStyleNames(String styleNames) {
316 StringBuffer styleBuf = new StringBuffer();
317
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
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
377 if (getLeft(Placement.RIGHT) + getOffsetWidth() > Document.get().getClientWidth()) {
378 return Placement.LEFT;
379 }
380
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
421
422 Style style = getElement().getStyle();
423 style.clearMarginLeft();
424 style.clearMarginTop();
425 }
426 }
427 }