@brftech/filex-core 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/{ArchiveViewer-B_0IQExT.js → ArchiveViewer-DKW-HyXT.js} +2 -2
  2. package/dist/{ArchiveViewer-B_0IQExT.js.map → ArchiveViewer-DKW-HyXT.js.map} +1 -1
  3. package/dist/{CsvViewer-maMfUy5i.js → CsvViewer-D2HmP2yr.js} +2 -2
  4. package/dist/{CsvViewer-maMfUy5i.js.map → CsvViewer-D2HmP2yr.js.map} +1 -1
  5. package/dist/{DrawioViewer-DP_pXI00.js → DrawioViewer-DPNvOFCs.js} +2 -2
  6. package/dist/{DrawioViewer-DP_pXI00.js.map → DrawioViewer-DPNvOFCs.js.map} +1 -1
  7. package/dist/{EpubViewer-DSaqGnVx.js → EpubViewer-ByqjNuNL.js} +2 -2
  8. package/dist/{EpubViewer-DSaqGnVx.js.map → EpubViewer-ByqjNuNL.js.map} +1 -1
  9. package/dist/{IpynbViewer-BmdBanF1.js → IpynbViewer-2iONhbuJ.js} +2 -2
  10. package/dist/{IpynbViewer-BmdBanF1.js.map → IpynbViewer-2iONhbuJ.js.map} +1 -1
  11. package/dist/{MermaidViewer-Drlkl0Z5.js → MermaidViewer-CmiU7hIT.js} +2 -2
  12. package/dist/{MermaidViewer-Drlkl0Z5.js.map → MermaidViewer-CmiU7hIT.js.map} +1 -1
  13. package/dist/{PsdViewer-gftUJrsm.js → PsdViewer-Inhl2yN9.js} +2 -2
  14. package/dist/{PsdViewer-gftUJrsm.js.map → PsdViewer-Inhl2yN9.js.map} +1 -1
  15. package/dist/{TiffViewer-0Uv0-Lxh.js → TiffViewer-C2AFFEze.js} +2 -2
  16. package/dist/{TiffViewer-0Uv0-Lxh.js.map → TiffViewer-C2AFFEze.js.map} +1 -1
  17. package/dist/{Viewer3D-Gu3kUpxV.js → Viewer3D-YdW14q_n.js} +2 -2
  18. package/dist/{Viewer3D-Gu3kUpxV.js.map → Viewer3D-YdW14q_n.js.map} +1 -1
  19. package/dist/filex-core.js +47 -25
  20. package/dist/filex-core.umd.cjs +40 -40
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-eARevpz0.js +9952 -0
  23. package/dist/index-eARevpz0.js.map +1 -0
  24. package/dist/index.d.ts +363 -3
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +269 -1
  28. package/src/components/CommandPalette.vue +12 -0
  29. package/src/components/ContextMenu.vue +114 -8
  30. package/src/components/GridView.vue +33 -1
  31. package/src/components/ListView.vue +39 -6
  32. package/src/components/OnboardingTour.vue +332 -0
  33. package/src/components/OperationsCenter.vue +284 -0
  34. package/src/components/PendingOpsTray.vue +50 -66
  35. package/src/components/QuickLook.vue +142 -0
  36. package/src/components/ShortcutSettings.vue +270 -0
  37. package/src/components/ShortcutsHelp.vue +30 -9
  38. package/src/components/ThemeGallery.vue +253 -0
  39. package/src/components/Toolbar.vue +52 -6
  40. package/src/components/UploadProgress.vue +49 -57
  41. package/src/composables/useKeyboardShortcuts.ts +321 -137
  42. package/src/composables/useOperations.ts +291 -0
  43. package/src/index.ts +45 -0
  44. package/src/lib/dragGhost.ts +41 -0
  45. package/src/lib/themes.ts +505 -0
  46. package/src/locales/en.ts +92 -0
  47. package/src/locales/tr.ts +92 -0
  48. package/src/modals/Modal.vue +41 -5
  49. package/src/styles/base.css +672 -0
  50. package/dist/index-BLocdbmc.js +0 -7993
  51. package/dist/index-BLocdbmc.js.map +0 -1
