mktcms 0.3.17 → 0.3.19
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 +29 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +122 -33
- package/dist/runtime/app/components/content/editor/frontmatter/filePicker/modal.d.vue.ts +1 -1
- package/dist/runtime/app/components/content/editor/frontmatter/filePicker/modal.vue.d.ts +1 -1
- package/dist/runtime/app/components/content/editor/frontmatter/filePicker/node.d.vue.ts +1 -1
- package/dist/runtime/app/components/content/editor/frontmatter/filePicker/node.vue.d.ts +1 -1
- package/dist/runtime/app/components/content/editor/monacoEditor.vue +275 -5
- package/dist/runtime/server/api/admin/list.js +4 -1
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -28,6 +28,35 @@ This is my personal, minimalist alternative to @nuxt/content and Studio, which a
|
|
|
28
28
|
npx nuxi module add mktcms
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
The module also applies a small set of app defaults that you can still override in your own `nuxt.config.ts`:
|
|
32
|
+
|
|
33
|
+
- `router.options.scrollBehaviorType = 'smooth'`
|
|
34
|
+
- default German `app.head` language, title, description, and favicon
|
|
35
|
+
- `mdc.headings.anchorLinks = false`
|
|
36
|
+
- includes and configures `@nuxtjs/robots` with admin route disallow rules
|
|
37
|
+
- includes and configures `@nuxt/fonts` with default font weights
|
|
38
|
+
- includes and configures `@nuxtjs/plausible` with `proxy = true` and `autoPageviews = false`
|
|
39
|
+
- default frontmatter schema for `Seiten/Startseite.md` and `Seiten/**/*.md`
|
|
40
|
+
|
|
41
|
+
Example override:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export default defineNuxtConfig({
|
|
45
|
+
app: {
|
|
46
|
+
head: {
|
|
47
|
+
title: 'Meine Website',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
mktcms: {
|
|
51
|
+
frontmatter: {
|
|
52
|
+
'Seiten/**/*.md': {
|
|
53
|
+
seoTitle: { type: 'string', label: 'Eigener SEO-Titel' },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
31
60
|
```bash
|
|
32
61
|
NUXT_PUBLIC_MKTCMS_SITE_URL="http://localhost:3000"
|
|
33
62
|
NUXT_PUBLIC_MKTCMS_SHOW_VERSIONING=false
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,6 +1,100 @@
|
|
|
1
1
|
import { defineNuxtModule, createResolver, addComponent, addImports, addServerImports, addServerPlugin, addServerHandler, extendPages } from '@nuxt/kit';
|
|
2
2
|
import defu from 'defu';
|
|
3
3
|
|
|
4
|
+
const DEFAULT_HEAD_META = [
|
|
5
|
+
{
|
|
6
|
+
name: "description",
|
|
7
|
+
content: "Meine neue mktCMS Website"
|
|
8
|
+
}
|
|
9
|
+
];
|
|
10
|
+
const DEFAULT_HEAD_LINK = [
|
|
11
|
+
{
|
|
12
|
+
rel: "icon",
|
|
13
|
+
type: "image/png",
|
|
14
|
+
href: "/favicon.png"
|
|
15
|
+
}
|
|
16
|
+
];
|
|
17
|
+
const defaultFrontmatterSchema = {
|
|
18
|
+
seoTitle: {
|
|
19
|
+
type: "string",
|
|
20
|
+
label: "SEO-Titel"
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const defaultFrontmatterSchemas = {
|
|
24
|
+
"Seiten/Startseite.md": defaultFrontmatterSchema,
|
|
25
|
+
"Seiten/**/*.md": defaultFrontmatterSchema
|
|
26
|
+
};
|
|
27
|
+
function mergeHeadMeta(meta) {
|
|
28
|
+
const nextMeta = Array.isArray(meta) ? [...meta] : [];
|
|
29
|
+
for (const defaultMeta of DEFAULT_HEAD_META) {
|
|
30
|
+
const hasMatch = nextMeta.some((item) => item?.name === defaultMeta.name);
|
|
31
|
+
if (!hasMatch) {
|
|
32
|
+
nextMeta.push(defaultMeta);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return nextMeta;
|
|
36
|
+
}
|
|
37
|
+
function mergeHeadLinks(links) {
|
|
38
|
+
const nextLinks = Array.isArray(links) ? [...links] : [];
|
|
39
|
+
for (const defaultLink of DEFAULT_HEAD_LINK) {
|
|
40
|
+
const hasMatch = nextLinks.some((item) => item?.rel === defaultLink.rel);
|
|
41
|
+
if (!hasMatch) {
|
|
42
|
+
nextLinks.push(defaultLink);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return nextLinks;
|
|
46
|
+
}
|
|
47
|
+
function applyMktcmsNuxtDefaults(nuxtOptions, moduleOptions = {}) {
|
|
48
|
+
nuxtOptions.router = defu(nuxtOptions.router, {
|
|
49
|
+
options: {
|
|
50
|
+
scrollBehaviorType: "smooth"
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
nuxtOptions.app = defu(nuxtOptions.app, {
|
|
54
|
+
head: {
|
|
55
|
+
htmlAttrs: {
|
|
56
|
+
lang: "de"
|
|
57
|
+
},
|
|
58
|
+
title: "Neue Website"
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
nuxtOptions.app.head = nuxtOptions.app.head || {};
|
|
62
|
+
nuxtOptions.app.head.meta = mergeHeadMeta(nuxtOptions.app.head.meta);
|
|
63
|
+
nuxtOptions.app.head.link = mergeHeadLinks(nuxtOptions.app.head.link);
|
|
64
|
+
nuxtOptions.runtimeConfig = defu(nuxtOptions.runtimeConfig, {
|
|
65
|
+
plausibleApiKey: ""
|
|
66
|
+
});
|
|
67
|
+
nuxtOptions.runtimeConfig.public = defu(nuxtOptions.runtimeConfig.public, {
|
|
68
|
+
plausibleApiHost: ""
|
|
69
|
+
});
|
|
70
|
+
nuxtOptions.runtimeConfig.mktcms = defu(nuxtOptions.runtimeConfig.mktcms, {
|
|
71
|
+
adminAuthKey: "",
|
|
72
|
+
authCookieMaxAgeSeconds: 7 * 24 * 60 * 60,
|
|
73
|
+
authCookiePath: "/",
|
|
74
|
+
authCookieSameSite: "lax",
|
|
75
|
+
authCookieSecure: process.env.NODE_ENV === "production",
|
|
76
|
+
loginRateLimitMaxAttempts: 5,
|
|
77
|
+
loginRateLimitWindowSeconds: 300,
|
|
78
|
+
loginRateLimitBlockSeconds: 600,
|
|
79
|
+
uploadMaxBytes: 50 * 1024 * 1024,
|
|
80
|
+
smtpHost: "",
|
|
81
|
+
smtpPort: 465,
|
|
82
|
+
smtpSecure: true,
|
|
83
|
+
smtpUser: "",
|
|
84
|
+
smtpPass: "",
|
|
85
|
+
mailerFrom: "",
|
|
86
|
+
mailerTo: "",
|
|
87
|
+
gitUser: "",
|
|
88
|
+
gitRepo: "",
|
|
89
|
+
gitToken: "",
|
|
90
|
+
frontmatter: defu(moduleOptions.frontmatter || {}, defaultFrontmatterSchemas)
|
|
91
|
+
});
|
|
92
|
+
nuxtOptions.runtimeConfig.public.mktcms = defu(nuxtOptions.runtimeConfig.public.mktcms, {
|
|
93
|
+
siteUrl: "",
|
|
94
|
+
showVersioning: false
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
4
98
|
const module$1 = defineNuxtModule({
|
|
5
99
|
meta: {
|
|
6
100
|
name: "mktcms",
|
|
@@ -8,43 +102,38 @@ const module$1 = defineNuxtModule({
|
|
|
8
102
|
},
|
|
9
103
|
moduleDependencies: {
|
|
10
104
|
"@nuxtjs/mdc": {
|
|
11
|
-
version: "^0.20.0"
|
|
105
|
+
version: "^0.20.0",
|
|
106
|
+
defaults: {
|
|
107
|
+
headings: {
|
|
108
|
+
anchorLinks: false
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"@nuxt/fonts": {
|
|
113
|
+
version: "^0.14.0",
|
|
114
|
+
defaults: {
|
|
115
|
+
defaults: {
|
|
116
|
+
weights: [300, 400, 700, 800]
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"@nuxtjs/robots": {
|
|
121
|
+
version: "^6.0.6",
|
|
122
|
+
defaults: {
|
|
123
|
+
disallow: ["/api/admin/*", "/admin/*"]
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"@nuxtjs/plausible": {
|
|
127
|
+
version: "^3.0.2",
|
|
128
|
+
defaults: {
|
|
129
|
+
proxy: true,
|
|
130
|
+
autoPageviews: false
|
|
131
|
+
}
|
|
12
132
|
}
|
|
13
133
|
},
|
|
14
134
|
setup(_options, _nuxt) {
|
|
15
135
|
const resolver = createResolver(import.meta.url);
|
|
16
|
-
|
|
17
|
-
plausibleApiKey: ""
|
|
18
|
-
});
|
|
19
|
-
_nuxt.options.runtimeConfig.public = defu(_nuxt.options.runtimeConfig.public, {
|
|
20
|
-
plausibleApiHost: ""
|
|
21
|
-
});
|
|
22
|
-
_nuxt.options.runtimeConfig.mktcms = defu((_nuxt.options.runtimeConfig.mktcms, {
|
|
23
|
-
adminAuthKey: "",
|
|
24
|
-
authCookieMaxAgeSeconds: 7 * 24 * 60 * 60,
|
|
25
|
-
authCookiePath: "/",
|
|
26
|
-
authCookieSameSite: "lax",
|
|
27
|
-
authCookieSecure: process.env.NODE_ENV === "production",
|
|
28
|
-
loginRateLimitMaxAttempts: 5,
|
|
29
|
-
loginRateLimitWindowSeconds: 300,
|
|
30
|
-
loginRateLimitBlockSeconds: 600,
|
|
31
|
-
uploadMaxBytes: 50 * 1024 * 1024,
|
|
32
|
-
smtpHost: "",
|
|
33
|
-
smtpPort: 465,
|
|
34
|
-
smtpSecure: true,
|
|
35
|
-
smtpUser: "",
|
|
36
|
-
smtpPass: "",
|
|
37
|
-
mailerFrom: "",
|
|
38
|
-
mailerTo: "",
|
|
39
|
-
gitUser: "",
|
|
40
|
-
gitRepo: "",
|
|
41
|
-
gitToken: "",
|
|
42
|
-
frontmatter: _options.frontmatter || {}
|
|
43
|
-
}));
|
|
44
|
-
_nuxt.options.runtimeConfig.public.mktcms = defu((_nuxt.options.runtimeConfig.public.mktcms, {
|
|
45
|
-
siteUrl: "",
|
|
46
|
-
showVersioning: false
|
|
47
|
-
}));
|
|
136
|
+
applyMktcmsNuxtDefaults(_nuxt.options, _options);
|
|
48
137
|
addComponent({
|
|
49
138
|
name: "AdminWidget",
|
|
50
139
|
filePath: resolver.resolve("runtime/app/components/frontend/widget.vue")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
isOpen: boolean;
|
|
3
|
-
uiHint: 'image' | 'pdf' | 'file';
|
|
3
|
+
uiHint: 'image' | 'pdf' | 'file' | 'media';
|
|
4
4
|
};
|
|
5
5
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
6
6
|
select: (path: string) => any;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
isOpen: boolean;
|
|
3
|
-
uiHint: 'image' | 'pdf' | 'file';
|
|
3
|
+
uiHint: 'image' | 'pdf' | 'file' | 'media';
|
|
4
4
|
};
|
|
5
5
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
6
6
|
select: (path: string) => any;
|
|
@@ -2,7 +2,7 @@ type __VLS_Props = {
|
|
|
2
2
|
path: string;
|
|
3
3
|
name: string;
|
|
4
4
|
level: number;
|
|
5
|
-
uiHint: 'image' | 'pdf' | 'file';
|
|
5
|
+
uiHint: 'image' | 'pdf' | 'file' | 'media';
|
|
6
6
|
};
|
|
7
7
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
8
8
|
select: (path: string) => any;
|
|
@@ -2,7 +2,7 @@ type __VLS_Props = {
|
|
|
2
2
|
path: string;
|
|
3
3
|
name: string;
|
|
4
4
|
level: number;
|
|
5
|
-
uiHint: 'image' | 'pdf' | 'file';
|
|
5
|
+
uiHint: 'image' | 'pdf' | 'file' | 'media';
|
|
6
6
|
};
|
|
7
7
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
8
8
|
select: (path: string) => any;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
3
|
+
import TurndownService from "turndown";
|
|
4
|
+
import FilePickerModal from "./frontmatter/filePicker/modal.vue";
|
|
5
|
+
import { isImagePath } from "../../../../shared/contentFiles";
|
|
3
6
|
import "monaco-editor/min/vs/editor/editor.main.css";
|
|
4
7
|
import "monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution.js";
|
|
5
8
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
@@ -11,9 +14,238 @@ const props = defineProps({
|
|
|
11
14
|
});
|
|
12
15
|
const emit = defineEmits(["update:modelValue"]);
|
|
13
16
|
const rootEl = ref(null);
|
|
17
|
+
const isFilePickerOpen = ref(false);
|
|
18
|
+
const isContextMenuOpen = ref(false);
|
|
19
|
+
const contextMenuPosition = ref({ x: 0, y: 0 });
|
|
14
20
|
let editor;
|
|
21
|
+
let pendingFileInsertionSelection;
|
|
15
22
|
let resizeObserver;
|
|
16
23
|
let suppressModelEmit = false;
|
|
24
|
+
const allowedPasteElementNames = /* @__PURE__ */ new Set([
|
|
25
|
+
"a",
|
|
26
|
+
"b",
|
|
27
|
+
"blockquote",
|
|
28
|
+
"br",
|
|
29
|
+
"em",
|
|
30
|
+
"h1",
|
|
31
|
+
"h2",
|
|
32
|
+
"h3",
|
|
33
|
+
"h4",
|
|
34
|
+
"h5",
|
|
35
|
+
"h6",
|
|
36
|
+
"i",
|
|
37
|
+
"li",
|
|
38
|
+
"ol",
|
|
39
|
+
"p",
|
|
40
|
+
"strong",
|
|
41
|
+
"ul"
|
|
42
|
+
]);
|
|
43
|
+
const removablePasteElementNames = /* @__PURE__ */ new Set([
|
|
44
|
+
"applet",
|
|
45
|
+
"area",
|
|
46
|
+
"audio",
|
|
47
|
+
"button",
|
|
48
|
+
"canvas",
|
|
49
|
+
"caption",
|
|
50
|
+
"col",
|
|
51
|
+
"colgroup",
|
|
52
|
+
"embed",
|
|
53
|
+
"figcaption",
|
|
54
|
+
"figure",
|
|
55
|
+
"form",
|
|
56
|
+
"hr",
|
|
57
|
+
"iframe",
|
|
58
|
+
"img",
|
|
59
|
+
"input",
|
|
60
|
+
"link",
|
|
61
|
+
"map",
|
|
62
|
+
"math",
|
|
63
|
+
"meta",
|
|
64
|
+
"noscript",
|
|
65
|
+
"object",
|
|
66
|
+
"option",
|
|
67
|
+
"picture",
|
|
68
|
+
"script",
|
|
69
|
+
"select",
|
|
70
|
+
"source",
|
|
71
|
+
"style",
|
|
72
|
+
"svg",
|
|
73
|
+
"table",
|
|
74
|
+
"tbody",
|
|
75
|
+
"td",
|
|
76
|
+
"textarea",
|
|
77
|
+
"tfoot",
|
|
78
|
+
"th",
|
|
79
|
+
"thead",
|
|
80
|
+
"title",
|
|
81
|
+
"tr",
|
|
82
|
+
"track",
|
|
83
|
+
"video"
|
|
84
|
+
]);
|
|
85
|
+
const turndownService = new TurndownService({
|
|
86
|
+
headingStyle: "atx",
|
|
87
|
+
codeBlockStyle: "fenced",
|
|
88
|
+
bulletListMarker: "-"
|
|
89
|
+
});
|
|
90
|
+
turndownService.addRule("removeUnsupportedPasteElements", {
|
|
91
|
+
filter: (node) => removablePasteElementNames.has(node.nodeName.toLowerCase()) || node.nodeName.includes(":"),
|
|
92
|
+
replacement: () => ""
|
|
93
|
+
});
|
|
94
|
+
function unwrapElement(element) {
|
|
95
|
+
const parent = element.parentNode;
|
|
96
|
+
if (!parent)
|
|
97
|
+
return;
|
|
98
|
+
while (element.firstChild)
|
|
99
|
+
parent.insertBefore(element.firstChild, element);
|
|
100
|
+
parent.removeChild(element);
|
|
101
|
+
}
|
|
102
|
+
function isSafeHref(href) {
|
|
103
|
+
if (!href.trim())
|
|
104
|
+
return false;
|
|
105
|
+
try {
|
|
106
|
+
const url = new URL(href, window.location.origin);
|
|
107
|
+
return ["http:", "https:", "mailto:", "tel:"].includes(url.protocol);
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function sanitizePastedHtml(html) {
|
|
113
|
+
const clipboardDocument = new DOMParser().parseFromString(html, "text/html");
|
|
114
|
+
const comments = clipboardDocument.createTreeWalker(clipboardDocument.body, NodeFilter.SHOW_COMMENT);
|
|
115
|
+
const commentsToRemove = [];
|
|
116
|
+
while (comments.nextNode())
|
|
117
|
+
commentsToRemove.push(comments.currentNode);
|
|
118
|
+
for (const comment of commentsToRemove)
|
|
119
|
+
comment.remove();
|
|
120
|
+
const elements = Array.from(clipboardDocument.body.querySelectorAll("*"));
|
|
121
|
+
for (const element of elements) {
|
|
122
|
+
if (!element.isConnected)
|
|
123
|
+
continue;
|
|
124
|
+
const tagName = element.tagName.toLowerCase();
|
|
125
|
+
const inlineStyle = element.getAttribute("style") ?? "";
|
|
126
|
+
if (/display\s*:\s*none/i.test(inlineStyle) || /mso-hide\s*:\s*all/i.test(inlineStyle)) {
|
|
127
|
+
element.remove();
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (tagName.includes(":") || removablePasteElementNames.has(tagName)) {
|
|
131
|
+
element.remove();
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (tagName === "a") {
|
|
135
|
+
const href = element.getAttribute("href") ?? "";
|
|
136
|
+
if (!isSafeHref(href)) {
|
|
137
|
+
unwrapElement(element);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
141
|
+
if (attribute.name !== "href")
|
|
142
|
+
element.removeAttribute(attribute.name);
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!allowedPasteElementNames.has(tagName)) {
|
|
147
|
+
unwrapElement(element);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const attribute of Array.from(element.attributes))
|
|
151
|
+
element.removeAttribute(attribute.name);
|
|
152
|
+
}
|
|
153
|
+
return clipboardDocument.body;
|
|
154
|
+
}
|
|
155
|
+
function insertMarkdown(markdown, selections = editor?.getSelections()) {
|
|
156
|
+
if (!editor || !selections?.length)
|
|
157
|
+
return;
|
|
158
|
+
editor.pushUndoStop();
|
|
159
|
+
editor.executeEdits("insert-markdown", selections.map((selection) => ({
|
|
160
|
+
range: selection,
|
|
161
|
+
text: markdown,
|
|
162
|
+
forceMoveMarkers: true
|
|
163
|
+
})));
|
|
164
|
+
editor.pushUndoStop();
|
|
165
|
+
editor.focus();
|
|
166
|
+
}
|
|
167
|
+
function escapeMarkdownLabel(label) {
|
|
168
|
+
return label.replaceAll("\\", "\\\\").replaceAll("[", "\\[").replaceAll("]", "\\]");
|
|
169
|
+
}
|
|
170
|
+
function filenameWithoutExtension(path) {
|
|
171
|
+
const filename = path.split(":").at(-1) ?? path;
|
|
172
|
+
return filename.replace(/\.[^/.]+$/, "");
|
|
173
|
+
}
|
|
174
|
+
function toContentUrl(path) {
|
|
175
|
+
return `/api/content/${encodeURIComponent(path)}`;
|
|
176
|
+
}
|
|
177
|
+
function toMarkdownFileReference(path) {
|
|
178
|
+
const label = escapeMarkdownLabel(filenameWithoutExtension(path));
|
|
179
|
+
const url = toContentUrl(path);
|
|
180
|
+
if (isImagePath(path))
|
|
181
|
+
return ``;
|
|
182
|
+
return `[${label}](${url})`;
|
|
183
|
+
}
|
|
184
|
+
function closeContextMenu() {
|
|
185
|
+
isContextMenuOpen.value = false;
|
|
186
|
+
}
|
|
187
|
+
function openFilePicker() {
|
|
188
|
+
closeContextMenu();
|
|
189
|
+
isFilePickerOpen.value = true;
|
|
190
|
+
}
|
|
191
|
+
function insertSelectedFile(path) {
|
|
192
|
+
insertMarkdown(toMarkdownFileReference(path), pendingFileInsertionSelection ? [pendingFileInsertionSelection] : void 0);
|
|
193
|
+
pendingFileInsertionSelection = void 0;
|
|
194
|
+
}
|
|
195
|
+
function getMouseTargetPosition(event) {
|
|
196
|
+
const target = editor?.getTargetAtClientPoint(event.clientX, event.clientY);
|
|
197
|
+
if (target && "position" in target && target.position)
|
|
198
|
+
return target.position;
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
function handleContextMenu(event) {
|
|
202
|
+
if (!editor || !rootEl.value?.contains(event.target))
|
|
203
|
+
return;
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
event.stopPropagation();
|
|
206
|
+
const position = getMouseTargetPosition(event);
|
|
207
|
+
if (position) {
|
|
208
|
+
pendingFileInsertionSelection = new monaco.Selection(
|
|
209
|
+
position.lineNumber,
|
|
210
|
+
position.column,
|
|
211
|
+
position.lineNumber,
|
|
212
|
+
position.column
|
|
213
|
+
);
|
|
214
|
+
editor.setPosition(position);
|
|
215
|
+
} else {
|
|
216
|
+
pendingFileInsertionSelection = editor.getSelection() ?? void 0;
|
|
217
|
+
}
|
|
218
|
+
contextMenuPosition.value = {
|
|
219
|
+
x: event.clientX,
|
|
220
|
+
y: event.clientY
|
|
221
|
+
};
|
|
222
|
+
isContextMenuOpen.value = true;
|
|
223
|
+
}
|
|
224
|
+
function handleDocumentClick(event) {
|
|
225
|
+
if (!isContextMenuOpen.value)
|
|
226
|
+
return;
|
|
227
|
+
const target = event.target;
|
|
228
|
+
if (target.closest("[data-monaco-custom-context-menu]"))
|
|
229
|
+
return;
|
|
230
|
+
closeContextMenu();
|
|
231
|
+
}
|
|
232
|
+
function handleDocumentKeydown(event) {
|
|
233
|
+
if (event.key === "Escape")
|
|
234
|
+
closeContextMenu();
|
|
235
|
+
}
|
|
236
|
+
function handlePaste(event) {
|
|
237
|
+
if (!editor?.hasTextFocus())
|
|
238
|
+
return;
|
|
239
|
+
const html = event.clipboardData?.getData("text/html");
|
|
240
|
+
if (!html)
|
|
241
|
+
return;
|
|
242
|
+
const markdown = turndownService.turndown(sanitizePastedHtml(html)).trim();
|
|
243
|
+
if (!markdown)
|
|
244
|
+
return;
|
|
245
|
+
event.preventDefault();
|
|
246
|
+
event.stopPropagation();
|
|
247
|
+
insertMarkdown(markdown);
|
|
248
|
+
}
|
|
17
249
|
function ensureMonacoWorkers() {
|
|
18
250
|
const globalAny = globalThis;
|
|
19
251
|
if (globalAny.MonacoEnvironment?.getWorker)
|
|
@@ -35,13 +267,18 @@ onMounted(() => {
|
|
|
35
267
|
minimap: { enabled: false },
|
|
36
268
|
scrollBeyondLastLine: false,
|
|
37
269
|
wordWrap: "on",
|
|
38
|
-
automaticLayout: true
|
|
270
|
+
automaticLayout: true,
|
|
271
|
+
contextmenu: false
|
|
39
272
|
});
|
|
40
273
|
editor.onDidChangeModelContent(() => {
|
|
41
274
|
if (!editor || suppressModelEmit)
|
|
42
275
|
return;
|
|
43
276
|
emit("update:modelValue", editor.getValue());
|
|
44
277
|
});
|
|
278
|
+
document.addEventListener("paste", handlePaste, true);
|
|
279
|
+
document.addEventListener("contextmenu", handleContextMenu, true);
|
|
280
|
+
document.addEventListener("click", handleDocumentClick, true);
|
|
281
|
+
document.addEventListener("keydown", handleDocumentKeydown, true);
|
|
45
282
|
resizeObserver = new ResizeObserver(() => {
|
|
46
283
|
editor?.layout();
|
|
47
284
|
});
|
|
@@ -63,14 +300,47 @@ watch(() => props.modelValue, (nextValue) => {
|
|
|
63
300
|
onBeforeUnmount(() => {
|
|
64
301
|
resizeObserver?.disconnect();
|
|
65
302
|
resizeObserver = void 0;
|
|
303
|
+
document.removeEventListener("paste", handlePaste, true);
|
|
304
|
+
document.removeEventListener("contextmenu", handleContextMenu, true);
|
|
305
|
+
document.removeEventListener("click", handleDocumentClick, true);
|
|
306
|
+
document.removeEventListener("keydown", handleDocumentKeydown, true);
|
|
66
307
|
editor?.dispose();
|
|
67
308
|
editor = void 0;
|
|
68
309
|
});
|
|
69
310
|
</script>
|
|
70
311
|
|
|
71
312
|
<template>
|
|
72
|
-
<div
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
313
|
+
<div class="relative w-full h-full">
|
|
314
|
+
<div
|
|
315
|
+
ref="rootEl"
|
|
316
|
+
class="w-full h-full"
|
|
317
|
+
/>
|
|
318
|
+
|
|
319
|
+
<div
|
|
320
|
+
v-if="isContextMenuOpen"
|
|
321
|
+
data-monaco-custom-context-menu
|
|
322
|
+
class="fixed z-9999 min-w-48 rounded-md border border-black/10 bg-white p-1 shadow-[0_8px_24px_rgba(0,0,0,0.18)]"
|
|
323
|
+
:style="{
|
|
324
|
+
left: `${contextMenuPosition.x}px`,
|
|
325
|
+
top: `${contextMenuPosition.y}px`
|
|
326
|
+
}"
|
|
327
|
+
role="menu"
|
|
328
|
+
>
|
|
329
|
+
<button
|
|
330
|
+
type="button"
|
|
331
|
+
class="w-full rounded px-3 py-2 text-left text-sm hover:bg-gray-100"
|
|
332
|
+
role="menuitem"
|
|
333
|
+
@click="openFilePicker"
|
|
334
|
+
>
|
|
335
|
+
Datei auswählen
|
|
336
|
+
</button>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<FilePickerModal
|
|
340
|
+
:is-open="isFilePickerOpen"
|
|
341
|
+
ui-hint="media"
|
|
342
|
+
@close="isFilePickerOpen = false"
|
|
343
|
+
@select="insertSelectedFile"
|
|
344
|
+
/>
|
|
345
|
+
</div>
|
|
76
346
|
</template>
|
|
@@ -8,7 +8,7 @@ function alphaSort(a, b) {
|
|
|
8
8
|
}
|
|
9
9
|
const querySchema = z.object({
|
|
10
10
|
path: z.string().optional(),
|
|
11
|
-
type: z.enum(["image", "pdf", "file"]).optional()
|
|
11
|
+
type: z.enum(["image", "pdf", "file", "media"]).optional()
|
|
12
12
|
});
|
|
13
13
|
export default defineEventHandler(async (event) => {
|
|
14
14
|
const { path, type } = await getValidatedQuery(event, (query) => querySchema.parse(query));
|
|
@@ -23,6 +23,9 @@ export default defineEventHandler(async (event) => {
|
|
|
23
23
|
if (type === "pdf") {
|
|
24
24
|
return isPdfPath(key);
|
|
25
25
|
}
|
|
26
|
+
if (type === "media") {
|
|
27
|
+
return isImagePath(key) || isPdfPath(key);
|
|
28
|
+
}
|
|
26
29
|
return true;
|
|
27
30
|
});
|
|
28
31
|
const filteredFiles = matchingKeys.filter((key) => !key.includes(":"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mktcms",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.19",
|
|
4
4
|
"description": "Simple CMS module for Nuxt",
|
|
5
5
|
"repository": "mktcode/mktcms",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,8 +38,11 @@
|
|
|
38
38
|
"css:watch": "tailwindcss -i ./src/runtime/app/styles/admin.css -o ./src/runtime/app/styles/admin.min.css --minify --watch"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
+
"@nuxt/fonts": "^0.14.0",
|
|
41
42
|
"@nuxt/kit": "^4.2.2",
|
|
42
43
|
"@nuxtjs/mdc": "^0.20.1",
|
|
44
|
+
"@nuxtjs/plausible": "^3.0.2",
|
|
45
|
+
"@nuxtjs/robots": "^6.0.6",
|
|
43
46
|
"@types/ejs": "^3.1.5",
|
|
44
47
|
"@vueuse/core": "^14.2.1",
|
|
45
48
|
"csv-parse": "^6.1.0",
|
|
@@ -52,6 +55,7 @@
|
|
|
52
55
|
"nodemailer": "^7.0.13",
|
|
53
56
|
"sharp": "^0.34.5",
|
|
54
57
|
"simple-git": "^3.32.2",
|
|
58
|
+
"turndown": "^7.2.4",
|
|
55
59
|
"unzipper": "^0.12.3",
|
|
56
60
|
"yaml": "^2.8.2",
|
|
57
61
|
"zod": "^4.3.6"
|
|
@@ -66,6 +70,7 @@
|
|
|
66
70
|
"@tailwindcss/typography": "^0.5.19",
|
|
67
71
|
"@types/node": "^25.3.0",
|
|
68
72
|
"@types/nodemailer": "^7.0.11",
|
|
73
|
+
"@types/turndown": "^5.0.6",
|
|
69
74
|
"@types/unzipper": "^0.10.11",
|
|
70
75
|
"changelogen": "^0.6.2",
|
|
71
76
|
"eslint": "^9.39.3",
|