@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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/dist/ArchiveViewer-CJdaw5x3.js +99 -0
  4. package/dist/ArchiveViewer-CJdaw5x3.js.map +1 -0
  5. package/dist/CsvViewer-QYx_tePQ.js +132 -0
  6. package/dist/CsvViewer-QYx_tePQ.js.map +1 -0
  7. package/dist/DrawioViewer-GWt0TyB7.js +141 -0
  8. package/dist/DrawioViewer-GWt0TyB7.js.map +1 -0
  9. package/dist/EpubViewer-Je0ywklQ.js +146 -0
  10. package/dist/EpubViewer-Je0ywklQ.js.map +1 -0
  11. package/dist/IpynbViewer-TsB4gqP-.js +175 -0
  12. package/dist/IpynbViewer-TsB4gqP-.js.map +1 -0
  13. package/dist/MermaidViewer-CXfeDS-7.js +128 -0
  14. package/dist/MermaidViewer-CXfeDS-7.js.map +1 -0
  15. package/dist/PsdViewer-VtPpxKVe.js +113 -0
  16. package/dist/PsdViewer-VtPpxKVe.js.map +1 -0
  17. package/dist/TiffViewer-OINn9bbb.js +133 -0
  18. package/dist/TiffViewer-OINn9bbb.js.map +1 -0
  19. package/dist/UTIF-Cjsrlm0l.js +3104 -0
  20. package/dist/UTIF-Cjsrlm0l.js.map +1 -0
  21. package/dist/Viewer3D-xvvyiWHd.js +61 -0
  22. package/dist/Viewer3D-xvvyiWHd.js.map +1 -0
  23. package/dist/_commonjsHelpers-DaMA6jEr.js +9 -0
  24. package/dist/_commonjsHelpers-DaMA6jEr.js.map +1 -0
  25. package/dist/filex-core.js +26 -0
  26. package/dist/filex-core.js.map +1 -0
  27. package/dist/filex-core.umd.cjs +285 -0
  28. package/dist/filex-core.umd.cjs.map +1 -0
  29. package/dist/index-CmSYzt6L.js +5404 -0
  30. package/dist/index-CmSYzt6L.js.map +1 -0
  31. package/dist/index-UFULWo35.js +10736 -0
  32. package/dist/index-UFULWo35.js.map +1 -0
  33. package/dist/index.d.ts +1089 -0
  34. package/dist/katex-yuB6V-q6.js +11616 -0
  35. package/dist/katex-yuB6V-q6.js.map +1 -0
  36. package/dist/papaparse.min-VB1HBwYX.js +441 -0
  37. package/dist/papaparse.min-VB1HBwYX.js.map +1 -0
  38. package/dist/style.css +1 -0
  39. package/dist/useViewerFetch-czqbd2Lj.js +25 -0
  40. package/dist/useViewerFetch-czqbd2Lj.js.map +1 -0
  41. package/package.json +113 -0
  42. package/src/FileExplorer.vue +1872 -0
  43. package/src/components/Breadcrumb.vue +282 -0
  44. package/src/components/ContextMenu.vue +152 -0
  45. package/src/components/GridView.vue +166 -0
  46. package/src/components/ListView.vue +212 -0
  47. package/src/components/PendingOpsTray.vue +91 -0
  48. package/src/components/RecentlyOpened.vue +155 -0
  49. package/src/components/StarButton.vue +101 -0
  50. package/src/components/TagPicker.vue +184 -0
  51. package/src/components/Toolbar.vue +220 -0
  52. package/src/components/UploadProgress.vue +78 -0
  53. package/src/composables/useFileApi.ts +692 -0
  54. package/src/composables/useKeyboardShortcuts.ts +144 -0
  55. package/src/composables/useLocale.ts +60 -0
  56. package/src/composables/useMonacoLoader.ts +98 -0
  57. package/src/composables/usePendingOps.ts +200 -0
  58. package/src/composables/useSelection.ts +81 -0
  59. package/src/composables/useUploadChunked.ts +250 -0
  60. package/src/composables/useViewerFetch.ts +58 -0
  61. package/src/index.ts +80 -0
  62. package/src/locales/en.ts +122 -0
  63. package/src/locales/index.ts +12 -0
  64. package/src/locales/tr.ts +122 -0
  65. package/src/modals/ConvertModal.vue +254 -0
  66. package/src/modals/DeleteConfirmModal.vue +32 -0
  67. package/src/modals/Modal.vue +104 -0
  68. package/src/modals/NewFolderModal.vue +64 -0
  69. package/src/modals/PermissionsModal.vue +815 -0
  70. package/src/modals/PreviewModal.vue +1070 -0
  71. package/src/modals/RenameModal.vue +58 -0
  72. package/src/modals/ShareModal.vue +135 -0
  73. package/src/styles/base.css +1224 -0
  74. package/src/styles/variables.css +90 -0
  75. package/src/types/ExplorerConfig.ts +282 -0
  76. package/src/types/FileNode.ts +136 -0
  77. package/src/types/index.ts +27 -0
  78. package/src/types/peers.d.ts +45 -0
  79. package/src/viewers/ArchiveViewer.vue +186 -0
  80. package/src/viewers/CsvViewer.vue +319 -0
  81. package/src/viewers/DrawioViewer.vue +243 -0
  82. package/src/viewers/EpubViewer.vue +307 -0
  83. package/src/viewers/IpynbViewer.vue +365 -0
  84. package/src/viewers/MermaidViewer.vue +259 -0
  85. package/src/viewers/PdfViewer.vue +562 -0
  86. package/src/viewers/PsdViewer.vue +264 -0
  87. package/src/viewers/TiffViewer.vue +273 -0
  88. 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>