/** * This class monitors DOM elements that have attributes encoded * to show a tooltip. A single {@link Ext.tip.ToolTip} instance is reused * and reconfigured with the attributes retrieved from the DOM. * * Typically, this class will not be created directly, but rather configured * via the application with the {@link Ext.app.Application#quickTips} config to * enable this globally. * * # Configuring via Ext.Component * * A configuration given to a {@link Ext.Component#tooltip tooltip} config will * be registered with this manager and shared tips will be displayed when that * component is activated. See {@link Ext.Component#tooltip tooltip} for details. * * # Configuring via HTML attributes * * A tip may also be configured by adding data attributes to DOM elements. The * following attribute names are supported and map to configurations on the * {@link Ext.tip.ToolTip} class. The following are supported: * - `data-qtip`: {@link Ext.tip.ToolTip#html} * - `data-qwidth`: {@link Ext.tip.ToolTip#width} * - `data-qminWidth`: {@link Ext.tip.ToolTip#minWidth} * - `data-qmaxWidth`: {@link Ext.tip.ToolTip#maxWidth} * - `data-qtitle`: {@link Ext.tip.ToolTip#title} * - `data-qautoHide`: {@link Ext.tip.ToolTip#autoHide} * - `data-qcls`: {@link Ext.tip.ToolTip#cls} * - `data-qalign`: {@link Ext.tip.ToolTip#align} * - `data-qalignDelegate`: {@link Ext.tip.ToolTip#alignDelegate} * - `data-qanchor`: {@link Ext.tip.ToolTip#anchor} * - `data-qanchorToTarget`: {@link Ext.tip.ToolTip#anchorToTarget} * - `data-qallowOver`: {@link Ext.tip.ToolTip#allowOver} * - `data-qshowDelay`: {@link Ext.tip.ToolTip#showDelay} * - `data-qhideDelay`: {@link Ext.tip.ToolTip#hideDelay} * - `data-qdismissDelay`: {@link Ext.tip.ToolTip#dismissDelay} * - `data-qtrackMouse`: {@link Ext.tip.ToolTip#trackMouse} * - `data-qoverfow`: Indicates that if the element's text is clipped * using the `text-overflow:ellipsis` style, the shared tip will be shown * containing the `innerHTML`. * * Example usage: * * <div class="foo" data-qtip="Message goes here">Hover me</div> * <p> * <div * data-qoverflow="true" * data-qalign="b-t?" * data-qanchor="true" * style="width:100px;text-overflow:ellipsis" * > * Hover Me! Lorem ipsum dolor sit amet, duis dicta solet * et qui. Ut mea soleat mediocritatem, blandit democritum mea ex. His id nostro * conceptam dissentiet, ignota discere id mea. Qui eu virtute invenire, an per * ipsum invidunt senserit. Ne legendos expetenda mei. Ad homero percipit liberavisse * quo, te quando vocibus usu. * </div> */Ext.define('Ext.tip.Manager', { requires: ['Ext.tip.ToolTip'], config: { tooltip: { xtype: 'tooltip', // Default to mouse alignment align: '', anchorToTarget: false, anchor: false, closeAction: 'hide', quickShowInterval: 0, maxWidth: '80vw' }, /** * A configuration object to use when the shared tip instance is triggered by * an text-clipped element tagged with the `data-qoverflow` attribute. * For example, you could change the default * {@link Ext.tip.ToolTip#dismissDelay dismissDelay} to be longer, to allow long content * to be read, or set {@link Ext.tip.ToolTip#autoHide autoHide} to false by setting your * application's {@link Ext.app.Application#quickTips quickTips} config. * * quickTips: { * tooltip: { * showOnTap: true * }, * overflowTip: { * dismissDelay: 10000 * } * } * * */ overflowTip: { align: 'l-r?', anchor: true, showOnTap: true } }, /** * @cfg {Boolean} interceptTitles * Set to `true` to automatically use an element's DOM `title` attribute if one is * available. */ interceptTitles: false, constructor: function(config) { var me = this, tip; me.initConfig(config); me._fly = new Ext.dom.Fly(); me.tip = tip = Ext.create(me.createTooltip()); // Prevent auto alignment on config changes since we'll be // doing then in bulk tip.allowRealign = false; tip.on({ beforeshow: 'onBeforeShow', hovertarget: 'onHoverTarget', scope: me }); me.globalListeners = Ext.on({ scope: me, destroyable: true, dragstart: 'dragDisable', dragend: 'dragEnable', dragcancel: 'dragEnable' }); if (!me.self.instance) { me.self.instance = me; } }, /** * Disable this manager. Tips will not be able to show. */ disable: function() { var n = ++this.disabled; if (n === 1) { this.getTooltip().disable(); } }, /** * Enable this manager. Tips will be able to show. */ enable: function() { var n = --this.disabled; if (n === 0) { this.getTooltip().enable(); } else if (n < 0) { this.disabled = 0; } }, destroy: function() { var me = this; if (me.self.instance === me) { me.self.instance = null; } me._fly.detach(); // just in case me.globalListeners = me.tip = Ext.destroy(me.tip, me.globalListeners); me.callParent(); }, createTooltip: function() { var me = this, config = me.getTooltip(); return Ext.apply({ id: 'ext-global-tooltip', delegate: me.delegateQuickTip.bind(me), target: Ext.getBody() }, config); }, hide: function() { if (this.tip) { this.tip.hide(); } }, privates: { disabled: 0, /** * @property {Object} _propertyMap * The key are the configs for the `ToolTip` and the values are the corresponding * DOM attribute names. * @private * @readonly */ _propertyMap: (function() { var numFn = function(v) { return parseInt(v, 10); }, boolFn = function(v) { return !!v; }, fn = Ext.identityFn; return { ui: { prop: 'data-qui', parse: fn }, html: { prop: 'data-qtip', parse: fn }, width: { prop: 'data-qwidth', parse: numFn }, minWidth: { prop: 'data-qminWidth', parse: fn }, maxWidth: { prop: 'data-qmaxWidth', parse: fn }, title: { prop: 'data-qtitle', parse: fn }, autoHide: { prop: 'data-qautoHide', parse: boolFn }, cls: { prop: 'data-qcls', parse: fn }, axisLock: { prop: 'data-axislock', parse: fn }, align: { prop: 'data-qalign', parse: fn }, alignDelegate: { prop: 'data-qaligndelegate', parse: fn }, anchor: { prop: 'data-qanchor', parse: fn }, showDelay: { prop: 'data-qshowDelay', parse: numFn }, hideDelay: { prop: 'data-qhideDelay', parse: numFn }, dismissDelay: { prop: 'data-qdismissDelay', parse: numFn }, trackMouse: { prop: 'data-qtrackMouse', parse: boolFn }, anchorToTarget: { prop: 'data-qanchorToTarget', parse: boolFn }, allowOver: { prop: 'data-qallowover', parse: boolFn }, closable: true }; })(), applyOverflowTip: function(config) { var phone = Ext.platformTags.phone; return Ext.apply({ // Less unexpected movement with axisLock // But with limited space, a phone needs to try all other edges. axisLock: !phone, // Stop long overflowing text from sprawling. maxWidth: phone ? '80vw' : 400 }, config); }, delegateQuickTip: function(dom) { var qtip = this.getTipConfig(dom, 'html'); return !!qtip; }, dragDisable: function() { if (!this.disabled) { this.tip.disable(); } }, dragEnable: function() { if (!this.disabled) { this.tip.enable(); } }, getTipConfig: function(dom, property) { var me = this, propertyMap = me._propertyMap, tipDefaults = me._tipDefaults, fly = me._fly, data = fly.attach(dom).getData().qtip, tip = me.tip, textAttr = propertyMap.html.prop, name, text, ret, value, item; // Before we ever reconfigure the toolTip, we need to snag its default // values so we can restore them. Don't bother if all we want is the tip // text. if (!tipDefaults && property !== 'html') { me._tipDefaults = tipDefaults = {}; for (name in propertyMap) { tipDefaults[name] = tip.getConfig(name); } } if (data) { if (property) { ret = data[property]; } else { ret = Ext.apply({}, tipDefaults); Ext.apply(ret, data); } } else { if (dom.hasAttribute(textAttr)) { text = dom.getAttribute(textAttr); if (!text) { text = me.interceptTitles && dom.title; if (text) { dom.setAttribute(textAttr, text); dom.removeAttribute('title'); } } } else if (dom.hasAttribute('data-qoverflow')) { text = fly.dom.innerHTML; // If we are not just being called as a delegate selector, // augment the tipDefaults that we are going to reconfigure with if (property !== 'html') { tipDefaults = Ext.apply({}, me.getOverflowTip(), tipDefaults); } // This is the beforeshow invocation which wants the full config if (!property) { // This element has text-overflow:ellipsis AND the overflowing text. // No overflow found, veto the show by returning false. if (!me.hasTextOverflow(dom)) { return false; } } } if (text) { // if there is no qtip text there is no tooltip if (property === 'html') { ret = text; } else if (property) { item = propertyMap[property]; if (item.prop) { if (dom.hasAttribute(item.prop)) { ret = item.parse(dom.getAttribute(item.prop)); } } } else { ret = data = { html: text }; for (name in propertyMap) { if (name !== 'html') { item = propertyMap[name]; value = null; if (item.prop) { if (dom.hasAttribute(item.prop)) { value = item.parse(dom.getAttribute(item.prop)); } } if (value === null) { value = tipDefaults[name]; } data[name] = value; } } } } } // No displaying configs set, now see if it's eligible for an overflow tip fly.detach(); if (property && ret == null && property !== 'html') { ret = tipDefaults[property]; } return ret; }, onBeforeShow: function(tip) { var me = this, dom = tip.currentTarget.dom, data, header; if (dom) { data = me.getTipConfig(dom); // If the config returns false, it means no show. // data-qoverflow tips leave the decision this late to // delay the expensive isStyle() an TextMetrics calls. if (data === false) { return false; } // data could be undefined if (!data) { return; } data.anchorToTarget = !!(data.align || data.anchor); tip.setConfig(data); header = tip.getHeader(); if (header) { header.setHidden(!data.title && !data.closable); } } }, priorityConfigs: ['showDelay', 'anchor', 'anchorToTarget', 'align', 'trackMouse'], onHoverTarget: function(tip, currentTarget) { var dom = currentTarget.dom, cfg; if (dom) { cfg = {}; this.priorityConfigs.forEach(function(name) { cfg[name] = this.getTipConfig(dom, name); }, this); cfg.anchorToTarget = !!(cfg.align || cfg.anchor); tip.setConfig(cfg); } }, hasTextOverflow: function(candidate) { var textSize; // Filter for only elements with text-overflow:ellipsis if (Ext.fly(candidate).isStyle('text-overflow', 'ellipsis')) { textSize = Ext.util.TextMetrics.measure(candidate, candidate.innerHTML); return (textSize.width > Ext.fly(candidate).getViewRegion().width); } } }});