@oslokommune/punkt-elements 16.13.2 → 16.15.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.
Files changed (157) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/dist/calendar-Ch1dgnBo.js +656 -0
  3. package/dist/calendar-DkjXla0I.cjs +90 -0
  4. package/dist/{card-CQYxrtL0.cjs → card-C3POnP3O.cjs} +1 -1
  5. package/dist/{card-Bir3-pl2.js → card-CEDbo1t7.js} +2 -2
  6. package/dist/{checkbox-Cspxcvt_.js → checkbox-Bjp43NxA.js} +1 -1
  7. package/dist/{checkbox-BARDb9iV.cjs → checkbox-nTbihMxW.cjs} +1 -1
  8. package/dist/{combobox-CGbFTWq_.cjs → combobox-C4Q6kQEs.cjs} +1 -1
  9. package/dist/{combobox-BcdLZRDU.js → combobox-CL9BLuxt.js} +6 -6
  10. package/dist/components/fileupload/fileupload-base.d.ts +116 -0
  11. package/dist/components/fileupload/fileupload-operations.d.ts +4 -0
  12. package/dist/components/fileupload/fileupload-renderers.d.ts +28 -0
  13. package/dist/components/fileupload/fileupload-types.d.ts +70 -0
  14. package/dist/components/fileupload/fileupload.d.ts +25 -0
  15. package/dist/components/fileupload/fileupload.test.d.ts +0 -0
  16. package/dist/components/fileupload/index.d.ts +4 -0
  17. package/dist/components/index.d.ts +2 -0
  18. package/dist/components/input-wrapper/input-wrapper.d.ts +2 -0
  19. package/dist/controllers/focus-modality-controller.d.ts +31 -0
  20. package/dist/converters-Bffq_YDm.cjs +1 -0
  21. package/dist/converters-jauYkvNh.js +1378 -0
  22. package/dist/{datepicker-OVd8pmnZ.js → datepicker-BUK1Qanw.js} +22 -21
  23. package/dist/{datepicker-DpmlsnH_.cjs → datepicker-DYd3JCs_.cjs} +10 -10
  24. package/dist/docs/components/fileupload.d.ts +27 -0
  25. package/dist/fileupload-BRqioKSY.cjs +396 -0
  26. package/dist/fileupload-Bnl303X0.js +1372 -0
  27. package/dist/fileupload.d.ts +1 -0
  28. package/dist/{header-DWgPgGfS.cjs → header-Dps8o-ZY.cjs} +1 -1
  29. package/dist/{header-a6ztVNp3.js → header-T5O6sfCN.js} +3 -3
  30. package/dist/{helptext-CCBKKLus.js → helptext-C5rG4VkA.js} +2 -2
  31. package/dist/{helptext-D-3QfZv5.cjs → helptext-DQgStO4D.cjs} +1 -1
  32. package/dist/{input-element-Bw1s6NlF.js → input-element-CjebcQd1.js} +3 -3
  33. package/dist/{input-element-B1yxcfz4.cjs → input-element-FGcr5hGd.cjs} +1 -1
  34. package/dist/input-wrapper-B_XFi211.cjs +55 -0
  35. package/dist/{input-wrapper-YfTmpSSY.js → input-wrapper-DBpMVSxt.js} +41 -15
  36. package/dist/{listbox-DuBgTFvH.js → listbox-0NHJvNjz.js} +2 -2
  37. package/dist/{listbox-DCA2JoPV.cjs → listbox-CcThiNBY.cjs} +1 -1
  38. package/dist/{modal-CBEWrAi2.cjs → modal-B6591sWa.cjs} +1 -1
  39. package/dist/{modal-pPp8nia6.js → modal-DyIC3uhs.js} +1 -1
  40. package/dist/pkt-calendar.cjs +1 -1
  41. package/dist/pkt-calendar.js +1 -1
  42. package/dist/pkt-card.cjs +1 -1
  43. package/dist/pkt-card.js +1 -1
  44. package/dist/pkt-checkbox.cjs +1 -1
  45. package/dist/pkt-checkbox.js +1 -1
  46. package/dist/pkt-combobox.cjs +1 -1
  47. package/dist/pkt-combobox.js +1 -1
  48. package/dist/pkt-consent.cjs +1 -1
  49. package/dist/pkt-consent.js +1 -1
  50. package/dist/pkt-datepicker.cjs +1 -1
  51. package/dist/pkt-datepicker.js +1 -1
  52. package/dist/pkt-fileupload.cjs +1 -0
  53. package/dist/pkt-fileupload.js +2 -0
  54. package/dist/pkt-header.cjs +1 -1
  55. package/dist/pkt-header.js +1 -1
  56. package/dist/pkt-heading.cjs +1 -1
  57. package/dist/pkt-heading.js +1 -1
  58. package/dist/pkt-helptext.cjs +1 -1
  59. package/dist/pkt-helptext.js +1 -1
  60. package/dist/pkt-index.cjs +1 -1
  61. package/dist/pkt-index.js +25 -24
  62. package/dist/pkt-input-wrapper.cjs +1 -1
  63. package/dist/pkt-input-wrapper.js +1 -1
  64. package/dist/pkt-link.cjs +1 -1
  65. package/dist/pkt-link.js +1 -1
  66. package/dist/pkt-linkcard.cjs +1 -1
  67. package/dist/pkt-linkcard.js +1 -1
  68. package/dist/pkt-listbox.cjs +1 -1
  69. package/dist/pkt-listbox.js +1 -1
  70. package/dist/pkt-loader.cjs +1 -1
  71. package/dist/pkt-loader.js +1 -1
  72. package/dist/pkt-messagebox.cjs +1 -1
  73. package/dist/pkt-messagebox.js +1 -1
  74. package/dist/pkt-modal.cjs +1 -1
  75. package/dist/pkt-modal.js +1 -1
  76. package/dist/pkt-progressbar.cjs +1 -1
  77. package/dist/pkt-progressbar.js +1 -1
  78. package/dist/pkt-radiobutton.cjs +1 -1
  79. package/dist/pkt-radiobutton.js +1 -1
  80. package/dist/pkt-select.cjs +1 -1
  81. package/dist/pkt-select.js +1 -1
  82. package/dist/pkt-tabs.cjs +1 -1
  83. package/dist/pkt-tabs.js +1 -1
  84. package/dist/pkt-tag.cjs +1 -1
  85. package/dist/pkt-tag.js +1 -1
  86. package/dist/pkt-textarea.cjs +1 -1
  87. package/dist/pkt-textarea.js +1 -1
  88. package/dist/pkt-textinput.cjs +1 -1
  89. package/dist/pkt-textinput.js +1 -1
  90. package/dist/pkt-timepicker.cjs +1 -1
  91. package/dist/pkt-timepicker.js +1 -1
  92. package/dist/{progressbar-CdXtmOBb.cjs → progressbar-CrwpnIkv.cjs} +1 -1
  93. package/dist/{progressbar-Dy9AiYQz.js → progressbar-uC-0W6o4.js} +1 -1
  94. package/dist/{radiobutton-V_KPbup_.js → radiobutton-B-8-wWFf.js} +1 -1
  95. package/dist/{radiobutton-GOnpUbCW.cjs → radiobutton-DvXr7tDk.cjs} +1 -1
  96. package/dist/{select-C2i8sOk5.cjs → select-B75wzvyz.cjs} +1 -1
  97. package/dist/{select-BSAFd8Wa.js → select-D9B8Ighz.js} +3 -3
  98. package/dist/shared-types/fileupload.d.ts +73 -0
  99. package/dist/shared-types/index.d.ts +2 -0
  100. package/dist/shared-utils/fileupload/announcements.d.ts +11 -0
  101. package/dist/shared-utils/fileupload/filename.d.ts +13 -0
  102. package/dist/shared-utils/fileupload/focus-trap.d.ts +12 -0
  103. package/dist/shared-utils/fileupload/formats.d.ts +30 -0
  104. package/dist/shared-utils/fileupload/index.d.ts +15 -0
  105. package/dist/shared-utils/fileupload/navigation.d.ts +5 -0
  106. package/dist/shared-utils/fileupload/size.d.ts +12 -0
  107. package/dist/shared-utils/fileupload/transfers.d.ts +17 -0
  108. package/dist/shared-utils/fileupload/truncate.d.ts +13 -0
  109. package/dist/shared-utils/fileupload/validation.d.ts +24 -0
  110. package/dist/tests/component-registry.d.ts +2 -0
  111. package/dist/{textarea-DYOBhJrN.js → textarea-CIWXMaAD.js} +2 -2
  112. package/dist/{textarea-D7aGOyDf.cjs → textarea-CjqprL6Y.cjs} +1 -1
  113. package/dist/{textinput-DFh8_9cx.cjs → textinput-B0VWmVIF.cjs} +1 -1
  114. package/dist/{textinput-BUgntXnM.js → textinput-CxzAxM8E.js} +2 -2
  115. package/dist/{timepicker-BIfE7KKJ.cjs → timepicker-Cv9juQeB.cjs} +1 -1
  116. package/dist/{timepicker-DaSwn1FA.js → timepicker-DXrpg5C7.js} +2 -2
  117. package/package.json +2 -2
  118. package/src/components/fileupload/fileupload-base.ts +557 -0
  119. package/src/components/fileupload/fileupload-operations.ts +213 -0
  120. package/src/components/fileupload/fileupload-renderers.ts +769 -0
  121. package/src/components/fileupload/fileupload-types.ts +88 -0
  122. package/src/components/fileupload/fileupload.test.ts +949 -0
  123. package/src/components/fileupload/fileupload.ts +469 -0
  124. package/src/components/fileupload/index.ts +34 -0
  125. package/src/components/index.ts +16 -0
  126. package/src/components/input-wrapper/input-wrapper.ts +10 -3
  127. package/dist/calendar-BzEtOJla.js +0 -2031
  128. package/dist/calendar-E5jJLt8D.cjs +0 -90
  129. package/dist/input-wrapper-BRuVELpK.cjs +0 -54
  130. /package/dist/{booleanish-jJLlnr6-.cjs → booleanish-B5EFqFpJ.cjs} +0 -0
  131. /package/dist/{booleanish-BmcOU-43.js → booleanish-CRaKZ75t.js} +0 -0
  132. /package/dist/{consent-DeB_5sWH.js → consent-f_IGyHss.js} +0 -0
  133. /package/dist/{consent-hwu7pp3S.cjs → consent-zh2npq2K.cjs} +0 -0
  134. /package/dist/{dialog-polyfill.esm-1hPr0gl9.js → dialog-polyfill.esm-BmVHGRTX.js} +0 -0
  135. /package/dist/{dialog-polyfill.esm-D3Oinvk0.cjs → dialog-polyfill.esm-CXDsb3l-.cjs} +0 -0
  136. /package/dist/{heading-DiQLPxdf.js → heading-BxvE80bb.js} +0 -0
  137. /package/dist/{heading-BgSWIRii.cjs → heading-DRJTS27k.cjs} +0 -0
  138. /package/dist/{input-wrapper-Dwd-_sKm.cjs → input-wrapper-CYCCx8MP.cjs} +0 -0
  139. /package/dist/{input-wrapper-CDyHTR1V.js → input-wrapper-kw2y6Jp_.js} +0 -0
  140. /package/dist/{link-CyTCpI0k.cjs → link-B8zYc5WK.cjs} +0 -0
  141. /package/dist/{link-CKAFMz9j.js → link-CjJcwcxR.js} +0 -0
  142. /package/dist/{linkcard-BE4-FewK.cjs → linkcard-C4x-NJMi.cjs} +0 -0
  143. /package/dist/{linkcard-B0KOGaGj.js → linkcard-Ddh5R_L-.js} +0 -0
  144. /package/dist/{loader-CX2rsC01.cjs → loader-CGQXAuyM.cjs} +0 -0
  145. /package/dist/{loader-6kFZUErT.js → loader-DeizpX45.js} +0 -0
  146. /package/dist/{messagebox-Din9X53b.cjs → messagebox-Ccu29rDv.cjs} +0 -0
  147. /package/dist/{messagebox-Cwlg5sOr.js → messagebox-DqoPqo4K.js} +0 -0
  148. /package/dist/{pkt-options-controller-CXUGx134.cjs → pkt-options-controller-BPBb_O2K.cjs} +0 -0
  149. /package/dist/{pkt-options-controller-CRrI6WMk.js → pkt-options-controller-DbLrdRwH.js} +0 -0
  150. /package/dist/{repeat-Cepr3469.js → repeat-DV1O43YU.js} +0 -0
  151. /package/dist/{repeat-CqcDYBxr.cjs → repeat-DaDFvx86.cjs} +0 -0
  152. /package/dist/{tabs-Rc9N9HWa.js → tabs-4eWA7CsK.js} +0 -0
  153. /package/dist/{tabs-BfurI6C3.cjs → tabs-RnDtIFv3.cjs} +0 -0
  154. /package/dist/{tag-DLthw5b4.cjs → tag-DWtajZk6.cjs} +0 -0
  155. /package/dist/{tag-VwykmuQR.js → tag-hl59gziI.js} +0 -0
  156. /package/dist/{utils-CUqCKGvp.js → utils-BOOZ0ppt.js} +0 -0
  157. /package/dist/{utils-CjAbIttF.cjs → utils-CM67Oudc.cjs} +0 -0
