| /* |
| * 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 zrUtil from 'zrender/src/core/util'; |
| 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 } 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 { getECData } from '../../util/innerStore'; |
| |
| type IconPath = ToolboxFeatureModel['iconPaths'][string]; |
| |
| type ExtendedPath = IconPath & { |
| __title: string |
| }; |
| |
| class ToolboxView extends ComponentView { |
| static type = 'toolbox' as const; |
| |
| _features: Dictionary<ToolboxFeature | UserDefinedToolboxFeature>; |
| |
| _featureNames: string[]; |
| |
| 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 featureOpts = toolboxModel.get('feature') || {}; |
| const features = this._features || (this._features = {}); |
| |
| const featureNames: string[] = []; |
| zrUtil.each(featureOpts, function (opt, name) { |
| featureNames.push(name); |
| }); |
| |
| (new DataDiffer(this._featureNames || [], featureNames)) |
| .add(processFeature) |
| .update(processFeature) |
| .remove(zrUtil.curry(processFeature, null)) |
| .execute(); |
| |
| // Keep for diff. |
| this._featureNames = featureNames; |
| |
| function processFeature(newIndex: number, oldIndex?: number) { |
| const featureName = featureNames[newIndex]; |
| const oldName = featureNames[oldIndex]; |
| const featureOpt = featureOpts[featureName]; |
| const featureModel = new Model(featureOpt, toolboxModel, toolboxModel.ecModel) as ToolboxFeatureModel; |
| let feature: ToolboxFeature | UserDefinedToolboxFeature; |
| |
| // FIX#11236, merge feature title from MagicType newOption. TODO: consider seriesIndex ? |
| if (payload && payload.newTitle != null && payload.featureName === featureName) { |
| featureOpt.title = payload.newTitle; |
| } |
| |
| if (featureName && !oldName) { // Create |
| if (isUserFeatureName(featureName)) { |
| feature = { |
| onclick: featureModel.option.onclick, |
| featureName: featureName |
| } as UserDefinedToolboxFeature; |
| } |
| else { |
| const Feature = getFeature(featureName); |
| if (!Feature) { |
| return; |
| } |
| feature = new Feature(); |
| } |
| features[featureName] = feature; |
| } |
| else { |
| feature = features[oldName]; |
| // If feature does not exsit. |
| if (!feature) { |
| return; |
| } |
| } |
| feature.uid = getUID('toolbox-feature'); |
| feature.model = featureModel; |
| feature.ecModel = ecModel; |
| feature.api = api; |
| |
| const isToolboxFeature = feature instanceof ToolboxFeature; |
| if (!featureName && oldName) { |
| isToolboxFeature |
| && (feature as ToolboxFeature).dispose |
| && (feature as ToolboxFeature).dispose(ecModel, api); |
| return; |
| } |
| |
| if (!featureModel.get('show') || (isToolboxFeature && (feature as ToolboxFeature).unusable)) { |
| isToolboxFeature |
| && (feature as ToolboxFeature).remove |
| && (feature as ToolboxFeature).remove(ecModel, api); |
| return; |
| } |
| |
| 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 (feature instanceof ToolboxFeature) { |
| if (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 mutiple icon. they are orginaized 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 (typeof icons === 'string') { |
| iconsMap = {}; |
| iconsMap[featureName] = icons; |
| } |
| else { |
| iconsMap = icons; |
| } |
| if (typeof titles === 'string') { |
| titlesMap = {}; |
| titlesMap[featureName] = titles as string; |
| } |
| else { |
| titlesMap = titles; |
| } |
| const iconPaths: ToolboxFeatureModel['iconPaths'] = featureModel.iconPaths = {}; |
| zrUtil.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 |
| const textContent = new ZRText({ |
| style: { |
| text: titlesMap[iconName], |
| align: iconStyleEmphasisModel.get('textAlign'), |
| borderRadius: iconStyleEmphasisModel.get('textBorderRadius'), |
| padding: iconStyleEmphasisModel.get('textPadding'), |
| fill: null |
| }, |
| ignore: true |
| }); |
| path.setTextContent(textContent); |
| |
| graphic.setTooltipConfig({ |
| el: path, |
| componentModel: toolboxModel, |
| itemName: iconName, |
| formatterParamsExtra: { |
| title: titlesMap[iconName] |
| } |
| }); |
| |
| // graphic.enableHoverEmphasis(path); |
| |
| (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 = toolboxModel.get('orient') === 'vertical' |
| ? (toolboxModel.get('right') == null ? 'right' as const : 'left' as const) |
| : (toolboxModel.get('bottom') == null ? 'bottom' as const : 'top' as const); |
| textContent.setStyle({ |
| fill: (iconStyleEmphasisModel.get('textFill') |
| || hoverStyle.fill || hoverStyle.stroke || '#000') 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. |
| enterEmphasis(this); |
| }) |
| .on('mouseout', function () { |
| if (featureModel.get(['iconStatus', iconName]) !== 'emphasis') { |
| leaveEmphasis(this); |
| } |
| textContent.hide(); |
| }); |
| (featureModel.get(['iconStatus', iconName]) === 'emphasis' ? enterEmphasis : leaveEmphasis)(path); |
| |
| group.add(path); |
| (path as graphic.Path).on('click', zrUtil.bind( |
| feature.onclick, feature, ecModel, api, iconName |
| )); |
| |
| iconPaths[iconName] = path; |
| }); |
| } |
| |
| listComponentHelper.layout(group, toolboxModel, api); |
| // Render background after group is layout |
| // FIXME |
| group.add(listComponentHelper.makeBackground(group.getBoundingRect(), toolboxModel)); |
| |
| // Adjust icon title positions to avoid them out of screen |
| 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.states.emphasis; |
| // May be background element |
| if (emphasisTextState && !zrUtil.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 + 8); |
| 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 |
| ) { |
| zrUtil.each(this._features, function (feature) { |
| feature instanceof ToolboxFeature |
| && feature.updateView && feature.updateView(feature.model, ecModel, api, payload); |
| }); |
| } |
| |
| // updateLayout(toolboxModel, ecModel, api, payload) { |
| // zrUtil.each(this._features, function (feature) { |
| // feature.updateLayout && feature.updateLayout(feature.model, ecModel, api, payload); |
| // }); |
| // }, |
| |
| remove(ecModel: GlobalModel, api: ExtensionAPI) { |
| zrUtil.each(this._features, function (feature) { |
| feature instanceof ToolboxFeature |
| && feature.remove && feature.remove(ecModel, api); |
| }); |
| this.group.removeAll(); |
| } |
| |
| dispose(ecModel: GlobalModel, api: ExtensionAPI) { |
| zrUtil.each(this._features, function (feature) { |
| feature instanceof ToolboxFeature |
| && feature.dispose && feature.dispose(ecModel, api); |
| }); |
| } |
| } |
| |
| |
| function isUserFeatureName(featureName: string): boolean { |
| return featureName.indexOf('my') === 0; |
| } |
| export default ToolboxView; |