@adminforth/markdown 1.10.3 → 1.10.5

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/build.log CHANGED
@@ -6,11 +6,13 @@ sending incremental file list
6
6
  custom/
7
7
  custom/MarkdownEditor.vue
8
8
  custom/MarkdownRenderer.vue
9
+ custom/package-lock.json
9
10
  custom/package.json
10
11
  custom/pnpm-lock.yaml
12
+ custom/topPanelButtons.vue
11
13
  custom/tsconfig.json
12
14
  custom/utils/
13
15
  custom/utils/monacoMarkdownToggle.ts
14
16
 
15
- sent 35,946 bytes received 142 bytes 72,176.00 bytes/sec
16
- total size is 35,421 speedup is 0.98
17
+ sent 45,566 bytes received 180 bytes 91,492.00 bytes/sec
18
+ total size is 44,884 speedup is 0.98
@@ -1,15 +1,17 @@
1
1
  <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
+ <TopPanelButtons :editor="editor" :meta="meta" />
4
+ <div
5
+ ref="editorContainer"
6
+ id="editor"
7
+ :class="[
8
+ 'text-sm block w-full transition-all box-border overflow-hidden rounded-b-lg border border-t-0 pt-3',
9
+ isFocused
10
+ ? 'ring-1 ring-lightPrimary border-lightPrimary dark:ring-darkPrimary dark:border-darkPrimary'
11
+ : 'border-gray-300 dark:border-gray-600',
12
+ ]"
13
+ ></div>
14
+ </div>
13
15
  </template>
14
16
 
15
17
  <script setup lang="ts">
@@ -19,6 +21,7 @@ import * as monaco from 'monaco-editor';
19
21
  import TurndownService from 'turndown';
20
22
  import { gfm, tables } from 'turndown-plugin-gfm';
21
23
  import { toggleWrapSmart } from './utils/monacoMarkdownToggle';
24
+ import TopPanelButtons from './topPanelButtons.vue';
22
25
 