@@ -0,0 +1,469 @@
1
+ import { html } from 'lit'
2
+ import { classMap } from 'lit/directives/class-map.js'
3
+ import { ifDefined } from 'lit/directives/if-defined.js'
4
+ import { customElement, state } from 'lit/decorators.js'
5
+
6
+ import { defaultFileUploadStrings } from 'shared-types'
7
+ import { navigateCyclic, trapTabInside, type IFileAndTransfer } from 'shared-utils/fileupload'
8
+
9
+ import '@/components/alert'
10
+ import '@/components/icon'
11
+ import '@/components/input-wrapper'
12
+ import '@/components/modal'
13
+ import '@/components/progressbar'
14
+ import { PktFileUploadBase } from './fileupload-base'
15
+ import {
16
+ createCommentOperation,
17
+ createRemoveOperation,
18
+ createRenameOperation,
19
+ } from './fileupload-operations'
20
+ import { renderFilenameQueueItem, renderThumbnailQueueItem } from './fileupload-renderers'
21
+ import type {
22
+ FileItem,
23
+ IPktFileUpload,
24
+ TQueueItemOperation,
25
+ TQueueOperationContext,
26
+ TQueueOperationLabel,
27
+ TFileComment,
28
+ TFileTransfer,
29
+ TFileUploadItemRenderer,
30
+ TTransferCancelledDetail,
31
+ TTransferProgress,
32
+ TFileValidateDetail,
33
+ TFileValidator,
34
+ TUploadStrategy,
35
+ } from './fileupload-types'
36
+ export type {
37
+ FileItem,
38
+ IPktFileUpload,
39
+ TQueueItemOperation,
40
+ TQueueOperationContext,
41
+ TQueueOperationLabel,
42
+ TFileComment,
43
+ TFileTransfer,
44
+ TFileUploadItemRenderer,
45
+ TTransferCancelledDetail,
46
+ TTransferProgress,
47
+ TFileValidateDetail,
48
+ TFileValidator,
49
+ TUploadStrategy,
50
+ }
51
+
52
+ type FileImageFailureFlags = { thumbnail?: boolean; preview?: boolean }
53
+
54
+ @customElement('pkt-fileupload')
55
+ export class PktFileUpload extends PktFileUploadBase implements IPktFileUpload {
56
+ @state() private activeOperationByFileId: Record<string, string | undefined> = {}
57
+ @state() private isPreviewModalOpen = false
58
+ @state() private previewCurrentIndex = 0
59
+ @state() private failedImageFileIds: Record<string, FileImageFailureFlags> = {}
60
+
61
+ render() {
62
+ const content = this.renderContent()
63
+
64
+ if (!this.label) return content
65
+
66
+ return html`
67
+ <pkt-input-wrapper
68
+ .forId=${`${this.id}-input`}
69
+ .label=${this.label}
70
+ .helptext=${this.helptext}
71
+ ?disabled=${this.disabled}
72
+ ?hasError=${this.hasEffectiveError}
73
+ ?optionalTag=${this.optionalTag}
74
+ ?requiredTag=${this.requiredTag}
75
+ class=${classMap({
76
+ 'pkt-fileupload-wrapper': true,
77
+ 'pkt-fileupload-wrapper--full-width': this.fullwidth,
78
+ })}
79
+ >
80
+ ${content}
81
+ </pkt-input-wrapper>
82
+ `
83
+ }
84
+
85
+ private renderContent() {
86
+ const filesAndTransfers = this.getSortedFilesAndTransfers()
87
+ const previewableImages = this.getPreviewableImages(filesAndTransfers)
88
+ const supportedFormatsText = this.getSupportedFormatsText()
89
+ const helptextId = !this.label && this.helptext ? `${this.id}-helptext` : undefined
90
+ const errorId = this.hasEffectiveError ? `${this.id}-error` : undefined
91
+ const describedBy = [helptextId, errorId].filter(Boolean).join(' ') || undefined
92
+ const isThumbnailView = this.itemRenderer === 'thumbnail'
93
+ const dropZoneCopy = isThumbnailView
94
+ ? this.multiple
95
+ ? {
96
+ dragInactive: defaultFileUploadStrings.dropZoneDragMultipleThumbnail,
97
+ dragActive: defaultFileUploadStrings.dropZoneDragActiveMultipleThumbnail,
98
+ openFileDialog: defaultFileUploadStrings.dropZoneOpenFileDialogMultipleThumbnail,
99
+ }
100
+ : {
101
+ dragInactive: defaultFileUploadStrings.dropZoneDragSingleThumbnail,
102
+ dragActive: defaultFileUploadStrings.dropZoneDragActiveSingleThumbnail,
103
+ openFileDialog: defaultFileUploadStrings.dropZoneOpenFileDialogSingleThumbnail,
104
+ }
105
+ : this.multiple
106
+ ? {
107
+ dragInactive: defaultFileUploadStrings.dropZoneDragMultiple,
108
+ dragActive: defaultFileUploadStrings.dropZoneDragActiveMultiple,
109
+ openFileDialog: defaultFileUploadStrings.dropZoneOpenFileDialogMultiple,
110
+ }
111
+ : {
112
+ dragInactive: defaultFileUploadStrings.dropZoneDragSingle,
113
+ dragActive: defaultFileUploadStrings.dropZoneDragActiveSingle,
114
+ openFileDialog: defaultFileUploadStrings.dropZoneOpenFileDialogSingle,
115
+ }
116
+
117
+ return html`
118
+ <div
119
+ class=${classMap({
120
+ 'pkt-fileupload': true,
121
+ 'pkt-fileupload--full-width': this.fullwidth,
122
+ 'pkt-fileupload--error': this.hasEffectiveError,
123
+ 'pkt-fileupload--disabled': this.disabled,
124
+ })}
125
+ aria-disabled=${String(this.disabled)}
126
+ ?inert=${this.disabled}
127
+ >
128
+ <div class="pkt-sr-only" aria-live="polite" aria-atomic="true">
129
+ ${this.getSrUploadedMessage()}
130
+ </div>
131
+ <div class="pkt-sr-only" aria-live="assertive" aria-atomic="true">
132
+ ${this.getSrErrorsMessage()}
133
+ </div>
134
+ <div
135
+ class=${classMap({
136
+ 'pkt-fileupload__drop-zone': true,
137
+ 'pkt-fileupload__drop-zone--drag-active': this.isDragActive,
138
+ 'pkt-fileupload__drop-zone--disabled': this.disabled,
139
+ })}
140
+ @dragover=${this.onDragOver}
141
+ @dragleave=${this.onDragLeave}
142
+ @drop=${this.onDrop}
143
+ @click=${this.onDropZoneClick}
144
+ >
145
+ <input
146
+ id=${`${this.id}-input`}
147
+ type="file"
148
+ name=${ifDefined(this.uploadStrategy === 'form' ? this.name : undefined)}
149
+ ?multiple=${this.multiple}
150
+ ?disabled=${this.disabled}
151
+ ?required=${this.required && this.uploadStrategy === 'form'}
152
+ accept=${this.getResolvedAcceptValue()}
153
+ aria-label=${ifDefined(
154
+ this.label ? undefined : this.multiple ? 'Velg filer' : 'Velg fil',
155
+ )}
156
+ aria-required=${ifDefined(this.required ? 'true' : undefined)}
157
+ aria-invalid=${ifDefined(this.hasEffectiveError ? 'true' : undefined)}
158
+ aria-describedby=${ifDefined(describedBy)}
159
+ @change=${this.onNativeFileChange}
160
+ />
161
+ ${this.uploadStrategy === 'custom'
162
+ ? filesAndTransfers.map(
163
+ (fileItem) =>
164
+ html`<input type="hidden" name=${this.name} value=${fileItem.fileId} />`,
165
+ )
166
+ : null}
167
+ <div class="pkt-fileupload__drop-zone__placeholder">
168
+ <pkt-icon
169
+ name="attachment"
170
+ class="pkt-fileupload__drop-zone__placeholder__icon"
171
+ aria-hidden="true"
172
+ ></pkt-icon>
173
+ <p class="pkt-fileupload__drop-zone__placeholder__title">
174
+ ${this.isDragActive
175
+ ? `${dropZoneCopy.dragActive} ...`
176
+ : html`${dropZoneCopy.dragInactive}
177
+ <button
178
+ type="button"
179
+ class="pkt-fileupload__drop-zone__placeholder__title__open-file-dialog"
180
+ @click=${this.openFileDialog}
181
+ >
182
+ ${dropZoneCopy.openFileDialog}
183
+ </button>`}
184
+ </p>
185
+ ${supportedFormatsText
186
+ ? html`<p class="pkt-fileupload__drop-zone__placeholder__formats">
187
+ ${defaultFileUploadStrings.supportedFormatsPrefix} ${supportedFormatsText}
188
+ </p>`
189
+ : null}
190
+ </div>
191
+ </div>
192
+ ${this.hasEffectiveError
193
+ ? html`
194
+ <pkt-alert
195
+ skin="error"
196
+ role="alert"
197
+ aria-live="assertive"
198
+ compact
199
+ id=${`${this.id}-error`}
200
+ class="pkt-alert--error pkt-fileupload__error-alert"
201
+ >
202
+ ${this.effectiveErrorMessage}
203
+ </pkt-alert>
204
+ `
205
+ : null}
206
+ ${filesAndTransfers.length > 0
207
+ ? html`<ul class="pkt-fileupload__queue-display">
208
+ ${filesAndTransfers.map((file) => this.renderQueueItem(file, previewableImages))}
209
+ </ul>`
210
+ : null}
211
+ ${this.renderPreviewModal(previewableImages)}
212
+ </div>
213
+ `
214
+ }
215
+
216
+ private renderQueueItem(file: IFileAndTransfer, previewableImages: IFileAndTransfer[]) {
217
+ const fileSize = typeof file.file?.size === 'number' ? this.formatFileSize(file.file.size) : ''
218
+ const operations = this.getQueueItemOperations()
219
+
220
+ if (this.itemRenderer === 'thumbnail') {
221
+ const canOpenPreview = this.canOpenPreview(file)
222
+ const thumbnailUrl = this.failedImageFileIds[file.fileId]?.thumbnail
223
+ ? undefined
224
+ : this.getThumbnailUrl(file)
225
+ return renderThumbnailQueueItem({
226
+ file,
227
+ inputName: this.name,
228
+ disabled: this.disabled,
229
+ fileSize,
230
+ operations,
231
+ activeOperationId: this.activeOperationByFileId[file.fileId],
232
+ onActivateOperation: (fileId, operationId) => this.activateOperation(fileId, operationId),
233
+ onCloseOperation: (fileId) => this.closeOperation(fileId),
234
+ getFileAttribute: <T>(fileId: string, name: string) =>
235
+ this.getFileAttribute<T>(fileId, name),
236
+ setFileAttribute: (fileId: string, name: string, value: unknown) =>
237
+ this.setFileAttribute(fileId, name, value),
238
+ thumbnailUrl,
239
+ previewEnabled: this.enableImagePreview,
240
+ canOpenPreview,
241
+ transferProgress: file.progress,
242
+ transferErrorMessage: file.errorMessage,
243
+ transferShowProgress: file.showProgress,
244
+ transferLastProgress: file.lastProgress,
245
+ loadingText: 'Laster opp',
246
+ truncateTail: this.truncateTail,
247
+ onCancel: (fileId) => this.cancelTransfer(fileId),
248
+ onOpenPreview: (fileId) => this.openPreview(fileId, previewableImages),
249
+ onThumbnailImageError: (fileId) => this.markImageFailed(fileId, 'thumbnail'),
250
+ })
251
+ }
252
+
253
+ return renderFilenameQueueItem({
254
+ file,
255
+ inputName: this.name,
256
+ disabled: this.disabled,
257
+ fileSize,
258
+ operations,
259
+ activeOperationId: this.activeOperationByFileId[file.fileId],
260
+ onActivateOperation: (fileId, operationId) => this.activateOperation(fileId, operationId),
261
+ onCloseOperation: (fileId) => this.closeOperation(fileId),
262
+ getFileAttribute: <T>(fileId: string, name: string) => this.getFileAttribute<T>(fileId, name),
263
+ setFileAttribute: (fileId: string, name: string, value: unknown) =>
264
+ this.setFileAttribute(fileId, name, value),
265
+ transferProgress: file.progress,
266
+ transferErrorMessage: file.errorMessage,
267
+ transferShowProgress: file.showProgress,
268
+ transferLastProgress: file.lastProgress,
269
+ loadingText: 'Laster opp',
270
+ truncateTail: this.truncateTail,
271
+ onCancel: (fileId) => this.cancelTransfer(fileId),
272
+ })
273
+ }
274
+
275
+ private markImageFailed(fileId: string, kind: keyof FileImageFailureFlags): void {
276
+ this.failedImageFileIds = {
277
+ ...this.failedImageFileIds,
278
+ [fileId]: { ...this.failedImageFileIds[fileId], [kind]: true },
279
+ }
280
+ }
281
+
282
+ private getPreviewableImages(filesAndTransfers: IFileAndTransfer[]): IFileAndTransfer[] {
283
+ if (!this.enableImagePreview) return []
284
+ return filesAndTransfers.filter((item) => item.progress === 'done' && this.isImageFile(item))
285
+ }
286
+
287
+ private canOpenPreview(file: IFileAndTransfer): boolean {
288
+ return this.enableImagePreview && file.progress === 'done' && this.isImageFile(file)
289
+ }
290
+
291
+ private openPreview(fileId: string, previewableImages: IFileAndTransfer[]): void {
292
+ const index = previewableImages.findIndex((item) => item.fileId === fileId)
293
+ if (index < 0) return
294
+ this.previewCurrentIndex = index
295
+ this.isPreviewModalOpen = true
296
+ this.failedImageFileIds = {
297
+ ...this.failedImageFileIds,
298
+ [fileId]: { ...this.failedImageFileIds[fileId], preview: false },
299
+ }
300
+ }
301
+
302
+ private closePreview = (): void => {
303
+ this.isPreviewModalOpen = false
304
+ }
305
+
306
+ private navigatePreview(direction: 'prev' | 'next', previewableImages: IFileAndTransfer[]): void {
307
+ if (previewableImages.length === 0) return
308
+ this.previewCurrentIndex = navigateCyclic(
309
+ this.previewCurrentIndex,
310
+ direction,
311
+ previewableImages.length,
312
+ )
313
+ }
314
+
315
+ private onPreviewKeyDown = (
316
+ event: KeyboardEvent,
317
+ previewableImages: IFileAndTransfer[],
318
+ ): void => {
319
+ if (!this.isPreviewModalOpen) return
320
+ if (event.key === 'Tab') {
321
+ trapTabInside(event, event.currentTarget as HTMLElement | null)
322
+ return
323
+ }
324
+ switch (event.key) {
325
+ case 'ArrowLeft':
326
+ event.preventDefault()
327
+ this.navigatePreview('prev', previewableImages)
328
+ break
329
+ case 'ArrowRight':
330
+ event.preventDefault()
331
+ this.navigatePreview('next', previewableImages)
332
+ break
333
+ case 'Escape':
334
+ event.preventDefault()
335
+ this.closePreview()
336
+ break
337
+ default:
338
+ break
339
+ }
340
+ }
341
+
342
+ private renderPreviewModal(previewableImages: IFileAndTransfer[]) {
343
+ if (!this.enableImagePreview || previewableImages.length === 0) return null
344
+ const currentImage = previewableImages[this.previewCurrentIndex]
345
+ if (!currentImage) return null
346
+ const hasMultipleImages = previewableImages.length > 1
347
+ const imageUrl = this.getThumbnailUrl(currentImage)
348
+ const hasImage = !!imageUrl && !this.failedImageFileIds[currentImage.fileId]?.preview
349
+ const filename =
350
+ currentImage.attributes?.targetFilename || currentImage.file?.name || 'Forhåndsvisning'
351
+
352
+ return html`
353
+ <pkt-modal
354
+ .open=${this.isPreviewModalOpen}
355
+ .headingText=${filename}
356
+ closeOnBackdropClick
357
+ class="pkt-fileupload__image-preview-modal"
358
+ @close=${this.closePreview}
359
+ @keydown=${(event: KeyboardEvent) => this.onPreviewKeyDown(event, previewableImages)}
360
+ >
361
+ <div class="pkt-fileupload__image-preview">
362
+ ${hasMultipleImages
363
+ ? html`<button
364
+ type="button"
365
+ class="pkt-fileupload__image-preview__nav pkt-fileupload__image-preview__nav--prev pkt-btn pkt-btn--medium pkt-btn--secondary pkt-btn--icon-only"
366
+ aria-label=${`Forrige bilde ${this.previewCurrentIndex} / ${previewableImages.length}`}
367
+ @click=${() => this.navigatePreview('prev', previewableImages)}
368
+ >
369
+ <pkt-icon
370
+ class="pkt-btn__icon pkt-icon"
371
+ name="chevron-thin-left"
372
+ aria-hidden="true"
373
+ ></pkt-icon>
374
+ <span class="pkt-btn__text"></span>
375
+ </button>`
376
+ : null}
377
+
378
+ <div class="pkt-fileupload__image-preview__content">
379
+ ${hasImage
380
+ ? html`<img
381
+ src=${imageUrl}
382
+ alt=${`${filename} - ${this.previewCurrentIndex + 1} av ${previewableImages.length}`}
383
+ class="pkt-fileupload__image-preview__image"
384
+ @error=${() => this.markImageFailed(currentImage.fileId, 'preview')}
385
+ />`
386
+ : html`<pkt-icon
387
+ name="picture"
388
+ class="pkt-fileupload__queue-display__item__icon pkt-icon--medium"
389
+ aria-hidden="true"
390
+ ></pkt-icon>`}
391
+ </div>
392
+
393
+ ${hasMultipleImages
394
+ ? html`<span class="pkt-fileupload__image-preview__counter">
395
+ ${this.previewCurrentIndex + 1} / ${previewableImages.length}
396
+ </span>`
397
+ : null}
398
+ ${hasMultipleImages
399
+ ? html`<button
400
+ type="button"
401
+ class="pkt-fileupload__image-preview__nav pkt-fileupload__image-preview__nav--next pkt-btn pkt-btn--medium pkt-btn--secondary pkt-btn--icon-only"
402
+ aria-label=${`Neste bilde ${this.previewCurrentIndex + 2} / ${previewableImages.length}`}
403
+ @click=${() => this.navigatePreview('next', previewableImages)}
404
+ >
405
+ <pkt-icon
406
+ class="pkt-btn__icon pkt-icon"
407
+ name="chevron-thin-right"
408
+ aria-hidden="true"
409
+ ></pkt-icon>
410
+ <span class="pkt-btn__text"></span>
411
+ </button>`
412
+ : null}
413
+ </div>
414
+ </pkt-modal>
415
+ `
416
+ }
417
+
418
+ private getQueueItemOperations(): TQueueItemOperation[] {
419
+ const operations: TQueueItemOperation[] = []
420
+
421
+ if (this.renameFilesEnabled && this.itemRenderer !== 'thumbnail') {
422
+ operations.push(createRenameOperation())
423
+ }
424
+
425
+ if (this.addCommentsEnabled && this.itemRenderer !== 'thumbnail') {
426
+ operations.push(createCommentOperation())
427
+ }
428
+
429
+ operations.push(...this.extraOperations)
430
+ operations.push(createRemoveOperation((fileId) => this.removeFileItem(fileId)))
431
+
432
+ return operations
433
+ }
434
+
435
+ private activateOperation(fileId: string, operationId: string): void {
436
+ this.activeOperationByFileId = {
437
+ ...this.activeOperationByFileId,
438
+ [fileId]: operationId,
439
+ }
440
+ }
441
+
442
+ private closeOperation(fileId: string): void {
443
+ this.activeOperationByFileId = {
444
+ ...this.activeOperationByFileId,
445
+ [fileId]: undefined,
446
+ }
447
+ }
448
+
449
+ private getFileAttribute<T>(fileId: string, name: string): T | undefined {
450
+ const currentFile = this.getCurrentFiles().find((item) => item.fileId === fileId)
451
+ if (!currentFile) return undefined
452
+ return (currentFile.attributes?.[name] as T | undefined) ?? undefined
453
+ }
454
+
455
+ private setFileAttribute(fileId: string, name: string, value: unknown): void {
456
+ const currentFile = this.getCurrentFiles().find((item) => item.fileId === fileId)
457
+ if (!currentFile) return
458
+ const nextAttributes = {
459
+ ...currentFile.attributes,
460
+ [name]: value,
461
+ }
462
+ if (value === undefined) {
463
+ delete nextAttributes[name]
464
+ }
465
+ this.updateFileItem(fileId, {
466
+ attributes: nextAttributes,
467
+ })
468
+ }
469
+ }
@@ -0,0 +1,34 @@
1
+ import {
2
+ PktFileUpload,
3
+ type FileItem,
4
+ type IPktFileUpload,
5
+ type TFileComment,
6
+ type TQueueItemOperation,
7
+ type TQueueOperationContext,
8
+ type TQueueOperationLabel,
9
+ type TFileTransfer,
10
+ type TFileUploadItemRenderer,
11
+ type TTransferCancelledDetail,
12
+ type TTransferProgress,
13
+ type TFileValidateDetail,
14
+ type TFileValidator,
15
+ type TUploadStrategy,
16
+ } from './fileupload'
17
+
18
+ export { PktFileUpload }
19
+ export type {
20
+ FileItem,
21
+ IPktFileUpload,
22
+ TFileComment,
23
+ TQueueItemOperation,
24
+ TQueueOperationContext,
25
+ TQueueOperationLabel,
26
+ TFileTransfer,
27
+ TFileUploadItemRenderer,
28
+ TTransferCancelledDetail,
29
+ TTransferProgress,
30
+ TFileValidateDetail,
31
+ TFileValidator,
32
+ TUploadStrategy,
33
+ }
34
+ export default PktFileUpload
@@ -9,6 +9,7 @@ export { PktCard } from '@/components/card'
9
9
  export { PktCombobox } from './combobox'
