PluginProbe
Extendify / 2.2.2
Extendify v2.2.2
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 2.2.2, at src/Draft/components/InsertMenu.jsx

255 lines 7.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { store as blockEditorStore } from '@wordpress/block-editor';
2 import { createBlock, pasteHandler } from '@wordpress/blocks';
3 import {
4 MenuGroup,
5 MenuItem,
6 __experimentalDivider as Divider,
7 } from '@wordpress/components';
8 import { useDispatch, useSelect, select, dispatch } from '@wordpress/data';
9 import { useEffect } from '@wordpress/element';
10 import { __, isRTL } from '@wordpress/i18n';
11 import {
12 addSubmenu,
13 rotateLeft,
14 trash,
15 replace,
16 insertAfter,
17 Icon,
18 } from '@wordpress/icons';
19 import { useContentHighlight } from '@draft/hooks/useContentHighlight';
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) ? false : true;
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.prototype.hasOwnProperty.call(one?.attributes, 'content') &&
131 Object.prototype.hasOwnProperty.call(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 <span className="whitespace-normal break-words text-start">
194 {__('Replace selected block text', 'extendify-local')}
195 </span>
196 </MenuItem>
197 <MenuItem
198 onClick={() =>
199 insertCompletion({ replaceContent: false, position: 'top' })
200 }
201 disabled={loading}
202 iconPosition="left"
203 data-test="insert-top"
204 className="h-auto min-h-10 items-start">
205 <div className={isRTL() ? '-mr-1' : '-ml-1'}>
206 <Icon icon={addSubmenu} className="rotate-180" />
207 </div>
208 <div className="whitespace-normal break-words px-1 text-start">
209 {__('Insert at top', 'extendify-local')}
210 </div>
211 </MenuItem>
212 <MenuItem
213 onClick={() => insertCompletion({ replaceContent: false })}
214 onMouseEnter={() => toggleInsertionPoint(true)}
215 onMouseLeave={() => toggleInsertionPoint(false)}
216 disabled={loading || !canInsertAfter()}
217 icon={insertAfter}
218 iconPosition="left"
219 data-test="insert-after"
220 className="h-auto min-h-10 items-start">
221 <span className="whitespace-normal break-words text-start">
222 {__('Insert after the selected text', 'extendify-local')}
223 </span>
224 </MenuItem>
225 <MenuItem
226 onClick={() =>
227 insertCompletion({ replaceContent: false, position: 'end' })
228 }
229 disabled={loading}
230 icon={addSubmenu}
231 iconPosition="left"
232 data-test="insert-bottom">
233 {__('Insert at bottom', 'extendify-local')}
234 </MenuItem>
235 <Divider />
236 <MenuItem
237 onClick={retry}
238 disabled={loading}
239 icon={rotateLeft}
240 iconPosition="left"
241 data-test="try-again-button">
242 {__('Try again', 'extendify-local')}
243 </MenuItem>
244 <MenuItem
245 onClick={discard}
246 disabled={loading}
247 icon={trash}
248 iconPosition="left"
249 data-test="discard-button">
250 {__('Discard', 'extendify-local')}
251 </MenuItem>
252 </MenuGroup>
253 );
254 };
255