diff --git a/abstract/ActivityBlock.js b/abstract/ActivityBlock.js index 24ef37903..e09e1d2be 100644 --- a/abstract/ActivityBlock.js +++ b/abstract/ActivityBlock.js @@ -1,5 +1,4 @@ // @ts-check -import { Modal } from '../blocks/Modal/Modal.js'; import { debounce } from '../blocks/utils/debounce.js'; import { Block } from './Block.js'; import { activityBlockCtx } from './CTX.js'; diff --git a/abstract/Block.js b/abstract/Block.js index 9aeb18e29..c275720a6 100644 --- a/abstract/Block.js +++ b/abstract/Block.js @@ -1,14 +1,15 @@ // @ts-check import { BaseComponent } from '@symbiotejs/symbiote'; +import { EventEmitter } from '../blocks/UploadCtxProvider/EventEmitter.js'; import { createWindowHeightTracker, getIsWindowHeightTracked } from '../utils/createWindowHeightTracker.js'; +import { getPluralForm } from '../utils/getPluralForm.js'; import { applyTemplateData, getPluralObjects } from '../utils/template-utils.js'; +import { toKebabCase } from '../utils/toKebabCase.js'; +import { waitForAttribute } from '../utils/waitForAttribute.js'; +import { warnOnce } from '../utils/warnOnce.js'; import { blockCtx } from './CTX.js'; import { l10nProcessor } from './l10nProcessor.js'; import { sharedConfigKey } from './sharedConfigKey.js'; -import { toKebabCase } from '../utils/toKebabCase.js'; -import { warnOnce } from '../utils/warnOnce.js'; -import { getPluralForm } from '../utils/getPluralForm.js'; -import { waitForAttribute } from '../utils/waitForAttribute.js'; const TAG_PREFIX = 'lr-'; @@ -77,6 +78,20 @@ export class Block extends BaseComponent { this.__l10nKeys = []; } + /** + * @template {typeof import('../blocks/UploadCtxProvider/EventEmitter.js').EventType[keyof typeof import('../blocks/UploadCtxProvider/EventEmitter.js').EventType]} T + * @param {T} type + * @param {import('../blocks/UploadCtxProvider/EventEmitter.js').EventPayload[T]} [payload] + */ + emit(type, payload) { + /** @type {import('../blocks/UploadCtxProvider/EventEmitter.js').EventEmitter} */ + const eventEmitter = this.has('*eventEmitter') && this.$['*eventEmitter']; + if (!eventEmitter) { + return; + } + eventEmitter.emit(type, payload); + } + /** * @param {String} localPropKey * @param {String} l10nKey @@ -165,6 +180,10 @@ export class Block extends BaseComponent { initCallback() { let blocksRegistry = this.$['*blocksRegistry']; blocksRegistry.add(this); + + if (!this.$['*eventEmitter']) { + this.$['*eventEmitter'] = new EventEmitter(() => this.ctxName); + } } destroyCallback() { @@ -271,7 +290,12 @@ export class Block extends BaseComponent { } } + /** @deprecated */ updateCtxCssData = () => { + warnOnce( + 'Using CSS variables for configuration is deprecated. Please use `lr-config` instead. See migration guide: https://uploadcare.com/docs/file-uploader/migration-to-0.25.0/' + ); + /** @type {Set} */ let blocks = this.$['*blocksRegistry']; for (let block of blocks) { diff --git a/abstract/CTX.js b/abstract/CTX.js index 575e12e78..80a92632f 100644 --- a/abstract/CTX.js +++ b/abstract/CTX.js @@ -4,6 +4,8 @@ import { Queue } from '@uploadcare/upload-client'; export const blockCtx = () => ({ /** @type {Set} */ '*blocksRegistry': new Set(), + /** @type {import('../blocks/UploadCtxProvider/EventEmitter.js').EventEmitter | null} */ + '*eventEmitter': null, }); /** @param {import('./Block').Block} fnCtx */ diff --git a/abstract/EventManager.js b/abstract/EventManager.js deleted file mode 100644 index 82f4c8f6a..000000000 --- a/abstract/EventManager.js +++ /dev/null @@ -1,66 +0,0 @@ -/** @enum {String} */ -export const EVENT_TYPES = { - UPLOAD_START: 'UPLOAD_START', - REMOVE: 'REMOVE', - UPLOAD_PROGRESS: 'UPLOAD_PROGRESS', - UPLOAD_FINISH: 'UPLOAD_FINISH', - UPLOAD_ERROR: 'UPLOAD_ERROR', - VALIDATION_ERROR: 'VALIDATION_ERROR', - CLOUD_MODIFICATION: 'CLOUD_MODIFICATION', - DATA_OUTPUT: 'DATA_OUTPUT', - DONE_FLOW: 'DONE_FLOW', - INIT_FLOW: 'INIT_FLOW', -}; - -export class EventData { - /** - * @param {Object} src - * @param {EVENT_TYPES} src.type - * @param {String} src.ctx - * @param {any} [src.data] - */ - constructor(src) { - /** @type {String} */ - this.ctx = src.ctx; - /** @type {EVENT_TYPES} */ - this.type = src.type; - this.data = src.data; - } -} - -export class EventManager { - /** @param {EVENT_TYPES} type */ - static eName(type) { - return 'LR_' + type; - } - - /** @private */ - static _timeoutStore = Object.create(null); - - /** - * @param {EventData} eData - * @param {import('./UploaderBlock.js').UploaderBlock | Window} [el] - * @param {Boolean} [debounce] - */ - static emit(eData, el = window, debounce = true) { - let dispatch = () => { - el.dispatchEvent( - new CustomEvent(this.eName(eData.type), { - detail: eData, - }) - ); - }; - if (!debounce) { - dispatch(); - return; - } - let timeoutKey = eData.type + eData.ctx; - if (this._timeoutStore[timeoutKey]) { - window.clearTimeout(this._timeoutStore[timeoutKey]); - } - this._timeoutStore[timeoutKey] = window.setTimeout(() => { - dispatch(); - delete this._timeoutStore[timeoutKey]; - }, 20); - } -} diff --git a/abstract/UploaderBlock.js b/abstract/UploaderBlock.js index b66b46402..2ca642892 100644 --- a/abstract/UploaderBlock.js +++ b/abstract/UploaderBlock.js @@ -4,6 +4,7 @@ import { ActivityBlock } from './ActivityBlock.js'; import { Data } from '@symbiotejs/symbiote'; import { calculateMaxCenteredCropFrame } from '../blocks/CloudImageEditor/src/crop-utils.js'; import { parseCropPreset } from '../blocks/CloudImageEditor/src/lib/parseCropPreset.js'; +import { EventType } from '../blocks/UploadCtxProvider/EventEmitter.js'; import { UploadSource } from '../blocks/utils/UploadSource.js'; import { serializeCsv } from '../blocks/utils/comma-separated.js'; import { debounce } from '../blocks/utils/debounce.js'; @@ -14,7 +15,6 @@ import { prettyBytes } from '../utils/prettyBytes.js'; import { stringToArray } from '../utils/stringToArray.js'; import { warnOnce } from '../utils/warnOnce.js'; import { uploaderBlockCtx } from './CTX.js'; -import { EVENT_TYPES, EventData, EventManager } from './EventManager.js'; import { TypedCollection } from './TypedCollection.js'; import { uploadEntrySchema } from './uploadEntrySchema.js'; @@ -268,14 +268,7 @@ export class UploaderBlock extends ActivityBlock { this.setOrAddState('*modalActive', true); } } - EventManager.emit( - new EventData({ - type: EVENT_TYPES.INIT_FLOW, - ctx: this.ctxName, - }), - undefined, - false - ); + this.emit(EventType.INIT_FLOW); } doneFlow() { @@ -286,14 +279,7 @@ export class UploaderBlock extends ActivityBlock { if (!this.$['*currentActivity']) { this.setOrAddState('*modalActive', false); } - EventManager.emit( - new EventData({ - type: EVENT_TYPES.DONE_FLOW, - ctx: this.ctxName, - }), - undefined, - false - ); + this.emit(EventType.DONE_FLOW); } /** @returns {TypedCollection} */ @@ -420,15 +406,7 @@ export class UploaderBlock extends ActivityBlock { if (data.length !== this.uploadCollection.size) { return; } - EventManager.emit( - new EventData({ - type: EVENT_TYPES.DATA_OUTPUT, - // @ts-ignore TODO: fix this - ctx: this.ctxName, - // @ts-ignore TODO: fix this - data, - }) - ); + this.emit(EventType.DATA_OUTPUT, data); // @ts-ignore TODO: fix this this.$['*outputData'] = data; }, 100); @@ -483,15 +461,7 @@ export class UploaderBlock extends ActivityBlock { }); let progress = Math.round(commonProgress / items.length); this.$['*commonProgress'] = progress; - EventManager.emit( - new EventData({ - type: EVENT_TYPES.UPLOAD_PROGRESS, - ctx: this.ctxName, - data: progress, - }), - undefined, - progress === 100 - ); + this.emit(EventType.UPLOAD_PROGRESS, progress); } if (changeMap.fileInfo) { if (this.cfg.cropPreset) { @@ -507,14 +477,7 @@ export class UploaderBlock extends ActivityBlock { let data = this.getOutputData((dataItem) => { return !!dataItem.getValue('fileInfo') && !dataItem.getValue('silentUpload'); }); - data.length > 0 && - EventManager.emit( - new EventData({ - type: EVENT_TYPES.UPLOAD_FINISH, - ctx: this.ctxName, - data, - }) - ); + data.length > 0 && this.emit(EventType.UPLOAD_FINISH, data); } } if (changeMap.uploadError) { @@ -522,15 +485,7 @@ export class UploaderBlock extends ActivityBlock { return !!entry.getValue('uploadError'); }); items.forEach((id) => { - EventManager.emit( - new EventData({ - type: EVENT_TYPES.UPLOAD_ERROR, - ctx: this.ctxName, - data: uploadCollection.readProp(id, 'uploadError'), - }), - undefined, - false - ); + this.emit(EventType.UPLOAD_ERROR, uploadCollection.readProp(id, 'uploadError')); }); } if (changeMap.validationErrorMsg) { @@ -538,15 +493,7 @@ export class UploaderBlock extends ActivityBlock { return !!entry.getValue('validationErrorMsg'); }); items.forEach((id) => { - EventManager.emit( - new EventData({ - type: EVENT_TYPES.VALIDATION_ERROR, - ctx: this.ctxName, - data: uploadCollection.readProp(id, 'validationErrorMsg'), - }), - undefined, - false - ); + this.emit(EventType.VALIDATION_ERROR, uploadCollection.readProp(id, 'validationErrorMsg')); }); } if (changeMap.cdnUrlModifiers) { @@ -554,15 +501,7 @@ export class UploaderBlock extends ActivityBlock { return !!entry.getValue('cdnUrlModifiers'); }); items.forEach((id) => { - EventManager.emit( - new EventData({ - type: EVENT_TYPES.CLOUD_MODIFICATION, - ctx: this.ctxName, - data: Data.getCtx(id).store, - }), - undefined, - false - ); + this.emit(EventType.CLOUD_MODIFICATION, uploadCollection.readProp(id, 'cdnUrlModifiers')); }); } }; diff --git a/blocks/FileItem/FileItem.js b/blocks/FileItem/FileItem.js index 85bf40f89..401c4310e 100644 --- a/blocks/FileItem/FileItem.js +++ b/blocks/FileItem/FileItem.js @@ -1,9 +1,9 @@ // @ts-check import { UploadClientError, uploadFile } from '@uploadcare/upload-client'; import { ActivityBlock } from '../../abstract/ActivityBlock.js'; -import { EVENT_TYPES, EventData, EventManager } from '../../abstract/EventManager.js'; import { UploaderBlock } from '../../abstract/UploaderBlock.js'; import { createCdnUrl, createCdnUrlModifiers, createOriginalUrl } from '../../utils/cdn-utils.js'; +import { EventType } from '../UploadCtxProvider/EventEmitter.js'; import { fileCssBg } from '../svg-backgrounds/svg-backgrounds.js'; import { debounce } from '../utils/debounce.js'; import { generateThumb } from '../utils/resizeImage.js'; @@ -74,13 +74,7 @@ export class FileItem extends UploaderBlock { let data = this.getOutputData((dataItem) => { return dataItem.getValue('uuid') === entryUuid; }); - EventManager.emit( - new EventData({ - type: EVENT_TYPES.REMOVE, - ctx: this.ctxName, - data, - }) - ); + this.emit(EventType.REMOVE, data); } this.uploadCollection.remove(this.$.uid); }, @@ -370,13 +364,7 @@ export class FileItem extends UploaderBlock { return !dataItem.getValue('fileInfo'); }); - EventManager.emit( - new EventData({ - type: EVENT_TYPES.UPLOAD_START, - ctx: this.ctxName, - data, - }) - ); + this.emit(EventType.UPLOAD_START, data); this._debouncedCalculateState(); entry.setValue('isUploading', true); diff --git a/blocks/UploadCtxProvider/EventEmitter.js b/blocks/UploadCtxProvider/EventEmitter.js new file mode 100644 index 000000000..8bb616807 --- /dev/null +++ b/blocks/UploadCtxProvider/EventEmitter.js @@ -0,0 +1,95 @@ +// @ts-check + +export const EventType = Object.freeze({ + UPLOAD_START: 'uploadstart', + REMOVE: 'remove', + UPLOAD_PROGRESS: 'uploadprogress', + UPLOAD_FINISH: 'uploadfinish', + UPLOAD_ERROR: 'uploaderror', + VALIDATION_ERROR: 'validationerror', + CLOUD_MODIFICATION: 'cloudmodification', + DATA_OUTPUT: 'dataoutput', + DONE_FLOW: 'doneflow', + INIT_FLOW: 'initflow', +}); + +/** Those are legacy events that are saved for backward compatibility. Should be removed before v1. */ +export const GlobalEventType = Object.freeze({ + [EventType.UPLOAD_START]: 'LR_UPLOAD_START', + [EventType.REMOVE]: 'LR_REMOVE', + [EventType.UPLOAD_PROGRESS]: 'LR_UPLOAD_PROGRESS', + [EventType.UPLOAD_FINISH]: 'LR_UPLOAD_FINISH', + [EventType.UPLOAD_ERROR]: 'LR_UPLOAD_ERROR', + [EventType.VALIDATION_ERROR]: 'LR_VALIDATION_ERROR', + [EventType.CLOUD_MODIFICATION]: 'LR_CLOUD_MODIFICATION', + [EventType.DATA_OUTPUT]: 'LR_DATA_OUTPUT', + [EventType.DONE_FLOW]: 'LR_DONE_FLOW', + [EventType.INIT_FLOW]: 'LR_INIT_FLOW', +}); + +/** + * @typedef {{ + * [EventType.UPLOAD_START]: import('../../index.js').OutputFileEntry[]; + * [EventType.REMOVE]: import('../../index.js').OutputFileEntry[]; + * [EventType.UPLOAD_PROGRESS]: number; + * [EventType.UPLOAD_FINISH]: import('../../index.js').OutputFileEntry[]; + * [EventType.UPLOAD_ERROR]: Error | null; + * [EventType.VALIDATION_ERROR]: string | null; + * [EventType.CLOUD_MODIFICATION]: string | null; + * [EventType.DATA_OUTPUT]: import('../../index.js').OutputFileEntry[]; + * [EventType.DONE_FLOW]: never; + * [EventType.INIT_FLOW]: never; + * }} EventPayload + */ + +/** + * @typedef {{ + * [T in (typeof EventType)[keyof typeof EventType] as (typeof GlobalEventType)[T]]: { + * type: (typeof GlobalEventType)[T]; + * ctx: string; + * data: EventPayload[T]; + * }; + * }} GlobalEventPayload + */ + +export class EventEmitter { + /** @param {() => string} getCtxName */ + constructor(getCtxName) { + /** @private */ + this._getCtxName = getCtxName; + /** + * @private + * @type {import('../../abstract/Block.js').Block} + */ + } + + /** @param {import('../../abstract/Block.js').Block} target */ + bindTarget(target) { + /** @private */ + this._target = target; + } + + /** + * @template {(typeof EventType)[keyof typeof EventType]} T + * @param {T} type + * @param {EventPayload[T]} [payload] + */ + emit(type, payload) { + this._target?.dispatchEvent( + new CustomEvent(type, { + detail: payload, + }) + ); + + const globalEventType = `LR_${GlobalEventType[type]}}`; + window.dispatchEvent( + new CustomEvent(globalEventType, { + detail: { + ctx: this._getCtxName(), + type: globalEventType, + data: payload, + }, + }) + ); + } +} diff --git a/blocks/UploadCtxProvider/UploadCtxProvider.js b/blocks/UploadCtxProvider/UploadCtxProvider.js index 70b169dda..93b470865 100644 --- a/blocks/UploadCtxProvider/UploadCtxProvider.js +++ b/blocks/UploadCtxProvider/UploadCtxProvider.js @@ -1,5 +1,11 @@ +// @ts-check + import { UploaderBlock } from '../../abstract/UploaderBlock.js'; export class UploadCtxProvider extends UploaderBlock { requireCtxName = true; + + initCallback() { + this.$['*eventEmitter'].bindTarget(this); + } } diff --git a/blocks/UploadList/UploadList.js b/blocks/UploadList/UploadList.js index 5970b85ea..760c99262 100644 --- a/blocks/UploadList/UploadList.js +++ b/blocks/UploadList/UploadList.js @@ -1,8 +1,8 @@ // @ts-check -import { UploaderBlock } from '../../abstract/UploaderBlock.js'; import { ActivityBlock } from '../../abstract/ActivityBlock.js'; +import { UploaderBlock } from '../../abstract/UploaderBlock.js'; import { UiMessage } from '../MessageBox/MessageBox.js'; -import { EVENT_TYPES, EventData, EventManager } from '../../abstract/EventManager.js'; +import { EventType } from '../UploadCtxProvider/EventEmitter.js'; import { debounce } from '../utils/debounce.js'; /** @@ -47,13 +47,7 @@ export class UploadList extends UploaderBlock { let data = this.getOutputData((dataItem) => { return !!dataItem.getValue('fileInfo'); }); - EventManager.emit( - new EventData({ - type: EVENT_TYPES.REMOVE, - ctx: this.ctxName, - data, - }) - ); + this.emit(EventType.REMOVE, data); this.uploadCollection.clearAll(); }, }; diff --git a/blocks/test/raw-regular.htm b/blocks/test/raw-regular.htm index 28ed61086..4b0a8c32f 100644 --- a/blocks/test/raw-regular.htm +++ b/blocks/test/raw-regular.htm @@ -26,9 +26,19 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/types/events.d.ts b/types/events.d.ts index 43c45c330..a4c144cdd 100644 --- a/types/events.d.ts +++ b/types/events.d.ts @@ -1,24 +1,13 @@ -// TODO: Add event types -interface CustomEventMap { - UPLOAD_START: CustomEvent; - REMOVE: CustomEvent; - UPLOAD_PROGRESS: CustomEvent; - UPLOAD_FINISH: CustomEvent; - UPLOAD_ERROR: CustomEvent; - VALIDATION_ERROR: CustomEvent; - CLOUD_MODIFICATION: CustomEvent; - DATA_OUTPUT: CustomEvent; - DONE_FLOW: CustomEvent; - INIT_FLOW: CustomEvent; -} +import type { GlobalEventPayload } from '../blocks/UploadCtxProvider/EventEmitter'; + +type CustomEventMap = { + [T in keyof GlobalEventPayload]: CustomEvent; +}; declare global { interface Window { - addEventListener(type: `LR_${K}`, listener: (e: CustomEventMap[K]) => void): void; - removeEventListener( - type: `LR_${K}`, - listener: (e: CustomEventMap[K]) => void - ): void; + addEventListener(type: T, listener: (e: CustomEventMap[T]) => void): void; + removeEventListener(type: T, listener: (e: CustomEventMap[T]) => void): void; } } -export {}; \ No newline at end of file +export {}; diff --git a/types/exported.d.ts b/types/exported.d.ts index 1f19b38ef..4b50d8b48 100644 --- a/types/exported.d.ts +++ b/types/exported.d.ts @@ -72,4 +72,6 @@ export type OutputFileEntry = Pick & uploadProgress: number; }; +export { EventType, GlobalEventType, GlobalEventPayload, EventPayload } from '../blocks/UploadCtxProvider/EventEmitter'; + export {}; diff --git a/types/jsx.d.ts b/types/jsx.d.ts index d62081399..56ea28634 100644 --- a/types/jsx.d.ts +++ b/types/jsx.d.ts @@ -82,7 +82,7 @@ declare namespace JSX { 'lr-file-uploader-regular': CustomElement; 'lr-file-uploader-minimal': CustomElement; 'lr-file-uploader-inline': CustomElement; - 'lr-upload-ctx-provider': CustomElement; + 'lr-upload-ctx-provider': CustomElement; 'lr-config': CustomElement; } } \ No newline at end of file