/** * ToolTip is a {@link Ext.tip.Tip} implementation that handles the common case of displaying a * tooltip when hovering over a certain element or elements on the page. It allows fine-grained * control over the tooltip's alignment relative to the target element or mouse, and the timing * of when it is automatically shown and hidden. * * This implementation does **not** have a built-in method of automatically populating the tooltip's * text based on the target element; you must either configure a fixed {@link #html} value for each * ToolTip instance, or implement custom logic (e.g. in a {@link #beforeshow} event listener) to * generate the appropriate tooltip content on the fly. See {@link Ext.tip.QuickTip} for a more * convenient way of automatically populating and configuring a tooltip based on specific DOM * attributes of each target element. * * # Basic Example * * @example * Ext.getBody().appendChild({ * id: 'clearButton', * html: 'Clear Button', * style: 'display:inline-block;background:#A2C841;padding:7px;cursor:pointer;' * }); * * var tip = Ext.create('Ext.tip.ToolTip', { * target: 'clearButton', * html: 'Press this button to clear the form' * }); * * # Delegation * * In addition to attaching a ToolTip to a single element, you can also use delegation to attach * one ToolTip to many elements under a common parent. This is more efficient than creating many * ToolTip instances. To do this, point the {@link #target} config to a common ancestor of all the * elements, and then set the {@link #delegate} config to a CSS selector that will select all the * appropriate sub-elements. * * When using delegation, it is likely that you will want to programmatically change the content * of the ToolTip based on each delegate element; you can do this by implementing a custom * listener for the {@link #beforeshow} event. Example: * * @example * var store = Ext.create('Ext.data.ArrayStore', { * fields: ['company', 'price', 'change'], * data: [ * ['3m Co', 71.72, 0.02], * ['Alcoa Inc', 29.01, 0.42], * ['Altria Group Inc', 83.81, 0.28], * ['American Express Company', 52.55, 0.01], * ['American International Group, Inc.', 64.13, 0.31], * ['AT&T Inc.', 31.61, -0.48] * ] * }); * * var grid = Ext.create('Ext.grid.Panel', { * title: 'Array Grid', * store: store, * columns: [ * {text: 'Company', flex: 1, dataIndex: 'company'}, * {text: 'Price', width: 75, dataIndex: 'price'}, * {text: 'Change', width: 75, dataIndex: 'change'} * ], * height: 200, * width: 400, * renderTo: Ext.getBody() * }); * * var view = grid.getView(); * var tip = Ext.create('Ext.tip.ToolTip', { * // The overall target element. * target: view.el, * // Each grid row causes its own separate show and hide. * delegate: view.itemSelector, * // Moving within the row should not hide the tip. * trackMouse: true, * // Render immediately so that tip.body can be referenced prior to the first show. * renderTo: Ext.getBody(), * listeners: { * // Change content dynamically depending on which element triggered the show. * beforeshow: function updateTipBody(tip) { * tip.update('Over company "' + view.getRecord(tip.triggerElement).get('company') + '"'); * } * } * }); * * # Alignment * * The following configuration properties allow control over how the ToolTip is aligned relative to * the target element and/or mouse pointer: * * - {@link #anchor} * - {@link #anchorToTarget} * - {@link #trackMouse} * - {@link #mouseOffset} * * # Showing/Hiding * * The following configuration properties allow control over how and when the ToolTip is automatically * shown and hidden: * * - {@link #autoHide} * - {@link #showDelay} * - {@link #hideDelay} * - {@link #dismissDelay} */Ext.define('Ext.tip.ToolTip', { extend: 'Ext.tip.Tip', alias: 'widget.tooltip', alternateClassName: 'Ext.ToolTip', requires: ['Ext.util.Offset'], /** * @property {HTMLElement} triggerElement * When a ToolTip is configured with the `{@link #delegate}` * option to cause selected child elements of the `{@link #target}` * Element to each trigger a separate show event, this property is set to * the DOM element which triggered the show. */ /** * @cfg {HTMLElement/Ext.dom.Element/String} target * The target element or string id to monitor for mouseover events to trigger * showing this ToolTip. */ /** * @cfg {Boolean} [autoHide=true] * True to automatically hide the tooltip after the * mouse exits the target element or after the `{@link #dismissDelay}` * has expired if set. If `{@link #closable} = true` * a close tool button will be rendered into the tooltip header. */ autoHide: true, /** * @cfg {Number} showDelay * Delay in milliseconds before the tooltip displays after the mouse enters the target element. */ showDelay: 500, /** * @cfg {Number} hideDelay * Delay in milliseconds after the mouse exits the target element but before the tooltip actually hides. * Set to 0 for the tooltip to hide immediately. */ hideDelay: 200, /** * @cfg {Number} dismissDelay * Delay in milliseconds before the tooltip automatically hides. To disable automatic hiding, set * dismissDelay = 0. */ dismissDelay: 5000, /** * @cfg {Number[]} [targetOffset=[0, 0]] * When {@link #anchorToTarget} is being used to position this tip relative to its target element, * this may be used as an extra XY offset from the target element. */ /** * @cfg {Number[]} [mouseOffset=[15, 18]] * An XY offset from the mouse position where the tooltip should be shown. */ mouseOffset: [15, 18], /** * @cfg {Boolean} trackMouse * True to have the tooltip follow the mouse as it moves over the target element. */ trackMouse: false, /** * @cfg {String} anchor * If specified, indicates that the tip should be anchored to a * particular side of the target element or mouse pointer ("top", "right", "bottom", * or "left"), with an arrow pointing back at the target or mouse pointer. If * {@link #constrainPosition} is enabled, this will be used as a preferred value * only and may be flipped as needed. */ /** * @cfg {Boolean} anchorToTarget * True to anchor the tooltip to the target element, false to anchor it relative to the mouse coordinates. * When `anchorToTarget` is true, use `{@link #defaultAlign}` to control tooltip alignment to the * target element. When `anchorToTarget` is false, use `{@link #anchor}` instead to control alignment. */ anchorToTarget: true, /** * @cfg {String} delegate * * A {@link Ext.DomQuery DomQuery} simple selector which allows selection of individual elements within the * `{@link #target}` element to trigger showing and hiding the ToolTip as the mouse moves within the * target. See {@link Ext.dom.Query} for information about simple selectors. * * When specified, the child element of the target which caused a show event is placed into the * `{@link #triggerElement}` property before the ToolTip is shown. * * This may be useful when a Component has regular, repeating elements in it, each of which need a * ToolTip which contains information specific to that element. * * See the delegate example in class documentation of {@link Ext.tip.ToolTip}. */ /** * @cfg {Boolean} [showOnTap=false] * On touch platforms, if {@link #showOnTap} is `true`, a tap on the target shows the tip. * In this case any {@link #showDelay} is ignored. * * This is useful for adding tips on elements which do not have tap listeners. It would * not be appropriate for a ToolTip on a {@link Ext.Button Button}. */ /** * @private */ targetCounter: 0, quickShowInterval: 250, /** * @cfg {String} [hideAction="hide"] * The method to use to hide the tooltip. Another useful method for this is `fadeOut`. */ hideAction: 'hide', /** * @cfg {Number} [fadeOutDuration=1000] * The number of milliseconds for the `fadeOut` animation. Only valid if `hideAction` * is set to `fadeOut`. */ fadeOutDuration: 1000, /** * @cfg {String} defaultAlign * A string which specifies how this ToolTip is to align with regard to its * {@link #currentTarget} by means of identifying the point of the tooltip to * join to the point of the target. * * By default, the tooltip shows at {@link #mouseOffset} pixels from the * triggering pointer event. Using this config anchors the ToolTip to its target * instead. * * This may take the following forms: * * - **Blank**: Defaults to aligning the element's top-left corner to the target's * bottom-left corner ("tl-bl"). * - **Two anchors**: If two values from the table below are passed separated by a dash, * the first value is used as the element's anchor point, and the second value is * used as the target's anchor point. * - **Two edge/offset descriptors:** An edge/offset descriptor is an edge initial * (`t`/`r`/`b`/`l`) followed by a percentage along that side. This describes a * point to align with a similar point in the target. So `'t0-b0'` would be * the same as `'tl-bl'`, `'l0-r50'` would place the top left corner of this item * halfway down the right edge of the target item. This allows more flexibility * and also describes which two edges are considered adjacent when positioning a tip pointer. * * By default, tooltips are constrained to the viewport, but if {@link #constrain} * is configured as `false`, the position parameter also supports the "?" * character. If "?" is passed at the end of the position string, the element will * attempt to align as specified, but the position will be adjusted to constrain to * the viewport if necessary. Note that the element being aligned might be swapped to * align to a different position than that specified in order to enforce the viewport * constraints. Following are all of the supported anchor positions: * * Value Description * ----- ----------------------------- * tl The top left corner * t The center of the top edge * tr The top right corner * l The center of the left edge * c The center * r The center of the right edge * bl The bottom left corner * b The center of the bottom edge * br The bottom right corner * * Example Usage: * * // align the top left corner of the tooltip with the top right corner of its target. * defaultAlign: 'tl-tr' * * // align the bottom right corner of the tooltip with the center left edge of its target. * defaultAlign: 'br-l' * * // align the top center of the tooltip with the bottom left corner of its target. * defaultAlign: 't-bl' * * // align the 25% point on the bottom edge of this tooltip * // with the 75% point on the top edge of its target. * defaultAlign: 'b25-c75' */ defaultAlign: 'bl-tl', ariaRole: 'tooltip', alwaysOnTop: true, initComponent: function() { var me = this; me.callParent(); me.setTarget(me.target); // currentTarget is a flyweight which points to the activeTarget. me.currentTarget = new Ext.dom.Fly(); }, onRender: function(ct, position) { var me = this; me.callParent(arguments); //<debug> if (me.sticky) { // tell the spec runner to ignore this element when checking if the dom is clean me.el.dom.setAttribute('data-sticky', true); } //</debug> me.anchorEl = me.el.createChild({ role: 'presentation', cls: Ext.baseCSSPrefix + 'tip-anchor' }); }, show: function() { // A programmatic show should align to the target if (!this.currentTarget.dom && this.target) { return this.showBy(this.target); } this.callParent(); }, /** * Binds this ToolTip to the specified element. The tooltip will be displayed when the mouse moves over the element. * @param {String/HTMLElement/Ext.dom.Element} target The Element, HTMLElement, or * ID of an element to bind to */ setTarget: function(target) { var me = this, listeners; if (me.targetListeners) { me.targetListeners.destroy(); } if (target) { me.target = target = Ext.get(target.el || target); listeners = { mouseover: 'onTargetOver', mouseout: 'onTargetOut', mousemove: 'onMouseMove', tap: 'onTargetTap', scope: me, destroyable: true }; me.targetListeners = target.on(listeners); } else { me.target = null; } }, /** * @private */ onMouseMove: function(e) { var me = this, dismissDelay = me.dismissDelay; // Always update pointerEvent, so that if there's a delayed show // scheduled, it gets the latest pointer to align to. me.pointerEvent = e; if (me.isVisible() && me.currentTarget.contains(e.target)) { // If they move the mouse, restart the dismiss delay if (dismissDelay && me.autoHide !== false) { me.clearTimer('dismiss'); me.dismissTimer = Ext.defer(me.hide, dismissDelay, me); } if (me.trackMouse) { me.doAlignment(me.getAlignRegion()); } } }, /** * @private */ getAlignRegion: function() { var me = this, anchorEl = me.anchorEl, align = me.getAnchorAlign(), overlap, alignSpec, target, mouseOffset = me.mouseOffset; if (!me.anchorSize) { anchorEl.addCls(Ext.baseCSSPrefix + 'tip-anchor-top'); anchorEl.show(); me.anchorSize = new Ext.util.Offset(anchorEl.getWidth(), anchorEl.getHeight()); anchorEl.removeCls(Ext.baseCSSPrefix + 'tip-anchor-top'); anchorEl.hide(); } // Target region from the anchorTarget element unless trackMouse set if ((me.anchor || me.align) && me.anchorToTarget && !me.trackMouse) { target = me.currentTarget.getRegion(); } // Here, we're either trackMouse: true, or we're not anchored to the target // element, so we should show offset from the mouse. // If we are being shown programatically, use 0, 0 else { target = me.pointerEvent ? me.pointerEvent.getPoint().adjust(-mouseOffset[1], mouseOffset[0], mouseOffset[1], -mouseOffset[0]) : new Ext.util.Point(); if (!me.anchor) { overlap = true; if (mouseOffset[0] > 0) { if (mouseOffset[1] > 0) { align = 'tl-br'; } else { align = 'bl-tr'; } } else { if (mouseOffset[1] > 0) { align = 'tr-bl'; } else { align = 'br-tl'; } } } } alignSpec = { align: me.convertPositionSpec(align), axisLock: me.axisLock, target: target, overlap: overlap, offset: me.targetOffset, inside: me.constrainPosition ? Ext.getBody().getRegion().adjust(5, -5, -5, 5) : null } if (me.anchor) { alignSpec.anchorSize = me.anchorSize; } return me.getRegion().alignTo(alignSpec); }, fadeOut: function () { var me = this; me.el.fadeOut({ duration: me.fadeOutDuration, callback: function () { me.hide(); me.el.setOpacity(''); } }); }, /** * @private */ getAnchorAlign: function() { switch (this.anchor) { case 'top': return 'tl-bl'; case 'left': return 'tl-tr'; case 'right': return 'tr-tl'; default: return this.defaultAlign; } }, onTargetTap: function(e) { // On hybrid mouse/touch systems, we want to show the tip on touch, but // we don't want to show it if this is coming from a click event, because // the mouse is already hovered. if (this.showOnTap && e.pointerType !== 'mouse') { this.onTargetOver(e); } }, /** * @private */ onTargetOver: function(e) { var me = this, delegate = me.delegate, currentTarget = me.currentTarget, fromElement = e.relatedTarget || e.fromElement, newTarget, myListeners = me.hasListeners; if (me.disabled) { return; } if (delegate) { // Moving inside a delegate if (currentTarget.contains(e.target)) { return; } newTarget = e.getTarget(delegate); // Move inside a delegate with no currentTarget if (newTarget && Ext.fly(newTarget).contains(e.fromElement)) { return; } } // Moved from outside the target else if (!me.target.contains(fromElement)) { newTarget = me.target.dom; } // Moving inside the target else { return; } // If pointer entered the target or a delegate child, then show. if (newTarget) { // If users need to see show events on target change, we must hide. if ((myListeners.beforeshow || myListeners.show) && me.isVisible()) { me.hide(); } me.triggerElement = newTarget; me.pointerEvent = e; currentTarget.attach(newTarget); me.handleTargetOver(newTarget, e); } // If over a non-delegate child, behave as in target out else if (currentTarget.dom) { me.handleTargetOut(); } }, handleTargetOver: function(target, event) { // Separated from onTargetOver so that subclasses can handle target over in any way. // If we are showing on tap, show immediately if (event.pointerType !== 'mouse') { this.showFromDelay(); } else { this.delayShow(); } }, /** * @private */ delayShow: function() { var me = this; me.clearTimer('hide'); if (me.hidden && !me.showTimer) { if (me.delegate && Ext.Date.getElapsed(me.lastHidden) < me.quickShowInterval) { me.showFromDelay(); } else { me.showTimer = Ext.defer(me.showFromDelay, me.pointerEvent.pointerType !== 'mouse' ? 0 : me.showDelay, me); } } else if (!me.hidden && me.autoHide !== false) { me.showFromDelay(); } }, showFromDelay: function () { var me = this; // Need to check this here since onDisable only gets called after render, which // the show call below may trigger if (!me.disabled) { me.fireEvent('hovertarget', me, me.currentTarget, me.currentTarget.dom); if (me.isVisible()) { me.handleAfterShow(); } else { me.triggerElement = me.currentTarget.dom; me.fromDelayShow = true; me.show(); me.fromDelayShow = false; } } }, /** * @private */ onTargetOut: function(e) { // We have exited the current target if (this.currentTarget.dom && !this.currentTarget.contains(e.relatedTarget)) { this.handleTargetOut(); } }, handleTargetOut: function() { var me = this; if (me.showTimer) { me.clearTimer('show'); } if (me.isVisible() && me.autoHide) { me.delayHide(); } }, /** * @private */ delayHide: function() { var me = this; if (!me.hidden && !me.hideTimer) { me.clearTimer('dismiss'); me.hideTimer = Ext.defer(me[me.hideAction], me.hideDelay, me); } }, /** * Hides this tooltip if visible. */ hide: function() { var me = this; // Must also do this on hide in case it was dismissed while over me.currentTarget.detach(); me.clearTimer('dismiss'); me.lastHidden = new Date(); if (me.anchorEl) { me.anchorEl.hide(); } me.callParent(arguments); me.triggerElement = null; }, /** * Ensures this tooltip at the current event target XY position. */ afterShow: function () { this.callParent(); this.handleAfterShow(); }, handleAfterShow: function() { var me = this; me.clearTimers(); if (!me.calledFromShowAt) { me.doAlignment(me.getAlignRegion()); } if (me.dismissDelay && me.autoHide !== false) { me.dismissTimer = Ext.defer(me.hide, me.dismissDelay, me); } }, /** * Shows this ToolTip aligned to the passed Component or element according to the {@link #anchor} config. * @param {Ext.Component/Ext.dom.Element} target The {@link Ext.Component} or {@link Ext.dom.Element} to show this ToolTip by. */ showBy: function(target) { this.align = this.defaultAlign; this.currentTarget.attach(Ext.getDom(target.el || target)); this.triggerElement = this.currentTarget.dom; this.show(); }, _timerNames: {}, /** * @private */ clearTimer: function (name) { var me = this, names = me._timerNames, propName = names[name] || (names[name] = name + 'Timer'), timer = me[propName]; if (timer) { clearTimeout(timer); me[propName] = null; // We were going to show against the target, but now not. if (name === 'show' && me.isHidden()) { me.currentTarget.detach(); } } }, /** * @private */ clearTimers: function() { var me = this; me.clearTimer('show'); me.clearTimer('dismiss'); me.clearTimer('hide'); }, onShow: function() { var me = this; me.callParent(); me.mon(Ext.getDoc(), 'mousedown', me.onDocMouseDown, me); }, onHide: function() { var me = this; me.callParent(); me.mun(Ext.getDoc(), 'mousedown', me.onDocMouseDown, me); }, /** * @private */ onDocMouseDown: function(e) { var me = this; if (!me.closable && !e.within(me.el.dom)) { me.disable(); Ext.defer(me.doEnable, 100, me); } }, /** * @private */ doEnable: function() { if (!this.destroyed) { this.enable(); } }, onDisable: function() { this.callParent(); this.clearTimers(); this.hide(); }, doDestroy: function() { var me = this; me.clearTimers(); Ext.getDoc().un('mousedown', me.onDocMouseDown, me); Ext.destroy(me.anchorEl); me.callParent(); }, privates: { clipTo: function(clippingEl, sides) { // Override because we also need to clip the anchor var clippingRegion; // Allow a Region to be passed if (clippingEl.isRegion) { clippingRegion = clippingEl; } else { clippingRegion = (clippingEl.isComponent ? clippingEl.el : Ext.fly(clippingEl)).getConstrainRegion(); } this.callParent([clippingRegion, sides]); // Clip the anchor to the same bounds this.anchorEl.clipTo(clippingRegion, sides); } }});