@griddo/ax 12.13.0-rc.1 → 12.13.1-beta.1
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/README.md +12 -12
- package/config/webpack.config.js +1 -1
- package/package.json +4 -4
- package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.caption.test.tsx +232 -0
- package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.toolbar.test.tsx +75 -0
- package/src/components/Fields/Wysiwyg/config.tsx +6 -1
- package/src/components/Fields/Wysiwyg/helpers.tsx +17 -4
- package/src/components/Fields/Wysiwyg/index.tsx +69 -1
- package/src/modules/ActivityLog/DownloadModal/index.tsx +2 -4
- package/src/modules/ActivityLog/index.tsx +5 -6
package/README.md
CHANGED
|
@@ -9,27 +9,27 @@
|
|
|
9
9
|
|
|
10
10
|
### Desarrollo
|
|
11
11
|
|
|
12
|
-
- `
|
|
13
|
-
- `
|
|
14
|
-
- `
|
|
15
|
-
- `
|
|
12
|
+
- `yarn start` - Alias para `start:dev`
|
|
13
|
+
- `yarn start:local` - Inicia el servidor de desarrollo en entorno local
|
|
14
|
+
- `yarn start:dev` - Inicia el servidor de desarrollo en entorno de desarrollo
|
|
15
|
+
- `yarn start:staging` - Inicia el servidor de desarrollo en entorno de staging
|
|
16
16
|
|
|
17
17
|
### Build
|
|
18
18
|
|
|
19
|
-
- `
|
|
20
|
-
- `
|
|
19
|
+
- `yarn build` - Construye la aplicación para producción
|
|
20
|
+
- `yarn build:dev` - Construye la aplicación para desarrollo
|
|
21
21
|
|
|
22
22
|
### Testing
|
|
23
23
|
|
|
24
|
-
- `
|
|
25
|
-
- `
|
|
26
|
-
- `
|
|
27
|
-
- `
|
|
24
|
+
- `yarn test` - Ejecuta los tests con Jest
|
|
25
|
+
- `yarn test:watch` - Ejecuta los tests en modo watch
|
|
26
|
+
- `yarn test:ci` - Ejecuta los tests en modo CI (sin paralelización)
|
|
27
|
+
- `yarn test:coverage` - Ejecuta los tests y genera reporte de cobertura
|
|
28
28
|
|
|
29
29
|
### Linting y Formato
|
|
30
30
|
|
|
31
|
-
- `
|
|
32
|
-
- `
|
|
31
|
+
- `yarn lint` - Verifica el código con Biome (sin escribir)
|
|
32
|
+
- `yarn format:write` - Formatea el código con Biome (escribiendo)
|
|
33
33
|
|
|
34
34
|
¿Quieres comprobar solo una regla o categoría específica?
|
|
35
35
|
```bash
|
package/config/webpack.config.js
CHANGED
|
@@ -83,7 +83,7 @@ const hasJsxRuntime = (() => {
|
|
|
83
83
|
|
|
84
84
|
// This is the production and development configuration.
|
|
85
85
|
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
|
|
86
|
-
module.exports = (webpackEnv)
|
|
86
|
+
module.exports = function (webpackEnv) {
|
|
87
87
|
const isEnvDevelopment = webpackEnv === "development";
|
|
88
88
|
const isEnvProduction = webpackEnv === "production";
|
|
89
89
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@griddo/ax",
|
|
3
3
|
"description": "Griddo Author Experience",
|
|
4
|
-
"version": "12.13.
|
|
4
|
+
"version": "12.13.1-beta.1",
|
|
5
5
|
"homepage": "https://griddo.io",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
},
|
|
128
128
|
"devDependencies": {
|
|
129
129
|
"@biomejs/biome": "2.5.15",
|
|
130
|
-
"@griddo/core": "12.13.
|
|
130
|
+
"@griddo/core": "12.13.1-beta.1",
|
|
131
131
|
"@types/redux-mock-store": "1.5.0",
|
|
132
132
|
"@vitest/coverage-v8": "4.1.11",
|
|
133
133
|
"axios-mock-adapter": "1.22.0",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"vitest-styled-components": "1.0.0"
|
|
139
139
|
},
|
|
140
140
|
"peerDependencies": {
|
|
141
|
-
"@griddo/core": "12.13.
|
|
141
|
+
"@griddo/core": "12.13.1-beta.1"
|
|
142
142
|
},
|
|
143
143
|
"babel": {
|
|
144
144
|
"presets": [
|
|
@@ -171,5 +171,5 @@
|
|
|
171
171
|
"publishConfig": {
|
|
172
172
|
"access": "public"
|
|
173
173
|
},
|
|
174
|
-
"gitHead": "
|
|
174
|
+
"gitHead": "2570d2896bbdf191bdc40861f422ca1d053f6e5c"
|
|
175
175
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import Wysiwyg from "@ax/components/Fields/Wysiwyg";
|
|
2
|
+
import { parseTheme } from "@ax/helpers";
|
|
3
|
+
import globalTheme from "@ax/themes/theme.json";
|
|
4
|
+
|
|
5
|
+
import { config } from "components";
|
|
6
|
+
import FroalaEditor from "froala-editor";
|
|
7
|
+
import configureStore from "redux-mock-store";
|
|
8
|
+
import { ThemeProvider } from "styled-components";
|
|
9
|
+
|
|
10
|
+
import { cleanup, render, waitFor } from "../../../../../config/tests/test-utils";
|
|
11
|
+
|
|
12
|
+
// The real Froala, not the mock the rest of the Wysiwyg tests use: the bug lives
|
|
13
|
+
// in its image plugin, so only the plugin itself can show it is gone.
|
|
14
|
+
|
|
15
|
+
const schemasConfig = config.schemas.config;
|
|
16
|
+
const store = configureStore()({ app: { token: "token" } });
|
|
17
|
+
|
|
18
|
+
const mountEditor = async (value: string) => {
|
|
19
|
+
const { container } = render(
|
|
20
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
21
|
+
<Wysiwyg value={value} onChange={vi.fn()} site={null} />
|
|
22
|
+
</ThemeProvider>,
|
|
23
|
+
{ store },
|
|
24
|
+
);
|
|
25
|
+
let editor: any;
|
|
26
|
+
await waitFor(() => {
|
|
27
|
+
const el = container.querySelector(".fr-element");
|
|
28
|
+
editor = (FroalaEditor as any).INSTANCES.find((instance: any) => instance.el === el);
|
|
29
|
+
expect(editor?.image).toBeTruthy();
|
|
30
|
+
});
|
|
31
|
+
return editor;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const toggleCaption = (editor: any, img: HTMLElement = editor.el.querySelector("img")) => {
|
|
35
|
+
editor.image.edit(editor.$(img));
|
|
36
|
+
editor.commands.exec("imageCaption");
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// jsdom has no layout and no DOMRect
|
|
40
|
+
const rect = (width: number) =>
|
|
41
|
+
({ x: 0, y: 0, top: 0, left: 0, right: width, bottom: 200, width, height: 200 }) as DOMRect;
|
|
42
|
+
|
|
43
|
+
const caption = (editor: any): HTMLElement | null => editor.el.querySelector(".fr-img-caption");
|
|
44
|
+
|
|
45
|
+
describe("Wysiwyg image caption", () => {
|
|
46
|
+
beforeAll(() => {
|
|
47
|
+
// Froala's licence check splits document.domain, which jsdom leaves undefined
|
|
48
|
+
Object.defineProperty(document, "domain", { value: "localhost", configurable: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
cleanup();
|
|
53
|
+
delete schemasConfig.richTextConfig;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("with image resizing on", () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
schemasConfig.richTextConfig = { imageResize: true };
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it.each([
|
|
62
|
+
["a new image in a paragraph", '<p><img src="http://x/a.png" alt="" class="fr-dib"></p>'],
|
|
63
|
+
["a linked image", '<p><a href="http://x"><img src="http://x/a.png" alt="" class="fr-dib"></a></p>'],
|
|
64
|
+
["an image outside a paragraph", '<ul><li><img src="http://x/a.png" alt="" class="fr-dib"></li></ul>'],
|
|
65
|
+
])("should open an editable caption under %s", async (_, html) => {
|
|
66
|
+
const editor = await mountEditor(html);
|
|
67
|
+
|
|
68
|
+
toggleCaption(editor);
|
|
69
|
+
|
|
70
|
+
const inner = caption(editor)?.querySelector(".fr-inner");
|
|
71
|
+
expect(inner?.textContent).toBe("Image Caption");
|
|
72
|
+
expect(inner?.getAttribute("contenteditable")).toBe("true");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should give the caption the width the image is showing at", async () => {
|
|
76
|
+
const editor = await mountEditor('<p><img src="http://x/a.png" alt="" class="fr-dib"></p>');
|
|
77
|
+
vi.spyOn(editor.el.querySelector("img"), "getBoundingClientRect").mockReturnValue(rect(300));
|
|
78
|
+
vi.spyOn(editor.el.querySelector("p"), "getBoundingClientRect").mockReturnValue(rect(600));
|
|
79
|
+
|
|
80
|
+
toggleCaption(editor);
|
|
81
|
+
|
|
82
|
+
expect(caption(editor)?.style.width).toBe("50%");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should fall back to the full width when the image cannot be measured", async () => {
|
|
86
|
+
const editor = await mountEditor('<p><img src="http://x/a.png" alt="" class="fr-dib"></p>');
|
|
87
|
+
|
|
88
|
+
toggleCaption(editor);
|
|
89
|
+
|
|
90
|
+
expect(caption(editor)?.style.width).toBe("100%");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should move a resized image's width to its caption and back", async () => {
|
|
94
|
+
const editor = await mountEditor('<p><img src="http://x/a.png" alt="" class="fr-dib" style="width: 40%;"></p>');
|
|
95
|
+
const img = editor.el.querySelector("img");
|
|
96
|
+
|
|
97
|
+
toggleCaption(editor);
|
|
98
|
+
|
|
99
|
+
expect(caption(editor)?.style.width).toBe("40%");
|
|
100
|
+
expect(img.style.width).toBe("");
|
|
101
|
+
|
|
102
|
+
toggleCaption(editor);
|
|
103
|
+
|
|
104
|
+
expect(caption(editor)).toBeNull();
|
|
105
|
+
expect(img.style.width).toBe("40%");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should not leave the width it hands Froala in the content", async () => {
|
|
109
|
+
const editor = await mountEditor('<p><img src="http://x/a.png" alt="" class="fr-dib"></p>');
|
|
110
|
+
|
|
111
|
+
toggleCaption(editor);
|
|
112
|
+
|
|
113
|
+
expect(editor.el.querySelector("[width]")).toBeNull();
|
|
114
|
+
expect(editor.el.querySelector("img").hasAttribute("style")).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should turn a pixel width from before percentages into the caption's share", async () => {
|
|
118
|
+
const editor = await mountEditor(
|
|
119
|
+
'<ul><li><img src="http://x/a.png" alt="" class="fr-dib" style="width: 300px;"></li></ul>',
|
|
120
|
+
);
|
|
121
|
+
const img = editor.el.querySelector("img");
|
|
122
|
+
vi.spyOn(img, "getBoundingClientRect").mockReturnValue(rect(300));
|
|
123
|
+
vi.spyOn(editor.el.querySelector("li"), "getBoundingClientRect").mockReturnValue(rect(600));
|
|
124
|
+
|
|
125
|
+
toggleCaption(editor);
|
|
126
|
+
|
|
127
|
+
expect(caption(editor)?.style.width).toBe("50%");
|
|
128
|
+
expect(img.style.width).toBe("");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should keep a resized image's size when its caption is put back", async () => {
|
|
132
|
+
const editor = await mountEditor('<p><img src="http://x/a.png" alt="" class="fr-dib" style="width: 40%;"></p>');
|
|
133
|
+
const img = editor.el.querySelector("img");
|
|
134
|
+
|
|
135
|
+
toggleCaption(editor);
|
|
136
|
+
toggleCaption(editor);
|
|
137
|
+
toggleCaption(editor);
|
|
138
|
+
|
|
139
|
+
expect(caption(editor)?.style.width).toBe("40%");
|
|
140
|
+
expect(img.style.width).toBe("");
|
|
141
|
+
expect(editor.el.querySelectorAll(".fr-img-space-wrap")).toHaveLength(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should leave the other captions alone when an image is captioned again", async () => {
|
|
145
|
+
const editor = await mountEditor(
|
|
146
|
+
'<p><img id="first" src="http://x/a.png" alt="" class="fr-dib"></p><p><img id="second" src="http://x/b.png" alt="" class="fr-dib"></p>',
|
|
147
|
+
);
|
|
148
|
+
const first = editor.el.querySelector("#first");
|
|
149
|
+
const second = editor.el.querySelector("#second");
|
|
150
|
+
|
|
151
|
+
toggleCaption(editor, first);
|
|
152
|
+
toggleCaption(editor, second);
|
|
153
|
+
toggleCaption(editor, second);
|
|
154
|
+
toggleCaption(editor, second);
|
|
155
|
+
|
|
156
|
+
expect(first.closest(".fr-img-space-wrap")).not.toBeNull();
|
|
157
|
+
expect(first.closest(".fr-img-caption")).not.toBeNull();
|
|
158
|
+
expect(second.closest(".fr-img-caption")).not.toBeNull();
|
|
159
|
+
expect(editor.el.querySelectorAll(".fr-img-space-wrap")).toHaveLength(2);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("when resizing a captioned image", () => {
|
|
163
|
+
// jsdom has no layout: the column is 600px, and the caption, with the
|
|
164
|
+
// paragraph and image inside it, takes the share its style asks for
|
|
165
|
+
const COLUMN = 600;
|
|
166
|
+
|
|
167
|
+
beforeEach(() => {
|
|
168
|
+
vi.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockImplementation(function (this: HTMLElement) {
|
|
169
|
+
const box = this.closest(".fr-img-caption") as HTMLElement | null;
|
|
170
|
+
return box ? (Number.parseFloat(box.style.width) / 100) * COLUMN : COLUMN;
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const captioned = async () => {
|
|
175
|
+
const editor = await mountEditor('<p><img src="http://x/a.png" alt="" class="fr-dib" style="width: 50%;"></p>');
|
|
176
|
+
const img = editor.el.querySelector("img");
|
|
177
|
+
toggleCaption(editor);
|
|
178
|
+
editor.image.edit(editor.$(img));
|
|
179
|
+
return { editor, img };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Froala reads pageX, which jsdom does not derive from clientX
|
|
183
|
+
const mouse = (type: string, pageX = 0) =>
|
|
184
|
+
Object.defineProperty(new MouseEvent(type, { bubbles: true }), "pageX", { value: pageX });
|
|
185
|
+
|
|
186
|
+
const dragHandle = (moves: number[]) => {
|
|
187
|
+
const handle = document.querySelector(".fr-image-resizer .fr-hse") as HTMLElement;
|
|
188
|
+
handle.dispatchEvent(mouse("mousedown", 100));
|
|
189
|
+
return moves.map((x) => {
|
|
190
|
+
document.dispatchEvent(mouse("mousemove", x));
|
|
191
|
+
return (document.querySelector(".fr-img-caption") as HTMLElement).style.width;
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
it("should follow the pointer without bouncing", async () => {
|
|
196
|
+
await captioned();
|
|
197
|
+
|
|
198
|
+
expect(dragHandle([160, 160, 160, 130])).toEqual(["60%", "60%", "60%", "55%"]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("should put the caption back in its paragraph when the drag ends", async () => {
|
|
202
|
+
const { img } = await captioned();
|
|
203
|
+
|
|
204
|
+
dragHandle([160]);
|
|
205
|
+
document.body.dispatchEvent(mouse("mouseup"));
|
|
206
|
+
|
|
207
|
+
const paragraph = img.closest(".fr-img-wrap > p");
|
|
208
|
+
expect(paragraph).not.toBeNull();
|
|
209
|
+
expect(paragraph.querySelector(".fr-inner")?.textContent).toBe("Image Caption");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should remove the caption it added", async () => {
|
|
214
|
+
const editor = await mountEditor('<p><img src="http://x/a.png" alt="" class="fr-dib"></p>');
|
|
215
|
+
|
|
216
|
+
toggleCaption(editor);
|
|
217
|
+
toggleCaption(editor);
|
|
218
|
+
|
|
219
|
+
expect(caption(editor)).toBeNull();
|
|
220
|
+
expect(editor.el.querySelector(".fr-inner")).toBeNull();
|
|
221
|
+
expect(editor.el.querySelector("img")).not.toBeNull();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("should leave Froala's pixel caption alone when resizing is off", async () => {
|
|
226
|
+
const editor = await mountEditor('<p><img src="http://x/a.png" alt="" class="fr-dib"></p>');
|
|
227
|
+
|
|
228
|
+
toggleCaption(editor);
|
|
229
|
+
|
|
230
|
+
expect(caption(editor)?.style.width).toMatch(/px$/);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import Wysiwyg from "@ax/components/Fields/Wysiwyg";
|
|
2
|
+
import { parseTheme } from "@ax/helpers";
|
|
3
|
+
import globalTheme from "@ax/themes/theme.json";
|
|
4
|
+
|
|
5
|
+
import FroalaEditor from "froala-editor";
|
|
6
|
+
import configureStore from "redux-mock-store";
|
|
7
|
+
import { ThemeProvider } from "styled-components";
|
|
8
|
+
|
|
9
|
+
import { cleanup, render, waitFor } from "../../../../../config/tests/test-utils";
|
|
10
|
+
|
|
11
|
+
// The real Froala, not the mock the rest of the Wysiwyg tests use: the bug lives
|
|
12
|
+
// in its sticky toolbar, so only Froala itself can show it is gone.
|
|
13
|
+
|
|
14
|
+
const store = configureStore()({ app: { token: "token" } });
|
|
15
|
+
|
|
16
|
+
const mountEditor = async () => {
|
|
17
|
+
const { container } = render(
|
|
18
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
19
|
+
<Wysiwyg value="<p>Lorem ipsum</p>" onChange={vi.fn()} site={null} />
|
|
20
|
+
</ThemeProvider>,
|
|
21
|
+
{ store },
|
|
22
|
+
);
|
|
23
|
+
let editor: any;
|
|
24
|
+
await waitFor(() => {
|
|
25
|
+
const el = container.querySelector(".fr-element");
|
|
26
|
+
editor = (FroalaEditor as any).INSTANCES.find((instance: any) => instance.el === el);
|
|
27
|
+
expect(editor?.$tb).toBeTruthy();
|
|
28
|
+
});
|
|
29
|
+
return editor;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// jsdom has no layout: Froala measures with offsetWidth/offsetHeight and places
|
|
33
|
+
// the box with getBoundingClientRect
|
|
34
|
+
const layOut = (box: HTMLElement, top: number) => {
|
|
35
|
+
vi.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockReturnValue(300);
|
|
36
|
+
vi.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockImplementation(function (this: HTMLElement) {
|
|
37
|
+
return this === box ? 400 : 50;
|
|
38
|
+
});
|
|
39
|
+
vi.spyOn(box, "getBoundingClientRect").mockReturnValue({
|
|
40
|
+
x: 0,
|
|
41
|
+
y: top,
|
|
42
|
+
top,
|
|
43
|
+
left: 0,
|
|
44
|
+
right: 300,
|
|
45
|
+
bottom: top + 400,
|
|
46
|
+
width: 300,
|
|
47
|
+
height: 400,
|
|
48
|
+
} as DOMRect);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
describe("Wysiwyg toolbar", () => {
|
|
52
|
+
beforeAll(() => {
|
|
53
|
+
// Froala's licence check splits document.domain, which jsdom leaves undefined
|
|
54
|
+
Object.defineProperty(document, "domain", { value: "localhost", configurable: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
cleanup();
|
|
59
|
+
vi.restoreAllMocks();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// AX scrolls an inner container, not the window, and Froala only re-places its
|
|
63
|
+
// sticky toolbar on window scroll, focus and resize. Focusing a half scrolled-out
|
|
64
|
+
// field pinned the toolbar under the app header, and scrolling back left it
|
|
65
|
+
// there, with an empty gap where it belonged, until the next focus.
|
|
66
|
+
it("should keep the toolbar in place when the field gets focus half scrolled out", async () => {
|
|
67
|
+
const editor = await mountEditor();
|
|
68
|
+
layOut(editor.$box.get(0), -100);
|
|
69
|
+
|
|
70
|
+
editor.events.trigger("focus");
|
|
71
|
+
|
|
72
|
+
expect(editor.$tb.get(0).classList.contains("fr-sticky-on")).toBe(false);
|
|
73
|
+
expect(editor.$box.get(0).querySelector(".fr-sticky-dummy")).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -120,6 +120,11 @@ const getWysiwygConfig = () => ({
|
|
|
120
120
|
pasteDeniedAttrs: ["class", "id", "style", "dir"],
|
|
121
121
|
listAdvancedTypes: false,
|
|
122
122
|
attribution: false,
|
|
123
|
+
// Froala only re-places its sticky toolbar on window scroll, but AX scrolls an
|
|
124
|
+
// inner container: focusing a half scrolled-out field pinned the toolbar under
|
|
125
|
+
// the app header and left it there. The content scrolls inside a capped box
|
|
126
|
+
// anyway, so the toolbar never ends up far from the text.
|
|
127
|
+
toolbarSticky: false,
|
|
123
128
|
paragraphFormat: {
|
|
124
129
|
N: "Normal",
|
|
125
130
|
H1: "Heading 1",
|
|
@@ -380,8 +385,8 @@ FroalaEditor.RegisterCommand("imageReplaceGriddo", {
|
|
|
380
385
|
});
|
|
381
386
|
|
|
382
387
|
export {
|
|
383
|
-
getButtonsFull as buttonsFull,
|
|
384
388
|
getButtons as buttons,
|
|
389
|
+
getButtonsFull as buttonsFull,
|
|
385
390
|
getInlineToolbar as inlineToolbar,
|
|
386
391
|
getWysiwygConfig as wysiwygConfig,
|
|
387
392
|
};
|
|
@@ -140,12 +140,25 @@ const createPopupFunctions = (editor: any, editorEl: HTMLElement) => {
|
|
|
140
140
|
return { showPopup, showErrorPopup };
|
|
141
141
|
};
|
|
142
142
|
|
|
143
|
+
// The width an image is showing at, as the share of its block that Froala's
|
|
144
|
+
// percentage resizing stores. Unmeasurable (hidden, or no layout) falls back to
|
|
145
|
+
// the full width, as Froala does.
|
|
146
|
+
const getImagePercentWidth = (editor: any, $img: any) => {
|
|
147
|
+
const img: HTMLElement = $img.get(0);
|
|
148
|
+
if (img.style.width.endsWith("%")) return img.style.width;
|
|
149
|
+
|
|
150
|
+
const block: HTMLElement = $img.parentsUntil(editor.$el, editor.html.blockTagsQuery()).get(0) || editor.el;
|
|
151
|
+
const percent = Math.round((img.getBoundingClientRect().width / block.getBoundingClientRect().width) * 100);
|
|
152
|
+
return percent > 0 && percent < 100 ? `${percent}%` : "100%";
|
|
153
|
+
};
|
|
154
|
+
|
|
143
155
|
export {
|
|
144
|
-
|
|
145
|
-
|
|
156
|
+
createPopupFunctions,
|
|
157
|
+
getImagePercentWidth,
|
|
146
158
|
getLanguageMenuHtml,
|
|
147
|
-
setLinkAttributes,
|
|
148
159
|
getPopupPosition,
|
|
149
|
-
|
|
160
|
+
getRichTextConfig,
|
|
150
161
|
type IRichTextConfig,
|
|
162
|
+
parseClassNames,
|
|
163
|
+
setLinkAttributes,
|
|
151
164
|
};
|
|
@@ -11,7 +11,7 @@ import FroalaEditor from "froala-editor";
|
|
|
11
11
|
|
|
12
12
|
import { LinkPopover } from "./atoms";
|
|
13
13
|
import { buttons, buttonsFull, inlineToolbar, wysiwygConfig } from "./config";
|
|
14
|
-
import { createPopupFunctions, getPopupPosition, setLinkAttributes } from "./helpers";
|
|
14
|
+
import { createPopupFunctions, getImagePercentWidth, getPopupPosition, setLinkAttributes } from "./helpers";
|
|
15
15
|
import "./vendors";
|
|
16
16
|
|
|
17
17
|
import { useModals } from "@ax/hooks";
|
|
@@ -51,6 +51,8 @@ const Wysiwyg = (props: IWysiwygProps) => {
|
|
|
51
51
|
isImageLink?: boolean;
|
|
52
52
|
} | null>(null);
|
|
53
53
|
const replaceImgRef = useRef<any>(null);
|
|
54
|
+
const captionWidthRef = useRef<any>(null);
|
|
55
|
+
const liftedCaptionRef = useRef<{ paragraph: HTMLElement; nodes: ChildNode[] } | null>(null);
|
|
54
56
|
const lastValueRef = useRef(value);
|
|
55
57
|
|
|
56
58
|
useEffect(() => {
|
|
@@ -209,6 +211,28 @@ const Wysiwyg = (props: IWysiwygProps) => {
|
|
|
209
211
|
const editorEl = this.$el?.get?.(0) || this.el;
|
|
210
212
|
if (!editorEl) return;
|
|
211
213
|
|
|
214
|
+
// Froala resizes a percentage image against the nearest block around
|
|
215
|
+
// it, and with a caption on that is the <p> the caption keeps inside:
|
|
216
|
+
// every step of the drag measures against the width the step before
|
|
217
|
+
// just set, and the image bounces. Lift the caption's content out of
|
|
218
|
+
// that paragraph while dragging, so it measures against the column,
|
|
219
|
+
// and put it back in "image.resizeEnd". Capturing runs it before
|
|
220
|
+
// Froala's own mousedown on the handle.
|
|
221
|
+
if (this.opts.imageResizeWithPercent) {
|
|
222
|
+
this.$box?.get?.(0)?.addEventListener(
|
|
223
|
+
"mousedown",
|
|
224
|
+
(e: MouseEvent) => {
|
|
225
|
+
if (!(e.target as Element).closest?.(".fr-handler")) return;
|
|
226
|
+
const paragraph = this.image.get()?.get(0)?.closest(".fr-img-wrap > p");
|
|
227
|
+
if (!paragraph) return;
|
|
228
|
+
const nodes = [...paragraph.childNodes];
|
|
229
|
+
paragraph.replaceWith(...nodes);
|
|
230
|
+
liftedCaptionRef.current = { paragraph, nodes };
|
|
231
|
+
},
|
|
232
|
+
true,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
212
236
|
editorEl.addEventListener("griddo:openLinkPopover", (e: Event) => {
|
|
213
237
|
const detail = (e as CustomEvent).detail;
|
|
214
238
|
setLinkPopoverOpen(true);
|
|
@@ -292,15 +316,59 @@ const Wysiwyg = (props: IWysiwygProps) => {
|
|
|
292
316
|
// Froala's own 2147483642, and with the option gone nothing ever clears
|
|
293
317
|
// it: the toolbar then paints over every modal the field opens. Seed the
|
|
294
318
|
// key the plugin reads, and drop what it left inline on the way out.
|
|
319
|
+
//
|
|
320
|
+
// With imageResizeWithPercent, Froala sizes a new caption from the width
|
|
321
|
+
// of the image, or of the <p>/<a> around it, and reads that width without
|
|
322
|
+
// checking there is one: on an image inside a paragraph, or never resized,
|
|
323
|
+
// it throws and no caption appears. Hand it the image's own width, and
|
|
324
|
+
// once the caption is in, give that width to the caption, as Froala's
|
|
325
|
+
// resizing expects, and take it off the image so it is not applied twice.
|
|
326
|
+
// Removing the caption gives it back to the image.
|
|
327
|
+
//
|
|
328
|
+
// Removing a caption also leaves the image in Froala's wrapper div, out
|
|
329
|
+
// of its paragraph. Captioning it again there nests a second wrapper that
|
|
330
|
+
// Froala then fails to unwrap, so put the image back in a paragraph first.
|
|
295
331
|
"commands.before": function (this: any, cmd: string) {
|
|
296
332
|
if (cmd === "fullscreen" && !this.fullscreen.isActive()) {
|
|
297
333
|
this.opts.z_index = this.opts.zIndex;
|
|
298
334
|
}
|
|
335
|
+
if (cmd === "imageCaption" && this.opts.imageResizeWithPercent && !this.image.hasCaption()) {
|
|
336
|
+
const $img = this.image.get();
|
|
337
|
+
if (!$img) return;
|
|
338
|
+
const img = $img.get(0);
|
|
339
|
+
const leftoverWrap = img.closest(".fr-img-space-wrap");
|
|
340
|
+
if (leftoverWrap) {
|
|
341
|
+
const p = document.createElement("p");
|
|
342
|
+
p.append(...leftoverWrap.childNodes);
|
|
343
|
+
leftoverWrap.replaceWith(p);
|
|
344
|
+
}
|
|
345
|
+
const width = getImagePercentWidth(this, $img);
|
|
346
|
+
const $sized = $img.parent().is("a") || $img.parent().is("p") ? $img.parent() : $img;
|
|
347
|
+
const seeded = !$sized.attr("style") && !$sized.attr("width");
|
|
348
|
+
if (seeded) $sized.attr("width", width);
|
|
349
|
+
captionWidthRef.current = { img, width, $seeded: seeded ? $sized : null };
|
|
350
|
+
}
|
|
299
351
|
},
|
|
300
352
|
"commands.after": function (this: any, cmd: string) {
|
|
301
353
|
if (cmd === "fullscreen" && !this.fullscreen.isActive()) {
|
|
302
354
|
this.$tb.css("z-index", "");
|
|
303
355
|
}
|
|
356
|
+
if (cmd === "imageCaption" && captionWidthRef.current) {
|
|
357
|
+
const { img, width, $seeded } = captionWidthRef.current;
|
|
358
|
+
$seeded?.removeAttr("width");
|
|
359
|
+
const caption = img.closest(".fr-img-caption");
|
|
360
|
+
if (caption) caption.style.width = width;
|
|
361
|
+
img.style.removeProperty("width");
|
|
362
|
+
if (!img.getAttribute("style")) img.removeAttribute("style");
|
|
363
|
+
captionWidthRef.current = null;
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
"image.resizeEnd": () => {
|
|
367
|
+
const lifted = liftedCaptionRef.current;
|
|
368
|
+
if (!lifted) return;
|
|
369
|
+
lifted.nodes[0].before(lifted.paragraph);
|
|
370
|
+
lifted.paragraph.append(...lifted.nodes);
|
|
371
|
+
liftedCaptionRef.current = null;
|
|
304
372
|
},
|
|
305
373
|
// Froala's shortcut dispatcher calls plugin commands unbound, so its own
|
|
306
374
|
// Ctrl/Cmd+K throws instead of opening anything. Returning false cancels
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
|
|
3
|
-
import { FieldsBehavior, Modal } from "@ax/components";
|
|
4
|
-
import type { IModal } from "@ax/types";
|
|
5
|
-
|
|
6
3
|
import type { ExportFormat } from "@griddo/api-types";
|
|
7
|
-
|
|
4
|
+
import type { IModal } from "@ax/types";
|
|
5
|
+
import { FieldsBehavior, Modal } from "@ax/components";
|
|
8
6
|
import type { IDownloadFormState } from "..";
|
|
9
7
|
|
|
10
8
|
import * as S from "./style";
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { connect } from "react-redux";
|
|
3
|
+
import { format, subDays } from "date-fns";
|
|
3
4
|
|
|
5
|
+
import type { ExportFormat, LogActivityExportRequest, LogActivityPaginationRequest } from "@griddo/api-types";
|
|
6
|
+
import type { ILogItemsByDay, ILogItemsByUser, IQueryValue, IRootState, IStructuredData } from "@ax/types";
|
|
4
7
|
import { EmptyState, ErrorToast, FilterTagsBar, MainWrapper, SearchTagsBar, Select, TableList } from "@ax/components";
|
|
5
8
|
import { activityLogActions } from "@ax/containers/ActivityLog";
|
|
6
9
|
import { structuredDataActions } from "@ax/containers/StructuredData";
|
|
7
10
|
import { useModal } from "@ax/hooks";
|
|
8
|
-
import type { ILogItemsByDay, ILogItemsByUser, IQueryValue, IRootState, IStructuredData } from "@ax/types";
|
|
9
11
|
|
|
10
|
-
import type { ExportFormat, LogActivityExportRequest, LogActivityPaginationRequest } from "@griddo/api-types";
|
|
11
|
-
import { format, subDays } from "date-fns";
|
|
12
|
-
|
|
13
|
-
import DownloadModal from "./DownloadModal";
|
|
14
12
|
import { useFilterQuery } from "./hooks";
|
|
13
|
+
import TableHeader from "./TableHeader";
|
|
15
14
|
import ItemLog from "./ItemLog";
|
|
16
15
|
import ItemLogUser from "./ItemLogUser";
|
|
17
|
-
import
|
|
16
|
+
import DownloadModal from "./DownloadModal";
|
|
18
17
|
|
|
19
18
|
import * as S from "./style";
|
|
20
19
|
|