@griddo/ax 11.12.1-rc.6 → 11.12.1-rc.8
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/package.json +2 -2
- package/src/__tests__/components/Fields/IntegrationsField/IntegrationsField.test.tsx +1 -1
- package/src/__tests__/components/Fields/UrlField/UrlField.test.tsx +19 -1
- package/src/__tests__/hooks/modals.test.tsx +303 -0
- package/src/components/FloatingPanel/index.tsx +4 -3
- package/src/components/HeadingsPreviewModal/index.tsx +1 -0
- package/src/components/Modal/index.tsx +1 -0
- package/src/hooks/modals.tsx +9 -6
- package/src/modules/FramePreview/HeadingsOverlay/index.tsx +3 -3
- package/src/modules/FramePreview/utils.tsx +4 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@griddo/ax",
|
|
3
3
|
"description": "Griddo Author Experience",
|
|
4
|
-
"version": "11.12.1-rc.
|
|
4
|
+
"version": "11.12.1-rc.8",
|
|
5
5
|
"authors": [
|
|
6
6
|
"Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
|
|
7
7
|
"Diego M. Béjar <diego.bejar@secuoyas.com>",
|
|
@@ -217,5 +217,5 @@
|
|
|
217
217
|
"publishConfig": {
|
|
218
218
|
"access": "public"
|
|
219
219
|
},
|
|
220
|
-
"gitHead": "
|
|
220
|
+
"gitHead": "dd8aea4aa1eeea915e2b432f15bded59ff906892"
|
|
221
221
|
}
|
|
@@ -312,7 +312,7 @@ describe("Events", () => {
|
|
|
312
312
|
fireEvent.click(actionMenu[1]);
|
|
313
313
|
|
|
314
314
|
const buttonsDefault = screen.getAllByTestId("button-default");
|
|
315
|
-
fireEvent.click(buttonsDefault[
|
|
315
|
+
fireEvent.click(buttonsDefault[buttonsDefault.length - 1]);
|
|
316
316
|
|
|
317
317
|
const integrationsWithDeletion = initialStore.integrations.integrations.slice(0, 3);
|
|
318
318
|
|
|
@@ -290,6 +290,12 @@ describe("UrlField component rendering", () => {
|
|
|
290
290
|
render(Component, { store });
|
|
291
291
|
});
|
|
292
292
|
|
|
293
|
+
// Open FloatingPanel first
|
|
294
|
+
const pageField = screen.getByTestId("page-field");
|
|
295
|
+
await act(async () => {
|
|
296
|
+
fireEvent.click(pageField);
|
|
297
|
+
});
|
|
298
|
+
|
|
293
299
|
const selectComponents = screen.getAllByTestId("select-component");
|
|
294
300
|
|
|
295
301
|
await act(async () => {
|
|
@@ -384,6 +390,12 @@ describe("onClick events", () => {
|
|
|
384
390
|
render(Component, { store });
|
|
385
391
|
});
|
|
386
392
|
|
|
393
|
+
// Open FloatingPanel first
|
|
394
|
+
const pageField = screen.getByTestId("page-field");
|
|
395
|
+
await act(async () => {
|
|
396
|
+
fireEvent.click(pageField);
|
|
397
|
+
});
|
|
398
|
+
|
|
387
399
|
await act(async () => {
|
|
388
400
|
const selectionListItem = screen.getAllByTestId("selection-list-item");
|
|
389
401
|
selectionListItem[0].click();
|
|
@@ -430,10 +442,16 @@ describe("onClick events", () => {
|
|
|
430
442
|
render(Component, { store });
|
|
431
443
|
});
|
|
432
444
|
|
|
433
|
-
const selectionListItem = screen.getAllByTestId("selection-list-item");
|
|
434
445
|
const checkFieldInputs = screen.getAllByTestId("check-field-input");
|
|
435
446
|
const pageField = screen.getByTestId("page-field");
|
|
436
447
|
|
|
448
|
+
// Open FloatingPanel first
|
|
449
|
+
await act(async () => {
|
|
450
|
+
fireEvent.click(pageField);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const selectionListItem = screen.getAllByTestId("selection-list-item");
|
|
454
|
+
|
|
437
455
|
await act(async () => {
|
|
438
456
|
selectionListItem[0].click();
|
|
439
457
|
fireEvent.click(checkFieldInputs[0]);
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import "@testing-library/jest-dom";
|
|
3
|
+
|
|
4
|
+
import { useModal, useModals } from "@ax/hooks";
|
|
5
|
+
|
|
6
|
+
describe("useModal hook", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
document.body.classList.remove("modal-open");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
document.body.classList.remove("modal-open");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should initialize with false when no initialState provided", () => {
|
|
16
|
+
const { result } = renderHook(() => useModal());
|
|
17
|
+
expect(result.current.isOpen).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should initialize with provided initialState", () => {
|
|
21
|
+
const { result } = renderHook(() => useModal(true));
|
|
22
|
+
expect(result.current.isOpen).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should toggle modal state when toggleModal is called", () => {
|
|
26
|
+
const { result } = renderHook(() => useModal(false));
|
|
27
|
+
expect(result.current.isOpen).toBe(false);
|
|
28
|
+
|
|
29
|
+
act(() => {
|
|
30
|
+
result.current.toggleModal();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(result.current.isOpen).toBe(true);
|
|
34
|
+
|
|
35
|
+
act(() => {
|
|
36
|
+
result.current.toggleModal();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(result.current.isOpen).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should add modal-open class to body when modal opens and bodyBlock is true", () => {
|
|
43
|
+
const { result } = renderHook(() => useModal(false, true));
|
|
44
|
+
expect(document.body.classList.contains("modal-open")).toBe(false);
|
|
45
|
+
|
|
46
|
+
act(() => {
|
|
47
|
+
result.current.toggleModal();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(document.body.classList.contains("modal-open")).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should not add modal-open class to body when bodyBlock is false", () => {
|
|
54
|
+
const { result } = renderHook(() => useModal(false, false));
|
|
55
|
+
|
|
56
|
+
act(() => {
|
|
57
|
+
result.current.toggleModal();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(document.body.classList.contains("modal-open")).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should check if modal-open class already exists before adding", () => {
|
|
64
|
+
document.body.classList.add("modal-open");
|
|
65
|
+
|
|
66
|
+
const { result } = renderHook(() => useModal(false, true));
|
|
67
|
+
|
|
68
|
+
act(() => {
|
|
69
|
+
result.current.toggleModal();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Class should still be present
|
|
73
|
+
expect(document.body.classList.contains("modal-open")).toBe(true);
|
|
74
|
+
|
|
75
|
+
// Count of modal-open should be 1
|
|
76
|
+
const classList = [...document.body.classList];
|
|
77
|
+
const count = classList.filter((c) => c === "modal-open").length;
|
|
78
|
+
expect(count).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("useModals hook", () => {
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
document.body.classList.remove("modal-open");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
document.body.classList.remove("modal-open");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should initialize all modals as closed", () => {
|
|
92
|
+
const { result } = renderHook(() => useModals(["modal1", "modal2", "modal3"]));
|
|
93
|
+
|
|
94
|
+
expect(result.current.isOpen("modal1")).toBe(false);
|
|
95
|
+
expect(result.current.isOpen("modal2")).toBe(false);
|
|
96
|
+
expect(result.current.isOpen("modal3")).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should open a modal with openModal", () => {
|
|
100
|
+
const { result } = renderHook(() => useModals(["modal1", "modal2"]));
|
|
101
|
+
|
|
102
|
+
act(() => {
|
|
103
|
+
result.current.openModal("modal1");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(result.current.isOpen("modal1")).toBe(true);
|
|
107
|
+
expect(result.current.isOpen("modal2")).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should close a modal with closeModal", () => {
|
|
111
|
+
const { result } = renderHook(() => useModals(["modal1"]));
|
|
112
|
+
|
|
113
|
+
act(() => {
|
|
114
|
+
result.current.openModal("modal1");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(result.current.isOpen("modal1")).toBe(true);
|
|
118
|
+
|
|
119
|
+
act(() => {
|
|
120
|
+
result.current.closeModal("modal1");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.current.isOpen("modal1")).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should toggle a modal with toggleModal", () => {
|
|
127
|
+
const { result } = renderHook(() => useModals(["modal1"]));
|
|
128
|
+
|
|
129
|
+
expect(result.current.isOpen("modal1")).toBe(false);
|
|
130
|
+
|
|
131
|
+
act(() => {
|
|
132
|
+
result.current.toggleModal("modal1");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result.current.isOpen("modal1")).toBe(true);
|
|
136
|
+
|
|
137
|
+
act(() => {
|
|
138
|
+
result.current.toggleModal("modal1");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(result.current.isOpen("modal1")).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should handle multiple modals independently", () => {
|
|
145
|
+
const { result } = renderHook(() => useModals(["modal1", "modal2", "modal3"]));
|
|
146
|
+
|
|
147
|
+
act(() => {
|
|
148
|
+
result.current.openModal("modal1");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(result.current.isOpen("modal1")).toBe(true);
|
|
152
|
+
expect(result.current.isOpen("modal2")).toBe(false);
|
|
153
|
+
expect(result.current.isOpen("modal3")).toBe(false);
|
|
154
|
+
|
|
155
|
+
act(() => {
|
|
156
|
+
result.current.openModal("modal2");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(result.current.isOpen("modal1")).toBe(true);
|
|
160
|
+
expect(result.current.isOpen("modal2")).toBe(true);
|
|
161
|
+
expect(result.current.isOpen("modal3")).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should add modal-open class when a modal is opened", () => {
|
|
165
|
+
const { result } = renderHook(() => useModals(["modal1"], true));
|
|
166
|
+
|
|
167
|
+
expect(document.body.classList.contains("modal-open")).toBe(false);
|
|
168
|
+
|
|
169
|
+
act(() => {
|
|
170
|
+
result.current.openModal("modal1");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(document.body.classList.contains("modal-open")).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should remove modal-open class when all modals are closed", () => {
|
|
177
|
+
const { result } = renderHook(() => useModals(["modal1", "modal2"], true));
|
|
178
|
+
|
|
179
|
+
act(() => {
|
|
180
|
+
result.current.openModal("modal1");
|
|
181
|
+
result.current.openModal("modal2");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(document.body.classList.contains("modal-open")).toBe(true);
|
|
185
|
+
|
|
186
|
+
act(() => {
|
|
187
|
+
result.current.closeModal("modal1");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(document.body.classList.contains("modal-open")).toBe(true);
|
|
191
|
+
|
|
192
|
+
act(() => {
|
|
193
|
+
result.current.closeModal("modal2");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(document.body.classList.contains("modal-open")).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should close multiple modals individually", () => {
|
|
200
|
+
const { result } = renderHook(() => useModals(["modal1", "modal2", "modal3"]));
|
|
201
|
+
|
|
202
|
+
act(() => {
|
|
203
|
+
result.current.openModal("modal1");
|
|
204
|
+
result.current.openModal("modal2");
|
|
205
|
+
result.current.openModal("modal3");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(result.current.isOpen("modal1")).toBe(true);
|
|
209
|
+
expect(result.current.isOpen("modal2")).toBe(true);
|
|
210
|
+
expect(result.current.isOpen("modal3")).toBe(true);
|
|
211
|
+
|
|
212
|
+
act(() => {
|
|
213
|
+
result.current.closeModal("modal1");
|
|
214
|
+
result.current.closeModal("modal2");
|
|
215
|
+
result.current.closeModal("modal3");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(result.current.isOpen("modal1")).toBe(false);
|
|
219
|
+
expect(result.current.isOpen("modal2")).toBe(false);
|
|
220
|
+
expect(result.current.isOpen("modal3")).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should not add modal-open class if bodyBlock is false", () => {
|
|
224
|
+
const { result } = renderHook(() => useModals(["modal1"], false));
|
|
225
|
+
|
|
226
|
+
act(() => {
|
|
227
|
+
result.current.openModal("modal1");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(document.body.classList.contains("modal-open")).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should not duplicate modal-open class when multiple modals are open", () => {
|
|
234
|
+
const { result } = renderHook(() => useModals(["modal1", "modal2"], true));
|
|
235
|
+
|
|
236
|
+
act(() => {
|
|
237
|
+
result.current.openModal("modal1");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const classList1 = [...document.body.classList];
|
|
241
|
+
const count1 = classList1.filter((c) => c === "modal-open").length;
|
|
242
|
+
|
|
243
|
+
act(() => {
|
|
244
|
+
result.current.openModal("modal2");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const classList2 = [...document.body.classList];
|
|
248
|
+
const count2 = classList2.filter((c) => c === "modal-open").length;
|
|
249
|
+
|
|
250
|
+
expect(count1).toBe(1);
|
|
251
|
+
expect(count2).toBe(1);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("should handle isOpen callback correctly", () => {
|
|
255
|
+
const { result } = renderHook(() => useModals(["modal1"]));
|
|
256
|
+
|
|
257
|
+
act(() => {
|
|
258
|
+
result.current.openModal("modal1");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const isOpenResult = result.current.isOpen("modal1");
|
|
262
|
+
expect(isOpenResult).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("useModal and useModals working together", () => {
|
|
267
|
+
beforeEach(() => {
|
|
268
|
+
document.body.classList.remove("modal-open");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
afterEach(() => {
|
|
272
|
+
document.body.classList.remove("modal-open");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should verify both hooks add modal-open class independently", () => {
|
|
276
|
+
const { result: modalResult } = renderHook(() => useModal(false, true));
|
|
277
|
+
const { result: modalsResult } = renderHook(() => useModals(["modal1"], true));
|
|
278
|
+
|
|
279
|
+
// useModal should add class
|
|
280
|
+
act(() => {
|
|
281
|
+
modalResult.current.toggleModal();
|
|
282
|
+
});
|
|
283
|
+
expect(document.body.classList.contains("modal-open")).toBe(true);
|
|
284
|
+
|
|
285
|
+
// useModals should keep class
|
|
286
|
+
act(() => {
|
|
287
|
+
modalsResult.current.openModal("modal1");
|
|
288
|
+
});
|
|
289
|
+
expect(document.body.classList.contains("modal-open")).toBe(true);
|
|
290
|
+
|
|
291
|
+
// Closing useModal should keep class (useModals still open)
|
|
292
|
+
act(() => {
|
|
293
|
+
modalResult.current.toggleModal();
|
|
294
|
+
});
|
|
295
|
+
// Note: This depends on DOM check in useModal
|
|
296
|
+
|
|
297
|
+
// Closing useModals should remove class
|
|
298
|
+
act(() => {
|
|
299
|
+
modalsResult.current.closeModal("modal1");
|
|
300
|
+
});
|
|
301
|
+
expect(document.body.classList.contains("modal-open")).toBe(false);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { memo, useRef } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
|
|
4
|
-
import { useHandleClickOutside } from "@ax/hooks";
|
|
5
4
|
import { IconAction } from "@ax/components";
|
|
5
|
+
import { useHandleClickOutside } from "@ax/hooks";
|
|
6
6
|
|
|
7
7
|
import * as S from "./style";
|
|
8
8
|
|
|
@@ -40,9 +40,10 @@ const FloatingPanel = (props: IFloatingPanelProps): JSX.Element | null => {
|
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
return
|
|
43
|
+
return createPortal(
|
|
44
44
|
<S.Wrapper
|
|
45
45
|
data-testid="floating-panel"
|
|
46
|
+
data-is-open={isOpen}
|
|
46
47
|
ref={node}
|
|
47
48
|
isOpen={isOpen}
|
|
48
49
|
isOpenedSecond={isOpenedSecond}
|
|
@@ -60,7 +61,7 @@ const FloatingPanel = (props: IFloatingPanelProps): JSX.Element | null => {
|
|
|
60
61
|
<S.Content>{children}</S.Content>
|
|
61
62
|
</S.Wrapper>,
|
|
62
63
|
document.body,
|
|
63
|
-
)
|
|
64
|
+
);
|
|
64
65
|
};
|
|
65
66
|
|
|
66
67
|
export interface IFloatingPanelProps {
|
package/src/hooks/modals.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
const useModal = (initialState?: boolean, bodyBlock = true) => {
|
|
4
4
|
const [isOpen, setIsOpen] = useState(initialState || false);
|
|
@@ -12,10 +12,9 @@ const useModal = (initialState?: boolean, bodyBlock = true) => {
|
|
|
12
12
|
document.body.classList.add("modal-open");
|
|
13
13
|
}
|
|
14
14
|
} else if (!isOpen && bodyBlock) {
|
|
15
|
-
// Solo remover si no hay otros modales abiertos
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
if (!hasFloatingPanels && !hasModals) {
|
|
15
|
+
// Solo remover si no hay otros modales o FloatingPanels abiertos
|
|
16
|
+
const hasOpenModals = document.querySelectorAll('[data-is-open="true"]').length > 0;
|
|
17
|
+
if (!hasOpenModals) {
|
|
19
18
|
document.body.classList.remove("modal-open");
|
|
20
19
|
}
|
|
21
20
|
}
|
|
@@ -65,7 +64,11 @@ const useModals = <T extends string>(modalKeys: readonly T[], bodyBlock = true)
|
|
|
65
64
|
document.body.classList.add("modal-open");
|
|
66
65
|
}
|
|
67
66
|
} else if (!hasOpenModals) {
|
|
68
|
-
|
|
67
|
+
// Solo remover si no hay otros modales o FloatingPanels abiertos
|
|
68
|
+
const hasAnyOpenModals = document.querySelectorAll('[data-is-open="true"]').length > 0;
|
|
69
|
+
if (!hasAnyOpenModals) {
|
|
70
|
+
document.body.classList.remove("modal-open");
|
|
71
|
+
}
|
|
69
72
|
}
|
|
70
73
|
}, [openModals, bodyBlock]);
|
|
71
74
|
|
|
@@ -55,18 +55,18 @@ const HeadingsOverlay = ({ headingFilter }: IHeadingsOverlayProps) => {
|
|
|
55
55
|
|
|
56
56
|
update();
|
|
57
57
|
|
|
58
|
+
document.fonts.ready.then(update);
|
|
59
|
+
|
|
58
60
|
window.addEventListener("resize", update);
|
|
59
|
-
window.addEventListener("scroll", update, true);
|
|
60
61
|
document.addEventListener("animationend", update, true);
|
|
61
62
|
document.addEventListener("transitionend", update, true);
|
|
62
63
|
|
|
63
64
|
const observer = new MutationObserver(update);
|
|
64
|
-
observer.observe(document.body, { childList: true, subtree: true
|
|
65
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
65
66
|
|
|
66
67
|
return () => {
|
|
67
68
|
cancelAnimationFrame(rafRef.current);
|
|
68
69
|
window.removeEventListener("resize", update);
|
|
69
|
-
window.removeEventListener("scroll", update, true);
|
|
70
70
|
document.removeEventListener("animationend", update, true);
|
|
71
71
|
document.removeEventListener("transitionend", update, true);
|
|
72
72
|
observer.disconnect();
|
|
@@ -6,7 +6,10 @@ const addIdsToHeadings = (container: HTMLElement, headingFilter: string | null):
|
|
|
6
6
|
const text = heading.textContent?.trim();
|
|
7
7
|
if (!text) return;
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const id = `heading-${index + 1}`;
|
|
10
|
+
if (heading.dataset.griddoid !== id) {
|
|
11
|
+
heading.dataset.griddoid = id;
|
|
12
|
+
}
|
|
10
13
|
});
|
|
11
14
|
};
|
|
12
15
|
|