@brftech/filex-core 0.42.1 → 0.43.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.
- package/README.md +14 -2
- package/dist/ArchiveViewer-BqU761Wk.js +149 -0
- package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
- package/dist/CsvViewer-ByHVK5ga.js +172 -0
- package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
- package/dist/{DrawioViewer-BuhZOAx6.js → DrawioViewer-Bgrj8-zG.js} +45 -44
- package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
- package/dist/{EpubViewer-DS3mb3k8.js → EpubViewer-DJwUKlHW.js} +50 -50
- package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
- package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
- package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
- package/dist/MermaidViewer-CEDTD_pp.js +136 -0
- package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
- package/dist/PsdViewer-CZcxUpZO.js +121 -0
- package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
- package/dist/TiffViewer-BwI62CbQ.js +141 -0
- package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
- package/dist/{Viewer3D-CIrOn63I.js → Viewer3D-DoeZcuMm.js} +16 -16
- package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
- package/dist/filex-core.js +475 -219
- package/dist/filex-core.umd.cjs +165 -108
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/fonts/caveat-latin-ext.woff2 +0 -0
- package/dist/fonts/caveat-latin.woff2 +0 -0
- package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
- package/dist/fonts/dancing-script-latin.woff2 +0 -0
- package/dist/fonts/homemade-apple-latin.woff2 +0 -0
- package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
- package/dist/fonts/source-serif-4-latin.woff2 +0 -0
- package/dist/index-DuWnGuv5.js +35901 -0
- package/dist/index-DuWnGuv5.js.map +1 -0
- package/dist/index.d.ts +5241 -553
- package/dist/pdf-CaPYsOUo.js +21302 -0
- package/dist/pdf-CaPYsOUo.js.map +1 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +1207 -343
- package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
- package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
- package/src/assets/fonts/README.md +58 -0
- package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
- package/src/assets/fonts/caveat-latin.woff2 +0 -0
- package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
- package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
- package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
- package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
- package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
- package/src/components/AdvancedSearch.vue +49 -0
- package/src/components/Breadcrumb.vue +5 -5
- package/src/components/ChoiceButtons.vue +152 -0
- package/src/components/CommandPalette.vue +5 -5
- package/src/components/ConnectionGuideView.vue +1 -1
- package/src/components/ConnectionNotice.vue +59 -0
- package/src/components/ConnectionsPanel.vue +30 -568
- package/src/components/ContextMenu.vue +31 -5
- package/src/components/DataTable.vue +1331 -0
- package/src/components/FilePane.vue +3 -0
- package/src/components/FilterBar.vue +12 -5
- package/src/components/GalleryView.vue +50 -2
- package/src/components/GridView.vue +51 -3
- package/src/components/HomeView.vue +18 -6
- package/src/components/InspectorPanel.vue +134 -17
- package/src/components/ListView.vue +366 -900
- package/src/components/LogoMark.vue +57 -0
- package/src/components/NFSExportsPanel.vue +76 -28
- package/src/components/OnboardingTour.vue +19 -7
- package/src/components/OperationsCenter.vue +77 -1
- package/src/components/PendingOpsTray.vue +59 -5
- package/src/components/PresenceBar.vue +7 -7
- package/src/components/ProductVersion.vue +31 -0
- package/src/components/QuickLook.vue +10 -14
- package/src/components/RecentlyOpened.vue +47 -17
- package/src/components/RecoveryKeyModal.vue +1 -1
- package/src/components/RowActions.vue +134 -0
- package/src/components/S3KeysPanel.vue +98 -56
- package/src/components/SSHKeysPanel.vue +79 -26
- package/src/components/SideNav.vue +260 -96
- package/src/components/StarButton.vue +1 -0
- package/src/components/StorageFields.vue +74 -6
- package/src/components/StorageTags.vue +62 -0
- package/src/components/TabBar.vue +1 -1
- package/src/components/TagKindIcon.vue +56 -0
- package/src/components/TagPicker.vue +226 -40
- package/src/components/ThemePalette.vue +4 -4
- package/src/components/TimeZonePicker.vue +29 -10
- package/src/components/TokensPanel.vue +82 -27
- package/src/components/Toolbar.vue +28 -6
- package/src/components/plugin/PluginConfirmModal.vue +54 -0
- package/src/components/plugin/PluginInspectorSection.vue +149 -0
- package/src/components/plugin/PluginPageView.vue +339 -0
- package/src/components/plugin/PluginViewModal.vue +159 -0
- package/src/components/plugin/SurfaceConversation.vue +76 -0
- package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
- package/src/components/plugin/SurfaceRenderer.vue +299 -0
- package/src/components/plugin/SurfaceSections.vue +91 -0
- package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
- package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
- package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
- package/src/components/plugin/nodes/SurfaceList.vue +127 -0
- package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
- package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
- package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
- package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
- package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
- package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
- package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
- package/src/components/public/PublicLanguagePicker.vue +58 -0
- package/src/components/public/PublicLinkPage.vue +286 -0
- package/src/components/public/PublicLinkPreview.vue +85 -0
- package/src/components/public/PublicPinGate.vue +108 -0
- package/src/components/public/PublicRequestBody.vue +204 -0
- package/src/components/public/PublicShareBody.vue +188 -0
- package/src/components/public/PublicShell.vue +332 -0
- package/src/composables/useConnections.ts +8 -2
- package/src/composables/useFileApi.ts +183 -44
- package/src/composables/useLocale.ts +277 -41
- package/src/composables/useNFSExports.ts +8 -10
- package/src/composables/useOperations.ts +44 -3
- package/src/composables/usePendingOps.ts +59 -8
- package/src/composables/usePluginActions.ts +121 -0
- package/src/composables/usePluginSurface.ts +223 -0
- package/src/composables/usePublicBranding.ts +172 -0
- package/src/composables/usePublicLink.ts +573 -0
- package/src/composables/usePublicPage.ts +61 -0
- package/src/composables/useS3Keys.ts +17 -12
- package/src/composables/useSSHKeys.ts +8 -10
- package/src/composables/useTokens.ts +8 -12
- package/src/composables/useUploadChunked.ts +18 -8
- package/src/composables/useViewerFetch.ts +6 -3
- package/src/index.ts +404 -8
- package/src/lib/actionIcons.ts +46 -1
- package/src/lib/advSearch.ts +14 -7
- package/src/lib/appLock.ts +130 -0
- package/src/lib/connection.ts +110 -0
- package/src/lib/destinationTree.ts +19 -3
- package/src/lib/direction.ts +353 -0
- package/src/lib/dragGhost.ts +6 -0
- package/src/lib/dropLimits.ts +69 -0
- package/src/lib/errorWords.ts +394 -0
- package/src/lib/externalReach.ts +5 -1
- package/src/lib/fileFilters.ts +7 -2
- package/src/lib/fileIcons.ts +4 -0
- package/src/lib/internalPaths.ts +77 -0
- package/src/lib/listInput.ts +64 -0
- package/src/lib/listing.ts +58 -23
- package/src/lib/modalStack.ts +26 -0
- package/src/lib/nodeRow.ts +121 -0
- package/src/lib/pdfFieldRules.ts +126 -0
- package/src/lib/pdfFields.ts +589 -0
- package/src/lib/pdfFieldsGeom.ts +203 -0
- package/src/lib/pdfjsLoader.ts +51 -0
- package/src/lib/personName.ts +47 -0
- package/src/lib/pluginApplies.ts +123 -0
- package/src/lib/pluginLabel.ts +72 -0
- package/src/lib/pluginMenu.ts +253 -0
- package/src/lib/pluginPage.ts +58 -0
- package/src/lib/plural.ts +181 -0
- package/src/lib/popupLayer.ts +103 -0
- package/src/lib/prefs.ts +563 -0
- package/src/lib/productVersion.ts +35 -0
- package/src/lib/publicLayout.ts +39 -0
- package/src/lib/serviceGate.ts +104 -0
- package/src/lib/shareTtl.ts +25 -11
- package/src/lib/signFonts.ts +85 -0
- package/src/lib/signaturePad.ts +111 -0
- package/src/lib/sortOrder.ts +100 -38
- package/src/lib/star.ts +7 -1
- package/src/lib/surfaceCell.ts +41 -0
- package/src/lib/surfaceConditions.ts +156 -0
- package/src/lib/surfaceOpen.ts +106 -0
- package/src/lib/surfaceSteps.ts +68 -0
- package/src/lib/surfaceValues.ts +191 -0
- package/src/lib/symlink.ts +150 -0
- package/src/lib/tableColumns.ts +493 -0
- package/src/lib/tableEnv.ts +34 -0
- package/src/lib/tablePrefs.ts +84 -0
- package/src/lib/tags.ts +121 -12
- package/src/lib/themes.ts +286 -18
- package/src/lib/tour.ts +152 -0
- package/src/lib/trashEmpty.ts +111 -0
- package/src/lib/trashTimeLeft.ts +23 -0
- package/src/lib/uiLocales.ts +460 -0
- package/src/lib/viewPrefs.ts +696 -660
- package/src/lib/viewPrefsHttp.ts +78 -0
- package/src/locales/en.ts +522 -119
- package/src/locales/resolve.ts +17 -8
- package/src/locales/tr.ts +539 -155
- package/src/modals/ConvertModal.vue +62 -11
- package/src/modals/DestinationPickerModal.vue +59 -16
- package/src/modals/Modal.vue +113 -23
- package/src/modals/NewDocumentModal.vue +16 -6
- package/src/modals/NewFolderModal.vue +8 -0
- package/src/modals/PermissionsModal.vue +272 -163
- package/src/modals/PreviewModal.vue +119 -20
- package/src/modals/RenameModal.vue +37 -1
- package/src/styles/base.css +2895 -241
- package/src/styles/sign-fonts.css +118 -0
- package/src/styles/variables.css +60 -0
- package/src/types/Connections.ts +17 -0
- package/src/types/ExplorerConfig.ts +117 -1
- package/src/types/FileNode.ts +58 -0
- package/src/types/Plugins.ts +494 -0
- package/src/types/Public.ts +212 -0
- package/src/viewers/ArchiveViewer.vue +93 -48
- package/src/viewers/CsvViewer.vue +99 -53
- package/src/viewers/DrawioViewer.vue +35 -9
- package/src/viewers/EpubViewer.vue +7 -8
- package/src/viewers/IpynbViewer.vue +3 -1
- package/src/viewers/MermaidViewer.vue +4 -5
- package/src/viewers/PdfViewer.vue +8 -17
- package/src/viewers/PsdViewer.vue +2 -4
- package/src/viewers/TiffViewer.vue +4 -5
- package/src/viewers/Viewer3D.vue +1 -1
- package/LICENSE +0 -21
- package/dist/ArchiveViewer-CLNevfPE.js +0 -124
- package/dist/ArchiveViewer-CLNevfPE.js.map +0 -1
- package/dist/CsvViewer-K8XA3z7N.js +0 -141
- package/dist/CsvViewer-K8XA3z7N.js.map +0 -1
- package/dist/DrawioViewer-BuhZOAx6.js.map +0 -1
- package/dist/EpubViewer-DS3mb3k8.js.map +0 -1
- package/dist/IpynbViewer-BTk7p4Dn.js +0 -184
- package/dist/IpynbViewer-BTk7p4Dn.js.map +0 -1
- package/dist/MermaidViewer-DqucH_io.js +0 -137
- package/dist/MermaidViewer-DqucH_io.js.map +0 -1
- package/dist/PsdViewer-CwiVENmq.js +0 -122
- package/dist/PsdViewer-CwiVENmq.js.map +0 -1
- package/dist/TiffViewer-CpewSVmQ.js +0 -142
- package/dist/TiffViewer-CpewSVmQ.js.map +0 -1
- package/dist/Viewer3D-CIrOn63I.js.map +0 -1
- package/dist/index-Dx0IbBcI.js +0 -26831
- package/dist/index-Dx0IbBcI.js.map +0 -1
- package/dist/useViewerFetch-czqbd2Lj.js +0 -25
- package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* ListView —
|
|
3
|
+
* ListView — the explorer's listing: files, in the product's ONE table.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* ⚠⚠ Since 2026-09-21 the table itself is `DataTable`, extracted from this
|
|
6
|
+
* file so that every table in the product — admin pages, connection panels,
|
|
7
|
+
* an app's `list` node — runs the same code this listing does. The owner:
|
|
8
|
+
* "Artık explore tablomuz bizim her yerde kullanacağımız tablo yapısıdır."
|
|
9
|
+
* This file is the layer ON TOP of it that knows about files: the columns
|
|
10
|
+
* (Name · Type · Location · Owner · Modified · Size · ★), what each prints for
|
|
11
|
+
* a file, the row's gestures (open, select, drag, drop, touch), the date
|
|
12
|
+
* headings, and where the arrangement is remembered — per folder.
|
|
13
|
+
*
|
|
14
|
+
* Selection + context menu + double-click open are caller-owned — we just
|
|
15
|
+
* emit the events and let FileExplorer.vue handle them uniformly across List
|
|
16
|
+
* and Grid.
|
|
8
17
|
*/
|
|
9
|
-
import { computed,
|
|
18
|
+
import { computed, ref, watch } from 'vue';
|
|
10
19
|
import { hasInternalDrag } from '../lib/dragOut';
|
|
11
20
|
import type { FileNode } from '../types/FileNode';
|
|
12
21
|
import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
13
22
|
import { useLocale } from '../composables/useLocale';
|
|
23
|
+
import { inlineStartX } from '../lib/direction';
|
|
24
|
+
import { lockOf, lockWords } from '../lib/appLock';
|
|
25
|
+
import { linkWordsFor } from '../lib/symlink'; /* issue #34 — a link that will not open */
|
|
14
26
|
import { checkMod, clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
|
|
15
|
-
import ItemCheck from './ItemCheck.vue';
|
|
16
27
|
import {
|
|
17
28
|
arrivedFromOutside,
|
|
18
29
|
ownedByViewer,
|
|
@@ -27,6 +38,7 @@ import {
|
|
|
27
38
|
typeLabelFor,
|
|
28
39
|
} from '../lib/fileIcons'; /* gorunum:v1-preview · ikon:emoji */
|
|
29
40
|
import {
|
|
41
|
+
SORT_KEYS,
|
|
30
42
|
setSortLocale,
|
|
31
43
|
useSortStore,
|
|
32
44
|
type ListingOrder,
|
|
@@ -36,32 +48,22 @@ import { matchedInContent, snippetSegments } from '../lib/snippet'; /* bul:s3 */
|
|
|
36
48
|
import { drawsAsPage, previewKindFor } from '../lib/filePreview'; /* tablo:t1 */
|
|
37
49
|
import { applyDragGhost } from '../lib/dragGhost'; /* wiring:c4 */
|
|
38
50
|
import { parentDirOf } from '../lib/listing'; /* Location column — one split rule for every view */
|
|
51
|
+
import { trashTimeLeft } from '../lib/trashTimeLeft'; /* one "time left" sentence, shared with the admin Trash page */
|
|
39
52
|
import {
|
|
40
53
|
groupByDate,
|
|
41
54
|
groupingActive,
|
|
42
55
|
type DateRun,
|
|
43
56
|
} from '../lib/dateGroups'; /* zaman:z1 · gruplama */
|
|
44
57
|
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
columnsCustomised,
|
|
49
|
-
canMoveColumn,
|
|
50
|
-
columnOrder,
|
|
58
|
+
EXPLORER_METRICS,
|
|
59
|
+
applyToAllFolders,
|
|
60
|
+
folderColumnStore,
|
|
51
61
|
folderIsRemembered,
|
|
52
|
-
freezeWidths,
|
|
53
|
-
moveColumn,
|
|
54
|
-
moveColumnBy,
|
|
55
|
-
forgetAllFolders,
|
|
56
62
|
forgetFolder,
|
|
57
63
|
rememberedCount,
|
|
58
|
-
resetColumns,
|
|
59
|
-
setColumnHidden,
|
|
60
|
-
setColumnWidth,
|
|
61
|
-
tableLayout,
|
|
62
|
-
widthsAreAuto,
|
|
63
64
|
type ColumnId,
|
|
64
|
-
} from '../lib/viewPrefs'; /* tablo:t1 */
|
|
65
|
+
} from '../lib/viewPrefs'; /* tablo:t1 — per-folder columns */
|
|
66
|
+
import DataTable, { type DataColumn } from './DataTable.vue';
|
|
65
67
|
import StarButton from './StarButton.vue';
|
|
66
68
|
|
|
67
69
|
const props = defineProps<{
|
|
@@ -137,6 +139,19 @@ const props = defineProps<{
|
|
|
137
139
|
* every row would be worse than the glyph it replaced.
|
|
138
140
|
*/
|
|
139
141
|
thumbSrc?: (n: FileNode) => string | null;
|
|
142
|
+
/**
|
|
143
|
+
* These rows are the TRASH.
|
|
144
|
+
*
|
|
145
|
+
* ⚠ A deleted item's facts are not a file's facts (QA, 2026-09-21): its
|
|
146
|
+
* date column read "—" (a trashed row has no modification date to show),
|
|
147
|
+
* it said nothing about how long it had left or where it had been, and its
|
|
148
|
+
* Owner read "System" for every row because the trash listing carries no
|
|
149
|
+
* owner. So in the Trash the date is WHEN it was deleted, Location is where
|
|
150
|
+
* it was deleted FROM, a column says how long until it is gone for good, and
|
|
151
|
+
* Owner and the star are not drawn — the same facts the admin's Trash page
|
|
152
|
+
* shows, in the explorer's own table.
|
|
153
|
+
*/
|
|
154
|
+
trash?: boolean;
|
|
140
155
|
}>();
|
|
141
156
|
|
|
142
157
|
const emit = defineEmits<{
|
|
@@ -168,8 +183,28 @@ const {
|
|
|
168
183
|
zonedYearMonth,
|
|
169
184
|
toDate,
|
|
170
185
|
nodeDisplayName,
|
|
186
|
+
dir,
|
|
171
187
|
} = useLocale(() => props.locale);
|
|
172
188
|
|
|
189
|
+
|
|
190
|
+
/* App plugins — an app's hold on a row (docs/APP-PLUGINS-API.md → "File
|
|
191
|
+
locks"). The listing already caps `perm` at viewer, so the write verbs are
|
|
192
|
+
gone without this; the badge is what stops that reading as a bug. ⚠ One
|
|
193
|
+
source of words for every view and the details panel (lib/appLock). */
|
|
194
|
+
function lockTitleOf(n: FileNode): string {
|
|
195
|
+
return lockWords(lockOf(n), { t, formatDate, locale: props.locale });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* issue #34 — a symlink the server will not follow. Same shape as the lock
|
|
199
|
+
badge above and for the same reason: the words live in ONE module
|
|
200
|
+
(lib/symlink), so the row, the details panel and the toast that explains a
|
|
201
|
+
refused open cannot drift apart. `null` for every ordinary row — a link
|
|
202
|
+
whose target is inside the root was already followed and arrives as that
|
|
203
|
+
target, so it is not one of these. */
|
|
204
|
+
function linkOf(n: FileNode) {
|
|
205
|
+
return linkWordsFor(n, { t });
|
|
206
|
+
}
|
|
207
|
+
|
|
173
208
|
function isSelected(n: FileNode): boolean {
|
|
174
209
|
return props.selected.has(n.path);
|
|
175
210
|
}
|
|
@@ -205,7 +240,9 @@ function onRowMenu(n: FileNode, ev: MouseEvent) {
|
|
|
205
240
|
const el = ev.currentTarget as HTMLElement | null;
|
|
206
241
|
const box = el?.getBoundingClientRect();
|
|
207
242
|
emit('context-row', n, {
|
|
208
|
-
|
|
243
|
+
// ⚠ RTL: the button's START corner — its right one in RTL — so the menu
|
|
244
|
+
// hangs from the control and opens the way the line reads.
|
|
245
|
+
clientX: box ? inlineStartX(box, dir.value) : ev.clientX,
|
|
209
246
|
clientY: box ? box.bottom : ev.clientY,
|
|
210
247
|
preventDefault: () => {},
|
|
211
248
|
stopPropagation: () => {},
|
|
@@ -383,6 +420,17 @@ function locationLabel(n: FileNode): string {
|
|
|
383
420
|
return [store, dir].filter(Boolean).join('/');
|
|
384
421
|
}
|
|
385
422
|
|
|
423
|
+
/**
|
|
424
|
+
* The Trash's "time left": the server's own count of whole days before the
|
|
425
|
+
* item is purged (`ttl_days`, trash/service.go — retention minus the days
|
|
426
|
+
* since deletion, never below zero). No count, no claim.
|
|
427
|
+
*/
|
|
428
|
+
function remainingLabel(n: FileNode): string {
|
|
429
|
+
const meta = (n as Record<string, unknown>).extra_metadata as { ttl_days?: number | null } | undefined;
|
|
430
|
+
const ttl = meta?.ttl_days;
|
|
431
|
+
return trashTimeLeft(ttl, t);
|
|
432
|
+
}
|
|
433
|
+
|
|
386
434
|
/* bul:s3 — search-result enrichment. The v0.2 backend inlines `snippet`
|
|
387
435
|
* (plain text, «» highlights) + `matched` on search hits; regular listings
|
|
388
436
|
* never carry them, so presence-gating keeps normal rows untouched and an
|
|
@@ -395,41 +443,27 @@ function rowSnippet(n: FileNode): string {
|
|
|
395
443
|
function rowInContent(n: FileNode): boolean {
|
|
396
444
|
return matchedInContent((n as Record<string, unknown>).matched);
|
|
397
445
|
}
|
|
398
|
-
|
|
399
446
|
// ------------------------------------------------------------------
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
// ⚠⚠ NOT local to the list view any more. The key, the direction, the click
|
|
403
|
-
// cycle and the comparator all live in `lib/sortOrder`, the module-level
|
|
404
|
-
// singleton the filter row's sort control also drives — so these headers and
|
|
405
|
-
// that control are two handles on ONE piece of state rather than two sorts
|
|
406
|
-
// racing to re-order the same rows (filex lesson #67, one level up from the
|
|
407
|
-
// "folders first" copy it is about). The headers kept their behaviour: click
|
|
408
|
-
// a column to sort by it, click it again to reverse.
|
|
447
|
+
// Sorting.
|
|
409
448
|
//
|
|
410
|
-
// ⚠⚠
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
// describe — which is the thing that made `key: null` unworkable as a key.
|
|
449
|
+
// ⚠⚠ NOT local to the list view. The key, the direction, the click cycle and
|
|
450
|
+
// the comparator live in `lib/sortOrder`, the store of the PANE this list is
|
|
451
|
+
// inside, which the filter row's sort control also drives — so these headers
|
|
452
|
+
// and that control are two handles on ONE piece of state (filex lesson #67).
|
|
453
|
+
// The table draws the headers and reports a click; the store decides.
|
|
416
454
|
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
455
|
+
// ⚠⚠ `order === 'relevance'` says the server ranked this listing: no key is
|
|
456
|
+
// applied, no column claims to be sorting, and the headers CLOSE and say why.
|
|
457
|
+
// A header click over a ranked listing would set the pane's key, move nothing
|
|
458
|
+
// at all, and then re-order the FOLDER the person returns to — a control
|
|
459
|
+
// whose only effect is somewhere the user is not looking.
|
|
421
460
|
// ------------------------------------------------------------------
|
|
422
461
|
|
|
423
|
-
|
|
424
|
-
*
|
|
425
|
-
|
|
426
|
-
* Two panes of a split each draw a listing and each carry a sort control; one
|
|
427
|
-
* shared answer means the control in the right pane re-orders the left one.
|
|
428
|
-
* With no provider (a bare <ListView>, a host with one listing) this is the
|
|
429
|
-
* default store, i.e. exactly the old behaviour. */
|
|
462
|
+
/* ⚠⚠ The store of the PANE this list is inside, not a module-level singleton:
|
|
463
|
+
* two panes of a split each carry a sort control, and one shared answer means
|
|
464
|
+
* the control in the right pane re-orders the left one. */
|
|
430
465
|
const sort = useSortStore();
|
|
431
466
|
const sortKey = computed<SortKey>(() => sort.key.value);
|
|
432
|
-
const sortDir = computed(() => sort.dir.value);
|
|
433
467
|
/** These rows are the server's ranked answer — see the `order` prop. */
|
|
434
468
|
const ranked = computed(() => props.order === 'relevance');
|
|
435
469
|
|
|
@@ -437,62 +471,38 @@ const ranked = computed(() => props.order === 'relevance');
|
|
|
437
471
|
* the catalogue that word comes from. */
|
|
438
472
|
watch(() => props.locale, (l) => setSortLocale(l), { immediate: true });
|
|
439
473
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
* click dispatched in script still runs this listener — and an embedder is
|
|
444
|
-
* free to restyle or re-render these headers. The consequence of letting one
|
|
445
|
-
* through is invisible and delayed: nothing moves on screen (the listing is
|
|
446
|
-
* ranked), the shared key is written to `filex.list-sort`, and the FOLDER
|
|
447
|
-
* the person returns to comes back in an order they never asked for.
|
|
448
|
-
* Measured 2026-09-13: a scripted click on the closed Size header left the
|
|
449
|
-
* rows untouched and still turned the stored sort from Name ↑ into Size ↑. */
|
|
450
|
-
if (ranked.value) return;
|
|
451
|
-
sort.chooseSortKey(key);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/** What a column header promises. Over a ranked listing the headers are
|
|
455
|
-
* closed, so they say WHY rather than going quietly grey — the same sentence
|
|
456
|
-
* the filter row's sort button carries, because it is the same fact. */
|
|
457
|
-
const sortTitle = computed(() => t(ranked.value ? 'sort.relevance_why' : 'col.sort'));
|
|
474
|
+
const tableSort = computed(() =>
|
|
475
|
+
ranked.value ? null : { key: sortKey.value as string, dir: sort.dir.value },
|
|
476
|
+
);
|
|
458
477
|
|
|
459
|
-
function
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
478
|
+
function onSort(p: { key: string; dir: 'asc' | 'desc' }) {
|
|
479
|
+
/* The table computed a direction too, but the STORE owns the click
|
|
480
|
+
vocabulary (`chooseSortKey`: a new key arrives in its own default
|
|
481
|
+
direction, the active one flips) — handing it the key keeps one answer. */
|
|
482
|
+
if (ranked.value) return;
|
|
483
|
+
if ((SORT_KEYS as readonly string[]).includes(p.key)) sort.chooseSortKey(p.key as SortKey);
|
|
464
484
|
}
|
|
465
485
|
|
|
466
486
|
// ------------------------------------------------------------------
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
// ⚠⚠ THE LAYOUT IS COMPUTED HERE, IN JAVASCRIPT, and applied as inline widths
|
|
470
|
-
// — it is not a stylesheet rule. It used to be three places at once (a base
|
|
471
|
-
// template in `base.css`, a `.fe--narrow` override restating it and a
|
|
472
|
-
// `@media (max-width: 640px)` rule restating it again, each with its own list
|
|
473
|
-
// of cells to `display: none`), and they disagreed: the media query answers
|
|
474
|
-
// the VIEWPORT while `.fe--narrow` answers the CONTAINER, so an embed 500px
|
|
475
|
-
// wide on a 1400px screen got one rule's hiding and the other rule's tracks.
|
|
487
|
+
// The columns.
|
|
476
488
|
//
|
|
477
|
-
// ⚠⚠
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
// is more than it has, the ⋮ stays pinned to the right edge, and a table
|
|
485
|
-
// narrower than its pane sits at the LEFT with the slack after it.
|
|
489
|
+
// ⚠⚠ THE TABLE ITSELF IS `DataTable` — the product's one table, which this
|
|
490
|
+
// view used to BE. Everything that is not about files moved there on
|
|
491
|
+
// 2026-09-21 (resizing, reordering, the column menu, the frozen edges, the
|
|
492
|
+
// sideways scroll) so that every other table in the product runs the same
|
|
493
|
+
// code rather than an imitation of it. What stays here is what only a file
|
|
494
|
+
// listing knows: which columns exist, what a cell prints for a file, the row's
|
|
495
|
+
// gestures, and where the arrangement is remembered — per FOLDER.
|
|
486
496
|
// ------------------------------------------------------------------
|
|
487
497
|
|
|
488
498
|
/** Which optional columns this listing is willing to draw at all, before any
|
|
489
|
-
* question of
|
|
499
|
+
* question of the person's own choices. */
|
|
490
500
|
const candidateCols = computed<ColumnId[]>(() => {
|
|
501
|
+
if (props.trash) return ['type', 'location', 'modified', 'remaining', 'size'];
|
|
491
502
|
const out: ColumnId[] = ['type'];
|
|
492
|
-
/* Location only where the rows come from more than one folder
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
same string. */
|
|
503
|
+
/* Location only where the rows come from more than one folder. In an
|
|
504
|
+
ordinary folder every row shares one location and the column would be a
|
|
505
|
+
wall of the same string. */
|
|
496
506
|
if (props.showParentPath) out.push('location');
|
|
497
507
|
out.push('owner', 'modified', 'size');
|
|
498
508
|
if (props.starEnabled !== false && (!!props.apiBase || props.apiBase === '')) out.push('star');
|
|
@@ -500,138 +510,76 @@ const candidateCols = computed<ColumnId[]>(() => {
|
|
|
500
510
|
});
|
|
501
511
|
|
|
502
512
|
/**
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
* the case the old media query could not see. It is also the SCROLLPORT's
|
|
508
|
-
* width — `.fe-list` is the scrolling box now, so `contentRect` already has
|
|
509
|
-
* the vertical scrollbar taken out of it and the auto widths are computed
|
|
510
|
-
* against the room a row really has. 0 until the first observation, which
|
|
511
|
-
* `tableLayout` reads as "keep the shipped widths" so the first paint is not a
|
|
512
|
-
* guess that then jumps.
|
|
513
|
-
*/
|
|
514
|
-
const listEl = ref<HTMLElement | null>(null);
|
|
515
|
-
const listWidth = ref(0);
|
|
516
|
-
let ro: ResizeObserver | null = null;
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Is the table scrolled off its left edge?
|
|
513
|
+
* THIS FOLDER's columns (`lib/viewPrefs.folderColumnStore`): its own, else the
|
|
514
|
+
* person's default, else the instance's, else the shipped layout. A column
|
|
515
|
+
* dragged wider here stays here — the owner, 2026-09-21: "Explore içindeki
|
|
516
|
+
* değişikliklerimiz o klasör özelinde olmalı."
|
|
520
517
|
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
* table that fits.
|
|
518
|
+
* ⚠ Built once, reading `folderKey` through a getter, so a navigation swaps
|
|
519
|
+
* the arrangement on screen without rebuilding the store.
|
|
524
520
|
*/
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (!listEl.value || typeof ResizeObserver === 'undefined') return;
|
|
535
|
-
ro = new ResizeObserver((entries) => {
|
|
536
|
-
const w = entries[0]?.contentRect?.width ?? 0;
|
|
537
|
-
if (Math.abs(w - listWidth.value) >= 1) listWidth.value = w;
|
|
538
|
-
});
|
|
539
|
-
ro.observe(listEl.value);
|
|
540
|
-
});
|
|
541
|
-
onBeforeUnmount(() => {
|
|
542
|
-
ro?.disconnect();
|
|
543
|
-
ro = null;
|
|
544
|
-
window.removeEventListener('pointermove', onResizeMove);
|
|
545
|
-
window.removeEventListener('pointerup', onResizeEnd);
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
const layout = computed(() => tableLayout(listWidth.value, candidateCols.value));
|
|
549
|
-
const visibleCols = computed<ColumnId[]>(() => layout.value.visible);
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* ONE object, bound to the header and to every row.
|
|
553
|
-
*
|
|
554
|
-
* ⚠⚠ The header has to be exactly as wide as the rows or it drifts out of line
|
|
555
|
-
* with its own columns the moment the table is scrolled sideways — which is
|
|
556
|
-
* worse than having no header at all, because it labels the wrong values.
|
|
557
|
-
* `min-width: 100%` in the stylesheet is what keeps a NARROW table's rows
|
|
558
|
-
* spanning the pane (so hover and selection do not stop halfway across) while
|
|
559
|
-
* the cells inside them still pack left.
|
|
560
|
-
*/
|
|
561
|
-
const tableStyle = computed(() => ({ width: `${layout.value.total}px` }));
|
|
562
|
-
|
|
563
|
-
/** A drawn track's width, as the inline style the cell carries. `flex-basis`
|
|
564
|
-
* rather than a grid track, because the pinned ⋮ has to be able to move
|
|
565
|
-
* within the row — a sticky GRID item is confined to its own grid area and
|
|
566
|
-
* therefore cannot move at all. */
|
|
567
|
-
function colStyle(id: ColumnId) {
|
|
568
|
-
return { flex: `0 0 ${layout.value.widths[id] ?? columnWidth(id)}px` };
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/** A column's cell class — the existing `fe-list__col--*` names, so every
|
|
572
|
-
* style already written against them keeps applying. */
|
|
573
|
-
function colClass(id: ColumnId): string {
|
|
574
|
-
return `fe-list__col fe-list__col--${id === 'modified' ? 'mod' : id}`;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
/** The four keys a header can sort by, keyed by column. Location and the star
|
|
578
|
-
* are not sorts: there is no `SortKey` for either, and inventing one would put
|
|
579
|
-
* a word in the Sort-by menu that means nothing in nine listings out of ten. */
|
|
580
|
-
const SORT_OF: Partial<Record<ColumnId, SortKey>> = {
|
|
581
|
-
type: 'type',
|
|
582
|
-
modified: 'modified',
|
|
583
|
-
size: 'size',
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
const COL_LABEL: Record<ColumnId, string> = {
|
|
587
|
-
name: 'col.name',
|
|
588
|
-
type: 'col.type',
|
|
589
|
-
location: 'col.location',
|
|
590
|
-
owner: 'col.owner',
|
|
591
|
-
modified: 'col.modified',
|
|
592
|
-
size: 'col.size',
|
|
593
|
-
star: 'col.star',
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
function colLabel(id: ColumnId): string {
|
|
597
|
-
return t(COL_LABEL[id]);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/** What the cell prints for this row. One switch, so the head's order and the
|
|
601
|
-
* body's values are driven by the same list. */
|
|
602
|
-
function cellText(id: ColumnId, n: FileNode): string {
|
|
603
|
-
switch (id) {
|
|
604
|
-
case 'type':
|
|
605
|
-
return typeLabel(n);
|
|
606
|
-
case 'location':
|
|
607
|
-
return locationLabel(n);
|
|
608
|
-
case 'owner':
|
|
609
|
-
return ownerLabel(n);
|
|
610
|
-
case 'modified':
|
|
611
|
-
return displayDate(n.last_modified);
|
|
612
|
-
case 'size':
|
|
613
|
-
return formatSize(n.size);
|
|
614
|
-
default:
|
|
615
|
-
return '';
|
|
616
|
-
}
|
|
617
|
-
}
|
|
521
|
+
const cols = folderColumnStore(() => props.folderKey ?? '');
|
|
522
|
+
|
|
523
|
+
/** The optional columns on screen, in the person's order — the same list the
|
|
524
|
+
* table draws (width never sheds a column). */
|
|
525
|
+
const visibleCols = computed<ColumnId[]>(() =>
|
|
526
|
+
cols
|
|
527
|
+
.order()
|
|
528
|
+
.filter((id) => id !== 'name' && candidateCols.value.includes(id) && !cols.hidden(id)),
|
|
529
|
+
);
|
|
618
530
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
531
|
+
/** The column definitions the table draws, in the explorer's own classes so
|
|
532
|
+
* every rule written against `fe-list__col--*` keeps applying. */
|
|
533
|
+
const columns = computed<DataColumn<FileNode>[]>(() => [
|
|
534
|
+
{ id: 'name', label: t('col.name'), lead: true, sortable: true, class: 'fe-list__col--name' },
|
|
535
|
+
{ id: 'type', label: t('col.type'), sortable: true, class: 'fe-list__col--type', format: typeLabel },
|
|
536
|
+
{
|
|
537
|
+
id: 'location',
|
|
538
|
+
label: props.trash ? t('col.deleted_from') : t('col.location'),
|
|
539
|
+
class: 'fe-list__col--location',
|
|
540
|
+
format: locationLabel,
|
|
541
|
+
title: (n) => locationLabel(n) || undefined,
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
id: 'owner',
|
|
545
|
+
label: t('col.owner'),
|
|
546
|
+
class: 'fe-list__col--owner',
|
|
547
|
+
format: ownerLabel,
|
|
548
|
+
title: ownerTitle,
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
id: 'modified',
|
|
552
|
+
label: props.trash ? t('col.deleted') : t('col.modified'),
|
|
553
|
+
sortable: true,
|
|
554
|
+
sortDir: 'desc',
|
|
555
|
+
class: 'fe-list__col--mod',
|
|
556
|
+
format: (n) => displayDate(n.last_modified),
|
|
557
|
+
title: (n) => fullDate(n.last_modified),
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
id: 'remaining',
|
|
561
|
+
label: t('col.remaining'),
|
|
562
|
+
class: 'fe-list__col--remaining',
|
|
563
|
+
format: remainingLabel,
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
id: 'size',
|
|
567
|
+
label: t('col.size'),
|
|
568
|
+
sortable: true,
|
|
569
|
+
class: 'fe-list__col--size',
|
|
570
|
+
format: (n) => formatSize(n.size),
|
|
571
|
+
title: () => undefined,
|
|
572
|
+
},
|
|
573
|
+
{ id: 'star', label: t('col.star'), class: 'fe-list__col--star', headerLabel: false },
|
|
574
|
+
]);
|
|
625
575
|
|
|
626
576
|
/**
|
|
627
577
|
* The name cell's hover text.
|
|
628
578
|
*
|
|
629
|
-
* ⚠ Carries the location too when the Location column is not drawn
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
* must not silently destroy the only copy of a fact, or `report.pdf` in two
|
|
634
|
-
* folders is two identical rows.
|
|
579
|
+
* ⚠ Carries the location too when the Location column is not drawn (only when
|
|
580
|
+
* the person has turned it off): a column somebody hid must not silently
|
|
581
|
+
* destroy the only copy of a fact, or `report.pdf` in two folders is two
|
|
582
|
+
* identical rows.
|
|
635
583
|
*/
|
|
636
584
|
function nameTitle(n: FileNode): string {
|
|
637
585
|
if (!props.showParentPath || visibleCols.value.includes('location')) return n.basename;
|
|
@@ -639,348 +587,50 @@ function nameTitle(n: FileNode): string {
|
|
|
639
587
|
return loc ? `${n.basename} · ${loc}` : n.basename;
|
|
640
588
|
}
|
|
641
589
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
* ⚠⚠ NAME HAS A HANDLE NOW, and the note that stood here said the opposite:
|
|
650
|
-
* "there is deliberately NO handle on Name… it is whatever the fixed columns
|
|
651
|
-
* leave, so dragging Name can only mean dragging everything else." That was
|
|
652
|
-
* true of the flexible `minmax(220px, 1fr)` track, and the track is what the
|
|
653
|
-
* owner rejected: "küçültme yapınca name kısmı büyüyormuş gibi davranıyor — bu
|
|
654
|
-
* davranış yanlış, istersem name'i de kısabilir olmalıyım." Name is an
|
|
655
|
-
* ordinary column with a width of its own, so its handle resizes the column it
|
|
656
|
-
* sits on, like every other.
|
|
657
|
-
*/
|
|
658
|
-
let resizing: { id: ColumnId; startX: number; startW: number } | null = null;
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* THE FIRST DRAG FREEZES THE WHOLE ROW.
|
|
662
|
-
*
|
|
663
|
-
* ⚠⚠ Not just the column under the pointer. Until somebody sizes a column the
|
|
664
|
-
* widths are derived from the pane (`tableLayout`, auto), and auto Name takes
|
|
665
|
-
* whatever slack is going — so narrowing Size while Name is still auto would
|
|
666
|
-
* widen Name, which is precisely the behaviour being fixed. Committing every
|
|
667
|
-
* drawn width the instant a gesture starts turns the drag into what it looks
|
|
668
|
-
* like, and is also what makes "I have never touched this" distinguishable
|
|
669
|
-
* from "I deliberately made Name narrow" forever after (`widthsAreAuto`).
|
|
670
|
-
*/
|
|
671
|
-
function freezeBeforeEdit() {
|
|
672
|
-
if (!widthsAreAuto()) return;
|
|
673
|
-
freezeWidths(layout.value.widths);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function onResizeStart(id: ColumnId, ev: PointerEvent) {
|
|
677
|
-
if (!isResizable(id)) return;
|
|
678
|
-
ev.preventDefault();
|
|
679
|
-
ev.stopPropagation();
|
|
680
|
-
freezeBeforeEdit();
|
|
681
|
-
resizing = { id, startX: ev.clientX, startW: columnWidth(id) };
|
|
682
|
-
/* ⚠ On the WINDOW, not on the handle with setPointerCapture: the pointer
|
|
683
|
-
* leaves the 9px handle within the first few pixels of any real drag, and a
|
|
684
|
-
* capture on an element Vue may re-render mid-drag (the template width is
|
|
685
|
-
* reactive and changes on every move) is released the moment that element is
|
|
686
|
-
* patched — the column would stop following the cursor halfway. */
|
|
687
|
-
window.addEventListener('pointermove', onResizeMove);
|
|
688
|
-
window.addEventListener('pointerup', onResizeEnd);
|
|
590
|
+
function rowAttrs(n: FileNode): Record<string, unknown> {
|
|
591
|
+
return {
|
|
592
|
+
tabindex: 0,
|
|
593
|
+
'aria-label': nodeDisplayName(n) /* wiring:c4 */,
|
|
594
|
+
'data-fe-path': n.path /* wiring:d1 — middle-click open-in-new-tab delegation */,
|
|
595
|
+
draggable: 'true',
|
|
596
|
+
};
|
|
689
597
|
}
|
|
690
598
|
|
|
691
|
-
function
|
|
692
|
-
|
|
693
|
-
|
|
599
|
+
function rowClass(n: FileNode) {
|
|
600
|
+
return {
|
|
601
|
+
'is-dir': n.type === 'dir',
|
|
602
|
+
'is-trash': !!n.trashed,
|
|
603
|
+
'is-clipped': !!props.clipped?.has(n.path),
|
|
604
|
+
'is-droptarget': dropTargetPath.value === n.path /* wiring:c4 */,
|
|
605
|
+
};
|
|
694
606
|
}
|
|
695
607
|
|
|
696
|
-
|
|
697
|
-
resizing = null;
|
|
698
|
-
window.removeEventListener('pointermove', onResizeMove);
|
|
699
|
-
window.removeEventListener('pointerup', onResizeEnd);
|
|
700
|
-
}
|
|
608
|
+
// ── the column menu's folder rows ─────────────────────────────────────
|
|
701
609
|
|
|
702
|
-
/** Keyboard resizing — the handle is focusable, so it has to do something.
|
|
703
|
-
* 16px a step, which is about one character of the 13px body text. */
|
|
704
|
-
function onResizeKey(id: ColumnId, ev: KeyboardEvent) {
|
|
705
|
-
const step = ev.key === 'ArrowLeft' ? -16 : ev.key === 'ArrowRight' ? 16 : 0;
|
|
706
|
-
if (!step) return;
|
|
707
|
-
ev.preventDefault();
|
|
708
|
-
freezeBeforeEdit();
|
|
709
|
-
setColumnWidth(id, columnWidth(id) + step);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
/** Double-click a handle: back to this column's shipped width. "Size to fit"
|
|
713
|
-
* would mean measuring every cell in a listing that may be thousands of rows
|
|
714
|
-
* long, and the honest cheap answer is the default. */
|
|
715
|
-
function onResizeReset(id: ColumnId) {
|
|
716
|
-
const spec = COLUMNS.find((c) => c.id === id);
|
|
717
|
-
if (!spec) return;
|
|
718
|
-
freezeBeforeEdit();
|
|
719
|
-
setColumnWidth(id, spec.width);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// ── reordering ────────────────────────────────────────────
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* DRAG A HEADER TO MOVE ITS COLUMN — the owner's third ask for this table:
|
|
726
|
-
* "ayrıca yerlerini falan değiştirebilir olayım ya o sütunların."
|
|
727
|
-
*
|
|
728
|
-
* ⚠⚠ The threshold is the whole difficulty. Clicking a header sorts and
|
|
729
|
-
* dragging it moves, and both start with the same pointerdown — so a reorder
|
|
730
|
-
* that begins on the first pixel of movement turns every sort click into an
|
|
731
|
-
* accidental two-pixel drag. Nothing happens here until the pointer has
|
|
732
|
-
* travelled `DRAG_SLOP`; below that the gesture stays a click and the sort
|
|
733
|
-
* button gets it, untouched. Above it, the click the browser fires on
|
|
734
|
-
* pointerup is swallowed on its way past (`onHeadClickCapture`), or every
|
|
735
|
-
* completed drag would also re-sort the column it had just moved.
|
|
736
|
-
*
|
|
737
|
-
* ⚠ Name is not in this: it is pinned first and nothing may be dropped before
|
|
738
|
-
* it. It carries the tick, the icon and the row's click target, which is why
|
|
739
|
-
* every file manager pins it — and `columnOrder` enforces it, so a document
|
|
740
|
-
* that claims otherwise cannot put a column ahead of the one the row is read
|
|
741
|
-
* by. Its WIDTH is the person's, though; only its place is fixed.
|
|
742
|
-
*/
|
|
743
|
-
const DRAG_SLOP = 5;
|
|
744
|
-
let dragState: { id: ColumnId; startX: number; armed: boolean } | null = null;
|
|
745
|
-
/** The column being dragged, once the slop is cleared. */
|
|
746
|
-
const dragCol = ref<ColumnId | null>(null);
|
|
747
|
-
/** Where it would land: an index into the drawn columns. */
|
|
748
|
-
const dropAt = ref<number | null>(null);
|
|
749
|
-
/**
|
|
750
|
-
* When a real drag last ended — the one thing standing between "I moved a
|
|
751
|
-
* column" and "I moved a column and also re-sorted it".
|
|
752
|
-
*
|
|
753
|
-
* ⚠⚠ A TIMESTAMP and not a boolean. A flag is armed on pointerup and
|
|
754
|
-
* disarmed by the click that follows — except when no click follows, which
|
|
755
|
-
* happens whenever the pointer went down on one header and up on another and
|
|
756
|
-
* the browser found no common ancestor to fire on. The flag then sits armed
|
|
757
|
-
* and eats the NEXT header click the person makes, which is a sort that
|
|
758
|
-
* silently does nothing minutes later. A window bounds the damage to the
|
|
759
|
-
* gesture it belongs to. Measured 2026-09-13: a sort click after a reorder
|
|
760
|
-
* drag did nothing at all.
|
|
761
|
-
*/
|
|
762
|
-
let dragEndedAt = 0;
|
|
763
|
-
const CLICK_SWALLOW_MS = 300;
|
|
764
|
-
/** Set by Escape, cleared by the pointerup it pre-empts. */
|
|
765
|
-
let cancelled = false;
|
|
766
|
-
|
|
767
|
-
function isMovable(id: ColumnId): boolean {
|
|
768
|
-
return COLUMNS.find((c) => c.id === id)?.hideable === true;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
function onHeadPointerDown(id: ColumnId, ev: PointerEvent) {
|
|
772
|
-
if (ev.button !== 0 || !isMovable(id)) return;
|
|
773
|
-
dragState = { id, startX: ev.clientX, armed: false };
|
|
774
|
-
window.addEventListener('pointermove', onHeadPointerMove);
|
|
775
|
-
window.addEventListener('pointerup', onHeadPointerUp);
|
|
776
|
-
window.addEventListener('keydown', onDragKey, true);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function onHeadPointerMove(ev: PointerEvent) {
|
|
780
|
-
if (!dragState) return;
|
|
781
|
-
if (!dragState.armed) {
|
|
782
|
-
if (Math.abs(ev.clientX - dragState.startX) < DRAG_SLOP) return;
|
|
783
|
-
dragState.armed = true;
|
|
784
|
-
dragCol.value = dragState.id;
|
|
785
|
-
}
|
|
786
|
-
/* Where the pointer is, in terms of the drawn header cells. The insertion
|
|
787
|
-
* point is the gap nearest the cursor, and it is ONE number — the marker
|
|
788
|
-
* draws it and the drop applies it, so the picture cannot promise a landing
|
|
789
|
-
* the drop does not honour. */
|
|
790
|
-
const head = listEl.value?.querySelector('.fe-list__head');
|
|
791
|
-
if (!head) return;
|
|
792
|
-
const cells = [...head.querySelectorAll('.fe-list__col')] as HTMLElement[];
|
|
793
|
-
/* The first two cells are the tick and Name, the last is the row menu; only
|
|
794
|
-
the ones between are positions a column can take. */
|
|
795
|
-
const movable = cells.slice(2, 2 + visibleCols.value.length);
|
|
796
|
-
let idx = movable.length;
|
|
797
|
-
for (let i = 0; i < movable.length; i++) {
|
|
798
|
-
const box = movable[i].getBoundingClientRect();
|
|
799
|
-
if (ev.clientX < box.left + box.width / 2) {
|
|
800
|
-
idx = i;
|
|
801
|
-
break;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
dropAt.value = idx;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function onHeadPointerUp() {
|
|
808
|
-
const st = dragState;
|
|
809
|
-
const target = dropAt.value;
|
|
810
|
-
endDrag();
|
|
811
|
-
/* ⚠ `cancelled` is checked as well as `armed`: Escape clears `dragState`,
|
|
812
|
-
* but a pointerup already queued behind it must not be able to complete the
|
|
813
|
-
* drop the person just abandoned. */
|
|
814
|
-
if (!st?.armed || cancelled) {
|
|
815
|
-
cancelled = false;
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
dragEndedAt = Date.now();
|
|
819
|
-
if (target == null) return;
|
|
820
|
-
/* Translate a position among the DRAWN columns into a position in the full
|
|
821
|
-
* stored order: a column shed for want of room still has a place in the
|
|
822
|
-
* sequence, and dropping between two visible columns must not silently
|
|
823
|
-
* reshuffle the one hiding between them. */
|
|
824
|
-
const drawn = visibleCols.value.filter((id) => id !== st.id);
|
|
825
|
-
const before = drawn[Math.min(target, drawn.length)];
|
|
826
|
-
const full = columnOrder();
|
|
827
|
-
moveColumn(st.id, before ? full.indexOf(before) : full.length);
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function onDragKey(ev: KeyboardEvent) {
|
|
831
|
-
if (ev.key !== 'Escape') return;
|
|
832
|
-
/* Cancel: the drop is abandoned and nothing is written. The click is still
|
|
833
|
-
* swallowed, because the pointer is mid-gesture and releasing it over a
|
|
834
|
-
* header would otherwise sort the very column the person was rescuing. */
|
|
835
|
-
ev.stopPropagation();
|
|
836
|
-
const armed = dragState?.armed === true;
|
|
837
|
-
endDrag();
|
|
838
|
-
if (armed) {
|
|
839
|
-
cancelled = true;
|
|
840
|
-
dragEndedAt = Date.now();
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function endDrag() {
|
|
845
|
-
dragState = null;
|
|
846
|
-
dragCol.value = null;
|
|
847
|
-
dropAt.value = null;
|
|
848
|
-
window.removeEventListener('pointermove', onHeadPointerMove);
|
|
849
|
-
window.removeEventListener('pointerup', onHeadPointerUp);
|
|
850
|
-
window.removeEventListener('keydown', onDragKey, true);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
/** Capture phase, on the header row: a click that ended a drag never reaches
|
|
854
|
-
* the sort button underneath it. */
|
|
855
|
-
function onHeadClickCapture(ev: MouseEvent) {
|
|
856
|
-
if (Date.now() - dragEndedAt > CLICK_SWALLOW_MS) return;
|
|
857
|
-
dragEndedAt = 0;
|
|
858
|
-
ev.preventDefault();
|
|
859
|
-
ev.stopPropagation();
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// ── the column menu ───────────────────────────────────────────────────
|
|
863
|
-
|
|
864
|
-
/**
|
|
865
|
-
* A small settings popover, opened by the header's own ⋮ or by right-clicking
|
|
866
|
-
* the header — the two gestures the owner named.
|
|
867
|
-
*
|
|
868
|
-
* ⚠ Not `ContextMenu`, and that is not a second menu in the sense filex lesson
|
|
869
|
-
* #67 warns about: nothing here is an action on a file, so there is no action
|
|
870
|
-
* list to drift out of step with the row menu's. It is a panel of checkboxes,
|
|
871
|
-
* which a menu cannot be — a menu closes on every pick, so turning four
|
|
872
|
-
* columns off would be four separate right-clicks.
|
|
873
|
-
*/
|
|
874
|
-
const colMenuOpen = ref(false);
|
|
875
|
-
const colMenuX = ref(0);
|
|
876
|
-
const colMenuY = ref(0);
|
|
877
|
-
const colMenuEl = ref<HTMLElement | null>(null);
|
|
878
|
-
|
|
879
|
-
const prefersDark = ref(false);
|
|
880
|
-
onMounted(() => {
|
|
881
|
-
try {
|
|
882
|
-
prefersDark.value = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
|
|
883
|
-
} catch {
|
|
884
|
-
/* jsdom / no matchMedia */
|
|
885
|
-
}
|
|
886
|
-
});
|
|
887
|
-
const colMenuThemeClass = computed(
|
|
888
|
-
() => `fe-ctx-backdrop--theme-${props.theme || 'auto'}`,
|
|
889
|
-
);
|
|
890
|
-
|
|
891
|
-
/** Everything the person may toggle here: the candidates, minus the ones that
|
|
892
|
-
* are not theirs to hide (the star follows the Starred view's own rule). */
|
|
893
|
-
const menuCols = computed(() =>
|
|
894
|
-
/* In the person's OWN order, so this list reads as the list on screen — a
|
|
895
|
-
settings panel that disagrees with the thing it configures is how someone
|
|
896
|
-
ends up nudging the wrong column left. */
|
|
897
|
-
columnOrder()
|
|
898
|
-
.filter((id) => COLUMNS.find((c) => c.id === id)?.hideable && candidateCols.value.includes(id))
|
|
899
|
-
/* ⚠ No `squeezed` flag any more, and no "no room" note beside a ticked
|
|
900
|
-
row. It existed because a column could be ON and still not drawn, for
|
|
901
|
-
want of width; nothing is dropped for width now, so a ticked column is
|
|
902
|
-
on screen — possibly past the right edge, where the table scrolls to
|
|
903
|
-
reach it. A note that can no longer be true is worse than no note. */
|
|
904
|
-
.map((id) => ({
|
|
905
|
-
id,
|
|
906
|
-
label: colLabel(id),
|
|
907
|
-
on: !columnHidden(id),
|
|
908
|
-
canLeft: canMoveColumn(id, -1),
|
|
909
|
-
canRight: canMoveColumn(id, 1),
|
|
910
|
-
})),
|
|
911
|
-
);
|
|
912
|
-
|
|
913
|
-
/** The same move the drag performs, reachable from the keyboard. A reorder
|
|
914
|
-
* only a mouse can do is a reorder half the people using this cannot do. */
|
|
915
|
-
function nudgeCol(id: ColumnId, delta: -1 | 1) {
|
|
916
|
-
moveColumnBy(id, delta);
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
const canResetCols = computed(() => columnsCustomised());
|
|
920
610
|
const rememberedFolders = computed(() => rememberedCount());
|
|
921
611
|
const thisFolderRemembered = computed(() => !!props.folderKey && folderIsRemembered(props.folderKey));
|
|
922
612
|
|
|
923
|
-
async function openColMenu(ev: MouseEvent) {
|
|
924
|
-
ev.preventDefault();
|
|
925
|
-
ev.stopPropagation();
|
|
926
|
-
const el = ev.currentTarget as HTMLElement | null;
|
|
927
|
-
const box = el?.getBoundingClientRect();
|
|
928
|
-
/* Anchored to the BUTTON's corner when it came from the ⋮, to the pointer
|
|
929
|
-
* when it came from a right-click — the two gestures mean different things
|
|
930
|
-
* about where the person is looking. */
|
|
931
|
-
colMenuX.value = ev.clientX || (box ? box.left : 0);
|
|
932
|
-
colMenuY.value = ev.clientY || (box ? box.bottom : 0);
|
|
933
|
-
colMenuOpen.value = true;
|
|
934
|
-
await nextTick();
|
|
935
|
-
clampColMenu();
|
|
936
|
-
colMenuEl.value?.focus();
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
/** Keep it on screen. Opened from a header at the right edge it would
|
|
940
|
-
* otherwise hang off the window with no way to reach the last row. */
|
|
941
|
-
function clampColMenu() {
|
|
942
|
-
const el = colMenuEl.value;
|
|
943
|
-
if (!el) return;
|
|
944
|
-
const box = el.getBoundingClientRect();
|
|
945
|
-
const pad = 8;
|
|
946
|
-
if (box.right > window.innerWidth - pad) {
|
|
947
|
-
colMenuX.value = Math.max(pad, window.innerWidth - pad - box.width);
|
|
948
|
-
}
|
|
949
|
-
if (box.bottom > window.innerHeight - pad) {
|
|
950
|
-
colMenuY.value = Math.max(pad, window.innerHeight - pad - box.height);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
function closeColMenu() {
|
|
955
|
-
colMenuOpen.value = false;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function toggleCol(id: ColumnId) {
|
|
959
|
-
setColumnHidden(id, !columnHidden(id));
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
function onResetColumns() {
|
|
963
|
-
resetColumns();
|
|
964
|
-
}
|
|
965
|
-
|
|
966
613
|
/**
|
|
967
|
-
* "Apply to all folders" — the
|
|
614
|
+
* "Apply to all folders" — makes THIS folder's setup the person's default and
|
|
615
|
+
* forgets every folder's own, which is what the words promise.
|
|
968
616
|
*
|
|
969
|
-
* ⚠ It
|
|
970
|
-
*
|
|
971
|
-
* the
|
|
972
|
-
* `
|
|
973
|
-
* answer standing — the one on screen. That property is the reason the rule
|
|
974
|
-
* fits in a sentence.
|
|
617
|
+
* ⚠ It has to WRITE the default. It used to be a pure forget, because every
|
|
618
|
+
* click already wrote a global default — and that implicit write was the leak
|
|
619
|
+
* the owner reported ("tüm klasörlerde görünüm değişikliği geçerli oluyor").
|
|
620
|
+
* The view mode is `list` because this menu only exists in the list.
|
|
975
621
|
*/
|
|
976
|
-
function onApplyToAll() {
|
|
977
|
-
|
|
978
|
-
|
|
622
|
+
function onApplyToAll(close: () => void) {
|
|
623
|
+
applyToAllFolders({
|
|
624
|
+
v: 'list',
|
|
625
|
+
...(ranked.value ? {} : { k: sort.key.value, d: sort.dir.value }),
|
|
626
|
+
c: cols.state(),
|
|
627
|
+
});
|
|
628
|
+
close();
|
|
979
629
|
}
|
|
980
630
|
|
|
981
|
-
function onForgetFolder() {
|
|
631
|
+
function onForgetFolder(close: () => void) {
|
|
982
632
|
if (props.folderKey) forgetFolder(props.folderKey);
|
|
983
|
-
|
|
633
|
+
close();
|
|
984
634
|
}
|
|
985
635
|
|
|
986
636
|
const rows = computed(() => {
|
|
@@ -1076,364 +726,180 @@ const segments = computed<DateRun<FileNode>[]>(() =>
|
|
|
1076
726
|
labels: groupLabels.value,
|
|
1077
727
|
}).runs,
|
|
1078
728
|
);
|
|
729
|
+
/** The date runs, in the shape the table draws groups in. */
|
|
730
|
+
const tableGroups = computed(() =>
|
|
731
|
+
segments.value.map((s) => ({ id: s.id, label: s.label ?? undefined, items: s.items })),
|
|
732
|
+
);
|
|
733
|
+
|
|
1079
734
|
</script>
|
|
1080
735
|
|
|
1081
736
|
<template>
|
|
1082
|
-
<!--
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
737
|
+
<!-- tablo:t3 — the explorer's listing is DataTable, the product's one table,
|
|
738
|
+
with the file-specific parts handed in: the columns, what a cell prints
|
|
739
|
+
for a file, the row's gestures and a per-FOLDER column store. Every
|
|
740
|
+
class the explorer's stylesheet and the e2e suites address
|
|
741
|
+
(`fe-list__row`, `fe-list__col--name`, `[data-fe-path]`, the ⋮) is
|
|
742
|
+
still drawn, by DataTable. -->
|
|
743
|
+
<DataTable
|
|
744
|
+
:rows="rows"
|
|
745
|
+
:groups="tableGroups"
|
|
746
|
+
:columns="columns"
|
|
747
|
+
:column-store="cols"
|
|
748
|
+
:candidates="candidateCols"
|
|
749
|
+
:metrics="EXPLORER_METRICS"
|
|
750
|
+
:row-key="(n: FileNode) => n.path"
|
|
751
|
+
:locale="locale"
|
|
752
|
+
:theme="theme"
|
|
753
|
+
:loading="loading"
|
|
754
|
+
:empty="t('empty.folder')"
|
|
1093
755
|
:aria-label="t('list.aria')"
|
|
1094
|
-
:
|
|
1095
|
-
|
|
756
|
+
:framed="false"
|
|
757
|
+
selectable
|
|
758
|
+
:is-selected="isSelected"
|
|
759
|
+
:sort="tableSort"
|
|
760
|
+
:sort-closed="ranked ? t('sort.relevance_why') : null"
|
|
761
|
+
:row-class="rowClass"
|
|
762
|
+
:row-attrs="rowAttrs"
|
|
763
|
+
@sort="onSort"
|
|
764
|
+
@row-click="onRowClick"
|
|
765
|
+
@row-dblclick="onRowDbl"
|
|
766
|
+
@row-contextmenu="onRowCtx"
|
|
767
|
+
@check-click="onCheckClick"
|
|
768
|
+
@row-dragstart="onItemDragStart"
|
|
769
|
+
@row-dragover="onItemDragOver"
|
|
770
|
+
@row-dragleave="onItemDragLeave"
|
|
771
|
+
@row-drop="onItemDrop"
|
|
772
|
+
@row-touchstart="(n: FileNode, ev: TouchEvent) => touch.onTouchStart(n, ev)"
|
|
773
|
+
@row-touchend="(ev: TouchEvent) => touch.onTouchEnd(ev)"
|
|
774
|
+
@row-touchmove="(ev: TouchEvent) => touch.onTouchMove(ev)"
|
|
1096
775
|
>
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
<!--
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
<
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
:
|
|
1117
|
-
:
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
:
|
|
1133
|
-
|
|
1134
|
-
@pointerdown="onResizeStart('name', $event)"
|
|
1135
|
-
@dblclick.stop="onResizeReset('name')"
|
|
1136
|
-
@keydown="onResizeKey('name', $event)"
|
|
1137
|
-
></span>
|
|
1138
|
-
</div>
|
|
1139
|
-
<!-- surucu:d1-sort — Type / Modified / Size sort; Location and the star
|
|
1140
|
-
do not, because there is no SortKey for either and inventing one
|
|
1141
|
-
would put a word in the Sort-by menu that means nothing in nine
|
|
1142
|
-
listings out of ten. Owner is the same case: the People chip is how
|
|
1143
|
-
you narrow by person. -->
|
|
1144
|
-
<div
|
|
1145
|
-
v-for="(id, ci) in visibleCols"
|
|
1146
|
-
:key="id"
|
|
1147
|
-
:class="[
|
|
1148
|
-
colClass(id),
|
|
1149
|
-
{
|
|
1150
|
-
'is-col-dragging': dragCol === id,
|
|
1151
|
-
'is-drop-before': dropAt === ci && !!dragCol && dragCol !== id,
|
|
1152
|
-
'is-drop-after': dropAt === visibleCols.length && ci === visibleCols.length - 1 && !!dragCol,
|
|
1153
|
-
'is-movable': isMovable(id),
|
|
1154
|
-
},
|
|
1155
|
-
]"
|
|
1156
|
-
role="columnheader"
|
|
1157
|
-
:style="colStyle(id)"
|
|
1158
|
-
:aria-sort="SORT_OF[id] ? ariaSort(SORT_OF[id]!) : undefined"
|
|
1159
|
-
:aria-label="id === 'star' ? t('col.star') : undefined"
|
|
1160
|
-
@pointerdown="onHeadPointerDown(id, $event)"
|
|
1161
|
-
>
|
|
1162
|
-
<button
|
|
1163
|
-
v-if="SORT_OF[id]"
|
|
1164
|
-
type="button"
|
|
1165
|
-
class="fe-list__sort"
|
|
1166
|
-
:disabled="ranked"
|
|
1167
|
-
:title="sortTitle"
|
|
1168
|
-
@click="toggleSort(SORT_OF[id]!)"
|
|
1169
|
-
>
|
|
1170
|
-
{{ colLabel(id) }}
|
|
1171
|
-
<span v-if="!ranked && sortKey === SORT_OF[id]" class="fe-list__sort-arrow" aria-hidden="true">{{ sortDir === 'asc' ? '↑' : '↓' }}</span>
|
|
1172
|
-
</button>
|
|
1173
|
-
<span v-else-if="id !== 'star'" class="fe-list__head-label">{{ colLabel(id) }}</span>
|
|
1174
|
-
<!-- The drag handle straddles the divider on this column's trailing
|
|
1175
|
-
edge. `role="separator"` and focusable, so the arrow keys move it
|
|
1176
|
-
too — a handle you can only reach with a mouse is a handle half
|
|
1177
|
-
the people using this cannot reach at all. -->
|
|
1178
|
-
<span
|
|
1179
|
-
v-if="isResizable(id)"
|
|
1180
|
-
class="fe-list__resize"
|
|
1181
|
-
role="separator"
|
|
1182
|
-
aria-orientation="vertical"
|
|
1183
|
-
tabindex="0"
|
|
1184
|
-
:aria-label="t('cols.resize', { col: colLabel(id) })"
|
|
1185
|
-
:title="t('cols.resize', { col: colLabel(id) })"
|
|
1186
|
-
@pointerdown="onResizeStart(id, $event)"
|
|
1187
|
-
@dblclick.stop="onResizeReset(id)"
|
|
1188
|
-
@keydown="onResizeKey(id, $event)"
|
|
1189
|
-
></span>
|
|
1190
|
-
</div>
|
|
1191
|
-
<div class="fe-list__col fe-list__col--menu" role="columnheader">
|
|
1192
|
-
<button
|
|
1193
|
-
type="button"
|
|
1194
|
-
class="fe-list__menu fe-list__colmenu-btn"
|
|
1195
|
-
:title="t('cols.menu')"
|
|
1196
|
-
:aria-label="t('cols.menu')"
|
|
1197
|
-
aria-haspopup="dialog"
|
|
1198
|
-
:aria-expanded="colMenuOpen ? 'true' : 'false'"
|
|
1199
|
-
@click="openColMenu"
|
|
1200
|
-
><span aria-hidden="true">⋮</span></button>
|
|
1201
|
-
</div>
|
|
1202
|
-
</div>
|
|
1203
|
-
<div class="fe-list__body" role="rowgroup">
|
|
1204
|
-
<template v-for="seg in segments" :key="seg.id">
|
|
1205
|
-
<div v-if="seg.label" class="fe-list__group" role="presentation">{{ seg.label }}</div>
|
|
1206
|
-
<div
|
|
1207
|
-
v-for="n in seg.items"
|
|
1208
|
-
:key="n.path"
|
|
1209
|
-
class="fe-list__row"
|
|
1210
|
-
:class="{
|
|
1211
|
-
'is-selected': isSelected(n),
|
|
1212
|
-
'is-dir': n.type === 'dir',
|
|
1213
|
-
'is-trash': n.trashed,
|
|
1214
|
-
'is-clipped': clipped?.has(n.path),
|
|
1215
|
-
'is-droptarget': dropTargetPath === n.path /* wiring:c4 */,
|
|
1216
|
-
}"
|
|
1217
|
-
role="row"
|
|
1218
|
-
tabindex="0"
|
|
1219
|
-
:style="tableStyle /* tablo:t2 — the head's own width, so the two cannot drift */"
|
|
1220
|
-
:aria-selected="isSelected(n) ? 'true' : 'false'"
|
|
1221
|
-
:aria-label="nodeDisplayName(n) /* wiring:c4 */"
|
|
1222
|
-
:data-fe-path="n.path /* wiring:d1 — middle-click open-in-new-tab delegation */"
|
|
1223
|
-
draggable="true"
|
|
1224
|
-
@click="onRowClick(n, $event)"
|
|
1225
|
-
@dblclick="onRowDbl(n)"
|
|
1226
|
-
@contextmenu="onRowCtx(n, $event)"
|
|
1227
|
-
@dragstart="onItemDragStart(n, $event)"
|
|
1228
|
-
@dragover="onItemDragOver(n, $event)"
|
|
1229
|
-
@dragleave="onItemDragLeave(n) /* wiring:c4 */"
|
|
1230
|
-
@drop="onItemDrop(n, $event)"
|
|
1231
|
-
@touchstart.passive="touch.onTouchStart(n, $event)"
|
|
1232
|
-
@touchend="touch.onTouchEnd"
|
|
1233
|
-
@touchmove.passive="touch.onTouchMove"
|
|
1234
|
-
>
|
|
1235
|
-
<!-- issue #26 — the tick is the one click that selects; the whole
|
|
1236
|
-
cell is its target, so a click that just misses the box does not
|
|
1237
|
-
open the row instead. `data-fe-control` keeps a finger on the
|
|
1238
|
-
cell from counting as a tap on the row (useRowTouch). -->
|
|
1239
|
-
<div
|
|
1240
|
-
class="fe-list__col fe-list__col--check"
|
|
1241
|
-
role="gridcell"
|
|
1242
|
-
data-fe-control
|
|
1243
|
-
@click.stop="onCheckClick(n, $event)"
|
|
1244
|
-
@dblclick.stop
|
|
1245
|
-
>
|
|
1246
|
-
<ItemCheck :on="isSelected(n)" :label="nodeDisplayName(n)" />
|
|
1247
|
-
</div>
|
|
1248
|
-
<div class="fe-list__col fe-list__col--name" role="gridcell" :style="colStyle('name')">
|
|
1249
|
-
<!-- ikon:emoji — an encrypted folder is still a FOLDER: it keeps the
|
|
1250
|
-
folder's shape and colour with the padlock cut out of it, and the
|
|
1251
|
-
span carries the name the bare lock emoji never had. One definition
|
|
1252
|
-
for all three views, in lib/fileIcons. -->
|
|
1253
|
-
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
|
|
776
|
+
<template #cell-name="{ row }">
|
|
777
|
+
<!-- ikon:emoji — an encrypted folder is still a FOLDER: it keeps the
|
|
778
|
+
folder's shape and colour with the padlock cut out of it. One
|
|
779
|
+
definition for all three views, in lib/fileIcons. -->
|
|
780
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
|
|
781
|
+
<span
|
|
782
|
+
v-if="isEncryptedFolder(row)"
|
|
783
|
+
class="fe-list__icon fe-list__icon--svg"
|
|
784
|
+
role="img"
|
|
785
|
+
:aria-label="t('e2e.badge')"
|
|
786
|
+
v-html="encryptedFolderTile()"
|
|
787
|
+
></span>
|
|
788
|
+
<!-- tablo:t1 — the real thumbnail, at the tile's own size so the row
|
|
789
|
+
height does not move. `draggable="false"`: dragging an <img> puts a
|
|
790
|
+
'Files' MIME on the dataTransfer and the parent's upload handler
|
|
791
|
+
re-uploads the thing you were only moving. -->
|
|
792
|
+
<img
|
|
793
|
+
v-else-if="thumbOf(row)"
|
|
794
|
+
class="fe-list__icon fe-list__icon--img"
|
|
795
|
+
:class="{ 'fe-thumb--page': drawsAsPage(row) }"
|
|
796
|
+
:src="thumbOf(row)!"
|
|
797
|
+
alt=""
|
|
798
|
+
aria-hidden="true"
|
|
799
|
+
loading="lazy"
|
|
800
|
+
draggable="false"
|
|
801
|
+
/>
|
|
802
|
+
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
|
|
803
|
+
<span v-else class="fe-list__icon fe-list__icon--svg" aria-hidden="true" v-html="fileIconTile(row)"></span>
|
|
804
|
+
<div class="fe-list__name-wrap">
|
|
805
|
+
<span class="fe-list__name" :title="nameTitle(row)">
|
|
806
|
+
<!-- ⚠ RTL: `<bdi>` — the name is the person's text, not the
|
|
807
|
+
interface's: its own letters decide its direction, so `2026
|
|
808
|
+
report.pdf` keeps its number in front in an Arabic list and an
|
|
809
|
+
Arabic name keeps its order in an English one. -->
|
|
810
|
+
<bdi>{{ nodeDisplayName(row) }}</bdi>
|
|
811
|
+
<!-- bul:s3 — content-match badge -->
|
|
812
|
+
<span v-if="rowInContent(row)" class="fe-list__badge">{{ t('search.in_content') }}</span>
|
|
1254
813
|
<span
|
|
1255
|
-
v-if="
|
|
1256
|
-
class="fe-
|
|
814
|
+
v-if="keepBadgeFor && keepBadgeFor(row)"
|
|
815
|
+
:class="['fe-keepbadge', 'fe-keepbadge--' + keepBadgeFor(row)]"
|
|
816
|
+
:title="t('keep.badge_' + keepBadgeFor(row))"
|
|
1257
817
|
role="img"
|
|
1258
|
-
:aria-label="t('
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
class="fe-list__icon fe-list__icon--img"
|
|
1269
|
-
:class="{ 'fe-thumb--page': drawsAsPage(n) }"
|
|
1270
|
-
:src="thumbOf(n)!"
|
|
1271
|
-
alt=""
|
|
1272
|
-
aria-hidden="true"
|
|
1273
|
-
loading="lazy"
|
|
1274
|
-
draggable="false"
|
|
1275
|
-
/>
|
|
1276
|
-
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
|
|
1277
|
-
<span v-else class="fe-list__icon fe-list__icon--svg" aria-hidden="true" v-html="fileIconTile(n)"></span>
|
|
1278
|
-
<div class="fe-list__name-wrap">
|
|
1279
|
-
<span class="fe-list__name" :title="nameTitle(n)">
|
|
1280
|
-
{{ nodeDisplayName(n) }}
|
|
1281
|
-
<!-- bul:s3 — content-match badge -->
|
|
1282
|
-
<span v-if="rowInContent(n)" class="fe-list__badge">{{ t('search.in_content') }}</span>
|
|
1283
|
-
<span
|
|
1284
|
-
v-if="keepBadgeFor && keepBadgeFor(n)"
|
|
1285
|
-
:class="['fe-keepbadge', 'fe-keepbadge--' + keepBadgeFor(n)]"
|
|
1286
|
-
:title="t('keep.badge_' + keepBadgeFor(n))"
|
|
1287
|
-
role="img"
|
|
1288
|
-
:aria-label="t('keep.badge_' + keepBadgeFor(n))"
|
|
1289
|
-
>{{ keepGlyph(keepBadgeFor(n)!) }}</span>
|
|
1290
|
-
</span>
|
|
1291
|
-
<!-- tablo:t1 — the parent path used to be a second line HERE, and
|
|
1292
|
-
printed an em dash when a row had no sub-folder. That is the
|
|
1293
|
-
dash the owner reported under the filenames; it is a Location
|
|
1294
|
-
COLUMN now ("her değer kendi sütununda gözükmesi gerekmekte"),
|
|
1295
|
-
and a row at a storage's root simply has an empty cell. -->
|
|
1296
|
-
<!-- bul:s3 — content snippet («» → <mark> via TEXT segments, no innerHTML) -->
|
|
1297
|
-
<span v-if="rowSnippet(n)" class="fe-list__snippet">
|
|
1298
|
-
<template v-for="(seg, si) in snippetSegments(rowSnippet(n))" :key="si">
|
|
1299
|
-
<mark v-if="seg.match" class="fe-list__mark">{{ seg.text }}</mark>
|
|
1300
|
-
<template v-else>{{ seg.text }}</template>
|
|
1301
|
-
</template>
|
|
1302
|
-
</span>
|
|
1303
|
-
</div>
|
|
1304
|
-
<!-- gorunum:v1 — the kind, said in words beside the name. OUTSIDE
|
|
1305
|
-
`.fe-list__name`: the shot scripts read that element's
|
|
1306
|
-
textContent as the filename (e2e/shots/starstags.mjs), and a
|
|
1307
|
-
row whose name reads "Reports Folder" breaks them silently. -->
|
|
1308
|
-
<span v-if="isPlainDir(n) && !typeColumnShown" class="fe-list__kind">{{ t('node.folder') }}</span>
|
|
1309
|
-
</div>
|
|
1310
|
-
<!-- tablo:t1 — one loop over the SAME list the header draws, so a
|
|
1311
|
-
column cannot exist in one and not the other, and the values land
|
|
1312
|
-
in the tracks the inline template allocated. -->
|
|
1313
|
-
<template v-for="id in visibleCols" :key="id">
|
|
1314
|
-
<div
|
|
1315
|
-
v-if="id === 'star'"
|
|
1316
|
-
class="fe-list__col fe-list__col--star"
|
|
1317
|
-
role="gridcell"
|
|
1318
|
-
:style="colStyle(id)"
|
|
1319
|
-
>
|
|
1320
|
-
<!-- issue #26 — only the star itself is a control; the rest of the
|
|
1321
|
-
cell (all of it on a folder, which has no star) opens the row
|
|
1322
|
-
like any other empty space on it. `display: contents`, so the
|
|
1323
|
-
wrapper stops the click without moving the star. -->
|
|
1324
|
-
<span
|
|
1325
|
-
v-if="starEnabled !== false && typeof n.id === 'number' && n.type === 'file'"
|
|
1326
|
-
class="fe-list__star-hit"
|
|
1327
|
-
data-fe-control
|
|
1328
|
-
@click.stop
|
|
1329
|
-
@dblclick.stop
|
|
1330
|
-
>
|
|
1331
|
-
<StarButton
|
|
1332
|
-
:starred="!!starredIds?.has(n.id)"
|
|
1333
|
-
:node-id="n.id"
|
|
1334
|
-
:api-base="apiBase"
|
|
1335
|
-
:auth-headers="authHeaders"
|
|
1336
|
-
:auth-credentials="authCredentials"
|
|
1337
|
-
:locale="locale"
|
|
1338
|
-
compact
|
|
1339
|
-
@change="(val: boolean) => emit('star-change', n, val)"
|
|
1340
|
-
/>
|
|
1341
|
-
</span>
|
|
1342
|
-
</div>
|
|
1343
|
-
<div v-else :class="colClass(id)" role="gridcell" :style="colStyle(id)" :title="cellTitle(id, n)">
|
|
1344
|
-
{{ cellText(id, n) }}
|
|
1345
|
-
</div>
|
|
1346
|
-
</template>
|
|
1347
|
-
<div class="fe-list__col fe-list__col--menu" role="gridcell" data-fe-control @click.stop @dblclick.stop>
|
|
1348
|
-
<button
|
|
1349
|
-
type="button"
|
|
1350
|
-
class="fe-list__menu"
|
|
1351
|
-
:title="t('toolbar.more')"
|
|
1352
|
-
:aria-label="t('toolbar.more')"
|
|
1353
|
-
@click="onRowMenu(n, $event)"
|
|
1354
|
-
><span aria-hidden="true">⋮</span></button>
|
|
1355
|
-
</div>
|
|
818
|
+
:aria-label="t('keep.badge_' + keepBadgeFor(row))"
|
|
819
|
+
>{{ keepGlyph(keepBadgeFor(row)!) }}</span>
|
|
820
|
+
</span>
|
|
821
|
+
<!-- bul:s3 — content snippet («» → <mark> via TEXT segments, no innerHTML) -->
|
|
822
|
+
<span v-if="rowSnippet(row)" class="fe-list__snippet">
|
|
823
|
+
<template v-for="(seg, si) in snippetSegments(rowSnippet(row))" :key="si">
|
|
824
|
+
<mark v-if="seg.match" class="fe-list__mark">{{ seg.text }}</mark>
|
|
825
|
+
<template v-else>{{ seg.text }}</template>
|
|
826
|
+
</template>
|
|
827
|
+
</span>
|
|
1356
828
|
</div>
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
829
|
+
<!-- ⚠ Badges OUTSIDE `.fe-list__name`: the shot scripts read that
|
|
830
|
+
element's textContent as the filename, and a row whose name reads
|
|
831
|
+
"nda.pdf Locked" breaks them silently (lesson #29). -->
|
|
832
|
+
<span
|
|
833
|
+
v-if="lockTitleOf(row)"
|
|
834
|
+
class="fe-applock"
|
|
835
|
+
role="img"
|
|
836
|
+
:title="lockTitleOf(row)"
|
|
837
|
+
:aria-label="lockTitleOf(row)"
|
|
838
|
+
data-testid="lock-badge"
|
|
839
|
+
><span class="fe-applock__glyph" aria-hidden="true">🔒</span>{{ t('applock.badge') }}</span>
|
|
840
|
+
<!-- issue #34 — a symlink the server will not follow; `aria-label`
|
|
841
|
+
carries the SENTENCE, because "Outside storage" read aloud is a
|
|
842
|
+
second riddle. -->
|
|
843
|
+
<span
|
|
844
|
+
v-if="linkOf(row)"
|
|
845
|
+
class="fe-symlink"
|
|
846
|
+
:class="'fe-symlink--' + linkOf(row)!.state"
|
|
847
|
+
role="img"
|
|
848
|
+
:title="linkOf(row)!.why"
|
|
849
|
+
:aria-label="linkOf(row)!.why"
|
|
850
|
+
data-testid="symlink-badge"
|
|
851
|
+
:data-link-state="linkOf(row)!.state"
|
|
852
|
+
><span class="fe-symlink__glyph" aria-hidden="true">🔗</span>{{ linkOf(row)!.badge }}</span>
|
|
853
|
+
<span v-if="isPlainDir(row) && !typeColumnShown" class="fe-list__kind">{{ t('node.folder') }}</span>
|
|
854
|
+
</template>
|
|
855
|
+
|
|
856
|
+
<template #cell-star="{ row }">
|
|
857
|
+
<!-- issue #26 — only the star itself is a control; the rest of the cell
|
|
858
|
+
opens the row like any other empty space on it. -->
|
|
859
|
+
<span
|
|
860
|
+
v-if="starEnabled !== false && typeof row.id === 'number' && row.type === 'file'"
|
|
861
|
+
class="fe-list__star-hit"
|
|
862
|
+
data-fe-control
|
|
1385
863
|
@click.stop
|
|
1386
|
-
@
|
|
864
|
+
@dblclick.stop
|
|
1387
865
|
>
|
|
1388
|
-
<
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
<
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
</
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
A per-folder memory nobody can explain is the difference between a
|
|
1428
|
-
feature and a haunting. -->
|
|
1429
|
-
<p v-if="thisFolderRemembered || rememberedFolders > 0" class="fe-colmenu__hint">{{ t('cols.folder_hint') }}</p>
|
|
1430
|
-
<button v-if="thisFolderRemembered" type="button" class="fe-colmenu__row" @click="onForgetFolder">
|
|
1431
|
-
<span class="fe-colmenu__label">{{ t('cols.forget_folder') }}</span>
|
|
1432
|
-
</button>
|
|
1433
|
-
<button v-if="rememberedFolders > 0" type="button" class="fe-colmenu__row" @click="onApplyToAll">
|
|
1434
|
-
<span class="fe-colmenu__label">{{ t('cols.apply_all', { count: rememberedFolders }) }}</span>
|
|
1435
|
-
</button>
|
|
1436
|
-
</div>
|
|
1437
|
-
</div>
|
|
1438
|
-
</Teleport>
|
|
866
|
+
<StarButton
|
|
867
|
+
:starred="!!starredIds?.has(row.id)"
|
|
868
|
+
:node-id="row.id"
|
|
869
|
+
:api-base="apiBase"
|
|
870
|
+
:auth-headers="authHeaders"
|
|
871
|
+
:auth-credentials="authCredentials"
|
|
872
|
+
:locale="locale"
|
|
873
|
+
compact
|
|
874
|
+
@change="(val: boolean) => emit('star-change', row, val)"
|
|
875
|
+
/>
|
|
876
|
+
</span>
|
|
877
|
+
</template>
|
|
878
|
+
|
|
879
|
+
<template #actions="{ row }">
|
|
880
|
+
<!-- gorunum:v1 — the ⋮ opens THE SAME menu the right-click opens. -->
|
|
881
|
+
<button
|
|
882
|
+
type="button"
|
|
883
|
+
class="fe-list__menu"
|
|
884
|
+
:title="t('toolbar.more')"
|
|
885
|
+
:aria-label="t('toolbar.more')"
|
|
886
|
+
@click="onRowMenu(row, $event)"
|
|
887
|
+
><span aria-hidden="true">⋮</span></button>
|
|
888
|
+
</template>
|
|
889
|
+
|
|
890
|
+
<template #colmenu-extra="{ close }">
|
|
891
|
+
<div v-if="thisFolderRemembered || rememberedFolders > 0" class="fe-colmenu__sep" role="separator"></div>
|
|
892
|
+
<p v-if="thisFolderRemembered || rememberedFolders > 0" class="fe-colmenu__title">{{ t('cols.folder_title') }}</p>
|
|
893
|
+
<!-- The rule, in one sentence, next to the two buttons that act on it.
|
|
894
|
+
A per-folder memory nobody can explain is the difference between a
|
|
895
|
+
feature and a haunting. -->
|
|
896
|
+
<p v-if="thisFolderRemembered || rememberedFolders > 0" class="fe-colmenu__hint">{{ t('cols.folder_hint') }}</p>
|
|
897
|
+
<button v-if="thisFolderRemembered" type="button" class="fe-colmenu__row" @click="onForgetFolder(close)">
|
|
898
|
+
<span class="fe-colmenu__label">{{ t('cols.forget_folder') }}</span>
|
|
899
|
+
</button>
|
|
900
|
+
<button v-if="rememberedFolders > 0 || thisFolderRemembered" type="button" class="fe-colmenu__row" @click="onApplyToAll(close)">
|
|
901
|
+
<span class="fe-colmenu__label">{{ t('cols.apply_all', { count: rememberedFolders }) }}</span>
|
|
902
|
+
</button>
|
|
903
|
+
</template>
|
|
904
|
+
</DataTable>
|
|
1439
905
|
</template>
|