/
githubmirror
/
echarts
Обзор
Документация
Войти
/
githubmirror
/
echarts
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/component/toolbox/ToolboxView.ts
407 строк
16 KB
100pah
fix(toolbox): Simplify toolbox and fix that toolbox throw error when remove DataZoom feature.
10 мар 2026, 16:39
10 мар 2026, 16:39
6de824d
Код
Авторство
О чём код?
/* * 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 * as textContain from 'zrender/src/contain/text'; import * as graphic from '../../util/graphic'; import { enterEmphasis, leaveEmphasis } from '../../util/states'; import Model from '../../model/Model'; import DataDiffer from '../../data/DataDiffer'; import * as listComponentHelper from '../helper/listComponent'; import ComponentView from '../../view/Component'; import ToolboxModel from './ToolboxModel'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { DisplayState, Dictionary, Payload, NullUndefined } from '../../util/types'; import { ToolboxFeature, getFeature, ToolboxFeatureModel, ToolboxFeatureOption, UserDefinedToolboxFeature } from './featureManager'; import { getUID } from '../../util/component'; import Displayable from 'zrender/src/graphic/Displayable'; import ZRText from 'zrender/src/graphic/Text'; import { getFont } from '../../label/labelStyle'; import { box, createBoxLayoutReference, getLayoutRect, positionElement } from '../../util/layout'; import tokens from '../../visual/tokens'; import { bind, createHashMap, curry, each, filter, HashMap, isFunction, isString } from 'zrender/src/core/util'; type IconPath = ToolboxFeatureModel['iconPaths'][string]; type FeatureName = string; type ExtendedPath = IconPath & { __title: string }; class ToolboxView extends ComponentView { static type = 'toolbox' as const; /** * Current enabled features, including only features having `show: true`. */ _features: HashMap<ToolboxFeature | UserDefinedToolboxFeature | NullUndefined, FeatureName>; /** * Current enabled feature names, including only features having `show: true`. */ _featureNames: FeatureName[]; render( toolboxModel: ToolboxModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload & { newTitle?: ToolboxFeatureOption['title'] } ) { const group = this.group; group.removeAll(); if (!toolboxModel.get('show')) { return; } const itemSize = +toolboxModel.get('itemSize'); const isVertical = toolboxModel.get('orient') === 'vertical'; const featureOpts = toolboxModel.get('feature') || {}; const features = this._features || (this._features = createHashMap()); const newFeatureNames: FeatureName[] = []; // Includes both `show: true/false`. each(featureOpts, function (opt, name) { newFeatureNames.push(name); }); // Diff by feature name. (new DataDiffer(this._featureNames || [], newFeatureNames)) .add(processFeature) .update(processFeature) .remove(curry(processFeature, null)) .execute(); // Keep for diff. this._featureNames = filter(newFeatureNames, function (name) { return features.hasKey(name); }); function processFeature(newIndex: number | NullUndefined, oldIndex?: number | NullUndefined) { const isDiffAdd = newIndex != null && oldIndex == null; const isDiffUpdate = newIndex != null && oldIndex != null; const isDiffRemove = newIndex == null; const featureName = (isDiffAdd || isDiffUpdate) ? newFeatureNames[newIndex] : newFeatureNames[oldIndex]; const featureOpt = featureOpts[featureName]; const featureModel = (isDiffAdd || isDiffUpdate) ? new Model(featureOpt, toolboxModel, ecModel) as ToolboxFeatureModel : null; // `.get('show')` Also considered UserDefinedToolboxFeature const isFeatureShow = featureModel && featureModel.get('show'); let feature: ToolboxFeature | UserDefinedToolboxFeature; if (isDiffAdd) { // DIFF_ADD if (!isFeatureShow) { return; } if (isUserFeatureName(featureName)) { feature = { onclick: featureModel.option.onclick, featureName: featureName } as UserDefinedToolboxFeature; } else { const Feature = getFeature(featureName); if (!Feature) { return; } feature = new Feature(); } features.set(featureName, feature); } else { // DIFF_UPDATE or DIFF_REMOVE feature = features.get(featureName); } if (isDiffRemove || !isFeatureShow) { if (isTooltipFeature(feature) && feature.dispose) { feature.dispose(ecModel, api); } features.removeKey(featureName); return; } // FIX#11236, merge feature title from MagicType newOption. TODO: consider seriesIndex ? if (payload && payload.newTitle != null && payload.featureName === featureName) { // FIXME: ec option should not be modified here. featureOpt.title = payload.newTitle; } if (isDiffAdd) { feature.uid = getUID('toolbox-feature'); } feature.model = featureModel; feature.ecModel = ecModel; feature.api = api; createIconPaths(featureModel, feature, featureName); featureModel.setIconStatus = function (this: ToolboxFeatureModel, iconName: string, status: DisplayState) { const option = this.option; const iconPaths = this.iconPaths; option.iconStatus = option.iconStatus || {}; option.iconStatus[iconName] = status; if (iconPaths[iconName]) { (status === 'emphasis' ? enterEmphasis : leaveEmphasis)(iconPaths[iconName]); } }; if (isTooltipFeature(feature) && feature.render) { feature.render(featureModel, ecModel, api, payload); } } function createIconPaths( featureModel: ToolboxFeatureModel, feature: ToolboxFeature | UserDefinedToolboxFeature, featureName: string ) { const iconStyleModel = featureModel.getModel('iconStyle'); const iconStyleEmphasisModel = featureModel.getModel(['emphasis', 'iconStyle']); // If one feature has multiple icons, they are organized as // { // icon: { // foo: '', // bar: '' // }, // title: { // foo: '', // bar: '' // } // } const icons = (feature instanceof ToolboxFeature && feature.getIcons) ? feature.getIcons() : featureModel.get('icon'); const titles = featureModel.get('title') || {}; let iconsMap: Dictionary<string>; let titlesMap: Dictionary<string>; if (isString(icons)) { iconsMap = {}; iconsMap[featureName] = icons; } else { iconsMap = icons; } if (isString(titles)) { titlesMap = {}; titlesMap[featureName] = titles as string; } else { titlesMap = titles; } const iconPaths: ToolboxFeatureModel['iconPaths'] = featureModel.iconPaths = {}; each(iconsMap, function (iconStr, iconName) { const path = graphic.createIcon( iconStr, {}, { x: -itemSize / 2, y: -itemSize / 2, width: itemSize, height: itemSize } ) as Displayable; // TODO handling image path.setStyle(iconStyleModel.getItemStyle()); const pathEmphasisState = path.ensureState('emphasis'); pathEmphasisState.style = iconStyleEmphasisModel.getItemStyle(); // Text position calculation // TODO: extract `textStyle` from `iconStyle` and use `createTextStyle` const textContent = new ZRText({ style: { text: titlesMap[iconName], align: iconStyleEmphasisModel.get('textAlign'), borderRadius: iconStyleEmphasisModel.get('textBorderRadius'), padding: iconStyleEmphasisModel.get('textPadding'), fill: null, font: getFont({ fontStyle: iconStyleEmphasisModel.get('textFontStyle'), fontFamily: iconStyleEmphasisModel.get('textFontFamily'), fontSize: iconStyleEmphasisModel.get('textFontSize'), fontWeight: iconStyleEmphasisModel.get('textFontWeight') }, ecModel) }, ignore: true }); path.setTextContent(textContent); graphic.setTooltipConfig({ el: path, componentModel: toolboxModel, itemName: iconName, formatterParamsExtra: { title: titlesMap[iconName] } }); (path as ExtendedPath).__title = titlesMap[iconName]; (path as graphic.Path).on('mouseover', function () { // Should not reuse above hoverStyle, which might be modified. const hoverStyle = iconStyleEmphasisModel.getItemStyle(); const defaultTextPosition = isVertical ? ( toolboxModel.get('right') == null && toolboxModel.get('left') !== 'right' ? 'right' as const : 'left' as const ) : ( toolboxModel.get('bottom') == null && toolboxModel.get('top') !== 'bottom' ? 'bottom' as const : 'top' as const ); textContent.setStyle({ fill: (iconStyleEmphasisModel.get('textFill') || hoverStyle.fill || hoverStyle.stroke || tokens.color.neutral99) as string, backgroundColor: iconStyleEmphasisModel.get('textBackgroundColor') }); path.setTextConfig({ position: iconStyleEmphasisModel.get('textPosition') || defaultTextPosition }); textContent.ignore = !toolboxModel.get('showTitle'); // Use enterEmphasis and leaveEmphasis provide by ec. // There are flags managed by the echarts. api.enterEmphasis(this); }) .on('mouseout', function () { if (featureModel.get(['iconStatus', iconName]) !== 'emphasis') { api.leaveEmphasis(this); } textContent.hide(); }); (featureModel.get(['iconStatus', iconName]) === 'emphasis' ? enterEmphasis : leaveEmphasis)(path); group.add(path); (path as graphic.Path).on('click', bind( feature.onclick, feature, ecModel, api, iconName )); iconPaths[iconName] = path; }); } const refContainer = createBoxLayoutReference(toolboxModel, api).refContainer; const boxLayoutParams = toolboxModel.getBoxLayoutParams(); const padding = toolboxModel.get('padding'); const viewRect = getLayoutRect( boxLayoutParams, refContainer, padding ); box( toolboxModel.get('orient'), group, toolboxModel.get('itemGap'), viewRect.width, viewRect.height ); positionElement( group, boxLayoutParams, refContainer, padding ); // Render background after group is layout // FIXME group.add(listComponentHelper.makeBackground(group.getBoundingRect(), toolboxModel)); // Adjust icon title positions to avoid them out of screen isVertical || group.eachChild(function (icon: IconPath) { const titleText = (icon as ExtendedPath).__title; // const hoverStyle = icon.hoverStyle; // TODO simplify code? const emphasisState = icon.ensureState('emphasis'); const emphasisTextConfig = emphasisState.textConfig || (emphasisState.textConfig = {}); const textContent = icon.getTextContent(); const emphasisTextState = textContent && textContent.ensureState('emphasis'); // May be background element if (emphasisTextState && !isFunction(emphasisTextState) && titleText) { const emphasisTextStyle = emphasisTextState.style || (emphasisTextState.style = {}); const rect = textContain.getBoundingRect( titleText, ZRText.makeFont(emphasisTextStyle) ); const offsetX = icon.x + group.x; const offsetY = icon.y + group.y + itemSize; let needPutOnTop = false; if (offsetY + rect.height > api.getHeight()) { emphasisTextConfig.position = 'top'; needPutOnTop = true; } const topOffset = needPutOnTop ? (-5 - rect.height) : (itemSize + 10); if (offsetX + rect.width / 2 > api.getWidth()) { emphasisTextConfig.position = ['100%', topOffset]; emphasisTextStyle.align = 'right'; } else if (offsetX - rect.width / 2 < 0) { emphasisTextConfig.position = [0, topOffset]; emphasisTextStyle.align = 'left'; } } }); } updateView( toolboxModel: ToolboxModel, ecModel: GlobalModel, api: ExtensionAPI, payload: unknown ) { each(this._features, function (feature) { feature && feature instanceof ToolboxFeature && feature.updateView && feature.updateView(feature.model, ecModel, api, payload); }); } dispose(ecModel: GlobalModel, api: ExtensionAPI) { each(this._features, function (feature) { feature && feature instanceof ToolboxFeature && feature.dispose && feature.dispose(ecModel, api); }); } } function isUserFeatureName(featureName: string): boolean { return featureName.indexOf('my') === 0; } function isTooltipFeature(feature: ToolboxFeature | UserDefinedToolboxFeature): feature is ToolboxFeature { return feature instanceof ToolboxFeature; } export default ToolboxView;