@brftech/filex-core 0.1.56
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 -0
- package/README.md +108 -0
- package/dist/ArchiveViewer-CJdaw5x3.js +99 -0
- package/dist/ArchiveViewer-CJdaw5x3.js.map +1 -0
- package/dist/CsvViewer-QYx_tePQ.js +132 -0
- package/dist/CsvViewer-QYx_tePQ.js.map +1 -0
- package/dist/DrawioViewer-GWt0TyB7.js +141 -0
- package/dist/DrawioViewer-GWt0TyB7.js.map +1 -0
- package/dist/EpubViewer-Je0ywklQ.js +146 -0
- package/dist/EpubViewer-Je0ywklQ.js.map +1 -0
- package/dist/IpynbViewer-TsB4gqP-.js +175 -0
- package/dist/IpynbViewer-TsB4gqP-.js.map +1 -0
- package/dist/MermaidViewer-CXfeDS-7.js +128 -0
- package/dist/MermaidViewer-CXfeDS-7.js.map +1 -0
- package/dist/PsdViewer-VtPpxKVe.js +113 -0
- package/dist/PsdViewer-VtPpxKVe.js.map +1 -0
- package/dist/TiffViewer-OINn9bbb.js +133 -0
- package/dist/TiffViewer-OINn9bbb.js.map +1 -0
- package/dist/UTIF-Cjsrlm0l.js +3104 -0
- package/dist/UTIF-Cjsrlm0l.js.map +1 -0
- package/dist/Viewer3D-xvvyiWHd.js +61 -0
- package/dist/Viewer3D-xvvyiWHd.js.map +1 -0
- package/dist/_commonjsHelpers-DaMA6jEr.js +9 -0
- package/dist/_commonjsHelpers-DaMA6jEr.js.map +1 -0
- package/dist/filex-core.js +26 -0
- package/dist/filex-core.js.map +1 -0
- package/dist/filex-core.umd.cjs +285 -0
- package/dist/filex-core.umd.cjs.map +1 -0
- package/dist/index-CmSYzt6L.js +5404 -0
- package/dist/index-CmSYzt6L.js.map +1 -0
- package/dist/index-UFULWo35.js +10736 -0
- package/dist/index-UFULWo35.js.map +1 -0
- package/dist/index.d.ts +1089 -0
- package/dist/katex-yuB6V-q6.js +11616 -0
- package/dist/katex-yuB6V-q6.js.map +1 -0
- package/dist/papaparse.min-VB1HBwYX.js +441 -0
- package/dist/papaparse.min-VB1HBwYX.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/useViewerFetch-czqbd2Lj.js +25 -0
- package/dist/useViewerFetch-czqbd2Lj.js.map +1 -0
- package/package.json +113 -0
- package/src/FileExplorer.vue +1872 -0
- package/src/components/Breadcrumb.vue +282 -0
- package/src/components/ContextMenu.vue +152 -0
- package/src/components/GridView.vue +166 -0
- package/src/components/ListView.vue +212 -0
- package/src/components/PendingOpsTray.vue +91 -0
- package/src/components/RecentlyOpened.vue +155 -0
- package/src/components/StarButton.vue +101 -0
- package/src/components/TagPicker.vue +184 -0
- package/src/components/Toolbar.vue +220 -0
- package/src/components/UploadProgress.vue +78 -0
- package/src/composables/useFileApi.ts +692 -0
- package/src/composables/useKeyboardShortcuts.ts +144 -0
- package/src/composables/useLocale.ts +60 -0
- package/src/composables/useMonacoLoader.ts +98 -0
- package/src/composables/usePendingOps.ts +200 -0
- package/src/composables/useSelection.ts +81 -0
- package/src/composables/useUploadChunked.ts +250 -0
- package/src/composables/useViewerFetch.ts +58 -0
- package/src/index.ts +80 -0
- package/src/locales/en.ts +122 -0
- package/src/locales/index.ts +12 -0
- package/src/locales/tr.ts +122 -0
- package/src/modals/ConvertModal.vue +254 -0
- package/src/modals/DeleteConfirmModal.vue +32 -0
- package/src/modals/Modal.vue +104 -0
- package/src/modals/NewFolderModal.vue +64 -0
- package/src/modals/PermissionsModal.vue +815 -0
- package/src/modals/PreviewModal.vue +1070 -0
- package/src/modals/RenameModal.vue +58 -0
- package/src/modals/ShareModal.vue +135 -0
- package/src/styles/base.css +1224 -0
- package/src/styles/variables.css +90 -0
- package/src/types/ExplorerConfig.ts +282 -0
- package/src/types/FileNode.ts +136 -0
- package/src/types/index.ts +27 -0
- package/src/types/peers.d.ts +45 -0
- package/src/viewers/ArchiveViewer.vue +186 -0
- package/src/viewers/CsvViewer.vue +319 -0
- package/src/viewers/DrawioViewer.vue +243 -0
- package/src/viewers/EpubViewer.vue +307 -0
- package/src/viewers/IpynbViewer.vue +365 -0
- package/src/viewers/MermaidViewer.vue +259 -0
- package/src/viewers/PdfViewer.vue +562 -0
- package/src/viewers/PsdViewer.vue +264 -0
- package/src/viewers/TiffViewer.vue +273 -0
- package/src/viewers/Viewer3D.vue +120 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ConvertModal — universal file converter via a hidden, embedded iframe to
|
|
4
|
+
* the p2r3/convert fork running at `convertUrl` (loaded with `?embed=1`).
|
|
5
|
+
*
|
|
6
|
+
* The iframe performs the actual conversion in-browser (WASM handlers); we
|
|
7
|
+
* drive it headlessly over postMessage (protocol defined in the fork's
|
|
8
|
+
* `setupEmbedBridge`):
|
|
9
|
+
* 1. iframe → { source:'convert-embed', event:'ready' }
|
|
10
|
+
* 2. we → { target:'convert-embed', cmd:'listFormats', id }
|
|
11
|
+
* iframe→ { source, id, ok, formats:[{index,ext,format,mime,name,from,to}] }
|
|
12
|
+
* 3. user searches + picks a target format
|
|
13
|
+
* 4. we fetch the source bytes, → { cmd:'convert', name, bytes, fromIndex, toIndex }
|
|
14
|
+
* iframe→ { ok, name, ext, bytes:ArrayBuffer }
|
|
15
|
+
* 5. we wrap the bytes in a File and upload it to the current folder.
|
|
16
|
+
*/
|
|
17
|
+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
18
|
+
|
|
19
|
+
const props = defineProps<{
|
|
20
|
+
/** Converter base, e.g. https://fm.example.com/convert */
|
|
21
|
+
convertUrl: string;
|
|
22
|
+
/** Source file name with extension, e.g. "clip.avi" */
|
|
23
|
+
fileName: string;
|
|
24
|
+
/** Lazily fetch the source file bytes (api.fetchArrayBuffer bound to the path). */
|
|
25
|
+
fetchBytes: () => Promise<ArrayBuffer>;
|
|
26
|
+
/** Upload the produced File into the current folder (api.uploadMultipart bound to dir). */
|
|
27
|
+
upload: (file: File) => Promise<void>;
|
|
28
|
+
t?: (key: string, fallback: string) => string;
|
|
29
|
+
}>();
|
|
30
|
+
|
|
31
|
+
const emit = defineEmits<{ (e: 'close'): void; (e: 'done', name: string): void }>();
|
|
32
|
+
|
|
33
|
+
interface Fmt {
|
|
34
|
+
index: number; ext: string; format: string; mime: string;
|
|
35
|
+
name: string; from: boolean; to: boolean; category: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const iframeRef = ref<HTMLIFrameElement | null>(null);
|
|
39
|
+
const status = ref<'loading' | 'ready' | 'converting' | 'done' | 'error'>('loading');
|
|
40
|
+
const error = ref<string | null>(null);
|
|
41
|
+
const formats = ref<Fmt[]>([]);
|
|
42
|
+
const search = ref('');
|
|
43
|
+
const selectedTo = ref<Fmt | null>(null);
|
|
44
|
+
|
|
45
|
+
const srcExt = computed(() => (props.fileName.split('.').pop() || '').toLowerCase());
|
|
46
|
+
|
|
47
|
+
const fromFmt = computed<Fmt | null>(() => {
|
|
48
|
+
const ins = formats.value.filter((f) => f.from);
|
|
49
|
+
return (
|
|
50
|
+
ins.find((f) => (f.ext || '').toLowerCase() === srcExt.value) ||
|
|
51
|
+
ins.find((f) => (f.format || '').toLowerCase() === srcExt.value) ||
|
|
52
|
+
null
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const toList = computed<Fmt[]>(() => {
|
|
57
|
+
const q = search.value.trim().toLowerCase();
|
|
58
|
+
const seen = new Set<string>();
|
|
59
|
+
const uniq: Fmt[] = [];
|
|
60
|
+
for (const f of formats.value) {
|
|
61
|
+
if (!f.to) continue;
|
|
62
|
+
const key = (f.ext || f.format || '').toLowerCase();
|
|
63
|
+
if (!key || seen.has(key)) continue;
|
|
64
|
+
seen.add(key);
|
|
65
|
+
uniq.push(f);
|
|
66
|
+
}
|
|
67
|
+
uniq.sort((a, b) => (a.ext || a.format).localeCompare(b.ext || b.format));
|
|
68
|
+
if (!q) return uniq;
|
|
69
|
+
return uniq.filter(
|
|
70
|
+
(f) =>
|
|
71
|
+
(f.ext || '').toLowerCase().includes(q) ||
|
|
72
|
+
(f.format || '').toLowerCase().includes(q) ||
|
|
73
|
+
(f.name || '').toLowerCase().includes(q) ||
|
|
74
|
+
(f.mime || '').toLowerCase().includes(q),
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const iframeSrc = computed(() => `${props.convertUrl.replace(/\/$/, '')}/?embed=1`);
|
|
79
|
+
|
|
80
|
+
let msgId = 0;
|
|
81
|
+
const pending = new Map<number, { resolve: (v: any) => void; reject: (e: any) => void }>();
|
|
82
|
+
|
|
83
|
+
function send(cmd: string, extra: Record<string, unknown> = {}, transfer: Transferable[] = []): Promise<any> {
|
|
84
|
+
const id = ++msgId;
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
pending.set(id, { resolve, reject });
|
|
87
|
+
iframeRef.value?.contentWindow?.postMessage({ target: 'convert-embed', id, cmd, ...extra }, '*', transfer);
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
if (pending.has(id)) { pending.delete(id); reject(new Error('convert timeout')); }
|
|
90
|
+
}, 180000);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function onMessage(ev: MessageEvent) {
|
|
95
|
+
const d = ev.data;
|
|
96
|
+
if (!d || d.source !== 'convert-embed') return;
|
|
97
|
+
if (d.event === 'ready') { void loadFormats(); return; }
|
|
98
|
+
const p = pending.get(d.id);
|
|
99
|
+
if (!p) return;
|
|
100
|
+
pending.delete(d.id);
|
|
101
|
+
if (d.ok) p.resolve(d); else p.reject(new Error(d.error || 'convert error'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function loadFormats() {
|
|
105
|
+
try {
|
|
106
|
+
const res = await send('listFormats');
|
|
107
|
+
formats.value = res.formats || [];
|
|
108
|
+
status.value = 'ready';
|
|
109
|
+
if (!fromFmt.value) {
|
|
110
|
+
error.value = tt('convert.unsupportedInput', 'Bu dosya tipi için kaynak format bulunamadı.');
|
|
111
|
+
}
|
|
112
|
+
} catch (e) {
|
|
113
|
+
error.value = (e as Error).message;
|
|
114
|
+
status.value = 'error';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function doConvert() {
|
|
119
|
+
if (!selectedTo.value || !fromFmt.value) return;
|
|
120
|
+
status.value = 'converting';
|
|
121
|
+
error.value = null;
|
|
122
|
+
try {
|
|
123
|
+
const buf = await props.fetchBytes();
|
|
124
|
+
const res = await send(
|
|
125
|
+
'convert',
|
|
126
|
+
{ name: props.fileName, bytes: buf, fromIndex: fromFmt.value.index, toIndex: selectedTo.value.index },
|
|
127
|
+
[buf],
|
|
128
|
+
);
|
|
129
|
+
const base = props.fileName.replace(/\.[^.]+$/, '');
|
|
130
|
+
const ext = res.ext || selectedTo.value.ext || selectedTo.value.format;
|
|
131
|
+
const outName = `${base}.${ext}`;
|
|
132
|
+
const file = new File([res.bytes], outName, { type: selectedTo.value.mime || 'application/octet-stream' });
|
|
133
|
+
await props.upload(file);
|
|
134
|
+
status.value = 'done';
|
|
135
|
+
emit('done', outName);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
error.value = (e as Error).message;
|
|
138
|
+
status.value = 'error';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function tt(k: string, fb: string) { return props.t ? props.t(k, fb) : fb; }
|
|
143
|
+
|
|
144
|
+
onMounted(() => window.addEventListener('message', onMessage));
|
|
145
|
+
onBeforeUnmount(() => window.removeEventListener('message', onMessage));
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<template>
|
|
149
|
+
<div class="filex-cv__bg" @click.self="emit('close')">
|
|
150
|
+
<div class="filex-cv">
|
|
151
|
+
<header class="filex-cv__head">
|
|
152
|
+
<h3>{{ tt('convert.title', 'Dönüştür') }} — {{ fileName }}</h3>
|
|
153
|
+
<button class="filex-cv__x" @click="emit('close')">✕</button>
|
|
154
|
+
</header>
|
|
155
|
+
|
|
156
|
+
<div v-if="status === 'loading'" class="filex-cv__msg">
|
|
157
|
+
{{ tt('convert.loading', 'Dönüştürücü yükleniyor…') }}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div v-else-if="status === 'done'" class="filex-cv__msg filex-cv__ok">
|
|
161
|
+
<p>✓ {{ tt('convert.done', 'Dönüştürüldü ve klasöre yüklendi.') }}</p>
|
|
162
|
+
<button class="filex-cv__convert" @click="emit('close')">{{ tt('convert.close', 'Kapat') }}</button>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<template v-else>
|
|
166
|
+
<p class="filex-cv__src">
|
|
167
|
+
{{ tt('convert.from', 'Kaynak') }}: <b>{{ srcExt || '?' }}</b>
|
|
168
|
+
</p>
|
|
169
|
+
<input
|
|
170
|
+
v-model="search"
|
|
171
|
+
class="filex-cv__search"
|
|
172
|
+
:placeholder="tt('convert.searchFmt', 'Hedef format ara (pdf, mp4, png…)')"
|
|
173
|
+
/>
|
|
174
|
+
<div class="filex-cv__list">
|
|
175
|
+
<button
|
|
176
|
+
v-for="f in toList"
|
|
177
|
+
:key="f.index"
|
|
178
|
+
:class="['filex-cv__fmt', { 'is-sel': selectedTo?.index === f.index }]"
|
|
179
|
+
@click="selectedTo = f"
|
|
180
|
+
>
|
|
181
|
+
<b>{{ (f.ext || f.format).toUpperCase() }}</b>
|
|
182
|
+
<small>{{ f.name }}</small>
|
|
183
|
+
</button>
|
|
184
|
+
<div v-if="toList.length === 0" class="filex-cv__empty">
|
|
185
|
+
{{ tt('convert.noFmt', 'Eşleşen format yok.') }}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
<div v-if="error" class="filex-cv__err">{{ error }}</div>
|
|
189
|
+
<footer class="filex-cv__foot">
|
|
190
|
+
<button
|
|
191
|
+
class="filex-cv__convert"
|
|
192
|
+
:disabled="!selectedTo || !fromFmt || status === 'converting'"
|
|
193
|
+
@click="doConvert"
|
|
194
|
+
>
|
|
195
|
+
{{ status === 'converting' ? tt('convert.converting', 'Dönüştürülüyor…') : tt('convert.convert', 'Dönüştür') }}
|
|
196
|
+
</button>
|
|
197
|
+
</footer>
|
|
198
|
+
</template>
|
|
199
|
+
|
|
200
|
+
<!-- hidden headless converter engine -->
|
|
201
|
+
<iframe ref="iframeRef" :src="iframeSrc" class="filex-cv__frame" title="converter" />
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</template>
|
|
205
|
+
|
|
206
|
+
<style scoped>
|
|
207
|
+
.filex-cv__bg {
|
|
208
|
+
position: fixed; inset: 0; z-index: 1000;
|
|
209
|
+
background: rgba(0, 0, 0, 0.55);
|
|
210
|
+
display: flex; align-items: center; justify-content: center;
|
|
211
|
+
}
|
|
212
|
+
.filex-cv {
|
|
213
|
+
width: min(560px, 92vw); max-height: 80vh; overflow: hidden;
|
|
214
|
+
display: flex; flex-direction: column;
|
|
215
|
+
background: var(--fe-bg, #fff); color: var(--fe-text, #1a1f29);
|
|
216
|
+
border: 1px solid var(--fe-border, #e2e6ed); border-radius: 12px;
|
|
217
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
|
|
218
|
+
}
|
|
219
|
+
.filex-cv__head {
|
|
220
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
221
|
+
padding: 12px 16px; border-bottom: 1px solid var(--fe-border, #e2e6ed);
|
|
222
|
+
}
|
|
223
|
+
.filex-cv__head h3 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
224
|
+
.filex-cv__x { border: 0; background: none; cursor: pointer; font-size: 16px; color: var(--fe-text-muted, #5a6475); }
|
|
225
|
+
.filex-cv__msg { padding: 28px; text-align: center; color: var(--fe-text-muted, #5a6475); }
|
|
226
|
+
.filex-cv__ok { color: #059669; }
|
|
227
|
+
.filex-cv__src { padding: 12px 16px 0; font-size: 13px; color: var(--fe-text-muted, #5a6475); }
|
|
228
|
+
.filex-cv__search {
|
|
229
|
+
margin: 8px 16px; padding: 8px 10px;
|
|
230
|
+
border: 1px solid var(--fe-border, #e2e6ed); border-radius: 8px;
|
|
231
|
+
background: var(--fe-bg-elev, #f7f8fa); color: inherit; font-size: 13px;
|
|
232
|
+
}
|
|
233
|
+
.filex-cv__list {
|
|
234
|
+
flex: 1; overflow-y: auto; padding: 4px 12px 12px;
|
|
235
|
+
display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px;
|
|
236
|
+
}
|
|
237
|
+
.filex-cv__fmt {
|
|
238
|
+
display: flex; flex-direction: column; gap: 2px; align-items: flex-start;
|
|
239
|
+
padding: 8px 10px; cursor: pointer; text-align: left;
|
|
240
|
+
border: 1px solid var(--fe-border, #e2e6ed); border-radius: 8px;
|
|
241
|
+
background: var(--fe-bg-elev, #f7f8fa); color: inherit;
|
|
242
|
+
}
|
|
243
|
+
.filex-cv__fmt small { color: var(--fe-text-muted, #5a6475); font-size: 11px; }
|
|
244
|
+
.filex-cv__fmt.is-sel { border-color: #44c878; background: rgba(68, 200, 120, 0.12); }
|
|
245
|
+
.filex-cv__empty { grid-column: 1 / -1; text-align: center; color: var(--fe-text-muted, #5a6475); padding: 16px; }
|
|
246
|
+
.filex-cv__err { padding: 0 16px; color: var(--fe-danger, #dc2626); font-size: 13px; }
|
|
247
|
+
.filex-cv__foot { padding: 12px 16px; border-top: 1px solid var(--fe-border, #e2e6ed); text-align: right; }
|
|
248
|
+
.filex-cv__convert {
|
|
249
|
+
padding: 8px 18px; border: 0; border-radius: 8px; cursor: pointer;
|
|
250
|
+
background: #44c878; color: #03200f; font-weight: 600; font-size: 13px;
|
|
251
|
+
}
|
|
252
|
+
.filex-cv__convert:disabled { opacity: 0.5; cursor: default; }
|
|
253
|
+
.filex-cv__frame { position: absolute; width: 0; height: 0; border: 0; left: -9999px; }
|
|
254
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
3
|
+
import { useLocale } from '../composables/useLocale';
|
|
4
|
+
import Modal from './Modal.vue';
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
open: boolean;
|
|
8
|
+
locale: LocaleCode;
|
|
9
|
+
count: number;
|
|
10
|
+
}>();
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
(e: 'close'): void;
|
|
14
|
+
(e: 'confirm'): void;
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
const { t } = useLocale(() => props.locale);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<Modal :open="open" :title="t('modal.delete.title')" size="sm" @close="emit('close')">
|
|
22
|
+
<p>{{ t('modal.delete.message', { count }) }}</p>
|
|
23
|
+
<template #actions>
|
|
24
|
+
<button type="button" class="fe-btn" @click="emit('close')">
|
|
25
|
+
{{ t('modal.delete.cancel') }}
|
|
26
|
+
</button>
|
|
27
|
+
<button type="button" class="fe-btn fe-btn--danger" @click="emit('confirm')">
|
|
28
|
+
{{ t('modal.delete.confirm') }}
|
|
29
|
+
</button>
|
|
30
|
+
</template>
|
|
31
|
+
</Modal>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Modal — tiny headless wrapper. Backdrop + centered card + ESC + autofocus.
|
|
4
|
+
*/
|
|
5
|
+
import { watch, onBeforeUnmount, ref } from 'vue';
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
open: boolean;
|
|
9
|
+
title?: string;
|
|
10
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
11
|
+
closeOnBackdrop?: boolean;
|
|
12
|
+
/** When true, drop the dialog chrome (backdrop tint, header, footer,
|
|
13
|
+
* centered card with border-radius) and render the slot full-bleed.
|
|
14
|
+
* Used by the standalone /files/edit route where the browser tab IS
|
|
15
|
+
* the container — a modal frame on top of it just steals real estate
|
|
16
|
+
* from the editor. ESC + emit('close') still wire through so the
|
|
17
|
+
* parent route can window.close() the tab. */
|
|
18
|
+
chromeless?: boolean;
|
|
19
|
+
/** Explicit theme. When set the modal stamps the appropriate
|
|
20
|
+
* `.fe--theme-{light,dark}` class on its `.fe` backdrop so the CSS
|
|
21
|
+
* variable cascade matches the host shell regardless of OS
|
|
22
|
+
* preference (Modal is a portal-shaped descendant of `<body>`
|
|
23
|
+
* rather than a child of the parent component, so plain DOM
|
|
24
|
+
* inheritance isn't always enough). */
|
|
25
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits<{
|
|
29
|
+
(e: 'close'): void;
|
|
30
|
+
}>();
|
|
31
|
+
|
|
32
|
+
const cardEl = ref<HTMLElement | null>(null);
|
|
33
|
+
|
|
34
|
+
watch(
|
|
35
|
+
() => props.open,
|
|
36
|
+
(v) => {
|
|
37
|
+
if (v) {
|
|
38
|
+
document.addEventListener('keydown', onKey);
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
const focusable = cardEl.value?.querySelector<HTMLElement>(
|
|
41
|
+
'input,select,textarea,button,[tabindex]:not([tabindex="-1"])',
|
|
42
|
+
);
|
|
43
|
+
focusable?.focus();
|
|
44
|
+
}, 30);
|
|
45
|
+
} else {
|
|
46
|
+
document.removeEventListener('keydown', onKey);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
onBeforeUnmount(() => document.removeEventListener('keydown', onKey));
|
|
52
|
+
|
|
53
|
+
function onKey(e: KeyboardEvent) {
|
|
54
|
+
if (e.key === 'Escape') emit('close');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function onBackdrop() {
|
|
58
|
+
if (props.closeOnBackdrop !== false) emit('close');
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<transition name="fe-modal">
|
|
64
|
+
<div
|
|
65
|
+
v-if="open"
|
|
66
|
+
class="fe fe-modal__backdrop"
|
|
67
|
+
:class="[
|
|
68
|
+
{ 'fe-modal__backdrop--chromeless': chromeless },
|
|
69
|
+
theme === 'light' ? 'fe--theme-light' : '',
|
|
70
|
+
theme === 'dark' ? 'fe--theme-dark' : '',
|
|
71
|
+
]"
|
|
72
|
+
role="presentation"
|
|
73
|
+
@click="onBackdrop"
|
|
74
|
+
>
|
|
75
|
+
<div
|
|
76
|
+
ref="cardEl"
|
|
77
|
+
class="fe-modal__card"
|
|
78
|
+
:class="[
|
|
79
|
+
`fe-modal__card--${size || 'md'}`,
|
|
80
|
+
chromeless && 'fe-modal__card--chromeless',
|
|
81
|
+
]"
|
|
82
|
+
role="dialog"
|
|
83
|
+
aria-modal="true"
|
|
84
|
+
@click.stop
|
|
85
|
+
>
|
|
86
|
+
<header v-if="title && !chromeless" class="fe-modal__head">
|
|
87
|
+
<h2 class="fe-modal__title">{{ title }}</h2>
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
class="fe-modal__close"
|
|
91
|
+
aria-label="Close"
|
|
92
|
+
@click="emit('close')"
|
|
93
|
+
>×</button>
|
|
94
|
+
</header>
|
|
95
|
+
<div class="fe-modal__body">
|
|
96
|
+
<slot />
|
|
97
|
+
</div>
|
|
98
|
+
<footer v-if="$slots.actions && !chromeless" class="fe-modal__actions">
|
|
99
|
+
<slot name="actions" />
|
|
100
|
+
</footer>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</transition>
|
|
104
|
+
</template>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch } from 'vue';
|
|
3
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
4
|
+
import { useLocale } from '../composables/useLocale';
|
|
5
|
+
import Modal from './Modal.vue';
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
open: boolean;
|
|
9
|
+
locale: LocaleCode;
|
|
10
|
+
}>();
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
(e: 'close'): void;
|
|
14
|
+
(e: 'submit', name: string): void;
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
const { t } = useLocale(() => props.locale);
|
|
18
|
+
const name = ref('');
|
|
19
|
+
const err = ref<string | null>(null);
|
|
20
|
+
|
|
21
|
+
watch(() => props.open, (v) => {
|
|
22
|
+
if (v) {
|
|
23
|
+
name.value = '';
|
|
24
|
+
err.value = null;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function submit() {
|
|
29
|
+
const clean = name.value.trim();
|
|
30
|
+
if (!clean) {
|
|
31
|
+
err.value = t('modal.newfolder.placeholder');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (/[\\/]/.test(clean) || clean === '.' || clean === '..') {
|
|
35
|
+
err.value = 'Geçersiz karakter';
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
emit('submit', clean);
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<Modal :open="open" :title="t('modal.newfolder.title')" size="sm" @close="emit('close')">
|
|
44
|
+
<form @submit.prevent="submit">
|
|
45
|
+
<input
|
|
46
|
+
v-model="name"
|
|
47
|
+
type="text"
|
|
48
|
+
class="fe-input"
|
|
49
|
+
:placeholder="t('modal.newfolder.placeholder')"
|
|
50
|
+
autocomplete="off"
|
|
51
|
+
@keydown.enter.prevent="submit"
|
|
52
|
+
/>
|
|
53
|
+
<p v-if="err" class="fe-form__error">{{ err }}</p>
|
|
54
|
+
</form>
|
|
55
|
+
<template #actions>
|
|
56
|
+
<button type="button" class="fe-btn" @click="emit('close')">
|
|
57
|
+
{{ t('modal.newfolder.cancel') }}
|
|
58
|
+
</button>
|
|
59
|
+
<button type="button" class="fe-btn fe-btn--primary" @click="submit">
|
|
60
|
+
{{ t('modal.newfolder.create') }}
|
|
61
|
+
</button>
|
|
62
|
+
</template>
|
|
63
|
+
</Modal>
|
|
64
|
+
</template>
|