| /* |
| * 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 { Dictionary } from 'zrender/src/core/types'; |
| import { ComponentFullType, ComponentTypeInfo, ComponentMainType, ComponentSubType } from './types'; |
| |
| const TYPE_DELIMITER = '.'; |
| const IS_CONTAINER = '___EC__COMPONENT__CONTAINER___' as const; |
| const IS_EXTENDED_CLASS = '___EC__EXTENDED_CLASS___' as const; |
| |
| /** |
| * Notice, parseClassType('') should returns {main: '', sub: ''} |
| * @public |
| */ |
| export function parseClassType(componentType: ComponentFullType): ComponentTypeInfo { |
| const ret = {main: '', sub: ''}; |
| if (componentType) { |
| const typeArr = componentType.split(TYPE_DELIMITER); |
| ret.main = typeArr[0] || ''; |
| ret.sub = typeArr[1] || ''; |
| } |
| return ret; |
| } |
| |
| /** |
| * @public |
| */ |
| function checkClassType(componentType: ComponentFullType): void { |
| zrUtil.assert( |
| /^[a-zA-Z0-9_]+([.][a-zA-Z0-9_]+)?$/.test(componentType), |
| 'componentType "' + componentType + '" illegal' |
| ); |
| } |
| |
| export function isExtendedClass(clz: any): boolean { |
| return !!(clz && clz[IS_EXTENDED_CLASS]); |
| } |
| |
| |
| export interface ExtendableConstructor { |
| new (...args: any): any; |
| $constructor?: new (...args: any) => any; |
| extend: (proto: {[name: string]: any}) => ExtendableConstructor; |
| superCall: (context: any, methodName: string, ...args: any) => any; |
| superApply: (context: any, methodName: string, args: []) => any; |
| superClass?: ExtendableConstructor; |
| [IS_EXTENDED_CLASS]?: boolean; |
| } |
| |
| /** |
| * Implements `ExtendableConstructor` for `rootClz`. |
| * |
| * @usage |
| * ```ts |
| * class Xxx {} |
| * type XxxConstructor = typeof Xxx & ExtendableConstructor |
| * enableClassExtend(Xxx as XxxConstructor); |
| * ``` |
| */ |
| export function enableClassExtend(rootClz: ExtendableConstructor, mandatoryMethods?: string[]): void { |
| |
| rootClz.$constructor = rootClz; // FIXME: not necessary? |
| |
| rootClz.extend = function (proto: Dictionary<any>) { |
| if (__DEV__) { |
| zrUtil.each(mandatoryMethods, function (method) { |
| if (!proto[method]) { |
| console.warn( |
| 'Method `' + method + '` should be implemented' |
| + (proto.type ? ' in ' + proto.type : '') + '.' |
| ); |
| } |
| }); |
| } |
| |
| const superClass = this; |
| // For backward compat, we both support ts class inheritance and this |
| // "extend" approach. |
| // The constructor should keep the same behavior as ts class inheritance: |
| // If this constructor/$constructor is not declared, auto invoke the super |
| // constructor. |
| // If this constructor/$constructor is declared, it is responsible for |
| // calling the super constructor. |
| function ExtendedClass(this: any, ...args: any[]) { |
| if (!proto.$constructor) { |
| |
| if (!isESClass(superClass)) { |
| // Will throw error if superClass is an es6 native class. |
| superClass.apply(this, arguments); |
| } |
| else { |
| const ins = zrUtil.createObject( |
| // @ts-ignore |
| ExtendedClass.prototype, new superClass(...args) |
| ); |
| return ins; |
| } |
| } |
| else { |
| proto.$constructor.apply(this, arguments); |
| } |
| } |
| ExtendedClass[IS_EXTENDED_CLASS] = true; |
| |
| zrUtil.extend(ExtendedClass.prototype, proto); |
| |
| ExtendedClass.extend = this.extend; |
| ExtendedClass.superCall = superCall; |
| ExtendedClass.superApply = superApply; |
| zrUtil.inherits(ExtendedClass, this); |
| ExtendedClass.superClass = superClass; |
| |
| return ExtendedClass as unknown as ExtendableConstructor; |
| }; |
| } |
| |
| function isESClass(fn: unknown): boolean { |
| return typeof fn === 'function' |
| && /^class\s/.test(Function.prototype.toString.call(fn)); |
| } |
| |
| /** |
| * A work around to both support ts extend and this extend mechanism. |
| * on sub-class. |
| * @usage |
| * ```ts |
| * class Component { ... } |
| * classUtil.enableClassExtend(Component); |
| * classUtil.enableClassManagement(Component, {registerWhenExtend: true}); |
| * |
| * class Series extends Component { ... } |
| * // Without calling `markExtend`, `registerWhenExtend` will not work. |
| * Component.markExtend(Series); |
| * ``` |
| */ |
| export function mountExtend(SubClz: any, SupperClz: any): void { |
| SubClz.extend = SupperClz.extend; |
| } |
| |
| |
| export interface CheckableConstructor { |
| new (...args: any): any; |
| isInstance: (ins: any) => boolean; |
| } |
| |
| // A random offset. |
| let classBase = Math.round(Math.random() * 10); |
| |
| /** |
| * Implements `CheckableConstructor` for `target`. |
| * Can not use instanceof, consider different scope by |
| * cross domain or es module import in ec extensions. |
| * Mount a method "isInstance()" to Clz. |
| * |
| * @usage |
| * ```ts |
| * class Xxx {} |
| * type XxxConstructor = typeof Xxx & CheckableConstructor; |
| * enableClassCheck(Xxx as XxxConstructor) |
| * ``` |
| */ |
| export function enableClassCheck(target: CheckableConstructor): void { |
| const classAttr = ['__\0is_clz', classBase++].join('_'); |
| target.prototype[classAttr] = true; |
| |
| if (__DEV__) { |
| zrUtil.assert(!target.isInstance, 'The method "is" can not be defined.'); |
| } |
| |
| target.isInstance = function (obj) { |
| return !!(obj && obj[classAttr]); |
| }; |
| } |
| |
| // superCall should have class info, which can not be fetch from 'this'. |
| // Consider this case: |
| // class A has method f, |
| // class B inherits class A, overrides method f, f call superApply('f'), |
| // class C inherits class B, do not overrides method f, |
| // then when method of class C is called, dead loop occured. |
| function superCall(this: any, context: any, methodName: string, ...args: any): any { |
| return this.superClass.prototype[methodName].apply(context, args); |
| } |
| |
| function superApply(this: any, context: any, methodName: string, args: any): any { |
| return this.superClass.prototype[methodName].apply(context, args); |
| } |
| |
| export type Constructor = new (...args: any) => any; |
| type SubclassContainer = {[subType: string]: Constructor} & {[IS_CONTAINER]?: true}; |
| |
| export interface ClassManager { |
| registerClass: (clz: Constructor) => Constructor; |
| getClass: ( |
| componentMainType: ComponentMainType, subType?: ComponentSubType, throwWhenNotFound?: boolean |
| ) => Constructor; |
| getClassesByMainType: (componentType: ComponentMainType) => Constructor[]; |
| hasClass: (componentType: ComponentFullType) => boolean; |
| getAllClassMainTypes: () => ComponentMainType[]; |
| hasSubTypes: (componentType: ComponentFullType) => boolean; |
| } |
| |
| /** |
| * Implements `ClassManager` for `target` |
| * |
| * @usage |
| * ```ts |
| * class Xxx {} |
| * type XxxConstructor = typeof Xxx & ClassManager |
| * enableClassManagement(Xxx as XxxConstructor); |
| * ``` |
| */ |
| export function enableClassManagement( |
| target: ClassManager, |
| options?: {registerWhenExtend?: boolean} |
| ): void { |
| |
| options = options || {}; |
| |
| /** |
| * Component model classes |
| * key: componentType, |
| * value: |
| * componentClass, when componentType is 'xxx' |
| * or Object.<subKey, componentClass>, when componentType is 'xxx.yy' |
| */ |
| const storage: { |
| [componentMainType: string]: (Constructor | SubclassContainer) |
| } = {}; |
| |
| target.registerClass = function ( |
| clz: Constructor |
| ): Constructor { |
| |
| // `type` should not be a "instance memeber". |
| // If using TS class, should better declared as `static type = 'series.pie'`. |
| // otherwise users have to mount `type` on prototype manually. |
| // For backward compat and enable instance visit type via `this.type`, |
| // we stil support fetch `type` from prototype. |
| const componentFullType = (clz as any).type || clz.prototype.type; |
| |
| if (componentFullType) { |
| checkClassType(componentFullType); |
| |
| // If only static type declared, we assign it to prototype mandatorily. |
| clz.prototype.type = componentFullType; |
| |
| const componentTypeInfo = parseClassType(componentFullType); |
| |
| if (!componentTypeInfo.sub) { |
| if (__DEV__) { |
| if (storage[componentTypeInfo.main]) { |
| console.warn(componentTypeInfo.main + ' exists.'); |
| } |
| } |
| storage[componentTypeInfo.main] = clz; |
| } |
| else if (componentTypeInfo.sub !== IS_CONTAINER) { |
| const container = makeContainer(componentTypeInfo); |
| container[componentTypeInfo.sub] = clz; |
| } |
| } |
| return clz; |
| }; |
| |
| target.getClass = function ( |
| mainType: ComponentMainType, |
| subType?: ComponentSubType, |
| throwWhenNotFound?: boolean |
| ): Constructor { |
| let clz = storage[mainType]; |
| |
| if (clz && (clz as SubclassContainer)[IS_CONTAINER]) { |
| clz = subType ? (clz as SubclassContainer)[subType] : null; |
| } |
| |
| if (throwWhenNotFound && !clz) { |
| throw new Error( |
| !subType |
| ? mainType + '.' + 'type should be specified.' |
| : 'Component ' + mainType + '.' + (subType || '') + ' not exists. Load it first.' |
| ); |
| } |
| |
| return clz as Constructor; |
| }; |
| |
| target.getClassesByMainType = function (componentType: ComponentFullType): Constructor[] { |
| const componentTypeInfo = parseClassType(componentType); |
| |
| const result: Constructor[] = []; |
| const obj = storage[componentTypeInfo.main]; |
| |
| if (obj && (obj as SubclassContainer)[IS_CONTAINER]) { |
| zrUtil.each(obj as SubclassContainer, function (o, type) { |
| type !== IS_CONTAINER && result.push(o as Constructor); |
| }); |
| } |
| else { |
| result.push(obj as Constructor); |
| } |
| |
| return result; |
| }; |
| |
| target.hasClass = function (componentType: ComponentFullType): boolean { |
| // Just consider componentType.main. |
| const componentTypeInfo = parseClassType(componentType); |
| return !!storage[componentTypeInfo.main]; |
| }; |
| |
| /** |
| * @return Like ['aa', 'bb'], but can not be ['aa.xx'] |
| */ |
| target.getAllClassMainTypes = function (): ComponentMainType[] { |
| const types: string[] = []; |
| zrUtil.each(storage, function (obj, type) { |
| types.push(type); |
| }); |
| return types; |
| }; |
| |
| /** |
| * If a main type is container and has sub types |
| */ |
| target.hasSubTypes = function (componentType: ComponentFullType): boolean { |
| const componentTypeInfo = parseClassType(componentType); |
| const obj = storage[componentTypeInfo.main]; |
| return obj && (obj as SubclassContainer)[IS_CONTAINER]; |
| }; |
| |
| function makeContainer(componentTypeInfo: ComponentTypeInfo): SubclassContainer { |
| let container = storage[componentTypeInfo.main]; |
| if (!container || !(container as SubclassContainer)[IS_CONTAINER]) { |
| container = storage[componentTypeInfo.main] = {}; |
| container[IS_CONTAINER] = true; |
| } |
| return container as SubclassContainer; |
| } |
| |
| // FIXME:TS remove `registerWhenExtend` finally when ts migration completed? |
| if (options.registerWhenExtend) { |
| const originalExtend = (target as any).extend; |
| if (originalExtend) { |
| (target as any).extend = function (proto: any) { |
| const ExtendedClass = originalExtend.call(this, proto); |
| return target.registerClass(ExtendedClass); |
| }; |
| } |
| } |
| } |
| |
| // /** |
| // * @param {string|Array.<string>} properties |
| // */ |
| // export function setReadOnly(obj, properties) { |
| // FIXME It seems broken in IE8 simulation of IE11 |
| // if (!zrUtil.isArray(properties)) { |
| // properties = properties != null ? [properties] : []; |
| // } |
| // zrUtil.each(properties, function (prop) { |
| // let value = obj[prop]; |
| |
| // Object.defineProperty |
| // && Object.defineProperty(obj, prop, { |
| // value: value, writable: false |
| // }); |
| // zrUtil.isArray(obj[prop]) |
| // && Object.freeze |
| // && Object.freeze(obj[prop]); |
| // }); |
| // } |