PluginProbe
Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels / trunk
Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels vtrunk
2.9.1 2.9.0 trunk 1.0.0 1.1.0 1.2.0 1.3.0 1.4.0 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 2.0.0 2.1.0 2.2.0 2.3.2 2.3.3 2.3.5 2.3.6 2.3.8 2.3.9 2.4.3 All 37 releases
automatic-youtube-gallery / admin / assets / js / admin.js

admin.js in Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels trunk, at admin/assets/js/admin.js

543 lines 19.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function( $ ) {
2 'use strict';
3
4 /**
5 * Display the media uploader.
6 *
7 * @since 1.0.0
8 */
9 function renderMediaUploader( $elem ) {
10 let file_frame, attachment;
11
12 // If an instance of file_frame already exists, then we can open it rather than creating a new instance
13 if ( file_frame ) {
14 file_frame.open();
15 return;
16 };
17
18 // Use the wp.media library to define the settings of the media uploader
19 file_frame = wp.media.frames.file_frame = wp.media({
20 frame: 'post',
21 state: 'insert',
22 multiple: false
23 });
24
25 // Setup an event handler for what to do when a media has been selected
26 file_frame.on( 'insert', function() {
27 // Read the JSON data returned from the media uploader
28 attachment = file_frame.state().get( 'selection' ).first().toJSON();
29
30 // First, make sure that we have the URL of the media to display
31 if ( 0 > $.trim( attachment.url.length ) ) {
32 return;
33 };
34
35 // Set the data
36 $elem.prev( '.ayg-settings-url' ).val( attachment.url );
37 });
38
39 // Now display the actual file_frame
40 file_frame.open();
41 };
42
43 /**
44 * Widget: Initiate color picker
45 *
46 * @since 1.0.0
47 */
48 function initWidgetColorPicker( widget ) {
49 widget.find( '.ayg-color-picker' ).wpColorPicker( {
50 change: _.throttle( function() { // For Customizer
51 $( this ).trigger( 'change' );
52 }, 3000 )
53 });
54 }
55
56 function onWidgetUpdate( event, widget ) {
57 initWidgetColorPicker( widget );
58 }
59
60 /**
61 * Gallery Builder: Run the import batch loop for a gallery.
62 *
63 * Calls the ayg_import_gallery AJAX action repeatedly, passing back the
64 * page token returned by the previous call, until the server reports done.
65 *
66 * @since 2.8.0
67 */
68 function importGallery( id, callbacks ) {
69 const runBatch = function( pageToken ) {
70 const data = {
71 action: 'ayg_import_gallery',
72 id: id,
73 page_token: pageToken,
74 security: ayg_admin.ajax_nonce
75 };
76
77 $.post( ajaxurl, data, function( response ) {
78 if ( ! response.success ) {
79 callbacks.error( response.data || {} );
80 return;
81 }
82
83 callbacks.progress( response.data );
84
85 if ( response.data.done ) {
86 callbacks.complete( response.data );
87 } else {
88 runBatch( response.data.next_page_token );
89 }
90 } ).fail( function() {
91 callbacks.error( {} );
92 } );
93 };
94
95 runBatch( '' );
96 }
97
98 /**
99 * Called when the page has loaded.
100 *
101 * @since 1.0.0
102 */
103 $(function() {
104 // Common: Initialize the color picker
105 $( '.ayg-color-picker' ).wpColorPicker();
106
107 // Dashboard: Save API key
108 $( '#ayg-button-save-api-key' ).on( 'click', function( event ) {
109 event.preventDefault();
110
111 const $button = $( this );
112 const $field = $( '#ayg-form-field-api-key' );
113 const $control = $field.closest( '.ayg-form-control' );
114 const $status = $button.siblings( '.ayg-form-status' );
115
116 const data = {
117 'action': 'ayg_save_api_key',
118 'api_key': $field.val(),
119 'security': ayg_admin.ajax_nonce
120 };
121
122 // Empty validation: Show the message inside the field control, then stop
123 if ( ! $.trim( data.api_key ) ) {
124 $control.addClass( 'ayg-form-invalid' );
125 $control.find( '.ayg-form-status' ).html( '<span class="ayg-text-error"><span class="dashicons dashicons-dismiss" aria-hidden="true"></span>' + ayg_admin.i18n.invalid_api_key + '</span>' );
126 $field.trigger( 'focus' );
127 return;
128 }
129
130 $control.removeClass( 'ayg-form-invalid' );
131 $control.find( '.ayg-form-status' ).html( '' );
132
133 // Disable the button and show a spinner while saving
134 $button.prop( 'disabled', true ).prepend( '<span class="spinner"></span>' );
135 $status.html( '' );
136
137 // Perform AJAX request to save the API key
138 $.post( ajaxurl, data, function( response ) {
139 if ( ! response.success ) {
140 const message = ( response.data && response.data.message ) ? response.data.message : ayg_admin.i18n.save_failed;
141
142 $button.prop( 'disabled', false ).find( '.spinner' ).remove();
143 $status.html( '<span class="ayg-text-error"><span class="dashicons dashicons-dismiss" aria-hidden="true"></span>' + message + '</span>' );
144 return;
145 }
146
147 window.location.reload(); // Reload document
148 } ).fail( function() {
149 $button.prop( 'disabled', false ).find( '.spinner' ).remove();
150 $status.html( '<span class="ayg-text-error"><span class="dashicons dashicons-dismiss" aria-hidden="true"></span>' + ayg_admin.i18n.save_failed + '</span>' );
151 } );
152 });
153
154 // Dashboard: Clear the API key field error as the user types
155 $( '#ayg-form-field-api-key' ).on( 'input', function() {
156 const $control = $( this ).closest( '.ayg-form-control' );
157
158 if ( $.trim( $( this ).val() ).length > 0 ) {
159 $control.removeClass( 'ayg-form-invalid' );
160 $control.find( '.ayg-form-status' ).html( '' );
161 }
162 });
163
164 // Gallery Form: Warn before leaving with unsaved changes, or while an import is still running
165 // (leaving mid-import leaves the gallery in a "running" state until cron resumes it).
166 let form_dirty = false;
167 let importing = false;
168
169 $( '#ayg-gallery-form' ).on( 'change input', ':input', function() {
170 form_dirty = true;
171 } );
172
173 $( window ).on( 'beforeunload', function( event ) {
174 if ( form_dirty || importing ) {
175 // Modern browsers show their own generic prompt and ignore a custom message
176 // string; they require preventDefault() and/or returnValue to be set to fire it.
177 event.preventDefault();
178 ( event.originalEvent || event ).returnValue = ayg_admin.i18n.unsaved_changes;
179 return ayg_admin.i18n.unsaved_changes;
180 }
181 } );
182
183 // Gallery Form: Validate / clear a source field error as the user edits it.
184 // The @handle pattern is deterministic, so the channel field (shared by channel
185 // and livestream) re-checks it live; the empty check stays submit-only.
186 $( '#ayg-gallery-form' ).on( 'input', '.ayg-form-section-source .ayg-form-field', function() {
187 const $field = $( this );
188 const $control = $field.closest( '.ayg-form-control' );
189
190 if ( 'ayg-form-field-channel' === $field.attr( 'id' ) && $field.val().indexOf( '@' ) !== -1 ) {
191 $control.addClass( 'ayg-form-invalid' );
192
193 const $error = $( '<span class="ayg-text-error"><span class="dashicons dashicons-dismiss" aria-hidden="true"></span></span>' );
194 $error.append( document.createTextNode( ayg_admin.i18n.channel_handle ) );
195 $control.find( '.ayg-form-status' ).html( '' ).append( $error );
196 return;
197 }
198
199 if ( $.trim( $( this ).val() ).length > 0 ) {
200 $control.removeClass( 'ayg-form-invalid' );
201 $control.find( '.ayg-form-status' ).html( '' );
202 }
203 } );
204
205 // Gallery Form: Create / Update Gallery, then import videos inline
206 $( '#ayg-button-save-gallery' ).on( 'click', function( event ) {
207 event.preventDefault();
208
209 const $button = $( this );
210 const $form = $button.closest( 'form' );
211 const $status = $button.siblings( '.ayg-form-status' );
212 const $actions = $button.closest( '.ayg-form-actions' );
213 const isNew = '0' === $form.find( 'input[name="gallery_id"]' ).val();
214
215 // Validate the active source field, unless it is locked (disabled). The source
216 // type maps to a single value field; livestream reuses the channel field.
217 const type = $form.find( '#ayg-form-field-type' ).val();
218 const sourceFieldMap = { playlist: 'playlist', channel: 'channel', livestream: 'channel', username: 'username', search: 'search', video: 'video', videos: 'videos' };
219 const sourceField = sourceFieldMap[ type ];
220 const $sourceField = $form.find( '#ayg-form-field-' + sourceField );
221
222 if ( $sourceField.length && ! $sourceField.prop( 'disabled' ) ) {
223 const $control = $sourceField.closest( '.ayg-form-control' );
224 const value = $.trim( $sourceField.val() );
225
226 // Show a field-level message, focus and scroll to the field, then stop
227 const showSourceError = function( message ) {
228 $control.addClass( 'ayg-form-invalid' );
229
230 const $error = $( '<span class="ayg-text-error"><span class="dashicons dashicons-dismiss" aria-hidden="true"></span></span>' );
231 $error.append( document.createTextNode( message ) );
232 $control.find( '.ayg-form-status' ).html( '' ).append( $error );
233
234 const field = $sourceField.get( 0 );
235 field.focus( { preventScroll: true } );
236 field.scrollIntoView( { behavior: 'smooth', block: 'center' } );
237 };
238
239 if ( ! value ) {
240 showSourceError( ayg_admin.i18n.source_required[ type ] );
241 return;
242 }
243
244 // @handle URLs can't be resolved for channel / livestream sources
245 if ( ( 'channel' === type || 'livestream' === type ) && value.indexOf( '@' ) !== -1 ) {
246 showSourceError( ayg_admin.i18n.channel_handle );
247 return;
248 }
249 }
250
251 // Disable the buttons and show a live status (spinner + green message + animated dots).
252 // Reuses the existing message element when present so the spinner and dots keep
253 // animating across updates instead of restarting on each call.
254 const showStatus = function( message ) {
255 $actions.find( 'button' ).prop( 'disabled', true );
256
257 let $message = $status.find( '.ayg-text-success' );
258
259 if ( ! $message.length ) {
260 $message = $( '<span class="ayg-text-success"></span>' ).append( '<span class="ayg-animate-dots"></span>' );
261 $status.html( '<span class="spinner"></span>' ).append( $message );
262 }
263
264 // Update only the leading text node, leaving the trailing dots span intact.
265 const node = $message.get( 0 ).firstChild;
266
267 if ( node && 3 === node.nodeType ) {
268 node.nodeValue = message;
269 } else {
270 $message.prepend( document.createTextNode( message ) );
271 }
272 };
273
274 // Restore the buttons and show an error message in the form status
275 const showError = function( message ) {
276 $actions.find( 'button' ).prop( 'disabled', false );
277
278 const $error = $( '<span class="ayg-text-error"><span class="dashicons dashicons-dismiss" aria-hidden="true"></span></span>' );
279 $error.append( document.createTextNode( message ) );
280 $status.html( '' ).append( $error );
281 };
282
283 // Fill a status template's %imported% / %updated% / %deleted% tokens from an import response.
284 const fillCounts = function( template, data ) {
285 return template
286 .replace( '%imported%', data && data.imported ? data.imported : 0 )
287 .replace( '%updated%', data && data.updated ? data.updated : 0 )
288 .replace( '%deleted%', data && data.deleted ? data.deleted : 0 );
289 };
290
291 // Stage 1: "Processing..."
292 showStatus( ayg_admin.i18n.processing );
293
294 // Perform AJAX request to save the gallery
295 const data = $form.serialize() + '&action=ayg_save_gallery&security=' + encodeURIComponent( ayg_admin.ajax_nonce );
296
297 $.post( ajaxurl, data, function( response ) {
298 if ( ! response.success ) {
299 showError( ( response.data && response.data.message ) ? response.data.message : ayg_admin.i18n.save_failed );
300 return;
301 }
302
303 form_dirty = false;
304
305 const id = response.data.gallery_id;
306 const editUrl = ayg_admin.admin_url + '?page=automatic-youtube-gallery&action=edit&id=' + id;
307 const doneUrl = editUrl + ( isNew ? '&saved=1' : '&updated=1' );
308
309 // "Import videos" unchecked = display-only edit; save the config but skip the import.
310 const $canImport = $form.find( '#ayg-can-import-videos' );
311 if ( $canImport.length && ! $canImport.is( ':checked' ) ) {
312 window.location.href = doneUrl;
313 return;
314 }
315
316 // Paused galleries skip importing — saving must not re-trigger the import,
317 // which would flip import_status back from 'paused' to 'idle'.
318 if ( 'paused' === $form.find( '[name="schedule"]' ).val() ) {
319 window.location.href = doneUrl;
320 return;
321 }
322
323 // Live galleries (search / livestream / single video) query the live API at display time — nothing to import
324 const sourceType = $form.find( '#ayg-form-field-type' ).val();
325 if ( 'search' === sourceType || 'livestream' === sourceType || 'video' === sourceType ) {
326 window.location.href = doneUrl;
327 return;
328 }
329
330 // Point the form at the saved gallery before importing, so a refresh or a
331 // retry after an import error updates this gallery instead of creating a new one
332 $form.find( 'input[name="gallery_id"]' ).val( id );
333 window.history.replaceState( null, '', editUrl );
334
335 // Block the leave-warning's "import in progress" guard for the duration of the run.
336 importing = true;
337
338 importGallery( id, {
339 progress: function( data ) {
340 // Stage 2: Running "Imported: X, Updated: Y..." breakdown as batches return.
341 showStatus( fillCounts( ayg_admin.i18n.import_progress, data ) );
342 },
343 complete: function( data ) {
344 // Stage 3: Final breakdown, then redirect.
345 importing = false;
346 showStatus( fillCounts( ayg_admin.i18n.import_complete, data ) );
347 window.location.href = doneUrl;
348 },
349 error: function( data ) {
350 importing = false;
351 showError( data.message || ( data.quota_exceeded ? ayg_admin.i18n.quota_exceeded : ayg_admin.i18n.import_failed ) );
352
353 // The gallery exists now — clicking the button again updates and resumes
354 if ( isNew ) {
355 $button.text( ayg_admin.i18n.update_gallery );
356 }
357 }
358 } );
359 } ).fail( function() {
360 showError( ayg_admin.i18n.save_failed );
361 } );
362 } );
363
364 // Gallery List: Confirm before bulk deleting galleries
365 $( '#ayg-gallery-list' ).on( 'submit', '.ayg-form', function( event ) {
366 const action = $( this ).find( 'select[name="action"], select[name="action2"]' ).filter( function() {
367 return 'delete' === $( this ).val();
368 } );
369
370 if ( action.length && ! window.confirm( ayg_admin.i18n.confirm_bulk_delete ) ) {
371 event.preventDefault();
372 }
373 } );
374
375 // Gallery List / Form: Delete Gallery
376 $( document ).on( 'click', '.ayg-button-delete-gallery', function( event ) {
377 event.preventDefault();
378
379 if ( ! window.confirm( ayg_admin.i18n.confirm_delete ) ) {
380 return;
381 }
382
383 const $button = $( this );
384 const id = $button.data( 'id' );
385
386 // Disable the button while deleting
387 $button.prop( 'disabled', true );
388
389 // Perform AJAX request to delete the gallery
390 const data = {
391 action: 'ayg_delete_gallery',
392 id: id,
393 security: ayg_admin.ajax_nonce
394 };
395
396 $.post( ajaxurl, data, function( response ) {
397 if ( ! response.success ) {
398 const message = ( response.data && response.data.message ) ? response.data.message : ayg_admin.i18n.delete_failed;
399 window.alert( message );
400 $button.prop( 'disabled', false );
401 return;
402 }
403
404 form_dirty = false;
405
406 const doneUrl = ayg_admin.admin_url + '?page=automatic-youtube-gallery&deleted=1';
407 window.location.href = doneUrl;
408 } ).fail( function() {
409 window.alert( ayg_admin.i18n.delete_failed );
410 $button.prop( 'disabled', false );
411 } );
412 } );
413
414 // Gallery List / Form: Copy Shortcode button
415 $( document ).on( 'click', '.ayg-button-copy-shortcode', function() {
416 const text = $( this ).data( 'clipboard-text' );
417
418 // Confirm the copy with an alert that echoes the copied shortcode.
419 const showCopied = function() {
420 window.alert( ayg_admin.i18n.shortcode_copied + '\n\n' + text );
421 }
422
423 // Use the modern Clipboard API if available, otherwise fall back to the legacy method
424 if ( navigator.clipboard && navigator.clipboard.writeText ) {
425 navigator.clipboard.writeText( text ).then( showCopied );
426 } else {
427 const $temp = $( '<textarea>' ).css( { position: 'fixed', opacity: 0 } ).val( text ).appendTo( 'body' );
428 $temp.get( 0 ).select();
429 document.execCommand( 'copy' );
430 $temp.remove();
431
432 showCopied();
433 }
434 } );
435
436 // Editor: Toggle fields based on the source type
437 $( document ).on( 'change', '.ayg-form-field-type', function() {
438 const $wrapper = $( this ).closest( '.ayg-form' );
439 const value = $( this ).val();
440
441 $wrapper.removeClass(function( index, classes ) {
442 const matches = classes.match( /\ayg-form-field-type-\S+/ig );
443 return ( matches ) ? matches.join( ' ' ) : '';
444 });
445
446 $wrapper.addClass( 'ayg-form-field-type-' + value );
447
448 // Gallery Form: keep the source hint in sync with the selected type (live vs importable).
449 // No-op where the hint isn't present (e.g. the widget form), so it stays safe globally.
450 const $hint = $wrapper.find( '.ayg-source-hint' );
451 if ( $hint.length ) {
452 $hint.text( ayg_admin.live_types.indexOf( value ) !== -1 ? ayg_admin.i18n.source_live : ayg_admin.i18n.source_importable );
453 }
454 });
455
456 // Editor: Toggle fields based on the theme
457 $( document ).on( 'change', '.ayg-form-field-theme', function() {
458 const $wrapper = $( this ).closest( '.ayg-form' );
459 const value = $( this ).val();
460
461 $wrapper.removeClass(function( index, classes ) {
462 const matches = classes.match( /\ayg-form-field-theme-\S+/ig );
463 return ( matches ) ? matches.join( ' ' ) : '';
464 });
465
466 $wrapper.addClass( 'ayg-form-field-theme-' + value );
467 });
468
469 // Settings: Toggle fields based on the theme
470 $( '#ayg-settings' ).on( 'change', 'tr.theme select', function() {
471 const $wrapper = $( '#ayg-settings' );
472 const value = $( this ).val();
473
474 $wrapper.removeClass(function( index, classes ) {
475 const matches = classes.match( /theme-\S+/ig );
476 return ( matches ) ? matches.join( ' ' ) : '';
477 });
478
479 $wrapper.addClass( 'theme-' + value );
480 });
481
482 // Settings: Toggle fields based on the player type
483 $( '#ayg-settings' ).on( 'change', 'tr.player_type input[type="radio"]', function() {
484 const $wrapper = $( '#ayg-settings' );
485 const value = $wrapper.find( 'tr.player_type input[type="radio"]:checked' ).val();
486
487 $wrapper.removeClass(function( index, classes ) {
488 const matches = classes.match( /player_type-\S+/ig );
489 return ( matches ) ? matches.join( ' ' ) : '';
490 });
491
492 $wrapper.addClass( 'player_type-' + value );
493 });
494
495 // Settings: Upload button
496 $( '.ayg-button-upload-media' ).on( 'click', function( event ) {
497 event.preventDefault();
498 renderMediaUploader( $( this ) );
499 });
500
501 // Settings: Delete cache
502 $( '#ayg-button-delete-cache' ).on( 'click', function( event ) {
503 event.preventDefault();
504
505 const $button = $( this );
506
507 // Disable the button and show a spinner while deleting cache
508 $button.prop( 'disabled', true );
509 $( '.ayg-form-status', '#ayg-table-delete-cache' ).html( '<span class="spinner"></span>' );
510
511 // Perform AJAX request to delete the cache
512 const data = {
513 'action': 'ayg_delete_cache',
514 'security': ayg_admin.ajax_nonce
515 };
516
517 $.post( ajaxurl, data, function( response ) {
518 if ( ! response.success ) {
519 const message = ( response.data && response.data.message ) ? response.data.message : ayg_admin.i18n.delete_failed;
520
521 $button.prop( 'disabled', false );
522 $( '.ayg-form-status', '#ayg-table-delete-cache' ).html( '<span class="ayg-text-error"><span class="dashicons dashicons-dismiss" aria-hidden="true"></span>' + message + '</span>' );
523 return;
524 }
525
526 $button.prop( 'disabled', false );
527 $( '.ayg-form-status', '#ayg-table-delete-cache' ).html( '<span class="ayg-text-success"><span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>' + ayg_admin.i18n.cache_cleared + '</span>' );
528 } ).fail( function() {
529 $button.prop( 'disabled', false );
530 $( '.ayg-form-status', '#ayg-table-delete-cache' ).html( '<span class="ayg-text-error"><span class="dashicons dashicons-dismiss" aria-hidden="true"></span>' + ayg_admin.i18n.delete_failed + '</span>' );
531 } );
532 });
533
534 // Widget: Initiate color picker
535 $( '#widgets-right .widget:has(.ayg-color-picker)' ).each(function() {
536 initWidgetColorPicker( $( this ) );
537 });
538
539 $( document ).on( 'widget-added widget-updated', onWidgetUpdate );
540 });
541
542 })( jQuery );
543