@griddo/ax 11.15.7 → 11.15.8-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 (88) hide show
  1. package/package.json +2 -2
  2. package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.atoms.test.tsx +409 -0
  3. package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.helpers.test.tsx +418 -0
  4. package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.test.tsx +776 -29
  5. package/src/__tests__/components/FieldsBehavior/FieldsBehavior.test.tsx +368 -11
  6. package/src/__tests__/modules/Users/UserList/UserList.test.tsx +6 -6
  7. package/src/components/Fields/IntegrationsField/IntegrationItem/atoms.tsx +14 -5
  8. package/src/components/Fields/IntegrationsField/IntegrationItem/index.tsx +1 -8
  9. package/src/components/Fields/UrlField/index.tsx +5 -4
  10. package/src/components/Fields/Wysiwyg/atoms.tsx +140 -0
  11. package/src/components/Fields/Wysiwyg/config.tsx +190 -9
  12. package/src/components/Fields/Wysiwyg/helpers.tsx +54 -1
  13. package/src/components/Fields/Wysiwyg/index.tsx +210 -61
  14. package/src/components/Fields/Wysiwyg/style.tsx +32 -1
  15. package/src/components/Fields/Wysiwyg/vendors.tsx +0 -1
  16. package/src/components/FieldsBehavior/index.tsx +66 -31
  17. package/src/components/Icon/components/Sync.js +16 -0
  18. package/src/components/Icon/svgs/Sync.svg +3 -0
  19. package/src/components/RestoreModal/index.tsx +1 -3
  20. package/src/components/ScheduleModal/index.tsx +3 -3
  21. package/src/components/TableList/index.tsx +5 -3
  22. package/src/containers/FileDrive/actions.tsx +5 -5
  23. package/src/containers/Forms/actions.tsx +40 -38
  24. package/src/containers/Gallery/actions.tsx +27 -27
  25. package/src/containers/Integrations/actions.tsx +2 -2
  26. package/src/containers/Navigation/Defaults/actions.tsx +1 -1
  27. package/src/containers/PageEditor/actions.tsx +3 -5
  28. package/src/containers/Redirects/actions.tsx +2 -2
  29. package/src/containers/Sites/actions.tsx +15 -8
  30. package/src/containers/StructuredData/actions.tsx +5 -5
  31. package/src/containers/Users/actions.tsx +2 -2
  32. package/src/modules/Categories/CategoriesList/CategoryItem/index.tsx +36 -43
  33. package/src/modules/Categories/CategoriesList/CategoryPanel/index.tsx +15 -11
  34. package/src/modules/Categories/CategoriesList/atoms.tsx +42 -12
  35. package/src/modules/Categories/CategoriesList/index.tsx +8 -30
  36. package/src/modules/Content/PageItem/index.tsx +6 -5
  37. package/src/modules/Content/atoms.tsx +7 -3
  38. package/src/modules/Content/index.tsx +8 -16
  39. package/src/modules/FileDrive/FileModal/DetailPanel/index.tsx +14 -14
  40. package/src/modules/FileDrive/FolderItem/index.tsx +46 -54
  41. package/src/modules/FileDrive/GridItem/index.tsx +32 -37
  42. package/src/modules/FileDrive/ListItem/index.tsx +31 -37
  43. package/src/modules/FileDrive/atoms.tsx +59 -12
  44. package/src/modules/FileDrive/index.tsx +88 -98
  45. package/src/modules/Forms/FormCategoriesList/CategoryItem/index.tsx +42 -34
  46. package/src/modules/Forms/FormCategoriesList/atoms.tsx +17 -6
  47. package/src/modules/Forms/FormCategoriesList/index.tsx +30 -34
  48. package/src/modules/Forms/FormEditor/index.tsx +14 -6
  49. package/src/modules/Forms/FormList/FormItem/index.tsx +5 -1
  50. package/src/modules/Forms/FormList/index.tsx +15 -9
  51. package/src/modules/Forms/atoms.tsx +7 -4
  52. package/src/modules/GlobalEditor/atoms.tsx +77 -0
  53. package/src/modules/GlobalEditor/index.tsx +24 -7
  54. package/src/modules/MediaGallery/FolderItem/index.tsx +36 -52
  55. package/src/modules/MediaGallery/GridItem/index.tsx +27 -38
  56. package/src/modules/MediaGallery/ImageModal/DetailPanel/index.tsx +13 -13
  57. package/src/modules/MediaGallery/ImageModal/index.tsx +2 -16
  58. package/src/modules/MediaGallery/ListItem/index.tsx +25 -37
  59. package/src/modules/MediaGallery/atoms.tsx +84 -15
  60. package/src/modules/MediaGallery/index.tsx +62 -122
  61. package/src/modules/Navigation/Defaults/Item/atoms.tsx +2 -4
  62. package/src/modules/Navigation/Defaults/Item/index.tsx +80 -94
  63. package/src/modules/Navigation/Defaults/atoms.tsx +179 -7
  64. package/src/modules/Navigation/Defaults/index.tsx +51 -18
  65. package/src/modules/Navigation/Defaults/style.tsx +10 -7
  66. package/src/modules/Navigation/Menus/List/Table/Item/atoms.tsx +33 -19
  67. package/src/modules/Navigation/Menus/List/Table/Item/index.tsx +18 -19
  68. package/src/modules/Navigation/Menus/List/Table/index.tsx +15 -13
  69. package/src/modules/Navigation/Menus/List/index.tsx +1 -1
  70. package/src/modules/PageEditor/atoms.tsx +14 -11
  71. package/src/modules/PageEditor/index.tsx +11 -16
  72. package/src/modules/Redirects/RedirectItem/index.tsx +37 -39
  73. package/src/modules/Redirects/atoms.tsx +58 -12
  74. package/src/modules/Redirects/index.tsx +30 -58
  75. package/src/modules/Settings/Integrations/IntegrationItem/CopyModal/index.tsx +1 -1
  76. package/src/modules/Settings/Integrations/IntegrationItem/index.tsx +57 -69
  77. package/src/modules/Settings/Integrations/atoms.tsx +72 -8
  78. package/src/modules/Settings/Integrations/index.tsx +34 -48
  79. package/src/modules/StructuredData/Form/index.tsx +25 -8
  80. package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/atoms.tsx +26 -75
  81. package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/index.tsx +15 -33
  82. package/src/modules/StructuredData/StructuredDataList/StructuredDataItem/index.tsx +63 -41
  83. package/src/modules/StructuredData/StructuredDataList/StructuredDataItem/style.tsx +13 -8
  84. package/src/modules/StructuredData/StructuredDataList/index.tsx +11 -16
  85. package/src/modules/StructuredData/{StructuredDataList/atoms.tsx → atoms.tsx} +25 -28
  86. package/src/modules/Users/UserList/UserItem/index.tsx +18 -35
  87. package/src/modules/Users/UserList/atoms.tsx +61 -0
  88. package/src/modules/Users/UserList/index.tsx +12 -32
