@vielzeug/refine 2.2.5 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/carousel.cjs +1 -1
- package/dist/carousel.cjs.map +1 -1
- package/dist/carousel.js +1 -1
- package/dist/carousel.js.map +1 -1
- package/dist/chat-message.cjs +9 -9
- package/dist/chat-message.cjs.map +1 -1
- package/dist/chat-message.js +40 -37
- package/dist/chat-message.js.map +1 -1
- package/dist/command-palette.cjs +1 -1
- package/dist/command-palette.cjs.map +1 -1
- package/dist/command-palette.js +1 -1
- package/dist/command-palette.js.map +1 -1
- package/dist/core/announcer.cjs +1 -1
- package/dist/core/announcer.cjs.map +1 -1
- package/dist/core/announcer.d.ts +13 -3
- package/dist/core/announcer.d.ts.map +1 -1
- package/dist/core/announcer.js +16 -9
- package/dist/core/announcer.js.map +1 -1
- package/dist/custom-elements.json +4328 -4328
- package/dist/feedback/typing-indicator/typing-indicator.d.ts +2 -2
- package/dist/file-input.cjs +49 -49
- package/dist/file-input.cjs.map +1 -1
- package/dist/file-input.js +128 -127
- package/dist/file-input.js.map +1 -1
- package/dist/inputs/file-input/file-input-upload.cjs +1 -1
- package/dist/inputs/file-input/file-input-upload.cjs.map +1 -1
- package/dist/inputs/file-input/file-input-upload.d.ts +2 -0
- package/dist/inputs/file-input/file-input-upload.d.ts.map +1 -1
- package/dist/inputs/file-input/file-input-upload.js +5 -2
- package/dist/inputs/file-input/file-input-upload.js.map +1 -1
- package/dist/inputs/file-input/file-input.d.ts.map +1 -1
- package/dist/refine.cjs +62 -62
- package/dist/refine.cjs.map +1 -1
- package/dist/refine.iife.js +62 -62
- package/dist/refine.iife.js.map +1 -1
- package/dist/refine.js +62 -62
- package/dist/refine.js.map +1 -1
- package/dist/typing-indicator.cjs +1 -1
- package/dist/typing-indicator.cjs.map +1 -1
- package/dist/typing-indicator.js +8 -7
- package/dist/typing-indicator.js.map +1 -1
- package/package.json +4 -4
package/dist/file-input.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-input.js","names":[],"sources":["../src/inputs/file-input/file-input.ts"],"sourcesContent":["import { createDropZone } from '@vielzeug/dnd';\nimport {\n bind,\n createStableId,\n define,\n html,\n onCleanup,\n onElement,\n onEvent,\n onMounted,\n prop,\n ref,\n useEmit,\n useField,\n when,\n} from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\n\nimport '../../content/icon/icon';\nimport '../../feedback/progress/progress';\nimport { bindRefCallback, createInteraction } from '../../core';\nimport { FILE_INPUT_SIZE_PRESET } from '../../shared';\nimport {\n coarsePointerMixin,\n colorThemeMixin,\n disabledLoadingMixin,\n forcedColorsFocusMixin,\n reducedMotionMixin,\n roundedVariantMixin,\n sizeVariantMixin,\n} from '../../styles';\nimport componentStyles from './file-input.css?inline';\nimport { createFileQueue, type FileUploadFn, formatBytes } from './file-input-upload';\n\nexport type { FileUploadFn, FileUploadState, FileUploadStatus } from './file-input-upload';\n\nconst isImageFile = (file: File): boolean => file.type.startsWith('image/');\n\n/** File input component properties */\nexport type OreFileInputProps = {\n /** Accepted file types (comma-separated, e.g. '.jpg, .png, image/*') */\n accept?: string;\n /** Theme color tint */\n color?: string;\n /** Disabled state */\n disabled?: boolean;\n /** Error message text */\n error?: string;\n /**\n * Render selected files as a grid of image thumbnails/previews instead of the default\n * single-column list. Non-image files fall back to a generic file icon in the same grid.\n */\n gallery?: boolean;\n /** Helper text displayed below the input */\n helper?: string;\n /** Input label text */\n label?: string;\n /** Max number of files allowed (only used if multiple is true) */\n 'max-files'?: number;\n /** Max size of a single file in bytes */\n 'max-size'?: number;\n /** Allow multiple files selection */\n multiple?: boolean;\n /** Form field name */\n name?: string;\n /**\n * JS-only callback fired with the inner `<input type=\"file\">` element when it\n * mounts, and with `null` when it unmounts. Intended for composed components\n * that need imperative access to the raw element.\n * Set as a JS property: `bitFileInput.ref = (el) => { ... }`.\n */\n ref?: ((el: HTMLInputElement | null) => void) | null;\n /** Required field */\n required?: boolean;\n /** Field size preset */\n size?: string;\n /**\n * JS-only upload transport (see `FileUploadFn`). When set, every newly added file starts\n * uploading immediately and independently — progress/speed/ETA, retry-on-failure, and a\n * success confirmation card are all driven from here. Omit it to keep the component in its\n * original picker-only mode (select files, no upload lifecycle).\n * Set as a JS property: `fileInput.upload = async (file, { onProgress, signal }) => { ... }`.\n */\n upload?: FileUploadFn | null;\n};\n\n/** Events emitted by the file-input component */\nexport type OreFileInputEvents = {\n /** Emitted when files are added or removed */\n change: { files: File[]; originalEvent?: Event; value: File[] };\n /** Emitted when a specific file is removed */\n remove: { file: File; files: File[]; originalEvent?: Event; value: File[] };\n /** Emitted when `upload` rejects for a file (after a fresh attempt or a retry) */\n 'upload-error': { error: unknown; file: File };\n /** Emitted whenever `upload`'s `onProgress` reports new bytes for a file */\n 'upload-progress': { file: File; loaded: number; total: number };\n /** Emitted when `upload` resolves for a file */\n 'upload-success': { file: File };\n};\n\n/**\n * A file upload field with drag-and-drop support, built-in validation messaging, and — once a\n * `upload` transport is wired up — a full per-file upload lifecycle: live progress/speed/ETA,\n * retry-on-failure, a success confirmation card, and fully independent handling of concurrent\n * files (one failing never blocks or cancels the others). Picking files with no `upload` set\n * keeps the original, upload-free selection-only behavior.\n *\n * @element ore-file-input\n *\n * @attr {string} accept - Comma-separated file extensions or MIME types\n * @attr {boolean} multiple - Enable multiple files selection\n * @attr {boolean} gallery - Show selected files as an image thumbnail/preview grid\n * @attr {number} max-files - Max number of files allowed\n * @attr {number} max-size - Max size of each file in bytes\n * @attr {boolean} disabled - Disable interaction\n * @attr {string} error - Show an error state/message\n * @attr {string} helper - Provide helper context below the dropzone\n *\n * @fires change - detail: { files: File[], value: File[] }\n * @fires remove - detail: { file: File, files: File[] }\n * @fires upload-progress - detail: { file: File, loaded: number, total: number }\n * @fires upload-success - detail: { file: File }\n * @fires upload-error - detail: { file: File, error: unknown }\n *\n * @cssprop --file-input-bg - Dropzone background color\n * @cssprop --file-input-border-color - Dropzone border color\n * @cssprop --file-input-font-size - Font size\n * @cssprop --file-input-radius - Dropzone border radius\n * @cssprop --file-input-min-height - Minimum dropzone height\n * @cssprop --file-input-hover-bg - Dropzone background on hover (flat/ghost variants)\n * @cssprop --file-input-hover-border-color - Dropzone border on hover (flat/bordered variants)\n * @cssprop --file-input-focus-bg - Dropzone background when focused/drag-over (flat variant)\n * @cssprop --file-input-focus-border-color - Dropzone border when focused/drag-over (flat variant)\n * @cssprop --file-input-thumb-size - Gallery thumbnail width/height\n * @part wrapper - Root wrapper around the file input field\n * @part label - Visible label rendered above the dropzone\n * @part dropzone - Interactive drag-and-drop target\n * @part input - Native file input element\n * @part gallery - Gallery grid container (rendered instead of `file-list` when `gallery` is set)\n * @part helper - Helper text shown beneath the dropzone\n * @part error - Error message shown beneath the field\n * @example\n * ```html\n * <ore-file-input label=\"Upload files\" accept=\"image/*\" multiple />\n * <ore-file-input label=\"Photos\" accept=\"image/*\" multiple gallery />\n * <ore-file-input label=\"Resume\" accept=\".pdf,.doc,.docx\" max-size=\"5242880\" />\n * <ore-file-input variant=\"bordered\" color=\"primary\" />\n * ```\n * ```ts\n * // Wire up a real upload transport — progress/retry/success are then handled automatically.\n * // `XMLHttpRequest` is used here (not `fetch`) because it's the only browser API that reports\n * // upload progress; swap in whatever transport the app already uses.\n * const input = document.querySelector('ore-file-input');\n * input.upload = (file, { onProgress, signal, resumeFrom }) =>\n * new Promise((resolve, reject) => {\n * const xhr = new XMLHttpRequest();\n *\n * xhr.upload.addEventListener('progress', (e) => onProgress(resumeFrom + e.loaded, file.size));\n * xhr.addEventListener('load', () => (xhr.status < 400 ? resolve() : reject(new Error(xhr.statusText))));\n * xhr.addEventListener('error', () => reject(new Error('Network error')));\n * signal.addEventListener('abort', () => xhr.abort());\n *\n * xhr.open('PUT', '/uploads');\n * if (resumeFrom > 0) xhr.setRequestHeader('Content-Range', `bytes ${resumeFrom}-/${file.size}`);\n * xhr.send(file);\n * });\n * ```\n */\nexport const FILE_INPUT_TAG = 'ore-file-input' as const;\ndefine<OreFileInputProps>(FILE_INPUT_TAG, {\n formAssociated: true,\n props: {\n accept: prop.string(),\n color: prop.string(),\n disabled: prop.bool(false),\n error: prop.string(),\n gallery: prop.bool(false),\n helper: prop.string(),\n label: prop.string(),\n 'max-files': prop.number(0),\n 'max-size': prop.number(0),\n multiple: prop.bool(false),\n name: prop.string(),\n ref: prop.data<((el: HTMLInputElement | null) => void) | null>(),\n required: prop.bool(false),\n size: prop.string(),\n upload: prop.data<FileUploadFn>(),\n },\n setup(props) {\n const emit = useEmit<OreFileInputEvents>();\n\n // ============================================\n // State\n // ============================================\n\n const isDragging = signal(false);\n // Set while a \"Replace\" action is waiting for the (shared) native file input's next\n // `change` — routes that selection into `queue.replaceFile()` instead of `queue.addFiles()`.\n const replaceTarget = signal<File | null>(null);\n\n const isDisabled = computed(() => Boolean(props.disabled.value));\n const maxFilesLimit = computed(() => props['max-files'].value ?? 0);\n const maxSizeLimit = computed(() => props['max-size'].value ?? 0);\n\n // File selection + the opt-in upload lifecycle (progress, retry, replace, per-file\n // isolation) live in `createFileQueue` — see file-input-upload.ts for why they're one unit.\n const queue = createFileQueue({\n accept: props.accept,\n disabled: isDisabled,\n maxFiles: maxFilesLimit,\n maxSize: maxSizeLimit,\n multiple: computed(() => Boolean(props.multiple.value)),\n onChange: (changedFiles, originalEvent) =>\n emit('change', { files: changedFiles, originalEvent, value: changedFiles }),\n onRemove: (file, remainingFiles, originalEvent) =>\n emit('remove', { file, files: remainingFiles, originalEvent, value: remainingFiles }),\n onUploadError: (file, error) => emit('upload-error', { error, file }),\n onUploadProgress: (file, loaded, total) => emit('upload-progress', { file, loaded, total }),\n onUploadSuccess: (file) => emit('upload-success', { file }),\n upload: computed(() => props.upload.value ?? undefined),\n });\n\n onCleanup(() => queue.dispose());\n\n // ============================================\n // Form Integration\n // ============================================\n\n useField({\n disabled: isDisabled,\n toFormValue: (fi: File[]) => {\n if (fi.length === 0) return null;\n\n const name = props.name.value || 'file';\n const fd = new FormData();\n\n for (const file of fi) fd.append(name, file);\n\n return fd;\n },\n value: queue.files,\n });\n\n // Sync host attributes for CSS selectors\n const isInvalid = computed(() => Boolean(props.error.value));\n\n bind({\n attr: {\n 'drag-over': () => (isDragging.value ? true : undefined),\n invalid: () => (isInvalid.value ? true : undefined),\n size: props.size,\n },\n });\n\n // ============================================\n // IDs\n // ============================================\n const fileInputId = createStableId('file-input');\n const labelId = `label-${fileInputId}`;\n const helperId = `helper-${fileInputId}`;\n const errorId = `error-${fileInputId}`;\n\n // ============================================\n // Refs\n // ============================================\n const dropzoneRef = ref<HTMLDivElement>();\n const inputRef = ref<HTMLInputElement>();\n const hintText = computed(() => {\n const parts: string[] = [];\n\n if (props.accept.value) {\n parts.push(\n props.accept.value\n .split(',')\n .map((s: string) => s.trim())\n .join(', '),\n );\n }\n\n const maxSize = maxSizeLimit.value;\n\n if (maxSize > 0) parts.push(`max ${formatBytes(maxSize)}`);\n\n const maxFiles = maxFilesLimit.value;\n\n if (maxFiles > 0) parts.push(`up to ${maxFiles} file${maxFiles !== 1 ? 's' : ''}`);\n\n return parts.join(' · ');\n });\n\n // ============================================\n // Replace (\"swap this one file\" — see file-input-upload.ts's `replaceFile`)\n // ============================================\n function beginReplace(file: File): void {\n replaceTarget.value = file;\n inputRef.value?.click();\n }\n\n // ============================================\n // Gallery Preview URLs\n // ============================================\n // Object URLs are created lazily (only while `gallery` is enabled) and revoked as soon\n // as their file is no longer selected, plus unconditionally on disconnect — otherwise\n // each preview leaks its backing blob for the life of the page.\n const previewUrls = new Map<File, string>();\n\n function getPreviewUrl(file: File): string {\n let url = previewUrls.get(file);\n\n if (!url) {\n url = URL.createObjectURL(file);\n previewUrls.set(file, url);\n }\n\n return url;\n }\n\n function revokeStalePreviewUrls(activeFiles: File[]): void {\n const keep = new Set(activeFiles);\n\n for (const [file, url] of previewUrls) {\n if (!keep.has(file)) {\n URL.revokeObjectURL(url);\n previewUrls.delete(file);\n }\n }\n }\n\n watch(queue.files, revokeStalePreviewUrls);\n\n onCleanup(() => {\n for (const url of previewUrls.values()) URL.revokeObjectURL(url);\n\n previewUrls.clear();\n });\n\n // ============================================\n // Mount\n // ============================================\n // ============================================\n // Template\n // ============================================\n onElement(inputRef, (inp) => bindRefCallback(props.ref, inp));\n\n onMounted(() => {\n const inp = inputRef.value!;\n const dz = dropzoneRef.value!;\n let skipNextClick = false;\n const pressControl = createInteraction({\n disabled: isDisabled,\n onPress: () => {\n inp.click();\n },\n });\n\n // Native input → add files, or — if a \"Replace\" action opened the picker — swap the one\n // file it targeted instead of appending a new entry.\n onEvent(inp, 'change', (e: Event) => {\n const input = e.target as HTMLInputElement;\n const target = replaceTarget.value;\n\n replaceTarget.value = null;\n\n if (target && input.files?.[0]) queue.replaceFile(target, input.files[0], e);\n else if (input.files?.length) queue.addFiles(Array.from(input.files), e);\n\n input.value = ''; // reset so the same file triggers change again\n });\n // Click dropzone → open file picker\n onEvent(dz, 'click', (e: MouseEvent) => {\n if (e.target === inp) return;\n\n if (skipNextClick) {\n skipNextClick = false;\n\n return;\n }\n\n if (!isDisabled.value) inp.click();\n });\n // Keyboard: Enter / Space → open picker\n onEvent(dz, 'keydown', (e: KeyboardEvent) => {\n skipNextClick = pressControl.handleKeydown(e) && e.key === 'Enter';\n });\n\n // `createDropZone` has no way to update `disabled` after creation — recreate the zone\n // whenever the prop changes instead of capturing a stale snapshot from this one `onMounted`\n // run. `watch`'s own returned cleanup (not a second `onCleanup`) disposes the current zone\n // both on the next toggle and on final teardown.\n watch(\n isDisabled,\n (disabled) => {\n const dropZone = createDropZone({\n disabled,\n element: dz,\n onDrop: (droppedFiles) => queue.addFiles(droppedFiles),\n onHoverChange: (hovered) => {\n isDragging.value = hovered;\n },\n });\n\n return () => dropZone.dispose();\n },\n { immediate: true },\n );\n });\n\n return html`\n <div class=\"file-input-wrapper\" part=\"wrapper\">\n <label class=\"label-outside\" id=\"${labelId}\" part=\"label\" ?hidden=${() => !props.label.value}>\n ${props.label}\n </label>\n <div\n class=\"dropzone\"\n part=\"dropzone\"\n ref=${dropzoneRef}\n role=\"button\"\n tabindex=\"${() => (isDisabled.value ? '-1' : '0')}\"\n aria-disabled=\"${() => String(isDisabled.value)}\"\n aria-label=\"${() => (!props.label.value ? 'File upload drop zone' : null)}\"\n aria-labelledby=\"${() => (props.label.value ? labelId : null)}\"\n aria-describedby=\"${helperId}\">\n <input\n type=\"file\"\n ref=${inputRef}\n part=\"input\"\n id=\"${fileInputId}\"\n accept=\"${props.accept}\"\n ?multiple=\"${props.multiple}\"\n ?required=\"${props.required}\"\n ?disabled=\"${isDisabled}\"\n name=\"${props.name}\"\n hidden\n inert\n tabindex=\"-1\" />\n <div class=\"dropzone-content\">\n <span class=\"dropzone-icon\" aria-hidden=\"true\" data-status=\"${() => (isInvalid.value ? 'error' : null)}\">\n ${() =>\n isInvalid.value\n ? html`\n <ore-icon name=\"alert-circle\" size=\"36\" stroke-width=\"1.5\" aria-hidden=\"true\"></ore-icon>\n `\n : html`\n <ore-icon name=\"upload\" size=\"36\" stroke-width=\"1.5\" aria-hidden=\"true\"></ore-icon>\n `}\n </span>\n <!-- Signal 01: the copy itself shifts on drag-entry (not just border/glow) — a\n static \"Drop files here\" during an active drag reads as if the drop target\n hasn't noticed the file yet. -->\n ${when(\n () => isDragging.value,\n () => html`\n <span class=\"dropzone-title dropzone-title-active\">Release to upload</span>\n `,\n )}\n ${when(\n () => !isDragging.value,\n () => html`\n <span class=\"dropzone-title\">\n Drop files here or\n <u>click to browse</u>\n </span>\n `,\n )}\n <span class=\"dropzone-hint\" ?hidden=${() => !hintText.value}>${hintText}</span>\n </div>\n </div>\n <ul\n class=\"${() => (props.gallery.value ? 'file-grid' : 'file-list')}\"\n part=\"${() => (props.gallery.value ? 'gallery' : null)}\"\n role=\"list\"\n aria-label=\"Selected files\"\n ?hidden=${() => queue.files.value.length === 0}>\n ${() =>\n queue.files.value.map((file: File) =>\n props.gallery.value\n ? html`\n <li class=\"file-card\" data-status=${() => queue.fileState(file).status}>\n <span class=\"file-thumb-frame\">\n ${\n // Decorative: the file name is already announced via the visible\n // `.file-card-name` caption below — repeating it as `alt` text would\n // duplicate it (and often trips redundant-alt checks, since real\n // filenames commonly contain words like \"photo\" or \"image\").\n isImageFile(file)\n ? // Object URLs use the `blob:` scheme, which ore's attribute-level\n // XSS guard blocks unconditionally on `src` (and other\n // URL-accepting attributes) — set it as a DOM property via `ref`\n // instead, bypassing that string-based check. Safe here: the value\n // comes from `URL.createObjectURL(file)` on a real `File` object we\n // control, never from untrusted text.\n html`\n <img\n class=\"file-thumb\"\n alt=\"\"\n ref=\"${(el: HTMLImageElement | null) => {\n if (el) el.src = getPreviewUrl(file);\n }}\" />\n `\n : html`\n <span class=\"file-thumb file-thumb-generic\" aria-hidden=\"true\">\n <ore-icon name=\"file\" size=\"28\" stroke-width=\"1.5\" aria-hidden=\"true\"></ore-icon>\n </span>\n `\n }\n ${when(\n () => queue.fileState(file).status === 'success',\n () => html`\n <span class=\"file-status-badge file-status-badge-success\" aria-hidden=\"true\">\n <ore-icon name=\"check\" size=\"11\" stroke-width=\"3\" aria-hidden=\"true\"></ore-icon>\n </span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <span class=\"file-status-badge file-status-badge-error\" aria-hidden=\"true\">\n <ore-icon name=\"alert-circle\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </span>\n `,\n )}\n <span class=\"file-card-actions\">\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <button\n class=\"file-card-action\"\n type=\"button\"\n aria-label=\"${`Retry uploading ${file.name}`}\"\n @click=${() => queue.retryUpload(file)}>\n <ore-icon name=\"refresh-cw\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n ${when(\n () =>\n queue.fileState(file).status === 'success' &&\n Boolean(props.multiple.value) &&\n Boolean(props.upload.value),\n () => html`\n <button\n class=\"file-card-action\"\n type=\"button\"\n aria-label=\"${`Replace ${file.name}`}\"\n @click=${() => beginReplace(file)}>\n <ore-icon name=\"upload\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n <button\n class=\"file-card-action file-card-remove\"\n type=\"button\"\n aria-label=\"${`Remove ${file.name}`}\"\n @click=${(e: Event) => queue.removeFile(file, e)}>\n <ore-icon name=\"x\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n </span>\n </span>\n <span class=\"file-card-name\" title=\"${file.name}\">${file.name}</span>\n ${when(\n () => queue.fileState(file).status === 'uploading',\n () => html`\n <ore-progress\n type=\"linear\"\n size=\"sm\"\n value=${() => queue.uploadPercent(file)}\n label=${() => `${queue.uploadPercent(file)}%`}></ore-progress>\n <span class=\"file-progress-meta\">${() => queue.uploadMetaText(file)}</span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <span class=\"file-error-text\">${() => queue.fileState(file).error}</span>\n `,\n )}\n </li>\n `\n : html`\n <li class=\"file-item\" data-status=${() => queue.fileState(file).status}>\n <span class=\"file-icon\" aria-hidden=\"true\">\n <ore-icon\n name=${() => queue.statusIconName(file)}\n size=\"18\"\n stroke-width=\"1.75\"\n aria-hidden=\"true\"></ore-icon>\n </span>\n <span class=\"file-meta\">\n <span class=\"file-name\" title=\"${file.name}\">${file.name}</span>\n ${when(\n () => queue.fileState(file).status === 'uploading',\n () => html`\n <ore-progress\n type=\"linear\"\n size=\"sm\"\n value=${() => queue.uploadPercent(file)}\n label=${() => `${queue.uploadPercent(file)}%`}></ore-progress>\n <span class=\"file-progress-meta\">${() => queue.uploadMetaText(file)}</span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <span class=\"file-error-text\">${() => queue.fileState(file).error}</span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'idle' || queue.fileState(file).status === 'success',\n () => html`\n <span class=\"file-size\">${formatBytes(file.size)}</span>\n `,\n )}\n </span>\n <span class=\"file-actions\">\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <button\n class=\"file-action\"\n type=\"button\"\n aria-label=\"${`Retry uploading ${file.name}`}\"\n @click=${() => queue.retryUpload(file)}>\n <ore-icon name=\"refresh-cw\" size=\"14\" stroke-width=\"2\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n ${when(\n () =>\n queue.fileState(file).status === 'success' &&\n Boolean(props.multiple.value) &&\n Boolean(props.upload.value),\n () => html`\n <button\n class=\"file-action\"\n type=\"button\"\n aria-label=\"${`Replace ${file.name}`}\"\n @click=${() => beginReplace(file)}>\n <ore-icon name=\"upload\" size=\"14\" stroke-width=\"2\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n <button\n class=\"file-remove\"\n type=\"button\"\n aria-label=\"${`Remove ${file.name}`}\"\n @click=${(e: Event) => queue.removeFile(file, e)}>\n <ore-icon name=\"x\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n </span>\n </li>\n `,\n )}\n </ul>\n <div class=\"helper-text\" id=\"${helperId}\" part=\"helper\" ?hidden=${() => isInvalid.value || !props.helper.value}>\n ${props.helper}\n </div>\n <div\n class=\"helper-text helper-text-error\"\n id=\"${errorId}\"\n role=\"alert\"\n part=\"error\"\n ?hidden=${() => !isInvalid.value}>\n ${() => props.error.value ?? ''}\n </div>\n </div>\n `;\n },\n shadow: { delegatesFocus: true },\n styles: [\n colorThemeMixin,\n coarsePointerMixin,\n reducedMotionMixin,\n roundedVariantMixin,\n disabledLoadingMixin,\n sizeVariantMixin(FILE_INPUT_SIZE_PRESET),\n forcedColorsFocusMixin('.dropzone'),\n componentStyles,\n ],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-input.js","names":[],"sources":["../src/inputs/file-input/file-input.ts"],"sourcesContent":["import { createDropZone } from '@vielzeug/dnd';\nimport {\n bind,\n createStableId,\n define,\n getHost,\n html,\n onCleanup,\n onElement,\n onEvent,\n onMounted,\n prop,\n ref,\n useEmit,\n useField,\n when,\n} from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\n\nimport '../../content/icon/icon';\nimport '../../feedback/progress/progress';\nimport { bindRefCallback, createInteraction } from '../../core';\nimport { FILE_INPUT_SIZE_PRESET } from '../../shared';\nimport {\n coarsePointerMixin,\n colorThemeMixin,\n disabledLoadingMixin,\n forcedColorsFocusMixin,\n reducedMotionMixin,\n roundedVariantMixin,\n sizeVariantMixin,\n} from '../../styles';\nimport componentStyles from './file-input.css?inline';\nimport { createFileQueue, type FileUploadFn, formatBytes } from './file-input-upload';\n\nexport type { FileUploadFn, FileUploadState, FileUploadStatus } from './file-input-upload';\n\nconst isImageFile = (file: File): boolean => file.type.startsWith('image/');\n\n/** File input component properties */\nexport type OreFileInputProps = {\n /** Accepted file types (comma-separated, e.g. '.jpg, .png, image/*') */\n accept?: string;\n /** Theme color tint */\n color?: string;\n /** Disabled state */\n disabled?: boolean;\n /** Error message text */\n error?: string;\n /**\n * Render selected files as a grid of image thumbnails/previews instead of the default\n * single-column list. Non-image files fall back to a generic file icon in the same grid.\n */\n gallery?: boolean;\n /** Helper text displayed below the input */\n helper?: string;\n /** Input label text */\n label?: string;\n /** Max number of files allowed (only used if multiple is true) */\n 'max-files'?: number;\n /** Max size of a single file in bytes */\n 'max-size'?: number;\n /** Allow multiple files selection */\n multiple?: boolean;\n /** Form field name */\n name?: string;\n /**\n * JS-only callback fired with the inner `<input type=\"file\">` element when it\n * mounts, and with `null` when it unmounts. Intended for composed components\n * that need imperative access to the raw element.\n * Set as a JS property: `bitFileInput.ref = (el) => { ... }`.\n */\n ref?: ((el: HTMLInputElement | null) => void) | null;\n /** Required field */\n required?: boolean;\n /** Field size preset */\n size?: string;\n /**\n * JS-only upload transport (see `FileUploadFn`). When set, every newly added file starts\n * uploading immediately and independently — progress/speed/ETA, retry-on-failure, and a\n * success confirmation card are all driven from here. Omit it to keep the component in its\n * original picker-only mode (select files, no upload lifecycle).\n * Set as a JS property: `fileInput.upload = async (file, { onProgress, signal }) => { ... }`.\n */\n upload?: FileUploadFn | null;\n};\n\n/** Events emitted by the file-input component */\nexport type OreFileInputEvents = {\n /** Emitted when files are added or removed */\n change: { files: File[]; originalEvent?: Event; value: File[] };\n /** Emitted when a specific file is removed */\n remove: { file: File; files: File[]; originalEvent?: Event; value: File[] };\n /** Emitted when `upload` rejects for a file (after a fresh attempt or a retry) */\n 'upload-error': { error: unknown; file: File };\n /** Emitted whenever `upload`'s `onProgress` reports new bytes for a file */\n 'upload-progress': { file: File; loaded: number; total: number };\n /** Emitted when `upload` resolves for a file */\n 'upload-success': { file: File };\n};\n\n/**\n * A file upload field with drag-and-drop support, built-in validation messaging, and — once a\n * `upload` transport is wired up — a full per-file upload lifecycle: live progress/speed/ETA,\n * retry-on-failure, a success confirmation card, and fully independent handling of concurrent\n * files (one failing never blocks or cancels the others). Picking files with no `upload` set\n * keeps the original, upload-free selection-only behavior.\n *\n * @element ore-file-input\n *\n * @attr {string} accept - Comma-separated file extensions or MIME types\n * @attr {boolean} multiple - Enable multiple files selection\n * @attr {boolean} gallery - Show selected files as an image thumbnail/preview grid\n * @attr {number} max-files - Max number of files allowed\n * @attr {number} max-size - Max size of each file in bytes\n * @attr {boolean} disabled - Disable interaction\n * @attr {string} error - Show an error state/message\n * @attr {string} helper - Provide helper context below the dropzone\n *\n * @fires change - detail: { files: File[], value: File[] }\n * @fires remove - detail: { file: File, files: File[] }\n * @fires upload-progress - detail: { file: File, loaded: number, total: number }\n * @fires upload-success - detail: { file: File }\n * @fires upload-error - detail: { file: File, error: unknown }\n *\n * @cssprop --file-input-bg - Dropzone background color\n * @cssprop --file-input-border-color - Dropzone border color\n * @cssprop --file-input-font-size - Font size\n * @cssprop --file-input-radius - Dropzone border radius\n * @cssprop --file-input-min-height - Minimum dropzone height\n * @cssprop --file-input-hover-bg - Dropzone background on hover (flat/ghost variants)\n * @cssprop --file-input-hover-border-color - Dropzone border on hover (flat/bordered variants)\n * @cssprop --file-input-focus-bg - Dropzone background when focused/drag-over (flat variant)\n * @cssprop --file-input-focus-border-color - Dropzone border when focused/drag-over (flat variant)\n * @cssprop --file-input-thumb-size - Gallery thumbnail width/height\n * @part wrapper - Root wrapper around the file input field\n * @part label - Visible label rendered above the dropzone\n * @part dropzone - Interactive drag-and-drop target\n * @part input - Native file input element\n * @part gallery - Gallery grid container (rendered instead of `file-list` when `gallery` is set)\n * @part helper - Helper text shown beneath the dropzone\n * @part error - Error message shown beneath the field\n * @example\n * ```html\n * <ore-file-input label=\"Upload files\" accept=\"image/*\" multiple />\n * <ore-file-input label=\"Photos\" accept=\"image/*\" multiple gallery />\n * <ore-file-input label=\"Resume\" accept=\".pdf,.doc,.docx\" max-size=\"5242880\" />\n * <ore-file-input variant=\"bordered\" color=\"primary\" />\n * ```\n * ```ts\n * // Wire up a real upload transport — progress/retry/success are then handled automatically.\n * // `XMLHttpRequest` is used here (not `fetch`) because it's the only browser API that reports\n * // upload progress; swap in whatever transport the app already uses.\n * const input = document.querySelector('ore-file-input');\n * input.upload = (file, { onProgress, signal, resumeFrom }) =>\n * new Promise((resolve, reject) => {\n * const xhr = new XMLHttpRequest();\n *\n * xhr.upload.addEventListener('progress', (e) => onProgress(resumeFrom + e.loaded, file.size));\n * xhr.addEventListener('load', () => (xhr.status < 400 ? resolve() : reject(new Error(xhr.statusText))));\n * xhr.addEventListener('error', () => reject(new Error('Network error')));\n * signal.addEventListener('abort', () => xhr.abort());\n *\n * xhr.open('PUT', '/uploads');\n * if (resumeFrom > 0) xhr.setRequestHeader('Content-Range', `bytes ${resumeFrom}-/${file.size}`);\n * xhr.send(file);\n * });\n * ```\n */\nexport const FILE_INPUT_TAG = 'ore-file-input' as const;\ndefine<OreFileInputProps>(FILE_INPUT_TAG, {\n formAssociated: true,\n props: {\n accept: prop.string(),\n color: prop.string(),\n disabled: prop.bool(false),\n error: prop.string(),\n gallery: prop.bool(false),\n helper: prop.string(),\n label: prop.string(),\n 'max-files': prop.number(0),\n 'max-size': prop.number(0),\n multiple: prop.bool(false),\n name: prop.string(),\n ref: prop.data<((el: HTMLInputElement | null) => void) | null>(),\n required: prop.bool(false),\n size: prop.string(),\n upload: prop.data<FileUploadFn>(),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreFileInputEvents>();\n\n // ============================================\n // State\n // ============================================\n\n const isDragging = signal(false);\n // Set while a \"Replace\" action is waiting for the (shared) native file input's next\n // `change` — routes that selection into `queue.replaceFile()` instead of `queue.addFiles()`.\n const replaceTarget = signal<File | null>(null);\n\n const isDisabled = computed(() => Boolean(props.disabled.value));\n const maxFilesLimit = computed(() => props['max-files'].value ?? 0);\n const maxSizeLimit = computed(() => props['max-size'].value ?? 0);\n\n // File selection + the opt-in upload lifecycle (progress, retry, replace, per-file\n // isolation) live in `createFileQueue` — see file-input-upload.ts for why they're one unit.\n const queue = createFileQueue({\n accept: props.accept,\n disabled: isDisabled,\n document: el.ownerDocument,\n maxFiles: maxFilesLimit,\n maxSize: maxSizeLimit,\n multiple: computed(() => Boolean(props.multiple.value)),\n onChange: (changedFiles, originalEvent) =>\n emit('change', { files: changedFiles, originalEvent, value: changedFiles }),\n onRemove: (file, remainingFiles, originalEvent) =>\n emit('remove', { file, files: remainingFiles, originalEvent, value: remainingFiles }),\n onUploadError: (file, error) => emit('upload-error', { error, file }),\n onUploadProgress: (file, loaded, total) => emit('upload-progress', { file, loaded, total }),\n onUploadSuccess: (file) => emit('upload-success', { file }),\n upload: computed(() => props.upload.value ?? undefined),\n });\n\n onCleanup(() => queue.dispose());\n\n // ============================================\n // Form Integration\n // ============================================\n\n useField({\n disabled: isDisabled,\n toFormValue: (fi: File[]) => {\n if (fi.length === 0) return null;\n\n const name = props.name.value || 'file';\n const fd = new FormData();\n\n for (const file of fi) fd.append(name, file);\n\n return fd;\n },\n value: queue.files,\n });\n\n // Sync host attributes for CSS selectors\n const isInvalid = computed(() => Boolean(props.error.value));\n\n bind({\n attr: {\n 'drag-over': () => (isDragging.value ? true : undefined),\n invalid: () => (isInvalid.value ? true : undefined),\n size: props.size,\n },\n });\n\n // ============================================\n // IDs\n // ============================================\n const fileInputId = createStableId('file-input');\n const labelId = `label-${fileInputId}`;\n const helperId = `helper-${fileInputId}`;\n const errorId = `error-${fileInputId}`;\n\n // ============================================\n // Refs\n // ============================================\n const dropzoneRef = ref<HTMLDivElement>();\n const inputRef = ref<HTMLInputElement>();\n const hintText = computed(() => {\n const parts: string[] = [];\n\n if (props.accept.value) {\n parts.push(\n props.accept.value\n .split(',')\n .map((s: string) => s.trim())\n .join(', '),\n );\n }\n\n const maxSize = maxSizeLimit.value;\n\n if (maxSize > 0) parts.push(`max ${formatBytes(maxSize)}`);\n\n const maxFiles = maxFilesLimit.value;\n\n if (maxFiles > 0) parts.push(`up to ${maxFiles} file${maxFiles !== 1 ? 's' : ''}`);\n\n return parts.join(' · ');\n });\n\n // ============================================\n // Replace (\"swap this one file\" — see file-input-upload.ts's `replaceFile`)\n // ============================================\n function beginReplace(file: File): void {\n replaceTarget.value = file;\n inputRef.value?.click();\n }\n\n // ============================================\n // Gallery Preview URLs\n // ============================================\n // Object URLs are created lazily (only while `gallery` is enabled) and revoked as soon\n // as their file is no longer selected, plus unconditionally on disconnect — otherwise\n // each preview leaks its backing blob for the life of the page.\n const previewUrls = new Map<File, string>();\n\n function getPreviewUrl(file: File): string {\n let url = previewUrls.get(file);\n\n if (!url) {\n url = URL.createObjectURL(file);\n previewUrls.set(file, url);\n }\n\n return url;\n }\n\n function revokeStalePreviewUrls(activeFiles: File[]): void {\n const keep = new Set(activeFiles);\n\n for (const [file, url] of previewUrls) {\n if (!keep.has(file)) {\n URL.revokeObjectURL(url);\n previewUrls.delete(file);\n }\n }\n }\n\n watch(queue.files, revokeStalePreviewUrls);\n\n onCleanup(() => {\n for (const url of previewUrls.values()) URL.revokeObjectURL(url);\n\n previewUrls.clear();\n });\n\n // ============================================\n // Mount\n // ============================================\n // ============================================\n // Template\n // ============================================\n onElement(inputRef, (inp) => bindRefCallback(props.ref, inp));\n\n onMounted(() => {\n const inp = inputRef.value!;\n const dz = dropzoneRef.value!;\n let skipNextClick = false;\n const pressControl = createInteraction({\n disabled: isDisabled,\n onPress: () => {\n inp.click();\n },\n });\n\n // Native input → add files, or — if a \"Replace\" action opened the picker — swap the one\n // file it targeted instead of appending a new entry.\n onEvent(inp, 'change', (e: Event) => {\n const input = e.target as HTMLInputElement;\n const target = replaceTarget.value;\n\n replaceTarget.value = null;\n\n if (target && input.files?.[0]) queue.replaceFile(target, input.files[0], e);\n else if (input.files?.length) queue.addFiles(Array.from(input.files), e);\n\n input.value = ''; // reset so the same file triggers change again\n });\n // Click dropzone → open file picker\n onEvent(dz, 'click', (e: MouseEvent) => {\n if (e.target === inp) return;\n\n if (skipNextClick) {\n skipNextClick = false;\n\n return;\n }\n\n if (!isDisabled.value) inp.click();\n });\n // Keyboard: Enter / Space → open picker\n onEvent(dz, 'keydown', (e: KeyboardEvent) => {\n skipNextClick = pressControl.handleKeydown(e) && e.key === 'Enter';\n });\n\n // `createDropZone` has no way to update `disabled` after creation — recreate the zone\n // whenever the prop changes instead of capturing a stale snapshot from this one `onMounted`\n // run. `watch`'s own returned cleanup (not a second `onCleanup`) disposes the current zone\n // both on the next toggle and on final teardown.\n watch(\n isDisabled,\n (disabled) => {\n const dropZone = createDropZone({\n disabled,\n element: dz,\n onDrop: (droppedFiles) => queue.addFiles(droppedFiles),\n onHoverChange: (hovered) => {\n isDragging.value = hovered;\n },\n });\n\n return () => dropZone.dispose();\n },\n { immediate: true },\n );\n });\n\n return html`\n <div class=\"file-input-wrapper\" part=\"wrapper\">\n <label class=\"label-outside\" id=\"${labelId}\" part=\"label\" ?hidden=${() => !props.label.value}>\n ${props.label}\n </label>\n <div\n class=\"dropzone\"\n part=\"dropzone\"\n ref=${dropzoneRef}\n role=\"button\"\n tabindex=\"${() => (isDisabled.value ? '-1' : '0')}\"\n aria-disabled=\"${() => String(isDisabled.value)}\"\n aria-label=\"${() => (!props.label.value ? 'File upload drop zone' : null)}\"\n aria-labelledby=\"${() => (props.label.value ? labelId : null)}\"\n aria-describedby=\"${helperId}\">\n <input\n type=\"file\"\n ref=${inputRef}\n part=\"input\"\n id=\"${fileInputId}\"\n accept=\"${props.accept}\"\n ?multiple=\"${props.multiple}\"\n ?required=\"${props.required}\"\n ?disabled=\"${isDisabled}\"\n name=\"${props.name}\"\n hidden\n inert\n tabindex=\"-1\" />\n <div class=\"dropzone-content\">\n <span class=\"dropzone-icon\" aria-hidden=\"true\" data-status=\"${() => (isInvalid.value ? 'error' : null)}\">\n ${() =>\n isInvalid.value\n ? html`\n <ore-icon name=\"alert-circle\" size=\"36\" stroke-width=\"1.5\" aria-hidden=\"true\"></ore-icon>\n `\n : html`\n <ore-icon name=\"upload\" size=\"36\" stroke-width=\"1.5\" aria-hidden=\"true\"></ore-icon>\n `}\n </span>\n <!-- Signal 01: the copy itself shifts on drag-entry (not just border/glow) — a\n static \"Drop files here\" during an active drag reads as if the drop target\n hasn't noticed the file yet. -->\n ${when(\n () => isDragging.value,\n () => html`\n <span class=\"dropzone-title dropzone-title-active\">Release to upload</span>\n `,\n )}\n ${when(\n () => !isDragging.value,\n () => html`\n <span class=\"dropzone-title\">\n Drop files here or\n <u>click to browse</u>\n </span>\n `,\n )}\n <span class=\"dropzone-hint\" ?hidden=${() => !hintText.value}>${hintText}</span>\n </div>\n </div>\n <ul\n class=\"${() => (props.gallery.value ? 'file-grid' : 'file-list')}\"\n part=\"${() => (props.gallery.value ? 'gallery' : null)}\"\n role=\"list\"\n aria-label=\"Selected files\"\n ?hidden=${() => queue.files.value.length === 0}>\n ${() =>\n queue.files.value.map((file: File) =>\n props.gallery.value\n ? html`\n <li class=\"file-card\" data-status=${() => queue.fileState(file).status}>\n <span class=\"file-thumb-frame\">\n ${\n // Decorative: the file name is already announced via the visible\n // `.file-card-name` caption below — repeating it as `alt` text would\n // duplicate it (and often trips redundant-alt checks, since real\n // filenames commonly contain words like \"photo\" or \"image\").\n isImageFile(file)\n ? // Object URLs use the `blob:` scheme, which ore's attribute-level\n // XSS guard blocks unconditionally on `src` (and other\n // URL-accepting attributes) — set it as a DOM property via `ref`\n // instead, bypassing that string-based check. Safe here: the value\n // comes from `URL.createObjectURL(file)` on a real `File` object we\n // control, never from untrusted text.\n html`\n <img\n class=\"file-thumb\"\n alt=\"\"\n ref=\"${(el: HTMLImageElement | null) => {\n if (el) el.src = getPreviewUrl(file);\n }}\" />\n `\n : html`\n <span class=\"file-thumb file-thumb-generic\" aria-hidden=\"true\">\n <ore-icon name=\"file\" size=\"28\" stroke-width=\"1.5\" aria-hidden=\"true\"></ore-icon>\n </span>\n `\n }\n ${when(\n () => queue.fileState(file).status === 'success',\n () => html`\n <span class=\"file-status-badge file-status-badge-success\" aria-hidden=\"true\">\n <ore-icon name=\"check\" size=\"11\" stroke-width=\"3\" aria-hidden=\"true\"></ore-icon>\n </span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <span class=\"file-status-badge file-status-badge-error\" aria-hidden=\"true\">\n <ore-icon name=\"alert-circle\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </span>\n `,\n )}\n <span class=\"file-card-actions\">\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <button\n class=\"file-card-action\"\n type=\"button\"\n aria-label=\"${`Retry uploading ${file.name}`}\"\n @click=${() => queue.retryUpload(file)}>\n <ore-icon name=\"refresh-cw\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n ${when(\n () =>\n queue.fileState(file).status === 'success' &&\n Boolean(props.multiple.value) &&\n Boolean(props.upload.value),\n () => html`\n <button\n class=\"file-card-action\"\n type=\"button\"\n aria-label=\"${`Replace ${file.name}`}\"\n @click=${() => beginReplace(file)}>\n <ore-icon name=\"upload\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n <button\n class=\"file-card-action file-card-remove\"\n type=\"button\"\n aria-label=\"${`Remove ${file.name}`}\"\n @click=${(e: Event) => queue.removeFile(file, e)}>\n <ore-icon name=\"x\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n </span>\n </span>\n <span class=\"file-card-name\" title=\"${file.name}\">${file.name}</span>\n ${when(\n () => queue.fileState(file).status === 'uploading',\n () => html`\n <ore-progress\n type=\"linear\"\n size=\"sm\"\n value=${() => queue.uploadPercent(file)}\n label=${() => `${queue.uploadPercent(file)}%`}></ore-progress>\n <span class=\"file-progress-meta\">${() => queue.uploadMetaText(file)}</span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <span class=\"file-error-text\">${() => queue.fileState(file).error}</span>\n `,\n )}\n </li>\n `\n : html`\n <li class=\"file-item\" data-status=${() => queue.fileState(file).status}>\n <span class=\"file-icon\" aria-hidden=\"true\">\n <ore-icon\n name=${() => queue.statusIconName(file)}\n size=\"18\"\n stroke-width=\"1.75\"\n aria-hidden=\"true\"></ore-icon>\n </span>\n <span class=\"file-meta\">\n <span class=\"file-name\" title=\"${file.name}\">${file.name}</span>\n ${when(\n () => queue.fileState(file).status === 'uploading',\n () => html`\n <ore-progress\n type=\"linear\"\n size=\"sm\"\n value=${() => queue.uploadPercent(file)}\n label=${() => `${queue.uploadPercent(file)}%`}></ore-progress>\n <span class=\"file-progress-meta\">${() => queue.uploadMetaText(file)}</span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <span class=\"file-error-text\">${() => queue.fileState(file).error}</span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'idle' || queue.fileState(file).status === 'success',\n () => html`\n <span class=\"file-size\">${formatBytes(file.size)}</span>\n `,\n )}\n </span>\n <span class=\"file-actions\">\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <button\n class=\"file-action\"\n type=\"button\"\n aria-label=\"${`Retry uploading ${file.name}`}\"\n @click=${() => queue.retryUpload(file)}>\n <ore-icon name=\"refresh-cw\" size=\"14\" stroke-width=\"2\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n ${when(\n () =>\n queue.fileState(file).status === 'success' &&\n Boolean(props.multiple.value) &&\n Boolean(props.upload.value),\n () => html`\n <button\n class=\"file-action\"\n type=\"button\"\n aria-label=\"${`Replace ${file.name}`}\"\n @click=${() => beginReplace(file)}>\n <ore-icon name=\"upload\" size=\"14\" stroke-width=\"2\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n <button\n class=\"file-remove\"\n type=\"button\"\n aria-label=\"${`Remove ${file.name}`}\"\n @click=${(e: Event) => queue.removeFile(file, e)}>\n <ore-icon name=\"x\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n </span>\n </li>\n `,\n )}\n </ul>\n <div class=\"helper-text\" id=\"${helperId}\" part=\"helper\" ?hidden=${() => isInvalid.value || !props.helper.value}>\n ${props.helper}\n </div>\n <div\n class=\"helper-text helper-text-error\"\n id=\"${errorId}\"\n role=\"alert\"\n part=\"error\"\n ?hidden=${() => !isInvalid.value}>\n ${() => props.error.value ?? ''}\n </div>\n </div>\n `;\n },\n shadow: { delegatesFocus: true },\n styles: [\n colorThemeMixin,\n coarsePointerMixin,\n reducedMotionMixin,\n roundedVariantMixin,\n disabledLoadingMixin,\n sizeVariantMixin(FILE_INPUT_SIZE_PRESET),\n forcedColorsFocusMixin('.dropzone'),\n componentStyles,\n ],\n});\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../core/announcer.cjs");let t=require("@vielzeug/ripple"),n=require("@vielzeug/dnd");var r=e=>{if(e===0)return`0 B`;let t=1024,n=[`B`,`KB`,`MB`,`GB`,`TB`],r=Math.floor(Math.log(e)/Math.log(t));return`${parseFloat((e/t**r).toFixed(1))} ${n[r]}`},i=(e,t)=>!t||e.size<=t,a=e=>e>0?`${r(e)}/s`:``,o=e=>e?e.split(`,`).map(e=>e.trim()):[],s={error:null,loaded:0,speedBps:0,status:`idle`},c=e=>{if(e.speedBps<=0||e.status!==`uploading`)return``;let t=Math.max(0,Math.round((e.total-e.loaded)/e.speedBps));return t<60?`${t}s left`:`${Math.round(t/60)}m left`};function l(r){let l=(0,t.signal)([]),u=new Map,d=new Map;function f(e){let n=u.get(e);return n||(n=(0,t.signal)({...s,total:e.size}),u.set(e,n)),n}function p(e){return f(e).value}function m(e,t){let n=f(e);n.value={...n.value,...t}}function h(t,n=0){if(r.disabled.value)return;let i=r.upload.value;if(!i)return;d.get(t)?.abort();let a=new AbortController;d.set(t,a),m(t,{error:null,loaded:n,speedBps:0,status:`uploading`,total:t.size});let o=performance.now(),s=n;i(t,{onProgress:(e,n)=>{if(a.signal.aborted)return;let i=performance.now(),c=(i-o)/1e3,l=c>0?Math.max(0,(e-s)/c):0,u=p(t).speedBps,d=u>0?u*.7+l*.3:l;o=i,s=e,m(t,{loaded:e,speedBps:d,total:n||t.size}),r.onUploadProgress(t,e,n||t.size)},resumeFrom:n,signal:a.signal}).then(()=>{a.signal.aborted||(m(t,{loaded:t.size,speedBps:0,status:`success`}),e.announce(`${t.name} uploaded successfully
|
|
1
|
+
const e=require("../../core/announcer.cjs");let t=require("@vielzeug/ripple"),n=require("@vielzeug/dnd");var r=e=>{if(e===0)return`0 B`;let t=1024,n=[`B`,`KB`,`MB`,`GB`,`TB`],r=Math.floor(Math.log(e)/Math.log(t));return`${parseFloat((e/t**r).toFixed(1))} ${n[r]}`},i=(e,t)=>!t||e.size<=t,a=e=>e>0?`${r(e)}/s`:``,o=e=>e?e.split(`,`).map(e=>e.trim()):[],s={error:null,loaded:0,speedBps:0,status:`idle`},c=e=>{if(e.speedBps<=0||e.status!==`uploading`)return``;let t=Math.max(0,Math.round((e.total-e.loaded)/e.speedBps));return t<60?`${t}s left`:`${Math.round(t/60)}m left`};function l(r){let l=(0,t.signal)([]),u=new Map,d=new Map;function f(e){let n=u.get(e);return n||(n=(0,t.signal)({...s,total:e.size}),u.set(e,n)),n}function p(e){return f(e).value}function m(e,t){let n=f(e);n.value={...n.value,...t}}function h(t,n=0){if(r.disabled.value)return;let i=r.upload.value;if(!i)return;d.get(t)?.abort();let a=new AbortController;d.set(t,a),m(t,{error:null,loaded:n,speedBps:0,status:`uploading`,total:t.size});let o=performance.now(),s=n;i(t,{onProgress:(e,n)=>{if(a.signal.aborted)return;let i=performance.now(),c=(i-o)/1e3,l=c>0?Math.max(0,(e-s)/c):0,u=p(t).speedBps,d=u>0?u*.7+l*.3:l;o=i,s=e,m(t,{loaded:e,speedBps:d,total:n||t.size}),r.onUploadProgress(t,e,n||t.size)},resumeFrom:n,signal:a.signal}).then(()=>{a.signal.aborted||(m(t,{loaded:t.size,speedBps:0,status:`success`}),e.announce(`${t.name} uploaded successfully.`,{document:r.document}),r.onUploadSuccess(t))}).catch(n=>{if(a.signal.aborted)return;let i=n instanceof Error?n.message:String(n);m(t,{error:i,speedBps:0,status:`error`}),e.announce(`${t.name} failed to upload: ${i}`,{document:r.document,politeness:`assertive`}),r.onUploadError(t,n)})}function g(e){r.disabled.value||h(e,p(e).loaded)}function _(e){let t=new Set(e);for(let e of u.keys())t.has(e)||(d.get(e)?.abort(),d.delete(e),u.delete(e))}let v=(0,t.watch)(l,_);function y(e,t){if(r.disabled.value)return;let a=r.multiple.value,s=Array.from(e);a||(s=s.slice(0,1));let c=o(r.accept.value),u=r.maxSize.value;s=s.filter(e=>(0,n.matchesAccept)(e,c)&&i(e,u));let d=a?[...l.value]:[],f=[];for(let e of s)d.includes(e)||(d.push(e),f.push(e));let p=r.maxFiles.value;p>0&&d.length>p&&(d=d.slice(0,p)),l.value=d,r.onChange(l.value,t);for(let e of f)l.value.includes(e)&&h(e)}function b(e,t){r.disabled.value||(l.value=l.value.filter(t=>t!==e),r.onRemove(e,l.value,t),r.onChange(l.value,t))}function x(e,t,a){if(r.disabled.value)return!1;let s=o(r.accept.value);return!(0,n.matchesAccept)(t,s)||!i(t,r.maxSize.value)?!1:(l.value=l.value.map(n=>n===e?t:n),r.onRemove(e,l.value,a),r.onChange(l.value,a),h(t),!0)}function S(e){let t=p(e);return t.total<=0?0:Math.min(100,Math.round(t.loaded/t.total*100))}function C(e){let t=p(e);return[a(t.speedBps),c(t)].filter(Boolean).join(` · `)}function w(e){let t=p(e).status;return t===`success`?`check`:t===`error`?`alert-circle`:`file`}function T(){v.dispose();for(let e of d.values())e.abort();d.clear(),u.clear()}return{addFiles:y,dispose:T,fileState:p,files:l,removeFile:b,replaceFile:x,retryUpload:g,statusIconName:w,uploadMetaText:C,uploadPercent:S}}exports.createFileQueue=l,exports.formatBytes=r;
|
|
2
2
|
//# sourceMappingURL=file-input-upload.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-input-upload.cjs","names":[],"sources":["../../../src/inputs/file-input/file-input-upload.ts"],"sourcesContent":["import { matchesAccept } from '@vielzeug/dnd';\nimport { type Readable, signal, watch } from '@vielzeug/ripple';\n\nimport { announce } from '../../core';\n\n// ── Byte/speed formatting ────────────────────────────────────────────────────\n\nexport const formatBytes = (bytes: number): string => {\n if (bytes === 0) return '0 B';\n\n const k = 1024;\n const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n};\n\nconst isFileSizeAllowed = (file: File, maxSize: number): boolean => !maxSize || file.size <= maxSize;\n\nconst formatSpeed = (bytesPerSecond: number): string => (bytesPerSecond > 0 ? `${formatBytes(bytesPerSecond)}/s` : '');\n\nconst parseAccept = (accept: string | undefined): string[] => (accept ? accept.split(',').map((t) => t.trim()) : []);\n\n// ── Upload lifecycle types ───────────────────────────────────────────────────\n\n/** Upload lifecycle status for a single file. `idle` covers both \"no `upload` handler wired up\"\n * (picker-only mode, fully supported) and \"not started yet\". */\nexport type FileUploadStatus = 'error' | 'idle' | 'success' | 'uploading';\n\n/** Per-file upload snapshot. One reactive signal per file — see `createFileQueue` — not one\n * signal holding a map, so a progress tick for one file never invalidates another's bindings. */\nexport type FileUploadState = {\n error: string | null;\n loaded: number;\n speedBps: number;\n status: FileUploadStatus;\n total: number;\n};\n\nconst IDLE_UPLOAD_STATE: Omit<FileUploadState, 'total'> = { error: null, loaded: 0, speedBps: 0, status: 'idle' };\n\nconst formatEta = (state: FileUploadState): string => {\n if (state.speedBps <= 0 || state.status !== 'uploading') return '';\n\n const remainingSeconds = Math.max(0, Math.round((state.total - state.loaded) / state.speedBps));\n\n if (remainingSeconds < 60) return `${remainingSeconds}s left`;\n\n return `${Math.round(remainingSeconds / 60)}m left`;\n};\n\n/**\n * Consumer-supplied transport — this component has no built-in network layer (zero-dependency,\n * transport-agnostic by design, same reasoning as `courier`'s own fetch-wrapping-only stance).\n * This function is the seam between the UI's upload lifecycle (progress, retry, per-file\n * isolation) and however the app actually moves bytes (`fetch`, `XMLHttpRequest`, a presigned S3\n * PUT, `@vielzeug/courier`, …).\n *\n * `resumeFrom` is a *hint*, not a guarantee: it's the byte offset the component last observed\n * via `onProgress` before the previous attempt failed (0 on a fresh upload). A transport that\n * supports byte ranges (e.g. an HTTP `Range`/`Content-Range` header, S3 multipart, tus) can use\n * it to genuinely resume from that point; a plain `fetch(url, { body: file })` can ignore it and\n * simply re-send the whole file — the UI still shows a resumed-from-N-bytes experience either\n * way, since the file itself was never dropped from memory.\n */\nexport type FileUploadFn = (\n file: File,\n ctx: { onProgress: (loaded: number, total: number) => void; resumeFrom: number; signal: AbortSignal },\n) => Promise<void>;\n\n// ── Queue ─────────────────────────────────────────────────────────────────────\n\nexport type FileQueueOptions = {\n accept: Readable<string | undefined>;\n disabled: Readable<boolean>;\n maxFiles: Readable<number>;\n maxSize: Readable<number>;\n multiple: Readable<boolean>;\n onChange: (files: File[], originalEvent?: Event) => void;\n onRemove: (file: File, files: File[], originalEvent?: Event) => void;\n onUploadError: (file: File, error: unknown) => void;\n onUploadProgress: (file: File, loaded: number, total: number) => void;\n onUploadSuccess: (file: File) => void;\n upload: Readable<FileUploadFn | undefined>;\n};\n\nexport type FileQueue = {\n addFiles(newFiles: File[], originalEvent?: Event): void;\n dispose(): void;\n readonly files: Readable<File[]>;\n fileState(file: File): FileUploadState;\n removeFile(file: File, originalEvent?: Event): void;\n /** Validates `newFile` (accept/max-size) and, if accepted, swaps it in for `oldFile` in place\n * and starts its upload. Returns whether the replacement was accepted. Also fires `onRemove`\n * for `oldFile` — a replace is a removal-and-add that happen to land in the same slot, and a\n * consumer listening only for removal (e.g. to clean up a server-side record) still needs to\n * hear that the old file is gone. */\n replaceFile(oldFile: File, newFile: File, originalEvent?: Event): boolean;\n retryUpload(file: File): void;\n statusIconName(file: File): string;\n uploadMetaText(file: File): string;\n uploadPercent(file: File): number;\n};\n\n/**\n * Owns file selection *and* the opt-in upload lifecycle together — they were never really\n * separable (adding a file starts its upload; removing one must abort it; replacing one is both\n * at once). Independent of `html`/rendering, following the same shape as `datagrid`'s\n * `createDataGridControls` and `combobox`'s `combobox-options.ts`: a factory `file-input.ts`\n * wires into drag/drop/native-input events and the template, directly unit-testable without\n * mounting a component.\n *\n * Every file-level rule (accept, max-size, max-files, disabled) and every upload-lifecycle rule\n * (progress, retry-from-offset, per-file isolation) lives here in one place.\n */\nexport function createFileQueue(options: FileQueueOptions): FileQueue {\n const files = signal<File[]>([]);\n const stateSignals = new Map<File, ReturnType<typeof signal<FileUploadState>>>();\n const controllers = new Map<File, AbortController>();\n\n function stateSignal(file: File): ReturnType<typeof signal<FileUploadState>> {\n let s = stateSignals.get(file);\n\n if (!s) {\n s = signal<FileUploadState>({ ...IDLE_UPLOAD_STATE, total: file.size });\n stateSignals.set(file, s);\n }\n\n return s;\n }\n\n function fileState(file: File): FileUploadState {\n return stateSignal(file).value;\n }\n\n function patchState(file: File, patch: Partial<FileUploadState>): void {\n const s = stateSignal(file);\n\n s.value = { ...s.value, ...patch };\n }\n\n // Each file gets its own `AbortController` and its own promise chain — nothing here `await`s\n // another file's upload, so one file's rejection can never block, freeze, or cancel the others.\n function startUpload(file: File, resumeFrom = 0): void {\n if (options.disabled.value) return;\n\n const uploadFn = options.upload.value;\n\n if (!uploadFn) return;\n\n controllers.get(file)?.abort();\n\n const controller = new AbortController();\n\n controllers.set(file, controller);\n patchState(file, { error: null, loaded: resumeFrom, speedBps: 0, status: 'uploading', total: file.size });\n\n // Rolling (exponential-moving-average) speed estimate rather than a naive\n // `loaded / elapsedSinceStart` average — the latter drags heavily behind the file's actual\n // *current* rate right after a slow start or a mid-transfer stall.\n let lastSampleAt = performance.now();\n let lastSampleLoaded = resumeFrom;\n\n uploadFn(file, {\n onProgress: (loaded, total) => {\n if (controller.signal.aborted) return;\n\n const now = performance.now();\n const elapsedSeconds = (now - lastSampleAt) / 1000;\n const instantBps = elapsedSeconds > 0 ? Math.max(0, (loaded - lastSampleLoaded) / elapsedSeconds) : 0;\n const previousBps = fileState(file).speedBps;\n const speedBps = previousBps > 0 ? previousBps * 0.7 + instantBps * 0.3 : instantBps;\n\n lastSampleAt = now;\n lastSampleLoaded = loaded;\n patchState(file, { loaded, speedBps, total: total || file.size });\n options.onUploadProgress(file, loaded, total || file.size);\n },\n resumeFrom,\n signal: controller.signal,\n })\n .then(() => {\n if (controller.signal.aborted) return;\n\n patchState(file, { loaded: file.size, speedBps: 0, status: 'success' });\n announce(`${file.name} uploaded successfully.`);\n options.onUploadSuccess(file);\n })\n .catch((error: unknown) => {\n if (controller.signal.aborted) return;\n\n const message = error instanceof Error ? error.message : String(error);\n\n patchState(file, { error: message, speedBps: 0, status: 'error' });\n announce(`${file.name} failed to upload: ${message}`, { politeness: 'assertive' });\n options.onUploadError(file, error);\n });\n }\n\n // Resumes from the last byte offset this file actually reached, not from 0 — see\n // `FileUploadFn`'s doc comment on what \"resume\" honestly means when the transport is\n // consumer-supplied.\n function retryUpload(file: File): void {\n if (options.disabled.value) return;\n\n startUpload(file, fileState(file).loaded);\n }\n\n // Aborts + drops upload state for any file no longer in `files` — covers plain removal, a\n // non-`multiple` selection replacing the previous file, and `replaceFile()` swapping one File\n // for another. One cleanup path instead of duplicating it at every call site that can shrink\n // `files`.\n function dropOrphanedState(activeFiles: File[]): void {\n const keep = new Set(activeFiles);\n\n for (const file of stateSignals.keys()) {\n if (keep.has(file)) continue;\n\n controllers.get(file)?.abort();\n controllers.delete(file);\n stateSignals.delete(file);\n }\n }\n\n const filesWatch = watch(files, dropOrphanedState);\n\n function addFiles(newFiles: File[], originalEvent?: Event): void {\n if (options.disabled.value) return;\n\n const isMultiple = options.multiple.value;\n let incoming = Array.from(newFiles);\n\n if (!isMultiple) incoming = incoming.slice(0, 1);\n\n const acceptList = parseAccept(options.accept.value);\n const maxSize = options.maxSize.value;\n\n incoming = incoming.filter((f) => matchesAccept(f, acceptList) && isFileSizeAllowed(f, maxSize));\n\n let updated: File[] = isMultiple ? [...files.value] : [];\n const added: File[] = [];\n\n for (const f of incoming) {\n if (!updated.includes(f)) {\n updated.push(f);\n added.push(f);\n }\n }\n\n const maxFiles = options.maxFiles.value;\n\n if (maxFiles > 0 && updated.length > maxFiles) updated = updated.slice(0, maxFiles);\n\n files.value = updated;\n options.onChange(files.value, originalEvent);\n\n for (const f of added) {\n if (files.value.includes(f)) startUpload(f);\n }\n }\n\n function removeFile(file: File, originalEvent?: Event): void {\n if (options.disabled.value) return;\n\n files.value = files.value.filter((f) => f !== file);\n options.onRemove(file, files.value, originalEvent);\n options.onChange(files.value, originalEvent);\n }\n\n function replaceFile(oldFile: File, newFile: File, originalEvent?: Event): boolean {\n if (options.disabled.value) return false;\n\n const acceptList = parseAccept(options.accept.value);\n\n if (!matchesAccept(newFile, acceptList) || !isFileSizeAllowed(newFile, options.maxSize.value)) return false;\n\n files.value = files.value.map((f) => (f === oldFile ? newFile : f));\n options.onRemove(oldFile, files.value, originalEvent);\n options.onChange(files.value, originalEvent);\n startUpload(newFile);\n\n return true;\n }\n\n function uploadPercent(file: File): number {\n const state = fileState(file);\n\n if (state.total <= 0) return 0;\n\n return Math.min(100, Math.round((state.loaded / state.total) * 100));\n }\n\n // Speed + ETA on one line — percent itself is rendered separately (e.g. `<ore-progress>`'s\n // own `label`), so this only needs the two metrics that wouldn't otherwise fit there.\n function uploadMetaText(file: File): string {\n const state = fileState(file);\n\n return [formatSpeed(state.speedBps), formatEta(state)].filter(Boolean).join(' · ');\n }\n\n function statusIconName(file: File): string {\n const status = fileState(file).status;\n\n if (status === 'success') return 'check';\n\n if (status === 'error') return 'alert-circle';\n\n return 'file';\n }\n\n function dispose(): void {\n filesWatch.dispose();\n\n for (const controller of controllers.values()) controller.abort();\n\n controllers.clear();\n stateSignals.clear();\n }\n\n return {\n addFiles,\n dispose,\n fileState,\n files,\n removeFile,\n replaceFile,\n retryUpload,\n statusIconName,\n uploadMetaText,\n uploadPercent,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-input-upload.cjs","names":[],"sources":["../../../src/inputs/file-input/file-input-upload.ts"],"sourcesContent":["import { matchesAccept } from '@vielzeug/dnd';\nimport { type Readable, signal, watch } from '@vielzeug/ripple';\n\nimport { announce } from '../../core';\n\n// ── Byte/speed formatting ────────────────────────────────────────────────────\n\nexport const formatBytes = (bytes: number): string => {\n if (bytes === 0) return '0 B';\n\n const k = 1024;\n const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n};\n\nconst isFileSizeAllowed = (file: File, maxSize: number): boolean => !maxSize || file.size <= maxSize;\n\nconst formatSpeed = (bytesPerSecond: number): string => (bytesPerSecond > 0 ? `${formatBytes(bytesPerSecond)}/s` : '');\n\nconst parseAccept = (accept: string | undefined): string[] => (accept ? accept.split(',').map((t) => t.trim()) : []);\n\n// ── Upload lifecycle types ───────────────────────────────────────────────────\n\n/** Upload lifecycle status for a single file. `idle` covers both \"no `upload` handler wired up\"\n * (picker-only mode, fully supported) and \"not started yet\". */\nexport type FileUploadStatus = 'error' | 'idle' | 'success' | 'uploading';\n\n/** Per-file upload snapshot. One reactive signal per file — see `createFileQueue` — not one\n * signal holding a map, so a progress tick for one file never invalidates another's bindings. */\nexport type FileUploadState = {\n error: string | null;\n loaded: number;\n speedBps: number;\n status: FileUploadStatus;\n total: number;\n};\n\nconst IDLE_UPLOAD_STATE: Omit<FileUploadState, 'total'> = { error: null, loaded: 0, speedBps: 0, status: 'idle' };\n\nconst formatEta = (state: FileUploadState): string => {\n if (state.speedBps <= 0 || state.status !== 'uploading') return '';\n\n const remainingSeconds = Math.max(0, Math.round((state.total - state.loaded) / state.speedBps));\n\n if (remainingSeconds < 60) return `${remainingSeconds}s left`;\n\n return `${Math.round(remainingSeconds / 60)}m left`;\n};\n\n/**\n * Consumer-supplied transport — this component has no built-in network layer (zero-dependency,\n * transport-agnostic by design, same reasoning as `courier`'s own fetch-wrapping-only stance).\n * This function is the seam between the UI's upload lifecycle (progress, retry, per-file\n * isolation) and however the app actually moves bytes (`fetch`, `XMLHttpRequest`, a presigned S3\n * PUT, `@vielzeug/courier`, …).\n *\n * `resumeFrom` is a *hint*, not a guarantee: it's the byte offset the component last observed\n * via `onProgress` before the previous attempt failed (0 on a fresh upload). A transport that\n * supports byte ranges (e.g. an HTTP `Range`/`Content-Range` header, S3 multipart, tus) can use\n * it to genuinely resume from that point; a plain `fetch(url, { body: file })` can ignore it and\n * simply re-send the whole file — the UI still shows a resumed-from-N-bytes experience either\n * way, since the file itself was never dropped from memory.\n */\nexport type FileUploadFn = (\n file: File,\n ctx: { onProgress: (loaded: number, total: number) => void; resumeFrom: number; signal: AbortSignal },\n) => Promise<void>;\n\n// ── Queue ─────────────────────────────────────────────────────────────────────\n\nexport type FileQueueOptions = {\n accept: Readable<string | undefined>;\n disabled: Readable<boolean>;\n /** Document whose `<body>` hosts the announcer live region. Defaults to the global `document`. */\n document?: Document;\n maxFiles: Readable<number>;\n maxSize: Readable<number>;\n multiple: Readable<boolean>;\n onChange: (files: File[], originalEvent?: Event) => void;\n onRemove: (file: File, files: File[], originalEvent?: Event) => void;\n onUploadError: (file: File, error: unknown) => void;\n onUploadProgress: (file: File, loaded: number, total: number) => void;\n onUploadSuccess: (file: File) => void;\n upload: Readable<FileUploadFn | undefined>;\n};\n\nexport type FileQueue = {\n addFiles(newFiles: File[], originalEvent?: Event): void;\n dispose(): void;\n readonly files: Readable<File[]>;\n fileState(file: File): FileUploadState;\n removeFile(file: File, originalEvent?: Event): void;\n /** Validates `newFile` (accept/max-size) and, if accepted, swaps it in for `oldFile` in place\n * and starts its upload. Returns whether the replacement was accepted. Also fires `onRemove`\n * for `oldFile` — a replace is a removal-and-add that happen to land in the same slot, and a\n * consumer listening only for removal (e.g. to clean up a server-side record) still needs to\n * hear that the old file is gone. */\n replaceFile(oldFile: File, newFile: File, originalEvent?: Event): boolean;\n retryUpload(file: File): void;\n statusIconName(file: File): string;\n uploadMetaText(file: File): string;\n uploadPercent(file: File): number;\n};\n\n/**\n * Owns file selection *and* the opt-in upload lifecycle together — they were never really\n * separable (adding a file starts its upload; removing one must abort it; replacing one is both\n * at once). Independent of `html`/rendering, following the same shape as `datagrid`'s\n * `createDataGridControls` and `combobox`'s `combobox-options.ts`: a factory `file-input.ts`\n * wires into drag/drop/native-input events and the template, directly unit-testable without\n * mounting a component.\n *\n * Every file-level rule (accept, max-size, max-files, disabled) and every upload-lifecycle rule\n * (progress, retry-from-offset, per-file isolation) lives here in one place.\n */\nexport function createFileQueue(options: FileQueueOptions): FileQueue {\n const files = signal<File[]>([]);\n const stateSignals = new Map<File, ReturnType<typeof signal<FileUploadState>>>();\n const controllers = new Map<File, AbortController>();\n\n function stateSignal(file: File): ReturnType<typeof signal<FileUploadState>> {\n let s = stateSignals.get(file);\n\n if (!s) {\n s = signal<FileUploadState>({ ...IDLE_UPLOAD_STATE, total: file.size });\n stateSignals.set(file, s);\n }\n\n return s;\n }\n\n function fileState(file: File): FileUploadState {\n return stateSignal(file).value;\n }\n\n function patchState(file: File, patch: Partial<FileUploadState>): void {\n const s = stateSignal(file);\n\n s.value = { ...s.value, ...patch };\n }\n\n // Each file gets its own `AbortController` and its own promise chain — nothing here `await`s\n // another file's upload, so one file's rejection can never block, freeze, or cancel the others.\n function startUpload(file: File, resumeFrom = 0): void {\n if (options.disabled.value) return;\n\n const uploadFn = options.upload.value;\n\n if (!uploadFn) return;\n\n controllers.get(file)?.abort();\n\n const controller = new AbortController();\n\n controllers.set(file, controller);\n patchState(file, { error: null, loaded: resumeFrom, speedBps: 0, status: 'uploading', total: file.size });\n\n // Rolling (exponential-moving-average) speed estimate rather than a naive\n // `loaded / elapsedSinceStart` average — the latter drags heavily behind the file's actual\n // *current* rate right after a slow start or a mid-transfer stall.\n let lastSampleAt = performance.now();\n let lastSampleLoaded = resumeFrom;\n\n uploadFn(file, {\n onProgress: (loaded, total) => {\n if (controller.signal.aborted) return;\n\n const now = performance.now();\n const elapsedSeconds = (now - lastSampleAt) / 1000;\n const instantBps = elapsedSeconds > 0 ? Math.max(0, (loaded - lastSampleLoaded) / elapsedSeconds) : 0;\n const previousBps = fileState(file).speedBps;\n const speedBps = previousBps > 0 ? previousBps * 0.7 + instantBps * 0.3 : instantBps;\n\n lastSampleAt = now;\n lastSampleLoaded = loaded;\n patchState(file, { loaded, speedBps, total: total || file.size });\n options.onUploadProgress(file, loaded, total || file.size);\n },\n resumeFrom,\n signal: controller.signal,\n })\n .then(() => {\n if (controller.signal.aborted) return;\n\n patchState(file, { loaded: file.size, speedBps: 0, status: 'success' });\n announce(`${file.name} uploaded successfully.`, { document: options.document });\n options.onUploadSuccess(file);\n })\n .catch((error: unknown) => {\n if (controller.signal.aborted) return;\n\n const message = error instanceof Error ? error.message : String(error);\n\n patchState(file, { error: message, speedBps: 0, status: 'error' });\n announce(`${file.name} failed to upload: ${message}`, {\n document: options.document,\n politeness: 'assertive',\n });\n options.onUploadError(file, error);\n });\n }\n\n // Resumes from the last byte offset this file actually reached, not from 0 — see\n // `FileUploadFn`'s doc comment on what \"resume\" honestly means when the transport is\n // consumer-supplied.\n function retryUpload(file: File): void {\n if (options.disabled.value) return;\n\n startUpload(file, fileState(file).loaded);\n }\n\n // Aborts + drops upload state for any file no longer in `files` — covers plain removal, a\n // non-`multiple` selection replacing the previous file, and `replaceFile()` swapping one File\n // for another. One cleanup path instead of duplicating it at every call site that can shrink\n // `files`.\n function dropOrphanedState(activeFiles: File[]): void {\n const keep = new Set(activeFiles);\n\n for (const file of stateSignals.keys()) {\n if (keep.has(file)) continue;\n\n controllers.get(file)?.abort();\n controllers.delete(file);\n stateSignals.delete(file);\n }\n }\n\n const filesWatch = watch(files, dropOrphanedState);\n\n function addFiles(newFiles: File[], originalEvent?: Event): void {\n if (options.disabled.value) return;\n\n const isMultiple = options.multiple.value;\n let incoming = Array.from(newFiles);\n\n if (!isMultiple) incoming = incoming.slice(0, 1);\n\n const acceptList = parseAccept(options.accept.value);\n const maxSize = options.maxSize.value;\n\n incoming = incoming.filter((f) => matchesAccept(f, acceptList) && isFileSizeAllowed(f, maxSize));\n\n let updated: File[] = isMultiple ? [...files.value] : [];\n const added: File[] = [];\n\n for (const f of incoming) {\n if (!updated.includes(f)) {\n updated.push(f);\n added.push(f);\n }\n }\n\n const maxFiles = options.maxFiles.value;\n\n if (maxFiles > 0 && updated.length > maxFiles) updated = updated.slice(0, maxFiles);\n\n files.value = updated;\n options.onChange(files.value, originalEvent);\n\n for (const f of added) {\n if (files.value.includes(f)) startUpload(f);\n }\n }\n\n function removeFile(file: File, originalEvent?: Event): void {\n if (options.disabled.value) return;\n\n files.value = files.value.filter((f) => f !== file);\n options.onRemove(file, files.value, originalEvent);\n options.onChange(files.value, originalEvent);\n }\n\n function replaceFile(oldFile: File, newFile: File, originalEvent?: Event): boolean {\n if (options.disabled.value) return false;\n\n const acceptList = parseAccept(options.accept.value);\n\n if (!matchesAccept(newFile, acceptList) || !isFileSizeAllowed(newFile, options.maxSize.value)) return false;\n\n files.value = files.value.map((f) => (f === oldFile ? newFile : f));\n options.onRemove(oldFile, files.value, originalEvent);\n options.onChange(files.value, originalEvent);\n startUpload(newFile);\n\n return true;\n }\n\n function uploadPercent(file: File): number {\n const state = fileState(file);\n\n if (state.total <= 0) return 0;\n\n return Math.min(100, Math.round((state.loaded / state.total) * 100));\n }\n\n // Speed + ETA on one line — percent itself is rendered separately (e.g. `<ore-progress>`'s\n // own `label`), so this only needs the two metrics that wouldn't otherwise fit there.\n function uploadMetaText(file: File): string {\n const state = fileState(file);\n\n return [formatSpeed(state.speedBps), formatEta(state)].filter(Boolean).join(' · ');\n }\n\n function statusIconName(file: File): string {\n const status = fileState(file).status;\n\n if (status === 'success') return 'check';\n\n if (status === 'error') return 'alert-circle';\n\n return 'file';\n }\n\n function dispose(): void {\n filesWatch.dispose();\n\n for (const controller of controllers.values()) controller.abort();\n\n controllers.clear();\n stateSignals.clear();\n }\n\n return {\n addFiles,\n dispose,\n fileState,\n files,\n removeFile,\n replaceFile,\n retryUpload,\n statusIconName,\n uploadMetaText,\n uploadPercent,\n };\n}\n"],"mappings":"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"}
|
|
@@ -34,6 +34,8 @@ export type FileUploadFn = (file: File, ctx: {
|
|
|
34
34
|
export type FileQueueOptions = {
|
|
35
35
|
accept: Readable<string | undefined>;
|
|
36
36
|
disabled: Readable<boolean>;
|
|
37
|
+
/** Document whose `<body>` hosts the announcer live region. Defaults to the global `document`. */
|
|
38
|
+
document?: Document;
|
|
37
39
|
maxFiles: Readable<number>;
|
|
38
40
|
maxSize: Readable<number>;
|
|
39
41
|
multiple: Readable<boolean>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-input-upload.d.ts","sourceRoot":"","sources":["../../../src/inputs/file-input/file-input-upload.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,QAAQ,EAAiB,MAAM,kBAAkB,CAAC;AAMhE,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,KAAG,MAQ3C,CAAC;AAUF;gEACgE;AAChE,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC;AAE1E;iGACiG;AACjG,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAcF;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,YAAY,GAAG,CACzB,IAAI,EAAE,IAAI,EACV,GAAG,EAAE;IAAE,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,KAClG,OAAO,CAAC,IAAI,CAAC,CAAC;AAInB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACrC,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC5B,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC3B,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC1B,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC5B,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,aAAa,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;IACzD,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,aAAa,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;IACrE,aAAa,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACpD,gBAAgB,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtE,eAAe,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACtC,MAAM,EAAE,QAAQ,CAAC,YAAY,GAAG,SAAS,CAAC,CAAC;CAC5C,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,aAAa,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IACxD,OAAO,IAAI,IAAI,CAAC;IAChB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjC,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,eAAe,CAAC;IACvC,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,aAAa,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IACpD;;;;yCAIqC;IACrC,WAAW,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC;IAC1E,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAC9B,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAAC;IACnC,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAAC;IACnC,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAAC;CACnC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"file-input-upload.d.ts","sourceRoot":"","sources":["../../../src/inputs/file-input/file-input-upload.ts"],"names":[],"mappings":"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"}
|
|
@@ -72,7 +72,7 @@ function u(i) {
|
|
|
72
72
|
loaded: t.size,
|
|
73
73
|
speedBps: 0,
|
|
74
74
|
status: "success"
|
|
75
|
-
}), e(`${t.name} uploaded successfully
|
|
75
|
+
}), e(`${t.name} uploaded successfully.`, { document: i.document }), i.onUploadSuccess(t));
|
|
76
76
|
}).catch((n) => {
|
|
77
77
|
if (a.signal.aborted) return;
|
|
78
78
|
let r = n instanceof Error ? n.message : String(n);
|
|
@@ -80,7 +80,10 @@ function u(i) {
|
|
|
80
80
|
error: r,
|
|
81
81
|
speedBps: 0,
|
|
82
82
|
status: "error"
|
|
83
|
-
}), e(`${t.name} failed to upload: ${r}`, {
|
|
83
|
+
}), e(`${t.name} failed to upload: ${r}`, {
|
|
84
|
+
document: i.document,
|
|
85
|
+
politeness: "assertive"
|
|
86
|
+
}), i.onUploadError(t, n);
|
|
84
87
|
});
|
|
85
88
|
}
|
|
86
89
|
function _(e) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-input-upload.js","names":[],"sources":["../../../src/inputs/file-input/file-input-upload.ts"],"sourcesContent":["import { matchesAccept } from '@vielzeug/dnd';\nimport { type Readable, signal, watch } from '@vielzeug/ripple';\n\nimport { announce } from '../../core';\n\n// ── Byte/speed formatting ────────────────────────────────────────────────────\n\nexport const formatBytes = (bytes: number): string => {\n if (bytes === 0) return '0 B';\n\n const k = 1024;\n const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n};\n\nconst isFileSizeAllowed = (file: File, maxSize: number): boolean => !maxSize || file.size <= maxSize;\n\nconst formatSpeed = (bytesPerSecond: number): string => (bytesPerSecond > 0 ? `${formatBytes(bytesPerSecond)}/s` : '');\n\nconst parseAccept = (accept: string | undefined): string[] => (accept ? accept.split(',').map((t) => t.trim()) : []);\n\n// ── Upload lifecycle types ───────────────────────────────────────────────────\n\n/** Upload lifecycle status for a single file. `idle` covers both \"no `upload` handler wired up\"\n * (picker-only mode, fully supported) and \"not started yet\". */\nexport type FileUploadStatus = 'error' | 'idle' | 'success' | 'uploading';\n\n/** Per-file upload snapshot. One reactive signal per file — see `createFileQueue` — not one\n * signal holding a map, so a progress tick for one file never invalidates another's bindings. */\nexport type FileUploadState = {\n error: string | null;\n loaded: number;\n speedBps: number;\n status: FileUploadStatus;\n total: number;\n};\n\nconst IDLE_UPLOAD_STATE: Omit<FileUploadState, 'total'> = { error: null, loaded: 0, speedBps: 0, status: 'idle' };\n\nconst formatEta = (state: FileUploadState): string => {\n if (state.speedBps <= 0 || state.status !== 'uploading') return '';\n\n const remainingSeconds = Math.max(0, Math.round((state.total - state.loaded) / state.speedBps));\n\n if (remainingSeconds < 60) return `${remainingSeconds}s left`;\n\n return `${Math.round(remainingSeconds / 60)}m left`;\n};\n\n/**\n * Consumer-supplied transport — this component has no built-in network layer (zero-dependency,\n * transport-agnostic by design, same reasoning as `courier`'s own fetch-wrapping-only stance).\n * This function is the seam between the UI's upload lifecycle (progress, retry, per-file\n * isolation) and however the app actually moves bytes (`fetch`, `XMLHttpRequest`, a presigned S3\n * PUT, `@vielzeug/courier`, …).\n *\n * `resumeFrom` is a *hint*, not a guarantee: it's the byte offset the component last observed\n * via `onProgress` before the previous attempt failed (0 on a fresh upload). A transport that\n * supports byte ranges (e.g. an HTTP `Range`/`Content-Range` header, S3 multipart, tus) can use\n * it to genuinely resume from that point; a plain `fetch(url, { body: file })` can ignore it and\n * simply re-send the whole file — the UI still shows a resumed-from-N-bytes experience either\n * way, since the file itself was never dropped from memory.\n */\nexport type FileUploadFn = (\n file: File,\n ctx: { onProgress: (loaded: number, total: number) => void; resumeFrom: number; signal: AbortSignal },\n) => Promise<void>;\n\n// ── Queue ─────────────────────────────────────────────────────────────────────\n\nexport type FileQueueOptions = {\n accept: Readable<string | undefined>;\n disabled: Readable<boolean>;\n maxFiles: Readable<number>;\n maxSize: Readable<number>;\n multiple: Readable<boolean>;\n onChange: (files: File[], originalEvent?: Event) => void;\n onRemove: (file: File, files: File[], originalEvent?: Event) => void;\n onUploadError: (file: File, error: unknown) => void;\n onUploadProgress: (file: File, loaded: number, total: number) => void;\n onUploadSuccess: (file: File) => void;\n upload: Readable<FileUploadFn | undefined>;\n};\n\nexport type FileQueue = {\n addFiles(newFiles: File[], originalEvent?: Event): void;\n dispose(): void;\n readonly files: Readable<File[]>;\n fileState(file: File): FileUploadState;\n removeFile(file: File, originalEvent?: Event): void;\n /** Validates `newFile` (accept/max-size) and, if accepted, swaps it in for `oldFile` in place\n * and starts its upload. Returns whether the replacement was accepted. Also fires `onRemove`\n * for `oldFile` — a replace is a removal-and-add that happen to land in the same slot, and a\n * consumer listening only for removal (e.g. to clean up a server-side record) still needs to\n * hear that the old file is gone. */\n replaceFile(oldFile: File, newFile: File, originalEvent?: Event): boolean;\n retryUpload(file: File): void;\n statusIconName(file: File): string;\n uploadMetaText(file: File): string;\n uploadPercent(file: File): number;\n};\n\n/**\n * Owns file selection *and* the opt-in upload lifecycle together — they were never really\n * separable (adding a file starts its upload; removing one must abort it; replacing one is both\n * at once). Independent of `html`/rendering, following the same shape as `datagrid`'s\n * `createDataGridControls` and `combobox`'s `combobox-options.ts`: a factory `file-input.ts`\n * wires into drag/drop/native-input events and the template, directly unit-testable without\n * mounting a component.\n *\n * Every file-level rule (accept, max-size, max-files, disabled) and every upload-lifecycle rule\n * (progress, retry-from-offset, per-file isolation) lives here in one place.\n */\nexport function createFileQueue(options: FileQueueOptions): FileQueue {\n const files = signal<File[]>([]);\n const stateSignals = new Map<File, ReturnType<typeof signal<FileUploadState>>>();\n const controllers = new Map<File, AbortController>();\n\n function stateSignal(file: File): ReturnType<typeof signal<FileUploadState>> {\n let s = stateSignals.get(file);\n\n if (!s) {\n s = signal<FileUploadState>({ ...IDLE_UPLOAD_STATE, total: file.size });\n stateSignals.set(file, s);\n }\n\n return s;\n }\n\n function fileState(file: File): FileUploadState {\n return stateSignal(file).value;\n }\n\n function patchState(file: File, patch: Partial<FileUploadState>): void {\n const s = stateSignal(file);\n\n s.value = { ...s.value, ...patch };\n }\n\n // Each file gets its own `AbortController` and its own promise chain — nothing here `await`s\n // another file's upload, so one file's rejection can never block, freeze, or cancel the others.\n function startUpload(file: File, resumeFrom = 0): void {\n if (options.disabled.value) return;\n\n const uploadFn = options.upload.value;\n\n if (!uploadFn) return;\n\n controllers.get(file)?.abort();\n\n const controller = new AbortController();\n\n controllers.set(file, controller);\n patchState(file, { error: null, loaded: resumeFrom, speedBps: 0, status: 'uploading', total: file.size });\n\n // Rolling (exponential-moving-average) speed estimate rather than a naive\n // `loaded / elapsedSinceStart` average — the latter drags heavily behind the file's actual\n // *current* rate right after a slow start or a mid-transfer stall.\n let lastSampleAt = performance.now();\n let lastSampleLoaded = resumeFrom;\n\n uploadFn(file, {\n onProgress: (loaded, total) => {\n if (controller.signal.aborted) return;\n\n const now = performance.now();\n const elapsedSeconds = (now - lastSampleAt) / 1000;\n const instantBps = elapsedSeconds > 0 ? Math.max(0, (loaded - lastSampleLoaded) / elapsedSeconds) : 0;\n const previousBps = fileState(file).speedBps;\n const speedBps = previousBps > 0 ? previousBps * 0.7 + instantBps * 0.3 : instantBps;\n\n lastSampleAt = now;\n lastSampleLoaded = loaded;\n patchState(file, { loaded, speedBps, total: total || file.size });\n options.onUploadProgress(file, loaded, total || file.size);\n },\n resumeFrom,\n signal: controller.signal,\n })\n .then(() => {\n if (controller.signal.aborted) return;\n\n patchState(file, { loaded: file.size, speedBps: 0, status: 'success' });\n announce(`${file.name} uploaded successfully.`);\n options.onUploadSuccess(file);\n })\n .catch((error: unknown) => {\n if (controller.signal.aborted) return;\n\n const message = error instanceof Error ? error.message : String(error);\n\n patchState(file, { error: message, speedBps: 0, status: 'error' });\n announce(`${file.name} failed to upload: ${message}`, { politeness: 'assertive' });\n options.onUploadError(file, error);\n });\n }\n\n // Resumes from the last byte offset this file actually reached, not from 0 — see\n // `FileUploadFn`'s doc comment on what \"resume\" honestly means when the transport is\n // consumer-supplied.\n function retryUpload(file: File): void {\n if (options.disabled.value) return;\n\n startUpload(file, fileState(file).loaded);\n }\n\n // Aborts + drops upload state for any file no longer in `files` — covers plain removal, a\n // non-`multiple` selection replacing the previous file, and `replaceFile()` swapping one File\n // for another. One cleanup path instead of duplicating it at every call site that can shrink\n // `files`.\n function dropOrphanedState(activeFiles: File[]): void {\n const keep = new Set(activeFiles);\n\n for (const file of stateSignals.keys()) {\n if (keep.has(file)) continue;\n\n controllers.get(file)?.abort();\n controllers.delete(file);\n stateSignals.delete(file);\n }\n }\n\n const filesWatch = watch(files, dropOrphanedState);\n\n function addFiles(newFiles: File[], originalEvent?: Event): void {\n if (options.disabled.value) return;\n\n const isMultiple = options.multiple.value;\n let incoming = Array.from(newFiles);\n\n if (!isMultiple) incoming = incoming.slice(0, 1);\n\n const acceptList = parseAccept(options.accept.value);\n const maxSize = options.maxSize.value;\n\n incoming = incoming.filter((f) => matchesAccept(f, acceptList) && isFileSizeAllowed(f, maxSize));\n\n let updated: File[] = isMultiple ? [...files.value] : [];\n const added: File[] = [];\n\n for (const f of incoming) {\n if (!updated.includes(f)) {\n updated.push(f);\n added.push(f);\n }\n }\n\n const maxFiles = options.maxFiles.value;\n\n if (maxFiles > 0 && updated.length > maxFiles) updated = updated.slice(0, maxFiles);\n\n files.value = updated;\n options.onChange(files.value, originalEvent);\n\n for (const f of added) {\n if (files.value.includes(f)) startUpload(f);\n }\n }\n\n function removeFile(file: File, originalEvent?: Event): void {\n if (options.disabled.value) return;\n\n files.value = files.value.filter((f) => f !== file);\n options.onRemove(file, files.value, originalEvent);\n options.onChange(files.value, originalEvent);\n }\n\n function replaceFile(oldFile: File, newFile: File, originalEvent?: Event): boolean {\n if (options.disabled.value) return false;\n\n const acceptList = parseAccept(options.accept.value);\n\n if (!matchesAccept(newFile, acceptList) || !isFileSizeAllowed(newFile, options.maxSize.value)) return false;\n\n files.value = files.value.map((f) => (f === oldFile ? newFile : f));\n options.onRemove(oldFile, files.value, originalEvent);\n options.onChange(files.value, originalEvent);\n startUpload(newFile);\n\n return true;\n }\n\n function uploadPercent(file: File): number {\n const state = fileState(file);\n\n if (state.total <= 0) return 0;\n\n return Math.min(100, Math.round((state.loaded / state.total) * 100));\n }\n\n // Speed + ETA on one line — percent itself is rendered separately (e.g. `<ore-progress>`'s\n // own `label`), so this only needs the two metrics that wouldn't otherwise fit there.\n function uploadMetaText(file: File): string {\n const state = fileState(file);\n\n return [formatSpeed(state.speedBps), formatEta(state)].filter(Boolean).join(' · ');\n }\n\n function statusIconName(file: File): string {\n const status = fileState(file).status;\n\n if (status === 'success') return 'check';\n\n if (status === 'error') return 'alert-circle';\n\n return 'file';\n }\n\n function dispose(): void {\n filesWatch.dispose();\n\n for (const controller of controllers.values()) controller.abort();\n\n controllers.clear();\n stateSignals.clear();\n }\n\n return {\n addFiles,\n dispose,\n fileState,\n files,\n removeFile,\n replaceFile,\n retryUpload,\n statusIconName,\n uploadMetaText,\n uploadPercent,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-input-upload.js","names":[],"sources":["../../../src/inputs/file-input/file-input-upload.ts"],"sourcesContent":["import { matchesAccept } from '@vielzeug/dnd';\nimport { type Readable, signal, watch } from '@vielzeug/ripple';\n\nimport { announce } from '../../core';\n\n// ── Byte/speed formatting ────────────────────────────────────────────────────\n\nexport const formatBytes = (bytes: number): string => {\n if (bytes === 0) return '0 B';\n\n const k = 1024;\n const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n};\n\nconst isFileSizeAllowed = (file: File, maxSize: number): boolean => !maxSize || file.size <= maxSize;\n\nconst formatSpeed = (bytesPerSecond: number): string => (bytesPerSecond > 0 ? `${formatBytes(bytesPerSecond)}/s` : '');\n\nconst parseAccept = (accept: string | undefined): string[] => (accept ? accept.split(',').map((t) => t.trim()) : []);\n\n// ── Upload lifecycle types ───────────────────────────────────────────────────\n\n/** Upload lifecycle status for a single file. `idle` covers both \"no `upload` handler wired up\"\n * (picker-only mode, fully supported) and \"not started yet\". */\nexport type FileUploadStatus = 'error' | 'idle' | 'success' | 'uploading';\n\n/** Per-file upload snapshot. One reactive signal per file — see `createFileQueue` — not one\n * signal holding a map, so a progress tick for one file never invalidates another's bindings. */\nexport type FileUploadState = {\n error: string | null;\n loaded: number;\n speedBps: number;\n status: FileUploadStatus;\n total: number;\n};\n\nconst IDLE_UPLOAD_STATE: Omit<FileUploadState, 'total'> = { error: null, loaded: 0, speedBps: 0, status: 'idle' };\n\nconst formatEta = (state: FileUploadState): string => {\n if (state.speedBps <= 0 || state.status !== 'uploading') return '';\n\n const remainingSeconds = Math.max(0, Math.round((state.total - state.loaded) / state.speedBps));\n\n if (remainingSeconds < 60) return `${remainingSeconds}s left`;\n\n return `${Math.round(remainingSeconds / 60)}m left`;\n};\n\n/**\n * Consumer-supplied transport — this component has no built-in network layer (zero-dependency,\n * transport-agnostic by design, same reasoning as `courier`'s own fetch-wrapping-only stance).\n * This function is the seam between the UI's upload lifecycle (progress, retry, per-file\n * isolation) and however the app actually moves bytes (`fetch`, `XMLHttpRequest`, a presigned S3\n * PUT, `@vielzeug/courier`, …).\n *\n * `resumeFrom` is a *hint*, not a guarantee: it's the byte offset the component last observed\n * via `onProgress` before the previous attempt failed (0 on a fresh upload). A transport that\n * supports byte ranges (e.g. an HTTP `Range`/`Content-Range` header, S3 multipart, tus) can use\n * it to genuinely resume from that point; a plain `fetch(url, { body: file })` can ignore it and\n * simply re-send the whole file — the UI still shows a resumed-from-N-bytes experience either\n * way, since the file itself was never dropped from memory.\n */\nexport type FileUploadFn = (\n file: File,\n ctx: { onProgress: (loaded: number, total: number) => void; resumeFrom: number; signal: AbortSignal },\n) => Promise<void>;\n\n// ── Queue ─────────────────────────────────────────────────────────────────────\n\nexport type FileQueueOptions = {\n accept: Readable<string | undefined>;\n disabled: Readable<boolean>;\n /** Document whose `<body>` hosts the announcer live region. Defaults to the global `document`. */\n document?: Document;\n maxFiles: Readable<number>;\n maxSize: Readable<number>;\n multiple: Readable<boolean>;\n onChange: (files: File[], originalEvent?: Event) => void;\n onRemove: (file: File, files: File[], originalEvent?: Event) => void;\n onUploadError: (file: File, error: unknown) => void;\n onUploadProgress: (file: File, loaded: number, total: number) => void;\n onUploadSuccess: (file: File) => void;\n upload: Readable<FileUploadFn | undefined>;\n};\n\nexport type FileQueue = {\n addFiles(newFiles: File[], originalEvent?: Event): void;\n dispose(): void;\n readonly files: Readable<File[]>;\n fileState(file: File): FileUploadState;\n removeFile(file: File, originalEvent?: Event): void;\n /** Validates `newFile` (accept/max-size) and, if accepted, swaps it in for `oldFile` in place\n * and starts its upload. Returns whether the replacement was accepted. Also fires `onRemove`\n * for `oldFile` — a replace is a removal-and-add that happen to land in the same slot, and a\n * consumer listening only for removal (e.g. to clean up a server-side record) still needs to\n * hear that the old file is gone. */\n replaceFile(oldFile: File, newFile: File, originalEvent?: Event): boolean;\n retryUpload(file: File): void;\n statusIconName(file: File): string;\n uploadMetaText(file: File): string;\n uploadPercent(file: File): number;\n};\n\n/**\n * Owns file selection *and* the opt-in upload lifecycle together — they were never really\n * separable (adding a file starts its upload; removing one must abort it; replacing one is both\n * at once). Independent of `html`/rendering, following the same shape as `datagrid`'s\n * `createDataGridControls` and `combobox`'s `combobox-options.ts`: a factory `file-input.ts`\n * wires into drag/drop/native-input events and the template, directly unit-testable without\n * mounting a component.\n *\n * Every file-level rule (accept, max-size, max-files, disabled) and every upload-lifecycle rule\n * (progress, retry-from-offset, per-file isolation) lives here in one place.\n */\nexport function createFileQueue(options: FileQueueOptions): FileQueue {\n const files = signal<File[]>([]);\n const stateSignals = new Map<File, ReturnType<typeof signal<FileUploadState>>>();\n const controllers = new Map<File, AbortController>();\n\n function stateSignal(file: File): ReturnType<typeof signal<FileUploadState>> {\n let s = stateSignals.get(file);\n\n if (!s) {\n s = signal<FileUploadState>({ ...IDLE_UPLOAD_STATE, total: file.size });\n stateSignals.set(file, s);\n }\n\n return s;\n }\n\n function fileState(file: File): FileUploadState {\n return stateSignal(file).value;\n }\n\n function patchState(file: File, patch: Partial<FileUploadState>): void {\n const s = stateSignal(file);\n\n s.value = { ...s.value, ...patch };\n }\n\n // Each file gets its own `AbortController` and its own promise chain — nothing here `await`s\n // another file's upload, so one file's rejection can never block, freeze, or cancel the others.\n function startUpload(file: File, resumeFrom = 0): void {\n if (options.disabled.value) return;\n\n const uploadFn = options.upload.value;\n\n if (!uploadFn) return;\n\n controllers.get(file)?.abort();\n\n const controller = new AbortController();\n\n controllers.set(file, controller);\n patchState(file, { error: null, loaded: resumeFrom, speedBps: 0, status: 'uploading', total: file.size });\n\n // Rolling (exponential-moving-average) speed estimate rather than a naive\n // `loaded / elapsedSinceStart` average — the latter drags heavily behind the file's actual\n // *current* rate right after a slow start or a mid-transfer stall.\n let lastSampleAt = performance.now();\n let lastSampleLoaded = resumeFrom;\n\n uploadFn(file, {\n onProgress: (loaded, total) => {\n if (controller.signal.aborted) return;\n\n const now = performance.now();\n const elapsedSeconds = (now - lastSampleAt) / 1000;\n const instantBps = elapsedSeconds > 0 ? Math.max(0, (loaded - lastSampleLoaded) / elapsedSeconds) : 0;\n const previousBps = fileState(file).speedBps;\n const speedBps = previousBps > 0 ? previousBps * 0.7 + instantBps * 0.3 : instantBps;\n\n lastSampleAt = now;\n lastSampleLoaded = loaded;\n patchState(file, { loaded, speedBps, total: total || file.size });\n options.onUploadProgress(file, loaded, total || file.size);\n },\n resumeFrom,\n signal: controller.signal,\n })\n .then(() => {\n if (controller.signal.aborted) return;\n\n patchState(file, { loaded: file.size, speedBps: 0, status: 'success' });\n announce(`${file.name} uploaded successfully.`, { document: options.document });\n options.onUploadSuccess(file);\n })\n .catch((error: unknown) => {\n if (controller.signal.aborted) return;\n\n const message = error instanceof Error ? error.message : String(error);\n\n patchState(file, { error: message, speedBps: 0, status: 'error' });\n announce(`${file.name} failed to upload: ${message}`, {\n document: options.document,\n politeness: 'assertive',\n });\n options.onUploadError(file, error);\n });\n }\n\n // Resumes from the last byte offset this file actually reached, not from 0 — see\n // `FileUploadFn`'s doc comment on what \"resume\" honestly means when the transport is\n // consumer-supplied.\n function retryUpload(file: File): void {\n if (options.disabled.value) return;\n\n startUpload(file, fileState(file).loaded);\n }\n\n // Aborts + drops upload state for any file no longer in `files` — covers plain removal, a\n // non-`multiple` selection replacing the previous file, and `replaceFile()` swapping one File\n // for another. One cleanup path instead of duplicating it at every call site that can shrink\n // `files`.\n function dropOrphanedState(activeFiles: File[]): void {\n const keep = new Set(activeFiles);\n\n for (const file of stateSignals.keys()) {\n if (keep.has(file)) continue;\n\n controllers.get(file)?.abort();\n controllers.delete(file);\n stateSignals.delete(file);\n }\n }\n\n const filesWatch = watch(files, dropOrphanedState);\n\n function addFiles(newFiles: File[], originalEvent?: Event): void {\n if (options.disabled.value) return;\n\n const isMultiple = options.multiple.value;\n let incoming = Array.from(newFiles);\n\n if (!isMultiple) incoming = incoming.slice(0, 1);\n\n const acceptList = parseAccept(options.accept.value);\n const maxSize = options.maxSize.value;\n\n incoming = incoming.filter((f) => matchesAccept(f, acceptList) && isFileSizeAllowed(f, maxSize));\n\n let updated: File[] = isMultiple ? [...files.value] : [];\n const added: File[] = [];\n\n for (const f of incoming) {\n if (!updated.includes(f)) {\n updated.push(f);\n added.push(f);\n }\n }\n\n const maxFiles = options.maxFiles.value;\n\n if (maxFiles > 0 && updated.length > maxFiles) updated = updated.slice(0, maxFiles);\n\n files.value = updated;\n options.onChange(files.value, originalEvent);\n\n for (const f of added) {\n if (files.value.includes(f)) startUpload(f);\n }\n }\n\n function removeFile(file: File, originalEvent?: Event): void {\n if (options.disabled.value) return;\n\n files.value = files.value.filter((f) => f !== file);\n options.onRemove(file, files.value, originalEvent);\n options.onChange(files.value, originalEvent);\n }\n\n function replaceFile(oldFile: File, newFile: File, originalEvent?: Event): boolean {\n if (options.disabled.value) return false;\n\n const acceptList = parseAccept(options.accept.value);\n\n if (!matchesAccept(newFile, acceptList) || !isFileSizeAllowed(newFile, options.maxSize.value)) return false;\n\n files.value = files.value.map((f) => (f === oldFile ? newFile : f));\n options.onRemove(oldFile, files.value, originalEvent);\n options.onChange(files.value, originalEvent);\n startUpload(newFile);\n\n return true;\n }\n\n function uploadPercent(file: File): number {\n const state = fileState(file);\n\n if (state.total <= 0) return 0;\n\n return Math.min(100, Math.round((state.loaded / state.total) * 100));\n }\n\n // Speed + ETA on one line — percent itself is rendered separately (e.g. `<ore-progress>`'s\n // own `label`), so this only needs the two metrics that wouldn't otherwise fit there.\n function uploadMetaText(file: File): string {\n const state = fileState(file);\n\n return [formatSpeed(state.speedBps), formatEta(state)].filter(Boolean).join(' · ');\n }\n\n function statusIconName(file: File): string {\n const status = fileState(file).status;\n\n if (status === 'success') return 'check';\n\n if (status === 'error') return 'alert-circle';\n\n return 'file';\n }\n\n function dispose(): void {\n filesWatch.dispose();\n\n for (const controller of controllers.values()) controller.abort();\n\n controllers.clear();\n stateSignals.clear();\n }\n\n return {\n addFiles,\n dispose,\n fileState,\n files,\n removeFile,\n replaceFile,\n retryUpload,\n statusIconName,\n uploadMetaText,\n uploadPercent,\n };\n}\n"],"mappings":";;;;AAOA,IAAa,KAAe,MAA0B;CACpD,IAAI,MAAU,GAAG,OAAO;CAExB,IAAM,IAAI,MACJ,IAAQ;EAAC;EAAK;EAAM;EAAM;EAAM;CAAI,GACpC,IAAI,KAAK,MAAM,KAAK,IAAI,CAAK,IAAI,KAAK,IAAI,CAAC,CAAC;CAElD,OAAO,GAAG,YAAY,IAAQ,KAAK,EAAA,CAAG,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAM;AAC7D,GAEM,KAAqB,GAAY,MAA6B,CAAC,KAAW,EAAK,QAAQ,GAEvF,KAAe,MAAoC,IAAiB,IAAI,GAAG,EAAY,CAAc,EAAE,MAAM,IAE7G,KAAe,MAA0C,IAAS,EAAO,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,GAkB5G,IAAoD;CAAE,OAAO;CAAM,QAAQ;CAAG,UAAU;CAAG,QAAQ;AAAO,GAE1G,KAAa,MAAmC;CACpD,IAAI,EAAM,YAAY,KAAK,EAAM,WAAW,aAAa,OAAO;CAEhE,IAAM,IAAmB,KAAK,IAAI,GAAG,KAAK,OAAO,EAAM,QAAQ,EAAM,UAAU,EAAM,QAAQ,CAAC;CAI9F,OAFI,IAAmB,KAAW,GAAG,EAAiB,UAE/C,GAAG,KAAK,MAAM,IAAmB,EAAE,EAAE;AAC9C;AAoEA,SAAgB,EAAgB,GAAsC;CACpE,IAAM,IAAQ,EAAe,CAAC,CAAC,GACzB,oBAAe,IAAI,IAAsD,GACzE,oBAAc,IAAI,IAA2B;CAEnD,SAAS,EAAY,GAAwD;EAC3E,IAAI,IAAI,EAAa,IAAI,CAAI;EAO7B,OALK,MACH,IAAI,EAAwB;GAAE,GAAG;GAAmB,OAAO,EAAK;EAAK,CAAC,GACtE,EAAa,IAAI,GAAM,CAAC,IAGnB;CACT;CAEA,SAAS,EAAU,GAA6B;EAC9C,OAAO,EAAY,CAAI,CAAC,CAAC;CAC3B;CAEA,SAAS,EAAW,GAAY,GAAuC;EACrE,IAAM,IAAI,EAAY,CAAI;EAE1B,EAAE,QAAQ;GAAE,GAAG,EAAE;GAAO,GAAG;EAAM;CACnC;CAIA,SAAS,EAAY,GAAY,IAAa,GAAS;EACrD,IAAI,EAAQ,SAAS,OAAO;EAE5B,IAAM,IAAW,EAAQ,OAAO;EAEhC,IAAI,CAAC,GAAU;EAEf,EAAY,IAAI,CAAI,CAAC,EAAE,MAAM;EAE7B,IAAM,IAAa,IAAI,gBAAgB;EAGvC,AADA,EAAY,IAAI,GAAM,CAAU,GAChC,EAAW,GAAM;GAAE,OAAO;GAAM,QAAQ;GAAY,UAAU;GAAG,QAAQ;GAAa,OAAO,EAAK;EAAK,CAAC;EAKxG,IAAI,IAAe,YAAY,IAAI,GAC/B,IAAmB;EAEvB,EAAS,GAAM;GACb,aAAa,GAAQ,MAAU;IAC7B,IAAI,EAAW,OAAO,SAAS;IAE/B,IAAM,IAAM,YAAY,IAAI,GACtB,KAAkB,IAAM,KAAgB,KACxC,IAAa,IAAiB,IAAI,KAAK,IAAI,IAAI,IAAS,KAAoB,CAAc,IAAI,GAC9F,IAAc,EAAU,CAAI,CAAC,CAAC,UAC9B,IAAW,IAAc,IAAI,IAAc,KAAM,IAAa,KAAM;IAK1E,AAHA,IAAe,GACf,IAAmB,GACnB,EAAW,GAAM;KAAE;KAAQ;KAAU,OAAO,KAAS,EAAK;IAAK,CAAC,GAChE,EAAQ,iBAAiB,GAAM,GAAQ,KAAS,EAAK,IAAI;GAC3D;GACA;GACA,QAAQ,EAAW;EACrB,CAAC,CAAC,CACC,WAAW;GACN,EAAW,OAAO,YAEtB,EAAW,GAAM;IAAE,QAAQ,EAAK;IAAM,UAAU;IAAG,QAAQ;GAAU,CAAC,GACtE,EAAS,GAAG,EAAK,KAAK,0BAA0B,EAAE,UAAU,EAAQ,SAAS,CAAC,GAC9E,EAAQ,gBAAgB,CAAI;EAC9B,CAAC,CAAC,CACD,OAAO,MAAmB;GACzB,IAAI,EAAW,OAAO,SAAS;GAE/B,IAAM,IAAU,aAAiB,QAAQ,EAAM,UAAU,OAAO,CAAK;GAOrE,AALA,EAAW,GAAM;IAAE,OAAO;IAAS,UAAU;IAAG,QAAQ;GAAQ,CAAC,GACjE,EAAS,GAAG,EAAK,KAAK,qBAAqB,KAAW;IACpD,UAAU,EAAQ;IAClB,YAAY;GACd,CAAC,GACD,EAAQ,cAAc,GAAM,CAAK;EACnC,CAAC;CACL;CAKA,SAAS,EAAY,GAAkB;EACjC,EAAQ,SAAS,SAErB,EAAY,GAAM,EAAU,CAAI,CAAC,CAAC,MAAM;CAC1C;CAMA,SAAS,EAAkB,GAA2B;EACpD,IAAM,IAAO,IAAI,IAAI,CAAW;EAEhC,KAAK,IAAM,KAAQ,EAAa,KAAK,GAC/B,EAAK,IAAI,CAAI,MAEjB,EAAY,IAAI,CAAI,CAAC,EAAE,MAAM,GAC7B,EAAY,OAAO,CAAI,GACvB,EAAa,OAAO,CAAI;CAE5B;CAEA,IAAM,IAAa,EAAM,GAAO,CAAiB;CAEjD,SAAS,EAAS,GAAkB,GAA6B;EAC/D,IAAI,EAAQ,SAAS,OAAO;EAE5B,IAAM,IAAa,EAAQ,SAAS,OAChC,IAAW,MAAM,KAAK,CAAQ;EAElC,AAAK,MAAY,IAAW,EAAS,MAAM,GAAG,CAAC;EAE/C,IAAM,IAAa,EAAY,EAAQ,OAAO,KAAK,GAC7C,IAAU,EAAQ,QAAQ;EAEhC,IAAW,EAAS,QAAQ,MAAM,EAAc,GAAG,CAAU,KAAK,EAAkB,GAAG,CAAO,CAAC;EAE/F,IAAI,IAAkB,IAAa,CAAC,GAAG,EAAM,KAAK,IAAI,CAAC,GACjD,IAAgB,CAAC;EAEvB,KAAK,IAAM,KAAK,GACd,AAAK,EAAQ,SAAS,CAAC,MACrB,EAAQ,KAAK,CAAC,GACd,EAAM,KAAK,CAAC;EAIhB,IAAM,IAAW,EAAQ,SAAS;EAKlC,AAHI,IAAW,KAAK,EAAQ,SAAS,MAAU,IAAU,EAAQ,MAAM,GAAG,CAAQ,IAElF,EAAM,QAAQ,GACd,EAAQ,SAAS,EAAM,OAAO,CAAa;EAE3C,KAAK,IAAM,KAAK,GACd,AAAI,EAAM,MAAM,SAAS,CAAC,KAAG,EAAY,CAAC;CAE9C;CAEA,SAAS,EAAW,GAAY,GAA6B;EACvD,EAAQ,SAAS,UAErB,EAAM,QAAQ,EAAM,MAAM,QAAQ,MAAM,MAAM,CAAI,GAClD,EAAQ,SAAS,GAAM,EAAM,OAAO,CAAa,GACjD,EAAQ,SAAS,EAAM,OAAO,CAAa;CAC7C;CAEA,SAAS,EAAY,GAAe,GAAe,GAAgC;EACjF,IAAI,EAAQ,SAAS,OAAO,OAAO;EAEnC,IAAM,IAAa,EAAY,EAAQ,OAAO,KAAK;EASnD,OAPI,CAAC,EAAc,GAAS,CAAU,KAAK,CAAC,EAAkB,GAAS,EAAQ,QAAQ,KAAK,IAAU,MAEtG,EAAM,QAAQ,EAAM,MAAM,KAAK,MAAO,MAAM,IAAU,IAAU,CAAE,GAClE,EAAQ,SAAS,GAAS,EAAM,OAAO,CAAa,GACpD,EAAQ,SAAS,EAAM,OAAO,CAAa,GAC3C,EAAY,CAAO,GAEZ;CACT;CAEA,SAAS,EAAc,GAAoB;EACzC,IAAM,IAAQ,EAAU,CAAI;EAI5B,OAFI,EAAM,SAAS,IAAU,IAEtB,KAAK,IAAI,KAAK,KAAK,MAAO,EAAM,SAAS,EAAM,QAAS,GAAG,CAAC;CACrE;CAIA,SAAS,EAAe,GAAoB;EAC1C,IAAM,IAAQ,EAAU,CAAI;EAE5B,OAAO,CAAC,EAAY,EAAM,QAAQ,GAAG,EAAU,CAAK,CAAC,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,KAAK;CACnF;CAEA,SAAS,EAAe,GAAoB;EAC1C,IAAM,IAAS,EAAU,CAAI,CAAC,CAAC;EAM/B,OAJI,MAAW,YAAkB,UAE7B,MAAW,UAAgB,iBAExB;CACT;CAEA,SAAS,IAAgB;EACvB,EAAW,QAAQ;EAEnB,KAAK,IAAM,KAAc,EAAY,OAAO,GAAG,EAAW,MAAM;EAGhE,AADA,EAAY,MAAM,GAClB,EAAa,MAAM;CACrB;CAEA,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-input.d.ts","sourceRoot":"","sources":["../../../src/inputs/file-input/file-input.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file-input.d.ts","sourceRoot":"","sources":["../../../src/inputs/file-input/file-input.ts"],"names":[],"mappings":"AAmBA,OAAO,yBAAyB,CAAC;AACjC,OAAO,kCAAkC,CAAC;AAa1C,OAAO,EAAmB,KAAK,YAAY,EAAe,MAAM,qBAAqB,CAAC;AAEtF,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAI3F,sCAAsC;AACtC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sBAAsB;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACrD,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wBAAwB;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;CAC9B,CAAC;AAEF,iDAAiD;AACjD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,8CAA8C;IAC9C,MAAM,EAAE;QAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAAC,aAAa,CAAC,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,IAAI,EAAE,CAAA;KAAE,CAAC;IAChE,8CAA8C;IAC9C,MAAM,EAAE;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,KAAK,EAAE,IAAI,EAAE,CAAC;QAAC,aAAa,CAAC,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,IAAI,EAAE,CAAA;KAAE,CAAC;IAC5E,kFAAkF;IAClF,cAAc,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC;IAC/C,4EAA4E;IAC5E,iBAAiB,EAAE;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,gDAAgD;IAChD,gBAAgB,EAAE;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC;CAClC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AACH,eAAO,MAAM,cAAc,EAAG,gBAAyB,CAAC"}
|