mktcms 0.3.18 → 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/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mktcms",
3
3
  "configKey": "mktcms",
4
- "version": "0.3.18",
4
+ "version": "0.3.19",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -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,6 +1,8 @@
1
1
  <script setup>
2
2
  import { onBeforeUnmount, onMounted, ref, watch } from "vue";
3
3
  import TurndownService from "turndown";
4
+ import FilePickerModal from "./frontmatter/filePicker/modal.vue";
5
+ import { isImagePath } from "../../../../shared/contentFiles";
4
6
  import "monaco-editor/min/vs/editor/editor.main.css";
5
7
  import "monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution.js";
6
8
  import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
@@ -12,7 +14,11 @@ const props = defineProps({
12
14
  });
13
15
  const emit = defineEmits(["update:modelValue"]);
14
16
  const rootEl = ref(null);
17
+ const isFilePickerOpen = ref(false);
18
+ const isContextMenuOpen = ref(false);
19
+ const contextMenuPosition = ref({ x: 0, y: 0 });
15
20
  let editor;
21
+ let pendingFileInsertionSelection;
16
22
  let resizeObserver;
17
23
  let suppressModelEmit = false;
18
24
  const allowedPasteElementNames = /* @__PURE__ */ new Set([
@@ -146,14 +152,11 @@ function sanitizePastedHtml(html) {
146
152
  }
147
153
  return clipboardDocument.body;
148
154
  }
149
- function insertMarkdown(markdown) {
150
- if (!editor)
151
- return;
152
- const selections = editor.getSelections();
153
- if (!selections?.length)
155
+ function insertMarkdown(markdown, selections = editor?.getSelections()) {
156
+ if (!editor || !selections?.length)
154
157
  return;
155
158
  editor.pushUndoStop();
156
- editor.executeEdits("paste-html-as-markdown", selections.map((selection) => ({
159
+ editor.executeEdits("insert-markdown", selections.map((selection) => ({
157
160
  range: selection,
158
161
  text: markdown,
159
162
  forceMoveMarkers: true
@@ -161,6 +164,75 @@ function insertMarkdown(markdown) {
161
164
  editor.pushUndoStop();
162
165
  editor.focus();
163
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 `![${label}](${url})`;
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
+ }
164
236
  function handlePaste(event) {
165
237
  if (!editor?.hasTextFocus())
166
238
  return;
@@ -195,7 +267,8 @@ onMounted(() => {
195
267
  minimap: { enabled: false },
196
268
  scrollBeyondLastLine: false,
197
269
  wordWrap: "on",
198
- automaticLayout: true
270
+ automaticLayout: true,
271
+ contextmenu: false
199
272
  });
200
273
  editor.onDidChangeModelContent(() => {
201
274
  if (!editor || suppressModelEmit)
@@ -203,6 +276,9 @@ onMounted(() => {
203
276
  emit("update:modelValue", editor.getValue());
204
277
  });
205
278
  document.addEventListener("paste", handlePaste, true);
279
+ document.addEventListener("contextmenu", handleContextMenu, true);
280
+ document.addEventListener("click", handleDocumentClick, true);
281
+ document.addEventListener("keydown", handleDocumentKeydown, true);
206
282
  resizeObserver = new ResizeObserver(() => {
207
283
  editor?.layout();
208
284
  });
@@ -225,14 +301,46 @@ onBeforeUnmount(() => {
225
301
  resizeObserver?.disconnect();
226
302
  resizeObserver = void 0;
227
303
  document.removeEventListener("paste", handlePaste, true);
304
+ document.removeEventListener("contextmenu", handleContextMenu, true);
305
+ document.removeEventListener("click", handleDocumentClick, true);
306
+ document.removeEventListener("keydown", handleDocumentKeydown, true);
228
307
  editor?.dispose();
229
308
  editor = void 0;
230
309
  });
231
310
  </script>
232
311
 
233
312
  <template>
234
- <div
235
- ref="rootEl"
236
- class="w-full"
237
- />
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>
238
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.18",
3
+ "version": "0.3.19",
4
4
  "description": "Simple CMS module for Nuxt",
5
5
  "repository": "mktcode/mktcms",
6
6
  "license": "MIT",