@brftech/filex-core 0.39.1 → 0.41.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 +32 -25
- package/dist/ArchiveViewer-BNqukFg8.js +124 -0
- package/dist/ArchiveViewer-BNqukFg8.js.map +1 -0
- package/dist/CsvViewer-ii_-MgmG.js +141 -0
- package/dist/CsvViewer-ii_-MgmG.js.map +1 -0
- package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-B2tuu4rX.js} +48 -42
- package/dist/DrawioViewer-B2tuu4rX.js.map +1 -0
- package/dist/EpubViewer-p4B8iiWb.js +163 -0
- package/dist/EpubViewer-p4B8iiWb.js.map +1 -0
- package/dist/IpynbViewer-D_qYRJhu.js +184 -0
- package/dist/IpynbViewer-D_qYRJhu.js.map +1 -0
- package/dist/MermaidViewer-3ZkfNM8v.js +137 -0
- package/dist/MermaidViewer-3ZkfNM8v.js.map +1 -0
- package/dist/PsdViewer-CuV-OPSd.js +122 -0
- package/dist/PsdViewer-CuV-OPSd.js.map +1 -0
- package/dist/TiffViewer-BLeoF4Be.js +142 -0
- package/dist/TiffViewer-BLeoF4Be.js.map +1 -0
- package/dist/Viewer3D-DHhhU76E.js +75 -0
- package/dist/Viewer3D-DHhhU76E.js.map +1 -0
- package/dist/filex-core.js +217 -20077
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +61 -62
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-BraG7Cz4.js +26664 -0
- package/dist/index-BraG7Cz4.js.map +1 -0
- package/dist/index.d.ts +4276 -851
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +2170 -492
- package/src/components/AdvancedSearch.vue +870 -0
- package/src/components/Breadcrumb.vue +191 -19
- package/src/components/CommandPalette.vue +66 -22
- package/src/components/ConnectionsPanel.vue +6 -2
- package/src/components/ContextMenu.vue +57 -2
- package/src/components/EncryptedFolderModal.vue +31 -25
- package/src/components/FilePane.vue +939 -0
- package/src/components/FilterBar.vue +568 -72
- package/src/components/GalleryView.vue +162 -30
- package/src/components/GridView.vue +322 -58
- package/src/components/HomeView.vue +331 -0
- package/src/components/InspectorPanel.vue +296 -53
- package/src/components/ListView.vue +1158 -167
- package/src/components/NFSExportsPanel.vue +7 -4
- package/src/components/OnboardingTour.vue +156 -33
- package/src/components/QuickLook.vue +34 -5
- package/src/components/RecentlyOpened.vue +17 -2
- package/src/components/S3KeysPanel.vue +7 -4
- package/src/components/SSHKeysPanel.vue +7 -4
- package/src/components/ShortcutSettings.vue +43 -2
- package/src/components/ShortcutsHelp.vue +4 -1
- package/src/components/SideNav.vue +206 -140
- package/src/components/StorageFields.vue +19 -1
- package/src/components/TabBar.vue +87 -1
- package/src/components/TagPicker.vue +77 -6
- package/src/components/ThemeGallery.vue +13 -198
- package/src/components/ThemePalette.vue +244 -0
- package/src/components/TimeZoneDialog.vue +127 -0
- package/src/components/TimeZonePicker.vue +696 -0
- package/src/components/TokensPanel.vue +16 -4
- package/src/components/Toolbar.vue +965 -469
- package/src/components/ViewSwitcher.vue +9 -4
- package/src/composables/useConnections.ts +35 -4
- package/src/composables/useExplorerTimeZone.ts +85 -0
- package/src/composables/useFileApi.ts +53 -4
- package/src/composables/useKeyboardShortcuts.ts +122 -0
- package/src/composables/useLocale.ts +365 -25
- package/src/index.ts +182 -3
- package/src/lib/actionIcons.ts +330 -0
- package/src/lib/advSearch.ts +162 -0
- package/src/lib/dateGroups.ts +197 -0
- package/src/lib/destinationTree.ts +235 -0
- package/src/lib/downloadSelection.ts +154 -0
- package/src/lib/fileFilters.ts +319 -12
- package/src/lib/fileIcons.ts +299 -5
- package/src/lib/filePreview.ts +605 -0
- package/src/lib/listing.ts +72 -0
- package/src/lib/shareTtl.ts +11 -1
- package/src/lib/sortOrder.ts +459 -0
- package/src/lib/themes.ts +152 -23
- package/src/lib/timezone.ts +456 -0
- package/src/lib/uiProfile.ts +82 -0
- package/src/lib/viewPrefs.ts +1036 -0
- package/src/locales/en.ts +432 -25
- package/src/locales/tr.ts +425 -25
- package/src/modals/ConvertModal.vue +23 -3
- package/src/modals/DestinationPickerModal.vue +341 -0
- package/src/modals/Modal.vue +18 -4
- package/src/modals/NewDocumentModal.vue +525 -0
- package/src/modals/NewFolderModal.vue +15 -9
- package/src/modals/PermissionsModal.vue +543 -473
- package/src/modals/PreviewModal.vue +516 -156
- package/src/styles/base.css +5435 -8
- package/src/styles/variables.css +140 -60
- package/src/types/Connections.ts +9 -0
- package/src/types/ExplorerConfig.ts +144 -39
- package/src/types/FileNode.ts +28 -0
- package/src/viewers/ArchiveViewer.vue +46 -8
- package/src/viewers/CsvViewer.vue +19 -2
- package/src/viewers/DrawioViewer.vue +12 -1
- package/src/viewers/EpubViewer.vue +50 -6
- package/src/viewers/IpynbViewer.vue +19 -2
- package/src/viewers/MermaidViewer.vue +20 -3
- package/src/viewers/PdfViewer.vue +24 -4
- package/src/viewers/PsdViewer.vue +28 -4
- package/src/viewers/TiffViewer.vue +19 -2
- package/src/viewers/Viewer3D.vue +36 -2
- package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
- package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
- package/dist/CsvViewer-CqWeV8VO.js +0 -131
- package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
- package/dist/DrawioViewer-BNALOB04.js.map +0 -1
- package/dist/EpubViewer-BPmlImG7.js +0 -145
- package/dist/EpubViewer-BPmlImG7.js.map +0 -1
- package/dist/IpynbViewer-CDlQmuCV.js +0 -175
- package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
- package/dist/MermaidViewer-sfZWdR-d.js +0 -127
- package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
- package/dist/PsdViewer-DwuySZsb.js +0 -112
- package/dist/PsdViewer-DwuySZsb.js.map +0 -1
- package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
- package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
- package/dist/Viewer3D-CJJJgkuv.js +0 -60
- package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
- package/src/components/SecondaryPane.vue +0 -447
- package/src/modals/ShareModal.vue +0 -139
|
@@ -3,33 +3,82 @@
|
|
|
3
3
|
* FilterBar — surucu:d1, the chip row under the breadcrumb in the `drive`
|
|
4
4
|
* profile.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Four chips: Type · People · Modified · Size. Each opens a small single-choice
|
|
7
7
|
* popover; a chip with a choice made carries that choice as its label, so the
|
|
8
8
|
* row says what is being filtered without opening anything.
|
|
9
9
|
*
|
|
10
|
-
* ⚠
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* ⚠ Type comes FIRST. The row used to open with People, which put the one
|
|
11
|
+
* chip that is empty in a single-account install in front of the one every
|
|
12
|
+
* listing has an answer for.
|
|
13
|
+
*
|
|
14
|
+
* ⚠ It was THREE for a long time, and the reason was written here: there was
|
|
15
|
+
* nothing behind a People chip, because a listing row carried no owner and
|
|
16
|
+
* `nodes.owner_id` was quota bookkeeping nobody serialized. That is no longer
|
|
17
|
+
* true — migration 00038 made ownership a fact on the row and the listing
|
|
18
|
+
* projection carries it — so the chip now filters on data the row already has,
|
|
19
|
+
* like the other three. If ownership is ever taken back off the wire, this chip
|
|
20
|
+
* has to go with it: a control that opens, offers names and changes nothing is
|
|
21
|
+
* worse than an absent one.
|
|
22
|
+
*
|
|
23
|
+
* ⚠ The People options are NOT a directory listing. `Anyone`, `You` and
|
|
24
|
+
* `System` are always offered; the named accounts are only the ones that
|
|
25
|
+
* actually own something in the rows on screen (`peopleOptions`). Offering
|
|
26
|
+
* every account on the install would put inert names in the menu and leak the
|
|
27
|
+
* user list into a folder view.
|
|
28
|
+
*
|
|
29
|
+
* === surucu:d1-sort — the trailing edge ===============================
|
|
30
|
+
* Two controls at the right of the same row: a button that NAMES the active
|
|
31
|
+
* sort and flips its direction, and a chevron whose menu picks the key.
|
|
32
|
+
*
|
|
33
|
+
* ⚠⚠ They do not own a sort. Both reach for `lib/sortOrder`'s store for the
|
|
34
|
+
* PANE they are inside, the same one the list view's own column headers reach
|
|
35
|
+
* for, so the chip row and the headers are two handles on ONE piece of state
|
|
36
|
+
* — sort by Size here and
|
|
37
|
+
* the Size column's arrow moves, click the Name header and this button says
|
|
38
|
+
* Name. A control with its own private copy of the answer is filex lesson #67
|
|
39
|
+
* one level up, and it is what this row was one commit away from becoming.
|
|
40
|
+
*
|
|
41
|
+
* ⚠⚠ …and while a SEARCH RESULT is on screen there is no key to name. The
|
|
42
|
+
* rows were ranked by the server (`order === 'relevance'`, see
|
|
43
|
+
* `lib/sortOrder`), so this button prints "Relevance" instead of the key,
|
|
44
|
+
* drops its direction arrow, and closes — with the reason in its own tooltip
|
|
45
|
+
* and accessible name. A button reading "Name ↑" over a list that is not in
|
|
46
|
+
* name order is the failure this whole row is otherwise built to avoid, and a
|
|
47
|
+
* control that simply went inert would be the same lie without the wording.
|
|
48
|
+
* The key itself is untouched: it is still there the moment the search is
|
|
49
|
+
* left.
|
|
50
|
+
*
|
|
51
|
+
* ⚠ Deliberately NOT here: the reference build's "Listing actions" ⋮ that sits
|
|
52
|
+
* beside these two. Its menu is New folder / New file / Upload files / Paste /
|
|
53
|
+
* Select all / Deselect all — six commands this component cannot perform and
|
|
54
|
+
* has no handle on. Drawing the menu here would mean six items that open,
|
|
55
|
+
* offer a word and do nothing, which is the failure the People note above is
|
|
56
|
+
* about. It needs events from the explorer; see the report.
|
|
18
57
|
*
|
|
19
58
|
* ⚠ Popovers are TELEPORTED to <body>, like ContextMenu, and positioned
|
|
20
59
|
* `fixed`. An absolutely-positioned panel inside the explorer is clipped by
|
|
21
60
|
* `.fe__body`'s own scroll container, which is how a menu ends up half visible
|
|
22
|
-
* with its own scrollbar.
|
|
61
|
+
* with its own scrollbar. ONE popover machine serves the chips and the sort
|
|
62
|
+
* menu — same positioning, same outside-click, same Escape — because two of
|
|
63
|
+
* them is two chances for a panel to be left open behind the other.
|
|
23
64
|
*
|
|
24
65
|
* ⚠ No `<style>` block. Package CSS lives in `styles/base.css` — a scoped block
|
|
25
66
|
* compiles to `.cls[data-v-HASH]` and the hash does not match in the
|
|
26
67
|
* web-component build, so the rules silently stop applying in every embed.
|
|
27
68
|
*/
|
|
28
|
-
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
69
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
29
70
|
import { useLocale } from '../composables/useLocale';
|
|
30
71
|
import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
72
|
+
import type { FileNode } from '../types/FileNode';
|
|
31
73
|
import type { DriveFilters, ModifiedFilter, SizeFilter, TypeFilter } from '../lib/fileFilters';
|
|
32
|
-
import { activeFilterCount } from '../lib/fileFilters';
|
|
74
|
+
import { activeFilterCount, peopleOptions } from '../lib/fileFilters';
|
|
75
|
+
import {
|
|
76
|
+
SORT_KEYS,
|
|
77
|
+
setSortLocale,
|
|
78
|
+
useSortStore,
|
|
79
|
+
type ListingOrder,
|
|
80
|
+
type SortKey,
|
|
81
|
+
} from '../lib/sortOrder';
|
|
33
82
|
|
|
34
83
|
const props = defineProps<{
|
|
35
84
|
value: DriveFilters;
|
|
@@ -39,13 +88,87 @@ const props = defineProps<{
|
|
|
39
88
|
/** Rows currently shown / rows the folder holds, for the count chip. */
|
|
40
89
|
shown?: number;
|
|
41
90
|
total?: number;
|
|
91
|
+
/**
|
|
92
|
+
* The rows the People chip derives its named accounts from — the listing
|
|
93
|
+
* BEFORE filtering, so choosing a person does not empty the menu that
|
|
94
|
+
* offered them.
|
|
95
|
+
*
|
|
96
|
+
* ⚠ Optional. Without it the chip still offers Anyone / You / System, which
|
|
97
|
+
* are the three that never depend on data; an embedder who passes nothing
|
|
98
|
+
* gets a working control rather than a broken one.
|
|
99
|
+
*/
|
|
100
|
+
files?: FileNode[];
|
|
101
|
+
/* === surucu:d1-actions — the "⋮" listing menu ======================
|
|
102
|
+
* ⚠ Every row in it is a command this component cannot run: the clipboard,
|
|
103
|
+
* the selection and the write permissions all live in the explorer. So the
|
|
104
|
+
* menu takes its two ENABLED states as props and emits the verbs, and the
|
|
105
|
+
* host decides whether a row is reachable. A menu that draws "Paste" alive
|
|
106
|
+
* over an empty clipboard is a menu that lies once per folder. */
|
|
107
|
+
/** There is something on the clipboard to paste here. */
|
|
108
|
+
canPaste?: boolean;
|
|
109
|
+
/** This folder can be written to (false at a read-only root, in the trash). */
|
|
110
|
+
canWrite?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Where the listing behind this row got its order — see `lib/sortOrder`.
|
|
113
|
+
* `relevance` (a search result) closes the sort control and makes it name
|
|
114
|
+
* the order it is actually in. Omitted = an ordinary folder listing, which
|
|
115
|
+
* is what every embedder that never searches has.
|
|
116
|
+
*/
|
|
117
|
+
order?: ListingOrder;
|
|
118
|
+
/* === surucu:d1-scope — the row where there is nothing to chip ==========
|
|
119
|
+
*
|
|
120
|
+
* ⚠⚠ `find` is not "a smaller filter bar". It is this row over a listing
|
|
121
|
+
* whose rows are not files: the multi-storage ROOT (every row is a drive)
|
|
122
|
+
* and HOME (three blocks of cards). Type / People / Modified / Size all
|
|
123
|
+
* answer from `extension`, `mime_type`, `owner_id`, `size` and
|
|
124
|
+
* `last_modified`, and a drive has none of the five — the chips would open,
|
|
125
|
+
* offer ten words and narrow nothing, which is the exact failure the People
|
|
126
|
+
* note above is about. The name box is the one dimension those rows DO
|
|
127
|
+
* have, so it is the one that stays.
|
|
128
|
+
*
|
|
129
|
+
* ⚠ And it stays REACHABLE, rather than the row being dropped: "root
|
|
130
|
+
* folder'da filtre barı kalsın, en kötü o bardan search kalacak — orada adam
|
|
131
|
+
* isterse storage ismi aratabilir" (owner, 2026-09-13). A box that filters
|
|
132
|
+
* nothing would be worse than no box; this one filters the rows in hand
|
|
133
|
+
* through the same `DriveFilters.name` predicate every other listing uses.
|
|
134
|
+
*/
|
|
135
|
+
mode?: 'full' | 'find';
|
|
136
|
+
/**
|
|
137
|
+
* What the name box says it narrows. The default names a FOLDER, which is
|
|
138
|
+
* wrong in both `find` places — neither the drive list nor Home is one.
|
|
139
|
+
* Passed already translated: the pane knows which of the two it is, the
|
|
140
|
+
* catalogue lookup belongs with the caller that knows.
|
|
141
|
+
*/
|
|
142
|
+
findLabel?: string;
|
|
42
143
|
}>();
|
|
43
144
|
|
|
44
|
-
const emit = defineEmits<{
|
|
145
|
+
const emit = defineEmits<{
|
|
146
|
+
(e: 'update:value', v: DriveFilters): void;
|
|
147
|
+
/* surucu:d1-actions — the listing's own verbs. Names match the events the
|
|
148
|
+
navigation panel already emits for the same two commands, so the host
|
|
149
|
+
wires one handler for each rather than one per menu. */
|
|
150
|
+
(e: 'new-folder'): void;
|
|
151
|
+
(e: 'upload'): void;
|
|
152
|
+
(e: 'paste'): void;
|
|
153
|
+
(e: 'select-all'): void;
|
|
154
|
+
}>();
|
|
45
155
|
|
|
46
156
|
const { t } = useLocale(() => props.locale);
|
|
47
157
|
|
|
48
|
-
|
|
158
|
+
/** surucu:d1-scope — everything except the name box is drawn only in `full`. */
|
|
159
|
+
const full = computed(() => (props.mode ?? 'full') === 'full');
|
|
160
|
+
/** The name box's placeholder AND its accessible name — one value, read twice,
|
|
161
|
+
* so the two cannot describe different things. */
|
|
162
|
+
const findLabel = computed(() => props.findLabel || t('filter.find'));
|
|
163
|
+
|
|
164
|
+
type Group = 'people' | 'type' | 'modified' | 'size';
|
|
165
|
+
/** Everything this row can open. `sort` is not a filter group — it shares the
|
|
166
|
+
* popover machine, not the filter model. */
|
|
167
|
+
type PopId = Group | 'sort' | 'actions';
|
|
168
|
+
|
|
169
|
+
/** The chips, in the order they are drawn. One array, so the template loop and
|
|
170
|
+
* `optionsFor` cannot disagree about how many there are. */
|
|
171
|
+
const GROUPS: Group[] = ['type', 'people', 'modified', 'size'];
|
|
49
172
|
|
|
50
173
|
const TYPE_OPTIONS: TypeFilter[] = [
|
|
51
174
|
'any', 'folder', 'document', 'spreadsheet', 'presentation', 'pdf',
|
|
@@ -54,42 +177,171 @@ const TYPE_OPTIONS: TypeFilter[] = [
|
|
|
54
177
|
const MODIFIED_OPTIONS: ModifiedFilter[] = ['any', 'today', '7d', '30d', 'year'];
|
|
55
178
|
const SIZE_OPTIONS: SizeFilter[] = ['any', 'lt1', '1to10', '10to100', 'gt100'];
|
|
56
179
|
|
|
180
|
+
/** Anyone / You / System, plus every other account owning a row on screen. */
|
|
181
|
+
const peopleChoices = computed(() => peopleOptions(props.files ?? []));
|
|
182
|
+
|
|
57
183
|
function optionsFor(g: Group): string[] {
|
|
184
|
+
if (g === 'people') return peopleChoices.value.map((o) => o.value);
|
|
58
185
|
if (g === 'type') return TYPE_OPTIONS;
|
|
59
186
|
if (g === 'modified') return MODIFIED_OPTIONS;
|
|
60
187
|
return SIZE_OPTIONS;
|
|
61
188
|
}
|
|
62
189
|
|
|
63
190
|
function optionLabel(g: Group, v: string): string {
|
|
191
|
+
// A named account is labelled by its NAME, which no locale file can know.
|
|
192
|
+
// The three fixed members are ordinary keys like every other group's.
|
|
193
|
+
if (g === 'people' && v.startsWith('u:')) {
|
|
194
|
+
return peopleChoices.value.find((o) => o.value === v)?.name || v.slice(2);
|
|
195
|
+
}
|
|
64
196
|
return t(`filter.${g}.${v}`);
|
|
65
197
|
}
|
|
66
198
|
|
|
67
199
|
/** The chip's own label: the group name until a choice is made, then the
|
|
68
200
|
* choice — a row of three identical words tells the reader nothing. */
|
|
69
201
|
function chipLabel(g: Group): string {
|
|
70
|
-
const v = props.value[g];
|
|
202
|
+
const v = g === 'people' ? (props.value.people ?? 'any') : props.value[g];
|
|
71
203
|
return v === 'any' ? t(`filter.${g}`) : optionLabel(g, v);
|
|
72
204
|
}
|
|
73
205
|
|
|
206
|
+
/** `value[g]` for a group whose field is optional. */
|
|
207
|
+
function chosen(g: Group): string {
|
|
208
|
+
return g === 'people' ? (props.value.people ?? 'any') : props.value[g];
|
|
209
|
+
}
|
|
210
|
+
|
|
74
211
|
const activeCount = computed(() => activeFilterCount(props.value));
|
|
75
212
|
|
|
213
|
+
// ── surucu:d1-sort — the sort control ─────────────────────────────────
|
|
214
|
+
// Reading through the STORE OF THE PANE this row is inside, so the call
|
|
215
|
+
// subscribes this render to that pane's state and there is no second place
|
|
216
|
+
// holding a copy that could go stale.
|
|
217
|
+
//
|
|
218
|
+
// ⚠⚠ Per pane, not per bundle. The split view draws this row in both halves;
|
|
219
|
+
// while the key lived in a module-level singleton, choosing "Size ↓" on the
|
|
220
|
+
// right re-ordered the listing on the left (measured 2026-09-13). With no
|
|
221
|
+
// provider — one listing, a bare <FilterBar> in a test — this is the default
|
|
222
|
+
// store and nothing changes.
|
|
223
|
+
const sort = useSortStore();
|
|
224
|
+
const sortKey = computed<SortKey>(() => sort.key.value);
|
|
225
|
+
const sortAscending = computed(() => sort.dir.value === 'asc');
|
|
226
|
+
|
|
227
|
+
/** The rows behind this row are the server's ranked answer, so the key below
|
|
228
|
+
* is not what is on screen. One boolean, read by everything in the control. */
|
|
229
|
+
const ranked = computed(() => props.order === 'relevance');
|
|
230
|
+
|
|
231
|
+
/** "Modified", "Size" … — the SAME words the list view's column headers use
|
|
232
|
+
* (`col.*`), because they name the same sort. Over a ranked listing it is
|
|
233
|
+
* "Relevance" instead: the button must name the order it is IN. */
|
|
234
|
+
const sortKeyLabel = computed(() => (ranked.value ? t('sort.relevance') : t(`col.${sortKey.value}`)));
|
|
235
|
+
/** Read only by a screen reader, exactly as in the reference build: the arrow
|
|
236
|
+
* says the direction to everyone who can see it. Over a ranked listing the
|
|
237
|
+
* arrow is gone and this carries the REASON instead — the same sentence the
|
|
238
|
+
* tooltip shows, because "disabled" with no explanation is the state the
|
|
239
|
+
* owner specifically did not want. */
|
|
240
|
+
const sortDirLabel = computed(() =>
|
|
241
|
+
ranked.value ? t('sort.relevance_why') : t(sortAscending.value ? 'sort.asc' : 'sort.desc'),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
/** The sort's own `type` order is by the word the Type column prints, so the
|
|
245
|
+
* comparator has to know which catalogue that word comes from. */
|
|
246
|
+
watch(() => props.locale, (l) => setSortLocale(l), { immediate: true });
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* ⚠ `setSort`, not `chooseSortKey`: a menu item NAMES a destination, so
|
|
250
|
+
* picking the key that is already active must leave it where it is rather
|
|
251
|
+
* than quietly reverse it. The button beside this menu is the toggle, and a
|
|
252
|
+
* column header — where clicking the active one again is what everybody
|
|
253
|
+
* expects — uses the other one.
|
|
254
|
+
*/
|
|
255
|
+
function pickSortKey(k: SortKey): void {
|
|
256
|
+
sort.setSort(k);
|
|
257
|
+
close(true);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* ⚠⚠ The two sort handles refuse to act over a RANKED listing, in the handler
|
|
262
|
+
* and not only through the `disabled` attribute on the button.
|
|
263
|
+
*
|
|
264
|
+
* `disabled` suppresses the browser's own activation, but a click dispatched
|
|
265
|
+
* in script still reaches this listener, and the damage is silent: nothing on
|
|
266
|
+
* screen moves (the rows are the server's), the shared key is written to
|
|
267
|
+
* `filex.list-sort`, and the FOLDER the person goes back to is re-ordered
|
|
268
|
+
* behind their back. Measured 2026-09-13 on the list view's twin control — a
|
|
269
|
+
* scripted click on the closed Size header left the rows untouched and still
|
|
270
|
+
* turned the stored sort from Name ↑ into Size ↑.
|
|
271
|
+
*/
|
|
272
|
+
function flipSortDir(): void {
|
|
273
|
+
if (ranked.value) return;
|
|
274
|
+
sort.toggleSortDir();
|
|
275
|
+
}
|
|
276
|
+
function openSortMenu(): void {
|
|
277
|
+
if (ranked.value) return;
|
|
278
|
+
void toggle('sort');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* === surucu:d1-actions — what the "⋮" offers ==========================
|
|
282
|
+
* The reference build's listing menu, minus one row: it also carries "File",
|
|
283
|
+
* and this explorer has no create-empty-file command anywhere — not in the
|
|
284
|
+
* "+ New" menu, not in the right-click menu, not as a shortcut. Shipping the
|
|
285
|
+
* word with nothing behind it is the one thing this menu was held back for.
|
|
286
|
+
*
|
|
287
|
+
* ⚠ Disabled rather than hidden, all of them. A menu whose height changes
|
|
288
|
+
* between folders is a menu people misclick, and "Paste" greyed out says the
|
|
289
|
+
* clipboard is empty — "Paste" absent says the feature does not exist.
|
|
290
|
+
*
|
|
291
|
+
* ⚠⚠ And there is no "Deselect all", which the reference build does carry.
|
|
292
|
+
* MEASURED, not assumed: the instant anything is selected this whole row is
|
|
293
|
+
* replaced by the selection bar (`.fe-selbar-slot.is-active`) — filter row
|
|
294
|
+
* hidden, this button hidden — and that bar draws its own "Clear selection"
|
|
295
|
+
* button. So a Deselect row here could never be reached (having a selection is
|
|
296
|
+
* exactly the state that hides the menu) and would duplicate a control that IS
|
|
297
|
+
* on screen at that moment. "Select all" is the opposite case and stays: it is
|
|
298
|
+
* reachable precisely when it is useful, and it exists nowhere else but a
|
|
299
|
+
* keystroke.
|
|
300
|
+
*/
|
|
301
|
+
const actionRows = computed(() => [
|
|
302
|
+
{ key: 'new-folder', label: t('toolbar.new_folder'), disabled: props.canWrite === false },
|
|
303
|
+
{ key: 'upload', label: t('toolbar.upload'), disabled: props.canWrite === false },
|
|
304
|
+
{ key: 'paste', label: t('ctx.paste'), disabled: !props.canPaste || props.canWrite === false },
|
|
305
|
+
{ key: 'sep', label: '', divider: true, disabled: true },
|
|
306
|
+
{ key: 'select-all', label: t('shortcuts.select_all'), disabled: false },
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
function runAction(key: string): void {
|
|
310
|
+
close(true);
|
|
311
|
+
if (key === 'new-folder') emit('new-folder');
|
|
312
|
+
else if (key === 'upload') emit('upload');
|
|
313
|
+
else if (key === 'paste') emit('paste');
|
|
314
|
+
else if (key === 'select-all') emit('select-all');
|
|
315
|
+
}
|
|
316
|
+
|
|
76
317
|
// ── the popover ──────────────────────────────────────────────────────
|
|
77
|
-
const
|
|
318
|
+
const openPop = ref<PopId | null>(null);
|
|
78
319
|
const pos = ref({ x: 0, y: 0 });
|
|
79
320
|
const panelEl = ref<HTMLElement | null>(null);
|
|
80
|
-
const
|
|
321
|
+
const anchorEls = ref<Record<string, HTMLElement | null>>({});
|
|
81
322
|
|
|
82
|
-
function
|
|
83
|
-
|
|
323
|
+
function setAnchorEl(id: PopId, el: unknown) {
|
|
324
|
+
anchorEls.value[id] = (el as HTMLElement | null) ?? null;
|
|
84
325
|
}
|
|
85
326
|
|
|
86
|
-
// The open popover's data, precomputed. Reading `value[
|
|
327
|
+
// The open popover's data, precomputed. Reading `value[openPop]` straight
|
|
87
328
|
// from the template would lean on template narrowing across a Teleport, and
|
|
88
329
|
// `vue-tsc --noEmit` is part of this package's build.
|
|
330
|
+
const openGroup = computed<Group | null>(() =>
|
|
331
|
+
openPop.value && openPop.value !== 'sort' && openPop.value !== 'actions'
|
|
332
|
+
? openPop.value
|
|
333
|
+
: null,
|
|
334
|
+
);
|
|
89
335
|
const popOptions = computed<string[]>(() => (openGroup.value ? optionsFor(openGroup.value) : []));
|
|
90
|
-
const popTitle = computed(() =>
|
|
336
|
+
const popTitle = computed(() =>
|
|
337
|
+
openGroup.value
|
|
338
|
+
? t(`filter.${openGroup.value}`)
|
|
339
|
+
: openPop.value === 'actions'
|
|
340
|
+
? t('filter.actions')
|
|
341
|
+
: t('sort.by'),
|
|
342
|
+
);
|
|
91
343
|
function popChecked(opt: string): boolean {
|
|
92
|
-
return !!openGroup.value &&
|
|
344
|
+
return !!openGroup.value && chosen(openGroup.value) === opt;
|
|
93
345
|
}
|
|
94
346
|
function popLabel(opt: string): string {
|
|
95
347
|
return openGroup.value ? optionLabel(openGroup.value, opt) : opt;
|
|
@@ -98,14 +350,14 @@ function popPick(opt: string): void {
|
|
|
98
350
|
if (openGroup.value) pick(openGroup.value, opt);
|
|
99
351
|
}
|
|
100
352
|
|
|
101
|
-
async function toggle(
|
|
102
|
-
if (
|
|
353
|
+
async function toggle(id: PopId) {
|
|
354
|
+
if (openPop.value === id) {
|
|
103
355
|
close();
|
|
104
356
|
return;
|
|
105
357
|
}
|
|
106
|
-
const r =
|
|
358
|
+
const r = anchorEls.value[id]?.getBoundingClientRect();
|
|
107
359
|
pos.value = { x: r ? r.left : 8, y: r ? r.bottom + 6 : 8 };
|
|
108
|
-
|
|
360
|
+
openPop.value = id;
|
|
109
361
|
await nextTick();
|
|
110
362
|
// Keep it on screen: a chip near the right edge would otherwise open a panel
|
|
111
363
|
// that runs off it, and the row is right beside the info panel.
|
|
@@ -116,38 +368,107 @@ async function toggle(g: Group) {
|
|
|
116
368
|
pos.value = { ...pos.value, x: Math.max(8, window.innerWidth - 8 - box.width) };
|
|
117
369
|
}
|
|
118
370
|
if (box.bottom > window.innerHeight - 8) {
|
|
119
|
-
const r2 =
|
|
371
|
+
const r2 = anchorEls.value[id]?.getBoundingClientRect();
|
|
120
372
|
pos.value = { ...pos.value, y: Math.max(8, (r2 ? r2.top : 0) - box.height - 6) };
|
|
121
373
|
}
|
|
122
|
-
|
|
374
|
+
// `data-active` rather than a tick: the sort menu draws no check mark (the
|
|
375
|
+
// button beside it already names the active key), but the keyboard still
|
|
376
|
+
// has to land on it.
|
|
377
|
+
panel.querySelector<HTMLElement>('[data-active="true"]')?.focus();
|
|
123
378
|
}
|
|
124
379
|
}
|
|
125
380
|
|
|
126
381
|
function close(restoreFocus = false) {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
if (restoreFocus &&
|
|
382
|
+
const id = openPop.value;
|
|
383
|
+
openPop.value = null;
|
|
384
|
+
if (restoreFocus && id) anchorEls.value[id]?.focus();
|
|
130
385
|
}
|
|
131
386
|
|
|
387
|
+
/* surucu:d1-sort — ⚠ a listing can BECOME ranked while the Sort-by menu is
|
|
388
|
+
* open: the header field searches on a debounce, so the rows change under a
|
|
389
|
+
* popover that is already up. Leaving it there would offer four keys the
|
|
390
|
+
* listing no longer obeys, and the anchor it is positioned against has just
|
|
391
|
+
* gone disabled — so it would also be unreachable by keyboard. */
|
|
392
|
+
watch(
|
|
393
|
+
() => props.order,
|
|
394
|
+
(o) => {
|
|
395
|
+
if (o === 'relevance' && openPop.value === 'sort') close();
|
|
396
|
+
},
|
|
397
|
+
);
|
|
398
|
+
|
|
132
399
|
function pick(g: Group, v: string) {
|
|
133
400
|
emit('update:value', { ...props.value, [g]: v } as DriveFilters);
|
|
134
401
|
close(true);
|
|
135
402
|
}
|
|
136
403
|
|
|
137
404
|
function clearAll() {
|
|
138
|
-
|
|
405
|
+
localName.value = '';
|
|
406
|
+
// ⚠ Every dimension, named. This literal is hand-written rather than
|
|
407
|
+
// EMPTY_FILTERS because it must NOT reset the advanced-search fields the
|
|
408
|
+
// dialog owns — but a dimension left out here is a chip "Clear" cannot
|
|
409
|
+
// clear, which is how `people` would have stayed set forever.
|
|
410
|
+
//
|
|
411
|
+
// ⚠ The sort is NOT one of them. "Clear filters" un-hides rows; it does not
|
|
412
|
+
// re-order the ones that were never hidden, and a button that silently threw
|
|
413
|
+
// away the order you chose would be a second, invisible job.
|
|
414
|
+
emit('update:value', {
|
|
415
|
+
...props.value,
|
|
416
|
+
type: 'any',
|
|
417
|
+
modified: 'any',
|
|
418
|
+
size: 'any',
|
|
419
|
+
name: '',
|
|
420
|
+
people: 'any',
|
|
421
|
+
});
|
|
139
422
|
}
|
|
140
423
|
|
|
424
|
+
/* === gorunum:v1 — "Filter in this folder…" ============================
|
|
425
|
+
* The chips narrow the rows in hand by type/date/size; this narrows the same
|
|
426
|
+
* rows by name, and it is part of the SAME model (`DriveFilters.name`) rather
|
|
427
|
+
* than a second channel — one predicate, one "Clear", one empty state.
|
|
428
|
+
*
|
|
429
|
+
* ⚠ Not the header field. That one runs a server search and replaces the
|
|
430
|
+
* listing with hits from below this folder; this one never leaves the folder
|
|
431
|
+
* and never issues a request.
|
|
432
|
+
*
|
|
433
|
+
* Debounced, because every emit clears the selection in the parent (a row the
|
|
434
|
+
* filter just hid would still be what Delete acts on) — doing that on each
|
|
435
|
+
* keystroke would be a selection that dies while you type. */
|
|
436
|
+
const localName = ref(props.value.name ?? '');
|
|
437
|
+
watch(
|
|
438
|
+
() => props.value.name ?? '',
|
|
439
|
+
(v) => {
|
|
440
|
+
// The parent resets the filters on navigation; the box has to follow, or
|
|
441
|
+
// it keeps printing a word that no longer filters anything.
|
|
442
|
+
if (v !== localName.value.trim()) localName.value = v;
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
let nameTimer: ReturnType<typeof setTimeout> | undefined;
|
|
446
|
+
function onNameInput(ev: Event) {
|
|
447
|
+
localName.value = (ev.target as HTMLInputElement).value;
|
|
448
|
+
if (nameTimer) clearTimeout(nameTimer);
|
|
449
|
+
nameTimer = setTimeout(() => {
|
|
450
|
+
emit('update:value', { ...props.value, name: localName.value.trim() });
|
|
451
|
+
}, 140);
|
|
452
|
+
}
|
|
453
|
+
function clearName() {
|
|
454
|
+
if (nameTimer) clearTimeout(nameTimer);
|
|
455
|
+
localName.value = '';
|
|
456
|
+
emit('update:value', { ...props.value, name: '' });
|
|
457
|
+
}
|
|
458
|
+
onBeforeUnmount(() => {
|
|
459
|
+
if (nameTimer) clearTimeout(nameTimer);
|
|
460
|
+
});
|
|
461
|
+
|
|
141
462
|
function onDocPointer(ev: PointerEvent) {
|
|
142
|
-
if (!
|
|
463
|
+
if (!openPop.value) return;
|
|
143
464
|
const el = ev.target as Node;
|
|
144
465
|
if (panelEl.value?.contains(el)) return;
|
|
145
|
-
if (Object.values(
|
|
466
|
+
if (Object.values(anchorEls.value).some((c) => c?.contains(el))) return;
|
|
146
467
|
close();
|
|
147
468
|
}
|
|
148
469
|
|
|
149
470
|
function onKey(ev: KeyboardEvent) {
|
|
150
|
-
if (ev.key === 'Escape' &&
|
|
471
|
+
if (ev.key === 'Escape' && openPop.value) {
|
|
151
472
|
ev.stopPropagation();
|
|
152
473
|
close(true);
|
|
153
474
|
}
|
|
@@ -164,37 +485,70 @@ onBeforeUnmount(() => {
|
|
|
164
485
|
</script>
|
|
165
486
|
|
|
166
487
|
<template>
|
|
167
|
-
<div class="fe-filterbar"
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
488
|
+
<div class="fe-filterbar" :data-mode="full ? 'full' : 'find'" data-testid="filterbar">
|
|
489
|
+
<!-- `display: contents` — the chips stay flex items of the row while the
|
|
490
|
+
group role covers exactly the four that ARE filters. The sort control
|
|
491
|
+
at the far end is not one of them and must not be announced as one. -->
|
|
492
|
+
<div v-if="full" class="fe-filterbar__chips" role="group" :aria-label="t('filter.aria')">
|
|
493
|
+
<button
|
|
494
|
+
v-for="g in GROUPS"
|
|
495
|
+
:key="g"
|
|
496
|
+
:ref="(el) => setAnchorEl(g, el)"
|
|
497
|
+
type="button"
|
|
498
|
+
class="fe-filterbar__chip"
|
|
499
|
+
:class="{ 'is-set': chosen(g) !== 'any', 'is-open': openPop === g }"
|
|
500
|
+
:aria-expanded="openPop === g"
|
|
501
|
+
aria-haspopup="listbox"
|
|
502
|
+
:data-testid="`filter-${g}`"
|
|
503
|
+
@click="toggle(g)"
|
|
504
|
+
>
|
|
505
|
+
<span class="fe-filterbar__label">{{ chipLabel(g) }}</span>
|
|
506
|
+
<svg
|
|
507
|
+
class="fe-ficon fe-filterbar__caret"
|
|
508
|
+
viewBox="0 0 24 24"
|
|
509
|
+
fill="none"
|
|
510
|
+
stroke="currentColor"
|
|
511
|
+
stroke-width="2"
|
|
512
|
+
stroke-linecap="round"
|
|
513
|
+
stroke-linejoin="round"
|
|
514
|
+
aria-hidden="true"
|
|
515
|
+
focusable="false"
|
|
516
|
+
>
|
|
517
|
+
<path d="M7 10l5 5 5-5" />
|
|
518
|
+
</svg>
|
|
519
|
+
</button>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
<!-- gorunum:v1 — the name box. Same height as the chips beside it; it is
|
|
523
|
+
one more way to narrow THIS folder, not a second search. -->
|
|
524
|
+
<div class="fe-filterbar__find">
|
|
181
525
|
<svg
|
|
182
|
-
class="fe-ficon fe-
|
|
526
|
+
class="fe-ficon fe-filterbar__find-glass"
|
|
183
527
|
viewBox="0 0 24 24"
|
|
184
528
|
fill="none"
|
|
185
529
|
stroke="currentColor"
|
|
186
|
-
stroke-width="
|
|
530
|
+
stroke-width="1.9"
|
|
187
531
|
stroke-linecap="round"
|
|
188
|
-
stroke-linejoin="round"
|
|
189
532
|
aria-hidden="true"
|
|
190
533
|
focusable="false"
|
|
191
534
|
>
|
|
192
|
-
<
|
|
535
|
+
<circle cx="11" cy="11" r="6.5" />
|
|
536
|
+
<path d="M16 16l4.5 4.5" />
|
|
193
537
|
</svg>
|
|
194
|
-
|
|
538
|
+
<input
|
|
539
|
+
type="search"
|
|
540
|
+
class="fe-filterbar__find-input"
|
|
541
|
+
:placeholder="findLabel"
|
|
542
|
+
:aria-label="findLabel"
|
|
543
|
+
:value="localName"
|
|
544
|
+
data-testid="filter-find"
|
|
545
|
+
@input="onNameInput"
|
|
546
|
+
@keydown.esc.stop.prevent="clearName"
|
|
547
|
+
/>
|
|
548
|
+
</div>
|
|
195
549
|
|
|
196
550
|
<button
|
|
197
|
-
v-if="activeCount > 0"
|
|
551
|
+
v-if="full && activeCount > 0"
|
|
198
552
|
type="button"
|
|
199
553
|
class="fe-filterbar__clear"
|
|
200
554
|
data-testid="filter-clear"
|
|
@@ -204,40 +558,182 @@ onBeforeUnmount(() => {
|
|
|
204
558
|
</button>
|
|
205
559
|
|
|
206
560
|
<span
|
|
207
|
-
v-if="activeCount > 0 && typeof shown === 'number' && typeof total === 'number'"
|
|
561
|
+
v-if="full && activeCount > 0 && typeof shown === 'number' && typeof total === 'number'"
|
|
208
562
|
class="fe-filterbar__count"
|
|
209
563
|
role="status"
|
|
210
564
|
data-testid="filter-count"
|
|
211
565
|
>{{ t('filter.count', { shown: String(shown), total: String(total) }) }}</span
|
|
212
566
|
>
|
|
213
567
|
|
|
568
|
+
<!-- surucu:d1-sort — pushed to the trailing edge of the row.
|
|
569
|
+
⚠ `full` only: a drive list and Home's three card blocks are not a
|
|
570
|
+
sorted listing this row owns, so a button naming a sort key would be
|
|
571
|
+
naming one nothing on screen obeys — the same lie the ranked-search
|
|
572
|
+
case above is built to avoid. -->
|
|
573
|
+
<div v-if="full" class="fe-filterbar__sort">
|
|
574
|
+
<button
|
|
575
|
+
:ref="(el) => setAnchorEl('sort', el)"
|
|
576
|
+
type="button"
|
|
577
|
+
class="fe-filterbar__sortdir"
|
|
578
|
+
:class="{ 'is-ranked': ranked }"
|
|
579
|
+
:disabled="ranked"
|
|
580
|
+
:title="ranked ? sortDirLabel : undefined"
|
|
581
|
+
data-testid="sort-dir"
|
|
582
|
+
@click="flipSortDir()"
|
|
583
|
+
>
|
|
584
|
+
<span>{{ sortKeyLabel }}</span>
|
|
585
|
+
<!-- The accessible name becomes "Modified, Descending". The arrow
|
|
586
|
+
carries the same fact for everyone who can see it, so it is not
|
|
587
|
+
repeated as a title. Ranked: "Relevance, <why it is closed>" —
|
|
588
|
+
the reason reaches a screen reader through the same channel it
|
|
589
|
+
reaches a mouse, rather than living only in a tooltip. -->
|
|
590
|
+
<span class="fe-sr-only">, {{ sortDirLabel }}</span>
|
|
591
|
+
<svg
|
|
592
|
+
v-if="!ranked"
|
|
593
|
+
class="fe-ficon fe-filterbar__sortarrow"
|
|
594
|
+
viewBox="0 0 24 24"
|
|
595
|
+
fill="none"
|
|
596
|
+
stroke="currentColor"
|
|
597
|
+
stroke-width="2"
|
|
598
|
+
stroke-linecap="round"
|
|
599
|
+
stroke-linejoin="round"
|
|
600
|
+
aria-hidden="true"
|
|
601
|
+
focusable="false"
|
|
602
|
+
>
|
|
603
|
+
<template v-if="sortAscending">
|
|
604
|
+
<path d="M12 19V5" />
|
|
605
|
+
<path d="M5 12l7-7 7 7" />
|
|
606
|
+
</template>
|
|
607
|
+
<template v-else>
|
|
608
|
+
<path d="M12 5v14" />
|
|
609
|
+
<path d="M19 12l-7 7-7-7" />
|
|
610
|
+
</template>
|
|
611
|
+
</svg>
|
|
612
|
+
</button>
|
|
613
|
+
<button
|
|
614
|
+
type="button"
|
|
615
|
+
class="fe-filterbar__sortpick"
|
|
616
|
+
:aria-label="ranked ? sortDirLabel : t('sort.by')"
|
|
617
|
+
:title="ranked ? sortDirLabel : t('sort.by')"
|
|
618
|
+
:disabled="ranked"
|
|
619
|
+
aria-haspopup="menu"
|
|
620
|
+
:aria-expanded="openPop === 'sort'"
|
|
621
|
+
data-testid="sort-menu"
|
|
622
|
+
@click="openSortMenu()"
|
|
623
|
+
>
|
|
624
|
+
<svg
|
|
625
|
+
class="fe-ficon fe-filterbar__caret"
|
|
626
|
+
viewBox="0 0 24 24"
|
|
627
|
+
fill="none"
|
|
628
|
+
stroke="currentColor"
|
|
629
|
+
stroke-width="2"
|
|
630
|
+
stroke-linecap="round"
|
|
631
|
+
stroke-linejoin="round"
|
|
632
|
+
aria-hidden="true"
|
|
633
|
+
focusable="false"
|
|
634
|
+
>
|
|
635
|
+
<path d="M7 10l5 5 5-5" />
|
|
636
|
+
</svg>
|
|
637
|
+
</button>
|
|
638
|
+
</div>
|
|
639
|
+
|
|
640
|
+
<!-- surucu:d1-actions — the listing's own menu, at the very end of the row.
|
|
641
|
+
⚠ VERTICAL dots, and the header's overflow is HORIZONTAL dots, which is
|
|
642
|
+
the whole distinction: in this product ⋮ means "act on this thing" (it
|
|
643
|
+
is the glyph on every file row) and ⋯ means "settings for the app". Two
|
|
644
|
+
menus one row apart would otherwise read as two doors to one place. -->
|
|
645
|
+
<button
|
|
646
|
+
v-if="full"
|
|
647
|
+
:ref="(el) => setAnchorEl('actions', el)"
|
|
648
|
+
type="button"
|
|
649
|
+
class="fe-filterbar__actions"
|
|
650
|
+
:aria-label="t('filter.actions')"
|
|
651
|
+
:title="t('filter.actions')"
|
|
652
|
+
aria-haspopup="menu"
|
|
653
|
+
:aria-expanded="openPop === 'actions'"
|
|
654
|
+
data-testid="listing-actions"
|
|
655
|
+
@click="toggle('actions')"
|
|
656
|
+
>
|
|
657
|
+
<svg
|
|
658
|
+
class="fe-ficon"
|
|
659
|
+
viewBox="0 0 24 24"
|
|
660
|
+
fill="none"
|
|
661
|
+
stroke="currentColor"
|
|
662
|
+
stroke-width="2"
|
|
663
|
+
stroke-linecap="round"
|
|
664
|
+
aria-hidden="true"
|
|
665
|
+
focusable="false"
|
|
666
|
+
>
|
|
667
|
+
<circle cx="12" cy="5.5" r="1.2" fill="currentColor" stroke="none" />
|
|
668
|
+
<circle cx="12" cy="12" r="1.2" fill="currentColor" stroke="none" />
|
|
669
|
+
<circle cx="12" cy="18.5" r="1.2" fill="currentColor" stroke="none" />
|
|
670
|
+
</svg>
|
|
671
|
+
</button>
|
|
672
|
+
|
|
214
673
|
<Teleport to="body">
|
|
215
674
|
<div
|
|
216
|
-
v-if="
|
|
675
|
+
v-if="openPop"
|
|
217
676
|
ref="panelEl"
|
|
218
677
|
class="fe-filterpop"
|
|
219
678
|
:class="{
|
|
220
679
|
'fe--theme-light': theme === 'light',
|
|
221
680
|
'fe--theme-dark': theme === 'dark',
|
|
681
|
+
'fe-filterpop--sort': openPop === 'sort',
|
|
682
|
+
'fe-filterpop--actions': openPop === 'actions',
|
|
222
683
|
}"
|
|
223
|
-
role="listbox"
|
|
684
|
+
:role="openPop === 'sort' || openPop === 'actions' ? 'menu' : 'listbox'"
|
|
224
685
|
:aria-label="popTitle"
|
|
225
686
|
:style="{ left: pos.x + 'px', top: pos.y + 'px' }"
|
|
226
687
|
>
|
|
227
|
-
<
|
|
228
|
-
v-for="
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
688
|
+
<template v-if="openPop === 'actions'">
|
|
689
|
+
<template v-for="a in actionRows" :key="a.key">
|
|
690
|
+
<hr v-if="a.divider" class="fe-filterpop__rule" aria-hidden="true" />
|
|
691
|
+
<button
|
|
692
|
+
v-else
|
|
693
|
+
type="button"
|
|
694
|
+
class="fe-filterpop__item"
|
|
695
|
+
role="menuitem"
|
|
696
|
+
:disabled="a.disabled"
|
|
697
|
+
:aria-disabled="a.disabled"
|
|
698
|
+
:data-active="'false'"
|
|
699
|
+
:data-testid="`listing-action-${a.key}`"
|
|
700
|
+
@click="runAction(a.key)"
|
|
701
|
+
>
|
|
702
|
+
<span>{{ a.label }}</span>
|
|
703
|
+
</button>
|
|
704
|
+
</template>
|
|
705
|
+
</template>
|
|
706
|
+
<template v-else-if="openPop === 'sort'">
|
|
707
|
+
<button
|
|
708
|
+
v-for="k in SORT_KEYS"
|
|
709
|
+
:key="k"
|
|
710
|
+
type="button"
|
|
711
|
+
class="fe-filterpop__item"
|
|
712
|
+
role="menuitem"
|
|
713
|
+
:data-active="sortKey === k ? 'true' : 'false'"
|
|
714
|
+
:data-testid="`sort-opt-${k}`"
|
|
715
|
+
@click="pickSortKey(k)"
|
|
716
|
+
>
|
|
717
|
+
<span>{{ t(`col.${k}`) }}</span>
|
|
718
|
+
</button>
|
|
719
|
+
</template>
|
|
720
|
+
<template v-else>
|
|
721
|
+
<button
|
|
722
|
+
v-for="opt in popOptions"
|
|
723
|
+
:key="opt"
|
|
724
|
+
type="button"
|
|
725
|
+
class="fe-filterpop__item"
|
|
726
|
+
role="option"
|
|
727
|
+
:aria-selected="popChecked(opt)"
|
|
728
|
+
:data-checked="popChecked(opt) ? 'true' : 'false'"
|
|
729
|
+
:data-active="popChecked(opt) ? 'true' : 'false'"
|
|
730
|
+
:data-testid="`filter-opt-${opt}`"
|
|
731
|
+
@click="popPick(opt)"
|
|
732
|
+
>
|
|
733
|
+
<span class="fe-filterpop__tick" aria-hidden="true">{{ popChecked(opt) ? '✓' : '' }}</span>
|
|
734
|
+
<span>{{ popLabel(opt) }}</span>
|
|
735
|
+
</button>
|
|
736
|
+
</template>
|
|
241
737
|
</div>
|
|
242
738
|
</Teleport>
|
|
243
739
|
</div>
|