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';
1942import TurndownService from ' turndown' ;
2043import { gfm , tables } from ' turndown-plugin-gfm' ;
2144import { 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
2348const 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+
2956const emit = defineEmits ([' update:value' ]);
3057const editorContainer = ref <HTMLElement | null >(null );
3158const 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+
495600onMounted (async () => {
496601 if (! editorContainer .value ) return ;
497602 try {
0 commit comments