@akinon/ui-editor 1.0.5 → 1.0.7-rc.0

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 (61) hide show
  1. package/dist/cjs/i18n/translations/en.d.ts +1 -0
  2. package/dist/cjs/i18n/translations/en.d.ts.map +1 -1
  3. package/dist/cjs/i18n/translations/en.js +1 -0
  4. package/dist/cjs/i18n/translations/tr.d.ts +1 -0
  5. package/dist/cjs/i18n/translations/tr.d.ts.map +1 -1
  6. package/dist/cjs/i18n/translations/tr.js +1 -0
  7. package/dist/cjs/tiptap/components/__tests__/html-code-viewer.test.js +0 -8
  8. package/dist/cjs/tiptap/components/__tests__/html-code-viewer.test.tsx +0 -11
  9. package/dist/cjs/tiptap/components/__tests__/tiptap.test.js +53 -12
  10. package/dist/cjs/tiptap/components/__tests__/tiptap.test.tsx +79 -13
  11. package/dist/cjs/tiptap/components/html-code-viewer.d.ts.map +1 -1
  12. package/dist/cjs/tiptap/components/html-code-viewer.js +1 -24
  13. package/dist/cjs/tiptap/components/menu-bar/__tests__/action-toolbar.test.js +7 -7
  14. package/dist/cjs/tiptap/components/menu-bar/__tests__/action-toolbar.test.tsx +7 -0
  15. package/dist/cjs/tiptap/components/menu-bar/__tests__/index.test.js +1 -0
  16. package/dist/cjs/tiptap/components/menu-bar/__tests__/index.test.tsx +1 -0
  17. package/dist/cjs/tiptap/components/menu-bar/action-toolbar.d.ts +2 -1
  18. package/dist/cjs/tiptap/components/menu-bar/action-toolbar.d.ts.map +1 -1
  19. package/dist/cjs/tiptap/components/menu-bar/action-toolbar.js +10 -2
  20. package/dist/cjs/tiptap/components/menu-bar/index.d.ts +2 -1
  21. package/dist/cjs/tiptap/components/menu-bar/index.d.ts.map +1 -1
  22. package/dist/cjs/tiptap/components/menu-bar/index.js +2 -2
  23. package/dist/cjs/tiptap/components/tiptap.d.ts.map +1 -1
  24. package/dist/cjs/tiptap/components/tiptap.js +23 -9
  25. package/dist/cjs/tiptap/constants/editor-config.d.ts +1 -0
  26. package/dist/cjs/tiptap/constants/editor-config.d.ts.map +1 -1
  27. package/dist/cjs/tiptap/constants/editor-config.js +1 -0
  28. package/dist/cjs/tiptap/utils/__tests__/clean-html.test.js +26 -7
  29. package/dist/cjs/tiptap/utils/__tests__/clean-html.test.ts +29 -7
  30. package/dist/cjs/tiptap/utils/prettier-format.js +1 -1
  31. package/dist/esm/i18n/translations/en.d.ts +1 -0
  32. package/dist/esm/i18n/translations/en.d.ts.map +1 -1
  33. package/dist/esm/i18n/translations/en.js +1 -0
  34. package/dist/esm/i18n/translations/tr.d.ts +1 -0
  35. package/dist/esm/i18n/translations/tr.d.ts.map +1 -1
  36. package/dist/esm/i18n/translations/tr.js +1 -0
  37. package/dist/esm/tiptap/components/__tests__/html-code-viewer.test.js +0 -8
  38. package/dist/esm/tiptap/components/__tests__/html-code-viewer.test.tsx +0 -11
  39. package/dist/esm/tiptap/components/__tests__/tiptap.test.js +53 -12
  40. package/dist/esm/tiptap/components/__tests__/tiptap.test.tsx +79 -13
  41. package/dist/esm/tiptap/components/html-code-viewer.d.ts.map +1 -1
  42. package/dist/esm/tiptap/components/html-code-viewer.js +1 -24
  43. package/dist/esm/tiptap/components/menu-bar/__tests__/action-toolbar.test.js +7 -7
  44. package/dist/esm/tiptap/components/menu-bar/__tests__/action-toolbar.test.tsx +7 -0
  45. package/dist/esm/tiptap/components/menu-bar/__tests__/index.test.js +1 -0
  46. package/dist/esm/tiptap/components/menu-bar/__tests__/index.test.tsx +1 -0
  47. package/dist/esm/tiptap/components/menu-bar/action-toolbar.d.ts +2 -1
  48. package/dist/esm/tiptap/components/menu-bar/action-toolbar.d.ts.map +1 -1
  49. package/dist/esm/tiptap/components/menu-bar/action-toolbar.js +10 -2
  50. package/dist/esm/tiptap/components/menu-bar/index.d.ts +2 -1
  51. package/dist/esm/tiptap/components/menu-bar/index.d.ts.map +1 -1
  52. package/dist/esm/tiptap/components/menu-bar/index.js +2 -2
  53. package/dist/esm/tiptap/components/tiptap.d.ts.map +1 -1
  54. package/dist/esm/tiptap/components/tiptap.js +23 -9
  55. package/dist/esm/tiptap/constants/editor-config.d.ts +1 -0
  56. package/dist/esm/tiptap/constants/editor-config.d.ts.map +1 -1
  57. package/dist/esm/tiptap/constants/editor-config.js +1 -0
  58. package/dist/esm/tiptap/utils/__tests__/clean-html.test.js +26 -7
  59. package/dist/esm/tiptap/utils/__tests__/clean-html.test.ts +29 -7
  60. package/dist/esm/tiptap/utils/prettier-format.js +1 -1
  61. package/package.json +12 -12
