@securancy/file-explorer 1.0.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 (36) hide show
  1. package/README.md +7 -0
  2. package/default-styling.pcss +39 -0
  3. package/dist/components/FileExplorer.svelte +443 -0
  4. package/dist/components/FileExplorer.svelte.d.ts +63 -0
  5. package/dist/components/FileExplorerBreadcrumb.svelte +263 -0
  6. package/dist/components/FileExplorerBreadcrumb.svelte.d.ts +18 -0
  7. package/dist/components/FileExplorerCreateDirectory.svelte +77 -0
  8. package/dist/components/FileExplorerCreateDirectory.svelte.d.ts +24 -0
  9. package/dist/components/FileExplorerDirectoryColumn.svelte +456 -0
  10. package/dist/components/FileExplorerDirectoryColumn.svelte.d.ts +39 -0
  11. package/dist/components/FileExplorerDirectoryIndex.svelte +367 -0
  12. package/dist/components/FileExplorerDirectoryIndex.svelte.d.ts +33 -0
  13. package/dist/components/FileExplorerDisplayModeSwitcher.svelte +300 -0
  14. package/dist/components/FileExplorerDisplayModeSwitcher.svelte.d.ts +19 -0
  15. package/dist/components/FileExplorerFileDetailColumn.svelte +340 -0
  16. package/dist/components/FileExplorerFileDetailColumn.svelte.d.ts +24 -0
  17. package/dist/components/FileIcon.svelte +310 -0
  18. package/dist/components/FileIcon.svelte.d.ts +19 -0
  19. package/dist/file-explorer.pcss +304 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +2 -0
  22. package/dist/models/file-explorer-display-mode.d.ts +4 -0
  23. package/dist/models/file-explorer-display-mode.js +5 -0
  24. package/dist/models/file-item.d.ts +23 -0
  25. package/dist/models/file-item.js +1 -0
  26. package/dist/models/index.d.ts +3 -0
  27. package/dist/models/index.js +3 -0
  28. package/dist/models/translations.d.ts +19 -0
  29. package/dist/models/translations.js +17 -0
  30. package/dist/utilities/file-utilities.d.ts +8 -0
  31. package/dist/utilities/file-utilities.js +49 -0
  32. package/dist/utilities/index.d.ts +1 -0
  33. package/dist/utilities/index.js +1 -0
  34. package/dist/validation/file-upload-schema.d.ts +2 -0
  35. package/dist/validation/file-upload-schema.js +2 -0
  36. package/package.json +72 -0
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @securancy/file-explorer
2
+
3
+ A ready-to-use component for browsing files and directories.
4
+
5
+ ## Building
6
+
7
+ Run `nx build @securancy/file-explorer` to build the library.
@@ -0,0 +1,39 @@
1
+ :root {
2
+ /* Theming */
3
+ --file-explorer-background: var(--color-background);
4
+ --file-explorer-background-shade: var(--color-background--dimmed);
5
+ --file-explorer-background-dragover: rgba(var(--color-primary-rgb), 40%);
6
+ --file-explorer-border-style: solid 1px var(--color-border);
7
+ --file-explorer-message-color: var(--color-text--soft);
8
+ --file-explorer-action-primary-color: var(--color-primary);
9
+ --file-explorer-action-secondary-color: var(--color-secondary);
10
+ --file-explorer-action-primary-color-contrast: white;
11
+ --file-explorer-text-semi-bold: var(--font-weight--semi-bold);
12
+ --file-explorer-create-directory-border: var(--color-border);
13
+ --file-explorer-create-directory-border-radius: var(--border-radius--large);
14
+
15
+ /* List items */
16
+ --file-explorer-list-item-color: var(--color-text);
17
+ --file-explorer-list-item-color-active: var(--file-explorer-action-primary-color);
18
+ --file-explorer-list-item-border-radius: 4px;
19
+ --file-explorer-list-header-color: var(--color-text);
20
+ --file-explorer-list-item-background-hover: var(--color-background--dimmed);
21
+ --file-explorer-list-item-background-active: rgba(var(--color-primary-rgb), 10%);
22
+
23
+ /* Columns */
24
+ --file-explorer-column-default-width: 270px;
25
+ --file-explorer-index-default-width: 100%;
26
+ --file-explorer-preview-default-width: 320px;
27
+ --file-explorer-column-min-width: 80%;
28
+
29
+ /* Spacing & sizing */
30
+ --file-explorer-list-item-spacing: 16px;
31
+ --file-explorer-default-spacing: 20px;
32
+ --file-explorer-preview-icon-size: 150px;
33
+
34
+ @media (--screen-sm) {
35
+ --file-explorer-column-min-width: 140px;
36
+ --file-explorer-index-default-width: auto;
37
+ --file-explorer-list-item-spacing: 8px;
38
+ }
39
+ }
@@ -0,0 +1,443 @@
1
+ <script context="module"></script>
2
+
3
+ <script>import { createEventDispatcher, tick } from "svelte";
4
+ import FileExplorerBreadcrumb from "./FileExplorerBreadcrumb.svelte";
5
+ import FileExplorerDisplayModeSwitcher from "./FileExplorerDisplayModeSwitcher.svelte";
6
+ import { defaultTranslations, FileExplorerDisplayMode } from "../index.js";
7
+ import FileExplorerDirectoryColumn from "./FileExplorerDirectoryColumn.svelte";
8
+ import FileExplorerCreateDirectory from "./FileExplorerCreateDirectory.svelte";
9
+ import { Button, Modal } from "@securancy/svelte-components";
10
+ export let displayMode = FileExplorerDisplayMode.Columns;
11
+ export let filePath;
12
+ export let documents;
13
+ export let loading;
14
+ export let routePrefix;
15
+ export let canEdit;
16
+ export let translations = defaultTranslations;
17
+ export let getFilePreview;
18
+ export let createDirectory = void 0;
19
+ export let uploadFiles = void 0;
20
+ export let deleteFile = void 0;
21
+ const dispatch = createEventDispatcher();
22
+ const scrollBackButtonThreshold = 200;
23
+ let fileExplorerContentContainer;
24
+ let showScrollBackButton = false;
25
+ let createDirectoryPath;
26
+ let createDirectoryModalOpen = false;
27
+ let deleteFileModalOpen = false;
28
+ let deleteFileEvent;
29
+ let deleting = false;
30
+ $: if (filePath) tick().then(() => scrollToRight());
31
+ async function scrollToRight() {
32
+ if (!fileExplorerContentContainer) return;
33
+ fileExplorerContentContainer.scroll({
34
+ left: fileExplorerContentContainer.scrollWidth,
35
+ behavior: "smooth"
36
+ });
37
+ }
38
+ function handleScroll() {
39
+ showScrollBackButton = fileExplorerContentContainer.scrollLeft > scrollBackButtonThreshold;
40
+ }
41
+ function handleScrollBack() {
42
+ fileExplorerContentContainer.scroll({
43
+ left: 0,
44
+ behavior: "smooth"
45
+ });
46
+ }
47
+ async function handleUploadFile(event) {
48
+ if (uploadFiles) {
49
+ if (event.detail.files) {
50
+ await uploadFiles({
51
+ path: event.detail.path,
52
+ files: event.detail.files
53
+ });
54
+ documents.refresh();
55
+ } else {
56
+ const input = document.createElement("input");
57
+ input.type = "file";
58
+ input.multiple = true;
59
+ input.click();
60
+ input.addEventListener("change", async (changeEvent) => {
61
+ if (input.files && uploadFiles) {
62
+ await uploadFiles({
63
+ path: event.detail.path,
64
+ files: input.files
65
+ });
66
+ documents.refresh();
67
+ }
68
+ });
69
+ }
70
+ } else {
71
+ dispatch("upload-file", event.detail);
72
+ }
73
+ }
74
+ function handleCreateDirectory(event) {
75
+ if (createDirectory) {
76
+ createDirectoryPath = event.detail.path;
77
+ setTimeout(() => createDirectoryModalOpen = true);
78
+ } else {
79
+ dispatch("create-directory", event.detail);
80
+ }
81
+ }
82
+ function handleDeleteFile(event) {
83
+ if (deleteFile) {
84
+ deleteFileEvent = event.detail;
85
+ deleteFileModalOpen = true;
86
+ } else {
87
+ dispatch("delete-file", event.detail);
88
+ }
89
+ }
90
+ function deleteFileInternal() {
91
+ if (deleteFile && deleteFileEvent) {
92
+ deleting = true;
93
+ deleteFile({
94
+ type: "file",
95
+ ...deleteFileEvent
96
+ }).then(() => {
97
+ documents.refresh();
98
+ deleteFileModalOpen = false;
99
+ }).catch((error) => {
100
+ console.error("Failed to delete item.", error);
101
+ }).finally(() => {
102
+ deleting = false;
103
+ deleteFileModalOpen = false;
104
+ });
105
+ }
106
+ }
107
+ </script>
108
+
109
+ <div class="file-explorer file-explorer--display-mode-{displayMode}"
110
+ >
111
+ <div class="file-explorer__controls">
112
+ <FileExplorerBreadcrumb {filePath} />
113
+ <FileExplorerDisplayModeSwitcher bind:displayMode />
114
+ </div>
115
+ <div
116
+ bind:this={fileExplorerContentContainer}
117
+ class="file-explorer__content"
118
+ on:scroll={handleScroll}
119
+ >
120
+ <FileExplorerDirectoryColumn
121
+ {canEdit}
122
+ currentPath=""
123
+ directoryName="Items"
124
+ {displayMode}
125
+ {documents}
126
+ fileItems={$documents}
127
+ {filePath}
128
+ {getFilePreview}
129
+ {loading}
130
+ {routePrefix}
131
+ {showScrollBackButton}
132
+ {translations}
133
+ on:create-directory={handleCreateDirectory}
134
+ on:delete-file={handleDeleteFile}
135
+ on:scroll-back={handleScrollBack}
136
+ on:upload-file={handleUploadFile}
137
+ >
138
+ <svelte:fragment slot="top">
139
+ {#if $$slots.search}
140
+ <div class="file-explorer__search">
141
+ <slot name="search" />
142
+ </div>
143
+ {/if}
144
+ </svelte:fragment>
145
+ </FileExplorerDirectoryColumn>
146
+ </div>
147
+ </div>
148
+
149
+ {#if createDirectory}
150
+ <FileExplorerCreateDirectory
151
+ {createDirectory}
152
+ {createDirectoryPath}
153
+ {documents}
154
+ {translations}
155
+ bind:createDirectoryModalOpen
156
+ />
157
+ {/if}
158
+
159
+ {#if deleteFile}
160
+ <Modal
161
+ title={translations.deleteFile}
162
+ bind:opened={deleteFileModalOpen}
163
+ >
164
+ <div>
165
+ {translations.deleteFileConfirmation}
166
+ </div>
167
+ <div class="file-explorer__text--semi-bold">
168
+ {filePath.split('/').at(-1)}?
169
+ </div>
170
+ <svelte:fragment slot="footer">
171
+ <Button
172
+ color="soft"
173
+ fill="clear"
174
+ on:click={() => deleteFileModalOpen = false}
175
+ >
176
+ {translations.cancel}
177
+ </Button>
178
+ <Button color="danger" on:click={deleteFileInternal}>
179
+ {#if deleting}
180
+ {translations.deleting}
181
+ {:else}
182
+ {translations.delete}
183
+ {/if}
184
+ </Button>
185
+ </svelte:fragment>
186
+ </Modal>
187
+ {/if}
188
+
189
+ <style>/* eslint-disable */
190
+ /**
191
+ * Media queries and devices
192
+ */
193
+ /**
194
+ * Some mixins that can be used dynamically inside @each loops
195
+ */
196
+ .file-explorer {
197
+ position: relative;
198
+ display: flex;
199
+ flex-direction: column;
200
+ height: 100%;
201
+ width: 100%;
202
+ background: var(--file-explorer-background);
203
+ overflow: hidden;
204
+ }
205
+ .file-explorer__controls {
206
+ flex-shrink: 0;
207
+ display: flex;
208
+ border-bottom: var(--file-explorer-border-style);
209
+ padding: var(--file-explorer-default-spacing);
210
+ background: var(--file-explorer-background-shade);
211
+ }
212
+ .file-explorer__content {
213
+ flex-grow: 1;
214
+ display: flex;
215
+ overflow-x: auto;
216
+ scroll-behavior: smooth;
217
+ }
218
+ /* Keep the index within screen */
219
+ @media (width >= 769px) {
220
+ /* @formatter:off */
221
+ .file-explorer--display-mode-index .file-explorer__content {
222
+ /* @formatter:on */
223
+ overflow-x: inherit;
224
+ }
225
+ }
226
+ .file-explorer__column, .file-explorer__index {
227
+ flex-shrink: 0;
228
+ border-right: var(--file-explorer-border-style);
229
+ background: var(--file-explorer-background);
230
+ }
231
+ .file-explorer__column {
232
+ display: flex;
233
+ flex-direction: column;
234
+ width: var(--file-explorer-column-default-width);
235
+ min-width: var(--file-explorer-column-min-width);
236
+ overflow: hidden;
237
+ }
238
+ .file-explorer__column--file-detail {
239
+ flex-grow: 1;
240
+ width: var(--file-explorer-preview-default-width);
241
+ display: flex;
242
+ flex-direction: column;
243
+ }
244
+ .file-explorer__column--sticky {
245
+ position: sticky;
246
+ left: 0;
247
+ z-index: 1;
248
+ }
249
+ .file-explorer__column::after {
250
+ content: '';
251
+ position: absolute;
252
+ inset: 0;
253
+ pointer-events: none;
254
+ transition: background-color 0.2s ease-in-out;
255
+ }
256
+ .file-explorer__column--dragover {
257
+ cursor: copy;
258
+ }
259
+ .file-explorer__column--dragover::after {
260
+ pointer-events: all;
261
+ background-color: var(--file-explorer-background-dragover);
262
+ }
263
+ .file-explorer__index {
264
+ display: flex;
265
+ flex-direction: column;
266
+ width: var(--file-explorer-index-default-width);
267
+ overflow: hidden;
268
+ }
269
+ @media (width >= 769px) {
270
+ .file-explorer__index {
271
+ flex-shrink: 1
272
+ }
273
+ }
274
+ .file-explorer__index--hidden {
275
+ display: none;
276
+ }
277
+ .file-explorer__column-scroll-container, .file-explorer__index-scroll-container {
278
+ flex-grow: 1;
279
+ overflow-y: auto;
280
+ padding: calc(var(--file-explorer-default-spacing) * 1.5) var(--file-explorer-default-spacing) 0 var(--file-explorer-default-spacing);
281
+ }
282
+ .file-explorer__grid {
283
+ display: flex;
284
+ align-items: flex-start;
285
+ flex-wrap: wrap;
286
+ gap: var(--file-explorer-default-spacing);
287
+ }
288
+ .file-explorer__breadcrumb {
289
+ /* @todo */
290
+ }
291
+ .file-explorer__display-mode-switcher {
292
+ display: flex;
293
+ gap: calc(0.5 * var(--file-explorer-default-spacing));
294
+ margin-left: auto;
295
+ }
296
+ .file-explorer__search {
297
+ margin-bottom: var(--file-explorer-default-spacing);
298
+ padding: calc(var(--file-explorer-default-spacing) * 1.5) var(--file-explorer-default-spacing) 0 var(--file-explorer-default-spacing);
299
+ }
300
+ .file-explorer__list {
301
+ margin-bottom: var(--file-explorer-default-spacing);
302
+ }
303
+ .file-explorer__list-header {
304
+ display: flex;
305
+ justify-content: space-between;
306
+ gap: calc(0.5 * var(--file-explorer-default-spacing));
307
+ width: 100%;
308
+ margin-bottom: calc(0.5 * var(--file-explorer-default-spacing));
309
+ color: var(--file-explorer-list-header-color);
310
+ }
311
+ .file-explorer__list-header span {
312
+ overflow: hidden;
313
+ white-space: nowrap;
314
+ text-overflow: ellipsis;
315
+ }
316
+ .file-explorer__list-header button {
317
+ flex-shrink: 0;
318
+ color: var(--file-explorer-action-primary-color);
319
+ cursor: pointer;
320
+ }
321
+ .file-explorer__grid-header {
322
+ width: 100%;
323
+ }
324
+ .file-explorer__grid-header button {
325
+ color: var(--file-explorer-action-primary-color);
326
+ cursor: pointer;
327
+ }
328
+ .file-explorer__list-item, .file-explorer__grid-item {
329
+ border-radius: var(--file-explorer-list-item-border-radius);
330
+ background: none;
331
+ color: var(--file-explorer-list-item-color);
332
+ }
333
+ .file-explorer__list-item > :global(i), .file-explorer__grid-item > :global(i) {
334
+ flex-shrink: 0;
335
+ }
336
+ .file-explorer__list-item:not(.file-explorer__list-item--active):hover, .file-explorer__grid-item:not(.file-explorer__grid-item--active):hover {
337
+ background: var(--file-explorer-list-item-background-hover);
338
+ }
339
+ .file-explorer__list-item--active, .file-explorer__grid-item--active {
340
+ color: var(--file-explorer-list-item-color-active);
341
+ background: var(--file-explorer-list-item-background-active);
342
+ }
343
+ .file-explorer__grid-item {
344
+ display: flex;
345
+ flex-direction: column;
346
+ align-items: center;
347
+
348
+ /* 3 items in a grid, spaced out with default spacing means the width of 2 gaps must be subtracted from width */
349
+ width: calc(33.33% - (0.6666 * var(--file-explorer-default-spacing)));
350
+ padding: 5px 2px;
351
+ }
352
+ @media (width >= 769px) {
353
+ .file-explorer__grid-item {
354
+ width: 100px
355
+ }
356
+ }
357
+ .file-explorer__grid-item--active {
358
+ /* @todo */
359
+ }
360
+ .file-explorer__grid-item > span {
361
+ word-break: break-word;
362
+ margin-top: calc(0.25 * var(--file-explorer-default-spacing));
363
+ }
364
+ .file-explorer__list-item {
365
+ display: flex;
366
+ align-items: baseline;
367
+ width: 100%;
368
+ text-align: left;
369
+ padding: var(--file-explorer-list-item-spacing);
370
+ white-space: nowrap;
371
+ overflow: hidden;
372
+ text-overflow: ellipsis;
373
+ }
374
+ .file-explorer__list-item > span {
375
+ flex-grow: 1;
376
+ min-width: 0;
377
+ overflow: hidden;
378
+ text-overflow: ellipsis;
379
+ }
380
+ .file-explorer__list-item > :global(i:first-child) {
381
+ margin-right: var(--file-explorer-list-item-spacing);
382
+ }
383
+ .file-explorer__list-item > :global(i:last-child) {
384
+ margin-left: var(--file-explorer-list-item-spacing);
385
+ }
386
+ .file-explorer__message {
387
+ display: block;
388
+ padding: var(--file-explorer-list-item-spacing) 0;
389
+ color: var(--file-explorer-message-color);
390
+ }
391
+ .file-explorer__image-preview {
392
+ max-width: 100%;
393
+ margin: 0 auto;
394
+ }
395
+ .file-explorer-preview__title {
396
+ font-size: var(--font-size--h1);
397
+ font-weight: var(--font-weight--semi-bold);
398
+ overflow: hidden;
399
+ text-overflow: ellipsis;
400
+ white-space: nowrap;
401
+ }
402
+ .file-explorer-preview__download {
403
+ color: var(--file-explorer-action-primary-color);
404
+ font-weight: var(--font-weight--semi-bold);
405
+ }
406
+ .file-explorer-preview__download:hover {
407
+ text-decoration-line: underline;
408
+ }
409
+ .file-explorer-preview__info {
410
+ margin-top: 14px;
411
+ }
412
+ .file-explorer-preview__info span {
413
+ color: var(--file-explorer-message-color);
414
+ }
415
+ .file-explorer-preview__info button {
416
+ cursor: pointer;
417
+ }
418
+ .file-explorer__scroll-back-button {
419
+ position: fixed;
420
+ bottom: var(--file-explorer-default-spacing);
421
+ right: var(--file-explorer-default-spacing);
422
+ z-index: 2;
423
+ background: var(--file-explorer-action-primary-color);
424
+ color: var(--file-explorer-action-primary-color-contrast);
425
+ padding: var(--file-explorer-list-item-spacing);
426
+ border-radius: var(--file-explorer-list-item-border-radius);
427
+ }
428
+ .file-explorer__file-icon--large {
429
+ width: 100%;
430
+ text-align: center;
431
+ padding: calc(2 * var(--file-explorer-default-spacing));
432
+ }
433
+ .file-explorer__file-icon--large :global(i) {
434
+ font-size: var(--file-explorer-preview-icon-size);
435
+ }
436
+ .file-explorer__text--semi-bold {
437
+ font-weight: var(--file-explorer-text-semi-bold);
438
+ }
439
+ .file-explorer__loader {
440
+ display: flex;
441
+ justify-content: center;
442
+ }
443
+ </style>
@@ -0,0 +1,63 @@
1
+ import { SvelteComponent } from "svelte";
2
+ export type UploadFileEvent = {
3
+ path: string;
4
+ files?: FileList;
5
+ };
6
+ export type CreateDirectoryEvent = {
7
+ path: string;
8
+ };
9
+ export type DeleteFileEvent = {
10
+ path: string;
11
+ name: string;
12
+ };
13
+ import type { Refreshable } from '@securancy/svelte-utilities';
14
+ import { type CreateDirectoryRequest, FileExplorerDisplayMode, type FileItem, type FilePreview, type FileSystemItem, type UploadFilesRequest } from '../index.js';
15
+ declare const __propDef: {
16
+ props: {
17
+ displayMode?: FileExplorerDisplayMode;
18
+ filePath: string;
19
+ documents: Refreshable<FileSystemItem[]>;
20
+ loading: boolean;
21
+ routePrefix: string;
22
+ canEdit: boolean;
23
+ translations?: {
24
+ readonly createNewDirectoryModalTitle: "Create new directory";
25
+ readonly cancel: "Cancel";
26
+ readonly create: "Create";
27
+ readonly creating: "Creating...";
28
+ readonly itemTitle: (item: FileSystemItem) => string;
29
+ readonly emptyDirectory: "Empty directory";
30
+ readonly previewFile: "Preview file";
31
+ readonly createdAt: "Created at";
32
+ readonly modifiedAt: "Last modified at";
33
+ readonly deleteFile: "Delete file";
34
+ readonly deleteFileConfirmation: "Are you sure you want to delete:";
35
+ readonly deleting: "Deleting...";
36
+ readonly delete: "Delete";
37
+ readonly createDirectory: "Create directory";
38
+ readonly uploadFile: "Upload file";
39
+ };
40
+ getFilePreview: (fileItem: FileItem) => Promise<FilePreview>;
41
+ createDirectory?: ((request: CreateDirectoryRequest) => Promise<void>) | undefined;
42
+ uploadFiles?: ((request: UploadFilesRequest) => Promise<void>) | undefined;
43
+ deleteFile?: ((fileItem: FileItem) => Promise<void>) | undefined;
44
+ };
45
+ events: {
46
+ 'upload-file': CustomEvent<UploadFileEvent>;
47
+ 'create-directory': CustomEvent<CreateDirectoryEvent>;
48
+ 'delete-file': CustomEvent<DeleteFileEvent>;
49
+ } & {
50
+ [evt: string]: CustomEvent<any>;
51
+ };
52
+ slots: {
53
+ search: {};
54
+ };
55
+ exports?: {} | undefined;
56
+ bindings?: string | undefined;
57
+ };
58
+ export type FileExplorerProps = typeof __propDef.props;
59
+ export type FileExplorerEvents = typeof __propDef.events;
60
+ export type FileExplorerSlots = typeof __propDef.slots;
61
+ export default class FileExplorer extends SvelteComponent<FileExplorerProps, FileExplorerEvents, FileExplorerSlots> {
62
+ }
63
+ export {};