@xleddyl/nuxt-cms 0.1.60 → 0.2.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/dist/module.json +1 -1
- package/dist/module.mjs +137 -9
- package/dist/runtime/app/components/cms/BlocksField.vue +86 -10
- package/dist/runtime/app/components/cms/BrandMark.d.vue.ts +6 -0
- package/dist/runtime/app/components/cms/BrandMark.vue +19 -0
- package/dist/runtime/app/components/cms/BrandMark.vue.d.ts +6 -0
- package/dist/runtime/app/components/cms/Checkbox.d.vue.ts +11 -0
- package/dist/runtime/app/components/cms/Checkbox.vue +21 -0
- package/dist/runtime/app/components/cms/Checkbox.vue.d.ts +11 -0
- package/dist/runtime/app/components/cms/EmptyState.vue +4 -2
- package/dist/runtime/app/components/cms/EntryDrawer.vue +1 -0
- package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +1 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +105 -30
- package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +1 -0
- package/dist/runtime/app/components/cms/FieldInput.vue +20 -5
- package/dist/runtime/app/components/cms/FormField.d.vue.ts +5 -3
- package/dist/runtime/app/components/cms/FormField.vue +8 -2
- package/dist/runtime/app/components/cms/FormField.vue.d.ts +5 -3
- package/dist/runtime/app/components/cms/Icon.vue +1 -1
- package/dist/runtime/app/components/cms/MediaDetail.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/MediaDetail.vue +169 -0
- package/dist/runtime/app/components/cms/MediaDetail.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/MediaField.vue +9 -2
- package/dist/runtime/app/components/cms/MediaFolderPicker.d.vue.ts +2 -2
- package/dist/runtime/app/components/cms/MediaFolderPicker.vue +55 -54
- package/dist/runtime/app/components/cms/MediaFolderPicker.vue.d.ts +2 -2
- package/dist/runtime/app/components/cms/MediaGallery.d.vue.ts +8 -2
- package/dist/runtime/app/components/cms/MediaGallery.vue +927 -336
- package/dist/runtime/app/components/cms/MediaGallery.vue.d.ts +8 -2
- package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +1 -0
- package/dist/runtime/app/components/cms/MediaUpload.vue +76 -139
- package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +1 -0
- package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +5 -3
- package/dist/runtime/app/components/cms/PageHeader.vue +16 -6
- package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +5 -3
- package/dist/runtime/app/components/cms/PasswordInput.d.vue.ts +17 -0
- package/dist/runtime/app/components/cms/PasswordInput.vue +77 -0
- package/dist/runtime/app/components/cms/PasswordInput.vue.d.ts +17 -0
- package/dist/runtime/app/components/cms/SelectMenu.vue +3 -5
- package/dist/runtime/app/components/cms/SettingsAccount.d.vue.ts +3 -0
- package/dist/runtime/app/components/cms/SettingsAccount.vue +138 -0
- package/dist/runtime/app/components/cms/SettingsAccount.vue.d.ts +3 -0
- package/dist/runtime/app/components/cms/SettingsColumns.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsColumns.vue +102 -0
- package/dist/runtime/app/components/cms/SettingsColumns.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsEntry.d.vue.ts +6 -0
- package/dist/runtime/app/components/cms/SettingsEntry.vue +133 -0
- package/dist/runtime/app/components/cms/SettingsEntry.vue.d.ts +6 -0
- package/dist/runtime/app/components/cms/SettingsLayout.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsLayout.vue +177 -0
- package/dist/runtime/app/components/cms/SettingsLayout.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsModal.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsModal.vue +179 -0
- package/dist/runtime/app/components/cms/SettingsModal.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsUsers.d.vue.ts +3 -0
- package/dist/runtime/app/components/cms/SettingsUsers.vue +270 -0
- package/dist/runtime/app/components/cms/SettingsUsers.vue.d.ts +3 -0
- package/dist/runtime/app/components/cms/Spinner.vue +2 -2
- package/dist/runtime/app/components/cms/Table.d.vue.ts +7 -2
- package/dist/runtime/app/components/cms/Table.vue +38 -1
- package/dist/runtime/app/components/cms/Table.vue.d.ts +7 -2
- package/dist/runtime/app/components/cms/Toaster.vue +1 -1
- package/dist/runtime/app/components/cms/WelcomeModal.d.vue.ts +3 -0
- package/dist/runtime/app/components/cms/WelcomeModal.vue +69 -0
- package/dist/runtime/app/components/cms/WelcomeModal.vue.d.ts +3 -0
- package/dist/runtime/app/composables/cms-account.d.ts +11 -0
- package/dist/runtime/app/composables/cms-account.js +23 -0
- package/dist/runtime/app/composables/cms-media-library.d.ts +75 -0
- package/dist/runtime/app/composables/cms-media-library.js +151 -0
- package/dist/runtime/app/composables/cms-media-uploader.d.ts +22 -0
- package/dist/runtime/app/composables/cms-media-uploader.js +244 -0
- package/dist/runtime/app/composables/cms-settings.d.ts +13 -0
- package/dist/runtime/app/composables/cms-settings.js +59 -0
- package/dist/runtime/app/composables/cms-theme.d.ts +9 -0
- package/dist/runtime/app/composables/cms-theme.js +27 -0
- package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
- package/dist/runtime/app/layouts/cms-admin.vue +71 -17
- package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
- package/dist/runtime/app/pages/admin-collection.vue +127 -101
- package/dist/runtime/app/pages/admin-entry.vue +1 -0
- package/dist/runtime/app/pages/admin-login.vue +18 -3
- package/dist/runtime/app/pages/admin-media.vue +19 -3
- package/dist/runtime/app/utils/ui.d.ts +2 -0
- package/dist/runtime/app/utils/ui.js +18 -0
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/server/api/account-password.put.d.ts +4 -0
- package/dist/runtime/server/api/account-password.put.js +38 -0
- package/dist/runtime/server/api/account-welcome.post.d.ts +4 -0
- package/dist/runtime/server/api/account-welcome.post.js +14 -0
- package/dist/runtime/server/api/account.put.d.ts +3 -0
- package/dist/runtime/server/api/account.put.js +25 -0
- package/dist/runtime/server/api/media-delete.post.d.ts +4 -0
- package/dist/runtime/server/api/media-delete.post.js +23 -0
- package/dist/runtime/server/api/media-folder.delete.js +6 -16
- package/dist/runtime/server/api/media-folder.patch.d.ts +5 -0
- package/dist/runtime/server/api/media-folder.patch.js +68 -0
- package/dist/runtime/server/api/media-folder.post.js +3 -15
- package/dist/runtime/server/api/media-move.post.d.ts +5 -0
- package/dist/runtime/server/api/media-move.post.js +28 -0
- package/dist/runtime/server/api/media-usage.post.d.ts +5 -0
- package/dist/runtime/server/api/media-usage.post.js +15 -0
- package/dist/runtime/server/api/settings.delete.d.ts +4 -0
- package/dist/runtime/server/api/settings.delete.js +12 -0
- package/dist/runtime/server/api/settings.get.d.ts +3 -0
- package/dist/runtime/server/api/settings.get.js +15 -0
- package/dist/runtime/server/api/settings.put.d.ts +3 -0
- package/dist/runtime/server/api/settings.put.js +14 -0
- package/dist/runtime/server/api/user.delete.d.ts +4 -0
- package/dist/runtime/server/api/user.delete.js +12 -0
- package/dist/runtime/server/api/user.put.d.ts +3 -0
- package/dist/runtime/server/api/user.put.js +27 -0
- package/dist/runtime/server/api/users.get.d.ts +3 -0
- package/dist/runtime/server/api/users.get.js +11 -0
- package/dist/runtime/server/api/users.post.d.ts +3 -0
- package/dist/runtime/server/api/users.post.js +32 -0
- package/dist/runtime/server/plugins/session-validate.d.ts +2 -0
- package/dist/runtime/server/plugins/session-validate.js +13 -0
- package/dist/runtime/server/routes/auth/login.post.js +18 -8
- package/dist/runtime/server/utils/media-folders.d.ts +86 -0
- package/dist/runtime/server/utils/media-folders.js +37 -0
- package/dist/runtime/server/utils/media-references.d.ts +7 -1
- package/dist/runtime/server/utils/media-references.js +40 -0
- package/dist/runtime/server/utils/media-usage.d.ts +2 -0
- package/dist/runtime/server/utils/media-usage.js +18 -0
- package/dist/runtime/server/utils/password.d.ts +3 -0
- package/dist/runtime/server/utils/password.js +31 -0
- package/dist/runtime/server/utils/require-admin.d.ts +6 -2
- package/dist/runtime/server/utils/require-admin.js +33 -28
- package/dist/runtime/server/utils/same-origin.d.ts +2 -0
- package/dist/runtime/server/utils/same-origin.js +24 -0
- package/dist/runtime/server/utils/settings.d.ts +16 -0
- package/dist/runtime/server/utils/settings.js +31 -0
- package/dist/runtime/server/utils/users.d.ts +15 -0
- package/dist/runtime/server/utils/users.js +34 -0
- package/dist/runtime/shared/index.d.ts +29 -1
- package/dist/runtime/shared/index.js +36 -2
- package/dist/runtime/shared/layout.d.ts +34 -0
- package/dist/runtime/shared/layout.js +130 -0
- package/dist/runtime/shared/users.d.ts +18 -0
- package/dist/runtime/shared/users.js +1 -0
- package/dist/runtime/shared/validation.js +5 -4
- package/package.json +3 -3
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cms-settings-columns">
|
|
3
|
+
<p class="cms-settings-hint">
|
|
4
|
+
Choose the columns of the table. Drag the visible columns to change their order.
|
|
5
|
+
</p>
|
|
6
|
+
<ul class="cms-settings-list">
|
|
7
|
+
<li
|
|
8
|
+
v-for="(key, index) in model"
|
|
9
|
+
:key="key"
|
|
10
|
+
class="cms-settings-item is-draggable"
|
|
11
|
+
:class="{
|
|
12
|
+
'is-dragging': dragging === key,
|
|
13
|
+
'is-drop-before': dropIndex === index && dragging !== key
|
|
14
|
+
}"
|
|
15
|
+
draggable="true"
|
|
16
|
+
@dragstart="onDragStart($event, key)"
|
|
17
|
+
@dragover.prevent="dropIndex = index"
|
|
18
|
+
@drop.prevent="onDrop(index)"
|
|
19
|
+
@dragend="onDragEnd"
|
|
20
|
+
>
|
|
21
|
+
<CmsIcon name="bars-2" class="cms-settings-grip size-4" />
|
|
22
|
+
<CmsIcon :name="iconOf(key)" class="cms-settings-item-icon size-4" />
|
|
23
|
+
<span class="cms-settings-item-label">{{ labelOf(key) }}</span>
|
|
24
|
+
<CmsSwitch
|
|
25
|
+
:model-value="true"
|
|
26
|
+
:aria-label="`Hide ${labelOf(key)}`"
|
|
27
|
+
:disabled="model.length === 1"
|
|
28
|
+
@update:model-value="hide(key)"
|
|
29
|
+
/>
|
|
30
|
+
</li>
|
|
31
|
+
<li
|
|
32
|
+
v-if="dragging"
|
|
33
|
+
class="cms-settings-drop-end"
|
|
34
|
+
:class="{ 'is-over': dropIndex === model.length }"
|
|
35
|
+
@dragover.prevent="dropIndex = model.length"
|
|
36
|
+
@drop.prevent="onDrop(model.length)"
|
|
37
|
+
/>
|
|
38
|
+
</ul>
|
|
39
|
+
|
|
40
|
+
<template v-if="hidden.length">
|
|
41
|
+
<p class="cms-settings-subtitle">Hidden</p>
|
|
42
|
+
<ul class="cms-settings-list">
|
|
43
|
+
<li v-for="key in hidden" :key="key" class="cms-settings-item is-muted">
|
|
44
|
+
<CmsIcon :name="iconOf(key)" class="cms-settings-item-icon size-4" />
|
|
45
|
+
<span class="cms-settings-item-label">{{ labelOf(key) }}</span>
|
|
46
|
+
<CmsSwitch
|
|
47
|
+
:model-value="false"
|
|
48
|
+
:aria-label="`Show ${labelOf(key)}`"
|
|
49
|
+
@update:model-value="show(key)"
|
|
50
|
+
/>
|
|
51
|
+
</li>
|
|
52
|
+
</ul>
|
|
53
|
+
</template>
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<script setup>
|
|
58
|
+
import { computed, ref } from "#imports";
|
|
59
|
+
import { fieldIcon } from "../../utils/ui";
|
|
60
|
+
const props = defineProps({
|
|
61
|
+
fields: { type: Object, required: true },
|
|
62
|
+
extra: { type: Array, required: true }
|
|
63
|
+
});
|
|
64
|
+
const model = defineModel({ type: Array, ...{ required: true } });
|
|
65
|
+
const available = computed(() => [...Object.keys(props.fields), ...props.extra]);
|
|
66
|
+
const hidden = computed(() => available.value.filter((key) => !model.value.includes(key)));
|
|
67
|
+
function labelOf(key) {
|
|
68
|
+
return props.fields[key]?.label ?? (key === "status" ? "Status" : key);
|
|
69
|
+
}
|
|
70
|
+
function iconOf(key) {
|
|
71
|
+
const field = props.fields[key];
|
|
72
|
+
return field ? fieldIcon(field) : "check-circle";
|
|
73
|
+
}
|
|
74
|
+
function hide(key) {
|
|
75
|
+
if (model.value.length === 1) return;
|
|
76
|
+
model.value = model.value.filter((k) => k !== key);
|
|
77
|
+
}
|
|
78
|
+
function show(key) {
|
|
79
|
+
model.value = [...model.value, key];
|
|
80
|
+
}
|
|
81
|
+
const dragging = ref(null);
|
|
82
|
+
const dropIndex = ref(null);
|
|
83
|
+
function onDragStart(event, key) {
|
|
84
|
+
dragging.value = key;
|
|
85
|
+
event.dataTransfer?.setData("text/plain", key);
|
|
86
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
|
87
|
+
}
|
|
88
|
+
function onDrop(index) {
|
|
89
|
+
const key = dragging.value;
|
|
90
|
+
if (key) {
|
|
91
|
+
const next = model.value.filter((k) => k !== key);
|
|
92
|
+
const from = model.value.indexOf(key);
|
|
93
|
+
next.splice(from < index ? index - 1 : index, 0, key);
|
|
94
|
+
model.value = next;
|
|
95
|
+
}
|
|
96
|
+
onDragEnd();
|
|
97
|
+
}
|
|
98
|
+
function onDragEnd() {
|
|
99
|
+
dragging.value = null;
|
|
100
|
+
dropIndex.value = null;
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FieldConfig } from '#nuxt-cms';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
fields: Record<string, FieldConfig>;
|
|
4
|
+
extra: string[];
|
|
5
|
+
};
|
|
6
|
+
type __VLS_ModelProps = {
|
|
7
|
+
modelValue: string[];
|
|
8
|
+
};
|
|
9
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
+
"update:modelValue": (value: string[]) => any;
|
|
12
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
13
|
+
"onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
|
|
14
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
15
|
+
declare const _default: typeof __VLS_export;
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
name: string;
|
|
3
|
+
};
|
|
4
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
5
|
+
declare const _default: typeof __VLS_export;
|
|
6
|
+
export default _default;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cms-settings-entry">
|
|
3
|
+
<div class="cms-settings-entry-header">
|
|
4
|
+
<div class="cms-settings-heading">
|
|
5
|
+
<h2 class="cms-title cms-title-sm">{{ entry.label }}</h2>
|
|
6
|
+
<p class="cms-settings-hint">{{ KIND_LABELS[entry.kind] }}</p>
|
|
7
|
+
</div>
|
|
8
|
+
<span class="cms-settings-status" :class="`is-${status}`" aria-live="polite">
|
|
9
|
+
{{ STATUS_LABELS[status] }}
|
|
10
|
+
</span>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div v-if="parts.length > 1" class="cms-tabs" role="tablist">
|
|
14
|
+
<button
|
|
15
|
+
v-for="part in parts"
|
|
16
|
+
:key="part.id"
|
|
17
|
+
type="button"
|
|
18
|
+
role="tab"
|
|
19
|
+
class="cms-tab"
|
|
20
|
+
:class="{ 'is-active': activePart === part.id }"
|
|
21
|
+
:aria-selected="activePart === part.id"
|
|
22
|
+
@click="activePart = part.id"
|
|
23
|
+
>
|
|
24
|
+
{{ part.label }}
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<CmsSettingsColumns
|
|
29
|
+
v-if="activePart === 'columns'"
|
|
30
|
+
:model-value="columns"
|
|
31
|
+
:fields="entry.fields"
|
|
32
|
+
:extra="listExtraColumns(name)"
|
|
33
|
+
@update:model-value="saveColumns"
|
|
34
|
+
/>
|
|
35
|
+
<CmsSettingsLayout
|
|
36
|
+
v-else
|
|
37
|
+
:model-value="layout"
|
|
38
|
+
:fields="fields"
|
|
39
|
+
:tabs="entry.tabs"
|
|
40
|
+
@update:model-value="saveLayout"
|
|
41
|
+
/>
|
|
42
|
+
|
|
43
|
+
<div class="cms-settings-footer">
|
|
44
|
+
<CmsButton
|
|
45
|
+
label="Reset to default"
|
|
46
|
+
icon="arrow-uturn-left"
|
|
47
|
+
variant="ghost"
|
|
48
|
+
color="neutral"
|
|
49
|
+
size="sm"
|
|
50
|
+
:disabled="!isCustom"
|
|
51
|
+
@click="resetPart"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<script setup>
|
|
58
|
+
import { computed, ref, watch } from "#imports";
|
|
59
|
+
import cmsConfig from "#cms-config";
|
|
60
|
+
import {
|
|
61
|
+
entryFields,
|
|
62
|
+
entryFormLayout,
|
|
63
|
+
entryListColumns,
|
|
64
|
+
listExtraColumns,
|
|
65
|
+
useCmsSettings
|
|
66
|
+
} from "../../composables/cms-settings";
|
|
67
|
+
import { useCmsToast } from "../../composables/cms-toast";
|
|
68
|
+
import { errorMessage } from "../../utils/ui";
|
|
69
|
+
const KIND_LABELS = {
|
|
70
|
+
collection: "Collection",
|
|
71
|
+
single: "Single",
|
|
72
|
+
page: "Pages"
|
|
73
|
+
};
|
|
74
|
+
const STATUS_LABELS = {
|
|
75
|
+
idle: "Changes save automatically",
|
|
76
|
+
saving: "Saving\u2026",
|
|
77
|
+
saved: "Saved",
|
|
78
|
+
error: "Not saved"
|
|
79
|
+
};
|
|
80
|
+
const props = defineProps({
|
|
81
|
+
name: { type: String, required: true }
|
|
82
|
+
});
|
|
83
|
+
const { settings, save, reset } = useCmsSettings();
|
|
84
|
+
const toast = useCmsToast();
|
|
85
|
+
const entry = computed(() => cmsConfig[props.name]);
|
|
86
|
+
const fields = computed(() => entryFields(props.name));
|
|
87
|
+
const parts = computed(() => [
|
|
88
|
+
...entry.value.kind === "collection" ? [{ id: "columns", label: "Table columns" }] : [],
|
|
89
|
+
{ id: "layout", label: "Form layout" }
|
|
90
|
+
]);
|
|
91
|
+
const activePart = ref(parts.value[0].id);
|
|
92
|
+
watch(
|
|
93
|
+
() => props.name,
|
|
94
|
+
() => {
|
|
95
|
+
activePart.value = parts.value[0].id;
|
|
96
|
+
status.value = "idle";
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
const columns = computed(() => entryListColumns(props.name, settings.value));
|
|
100
|
+
const layout = computed(() => entryFormLayout(props.name, settings.value, fields.value));
|
|
101
|
+
const isCustom = computed(() => {
|
|
102
|
+
const saved = settings.value[props.name];
|
|
103
|
+
return activePart.value === "columns" ? !!saved?.columns : !!saved?.layout;
|
|
104
|
+
});
|
|
105
|
+
const status = ref("idle");
|
|
106
|
+
async function persist(next) {
|
|
107
|
+
status.value = "saving";
|
|
108
|
+
try {
|
|
109
|
+
if (!next.columns && !next.layout) await reset(props.name);
|
|
110
|
+
else await save(props.name, next);
|
|
111
|
+
status.value = "saved";
|
|
112
|
+
} catch (error) {
|
|
113
|
+
status.value = "error";
|
|
114
|
+
toast.add({ title: "Settings not saved", description: errorMessage(error), color: "error" });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function current() {
|
|
118
|
+
const saved = settings.value[props.name] ?? {};
|
|
119
|
+
return { columns: saved.columns, layout: saved.layout };
|
|
120
|
+
}
|
|
121
|
+
function saveColumns(value) {
|
|
122
|
+
void persist({ ...current(), columns: value });
|
|
123
|
+
}
|
|
124
|
+
function saveLayout(value) {
|
|
125
|
+
void persist({ ...current(), layout: value });
|
|
126
|
+
}
|
|
127
|
+
function resetPart() {
|
|
128
|
+
const next = current();
|
|
129
|
+
if (activePart.value === "columns") next.columns = void 0;
|
|
130
|
+
else next.layout = void 0;
|
|
131
|
+
void persist(next);
|
|
132
|
+
}
|
|
133
|
+
</script>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
name: string;
|
|
3
|
+
};
|
|
4
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
5
|
+
declare const _default: typeof __VLS_export;
|
|
6
|
+
export default _default;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CmsFormLayout, CmsTab, FieldConfig } from '#nuxt-cms';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
fields: Record<string, FieldConfig>;
|
|
4
|
+
tabs?: CmsTab[];
|
|
5
|
+
};
|
|
6
|
+
type __VLS_ModelProps = {
|
|
7
|
+
modelValue: CmsFormLayout;
|
|
8
|
+
};
|
|
9
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
+
"update:modelValue": (value: CmsFormLayout) => any;
|
|
12
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
13
|
+
"onUpdate:modelValue"?: ((value: CmsFormLayout) => any) | undefined;
|
|
14
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
15
|
+
declare const _default: typeof __VLS_export;
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cms-layout-editor" :class="{ 'is-dragging': dragging }">
|
|
3
|
+
<p class="cms-settings-hint">
|
|
4
|
+
Drag a field up or down to move it. Drop it on the side of another field to put them in the
|
|
5
|
+
same row (up to {{ MAX_LAYOUT_ROW_FIELDS }}).
|
|
6
|
+
</p>
|
|
7
|
+
<div class="cms-layout-areas">
|
|
8
|
+
<div v-for="area in AREAS" :key="area.id" class="cms-layout-area" :class="`is-${area.id}`">
|
|
9
|
+
<div
|
|
10
|
+
v-if="!model[area.id].some((section2) => section2.rows.length || section2.title)"
|
|
11
|
+
class="cms-layout-empty"
|
|
12
|
+
:class="{ 'is-over': isOver({ type: 'area', area: area.id }) }"
|
|
13
|
+
@dragover.prevent="setTarget({ type: 'area', area: area.id })"
|
|
14
|
+
@drop.prevent="drop"
|
|
15
|
+
>
|
|
16
|
+
{{ area.empty }}
|
|
17
|
+
</div>
|
|
18
|
+
<div
|
|
19
|
+
v-for="(section, sectionIndex) in model[area.id]"
|
|
20
|
+
v-else
|
|
21
|
+
:key="`${area.id}-${sectionIndex}`"
|
|
22
|
+
class="cms-layout-section"
|
|
23
|
+
:class="{ 'is-titled': !!section.title }"
|
|
24
|
+
>
|
|
25
|
+
<p v-if="section.title" class="cms-layout-section-title">
|
|
26
|
+
{{ section.title }}
|
|
27
|
+
<span v-if="section.collapsed" class="cms-badge is-muted">Collapsed</span>
|
|
28
|
+
</p>
|
|
29
|
+
<template v-for="(row, rowIndex) in section.rows" :key="row.join('|')">
|
|
30
|
+
<div
|
|
31
|
+
class="cms-layout-gap"
|
|
32
|
+
:class="{ 'is-over': isOver(gapTarget(area.id, sectionIndex, rowIndex)) }"
|
|
33
|
+
@dragover.prevent="setTarget(gapTarget(area.id, sectionIndex, rowIndex))"
|
|
34
|
+
@drop.prevent="drop"
|
|
35
|
+
/>
|
|
36
|
+
<div class="cms-layout-row">
|
|
37
|
+
<div
|
|
38
|
+
v-for="key in row"
|
|
39
|
+
:key="key"
|
|
40
|
+
class="cms-layout-chip"
|
|
41
|
+
:class="{
|
|
42
|
+
'is-dragging': dragging === key,
|
|
43
|
+
'is-drop-before': isOver({ type: 'inline', key, after: false }),
|
|
44
|
+
'is-drop-after': isOver({ type: 'inline', key, after: true }),
|
|
45
|
+
'is-blocked': blocked === key
|
|
46
|
+
}"
|
|
47
|
+
draggable="true"
|
|
48
|
+
@dragstart="onDragStart($event, key)"
|
|
49
|
+
@dragover.prevent="onChipOver($event, key, row)"
|
|
50
|
+
@drop.prevent="drop"
|
|
51
|
+
@dragend="onDragEnd"
|
|
52
|
+
>
|
|
53
|
+
<CmsIcon name="bars-2" class="cms-settings-grip size-3.5" />
|
|
54
|
+
<CmsIcon :name="iconOf(key)" class="cms-settings-item-icon size-3.5" />
|
|
55
|
+
<span class="cms-layout-chip-label">{{ fields[key]?.label ?? key }}</span>
|
|
56
|
+
<span v-if="tabLabel(key)" class="cms-layout-chip-tab">{{
|
|
57
|
+
tabLabel(key)
|
|
58
|
+
}}</span>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
62
|
+
<div
|
|
63
|
+
class="cms-layout-gap is-end"
|
|
64
|
+
:class="{
|
|
65
|
+
'is-over': isOver(gapTarget(area.id, sectionIndex, section.rows.length)),
|
|
66
|
+
'is-empty': !section.rows.length
|
|
67
|
+
}"
|
|
68
|
+
@dragover.prevent="
|
|
69
|
+
setTarget(gapTarget(area.id, sectionIndex, section.rows.length))
|
|
70
|
+
"
|
|
71
|
+
@drop.prevent="drop"
|
|
72
|
+
>
|
|
73
|
+
<span v-if="!section.rows.length">Drop fields here</span>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<script setup>
|
|
82
|
+
import { MAX_LAYOUT_ROW_FIELDS, fieldTab } from "#nuxt-cms";
|
|
83
|
+
import { ref } from "#imports";
|
|
84
|
+
import { fieldIcon } from "../../utils/ui";
|
|
85
|
+
const AREAS = [{ id: "main", empty: "Drop fields here" }];
|
|
86
|
+
const props = defineProps({
|
|
87
|
+
fields: { type: Object, required: true },
|
|
88
|
+
tabs: { type: Array, required: false }
|
|
89
|
+
});
|
|
90
|
+
const model = defineModel({ type: Object, ...{ required: true } });
|
|
91
|
+
const dragging = ref(null);
|
|
92
|
+
const target = ref(null);
|
|
93
|
+
const blocked = ref(null);
|
|
94
|
+
function iconOf(key) {
|
|
95
|
+
const field = props.fields[key];
|
|
96
|
+
return field ? fieldIcon(field) : "bars-3-bottom-left";
|
|
97
|
+
}
|
|
98
|
+
function tabLabel(key) {
|
|
99
|
+
const field = props.fields[key];
|
|
100
|
+
if (!field || !props.tabs?.length) return "";
|
|
101
|
+
const id = fieldTab(field, props.tabs);
|
|
102
|
+
return props.tabs.find((tab) => tab.id === id)?.label ?? "";
|
|
103
|
+
}
|
|
104
|
+
function gapTarget(area, section, row) {
|
|
105
|
+
return { type: "gap", area, section, row };
|
|
106
|
+
}
|
|
107
|
+
function sameTarget(a, b) {
|
|
108
|
+
return !!a && JSON.stringify(a) === JSON.stringify(b);
|
|
109
|
+
}
|
|
110
|
+
function isOver(candidate) {
|
|
111
|
+
return sameTarget(target.value, candidate);
|
|
112
|
+
}
|
|
113
|
+
function setTarget(candidate) {
|
|
114
|
+
blocked.value = null;
|
|
115
|
+
if (!sameTarget(target.value, candidate)) target.value = candidate;
|
|
116
|
+
}
|
|
117
|
+
function onDragStart(event, key) {
|
|
118
|
+
dragging.value = key;
|
|
119
|
+
event.dataTransfer?.setData("text/plain", key);
|
|
120
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
|
121
|
+
}
|
|
122
|
+
function onChipOver(event, key, row) {
|
|
123
|
+
if (!dragging.value || key === dragging.value) {
|
|
124
|
+
target.value = null;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const full = row.length >= MAX_LAYOUT_ROW_FIELDS && !row.includes(dragging.value);
|
|
128
|
+
if (full) {
|
|
129
|
+
target.value = null;
|
|
130
|
+
blocked.value = key;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
134
|
+
setTarget({ type: "inline", key, after: event.clientX > box.left + box.width / 2 });
|
|
135
|
+
}
|
|
136
|
+
function onDragEnd() {
|
|
137
|
+
dragging.value = null;
|
|
138
|
+
target.value = null;
|
|
139
|
+
blocked.value = null;
|
|
140
|
+
}
|
|
141
|
+
function cloneLayout(layout) {
|
|
142
|
+
const copy = (sections) => sections.map((section) => ({ ...section, rows: section.rows.map((row) => [...row]) }));
|
|
143
|
+
return { main: copy(layout.main) };
|
|
144
|
+
}
|
|
145
|
+
function drop() {
|
|
146
|
+
const key = dragging.value;
|
|
147
|
+
const to = target.value;
|
|
148
|
+
onDragEnd();
|
|
149
|
+
if (!key || !to) return;
|
|
150
|
+
const next = cloneLayout(model.value);
|
|
151
|
+
const sections = next.main;
|
|
152
|
+
const anchorRow = to.type === "gap" ? next[to.area][to.section]?.rows[to.row] ?? null : null;
|
|
153
|
+
for (const section of sections) {
|
|
154
|
+
for (const row of section.rows) {
|
|
155
|
+
const index = row.indexOf(key);
|
|
156
|
+
if (index !== -1) row.splice(index, 1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (to.type === "inline") {
|
|
160
|
+
const row = sections.flatMap((section) => section.rows).find((r) => r.includes(to.key));
|
|
161
|
+
if (!row) return;
|
|
162
|
+
row.splice(row.indexOf(to.key) + (to.after ? 1 : 0), 0, key);
|
|
163
|
+
} else if (to.type === "gap") {
|
|
164
|
+
const section = next[to.area][to.section];
|
|
165
|
+
if (!section) return;
|
|
166
|
+
const index = anchorRow ? section.rows.indexOf(anchorRow) : section.rows.length;
|
|
167
|
+
section.rows.splice(index === -1 ? section.rows.length : index, 0, [key]);
|
|
168
|
+
} else {
|
|
169
|
+
next[to.area] = [...next[to.area].filter((s) => s.title), { rows: [[key]] }];
|
|
170
|
+
}
|
|
171
|
+
for (const section of sections) {
|
|
172
|
+
section.rows = section.rows.filter((row) => row.length);
|
|
173
|
+
}
|
|
174
|
+
next.main = next.main.filter((section) => section.title || section.rows.length);
|
|
175
|
+
model.value = next;
|
|
176
|
+
}
|
|
177
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CmsFormLayout, CmsTab, FieldConfig } from '#nuxt-cms';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
fields: Record<string, FieldConfig>;
|
|
4
|
+
tabs?: CmsTab[];
|
|
5
|
+
};
|
|
6
|
+
type __VLS_ModelProps = {
|
|
7
|
+
modelValue: CmsFormLayout;
|
|
8
|
+
};
|
|
9
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
+
"update:modelValue": (value: CmsFormLayout) => any;
|
|
12
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
13
|
+
"onUpdate:modelValue"?: ((value: CmsFormLayout) => any) | undefined;
|
|
14
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
15
|
+
declare const _default: typeof __VLS_export;
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type CmsTheme } from '../../composables/cms-theme.js';
|
|
2
|
+
type __VLS_ModelProps = {
|
|
3
|
+
'open'?: boolean;
|
|
4
|
+
'theme': CmsTheme;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
logout: () => any;
|
|
8
|
+
"update:open": (value: boolean) => any;
|
|
9
|
+
"update:theme": (value: CmsTheme) => any;
|
|
10
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
11
|
+
onLogout?: (() => any) | undefined;
|
|
12
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
13
|
+
"onUpdate:theme"?: ((value: CmsTheme) => any) | undefined;
|
|
14
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
15
|
+
declare const _default: typeof __VLS_export;
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="body">
|
|
3
|
+
<Transition name="cms-modal">
|
|
4
|
+
<div v-if="open" class="cms-scope cms-overlay" @click.self="close">
|
|
5
|
+
<div
|
|
6
|
+
class="cms-modal cms-settings"
|
|
7
|
+
role="dialog"
|
|
8
|
+
aria-modal="true"
|
|
9
|
+
aria-label="Settings"
|
|
10
|
+
>
|
|
11
|
+
<aside class="cms-settings-nav">
|
|
12
|
+
<CmsButton
|
|
13
|
+
icon="x-mark"
|
|
14
|
+
variant="ghost"
|
|
15
|
+
color="neutral"
|
|
16
|
+
size="sm"
|
|
17
|
+
aria-label="Close settings"
|
|
18
|
+
class="cms-settings-close"
|
|
19
|
+
@click="close"
|
|
20
|
+
/>
|
|
21
|
+
<div class="cms-settings-nav-list">
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
class="cms-settings-tab"
|
|
25
|
+
:class="{ 'is-active': active === 'general' }"
|
|
26
|
+
@click="active = 'general'"
|
|
27
|
+
>
|
|
28
|
+
<CmsIcon name="cog-6-tooth" class="size-4 shrink-0" />
|
|
29
|
+
General
|
|
30
|
+
</button>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
class="cms-settings-tab"
|
|
34
|
+
:class="{ 'is-active': active === 'account' }"
|
|
35
|
+
@click="active = 'account'"
|
|
36
|
+
>
|
|
37
|
+
<CmsIcon name="user-circle" class="size-4 shrink-0" />
|
|
38
|
+
Account
|
|
39
|
+
</button>
|
|
40
|
+
<button
|
|
41
|
+
v-if="isSuperAdmin"
|
|
42
|
+
type="button"
|
|
43
|
+
class="cms-settings-tab"
|
|
44
|
+
:class="{ 'is-active': active === 'users' }"
|
|
45
|
+
@click="active = 'users'"
|
|
46
|
+
>
|
|
47
|
+
<CmsIcon name="users" class="size-4 shrink-0" />
|
|
48
|
+
Users
|
|
49
|
+
</button>
|
|
50
|
+
<p class="cms-settings-nav-label">Content</p>
|
|
51
|
+
<button
|
|
52
|
+
v-for="item in entries"
|
|
53
|
+
:key="item.name"
|
|
54
|
+
type="button"
|
|
55
|
+
class="cms-settings-tab"
|
|
56
|
+
:class="{ 'is-active': active === item.key }"
|
|
57
|
+
@click="active = item.key"
|
|
58
|
+
>
|
|
59
|
+
<CmsIcon :name="item.icon" class="size-4 shrink-0" />
|
|
60
|
+
<span class="truncate">{{ item.label }}</span>
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
<hr class="cms-settings-divider" />
|
|
64
|
+
<button type="button" class="cms-settings-tab is-danger" @click="emit('logout')">
|
|
65
|
+
<CmsIcon name="arrow-right-start-on-rectangle" class="size-4 shrink-0" />
|
|
66
|
+
Sign out
|
|
67
|
+
</button>
|
|
68
|
+
</aside>
|
|
69
|
+
|
|
70
|
+
<section v-if="active === 'account'" class="cms-settings-content">
|
|
71
|
+
<CmsSettingsAccount />
|
|
72
|
+
</section>
|
|
73
|
+
<section v-else-if="active === 'users'" class="cms-settings-content">
|
|
74
|
+
<CmsSettingsUsers />
|
|
75
|
+
</section>
|
|
76
|
+
<section v-else-if="activeEntry" class="cms-settings-content">
|
|
77
|
+
<CmsSettingsEntry :key="activeEntry" :name="activeEntry" />
|
|
78
|
+
</section>
|
|
79
|
+
<section v-else class="cms-settings-content">
|
|
80
|
+
<h2 class="cms-title cms-title-sm">General</h2>
|
|
81
|
+
|
|
82
|
+
<div class="cms-settings-group">
|
|
83
|
+
<div class="cms-settings-heading">
|
|
84
|
+
<p class="cms-settings-label">Theme</p>
|
|
85
|
+
<p class="cms-settings-hint">
|
|
86
|
+
Choose how the panel looks. System follows your device.
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="cms-theme-cards" role="radiogroup" aria-label="Theme">
|
|
90
|
+
<button
|
|
91
|
+
v-for="option in CMS_THEMES"
|
|
92
|
+
:key="option.value"
|
|
93
|
+
type="button"
|
|
94
|
+
role="radio"
|
|
95
|
+
:aria-checked="theme === option.value"
|
|
96
|
+
class="cms-theme-card"
|
|
97
|
+
:class="{ 'is-active': theme === option.value }"
|
|
98
|
+
@click="selectTheme(option.value)"
|
|
99
|
+
>
|
|
100
|
+
<span class="cms-theme-preview" :class="`is-${option.value}`">
|
|
101
|
+
<span
|
|
102
|
+
v-for="half in previewHalves(option.value)"
|
|
103
|
+
:key="half"
|
|
104
|
+
class="cms-theme-preview-app"
|
|
105
|
+
:class="`is-${half}`"
|
|
106
|
+
>
|
|
107
|
+
<span class="cms-theme-preview-side" />
|
|
108
|
+
<span class="cms-theme-preview-panel">
|
|
109
|
+
<span class="cms-theme-preview-line is-accent" />
|
|
110
|
+
<span class="cms-theme-preview-line" />
|
|
111
|
+
<span class="cms-theme-preview-line is-short" />
|
|
112
|
+
</span>
|
|
113
|
+
</span>
|
|
114
|
+
</span>
|
|
115
|
+
<span class="cms-theme-card-label">
|
|
116
|
+
<CmsIcon :name="option.icon" class="size-3.5" />
|
|
117
|
+
{{ option.label }}
|
|
118
|
+
<CmsIcon
|
|
119
|
+
v-if="theme === option.value"
|
|
120
|
+
name="check"
|
|
121
|
+
class="cms-theme-card-check size-3.5"
|
|
122
|
+
/>
|
|
123
|
+
</span>
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</section>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</Transition>
|
|
131
|
+
</Teleport>
|
|
132
|
+
</template>
|
|
133
|
+
|
|
134
|
+
<script setup>
|
|
135
|
+
import { computed, onBeforeUnmount, ref, watch } from "#imports";
|
|
136
|
+
import cmsConfig from "#cms-config";
|
|
137
|
+
import { CMS_THEMES, setCmsTheme } from "../../composables/cms-theme";
|
|
138
|
+
import { useCmsAccount } from "../../composables/cms-account";
|
|
139
|
+
const { isSuperAdmin } = useCmsAccount();
|
|
140
|
+
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
141
|
+
const theme = defineModel("theme", { type: String, ...{ required: true } });
|
|
142
|
+
const emit = defineEmits(["logout"]);
|
|
143
|
+
const KIND_ICONS = {
|
|
144
|
+
collection: "square-3-stack-3d",
|
|
145
|
+
single: "document-text",
|
|
146
|
+
page: "window"
|
|
147
|
+
};
|
|
148
|
+
const ENTRY_PREFIX = "entry:";
|
|
149
|
+
const entries = Object.entries(cmsConfig).map(([name, entry]) => ({
|
|
150
|
+
name,
|
|
151
|
+
key: `${ENTRY_PREFIX}${name}`,
|
|
152
|
+
label: entry.label,
|
|
153
|
+
icon: entry.icon ?? KIND_ICONS[entry.kind]
|
|
154
|
+
}));
|
|
155
|
+
const active = ref("general");
|
|
156
|
+
const activeEntry = computed(
|
|
157
|
+
() => active.value.startsWith(ENTRY_PREFIX) ? active.value.slice(ENTRY_PREFIX.length) : null
|
|
158
|
+
);
|
|
159
|
+
function selectTheme(value) {
|
|
160
|
+
setCmsTheme(theme, value);
|
|
161
|
+
}
|
|
162
|
+
function previewHalves(value) {
|
|
163
|
+
return value === "system" ? ["light", "dark"] : [value];
|
|
164
|
+
}
|
|
165
|
+
function close() {
|
|
166
|
+
open.value = false;
|
|
167
|
+
}
|
|
168
|
+
function onKeydown(event) {
|
|
169
|
+
if (event.key === "Escape") close();
|
|
170
|
+
}
|
|
171
|
+
watch(open, (value) => {
|
|
172
|
+
if (import.meta.server) return;
|
|
173
|
+
if (value) document.addEventListener("keydown", onKeydown);
|
|
174
|
+
else document.removeEventListener("keydown", onKeydown);
|
|
175
|
+
});
|
|
176
|
+
onBeforeUnmount(() => {
|
|
177
|
+
if (import.meta.client) document.removeEventListener("keydown", onKeydown);
|
|
178
|
+
});
|
|
179
|
+
</script>
|