@squiz/formatted-text-editor 2.0.1 → 2.1.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/CHANGELOG.md +6 -0
- package/demo/AppContext.tsx +40 -17
- package/jest.config.ts +1 -1
- package/jest.setup.ts +16 -0
- package/lib/EditorToolbar/Toolbar.js +3 -1
- package/lib/EditorToolbar/Tools/ContentTools/ContentToolsDropdown.d.ts +3 -0
- package/lib/EditorToolbar/Tools/ContentTools/ContentToolsDropdown.js +35 -0
- package/lib/EditorToolbar/Tools/Table/TableButton.js +1 -3
- package/lib/Icons/AiIcon.d.ts +2 -0
- package/lib/Icons/AiIcon.js +60 -0
- package/lib/index.css +4224 -0
- package/package.json +3 -1
- package/src/EditorToolbar/Toolbar.tsx +2 -0
- package/src/EditorToolbar/Tools/ContentTools/ContentToolsDropdown.spec.tsx +78 -0
- package/src/EditorToolbar/Tools/ContentTools/ContentToolsDropdown.tsx +46 -0
- package/src/EditorToolbar/Tools/ContentTools/_content-tools.scss +45 -0
- package/src/EditorToolbar/Tools/Table/TableButton.tsx +0 -2
- package/src/Icons/AiIcon.tsx +140 -0
- package/src/index.scss +4 -0
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@squiz/formatted-text-editor",
|
3
|
-
"version": "2.0
|
3
|
+
"version": "2.1.0",
|
4
4
|
"main": "lib/index.js",
|
5
5
|
"types": "lib/index.d.ts",
|
6
6
|
"private": false,
|
@@ -25,8 +25,10 @@
|
|
25
25
|
"@remirror/extension-react-tables": "^2.2.19",
|
26
26
|
"@remirror/react": "2.0.35",
|
27
27
|
"@squiz/dx-json-schema-lib": "^1.65.1",
|
28
|
+
"@squiz/dxp-ai-client-react": "^0.1.3-alpha",
|
28
29
|
"@squiz/matrix-resource-browser-plugin": "^2.0.0",
|
29
30
|
"@squiz/resource-browser": "^2.0.0",
|
31
|
+
"@squiz/sds": "^1.0.0-alpha.50",
|
30
32
|
"clsx": "2.1.1",
|
31
33
|
"react-hook-form": "7.51.4",
|
32
34
|
"react-image-size": "2.0.0",
|
@@ -15,6 +15,7 @@ import ClearFormattingButton from './Tools/ClearFormatting/ClearFormattingButton
|
|
15
15
|
import ListButtons from './Tools/Lists/ListButtons';
|
16
16
|
import HorizontalLineButton from './Tools/HorizontalLine/HorizontalLineButton';
|
17
17
|
import TableButton from './Tools/Table/TableButton';
|
18
|
+
import ContentToolsDropdown from './Tools/ContentTools/ContentToolsDropdown';
|
18
19
|
import { useExtensionNames } from '../hooks';
|
19
20
|
|
20
21
|
type ToolbarProps = {
|
@@ -54,6 +55,7 @@ export const Toolbar = ({ isVisible, enableTableTool }: ToolbarProps) => {
|
|
54
55
|
{extensionNames.image && <ImageButton />}
|
55
56
|
{extensionNames.clearFormatting && <ClearFormattingButton />}
|
56
57
|
{enableTableTool && extensionNames.table && <TableButton />}
|
58
|
+
<ContentToolsDropdown />
|
57
59
|
</div>
|
58
60
|
</RemirrorToolbar>
|
59
61
|
);
|
@@ -0,0 +1,78 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import { fireEvent } from '@testing-library/react';
|
3
|
+
import React from 'react';
|
4
|
+
import { useAiService } from '@squiz/dxp-ai-client-react';
|
5
|
+
import ContentToolsDropdown from './ContentToolsDropdown';
|
6
|
+
import { renderWithContext } from '../../../../tests';
|
7
|
+
|
8
|
+
// Mock the module
|
9
|
+
jest.mock('@squiz/dxp-ai-client-react', () => ({
|
10
|
+
useAiService: jest.fn(),
|
11
|
+
}));
|
12
|
+
|
13
|
+
// Cast useAiService to a Jest mock
|
14
|
+
const mockedUseAiService = useAiService as jest.MockedFunction<typeof useAiService>;
|
15
|
+
|
16
|
+
describe('Content tools dropdown', () => {
|
17
|
+
it('should not render the content tools dropdown component when there are no content tools', () => {
|
18
|
+
mockedUseAiService.mockReturnValue({
|
19
|
+
contentTools: [],
|
20
|
+
} as any);
|
21
|
+
|
22
|
+
const { baseElement } = renderWithContext(<ContentToolsDropdown />);
|
23
|
+
|
24
|
+
expect(baseElement).toBeTruthy();
|
25
|
+
expect(baseElement.querySelector('.sds-dropdown.active')).toBeFalsy();
|
26
|
+
});
|
27
|
+
|
28
|
+
it('should render the content tools dropdown when there are content tools', () => {
|
29
|
+
mockedUseAiService.mockReturnValue({
|
30
|
+
contentTools: [
|
31
|
+
{
|
32
|
+
id: 'unique-id',
|
33
|
+
name: 'Testing content tools',
|
34
|
+
},
|
35
|
+
],
|
36
|
+
} as any);
|
37
|
+
|
38
|
+
const { baseElement } = renderWithContext(<ContentToolsDropdown />);
|
39
|
+
|
40
|
+
expect(baseElement).toBeTruthy();
|
41
|
+
expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeFalsy();
|
42
|
+
|
43
|
+
const launchButton = baseElement.querySelector(`.sds-button`) as HTMLButtonElement;
|
44
|
+
expect(launchButton).toBeTruthy();
|
45
|
+
|
46
|
+
fireEvent.click(launchButton);
|
47
|
+
expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeTruthy();
|
48
|
+
});
|
49
|
+
|
50
|
+
it('should handle when a content tool is clicked', () => {
|
51
|
+
mockedUseAiService.mockReturnValue({
|
52
|
+
contentTools: [
|
53
|
+
{
|
54
|
+
id: 'unique-id',
|
55
|
+
name: 'Testing content tools',
|
56
|
+
},
|
57
|
+
],
|
58
|
+
} as any);
|
59
|
+
|
60
|
+
const { baseElement } = renderWithContext(<ContentToolsDropdown />);
|
61
|
+
|
62
|
+
expect(baseElement).toBeTruthy();
|
63
|
+
expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeFalsy();
|
64
|
+
|
65
|
+
const launchButton = baseElement.querySelector(`.sds-button`) as HTMLButtonElement;
|
66
|
+
expect(launchButton).toBeTruthy();
|
67
|
+
|
68
|
+
fireEvent.click(launchButton);
|
69
|
+
expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeTruthy();
|
70
|
+
|
71
|
+
const actionItems = baseElement.querySelectorAll(`.sds-dropdown-button`);
|
72
|
+
const firstItem = actionItems[0] as HTMLButtonElement;
|
73
|
+
expect(firstItem.textContent).toBe('Testing content tools');
|
74
|
+
|
75
|
+
fireEvent.click(firstItem);
|
76
|
+
expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeFalsy();
|
77
|
+
});
|
78
|
+
});
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { ICON_AI } from '../../../Icons/AiIcon';
|
3
|
+
import { BUTTON_FORMAT_TRANSPARENT, BUTTON_THEME_DEFAULT, DROPDOWN_POSITION_RIGHT, Dropdown } from '@squiz/sds';
|
4
|
+
import { useAiService } from '@squiz/dxp-ai-client-react';
|
5
|
+
import { VerticalDivider } from '@remirror/react';
|
6
|
+
|
7
|
+
const ContentToolsDropdown = () => {
|
8
|
+
const { contentTools } = useAiService();
|
9
|
+
|
10
|
+
const dropdownItems = contentTools.map((item) => ({
|
11
|
+
items: [
|
12
|
+
{
|
13
|
+
action: () => alert(JSON.stringify(item, null, 2)),
|
14
|
+
key: item.id,
|
15
|
+
label: <span>{item.name}</span>,
|
16
|
+
},
|
17
|
+
],
|
18
|
+
key: item.id,
|
19
|
+
}));
|
20
|
+
|
21
|
+
// No content tools to show, don't show dropdown at all
|
22
|
+
if (contentTools.length === 0) {
|
23
|
+
return null;
|
24
|
+
}
|
25
|
+
|
26
|
+
return (
|
27
|
+
<>
|
28
|
+
<VerticalDivider />
|
29
|
+
<Dropdown
|
30
|
+
title="Content tools"
|
31
|
+
aria-label="Content tools"
|
32
|
+
buttonProps={{
|
33
|
+
format: BUTTON_FORMAT_TRANSPARENT,
|
34
|
+
icon: ICON_AI,
|
35
|
+
theme: BUTTON_THEME_DEFAULT,
|
36
|
+
}}
|
37
|
+
className="content-tools-dropdown"
|
38
|
+
dropdownPosition={DROPDOWN_POSITION_RIGHT}
|
39
|
+
heading={'Rewrite to...'}
|
40
|
+
sections={dropdownItems}
|
41
|
+
/>
|
42
|
+
</>
|
43
|
+
);
|
44
|
+
};
|
45
|
+
|
46
|
+
export default ContentToolsDropdown;
|
@@ -0,0 +1,45 @@
|
|
1
|
+
.content-tools-dropdown {
|
2
|
+
.sds-button-icon svg {
|
3
|
+
fill: none !important;
|
4
|
+
}
|
5
|
+
|
6
|
+
.sds-dropdown {
|
7
|
+
left: auto;
|
8
|
+
right: auto;
|
9
|
+
top: auto;
|
10
|
+
|
11
|
+
width: 188px;
|
12
|
+
margin-top: 0.45rem;
|
13
|
+
margin-left: -2rem;
|
14
|
+
|
15
|
+
.sds-dropdown-heading {
|
16
|
+
font-size: 0.8125rem;
|
17
|
+
font-weight: 600;
|
18
|
+
letter-spacing: 0;
|
19
|
+
line-height: 1rem;
|
20
|
+
background-color: #f5f5f5;
|
21
|
+
border-radius: 0.25rem;
|
22
|
+
padding: 0.25rem 0.5rem;
|
23
|
+
pointer-events: none;
|
24
|
+
text-transform: none;
|
25
|
+
}
|
26
|
+
|
27
|
+
.sds-dropdown-section {
|
28
|
+
border-top: 1px solid #ededed;
|
29
|
+
border-bottom: none;
|
30
|
+
padding-top: 0.25rem;
|
31
|
+
margin-bottom: 0;
|
32
|
+
|
33
|
+
.sds-dropdown-label {
|
34
|
+
font-size: 0.8125rem;
|
35
|
+
color: #4f4f4f;
|
36
|
+
line-height: 1rem;
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
&.active {
|
41
|
+
display: block;
|
42
|
+
padding: 0.25rem;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
}
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { useCommands, useActive } from '@remirror/react';
|
3
|
-
import { VerticalDivider } from '@remirror/react-components';
|
4
3
|
import { TableExtension } from '@remirror/extension-react-tables';
|
5
4
|
import Button from '../../../ui/Button/Button';
|
6
5
|
import TableViewRoundedIcon from '@mui/icons-material/TableViewRounded';
|
@@ -24,7 +23,6 @@ const TableButton = () => {
|
|
24
23
|
icon={<TableViewRoundedIcon />}
|
25
24
|
label="Insert table"
|
26
25
|
/>
|
27
|
-
<VerticalDivider />
|
28
26
|
</>
|
29
27
|
);
|
30
28
|
};
|
@@ -0,0 +1,140 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
export const ICON_AI = (
|
4
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
5
|
+
<g id="sq-AI-icon" clipPath="url(#clip0_3060_44545)">
|
6
|
+
<g id="circle" filter="url(#filter0_dd_3060_44545)">
|
7
|
+
<rect x="2" y="2" width="20" height="20" rx="10" fill="url(#paint0_linear_3060_44545)" />
|
8
|
+
<rect x="1.5" y="1.5" width="21" height="21" rx="10.5" stroke="white" />
|
9
|
+
</g>
|
10
|
+
<g id="Vector" filter="url(#filter1_d_3060_44545)">
|
11
|
+
<path
|
12
|
+
d="M11.2084 9.79157L10.1219 7.39977C9.88269 6.86674 9.11731 6.86674 8.87813 7.39977L7.79157 9.79157L5.39977 10.8781C4.86674 11.1241 4.86674 11.8827 5.39977 12.1219L7.79157 13.2084L8.87813 15.6002C9.12415 16.1333 9.88269 16.1333 10.1219 15.6002L11.2084 13.2084L13.6002 12.1219C14.1333 11.8759 14.1333 11.1173 13.6002 10.8781L11.2084 9.79157Z"
|
13
|
+
fill="white"
|
14
|
+
/>
|
15
|
+
</g>
|
16
|
+
<g id="Vector_2" opacity="0.8" filter="url(#filter2_d_3060_44545)">
|
17
|
+
<path
|
18
|
+
d="M17.4491 10.4491L16.8493 11.7779C16.7126 12.074 16.2874 12.074 16.1507 11.7779L15.5509 10.4415L14.2221 9.84169C13.926 9.70501 13.926 9.2874 14.2221 9.15072L15.5585 8.55087L16.1583 7.2221C16.295 6.92597 16.7126 6.92597 16.8493 7.2221L17.4491 8.55847L18.7779 9.15831C19.074 9.29499 19.074 9.7126 18.7779 9.84928L17.4491 10.4491Z"
|
19
|
+
fill="white"
|
20
|
+
/>
|
21
|
+
</g>
|
22
|
+
<g id="Ellipse 55" opacity="0.8" filter="url(#filter3_d_3060_44545)">
|
23
|
+
<circle cx="14.5" cy="16.5" r="1.5" fill="white" />
|
24
|
+
</g>
|
25
|
+
</g>
|
26
|
+
<defs>
|
27
|
+
<filter
|
28
|
+
id="filter0_dd_3060_44545"
|
29
|
+
x="0"
|
30
|
+
y="0"
|
31
|
+
width="24"
|
32
|
+
height="24"
|
33
|
+
filterUnits="userSpaceOnUse"
|
34
|
+
colorInterpolationFilters="sRGB"
|
35
|
+
>
|
36
|
+
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
37
|
+
<feColorMatrix
|
38
|
+
in="SourceAlpha"
|
39
|
+
type="matrix"
|
40
|
+
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
41
|
+
result="hardAlpha"
|
42
|
+
/>
|
43
|
+
<feOffset dx="1" dy="1" />
|
44
|
+
<feComposite in2="hardAlpha" operator="out" />
|
45
|
+
<feColorMatrix type="matrix" values="0 0 0 0 0.2 0 0 0 0 0.709941 0 0 0 0 1 0 0 0 0.64 0" />
|
46
|
+
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3060_44545" />
|
47
|
+
<feColorMatrix
|
48
|
+
in="SourceAlpha"
|
49
|
+
type="matrix"
|
50
|
+
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
51
|
+
result="hardAlpha"
|
52
|
+
/>
|
53
|
+
<feOffset dx="-1" dy="-1" />
|
54
|
+
<feComposite in2="hardAlpha" operator="out" />
|
55
|
+
<feColorMatrix type="matrix" values="0 0 0 0 0.96 0 0 0 0 0.76 0 0 0 0 0.48 0 0 0 0.64 0" />
|
56
|
+
<feBlend mode="normal" in2="effect1_dropShadow_3060_44545" result="effect2_dropShadow_3060_44545" />
|
57
|
+
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_3060_44545" result="shape" />
|
58
|
+
</filter>
|
59
|
+
<filter
|
60
|
+
id="filter1_d_3060_44545"
|
61
|
+
x="5"
|
62
|
+
y="7"
|
63
|
+
width="9"
|
64
|
+
height="10"
|
65
|
+
filterUnits="userSpaceOnUse"
|
66
|
+
colorInterpolationFilters="sRGB"
|
67
|
+
>
|
68
|
+
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
69
|
+
<feColorMatrix
|
70
|
+
in="SourceAlpha"
|
71
|
+
type="matrix"
|
72
|
+
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
73
|
+
result="hardAlpha"
|
74
|
+
/>
|
75
|
+
<feOffset dy="1" />
|
76
|
+
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" />
|
77
|
+
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3060_44545" />
|
78
|
+
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3060_44545" result="shape" />
|
79
|
+
</filter>
|
80
|
+
<filter
|
81
|
+
id="filter2_d_3060_44545"
|
82
|
+
x="14"
|
83
|
+
y="7"
|
84
|
+
width="5"
|
85
|
+
height="6.30682"
|
86
|
+
filterUnits="userSpaceOnUse"
|
87
|
+
colorInterpolationFilters="sRGB"
|
88
|
+
>
|
89
|
+
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
90
|
+
<feColorMatrix
|
91
|
+
in="SourceAlpha"
|
92
|
+
type="matrix"
|
93
|
+
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
94
|
+
result="hardAlpha"
|
95
|
+
/>
|
96
|
+
<feOffset dy="1.30682" />
|
97
|
+
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" />
|
98
|
+
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3060_44545" />
|
99
|
+
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3060_44545" result="shape" />
|
100
|
+
</filter>
|
101
|
+
<filter
|
102
|
+
id="filter3_d_3060_44545"
|
103
|
+
x="13"
|
104
|
+
y="15"
|
105
|
+
width="3"
|
106
|
+
height="4.32373"
|
107
|
+
filterUnits="userSpaceOnUse"
|
108
|
+
colorInterpolationFilters="sRGB"
|
109
|
+
>
|
110
|
+
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
111
|
+
<feColorMatrix
|
112
|
+
in="SourceAlpha"
|
113
|
+
type="matrix"
|
114
|
+
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
115
|
+
result="hardAlpha"
|
116
|
+
/>
|
117
|
+
<feOffset dy="1.32373" />
|
118
|
+
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" />
|
119
|
+
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3060_44545" />
|
120
|
+
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3060_44545" result="shape" />
|
121
|
+
</filter>
|
122
|
+
<linearGradient
|
123
|
+
id="paint0_linear_3060_44545"
|
124
|
+
x1="6.54545"
|
125
|
+
y1="3.81818"
|
126
|
+
x2="17"
|
127
|
+
y2="20.1818"
|
128
|
+
gradientUnits="userSpaceOnUse"
|
129
|
+
>
|
130
|
+
<stop stopColor="#F5D6AB" />
|
131
|
+
<stop offset="0.251162" stopColor="#F895A7" />
|
132
|
+
<stop offset="0.585" stopColor="#77A1F1" />
|
133
|
+
<stop offset="0.861262" stopColor="#68C8FF" />
|
134
|
+
</linearGradient>
|
135
|
+
<clipPath id="clip0_3060_44545">
|
136
|
+
<rect width="24" height="24" fill="white" />
|
137
|
+
</clipPath>
|
138
|
+
</defs>
|
139
|
+
</svg>
|
140
|
+
);
|
package/src/index.scss
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
@import 'tailwindcss/components';
|
4
4
|
@import 'tailwindcss/utilities';
|
5
5
|
|
6
|
+
@import '@squiz/sds/lib/package.css';
|
7
|
+
|
6
8
|
/* So we can use icons inside of FTE content */
|
7
9
|
@import 'https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded';
|
8
10
|
|
@@ -22,3 +24,5 @@
|
|
22
24
|
@import './ui/CollapseBox/collapseBox';
|
23
25
|
|
24
26
|
@import './ui/Modal/modal';
|
27
|
+
|
28
|
+
@import './EditorToolbar/Tools/ContentTools/content-tools';
|