diff --git a/blocks/Config/Config.js b/blocks/Config/Config.js index a0a1487e6..6f6f6e851 100644 --- a/blocks/Config/Config.js +++ b/blocks/Config/Config.js @@ -133,6 +133,7 @@ class ConfigClass extends Block { ConfigClass.bindAttributes(attrStateMapping); -export const Config = ConfigClass; +/** @typedef {import('../../utils/mixinClass.js').MixinClass} Config */ -/** @typedef {typeof ConfigClass & import('../../types').ConfigType} Config */ +// This is workaround for jsdoc that allows us to export extended class type along with the class itself +export const Config = /** @type {Config} */ (/** @type {unknown} */ (ConfigClass)); diff --git a/blocks/DataOutput/DataOutput.js b/blocks/DataOutput/DataOutput.js index 664fba42a..9c0b3aa6a 100644 --- a/blocks/DataOutput/DataOutput.js +++ b/blocks/DataOutput/DataOutput.js @@ -11,7 +11,7 @@ import { applyStyles } from '@symbiotejs/symbiote'; * }} Output} */ -export class DataOutput extends UploaderBlock { +class DataOutputClass extends UploaderBlock { processInnerHtml = true; requireCtxName = true; @@ -148,7 +148,7 @@ export class DataOutput extends UploaderBlock { } } -DataOutput.dict = Object.freeze({ +DataOutputClass.dict = Object.freeze({ SRC_CTX_KEY: '*outputData', EVENT_NAME: 'lr-data-output', FIRE_EVENT_ATTR: 'use-event', @@ -158,3 +158,36 @@ DataOutput.dict = Object.freeze({ INPUT_NAME_ATTR: 'input-name', INPUT_REQUIRED: 'input-required', }); + +/** + * @typedef {import('../../utils/mixinClass.js').MixinClass< + * typeof DataOutputClass, + * { + * addEventListener( + * type: 'lr-data-output', + * listener: ( + * e: CustomEvent<{ + * timestamp: number; + * ctxName: string; + * data: Output; + * }> + * ) => void, + * options?: boolean | AddEventListenerOptions + * ): void; + * removeEventListener( + * type: 'lr-data-output', + * listener: ( + * e: CustomEvent<{ + * timestamp: number; + * ctxName: string; + * data: Output; + * }> + * ) => void, + * options?: boolean | EventListenerOptions + * ): void; + * } + * >} + * DataOutput + */ + +export const DataOutput = /** @type {DataOutput} */ (/** @type {unknown} */ (DataOutputClass)); diff --git a/blocks/ShadowWrapper/ShadowWrapper.js b/blocks/ShadowWrapper/ShadowWrapper.js index 5e78d2e57..eaef91370 100644 --- a/blocks/ShadowWrapper/ShadowWrapper.js +++ b/blocks/ShadowWrapper/ShadowWrapper.js @@ -5,18 +5,14 @@ import { waitForAttribute } from '../../utils/waitForAttribute.js'; const CSS_ATTRIBUTE = 'css-src'; /** - * @template T - * @typedef {new (...args: any[]) => T} GConstructor - */ - -/** - * @template {GConstructor} T + * @template {import('../../utils/mixinClass.js').GConstructor} T * @param {T} Base - * @returns {{ - * new (...args: ConstructorParameters): InstanceType & { + * @returns {import('../../utils/mixinClass.js').MixinClass< + * T, + * { * shadowReadyCallback(): void; - * }; - * } & Omit} + * } + * >} */ export function shadowed(Base) { // @ts-ignore diff --git a/blocks/UploadCtxProvider/UploadCtxProvider.js b/blocks/UploadCtxProvider/UploadCtxProvider.js index 050b4d7f3..42fd4279f 100644 --- a/blocks/UploadCtxProvider/UploadCtxProvider.js +++ b/blocks/UploadCtxProvider/UploadCtxProvider.js @@ -1,7 +1,6 @@ // @ts-check import { UploaderBlock } from '../../abstract/UploaderBlock.js'; - class UploadCtxProviderClass extends UploaderBlock { requireCtxName = true; @@ -10,23 +9,26 @@ class UploadCtxProviderClass extends UploaderBlock { } } -export const UploadCtxProvider = UploadCtxProviderClass; - /** - * @typedef {typeof UploadCtxProviderClass & { - * addEventListener< - * T extends typeof import('./EventEmitter.js').EventType[keyof typeof import('./EventEmitter.js').EventType] - * >( - * type: T, - * listener: (e: CustomEvent) => void, - * options?: boolean | AddEventListenerOptions - * ): void; - * removeEventListener< - * T extends typeof import('./EventEmitter.js').EventType[keyof typeof import('./EventEmitter.js').EventType] - * >( - * type: T, - * listener: (e: CustomEvent) => void, - * options?: boolean | EventListenerOptions - * ): void; - * }} UploadCtxProvider + * @typedef {import('../../utils/mixinClass.js').MixinClass< + * typeof UploadCtxProviderClass, + * { + * addEventListener< + * T extends typeof import('./EventEmitter.js').EventType[keyof typeof import('./EventEmitter.js').EventType] + * >( + * type: T, + * listener: (e: CustomEvent) => void, + * options?: boolean | AddEventListenerOptions + * ): void; + * removeEventListener< + * T extends typeof import('./EventEmitter.js').EventType[keyof typeof import('./EventEmitter.js').EventType] + * >( + * type: T, + * listener: (e: CustomEvent) => void, + * options?: boolean | EventListenerOptions + * ): void; + * } + * >} UploadCtxProvider */ + +export const UploadCtxProvider = /** @type {UploadCtxProvider} */ (/** @type {unknown} */ (UploadCtxProviderClass)); diff --git a/types/events.d.ts b/types/events.d.ts index 265411844..c669ff7ce 100644 --- a/types/events.d.ts +++ b/types/events.d.ts @@ -1,18 +1,23 @@ -import type { GlobalEventPayload } from '../blocks/UploadCtxProvider/EventEmitter'; +import type { GlobalEventPayload, EventPayload } from '../blocks/UploadCtxProvider/EventEmitter'; -type CustomEventMap = { +export type GlobalEventMap = { [T in keyof GlobalEventPayload]: CustomEvent; }; + +export type EventMap = { + [T in keyof EventPayload]: CustomEvent; +}; + declare global { interface Window { - addEventListener( + addEventListener( type: T, - listener: (e: CustomEventMap[T]) => void, + listener: (e: GlobalEventMap[T]) => void, options?: boolean | AddEventListenerOptions ): void; - removeEventListener( + removeEventListener( type: T, - listener: (e: CustomEventMap[T]) => void, + listener: (e: GlobalEventMap[T]) => void, options?: boolean | EventListenerOptions ): void; } diff --git a/types/jsx.d.ts b/types/jsx.d.ts index f758c3074..57a5de281 100644 --- a/types/jsx.d.ts +++ b/types/jsx.d.ts @@ -1,13 +1,13 @@ /// -type ConfigPlainType = import('./exported').ConfigPlainType; -type UploadCtxProvider = import('..').UploadCtxProvider; +type ConfigPlainType = import('./exported.js').ConfigPlainType; +type UploadCtxProvider = import('../index.js').UploadCtxProvider; type Config = import('../index.js').Config; -type FileUploaderInline = import('..').FileUploaderInline; -type FileUploaderRegular = import('..').FileUploaderRegular; -type FileUploaderMinimal = import('..').FileUploaderMinimal; -type DataOutput = import('..').DataOutput; -type CloudImageEditorBlock = import('..').CloudImageEditorBlock; +type FileUploaderInline = import('../index.js').FileUploaderInline; +type FileUploaderRegular = import('../index.js').FileUploaderRegular; +type FileUploaderMinimal = import('../index.js').FileUploaderMinimal; +type DataOutput = import('../index.js').DataOutput; +type CloudImageEditorBlock = import('../index.js').CloudImageEditorBlock; type CtxAttributes = { 'ctx-name': string; }; @@ -56,17 +56,17 @@ declare namespace JSX { 'lr-cloud-image-editor-activity': any; 'lr-cloud-image-editor-block': CustomElement< CloudImageEditorBlock, - CtxAttributes & { uuid: string; 'cdn-url': string } + CtxAttributes & ({ uuid: string } | { 'cdn-url': string }) & Partial<{ tabs: string; 'crop-preset': string }> >; 'lr-cloud-image-editor': CustomElement< CloudImageEditorBlock, - CtxAttributes & ShadowWrapperAttributes & { uuid: string; 'cdn-url': string } + JSX.IntrinsicElements['lr-cloud-image-editor-block'] & ShadowWrapperAttributes >; - 'lr-data-output': CustomElement; + 'lr-data-output': CustomElement, CtxAttributes>; 'lr-file-uploader-regular': CustomElement; 'lr-file-uploader-minimal': CustomElement; 'lr-file-uploader-inline': CustomElement; - 'lr-upload-ctx-provider': CustomElement; - 'lr-config': CustomElement>; + 'lr-upload-ctx-provider': CustomElement, CtxAttributes>; + 'lr-config': CustomElement, CtxAttributes & Partial>; } } diff --git a/types/test/events.test-d.ts b/types/test/events.test-d.ts deleted file mode 100644 index 5b269b9c1..000000000 --- a/types/test/events.test-d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expectType } from 'tsd'; -import { GlobalEventPayload } from '..'; -import type { EventPayload, UploadCtxProvider } from '../..'; - -window.addEventListener('LR_DATA_OUTPUT', (e) => { - expectType>(e); -}, { once: true }); - -const ctx: UploadCtxProvider = null as unknown as UploadCtxProvider; -ctx.addEventListener( - 'data-output', - (e) => { - expectType>(e); - } -); diff --git a/types/test/global-events.test-d.ts b/types/test/global-events.test-d.ts new file mode 100644 index 000000000..9a99a8051 --- /dev/null +++ b/types/test/global-events.test-d.ts @@ -0,0 +1,10 @@ +import { expectType } from 'tsd'; +import { GlobalEventPayload } from '../index.js'; + +window.addEventListener( + 'LR_DATA_OUTPUT', + (e) => { + expectType>(e); + }, + { once: true } +); diff --git a/types/test/lr-cloud-image-editor.test-d.tsx b/types/test/lr-cloud-image-editor.test-d.tsx new file mode 100644 index 000000000..28ab0a8a0 --- /dev/null +++ b/types/test/lr-cloud-image-editor.test-d.tsx @@ -0,0 +1,12 @@ +// @ts-expect-error - no props +() => ; + +// @ts-expect-error - no css-url +() => ; + +// @ts-expect-error - no css-src +() => ; + +() => ; +() => ; +() => ; diff --git a/types/test/lr-config.test-d.tsx b/types/test/lr-config.test-d.tsx index 97d3b5c72..b437476cf 100644 --- a/types/test/lr-config.test-d.tsx +++ b/types/test/lr-config.test-d.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { expectType } from 'tsd'; -import '../jsx'; -import { OutputFileEntry } from '..'; +import '../jsx.js'; +import { OutputFileEntry } from '../index.js'; // @ts-expect-error untyped props () => ; @@ -17,8 +17,8 @@ import { OutputFileEntry } from '..'; // allow useRef hook () => { - const ref = React.useRef(null); - expectType(ref.current); + const ref = React.useRef | null>(null); + expectType | null>(ref.current); ; }; @@ -27,15 +27,15 @@ import { OutputFileEntry } from '..'; { - expectType(el); + expectType | null>(el); }} >; }; // allow createRef () => { - const ref = React.createRef(); - expectType(ref.current); + const ref = React.createRef>(); + expectType | null>(ref.current); ; }; @@ -44,26 +44,25 @@ import { OutputFileEntry } from '..'; // allow to use DOM properties () => { - const ref = React.useRef(null); + const ref = React.useRef | null>(null); if (ref.current) { const config = ref.current; - config.metadata = {foo: 'bar'} - config.secureSignature = '1231' - config.multiple = true + config.metadata = { foo: 'bar' }; + config.secureSignature = '1231'; + config.multiple = true; } }; - // allow to pass metadata () => { - const ref = React.useRef(null); + const ref = React.useRef | null>(null); if (ref.current) { const config = ref.current; - config.metadata = {foo: 'bar'} - config.metadata = () => ({foo: 'bar'}) + config.metadata = { foo: 'bar' }; + config.metadata = () => ({ foo: 'bar' }); config.metadata = async (entry) => { - expectType(entry) - return {foo: 'bar'} - } + expectType(entry); + return { foo: 'bar' }; + }; } }; diff --git a/types/test/lr-data-output.test-d.tsx b/types/test/lr-data-output.test-d.tsx new file mode 100644 index 000000000..9bedbb390 --- /dev/null +++ b/types/test/lr-data-output.test-d.tsx @@ -0,0 +1,18 @@ +import { expectType } from 'tsd'; +import { DataOutput } from '../../index.js'; +import { Output } from '../../blocks/DataOutput/DataOutput.js'; + +() => ; + +const dataOutput = new DataOutput(); +dataOutput.addEventListener('lr-data-output', (e) => { + expectType< + CustomEvent<{ + timestamp: number; + ctxName: string; + data: Output; + }> + >(e); +}); + +dataOutput.validationInput; diff --git a/types/test/lr-upload-ctx-provider.test-d.tsx b/types/test/lr-upload-ctx-provider.test-d.tsx new file mode 100644 index 000000000..c2b4a6873 --- /dev/null +++ b/types/test/lr-upload-ctx-provider.test-d.tsx @@ -0,0 +1,27 @@ +import { expectType } from 'tsd'; +import { EventMap, UploadCtxProvider } from '../../index.js'; +import { useRef } from 'react'; + +const instance = new UploadCtxProvider(); + +instance.addFileFromUrl('https://example.com/image.png'); +instance.uploadCollection.size; +instance.setOrAddState('fileId', 'uploading'); + +instance.addEventListener('data-output', (e) => { + expectType(e); + + // @ts-expect-error - wrong event type + expectType(e); +}); + +const onDataOutput = (e: EventMap['data-output']) => { + // noop +}; + +instance.addEventListener('data-output', onDataOutput); + +() => { + const ref = useRef>(null); + return ; +}; diff --git a/utils/mixinClass.js b/utils/mixinClass.js new file mode 100644 index 000000000..8ff53daa7 --- /dev/null +++ b/utils/mixinClass.js @@ -0,0 +1,18 @@ +/** + * @template T + * @typedef {new (...args: any[]) => T} GConstructor + */ + +/** + * This is a helper to create a class type extended with the provided set of instance properties. It's useful when there + * are some dynamic generated properties or native overrides in the class. We're use it to define dynamic access + * properties and events to subscribe to. + * + * @template {GConstructor} Base + * @template {Record} [InstanceProperties={}] Default is `{}` + * @typedef {{ + * new (...args: ConstructorParameters): InstanceProperties & InstanceType; + * } & Omit} MixinClass + */ + +export {};