import declare = require("dojo/_base/declare"); import { each } from "@implab/core-amd/safe"; import { Constructor } from "@implab/core-amd/interfaces"; // declare const declare: any; type DeclareConstructor = dojo._base.DeclareConstructor; export interface AbstractConstructor { prototype: T; } interface DjMockConstructor { new(...args: unknown[]): T; mock: boolean; bases: AbstractConstructor[]; } export function djbase(): DeclareConstructor; export function djbase( b0: AbstractConstructor ): DeclareConstructor; export function djbase( b0: AbstractConstructor, b1: AbstractConstructor ): DeclareConstructor; export function djbase( b0: AbstractConstructor, b1: AbstractConstructor, b2: AbstractConstructor ): DeclareConstructor; export function djbase( b0: AbstractConstructor, b1: AbstractConstructor, b2: AbstractConstructor, b3: AbstractConstructor ): DeclareConstructor; export function djbase( b0: AbstractConstructor, b1: AbstractConstructor, b2: AbstractConstructor, b3: AbstractConstructor, b4: AbstractConstructor ): DeclareConstructor; export function djbase( b0: AbstractConstructor, b1: AbstractConstructor, b2: AbstractConstructor, b3: AbstractConstructor, b4: AbstractConstructor, b5: AbstractConstructor ): DeclareConstructor; export function djbase( b0: AbstractConstructor, b1: AbstractConstructor, b2: AbstractConstructor, b3: AbstractConstructor, b4: AbstractConstructor, b5: AbstractConstructor, b6: AbstractConstructor ): DeclareConstructor; export function djbase( b0: AbstractConstructor, b1: AbstractConstructor, b2: AbstractConstructor, b3: AbstractConstructor, b4: AbstractConstructor, b5: AbstractConstructor, b6: AbstractConstructor, b7: AbstractConstructor ): DeclareConstructor; /** Создает конструктор-заглушку из списка базовых классов, используется * для объявления классов при помощи `dojo/_base/declare`. * * Создает пустой конструктор, с пустым стандартным прототипом, это нужно, * поскольку в унаследованном классе конструктор обязательно должен вызвать * `super(...)`, таким образом он вызовет пустую функцию. * * Созданный конструктор хранит в себе список базовых классов, который будет * использован декоратором `djclass`, который вернет класс, объявленный при * помощи `dojo/_base/declare`. * * @param bases список базовых классов, от которых требуется унаследовать * новый класс. * */ export function djbase(...bases: AbstractConstructor[]): Constructor { const t = class { static mock: boolean; static bases: AbstractConstructor[]; }; t.mock = true; t.bases = bases; return t as Constructor; } function isMockConstructor(v: AbstractConstructor): v is DjMockConstructor { return v && "mock" in v; } /** Создает класс при помощи `dojo/_base/declare`. Для этого исходный класс * должен быть унаследован от `djbase(...)`. * * @param target Класс, который нужно объявить при помощи `dojo/_base/declare` */ export function djclass(target: T): T { // получаем базовый конструктор и его прототип const bp = target && !!target.prototype && Object.getPrototypeOf(target.prototype) as object; const bc = bp && bp.constructor; // проверка того, что класс унаследован от специальной заглушки if (isMockConstructor(bc)) { // bc.bases - базовый класс, объявленный при помощи dojo/_base/declare const cls = declare(bc.bases, target.prototype) as unknown as T; // bc - базовый класс, bc.prototype используется как super // при вызове базовых методов. Нужно создать bc.prototype // таким образом, чтобы он вызывал this.inherited(). // создаем новый прототип, он не в цепочке прототипов у текущего // класса, но super.some_method будет использовать именно его. // в этом объекте будут размещены прокси для переопределенных // методов. const nbp = bc.prototype = Object.create(cls.prototype) as Record; nbp.constructor = bc; // proxy - фабрика для создания прокси-методов, которые внутри // себя вызовут this.inherited с правильными параметрами. const proxy = (m: (...args: unknown[]) => unknown) => function (this: dojo._base.DeclareCreatedObject, ...args: unknown[]) { const f = this.getInherited({ callee: m, ...args, length: args.length}); return f ? f.apply(this, args) as unknown : undefined; // так сделать можно только dojo 1.15+ // return this.inherited(m, arguments); }; // у текущего класса прототип содержит методы, объявленные в этом // классе и его конструктор. Нужно пройти по всем методам и // создать для них прокси. // При этом только те, методы, которые есть в базовых классах // могут быть переопределены. each(target.prototype, (m: unknown, p: string) => { if (typeof m === "function" && p !== "constructor" && Object.prototype.hasOwnProperty.call(target, p) ) { nbp[p] = proxy(m as (...args: unknown[]) => unknown); } }); // TODO mixin static members return cls; } else { return target; } } function makeSetterName(prop: string) { return [ "_set", prop.replace(/^./, x => x.toUpperCase()), "Attr" ].join(""); } function makeGetterName(prop: string) { return [ "_get", prop.replace(/^./, x => x.toUpperCase()), "Attr" ].join(""); } interface NodeBindSpec { node: string; type: "attribute" | "innerText" | "textContent" | "innerHTML" | "class" | "toggleClass"; attribute?: string; className?: string; } /** * Описание привязки свойства виджета к свойству внутреннего объекта. */ interface MemberBindSpec { /** * Имя свойства со ссылкой на объект, к которому . */ member: string; /** * Свойство объекта к которому нужно осуществить привязку. */ property: string; /** * Привязка осуществляется не только на запись но и на чтение свойства. */ getter?: boolean; } function isNodeBindSpec(v: object): v is NodeBindSpec { return "node" in v; } /** Декорирует свойства виджета для привязки их к внутренним членам, либо DOM * элементам, либо свойству внутреннего объекта. * * @param {NodeBindSpec | MemberBindSpec} params Параметры связывания. */ export function bind(params: NodeBindSpec | MemberBindSpec) { if (isNodeBindSpec(params)) { return (target: Record, name: string) => { target[makeSetterName(name)] = params; }; } else { return (target: Record & { _set(name: string, v: unknown): void }, name: string) => { target[name] = null; target[makeSetterName(name)] = function (v: unknown) { this._set(name, v); const inner = this[params.member] as Record; if (typeof inner.set === "function") inner.set(params.property, v); }; if (params.getter) target[makeGetterName(name)] = function () { const inner = this[params.member] as Record; if (typeof inner.get === "function") return inner.get(params.property) as unknown; }; }; } } /** Создает в прототипе указанное свойство со значение `undefined`, данный * декоратор следует использовать для свойств, у которых нет значения по-умолчанию * и они не могут быть `null | undefined` */ export function prototype(): (p: object, name: string) => void; /** Создает в прототипе свойство с указанным значением. * @param value Значение, которое будет указано в прототипе */ export function prototype(value: T):

(p: P, name: K) => void; export function prototype(value?: T) { return

(p: P, name: K) => { p[name] = value as any; }; }