PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.2.2
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.2.2
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / _dev / src / bulk.js

bulk.js in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 2.2.2, at _dev/src/bulk.js

1,144 lines 42.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 window.imagify = window.imagify || {};
2
3 (function( $, undefined ) { // eslint-disable-line no-shadow, no-shadow-restricted-names
4
5 var jqPropHookChecked = $.propHooks.checked;
6
7 // Force `.prop()` to trigger a `change` event.
8 $.propHooks.checked = {
9 set: function( elem, value, name ) {
10 var ret;
11
12 if ( undefined === jqPropHookChecked ) {
13 ret = ( elem[ name ] = value );
14 } else {
15 ret = jqPropHookChecked( elem, value, name );
16 }
17
18 $( elem ).trigger( 'change.imagify' );
19
20 return ret;
21 }
22 };
23
24 // Custom jQuery functions =====================================================================
25 /**
26 * Hide element(s).
27 *
28 * @param {int} duration A duration in ms.
29 * @param {function} callback A callback to execute once the element is hidden.
30 * @return {element} The jQuery element(s).
31 */
32 $.fn.imagifyHide = function( duration, callback ) {
33 if ( duration && duration > 0 ) {
34 this.hide( duration, function() {
35 $( this ).addClass( 'hidden' ).css( 'display', '' );
36
37 if ( undefined !== callback ) {
38 callback();
39 }
40 } );
41 } else {
42 this.addClass( 'hidden' );
43
44 if ( undefined !== callback ) {
45 callback();
46 }
47 }
48
49 return this.attr( 'aria-hidden', 'true' );
50 };
51
52 /**
53 * Show element(s).
54 *
55 * @param {int} duration A duration in ms.
56 * @param {function} callback A callback to execute before starting to display the element.
57 * @return {element} The jQuery element(s).
58 */
59 $.fn.imagifyShow = function( duration, callback ) {
60 if ( undefined !== callback ) {
61 callback();
62 }
63
64 if ( duration && duration > 0 ) {
65 this.show( duration, function() {
66 $( this ).removeClass( 'hidden' ).css( 'display', '' );
67 } );
68 } else {
69 this.removeClass( 'hidden' );
70 }
71
72 return this.attr( 'aria-hidden', 'false' );
73 };
74
75 }( jQuery ));
76
77 (function($, d, w, undefined) { // eslint-disable-line no-unused-vars, no-shadow, no-shadow-restricted-names
78
79 w.imagify.bulk = {
80
81 // Properties ==============================================================================
82 charts: {
83 overview: {
84 canvas: false,
85 donut: false,
86 data: {
87 // Order: unoptimized, optimized, error.
88 labels: [
89 imagifyBulk.labels.overviewChartLabels.unoptimized,
90 imagifyBulk.labels.overviewChartLabels.optimized,
91 imagifyBulk.labels.overviewChartLabels.error
92 ],
93 datasets: [ {
94 data: [],
95 backgroundColor: [ '#10121A', '#46B1CE', '#C51162' ],
96 borderWidth: 0
97 } ]
98 }
99 },
100 files: {
101 donuts: {}
102 },
103 share: {
104 canvas: false,
105 donut: false
106 }
107 },
108 /**
109 * Folder types in queue.
110 * An array of objects: {
111 * @type {string} groupID The group ID, like 'library'.
112 * @type {string} context The context, like 'wp'.
113 * @type {int} level The optimization level: 0, 1, or 2.
114 * }
115 */
116 folderTypesQueue: [],
117 /**
118 * Status of each folder type. Type IDs are used as keys.
119 * Each object contains: {
120 * @type {bool} isError Tell if the status is considered as an error.
121 * @type {string} id ID of the status, like 'waiting', 'fetching', or 'optimizing'.
122 * }
123 */
124 status: {},
125 // Tell if the message displayed when retrieving the image IDs has been shown once.
126 displayedWaitMessage: false,
127 // Tell how many rows are available.
128 hasMultipleRows: true,
129 // Set to true to stop the whole thing.
130 processIsStopped: false,
131 // Global stats.
132 globalOptimizedCount: 0,
133 globalGain: 0,
134 globalOriginalSize: 0,
135 globalOptimizedSize: 0,
136 /**
137 * Folder types used in the page.
138 *
139 * @var {object} {
140 * An object of objects. The keys are like: {groupID|context}.
141 *
142 * @type {string} groupID The group ID.
143 * @type {string} context The context.
144 * }
145 */
146 folderTypesData: {},
147
148 // Methods =================================================================================
149
150 /*
151 * Init.
152 */
153 init: function () {
154 var $document = $( d );
155
156 // Overview chart.
157 this.drawOverviewChart();
158
159 this.hasMultipleRows = $( '.imagify-bulk-table [name="group[]"]' ).length > 1;
160
161 // Selectors (like the level selectors).
162 $( '.imagify-selector-button' )
163 .on( 'click.imagify', this.openSelectorFromButton );
164
165 $( '.imagify-selector-list input' )
166 .on( 'change.imagify init.imagify', this.syncSelectorFromRadio )
167 .filter( ':checked' )
168 .trigger( 'init.imagify' );
169
170 $document
171 .on( 'keypress.imagify click.imagify', this.closeSelectors );
172
173 // Other buttons/UI.
174 $( '.imagify-bulk-table [name="group[]"]' )
175 .on( 'change.imagify init.imagify', this.toggleOptimizationButton )
176 .trigger( 'init.imagify' );
177
178 $( '#imagify-bulk-action' )
179 .on( 'click.imagify', this.maybeLaunchAllProcesses );
180
181 // Optimization events.
182 $( w )
183 .on( 'processQueue.imagify', this.processQueue )
184 .on( 'queueEmpty.imagify', this.queueEmpty );
185
186 if ( imagifyBulk.ajaxActions.getStats && $( '.imagify-bulk-table [data-group-id="library"][data-context="wp"]' ).length ) {
187 // On large WP library, don't request stats periodically, only when everything is done.
188 imagifyBulk.imagifybeatIDs.stats = false;
189 }
190
191 if ( imagifyBulk.imagifybeatIDs.stats ) {
192 // Imagifybeat for stats.
193 $document
194 .on( 'imagifybeat-send', this.addStatsImagifybeat )
195 .on( 'imagifybeat-tick', this.processStatsImagifybeat );
196 }
197
198 // Imagifybeat for optimization queue.
199 $document
200 .on( 'imagifybeat-send', this.addQueueImagifybeat )
201 .on( 'imagifybeat-tick', this.processQueueImagifybeat );
202
203 // Imagifybeat for requirements.
204 $document
205 .on( 'imagifybeat-send', this.addRequirementsImagifybeat )
206 .on( 'imagifybeat-tick', this.processRequirementsImagifybeat );
207
208 if ( imagifyBulk.optimizing ) {
209 // Fasten Imagifybeat: 1 tick every 15 seconds, and disable suspend.
210 w.imagify.beat.interval( 15 );
211 w.imagify.beat.disableSuspend();
212 }
213 },
214
215 /*
216 * Get the URL used for ajax requests.
217 *
218 * @param {string} action An ajax action, or part of it.
219 * @param {object} item The current item.
220 * @return {string}
221 */
222 getAjaxUrl: function ( action, item ) {
223 var url = ajaxurl + w.imagify.concat + '_wpnonce=' + imagifyBulk.ajaxNonce + '&action=' + imagifyBulk.ajaxActions[ action ];
224
225 if ( item && item.context ) {
226 url += '&context=' + item.context;
227 }
228
229 if ( item && Number.isInteger( item.level ) ) {
230 url += '&optimization_level=' + item.level;
231 }
232
233 return url;
234 },
235
236 /**
237 * Get folder types used in the page.
238 *
239 * @see this.folderTypesData
240 * @return {object}
241 */
242 getFolderTypes: function () {
243 if ( ! $.isEmptyObject( w.imagify.bulk.folderTypesData ) ) {
244 return w.imagify.bulk.folderTypesData;
245 }
246
247 $( '.imagify-row-folder-type' ).each( function() {
248 var $this = $( this ),
249 data = {
250 groupID: $this.data( 'group-id' ),
251 context: $this.data( 'context' ),
252 level: $this.find( '.imagify-cell-level [name="level[' + $this.data( 'group-id' ) + ']"]:checked' ).val()
253 },
254 key = data.groupID + '|' + data.context;
255
256 w.imagify.bulk.folderTypesData[ key ] = data;
257 } );
258
259 return w.imagify.bulk.folderTypesData;
260 },
261
262 /*
263 * Get the message displayed to the user when (s)he leaves the page.
264 *
265 * @return {string}
266 */
267 getConfirmMessage: function () {
268 return imagifyBulk.labels.processing;
269 },
270
271 /*
272 * Close the given optimization level selector.
273 *
274 * @param {object} $lists A jQuery object.
275 * @param {int} timer Timer in ms to close the selector.
276 */
277 closeLevelSelector: function ( $lists, timer ) {
278 if ( ! $lists || ! $lists.length ) {
279 return;
280 }
281
282 if ( undefined !== timer && timer > 0 ) {
283 w.setTimeout( function() {
284 w.imagify.bulk.closeLevelSelector( $lists );
285 }, timer );
286 return;
287 }
288
289 $lists.attr( 'aria-hidden', 'true' );
290 },
291
292 /*
293 * Stop everything and update the current item status as an error.
294 *
295 * @param {string} errorId An error ID.
296 * @param {object} item The current item.
297 */
298 stopProcess: function ( errorId, item ) {
299 w.imagify.bulk.processIsStopped = true;
300
301 w.imagify.bulk.status[ item.groupID ] = {
302 isError: true,
303 id: errorId
304 };
305
306 $( w ).trigger( 'queueEmpty.imagify' );
307 },
308
309 /*
310 * Tell if we have a blocking error. Can also display an error message in a swal.
311 *
312 * @param {bool} displayErrorMessage False to not display any error message.
313 * @return {bool}
314 */
315 hasBlockingError: function ( displayErrorMessage ) {
316 displayErrorMessage = undefined !== displayErrorMessage && displayErrorMessage;
317
318 if ( imagifyBulk.curlMissing ) {
319 if ( displayErrorMessage ) {
320 w.imagify.bulk.displayError( {
321 html: imagifyBulk.labels.curlMissing
322 } );
323 }
324
325 w.imagify.bulk.processIsStopped = true;
326
327 return true;
328 }
329
330 if ( imagifyBulk.editorMissing ) {
331 if ( displayErrorMessage ) {
332 w.imagify.bulk.displayError( {
333 html: imagifyBulk.labels.editorMissing
334 } );
335 }
336
337 w.imagify.bulk.processIsStopped = true;
338
339 return true;
340 }
341
342 if ( imagifyBulk.extHttpBlocked ) {
343 if ( displayErrorMessage ) {
344 w.imagify.bulk.displayError( {
345 html: imagifyBulk.labels.extHttpBlocked
346 } );
347 }
348
349 w.imagify.bulk.processIsStopped = true;
350
351 return true;
352 }
353
354 if ( imagifyBulk.apiDown ) {
355 if ( displayErrorMessage ) {
356 w.imagify.bulk.displayError( {
357 html: imagifyBulk.labels.apiDown
358 } );
359 }
360
361 w.imagify.bulk.processIsStopped = true;
362
363 return true;
364 }
365
366 if ( ! imagifyBulk.keyIsValid ) {
367 if ( displayErrorMessage ) {
368 w.imagify.bulk.displayError( {
369 title: imagifyBulk.labels.invalidAPIKeyTitle,
370 type: 'info'
371 } );
372 }
373
374 w.imagify.bulk.processIsStopped = true;
375
376 return true;
377 }
378
379 if ( imagifyBulk.isOverQuota ) {
380 if ( displayErrorMessage ) {
381 w.imagify.bulk.displayError( {
382 title: imagifyBulk.labels.overQuotaTitle,
383 html: $( '#tmpl-imagify-overquota-alert' ).html(),
384 type: 'info',
385 customClass: 'imagify-swal-has-subtitle imagify-swal-error-header',
386 showConfirmButton: false
387 } );
388 }
389
390 w.imagify.bulk.processIsStopped = true;
391
392 return true;
393 }
394
395 return false;
396 },
397
398 /*
399 * Display an error message in a modal.
400 *
401 * @param {string} title The modal title.
402 * @param {string} text The modal text.
403 * @param {object} args Other less common args.
404 */
405 displayError: function ( title, text, args ) {
406 var def = {
407 title: '',
408 html: '',
409 type: 'error',
410 customClass: '',
411 width: 620,
412 padding: 0,
413 showCloseButton: true,
414 showConfirmButton: true
415 };
416
417 if ( $.isPlainObject( title ) ) {
418 args = $.extend( {}, def, title );
419 } else {
420 args = args || {};
421 args = $.extend( {}, def, {
422 title: title || '',
423 html: text || ''
424 }, args );
425 }
426
427 args.title = args.title || imagifyBulk.labels.error;
428 args.customClass += ' imagify-sweet-alert';
429
430 swal( args ).catch( swal.noop );
431 },
432
433 /*
434 * Display the share box.
435 */
436 displayShareBox: function () {
437 var $complete, globalSaved;
438
439 if ( ! this.globalGain || this.folderTypesQueue.length ) {
440 this.globalOptimizedCount = 0;
441 this.globalGain = 0;
442 this.globalOriginalSize = 0;
443 this.globalOptimizedSize = 0;
444 return;
445 }
446
447 globalSaved = this.globalOriginalSize - this.globalOptimizedSize;
448
449 $complete = $( '.imagify-row-complete' );
450 $complete.find( '.imagify-ac-rt-total-images' ).html( this.globalOptimizedCount );
451 $complete.find( '.imagify-ac-rt-total-gain' ).html( w.imagify.humanSize( globalSaved, 1 ) );
452 $complete.find( '.imagify-ac-rt-total-original' ).html( w.imagify.humanSize( this.globalOriginalSize, 1 ) );
453 $complete.find( '.imagify-ac-chart' ).attr( 'data-percent', Math.round( this.globalGain ) );
454
455 // Chart.
456 this.drawShareChart();
457
458 $complete.addClass( 'done' ).imagifyShow();
459
460 $( 'html, body' ).animate( {
461 scrollTop: $complete.offset().top
462 }, 200 );
463
464 // Reset the stats.
465 this.globalOptimizedCount = 0;
466 this.globalGain = 0;
467 this.globalOriginalSize = 0;
468 this.globalOptimizedSize = 0;
469 },
470
471 /**
472 * Print optimization stats.
473 *
474 * @param {object} data Object containing all Imagifybeat IDs.
475 */
476 updateStats: function ( data ) {
477 var donutData;
478
479 if ( ! data || ! $.isPlainObject( data ) ) {
480 return;
481 }
482
483 if ( w.imagify.bulk.charts.overview.donut.data ) {
484 donutData = w.imagify.bulk.charts.overview.donut.data.datasets[0].data;
485
486 if ( data.unoptimized_attachments === donutData[0] && data.optimized_attachments === donutData[1] && data.errors_attachments === donutData[2] ) {
487 return;
488 }
489 }
490
491 /**
492 * User account.
493 */
494 data.unconsumed_quota = data.unconsumed_quota.toFixed( 1 ); // A mystery where a float rounded on php side is not rounded here anymore. JavaScript is fun, it always surprises you in a manner you didn't expect.
495 $( '.imagify-meteo-icon' ).html( data.quota_icon );
496 $( '.imagify-unconsumed-percent' ).html( data.unconsumed_quota + '%' );
497 $( '.imagify-unconsumed-bar' ).css( 'width', data.unconsumed_quota + '%' ).parent().attr( 'class', data.quota_class );
498
499 /**
500 * Global chart.
501 */
502 $( '#imagify-overview-chart-percent' ).html( data.optimized_attachments_percent + '<span>%</span>' );
503 $( '.imagify-total-percent' ).html( data.optimized_attachments_percent + '%' );
504
505 w.imagify.bulk.drawOverviewChart( [
506 data.unoptimized_attachments,
507 data.optimized_attachments,
508 data.errors_attachments
509 ] );
510
511 /**
512 * Stats block.
513 */
514 // The total optimized images.
515 $( '#imagify-total-optimized-attachments' ).html( data.already_optimized_attachments );
516
517 // The original bar.
518 $( '#imagify-original-bar' ).find( '.imagify-barnb' ).html( data.original_human );
519
520 // The optimized bar.
521 $( '#imagify-optimized-bar' ).css( 'width', ( 100 - data.optimized_percent ) + '%' ).find( '.imagify-barnb' ).html( data.optimized_human );
522
523 // The Percent data.
524 $( '#imagify-total-optimized-attachments-pct' ).html( data.optimized_percent + '%' );
525 },
526
527 // Event callbacks =========================================================================
528
529 /*
530 * Selector (like optimization level selector): on button click, open the dropdown and focus the current radio input.
531 * The dropdown must be open or the focus event won't be triggered.
532 *
533 * @param {object} e jQuery's Event object.
534 */
535 openSelectorFromButton: function ( e ) {
536 var $list = $( '#' + $( this ).attr( 'aria-controls' ) );
537 // Stop click event from bubbling: this will allow to close the selector list if anything else id clicked.
538 e.stopPropagation();
539 // Close other lists.
540 $( '.imagify-selector-list' ).not( $list ).attr( 'aria-hidden', 'true' );
541 // Open the corresponding list and focus the radio.
542 $list.attr( 'aria-hidden', 'false' ).find( ':checked' ).trigger( 'focus.imagify' );
543 },
544
545 /*
546 * Selector: on radio change, make the row "current" and update the button text.
547 */
548 syncSelectorFromRadio: function () {
549 var $row = $( this ).closest( '.imagify-selector-choice' );
550 // Update rows attributes.
551 $row.addClass( 'imagify-selector-current-value' ).attr( 'aria-current', 'true' ).siblings( '.imagify-selector-choice' ).removeClass( 'imagify-selector-current-value' ).attr( 'aria-current', 'false' );
552 // Change the button text.
553 $row.closest( '.imagify-selector-list' ).siblings( '.imagify-selector-button' ).find( '.imagify-selector-current-value-info' ).html( $row.find( 'label' ).html() );
554 },
555
556 /*
557 * Selector: on Escape or Enter kaystroke, close the dropdown.
558 *
559 * @param {object} e jQuery's Event object.
560 */
561 closeSelectors: function ( e ) {
562 if ( 'keypress' === e.type && 27 !== e.keyCode && 13 !== e.keyCode ) {
563 return;
564 }
565 w.imagify.bulk.closeLevelSelector( $( '.imagify-selector-list[aria-hidden="false"]' ) );
566 },
567
568 /*
569 * Enable or disable the Optimization button depending on the checked checkboxes.
570 * Also, if there is only 1 checkbox in the page, don't allow it to be unchecked.
571 */
572 toggleOptimizationButton: function () {
573 // Prevent uncheck if there is only one checkbox.
574 if ( ! w.imagify.bulk.hasMultipleRows && ! this.checked ) {
575 $( this ).prop( 'checked', true );
576 return;
577 }
578
579 if ( imagifyBulk.optimizing ) {
580 $( '#imagify-bulk-action' ).prop( 'disabled', true );
581
582 return;
583 }
584
585 // Enable or disable the Optimization button.
586 if ( $( '.imagify-bulk-table [name="group[]"]:checked' ).length ) {
587 $( '#imagify-bulk-action' ).prop( 'disabled', false );
588 } else {
589 $( '#imagify-bulk-action' ).prop( 'disabled', true );
590 }
591 },
592
593 /*
594 * Maybe display a modal, then launch all processes.
595 */
596 maybeLaunchAllProcesses: function () {
597 var $infosModal;
598
599 if ( $( this ).prop('disabled') ) {
600 return;
601 }
602
603 if ( ! $( '.imagify-bulk-table [name="group[]"]:checked' ).length ) {
604 return;
605 }
606
607 if ( w.imagify.bulk.hasBlockingError( true ) ) {
608 return;
609 }
610
611 $infosModal = $( '#tmpl-imagify-bulk-infos' );
612
613 if ( ! $infosModal.length ) {
614 w.imagify.bulk.launchAllProcesses();
615 return;
616 }
617
618 // Swal Information before loading the optimize process.
619 swal( {
620 title: imagifyBulk.labels.bulkInfoTitle,
621 html: $infosModal.html(),
622 type: '',
623 customClass: 'imagify-sweet-alert imagify-swal-has-subtitle imagify-before-bulk-infos',
624 showCancelButton: true,
625 padding: 0,
626 width: 554,
627 confirmButtonText: imagifyBulk.labels.confirmBulk,
628 cancelButtonText: imagifySwal.labels.cancelButtonText,
629 reverseButtons: true
630 } ).then( function() {
631 var $row = $( '.imagify-bulk-table [name="group[]"]:checked' ).first().closest( '.imagify-row-folder-type' );
632
633 $.get( w.imagify.bulk.getAjaxUrl( 'bulkInfoSeen', {
634 context: $row.data( 'context' )
635 } ) );
636
637 $infosModal.remove();
638
639 w.imagify.bulk.launchAllProcesses();
640 } ).catch( swal.noop );
641 },
642
643 /*
644 * Build the queue and launch all processes.
645 */
646 launchAllProcesses: function () {
647 var $w = $( w ),
648 $button = $( '#imagify-bulk-action' );
649
650 // Disable the button.
651 $button.prop( 'disabled', true ).find( '.dashicons' ).addClass( 'rotate' );
652
653 // Hide the "Complete" message.
654 $( '.imagify-row-complete' ).imagifyHide( 200, function() {
655 $( this ).removeClass( 'done' );
656 } );
657
658 // Make sure to reset properties.
659 this.folderTypesQueue = [];
660 this.status = {};
661 this.displayedWaitMessage = false;
662 this.processIsStopped = false;
663 this.globalOptimizedCount = 0;
664 this.globalGain = 0;
665 this.globalOriginalSize = 0;
666 this.globalOptimizedSize = 0;
667
668 $( '.imagify-bulk-table [name="group[]"]:checked' ).each( function() {
669 var $checkbox = $( this ),
670 $row = $checkbox.closest( '.imagify-row-folder-type' ),
671 groupID = $row.data( 'group-id' ),
672 context = $row.data( 'context' ),
673 level = $row.find( '.imagify-cell-level [name="level[' + groupID + ']"]:checked' ).val();
674
675 // Build the queue.
676 w.imagify.bulk.folderTypesQueue.push( {
677 groupID: groupID,
678 context: context,
679 level: undefined === level ? -1 : parseInt( level, 10 )
680 } );
681
682 // Set the status.
683 w.imagify.bulk.status[ groupID ] = {
684 isError: false,
685 id: 'waiting'
686 };
687 } );
688
689 // Fasten Imagifybeat: 1 tick every 15 seconds, and disable suspend.
690 w.imagify.beat.interval( 15 );
691 w.imagify.beat.disableSuspend();
692
693 // Process the queue.
694 $w.trigger( 'processQueue.imagify' );
695 },
696
697 /*
698 * Process the first item in the queue.
699 */
700 processQueue: function () {
701 var $row, $table, $progressBar, $progress;
702
703 if ( w.imagify.bulk.processIsStopped ) {
704 return;
705 }
706
707 if ( ! w.imagify.bulk.displayedWaitMessage ) {
708 // Display an alert to wait.
709 swal( {
710 title: imagifyBulk.labels.waitTitle,
711 html: imagifyBulk.labels.waitText,
712 showConfirmButton: false,
713 padding: 0,
714 imageUrl: imagifyBulk.waitImageUrl,
715 customClass: 'imagify-sweet-alert'
716 } ).catch( swal.noop );
717 w.imagify.bulk.displayedWaitMessage = true;
718 }
719
720 w.imagify.bulk.folderTypesQueue.forEach( function( item ) {
721 // Start async process for current context
722 $.get( w.imagify.bulk.getAjaxUrl( 'bulkProcess', item ) )
723 .done( function( response ) {
724 var errorMessage;
725
726 swal.close();
727
728 if ( response.data && response.data.message ) {
729 errorMessage = response.data.message;
730 } else {
731 errorMessage = imagifyBulk.ajaxErrorText;
732 }
733
734 if ( ! response.success ) {
735 // Error.
736 w.imagify.bulk.stopProcess( errorMessage, item );
737 return;
738 }
739
740 if ( ! response.data || ! ( $.isPlainObject( response.data ) || $.isArray( response.data ) ) ) {
741 // Error: should be an array if empty, or an object otherwize.
742 w.imagify.bulk.stopProcess( errorMessage, item );
743 return;
744 }
745
746 // Success.
747 if ( response.success ) {
748 $row = $( '#cb-select-' + item.groupID ).closest( '.imagify-row-folder-type' );
749 $table = $row.closest( '.imagify-bulk-table' );
750 $progressBar = $table.find( '.imagify-row-progress' );
751 $progress = $progressBar.find( '.bar' );
752
753 $row.find( '.imagify-cell-checkbox-loader' ).removeClass( 'hidden' ).attr( 'aria-hidden', 'false' );
754 $row.find( '.imagify-cell-checkbox-box' ).addClass( 'hidden' ).attr( 'aria-hidden', 'true' );
755
756 // Reset and display the progress bar.
757 $progress.css( 'width', '0%' ).find( '.percent' ).text( '0%' );
758 $progressBar.slideDown().attr( 'aria-hidden', 'false' );
759 }
760 } )
761 .fail( function() {
762 // Error.
763 w.imagify.bulk.stopProcess( 'get-unoptimized-images', item );
764 } );
765 } );
766 },
767
768 /*
769 * End.
770 */
771 queueEmpty: function () {
772 var $tables = $( '.imagify-bulk-table' ),
773 errorArgs = {},
774 hasError = false,
775 noImages = true,
776 errorMsg = '';
777
778 // Reset Imagifybeat interval and enable suspend.
779 w.imagify.beat.resetInterval();
780 w.imagify.beat.enableSuspend();
781
782 // Reset the queue.
783 w.imagify.bulk.folderTypesQueue = [];
784
785 // Display the share box.
786 w.imagify.bulk.displayShareBox();
787
788 // Fetch and display generic stats if stats via Imagifybeat are disabled.
789 if ( ! imagifyBulk.imagifybeatIDs.stats ) {
790 $.get( w.imagify.bulk.getAjaxUrl( 'getStats' ), {
791 types: w.imagify.bulk.getFolderTypes()
792 } )
793 .done( function( response ) {
794 if ( response.success ) {
795 w.imagify.bulk.updateStats( response.data );
796 }
797 } );
798 }
799
800 // Maybe display error.
801 if ( ! $.isEmptyObject( w.imagify.bulk.status ) ) {
802 $.each( w.imagify.bulk.status, function( groupID, typeStatus ) {
803 if ( ! typeStatus.isError ) {
804 noImages = false;
805 } else if ( 'no-images' !== typeStatus.id && typeStatus.isError ) {
806 hasError = typeStatus.id;
807 noImages = false;
808 return false;
809 }
810 } );
811
812 if ( hasError ) {
813 if ( 'invalid-api-key' === hasError ) {
814 errorArgs = {
815 title: imagifyBulk.labels.invalidAPIKeyTitle,
816 type: 'info'
817 };
818 }
819 else if ( 'over-quota' === hasError ) {
820 errorArgs = {
821 title: imagifyBulk.labels.overQuotaTitle,
822 html: $( '#tmpl-imagify-overquota-alert' ).html(),
823 type: 'info',
824 customClass: 'imagify-swal-has-subtitle imagify-swal-error-header',
825 showConfirmButton: false
826 };
827 }
828 else if ( 'get-unoptimized-images' === hasError || 'consumed-all-data' === hasError ) {
829 errorArgs = {
830 title: imagifyBulk.labels.getUnoptimizedImagesErrorTitle,
831 html: imagifyBulk.labels.getUnoptimizedImagesErrorText,
832 type: 'info'
833 };
834 }
835 w.imagify.bulk.displayError( errorArgs );
836 }
837 else if ( noImages ) {
838 if ( Object.prototype.hasOwnProperty.call( imagifyBulk.labels.nothingToDoText, w.imagify.bulk.imagifyAction ) ) {
839 errorMsg = imagifyBulk.labels.nothingToDoText[ w.imagify.bulk.imagifyAction ];
840 } else {
841 errorMsg = imagifyBulk.labels.nothingToDoText.optimize;
842 }
843 w.imagify.bulk.displayError( {
844 title: imagifyBulk.labels.nothingToDoTitle,
845 html: errorMsg,
846 type: 'info'
847 } );
848 }
849 }
850
851 // Reset status.
852 w.imagify.bulk.status = {};
853
854 // Reset the progress bars.
855 $tables.find( '.imagify-row-progress' ).slideUp().attr( 'aria-hidden', 'true' ).find( '.bar' ).removeAttr( 'style' ).find( '.percent' ).text( '0%' );
856
857 $tables.find( '.imagify-cell-checkbox-loader' ).each( function() {
858 $(this).addClass( 'hidden' ).attr( 'aria-hidden', 'true' );
859 } );
860
861 $tables.find( '.imagify-cell-checkbox-box' ).each( function() {
862 $(this).removeClass( 'hidden' ).attr( 'aria-hidden', 'false' );
863 } );
864
865 // Enable (or not) the main button.
866 if ( $( '.imagify-bulk-table [name="group[]"]:checked' ).length ) {
867 $( '#imagify-bulk-action' ).prop( 'disabled', false ).find( '.dashicons' ).removeClass( 'rotate' );
868 } else {
869 $( '#imagify-bulk-action' ).find( '.dashicons' ).removeClass( 'rotate' );
870 }
871 },
872
873 // Imagifybeat =============================================================================
874
875 /**
876 * Add a Imagifybeat ID for global stats on "imagifybeat-send" event.
877 *
878 * @param {object} e Event object.
879 * @param {object} data Object containing all Imagifybeat IDs.
880 */
881 addStatsImagifybeat: function ( e, data ) {
882 data[ imagifyBulk.imagifybeatIDs.stats ] = Object.keys( w.imagify.bulk.getFolderTypes() );
883 },
884
885 /**
886 * Listen for the custom event "imagifybeat-tick" on $(document).
887 * It allows to update various data periodically.
888 *
889 * @param {object} e Event object.
890 * @param {object} data Object containing all Imagifybeat IDs.
891 */
892 processStatsImagifybeat: function ( e, data ) {
893 if ( typeof data[ imagifyBulk.imagifybeatIDs.stats ] !== 'undefined' ) {
894 w.imagify.bulk.updateStats( data[ imagifyBulk.imagifybeatIDs.stats ] );
895 }
896 },
897
898 /**
899 * Add a Imagifybeat ID on "imagifybeat-send" event to sync the optimization queue.
900 *
901 * @param {object} e Event object.
902 * @param {object} data Object containing all Imagifybeat IDs.
903 */
904 addQueueImagifybeat: function ( e, data ) {
905 data[ imagifyBulk.imagifybeatIDs.queue ] = Object.values( w.imagify.bulk.getFolderTypes() );
906 },
907
908 /**
909 * Listen for the custom event "imagifybeat-tick" on $(document).
910 * It allows to update various data periodically.
911 *
912 * @param {object} e Event object.
913 * @param {object} data Object containing all Imagifybeat IDs.
914 */
915 processQueueImagifybeat: function ( e, data ) {
916 var queue, $row, $progress, $bar;
917
918 if ( typeof data[ imagifyBulk.imagifybeatIDs.queue ] !== 'undefined' ) {
919 queue = data[ imagifyBulk.imagifybeatIDs.queue ];
920
921 if ( false !== queue.result ) {
922 w.imagify.bulk.globalOriginalSize = queue.result.original_size;
923 w.imagify.bulk.globalOptimizedSize = queue.result.optimized_size;
924 w.imagify.bulk.globalOptimizedCount = queue.result.total;
925 w.imagify.bulk.globalGain = w.imagify.bulk.globalOptimizedSize * 100 / w.imagify.bulk.globalOriginalSize;
926 }
927
928 if ( ! w.imagify.bulk.processIsStopped && w.imagify.bulk.hasBlockingError( true ) ) {
929 $( w ).trigger( 'queueEmpty.imagify' );
930 return;
931 }
932
933 if ( Object.prototype.hasOwnProperty.call( queue, 'groups_data' ) ) {
934 Object.entries( queue.groups_data ).forEach( function( item ) {
935 $row = $( '[data-context=' + item[0] + ']' );
936
937 $row.children( '.imagify-cell-count-optimized' ).first().html( item[1]['count-optimized'] );
938 $row.children( '.imagify-cell-count-errors' ).first().html( item[1]['count-errors'] );
939 $row.children( '.imagify-cell-optimized-size-size' ).first().html( item[1]['optimized-size'] );
940 $row.children( '.imagify-cell-original-size-size' ).first().html( item[1]['original-size'] );
941 } );
942 }
943
944 if ( 0 === queue.remaining ) {
945 $( w ).trigger( 'queueEmpty.imagify' );
946 return;
947 }
948
949 $progress = $( '.imagify-row-progress' );
950 $bar = $progress.find( '.bar' );
951
952 $bar.css( 'width', queue.percentage + '%' ).find( '.percent' ).html( queue.percentage + '%' );
953 $progress.slideDown().attr( 'aria-hidden', 'false' );
954 }
955 },
956
957 /**
958 * Add a Imagifybeat ID for requirements on "imagifybeat-send" event.
959 *
960 * @param {object} e Event object.
961 * @param {object} data Object containing all Imagifybeat IDs.
962 */
963 addRequirementsImagifybeat: function ( e, data ) {
964 data[ imagifyBulk.imagifybeatIDs.requirements ] = 1;
965 },
966
967 /**
968 * Listen for the custom event "imagifybeat-tick" on $(document).
969 * It allows to update requirements status periodically.
970 *
971 * @param {object} e Event object.
972 * @param {object} data Object containing all Imagifybeat IDs.
973 */
974 processRequirementsImagifybeat: function ( e, data ) {
975 if ( typeof data[ imagifyBulk.imagifybeatIDs.requirements ] === 'undefined' ) {
976 return;
977 }
978
979 data = data[ imagifyBulk.imagifybeatIDs.requirements ];
980
981 imagifyBulk.curlMissing = data.curl_missing;
982 imagifyBulk.editorMissing = data.editor_missing;
983 imagifyBulk.extHttpBlocked = data.external_http_blocked;
984 imagifyBulk.apiDown = data.api_down;
985 imagifyBulk.keyIsValid = data.key_is_valid;
986 imagifyBulk.isOverQuota = data.is_over_quota;
987 },
988
989 // Charts ==================================================================================
990
991 /**
992 * Overview chart.
993 * Used for the big overview chart.
994 */
995 drawOverviewChart: function ( data ) {
996 var initData, legend;
997
998 if ( ! this.charts.overview.canvas ) {
999 this.charts.overview.canvas = d.getElementById( 'imagify-overview-chart' );
1000
1001 if ( ! this.charts.overview.canvas ) {
1002 return;
1003 }
1004 }
1005
1006 data = data && $.isArray( data ) ? data : [];
1007
1008 if ( this.charts.overview.donut ) {
1009 // Update existing donut.
1010 if ( data.length ) {
1011 if ( data.reduce( function( a, b ) { return a + b; }, 0 ) === 0 ) {
1012 data[0] = 1;
1013 }
1014
1015 this.charts.overview.donut.data.datasets[0].data = data;
1016 this.charts.overview.donut.update();
1017 }
1018 return;
1019 }
1020
1021 // Create new donut.
1022 this.charts.overview.data.datasets[0].data = [
1023 parseInt( this.charts.overview.canvas.getAttribute( 'data-unoptimized' ), 10 ),
1024 parseInt( this.charts.overview.canvas.getAttribute( 'data-optimized' ), 10 ),
1025 parseInt( this.charts.overview.canvas.getAttribute( 'data-errors' ), 10 )
1026 ];
1027 initData = $.extend( {}, this.charts.overview.data );
1028
1029 if ( data.length ) {
1030 initData.datasets[0].data = data;
1031 }
1032
1033 if ( initData.datasets[0].data.reduce( function( a, b ) { return a + b; }, 0 ) === 0 ) {
1034 initData.datasets[0].data[0] = 1;
1035 }
1036
1037 this.charts.overview.donut = new w.imagify.Chart( this.charts.overview.canvas, {
1038 type: 'doughnut',
1039 data: initData,
1040 options: {
1041 plugins: {
1042 legend: {
1043 display: false
1044 }
1045 },
1046 events: [],
1047 animation: {
1048 easing: 'easeOutBounce'
1049 },
1050 tooltips: {
1051 displayColors: false,
1052 callbacks: {
1053 label: function( tooltipItem, localData ) {
1054 return localData.datasets[ tooltipItem.datasetIndex ].data[ tooltipItem.index ];
1055 }
1056 }
1057 },
1058 responsive: false,
1059 cutout: 75
1060 }
1061 } );
1062
1063 // Then generate the legend and insert it to your page somewhere.
1064 legend = '<ul class="imagify-doughnut-legend">';
1065
1066 $.each( initData.labels, function( i, label ) {
1067 legend += '<li><span style="background-color:' + initData.datasets[0].backgroundColor[ i ] + '"></span>' + label + '</li>';
1068 } );
1069
1070 legend += '</ul>';
1071
1072 d.getElementById( 'imagify-overview-chart-legend' ).innerHTML = legend;
1073 },
1074
1075 /*
1076 * Share Chart.
1077 * Used for the chart in the share box.
1078 */
1079 drawShareChart: function () {
1080 var value;
1081
1082 if ( ! this.charts.share.canvas ) {
1083 this.charts.share.canvas = d.getElementById( 'imagify-ac-chart' );
1084
1085 if ( ! this.charts.share.canvas ) {
1086 return;
1087 }
1088 }
1089
1090 value = parseInt( $( this.charts.share.canvas ).closest( '.imagify-ac-chart' ).attr( 'data-percent' ), 10 );
1091
1092 if ( this.charts.share.donut ) {
1093 // Update existing donut.
1094 this.charts.share.donut.data.datasets[0].data[0] = value;
1095 this.charts.share.donut.data.datasets[0].data[1] = 100 - value;
1096 this.charts.share.donut.update();
1097 return;
1098 }
1099
1100 // Create new donut.
1101 this.charts.share.donut = new w.imagify.Chart( this.charts.share.canvas, {
1102 type: 'doughnut',
1103 data: {
1104 datasets: [{
1105 data: [ value, 100 - value ],
1106 backgroundColor: [ '#40B1D0', '#FFFFFF' ],
1107 borderWidth: 0
1108 }]
1109 },
1110 options: {
1111 plugins: {
1112 legend: {
1113 display: false
1114 }
1115 },
1116 events: [],
1117 animation: {
1118 easing: 'easeOutBounce'
1119 },
1120 tooltips: {
1121 enabled: false
1122 },
1123 responsive: false,
1124 cutoutPercentage: 70
1125 }
1126 } );
1127 }
1128 };
1129
1130 w.imagify.bulk.init();
1131
1132 if (imagifyBulk.isOverQuota) {
1133 w.imagify.bulk.displayError( {
1134 title: imagifyBulk.labels.overQuotaTitle,
1135 html: $( '#tmpl-imagify-overquota-alert' ).html(),
1136 type: 'info',
1137 customClass: 'imagify-swal-has-subtitle imagify-swal-error-header',
1138 showConfirmButton: false
1139 } );
1140 }
1141
1142
1143 } )(jQuery, document, window);
1144