@griddo/ax 12.4.0 → 12.5.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 (35) hide show
  1. package/config/webpack.config.js +5 -0
  2. package/package.json +2 -2
  3. package/src/__tests__/components/ConfigPanel/Form/Form.test.tsx +47 -57
  4. package/src/__tests__/components/ConfigPanel/Header/Header.test.tsx +46 -42
  5. package/src/__tests__/components/ElementsTooltip/ElementsTooltip.test.tsx +6 -17
  6. package/src/__tests__/components/EmptyState/EmptyState.test.tsx +9 -9
  7. package/src/__tests__/components/Fields/AsyncSelect/AsyncSelect.test.tsx +60 -50
  8. package/src/__tests__/components/Fields/ColorPicker/ColorPicker.test.tsx +66 -58
  9. package/src/__tests__/components/Fields/ComponentContainer/ComponentContainer.test.tsx +222 -425
  10. package/src/__tests__/components/Fields/FileField/FileField.test.tsx +13 -14
  11. package/src/__tests__/components/Fields/ImageField/ImageField.test.tsx +275 -259
  12. package/src/__tests__/components/Fields/IntegrationsField/IntegrationsField.test.tsx +50 -48
  13. package/src/__tests__/components/Fields/NoteField/NoteField.test.tsx +9 -8
  14. package/src/__tests__/components/Fields/NumberField/NumberField.test.tsx +8 -12
  15. package/src/__tests__/components/Fields/TextArea/TextArea.test.tsx +6 -2
  16. package/src/__tests__/components/Fields/ToggleField/ToggleField.test.tsx +15 -23
  17. package/src/__tests__/components/Fields/Tooltip/Tooltip.test.tsx +8 -2
  18. package/src/__tests__/components/Fields/VisualUniqueSelection/VisualUniqueSelection.test.tsx +29 -21
  19. package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.config.test.tsx +103 -0
  20. package/src/__tests__/components/FieldsBehavior/FieldsBehavior.test.tsx +7 -2
  21. package/src/__tests__/components/ResizePanel/ResizePanel.test.tsx +8 -6
  22. package/src/__tests__/components/SideModal/SideModal.test.tsx +150 -104
  23. package/src/__tests__/components/TableFilters/LiveFilter/LiveFilter.test.tsx +15 -29
  24. package/src/__tests__/components/TableFilters/StatusFilter/StatusFilter.test.tsx +61 -36
  25. package/src/__tests__/components/Tabs/Tabs.test.tsx +65 -61
  26. package/src/__tests__/components/Toast/Toast.test.tsx +19 -21
  27. package/src/__tests__/modules/FramePreview/FramePreview.test.tsx +16 -0
  28. package/src/__tests__/modules/Sites/SitesList/ListView/BulkHeader/BulkHeader.test.tsx +24 -19
  29. package/src/__tests__/modules/Sites/SitesList/SitesList.test.tsx +15 -6
  30. package/src/__tests__/modules/Users/Roles/BulkHeader/BulkHeader.test.tsx +69 -64
  31. package/src/__tests__/modules/Users/Roles/Roles.test.tsx +7 -4
  32. package/src/components/Fields/Wysiwyg/config.tsx +68 -17
  33. package/src/components/Fields/Wysiwyg/helpers.tsx +2 -0
  34. package/src/components/Fields/Wysiwyg/index.tsx +41 -10
  35. package/src/components/ResizePanel/index.tsx +7 -2
@@ -17,28 +17,35 @@ import { defaultStore } from "./../../../../../../__mocks__/store/GenericStore";
17
17
  afterEach(cleanup);
18
18
 
19
19
  const store = configureStore([thunk])(defaultStore);
