| /* |
| * 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; |
| } |
| } |