@@ -0,0 +1,284 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * OperationsCenter — the single visible surface for long-running work
4
+ * (wiring:c3). Compact bottom-right badge (active count + aggregate
5
+ * progress ring; hidden when idle) that expands into a panel: active
6
+ * operations on top, session history below.
7
+ *
8
+ * Renders whatever the useOperations store aggregates — uploads, ops-queue
9
+ * jobs, future convert/archive kinds — with one uniform row template.
10
+ * Cancel / retry / dismiss route back through the store's action registry
11
+ * to the owning publisher.
12
+ */
13
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
14
+ import type { LocaleCode } from '../types/ExplorerConfig';
15
+ import type { Operation, OperationsStore } from '../composables/useOperations';
16
+ import { useLocale } from '../composables/useLocale';
17
+
18
+ const props = defineProps<{
19
+ center: OperationsStore;
20
+ locale: LocaleCode;
21
+ narrow?: boolean;
22
+ }>();
23
+
24
+ const { t, formatSize } = useLocale(() => props.locale);
25
+
26
+ const open = ref(false);
27
+ const rootEl = ref<HTMLElement | null>(null);
28
+
29
+ const active = computed(() => props.center.active.value);
30
+ const history = computed(() => props.center.history.value);
31
+ const hasError = computed(() => props.center.hasError.value);
32
+ const overall = computed(() => props.center.overallPercent.value);
33
+ const runningCount = computed(() => props.center.runningCount.value);
34
+
35
+ // Badge visible while anything is active; once the panel is open it stays
36
+ // up (showing history) until the user closes it. Idle + closed → hidden.
37
+ const visible = computed(() => active.value.length > 0 || (open.value && history.value.length > 0));
38
+
39
+ watch(visible, (v) => {
40
+ if (!v) open.value = false;
41
+ });
42
+
43
+ // ---- progress ring ----
44
+ const RING_R = 8.5;
45
+ const RING_C = 2 * Math.PI * RING_R;
46
+ const ringOffset = computed(() => {
47
+ const p = overall.value;
48
+ if (p === null) return RING_C * 0.72; // spinner arc
49
+ return RING_C * (1 - Math.min(100, Math.max(0, p)) / 100);
50
+ });
51
+ const ringSpin = computed(() => overall.value === null && runningCount.value > 0);
52
+
53
+ const pctText = computed(() => {
54
+ if (runningCount.value === 0 || overall.value === null) return '';
55
+ return t('opc.percent', { n: overall.value });
56
+ });
57
+
58
+ const badgeAria = computed(() => t('opc.aria_badge', { n: active.value.length }));
59
+
60
+ // ---- row helpers ----
61
+ const KIND_ICONS: Record<Operation['kind'], string> = {
62
+ upload: 'M12 16V5M7 9.5 12 5l5 4.5M5 19h14',
63
+ copy: 'M9 9h11v11H9zM5 15V4h11',
64
+ move: 'M4 12h13M12 6l6 6-6 6',
65
+ delete: 'M5 7h14M9 7V5h6v2M8 7l1 13h6l1-13',
66
+ convert: 'M20 8A8 8 0 0 0 6 6L4 8M4 16a8 8 0 0 0 14 2l2-2M20 3v5h-5M4 21v-5h5',
67
+ archive: 'M4 8V5h16v3zM5 8h14v12H5zM10 12h4',
68
+ };
69
+
70
+ function kindLabel(o: Operation): string {
71
+ return t(`opc.kind.${o.kind}`);
72
+ }
73
+
74
+ function rowTitle(o: Operation): string {
75
+ return o.name || kindLabel(o);
76
+ }
77
+
78
+ function statusText(o: Operation): string {
79
+ if (o.status === 'running') {
80
+ if (o.queued) return t('opc.queued');
81
+ if (o.totalBytes && o.totalBytes > 0) {
82
+ return `${formatSize(o.uploadedBytes ?? 0)} / ${formatSize(o.totalBytes)}`;
83
+ }
84
+ if (o.totalCount && o.totalCount > 0) {
85
+ return `${o.doneCount ?? 0}/${o.totalCount}`;
86
+ }
87
+ return t('opc.status.running');
88
+ }
89
+ if (o.status === 'done') return t('opc.status.done');
90
+ if (o.status === 'aborted') return t('opc.status.aborted');
91
+ return o.error || t('opc.status.error');
92
+ }
93
+
94
+ function chipText(o: Operation): string {
95
+ return t(`opc.status.${o.status === 'running' ? 'running' : o.status}`);
96
+ }
97
+
98
+ // ---- outside click + Escape ----
99
+ function onDocPointerDown(ev: MouseEvent) {
100
+ if (!open.value) return;
101
+ const el = rootEl.value;
102
+ if (el && ev.target instanceof Node && !el.contains(ev.target)) open.value = false;
103
+ }
104
+ function onDocKeydown(ev: KeyboardEvent) {
105
+ if (open.value && ev.key === 'Escape') open.value = false;
106
+ }
107
+ onMounted(() => {
108
+ document.addEventListener('mousedown', onDocPointerDown);
109
+ document.addEventListener('keydown', onDocKeydown);
110
+ });
111
+ onBeforeUnmount(() => {
112
+ document.removeEventListener('mousedown', onDocPointerDown);
113
+ document.removeEventListener('keydown', onDocKeydown);
114
+ });
115
+ </script>
116
+
117
+ <template>
118
+ <div
119
+ v-if="visible"
120
+ ref="rootEl"
121
+ class="fe-opc"
122
+ :class="{ 'fe-opc--narrow': narrow, 'fe-opc--open': open }"
123
+ >
124
+ <transition name="fe-opc-pop">
125
+ <section v-if="open" class="fe-opc__panel" role="dialog" :aria-label="t('opc.title')">
126
+ <header class="fe-opc__head">
127
+ <strong>{{ t('opc.title') }}</strong>
128
+ <button
129
+ type="button"
130
+ class="fe-opc__x"
131
+ :aria-label="t('opc.close')"
132
+ @click="open = false"
133
+ >×</button>
134
+ </header>
135
+ <div class="fe-opc__body">
136
+ <div v-if="active.length > 0" class="fe-opc__section" aria-live="polite">
137
+ <h3 class="fe-opc__section-title">{{ t('opc.active') }}</h3>
138
+ <ul class="fe-opc__list">
139
+ <li
140
+ v-for="o in active"
141
+ :key="o.key"
142
+ class="fe-opc__item"
143
+ :class="`is-${o.status}`"
144
+ >
145
+ <svg
146
+ class="fe-opc__kicon"
147
+ viewBox="0 0 24 24"
148
+ fill="none"
149
+ stroke="currentColor"
150
+ stroke-width="1.8"
151
+ stroke-linecap="round"
152
+ stroke-linejoin="round"
153
+ aria-hidden="true"
154
+ focusable="false"
155
+ >
156
+ <path :d="KIND_ICONS[o.kind]" />
157
+ </svg>
158
+ <div class="fe-opc__mid">
159
+ <div class="fe-opc__name" :title="rowTitle(o)">{{ rowTitle(o) }}</div>
160
+ <div class="fe-opc__sub">
161
+ <span class="fe-opc__kind">{{ kindLabel(o) }}</span>
162
+ <span class="fe-opc__dot" aria-hidden="true">·</span>
163
+ <span class="fe-opc__state">{{ statusText(o) }}</span>
164
+ </div>
165
+ <div
166
+ v-if="o.status === 'running' && o.percent !== null"
167
+ class="fe-opc__bar"
168
+ role="progressbar"
169
+ :aria-valuenow="o.percent"
170
+ aria-valuemin="0"
171
+ aria-valuemax="100"
172
+ >
173
+ <div class="fe-opc__bar-fill" :style="{ width: o.percent + '%' }" />
174
+ </div>
175
+ </div>
176
+ <span
177
+ v-if="o.status === 'running' && o.percent === null && !o.queued"
178
+ class="fe-opc__spin"
179
+ aria-hidden="true"
180
+ />
181
+ <div class="fe-opc__acts">
182
+ <button
183
+ v-if="o.status === 'running' && o.cancellable"
184
+ type="button"
185
+ class="fe-opc__act"
186
+ @click="center.cancel(o.key)"
187
+ >{{ t('opc.cancel') }}</button>
188
+ <button
189
+ v-if="o.status === 'error' && o.retryable"
190
+ type="button"
191
+ class="fe-opc__act fe-opc__act--retry"
192
+ @click="center.retry(o.key)"
193
+ >{{ t('opc.retry') }}</button>
194
+ <button
195
+ v-if="o.status !== 'running'"
196
+ type="button"
197
+ class="fe-opc__x"
198
+ :aria-label="t('opc.dismiss')"
199
+ @click="center.dismiss(o.key)"
200
+ >×</button>
201
+ </div>
202
+ </li>
203
+ </ul>
204
+ </div>
205
+
206
+ <div v-if="history.length > 0" class="fe-opc__section">
207
+ <h3 class="fe-opc__section-title">
208
+ {{ t('opc.history') }}
209
+ <button type="button" class="fe-opc__act" @click="center.clearHistory()">
210
+ {{ t('opc.clear') }}
211
+ </button>
212
+ </h3>
213
+ <ul class="fe-opc__list fe-opc__list--history">
214
+ <li
215
+ v-for="o in history"
216
+ :key="'h-' + o.key"
217
+ class="fe-opc__item fe-opc__item--history"
218
+ :class="`is-${o.status}`"
219
+ >
220
+ <svg
221
+ class="fe-opc__kicon"
222
+ viewBox="0 0 24 24"
223
+ fill="none"
224
+ stroke="currentColor"
225
+ stroke-width="1.8"
226
+ stroke-linecap="round"
227
+ stroke-linejoin="round"
228
+ aria-hidden="true"
229
+ focusable="false"
230
+ >
231
+ <path :d="KIND_ICONS[o.kind]" />
232
+ </svg>
233
+ <div class="fe-opc__mid">
234
+ <div class="fe-opc__name" :title="rowTitle(o)">{{ rowTitle(o) }}</div>
235
+ <div v-if="o.status === 'error' && o.error" class="fe-opc__sub fe-opc__sub--err">
236
+ {{ o.error }}
237
+ </div>
238
+ </div>
239
+ <span class="fe-opc__chip" :class="`is-${o.status}`">{{ chipText(o) }}</span>
240
+ </li>
241
+ </ul>
242
+ </div>
243
+
244
+ <p v-if="active.length === 0 && history.length === 0" class="fe-opc__empty">
245
+ {{ t('opc.empty') }}
246
+ </p>
247
+ </div>
248
+ </section>
249
+ </transition>
250
+
251
+ <button
252
+ type="button"
253
+ class="fe-opc__badge"
254
+ :class="{ 'is-error': hasError }"
255
+ :aria-expanded="open"
256
+ :aria-label="badgeAria"
257
+ @click="open = !open"
258
+ >
259
+ <svg
260
+ class="fe-opc__ring"
261
+ :class="{ 'fe-opc__ring--spin': ringSpin }"
262
+ viewBox="0 0 22 22"
263
+ aria-hidden="true"
264
+ focusable="false"
265
+ >
266
+ <circle class="fe-opc__ring-track" cx="11" cy="11" :r="RING_R" fill="none" stroke-width="2.5" />
267
+ <circle
268
+ class="fe-opc__ring-fill"
269
+ cx="11"
270
+ cy="11"
271
+ :r="RING_R"
272
+ fill="none"
273
+ stroke-width="2.5"
274
+ stroke-linecap="round"
275
+ :stroke-dasharray="RING_C"
276
+ :stroke-dashoffset="ringOffset"
277
+ transform="rotate(-90 11 11)"
278
+ />
279
+ </svg>
280
+ <span class="fe-opc__count">{{ active.length }}</span>
281
+ <span v-if="pctText" class="fe-opc__pct">{{ pctText }}</span>
282
+ </button>
283
+ </div>
284
+ </template>
@@ -1,91 +1,75 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * PendingOpsTray — bottom-left toast list for queued copy/move/delete.
3
+ * PendingOpsTray — renderless ops-queue publisher (wiring:c3).
4
4
  *
