blob: 373ea6830d73f91be0f79366e9c0cb77f362b9ec [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 * as zrUtil from 'zrender/src/core/util';
import Model from './Model';
import * as componentUtil from '../util/component';
import {
enableClassManagement,
parseClassType,
isExtendedClass,
ExtendableConstructor,
ClassManager,
mountExtend
} from '../util/clazz';
import {
makeInner, ModelFinderIndexQuery, queryReferringComponents, ModelFinderIdQuery, QueryReferringOpt
} from '../util/model';
import * as layout from '../util/layout';
import GlobalModel from './Global';
import {
ComponentOption,
ComponentMainType,
ComponentSubType,
ComponentFullType,
ComponentLayoutMode,
BoxLayoutOptionMixin
} from '../util/types';
const inner = makeInner<{
defaultOption: ComponentOption
}, ComponentModel>();
class ComponentModel<Opt extends ComponentOption = ComponentOption> extends Model<Opt> {
// [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.
/**
* @readonly
*/
type: ComponentFullType;
/**
* @readonly
*/
id: string;
/**
* Because simplified concept is probably better, series.name (or component.name)
* has been having too many resposibilities:
* (1) Generating id (which requires name in option should not be modified).
* (2) As an index to mapping series when merging option or calling API (a name
* can refer to more then one components, which is convinient is some case).
* (3) Display.
* @readOnly But injected
*/
name: string;
/**
* @readOnly
*/
mainType: ComponentMainType;
/**
* @readOnly
*/
subType: ComponentSubType;
/**
* @readOnly
*/
componentIndex: number;
/**
* @readOnly
*/
protected defaultOption: ComponentOption;
/**
* @readOnly
*/
ecModel: GlobalModel;
/**
* @readOnly
*/
static dependencies: string[];
readonly uid: string;
// // No common coordinateSystem needed. Each sub class implement
// // `CoordinateSystemHostModel` itself.
// coordinateSystem: CoordinateSystemMaster | CoordinateSystemExecutive;
/**
* Support merge layout params.
* Only support 'box' now (left/right/top/bottom/width/height).
*/
static layoutMode: ComponentLayoutMode | ComponentLayoutMode['type'];
/**
* Prevent from auto set z, zlevel, z2 by the framework.
*/
preventAutoZ: boolean;
// Injectable properties:
__viewId: string;
__requireNewView: boolean;
static protoInitialize = (function () {
const proto = ComponentModel.prototype;
proto.type = 'component';
proto.id = '';
proto.name = '';
proto.mainType = '';
proto.subType = '';
proto.componentIndex = 0;
})();
constructor(option: Opt, parentModel: Model, ecModel: GlobalModel) {
super(option, parentModel, ecModel);
this.uid = componentUtil.getUID('ec_cpt_model');
}
init(option: Opt, parentModel: Model, ecModel: GlobalModel): void {
this.mergeDefaultAndTheme(option, ecModel);
}
mergeDefaultAndTheme(option: Opt, ecModel: GlobalModel): void {
const layoutMode = layout.fetchLayoutMode(this);
const inputPositionParams = layoutMode
? layout.getLayoutParams(option as BoxLayoutOptionMixin) : {};
const themeModel = ecModel.getTheme();
zrUtil.merge(option, themeModel.get(this.mainType));
zrUtil.merge(option, this.getDefaultOption());
if (layoutMode) {
layout.mergeLayoutParam(option as BoxLayoutOptionMixin, inputPositionParams, layoutMode);
}
}
mergeOption(option: Opt, ecModel: GlobalModel): void {
zrUtil.merge(this.option, option, true);
const layoutMode = layout.fetchLayoutMode(this);
if (layoutMode) {
layout.mergeLayoutParam(
this.option as BoxLayoutOptionMixin,
option as BoxLayoutOptionMixin,
layoutMode
);
}
}
/**
* Called immediately after `init` or `mergeOption` of this instance called.
*/
optionUpdated(newCptOption: Opt, isInit: boolean): void {}
/**
* [How to declare defaultOption]:
*
* (A) If using class declaration in typescript (since echarts 5):
* ```ts
* import {ComponentOption} from '../model/option';
* export interface XxxOption extends ComponentOption {
* aaa: number
* }
* export class XxxModel extends Component {
* static type = 'xxx';
* static defaultOption: XxxOption = {
* aaa: 123
* }
* }
* Component.registerClass(XxxModel);
* ```
* ```ts
* import {inheritDefaultOption} from '../util/component';
* import {XxxModel, XxxOption} from './XxxModel';
* export interface XxxSubOption extends XxxOption {
* bbb: number
* }
* class XxxSubModel extends XxxModel {
* static defaultOption: XxxSubOption = inheritDefaultOption(XxxModel.defaultOption, {
* bbb: 456
* })
* fn() {
* let opt = this.getDefaultOption();
* // opt is {aaa: 123, bbb: 456}
* }
* }
* ```
*
* (B) If using class extend (previous approach in echarts 3 & 4):
* ```js
* let XxxComponent = Component.extend({
* defaultOption: {
* xx: 123
* }
* })
* ```
* ```js
* let XxxSubComponent = XxxComponent.extend({
* defaultOption: {
* yy: 456
* },
* fn: function () {
* let opt = this.getDefaultOption();
* // opt is {xx: 123, yy: 456}
* }
* })
* ```
*/
getDefaultOption(): Opt {
const ctor = this.constructor;
// If using class declaration, it is different to travel super class
// in legacy env and auto merge defaultOption. So if using class
// declaration, defaultOption should be merged manually.
if (!isExtendedClass(ctor)) {
// When using ts class, defaultOption must be declared as static.
return (ctor as any).defaultOption;
}
// FIXME: remove this approach?
const fields = inner(this);
if (!fields.defaultOption) {
const optList = [];
let clz = ctor as ExtendableConstructor;
while (clz) {
const opt = clz.prototype.defaultOption;
opt && optList.push(opt);
clz = clz.superClass;
}
let defaultOption = {};
for (let i = optList.length - 1; i >= 0; i--) {
defaultOption = zrUtil.merge(defaultOption, optList[i], true);
}
fields.defaultOption = defaultOption;
}
return fields.defaultOption as Opt;
}
/**
* Notice: always force to input param `useDefault` in case that forget to consider it.
* The same behavior as `modelUtil.parseFinder`.
*
* @param useDefault In many cases like series refer axis and axis refer grid,
* If axis index / axis id not specified, use the first target as default.
* In other cases like dataZoom refer axis, if not specified, measn no refer.
*/
getReferringComponents(mainType: ComponentMainType, opt: QueryReferringOpt): {
// Always be array rather than null/undefined, which is convenient to use.
models: ComponentModel[];
// Whether target compoent specified
specified: boolean;
} {
const indexKey = (mainType + 'Index') as keyof Opt;
const idKey = (mainType + 'Id') as keyof Opt;
return queryReferringComponents(
this.ecModel,
mainType,
{
index: this.get(indexKey, true) as unknown as ModelFinderIndexQuery,
id: this.get(idKey, true) as unknown as ModelFinderIdQuery
},
opt
);
}
getBoxLayoutParams() {
// Consider itself having box layout configs.
const boxLayoutModel = this as Model<ComponentOption & BoxLayoutOptionMixin>;
return {
left: boxLayoutModel.get('left'),
top: boxLayoutModel.get('top'),
right: boxLayoutModel.get('right'),
bottom: boxLayoutModel.get('bottom'),
width: boxLayoutModel.get('width'),
height: boxLayoutModel.get('height')
};
}
// // Interfaces for component / series with select ability.
// select(dataIndex?: number[], dataType?: string): void {}
// unSelect(dataIndex?: number[], dataType?: string): void {}
// getSelectedDataIndices(): number[] {
// return [];
// }
static registerClass: ClassManager['registerClass'];
static hasClass: ClassManager['hasClass'];
static registerSubTypeDefaulter: componentUtil.SubTypeDefaulterManager['registerSubTypeDefaulter'];
}
export type ComponentModelConstructor = typeof ComponentModel
& ClassManager
& componentUtil.SubTypeDefaulterManager
& ExtendableConstructor
& componentUtil.TopologicalTravelable<object>;
mountExtend(ComponentModel, Model);
enableClassManagement(ComponentModel as ComponentModelConstructor, {registerWhenExtend: true});
componentUtil.enableSubTypeDefaulter(ComponentModel as ComponentModelConstructor);
componentUtil.enableTopologicalTravel(ComponentModel as ComponentModelConstructor, getDependencies);
function getDependencies(componentType: string): string[] {
let deps: string[] = [];
zrUtil.each((ComponentModel as ComponentModelConstructor).getClassesByMainType(componentType), function (clz) {
deps = deps.concat((clz as any).dependencies || (clz as any).prototype.dependencies || []);
});
// Ensure main type.
deps = zrUtil.map(deps, function (type) {
return parseClassType(type).main;
});
// Hack dataset for convenience.
if (componentType !== 'dataset' && zrUtil.indexOf(deps, 'dataset') <= 0) {
deps.unshift('dataset');
}
return deps;
}
export default ComponentModel;