diff --git a/src/component/tooltip/TooltipHTMLContent.ts b/src/component/tooltip/TooltipHTMLContent.ts index 8f600934dc..85f4d8b5a1 100644 --- a/src/component/tooltip/TooltipHTMLContent.ts +++ b/src/component/tooltip/TooltipHTMLContent.ts @@ -17,26 +17,36 @@ * under the License. */ -import { isString, indexOf, map, each, bind, isArray, isDom } from 'zrender/src/core/util'; +import { isString, indexOf, each, bind, isArray, isDom } from 'zrender/src/core/util'; import { toHex } from 'zrender/src/tool/color'; 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 ExtensionAPI from '../../core/ExtensionAPI'; -import { ZRenderType } from 'zrender/src/zrender'; -import { TooltipOption } from './TooltipModel'; +import type ExtensionAPI from '../../core/ExtensionAPI'; +import type { ZRenderType } from 'zrender/src/zrender'; +import type { TooltipOption } from './TooltipModel'; import Model from '../../model/Model'; -import { ZRRawEvent } from 'zrender/src/core/types'; -import { ColorString, ZRColor } from '../../util/types'; -import CanvasPainter from 'zrender/src/canvas/Painter'; -import SVGPainter from 'zrender/src/svg/Painter'; -import { shouldTooltipConfine } from './helper'; +import type { ZRRawEvent } from 'zrender/src/core/types'; +import type { ColorString, 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'; -const vendors = ['-ms-', '-moz-', '-o-', '-webkit-', '']; +/* global document, window */ -const gCssText = 'position:absolute;display:block;border-style:solid;white-space:nowrap;z-index:9999999;'; +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' @@ -60,44 +70,66 @@ function assembleArrow( borderColor = convertToColorString(borderColor); const arrowPos = mirrorPos(arrowPosition); - let positionStyle = ''; - let transformStyle = ''; + let positionStyle = `${arrowPos}:-6px;`; + let transformStyle = CSS_TRANSFORM_VENDOR + ':'; if (indexOf(['left', 'right'], arrowPos) > -1) { - positionStyle = `${arrowPos}:-6px;top:50%;`; - transformStyle = `translateY(-50%) rotate(${arrowPos === 'left' ? -225 : -45}deg)`; + positionStyle += 'top:50%'; + transformStyle += `translateY(-50%) rotate(${arrowPos === 'left' ? -225 : -45}deg)`; } else { - positionStyle = `${arrowPos}:-6px;left:50%;`; - transformStyle = `translateX(-50%) rotate(${arrowPos === 'top' ? 225 : 45}deg)`; + positionStyle += 'left:50%'; + transformStyle += `translateX(-50%) rotate(${arrowPos === 'top' ? 225 : 45}deg)`; } - transformStyle = map(vendors, function (vendorPrefix) { - return vendorPrefix + 'transform:' + transformStyle; - }).join(';'); - + const borderStyle = `${borderColor} solid 1px;`; const styleCss = [ 'position:absolute;width:10px;height:10px;', - `${positionStyle}${transformStyle};`, - `border-bottom: ${borderColor} solid 1px;`, - `border-right: ${borderColor} solid 1px;`, - `background-color: ${backgroundColor};`, - 'box-shadow: 8px 8px 16px -3px #000;' + `${positionStyle};${transformStyle};`, + `border-bottom:${borderStyle}`, + `border-right:${borderStyle}`, + `background-color:${backgroundColor};`, + 'box-shadow:8px 8px 16px -3px #000;' ]; return `
`; } function assembleTransition(duration: number, onlyFade?: boolean): string { - const transitionCurve = 'cubic-bezier(0.23, 1, 0.32, 1)'; - let transitionText = 'opacity ' + (duration / 2) + 's ' + transitionCurve + ',' - + 'visibility ' + (duration / 2) + 's ' + transitionCurve; + const transitionCurve = 'cubic-bezier(0.23,1,0.32,1)'; + let transitionOption = ` ${duration / 2}s ${transitionCurve}`; + let transitionText = `opacity${transitionOption},visibility${transitionOption}`; if (!onlyFade) { - transitionText += ',left ' + duration + 's ' + transitionCurve - + ',top ' + duration + 's ' + transitionCurve; + transitionOption = ` ${duration}s ${transitionCurve}`; + transitionText += env.transformSupported + ? `,${TRANSFORM_VENDOR}${transitionOption}` + : `,left${transitionOption},top${transitionOption}`; } - return map(vendors, function (vendorPrefix) { - return vendorPrefix + 'transition:' + transitionText; - }).join(';'); + return CSS_TRANSITION_VENDOR + ':' + transitionText; +} + +function assembleTransform(el: HTMLElement, 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. + let x0; + let y0; + // not support transform, use `left` and `top` instead. + if (!env.transformSupported) { + x0 = x.toFixed(0); + y0 = y.toFixed(0); + return toString + ? `top:${y0}px;left:${x0}px;` + : [['top', `${y0}px`], ['left', `${x0}px`]]; + } + // support transform + // FIXME: the padding of parent element will affect the position of tooltip + const stl = getComputedStyle(el.parentElement); + x0 = (x - parseInt(stl.paddingLeft, 10)).toFixed(0); + y0 = (y - parseInt(stl.paddingTop, 10)).toFixed(0); + const is3d = env.transform3dSupported; + const translate = `translate${is3d ? '3d' : ''}(${x0}px,${y0}px${is3d ? ',0' : ''})`; + return toString + ? CSS_TRANSFORM_VENDOR + ':' + translate + ';' + : [[TRANSFORM_VENDOR, translate]]; } /** @@ -153,12 +185,12 @@ function assembleCssText(tooltipModel: Model, enableTransition?: if (backgroundColor) { if (env.canvasSupported) { - cssText.push('background-Color:' + backgroundColor); + cssText.push('background-color:' + backgroundColor); } else { // for ie cssText.push( - 'background-Color:#' + toHex(backgroundColor) + 'background-color:#' + toHex(backgroundColor) ); cssText.push('filter:alpha(opacity=70)'); } @@ -272,7 +304,8 @@ class TooltipHTMLContent { document.body.appendChild(el); } else { - container.appendChild(el); + // PENDING + container.prepend(el); } this._container = container; @@ -324,10 +357,9 @@ class TooltipHTMLContent { // FIXME // Move this logic to ec main? const container = this._container; - const stl = (container as any).currentStyle - || document.defaultView.getComputedStyle(container); + const position = getComputedStyle(container, 'position'); const domStyle = container.style; - if (domStyle.position !== 'absolute' && stl.position !== 'absolute') { + if (domStyle.position !== 'absolute' && position !== 'absolute') { domStyle.position = 'relative'; } @@ -347,25 +379,25 @@ class TooltipHTMLContent { clearTimeout(this._hideTimeout); clearTimeout(this._longHideTimeout); const el = this.el; + const style = el.style; const styleCoord = this._styleCoord; - const offset = el.offsetHeight / 2; - nearPointColor = convertToColorString(nearPointColor); - el.style.cssText = gCssText + assembleCssText(tooltipModel, !this._firstShow, this._longHide) - // Because of the reason described in: - // http://stackoverflow.com/questions/21125587/css3-transition-not-working-in-chrome-anymore - // we should set initial value to `left` and `top`. - + ';left:' + styleCoord[0] + 'px;top:' + (styleCoord[1] - offset) + 'px;' - + `border-color: ${nearPointColor};` - + (tooltipModel.get('extraCssText') || ''); - - el.style.display = el.innerHTML ? 'block' : 'none'; - - // 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. - el.style.pointerEvents = this._enterable ? 'auto' : 'none'; + if (!el.innerHTML) { + style.display = 'none'; + } + else { + style.cssText = gCssText + + assembleCssText(tooltipModel, !this._firstShow, this._longHide) + // initial transform + + assembleTransform(el, 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-event:${this._enterable ? 'auto' : 'none'}`; + } this._show = true; this._firstShow = false; @@ -421,10 +453,13 @@ class TooltipHTMLContent { if (styleCoord[0] != null && styleCoord[1] != null) { const style = this.el.style; - // If using float on style, the final width of the dom might - // keep changing slightly while mouse move. So `toFixed(0)` them. - style.left = styleCoord[0].toFixed(0) + 'px'; - style.top = styleCoord[1].toFixed(0) + 'px'; + const transforms = assembleTransform( + this.el, + styleCoord[0], styleCoord[1] + ) as string[][]; + each(transforms, (transform) => { + style[transform[0] as any] = transform[1]; + }); } } @@ -444,8 +479,10 @@ class TooltipHTMLContent { } hide() { - this.el.style.visibility = 'hidden'; - this.el.style.opacity = '0'; + 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; } @@ -478,12 +515,10 @@ class TooltipHTMLContent { // Consider browser compatibility. // IE8 does not support getComputedStyle. - if (document.defaultView && document.defaultView.getComputedStyle) { - const stl = document.defaultView.getComputedStyle(this.el); - if (stl) { - width += parseInt(stl.borderLeftWidth, 10) + parseInt(stl.borderRightWidth, 10); - height += parseInt(stl.borderTopWidth, 10) + parseInt(stl.borderBottomWidth, 10); - } + const stl = getComputedStyle(this.el); + if (stl) { + width += parseInt(stl.borderLeftWidth, 10) + parseInt(stl.borderRightWidth, 10); + height += parseInt(stl.borderTopWidth, 10) + parseInt(stl.borderBottomWidth, 10); } return {width: width, height: height}; diff --git a/src/component/tooltip/TooltipView.ts b/src/component/tooltip/TooltipView.ts index cd52e2b1e5..6a6da1e74e 100644 --- a/src/component/tooltip/TooltipView.ts +++ b/src/component/tooltip/TooltipView.ts @@ -718,6 +718,7 @@ class TooltipView extends ComponentView { tooltipModel.get('trigger'), tooltipModel.get('borderColor') ); + const nearPointColor = nearPoint.color; if (formatter && zrUtil.isString(formatter)) { const useUTC = tooltipModel.ecModel.get('useUTC'); @@ -732,7 +733,7 @@ class TooltipView extends ComponentView { else if (zrUtil.isFunction(formatter)) { const callback = bind(function (cbTicket: string, html: string | HTMLElement[]) { if (cbTicket === this._ticket) { - tooltipContent.setContent(html, markupStyleCreator, tooltipModel, nearPoint.color, positionExpr); + tooltipContent.setContent(html, markupStyleCreator, tooltipModel, nearPointColor, positionExpr); this._updatePosition( tooltipModel, positionExpr, x, y, tooltipContent, params, el ); @@ -742,8 +743,8 @@ class TooltipView extends ComponentView { html = formatter(params, asyncTicket, callback); } - tooltipContent.setContent(html, markupStyleCreator, tooltipModel, nearPoint.color, positionExpr); - tooltipContent.show(tooltipModel, nearPoint.color); + tooltipContent.setContent(html, markupStyleCreator, tooltipModel, nearPointColor, positionExpr); + tooltipContent.show(tooltipModel, nearPointColor); this._updatePosition( tooltipModel, positionExpr, x, y, tooltipContent, params, el ); diff --git a/src/component/tooltip/helper.ts b/src/component/tooltip/helper.ts index 352005662a..fa510b9e6c 100644 --- a/src/component/tooltip/helper.ts +++ b/src/component/tooltip/helper.ts @@ -19,6 +19,10 @@ import { TooltipOption } from './TooltipModel'; import Model from '../../model/Model'; +import { toCamelCase } from '../../util/format'; +import env from 'zrender/src/core/env'; + +/* global document */ export function shouldTooltipConfine(tooltipModel: Model): boolean { const confineOption = tooltipModel.get('confine'); @@ -27,3 +31,43 @@ export function shouldTooltipConfine(tooltipModel: Model): boolea // In richText mode, the outside part can not be visible. : tooltipModel.get('renderMode') === 'richText'; } + +function testStyle(styleProps: string[]): string | undefined { + if (!env.domSupported) { + return; + } + const style = document.documentElement.style; + for (let i = 0, len = styleProps.length; i < len; i++) { + if (styleProps[i] in style) { + return styleProps[i]; + } + } +} + +export const TRANSFORM_VENDOR = testStyle( + ['transform', 'webkitTransform', 'OTransform', 'MozTransform', 'msTransform'] +); + +export const TRANSITION_VENDOR = testStyle( + ['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition'] +); + +export function toCSSVendorPrefix(styleVendor: string, styleProp: string) { + if (!styleVendor) { + return styleProp; + } + styleProp = toCamelCase(styleProp, true); + const idx = styleVendor.indexOf(styleProp); + styleVendor = idx === -1 + ? styleProp + : `-${styleVendor.slice(0, idx)}-${styleProp}`; + return styleVendor.toLowerCase(); +} + +export function getComputedStyle(el: HTMLElement, style?: string) { + const stl = (el as any).currentStyle + || (document.defaultView && document.defaultView.getComputedStyle(el)); + return stl + ? style ? stl[style] : stl + : null; +}