5
- * Sits next to UploadProgress visually (offset to its left when both
6
- * are open). Each row shows op type + a progress bar driven by
7
- * `progress_done / progress_total`. Terminal rows linger for a moment
8
- * (RETAIN_MS in usePendingOps) so the final state is visible before
9
- * they fade out.
5
+ * Historically a bottom-left toast list for queued copy/move/delete. Since
6
+ * the unified operations center it renders NOTHING: it reconciles the
7
+ * usePendingOps rows into the shared useOperations store; OperationsCenter
8
+ * draws them. Terminal-state lingering ("bitti" flash history) and sticky
9
+ * errors are handled by the store — note usePendingOps sweeps terminal rows
10
+ * after its RETAIN window, which the store treats as "move to history"
11
+ * (errors stay pinned until the user dismisses them).
12
+ *
13
+ * Queue ops are not client-retryable: the normalized PendingOp no longer
14
+ * carries the source list, so a failed op only offers dismiss.
10
15
  */
11
- import { computed } from 'vue';
16
+ import { watch } from 'vue';
12
17
  import type { LocaleCode } from '../types/ExplorerConfig';
13
18
  import type { PendingOp } from '../composables/usePendingOps';
14
- import { useLocale } from '../composables/useLocale';
19
+ import type { OperationsStore, OperationStatus } from '../composables/useOperations';
15
20
 