20
- const defaultProps = mock<IBulkHeaderProps>();
20
+ // El escenario base es el que el primer test dejaba en el objeto compartido y
21
+ // del que el segundo dependía sin declararlo.
22
+ const makeProps = (overrides: Partial<IBulkHeaderProps> = {}): IBulkHeaderProps =>
23
+ Object.assign(
24
+ mock<IBulkHeaderProps>(),
25
+ {
26
+ showBulk: false,
27
+ checkState: { isAllSelected: false, indeterminate: false },
28
+ filterValues: {
29
+ order: [{ value: "lastAccess-desc", label: "lastAccess-desc" }],
30
+ liveStatus: [{ value: "all", label: "All" }],
31
+ },
32
+ sortedListStatus: {
33
+ isAscending: false,
34
+ sortedByTitle: false,
35
+ sortedByLastAccess: true,
36
+ sortedByDateCreated: false,
37
+ },
38
+ totalItems: 22,
39
+ },
40
+ overrides,
41
+ );
21
42
 
22
43
  describe("BulkHeader component rendering", () => {
23
44
  it("should render table header", async () => {
24
- defaultProps.showBulk = false;
25
- defaultProps.checkState = { isAllSelected: false, indeterminate: false };
26
- defaultProps.filterValues = {
27
- order: [{ value: "lastAccess-desc", label: "lastAccess-desc" }],
28
- liveStatus: [{ value: "all", label: "All" }],
29
- };
30
- defaultProps.sortedListStatus = {
31
- isAscending: false,
32
- sortedByTitle: false,
33
- sortedByLastAccess: true,
34
- sortedByDateCreated: false,
35
- };
36
- defaultProps.totalItems = 22;
37
-
38
45
  await act(async () => {
39
46
  render(
40
47
  <ThemeProvider theme={parseTheme(globalTheme)}>
41
- <BulkHeader {...defaultProps} />
48
+ <BulkHeader {...makeProps({ showBulk: false })} />
42
49
  </ThemeProvider>,
43
50
  { store },
44
51
  );
@@ -48,12 +55,10 @@ describe("BulkHeader component rendering", () => {
48
55
  });
49
56
 
50
57
  it("should render bulk header", async () => {
51
- defaultProps.showBulk = true;
52
-
53
58
  await act(async () => {
54
59
  render(
55
60
  <ThemeProvider theme={parseTheme(globalTheme)}>
56
- <BulkHeader {...defaultProps} />
61
+ <BulkHeader {...makeProps({ showBulk: true })} />
57
62
  </ThemeProvider>,
58
63
  { store },
59
64
  );
@@ -12,7 +12,7 @@ import configureStore from "redux-mock-store";
12
12
  import thunk from "redux-thunk";
13
13
  import { ThemeProvider } from "styled-components";
14
14
 
15
- import { act, cleanup, fireEvent, render, screen, waitFor } from "./../../../../../config/tests/test-utils";
15
+ import { act, cleanup, fireEvent, render, screen, waitFor, within } from "./../../../../../config/tests/test-utils";
16
16
  import { sitesResponse } from "./../../../../__mocks__/axios/SitesList";
17
17
  import { sitesOptions } from "./../../../../__mocks__/axios/SitesListOptions";
18
18
  import { sitesDataMock, sitesDataNoThumbnailMock, userDataMock } from "./../../../../__mocks__/store/SitesList";
@@ -59,6 +59,15 @@ const renderSitesList = async (storeData: any = initialStore) => {
59
59
 
60
60
  beforeEach(() => {
61
61
  mockedAxios.mockResolvedValue(sitesResponse);
62
+ // Tres cosas sobreviven al test que las toca y hacían que el resto solo
63
+ // funcionase en un orden concreto: el `history` de @ax/routes es un
64
+ // singleton y los tests de navegación lo dejaban en /sites/pages; SitesList
65
+ // guarda el modo de vista en localStorage, así que el siguiente test
66
+ // arrancaba en la vista equivocada y no encontraba los testids `grid-*`; y
67
+ // el parche de scrollTo se hacía sobre el prototipo dentro de dos tests.
68
+ history.replace("/");
69
+ localStorage.clear();
70
+ window.HTMLElement.prototype.scrollTo = vi.fn();
62
71
  });
63
72
 
64
73
  describe("Sites module rendering", () => {
@@ -198,8 +207,6 @@ describe("Sites module events", () => {
198
207
  });
199
208
 
200
209
  it("should go to site on click recent site item", async () => {
201
- window.HTMLElement.prototype.scrollTo = vi.fn();
202
-
203
210
  await renderSitesList();
204
211
 
205
212
  const recentSiteItems = screen.getAllByTestId("recent-sites-item");
@@ -224,8 +231,6 @@ describe("Sites module events", () => {
224
231
  });
225
232
 
226
233
  it("should go to site on click list site item", async () => {
227
- window.HTMLElement.prototype.scrollTo = vi.fn();
228
-
229
234
  await renderSitesList();
230
235
 
231
236
  const gridIconAction = screen.getByTestId("icon-action-Grid2");
@@ -238,7 +243,11 @@ describe("Sites module events", () => {
238
243
  const listSitesItems = screen.getAllByTestId("list-site-item");
239
244
  expect(listSitesItems.length).toEqual(2);
240
245
 
241
- fireEvent.click(listSitesItems[0]);
246
+ // La fila solo tiene onContextMenu; navegan las celdas. Antes se pulsaba la
247
+ // fila y no pasaba nada: la aserción se cumplía porque otro test había
248
+ // dejado ya el history en /sites/pages.
249
+ const nameCell = within(listSitesItems[0]).getAllByRole("cell")[2];
250
+ fireEvent.click(nameCell);
242
251
  await waitFor(() => expect(history.location.pathname).toEqual("/sites/pages"));
243
252
  });
244
253
 
@@ -13,49 +13,53 @@ import globalTheme from "@ax/themes/theme.json";
13
13
 
14
14
  afterEach(cleanup);
15
15
 
16
- const defaultProps = mock<IBulkHeaderProps>();
17
- defaultProps.selectedRoles = [];
18
- defaultProps.roles = [
19
- {
20
- id: 1,
21
- name: "Administrator",
22
- hex: "#C7ECF8",
23
- description:
24
- "You can manage all of your site's settings, users and roles. You can create and publish all pages of your site.",
25
- permissions: { totalPermissions: "78/78", sitePermissions: [], globalPermissions: [] },
26
- active: true,
27
- users: [],
28
- },
29
- {
30
- id: 2,
31
- name: "Constructor",
32
- hex: "#B5FBFF",
33
- description:
34
- "You can edit your website, add pages, modules, modify URLs, menus, colours and themes, html tags. You don't manage users, or manage the site configuration.",
35
- permissions: { totalPermissions: "49/78", sitePermissions: [], globalPermissions: [] },
36
- active: true,
37
- users: [],
38
- },
39
- {
40
- id: 3,
41
- name: "Editor",
42
- hex: "#E2EE9F",
43
- description: "You have permission to view, add and edit site content, such as pages and blocks.",
44
- permissions: { totalPermissions: "21/78", sitePermissions: [], globalPermissions: [] },
45
- active: false,
46
- users: [],
47
- },
48
- ];
16
+ const makeProps = (overrides: Partial<IBulkHeaderProps> = {}): IBulkHeaderProps =>
17
+ Object.assign(
18
+ mock<IBulkHeaderProps>(),
19
+ {
20
+ selectedRoles: [],
21
+ checkState: { isAllSelected: false },
22
+ roles: [
23
+ {
24
+ id: 1,
25
+ name: "Administrator",
26
+ hex: "#C7ECF8",
27
+ description:
28
+ "You can manage all of your site's settings, users and roles. You can create and publish all pages of your site.",
29
+ permissions: { totalPermissions: "78/78", sitePermissions: [], globalPermissions: [] },
30
+ active: true,
31
+ users: [],
32
+ },
33
+ {
34
+ id: 2,
35
+ name: "Constructor",
36
+ hex: "#B5FBFF",
37
+ description:
38
+ "You can edit your website, add pages, modules, modify URLs, menus, colours and themes, html tags. You don't manage users, or manage the site configuration.",
39
+ permissions: { totalPermissions: "49/78", sitePermissions: [], globalPermissions: [] },
40
+ active: true,
41
+ users: [],
42
+ },
43
+ {
44
+ id: 3,
45
+ name: "Editor",
46
+ hex: "#E2EE9F",
47
+ description: "You have permission to view, add and edit site content, such as pages and blocks.",
48
+ permissions: { totalPermissions: "21/78", sitePermissions: [], globalPermissions: [] },
49
+ active: false,
50
+ users: [],
51
+ },
52
+ ],
53
+ },
54
+ overrides,
55
+ );
49
56
 
50
57
  describe("BulkHeader component rendering", () => {
51
58
  it("should render bulk header", async () => {
52
- defaultProps.showBulk = true;
53
- defaultProps.checkState.isAllSelected = true;
54
-
55
59
  await act(async () => {
56
60
  render(
57
61
  <ThemeProvider theme={parseTheme(globalTheme)}>
58
- <BulkHeader {...defaultProps} />
62
+ <BulkHeader {...makeProps({ showBulk: true, checkState: { isAllSelected: true } })} />
59
63
  </ThemeProvider>,
60
64
  );
61
65
  });
@@ -64,13 +68,10 @@ describe("BulkHeader component rendering", () => {
64
68
  });
65
69
 
66
70
  it("should render bulk header options", async () => {
67
- defaultProps.showBulk = true;
68
- defaultProps.checkState.isAllSelected = true;
69
-
70
71
  await act(async () => {
71
72
  render(
72
73
  <ThemeProvider theme={parseTheme(globalTheme)}>
73
- <BulkHeader {...defaultProps} />
74
+ <BulkHeader {...makeProps({ showBulk: true, checkState: { isAllSelected: true } })} />
74
75
  </ThemeProvider>,
75
76
  );
76
77
  });
@@ -79,14 +80,16 @@ describe("BulkHeader component rendering", () => {
79
80
  });
80
81
 
81
82
  it("should render underline when isScrolling ", async () => {
82
- defaultProps.showBulk = false;
83
- defaultProps.isScrolling = true;
84
- defaultProps.sortedListStatus = { isAscending: false, sortedByTitle: false };
85
-
86
83
  await act(async () => {
87
84
  render(
88
85
  <ThemeProvider theme={parseTheme(globalTheme)}>
89
- <BulkHeader {...defaultProps} />
86
+ <BulkHeader
87
+ {...makeProps({
88
+ showBulk: false,
89
+ isScrolling: true,
90
+ sortedListStatus: { isAscending: false, sortedByTitle: false },
91
+ })}
92
+ />
90
93
  </ThemeProvider>,
91
94
  );
92
95
  });
@@ -95,14 +98,16 @@ describe("BulkHeader component rendering", () => {
95
98
  });
96
99
 
97
100
  it("should render users filter when isSiteView ", async () => {
98
- defaultProps.showBulk = false;
99
- defaultProps.sortedListStatus = { isAscending: false, sortedByTitle: false, sortedByUsers: false };
100
- defaultProps.isSiteView = true;
101
-
102
101
  await act(async () => {
103
102
  render(
104
103
  <ThemeProvider theme={parseTheme(globalTheme)}>
105
- <BulkHeader {...defaultProps} />
104
+ <BulkHeader
105
+ {...makeProps({
106
+ showBulk: false,
107
+ sortedListStatus: { isAscending: false, sortedByTitle: false, sortedByUsers: false },
108
+ isSiteView: true,
109
+ })}
110
+ />
106
111
  </ThemeProvider>,
107
112
  );
108
113
  });
@@ -114,18 +119,18 @@ describe("BulkHeader component rendering", () => {
114
119
 
115
120
  describe("BulkHeader module events", () => {
116
121
  it("should call deactivate role on click option", async () => {
117
- defaultProps.showBulk = true;
118
- defaultProps.isSiteView = false;
119
- defaultProps.checkState.isAllSelected = true;
120
122
  const activateRolesMock = vi.fn();
121
123
 
122
- defaultProps.activateRoles = activateRolesMock as CalledWithMock<void, [active: boolean]> &
123
- ((active: boolean) => void);
124
-
125
124
  await act(async () => {
126
125
  render(
127
126
  <ThemeProvider theme={parseTheme(globalTheme)}>
128
- <BulkHeader {...defaultProps} />
127
+ <BulkHeader
128
+ {...makeProps({
129
+ showBulk: true,
130
+ isSiteView: false,
131
+ activateRoles: activateRolesMock as CalledWithMock<void, [active: boolean]> & ((active: boolean) => void),
132
+ })}
133
+ />
129
134
  </ThemeProvider>,
130
135
  );
131
136
  });
@@ -136,17 +141,17 @@ describe("BulkHeader module events", () => {
136
141
  });
137
142
 
138
143
  it("should call activate role on click option", async () => {
139
- defaultProps.showBulk = true;
140
- defaultProps.checkState.isAllSelected = true;
141
144
  const activateRolesMock = vi.fn();
142
145
 
143
- defaultProps.activateRoles = activateRolesMock as CalledWithMock<void, [active: boolean]> &
144
- ((active: boolean) => void);
145
-
146
146
  await act(async () => {
147
147
  render(
148
148
  <ThemeProvider theme={parseTheme(globalTheme)}>
149
- <BulkHeader {...defaultProps} />
149
+ <BulkHeader
150
+ {...makeProps({
151
+ showBulk: true,
152
+ activateRoles: activateRolesMock as CalledWithMock<void, [active: boolean]> & ((active: boolean) => void),
153
+ })}
154
+ />
150
155
  </ThemeProvider>,
151
156
  );
152
157
  });
@@ -20,6 +20,13 @@ import { rolesDataResponse } from "./../../../../__mocks__/axios/Roles";
20
20
 
21
21
  afterEach(cleanup);
22
22
 
23
+ // jsdom no implementa scrollIntoView. Lo parcheaban cuatro tests dentro de su
24
+ // propio cuerpo, así que los que lo necesitaban solo funcionaban si alguno de
25
+ // esos ya había corrido: es un parche sobre el prototipo y sobrevive al test.
26
+ beforeEach(() => {
27
+ window.HTMLElement.prototype.scrollIntoView = vi.fn();
28
+ });
29
+
23
30
  vi.mock("axios");
24
31
  const mockedAxios = axios as MockedFunction<typeof axios>;
25
32
  const defaultProps = mock<IRolesProps>();
@@ -27,7 +34,6 @@ const defaultProps = mock<IRolesProps>();
27
34
  describe("Roles module rendering", () => {
28
35
  it("should render component", async () => {
29
36
  mockedAxios.mockResolvedValue(rolesDataResponse);
30
- window.HTMLElement.prototype.scrollIntoView = vi.fn();
31
37
  const initialStore = {
32
38
  app: { isLoading: false },
33
39
  users: { roles: rolesDataMock },
@@ -61,7 +67,6 @@ describe("Roles module rendering", () => {
61
67
 
62
68
  it("should render role items", async () => {
63
69
  mockedAxios.mockResolvedValue(rolesDataResponse);
64
- window.HTMLElement.prototype.scrollIntoView = vi.fn();
65
70
  const initialStore = {
66
71
  app: { isLoading: false },
67
72
  users: { roles: rolesDataMock },
@@ -93,7 +98,6 @@ describe("Roles module rendering", () => {
93
98
 
94
99
  it("should render tag colors", async () => {
95
100
  mockedAxios.mockResolvedValue(rolesDataResponse);
96
- window.HTMLElement.prototype.scrollIntoView = vi.fn();
97
101
  const initialStore = {
98
102
  app: { isLoading: false },
99
103
  users: { roles: rolesDataMock },
@@ -427,7 +431,6 @@ describe("Roles module events", () => {
427
431
 
428
432
  it("should render FullArrowDown icon if is filtered by descending name", async () => {
429
433
  mockedAxios.mockResolvedValue(rolesDataResponse);
430
- window.HTMLElement.prototype.scrollIntoView = vi.fn();
431
434
  const getRolesMock = vi.fn();
432
435
  const activateRolesMock = vi.fn();
433
436
  defaultProps.getRoles = getRolesMock as CalledWithMock<Promise<void>, [params?: IGetRoles | undefined]>;
@@ -19,6 +19,18 @@ const getTableCellStyles = () => {
19
19
  const config = getRichTextConfigLazy();
20
20
  return config?.tableCellStyles ? parseClassNames(config.tableCellStyles) : undefined;
21
21
  };
22
+ const getImageStyles = () => {
23
+ const config = getRichTextConfigLazy();
24
+ return config?.imageStyles ? parseClassNames(config.imageStyles) : undefined;
25
+ };
26
+ // Opt-in: instances not asking for it keep their images unresizable.
27
+ const getImageResize = () => getRichTextConfigLazy()?.imageResize === true;
28
+ // When resizing is allowed the size is stored as a rounded percentage, so it
29
+ // still adapts to its container instead of being pinned to a pixel width.
30
+ const getImageResizeConfig = () => {
31
+ const imageResize = getImageResize();
32
+ return { imageResize, imageResizeWithPercent: imageResize, imageRoundPercent: imageResize };
33
+ };
22
34
 
23
35
  const getMiscButtons = () => {
24
36
  const paragraphStyles = getParagraphStyles();
@@ -49,7 +61,14 @@ const getButtonsFull = () => ({
49
61
  buttonsVisible: 6,
50
62
  },
51
63
  moreText2: {
52
- buttons: ["insertLinkGriddo", "insertImageGriddo", "insertFileGriddo", "insertVideo", "clearFormatting", "fullscreen"],
64
+ buttons: [
65
+ "insertLinkGriddo",
66
+ "insertImageGriddo",
67
+ "insertFileGriddo",
68
+ "insertVideo",
69
+ "clearFormatting",
70
+ "fullscreen",
71
+ ],
53
72
  buttonsVisible: 6,
54
73
  },
55
74
  moreMisc: {
@@ -76,6 +95,25 @@ const getButtons = () => {
76
95
 
77
96
  const getInlineToolbar = () => ["langDropdown", "undo", "redo"];
78
97
 
98
+ const getImageEditButtons = () => {
99
+ const imageStyles = getImageStyles();
100
+ return [
101
+ "imageReplaceGriddo",
102
+ "imageAlign",
103
+ "imageCaption",
104
+ "imageRemove",
105
+ "|",
106
+ "imageLinkGriddo",
107
+ "linkOpen",
108
+ "linkRemove",
109
+ "-",
110
+ "imageDisplay",
111
+ imageStyles ? "imageStyle" : undefined,
112
+ "imageAlt",
113
+ getImageResize() ? "imageSize" : undefined,
114
+ ];
115
+ };
116
+
79
117
  const getWysiwygConfig = () => ({
80
118
  key: process.env.GRIDDO_FROALA_KEY || process.env.REACT_APP_FROALA_KEY,
81
119
  // pastePlain: false,
@@ -89,20 +127,10 @@ const getWysiwygConfig = () => ({
89
127
  H3: "Heading 3",
90
128
  H4: "Heading 4",
91
129
  },
92
- imageEditButtons: [
93
- "imageReplaceGriddo",
94
- "imageAlign",
95
- "imageCaption",
96
- "imageRemove",
97
- "|",
98
- "imageLinkGriddo",
99
- "linkOpen",
100
- "linkRemove",
101
- "-",
102
- "imageDisplay",
103
- "imageAlt",
104
- ],
105
- imageResize: false,
130
+ imageEditButtons: getImageEditButtons(),
131
+ ...getImageResizeConfig(),
132
+ imageDefaultWidth: null,
133
+ imageDefaultHeight: null,
106
134
  linkEditButtons: ["linkOpen", "editLinkGriddo", "linkRemove"],
107
135
  videoInsertButtons: ["videoBack", "|", "videoByURL", "videoEmbed"],
108
136
  imageAllowedTypes: ["jpeg", "jpg", "png", "svg"],
@@ -112,6 +140,7 @@ const getWysiwygConfig = () => ({
112
140
  paragraphStyles: getParagraphStyles(),
113
141
  tableStyles: getTableStyles(),
114
142
  tableCellStyles: getTableCellStyles(),
143
+ imageStyles: getImageStyles(),
115
144
  imageUploadRemoteUrls: false,
116
145
  });
117
146
 
@@ -161,6 +190,7 @@ FroalaEditor.RegisterCommand("insertImageGriddo", {
161
190
  undo: false,
162
191
  focus: false,
163
192
  callback: function (this: any) {
193
+ this.selection.save();
164
194
  const el = this.$el?.get?.(0) || this.el;
165
195
  el?.dispatchEvent(
166
196
  new CustomEvent("griddo:openGallery", {
@@ -177,6 +207,7 @@ FroalaEditor.RegisterCommand("insertFileGriddo", {
177
207
  undo: false,
178
208
  focus: false,
179
209
  callback: function (this: any) {
210
+ this.selection.save();
180
211
  const el = this.$el?.get?.(0) || this.el;
181
212
  el?.dispatchEvent(
182
213
  new CustomEvent("griddo:openFileGallery", {
@@ -296,7 +327,17 @@ FroalaEditor.RegisterCommand("imageLinkGriddo", {
296
327
  editorEl.dispatchEvent(
297
328
  new CustomEvent("griddo:openLinkPopover", {
298
329
  bubbles: true,
299
- detail: { x, y, selectedText: "", linkUrl, isNewTab, isNoFollow, isEditing: true, linkPageId, isImageLink: true },
330
+ detail: {
331
+ x,
332
+ y,
333
+ selectedText: "",
334
+ linkUrl,
335
+ isNewTab,
336
+ isNoFollow,
337
+ isEditing: true,
338
+ linkPageId,
339
+ isImageLink: true,
340
+ },
300
341
  }),
301
342
  );
302
343
  } else {
@@ -304,7 +345,17 @@ FroalaEditor.RegisterCommand("imageLinkGriddo", {
304
345
  editorEl.dispatchEvent(
305
346
  new CustomEvent("griddo:openLinkPopover", {
306
347
  bubbles: true,
307
- detail: { x, y, selectedText: "", linkUrl: "", isNewTab: false, isNoFollow: false, isEditing: false, linkPageId: null, isImageLink: true },
348
+ detail: {
349
+ x,
350
+ y,
351
+ selectedText: "",
352
+ linkUrl: "",
353
+ isNewTab: false,
354
+ isNoFollow: false,
355
+ isEditing: false,
356
+ linkPageId: null,
357
+ isImageLink: true,
358
+ },
308
359
  }),
309
360
  );
310
361
  }
@@ -17,6 +17,8 @@ interface IRichTextConfig {
17
17
  paragraphStyles?: { label: string; className: string }[];
18
18
  tableStyles?: { label: string; className: string }[];
19
19
  tableCellStyles?: { label: string; className: string }[];
20
+ imageStyles?: { label: string; className: string }[];
21
+ imageResize?: boolean;
20
22
  editorLangs?: { name: string; iso: string }[];
21
23
  }
22
24
 
@@ -84,10 +84,16 @@ const Wysiwyg = (props: IWysiwygProps) => {
84
84
 
85
85
  const handleFileSelected = (file: IFile) => {
86
86
  if (file?.url && editorRef.current) {
87
- editorRef.current.selection.restore();
88
- editorRef.current.link.insert(file.url, file.fileName, { target: "_blank" });
89
- const updatedHtml = editorRef.current.html.get();
90
- handleChange(updatedHtml);
87
+ // Wait for Froala to stabilize after re-render before restoring selection
88
+ setTimeout(() => {
89
+ if (!editorRef.current) return;
90
+ editorRef.current.selection.restore();
91
+ // Use html.insert instead of link.insert to avoid selection issues
92
+ const linkHtml = `<a href="${file.url}" target="_blank">${file.fileName}</a>`;
93
+ editorRef.current.html.insert(linkHtml);
94
+ const updatedHtml = editorRef.current.html.get();
95
+ handleChange(updatedHtml);
96
+ }, 100);
91
97
  }
92
98
  };
93
99
 
@@ -99,14 +105,23 @@ const Wysiwyg = (props: IWysiwygProps) => {
99
105
  if (img.alt) {
100
106
  imgElement.alt = img.alt;
101
107
  }
108
+ replaceImgRef.current = null;
109
+ const updatedHtml = editorRef.current.html.get();
110
+ handleChange(updatedHtml);
102
111
  } else {
103
- editorRef.current.selection.restore();
104
- editorRef.current.image.insert(img.url, true, { alt: img.alt || "" }, null, null);
112
+ // Wait for Froala to stabilize after re-render before restoring selection
113
+ setTimeout(() => {
114
+ if (!editorRef.current) return;
115
+ editorRef.current.selection.restore();
116
+ // Use html.insert instead of image.insert to avoid selection issues
117
+ const imgHtml = `<img src="${img.url}" alt="${img.alt || ""}" />`;
118
+ editorRef.current.html.insert(imgHtml);
119
+ const updatedHtml = editorRef.current.html.get();
120
+ handleChange(updatedHtml);
121
+ }, 100);
105
122
  }
106
123
 
107
124
  replaceImgRef.current = null;
108
- const updatedHtml = editorRef.current.html.get();
109
- handleChange(updatedHtml);
110
125
  }
111
126
  };
112
127
 
@@ -219,12 +234,10 @@ const Wysiwyg = (props: IWysiwygProps) => {
219
234
  editorEl.addEventListener("griddo:openGallery", (e: Event) => {
220
235
  const detail = (e as CustomEvent).detail;
221
236
  replaceImgRef.current = detail?.mode === "replace" ? detail.$img : null;
222
- editorRef.current?.selection.save();
223
237
  toggleModal("image");
224
238
  });
225
239
 
226
240
  editorEl.addEventListener("griddo:openFileGallery", () => {
227
- editorRef.current?.selection.save();
228
241
  toggleModal("file");
229
242
  });
230
243
 
@@ -278,6 +291,24 @@ const Wysiwyg = (props: IWysiwygProps) => {
278
291
  const stripedHtml = decodeEntities(html);
279
292
  handleValidation?.(stripedHtml);
280
293
  },
294
+ contentChanged: function (this: any) {
295
+ const editorEl = this.$el?.get?.(0) || this.el;
296
+ const images = editorEl?.querySelectorAll?.("img");
297
+ if (images) {
298
+ images.forEach((img: HTMLImageElement) => {
299
+ const hasFrDib = img.classList.contains("fr-dib");
300
+ const hasFrDii = img.classList.contains("fr-dii");
301
+
302
+ img.classList.remove("img-inline", "img-block");
303
+
304
+ if (hasFrDib) {
305
+ img.classList.add("img-block");
306
+ } else if (hasFrDii) {
307
+ img.classList.add("img-inline");
308
+ }
309
+ });
310
+ }
311
+ },
281
312
  },
282
313
  }),
283
314
  [placeholder, inline, full, disabled, token, toggleModal, handleValidation, imageSite, uploadImage],
@@ -27,8 +27,10 @@ const ResizePanel = (props: IResizePanelProps): JSX.Element => {
27
27
  };
28
28
 
29
29
  useEffect(() => {
30
+ let frame = 0;
31
+
30
32
  const updateWidth = () => {
31
- requestAnimationFrame(() => {
33
+ frame = requestAnimationFrame(() => {
32
34
  if (rightPanelRef.current) {
33
35
  setRwidth(rightPanelRef.current.offsetWidth);
34
36
  }
@@ -38,7 +40,10 @@ const ResizePanel = (props: IResizePanelProps): JSX.Element => {
38
40
  updateWidth();
39
41
 
40
42
  window.addEventListener("resize", updateWidth);
41
- return () => window.removeEventListener("resize", updateWidth);
43
+ return () => {
44
+ cancelAnimationFrame(frame);
45
+ window.removeEventListener("resize", updateWidth);
46
+ };
42
47
  }, []);
43
48
 
44
49
  useEffect(() => {