Skip to content

Commit 58dead0

Browse files
committed
fix: add top panel
1 parent b92a15d commit 58dead0

File tree

1 file changed

+116
-11
lines changed

1 file changed

+116
-11
lines changed

custom/MarkdownEditor.vue

Lines changed: 116 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,38 @@
11
<template>
2-
<div class="mb-2"></div>
3-
<div
4-
ref="editorContainer"
5-
id="editor"
6-
:class="[
7-
'text-sm rounded-lg block w-full transition-all box-border overflow-hidden',
8-
isFocused
9-
? 'ring-1 ring-lightPrimary border ring-lightPrimary border-lightPrimary dark:ring-darkPrimary dark:border-darkPrimary'
10-
: 'border border-gray-300 dark:border-gray-600',
11-
]"
12-
></div>
2+
<div class="mb-2 w-full flex flex-col">
3+
<div class="flex flex-wrap items-center gap-3 p-1.5 border border-gray-300 dark:border-gray-600 rounded-t-lg bg-gray-50 dark:bg-gray-800 w-full box-border ">
4+
<button type="button" @click="applyFormat('bold')" :class="btnClass" title="Bold"><IconLetterBoldOutline class="w-5 h-5" /></button>
5+
<button type="button" @click="applyFormat('italic')" :class="btnClass" title="Italic"><IconLetterItalicOutline class="w-5 h-5" /></button>
6+
<button type="button" @click="applyFormat('underline')" :class="btnClass" title="Underline"><IconLetterUnderlineOutline class="w-5 h-5" /></button>
7+
<button type="button" @click="applyFormat('strike')" :class="btnClass" title="Strikethrough"><IconTextSlashOutline class="w-5 h-5" /></button>
8+
9+
<div class="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-1"></div>
10+
11+
<button type="button" @click="applyFormat('h2')" :class="btnClass" title="Heading 2"><IconH216Solid class="w-5 h-5" /></button>
12+
<button type="button" @click="applyFormat('h3')" :class="btnClass" title="Heading 3"><IconH316Solid class="w-5 h-5" /></button>
13+
14+
<div class="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-1"></div>
15+
16+
<button type="button" @click="applyFormat('ul')" :class="btnClass" title="Bulleted List"><IconRectangleListOutline class="w-5 h-5" /></button>
17+
<button type="button" @click="applyFormat('ol')" :class="btnClass" title="Numbered List"><IconOrderedListOutline class="w-5 h-5" /></button>
18+
19+
<div class="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-1"></div>
20+
21+
<button type="button" @click="applyFormat('link')" :class="btnClass" title="Link"><IconLinkOutline class="w-5 h-5" /></button>
22+
<button type="button" @click="applyFormat('code')" :class="btnClass" title="Code"><IconCodeOutline class="w-5 h-5" /></button>
23+
</div>
24+
25+
<div
26+
ref="editorContainer"
27+
id="editor"
28+
:class="[
29+
'text-sm block w-full transition-all box-border overflow-hidden rounded-b-lg border border-t-0 pt-3',
30+
isFocused
31+
? 'ring-1 ring-lightPrimary border-lightPrimary dark:ring-darkPrimary dark:border-darkPrimary'
32+
: 'border-gray-300 dark:border-gray-600',
33+
]"
34+
></div>
35+
</div>
1336
</template>
1437

