cabloy 5.1.244 → 5.1.246

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/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.244
1
+ 5.1.246
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.246
4
+
5
+ ### Features
6
+
7
+ - Add `APP_LOCALE_DEFAULT` to configure the default application locale.
8
+
9
+ ### Improvements
10
+
11
+ - Enhance Markdown source mode.
12
+ - Update dependencies in the pnpm lockfile.
13
+
14
+ ## 5.1.245
15
+
16
+ ### Improvements
17
+
18
+ - Update the home user account tests.
19
+
3
20
  ## 5.1.244
4
21
 
5
22
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.244",
3
+ "version": "5.1.246",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -492,8 +492,8 @@ test(
492
492
  await expect(userMenu).toHaveCount(1);
493
493
  await expect(userMenu.locator('summary')).toContainText(profileName);
494
494
  await userMenu.locator('summary').click();
495
- const links = userMenu.locator('ul > li > a');
496
- await expect(links).toHaveText(['Account Settings', 'Logout']);
495
+ const actions = userMenu.locator('ul > li > a, ul > li > button');
496
+ await expect(actions).toHaveText(['Account Settings', 'Logout']);
497
497
  await userMenu.getByText('Account Settings', { exact: true }).click();
498
498
  await expect(page).toHaveURL(accountPath);
499
499
  await expect(page.getByRole('heading', { name: 'Account Settings' })).toBeVisible();
@@ -329,6 +329,22 @@ const product = '${suffix}';
329
329
  await expect(editor).toBeVisible();
330
330
  const toolbar = adminPage.getByRole('toolbar', { name: 'Markdown toolbar' });
331
331
  await expect(toolbar).toBeVisible();
332
+ const sourceButton = toolbar.getByRole('button', { name: 'Source editing mode' });
333
+ const sourceInput = adminPage.getByRole('textbox', { name: 'Markdown source' });
334
+ await expect(sourceButton).toHaveAttribute('type', 'button');
335
+ await expect(sourceButton).toHaveAttribute('aria-pressed', 'false');
336
+ await sourceButton.click();
337
+ await expect(sourceButton).toHaveAttribute('aria-pressed', 'true');
338
+ await expect(sourceInput).toHaveValue(markdown);
339
+ await expect(editor).toBeHidden();
340
+ await expect(toolbar.getByRole('button', { name: 'Bold', exact: true })).toHaveCount(0);
341
+ await sourceInput.click();
342
+ await expect(sourceInput).toBeFocused();
343
+ await sourceButton.click();
344
+ await expect(editor).toBeVisible();
345
+ await sourceButton.click();
346
+ await expect(sourceInput).toHaveValue(markdown);
347
+ await sourceButton.click();
332
348
  const undoButton = toolbar.getByRole('button', { name: 'Undo' });
333
349
  await expect(toolbar.getByRole('combobox', { name: 'Code block language' })).toHaveCount(0);
334
350
  const codeBlockToolbar = adminPage.getByRole('toolbar', { name: 'Code block language' });
@@ -456,20 +472,40 @@ const product = '${suffix}';
456
472
  await expect(tableToolbar).toBeHidden();
457
473
  expect(adminPageErrors).toEqual([]);
458
474
 
475
+ await sourceButton.click();
476
+ expect((await sourceInput.inputValue()).trimEnd()).toBe(
477
+ markdown.replace('```ts', '```python'),
478
+ );
479
+ await expect(tableToolbar).toBeHidden();
480
+ await expect(codeBlockToolbar).toBeHidden();
481
+ const sourceMarkdown = `${markdown.replace('```ts', '```python')}\n\n## Source heading\n\nSource **content**`;
482
+ await sourceInput.fill(sourceMarkdown);
483
+ await sourceButton.click();
484
+ await expect(editor.locator('h2')).toHaveText('Source heading');
485
+ await expect(editor.locator('strong')).toHaveText('content');
486
+ await sourceButton.click();
487
+ await expect(sourceInput).toHaveValue(sourceMarkdown);
488
+ const savedMarkdown = `${sourceMarkdown}\n\n- Saved from source\n`;
489
+ await sourceInput.fill(savedMarkdown);
490
+
459
491
  const productUpdate = waitForApiResponse(
460
492
  adminPage,
461
493
  'PATCH',
462
494
  `${productActionPath}/${productId}`,
463
495
  );
464
496
  await adminPage.getByRole('button', { name: 'Submit', exact: true }).click();
465
- await productUpdate;
497
+ const updateResponse = await productUpdate;
498
+ expect(updateResponse.request().postDataJSON().productContentForm.descriptionMarkdown).toBe(
499
+ savedMarkdown,
500
+ );
466
501
  const productResponseAfterUpdate = await adminPage.request.get(
467
502
  `${productActionPath}/${productId}`,
468
503
  { headers },
469
504
  );
470
505
  expect(productResponseAfterUpdate.ok()).toBeTruthy();
506
+ // Product content persistence trims boundary whitespace on the server.
471
507
  expect((await productResponseAfterUpdate.json()).productContentForm.descriptionMarkdown).toBe(
472
- markdown.replace('```ts', '```python'),
508
+ savedMarkdown.trim(),
473
509
  );
474
510
  expect(adminPageErrors).toEqual([]);
475
511
  } finally {
package/vona/env/.env CHANGED
@@ -3,6 +3,7 @@
3
3
  APP_NAME = cabloy-basic
4
4
  APP_TITLE = Cabloy Basic
5
5
  APP_VERSION = 5.0.0
6
+ APP_LOCALE_DEFAULT = en-us
6
7
 
7
8
  # server
8
9
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli",
3
- "version": "1.1.175",
3
+ "version": "1.1.176",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli-set-api",
3
- "version": "1.1.173",
3
+ "version": "1.1.174",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli-set-api",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona",
3
- "version": "5.1.123",
3
+ "version": "5.1.124",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-core",
3
- "version": "5.1.53",
3
+ "version": "5.1.54",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona",
6
6
  "keywords": [
@@ -7,6 +7,7 @@ export interface VonaConfigEnv {
7
7
  VONA_TEST_RUN_ID: string | undefined;
8
8
  APP_TITLE: string | undefined;
9
9
  APP_VERSION: string | undefined;
10
+ APP_LOCALE_DEFAULT: string | undefined;
10
11
  SERVER_KEYS: string | undefined;
11
12
  SERVER_GLOBALPREFIX: string | undefined;
12
13
  SERVER_PUBLICDIR: string | undefined;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-mock",
3
- "version": "6.1.53",
3
+ "version": "6.1.54",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona mock",
6
6
  "keywords": [
@@ -1990,7 +1990,7 @@ importers:
1990
1990
  specifier: ^5.0.3
1991
1991
  version: link:modules/a-markdown
1992
1992
  vona-module-a-rbac:
1993
- specifier: ^5.0.18
1993
+ specifier: ^5.0.19
1994
1994
  version: link:modules/a-rbac
1995
1995
  vona-module-a-socket:
1996
1996
  specifier: workspace:^
@@ -1,4 +1,9 @@
1
- import type { ILoggerOptionsClientInfo, VonaApplication, VonaConfigOptional } from 'vona';
1
+ import type {
2
+ ILocaleRecord,
3
+ ILoggerOptionsClientInfo,
4
+ VonaApplication,
5
+ VonaConfigOptional,
6
+ } from 'vona';
2
7
  import type { IMailClientRecord, TypeMailTransportService } from 'vona-module-a-mail';
3
8
  import type { IDatabaseClientRecord } from 'vona-module-a-orm';
4
9
 
@@ -45,7 +50,7 @@ export default async function (app: VonaApplication) {
45
50
  'en-us': 'LanguageEnglish',
46
51
  'zh-cn': 'LanguageChinese',
47
52
  },
48
- default: 'en-us',
53
+ default: (env.APP_LOCALE_DEFAULT || 'en-us') as keyof ILocaleRecord,
49
54
  };
50
55
 
51
56
  // instance
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-core",
3
- "version": "5.1.69",
3
+ "version": "5.1.70",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-vona",
3
- "version": "5.1.114",
3
+ "version": "5.1.115",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -81,7 +81,7 @@ export default function (sys: ZovaSys) {
81
81
 
82
82
  // locale
83
83
  config.locale = {
84
- default: env.APP_LOCALE_DEFAULT as keyof ILocaleRecord,
84
+ default: (env.APP_LOCALE_DEFAULT || 'en-us') as keyof ILocaleRecord,
85
85
  storeKey: 'locale',
86
86
  items: {
87
87
  'en-us': 'LanguageEnglish',
@@ -1,7 +1,12 @@
1
- import { BeanControllerBase, Use, usePrepareArg } from 'zova';
1
+ import { BeanControllerBase, ClientOnly, Use, usePrepareArg } from 'zova';
2
2
  import { Controller } from 'zova-module-a-bean';
3
+ import { $icon } from 'zova-module-a-icon';
3
4
  import { ZRouterViewEmpty } from 'zova-module-a-router';
4
- import { IServiceSsrLayoutOptions, ServiceSsrLayout } from 'zova-module-home-base';
5
+ import {
6
+ closeNearestDetails,
7
+ IServiceSsrLayoutOptions,
8
+ ServiceSsrLayout,
9
+ } from 'zova-module-home-base';
5
10
 
6
11
  export interface ControllerLayoutEmptyProps {}
7
12
 
@@ -18,7 +23,52 @@ export class ControllerLayoutEmpty extends BeanControllerBase {
18
23
  } satisfies IServiceSsrLayoutOptions);
19
24
  }
20
25
 
26
+ protected renderLocale() {
27
+ const currentRoute = this.$currentRoute;
28
+ const metaLocale = currentRoute?.meta?.locale;
29
+ const locales = this.app.meta.locale.getItems(currentRoute?.meta?.locales);
30
+ return (
31
+ <li>
32
+ <details>
33
+ <summary aria-label="Language">{$icon('::language', 24)}</summary>
34
+ <ClientOnly>
35
+ <ul class="bg-base-100 rounded-t-none p-2 w-48 right-0">
36
+ {Object.keys(locales).map(key => {
37
+ const title = this.$scopeBase.locale[locales[key]]();
38
+ return (
39
+ <li key={key} class={this.app.meta.locale.current === key ? 'disabled' : ''}>
40
+ <a
41
+ onClick={event => {
42
+ if (metaLocale) {
43
+ this.app.$gotoPage(currentRoute.matched[0].path, {
44
+ params: { ...currentRoute.params, locale: key },
45
+ query: currentRoute.query,
46
+ });
47
+ } else {
48
+ this.app.meta.locale.current = key as any;
49
+ }
50
+ closeNearestDetails(event);
51
+ }}
52
+ >
53
+ {$icon(this.app.meta.locale.current === key ? '::done' : '::none', 24)}
54
+ {title}
55
+ </a>
56
+ </li>
57
+ );
58
+ })}
59
+ </ul>
60
+ </ClientOnly>
61
+ </details>
62
+ </li>
63
+ );
64
+ }
65
+
21
66
  protected render() {
22
- return <ZRouterViewEmpty></ZRouterViewEmpty>;
67
+ return (
68
+ <>
69
+ <ZRouterViewEmpty></ZRouterViewEmpty>
70
+ <ul class="menu menu-horizontal fixed top-4 right-4 z-50">{this.renderLocale()}</ul>
71
+ </>
72
+ );
23
73
  }
24
74
  }
@@ -102,6 +102,8 @@ export class ControllerFormFieldMarkdown extends BeanControllerBase {
102
102
  editor?: Editor;
103
103
  value = '';
104
104
  readonly = false;
105
+ sourceMode = false;
106
+ private _sourceModeValue?: string;
105
107
  toolbarState: IMarkdownToolbarState = this._emptyToolbarState();
106
108
  tableToolbarPosition?: IMarkdownContextualToolbarPosition;
107
109
  codeBlockToolbarPosition?: IMarkdownContextualToolbarPosition;
@@ -232,8 +234,48 @@ export class ControllerFormFieldMarkdown extends BeanControllerBase {
232
234
  this._scheduleContextualToolbarPosition();
233
235
  }
234
236
 
235
- private _runCommand(command: (editor: Editor) => boolean) {
237
+ public toggleSourceMode() {
236
238
  if (this.readonly || !this.editor) return;
239
+ if (this.sourceMode) {
240
+ this._leaveSourceMode();
241
+ } else {
242
+ this._sourceModeValue = this.value;
243
+ this.sourceMode = true;
244
+ this.closeTablePicker();
245
+ this._imageUploadSelection = undefined;
246
+ this._setTableToolbarPosition();
247
+ this._setCodeBlockToolbarPosition();
248
+ }
249
+ }
250
+
251
+ private _leaveSourceMode() {
252
+ if (this.value !== this._sourceModeValue) this._syncEditorValue(this.value);
253
+ this.sourceMode = false;
254
+ this._sourceModeValue = undefined;
255
+ this._refreshToolbarState();
256
+ }
257
+
258
+ public handleSourceInput(value: string) {
259
+ if (this.readonly || !this.sourceMode) return;
260
+ this._setValue?.(value);
261
+ }
262
+
263
+ public handleSourceBlur() {
264
+ this._handleBlur?.();
265
+ }
266
+
267
+ private _syncEditorValue(value: string) {
268
+ if (!this.editor) return;
269
+ this._syncing = true;
270
+ try {
271
+ this.editor.commands.setContent(value, { contentType: 'markdown', emitUpdate: false });
272
+ } finally {
273
+ this._syncing = false;
274
+ }
275
+ }
276
+
277
+ private _runCommand(command: (editor: Editor) => boolean) {
278
+ if (this.readonly || this.sourceMode || !this.editor) return;
237
279
  command(this.editor);
238
280
  }
239
281
 
@@ -242,6 +284,7 @@ export class ControllerFormFieldMarkdown extends BeanControllerBase {
242
284
  return !!(
243
285
  editor &&
244
286
  !this.readonly &&
287
+ !this.sourceMode &&
245
288
  editor.isEditable &&
246
289
  editor.isActive('table') &&
247
290
  (editor.view.hasFocus() || this._tableToolbarFocused)
@@ -276,6 +319,7 @@ export class ControllerFormFieldMarkdown extends BeanControllerBase {
276
319
  return !!(
277
320
  editor &&
278
321
  !this.readonly &&
322
+ !this.sourceMode &&
279
323
  editor.isEditable &&
280
324
  (editor.isActive('codeBlock') || (this.toolbarState.codeBlock && toolbarInteraction)) &&
281
325
  (editor.view.hasFocus() || toolbarInteraction)
@@ -535,7 +579,7 @@ export class ControllerFormFieldMarkdown extends BeanControllerBase {
535
579
 
536
580
  public async editLink() {
537
581
  const editor = this.editor;
538
- if (this.readonly || !editor) return;
582
+ if (this.readonly || this.sourceMode || !editor) return;
539
583
  const selection = editor.state.selection;
540
584
  const linkActive = editor.isActive('link');
541
585
  const canEditLink = linkActive
@@ -550,7 +594,7 @@ export class ControllerFormFieldMarkdown extends BeanControllerBase {
550
594
  text: this.scope.locale.LinkUrl(),
551
595
  defaultValue: href,
552
596
  });
553
- if (this.readonly || this.editor !== editor || result === undefined) return;
597
+ if (this.readonly || this.sourceMode || this.editor !== editor || result === undefined) return;
554
598
 
555
599
  const value = result.trim();
556
600
  const chain = editor.chain().focus().setTextSelection({
@@ -644,7 +688,7 @@ export class ControllerFormFieldMarkdown extends BeanControllerBase {
644
688
 
645
689
  public beginImageUpload(chooseFiles: () => void) {
646
690
  const editor = this.editor;
647
- if (this.readonly || !editor || !editor.isEditable) return;
691
+ if (this.readonly || this.sourceMode || !editor || !editor.isEditable) return;
648
692
  this._imageUploadSelection = {
649
693
  from: editor.state.selection.from,
650
694
  to: editor.state.selection.to,
@@ -654,6 +698,7 @@ export class ControllerFormFieldMarkdown extends BeanControllerBase {
654
698
  }
655
699
 
656
700
  public handleImageUploaded(result: IImageUploaderResult) {
701
+ if (this.sourceMode || !this._imageUploadSelection) return;
657
702
  const editor = this.editor;
658
703
  const selection = this._imageUploadSelection;
659
704
  const src = this._resolveImageUrl(result.url);
@@ -743,7 +788,7 @@ export class ControllerFormFieldMarkdown extends BeanControllerBase {
743
788
  },
744
789
  },
745
790
  onUpdate: ({ editor }) => {
746
- if (!this._syncing) {
791
+ if (!this._syncing && !this.sourceMode) {
747
792
  this._setValue?.(editor.getMarkdown());
748
793
  }
749
794
  },
@@ -765,16 +810,15 @@ export class ControllerFormFieldMarkdown extends BeanControllerBase {
765
810
  this.$watch(
766
811
  () => this.value,
767
812
  value => {
768
- if (!this.editor || this.editor.getMarkdown() === value) return;
769
- this._syncing = true;
770
- this.editor.commands.setContent(value, { contentType: 'markdown' });
771
- this._syncing = false;
813
+ if (this.sourceMode || !this.editor || this.editor.getMarkdown() === value) return;
814
+ this._syncEditorValue(value);
772
815
  },
773
816
  );
774
817
  this.$watch(
775
818
  () => this.readonly,
776
819
  readonly => {
777
- this.editor?.setEditable(!readonly);
820
+ if (readonly && this.sourceMode) this._leaveSourceMode();
821
+ this.editor?.setEditable(!readonly, false);
778
822
  if (readonly) this.closeTablePicker();
779
823
  this._refreshToolbarState();
780
824
  },
@@ -189,123 +189,133 @@ export class RenderFormFieldMarkdown extends BeanRenderBase {
189
189
  role="toolbar"
190
190
  aria-label={this.scope.locale.MarkdownToolbar()}
191
191
  >
192
- {this._toolbarButton(':editor:undo', this.scope.locale.Undo(), () => this.undo(), {
193
- disabled: !state.canUndo,
194
- })}
195
- {this._toolbarButton(':editor:redo', this.scope.locale.Redo(), () => this.redo(), {
196
- disabled: !state.canRedo,
197
- })}
198
- <select
199
- class="select select-sm select-bordered w-auto min-w-28"
200
- aria-label={this.scope.locale.BlockStyle()}
201
- value={state.headingLevel ? `heading-${state.headingLevel}` : 'paragraph'}
202
- onMousedown={event => {
203
- event.stopPropagation();
204
- }}
205
- onChange={event => {
206
- event.stopPropagation();
207
- const value = (event.target as HTMLSelectElement).value;
208
- if (value === 'paragraph') {
209
- this.setParagraph();
210
- } else if (value.startsWith('heading-')) {
211
- this.setHeading(Number(value.slice('heading-'.length)) as 1 | 2 | 3 | 4 | 5 | 6);
212
- }
213
- }}
214
- >
215
- <option value="paragraph">{this.scope.locale.Paragraph()}</option>
216
- <option value="heading-1">{this.scope.locale.Heading1()}</option>
217
- <option value="heading-2">{this.scope.locale.Heading2()}</option>
218
- <option value="heading-3">{this.scope.locale.Heading3()}</option>
219
- <option value="heading-4">{this.scope.locale.Heading4()}</option>
220
- <option value="heading-5">{this.scope.locale.Heading5()}</option>
221
- <option value="heading-6">{this.scope.locale.Heading6()}</option>
222
- </select>
223
- {this._toolbarButton(
224
- ':editor:format-bold',
225
- this.scope.locale.Bold(),
226
- () => this.toggleBold(),
227
- { active: state.bold, disabled: !state.canBold },
228
- )}
229
- {this._toolbarButton(
230
- ':editor:format-italic',
231
- this.scope.locale.Italic(),
232
- () => this.toggleItalic(),
233
- { active: state.italic, disabled: !state.canItalic },
234
- )}
235
- {this._toolbarButton(
236
- ':editor:format-strikethrough',
237
- this.scope.locale.Strikethrough(),
238
- () => this.toggleStrike(),
239
- { active: state.strike, disabled: !state.canStrike },
240
- )}
241
- {this._toolbarButton(
242
- ':editor:code',
243
- this.scope.locale.InlineCode(),
244
- () => this.toggleCode(),
245
- { active: state.code, disabled: !state.canCode },
246
- )}
247
192
  {this._toolbarButton(
248
- ':editor:insert-link-outline',
249
- this.scope.locale.Link(),
250
- () => {
251
- void this.editLink();
252
- },
253
- { active: state.link, disabled: !state.canLink },
193
+ ':editor:source-outline',
194
+ this.scope.locale.SourceMode(),
195
+ () => this.toggleSourceMode(),
196
+ { active: this.sourceMode, disabled: !this.editor || imageUploader?.isUploading },
254
197
  )}
255
- {this._toolbarButton(
256
- ':editor:bookmark',
257
- this.scope.locale.Highlight(),
258
- () => this.toggleHighlight(),
259
- { active: state.highlight, disabled: !state.canHighlight },
260
- )}
261
- {this._toolbarButton(
262
- ':editor:format-list-bulleted',
263
- this.scope.locale.BulletList(),
264
- () => this.toggleBulletList(),
265
- { active: state.bulletList, disabled: !state.canBulletList },
266
- )}
267
- {this._toolbarButton(
268
- ':editor:format-list-numbered',
269
- this.scope.locale.OrderedList(),
270
- () => this.toggleOrderedList(),
271
- { active: state.orderedList, disabled: !state.canOrderedList },
272
- )}
273
- {this._toolbarButton(
274
- ':editor:task-alt',
275
- this.scope.locale.TaskList(),
276
- () => this.toggleTaskList(),
277
- { active: state.taskList, disabled: !state.canTaskList },
278
- )}
279
- {this._toolbarButton(
280
- ':editor:format-quote',
281
- this.scope.locale.Blockquote(),
282
- () => this.toggleBlockquote(),
283
- { active: state.blockquote, disabled: !state.canBlockquote },
284
- )}
285
- {this._toolbarButton(
286
- ':editor:code-block',
287
- this.scope.locale.CodeBlock(),
288
- () => this.toggleCodeBlock(),
289
- { active: state.codeBlock, disabled: !state.canCodeBlock },
290
- )}
291
- {this._toolbarButton(
292
- ':editor:horizontal-rule',
293
- this.scope.locale.HorizontalRule(),
294
- () => this.setHorizontalRule(),
295
- { disabled: !state.canHorizontalRule },
198
+ {!this.sourceMode && (
199
+ <>
200
+ {this._toolbarButton(':editor:undo', this.scope.locale.Undo(), () => this.undo(), {
201
+ disabled: !state.canUndo,
202
+ })}
203
+ {this._toolbarButton(':editor:redo', this.scope.locale.Redo(), () => this.redo(), {
204
+ disabled: !state.canRedo,
205
+ })}
206
+ <select
207
+ class="select select-sm select-bordered w-auto min-w-28"
208
+ aria-label={this.scope.locale.BlockStyle()}
209
+ value={state.headingLevel ? `heading-${state.headingLevel}` : 'paragraph'}
210
+ onMousedown={event => {
211
+ event.stopPropagation();
212
+ }}
213
+ onChange={event => {
214
+ event.stopPropagation();
215
+ const value = (event.target as HTMLSelectElement).value;
216
+ if (value === 'paragraph') {
217
+ this.setParagraph();
218
+ } else if (value.startsWith('heading-')) {
219
+ this.setHeading(Number(value.slice('heading-'.length)) as 1 | 2 | 3 | 4 | 5 | 6);
220
+ }
221
+ }}
222
+ >
223
+ <option value="paragraph">{this.scope.locale.Paragraph()}</option>
224
+ <option value="heading-1">{this.scope.locale.Heading1()}</option>
225
+ <option value="heading-2">{this.scope.locale.Heading2()}</option>
226
+ <option value="heading-3">{this.scope.locale.Heading3()}</option>
227
+ <option value="heading-4">{this.scope.locale.Heading4()}</option>
228
+ <option value="heading-5">{this.scope.locale.Heading5()}</option>
229
+ <option value="heading-6">{this.scope.locale.Heading6()}</option>
230
+ </select>
231
+ {this._toolbarButton(
232
+ ':editor:format-bold',
233
+ this.scope.locale.Bold(),
234
+ () => this.toggleBold(),
235
+ { active: state.bold, disabled: !state.canBold },
236
+ )}
237
+ {this._toolbarButton(
238
+ ':editor:format-italic',
239
+ this.scope.locale.Italic(),
240
+ () => this.toggleItalic(),
241
+ { active: state.italic, disabled: !state.canItalic },
242
+ )}
243
+ {this._toolbarButton(
244
+ ':editor:format-strikethrough',
245
+ this.scope.locale.Strikethrough(),
246
+ () => this.toggleStrike(),
247
+ { active: state.strike, disabled: !state.canStrike },
248
+ )}
249
+ {this._toolbarButton(
250
+ ':editor:code',
251
+ this.scope.locale.InlineCode(),
252
+ () => this.toggleCode(),
253
+ { active: state.code, disabled: !state.canCode },
254
+ )}
255
+ {this._toolbarButton(
256
+ ':editor:insert-link-outline',
257
+ this.scope.locale.Link(),
258
+ () => {
259
+ void this.editLink();
260
+ },
261
+ { active: state.link, disabled: !state.canLink },
262
+ )}
263
+ {this._toolbarButton(
264
+ ':editor:bookmark',
265
+ this.scope.locale.Highlight(),
266
+ () => this.toggleHighlight(),
267
+ { active: state.highlight, disabled: !state.canHighlight },
268
+ )}
269
+ {this._toolbarButton(
270
+ ':editor:format-list-bulleted',
271
+ this.scope.locale.BulletList(),
272
+ () => this.toggleBulletList(),
273
+ { active: state.bulletList, disabled: !state.canBulletList },
274
+ )}
275
+ {this._toolbarButton(
276
+ ':editor:format-list-numbered',
277
+ this.scope.locale.OrderedList(),
278
+ () => this.toggleOrderedList(),
279
+ { active: state.orderedList, disabled: !state.canOrderedList },
280
+ )}
281
+ {this._toolbarButton(
282
+ ':editor:task-alt',
283
+ this.scope.locale.TaskList(),
284
+ () => this.toggleTaskList(),
285
+ { active: state.taskList, disabled: !state.canTaskList },
286
+ )}
287
+ {this._toolbarButton(
288
+ ':editor:format-quote',
289
+ this.scope.locale.Blockquote(),
290
+ () => this.toggleBlockquote(),
291
+ { active: state.blockquote, disabled: !state.canBlockquote },
292
+ )}
293
+ {this._toolbarButton(
294
+ ':editor:code-block',
295
+ this.scope.locale.CodeBlock(),
296
+ () => this.toggleCodeBlock(),
297
+ { active: state.codeBlock, disabled: !state.canCodeBlock },
298
+ )}
299
+ {this._toolbarButton(
300
+ ':editor:horizontal-rule',
301
+ this.scope.locale.HorizontalRule(),
302
+ () => this.setHorizontalRule(),
303
+ { disabled: !state.canHorizontalRule },
304
+ )}
305
+ {imageUploader &&
306
+ this._toolbarButton(
307
+ ':editor:image-outline',
308
+ this.scope.locale.InsertImage(),
309
+ () => {
310
+ this.beginImageUpload(imageUploader.chooseFiles);
311
+ },
312
+ {
313
+ disabled: imageUploader.isUploading || imageUploader.policy.pending,
314
+ },
315
+ )}
316
+ {this._renderTablePickerTrigger()}
317
+ </>
296
318
  )}
297
- {imageUploader &&
298
- this._toolbarButton(
299
- ':editor:image-outline',
300
- this.scope.locale.InsertImage(),
301
- () => {
302
- this.beginImageUpload(imageUploader.chooseFiles);
303
- },
304
- {
305
- disabled: imageUploader.isUploading || imageUploader.policy.pending,
306
- },
307
- )}
308
- {this._renderTablePickerTrigger()}
309
319
  </div>
310
320
  );
311
321
  }
@@ -454,7 +464,7 @@ export class RenderFormFieldMarkdown extends BeanRenderBase {
454
464
  !$$formField.field.state.meta.isValid && 'border-error',
455
465
  ]}
456
466
  onClick={() => {
457
- if (!this.readonly) {
467
+ if (!this.readonly && !this.sourceMode) {
458
468
  this.editor?.commands.focus();
459
469
  }
460
470
  }}
@@ -476,14 +486,37 @@ export class RenderFormFieldMarkdown extends BeanRenderBase {
476
486
  slotDefault={state => this._renderToolbar(state)}
477
487
  ></ZImageUploader>
478
488
  )}
479
- {!this.readonly && this.editor && this._renderTableToolbar()}
480
- {!this.readonly && this.editor && this._renderCodeBlockToolbar()}
489
+ {!this.readonly &&
490
+ !this.sourceMode &&
491
+ this.editor &&
492
+ this._renderTableToolbar()}
493
+ {!this.readonly &&
494
+ !this.sourceMode &&
495
+ this.editor &&
496
+ this._renderCodeBlockToolbar()}
481
497
  {this.imageUploadError && (
482
498
  <p class="px-4 pt-3 text-sm text-error" role="alert" aria-live="polite">
483
499
  {this.imageUploadError}
484
500
  </p>
485
501
  )}
486
- <EditorContent editor={this.editor} class={this.cMarkdown} />
502
+ <EditorContent
503
+ editor={this.editor}
504
+ class={this.cMarkdown}
505
+ style={{ display: this.sourceMode ? 'none' : undefined }}
506
+ />
507
+ {this.sourceMode && (
508
+ <textarea
509
+ class="block min-h-96 w-full resize-y rounded-b-box bg-base-100 p-4 font-mono text-sm text-base-content outline-none"
510
+ aria-label={this.scope.locale.MarkdownSource()}
511
+ spellcheck={false}
512
+ readonly={this.readonly}
513
+ value={this.value}
514
+ onInput={event => {
515
+ this.handleSourceInput((event.target as HTMLTextAreaElement).value);
516
+ }}
517
+ onBlur={() => this.handleSourceBlur()}
518
+ ></textarea>
519
+ )}
487
520
  </>
488
521
  ),
489
522
  placeholder: () => <div class="min-h-96 p-4" aria-hidden="true"></div>,
@@ -22,6 +22,8 @@ export default {
22
22
  Link: 'Link',
23
23
  LinkUrl: 'Link URL',
24
24
  MarkdownToolbar: 'Markdown toolbar',
25
+ MarkdownSource: 'Markdown source',
26
+ SourceMode: 'Source editing mode',
25
27
  OrderedList: 'Ordered list',
26
28
  Paragraph: 'Paragraph',
27
29
  PlainText: 'Plain text',
@@ -22,6 +22,8 @@ export default {
22
22
  Link: '链接',
23
23
  LinkUrl: '链接地址',
24
24
  MarkdownToolbar: 'Markdown 工具栏',
25
+ MarkdownSource: 'Markdown 源码',
26
+ SourceMode: '源码编辑模式',
25
27
  OrderedList: '有序列表',
26
28
  Paragraph: '段落',
27
29
  PlainText: '纯文本',