Spaces:
Running
Running
| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| import { isString, indexOf, each, bind, isFunction, isArray, isDom } from 'zrender/src/core/util'; | |
| import { normalizeEvent } from 'zrender/src/core/event'; | |
| import { transformLocalCoord } from 'zrender/src/core/dom'; | |
| import env from 'zrender/src/core/env'; | |
| import { convertToColorString, toCamelCase, normalizeCssArray } from '../../util/format'; | |
| import type ExtensionAPI from '../../core/ExtensionAPI'; | |
| import type { ZRenderType } from 'zrender/src/zrender'; | |
| import type { TooltipOption } from './TooltipModel'; | |
| import Model from '../../model/Model'; | |
| import type { ZRRawEvent } from 'zrender/src/core/types'; | |
| import type { ZRColor } from '../../util/types'; | |
| import type CanvasPainter from 'zrender/src/canvas/Painter'; | |
| import type SVGPainter from 'zrender/src/svg/Painter'; | |
| import { | |
| shouldTooltipConfine, | |
| toCSSVendorPrefix, | |
| getComputedStyle, | |
| TRANSFORM_VENDOR, | |
| TRANSITION_VENDOR | |
| } from './helper'; | |
| import { getPaddingFromTooltipModel } from './tooltipMarkup'; | |
| /* global document, window */ | |
| const CSS_TRANSITION_VENDOR = toCSSVendorPrefix(TRANSITION_VENDOR, 'transition'); | |
| const CSS_TRANSFORM_VENDOR = toCSSVendorPrefix(TRANSFORM_VENDOR, 'transform'); | |
| // eslint-disable-next-line | |
| const gCssText = `position:absolute;display:block;border-style:solid;white-space:nowrap;z-index:9999999;${env.transform3dSupported ? 'will-change:transform;' : ''}`; | |
| function mirrorPos(pos: string): string { | |
| pos = pos === 'left' | |
| ? 'right' | |
| : pos === 'right' | |
| ? 'left' | |
| : pos === 'top' | |
| ? 'bottom' | |
| : 'top'; | |
| return pos; | |
| } | |
| function assembleArrow( | |
| tooltipModel: Model<TooltipOption>, | |
| borderColor: ZRColor, | |
| arrowPosition: TooltipOption['position'] | |
| ) { | |
| if (!isString(arrowPosition) || arrowPosition === 'inside') { | |
| return ''; | |
| } | |
| const backgroundColor = tooltipModel.get('backgroundColor'); | |
| const borderWidth = tooltipModel.get('borderWidth'); | |
| borderColor = convertToColorString(borderColor); | |
| const arrowPos = mirrorPos(arrowPosition); | |
| const arrowSize = Math.max(Math.round(borderWidth) * 1.5, 6); | |
| let positionStyle = ''; | |
| let transformStyle = CSS_TRANSFORM_VENDOR + ':'; | |
| let rotateDeg; | |
| if (indexOf(['left', 'right'], arrowPos) > -1) { | |
| positionStyle += 'top:50%'; | |
| transformStyle += `translateY(-50%) rotate(${rotateDeg = arrowPos === 'left' ? -225 : -45}deg)`; | |
| } | |
| else { | |
| positionStyle += 'left:50%'; | |
| transformStyle += `translateX(-50%) rotate(${rotateDeg = arrowPos === 'top' ? 225 : 45}deg)`; | |
| } | |
| const rotateRadian = rotateDeg * Math.PI / 180; | |
| const arrowWH = arrowSize + borderWidth; | |
| const rotatedWH = arrowWH * Math.abs(Math.cos(rotateRadian)) + arrowWH * Math.abs(Math.sin(rotateRadian)); | |
| const arrowOffset = Math.round(((rotatedWH - Math.SQRT2 * borderWidth) / 2 | |
| + Math.SQRT2 * borderWidth - (rotatedWH - arrowWH) / 2) * 100) / 100; | |
| positionStyle += `;${arrowPos}:-${arrowOffset}px`; | |
| const borderStyle = `${borderColor} solid ${borderWidth}px;`; | |
| const styleCss = [ | |
| `position:absolute;width:${arrowSize}px;height:${arrowSize}px;z-index:-1;`, | |
| `${positionStyle};${transformStyle};`, | |
| `border-bottom:${borderStyle}`, | |
| `border-right:${borderStyle}`, | |
| `background-color:${backgroundColor};` | |
| ]; | |
| return `<div style="${styleCss.join('')}"></div>`; | |
| } | |
| function assembleTransition(duration: number, onlyFade?: boolean): string { | |
| const transitionCurve = 'cubic-bezier(0.23,1,0.32,1)'; | |
| let transitionOption = ` ${duration / 2}s ${transitionCurve}`; | |
| let transitionText = `opacity${transitionOption},visibility${transitionOption}`; | |
| if (!onlyFade) { | |
| transitionOption = ` ${duration}s ${transitionCurve}`; | |
| transitionText += env.transformSupported | |
| ? `,${CSS_TRANSFORM_VENDOR}${transitionOption}` | |
| : `,left${transitionOption},top${transitionOption}`; | |
| } | |
| return CSS_TRANSITION_VENDOR + ':' + transitionText; | |
| } | |
| function assembleTransform(x: number, y: number, toString?: boolean) { | |
| // If using float on style, the final width of the dom might | |
| // keep changing slightly while mouse move. So `toFixed(0)` them. | |
| const x0 = x.toFixed(0) + 'px'; | |
| const y0 = y.toFixed(0) + 'px'; | |
| // not support transform, use `left` and `top` instead. | |
| if (!env.transformSupported) { | |
| return toString | |
| ? `top:${y0};left:${x0};` | |
| : [['top', y0], ['left', x0]]; | |
| } | |
| // support transform | |
| const is3d = env.transform3dSupported; | |
| const translate = `translate${is3d ? '3d' : ''}(${x0},${y0}${is3d ? ',0' : ''})`; | |
| return toString | |
| ? 'top:0;left:0;' + CSS_TRANSFORM_VENDOR + ':' + translate + ';' | |
| : [['top', 0], ['left', 0], [TRANSFORM_VENDOR, translate]]; | |
| } | |
| /** | |
| * @param {Object} textStyle | |
| * @return {string} | |
| * @inner | |
| */ | |
| function assembleFont(textStyleModel: Model<TooltipOption['textStyle']>): string { | |
| const cssText = []; | |
| const fontSize = textStyleModel.get('fontSize'); | |
| const color = textStyleModel.getTextColor(); | |
| color && cssText.push('color:' + color); | |
| cssText.push('font:' + textStyleModel.getFont()); | |
| fontSize | |
| // @ts-ignore, leave it to the tooltip refactor. | |
| && cssText.push('line-height:' + Math.round(fontSize * 3 / 2) + 'px'); | |
| const shadowColor = textStyleModel.get('textShadowColor'); | |
| const shadowBlur = textStyleModel.get('textShadowBlur') || 0; | |
| const shadowOffsetX = textStyleModel.get('textShadowOffsetX') || 0; | |
| const shadowOffsetY = textStyleModel.get('textShadowOffsetY') || 0; | |
| shadowColor && shadowBlur | |
| && cssText.push('text-shadow:' + shadowOffsetX + 'px ' + shadowOffsetY + 'px ' | |
| + shadowBlur + 'px ' + shadowColor); | |
| each(['decoration', 'align'] as const, function (name) { | |
| const val = textStyleModel.get(name); | |
| val && cssText.push('text-' + name + ':' + val); | |
| }); | |
| return cssText.join(';'); | |
| } | |
| function assembleCssText(tooltipModel: Model<TooltipOption>, enableTransition?: boolean, onlyFade?: boolean) { | |
| const cssText: string[] = []; | |
| const transitionDuration = tooltipModel.get('transitionDuration'); | |
| const backgroundColor = tooltipModel.get('backgroundColor'); | |
| const shadowBlur = tooltipModel.get('shadowBlur'); | |
| const shadowColor = tooltipModel.get('shadowColor'); | |
| const shadowOffsetX = tooltipModel.get('shadowOffsetX'); | |
| const shadowOffsetY = tooltipModel.get('shadowOffsetY'); | |
| const textStyleModel = tooltipModel.getModel('textStyle'); | |
| const padding = getPaddingFromTooltipModel(tooltipModel, 'html'); | |
| const boxShadow = `${shadowOffsetX}px ${shadowOffsetY}px ${shadowBlur}px ${shadowColor}`; | |
| cssText.push('box-shadow:' + boxShadow); | |
| // Animation transition. Do not animate when transitionDuration is 0. | |
| enableTransition && transitionDuration && cssText.push(assembleTransition(transitionDuration, onlyFade)); | |
| if (backgroundColor) { | |
| cssText.push('background-color:' + backgroundColor); | |
| } | |
| // Border style | |
| each(['width', 'color', 'radius'] as const, function (name) { | |
| const borderName = 'border-' + name; | |
| const camelCase = toCamelCase(borderName) as 'borderWidth' | 'borderColor' | 'borderRadius'; | |
| const val = tooltipModel.get(camelCase); | |
| val != null | |
| && cssText.push(borderName + ':' + val + (name === 'color' ? '' : 'px')); | |
| }); | |
| // Text style | |
| cssText.push(assembleFont(textStyleModel)); | |
| // Padding | |
| if (padding != null) { | |
| cssText.push('padding:' + normalizeCssArray(padding).join('px ') + 'px'); | |
| } | |
| return cssText.join(';') + ';'; | |
| } | |
| // If not able to make, do not modify the input `out`. | |
| function makeStyleCoord( | |
| out: number[], | |
| zr: ZRenderType, | |
| container: HTMLElement | null | undefined, | |
| zrX: number, | |
| zrY: number | |
| ) { | |
| const zrPainter = zr && zr.painter; | |
| if (container) { | |
| const zrViewportRoot = zrPainter && zrPainter.getViewportRoot(); | |
| if (zrViewportRoot) { | |
| // Some APPs might use scale on body, so we support CSS transform here. | |
| transformLocalCoord(out, zrViewportRoot, container, zrX, zrY); | |
| } | |
| } | |
| else { | |
| out[0] = zrX; | |
| out[1] = zrY; | |
| // xy should be based on canvas root. But tooltipContent is | |
| // the sibling of canvas root. So padding of ec container | |
| // should be considered here. | |
| const viewportRootOffset = zrPainter && (zrPainter as CanvasPainter | SVGPainter).getViewportRootOffset(); | |
| if (viewportRootOffset) { | |
| out[0] += viewportRootOffset.offsetLeft; | |
| out[1] += viewportRootOffset.offsetTop; | |
| } | |
| } | |
| out[2] = out[0] / zr.getWidth(); | |
| out[3] = out[1] / zr.getHeight(); | |
| } | |
| interface TooltipContentOption { | |
| /** | |
| * Specify target container of the tooltip element. | |
| * Can either be an HTMLElement, CSS selector string, or a function that returns an HTMLElement. | |
| */ | |
| appendTo: ((chartContainer: HTMLElement) => HTMLElement | undefined | null) | HTMLElement | string | |
| } | |
| class TooltipHTMLContent { | |
| el: HTMLDivElement; | |
| private _api: ExtensionAPI; | |
| private _container: HTMLElement | undefined | null; | |
| private _show: boolean = false; | |
| private _styleCoord: [number, number, number, number] = [0, 0, 0, 0]; | |
| private _enterable = true; | |
| private _zr: ZRenderType; | |
| private _alwaysShowContent: boolean = false; | |
| private _hideTimeout: number; | |
| /** | |
| * Hide delay time | |
| */ | |
| private _hideDelay: number; | |
| private _inContent: boolean; | |
| private _firstShow = true; | |
| private _longHide = true; | |
| /** | |
| * Record long-time hide | |
| */ | |
| private _longHideTimeout: number; | |
| constructor( | |
| api: ExtensionAPI, | |
| opt: TooltipContentOption | |
| ) { | |
| if (env.wxa) { | |
| return null; | |
| } | |
| const el = document.createElement('div'); | |
| // TODO: TYPE | |
| (el as any).domBelongToZr = true; | |
| this.el = el; | |
| const zr = this._zr = api.getZr(); | |
| const appendTo = opt.appendTo; | |
| const container: HTMLElement | null | undefined = appendTo && ( | |
| isString(appendTo) | |
| ? document.querySelector(appendTo) | |
| : isDom(appendTo) | |
| ? appendTo | |
| : isFunction(appendTo) && appendTo(api.getDom()) | |
| ); | |
| makeStyleCoord(this._styleCoord, zr, container, api.getWidth() / 2, api.getHeight() / 2); | |
| (container || api.getDom()).appendChild(el); | |
| this._api = api; | |
| this._container = container; | |
| // FIXME | |
| // Is it needed to trigger zr event manually if | |
| // the browser do not support `pointer-events: none`. | |
| const self = this; | |
| el.onmouseenter = function () { | |
| // clear the timeout in hideLater and keep showing tooltip | |
| if (self._enterable) { | |
| clearTimeout(self._hideTimeout); | |
| self._show = true; | |
| } | |
| self._inContent = true; | |
| }; | |
| el.onmousemove = function (e) { | |
| e = e || (window as any).event; | |
| if (!self._enterable) { | |
| // `pointer-events: none` is set to tooltip content div | |
| // if `enterable` is set as `false`, and `el.onmousemove` | |
| // can not be triggered. But in browser that do not | |
| // support `pointer-events`, we need to do this: | |
| // Try trigger zrender event to avoid mouse | |
| // in and out shape too frequently | |
| const handler = zr.handler; | |
| const zrViewportRoot = zr.painter.getViewportRoot(); | |
| normalizeEvent(zrViewportRoot, e as ZRRawEvent, true); | |
| handler.dispatch('mousemove', e); | |
| } | |
| }; | |
| el.onmouseleave = function () { | |
| // set `_inContent` to `false` before `hideLater` | |
| self._inContent = false; | |
| if (self._enterable) { | |
| if (self._show) { | |
| self.hideLater(self._hideDelay); | |
| } | |
| } | |
| }; | |
| } | |
| /** | |
| * Update when tooltip is rendered | |
| */ | |
| update(tooltipModel: Model<TooltipOption>) { | |
| // FIXME | |
| // Move this logic to ec main? | |
| if (!this._container) { | |
| const container = this._api.getDom(); | |
| const position = getComputedStyle(container, 'position'); | |
| const domStyle = container.style; | |
| if (domStyle.position !== 'absolute' && position !== 'absolute') { | |
| domStyle.position = 'relative'; | |
| } | |
| } | |
| // move tooltip if chart resized | |
| const alwaysShowContent = tooltipModel.get('alwaysShowContent'); | |
| alwaysShowContent && this._moveIfResized(); | |
| // update alwaysShowContent | |
| this._alwaysShowContent = alwaysShowContent; | |
| // update className | |
| this.el.className = tooltipModel.get('className') || ''; | |
| // Hide the tooltip | |
| // PENDING | |
| // this.hide(); | |
| } | |
| show(tooltipModel: Model<TooltipOption>, nearPointColor: ZRColor) { | |
| clearTimeout(this._hideTimeout); | |
| clearTimeout(this._longHideTimeout); | |
| const el = this.el; | |
| const style = el.style; | |
| const styleCoord = this._styleCoord; | |
| if (!el.innerHTML) { | |
| style.display = 'none'; | |
| } | |
| else { | |
| style.cssText = gCssText | |
| + assembleCssText(tooltipModel, !this._firstShow, this._longHide) | |
| // initial transform | |
| + assembleTransform(styleCoord[0], styleCoord[1], true) | |
| + `border-color:${convertToColorString(nearPointColor)};` | |
| + (tooltipModel.get('extraCssText') || '') | |
| // If mouse occasionally move over the tooltip, a mouseout event will be | |
| // triggered by canvas, and cause some unexpectable result like dragging | |
| // stop, "unfocusAdjacency". Here `pointer-events: none` is used to solve | |
| // it. Although it is not supported by IE8~IE10, fortunately it is a rare | |
| // scenario. | |
| + `;pointer-events:${this._enterable ? 'auto' : 'none'}`; | |
| } | |
| this._show = true; | |
| this._firstShow = false; | |
| this._longHide = false; | |
| } | |
| setContent( | |
| content: string | HTMLElement | HTMLElement[], | |
| markers: unknown, | |
| tooltipModel: Model<TooltipOption>, | |
| borderColor?: ZRColor, | |
| arrowPosition?: TooltipOption['position'] | |
| ) { | |
| const el = this.el; | |
| if (content == null) { | |
| el.innerHTML = ''; | |
| return; | |
| } | |
| let arrow = ''; | |
| if (isString(arrowPosition) && tooltipModel.get('trigger') === 'item' | |
| && !shouldTooltipConfine(tooltipModel)) { | |
| arrow = assembleArrow(tooltipModel, borderColor, arrowPosition); | |
| } | |
| if (isString(content)) { | |
| el.innerHTML = content + arrow; | |
| } | |
| else if (content) { | |
| // Clear previous | |
| el.innerHTML = ''; | |
| if (!isArray(content)) { | |
| content = [content]; | |
| } | |
| for (let i = 0; i < content.length; i++) { | |
| if (isDom(content[i]) && content[i].parentNode !== el) { | |
| el.appendChild(content[i]); | |
| } | |
| } | |
| // no arrow if empty | |
| if (arrow && el.childNodes.length) { | |
| // no need to create a new parent element, but it's not supported by IE 10 and older. | |
| // const arrowEl = document.createRange().createContextualFragment(arrow); | |
| const arrowEl = document.createElement('div'); | |
| arrowEl.innerHTML = arrow; | |
| el.appendChild(arrowEl); | |
| } | |
| } | |
| } | |
| setEnterable(enterable: boolean) { | |
| this._enterable = enterable; | |
| } | |
| getSize() { | |
| const el = this.el; | |
| return [el.offsetWidth, el.offsetHeight]; | |
| } | |
| moveTo(zrX: number, zrY: number) { | |
| const styleCoord = this._styleCoord; | |
| makeStyleCoord(styleCoord, this._zr, this._container, zrX, zrY); | |
| if (styleCoord[0] != null && styleCoord[1] != null) { | |
| const style = this.el.style; | |
| const transforms = assembleTransform(styleCoord[0], styleCoord[1]) as string[][]; | |
| each(transforms, (transform) => { | |
| style[transform[0] as any] = transform[1]; | |
| }); | |
| } | |
| } | |
| /** | |
| * when `alwaysShowContent` is true, | |
| * move the tooltip after chart resized | |
| */ | |
| _moveIfResized() { | |
| // The ratio of left to width | |
| const ratioX = this._styleCoord[2]; | |
| // The ratio of top to height | |
| const ratioY = this._styleCoord[3]; | |
| this.moveTo( | |
| ratioX * this._zr.getWidth(), | |
| ratioY * this._zr.getHeight() | |
| ); | |
| } | |
| hide() { | |
| const style = this.el.style; | |
| style.visibility = 'hidden'; | |
| style.opacity = '0'; | |
| env.transform3dSupported && (style.willChange = ''); | |
| this._show = false; | |
| this._longHideTimeout = setTimeout(() => this._longHide = true, 500) as any; | |
| } | |
| hideLater(time?: number) { | |
| if (this._show && !(this._inContent && this._enterable) && !this._alwaysShowContent) { | |
| if (time) { | |
| this._hideDelay = time; | |
| // Set show false to avoid invoke hideLater multiple times | |
| this._show = false; | |
| this._hideTimeout = setTimeout(bind(this.hide, this), time) as any; | |
| } | |
| else { | |
| this.hide(); | |
| } | |
| } | |
| } | |
| isShow() { | |
| return this._show; | |
| } | |
| dispose() { | |
| clearTimeout(this._hideTimeout); | |
| clearTimeout(this._longHideTimeout); | |
| const parentNode = this.el.parentNode; | |
| parentNode && parentNode.removeChild(this.el); | |
| this.el = this._container = null; | |
| } | |
| } | |
| export default TooltipHTMLContent; | |