cabloy 5.1.141 → 5.1.142

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.
Files changed (92) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/playwright-e2e.yml +2 -2
  3. package/CHANGELOG.md +21 -0
  4. package/cabloy-docs/frontend/form-guide.md +47 -0
  5. package/cabloy-docs/frontend/markdown-guide.md +50 -17
  6. package/cabloy-docs/frontend/route-alias-guide.md +20 -0
  7. package/cabloy-docs/frontend/scripts.md +9 -11
  8. package/cabloy-docs/frontend/zova-form-under-the-hood.md +25 -0
  9. package/cabloy-docs/fullstack/parallel-worktree-environment.md +1 -1
  10. package/cabloy-docs/fullstack/quickstart.md +5 -4
  11. package/cabloy-docs/reference/repo-scripts.md +29 -42
  12. package/e2e/config/playwright.config.ts +34 -0
  13. package/e2e/scripts/e2e.ts +2 -27
  14. package/e2e/scripts/runE2e.ts +62 -0
  15. package/e2e/scripts/runE2eArgs.test.ts +75 -0
  16. package/e2e/scripts/runE2eArgs.ts +158 -0
  17. package/e2e/specs/{a-commerce/commerce.spec.ts → a-commerce.spec.ts} +361 -30
  18. package/e2e/specs/{cabloy-basic/account.spec.ts → account.spec.ts} +1 -2
  19. package/oxlint.config.ts +3 -0
  20. package/package.json +3 -9
  21. package/scripts/upgrade.ts +63 -68
  22. package/vona/packages-utils/lint/package.json +1 -1
  23. package/vona/packages-utils/lint/src/oxc/lint.ts +1 -1
  24. package/vona/packages-utils/lint/src/oxc/lintVue.ts +1 -1
  25. package/vona/pnpm-lock.yaml +41 -41
  26. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundRecoveryView.tsx +7 -2
  27. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +31 -15
  28. package/vona/src/suite/a-home/modules/home-user/package.json +1 -0
  29. package/vona/src/suite/a-home/modules/home-user/src/service/account.ts +15 -2
  30. package/vona/src/suite/a-home/modules/home-user/test/accountActivation.test.ts +34 -4
  31. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/package.json +2 -1
  32. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/.metadata/index.ts +26 -0
  33. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/bean/imageScene.markdown.ts +13 -0
  34. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  35. package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  36. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +23 -0
  37. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +7 -0
  38. package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  39. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +1 -1
  40. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
  41. package/vona/src/suite-vendor/a-pay/package.json +5 -5
  42. package/zova/packages-utils/zova-jsx/package.json +2 -2
  43. package/zova/packages-zova/zova/package.json +3 -3
  44. package/zova/packages-zova/zova-core/package.json +1 -1
  45. package/zova/packages-zova/zova-core/src/bean/resource/locale/type.ts +1 -0
  46. package/zova/pnpm-lock.yaml +63 -18
  47. package/zova/src/front/config/config/config.cabloyBasicWeb.ts +3 -1
  48. package/zova/src/front/config/config/config.cabloyCommerce.ts +4 -2
  49. package/zova/src/suite/a-commerce/modules/commerce-member/src/page/address/controller.tsx +64 -63
  50. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +6 -1
  51. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +413 -134
  52. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +76 -0
  53. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +67 -0
  54. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +1 -0
  55. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/cart/controller.tsx +1 -1
  56. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +7 -7
  57. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/order/controller.tsx +46 -9
  58. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +50 -30
  59. package/zova/src/suite/cabloy-basic/modules/basic-captcha/src/component/formFieldCaptcha/controller.tsx +6 -2
  60. package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +9 -15
  61. package/zova/src/suite/cabloy-basic/modules/basic-markdown/package.json +10 -2
  62. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/index.ts +11 -2
  63. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/locales.ts +7 -0
  64. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/controller.tsx +723 -5
  65. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/render.tsx +461 -2
  66. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/config/locale/en-us.ts +59 -0
  67. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/config/locale/zh-cn.ts +59 -0
  68. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/index.ts +1 -0
  69. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/lib/codeBlockLanguages.ts +42 -0
  70. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/lib/richTextContentStyle.ts +20 -0
  71. package/zova/src/suite/cabloy-basic/modules/basic-markdown/test/lib/codeBlockLanguages.test.ts +25 -0
  72. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  73. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +1 -0
  74. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  75. package/zova/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  76. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/component/paymentNextAction/controller.tsx +26 -12
  77. package/zova/src/suite-vendor/a-pay/modules/pay-mock/cli/openapi.config.ts +1 -1
  78. package/zova/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  79. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/api/payMockMockPayment.ts +26 -0
  80. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/apiSchema/payMockMockPayment.ts +8 -1
  81. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/model/payMockPayment.ts +13 -1
  82. package/zova/src/suite-vendor/a-pay/package.json +3 -3
  83. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  84. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/formField/controller.tsx +2 -0
  85. package/zova/src/suite-vendor/a-zova/modules/a-form/src/types/formField.ts +7 -6
  86. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  87. package/zova/src/suite-vendor/a-zova/package.json +3 -3
  88. package/e2e/config/playwright.basic.config.ts +0 -3
  89. package/e2e/config/playwright.commerce.config.ts +0 -3
  90. package/e2e/config/playwright.shared.config.ts +0 -38
  91. package/e2e/scripts/testE2eClean.ts +0 -56
  92. /package/e2e/specs/{cabloy-basic/basic.spec.ts → cabloy-basic.spec.ts} +0 -0