16
21
  const props = defineProps<{
17
22
  ops: PendingOp[];
23
+ /** Kept for mount compatibility — strings now render in OperationsCenter. */
18
24
  locale: LocaleCode;
25
+ center: OperationsStore;
19
26
  }>();
20
27
 
21
28
  const emit = defineEmits<{
22
29
  (e: 'dismiss', id: number): void;
23
30
  }>();
24
31
 
25
- const { t } = useLocale(() => props.locale);
26
-
27
- const sorted = computed(() => [...props.ops].sort((a, b) => b.id - a.id));
32
+ function mapStatus(op: PendingOp): OperationStatus {
33
+ if (op.status === 'done') return 'done';
34
+ if (op.status === 'error') return 'error';
35
+ return 'running'; // pending | running
36
+ }
28
37
 
29
- function percent(op: PendingOp): number {
38
+ function percentOf(op: PendingOp): number | null {
30
39
  if (op.status === 'done') return 100;
31
- if (op.progress_total <= 0) return 0;
40
+ if (op.progress_total <= 0) return null;
32
41
  return Math.min(100, Math.round((op.progress_done / op.progress_total) * 100));
33
42
  }
34
43
 
35
- function statusClass(op: PendingOp): string {
36
- if (op.status === 'done') return 'is-done';
37
- if (op.status === 'error') return 'is-error';
38
- return '';
39
- }
40
-
41
- function summary(op: PendingOp): string {
42
- const verb =
43
- op.op_type === 'copy'
44
- ? t('ops.copy')
45
- : op.op_type === 'move'
46
- ? t('ops.move')
47
- : t('ops.delete');
48
- if (op.status === 'pending') return `${verb} (${t('ops.queued')})`;
49
- if (op.status === 'running') return `${verb} ${op.progress_done}/${op.progress_total}`;
50
- if (op.status === 'done') return `${verb} ${t('ops.done')} (${op.progress_total})`;
51
- if (op.status === 'error') return `${verb} ${t('ops.error')}`;
52
- return verb;
53
- }
44
+ watch(
45
+ () => props.ops,
46
+ (ops) => {
47
+ props.center.sync(
48
+ 'ops',
49
+ ops.map((op) => ({
50
+ input: {
51
+ id: op.id,
52
+ kind: op.op_type,
53
+ name: op.target_path || op.source_dir || '',
54
+ percent: percentOf(op),
55
+ status: mapStatus(op),
56
+ error: op.error_message,
57
+ queued: op.status === 'pending',
58
+ doneCount: op.progress_done,
59
+ totalCount: op.progress_total,
60
+ cancellable: false,
61
+ retryable: false,
62
+ },
63
+ actions: {
64
+ dismiss: () => emit('dismiss', op.id),
65
+ },
66
+ })),
67
+ );
68
+ },
69
+ { immediate: true },
70
+ );
54
71
  </script>
55
72
 
56
73
  <template>
57
- <transition-group
58
- v-if="sorted.length > 0"
59
- name="fe-ops-slide"
60
- tag="div"
61
- class="fe-ops"
62
- role="status"
63
- aria-live="polite"
64
- >
65
- <div
66
- v-for="op in sorted"
67
- :key="op.id"
68
- class="fe-ops__item"
69
- :class="statusClass(op)"
70
- >
71
- <div class="fe-ops__row">
72
- <span class="fe-ops__title" :title="op.target_path || op.source_dir || ''">
73
- {{ summary(op) }}
74
- </span>
75
- <button
76
- v-if="op.status === 'done' || op.status === 'error'"
77
- type="button"
78
- class="fe-ops__dismiss"
79
- aria-label="Dismiss"
80
- @click="emit('dismiss', op.id)"
81
- >×</button>
82
- </div>
83
- <div class="fe-ops__bar">
84
- <div class="fe-ops__bar-fill" :style="{ width: percent(op) + '%' }" />
85
- </div>
86
- <div v-if="op.status === 'error' && op.error_message" class="fe-ops__error">
87
- {{ op.error_message }}
88
- </div>
89
- </div>
90
- </transition-group>
74
+ <i v-if="false" />
91
75
  </template>
@@ -0,0 +1,142 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * QuickLook — wiring:c2 Space quick-look overlay.
4
+ *
5
+ * A thin behavioural wrapper over the existing PreviewModal (view
6
+ * mode), so every viewer the modal knows (image/video/pdf/office/code/
7
+ * 3D/archive/…) works in quick-look for free. What this layer adds:
8
+ *
9
+ * - Space closes again (macOS Quick Look convention)
10
+ * - ← ↑ / → ↓ ask the host to move the selection; the host keeps the
11
+ * `file` prop in sync so the preview follows the selection
12
+ * - Enter promotes the peek into the full open flow (emit 'open-full')
13
+ * - a small floating hint bar with the key legend
14
+ *
15
+ * Keys are handled in window CAPTURE phase with stopPropagation so the
16
+ * global shortcut registry never double-handles them; events that
17
+ * originate in a form control inside a viewer (e.g. the CSV filter
18
+ * input) are left alone. Esc is untouched — PreviewModal's own Modal
19
+ * already closes on it.
20
+ */
21
+ import { onBeforeUnmount, watch } from 'vue';
22
+ import type { FileNode } from '../types/FileNode';
23
+ import type { LocaleCode } from '../types/ExplorerConfig';
24
+ import { useLocale } from '../composables/useLocale';
25
+ import PreviewModal from '../modals/PreviewModal.vue';
26
+
27
+ const props = defineProps<{
28
+ open: boolean;
29
+ locale: LocaleCode;
30
+ file: FileNode | null;
31
+ previewUrl: (path: string) => string;
32
+ downloadUrl: (path: string) => string;
33
+ onlyOfficeBase?: string | null;
34
+ onlyOfficeConfigEndpoint?: string | null;
35
+ authHeaders?: () => Record<string, string>;
36
+ authCredentials?: RequestCredentials;
37
+ drawioUrl?: string | null;
38
+ pdfWorkerUrl?: string | null;
39
+ viewerBaseUrl?: string | null;
40
+ theme?: 'light' | 'dark' | 'auto';
41
+ }>();
42
+
43
+ const emit = defineEmits<{
44
+ (e: 'close'): void;
45
+ /** Arrow navigation: -1 = previous file, +1 = next file. */
46
+ (e: 'nav', delta: number): void;
47
+ /** Enter — close the peek and open the file for real. */
48
+ (e: 'open-full'): void;
49
+ }>();
50
+
51
+ const { t } = useLocale(() => props.locale);
52
+
53
+ function inFormControl(target: EventTarget | null): boolean {
54
+ const el = target as HTMLElement | null;
55
+ return !!(
56
+ el &&
57
+ (el.tagName === 'INPUT' ||
58
+ el.tagName === 'TEXTAREA' ||
59
+ el.tagName === 'SELECT' ||
60
+ el.isContentEditable)
61
+ );
62
+ }
63
+
64
+ function onKeydown(e: KeyboardEvent) {
65
+ if (!props.open) return;
66
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
67
+ if (inFormControl(e.target)) return;
68
+ switch (e.key) {
69
+ case ' ':
70
+ e.preventDefault();
71
+ e.stopPropagation();
72
+ emit('close');
73
+ break;
74
+ case 'ArrowRight':
75
+ case 'ArrowDown':
76
+ e.preventDefault();
77
+ e.stopPropagation();
78
+ emit('nav', 1);
79
+ break;
80
+ case 'ArrowLeft':
81
+ case 'ArrowUp':
82
+ e.preventDefault();
83
+ e.stopPropagation();
84
+ emit('nav', -1);
85
+ break;
86
+ case 'Enter':
87
+ e.preventDefault();
88
+ e.stopPropagation();
89
+ emit('open-full');
90
+ break;
91
+ }
92
+ }
93
+
94
+ watch(
95
+ () => props.open,
96
+ (open) => {
97
+ if (open) window.addEventListener('keydown', onKeydown, true);
98
+ else window.removeEventListener('keydown', onKeydown, true);
99
+ },
100
+ { immediate: true },
101
+ );
102
+
103
+ onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown, true));
104
+ </script>
105
+
106
+ <template>
107
+ <PreviewModal
108
+ class="fe-quicklook"
109
+ :open="open"
110
+ :locale="locale"
111
+ :file="file"
112
+ :theme="theme"
113
+ :preview-url="previewUrl"
114
+ :download-url="downloadUrl"
115
+ :only-office-base="onlyOfficeBase"
116
+ :only-office-config-endpoint="onlyOfficeConfigEndpoint"
117
+ open-mode="view"
118
+ :auth-headers="authHeaders"
119
+ :auth-credentials="authCredentials"
120
+ :drawio-url="drawioUrl"
121
+ :pdf-worker-url="pdfWorkerUrl"
122
+ :viewer-base-url="viewerBaseUrl"
123
+ @close="emit('close')"
124
+ />
125
+ <transition name="fe-toast">
126
+ <div
127
+ v-if="open"
128
+ class="fe fe-ql-hint"
129
+ :class="{
130
+ 'fe--theme-light': theme === 'light',
131
+ 'fe--theme-dark': theme === 'dark',
132
+ }"
133
+ aria-hidden="true"
134
+ >
135
+ <kbd class="fe-kbd">Space</kbd> {{ t('quicklook.hint_close') }}
136
+ <span class="fe-ql-hint__sep">·</span>
137
+ <kbd class="fe-kbd">↑</kbd><kbd class="fe-kbd">↓</kbd> {{ t('quicklook.hint_nav') }}
138
+ <span class="fe-ql-hint__sep">·</span>
139
+ <kbd class="fe-kbd">Enter</kbd> {{ t('quicklook.hint_open') }}
140
+ </div>
141
+ </transition>
142
+ </template>