/* * 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 env from 'zrender/src/core/env'; import { enableClassExtend, ExtendableConstructor, enableClassCheck, CheckableConstructor } from '../util/clazz'; import {AreaStyleMixin} from './mixin/areaStyle'; import TextStyleMixin from './mixin/textStyle'; import {LineStyleMixin} from './mixin/lineStyle'; import {ItemStyleMixin} from './mixin/itemStyle'; import GlobalModel from './Global'; import { AnimationOptionMixin, ModelOption } from '../util/types'; import { Dictionary } from 'zrender/src/core/types'; import { mixin, clone, merge } from 'zrender/src/core/util'; // Since model.option can be not only `Dictionary` but also primary types, // we do this conditional type to avoid getting type 'never'; // type Key = Opt extends Dictionary // ? keyof Opt : string; // type Value = Opt extends Dictionary // ? (R extends keyof Opt ? Opt[R] : ModelOption) // : ModelOption; // eslint-disable-next-line @typescript-eslint/no-unused-vars interface Model extends LineStyleMixin, ItemStyleMixin, TextStyleMixin, AreaStyleMixin {} class Model { // TODO: TYPE use unknown instead of any? // [Caution]: Because this class or desecendants can be used as `XXX.extend(subProto)`, // the class members must not be initialized in constructor or declaration place. // Otherwise there is bad case: // class A {xxx = 1;} // enableClassExtend(A); // class B extends A {} // var C = B.extend({xxx: 5}); // var c = new C(); // console.log(c.xxx); // expect 5 but always 1. parentModel: Model; ecModel: GlobalModel; option: Opt; // TODO Opt should only be object. constructor(option?: Opt, parentModel?: Model, ecModel?: GlobalModel) { this.parentModel = parentModel; this.ecModel = ecModel; this.option = option; // Simple optimization // if (this.init) { // if (arguments.length <= 4) { // this.init(option, parentModel, ecModel, extraOpt); // } // else { // this.init.apply(this, arguments); // } // } } init(option: Opt, parentModel?: Model, ecModel?: GlobalModel, ...rest: any): void {} /** * Merge the input option to me. */ mergeOption(option: Opt, ecModel?: GlobalModel): void { merge(this.option, option, true); } // FIXME:TS consider there is parentModel, // return type have to be ModelOption or can be Option? // (Is there any chance that parentModel value type is different?) get( path: R, ignoreParent?: boolean ): Opt[R]; get( path: readonly [R], ignoreParent?: boolean ): Opt[R]; get( path: readonly [R, S], ignoreParent?: boolean ): Opt[R][S]; get( path: readonly [R, S, T], ignoreParent?: boolean ): Opt[R][S][T]; // `path` can be 'a.b.c', so the return value type have to be `ModelOption` // TODO: TYPE strict key check? // get(path: string | string[], ignoreParent?: boolean): ModelOption; get(path: string | readonly string[], ignoreParent?: boolean): ModelOption { if (path == null) { return this.option; } return this._doGet( this.parsePath(path), !ignoreParent && this.parentModel ); } getShallow( key: R, ignoreParent?: boolean ): Opt[R] { const option = this.option; let val = option == null ? option : option[key]; if (val == null && !ignoreParent) { const parentModel = this.parentModel; if (parentModel) { // FIXME:TS do not know how to make it works val = parentModel.getShallow(key); } } return val as Opt[R]; } // TODO At most 3 depth? getModel( path: R, parentModel?: Model ): Model; getModel( path: readonly [R], parentModel?: Model ): Model; getModel( path: readonly [R, S], parentModel?: Model ): Model; getModel( path: readonly [Ra] | readonly [Rb, S], parentModel?: Model ): Model | Model; getModel( path: readonly [R, S, T], parentModel?: Model ): Model; // `path` can be 'a.b.c', so the return value type have to be `Model` // getModel(path: string | string[], parentModel?: Model): Model; // TODO 'a.b.c' is deprecated getModel(path: string | readonly string[], parentModel?: Model): Model { const hasPath = path != null; const pathFinal = hasPath ? this.parsePath(path) : null; const obj = hasPath ? this._doGet(pathFinal) : this.option; parentModel = parentModel || ( this.parentModel && this.parentModel.getModel(this.resolveParentPath(pathFinal) as [string]) ); return new Model(obj, parentModel, this.ecModel); } /** * If model has option */ isEmpty(): boolean { return this.option == null; } restoreData(): void {} // Pending clone(): Model { const Ctor = this.constructor; return new (Ctor as any)(clone(this.option)); } // setReadOnly(properties): void { // clazzUtil.setReadOnly(this, properties); // } // If path is null/undefined, return null/undefined. parsePath(path: string | readonly string[]): readonly string[] { if (typeof path === 'string') { return path.split('.'); } return path; } // Resolve path for parent. Perhaps useful when parent use a different property. // Default to be a identity resolver. // Can be modified to a different resolver. resolveParentPath(path: readonly string[]): string[] { return path as string[]; } // FIXME:TS check whether put this method here isAnimationEnabled(): boolean { if (!env.node && this.option) { if ((this.option as AnimationOptionMixin).animation != null) { return !!(this.option as AnimationOptionMixin).animation; } else if (this.parentModel) { return this.parentModel.isAnimationEnabled(); } } } private _doGet(pathArr: readonly string[], parentModel?: Model>) { let obj = this.option; if (!pathArr) { return obj; } for (let i = 0; i < pathArr.length; i++) { // Ignore empty if (!pathArr[i]) { continue; } // obj could be number/string/... (like 0) obj = (obj && typeof obj === 'object') ? (obj as ModelOption)[pathArr[i] as keyof ModelOption] : null; if (obj == null) { break; } } if (obj == null && parentModel) { obj = parentModel._doGet( this.resolveParentPath(pathArr) as [string], parentModel.parentModel ) as any; } return obj; } }; type ModelConstructor = typeof Model & ExtendableConstructor & CheckableConstructor; // Enable Model.extend. enableClassExtend(Model as ModelConstructor); enableClassCheck(Model as ModelConstructor); mixin(Model, LineStyleMixin); mixin(Model, ItemStyleMixin); mixin(Model, AreaStyleMixin); mixin(Model, TextStyleMixin); export default Model;