| /* | 
 | * 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'; | 
 |  | 
 | 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 isVertical = toolboxModel.get('orient') === 'vertical'; | 
 |         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 (zrUtil.isString(icons)) { | 
 |                 iconsMap = {}; | 
 |                 iconsMap[featureName] = icons; | 
 |             } | 
 |             else { | 
 |                 iconsMap = icons; | 
 |             } | 
 |             if (zrUtil.isString(titles)) { | 
 |                 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] | 
 |                     } | 
 |                 }); | 
 |  | 
 |                 (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 || '#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. | 
 |                     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', 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 | 
 |         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 && !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 + 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 | 
 |     ) { | 
 |         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; |