PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / 1.0.1
FrontBlocks for Gutenberg/GeneratePress v1.0.1
trunk 0.2.0 0.2.1 0.2.2 0.2.3 0.2.4 0.2.5 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.2.0 1.2.1 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 ci-artifacts
frontblocks / includes / post / frontblocks-insert-post-option.jsx
frontblocks / includes / post Last commit date
frontblocks-insert-post-option.jsx 11 months ago frontblocks-insert-post.css 11 months ago frontblocks-insert-post.php 11 months ago
frontblocks-insert-post-option.jsx
347 lines
1 // Insert Post Block Component
2 const { addFilter } = wp.hooks;
3 const { Fragment, useState, useEffect, useRef } = wp.element;
4 const { InspectorControls, useBlockProps, RichText } = wp.blockEditor;
5 const {
6 SelectControl,
7 TextControl,
8 PanelBody,
9 Button,
10 Spinner,
11 Notice,
12 Card,
13 CardBody,
14 CardHeader,
15 ToggleControl
16 } = wp.components;
17 const { __ } = wp.i18n;
18 const { apiFetch } = wp;
19
20 // Custom Insert Post Block
21 function InsertPostBlock(props) {
22 const { attributes, setAttributes } = props;
23 const {
24 selectedPostId = 0,
25 selectedPostType = 'post',
26 selectedPostTitle = '',
27 selectedPostContent = '',
28 className = ''
29 } = attributes;
30
31 const [searchTerm, setSearchTerm] = useState('');
32 const [isSearching, setIsSearching] = useState(false);
33 const [searchError, setSearchError] = useState('');
34 const [showSearch, setShowSearch] = useState(false);
35 const searchInputRef = useRef(null);
36
37 // Get available post types
38 const [postTypes, setPostTypes] = useState([
39 { label: __('Posts', 'frontblocks'), value: 'post' },
40 { label: __('Pages', 'frontblocks'), value: 'page' }
41 ]);
42
43 useEffect(() => {
44 // Get custom post types
45 apiFetch({ path: '/wp/v2/types' }).then(types => {
46 const customTypes = Object.keys(types)
47 .filter(type => type !== 'post' && type !== 'page')
48 .map(type => ({
49 label: types[type].name,
50 value: type
51 }));
52
53 setPostTypes(prev => [...prev, ...customTypes]);
54 }).catch(() => {
55 // If API fails, continue with default post types
56 });
57 }, []);
58
59 // Initialize jQuery autocomplete when component mounts or search is shown
60 useEffect(() => {
61 if (showSearch && searchInputRef.current && typeof jQuery !== 'undefined') {
62 const $input = jQuery(searchInputRef.current);
63
64 // Check if autocomplete is already initialized before destroying
65 if ($input.hasClass('ui-autocomplete-input')) {
66 try {
67 $input.autocomplete('destroy');
68 } catch (e) {
69 // If destroy fails, remove the autocomplete class manually
70 $input.removeClass('ui-autocomplete-input');
71 }
72 }
73
74 // Initialize autocomplete
75 $input.autocomplete({
76 source: function(request, response) {
77 if (request.term.length < 2) {
78 response([]);
79 return;
80 }
81
82 setIsSearching(true);
83 setSearchError('');
84
85 const formData = new FormData();
86 formData.append('action', 'frbl_search_posts');
87 formData.append('nonce', frblInsertPost.nonce);
88 formData.append('search', request.term);
89 formData.append('post_type', selectedPostType);
90
91 fetch(frblInsertPost.ajaxUrl, {
92 method: 'POST',
93 body: formData
94 })
95 .then(response => response.json())
96 .then(data => {
97 setIsSearching(false);
98 if (data.success) {
99 const results = data.data.map(post => ({
100 label: `${post.title} (${post.type})`,
101 value: post.title,
102 post: post
103 }));
104 response(results);
105 } else {
106 setSearchError(data.data || __('Search failed', 'frontblocks'));
107 response([]);
108 }
109 })
110 .catch(error => {
111 setIsSearching(false);
112 setSearchError(__('Search failed. Please try again.', 'frontblocks'));
113 response([]);
114 });
115 },
116 minLength: 2,
117 delay: 300,
118 select: function(event, ui) {
119 selectPost(ui.item.post);
120 return false;
121 },
122 open: function() {
123 jQuery(this).autocomplete('widget').css('z-index', 999999);
124 }
125 });
126
127 // Cleanup function
128 return () => {
129 try {
130 if ($input.hasClass('ui-autocomplete-input')) {
131 $input.autocomplete('destroy');
132 }
133 } catch (e) {
134 // If destroy fails, just remove the class
135 $input.removeClass('ui-autocomplete-input');
136 }
137 };
138 }
139 }, [showSearch, selectedPostType]);
140
141 const selectPost = (post) => {
142 setAttributes({
143 selectedPostId: post.id,
144 selectedPostTitle: post.title,
145 selectedPostType: post.type
146 });
147 setShowSearch(false);
148 setSearchTerm('');
149 setSearchError('');
150 };
151
152 const clearSelection = () => {
153 setAttributes({
154 selectedPostId: 0,
155 selectedPostTitle: '',
156 selectedPostContent: ''
157 });
158 setShowSearch(false);
159 setSearchTerm('');
160 setSearchError('');
161 };
162
163 const blockProps = useBlockProps({
164 className: `frbl-insert-post-block ${className}`
165 });
166
167 return (
168 <Fragment>
169 <div {...blockProps}>
170 {selectedPostId ? (
171 <div className="frbl-insert-post-preview">
172 <h2 className="frbl-insert-post-title">
173 {selectedPostTitle}
174 </h2>
175 <div className="frbl-insert-post-content">
176 {selectedPostContent || __('Content will be loaded on the frontend', 'frontblocks')}
177 </div>
178 <div className="frbl-insert-post-actions">
179 <Button
180 isSecondary
181 onClick={() => setShowSearch(true)}
182 >
183 {__('Change Post', 'frontblocks')}
184 </Button>
185 <Button
186 isDestructive
187 onClick={clearSelection}
188 >
189 {__('Clear Selection', 'frontblocks')}
190 </Button>
191 </div>
192 </div>
193 ) : (
194 <div className="frbl-insert-post-empty">
195 <p>{__('No post selected. Use the sidebar to search and select a post.', 'frontblocks')}</p>
196 <Button
197 isPrimary
198 onClick={() => setShowSearch(true)}
199 >
200 {__('Select Post', 'frontblocks')}
201 </Button>
202 </div>
203 )}
204 </div>
205
206 <InspectorControls>
207 <PanelBody
208 title={__('Insert Post Settings', 'frontblocks')}
209 initialOpen={true}
210 >
211 {(!selectedPostId || showSearch) && (
212 <>
213 <SelectControl
214 label={__('Post Type', 'frontblocks')}
215 value={selectedPostType}
216 options={postTypes}
217 onChange={(value) => setAttributes({ selectedPostType: value })}
218 />
219
220 <TextControl
221 ref={searchInputRef}
222 label={__('Search Posts', 'frontblocks')}
223 value={searchTerm}
224 onChange={setSearchTerm}
225 placeholder={__('Start typing to search posts...', 'frontblocks')}
226 help={__('Type at least 2 characters to search', 'frontblocks')}
227 />
228
229 {isSearching && (
230 <div style={{ marginTop: '10px' }}>
231 <Spinner />
232 <span style={{ marginLeft: '8px' }}>{__('Searching...', 'frontblocks')}</span>
233 </div>
234 )}
235
236 {searchError && (
237 <Notice status="error" isDismissible={false}>
238 {searchError}
239 </Notice>
240 )}
241 </>
242 )}
243
244 {selectedPostId && !showSearch && (
245 <div className="frbl-selected-post-info">
246 <h4>{__('Selected Post', 'frontblocks')}</h4>
247 <Card>
248 <CardBody>
249 <p><strong>{__('Title:', 'frontblocks')}</strong> {selectedPostTitle}</p>
250 <p><strong>{__('Type:', 'frontblocks')}</strong> {selectedPostType}</p>
251 <p><strong>{__('ID:', 'frontblocks')}</strong> {selectedPostId}</p>
252 </CardBody>
253 </Card>
254 <Button
255 isSecondary
256 onClick={() => setShowSearch(true)}
257 style={{ marginTop: '10px' }}
258 >
259 {__('Change Post', 'frontblocks')}
260 </Button>
261 </div>
262 )}
263 </PanelBody>
264 </InspectorControls>
265 </Fragment>
266 );
267 }
268
269 // Register the custom block
270 const { registerBlockType } = wp.blocks;
271
272 registerBlockType('frontblocks/insert-post', {
273 title: __('Insert Post', 'frontblocks'),
274 description: __('Display content from another post or page', 'frontblocks'),
275 category: 'generateblocks',
276 icon: 'admin-post',
277 keywords: [
278 __('post', 'frontblocks'),
279 __('content', 'frontblocks'),
280 __('insert', 'frontblocks'),
281 __('display', 'frontblocks')
282 ],
283 supports: {
284 html: false,
285 align: ['wide', 'full']
286 },
287 attributes: {
288 selectedPostId: {
289 type: 'number',
290 default: 0
291 },
292 selectedPostType: {
293 type: 'string',
294 default: 'post'
295 },
296 selectedPostTitle: {
297 type: 'string',
298 default: ''
299 },
300 selectedPostContent: {
301 type: 'string',
302 default: ''
303 },
304 className: {
305 type: 'string',
306 default: ''
307 }
308 },
309 edit: InsertPostBlock,
310 save: () => null // Using PHP render callback
311 });
312
313 // Add custom panel to existing GenerateBlocks Grid block
314 function addInsertPostPanel(BlockEdit) {
315 return (props) => {
316 if (props.name !== 'generateblocks/grid') {
317 return <BlockEdit {...props} />;
318 }
319
320 const { frblInsertPostEnabled = false } = props.attributes;
321
322 return (
323 <Fragment>
324 <BlockEdit {...props} />
325 <InspectorControls>
326 <PanelBody
327 title={__('Insert Post Integration', 'frontblocks')}
328 initialOpen={false}
329 >
330 <ToggleControl
331 label={__('Enable Insert Post Grid', 'frontblocks')}
332 checked={frblInsertPostEnabled}
333 onChange={(value) => props.setAttributes({ frblInsertPostEnabled: value })}
334 help={__('Enable insert post functionality for this grid', 'frontblocks')}
335 />
336 </PanelBody>
337 </InspectorControls>
338 </Fragment>
339 );
340 };
341 }
342
343 addFilter(
344 'editor.BlockEdit',
345 'frontblocks/insert-post-grid-panel',
346 addInsertPostPanel
347 );