@griddo/ax 11.12.0 → 11.12.1-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.
- package/config/jest/componentsMock.js +7 -5
- package/package.json +2 -2
- package/src/__tests__/components/Browser/Browser.test.tsx +438 -87
- package/src/__tests__/components/Browser/Browser.utils.test.ts +55 -0
- package/src/__tests__/components/ConfigPanel/ConfigPanel.test.tsx +1 -3
- package/src/__tests__/components/Fields/Button/Button.test.tsx +29 -27
- package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/ErrorItem.test.tsx +158 -0
- package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorsBanner.test.tsx +90 -0
- package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.test.tsx +178 -0
- package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.utils.test.tsx +150 -0
- package/src/__tests__/components/KeywordsPreviewModal/KeywordItem/KeywordItem.test.tsx +91 -0
- package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.test.tsx +122 -0
- package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.utils.test.ts +15 -0
- package/src/__tests__/components/KeywordsPreviewModal/atoms.test.tsx +101 -0
- package/src/__tests__/components/ResizePanel/ResizePanel.test.tsx +1 -1
- package/src/__tests__/modules/FramePreview/FramePreview.test.tsx +318 -0
- package/src/__tests__/modules/FramePreview/FramePreview.utils.test.ts +242 -0
- package/src/__tests__/modules/FramePreview/HeadingsOverlay/HeadingsOverlay.test.tsx +185 -0
- package/src/components/Browser/index.tsx +294 -149
- package/src/components/Browser/style.tsx +75 -6
- package/src/components/Browser/utils.tsx +13 -0
- package/src/components/Button/index.tsx +2 -1
- package/src/components/ConfigPanel/Form/ConnectedField/PageConnectedField/Field/index.tsx +2 -4
- package/src/components/Fields/AsyncSelect/style.tsx +13 -0
- package/src/components/Fields/FieldGroup/index.tsx +5 -2
- package/src/components/Fields/FieldGroup/style.tsx +32 -7
- package/src/components/Fields/HeadingField/index.tsx +22 -22
- package/src/components/Fields/HiddenField/style.tsx +1 -1
- package/src/components/Fields/NumberField/index.tsx +15 -16
- package/src/components/Fields/NumberField/style.tsx +2 -0
- package/src/components/Fields/ReferenceField/index.tsx +1 -1
- package/src/components/Fields/SEOPreview/index.tsx +36 -0
- package/src/components/Fields/SEOPreview/style.tsx +24 -0
- package/src/components/Fields/Select/index.tsx +5 -1
- package/src/components/Fields/Select/style.tsx +56 -0
- package/src/components/Fields/SummaryButton/index.tsx +18 -9
- package/src/components/Fields/SummaryButton/style.tsx +1 -2
- package/src/components/Fields/TagsField/index.tsx +8 -9
- package/src/components/Fields/UrlField/index.tsx +26 -27
- package/src/components/Fields/index.tsx +2 -0
- package/src/components/FloatingNote/index.tsx +35 -0
- package/src/components/FloatingNote/style.tsx +26 -0
- package/src/components/FloatingPanel/index.tsx +5 -2
- package/src/components/FloatingPanel/style.tsx +2 -1
- package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/index.tsx +85 -0
- package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/style.tsx +80 -0
- package/src/components/HeadingsPreviewModal/ErrorsBanner/index.tsx +57 -0
- package/src/components/HeadingsPreviewModal/ErrorsBanner/style.tsx +82 -0
- package/src/components/HeadingsPreviewModal/HeadingItem/index.tsx +71 -0
- package/src/components/HeadingsPreviewModal/HeadingItem/style.tsx +77 -0
- package/src/components/HeadingsPreviewModal/index.tsx +148 -0
- package/src/components/HeadingsPreviewModal/style.tsx +82 -0
- package/src/components/HeadingsPreviewModal/utils.tsx +329 -0
- package/src/components/Icon/index.tsx +1 -2
- package/src/components/IconAction/index.tsx +1 -1
- package/src/components/KeywordsPreviewModal/KeywordItem/index.tsx +46 -0
- package/src/components/KeywordsPreviewModal/KeywordItem/style.tsx +64 -0
- package/src/components/KeywordsPreviewModal/atoms.tsx +96 -0
- package/src/components/KeywordsPreviewModal/index.tsx +99 -0
- package/src/components/KeywordsPreviewModal/style.tsx +87 -0
- package/src/components/KeywordsPreviewModal/utils.tsx +22 -0
- package/src/components/MainWrapper/AppBar/index.tsx +8 -1
- package/src/components/MainWrapper/index.tsx +7 -1
- package/src/components/Notification/index.tsx +2 -2
- package/src/components/PageFinder/index.tsx +1 -1
- package/src/components/ResizePanel/index.tsx +4 -3
- package/src/components/ResizePanel/style.tsx +1 -1
- package/src/components/SearchField/style.tsx +2 -2
- package/src/components/SideModal/index.tsx +2 -1
- package/src/components/Tabs/index.tsx +13 -4
- package/src/components/Tabs/style.tsx +7 -8
- package/src/components/Toast/index.tsx +4 -2
- package/src/components/Tooltip/index.tsx +4 -3
- package/src/components/index.tsx +8 -0
- package/src/forms/fields.tsx +70 -68
- package/src/hooks/forms.tsx +22 -1
- package/src/hooks/index.tsx +13 -3
- package/src/hooks/modals.tsx +103 -15
- package/src/hooks/users.tsx +25 -8
- package/src/modules/Forms/atoms.tsx +2 -2
- package/src/modules/FramePreview/HeadingsOverlay/index.tsx +116 -0
- package/src/modules/FramePreview/HeadingsOverlay/style.tsx +34 -0
- package/src/modules/FramePreview/index.tsx +55 -16
- package/src/modules/FramePreview/style.tsx +34 -2
- package/src/modules/FramePreview/utils.tsx +140 -0
- package/src/modules/GlobalEditor/Editor/index.tsx +37 -3
- package/src/modules/GlobalEditor/PageBrowser/index.tsx +19 -2
- package/src/modules/GlobalEditor/Preview/index.tsx +0 -2
- package/src/modules/GlobalEditor/Preview/style.tsx +1 -1
- package/src/modules/GlobalEditor/index.tsx +119 -57
- package/src/modules/PageEditor/Editor/index.tsx +33 -2
- package/src/modules/PageEditor/PageBrowser/index.tsx +20 -2
- package/src/modules/PageEditor/Preview/index.tsx +0 -2
- package/src/modules/PageEditor/Preview/style.tsx +1 -1
- package/src/modules/PageEditor/atoms.tsx +1 -1
- package/src/modules/PageEditor/index.tsx +130 -66
- package/src/modules/PublicPreview/index.tsx +5 -2
- package/src/schemas/pages/GlobalPage.ts +87 -70
- package/src/schemas/pages/Page.ts +87 -70
- package/src/types/index.tsx +12 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { ThemeProvider } from "styled-components";
|
|
2
|
+
import { render, cleanup, screen, act } from "@testing-library/react";
|
|
3
|
+
import { parseTheme } from "@ax/helpers";
|
|
4
|
+
import "@testing-library/jest-dom";
|
|
5
|
+
|
|
6
|
+
import HeadingsOverlay from "@ax/modules/FramePreview/HeadingsOverlay";
|
|
7
|
+
import globalTheme from "@ax/themes/theme.json";
|
|
8
|
+
|
|
9
|
+
const renderWithTheme = (ui: React.ReactElement) =>
|
|
10
|
+
render(<ThemeProvider theme={parseTheme(globalTheme)}>{ui}</ThemeProvider>);
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => {
|
|
14
|
+
cb(0);
|
|
15
|
+
return 0;
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
cleanup();
|
|
21
|
+
for (const el of document.querySelectorAll("h1, h2, h3, h4, h5, h6")) el.remove();
|
|
22
|
+
jest.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("HeadingsOverlay rendering", () => {
|
|
26
|
+
it("should render without boxes when no headings exist in the document", () => {
|
|
27
|
+
renderWithTheme(<HeadingsOverlay headingFilter={null} />);
|
|
28
|
+
|
|
29
|
+
expect(screen.queryByText("H1")).toBeNull();
|
|
30
|
+
expect(screen.queryByText("H2")).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should render a labeled box for an h1 element with a non-zero bounding rect", async () => {
|
|
34
|
+
const h1 = document.createElement("h1");
|
|
35
|
+
h1.dataset.griddoid = "heading-1";
|
|
36
|
+
document.body.appendChild(h1);
|
|
37
|
+
jest.spyOn(h1, "getBoundingClientRect").mockReturnValue({
|
|
38
|
+
top: 10,
|
|
39
|
+
left: 10,
|
|
40
|
+
width: 200,
|
|
41
|
+
height: 40,
|
|
42
|
+
bottom: 50,
|
|
43
|
+
right: 210,
|
|
44
|
+
x: 10,
|
|
45
|
+
y: 10,
|
|
46
|
+
toJSON: () => {},
|
|
47
|
+
} as DOMRect);
|
|
48
|
+
|
|
49
|
+
await act(async () => {
|
|
50
|
+
renderWithTheme(<HeadingsOverlay headingFilter={null} />);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(screen.getByText("H1")).toBeTruthy();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should render an uppercase label for the heading tag", async () => {
|
|
57
|
+
const h2 = document.createElement("h2");
|
|
58
|
+
h2.dataset.griddoid = "heading-1";
|
|
59
|
+
document.body.appendChild(h2);
|
|
60
|
+
jest.spyOn(h2, "getBoundingClientRect").mockReturnValue({
|
|
61
|
+
top: 10,
|
|
62
|
+
left: 10,
|
|
63
|
+
width: 200,
|
|
64
|
+
height: 40,
|
|
65
|
+
bottom: 50,
|
|
66
|
+
right: 210,
|
|
67
|
+
x: 10,
|
|
68
|
+
y: 10,
|
|
69
|
+
toJSON: () => {},
|
|
70
|
+
} as DOMRect);
|
|
71
|
+
|
|
72
|
+
await act(async () => {
|
|
73
|
+
renderWithTheme(<HeadingsOverlay headingFilter={null} />);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(screen.getByText("H2")).toBeTruthy();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should not render boxes for headings with zero bounding rect", () => {
|
|
80
|
+
const h1 = document.createElement("h1");
|
|
81
|
+
document.body.appendChild(h1);
|
|
82
|
+
// JSDOM returns zero rect by default — no mock needed
|
|
83
|
+
|
|
84
|
+
renderWithTheme(<HeadingsOverlay headingFilter={null} />);
|
|
85
|
+
|
|
86
|
+
expect(screen.queryByText("H1")).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should not render a box for a heading with display:none", async () => {
|
|
90
|
+
const h1 = document.createElement("h1");
|
|
91
|
+
h1.dataset.griddoid = "heading-1";
|
|
92
|
+
document.body.appendChild(h1);
|
|
93
|
+
jest.spyOn(h1, "getBoundingClientRect").mockReturnValue({
|
|
94
|
+
top: 10,
|
|
95
|
+
left: 10,
|
|
96
|
+
width: 200,
|
|
97
|
+
height: 40,
|
|
98
|
+
bottom: 50,
|
|
99
|
+
right: 210,
|
|
100
|
+
x: 10,
|
|
101
|
+
y: 10,
|
|
102
|
+
toJSON: () => {},
|
|
103
|
+
} as DOMRect);
|
|
104
|
+
|
|
105
|
+
const originalGetComputedStyle = window.getComputedStyle.bind(window);
|
|
106
|
+
jest.spyOn(window, "getComputedStyle").mockImplementation((el) => {
|
|
107
|
+
if (el === h1) return { display: "none", visibility: "", opacity: "1" } as CSSStyleDeclaration;
|
|
108
|
+
return originalGetComputedStyle(el);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await act(async () => {
|
|
112
|
+
renderWithTheme(<HeadingsOverlay headingFilter={null} />);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(screen.queryByText("H1")).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should not render a box for a heading with opacity 0", async () => {
|
|
119
|
+
const h1 = document.createElement("h1");
|
|
120
|
+
h1.dataset.griddoid = "heading-1";
|
|
121
|
+
document.body.appendChild(h1);
|
|
122
|
+
jest.spyOn(h1, "getBoundingClientRect").mockReturnValue({
|
|
123
|
+
top: 10,
|
|
124
|
+
left: 10,
|
|
125
|
+
width: 200,
|
|
126
|
+
height: 40,
|
|
127
|
+
bottom: 50,
|
|
128
|
+
right: 210,
|
|
129
|
+
x: 10,
|
|
130
|
+
y: 10,
|
|
131
|
+
toJSON: () => {},
|
|
132
|
+
} as DOMRect);
|
|
133
|
+
|
|
134
|
+
const originalGetComputedStyle = window.getComputedStyle.bind(window);
|
|
135
|
+
jest.spyOn(window, "getComputedStyle").mockImplementation((el) => {
|
|
136
|
+
if (el === h1) return { display: "block", visibility: "visible", opacity: "0" } as CSSStyleDeclaration;
|
|
137
|
+
return originalGetComputedStyle(el);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await act(async () => {
|
|
141
|
+
renderWithTheme(<HeadingsOverlay headingFilter={null} />);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(screen.queryByText("H1")).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should only render boxes for headings matching the headingFilter", async () => {
|
|
148
|
+
const h1 = document.createElement("h1");
|
|
149
|
+
h1.dataset.griddoid = "heading-1";
|
|
150
|
+
document.body.appendChild(h1);
|
|
151
|
+
jest.spyOn(h1, "getBoundingClientRect").mockReturnValue({
|
|
152
|
+
top: 10,
|
|
153
|
+
left: 10,
|
|
154
|
+
width: 200,
|
|
155
|
+
height: 40,
|
|
156
|
+
bottom: 50,
|
|
157
|
+
right: 210,
|
|
158
|
+
x: 10,
|
|
159
|
+
y: 10,
|
|
160
|
+
toJSON: () => {},
|
|
161
|
+
} as DOMRect);
|
|
162
|
+
|
|
163
|
+
const h2 = document.createElement("h2");
|
|
164
|
+
h2.dataset.griddoid = "heading-2";
|
|
165
|
+
document.body.appendChild(h2);
|
|
166
|
+
jest.spyOn(h2, "getBoundingClientRect").mockReturnValue({
|
|
167
|
+
top: 60,
|
|
168
|
+
left: 10,
|
|
169
|
+
width: 200,
|
|
170
|
+
height: 40,
|
|
171
|
+
bottom: 100,
|
|
172
|
+
right: 210,
|
|
173
|
+
x: 10,
|
|
174
|
+
y: 60,
|
|
175
|
+
toJSON: () => {},
|
|
176
|
+
} as DOMRect);
|
|
177
|
+
|
|
178
|
+
await act(async () => {
|
|
179
|
+
renderWithTheme(<HeadingsOverlay headingFilter="h1" />);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(screen.getByText("H1")).toBeTruthy();
|
|
183
|
+
expect(screen.queryByText("H2")).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -1,20 +1,36 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import type { IShareData } from "@ax/api";
|
|
4
4
|
import { shareToken as shareTokenApi } from "@ax/api";
|
|
5
|
-
import { OcassionalToast } from "@ax/components";
|
|
5
|
+
import { BrowserContent, Icon, OcassionalToast, Select, SharePageModal, Toast, Tooltip } from "@ax/components";
|
|
6
6
|
import { findByEditorID } from "@ax/forms";
|
|
7
7
|
import { DEV_NOW, getShareTokenInfo } from "@ax/helpers";
|
|
8
8
|
import { useModal, useOnMessageReceivedFromIframe, useToast } from "@ax/hooks";
|
|
9
|
+
import type { HeadingFilter } from "@ax/types";
|
|
9
10
|
|
|
10
|
-
import
|
|
11
|
-
import Icon from "../Icon";
|
|
12
|
-
import SharePageModal from "../SharePageModal";
|
|
13
|
-
import Toast from "../Toast";
|
|
14
|
-
import Tooltip from "../Tooltip";
|
|
11
|
+
import { calcAutoZoom, calcDefaultResolution } from "./utils";
|
|
15
12
|
|
|
16
13
|
import * as S from "./style";
|
|
17
14
|
|
|
15
|
+
const DEFAULT_RESOLUTION = "1280px";
|
|
16
|
+
|
|
17
|
+
const resolutionOptions = [
|
|
18
|
+
{ value: "1920px", label: "1920px" },
|
|
19
|
+
{ value: "1440px", label: "1440px" },
|
|
20
|
+
{ value: "1280px", label: "1280px" },
|
|
21
|
+
{ value: "1024px", label: "1024px" },
|
|
22
|
+
{ value: "768px", label: "768px" },
|
|
23
|
+
{ value: "360px", label: "360px" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const zoomOptions = [
|
|
27
|
+
{ value: "reset", label: "Reset" },
|
|
28
|
+
{ value: "100", label: "100%" },
|
|
29
|
+
{ value: "75", label: "75%" },
|
|
30
|
+
{ value: "50", label: "50%" },
|
|
31
|
+
{ value: "25", label: "25%" },
|
|
32
|
+
];
|
|
33
|
+
|
|
18
34
|
const Browser = (props: IBrowserProps): JSX.Element => {
|
|
19
35
|
const {
|
|
20
36
|
url,
|
|
@@ -33,15 +49,27 @@ const Browser = (props: IBrowserProps): JSX.Element => {
|
|
|
33
49
|
browserRef,
|
|
34
50
|
actions,
|
|
35
51
|
editorType = "page",
|
|
52
|
+
headingFilter,
|
|
53
|
+
keywordsFilter,
|
|
54
|
+
toggleHeadingsPreview,
|
|
55
|
+
toggleKeywordsPreview,
|
|
36
56
|
} = props;
|
|
37
57
|
|
|
38
58
|
const { id, entity, haveDraftPage } = content;
|
|
39
59
|
const domain = window.location.origin;
|
|
40
|
-
const
|
|
60
|
+
const headingFilterParam = !headingFilter || headingFilter === "all" ? "" : `&headingFilter=${headingFilter}`;
|
|
61
|
+
const keywordsFilterParam = !keywordsFilter || !keywordsFilter.length ? "" : `&keywordFilter=${keywordsFilter}`;
|
|
41
62
|
const isPageEditor = editorType === "page";
|
|
42
63
|
const isFormEditor = editorType === "form";
|
|
64
|
+
const isHeadingsEditor = editorType === "headings";
|
|
65
|
+
const isKeywordsEditor = editorType === "keywords";
|
|
43
66
|
|
|
44
|
-
const
|
|
67
|
+
const frameWrapperRef = useRef<HTMLDivElement>(null);
|
|
68
|
+
|
|
69
|
+
const [previewResolution, setPreviewResolution] = useState("desktop");
|
|
70
|
+
const [dimensions, setDimensions] = useState({ resolution: DEFAULT_RESOLUTION, zoom: "100" });
|
|
71
|
+
|
|
72
|
+
const urlPreview = `${domain}/editor/page-preview?preview=${!!isPreview}&disabled=${!!disabled}&type=${editorType}${headingFilterParam}${keywordsFilterParam}`;
|
|
45
73
|
const { isVisible, toggleToast, setIsVisible, state: toastState } = useToast();
|
|
46
74
|
const { isOpen: isShareOpen, toggleModal: toggleSharePageModal } = useModal(false, true);
|
|
47
75
|
const [shareData, setShareData] = useState<IShareData | null>(null);
|
|
@@ -53,6 +81,24 @@ const Browser = (props: IBrowserProps): JSX.Element => {
|
|
|
53
81
|
useEffect(() => {
|
|
54
82
|
localStorage.setItem("selectedID", "0");
|
|
55
83
|
(window as any).browserRef = null;
|
|
84
|
+
|
|
85
|
+
const el = frameWrapperRef.current;
|
|
86
|
+
if (!el) return;
|
|
87
|
+
|
|
88
|
+
let lastWidth = 0;
|
|
89
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
90
|
+
const containerWidth = entry.contentRect.width;
|
|
91
|
+
if (containerWidth > 0 && Math.abs(containerWidth - lastWidth) > 20) {
|
|
92
|
+
lastWidth = containerWidth;
|
|
93
|
+
const resolution = calcDefaultResolution(containerWidth, resolutionOptions);
|
|
94
|
+
const newZoom = calcAutoZoom(containerWidth, resolution);
|
|
95
|
+
setDimensions({ resolution, zoom: newZoom });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
observer.observe(el);
|
|
100
|
+
|
|
101
|
+
return () => observer.disconnect();
|
|
56
102
|
}, []);
|
|
57
103
|
|
|
58
104
|
// Fetch share data when in preview mode
|
|
@@ -85,159 +131,254 @@ const Browser = (props: IBrowserProps): JSX.Element => {
|
|
|
85
131
|
}
|
|
86
132
|
};
|
|
87
133
|
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
actions?.deleteModuleAction?.([editorID]);
|
|
91
|
-
};
|
|
134
|
+
const getWidth = (res: string) => {
|
|
135
|
+
if (!isPreview) return dimensions.resolution;
|
|
92
136
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
137
|
+
switch (res) {
|
|
138
|
+
case "tablet":
|
|
139
|
+
return "768px";
|
|
140
|
+
case "phone":
|
|
141
|
+
return "425px";
|
|
142
|
+
default:
|
|
143
|
+
return "100%";
|
|
144
|
+
}
|
|
96
145
|
};
|
|
97
146
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
147
|
+
const moduleActions = useMemo(
|
|
148
|
+
() => ({
|
|
149
|
+
deleteModuleAction: (editorID: number) => {
|
|
150
|
+
actions?.setSelectedContentAction(0);
|
|
151
|
+
actions?.deleteModuleAction?.([editorID]);
|
|
152
|
+
},
|
|
153
|
+
duplicateModuleAction: (editorID: number) => {
|
|
154
|
+
const duplicatedEditorID = actions?.duplicateModuleAction?.([editorID]);
|
|
155
|
+
duplicatedEditorID && actions?.setSelectedContentAction(duplicatedEditorID);
|
|
156
|
+
},
|
|
157
|
+
copyModuleAction: (editorID: number) => {
|
|
158
|
+
const isCopied = actions?.copyModuleAction?.([editorID]);
|
|
159
|
+
isCopied && toggleToast("1 module copied to clipboard");
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
[actions, toggleToast],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const maxFittingZoom = parseInt(calcAutoZoom(frameWrapperRef.current?.clientWidth ?? 0, dimensions.resolution));
|
|
166
|
+
const zoomOptionsWithDisabled = useMemo(() => {
|
|
167
|
+
const isCurrentZoomInOptions = zoomOptions.some((opt) => opt.value === dimensions.zoom);
|
|
168
|
+
return [
|
|
169
|
+
...(!isCurrentZoomInOptions ? [{ value: dimensions.zoom, label: `${dimensions.zoom}%` }] : []),
|
|
170
|
+
...zoomOptions.map((option) => ({
|
|
171
|
+
...option,
|
|
172
|
+
isDisabled: option.value === "reset" ? !isCurrentZoomInOptions : parseInt(option.value) > maxFittingZoom,
|
|
173
|
+
})),
|
|
174
|
+
];
|
|
175
|
+
}, [dimensions.zoom, maxFittingZoom]);
|
|
176
|
+
|
|
177
|
+
const handleResolutionChange = (resolution: string) => {
|
|
178
|
+
const containerWidth = frameWrapperRef.current?.clientWidth ?? 0;
|
|
179
|
+
setDimensions({ resolution, zoom: calcAutoZoom(containerWidth, resolution) });
|
|
101
180
|
};
|
|
102
181
|
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
182
|
+
const handleZoomChange = (zoom: string) => {
|
|
183
|
+
if (zoom === "reset") {
|
|
184
|
+
const containerWidth = frameWrapperRef.current?.clientWidth ?? 0;
|
|
185
|
+
setDimensions((prev) => ({ ...prev, zoom: calcAutoZoom(containerWidth, prev.resolution) }));
|
|
186
|
+
} else {
|
|
187
|
+
setDimensions((prev) => ({ ...prev, zoom }));
|
|
188
|
+
}
|
|
107
189
|
};
|
|
108
190
|
|
|
191
|
+
const scaledWidth = !isPreview
|
|
192
|
+
? Math.floor(parseInt(dimensions.resolution) * (parseInt(dimensions.zoom) / 100))
|
|
193
|
+
: undefined;
|
|
194
|
+
|
|
195
|
+
const isCompact = scaledWidth !== undefined && scaledWidth <= 400;
|
|
196
|
+
const floatingNoteConfig = isHeadingsEditor
|
|
197
|
+
? { label: "Headings", onClick: toggleHeadingsPreview }
|
|
198
|
+
: isKeywordsEditor
|
|
199
|
+
? { label: "Keywords", onClick: toggleKeywordsPreview }
|
|
200
|
+
: null;
|
|
201
|
+
|
|
109
202
|
return (
|
|
110
|
-
<S.
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
<S.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<S.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
<Tooltip
|
|
122
|
-
hideOnClick={!haveDraftPage}
|
|
123
|
-
content={
|
|
124
|
-
haveDraftPage ? "Only available for drafts. This page already has a public URL." : "Share draft"
|
|
125
|
-
}
|
|
126
|
-
bottom
|
|
203
|
+
<S.OuterContainer ref={frameWrapperRef}>
|
|
204
|
+
<S.BrowserWrapper data-testid="browser-wrapper" ref={browserRef} scaledWidth={scaledWidth} isPreview={isPreview}>
|
|
205
|
+
{(isPageEditor || isHeadingsEditor || isKeywordsEditor) && (
|
|
206
|
+
<S.NavBar>
|
|
207
|
+
<S.NavUrl>{url}</S.NavUrl>
|
|
208
|
+
{isPreview ? (
|
|
209
|
+
<S.NavActions data-testid="nav-actions-wrapper">
|
|
210
|
+
<S.IconWrapper
|
|
211
|
+
data-testid="icon-wrapper-browser"
|
|
212
|
+
onClick={haveDraftPage ? undefined : toggleSharePageModal}
|
|
213
|
+
active={!haveDraftPage}
|
|
127
214
|
>
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
active={
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
215
|
+
<Tooltip
|
|
216
|
+
hideOnClick={!haveDraftPage}
|
|
217
|
+
content={
|
|
218
|
+
haveDraftPage ? "Only available for drafts. This page already has a public URL." : "Share draft"
|
|
219
|
+
}
|
|
220
|
+
bottom
|
|
221
|
+
>
|
|
222
|
+
<Icon name="share" size="24" />
|
|
223
|
+
</Tooltip>
|
|
224
|
+
</S.IconWrapper>
|
|
225
|
+
<S.IconWrapper active={previewResolution === "desktop"} onClick={() => setPreviewResolution("desktop")}>
|
|
226
|
+
<Tooltip content="Desktop" bottom>
|
|
227
|
+
<Icon name="desktop" size="24" />
|
|
228
|
+
</Tooltip>
|
|
229
|
+
</S.IconWrapper>
|
|
230
|
+
<S.IconWrapper
|
|
231
|
+
data-testid="icon-res-tablet"
|
|
232
|
+
active={previewResolution === "tablet"}
|
|
233
|
+
onClick={() => setPreviewResolution("tablet")}
|
|
234
|
+
>
|
|
235
|
+
<Tooltip content="Tablet" bottom>
|
|
236
|
+
<Icon name="tablet" size="24" />
|
|
237
|
+
</Tooltip>
|
|
238
|
+
</S.IconWrapper>
|
|
239
|
+
<S.IconWrapper
|
|
240
|
+
data-testid="icon-res-phone"
|
|
241
|
+
active={previewResolution === "phone"}
|
|
242
|
+
onClick={() => setPreviewResolution("phone")}
|
|
243
|
+
>
|
|
244
|
+
<Tooltip content="Mobile" bottom>
|
|
245
|
+
<Icon name="phone" size="24" />
|
|
246
|
+
</Tooltip>
|
|
247
|
+
</S.IconWrapper>
|
|
248
|
+
</S.NavActions>
|
|
249
|
+
) : (
|
|
250
|
+
<S.NavActions data-testid="nav-actions-wrapper">
|
|
251
|
+
<S.ResolutionWrapper>
|
|
252
|
+
<S.SelectLabel>Screen</S.SelectLabel>
|
|
253
|
+
<Select
|
|
254
|
+
name="resolution"
|
|
255
|
+
options={resolutionOptions}
|
|
256
|
+
value={dimensions.resolution}
|
|
257
|
+
onChange={handleResolutionChange}
|
|
258
|
+
type="round"
|
|
259
|
+
offSet="30%"
|
|
260
|
+
mandatory
|
|
261
|
+
/>
|
|
262
|
+
</S.ResolutionWrapper>
|
|
263
|
+
<S.ZoomWrapper>
|
|
264
|
+
<Select
|
|
265
|
+
name="zoom"
|
|
266
|
+
options={zoomOptionsWithDisabled}
|
|
267
|
+
value={dimensions.zoom}
|
|
268
|
+
onChange={handleZoomChange}
|
|
269
|
+
type="round"
|
|
270
|
+
mandatory
|
|
271
|
+
/>
|
|
272
|
+
</S.ZoomWrapper>
|
|
273
|
+
</S.NavActions>
|
|
274
|
+
)}
|
|
275
|
+
</S.NavBar>
|
|
276
|
+
)}
|
|
277
|
+
{showIframe ? (
|
|
278
|
+
<S.ContentWrapper>
|
|
279
|
+
{floatingNoteConfig && (
|
|
280
|
+
<S.StyledFloatingNote
|
|
281
|
+
message={`You are viewing the ${floatingNoteConfig.label} editor${isCompact ? "" : " of this page"}`}
|
|
282
|
+
btnText={isCompact ? "Back" : "Back to Page Editor"}
|
|
283
|
+
onClick={floatingNoteConfig.onClick}
|
|
284
|
+
$compact={isCompact}
|
|
285
|
+
/>
|
|
286
|
+
)}
|
|
287
|
+
<S.FrameWrapper
|
|
288
|
+
hasBorder={isPageEditor || isHeadingsEditor || isKeywordsEditor}
|
|
289
|
+
isFormEditor={isFormEditor}
|
|
290
|
+
data-testid="navbar-iframe-wrapper"
|
|
291
|
+
>
|
|
292
|
+
{isPreview ? (
|
|
293
|
+
<iframe
|
|
294
|
+
title="Preview"
|
|
295
|
+
width={getWidth(previewResolution)}
|
|
296
|
+
height="100%"
|
|
297
|
+
src={urlPreview}
|
|
298
|
+
loading="lazy"
|
|
299
|
+
className="frame-content"
|
|
300
|
+
/>
|
|
301
|
+
) : (
|
|
302
|
+
<div
|
|
303
|
+
style={{
|
|
304
|
+
width: `${scaledWidth}px`,
|
|
305
|
+
height: "100%",
|
|
306
|
+
overflow: "hidden",
|
|
307
|
+
flexShrink: 0,
|
|
308
|
+
}}
|
|
309
|
+
>
|
|
310
|
+
<iframe
|
|
311
|
+
title="Preview"
|
|
312
|
+
width={dimensions.resolution}
|
|
313
|
+
src={urlPreview}
|
|
314
|
+
loading="lazy"
|
|
315
|
+
className="frame-content"
|
|
316
|
+
style={{
|
|
317
|
+
display: "block",
|
|
318
|
+
transform: `scale(${parseInt(dimensions.zoom) / 100})`,
|
|
319
|
+
transformOrigin: "0 0",
|
|
320
|
+
height: `${Math.round(100 / (parseInt(dimensions.zoom) / 100))}%`,
|
|
321
|
+
}}
|
|
322
|
+
/>
|
|
323
|
+
</div>
|
|
324
|
+
)}
|
|
325
|
+
</S.FrameWrapper>
|
|
326
|
+
</S.ContentWrapper>
|
|
327
|
+
) : (
|
|
328
|
+
<S.Wrapper data-testid="browser-content-wrapper" className="browser-content">
|
|
329
|
+
<BrowserContent
|
|
330
|
+
cloudinaryName={cloudinaryName}
|
|
331
|
+
theme={theme}
|
|
332
|
+
socials={socials}
|
|
333
|
+
siteLangs={siteLangs}
|
|
334
|
+
selectEditorID={selectEditorID}
|
|
335
|
+
siteID={siteID}
|
|
336
|
+
isPage={isPage}
|
|
337
|
+
content={content}
|
|
338
|
+
header={header}
|
|
339
|
+
footer={footer}
|
|
340
|
+
languageID={content.language}
|
|
341
|
+
pageLanguages={content.pageLanguages}
|
|
342
|
+
moduleActions={moduleActions}
|
|
343
|
+
renderer="editor"
|
|
344
|
+
/>
|
|
345
|
+
</S.Wrapper>
|
|
346
|
+
)}
|
|
347
|
+
{isVisible && <Toast message={toastState} setIsVisible={setIsVisible} />}
|
|
348
|
+
{isPreview && shareData && tokenInfo && (
|
|
349
|
+
<OcassionalToast
|
|
350
|
+
message={
|
|
351
|
+
<>
|
|
352
|
+
{tokenInfo.tokenHasExpired
|
|
353
|
+
? `Access to this draft link expired on ${tokenInfo.tokenExpirationDate}. You need to generate a `
|
|
354
|
+
: `This draft link expires on ${tokenInfo.tokenExpirationDate}. ${tokenInfo.tokenCanBeRenewed ? "Renew it" : "Open details"}${" "}`}
|
|
355
|
+
<button type="button" onClick={toggleSharePageModal}>
|
|
356
|
+
<strong style={{ textDecoration: "underline" }}>
|
|
357
|
+
{tokenInfo.tokenHasExpired ? "new one" : "here"}
|
|
358
|
+
</strong>
|
|
359
|
+
</button>
|
|
360
|
+
</>
|
|
361
|
+
}
|
|
362
|
+
icon={tokenInfo.tokenHasExpired ? "hide" : "view"}
|
|
168
363
|
/>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
socials={socials}
|
|
181
|
-
siteLangs={siteLangs}
|
|
182
|
-
selectEditorID={selectEditorID}
|
|
183
|
-
siteID={siteID}
|
|
184
|
-
isPage={isPage}
|
|
185
|
-
content={content}
|
|
186
|
-
header={header}
|
|
187
|
-
footer={footer}
|
|
188
|
-
languageID={content.language}
|
|
189
|
-
pageLanguages={content.pageLanguages}
|
|
190
|
-
moduleActions={moduleActions}
|
|
191
|
-
renderer="editor"
|
|
364
|
+
)}
|
|
365
|
+
|
|
366
|
+
{isPreview && (
|
|
367
|
+
<SharePageModal
|
|
368
|
+
pageTitle={content.title}
|
|
369
|
+
isOpen={isShareOpen}
|
|
370
|
+
hide={toggleSharePageModal}
|
|
371
|
+
pageID={id}
|
|
372
|
+
entity={entity}
|
|
373
|
+
shareData={shareData}
|
|
374
|
+
onShareChange={setShareData}
|
|
192
375
|
/>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
{isVisible && <Toast message={toastState} setIsVisible={setIsVisible} />}
|
|
197
|
-
|
|
198
|
-
{isPreview && shareData && tokenInfo && (
|
|
199
|
-
<OcassionalToast
|
|
200
|
-
message={
|
|
201
|
-
<>
|
|
202
|
-
{tokenInfo.tokenHasExpired
|
|
203
|
-
? `Access to this draft link expired on ${tokenInfo.tokenExpirationDate}. You need to generate a `
|
|
204
|
-
: `This draft link expires on ${tokenInfo.tokenExpirationDate}. ${tokenInfo.tokenCanBeRenewed ? "Renew it" : "Open details"}${" "}`}
|
|
205
|
-
<button type="button" onClick={toggleSharePageModal}>
|
|
206
|
-
<strong style={{ textDecoration: "underline" }}>
|
|
207
|
-
{tokenInfo.tokenHasExpired ? "new one" : "here"}
|
|
208
|
-
</strong>
|
|
209
|
-
</button>
|
|
210
|
-
</>
|
|
211
|
-
}
|
|
212
|
-
icon={tokenInfo.tokenHasExpired ? "hide" : "view"}
|
|
213
|
-
/>
|
|
214
|
-
)}
|
|
215
|
-
|
|
216
|
-
{isPreview && (
|
|
217
|
-
<SharePageModal
|
|
218
|
-
pageTitle={content.title}
|
|
219
|
-
isOpen={isShareOpen}
|
|
220
|
-
hide={toggleSharePageModal}
|
|
221
|
-
pageID={id}
|
|
222
|
-
entity={entity}
|
|
223
|
-
shareData={shareData}
|
|
224
|
-
onShareChange={setShareData}
|
|
225
|
-
/>
|
|
226
|
-
)}
|
|
227
|
-
</S.BrowserWrapper>
|
|
376
|
+
)}
|
|
377
|
+
</S.BrowserWrapper>
|
|
378
|
+
</S.OuterContainer>
|
|
228
379
|
);
|
|
229
380
|
};
|
|
230
381
|
|
|
231
|
-
function getWidth(res: string) {
|
|
232
|
-
switch (res) {
|
|
233
|
-
case "tablet":
|
|
234
|
-
return "768px";
|
|
235
|
-
case "phone":
|
|
236
|
-
return "425px";
|
|
237
|
-
default:
|
|
238
|
-
return "100%";
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
382
|
export interface IBrowserProps {
|
|
242
383
|
content: any;
|
|
243
384
|
header?: any;
|
|
@@ -253,7 +394,11 @@ export interface IBrowserProps {
|
|
|
253
394
|
isPreview?: boolean;
|
|
254
395
|
showIframe?: boolean;
|
|
255
396
|
browserRef?: React.RefObject<HTMLDivElement>;
|
|
256
|
-
editorType?: "form" | "page";
|
|
397
|
+
editorType?: "form" | "page" | "headings" | "keywords";
|
|
398
|
+
headingFilter?: HeadingFilter;
|
|
399
|
+
keywordsFilter?: string[];
|
|
400
|
+
toggleHeadingsPreview?(): void;
|
|
401
|
+
toggleKeywordsPreview?(): void;
|
|
257
402
|
actions?: {
|
|
258
403
|
setSelectedContentAction(editorID: number): void;
|
|
259
404
|
deleteModuleAction?(editorID: number[]): void;
|