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
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
198
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
218
219
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
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);
299 super.ac = connection;
300 setStylePrimaryName(CLASSNAME + "-bubble");
301 setZIndex(Z_INDEX_BASE);
302 helpHtml = new HTML();
303 setWidget(helpHtml);
304
305 super.hide();
306 }
307
308 public void updateStyleNames(String styleNames) {
309 StringBuffer styleBuf = new StringBuffer();
310
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
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
370 if (Document.get().getClientWidth() < 1312) {
371 return Placement.LEFT;
372 }
373
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
414
415 Style style = getElement().getStyle();
416 style.clearMarginLeft();
417 style.clearMarginTop();
418 }
419 }
420 }