@securancy/file-explorer 1.0.2 → 1.0.3

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.
@@ -4,9 +4,11 @@
4
4
  import FileExplorerBreadcrumb from "./FileExplorerBreadcrumb.svelte";
5
5
  import FileExplorerDisplayModeSwitcher from "./FileExplorerDisplayModeSwitcher.svelte";
6
6
  import { defaultTranslations, FileExplorerDisplayMode } from "../index.js";
7
- import FileExplorerDirectoryColumn from "./FileExplorerDirectoryColumn.svelte";
8
7
  import FileExplorerCreateDirectory from "./FileExplorerCreateDirectory.svelte";
9
8
  import { Button, Modal } from "@securancy/svelte-components";
9
+ import FileExplorerDirectory from "./FileExplorerDirectory.svelte";
10
+ import { Icon } from "@securancy/svelte-components";
11
+ import { fade } from "svelte/transition";
10
12
  export let displayMode = FileExplorerDisplayMode.Columns;
11
13
  export let filePath;
12
14
  export let documents;
@@ -106,44 +108,57 @@ function deleteFileInternal() {
106
108
  }
107
109
  </script>
108
110
 
109
- <div class="file-explorer file-explorer--display-mode-{displayMode}"
110
- >
111
+ <div class="file-explorer file-explorer--display-mode-{displayMode}">
111
112
  <div class="file-explorer__controls">
112
- <FileExplorerBreadcrumb {filePath} />
113
+ <FileExplorerBreadcrumb {filePath} {routePrefix} />
113
114
  <FileExplorerDisplayModeSwitcher bind:displayMode />
114
115
  </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={translations.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}