@@ -4,8 +4,11 @@ import type {
4
4
  IFormFieldComponentOptions,
5
5
  IFormFieldRenderContextPropsBucket,
6
6
  } from 'zova-module-a-form';
7
- import type { IResourceFormFieldOptionsBase } from 'zova-module-a-openapi';
7
+ import type { IResourceFormFieldOptionsBase, IImageSceneRecord } from 'zova-module-a-openapi';
8
+ import type { IImageUploaderResult } from 'zova-module-basic-image';
8
9
 
10
+ import { findParentNodeClosestToPos } from '@tiptap/core';
11
+ import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
9
12
  import { Highlight } from '@tiptap/extension-highlight';
10
13
  import { Image } from '@tiptap/extension-image';
11
14
  import { TaskItem, TaskList } from '@tiptap/extension-list';
@@ -13,21 +16,84 @@ import { TableKit } from '@tiptap/extension-table';
13
16
  import { Markdown } from '@tiptap/markdown';
14
17
  import StarterKit from '@tiptap/starter-kit';
15
18
  import { Editor } from '@tiptap/vue-3';
19
+ import { common, createLowlight } from 'lowlight';
16
20
  import { BeanControllerBase } from 'zova';
17
21
  import { Controller } from 'zova-module-a-bean';
18
22
 
23
+ import { getCodeBlockLanguage, isCodeBlockLanguage } from '../../lib/codeBlockLanguages.js';
24
+
25
+ const markdownImageScene = 'a-markdown:markdown';
26
+ const lowlight = createLowlight(common);
27
+
19
28
  declare module 'zova-module-a-openapi' {
20
29
  export interface IResourceFormFieldRecord {
21
30
  'basic-markdown:formFieldMarkdown'?: IResourceFormFieldMarkdownOptions;
22
31
  }
23
32
  }
24
33
 
25
- export interface IResourceFormFieldMarkdownOptions extends IResourceFormFieldOptionsBase {}
34
+ export interface IResourceFormFieldMarkdownOptions extends IResourceFormFieldOptionsBase {
35
+ imageScene?: keyof IImageSceneRecord | string;
36
+ }
26
37
 
27
38
  export interface ControllerFormFieldMarkdownProps extends IFormFieldComponentOptions {
28
39
  options?: IResourceFormFieldMarkdownOptions;
29
40
  }
30
41
 
42
+ type IMarkdownHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
43
+
44
+ interface IMarkdownToolbarState {
45
+ bold: boolean;
46
+ italic: boolean;
47
+ strike: boolean;
48
+ code: boolean;
49
+ highlight: boolean;
50
+ bulletList: boolean;
51
+ orderedList: boolean;
52
+ taskList: boolean;
53
+ blockquote: boolean;
54
+ codeBlock: boolean;
55
+ codeBlockLanguage?: string;
56
+ link: boolean;
57
+ headingLevel: IMarkdownHeadingLevel | undefined;
58
+ canUndo: boolean;
59
+ canRedo: boolean;
60
+ canBold: boolean;
61
+ canItalic: boolean;
62
+ canStrike: boolean;
63
+ canCode: boolean;
64
+ canHighlight: boolean;
65
+ canBulletList: boolean;
66
+ canOrderedList: boolean;
67
+ canTaskList: boolean;
68
+ canBlockquote: boolean;
69
+ canCodeBlock: boolean;
70
+ canLink: boolean;
71
+ canHorizontalRule: boolean;
72
+ canTable: boolean;
73
+ canAddTableRowBefore: boolean;
74
+ canAddTableRowAfter: boolean;
75
+ canDeleteTableRow: boolean;
76
+ canAddTableColumnBefore: boolean;
77
+ canAddTableColumnAfter: boolean;
78
+ canDeleteTableColumn: boolean;
79
+ canDeleteTable: boolean;
80
+ }
81
+
82
+ interface IMarkdownContextualToolbarPosition {
83
+ left: number;
84
+ top: number;
85
+ }
86
+
87
+ interface IMarkdownTableDimensions {
88
+ rows: number;
89
+ cols: number;
90
+ }
91
+
92
+ interface IMarkdownSelection {
93
+ from: number;
94
+ to: number;
95
+ }
96
+
31
97
  @Controller()
