UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

770 lines (744 loc) 22.3 kB
import { html } from 'lit' import { classMap } from 'lit/directives/class-map.js' import { ifDefined } from 'lit/directives/if-defined.js' import { getDisplayFilename, splitFilenameForTruncation } from 'shared-utils/fileupload' import type { FileItem, TQueueItemOperation, TQueueOperationContext, TTransferProgress, } from './fileupload-types' type TRenderQueueItemProps = { file: FileItem inputName: string disabled: boolean fileSize: string thumbnailUrl?: string previewEnabled?: boolean canOpenPreview?: boolean transferProgress: TTransferProgress transferErrorMessage?: string transferShowProgress?: boolean transferLastProgress?: number loadingText?: string operations: TQueueItemOperation[] activeOperationId?: string truncateTail?: number onActivateOperation: (fileId: string, operationId: string) => void onCloseOperation: (fileId: string) => void getFileAttribute: <T>(fileId: string, name: string) => T | undefined setFileAttribute: (fileId: string, name: string, value: unknown) => void onCancel: (fileId: string) => void onOpenPreview?: (fileId: string) => void onThumbnailImageError?: (fileId: string) => void } /** * Render a filename with optional middle-truncation. The split-or-not decision * lives in `splitFilenameForTruncation` (shared with React) so the threshold * matches across both runtimes. */ const renderTruncatedFilename = (filename: string, truncateTail?: number) => { const split = splitFilenameForTruncation(filename, truncateTail) if (!split) return html`<span data-pkt-truncate-part="first">${filename}</span>` return html`<span class="pkt-fileupload__queue-display__item__title__head" data-pkt-truncate-part="first" >${split.head}</span ><span class="pkt-fileupload__queue-display__item__title__tail" data-pkt-truncate-part="tail" >${split.tail}</span >` } const renderTitleWithSize = (filename: string, fileSize: string, truncateTail?: number) => html` <p class="pkt-fileupload__queue-display__item__title"> ${renderTruncatedFilename(filename, truncateTail)}${fileSize ? html`<span class="pkt-fileupload__queue-display__item__filesize"> (${fileSize})</span>` : null} </p> ` const getProgressState = (progress: TTransferProgress): 'in-progress' | 'error' | 'idle' => { if (typeof progress === 'number') return 'in-progress' if (progress === 'error') return 'error' return 'idle' } const renderLoadingText = (loadingText?: string) => html`<span class="pkt-fileupload__queue-display__item__loading-text" aria-label="Laster opp fil" >${loadingText ?? 'Laster opp...'}</span >` const createOperationContext = ({ operationId, file, inputName, disabled, isActive, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }: { operationId: string file: FileItem inputName: string disabled: boolean isActive: boolean onActivateOperation: (fileId: string, operationId: string) => void onCloseOperation: (fileId: string) => void getFileAttribute: <T>(fileId: string, name: string) => T | undefined setFileAttribute: (fileId: string, name: string, value: unknown) => void }): TQueueOperationContext => ({ file, inputName, disabled, isActive, activate: () => onActivateOperation(file.fileId, operationId), close: () => onCloseOperation(file.fileId), getAttribute: <T>(name: string) => getFileAttribute<T>(file.fileId, name), setAttribute: (name: string, value: unknown) => setFileAttribute(file.fileId, name, value), }) const renderTransferInProgress = ({ file, fileSize, disabled, transferProgress, transferShowProgress, loadingText, truncateTail, onCancel, iconName = 'document-text', }: { file: FileItem fileSize: string disabled: boolean transferProgress: number transferShowProgress?: boolean loadingText?: string truncateTail?: number onCancel: (fileId: string) => void iconName?: string }) => { const filename = getDisplayFilename(file) const showProgressBar = transferShowProgress ?? transferProgress !== 0 return html` <pkt-icon name=${iconName} class="pkt-fileupload__queue-display__item__icon pkt-icon--medium" aria-hidden="true" ></pkt-icon> ${renderTitleWithSize(filename, fileSize, truncateTail)} <button type="button" class="pkt-fileupload__queue-display__item__cancel-button" aria-label="Avbryt opplasting" ?disabled=${disabled} @click=${() => onCancel(file.fileId)} > Avbryt </button> ${showProgressBar ? html` <pkt-progressbar class="pkt-fileupload__queue-display__item__progress" valueCurrent=${Math.round(transferProgress * 100)} valueMax=${100} statusType="none" statusPlacement="following" ></pkt-progressbar> <span class="pkt-fileupload__queue-display__item__percentage" aria-hidden="true" >${Math.round(transferProgress * 100)}%</span > ` : renderLoadingText(loadingText)} ` } const renderTransferError = ({ file, fileSize, disabled, transferShowProgress, transferLastProgress, transferErrorMessage, truncateTail, onCancel, }: { file: FileItem fileSize: string disabled: boolean transferShowProgress?: boolean transferLastProgress?: number transferErrorMessage?: string truncateTail?: number onCancel: (fileId: string) => void }) => { const filename = getDisplayFilename(file) const showProgressError = !!transferShowProgress const progressValue = Math.round((transferLastProgress ?? 1) * 100) return html` <pkt-icon name="alert-error" class="pkt-fileupload__queue-display__item__icon pkt-icon--medium" aria-hidden="true" ></pkt-icon> ${renderTitleWithSize(filename, fileSize, truncateTail)} <button type="button" class="pkt-fileupload__queue-display__item__cancel-button" aria-label="Fjern fil som feilet under opplasting" ?disabled=${disabled} @click=${() => onCancel(file.fileId)} > Fjern </button> ${showProgressError ? html` <pkt-progressbar class="pkt-fileupload__queue-display__item__progress pkt-fileupload__queue-display__item__progress--error" valueCurrent=${progressValue} valueMax=${100} statusType="none" statusPlacement="following" ></pkt-progressbar> ` : null} ${transferErrorMessage ? html`<span class="pkt-fileupload__queue-display__item__error-message" >${transferErrorMessage}</span >` : null} ` } const renderOperationButtons = ({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }: { file: FileItem disabled: boolean inputName: string operations: TQueueItemOperation[] activeOperationId?: string onActivateOperation: (fileId: string, operationId: string) => void onCloseOperation: (fileId: string) => void getFileAttribute: <T>(fileId: string, name: string) => T | undefined setFileAttribute: (fileId: string, name: string, value: unknown) => void }) => { const activeOperation = operations.find((operation) => operation.id === activeOperationId) const hasOperationUIActive = !!( activeOperation?.renderInlineUI || activeOperation?.renderExtendedUI ) if (hasOperationUIActive) return null const operationButtons = operations .map((operation) => { const context = createOperationContext({ operationId: operation.id, file, inputName, disabled, isActive: operation.id === activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) const title = typeof operation.title === 'function' ? operation.title(file) : operation.title if (!title) return null const ariaLabel = typeof operation.ariaLabel === 'function' ? operation.ariaLabel(file) : operation.ariaLabel return html`<button type="button" class="pkt-fileupload__queue-display__item__operation" aria-label=${ifDefined(ariaLabel)} ?disabled=${disabled} @click=${() => { if (operation.renderInlineUI || operation.renderExtendedUI) { context.activate() } operation.onClick?.(context) }} > ${title} </button>` }) .filter(Boolean) if (operationButtons.length === 0) return null return html`<div class="pkt-fileupload__queue-display__item__actions">${operationButtons}</div>` } const renderOperationContent = ({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }: { file: FileItem disabled: boolean inputName: string operations: TQueueItemOperation[] activeOperationId?: string onActivateOperation: (fileId: string, operationId: string) => void onCloseOperation: (fileId: string) => void getFileAttribute: <T>(fileId: string, name: string) => T | undefined setFileAttribute: (fileId: string, name: string, value: unknown) => void }) => operations.map((operation) => { if (!operation.renderContent) return null const context = createOperationContext({ operationId: operation.id, file, inputName, disabled, isActive: operation.id === activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) const content = operation.renderContent(context) if (content === null || content === undefined) { return null } return html`<div class="pkt-fileupload__queue-display__item__operation-content"> ${content} </div>` }) const renderExpandedOperationContent = ({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }: { file: FileItem disabled: boolean inputName: string operations: TQueueItemOperation[] activeOperationId?: string onActivateOperation: (fileId: string, operationId: string) => void onCloseOperation: (fileId: string) => void getFileAttribute: <T>(fileId: string, name: string) => T | undefined setFileAttribute: (fileId: string, name: string, value: unknown) => void }) => { if (!activeOperationId) return null const activeOperation = operations.find((operation) => operation.id === activeOperationId) if (!activeOperation?.renderExtendedUI) return null const context = createOperationContext({ operationId: activeOperation.id, file, inputName, disabled, isActive: true, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) return html`<div class="pkt-fileupload__queue-display__item__expanded-operation-ui"> ${activeOperation.renderExtendedUI(context)} </div>` } const renderHiddenOperationInputs = ({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }: { file: FileItem disabled: boolean inputName: string operations: TQueueItemOperation[] activeOperationId?: string onActivateOperation: (fileId: string, operationId: string) => void onCloseOperation: (fileId: string) => void getFileAttribute: <T>(fileId: string, name: string) => T | undefined setFileAttribute: (fileId: string, name: string, value: unknown) => void }) => operations.map((operation) => { if (!operation.renderHidden) return null const context = createOperationContext({ operationId: operation.id, file, inputName, disabled, isActive: operation.id === activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) return operation.renderHidden(context) }) const renderFilenameIdleMain = (file: FileItem, fileSize: string, truncateTail?: number) => { const filename = getDisplayFilename(file) return html` <pkt-icon name="document-text" class="pkt-fileupload__queue-display__item__icon pkt-icon--medium" aria-hidden="true" ></pkt-icon> ${renderTitleWithSize(filename, fileSize, truncateTail)} ` } const renderThumbnailIdleMain = ({ file, thumbnailUrl, previewEnabled, canOpenPreview, truncateTail, onOpenPreview, onThumbnailImageError, }: { file: FileItem thumbnailUrl?: string previewEnabled?: boolean canOpenPreview?: boolean truncateTail?: number onOpenPreview?: (fileId: string) => void onThumbnailImageError?: (fileId: string) => void }) => { const filename = getDisplayFilename(file) const hasThumbnail = !!thumbnailUrl const showExpandButton = !!previewEnabled && !!canOpenPreview const isInteractive = showExpandButton && !!onOpenPreview return html` <div class=${classMap({ 'pkt-fileupload__queue-display__item__thumbnail': true, 'pkt-fileupload__queue-display__item__thumbnail--fallback': !hasThumbnail, })} > <button type="button" class="pkt-fileupload__queue-display__item__thumbnail__image-wrapper pkt-btn pkt-btn--medium pkt-btn--secondary pkt-btn--label-only" aria-label=${`Forhåndsvis bilde ${filename}`} ?disabled=${!isInteractive} @click=${() => { if (!isInteractive || !onOpenPreview) return onOpenPreview(file.fileId) }} > <span class="pkt-btn__text"> ${showExpandButton ? html`<pkt-icon name="expand" class="pkt-fileupload__queue-display__item__thumbnail__expand-icon" aria-hidden="true" ></pkt-icon>` : null} ${hasThumbnail ? html`<img src=${ifDefined(thumbnailUrl)} alt=${filename} @error=${() => onThumbnailImageError?.(file.fileId)} />` : html`<pkt-icon name="document-text" class="pkt-fileupload__queue-display__item__icon pkt-icon--medium" aria-hidden="true" ></pkt-icon>`} </span> </button> <span class="pkt-fileupload__queue-display__item__thumbnail__title" >${renderTruncatedFilename(filename, truncateTail)}</span > </div> ` } export const renderFilenameQueueItem = ({ file, inputName, disabled, fileSize, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, transferProgress, transferErrorMessage, transferShowProgress, transferLastProgress, loadingText, truncateTail, onCancel, }: TRenderQueueItemProps) => { const state = getProgressState(transferProgress) const activeOperation = operations.find((operation) => operation.id === activeOperationId) const inlineOperationTemplate = activeOperation?.renderInlineUI ? activeOperation.renderInlineUI( createOperationContext({ operationId: activeOperation.id, file, inputName, disabled, isActive: true, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }), ) : null return html` <li class=${classMap({ 'pkt-fileupload__queue-display__item': true, 'pkt-fileupload__queue-display__item--in-progress': state === 'in-progress', 'pkt-fileupload__queue-display__item--error': state === 'error', [`pkt-fileupload__queue-display__item--${transferProgress}`]: typeof transferProgress === 'string', })} > ${renderHiddenOperationInputs({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, })} ${state === 'in-progress' ? renderTransferInProgress({ file, fileSize, disabled, transferProgress: transferProgress as number, transferShowProgress, loadingText, truncateTail, onCancel, }) : state === 'error' ? renderTransferError({ file, fileSize, disabled, transferShowProgress, transferLastProgress, transferErrorMessage, truncateTail, onCancel, }) : inlineOperationTemplate ? html` <pkt-icon name="document-text" class="pkt-fileupload__queue-display__item__icon pkt-icon--medium" aria-hidden="true" ></pkt-icon> <div class="pkt-fileupload__queue-display__item__inline-ui"> ${inlineOperationTemplate} </div> ` : renderFilenameIdleMain(file, fileSize, truncateTail)} ${state === 'idle' ? renderOperationButtons({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) : null} ${state === 'idle' ? renderOperationContent({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) : null} ${state === 'idle' ? renderExpandedOperationContent({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) : null} </li> ` } export const renderThumbnailQueueItem = ({ file, inputName, disabled, fileSize, thumbnailUrl, previewEnabled, canOpenPreview, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, transferProgress, transferErrorMessage, transferShowProgress, transferLastProgress, loadingText, truncateTail, onCancel, onOpenPreview, onThumbnailImageError, }: TRenderQueueItemProps) => { const state = getProgressState(transferProgress) const activeOperation = operations.find((operation) => operation.id === activeOperationId) const inlineOperationTemplate = activeOperation?.renderInlineUI ? activeOperation.renderInlineUI( createOperationContext({ operationId: activeOperation.id, file, inputName, disabled, isActive: true, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }), ) : null return html` <li class=${classMap({ 'pkt-fileupload__queue-display__item': true, 'pkt-fileupload__queue-display__item--in-progress': state === 'in-progress', 'pkt-fileupload__queue-display__item--error': state === 'error', [`pkt-fileupload__queue-display__item--${transferProgress}`]: typeof transferProgress === 'string', })} > ${renderHiddenOperationInputs({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, })} ${state === 'in-progress' ? renderTransferInProgress({ file, fileSize, disabled, transferProgress: transferProgress as number, transferShowProgress, loadingText, truncateTail, onCancel, iconName: 'picture', }) : state === 'error' ? renderTransferError({ file, fileSize, disabled, transferShowProgress, transferLastProgress, transferErrorMessage, truncateTail, onCancel, }) : inlineOperationTemplate ? html` <pkt-icon name="picture" class="pkt-fileupload__queue-display__item__icon pkt-icon--medium" aria-hidden="true" ></pkt-icon> <div class="pkt-fileupload__queue-display__item__inline-ui"> ${inlineOperationTemplate} </div> ` : renderThumbnailIdleMain({ file, thumbnailUrl, previewEnabled, canOpenPreview, truncateTail, onOpenPreview, onThumbnailImageError, })} ${state === 'idle' ? renderOperationButtons({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) : null} ${state === 'idle' ? renderOperationContent({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) : null} ${state === 'idle' ? renderExpandedOperationContent({ file, disabled, inputName, operations, activeOperationId, onActivateOperation, onCloseOperation, getFileAttribute, setFileAttribute, }) : null} </li> ` }