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 * as zrUtil from 'zrender/src/core/util'; | |
| import * as modelUtil from '../../util/model'; | |
| import { | |
| ComponentOption, | |
| BoxLayoutOptionMixin, | |
| Dictionary, | |
| ZRStyleProps, | |
| OptionId, | |
| CommonTooltipOption, | |
| AnimationOptionMixin, | |
| AnimationOption | |
| } from '../../util/types'; | |
| import ComponentModel from '../../model/Component'; | |
| import Element, { ElementTextConfig } from 'zrender/src/Element'; | |
| import Displayable from 'zrender/src/graphic/Displayable'; | |
| import { PathProps, PathStyleProps } from 'zrender/src/graphic/Path'; | |
| import { ImageStyleProps, ImageProps } from 'zrender/src/graphic/Image'; | |
| import { TextStyleProps, TextProps } from 'zrender/src/graphic/Text'; | |
| import GlobalModel from '../../model/Global'; | |
| import { copyLayoutParams, mergeLayoutParam } from '../../util/layout'; | |
| import { TransitionOptionMixin } from '../../animation/customGraphicTransition'; | |
| import { ElementKeyframeAnimationOption } from '../../animation/customGraphicKeyframeAnimation'; | |
| import { GroupProps } from 'zrender/src/graphic/Group'; | |
| import { TransformProp } from 'zrender/src/core/Transformable'; | |
| import { ElementEventNameWithOn } from 'zrender/src/core/types'; | |
| interface GraphicComponentBaseElementOption extends | |
| Partial<Pick< | |
| Element, TransformProp | | |
| 'silent' | | |
| 'ignore' | | |
| 'textConfig' | | |
| 'draggable' | | |
| ElementEventNameWithOn | |
| >>, | |
| /** | |
| * left/right/top/bottom: (like 12, '22%', 'center', default undefined) | |
| * If left/right is set, shape.x/shape.cx/position will not be used. | |
| * If top/bottom is set, shape.y/shape.cy/position will not be used. | |
| * This mechanism is useful when you want to position a group/element | |
| * against the right side or the center of this container. | |
| */ | |
| Partial<Pick<BoxLayoutOptionMixin, 'left' | 'right' | 'top' | 'bottom'>> { | |
| /** | |
| * element type, mandatory. | |
| * Only can be omit if call setOption not at the first time and perform merge. | |
| */ | |
| type?: string; | |
| id?: OptionId; | |
| name?: string; | |
| // Only internal usage. Use specified value does NOT make sense. | |
| parentId?: OptionId; | |
| parentOption?: GraphicComponentElementOption; | |
| children?: GraphicComponentElementOption[]; | |
| hv?: [boolean, boolean]; | |
| /** | |
| * bounding: (enum: 'all' (default) | 'raw') | |
| * Specify how to calculate boundingRect when locating. | |
| * 'all': Get uioned and transformed boundingRect | |
| * from both itself and its descendants. | |
| * This mode simplies confining a group of elements in the bounding | |
| * of their ancester container (e.g., using 'right: 0'). | |
| * 'raw': Only use the boundingRect of itself and before transformed. | |
| * This mode is similar to css behavior, which is useful when you | |
| * want an element to be able to overflow its container. (Consider | |
| * a rotated circle needs to be located in a corner.) | |
| */ | |
| bounding?: 'raw' | 'all'; | |
| /** | |
| * info: custom info. enables user to mount some info on elements and use them | |
| * in event handlers. Update them only when user specified, otherwise, remain. | |
| */ | |
| info?: GraphicExtraElementInfo; | |
| // `false` means remove the clipPath | |
| clipPath?: Omit<GraphicComponentZRPathOption, 'clipPath'> | false; | |
| textContent?: Omit<GraphicComponentTextOption, 'clipPath'>; | |
| textConfig?: ElementTextConfig; | |
| $action?: 'merge' | 'replace' | 'remove'; | |
| tooltip?: CommonTooltipOption<unknown>; | |
| enterAnimation?: AnimationOption | |
| updateAnimation?: AnimationOption | |
| leaveAnimation?: AnimationOption | |
| }; | |
| export interface GraphicComponentDisplayableOption extends | |
| GraphicComponentBaseElementOption, | |
| Partial<Pick<Displayable, 'zlevel' | 'z' | 'z2' | 'invisible' | 'cursor'>> { | |
| style?: ZRStyleProps | |
| z2?: number | |
| } | |
| // TODO: states? | |
| // interface GraphicComponentDisplayableOptionOnState extends Partial<Pick< | |
| // Displayable, TransformProp | 'textConfig' | 'z2' | |
| // >> { | |
| // style?: ZRStyleProps; | |
| // } | |
| export interface GraphicComponentGroupOption | |
| extends GraphicComponentBaseElementOption, TransitionOptionMixin<GroupProps> { | |
| type?: 'group'; | |
| /** | |
| * width/height: (can only be pixel value, default 0) | |
| * Is only used to specify container (group) size, if needed. And | |
| * cannot be a percentage value (like '33%'). See the reason in the | |
| * layout algorithm below. | |
| */ | |
| width?: number; | |
| height?: number; | |
| // TODO: Can only set focus, blur on the root element. | |
| // children: Omit<GraphicComponentElementOption, 'focus' | 'blurScope'>[]; | |
| children: GraphicComponentElementOption[]; | |
| keyframeAnimation?: ElementKeyframeAnimationOption<GroupProps> | ElementKeyframeAnimationOption<GroupProps>[] | |
| }; | |
| export interface GraphicComponentZRPathOption | |
| extends GraphicComponentDisplayableOption, TransitionOptionMixin<PathProps> { | |
| shape?: PathProps['shape'] & TransitionOptionMixin<PathProps['shape']>; | |
| style?: PathStyleProps & TransitionOptionMixin<PathStyleProps> | |
| keyframeAnimation?: ElementKeyframeAnimationOption<PathProps> | ElementKeyframeAnimationOption<PathProps>[]; | |
| } | |
| export interface GraphicComponentImageOption | |
| extends GraphicComponentDisplayableOption, TransitionOptionMixin<ImageProps> { | |
| type?: 'image'; | |
| style?: ImageStyleProps & TransitionOptionMixin<ImageStyleProps>; | |
| keyframeAnimation?: ElementKeyframeAnimationOption<ImageProps> | ElementKeyframeAnimationOption<ImageProps>[]; | |
| } | |
| // TODO: states? | |
| // interface GraphicComponentImageOptionOnState extends GraphicComponentDisplayableOptionOnState { | |
| // style?: ImageStyleProps; | |
| // } | |
| export interface GraphicComponentTextOption | |
| extends Omit<GraphicComponentDisplayableOption, 'textContent' | 'textConfig'>, TransitionOptionMixin<TextProps> { | |
| type?: 'text'; | |
| style?: TextStyleProps & TransitionOptionMixin<TextStyleProps>; | |
| keyframeAnimation?: ElementKeyframeAnimationOption<TextProps> | ElementKeyframeAnimationOption<TextProps>[]; | |
| } | |
| export type GraphicComponentElementOption = | |
| GraphicComponentGroupOption | | |
| GraphicComponentZRPathOption | | |
| GraphicComponentImageOption | | |
| GraphicComponentTextOption; | |
| // type GraphicComponentElementOptionOnState = | |
| // GraphicComponentDisplayableOptionOnState | |
| // | GraphicComponentImageOptionOnState; | |
| type GraphicExtraElementInfo = Dictionary<unknown>; | |
| export type ElementMap = zrUtil.HashMap<Element, string>; | |
| export type GraphicComponentLooseOption = (GraphicComponentOption | GraphicComponentElementOption) & { | |
| mainType?: 'graphic'; | |
| }; | |
| export interface GraphicComponentOption extends ComponentOption, AnimationOptionMixin { | |
| // Note: elements is always behind its ancestors in this elements array. | |
| elements?: GraphicComponentElementOption[]; | |
| }; | |
| export function setKeyInfoToNewElOption( | |
| resultItem: ReturnType<typeof modelUtil.mappingToExists>[number], | |
| newElOption: GraphicComponentElementOption | |
| ): void { | |
| const existElOption = resultItem.existing as GraphicComponentElementOption; | |
| // Set id and type after id assigned. | |
| newElOption.id = resultItem.keyInfo.id; | |
| !newElOption.type && existElOption && (newElOption.type = existElOption.type); | |
| // Set parent id if not specified | |
| if (newElOption.parentId == null) { | |
| const newElParentOption = newElOption.parentOption; | |
| if (newElParentOption) { | |
| newElOption.parentId = newElParentOption.id; | |
| } | |
| else if (existElOption) { | |
| newElOption.parentId = existElOption.parentId; | |
| } | |
| } | |
| // Clear | |
| newElOption.parentOption = null; | |
| } | |
| function isSetLoc( | |
| obj: GraphicComponentElementOption, | |
| props: ('left' | 'right' | 'top' | 'bottom')[] | |
| ): boolean { | |
| let isSet; | |
| zrUtil.each(props, function (prop) { | |
| obj[prop] != null && obj[prop] !== 'auto' && (isSet = true); | |
| }); | |
| return isSet; | |
| } | |
| function mergeNewElOptionToExist( | |
| existList: GraphicComponentElementOption[], | |
| index: number, | |
| newElOption: GraphicComponentElementOption | |
| ): void { | |
| // Update existing options, for `getOption` feature. | |
| const newElOptCopy = zrUtil.extend({}, newElOption); | |
| const existElOption = existList[index]; | |
| const $action = newElOption.$action || 'merge'; | |
| if ($action === 'merge') { | |
| if (existElOption) { | |
| if (__DEV__) { | |
| const newType = newElOption.type; | |
| zrUtil.assert( | |
| !newType || existElOption.type === newType, | |
| 'Please set $action: "replace" to change `type`' | |
| ); | |
| } | |
| // We can ensure that newElOptCopy and existElOption are not | |
| // the same object, so `merge` will not change newElOptCopy. | |
| zrUtil.merge(existElOption, newElOptCopy, true); | |
| // Rigid body, use ignoreSize. | |
| mergeLayoutParam(existElOption, newElOptCopy, { ignoreSize: true }); | |
| // Will be used in render. | |
| copyLayoutParams(newElOption, existElOption); | |
| // Copy transition info to new option so it can be used in the transition. | |
| // DO IT AFTER merge | |
| copyTransitionInfo(newElOption, existElOption); | |
| copyTransitionInfo(newElOption, existElOption, 'shape'); | |
| copyTransitionInfo(newElOption, existElOption, 'style'); | |
| copyTransitionInfo(newElOption, existElOption, 'extra'); | |
| // Copy clipPath | |
| newElOption.clipPath = existElOption.clipPath; | |
| } | |
| else { | |
| existList[index] = newElOptCopy; | |
| } | |
| } | |
| else if ($action === 'replace') { | |
| existList[index] = newElOptCopy; | |
| } | |
| else if ($action === 'remove') { | |
| // null will be cleaned later. | |
| existElOption && (existList[index] = null); | |
| } | |
| } | |
| const TRANSITION_PROPS_TO_COPY = ['transition', 'enterFrom', 'leaveTo']; | |
| const ROOT_TRANSITION_PROPS_TO_COPY = | |
| TRANSITION_PROPS_TO_COPY.concat(['enterAnimation', 'updateAnimation', 'leaveAnimation']); | |
| function copyTransitionInfo( | |
| target: GraphicComponentElementOption, | |
| source: GraphicComponentElementOption, | |
| targetProp?: string | |
| ) { | |
| if (targetProp) { | |
| if (!(target as any)[targetProp] | |
| && (source as any)[targetProp] | |
| ) { | |
| // TODO avoid creating this empty object when there is no transition configuration. | |
| (target as any)[targetProp] = {}; | |
| } | |
| target = (target as any)[targetProp]; | |
| source = (source as any)[targetProp]; | |
| } | |
| if (!target || !source) { | |
| return; | |
| } | |
| const props = targetProp ? TRANSITION_PROPS_TO_COPY : ROOT_TRANSITION_PROPS_TO_COPY; | |
| for (let i = 0; i < props.length; i++) { | |
| const prop = props[i]; | |
| if ((target as any)[prop] == null && (source as any)[prop] != null) { | |
| (target as any)[prop] = (source as any)[prop]; | |
| } | |
| } | |
| } | |
| function setLayoutInfoToExist( | |
| existItem: GraphicComponentElementOption, | |
| newElOption: GraphicComponentElementOption | |
| ) { | |
| if (!existItem) { | |
| return; | |
| } | |
| existItem.hv = newElOption.hv = [ | |
| // Rigid body, don't care about `width`. | |
| isSetLoc(newElOption, ['left', 'right']), | |
| // Rigid body, don't care about `height`. | |
| isSetLoc(newElOption, ['top', 'bottom']) | |
| ]; | |
| // Give default group size. Otherwise layout error may occur. | |
| if (existItem.type === 'group') { | |
| const existingGroupOpt = existItem as GraphicComponentGroupOption; | |
| const newGroupOpt = newElOption as GraphicComponentGroupOption; | |
| existingGroupOpt.width == null && (existingGroupOpt.width = newGroupOpt.width = 0); | |
| existingGroupOpt.height == null && (existingGroupOpt.height = newGroupOpt.height = 0); | |
| } | |
| } | |
| export class GraphicComponentModel extends ComponentModel<GraphicComponentOption> { | |
| static type = 'graphic'; | |
| type = GraphicComponentModel.type; | |
| preventAutoZ = true; | |
| static defaultOption: GraphicComponentOption = { | |
| elements: [] | |
| // parentId: null | |
| }; | |
| /** | |
| * Save el options for the sake of the performance (only update modified graphics). | |
| * The order is the same as those in option. (ancesters -> descendants) | |
| */ | |
| private _elOptionsToUpdate: GraphicComponentElementOption[]; | |
| mergeOption(option: GraphicComponentOption, ecModel: GlobalModel): void { | |
| // Prevent default merge to elements | |
| const elements = this.option.elements; | |
| this.option.elements = null; | |
| super.mergeOption(option, ecModel); | |
| this.option.elements = elements; | |
| } | |
| optionUpdated(newOption: GraphicComponentOption, isInit: boolean): void { | |
| const thisOption = this.option; | |
| const newList = (isInit ? thisOption : newOption).elements; | |
| const existList = thisOption.elements = isInit ? [] : thisOption.elements; | |
| const flattenedList = [] as GraphicComponentElementOption[]; | |
| this._flatten(newList, flattenedList, null); | |
| const mappingResult = modelUtil.mappingToExists(existList, flattenedList, 'normalMerge'); | |
| // Clear elOptionsToUpdate | |
| const elOptionsToUpdate = this._elOptionsToUpdate = [] as GraphicComponentElementOption[]; | |
| zrUtil.each(mappingResult, function (resultItem, index) { | |
| const newElOption = resultItem.newOption as GraphicComponentElementOption; | |
| if (__DEV__) { | |
| zrUtil.assert( | |
| zrUtil.isObject(newElOption) || resultItem.existing, | |
| 'Empty graphic option definition' | |
| ); | |
| } | |
| if (!newElOption) { | |
| return; | |
| } | |
| elOptionsToUpdate.push(newElOption); | |
| setKeyInfoToNewElOption(resultItem, newElOption); | |
| mergeNewElOptionToExist(existList, index, newElOption); | |
| setLayoutInfoToExist(existList[index], newElOption); | |
| }, this); | |
| // Clean | |
| thisOption.elements = zrUtil.filter(existList, (item) => { | |
| // $action should be volatile, otherwise option gotten from | |
| // `getOption` will contain unexpected $action. | |
| item && delete item.$action; | |
| return item != null; | |
| }); | |
| } | |
| /** | |
| * Convert | |
| * [{ | |
| * type: 'group', | |
| * id: 'xx', | |
| * children: [{type: 'circle'}, {type: 'polygon'}] | |
| * }] | |
| * to | |
| * [ | |
| * {type: 'group', id: 'xx'}, | |
| * {type: 'circle', parentId: 'xx'}, | |
| * {type: 'polygon', parentId: 'xx'} | |
| * ] | |
| */ | |
| private _flatten( | |
| optionList: GraphicComponentElementOption[], | |
| result: GraphicComponentElementOption[], | |
| parentOption: GraphicComponentElementOption | |
| ): void { | |
| zrUtil.each(optionList, function (option) { | |
| if (!option) { | |
| return; | |
| } | |
| if (parentOption) { | |
| option.parentOption = parentOption; | |
| } | |
| result.push(option); | |
| const children = option.children; | |
| // here we don't judge if option.type is `group` | |
| // when new option doesn't provide `type`, it will cause that the children can't be updated. | |
| if (children && children.length) { | |
| this._flatten(children, result, option); | |
| } | |
| // Deleting for JSON output, and for not affecting group creation. | |
| delete option.children; | |
| }, this); | |
| } | |
| // FIXME | |
| // Pass to view using payload? setOption has a payload? | |
| useElOptionsToUpdate(): GraphicComponentElementOption[] { | |
| const els = this._elOptionsToUpdate; | |
| // Clear to avoid render duplicately when zooming. | |
| this._elOptionsToUpdate = null; | |
| return els; | |
| } | |
| } | |