@@ -33,6 +33,7 @@ declare const translations: {
33
33
  readonly redo: "Redo";
34
34
  readonly source_mode: "Switch to Source Mode";
35
35
  readonly normal_mode: "Switch to Preview Mode";
36
+ readonly format: "Format HTML";
36
37
  readonly link: "Link";
37
38
  readonly align_left: "Align Left";
38
39
  readonly align_center: "Align Center";
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../../src/i18n/translations/en.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmFR,CAAC;AAEX,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../../src/i18n/translations/en.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoFR,CAAC;AAEX,eAAe,YAAY,CAAC"}
@@ -33,6 +33,7 @@ const translations = {
33
33
  redo: 'Redo',
34
34
  source_mode: 'Switch to Source Mode',
35
35
  normal_mode: 'Switch to Preview Mode',
36
+ format: 'Format HTML',
36
37
  link: 'Link',
37
38
  align_left: 'Align Left',
38
39
  align_center: 'Align Center',
@@ -33,6 +33,7 @@ declare const translations: {
33
33
  readonly redo: "Yinele";
34
34
  readonly source_mode: "Kaynak Koduna Geç";
35
35
  readonly normal_mode: "Önizleme Moduna Geç";
36
+ readonly format: "HTML Formatla";
36
37
  readonly link: "Bağlantı";
37
38
  readonly align_left: "Sola Hizala";
38
39
  readonly align_center: "Ortala";
@@ -1 +1 @@
1
- {"version":3,"file":"tr.d.ts","sourceRoot":"","sources":["../../../../src/i18n/translations/tr.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmFR,CAAC;AAEX,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"tr.d.ts","sourceRoot":"","sources":["../../../../src/i18n/translations/tr.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoFR,CAAC;AAEX,eAAe,YAAY,CAAC"}
@@ -33,6 +33,7 @@ const translations = {
33
33
  redo: 'Yinele',
34
34
  source_mode: 'Kaynak Koduna Geç',
35
35
  normal_mode: 'Önizleme Moduna Geç',
36
+ format: 'HTML Formatla',
36
37
  link: 'Bağlantı',
37
38
  align_left: 'Sola Hizala',
38
39
  align_center: 'Ortala',
@@ -98,14 +98,6 @@ describe('HtmlCodeViewer', () => {
98
98
  const textarea = screen.getByRole('textbox');
99
99
  expect(textarea).toBeInTheDocument();
100
100
  });
101
- it('should format code on mount', async () => {
102
- const formatted = '<p> Formatted </p>';
103
- vi.mocked(formatHtmlWithPrettier).mockResolvedValue(formatted);
104
- render(React.createElement(HtmlCodeViewer, Object.assign({}, defaultProps, { value: "<p>Raw</p>" })));
105
- await waitFor(() => {
106
- expect(defaultProps.onChange).toHaveBeenCalledWith(formatted);
107
- });
108
- });
109
101
  it('should handle paste event', async () => {
110
102
  const formatted = '<p>Formatted Paste</p>';
111
103
  vi.mocked(formatHtmlWithPrettier).mockResolvedValue(formatted);
@@ -143,17 +143,6 @@ describe('HtmlCodeViewer', () => {
143
143
  expect(textarea).toBeInTheDocument();
144
144
  });
145
145
 
146
- it('should format code on mount', async () => {
147
- const formatted = '<p> Formatted </p>';
148
- vi.mocked(formatHtmlWithPrettier).mockResolvedValue(formatted);
149
-
150
- render(<HtmlCodeViewer {...defaultProps} value="<p>Raw</p>" />);
151
-
152
- await waitFor(() => {
153
- expect(defaultProps.onChange).toHaveBeenCalledWith(formatted);
154
- });
155
- });
156
-
157
146
  it('should handle paste event', async () => {
158
147
  const formatted = '<p>Formatted Paste</p>';
159
148
  vi.mocked(formatHtmlWithPrettier).mockResolvedValue(formatted);
@@ -5,9 +5,10 @@ import React from 'react';
5
5
  import { beforeEach, describe, expect, it, vi } from 'vitest';
6
6
  import { MenuBar } from '../menu-bar';
7
7
  import { Tiptap } from '../tiptap';
8
- const { mockUseEditor, mockChangeLanguage } = vi.hoisted(() => ({
8
+ const { mockUseEditor, mockChangeLanguage, mockFormatHtmlWithPrettier } = vi.hoisted(() => ({
9
9
  mockUseEditor: vi.fn(),
10
- mockChangeLanguage: vi.fn()
10
+ mockChangeLanguage: vi.fn(),
11
+ mockFormatHtmlWithPrettier: vi.fn(async (html) => html)
11
12
  }));
12
13
  vi.useFakeTimers();
13
14
  vi.mock('@tiptap/react', () => ({
@@ -25,8 +26,9 @@ vi.mock('@tiptap/html', () => ({
25
26
  }));
26
27
  vi.mock('../menu-bar', () => ({
27
28
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
- MenuBar: vi.fn(({ toggleSource, isSourceMode }) => (React.createElement("div", { "data-testid": "menu-bar" },
29
- React.createElement("button", { onClick: toggleSource, "data-testid": "toggle-source" }, isSourceMode ? 'Switch to Visual' : 'Switch to Source'))))
29
+ MenuBar: vi.fn(({ toggleSource, isSourceMode, onFormat }) => (React.createElement("div", { "data-testid": "menu-bar" },
30
+ React.createElement("button", { onClick: toggleSource, "data-testid": "toggle-source" }, isSourceMode ? 'Switch to Visual' : 'Switch to Source'),
31
+ onFormat && (React.createElement("button", { onClick: onFormat, "data-testid": "format-button" }, "Format")))))
30
32
  }));
31
33
  vi.mock('../format-bubble-menu', () => ({
32
34
  FormatBubbleMenu: () => React.createElement("div", { "data-testid": "format-bubble-menu" })
@@ -43,7 +45,8 @@ vi.mock('../../assets/icons', () => ({
43
45
  }));
44
46
  vi.mock('../../utils', () => ({
45
47
  getExtensions: vi.fn(() => []),
46
- cleanDuplicateSpans: vi.fn(html => html)
48
+ cleanDuplicateSpans: vi.fn(html => html),
49
+ formatHtmlWithPrettier: mockFormatHtmlWithPrettier
47
50
  }));
48
51
  vi.mock('@akinon/akilocale/react', () => ({
49
52
  useTranslation: () => ({
@@ -78,6 +81,8 @@ describe('Tiptap', () => {
78
81
  getHTML: vi.fn(() => '<p>Initial Content</p>'),
79
82
  getJSON: vi.fn(() => ({ type: 'doc' })),
80
83
  setEditable: vi.fn(),
84
+ on: vi.fn(),
85
+ off: vi.fn(),
81
86
  commands: {
82
87
  setContent: vi.fn(),
83
88
  blur: vi.fn()
@@ -141,27 +146,31 @@ describe('Tiptap', () => {
141
146
  const { rerender } = renderComponent({ value: '<p>Old</p>' });
142
147
  mockEditor.getHTML.mockReturnValue('<p>Old</p>');
143
148
  rerender(React.createElement(Tiptap, { value: "<p>New</p>", onChange: onChange }));
144
- expect(mockEditor.commands.setContent).toHaveBeenCalledWith('<p>New</p>');
149
+ expect(mockEditor.commands.setContent).toHaveBeenCalledWith('<p>New</p>', {
150
+ emitUpdate: false
151
+ });
145
152
  });
146
153
  it('should update editor editable state when readOnly prop changes', () => {
147
154
  const { rerender } = renderComponent({ readOnly: false });
148
- expect(mockEditor.setEditable).toHaveBeenCalledWith(true);
155
+ expect(mockEditor.setEditable).toHaveBeenCalledWith(true, false);
149
156
  rerender(React.createElement(Tiptap, { value: "<p>Test</p>", readOnly: true, onChange: onChange }));
150
- expect(mockEditor.setEditable).toHaveBeenCalledWith(false);
157
+ expect(mockEditor.setEditable).toHaveBeenCalledWith(false, false);
151
158
  });
152
159
  it('should call onChange on editor update', () => {
160
+ var _a;
153
161
  renderComponent();
154
- const onUpdateCallback = mockUseEditor.mock.calls[0][0].onUpdate;
155
- onUpdateCallback({ editor: mockEditor });
162
+ const handler = (_a = mockEditor.on.mock.calls.find(([event]) => event === 'update')) === null || _a === void 0 ? void 0 : _a[1];
163
+ handler();
156
164
  expect(onChange).toHaveBeenCalledWith({
157
165
  html: '<p>Initial Content</p>',
158
166
  json: { type: 'doc' }
159
167
  });
160
168
  });
161
169
  it('should handle undefined onChange', () => {
170
+ var _a;
162
171
  const { rerender } = render(React.createElement(Tiptap, { value: "<p>Test</p>", onChange: undefined }));
163
- const onUpdateCallback = mockUseEditor.mock.calls[0][0].onUpdate;
164
- expect(() => onUpdateCallback({ editor: mockEditor })).not.toThrow();
172
+ const handler = (_a = mockEditor.on.mock.calls.find(([event]) => event === 'update')) === null || _a === void 0 ? void 0 : _a[1];
173
+ expect(() => handler()).not.toThrow();
165
174
  const viewer = screen.getByTestId('html-code-viewer');
166
175
  fireEvent.change(viewer, { target: { value: 'new' } });
167
176
  rerender(React.createElement(Tiptap, { value: "<p>Test</p>", onChange: undefined }));
@@ -310,4 +319,36 @@ describe('Tiptap', () => {
310
319
  const viewer = screen.getByTestId('html-code-viewer');
311
320
  expect(viewer).toHaveValue('<script>console.log("test")</script>');
312
321
  });
322
+ it('should format HTML when handleFormat is called and HTML changes', async () => {
323
+ const formattedHtml = '<p>\n Formatted Content\n</p>';
324
+ mockFormatHtmlWithPrettier.mockResolvedValueOnce(formattedHtml);
325
+ renderComponent({ value: '<p>Unformatted</p>' });
326
+ const formatButton = screen.getByTestId('format-button');
327
+ fireEvent.click(formatButton);
328
+ await vi.advanceTimersByTimeAsync(400);
329
+ expect(mockFormatHtmlWithPrettier).toHaveBeenCalledWith('<p>Unformatted</p>');
330
+ // Formatting should only update local rawHTML state, not trigger onChange
331
+ expect(onChange).not.toHaveBeenCalled();
332
+ // Verify the formatted HTML is displayed in the source viewer
333
+ const viewer = screen.getByTestId('html-code-viewer');
334
+ expect(viewer).toHaveValue(formattedHtml);
335
+ });
336
+ it('should not call onChange when formatted HTML is same as original', async () => {
337
+ const html = '<p>Already Formatted</p>';
338
+ mockFormatHtmlWithPrettier.mockResolvedValueOnce(html);
339
+ renderComponent({ value: html });
340
+ onChange.mockClear();
341
+ const formatButton = screen.getByTestId('format-button');
342
+ fireEvent.click(formatButton);
343
+ await vi.advanceTimersByTimeAsync(400);
344
+ expect(mockFormatHtmlWithPrettier).toHaveBeenCalledWith(html);
345
+ expect(onChange).not.toHaveBeenCalled();
346
+ });
347
+ it('should handle toggleSource gracefully when editor is null', () => {
348
+ mockUseEditor.mockReturnValueOnce(null);
349
+ renderComponent();
350
+ const toggleButton = screen.queryByTestId('toggle-source');
351
+ // MenuBar should not be rendered when editor is null
352
+ expect(toggleButton).not.toBeInTheDocument();
353
+ });
313
354
  });
@@ -8,10 +8,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
8
8
  import { MenuBar } from '../menu-bar';
9
9
  import { Tiptap } from '../tiptap';
10
10
 
11
- const { mockUseEditor, mockChangeLanguage } = vi.hoisted(() => ({
12
- mockUseEditor: vi.fn(),
13
- mockChangeLanguage: vi.fn()
14
- }));
11
+ const { mockUseEditor, mockChangeLanguage, mockFormatHtmlWithPrettier } =
12
+ vi.hoisted(() => ({
13
+ mockUseEditor: vi.fn(),
14
+ mockChangeLanguage: vi.fn(),
15
+ mockFormatHtmlWithPrettier: vi.fn(async (html: string) => html)
16
+ }));
15
17
 
16
18
  vi.useFakeTimers();
17
19
 
@@ -39,11 +41,16 @@ vi.mock('@tiptap/html', () => ({
39
41
 
40
42
  vi.mock('../menu-bar', () => ({
41
43
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
- MenuBar: vi.fn(({ toggleSource, isSourceMode }: any) => (
44
+ MenuBar: vi.fn(({ toggleSource, isSourceMode, onFormat }: any) => (
43
45
  <div data-testid="menu-bar">
44
46
  <button onClick={toggleSource} data-testid="toggle-source">
45
47
  {isSourceMode ? 'Switch to Visual' : 'Switch to Source'}
46
48
  </button>
49
+ {onFormat && (
50
+ <button onClick={onFormat} data-testid="format-button">
51
+ Format
52
+ </button>
53
+ )}
47
54
  </div>
48
55
  ))
49
56
  }));
@@ -73,7 +80,8 @@ vi.mock('../../assets/icons', () => ({
73
80
 
74
81
  vi.mock('../../utils', () => ({
75
82
  getExtensions: vi.fn(() => []),
76
- cleanDuplicateSpans: vi.fn(html => html)
83
+ cleanDuplicateSpans: vi.fn(html => html),
84
+ formatHtmlWithPrettier: mockFormatHtmlWithPrettier
77
85
  }));
78
86
 
79
87
  vi.mock('@akinon/akilocale/react', () => ({
@@ -113,6 +121,8 @@ describe('Tiptap', () => {
113
121
  getHTML: vi.fn(() => '<p>Initial Content</p>'),
114
122
  getJSON: vi.fn(() => ({ type: 'doc' })),
115
123
  setEditable: vi.fn(),
124
+ on: vi.fn(),
125
+ off: vi.fn(),
116
126
  commands: {
117
127
  setContent: vi.fn(),
118
128
  blur: vi.fn()
@@ -197,24 +207,28 @@ describe('Tiptap', () => {
197
207
 
198
208
  rerender(<Tiptap value="<p>New</p>" onChange={onChange} />);
199
209
 
200
- expect(mockEditor.commands.setContent).toHaveBeenCalledWith('<p>New</p>');
210
+ expect(mockEditor.commands.setContent).toHaveBeenCalledWith('<p>New</p>', {
211
+ emitUpdate: false
212
+ });
201
213
  });
202
214
 
203
215
  it('should update editor editable state when readOnly prop changes', () => {
204
216
  const { rerender } = renderComponent({ readOnly: false });
205
- expect(mockEditor.setEditable).toHaveBeenCalledWith(true);
217
+ expect(mockEditor.setEditable).toHaveBeenCalledWith(true, false);
206
218
 
207
219
  rerender(
208
220
  <Tiptap value="<p>Test</p>" readOnly={true} onChange={onChange} />
209
221
  );
210
- expect(mockEditor.setEditable).toHaveBeenCalledWith(false);
222
+ expect(mockEditor.setEditable).toHaveBeenCalledWith(false, false);
211
223
  });
212
224
 
213
225
  it('should call onChange on editor update', () => {
214
226
  renderComponent();
215
227
 
216
- const onUpdateCallback = mockUseEditor.mock.calls[0][0].onUpdate;
217
- onUpdateCallback({ editor: mockEditor });
228
+ const handler = mockEditor.on.mock.calls.find(
229
+ ([event]: [string]) => event === 'update'
230
+ )?.[1];
231
+ handler();
218
232
 
219
233
  expect(onChange).toHaveBeenCalledWith({
220
234
  html: '<p>Initial Content</p>',
@@ -227,8 +241,10 @@ describe('Tiptap', () => {
227
241
  <Tiptap value="<p>Test</p>" onChange={undefined} />
228
242
  );
229
243
 
230
- const onUpdateCallback = mockUseEditor.mock.calls[0][0].onUpdate;
231
- expect(() => onUpdateCallback({ editor: mockEditor })).not.toThrow();
244
+ const handler = mockEditor.on.mock.calls.find(
245
+ ([event]: [string]) => event === 'update'
246
+ )?.[1];
247
+ expect(() => handler()).not.toThrow();
232
248
 
233
249
  const viewer = screen.getByTestId('html-code-viewer');
234
250
  fireEvent.change(viewer, { target: { value: 'new' } });
@@ -425,4 +441,54 @@ describe('Tiptap', () => {
425
441
  const viewer = screen.getByTestId('html-code-viewer');
426
442
  expect(viewer).toHaveValue('<script>console.log("test")</script>');
427
443
  });
444
+
445
+ it('should format HTML when handleFormat is called and HTML changes', async () => {
446
+ const formattedHtml = '<p>\n Formatted Content\n</p>';
447
+ mockFormatHtmlWithPrettier.mockResolvedValueOnce(formattedHtml);
448
+
449
+ renderComponent({ value: '<p>Unformatted</p>' });
450
+
451
+ const formatButton = screen.getByTestId('format-button');
452
+ fireEvent.click(formatButton);
453
+
454
+ await vi.advanceTimersByTimeAsync(400);
455
+
456
+ expect(mockFormatHtmlWithPrettier).toHaveBeenCalledWith(
457
+ '<p>Unformatted</p>'
458
+ );
459
+
460
+ // Formatting should only update local rawHTML state, not trigger onChange
461
+ expect(onChange).not.toHaveBeenCalled();
462
+
463
+ // Verify the formatted HTML is displayed in the source viewer
464
+ const viewer = screen.getByTestId('html-code-viewer');
465
+ expect(viewer).toHaveValue(formattedHtml);
466
+ });
467
+
468
+ it('should not call onChange when formatted HTML is same as original', async () => {
469
+ const html = '<p>Already Formatted</p>';
470
+ mockFormatHtmlWithPrettier.mockResolvedValueOnce(html);
471
+
472
+ renderComponent({ value: html });
473
+ onChange.mockClear();
474
+
475
+ const formatButton = screen.getByTestId('format-button');
476
+ fireEvent.click(formatButton);
477
+
478
+ await vi.advanceTimersByTimeAsync(400);
479
+
480
+ expect(mockFormatHtmlWithPrettier).toHaveBeenCalledWith(html);
481
+ expect(onChange).not.toHaveBeenCalled();
482
+ });
483
+
484
+ it('should handle toggleSource gracefully when editor is null', () => {
485
+ mockUseEditor.mockReturnValueOnce(null);
486
+
487
+ renderComponent();
488
+
489
+ const toggleButton = screen.queryByTestId('toggle-source');
490
+
491
+ // MenuBar should not be rendered when editor is null
492
+ expect(toggleButton).not.toBeInTheDocument();
493
+ });
428
494
  });
@@ -1 +1 @@
1
- {"version":3,"file":"html-code-viewer.d.ts","sourceRoot":"","sources":["../../../../src/tiptap/components/html-code-viewer.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAkD,MAAM,OAAO,CAAC;AAKvE,UAAU,mBAAmB;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,cAAc,GAAI,yDAM5B,mBAAmB,sBA2KrB,CAAC"}
1
+ {"version":3,"file":"html-code-viewer.d.ts","sourceRoot":"","sources":["../../../../src/tiptap/components/html-code-viewer.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAK5D,UAAU,mBAAmB;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,cAAc,GAAI,yDAM5B,mBAAmB,sBAkJrB,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { getSafeCustomTokens, useToken } from '@akinon/ui-theme';
2
2
  import { useStyleRegister } from '@ant-design/cssinjs';
3
3
  import { ConfigProvider } from 'antd';
4
- import React, { useCallback, useEffect, useMemo, useRef } from 'react';
4
+ import React, { useCallback, useMemo, useRef } from 'react';
5
5
  import { EDITOR_CONTENT_HEIGHT } from '../constants';
6
6
  import { formatHtmlWithPrettier, highlightHtml } from '../utils';
7
7
  export const HtmlCodeViewer = ({ value, onChange, className, contentHeight = EDITOR_CONTENT_HEIGHT, readOnly = false }) => {
@@ -19,29 +19,6 @@ export const HtmlCodeViewer = ({ value, onChange, className, contentHeight = EDI
19
19
  const handleChange = useCallback((e) => {
20
20
  onChange(e.target.value);
21
21
  }, [onChange]);
22
- useEffect(() => {
23
- {
24
- /*
25
- Auto-format on mount (switching to source mode).
26
- The isMounted check is necessary because formatHtmlWithPrettier is async.
27
- If the component unmounts before format completes, we must not call onChange
28
- to avoid "state update on unmounted component" errors.
29
- */
30
- }
31
- let isMounted = true;
32
- const formatOnMount = async () => {
33
- if (value && !readOnly) {
34
- const formatted = await formatHtmlWithPrettier(value);
35
- if (isMounted && formatted !== value) {
36
- onChange(formatted);
37
- }
38
- }
39
- };
40
- formatOnMount();
41
- return () => {
42
- isMounted = false;
43
- };
44
- }, []);
45
22
  const handlePaste = useCallback(async (e) => {
46
23
  if (readOnly)
47
24
  return;
@@ -33,45 +33,45 @@ describe('ActionToolbar', () => {
33
33
  });
34
34
  });
35
35
  it('should render all action buttons', () => {
36
- render(React.createElement(ActionToolbar, { editor: editor, editorState: createEditorState(), isSourceMode: false, toggleSource: vi.fn() }));
36
+ render(React.createElement(ActionToolbar, { editor: editor, editorState: createEditorState(), isSourceMode: false, toggleSource: vi.fn(), onFormat: vi.fn() }));
37
37
  expect(screen.getByTestId('action-button-format-undo')).toBeInTheDocument();
38
38
  expect(screen.getByTestId('action-button-format-redo')).toBeInTheDocument();
39
39
  expect(screen.getByTestId('action-button-eye-open')).toBeInTheDocument();
40
40
  });
41
41
  it('should handle undo click', () => {
42
42
  const focusSpy = vi.spyOn(editor, 'chain');
43
- render(React.createElement(ActionToolbar, { editor: editor, editorState: createEditorState({ canUndo: true }), isSourceMode: false, toggleSource: vi.fn() }));
43
+ render(React.createElement(ActionToolbar, { editor: editor, editorState: createEditorState({ canUndo: true }), isSourceMode: false, toggleSource: vi.fn(), onFormat: vi.fn() }));
44
44
  const undoButton = screen.getByTestId('action-button-format-undo');
45
45
  fireEvent.click(undoButton);
46
46
  expect(focusSpy).toHaveBeenCalled();
47
47
  });
48
48
  it('should handle redo click', () => {
49
49
  const focusSpy = vi.spyOn(editor, 'chain');
50
- render(React.createElement(ActionToolbar, { editor: editor, editorState: createEditorState({ canRedo: true }), isSourceMode: false, toggleSource: vi.fn() }));
50
+ render(React.createElement(ActionToolbar, { editor: editor, editorState: createEditorState({ canRedo: true }), isSourceMode: false, toggleSource: vi.fn(), onFormat: vi.fn() }));
51
51
  const redoButton = screen.getByTestId('action-button-format-redo');
52
52
  fireEvent.click(redoButton);
53
53
  expect(focusSpy).toHaveBeenCalled();
54
54
  });
55
55
  it('should handle toggle source click', () => {
56
56
  const toggleSource = vi.fn();
57
- render(React.createElement(ActionToolbar, { editor: editor, editorState: createEditorState(), isSourceMode: false, toggleSource: toggleSource }));
57
+ render(React.createElement(ActionToolbar, { editor: editor, editorState: createEditorState(), isSourceMode: false, toggleSource: toggleSource, onFormat: vi.fn() }));
58
58
  const sourceButton = screen.getByTestId('action-button-eye-open');
59
59
  fireEvent.click(sourceButton);
60
60
  expect(toggleSource).toHaveBeenCalled();
61
61
  });
62
62
  it('should disable undo and redo when in source mode', () => {
63
- render(React.createElement(ActionToolbar, { editor: editor, editorState: createEditorState({ canUndo: true, canRedo: true }), isSourceMode: true, toggleSource: vi.fn() }));
63
+ render(React.createElement(ActionToolbar, { editor: editor, editorState: createEditorState({ canUndo: true, canRedo: true }), isSourceMode: true, toggleSource: vi.fn(), onFormat: vi.fn() }));
64
64
  expect(screen.getByTestId('action-button-format-undo')).toBeDisabled();
65
65
  expect(screen.getByTestId('action-button-format-redo')).toBeDisabled();
66
66
  });
67
67
  it('should show eye-close icon and normal mode tooltip when isSourceMode is true', () => {
68
- render(React.createElement(ActionToolbar, { editor: editor, editorState: createEditorState(), isSourceMode: true, toggleSource: vi.fn() }));
68
+ render(React.createElement(ActionToolbar, { editor: editor, editorState: createEditorState(), isSourceMode: true, toggleSource: vi.fn(), onFormat: vi.fn() }));
69
69
  const sourceButton = screen.getByTestId('action-button-eye-close');
70
70
  expect(sourceButton).toBeInTheDocument();
71
71
  expect(sourceButton).toHaveAttribute('aria-label', 'Normal Mode');
72
72
  });
73
73
  it('should apply active class to source mode button when active', () => {
74
- render(React.createElement(ActionToolbar, { editor: editor, editorState: createEditorState(), isSourceMode: true, toggleSource: vi.fn() }));
74
+ render(React.createElement(ActionToolbar, { editor: editor, editorState: createEditorState(), isSourceMode: true, toggleSource: vi.fn(), onFormat: vi.fn() }));
75
75
  const sourceButton = screen.getByTestId('action-button-eye-close');
76
76
  expect(sourceButton).toHaveClass('active');
77
77
  });
@@ -105,6 +105,7 @@ describe('ActionToolbar', () => {
105
105
  editorState={createEditorState()}
106
106
  isSourceMode={false}
107
107
  toggleSource={vi.fn()}
108
+ onFormat={vi.fn()}
108
109
  />
109
110
  );
110
111
 
@@ -122,6 +123,7 @@ describe('ActionToolbar', () => {
122
123
  editorState={createEditorState({ canUndo: true })}
123
124
  isSourceMode={false}
124
125
  toggleSource={vi.fn()}
126
+ onFormat={vi.fn()}
125
127
  />
126
128
  );
127
129
 
@@ -140,6 +142,7 @@ describe('ActionToolbar', () => {
140
142
  editorState={createEditorState({ canRedo: true })}
141
143
  isSourceMode={false}
142
144
  toggleSource={vi.fn()}
145
+ onFormat={vi.fn()}
143
146
  />
144
147
  );
145
148
 
@@ -158,6 +161,7 @@ describe('ActionToolbar', () => {
158
161
  editorState={createEditorState()}
159
162
  isSourceMode={false}
160
163
  toggleSource={toggleSource}
164
+ onFormat={vi.fn()}
161
165
  />
162
166
  );
163
167
 
@@ -174,6 +178,7 @@ describe('ActionToolbar', () => {
174
178
  editorState={createEditorState({ canUndo: true, canRedo: true })}
175
179
  isSourceMode={true}
176
180
  toggleSource={vi.fn()}
181
+ onFormat={vi.fn()}
177
182
  />
178
183
  );
179
184
 
@@ -188,6 +193,7 @@ describe('ActionToolbar', () => {
188
193
  editorState={createEditorState()}
189
194
  isSourceMode={true}
190
195
  toggleSource={vi.fn()}
196
+ onFormat={vi.fn()}
191
197
  />
192
198
  );
193
199
 
@@ -203,6 +209,7 @@ describe('ActionToolbar', () => {
203
209
  editorState={createEditorState()}
204
210
  isSourceMode={true}
205
211
  toggleSource={vi.fn()}
212
+ onFormat={vi.fn()}
206
213
  />
207
214
  );
208
215
 
@@ -89,6 +89,7 @@ describe('MenuBar', () => {
89
89
  const defaultProps = {
90
90
  isSourceMode: false,
91
91
  toggleSource: vi.fn(),
92
+ onFormat: vi.fn(),
92
93
  onSave: vi.fn()
93
94
  };
94
95
  beforeEach(() => {
@@ -99,6 +99,7 @@ describe('MenuBar', () => {
99
99
  const defaultProps = {
100
100
  isSourceMode: false,
101
101
  toggleSource: vi.fn(),
102
+ onFormat: vi.fn(),
102
103
  onSave: vi.fn()
103
104
  };
104
105
 
@@ -7,9 +7,10 @@ interface ActionToolbarProps {
7
7
  editorState: EditorState;
8
8
  isSourceMode: boolean;
9
9
  toggleSource: () => void;
10
+ onFormat: () => Promise<void>;
10
11
  disabled?: boolean;
11
12
  hiddenButtons?: Array<ToolbarButtonId>;
12
13
  }
13
- export declare const ActionToolbar: ({ editor, editorState, isSourceMode, toggleSource, disabled, hiddenButtons }: ActionToolbarProps) => React.JSX.Element;
14
+ export declare const ActionToolbar: ({ editor, editorState, isSourceMode, toggleSource, onFormat, disabled, hiddenButtons }: ActionToolbarProps) => React.JSX.Element;
14
15
  export {};
15
16
  //# sourceMappingURL=action-toolbar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"action-toolbar.d.ts","sourceRoot":"","sources":["../../../../../src/tiptap/components/menu-bar/action-toolbar.tsx"],"names":[],"mappings":"AAAA,OAAO,2BAA2B,CAAC;AAGnC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEhE,UAAU,kBAAkB;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;CACxC;AAYD,eAAO,MAAM,aAAa,GAAI,8EAO3B,kBAAkB,sBAmDpB,CAAC"}
1
+ {"version":3,"file":"action-toolbar.d.ts","sourceRoot":"","sources":["../../../../../src/tiptap/components/menu-bar/action-toolbar.tsx"],"names":[],"mappings":"AAAA,OAAO,2BAA2B,CAAC;AAGnC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEhE,UAAU,kBAAkB;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;CACxC;AAYD,eAAO,MAAM,aAAa,GAAI,wFAQ3B,kBAAkB,sBA6DpB,CAAC"}
@@ -3,7 +3,7 @@ import { Button } from '@akinon/ui-button';
3
3
  import React from 'react';
4
4
  import { i18n } from '../../../i18n';
5
5
  import { TOOLBAR_BUTTON_IDS } from '../../constants';
6
- export const ActionToolbar = ({ editor, editorState, isSourceMode, toggleSource, disabled = false, hiddenButtons = [] }) => {
6
+ export const ActionToolbar = ({ editor, editorState, isSourceMode, toggleSource, onFormat, disabled = false, hiddenButtons = [] }) => {
7
7
  const activeCls = 'active';
8
8
  const actionButtons = [
9
9
  {
@@ -23,8 +23,16 @@ export const ActionToolbar = ({ editor, editorState, isSourceMode, toggleSource,
23
23
  disabled: disabled || isSourceMode || !editorState.canRedo
24
24
  },
25
25
  {
26
- id: TOOLBAR_BUTTON_IDS.SOURCE_MODE,
26
+ id: TOOLBAR_BUTTON_IDS.FORMAT,
27
27
  key: 3,
28
+ icon: 'format-beautify',
29
+ tooltip: i18n.t('editor.toolbar.format'),
30
+ onClick: onFormat,
31
+ disabled: !isSourceMode
32
+ },
33
+ {
34
+ id: TOOLBAR_BUTTON_IDS.SOURCE_MODE,
35
+ key: 4,
28
36
  icon: isSourceMode ? 'eye-close' : 'eye-open',
29
37
  tooltip: isSourceMode
30
38
  ? i18n.t('editor.toolbar.normal_mode')
@@ -5,13 +5,14 @@ interface MenuBarProps {
5
5
  editor: Editor;
6
6
  isSourceMode: boolean;
7
7
  toggleSource: () => void;
8
+ onFormat: () => Promise<void>;
8
9
  onSave?: () => void;
9
10
  className?: string;
10
11
  toolbar?: ToolbarConfig;
11
12
  readOnly?: boolean;
12
13
  }
13
14
  export declare const MenuBar: {
14
- ({ editor, isSourceMode, toggleSource, className, toolbar, readOnly }: MenuBarProps): React.JSX.Element;
15
+ ({ editor, isSourceMode, toggleSource, onFormat, className, toolbar, readOnly }: MenuBarProps): React.JSX.Element;
15
16
  displayName: string;
16
17
  };
17
18
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/tiptap/components/menu-bar/index.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAI5C,OAAO,KAAK,MAAM,OAAO,CAAC;AAQ1B,OAAO,KAAK,EAAe,aAAa,EAAE,MAAM,aAAa,CAAC;AAS9D,UAAU,YAAY;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,eAAO,MAAM,OAAO;2EAOjB,YAAY;;CAqMd,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/tiptap/components/menu-bar/index.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAI5C,OAAO,KAAK,MAAM,OAAO,CAAC;AAQ1B,OAAO,KAAK,EAAe,aAAa,EAAE,MAAM,aAAa,CAAC;AAS9D,UAAU,YAAY;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,eAAO,MAAM,OAAO;qFAQjB,YAAY;;CAsMd,CAAC"}
@@ -12,7 +12,7 @@ import { LinkPopover } from './link-popover';
12
12
  import { ListAndBlockToolbar } from './list-and-block-toolbar';
13
13
  import { MediaToolbar } from './media-toolbar';
14
14
  import { TextFormattingToolbar } from './text-formatting-toolbar';
15
- export const MenuBar = ({ editor, isSourceMode, toggleSource, className, toolbar, readOnly = false }) => {
15
+ export const MenuBar = ({ editor, isSourceMode, toggleSource, onFormat, className, toolbar, readOnly = false }) => {
16
16
  var _a, _b, _c, _d, _e, _f, _g;
17
17
  const { getPrefixCls, theme } = React.useContext(ConfigProvider.ConfigContext);
18
18
  const { token, hashId } = useToken();
@@ -126,6 +126,6 @@ export const MenuBar = ({ editor, isSourceMode, toggleSource, className, toolbar
126
126
  !((_d = toolbar === null || toolbar === void 0 ? void 0 : toolbar.hiddenGroups) === null || _d === void 0 ? void 0 : _d.includes(TOOLBAR_GROUP_IDS.LINK)) && (React.createElement(LinkPopover, { editor: editor, editorState: editorState, disabled: isDisabled })),
127
127
  !((_e = toolbar === null || toolbar === void 0 ? void 0 : toolbar.hiddenGroups) === null || _e === void 0 ? void 0 : _e.includes(TOOLBAR_GROUP_IDS.MEDIA)) && (React.createElement(MediaToolbar, { editor: editor, editorState: editorState, disabled: isDisabled, hiddenButtons: toolbar === null || toolbar === void 0 ? void 0 : toolbar.hiddenButtons })),
128
128
  !((_f = toolbar === null || toolbar === void 0 ? void 0 : toolbar.hiddenGroups) === null || _f === void 0 ? void 0 : _f.includes(TOOLBAR_GROUP_IDS.ALIGNMENT)) && (React.createElement(AlignmentToolbar, { editor: editor, editorState: editorState, disabled: isDisabled })),
129
- !((_g = toolbar === null || toolbar === void 0 ? void 0 : toolbar.hiddenGroups) === null || _g === void 0 ? void 0 : _g.includes(TOOLBAR_GROUP_IDS.HISTORY)) && (React.createElement(ActionToolbar, { editor: editor, editorState: editorState, isSourceMode: isSourceMode, toggleSource: toggleSource, disabled: readOnly, hiddenButtons: toolbar === null || toolbar === void 0 ? void 0 : toolbar.hiddenButtons })))));
129
+ !((_g = toolbar === null || toolbar === void 0 ? void 0 : toolbar.hiddenGroups) === null || _g === void 0 ? void 0 : _g.includes(TOOLBAR_GROUP_IDS.HISTORY)) && (React.createElement(ActionToolbar, { editor: editor, editorState: editorState, isSourceMode: isSourceMode, toggleSource: toggleSource, onFormat: onFormat, disabled: readOnly, hiddenButtons: toolbar === null || toolbar === void 0 ? void 0 : toolbar.hiddenButtons })))));
130
130
  };
131
131
  MenuBar.displayName = 'MenuBar';
@@ -1 +1 @@
1
- {"version":3,"file":"tiptap.d.ts","sourceRoot":"","sources":["../../../../src/tiptap/components/tiptap.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAkD,MAAM,OAAO,CAAC;AAQvE,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAOhD,eAAO,MAAM,MAAM,GAAI,iHAUpB,eAAe,sBAiZjB,CAAC"}
1
+ {"version":3,"file":"tiptap.d.ts","sourceRoot":"","sources":["../../../../src/tiptap/components/tiptap.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAkD,MAAM,OAAO,CAAC;AAQvE,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAWhD,eAAO,MAAM,MAAM,GAAI,iHAUpB,eAAe,sBAmajB,CAAC"}