@@ -49,8 +49,8 @@ const getButtonsFull = () => ({
49
49
  buttonsVisible: 6,
50
50
  },
51
51
  moreText2: {
52
- buttons: ["insertLink", "insertImage", "insertVideo", "clearFormatting", "fullscreen"],
53
- buttonsVisible: 5,
52
+ buttons: ["insertLinkGriddo", "insertImageGriddo", "insertFileGriddo", "insertVideo", "clearFormatting", "fullscreen"],
53
+ buttonsVisible: 6,
54
54
  },
55
55
  moreMisc: {
56
56
  buttons: getMiscButtons(),
@@ -66,7 +66,7 @@ const getButtons = () => {
66
66
  "bold",
67
67
  "italic",
68
68
  "formatUL",
69
- "insertLink",
69
+ "insertLinkGriddo",
70
70
  "paragraphFormat",
71
71
  paragraphStyles ? "paragraphStyle" : undefined,
72
72
  "langDropdown",
@@ -90,21 +90,20 @@ const getWysiwygConfig = () => ({
90
90
  H4: "Heading 4",
91
91
  },
92
92
  imageEditButtons: [
93
- "imageReplace",
93
+ "imageReplaceGriddo",
94
94
  "imageAlign",
95
95
  "imageCaption",
96
96
  "imageRemove",
97
97
  "|",
98
- "imageLink",
98
+ "imageLinkGriddo",
99
99
  "linkOpen",
100
- "linkEdit",
101
100
  "linkRemove",
102
101
  "-",
103
102
  "imageDisplay",
104
103
  "imageAlt",
105
104
  ],
106
105
  imageResize: false,
107
- linkEditButtons: ["linkOpen", "linkEdit", "linkRemove"],
106
+ linkEditButtons: ["linkOpen", "editLinkGriddo", "linkRemove"],
108
107
  videoInsertButtons: ["videoBack", "|", "videoByURL", "videoEmbed"],
109
108
  imageAllowedTypes: ["jpeg", "jpg", "png", "svg"],
110
109
  imageMaxSize: 20 * 1024 * 1024,
@@ -121,9 +120,18 @@ FroalaEditor.DefineIcon("langIcon", {
121
120
  template: "svg",
122
121
  });
123
122
 
123
+ FroalaEditor.DefineIcon("griddoImageIcon", { SVG_KEY: "insertImage" });
124
+ FroalaEditor.DefineIcon("griddoReplaceIcon", { SVG_KEY: "replaceImage" });
125
+ FroalaEditor.DefineIcon("griddoFileIcon", {
126
+ PATH: "M8 16H16V18H8V16ZM8 12H16V14H8V12ZM14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.89 22 5.99 22H18C19.1 22 20 21.1 20 20V8L14 2ZM18 20H6V4H13V9H18V20Z",
127
+ template: "svg",
128
+ });
129
+
130
+ FroalaEditor.DefineIcon("griddoLinkIcon", { SVG_KEY: "insertLink" });
131
+
124
132
  Object.assign(FroalaEditor.POPUP_TEMPLATES, {
125
- "image.uploading": '[_CUSTOM_LAYER_]',
126
- "image.uploadError": '[_CUSTOM_LAYER_]'
133
+ "image.uploading": "[_CUSTOM_LAYER_]",
134
+ "image.uploadError": "[_CUSTOM_LAYER_]",
127
135
  });
128
136
 
129
137
  FroalaEditor.RegisterCommand("langDropdown", {
@@ -147,6 +155,179 @@ FroalaEditor.RegisterCommand("langDropdown", {
147
155
  },
148
156
  });
149
157
 
158
+ FroalaEditor.RegisterCommand("insertImageGriddo", {
159
+ title: "Insert Image",
160
+ icon: "griddoImageIcon",
161
+ undo: false,
162
+ focus: false,
163
+ callback: function (this: any) {
164
+ const el = this.$el?.get?.(0) || this.el;
165
+ el?.dispatchEvent(
166
+ new CustomEvent("griddo:openGallery", {
167
+ bubbles: true,
168
+ detail: { mode: "insert" },
169
+ }),
170
+ );
171
+ },
172
+ });
173
+
174
+ FroalaEditor.RegisterCommand("insertFileGriddo", {
175
+ title: "Insert File",
176
+ icon: "griddoFileIcon",
177
+ undo: false,
178
+ focus: false,
179
+ callback: function (this: any) {
180
+ const el = this.$el?.get?.(0) || this.el;
181
+ el?.dispatchEvent(
182
+ new CustomEvent("griddo:openFileGallery", {
183
+ bubbles: true,
184
+ detail: { mode: "insert" },
185
+ }),
186
+ );
187
+ },
188
+ });
189
+
190
+ FroalaEditor.RegisterCommand("insertLinkGriddo", {
191
+ title: "Insert Link",
192
+ icon: "griddoLinkIcon",
193
+ undo: false,
194
+ focus: false,
195
+ callback: function (this: any) {
196
+ let x = window.innerWidth / 2 - 200;
197
+ let y = 100;
198
+
199
+ // Get the editor's wrapper and find the button within it
200
+ const editorEl = this.$el?.get?.(0) || this.el;
201
+ if (editorEl) {
202
+ // Find the parent wrapper of this editor
203
+ const wrapper = editorEl.closest(".fr-box") || editorEl.parentElement;
204
+ if (wrapper) {
205
+ // Find the insertLinkGriddo button within this wrapper's toolbar
206
+ const linkBtn = wrapper.querySelector('[data-cmd="insertLinkGriddo"]') as HTMLElement;
207
+ if (linkBtn) {
208
+ const rect = linkBtn.getBoundingClientRect();
209
+ x = rect.left;
210
+ y = rect.bottom + 10;
211
+ } else {
212
+ // Fallback: use editor position
213
+ const editorRect = editorEl.getBoundingClientRect?.();
214
+ if (editorRect) {
215
+ x = editorRect.left + editorRect.width / 2 - 200;
216
+ y = editorRect.top + editorRect.height + 10;
217
+ }
218
+ }
219
+ }
220
+
221
+ const selectedText = this.selection.text();
222
+ this.selection.save();
223
+ editorEl.dispatchEvent(
224
+ new CustomEvent("griddo:openLinkPopover", {
225
+ bubbles: true,
226
+ detail: { x, y, selectedText },
227
+ }),
228
+ );
229
+ }
230
+ },
231
+ });
232
+
233
+ FroalaEditor.RegisterCommand("editLinkGriddo", {
234
+ title: "Edit Link",
235
+ icon: "linkEdit",
236
+ undo: false,
237
+ focus: false,
238
+ callback: function (this: any) {
239
+ const linkElement = this.link.get();
240
+ if (!linkElement) return;
241
+
242
+ const linkRect = linkElement.getBoundingClientRect();
243
+ const x = linkRect.left;
244
+ const y = linkRect.bottom + 10;
245
+
246
+ const editorEl = this.$el?.get?.(0) || this.el;
247
+ if (editorEl) {
248
+ const linkUrl = linkElement.href || "";
249
+ const linkText = linkElement.textContent || "";
250
+ const isNewTab = linkElement.target === "_blank";
251
+ const isNoFollow = linkElement.rel?.includes("nofollow") || false;
252
+ const linkPageId = linkElement.getAttribute("data-pageId");
253
+
254
+ linkElement.setAttribute("data-griddo-editing", "true");
255
+ this.selection.save();
256
+ editorEl.dispatchEvent(
257
+ new CustomEvent("griddo:openLinkPopover", {
258
+ bubbles: true,
259
+ detail: { x, y, selectedText: linkText, linkUrl, isNewTab, isNoFollow, isEditing: true, linkPageId },
260
+ }),
261
+ );
262
+ }
263
+ },
264
+ });
265
+
266
+ FroalaEditor.RegisterCommand("imageLinkGriddo", {
267
+ title: "Link",
268
+ icon: "griddoLinkIcon",
269
+ undo: false,
270
+ focus: false,
271
+ callback: function (this: any) {
272
+ const jqElement = this.image.get();
273
+ if (!jqElement) return;
274
+
275
+ const imgElement = jqElement[0] || jqElement;
276
+ if (!imgElement || typeof imgElement.getBoundingClientRect !== "function") return;
277
+
278
+ const imgRect = imgElement.getBoundingClientRect();
279
+ const x = imgRect.left;
280
+ const y = imgRect.bottom + 10;
281
+
282
+ const editorEl = this.$el?.get?.(0) || this.el;
283
+ if (editorEl) {
284
+ const currentLink = imgElement.parentElement?.tagName === "A" ? imgElement.parentElement : null;
285
+ const isEditing = !!currentLink;
286
+
287
+ this.selection.save();
288
+
289
+ if (isEditing) {
290
+ const linkUrl = currentLink.href || "";
291
+ const isNewTab = currentLink.target === "_blank";
292
+ const isNoFollow = currentLink.rel?.includes("nofollow") || false;
293
+ const linkPageId = currentLink.getAttribute("data-pageId");
294
+
295
+ currentLink.setAttribute("data-griddo-editing-image-link", "true");
296
+ editorEl.dispatchEvent(
297
+ new CustomEvent("griddo:openLinkPopover", {
298
+ bubbles: true,
299
+ detail: { x, y, selectedText: "", linkUrl, isNewTab, isNoFollow, isEditing: true, linkPageId, isImageLink: true },
300
+ }),
301
+ );
302
+ } else {
303
+ imgElement.setAttribute("data-griddo-editing-image", "true");
304
+ editorEl.dispatchEvent(
305
+ new CustomEvent("griddo:openLinkPopover", {
306
+ bubbles: true,
307
+ detail: { x, y, selectedText: "", linkUrl: "", isNewTab: false, isNoFollow: false, isEditing: false, linkPageId: null, isImageLink: true },
308
+ }),
309
+ );
310
+ }
311
+ }
312
+ },
313
+ });
314
+
315
+ FroalaEditor.RegisterCommand("imageReplaceGriddo", {
316
+ title: "Replace Image",
317
+ icon: "griddoReplaceIcon",
318
+ undo: true,
319
+ focus: false,
320
+ callback: function (this: any) {
321
+ const el = this.$el?.get?.(0) || this.el;
322
+ el?.dispatchEvent(
323
+ new CustomEvent("griddo:openGallery", {
324
+ bubbles: true,
325
+ detail: { mode: "replace", $img: this.image.get() },
326
+ }),
327
+ );
328
+ },
329
+ });
330
+
150
331
  export {
151
332
  getButtonsFull as buttonsFull,
152
333
  getButtons as buttons,
@@ -85,4 +85,57 @@ const getLanguageMenuHtml = (languages: { name: string; iso: string; featured?:
85
85
  );
86
86
  };
87
87
 
88
- export { getRichTextConfig, parseClassNames, getLanguageMenuHtml, type IRichTextConfig };
88
+ const setLinkAttributes = (
89
+ element: HTMLElement | null,
90
+ url: string,
91
+ options?: { newTab?: boolean; noFollow?: boolean; linkTo?: number | null }
92
+ ) => {
93
+ if (!element) return;
94
+ element.setAttribute("href", url);
95
+ if (options?.newTab) {
96
+ element.setAttribute("target", "_blank");
97
+ } else {
98
+ element.removeAttribute("target");
99
+ }
100
+ if (options?.noFollow) {
101
+ element.setAttribute("rel", "nofollow");
102
+ } else {
103
+ element.removeAttribute("rel");
104
+ }
105
+ if (options?.linkTo) {
106
+ element.setAttribute("data-pageId", String(options.linkTo));
107
+ } else {
108
+ element.removeAttribute("data-pageId");
109
+ }
110
+ };
111
+
112
+ const getPopupPosition = (editorEl: HTMLElement) => {
113
+ const editorRect = editorEl.getBoundingClientRect?.();
114
+ return {
115
+ x: editorRect ? editorRect.left + editorRect.width / 2 - 100 : window.innerWidth / 2 - 100,
116
+ y: editorRect ? editorRect.top + 50 : 50,
117
+ };
118
+ };
119
+
120
+ const createPopupFunctions = (editor: any, editorEl: HTMLElement) => {
121
+ const showPopup = (name: string, template: any, keepVisible = false) => {
122
+ editor.popups.create(name, template);
123
+ if (!keepVisible) {
124
+ editor.popups.onHide(name, () => false);
125
+ }
126
+ const pos = getPopupPosition(editorEl);
127
+ editor.popups.show(name, pos.x, pos.y, 50);
128
+ };
129
+
130
+ const showErrorPopup = () => {
131
+ const errorPopupTemplate = {
132
+ custom_layer: '<div style="padding: 15px 30px; text-align: center; color: #d32f2f;">Upload failed</div>',
133
+ };
134
+ showPopup("image.uploadError", errorPopupTemplate);
135
+ setTimeout(() => editor.popups.hide("image.uploadError"), 3000);
136
+ };
137
+
138
+ return { showPopup, showErrorPopup };
139
+ };
140
+
141
+ export { getRichTextConfig, parseClassNames, getLanguageMenuHtml, setLinkAttributes, getPopupPosition, createPopupFunctions, type IRichTextConfig };
@@ -1,16 +1,21 @@
1
- import { useEffect, useRef } from "react";
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import FroalaEditorComponent from "react-froala-wysiwyg";
3
3
  import { connect } from "react-redux";
4
4
 
5
+ import { FileGallery, Gallery, Modal } from "@ax/components";
5
6
  import { galleryActions } from "@ax/containers/Gallery";
6
7
  import { decodeEntities } from "@ax/helpers";
7
- import type { IImage, IRootState, ISite } from "@ax/types";
8
+ import type { IFile, IImage, IRootState, ISite } from "@ax/types";
8
9
 
9
10
  import FroalaEditor from "froala-editor";
10
11
 
12
+ import { LinkPopover } from "./atoms";
11
13
  import { buttons, buttonsFull, inlineToolbar, wysiwygConfig } from "./config";
14
+ import { createPopupFunctions, getPopupPosition, setLinkAttributes } from "./helpers";
12
15
  import "./vendors";
13
16
 
17
+ import { useModals } from "@ax/hooks";
18
+
14
19
  import * as S from "./style";
15
20
 
16
21
  const Wysiwyg = (props: IWysiwygProps) => {
@@ -33,12 +38,40 @@ const Wysiwyg = (props: IWysiwygProps) => {
33
38
  const errorRef = useRef(error);
34
39
  errorRef.current = error;
35
40
 
41
+ const { isOpen, toggleModal } = useModals(["image", "file"]);
42
+ const [linkPopoverOpen, setLinkPopoverOpen] = useState(false);
43
+ const [linkPopoverPos, setLinkPopoverPos] = useState({ x: 0, y: 0 });
44
+ const [selectedText, setSelectedText] = useState("");
45
+ const [linkData, setLinkData] = useState<{
46
+ url: string;
47
+ newTab: boolean;
48
+ noFollow: boolean;
49
+ isEditing: boolean;
50
+ linkPageId?: number | null;
51
+ isImageLink?: boolean;
52
+ } | null>(null);
53
+ const replaceImgRef = useRef<any>(null);
54
+ const lastValueRef = useRef(value);
55
+
36
56
  useEffect(() => {
37
57
  if (!editorRef.current) return;
38
58
  disabled ? editorRef.current.edit.off() : editorRef.current.edit.on();
39
59
  }, [disabled]);
40
60
 
61
+ useEffect(() => {
62
+ lastValueRef.current = value;
63
+ }, [value]);
64
+
41
65
  const handleChange = (model: string) => {
66
+ // Ignore if only difference is Froala's automatic empty paragraph
67
+ const cleanCurrent = model.replace(/<p><br><\/p>$/, "").trim();
68
+ const cleanLast = (lastValueRef.current || "").replace(/<p><br><\/p>$/, "").trim();
69
+
70
+ if (cleanCurrent === cleanLast) {
71
+ return;
72
+ }
73
+
74
+ lastValueRef.current = model;
42
75
  onChange(model);
43
76
  const stripedHtml = decodeEntities(model);
44
77
  setTimeout(() => {
@@ -46,53 +79,150 @@ const Wysiwyg = (props: IWysiwygProps) => {
46
79
  }, 300);
47
80
  };
48
81
 
49
- const dynamicConfig = {
50
- placeholderText: placeholder,
51
- toolbarButtons: inline ? inlineToolbar() : full ? buttonsFull() : buttons(),
52
- toolbarInline: inline,
53
- charCounterCount: !inline,
54
- imageUpload: false,
55
- multiLine: !inline,
56
- enter: inline ? FroalaEditor.ENTER_BR : FroalaEditor.ENTER_P,
57
- requestHeaders: {
58
- Authorization: `bearer ${token}`,
59
- },
60
- events: {
61
- initialized(this: any) {
62
- editorRef.current = this;
63
-
64
- if (disabled) {
65
- this.edit.off();
82
+ const handleFileSelected = (file: IFile) => {
83
+ if (file?.url && editorRef.current) {
84
+ editorRef.current.selection.restore();
85
+ editorRef.current.link.insert(file.url, file.fileName, { target: "_blank" });
86
+ const updatedHtml = editorRef.current.html.get();
87
+ handleChange(updatedHtml);
88
+ }
89
+ };
90
+
91
+ const handleImageSelected = (img: IImage | null) => {
92
+ if (img?.url && editorRef.current) {
93
+ if (replaceImgRef.current && replaceImgRef.current.length > 0) {
94
+ const imgElement = replaceImgRef.current[0];
95
+ imgElement.src = img.url;
96
+ if (img.alt) {
97
+ imgElement.alt = img.alt;
66
98
  }
99
+ } else {
100
+ editorRef.current.selection.restore();
101
+ editorRef.current.image.insert(img.url, true, { alt: img.alt || "" }, null, null);
102
+ }
103
+
104
+ replaceImgRef.current = null;
105
+ const updatedHtml = editorRef.current.html.get();
106
+ handleChange(updatedHtml);
107
+ }
108
+ };
109
+
110
+ const handleInsertLink = (
111
+ url: string,
112
+ text: string,
113
+ options?: { newTab: boolean; noFollow: boolean; linkTo?: number | null },
114
+ ) => {
115
+ if (url && editorRef.current) {
116
+ const linkOptions: any = {};
117
+ if (options?.newTab) {
118
+ linkOptions.target = "_blank";
119
+ }
120
+ if (options?.noFollow) {
121
+ linkOptions.rel = "nofollow";
122
+ }
123
+ if (options?.linkTo) {
124
+ linkOptions["data-pageId"] = options.linkTo;
125
+ }
126
+
127
+ if (linkData?.isImageLink) {
128
+ const editingImageLink = editorRef.current.$el
129
+ ?.get?.(0)
130
+ ?.querySelector('[data-griddo-editing-image-link="true"]');
131
+ const editingImage = editorRef.current.$el?.get?.(0)?.querySelector('[data-griddo-editing-image="true"]');
132
+
133
+ if (editingImageLink) {
134
+ setLinkAttributes(editingImageLink, url, options);
135
+ editingImageLink.removeAttribute("data-griddo-editing-image-link");
136
+ } else if (editingImage) {
137
+ let linkElement = editingImage.parentElement?.tagName === "A" ? editingImage.parentElement : null;
138
+
139
+ if (!linkElement) {
140
+ linkElement = document.createElement("a");
141
+ editingImage.parentElement?.insertBefore(linkElement, editingImage);
142
+ linkElement.appendChild(editingImage);
143
+ }
144
+
145
+ setLinkAttributes(linkElement, url, options);
146
+ editingImage.removeAttribute("data-griddo-editing-image");
147
+ }
148
+ } else if (linkData?.isEditing) {
149
+ const editingLink = editorRef.current.$el?.get?.(0)?.querySelector('[data-griddo-editing="true"]');
150
+ if (editingLink) {
151
+ setLinkAttributes(editingLink, url, options);
152
+ editingLink.textContent = text || url;
153
+ editingLink.removeAttribute("data-griddo-editing");
154
+ }
155
+ } else {
156
+ editorRef.current.selection.restore();
157
+ editorRef.current.link.insert(url, text || url, linkOptions);
158
+ }
159
+ setLinkPopoverOpen(false);
160
+ const updatedHtml = editorRef.current.html.get();
161
+ handleChange(updatedHtml);
162
+ }
163
+ };
164
+
165
+ const handleCloseLinkPopover = () => {
166
+ setLinkPopoverOpen(false);
167
+ setLinkData(null);
168
+ };
169
+
170
+ const dynamicConfig = useMemo(
171
+ () => ({
172
+ placeholderText: placeholder,
173
+ toolbarButtons: inline ? inlineToolbar() : full ? buttonsFull() : buttons(),
174
+ toolbarInline: inline,
175
+ charCounterCount: !inline,
176
+ imageUpload: false,
177
+ multiLine: !inline,
178
+ enter: inline ? FroalaEditor.ENTER_BR : FroalaEditor.ENTER_P,
179
+ requestHeaders: {
180
+ Authorization: `bearer ${token}`,
181
+ },
182
+ events: {
183
+ initialized(this: any) {
184
+ editorRef.current = this;
185
+
186
+ if (disabled) {
187
+ this.edit.off();
188
+ }
67
189
 
68
- if (full && !inline) {
69
190
  const editorEl = this.$el?.get?.(0) || this.el;
70
- if (editorEl) {
71
- const getPopupPosition = () => {
72
- const editorRect = editorEl.getBoundingClientRect?.();
73
- return {
74
- x: editorRect ? editorRect.left + editorRect.width / 2 - 100 : window.innerWidth / 2 - 100,
75
- y: editorRect ? editorRect.top + 50 : 50,
76
- };
77
- };
78
-
79
- const showPopup = (name: string, template: any, keepVisible = false) => {
80
- this.popups.create(name, template);
81
- if (!keepVisible) {
82
- this.popups.onHide(name, () => false);
83
- }
84
- const pos = getPopupPosition();
85
- this.popups.show(name, pos.x, pos.y, 50);
86
- };
87
-
88
- const showErrorPopup = () => {
89
- const errorPopupTemplate = {
90
- custom_layer:
91
- '<div style="padding: 15px 30px; text-align: center; color: #d32f2f;">Upload failed</div>',
92
- };
93
- showPopup("image.uploadError", errorPopupTemplate);
94
- setTimeout(() => this.popups.hide("image.uploadError"), 3000);
95
- };
191
+ if (!editorEl) return;
192
+
193
+ editorEl.addEventListener("griddo:openLinkPopover", (e: Event) => {
194
+ const detail = (e as CustomEvent).detail;
195
+ setLinkPopoverOpen(true);
196
+ setLinkPopoverPos({ x: detail.x, y: detail.y });
197
+ setSelectedText(detail.selectedText || "");
198
+ setLinkData(
199
+ detail.isEditing || detail.isImageLink
200
+ ? {
201
+ url: detail.linkUrl || "",
202
+ newTab: detail.isNewTab || false,
203
+ noFollow: detail.isNoFollow || false,
204
+ isEditing: detail.isEditing || false,
205
+ linkPageId: detail.linkPageId || null,
206
+ isImageLink: detail.isImageLink || false,
207
+ }
208
+ : null,
209
+ );
210
+ });
211
+
212
+ if (full && !inline) {
213
+ const { showErrorPopup } = createPopupFunctions(this, editorEl);
214
+
215
+ editorEl.addEventListener("griddo:openGallery", (e: Event) => {
216
+ const detail = (e as CustomEvent).detail;
217
+ replaceImgRef.current = detail?.mode === "replace" ? detail.$img : null;
218
+ editorRef.current?.selection.save();
219
+ toggleModal("image");
220
+ });
221
+
222
+ editorEl.addEventListener("griddo:openFileGallery", () => {
223
+ editorRef.current?.selection.save();
224
+ toggleModal("file");
225
+ });
96
226
 
97
227
  editorEl.addEventListener("drop", async (e: DragEvent) => {
98
228
  e.preventDefault();
@@ -110,13 +240,13 @@ const Wysiwyg = (props: IWysiwygProps) => {
110
240
  if (isUploading) {
111
241
  setTimeout(() => {
112
242
  if (isUploading) {
113
- const pos = getPopupPosition();
243
+ const pos = getPopupPosition(editorEl);
114
244
  this.popups.show("image.uploading", pos.x, pos.y, 50);
115
245
  }
116
246
  }, 0);
117
247
  }
118
248
  });
119
- const pos = getPopupPosition();
249
+ const pos = getPopupPosition(editorEl);
120
250
  this.popups.show("image.uploading", pos.x, pos.y, 50);
121
251
 
122
252
  try {
@@ -138,22 +268,41 @@ const Wysiwyg = (props: IWysiwygProps) => {
138
268
  }
139
269
  });
140
270
  }
141
- }
142
- },
143
- blur: function (this: any) {
144
- const html = this.html.get();
145
- const stripedHtml = decodeEntities(html);
146
- handleValidation?.(stripedHtml);
271
+ },
272
+ blur: function (this: any) {
273
+ const html = this.html.get();
274
+ const stripedHtml = decodeEntities(html);
275
+ handleValidation?.(stripedHtml);
276
+ },
147
277
  },
148
- },
149
- };
278
+ }),
279
+ [placeholder, inline, full, disabled, token, toggleModal, handleValidation, imageSite, uploadImage],
280
+ );
150
281
 
151
- const config = { ...wysiwygConfig(), ...dynamicConfig };
282
+ const config = useMemo(() => ({ ...wysiwygConfig(), ...dynamicConfig }), [dynamicConfig]);
152
283
 
153
284
  return (
154
- <S.EditorWrapper error={error} disabled={disabled} data-testid="wysiwyg-wrapper">
155
- <FroalaEditorComponent tag="textarea" model={value} config={config} onModelChange={handleChange} />
156
- </S.EditorWrapper>
285
+ <>
286
+ <S.EditorWrapper error={error} disabled={disabled} data-testid="wysiwyg-wrapper">
287
+ <FroalaEditorComponent tag="textarea" model={value} config={config} onModelChange={handleChange} />
288
+ </S.EditorWrapper>
289
+ <Modal isOpen={isOpen("image")} hide={() => toggleModal("image")} size="XL" title="Select image">
290
+ <Gallery getImageSelected={handleImageSelected} toggleModal={() => toggleModal("image")} site={site} />
291
+ </Modal>
292
+ <Modal isOpen={isOpen("file")} hide={() => toggleModal("file")} size="XL" title="Select file">
293
+ <FileGallery addFile={handleFileSelected} toggleModal={() => toggleModal("file")} site={site} />
294
+ </Modal>
295
+ {linkPopoverOpen && (
296
+ <LinkPopover
297
+ x={linkPopoverPos.x}
298
+ y={linkPopoverPos.y}
299
+ selectedText={selectedText}
300
+ linkData={linkData}
301
+ onInsert={handleInsertLink}
302
+ onClose={handleCloseLinkPopover}
303
+ />
304
+ )}
305
+ </>
157
306
  );
158
307
  };
159
308
 
@@ -166,7 +315,7 @@ interface IProps {
166
315
  onChange: (value: string) => void;
167
316
  disabled?: boolean;
168
317
  handleValidation?: (value: string) => void;
169
- site: ISite;
318
+ site: ISite | null;
170
319
  inline?: boolean;
171
320
  }
172
321
 
@@ -1,6 +1,37 @@
1
- import styled from "styled-components";
2
1
  import { Wrapper } from "@ax/components/FieldsBehavior/style";
3
2
 
3
+ import styled from "styled-components";
4
+
5
+ export const PopoverOverlay = styled.div`
6
+ position: fixed;
7
+ top: 0;
8
+ left: 0;
9
+ width: 100%;
10
+ height: 100%;
11
+ z-index: 999;
12
+ `;
13
+
14
+ export const PopoverContainer = styled.div<{ x: number; y: number }>`
15
+ position: fixed;
16
+ top: ${(p) => p.y}px;
17
+ left: ${(p) => p.x}px;
18
+ z-index: 1000;
19
+ background: ${(p) => p.theme.color.uiBackground02};
20
+ border-radius: 4px;
21
+ box-shadow: ${(p) => p.theme.shadow.shadowL};
22
+ padding: ${(p) => p.theme.spacing.s};
23
+ width: 320px;
24
+ max-height: 80vh;
25
+ overflow-y: auto;
26
+ `;
27
+
28
+ export const PopoverButtonWrapper = styled.div`
29
+ display: flex;
30
+ gap: ${(p) => p.theme.spacing.s};
31
+ justify-content: flex-end;
32
+ margin-top: ${(p) => p.theme.spacing.m};
33
+ `;
34
+
4
35
  export const EditorWrapper = styled.div<{ error: boolean | undefined; disabled?: boolean }>`
5
36
  margin-bottom: ${(p) => p.theme.spacing.xxs};
6
37
  .fr-box.fr-basic {
@@ -8,7 +8,6 @@ import "froala-editor/js/plugins/lists.min.js";
8
8
  import "froala-editor/js/plugins/code_view.min.js";
9
9
  import "froala-editor/js/plugins/code_beautifier.min.js";
10
10
  import "froala-editor/js/plugins/image.min.js";
11
- import "froala-editor/js/plugins/image_manager.min.js";
12
11
  import "froala-editor/js/plugins/link.min.js";
13
12
  import "froala-editor/js/plugins/video.min.js";
14
13
  import "froala-editor/js/plugins/quote.min.js";