1538
<script setup lang="ts">
@@ -19,13 +42,17 @@ import * as monaco from 'monaco-editor';
1942
import TurndownService from 'turndown';
2043
import { gfm, tables } from 'turndown-plugin-gfm';
2144
import { toggleWrapSmart } from './utils/monacoMarkdownToggle';
45+
import { IconLinkOutline, IconCodeOutline, IconRectangleListOutline, IconOrderedListOutline, IconLetterBoldOutline, IconLetterUnderlineOutline, IconLetterItalicOutline, IconTextSlashOutline} from '@iconify-prerendered/vue-flowbite';
46+
import { IconH216Solid, IconH316Solid } from '@iconify-prerendered/vue-heroicons';
2247
2348
const props = defineProps<{
2449
column: any,
2550
record: any,
2651
meta: any,
2752
}>()
2853
54+
const btnClass = "flex items-center justify-center h-8 px-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200";
55+
2956
const emit = defineEmits(['update:value']);
3057
const editorContainer = ref<HTMLElement | null>(null);
3158
const content = ref(String(props.record?.[props.column.name] ?? ''));
@@ -492,6 +519,84 @@ const onEditorCut = (e: ClipboardEvent) => {
492519
);
493520
};
494521
522+
const applyFormat = (type: string) => {
523+
if (!editor || !model) return;
524+
editor.focus();
525+
526+
const selection = editor.getSelection();
527+
if (!selection) return;
528+
const selectedText = model.getValueInRange(selection);
529+
530+
const applyEdits = (id: string, edits: monaco.editor.IIdentifiedSingleEditOperation[]) => {
531+
editor!.executeEdits(id, edits);
532+
};
533+
534+
const handleWrap = (wrap: string, endWrap?: string) => {
535+
const end = endWrap || wrap;
536+
if (selectedText.startsWith(wrap) && selectedText.endsWith(end)) {
537+
const newText = selectedText.substring(wrap.length, selectedText.length - end.length);
538+
applyEdits('unwrap', [{ range: selection, text: newText, forceMoveMarkers: true }]);
539+
} else {
540+
toggleWrapSmart(editor!, wrap, endWrap);
541+
}
542+
};
543+
544+
const handleCodeBlock = () => {
545+
const trimmed = selectedText.trim();
546+
if (trimmed.startsWith('```') && trimmed.endsWith('```')) {
547+
const content = trimmed.split('\n').slice(1, -1).join('\n');
548+
applyEdits('unwrap-code', [{ range: selection, text: content, forceMoveMarkers: true }]);
549+
} else {
550+
applyEdits('wrap-code', [{ range: selection, text: `\n\`\`\`\n${selectedText}\n\`\`\`\n`, forceMoveMarkers: true }]);
551+
}
552+
};
553+
554+
const handleLink = () => {
555+
const match = selectedText.match(/^\[(.*?)\]\(.*?\)$/);
556+
if (match) {
557+
applyEdits('unlink', [{ range: selection, text: match[1], forceMoveMarkers: true }]);
558+
} else {
559+
const text = selectedText || 'text';
560+
applyEdits('insert-link', [{ range: selection, text: `[${escapeMarkdownLinkText(text)}](url)`, forceMoveMarkers: true }]);
561+
}
562+
};
563+
564+
const handleBlockFormat = (formatType: string) => {
565+
const prefixMap: Record<string, string> = { h2: '## ', h3: '### ', ul: '* ' };
566+
const edits: monaco.editor.IIdentifiedSingleEditOperation[] = [];
567+
let olCounter = 1;
568+
569+
for (let i = selection.startLineNumber; i <= selection.endLineNumber; i++) {
570+
const line = model!.getLineContent(i);
571+
const targetPrefix = formatType === 'ol' ? `${olCounter++}. ` : prefixMap[formatType];
572+
const match = line.match(/^(#{1,3}\s|\*\s|\d+\.\s)/);
573+
574+
if (match) {
575+
const existing = match[0];
576+
const newText = (existing === targetPrefix) ? '' : targetPrefix;
577+
edits.push({ range: new monaco.Range(i, 1, i, existing.length + 1), text: newText, forceMoveMarkers: true });
578+
} else {
579+
edits.push({ range: new monaco.Range(i, 1, i, 1), text: targetPrefix, forceMoveMarkers: true });
580+
}
581+
}
582+
applyEdits('format-block', edits);
583+
};
584+
585+
switch (type) {
586+
case 'bold': handleWrap('**'); break;
587+
case 'italic': handleWrap('*'); break;
588+
case 'strike': handleWrap('~~'); break;
589+
case 'underline': handleWrap('<u>', '</u>'); break;
590+
case 'code': handleCodeBlock(); break;
591+
case 'link': handleLink(); break;
592+
case 'h2':
593+
case 'h3':
594+
case 'ul':
595+
case 'ol': handleBlockFormat(type); break;
596+
}
597+
};
598+
599+
495600
onMounted(async () => {
496601
if (!editorContainer.value) return;
497602
try {

0 commit comments

Comments
 (0)