PluginProbe
Extendify / 3.1.4
Extendify v3.1.4
3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 0.7.0 All 126 releases
extendify / src / Draft / components / InsertMenu.jsx

InsertMenu.jsx in Extendify 3.1.4, at src/Draft/components/InsertMenu.jsx

261 lines 7.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useContentHighlight } from '@draft/hooks/useContentHighlight';
2 import { store as blockEditorStore } from '@wordpress/block-editor';
3 import { createBlock, pasteHandler } from '@wordpress/blocks';
4 import {
5 __experimentalDivider as Divider,
6 MenuGroup,
7 MenuItem,
8 } from '@wordpress/components';
9 import { dispatch, select, useDispatch, useSelect } from '@wordpress/data';
10 import { useEffect } from '@wordpress/element';
11 import { __, isRTL } from '@wordpress/i18n';
12 import {
13 addSubmenu,
14 Icon,
15 insertAfter,
16 replace,
17 rotateLeft,
18 trash,
19 } from '@wordpress/icons';
20
21 export const InsertMenu = ({
22 prompt,
23 completion,
24 loading,
25 setPrompt,
26 setInputText,
27 }) => {
28 const { toggleHighlight, toggleInsertionPoint } = useContentHighlight();
29 const { insertBlocks, replaceBlocks } = useDispatch(blockEditorStore);
30 const {
31 getSelectedBlock,
32 getSelectedBlockClientIds,
33 getBlockRootClientId,
34 getBlockIndex,
35 getBlock,
36 } = useSelect((select) => select(blockEditorStore), []);
37 const selectedBlock = getSelectedBlock();
38 const selectedBlockIds = getSelectedBlockClientIds();
39
40 const canReplaceContent = () => {
41 const firstBlock = selectedBlock
42 ? selectedBlock
43 : getBlock(selectedBlockIds[0]);
44 if (!firstBlock) return false;
45 // If it's a header or a p, we can replace the content
46 // TODO: support more?
47 const unsupported = ['core/list-item', 'core/button'];
48 if (unsupported.includes(firstBlock?.name)) {
49 // Can we support the same block?
50 const blocks = plainTextToBlocks(completion);
51 return blocks[0]?.name === firstBlock?.name;
52 }
53 return true;
54 };
55
56 const canInsertAfter = () => {
57 const firstBlock = selectedBlock
58 ? selectedBlock
59 : getBlock(selectedBlockIds[0]);
60 if (!firstBlock) return true;
61 // TODO: more? or should we go up to the parent?
62 const unsupported = ['core/list-item', 'core/button'];
63 return !unsupported.includes(firstBlock?.name);
64 };
65
66 const plainTextToBlocks = (plainText) => {
67 const blocks = pasteHandler({ plainText: plainText });
68 if (!Array.isArray(blocks)) {
69 return [createBlock('core/paragraph', { content: blocks })];
70 }
71 return blocks;
72 };
73
74 const insertCompletion = async ({ replaceContent = false, position }) => {
75 setPrompt({ text: '', promptType: '', systemMessageKey: '' });
76
77 const targetBlockId = selectedBlock
78 ? selectedBlock?.clientId
79 : selectedBlockIds[0];
80 const targetBlock = getBlock(targetBlockId);
81
82 const renderingModes =
83 select('core/preferences').get('core', 'renderingModes') || {};
84 const currentTheme = select('core').getCurrentTheme()?.stylesheet;
85 const isTemplateShown =
86 renderingModes?.[currentTheme]?.page === 'template-locked';
87
88 const { set: setPreference } = dispatch('core/preferences');
89 const setRenderingMode = (mode) =>
90 setPreference('core', 'renderingModes', {
91 ...renderingModes,
92 [currentTheme]: { ...(renderingModes[currentTheme] || {}), page: mode },
93 });
94
95 const blocks = plainTextToBlocks(completion);
96 try {
97 if (!targetBlockId || position === 'end') {
98 if (isTemplateShown) {
99 setRenderingMode('post-only');
100 await new Promise((resolve) => requestAnimationFrame(resolve));
101 }
102
103 insertBlocks(blocks);
104 return;
105 }
106
107 if (position === 'top') {
108 if (isTemplateShown) {
109 setRenderingMode('post-only');
110 await new Promise((resolve) => requestAnimationFrame(resolve));
111 }
112
113 insertBlocks(blocks, 0);
114 return;
115 }
116 } finally {
117 if (isTemplateShown) setRenderingMode('template-locked');
118 }
119
120 const targetIsEmpty = targetBlock?.attributes?.content === '';
121 const parentBlockId = getBlockRootClientId(targetBlockId);
122 const blockIndex = getBlockIndex(selectedBlockIds.at(-1), parentBlockId);
123 if (!replaceContent && !targetIsEmpty) {
124 // Multiple blocks are selected, insert after
125 insertBlocks(blocks, blockIndex + 1, parentBlockId);
126 return;
127 }
128
129 const bothHaveContent = (one, two) =>
130 Object.hasOwn(one?.attributes, 'content') &&
131 Object.hasOwn(two?.attributes, 'content');
132 // If both have content, and it's only one block, they can be merged
133 const mergeable =
134 blocks.length === 1 && bothHaveContent(targetBlock, blocks[0]);
135
136 // Apply formatting to all the blocks
137 const formattedBlocks = blocks.map((incomingBlock) => ({
138 ...incomingBlock,
139 name: mergeable ? targetBlock.name : incomingBlock.name,
140 attributes: {
141 ...targetBlock.attributes,
142 content:
143 // If they both have content, they can merge and give it to the incoing block
144 // otherwise just default to the existing block content
145 bothHaveContent(incomingBlock, targetBlock)
146 ? incomingBlock?.attributes?.content
147 : incomingBlock?.attributes?.content,
148 },
149 }));
150
151 // TODO: some blocks are harder to replace, like list items
152 // Should we climb up to the parent in this case?
153 // See notes in canReplaceContent() above
154 replaceBlocks(selectedBlockIds, formattedBlocks);
155 };
156
157 const discard = () => {
158 setInputText('');
159 setPrompt({ text: '', promptType: '', systemMessageKey: '' });
160 };
161
162 const retry = () => {
163 setInputText('');
164 setPrompt({ text: '', promptType: '', systemMessageKey: '' });
165 setTimeout(() => setPrompt(prompt));
166 };
167
168 useEffect(() => {
169 return () => {
170 toggleHighlight(selectedBlockIds, { isHighlighted: false });
171 };
172 }, [selectedBlockIds, toggleHighlight]);
173
174 return (
175 <MenuGroup>
176 <MenuItem
177 onClick={() => insertCompletion({ replaceContent: true })}
178 onMouseEnter={() =>
179 toggleHighlight(selectedBlockIds, {
180 isHighlighted: true,
181 })
182 }
183 onMouseLeave={() =>
184 toggleHighlight(selectedBlockIds, {
185 isHighlighted: false,
186 })
187 }
188 disabled={loading || !canReplaceContent()}
189 icon={replace}
190 iconPosition="left"
191 data-test="replace-selected"
192 className="h-auto min-h-10 items-start"
193 >
194 <span className="whitespace-normal break-words text-start">
195 {__('Replace selected block text', 'extendify-local')}
196 </span>
197 </MenuItem>
198 <MenuItem
199 onClick={() =>
200 insertCompletion({ replaceContent: false, position: 'top' })
201 }
202 disabled={loading}
203 iconPosition="left"
204 data-test="insert-top"
205 className="h-auto min-h-10 items-start"
206 >
207 <div className={isRTL() ? '-mr-1' : '-ml-1'}>
208 <Icon icon={addSubmenu} className="rotate-180" />
209 </div>
210 <div className="whitespace-normal break-words px-1 text-start">
211 {__('Insert at top', 'extendify-local')}
212 </div>
213 </MenuItem>
214 <MenuItem
215 onClick={() => insertCompletion({ replaceContent: false })}
216 onMouseEnter={() => toggleInsertionPoint(true)}
217 onMouseLeave={() => toggleInsertionPoint(false)}
218 disabled={loading || !canInsertAfter()}
219 icon={insertAfter}
220 iconPosition="left"
221 data-test="insert-after"
222 className="h-auto min-h-10 items-start"
223 >
224 <span className="whitespace-normal break-words text-start">
225 {__('Insert after the selected text', 'extendify-local')}
226 </span>
227 </MenuItem>
228 <MenuItem
229 onClick={() =>
230 insertCompletion({ replaceContent: false, position: 'end' })
231 }
232 disabled={loading}
233 icon={addSubmenu}
234 iconPosition="left"
235 data-test="insert-bottom"
236 >
237 {__('Insert at bottom', 'extendify-local')}
238 </MenuItem>
239 <Divider />
240 <MenuItem
241 onClick={retry}
242 disabled={loading}
243 icon={rotateLeft}
244 iconPosition="left"
245 data-test="try-again-button"
246 >
247 {__('Try again', 'extendify-local')}
248 </MenuItem>
249 <MenuItem
250 onClick={discard}
251 disabled={loading}
252 icon={trash}
253 iconPosition="left"
254 data-test="discard-button"
255 >
256 {__('Discard', 'extendify-local')}
257 </MenuItem>
258 </MenuGroup>
259 );
260 };
261