@adminforth/markdown 1.10.4 → 1.10.6

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
@@ -9,9 +9,10 @@ custom/MarkdownRenderer.vue
9
9
  custom/package-lock.json
10
10
  custom/package.json
11
11
  custom/pnpm-lock.yaml
12
+ custom/topPanelButtons.vue
12
13
  custom/tsconfig.json
13
14
  custom/utils/
14
15
  custom/utils/monacoMarkdownToggle.ts
15
16
 
16
- sent 38,879 bytes received 161 bytes 78,080.00 bytes/sec
17
- total size is 38,266 speedup is 0.98
17
+ sent 45,576 bytes received 180 bytes 91,512.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,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,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];
@@ -155,7 +170,7 @@ export default class MarkdownPlugin extends AdminForthPlugin {
155
170
  if (!key) {
156
171
  return null;
157
172
  }
158
- return key;
173
+ return decodeURIComponent(key);
159
174
  };
160
175
  const upsertMeta = (byKey, key, next) => {
161
176
  var _a, _b;
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
 
@@ -168,7 +188,7 @@ export default class MarkdownPlugin extends AdminForthPlugin {
168
188
  if (!key) {
169
189
  return null;
170
190
  }
171
- return key;
191
+ return decodeURIComponent(key);
172
192
  };
173
193
 
174
194
  const upsertMeta = (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/markdown",
3
- "version": "1.10.4",
3
+ "version": "1.10.6",
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
  }