UNPKG

@mux/mux-uploader

Version:

An uploader elements to be used with Mux Direct Uploads

4 lines • 82.9 kB
{ "version": 3, "sources": ["../src/constants.ts", "../src/lang/en.ts", "../src/lang/es.ts", "../src/lang/fr.ts", "../src/lang/de.ts", "../src/lang/pt.ts", "../src/lang/it.ts", "../src/lang/zh.ts", "../src/utils/i18n.ts", "../src/polyfills/index.ts", "../src/mux-uploader.ts", "../src/utils/element-utils.ts", "../src/mux-uploader-drop.ts", "../src/utils/progress.ts", "../src/mux-uploader-progress.ts", "../src/mux-uploader-status.ts", "../src/mux-uploader-retry.ts", "../src/mux-uploader-pause.ts", "../src/mux-uploader-file-select.ts", "../src/layouts/block.ts", "../src/mux-uploader-sr-text.ts", "../src/index.ts"], "sourcesContent": ["export type ProgressTypes = {\n BAR: 'bar';\n RADIAL: 'radial';\n PERCENTAGE: 'percentage';\n};\n\nexport const ProgressTypes: ProgressTypes = {\n BAR: 'bar',\n RADIAL: 'radial',\n PERCENTAGE: 'percentage',\n};\n", "export const En = {\n 'Drop a video file here to upload': 'Drop a video file here to upload',\n or: 'or',\n 'Upload complete!': 'Upload complete!',\n Retry: 'Retry',\n 'Pausing...': 'Pausing...',\n Resume: 'Resume',\n Pause: 'Pause',\n 'Upload a video': 'Upload a video',\n 'No url or endpoint specified - cannot handle upload': 'No url or endpoint specified - cannot handle upload',\n} as const;\n\nexport type TranslateKeys = keyof typeof En;\n\nexport type TranslateDictionary = {\n [key in TranslateKeys]: string;\n};\n", "import type { TranslateDictionary } from './en.js';\n\nexport const Es: TranslateDictionary = {\n 'Drop a video file here to upload': 'Arrastra un archivo de video aqu\u00ED para subir',\n or: 'o',\n 'Upload complete!': '\u00A1Subida completada!',\n Retry: 'Reintentar',\n 'Pausing...': 'Pausando...',\n Resume: 'Reanudar',\n Pause: 'Pausar',\n 'Upload a video': 'Subir un video',\n 'No url or endpoint specified - cannot handle upload':\n 'No se especific\u00F3 URL o endpoint - no se puede manejar la subida',\n};\n", "import type { TranslateDictionary } from './en.js';\n\nexport const Fr: TranslateDictionary = {\n 'Drop a video file here to upload': 'D\u00E9posez un fichier vid\u00E9o ici pour le t\u00E9l\u00E9charger',\n or: 'ou',\n 'Upload complete!': 'T\u00E9l\u00E9chargement termin\u00E9!',\n Retry: 'R\u00E9essayer',\n 'Pausing...': 'En pause...',\n Resume: 'Reprendre',\n Pause: 'Pause',\n 'Upload a video': 'T\u00E9l\u00E9charger une vid\u00E9o',\n 'No url or endpoint specified - cannot handle upload':\n 'Aucune URL ou point de terminaison sp\u00E9cifi\u00E9 - impossible de g\u00E9rer le t\u00E9l\u00E9chargement',\n};\n", "import type { TranslateDictionary } from './en.js';\n\nexport const De: TranslateDictionary = {\n 'Drop a video file here to upload': 'Legen Sie hier eine Videodatei zum Hochladen ab',\n or: 'oder',\n 'Upload complete!': 'Upload abgeschlossen!',\n Retry: 'Wiederholen',\n 'Pausing...': 'Pausiere...',\n Resume: 'Fortsetzen',\n Pause: 'Pausieren',\n 'Upload a video': 'Video hochladen',\n 'No url or endpoint specified - cannot handle upload':\n 'Keine URL oder Endpunkt angegeben - Upload kann nicht verarbeitet werden',\n};\n", "import type { TranslateDictionary } from './en.js';\n\nexport const Pt: TranslateDictionary = {\n 'Drop a video file here to upload': 'Arraste um arquivo de v\u00EDdeo aqui para fazer o upload',\n or: 'ou',\n 'Upload complete!': 'Upload completo!',\n Retry: 'Tentar novamente',\n 'Pausing...': 'Pausando...',\n Resume: 'Retomar',\n Pause: 'Pausar',\n 'Upload a video': 'Fazer upload de um v\u00EDdeo',\n 'No url or endpoint specified - cannot handle upload':\n 'Nenhum URL ou endpoint especificado - n\u00E3o \u00E9 poss\u00EDvel processar o upload',\n};\n", "import type { TranslateDictionary } from './en.js';\n\nexport const It: TranslateDictionary = {\n 'Drop a video file here to upload': 'Trascina qui un file video per caricarlo',\n or: 'o',\n 'Upload complete!': 'Caricamento completato!',\n Retry: 'Riprova',\n 'Pausing...': 'In pausa...',\n Resume: 'Riprendi',\n Pause: 'Pausa',\n 'Upload a video': 'Carica un video',\n 'No url or endpoint specified - cannot handle upload':\n 'Nessun URL o endpoint specificato - impossibile gestire il caricamento',\n};\n", "import type { TranslateDictionary } from './en.js';\n\nexport const Zh: TranslateDictionary = {\n 'Drop a video file here to upload': '\u5C06\u89C6\u9891\u6587\u4EF6\u62D6\u653E\u5230\u6B64\u5904\u4EE5\u4E0A\u4F20',\n or: '\u6216',\n 'Upload complete!': '\u4E0A\u4F20\u5B8C\u6210\uFF01',\n Retry: '\u91CD\u8BD5',\n 'Pausing...': '\u6682\u505C...',\n Resume: '\u7EE7\u7EED',\n Pause: '\u6682\u505C',\n 'Upload a video': '\u4E0A\u4F20\u89C6\u9891',\n 'No url or endpoint specified - cannot handle upload': '\u672A\u6307\u5B9A URL \u6216 endpoint - \u65E0\u6CD5\u5904\u7406\u4E0A\u4F20',\n};\n", "import type { TranslateDictionary, TranslateKeys } from '../lang/en.js';\nimport { En } from '../lang/en.js';\nimport { Es } from '../lang/es.js';\nimport { Fr } from '../lang/fr.js';\nimport { De } from '../lang/de.js';\nimport { Pt } from '../lang/pt.js';\nimport { It } from '../lang/it.js';\nimport { Zh } from '../lang/zh.js';\n\nconst translationsLanguages: Record<string, TranslateDictionary> = {\n en: En,\n es: Es,\n fr: Fr,\n de: De,\n pt: Pt,\n it: It,\n zh: Zh,\n};\n\nexport const addTranslation = (langCode: string, languageDictionary: TranslateDictionary) => {\n translationsLanguages[langCode] = languageDictionary;\n if (!supportedLocales.includes(langCode)) {\n supportedLocales.push(langCode);\n }\n};\n\nconst getBrowserLanguage = (): string => {\n if (typeof globalThis.navigator === 'undefined' || !globalThis.navigator.language) {\n return 'en';\n }\n return globalThis.navigator.language.split('-')[0];\n};\n\nconst supportedLocales = ['en', 'es', 'fr', 'de', 'pt', 'it', 'zh'];\n\nconst getEffectiveLocale = (locale?: string | null): string => {\n // Use provided locale if supported\n if (locale && supportedLocales.includes(locale)) {\n return locale;\n }\n // Otherwise, use browser language if supported\n const browserLang = getBrowserLanguage();\n if (supportedLocales.includes(browserLang)) {\n return browserLang;\n }\n // Fallback to English\n return 'en';\n};\n\nexport const t = (key: TranslateKeys, locale?: string | null) => {\n const effectiveLocale = getEffectiveLocale(locale);\n const dictionary = translationsLanguages[effectiveLocale] || En;\n return dictionary[key] || En[key];\n};\n", "/* eslint @typescript-eslint/no-empty-function: \"off\" */\n\nclass EventTarget {\n addEventListener() {}\n removeEventListener() {}\n dispatchEvent(_event: Event) {\n return true;\n }\n}\n\n// @github/template-parts requires DocumentFragment to be available on globalThis for SSR\nif (typeof DocumentFragment === 'undefined') {\n class DocumentFragment extends EventTarget {}\n // @ts-ignore\n globalThis.DocumentFragment = DocumentFragment;\n}\n\nclass HTMLElement extends EventTarget {}\nclass HTMLVideoElement extends EventTarget {}\n\nconst customElements: CustomElementRegistry = {\n get(_name: string) {\n return undefined;\n },\n define(_name, _constructor, _options) {},\n getName(_constructor) {\n return null;\n },\n upgrade(_root) {},\n whenDefined(_name) {\n return Promise.resolve(HTMLElement as unknown as CustomElementConstructor);\n },\n};\n\nclass CustomEvent {\n #detail;\n get detail() {\n return this.#detail;\n }\n constructor(_typeArg: string, eventInitDict: CustomEventInit = {}) {\n // super(typeArg, eventInitDict);\n this.#detail = eventInitDict?.detail;\n }\n initCustomEvent() {}\n}\n\nfunction createElement(_tagName: string, _options?: ElementCreationOptions): HTMLElement {\n return new HTMLElement();\n}\n\nconst globalThisShim = {\n document: {\n createElement,\n },\n DocumentFragment,\n customElements,\n CustomEvent,\n EventTarget,\n HTMLElement,\n HTMLVideoElement,\n};\n\n// const isServer = typeof window === 'undefined' || typeof globalThis.customElements === 'undefined';\n// const GlobalThis = isServer ? globalThisShim : globalThis;\n// const Document = isServer ? globalThisShim.document : globalThis.document;\n//\n// export { GlobalThis as globalThis, Document as document };\nconst isServer = typeof window === 'undefined' || typeof globalThis.customElements === 'undefined';\ntype GlobalThis = typeof globalThis;\nconst internalGlobalThis: GlobalThis = (isServer ? globalThisShim : globalThis) as GlobalThis;\nconst internalDocument: Document = (isServer ? globalThisShim.document : globalThis.document) as Document;\n\nexport { internalGlobalThis as globalThis, internalDocument as document };\n", "import { globalThis, document } from './polyfills';\n\nimport { UpChunk } from '@mux/upchunk';\n\nimport blockLayout from './layouts/block';\nimport { ProgressTypes } from './constants';\nimport { t } from './utils/i18n.js';\n\nconst rootTemplate = document.createElement('template');\n\nrootTemplate.innerHTML = /*html*/ `\n<style>\n :host {\n display: flex;\n flex-direction: column;\n }\n\n mux-uploader-drop {\n flex-grow: 1;\n }\n\n input[type=\"file\"] {\n display: none;\n }\n</style>\n\n<input id=\"hidden-file-input\" type=\"file\" accept=\"video/*, audio/*\" />\n<mux-uploader-sr-text></mux-uploader-sr-text>\n`;\n\ntype Endpoint = UpChunk['endpoint'] | undefined | null;\ntype DynamicChunkSize = UpChunk['dynamicChunkSize'] | undefined;\n\ntype ErrorDetail = {\n message: string;\n chunkNumber?: number;\n attempts?: number;\n};\n\n// NOTE: Progress event is already determined on HTMLElement but have inconsistent types. Should consider renaming events (CJP)\nexport interface MuxUploaderElementEventMap extends Omit<HTMLElementEventMap, 'progress'> {\n uploadstart: CustomEvent<{ file: File; chunkSize: number }>;\n chunkattempt: CustomEvent<{\n chunkNumber: number;\n chunkSize: number;\n }>;\n chunksuccess: CustomEvent<{\n chunk: number;\n chunkSize: number;\n attempts: number;\n timeInterval: number;\n // Note: This should be more explicitly typed in Upchunk. (TD).\n response: any;\n }>;\n uploaderror: CustomEvent<ErrorDetail>;\n progress: CustomEvent<number>;\n success: CustomEvent<undefined | null>;\n 'file-ready': CustomEvent<File>;\n}\n\ninterface MuxUploaderElement extends HTMLElement {\n addEventListener<K extends keyof MuxUploaderElementEventMap>(\n type: K,\n listener: (this: HTMLMediaElement, ev: MuxUploaderElementEventMap[K]) => any,\n options?: boolean | AddEventListenerOptions\n ): void;\n addEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject,\n options?: boolean | AddEventListenerOptions\n ): void;\n removeEventListener<K extends keyof MuxUploaderElementEventMap>(\n type: K,\n listener: (this: HTMLMediaElement, ev: MuxUploaderElementEventMap[K]) => any,\n options?: boolean | EventListenerOptions\n ): void;\n removeEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject,\n options?: boolean | EventListenerOptions\n ): void;\n}\n\nclass MuxUploaderElement extends globalThis.HTMLElement implements MuxUploaderElement {\n static get observedAttributes() {\n return [\n 'pausable',\n 'type',\n 'no-drop',\n 'no-progress',\n 'no-status',\n 'no-retry',\n 'max-file-size',\n 'use-large-file-workaround',\n 'locale',\n ];\n }\n\n protected _endpoint: Endpoint;\n protected _upload?: UpChunk;\n\n constructor() {\n super();\n\n const shadow = this.attachShadow({ mode: 'open' });\n\n // Always attach the root template\n shadow.appendChild(rootTemplate.content.cloneNode(true));\n\n // Attach a layout\n this.updateLayout();\n\n this.hiddenFileInput?.addEventListener('change', () => {\n const file = this.hiddenFileInput?.files?.[0];\n this.toggleAttribute('file-ready', !!file);\n\n if (file) {\n this.dispatchEvent(\n new CustomEvent('file-ready', {\n composed: true,\n bubbles: true,\n detail: file,\n })\n );\n }\n });\n }\n\n connectedCallback() {\n this.addEventListener('file-ready', this.handleUpload);\n this.addEventListener('reset', this.resetState);\n }\n\n disconnectedCallback() {\n this.removeEventListener('file-ready', this.handleUpload, false);\n this.removeEventListener('reset', this.resetState);\n }\n\n attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {\n if (name === 'locale' && oldValue !== newValue) {\n // Dispatch event to notify subcomponents to update their texts\n this.dispatchEvent(new CustomEvent('localechange', { detail: { locale: newValue } }));\n } else if (name !== 'locale') {\n this.updateLayout();\n }\n }\n\n protected get hiddenFileInput() {\n return this.shadowRoot?.querySelector('#hidden-file-input') as HTMLInputElement;\n }\n\n get endpoint(): Endpoint {\n return this.getAttribute('endpoint') ?? this._endpoint;\n }\n\n set endpoint(value: Endpoint) {\n if (value === this.endpoint) return;\n if (typeof value === 'string') {\n this.setAttribute('endpoint', value);\n } else if (value == undefined) {\n this.removeAttribute('endpoint');\n }\n this._endpoint = value;\n }\n\n get type() {\n return (this.getAttribute('type') ?? undefined) as ProgressTypes[keyof ProgressTypes] | undefined;\n }\n\n set type(val: ProgressTypes[keyof ProgressTypes] | undefined) {\n if (val == this.type) return;\n if (!val) {\n this.removeAttribute('type');\n } else {\n this.setAttribute('type', val);\n }\n }\n\n get noDrop(): boolean {\n return this.hasAttribute('no-drop');\n }\n\n set noDrop(value: boolean) {\n this.toggleAttribute('no-drop', Boolean(value));\n }\n\n get noProgress(): boolean {\n return this.hasAttribute('no-progress');\n }\n\n set noProgress(value: boolean) {\n this.toggleAttribute('no-progress', Boolean(value));\n }\n\n get noStatus(): boolean {\n return this.hasAttribute('no-status');\n }\n\n set noStatus(value: boolean) {\n this.toggleAttribute('no-status', Boolean(value));\n }\n\n get noRetry(): boolean {\n return this.hasAttribute('no-retry');\n }\n\n set noRetry(value: boolean) {\n this.toggleAttribute('no-retry', Boolean(value));\n }\n\n get pausable() {\n return this.hasAttribute('pausable');\n }\n\n set pausable(value) {\n this.toggleAttribute('pausable', Boolean(value));\n }\n\n get dynamicChunkSize(): DynamicChunkSize {\n return this.hasAttribute('dynamic-chunk-size');\n }\n\n set dynamicChunkSize(value: DynamicChunkSize) {\n if (value === this.hasAttribute('dynamic-chunk-size')) return;\n if (value) {\n this.setAttribute('dynamic-chunk-size', '');\n } else {\n this.removeAttribute('dynamic-chunk-size');\n }\n }\n\n get useLargeFileWorkaround() {\n return this.hasAttribute('use-large-file-workaround');\n }\n\n set useLargeFileWorkaround(value: boolean | undefined) {\n if (value == this.useLargeFileWorkaround) return;\n this.toggleAttribute('use-large-file-workaround', !!value);\n }\n\n get maxFileSize(): number | undefined {\n const maxFileSize = this.getAttribute('max-file-size');\n return maxFileSize !== null ? parseInt(maxFileSize) : undefined;\n }\n\n set maxFileSize(value: number | undefined) {\n if (value) {\n this.setAttribute('max-file-size', value.toString());\n } else {\n this.removeAttribute('max-file-size');\n }\n }\n\n get chunkSize(): number | undefined {\n const chunkSize = this.getAttribute('chunk-size');\n return chunkSize !== null ? parseInt(chunkSize) : undefined;\n }\n\n set chunkSize(value: number | undefined) {\n if (value) {\n this.setAttribute('chunk-size', value.toString());\n } else {\n this.removeAttribute('chunk-size');\n }\n }\n\n get locale(): string | null {\n return this.getAttribute('locale');\n }\n\n set locale(value: string | undefined) {\n if (value === this.locale) return;\n if (!value) {\n this.removeAttribute('locale');\n } else {\n this.setAttribute('locale', value);\n }\n }\n\n get upload() {\n return this._upload;\n }\n\n get paused() {\n return this.upload?.paused ?? false;\n }\n\n set paused(value) {\n if (!this.upload) {\n console.warn('Pausing before an upload has begun is unsupported');\n return;\n }\n const boolVal = !!value;\n if (boolVal === this.paused) return;\n if (boolVal) {\n this.upload.pause();\n } else {\n this.upload.resume();\n }\n this.toggleAttribute('paused', boolVal);\n this.dispatchEvent(new CustomEvent('pausedchange', { detail: boolVal }));\n }\n\n updateLayout() {\n const oldLayout = this.shadowRoot?.querySelector('mux-uploader-drop, div');\n if (oldLayout) {\n oldLayout.remove();\n }\n const newLayout = blockLayout(this);\n this.shadowRoot?.appendChild(newLayout);\n }\n\n setError(message: string) {\n this.setAttribute('upload-error', '');\n this.dispatchEvent(new CustomEvent('uploaderror', { detail: { message } }));\n }\n\n resetState() {\n this.removeAttribute('upload-error');\n this.removeAttribute('upload-in-progress');\n this.removeAttribute('upload-complete');\n // Reset file to ensure change/input events will fire, even if selecting the same file (CJP).\n this.hiddenFileInput.value = '';\n }\n\n handleUpload(evt: CustomEvent) {\n const endpoint = this.endpoint;\n const dynamicChunkSize = this.dynamicChunkSize;\n\n if (!endpoint) {\n this.setError(t('No url or endpoint specified - cannot handle upload', this.locale));\n // Bail early if no endpoint.\n return;\n } else {\n this.removeAttribute('upload-error');\n }\n\n try {\n const upload = UpChunk.createUpload({\n endpoint,\n dynamicChunkSize,\n file: evt.detail,\n maxFileSize: this.maxFileSize,\n chunkSize: this.chunkSize,\n useLargeFileWorkaround: this.useLargeFileWorkaround,\n });\n\n this._upload = upload;\n\n this.dispatchEvent(\n new CustomEvent('uploadstart', { detail: { file: upload.file, chunkSize: upload.chunkSize } })\n );\n this.setAttribute('upload-in-progress', '');\n\n if (upload.offline) {\n this.dispatchEvent(new CustomEvent('offline'));\n }\n\n upload.on('attempt', (event: any) => {\n this.dispatchEvent(new CustomEvent('chunkattempt', event));\n });\n\n upload.on('chunkSuccess', (event: any) => {\n this.dispatchEvent(new CustomEvent('chunksuccess', event));\n });\n\n upload.on('error', (event: any) => {\n this.setAttribute('upload-error', '');\n console.error('error handler', event.detail.message);\n this.dispatchEvent(new CustomEvent('uploaderror', event));\n });\n\n upload.on('progress', (event: any) => {\n this.dispatchEvent(new CustomEvent('progress', event));\n });\n\n upload.on('success', (event: any) => {\n this.removeAttribute('upload-in-progress');\n this.setAttribute('upload-complete', '');\n\n this.dispatchEvent(new CustomEvent('success', event));\n });\n\n upload.on('offline', (event: any) => {\n this.dispatchEvent(new CustomEvent('offline', event));\n });\n upload.on('online', (event: any) => {\n this.dispatchEvent(new CustomEvent('online', event));\n });\n } catch (err) {\n if (err instanceof Error) {\n this.setError(err.message);\n }\n }\n }\n}\n\ntype MuxUploaderElementType = typeof MuxUploaderElement;\ndeclare global {\n // eslint-disable-next-line\n var MuxUploaderElement: MuxUploaderElementType;\n}\n\nif (!globalThis.customElements.get('mux-uploader')) {\n globalThis.customElements.define('mux-uploader', MuxUploaderElement);\n globalThis.MuxUploaderElement = MuxUploaderElement;\n}\n\nexport default MuxUploaderElement;\n", "import type MuxUploaderElement from '../mux-uploader';\n\nexport const closestComposedNode = (childNode: Element, selector: string): HTMLElement | null => {\n if (!childNode) return null;\n const closest = childNode.closest<HTMLElement>(selector);\n if (closest) return closest;\n return closestComposedNode((childNode.getRootNode() as ShadowRoot).host, selector);\n};\n\nexport const getMuxUploaderEl = (controlEl: Element): MuxUploaderElement | null => {\n const muxUploaderId = controlEl.getAttribute('mux-uploader');\n if (muxUploaderId) {\n return document.getElementById(muxUploaderId) as MuxUploaderElement;\n }\n return closestComposedNode(controlEl, 'mux-uploader') as MuxUploaderElement;\n};\n", "import { globalThis, document } from './polyfills';\nimport { getMuxUploaderEl } from './utils/element-utils';\nimport type MuxUploaderElement from './mux-uploader';\nimport { t } from './utils/i18n.js';\n\nconst template = document.createElement('template');\n\ntemplate.innerHTML = /*html*/ `\n<style>\n :host {\n position: relative;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n border: 2px dashed #ccc;\n padding: 2.5rem 2rem;\n border-radius: .25rem;\n }\n\n slot[name='heading'] > * {\n margin-bottom: 0.75rem;\n font-size: 1.75rem;\n text-align: center;\n }\n\n slot[name='separator'] > * {\n margin-bottom: 0.75rem;\n }\n\n #overlay {\n display: none;\n position: absolute;\n top: 0;\n bottom: 0;\n right: 0;\n left: 0;\n height: 100%;\n width: 100%;\n }\n\n :host([active][overlay]) > #overlay {\n background: var(--overlay-background-color, rgba(226, 253, 255, 0.95));\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n }\n\n :host([file-ready])::part(heading),\n :host([file-ready])::part(separator) {\n display: none;\n }\n</style>\n\n<slot name=\"heading\" part=\"heading\">\n <span id=\"drop-text\">Drop a video file here to upload</span>\n</slot>\n<slot name=\"separator\" part=\"separator\">\n <span id=\"separator-text\">or</span>\n</slot>\n<slot></slot>\n\n<div id=\"overlay\">\n <h1 id=\"overlay-label\"></h1>\n</div>\n`;\n\nconst Attributes = {\n MUX_UPLOADER: 'mux-uploader',\n OVERLAY_TEXT: 'overlay-text',\n};\n\nclass MuxUploaderDropElement extends globalThis.HTMLElement {\n #overlayTextEl: HTMLElement;\n #uploaderEl: MuxUploaderElement | null | undefined;\n\n #abortController: AbortController | undefined;\n\n constructor() {\n super();\n const shadowRoot = this.attachShadow({ mode: 'open' });\n shadowRoot.appendChild(template.content.cloneNode(true));\n\n this.#overlayTextEl = shadowRoot.getElementById('overlay-label') as HTMLElement;\n }\n\n connectedCallback() {\n this.#uploaderEl = getMuxUploaderEl(this);\n this.#abortController = new AbortController();\n\n if (this.#uploaderEl) {\n const opts = { signal: this.#abortController.signal };\n\n this.#uploaderEl.addEventListener('file-ready', () => this.toggleAttribute('file-ready', true), opts);\n this.#uploaderEl.addEventListener('uploadstart', () => this.toggleAttribute('upload-in-progress', true), opts);\n this.#uploaderEl.addEventListener(\n 'success',\n () => {\n this.toggleAttribute('upload-in-progress', false);\n this.toggleAttribute('upload-complete', true);\n },\n opts\n );\n this.#uploaderEl.addEventListener(\n 'reset',\n () => {\n this.toggleAttribute('file-ready', false);\n this.toggleAttribute('upload-in-progress', false);\n this.toggleAttribute('upload-complete', false);\n },\n opts\n );\n\n this.setupDragEvents(opts);\n\n this.toggleAttribute('upload-in-progress', this.#uploaderEl.hasAttribute('upload-in-progress'));\n this.toggleAttribute('upload-complete', this.#uploaderEl.hasAttribute('upload-complete'));\n this.toggleAttribute('file-ready', this.#uploaderEl.hasAttribute('file-ready'));\n\n this.#uploaderEl.addEventListener('localechange', () => this.updateText(), opts);\n\n this.updateText();\n }\n }\n\n disconnectedCallback() {\n this.#abortController?.abort();\n }\n\n attributeChangedCallback(attributeName: string, oldValue: string | null, newValue: string | null) {\n if (attributeName === Attributes.OVERLAY_TEXT && oldValue !== newValue) {\n this.#overlayTextEl.innerHTML = newValue ?? '';\n } else if (attributeName === 'active') {\n if (this.hasAttribute('overlay') && newValue != null) {\n this._currentDragTarget = this;\n }\n }\n }\n\n static get observedAttributes() {\n return [Attributes.OVERLAY_TEXT, Attributes.MUX_UPLOADER, 'active'];\n }\n\n protected _currentDragTarget?: Node;\n\n setupDragEvents(opts: AddEventListenerOptions) {\n this.addEventListener(\n 'dragenter',\n (evt) => {\n this._currentDragTarget = evt.target as Node;\n evt.preventDefault();\n evt.stopPropagation();\n this.toggleAttribute('active', true);\n },\n opts\n );\n\n this.addEventListener(\n 'dragleave',\n (evt) => {\n if (this._currentDragTarget === evt.target) {\n this._currentDragTarget = undefined;\n this.toggleAttribute('active', false);\n }\n },\n opts\n );\n\n this.addEventListener(\n 'dragover',\n (evt) => {\n evt.preventDefault();\n evt.stopPropagation();\n },\n opts\n );\n\n this.addEventListener(\n 'drop',\n (evt) => {\n evt.preventDefault();\n evt.stopPropagation();\n const { dataTransfer } = evt;\n //@ts-ignore\n const { files } = dataTransfer;\n const file = files[0];\n\n const uploaderController = this.#uploaderEl ?? this;\n\n uploaderController.dispatchEvent(\n new CustomEvent('file-ready', {\n composed: true,\n bubbles: true,\n detail: file,\n })\n );\n\n this.removeAttribute('active');\n },\n opts\n );\n }\n\n updateText() {\n const locale = (this.#uploaderEl as MuxUploaderElement)?.locale;\n\n const dropTextEl = this.shadowRoot?.getElementById('drop-text');\n const separatorTextEl = this.shadowRoot?.getElementById('separator-text');\n\n if (dropTextEl) {\n dropTextEl.textContent = t('Drop a video file here to upload', locale);\n }\n if (separatorTextEl) {\n separatorTextEl.textContent = t('or', locale);\n }\n }\n}\n\nif (!globalThis.customElements.get('mux-uploader-drop')) {\n globalThis.customElements.define('mux-uploader-drop', MuxUploaderDropElement);\n //@ts-ignore\n globalThis.MuxUploaderDropElement = MuxUploaderDropElement;\n}\n\nexport default MuxUploaderDropElement;\n", "export function formatProgress(percent: number): string {\n return `${Math.floor(percent)}%`;\n}\n", "import { globalThis, document } from './polyfills';\nimport { ProgressTypes } from './constants';\nimport { getMuxUploaderEl } from './utils/element-utils';\nimport { formatProgress } from './utils/progress';\n\nconst template = document.createElement('template');\nconst ariaDescription = 'Media upload progress bar';\n\ntemplate.innerHTML = /*html*/ `\n<style>\n :host {\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-direction: column;\n }\n\n .bar-type {\n background: var(--progress-bar-background-color, #e6e6e6);\n border-radius: var(--progress-bar-border-radius, 100px);\n height: var(--progress-bar-height, 4px);\n width: 100%;\n }\n\n .radial-type,\n .bar-type,\n #percentage-type,\n :host([type=\"bar\"][upload-error]) #percentage-type {\n display: none;\n }\n\n :host([type=\"radial\"][upload-in-progress]) .radial-type,\n :host([type=\"bar\"][upload-in-progress]) .bar-type {\n display: block;\n }\n\n :host([type=\"percentage\"][upload-in-progress]) #percentage-type {\n display: var(--progress-percentage-display, block);\n }\n\n :host([type=\"bar\"][upload-error]) .progress-bar {\n background: #e22c3e;\n }\n\n .progress-bar {\n box-shadow: var(--progress-bar-box-shadow, 0 10px 40px -10px #fff);\n border-radius: var(--progress-bar-border-radius, 100px);\n background: var(--progress-bar-fill-color, #000000);\n height: var(--progress-bar-height, 4px);\n width: 0%;\n transition: width 0.25s;\n }\n\n circle {\n stroke: var(--progress-radial-fill-color, black);\n stroke-width: 6; /* Thickness of the circle */\n fill: transparent; /* Make inside of the circle see-through */\n\n /* Animation */\n transition: 0.35s;\n transform: rotate(-90deg);\n transform-origin: 50% 50%;\n -webkit-transform-origin: 50% 50%;\n -moz-transform-origin: 50% 50%;\n }\n\n #percentage-type {\n font-size: inherit;\n margin: 0 0 1em;\n }\n</style>\n\n<slot></slot>\n\n<p id=\"percentage-type\"></p>\n<div class=\"bar-type\">\n <div role=\"progressbar\" aria-valuemin=\"0\" aria-valuemax=\"100\" class=\"progress-bar\" id=\"progress-bar\" tabindex=\"0\"></div>\n</div>\n<div class=\"radial-type\">\n <svg\n width=\"120\"\n height=\"120\">\n <!-- To prevent overflow of the SVG wrapper, radius must be (svgWidth / 2) - (circleStrokeWidth * 2)\n or use overflow: visible on the svg.-->\n <circle\n r=\"52\"\n cx=\"60\"\n cy=\"60\"\n />\n <svg>\n</div>\n`;\n\nclass MuxUploaderProgressElement extends globalThis.HTMLElement {\n #uploaderEl: HTMLElement | null | undefined;\n #abortController: AbortController | undefined;\n\n svgCircle: SVGCircleElement | null | undefined;\n progressBar: HTMLElement | null | undefined;\n uploadPercentage: HTMLElement | null | undefined;\n\n constructor() {\n super();\n const shadowRoot = this.attachShadow({ mode: 'open' });\n shadowRoot.appendChild(template.content.cloneNode(true));\n\n this.svgCircle = this.shadowRoot?.querySelector('circle');\n this.progressBar = this.shadowRoot?.getElementById('progress-bar');\n this.uploadPercentage = this.shadowRoot?.getElementById('percentage-type');\n this.progressBar?.setAttribute('aria-description', ariaDescription);\n }\n\n connectedCallback() {\n this.setDefaultType();\n\n this.#uploaderEl = getMuxUploaderEl(this);\n this.#abortController = new AbortController();\n\n if (this.#uploaderEl) {\n const opts = { signal: this.#abortController.signal };\n\n this.#uploaderEl.addEventListener('uploadstart', this.onUploadStart, opts);\n this.#uploaderEl.addEventListener('reset', this.onReset);\n this.#uploaderEl.addEventListener('progress', this.onProgress);\n this.#uploaderEl.addEventListener('success', this.onSuccess);\n this.toggleAttribute('upload-in-progress', this.#uploaderEl.hasAttribute('upload-in-progress'));\n this.toggleAttribute('upload-complete', this.#uploaderEl.hasAttribute('upload-complete'));\n }\n }\n\n disconnectedCallback() {\n this.#abortController?.abort();\n }\n\n onUploadStart = () => {\n this.progressBar?.focus();\n this.toggleAttribute('upload-in-progress', true);\n };\n\n onProgress = (e: Event) => {\n // @ts-ignore\n const percent = e.detail;\n this.progressBar?.setAttribute('aria-valuenow', `${Math.floor(percent)}`);\n\n switch (this.getAttribute('type')) {\n case ProgressTypes.BAR: {\n if (this.progressBar) {\n this.progressBar.style.width = `${percent}%`;\n }\n break;\n }\n case ProgressTypes.RADIAL: {\n if (this.svgCircle) {\n // The closer the upload percentage gets to 100%, the closer offset gets to 0.\n // The closer offset gets to 0, the more we can see the circumference of our circle. (TD).\n const offset = this.getCircumference() - (percent / 100) * this.getCircumference();\n\n this.svgCircle.style.strokeDashoffset = offset.toString();\n }\n break;\n }\n case ProgressTypes.PERCENTAGE: {\n if (this.uploadPercentage) this.uploadPercentage.innerHTML = formatProgress(percent);\n break;\n }\n }\n };\n\n onSuccess = () => {\n this.toggleAttribute('upload-in-progress', false);\n this.toggleAttribute('upload-complete', true);\n };\n\n onReset = () => {\n this.toggleAttribute('upload-in-progress', false);\n if (this.uploadPercentage) {\n this.uploadPercentage.innerHTML = '';\n }\n\n if (this.svgCircle) {\n this.svgCircle.style.strokeDashoffset = `${this.getCircumference()}`;\n }\n };\n\n getRadius() {\n return Number(this.svgCircle?.getAttribute('r'));\n }\n\n getCircumference() {\n return this.getRadius() * 2 * Math.PI;\n }\n\n setDefaultType() {\n const currentType = this.getAttribute('type');\n\n if (!currentType) {\n this.setAttribute('type', ProgressTypes.BAR);\n }\n\n if (currentType === ProgressTypes.RADIAL && this.svgCircle) {\n // strokeDasharray is the size of dashes used to draw the circle with the size of gaps in between.\n // If the dash number is the same as the gap number, no gap is visible: a full circle.\n // strokeDashoffset defines where along our circle the dashes (in our case, a dash as long as the\n // circumference of our circle) begins. The larger the offset, the farther into the circle you're\n // starting the \"dash\". In the beginning, offset is the same as the circumference. Meaning, the visible\n // dash starts at the end so we don't see the full circle. Instead we see a gap the size of the circle.\n // When the percentage is 100%, offset is 0 meaning the dash starts at the beginning so we can see the circle. (TD).\n this.svgCircle.style.strokeDasharray = `${this.getCircumference()} ${this.getCircumference()}`;\n this.svgCircle.style.strokeDashoffset = `${this.getCircumference()}`;\n }\n }\n}\n\nif (!globalThis.customElements.get('mux-uploader-progress')) {\n globalThis.customElements.define('mux-uploader-progress', MuxUploaderProgressElement);\n}\n\nexport default MuxUploaderProgressElement;\n", "import { globalThis, document } from './polyfills';\nimport { getMuxUploaderEl } from './utils/element-utils';\nimport { type MuxUploaderElementEventMap } from './mux-uploader';\nimport type MuxUploaderElement from './mux-uploader';\nimport { t } from './utils/i18n.js';\n\nconst template = document.createElement('template');\n\ntemplate.innerHTML = `\n<style>\n\n:host([upload-error]) {\n color: #e22c3e;\n}\n</style>\n\n<span id=\"status-message\" role=\"status\" aria-live=\"polite\"></span>\n`;\n\nclass MuxUploaderStatusElement extends globalThis.HTMLElement {\n statusMessage: HTMLElement | null | undefined;\n #uploaderEl: MuxUploaderElement | null | undefined;\n #abortController: AbortController | undefined;\n\n constructor() {\n super();\n const shadowRoot = this.attachShadow({ mode: 'open' });\n shadowRoot.appendChild(template.content.cloneNode(true));\n\n this.statusMessage = this.shadowRoot?.getElementById('status-message');\n }\n\n connectedCallback() {\n this.#uploaderEl = getMuxUploaderEl(this);\n this.#abortController = new AbortController();\n\n if (this.#uploaderEl) {\n const opts = { signal: this.#abortController.signal };\n this.#uploaderEl.addEventListener('reset', this.clearStatusMessage, opts);\n this.#uploaderEl.addEventListener('uploaderror', this.onUploadError, opts);\n this.#uploaderEl.addEventListener('success', this.onSuccess, opts);\n this.#uploaderEl.addEventListener('uploadstart', this.clearStatusMessage, opts);\n this.#uploaderEl.addEventListener('offline', this.onOffline, opts);\n this.#uploaderEl.addEventListener('online', this.clearStatusMessage, opts);\n\n this.toggleAttribute('upload-in-progress', this.#uploaderEl.hasAttribute('upload-in-progress'));\n this.toggleAttribute('upload-complete', this.#uploaderEl.hasAttribute('upload-complete'));\n this.toggleAttribute('upload-error', this.#uploaderEl.hasAttribute('upload-error'));\n\n this.#uploaderEl.addEventListener(\n 'localechange',\n () => {\n if (this.statusMessage?.textContent && this.#uploaderEl?.hasAttribute('upload-complete')) {\n const locale = (this.#uploaderEl as MuxUploaderElement)?.locale;\n this.statusMessage.innerHTML = t('Upload complete!', locale);\n }\n },\n opts\n );\n }\n }\n\n disconnectedCallback() {\n this.#abortController?.abort();\n }\n\n clearStatusMessage = () => {\n this.toggleAttribute('upload-error', false);\n if (this.statusMessage) {\n this.statusMessage.innerHTML = '';\n }\n };\n\n onUploadError = (e: MuxUploaderElementEventMap['uploaderror']) => {\n this.toggleAttribute('upload-error', true);\n if (this.statusMessage) {\n this.statusMessage.innerHTML = e.detail.message;\n }\n };\n\n onSuccess = () => {\n this.toggleAttribute('upload-error', false);\n\n const locale = (this.#uploaderEl as MuxUploaderElement)?.locale;\n const successMessage = t('Upload complete!', locale);\n\n if (this.statusMessage) {\n this.statusMessage.innerHTML = successMessage;\n }\n\n console.info(successMessage);\n };\n\n onOffline = () => {\n this.toggleAttribute('upload-error', false);\n const offlineMessage = 'Currently offline. Upload will resume automatically when online.';\n\n if (this.statusMessage) {\n this.statusMessage.innerHTML = offlineMessage;\n }\n };\n}\n\nif (!globalThis.customElements.get('mux-uploader-status')) {\n globalThis.customElements.define('mux-uploader-status', MuxUploaderStatusElement);\n}\n\nexport default MuxUploaderStatusElement;\n", "import { globalThis, document } from './polyfills';\nimport { getMuxUploaderEl } from './utils/element-utils';\nimport type MuxUploaderElement from './mux-uploader';\nimport { t } from './utils/i18n.js';\n\nconst template = document.createElement('template');\n\ntemplate.innerHTML = `\n<style>\n #retry-button {\n color: #e22c3e;\n text-decoration-line: underline;\n cursor: pointer;\n position: relative;\n display: none;\n }\n\n :host([upload-error]) #retry-button {\n display: inline-block;\n }\n</style>\n\n<span id=\"retry-button\" role=\"button\" tabindex=\"0\">Try again</span>\n`;\n\nclass MuxUploaderRetryElement extends globalThis.HTMLElement {\n retryButton: HTMLElement | null | undefined;\n #uploaderEl: MuxUploaderElement | null | undefined;\n #abortController: AbortController | undefined;\n\n constructor() {\n super();\n const shadowRoot = this.attachShadow({ mode: 'open' });\n shadowRoot.appendChild(template.content.cloneNode(true));\n\n this.retryButton = this.shadowRoot?.getElementById('retry-button');\n }\n\n connectedCallback() {\n this.#uploaderEl = getMuxUploaderEl(this);\n this.#abortController = new AbortController();\n\n if (this.#uploaderEl) {\n const opts = { signal: this.#abortController.signal };\n this.#uploaderEl.addEventListener('uploaderror', () => this.toggleAttribute('upload-error', true));\n this.#uploaderEl.addEventListener('reset', () => this.toggleAttribute('upload-error', false));\n this.retryButton?.addEventListener('click', this.triggerReset, opts);\n this.retryButton?.addEventListener('keyup', this.handleKeyup, opts);\n\n this.toggleAttribute('upload-error', this.#uploaderEl.hasAttribute('upload-error'));\n\n this.#uploaderEl.addEventListener('localechange', () => this.updateText(), opts);\n\n this.updateText();\n }\n }\n\n disconnectedCallback() {\n this.#abortController?.abort();\n }\n\n handleKeyup = (e: KeyboardEvent) => {\n const ButtonPressedKeys = ['Enter', ' '];\n const { key } = e;\n if (!ButtonPressedKeys.includes(key)) {\n return;\n }\n\n this.triggerReset();\n };\n\n triggerReset = () => {\n this.#uploaderEl?.dispatchEvent(new CustomEvent('reset'));\n };\n\n updateText() {\n const locale = (this.#uploaderEl as MuxUploaderElement)?.locale;\n if (this.retryButton) {\n this.retryButton.textContent = t('Retry', locale);\n }\n }\n}\n\nif (!globalThis.customElements.get('mux-uploader-retry')) {\n globalThis.customElements.define('mux-uploader-retry', MuxUploaderRetryElement);\n}\n\nexport default MuxUploaderRetryElement;\n", "import { globalThis, document } from './polyfills';\nimport { getMuxUploaderEl } from './utils/element-utils';\nimport type MuxUploaderElement from './mux-uploader';\nimport { t } from './utils/i18n.js';\n\nconst template = document.createElement('template');\n\ntemplate.innerHTML = /*html*/ `\n<style>\n#pause-button {\n cursor: pointer;\n line-height: 16px;\n background: #fff;\n border: 1px solid #000;\n color: #000000;\n padding: 16px 24px;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease;\n transition: all 0.2s ease;\n font-family: inherit;\n font-size: inherit;\n position: relative;\n display: none;\n}\n\n#pause-button:hover:not(:disabled) {\n color: #fff;\n background: #404040;\n}\n\n#pause-button:active {\n color: #fff;\n background: #000;\n}\n\n#pause-button:disabled {\n cursor: not-allowed;\n}\n\n:host([upload-in-progress]:not([upload-error], [upload-complete])) #pause-button {\n display: initial;\n}\n</style>\n\n<button id=\"pause-button\">Pause</span>\n`;\n\nclass MuxUploaderPauseElement extends globalThis.HTMLElement {\n #uploaderEl: MuxUploaderElement | null | undefined;\n #abortController: AbortController | undefined;\n\n constructor() {\n super();\n const shadowRoot = this.attachShadow({ mode: 'open' });\n shadowRoot.appendChild(template.content.cloneNode(true));\n }\n\n connectedCallback() {\n this.#uploaderEl = getMuxUploaderEl(this);\n this.#abortController = new AbortController();\n\n if (this.#uploaderEl) {\n const opts = { signal: this.#abortController.signal };\n this.#uploaderEl.addEventListener('uploadstart', () => this.toggleAttribute('upload-in-progress', true), opts);\n this.#uploaderEl.addEventListener('uploaderror', () => {\n this.toggleAttribute('upload-error', true);\n this.toggleAttribute('upload-complete', false);\n this.toggleAttribute('upload-in-progress', false);\n });\n this.#uploaderEl.addEventListener('success', () => {\n this.toggleAttribute('upload-complete', true);\n this.toggleAttribute('upload-error', false);\n this.toggleAttribute('upload-in-progress', false);\n });\n this.#uploaderEl.addEventListener('reset', () => {\n this.toggleAttribute('upload-error', false);\n this.toggleAttribute('upload-in-progress', false);\n this.toggleAttribute('upload-complete', false);\n });\n /** @TODO Implement a more robust \"pausedState\" in mux-uploader (plausibly in upchunk) to account for \"pausing\" (CJP) */\n this.#uploaderEl.addEventListener('pausedchange', () => {\n this.pauseButton.disabled = false;\n if (!this.#uploaderEl) return;\n const nextPausedState = this.#uploaderEl.paused ?? false;\n this.updateText();\n if (nextPausedState) {\n this.pauseButton.disabled = true;\n this.#uploaderEl.addEventListener(\n 'chunksuccess',\n () => {\n this.updateText();\n this.pauseButton.disabled = false;\n },\n { once: true }\n );\n }\n });\n\n this.pauseButton.addEventListener('click', this.triggerPause, opts);\n\n this.toggleAttribute('upload-in-progress', this.#uploaderEl.hasAttribute('upload-in-progress'));\n this.toggleAttribute('upload-complete', this.#uploaderEl.hasAttribute('upload-complete'));\n this.toggleAttribute('upload-error', this.#uploaderEl.hasAttribute('upload-error'));\n\n this.#uploaderEl.addEventListener('localechange', () => this.updateText(), opts);\n\n this.updateText();\n }\n }\n\n disconnectedCallback() {\n this.#abortController?.abort();\n }\n\n get pauseButton() {\n return this.shadowRoot?.getElementById('pause-button') as HTMLButtonElement;\n }\n\n updateText() {\n const locale = (this.#uploaderEl as MuxUploaderElement)?.locale;\n const isPaused = this.#uploaderEl?.paused ?? false;\n const isPausing = this.pauseButton?.disabled && isPaused;\n\n if (isPausing) {\n // If entered paused, currently does not take effect until current chunk completes upload,\n // so show as \"pausing\"\n this.pauseButton.innerHTML = t('Pausing...', locale);\n } else {\n // Recheck paused state just in case state changed while waiting for 'chunksuccess'\n this.pauseButton.innerHTML = isPaused ? t('Resume', locale) : t('Pause', locale);\n }\n }\n\n triggerPause = () => {\n if (!this.#uploaderEl) {\n console.warn('pausing before a mux-uploader element is associated is unsupported!');\n return;\n }\n if (this.pauseButton.disabled) {\n return;\n }\n this.#uploaderEl.paused = !this.#uploaderEl.paused;\n };\n}\n\nif (!globalThis.customElements.get('mux-uploader-pause')) {\n globalThis.customElements.define('mux-uploader-pause', MuxUploaderPauseElement);\n}\n\nexport default MuxUploaderPauseElement;\n", "import { globalThis, document } from './polyfills';\nimport { getMuxUploaderEl } from './utils/element-utils';\nimport type MuxUploaderElement from './mux-uploader';\nimport { t } from './utils/i18n.js';\n\nexport const fileSelectFragment = /*html*/ `\n <style>\n #file-select {\n cursor: pointer;\n line-height: 16px;\n background: #fff;\n border: 1px solid #000;\n color: #000000;\n padding: 16px 24px;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease;\n transition: all 0.2s ease;\n font-family: inherit;\n font-size: inherit;\n position: relative;\n }\n\n #file-select:hover {\n color: #fff;\n background: #404040;\n }\n\n #file-select:active {\n color: #fff;\n background: #000;\n }\n\n </style>\n\n <button id=\"file-select\" type=\"button\" part=\"file-select-button\">Upload a video</button>\n`;\n\nconst template = document.createElement('template');\n\ntemplate.innerHTML = /*html*/ `\n <style>\n :host { display: inline-block; }\n\n :host([file-ready]) > slot {\n display: none;\n }\n </style>\n\n <slot>\n ${fileSelectFragment}\n </slot>\n`;\n\nclass MuxUploaderFileSelectElement extends globalThis.HTMLElement {\n #filePickerEl: HTMLElement | null | undefined;\n #uploaderEl: MuxUploaderElement | null | undefined;\n\n #abortController: AbortController | undefined;\n\n constructor() {\n super();\n const shadowRoot = this.attachShadow({ mode: 'open' });\n shadowRoot.appendChild(template.content.cloneNode(true));\n\n // NOTE: Binding this so that we have a reference to remove the event listener\n // but can still reference `this` in the method. (CJP)\n this.handleFilePickerElClick = this.handleFilePickerElClick.bind(this);\n\n // Since we have a \"default slotted\" element, we still need to initialize the slottable elements\n // (Note the difference in selectors and related code in 'slotchange' handler, below)\n this.filePickerEl = this.shadowRoot?.querySelector('button');\n\n this.shadowRoot?.querySelector('slot')?.addEventListener('slotchange', (e) => {\n const slot = e.currentTarget as HTMLSlotElement;\n this.filePickerEl = slot\n .assignedElements({ flatten: true })\n .filter((el) => !['STYLE'].includes(el.nodeName))[0] as HTMLButtonElement;\n this.updateText();\n });\n }\n\n connectedCallback() {\n this.#uploaderEl = getMuxUploaderEl(this);\n this.#abortController = new AbortController();\n\n if (this.#uploaderEl) {\n const opts = { signal: this.#abortController.signal };\n\n this.#uploaderEl.addEventListener(\n 'file-ready',\n () => {\n this.toggleAttribute('file-ready', true);\n },\n opts\n );\n\n this.#uploaderEl.addEventListener('uploadstart', () => this.toggleAttribute('upload-in-progress', true), opts);\n\n this.#uploaderEl.addEventListener(\n 'success',\n () => {\n this.toggleAttribute('upload-in-progress', false);\n this.toggleAttribute('upload-complete', true);\n },\n opts\n );\n\n this.#uploaderEl.addEventListener(\n 'reset',\n () => {\n this.toggleAttribute('file-ready', false);\n },\n opts\n );\n\n this.toggleAttribute('upload-in-progress', this.#uploaderEl.hasAttribute('upload-in-progress'));\n this.toggleAttribute('upload-complete', this.#uploaderEl.hasAttribute('upload-complete'));\n this.toggleAttribute('file-ready', this.#uploaderEl.hasAttribute('file-ready'));\n\n this.#uploaderEl.addEventListener('localechange', () => this.updateText(), opts);\n\n this.updateText();\n }\n }\n\n disconnectedCallback() {\n this.#abortController?.abort();\n }\n\n protected get filePickerEl() {\n return this.#filePickerEl;\n }\n\n protected set filePickerEl(value: HTMLElement | null | undefined) {\n if (value === this.#filePickerEl) return;\n if (this.#filePickerEl) {\n this.#filePickerEl.removeEventListener('click', this.handleFilePickerElClick);\n }\n\n this.#filePickerEl = value;\n if (this.#filePickerEl) {\n this.#filePickerEl.addEventListener('click', this.handleFilePickerElClick);\n }\n }\n\n handleFilePickerElClick() {\n // TO-DO: Allow