UNPKG

@uploadcare/file-uploader

Version:

Building blocks for Uploadcare products integration

1,665 lines (1,635 loc) 406 kB
/** * @license * MIT License * * Copyright (c) 2025 Uploadcare (hello@uploadcare.com). All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * */ // src/index.ts import { BaseComponent as BaseComponent4, Data as Data8, UID as UID5 } from "@symbiotejs/symbiote"; // src/blocks/UploadCtxProvider/EventEmitter.ts var DEFAULT_DEBOUNCE_TIMEOUT = 20; var InternalEventType = Object.freeze({ INIT_SOLUTION: "init-solution", CHANGE_CONFIG: "change-config", ACTION_EVENT: "action-event", ERROR_EVENT: "error-event" }); var EventType = Object.freeze({ FILE_ADDED: "file-added", FILE_REMOVED: "file-removed", FILE_UPLOAD_START: "file-upload-start", FILE_UPLOAD_PROGRESS: "file-upload-progress", FILE_UPLOAD_SUCCESS: "file-upload-success", FILE_UPLOAD_FAILED: "file-upload-failed", FILE_URL_CHANGED: "file-url-changed", MODAL_OPEN: "modal-open", MODAL_CLOSE: "modal-close", DONE_CLICK: "done-click", UPLOAD_CLICK: "upload-click", ACTIVITY_CHANGE: "activity-change", COMMON_UPLOAD_START: "common-upload-start", COMMON_UPLOAD_PROGRESS: "common-upload-progress", COMMON_UPLOAD_SUCCESS: "common-upload-success", COMMON_UPLOAD_FAILED: "common-upload-failed", CHANGE: "change", GROUP_CREATED: "group-created" }); var EventEmitter = class { _timeoutStore = /* @__PURE__ */ new Map(); _targets = /* @__PURE__ */ new Set(); _debugPrint = null; constructor(debugPrint) { this._debugPrint = debugPrint; } bindTarget(target) { this._targets.add(target); } unbindTarget(target) { this._targets.delete(target); } _dispatch(type, payload) { for (const target of this._targets) { target.dispatchEvent( new CustomEvent(type, { detail: payload }) ); } this._debugPrint?.(() => { const copyPayload = !!payload && typeof payload === "object" ? { ...payload } : payload; return [`event "${type}"`, copyPayload]; }); } emit(type, payload, options = {}) { const { debounce: debounce2 } = options; if (typeof debounce2 !== "number" && !debounce2) { this._dispatch(type, typeof payload === "function" ? payload() : payload); return; } if (this._timeoutStore.has(type)) { window.clearTimeout(this._timeoutStore.get(type)); } const timeout = typeof debounce2 === "number" ? debounce2 : DEFAULT_DEBOUNCE_TIMEOUT; const timeoutId = window.setTimeout(() => { this._dispatch(type, typeof payload === "function" ? payload() : payload); this._timeoutStore.delete(type); }, timeout); this._timeoutStore.set(type, timeoutId); } }; // src/utils/debounce.ts function debounce(callback, wait) { let timer; const debounced = ((...args) => { if (timer) clearTimeout(timer); timer = setTimeout(() => callback(...args), wait); }); debounced.cancel = () => { if (timer) clearTimeout(timer); }; return debounced; } // src/abstract/Block.ts import { BaseComponent, Data } from "@symbiotejs/symbiote"; // src/utils/comma-separated.ts var deserializeCsv = (value) => { if (!value) { return []; } return value.split(",").map((item) => item.trim()).filter(Boolean); }; var serializeCsv = (value) => { return value.join(","); }; // src/utils/cdn-utils.ts var normalizeCdnOperation = (operation) => { if (typeof operation !== "string" || !operation) { return ""; } let str = operation.trim(); if (str.startsWith("-/")) { str = str.slice(2); } else if (str.startsWith("/")) { str = str.slice(1); } if (str.endsWith("/")) { str = str.slice(0, str.length - 1); } return str; }; var joinCdnOperations = (...operations) => { return operations.filter((op) => typeof op === "string" && !!op).map((op) => normalizeCdnOperation(op)).join("/-/"); }; var createCdnUrlModifiers = (...cdnOperations) => { const joined = joinCdnOperations(...cdnOperations); return joined ? `-/${joined}/` : ""; }; function extractFilename(cdnUrl) { const url = new URL(cdnUrl); const noOrigin = url.pathname + url.search + url.hash; const urlFilenameIdx = noOrigin.lastIndexOf("http"); const plainFilenameIdx = noOrigin.lastIndexOf("/"); let filename = ""; if (urlFilenameIdx >= 0) { filename = noOrigin.slice(urlFilenameIdx); } else if (plainFilenameIdx >= 0) { filename = noOrigin.slice(plainFilenameIdx + 1); } return filename; } function extractUuid(cdnUrl) { const url = new URL(cdnUrl); const { pathname } = url; const slashIndex = pathname.indexOf("/"); const secondSlashIndex = pathname.indexOf("/", slashIndex + 1); return pathname.substring(slashIndex + 1, secondSlashIndex); } function extractCdnUrlModifiers(cdnUrl) { const withoutFilename = trimFilename(cdnUrl); const url = new URL(withoutFilename); const operationsMarker = url.pathname.indexOf("/-/"); if (operationsMarker === -1) { return ""; } const operationsStr = url.pathname.substring(operationsMarker).slice(1); return operationsStr; } function extractOperations(cdnUrl) { const operationsStr = extractCdnUrlModifiers(cdnUrl); return operationsStr.split("/-/").filter(Boolean).map((operation) => normalizeCdnOperation(operation)); } function trimFilename(cdnUrl) { const url = new URL(cdnUrl); const filename = extractFilename(cdnUrl); const filenamePathPart = isFileUrl(filename) ? splitFileUrl(filename).pathname : filename; url.pathname = url.pathname.replace(filenamePathPart, ""); url.search = ""; url.hash = ""; return url.toString(); } function isFileUrl(filename) { return filename.startsWith("http"); } function splitFileUrl(fileUrl) { const url = new URL(fileUrl); return { pathname: `${url.origin}${url.pathname ?? ""}`, search: url.search ?? "", hash: url.hash ?? "" }; } var createCdnUrl = (baseCdnUrl, cdnModifiers, filename) => { const url = new URL(trimFilename(baseCdnUrl)); const resolvedFilename = filename ?? extractFilename(baseCdnUrl); const resolvedModifiers = cdnModifiers ?? ""; if (url.pathname.startsWith("//")) { url.pathname = url.pathname.replace("//", "/"); } if (resolvedFilename && isFileUrl(resolvedFilename)) { const splitted = splitFileUrl(resolvedFilename); url.pathname = `${url.pathname}${resolvedModifiers}${splitted.pathname || ""}`; url.search = splitted.search; url.hash = splitted.hash; } else { url.pathname = `${url.pathname}${resolvedModifiers}${resolvedFilename || ""}`; } return url.toString(); }; var createOriginalUrl = (cdnUrl, uuid) => { const url = new URL(cdnUrl); url.pathname = `${uuid}/`; return url.toString(); }; // src/utils/stringToArray.ts var stringToArray = (str, delimiter = ",") => { return str.trim().split(delimiter).map((part) => part.trim()).filter((part) => part.length > 0); }; // src/blocks/CloudImageEditor/src/lib/transformationUtils.ts var OPERATIONS_DEFAULTS = Object.freeze({ brightness: 0, exposure: 0, gamma: 100, contrast: 0, saturation: 0, vibrance: 0, warmth: 0, enhance: 0, filter: 0, rotate: 0, mirror: false, flip: false, crop: void 0 }); var SUPPORTED_OPERATIONS_ORDERED = [ "enhance", "brightness", "exposure", "gamma", "contrast", "saturation", "vibrance", "warmth", "filter", "mirror", "flip", "rotate", "crop" ]; function transformationToStr(operation, options) { if (typeof options === "number") { const value = options; return OPERATIONS_DEFAULTS[operation] !== value ? `${operation}/${value}` : ""; } if (typeof options === "boolean") { const value = options; return OPERATIONS_DEFAULTS[operation] !== value ? `${operation}` : ""; } if (operation === "filter" && options) { const { name, amount } = options; if (OPERATIONS_DEFAULTS.filter === amount) { return ""; } return `${operation}/${name}/${amount}`; } if (operation === "crop" && options) { const { dimensions, coords } = options; return `${operation}/${dimensions.join("x")}/${coords.join(",")}`; } return ""; } function transformationsToOperations(transformations) { return joinCdnOperations( ...SUPPORTED_OPERATIONS_ORDERED.filter( (operation) => typeof transformations[operation] !== "undefined" && transformations[operation] !== null ).map((operation) => { const options = transformations[operation]; return transformationToStr(operation, options); }).filter((str) => !!str) ); } var COMMON_OPERATIONS = joinCdnOperations("format/auto", "progressive/yes"); var asNumber = ([value]) => typeof value !== "undefined" ? Number(value) : void 0; var asBoolean = () => true; var asFilter = ([name, amount]) => ({ name, amount: typeof amount !== "undefined" ? Number(amount) : 100 }); var asCrop = ([dimensions, alignment]) => { if (!/\d+x\d+/.test(dimensions) || !/\d+,\d+/.test(alignment)) { throw new Error("Crop by aspect ratio, percentage or alignment shortcuts is not supported."); } return { dimensions: stringToArray(dimensions, "x").map(Number), coords: stringToArray(alignment).map(Number) }; }; var OPERATION_PROCESSORS = Object.freeze({ enhance: asNumber, brightness: asNumber, exposure: asNumber, gamma: asNumber, contrast: asNumber, saturation: asNumber, vibrance: asNumber, warmth: asNumber, filter: asFilter, mirror: asBoolean, flip: asBoolean, rotate: asNumber, crop: (args) => { const [dimensions, alignment] = args; const { dimensions: parsedDimensions, coords } = asCrop([dimensions, alignment]); return { dimensions: parsedDimensions, coords }; } }); function operationsToTransformations(operations) { const transformations = {}; for (const operation of operations) { const [name, ...args] = operation.split("/"); if (!name || !SUPPORTED_OPERATIONS_ORDERED.includes(name)) { continue; } const operationName = name; const processor = OPERATION_PROCESSORS[operationName]; try { const value = processor(args); transformations[operationName] = value; } catch (err) { console.warn( [ `Failed to parse URL operation "${operation}". It will be ignored.`, err instanceof Error ? `Error message: "${err.message}"` : err, "If you need this functionality, please feel free to open an issue at https://github.com/uploadcare/blocks/issues/new" ].join("\n") ); } } return transformations; } // src/blocks/CloudImageEditor/src/toolbar-constants.ts var TabId = Object.freeze({ CROP: "crop", TUNING: "tuning", FILTERS: "filters" }); var ALL_TABS = Object.freeze([TabId.CROP, TabId.TUNING, TabId.FILTERS]); var ALL_COLOR_OPERATIONS = Object.freeze([ "brightness", "exposure", "gamma", "contrast", "saturation", "vibrance", "warmth", "enhance" ]); var ALL_FILTERS = Object.freeze([ "adaris", "briaril", "calarel", "carris", "cynarel", "cyren", "elmet", "elonni", "enzana", "erydark", "fenralan", "ferand", "galen", "gavin", "gethriel", "iorill", "iothari", "iselva", "jadis", "lavra", "misiara", "namala", "nerion", "nethari", "pamaya", "sarnar", "sedis", "sewen", "sorahel", "sorlen", "tarian", "thellassan", "varriel", "varven", "vevera", "virkas", "yedis", "yllara", "zatvel", "zevcen" ]); var ALL_CROP_OPERATIONS = Object.freeze(["rotate", "mirror", "flip"]); var NUMERIC_OPERATION_DEFAULTS = OPERATIONS_DEFAULTS; var COLOR_OPERATIONS_CONFIG = Object.freeze({ brightness: { zero: NUMERIC_OPERATION_DEFAULTS.brightness, range: [-100, 100], keypointsNumber: 2 }, exposure: { zero: NUMERIC_OPERATION_DEFAULTS.exposure, range: [-500, 500], keypointsNumber: 2 }, gamma: { zero: NUMERIC_OPERATION_DEFAULTS.gamma, range: [0, 1e3], keypointsNumber: 2 }, contrast: { zero: NUMERIC_OPERATION_DEFAULTS.contrast, range: [-100, 500], keypointsNumber: 2 }, saturation: { zero: NUMERIC_OPERATION_DEFAULTS.saturation, range: [-100, 500], keypointsNumber: 1 }, vibrance: { zero: NUMERIC_OPERATION_DEFAULTS.vibrance, range: [-100, 500], keypointsNumber: 1 }, warmth: { zero: NUMERIC_OPERATION_DEFAULTS.warmth, range: [-100, 100], keypointsNumber: 1 }, enhance: { zero: NUMERIC_OPERATION_DEFAULTS.enhance, range: [0, 100], keypointsNumber: 1 }, filter: { zero: NUMERIC_OPERATION_DEFAULTS.filter, range: [0, 100], keypointsNumber: 1 } }); // src/blocks/Config/initialConfig.ts var DEFAULT_CDN_CNAME = "https://ucarecdn.com"; var DEFAULT_BASE_URL = "https://upload.uploadcare.com"; var DEFAULT_SOCIAL_BASE_URL = "https://social.uploadcare.com"; var DEFAULT_PREFIXED_CDN_BASE_DOMAIN = "https://ucarecd.net"; var config = { pubkey: "", multiple: true, multipleMin: 0, multipleMax: Number.MAX_SAFE_INTEGER, confirmUpload: false, imgOnly: false, accept: "", externalSourcesPreferredTypes: "", externalSourcesEmbedCss: "", store: "auto", cameraMirror: false, cameraCapture: "", sourceList: "local, url, camera, dropbox, gdrive", topLevelOrigin: "", cloudImageEditorTabs: serializeCsv(ALL_TABS), maxLocalFileSizeBytes: 0, thumbSize: 76, showEmptyList: false, useLocalImageEditor: false, useCloudImageEditor: true, removeCopyright: false, cropPreset: "", imageShrink: "", modalScrollLock: true, modalBackdropStrokes: false, sourceListWrap: true, remoteTabSessionKey: "", cdnCname: DEFAULT_CDN_CNAME, cdnCnamePrefixed: DEFAULT_PREFIXED_CDN_BASE_DOMAIN, baseUrl: DEFAULT_BASE_URL, socialBaseUrl: DEFAULT_SOCIAL_BASE_URL, secureSignature: "", secureExpire: "", secureDeliveryProxy: "", retryThrottledRequestMaxTimes: 3, retryNetworkErrorMaxTimes: 3, multipartMinFileSize: 26214400, multipartChunkSize: 5242880, maxConcurrentRequests: 10, multipartMaxConcurrentRequests: 4, multipartMaxAttempts: 3, checkForUrlDuplicates: false, saveUrlForRecurrentUploads: false, groupOutput: false, userAgentIntegration: "", debug: false, metadata: null, localeName: "en", localeDefinitionOverride: null, secureUploadsExpireThreshold: 10 * 60 * 1e3, secureUploadsSignatureResolver: null, secureDeliveryProxyUrlResolver: null, iconHrefResolver: null, fileValidators: [], collectionValidators: [], validationTimeout: 15 * 1e3, validationConcurrency: 100, cameraModes: "photo, video", defaultCameraMode: null, enableAudioRecording: true, enableVideoRecording: null, maxVideoRecordingDuration: null, mediaRecorderOptions: null, filesViewMode: "list", gridShowFileNames: false, cloudImageEditorAutoOpen: false, cloudImageEditorMaskHref: null, testMode: false, qualityInsights: true }; var initialConfig = Object.freeze(config); // src/utils/getLocaleDirection.ts var getLocaleDirection = (localeId) => { const locale2 = new Intl.Locale(localeId); let direction = "ltr"; const fromGetter = locale2.getTextInfo?.().direction; if (fromGetter) { direction = fromGetter; } else if (locale2.textInfo?.direction) { direction = locale2.textInfo.direction; } return direction; }; // src/utils/getPluralForm.ts var getPluralForm = (locale2, count) => { const pluralForm = new Intl.PluralRules(locale2).select(count); return pluralForm; }; // src/utils/template-utils.ts var DEFAULT_TRANSFORMER = (value) => value; var OPEN_TOKEN = "{{"; var CLOSE_TOKEN = "}}"; var PLURAL_PREFIX = "plural:"; function applyTemplateData(template, data = {}, options = {}) { const { openToken = OPEN_TOKEN, closeToken = CLOSE_TOKEN, transform = DEFAULT_TRANSFORMER } = options; for (const key in data) { const rawValue = data[key]; const value = rawValue != null ? rawValue.toString() : void 0; const replacement = typeof value === "string" ? transform(value) : String(value); template = template.replaceAll(openToken + key + closeToken, replacement); } return template; } function getPluralObjects(template) { const pluralObjects = []; let open = template.indexOf(OPEN_TOKEN); while (open !== -1) { const close = template.indexOf(CLOSE_TOKEN, open); if (close === -1) { break; } const variable = template.substring(open + 2, close); if (variable.startsWith(PLURAL_PREFIX)) { const keyValue = template.substring(open + 2, close).replace(PLURAL_PREFIX, ""); const key = keyValue.substring(0, keyValue.indexOf("(")); const count = keyValue.substring(keyValue.indexOf("(") + 1, keyValue.indexOf(")")); pluralObjects.push({ variable, pluralKey: key, countVariable: count }); } open = template.indexOf(OPEN_TOKEN, close); } return pluralObjects; } // src/utils/WindowHeightTracker.ts var WINDOW_HEIGHT_TRACKER_PROPERTY = "--uploadcare-blocks-window-height"; var WindowHeightTracker = class _WindowHeightTracker { static clientsRegistry = /* @__PURE__ */ new Set(); static flush = debounce(() => { document.documentElement.style.setProperty(WINDOW_HEIGHT_TRACKER_PROPERTY, `${window.innerHeight}px`); }, 100); static registerClient(client) { if (_WindowHeightTracker.clientsRegistry.size === 0) { _WindowHeightTracker.attachTracker(); } _WindowHeightTracker.clientsRegistry.add(client); } static unregisterClient(client) { _WindowHeightTracker.clientsRegistry.delete(client); if (_WindowHeightTracker.clientsRegistry.size === 0) { _WindowHeightTracker.detachTracker(); } } static attachTracker() { window.addEventListener("resize", _WindowHeightTracker.flush, { passive: true, capture: true }); _WindowHeightTracker.flush(); } static detachTracker() { window.removeEventListener("resize", _WindowHeightTracker.flush, { capture: true }); document.documentElement.style.removeProperty(WINDOW_HEIGHT_TRACKER_PROPERTY); } }; // src/utils/waitForAttribute.ts var waitForAttribute = ({ element, attribute, onSuccess, onTimeout, timeout = 300 }) => { const currentAttrValue = element.getAttribute(attribute); if (currentAttrValue !== null) { onSuccess(currentAttrValue); return; } const observer = new MutationObserver((mutations) => { const mutation = mutations[mutations.length - 1]; if (mutation) { handleMutation(mutation); } }); observer.observe(element, { attributes: true, attributeFilter: [attribute] }); const timeoutId = window.setTimeout(() => { observer.disconnect(); onTimeout(); }, timeout); const handleMutation = (mutation) => { const attrValue = element.getAttribute(attribute); if (mutation.type === "attributes" && mutation.attributeName === attribute && attrValue !== null) { window.clearTimeout(timeoutId); observer.disconnect(); onSuccess(attrValue); } }; }; // src/abstract/CTX.ts import { Queue } from "@uploadcare/upload-client"; var blockCtx = () => ({}); var activityBlockCtx = (fnCtx) => ({ ...blockCtx(), "*currentActivity": null, "*currentActivityParams": {}, "*history": [], "*historyBack": null, "*closeModal": () => { fnCtx.modalManager?.close(fnCtx.$["*currentActivity"]); fnCtx.set$({ "*currentActivity": null }); } }); var uploaderBlockCtx = (fnCtx) => ({ ...activityBlockCtx(fnCtx), "*commonProgress": 0, "*uploadList": [], "*uploadQueue": new Queue(1), "*collectionErrors": [], "*collectionState": null, "*groupInfo": null, "*uploadTrigger": /* @__PURE__ */ new Set(), "*secureUploadsManager": null }); var solutionBlockCtx = (fnCtx) => ({ ...uploaderBlockCtx(fnCtx), "*solution": null }); // src/locales/file-uploader/en.js var en_default = { "locale-id": "en", "social-source-lang": "en", "upload-file": "Upload file", "upload-files": "Upload files", "choose-file": "Choose file", "choose-files": "Choose files", "drop-files-here": "Drop files here", "select-file-source": "Select file source", selected: "Selected", upload: "Upload", "add-more": "Add more", cancel: "Cancel", "start-from-cancel": "Cancel", clear: "Clear", "camera-shot": "Shot", "upload-url": "Import", "upload-url-placeholder": "Paste link here", "edit-image": "Edit image", "edit-detail": "Details", back: "Back", done: "Done", ok: "Ok", "remove-from-list": "Remove", no: "No", yes: "Yes", "confirm-your-action": "Confirm your action", "are-you-sure": "Are you sure?", "selected-count": "{{count}} of {{total}} selected", "select-all": "Select all", "deselect-all": "Deselect all", "upload-error": "Upload error", "validation-error": "Validation error", "no-files": "No files selected", browse: "Browse", "not-uploaded-yet": "Not uploaded yet...", file__one: "file", file__other: "files", error__one: "error", error__other: "errors", "header-uploading": "Uploading {{count}} {{plural:file(count)}}", "header-failed": "{{count}} {{plural:error(count)}}", "header-succeed": "{{count}} {{plural:file(count)}} uploaded", "header-total": "{{count}} {{plural:file(count)}} selected", "src-type-local": "From device", "src-type-from-url": "From link", "src-type-camera": "Camera", "src-type-mobile-video-camera": "Video", "src-type-mobile-photo-camera": "Photo", "src-type-draw": "Draw", "src-type-facebook": "Facebook", "src-type-dropbox": "Dropbox", "src-type-gdrive": "Google Drive", "src-type-ngdrive": "Google Drive", "src-type-gphotos": "Google Photos", "src-type-flickr": "Flickr", "src-type-vk": "VK", "src-type-evernote": "Evernote", "src-type-box": "Box", "src-type-onedrive": "OneDrive", "src-type-huddle": "Huddle", "src-type-other": "Other", "caption-from-url": "Import from link", "caption-camera": "Camera", "caption-draw": "Draw", "caption-edit-file": "Edit file", "file-no-name": "No name...", "toggle-fullscreen": "Toggle fullscreen", "toggle-guides": "Toggle guides", rotate: "Rotate", "flip-vertical": "Flip vertical", "flip-horizontal": "Flip horizontal", apply: "Apply", brightness: "Brightness", contrast: "Contrast", saturation: "Saturation", exposure: "Exposure", gamma: "Gamma", vibrance: "Vibrance", warmth: "Warmth", enhance: "Enhance", original: "Original", resize: "Resize image", crop: "Crop", "select-color": "Select color", text: "Text", draw: "Draw", "cancel-edit": "Cancel edit", "tab-view": "Preview", "tab-details": "Details", "file-name": "Name", "file-size": "Size", "cdn-url": "CDN URL", "file-size-unknown": "Unknown", "camera-permissions-denied": "Camera access denied", "camera-permissions-prompt": "Please allow access to the camera", "camera-permissions-request": "Request access", "files-count-limit-error-title": "Files count limit overflow", "files-count-limit-error-too-few": "You\u2019ve chosen {{total}} {{plural:file(total)}}. At least {{min}} {{plural:file(min)}} required.", "files-count-limit-error-too-many": "You\u2019ve chosen too many files. {{max}} {{plural:file(max)}} is maximum.", "files-max-size-limit-error": "File is too big. Max file size is {{maxFileSize}}.", "has-validation-errors": "File validation error occurred. Please, check your files before upload.", "images-only-accepted": "Only image files are accepted.", "file-type-not-allowed": "Uploading of these file types is not allowed.", "some-files-were-not-uploaded": "Some files were not uploaded.", "file-item-edit-button": "Edit", "file-item-remove-button": "Remove", "a11y-editor-tab-filters": "Filters", "a11y-editor-tab-tuning": "Tuning", "a11y-editor-tab-crop": "Crop", "a11y-activity-header-button-close": "Close", flip: "Flip", mirror: "Mirror", "a11y-cloud-editor-apply-filter": "Apply {{name}} filter", "a11y-cloud-editor-apply-crop": "Apply {{name}} operation", "a11y-cloud-editor-apply-tuning": "Apply {{name}} tuning", "a11y-cloud-editor-apply-aspect-ratio": "Apply operation {{name}} {{value}}", finished: "Finished", failed: "Failed", uploading: "Uploading", idle: "Idle", "a11y-file-item-status": "File {{fileName}} in status {{status}}", "waiting-for": "Waiting for {{source}}", "queued-uploading": "Queued for upload", "queued-validation": "Queued for validation", validation: "Validating", "crop-to-shape": "Crop to {{value}}", custom: "Freeform", "freeform-crop": "Freeform crop" }; // src/abstract/localeRegistry.ts var localeRegistry = /* @__PURE__ */ new Map(); var localeResolvers = /* @__PURE__ */ new Map(); var defineLocaleSync = (localeName, definition) => { if (localeRegistry.has(localeName)) { console.log(`Locale ${localeName} is already defined. Overwriting...`); } localeRegistry.set(localeName, { ...en_default, ...definition }); }; var defineLocaleAsync = (localeName, definitionResolver) => { localeResolvers.set(localeName, definitionResolver); }; var defineLocale = (localeName, definitionOrResolver) => { if (typeof definitionOrResolver === "function") { defineLocaleAsync(localeName, definitionOrResolver); } else { defineLocaleSync(localeName, definitionOrResolver); } }; var resolveLocaleDefinition = async (localeName) => { if (!localeRegistry.has(localeName)) { if (!localeResolvers.has(localeName)) { throw new Error(`Locale ${localeName} is not defined`); } const definitionResolver = localeResolvers.get(localeName); const definition = await definitionResolver(); defineLocaleSync(localeName, definition); } return localeRegistry.get(localeName); }; defineLocale("en", en_default); // src/abstract/managers/LocaleManager.ts var localeStateKey = (key) => `*l10n/${key}`; var DEFAULT_LOCALE = "en"; var LocaleManager = class { _blockInstance = null; _localeName = ""; _callbacks = /* @__PURE__ */ new Set(); _boundBlocks = /* @__PURE__ */ new Map(); constructor(blockInstance) { this._blockInstance = blockInstance; for (const [key, value] of Object.entries(en_default)) { const noTranslation = this._blockInstance.has(localeStateKey(key)) ? !this._blockInstance.$[localeStateKey(key)] : true; this._blockInstance.add(localeStateKey(key), value, noTranslation); } setTimeout(() => { blockInstance.subConfigValue("localeName", async (localeName) => { if (!this._blockInstance || !localeName) { return; } this._localeName = localeName; const definition = await resolveLocaleDefinition(localeName); if (localeName !== DEFAULT_LOCALE && this._localeName !== localeName) { return; } const overrides = this._blockInstance.cfg.localeDefinitionOverride?.[localeName]; for (const [key, value] of Object.entries(definition)) { const overriddenValue = overrides?.[key]; this._blockInstance.add(localeStateKey(key), overriddenValue ?? value, true); for (const callback of this._callbacks) { callback(); } } }); blockInstance.subConfigValue("localeDefinitionOverride", (localeDefinitionOverride) => { if (!localeDefinitionOverride) { return; } const definition = localeDefinitionOverride[this._localeName]; if (!definition) { return; } for (const [key, value] of Object.entries(definition)) { this._blockInstance?.add(localeStateKey(key), value, true); for (const callback of this._callbacks) { callback(); } } }); }); } onLocaleChange(callback) { const debouncedCb = debounce(callback, 0); this._callbacks.add(debouncedCb); return () => { this._callbacks.delete(debouncedCb); }; } bindL10n(block, key, resolver) { block.$[key] = resolver(); if (!this._boundBlocks.has(block)) { this._boundBlocks.set(block, /* @__PURE__ */ new Map()); } this._boundBlocks.get(block)?.get(key)?.(); const destroyCallback = this.onLocaleChange(() => { block.$[key] = resolver(); }); this._boundBlocks.get(block)?.set(key, destroyCallback); } destroyL10nBindings(block) { const callbacks = this._boundBlocks.get(block); if (!callbacks) { return; } for (const callback of callbacks.values()) { callback(); } this._boundBlocks.delete(block); } destroy() { this._callbacks.clear(); } }; // src/abstract/l10nProcessor.ts function l10nProcessor(fr, fnCtx) { [...fr.querySelectorAll("[l10n]")].forEach((el) => { const key = el.getAttribute("l10n"); if (!key) { return; } const list = key.split(";"); for (const item of list) { if (item) locale(el, item, fnCtx); } }); } var locale = (el, key, fnCtx) => { let elProp = "textContent"; let useAttribute = false; if (key.includes(":")) { const arr = key.split(":"); if (arr.length !== 2) { console.warn(`l10n attribute value should be in format "property:key" or "key". Found: ${key}`); return; } const tuple = arr; elProp = tuple[0]; key = tuple[1]; if (elProp.startsWith("@")) { elProp = elProp.slice(1); useAttribute = true; } } const localCtxKey = key; if (fnCtx.has(localCtxKey)) { fnCtx.sub(localCtxKey, (mappedKey) => { if (!mappedKey) { return; } if (!fnCtx.l10nProcessorSubs.has(localCtxKey)) { fnCtx.l10nProcessorSubs.set(localCtxKey, /* @__PURE__ */ new Set()); } const keySubs = fnCtx.l10nProcessorSubs.get(localCtxKey); keySubs?.forEach((sub2) => { sub2.remove(); keySubs.delete(sub2); fnCtx.allSubs.delete(sub2); }); const nodeStateKey = localeStateKey(mappedKey).replace("*", ""); if (!fnCtx.nodeCtx.has(nodeStateKey)) { fnCtx.nodeCtx.add(nodeStateKey, mappedKey); } const sub = fnCtx.nodeCtx.sub(nodeStateKey, () => { el[elProp] = fnCtx.l10n(mappedKey); }); keySubs?.add(sub); fnCtx.allSubs.add(sub); el.removeAttribute("l10n"); }); } const stateKey = localeStateKey(key); if (!fnCtx.has(stateKey)) { fnCtx.add(stateKey, ""); } fnCtx.sub(stateKey, () => { if (useAttribute) { el.setAttribute(elProp, fnCtx.l10n(key)); } else { el[elProp] = fnCtx.l10n(key); } }); el.removeAttribute("l10n"); }; // src/abstract/managers/a11y.ts import { focusGroupKeyUX, hiddenKeyUX, jumpKeyUX, pressKeyUX, startKeyUX } from "keyux"; var ScopedMinimalWindow = class { _listeners = /* @__PURE__ */ new Map(); _scope = []; addEventListener(type, listener) { const wrappedListener = (event) => { const target = event.target; if (!(target instanceof Node)) { return; } if (this._scope.some((el) => el === target || el.contains(target))) { listener(event); } }; this._listeners.set(listener, wrappedListener); window.addEventListener(type, wrappedListener); } removeEventListener(type, listener) { const wrappedListener = this._listeners.get(listener); if (wrappedListener) { window.removeEventListener(type, wrappedListener); } this._listeners.delete(listener); } get CustomEvent() { return window.CustomEvent; } get document() { return window.document; } get navigator() { return window.navigator; } registerScope(scope) { this._scope.push(scope); } destroy() { this._scope = []; for (const wrappedListener of this._listeners.values()) { window.removeEventListener("keydown", wrappedListener); window.removeEventListener("keyup", wrappedListener); } this._listeners.clear(); } }; var A11y = class { _destroyKeyUX; _scopedWindow; constructor() { this._scopedWindow = new ScopedMinimalWindow(); this._destroyKeyUX = startKeyUX(this._scopedWindow, [ focusGroupKeyUX(), pressKeyUX("is-pressed"), jumpKeyUX(), hiddenKeyUX() ]); } registerBlock(scope) { this._scopedWindow.registerScope(scope); } destroy() { this._destroyKeyUX?.(); this._scopedWindow.destroy(); } }; // src/abstract/managers/ModalManager.ts var ModalEvents = Object.freeze({ ADD: "modal:add", DELETE: "modal:delete", OPEN: "modal:open", CLOSE: "modal:close", CLOSE_ALL: "modal:closeAll", DESTROY: "modal:destroy" }); var ModalManager = class { _modals = /* @__PURE__ */ new Map(); _activeModals = /* @__PURE__ */ new Set(); _subscribers = /* @__PURE__ */ new Map(); _block; constructor(block) { this._block = block; } _debugPrint(...args) { this._block.debugPrint("[modal-manager]", ...args); } /** * Register a modal with the manager * @param id Unique identifier for the modal * @param modal Modal component instance */ registerModal(id, modal) { this._modals.set(id, modal); this._notify(ModalEvents.ADD, { id, modal }); } /** Remove a modal by ID. */ deleteModal(id) { if (!this._modals.has(id)) return false; const modal = this._modals.get(id); this._modals.delete(id); this._activeModals.delete(id); this._notify(ModalEvents.DELETE, { id, modal }); return true; } /** Open a modal by its ID. */ open(id) { if (!this._modals.has(id)) { this._debugPrint(`Modal with ID "${id}" not found`); return false; } const modal = this._modals.get(id); this._activeModals.add(id); this._notify(ModalEvents.OPEN, { modal, id }); return true; } /** Close a specific modal by ID. */ close(id) { if (!this._modals.has(id) || !this._activeModals.has(id)) { this._debugPrint(`Modal with ID "${id}" not found or not active`); return false; } const modal = this._modals.get(id); this._activeModals.delete(id); this._notify(ModalEvents.CLOSE, { id, modal }); return true; } /** Toggle a modal - open if closed, close if open. */ toggle(id) { if (!this._modals.has(id)) { this._debugPrint(`Modal with ID "${id}" not found`); return false; } if (this._activeModals.has(id)) { return this.close(id); } else { return this.open(id); } } /** True if there are any active modals. */ get hasActiveModals() { return this._activeModals.size > 0; } /** Close the most recently opened modal and return to the previous one. */ back() { if (this._activeModals.size === 0) { this._debugPrint("No active modals to go back from"); return false; } const lastModalId = Array.from(this._activeModals).pop(); return lastModalId ? this.close(lastModalId) : false; } /** Close all open modals. */ closeAll() { const count = this._activeModals.size; this._activeModals.clear(); this._notify(ModalEvents.CLOSE_ALL, {}); return count; } /** * Subscribe to modal events * @returns Unsubscribe function */ subscribe(event, callback) { if (!this._subscribers.has(event)) { this._subscribers.set(event, /* @__PURE__ */ new Set()); } this._subscribers.get(event)?.add(callback); return () => this.unsubscribe(event, callback); } /** Unsubscribe from modal events */ unsubscribe(event, callback) { if (this._subscribers.has(event)) { this._subscribers.get(event)?.delete(callback); } } /** Notify all subscribers of a modal event. */ _notify(event, data) { if (this._subscribers.has(event)) { for (const callback of this._subscribers.get(event) ?? /* @__PURE__ */ new Set()) { try { callback(data); } catch (error) { this._block.telemetryManager.sendEventError(error, "modal subscriber"); this._debugPrint("Error in modal subscriber:", error); } } } } /** Destroy the modal manager, clean up resources */ destroy() { this.closeAll(); this._modals.clear(); this._subscribers.clear(); this._notify(ModalEvents.DESTROY, {}); } }; // src/abstract/managers/TelemetryManager.ts import { TelemetryAPIService } from "@uploadcare/quality-insights"; import { Queue as Queue2 } from "@uploadcare/upload-client"; // package.json var version = "1.25.0"; // src/env.ts var PACKAGE_NAME = "blocks"; var PACKAGE_VERSION = version; // src/abstract/managers/TelemetryManager.ts var TelemetryManager = class { _sessionId = crypto.randomUUID(); _telemetryInstance; _block; _config = structuredClone(initialConfig); _initialized = false; _lastPayload = null; _queue; constructor(block) { this._block = block; this._telemetryInstance = new TelemetryAPIService(); this._queue = new Queue2(10); for (const key of Object.keys(this._config)) { this._block.subConfigValue(key, (value) => { if (this._initialized && this._config[key] !== value) { this.sendEvent({ eventType: InternalEventType.CHANGE_CONFIG }); } this._setConfig(key, value); }); } } _init(type) { if (type === InternalEventType.INIT_SOLUTION && !this._initialized) { this._initialized = true; } } _setConfig(key, value) { if (this._config[key] === value) { return; } this._config[key] = value; } _formattingPayload(body) { const payload = body.payload ? { ...body.payload } : {}; if (payload.activity) { payload.activity = void 0; } const result = { ...body }; if (body.eventType === InternalEventType.INIT_SOLUTION || body.eventType === InternalEventType.CHANGE_CONFIG) { result.config = this._config; } return { ...result, appVersion: PACKAGE_VERSION, appName: PACKAGE_NAME, sessionId: this._sessionId, component: this._solution, activity: this._activity, projectPubkey: this._config.pubkey, userAgent: navigator.userAgent, eventType: result.eventType ?? "", eventTimestamp: this._timestamp, payload: { location: this._location, ...payload } }; } _excludedEvents(type) { if (type && [ EventType.CHANGE, EventType.COMMON_UPLOAD_PROGRESS, EventType.FILE_ADDED, EventType.FILE_REMOVED, EventType.FILE_UPLOAD_START, EventType.FILE_UPLOAD_PROGRESS, EventType.FILE_UPLOAD_SUCCESS, EventType.FILE_UPLOAD_FAILED ].includes(type)) { return true; } return false; } sendEvent(body) { const payload = this._formattingPayload({ eventType: body.eventType, payload: body.payload, config: body.config }); this._init(body.eventType); const hasExcludedEvents = this._excludedEvents(body.eventType); if (hasExcludedEvents) { return; } const hasDataSame = this._lastPayload && this._checkObj(this._lastPayload, payload); if (hasDataSame) { return; } this._queue.add(async () => { this._lastPayload = payload; await this._telemetryInstance.sendEvent(payload); }); } sendEventError(error, context = "unknown") { this.sendEvent({ eventType: InternalEventType.ERROR_EVENT, payload: { metadata: { event: "error", text: `Error in ${context}`, error: error.message } } }); } /** * Method to send telemetry event for Cloud Image Editor. */ sendEventCloudImageEditor(e, tabId, options = {}) { this.sendEvent({ eventType: InternalEventType.ACTION_EVENT, payload: { metadata: { tabId, node: e.currentTarget?.tagName, event: e.type, ...options } } }); } /** * Deeply compares two objects and returns true if they are equal, false otherwise. */ _checkObj(last, current) { if (JSON.stringify(last) === JSON.stringify(current)) return true; if (typeof last !== "object" || typeof current !== "object" || last == null || current == null) return false; const lastKeys = Object.keys(last); const currentKeys = Object.keys(current); if (lastKeys.length !== currentKeys.length) return false; for (const key of lastKeys) { if (!Object.hasOwn(current, key)) return false; if (!this._checkObj(last[key], current[key])) return false; } return true; } get _timestamp() { return Date.now(); } get _solution() { if (!this._block.has("*solution")) { return null; } const solution = this._block.$["*solution"]; return solution ? solution.toLowerCase() : null; } get _activity() { if (!this._block.has("*currentActivity")) { return null; } return this._block.$["*currentActivity"] ?? null; } get _location() { return location.origin; } }; // src/abstract/sharedConfigKey.ts var sharedConfigKey = (key) => `*cfg/${key}`; // src/abstract/testModeProcessor.ts function testModeProcessor(fr, fnCtx) { const elementsWithTestId = fr.querySelectorAll("[data-testid]"); if (elementsWithTestId.length === 0) { return; } const valuesPerElement = /* @__PURE__ */ new WeakMap(); for (const el of elementsWithTestId) { const testIdValue = el.getAttribute("data-testid"); if (testIdValue) { valuesPerElement.set(el, testIdValue); } } fnCtx.subConfigValue("testMode", (testMode) => { if (!testMode) { for (const el of elementsWithTestId) { el.removeAttribute("data-testid"); } return; } const testIdPrefix = fnCtx.testId; for (const el of elementsWithTestId) { const testIdValue = valuesPerElement.get(el); if (!testIdValue) { continue; } el.setAttribute(`data-testid`, `${testIdPrefix}--${testIdValue}`); } }); } // src/abstract/Block.ts var TAG_PREFIX = "uc-"; var Block = class extends BaseComponent { __cfgProxy; l10nProcessorSubs = /* @__PURE__ */ new Map(); static StateConsumerScope = null; static styleAttrs = []; requireCtxName = false; activityType = null; init$ = blockCtx(); l10n(str, variables = {}) { if (!str) { return ""; } const template = this.$[localeStateKey(str)] || str; const pluralObjects = getPluralObjects(template); for (const pluralObject of pluralObjects) { variables[pluralObject.variable] = this.pluralize( pluralObject.pluralKey, Number(variables[pluralObject.countVariable]) ); } const result = applyTemplateData(template, variables); return result; } pluralize(key, count) { const locale2 = this.l10n("locale-id") || "en"; const pluralForm = getPluralForm(locale2, count); return this.l10n(`${key}__${pluralForm}`); } bindL10n(key, resolver) { this.localeManager?.bindL10n(this, key, resolver); } constructor() { super(); this.addTemplateProcessor( l10nProcessor ); this.addTemplateProcessor( testModeProcessor ); } emit(type, payload, options) { const eventEmitter = this.has("*eventEmitter") ? this.$["*eventEmitter"] : void 0; if (!eventEmitter) { return; } eventEmitter.emit(type, payload, options); this.telemetryManager.sendEvent({ eventType: type, payload: typeof payload === "function" ? payload() : payload }); } hasBlockInCtx(callback) { for (const block of this.blocksRegistry) { if (callback(block)) { return true; } } return false; } setOrAddState(prop, newVal) { this.add$( { [prop]: newVal }, true ); } connectedCallback() { const styleAttrs = this.constructor.styleAttrs; styleAttrs.forEach((attr) => { this.setAttribute(attr, ""); }); if (this.hasAttribute("retpl")) { this.constructor["template"] = null; this.processInnerHtml = true; } if (this.requireCtxName) { waitForAttribute({ element: this, attribute: "ctx-name", onSuccess: () => { super.connectedCallback(); }, onTimeout: () => { console.error("Attribute `ctx-name` is required and it is not set."); } }); } else { super.connectedCallback(); } WindowHeightTracker.registerClient(this); } disconnectedCallback() { super.disconnectedCallback(); WindowHeightTracker.unregisterClient(this); } initCallback() { if (!this.has("*blocksRegistry")) { this.add("*blocksRegistry", /* @__PURE__ */ new Set()); } const 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(this.constructor); return testId; } get modalManager() { return this.has("*modalManager") ? this.$["*modalManager"] : void 0; } get telemetryManager() { if (!this.cfg.qualityInsights) { return { sendEvent: () => { }, sendEventCloudImageEditor: () => { }, sendEventError: () => { } }; } return this.has("*telemetryManager") && this.$["*telemetryManager"]; } get localeManager() { return this.has("*localeManager") ? this.$["*localeManager"] : null; } get a11y() { return this.has("*a11y") ? this.$["*a11y"] : null; } get blocksRegistry() { return this.$["*blocksRegistry"]; } destroyCallback() { super.destroyCallback(); const blocksRegistry = this.blocksRegistry; blocksRegistry?.delete(this); this.localeManager?.destroyL10nBindings(this); this.l10nProcessorSubs = /* @__PURE__ */ new Map(); Data.deleteCtx(this); if (blocksRegistry?.size === 0) { setTimeout(() => { this.destroyCtxCallback(); }, 0); } } /** * Called when the last block is removed from the context. Note that inheritors must run their callback before that. */ destroyCtxCallback() { Data.deleteCtx(this.ctxName); this.localeManager?.destroy(); this.modalManager?.destroy(); } 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; } get cfg() { if (!this.__cfgProxy) { const o = /* @__PURE__ */ Object.create(null); this.__cfgProxy = new Proxy(o, { set: (obj, key, value) => { if (typeof key !== "string") { return false; } const sharedKey = sharedConfigKey(key); if (!this.has(sharedKey)) { this.add(sharedKey, initialConfig[key]); } this.$[sharedKey] = value; return true; }, get: (_obj, key) => { const sharedKey = sharedConfigKey(key); if (!this.has(sharedKey)) { this.add(sharedKey, initialConfig[key]); } return this.$[sharedConfigKey(key)]; } }); } return this.__cfgProxy; } subConfigValue(key, callback) { const sharedKey = sharedConfigKey(key); if (!this.has(sharedKey)) { this.add(sharedKey, initialConfig[key]); } this.sub(sharedKey, callback);