PluginProbe
UpdraftCentral Dashboard / trunk
UpdraftCentral Dashboard vtrunk
0.8.33 0.7.2 0.7.3 0.7.4 0.8.0 0.8.1 0.8.10 0.8.11 0.8.12 0.8.13 0.8.14 0.8.15 0.8.16 0.8.17 0.8.18 0.8.19 0.8.2 0.8.20 0.8.21 0.8.22 0.8.23 0.8.24 0.8.25 0.8.26 0.8.27 All 51 releases
updraftcentral / js / uc-library.js

uc-library.js in UpdraftCentral Dashboard trunk, at js/uc-library.js

7,628 lines 267.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 jQuery(function($) {
2 window.UpdraftCentral_Library = new Fn_UpdraftCentral_Library();
3 window.UpdraftCentral_Storage = new UpdraftCentral_Storage();
4 });
5
6 /**
7 * Storage Class
8 *
9 * Manages CRUD operation for UpdraftCentral's local storage functionality either through
10 * IndexedDB or LocalStorage
11 *
12 * @uses {UpdraftCentral.storage_set}
13 * @uses {UpdraftCentral.storage_get}
14 * @uses {UpdraftCentral.storage_remove}
15 * @uses {UpdraftCentral.storage_purge}
16 * @constructor
17 */
18 function UpdraftCentral_Storage() {
19 var self = this;
20 var $ = jQuery;
21 var indexedDB;
22 var version = 1;
23 this.maximum_age = 600;
24
25 /**
26 * Checks whether the browser currently supports IndexedDB
27 *
28 * @return {boolean}
29 */
30 function has_indexedDB() {
31 indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB || window.shimIndexedDB;
32 if (!indexedDB) {
33 return false;
34 }
35 return true;
36 }
37
38 /**
39 * Opens and build an IndexedDB database and store for local storage usage
40 *
41 * @return {Promise}
42 */
43 function open_db() {
44 var deferred = $.Deferred();
45 if (!indexedDB) deferred.resolve(null);
46
47 var request = indexedDB.open('udc_local_database', version);
48 request.onupgradeneeded = function(event) {
49 event.target.result.createObjectStore('udc_store', { keyPath: 'name' });
50 };
51
52 request.onsuccess = function(event) {
53 deferred.resolve(event.target.result);
54 };
55
56 request.onerror = function(event) {
57 deferred.reject(event.target.error);
58 };
59 return deferred.promise();
60 }
61
62 /**
63 * Executes requested storage action
64 *
65 * @param {string} action The type of action to execute
66 * @param {object} db The currently opened IndexedDB database
67 * @param {string} key The key of the item
68 * @param {object} data The data to process or save
69 *
70 * @return {Promise}
71 */
72 function apply_requested_action(action, db, key, data) {
73 var deferred = $.Deferred();
74 var transaction = db.transaction('udc_store', 'readwrite');
75 var store = transaction.objectStore('udc_store');
76 var request;
77
78 switch (action) {
79 case 'set':
80 request = store.put(data);
81 break;
82 case 'get':
83 request = store.get(key);
84 break;
85 case 'delete':
86 request = store.delete(key);
87 break;
88 case 'count':
89 request = store.count();
90 break;
91 case 'clear':
92 request = store.clear();
93 break;
94 default:
95 break;
96 }
97
98 request.onsuccess = function(event) {
99 deferred.resolve(event.target.result);
100 };
101
102 request.onerror = function(event) {
103 deferred.reject(event.target.error);
104 }
105
106 transaction.onerror = function(event) {
107 deferred.reject(event.target.error);
108 };
109 return deferred.promise();
110 }
111
112 /**
113 * Generates an internal updraftcentral key to be use in local storage.
114 * This ensures that we don't clash or override other storage items saved
115 * by other plugins, etc.
116 *
117 * @param {string} key The submitted key
118 *
119 * @return {string}
120 */
121 function get_record_key(key) {
122 return 'updraftcentral_'+key;
123 }
124
125 /**
126 * A helper method to quickly identify which type of local storage UpdraftCentral
127 * is currently using.
128 *
129 * @return {string}
130 */
131 this.get_storage_type = function() {
132 if (has_indexedDB()) {
133 return 'indexedDB';
134 }
135 return 'localStorage';
136 }
137
138 /**
139 * Saves an object/document to the local storage
140 *
141 * @param {string} key The key of the item
142 * @param {mixed} value The object or document to save
143 * @param {boolean} can_expire Indicate whether this value will expire
144 *
145 * @return {Promise}
146 */
147 this.set_item = function(key, value, can_expire) {
148 var deferred = $.Deferred();
149 if (has_indexedDB()) {
150 open_db().then(function(db) {
151 if (db) {
152 var transaction = db.transaction('udc_store', 'readwrite');
153 var store = transaction.objectStore('udc_store');
154 var record_key = get_record_key(key);
155
156 var data = {
157 name: record_key,
158 value: JSON.stringify(value),
159 epoch_time: null,
160 };
161
162 if ('undefined' !== typeof can_expire && can_expire) {
163 data.epoch_time = Math.floor(Date.now() / 1000);
164 }
165
166 apply_requested_action('set', db, record_key, data).then(function(result) {
167 deferred.resolve(result);
168 }).fail(function(response) {
169 if (UpdraftCentral.get_debug_level() > 1) {
170 console.log('UpdraftCentral_Storage.set_item(key='+key+', can_expire='+can_expire+'): error saving item. Response object and submitted value follows.');
171 console.log(response);
172 console.log(value);
173 }
174 deferred.reject(false);
175 });
176 } else {
177 if (UpdraftCentral.get_debug_level() > 1) console.log('UpdraftCentral_Storage::set_item: no valid local database.');
178 deferred.reject(false);
179 }
180 }).fail(function(response) {
181 if (UpdraftCentral.get_debug_level() > 1) console.log('UpdraftCentral_Storage::set_item: failed to open local database.');
182 deferred.reject(response);
183 });
184 } else {
185 UpdraftCentral.storage_set(key, value, can_expire);
186 deferred.resolve();
187 }
188 return deferred.promise();
189 }
190
191 /**
192 * Retrieves an item from the local storage
193 *
194 * @param {string} key The key of the item
195 * @param {integer} maximum_age The maximum age of expiration
196 *
197 * @return {Promise}
198 */
199 this.get_item = function(key, maximum_age) {
200 var deferred = $.Deferred();
201 if (has_indexedDB()) {
202 open_db().then(function(db) {
203 if (db) {
204 var record_key = get_record_key(key);
205 apply_requested_action('get', db, record_key).then(function(record) {
206 var result = (record && record.hasOwnProperty('value')) ? JSON.parse(record.value) : record;
207 if (record && 'undefined' !== typeof maximum_age && maximum_age > 0) {
208 self.maximum_age = maximum_age;
209
210 var stored_at = record.epoch_time;
211 if (null === stored_at) {
212 deferred.resolve(stored_at);
213 } else {
214 var epoch_time = Math.floor(Date.now() / 1000);
215 var stored_ago = epoch_time - stored_at;
216 if (UpdraftCentral.get_debug_level() > 1) {
217 console.log('UpdraftCentral_Storage.get_item(key='+key+', maximum_age='+maximum_age+'): stored_at='+stored_at+', epoch_time='+epoch_time+', stored_ago='+stored_ago);
218 }
219 if (stored_ago > maximum_age) {
220 deferred.resolve(null);
221 } else {
222 deferred.resolve(result);
223 }
224 }
225 } else {
226 deferred.resolve(result);
227 }
228 }).fail(function(response) {
229 if (UpdraftCentral.get_debug_level() > 1) {
230 console.log('UpdraftCentral_Storage.get_item(key='+key+', maximum_age='+maximum_age+'): error retrieving item. Response object follows.');
231 console.log(response);
232 }
233 deferred.reject(response);
234 });
235 } else {
236 if (UpdraftCentral.get_debug_level() > 1) console.log('UpdraftCentral_Storage::get_item: no valid local database.');
237 deferred.reject(null);
238 }
239 }).fail(function(response) {
240 if (UpdraftCentral.get_debug_level() > 1) console.log('UpdraftCentral_Storage::get_item: failed to open local database.');
241 deferred.reject(response);
242 });
243 } else {
244 deferred.resolve(UpdraftCentral.storage_get(key, maximum_age));
245 }
246 return deferred.promise();
247 }
248
249 /**
250 * Removes an item from the local storage
251 *
252 * @param {string} key The key of the item
253 *
254 * @return {Promise}
255 */
256 this.remove_item = function(key) {
257 var deferred = $.Deferred();
258 if (has_indexedDB()) {
259 open_db().then(function(db) {
260 if (db) {
261 var record_key = get_record_key(key);
262 apply_requested_action('delete', db, record_key).then(function(result) {
263 deferred.resolve(result);
264 }).fail(function(response) {
265 if (UpdraftCentral.get_debug_level() > 1) {
266 console.log('UpdraftCentral_Storage.remove_item(key='+key+'): error removing item. Response object follows.');
267 console.log(response);
268 }
269 deferred.reject(response);
270 });
271 } else {
272 if (UpdraftCentral.get_debug_level() > 1) console.log('UpdraftCentral_Storage::remove_item: no valid local database.');
273 deferred.reject(false);
274 }
275 }).fail(function(response) {
276 if (UpdraftCentral.get_debug_level() > 1) console.log('UpdraftCentral_Storage::remove_item: failed to open local database.');
277 deferred.reject(response);
278 });
279 } else {
280 UpdraftCentral.storage_remove(key);
281 deferred.resolve();
282 }
283 return deferred.promise();
284 }
285
286 /**
287 * Counts the number of items from the local storage
288 *
289 * @return {Promise}
290 */
291 this.count = function() {
292 var deferred = $.Deferred();
293 if (has_indexedDB()) {
294 open_db().then(function(db) {
295 if (db) {
296 apply_requested_action('count', db).then(function(result) {
297 deferred.resolve(result);
298 }).fail(function(response) {
299 if (UpdraftCentral.get_debug_level() > 1) {
300 console.log('UpdraftCentral_Storage.count(): error retrieving local database record count. Response object follows.');
301 console.log(response);
302 }
303 deferred.reject(response);
304 });
305 } else {
306 if (UpdraftCentral.get_debug_level() > 1) console.log('UpdraftCentral_Storage::count: no valid local database.');
307 deferred.reject(0);
308 }
309 }).fail(function(response) {
310 if (UpdraftCentral.get_debug_level() > 1) console.log('UpdraftCentral_Storage::count: failed to open local database.');
311 deferred.reject(response);
312 });
313 } else {
314 // Here, we're only counting items from localStorage that are related to UpdraftCentral.
315 var total_items_in_storage = 0;
316 for (i = localStorage.length - 1; i >=0; i--) {
317 var key = localStorage.key(i);
318 if (null === key) { continue; }
319 if (key.substring(0, 15) == 'updraftcentral_') {
320 total_items_in_storage++;
321 }
322 }
323 deferred.resolve(total_items_in_storage);
324 }
325 return deferred.promise();
326 }
327
328 /**
329 * Purge expired items from the local storage
330 *
331 * @return {Promise}
332 */
333 this.purge = function() {
334 var deferred = $.Deferred();
335 if (has_indexedDB()) {
336 open_db().then(function(db) {
337 if (db) {
338 var transaction = db.transaction('udc_store', 'readwrite');
339 var store = transaction.objectStore('udc_store');
340 var request = store.openCursor();
341 var purged = 0;
342
343 request.onsuccess = function(event) {
344 var cursor = event.target.result;
345 if (cursor) {
346 var record = cursor.value;
347 if (record && 'undefined' !== typeof self.maximum_age && self.maximum_age > 0) {
348 var stored_at = record.epoch_time;
349 if (null === stored_at) {
350 cursor.continue();
351 } else {
352 var epoch_time = Math.floor(Date.now() / 1000);
353 var stored_ago = epoch_time - stored_at;
354 if (stored_ago > self.maximum_age) {
355 var delete_request = cursor.delete();
356 delete_request.onsuccess = function(event) {
357 purged++;
358 }
359 }
360 }
361 }
362 cursor.continue();
363 } else {
364 deferred.resolve(purged);
365 }
366 };
367
368 request.onerror = function(event) {
369 if (UpdraftCentral.get_debug_level() > 1) {
370 console.log('UpdraftCentral_Storage.purge(): error openning store cursor. Error object follows.');
371 console.log(event.target.error);
372 }
373 deferred.reject(event.target.error);
374 }
375
376 transaction.onerror = function(event) {
377 if (UpdraftCentral.get_debug_level() > 1) {
378 console.log('UpdraftCentral_Storage.purge(): error purging old records. Error object follows.');
379 console.log(event.target.error);
380 }
381 deferred.reject(event.target.error);
382 };
383 } else {
384 if (UpdraftCentral.get_debug_level() > 1) console.log('UpdraftCentral_Storage::purge: no valid local database.');
385 deferred.reject(0);
386 }
387 }).fail(function(response) {
388 if (UpdraftCentral.get_debug_level() > 1) console.log('UpdraftCentral_Storage::purge: failed to open local database.');
389 deferred.reject(response);
390 });
391 } else {
392 deferred.resolve(UpdraftCentral.storage_purge());
393 }
394 return deferred.promise();
395 }
396
397 /**
398 * Clears all items from the local storage. Clearing will only affect those
399 * items created for UpdraftCentral.
400 *
401 * @return {Promise}
402 */
403 this.clear = function() {
404 var deferred = $.Deferred();
405 if (has_indexedDB()) {
406 open_db().then(function(db) {
407 if (db) {
408 apply_requested_action('clear', db).then(function(result) {
409 deferred.resolve(result);
410 }).fail(function(response) {
411 if (UpdraftCentral.get_debug_level() > 1) {
412 console.log('UpdraftCentral_Storage.clear(): error clearing local database records. Response object follows.');
413 console.log(response);
414 }
415 deferred.reject(response);
416 });
417 } else {
418 if (UpdraftCentral.get_debug_level() > 1) console.log('UpdraftCentral_Storage::clear: no valid local database.');
419 deferred.reject(false);
420 }
421 }).fail(function(response) {
422 if (UpdraftCentral.get_debug_level() > 1) console.log('UpdraftCentral_Storage::clear: failed to open local database.');
423 deferred.reject(response);
424 });
425 } else {
426 // Here, we're only clearing items from localStorage that are related to UpdraftCentral.
427 for (i = localStorage.length - 1; i >=0; i--) {
428 var key = localStorage.key(i);
429 if (null === key) { continue; }
430 if (key.substring(0, 15) == 'updraftcentral_') {
431 UpdraftCentral.storage_remove(key);
432 }
433 }
434 deferred.resolve();
435 }
436 return deferred.promise();
437 }
438 }
439
440 /**
441 * Post Class
442 *
443 * Contains all common properties and methods of the post and page module classes, eliminating
444 * redundant/duplicate codebase for easier and quick (one-time) edit that applies to both
445 * modules instantenously.
446 *
447 * @example
448 * function UpdraftCentral_Page_Management() {
449 * var self = this;
450 * var $ = jQuery;
451 * this.type = 'page';
452 * var common = new UpdraftCentral_Post(this);
453 * ...
454 * ...
455 * ...
456 * }
457 *
458 * @constructor
459 */
460 function UpdraftCentral_Post(module) {
461 var self = this;
462 var $ = jQuery;
463 this.pagination;
464 this.current_group;
465 this.current_section;
466 this.manage_data = new UpdraftCentral_Collection();
467 this.uc_editor = {
468 edits: new UpdraftCentral_Collection(),
469 state: new UpdraftCentral_Collection()
470 }
471 this.quick_edits = new UpdraftCentral_Collection();
472 this.dirty_edits = new UpdraftCentral_Collection();
473 this.editor_reload_required = false;
474 this.wp_versions = new UpdraftCentral_Collection();
475 this.reset_info = { data: null, previous_status: null, location: null };
476 var filtered_posts = new UpdraftCentral_Collection();
477 var numberposts = 50;
478
479 /**
480 * Initializes event handlers for certain events/actions when managing post or pages.
481 *
482 * @return {void}
483 */
484 this.init = function() {
485 $(document.body).on('click', 'div.media-modal li.attachment', function() {
486 self.set_selected_image($(this));
487 });
488
489 $(document.body).on('mouseover', '.components-notice-list', function() {
490 $(this).find('a.components-notice__action.is-link').attr('target', '_blank');
491 });
492
493 $(document.body).on('click', 'input[name="uc-dont-show-again"]', function() {
494 if ('undefined' !== typeof localStorage && localStorage) {
495 if ($(this).is(':checked')) {
496 localStorage.setItem('uc-gutenberg-dialog-no-show', 1);
497 } else {
498 localStorage.removeItem('uc-gutenberg-dialog-no-show');
499 }
500 }
501 });
502
503 $(document.body).on('click', 'label.uc-dont-show-again-label', function() {
504 $(this).siblings('input[name="uc-dont-show-again"]').trigger('dblclick');
505 });
506
507 /**
508 * A fallback routine in case the user suddenly hits the "Escape" key
509 * rather than closing the editor using the close button when in fullscreen mode.
510 */
511 $(document).on("keyup", function (e) {
512 var code = e.keyCode || e.which;
513 if (27 === code || e.key === 'Escape') {
514 if (!$('#updraftcentral_dashboard').is(':visible')) {
515 $('#updraftcentral_dashboard').show();
516 }
517 }
518 });
519
520 $('#updraftcentral_dashboard').on('updraftcentral_dialog_opened', function() {
521 if ($.fullscreen.isFullScreen() && $('#classic_editor_container').is(':visible')) {
522 $('.bootbox.modal').appendTo('#classic_editor_container');
523 $('.modal-backdrop-container').appendTo('#classic_editor_container');
524 }
525 });
526
527 $('#updraftcentral_dashboard').on('editor-has-recovered', function() {
528 self.apply_subscriptions_observer(self.reset_info.data, self.reset_info.previous_status, self.reset_info.location);
529 self.clear_notices();
530 });
531
532 $(document.body).on('fullscreenchange', function() {
533 if (!$.fullscreen.isFullScreen()) {
534 var classic = $(document.body).find('div#classic_editor_container');
535 var gutenberg = $(document.body).find('div#gutenberg_editor_container');
536 $container = classic.is(':visible') ? classic : gutenberg;
537
538 if ('undefined' !== typeof $container && $container.length && $container.is(':visible')) {
539 var editor_container = $('#updraftcentral_dashboard').parent().find('#'+$container.attr('id'));
540 if ('undefined' !== typeof editor_container && editor_container.length) {
541 $('#updraftcentral_dashboard').parent().removeAttr('style');
542 editor_container.removeAttr('style');
543 editor_container.appendTo(document.body);
544 if (classic.is(':visible')) {
545 if ('undefined' !== typeof tinymce && tinymce) {
546 self.init_tiny_mce('uc_classic_editor');
547 }
548 }
549
550 // Reset implemented styles. This is done due to an unexpected exit from fullscreen mode
551 // (e.g. viewing of link in a new tab where it abruptly closes the fullscreen mode).
552 self.unload_remote_editor_styles();
553 self.load_remote_editor_styles($container);
554 }
555 }
556 }
557 });
558
559 $(document.body).on('mouseover', 'div.'+module.type+'-item-title', function() {
560 $(this).find('div.'+module.type+'-actions').show();
561 }).on('mouseout', function() {
562 $(this).find('div.'+module.type+'-actions').hide();
563 });
564
565 $('.updraftcentral-show-in-tab-'+module.type+'s button.updraftcentral_action_choose_another_site').on('click', function() {
566 self.editor_reload_required = true;
567 });
568
569 $(document.body).on('updraftcentral_'+module.type+'_editor_loaded', function(evt, $container) {
570 $(document.body).addClass('uc-editor-loaded');
571 self.load_remote_editor_styles($container);
572
573 if ($.fullscreen.isFullScreen()) {
574 if ('undefined' !== typeof $container && $container.length) $container.appendTo($('#updraftcentral_dashboard').parent());
575 }
576
577 self.listen_for_fullscreen_resources();
578 $('#updraftcentral_dashboard').hide();
579 });
580
581 $(document.body).on('updraftcentral_'+module.type+'_editor_closed', function(evt, $container) {
582 $(document.body).removeClass('uc-editor-loaded');
583 self.unload_remote_editor_styles();
584
585 if (!$.fullscreen.isFullScreen()) {
586 if ('undefined' !== typeof $container && $container.length) {
587 var editor_container = $('#updraftcentral_dashboard').parent().find('#'+$container.attr('id'));
588 if ('undefined' !== typeof editor_container && editor_container.length) {
589 editor_container.appendTo(document.body);
590 }
591 }
592 }
593
594 UpdraftCentral.unsubscribe_to_node_changes('uc_body_resources');
595 $('#updraftcentral_dashboard').show();
596
597 UpdraftCentral.clear_editor_container(null);
598 });
599 }
600
601 /**
602 * Register all action/event handlers needed when managing post/page items
603 *
604 * @return {void}
605 */
606 this.register_module_handlers = function() {
607
608 /**
609 * Register a click event handler for searching post/page items
610 *
611 * @see {UpdraftCentral.register_row_clicker}
612 */
613 UpdraftCentral.register_row_clicker('input.uc-'+module.type+'-search-'+module.type+'s', function($site_row, $site_id, e) {
614 if (13 === e.which) {
615 var keyword = $(this).val();
616 var selected_date = $('select.uc-'+module.type+'-date-filter').val();
617 var category = ('post' == module.type) ? $('select.uc-'+module.type+'-category-filter').val() : null;
618 var month_year = null;
619
620 if (selected_date) {
621 var dt = new Date(selected_date.replace(' ', ' 1, '));
622 month_year = (dt.getMonth()+1)+':'+dt.getFullYear();
623 }
624
625 render_post_items($site_row, null, self.current_group, keyword, month_year, category);
626 }
627 }, true, 'keyup');
628
629 /**
630 * Executes the bulk action request
631 *
632 * @see {UpdraftCentral.register_row_clicker}
633 */
634 UpdraftCentral.register_row_clicker('button#uc-apply-action', function($site_row) {
635 var action = $('.uc-'+module.type+'-buttons-filters select.uc-'+module.type+'-action').val();
636 var list = [];
637
638 $site_row.find('.uc-'+module.type+'-items-container .uc-'+module.type+'-item').each(function() {
639 var item = $(this);
640 var id = item.data('id');
641 if (item.find('input[name="post\[\]"]').is(':checked')) list.push(id);
642 });
643
644 if (list.length) {
645 var param = {
646 name: 'set_state',
647 arguments: {
648 list: list,
649 action: action,
650 paged: 1,
651 status: 'all',
652 }
653 };
654 param.arguments['number'+module.type+'s'] = numberposts;
655
656 send_command(param, $site_row).then(function(response) {
657 if ('undefined' !== typeof response[module.type+'s'] && response[module.type+'s']) {
658 if (response.hasOwnProperty('get') && response['get']) {
659 self.manage_data.update('response', response['get']);
660 process_response(response['get'], $site_row, true);
661 }
662
663 var action_label = $('.uc-'+module.type+'-buttons-filters select.uc-'+module.type+'-action > option[value="'+action+'"]').text();
664 UpdraftCentral_Library.dialog.alert('<h2>'+udclion[module.type+'s'].post_update_heading+'</h2><p>'+udclion[module.type+'s'].action_messages[action]+'</p>');
665 } else {
666 UpdraftCentral_Library.dialog.alert('<h2>'+udclion[module.type+'s'].post_update_heading+'</h2><p>'+udclion[module.type+'s'].unkown_error+'</p>');
667 }
668 });
669 }
670 });
671
672 /**
673 * Filters displayed items by category
674 *
675 * @see {UpdraftCentral.register_row_clicker}
676 */
677 UpdraftCentral.register_row_clicker('select.uc-post-category-filter', function($site_row) {
678 var value = $(this).val();
679 var selected_date = $('select.uc-post-date-filter').val();
680 var month_year = null;
681
682 if (selected_date) {
683 var dt = new Date(selected_date.replace(' ', ' 1, '));
684 month_year = (dt.getMonth()+1)+':'+dt.getFullYear();
685 }
686
687 var keyword = $('input.uc-post-search-posts').val();
688 render_post_items($site_row, null, self.current_group, keyword, month_year, value);
689 }, true, 'change');
690
691 /**
692 * Filters displayed items by their publication date
693 *
694 * @see {UpdraftCentral.register_row_clicker}
695 */
696 UpdraftCentral.register_row_clicker('select.uc-'+module.type+'-date-filter', function($site_row) {
697 var value = $(this).val();
698 var category = ('post' == module.type) ? $('select.uc-'+module.type+'-category-filter').val() : null;
699 var month_year = null;
700
701 if (value) {
702 var dt = new Date(value.replace(' ', ' 1, '));
703 month_year = (dt.getMonth()+1)+':'+dt.getFullYear();
704 }
705
706 var keyword = $('input.uc-'+module.type+'-search-'+module.type+'s').val();
707 render_post_items($site_row, null, self.current_group, keyword, month_year, category);
708 }, true, 'change');
709
710 /**
711 * Handles the changing and moving of post/page item's state/status (e.g. from publish to pending, draft to scheduled, etc.)
712 *
713 * @see {UpdraftCentral.register_row_clicker}
714 */
715 UpdraftCentral.register_row_clicker('ul#uc-navlinks a.uc-navlink-item', function($site_row) {
716 var group = $(this).data('group');
717 self.current_group = group;
718
719 update_action_options(group);
720
721 $('ul#uc-navlinks a.uc-navlink-item').css('cssText', 'font-weight: normal;');
722 $('ul#uc-navlinks a.uc-navlink-item[data-group="'+group+'"]').css('cssText', 'font-weight: bold !important;');
723
724 // Clear search, date and category filters when a new status/group is selected
725 // in order to refresh the data starting from post/page 1 as much as possible.
726 $('input.uc-'+module.type+'-search-'+module.type+'s').val('');
727 $('select.uc-'+module.type+'-date-filter').val('');
728 if ('post' == module.type) $('select.uc-'+module.type+'-category-filter').val('');
729
730 render_post_items($site_row, null, group);
731 });
732
733 /**
734 * Displays quick edit form for the selected post/page
735 *
736 * @see {UpdraftCentral.register_row_clicker}
737 */
738 UpdraftCentral.register_row_clicker('.uc-'+module.type+'-items-container a.'+module.type+'-action-item[data-action="quick-edit"]', function($site_row) {
739 // Closing any open quick edit form before we load a new one.
740 var quick_form = $('div.uc_quick_edit:visible');
741 if (quick_form.length) {
742 quick_form.find('button.cancel').trigger('click');
743 }
744
745 var id = $(this).data('id');
746 var quick_edit_form = $('div#'+module.type+'-quick-edit-'+id);
747 var author = quick_edit_form.data('author'),
748 parent = quick_edit_form.data('parent'),
749 template = quick_edit_form.data('template');
750
751 $('div#'+module.type+'-item-'+id).hide();
752 quick_edit_form.show();
753 if ('post' == module.type) quick_edit_form.find('div.category-checklist-wrapper').scrollTop(0);
754
755 // Clear edit storages and add listerners for the current quickedit form
756 self.quick_edits.clear();
757 self.dirty_edits.clear();
758 load_quick_edit_listeners(quick_edit_form);
759
760 quick_edit_form.find('.uc-'+module.type+'-author select[name="post_author"] > option[value="'+author+'"]').prop('selected', true);
761 quick_edit_form.find('.uc-'+module.type+'-parent select[name="post_parent"] > option[value="'+id+'"]').hide();
762 quick_edit_form.find('.uc-'+module.type+'-parent select[name="post_parent"] > option[value="'+parent+'"]').prop('selected', true);
763 quick_edit_form.find('.uc-'+module.type+'-template select[name="post_template"] > option[value="'+template+'"]').prop('selected', true);
764 });
765
766 /**
767 * Cancels quick edit form
768 *
769 * @see {UpdraftCentral.register_row_clicker}
770 */
771 UpdraftCentral.register_row_clicker('.uc_quick_edit button.cancel', function($site_row) {
772 var edit_container = $(this).closest('.uc_quick_edit');
773 var id = edit_container.data('id');
774
775 $('div#'+module.type+'-quick-edit-'+id).hide();
776 $('div#'+module.type+'-item-'+id).show();
777 });
778
779 /**
780 * Saves quick edit information for the selected post/page
781 *
782 * @see {UpdraftCentral.register_row_clicker}
783 */
784 UpdraftCentral.register_row_clicker('.uc_quick_edit button.save', function($site_row) {
785 var parent = $(this).closest('div.uc_quick_edit');
786 var id = parent.data('id');
787 var post_item = $('.uc-'+module.type+'-items-container div#'+module.type+'-item-'+id);
788
789 if (self.dirty_edits.count()) {
790 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+udclion[module.type+'s'].dirty_edits+'</p>');
791 return false;
792 }
793
794 if (self.quick_edits.count()) {
795 self.quick_edits.update('id', id);
796
797 if (self.has_quickedit_date_changed()) {
798 var $form = $('div#'+module.type+'-quick-edit-'+id);
799 var input_date = {
800 month: $form.find('.uc-'+module.type+'-date select[name="mm"]').val(),
801 day: $form.find('.uc-'+module.type+'-date input[name="jj"]').val(),
802 year: $form.find('.uc-'+module.type+'-date input[name="aa"]').val(),
803 hour: $form.find('.uc-'+module.type+'-date input[name="hh"]').val(),
804 minute: $form.find('.uc-'+module.type+'-date input[name="mn"]').val(),
805 second: $form.find('.uc-'+module.type+'-date input[name="ss"]').val()
806 };
807
808 if (self.validate_input_date(input_date)) {
809 var result = self.prepare_date(input_date);
810 self.quick_edits.update('date', result.date);
811 self.quick_edits.update('timestamp', result.timestamp);
812
813 // Update the status to "future" if date edits exists and it is scheduled or intended
814 // to be publish in the future. Only when the "status" field wasn't changed manually. The
815 // "status" change will override any date edits "future" (scheduled) computation.
816 if (self.quick_edits.exists('status') && 'publish' == self.quick_edits.item('status') && self.is_input_future_date(input_date)) {
817 self.quick_edits.update('status', 'future');
818 }
819 } else {
820 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+udclion[module.type+'s'].invalid_date_input+'</p>');
821 return false;
822 }
823 }
824
825 var param = {
826 name: 'save',
827 arguments: self.quick_edits.get_collection_object()
828 };
829
830 send_command(param, $site_row).then(function(response) {
831 if ('undefined' !== typeof response.post && response.post) {
832 // reload editor and update items list after publish
833 var post_data = {
834 post: JSON.parse(response.post),
835 misc: response.misc
836 }
837
838 if ('undefined' !== typeof response.preloaded && response.preloaded) {
839 var preloaded = JSON.parse(response.preloaded);
840 var preloaded_data = self.manage_data.item('preloaded_data');
841
842 if ('undefined' !== typeof preloaded_data && preloaded_data) {
843 preloaded_data = JSON.parse(preloaded_data);
844 preloaded_data.categories = preloaded.categories;
845 preloaded_data.tags = preloaded.tags;
846 self.manage_data.update('preloaded_data', JSON.stringify(preloaded_data));
847 }
848 }
849
850 update_items_list(post_data, post_item.data('status'));
851 if (response.hasOwnProperty('options')) update_filter_options(response.options);
852 } else {
853 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+udclion[module.type+'s'].unkown_error+'</p>');
854 parent.find('button.cancel').trigger('click');
855 }
856 });
857 } else {
858 UpdraftCentral_Library.dialog.alert('<h2>'+udclion[module.type+'s'].quick_edit_heading+'</h2><p>'+udclion[module.type+'s'].no_changes+'</p>');
859 parent.find('button.cancel').trigger('click');
860 }
861
862 });
863
864 /**
865 * Processes post/page action links (the links found under each items of the posts/pages table)
866 *
867 * @see {UpdraftCentral.register_row_clicker}
868 */
869 UpdraftCentral.register_row_clicker('.'+module.type+'-actions > a.'+module.type+'-action-item', function($site_row) {
870 var action = $(this).data('action');
871 var id = $(this).data('id');
872 var post_data = $(this).closest('div#'+module.type+'-item-'+id).data('json');
873
874 if (-1 !== $.inArray(action, ['edit-classic', 'edit-gutenberg'])) {
875 var editor = action.replace('edit-', '');
876 if ('gutenberg' === editor) {
877 var hide_dialog = false;
878 if ('undefined' !== typeof localStorage && localStorage) {
879 var no_show = localStorage.getItem('uc-gutenberg-dialog-no-show');
880 if ('undefined' !== typeof no_show && no_show && 1 === parseInt(no_show)) {
881 hide_dialog = true;
882 }
883 }
884
885 if (!hide_dialog) {
886 UpdraftCentral_Library.dialog.custom('<h2>'+udclion[module.type+'s'].gutenberg_support+'</h2><p>'+udclion[module.type+'s'].gutenberg_notice+'</p><p><input type="checkbox" name="uc-dont-show-again" id="uc-dont-show-again" value="1"><label class="uc-dont-show-again-label" for="uc-dont-show-again">'+udclion[module.type+'s'].dont_show+'</label></p>', null, {
887 classic: {
888 label: 'Use classic',
889 className: 'btn-secondary',
890 callback: function() {
891 self.load_updraftcentral_editor('classic', post_data, $site_row);
892 }
893 },
894 gutenberg: {
895 label: 'Continue',
896 className: 'btn-primary',
897 callback: function() {
898 self.load_updraftcentral_editor(editor, post_data, $site_row);
899 }
900 }
901 });
902 } else {
903 self.load_updraftcentral_editor(editor, post_data, $site_row);
904 }
905 } else {
906 self.load_updraftcentral_editor(editor, post_data, $site_row);
907 }
908 } else {
909 if (-1 !== $.inArray(action, ['trash', 'restore', 'delete'])) {
910 set_state(id, action, $site_row);
911 } else {
912 if ('take-over' === action) {
913 send_command({ name: 'take_over', arguments: { post_id: id }}, $site_row).then(function(response) {
914 if ('undefined' !== typeof response.lock_acquired && response.lock_acquired) {
915 $site_row.find('button.updraftcentral_manage_'+module.type+'s').trigger('click');
916 }
917 });
918 }
919 }
920 }
921 });
922
923 /**
924 * Creates a new post/post
925 *
926 * @see {UpdraftCentral.register_row_clicker}
927 */
928 UpdraftCentral.register_row_clicker('.updraftcentral_create_'+module.type, function($site_row) {
929 var $location = $site_row.find('.updraftcentral_row_extracontents');
930 $location.empty();
931 self.current_section = 'create';
932
933 var create_form = UpdraftCentral.template_replace(module.type+'s-create', {});
934 UpdraftCentral_Library.dialog.confirm('<h2>'+udclion[module.type+'s'].new_post+'</h2><p>'+create_form+'</p>', function(result) {
935 if (!result) return;
936
937 var title = $('input.uc-title-input').val();
938 if (0 == title.trim().length) {
939 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+udclion[module.type+'s'].title_missing+'</p>');
940 return;
941 }
942
943 var editor = $('input.uc-editor-choice:checked').val();
944 var param = {
945 name: 'save',
946 arguments: { title: title, content: '', new: 1 },
947 timeout: ('undefined' !== typeof udclion.user_defined_timeout && udclion.user_defined_timeout) ? udclion.user_defined_timeout : 30,
948 };
949
950 send_command(param, $site_row).then(function(response) {
951 if ('undefined' !== typeof response.post && response.post) {
952 // reload editor and update items list after publish
953 var post_data = {
954 post: JSON.parse(response.post),
955 misc: response.misc
956 }
957
958 // N.B. The "preloaded" property doesn't always return for every response that is
959 // why we're checking it here. If we have it, then we store it. It will only be
960 // requested when the "preload" parameter is set (when pressing the "Manage" button)
961 // or a new post is created.
962 if (response.hasOwnProperty('preloaded') && response.preloaded) {
963 self.manage_data.update('preloaded_data', response.preloaded);
964 }
965
966 if (response.hasOwnProperty('options') && response.options) {
967 if ('page' == module.type) {
968 if (!self.manage_data.exists('parent_options') && response.options.hasOwnProperty('page')) self.manage_data.add('parent_options', response.options.page);
969 }
970 if (!self.manage_data.exists('template_options') && response.options.hasOwnProperty('template')) self.manage_data.add('template_options', response.options.template);
971 }
972
973 self.load_updraftcentral_editor(editor, post_data, $site_row);
974 } else {
975 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+udclion[module.type+'s'].unkown_error+'</p>');
976 console.log(response);
977 }
978 });
979 });
980 }, true);
981
982 /**
983 * Manages existing posts/pages
984 *
985 * @see {UpdraftCentral.register_row_clicker}
986 */
987 UpdraftCentral.register_row_clicker('.updraftcentral_manage_'+module.type+'s', function($site_row) {
988 // Clear or reset manage data collection. We will use this to store our
989 // current response, so that we can easily retrieve them if we need to.
990 self.manage_data.clear();
991 self.current_section = 'manage';
992 self.current_group = 'all';
993
994 self.pagination = new UpdraftCentral_Pagination({
995 container: '.uc-'+module.type+'-pagination',
996 items_per_page: 50,
997 type: 'remote',
998 callback: function(page) {
999 var keyword = $('input.uc-'+module.type+'-search-posts').val();
1000 var selected_date = $('select.uc-'+module.type+'-date-filter').val();
1001 var month_year = null;
1002
1003 if (selected_date) {
1004 var dt = new Date(selected_date.replace(' ', ' 1, '));
1005 month_year = (dt.getMonth()+1)+':'+dt.getFullYear();
1006 }
1007
1008 var category = ('post' == module.type) ? $('select.uc-post-category-filter').val() : null;
1009 render_post_items($site_row, page, self.current_group, keyword, month_year, category);
1010 },
1011 });
1012
1013 render_post_items($site_row, null, null, null, null, null, true, true);
1014 }, true);
1015 }
1016
1017 /**
1018 * When in fullscreen mode check to see if media dialog and backdrop are added into the main body
1019 * element. If so, then we need to transfer it into the container/element that is currently in fullscreen mode.
1020 *
1021 * @return {void}
1022 */
1023 this.listen_for_fullscreen_resources = function() {
1024 UpdraftCentral.subscribe_to_node_changes(document.body, function(changes, observer) {
1025 if (!$.fullscreen.isFullScreen()) {
1026 $('#updraftcentral_dashboard').parent().find('div[id^="__wp-uploader-id"]').not('.media-frame').appendTo(document.body).hide();
1027 $('#updraftcentral_dashboard').parent().find('div.modal-backdrop-container').appendTo(document.body);
1028 } else {
1029 var media_modal = $('div[id^="__wp-uploader-id"]').not('.media-frame');
1030 if ('undefined' !== typeof media_modal && media_modal.length) {
1031 media_modal.appendTo($('#updraftcentral_dashboard').parent());
1032 }
1033
1034 var modal_backdrop = $(document.body).find('div.modal-backdrop-container');
1035 if ('undefined' !== typeof modal_backdrop && modal_backdrop.length) {
1036 modal_backdrop.appendTo($('#updraftcentral_dashboard').parent());
1037 }
1038 }
1039 }, 'uc_body_resources');
1040 }
1041
1042 /**
1043 * Checks whether any of the quick edit date fields was changed
1044 *
1045 * @return {boolean}
1046 */
1047 this.has_quickedit_date_changed = function() {
1048 var changed = false;
1049
1050 if (self.quick_edits.count()) {
1051 var date_fields = ['mm', 'jj', 'aa', 'hh', 'mn', 'ss'];
1052 for (var i=0; i<date_fields.length; i++) {
1053 if (self.quick_edits.exists(date_fields[i])) {
1054 changed = true;
1055 break;
1056 }
1057 }
1058 }
1059
1060 return changed;
1061 }
1062
1063 /**
1064 * Clear existing notices if present to give way for a new editing process.
1065 *
1066 * @return {void}
1067 */
1068 this.clear_notices = function() {
1069 var notices = $('#gutenberg_editor_container .components-notice-list');
1070 if (notices.length) {
1071 notices.find('.components-notice.is-dismissible').each(function() {
1072 $(this).find('button.components-notice__dismiss').trigger('click');
1073 });
1074 }
1075
1076 notices = wp.data.select('core/notices').getNotices();
1077 if ('undefined' !== typeof notices && notices && notices.length) {
1078 for (var i=0; i<notices.length; i++) {
1079 wp.data.dispatch('core/notices').removeNotice(notices[i].id);
1080 }
1081 }
1082 }
1083
1084 /**
1085 * Observes editor's attempt to recover from a broken feature so that we can re-initialize
1086 * our own resources (e.g. wp.data subscriptions, etc.)
1087 *
1088 * @return {void}
1089 */
1090 this.observe_editor_error_state = function() {
1091 var editor = $('.uc-block-editor-container #editor');
1092 UpdraftCentral.subscribe_to_node_changes(editor, function(changes, observer) {
1093 var added = [], removed = [];
1094 for (var i=0; i<changes.length; i++) {
1095 var record = changes[i];
1096 if (record.addedNodes.length) added.push(record.addedNodes[0].className);
1097 if (record.removedNodes.length) removed.push(record.removedNodes[0].className);
1098 }
1099
1100 if (-1 !== added.indexOf('components-drop-zone__provider') && -1 !== removed.indexOf('components-drop-zone__provider')) {
1101 $('#updraftcentral_dashboard').trigger('editor-has-recovered');
1102 }
1103 }, 'uc_editor');
1104 }
1105
1106 /**
1107 * Sends upload permission to the server
1108 *
1109 * @return {void}
1110 */
1111 this.send_upload_permissions = function(data) {
1112 var featured_image_container,
1113 intchecker;
1114
1115 intchecker = setInterval(function() {
1116 if (!wp.data.select('core/edit-post').isEditorPanelOpened('featured-image')) {
1117 wp.data.dispatch('core/edit-post').toggleEditorPanelOpened('featured-image');
1118 }
1119
1120 featured_image_container = $('div.edit-post-sidebar .editor-post-featured-image');
1121 if (featured_image_container.length) {
1122 clearInterval(intchecker);
1123 wp.data.dispatch('core').receiveUploadPermissions(data.has_upload_permissions);
1124 wp.data.dispatch('core/edit-post').toggleEditorPanelOpened('featured-image');
1125 }
1126 }, 1000);
1127 }
1128
1129 /**
1130 * Disables/enables the main publish button
1131 *
1132 * @return {void}
1133 */
1134 this.disable_publish_button = function(disable) {
1135 $('button.editor-post-publish-button').attr('aria-disabled', disable);
1136 $('button.editor-post-publish-button').prop('disabled', disable);
1137 $('button.editor-post-publish-panel__toggle').attr('aria-disabled', disable);
1138 $('button.editor-post-publish-panel__toggle').prop('disabled', disable);
1139 }
1140
1141 /**
1142 * Updates post title permalink preview button's click handler
1143 *
1144 * @return {void}
1145 */
1146 this.update_permalink_preview_handler = function() {
1147 $(document.body).on('click', 'textarea.editor-post-title__input', function() {
1148 var post = wp.data.select('core/editor').getCurrentPost();
1149 var title_block = $('.editor-post-title__block');
1150
1151 if ('undefined' !== typeof title_block && title_block.length && title_block.hasClass('is-selected')) {
1152 var permalink_interval = setInterval(function() {
1153 var permalink = $('.editor-post-permalink__link');
1154 if ('undefined' !== typeof permalink && permalink.length) {
1155 clearInterval(permalink_interval);
1156 $('.editor-post-permalink__link').attr('href', '#').removeAttr('target').attr('onclick', 'UpdraftCentral_Library.open_browser_at(UpdraftCentral.$site_row, { module: "direct_url", url: "'+post.link+'" }, jQuery(\'#updraftcentral_dashboard_wrapper\'));');
1157 }
1158 }, 500);
1159 }
1160 });
1161 }
1162
1163 /**
1164 * Reconstruct inputted date elements and prepare to either be use
1165 * for display or submission
1166 *
1167 * @param {object} d An object containing date and time properties based from user input
1168 *
1169 * @return {object}
1170 */
1171 this.prepare_date = function(d) {
1172 /**
1173 * Prepends a numerical (integer) representation of a date if less than 10
1174 * in order to align with the UI presentation. Internal to the "prepare_date" only.
1175 *
1176 * @param {mixed} n A numerical date representation
1177 *
1178 * @return {mixed}
1179 */
1180 function pad(n) {
1181 return n<10 ? '0'+n : n;
1182 }
1183
1184 var date = new Date(d.year, parseInt(d.month) - 1, d.day, d.hour, d.minute, d.second);
1185 var timestamp = {
1186 month: pad(date.getMonth()+1),
1187 day: pad(date.getDate()),
1188 year: date.getFullYear(),
1189 hour: pad(date.getHours()),
1190 minute: pad(date.getMinutes()),
1191 second: pad(date.getSeconds())
1192 };
1193
1194 var date_string = timestamp.year+'-'+timestamp.month+'-'+timestamp.day+'T'+timestamp.hour+':'+timestamp.minute+':'+timestamp.second;
1195 var today = new Date();
1196
1197 return {
1198 date: date_string,
1199 future: date.getTime() > today.getTime(),
1200 formatted: date.toLocaleString('default', { year: 'numeric', month: 'short', day: 'numeric'}) + ' @ ' + pad(date.getHours())+':'+ pad(date.getMinutes()),
1201 timestamp: timestamp
1202 }
1203 }
1204
1205 /**
1206 * Converting image's local reference to wp-content path to absolute urls
1207 *
1208 * @param {string} content The post content/body
1209 *
1210 * @return {string}
1211 */
1212 this.adjust_local_reference = function(content) {
1213 var temp = document.createElement('div');
1214 temp.innerHTML = content;
1215
1216 $(temp).find('img[src^="wp-content/"]').each(function() {
1217 if (udclion.hasOwnProperty('home_url') && udclion.home_url) {
1218 var src = $(this).attr('src');
1219 content = content.replace(src, udclion.home_url+'/'+src);
1220 }
1221
1222 });
1223
1224 return content;
1225 }
1226
1227 /**
1228 * Case-insensitive check for value existense in an array
1229 *
1230 * @param {string} value The value to check
1231 * @param {array} source The array to which the value is going to be check
1232 *
1233 * @return {boolean}
1234 */
1235 this.is_ivalue_exists = function(value, source) {
1236 var result = $.grep(source, function(item, index) {
1237 return value.toLowerCase() == item.toLowerCase();
1238 });
1239
1240 return result.length ? true : false;
1241 }
1242
1243 /**
1244 * Clears URL for any unwated padding when editing a post, in which case if not removed
1245 * will invalidate the current post when hitting the reload button and causes some error
1246 *
1247 * @return {void}
1248 */
1249 this.cleanupHistory = function() {
1250 var pathname = window.location.pathname;
1251 if (-1 != pathname.indexOf('post.php')) {
1252 pathname = pathname.substring(0, pathname.indexOf('post.php'));
1253 }
1254 history.pushState('', document.title, pathname);
1255 }
1256
1257 /**
1258 * Loads the UpdraftCentral editor
1259 *
1260 * @param {string} editor The type of editor to load (e.g. 'classic' or 'gutenberg')
1261 * @param {object} post_data An object containing the post object and its miscellaneous information
1262 * @param {object} $site_row A jQuery object representing the current site that is currently worked on
1263 *
1264 * @return {void}
1265 */
1266 this.load_updraftcentral_editor = function(editor, post_data, $site_row) {
1267 var editor_data = {
1268 editor: editor,
1269 post_data: JSON.stringify(post_data),
1270 site_id: $site_row.data('site_id'),
1271 preloaded_data: self.manage_data.item('preloaded_data'),
1272 template_options: self.manage_data.item('template_options')
1273 };
1274
1275 // Reset edits and state when editing new post
1276 self.uc_editor.edits.clear();
1277 self.uc_editor.state.clear();
1278
1279 self.send_local_command('load_post_editor', editor_data, $site_row).then(function(response) {
1280 self.process_load_editor_response(editor, response, $site_row);
1281 }).always(function() {
1282 var backdrop = $(document.body).find('div.modal-backdrop-container');
1283 if ('undefined' !== typeof backdrop && backdrop.length) backdrop.remove();
1284 });
1285 }
1286
1287 /**
1288 * Scrolls content to top when the editor loads
1289 *
1290 * @return {void}
1291 */
1292 this.scroll_content_to_top = function() {
1293 $(window).scrollTop(0);
1294 $('div.edit-post-sidebar').scrollTop(0);
1295 $('div.editor-post-taxonomies__hierarchical-terms-list').scrollTop(0);
1296
1297 var skeleton_content,
1298 intchecker;
1299
1300 intchecker = setInterval(function() {
1301 skeleton_container = $('div.block-editor-editor-skeleton__content');
1302 if (skeleton_container.length) {
1303 clearInterval(intchecker);
1304 skeleton_container.scrollTop(0);
1305 }
1306 }, 1000);
1307 }
1308
1309 /**
1310 * Remove any previously embedded remote styles and restores the local styles
1311 *
1312 * @return {void}
1313 */
1314 this.unload_remote_editor_styles = function() {
1315 self.disable_current_themes(false);
1316 self.disable_current_wp_theme(false);
1317 $('style[id^="uc_remote_styles-"]').remove();
1318 $('style[id^="uc_local_styles-"]').remove();
1319 var fonts = $('head > style[id="uc-remote-fonts-inline-css"]');
1320 if (fonts.length) fonts.remove();
1321 }
1322
1323 /**
1324 * Embed/create and load remote styles and disables local styles while editing a post
1325 *
1326 * @param {object} container The container element of the current editor
1327 *
1328 * @return {void}
1329 */
1330 this.load_remote_editor_styles = function(container) {
1331
1332 if (self.manage_data.exists('preloaded_data')) {
1333 var preloaded_data = self.manage_data.item('preloaded_data');
1334 var editor = $(container).data('editor');
1335
1336 if ('undefined' !== typeof preloaded_data && preloaded_data) {
1337 data = JSON.parse(preloaded_data);
1338
1339 if (data.hasOwnProperty('editor_styles') && data.editor_styles) {
1340
1341 switch (editor) {
1342 case 'gutenberg':
1343 var visual_editor_checker = setInterval(function() {
1344 var visual_editor = $(container).find('.edit-post-visual-editor');
1345 if ('undefined' !== typeof visual_editor && visual_editor.length) {
1346 clearInterval(visual_editor_checker);
1347 var editor_styles = data.editor_styles;
1348
1349 if (editor_styles.length) {
1350 self.disable_current_themes(true);
1351
1352 if (!visual_editor.hasClass('editor-styles-wrapper')) visual_editor.addClass('editor-styles-wrapper');
1353
1354 // Add custom override to maintain the editor's look and feel across all WP versions
1355 editor_styles.push({
1356 css: '.editor-styles-wrapper, .editor-styles-wrapper textarea { background: inherit; color: inherit; }\n\n.editor-styles-wrapper p { white-space: pre-wrap !important; word-wrap: break-word; }\n\n.editor-styles-wrapper img { vertical-align: inherit; border-style: inherit; }\n\n.screen-reader-text, .screen-reader-text span, .ui-helper-hidden-accessible { border: 0; clip: rect(1px, 1px, 1px, 1px); -webkit-clip-path: inset(50%); clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; word-wrap: normal !important; }\n\n.editor-styles-wrapper button, .editor-styles-wrapper input[type="button"], .editor-styles-wrapper input[type="reset"], .editor-styles-wrapper input[type="submit"] { text-transform: inherit !important; }'
1357 });
1358
1359 var uc_styles = ('page' == module.type) ? udcstyles_page : udcstyles;
1360
1361 // Re-post pages/posts styles to keep the main look and feel consistent
1362 if ('undefined' !== typeof uc_styles && uc_styles.hasOwnProperty('edit_'+module.type+'_css')) {
1363 $('<style/>', {
1364 id: 'uc_local_styles-inline-edit_post'
1365 }).html(uc_styles['edit_'+module.type+'_css']).appendTo(document.body);
1366 }
1367
1368 for (var i=0; i<editor_styles.length; i++) {
1369 if ('undefined' !== typeof editor_styles[i].css && editor_styles[i].css.trim().length) {
1370 $('<style/>', {
1371 id: 'uc_remote_styles-'+i
1372 }).html(editor_styles[i].css).appendTo(document.body);
1373
1374 if (editor_styles[i].hasOwnProperty('inline') && editor_styles[i].inline.length) {
1375 $('<style/>', {
1376 id: 'uc_remote_styles-inline-'+i
1377 }).html(editor_styles[i].inline).appendTo(document.body);
1378 }
1379 }
1380 }
1381
1382 // Re-post pages/posts styles to keep the main look and feel consistent
1383 if ('undefined' !== typeof uc_styles && uc_styles.hasOwnProperty(module.type+'s_inline_css')) {
1384 $('<style/>', {
1385 id: 'uc_local_styles-inline-posts'
1386 }).html(uc_styles[module.type+'s_inline_css']).appendTo(document.body);
1387 }
1388 }
1389 }
1390 }, 500);
1391 break;
1392 case 'classic':
1393 var active_editor_checker = setInterval(function() {
1394 if ('undefined' !== typeof tinymce.activeEditor && tinymce.activeEditor && tinymce.activeEditor.hasOwnProperty('dom') && tinymce.activeEditor.dom) {
1395 clearInterval(active_editor_checker);
1396 var editor_styles = data.editor_styles;
1397
1398 if (editor_styles.length) {
1399 self.disable_current_wp_theme(true);
1400
1401 if (tinymce.activeEditor.dom.hasOwnProperty('select')) {
1402 if (tinymce.activeEditor.dom.hasOwnProperty('remove')) {
1403 var link = tinymce.activeEditor.dom.select('head > link');
1404 if ('undefined' !== typeof link && link) {
1405 tinymce.activeEditor.dom.remove(link);
1406 }
1407 }
1408
1409 if (tinymce.activeEditor.dom.hasOwnProperty('hasClass') && tinymce.activeEditor.dom.hasOwnProperty('addClass')) {
1410 if (!tinyMCE.activeEditor.dom.hasClass(tinyMCE.activeEditor.dom.select('body'), 'editor-styles-wrapper')) {
1411 tinyMCE.activeEditor.dom.addClass(tinyMCE.activeEditor.dom.select('body'), 'editor-styles-wrapper');
1412 }
1413
1414 if (!tinyMCE.activeEditor.dom.hasClass(tinyMCE.activeEditor.dom.select('body'), 'entry-content')) {
1415 tinyMCE.activeEditor.dom.addClass(tinyMCE.activeEditor.dom.select('body'), 'entry-content');
1416 }
1417 }
1418 }
1419
1420 editor_styles.push({
1421 css: 'body, editor-styles-wrapper { background: inherit !important; max-width: inherit; }\n\nbody#tinymce { padding: 0px 20px; }\n\nbody::before { position: relative; }'
1422 });
1423
1424 for (var i=0; i<editor_styles.length; i++) {
1425 if ('undefined' !== typeof editor_styles[i].css && editor_styles[i].css.trim().length) {
1426 if (tinymce.activeEditor.dom.hasOwnProperty('addStyle')) {
1427 tinymce.activeEditor.dom.addStyle(editor_styles[i].css);
1428 }
1429 }
1430 }
1431 }
1432 }
1433 }, 500);
1434 break;
1435 default:
1436 break;
1437 }
1438
1439 }
1440 }
1441 }
1442 }
1443
1444 /**
1445 * Captures the selected image from the media modal and extract its details for later consumption.
1446 * Primarily used for editing using the classic editor.
1447 *
1448 * @param {object} selected A jQuery object representing the currently selected image in the media dialog
1449 *
1450 * @return void
1451 */
1452 this.set_selected_image = function(selected) {
1453 var orig_filename = selected.attr('aria-label');
1454 var media_id = selected.data('id');
1455 var img = selected.find('.thumbnail img');
1456
1457 if ('undefined' !== typeof img && img.length) {
1458 var src = img.attr('src');
1459 var path = '',
1460 ext = '';
1461
1462 if ('undefined' !== typeof src && src.length) {
1463 path = src.substring(0, src.lastIndexOf('/') + 1);
1464 if (-1 !== src.indexOf('.')) {
1465 ext = src.substr(src.lastIndexOf('.'));
1466 }
1467 }
1468
1469 var image_url = path + orig_filename;
1470 if (-1 == image_url.indexOf(ext)) {
1471 image_url += ext;
1472 }
1473
1474 self.uc_editor.state.update('featured_img', {
1475 thumbnail_url: src,
1476 folder: path,
1477 image_url: image_url
1478 });
1479
1480 self.uc_editor.edits.update('featured_media_url', image_url);
1481 self.uc_editor.edits.update('featured_media', media_id);
1482 }
1483 }
1484
1485 /**
1486 * A simple non-empty and numeric validation for date parts entry
1487 *
1488 * @param {object} input_date The date parts to check (e.g. day, year, etc.)
1489 *
1490 * @return {boolean}
1491 */
1492 this.validate_input_date = function(input_date) {
1493 var passed = true;
1494 for (var prop in input_date) {
1495 var item = input_date[prop];
1496 if ('undefined' == typeof item || 0 == item.length || !UpdraftCentral_Library.is_numeric(item)) {
1497 passed = false;
1498 break;
1499 }
1500 }
1501
1502 // Validate year, month, hour and minute entries individually
1503 if (passed) {
1504 // Validate day (1 to 31)
1505 if (parseInt(input_date.day) < 1 || parseInt(input_date.day) > 31) passed = false;
1506
1507 // Validate year (4 digits) - limit based on EPOCH year 1970
1508 if (passed && (4 !== input_date.year.length || parseInt(input_date.year) < 1970)) passed = false;
1509
1510 // Validate hour (0 to 23)
1511 if (passed && (parseInt(input_date.hour) < 0 || parseInt(input_date.hour) > 23)) passed = false;
1512
1513 // Validate minutes (0 to 59)
1514 if (passed && (parseInt(input_date.minute) < 0 || parseInt(input_date.minute) > 59)) passed = false;
1515 }
1516
1517 return passed;
1518 }
1519
1520 /**
1521 * Checks whether the inputted date is a future date
1522 *
1523 * @param {object} input_date The date parts to check (e.g. day, year, etc.)
1524 *
1525 * @return {boolean|null}
1526 */
1527 this.is_input_future_date = function(input_date) {
1528 var input;
1529 if ('undefined' !== typeof input_date && input_date) {
1530 input = input_date;
1531 } else {
1532 var parent = $('fieldset#timestampdiv');
1533 var wrapper = parent.find('div.timestamp-wrap');
1534 input = {
1535 month: wrapper.find('select#mm').val(),
1536 day: wrapper.find('input#jj').val(),
1537 year: wrapper.find('input#aa').val(),
1538 hour: wrapper.find('input#hh').val(),
1539 minute: wrapper.find('input#mn').val(),
1540 second: parent.find('input#ss').val()
1541 };
1542 }
1543
1544 if (self.validate_input_date(input)) {
1545 var user_date = new Date(input.year, parseInt(input.month)-1, input.day, input.hour, input.minute, input.second);
1546 var date_now = new Date();
1547
1548 return user_date > date_now ? true : false;
1549 }
1550
1551 return null;
1552 }
1553
1554 /**
1555 * Send command to the local server where UpdraftCentral is hosted. Gets to execute
1556 * commands for UpdraftCentral modules.
1557 *
1558 * @param {string} action The intended action to execute
1559 * @param {object} params An object that contains properties that will serve as parameters for the request
1560 * @param {object} $site_row A jQuery object representing the current site that is currently worked on
1561 *
1562 * @return {Promise}
1563 */
1564 this.send_local_command = function(action, params, $site_row) {
1565 var deferred = $.Deferred();
1566
1567 var $location = ('undefined' !== typeof $site_row && $site_row) ? $site_row : $('#updraftcentral_dashboard_existingsites');
1568 UpdraftCentral.send_ajax(action, params, null, 'via_mothership_encrypting', $location, function(resp, code, error_code) {
1569 if ('ok' === code) {
1570 if (resp.hasOwnProperty('message')) {
1571 deferred.resolve(resp.message);
1572 } else {
1573 deferred.reject();
1574 }
1575 }
1576 });
1577
1578 return deferred.promise();
1579 }
1580
1581 /**
1582 * Instantiate tinymce after ajax called for wp_editor configs.
1583 * set plugins and appropriate toolbars from wp_editor
1584 *
1585 * @param {int} mceid, textarea id
1586 *
1587 * @return {void}
1588 */
1589 this.init_tiny_mce = function(mceid) {
1590 if ("object" == typeof(tinymce) && "function" === typeof(tinymce.execCommand)) {
1591 // FIX: Making sure that we're clearing all existing editors
1592 // before initializing a new one.
1593 // N.B. This does not only apply to multiple editors but
1594 // to a single (previously) initialized editor as well.
1595 if ('undefined' !== typeof tinymce.EditorManager) { tinymce.EditorManager.editors = []; }
1596
1597 tinymce.init({
1598 selector: mceid,
1599 height: 800,
1600 branding: false,
1601 menubar: false,
1602 resize: false,
1603 force_br_newlines: true,
1604 force_p_newlines: true,
1605 plugins: "textcolor hr lists fullscreen paste wordpress wpdialogs wplink tabfocus",
1606 toolbar1: "formatselect bold italic numlist bullist blockquote alignleft aligncenter alignright link unlink wp_more fullscreen wp_adv",
1607 toolbar2: "strikethrough hr forecolor backcolor pastetext pasteword removeformat indent outdent undo redo | sizeselect fontselect fontsizeselect",
1608 fontsize_formats: "8pt 10pt 12pt 14pt 18pt 24pt 36pt",
1609 paste_as_text: true,
1610 color_picker_callback: function (callback, value) {
1611 callback('#888');
1612 },
1613 setup: function(ed) {
1614 if ($(document).find('.wp-core-ui.wp-editor-wrap').hasClass('html-active')) {
1615 $(document).find('.wp-core-ui.wp-editor-wrap').removeClass('html-active').addClass('tmce-active');
1616 }
1617 }
1618 });
1619
1620 tinymce.execCommand("mceRemoveEditor", false, mceid);
1621 tinymce.execCommand("mceAddEditor", false, mceid);
1622
1623 if ('undefined' !== typeof quicktags && quicktags) {
1624 quicktags({
1625 id: mceid,
1626 buttons: 'strong,em,link,block,del,ins,img,ul,ol,li,code,close'
1627 });
1628 }
1629 }
1630 }
1631
1632 /**
1633 * Resets all editor panels when the editor is loaded for a new edit task
1634 *
1635 * @return {void}
1636 */
1637 function reset_panels_on_load() {
1638 var panel_ids = ['featured-image', 'discussion-panel', 'post-link', 'post-excerpt', 'page-attributes'];
1639 if ('post' == module.type) {
1640 panel_ids = ['taxonomy-panel-category', 'taxonomy-panel-post_tag', 'featured-image', 'discussion-panel', 'post-link', 'post-excerpt', 'post-attributes'];
1641 }
1642
1643 for (var i=0; i<panel_ids.length; i++) {
1644 if (wp.data.select('core/edit-post').isEditorPanelOpened(panel_ids[i])) wp.data.dispatch('core/edit-post').toggleEditorPanelOpened(panel_ids[i]);
1645 }
1646 }
1647
1648 /**
1649 * Adds some housekeeping logic after the editor has been initialized/re-initialized and
1650 * subscribes to the "wp.data" store to manage our own post's data handling
1651 *
1652 * @param {object} data An object containing information of the current post being edited
1653 * @param {string} previous_status The previous status of the post before the changes were applied
1654 * @param {object} $location A jquery object representing the classic editor container
1655 *
1656 * @return {void}
1657 */
1658 this.apply_subscriptions_observer = function(data, previous_status, $location) {
1659 var post,
1660 toolbar_loaded = is_saving = false,
1661 core = wp.data.select('core'),
1662 featured_image_opened = false;
1663
1664 core.canUser = function(action, entity) {
1665 if ('create' === action && 'media' === entity) {
1666 return data.has_upload_permissions ? true : false;
1667 }
1668 }
1669
1670 self.observe_editor_error_state();
1671 self.send_upload_permissions(data);
1672 reset_panels_on_load();
1673 self.update_permalink_preview_handler();
1674
1675 window.wp_data_unsubscribe = wp.data.subscribe(function(event) {
1676 post = wp.data.select('core/editor').getCurrentPost();
1677
1678 var toolbar_container = $('div.edit-post-header-toolbar');
1679 var editor_container = $(document.body);
1680 var backdrop = editor_container.find('div.modal-backdrop-container');
1681
1682 $('a.components-notice__action.is-link').attr('target', '_blank');
1683 $('.components-snackbar-list__notice-container a.components-snackbar__action').attr('target', '_blank');
1684 $('.post-publish-panel__postpublish-buttons a').attr('target', '_blank');
1685
1686 if (toolbar_container.length && !toolbar_loaded) {
1687 // Add logo and custom buttons:
1688 attach_updraftcentral_buttons(data.logo, data.misc);
1689
1690 toolbar_loaded = true;
1691
1692 // Make sure that any visible spinner(s) are remove when the
1693 // editor is fully loaded.
1694 if ($('.updraftcentral_spinner').is(':visible')) {
1695 $('.updraftcentral_spinner').remove();
1696 }
1697
1698 self.scroll_content_to_top();
1699 update_view_posts_link();
1700
1701 if ('post' == module.type) wp.data.select('core').getTaxonomies();
1702 $(document.body).trigger('updraftcentral_'+module.type+'_editor_loaded', [$location]);
1703 } else {
1704 // Just in case the fullscreen mode is triggered after the editor has loaded then
1705 // we will update the view post link.
1706 update_view_posts_link();
1707 }
1708
1709 if (!wp.data.select('core/editor').isEditedPostDirty()) {
1710 self.disable_publish_button(true);
1711 } else {
1712 self.disable_publish_button(false);
1713 }
1714
1715 if (wp.data.select('core/editor').isSavingPost()) {
1716 if ('undefined' == typeof backdrop || 0 == backdrop.length) {
1717 editor_container.append('<div class="modal-backdrop-container"><div class="modal-backdrop fade show dynamic"></div><div class="updraftcentral_spinner"></div></div>');
1718 }
1719 }
1720
1721 // Make sure that all existing or on-demand notices links doesn't
1722 // close/replace our editor abruptly, it needs to be opened on a new tab or window.
1723 if (wp.data.select('core/editor').didPostSaveRequestSucceed() && !wp.data.select('core/editor').isSavingPost() && post.hasOwnProperty('post_data')) {
1724 $('#uc_preview_lnk').attr('href', '#').removeAttr('target').attr('onclick', 'UpdraftCentral_Library.open_browser_at(UpdraftCentral.$site_row, { module: "direct_url", url: "'+post.link+'" }, jQuery(\'#updraftcentral_dashboard_wrapper\'));');
1725
1726 // Update locally stored data
1727 if ('manage' === self.current_section) {
1728 update_items_list(post.post_data, previous_status);
1729 if (post.post_data.hasOwnProperty('options')) update_filter_options(post.post_data.options);
1730 }
1731
1732 // Removing post_data field after update
1733 delete post.post_data;
1734
1735 if ('undefined' !== typeof backdrop && backdrop.length) {
1736 backdrop.remove();
1737 self.cleanupHistory();
1738 }
1739 }
1740
1741 if (wp.data.select('core/editor').didPostSaveRequestFail()) {
1742 if ('undefined' !== typeof backdrop && backdrop.length) {
1743 backdrop.remove();
1744 self.cleanupHistory();
1745 }
1746 }
1747
1748 if (wp.data.select('core/edit-post').isEditorPanelOpened('post-link')) {
1749 var post_link_interval = setInterval(function() {
1750 var post_link = $('.edit-post-post-link__link');
1751 if ('undefined' !== typeof post_link && post_link.length) {
1752 clearInterval(post_link_interval);
1753 $('.edit-post-post-link__link').attr('href', post.link).removeAttr('target').attr('onclick', 'UpdraftCentral_Library.open_browser_at(UpdraftCentral.$site_row, { module: "direct_url", url: "'+post.link+'" }, jQuery(\'#updraftcentral_dashboard_wrapper\'));return false;');
1754 }
1755 }, 500);
1756 }
1757
1758 if ('post' == module.type) {
1759 if (wp.data.select('core/edit-post').isEditorPanelOpened('taxonomy-panel-category')) {
1760 var categories_interval = setInterval(function() {
1761 var terms_list = $('.editor-post-taxonomies__hierarchical-terms-list');
1762 if ('undefined' !== typeof terms_list && terms_list.length) {
1763 var block_wait = terms_list.parent().find('.uc_block_load_wait');
1764 if (0 == terms_list.html().trim().length) {
1765 if (!block_wait.length) terms_list.parent().append('<div class="uc_block_load_wait"><img src="'+udclion.wpo.images+'spinner-2x.gif" class="uc-block-spinner" /></div>');
1766 } else {
1767 clearInterval(categories_interval);
1768 block_wait.remove();
1769 }
1770 }
1771 }, 500);
1772 }
1773
1774 if (wp.data.select('core/edit-post').isEditorPanelOpened('taxonomy-panel-post_tag')) {
1775 var tags_interval = setInterval(function() {
1776 var token_list = $('.components-form-token-field__input-container');
1777 var current_tags = wp.data.select('core/editor').getEditedPostAttribute('tags');
1778
1779 if ('undefined' !== typeof token_list && token_list.length && current_tags.length) {
1780 var token = token_list.find('.components-form-token-field__token');
1781 var block_wait = token_list.parent().find('.uc_block_load_wait');
1782
1783 if (0 == token.length && post.tags.length && false != post.tags[0]) {
1784 if (!block_wait.length) token_list.parent().append('<div class="uc_block_load_wait"><img src="'+udclion.wpo.images+'spinner-2x.gif" class="uc-block-spinner" /></div>');
1785 } else {
1786 clearInterval(tags_interval);
1787 block_wait.remove();
1788 }
1789 }
1790 }, 500);
1791 }
1792 }
1793 });
1794 }
1795
1796 /**
1797 * Updates the current post/page object within the preloaded list with the
1798 * latest information of that particular post/page
1799 *
1800 * @param {object} post_data An object containing the latest post object and its miscellaneous information
1801 * @param {string} previous_status The previous status of this post before the changes were applied
1802 *
1803 * @return {void}
1804 */
1805 function update_items_list(post_data, previous_status) {
1806 var data = self.manage_data.item('response');
1807 var posts = [];
1808
1809 // We're going to replace the old post/page with the latest
1810 // and updated version of the post/page in the current collection.
1811 for (var i=0; i<data[module.type+'s'].length; i++) {
1812 var item = data[module.type+'s'][i];
1813 if (post_data.post.ID == item.post.ID) {
1814 posts.push({
1815 post: post_data.post,
1816 misc: post_data.misc
1817 });
1818 } else {
1819 posts.push({
1820 post: item.post,
1821 misc: item.misc
1822 })
1823 }
1824 }
1825
1826 self.pagination.set_remote_data({
1827 info: data.info,
1828 items: posts
1829 });
1830
1831 // Returns the currently rendered items and the pagination
1832 render_items_and_pagination(1, $site_row);
1833
1834 // Update post counts if applicable
1835 if (previous_status != post_data.post.post_status) {
1836 var counts = data[module.type+'s_count'];
1837 counts[previous_status] -= 1;
1838 counts[post_data.post.post_status] += 1;
1839
1840 data[module.type+'s_count'] = counts;
1841 self.manage_data.update('response', data);
1842
1843 update_item_count(counts);
1844 $('ul#uc-navlinks a.uc-navlink-item[data-group="'+post_data.post.post_status+'"]').trigger('click');
1845 }
1846 }
1847
1848 /**
1849 * Loads functions and listeners primarily used by the classic editor's
1850 * internal workings
1851 *
1852 * @param {object} response The response object from the load editor request
1853 * @param {object} $location A jquery object representing the classic editor container
1854 *
1855 * @return {void}
1856 */
1857 function load_auxillary_functions(response, $location) {
1858 if ('undefined' == typeof response.data.post) {
1859 console.log(udclion[module.type+'s'].unable_to_load_editor);
1860 return;
1861 }
1862
1863 // Some editor libraries does not adhere to strict mode, thus, causing some error
1864 // that hampers the successful loading of the editor. Since, we don't have
1865 // any control over those libraries, we will just capture the error even
1866 // without processing it so that the editor can continue to load successfully.
1867 try {
1868 var post = response.data.post;
1869 var misc = response.data.misc;
1870
1871 if ('publish' == post.post_status) {
1872 $('#uc-editor-buttons > button#uc-switch-draft').show();
1873 }
1874 } catch (err) {
1875 console.log(err);
1876 }
1877
1878 var editor_rendered = setInterval(function() {
1879 var wrapper = $('div#wp-uc_classic_editor-wrap');
1880 if (wrapper.length) {
1881 // Disabling any previously rendered handler for the submit button, so that it won't
1882 // conflict with our own submission process.
1883 $('#submitdiv div#publishing-action input#publish').attr('onsubmit', 'return false;');
1884 $('.wp-editor-tabs button.wp-switch-editor').on('click', function() {
1885 var is_textview = $(this).hasClass('switch-html');
1886 if (is_textview) {
1887 $('div.mce-tinymce.mce-container').hide();
1888 $('textarea.wp-editor-area').css('visibility', 'visible').show();
1889 } else {
1890 $('textarea.wp-editor-area').hide();
1891 $('div.mce-tinymce.mce-container').css('visibility', 'visible').show();
1892 }
1893 });
1894 clearInterval(editor_rendered);
1895 $(window).scrollTop(0);
1896 if ('post' == module.type) $('div#category-all.tabs-panel').scrollTop(0);
1897
1898 if ('undefined' !== typeof tinymce && tinymce) {
1899 self.init_tiny_mce('uc_classic_editor');
1900 }
1901
1902 if (-1 !== $.inArray(post.post_status, ['publish', 'future'])) $('div#submitpost input#publish').val(udclion[module.type+'s'].update);
1903 if ('future' !== post.post_status && self.is_input_future_date()) {
1904 $('div#submitpost input#publish').val(udclion[module.type+'s'].schedule);
1905 }
1906 if (misc.hasOwnProperty('sticky')) {
1907 $('div#post-visibility-select span#sticky-span input#sticky').prop('checked', misc.sticky);
1908 var visibility = $('div#post-visibility-select input[name="visibility"]:checked').val();
1909 if ('public' === visibility) {
1910 if ($('div#post-visibility-select span#sticky-span input#sticky').is(':checked')) {
1911 $('div#misc-publishing-actions span#post-visibility-display').html(udclion.posts.public+', '+udclion.posts.sticky);
1912 }
1913 }
1914 }
1915
1916 if (post.hasOwnProperty('post_parent') && post.post_parent) $('#uc-page-attributes-container select#parent_id').val(post.post_parent);
1917 if (misc.hasOwnProperty('template') && misc.template) $('#uc-page-attributes-container select#page_template').val(misc.template);
1918 }
1919
1920 $('div#submitpost').find('input#save-post, input#publish').attr('class', 'btn btn-primary publishing-buttons');
1921 if ('undefined' !== typeof misc.link && misc.link) {
1922 $('div#classic-editor-dashboard div.post-action-buttons a#uc_preview_changes').attr('href', '#').removeAttr('target').attr('onclick', 'UpdraftCentral_Library.open_browser_at(UpdraftCentral.$site_row, { module: "direct_url", url: "'+misc.link+'" }, jQuery(\'#updraftcentral_dashboard_wrapper\'));');
1923 }
1924
1925 var insert_featured_image = $('div#postimagediv').find('a#set-post-thumbnail');
1926 if ('undefined' !== typeof insert_featured_image && insert_featured_image.length) {
1927 insert_featured_image.attr('href', '');
1928 }
1929
1930 // Set handlers/listeners
1931 $('div#submitpost div#save-action input#save-post').on('click', function() {
1932 var value = $(this).val();
1933 switch (value.toLowerCase()) {
1934 case udclion[module.type+'s'].save_as_pending.toLowerCase():
1935 self.uc_editor.edits.update('status', 'pending');
1936 break;
1937 case udclion[module.type+'s'].save_draft.toLowerCase():
1938 self.uc_editor.edits.update('status', 'draft');
1939 break;
1940 }
1941
1942 self.uc_editor.state.update('save_draft_pending', true);
1943 $('div#submitpost div#publishing-action input#publish').trigger('click');
1944 });
1945
1946 $('div#submitpost div#publishing-action input#publish').on('click', function() {
1947 var container = $(this).closest('div#classic_editor_container');
1948 var value = $(this).val();
1949
1950 if (self.uc_editor.state.exists('editing') && self.uc_editor.state.item('editing')) {
1951 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+udclion[module.type+'s'].currently_editing+'</p>');
1952 return;
1953 }
1954
1955 // Make sure we switch to the "visual" tab before getting the content to reflect any
1956 // new changes done within the "text" tab, otherwise, new changes will be discarded
1957 // as soon as you call the getContent() method of the active editor.
1958 $('button#uc_classic_editor-tmce').trigger('click');
1959
1960 var content = tinyMCE.activeEditor.getContent();
1961 content = self.adjust_local_reference(content);
1962
1963 self.uc_editor.edits.bulk_update({
1964 id: post.ID,
1965 title: container.find('div.title-container input#title').val(),
1966 content: content,
1967 source: 'classic'
1968 });
1969
1970 // Override any selected status if the "Publish" button is clicked.
1971 if ('publish' == value.toLowerCase() && !self.uc_editor.state.exists('save_draft_pending')) {
1972 self.uc_editor.edits.update('status', 'publish');
1973 }
1974
1975 // Update the statuts to "future" if date edits exists and it is scheduled or intended
1976 // to be publish in the future.
1977 if (self.is_input_future_date()) {
1978 self.uc_editor.edits.update('status', 'future');
1979 }
1980
1981 if (self.uc_editor.edits.exists('featured_media')) {
1982 self.uc_editor.edits.update('featured_media', parseInt($('input#_thumbnail_id').val()));
1983 }
1984
1985 var param = {
1986 name: 'save',
1987 arguments: self.uc_editor.edits.get_collection_object()
1988 };
1989
1990 var editor_container = $(document.body);
1991 editor_container.append('<div class="modal-backdrop-container"><div class="modal-backdrop fade show"></div><div class="updraftcentral_spinner"></div></div>');
1992 send_command(param, $site_row).then(function(response) {
1993 if ('undefined' !== typeof response.post && response.post) {
1994 // reload editor and update items list after publish
1995 var post_data = {
1996 post: JSON.parse(response.post),
1997 misc: response.misc
1998 }
1999
2000 if ('post' == module.type) {
2001 if ('undefined' !== typeof response.preloaded && response.preloaded) {
2002 var preloaded = JSON.parse(response.preloaded);
2003 var preloaded_data = self.manage_data.item('preloaded_data');
2004
2005 if ('undefined' !== typeof preloaded_data && preloaded_data) {
2006 preloaded_data = JSON.parse(preloaded_data);
2007 preloaded_data.categories = preloaded.categories;
2008 preloaded_data.tags = preloaded.tags;
2009 self.manage_data.update('preloaded_data', JSON.stringify(preloaded_data));
2010 }
2011 }
2012 }
2013
2014 if ('manage' === self.current_section) update_items_list(post_data, post.post_status);
2015 self.load_updraftcentral_editor('classic', post_data, $site_row);
2016
2017 if (response.hasOwnProperty('options')) update_filter_options(response.options);
2018 } else {
2019 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+udclion[module.type+'s'].unkown_error+'</p>');
2020
2021 // Just making sure that the backdrop is removed after we've already received the
2022 // response from the remote site.
2023 var backdrop = editor_container.find('div.modal-backdrop-container');
2024 if ('undefined' !== typeof backdrop && backdrop.length) backdrop.remove();
2025 }
2026 }).always(function() {
2027 self.uc_editor.state.remove('save_draft_pending');
2028 });
2029 });
2030
2031 $('#uc-editor-buttons > button#uc-editor-close').on('click', function() {
2032 var editor_container = $(document.body);
2033 var classic_editor = editor_container.find('div#classic_editor_container');
2034 if ('undefined' !== typeof classic_editor && classic_editor) {
2035 if (self.uc_editor.state.exists('editing') && self.uc_editor.state.item('editing')) {
2036 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+udclion[module.type+'s'].currently_editing+'</p>');
2037 return;
2038 }
2039
2040 classic_editor.remove();
2041 // Make sure that any visible spinner(s) are remove on close.
2042 if ($('.updraftcentral_spinner').is(':visible')) {
2043 $('.updraftcentral_spinner').remove();
2044 }
2045
2046 if ('undefined' !== typeof tinymce && tinymce) {
2047 var editor_id = tinymce.activeEditor.id;
2048 tinymce.EditorManager.execCommand('mceRemoveEditor', true, editor_id);
2049
2050 // For old version:
2051 tinymce.EditorManager.execCommand('mceRemoveControl', true, editor_id);
2052 }
2053
2054 $(document.body).trigger('updraftcentral_'+module.type+'_editor_closed', [classic_editor]);
2055 }
2056 });
2057
2058 $('#uc-editor-buttons > button#uc-switch-draft').on('click', function() {
2059 self.uc_editor.edits.update('status', 'draft');
2060 $('div#submitpost div#publishing-action input#publish').trigger('click');
2061 });
2062
2063 $('#edit-slug-box span#edit-slug-buttons > button.edit-slug').on('click', function() {
2064 var editable_post = $('#edit-slug-box span#editable-post-name');
2065 var anchor = editable_post.closest('a');
2066 anchor.attr('onclick', 'return false;');
2067 anchor.attr('target', '_blank');
2068
2069 if ('undefined' !== typeof editable_post && editable_post) {
2070 self.uc_editor.state.update('editing', true);
2071 var input_slug = $('<input/>', {
2072 type: 'text',
2073 id: 'uc-input-slug',
2074 class: 'editable-slug',
2075 value: $('span#editable-post-name-full').html()
2076 });
2077 editable_post.html(input_slug);
2078
2079 $(this).hide();
2080 var btn_ok = $('<button/>', {
2081 text: udclion[module.type+'s'].ok,
2082 id: 'uc-btn-ok',
2083 class: 'uc-slug-buttons',
2084 });
2085
2086 btn_ok.on('click', function() {
2087 var slug = editable_post.find('input#uc-input-slug');
2088 if ('undefined' !== typeof slug && slug) {
2089 var slug_value = slug.val();
2090 if (0 == slug_value.trim().length) {
2091 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+udclion[module.type+'s'].slug_missing+'</p>');
2092 return;
2093 } else {
2094 $('span#editable-post-name-full').html(slug_value);
2095 slug.remove();
2096
2097 editable_post.html(slug_value);
2098 anchor.attr('href', anchor.text());
2099 anchor.removeAttr('onclick');
2100
2101 self.uc_editor.edits.update('slug', slug_value);
2102 self.uc_editor.state.remove('editing');
2103
2104 $('#edit-slug-box span#edit-slug-buttons > button#uc-btn-ok').remove();
2105 $('#edit-slug-box span#edit-slug-buttons > button#uc-btn-cancel').remove();
2106 $('#edit-slug-box span#edit-slug-buttons > button.edit-slug').show();
2107 }
2108 }
2109 });
2110 $('#edit-slug-box span#edit-slug-buttons').append(btn_ok);
2111
2112 var btn_cancel = $('<button/>', {
2113 text: udclion[module.type+'s'].cancel,
2114 id: 'uc-btn-cancel',
2115 class: 'uc-slug-buttons',
2116 });
2117
2118 btn_cancel.on('click', function() {
2119 var slug = editable_post.find('input#uc-input-slug');
2120 if ('undefined' !== typeof slug && slug) {
2121 slug.remove();
2122 editable_post.html($('span#editable-post-name-full').html());
2123 anchor.removeAttr('onclick');
2124 self.uc_editor.state.remove('editing');
2125
2126 $('#edit-slug-box span#edit-slug-buttons > button#uc-btn-ok').remove();
2127 $('#edit-slug-box span#edit-slug-buttons > button#uc-btn-cancel').remove();
2128 $('#edit-slug-box span#edit-slug-buttons > button.edit-slug').show();
2129 }
2130 });
2131 $('#edit-slug-box span#edit-slug-buttons').append(btn_cancel);
2132 }
2133 });
2134
2135 $('#classic-editor-dashboard div#submitdiv').find('a.button, a.button-cancel').not('a#post-preview').addClass('btn btn-secondary');
2136 $('div#classic_editor a.edit-post-status').attr('href', '#misc-publishing-actions');
2137 $('div#classic_editor a.edit-post-status').on('click', function(e) {
2138 $(this).hide();
2139 $('div#post-status-select').slideDown('fast');
2140 });
2141
2142 $('div#post-status-select a.save-post-status').on('click', function(e) {
2143 var parent = $(this).closest('div#post-status-select');
2144 var status = parent.find('select#post_status').val();
2145 self.uc_editor.edits.update('status', status);
2146
2147 $(this).siblings('a.cancel-post-status').trigger('click');
2148 switch (status) {
2149 case 'pending':
2150 $('div#submitpost input#save-post').val(udclion[module.type+'s'].save_as_pending).show();
2151 status_text = udclion[module.type+'s'].pending_review;
2152 break;
2153 case 'draft':
2154 $('div#submitpost input#save-post').val(udclion[module.type+'s'].save_draft).show();
2155 status_text = udclion[module.type+'s'].draft;
2156 break;
2157 case 'publish':
2158 $('div#submitpost input#save-post').hide();
2159 status_text = udclion[module.type+'s'].published;
2160 break;
2161 default:
2162 break;
2163 }
2164
2165 $('div#misc-publishing-actions span#post-status-display').html(status_text);
2166 if ('future' !== post.post_status && self.is_input_future_date()) {
2167 $('div#submitpost input#publish').val(udclion[module.type+'s'].schedule);
2168 }
2169 });
2170
2171 $('div#post-status-select a.cancel-post-status').on('click', function(e) {
2172 $('div#post-status-select').slideUp('fast', 'swing', function() {
2173 $('div#classic_editor a.edit-post-status').show();
2174 });
2175 });
2176
2177 $('div#classic_editor a.edit-visibility').on('click', function(e) {
2178 $(this).hide();
2179 if ('post' == module.type) {
2180 if ($('div#post-visibility-select input#visibility-radio-public').is(':checked')) {
2181 $('div#post-visibility-select span#sticky-span').show();
2182 } else {
2183 $('div#post-visibility-select span#sticky-span').hide();
2184 }
2185 }
2186
2187 if ($('div#post-visibility-select input#visibility-radio-password').is(':checked')) {
2188 $('div#post-visibility-select span#password-span').show();
2189 } else {
2190 $('div#post-visibility-select span#password-span').hide();
2191 }
2192 $('div#post-visibility-select').slideDown('fast');
2193 });
2194
2195 $('div#post-visibility-select span#sticky-span input#sticky').on('click', function(e) {
2196 self.uc_editor.edits.update('sticky', $(this).is(':checked') ? true : false);
2197 });
2198
2199 $('div#post-visibility-select a.save-post-visibility').on('click', function(e) {
2200 var parent = $(this).closest('div#post-visibility-select');
2201 var checked = parent.find('input[name="visibility"]:checked').val();
2202 self.uc_editor.edits.update('visibility', checked);
2203
2204 if ('private' == checked) {
2205 $('div#submitpost input#publish').val(udclion[module.type+'s'].update);
2206 $('div#misc-publishing-actions span#post-status-display').html(udclion[module.type+'s'].privately_published);
2207 $('div#misc-publishing-actions a.edit-post-status').hide();
2208 $('div#submitpost input#save-post').hide();
2209 } else {
2210 if (-1 === $.inArray(post.post_status, ['publish', 'future'])) {
2211 $('div#submitpost input#publish').val(udclion[module.type+'s'].publish);
2212 } else {
2213 if ('future' !== post.post_status && self.is_input_future_date()) {
2214 $('div#submitpost input#publish').val(udclion[module.type+'s'].schedule);
2215 } else {
2216 $('div#submitpost input#publish').val(udclion[module.type+'s'].update);
2217 }
2218 }
2219
2220 if ('password' == checked) {
2221 var password = parent.find('input#post_password').val();
2222 if (0 == password.trim().length) {
2223 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+udclion[module.type+'s'].password_missing+'</p>');
2224 return;
2225 } else {
2226 self.uc_editor.edits.update('password', password);
2227 }
2228 }
2229
2230 var status = self.uc_editor.edits.exists('status') ? self.uc_editor.edits.item('status') : post.post_status;
2231 switch (status) {
2232 case 'pending':
2233 status_text = udclion[module.type+'s'].pending_review;
2234 break;
2235 case 'draft':
2236 status_text = udclion[module.type+'s'].draft;
2237 break;
2238 case 'publish':
2239 status_text = udclion[module.type+'s'].published;
2240 break;
2241 default:
2242 status_text = udclion[module.type+'s'].published;
2243 $('#post-status-select select#post_status option[value="publish"]').prop('selected', true).html(status_text);
2244 break;
2245 }
2246
2247 $('div#misc-publishing-actions a.edit-post-status').show();
2248 $('div#misc-publishing-actions span#post-status-display').html(status_text);
2249 $('div#post-status-select a.save-post-status').trigger('click');
2250 }
2251
2252 switch (checked) {
2253 case 'public':
2254 visibility_text = udclion[module.type+'s'].public;
2255 if ($('div#post-visibility-select span#sticky-span input#sticky').is(':checked')) {
2256 visibility_text += ', '+udclion[module.type+'s'].sticky;
2257 }
2258 break;
2259 case 'password':
2260 visibility_text = udclion[module.type+'s'].password_protected;
2261 break;
2262 case 'private':
2263 visibility_text = udclion[module.type+'s'].private;
2264 break;
2265 default:
2266 break;
2267 }
2268
2269 $('div#misc-publishing-actions span#post-visibility-display').html(visibility_text);
2270 $(this).siblings('a.cancel-post-visibility').trigger('click');
2271 });
2272
2273 $('div#post-visibility-select a.cancel-post-visibility').on('click', function(e) {
2274 $('div#post-visibility-select').slideUp('fast', 'swing', function() {
2275 $('div#classic_editor a.edit-visibility').show();
2276 });
2277 });
2278
2279 $('div#post-visibility-select input#visibility-radio-password').on('click', function(e) {
2280 if ('post' == module.type) $('div#post-visibility-select span#sticky-span').hide();
2281 $('div#post-visibility-select span#password-span').show();
2282 });
2283
2284 $('div#post-visibility-select').on('click', 'input#visibility-radio-public, input#visibility-radio-private', function(e) {
2285 if ('post' == module.type) {
2286 var id = $(this).attr('id');
2287 if ('visibility-radio-public' === id) {
2288 $('div#post-visibility-select span#sticky-span').show();
2289 } else {
2290 $('div#post-visibility-select span#sticky-span').hide();
2291 }
2292 }
2293 $('div#post-visibility-select span#password-span').hide();
2294 });
2295
2296 $('div#classic_editor a.edit-timestamp').on('click', function(e) {
2297 $(this).hide();
2298 $('fieldset#timestampdiv').slideDown('fast');
2299
2300 var timestamp = self.uc_editor.edits.item('timestamp');
2301 var mm = ('undefined' !== typeof timestamp && 'undefined' !== typeof timestamp.month) ? timestamp.month : misc.published_date.mm;
2302 var jj = ('undefined' !== typeof timestamp && 'undefined' !== typeof timestamp.day) ? timestamp.day : misc.published_date.jj;
2303 var aa = ('undefined' !== typeof timestamp && 'undefined' !== typeof timestamp.year) ? timestamp.year : misc.published_date.aa;
2304 var hh = ('undefined' !== typeof timestamp && 'undefined' !== typeof timestamp.hour) ? timestamp.hour : misc.published_date.hh;
2305 var mn = ('undefined' !== typeof timestamp && 'undefined' !== typeof timestamp.minute) ? timestamp.minute : misc.published_date.mn;
2306 var ss = ('undefined' !== typeof timestamp && 'undefined' !== typeof timestamp.second) ? timestamp.second : misc.published_date.ss;
2307
2308 $('#timestampdiv select#mm').val(mm);
2309 $('#timestampdiv input#jj').val(jj);
2310 $('#timestampdiv input#aa').val(aa);
2311 $('#timestampdiv input#hh').val(hh);
2312 $('#timestampdiv input#mn').val(mn);
2313 $('#timestampdiv input#ss').val(ss);
2314 });
2315
2316 $('fieldset#timestampdiv a.save-timestamp').on('click', function(e) {
2317 var parent = $(this).closest('fieldset#timestampdiv');
2318 var wrapper = parent.find('div.timestamp-wrap');
2319 var input_date = {
2320 month: wrapper.find('select#mm').val(),
2321 day: wrapper.find('input#jj').val(),
2322 year: wrapper.find('input#aa').val(),
2323 hour: wrapper.find('input#hh').val(),
2324 minute: wrapper.find('input#mn').val(),
2325 second: parent.find('input#ss').val()
2326 };
2327
2328 if (self.validate_input_date(input_date)) {
2329 var result = self.prepare_date(input_date);
2330 var timestamp = $('div#misc-publishing-actions span#timestamp > b').html(result.formatted);
2331 self.uc_editor.edits.update('date', result.date);
2332 self.uc_editor.edits.update('timestamp', result.timestamp);
2333
2334 $(this).siblings('a.cancel-timestamp').trigger('click');
2335 if ('future' !== post.post_status && self.is_input_future_date()) {
2336 $('div#submitpost input#publish').val(udclion[module.type+'s'].schedule);
2337 }
2338 } else {
2339 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+udclion[module.type+'s'].invalid_date_input+'</p>');
2340 }
2341 });
2342
2343 $('fieldset#timestampdiv a.cancel-timestamp').on('click', function(e) {
2344 $('fieldset#timestampdiv').slideUp('fast', 'swing', function() {
2345 $('div#classic_editor a.edit-timestamp').show();
2346 });
2347 });
2348
2349 $('div#postimagediv').on('click', 'a#set-post-thumbnail', function(e) {
2350 var media_button = $('#insert-media-button');
2351 if ('undefined' == typeof media_button || 0 == media_button.length) media_button = $('button.insert-media');
2352
2353 media_button.trigger('click', ['featured_image']);
2354 });
2355
2356 $('.wp-media-buttons').on('click', '#insert-media-button, button.insert-media', function(event, data) {
2357 var feature_image = ('undefined' !== typeof data) ? true : false;
2358 update_media_display(feature_image);
2359 });
2360
2361 $('div#postimagediv').on('click', 'a#remove-post-thumbnail', remove_featured_image);
2362 $(document).on('click', 'button.media-button-select', function(e) {
2363 var select_media_interval = setInterval(function() {
2364 var attachment = $('#postimagediv img.attachment-post-thumbnail');
2365 var href = $('div#postimagediv a#remove-post-thumbnail').attr('href');
2366 if ('undefined' !== typeof attachment && attachment.length && '#' == href) {
2367 clearInterval(select_media_interval);
2368
2369 $('div#postimagediv a#remove-post-thumbnail').attr('href', '#set-post-thumbnail');
2370 self.uc_editor.edits.update('featured_media', $('input#_thumbnail_id').val());
2371 }
2372 }, 100);
2373 });
2374
2375 if ('post' == module.type) {
2376 $('#categorydiv a#category-add-toggle').on('click', function(e) {
2377 $('div#category-adder p#category-add').show();
2378 });
2379
2380 $('a#link-post_tag').on('click', function(e) {
2381 var mostused = $('div#mostused-post_tag').html().trim();
2382 if (0 == mostused.length && mostused !== udclion.posts.no_tags_found) $('div#mostused-post_tag').html(udclion.posts.no_tags_found);
2383 if (!$('div#mostused-post_tag').is(':visible')) {
2384 $('div#mostused-post_tag').show();
2385 } else {
2386 $('div#mostused-post_tag').hide();
2387 }
2388 });
2389
2390 $('div.categorydiv').on('click', 'ul#categorychecklist input[type="checkbox"], ul#categorychecklist-pop input[type="checkbox"]', function() {
2391 var id = $(this).val();
2392 var section = $(this).closest('.categorychecklist').attr('id');
2393
2394 switch (section) {
2395 case 'categorychecklist':
2396 $('ul#categorychecklist-pop li input[value="'+id+'"]').prop('checked', $(this).is(':checked'));
2397 break;
2398 case 'categorychecklist-pop':
2399 $('ul#categorychecklist li input[value="'+id+'"]').prop('checked', $(this).is(':checked'));
2400 break;
2401 default:
2402 break;
2403 }
2404
2405 var categories = [];
2406 $('#categorychecklist li > label.selectit > input[type="checkbox"]').each(function() {
2407 if ($(this).is(':checked')) {
2408 categories.push($(this).val());
2409 }
2410 });
2411 self.uc_editor.edits.update('categories', categories);
2412 });
2413
2414 $('div.categorydiv input#category-add-submit').on('click', function() {
2415 var category = $('input#newcategory');
2416 var parent = $('select#newcategory_parent');
2417
2418 if ('undefined' !== typeof category.val() && category.val().length) {
2419 var value = category.val().trim();
2420 var new_category = parent.val().length ? parent.val()+':'+value : value;
2421
2422 $('#categorychecklist').prepend('<li id="category-0" class="popular-category"><label class="selectit"><input value="'+new_category+'" type="checkbox" name="post_category[]" id="in-category-0" checked="checked"> '+category.val()+'</label></li>');
2423
2424 category.val('');
2425 parent.val('');
2426
2427 $(this).closest('p#category-add').hide();
2428
2429 var categories = [];
2430 $('#categorychecklist li > label.selectit > input[type="checkbox"]').each(function() {
2431 if ($(this).is(':checked')) {
2432 categories.push($(this).val());
2433 }
2434 });
2435 self.uc_editor.edits.update('categories', categories);
2436 } else {
2437 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+udclion.posts.need_category_to_add+'</p>');
2438 }
2439 });
2440
2441 $('#tagsdiv-post_tag input.tagadd').on('click', function() {
2442 var new_tag = $('#tagsdiv-post_tag input#new-tag-post_tag');
2443 if ('undefined' !== typeof new_tag.val() && new_tag.val().length) {
2444 var tags = [new_tag.val()];
2445 if (-1 !== new_tag.val().indexOf(',')) {
2446 tags = new_tag.val().split(',');
2447 }
2448
2449 var post_tags = [];
2450 $('#tagsdiv-post_tag .tagchecklist > span').each(function() {
2451 post_tags.push($(this).text().trim());
2452 });
2453
2454 var tags_container = $('#tagsdiv-post_tag .tagchecklist');
2455 var last_index = tags_container.find('> span').length;
2456 for (var i=0; i<tags.length; i++) {
2457 var tag = tags[i].trim();
2458
2459 if (!self.is_ivalue_exists(tag, post_tags)) {
2460 tags_container.append('<span><button type="button" id="post_tag-check-num-'+last_index+'" class="ntdelbutton"><span class="remove-tag-icon" aria-hidden="true"></span></button>&nbsp;'+tag+'</span>');
2461 tags_container.find('#post_tag-check-num-'+last_index).on('click', function() {
2462 $(this).parent().remove();
2463 var current_tags = [];
2464 $('#tagsdiv-post_tag .tagchecklist > span').each(function() {
2465 current_tags.push($(this).text().trim());
2466 });
2467 self.uc_editor.edits.update('tags', current_tags);
2468 });
2469
2470 post_tags.push(tag);
2471 last_index++;
2472 }
2473 }
2474
2475 new_tag.val('');
2476 self.uc_editor.edits.update('tags', post_tags);
2477 } else {
2478 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+udclion.posts.need_something_to_add+'</p>');
2479 }
2480 });
2481
2482 $('#tagsdiv-post_tag .tagchecklist button.ntdelbutton').on('click', function() {
2483 $(this).parent().remove();
2484 var tags = [];
2485 $('#tagsdiv-post_tag .tagchecklist > span').each(function() {
2486 tags.push($(this).text().trim());
2487 });
2488 self.uc_editor.edits.update('tags', tags);
2489 });
2490
2491 $('#tagsdiv-post_tag #mostused-post_tag a').each(function() {
2492 // Disabling any links associated with the "Most Used" items
2493 $(this).attr('href', 'javascript://');
2494 });
2495
2496 $('#tagsdiv-post_tag #mostused-post_tag a').on('click', function() {
2497 var tags = [];
2498
2499 if (self.uc_editor.edits.exists('tags')) {
2500 tags = self.uc_editor.edits.item('tags');
2501 } else {
2502 $('#tagsdiv-post_tag .tagchecklist > span').each(function() {
2503 tags.push($(this).text().trim());
2504 });
2505 }
2506
2507 var tags_container = $('#tagsdiv-post_tag .tagchecklist');
2508 var last_index = tags_container.find('> span').length;
2509 var value = $(this).text().replace(/\(\d\)/gi, '').trim();
2510
2511 if (!self.is_ivalue_exists(value, tags)) {
2512 tags_container.append('<span><button type="button" id="post_tag-check-num-'+last_index+'" class="ntdelbutton"><span class="remove-tag-icon" aria-hidden="true"></span></button>&nbsp;'+value+'</span>');
2513 tags_container.find('#post_tag-check-num-'+last_index).on('click', function() {
2514 $(this).parent().remove();
2515 var current_tags = [];
2516 $('#tagsdiv-post_tag .tagchecklist > span').each(function() {
2517 current_tags.push($(this).text().trim());
2518 });
2519 self.uc_editor.edits.update('tags', current_tags);
2520 });
2521
2522 tags.push(value);
2523 self.uc_editor.edits.update('tags', tags);
2524 }
2525 });
2526
2527 $('#categorydiv a#uc_tab_category_all').on('click', function(e) {
2528 $('#categorydiv a#uc_tab_category_all').closest('li').toggleClass('tabs');
2529 $('#categorydiv a#uc_tab_category_pop').closest('li').toggleClass('tabs');
2530 $('div#category-pop').hide();
2531 $('div#category-all').show();
2532 });
2533
2534 $('#categorydiv a#uc_tab_category_pop').on('click', function(e) {
2535 $('div#category-all ul#categorychecklist li input:checked').each(function() {
2536 var id = $(this).val();
2537 $('div#category-pop ul#categorychecklist-pop li input[value="'+id+'"]').prop('checked', true);
2538 });
2539
2540 $('#categorydiv a#uc_tab_category_all').closest('li').toggleClass('tabs');
2541 $('#categorydiv a#uc_tab_category_pop').closest('li').toggleClass('tabs');
2542 $('div#category-all').hide();
2543 $('div#category-pop').show();
2544 });
2545 }
2546
2547 $('#uc-page-attributes-container select[name="parent_id"]').on('change', function(e) {
2548 self.uc_editor.edits.update('parent', $(this).val());
2549 });
2550
2551 $('#uc-page-attributes-container select[name="page_template"]').on('change', function(e) {
2552 self.uc_editor.edits.update('template', $(this).val());
2553 });
2554
2555 $('#uc-page-attributes-container input[name="menu_order"]').on('change', function(e) {
2556 validate_quick_edit(null, $(this), 'order', true, true);
2557 });
2558 }, 500);
2559 }
2560
2561 /**
2562 * Updates the current filter options with the latest options received
2563 * from the remote site
2564 *
2565 * @param {object} options An object containing the updated options for display
2566 *
2567 * @return {void}
2568 */
2569 function update_filter_options(options) {
2570 if ('undefined' !== typeof options && options) {
2571 var filter_template, date_options;
2572 var params = {
2573 date_filter: options.date
2574 };
2575
2576 if ('post' == module.type) params.category_filter = options.category;
2577 filter_template = UpdraftCentral.template_replace(module.type+'s-manage-filters', params);
2578
2579 date_options = $('<div/>', {
2580 class: 'hidden'
2581 }).html(filter_template).find('select.uc-'+module.type+'-date-filter');
2582 $('.uc-'+module.type+'-buttons-filters select.uc-'+module.type+'-date-filter').replaceWith(date_options);
2583
2584 if ('post' == module.type) {
2585 var category_options = $('<div/>', {
2586 class: 'hidden'
2587 }).html(filter_template).find('select.uc-post-category-filter');
2588 $('.uc-post-buttons-filters select.uc-post-category-filter').replaceWith(category_options);
2589 }
2590 }
2591 }
2592
2593 /**
2594 * Updates the media library display by triggering either the "feature image" section or the "add media"
2595 *
2596 * @param {boolean} feature_image Indicate wether to pull the feature image section automatically
2597 *
2598 * @return {void}
2599 */
2600 function update_media_display(feature_image) {
2601 var media_interval = setInterval(function() {
2602 var media_modal = $("div[id^='__wp-uploader'].supports-drag-drop");
2603 if ('undefined' !== typeof media_modal && media_modal.length) {
2604 clearInterval(media_interval);
2605 var media_frame = media_modal.find('.media-frame');
2606
2607 if (feature_image) {
2608 var featured_image_menu = media_modal.find(".media-menu-item:contains('"+udclion[module.type+'s'].featured_image+"')");
2609 if (0 == featured_image_menu.length) {
2610 featured_image_menu = media_modal.find("#menu-item-featured-image");
2611 }
2612
2613 if ('undefined' !== typeof featured_image_menu && featured_image_menu.length) {
2614 featured_image_menu.trigger('click');
2615 var media_lib = media_modal.find(".media-menu-item:contains('"+udclion[module.type+'s'].media_library+"')");
2616 if (media_lib.length && !media_lib.hasClass('active')) media_lib.trigger('click');
2617 }
2618
2619 media_frame.find('div.media-frame-menu').hide();
2620 media_frame.find('.media-frame-menu-heading').hide();
2621 media_frame.find("div[class^='media-frame-']").not('.media-frame-menu').css('left', '0');
2622 } else {
2623 var add_media = media_modal.find(".media-menu-item:contains('"+udclion[module.type+'s'].add_media+"')");
2624 if (0 == add_media.length) {
2625 add_media = media_modal.find("#menu-item-insert");
2626 }
2627
2628 if ('undefined' !== typeof add_media && add_media.length) add_media.trigger('click');
2629 media_frame.find("div[class^='media-frame-']").not('.media-frame-menu').css('left', '200px');
2630 media_frame.find('.media-frame-menu-heading').show();
2631 media_frame.find('div.media-frame-menu').show();
2632 }
2633 }
2634 }, 100);
2635 }
2636
2637 /**
2638 * Removes featured image using the classic editor
2639 *
2640 * @param {object} e Event object
2641 *
2642 * @return {void}
2643 */
2644 function remove_featured_image(e) {
2645 e.preventDefault();
2646
2647 var inside = $(this).closest('.inside');
2648 inside.find('#set-post-thumbnail-desc').remove();
2649 inside.find('#set-post-thumbnail').removeAttr('aria-describedby').html(udclion[module.type+'s'].set_featured_image);
2650 inside.find('input#_thumbnail_id').val(0);
2651
2652 $(this).parent().remove();
2653 self.uc_editor.edits.update('featured_media', 0);
2654 }
2655
2656 /**
2657 * Attaches UpdraftCentral logo and some action buttons to the Block editor (Gutenberg)
2658 *
2659 * @param {string} logo A string containing the url of the UpdraftCentral logo
2660 * @param {object} misc The post object miscellaneous data
2661 *
2662 * @return {void}
2663 */
2664 function attach_updraftcentral_buttons(logo, misc) {
2665 var toolbar_container = $('div.edit-post-header-toolbar');
2666 if (toolbar_container.length) {
2667 if (!toolbar_container.find('.uc-logo-container').length) {
2668 var logo_container = $('<div/>', {
2669 class: 'uc-logo-container',
2670 });
2671 logo_container.html('<img class="logo-landscape" src="'+logo+'" alt="UpdraftCentral" width="165" height="30">');
2672 toolbar_container.prepend(logo_container);
2673 }
2674 }
2675
2676 var container = $('div.edit-post-header__settings');
2677 if (container.length) {
2678 if (!container.find('#uc_close_editor').length) {
2679 var btn_close = $('<button/>', {
2680 text: udclion[module.type+'s'].close_editor,
2681 id: 'uc_close_editor',
2682 type: 'button',
2683 class: 'components-button editor-post-close is-button is-default is-large',
2684 style: 'padding: 0 12px 2px; margin: 2px; height: 33px; line-height: 32px;',
2685 });
2686
2687 btn_close.on('click', function() {
2688 var editor_container = $(document.body);
2689 var container = editor_container.find('div#gutenberg_editor_container');
2690 if (container.length) {
2691 container.hide();
2692 }
2693
2694 // Make sure that any visible spinner(s) are remove on close.
2695 if ($('.updraftcentral_spinner').is(':visible')) {
2696 $('.updraftcentral_spinner').remove();
2697 }
2698
2699 $(document.body).trigger('updraftcentral_'+module.type+'_editor_closed', [container]);
2700 });
2701 container.prepend(btn_close);
2702 }
2703
2704 if (!container.find('#uc_preview_lnk').length && 'undefined' !== typeof misc) {
2705 var anchor_preview = $('<a/>', {
2706 id: 'uc_preview_lnk',
2707 href: '#',
2708 onclick: 'UpdraftCentral_Library.open_browser_at(UpdraftCentral.$site_row, { module: "direct_url", url: "'+misc.link+'" }, jQuery(\'#updraftcentral_dashboard_wrapper\'));',
2709 });
2710
2711 var btn_preview = $('<button/>', {
2712 text: udclion[module.type+'s'].preview,
2713 id: 'uc_preview_post',
2714 type: 'button',
2715 class: 'components-button editor-preview-post is-button is-default is-large',
2716 });
2717 anchor_preview.append(btn_preview);
2718 container.find('#uc_close_editor').after(anchor_preview);
2719 }
2720 }
2721 }
2722
2723 /**
2724 * Updates the block editor's embedded local view posts/pages link to connect and open the list
2725 * of posts/pages from the remote site.
2726 *
2727 * @return {void}
2728 */
2729 function update_view_posts_link() {
2730 var admin_url = UpdraftCentral.$site_row.data('admin_url'),
2731 view_posts, repeat_count = 0,
2732 lnk_checker, default_tries = 60;
2733
2734 lnk_checker = setInterval(function() {
2735 view_posts = $('.edit-post-fullscreen-mode-close__toolbar > a');
2736 if ('undefined' == typeof view_posts || null == view_posts || 0 == view_posts.length) {
2737 view_posts = $('a.edit-post-fullscreen-mode-close');
2738 }
2739
2740 if (view_posts.length) {
2741 clearInterval(lnk_checker);
2742
2743 if ('#' !== view_posts.attr('href')) {
2744 redirect_url = admin_url+view_posts.attr('href');
2745 view_posts.attr('href', '#').removeAttr('target').attr('onclick', 'UpdraftCentral_Library.open_browser_at(UpdraftCentral.$site_row, { module: "direct_url", url: "'+redirect_url+'" }, jQuery(\'#updraftcentral_dashboard_wrapper\'));');
2746 }
2747 } else {
2748 // If the view posts/pages link is not found after some tries then we can assumed that the fullscreen mode
2749 // is not set. We only add this check because by the time the editor loads and the fullscreen mode
2750 // was set it will still take a few seconds for it to render completely. The 60 is just a conservative value
2751 // in order to make sure that we can replace the link successfully with the actual remote link.
2752 if (repeat_count > default_tries) clearInterval(lnk_checker);
2753 }
2754
2755 repeat_count++;
2756 }, 1000);
2757 }
2758
2759 /**
2760 * Filters the metaboxes content with only the allowed metaboxes to display
2761 *
2762 * @param {string} content The metaboxes content to filter
2763 *
2764 * @return {string}
2765 */
2766 function filter_metaboxes(content) {
2767 var container = $('<div/>').html(content),
2768 id;
2769
2770 var allowed = ['submitdiv', 'pageparentdiv', 'postimagediv'];
2771 if ('post' == module.type) {
2772 allowed = ['submitdiv', 'categorydiv', 'tagsdiv-post_tag', 'postimagediv'];
2773 }
2774
2775 container.find('div.postbox').each(function() {
2776 id = $(this).attr('id');
2777 if (Array.isArray(allowed) && allowed.length && -1 == $.inArray(id, allowed)) container.find('#'+id).remove();
2778 });
2779
2780 return container.html();
2781 }
2782
2783 /**
2784 * Set block categories,definitions and unregister blocks for new editing
2785 *
2786 * @param {object} data The object holding the block settings
2787 *
2788 * @return {void}
2789 */
2790 function initiate_blocks_reset(data) {
2791 wp.blocks.setCategories(data.block_categories);
2792 wp.blocks.unstable__bootstrapServerSideBlockDefinitions(data.block_definitions);
2793
2794 var block_types = wp.blocks.getBlockTypes();
2795 if (0 < block_types.length) {
2796 for (var i=0; i<block_types.length; i++) {
2797 var type = block_types[i];
2798 wp.blocks.unregisterBlockType(type.name);
2799 }
2800 }
2801 }
2802
2803 /**
2804 * Processes the response from a load editor request
2805 *
2806 * @param {string} editor The type of editor to load (e.g. 'classic' or 'gutenberg')
2807 * @param {object} response The response object containing the needed information to successfully edit the post object
2808 * @param {object} $site_row A jQuery object representing the current site that is currently worked on
2809 *
2810 * @return {void}
2811 */
2812 this.process_load_editor_response = function(editor, response, $site_row) {
2813 var editor_container = $(document.body);
2814 var $location = editor_container.find('div#'+editor+'_editor_container');
2815 var data = response.data;
2816 var preloaded_data = self.manage_data.item('preloaded_data');
2817 if ('undefined' !== typeof preloaded_data && preloaded_data) {
2818 preloaded_data = JSON.parse(preloaded_data);
2819 }
2820
2821 if ('undefined' == typeof $location || 0 == $location.length) {
2822 $location = $('<div/>', {
2823 id: editor+'_editor_container'
2824 });
2825
2826 $location.data('editor', editor);
2827 editor_container.append($location);
2828 }
2829
2830 var editor_wrapper = ('gutenberg' === editor) ? '<div class="'+editor+'-editor-post"></div>' : '<div id="'+editor+'_editor" class="'+editor+'"><div id="editor" class="'+editor+'__editor"></div></div>';
2831 if ($location.length && $location.is(':visible')) {
2832 $location.html(editor_wrapper);
2833 $location.css('min-height', screen.height);
2834 }
2835
2836 switch (editor) {
2837 case 'classic':
2838 var misc = $.extend(true, {}, data.misc);
2839 if (misc.hasOwnProperty('sample_permalink') && misc.sample_permalink.length) {
2840 if (-1 !== misc.sample_permalink[0].indexOf('%postname%')) {
2841 misc.site_url = misc.sample_permalink[0].replace('%postname%/', '');
2842 if ('undefined' !== typeof misc.sample_permalink[1] && misc.hasOwnProperty('slug') && 0 == misc.slug.trim().length) {
2843 misc.slug = misc.sample_permalink[1];
2844 }
2845 } else {
2846 if (-1 !== misc.sample_permalink[0].indexOf('?p=')) $('#edit-slug-buttons button.edit-slug').hide();
2847 }
2848 }
2849
2850 var hide_edit_button = false;
2851 if (misc.hasOwnProperty('link') && misc.link) {
2852 if (misc.hasOwnProperty('slug') && misc.slug && -1 !== misc.link.indexOf(misc.slug)) {
2853 misc.site_url = misc.link.replace(misc.slug, '');
2854 }
2855
2856 if (-1 !== misc.link.indexOf('page_id') || -1 !== misc.link.indexOf('post_id')) {
2857 misc.link = misc.link.replace(misc.slug, '');
2858 misc.site_url = misc.link;
2859 misc.slug = '';
2860 hide_edit_button = true;
2861 }
2862 }
2863
2864 var template = UpdraftCentral.template_replace('dashboard-classic_editor', {
2865 post: data.post,
2866 misc: misc,
2867 editor: data.editor,
2868 metaboxes: filter_metaboxes(data.metaboxes),
2869 logo: data.logo
2870 });
2871
2872 $location.find('#'+editor+'_editor').html(template);
2873 if (hide_edit_button) $('#edit-slug-buttons').hide();
2874
2875 var page_attributes_metabox_content = {
2876 order: data.post.menu_order
2877 }
2878
2879 if (self.manage_data.exists('template_options')) {
2880 page_attributes_metabox_content.template = render_options(self.manage_data.item('template_options'), {
2881 value: 'filename',
2882 label: 'template'
2883 });
2884 }
2885
2886 if (self.manage_data.exists('parent_options')) {
2887 page_attributes_metabox_content.page = render_options(self.manage_data.item('parent_options'), {
2888 value: 'id',
2889 label: 'title'
2890 }, null, [data.post.ID]);
2891 }
2892
2893 if ('post' == module.type) {
2894 var categories_inside_content = UpdraftCentral.template_replace('posts-categories', data.categories_metabox_content);
2895 $location.find('#categorydiv .inside').html(categories_inside_content);
2896
2897 var tags_inside_content = UpdraftCentral.template_replace('posts-tags', data.tags_metabox_content);
2898 $location.find('#tagsdiv-post_tag .inside').html(tags_inside_content);
2899 } else {
2900 var page_inside_content = UpdraftCentral.template_replace('pages-page-attributes', page_attributes_metabox_content);
2901 $location.find('#pageparentdiv .inside').html(page_inside_content);
2902 }
2903
2904 load_auxillary_functions(response, $location);
2905 $(document.body).trigger('updraftcentral_'+module.type+'_editor_loaded', [$location]);
2906 break;
2907 case 'gutenberg':
2908 window.qstring = 'site_id='+$site_row.data('site_id')+'&uc_nonce='+data.info.uc_nonce+'&uc_refIds='+data.info.uc_refIds+'&post_type='+data.post.post_type;
2909 var previous_status = data.post.post_status;
2910
2911 // Manually add a spinner to indicate that UC is actually loading the block/gutenberg editor,
2912 // since the editor will take a few seconds to load and there will be a gap between clicking
2913 // the load editor link to the actual loading of the editor.
2914 if ($.fullscreen.isFullScreen()) {
2915 $site_row.prepend('<div class="updraftcentral_spinner"></div>');
2916 } else {
2917 $(document.body).append('<div class="updraftcentral_spinner"></div>');
2918 }
2919
2920 self.clear_notices();
2921 var template = UpdraftCentral.template_replace('dashboard-block_editor', {
2922 title: data.post.post_title,
2923 metaboxes: data.metaboxes
2924 });
2925
2926 $location.find('.'+editor+'-editor-post').html(template);
2927
2928 var settings = data.settings;
2929 settings.autosave = null;
2930 settings.autosaveInterval = 86400;
2931 if ('page' == data.post.post_type && preloaded_data.hasOwnProperty('templates') && preloaded_data.templates) {
2932 settings.availableTemplates = preloaded_data.templates;
2933 }
2934
2935 var nux = wp.data.dispatch('core/nux');
2936 if ('undefined' !== typeof nux && nux) nux.disableTips();
2937
2938 var fonts_map = {
2939 'fonts': 'uc-remote-fonts-inline-css',
2940 'theme_fonts': 'uc-remote-theme-fonts-inline-css',
2941 }
2942
2943 for (var prop in fonts_map) {
2944 if (settings.hasOwnProperty(prop) && settings[prop]) {
2945 var font_style = $('head > style[id="'+fonts_map[prop]+'"]');
2946 if (0 == font_style.length) {
2947 font_style = $('<style/>', {
2948 id: fonts_map[prop]
2949 }).html(settings[prop].replace('CENTRAL_URL', udclion.home_url)).appendTo($('head'));
2950 } else {
2951 font_style.html(settings[prop].replace('CENTRAL_URL', udclion.home_url));
2952 }
2953 }
2954 }
2955
2956 if (settings.hasOwnProperty('editor_assets') && settings.editor_assets) {
2957 window.__editorAssets = JSON.parse(settings.editor_assets);
2958 }
2959
2960 if ('string' == typeof settings['defaultEditorStyles']) {
2961 settings['defaultEditorStyles'] = [{ css: settings['defaultEditorStyles'] }];
2962 }
2963
2964 window._wpLoadBlockEditor = new Promise(function(resolve, reject) {
2965 wp.domReady(function() {
2966 wp.apiFetch.use(function(options, next) {
2967 if (options.hasOwnProperty('path') && options.path && -1 === options.path.indexOf('uc_nonce')) {
2968 options.path += (-1 !== options.path.indexOf('?')) ? '&' : '?';
2969 options.path += window.qstring;
2970 }
2971 return next(options);
2972 });
2973
2974 var core = wp.data.select('core'),
2975 core_dispatch = wp.data.dispatch('core'),
2976 record = core.getEntityRecord('postType', 'post', data.post.ID),
2977 bypass = false,
2978 edits = null;
2979
2980 if ('undefined' !== typeof record && record) {
2981 // Other posts from a different site can have the same post ID so, we need to remove
2982 // any underlying reference to it otherwise, details from that other post will be
2983 // displayed instead of the actual information of the current post to be edited.
2984 var guid = record.guid;
2985 if ('object' === typeof guid && guid.hasOwnProperty('raw')) guid = guid.raw;
2986
2987 // Same post ID from a different server
2988 if (record.id == data.post.ID && 'undefined' !== typeof guid && guid != data.post.guid) {
2989 // Older WordPress version's block editor does not have the "deleteEntityRecord" method
2990 // thus, we do this check in order to make sure that the below code block will only run
2991 // on latest block editor's modules.
2992 //
2993 // N.B. It's probably best to advise users to use the latest WP version because older
2994 // WP version contains outdated (some deprecated or was heavily changed) version of
2995 // the block editor's modules.
2996 if ('function' === typeof core_dispatch.deleteEntityRecord) {
2997 core_dispatch.deleteEntityRecord('postType', 'post', data.post.ID).then(function() {
2998 wp.apiFetch({
2999 path: '/wp/v2/posts/'+data.post.ID+'?context=edit',
3000 method: 'GET'
3001 }).then(function(post) {
3002 // We are invalidating previous edits from other post with similar IDs (if there are any)
3003 // to load the content successfully and to give way to a fresh editing start.
3004 edits = core.getEntityRecordEdits('postType', 'post', post.id);
3005
3006 core_dispatch.receiveEntityRecords('postType', 'post', post, null, false, edits).then(function() {
3007 receiveAuthors(data, post, settings).then(function(response) {
3008 initiate_blocks_reset(response.data);
3009 resolve(wp.editPost.initializeEditor('editor', response.post.type, response.post.id, response.settings, {}));
3010 });
3011 });
3012 });
3013 });
3014 bypass = true;
3015 }
3016 }
3017 }
3018
3019 if (!bypass) {
3020 receiveAuthors(data, data.post, settings).then(function(response) {
3021 initiate_blocks_reset(response.data);
3022 resolve(wp.editPost.initializeEditor('editor', response.post.post_type, response.post.ID, response.settings, {}));
3023 });
3024 }
3025 });
3026
3027 }).then(function() {
3028 // We need to store the last information processed by the editor, just in case
3029 // something happened (e.g. the editor is broken/corrupted) then we will reset
3030 // the editor from the last information it processed.
3031 self.reset_info.data = data;
3032 self.reset_info.previous_status = previous_status;
3033 self.reset_info.location = $location;
3034
3035 self.apply_subscriptions_observer(data, previous_status, $location);
3036 });
3037 break;
3038 default:
3039 break;
3040 }
3041 }
3042
3043 /**
3044 * Retrieves a list of authors that was preloaded from the remote site
3045 *
3046 * @param {Object} data An object containing block categories and definitions.
3047 * @param {Object} post The post to edit
3048 * @param {Object} settings Editor settings
3049 *
3050 * @return {Object} A jQuery promise
3051 */
3052 function receiveAuthors(data, post, settings) {
3053 var deferred = $.Deferred();
3054
3055 var path = '/wp/v2/users/?who=authors&per_page=100';
3056 wp.apiFetch({
3057 path: path,
3058 method: 'GET'
3059 }).then(function(authors) {
3060 wp.data.dispatch('core').receiveUserQuery(path, authors).then(function(response) {
3061 wp.data.select('core').getUsers = function(args) {
3062 return response.users;
3063 };
3064 deferred.resolve({
3065 authors: response.users,
3066 data: data,
3067 post: post,
3068 settings: settings
3069 });
3070 });
3071 }).catch(function(error) {
3072 deferred.reject(error);
3073 });
3074
3075 return deferred.promise();
3076 }
3077
3078 /**
3079 * Renders the actual items and updates all the needed information in the UpdraftCentral UI
3080 *
3081 * @param {object} response The response object to process
3082 * @param {object} $site_row A jQuery object representing the current site that is currently worked on
3083 * @param {boolean} refresh Indicates whether to update the links and current selection
3084 *
3085 * @return {void}
3086 */
3087 function process_response(response, $site_row, refresh) {
3088 var $location = $site_row.find('.updraftcentral_row_extracontents');
3089 if ('undefined' !== typeof response[module.type+'s'] && response[module.type+'s']) {
3090 if (0 === $location.find('.uc-navlinks-container').length || ('undefined' !== typeof refresh && refresh)) {
3091 var params = {
3092 date_filter: response.options.date
3093 };
3094
3095 if ('post' == module.type) params.category_filter = response.options.category;
3096 var filter_template = UpdraftCentral.template_replace(module.type+'s-manage-filters', params);
3097 var item_template = UpdraftCentral.template_replace(module.type+'s-items', {});
3098
3099 $location.html(filter_template + item_template);
3100 $location.find('input.uc-'+module.type+'-check-all').on('click', function(e) {
3101 select_items_for_processing($(this), $site_row);
3102 });
3103 }
3104
3105 if ('undefined' !== typeof refresh && refresh) {
3106 self.current_group = 'all';
3107 update_action_options(self.current_group);
3108 $('ul#uc-navlinks a.uc-navlink-item').css('cssText', 'font-weight: normal;');
3109 $('ul#uc-navlinks a.uc-navlink-item[data-group="'+self.current_group+'"]').css('cssText', 'font-weight: bold !important;');
3110 }
3111
3112 // Set remote data
3113 self.pagination.set_remote_data({
3114 info: response.info,
3115 items: response[module.type+'s']
3116 });
3117
3118 // Returns the currently rendered items and the pagination
3119 render_items_and_pagination(1, $site_row);
3120 update_item_count(response[module.type+'s_count']);
3121
3122 UpdraftCentral_Library.enable_actions();
3123 }
3124 }
3125
3126 /**
3127 * Toggle the enabled properties of the link and style elements based on the submitted value
3128 *
3129 * @param {boolean} value "true" to disable, "false" otherwise
3130 *
3131 * @return {void}
3132 */
3133 this.disable_current_themes = function(value) {
3134 if (('page' == module.type && 'undefined' !== typeof udcstyles_page && udcstyles_page.hasOwnProperty('styles')) || ('post' == module.type && 'undefined' !== typeof udcstyles && udcstyles.hasOwnProperty('styles'))) {
3135 var styles = ('post' == module.type) ? udcstyles.styles : udcstyles_page.styles;
3136
3137 // Disable local styles temporarily while editing as not to override original styles from controlled sites
3138 for (var i=0; i<styles.length; i++) {
3139 if (-1 === $.inArray(styles[i].id, ['updraftcentral-dashboard-css', 'media-views', 'mediaelement', 'imgareaselect', 'buttons', 'editor-buttons'])) {
3140 if ($('link[id="'+styles[i].id+'-css"').length) $('link[id="'+styles[i].id+'-css"').prop('disabled', value);
3141 if ($('style[id="'+styles[i].id+'-inline-css"').length) $('style[id="'+styles[i].id+'-inline-css"').prop('disabled', value);
3142 }
3143 }
3144
3145 var admin_css = ['common-css', 'forms-css', 'media-css'];
3146 for (var i=0; i<admin_css.length; i++) {
3147 admin_css_item = $('link[id="'+admin_css[i]+'"]');
3148 if ('undefined' !== typeof admin_css_item && admin_css_item.length) {
3149 admin_css_item.prop('disabled', value);
3150 }
3151 }
3152
3153 // Make sure that any local default editor (classic) styles won't interfere with the block editor
3154 // when rendering content for editing. For classic editor this is already embedded, so ne need to
3155 // re-use these.
3156 $('link[id="wp-editor-css"]').prop('disabled', value);
3157 $('link[id="wp-editor-inline-css"]').prop('disabled', value);
3158
3159 // We put boostrap style on-hold as well (if we found one)
3160 $('link[id^="bootstrap"]').prop('disabled', value);
3161 $('style[id^="boostrap"]').prop('disabled', value);
3162
3163 // Make sure local reset.css does not interfere and reset remote styles before we can
3164 // actually used them
3165 $('link[id="wp-reset-editor-styles-css"]').prop('disabled', value);
3166 }
3167 }
3168
3169 /**
3170 * Temporarily disables the current theme's style when editing the post in order not to override
3171 * the remote styling when the user is currently editing the post.
3172 *
3173 * @param {boolean} value "true" to disable, "false" otherwise
3174 *
3175 * @return {void}
3176 */
3177 this.disable_current_wp_theme = function(value) {
3178 if ($.fullscreen.isFullScreen()) {
3179 var theme_css;
3180 if ('post' == module.type) {
3181 if (udcstyles.hasOwnProperty('current_theme_uri') && udcstyles.current_theme_uri) {
3182 theme_css = $('link[href^="'+udcstyles.current_theme_uri+'"]');
3183 }
3184 } else {
3185 if (udcstyles_page.hasOwnProperty('current_theme_uri') && udcstyles_page.current_theme_uri) {
3186 theme_css = $('link[href^="'+udcstyles_page.current_theme_uri+'"]');
3187 }
3188 }
3189
3190 if ('undefined' !== typeof theme_css && theme_css.length) {
3191 theme_css.prop('disabled', value);
3192 }
3193 }
3194 }
3195
3196 /**
3197 * Remove gutenberg edit button if the controlled site's WP version is below 5
3198 *
3199 * @param {object} $site_row A jQuery object representing the current site that is currently worked on
3200 *
3201 * @return {void}
3202 */
3203 function maybe_remove_block_editing($site_row) {
3204 var $location = $site_row.find('.updraftcentral_row_extracontents'),
3205 site_id = $site_row.data('site_id'),
3206 site_wp_version;
3207
3208 if (self.wp_versions.exists(site_id)) {
3209 site_wp_version = self.wp_versions.item(site_id);
3210
3211 if (parseInt(site_wp_version) < 5) {
3212 $location.find('a.'+module.type+'-action-item[data-action="edit-gutenberg"]').remove();
3213 }
3214 } else {
3215 $location.find('a.'+module.type+'-action-item[data-action="edit-gutenberg"]').remove();
3216 }
3217 }
3218
3219 /**
3220 * Sends request to the remote website to update the posts table based from the
3221 * submmitted parameters/filters
3222 *
3223 * @param {object} $site_row A jQuery object representing the current site that is currently worked on
3224 * @param {int} page The page number to display
3225 * @param {string} status All posts having this status
3226 * @param {string} keyword Posts matching this keyword
3227 * @param {string} date Posts published within this month year period (e.g. JANUARY 2019)
3228 * @param {boolean} refresh Indicates whether to update the links and current selection
3229 * @param {boolean} preload Include preloaded information in the response
3230 *
3231 * @return {void}
3232 */
3233 function render_post_items($site_row, page, status, keyword, date, category, refresh, preload) {
3234 var param = {
3235 name: 'get',
3236 arguments: {
3237 numberposts: numberposts,
3238 paged: ('undefined' !== typeof page && page) ? page : 1,
3239 status: ('undefined' !== typeof status && status) ? status : 'all',
3240 keyword: ('undefined' !== typeof keyword && keyword) ? keyword : '',
3241 date: ('undefined' !== typeof date && date) ? date : '',
3242 timeout: ('undefined' !== typeof udclion.user_defined_timeout && udclion.user_defined_timeout) ? udclion.user_defined_timeout : 30,
3243 }
3244 }
3245
3246 if ('post' == module.type) {
3247 param.arguments.category = ('undefined' !== typeof category && category) ? category : '';
3248 }
3249
3250 if ('undefined' !== typeof preload && preload) {
3251 param.arguments.preload = 1;
3252 }
3253
3254 UpdraftCentral_Library.disable_actions();
3255 send_command(param, $site_row).then(function(response) {
3256 if ('undefined' !== typeof status && status) {
3257 $('ul#uc-navlinks a.uc-navlink-item[data-group="'+status+'"]').css('cssText', 'font-weight: bold !important;');
3258 }
3259
3260 // Register response for later access before proceeding in processing it.
3261 if (response.hasOwnProperty(module.type+'s') && response[module.type+'s']) {
3262 self.manage_data.update('response', response);
3263
3264 // Preloaded data are only requested once to avoid a long process of pulling
3265 // those time consuming information retrieval from the remote website.
3266 //
3267 // N.B. The "preloaded" property doesn't always return for every response that is
3268 // why we're checking it here. If we have it, then we store it. It will only be
3269 // requested when the "preload" parameter is set (when pressing the "Manage" button).
3270 if (response.hasOwnProperty('preloaded') && response.preloaded) {
3271 self.manage_data.update('preloaded_data', response.preloaded);
3272
3273 // Save pulled "wp version" for the current site for quick access later on
3274 var data = JSON.parse(response.preloaded),
3275 site_id = $site_row.data('site_id');
3276
3277 if (data.hasOwnProperty('wp_version') && data.wp_version) {
3278 if (!self.wp_versions.exists(site_id)) {
3279 self.wp_versions.add(site_id, data.wp_version);
3280 }
3281 }
3282 }
3283
3284 if (response.hasOwnProperty('options') && response.options) {
3285 if ('page' == module.type) {
3286 if (!self.manage_data.exists('parent_options') && response.options.hasOwnProperty('page')) self.manage_data.add('parent_options', response.options.page);
3287 }
3288 if (!self.manage_data.exists('template_options') && response.options.hasOwnProperty('template')) self.manage_data.add('template_options', response.options.template);
3289 }
3290 }
3291
3292 refresh = ('undefined' !== typeof refresh) ? refresh : false;
3293 process_response(response, $site_row, refresh);
3294 });
3295 }
3296
3297 /**
3298 * A simple validation function that validates some quick edit input fields
3299 * for non-empty value with some numeric condition
3300 *
3301 * @param {int} id The ID of the currently selected post
3302 * @param {object} item A jQuery object representing the post item
3303 * @param {string} field The name of the field to check
3304 * @param {boolean} numeric_check Indicates whether to check for numeric as well
3305 * @param {boolean} general_edit Indicates whether this is for general editing and not for quick edit area
3306 *
3307 * @return {boolean}
3308 */
3309 function validate_quick_edit(id, item, field, numeric_check, general_edit) {
3310 var proceed = true;
3311 var value = item.val();
3312
3313 if ('undefined' !== typeof numeric_check && numeric_check) {
3314 if (!UpdraftCentral_Library.is_numeric(value)) proceed = false;
3315 }
3316
3317 if (0 == value.trim().length || !proceed) {
3318 self.dirty_edits.update(field, 1);
3319 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+sprintf(udclion[module.type+'s'].invalid_missing_value, field)+'</p>', function() {
3320 setTimeout(function() {
3321 item.trigger('focus');
3322 }, 500);
3323 });
3324 return false;
3325 } else {
3326 self.dirty_edits.remove(field);
3327 if ('undefined' !== typeof general_edit && general_edit) {
3328 self.uc_editor.edits.update(field, value);
3329 } else {
3330 self.quick_edits.update(field, value);
3331 }
3332 }
3333
3334 return true;
3335 }
3336
3337 /**
3338 * Sets or loads listeners for the quick edit form events
3339 *
3340 * @param {object} $form A jQuery object representing the quick edit form
3341 *
3342 * @return {void}
3343 */
3344 function load_quick_edit_listeners($form) {
3345 if ('undefined' !== typeof $form && $form) {
3346 var id = $form.data('id');
3347 $form.find('.uc-'+module.type+'-title input[name="post_title"]').off('change').on('change', function() {
3348 validate_quick_edit(id, $(this), 'title');
3349 });
3350
3351 $form.find('.uc-'+module.type+'-slug input[name="post_name"]').off('change').on('change', function() {
3352 validate_quick_edit(id, $(this), 'slug');
3353 });
3354
3355 $form.find('.uc-'+module.type+'-date select[name="mm"]').off('change').on('change', function() {
3356 self.quick_edits.update('mm', $(this).val());
3357 });
3358
3359 $form.find('.uc-'+module.type+'-date input[name="jj"]').off('change').on('change', function() {
3360 validate_quick_edit(id, $(this), 'jj', true);
3361 });
3362
3363 $form.find('.uc-'+module.type+'-date input[name="aa"]').off('change').on('change', function() {
3364 validate_quick_edit(id, $(this), 'aa', true);
3365 });
3366
3367 $form.find('.uc-'+module.type+'-date input[name="hh"]').off('change').on('change', function() {
3368 validate_quick_edit(id, $(this), 'hh', true);
3369 });
3370
3371 $form.find('.uc-'+module.type+'-date input[name="mn"]').off('change').on('change', function() {
3372 validate_quick_edit(id, $(this), 'mn', true);
3373 });
3374
3375 $form.find('.uc-'+module.type+'-author select[name="post_author"]').off('change').on('change', function() {
3376 self.quick_edits.update('author', $(this).val());
3377 });
3378
3379 $form.find('.uc-'+module.type+'-password input[name="post_password"]').off('change').on('change', function() {
3380 var result = validate_quick_edit(id, $(this), 'password');
3381 if (result) {
3382 self.quick_edits.update('visibility', 'password');
3383 } else {
3384 // Check if private is not checked, thus, we have a "public" visibility if
3385 // both "password" and "private" options are both empty.
3386 if (!$('.uc-'+module.type+'-password input[name="keep_private"]').is(':checked')) {
3387 self.quick_edits.update('visibility', 'public');
3388 }
3389 }
3390 });
3391
3392 $form.find('.uc-'+module.type+'-password input[name="keep_private"]').off('click').on('click', function() {
3393 if ($(this).is(':checked')) {
3394 self.quick_edits.update('visibility', 'private');
3395 } else {
3396 // Check if password is empty, thus, we have a "public" visibility if
3397 // both "password" and "private" options are both empty.
3398 if (0 == $('.uc-'+module.type+'-password input[name="post_password"]').val().length) {
3399 self.quick_edits.update('visibility', 'public');
3400 }
3401 }
3402 });
3403
3404 $form.find('.uc-'+module.type+'-parent select[name="post_parent"]').off('change').on('change', function() {
3405 self.quick_edits.update('parent', $(this).val());
3406 });
3407
3408 $form.find('.uc-'+module.type+'-order input[name="post_order"]').off('change').on('change', function() {
3409 validate_quick_edit(id, $(this), 'order', true);
3410 });
3411
3412 $form.find('.uc-'+module.type+'-order input[name="menu_order"]').off('change').on('change', function() {
3413 validate_quick_edit(id, $(this), 'order', true);
3414 });
3415
3416 $form.find('.uc-'+module.type+'-template select[name="post_template"]').off('change').on('change', function() {
3417 self.quick_edits.update('template', $(this).val());
3418 });
3419
3420 $form.find('.uc-'+module.type+'-comment-ping input[name="comment_status"]').off('click').on('click', function() {
3421 var comment_status = $(this).is(':checked') ? 'open' : 'closed';
3422 self.quick_edits.update('comment_status', comment_status);
3423 });
3424
3425 $form.find('.uc-'+module.type+'-comment-ping input[name="ping_status"]').off('click').on('click', function() {
3426 var ping_status = $(this).is(':checked') ? 'open' : 'closed';
3427 self.quick_edits.update('ping_status', ping_status);
3428 });
3429
3430 $form.find('.uc-'+module.type+'-status select[name="status"]').off('change').on('change', function() {
3431 self.quick_edits.update('status', $(this).val());
3432 });
3433
3434 $form.find('.uc-post-sticky input[name="sticky"]').off('click').on('click', function() {
3435 var sticky = $(this).is(':checked') ? 1 : 0;
3436 self.quick_edits.update('sticky', sticky);
3437 });
3438
3439 $form.find('ul#category-checklist input[type="checkbox"]').off('click').on('click', function() {
3440 var categories = [];
3441 $form.find('ul#category-checklist input[type="checkbox"]:checked').each(function() {
3442 categories.push($(this).val());
3443 });
3444
3445 self.quick_edits.update('categories', categories);
3446 });
3447
3448 $form.find('textarea#post_tag').off('change').on('change', function() {
3449 if ($(this).val().length) {
3450 self.quick_edits.update('tags', $(this).val().split(','));
3451 }
3452 });
3453 }
3454 }
3455
3456 /**
3457 * Updates the action available as bulk options when navigating from a group
3458 * of items (e.g. 'draft', 'published', 'pending', etc.)
3459 *
3460 * @param {string} group The currently selected group
3461 *
3462 * @return {void}
3463 */
3464 function update_action_options(group) {
3465 var post_action = $('.uc-'+module.type+'-buttons-filters select.uc-'+module.type+'-action');
3466 switch (group) {
3467 case 'publish':
3468 post_action.find('option[value="publish"], option[value="restore"], option[value="delete"]').hide();
3469 post_action.find('option[value="draft"], option[value="trash"]').show();
3470 break;
3471 case 'private':
3472 case 'draft':
3473 case 'pending':
3474 post_action.find('option[value="draft"], option[value="restore"], option[value="delete"]').hide();
3475 post_action.find('option[value="trash"], option[value="publish"]').show();
3476 break;
3477 case 'trash':
3478 post_action.find('option[value="draft"], option[value="trash"], option[value="publish"]').hide();
3479 post_action.find('option[value="restore"], option[value="delete"]').show();
3480 break;
3481 default:
3482 post_action.find('option[value="draft"], option[value="publish"], option[value="restore"], option[value="delete"]').hide();
3483 post_action.find('option[value="trash"]').show();
3484 break;
3485 }
3486 }
3487
3488 /**
3489 * Changes the state or status of the given post/page, including deletion
3490 *
3491 * @param {int} id The ID of the current post
3492 * @param {string} action The type of action that needs to be applied to the current post
3493 * @param {object} $site_row A jQuery object representing the current site that is currently worked on
3494 *
3495 * @return {void}
3496 */
3497 function set_state(id, action, $site_row) {
3498 if ('undefined' !== typeof action && action) {
3499 var param = {
3500 name: 'set_state',
3501 arguments: {
3502 id: id,
3503 action: action,
3504 paged: 1,
3505 status: 'all',
3506 }
3507 };
3508 param.arguments['number'+module.type+'s'] = numberposts;
3509
3510 send_command(param, $site_row).then(function(response) {
3511 if ('undefined' !== typeof response[module.type]) {
3512 // Update row and loaded information.
3513 if (response.hasOwnProperty('get') && response['get']) {
3514 self.manage_data.update('response', response['get']);
3515 process_response(response['get'], $site_row, true);
3516 }
3517
3518 var action_label = $('.uc-'+module.type+'-buttons-filters select.uc-'+module.type+'-action > option[value="'+action+'"]').text();
3519 UpdraftCentral_Library.dialog.alert('<h2>'+udclion[module.type+'s'].post_update_heading+'</h2><p>'+udclion[module.type+'s'].action_messages[action]+'</p>');
3520 } else {
3521 UpdraftCentral_Library.dialog.alert('<h2>'+udclion[module.type+'s'].post_update_heading+'</h2><p>'+udclion[module.type+'s'].unkown_error+'</p>');
3522 }
3523 });
3524 }
3525 }
3526
3527 /**
3528 * Sends command to the remote server
3529 *
3530 * @param {Object} params An object containing details of the command to execute.
3531 * @param {Object} $site_row The jQuery object representing the current site selected.
3532 *
3533 * @return {Object} A jQuery promise
3534 */
3535 function send_command(params, $site_row) {
3536 var deferred = $.Deferred();
3537
3538 UpdraftCentral.send_site_rpc(module.type+'s.'+params.name, params.arguments, $site_row, function(response, code, error_code) {
3539 // Since we've already received a response then we're going to terminate the processing flag
3540 // as there are some non-ajax based buttons that we need to trigger right after for user convenience.
3541 UpdraftCentral.ajax_request_processing = false;
3542
3543 if (code === 'ok' && 'undefined' !== typeof response.data && null !== response.data && !response.data.error) {
3544 deferred.resolve(response.data);
3545 } else {
3546 if ('undefined' !== typeof response.data && response.data && 'undefined' !== typeof response.data.error) {
3547
3548 var message = '';
3549 if ('undefined' !== typeof udclion.plugin[response.data.message]) {
3550 message = udclion.plugin[response.data.message];
3551 if ('undefined' !== typeof response.data.values && Array.isArray(response.data.values)) {
3552 message = vsprintf(message, response.data.values);
3553 }
3554 } else {
3555 // Check from the global translation
3556 if ('undefined' !== typeof udclion[response.data.message]) {
3557 message = udclion[response.data.message];
3558 if ('undefined' !== typeof response.data.values && Array.isArray(response.data.values)) {
3559 message = vsprintf(message, response.data.values);
3560 }
3561 } else {
3562 var error_message = response.data.message;
3563 if ('undefined' !== typeof response.data.messages && response.data.messages.length) error_message = response.data.messages[0];
3564
3565 message = sprintf(udclion.plugin.general_error, error_message);
3566 }
3567 }
3568
3569 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.error+'</h2><p>'+message+'</p>');
3570 }
3571
3572 deferred.reject(response);
3573 }
3574 }, $site_row, 90);
3575
3576 return deferred.promise();
3577 }
3578
3579 /**
3580 * Renders raw options into a HTML <option> string before passing into the template
3581 *
3582 * @param {array} data Options array that contains data to render
3583 * @param {object} fields Fields to render
3584 * @param {string} type Optional. The type of rendering needed (e.g. 'categories', 'tags')
3585 * @param {array} exclude Do not include as options
3586 *
3587 * @return {string}
3588 */
3589 function render_options(data, fields, type, exclude) {
3590 var options = '';
3591 if ('undefined' !== typeof data && data) {
3592 for (var i=0, info; i<data.length; i++) {
3593 info = data[i];
3594 if ('undefined' !== typeof fields && fields) {
3595 if ('undefined' !== typeof exclude && Array.isArray(exclude) && exclude.length && -1 !== $.inArray(info[fields.value], exclude)) {
3596 continue;
3597 }
3598 if ('undefined' !== typeof type && type) {
3599 switch (type) {
3600 case 'categories':
3601 options += '<li id="category-'+info[fields.value]+'"><label><input id="in-category-'+info[fields.value]+'" type="checkbox" name="post_category[]" value="'+info[fields.value]+'" /> '+info[fields.label]+'</label></li>';
3602 break;
3603 case 'tags':
3604 options += (0 == i) ? info[fields.label] : ', ' + info[fields.label];
3605 break;
3606 }
3607 } else {
3608 options += '<option value="'+info[fields.value]+'">'+info[fields.label]+'</option>';
3609 }
3610 }
3611 }
3612 }
3613
3614 return options;
3615 }
3616
3617 /**
3618 * Pulls the paginated data and renders the pagination system based from the given post
3619 *
3620 * @param {integer} post An optional post number to pull the items from
3621 * @param {Object} $site_row The jQuery object representing the current site selected.
3622 *
3623 * @return {void}
3624 */
3625 function render_items_and_pagination(page, $site_row) {
3626 var data = self.pagination.get_data(page);
3627 var $location = $site_row.find('.updraftcentral_row_extracontents');
3628 var response = self.manage_data.item('response');
3629
3630 // Render options - convenience of just embedding it inside the template
3631 // rather than running and rendering it individually for each item.
3632 var options = {
3633 template: render_options(response.options.template, {
3634 value: 'filename',
3635 label: 'template'
3636 }),
3637 page: render_options(response.options.page, {
3638 value: 'id',
3639 label: 'title'
3640 }),
3641 author: render_options(response.options.author, {
3642 value: 'id',
3643 label: 'name'
3644 })
3645 }
3646
3647 if ('post' == module.type) {
3648 options.category = render_options(response.options.category, {
3649 value: 'id',
3650 label: 'name'
3651 }, 'categories');
3652 options.tag = render_options(response.options.tag, {
3653 value: 'id',
3654 label: 'name'
3655 }, 'tags');
3656 }
3657
3658 var params = {
3659 options: options,
3660 posts: data.items,
3661 };
3662
3663 var content = UpdraftCentral.template_replace(module.type+'s-items', params);
3664 $location.find('.uc-'+module.type+'-items-container').html(content);
3665 post_load_housekeeping($site_row);
3666 self.pagination.render();
3667 maybe_remove_block_editing($site_row);
3668 }
3669
3670 /**
3671 * Adds alternate colors for items and limit categories and tags display
3672 *
3673 * @param {Object} $site_row The jQuery object representing the current site selected.
3674 *
3675 * @return {void}
3676 */
3677 function post_load_housekeeping($site_row) {
3678 var $location = $site_row.find('.updraftcentral_row_extracontents');
3679 $location.find('.row.uc-'+module.type+'-item:even').addClass('uc-'+module.type+'-item-even');
3680 $location.find('.row.uc-'+module.type+'-item:odd').addClass('uc-'+module.type+'-item-odd');
3681
3682 if ('post' == module.type) {
3683 var fields = ['.post-item-categories', '.post-item-tags'];
3684 for (var i=0; i<fields.length; i++) {
3685 $('.uc-post-item '+fields[i]).each(function() {
3686 var list = '', content, items = [];
3687
3688 content = $(this).html().trim();
3689 if (content.length && -1 !== content.indexOf(',')) {
3690 items = content.split(',');
3691 if (items.length > 5) {
3692 for (var ii=0; ii<5; ii++) list += (0 == ii) ? items[ii] : ', '+items[ii];
3693 list += '...';
3694 }
3695
3696 if (list.length) $(this).html(list);
3697 }
3698 });
3699 }
3700 }
3701 }
3702
3703 /**
3704 * Toggles the bulk selection of the plugin items
3705 *
3706 * @param {object} check_all A jquery object representing the "select all" checkbox element
3707 * @param {Object} $site_row The jQuery object representing the current site selected.
3708 *
3709 * @return {void}
3710 */
3711 function select_items_for_processing(check_all, $site_row) {
3712 var item_container = $site_row.find('.uc-'+module.type+'-items-container .uc-'+module.type+'-item');
3713 if (check_all.is(':checked') && item_container.length) {
3714 item_container.find('input[name="post\[\]"]').prop('checked', true);
3715 } else {
3716 item_container.find('input[name="post\[\]"]').prop('checked', false);
3717 }
3718 }
3719
3720 /**
3721 * Updates navigation links count
3722 *
3723 * @return {void}
3724 */
3725 function update_item_count(posts_count) {
3726
3727 if ('undefined' !== typeof posts_count && posts_count) {
3728 var parent = $('div.uc-navlinks-container ul#uc-navlinks');
3729
3730 parent.find('li.all span.count').html('('+posts_count.all+')');
3731 parent.find('li.publish span.count').html('('+posts_count.publish+')');
3732 parent.find('li.private span.count').html('('+posts_count.private+')');
3733 parent.find('li.draft span.count').html('('+posts_count.draft+')');
3734 parent.find('li.pending span.count').html('('+posts_count.pending+')');
3735 parent.find('li.future span.count').html('('+posts_count.future+')');
3736 parent.find('li.trash span.count').html('('+posts_count.trash+')');
3737 }
3738 }
3739 }
3740
3741 /**
3742 * Pagination Class
3743 *
3744 * A pagination renderer for both remote and preloaded items
3745 *
3746 * N.B. preloaded items are simply results from a previous remote website request and
3747 * stored for fast management of items without the need to constantly pull items from
3748 * the remote website everytime an action is triggered.
3749 *
3750 * @example
3751 * Parameter "config" example value:
3752 * config = {
3753 * container: ,
3754 * items_per_page: ,
3755 * type: local | remote,
3756 * callback: function(page) {
3757 * // The current page where the rendered items are pulled in the collection
3758 * },
3759 * }
3760 *
3761 * @constructor
3762 */
3763 function UpdraftCentral_Pagination(config) {
3764 var self = this;
3765 var $ = jQuery;
3766 var config = $.extend({}, config);
3767 var preloaded_items = new UpdraftCentral_Collection();
3768 var filtered_items = new UpdraftCentral_Collection();
3769 var remote_data;
3770 var current_data;
3771
3772 /**
3773 * Calculates the pagination info (pages, etc.) and pulls the paged items based
3774 * from the current page submitted
3775 *
3776 * @private
3777 * @param {integer} page The current page where the items is to be pulled from
3778 *
3779 * @returns {object}
3780 */
3781 var get_results_info = function(page) {
3782 var data = { info: {}, items: [] };
3783 var items = (filtered_items.count()) ? filtered_items : preloaded_items;
3784 var items_per_page = (config.hasOwnProperty('items_per_page')) ? config.items_per_page : 10;
3785
3786 if (items.count()) {
3787 page = ('undefined' !== typeof page) ? page : 1;
3788
3789 // Computes the elements of the pagination interface based from the preset items information
3790 var total_count = items.count();
3791 var extra_page = parseInt(total_count % items_per_page) > 0 ? 1 : 0;
3792 var pages = parseInt(total_count / items_per_page) + extra_page;
3793 var index = (page * items_per_page) - items_per_page;
3794
3795 var keys = items.keys();
3796 var paged_keys = keys.splice(index, items_per_page);
3797
3798 var items_from = index + 1;
3799 var items_to = (page === pages) ? total_count : page * items_per_page;
3800
3801 // Pulls the items based from the current page submitted
3802 var results = [];
3803 for (var i=0; i<paged_keys.length; i++) {
3804 var item = items.item(paged_keys[i]);
3805 results.push(item);
3806 }
3807
3808 // Wraps result in this structure before returning
3809 data = {
3810 info: {
3811 page: page,
3812 pages: pages,
3813 results: total_count,
3814 items_from: items_from,
3815 items_to: items_to
3816 },
3817 items: results
3818 }
3819 }
3820
3821 return data;
3822 }
3823
3824 /**
3825 * Calculates the items boundary if not defined
3826 *
3827 * @private
3828 * @param {object} info Contains the page information to render
3829 *
3830 * @returns {object}
3831 */
3832 var maybe_calculate_bounds = function(info) {
3833 // For remote type, these "items_from" and "items_to" does not exists
3834 // so, we're building it based on the above info
3835 if ('undefined' === typeof info.items_from) {
3836 var page = parseInt(info.page);
3837 var pages = parseInt(info.pages);
3838 var results = parseInt(info.results);
3839 var extra_item = parseInt(results % pages) > 0 ? 1 : 0;
3840 var limit = parseInt(results / pages) + extra_item;
3841 var index = (page * limit) - limit;
3842
3843 info.items_from = index + 1;
3844 info.items_to = (page === pages) ? results : page * limit;
3845 }
3846
3847 return info;
3848 }
3849
3850 /**
3851 * Builds the pagination interface
3852 *
3853 * @private
3854 * @param {object} info Contains the page information to render
3855 *
3856 * @returns {string}
3857 */
3858 var build_interface = function(info) {
3859 if ('undefined' === typeof info) {
3860 console.log('Error: uc-library.js:UpdraftCentral_Pagination:build_interface - unable to construct pagination interface because the required info object is not defined. Possible cause, the module\'s handler in UpdraftPlus is outdated.');
3861 return;
3862 }
3863
3864 info = maybe_calculate_bounds(info);
3865
3866 var page = parseInt(info.page);
3867 var pages = parseInt(info.pages);
3868 var results = parseInt(info.results);
3869 var items_from = parseInt(info.items_from);
3870 var items_to = parseInt(info.items_to);
3871
3872 var loaded = false;
3873 if (config.hasOwnProperty('type') && 'local' === config.type) {
3874 loaded = true;
3875 }
3876
3877 var pagination = '';
3878 if (1 !== page) {
3879 pagination += '<a href="#" data-page="1" data-preloaded="'+loaded+'" class="uc-pagination-first-button">'+udclion.first+'</a>';
3880 pagination += ' <span class="uc-pagination-separator">|</span> ';
3881 }
3882 if (page > 1) pagination += '<a href="#" data-page="'+(page-1)+'" data-preloaded="'+loaded+'" class="uc-pagination-previous-button">'+udclion.previous+'</a>';
3883 if (page > 1 && page < pages) pagination += ' <span class="uc-pagination-separator">|</span> ';
3884 if (page < pages) pagination += '<a href="#" data-page="'+(page+1)+'" data-preloaded="'+loaded+'" class="uc-pagination-next-button">'+udclion.next+'</a>';
3885 if (pages !== page) {
3886 pagination += ' <span class="uc-pagination-separator">|</span> ';
3887 pagination += '<a href="#" data-page="'+pages+'" data-preloaded="'+loaded+'" class="uc-pagination-last-button">'+udclion.last+'</a>';
3888 }
3889 pagination += '<div class="uc-pagination-results-info">'+sprintf(udclion.page_of, page, pages)+', <span class="uc-pagination-item-bounds">'+sprintf(udclion.total_items, items_from, items_to, results)+'</span></div>';
3890
3891 return pagination;
3892 }
3893
3894 /**
3895 * Preloads an item for fast navigation
3896 *
3897 * @param array items An array of objects representing the items from a queried results.
3898 * Object must need at least two properties "name" and "website". These
3899 * makes them unique across multiple websites.
3900 *
3901 * @example
3902 * Parameter "items" example value:
3903 * items = [
3904 * { name: '', website: '', ... },
3905 * { name: '', website: '', ... },
3906 * { name: '', website: '', ... },
3907 * ]
3908 */
3909 this.preload_items = function(items) {
3910 if (preloaded_items.count()) preloaded_items.clear();
3911 if ('undefined' !== typeof items && items) {
3912 for (var i=0; i<items.length; i++) {
3913 var item = items[i];
3914
3915 if (item.hasOwnProperty('name') && item.hasOwnProperty('website')) {
3916 var key = item.name + '_' + item.website;
3917 preloaded_items.add(key, item);
3918 }
3919 }
3920 }
3921 }
3922
3923 /**
3924 * Set filtered items to factor in when rendering the pagination interface
3925 *
3926 * @param {array} items A collection of filtered items
3927 *
3928 * @returns {void}
3929 */
3930 this.set_filtered_items = function(items) {
3931 filtered_items = items;
3932 }
3933
3934 /**
3935 * Set remote data from the results of a remote website query
3936 *
3937 * @param {array} items A collection of items from the remote query
3938 *
3939 * @returns {void}
3940 */
3941 this.set_remote_data = function(items) {
3942 remote_data = items;
3943 }
3944
3945 /**
3946 * Extract the paged data/items
3947 *
3948 * @param {integer} page The current page where the items is to be pulled from
3949 *
3950 * @returns {object}
3951 */
3952 this.get_data = function(page) {
3953 current_data = (config.hasOwnProperty('type') && 'local' === config.type) ? get_results_info(page) : remote_data;
3954 return current_data;
3955 }
3956
3957 /**
3958 * Renders the pagination interface based from the pre-set data and the current page
3959 *
3960 * @param {integer} page The current page where the items is to be pulled from within the collection
3961 *
3962 * @returns {void}
3963 */
3964 this.render = function(page) {
3965 if ('undefined' === typeof current_data || !current_data || 'undefined' !== typeof page) self.get_data(page);
3966 if ('undefined' !== typeof current_data && current_data) {
3967 var $ui = build_interface(current_data.info);
3968
3969 if (config.hasOwnProperty('container') && config.container) {
3970 $(config.container).html($ui);
3971 $(config.container).find('a.uc-pagination-first-button, a.uc-pagination-previous-button, a.uc-pagination-next-button, a.uc-pagination-last-button').off('click').on('click', function() {
3972 var page = $(this).data('page');
3973
3974 if (config.hasOwnProperty('callback') && config.callback) {
3975 config.callback.apply(null, [page]);
3976 }
3977 });
3978 }
3979 }
3980 }
3981 }
3982
3983 /**
3984 * Site Filter Class
3985 *
3986 * A widget build to allow users to select multiple websites to apply any actions
3987 * selected or triggered by the user
3988 *
3989 * @example
3990 * Parameter "config" sample.
3991 * config = {
3992 * container: ,
3993 * extra_classes: ,
3994 * label: ,
3995 * placeholder: ,
3996 * buttons: [
3997 * {
3998 * id: ,
3999 * name: ,
4000 * class:,
4001 * callback: function() {
4002 *
4003 * }
4004 * }
4005 * ]
4006 * }
4007 *
4008 * @constructor
4009 */
4010 function UpdraftCentral_Site_Filter(config) {
4011 var self = this;
4012 var $ = jQuery;
4013 var sites = new UpdraftCentral_Collection();
4014 var options = [];
4015 var config = $.extend({}, config);
4016 var $site_row = (config.hasOwnProperty('default_site') && config.default_site) ? config.default_site : null;
4017 var prefix = 'uc_site_filter_';
4018 var group_options = new UpdraftCentral_Collection();
4019
4020 /**
4021 * Gets all available websites and wrap it up as filter options when users
4022 * apply certain actions
4023 *
4024 * @private
4025 * @returns {array}
4026 */
4027 var get_site_options = function() {
4028 var site_options = [];
4029 var all_options = [];
4030
4031 // Adding all websites option
4032 site_options.push({
4033 id: 0,
4034 text: udclion.add_all_sites
4035 });
4036
4037 // Insert site_tagged options if available
4038 site_options = maybe_add_site_tagged_options(site_options);
4039
4040 $('.updraftcentral_site_row:not(.suspended)').each(function() {
4041 var id = $(this).data('site_id');
4042 var description = $(this).data('site_description');
4043
4044 var option = {
4045 id: id,
4046 text: description
4047 };
4048 site_options.push(option);
4049
4050 // This is different from site_options array as this does not
4051 // include the site tagged options. It only contains the individual
4052 // site information to be rendered later.
4053 all_options.push(id);
4054 });
4055
4056 group_options.add(0, all_options);
4057 return site_options;
4058 }
4059
4060 /**
4061 * Add site tagged options if available
4062 *
4063 * @private
4064 * @param {array} site_options An array of objects containing the id and name fields that will be displayed as dropdown options
4065 *
4066 * @returns {array}
4067 */
4068 var maybe_add_site_tagged_options = function(site_options) {
4069 $('.updraftcentral_site_row:not(.suspended)').each(function() {
4070 var site_id = $(this).data('site_id');
4071 var tag_items = $(this).find('li.udc_tag_item .udc_tag_text');
4072
4073 if ('undefined' !== typeof tag_items && tag_items && tag_items.length) {
4074 tag_items.each(function() {
4075 var name = $(this).data('tag_name');
4076 if (!group_options.exists(name)) {
4077 site_options.push({
4078 id: name,
4079 text: sprintf(udclion.add_all_sites_tagged, name)
4080 });
4081
4082 group_options.add(name, [site_id]);
4083 } else {
4084 var sites_array = group_options.item(name);
4085 sites_array.push(site_id);
4086 group_options.update(name, sites_array);
4087 }
4088 });
4089 }
4090 });
4091
4092 return site_options;
4093 }
4094
4095 /**
4096 * Builds the interface based from the initial configuration submitted
4097 *
4098 * @private
4099 * @returns {object}
4100 */
4101 var build_interface = function() {
4102 var extra_classes = (config.hasOwnProperty('extra_classes') && config.extra_classes) ? ' '+config.extra_classes : '';
4103
4104 // Create basic elements to apply the widget
4105 var $container = $('<div></div>', {
4106 class: prefix + 'container' + extra_classes,
4107 });
4108
4109 var $label = $('<div></div>', {
4110 class: prefix + 'label',
4111 }).html(config.label).appendTo($container);
4112
4113
4114 var $sites_filter = $('<div></div>', {
4115 class: prefix + 'sites_container',
4116 });
4117
4118 var $sites = $('<select></select>', {
4119 class: prefix + 'sites',
4120 name: 'sites[]',
4121 multiple: 'multiple',
4122 }).appendTo($sites_filter);
4123
4124 $sites_filter.appendTo($container);
4125
4126 var $buttons = $('<div></div>', {
4127 class: prefix + 'buttons',
4128 });
4129
4130 // Generates widget buttons if set in the initial configuration and apply
4131 // any callback handlers whenever applicable
4132 if (config.hasOwnProperty('buttons') && Array.isArray(config.buttons)) {
4133 for (var i=0; i<config.buttons.length; i++) {
4134 var item = config.buttons[i];
4135 var text = item.hasOwnProperty('text') ? item.text : udclion.submit;
4136 var button_index = i + 1;
4137
4138 var $button = $('<button></button>', {
4139 id: item.hasOwnProperty('id') ? item.id : prefix + 'button' + button_index,
4140 name: item.hasOwnProperty('name') ? item.name : prefix + 'button' + button_index,
4141 class: item.hasOwnProperty('class') ? 'btn btn-primary ' + item.class : 'btn btn-primary',
4142 }).text(text).on('click', function() {
4143 if (item.hasOwnProperty('callback') && 'function' === typeof item.callback) {
4144 item.callback.apply(null, []);
4145 }
4146 }).appendTo($buttons);
4147 }
4148 }
4149
4150 $buttons.appendTo($container);
4151
4152 return $container;
4153 }
4154
4155 /**
4156 * Renders the site filter widget where the user can select one or more websites where to
4157 * apply his or her selected/triggered actions
4158 *
4159 * @returns {void}
4160 */
4161 this.render = function() {
4162 var $ui = build_interface();
4163 if (config.hasOwnProperty('container') && config.container) {
4164 $(config.container).append($ui);
4165
4166 // Apply select2js library when rendering the widget and implement
4167 // action listeners and handlers
4168 $(config.container).find('select.'+ prefix + 'sites').select2({
4169 data: get_site_options(),
4170 placeholder: config.hasOwnProperty('placeholder') ? config.placeholder : udclion.press_to_select_website
4171 });
4172
4173 // Selection overrides for custom group option ('all sites', 'sites with tag')
4174 $(config.container).find('select.'+ prefix + 'sites').off('select2:select').on('select2:select', function(e) {
4175 var data = e.params.data;
4176 if (group_options.exists(data.id)) {
4177 // Clear any previous selections. We're going to replace
4178 // them with the actual sites associated with the currently
4179 // selected group option.
4180 $(this).val(null).trigger('change');
4181
4182 // Select the provided values and trigger change to refresh the UI.
4183 var site_ids = group_options.item(data.id);
4184 $(this).val(site_ids).trigger('change');
4185 }
4186 });
4187
4188 $(config.container).find('select.'+ prefix + 'sites').off('select2:open').on('select2:open', function() {
4189 if ($.fullscreen.isFullScreen()) {
4190 $(document.body).children('span.select2-container--open').appendTo('#updraftcentral_dashboard');
4191 }
4192 });
4193
4194 $(config.container).find('select.'+ prefix + 'sites').off('select2:opening select2:closing').on('select2:opening select2:closing', function(event) {
4195 var $searchfield = $(this).parent().find('.select2-search__field');
4196 $searchfield.prop('disabled', true);
4197 });
4198
4199 // We're disabling input by hiding it since we're loading a predefined sites collection as options.
4200 $(config.container).find('input.select2-search__field').hide();
4201
4202 if ('undefined' !== typeof $site_row && $site_row) {
4203 // Add the default site:
4204 $(config.container).find('select.'+ prefix + 'sites').select2("trigger", "select", {
4205 data: {
4206 id: $site_row.data('site_id'),
4207 text: $site_row.data('site_description')
4208 }
4209 });
4210 }
4211 }
4212 }
4213
4214 /**
4215 * Gets or retrieves the selected websites using this site filter widget
4216 *
4217 * @returns {object}
4218 */
4219 this.get_selected_sites = function() {
4220 // Adding sites to the collection
4221 var target_sites = $('select.'+ prefix + 'sites').val();
4222 var is_empty = false;
4223
4224 // Clear sites to any new request to this function. No need to create a new "UpdraftCentral_Collection"
4225 // instance everytime this function is called, so we're clearing it instead.
4226 if (sites.count()) sites.clear();
4227
4228 if ('undefined' !== typeof target_sites && target_sites && target_sites.length) {
4229 var ids = ('string' === typeof target_sites) ? target_sites.split(',') : target_sites;
4230 for (var i=0; i<ids.length; i++) {
4231 var $site = $('.updraftcentral_site_row[data-site_id="'+ids[i]+'"');
4232 if ($site.length) {
4233 sites.add($site.data('site_id'), $site);
4234 }
4235 }
4236 }
4237
4238 if (!sites.count()) {
4239 // If sites filter is empty meaning the user didn't select any additional websites
4240 // to install the plugin(s), then we fall back to the default (host) website to execute the process
4241 sites.add($site_row.data('site_id'), $site_row);
4242
4243 // We've reached here as a fallback because the site selections is empty, thus, we're setting
4244 // the "is_empty" flag to inform the caller of the actual status even if we have defaulted to the
4245 // host site (the site that is currently choosen to work on by the user).
4246 is_empty = true;
4247 }
4248
4249 return {
4250 sites: sites,
4251 selection_empty: is_empty
4252 };
4253 }
4254
4255 }
4256
4257 /**
4258 * Recorder Class
4259 *
4260 * A non-intrusive recording of current menu's event/content and site selection.
4261 *
4262 * @constructor
4263 */
4264 function UpdraftCentral_Recorder() {
4265 var self = this;
4266 var $ = jQuery;
4267 var cache;
4268 var current_menu;
4269 var current_site;
4270 var storeable;
4271 var reset;
4272
4273 /**
4274 * Initializes local variables
4275 */
4276 var init = function() {
4277 cache = new UpdraftCentral_Collection();
4278 storeable = false;
4279 reset = false;
4280 }
4281 init();
4282
4283 /**
4284 * Resets stored data, setting and site selection
4285 *
4286 * @returns {void}
4287 */
4288 this.reset_recorder = function() {
4289 init();
4290 reset_site_selection();
4291 }
4292
4293 /**
4294 * Returns the current site being monitored and recorded
4295 *
4296 * @returns {object|null}
4297 */
4298 this.get_current_site = function() {
4299 return ('undefined' !== typeof self.current_site && self.current_site.length) ? self.current_site : null;
4300 }
4301
4302 /**
4303 * Resets site selection
4304 *
4305 * @returns {void}
4306 */
4307 function reset_site_selection() {
4308 self.current_site = '';
4309 UpdraftCentral.$site_row = null;
4310 }
4311
4312 /**
4313 * Hides or empty elements not needed and add some custom container for
4314 * site selection status when recorder first loads to avoid confusions
4315 * on user selections/options
4316 *
4317 * @returns {void}
4318 */
4319 function onload_housekeeping() {
4320 $('.updraftcentral-search-area input#udc_search_tag').val('');
4321
4322 // Create site selected status container. This is to avoid confusion by showing the currently selected site
4323 var site_status_container = $('#updraft-central-navigation > div.updraftcentral_site_selected');
4324 if (!site_status_container.length) {
4325 $('#updraft-central-navigation').append('<div class="updraftcentral_site_selected">'+udclion.no_site_selected+'</div>');
4326 }
4327 }
4328
4329 /**
4330 * Reloads selected options from select elements
4331 *
4332 * @param {object} $container - A jQuery object representing the container where the content is loaded
4333 * @param {array} options - An array of select/dropdown elements
4334 * @returns {void}
4335 */
4336 function reload_dropdown_events($container, options) {
4337 if ('undefined' !== typeof options) {
4338 for (var i=0; i<options.length; i++) {
4339 var dropdown = options[i].dropdown;
4340 var value = options[i].selected;
4341 var element_class = $(dropdown).prop('class');
4342 var element_id = $(dropdown).prop('id');
4343
4344 var selector = '';
4345 if ('undefined' !== typeof element_class && element_class.length) {
4346 selector = 'select.'+element_class.replace(/\s/, '.');
4347 } else {
4348 if ('undefined' !== typeof element_id && element_id.length) {
4349 selector = 'select#'+element_id;
4350 }
4351 }
4352
4353 if (selector.length) {
4354 var element = $container.find(selector);
4355 var option = element.find('option[value="'+value+'"]');
4356
4357 option.prop('selected', 'selected');
4358 }
4359 }
4360 }
4361 }
4362
4363 /**
4364 * Reloads or reset to the initial state of the interface when first accessed
4365 *
4366 * Basically, removing any previous site selections, deleting settings along with displaying hidden items/sections
4367 * if applicable.
4368 *
4369 * @returns {void}
4370 */
4371 function reload_selection() {
4372 // Reset settings and storage
4373 cache = new UpdraftCentral_Collection();
4374 storeable = false;
4375 reset = false;
4376 self.current_site = '';
4377 UpdraftCentral.$site_row = null;
4378
4379 $('.updraftcentral-search-area').show();
4380 $('#updraftcentral_dashboard_existingsites').find('.ui-sortable-handle').show();
4381 $('#updraft-central-navigation > div.updraftcentral_site_selected').html(udclion.no_site_selected);
4382 }
4383
4384 /**
4385 * Loads stored content that is not associated with a site
4386 *
4387 * @param {object} menu - The menu container that holds the sites and no_sites collection
4388 * @param {string} mode - The mode or menu section currently processed
4389 */
4390 function load_no_site_content(menu, mode) {
4391 var container = menu.no_sites[mode].container;
4392 var callback = menu.no_sites[mode].post_load_callback;
4393 var content_mode = menu.no_sites[mode].mode;
4394 var options = menu.no_sites[mode].options;
4395
4396 $('.updraftcentral-search-area').hide();
4397 $(container).parent().find('.ui-sortable-handle').hide();
4398 $(container).html(menu.no_sites[mode].html);
4399
4400 reload_dropdown_events($(container), options);
4401
4402 $('#updraftcentral_dashboard_existingsites').trigger('extra_contents_loaded_'+mode, [null]);
4403 $('#updraftcentral_dashboard_existingsites').trigger('recorder_content_loaded', [mode, $(container).html(), null]);
4404 if (content_mode === mode && 'function' === typeof callback) {
4405 callback.apply(null, []);
4406 }
4407 }
4408
4409 /**
4410 * Removes any cached content stored for the given area(s)/section(s)
4411 *
4412 * @param {integer} site_id ID of the currently selected site
4413 * @param {array|string} mode The mode or menu section currently processed
4414 * @returns {void}
4415 */
4416 this.invalidate_cached_content = function(site_id, modes) {
4417 if (!Array.isArray(modes) && 'string' !== typeof modes) return;
4418 if ('string' === typeof modes) modes = [modes];
4419
4420 $.each(modes, function(key, mode) {
4421 if (cache.exists(mode)) {
4422 var menu = cache.item(mode);
4423 if ('undefined' !== typeof menu.sites[site_id]) {
4424 menu.sites[site_id] = {};
4425 cache.update(mode, menu);
4426 }
4427 }
4428 });
4429 }
4430
4431 /**
4432 * Loads the event handlers needed for the recording process to work
4433 */
4434 this.load = function() {
4435
4436 $('#updraftcentral_dashboard_existingsites').on('extra_contents_loaded_updates', function(event, $site_row) {
4437
4438 // We are after for the mass update to update the tool tip when its content is loaded from the recorder,
4439 // so we only process if $site_row == null, since technically mass updates isn't linked to a particular site.
4440 if (null === $site_row) {
4441 var $container = $('#updraftcentral_dashboard_existingsites').find('#updates_container');
4442 if ('undefined' !== typeof $container && $container.length && $container.is(':visible')) {
4443 reset_site_selection();
4444
4445 // Shows "All sites selected" on mass updates
4446 $('#updraft-central-navigation > div.updraftcentral_site_selected').html(udclion.all_sites_selected);
4447 $('#updraft-central-content button.updraftcentral_action_choose_another_site').show();
4448 }
4449 }
4450 });
4451
4452 /**
4453 * Sets listener for the "updraftcentral_dashboard_mode_set_before"
4454 *
4455 * Listening to this event will give us a way to store the previously accessed content
4456 * before emptying the container for the new content to follow.
4457 *
4458 * @see {@link http://api.jquery.com/on}
4459 */
4460 $('#updraftcentral_dashboard_existingsites').on('updraftcentral_dashboard_mode_set_before', function(event, data) {
4461
4462 self.current_menu = data.new_mode;
4463
4464 if (!cache.exists(data.new_mode)) {
4465 cache.add(data.new_mode, {
4466 sites: {},
4467 no_sites: {}
4468 });
4469 }
4470
4471 if (!cache.exists(data.previous_mode)) {
4472 cache.add(data.previous_mode, {
4473 sites: {},
4474 no_sites: {}
4475 });
4476 }
4477
4478 var menu = cache.item(data.previous_mode);
4479
4480 if ('undefined' !== typeof self.current_site && self.current_site.length) {
4481 var extra_contents = data.extra_contents;
4482 var site_id = self.current_site.data('site_id');
4483 var html = extra_contents.children().clone(true, true);
4484
4485 if ('undefined' === typeof menu.sites[site_id]) {
4486 menu.sites[site_id] = {};
4487 }
4488
4489 menu.sites[site_id].html = html;
4490 menu.sites[site_id].force = data.force;
4491 menu.sites[site_id].options = [];
4492
4493 extra_contents.find('select').each(function() {
4494 menu.sites[site_id].options.push({
4495 dropdown: $(this),
4496 selected: $(this).find('option').filter(':selected').val()
4497 })
4498 });
4499
4500 cache.update(data.previous_mode, menu);
4501 extra_contents.html('');
4502 }
4503
4504
4505 if ('undefined' !== typeof menu.no_sites[data.previous_mode]) {
4506 var container = menu.no_sites[data.previous_mode].container;
4507 var extra_contents = $(container);
4508 var html = extra_contents.children().clone(true, true);
4509
4510 menu.no_sites[data.previous_mode].html = html;
4511 menu.no_sites[data.previous_mode].options = [];
4512
4513 extra_contents.find('select').each(function() {
4514 menu.no_sites[data.previous_mode].options.push({
4515 dropdown: $(this),
4516 selected: $(this).find('option').filter(':selected').val()
4517 })
4518 });
4519
4520 cache.update(data.previous_mode, menu);
4521
4522 // Clear container's content after saving
4523 $(container).html('');
4524 }
4525
4526 // Check if we need to do a reset
4527 if (true === data.force && data.new_mode === data.previous_mode) {
4528 self.reset = true;
4529 }
4530
4531 // N.B. Typing or searching using the search bar is used to narrow down the list not actually selecting any
4532 // one of the sites listed, one must click either one of the site action buttons to select it. So, here we're
4533 // making sure that we temporarily store the keyword typed by the user to persist it or preserving what was original type
4534 // (by retrieving it later when he/she comes back to the "Sites" section). This is done before clearing the search bar when
4535 // switching to a different tabs. Otherwise, we get a blank site list on the other tabs if the keyword/tag
4536 // typed by the user points to a different site other than the one previously selected (stored site).
4537 if (!cache.exists('search_tag')) {
4538 cache.add('search_tag', '');
4539 }
4540
4541 // N.B. This only applies if a site has already been selected
4542 if ('undefined' !== typeof self.current_site && self.current_site.length) {
4543 if ('sites' === data.previous_mode) {
4544 cache.update('search_tag', $('.updraftcentral-search-area input#udc_search_tag').val());
4545 $('.updraftcentral-search-area input#udc_search_tag').val('');
4546 } else {
4547 if ('sites' !== data.new_mode) {
4548 $('.updraftcentral-search-area').hide();
4549 } else {
4550 $('.updraftcentral-search-area input#udc_search_tag').val(cache.item('search_tag'));
4551 }
4552 }
4553 }
4554
4555 // Reset or close open site actions dropdowns when switching tabs
4556 var dropdown = $('.updraft_site_actions.open');
4557 if (dropdown.length) {
4558 dropdown.removeClass('open');
4559 } else {
4560 dropdown = $('.updraft_site_actions.show');
4561 if (dropdown.length) dropdown.removeClass('show');
4562 }
4563
4564 // Reset tool tip if no site is currently selected
4565 if ('undefined' !== typeof self.current_site && !self.current_site.length) {
4566 $('#updraft-central-navigation > div.updraftcentral_site_selected').html(udclion.no_site_selected);
4567 }
4568 });
4569
4570 /**
4571 * Sets listener for the "updraftcentral_dashboard_mode_set"
4572 *
4573 * Listening to this event will give us a way to process the currently selected site
4574 * and its content.
4575 *
4576 * @see {@link http://api.jquery.com/on}
4577 */
4578 $('#updraftcentral_dashboard_existingsites').on('updraftcentral_dashboard_mode_set', function(event, data) {
4579
4580 // Trying to avoid doing the same process within the same context.
4581 // Thus, we're only going to process this event if both "new_mode" and
4582 // "previous_mode" are not the same.
4583 if (data.new_mode !== data.previous_mode) {
4584
4585 // Hide other sites
4586 if ('undefined' !== typeof self.current_site && self.current_site.length) {
4587 var site_id = self.current_site.data('site_id');
4588
4589 // Need to make sure that we have a valid site in the DOM before executing the needed action, because
4590 // there are circumstances where the site was removed or deleted from a previous action.
4591 var $site_row = $('.updraftcentral_site_row[data-site_id="'+site_id+'"]');
4592
4593 if ('undefined' !== typeof $site_row && $site_row && $site_row.length) {
4594 var restore_hidden = false;
4595
4596 // Make sure that we hide all other sites aside from the ones currently
4597 // selected by the user, so that he or she won't have to re-select the same
4598 // site all over again, and make sure the "Choose another site to manage" button
4599 // is visible in case he wishes to choose another site to work on.
4600 if ('sites' !== data.new_mode) {
4601
4602 var $parent = $site_row.closest('#updraftcentral_dashboard_existingsites');
4603 $parent.find('.updraftcentral_site_row[data-site_id="'+site_id+'"]').parent().show();
4604
4605 $parent.find('.updraftcentral_site_row[data-site_id!="'+site_id+'"]').each(function() {
4606 var $other = $(this);
4607
4608 $other.parent().hide();
4609 });
4610
4611 $('.updraftcentral-search-area').hide();
4612 $('#updraft-central-content button.updraftcentral_action_choose_another_site').show();
4613
4614 // Re-populate the "extracontents" container with the last previously accessed content
4615 // for the given menu under the currently selected site.
4616 var menu = cache.item(data.new_mode);
4617 if ('undefined' !== typeof menu) {
4618 var site = menu.sites[site_id];
4619
4620 if ('undefined' !== typeof menu.no_sites[data.new_mode]) {
4621 load_no_site_content(menu, data.new_mode);
4622 } else if ('undefined' !== typeof site) {
4623
4624 if (!site.force) {
4625 if (!site.storeable && 'undefined' !== typeof site.action_item) {
4626 /**
4627 * Sets listener for the "updraftcentral_dashboard_mode_set_after"
4628 *
4629 * Listening to this event will give us a way to safely trigger the click event
4630 * of the action item/button after it has been loaded.
4631 *
4632 * @see {@link http://api.jquery.com/on}
4633 */
4634 $('#updraftcentral_dashboard_existingsites').on('updraftcentral_dashboard_mode_set_after', function(event, data) {
4635
4636 // Turn-off the listener to make sure that we don't process more than once everytime the event
4637 // is called, most especially when setting a new dashboard mode. It should only be called once
4638 // when the recorder process runs through this line.
4639 $('#updraftcentral_dashboard_existingsites').off('updraftcentral_dashboard_mode_set_after');
4640
4641 // Since this button is not storeable, then we need to trigger its
4642 // click event to re-execute its underlying process.
4643 $(site.action_item).trigger('click');
4644
4645 });
4646
4647 } else {
4648 // Stored contents are re-displayed to the user
4649 var extra_contents = $site_row.find('.updraftcentral_row_extracontents');
4650 var options = site.options;
4651
4652 extra_contents.html(site.html);
4653 reload_dropdown_events(extra_contents, options);
4654
4655 $('#updraftcentral_dashboard_existingsites').trigger('extra_contents_loaded_'+data.new_mode, [$site_row]);
4656 $('#updraftcentral_dashboard_existingsites').trigger('recorder_content_loaded', [data.new_mode, site.html, site]);
4657 }
4658 } else {
4659 restore_hidden = true;
4660 }
4661 }
4662 }
4663
4664 } else {
4665 restore_hidden = true;
4666 }
4667
4668 if (restore_hidden) {
4669 $('.updraftcentral-search-area').show();
4670 $site_row.closest('#updraftcentral_dashboard_existingsites').find('.updraftcentral_site_row').parent().show();
4671 }
4672 }
4673 } else {
4674 var menu = cache.item(data.new_mode);
4675 if ('undefined' !== typeof menu) {
4676 if ('undefined' !== typeof menu.no_sites[data.new_mode]) {
4677 load_no_site_content(menu, data.new_mode);
4678 } else {
4679 $('.updraftcentral-search-area').show();
4680 $('#updraftcentral_dashboard_existingsites').find('.ui-sortable-handle').show();
4681 }
4682 }
4683 }
4684 } else {
4685 if (self.reset) {
4686 reload_selection();
4687 }
4688 }
4689
4690 /**
4691 * Registers click event for the "Show updates for all sites" action button
4692 *
4693 * @see {UpdraftCentral.register_row_clicker}
4694 */
4695 UpdraftCentral.register_event_handler('click', '.updraftcentral_mode_actions button.updraftcentral_action_show_all_updates', function() {
4696
4697 var menu = cache.item(self.current_menu);
4698 if ('undefined' !== typeof menu && menu) {
4699
4700 if ('undefined' === typeof menu.no_sites[self.current_menu]) {
4701 menu.no_sites[self.current_menu] = {
4702 container: '#updates_container.updraftcentral_row_extracontents',
4703 html: '', // This gets populated later when switching menus or modes
4704 mode: 'updates', // Same as menu, we're just using "mode" here to be consistent with the check description
4705 post_load_callback: function() {
4706
4707 // Refresh the interface cache from previous usage, specifically from mass updates activities.
4708 if ('function' === typeof UpdraftCentral_Updates.reload_interface) {
4709 UpdraftCentral_Updates.reload_interface();
4710 }
4711 }
4712 };
4713 }
4714
4715 cache.update(self.current_menu, menu);
4716 }
4717
4718 if ('undefined' !== typeof self.current_site && self.current_site.length) {
4719 $('#updraft-central-content button.updraftcentral_action_choose_another_site').show();
4720 }
4721
4722 reset_site_selection();
4723
4724 // Shows "All sites selected" on mass updates
4725 $('#updraft-central-navigation > div.updraftcentral_site_selected').html(udclion.all_sites_selected);
4726 $('#updraft-central-content button.updraftcentral_action_choose_another_site').show();
4727 });
4728
4729
4730 /**
4731 * Registers click event for the top level section buttons
4732 *
4733 * @see {UpdraftCentral.register_row_clicker}
4734 */
4735 UpdraftCentral.register_row_clicker('.btn-group > button', function($site_row) {
4736
4737 // We're going to exempt the dropdown menu button from the below process because
4738 // it is conflicting with the visibilty when a site is either suspended or unsuspended.
4739 // Besides, clicking the dropdown button does not mean that a content is actually loaded instead
4740 // is it showing you options to choose from, thus, we're going to bypass it in this case.
4741 if ($(this).closest('.updraft_site_actions').hasClass('more-option-container')) {
4742 return;
4743 }
4744
4745 self.current_site = $site_row;
4746
4747 // Update site selected status
4748 var site_status_container = $('#updraft-central-navigation > div.updraftcentral_site_selected');
4749 site_status_container.html(udclion.selected_site+': '+self.current_site.data('site_description'));
4750
4751
4752 // "Sites" tab or area must not be affected by the below process other than
4753 // storing the currently selected site, since the "Sites" area must allow users
4754 // to see all their registered sites in UDC, whom they can manage or work on.
4755 if ('sites' !== self.current_menu) {
4756 action_item = $(this);
4757
4758 // We're hiding the search box here, since clicking these top level buttons automatically selects
4759 // a site to work on. This is restored when user visits the "Sites" menu or clicking the "Choose
4760 // Another Site To Manage..." button.
4761 //
4762 // Before hiding the search bar when a new site has been selected we keep the current search tag/keyword
4763 // if not empty so that it will persist when the user goes back to the "Sites" area.
4764 var search_tag = $('.updraftcentral-search-area input#udc_search_tag').val();
4765 if (search_tag.length) {
4766 cache.update('search_tag', search_tag);
4767 $('.updraftcentral-search-area input#udc_search_tag').val('');
4768 }
4769 $('.updraftcentral-search-area').hide();
4770
4771
4772 // Check to see if "data-storeable" property is present in the element and it was set to "false". Thus,
4773 // requiring the action_item's (buttons) "click" event to be triggered to refresh its contents, otherwise,
4774 // the stored data or contents will be displayed immediately.
4775 var storeable = true;
4776 if ('undefined' !== typeof action_item.data('storeable') && false === action_item.data('storeable')) {
4777 storeable = false;
4778 }
4779
4780 var site_id = self.current_site.data('site_id');
4781 var menu = cache.item(self.current_menu);
4782
4783
4784 if ('undefined' !== typeof menu && menu) {
4785 if ('undefined' === typeof menu.sites[site_id]) {
4786 menu.sites[site_id] = {};
4787 }
4788
4789 menu.sites[site_id].action_item = action_item.get(0);
4790 menu.sites[site_id].storeable = storeable;
4791
4792 // Here, we're clearing the no_sites property for this menu, since
4793 // we're currently storing a site specific content. Otherwise, the no_sites
4794 // entries will take precedence when rendering the content to the user.
4795 menu.no_sites = {};
4796
4797 cache.update(self.current_menu, menu);
4798 }
4799
4800 if ('sites' !== self.current_menu) {
4801 var $parent = $site_row.closest('#updraftcentral_dashboard_existingsites');
4802 $parent.find('.updraftcentral_site_row:not([data-site_id="'+site_id+'"])').parent().slideUp();
4803 }
4804 }
4805
4806 });
4807 });
4808
4809 /**
4810 * Housekeeping in preparation for the recording process
4811 *
4812 * @see {UpdraftCentral_Recorder#onload_housekeeping}
4813 */
4814 onload_housekeeping();
4815 }
4816 }
4817
4818 /**
4819 * Keyboard Shortcuts Class
4820 *
4821 * Handles and processes keyboard shortcut for UpdraftCentral major functions.
4822 *
4823 * @constructor
4824 */
4825 function UpdraftCentral_Keyboard_Shortcuts() {
4826 var self = this;
4827 var $ = jQuery;
4828 var keys = '';
4829 var processing = false;
4830 var site_required_exceptions,
4831 shortcuts_sorted,
4832 shortcuts,
4833 content_loaded,
4834 popups;
4835 this.is_macintosh = false;
4836
4837 /**
4838 * Initializes local variables, collection, events and keyboard shortcuts
4839 *
4840 * @private
4841 * @param {function} onload_callback An optional callback function to execute when all shortcuts are loaded
4842 * @returns {void}
4843 */
4844 this.init = function(onload_callback) {
4845 shortcuts = new UpdraftCentral_Collection();
4846 popups = new UpdraftCentral_Collection();
4847 site_required_exceptions = {};
4848 shortcuts_sorted = [];
4849 self.originals = udclion.keyboard_shortcuts;
4850 content_loaded = null;
4851
4852 // Run platform check whether it's Macintosh or not
4853 is_platform_macintosh().then(function(result) {
4854 self.is_macintosh = result;
4855 });
4856
4857 // Load default shortcuts
4858 load_shortcuts(onload_callback);
4859
4860 if ('undefined' === typeof onload_callback) {
4861 $('#updraftcentral_dashboard_existingsites').on('recorder_content_loaded', function(event, menu, content, site) {
4862 content_loaded = {
4863 menu: menu,
4864 content: content,
4865 site: site
4866 };
4867 });
4868
4869 // Registers all needed events for this module/class to the document object.
4870 $(document).on('keydown', process_shortcut);
4871 $('#updraftcentral_dashboard').on('updraftcentral_bootbox_dialog_opened', function(event, id, $dialog) {
4872 add_shortcut_display_handler($dialog);
4873 });
4874 add_shortcut_display_handler($('#updraftcentral_modal_dialog'));
4875
4876
4877 // Register custom edit listeners, in case the user wishes to edit the default shortcut keys
4878 // with those keys that is suitable to his environment or preference.
4879 load_edit_listeners();
4880 }
4881 }
4882
4883 /**
4884 * Registers a keyboard shortcut
4885 *
4886 * @private
4887 * @param {Object} info A object containing the shortcut details.
4888 * @param {string} info.name A unique identifier for the given shortcut.
4889 * @param {string} info.key The keyboard key combination that represents the shortcut (e.g. ALT+K).
4890 * @param {string} info.description An information describing what the shortcut is for.
4891 * @param {string} info.menu (Optional) The UpdraftCentral menu/module name that is needed if the shortcut action is non-function.
4892 * @param {boolean} info.site_required "True" if a site is require to process the action, "False" otherwise.
4893 * @param {string|Function} action A mixed parameter either containing the button id or class name or a callback function.
4894 * @param {boolean} override "True" to override existing shortcut with the same name or identifier, "False" otherwise.
4895 * @returns {void}
4896 */
4897 this.register_shortcut = function(info, action, override) {
4898
4899 var shortcut_action = action;
4900 if ('function' === typeof action) {
4901 shortcut_action = function() {
4902 // Check and verify that a process is currently not running before
4903 // executing the below code to prevent from abruptly aborting the current process
4904 // which may lead to JS errors or/and inconsistency of information displayed to the user
4905 if (UpdraftCentral.check_processing_state()) return;
4906
4907 action.apply(null, [info.name, info.menu]);
4908 }
4909 }
4910
4911 var shortcut = {
4912 name: info.name,
4913 key: info.key,
4914 description: info.description,
4915 action: shortcut_action,
4916 menu: ('undefined' !== typeof info.menu) ? info.menu : '',
4917 site_required: ('undefined' !== typeof info.site_required) ? info.site_required : true,
4918 }
4919
4920 add_site_required_exceptions(shortcut);
4921
4922 if (!shortcuts.exists(info.key)) {
4923 shortcuts.add(info.key, shortcut);
4924 } else {
4925 if ('undefined' !== typeof override && override) {
4926 shortcuts.update(info.key, shortcut);
4927 }
4928 }
4929 }
4930
4931 /**
4932 * Saves user-defined keyboard shortcut entered by the user
4933 *
4934 * @param {string} $shortcut_name The name of the shortcut to be overriden
4935 * @param {string} $shortcut_key The new shortcut key entered by the user
4936 * @param {object} $spinner_where A jquery object that serves as a container for the spinner
4937 * @returns {object} jQuery promise object
4938 */
4939 this.save_shortcut = function(shortcut_name, shortcut_key, $spinner_where) {
4940 var deferred = $.Deferred();
4941
4942 UpdraftCentral.send_ajax('shortcuts', { name: shortcut_name, key: shortcut_key }, null, 'via_mothership_encrypting', $spinner_where, function(resp, code, error_code) {
4943 if ('ok' === code) {
4944 if (resp.hasOwnProperty('message')) {
4945 if ('success' === resp.message) {
4946 deferred.resolve();
4947 } else {
4948 deferred.reject();
4949 }
4950 }
4951 }
4952 });
4953
4954 return deferred.promise();
4955 }
4956
4957 /**
4958 * Clears user-defined keyboard shortcuts
4959 *
4960 * @param {object} $spinner_where A jquery object that serves as a container for the spinner
4961 * @returns {object} jQuery promise object
4962 */
4963 this.clear_shortcuts = function($spinner_where) {
4964 var deferred = $.Deferred();
4965
4966 UpdraftCentral.send_ajax('shortcuts', { clear: true }, null, 'via_mothership_encrypting', $spinner_where, function(resp, code, error_code) {
4967 if ('ok' === code) {
4968 if (resp.hasOwnProperty('message')) {
4969 if ('success' === resp.message) {
4970 deferred.resolve(resp);
4971 } else {
4972 deferred.reject();
4973 }
4974 }
4975 }
4976 });
4977
4978 return deferred.promise();
4979 }
4980
4981 /**
4982 * Legacy checking for Macintosh platform
4983 *
4984 * @returns {boolean} - true if platform/OS is Mac, false otherwise
4985 */
4986 var legacy_platform_check = function() {
4987 if ('undefined' !== typeof window.navigator.userAgent) {
4988 // We have everything we need in the userAgent string to identify if the platform is Mac or not.
4989 var user_agent = window.navigator.userAgent.toLowerCase();
4990
4991 // For Opera and IE browsers on Macintosh system they only have "Mac_PowerPC" in their userAgent
4992 // string to identify the system as Mac. The rest of the browsers contains "Macintosh" keyword. Thus,
4993 // the conditions below should be enough and precise to cover every browsers on Macintosh systems.
4994 if (-1 !== user_agent.indexOf('macintosh') || -1 !== user_agent.indexOf('mac_powerpc')) {
4995 return true;
4996 }
4997 }
4998
4999 return false;
5000 }
5001
5002 /**
5003 * Check whether the platform/OS used by the user is Macintosh
5004 *
5005 * @returns {object} jQuery promise object
5006 */
5007 var is_platform_macintosh = function() {
5008 var deferred = $.Deferred();
5009
5010 // Using future User Agent Client Hints (UA-CH)
5011 if ('undefined' !== typeof window.navigator.userAgentData && window.navigator.userAgentData) {
5012 if ('function' === typeof window.navigator.userAgentData.getHighEntropyValues) {
5013 // Even though this function is returning a promise object the result of getting the desired
5014 // value that we need (e.g. platform) is instantaneous, thus, we won't have to worry whether
5015 // there's a delay in querying if the platform is mac or not. We should be okay here as we're
5016 // preloading the check as soon as UpdraftCentral loads and it is a local query on the browser
5017 // so it should be fast enough to fill/update the `is_macintosh` variable/flag.
5018 window.navigator.userAgentData.getHighEntropyValues(['platform']).then(function(hints) {
5019 if ('undefined' !== typeof hints && 'undefined' !== typeof hints.platform) {
5020 var platform = hints.platform.toLowerCase();
5021 if (-1 !== platform.indexOf('mac')) {
5022 deferred.resolve(true);
5023 } else {
5024 deferred.resolve(false);
5025 }
5026 } else {
5027 deferred.resolve(legacy_platform_check());
5028 }
5029 });
5030 } else {
5031 deferred.resolve(legacy_platform_check());
5032 }
5033 } else {
5034 deferred.resolve(legacy_platform_check());
5035 }
5036
5037 return deferred.promise();
5038 }
5039
5040 /**
5041 * Add a click event handler to the keyboard_shortcuts link contained
5042 * inside a dialog container
5043 *
5044 * @param {object} $dialog A jquery object that represents the current dialog
5045 * @returns {void}
5046 */
5047 var add_shortcut_display_handler = function($dialog) {
5048 if ('undefined' !== typeof $dialog && $dialog) {
5049 $dialog.on('click', 'a.keyboard_shortcuts', function() {
5050 var $modal = $(this).closest('.bootbox.modal.show');
5051 if (!$modal.length) {
5052 $modal = $(this).closest('#updraftcentral_modal_dialog.show');
5053 }
5054
5055 if ($modal.length) {
5056 $modal.removeClass('fade').scrollTop(0).hide();
5057 }
5058
5059 display_available_shortcuts(null, $modal);
5060 });
5061 }
5062 }
5063
5064 /**
5065 * Adds the keyboard shortcut that does not require a site to
5066 * process its action
5067 *
5068 * We used this exceptions to determine the correct container to
5069 * search for the trigger (e.g. button) of the action.
5070 *
5071 * @private
5072 * @param {Object} shortcut Keyboard shortcut object containing the shortcut details.
5073 * @param {string} shortcut.name A unique identifier for the given shortcut.
5074 * @param {string} shortcut.key The keyboard key combination that represents the shortcut (e.g. ALT+K).
5075 * @param {string} shortcut.description An information describing what the shortcut is for.
5076 * @param {string|Function} shortcut.action A mixed parameter either containing the button id or class name or a callback function.
5077 * @param {string} shortcut.menu (Optional) The UpdraftCentral menu/module name that is needed if the shortcut action is non-function.
5078 * @param {boolean} shortcut.site_required "True" if a site is require to process the action, "False" otherwise.
5079 * @returns {void}
5080 */
5081 var add_site_required_exceptions = function(shortcut) {
5082 // Actions that does not require a site to execute must
5083 // be registered under the exceptions object
5084 if ('undefined' !== typeof shortcut.site_required && !shortcut.site_required) {
5085 site_required_exceptions[shortcut.name] = !shortcut.site_required;
5086 }
5087 }
5088
5089 /**
5090 * Sorts shortcuts collection to maintain index/position upon display
5091 * even when removing or adding object's property on the fly.
5092 *
5093 * @returns {void}
5094 */
5095 var manage_indexes = function() {
5096 shortcuts_sorted = shortcuts.get_items().sort(function(a,b) {
5097 if (a.name < b.name) return -1;
5098 if (a.name > b.name) return 1;
5099 return 0;
5100 });
5101 }
5102
5103 /**
5104 * Loads all available keyboard shortcuts from the keyboard-mappings file
5105 *
5106 * @private
5107 * @param {function} onload_callback An optional callback function to execute when all shortcuts are loaded
5108 * @returns {void}
5109 */
5110 var load_shortcuts = function(onload_callback) {
5111
5112 // Register premium shortcuts when necessary
5113 maybe_register_premium_shortcuts();
5114
5115 // Load locally registered shortcuts
5116 load_local_shortcuts();
5117
5118 var shortcut_storage = udclion.user_defined_shortcuts;
5119 if ('undefined' !== typeof udclion && 'undefined' !== typeof udclion.keyboard_shortcuts) {
5120 for (var name in udclion.keyboard_shortcuts) {
5121 var shortcut = JSON.parse(JSON.stringify(udclion.keyboard_shortcuts[name]));
5122 var key = shortcut.key;
5123 var bypass = false;
5124
5125 if (shortcut.menu.length && !$('#updraft-menu-item-'+shortcut.menu).is(':visible')) {
5126 bypass = true;
5127 }
5128
5129 if (!bypass) {
5130 shortcut.name = name;
5131 shortcuts.add(key, shortcut);
5132
5133 if ('undefined' !== typeof shortcut.popup && shortcut.popup) {
5134 popups.add(key, true);
5135 }
5136
5137 add_site_required_exceptions(shortcut);
5138 }
5139 }
5140 }
5141
5142 // Override default/system shortcuts with user-defined shortcuts
5143 var items = shortcuts.get_items();
5144 for (var i=0; i<items.length; i++) {
5145 var shortcut = items[i];
5146
5147 if ('undefined' !== typeof shortcut_storage[shortcut.name]) {
5148 var current_key = shortcut.key;
5149 var local_key = shortcut_storage[shortcut.name];
5150
5151 // Override the existing key, if user previously edited the default shortcut
5152 if (local_key.length) {
5153 shortcut.key = local_key;
5154
5155 shortcuts.add(shortcut.key, shortcut);
5156 shortcuts.remove(current_key);
5157 }
5158 }
5159 }
5160
5161 manage_indexes();
5162
5163 if ('function' === typeof onload_callback) {
5164 onload_callback.apply(null, []);
5165 }
5166
5167 }
5168
5169 /**
5170 * Processes the keyboard shortcut triggered by the user
5171 *
5172 * Currently, we only support CTRL/CONTROL, ALT/OPTION and SHIFT keys in combination letters.
5173 *
5174 * @private
5175 * @param {Object} e The keyboard event object
5176 * @returns {void}
5177 */
5178 var process_shortcut = function(e) {
5179
5180 // We're not processing any shortcuts if one of the dialogs are currently visible.
5181 if (UpdraftCentral_Library.is_dialog_opened()) {
5182 return;
5183 }
5184
5185 // Prevent processing of shortcut if user opted to deactivate it from the settings area
5186 if ('inactive' === udclion.shortcut_status) {
5187 return;
5188 }
5189
5190 var event = e ? e : window.event;
5191 var keyCode = e ? e.which : event.keyCode;
5192 var character = String.fromCharCode(keyCode),
5193 combination = '',
5194 modifier_keys = '';
5195
5196 // Check if user is currently typing. If so, we disable processing of keyboard shortcuts temporarily
5197 // until the user is done typing (meaning, no input focus is given to an "input" or "textarea" element).
5198 // This prevents keyboard "letter" shortcuts (e.g. "U", "B", "L", etc.) as requested by David A from interfering with the user
5199 // typing something.
5200 var has_focus_on_content = $('div#updraft-central-content').find('input, textarea').is(':focus');
5201 var has_focus_on_dialog = $('div#updraftcentral_modal_dialog').find('input, textarea').is(':focus');
5202 var has_focus_on_bootbox_dialog = $('div.bootbox.modal').find('input, textarea').is(':focus');
5203
5204 // NEW: When the UC page/post editor loads we bypass processing of shortcuts as to allow
5205 // any available shortcuts the current editor has.
5206 var has_editor_loaded = false;
5207 if ($('#classic_editor_container').is(':visible') || $('#gutenberg_editor_container').is(':visible')) has_editor_loaded = true;
5208
5209 if (has_focus_on_content || has_focus_on_dialog || has_focus_on_bootbox_dialog || has_editor_loaded) {
5210 return;
5211 }
5212
5213 // Check whether we need Mac OS keys to map and process. By default,
5214 // these are windows description.
5215 var ctrl_key = 'CTRL',
5216 alt_key = 'ALT',
5217 shift_key = 'SHIFT';
5218
5219 // Construct and combine the inputted keys that will be used to run and execute based on
5220 // our registered keyboard shortcuts collection.
5221 //
5222 // If for some reason, the ctrlKey, altKey and shiftKey event properties check failed to capture the event
5223 // on Mac OS, we run the check against the keyCode instead, since Mac browsers adheres to raising keydown
5224 // event everytime a key is pressed, thus, we can safely check and validate the code for these (control, option and shift) keys.
5225 if (17 === keyCode) modifier_keys += ctrl_key;
5226 if (18 === keyCode) modifier_keys += (modifier_keys.length) ? '+' + alt_key : alt_key;
5227 if (16 === keyCode) modifier_keys += (modifier_keys.length) ? '+' + shift_key : shift_key;
5228
5229 // Append current key pressed to keys variable
5230 if (modifier_keys.length) {
5231 keys += (keys.length) ? '+' + modifier_keys : modifier_keys;
5232 } else if (character.length) {
5233 keys += (keys.length) ? '+' + character : character;
5234 }
5235
5236 var combination = keys.split('+');
5237 if (1 === combination.length && -1 !== $.inArray(combination[0], [ctrl_key, alt_key, shift_key])) {
5238 return;
5239 }
5240
5241 if (keys.length) {
5242 var lowerKeys = keys.toLowerCase();
5243 var upperKeys = keys.toUpperCase();
5244
5245 if (-1 !== lowerKeys.indexOf('+')) {
5246 var key = lowerKeys.split('+');
5247 lowerKeys = key[0].toUpperCase()+'+'+key[1];
5248 }
5249
5250 // If the key combination exists in our keyboard shortcut collection then
5251 // we process or trigger the associated action.
5252 if ((shortcuts.exists(lowerKeys) || shortcuts.exists(upperKeys)) && !processing) {
5253 var shortcut = (shortcuts.exists(lowerKeys)) ? shortcuts.item(lowerKeys) : shortcuts.item(upperKeys);
5254 processing = true;
5255
5256 if ('function' === typeof shortcut.action) {
5257 // If the action is of type "function" then trigger the callback function
5258 // associated with the keyboard shortcut.
5259 shortcut.action.apply(null, [shortcut.name, shortcut.menu]);
5260
5261 } else if ('string' === typeof shortcut.action && shortcut.action.length) {
5262 var $container = UpdraftCentral.$site_row;
5263
5264 if ('undefined' !== typeof site_required_exceptions[shortcut.name] && site_required_exceptions[shortcut.name]) {
5265 $container = $('div#updraft-central-content');
5266 } else {
5267 // If the user haven't selected a site yet then we processes the first available site
5268 // from the sites collection that the user have.
5269 if ('undefined' === typeof $container || !$container) {
5270 var $parent = $('div#updraftcentral_dashboard_existingsites > div.ui-sortable-handle');
5271 var $site_row = $parent.find('.updraftcentral_site_row:not(.suspended)').first();
5272
5273 if ($site_row.length) {
5274 UpdraftCentral.$site_row = $site_row;
5275 $container = $site_row;
5276 }
5277 }
5278 }
5279
5280 // We make sure that we have a valid container before processing the action.
5281 if ('undefined' !== typeof $container && $container && $container.length) {
5282
5283 // Check whether we only have menu but with an empty action which
5284 // means that the user will only be taken to the section/tab without
5285 // initiating or triggering any action buttons.
5286 if (shortcut.menu.length && !shortcut.action.length) {
5287 $('#updraft-menu-item-'+shortcut.menu).trigger('click');
5288 return;
5289 }
5290
5291 // Process or trigger the button with the given DOM id or class name that is
5292 // associated with the keyboard shortcut.
5293 var button = $container.find('#'+shortcut.action),
5294 use_id = ('undefined' !== typeof button && button.length) ? true : false,
5295 use_selector;
5296
5297 if (!use_id) {
5298 // If id is not valid then use class name instead.
5299 button = $container.find('.'+shortcut.action);
5300 use_selector = ('undefined' !== typeof button && button.length) ? true : false;
5301
5302 if (!use_selector) {
5303 // If class name is not valid then use selector instead.
5304 button = $container.find(shortcut.action);
5305 }
5306 }
5307
5308 // Check if previously recorded content are already loaded, if so
5309 // skip triggering the button's click event.
5310 var extra_contents = $container.find('.updraftcentral_row_extracontents');
5311
5312 if ('undefined' !== typeof button && button && button.length) {
5313 // If no menu is found, we default to "Sites" section. Usually, this applies to site configuration
5314 // since it runs across all sections/tabs. But the developer should actually know this beforehand when
5315 // he or she used the add_filter function when adding shortcuts.
5316 if (!shortcut.menu || !shortcut.menu.length) {
5317 shortcut.menu = 'sites';
5318 }
5319
5320 var active_menu = $('.updraft-menu-item-links-active').prop('id').replace('updraft-menu-item-', '');
5321 if (shortcut.menu === active_menu) {
5322
5323 // Skip button's click event if recorded content from recorder
5324 // was already loaded.
5325 if (content_loaded && 'undefined' !== typeof content_loaded.menu && shortcut.menu === content_loaded.menu) {
5326 if (content_loaded.site && content_loaded.site.action_item) {
5327 var action_button = $(content_loaded.site.action_item);
5328
5329 if (button.prop('class') === action_button.prop('class') && !popups.exists(shortcut.key) && extra_contents.find('div').html()) {
5330 processing = false;
5331 content_loaded = null;
5332 return;
5333 }
5334 }
5335 }
5336
5337 if (popups.exists(shortcut.key)) extra_contents.html('');
5338 button.trigger('click');
5339 } else {
5340 /**
5341 * Sets listener for the "updraftcentral_dashboard_mode_set_after"
5342 *
5343 * Listening to this event will give us a way to safely trigger the click event
5344 * of the action item/button after it has been loaded.
5345 *
5346 * @see {@link http://api.jquery.com/on}
5347 */
5348 $('#updraftcentral_dashboard_existingsites').on('updraftcentral_dashboard_mode_set_after', function(event, data) {
5349 // Doing the check once again (from above) to ensure that we have a valid button before
5350 // calling the trigger method since this is using a listener which will be triggered
5351 // everytime a menu has changed so we need to make sure that we only take action within
5352 // the context of this process.
5353 if ('undefined' !== typeof button && button && button.length) {
5354 $('#updraftcentral_dashboard_existingsites').off('updraftcentral_dashboard_mode_set_after');
5355
5356 // Skip button's click event if recorded content from recorder
5357 // was already loaded.
5358 if (content_loaded && 'undefined' !== typeof content_loaded.menu && shortcut.menu === content_loaded.menu) {
5359 if (content_loaded.site && content_loaded.site.action_item) {
5360 var action_button = $(content_loaded.site.action_item);
5361
5362 if (button.prop('class') === action_button.prop('class') && !popups.exists(shortcut.key) && extra_contents.find('div').html()) {
5363 processing = false;
5364 content_loaded = null;
5365 return;
5366 }
5367 }
5368 }
5369
5370 if (popups.exists(shortcut.key)) extra_contents.html('');
5371 button.trigger('click');
5372 }
5373 });
5374
5375 // Activate the menu associated with the shortcut, so that we will have
5376 // access to the button that we need to trigger. Basically, automating the
5377 // clicking of the menu located at the side bar.
5378 $('#updraft-menu-item-'+shortcut.menu).trigger('click');
5379 }
5380
5381 }
5382 }
5383
5384 } else {
5385 // Same as "upgrade" section - no action but with menu
5386 if (shortcut.menu.length) {
5387 // Activate the menu associated with the shortcut to display the content.
5388 // Basically, automating the clicking of the menu located at the side bar.
5389 $('#updraft-menu-item-'+shortcut.menu).trigger('click');
5390 }
5391 }
5392
5393 processing = false;
5394 }
5395
5396 // Cleared keys for new key combination
5397 keys = '';
5398 }
5399 }
5400
5401 /**
5402 * Displays all registered keyboard shortcuts and their information
5403 *
5404 * @private
5405 * @param {object|null} $container The section where the data is to be refresh or re-loaded
5406 * @param {object} modal The source popup window where the shortcut dialog was triggered
5407 * @returns {void}
5408 */
5409 var display_available_shortcuts = function($container, modal) {
5410 var list = '<ul class="uc_shortcuts">';
5411 var items = shortcuts_sorted;
5412
5413 // Constructs a list of all available keyboard shortcuts
5414 for (var i=0; i<items.length; i++) {
5415 var shortcut = items[i];
5416 var key = shortcut.key;
5417
5418 // Here, we will automatically replace windows specific modifier key description/name with
5419 // Mac key names when we're viewing UpdraftCentral on Mac OS's browsers, to make it consistent with the
5420 // check done on UpdraftCentral_Keyboard_Shortcuts#process_shortcut.
5421 var shortcut_display = '<span class="uc_current_shortcut">'+key+'</span>';
5422 if (self.is_macintosh) {
5423 var mac_key = key.replace('CTRL', 'CONTROL').replace('ALT', 'OPTION');
5424 // We keep the "uc_current_shortcut" in here by hiding it instead of completely removing it
5425 // because this is being refered to when saving a new key. We're not allowing the user
5426 // to control its visibility thus, we're adding the style="display:none;" attribute directly.
5427 shortcut_display = '<span class="uc_current_shortcut" style="display:none;">'+key+'</span><span class="uc_current_mac_shortcut">'+mac_key+'</span>';
5428 }
5429
5430 list += '<li data-name="'+shortcut.name+'">'+shortcut.description+' <span>('+udclion.shortcut_key+': '+shortcut_display+')</span> <span class="uc_change_shortcut">'+udclion.keyboard_change_shortcut+'</span> <div class="uc_shortcut_elements"></div></li>';
5431 }
5432 list += '</ul>';
5433
5434 if ('undefined' !== typeof $container && $container && $container.length) {
5435 $container.replaceWith(list);
5436 } else {
5437
5438 // Set unique identifier for validation
5439 var dialog_id = 'shortcut_dialog_id';
5440 var position = null;
5441 var backdrop;
5442
5443 if ('undefined' !== typeof modal && modal) {
5444 position = modal.find('div.modal-dialog').offset();
5445 backdrop = false;
5446 }
5447
5448 // Set alert dialog listener
5449 $('#updraftcentral_dashboard').on('updraftcentral_bootbox_dialog_opened', function(event, id, $dialog) {
5450 // We're making sure that we're only processing this for the keyboard
5451 // shortcuts display by validating its ID.
5452 if (id === dialog_id && position) {
5453 $dialog.find('div.modal-dialog').css({
5454 top: position.top
5455 });
5456
5457 $dialog.scrollTop(0);
5458 }
5459 $dialog.removeClass('fade');
5460 });
5461
5462 $('#updraftcentral_dashboard').on('updraftcentral_bootbox_dialog_closed', function(event, id, $dialog) {
5463 // We're making sure that we're only processing this for the keyboard
5464 // shortcuts display by validating its ID.
5465 if (id === dialog_id) {
5466 // Fix modal window clipped off when re-displayed after an internal window
5467 // is triggered inside the current modal window.
5468 if (!$('body').hasClass('modal-open')) {
5469 $('body').addClass('modal-open');
5470 }
5471 }
5472 });
5473
5474 // Display keyboard shortcuts dialog
5475 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.keyboard_shortcuts_heading+' <a class="uc_clear_local_shortcuts">('+udclion.reset_shortcuts+')</a></h2><p>'+udclion.keyboard_shortcuts_message+'</p><div class="uc_shortcuts_spacer"><div class="uc_shortcuts_spinner"></div></div><p>'+list+'</p>', function() {
5476 if ('undefined' !== typeof modal && modal) {
5477 modal.removeClass('fade').show().scrollTop();
5478 }
5479 }, false, dialog_id, backdrop);
5480
5481 }
5482 }
5483
5484 /**
5485 * Loads or registers keyboard shortcut edit listeners
5486 *
5487 * @returns {void}
5488 */
5489 var load_edit_listeners = function() {
5490 UpdraftCentral.register_event_handler('click', '.modal-dialog span.uc_change_shortcut', function() {
5491 var $container = $(this).closest('li').find('div.uc_shortcut_elements');
5492 var modifiers = ['CTRL', 'ALT', 'SHIFT'];
5493
5494 var modifier_options = '',
5495 option = '';
5496 for (var i=0; i<modifiers.length; i++) {
5497 option = modifiers[i];
5498 if (self.is_macintosh) {
5499 option = option.replace('CTRL', 'CONTROL').replace('ALT', 'OPTION');
5500 }
5501
5502 modifier_options += '<option value="'+modifiers[i]+'">'+option+'</option>';
5503 }
5504
5505 var modifier_options = '<select><option value="">'+udclion.keyboard_choose_modifiers+'</option>'+modifier_options+'</select> ';
5506 $container.html(modifier_options+'<input type="text" maxlength="1" value="" /> <button class="btn btn-primary uc_save_shortcut">'+udclion.keyboard_save_shortcut+'</button> <button class="btn btn-primary uc_cancel_shortcut">'+udclion.keyboard_cancel_shortcut+'</button> <span class="uc_new_shortcut"></span>');
5507 $(this).hide();
5508 });
5509
5510 UpdraftCentral.register_event_handler('click', '.modal-dialog button.uc_save_shortcut', function() {
5511 var $container = $(this).closest('li').find('div.uc_shortcut_elements');
5512
5513 // Save options
5514 var modifier = $container.find('select').val();
5515 var character = $container.find('input[type="text"]').val();
5516 var name = $container.parent().data('name');
5517
5518 if (!character || !character.length || !character.match(/^[a-zA-Z]$/g)) {
5519 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.keyboard_shortcut_required_heading+'</h2><p>'+udclion.keyboard_shortcut_required+'</p>');
5520 return;
5521 }
5522
5523 var connector = '+';
5524 if ('undefined' === typeof modifier || !modifier.length) {
5525 connector = '';
5526 modifier = '';
5527 }
5528
5529 var new_shortcut = modifier+connector+character;
5530 if (new_shortcut.length) {
5531
5532 // Check whether some shortcut already has the same shortcut
5533 var exist = false;
5534 $container.closest('ul.uc_shortcuts').find('li[name!="'+name+'"] span.uc_current_shortcut').each(function() {
5535 var key = $(this).html();
5536 if (new_shortcut.toUpperCase() === key.toUpperCase()) {
5537 exist = true;
5538 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.keyboard_shortcut_exist_heading+'</h2><p>'+udclion.keyboard_shortcut_exist+'</p>');
5539 return false;
5540 }
5541 });
5542
5543 if (!exist) {
5544 var current_shortcut_container = $container.parent().find('span.uc_current_shortcut');
5545 var current_key = current_shortcut_container.html();
5546
5547 if (shortcuts.exists(current_key)) {
5548 var shortcut = shortcuts.item(current_key);
5549 shortcut.key = new_shortcut;
5550
5551 self.save_shortcut(name, shortcut.key, $container.parent()).then(function(response) {
5552 shortcuts.add(shortcut.key, shortcut);
5553 shortcuts.remove(current_key);
5554
5555 current_shortcut_container.html(shortcut.key);
5556 if (self.is_macintosh) {
5557 var mac_key = shortcut.key.replace('CTRL', 'CONTROL').replace('ALT', 'OPTION');
5558 current_shortcut_container.siblings('.uc_current_mac_shortcut').html(mac_key);
5559 }
5560
5561 // Update popups collection with the new key:
5562 if (popups.exists(current_key)) {
5563 popups.remove(current_key);
5564 popups.add(new_shortcut, true);
5565 }
5566 }).always(function() {
5567 $container.parent().find('span.uc_change_shortcut').show();
5568 $container.html('');
5569 });
5570 }
5571 }
5572
5573 }
5574 });
5575
5576 UpdraftCentral.register_event_handler('click', '.modal-dialog button.uc_cancel_shortcut', function() {
5577 var $container = $(this).closest('li');
5578 $container.find('span.uc_change_shortcut').show();
5579 $container.find('div.uc_shortcut_elements').html('');
5580 });
5581
5582 UpdraftCentral.register_event_handler('change', '.modal-dialog div.uc_shortcut_elements select', function(event) {
5583 $('.modal-dialog').trigger('keyboard_shortcut_changed', [this]);
5584 });
5585
5586 UpdraftCentral.register_event_handler('keyup', '.modal-dialog div.uc_shortcut_elements input', function(event) {
5587 var $container = $(this).closest('li').find('div.uc_shortcut_elements');
5588 var character = $container.find('input[type="text"]').val();
5589
5590 if (character.match(/^[a-zA-Z]$/g)) {
5591 $container.find('button.uc_save_shortcut').prop('disabled', false);
5592 } else {
5593 $container.find('button.uc_save_shortcut').prop('disabled', true);
5594 }
5595
5596 $('.modal-dialog').trigger('keyboard_shortcut_changed', [this]);
5597 });
5598
5599 UpdraftCentral.register_event_handler('keyboard_shortcut_changed', '.modal-dialog', function(event, element) {
5600 var $container = $(element).closest('li').find('div.uc_shortcut_elements');
5601 var $tip = $container.find('span.uc_new_shortcut');
5602
5603 // Save options
5604 var modifier = $container.find('select').val();
5605 var character = $container.find('input[type="text"]').val();
5606
5607 var connector = '+';
5608 if ('undefined' === typeof modifier || !modifier.length) {
5609 connector = '';
5610 modifier = '';
5611 }
5612 if (!character || !character.length) connector = '';
5613
5614 var new_shortcut = modifier+connector+character;
5615 if (character && character.length) {
5616 if (character.match(/^[a-zA-Z]$/g)) {
5617 $tip.html('<div class="uc_tip_wrapper">'+udclion.keyboard_new_shortcut+': '+new_shortcut+'</div>');
5618 } else {
5619 $tip.html('<div class="uc_tip_wrapper">'+udclion.keyboard_invalid_key+'</div>');
5620 }
5621 } else {
5622 $tip.html('');
5623 $container.find('button.uc_save_shortcut').prop('disabled', false);
5624 }
5625 });
5626
5627 UpdraftCentral.register_event_handler('click', 'a.uc_clear_local_shortcuts', function() {
5628 var $container = $('.bootbox.modal ul.uc_shortcuts');
5629 var $spinner_where = $('.bootbox.modal div.uc_shortcuts_spinner');
5630
5631 self.clear_shortcuts($spinner_where).then(function(response) {
5632 if ('undefined' !== typeof response.shortcuts) {
5633 udclion.user_defined_shortcuts = response.shortcuts;
5634 }
5635
5636 self.init(function() {
5637 display_available_shortcuts($container);
5638 });
5639 });
5640 });
5641 }
5642
5643 /**
5644 * Registers UpdraftCentral premium feature's shortcuts if not available
5645 *
5646 * N.B. Usually, the registration of these shortcuts are done from the premium module's code
5647 * itself by calling the add_filter('updraftcentral_keyboard_shortcuts', ...) inside their
5648 * respective loader.php (please see updraftcentral/modules/updates/loader.php for sample)
5649 *
5650 * @returns {void}
5651 */
5652 var maybe_register_premium_shortcuts = function() {
5653
5654 var features = [
5655 {
5656 name: 'show_comments',
5657 key: 'C',
5658 description: udclion.show_comments,
5659 menu: 'comments',
5660 action: 'updraftcentral_site_comments_manage'
5661 },
5662 {
5663 name: 'show_users',
5664 key: 'U',
5665 description: udclion.show_users,
5666 menu: 'users',
5667 action: 'updraftcentral_site_users_manage'
5668 },
5669 {
5670 name: 'show_analytics',
5671 key: 'A',
5672 description: udclion.show_analytics,
5673 menu: 'analytics',
5674 action: 'updraftcentral_site_analytics_show'
5675 }
5676 ];
5677
5678 for (var i=0; i<features.length; i++) {
5679 var shortcut = features[i];
5680 if ('undefined' === typeof udclion.keyboard_shortcuts[shortcut.name]) {
5681 if (shortcut.menu.length && $('#updraft-menu-item-'+shortcut.menu).is(':visible')) {
5682 var info = {
5683 name: shortcut.name,
5684 key: shortcut.key,
5685 description: shortcut.description,
5686 menu: shortcut.menu
5687 }
5688 self.register_shortcut(info, shortcut.action, true);
5689 }
5690 }
5691 }
5692 }
5693
5694 /**
5695 * Loads locally registered shortcuts
5696 *
5697 * N.B. Shortcuts mostly registered for the help list and any local overrides
5698 * that needs custom or extra process other than just openning the sections/tabs.
5699 *
5700 * @returns {void}
5701 */
5702 var load_local_shortcuts = function() {
5703 // Register a shortcut for the "Keyboard Shortcuts" dialog. Menu property is empty since we're passing
5704 // a callback function as action instead of a common button id or class name.
5705 var info = {
5706 name: 'z_keyboard_shortcuts_help',
5707 key: 'K',
5708 description: udclion.keyboard_shortcuts_help,
5709 menu: '',
5710 site_required: false
5711 }
5712
5713 self.register_shortcut(info, function(name) {
5714 display_available_shortcuts();
5715 }, true);
5716
5717
5718 // Before loading or executing the Single Updates shortcut we make sure that the site's container
5719 // is visible before loading the content.
5720 var update_info = {
5721 name: 'site_update',
5722 key: 'D',
5723 description: udclion.site_update,
5724 menu: 'updates'
5725 }
5726
5727 self.register_shortcut(update_info, function(name, menu) {
5728 var $site_row = UpdraftCentral.$site_row;
5729 if (menu.length) {
5730 var menu_active = $('#updraft-menu-item-'+menu+'.updraft-menu-item-links-active');
5731 if (!menu_active.length) {
5732 $('#updraft-menu-item-'+menu).trigger('click');
5733 }
5734 }
5735
5736 if ('undefined' === typeof $site_row || !$site_row) {
5737 var $parent = $('div#updraftcentral_dashboard_existingsites > div.ui-sortable-handle:first-child');
5738 var $site_row = $parent.find('.updraftcentral_site_row');
5739 }
5740
5741 $mass_updates_container = $site_row.closest('#updraftcentral_dashboard_existingsites').find('#updates_container');
5742 if ('undefined' !== typeof $mass_updates_container && $mass_updates_container.length) {
5743 $mass_updates_container.remove();
5744 }
5745
5746 var button = $site_row.find('button.updraftcentral_action_show_updates');
5747 if ('undefined' !== typeof button && button.length) {
5748 // Clean container first before loading if we're on the "updates" menu since
5749 // single site and mass updates share the same menu context.
5750 if ('undefined' !== typeof menu && 'updates' === menu) {
5751 $site_row.find('.updraftcentral_row_extracontents').html('');
5752 }
5753
5754 button.trigger('click');
5755 }
5756
5757 $site_row.show();
5758 $site_row.closest('.ui-sortable-handle').show();
5759 }, true);
5760 }
5761 }
5762
5763 /**
5764 * Progress Bar Class
5765 *
5766 * A generic progress bar class that uses the jQuery
5767 * progressbar widget.
5768 *
5769 * @constructor
5770 * @uses {jQuery.ui.progressbar}
5771 */
5772 function UpdraftCentral_Task_Progress() {
5773 var self = this;
5774 var $ = jQuery;
5775 var $progress_bar;
5776 var $container;
5777 var processed_tasks;
5778 var custom_status;
5779 var current_task;
5780 this.total_items;
5781
5782 /**
5783 * Displays message to the progress bar
5784 *
5785 * @param {boolean} detailed Indicates whether to show the percent completion label or not
5786 * @returns {void}
5787 */
5788 var show_progress = function(detailed) {
5789 var status = $progress_bar.progressbar('option', 'value') + '%';
5790 if ('undefined' !== typeof custom_status && custom_status.length) {
5791 if ('undefined' !== typeof detailed && detailed) {
5792 status = custom_status+' '+status;
5793 } else {
5794 status = custom_status;
5795 }
5796 }
5797 $container.find('#uc_task_progress_status').text(status);
5798 }
5799
5800 /**
5801 * Renders and displays the progress bar html
5802 *
5803 * @returns {void}
5804 * @uses {jQuery.ui.progressbar}
5805 */
5806 var render = function() {
5807 if ($container.length == 0) {
5808 if (UpdraftCentral.get_debug_level() > 0) {
5809 console.log('UpdraftCentral_Task_Progress: Progress bar container not defined - exiting');
5810 }
5811 return;
5812 }
5813
5814 if (typeof $.ui !== 'undefined' && typeof $.ui.progressbar === 'function') {
5815 var $progress_container = $container.find('#uc_task_progress');
5816
5817 if ($progress_container.length === 0) {
5818 $container.append('<div id="uc_task_progress"><div id="uc_task_progress_bar"><div id="uc_task_progress_status"></div></div></div>');
5819 }
5820
5821 $progress_bar = $container.find('#uc_task_progress > div#uc_task_progress_bar');
5822 if (!$progress_bar.is(':ui-progressbar')) {
5823 $progress_bar.progressbar({
5824 max: 100,
5825 value: 0,
5826 change: function() {
5827 if ($container) {
5828 if (!$('.updraftcentral_spinner').is(':visible')) {
5829 $($container).prepend('<div class="updraftcentral_spinner"></div>');
5830 }
5831 }
5832 show_progress(true);
5833 },
5834 complete: function() {;}
5835 });
5836 } else {
5837 $progress_bar.progressbar('option', 'value', 0);
5838 }
5839 } else {
5840 if (UpdraftCentral.get_debug_level() > 0) {
5841 console.log('UpdraftCentral_Task_Progress: jQuery progressbar or one of its dependency is not installed - exiting');
5842 }
5843 return;
5844 }
5845 }
5846
5847 /**
5848 * Initializes local variables
5849 *
5850 * Checks whether we have a valid container where to display
5851 * the progress bar widget. Otherwise, it will throw an error.
5852 *
5853 * @returns {void}
5854 */
5855 var init = function() {
5856 if (typeof $container === 'undefined') {
5857 if (UpdraftCentral.get_debug_level() > 0) {
5858 console.log('UpdraftCentral_Task_Progress: A container element is required. This is where the progress bar is to be appended - exiting');
5859 }
5860 return;
5861 }
5862 self.total_items = 0;
5863 processed_tasks = 0;
5864 custom_status = '';
5865 current_task = null;
5866 }
5867 init();
5868
5869 /**
5870 * Stores the currently run task
5871 *
5872 * @param {object} task The task object currently running
5873 * @returns {void}
5874 */
5875 this.current = function(task) {
5876 current_task = task;
5877 }
5878
5879 /**
5880 * Sets a custom message displayed as a progress bar status
5881 *
5882 * @param {string} message Text to display on the progress bar
5883 * @param {boolean} show_completion Show percent completion details
5884 * @returns {void}
5885 */
5886 this.set_custom_status = function(message, show_completion) {
5887 var status = $container.find('#uc_task_progress_status');
5888 if (status.length == 0) return;
5889
5890 if ('undefined' !== typeof message && message) {
5891 custom_status = message;
5892 status.text(message);
5893 }
5894
5895 var detailed = ('undefined' !== typeof show_completion && show_completion) ? true : false;
5896 show_progress(detailed);
5897 }
5898
5899 /**
5900 * Setting the progressbar completion with custom message support
5901 *
5902 * @param {string} message - An optional message to display on the progress bar.
5903 * If not empty, will be used to indicate that the process
5904 * has already been completed.
5905 * @returns {void}
5906 */
5907 this.set_complete = function(message) {
5908 current_bar = 100;
5909 $progress_bar.progressbar('option', 'value', current_bar);
5910
5911 message = ('undefined' !== typeof message) ? message : udclion.process_completed;
5912 self.set_custom_status(message);
5913 }
5914
5915 /**
5916 * Updates the progress bar data by recomputing the current progress
5917 *
5918 * @returns {void}
5919 */
5920 this.update = function() {
5921 // Increment the processed_tasks variable everything update has been called
5922 processed_tasks++;
5923
5924 // Calculates the current progress percentage and updates
5925 // The progress bar value with the result of the computation.
5926 var total = parseInt(self.total_items);
5927 var new_value = parseInt(100 * (processed_tasks / total));
5928 if (new_value < 100) {
5929 $progress_bar.progressbar('option', 'value', new_value);
5930 }
5931 }
5932
5933 /**
5934 * Sets the container where the progress bar should be appended
5935 *
5936 * @param {object} $new_container - A jQuery object/element that will hold the
5937 * rendered progress bar.
5938 * @returns {void}
5939 */
5940 this.set_container = function($new_container) {
5941 $container = $new_container;
5942 render();
5943 }
5944
5945 /**
5946 * Updates the status message on error
5947 *
5948 * @param {string} message - An optional message to display as opposed to the default message.
5949 */
5950 this.abort = function(message) {
5951 message = ('undefined' !== typeof message) ? message : udclion.process_aborted;
5952 self.set_custom_status(message);
5953 }
5954
5955 /**
5956 * Resets the progress bar to its initial state
5957 *
5958 * @returns {void}
5959 */
5960 this.reset = function() {
5961 var $progress_container = $container.find('#uc_task_progress');
5962 if ($progress_container.length == 0) return;
5963
5964 self.clear();
5965 $progress_container.remove();
5966
5967 var spinner = $container.find('.updraftcentral_spinner');
5968 if ('undefined' !== typeof spinner && spinner.length && spinner.is(':visible')) {
5969 spinner.remove();
5970 }
5971 }
5972
5973 /**
5974 * Clears variables in preparation for a new process
5975 *
5976 * @returns {void}
5977 */
5978 this.clear = function() {
5979 processed_tasks = 0;
5980 self.total_items = 0;
5981 custom_status = '';
5982 current_task = null;
5983 }
5984
5985 /**
5986 * Hides the progress bar widget
5987 *
5988 * @returns {void}
5989 */
5990 this.hide = function() {
5991 var $progress_container = $container.find('#uc_task_progress');
5992 if ($progress_container.length == 0) return;
5993
5994 $progress_container.hide();
5995 }
5996
5997 /**
5998 * Shows the progress bar widget
5999 *
6000 * @returns {void}
6001 */
6002 this.show = function() {
6003 var $progress_container = $container.find('#uc_task_progress');
6004 if ($progress_container.length == 0) return;
6005
6006 $progress_container.show();
6007 }
6008 }
6009
6010 /**
6011 * Generic Collection Class
6012 *
6013 * Serves as a generic storage for all kinds of uses.
6014 *
6015 * @constructor
6016 */
6017 function UpdraftCentral_Collection() {
6018 var self = this;
6019 var count = 0;
6020 var collection = {};
6021
6022 /**
6023 * Adds an item with a specified key to the collection
6024 *
6025 * @param {string} key - A unique identifier for the item.
6026 * @param {object|array|string|number|boolean} item - Can be of any type.
6027 * @returns {boolean}
6028 */
6029 this.add = function(key, item) {
6030 if (!self.exists(key)) {
6031 collection[key] = item;
6032 count++;
6033 return true;
6034 }
6035 return false;
6036 }
6037
6038 /**
6039 * Updates a collection item with a specified key
6040 *
6041 * @param {string} key - A unique identifier for the item.
6042 * @param {object|array|string|number|boolean} item - The updated item. Can be of any type.
6043 * @returns {boolean}
6044 */
6045 this.update = function(key, item) {
6046 if (self.exists(key)) {
6047 collection[key] = item;
6048 return true;
6049 } else {
6050 return self.add(key, item);
6051 }
6052 return false;
6053 }
6054
6055 this.bulk_update = function(data) {
6056 for (var prop in data) {
6057 self.update(prop, data[prop]);
6058 }
6059 }
6060
6061 /**
6062 * Removes an item with a specified key from the collection
6063 *
6064 * @param {string} key - The identifier of the item to be removed.
6065 * @returns {boolean}
6066 */
6067 this.remove = function(key) {
6068 if (self.exists(key)) {
6069 delete collection[key];
6070 count--;
6071 return true;
6072 }
6073 return false;
6074 }
6075
6076 /**
6077 * Retrieves an item with a specified key from the collection
6078 *
6079 * @param {string} key - The identifier of the item to be retrieved.
6080 * @returns {object|array|string|number|boolean}
6081 */
6082 this.item = function(key) {
6083 return collection[key];
6084 }
6085
6086 /**
6087 * Returns all available keys from the collection
6088 *
6089 * @returns {array}
6090 */
6091 this.keys = function() {
6092 var keys = [];
6093 for (var k in collection) keys.push(k);
6094
6095 return keys;
6096 }
6097
6098 /**
6099 * Checks whether an item with a specified key exists in the collection
6100 *
6101 * @param {string} key - The identifier of the item to be checked.
6102 * @returns {boolean}
6103 */
6104 this.exists = function(key) {
6105 return ('undefined' !== typeof collection[key]);
6106 }
6107
6108 /**
6109 * Empty or resets the collection
6110 *
6111 * @returns {void}
6112 */
6113 this.clear = function() {
6114 count = 0;
6115 collection = {};
6116 }
6117
6118 /**
6119 * Returns the number of items found in the collection
6120 *
6121 * @returns {number}
6122 */
6123 this.count = function() {
6124 return count;
6125 }
6126
6127 /**
6128 * Returns all items in the collection
6129 *
6130 * @returns {array}
6131 */
6132 this.get_items = function() {
6133 var items = [];
6134 for (var k in collection) items.push(collection[k]);
6135
6136 return items;
6137 }
6138
6139 this.get_collection_object = function() {
6140 return collection;
6141 }
6142 }
6143
6144 /**
6145 * A Tasks Runner Function
6146 *
6147 * Runs and execute queued tasks using the d3queue library. This class
6148 * was created in preparation for the mass updates feature of all sites
6149 * under the UpdraftCentral plugin.
6150 *
6151 * @constructor
6152 * @see {UpdraftCentral_Collection}
6153 * @see {d3queue}
6154 * @param {object} options - Task runner's options
6155 * @param {number} options.concurrency - Number of concurrency needed to run the process
6156 */
6157 function UpdraftCentral_Tasks_Runner(options) {
6158 var self = this;
6159 var options = options || {};
6160 var storage;
6161 this.progress;
6162
6163 /**
6164 * Initialize and/or checks required process variables
6165 *
6166 * @private
6167 * @returns {void}
6168 * @uses {UpdraftCentral_Collection}
6169 * @uses {UpdraftCentral_Task_Progress}
6170 * @uses {d3queue}
6171 */
6172 var init = function() {
6173 storage = new UpdraftCentral_Collection();
6174 self.progress = new UpdraftCentral_Task_Progress();
6175 }
6176 init();
6177
6178 /**
6179 * Runs a specified task
6180 *
6181 * @private
6182 * @param {object} task - An object containing the callback function to be executed and its arguments.
6183 * @param {function} task.func - A deferred function that returns a jQuery promise object that
6184 * @param {array} task.args - An array containing the arguments of the deferred function.
6185 * @param {function} callback - A callback function that will be executed after the task is run.
6186 * A pre-requisite of the d3queue library.
6187 */
6188 var run_task = function(task, callback) {
6189 self.progress.current(task);
6190 task.func.apply(null, task.args).then(function(result) {
6191 callback(null, result);
6192 }).fail(function(result) {
6193 var error = result;
6194 if (typeof error === 'undefined') error = true;
6195
6196 callback(error);
6197 }).always(function(result) {
6198 self.progress.update();
6199 });
6200 }
6201
6202 /**
6203 * Returns the total count of the tasks currently
6204 * being queued for process
6205 *
6206 * @returns {number} - The total count of the queued items/tasks
6207 */
6208 this.tasks_count = function() {
6209 return storage.count();
6210 }
6211
6212 /**
6213 * Removes the specified task from the collection of queued tasks
6214 *
6215 * @param {string} task_key - The generated key produced when the task was
6216 * successfully added.
6217 * @returns {boolean} - "True" if task was successfully removed, "False" otherwise.
6218 */
6219 this.remove_task = function(task_key) {
6220 return storage.remove(task_key);
6221 }
6222
6223 /**
6224 * Adds task to execute
6225 *
6226 * @uses {UpdraftCentral_Library.md5}
6227 * @param {function} callback - A function or process that will be executed when the task is run.
6228 * @param {array} args - The arguments or parameters to the callback function.
6229 * @returns {boolean|string} - The associated key if the task was successfully added, "False" otherwise.
6230 */
6231 this.add_task = function(callback, args) {
6232 var options = options || {};
6233
6234 if (typeof callback === 'function') {
6235 var timestamp = new Date().getTime();
6236 var rand = Math.ceil(Math.random()*1000);
6237 var key = UpdraftCentral_Library.md5('_key_' + timestamp + rand);
6238
6239 var task = {
6240 func: callback,
6241 args: args
6242 }
6243
6244 // Add reference to the class instance that will
6245 // Serve as a context when using the progress bar.
6246 // This will be appended as the last argument to the
6247 // Submitted callback above.
6248 task.args.push(self);
6249
6250 if (storage.add(key, task)) {
6251 return key;
6252 }
6253 }
6254 return false;
6255 }
6256
6257 /**
6258 * Clears all previously saved tasks
6259 *
6260 * @returns {void}
6261 */
6262 this.clear_tasks = function() {
6263 self.progress.clear();
6264 storage.clear();
6265 }
6266
6267 /**
6268 * Aborts all active tasks
6269 *
6270 * @returns {void}
6271 */
6272 this.abort = function() {
6273 queue.abort();
6274 self.clear_tasks();
6275 }
6276
6277 /**
6278 * Process all tasks in queue
6279 *
6280 * @returns {object} - A jQuery promise
6281 * @uses {jQuery.Deferred}
6282 * @uses {d3queue}
6283 */
6284 this.process_tasks = function() {
6285 var deferred = jQuery.Deferred();
6286
6287 if (storage.count() > 0) {
6288 self.progress.total_items = storage.count();
6289
6290 // So far, the safest concurrency we can add is 6 basing on research and with actual
6291 // observation on performance when we run tasks more than 40. Most browsers has 6 on
6292 // minimum and varies on the maximum. Thus, we will leave it at 6 for now.
6293 var max_http_connection = udclion.hasOwnProperty('max_http_connection') ? parseInt(udclion.max_http_connection) : 6;
6294 var queue = d3.queue(max_http_connection);
6295
6296 var keys = storage.keys();
6297 for (var i=0; i<keys.length; i++) {
6298 var task = storage.item(keys[i]);
6299 queue.defer(run_task, task);
6300 }
6301
6302 // Executes when all tasks has been completed or aborted due to error
6303 // Or clicking the abort button itself
6304 queue.awaitAll(function(error, data) {
6305 if (error) {
6306 deferred.reject(error);
6307 } else {
6308 // Returns an array of results
6309 deferred.resolve(data);
6310 }
6311 });
6312 } else {
6313 deferred.reject(udclion.tasks_queue_empty);
6314 }
6315
6316 return deferred.promise();
6317 }
6318
6319 }
6320
6321 /**
6322 * Abstraction Layer/Class for Site Credentials
6323 *
6324 * @constructor
6325 * @see {UpdraftCentral_Collection}
6326 * @see {UpdraftCentral_Queueable_Modal}
6327 */
6328 function UpdraftCentral_Credentials() {
6329 var self = this;
6330 var storage;
6331 var $modal;
6332 var close_event;
6333
6334 /**
6335 * Initializes and/or checks variables or parameters
6336 *
6337 * @private
6338 * @returns {void}
6339 */
6340 var init = function() {
6341 storage = new UpdraftCentral_Collection();
6342 $modal = jQuery('#updraftcentral_modal_dialog');
6343 close_event = 'hidden.bs.modal';
6344 }
6345 init();
6346
6347 /**
6348 * Gets the credentials of the given site
6349 *
6350 * It automatically opens the credential's form when there's no sufficient permission to edit
6351 * or upgrade one or more plugins, themes or the WP core.
6352 *
6353 * @param {object} site - A UpdraftCentral_Site object containing all possible information relating to the Site.
6354 * @returns {string} - A serialized string representing the site's credentials in encoded format.
6355 * @borrows UpdraftCentral.get_site_heading
6356 * @borrows UpdraftCentral.open_modal
6357 * @borrows UpdraftCentral.close_modal
6358 * @borrows UpdraftCentral_Library.unserialize
6359 * @borrows UpdraftCentral_Library.dialog.alert
6360 */
6361 this.get_credentials = function(site) {
6362 var deferred = jQuery.Deferred();
6363 var $site_row = site.site_row;
6364 var site_id = site.id;
6365
6366 if (storage.exists(site_id)) {
6367 var credentials = storage.item(site_id);
6368 var requests = credentials.request_filesystem_credentials || {};
6369
6370 var show_form = false;
6371 var entity;
6372 for (var item in requests) {
6373 if (requests[item]) {
6374 show_form = true;
6375 entity = item;
6376 }
6377 }
6378
6379 if (typeof credentials.site_credentials === 'undefined') {
6380 if (show_form) {
6381 var possible_credentials = UpdraftCentral.storage_get('filesystem_credentials_'+site.site_hash);
6382 var site_heading = UpdraftCentral.get_site_heading($site_row);
6383
6384 UpdraftCentral.open_modal(udclion.updates.connection_information, UpdraftCentral.template_replace('updates-request-credentials', {
6385 credentials_form: credentials.filesystem_form,
6386 site_heading: site_heading
6387 }), function() {
6388 var save_credentials_in_browser = jQuery('#updraftcentral_modal #filesystem-credentials-save-in-browser').is(':checked');
6389 var site_credentials = jQuery('#updraftcentral_modal .request-filesystem-credentials-dialog-content input').serialize();
6390
6391 validate_remote_credentials($site_row, entity, site_credentials).then(function(response) {
6392 credentials.site_credentials = site_credentials;
6393 site.site_credentials = site_credentials;
6394
6395 if (save_credentials_in_browser) {
6396 site.save_credentials_in_browser = true;
6397 }
6398
6399 deferred.resolve(site);
6400 }).fail(function(response, code, error_code) {
6401 if (UpdraftCentral.get_debug_level() > 0) {
6402 console.log("Failed result follows for 'validate_remote_credentials' method called within UpdraftCentral_Credentials.get_credentials:");
6403 console.log(response);
6404 }
6405
6406 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.failed_credentials_heading+'</h2><p>'+udclion.failed_credentials+'</p>');
6407 deferred.reject(response, code, error_code);
6408 }).always(function() {
6409 $modal.off(close_event);
6410 UpdraftCentral.close_modal();
6411 });
6412 }, udclion.updates.update, function() {
6413 jQuery('#updraftcentral_modal .request-filesystem-credentials-dialog-content input[value=""]:first').trigger('focus');
6414
6415 if (possible_credentials) {
6416 saved_credentials = UpdraftCentral_Library.unserialize(possible_credentials);
6417 if (saved_credentials) {
6418
6419 jQuery.each(saved_credentials, function(index, value) {
6420 var type = jQuery('#updraftcentral_modal .request-filesystem-credentials-dialog-content input[name="'+index+'"]').attr('type');
6421 if ('text' == type || 'number' == type || 'password' == type) {
6422 jQuery('#updraftcentral_modal .request-filesystem-credentials-dialog-content input[name="'+index+'"]').val(value);
6423 } else if ('checkbox' == type) {
6424 if (value) {
6425 jQuery('#updraftcentral_modal .request-filesystem-credentials-dialog-content input[name="'+index+'"]').prop('checked', true);
6426 } else {
6427 jQuery('#updraftcentral_modal .request-filesystem-credentials-dialog-content input[name="'+index+'"]').prop('false', true);
6428 }
6429 } else if ('radio' == type) {
6430 jQuery('#updraftcentral_modal .request-filesystem-credentials-dialog-content input[name="'+index+'"][value="'+value+'"]').prop('checked', true);
6431 } else if (type) {
6432 console.log("UpdraftCentral: unrecognised field type in credential form: type="+type+", field index="+index);
6433 }
6434 });
6435
6436 jQuery('#updraftcentral_modal #filesystem-credentials-save-in-browser').prop('checked', true);
6437 }
6438 }
6439
6440 // We actually don't have any control on the form since it is a local implementation of the site WP instance, and
6441 // Each versions of WP may or may not have a difference in terms of form contents.
6442 // Thus, we're making sure we only get one "Connection Type" heading, by hiding
6443 // Others if there are any.
6444 jQuery('#updraftcentral_modal div#request-filesystem-credentials-form fieldset > legend:contains("Connection Type"):gt(0)').hide();
6445
6446 // Here, we're listening to the close event of the modal.
6447 // If the user, for some reason close the modal without clicking the "Update" button
6448 // We return it as fail, so that the process can safely return to it's original state for
6449 // A fresh restart of the process (the restart is handled by the consumer of this promise object)
6450 $modal.on(close_event, function() {
6451 deferred.reject();
6452 });
6453 }, true, '', function() {
6454 // User clicks either the "X" or close button of the modal without
6455 // going into the validation process for credentials
6456 deferred.reject();
6457 });
6458 } else {
6459 deferred.reject(site);
6460 }
6461 } else {
6462 site.site_credentials = credentials.site_credentials;
6463 deferred.resolve(site);
6464 }
6465 } else {
6466 deferred.reject(site);
6467 }
6468
6469 return deferred.promise();
6470 }
6471
6472 /**
6473 * Pre-load the credentials of the given site (single loading)
6474 *
6475 * @param {object} site - A UpdraftCentral_Site object containing all possible information relating to the Site.
6476 * @returns {object} - A jQuery promise with the response from "get_remote_credentials" process
6477 * @borrows get_remote_credentials
6478 */
6479 this.load_credentials = function(site) {
6480 var deferred = jQuery.Deferred();
6481 var site_id = site.id;
6482 var $site_row = site.site_row;
6483
6484 if (typeof site_id !== 'undefined') {
6485 if (storage.exists(site_id)) {
6486 deferred.resolve(storage.item(site_id));
6487 } else {
6488 get_remote_credentials($site_row).then(function(response) {
6489 storage.add(site_id, response);
6490 deferred.resolve(response);
6491 }).fail(function(response, code, error_code) {
6492 deferred.reject(response, code, error_code);
6493 });
6494 }
6495 } else {
6496 deferred.reject();
6497 }
6498 return deferred.promise();
6499 }
6500
6501 /**
6502 * Pre-load all credentials of the given sites (mass loading)
6503 *
6504 * Intended for bulk or mass retrieval of site credentials. Extract each site's credentials and load them into
6505 * a storage which can be called and used for whatever purpose it may serve later on (e.g. mass updates, etc.).
6506 *
6507 * @param {array} sites - An array of UpdraftCentral_Site objects representing a Site
6508 * @returns {object} - A jQuery promise with the response from "get_remote_credentials" process
6509 * @borrows get_remote_credentials
6510 */
6511 this.load_all_credentials = function(sites) {
6512 var deferred = jQuery.Deferred();
6513 var processed_sites = [];
6514
6515 if (typeof sites !== 'undefined' && sites.length > 0) {
6516 for (var i=0; i < sites.length; i++) {
6517 var site = sites[i];
6518 var $site_row = site.site_row;
6519
6520 processed_sites.push(get_remote_credentials($site_row).then(function(response) {
6521 var site_id = site.id;
6522
6523 if (typeof site_id !== 'undefined') {
6524 storage.add(site_id, response);
6525 }
6526 }));
6527 }
6528
6529 jQuery.when.apply(jQuery, processed_sites).then(function() {
6530 deferred.resolve();
6531 });
6532 }
6533
6534 return deferred.promise();
6535 }
6536
6537 /**
6538 * Retrieves any available credentials information from the "updates.get_updates"
6539 * cached data if available and it is still good to use meaning the data is less than 12 hours
6540 *
6541 * @param {integer} site_id The ID of the currently processed site
6542 * @returns {object|boolean}
6543 */
6544 var maybe_retrieve_credentials_info = function(site_id) {
6545 // N.B. The best place to look for any available creds info is within the 'updates.get_updates' command if they
6546 // exists in the site_info array since the 'updates.get_updates' response has a "meta" field that already contains
6547 // the "request_filesystem_credentials" and "filesystem_form" informations of the requested site. So, instead of
6548 // sending a new remote request we just have to retrieve the cached info and return them if available, otherwise,
6549 // we'll proceed in sending the request to the remote site.
6550 //
6551 // If found, this cuts the loading process within the "updates" area whether for the individual site or mass
6552 // updates in half.
6553 var cached_response = UpdraftCentral.get_cached_response(site_id, 'updates.get_updates');
6554 if (null !== cached_response && UpdraftCentral.is_data_good_to_use(cached_response.created)) {
6555 var data = cached_response.reply.data;
6556 if (data.hasOwnProperty('meta') && data.meta) {
6557 return data.meta;
6558 }
6559 }
6560
6561 return false;
6562 }
6563
6564 /**
6565 * Gets the credentials from the remote server
6566 *
6567 * @returns {object} - A jQuery promise with the response from the server
6568 * @borrows {UpdraftCentral.send_site_rpc}
6569 */
6570 var get_remote_credentials = function($site_row) {
6571 var deferred = jQuery.Deferred();
6572 var info = maybe_retrieve_credentials_info($site_row.data('site_id'));
6573 if (false !== info) {
6574 deferred.resolve(info);
6575 } else {
6576 UpdraftCentral.send_site_rpc('core.get_credentials', null, $site_row, function(response, code, error_code) {
6577 if (code === 'ok' && response) {
6578 deferred.resolve(response.data);
6579 } else {
6580 deferred.reject(response, code, error_code);
6581 return true;
6582 }
6583 });
6584 }
6585
6586 return deferred.promise();
6587 }
6588
6589 /**
6590 * Validates the newly entered credentials
6591 *
6592 * @returns {object} - A jQuery promise with the response from the server
6593 * @borrows {UpdraftCentral.send_site_rpc}
6594 */
6595 var validate_remote_credentials = function($site_row, entity, credentials) {
6596 var deferred = jQuery.Deferred();
6597
6598 var creds = {
6599 entity: entity,
6600 filesystem_credentials: credentials
6601 };
6602
6603 UpdraftCentral.send_site_rpc('core.validate_credentials', creds, $site_row, function(response, code, error_code) {
6604 if (code === 'ok' && !response.data.error) {
6605 deferred.resolve(response.data);
6606 } else {
6607 deferred.reject(response, code, error_code);
6608 }
6609 });
6610
6611 return deferred.promise();
6612 }
6613
6614 }
6615
6616 /**
6617 * Site Class
6618 *
6619 * A convenient way of containing and pulling site information and passing
6620 * it across the code. Solely created for abstraction purposes and in preparation
6621 * for the mass updates process.
6622 *
6623 * @constructor
6624 */
6625 function UpdraftCentral_Site($site_row) {
6626 var self = this;
6627 this.id;
6628 this.site_description;
6629 this.site_url;
6630 this.site_hash;
6631 this.site_row = $site_row;
6632 this.save_credentials_in_browser;
6633 this.site_credentials;
6634 this.credentials_required;
6635 this.automatic_backups;
6636 this.autobackup_options;
6637 this.autobackup_requested;
6638 this.autobackup_complete;
6639 this.updates;
6640 this.update_requests;
6641 this.update_processing;
6642 this.additional_options;
6643 this.mass_update;
6644 this.update_queue;
6645
6646 /**
6647 * Initializes variables and containers.
6648 *
6649 * @see {UpdraftCentral_Collection}
6650 * @borrows {UpdraftCentral_Library.md5}
6651 */
6652 var init = function() {
6653 self.id = self.site_row.data('site_id');
6654 self.site_description = self.site_row.data('site_description');
6655 self.site_url = self.site_row.data('site_url');
6656 self.site_hash = UpdraftCentral_Library.md5(self.id + '_' + self.site_row.data('site_url'));
6657 self.save_credentials_in_browser = false;
6658 self.site_credentials = false;
6659 self.credentials_required = false;
6660 self.automatic_backups = false;
6661 self.autobackup_options = {};
6662 self.autobackup_requested = false;
6663 self.autobackup_complete = false;
6664 self.backup_completed = false;
6665 self.updates = {
6666 plugin: new UpdraftCentral_Collection(),
6667 theme: new UpdraftCentral_Collection(),
6668 core: new UpdraftCentral_Collection(),
6669 translation: new UpdraftCentral_Collection()
6670 };
6671 self.update_requests = new UpdraftCentral_Queue();
6672 self.update_processing = false;
6673 self.mass_update = false;
6674 self.update_queue = new UpdraftCentral_Collection();
6675 }
6676 init();
6677
6678 /**
6679 * Gets the update information of a certain item
6680 *
6681 * @param {string} entity - A string representing an entity (plugin, theme, core)
6682 * @param {string} key - A string identifier for an item for updates under a certain entity
6683 * @returns {boolean}
6684 */
6685 this.get_update_info = function(entity, key) {
6686 if (self.updates[entity].exists(key)) {
6687 return self.updates[entity].item(key);
6688 }
6689 return false;
6690 }
6691 }
6692
6693 /**
6694 * Abstraction Class for Modal Window Implementation (with Queue-able feature)
6695 *
6696 * N.B.:
6697 * This isn't a new modal implementation, instead it uses the legacy modal implementation
6698 * and enhancing it to add a queue-able feature.
6699 *
6700 * @constructor
6701 * @see {UpdraftCentral_Queue}
6702 * @see {UpdraftCentral}
6703 */
6704 function UpdraftCentral_Queueable_Modal(element) {
6705 var self = this;
6706 var close_event;
6707 var queue,
6708 listener_off = false,
6709 _element = element || jQuery('#updraftcentral_modal_dialog');
6710
6711 /**
6712 * Sets modal close handler/listener
6713 *
6714 * @private
6715 * @borrows {UpdraftCentral_Queue.dequeue}
6716 * @borrows {UpdraftCentral_Queue.is_empty}
6717 * @borrows {UpdraftCentral_Queue.unlock}
6718 * @borrows {UpdraftCentral_Queueable_Modal.open_modal}
6719 * @returns {void}
6720 */
6721 var set_modal_close_listener = function() {
6722 if (typeof _element !== 'undefined') {
6723 _element.on(close_event, function() {
6724 if (!queue.is_empty()) {
6725 var options = queue.dequeue();
6726 if (typeof options !== 'undefined') {
6727 open_modal(options);
6728 }
6729 } else {
6730 listener_off = true;
6731 _element.off(close_event);
6732 queue.unlock();
6733 }
6734 });
6735 } else {
6736 console.log('UpdraftCentral_Queueable_Modal: Modal element does not exist.');
6737 }
6738 }
6739
6740 /**
6741 * Initializes variable(s), queue and close listener
6742 *
6743 * @private
6744 * @see {UpdraftCentral_Queue}
6745 * @borrows {UpdraftCentral_Queueable_Modal.set_modal_close_listener}
6746 * @returns {void}
6747 */
6748 var init = function() {
6749 queue = new UpdraftCentral_Queue();
6750 close_event = 'hidden.bs.modal';
6751 set_modal_close_listener();
6752 }
6753 init();
6754
6755 /**
6756 * Opens or executes the legacy "open_modal" function from UpdraftCentral
6757 *
6758 * @private
6759 * @param {object} options - An object containing the legacy arguments of the modal window.
6760 * @borrows {UpdraftCentral.open_modal}
6761 * @returns {void}
6762 */
6763 var open_modal = function(options) {
6764 UpdraftCentral.open_modal(
6765 options.title,
6766 options.body,
6767 options.action_button_callback,
6768 options.action_button_text,
6769 options.pre_open_callback,
6770 options.sanitize_body,
6771 options.extra_classes
6772 );
6773 }
6774
6775 /**
6776 * Loads queued modal options/arguments from queue
6777 *
6778 * Basically, this will be the trigger method to load all items from queue,
6779 * since closing the modal will trigger another dequeuing process.
6780 *
6781 * @borrows {UpdraftCentral_Queue.get_lock}
6782 * @borrows {UpdraftCentral_Queue.dequeue}
6783 * @borrows {UpdraftCentral_Queueable_Modal.open_modal}
6784 */
6785 this.load = function() {
6786 if (!queue.is_empty() && queue.get_lock()) {
6787 var options = queue.dequeue();
6788 if (typeof options !== 'undefined') {
6789 open_modal(options);
6790 }
6791 }
6792 }
6793
6794 /**
6795 * Handles either opening a modal window immediately or queue
6796 * the information (e.g. modal options/arguments) for later use.
6797 *
6798 * @param {object} options - An object containing the legacy arguments of the modal window.
6799 * @param {boolean} enqueue - A flag that will determined if the information passed is to be queued.
6800 * @borrows {UpdraftCentral_Queue.enqueue}
6801 * @borrows {UpdraftCentral_Queue.is_empty}
6802 * @borrows {UpdraftCentral_Queueable_Modal.set_modal_close_listener}
6803 * @borrows {UpdraftCentral_Queueable_Modal.open_modal}
6804 * @returns {void}
6805 */
6806 this.open = function(options, enqueue) {
6807 if (typeof enqueue !== 'undefined' && enqueue) {
6808 if (!queue.is_locked()) {
6809 queue.enqueue(options);
6810 if (!queue.is_empty() && listener_off) {
6811 set_modal_close_listener();
6812 listener_off = false;
6813 }
6814 }
6815 } else {
6816 open_modal(options);
6817 }
6818 }
6819
6820 /**
6821 * Closes or executes the legacy "close_modal" function from UpdraftCentral
6822 *
6823 * @borrows {UpdraftCentral.close_modal}
6824 * @returns {void}
6825 */
6826 this.close = function() {
6827 UpdraftCentral.close_modal();
6828 }
6829
6830 /**
6831 * Gets the total count of queued items
6832 *
6833 * @borrows {UpdraftCentral_Queue.get_length}
6834 * @returns {number} - Total count of items in the queue
6835 */
6836 this.get_queue_item_count = function() {
6837 return queue.get_length();
6838 }
6839 }
6840
6841 /**
6842 * UpdraftCentral_Library
6843 */
6844 function Fn_UpdraftCentral_Library() {
6845 // Dialog methods - this is just an abstraction layer (currently onto Bootbox, http://bootboxjs.com), allowing us to easily swap to a different provider if we ever need to
6846 this.dialog = {};
6847
6848 var $ = jQuery;
6849
6850 var collection = new UpdraftCentral_Collection();
6851
6852 /**
6853 * Determines whether its argument represents a JavaScript number
6854 *
6855 * @param mixed value The value to evaluate
6856 *
6857 * @returns boolean
6858 */
6859 this.is_numeric = function(value) {
6860 if ('number' === typeof value) return true;
6861 if ('string' !== typeof value) return false;
6862
6863 return !isNaN(value) && !isNaN(parseFloat(value));
6864 }
6865
6866 /**
6867 * Parses the input value to its actual boolean representation if applicable
6868 *
6869 * @param mixed value The value to evaluate
6870 *
6871 * @returns boolean
6872 */
6873 this.parseBool = function(value) {
6874 // This effort may look unnecessary but it helps to prevent varying values that equates to boolean
6875 // which might give unexpected results if not properly converted to the expected type. Using
6876 // JSON.parse or a simple comparison logic may not always work in all cases, thus, we go the
6877 // extra length to prevent any unexpected bugs.
6878 if ('string' === typeof value && ('true' == value.toLowerCase() || '1' == value)) value = true;
6879 if ('string' === typeof value && ('false' == value.toLowerCase() || '0' == value)) value = false;
6880 if ('number' === typeof value && 1 == value) value = true;
6881 if ('number' === typeof value && 0 == value) value = false;
6882
6883 // For any type other than we expect for a boolean value we set it to "false" to return gracefully.
6884 if ('boolean' !== typeof value) value = false;
6885
6886 return value;
6887 }
6888
6889 /**
6890 * Sorts an array of objects
6891 *
6892 * @param {array} items Array of objects to sort
6893 * @param {string} field The field/attribute to sort
6894 * @param {string} order (Optional) The sort order requested (e.g. asc = ascending or desc = descending)
6895 *
6896 * @return {array|void}
6897 */
6898 this.sort = function(items, field, order) {
6899 if (!Array.isArray(items) || 'string' !== typeof field) return;
6900 if ('undefined' === typeof order) order = 'asc';
6901
6902 items.sort(function(a, b) {
6903 if ('object' === typeof a && a.hasOwnProperty(field) && 'object' === typeof b && b.hasOwnProperty(field)) {
6904 var item1 = a[field].toString().toLowerCase();
6905 var item2 = b[field].toString().toLowerCase();
6906
6907 if ('desc' === order) {
6908 return (item2 < item1) ? -1 : (item2 > item1) ? 1 : 0;
6909 } else {
6910 return (item1 < item2) ? -1 : (item1 > item2) ? 1 : 0;
6911 }
6912 }
6913
6914 return 0;
6915 });
6916
6917 return items;
6918 }
6919
6920 /**
6921 * Filter an array of objects
6922 *
6923 * @param {array} items Array of objects to filter
6924 * @param {array} fields An array of field names where the filter string should be run against
6925 * @param {string} keyword The filter string to search for from the filter fields
6926 * @param {boolean} validate_field_only Check only if fields exists on item(s)
6927 *
6928 * @param {array|void}
6929 */
6930 this.filter = function(items, fields, keyword, validate_field_only) {
6931 if (!Array.isArray(items) || !Array.isArray(fields) || 'string' !== typeof keyword) return;
6932
6933 var data = $.grep(items, function(obj) {
6934 var field_data = [];
6935 for (var i=0; i<fields.length; i++) {
6936 var field = fields[i];
6937
6938 if ('object' === typeof obj && obj.hasOwnProperty(field)) {
6939 field_data.push(obj[field].toString());
6940 }
6941 }
6942
6943 if (field_data.length) {
6944 if ('undefined' !== typeof validate_field_only && validate_field_only) {
6945 // Here, we're filtering only those items that has the fields submitted,
6946 // no string/content search whatsoever. Only plain field(s) validation if they exists.
6947 if (field_data.length === fields.length) return true;
6948 } else {
6949 if (1 === field_data.length && -1 == field_data[0].indexOf(',') && keyword.length) {
6950 return field_data[0].toLowerCase() === keyword.toLowerCase();
6951 } else {
6952 var found = false;
6953 for (var i=0; i<field_data.length; i++) {
6954 var data_index = field_data[i];
6955 if (data_index.toLowerCase() === keyword.toLowerCase()) {
6956 found = true;
6957 break;
6958 } else {
6959 if (-1 !== data_index.toLowerCase().indexOf(keyword.toLowerCase())) {
6960 found = true;
6961 break;
6962 }
6963 }
6964 }
6965
6966 return found;
6967 }
6968 }
6969 }
6970
6971 return false;
6972 });
6973
6974 return data;
6975 }
6976
6977 /**
6978 * Enables or restores form field's from its original enabled state
6979 *
6980 * @param {string} selector A class that represents the container of the form fields
6981 *
6982 * @return {void}
6983 */
6984 this.enable_actions = function(selector) {
6985 selector = ('undefined' !== typeof selector && selector) ? selector : '.updraftcentral_row_extracontents';
6986 $(selector).find('input, button, select, a').prop('disabled', false).removeClass('disabled_cursor');
6987 }
6988
6989 /**
6990 * Disables form field's temporarily while actions are being processed
6991 *
6992 * @param {string} selector A class that represents the container of the form fields
6993 * @param {array} exceptions Optional. An array of selectors that will be excluded by the disabling process
6994 *
6995 * @return {void}
6996 */
6997 this.disable_actions = function(selector, exceptions) {
6998 selector = ('undefined' !== typeof selector && selector) ? selector : '.updraftcentral_row_extracontents';
6999 var not_including = '';
7000 if ('undefined' !== typeof exceptions && Array.isArray(exceptions)) {
7001 if (exceptions.length) {
7002 not_including = exceptions.join(',');
7003 }
7004 }
7005
7006 $(selector).find('input, button, select, a').not(not_including).prop('disabled', true).addClass('disabled_cursor');
7007 }
7008
7009 /**
7010 * Checks whether any of the dialog boxes are currently opened
7011 *
7012 * @returns {boolean} - Returns true if opened, false otherwise.
7013 */
7014 this.is_dialog_opened = function() {
7015 if ($('div.bootbox.modal').is(':visible') || $('div#updraftcentral_modal_dialog').is(':visible')) {
7016 return true;
7017 }
7018
7019 return false;
7020 }
7021
7022 /**
7023 * Process site meta commands (add, delete, get and update)
7024 *
7025 * @param {Object} param Holds the needed parameters for the current "meta" process
7026 * @returns {Object} A jQuery promise object that holds results of the currently executed action
7027 */
7028 var send_meta_request = function(param) {
7029 var deferred = jQuery.Deferred();
7030
7031 UpdraftCentral.send_ajax('manage_site_meta', param, null, 'via_mothership_encrypting', null, function(resp, code, error_code) {
7032 if ('ok' == code && 'undefined' !== typeof resp.data) {
7033 deferred.resolve(resp.data);
7034 } else {
7035 deferred.reject(resp.message);
7036 }
7037 });
7038
7039 return deferred.promise();
7040 }
7041
7042
7043 /**
7044 * Add meta data field to a site.
7045 *
7046 * @param {Number} site_id - Site ID.
7047 * @param {String} meta_key - Metadata name.
7048 * @param {Mixed} meta_value - Metadata value.
7049 * @param {Boolean} [unique=false] - Whether the same key should not be added.
7050 *
7051 * @returns {Object} - A jQuery promise object that holds the Meta ID on success, false on failure.
7052 */
7053 this.add_site_meta = function(site_id, meta_key, meta_value, unique) {
7054 if ('undefined' === typeof unique) unique = false;
7055
7056 return send_meta_request({
7057 action: 'add',
7058 site_id: site_id,
7059 meta_key: meta_key,
7060 meta_value: meta_value,
7061 unique: unique
7062 });
7063 }
7064
7065 /**
7066 * Remove metadata matching criteria from a site.
7067 *
7068 * You can match based on the key, or key and value. Removing based on key and
7069 * value, will keep from removing duplicate metadata with the same key. It also
7070 * allows removing all metadata matching key, if needed.
7071 *
7072 * @param {Number} site_id - Site ID
7073 * @param {String} meta_key - Metadata name.
7074 * @param {Mixed} [meta_value='']. Metadata value.
7075 *
7076 * @returns {Object} - A jQuery promise object that holds a boolean value of true on success, false on failure.
7077 */
7078 this.delete_site_meta = function(site_id, meta_key, meta_value) {
7079 if ('undefined' === typeof meta_value) meta_value = '';
7080
7081 return send_meta_request({
7082 action: 'delete',
7083 site_id: site_id,
7084 meta_key: meta_key,
7085 meta_value: meta_value
7086 });
7087 }
7088
7089 /**
7090 * Retrieve site meta field for a site.
7091 *
7092 * @param {Number} site_id - Site ID.
7093 * @param {String} [key=''] - The meta key to retrieve. By default, returns data for all keys.
7094 * @param {Boolean} [single=false] - Whether to return a single value.
7095 *
7096 * @returns {Object} - A jQuery promise object that holds an array if single is false. Will be value of meta data field if single is true.
7097 */
7098 this.get_site_meta = function(site_id, key, single) {
7099 if ('undefined' === typeof key) key = '';
7100 if ('undefined' === typeof single) single = false;
7101
7102 return send_meta_request({
7103 action: 'get',
7104 site_id: site_id,
7105 key: key,
7106 single: single
7107 });
7108 }
7109
7110 /**
7111 * Update site meta field based on site ID.
7112 *
7113 * Use the $prev_value parameter to differentiate between meta fields with the
7114 * same key and site ID.
7115 *
7116 * If the meta field for the site does not exist, it will be added.
7117 *
7118 * @param {number} site_id - Site ID.
7119 * @param {string} meta_key - Metadata key.
7120 * @param {mixed} meta_value - Metadata value.
7121 * @param {mixed} [prev_value=''] - Previous value to check before removing.
7122 *
7123 * @returns {Object} - A jQuery promise object that holds the Meta ID if the key didn't exist, true on successful update, false on failure.
7124 */
7125 this.update_site_meta = function(site_id, meta_key, meta_value, prev_value) {
7126 if ('undefined' === typeof prev_value) prev_value = '';
7127
7128 return send_meta_request({
7129 action: 'update',
7130 site_id: site_id,
7131 meta_key: meta_key,
7132 meta_value: meta_value,
7133 prev_value: prev_value
7134 });
7135 }
7136
7137 /**
7138 * Function to be called whenever a bootbox dialog is opened
7139 * We use it simply to move the bootbox within the DOM if in fullscreen mode (because otherwise it won't be seen).
7140 *
7141 * @param {string} key - A unique identifier that will serve as an id for the dialog
7142 * @param {object} dialog - The bootbox dialog object that was created
7143 * @returns {void}
7144 */
7145 var bootbox_opened = function(key, dialog) {
7146 // It only needs moving if in full-screen mode; so, we're conservative and otherwise leave it alone
7147 if ($.fullscreen.isFullScreen()) {
7148 $('.bootbox.modal').appendTo('#updraftcentral_dashboard');
7149 }
7150 // Use a new browser portal for any clicks to updraftplus.com
7151 $('.bootbox.modal').on('click', 'a', function(e) {
7152 var href = $(this).attr('href');
7153
7154 // This is causing some error. We're making sure that we have a valid
7155 // function before calling it.
7156 if ('function' === typeof redirect_updraft_website_links) {
7157 redirect_updraft_website_links(href, e);
7158 }
7159 });
7160
7161 $('.bootbox.modal .updraftcentral_site_editdescription').on('click', function(e) {
7162 e.preventDefault();
7163 $(this).closest('.modal').modal('hide');
7164 open_site_configuration(UpdraftCentral.$site_row);
7165 });
7166
7167 $('.bootbox.modal .updraftcentral_test_other_connection_methods').on('click', function(e) {
7168 e.preventDefault();
7169 $(this).closest('.modal').modal('hide');
7170 open_connection_test(UpdraftCentral.$site_row);
7171 });
7172
7173 $('.bootbox.modal').data('id', key);
7174 $('.bootbox.modal.bootbox-alert, .bootbox.modal.bootbox-confirm, .bootbox.modal.bootbox-prompt').find('button[data-bs-dismiss="modal"], button[data-bb-handler="ok"], button[data-bb-handler="cancel"], button[data-bb-handler="confirm"]').off('click').on('click', function() {
7175 // Popup is closing, we remove previously stored key
7176 // to allow the popup to be opened once again when needed.
7177 if (collection.exists(key)) {
7178 collection.remove(key);
7179 }
7180
7181 // Trigger dashboard-wide dialog closed event (applies to both bootbox and bootstrap modal)
7182 $('#updraftcentral_dashboard').trigger('updraftcentral_dialog_closed');
7183 });
7184
7185 $('.bootbox.modal').on('hidden.bs.modal', function() {
7186 if (collection.exists(key)) {
7187 collection.remove(key);
7188 }
7189
7190 // Trigger dashboard-wide dialog closed event (applies to both bootbox and bootstrap modal)
7191 $('#updraftcentral_dashboard').trigger('updraftcentral_dialog_closed');
7192 });
7193
7194 if ('undefined' !== typeof dialog && dialog) {
7195 dialog.on("shown.bs.modal", function() {
7196 $('#updraftcentral_dashboard').trigger('updraftcentral_bootbox_dialog_opened', [key, dialog]);
7197 });
7198
7199 dialog.on("hidden.bs.modal", function() {
7200 $('#updraftcentral_dashboard').trigger('updraftcentral_bootbox_dialog_closed', [key, dialog]);
7201 });
7202 }
7203
7204 // Trigger dashboard-wide dialog opened event (applies to both bootbox and bootstrap modal)
7205 $('#updraftcentral_dashboard').trigger('updraftcentral_dialog_opened');
7206 }
7207
7208 /**
7209 * Converts the first letter of a string to uppercase
7210 *
7211 * @param {string} str - A string to convert
7212 * @returns {string} - Converted string
7213 */
7214 this.ucfirst = function(str) {
7215 return str.charAt(0).toUpperCase() + str.slice(1);
7216 }
7217
7218 /**
7219 * Opens the site connection test dialog for the specified site
7220 *
7221 * @param {Object} $site_row - the jQuery row object for the site whose configuration is to be edited
7222 * @returns {void}
7223 */
7224 this.open_connection_test = function($site_row) {
7225
7226 var site_url = UpdraftCentral.get_contact_url($site_row);
7227 var site_id = $site_row.data('site_id');
7228
7229 var current_connection_method = $site_row.data('connection_method');
7230
7231 if ('via_mothership_encrypting' == current_connection_method) {
7232 UpdraftCentral_Library.dialog.alert('<h2>'+udclion.test_connection_methods+'</h2><p>'+udclion.test_not_possible_in_current_mode+'</p>');
7233 // <p><a href="#" class="updraftcentral_site_editdescription">'+udclion.open_site_configuration+'...</a></p>
7234 return;
7235 }
7236
7237 var current_method_simplified = ('direct_jquery_auth' == current_connection_method || 'direct_default_auth' == current_connection_method || 'direct_default_auth' == current_connection_method) ? 'direct' : current_connection_method;
7238
7239 UpdraftCentral.open_modal(udclion.test_connection_methods, UpdraftCentral.template_replace('sites-connection-test', { site_url: site_url }), true, false, function() {
7240
7241 var direct_method_can_be_attempted = true;
7242 if ('https:'== document.location.protocol) {
7243 if (site_url.substring(0, 5).toLowerCase() == 'http:') {
7244 direct_method_can_be_attempted = false;
7245 }
7246 }
7247
7248 if (direct_method_can_be_attempted) {
7249
7250 $('#updraftcentral_modal .connection-test-direct .connection-test-result').html('');
7251
7252 UpdraftCentral.send_ajax('ping', null, $site_row, 'direct_default_auth', '#updraftcentral_modal .connection-test-direct .connection-test-result', function(response, code, error_code) {
7253 if (UpdraftCentral.get_debug_level() > 0) {
7254 console.log("Result follows for 'direct_default_auth' method:");
7255 console.log(response);
7256 }
7257 if ('ok' == code) {
7258 var new_html = '<span class="connection-test-succeeded">'+udclion.succeeded+'</span> ';
7259 if ('direct' == current_method_simplified) {
7260 new_html += udclion.current_method+' '+udclion.best_method+' '+udclion.recommend_keep;
7261 } else {
7262 new_html += udclion.best_method+' '+udclion.recommend_use+' <a href="#" class="connection-test-switch" data-site_id="'+site_id+'" data-connection_method="direct_default_auth">'+udclion.switch_to+'...</a>';
7263 }
7264 $('#updraftcentral_modal .connection-test-direct .connection-test-result').html(new_html);
7265 } else {
7266 $('#updraftcentral_modal .connection-test-direct .connection-test-result').html('<span class="connection-test-failed">'+udclion.failed+' ('+error_code+')</span>');
7267 }
7268 }, 30, false);
7269
7270 } else {
7271 $('#updraftcentral_modal .connection-test-direct .connection-test-result').html(udclion.not_possible_browser_restrictions);
7272 }
7273
7274 $('#updraftcentral_modal .connection-test-via_mothership .connection-test-result').html('');
7275 UpdraftCentral.send_ajax('ping', null, $site_row, 'via_mothership', '#updraftcentral_modal .connection-test-via_mothership .connection-test-result', function(response, code, error_code) {
7276 $('#updraftcentral_modal .connection-test-via_mothership .connection-test-result').html(code);
7277 if (UpdraftCentral.get_debug_level() > 0) {
7278 console.log("Result follows for 'via_mothership' method:");
7279 console.log(response);
7280 }
7281 if ('ok' == code) {
7282 var new_html = '<span class="connection-test-succeeded">'+udclion.succeeded+'</span> ';
7283 if ('via_mothership' != current_connection_method) {
7284 new_html += '<a href="#" class="connection-test-switch" data-site_id="'+site_id+'" data-connection_method="via_mothership">'+udclion.switch_to+'...</a>';
7285 } else {
7286 new_html += udclion.current_method;
7287 }
7288 $('#updraftcentral_modal .connection-test-via_mothership .connection-test-result').html(new_html);
7289 } else {
7290
7291 var code_msg = error_code;
7292 if ('unexpected_http_code' == error_code) {
7293 if (null != response && response.hasOwnProperty('data') && null != response.data && response.data.hasOwnProperty('response') && response.data.response.hasOwnProperty('code')) {
7294 code_msg += ' - '+response.data.response.code;
7295 }
7296 if (null != response && response.hasOwnProperty('data') && null != response.data && response.data.hasOwnProperty('response') && response.data.response.hasOwnProperty('message')) {
7297 code_msg += ' - '+response.data.response.message;
7298 }
7299 }
7300
7301 $('#updraftcentral_modal .connection-test-via_mothership .connection-test-result').html('<span class="connection-test-failed">'+udclion.failed+' ('+code_msg+')</span>');
7302 }
7303 }, 30, false);
7304
7305 $('#updraftcentral_modal .connection-test-via_mothership_encrypting .connection-test-result').html('');
7306 UpdraftCentral.send_ajax('ping', null, $site_row, 'via_mothership_encrypting', '#updraftcentral_modal .connection-test-via_mothership_encrypting .connection-test-result', function(response, code, error_code) {
7307 if (UpdraftCentral.get_debug_level() > 0) {
7308 console.log("Result follows for 'via_mothership_encrypting' method:");
7309 console.log(response);
7310 }
7311 if ('ok' == code) {
7312 var new_html = '<span class="connection-test-succeeded">'+udclion.succeeded+'</span> ';
7313 if ('via_mothership_encrypting' != current_connection_method) {
7314 new_html += '<a href="#" class="connection-test-switch" data-site_id="'+site_id+'" data-connection_method="via_mothership_encrypting">'+udclion.switch_to+'...</a>';
7315 } else {
7316 new_html += udclion.current_method;
7317 }
7318 $('#updraftcentral_modal .connection-test-via_mothership_encrypting .connection-test-result').html(new_html);
7319 } else {
7320 var code_msg = error_code;
7321 if ('unexpected_http_code' == error_code) {
7322 if (null != response && response.hasOwnProperty('data') && null != response.data && response.data.hasOwnProperty('response') && response.data.response.hasOwnProperty('code')) {
7323 code_msg += ' - '+response.data.response.code;
7324 }
7325 if (null != response && response.hasOwnProperty('data') && null != response.data && response.data.hasOwnProperty('response') && response.data.response.hasOwnProperty('message')) {
7326 code_msg += ' - '+response.data.response.message;
7327 }
7328 }
7329
7330 $('#updraftcentral_modal .connection-test-via_mothership_encrypting .connection-test-result').html('<span class="connection-test-failed">'+udclion.failed+' ('+code_msg+')</span>');
7331 }
7332 }, 30, false);
7333
7334 }, true, 'modal-lg');
7335 }
7336 /**
7337 * Open an alert box (as a more aesthetic alternative to the traditional browser-provided alert()).
7338 *
7339 * @param {string} message - the message to display in the alert box
7340 * @param {dialogresultCallback} result_callback - callback function that is invoked when the alert box is closed
7341 * @param {boolean} [sanitize_message=true] - whether or not to put the message through sanitize_html()
7342 * @param {string} [id] - a unique identifier that will be used to identify and check whether the dialog is currently opened
7343 * @param {boolean} [backdrop] - indicates whether the dialog will have a backdrop or not
7344 * @returns {void}
7345 * @uses sanitize_html
7346 */
7347 this.dialog.alert = function(message, result_callback, sanitize_message, id, backdrop) {
7348 sanitize_message = ('undefined' == sanitize_message) ? true : sanitize_message;
7349 if (sanitize_message) {
7350 message = this.sanitize_html(message);
7351 }
7352
7353 var key = ('undefined' !== typeof id) ? id : UpdraftCentral_Library.md5('_alert_'+message);
7354 if (!collection.exists(key)) {
7355 collection.add(key, true);
7356
7357 var dialog = bootbox.alert({
7358 message: message,
7359 callback: result_callback,
7360 backdrop: backdrop
7361 });
7362 bootbox_opened(key, dialog);
7363 }
7364 }
7365
7366 /**
7367 * Open a confirmation box (as a more aesthetic alternative to the traditional browser-provided confirm()).
7368 *
7369 * @param {string} question - the message to display in the alert box
7370 * @param {dialogresultCallback} result_callback - callback function that is invoked when the alert box is closed
7371 * @param {string|null} id - a unique identifier that will be used to identify and check whether the dialog is currently opened
7372 * @param {object} labels - custom button labels for confirm and cancel buttons (e.g. { confirm: 'Yes', cancel: 'No' })
7373 * @returns {void}
7374 */
7375 this.dialog.confirm = function(question, result_callback, id, labels) {
7376 var key = ('undefined' !== typeof id && id) ? id : UpdraftCentral_Library.md5('_confirm_'+question);
7377 if (!collection.exists(key)) {
7378 collection.add(key, true);
7379
7380 var config = {
7381 message: question,
7382 callback: result_callback
7383 };
7384
7385 if ('undefined' !== typeof labels && labels) {
7386 var buttons = {
7387 confirm: {label: udclion.ok},
7388 cancel: {label: udclion.cancel}
7389 }
7390
7391 if (labels.hasOwnProperty('confirm') && labels.confirm) buttons.confirm.label = labels.confirm;
7392 if (labels.hasOwnProperty('cancel') && labels.cancel) buttons.cancel.label = labels.cancel;
7393
7394 config.buttons = buttons;
7395 }
7396
7397 var dialog = bootbox.confirm(config);
7398 bootbox_opened(key, dialog);
7399 }
7400 }
7401
7402 /**
7403 * Open a prompt box (as a more aesthetic alternative to the traditional browser-provided prompt()).
7404 *
7405 * @param {string} title - the message to display in the alert box
7406 * @param {string} default_value - the default value for the user response field
7407 * @param {dialogresultCallback} result_callback - callback function that is invoked when the alert box is closed
7408 * @param {string} id - a unique identifier that will be used to identify and check whether the dialog is currently opened
7409 * @returns {void}
7410 */
7411 this.dialog.prompt = function(title, default_value, result_callback, id) {
7412 var key = ('undefined' !== typeof id) ? id : UpdraftCentral_Library.md5('_prompt_'+title);
7413 if (!collection.exists(key)) {
7414 collection.add(key, true);
7415
7416 var dialog = bootbox.prompt({ title: title, value: default_value, callback: result_callback});
7417 bootbox_opened(key, dialog);
7418 }
7419 }
7420
7421 /**
7422 * Open a message box with custom buttons to attach to the dialog
7423 *
7424 * @param {string} message - the message to display in the message box
7425 * @param {string} id - a unique identifier that will be used to identify and check whether the dialog is currently opened
7426 * @param {object} buttons - custom buttons to attach
7427 * @returns {void}
7428 */
7429 this.dialog.custom = function(message, id, buttons) {
7430 var key = ('undefined' !== typeof id && id) ? id : UpdraftCentral_Library.md5('_custom_'+message);
7431 if (!collection.exists(key)) {
7432 collection.add(key, true);
7433
7434 var dialog = bootbox.dialog({
7435 message: message,
7436 backdrop: 'static',
7437 closeButton: true,
7438 buttons: buttons
7439 });
7440 bootbox_opened(key, dialog);
7441 }
7442 }
7443
7444 /**
7445 * Calculate an MD5 hash
7446 *
7447 * @param {string} data - the data to hash
7448 * @returns {string} - the encoded data, in hex format
7449 */
7450 this.md5 = function(data) {
7451 var md = forge.md.md5.create();
7452 md.update(data);
7453 return md.digest().toHex();
7454 }
7455
7456 /**
7457 * Sanitizes passed HTML, so that it is safe for display. Uses Google's Caja parser.
7458 *
7459 * @param {string} html - the potentially suspicious HTML
7460 * @returns {string} The sanitized HTML
7461 */
7462 this.sanitize_html = function(html) {
7463 var web_only = function(url) {
7464 if (/^https?:\/\//.test(url)) { return url;
7465 }}
7466 var same_id = function(id) {
7467 return id;
7468 }
7469 // The html_sanitize object comes from Google's Caja
7470 // This version retains data- attributes. It removes style attributes (but not CSS classes)
7471 return html_sanitize.sanitize(html, web_only, same_id);
7472 }
7473
7474 /**
7475 * Escapes passed HTML, so that it is safe for display.
7476 *
7477 * @param {string} html - the potentially suspicious HTML
7478 * @returns {string}
7479 */
7480 this.escape_attrib = function(html) {
7481 return html_sanitize.escapeAttrib(html);
7482 }
7483
7484 /**
7485 * Quote the input, so that it is suitable for placing in HTML attributes values
7486 *
7487 * @param {string} s - The string to be quoted
7488 * @param {boolean} preserveCR - if true, then \r and \n are replaced with an HTML entity; otherwise with \n
7489 *
7490 * @see https://stackoverflow.com/questions/7753448/how-do-i-escape-quotes-in-html-attribute-values
7491 *
7492 * @returns {string} the quoted string
7493 */
7494 this.quote_attribute = function(s, preserveCR) {
7495 preserveCR = preserveCR ? '&#13;' : '\n';
7496 return ('' + s) // Forces the conversion to string.
7497 .replace(/&/g, '&amp;') // This MUST be the 1st replacement.
7498 .replace(/'/g, '&apos;') // The 4 other predefined entities, required.
7499 .replace(/"/g, '&quot;')
7500 .replace(/</g, '&lt;')
7501 .replace(/>/g, '&gt;')
7502 // You may add other replacements here for HTML only (but it's not necessary). Or for XML, only if the named entities are defined in its DTD.
7503 .replace(/\r\n/g, preserveCR) // Must be before the next replacement.
7504 .replace(/[\r\n]/g, preserveCR);
7505 }
7506
7507 /**
7508 * Opens a new browser portal at the specified URL
7509 *
7510 * @param {Object} $site_row - jQuery object for the site row
7511 * @param {Object|string|null} [redirect_to=null] - where to redirect to (defaults to the network admin)
7512 * @param {Object} [spinner_where=$site_row] - jQuery object indicating where to put the site row.
7513 * @returns {void}
7514 */
7515 this.open_browser_at = function($site_row, redirect_to, spinner_where) {
7516 redirect_to = typeof redirect_to !== 'undefined' ? redirect_to : null;
7517 spinner_where = ('undefined' === typeof spinner_where) ? $site_row : spinner_where;
7518 UpdraftCentral.send_site_rpc('core.get_login_url', redirect_to, $site_row, function(response, code, error_code) {
7519 if ('ok' == code && false !== response && response.hasOwnProperty('data')) {
7520 var login_url = response.data.login_url;
7521 var win = window.open(login_url, '_blank');
7522 UpdraftCentral_Library.focus_window_or_error(win);
7523 }
7524 }, spinner_where);
7525 }
7526
7527 /**
7528 * Either focuses the window, or tells the user to check whether they have a pop-up blocker
7529 *
7530 * @param {Object|null} - either a window object that should have focus() called on it, or null to instead show an alert
7531 * @returns {void}
7532 */
7533 this.focus_window_or_error = function(win) {
7534 if ('undefined' != typeof win && null !== win) {
7535 if (win instanceof jQuery) {
7536 win.trigger('focus');
7537 } else {
7538 win.focus();
7539 }
7540 } else {
7541 this.dialog.alert('<h2>'+udclion.open_new_window+'</h2>'+udclion.window_may_be_blocked);
7542 }
7543 }
7544
7545 /**
7546 * Toggle whether or not UpdraftCentral is in "full screen" mode
7547 *
7548 * @returns {void}
7549 */
7550 this.toggle_fullscreen = function() {
7551 // https://github.com/private-face/jquery.fullscreen
7552 if ($.fullscreen.isFullScreen()) {
7553 $('footer').show();
7554 $.fullscreen.exit();
7555 $('#updraftcentral_modal_dialog').appendTo(document.body);
7556 } else {
7557 $('footer').hide();
7558 $('#updraftcentral_dashboard').parent().fullscreen({toggleClass: 'updraft-fullscreen' });
7559 $('#updraftcentral_modal_dialog').appendTo('#updraftcentral_dashboard');
7560 }
7561 }
7562
7563 /**
7564 * Reverses serialisation that was performed using jQuery's .serialize() method
7565 * From: https://gist.github.com/brucekirkpatrick/7026682
7566 *
7567 * @param {string} serialized_string - the string to unserialize
7568 * @returns {Object} - the resulting object
7569 */
7570 this.unserialize = function(serialized_string) {
7571 var str = decodeURI(serialized_string);
7572 var pairs = str.split('&');
7573 var obj = {}, p, idx;
7574 for (var i=0, n=pairs.length; i < n; i++) {
7575 p = pairs[i].split('=');
7576 idx = p[0];
7577 if (undefined === obj[idx]) {
7578 obj[idx] = unescape(p[1]);
7579 } else {
7580 if ("string" == typeof obj[idx]) {
7581 obj[idx] = [obj[idx]];
7582 }
7583 obj[idx].push(unescape(p[1]));
7584 }
7585 }
7586 return obj;
7587 }
7588
7589 /**
7590 * Get serialized options within a specified selector. Includes making sure that checkboxes are included when not checked.
7591 *
7592 * @param {string} selector - the jQuery selector to use to locate the options
7593 * @returns {string} - the serialized options
7594 */
7595 this.get_serialized_options = function(selector) {
7596 var form_data = $(selector).serialize();
7597 $.each($(selector+' input[type=checkbox]')
7598 .filter(function(idx) {
7599 return $(this).prop('checked') == false
7600 }),
7601 function(idx, el) {
7602 // Attach matched element names to the form_data with chosen value.
7603 var empty_val = '0';
7604 form_data += '&' + $(el).attr('name') + '=' + empty_val;
7605 });
7606 return form_data;
7607 }
7608
7609 /**
7610 * Allow the user to download/save a file, with contents supplied from the inner HTML of a specified element
7611 *
7612 * @param {string} filename - the filename that will be suggested to the user to save as
7613 * @param {string} element_id - the DOM id of the element whose inner HTML is to be used as content
7614 * @param {string} [mime_type='text/plain'] - the MIME type to indicate in the header sent to the browser
7615 * @returns {void}
7616 */
7617 this.download_inner_html = function(filename, element_id, mime_type) {
7618 mime_type = mime_type || 'text/plain';
7619 var element_html = document.getElementById(element_id).innerHTML;
7620 var link = document.body.appendChild(document.createElement('a'));
7621 link.setAttribute('download', filename);
7622 link.setAttribute('style', "display:none;");
7623 link.setAttribute('href', 'data:' + mime_type + ';charset=utf-8,' + encodeURIComponent(element_html));
7624 link.click();
7625 }
7626
7627 }
7628