116
+ {#if $$slots.search}
117
+ <div class="file-explorer__search">
118
+ <slot name="search" />
119
+ </div>
120
+ {/if}
121
+ {#if loading}
122
+ <div class="file-explorer__loader">
123
+ <Icon class="fa-light fa-spinner-third fa-spin" />
124
+ </div>
125
+ {:else}
126
+ <div
127
+ bind:this={fileExplorerContentContainer}
128
+ class="file-explorer__content"
129
+ on:scroll={handleScroll}
137
130
  >
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>
131
+ <FileExplorerDirectory
132
+ {canEdit}
133
+ currentPath={filePath}
134
+ {displayMode}
135
+ {getFilePreview}
136
+ item={{
137
+ type: 'directory',
138
+ name: '',
139
+ path: '',
140
+ children: $documents,
141
+ }}
142
+ {routePrefix}
143
+ {translations}
144
+ on:create-directory={handleCreateDirectory}
145
+ on:delete-file={handleDeleteFile}
146
+ on:upload-file={handleUploadFile}
147
+ />
148
+ </div>
149
+ {/if}
150
+
151
+ <!-- Button to scroll back to the left -->
152
+ {#if showScrollBackButton}
153
+ <button
154
+ class="file-explorer__scroll-back-button"
155
+ type="button"
156
+ on:click={handleScrollBack}
157
+ transition:fade|local
158
+ >
159
+ <Icon class="fa-light fa-chevron-left" size="large" />
160
+ </button>
161
+ {/if}
147
162
  </div>
148
163
 
149
164
  {#if createDirectory}
@@ -294,8 +309,7 @@ function deleteFileInternal() {
294
309
  margin-left: auto;
295
310
  }
296
311
  .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);
312
+ padding: calc(var(--file-explorer-default-spacing) * 0.5) calc(var(--file-explorer-default-spacing) * 0.5) 0 calc(var(--file-explorer-default-spacing) * 0.5);
299
313
  }
300
314
  .file-explorer__list {
301
315
  margin-bottom: var(--file-explorer-default-spacing);
@@ -439,5 +453,9 @@ function deleteFileInternal() {
439
453
  .file-explorer__loader {
440
454
  display: flex;
441
455
  justify-content: center;
456
+ align-items: center;
457
+ min-height: 50px;
458
+ height: 100%;
459
+ font-size: 25px;
442
460
  }
443
461
  </style>
@@ -19,7 +19,7 @@ declare const __propDef: {
19
19
  documents: Refreshable<FileSystemItem[]>;
20
20
  loading: boolean;
21
21
  routePrefix: string;
22
- canEdit: boolean;
22
+ canEdit: boolean | ((fileItem: FileSystemItem) => boolean);
23
23
  translations?: {
24
24
  createDirectoryModalTitle: string;
25
25
  cancel: string;
@@ -38,6 +38,7 @@ declare const __propDef: {
38
38
  uploadFile: string;
39
39
  size: string;
40
40
  items: string;
41
+ directoryAlreadyExists: string;
41
42
  };
42
43
  getFilePreview: (fileItem: FileItem) => Promise<FilePreview>;
43
44
  createDirectory?: ((request: CreateDirectoryRequest) => Promise<void>) | undefined;
@@ -1,9 +1,26 @@
1
- <script>export let filePath;
1
+ <script>import { Icon } from "@securancy/svelte-components";
2
+ export let filePath;
3
+ export let routePrefix;
4
+ $: parts = filePath.split("/");
5
+ function getHref(parts2, index, routePrefix2) {
6
+ const href = [
7
+ ...routePrefix2.split("/"),
8
+ ...parts2.slice(0, index + 1)
9
+ ].filter((node) => node !== "").map(encodeURIComponent).join("/");
10
+ return "/" + href;
11
+ }
2
12
  </script>
3
13
 
4
- <!-- @todo make it clickable -->
5
14
  <div class="file-explorer__breadcrumb">
6
- {filePath}
15
+ <a href={routePrefix.startsWith('/') ? routePrefix : `/${routePrefix}`} title="Home">
16
+ <Icon class="fa-light fa-house" />
17
+ </a>
18
+ {#each parts as part, index}
19
+ <Icon class="fa-light fa-chevron-right" />
20
+ <a href={getHref(parts, index, routePrefix)} title={parts.slice(0, index + 1).join('/')}>
21
+ {part}
22
+ </a>
23
+ {/each}
7
24
  </div>
8
25
 
9
26
  <style>/* eslint-disable */
@@ -114,8 +131,7 @@
114
131
  margin-left: auto;
115
132
  }
116
133
  .file-explorer__search {
117
- margin-bottom: var(--file-explorer-default-spacing);
118
- padding: calc(var(--file-explorer-default-spacing) * 1.5) var(--file-explorer-default-spacing) 0 var(--file-explorer-default-spacing);
134
+ padding: calc(var(--file-explorer-default-spacing) * 0.5) calc(var(--file-explorer-default-spacing) * 0.5) 0 calc(var(--file-explorer-default-spacing) * 0.5);
119
135
  }
120
136
  .file-explorer__list {
121
137
  margin-bottom: var(--file-explorer-default-spacing);
@@ -259,5 +275,9 @@
259
275
  .file-explorer__loader {
260
276
  display: flex;
261
277
  justify-content: center;
278
+ align-items: center;
279
+ min-height: 50px;
280
+ height: 100%;
281
+ font-size: 25px;
262
282
  }
263
283
  </style>
@@ -2,6 +2,7 @@ import { SvelteComponent } from "svelte";
2
2
  declare const __propDef: {
3
3
  props: {
4
4
  filePath: string;
5
+ routePrefix: string;
5
6
  };
6
7
  events: {
7
8
  [evt: string]: CustomEvent<any>;
@@ -4,6 +4,8 @@ export let createDirectoryModalOpen = false;
4
4
  export let documents;
5
5
  export let createDirectory;
6
6
  export let translations;
7
+ $: existingFoldersInPath = getExistingFoldersInPath(createDirectoryPath, $documents);
8
+ $: directoryAlreadyExists = existingFoldersInPath.some((x) => x.name === request.name);
7
9
  let request;
8
10
  let submitting = false;
9
11
  let input = void 0;
@@ -25,6 +27,16 @@ async function submitFiles() {
25
27
  });
26
28
  documents.refresh();
27
29
  }
30
+ function getExistingFoldersInPath(path, documents2) {
31
+ if (!path || !documents2) return [];
32
+ const pathParts = path.split("/");
33
+ let existingFoldersInPath2 = documents2.filter((x) => x.type === "directory");
34
+ for (const pathPart of pathParts) {
35
+ const currentFolder = existingFoldersInPath2.find((document) => document.name === pathPart);
36
+ existingFoldersInPath2 = currentFolder ? currentFolder.children.filter((x) => x.type === "directory") : [];
37
+ }
38
+ return existingFoldersInPath2;
39
+ }
28
40
  </script>
29
41
 
30
42
  <Modal
@@ -33,8 +45,12 @@ async function submitFiles() {
33
45
  >
34
46
  <div class="create-directory">
35
47
  <span class="create-directory__path">{createDirectoryPath}/</span>
36
- <FormElement clearable>
37
- <input bind:this={input} bind:value={request.name}>
48
+ <FormElement
49
+ clearable
50
+ error={directoryAlreadyExists ? translations.directoryAlreadyExists : undefined}
51
+ floatError
52
+ >
53
+ <input bind:this={input} type="text" bind:value={request.name}>
38
54
  </FormElement>
39
55
  </div>
40
56
  <svelte:fragment slot="footer">
@@ -46,7 +62,7 @@ async function submitFiles() {
46
62
  {translations.cancel}
47
63
  </Button>
48
64
  <Button
49
- disabled={(!request.name || request.name.length === 0) || submitting}
65
+ disabled={(!request.name || request.name.length === 0) || directoryAlreadyExists || submitting}
50
66
  on:click={submitFiles}
51
67
  >
52
68
  {#if submitting}
@@ -0,0 +1,323 @@
1
+ <script>import {} from "..";
2
+ import FileExplorerDirectoryColumn from "./FileExplorerDirectoryColumn.svelte";
3
+ import FileExplorerFileDetailColumn from "./FileExplorerFileDetailColumn.svelte";
4
+ import FileExplorerDirectoryIndex from "./FileExplorerDirectoryIndex.svelte";
5
+ import { joinPaths } from "../utilities";
6
+ export let item;
7
+ export let translations;
8
+ export let routePrefix;
9
+ export let currentPath;
10
+ export let canEdit;
11
+ export let displayMode;
12
+ export let getFilePreview;
13
+ $: activeFileItem = item.children.find((fi) => currentPath.startsWith(joinPaths(fi.path, fi.name)));
14
+ function canEditItem(item2, canEdit2) {
15
+ return typeof canEdit2 === "function" ? canEdit2(item2) : canEdit2 === true;
16
+ }
17
+ </script>
18
+
19
+ {#if displayMode === 'columns'}
20
+ <FileExplorerDirectoryColumn
21
+ {activeFileItem}
22
+ canEdit={canEditItem(item, canEdit)}
23
+ {item}
24
+ {routePrefix}
25
+ {translations}
26
+ on:upload-file
27
+ on:create-directory
28
+ />
29
+ {:else}
30
+ <FileExplorerDirectoryIndex
31
+ {activeFileItem}
32
+ canEdit={canEditItem(item, canEdit)}
33
+ {item}
34
+ {routePrefix}
35
+ {translations}
36
+ on:upload-file
37
+ on:create-directory
38
+ />
39
+ {/if}
40
+
41
+ <!-- The component repeats itself until there is no selected item or it reaches a fileItem -->
42
+ {#if activeFileItem}
43
+ {#if activeFileItem.type === 'directory'}
44
+ <svelte:self
45
+ {canEdit}
46
+ {currentPath}
47
+ {displayMode}
48
+ {getFilePreview}
49
+ item={activeFileItem}
50
+ {routePrefix}
51
+ {translations}
52
+ on:upload-file
53
+ on:create-directory
54
+ on:delete-file
55
+ />
56
+ {:else}
57
+ <FileExplorerFileDetailColumn
58
+ canEdit={canEditItem(activeFileItem, canEdit)}
59
+ fileItem={activeFileItem}
60
+ {getFilePreview}
61
+ {translations}
62
+ on:delete-file
63
+ />
64
+ {/if}
65
+ {/if}
66
+ <style>/* eslint-disable */
67
+ /**
68
+ * Media queries and devices
69
+ */
70
+ /**
71
+ * Some mixins that can be used dynamically inside @each loops
72
+ */
73
+ .file-explorer {
74
+ position: relative;
75
+ display: flex;
76
+ flex-direction: column;
77
+ height: 100%;
78
+ width: 100%;
79
+ background: var(--file-explorer-background);
80
+ overflow: hidden;
81
+ }
82
+ .file-explorer__controls {
83
+ flex-shrink: 0;
84
+ display: flex;
85
+ border-bottom: var(--file-explorer-border-style);
86
+ padding: var(--file-explorer-default-spacing);
87
+ background: var(--file-explorer-background-shade);
88
+ }
89
+ .file-explorer__content {
90
+ flex-grow: 1;
91
+ display: flex;
92
+ overflow-x: auto;
93
+ scroll-behavior: smooth;
94
+ }
95
+ /* Keep the index within screen */
96
+ @media (width >= 769px) {
97
+ /* @formatter:off */
98
+ .file-explorer--display-mode-index .file-explorer__content {
99
+ /* @formatter:on */
100
+ overflow-x: inherit;
101
+ }
102
+ }
103
+ .file-explorer__column, .file-explorer__index {
104
+ flex-shrink: 0;
105
+ border-right: var(--file-explorer-border-style);
106
+ background: var(--file-explorer-background);
107
+ }
108
+ .file-explorer__column {
109
+ display: flex;
110
+ flex-direction: column;
111
+ width: var(--file-explorer-column-default-width);
112
+ min-width: var(--file-explorer-column-min-width);
113
+ overflow: hidden;
114
+ }
115
+ .file-explorer__column--file-detail {
116
+ flex-grow: 1;
117
+ width: var(--file-explorer-preview-default-width);
118
+ display: flex;
119
+ flex-direction: column;
120
+ }
121
+ .file-explorer__column--sticky {
122
+ position: sticky;
123
+ left: 0;
124
+ z-index: 1;
125
+ }
126
+ .file-explorer__column::after {
127
+ content: '';
128
+ position: absolute;
129
+ inset: 0;
130
+ pointer-events: none;
131
+ transition: background-color 0.2s ease-in-out;
132
+ }
133
+ .file-explorer__column--dragover {
134
+ cursor: copy;
135
+ }
136
+ .file-explorer__column--dragover::after {
137
+ pointer-events: all;
138
+ background-color: var(--file-explorer-background-dragover);
139
+ }
140
+ .file-explorer__index {
141
+ display: flex;
142
+ flex-direction: column;
143
+ width: var(--file-explorer-index-default-width);
144
+ overflow: hidden;
145
+ }
146
+ @media (width >= 769px) {
147
+ .file-explorer__index {
148
+ flex-shrink: 1
149
+ }
150
+ }
151
+ .file-explorer__index--hidden {
152
+ display: none;
153
+ }
154
+ .file-explorer__column-scroll-container, .file-explorer__index-scroll-container {
155
+ flex-grow: 1;
156
+ overflow-y: auto;
157
+ padding: calc(var(--file-explorer-default-spacing) * 1.5) var(--file-explorer-default-spacing) 0 var(--file-explorer-default-spacing);
158
+ }
159
+ .file-explorer__grid {
160
+ display: flex;
161
+ align-items: flex-start;
162
+ flex-wrap: wrap;
163
+ gap: var(--file-explorer-default-spacing);
164
+ }
165
+ .file-explorer__breadcrumb {
166
+ /* @todo */
167
+ }
168
+ .file-explorer__display-mode-switcher {
169
+ display: flex;
170
+ gap: calc(0.5 * var(--file-explorer-default-spacing));
171
+ margin-left: auto;
172
+ }
173
+ .file-explorer__search {
174
+ padding: calc(var(--file-explorer-default-spacing) * 0.5) calc(var(--file-explorer-default-spacing) * 0.5) 0 calc(var(--file-explorer-default-spacing) * 0.5);
175
+ }
176
+ .file-explorer__list {
177
+ margin-bottom: var(--file-explorer-default-spacing);
178
+ }
179
+ .file-explorer__list-header {
180
+ display: flex;
181
+ justify-content: space-between;
182
+ gap: calc(0.5 * var(--file-explorer-default-spacing));
183
+ width: 100%;
184
+ margin-bottom: calc(0.5 * var(--file-explorer-default-spacing));
185
+ color: var(--file-explorer-list-header-color);
186
+ }
187
+ .file-explorer__list-header span {
188
+ overflow: hidden;
189
+ white-space: nowrap;
190
+ text-overflow: ellipsis;
191
+ }
192
+ .file-explorer__list-header button {
193
+ flex-shrink: 0;
194
+ color: var(--file-explorer-action-primary-color);
195
+ cursor: pointer;
196
+ }
197
+ .file-explorer__grid-header {
198
+ width: 100%;
199
+ }
200
+ .file-explorer__grid-header button {
201
+ color: var(--file-explorer-action-primary-color);
202
+ cursor: pointer;
203
+ }
204
+ .file-explorer__list-item, .file-explorer__grid-item {
205
+ border-radius: var(--file-explorer-list-item-border-radius);
206
+ background: none;
207
+ color: var(--file-explorer-list-item-color);
208
+ }
209
+ .file-explorer__list-item > :global(i), .file-explorer__grid-item > :global(i) {
210
+ flex-shrink: 0;
211
+ }
212
+ .file-explorer__list-item:not(.file-explorer__list-item--active):hover, .file-explorer__grid-item:not(.file-explorer__grid-item--active):hover {
213
+ background: var(--file-explorer-list-item-background-hover);
214
+ }
215
+ .file-explorer__list-item--active, .file-explorer__grid-item--active {
216
+ color: var(--file-explorer-list-item-color-active);
217
+ background: var(--file-explorer-list-item-background-active);
218
+ }
219
+ .file-explorer__grid-item {
220
+ display: flex;
221
+ flex-direction: column;
222
+ align-items: center;
223
+
224
+ /* 3 items in a grid, spaced out with default spacing means the width of 2 gaps must be subtracted from width */
225
+ width: calc(33.33% - (0.6666 * var(--file-explorer-default-spacing)));
226
+ padding: 5px 2px;
227
+ }
228
+ @media (width >= 769px) {
229
+ .file-explorer__grid-item {
230
+ width: 100px
231
+ }
232
+ }
233
+ .file-explorer__grid-item--active {
234
+ /* @todo */
235
+ }
236
+ .file-explorer__grid-item > span {
237
+ word-break: break-word;
238
+ margin-top: calc(0.25 * var(--file-explorer-default-spacing));
239
+ }
240
+ .file-explorer__list-item {
241
+ display: flex;
242
+ align-items: baseline;
243
+ width: 100%;
244
+ text-align: left;
245
+ padding: var(--file-explorer-list-item-spacing);
246
+ white-space: nowrap;
247
+ overflow: hidden;
248
+ text-overflow: ellipsis;
249
+ }
250
+ .file-explorer__list-item > span {
251
+ flex-grow: 1;
252
+ min-width: 0;
253
+ overflow: hidden;
254
+ text-overflow: ellipsis;
255
+ }
256
+ .file-explorer__list-item > :global(i:first-child) {
257
+ margin-right: var(--file-explorer-list-item-spacing);
258
+ }
259
+ .file-explorer__list-item > :global(i:last-child) {
260
+ margin-left: var(--file-explorer-list-item-spacing);
261
+ }
262
+ .file-explorer__message {
263
+ display: block;
264
+ padding: var(--file-explorer-list-item-spacing) 0;
265
+ color: var(--file-explorer-message-color);
266
+ }
267
+ .file-explorer__image-preview {
268
+ max-width: 100%;
269
+ margin: 0 auto;
270
+ }
271
+ .file-explorer-preview__title {
272
+ font-size: var(--font-size--h1);
273
+ font-weight: var(--font-weight--semi-bold);
274
+ overflow: hidden;
275
+ text-overflow: ellipsis;
276
+ white-space: nowrap;
277
+ }
278
+ .file-explorer-preview__download {
279
+ color: var(--file-explorer-action-primary-color);
280
+ font-weight: var(--font-weight--semi-bold);
281
+ }
282
+ .file-explorer-preview__download:hover {
283
+ text-decoration-line: underline;
284
+ }
285
+ .file-explorer-preview__info {
286
+ margin-top: 14px;
287
+ }
288
+ .file-explorer-preview__info span {
289
+ color: var(--file-explorer-message-color);
290
+ }
291
+ .file-explorer-preview__info button {
292
+ cursor: pointer;
293
+ }
294
+ .file-explorer__scroll-back-button {
295
+ position: fixed;
296
+ bottom: var(--file-explorer-default-spacing);
297
+ right: var(--file-explorer-default-spacing);
298
+ z-index: 2;
299
+ background: var(--file-explorer-action-primary-color);
300
+ color: var(--file-explorer-action-primary-color-contrast);
301
+ padding: var(--file-explorer-list-item-spacing);
302
+ border-radius: var(--file-explorer-list-item-border-radius);
303
+ }
304
+ .file-explorer__file-icon--large {
305
+ width: 100%;
306
+ text-align: center;
307
+ padding: calc(2 * var(--file-explorer-default-spacing));
308
+ }
309
+ .file-explorer__file-icon--large :global(i) {
310
+ font-size: var(--file-explorer-preview-icon-size);
311
+ }
312
+ .file-explorer__text--semi-bold {
313
+ font-weight: var(--file-explorer-text-semi-bold);
314
+ }
315
+ .file-explorer__loader {
316
+ display: flex;
317
+ justify-content: center;
318
+ align-items: center;
319
+ min-height: 50px;
320
+ height: 100%;
321
+ font-size: 25px;
322
+ }
323
+ </style>
@@ -0,0 +1,30 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import type { FileExplorerDisplayMode, FileSystemItem } from '..';
3
+ import { type DirectoryItem, type DocumentTranslations, type FileItem, type FilePreview } from '..';
4
+ declare const __propDef: {
5
+ props: {
6
+ item: DirectoryItem;
7
+ translations: DocumentTranslations;
8
+ routePrefix: string;
9
+ currentPath: string;
10
+ canEdit: boolean | ((fileItem: FileSystemItem) => boolean);
11
+ displayMode: FileExplorerDisplayMode;
12
+ getFilePreview: (fileItem: FileItem) => Promise<FilePreview>;
13
+ };
14
+ events: {
15
+ 'upload-file': CustomEvent<import("..").UploadFileEvent>;
16
+ 'create-directory': CustomEvent<import("..").CreateDirectoryEvent>;
17
+ 'delete-file': CustomEvent<import("..").DeleteFileEvent>;
18
+ } & {
19
+ [evt: string]: CustomEvent<any>;
20
+ };
21
+ slots: {};
22
+ exports?: {} | undefined;
23
+ bindings?: string | undefined;
24
+ };
25
+ export type FileExplorerDirectoryProps = typeof __propDef.props;
26
+ export type FileExplorerDirectoryEvents = typeof __propDef.events;
27
+ export type FileExplorerDirectorySlots = typeof __propDef.slots;
28
+ export default class FileExplorerDirectory extends SvelteComponent<FileExplorerDirectoryProps, FileExplorerDirectoryEvents, FileExplorerDirectorySlots> {
29
+ }
30
+ export {};
@@ -1,42 +1,24 @@
1
1
  <script>import { createEventDispatcher } from "svelte";
2
- import { fade } from "svelte/transition";
3
- import { FileExplorerDisplayMode } from "..";
2
+ import {} from "..";
4
3
  import { resize, ResizeDirections } from "@securancy/svelte-utilities";
5
4
  import { Icon, Popover, PopoverItem } from "@securancy/svelte-components";
6
5
  import { getExtension, openItem } from "../utilities/index.js";
7
6
  import FileIcon from "./FileIcon.svelte";
8
- import FileExplorerFileDetailColumn from "./FileExplorerFileDetailColumn.svelte";
9
- import FileExplorerDirectoryIndex from "./FileExplorerDirectoryIndex.svelte";
7
+ import { joinPaths } from "../utilities/index.js";
10
8
  const dispatch = createEventDispatcher();
11
- export let directoryName;
12
- export let filePath;
13
- export let currentPath;
14
- export let fileItems;
15
- export let documents;
16
- export let displayMode;
9
+ export let item;
17
10
  export let sticky = false;
18
- export let showScrollBackButton = false;
19
- export let loading;
20
11
  export let routePrefix;
21
12
  export let canEdit;
22
13
  export let translations;
23
- export let getFilePreview;
24
- let currentFileItem;
25
- let activeFileItem;
26
- let filePathSplit;
14
+ export let activeFileItem;
27
15
  let dragoverIndex = 0;
28
16
  let innerWidth = 0;
29
- $: filePathSplit = filePath.split("/");
30
- $: currentFileItem = filePathSplit.shift();
31
- $: activeFileItem = fileItems.find((fi) => fi.name === currentFileItem);
32
- function handleScrollBack() {
33
- dispatch("scroll-back");
34
- }
35
17
  function addItem() {
36
- dispatch("upload-file", { path: currentPath });
18
+ dispatch("upload-file", { path: joinPaths(item.path, item.name) });
37
19
  }
38
20
  function createDirectory() {
39
- dispatch("create-directory", { path: currentPath });
21
+ dispatch("create-directory", { path: joinPaths(item.path, item.name) });
40
22
  }
41
23
  function handleDragEnter(event) {
42
24
  event.preventDefault();
@@ -47,7 +29,7 @@ function handleDrop(event) {
47
29
  event.preventDefault();
48
30
  event.stopPropagation();
49
31
  dragoverIndex = 0;
50
- const eventData = { path: currentPath };
32
+ const eventData = { path: joinPaths(item.path, item.name) };
51
33
  if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) {
52
34
  eventData.files = event.dataTransfer.files;
53
35
  }
@@ -89,49 +71,42 @@ function handleDragOver(event) {
89
71
  parentPadding: true,
90
72
  }}
91
73
  >
92
- <slot name="top" />
93
74
  <div class="file-explorer__column-scroll-container">
94
75
  <div class="file-explorer__list">
95
76
  <div class="file-explorer__list-header">
96
- {#if !loading}
97
- <span>
98
- {directoryName} ({fileItems?.length ?? 0})
99
- </span>
100
- {#if canEdit}
101
- <Popover placement="bottom-end" trigger="click">
102
- <button slot="trigger" type="button">
103
- <Icon class="fa-solid fa-add" />
104
- </button>
105
- <PopoverItem closeOnClick on:click={createDirectory}>{translations.createDirectory}</PopoverItem>
106
- <PopoverItem closeOnClick on:click={addItem}>{translations.uploadFile}</PopoverItem>
107
- </Popover>
108
- {/if}
77
+ <span>
78
+ {item.name === '' ? translations.items : item.name} ({item.children.length})
79
+ </span>
80
+ {#if canEdit}
81
+ <Popover placement="bottom-end" trigger="click">
82
+ <button slot="trigger" type="button">
83
+ <Icon class="fa-solid fa-add" />
84
+ </button>
85
+ <PopoverItem closeOnClick on:click={createDirectory}>{translations.createDirectory}</PopoverItem>
86
+ <PopoverItem closeOnClick on:click={addItem}>{translations.uploadFile}</PopoverItem>
87
+ </Popover>
109
88
  {/if}
110
89
  </div>
111
- {#if fileItems.length > 0}
112
- {#each fileItems as item}
90
+ {#if item.children.length > 0}
91
+ {#each item.children as child}
113
92
  <button
114
93
  class="file-explorer__list-item"
115
- class:file-explorer__list-item--active={activeFileItem === item}
116
- title={translations.itemTitle(item)}
94
+ class:file-explorer__list-item--active={activeFileItem === child}
95
+ title={translations.itemTitle(child)}
117
96
  type="button"
118
- on:click={() => openItem(item, routePrefix)}
97
+ on:click={() => openItem(child, routePrefix)}
119
98
  >
120
- {#if item.type === 'directory'}
121
- <Icon class="fa-light fa-folder{activeFileItem === item ? '-open' : ''}" />
99
+ {#if child.type === 'directory'}
100
+ <Icon class="fa-light fa-folder{activeFileItem === child ? '-open' : ''}" />
122
101
  {:else}
123
- <FileIcon extension={getExtension(item.name)} />
102
+ <FileIcon extension={getExtension(child.name)} />
124
103
  {/if}
125
- <span>{item.name}</span>
126
- {#if item.type === 'directory'}
104
+ <span>{child.name}</span>
105
+ {#if child.type === 'directory'}
127
106
  <Icon class="fa-light fa-sm fa-chevron-right" size="custom" />
128
107
  {/if}
129
108
  </button>
130
109
  {/each}
131
- {:else if loading}
132
- <div class="file-explorer__loader">
133
- <Icon class="fa-light fa-spinner-third fa-spin" />
134
- </div>
135
110
  {:else}
136
111
  <p class="file-explorer__message">
137
112
  {translations.emptyDirectory}
@@ -141,64 +116,6 @@ function handleDragOver(event) {
141
116
  </div>
142
117
  </div>
143
118
 
144
- <!-- The component repeats itself until the end of the filePath is reached or shows a FileDetail -->
145
- {#if activeFileItem}
146
- {#if activeFileItem.type === 'directory'}
147
- {#if displayMode === FileExplorerDisplayMode.Columns}
148
- <svelte:self
149
- {canEdit}
150
- currentPath={[activeFileItem.path, activeFileItem.name].join('/')}
151
- directoryName={currentFileItem || ''}
152
- {displayMode}
153
- {documents}
154
- fileItems={activeFileItem.children || []}
155
- filePath={filePathSplit.join('/')}
156
- {getFilePreview}
157
- {loading}
158
- {routePrefix}
159
- {translations}
160
- on:upload-file
161
- on:create-directory
162
- on:delete-file
163
- />
164
- {:else}
165
- <FileExplorerDirectoryIndex
166
- {canEdit}
167
- currentPath={[activeFileItem.path, activeFileItem.name].join('/')}
168
- directoryName={currentFileItem || ''}
169
- {documents}
170
- fileItems={activeFileItem.children || []}
171
- filePath={filePathSplit.join('/')}
172
- {getFilePreview}
173
- {routePrefix}
174
- {translations}
175
- on:upload-file
176
- on:create-directory
177
- on:delete-file
178
- />
179
- {/if}
180
- {:else}
181
- <FileExplorerFileDetailColumn
182
- fileItem={activeFileItem}
183
- {getFilePreview}
184
- {translations}
185
- on:delete-file
186
- />
187
- {/if}
188
- {/if}
189
-
190
- <!-- Button to scroll back to the left -->
191
- {#if showScrollBackButton}
192
- <button
193
- class="file-explorer__scroll-back-button"
194
- type="button"
195
- on:click={handleScrollBack}
196
- transition:fade|local
197
- >
198
- <Icon class="fa-light fa-chevron-left" />
199
- </button>
200
- {/if}
201
-
202
119
  <style>/* eslint-disable */
203
120
  /**
204
121
  * Media queries and devices
@@ -307,8 +224,7 @@ function handleDragOver(event) {
307
224
  margin-left: auto;
308
225
  }
309
226
  .file-explorer__search {
310
- margin-bottom: var(--file-explorer-default-spacing);
311
- padding: calc(var(--file-explorer-default-spacing) * 1.5) var(--file-explorer-default-spacing) 0 var(--file-explorer-default-spacing);
227
+ padding: calc(var(--file-explorer-default-spacing) * 0.5) calc(var(--file-explorer-default-spacing) * 0.5) 0 calc(var(--file-explorer-default-spacing) * 0.5);
312
228
  }
313
229
  .file-explorer__list {
314
230
  margin-bottom: var(--file-explorer-default-spacing);
@@ -452,5 +368,9 @@ function handleDragOver(event) {
452
368
  .file-explorer__loader {
453
369
  display: flex;
454
370
  justify-content: center;
371
+ align-items: center;
372
+ min-height: 50px;
373
+ height: 100%;
374
+ font-size: 25px;
455
375
  }
456
376
  </style>
@@ -1,33 +1,24 @@
1
1
  import { SvelteComponent } from "svelte";
2
- import { type DocumentTranslations, FileExplorerDisplayMode, type FileItem, type FilePreview, type FileSystemItem } from '..';
3
- import type { Refreshable } from '@securancy/svelte-utilities';
2
+ import type { UploadFileEvent } from './FileExplorer.svelte';
3
+ import { type DirectoryItem, type DocumentTranslations, type FileSystemItem } from '..';
4
+ import type { CreateDirectoryEvent } from './FileExplorer.svelte';
4
5
  declare const __propDef: {
5
6
  props: {
6
- directoryName: string;
7
- filePath: string;
8
- currentPath: string;
9
- fileItems: FileSystemItem[];
10
- documents: Refreshable<FileSystemItem[]>;
11
- displayMode: FileExplorerDisplayMode;
7
+ item: DirectoryItem;
12
8
  sticky?: boolean;
13
- showScrollBackButton?: boolean;
14
- loading: boolean;
15
9
  routePrefix: string;
16
10
  canEdit: boolean;
17
11
  translations: DocumentTranslations;
18
- getFilePreview: (fileItem: FileItem) => Promise<FilePreview>;
12
+ activeFileItem: FileSystemItem | undefined;
19
13
  };
20
14
  events: {
21
- 'upload-file': CustomEvent<any>;
22
- 'create-directory': CustomEvent<any>;
23
- 'delete-file': CustomEvent<import("..").DeleteFileEvent>;
24
15
  'scroll-back': CustomEvent<void>;
16
+ 'upload-file': CustomEvent<UploadFileEvent>;
17
+ 'create-directory': CustomEvent<CreateDirectoryEvent>;
25
18
  } & {
26
19
  [evt: string]: CustomEvent<any>;
27
20
  };
28
- slots: {
29
- top: {};
30
- };
21
+ slots: {};
31
22
  exports?: {} | undefined;
32
23
  bindings?: string | undefined;
33
24
  };
@@ -1,31 +1,20 @@
1
1
  <script>import FileIcon from "./FileIcon.svelte";
2
- import FileExplorerFileDetailColumn from "./FileExplorerFileDetailColumn.svelte";
3
2
  import { createEventDispatcher } from "svelte";
4
3
  import { resize, ResizeDirections } from "@securancy/svelte-utilities";
5
4
  import { Icon, Popover, PopoverItem } from "@securancy/svelte-components";
6
- import { getExtension, openItem } from "../utilities/index.js";
5
+ import { getExtension, joinPaths, openItem } from "../utilities/index.js";
7
6
  const dispatch = createEventDispatcher();
8
- export let directoryName;
9
- export let filePath;
10
- export let fileItems;
11
- export let currentPath;
7
+ export let item;
12
8
  export let routePrefix;
13
9
  export let translations;
14
10
  export let canEdit;
15
- export let documents;
16
- export let getFilePreview;
17
- let currentFileItem;
18
- let activeFileItem;
19
- let filePathSplit;
11
+ export let activeFileItem;
20
12
  let innerWidth = 0;
21
- $: filePathSplit = filePath.split("/");
22
- $: currentFileItem = filePathSplit.shift();
23
- $: activeFileItem = fileItems.find((fi) => fi.name === currentFileItem);
24
13
  function addItem() {
25
- dispatch("upload-file", { path: currentPath });
14
+ dispatch("upload-file", { path: joinPaths(item.path, item.name) });
26
15
  }
27
16
  function createDirectory() {
28
- dispatch("create-directory", { path: currentPath });
17
+ dispatch("create-directory", { path: joinPaths(item.path, item.name) });
29
18
  }
30
19
  </script>
31
20
 
@@ -44,7 +33,7 @@ function createDirectory() {
44
33
  <div class="file-explorer__grid">
45
34
  <div class="file-explorer__grid-header">
46
35
  <span>
47
- {directoryName} ({fileItems?.length ?? 0})
36
+ {item.name === '' ? translations.items : item.name} ({item.children.length})
48
37
  </span>
49
38
  {#if canEdit}
50
39
  <div class="inline-block">
@@ -58,23 +47,23 @@ function createDirectory() {
58
47
  </div>
59
48
  {/if}
60
49
  </div>
61
- {#each fileItems as item}
50
+ {#each item.children as child}
62
51
  <button
63
52
  class="file-explorer__grid-item"
64
- class:file-explorer__grid-item--active={activeFileItem === item}
65
- title={translations.itemTitle(item)}
53
+ class:file-explorer__grid-item--active={activeFileItem === child}
54
+ title={translations.itemTitle(child)}
66
55
  type="button"
67
- on:click={() => openItem(item, routePrefix)}
56
+ on:click={() => openItem(child, routePrefix)}
68
57
  >
69
- {#if item.type === 'directory'}
58
+ {#if child.type === 'directory'}
70
59
  <Icon
71
- class="fa-light fa-2xl fa-folder{activeFileItem?.name === item.name ? '-open' : ''}"
60
+ class="fa-light fa-2xl fa-folder{activeFileItem?.name === child.name ? '-open' : ''}"
72
61
  size="custom"
73
62
  />
74
63
  {:else}
75
- <FileIcon extension={getExtension(item.name)} size="grid" />
64
+ <FileIcon extension={getExtension(child.name)} size="grid" />
76
65
  {/if}
77
- <span>{item.name}</span>
66
+ <span>{child.name}</span>
78
67
  </button>
79
68
  {:else}
80
69
  <p class="file-explorer__message">
@@ -86,30 +75,6 @@ function createDirectory() {
86
75
  </div>
87
76
  {/if}
88
77
 
89
- <!-- The component repeats itself until the end of the filePath is reached or shows a FileDetail -->
90
- {#if activeFileItem}
91
- {#if activeFileItem.type === 'directory'}
92
- <svelte:self
93
- {canEdit}
94
- currentPath={[activeFileItem.path, activeFileItem.name].join('/')}
95
- directoryName={currentFileItem || ''}
96
- {documents}
97
- fileItems={activeFileItem.children || []}
98
- filePath={filePathSplit.join('/')}
99
- {getFilePreview}
100
- {routePrefix}
101
- {translations}
102
- />
103
- {:else}
104
- <FileExplorerFileDetailColumn
105
- fileItem={activeFileItem}
106
- {getFilePreview}
107
- {translations}
108
- on:delete-file
109
- />
110
- {/if}
111
- {/if}
112
-
113
78
  <style>/* eslint-disable */
114
79
  /**
115
80
  * Media queries and devices
@@ -218,8 +183,7 @@ function createDirectory() {
218
183
  margin-left: auto;
219
184
  }
220
185
  .file-explorer__search {
221
- margin-bottom: var(--file-explorer-default-spacing);
222
- padding: calc(var(--file-explorer-default-spacing) * 1.5) var(--file-explorer-default-spacing) 0 var(--file-explorer-default-spacing);
186
+ padding: calc(var(--file-explorer-default-spacing) * 0.5) calc(var(--file-explorer-default-spacing) * 0.5) 0 calc(var(--file-explorer-default-spacing) * 0.5);
223
187
  }
224
188
  .file-explorer__list {
225
189
  margin-bottom: var(--file-explorer-default-spacing);
@@ -363,5 +327,9 @@ function createDirectory() {
363
327
  .file-explorer__loader {
364
328
  display: flex;
365
329
  justify-content: center;
330
+ align-items: center;
331
+ min-height: 50px;
332
+ height: 100%;
333
+ font-size: 25px;
366
334
  }
367
335
  </style>
@@ -1,21 +1,15 @@
1
1
  import { SvelteComponent } from "svelte";
2
- import type { DocumentTranslations, FileItem, FilePreview, FileSystemItem } from '../index.js';
3
- import type { Refreshable } from '@securancy/svelte-utilities';
2
+ import type { DirectoryItem, DocumentTranslations, FileSystemItem } from '../index.js';
4
3
  import type { CreateDirectoryEvent, UploadFileEvent } from './FileExplorer.svelte';
5
4
  declare const __propDef: {
6
5
  props: {
7
- directoryName: string;
8
- filePath: string;
9
- fileItems: FileSystemItem[];
10
- currentPath: string;
6
+ item: DirectoryItem;
11
7
  routePrefix: string;
12
8
  translations: DocumentTranslations;
13
9
  canEdit: boolean;
14
- documents: Refreshable<FileSystemItem[]>;
15
- getFilePreview: (fileItem: FileItem) => Promise<FilePreview>;
10
+ activeFileItem: FileSystemItem | undefined;
16
11
  };
17
12
  events: {
18
- 'delete-file': CustomEvent<import("./FileExplorer.svelte").DeleteFileEvent>;
19
13
  'upload-file': CustomEvent<UploadFileEvent>;
20
14
  'create-directory': CustomEvent<CreateDirectoryEvent>;
21
15
  } & {
@@ -136,8 +136,7 @@ const modeIcons = {
136
136
  margin-left: auto;
137
137
  }
138
138
  .file-explorer__search {
139
- margin-bottom: var(--file-explorer-default-spacing);
140
- padding: calc(var(--file-explorer-default-spacing) * 1.5) var(--file-explorer-default-spacing) 0 var(--file-explorer-default-spacing);
139
+ padding: calc(var(--file-explorer-default-spacing) * 0.5) calc(var(--file-explorer-default-spacing) * 0.5) 0 calc(var(--file-explorer-default-spacing) * 0.5);
141
140
  }
142
141
  .file-explorer__list {
143
142
  margin-bottom: var(--file-explorer-default-spacing);
@@ -281,6 +280,10 @@ const modeIcons = {
281
280
  .file-explorer__loader {
282
281
  display: flex;
283
282
  justify-content: center;
283
+ align-items: center;
284
+ min-height: 50px;
285
+ height: 100%;
286
+ font-size: 25px;
284
287
  }
285
288
  label {
286
289
  position: relative;
@@ -7,6 +7,7 @@ import { createEventDispatcher } from "svelte";
7
7
  export let fileItem;
8
8
  export let getFilePreview;
9
9
  export let translations;
10
+ export let canEdit;
10
11
  const dispatch = createEventDispatcher();
11
12
  const fileDateFormat = "cccc FF";
12
13
  let filePreview;
@@ -71,11 +72,13 @@ async function refresh(_fileItem) {
71
72
  <div>{DateTime.fromJSDate(filePreview.modifiedAt).toFormat(fileDateFormat)}</div>
72
73
  </div>
73
74
  {/if}
74
- <div class="file-explorer-preview__info">
75
- <button type="button" on:click={() => dispatch('delete-file', fileItem)}>
76
- <Icon class="fa-solid fa-trash" />
77
- </button>
78
- </div>
75
+ {#if canEdit}
76
+ <div class="file-explorer-preview__info">
77
+ <button type="button" on:click={() => dispatch('delete-file', fileItem)}>
78
+ <Icon class="fa-solid fa-trash" />
79
+ </button>
80
+ </div>
81
+ {/if}
79
82
  </Card>
80
83
  {:else}
81
84
  <!-- @todo add loader if needed (test performance) -->
@@ -191,8 +194,7 @@ async function refresh(_fileItem) {
191
194
  margin-left: auto;
192
195
  }
193
196
  .file-explorer__search {
194
- margin-bottom: var(--file-explorer-default-spacing);
195
- padding: calc(var(--file-explorer-default-spacing) * 1.5) var(--file-explorer-default-spacing) 0 var(--file-explorer-default-spacing);
197
+ padding: calc(var(--file-explorer-default-spacing) * 0.5) calc(var(--file-explorer-default-spacing) * 0.5) 0 calc(var(--file-explorer-default-spacing) * 0.5);
196
198
  }
197
199
  .file-explorer__list {
198
200
  margin-bottom: var(--file-explorer-default-spacing);
@@ -336,5 +338,9 @@ async function refresh(_fileItem) {
336
338
  .file-explorer__loader {
337
339
  display: flex;
338
340
  justify-content: center;
341
+ align-items: center;
342
+ min-height: 50px;
343
+ height: 100%;
344
+ font-size: 25px;
339
345
  }
340
346
  </style>
@@ -6,6 +6,7 @@ declare const __propDef: {
6
6
  fileItem: FileItem;
7
7
  getFilePreview: (fileItem: FileItem) => Promise<FilePreview>;
8
8
  translations: DocumentTranslations;
9
+ canEdit: boolean;
9
10
  };
10
11
  events: {
11
12
  'delete-file': CustomEvent<DeleteFileEvent>;
@@ -161,8 +161,7 @@ function determineIconString(_extension) {
161
161
  margin-left: auto;
162
162
  }
163
163
  .file-explorer__search {
164
- margin-bottom: var(--file-explorer-default-spacing);
165
- padding: calc(var(--file-explorer-default-spacing) * 1.5) var(--file-explorer-default-spacing) 0 var(--file-explorer-default-spacing);
164
+ padding: calc(var(--file-explorer-default-spacing) * 0.5) calc(var(--file-explorer-default-spacing) * 0.5) 0 calc(var(--file-explorer-default-spacing) * 0.5);
166
165
  }
167
166
  .file-explorer__list {
168
167
  margin-bottom: var(--file-explorer-default-spacing);
@@ -306,5 +305,9 @@ function determineIconString(_extension) {
306
305
  .file-explorer__loader {
307
306
  display: flex;
308
307
  justify-content: center;
308
+ align-items: center;
309
+ min-height: 50px;
310
+ height: 100%;
311
+ font-size: 25px;
309
312
  }
310
313
  </style>
@@ -121,8 +121,7 @@
121
121
  }
122
122
 
123
123
  &__search {
124
- margin-bottom: var(--file-explorer-default-spacing);
125
- padding: calc(var(--file-explorer-default-spacing) * 1.5) var(--file-explorer-default-spacing) 0 var(--file-explorer-default-spacing);
124
+ padding: calc(var(--file-explorer-default-spacing) * 0.5) calc(var(--file-explorer-default-spacing) * 0.5) 0 calc(var(--file-explorer-default-spacing) * 0.5);
126
125
  }
127
126
 
128
127
  &__list {
@@ -300,5 +299,9 @@
300
299
  &__loader {
301
300
  display: flex;
302
301
  justify-content: center;
302
+ align-items: center;
303
+ min-height: 50px;
304
+ height: 100%;
305
+ font-size: 25px;
303
306
  }
304
307
  }
@@ -17,5 +17,6 @@ export declare const defaultTranslations: {
17
17
  uploadFile: string;
18
18
  size: string;
19
19
  items: string;
20
+ directoryAlreadyExists: string;
20
21
  };
21
22
  export type DocumentTranslations = typeof defaultTranslations;
@@ -16,4 +16,5 @@ export const defaultTranslations = {
16
16
  uploadFile: 'Upload file',
17
17
  size: 'Size',
18
18
  items: 'Items',
19
+ directoryAlreadyExists: 'Directory already exists',
19
20
  };
@@ -6,3 +6,4 @@ export declare const videoExtensions: string[];
6
6
  export declare function isImage(path: string): boolean;
7
7
  export declare function formatBytes(bytes: number, decimals?: number): string;
8
8
  export declare function formatBytesParts(bytes: number, decimals?: number): [number, string];
9
+ export declare function joinPaths(...paths: string[]): string;
@@ -47,3 +47,8 @@ export function formatBytesParts(bytes, decimals = 2) {
47
47
  const index = Math.floor(Math.log(bytes) / Math.log(k));
48
48
  return [Number.parseFloat((bytes / Math.pow(k, index)).toFixed(dm)), sizes[index]];
49
49
  }
50
+ export function joinPaths(...paths) {
51
+ return paths
52
+ .filter((path) => path !== '')
53
+ .join('/');
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securancy/file-explorer",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "svelte": "./dist/index.js",
@@ -48,11 +48,12 @@
48
48
  "zod": "^3.23.8",
49
49
  "@securancy/eslint-config": "0.2.1",
50
50
  "@securancy/stylelint-config": "0.1.2",
51
- "@securancy/svelte-components": "4.2.4",
51
+ "@securancy/svelte-components": "4.3.0",
52
52
  "@securancy/svelte-utilities": "2.0.1"
53
53
  },
54
54
  "peerDependencies": {
55
- "svelte": "^4.2.18"
55
+ "svelte": "^4.2.18",
56
+ "@sveltejs/kit": "^2.5.18"
56
57
  },
57
58
  "scripts": {
58
59
  "dev": "vite dev",