@uploadcare/file-uploader
Version:
Building blocks for Uploadcare products integration
411 lines (362 loc) • 11.4 kB
JavaScript
// @ts-check
import { BaseComponent, Data } from '@symbiotejs/symbiote';
import { initialConfig } from '../blocks/Config/initialConfig.js';
import { EventEmitter } from '../blocks/UploadCtxProvider/EventEmitter.js';
import { WindowHeightTracker } from '../utils/WindowHeightTracker.js';
import { extractFilename, extractCdnUrlModifiers, extractUuid } from '../utils/cdn-utils.js';
import { getLocaleDirection } from '../utils/getLocaleDirection.js';
import { getPluralForm } from '../utils/getPluralForm.js';
import { applyTemplateData, getPluralObjects } from '../utils/template-utils.js';
import { waitForAttribute } from '../utils/waitForAttribute.js';
import { blockCtx } from './CTX.js';
import { LocaleManager, localeStateKey } from './managers/LocaleManager.js';
import { l10nProcessor } from './l10nProcessor.js';
import { sharedConfigKey } from './sharedConfigKey.js';
import { testModeProcessor } from './testModeProcessor.js';
import { A11y } from './managers/a11y.js';
import { ModalManager } from './managers/ModalManager.js';
import { TelemetryManager } from './managers/TelemetryManager.js';
const TAG_PREFIX = 'uc-';
// @ts-ignore TODO: fix this
export class Block extends BaseComponent {
/** @type {string | null} */
static StateConsumerScope = null;
/** @type {string[]} */
static styleAttrs = [];
/** @protected */
requireCtxName = false;
/** @type {import('./ActivityBlock.js').ActivityType} */
activityType = null;
init$ = blockCtx();
/**
* @param {String} str
* @param {{ [key: string]: string | number }} variables
* @returns {String}
*/
l10n(str, variables = {}) {
if (!str) {
return '';
}
let template = this.$[localeStateKey(str)] || str;
let pluralObjects = getPluralObjects(template);
for (let pluralObject of pluralObjects) {
variables[pluralObject.variable] = this.pluralize(
pluralObject.pluralKey,
Number(variables[pluralObject.countVariable]),
);
}
let result = applyTemplateData(template, variables);
return result;
}
/**
* @private
* @param {string} key
* @param {number} count
* @returns {string}
*/
pluralize(key, count) {
const locale = this.l10n('locale-id') || 'en';
const pluralForm = getPluralForm(locale, count);
return this.l10n(`${key}__${pluralForm}`);
}
/**
* @param {string} key
* @param {() => void} resolver
* @protected
*/
bindL10n(key, resolver) {
this.localeManager?.bindL10n(this, key, resolver);
}
constructor() {
super();
/** @type {Map<string, Set<{ remove: () => void }>>} */
this.l10nProcessorSubs = new Map();
// @ts-ignore TODO: fix this
this.addTemplateProcessor(l10nProcessor);
// @ts-ignore TODO: fix this
this.addTemplateProcessor(testModeProcessor);
}
/**
* @param {Parameters<import('../blocks/UploadCtxProvider/EventEmitter.js').EventEmitter['emit']>[0]} type
* @param {Parameters<import('../blocks/UploadCtxProvider/EventEmitter.js').EventEmitter['emit']>[1]} [payload]
* @param {Parameters<import('../blocks/UploadCtxProvider/EventEmitter.js').EventEmitter['emit']>[2]} [options]
*/
emit(type, payload, options) {
/** @type {import('../blocks/UploadCtxProvider/EventEmitter.js').EventEmitter} */
const eventEmitter = this.has('*eventEmitter') && this.$['*eventEmitter'];
if (!eventEmitter) {
return;
}
eventEmitter.emit(type, payload, options);
this.telemetryManager.sendEvent({
eventType: type,
payload: typeof payload === 'function' ? payload() : payload,
});
}
/**
* @param {(block: Block) => boolean} callback
* @returns {Boolean}
*/
hasBlockInCtx(callback) {
for (let block of this.blocksRegistry) {
if (callback(block)) {
return true;
}
}
return false;
}
/**
* @param {String} prop
* @param {any} newVal
*/
setOrAddState(prop, newVal) {
this.add$(
{
[prop]: newVal,
},
true,
);
}
/** @protected */
connectedCallback() {
const styleAttrs = /** @type {typeof Block} */ (this.constructor).styleAttrs;
styleAttrs.forEach((attr) => {
this.setAttribute(attr, '');
});
if (this.hasAttribute('retpl')) {
// @ts-ignore TODO: fix this
this.constructor['template'] = null;
this.processInnerHtml = true;
}
if (this.requireCtxName) {
waitForAttribute({
element: this,
attribute: 'ctx-name',
onSuccess: () => {
// async wait for ctx-name attribute to be set, needed for Angular because it sets attributes after mount
// TODO: should be moved to the symbiote core
super.connectedCallback();
},
onTimeout: () => {
console.error('Attribute `ctx-name` is required and it is not set.');
},
});
} else {
super.connectedCallback();
}
WindowHeightTracker.registerClient(this);
}
/** @protected */
disconnectedCallback() {
super.disconnectedCallback();
WindowHeightTracker.unregisterClient(this);
}
/** @protected */
initCallback() {
if (!this.has('*blocksRegistry')) {
this.add('*blocksRegistry', new Set());
}
let blocksRegistry = this.$['*blocksRegistry'];
blocksRegistry.add(this);
if (!this.has('*eventEmitter')) {
this.add('*eventEmitter', new EventEmitter(this.debugPrint.bind(this)));
}
if (!this.has('*localeManager')) {
this.add('*localeManager', new LocaleManager(this));
}
if (this.cfg.qualityInsights && !this.has('*telemetryManager')) {
this.add('*telemetryManager', new TelemetryManager(this));
}
if (!this.has('*a11y')) {
this.add('*a11y', new A11y());
}
if (!this.has('*modalManager')) {
this.add('*modalManager', new ModalManager(this));
}
this.sub(localeStateKey('locale-id'), (localeId) => {
const direction = getLocaleDirection(localeId);
this.style.direction = direction === 'ltr' ? '' : direction;
});
this.subConfigValue('testMode', (testMode) => {
if (!testMode || !this.testId) {
this.removeAttribute('data-testid');
return;
}
this.setAttribute('data-testid', this.testId);
});
}
get testId() {
const testId = window.customElements.getName(/** @type {CustomElementConstructor} */ (this.constructor));
return testId;
}
/**
* @returns {ModalManager}
* @public
*/
get modalManager() {
return this.has('*modalManager') && this.$['*modalManager'];
}
/**
* @returns {TelemetryManager
* | { sendEvent: () => void; sendEventCloudImageEditor: () => void; sendEventError: () => void }}
* @public
*/
get telemetryManager() {
if (!this.cfg.qualityInsights) {
return {
sendEvent: () => {},
sendEventCloudImageEditor: () => {},
sendEventError: () => {},
};
}
return this.has('*telemetryManager') && this.$['*telemetryManager'];
}
/**
* @private
* @returns {LocaleManager | null}
*/
get localeManager() {
return this.has('*localeManager') ? this.$['*localeManager'] : null;
}
/**
* @returns {A11y | null}
* @protected
*/
get a11y() {
return this.has('*a11y') ? this.$['*a11y'] : null;
}
/** @type {Set<Block>} */
get blocksRegistry() {
return this.$['*blocksRegistry'];
}
/** @protected */
destroyCallback() {
super.destroyCallback();
let blocksRegistry = this.blocksRegistry;
blocksRegistry?.delete(this);
this.localeManager?.destroyL10nBindings(this);
this.l10nProcessorSubs = new Map();
// Destroy local context
// TODO: this should be done inside symbiote
Data.deleteCtx(this);
if (blocksRegistry?.size === 0) {
setTimeout(() => {
// Destroy global context after all blocks are destroyed and all callbacks are run
this.destroyCtxCallback();
}, 0);
}
}
/**
* Called when the last block is removed from the context. Note that inheritors must run their callback before that.
*
* @protected
*/
destroyCtxCallback() {
Data.deleteCtx(this.ctxName);
this.localeManager?.destroy();
this.modalManager && this.modalManager?.destroy();
}
/**
* @param {String} url
* @returns {Promise<String>}
* @protected
*/
async proxyUrl(url) {
if (this.cfg.secureDeliveryProxy && this.cfg.secureDeliveryProxyUrlResolver) {
console.warn(
'Both secureDeliveryProxy and secureDeliveryProxyUrlResolver are set. The secureDeliveryProxyUrlResolver will be used.',
);
}
if (this.cfg.secureDeliveryProxyUrlResolver) {
try {
return await this.cfg.secureDeliveryProxyUrlResolver(url, {
uuid: extractUuid(url),
cdnUrlModifiers: extractCdnUrlModifiers(url),
fileName: extractFilename(url),
});
} catch (err) {
console.error('Failed to resolve secure delivery proxy URL. Falling back to the default URL.', err);
this.telemetryManager.sendEventError(
err,
'secureDeliveryProxyUrlResolver. Failed to resolve secure delivery proxy URL. Falling back to the default URL.',
);
return url;
}
}
if (this.cfg.secureDeliveryProxy) {
return applyTemplateData(
this.cfg.secureDeliveryProxy,
{ previewUrl: url },
{ transform: (value) => window.encodeURIComponent(value) },
);
}
return url;
}
/** @returns {import('../types').ConfigType} } */
get cfg() {
if (!this.__cfgProxy) {
let o = Object.create(null);
/** @private */
this.__cfgProxy = new Proxy(o, {
set: (obj, key, value) => {
if (typeof key !== 'string') {
return false;
}
const sharedKey = sharedConfigKey(/** @type {keyof import('../types').ConfigType} */ (key));
if (!this.has(sharedKey)) {
this.add(sharedKey, initialConfig[/** @type {keyof import('../types').ConfigType} */ (key)]);
}
this.$[sharedKey] = value;
return true;
},
/**
* @param {never} obj
* @param {keyof import('../types').ConfigType} key
*/
get: (obj, key) => {
const sharedKey = sharedConfigKey(key);
if (!this.has(sharedKey)) {
this.add(sharedKey, initialConfig[key]);
}
return this.$[sharedConfigKey(key)];
},
});
}
return this.__cfgProxy;
}
/**
* @template {keyof import('../types').ConfigType} T
* @param {T} key
* @param {(value: import('../types').ConfigType[T]) => void} callback
*/
subConfigValue(key, callback) {
const sharedKey = sharedConfigKey(key);
if (!this.has(sharedKey)) {
this.add(sharedKey, initialConfig[key]);
}
this.sub(sharedKey, callback);
}
/** @param {unknown[]} args */
debugPrint(...args) {
if (!this.cfg.debug) {
return;
}
let consoleArgs = args;
if (typeof args?.[0] === 'function') {
const resolver = args[0];
consoleArgs = resolver();
}
console.log(`[${this.ctxName}]`, ...consoleArgs);
}
/** @param {String} [name] */
static reg(name) {
if (!name) {
super.reg();
return;
}
if (name.startsWith(TAG_PREFIX)) {
super.reg(name);
}
}
}
export { BaseComponent };