10
10
  export { PktConsent } from './consent'
11
11
  export { PktCheckbox } from '@/components/checkbox'
12
+ export { PktFileUpload } from '@/components/fileupload'
12
13
  export { PktDateTags } from '@/components/datepicker/date-tags.js'
13
14
  export { PktDatepicker } from '@/components/datepicker/datepicker.js'
14
15
  export { PktHeader, PktHeaderService, PktHeaderUserMenu } from '@/components/header'
@@ -88,5 +89,20 @@ export type { IPktBreadcrumbs, IBreadcrumbItem } from '@/components/breadcrumbs'
88
89
  export type { IPktLoader, TPktLoaderVariant } from '@/components/loader'
89
90
 
90
91
  export type { IPktConsent } from '@/components/consent'
92
+ export type {
93
+ FileItem,
94
+ IPktFileUpload,
95
+ TFileComment,
96
+ TQueueItemOperation,
97
+ TQueueOperationContext,
98
+ TQueueOperationLabel,
99
+ TFileTransfer,
100
+ TFileUploadItemRenderer,
101
+ TTransferCancelledDetail,
102
+ TTransferProgress,
103
+ TFileValidateDetail,
104
+ TFileValidator,
105
+ TUploadStrategy,
106
+ } from '@/components/fileupload'
91
107
 