23
26
  const props = defineProps<{
24
27
  column: any,
@@ -529,6 +532,33 @@ onMounted(async () => {
529
532
  editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyU, () => {
530
533
  toggleWrapSmart(editor!, '<u>', '</u>');
531
534
  });
535
+
536
+ disposables.push(
537
+ editor.onKeyDown((e) => {
538
+ if (e.keyCode !== monaco.KeyCode.Enter) {
539
+ return;
540
+ }
541
+ const pos = editor!.getPosition();
542
+ if (!pos) {
543
+ return;
544
+ }
545
+ const line = model!.getLineContent(pos.lineNumber);
546
+ const match = line.match(/^(\s*)([*+-]|\d+\.)\s+/);
547
+ if (!match) {
548
+ return;
549
+ }
550
+ e.preventDefault();
551
+
552
+ if (line.trim() === match[2].trim()) {
553
+ const range = new monaco.Range(pos.lineNumber, 1, pos.lineNumber, line.length + 1);
554
+ editor!.executeEdits('exit-list', [{ range, text: '', forceMoveMarkers: true }]);
555
+ } else {
556
+ const isNum = match[2].includes('.');
557
+ const next = isNum ? `${parseInt(match[2]) + 1}. ` : `${match[2]} `;
558
+ editor!.trigger('keyboard', 'type', { text: `\n${match[1]}${next}` });
559
+ }
560
+ }),
561
+ );
532
562
 
533
563
  editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK, () => {
534
564
  const selection = editor!.getSelection();
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "custom",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "custom",
9
+ "version": "1.0.0",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "dompurify": "^3.2.4",
13
+ "marked": "^15.0.7",
14
+ "monaco-editor": "^0.45.0",
15
+ "turndown": "^7.2.2",
16
+ "turndown-plugin-gfm": "^1.0.2"
17
+ }
18
+ },
19
+ "node_modules/@mixmark-io/domino": {
20
+ "version": "2.2.0",
21
+ "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
22
+ "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
23
+ "license": "BSD-2-Clause"
24
+ },
25
+ "node_modules/@types/trusted-types": {
26
+ "version": "2.0.7",
27
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
28
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
29
+ "license": "MIT",
30
+ "optional": true
31
+ },
32
+ "node_modules/dompurify": {
33
+ "version": "3.3.2",
34
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
35
+ "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
36
+ "license": "(MPL-2.0 OR Apache-2.0)",
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "optionalDependencies": {
41
+ "@types/trusted-types": "^2.0.7"
42
+ }
43
+ },
44
+ "node_modules/marked": {
45
+ "version": "15.0.12",
46
+ "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
47
+ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
48
+ "license": "MIT",
49
+ "bin": {
50
+ "marked": "bin/marked.js"
51
+ },
52
+ "engines": {
53
+ "node": ">= 18"
54
+ }
55
+ },
56
+ "node_modules/monaco-editor": {
57
+ "version": "0.45.0",
58
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.45.0.tgz",
59
+ "integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==",
60
+ "license": "MIT"
61
+ },
62
+ "node_modules/turndown": {
63
+ "version": "7.2.2",
64
+ "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz",
65
+ "integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==",
66
+ "license": "MIT",
67
+ "dependencies": {
68
+ "@mixmark-io/domino": "^2.2.0"
69
+ }
70
+ },
71
+ "node_modules/turndown-plugin-gfm": {
72
+ "version": "1.0.2",
73
+ "resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz",
74
+ "integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==",
75
+ "license": "MIT"
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,131 @@
1
+ <script setup lang="ts">
2
+ import { markRaw } from 'vue';
3
+ import * as monaco from 'monaco-editor';
4
+ import { toggleWrapSmart } from './utils/monacoMarkdownToggle';
5
+
6
+ import {
7
+ IconLinkOutline, IconCodeOutline, IconRectangleListOutline,
8
+ IconOrderedListOutline, IconLetterBoldOutline, IconLetterUnderlineOutline,
9
+ IconLetterItalicOutline, IconTextSlashOutline
10
+ } from '@iconify-prerendered/vue-flowbite';
11
+ import { IconH116Solid, IconH216Solid, IconH316Solid } from '@iconify-prerendered/vue-heroicons';
12
+
13
+ const props = defineProps<{
14
+ editor: monaco.editor.IStandaloneCodeEditor | null;
15
+ meta: any;
16
+ }>();
17
+
18
+ const isBtnVisible = (btnKey: string) => {
19
+ const settings = props.meta?.topPanelSettings;
20
+ if (!settings || Object.keys(settings).length === 0) return true;
21
+ return settings[btnKey] !== undefined ? settings[btnKey] : true;
22
+ };
23
+
24
+ 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';
25
+
26
+ const fenceForCodeBlock = (text: string): string => {
27
+ let maxBackticks = 0;
28
+ let current = 0;
29
+ for (let i = 0; i < text.length; i++) {
30
+ if (text[i] === '`') { current++; if (current > maxBackticks) maxBackticks = current; }
31
+ else { current = 0; }
32
+ }
33
+ return '`'.repeat(Math.max(3, maxBackticks + 1));
34
+ };
35
+
36
+ const applyFormat = (type: string) => {
37
+ const editor = props.editor;
38
+ if (!editor) return;
39
+
40
+ const model = editor.getModel();
41
+ if (!model) return;
42
+
43
+ editor.focus();
44
+ const rawSelection = editor.getSelection();
45
+ if (!rawSelection) return;
46
+
47
+ const selection = rawSelection.startLineNumber !== rawSelection.endLineNumber && rawSelection.endColumn === 1
48
+ ? new monaco.Selection(rawSelection.startLineNumber, rawSelection.startColumn, rawSelection.endLineNumber - 1, model.getLineMaxColumn(rawSelection.endLineNumber - 1))
49
+ : rawSelection;
50
+
51
+ const selectedText = model.getValueInRange(selection);
52
+
53
+ const applyEdits = (id: string, edits: monaco.editor.IIdentifiedSingleEditOperation[]) => {
54
+ editor.executeEdits(id, edits);
55
+ };
56
+
57
+ switch (type) {
58
+ case 'bold': toggleWrapSmart(editor, '**'); break;
59
+ case 'italic': toggleWrapSmart(editor, '*'); break;
60
+ case 'strike': toggleWrapSmart(editor, '~~'); break;
61
+ case 'underline': toggleWrapSmart(editor, '<u>', '</u>'); break;
62
+ case 'codeBlock': {
63
+ const trimmed = selectedText.trim();
64
+ const match = trimmed.match(/^(`{3,})[^\n]*\n([\s\S]*)\n\1$/);
65
+ if (match) {
66
+ applyEdits('unwrap-code', [{ range: selection, text: match[2], forceMoveMarkers: true }]);
67
+ } else {
68
+ const fence = fenceForCodeBlock(selectedText);
69
+ applyEdits('wrap-code', [{ range: selection, text: `\n${fence}\n${selectedText}\n${fence}\n`, forceMoveMarkers: true }]);
70
+ }
71
+ break;
72
+ }
73
+ case 'link': {
74
+ const match = selectedText.trim().match(/^\[(.*?)\]\(.*?\)$/);
75
+ if (match) {
76
+ applyEdits('unlink', [{ range: selection, text: match[1], forceMoveMarkers: true }]);
77
+ } else {
78
+ applyEdits('insert-link', [{ range: selection, text: `[${selectedText}](url)`, forceMoveMarkers: true }]);
79
+ }
80
+ break;
81
+ }
82
+ case 'h1': case 'h2': case 'h3': case 'ul': case 'ol': {
83
+ const prefixMap: any = { h1: '# ', h2: '## ', h3: '### ', ul: '* ' };
84
+ const edits: any[] = [];
85
+ for (let i = selection.startLineNumber; i <= selection.endLineNumber; i++) {
86
+ const line = model.getLineContent(i);
87
+ const targetPrefix = type === 'ol' ? `${i - selection.startLineNumber + 1}. ` : prefixMap[type];
88
+ const match = line.match(/^(#{1,6}\s+|[*+-]\s+|\d+[.)]\s+)/);
89
+ if (match) {
90
+ edits.push({ range: new monaco.Range(i, 1, i, match[0].length + 1), text: match[0].trim() === targetPrefix.trim() ? '' : targetPrefix });
91
+ } else {
92
+ edits.push({ range: new monaco.Range(i, 1, i, 1), text: targetPrefix });
93
+ }
94
+ }
95
+ applyEdits('format-block', edits);
96
+ break;
97
+ }
98
+ }
99
+ };
100
+
101
+ const buttons = [
102
+ { id: 'bold', title: 'Bold', icon: markRaw(IconLetterBoldOutline), group: 1 },
103
+ { id: 'italic', title: 'Italic', icon: markRaw(IconLetterItalicOutline), group: 1 },
104
+ { id: 'underline', title: 'Underline', icon: markRaw(IconLetterUnderlineOutline), group: 1 },
105
+ { id: 'strike', title: 'Strike', icon: markRaw(IconTextSlashOutline), group: 1, separator: true },
106
+ { id: 'h1', title: 'H1', icon: markRaw(IconH116Solid), group: 2 },
107
+ { id: 'h2', title: 'H2', icon: markRaw(IconH216Solid), group: 2 },
108
+ { id: 'h3', title: 'H3', icon: markRaw(IconH316Solid), group: 2, separator: true },
109
+ { id: 'ul', title: 'UL', icon: markRaw(IconRectangleListOutline), group: 3 },
110
+ { id: 'ol', title: 'OL', icon: markRaw(IconOrderedListOutline), group: 3 },
111
+ { id: 'link', title: 'Link', icon: markRaw(IconLinkOutline), group: 3 },
112
+ { id: 'codeBlock', title: 'Code', icon: markRaw(IconCodeOutline), group: 3 },
113
+ ];
114
+ </script>
115
+
116
+ <template>
117
+ <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">
118
+ <template v-for="btn in buttons" :key="btn.id">
119
+ <button
120
+ v-if="isBtnVisible(btn.id)"
121
+ type="button"
122
+ @click="applyFormat(btn.id)"
123
+ :class="btnClass"
124
+ :title="btn.title"
125
+ >
126
+ <component :is="btn.icon" class="w-5 h-5" />
127
+ </button>
128
+ <div v-if="btn.separator && isBtnVisible(btn.id)" class="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-1"></div>
129
+ </template>
130
+ </div>
131
+ </template>
@@ -1,15 +1,17 @@
1
1
  <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
+ <TopPanelButtons :editor="editor" :meta="meta" />
4
+ <div
5
+ ref="editorContainer"
6
+ id="editor"
7
+ :class="[
8
+ 'text-sm block w-full transition-all box-border overflow-hidden rounded-b-lg border border-t-0 pt-3',
9
+ isFocused
10
+ ? 'ring-1 ring-lightPrimary border-lightPrimary dark:ring-darkPrimary dark:border-darkPrimary'
11
+ : 'border-gray-300 dark:border-gray-600',
12
+ ]"
13
+ ></div>
14
+ </div>
13
15
  </template>
14
16
 
15
17
  <script setup lang="ts">
@@ -19,6 +21,7 @@ import * as monaco from 'monaco-editor';
19
21
  import TurndownService from 'turndown';
20
22
  import { gfm, tables } from 'turndown-plugin-gfm';
21
23
  import { toggleWrapSmart } from './utils/monacoMarkdownToggle';
24
+ import TopPanelButtons from './topPanelButtons.vue';
22
25
 
23
26
  const props = defineProps<{
24
27
  column: any,
@@ -529,6 +532,33 @@ onMounted(async () => {
529
532
  editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyU, () => {
530
533
  toggleWrapSmart(editor!, '<u>', '</u>');
531
534
  });
535
+
536
+ disposables.push(
537
+ editor.onKeyDown((e) => {
538
+ if (e.keyCode !== monaco.KeyCode.Enter) {
539
+ return;
540
+ }
541
+ const pos = editor!.getPosition();
542
+ if (!pos) {
543
+ return;
544
+ }
545
+ const line = model!.getLineContent(pos.lineNumber);
546
+ const match = line.match(/^(\s*)([*+-]|\d+\.)\s+/);
547
+ if (!match) {
548
+ return;
549
+ }
550
+ e.preventDefault();
551
+
552
+ if (line.trim() === match[2].trim()) {
553
+ const range = new monaco.Range(pos.lineNumber, 1, pos.lineNumber, line.length + 1);
554
+ editor!.executeEdits('exit-list', [{ range, text: '', forceMoveMarkers: true }]);
555
+ } else {
556
+ const isNum = match[2].includes('.');
557
+ const next = isNum ? `${parseInt(match[2]) + 1}. ` : `${match[2]} `;
558
+ editor!.trigger('keyboard', 'type', { text: `\n${match[1]}${next}` });
559
+ }
560
+ }),
561
+ );
532
562
 
533
563
  editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK, () => {
534
564
  const selection = editor!.getSelection();
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "custom",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "custom",
9
+ "version": "1.0.0",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "dompurify": "^3.2.4",
13
+ "marked": "^15.0.7",
14
+ "monaco-editor": "^0.45.0",
15
+ "turndown": "^7.2.2",
16
+ "turndown-plugin-gfm": "^1.0.2"
17
+ }
18
+ },
19
+ "node_modules/@mixmark-io/domino": {
20
+ "version": "2.2.0",
21
+ "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
22
+ "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
23
+ "license": "BSD-2-Clause"
24
+ },
25
+ "node_modules/@types/trusted-types": {
26
+ "version": "2.0.7",
27
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
28
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
29
+ "license": "MIT",
30
+ "optional": true
31
+ },
32
+ "node_modules/dompurify": {
33
+ "version": "3.3.2",
34
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
35
+ "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
36
+ "license": "(MPL-2.0 OR Apache-2.0)",
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "optionalDependencies": {
41
+ "@types/trusted-types": "^2.0.7"
42
+ }
43
+ },
44
+ "node_modules/marked": {
45
+ "version": "15.0.12",
46
+ "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
47
+ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
48
+ "license": "MIT",
49
+ "bin": {
50
+ "marked": "bin/marked.js"
51
+ },
52
+ "engines": {
53
+ "node": ">= 18"
54
+ }
55
+ },
56
+ "node_modules/monaco-editor": {
57
+ "version": "0.45.0",
58
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.45.0.tgz",
59
+ "integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==",
60
+ "license": "MIT"
61
+ },
62
+ "node_modules/turndown": {
63
+ "version": "7.2.2",
64
+ "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz",
65
+ "integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==",
66
+ "license": "MIT",
67
+ "dependencies": {
68
+ "@mixmark-io/domino": "^2.2.0"
69
+ }
70
+ },
71
+ "node_modules/turndown-plugin-gfm": {
72
+ "version": "1.0.2",
73
+ "resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz",
74
+ "integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==",
75
+ "license": "MIT"
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,131 @@
1
+ <script setup lang="ts">
2
+ import { markRaw } from 'vue';
3
+ import * as monaco from 'monaco-editor';
4
+ import { toggleWrapSmart } from './utils/monacoMarkdownToggle';
5
+
6
+ import {
7
+ IconLinkOutline, IconCodeOutline, IconRectangleListOutline,
8
+ IconOrderedListOutline, IconLetterBoldOutline, IconLetterUnderlineOutline,
9
+ IconLetterItalicOutline, IconTextSlashOutline
10
+ } from '@iconify-prerendered/vue-flowbite';
11
+ import { IconH116Solid, IconH216Solid, IconH316Solid } from '@iconify-prerendered/vue-heroicons';
12
+
13
+ const props = defineProps<{
14
+ editor: monaco.editor.IStandaloneCodeEditor | null;
15
+ meta: any;
16
+ }>();
17
+
18
+ const isBtnVisible = (btnKey: string) => {
19
+ const settings = props.meta?.topPanelSettings;
20
+ if (!settings || Object.keys(settings).length === 0) return true;
21
+ return settings[btnKey] !== undefined ? settings[btnKey] : true;
22
+ };
23
+
24
+ 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';
25
+
26
+ const fenceForCodeBlock = (text: string): string => {
27
+ let maxBackticks = 0;
28
+ let current = 0;
29
+ for (let i = 0; i < text.length; i++) {
30
+ if (text[i] === '`') { current++; if (current > maxBackticks) maxBackticks = current; }
31
+ else { current = 0; }
32
+ }
33
+ return '`'.repeat(Math.max(3, maxBackticks + 1));
34
+ };
35
+
36
+ const applyFormat = (type: string) => {
37
+ const editor = props.editor;
38
+ if (!editor) return;
39
+
40
+ const model = editor.getModel();
41
+ if (!model) return;
42
+
43
+ editor.focus();
44
+ const rawSelection = editor.getSelection();
45
+ if (!rawSelection) return;
46
+
47
+ const selection = rawSelection.startLineNumber !== rawSelection.endLineNumber && rawSelection.endColumn === 1
48
+ ? new monaco.Selection(rawSelection.startLineNumber, rawSelection.startColumn, rawSelection.endLineNumber - 1, model.getLineMaxColumn(rawSelection.endLineNumber - 1))
49
+ : rawSelection;
50
+
51
+ const selectedText = model.getValueInRange(selection);
52
+
53
+ const applyEdits = (id: string, edits: monaco.editor.IIdentifiedSingleEditOperation[]) => {
54
+ editor.executeEdits(id, edits);
55
+ };
56
+
57
+ switch (type) {
58
+ case 'bold': toggleWrapSmart(editor, '**'); break;
59
+ case 'italic': toggleWrapSmart(editor, '*'); break;
60
+ case 'strike': toggleWrapSmart(editor, '~~'); break;
61
+ case 'underline': toggleWrapSmart(editor, '<u>', '</u>'); break;
62
+ case 'codeBlock': {
63
+ const trimmed = selectedText.trim();
64
+ const match = trimmed.match(/^(`{3,})[^\n]*\n([\s\S]*)\n\1$/);
65
+ if (match) {
66
+ applyEdits('unwrap-code', [{ range: selection, text: match[2], forceMoveMarkers: true }]);
67
+ } else {
68
+ const fence = fenceForCodeBlock(selectedText);
69
+ applyEdits('wrap-code', [{ range: selection, text: `\n${fence}\n${selectedText}\n${fence}\n`, forceMoveMarkers: true }]);
70
+ }
71
+ break;
72
+ }
73
+ case 'link': {
74
+ const match = selectedText.trim().match(/^\[(.*?)\]\(.*?\)$/);
75
+ if (match) {
76
+ applyEdits('unlink', [{ range: selection, text: match[1], forceMoveMarkers: true }]);
77
+ } else {
78
+ applyEdits('insert-link', [{ range: selection, text: `[${selectedText}](url)`, forceMoveMarkers: true }]);
79
+ }
80
+ break;
81
+ }
82
+ case 'h1': case 'h2': case 'h3': case 'ul': case 'ol': {
83
+ const prefixMap: any = { h1: '# ', h2: '## ', h3: '### ', ul: '* ' };
84
+ const edits: any[] = [];
85
+ for (let i = selection.startLineNumber; i <= selection.endLineNumber; i++) {
86
+ const line = model.getLineContent(i);
87
+ const targetPrefix = type === 'ol' ? `${i - selection.startLineNumber + 1}. ` : prefixMap[type];
88
+ const match = line.match(/^(#{1,6}\s+|[*+-]\s+|\d+[.)]\s+)/);
89
+ if (match) {
90
+ edits.push({ range: new monaco.Range(i, 1, i, match[0].length + 1), text: match[0].trim() === targetPrefix.trim() ? '' : targetPrefix });
91
+ } else {
92
+ edits.push({ range: new monaco.Range(i, 1, i, 1), text: targetPrefix });
93
+ }
94
+ }
95
+ applyEdits('format-block', edits);
96
+ break;
97
+ }
98
+ }
99
+ };
100
+
101
+ const buttons = [
102
+ { id: 'bold', title: 'Bold', icon: markRaw(IconLetterBoldOutline), group: 1 },
103
+ { id: 'italic', title: 'Italic', icon: markRaw(IconLetterItalicOutline), group: 1 },
104
+ { id: 'underline', title: 'Underline', icon: markRaw(IconLetterUnderlineOutline), group: 1 },
105
+ { id: 'strike', title: 'Strike', icon: markRaw(IconTextSlashOutline), group: 1, separator: true },
106
+ { id: 'h1', title: 'H1', icon: markRaw(IconH116Solid), group: 2 },
107
+ { id: 'h2', title: 'H2', icon: markRaw(IconH216Solid), group: 2 },
108
+ { id: 'h3', title: 'H3', icon: markRaw(IconH316Solid), group: 2, separator: true },
109
+ { id: 'ul', title: 'UL', icon: markRaw(IconRectangleListOutline), group: 3 },
110
+ { id: 'ol', title: 'OL', icon: markRaw(IconOrderedListOutline), group: 3 },
111
+ { id: 'link', title: 'Link', icon: markRaw(IconLinkOutline), group: 3 },
112
+ { id: 'codeBlock', title: 'Code', icon: markRaw(IconCodeOutline), group: 3 },
113
+ ];
114
+ </script>
115
+
116
+ <template>
117
+ <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">
118
+ <template v-for="btn in buttons" :key="btn.id">
119
+ <button
120
+ v-if="isBtnVisible(btn.id)"
121
+ type="button"
122
+ @click="applyFormat(btn.id)"
123
+ :class="btnClass"
124
+ :title="btn.title"
125
+ >
126
+ <component :is="btn.icon" class="w-5 h-5" />
127
+ </button>
128
+ <div v-if="btn.separator && isBtnVisible(btn.id)" class="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-1"></div>
129
+ </template>
130
+ </div>
131
+ </template>
package/dist/index.js CHANGED
@@ -41,7 +41,7 @@ export default class MarkdownPlugin extends AdminForthPlugin {
41
41
  modifyResourceConfig: { get: () => super.modifyResourceConfig }
42
42
  });
43
43
  return __awaiter(this, void 0, void 0, function* () {
44
- var _a, _b;
44
+ var _a, _b, _c;
45
45
  _super.modifyResourceConfig.call(this, adminforth, resourceConfig);
46
46
  this.resourceConfig = resourceConfig;
47
47
  const fieldName = this.options.fieldName;
@@ -117,6 +117,21 @@ export default class MarkdownPlugin extends AdminForthPlugin {
117
117
  uploadPluginInstanceId: (_b = this.uploadPlugin) === null || _b === void 0 ? void 0 : _b.pluginInstanceId,
118
118
  },
119
119
  };
120
+ const topPanelSettings = this.options.topPanelSettings || {};
121
+ const commonMeta = {
122
+ pluginInstanceId: this.pluginInstanceId,
123
+ columnName: fieldName,
124
+ uploadPluginInstanceId: (_c = this.uploadPlugin) === null || _c === void 0 ? void 0 : _c.pluginInstanceId,
125
+ topPanelSettings: topPanelSettings,
126
+ };
127
+ column.components.edit = {
128
+ file: this.componentPath("MarkdownEditor.vue"),
129
+ meta: commonMeta,
130
+ };
131
+ column.components.create = {
132
+ file: this.componentPath("MarkdownEditor.vue"),
133
+ meta: commonMeta,
134
+ };
120
135
  const editorRecordPkField = resourceConfig.columns.find(c => c.primaryKey);
121
136
  if (this.options.attachments) {
122
137
  const stripQueryAndHash = (value) => value.split('#')[0].split('?')[0];
package/index.ts CHANGED
@@ -126,6 +126,26 @@ export default class MarkdownPlugin extends AdminForthPlugin {
126
126
  uploadPluginInstanceId: this.uploadPlugin?.pluginInstanceId,
127
127
  },
128
128
  };
129
+
130
+ const topPanelSettings = this.options.topPanelSettings || {};
131
+
132
+ const commonMeta = {
133
+ pluginInstanceId: this.pluginInstanceId,
134
+ columnName: fieldName,
135
+ uploadPluginInstanceId: this.uploadPlugin?.pluginInstanceId,
136
+ topPanelSettings: topPanelSettings,
137
+ };
138
+
139
+ column.components.edit = {
140
+ file: this.componentPath("MarkdownEditor.vue"),
141
+ meta: commonMeta,
142
+ };
143
+
144
+ column.components.create = {
145
+ file: this.componentPath("MarkdownEditor.vue"),
146
+ meta: commonMeta,
147
+ };
148
+
129
149
  const editorRecordPkField = resourceConfig.columns.find(c => c.primaryKey);
130
150
  if (this.options.attachments) {
131
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/markdown",
3
- "version": "1.10.3",
3
+ "version": "1.10.5",
4
4
  "description": "Markdown plugin for adminforth",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/types.ts CHANGED
@@ -53,4 +53,33 @@ export interface PluginOptions {
53
53
  */
54
54
  attachmentAltFieldName?: string; // e.g. 'alt',
55
55
  },
56
+
57
+ /**
58
+ * Optional configuration for the editor's top toolbar (formatting panel).
59
+ *
60
+ * If `topPanelSettings` is omitted, the editor uses its internal default
61
+ * toolbar configuration.
62
+ *
63
+ * If `topPanelSettings` is provided as an empty object, all controls behave
64
+ * as if their flags were `undefined`, i.e. they also fall back to the same
65
+ * internal defaults.
66
+ *
67
+ * For each flag below:
68
+ * - `true` – explicitly enable/show the control in the top panel.
69
+ * - `false` – explicitly disable/hide the control.
70
+ * - `undefined` – use the editor's default behavior for that control.
71
+ */
72
+ topPanelSettings?: {
73
+ bold?: boolean;
74
+ italic?: boolean;
75
+ underline?: boolean;
76
+ strike?: boolean;
77
+ h1?: boolean;
78
+ h2?: boolean;
79
+ h3?: boolean;
80
+ ul?: boolean;
81
+ ol?: boolean;
82
+ link?: boolean;
83
+ codeBlock?: boolean;
84
+ };
56
85
  }