@xleddyl/nuxt-cms 0.2.3 → 0.4.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 +8 -1
- package/dist/module.d.mts +3 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +113 -3
- package/dist/runtime/app/components/cms/BrandMark.vue +16 -1
- package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +2 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +34 -3
- package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +2 -0
- package/dist/runtime/app/components/cms/MediaDetail.vue +25 -13
- package/dist/runtime/app/components/cms/Search.d.vue.ts +7 -0
- package/dist/runtime/app/components/cms/Search.vue +347 -0
- package/dist/runtime/app/components/cms/Search.vue.d.ts +7 -0
- package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +2 -0
- package/dist/runtime/app/components/cms/VisualEditor.vue +21 -1
- package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +2 -0
- package/dist/runtime/app/components/cms/WelcomeModal.vue +7 -1
- package/dist/runtime/app/composables/cms-field-focus.d.ts +4 -0
- package/dist/runtime/app/composables/cms-field-focus.js +20 -0
- package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
- package/dist/runtime/app/layouts/cms-admin.vue +8 -3
- package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
- package/dist/runtime/app/pages/admin-collection.vue +3 -0
- package/dist/runtime/app/pages/admin-content.vue +3 -0
- package/dist/runtime/app/pages/admin-entry.vue +3 -0
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/server/api/collection.get.js +2 -4
- package/dist/runtime/server/api/media-download.get.d.ts +2 -0
- package/dist/runtime/server/api/media-download.get.js +90 -0
- package/dist/runtime/server/api/search.get.d.ts +3 -0
- package/dist/runtime/server/api/search.get.js +24 -0
- package/dist/runtime/server/utils/media-download.d.ts +1 -0
- package/dist/runtime/server/utils/media-download.js +8 -0
- package/dist/runtime/server/utils/media.d.ts +1 -0
- package/dist/runtime/server/utils/media.js +18 -3
- package/dist/runtime/server/utils/search.d.ts +20 -0
- package/dist/runtime/server/utils/search.js +233 -0
- package/dist/runtime/shared/search.d.ts +52 -0
- package/dist/runtime/shared/search.js +207 -0
- package/package.json +2 -2
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
ref="trigger"
|
|
4
|
+
type="button"
|
|
5
|
+
class="cms-search-trigger"
|
|
6
|
+
aria-haspopup="dialog"
|
|
7
|
+
:aria-expanded="open"
|
|
8
|
+
aria-keyshortcuts="Meta+K Control+K"
|
|
9
|
+
@click="show"
|
|
10
|
+
>
|
|
11
|
+
<CmsIcon name="magnifying-glass" class="cms-search-trigger-icon size-4 shrink-0" />
|
|
12
|
+
<span class="cms-search-trigger-text">Search</span>
|
|
13
|
+
<kbd class="cms-kbd">{{ shortcut }}</kbd>
|
|
14
|
+
</button>
|
|
15
|
+
|
|
16
|
+
<Teleport to="body">
|
|
17
|
+
<Transition name="cms-modal">
|
|
18
|
+
<div v-if="open" class="cms-scope cms-overlay cms-search-overlay" @click.self="close">
|
|
19
|
+
<div
|
|
20
|
+
class="cms-modal cms-search"
|
|
21
|
+
role="dialog"
|
|
22
|
+
aria-modal="true"
|
|
23
|
+
aria-labelledby="cms-search-label"
|
|
24
|
+
>
|
|
25
|
+
<div class="cms-search-bar">
|
|
26
|
+
<label id="cms-search-label" for="cms-search-input" class="sr-only">
|
|
27
|
+
Search the CMS
|
|
28
|
+
</label>
|
|
29
|
+
<CmsIcon name="magnifying-glass" class="cms-search-bar-icon size-5 shrink-0" />
|
|
30
|
+
<input
|
|
31
|
+
id="cms-search-input"
|
|
32
|
+
ref="input"
|
|
33
|
+
v-model="query"
|
|
34
|
+
type="text"
|
|
35
|
+
role="combobox"
|
|
36
|
+
class="cms-search-input"
|
|
37
|
+
placeholder="Search content, fields and pages"
|
|
38
|
+
autocomplete="off"
|
|
39
|
+
spellcheck="false"
|
|
40
|
+
aria-autocomplete="list"
|
|
41
|
+
aria-controls="cms-search-results"
|
|
42
|
+
:aria-expanded="hits.length > 0"
|
|
43
|
+
:aria-activedescendant="activeId"
|
|
44
|
+
@keydown="onKeydown"
|
|
45
|
+
/>
|
|
46
|
+
<CmsSpinner v-if="loading" />
|
|
47
|
+
<kbd class="cms-kbd">Esc</kbd>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="cms-search-body">
|
|
51
|
+
<p v-if="hint" class="cms-search-hint">{{ hint }}</p>
|
|
52
|
+
<div
|
|
53
|
+
id="cms-search-results"
|
|
54
|
+
role="listbox"
|
|
55
|
+
aria-labelledby="cms-search-label"
|
|
56
|
+
class="cms-search-results"
|
|
57
|
+
>
|
|
58
|
+
<div
|
|
59
|
+
v-for="(group, groupIndex) in groups"
|
|
60
|
+
:key="group.key"
|
|
61
|
+
role="group"
|
|
62
|
+
:aria-labelledby="`cms-search-group-${groupIndex}`"
|
|
63
|
+
class="cms-search-group"
|
|
64
|
+
>
|
|
65
|
+
<div :id="`cms-search-group-${groupIndex}`" class="cms-search-group-title">
|
|
66
|
+
<span>{{ group.label }}</span>
|
|
67
|
+
<span class="cms-search-group-kind">{{ KIND_LABELS[group.kind] }}</span>
|
|
68
|
+
</div>
|
|
69
|
+
<div
|
|
70
|
+
v-for="item in group.items"
|
|
71
|
+
:id="optionId(item.index)"
|
|
72
|
+
:key="item.index"
|
|
73
|
+
role="option"
|
|
74
|
+
class="cms-search-option"
|
|
75
|
+
:class="{ 'is-active': item.index === active }"
|
|
76
|
+
:aria-selected="item.index === active"
|
|
77
|
+
@mousemove="active = item.index"
|
|
78
|
+
@click="go(item.hit)"
|
|
79
|
+
>
|
|
80
|
+
<CmsIcon
|
|
81
|
+
:name="iconOf(item.hit)"
|
|
82
|
+
class="cms-search-option-icon size-4 shrink-0"
|
|
83
|
+
/>
|
|
84
|
+
<span class="cms-search-option-text">
|
|
85
|
+
<template v-if="isValueHit(item.hit)">
|
|
86
|
+
<span class="cms-search-option-head">
|
|
87
|
+
<span class="cms-search-option-title">
|
|
88
|
+
{{ item.hit.title ?? item.hit.entryLabel }}
|
|
89
|
+
</span>
|
|
90
|
+
<span v-if="item.hit.fieldLabel" class="cms-search-option-meta">
|
|
91
|
+
{{ item.hit.fieldLabel }}
|
|
92
|
+
</span>
|
|
93
|
+
<span v-if="item.hit.locale" class="cms-badge is-muted">
|
|
94
|
+
{{ item.hit.locale }}
|
|
95
|
+
</span>
|
|
96
|
+
</span>
|
|
97
|
+
<span class="cms-search-snippet">
|
|
98
|
+
{{ item.hit.snippet.before
|
|
99
|
+
}}<mark class="cms-search-mark">{{
|
|
100
|
+
item.hit.snippet.match
|
|
101
|
+
}}</mark
|
|
102
|
+
>{{ item.hit.snippet.after }}
|
|
103
|
+
</span>
|
|
104
|
+
</template>
|
|
105
|
+
<template v-else>
|
|
106
|
+
<span class="cms-search-option-title">
|
|
107
|
+
{{ item.hit.snippet.before
|
|
108
|
+
}}<mark class="cms-search-mark">{{
|
|
109
|
+
item.hit.snippet.match
|
|
110
|
+
}}</mark
|
|
111
|
+
>{{ item.hit.snippet.after }}
|
|
112
|
+
</span>
|
|
113
|
+
<span class="cms-search-option-meta">{{
|
|
114
|
+
labelMeta(item.hit)
|
|
115
|
+
}}</span>
|
|
116
|
+
</template>
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<p v-if="truncated" class="cms-search-hint">
|
|
122
|
+
Showing the first results. Type more to narrow them down.
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="cms-search-footer" aria-hidden="true">
|
|
127
|
+
<span><kbd class="cms-kbd">↑</kbd><kbd class="cms-kbd">↓</kbd> to move</span>
|
|
128
|
+
<span><kbd class="cms-kbd">↵</kbd> to open</span>
|
|
129
|
+
<span><kbd class="cms-kbd">Esc</kbd> to close</span>
|
|
130
|
+
</div>
|
|
131
|
+
<p class="sr-only" aria-live="polite">{{ status }}</p>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</Transition>
|
|
135
|
+
</Teleport>
|
|
136
|
+
</template>
|
|
137
|
+
|
|
138
|
+
<script setup>
|
|
139
|
+
import { computed, navigateTo, nextTick, onBeforeUnmount, onMounted, ref, watch } from "#imports";
|
|
140
|
+
import { isSearchableQuery, normalizeSearchQuery } from "../../../shared/search";
|
|
141
|
+
import { useCmsOverlay } from "../../composables/cms-overlay";
|
|
142
|
+
import { cmsApi } from "../../utils/api";
|
|
143
|
+
const DEBOUNCE_MS = 180;
|
|
144
|
+
const KIND_LABELS = {
|
|
145
|
+
collection: "Collection",
|
|
146
|
+
single: "Single",
|
|
147
|
+
content: "Content",
|
|
148
|
+
page: "Pages",
|
|
149
|
+
media: "Library"
|
|
150
|
+
};
|
|
151
|
+
const KIND_ICONS = {
|
|
152
|
+
collection: "square-3-stack-3d",
|
|
153
|
+
single: "document-text",
|
|
154
|
+
content: "newspaper",
|
|
155
|
+
page: "window",
|
|
156
|
+
media: "photo"
|
|
157
|
+
};
|
|
158
|
+
const TYPE_LABELS = {
|
|
159
|
+
entry: "Section",
|
|
160
|
+
page: "Page",
|
|
161
|
+
tab: "Tab",
|
|
162
|
+
field: "Field",
|
|
163
|
+
block: "Block",
|
|
164
|
+
value: "Value",
|
|
165
|
+
media: "Media"
|
|
166
|
+
};
|
|
167
|
+
const TYPE_ICONS = {
|
|
168
|
+
page: "window",
|
|
169
|
+
tab: "rectangle-group",
|
|
170
|
+
field: "tag",
|
|
171
|
+
block: "squares-2x2",
|
|
172
|
+
value: "bars-3-bottom-left",
|
|
173
|
+
media: "photo"
|
|
174
|
+
};
|
|
175
|
+
const emit = defineEmits(["open"]);
|
|
176
|
+
const open = ref(false);
|
|
177
|
+
const query = ref("");
|
|
178
|
+
const hits = ref([]);
|
|
179
|
+
const truncated = ref(false);
|
|
180
|
+
const loading = ref(false);
|
|
181
|
+
const failed = ref(false);
|
|
182
|
+
const searched = ref("");
|
|
183
|
+
const active = ref(0);
|
|
184
|
+
const shortcut = ref("Ctrl K");
|
|
185
|
+
const trigger = ref(null);
|
|
186
|
+
const input = ref(null);
|
|
187
|
+
let timer;
|
|
188
|
+
let request = 0;
|
|
189
|
+
let returnFocus = null;
|
|
190
|
+
const groups = computed(() => {
|
|
191
|
+
const map = /* @__PURE__ */ new Map();
|
|
192
|
+
hits.value.forEach((hit, index2) => {
|
|
193
|
+
const key = hit.collection ?? hit.kind;
|
|
194
|
+
let group = map.get(key);
|
|
195
|
+
if (!group) {
|
|
196
|
+
group = { key, label: hit.entryLabel, kind: hit.kind, items: [] };
|
|
197
|
+
map.set(key, group);
|
|
198
|
+
}
|
|
199
|
+
group.items.push({ hit, index: index2 });
|
|
200
|
+
});
|
|
201
|
+
const ordered = [...map.values()];
|
|
202
|
+
let index = 0;
|
|
203
|
+
for (const group of ordered) {
|
|
204
|
+
for (const item of group.items) item.index = index++;
|
|
205
|
+
}
|
|
206
|
+
return ordered;
|
|
207
|
+
});
|
|
208
|
+
const flat = computed(() => groups.value.flatMap((group) => group.items.map((item) => item.hit)));
|
|
209
|
+
const activeId = computed(() => flat.value.length ? optionId(active.value) : void 0);
|
|
210
|
+
const hint = computed(() => {
|
|
211
|
+
if (!isSearchableQuery(query.value)) return "Type at least 2 characters to search.";
|
|
212
|
+
if (failed.value) return "Search failed. Try again.";
|
|
213
|
+
if (!loading.value && searched.value && !flat.value.length) {
|
|
214
|
+
return `No results for "${searched.value}".`;
|
|
215
|
+
}
|
|
216
|
+
return "";
|
|
217
|
+
});
|
|
218
|
+
const status = computed(() => {
|
|
219
|
+
if (hint.value) return hint.value;
|
|
220
|
+
if (loading.value) return "Searching";
|
|
221
|
+
return `${flat.value.length} results`;
|
|
222
|
+
});
|
|
223
|
+
function optionId(index) {
|
|
224
|
+
return `cms-search-option-${index}`;
|
|
225
|
+
}
|
|
226
|
+
function isValueHit(hit) {
|
|
227
|
+
return hit.type === "value" || hit.type === "media";
|
|
228
|
+
}
|
|
229
|
+
function iconOf(hit) {
|
|
230
|
+
return hit.type === "entry" ? KIND_ICONS[hit.kind] : TYPE_ICONS[hit.type] ?? "tag";
|
|
231
|
+
}
|
|
232
|
+
function labelMeta(hit) {
|
|
233
|
+
const parts = [TYPE_LABELS[hit.type]];
|
|
234
|
+
if (hit.title) parts.push(hit.title);
|
|
235
|
+
if (hit.fieldLabel && hit.type !== "tab") parts.push(hit.fieldLabel);
|
|
236
|
+
if (hit.type === "page" && hit.id) parts.push(hit.to);
|
|
237
|
+
return parts.join(" \xB7 ");
|
|
238
|
+
}
|
|
239
|
+
async function run(term) {
|
|
240
|
+
const current = ++request;
|
|
241
|
+
loading.value = true;
|
|
242
|
+
failed.value = false;
|
|
243
|
+
try {
|
|
244
|
+
const response = await cmsApi("/api/cms/search", { query: { q: term } });
|
|
245
|
+
if (current !== request) return;
|
|
246
|
+
hits.value = response.hits;
|
|
247
|
+
truncated.value = response.truncated;
|
|
248
|
+
searched.value = response.query;
|
|
249
|
+
active.value = 0;
|
|
250
|
+
} catch {
|
|
251
|
+
if (current !== request) return;
|
|
252
|
+
hits.value = [];
|
|
253
|
+
truncated.value = false;
|
|
254
|
+
failed.value = true;
|
|
255
|
+
} finally {
|
|
256
|
+
if (current === request) loading.value = false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
watch(query, (value) => {
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
const term = normalizeSearchQuery(value);
|
|
262
|
+
if (!isSearchableQuery(term)) {
|
|
263
|
+
request++;
|
|
264
|
+
hits.value = [];
|
|
265
|
+
truncated.value = false;
|
|
266
|
+
loading.value = false;
|
|
267
|
+
failed.value = false;
|
|
268
|
+
searched.value = "";
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
timer = setTimeout(() => run(term), DEBOUNCE_MS);
|
|
272
|
+
});
|
|
273
|
+
watch(active, (index) => {
|
|
274
|
+
void nextTick(
|
|
275
|
+
() => document.getElementById(optionId(index))?.scrollIntoView({ block: "nearest" })
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
function focusInput() {
|
|
279
|
+
void nextTick(() => {
|
|
280
|
+
input.value?.focus();
|
|
281
|
+
input.value?.select();
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function show() {
|
|
285
|
+
if (open.value) {
|
|
286
|
+
focusInput();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
290
|
+
emit("open");
|
|
291
|
+
open.value = true;
|
|
292
|
+
}
|
|
293
|
+
function close() {
|
|
294
|
+
open.value = false;
|
|
295
|
+
}
|
|
296
|
+
const overlay = useCmsOverlay(close);
|
|
297
|
+
watch(open, (value) => {
|
|
298
|
+
if (value) {
|
|
299
|
+
overlay.activate();
|
|
300
|
+
focusInput();
|
|
301
|
+
} else {
|
|
302
|
+
overlay.deactivate();
|
|
303
|
+
const target = returnFocus ?? trigger.value;
|
|
304
|
+
returnFocus = null;
|
|
305
|
+
void nextTick(() => target?.focus());
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
function go(hit) {
|
|
309
|
+
if (!hit) return;
|
|
310
|
+
returnFocus = null;
|
|
311
|
+
close();
|
|
312
|
+
void navigateTo(hit.to);
|
|
313
|
+
}
|
|
314
|
+
function onKeydown(event) {
|
|
315
|
+
const count = flat.value.length;
|
|
316
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
if (!count) return;
|
|
319
|
+
const step = event.key === "ArrowDown" ? 1 : -1;
|
|
320
|
+
active.value = (active.value + step + count) % count;
|
|
321
|
+
} else if (event.key === "Home" && count && event.ctrlKey) {
|
|
322
|
+
event.preventDefault();
|
|
323
|
+
active.value = 0;
|
|
324
|
+
} else if (event.key === "End" && count && event.ctrlKey) {
|
|
325
|
+
event.preventDefault();
|
|
326
|
+
active.value = count - 1;
|
|
327
|
+
} else if (event.key === "Enter") {
|
|
328
|
+
event.preventDefault();
|
|
329
|
+
go(flat.value[active.value]);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function onGlobalKeydown(event) {
|
|
333
|
+
if (event.key.toLowerCase() !== "k" || event.altKey || event.shiftKey) return;
|
|
334
|
+
if (!event.metaKey && !event.ctrlKey) return;
|
|
335
|
+
event.preventDefault();
|
|
336
|
+
show();
|
|
337
|
+
}
|
|
338
|
+
onMounted(() => {
|
|
339
|
+
const platform = navigator.userAgentData?.platform ?? navigator.platform ?? navigator.userAgent;
|
|
340
|
+
if (/mac|iphone|ipad|ipod/i.test(platform)) shortcut.value = "\u2318K";
|
|
341
|
+
document.addEventListener("keydown", onGlobalKeydown);
|
|
342
|
+
});
|
|
343
|
+
onBeforeUnmount(() => {
|
|
344
|
+
clearTimeout(timer);
|
|
345
|
+
document.removeEventListener("keydown", onGlobalKeydown);
|
|
346
|
+
});
|
|
347
|
+
</script>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
2
|
+
open: () => any;
|
|
3
|
+
}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
|
|
4
|
+
onOpen?: (() => any) | undefined;
|
|
5
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
6
|
+
declare const _default: typeof __VLS_export;
|
|
7
|
+
export default _default;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { FieldConfig } from '#nuxt-cms';
|
|
2
|
+
import type { CmsSearchFocus } from '../../../shared/search.js';
|
|
2
3
|
type __VLS_Props = {
|
|
3
4
|
entryName: string;
|
|
4
5
|
fieldKey: string;
|
|
5
6
|
fields: Record<string, FieldConfig>;
|
|
6
7
|
title: string;
|
|
7
8
|
content?: boolean;
|
|
9
|
+
focus?: CmsSearchFocus | null;
|
|
8
10
|
};
|
|
9
11
|
declare function validate(): boolean;
|
|
10
12
|
type __VLS_ModelProps = {
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
|
|
76
76
|
<div
|
|
77
77
|
:id="`cms-editor-tabpanel-${activeTab}`"
|
|
78
|
+
ref="panel"
|
|
78
79
|
class="cms-editor-panel-body"
|
|
79
80
|
role="tabpanel"
|
|
80
81
|
:aria-labelledby="tabList.length > 1 ? `cms-editor-tab-${activeTab}` : void 0"
|
|
@@ -213,6 +214,8 @@
|
|
|
213
214
|
:icon="fieldIcon(fields[key])"
|
|
214
215
|
:name="key"
|
|
215
216
|
:required="fields[key].required"
|
|
217
|
+
:data-cms-field="key"
|
|
218
|
+
:class="{ 'is-search-focus': highlighted === key }"
|
|
216
219
|
>
|
|
217
220
|
<CmsFieldInput
|
|
218
221
|
:model-value="state[key]"
|
|
@@ -267,6 +270,7 @@ import {
|
|
|
267
270
|
toggleBlockHidden
|
|
268
271
|
} from "../../../shared/preview";
|
|
269
272
|
import { useCmsConfirm } from "../../composables/cms-confirm";
|
|
273
|
+
import { useCmsFieldFocus } from "../../composables/cms-field-focus";
|
|
270
274
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
271
275
|
import { entryFormLayout, useCmsSettingsState } from "../../composables/cms-settings";
|
|
272
276
|
import { CMS_FORM_ERRORS, fieldIcon } from "../../utils/ui";
|
|
@@ -280,7 +284,8 @@ const props = defineProps({
|
|
|
280
284
|
fieldKey: { type: String, required: true },
|
|
281
285
|
fields: { type: Object, required: true },
|
|
282
286
|
title: { type: String, required: true },
|
|
283
|
-
content: { type: Boolean, required: false }
|
|
287
|
+
content: { type: Boolean, required: false },
|
|
288
|
+
focus: { type: [Object, null], required: false }
|
|
284
289
|
});
|
|
285
290
|
const state = defineModel("state", { type: Object, ...{ required: true } });
|
|
286
291
|
const runtime = useCmsRuntime();
|
|
@@ -465,9 +470,24 @@ async function loadLibrary() {
|
|
|
465
470
|
library.value = /* @__PURE__ */ new Map();
|
|
466
471
|
}
|
|
467
472
|
}
|
|
473
|
+
const panel = ref(null);
|
|
474
|
+
const { highlighted, reveal } = useCmsFieldFocus();
|
|
475
|
+
function applyFocus(focus) {
|
|
476
|
+
if (focus.locale && i18n.locales.includes(focus.locale)) locale.value = focus.locale;
|
|
477
|
+
const key = focus.field;
|
|
478
|
+
if (!key || !Object.hasOwn(props.fields, key)) return;
|
|
479
|
+
if (key === props.fieldKey) {
|
|
480
|
+
activeTab.value = "block";
|
|
481
|
+
if (focus.block != null && focus.block < blocks.value.length) select(focus.block);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
activeTab.value = tabOfKey(key);
|
|
485
|
+
reveal(() => panel.value, key);
|
|
486
|
+
}
|
|
468
487
|
onMounted(() => {
|
|
469
488
|
window.addEventListener("message", onMessage);
|
|
470
489
|
void loadLibrary();
|
|
490
|
+
if (props.focus) applyFocus(props.focus);
|
|
471
491
|
});
|
|
472
492
|
onBeforeUnmount(() => {
|
|
473
493
|
window.removeEventListener("message", onMessage);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { FieldConfig } from '#nuxt-cms';
|
|
2
|
+
import type { CmsSearchFocus } from '../../../shared/search.js';
|
|
2
3
|
type __VLS_Props = {
|
|
3
4
|
entryName: string;
|
|
4
5
|
fieldKey: string;
|
|
5
6
|
fields: Record<string, FieldConfig>;
|
|
6
7
|
title: string;
|
|
7
8
|
content?: boolean;
|
|
9
|
+
focus?: CmsSearchFocus | null;
|
|
8
10
|
};
|
|
9
11
|
declare function validate(): boolean;
|
|
10
12
|
type __VLS_ModelProps = {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<CmsModal
|
|
2
|
+
<CmsModal
|
|
3
|
+
:open="open"
|
|
4
|
+
size="sm"
|
|
5
|
+
:title="`Welcome to ${cmsBrand.title}`"
|
|
6
|
+
@update:open="(v) => !v && keep()"
|
|
7
|
+
>
|
|
3
8
|
<template #body>
|
|
4
9
|
<CmsForm :state="state" @submit="save">
|
|
5
10
|
<p class="cms-settings-hint">
|
|
@@ -32,6 +37,7 @@
|
|
|
32
37
|
|
|
33
38
|
<script setup>
|
|
34
39
|
import { MIN_PASSWORD_LENGTH } from "#nuxt-cms";
|
|
40
|
+
import { cmsBrand } from "#cms-brand";
|
|
35
41
|
import { computed, ref } from "#imports";
|
|
36
42
|
import { useCmsAccount } from "../../composables/cms-account";
|
|
37
43
|
import { useCmsToast } from "../../composables/cms-toast";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { nextTick, onBeforeUnmount, ref } from "#imports";
|
|
2
|
+
const HIGHLIGHT_MS = 2400;
|
|
3
|
+
export function useCmsFieldFocus() {
|
|
4
|
+
const highlighted = ref(null);
|
|
5
|
+
let timer;
|
|
6
|
+
function reveal(root, key) {
|
|
7
|
+
void nextTick(() => {
|
|
8
|
+
const element = root()?.querySelector(`[data-cms-field="${CSS.escape(key)}"]`);
|
|
9
|
+
if (!element) return;
|
|
10
|
+
element.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
11
|
+
highlighted.value = key;
|
|
12
|
+
clearTimeout(timer);
|
|
13
|
+
timer = setTimeout(() => {
|
|
14
|
+
highlighted.value = null;
|
|
15
|
+
}, HIGHLIGHT_MS);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
onBeforeUnmount(() => clearTimeout(timer));
|
|
19
|
+
return { highlighted, reveal };
|
|
20
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
declare var
|
|
1
|
+
declare var __VLS_41: {};
|
|
2
2
|
type __VLS_Slots = {} & {
|
|
3
|
-
default?: (props: typeof
|
|
3
|
+
default?: (props: typeof __VLS_41) => any;
|
|
4
4
|
};
|
|
5
5
|
declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
6
6
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
<div class="cms-sidebar-brand">
|
|
6
6
|
<CmsBrandMark />
|
|
7
7
|
<div class="cms-brand-text">
|
|
8
|
-
<span class="cms-brand-name">
|
|
9
|
-
<span class="cms-brand-meta">
|
|
8
|
+
<span class="cms-brand-name">{{ cmsBrand.title }}</span>
|
|
9
|
+
<span v-if="cmsBrand.subtitle" class="cms-brand-meta">{{
|
|
10
|
+
cmsBrand.subtitle
|
|
11
|
+
}}</span>
|
|
10
12
|
</div>
|
|
11
13
|
</div>
|
|
12
14
|
|
|
15
|
+
<CmsSearch @open="navOpen = false" />
|
|
16
|
+
|
|
13
17
|
<nav class="cms-sidebar-nav">
|
|
14
18
|
<div v-for="group in groups" :key="group.title" class="cms-sidebar-group">
|
|
15
19
|
<div class="cms-label">
|
|
@@ -62,7 +66,7 @@
|
|
|
62
66
|
@click="navOpen = true"
|
|
63
67
|
/>
|
|
64
68
|
<CmsBrandMark />
|
|
65
|
-
<span class="cms-brand-name">
|
|
69
|
+
<span class="cms-brand-name">{{ cmsBrand.title }}</span>
|
|
66
70
|
</header>
|
|
67
71
|
<div class="cms-main-inner">
|
|
68
72
|
<slot />
|
|
@@ -80,6 +84,7 @@
|
|
|
80
84
|
<script setup>
|
|
81
85
|
import { computed, navigateTo, ref, useRoute, useUserSession, watch } from "#imports";
|
|
82
86
|
import cmsConfig from "#cms-config";
|
|
87
|
+
import { cmsBrand } from "#cms-brand";
|
|
83
88
|
import { useCmsTheme } from "../composables/cms-theme";
|
|
84
89
|
import { useCmsAccount } from "../composables/cms-account";
|
|
85
90
|
import { loadCmsSettings } from "../composables/cms-settings";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
declare var
|
|
1
|
+
declare var __VLS_41: {};
|
|
2
2
|
type __VLS_Slots = {} & {
|
|
3
|
-
default?: (props: typeof
|
|
3
|
+
default?: (props: typeof __VLS_41) => any;
|
|
4
4
|
};
|
|
5
5
|
declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
6
6
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
:entry-name="name"
|
|
36
36
|
:form-id="FORM_ID"
|
|
37
37
|
:loading="saving"
|
|
38
|
+
:focus="focus"
|
|
38
39
|
@submit="saveSingle"
|
|
39
40
|
/>
|
|
40
41
|
</div>
|
|
@@ -219,6 +220,7 @@ import {
|
|
|
219
220
|
watch
|
|
220
221
|
} from "#imports";
|
|
221
222
|
import cmsConfig from "#cms-config";
|
|
223
|
+
import { readSearchFocus } from "../../shared/search";
|
|
222
224
|
import { useCmsConfirm } from "../composables/cms-confirm";
|
|
223
225
|
import { useCmsRuntime } from "../composables/cms-runtime";
|
|
224
226
|
import { entryListColumns, useCmsSettingsState } from "../composables/cms-settings";
|
|
@@ -234,6 +236,7 @@ definePageMeta({
|
|
|
234
236
|
const FORM_ID = "cms-single-form";
|
|
235
237
|
const route = useRoute();
|
|
236
238
|
const toast = useCmsToast();
|
|
239
|
+
const focus = readSearchFocus(route.query);
|
|
237
240
|
const name = route.params.collection;
|
|
238
241
|
const config = cmsConfig[name];
|
|
239
242
|
if (!config) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
:field-key="CONTENT_BODY_FIELD"
|
|
7
7
|
:fields="config.fields"
|
|
8
8
|
:title="headerTitle"
|
|
9
|
+
:focus="focus"
|
|
9
10
|
content
|
|
10
11
|
>
|
|
11
12
|
<template #start>
|
|
@@ -120,6 +121,7 @@ import {
|
|
|
120
121
|
watch
|
|
121
122
|
} from "#imports";
|
|
122
123
|
import cmsConfig from "#cms-config";
|
|
124
|
+
import { readSearchFocus } from "../../shared/search";
|
|
123
125
|
import { useCmsConfirm } from "../composables/cms-confirm";
|
|
124
126
|
import { useCmsRuntime } from "../composables/cms-runtime";
|
|
125
127
|
import { useCmsToast } from "../composables/cms-toast";
|
|
@@ -133,6 +135,7 @@ definePageMeta({
|
|
|
133
135
|
const route = useRoute();
|
|
134
136
|
const toast = useCmsToast();
|
|
135
137
|
const confirmAction = useCmsConfirm();
|
|
138
|
+
const focus = readSearchFocus(route.query);
|
|
136
139
|
const { i18n } = useCmsRuntime();
|
|
137
140
|
const name = String(route.meta.cmsEntry ?? route.path.split("/")[2] ?? "");
|
|
138
141
|
const config = cmsConfig[name];
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
:drafts="drafts"
|
|
28
28
|
:form-id="FORM_ID"
|
|
29
29
|
:loading="saving"
|
|
30
|
+
:focus="focus"
|
|
30
31
|
@submit="save"
|
|
31
32
|
@error="revertStatus"
|
|
32
33
|
/>
|
|
@@ -49,6 +50,7 @@ import {
|
|
|
49
50
|
watch
|
|
50
51
|
} from "#imports";
|
|
51
52
|
import cmsConfig from "#cms-config";
|
|
53
|
+
import { readSearchFocus } from "../../shared/search";
|
|
52
54
|
import { useCmsConfirm } from "../composables/cms-confirm";
|
|
53
55
|
import { useCmsToast } from "../composables/cms-toast";
|
|
54
56
|
import { cmsApi } from "../utils/api";
|
|
@@ -62,6 +64,7 @@ definePageMeta({
|
|
|
62
64
|
});
|
|
63
65
|
const route = useRoute();
|
|
64
66
|
const toast = useCmsToast();
|
|
67
|
+
const focus = readSearchFocus(route.query);
|
|
65
68
|
const name = route.params.collection;
|
|
66
69
|
const config = cmsConfig[name];
|
|
67
70
|
if (!config || config.kind !== "collection" && config.kind !== "page") {
|