@brftech/filex-core 0.1.56 → 0.1.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +108 -108
- package/dist/{ArchiveViewer-CJdaw5x3.js → ArchiveViewer-CbUUEREI.js} +20 -20
- package/dist/ArchiveViewer-CbUUEREI.js.map +1 -0
- package/dist/{CsvViewer-QYx_tePQ.js → CsvViewer-72rDsQJX.js} +3 -3
- package/dist/CsvViewer-72rDsQJX.js.map +1 -0
- package/dist/{DrawioViewer-GWt0TyB7.js → DrawioViewer-DSyMwGvg.js} +7 -7
- package/dist/DrawioViewer-DSyMwGvg.js.map +1 -0
- package/dist/{EpubViewer-Je0ywklQ.js → EpubViewer-DvDa17d7.js} +3 -3
- package/dist/EpubViewer-DvDa17d7.js.map +1 -0
- package/dist/{IpynbViewer-TsB4gqP-.js → IpynbViewer-DibIkdsz.js} +3 -3
- package/dist/IpynbViewer-DibIkdsz.js.map +1 -0
- package/dist/{MermaidViewer-CXfeDS-7.js → MermaidViewer-BoFLtGvl.js} +3 -3
- package/dist/MermaidViewer-BoFLtGvl.js.map +1 -0
- package/dist/{PsdViewer-VtPpxKVe.js → PsdViewer-K2iZkwax.js} +3 -3
- package/dist/PsdViewer-K2iZkwax.js.map +1 -0
- package/dist/{TiffViewer-OINn9bbb.js → TiffViewer-JzDzbqpO.js} +3 -3
- package/dist/TiffViewer-JzDzbqpO.js.map +1 -0
- package/dist/{Viewer3D-xvvyiWHd.js → Viewer3D-WEMYCP8l.js} +7 -7
- package/dist/Viewer3D-WEMYCP8l.js.map +1 -0
- package/dist/filex-core.js +1 -1
- package/dist/filex-core.umd.cjs +4 -4
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/{index-CmSYzt6L.js → index-B5YT-m5Q.js} +209 -209
- package/dist/index-B5YT-m5Q.js.map +1 -0
- package/dist/style.css +1 -1
- package/dist/useViewerFetch-czqbd2Lj.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Breadcrumb.vue +282 -282
- package/src/components/ContextMenu.vue +152 -152
- package/src/components/GridView.vue +166 -166
- package/src/components/ListView.vue +212 -212
- package/src/components/PendingOpsTray.vue +91 -91
- package/src/components/RecentlyOpened.vue +155 -155
- package/src/components/StarButton.vue +101 -101
- package/src/components/TagPicker.vue +184 -184
- package/src/components/Toolbar.vue +220 -220
- package/src/components/UploadProgress.vue +78 -78
- package/src/composables/useFileApi.ts +692 -692
- package/src/composables/useKeyboardShortcuts.ts +144 -144
- package/src/composables/useLocale.ts +60 -60
- package/src/composables/useMonacoLoader.ts +98 -98
- package/src/composables/usePendingOps.ts +200 -200
- package/src/composables/useSelection.ts +81 -81
- package/src/composables/useUploadChunked.ts +250 -250
- package/src/composables/useViewerFetch.ts +58 -58
- package/src/index.ts +80 -80
- package/src/locales/en.ts +122 -122
- package/src/locales/index.ts +12 -12
- package/src/locales/tr.ts +122 -122
- package/src/modals/DeleteConfirmModal.vue +32 -32
- package/src/modals/Modal.vue +104 -104
- package/src/modals/NewFolderModal.vue +64 -64
- package/src/modals/PermissionsModal.vue +815 -815
- package/src/modals/PreviewModal.vue +1070 -1070
- package/src/modals/RenameModal.vue +58 -58
- package/src/modals/ShareModal.vue +135 -135
- package/src/styles/base.css +1224 -1224
- package/src/styles/variables.css +90 -90
- package/src/types/FileNode.ts +136 -136
- package/src/types/index.ts +27 -27
- package/src/types/peers.d.ts +45 -45
- package/src/viewers/ArchiveViewer.vue +186 -186
- package/src/viewers/CsvViewer.vue +319 -319
- package/src/viewers/DrawioViewer.vue +243 -243
- package/src/viewers/EpubViewer.vue +307 -307
- package/src/viewers/IpynbViewer.vue +365 -365
- package/src/viewers/MermaidViewer.vue +259 -259
- package/src/viewers/PdfViewer.vue +562 -562
- package/src/viewers/PsdViewer.vue +264 -264
- package/src/viewers/TiffViewer.vue +273 -273
- package/src/viewers/Viewer3D.vue +120 -120
- package/dist/ArchiveViewer-CJdaw5x3.js.map +0 -1
- package/dist/CsvViewer-QYx_tePQ.js.map +0 -1
- package/dist/DrawioViewer-GWt0TyB7.js.map +0 -1
- package/dist/EpubViewer-Je0ywklQ.js.map +0 -1
- package/dist/IpynbViewer-TsB4gqP-.js.map +0 -1
- package/dist/MermaidViewer-CXfeDS-7.js.map +0 -1
- package/dist/PsdViewer-VtPpxKVe.js.map +0 -1
- package/dist/TiffViewer-OINn9bbb.js.map +0 -1
- package/dist/Viewer3D-xvvyiWHd.js.map +0 -1
- package/dist/index-CmSYzt6L.js.map +0 -1
|
@@ -1,282 +1,282 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* Breadcrumb — adapter-aware path crumbs.
|
|
4
|
-
*
|
|
5
|
-
* Dirname arrives as `<adapter>://<rel>` (e.g. `s3-test://aa/bb`). The
|
|
6
|
-
* first crumb is ALWAYS the storage root — its label is the adapter
|
|
7
|
-
* name itself (`s3-test`) and clicking it lands you at `<adapter>://`,
|
|
8
|
-
* NOT at the first sub-folder you happened to be inside. The remaining
|
|
9
|
-
* crumbs walk down the path one segment at a time.
|
|
10
|
-
*
|
|
11
|
-
* Examples:
|
|
12
|
-
* `s3-test://` → [s3-test]
|
|
13
|
-
* `s3-test://aa` → [s3-test] › [aa]
|
|
14
|
-
* `s3-test://aa/bb` → [s3-test] › [aa] › [bb]
|
|
15
|
-
*
|
|
16
|
-
* `rootLabel` overrides the adapter name when the embedder wants a
|
|
17
|
-
* branded label ('Files', 'My Drive', etc). Defaults to the adapter.
|
|
18
|
-
*
|
|
19
|
-
* The ✏ button swaps the crumbs for a free-form input; Enter
|
|
20
|
-
* navigates, Escape cancels.
|
|
21
|
-
*/
|
|
22
|
-
import { computed, nextTick, ref } from 'vue';
|
|
23
|
-
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
24
|
-
import { useLocale } from '../composables/useLocale';
|
|
25
|
-
|
|
26
|
-
const props = defineProps<{
|
|
27
|
-
dirname: string;
|
|
28
|
-
adapter: string;
|
|
29
|
-
rootLabel: string;
|
|
30
|
-
locale: LocaleCode;
|
|
31
|
-
/**
|
|
32
|
-
* Multi-storage mode — when true the breadcrumb prepends a global
|
|
33
|
-
* "/" crumb so the user can pop all the way back to the storage
|
|
34
|
-
* picker. Storage name then occupies crumb #1 instead of #0.
|
|
35
|
-
*/
|
|
36
|
-
multiStorageRoot?: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* Root confinement (qualified `<adapter>://<rel>`). When set the breadcrumb
|
|
39
|
-
* treats this folder as `/`: crumbs above it are hidden, the root crumb is
|
|
40
|
-
* the confined folder's own name, and the ✏ path editor shows/accepts paths
|
|
41
|
-
* relative to it (`/`, `/sub`). Pairs with FileExplorer's rootPath clamp.
|
|
42
|
-
*/
|
|
43
|
-
rootPath?: string;
|
|
44
|
-
}>();
|
|
45
|
-
|
|
46
|
-
// Confined root parsed into its adapter-relative form ("projeler/acme") and
|
|
47
|
-
// the trailing folder name used as the "/" crumb label.
|
|
48
|
-
const confinedRel = computed(() => {
|
|
49
|
-
if (!props.rootPath) return '';
|
|
50
|
-
const r = props.rootPath;
|
|
51
|
-
const i = r.indexOf('://');
|
|
52
|
-
return (i >= 0 ? r.slice(i + 3) : r).replace(/^\/+|\/+$/g, '');
|
|
53
|
-
});
|
|
54
|
-
const floorLabel = computed(() => {
|
|
55
|
-
const rel = confinedRel.value;
|
|
56
|
-
if (!rel) return props.rootLabel || props.adapter;
|
|
57
|
-
const parts = rel.split('/');
|
|
58
|
-
return parts[parts.length - 1] || (props.rootLabel || props.adapter);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const { t } = useLocale(() => props.locale);
|
|
62
|
-
|
|
63
|
-
const emit = defineEmits<{
|
|
64
|
-
(e: 'navigate', adapterPath: string): void;
|
|
65
|
-
(e: 'copy-path', adapterPath: string): void;
|
|
66
|
-
(e: 'crumb-context', payload: { x: number; y: number; adapterPath: string; label: string }): void;
|
|
67
|
-
(e: 'crumb-drop', adapterPath: string, ev: DragEvent): void;
|
|
68
|
-
}>();
|
|
69
|
-
|
|
70
|
-
interface Crumb {
|
|
71
|
-
label: string;
|
|
72
|
-
adapterPath: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const crumbs = computed<Crumb[]>(() => {
|
|
76
|
-
const adapterPrefix = `${props.adapter}://`;
|
|
77
|
-
const raw = props.dirname.startsWith(adapterPrefix)
|
|
78
|
-
? props.dirname.slice(adapterPrefix.length)
|
|
79
|
-
: props.dirname;
|
|
80
|
-
const parts = raw.split('/').filter(Boolean);
|
|
81
|
-
|
|
82
|
-
const out: Crumb[] = [];
|
|
83
|
-
|
|
84
|
-
// Confined: the root folder is the top crumb (its own name), and only the
|
|
85
|
-
// path BELOW it is walked — segments above the confinement are never shown.
|
|
86
|
-
if (props.rootPath) {
|
|
87
|
-
const floor = confinedRel.value;
|
|
88
|
-
const cur = raw.replace(/^\/+|\/+$/g, '');
|
|
89
|
-
const below = floor && (cur === floor || cur.startsWith(floor + '/'))
|
|
90
|
-
? cur.slice(floor.length).replace(/^\/+/, '')
|
|
91
|
-
: '';
|
|
92
|
-
out.push({ label: floorLabel.value, adapterPath: props.rootPath });
|
|
93
|
-
let acc = floor;
|
|
94
|
-
for (const part of below.split('/').filter(Boolean)) {
|
|
95
|
-
acc = acc ? `${acc}/${part}` : part;
|
|
96
|
-
out.push({ label: part === '.trash' ? t('node.trash') : part, adapterPath: `${adapterPrefix}${acc}` });
|
|
97
|
-
}
|
|
98
|
-
return out;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (props.multiStorageRoot) {
|
|
102
|
-
// Global "/" crumb — clicking it pops back to the storage picker
|
|
103
|
-
// (FileExplorer treats the empty string as the virtual root).
|
|
104
|
-
out.push({ label: '/', adapterPath: '' });
|
|
105
|
-
if (props.adapter) {
|
|
106
|
-
// Storage segment — wire form `<adapter>://` lands at storage
|
|
107
|
-
// root regardless of where we are now.
|
|
108
|
-
out.push({ label: props.rootLabel || props.adapter, adapterPath: adapterPrefix });
|
|
109
|
-
}
|
|
110
|
-
} else {
|
|
111
|
-
// Single-storage mode: the storage name IS the root, no "/"
|
|
112
|
-
// above it.
|
|
113
|
-
out.push({
|
|
114
|
-
label: props.rootLabel || props.adapter,
|
|
115
|
-
adapterPath: adapterPrefix,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let acc = '';
|
|
120
|
-
for (const part of parts) {
|
|
121
|
-
acc = acc ? `${acc}/${part}` : part;
|
|
122
|
-
const label = part === '.trash' ? t('node.trash') : part;
|
|
123
|
-
out.push({ label, adapterPath: `${adapterPrefix}${acc}` });
|
|
124
|
-
}
|
|
125
|
-
return out;
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
function onClick(crumb: Crumb) {
|
|
129
|
-
emit('navigate', crumb.adapterPath);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function onContext(ev: MouseEvent, crumb: Crumb) {
|
|
133
|
-
ev.preventDefault();
|
|
134
|
-
ev.stopPropagation();
|
|
135
|
-
emit('crumb-context', {
|
|
136
|
-
x: ev.clientX,
|
|
137
|
-
y: ev.clientY,
|
|
138
|
-
adapterPath: crumb.adapterPath,
|
|
139
|
-
label: crumb.label,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function onCrumbDragOver(ev: DragEvent) {
|
|
144
|
-
if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
|
|
145
|
-
ev.preventDefault();
|
|
146
|
-
ev.stopPropagation();
|
|
147
|
-
if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function onCrumbDrop(ev: DragEvent, crumb: Crumb) {
|
|
151
|
-
if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
|
|
152
|
-
ev.preventDefault();
|
|
153
|
-
ev.stopPropagation();
|
|
154
|
-
emit('crumb-drop', crumb.adapterPath, ev);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const editing = ref(false);
|
|
158
|
-
const pathDraft = ref('');
|
|
159
|
-
const pathInput = ref<HTMLInputElement | null>(null);
|
|
160
|
-
|
|
161
|
-
function startEdit() {
|
|
162
|
-
const adapterPrefix = `${props.adapter}://`;
|
|
163
|
-
const rel = props.dirname.startsWith(adapterPrefix)
|
|
164
|
-
? props.dirname.slice(adapterPrefix.length)
|
|
165
|
-
: props.dirname;
|
|
166
|
-
// Confined: edit relative to the root folder — `/` is the root, `/sub` below.
|
|
167
|
-
if (props.rootPath) {
|
|
168
|
-
const floor = confinedRel.value;
|
|
169
|
-
const cur = rel.replace(/^\/+|\/+$/g, '');
|
|
170
|
-
const below = floor && (cur === floor || cur.startsWith(floor + '/'))
|
|
171
|
-
? cur.slice(floor.length).replace(/^\/+/, '')
|
|
172
|
-
: '';
|
|
173
|
-
pathDraft.value = below ? `/${below}` : '/';
|
|
174
|
-
editing.value = true;
|
|
175
|
-
void nextTick(() => {
|
|
176
|
-
pathInput.value?.focus();
|
|
177
|
-
pathInput.value?.select();
|
|
178
|
-
});
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
// Multi-storage: show `/<storage>/<rel>` so the user can edit the
|
|
182
|
-
// whole tree (`/main/foo`, `/s3-test/example`, just `/` for root).
|
|
183
|
-
if (props.multiStorageRoot) {
|
|
184
|
-
if (!props.adapter) {
|
|
185
|
-
pathDraft.value = '/';
|
|
186
|
-
} else {
|
|
187
|
-
const trimmed = rel.replace(/^\/+|\/+$/g, '');
|
|
188
|
-
pathDraft.value = trimmed
|
|
189
|
-
? `/${props.adapter}/${trimmed}`
|
|
190
|
-
: `/${props.adapter}`;
|
|
191
|
-
}
|
|
192
|
-
} else {
|
|
193
|
-
pathDraft.value = rel;
|
|
194
|
-
}
|
|
195
|
-
editing.value = true;
|
|
196
|
-
void nextTick(() => {
|
|
197
|
-
pathInput.value?.focus();
|
|
198
|
-
pathInput.value?.select();
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function submitPath() {
|
|
203
|
-
const raw = pathDraft.value.trim();
|
|
204
|
-
editing.value = false;
|
|
205
|
-
|
|
206
|
-
// Confined: the typed path is relative to the root folder; prepend the floor.
|
|
207
|
-
if (props.rootPath) {
|
|
208
|
-
const sub = raw.replace(/^\/+|\/+$/g, '');
|
|
209
|
-
const floor = confinedRel.value;
|
|
210
|
-
const rel = sub ? (floor ? `${floor}/${sub}` : sub) : floor;
|
|
211
|
-
emit('navigate', rel ? `${props.adapter}://${rel}` : `${props.adapter}://`);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (props.multiStorageRoot) {
|
|
216
|
-
// Strip leading/trailing slashes. First segment = storage.
|
|
217
|
-
const clean = raw.replace(/^\/+|\/+$/g, '');
|
|
218
|
-
if (!clean) {
|
|
219
|
-
// navigate to global root by emitting empty wire path; the
|
|
220
|
-
// FileExplorer treats it as virtual storage list.
|
|
221
|
-
emit('navigate', '');
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
const slash = clean.indexOf('/');
|
|
225
|
-
const adapter = slash === -1 ? clean : clean.slice(0, slash);
|
|
226
|
-
const rel = slash === -1 ? '' : clean.slice(slash + 1);
|
|
227
|
-
emit('navigate', rel ? `${adapter}://${rel}` : `${adapter}://`);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const v = raw.replace(/^\/+|\/+$/g, '');
|
|
232
|
-
if (!v) return;
|
|
233
|
-
emit('navigate', `${props.adapter}://${v}`);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function cancelEdit() {
|
|
237
|
-
editing.value = false;
|
|
238
|
-
}
|
|
239
|
-
</script>
|
|
240
|
-
|
|
241
|
-
<template>
|
|
242
|
-
<nav class="fe-breadcrumb" aria-label="Breadcrumb">
|
|
243
|
-
<template v-if="!editing">
|
|
244
|
-
<button
|
|
245
|
-
v-for="(c, i) in crumbs"
|
|
246
|
-
:key="c.adapterPath"
|
|
247
|
-
class="fe-breadcrumb__crumb"
|
|
248
|
-
:class="{ 'is-last': i === crumbs.length - 1 }"
|
|
249
|
-
type="button"
|
|
250
|
-
:aria-current="i === crumbs.length - 1 ? 'page' : undefined"
|
|
251
|
-
@click="onClick(c)"
|
|
252
|
-
@contextmenu="onContext($event, c)"
|
|
253
|
-
@dragover="onCrumbDragOver"
|
|
254
|
-
@drop="onCrumbDrop($event, c)"
|
|
255
|
-
>
|
|
256
|
-
<span>{{ c.label }}</span>
|
|
257
|
-
<span v-if="i < crumbs.length - 1" class="fe-breadcrumb__sep" aria-hidden="true">›</span>
|
|
258
|
-
</button>
|
|
259
|
-
<button
|
|
260
|
-
type="button"
|
|
261
|
-
class="fe-breadcrumb__edit"
|
|
262
|
-
:title="t('breadcrumb.go_to_path')"
|
|
263
|
-
:aria-label="t('breadcrumb.go_to_path')"
|
|
264
|
-
@click="startEdit"
|
|
265
|
-
>✏</button>
|
|
266
|
-
</template>
|
|
267
|
-
<template v-else>
|
|
268
|
-
<input
|
|
269
|
-
ref="pathInput"
|
|
270
|
-
v-model="pathDraft"
|
|
271
|
-
type="text"
|
|
272
|
-
class="fe-breadcrumb__input"
|
|
273
|
-
:placeholder="t('breadcrumb.path_placeholder')"
|
|
274
|
-
spellcheck="false"
|
|
275
|
-
autocomplete="off"
|
|
276
|
-
@keydown.enter.prevent="submitPath"
|
|
277
|
-
@keydown.escape.prevent="cancelEdit"
|
|
278
|
-
@blur="cancelEdit"
|
|
279
|
-
/>
|
|
280
|
-
</template>
|
|
281
|
-
</nav>
|
|
282
|
-
</template>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Breadcrumb — adapter-aware path crumbs.
|
|
4
|
+
*
|
|
5
|
+
* Dirname arrives as `<adapter>://<rel>` (e.g. `s3-test://aa/bb`). The
|
|
6
|
+
* first crumb is ALWAYS the storage root — its label is the adapter
|
|
7
|
+
* name itself (`s3-test`) and clicking it lands you at `<adapter>://`,
|
|
8
|
+
* NOT at the first sub-folder you happened to be inside. The remaining
|
|
9
|
+
* crumbs walk down the path one segment at a time.
|
|
10
|
+
*
|
|
11
|
+
* Examples:
|
|
12
|
+
* `s3-test://` → [s3-test]
|
|
13
|
+
* `s3-test://aa` → [s3-test] › [aa]
|
|
14
|
+
* `s3-test://aa/bb` → [s3-test] › [aa] › [bb]
|
|
15
|
+
*
|
|
16
|
+
* `rootLabel` overrides the adapter name when the embedder wants a
|
|
17
|
+
* branded label ('Files', 'My Drive', etc). Defaults to the adapter.
|
|
18
|
+
*
|
|
19
|
+
* The ✏ button swaps the crumbs for a free-form input; Enter
|
|
20
|
+
* navigates, Escape cancels.
|
|
21
|
+
*/
|
|
22
|
+
import { computed, nextTick, ref } from 'vue';
|
|
23
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
24
|
+
import { useLocale } from '../composables/useLocale';
|
|
25
|
+
|
|
26
|
+
const props = defineProps<{
|
|
27
|
+
dirname: string;
|
|
28
|
+
adapter: string;
|
|
29
|
+
rootLabel: string;
|
|
30
|
+
locale: LocaleCode;
|
|
31
|
+
/**
|
|
32
|
+
* Multi-storage mode — when true the breadcrumb prepends a global
|
|
33
|
+
* "/" crumb so the user can pop all the way back to the storage
|
|
34
|
+
* picker. Storage name then occupies crumb #1 instead of #0.
|
|
35
|
+
*/
|
|
36
|
+
multiStorageRoot?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Root confinement (qualified `<adapter>://<rel>`). When set the breadcrumb
|
|
39
|
+
* treats this folder as `/`: crumbs above it are hidden, the root crumb is
|
|
40
|
+
* the confined folder's own name, and the ✏ path editor shows/accepts paths
|
|
41
|
+
* relative to it (`/`, `/sub`). Pairs with FileExplorer's rootPath clamp.
|
|
42
|
+
*/
|
|
43
|
+
rootPath?: string;
|
|
44
|
+
}>();
|
|
45
|
+
|
|
46
|
+
// Confined root parsed into its adapter-relative form ("projeler/acme") and
|
|
47
|
+
// the trailing folder name used as the "/" crumb label.
|
|
48
|
+
const confinedRel = computed(() => {
|
|
49
|
+
if (!props.rootPath) return '';
|
|
50
|
+
const r = props.rootPath;
|
|
51
|
+
const i = r.indexOf('://');
|
|
52
|
+
return (i >= 0 ? r.slice(i + 3) : r).replace(/^\/+|\/+$/g, '');
|
|
53
|
+
});
|
|
54
|
+
const floorLabel = computed(() => {
|
|
55
|
+
const rel = confinedRel.value;
|
|
56
|
+
if (!rel) return props.rootLabel || props.adapter;
|
|
57
|
+
const parts = rel.split('/');
|
|
58
|
+
return parts[parts.length - 1] || (props.rootLabel || props.adapter);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const { t } = useLocale(() => props.locale);
|
|
62
|
+
|
|
63
|
+
const emit = defineEmits<{
|
|
64
|
+
(e: 'navigate', adapterPath: string): void;
|
|
65
|
+
(e: 'copy-path', adapterPath: string): void;
|
|
66
|
+
(e: 'crumb-context', payload: { x: number; y: number; adapterPath: string; label: string }): void;
|
|
67
|
+
(e: 'crumb-drop', adapterPath: string, ev: DragEvent): void;
|
|
68
|
+
}>();
|
|
69
|
+
|
|
70
|
+
interface Crumb {
|
|
71
|
+
label: string;
|
|
72
|
+
adapterPath: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const crumbs = computed<Crumb[]>(() => {
|
|
76
|
+
const adapterPrefix = `${props.adapter}://`;
|
|
77
|
+
const raw = props.dirname.startsWith(adapterPrefix)
|
|
78
|
+
? props.dirname.slice(adapterPrefix.length)
|
|
79
|
+
: props.dirname;
|
|
80
|
+
const parts = raw.split('/').filter(Boolean);
|
|
81
|
+
|
|
82
|
+
const out: Crumb[] = [];
|
|
83
|
+
|
|
84
|
+
// Confined: the root folder is the top crumb (its own name), and only the
|
|
85
|
+
// path BELOW it is walked — segments above the confinement are never shown.
|
|
86
|
+
if (props.rootPath) {
|
|
87
|
+
const floor = confinedRel.value;
|
|
88
|
+
const cur = raw.replace(/^\/+|\/+$/g, '');
|
|
89
|
+
const below = floor && (cur === floor || cur.startsWith(floor + '/'))
|
|
90
|
+
? cur.slice(floor.length).replace(/^\/+/, '')
|
|
91
|
+
: '';
|
|
92
|
+
out.push({ label: floorLabel.value, adapterPath: props.rootPath });
|
|
93
|
+
let acc = floor;
|
|
94
|
+
for (const part of below.split('/').filter(Boolean)) {
|
|
95
|
+
acc = acc ? `${acc}/${part}` : part;
|
|
96
|
+
out.push({ label: part === '.trash' ? t('node.trash') : part, adapterPath: `${adapterPrefix}${acc}` });
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (props.multiStorageRoot) {
|
|
102
|
+
// Global "/" crumb — clicking it pops back to the storage picker
|
|
103
|
+
// (FileExplorer treats the empty string as the virtual root).
|
|
104
|
+
out.push({ label: '/', adapterPath: '' });
|
|
105
|
+
if (props.adapter) {
|
|
106
|
+
// Storage segment — wire form `<adapter>://` lands at storage
|
|
107
|
+
// root regardless of where we are now.
|
|
108
|
+
out.push({ label: props.rootLabel || props.adapter, adapterPath: adapterPrefix });
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
// Single-storage mode: the storage name IS the root, no "/"
|
|
112
|
+
// above it.
|
|
113
|
+
out.push({
|
|
114
|
+
label: props.rootLabel || props.adapter,
|
|
115
|
+
adapterPath: adapterPrefix,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let acc = '';
|
|
120
|
+
for (const part of parts) {
|
|
121
|
+
acc = acc ? `${acc}/${part}` : part;
|
|
122
|
+
const label = part === '.trash' ? t('node.trash') : part;
|
|
123
|
+
out.push({ label, adapterPath: `${adapterPrefix}${acc}` });
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
function onClick(crumb: Crumb) {
|
|
129
|
+
emit('navigate', crumb.adapterPath);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function onContext(ev: MouseEvent, crumb: Crumb) {
|
|
133
|
+
ev.preventDefault();
|
|
134
|
+
ev.stopPropagation();
|
|
135
|
+
emit('crumb-context', {
|
|
136
|
+
x: ev.clientX,
|
|
137
|
+
y: ev.clientY,
|
|
138
|
+
adapterPath: crumb.adapterPath,
|
|
139
|
+
label: crumb.label,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function onCrumbDragOver(ev: DragEvent) {
|
|
144
|
+
if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
|
|
145
|
+
ev.preventDefault();
|
|
146
|
+
ev.stopPropagation();
|
|
147
|
+
if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function onCrumbDrop(ev: DragEvent, crumb: Crumb) {
|
|
151
|
+
if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
|
|
152
|
+
ev.preventDefault();
|
|
153
|
+
ev.stopPropagation();
|
|
154
|
+
emit('crumb-drop', crumb.adapterPath, ev);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const editing = ref(false);
|
|
158
|
+
const pathDraft = ref('');
|
|
159
|
+
const pathInput = ref<HTMLInputElement | null>(null);
|
|
160
|
+
|
|
161
|
+
function startEdit() {
|
|
162
|
+
const adapterPrefix = `${props.adapter}://`;
|
|
163
|
+
const rel = props.dirname.startsWith(adapterPrefix)
|
|
164
|
+
? props.dirname.slice(adapterPrefix.length)
|
|
165
|
+
: props.dirname;
|
|
166
|
+
// Confined: edit relative to the root folder — `/` is the root, `/sub` below.
|
|
167
|
+
if (props.rootPath) {
|
|
168
|
+
const floor = confinedRel.value;
|
|
169
|
+
const cur = rel.replace(/^\/+|\/+$/g, '');
|
|
170
|
+
const below = floor && (cur === floor || cur.startsWith(floor + '/'))
|
|
171
|
+
? cur.slice(floor.length).replace(/^\/+/, '')
|
|
172
|
+
: '';
|
|
173
|
+
pathDraft.value = below ? `/${below}` : '/';
|
|
174
|
+
editing.value = true;
|
|
175
|
+
void nextTick(() => {
|
|
176
|
+
pathInput.value?.focus();
|
|
177
|
+
pathInput.value?.select();
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// Multi-storage: show `/<storage>/<rel>` so the user can edit the
|
|
182
|
+
// whole tree (`/main/foo`, `/s3-test/example`, just `/` for root).
|
|
183
|
+
if (props.multiStorageRoot) {
|
|
184
|
+
if (!props.adapter) {
|
|
185
|
+
pathDraft.value = '/';
|
|
186
|
+
} else {
|
|
187
|
+
const trimmed = rel.replace(/^\/+|\/+$/g, '');
|
|
188
|
+
pathDraft.value = trimmed
|
|
189
|
+
? `/${props.adapter}/${trimmed}`
|
|
190
|
+
: `/${props.adapter}`;
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
pathDraft.value = rel;
|
|
194
|
+
}
|
|
195
|
+
editing.value = true;
|
|
196
|
+
void nextTick(() => {
|
|
197
|
+
pathInput.value?.focus();
|
|
198
|
+
pathInput.value?.select();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function submitPath() {
|
|
203
|
+
const raw = pathDraft.value.trim();
|
|
204
|
+
editing.value = false;
|
|
205
|
+
|
|
206
|
+
// Confined: the typed path is relative to the root folder; prepend the floor.
|
|
207
|
+
if (props.rootPath) {
|
|
208
|
+
const sub = raw.replace(/^\/+|\/+$/g, '');
|
|
209
|
+
const floor = confinedRel.value;
|
|
210
|
+
const rel = sub ? (floor ? `${floor}/${sub}` : sub) : floor;
|
|
211
|
+
emit('navigate', rel ? `${props.adapter}://${rel}` : `${props.adapter}://`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (props.multiStorageRoot) {
|
|
216
|
+
// Strip leading/trailing slashes. First segment = storage.
|
|
217
|
+
const clean = raw.replace(/^\/+|\/+$/g, '');
|
|
218
|
+
if (!clean) {
|
|
219
|
+
// navigate to global root by emitting empty wire path; the
|
|
220
|
+
// FileExplorer treats it as virtual storage list.
|
|
221
|
+
emit('navigate', '');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const slash = clean.indexOf('/');
|
|
225
|
+
const adapter = slash === -1 ? clean : clean.slice(0, slash);
|
|
226
|
+
const rel = slash === -1 ? '' : clean.slice(slash + 1);
|
|
227
|
+
emit('navigate', rel ? `${adapter}://${rel}` : `${adapter}://`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const v = raw.replace(/^\/+|\/+$/g, '');
|
|
232
|
+
if (!v) return;
|
|
233
|
+
emit('navigate', `${props.adapter}://${v}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function cancelEdit() {
|
|
237
|
+
editing.value = false;
|
|
238
|
+
}
|
|
239
|
+
</script>
|
|
240
|
+
|
|
241
|
+
<template>
|
|
242
|
+
<nav class="fe-breadcrumb" aria-label="Breadcrumb">
|
|
243
|
+
<template v-if="!editing">
|
|
244
|
+
<button
|
|
245
|
+
v-for="(c, i) in crumbs"
|
|
246
|
+
:key="c.adapterPath"
|
|
247
|
+
class="fe-breadcrumb__crumb"
|
|
248
|
+
:class="{ 'is-last': i === crumbs.length - 1 }"
|
|
249
|
+
type="button"
|
|
250
|
+
:aria-current="i === crumbs.length - 1 ? 'page' : undefined"
|
|
251
|
+
@click="onClick(c)"
|
|
252
|
+
@contextmenu="onContext($event, c)"
|
|
253
|
+
@dragover="onCrumbDragOver"
|
|
254
|
+
@drop="onCrumbDrop($event, c)"
|
|
255
|
+
>
|
|
256
|
+
<span>{{ c.label }}</span>
|
|
257
|
+
<span v-if="i < crumbs.length - 1" class="fe-breadcrumb__sep" aria-hidden="true">›</span>
|
|
258
|
+
</button>
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
class="fe-breadcrumb__edit"
|
|
262
|
+
:title="t('breadcrumb.go_to_path')"
|
|
263
|
+
:aria-label="t('breadcrumb.go_to_path')"
|
|
264
|
+
@click="startEdit"
|
|
265
|
+
>✏</button>
|
|
266
|
+
</template>
|
|
267
|
+
<template v-else>
|
|
268
|
+
<input
|
|
269
|
+
ref="pathInput"
|
|
270
|
+
v-model="pathDraft"
|
|
271
|
+
type="text"
|
|
272
|
+
class="fe-breadcrumb__input"
|
|
273
|
+
:placeholder="t('breadcrumb.path_placeholder')"
|
|
274
|
+
spellcheck="false"
|
|
275
|
+
autocomplete="off"
|
|
276
|
+
@keydown.enter.prevent="submitPath"
|
|
277
|
+
@keydown.escape.prevent="cancelEdit"
|
|
278
|
+
@blur="cancelEdit"
|
|
279
|
+
/>
|
|
280
|
+
</template>
|
|
281
|
+
</nav>
|
|
282
|
+
</template>
|