const G = require('@antv/g/lib'); const Tooltip = require('./base'); const Util = require('../util'); const DomUtil = Util.DomUtil; const TooltipTheme = require('./theme'); const Crosshair = require('./crosshair'); const PositionMixin = require('./mixin/position'); const MarkerGroupMixin = require('./mixin/marker-group'); const CONTAINER_CLASS = 'g2-tooltip'; const TITLE_CLASS = 'g2-tooltip-title'; const LIST_CLASS = 'g2-tooltip-list'; const MARKER_CLASS = 'g2-tooltip-marker'; const VALUE_CLASS = 'g2-tooltip-value'; const LIST_ITEM_CLASS = 'g2-tooltip-list-item'; const MARKER_SIZE = 5; const { Marker } = G; function find(dom, cls) { return dom.getElementsByClassName(cls)[0]; } function mergeStyles(styles, cfg) { Object.keys(styles).forEach(function(k) { if (cfg[k]) { styles[k] = Util.mix(styles[k], cfg[k]); } }); return styles; } class HtmlTooltip extends Tooltip { getDefaultCfg() { const cfg = super.getDefaultCfg(); return Util.mix({}, cfg, { /** * tooltip 容器模板 * @type {String} */ containerTpl: '
' + '
' + '' + '
', /** * tooltip 列表项模板 * @type {String} */ itemTpl: `
  • {name}{value}
  • `, /** * tooltip html内容 * @type {String} */ htmlContent: null, /** * tooltip 内容跟随鼠标移动 * @type {Boolean} */ follow: true, /** * 是否允许鼠标停留在 tooltip 上,默认不允许 * @type {Boolean} */ enterable: false }); } constructor(cfg) { super(cfg); Util.assign(this, PositionMixin); Util.assign(this, MarkerGroupMixin); const style = TooltipTheme; this.style = mergeStyles(style, cfg); this._init_(); if (this.get('items')) { this.render(); } // crosshair const crosshair = this.get('crosshairs'); if (crosshair) { const plot = crosshair.type === 'rect' ? this.get('backPlot') : this.get('frontPlot'); const crosshairGroup = new Crosshair(Util.mix({ plot, plotRange: this.get('plotRange'), canvas: this.get('canvas') }, this.get('crosshairs'))); crosshairGroup.hide(); this.set('crosshairGroup', crosshairGroup); } } _init_() { const self = this; const containerTpl = self.get('containerTpl'); const outterNode = self.get('canvas').get('el').parentNode; let container; if (!this.get('htmlContent')) { if (/^\#/.test(containerTpl)) { // 如果传入 dom 节点的 id const id = containerTpl.replace('#', ''); container = document.getElementById(id); } else { container = DomUtil.createDom(containerTpl); DomUtil.modifyCSS(container, self.style[CONTAINER_CLASS]); outterNode.appendChild(container); outterNode.style.position = 'relative'; } self.set('container', container); } } render() { const self = this; self.clear(); if (self.get('htmlContent')) { const outterNode = self.get('canvas').get('el').parentNode; const container = self._getHtmlContent(); outterNode.appendChild(container); self.set('container', container); } else { self._renderTpl(); } } _renderTpl() { const self = this; const showTitle = self.get('showTitle'); const titleContent = self.get('titleContent'); const container = self.get('container'); const titleDom = find(container, TITLE_CLASS); const listDom = find(container, LIST_CLASS); const items = self.get('items'); if (titleDom && showTitle) { DomUtil.modifyCSS(titleDom, self.style[TITLE_CLASS]); titleDom.innerHTML = titleContent; } if (listDom) { DomUtil.modifyCSS(listDom, self.style[LIST_CLASS]); Util.each(items, (item, index) => { listDom.appendChild(self._addItem(item, index)); }); } } clear() { const container = this.get('container'); if (this.get('htmlContent')) { container && container.remove(); } else { const titleDom = find(container, TITLE_CLASS); const listDom = find(container, LIST_CLASS); if (titleDom) { titleDom.innerHTML = ''; } if (listDom) { listDom.innerHTML = ''; } } } show() { const container = this.get('container'); if (!container || this.destroyed) { // 防止容器不存在或者被销毁时报错 return; } container.style.visibility = 'visible'; container.style.display = 'block'; const crosshairGroup = this.get('crosshairGroup'); crosshairGroup && crosshairGroup.show(); const markerGroup = this.get('markerGroup'); markerGroup && markerGroup.show(); super.show(); this.get('canvas').draw(); } hide() { const container = this.get('container'); // relative: https://github.com/antvis/g2/issues/1221 if (!container || this.destroyed) { return; } container.style.visibility = 'hidden'; container.style.display = 'none'; const crosshairGroup = this.get('crosshairGroup'); crosshairGroup && crosshairGroup.hide(); const markerGroup = this.get('markerGroup'); markerGroup && markerGroup.hide(); super.hide(); this.get('canvas').draw(); } destroy() { const self = this; const container = self.get('container'); const containerTpl = self.get('containerTpl'); if (container && !(/^\#/.test(containerTpl))) { container.parentNode.removeChild(container); } const crosshairGroup = this.get('crosshairGroup'); crosshairGroup && crosshairGroup.destroy(); const markerGroup = this.get('markerGroup'); markerGroup && markerGroup.remove(); super.destroy(); } _getMarkerSvg(item) { const marker = item.marker || {}; const symbol = marker.activeSymbol || marker.symbol; let method; if (Util.isFunction(symbol)) { method = symbol; } else if (Util.isString(symbol)) { method = Marker.Symbols[symbol]; } method = Util.isFunction(method) ? method : Marker.Symbols.circle; const pathArr = method(MARKER_SIZE / 2, MARKER_SIZE / 2, MARKER_SIZE / 2); const path = pathArr.reduce((res, arr) => { return `${res}${arr[0]}${arr.slice(1).join(',')}`; }, ''); return ``; } _addItem(item, index) { const itemTpl = this.get('itemTpl'); // TODO: 有可能是个回调函数 const itemDiv = Util.substitute(itemTpl, Util.mix({ index }, item)); const itemDOM = DomUtil.createDom(itemDiv); DomUtil.modifyCSS(itemDOM, this.style[LIST_ITEM_CLASS]); const markerDom = find(itemDOM, MARKER_CLASS); if (markerDom) { DomUtil.modifyCSS(markerDom, { ...this.style[MARKER_CLASS], borderRadius: 'unset' }); const markerPath = this._getMarkerSvg(item); markerDom.innerHTML = markerPath; } const valueDom = find(itemDOM, VALUE_CLASS); if (valueDom) { DomUtil.modifyCSS(valueDom, this.style[VALUE_CLASS]); } return itemDOM; } _getHtmlContent() { const htmlContent = this.get('htmlContent'); const title = this.get('titleContent'); const items = this.get('items'); const htmlString = htmlContent(title, items); const ele = DomUtil.createDom(htmlString); return ele; } setPosition(x, y, target) { const container = this.get('container'); const outterNode = this.get('canvas').get('el'); const viewWidth = DomUtil.getWidth(outterNode); const viewHeight = DomUtil.getHeight(outterNode); let containerWidth = container.clientWidth; let containerHeight = container.clientHeight; let endx = x; let endy = y; let position; const prePosition = this.get('prePosition') || { x: 0, y: 0 }; // @2019-01-30 by blue.lb 由于display:none的元素获取clientWidth和clientHeight的值为0,这里强制显隐一下,其实直接在show和hide中去掉display设置最好,猜测为了更好的兼容浏览器 if (!containerWidth) { container.style.display = 'block'; containerWidth = container.clientWidth; containerHeight = container.clientHeight; container.style.display = 'none'; } if (this.get('enterable')) { y = y - container.clientHeight / 2; position = [ x, y ]; if (prePosition && x - prePosition.x > 0) { // 留 1px 防止鼠标点击事件无法在画布上触发 x -= (container.clientWidth + 1); } else { x += 1; } } else if (this.get('position')) { // @2019-01-30 by blue.lb 这里应该是多余代码 // const containerWidth = container.clientWidth; // const containerHeight = container.clientHeight; position = this._calcTooltipPosition(x, y, this.get('position'), containerWidth, containerHeight, target); x = position[0]; y = position[1]; } else { position = this._constraintPositionInBoundary(x, y, containerWidth, containerHeight, viewWidth, viewHeight); x = position[0]; y = position[1]; } if (this.get('inPlot')) { // tooltip 必须限制在绘图区域内 const plotRange = this.get('plotRange'); position = this._constraintPositionInPlot(x, y, containerWidth, containerHeight, plotRange, this.get('enterable')); x = position[0]; y = position[1]; } const markerItems = this.get('markerItems'); if (!Util.isEmpty(markerItems)) { endx = markerItems[0].x; endy = markerItems[0].y; } this.set('prePosition', position); // 记录上次的位置 const follow = this.get('follow'); if (follow) { container.style.left = x + 'px'; container.style.top = y + 'px'; } const crosshairGroup = this.get('crosshairGroup'); if (crosshairGroup) { const items = this.get('items'); crosshairGroup.setPosition(endx, endy, items); } super.setPosition(x, y); } } module.exports = HtmlTooltip;