blob: 559c3e96d7c19edbec01bdb45da933722680bc3f [file] [log] [blame]
/*
* 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> = Opt extends Dictionary<any>
? keyof Opt : string;
type Value<Opt, R> = Opt extends Dictionary<any>
? (R extends keyof Opt ? Opt[R] : ModelOption)
: ModelOption;
interface Model<Opt = ModelOption>
extends LineStyleMixin, ItemStyleMixin, TextStyleMixin, AreaStyleMixin {}
class Model<Opt = ModelOption> { // TODO: TYPE use unkown insteadof any?
// [Caution]: Becuase 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<R>?
// (Is there any chance that parentModel value type is different?)
get<R extends keyof Opt>(
path: R, ignoreParent?: boolean
): Opt[R];
get<R extends keyof Opt>(
path: readonly [R], ignoreParent?: boolean
): Opt[R];
get<R extends keyof Opt, S extends keyof Opt[R]>(
path: readonly [R, S], ignoreParent?: boolean
): Opt[R][S];
get<R extends keyof Opt, S extends keyof Opt[R], T extends keyof Opt[R][S]>(
path: readonly [R, S, T], ignoreParent?: boolean
): Opt[R][S][T];
// `path` can be 'xxx.yyy.zzz', 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<R extends keyof Opt>(
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<R extends keyof Opt>(
path: R, parentModel?: Model
): Model<Opt[R]>;
getModel<R extends keyof Opt>(
path: readonly [R], parentModel?: Model
): Model<Opt[R]>;
getModel<R extends keyof Opt, S extends keyof Opt[R]>(
path: readonly [R, S], parentModel?: Model
): Model<Opt[R][S]>;
getModel<Ra extends keyof Opt, Rb extends keyof Opt, S extends keyof Opt[Rb]>(
path: readonly [Ra] | readonly [Rb, S], parentModel?: Model
): Model<Opt[Ra]> | Model<Opt[Rb][S]>;
getModel<R extends keyof Opt, S extends keyof Opt[R], T extends keyof Opt[R][S]>(
path: readonly [R, S, T], parentModel?: Model
): Model<Opt[R][S][T]>;
// `path` can be 'xxx.yyy.zzz', so the return value type have to be `Model<ModelOption>`
// getModel(path: string | string[], parentModel?: Model): Model;
// TODO 'xxx.yyy.zzz' is deprecated
getModel(path: string | readonly string[], parentModel?: Model): Model<any> {
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);
}
/**
* Squash option stack into one.
* parentModel will be removed after squashed.
*
* NOTE: resolveParentPath will not be applied here for simplicity. DON'T use this function
* if resolveParentPath is modified.
*
* @param deepMerge If do deep merge. Default to be false.
*/
// squash(
// deepMerge?: boolean,
// handleCallback?: (func: () => object) => object
// ) {
// const optionStack = [];
// let model: Model = this;
// while (model) {
// if (model.option) {
// optionStack.push(model.option);
// }
// model = model.parentModel;
// }
// const newOption = {} as Opt;
// let option;
// while (option = optionStack.pop()) { // Top down merge
// if (isFunction(option) && handleCallback) {
// option = handleCallback(option);
// }
// if (deepMerge) {
// merge(newOption, option);
// }
// else {
// extend(newOption, option);
// }
// }
// // Remove parentModel
// this.option = newOption;
// this.parentModel = null;
// }
/**
* If model has option
*/
isEmpty(): boolean {
return this.option == null;
}
restoreData(): void {}
// Pending
clone(): Model<Opt> {
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<Dictionary<any>>) {
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;