92
108
  export type { IPktTimepicker } from '@/components/timepicker'
@@ -7,8 +7,8 @@ import { classMap } from 'lit/directives/class-map.js'
7
7
  import { customElement, property } from 'lit/decorators.js'
8
8
  import { ElementProps } from '@/types/typeUtils'
9
9
  import { uuidish } from 'shared-utils/utils'
10
- import { booleanishConverter } from 'shared-types'
11
- import type { Booleanish } from 'shared-types'
10
+ import { booleanishConverter, type Booleanish } from 'shared-types'
11
+ import { FocusModalityController } from '@/controllers/focus-modality-controller'
12
12
  import specs from 'componentSpecs/input-wrapper.json'
13
13
  import '@/components/helptext'
14
14
  import '@/components/icon'
@@ -74,6 +74,12 @@ export class PktInputWrapper extends PktElementWithSlot<Props> {
74
74
  super.updated(changedProperties)
75
75
  }
76
76
 
77
+ // Speiler navigeringsmodus til den interne pkt-inputwrapper-div-en som
78
+ // data-navtype="pointer" (eller fjerner attributtet) så CSS kan skille
79
+ // dempet fokus (klikk) fra kraftig fokus (tab). Controlleren registrerer
80
+ // seg selv på hosten i konstruktøren, så vi trenger ikke beholde referansen.
81
+ focusModality = new FocusModalityController(this, '.pkt-inputwrapper')
82
+
77
83
  render() {
78
84
  const classes = {
79
85
  'pkt-inputwrapper': true,
@@ -148,7 +154,8 @@ export class PktInputWrapper extends PktElementWithSlot<Props> {
148
154
  @toggleHelpText=${(e: CustomEvent) => {
149
155
  this.toggleDropdown(e)
150
156
  }}
151
- >${slotContent(this, 'helptext')}</pkt-helptext>
157
+ >${slotContent(this, 'helptext')}</pkt-helptext
158
+ >
152
159
  `
153
160
  }
154
161