@oslokommune/punkt-react 15.0.3 → 15.1.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.
@@ -0,0 +1,137 @@
1
+ import {
2
+ createContext,
3
+ HTMLAttributes,
4
+ useCallback,
5
+ useContext,
6
+ useLayoutEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from 'react'
11
+
12
+ const MAX_LINES = 2
13
+
14
+ const getLineHeightPx = (element: HTMLElement): number => {
15
+ const style = window.getComputedStyle(element)
16
+ const lineHeight = parseFloat(style.lineHeight)
17
+ if (!Number.isNaN(lineHeight) && lineHeight > 0) return lineHeight
18
+
19
+ // `line-height: normal` fallback (approx. 1.2 * font-size)
20
+ const fontSize = parseFloat(style.fontSize)
21
+ return (!Number.isNaN(fontSize) && fontSize > 0 ? fontSize : 16) * 1.2
22
+ }
23
+
24
+ const elementOverflowsLines = (measureEl: HTMLElement, lineHeightPx: number, maxLines: number): boolean => {
25
+ const maxHeight = lineHeightPx * maxLines
26
+ const height = measureEl.getBoundingClientRect().height
27
+ return height > maxHeight + 0.5
28
+ }
29
+
30
+ export interface IMiddleTruncate extends HTMLAttributes<HTMLSpanElement> {
31
+ children: string
32
+ tail?: number
33
+ }
34
+
35
+ type TTruncateContext = { tail: number | undefined }
36
+ export const TruncateContext = createContext<TTruncateContext>({ tail: 5 })
37
+
38
+ export const Truncate = ({ children, tail: tailProp, ...props }: IMiddleTruncate): JSX.Element => {
39
+ const firstPart = children
40
+ const context = useContext(TruncateContext)
41
+ const tail = tailProp !== undefined ? tailProp : context.tail || 0
42
+ const secondPart = children.length <= tail + 3 ? null : children.slice(-tail)
43
+ const showingSecondPart = secondPart !== null && tail > 0
44
+ const wrapperRef = useRef<HTMLSpanElement>(null)
45
+ const measureRef = useRef<HTMLSpanElement>(null)
46
+ const [isOverflowing, setIsOverflowing] = useState(false)
47
+
48
+ const computeOverflow = useCallback(() => {
49
+ const wrapper = wrapperRef.current
50
+ const measureEl = measureRef.current
51
+ if (!wrapper || !measureEl) return
52
+
53
+ const lineHeightPx = getLineHeightPx(wrapper)
54
+ setIsOverflowing(elementOverflowsLines(measureEl, lineHeightPx, MAX_LINES))
55
+ }, [])
56
+
57
+ const resizeObserver = useMemo(() => {
58
+ return new ResizeObserver((entries) => {
59
+ if (entries.some((entry) => entry.target === wrapperRef.current)) {
60
+ computeOverflow()
61
+ }
62
+ })
63
+ }, [computeOverflow])
64
+ useLayoutEffect(() => {
65
+ if (wrapperRef.current) {
66
+ resizeObserver.observe(wrapperRef.current)
67
+ }
68
+ computeOverflow()
69
+ return () => resizeObserver.disconnect()
70
+ }, [children, resizeObserver, computeOverflow])
71
+ return (
72
+ <span
73
+ ref={wrapperRef}
74
+ {...props}
75
+ data-pkt-truncate-part="wrapper"
76
+ style={{
77
+ maxWidth: '100%',
78
+ width: '100%',
79
+ display: 'grid',
80
+ gridTemplateColumns: 'minmax(0, 1fr) auto',
81
+ alignItems: 'end',
82
+ columnGap: '0.25rem',
83
+ overflow: 'hidden',
84
+ position: 'relative',
85
+ ...props.style,
86
+ }}
87
+ >
88
+ <span
89
+ data-pkt-truncate-part="first"
90
+ style={{
91
+ minWidth: 0,
92
+ display: '-webkit-box',
93
+ WebkitBoxOrient: 'vertical',
94
+ WebkitLineClamp: MAX_LINES,
95
+ overflow: 'hidden',
96
+ textOverflow: 'ellipsis',
97
+ whiteSpace: 'normal',
98
+ overflowWrap: 'anywhere',
99
+ }}
100
+ >
101
+ {firstPart}
102
+ </span>
103
+ {tail !== 0 && (
104
+ <span
105
+ data-pkt-truncate-part="tail"
106
+ style={{
107
+ whiteSpace: 'nowrap',
108
+ display: isOverflowing ? 'inline' : 'none',
109
+ paddingRight: '0.5rem',
110
+ }}
111
+ aria-hidden={true}
112
+ >
113
+ {secondPart}
114
+ </span>
115
+ )}
116
+
117
+ {/* Hidden measuring element (full text, wraps normally) */}
118
+ <span
119
+ ref={measureRef}
120
+ aria-hidden={true}
121
+ data-pkt-truncate-part="measure"
122
+ style={{
123
+ position: 'absolute',
124
+ top: 0,
125
+ left: 0,
126
+ width: '100%',
127
+ visibility: 'hidden',
128
+ pointerEvents: 'none',
129
+ whiteSpace: 'normal',
130
+ overflowWrap: 'anywhere',
131
+ }}
132
+ >
133
+ {children}
134
+ </span>
135
+ </span>
136
+ )
137
+ }
@@ -0,0 +1,188 @@
1
+ import { useContext, useLayoutEffect, useRef } from 'react'
2
+
3
+ import { PktButton } from '../../button/Button'
4
+ import { PktIcon } from '../../icon/Icon'
5
+ import { TFileAttributes, FileItem, PktFileUploadContext, TQueueItemOperation } from '../types'
6
+ import { PktTextarea } from '../../textarea/Textarea'
7
+
8
+ const COMMENT_SYMBOL = Symbol('comment')
9
+
10
+ export interface IComment {
11
+ text: string
12
+ timestamp: Date
13
+ }
14
+
15
+ const formatTimestamp = (date: Date): string => {
16
+ const day = String(date.getDate()).padStart(2, '0')
17
+ const month = String(date.getMonth() + 1).padStart(2, '0')
18
+ const year = date.getFullYear()
19
+ const hours = String(date.getHours()).padStart(2, '0')
20
+ const minutes = String(date.getMinutes()).padStart(2, '0')
21
+ return `${day}.${month}.${year} kl. ${hours}:${minutes}`
22
+ }
23
+
24
+ const AddComment = ({
25
+ fileItem,
26
+ closeOperationUi,
27
+ onAddComment,
28
+ existingComment,
29
+ }: {
30
+ fileItem: FileItem
31
+ closeOperationUi: () => void
32
+ onAddComment: (comment: IComment) => void
33
+ existingComment?: IComment
34
+ }) => {
35
+ const inputRef = useRef<HTMLTextAreaElement>(null)
36
+ const isEditing = !!existingComment
37
+
38
+ const handleAddComment = () => {
39
+ const text = inputRef.current?.value?.trim()
40
+ if (text) {
41
+ onAddComment({
42
+ text,
43
+ timestamp: new Date(),
44
+ })
45
+ }
46
+ closeOperationUi()
47
+ }
48
+
49
+ useLayoutEffect(() => {
50
+ const t = setTimeout(() => inputRef.current?.focus({ preventScroll: true }), 0)
51
+ return () => clearTimeout(t)
52
+ }, [fileItem.fileId])
53
+
54
+ return (
55
+ <>
56
+ <PktTextarea
57
+ autoFocus
58
+ label={isEditing ? 'Rediger kommentar' : 'Legg til kommentar'}
59
+ name={`comment-${fileItem.fileId}`}
60
+ className="pkt-fileupload__queue-display__item__comment-input"
61
+ placeholder="Skriv inn kommentar"
62
+ rows={2}
63
+ id={`comment-${fileItem.fileId}`}
64
+ ref={inputRef}
65
+ defaultValue={existingComment?.text}
66
+ />
67
+
68
+ <PktButton skin="secondary" size="small" onClick={handleAddComment}>
69
+ {isEditing ? 'Lagre kommentar' : 'Legg til kommentar'}
70
+ </PktButton>
71
+ <PktButton skin="tertiary" size="small" onClick={closeOperationUi}>
72
+ Avbryt
73
+ </PktButton>
74
+ </>
75
+ )
76
+ }
77
+
78
+ const ShowComments = ({
79
+ comments,
80
+ onDeleteComment,
81
+ onEditComment,
82
+ }: {
83
+ comments?: IComment[]
84
+ onDeleteComment?: (index: number) => void
85
+ onEditComment?: (index: number) => void
86
+ }) =>
87
+ comments && comments.length > 0 ? (
88
+ <div className="pkt-fileupload__queue-display__item__comments">
89
+ {comments.map((comment, index) => (
90
+ <div key={index} className="pkt-fileupload__queue-display__item__comment">
91
+ <div className="pkt-fileupload__queue-display__item__comment__content">
92
+ <span className="pkt-fileupload__queue-display__item__comment__text" aria-label="Kommentar tekst">
93
+ {comment.text}
94
+ </span>
95
+ <time className="pkt-fileupload__queue-display__item__comment__time">
96
+ {formatTimestamp(comment.timestamp)}
97
+ </time>
98
+ </div>
99
+ <div className="pkt-fileupload__queue-display__item__comment__actions">
100
+ {onDeleteComment && (
101
+ <button
102
+ type="button"
103
+ className="pkt-fileupload__queue-display__item__comment__action"
104
+ onClick={() => onDeleteComment(index)}
105
+ aria-label="Slett kommentar"
106
+ >
107
+ <PktIcon name="trash-can" />
108
+ </button>
109
+ )}
110
+ {onEditComment && (
111
+ <button
112
+ type="button"
113
+ className="pkt-fileupload__queue-display__item__comment__action"
114
+ onClick={() => onEditComment(index)}
115
+ aria-label="Rediger kommentar"
116
+ >
117
+ <PktIcon name="edit" />
118
+ </button>
119
+ )}
120
+ </div>
121
+ </div>
122
+ ))}
123
+ </div>
124
+ ) : null
125
+
126
+ const CommentHiddenInput = (props: { comments: IComment[] | undefined }) => {
127
+ const context = useContext(PktFileUploadContext)
128
+ return (
129
+ <input
130
+ type="hidden"
131
+ name={`${context.name}-comments`}
132
+ value={props.comments ? JSON.stringify(props.comments) : ''}
133
+ />
134
+ )
135
+ }
136
+
137
+ export const addCommentOperation = (attributes: TFileAttributes): TQueueItemOperation => {
138
+ const commentsAttribute = attributes<IComment[]>('comments')
139
+
140
+ const addComment = (fileId: string, newComment: IComment) => {
141
+ commentsAttribute.set(fileId, [newComment])
142
+ }
143
+
144
+ const deleteComment = (fileId: string, commentIndex: number) => {
145
+ const existingComments = commentsAttribute.get(fileId) || []
146
+ const newComments = existingComments.filter((_, i) => i !== commentIndex)
147
+ commentsAttribute.set(fileId, newComments.length > 0 ? newComments : undefined)
148
+ }
149
+
150
+ return {
151
+ // Only show button text when no comment exists yet
152
+ title: (fileItem: FileItem) => {
153
+ const comments = commentsAttribute.get(fileItem.fileId)
154
+ // If comment exists, return empty string to hide the button (icons are shown in renderContent)
155
+ if (comments && comments.length > 0) return ''
156
+ return 'Legg til kommentar'
157
+ },
158
+ renderExtendedUI: (fileItem: FileItem, closeOperationUi: () => void) => {
159
+ const existingComments = commentsAttribute.get(fileItem.fileId)
160
+ const existingComment = existingComments?.[0]
161
+ return (
162
+ <AddComment
163
+ fileItem={fileItem}
164
+ closeOperationUi={closeOperationUi}
165
+ onAddComment={(comment) => addComment(fileItem.fileId, comment)}
166
+ existingComment={existingComment}
167
+ />
168
+ )
169
+ },
170
+ renderContent: (fileItem: FileItem, activateOperation?: () => void, isOperationActive?: boolean) => {
171
+ // Hide comment preview when editing
172
+ if (isOperationActive) return null
173
+
174
+ const comments = commentsAttribute.get(fileItem.fileId)
175
+ return (
176
+ <ShowComments
177
+ comments={comments}
178
+ onDeleteComment={(index) => deleteComment(fileItem.fileId, index)}
179
+ onEditComment={activateOperation ? () => activateOperation() : undefined}
180
+ />
181
+ )
182
+ },
183
+ renderHidden: (fileItem: FileItem) => (
184
+ <CommentHiddenInput key={`comments${fileItem.fileId}`} comments={commentsAttribute.get(fileItem.fileId)} />
185
+ ),
186
+ symbol: COMMENT_SYMBOL,
187
+ }
188
+ }
@@ -0,0 +1,10 @@
1
+ import { TFileId, FileItem, TQueueItemOperation } from '@/components/fileupload/types'
2
+
3
+ const DELETE_SYMBOL = Symbol('deleteFile')
4
+
5
+ export const removeFileOperation = (onFileRemoved: (fileId: TFileId) => void): TQueueItemOperation => ({
6
+ title: 'Slett',
7
+ ariaLabel: 'Slett fil',
8
+ onClick: (fileItem: FileItem) => onFileRemoved(fileItem.fileId),
9
+ symbol: DELETE_SYMBOL,
10
+ })
@@ -0,0 +1,79 @@
1
+ import { useContext, useId, useRef } from 'react'
2
+
3
+ import { PktButton } from '../../button/Button'
4
+ import { TFileAttributes, FileItem, PktFileUploadContext, TQueueItemOperation } from '../types'
5
+
6
+ const RENAME_SYMBOL = Symbol('renameFile')
7
+
8
+ const RenameFile = (props: { value: string | undefined; onSave: (newName: string) => void; onCancel: () => void }) => {
9
+ const inputRef = useRef<HTMLInputElement>(null)
10
+ const inputId = useId()
11
+
12
+ const save = () => {
13
+ props.onSave(inputRef.current?.value ?? '')
14
+ }
15
+
16
+ return (
17
+ <>
18
+ <label htmlFor={inputId} className="pkt-sr-only">
19
+ Endre filnavn
20
+ </label>
21
+ <input
22
+ id={inputId}
23
+ ref={inputRef}
24
+ type="text"
25
+ autoFocus
26
+ defaultValue={props.value}
27
+ className="pkt-fileupload__queue-display__item__rename-input"
28
+ onKeyDown={(e) => {
29
+ if (e.key === 'Enter') {
30
+ e.preventDefault()
31
+ e.stopPropagation()
32
+ save()
33
+ }
34
+ if (e.key === 'Escape') {
35
+ e.preventDefault()
36
+ e.stopPropagation()
37
+ props.onCancel()
38
+ }
39
+ }}
40
+ />
41
+ <PktButton skin="secondary" size="small" onClick={() => props.onSave(inputRef.current!.value)}>
42
+ Lagre
43
+ </PktButton>
44
+ <PktButton skin="tertiary" size="small" onClick={props.onCancel}>
45
+ Avbryt
46
+ </PktButton>
47
+ </>
48
+ )
49
+ }
50
+
51
+ const RenameHiddenInput = (props: { targetFilename: string }) => {
52
+ const context = useContext(PktFileUploadContext)
53
+ return <input type="hidden" name={`${context.name}-targetFilename`} value={props.targetFilename} />
54
+ }
55
+
56
+ export const renameFileOperation = (attributes: TFileAttributes): TQueueItemOperation => {
57
+ const targetFilenameAttribute = attributes<string>('targetFilename')
58
+ return {
59
+ title: 'Rediger',
60
+ ariaLabel: 'Rediger filnavn',
61
+ renderInlineUI: (fileItem: FileItem, closeOperationUi: () => void) => (
62
+ <RenameFile
63
+ value={targetFilenameAttribute.get(fileItem.fileId) || undefined}
64
+ onSave={(newName: string) => {
65
+ targetFilenameAttribute.set(fileItem.fileId, newName)
66
+ closeOperationUi()
67
+ }}
68
+ onCancel={closeOperationUi}
69
+ />
70
+ ),
71
+ renderHidden: (fileItem: FileItem) => (
72
+ <RenameHiddenInput
73
+ key={`rename${fileItem.fileId}`}
74
+ targetFilename={targetFilenameAttribute.get(fileItem.fileId) || fileItem.file.name}
75
+ />
76
+ ),
77
+ symbol: RENAME_SYMBOL,
78
+ }
79
+ }
@@ -0,0 +1,3 @@
1
+ export { useFileAttributes } from './useFileAttributes'
2
+ export { useImagePreview } from './useImagePreview'
3
+ export { useOperationState } from './useOperationState'
@@ -0,0 +1,46 @@
1
+ import { useCallback } from 'react'
2
+
3
+ import { TFileAttributes, TFileId, FileItem, TFileItemList } from '../types'
4
+
5
+ export const useFileAttributes = (
6
+ value: TFileItemList,
7
+ onFileUpdated: (TFileId: TFileId, updates: Partial<FileItem>) => void,
8
+ ) => {
9
+ const setAttributeForFile = useCallback(
10
+ (TFileId: TFileId, attributeName: string, attributeValue: any) => {
11
+ const fileItem = value.find((file) => file.fileId === TFileId)!
12
+ const attributes = fileItem.attributes || {}
13
+ fileItem.attributes = {
14
+ ...attributes,
15
+ [attributeName]: attributeValue,
16
+ }
17
+ onFileUpdated(TFileId, { attributes: fileItem.attributes })
18
+ },
19
+ [onFileUpdated, value],
20
+ )
21
+
22
+ const getAttributeForFile = useCallback(
23
+ (fileId: TFileId, attributeName: string): any | undefined => {
24
+ const fileItem = value.find((file) => file.fileId === fileId)!
25
+ const attributes = fileItem.attributes || {}
26
+ return attributes[attributeName]
27
+ },
28
+ [value],
29
+ )
30
+
31
+ const fileAttributes: TFileAttributes = useCallback(
32
+ <T>(attributeName: string) => {
33
+ return {
34
+ get: (fileId: TFileId): T | undefined => {
35
+ return getAttributeForFile(fileId, attributeName)
36
+ },
37
+ set: (fileId: TFileId, attributeValue: T | undefined) => {
38
+ setAttributeForFile(fileId, attributeName, attributeValue)
39
+ },
40
+ }
41
+ },
42
+ [getAttributeForFile, setAttributeForFile],
43
+ )
44
+
45
+ return { fileAttributes } as const
46
+ }
@@ -0,0 +1,42 @@
1
+ import { useCallback, useRef, useState } from 'react'
2
+
3
+ import { TFileAndTransfer, TFileId } from '../types'
4
+
5
+ export const useImagePreview = (previewableImages: TFileAndTransfer[]) => {
6
+ const [isOpen, setIsOpen] = useState(false)
7
+ const [currentIndex, setCurrentIndex] = useState(0)
8
+ const modalRef = useRef<HTMLDialogElement>(null)
9
+
10
+ const open = useCallback(
11
+ (fileId: TFileId) => {
12
+ const index = previewableImages.findIndex((item) => item.fileId === fileId)
13
+ if (index >= 0) {
14
+ setCurrentIndex(index)
15
+ setIsOpen(true)
16
+ modalRef.current?.showModal()
17
+ }
18
+ },
19
+ [previewableImages],
20
+ )
21
+
22
+ const close = useCallback(() => {
23
+ setIsOpen(false)
24
+ modalRef.current?.close()
25
+ }, [])
26
+
27
+ const navigate = useCallback(
28
+ (direction: 'prev' | 'next') => {
29
+ if (previewableImages.length === 0) return
30
+
31
+ setCurrentIndex((prev) => {
32
+ if (direction === 'prev') {
33
+ return prev <= 0 ? previewableImages.length - 1 : prev - 1
34
+ }
35
+ return prev >= previewableImages.length - 1 ? 0 : prev + 1
36
+ })
37
+ },
38
+ [previewableImages.length],
39
+ )
40
+
41
+ return { isOpen, currentIndex, modalRef, open, close, navigate }
42
+ }
@@ -0,0 +1,30 @@
1
+ import { useCallback, useState } from 'react'
2
+
3
+ import { TFileId, TQueueItemOperation } from '../types'
4
+
5
+ export const useOperationState = (queueItemOperations: TQueueItemOperation[]) => {
6
+ const [activatedSymbols, setActivatedSymbols] = useState<Record<TFileId, symbol>>({})
7
+
8
+ const getActivated = useCallback(
9
+ (fileId: TFileId): TQueueItemOperation | undefined => {
10
+ const symbol = activatedSymbols[fileId]
11
+ if (!symbol) return undefined
12
+ return queueItemOperations.find((op) => op.symbol === symbol)
13
+ },
14
+ [activatedSymbols, queueItemOperations],
15
+ )
16
+
17
+ const activate = useCallback((fileId: TFileId, operation: TQueueItemOperation) => {
18
+ setActivatedSymbols((prev) => ({ ...prev, [fileId]: operation.symbol }))
19
+ }, [])
20
+
21
+ const close = useCallback((fileId: TFileId) => {
22
+ setActivatedSymbols((prev) => {
23
+ const updated = { ...prev }
24
+ delete updated[fileId]
25
+ return updated
26
+ })
27
+ }, [])
28
+
29
+ return { getActivated, activate, close }
30
+ }
@@ -0,0 +1,4 @@
1
+ // Re-export all hooks from the hooks folder
2
+ export { useFileAttributes } from './hooks/useFileAttributes'
3
+ export { useImagePreview } from './hooks/useImagePreview'
4
+ export { useOperationState } from './hooks/useOperationState'
@@ -0,0 +1,20 @@
1
+ export const uiMultipleTexts = {
2
+ dropFilesHere: 'Slipp filene her ...',
3
+ selectOrDragFiles: 'Dra filer hit for å laste dem opp eller ',
4
+ chooseFiles: 'velg filer',
5
+ }
6
+ export const uiSingleTexts: typeof uiMultipleTexts = {
7
+ dropFilesHere: 'Slipp filen her ...',
8
+ selectOrDragFiles: 'Dra en fil for å laste den opp eller ',
9
+ chooseFiles: 'velg en fil',
10
+ }
11
+ export const uiThumbnailMultipleTexts: typeof uiMultipleTexts = {
12
+ dropFilesHere: 'Slipp bildene her ...',
13
+ selectOrDragFiles: 'Dra bilder hit for å laste dem opp eller ',
14
+ chooseFiles: 'velg fra kamerarull',
15
+ }
16
+ export const uiThumbnailSingleTexts: typeof uiMultipleTexts = {
17
+ dropFilesHere: 'Slipp bildet her ...',
18
+ selectOrDragFiles: 'Dra et bilde for å laste det opp eller ',
19
+ chooseFiles: 'velg fra kamerarull',
20
+ }
@@ -0,0 +1,94 @@
1
+ import { createContext, ReactElement, ReactNode } from 'react'
2
+
3
+ import { uuidish } from '../../utils/stringutils'
4
+
5
+ /** Unique identifier assigned to each selected file. */
6
+ export type TFileId = string
7
+
8
+ /**
9
+ * A selected file plus metadata used by the FileUpload UI.
10
+ *
11
+ * Notes:
12
+ * - A `fileId` is generated automatically if omitted.
13
+ * - `attributes.targetFilename` is the filename shown in the queue and may be updated by extensions (e.g. rename).
14
+ */
15
+ export class FileItem implements FileItem {
16
+ fileId: TFileId
17
+ file: File
18
+ attributes: Record<string, any> = {}
19
+
20
+ constructor(file: File, fileId?: TFileId) {
21
+ this.fileId = fileId || uuidish()
22
+ this.file = file
23
+ this.attributes = {
24
+ targetFilename: file.name,
25
+ }
26
+ }
27
+ }
28
+
29
+ /** Internal context used by queue item operations and hidden inputs. */
30
+ export type TPktFileUploadContext = { name?: string; multiple: boolean; id?: string }
31
+ export const PktFileUploadContext = createContext<TPktFileUploadContext>({} as TPktFileUploadContext)
32
+
33
+ /** The value type for `PktFileUpload` (`value` / `defaultValue`). */
34
+ export type TFileItemList = Array<FileItem>
35
+
36
+ /**
37
+ * Transfer status for a file when using `uploadStrategy="custom"`.
38
+ *
39
+ * - `progress`: `0..1` during upload, or a state (`queued`, `done`, `error`, `canceled`)
40
+ * - `showProgress`: if false, UI uses indeterminate "Laster opp..." style instead of progress bar
41
+ * - `lastProgress`: used to render a "failed at X%" bar when an upload errors
42
+ */
43
+ export type TFileTransfer = {
44
+ fileId: TFileId
45
+ progress: number | 'done' | 'error' | 'canceled' | 'queued' // i tilfelle number: 0-1
46
+ errorMessage?: string
47
+ showProgress?: boolean // true = show progress bar/file size, false = show "Laster opp..."/just error message
48
+ lastProgress?: number // Store progress value when error occurs (0-1)
49
+ }
50
+
51
+ /** Upload mode: `form` posts the files on submit, `custom` posts file IDs and uploads separately. */
52
+ export type TUploadStrategy = 'custom' | 'form'
53
+ export type TFileAttribute<T> = {
54
+ get: (fileId: TFileId) => T | undefined
55
+ set: (fileId: TFileId, attributeValue: T | undefined) => void
56
+ }
57
+ export type TFileAttributes = <T>(attributeName: string) => TFileAttribute<T>
58
+
59
+ /**
60
+ * An operation that can be attached to a queue item (rename, comment, remove, etc).
61
+ *
62
+ * An operation may:
63
+ * - be a simple action (`onClick`)
64
+ * - render inline UI (e.g. rename)
65
+ * - render extended UI (e.g. comments)
66
+ * - render hidden inputs for form submission (`renderHidden`)
67
+ */
68
+ export type TQueueItemOperation = {
69
+ title: string | ((fileItem: FileItem) => string)
70
+ ariaLabel?: string | ((fileItem: FileItem) => string)
71
+ onClick?: (transferItem: FileItem) => void
72
+ renderInlineUI?: (fileItem: FileItem, closeOperationUi: () => void) => ReactNode
73
+ renderExtendedUI?: (fileItem: FileItem, closeOperationUi: () => void) => ReactNode
74
+ renderContent?: (fileItem: FileItem, activateOperation?: () => void, isOperationActive?: boolean) => ReactNode
75
+ renderHidden?: (fileItem: FileItem) => ReactNode
76
+ symbol: symbol
77
+ }
78
+
79
+ /** Factory that produces a queue item operation given access to file attributes. */
80
+ export type TQueueItemExtension = (attributes: TFileAttributes) => TQueueItemOperation
81
+ export type TFileAndTransfer = FileItem &
82
+ Pick<TFileTransfer, 'progress' | 'errorMessage' | 'showProgress' | 'lastProgress'>
83
+ export type TTransferItemInProgress = TFileAndTransfer & { progress: number }
84
+
85
+ /**
86
+ * Custom renderer for how a queue item is displayed (e.g. filename vs thumbnail grid).
87
+ *
88
+ * Must return a renderable React element (or `null`).
89
+ */
90
+ export type TItemRenderer = (props: {
91
+ transferItem: TFileAndTransfer
92
+ queueItemOperations: Array<TQueueItemOperation>
93
+ onPreviewClick?: () => void
94
+ }) => ReactElement | null