@oslokommune/punkt-elements 16.14.0 → 16.15.1
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/CHANGELOG.md +34 -1
- package/dist/calendar-Ch1dgnBo.js +656 -0
- package/dist/calendar-DkjXla0I.cjs +90 -0
- package/dist/{card-CQYxrtL0.cjs → card-C3POnP3O.cjs} +1 -1
- package/dist/{card-Bir3-pl2.js → card-CEDbo1t7.js} +2 -2
- package/dist/{checkbox-Cspxcvt_.js → checkbox-Bjp43NxA.js} +1 -1
- package/dist/{checkbox-BARDb9iV.cjs → checkbox-nTbihMxW.cjs} +1 -1
- package/dist/{combobox-CstFVsy1.cjs → combobox-C4Q6kQEs.cjs} +1 -1
- package/dist/{combobox-DtYloYZq.js → combobox-CL9BLuxt.js} +6 -6
- package/dist/components/fileupload/fileupload-base.d.ts +116 -0
- package/dist/components/fileupload/fileupload-operations.d.ts +4 -0
- package/dist/components/fileupload/fileupload-renderers.d.ts +28 -0
- package/dist/components/fileupload/fileupload-types.d.ts +70 -0
- package/dist/components/fileupload/fileupload.d.ts +25 -0
- package/dist/components/fileupload/fileupload.test.d.ts +0 -0
- package/dist/components/fileupload/index.d.ts +4 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/converters-Bffq_YDm.cjs +1 -0
- package/dist/converters-jauYkvNh.js +1378 -0
- package/dist/{datepicker-gO2EuaBX.js → datepicker-BUK1Qanw.js} +22 -21
- package/dist/{datepicker-DfSdjIjl.cjs → datepicker-DYd3JCs_.cjs} +10 -10
- package/dist/docs/components/fileupload.d.ts +27 -0
- package/dist/fileupload-DbmqRkz3.js +1373 -0
- package/dist/fileupload-DjUAvBcP.cjs +396 -0
- package/dist/fileupload.d.ts +1 -0
- package/dist/{header-DpU4HYdj.cjs → header-Dps8o-ZY.cjs} +1 -1
- package/dist/{header-BHrgTCxj.js → header-T5O6sfCN.js} +3 -3
- package/dist/{helptext-CCBKKLus.js → helptext-C5rG4VkA.js} +2 -2
- package/dist/{helptext-D-3QfZv5.cjs → helptext-DQgStO4D.cjs} +1 -1
- package/dist/{input-element-Bw1s6NlF.js → input-element-CjebcQd1.js} +3 -3
- package/dist/{input-element-B1yxcfz4.cjs → input-element-FGcr5hGd.cjs} +1 -1
- package/dist/{input-wrapper-CzS4rUrK.cjs → input-wrapper-B_XFi211.cjs} +1 -1
- package/dist/{input-wrapper-XuUnGN_U.js → input-wrapper-DBpMVSxt.js} +4 -4
- package/dist/{listbox-DuBgTFvH.js → listbox-0NHJvNjz.js} +2 -2
- package/dist/{listbox-DCA2JoPV.cjs → listbox-CcThiNBY.cjs} +1 -1
- package/dist/{modal-CBEWrAi2.cjs → modal-B6591sWa.cjs} +1 -1
- package/dist/{modal-pPp8nia6.js → modal-DyIC3uhs.js} +1 -1
- package/dist/pkt-calendar.cjs +1 -1
- package/dist/pkt-calendar.js +1 -1
- package/dist/pkt-card.cjs +1 -1
- package/dist/pkt-card.js +1 -1
- package/dist/pkt-checkbox.cjs +1 -1
- package/dist/pkt-checkbox.js +1 -1
- package/dist/pkt-combobox.cjs +1 -1
- package/dist/pkt-combobox.js +1 -1
- package/dist/pkt-consent.cjs +1 -1
- package/dist/pkt-consent.js +1 -1
- package/dist/pkt-datepicker.cjs +1 -1
- package/dist/pkt-datepicker.js +1 -1
- package/dist/pkt-fileupload.cjs +1 -0
- package/dist/pkt-fileupload.js +2 -0
- package/dist/pkt-header.cjs +1 -1
- package/dist/pkt-header.js +1 -1
- package/dist/pkt-heading.cjs +1 -1
- package/dist/pkt-heading.js +1 -1
- package/dist/pkt-helptext.cjs +1 -1
- package/dist/pkt-helptext.js +1 -1
- package/dist/pkt-index.cjs +1 -1
- package/dist/pkt-index.js +25 -24
- package/dist/pkt-input-wrapper.cjs +1 -1
- package/dist/pkt-input-wrapper.js +1 -1
- package/dist/pkt-link.cjs +1 -1
- package/dist/pkt-link.js +1 -1
- package/dist/pkt-linkcard.cjs +1 -1
- package/dist/pkt-linkcard.js +1 -1
- package/dist/pkt-listbox.cjs +1 -1
- package/dist/pkt-listbox.js +1 -1
- package/dist/pkt-loader.cjs +1 -1
- package/dist/pkt-loader.js +1 -1
- package/dist/pkt-messagebox.cjs +1 -1
- package/dist/pkt-messagebox.js +1 -1
- package/dist/pkt-modal.cjs +1 -1
- package/dist/pkt-modal.js +1 -1
- package/dist/pkt-progressbar.cjs +1 -1
- package/dist/pkt-progressbar.js +1 -1
- package/dist/pkt-radiobutton.cjs +1 -1
- package/dist/pkt-radiobutton.js +1 -1
- package/dist/pkt-select.cjs +1 -1
- package/dist/pkt-select.js +1 -1
- package/dist/pkt-tabs.cjs +1 -1
- package/dist/pkt-tabs.js +1 -1
- package/dist/pkt-tag.cjs +1 -1
- package/dist/pkt-tag.js +1 -1
- package/dist/pkt-textarea.cjs +1 -1
- package/dist/pkt-textarea.js +1 -1
- package/dist/pkt-textinput.cjs +1 -1
- package/dist/pkt-textinput.js +1 -1
- package/dist/pkt-timepicker.cjs +1 -1
- package/dist/pkt-timepicker.js +1 -1
- package/dist/{progressbar-CdXtmOBb.cjs → progressbar-CrwpnIkv.cjs} +1 -1
- package/dist/{progressbar-Dy9AiYQz.js → progressbar-uC-0W6o4.js} +1 -1
- package/dist/{radiobutton-V_KPbup_.js → radiobutton-B-8-wWFf.js} +1 -1
- package/dist/{radiobutton-GOnpUbCW.cjs → radiobutton-DvXr7tDk.cjs} +1 -1
- package/dist/{select-Cwzq_O8S.cjs → select-B75wzvyz.cjs} +1 -1
- package/dist/{select-Bo-w84pI.js → select-D9B8Ighz.js} +3 -3
- package/dist/shared-types/fileupload.d.ts +73 -0
- package/dist/shared-types/index.d.ts +2 -0
- package/dist/shared-utils/fileupload/announcements.d.ts +11 -0
- package/dist/shared-utils/fileupload/filename.d.ts +13 -0
- package/dist/shared-utils/fileupload/focus-trap.d.ts +12 -0
- package/dist/shared-utils/fileupload/formats.d.ts +30 -0
- package/dist/shared-utils/fileupload/index.d.ts +15 -0
- package/dist/shared-utils/fileupload/navigation.d.ts +5 -0
- package/dist/shared-utils/fileupload/size.d.ts +12 -0
- package/dist/shared-utils/fileupload/transfers.d.ts +17 -0
- package/dist/shared-utils/fileupload/truncate.d.ts +13 -0
- package/dist/shared-utils/fileupload/validation.d.ts +24 -0
- package/dist/tests/component-registry.d.ts +2 -0
- package/dist/{textarea-DYInBFHP.js → textarea-CIWXMaAD.js} +2 -2
- package/dist/{textarea-CbqiuSE8.cjs → textarea-CjqprL6Y.cjs} +1 -1
- package/dist/{textinput-Bp-UeGOq.cjs → textinput-B0VWmVIF.cjs} +1 -1
- package/dist/{textinput-DO9RldRk.js → textinput-CxzAxM8E.js} +2 -2
- package/dist/{timepicker-BpBcPB-r.cjs → timepicker-Cv9juQeB.cjs} +1 -1
- package/dist/{timepicker-D_XpQJXa.js → timepicker-DXrpg5C7.js} +2 -2
- package/package.json +2 -2
- package/src/components/fileupload/fileupload-base.ts +558 -0
- package/src/components/fileupload/fileupload-operations.ts +213 -0
- package/src/components/fileupload/fileupload-renderers.ts +769 -0
- package/src/components/fileupload/fileupload-types.ts +88 -0
- package/src/components/fileupload/fileupload.test.ts +949 -0
- package/src/components/fileupload/fileupload.ts +469 -0
- package/src/components/fileupload/index.ts +34 -0
- package/src/components/index.ts +16 -0
- package/dist/calendar-BzEtOJla.js +0 -2031
- package/dist/calendar-E5jJLt8D.cjs +0 -90
- /package/dist/{booleanish-jJLlnr6-.cjs → booleanish-B5EFqFpJ.cjs} +0 -0
- /package/dist/{booleanish-BmcOU-43.js → booleanish-CRaKZ75t.js} +0 -0
- /package/dist/{consent-DeB_5sWH.js → consent-f_IGyHss.js} +0 -0
- /package/dist/{consent-hwu7pp3S.cjs → consent-zh2npq2K.cjs} +0 -0
- /package/dist/{dialog-polyfill.esm-1hPr0gl9.js → dialog-polyfill.esm-BmVHGRTX.js} +0 -0
- /package/dist/{dialog-polyfill.esm-D3Oinvk0.cjs → dialog-polyfill.esm-CXDsb3l-.cjs} +0 -0
- /package/dist/{heading-DiQLPxdf.js → heading-BxvE80bb.js} +0 -0
- /package/dist/{heading-BgSWIRii.cjs → heading-DRJTS27k.cjs} +0 -0
- /package/dist/{input-wrapper-Dwd-_sKm.cjs → input-wrapper-CYCCx8MP.cjs} +0 -0
- /package/dist/{input-wrapper-CDyHTR1V.js → input-wrapper-kw2y6Jp_.js} +0 -0
- /package/dist/{link-CyTCpI0k.cjs → link-B8zYc5WK.cjs} +0 -0
- /package/dist/{link-CKAFMz9j.js → link-CjJcwcxR.js} +0 -0
- /package/dist/{linkcard-BE4-FewK.cjs → linkcard-C4x-NJMi.cjs} +0 -0
- /package/dist/{linkcard-B0KOGaGj.js → linkcard-Ddh5R_L-.js} +0 -0
- /package/dist/{loader-CX2rsC01.cjs → loader-CGQXAuyM.cjs} +0 -0
- /package/dist/{loader-6kFZUErT.js → loader-DeizpX45.js} +0 -0
- /package/dist/{messagebox-Din9X53b.cjs → messagebox-Ccu29rDv.cjs} +0 -0
- /package/dist/{messagebox-Cwlg5sOr.js → messagebox-DqoPqo4K.js} +0 -0
- /package/dist/{pkt-options-controller-CXUGx134.cjs → pkt-options-controller-BPBb_O2K.cjs} +0 -0
- /package/dist/{pkt-options-controller-CRrI6WMk.js → pkt-options-controller-DbLrdRwH.js} +0 -0
- /package/dist/{repeat-Cepr3469.js → repeat-DV1O43YU.js} +0 -0
- /package/dist/{repeat-CqcDYBxr.cjs → repeat-DaDFvx86.cjs} +0 -0
- /package/dist/{tabs-Rc9N9HWa.js → tabs-4eWA7CsK.js} +0 -0
- /package/dist/{tabs-BfurI6C3.cjs → tabs-RnDtIFv3.cjs} +0 -0
- /package/dist/{tag-DLthw5b4.cjs → tag-DWtajZk6.cjs} +0 -0
- /package/dist/{tag-VwykmuQR.js → tag-hl59gziI.js} +0 -0
- /package/dist/{utils-CUqCKGvp.js → utils-BOOZ0ppt.js} +0 -0
- /package/dist/{utils-CjAbIttF.cjs → utils-CM67Oudc.cjs} +0 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { PropertyValues } from 'lit'
|
|
2
|
+
import { property, state } from 'lit/decorators.js'
|
|
3
|
+
|
|
4
|
+
import { defaultFileUploadStrings } from 'shared-types'
|
|
5
|
+
import {
|
|
6
|
+
countFileTransferStates,
|
|
7
|
+
formatFileSize,
|
|
8
|
+
getSupportedFormatsText as sharedGetSupportedFormatsText,
|
|
9
|
+
isImageFileLike,
|
|
10
|
+
mergeFilesAndTransfers,
|
|
11
|
+
resolveAcceptAttribute,
|
|
12
|
+
validateFile as sharedValidateFile,
|
|
13
|
+
} from 'shared-utils/fileupload'
|
|
14
|
+
|
|
15
|
+
import { PktElement } from '@/base-elements/element'
|
|
16
|
+
import converters from '../../helpers/converters'
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
FileItem,
|
|
20
|
+
IPktFileUpload,
|
|
21
|
+
TQueueItemOperation,
|
|
22
|
+
TFileTransfer,
|
|
23
|
+
TFileUploadItemRenderer,
|
|
24
|
+
TFileValidateDetail,
|
|
25
|
+
TFilesChangedReason,
|
|
26
|
+
TTransferCancelledDetail,
|
|
27
|
+
TTransferProgress,
|
|
28
|
+
TUploadStrategy,
|
|
29
|
+
} from './fileupload-types'
|
|
30
|
+
import { uuidish } from 'shared-utils/utils'
|
|
31
|
+
|
|
32
|
+
export abstract class PktFileUploadBase
|
|
33
|
+
extends PktElement<IPktFileUpload>
|
|
34
|
+
implements IPktFileUpload
|
|
35
|
+
{
|
|
36
|
+
/** Unique id used for internal input + wrapper wiring. Defaults to a per-instance uuid. */
|
|
37
|
+
@property({ type: String }) id: string = `pkt-fileupload-${uuidish()}`
|
|
38
|
+
/** Field name used by native input (`form`) or hidden fileId inputs (`custom`). */
|
|
39
|
+
@property({ type: String }) name: string = 'files'
|
|
40
|
+
/** Optional label shown by `pkt-input-wrapper`. */
|
|
41
|
+
@property({ type: String }) label: string = ''
|
|
42
|
+
/** Optional help text shown under label. */
|
|
43
|
+
@property({ type: String }) helptext: string = ''
|
|
44
|
+
/** Mark input as required in form strategy. */
|
|
45
|
+
@property({ type: Boolean, reflect: true }) required = false
|
|
46
|
+
/** Allow selecting/dropping multiple files. */
|
|
47
|
+
@property({ type: Boolean, reflect: true }) multiple = false
|
|
48
|
+
/** Disable all interaction in drop zone + queue actions. */
|
|
49
|
+
@property({ type: Boolean, reflect: true }) disabled = false
|
|
50
|
+
/** Stretch component width to container. */
|
|
51
|
+
@property({ type: Boolean, reflect: true, attribute: 'fullwidth' }) fullwidth = false
|
|
52
|
+
/** Upload mode: `form` uses native file submit, `custom` emits upload request events per file. */
|
|
53
|
+
@property({ type: String, reflect: true, attribute: 'upload-strategy' })
|
|
54
|
+
uploadStrategy: TUploadStrategy = 'form'
|
|
55
|
+
/** Queue visual mode (`filename` or `thumbnail`). */
|
|
56
|
+
@property({ type: String, reflect: true, attribute: 'item-renderer' })
|
|
57
|
+
itemRenderer: TFileUploadItemRenderer = 'filename'
|
|
58
|
+
/** Native file input accept hint (browser picker filtering). */
|
|
59
|
+
@property({ type: String }) accept: string = ''
|
|
60
|
+
/** Built-in format validation source (csv attribute or array property). */
|
|
61
|
+
@property({ converter: converters.csvToArray, attribute: 'allowed-formats' })
|
|
62
|
+
allowedFormats: string[] = []
|
|
63
|
+
/** Optional custom format validation message. Supports `{formats}` placeholder. */
|
|
64
|
+
@property({ type: String, attribute: 'format-error-message' }) formatErrorMessage = ''
|
|
65
|
+
/** Max allowed file size (bytes or string like `500KB`, `5MB`). */
|
|
66
|
+
@property({ attribute: 'max-file-size' }) maxFileSize?: string | number
|
|
67
|
+
/** Optional custom size validation message. Supports `{maxSize}` placeholder. */
|
|
68
|
+
@property({ type: String, attribute: 'size-error-message' }) sizeErrorMessage = ''
|
|
69
|
+
/** Optional JS callback for custom validation. Property-only on purpose (not HTML-attribute friendly). */
|
|
70
|
+
@property({ attribute: false }) onFileValidation?: (file: File) => string | null
|
|
71
|
+
/** Transfer state list keyed by `fileId` (used for custom upload progress/error/cancel UI). */
|
|
72
|
+
@property({ attribute: false }) transfers: TFileTransfer[] = []
|
|
73
|
+
/** Enables built-in comment operation in queue items (disabled in thumbnail view for parity with React). */
|
|
74
|
+
@property({ type: Boolean, attribute: 'add-comments-enabled' }) addCommentsEnabled = false
|
|
75
|
+
/** Enables built-in rename operation in queue items (disabled in thumbnail view for parity with React). */
|
|
76
|
+
@property({ type: Boolean, attribute: 'rename-files-enabled' }) renameFilesEnabled = false
|
|
77
|
+
/** Custom queue operations (JS property only). Supports inline + expanded operation UIs. */
|
|
78
|
+
@property({ attribute: false }) extraOperations: TQueueItemOperation[] = []
|
|
79
|
+
/** Toggle image thumbnail behavior in queue when renderer supports it. */
|
|
80
|
+
@property({ type: Boolean, reflect: true, attribute: 'enable-image-preview' })
|
|
81
|
+
enableImagePreview = false
|
|
82
|
+
/** External error flag — combines with internal validation errors. */
|
|
83
|
+
@property({ type: Boolean, attribute: 'has-error' }) hasError = false
|
|
84
|
+
/** External error message shown in the alert under the drop zone. */
|
|
85
|
+
@property({ type: String, attribute: 'error-message' }) errorMessage = ''
|
|
86
|
+
/** Show "Valgfritt" tag in the input wrapper. */
|
|
87
|
+
@property({ type: Boolean, attribute: 'optional-tag' }) optionalTag = false
|
|
88
|
+
/** Show "Må fylles ut" tag in the input wrapper. */
|
|
89
|
+
@property({ type: Boolean, attribute: 'required-tag' }) requiredTag = false
|
|
90
|
+
/** Trailing characters to keep when middle-truncating long filenames. Set to `0` to disable. */
|
|
91
|
+
@property({ type: Number, attribute: 'truncate-tail' }) truncateTail: number = 4
|
|
92
|
+
/** Controlled mode source of truth. Parent owns file list and updates this prop from events. */
|
|
93
|
+
@property({ attribute: false }) value?: FileItem[]
|
|
94
|
+
/** Uncontrolled initial file list. Used once during first initialization. */
|
|
95
|
+
@property({ attribute: false }) defaultValue?: FileItem[]
|
|
96
|
+
|
|
97
|
+
@state() protected files: FileItem[] = []
|
|
98
|
+
@state() protected isDragActive = false
|
|
99
|
+
@state() protected validationErrorMessage: string | null = null
|
|
100
|
+
@state() protected addedAnnouncement = ''
|
|
101
|
+
@state() private thumbnailUrls: Record<string, string> = {}
|
|
102
|
+
|
|
103
|
+
private hasInitializedValue = false
|
|
104
|
+
private hasWarnedInvalidValueCombo = false
|
|
105
|
+
private thumbnailFileById = new Map<string, File>()
|
|
106
|
+
private connectedForm: HTMLFormElement | null = null
|
|
107
|
+
private addedAnnouncementTimer: ReturnType<typeof setTimeout> | null = null
|
|
108
|
+
private readonly requiredSelectionMessage = defaultFileUploadStrings.requiredMissing
|
|
109
|
+
|
|
110
|
+
protected get isControlled() {
|
|
111
|
+
return this.value !== undefined
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** True if the component itself wants to show an error (internal validation or external prop). */
|
|
115
|
+
protected get hasEffectiveError() {
|
|
116
|
+
return !!this.validationErrorMessage || this.hasError || !!this.errorMessage.trim()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Message to show in the alert under the drop zone. Internal validation takes priority. */
|
|
120
|
+
protected get effectiveErrorMessage(): string {
|
|
121
|
+
return this.validationErrorMessage ?? this.errorMessage ?? ''
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
protected get hasValidationError() {
|
|
125
|
+
return !!this.validationErrorMessage
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
protected firstUpdated(changedProperties: PropertyValues): void {
|
|
129
|
+
super.firstUpdated?.(changedProperties)
|
|
130
|
+
if (!this.hasInitializedValue) {
|
|
131
|
+
this.files = this.value ?? this.defaultValue ?? []
|
|
132
|
+
this.syncThumbnailUrls(this.files)
|
|
133
|
+
this.hasInitializedValue = true
|
|
134
|
+
}
|
|
135
|
+
if (this.value !== undefined && this.defaultValue !== undefined) {
|
|
136
|
+
this.warnInvalidValueCombo()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
protected willUpdate(changedProperties: PropertyValues): void {
|
|
141
|
+
super.willUpdate(changedProperties)
|
|
142
|
+
if (!this.hasInitializedValue) return
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
this.isControlled &&
|
|
146
|
+
(changedProperties.has('value') ||
|
|
147
|
+
changedProperties.has('itemRenderer') ||
|
|
148
|
+
changedProperties.has('enableImagePreview'))
|
|
149
|
+
) {
|
|
150
|
+
this.files = this.value ?? []
|
|
151
|
+
this.syncThumbnailUrls(this.files)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
protected updated(changedProperties: PropertyValues): void {
|
|
156
|
+
super.updated(changedProperties)
|
|
157
|
+
if (
|
|
158
|
+
changedProperties.has('uploadStrategy') ||
|
|
159
|
+
changedProperties.has('required') ||
|
|
160
|
+
changedProperties.has('value')
|
|
161
|
+
) {
|
|
162
|
+
this.syncFormSubmitListener()
|
|
163
|
+
}
|
|
164
|
+
if (
|
|
165
|
+
this.uploadStrategy === 'form' &&
|
|
166
|
+
(changedProperties.has('value') ||
|
|
167
|
+
changedProperties.has('files') ||
|
|
168
|
+
changedProperties.has('uploadStrategy'))
|
|
169
|
+
) {
|
|
170
|
+
this.syncNativeInputFiles()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private syncNativeInputFiles() {
|
|
175
|
+
const input = this.querySelector<HTMLInputElement>(`#${this.id}-input`)
|
|
176
|
+
if (!input || typeof DataTransfer === 'undefined') return
|
|
177
|
+
try {
|
|
178
|
+
const dataTransfer = new DataTransfer()
|
|
179
|
+
for (const item of this.getCurrentFiles()) {
|
|
180
|
+
if (item.file) dataTransfer.items.add(item.file)
|
|
181
|
+
}
|
|
182
|
+
input.files = dataTransfer.files
|
|
183
|
+
} catch {
|
|
184
|
+
// jsdom and some test environments do not implement DataTransfer; not critical.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private warnInvalidValueCombo() {
|
|
189
|
+
if (this.hasWarnedInvalidValueCombo) return
|
|
190
|
+
|
|
191
|
+
console.warn(
|
|
192
|
+
'PktFileUpload: Både value og defaultValue er angitt. Komponenten kan være enten kontrollert eller ukontrollert, ikke begge. value vil bli prioritert.',
|
|
193
|
+
)
|
|
194
|
+
this.hasWarnedInvalidValueCombo = true
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
disconnectedCallback(): void {
|
|
198
|
+
this.teardownFormSubmitListener()
|
|
199
|
+
if (this.addedAnnouncementTimer) {
|
|
200
|
+
clearTimeout(this.addedAnnouncementTimer)
|
|
201
|
+
this.addedAnnouncementTimer = null
|
|
202
|
+
}
|
|
203
|
+
super.disconnectedCallback()
|
|
204
|
+
this.revokeAllThumbnailUrls()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private setAddedAnnouncement(message: string) {
|
|
208
|
+
this.addedAnnouncement = message
|
|
209
|
+
if (this.addedAnnouncementTimer) clearTimeout(this.addedAnnouncementTimer)
|
|
210
|
+
this.addedAnnouncementTimer = setTimeout(() => {
|
|
211
|
+
this.addedAnnouncement = ''
|
|
212
|
+
this.addedAnnouncementTimer = null
|
|
213
|
+
}, 1500)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
connectedCallback(): void {
|
|
217
|
+
super.connectedCallback()
|
|
218
|
+
this.syncFormSubmitListener()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
protected onNativeFileChange = (event: Event) => {
|
|
222
|
+
const target = event.target as HTMLInputElement
|
|
223
|
+
this.addFiles(target.files ? Array.from(target.files) : [])
|
|
224
|
+
// In custom mode we do not submit native file input values, so we can clear the input
|
|
225
|
+
// to allow selecting the same file again. In form mode we keep native files for form submit.
|
|
226
|
+
if (this.uploadStrategy === 'custom') {
|
|
227
|
+
target.value = ''
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
protected openFileDialog = (event: Event) => {
|
|
232
|
+
event.preventDefault()
|
|
233
|
+
event.stopPropagation()
|
|
234
|
+
if (this.disabled) return
|
|
235
|
+
const input = this.querySelector<HTMLInputElement>(`#${this.id}-input`)
|
|
236
|
+
input?.click()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
protected onDropZoneClick = (event: MouseEvent) => {
|
|
240
|
+
if (this.disabled) return
|
|
241
|
+
const target = event.target as HTMLElement
|
|
242
|
+
if (target.closest('.pkt-fileupload__drop-zone__placeholder__title__open-file-dialog')) return
|
|
243
|
+
const input = this.querySelector<HTMLInputElement>(`#${this.id}-input`)
|
|
244
|
+
input?.click()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
protected onDragOver = (event: DragEvent) => {
|
|
248
|
+
event.preventDefault()
|
|
249
|
+
if (this.disabled) return
|
|
250
|
+
this.isDragActive = true
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
protected onDragLeave = () => {
|
|
254
|
+
this.isDragActive = false
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
protected onDrop = (event: DragEvent) => {
|
|
258
|
+
event.preventDefault()
|
|
259
|
+
this.isDragActive = false
|
|
260
|
+
if (this.disabled) return
|
|
261
|
+
const droppedFiles = event.dataTransfer?.files ? Array.from(event.dataTransfer.files) : []
|
|
262
|
+
this.addFiles(droppedFiles)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
protected addFiles(selectedFiles: File[]) {
|
|
266
|
+
if (selectedFiles.length === 0) return
|
|
267
|
+
const normalizedFiles = this.multiple ? selectedFiles : [selectedFiles[0]]
|
|
268
|
+
|
|
269
|
+
for (const file of normalizedFiles) {
|
|
270
|
+
const validationError = this.validateFile(file)
|
|
271
|
+
if (validationError) {
|
|
272
|
+
this.validationErrorMessage = validationError
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this.validationErrorMessage = null
|
|
278
|
+
const newItems: FileItem[] = normalizedFiles.map((file) => ({
|
|
279
|
+
fileId: uuidish(),
|
|
280
|
+
file,
|
|
281
|
+
attributes: {
|
|
282
|
+
targetFilename: file.name,
|
|
283
|
+
},
|
|
284
|
+
}))
|
|
285
|
+
|
|
286
|
+
this.setAddedAnnouncement(
|
|
287
|
+
newItems.length === 1
|
|
288
|
+
? defaultFileUploadStrings.srFileAdded(newItems[0].attributes.targetFilename)
|
|
289
|
+
: defaultFileUploadStrings.srFilesAdded(newItems.length),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
const currentFiles = this.getCurrentFiles()
|
|
293
|
+
const nextFiles = this.multiple ? [...currentFiles, ...newItems] : [newItems[0]]
|
|
294
|
+
this.commitFiles(
|
|
295
|
+
nextFiles,
|
|
296
|
+
'add',
|
|
297
|
+
newItems.map((item) => item.fileId),
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if (this.uploadStrategy === 'custom') {
|
|
301
|
+
newItems.forEach((fileItem) => {
|
|
302
|
+
this.dispatchEvent(
|
|
303
|
+
new CustomEvent('file-upload-requested', {
|
|
304
|
+
detail: {
|
|
305
|
+
fileId: fileItem.fileId,
|
|
306
|
+
file: fileItem.file,
|
|
307
|
+
attributes: fileItem.attributes,
|
|
308
|
+
},
|
|
309
|
+
bubbles: true,
|
|
310
|
+
composed: true,
|
|
311
|
+
}),
|
|
312
|
+
)
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
protected getCurrentFiles(): FileItem[] {
|
|
318
|
+
return this.isControlled ? (this.value ?? []) : this.files
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
protected cancelTransfer(fileId: string) {
|
|
322
|
+
if (this.disabled) return
|
|
323
|
+
const currentItem = this.getCurrentFiles().find((item) => item.fileId === fileId)
|
|
324
|
+
if (!currentItem) return
|
|
325
|
+
|
|
326
|
+
this.removeFileItem(fileId)
|
|
327
|
+
|
|
328
|
+
// CE-native deviation from React callback props: emit a bubbling event for host-driven cancel/delete logic.
|
|
329
|
+
this.dispatchEvent(
|
|
330
|
+
new CustomEvent<TTransferCancelledDetail>('transfer-cancelled', {
|
|
331
|
+
detail: {
|
|
332
|
+
fileId: currentItem.fileId,
|
|
333
|
+
file: currentItem.file,
|
|
334
|
+
attributes: currentItem.attributes,
|
|
335
|
+
},
|
|
336
|
+
bubbles: true,
|
|
337
|
+
composed: true,
|
|
338
|
+
}),
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
protected removeFileItem(fileId: string) {
|
|
343
|
+
if (this.disabled) return
|
|
344
|
+
const currentFiles = this.getCurrentFiles()
|
|
345
|
+
const nextFiles = currentFiles.filter((item) => item.fileId !== fileId)
|
|
346
|
+
if (nextFiles.length === currentFiles.length) return
|
|
347
|
+
this.commitFiles(nextFiles, 'remove', [fileId])
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// CE-native deviation: this is a method-based update hook instead of React callback props.
|
|
351
|
+
// It keeps queue lifecycle explicit without introducing React-specific abstractions.
|
|
352
|
+
public updateFileItem(fileId: string, updates: Partial<FileItem>) {
|
|
353
|
+
const currentFiles = this.getCurrentFiles()
|
|
354
|
+
const nextFiles = currentFiles.map((item) =>
|
|
355
|
+
item.fileId === fileId ? { ...item, ...updates } : item,
|
|
356
|
+
)
|
|
357
|
+
this.commitFiles(nextFiles, 'update', [fileId])
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
protected commitFiles(
|
|
361
|
+
nextFiles: FileItem[],
|
|
362
|
+
reason: TFilesChangedReason,
|
|
363
|
+
changedFileIds?: string[],
|
|
364
|
+
) {
|
|
365
|
+
if (!this.isControlled) {
|
|
366
|
+
this.files = nextFiles
|
|
367
|
+
this.syncThumbnailUrls(nextFiles)
|
|
368
|
+
}
|
|
369
|
+
if (this.validationErrorMessage === this.requiredSelectionMessage && nextFiles.length > 0) {
|
|
370
|
+
this.validationErrorMessage = null
|
|
371
|
+
}
|
|
372
|
+
this.dispatchEvent(
|
|
373
|
+
new CustomEvent('files-changed', {
|
|
374
|
+
detail: { files: nextFiles, reason, changedFileIds },
|
|
375
|
+
bubbles: true,
|
|
376
|
+
composed: true,
|
|
377
|
+
}),
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
protected getThumbnailUrl(file: FileItem): string | undefined {
|
|
382
|
+
return this.thumbnailUrls[file.fileId]
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
protected getTransferProgress(fileId: string): TTransferProgress {
|
|
386
|
+
const transfer = this.transfers.find((item) => item.fileId === fileId)
|
|
387
|
+
if (transfer) return transfer.progress
|
|
388
|
+
return this.uploadStrategy === 'form' ? 'done' : 'queued'
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
protected getTransferForFile(fileId: string): TFileTransfer | undefined {
|
|
392
|
+
return this.transfers.find((item) => item.fileId === fileId)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
protected isImageFile(file: FileItem): boolean {
|
|
396
|
+
return isImageFileLike(file)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
protected formatFileSize(bytes: number): string {
|
|
400
|
+
return formatFileSize(bytes)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
protected getResolvedAcceptValue(): string {
|
|
404
|
+
return resolveAcceptAttribute(this.accept, this.allowedFormats)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
protected getSupportedFormatsText(): string {
|
|
408
|
+
return sharedGetSupportedFormatsText(this.allowedFormats, this.accept)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
protected getSortedFilesAndTransfers() {
|
|
412
|
+
return mergeFilesAndTransfers(this.getCurrentFiles(), this.transfers, this.uploadStrategy)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private validateFile(file: File): string | null {
|
|
416
|
+
const baseError = sharedValidateFile(file, {
|
|
417
|
+
allowedFormats: this.allowedFormats,
|
|
418
|
+
maxFileSize: this.maxFileSize,
|
|
419
|
+
formatErrorMessage: this.formatErrorMessage,
|
|
420
|
+
sizeErrorMessage: this.sizeErrorMessage,
|
|
421
|
+
onFileValidation: this.onFileValidation,
|
|
422
|
+
})
|
|
423
|
+
if (baseError) return baseError
|
|
424
|
+
|
|
425
|
+
const eventDetail: TFileValidateDetail = { file, errorMessage: null }
|
|
426
|
+
// CE-native deviation from React: callbacks cannot be passed declaratively in HTML,
|
|
427
|
+
// so we expose an event hook where consumers can set detail.errorMessage.
|
|
428
|
+
const validationEvent = new CustomEvent<TFileValidateDetail>('file-validate', {
|
|
429
|
+
detail: eventDetail,
|
|
430
|
+
bubbles: true,
|
|
431
|
+
composed: true,
|
|
432
|
+
cancelable: true,
|
|
433
|
+
})
|
|
434
|
+
this.dispatchEvent(validationEvent)
|
|
435
|
+
|
|
436
|
+
if (eventDetail.errorMessage) return eventDetail.errorMessage
|
|
437
|
+
if (validationEvent.defaultPrevented) return defaultFileUploadStrings.genericValidationRejection
|
|
438
|
+
return null
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private syncThumbnailUrls(nextFiles: FileItem[]) {
|
|
442
|
+
const supportsObjectUrl =
|
|
443
|
+
typeof URL !== 'undefined' &&
|
|
444
|
+
typeof URL.createObjectURL === 'function' &&
|
|
445
|
+
typeof URL.revokeObjectURL === 'function'
|
|
446
|
+
|
|
447
|
+
if (!supportsObjectUrl) {
|
|
448
|
+
this.thumbnailUrls = {}
|
|
449
|
+
this.thumbnailFileById.clear()
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const nextIds = new Set(nextFiles.map((item) => item.fileId))
|
|
454
|
+
const nextUrlMap: Record<string, string> = {}
|
|
455
|
+
|
|
456
|
+
for (const [fileId, url] of Object.entries(this.thumbnailUrls)) {
|
|
457
|
+
if (!nextIds.has(fileId)) {
|
|
458
|
+
URL.revokeObjectURL(url)
|
|
459
|
+
this.thumbnailFileById.delete(fileId)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
for (const item of nextFiles) {
|
|
464
|
+
const previousUrl = this.thumbnailUrls[item.fileId]
|
|
465
|
+
const previousFile = this.thumbnailFileById.get(item.fileId)
|
|
466
|
+
|
|
467
|
+
if (!item.file || !this.isImageFile(item)) {
|
|
468
|
+
if (previousUrl) {
|
|
469
|
+
URL.revokeObjectURL(previousUrl)
|
|
470
|
+
this.thumbnailFileById.delete(item.fileId)
|
|
471
|
+
}
|
|
472
|
+
continue
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (previousUrl && previousFile === item.file) {
|
|
476
|
+
nextUrlMap[item.fileId] = previousUrl
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (previousUrl && previousFile !== item.file) {
|
|
481
|
+
URL.revokeObjectURL(previousUrl)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const nextUrl = URL.createObjectURL(item.file)
|
|
485
|
+
this.thumbnailFileById.set(item.fileId, item.file)
|
|
486
|
+
nextUrlMap[item.fileId] = nextUrl
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
this.thumbnailUrls = nextUrlMap
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private revokeAllThumbnailUrls() {
|
|
493
|
+
if (typeof URL === 'undefined' || typeof URL.revokeObjectURL !== 'function') {
|
|
494
|
+
this.thumbnailUrls = {}
|
|
495
|
+
this.thumbnailFileById.clear()
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
Object.values(this.thumbnailUrls).forEach((url) => URL.revokeObjectURL(url))
|
|
500
|
+
this.thumbnailUrls = {}
|
|
501
|
+
this.thumbnailFileById.clear()
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Polite summary: prefer the transient "file added" announcement, then fall back to upload completion. */
|
|
505
|
+
protected getSrUploadedMessage(): string {
|
|
506
|
+
if (this.addedAnnouncement) return this.addedAnnouncement
|
|
507
|
+
const { totalCount, uploadedCount } = countFileTransferStates(
|
|
508
|
+
this.getCurrentFiles(),
|
|
509
|
+
this.transfers,
|
|
510
|
+
)
|
|
511
|
+
if (totalCount === 0 || uploadedCount === 0) return ''
|
|
512
|
+
return defaultFileUploadStrings.srFilesUploadedOfTotal(
|
|
513
|
+
uploadedCount,
|
|
514
|
+
totalCount,
|
|
515
|
+
defaultFileUploadStrings.fileLabel(totalCount),
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** Assertive summary for transfer errors (parity with React `FileUpload`). */
|
|
520
|
+
protected getSrErrorsMessage(): string {
|
|
521
|
+
const { totalCount, failedCount } = countFileTransferStates(
|
|
522
|
+
this.getCurrentFiles(),
|
|
523
|
+
this.transfers,
|
|
524
|
+
)
|
|
525
|
+
if (totalCount === 0 || failedCount === 0) return ''
|
|
526
|
+
return defaultFileUploadStrings.srFilesFailedOfTotal(
|
|
527
|
+
failedCount,
|
|
528
|
+
totalCount,
|
|
529
|
+
defaultFileUploadStrings.fileLabel(totalCount),
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private syncFormSubmitListener() {
|
|
534
|
+
const nextForm = this.closest('form')
|
|
535
|
+
if (this.connectedForm === nextForm) return
|
|
536
|
+
|
|
537
|
+
this.teardownFormSubmitListener()
|
|
538
|
+
this.connectedForm = nextForm
|
|
539
|
+
|
|
540
|
+
if (this.connectedForm) {
|
|
541
|
+
this.connectedForm.addEventListener('submit', this.onHostFormSubmit)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private teardownFormSubmitListener() {
|
|
546
|
+
if (!this.connectedForm) return
|
|
547
|
+
this.connectedForm.removeEventListener('submit', this.onHostFormSubmit)
|
|
548
|
+
this.connectedForm = null
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private onHostFormSubmit = (event: Event) => {
|
|
552
|
+
if (!this.required || this.uploadStrategy !== 'custom') return
|
|
553
|
+
if (this.getCurrentFiles().length > 0) return
|
|
554
|
+
|
|
555
|
+
this.validationErrorMessage = this.requiredSelectionMessage
|
|
556
|
+
event.preventDefault()
|
|
557
|
+
}
|
|
558
|
+
}
|