32
98
  export class ControllerFormFieldMarkdown extends BeanControllerBase {
33
99
  static $propsDefault = {};
@@ -36,16 +102,632 @@ export class ControllerFormFieldMarkdown extends BeanControllerBase {
36
102
  editor?: Editor;
37
103
  value = '';
38
104
  readonly = false;
105
+ toolbarState: IMarkdownToolbarState = this._emptyToolbarState();
106
+ tableToolbarPosition?: IMarkdownContextualToolbarPosition;
107
+ codeBlockToolbarPosition?: IMarkdownContextualToolbarPosition;
108
+ readonly tablePickerMaxSize = 8;
109
+ tablePickerOpen = false;
110
+ tablePickerPreview: IMarkdownTableDimensions = { rows: 1, cols: 1 };
111
+ tablePickerActive: IMarkdownTableDimensions = { rows: 1, cols: 1 };
112
+ imageScene = markdownImageScene;
113
+ imageUploadError?: string;
114
+ private _imageUploadSelection?: IMarkdownSelection;
39
115
  private _syncing = false;
40
116
  private _setValue?: (value: string, disableNotifyChanged?: boolean) => void;
41
117
  private _handleBlur?: () => void;
118
+ private _tableToolbarHost?: HTMLElement;
119
+ private _tableToolbarFocused = false;
120
+ private _codeBlockToolbarHost?: HTMLElement;
121
+ private _codeBlockToolbarFocused = false;
122
+ private _codeBlockToolbarHovered = false;
123
+ private _codeBlockToolbarInteracting = false;
124
+ private _contextualToolbarUpdateFrame?: number;
125
+ private _contextualToolbarViewportHandler?: () => void;
126
+ private _tablePickerTrigger?: HTMLButtonElement;
127
+ private _tablePickerRoot?: HTMLElement;
128
+ private _tablePickerPointerHandler?: (event: PointerEvent) => void;
129
+ private _tablePickerKeyHandler?: (event: KeyboardEvent) => void;
130
+
131
+ private _emptyToolbarState(): IMarkdownToolbarState {
132
+ return {
133
+ bold: false,
134
+ italic: false,
135
+ strike: false,
136
+ code: false,
137
+ highlight: false,
138
+ bulletList: false,
139
+ orderedList: false,
140
+ taskList: false,
141
+ blockquote: false,
142
+ codeBlock: false,
143
+ codeBlockLanguage: undefined,
144
+ link: false,
145
+ headingLevel: undefined,
146
+ canUndo: false,
147
+ canRedo: false,
148
+ canBold: false,
149
+ canItalic: false,
150
+ canStrike: false,
151
+ canCode: false,
152
+ canHighlight: false,
153
+ canBulletList: false,
154
+ canOrderedList: false,
155
+ canTaskList: false,
156
+ canBlockquote: false,
157
+ canCodeBlock: false,
158
+ canLink: false,
159
+ canHorizontalRule: false,
160
+ canTable: false,
161
+ canAddTableRowBefore: false,
162
+ canAddTableRowAfter: false,
163
+ canDeleteTableRow: false,
164
+ canAddTableColumnBefore: false,
165
+ canAddTableColumnAfter: false,
166
+ canDeleteTableColumn: false,
167
+ canDeleteTable: false,
168
+ };
169
+ }
170
+
171
+ private _refreshToolbarState() {
172
+ const editor = this.editor;
173
+ if (!editor) {
174
+ this.toolbarState = this._emptyToolbarState();
175
+ this._scheduleContextualToolbarPosition();
176
+ return;
177
+ }
178
+ const can = editor.can();
179
+ const heading = editor.getAttributes('heading').level;
180
+ const codeBlockActive = editor.isActive('codeBlock');
181
+ const codeBlockLanguage = editor.getAttributes('codeBlock').language;
182
+ const tableActive = editor.isActive('table');
183
+ this.toolbarState = {
184
+ bold: editor.isActive('bold'),
185
+ italic: editor.isActive('italic'),
186
+ strike: editor.isActive('strike'),
187
+ code: editor.isActive('code'),
188
+ highlight: editor.isActive('highlight'),
189
+ bulletList: editor.isActive('bulletList'),
190
+ orderedList: editor.isActive('orderedList'),
191
+ taskList: editor.isActive('taskList'),
192
+ blockquote: editor.isActive('blockquote'),
193
+ codeBlock: codeBlockActive,
194
+ codeBlockLanguage: codeBlockActive
195
+ ? (getCodeBlockLanguage(codeBlockLanguage) ?? codeBlockLanguage ?? undefined)
196
+ : undefined,
197
+ link: editor.isActive('link'),
198
+ headingLevel:
199
+ heading === 1 ||
200
+ heading === 2 ||
201
+ heading === 3 ||
202
+ heading === 4 ||
203
+ heading === 5 ||
204
+ heading === 6
205
+ ? heading
206
+ : undefined,
207
+ canUndo: can.undo(),
208
+ canRedo: can.redo(),
209
+ canBold: can.toggleBold(),
210
+ canItalic: can.toggleItalic(),
211
+ canStrike: can.toggleStrike(),
212
+ canCode: can.toggleCode(),
213
+ canHighlight: can.toggleHighlight(),
214
+ canBulletList: can.toggleBulletList(),
215
+ canOrderedList: can.toggleOrderedList(),
216
+ canTaskList: can.toggleTaskList(),
217
+ canBlockquote: can.toggleBlockquote(),
218
+ canCodeBlock: can.toggleCodeBlock(),
219
+ canLink:
220
+ editor.isActive('link') ||
221
+ (!editor.state.selection.empty && can.setLink({ href: 'https://example.com' })),
222
+ canHorizontalRule: can.setHorizontalRule(),
223
+ canTable: can.insertTable({ rows: 3, cols: 3, withHeaderRow: true }),
224
+ canAddTableRowBefore: tableActive ? can.addRowBefore() : false,
225
+ canAddTableRowAfter: tableActive ? can.addRowAfter() : false,
226
+ canDeleteTableRow: tableActive ? can.deleteRow() : false,
227
+ canAddTableColumnBefore: tableActive ? can.addColumnBefore() : false,
228
+ canAddTableColumnAfter: tableActive ? can.addColumnAfter() : false,
229
+ canDeleteTableColumn: tableActive ? can.deleteColumn() : false,
230
+ canDeleteTable: tableActive ? can.deleteTable() : false,
231
+ };
232
+ this._scheduleContextualToolbarPosition();
233
+ }
234
+
235
+ private _runCommand(command: (editor: Editor) => boolean) {
236
+ if (this.readonly || !this.editor) return;
237
+ command(this.editor);
238
+ }
239
+
240
+ private _shouldShowTableToolbar() {
241
+ const editor = this.editor;
242
+ return !!(
243
+ editor &&
244
+ !this.readonly &&
245
+ editor.isEditable &&
246
+ editor.isActive('table') &&
247
+ (editor.view.hasFocus() || this._tableToolbarFocused)
248
+ );
249
+ }
250
+
251
+ private _setTableToolbarPosition(position?: IMarkdownContextualToolbarPosition) {
252
+ if (
253
+ this.tableToolbarPosition?.left === position?.left &&
254
+ this.tableToolbarPosition?.top === position?.top
255
+ ) {
256
+ return;
257
+ }
258
+ this.tableToolbarPosition = position;
259
+ }
260
+
261
+ private _scheduleContextualToolbarPosition() {
262
+ if (!process.env.CLIENT || this._contextualToolbarUpdateFrame !== undefined) return;
263
+ this._contextualToolbarUpdateFrame = window.requestAnimationFrame(() => {
264
+ this._contextualToolbarUpdateFrame = undefined;
265
+ this._updateTableToolbarPosition();
266
+ this._updateCodeBlockToolbarPosition();
267
+ });
268
+ }
269
+
270
+ private _shouldShowCodeBlockToolbar() {
271
+ const editor = this.editor;
272
+ const toolbarInteraction =
273
+ this._codeBlockToolbarFocused ||
274
+ this._codeBlockToolbarHovered ||
275
+ this._codeBlockToolbarInteracting;
276
+ return !!(
277
+ editor &&
278
+ !this.readonly &&
279
+ editor.isEditable &&
280
+ (editor.isActive('codeBlock') || (this.toolbarState.codeBlock && toolbarInteraction)) &&
281
+ (editor.view.hasFocus() || toolbarInteraction)
282
+ );
283
+ }
284
+
285
+ private _setCodeBlockToolbarPosition(position?: IMarkdownContextualToolbarPosition) {
286
+ if (
287
+ this.codeBlockToolbarPosition?.left === position?.left &&
288
+ this.codeBlockToolbarPosition?.top === position?.top
289
+ ) {
290
+ return;
291
+ }
292
+ this.codeBlockToolbarPosition = position;
293
+ }
294
+
295
+ private _updateCodeBlockToolbarPosition() {
296
+ const editor = this.editor;
297
+ const host = this._codeBlockToolbarHost;
298
+ if (!editor || !host || !this._shouldShowCodeBlockToolbar()) {
299
+ this._setCodeBlockToolbarPosition();
300
+ return;
301
+ }
302
+ const codeBlock = findParentNodeClosestToPos(
303
+ editor.state.selection.$from,
304
+ node => node.type.name === 'codeBlock',
305
+ );
306
+ if (!codeBlock) {
307
+ this._setCodeBlockToolbarPosition();
308
+ return;
309
+ }
310
+ const dom = editor.view.nodeDOM(codeBlock.pos);
311
+ if (!(dom instanceof HTMLElement)) {
312
+ this._setCodeBlockToolbarPosition();
313
+ return;
314
+ }
315
+ const codeBlockElement = dom.matches('pre') ? dom : dom.querySelector('pre');
316
+ if (!codeBlockElement) {
317
+ this._setCodeBlockToolbarPosition();
318
+ return;
319
+ }
320
+ const codeBlockRect = codeBlockElement.getBoundingClientRect();
321
+ const hostRect = host.getBoundingClientRect();
322
+ this._setCodeBlockToolbarPosition({
323
+ left: codeBlockRect.left - hostRect.left + codeBlockRect.width / 2,
324
+ top: codeBlockRect.top - hostRect.top,
325
+ });
326
+ }
327
+
328
+ public setCodeBlockToolbarHost(host: HTMLElement | null) {
329
+ if (this._codeBlockToolbarHost === host) return;
330
+ this._codeBlockToolbarHost = host ?? undefined;
331
+ this._scheduleContextualToolbarPosition();
332
+ }
333
+
334
+ public setCodeBlockToolbarFocused(focused: boolean) {
335
+ this._codeBlockToolbarFocused = focused;
336
+ this._scheduleContextualToolbarPosition();
337
+ }
338
+
339
+ public setCodeBlockToolbarHovered(hovered: boolean) {
340
+ this._codeBlockToolbarHovered = hovered;
341
+ this._scheduleContextualToolbarPosition();
342
+ }
343
+
344
+ public setCodeBlockToolbarInteracting(interacting: boolean) {
345
+ this._codeBlockToolbarInteracting = interacting;
346
+ this._scheduleContextualToolbarPosition();
347
+ }
348
+
349
+ private _updateTableToolbarPosition() {
350
+ const editor = this.editor;
351
+ const host = this._tableToolbarHost;
352
+ if (!editor || !host || !this._shouldShowTableToolbar()) {
353
+ this._setTableToolbarPosition();
354
+ return;
355
+ }
356
+ const table = findParentNodeClosestToPos(
357
+ editor.state.selection.$from,
358
+ node => node.type.name === 'table',
359
+ );
360
+ if (!table) {
361
+ this._setTableToolbarPosition();
362
+ return;
363
+ }
364
+ const dom = editor.view.nodeDOM(table.pos);
365
+ if (!(dom instanceof HTMLElement)) {
366
+ this._setTableToolbarPosition();
367
+ return;
368
+ }
369
+ const tableElement = dom.querySelector('table') ?? dom;
370
+ const tableRect = tableElement.getBoundingClientRect();
371
+ const hostRect = host.getBoundingClientRect();
372
+ this._setTableToolbarPosition({
373
+ left: tableRect.left - hostRect.left + tableRect.width / 2,
374
+ top: tableRect.top - hostRect.top - 8,
375
+ });
376
+ }
377
+
378
+ public setTableToolbarHost(host: HTMLElement | null) {
379
+ if (this._tableToolbarHost === host) return;
380
+ this._tableToolbarHost = host ?? undefined;
381
+ this._scheduleContextualToolbarPosition();
382
+ }
383
+
384
+ public setTableToolbarFocused(focused: boolean) {
385
+ this._tableToolbarFocused = focused;
386
+ this._scheduleContextualToolbarPosition();
387
+ }
388
+
389
+ private _isValidTablePickerDimensions(rows: number, cols: number) {
390
+ return (
391
+ Number.isInteger(rows) &&
392
+ Number.isInteger(cols) &&
393
+ rows >= 1 &&
394
+ rows <= this.tablePickerMaxSize &&
395
+ cols >= 1 &&
396
+ cols <= this.tablePickerMaxSize
397
+ );
398
+ }
399
+
400
+ private _setTablePickerActive(rows: number, cols: number, focus = false) {
401
+ if (!this._isValidTablePickerDimensions(rows, cols)) return;
402
+ this.tablePickerActive = { rows, cols };
403
+ this.tablePickerPreview = { rows, cols };
404
+ if (focus && this._tablePickerRoot) {
405
+ const cell = this._tablePickerRoot.querySelector<HTMLButtonElement>(
406
+ `[data-table-picker-cell="${rows}-${cols}"]`,
407
+ );
408
+ cell?.focus();
409
+ }
410
+ }
411
+
412
+ public setTablePickerTrigger(trigger: HTMLButtonElement | null) {
413
+ this._tablePickerTrigger = trigger ?? undefined;
414
+ }
415
+
416
+ public setTablePickerRoot(root: HTMLElement | null) {
417
+ this._tablePickerRoot = root ?? undefined;
418
+ }
419
+
420
+ public openTablePicker() {
421
+ if (this.readonly || !this.toolbarState.canTable) return;
422
+ this.tablePickerOpen = true;
423
+ this._setTablePickerActive(1, 1);
424
+ }
425
+
426
+ public closeTablePicker(options: { restoreFocus?: boolean } = {}) {
427
+ if (!this.tablePickerOpen) return;
428
+ this.tablePickerOpen = false;
429
+ this._setTablePickerActive(1, 1);
430
+ if (options.restoreFocus) {
431
+ this._tablePickerTrigger?.focus();
432
+ }
433
+ }
434
+
435
+ public toggleTablePicker() {
436
+ if (this.tablePickerOpen) {
437
+ this.closeTablePicker({ restoreFocus: true });
438
+ } else {
439
+ this.openTablePicker();
440
+ }
441
+ }
442
+
443
+ public setTablePickerPreview(rows: number, cols: number) {
444
+ this._setTablePickerActive(rows, cols);
445
+ }
446
+
447
+ public selectTablePickerSize(rows: number, cols: number) {
448
+ if (
449
+ !this.tablePickerOpen ||
450
+ this.readonly ||
451
+ !this.toolbarState.canTable ||
452
+ !this._isValidTablePickerDimensions(rows, cols)
453
+ ) {
454
+ return;
455
+ }
456
+ this.closeTablePicker();
457
+ this.insertTable(rows, cols);
458
+ }
459
+
460
+ public handleTablePickerKeydown(event: KeyboardEvent) {
461
+ if (!this.tablePickerOpen) return;
462
+ const { rows, cols } = this.tablePickerActive;
463
+ let nextRows = rows;
464
+ let nextCols = cols;
465
+ switch (event.key) {
466
+ case 'ArrowUp':
467
+ nextRows = Math.max(1, rows - 1);
468
+ break;
469
+ case 'ArrowDown':
470
+ nextRows = Math.min(this.tablePickerMaxSize, rows + 1);
471
+ break;
472
+ case 'ArrowLeft':
473
+ nextCols = Math.max(1, cols - 1);
474
+ break;
475
+ case 'ArrowRight':
476
+ nextCols = Math.min(this.tablePickerMaxSize, cols + 1);
477
+ break;
478
+ case 'Home':
479
+ nextCols = 1;
480
+ break;
481
+ case 'End':
482
+ nextCols = this.tablePickerMaxSize;
483
+ break;
484
+ case 'Enter':
485
+ case ' ': {
486
+ event.preventDefault();
487
+ this.selectTablePickerSize(rows, cols);
488
+ return;
489
+ }
490
+ case 'Escape':
491
+ event.preventDefault();
492
+ this.closeTablePicker({ restoreFocus: true });
493
+ return;
494
+ case 'Tab':
495
+ this.closeTablePicker();
496
+ return;
497
+ default:
498
+ return;
499
+ }
500
+ event.preventDefault();
501
+ this._setTablePickerActive(nextRows, nextCols, true);
502
+ }
503
+
504
+ public undo() {
505
+ this._runCommand(editor => editor.chain().focus().undo().run());
506
+ }
507
+
508
+ public redo() {
509
+ this._runCommand(editor => editor.chain().focus().redo().run());
510
+ }
511
+
512
+ public setParagraph() {
513
+ this._runCommand(editor => editor.chain().focus().setParagraph().run());
514
+ }
515
+
516
+ public setHeading(level: IMarkdownHeadingLevel) {
517
+ this._runCommand(editor => editor.chain().focus().toggleHeading({ level }).run());
518
+ }
519
+
520
+ public toggleBold() {
521
+ this._runCommand(editor => editor.chain().focus().toggleBold().run());
522
+ }
523
+
524
+ public toggleItalic() {
525
+ this._runCommand(editor => editor.chain().focus().toggleItalic().run());
526
+ }
527
+
528
+ public toggleStrike() {
529
+ this._runCommand(editor => editor.chain().focus().toggleStrike().run());
530
+ }
531
+
532
+ public toggleCode() {
533
+ this._runCommand(editor => editor.chain().focus().toggleCode().run());
534
+ }
535
+
536
+ public async editLink() {
537
+ const editor = this.editor;
538
+ if (this.readonly || !editor) return;
539
+ const selection = editor.state.selection;
540
+ const linkActive = editor.isActive('link');
541
+ const canEditLink = linkActive
542
+ ? editor.can().unsetLink()
543
+ : !selection.empty && editor.can().setLink({ href: 'https://example.com' });
544
+ if (!canEditLink) return;
545
+
546
+ const href = editor.getAttributes('link').href ?? '';
547
+ const result = await this.$appModal.prompt({
548
+ icon: ':editor:insert-link-outline',
549
+ title: this.scope.locale.Link(),
550
+ text: this.scope.locale.LinkUrl(),
551
+ defaultValue: href,
552
+ });
553
+ if (this.readonly || this.editor !== editor || result === undefined) return;
554
+
555
+ const value = result.trim();
556
+ const chain = editor.chain().focus().setTextSelection({
557
+ from: selection.from,
558
+ to: selection.to,
559
+ });
560
+ if (linkActive) {
561
+ chain.extendMarkRange('link');
562
+ }
563
+ if (!value) {
564
+ if (linkActive) chain.unsetLink().run();
565
+ return;
566
+ }
567
+ chain.setLink({ href: value }).run();
568
+ }
569
+
570
+ public toggleHighlight() {
571
+ this._runCommand(editor => editor.chain().focus().toggleHighlight().run());
572
+ }
573
+
574
+ public toggleBulletList() {
575
+ this._runCommand(editor => editor.chain().focus().toggleBulletList().run());
576
+ }
577
+
578
+ public toggleOrderedList() {
579
+ this._runCommand(editor => editor.chain().focus().toggleOrderedList().run());
580
+ }
581
+
582
+ public toggleTaskList() {
583
+ this._runCommand(editor => editor.chain().focus().toggleTaskList().run());
584
+ }
585
+
586
+ public toggleBlockquote() {
587
+ this._runCommand(editor => editor.chain().focus().toggleBlockquote().run());
588
+ }
589
+
590
+ public toggleCodeBlock() {
591
+ this._runCommand(editor => editor.chain().focus().toggleCodeBlock().run());
592
+ }
593
+
594
+ public setCodeBlockLanguage(language?: string) {
595
+ if (language && !isCodeBlockLanguage(language)) return;
596
+ this._runCommand(editor => {
597
+ if (!editor.isActive('codeBlock')) return false;
598
+ return editor
599
+ .chain()
600
+ .focus()
601
+ .updateAttributes('codeBlock', { language: language || null })
602
+ .run();
603
+ });
604
+ }
605
+
606
+ public setHorizontalRule() {
607
+ this._runCommand(editor => editor.chain().focus().setHorizontalRule().run());
608
+ }
609
+
610
+ public insertTable(rows: number, cols: number) {
611
+ if (!this._isValidTablePickerDimensions(rows, cols)) return;
612
+ this._runCommand(editor =>
613
+ editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run(),
614
+ );
615
+ }
616
+
617
+ public addTableRowBefore() {
618
+ this._runCommand(editor => editor.chain().focus().addRowBefore().run());
619
+ }
620
+
621
+ public addTableRowAfter() {
622
+ this._runCommand(editor => editor.chain().focus().addRowAfter().run());
623
+ }
624
+
625
+ public deleteTableRow() {
626
+ this._runCommand(editor => editor.chain().focus().deleteRow().run());
627
+ }
628
+
629
+ public addTableColumnBefore() {
630
+ this._runCommand(editor => editor.chain().focus().addColumnBefore().run());
631
+ }
632
+
633
+ public addTableColumnAfter() {
634
+ this._runCommand(editor => editor.chain().focus().addColumnAfter().run());
635
+ }
636
+
637
+ public deleteTableColumn() {
638
+ this._runCommand(editor => editor.chain().focus().deleteColumn().run());
639
+ }
640
+
641
+ public deleteTable() {
642
+ this._runCommand(editor => editor.chain().focus().deleteTable().run());
643
+ }
644
+
645
+ public beginImageUpload(chooseFiles: () => void) {
646
+ const editor = this.editor;
647
+ if (this.readonly || !editor || !editor.isEditable) return;
648
+ this._imageUploadSelection = {
649
+ from: editor.state.selection.from,
650
+ to: editor.state.selection.to,
651
+ };
652
+ this.imageUploadError = undefined;
653
+ chooseFiles();
654
+ }
655
+
656
+ public handleImageUploaded(result: IImageUploaderResult) {
657
+ const editor = this.editor;
658
+ const selection = this._imageUploadSelection;
659
+ const src = this._resolveImageUrl(result.url);
660
+ if (!editor || this.readonly || !editor.isEditable || !selection || !src) {
661
+ this.imageUploadError = this.scope.locale.ImageUploadInvalidUrl();
662
+ return;
663
+ }
664
+ const { from, to } = selection;
665
+ if (from < 0 || to < from || to > editor.state.doc.content.size) {
666
+ this.imageUploadError = this.scope.locale.ImageUploadInvalidUrl();
667
+ return;
668
+ }
669
+ const chain = editor.chain().focus().setTextSelection({ from, to });
670
+ if (!chain.setImage({ src, ...(result.filename ? { alt: result.filename } : {}) }).run()) {
671
+ this.imageUploadError = this.scope.locale.ImageUploadFailed();
672
+ return;
673
+ }
674
+ this._imageUploadSelection = undefined;
675
+ this.imageUploadError = undefined;
676
+ }
677
+
678
+ public handleImageUploadError(error: Error) {
679
+ this.imageUploadError = error.message || this.scope.locale.ImageUploadFailed();
680
+ }
681
+
682
+ private _resolveImageUrl(url: string | undefined) {
683
+ if (!url) return undefined;
684
+ const resolved =
685
+ url.startsWith('/api/') && this.sys.config.api.baseURL
686
+ ? `${this.sys.config.api.baseURL.replace(/\/$/, '')}${url}`
687
+ : url;
688
+ try {
689
+ const parsed = new URL(resolved);
690
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:'
691
+ ? parsed.toString()
692
+ : undefined;
693
+ } catch {
694
+ return undefined;
695
+ }
696
+ }
42
697
 
43
698
  protected async __init__() {
44
699
  this.$controllerMounted(() => {
700
+ this._contextualToolbarViewportHandler = () => {
701
+ if (
702
+ this._shouldShowTableToolbar() ||
703
+ this._shouldShowCodeBlockToolbar() ||
704
+ this._codeBlockToolbarHovered
705
+ ) {
706
+ this._scheduleContextualToolbarPosition();
707
+ }
708
+ };
709
+ this._tablePickerPointerHandler = event => {
710
+ const target = event.target;
711
+ if (
712
+ target instanceof Node &&
713
+ (this._tablePickerTrigger?.contains(target) || this._tablePickerRoot?.contains(target))
714
+ ) {
715
+ return;
716
+ }
717
+ this.closeTablePicker();
718
+ };
719
+ this._tablePickerKeyHandler = event => {
720
+ this.handleTablePickerKeydown(event);
721
+ };
722
+ window.addEventListener('resize', this._contextualToolbarViewportHandler);
723
+ window.addEventListener('scroll', this._contextualToolbarViewportHandler, true);
724
+ document.addEventListener('pointerdown', this._tablePickerPointerHandler);
725
+ document.addEventListener('keydown', this._tablePickerKeyHandler);
45
726
  this.editor = new Editor({
46
727
  extensions: [
47
728
  Markdown,
48
- StarterKit,
729
+ StarterKit.configure({ codeBlock: false }),
730
+ CodeBlockLowlight.configure({ lowlight }),
49
731
  TaskList,
50
732
  TaskItem.configure({ nested: true }),
51
733
  Image,
@@ -61,13 +743,25 @@ export class ControllerFormFieldMarkdown extends BeanControllerBase {
61
743
  },
62
744
  },
63
745
  onUpdate: ({ editor }) => {
64
- if (this._syncing) return;
65
- this._setValue?.(editor.getMarkdown());
746
+ if (!this._syncing) {
747
+ this._setValue?.(editor.getMarkdown());
748
+ }
749
+ },
750
+ onSelectionUpdate: () => {
751
+ this._refreshToolbarState();
752
+ },
753
+ onTransaction: () => {
754
+ this._refreshToolbarState();
755
+ },
756
+ onFocus: () => {
757
+ this._scheduleContextualToolbarPosition();
66
758
  },
67
759
  onBlur: () => {
68
760
  this._handleBlur?.();
761
+ this._scheduleContextualToolbarPosition();
69
762
  },
70
763
  });
764
+ this._refreshToolbarState();
71
765
  this.$watch(
72
766
  () => this.value,
73
767
  value => {
@@ -81,12 +775,33 @@ export class ControllerFormFieldMarkdown extends BeanControllerBase {
81
775
  () => this.readonly,
82
776
  readonly => {
83
777
  this.editor?.setEditable(!readonly);
778
+ if (readonly) this.closeTablePicker();
779
+ this._refreshToolbarState();
84
780
  },
85
781
  );
86
782
  });
87
783
  }
88
784
 
89
785
  protected __dispose__() {
786
+ if (this._contextualToolbarUpdateFrame !== undefined) {
787
+ window.cancelAnimationFrame(this._contextualToolbarUpdateFrame);
788
+ }
789
+ if (this._contextualToolbarViewportHandler) {
790
+ window.removeEventListener('resize', this._contextualToolbarViewportHandler);
791
+ window.removeEventListener('scroll', this._contextualToolbarViewportHandler, true);
792
+ }
793
+ if (this._tablePickerPointerHandler) {
794
+ document.removeEventListener('pointerdown', this._tablePickerPointerHandler);
795
+ }
796
+ if (this._tablePickerKeyHandler) {
797
+ document.removeEventListener('keydown', this._tablePickerKeyHandler);
798
+ }
799
+ this._tablePickerTrigger = undefined;
800
+ this._tablePickerRoot = undefined;
801
+ this._tableToolbarHost = undefined;
802
+ this._codeBlockToolbarHost = undefined;
803
+ this._setTableToolbarPosition();
804
+ this._setCodeBlockToolbarPosition();
90
805
  this.editor?.destroy();
91
806
  }
92
807
 
@@ -96,8 +811,11 @@ export class ControllerFormFieldMarkdown extends BeanControllerBase {
96
811
  ) {
97
812
  const value = propsBucket.value ?? '';
98
813
  const readonly = propsBucket.readonly ?? false;
814
+ const options = propsBucket.options ?? {};
815
+ const imageScene = options.imageScene ?? markdownImageScene;
99
816
  if (this.value !== value) this.value = value;
100
817
  if (this.readonly !== readonly) this.readonly = readonly;
818
+ if (this.imageScene !== imageScene) this.imageScene = imageScene;
101
819
  if (this._setValue && this._handleBlur) return;
102
820
  this._setValue = (value, disableNotifyChanged) => {
103
821
  if (this.readonly) return;