package org.ovirt.engine.ui.common.utils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.gwtbootstrap3.client.ui.constants.Placement; import org.ovirt.engine.ui.common.widget.MenuBar; import org.ovirt.engine.ui.common.widget.tooltip.TooltipConfig; import org.ovirt.engine.ui.uicompat.external.StringUtils; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.RepeatingCommand; import com.google.gwt.dom.client.BrowserEvents; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.event.dom.client.MouseDownEvent; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.ui.RootPanel; /** * jQuery/Bootstrap tooltip utility methods. * * @see org.ovirt.engine.ui.common.widget.tooltip.WidgetTooltip */ public final class ElementTooltipUtils { public static class CellWidgetTooltipReaper implements RepeatingCommand { private static final int REAPER_PERIOD = 10000; // ms public CellWidgetTooltipReaper() { Scheduler.get().scheduleFixedDelay(this, REAPER_PERIOD); } @Override public boolean execute() { ElementTooltipUtils.reapCellWidgetTooltips(); return true; } } public static class TooltipHideOnRootPanelClick { public TooltipHideOnRootPanelClick() { RootPanel.get().addDomHandler(event -> ElementTooltipUtils.hideAllTooltips(), MouseDownEvent.getType()); } } // Reaper lists to track elements on which the tooltip must be destroyed. // These reaper lists basically represent different categories of non-singleton // DOM elements that require manual tooltip destruction to prevent memory leaks. private static final List<Element> cellWidgetElementReapList = new ArrayList<>(); private static final List<Element> popupContentElementReapList = new ArrayList<>(); /** * Apply tooltip on the given element. Uses default config. */ public static void setTooltipOnElement(Element e, SafeHtml tooltip) { setTooltipOnElement(e, tooltip, new TooltipConfig()); } /** * Apply tooltip on the given element. Uses default config. */ public static void setTooltipOnElement(Element e, SafeHtml tooltip, Placement placement) { setTooltipOnElement(e, tooltip, new TooltipConfig().setPlacement(placement)); } /** * Apply tooltip on the given element. */ public static void setTooltipOnElement(Element e, SafeHtml tooltip, TooltipConfig config) { // Try not to set (destroy & create) the same tooltip again. if (sameTooltipOnElement(e, tooltip)) { return; } // Destroy existing tooltip first. destroyTooltip(e); // Create new tooltip. String tooltipHtmlString = getTooltipHtmlString(tooltip); if (!tooltipHtmlString.isEmpty()) { createTooltipImpl(e, tooltipHtmlString, config.getPlacement().getCssName(), config.getTooltipTemplate(), config.isForceShow()); // Update reaper lists. if (config.isForCellWidgetElement()) { cellWidgetElementReapList.add(e); } else if (isPopupContentElement(e)) { popupContentElementReapList.add(e); } } } private static boolean sameTooltipOnElement(Element e, SafeHtml maybeNewTooltip) { String existingTooltipHtmlString = getExistingTooltipHtml(e); String maybeNewTooltipHtmlString = getTooltipHtmlString(maybeNewTooltip); return maybeNewTooltipHtmlString.equals(existingTooltipHtmlString); } private static String getTooltipHtmlString(SafeHtml tooltip) { return (tooltip != null) ? tooltip.asString() : ""; } private static native boolean isPopupContentElement(Element e) /*-{ var popupContentSelector = '.' + @org.ovirt.engine.ui.common.view.AbstractPopupView::POPUP_CONTENT_STYLE_NAME; return $wnd.jQuery(e).closest(popupContentSelector).length > 0; }-*/; /** * Returns tooltip HTML string or empty string if the element has no tooltip attached. */ private static native String getExistingTooltipHtml(Element e) /*-{ return $wnd.jQuery(e).attr('data-tooltip-content') || ''; }-*/; /** * Returns {@code true} if the given element has tooltip attached. */ private static native boolean hasTooltip(Element e) /*-{ return $wnd.jQuery(e).attr('rel') === 'tooltip'; }-*/; private static native void createTooltipImpl(Element e, String html, String placement, String template, boolean forceShow) /*-{ var $e = $wnd.jQuery(e); // `rel=tooltip` identifies a tooltipped element. $e.attr('rel', 'tooltip'); // `data-tooltip-content` contains tooltip's HTML string. $e.attr('data-tooltip-content', html); $e.tooltip({ animation: true, container: 'body', trigger: 'hover', delay: { show: 500, hide: 0 }, title: html, html: true, placement: placement, template: template }); if (forceShow) { $e.tooltip('show'); } }-*/; /** * Destroy tooltip on the given element. */ public static void destroyTooltip(Element e) { destroyTooltipImpl(e, true); removeElementFromReaperLists(e); } /** * @return {@code true} if the tooltip was successfully destroyed on the given * element. Always returns {@code true} if {@code forceDestroy} is {@code true}. */ private static native boolean destroyTooltipImpl(Element e, boolean forceDestroy) /*-{ // No tooltip means nothing to destroy. if (!@org.ovirt.engine.ui.common.utils.ElementTooltipUtils::hasTooltip(Lcom/google/gwt/dom/client/Element;)(e)) { return true; } var $e = $wnd.jQuery(e); // Don't destroy if the element is still part of live DOM. if (forceDestroy || !$wnd.jQuery.contains($doc, e)) { $e.removeAttr('rel'); $e.removeAttr('data-tooltip-content'); $e.tooltip('destroy'); return true; } return false; }-*/; /** * Hide tooltip on the given element. */ public static native void hideTooltip(Element e) /*-{ $wnd.jQuery(e).tooltip('hide'); }-*/; /** * Hide all tooltips that might be currently visible. */ public static native void hideAllTooltips() /*-{ $wnd.jQuery('[rel=tooltip]').tooltip('hide'); // Take the opportunity to reap cell widget tooltips. @org.ovirt.engine.ui.common.utils.ElementTooltipUtils::reapCellWidgetTooltips()(); }-*/; /** * Reap all tooltips attached to cell widgets. * <p> * Triggered automatically by {@linkplain CellWidgetTooltipReaper cell widget * tooltip reaper} and when calling {@link #hideAllTooltips} method manually. */ public static void reapCellWidgetTooltips() { reapTooltips(cellWidgetElementReapList); } /** * Reap all tooltips attached to popup content. * <p> * Triggered automatically upon closing each popup, given there are no popups * currently active. */ public static void reapPopupContentTooltips() { reapTooltips(popupContentElementReapList); } private static void reapTooltips(List<Element> elements) { Iterator<Element> it = elements.iterator(); while (it.hasNext()) { Element e = it.next(); if (destroyTooltipImpl(e, false)) { it.remove(); } } } private static void removeElementFromReaperLists(Element e) { List<Element> singletonList = Collections.singletonList(e); cellWidgetElementReapList.removeAll(singletonList); popupContentElementReapList.removeAll(singletonList); } // -- Cell widget utilities -- /** * TODO-GWT things would be much easier if GWT supported mouseenter + mouseleave * * @see com.google.gwt.user.client.impl.DOMImpl#eventGetTypeInt(java.lang.String) */ public static final Set<String> HANDLED_CELL_EVENTS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( BrowserEvents.MOUSEOVER, BrowserEvents.MOUSEDOWN ))); private static native Element findParentTableCellElement(Element e) /*-{ return $wnd.jQuery(e).closest('td, th')[0]; }-*/; private static void hideAllCellWidgetTooltipsExcept(String elementId) { for (Element e : cellWidgetElementReapList) { if (!elementId.equals(e.getId())) { hideTooltip(e); } } } public static void handleCellEvent(NativeEvent event, Element e, SafeHtml tooltip) { String eventType = event.getType(); if (BrowserEvents.MOUSEOVER.equals(eventType)) { Element parentTableCellElement = findParentTableCellElement(e); // Assign unique ID to the parent TD/TH element. String parentTableCellElementId = parentTableCellElement.getId(); if (StringUtils.isEmpty(parentTableCellElementId)) { parentTableCellElementId = DOM.createUniqueId(); parentTableCellElement.setId(parentTableCellElementId); } // Make sure the tooltip is set only once on the parent TD/TH element. if (!hasTooltip(parentTableCellElement)) { setTooltipOnElement(parentTableCellElement, tooltip, new TooltipConfig().setForceShow().markAsCellWidgetTooltip()); } // Prevent other cell widget tooltips from hanging open. hideAllCellWidgetTooltipsExcept(parentTableCellElementId); } else if (BrowserEvents.MOUSEDOWN.equals(eventType)) { hideAllTooltips(); } } // -- Menu widget utilities -- public static void destroyMenuItemTooltips(MenuBar menuBar) { for (int i = 0; i < menuBar.getItemCount(); ++i) { destroyTooltip(menuBar.getItem(i).getElement()); } } }