PluginProbe ʕ •ᴥ•ʔ
Presto Player / trunk
Presto Player vtrunk
4.3.1 4.3.0 4.2.4 4.2.3 4.2.2 4.2.0 4.2.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.12.0 1.13.0 1.14.0 1.14.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.13 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3-beta1 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.0-beta1 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1.0 3.1.1 3.1.2 3.1.3 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4
presto-player / src / admin / integrations / divi / d5 / fields / MediaHubField.jsx
presto-player / src / admin / integrations / divi / d5 / fields Last commit date
MediaHubField.jsx 3 days ago
MediaHubField.jsx
257 lines
1 import React, { useState, useEffect, useCallback, useRef } from 'react';
2 import { __ } from '@wordpress/i18n';
3 import debounce from 'debounce-promise';
4
5 // Deliberately hand-rolled rather than reusing the D4 VideoSelector (react-select/AsyncSelect):
6 // the divi-d5 bundle externalizes react/react-dom to Divi VB's shared window.vendor.React to avoid
7 // the two-React-copies hook error (#321), and pulling react-select in here reintroduces that risk
8 // plus extra weight. This picker reuses the same presto_fetch_videos endpoint and debounce-promise.
9 // TODO: revisit sharing a common video-search hook / ComboboxControl once the VB React setup allows.
10
11 // resolve ajaxurl + nonce: D5 app-iframe has PrestoPlayerDiviD5Data injected by PHP
12 function getAjaxConfig() {
13 const d5 = window.PrestoPlayerDiviD5Data || window.parent?.PrestoPlayerDiviD5Data || {};
14 const pp = window.prestoPlayer || window.parent?.prestoPlayer || {};
15 return {
16 ajaxurl: d5.ajaxurl || pp.ajaxurl || window.ajaxurl || window.parent?.ajaxurl || '',
17 nonce: d5.nonce || pp.nonce || '',
18 };
19 }
20
21 function fetchVideos( search = '', postId = '' ) {
22 const { ajaxurl: url, nonce } = getAjaxConfig();
23 const body = new URLSearchParams( {
24 action: 'presto_fetch_videos',
25 _wpnonce: nonce || '',
26 } );
27 if ( search ) body.set( 'search', search );
28 if ( postId ) body.set( 'post_id', String( postId ) );
29
30 return fetch( url, {
31 method: 'POST',
32 credentials: 'same-origin',
33 headers: {
34 'Content-Type': 'application/x-www-form-urlencoded',
35 'Cache-Control': 'no-cache',
36 },
37 body,
38 } )
39 .then( ( r ) => {
40 if ( ! r.ok ) {
41 throw new Error( `Request failed: ${ r.status }` );
42 }
43 return r.json();
44 } )
45 .then( ( r ) => {
46 // The AJAX handler returns HTTP 200 with { success: false } on nonce/cap failure.
47 if ( ! r?.success ) {
48 throw new Error( r?.data?.message || 'Request failed' );
49 }
50 return ( r.data || [] ).map( ( v ) => ( { value: v.ID, label: v.post_title || __( 'Untitled', 'presto-player' ) } ) );
51 } );
52 }
53
54 const fieldName = 'presto/media-hub';
55
56 export default function MediaHubField( { value, onChange } ) {
57 const [ options, setOptions ] = useState( [] );
58 const [ inputValue, setInputValue ] = useState( '' );
59 const [ isOpen, setIsOpen ] = useState( false );
60 const [ loading, setLoading ] = useState( false );
61 const [ error, setError ] = useState( false );
62 const wrapRef = useRef( null );
63 const valueRef = useRef( value );
64 valueRef.current = value;
65 const reqRef = useRef( 0 );
66
67 const selectedLabel = options.find( ( o ) => String( o.value ) === String( value ) )?.label || '';
68
69 // initial load: fetch list + selected video
70 useEffect( () => {
71 let cancelled = false;
72 // join the same request sequence as debouncedSearch so an early
73 // search can't get clobbered by this slower initial load
74 const myReq = ++reqRef.current;
75 setLoading( true );
76 setError( false );
77 Promise.all( [
78 fetchVideos( '' ),
79 value ? fetchVideos( '', value ) : Promise.resolve( [] ),
80 ] )
81 .then( ( [ list, current ] ) => {
82 if ( cancelled || myReq !== reqRef.current ) return;
83 const merged = [ ...list ];
84 ( current || [] ).forEach( ( v ) => {
85 if ( ! merged.find( ( o ) => o.value === v.value ) ) merged.push( v );
86 } );
87 setOptions( merged );
88 } )
89 .catch( ( err ) => {
90 if ( cancelled || myReq !== reqRef.current ) return;
91 // eslint-disable-next-line no-console
92 console.error( 'Presto Player: failed to fetch videos', err );
93 setError( true );
94 } )
95 .finally( () => {
96 if ( ! cancelled && myReq === reqRef.current ) setLoading( false );
97 } );
98 return () => {
99 cancelled = true;
100 };
101 }, [] ); // run once on mount
102
103 const debouncedSearch = useCallback(
104 debounce( ( term ) => {
105 const myReq = ++reqRef.current;
106 setLoading( true );
107 setError( false );
108 return fetchVideos( term )
109 .then( ( list ) => {
110 // ignore stale responses that resolve after a newer search
111 if ( myReq !== reqRef.current ) return list;
112 setOptions( ( prev ) => {
113 const selected = prev.find( ( o ) => String( o.value ) === String( valueRef.current ) );
114 if ( selected && ! list.find( ( o ) => String( o.value ) === String( selected.value ) ) ) {
115 return [ selected, ...list ];
116 }
117 return list;
118 } );
119 return list;
120 } )
121 .catch( ( err ) => {
122 if ( myReq !== reqRef.current ) return;
123 // eslint-disable-next-line no-console
124 console.error( 'Presto Player: failed to fetch videos', err );
125 setError( true );
126 } )
127 .finally( () => {
128 if ( myReq === reqRef.current ) setLoading( false );
129 } );
130 }, 400 ),
131 []
132 );
133
134 function handleInputChange( e ) {
135 const term = e.target.value;
136 setInputValue( term );
137 debouncedSearch( term );
138 }
139
140 function handleSelect( option ) {
141 onChange( { inputValue: String( option.value ) } );
142 setIsOpen( false );
143 setInputValue( '' );
144 }
145
146 // close on outside click
147 useEffect( () => {
148 if ( ! isOpen ) return;
149 function handler( e ) {
150 if ( wrapRef.current && ! wrapRef.current.contains( e.target ) ) {
151 setIsOpen( false );
152 }
153 }
154 document.addEventListener( 'mousedown', handler );
155 return () => document.removeEventListener( 'mousedown', handler );
156 }, [ isOpen ] );
157
158 const containerStyle = {
159 position: 'relative',
160 fontFamily: 'inherit',
161 fontSize: '13px',
162 };
163 const triggerStyle = {
164 display: 'flex',
165 alignItems: 'center',
166 justifyContent: 'space-between',
167 padding: '6px 8px',
168 border: '1px solid #ccc',
169 borderRadius: '3px',
170 background: '#fff',
171 cursor: 'pointer',
172 minHeight: '32px',
173 };
174 const dropdownStyle = {
175 position: 'absolute',
176 top: '100%',
177 left: 0,
178 right: 0,
179 zIndex: 9999,
180 background: '#fff',
181 border: '1px solid #ccc',
182 borderTop: 'none',
183 borderRadius: '0 0 3px 3px',
184 maxHeight: '220px',
185 overflowY: 'auto',
186 boxShadow: '0 4px 8px rgba(0,0,0,0.15)',
187 };
188 const inputStyle = {
189 width: '100%',
190 padding: '6px 8px',
191 border: 'none',
192 borderBottom: '1px solid #eee',
193 outline: 'none',
194 fontSize: '13px',
195 boxSizing: 'border-box',
196 };
197 const itemStyle = ( selected ) => ( {
198 padding: '6px 10px',
199 cursor: 'pointer',
200 background: selected ? '#f0f0f0' : 'transparent',
201 } );
202
203 return (
204 <div ref={ wrapRef } style={ containerStyle }>
205 <div
206 style={ triggerStyle }
207 onClick={ () => setIsOpen( ( v ) => ! v ) }
208 role="button"
209 tabIndex={ 0 }
210 onKeyDown={ ( e ) => e.key === 'Enter' && setIsOpen( ( v ) => ! v ) }
211 >
212 <span style={ { flexGrow: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: selectedLabel ? '#333' : '#999' } }>
213 { selectedLabel || __( 'Choose a video…', 'presto-player' ) }
214 </span>
215 <span style={ { marginLeft: '6px', color: '#999', fontSize: '10px' } }>{ isOpen ? '' : '' }</span>
216 </div>
217
218 { isOpen && (
219 <div style={ dropdownStyle }>
220 <input
221 style={ inputStyle }
222 type="text"
223 value={ inputValue }
224 onChange={ handleInputChange }
225 placeholder={ __( 'Search videos…', 'presto-player' ) }
226 autoFocus
227 />
228 { loading && (
229 <div style={ { padding: '8px 10px', color: '#999' } }>{ __( 'Loading…', 'presto-player' ) }</div>
230 ) }
231 { ! loading && error && (
232 <div style={ { padding: '8px 10px', color: '#cc1818' } }>{ __( 'Could not load videos. Please reload and try again.', 'presto-player' ) }</div>
233 ) }
234 { ! loading && ! error && options.length === 0 && (
235 <div style={ { padding: '8px 10px', color: '#999' } }>{ __( 'No videos found.', 'presto-player' ) }</div>
236 ) }
237 { ! loading &&
238 options.map( ( opt ) => (
239 <div
240 key={ opt.value }
241 style={ itemStyle( String( opt.value ) === String( value ) ) }
242 onMouseDown={ ( e ) => {
243 e.preventDefault();
244 handleSelect( opt );
245 } }
246 >
247 { opt.label }
248 </div>
249 ) ) }
250 </div>
251 ) }
252 </div>
253 );
254 }
255
256 MediaHubField.fieldName = fieldName;
257