PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
mainwp / assets / js / mainwp-theme.js

mainwp-theme.js in MainWP Dashboard: Self-hosted WordPress Management for Agencies 6.2.2, at assets/js/mainwp-theme.js

1,753 lines 57.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* eslint complexity: ["error", 100] */
2 // current complexity is the only way to achieve desired results, pull request solutions appreciated.
3
4 /* global _mainwpThemeSettings, confirm */
5 globalThis.wp = globalThis.wp || {};
6
7 (function ($) {
8
9 // Set up our namespace...
10 let themes, l10n;
11 themes = wp.themes = wp.themes || {};
12
13 // Store the theme data and settings for organized and quick access
14 // themes.data.settings, themes.data.themes, themes.data.l10n
15 themes.data = _mainwpThemeSettings;
16 l10n = themes.data.l10n;
17
18 // Shortcut for isInstall check
19 themes.isInstall = !!themes.data.settings.isInstall;
20
21 // Setup app structure
22 _.extend(themes, { model: {}, view: {}, routes: {}, router: {}, template: wp.template });
23
24 themes.Model = Backbone.Model.extend({
25 // Adds attributes to the default data coming through the .org themes api
26 // Map `id` to `slug` for shared code
27 initialize: function () {
28 let description;
29
30 // If theme is already installed, set an attribute.
31 if (_.indexOf(themes.data.installedThemes, this.get('slug')) !== -1) {
32 this.set({ installed: true });
33 }
34
35 // Set the attributes
36 this.set({
37 // slug is for installation, id is for existing.
38 id: this.get('slug') || this.get('id')
39 });
40
41 // Map `section.description` to `description`
42 // as the API sometimes returns it differently
43 if (this.has('sections')) {
44 description = this.get('sections').description;
45 this.set({ description: description });
46 }
47 }
48 });
49
50 // Main view controller for themes.php
51 // Unifies and renders all available views
52 themes.view.Appearance = wp.Backbone.View.extend({
53
54 el: '#wpbody-content .mainwp-browse-themes',
55
56 window: $(globalThis.window),
57 // Pagination instance
58 page: 0,
59
60 // Sets up a throttler for binding to 'scroll'
61 initialize: function (options) {
62 // Scroller checks how far the scroll position is
63 _.bindAll(this, 'scroller');
64
65 this.SearchView = options.SearchView ? options.SearchView : themes.view.Search;
66 // Bind to the scroll event and throttle
67 // the results from this.scroller
68 this.window.on('scroll', _.throttle(this.scroller, 300));
69 },
70
71 // Main render control
72 render: function () {
73 // Setup the main theme view
74 // with the current theme collection
75 this.view = new themes.view.Themes({
76 collection: this.collection,
77 parent: this
78 });
79
80 // Render search form.
81 this.search();
82
83 // Render and append
84 this.view.render();
85 this.$el.empty().append(this.view.el).addClass('rendered');
86 this.$el.append('<br class="clear"/>');
87 },
88
89 // Defines search element container
90 searchContainer: $('#wpbody'), // init container
91
92 // Search input and view
93 // for current theme collection
94 search: function () {
95 // Don't render the search if there is only one theme
96 if (themes.data.themes.length === 1) {
97 return;
98 }
99
100 const view = new this.SearchView({
101 collection: this.collection,
102 parent: this
103 });
104
105 // Render and append after screen title
106 view.render();
107 this.searchContainer
108 .append(view.el)
109 .append('<i class="search icon"></i>');
110 },
111
112 // Checks when the user gets close to the bottom
113 // of the mage and triggers a theme:scroll event
114 scroller: function () {
115 const bottom = this.window.scrollTop() + this.window.height();
116 let threshold = this.$el.offset().top + this.$el.outerHeight(false) - this.window.height();
117 threshold = Math.round(threshold * 0.9);
118
119 if (bottom > threshold) {
120 this.trigger('theme:scroll');
121 }
122 }
123 });
124
125 // Set up the Collection for our theme data
126 // @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ...
127 themes.Collection = Backbone.Collection.extend({
128
129 model: themes.Model,
130
131 // Search terms
132 terms: '',
133
134 // Controls searching on the current theme collection
135 // and triggers an update event
136 doSearch: function (value) {
137
138 // Don't do anything if we've already done this search
139 // Useful because the Search handler fires multiple times per keystroke
140 if (this.terms === value) {
141 return;
142 }
143
144 // Updates terms with the value passed
145 this.terms = value;
146
147 // If we have terms, run a search...
148 if (this.terms.length > 0) {
149 this.search(this.terms);
150 }
151
152 // If search is blank, show all themes
153 // Useful for resetting the views when you clean the input
154 if (this.terms === '') {
155 this.reset(themes.data.themes);
156 $('body').removeClass('no-results');
157 }
158
159 // Trigger an 'update' event
160 this.trigger('update');
161 },
162
163 // Performs a search within the collection
164 // @uses RegExp
165 search: function (term) {
166 let match, results, haystack, name, description, author;
167
168 // Start with a full collection
169 this.reset(themes.data.themes, { silent: true });
170
171 // Escape the term string for RegExp meta characters
172 term = term.replaceAll(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); // NOSONAR - safe pattern.
173
174 // Consider spaces as word delimiters and match the whole string
175 // so matching terms can be combined
176 term = term.replaceAll(' ', ')(?=.*');
177 match = new RegExp('^(?=.*' + term + ').+', 'i');
178
179 // Find results
180 // _.filter and .test
181 results = this.filter(function (data) {
182 name = data.get('name').replace(/(<([^>]+)>)/ig, ''); // NOSONAR - safe pattern from WP.
183 description = data.get('description').replace(/(<([^>]+)>)/ig, ''); // NOSONAR - safe pattern from WP.
184 author = data.get('author').replace(/(<([^>]+)>)/ig, ''); // NOSONAR - safe pattern from WP.
185
186 haystack = _.union(name, data.get('id'), description, author, data.get('tags'));
187
188 if (match.test(data.get('author')) && term.length > 2) {
189 data.set('displayAuthor', true);
190 }
191
192 return match.test(haystack);
193 });
194
195 if (results.length === 0) {
196 this.trigger('query:empty');
197 } else {
198 $('body').removeClass('no-results');
199 }
200
201 this.reset(results);
202 },
203
204 // Paginates the collection with a helper method
205 // that slices the collection
206 paginate: function (instance = 0) {
207 // Themes per instance are set at 20
208 let collection = _(this.rest(20 * instance));
209 collection = _(collection.first(20));
210
211 return collection;
212
213 },
214
215 count: false,
216
217 // Handles requests for more themes
218 // and caches results
219 //
220 // When we are missing a cache object we fire an apiCall()
221 // which triggers events of `query:success` or `query:fail`
222 query: function (request) {
223
224 let queries = this.queries;
225 let query, isPaginated, count;
226
227 // Store current query request args
228 this.currentQuery.request = request;
229
230 // Search the query cache for matches.
231 query = _.find(queries, (query) => {
232 return _.isEqual(query.request, request);
233 });
234
235 isPaginated = _.has(request, 'page');
236
237 if (!isPaginated) {
238 this.currentQuery.page = 1;
239 }
240
241 if (!query && !isPaginated) {
242
243 this.apiCall(request)
244 .done((data) => {
245
246 if (data.themes) {
247 this.reset(data.themes);
248 count = data.info.results;
249
250 queries.push({
251 themes: data.themes,
252 request: request,
253 total: count
254 });
255 }
256
257 this.trigger('update');
258 this.trigger('query:success', count);
259
260 if (data?.themes?.length === 0) {
261 this.trigger('query:empty');
262 }
263
264 })
265 .fail(() => {
266 this.trigger('query:fail');
267 });
268
269 } else {
270
271 if (isPaginated) {
272 return this.apiCall(request, isPaginated)
273 .done((data) => {
274
275 this.add(data.themes);
276 this.trigger('query:success');
277
278 this.loadingThemes = false;
279
280 })
281 .fail(() => {
282 this.trigger('query:fail');
283 });
284 }
285
286 if (query.themes.length === 0) {
287 this.trigger('query:empty');
288 } else {
289 $('body').removeClass('no-results');
290 }
291
292 if (_.isNumber(query.total)) {
293 this.count = query.total;
294 }
295
296 this.reset(query.themes);
297
298 if (!query.total) {
299 this.count = this.length;
300 }
301
302 this.trigger('update');
303 this.trigger('query:success', this.count);
304 }
305 },
306
307 // Local cache array for API queries
308 queries: [],
309
310 // Keep track of current query so we can handle pagination
311 currentQuery: {
312 page: 1,
313 request: {}
314 },
315
316 // Send request to api.wordpress.org/themes
317 apiCall: function (request, paginated) {
318 return wp.ajax.send('query-themes', {
319 data: {
320 // Request data
321 request: _.extend({
322 per_page: 100,
323 fields: {
324 description: true,
325 tested: true,
326 requires: true,
327 rating: true,
328 downloaded: true,
329 downloadLink: true,
330 last_updated: true,
331 homepage: true,
332 num_ratings: true
333 }
334 }, request)
335 },
336
337 beforeSend: function () {
338 if (!paginated) {
339 // Spin it
340 $('body').addClass('loading-content').removeClass('no-results');
341 }
342 }
343 }).done(function (response) {
344 if (response?.themes) {
345 let favThemes = jQuery('#mainwp-favorites-themes').attr('favorites-themes');
346 if ('' !== favThemes) {
347 try {
348 let decoded_favThemes = JSON.parse(favThemes);
349 response.themes.forEach(function (part, index) {
350 if (decoded_favThemes.hasOwnProperty(this[index].slug)) {
351 this[index].added_fav = 1;
352 } else {
353 this[index].added_fav = 0;
354 }
355 }, response.themes); // use second param as this.
356 } catch {
357 console.log('Invalid favorites themes data.');
358 }
359 }
360 }
361 });
362 },
363
364 // Static status controller for when we are loading themes.
365 loadingThemes: false
366 });
367
368 // This is the view that controls each theme item
369 // that will be displayed on the screen
370 themes.view.Theme = wp.Backbone.View.extend({
371
372 // Wrap theme data on a div.theme element
373 className: 'theme card',
374
375 // Reflects which theme view we have
376 // 'grid' (default) or 'detail'
377 state: 'grid',
378
379 // The HTML template for each element to be rendered
380 html: themes.template('theme'),
381
382 events: {
383 'click': themes.isInstall ? 'preview' : 'expand',
384 'keydown': themes.isInstall ? 'preview' : 'expand',
385 'touchend': themes.isInstall ? 'preview' : 'expand',
386 'keyup': 'addFocus',
387 'touchmove': 'preventExpand'
388 },
389
390 touchDrag: false,
391
392 render: function () {
393 let data = this.model.toJSON();
394 // Render themes using the html template
395 this.$el.html(this.html(data)).attr({
396 tabindex: 0,
397 'aria-describedby': data.id + '-action ' + data.id + '-name'
398 });
399
400 // Renders active theme styles
401 this.activeTheme();
402
403 if (this.model.get('displayAuthor')) {
404 this.$el.addClass('display-author');
405 }
406
407 if (this.model.get('installed')) {
408 this.$el.addClass('is-installed');
409 }
410 },
411
412 // Adds a class to the currently active theme
413 // and to the overlay in detailed view mode
414 activeTheme: function () {
415 if (this.model.get('active')) {
416 this.$el.addClass('active');
417 }
418 },
419
420 // Add class of focus to the theme we are focused on.
421 addFocus: function () {
422 let $themeToFocus = ($(':focus').hasClass('theme')) ? $(':focus') : $(':focus').parents('.theme');
423
424 $('.theme.focus').removeClass('focus');
425 $themeToFocus.addClass('focus');
426 },
427
428 // Single theme overlay screen
429 // It's shown when clicking a theme
430 expand: function (event) {
431
432 event = event || window.event; // NOSONAR - compatible.
433
434 // 'enter' and 'space' keys expand the details view when a theme is :focused
435 if (event.type === 'keydown' && (event.which !== 13 && event.which !== 32)) {
436 return;
437 }
438
439 // Bail if the user scrolled on a touch device
440 if (this.touchDrag === true) {
441 this.touchDrag = false;
442 return this.touchDrag;
443 }
444
445 // Prevent the modal from showing when the user clicks
446 // one of the direct action buttons
447 if ($(event.target).is('.theme-actions a')) {
448 return;
449 }
450
451 // Set focused theme to current element
452 themes.focusedTheme = this.$el;
453
454 this.trigger('theme:expand', this.model.cid);
455 },
456
457 preventExpand: function () {
458 this.touchDrag = true;
459 },
460
461 preview: function (event) {
462 let current, preview;
463
464 // Bail if the user scrolled on a touch device
465 if (this.touchDrag === true) {
466 this.touchDrag = false;
467 return this.touchDrag;
468 }
469
470 // Allow direct link path to installing a theme.
471 if ($(event.target).hasClass('button-primary')) {
472 return;
473 }
474
475 if ($(event.target).is('.mainwp-theme-lnks input[type="radio"]')) {
476 return;
477 }
478 if (!$(event.target).is('.mainwp-theme-preview')) {
479 return;
480 }
481
482 // 'enter' and 'space' keys expand the details view when a theme is :focused
483 if (event.type === 'keydown' && (event.which !== 13 && event.which !== 32)) {
484 return;
485 }
486
487 // pressing enter while focused on the buttons shouldn't open the preview
488 if (event.type === 'keydown' && event.which !== 13 && $(':focus').hasClass('button')) {
489 return;
490 }
491
492 event.preventDefault();
493
494 event = event || window.event; // NOSONAR - compatible.
495
496 // Set focus to current theme.
497 themes.focusedTheme = this.$el;
498
499 // Construct a new Preview view.
500 preview = new themes.view.Preview({
501 model: this.model
502 });
503
504 // Render the view and append it.
505 preview.render();
506 this.setNavButtonsState();
507
508 // Hide previous/next navigation if there is only one theme
509 if (this.model.collection.length === 1) {
510 preview.$el.addClass('no-navigation');
511 } else {
512 preview.$el.removeClass('no-navigation');
513 }
514
515 // Append preview
516 $('div.mainwp-content-wrap').append(preview.el);
517
518 // Listen to our preview object
519 // for `theme:next` and `theme:previous` events.
520 this.listenTo(preview, 'theme:next', () => {
521
522 // Keep local track of current theme model.
523 current = this.model;
524
525 // If we have ventured away from current model update the current model position.
526 if (!_.isUndefined(this.current)) {
527 current = this.current;
528 }
529
530 // Get next theme model.
531 this.current = this.model.collection.at(this.model.collection.indexOf(current) + 1);
532
533 // If we have no more themes, bail.
534 if (_.isUndefined(this.current)) {
535 this.options.parent.parent.trigger('theme:end');
536 this.current = current;
537 return this.current;
538 }
539
540 preview.model = this.current;
541
542 // Render and append.
543 preview.render();
544 this.setNavButtonsState();
545 $('.next-theme').trigger('focus');
546 })
547 .listenTo(preview, 'theme:previous', () => {
548
549 // Keep track of current theme model.
550 current = this.model;
551
552 // Bail early if we are at the beginning of the collection
553 if (this.model.collection.indexOf(this.current) === 0) {
554 return;
555 }
556
557 // If we have ventured away from current model update the current model position.
558 if (!_.isUndefined(this.current)) {
559 current = this.current;
560 }
561
562 // Get previous theme model.
563 this.current = this.model.collection.at(this.model.collection.indexOf(current) - 1);
564
565 // If we have no more themes, bail.
566 if (_.isUndefined(this.current)) {
567 return;
568 }
569
570 preview.model = this.current;
571
572 // Render and append.
573 preview.render();
574 this.setNavButtonsState();
575 $('.previous-theme').trigger('focus');
576 });
577
578 this.listenTo(preview, 'preview:close', () => {
579 this.current = this.model;
580 });
581 },
582
583 // Handles .disabled classes for previous/next buttons in theme installer preview
584 setNavButtonsState: function () {
585 let $themeInstaller = $('.theme-install-overlay'),
586 current = _.isUndefined(this.current) ? this.model : this.current;
587
588 // Disable previous at the zero position
589 if (0 === this.model.collection.indexOf(current)) {
590 $themeInstaller.find('.previous-theme').addClass('disabled');
591 }
592
593 // Disable next if the next model is undefined
594 if (_.isUndefined(this.model.collection.at(this.model.collection.indexOf(current) + 1))) {
595 $themeInstaller.find('.next-theme').addClass('disabled');
596 }
597 }
598 });
599
600 // Theme Details view
601 // Set ups a modal overlay with the expanded theme data
602 themes.view.Details = wp.Backbone.View.extend({
603
604 // Wrap theme data on a div.theme element
605 className: 'theme-overlay',
606
607 events: {
608 'click': 'collapse',
609 'click .delete-theme': 'deleteTheme',
610 'click .left': 'previousTheme',
611 'click .right': 'nextTheme'
612 },
613
614 // The HTML template for the theme overlay
615 html: themes.template('theme-single'),
616
617 render: function () {
618 let data = this.model.toJSON();
619 this.$el.html(this.html(data));
620 // Renders active theme styles
621 this.activeTheme();
622 // Set up navigation events
623 this.navigation();
624 // Checks screenshot size
625 this.screenshotCheck(this.$el);
626 // Contain "tabbing" inside the overlay
627 this.containFocus(this.$el);
628 },
629
630 // Adds a class to the currently active theme
631 // and to the overlay in detailed view mode
632 activeTheme: function () {
633 // Check the model has the active property
634 this.$el.toggleClass('active', this.model.get('active'));
635 },
636
637 // Keeps :focus within the theme details elements
638 containFocus: function ($el) {
639 let $target;
640
641 // Move focus to the primary action
642 _.delay(function () {
643 $('.theme-wrap a.button-primary:visible').trigger('focus');
644 }, 500);
645
646 $el.on('keydown.wp-themes', function (event) {
647
648 // Tab key
649 if (event.which === 9) {
650 $target = $(event.target);
651
652 // Keep focus within the overlay by making the last link on theme actions
653 // switch focus to button.left on tabbing and vice versa
654 if ($target.is('button.left') && event.shiftKey) {
655 $el.find('.theme-actions a:last-child').trigger('focus');
656 event.preventDefault();
657 } else if ($target.is('.theme-actions a:last-child')) {
658 $el.find('button.left').trigger('focus');
659 event.preventDefault();
660 }
661 }
662 });
663 },
664
665 // Single theme overlay screen
666 // It's shown when clicking a theme
667 collapse: function (event) {
668 let scroll;
669 event = event || window.event; // NOSONAR - compatible.
670
671 // Prevent collapsing detailed view when there is only one theme available
672 if (themes.data.themes.length === 1) {
673 return;
674 }
675
676 // Detect if the click is inside the overlay
677 // and don't close it unless the target was
678 // the div.back button
679 if ($(event.target).is('.theme-backdrop') || $(event.target).is('.close') || event.keyCode === 27) {
680
681 // Add a temporary closing class while overlay fades out
682 $('body').addClass('closing-overlay');
683
684 // With a quick fade out animation
685 this.$el.fadeOut(130, () => {
686 // Clicking outside the modal box closes the overlay
687 $('body').removeClass('closing-overlay');
688 // Handle event cleanup
689 this.closeOverlay();
690
691 // Get scroll position to avoid jumping to the top
692 scroll = document.body.scrollTop;
693
694 // Clean the url structure
695 themes.router.navigate(themes.router.baseUrl(''));
696
697 // Restore scroll position
698 document.body.scrollTop = scroll;
699
700 // Return focus to the theme div
701 if (themes.focusedTheme) {
702 themes.focusedTheme.trigger('focus');
703 }
704 });
705 }
706 },
707
708 // Handles .disabled classes for next/previous buttons
709 navigation: function () {
710
711 // Disable Left/Right when at the start or end of the collection
712 if (this.model.cid === this.model.collection.at(0).cid) {
713 this.$el.find('.left').addClass('disabled');
714 }
715 if (this.model.cid === this.model.collection.at(-1).cid) {
716 this.$el.find('.right').addClass('disabled');
717 }
718 },
719
720 // Performs the actions to effectively close
721 // the theme details overlay
722 closeOverlay: function () {
723 $('body').removeClass('modal-open');
724 this.remove();
725 this.unbind();
726 this.trigger('theme:collapse');
727 },
728
729 // Confirmation dialog for deleting a theme
730 deleteTheme: function () {
731 return confirm(themes.data.settings.confirmDelete);
732 },
733
734 nextTheme: function () {
735 this.trigger('theme:next', this.model.cid);
736 return false;
737 },
738
739 previousTheme: function () {
740 this.trigger('theme:previous', this.model.cid);
741 return false;
742 },
743
744 // Checks if the theme screenshot is the old 300px width version
745 // and adds a corresponding class if it's true
746 screenshotCheck: function (el) {
747 let screenshot, image;
748
749 screenshot = el.find('.screenshot img');
750 image = new Image();
751 image.src = screenshot.attr('src');
752
753 // Width check
754 if (image.width && image.width <= 300) {
755 el.addClass('small-screenshot');
756 }
757 }
758 });
759
760 // Theme Preview view
761 // Set ups a modal overlay with the expanded theme data
762 themes.view.Preview = themes.view.Details.extend({
763
764 className: 'wp-full-overlay expanded',
765 el: '.theme-install-overlay',
766
767 events: {
768 'click .close-full-overlay': 'close',
769 'click .collapse-sidebar': 'collapse',
770 'click .previous-theme': 'previousTheme',
771 'click .next-theme': 'nextTheme',
772 'keyup': 'keyEvent'
773 },
774
775 // The HTML template for the theme preview
776 html: themes.template('theme-preview'),
777
778 render: function () {
779 let data = this.model.toJSON();
780
781 this.$el.html(this.html(data));
782
783 themes.router.navigate(themes.router.baseUrl(themes.router.themePath + this.model.get('id')), { replace: true });
784
785 this.$el.fadeIn(200, function () {
786 $('body').addClass('theme-installer-active full-overlay-active');
787 $('.close-full-overlay').trigger('focus');
788 });
789 },
790
791 close: function () {
792 this.$el.fadeOut(200, function () {
793 $('body').removeClass('theme-installer-active full-overlay-active');
794
795 // Return focus to the theme div
796 if (themes.focusedTheme) {
797 themes.focusedTheme.trigger('focus');
798 }
799 });
800
801 themes.router.navigate(themes.router.baseUrl(''));
802 this.trigger('preview:close');
803 this.undelegateEvents();
804 this.unbind();
805 return false;
806 },
807
808 collapse: function (event) {
809 let $button = $(event.currentTarget);
810 if ('true' === $button.attr('aria-expanded')) {
811 $button.attr({ 'aria-expanded': 'false', 'aria-label': l10n.expandSidebar });
812 } else {
813 $button.attr({ 'aria-expanded': 'true', 'aria-label': l10n.collapseSidebar });
814 }
815
816 this.$el.toggleClass('collapsed').toggleClass('expanded');
817 return false;
818 },
819
820 keyEvent: function (event) {
821 // The escape key closes the preview
822 if (event.keyCode === 27) {
823 this.undelegateEvents();
824 this.close();
825 }
826 // The right arrow key, next theme
827 if (event.keyCode === 39) {
828 _.once(this.nextTheme());
829 }
830
831 // The left arrow key, previous theme
832 if (event.keyCode === 37) {
833 this.previousTheme();
834 }
835 }
836 });
837
838 // Controls the rendering of div.themes,
839 // a wrapper that will hold all the theme elements
840 themes.view.Themes = wp.Backbone.View.extend({
841
842 className: 'themes ui three cards',
843 $overlay: $('div.theme-overlay'),
844
845 // Number to keep track of scroll position
846 // while in theme-overlay mode
847 index: 0,
848
849 // The theme count element
850 count: $('.wp-core-ui .theme-count'),
851
852 // The live themes count
853 liveThemeCount: 0,
854
855 initialize: function (options) {
856
857 // Set up parent
858 this.parent = options.parent;
859
860 // Set current view to [grid]
861 this.setView('grid');
862
863 // Move the active theme to the beginning of the collection
864 this.currentTheme();
865
866 // When the collection is updated by user input...
867 this.listenTo(this.collection, 'update', () => {
868 this.parent.page = 0;
869 this.currentTheme();
870 this.render(this);
871 });
872
873 // Update theme count to full result set when available.
874 this.listenTo(this.collection, 'query:success', (count) => {
875 if (_.isNumber(count)) {
876 this.count.text(count);
877 this.announceSearchResults(count);
878 } else {
879 this.count.text(this.collection.length);
880 this.announceSearchResults(this.collection.length);
881 }
882 });
883
884 this.listenTo(this.collection, 'query:empty', () => {
885 $('body').addClass('no-results');
886 });
887
888 this.listenTo(this.parent, 'theme:scroll', () => {
889 this.renderThemes(this.parent.page);
890 });
891
892 this.listenTo(this.parent, 'theme:close', () => {
893 if (this.overlay) {
894 this.overlay.closeOverlay();
895 }
896 });
897
898 // Bind keyboard events.
899 $('body').on('keyup', (event) => {
900 if (!this.overlay) {
901 return;
902 }
903
904 // Pressing the right arrow key fires a theme:next event
905 if (event.keyCode === 39) {
906 this.overlay.nextTheme();
907 }
908
909 // Pressing the left arrow key fires a theme:previous event
910 if (event.keyCode === 37) {
911 this.overlay.previousTheme();
912 }
913
914 // Pressing the escape key fires a theme:collapse event
915 if (event.keyCode === 27) {
916 this.overlay.collapse(event);
917 }
918 });
919 },
920
921 // Manages rendering of theme pages
922 // and keeping theme count in sync
923 render: function () {
924 // Clear the DOM, please
925 this.$el.empty();
926
927 // If the user doesn't have switch capabilities
928 // or there is only one theme in the collection
929 // render the detailed view of the active theme
930 if (themes.data.themes.length === 1) {
931
932 // Constructs the view
933 this.singleTheme = new themes.view.Details({
934 model: this.collection.models[0]
935 });
936
937 // Render and apply a 'single-theme' class to our container
938 this.singleTheme.render();
939 this.$el.addClass('single-theme');
940 this.$el.append(this.singleTheme.el);
941 }
942
943 // Generate the themes
944 // Using page instance
945 // While checking the collection has items
946 if (this.options.collection.length > 0) {
947 this.renderThemes(this.parent.page);
948 }
949
950 // Display a live theme count for the collection
951 this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length;
952 this.count.text(this.liveThemeCount);
953
954 this.announceSearchResults(this.liveThemeCount);
955 },
956
957 // Iterates through each instance of the collection
958 // and renders each theme module
959 renderThemes: function (page) {
960
961 this.instance = this.collection.paginate(page);
962
963 // If we have no more themes bail
964 if (this.instance.length === 0) {
965 this.parent.trigger('theme:end');
966 return;
967 }
968
969 // Make sure the add-new stays at the end
970 if (page >= 1) {
971 $('.add-new-theme').remove();
972 }
973
974 // Loop through the themes and setup each theme view
975 this.instance.each((theme) => {
976
977 this.theme = new themes.view.Theme({
978 model: theme,
979 parent: this
980 });
981
982 // Render the view
983 this.theme.render();
984
985 // Append to container
986 this.$el.append(this.theme.el);
987
988 // Bind expand event
989 this.listenTo(this.theme, 'theme:expand', this.expand);
990 });
991
992 // 'Add new theme' element shown at the end of the grid
993 if (themes.data.settings.canInstall) {
994 this.$el.append(
995 '<div class="theme add-new-theme">' +
996 '<a href="' + themes.data.settings.installURI + '">' +
997 '<div class="theme-screenshot"><span></span></div>' +
998 '<h3 class="theme-name">' + l10n.addNew + '</h3>' +
999 '</a>' +
1000 '</div>'
1001 );
1002 }
1003
1004 this.parent.page++;
1005 },
1006
1007 // Grabs current theme and puts it at the beginning of the collection
1008 currentTheme: function () {
1009 const current = this.collection.findWhere({ active: true });
1010
1011 // Move the active theme to the beginning of the collection
1012 if (current) {
1013 this.collection.remove(current);
1014 this.collection.add(current, { at: 0 });
1015 }
1016 },
1017
1018 // Sets current view
1019 setView: function (view) {
1020 return view;
1021 },
1022
1023 // Renders the overlay with the ThemeDetails view
1024 // Uses the current model data
1025 expand: function (id) {
1026 // Set the current theme model
1027 this.model = this.collection.get(id);
1028
1029 // Trigger a route update for the current model
1030 themes.router.navigate(themes.router.baseUrl(themes.router.themePath + this.model.id));
1031
1032 // Sets this.view to 'detail'
1033 this.setView('detail');
1034 $('body').addClass('modal-open');
1035
1036 // Set up the theme details view
1037 this.overlay = new themes.view.Details({
1038 model: this.model
1039 });
1040
1041 this.overlay.render();
1042 this.$overlay.html(this.overlay.el);
1043
1044 // Bind to theme:next and theme:previous
1045 // triggered by the arrow keys
1046 //
1047 // Keep track of the current model so we
1048 // can infer an index position
1049 this.listenTo(this.overlay, 'theme:next', () => {
1050 // Renders the next theme on the overlay
1051 this.next([this.model.cid]);
1052 })
1053 .listenTo(this.overlay, 'theme:previous', () => {
1054 // Renders the previous theme on the overlay
1055 this.previous([this.model.cid]);
1056 });
1057 },
1058
1059 // This method renders the next theme on the overlay modal
1060 // based on the current position in the collection
1061 // @params [model cid]
1062 next: function (args) {
1063 let model, nextModel;
1064
1065 // Get the current theme
1066 model = this.collection.get(args[0]);
1067 // Find the next model within the collection
1068 nextModel = this.collection.at(this.collection.indexOf(model) + 1);
1069
1070 // Sanity check which also serves as a boundary test
1071 if (nextModel !== undefined) {
1072
1073 // We have a new theme...
1074 // Close the overlay
1075 this.overlay.closeOverlay();
1076
1077 // Trigger a route update for the current model
1078 this.theme.trigger('theme:expand', nextModel.cid);
1079
1080 }
1081 },
1082
1083 // This method renders the previous theme on the overlay modal
1084 // based on the current position in the collection
1085 // @params [model cid]
1086 previous: function (args) {
1087 let model, previousModel;
1088
1089 // Get the current theme
1090 model = this.collection.get(args[0]);
1091 // Find the previous model within the collection
1092 previousModel = this.collection.at(this.collection.indexOf(model) - 1);
1093
1094 if (previousModel !== undefined) {
1095
1096 // We have a new theme...
1097 // Close the overlay
1098 this.overlay.closeOverlay();
1099
1100 // Trigger a route update for the current model
1101 this.theme.trigger('theme:expand', previousModel.cid);
1102
1103 }
1104 },
1105
1106 // Dispatch audible search results feedback message
1107 announceSearchResults: function (count) {
1108 if (0 === count) {
1109 wp.a11y.speak(l10n.noThemesFound);
1110 } else {
1111 wp.a11y.speak(l10n.themesFound.replace('%d', count));
1112 }
1113 }
1114 });
1115
1116 // Search input view controller.
1117 themes.view.Search = wp.Backbone.View.extend({
1118 tagName: 'input',
1119 className: 'wp-filter-search fluid prompt',
1120 id: 'wp-filter-search-input',
1121 searching: false,
1122
1123
1124 attributes: {
1125 placeholder: 'Search themes...',
1126 type: 'text',
1127 'aria-describedby': 'live-search-desc'
1128 },
1129
1130 events: {
1131 'input': 'search',
1132 'keyup': 'search',
1133 'blur': 'pushState'
1134 },
1135
1136 initialize: function (options) {
1137
1138 this.parent = options.parent;
1139
1140 this.listenTo(this.parent, 'theme:close', function () {
1141 this.searching = false;
1142 });
1143
1144 },
1145
1146 search: function (event) {
1147 // Clear on escape.
1148 if (event.type === 'keyup' && event.which === 27) {
1149 event.target.value = '';
1150 }
1151
1152 /**
1153 * Since doSearch is debounced, it will only run when user input comes to a rest
1154 */
1155 this.doSearch(event);
1156 },
1157
1158 // Runs a search on the theme collection.
1159 doSearch: _.debounce(function (event) {
1160 let options = {};
1161
1162 this.collection.doSearch(event.target.value);
1163
1164 // if search is initiated and key is not return
1165 if (this.searching && event.which !== 13) {
1166 options.replace = true;
1167 } else {
1168 this.searching = true;
1169 }
1170
1171 // Update the URL hash
1172 if (event.target.value) {
1173 themes.router.navigate(themes.router.baseUrl(themes.router.searchPath + event.target.value), options);
1174 } else {
1175 themes.router.navigate(themes.router.baseUrl(''));
1176 }
1177 }, 500),
1178
1179 pushState: function (event) {
1180 let url = themes.router.baseUrl('');
1181
1182 if (event.target.value) {
1183 url = themes.router.baseUrl(themes.router.searchPath + event.target.value);
1184 }
1185
1186 this.searching = false;
1187 themes.router.navigate(url);
1188
1189 }
1190 });
1191
1192 // Sets up the routes events for relevant url queries
1193 // Listens to [theme] and [search] params
1194 themes.Router = Backbone.Router.extend({
1195
1196 routes: {
1197 'themes.php?theme=:slug': 'theme',
1198 'themes.php?search=:query': 'search',
1199 'themes.php?s=:query': 'search',
1200 'themes.php': 'themes',
1201 '': 'themes'
1202 },
1203
1204 baseUrl: function (url) {
1205 return 'themes.php' + url;
1206 },
1207
1208 themePath: '?theme=',
1209 searchPath: '?search=',
1210
1211 search: function (query) {
1212 $('.wp-filter-search').val(query);
1213 },
1214
1215 themes: function () {
1216 $('.wp-filter-search').val('');
1217 },
1218
1219 navigate: function () {
1220 if (Backbone.history._hasPushState) {
1221 Backbone.Router.prototype.navigate.apply(this, arguments);
1222 }
1223 }
1224
1225 });
1226
1227 // Execute and setup the application
1228 themes.Run = {
1229 init: function () {
1230 // Initializes the blog's theme library view
1231 // Create a new collection with data
1232 this.themes = new themes.Collection(themes.data.themes);
1233
1234 // Set up the view
1235 this.view = new themes.view.Appearance({
1236 collection: this.themes
1237 });
1238
1239 this.render();
1240 },
1241
1242 render: function () {
1243
1244 // Render results
1245 this.view.render();
1246 this.routes();
1247
1248 Backbone.history.start({
1249 root: themes.data.settings.adminUrl,
1250 pushState: true,
1251 hashChange: false
1252 });
1253 },
1254 routes: function () {
1255
1256 // Bind to our global themes object
1257 themes.router = new themes.Router();
1258
1259 // Handles theme details route event
1260 themes.router.on('route:theme', (slug) => {
1261 this.view.view.expand(slug);
1262 });
1263
1264 themes.router.on('route:themes', () => {
1265 this.themes.doSearch('');
1266 this.view.trigger('theme:close');
1267 });
1268
1269 // Handles search route event
1270 themes.router.on('route:search', () => {
1271 $('.wp-filter-search').trigger('keyup');
1272 });
1273
1274 this.extraRoutes();
1275 },
1276
1277
1278 extraRoutes: function () {
1279 return false;
1280 }
1281 };
1282
1283 // Extend the main Search view
1284 themes.view.InstallerSearch = themes.view.Search.extend({
1285
1286 events: {
1287 'input': 'search',
1288 'keyup': 'search'
1289 },
1290
1291 // Handles Ajax request for searching through themes in public repo
1292 search: function (event) {
1293
1294 // Tabbing or reverse tabbing into the search input shouldn't trigger a search
1295 if (event.type === 'keyup' && (event.which === 9 || event.which === 16)) {
1296 return;
1297 }
1298
1299 this.collection = this.options.parent.view.collection;
1300
1301 // Clear on escape.
1302 if (event.type === 'keyup' && event.which === 27) {
1303 event.target.value = '';
1304 }
1305
1306 this.doSearch(event.target.value);
1307 },
1308
1309 doSearch: _.debounce(function (value) {
1310 let request = {};
1311
1312 request.search = value;
1313
1314 // Intercept an [author] search.
1315 //
1316 // If input value starts with `author:` send a request
1317 // for `author` instead of a regular `search`
1318 if (value.substring(0, 7) === 'author:') {
1319 request.search = '';
1320 request.author = value.slice(7);
1321 }
1322
1323 // Intercept a [tag] search.
1324 //
1325 // If input value starts with `tag:` send a request
1326 // for `tag` instead of a regular `search`
1327 if (value.substring(0, 4) === 'tag:') {
1328 request.search = '';
1329 request.tag = [value.slice(4)];
1330 }
1331
1332 $('.filter-links li > a.current').removeClass('current');
1333 $('body').removeClass('show-filters filters-applied');
1334
1335 // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1336 // or searching the local cache
1337 this.collection.query(request);
1338
1339 // Set route
1340 themes.router.navigate(themes.router.baseUrl(themes.router.searchPath + value), { replace: true });
1341 }, 500)
1342 });
1343
1344 themes.view.Installer = themes.view.Appearance.extend({
1345
1346 el: '#wpbody-content .mainwp-content-wrap',
1347
1348 // Register events for sorting and filters in theme-navigation
1349 events: {
1350 'click .filter-links li > a': 'onSort',
1351 'click .theme-filter': 'onFilter',
1352 'click .drawer-toggle': 'moreFilters',
1353 'click .filter-drawer .apply-filters': 'applyFilters',
1354 'click .filter-group [type="checkbox"]': 'addFilter',
1355 'click .filter-drawer .clear-filters': 'clearFilters',
1356 'click .filtered-by': 'backToFilters'
1357 },
1358
1359 // Initial render method
1360 render: function () {
1361
1362 this.search();
1363 this.uploader();
1364
1365 this.collection = new themes.Collection();
1366
1367 // Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
1368 this.listenTo(this, 'theme:end', () => {
1369
1370 // Make sure we are not already loading
1371 if (this.collection.loadingThemes) {
1372 return;
1373 }
1374
1375 // Set loadingThemes to true and bump page instance of currentQuery.
1376 this.collection.loadingThemes = true;
1377 this.collection.currentQuery.page++;
1378
1379 // Use currentQuery.page to build the themes request.
1380 _.extend(this.collection.currentQuery.request, { page: this.collection.currentQuery.page });
1381 this.collection.query(this.collection.currentQuery.request);
1382 });
1383
1384 this.listenTo(this.collection, 'query:success', function () {
1385 $('body').removeClass('loading-content');
1386 $('.mainwp-browse-themes').find('div.error').remove();
1387 $('.card .ui.star.rating').rating(); // for adding to favorites
1388 });
1389
1390 this.listenTo(this.collection, 'query:fail', function () {
1391 $('body').removeClass('loading-content');
1392 $('.mainwp-browse-themes').find('div.error').remove();
1393 $('.mainwp-browse-themes').find('div.themes').before('<div class="error"><p>' + l10n.error + '</p></div>');
1394 });
1395
1396 if (this.view) {
1397 this.view.remove();
1398 }
1399
1400 // Set ups the view and passes the section argument
1401 this.view = new themes.view.Themes({
1402 collection: this.collection,
1403 parent: this
1404 });
1405
1406 // Reset pagination every time the install view handler is run
1407 this.page = 0;
1408
1409 // Render and append
1410 this.$el.find('.themes').remove();
1411 this.view.render();
1412 this.$el.find('.mainwp-browse-themes').append(this.view.el).addClass('rendered');
1413 },
1414
1415 // Handles all the rendering of the public theme directory
1416 browse: function (section) {
1417 // Create a new collection with the proper theme data
1418 // for each section
1419 this.collection.query({ browse: section });
1420 },
1421
1422 // Sorting navigation
1423 onSort: function (event) {
1424 let $el = $(event.target),
1425 sort = $el.data('sort');
1426
1427 event.preventDefault();
1428
1429 $('body').removeClass('filters-applied show-filters');
1430
1431 // Bail if this is already active
1432 if ($el.hasClass(this.activeClass)) {
1433 return;
1434 }
1435
1436 this.sort(sort);
1437
1438 // Trigger a router.naviagte update
1439 themes.router.navigate(themes.router.baseUrl(themes.router.browsePath + sort));
1440 },
1441
1442 sort: function (sort) {
1443 this.clearSearch();
1444
1445 $('.filter-links li > a, .theme-filter').removeClass(this.activeClass);
1446 $('[data-sort="' + sort + '"]').addClass(this.activeClass);
1447
1448 this.browse(sort);
1449 },
1450
1451 // Filters and Tags
1452 onFilter: function (event) {
1453 let request,
1454 $el = $(event.target),
1455 filter = $el.data('filter');
1456
1457 // Bail if this is already active
1458 if ($el.hasClass(this.activeClass)) {
1459 return;
1460 }
1461
1462 $('.filter-links li > a, .theme-section').removeClass(this.activeClass);
1463 $el.addClass(this.activeClass);
1464
1465 if (!filter) {
1466 return;
1467 }
1468
1469 // Construct the filter request
1470 // using the default values
1471 filter = _.union(filter, this.filtersChecked());
1472 request = { tag: [filter] };
1473
1474 // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1475 // or searching the local cache
1476 this.collection.query(request);
1477 },
1478
1479 // Clicking on a checkbox to add another filter to the request
1480 addFilter: function () {
1481 this.filtersChecked();
1482 },
1483
1484 // Applying filters triggers a tag request
1485 applyFilters: function (event) {
1486 let name,
1487 tags = this.filtersChecked(),
1488 request = { tag: tags },
1489 filteringBy = $('.filtered-by .tags');
1490
1491 if (event) {
1492 event.preventDefault();
1493 }
1494
1495 $('body').addClass('filters-applied');
1496 $('.filter-links li > a.current').removeClass('current');
1497 filteringBy.empty();
1498
1499 _.each(tags, function (tag) {
1500 name = $('label[for="filter-id-' + tag + '"]').text();
1501 filteringBy.append('<span class="tag">' + name + '</span>');
1502 });
1503
1504 // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1505 // or searching the local cache
1506 this.collection.query(request);
1507 },
1508
1509 // Get the checked filters
1510 // @return {array} of tags or false
1511 filtersChecked: function () {
1512 let items = $('.filter-group').find(':checkbox'),
1513 tags = [];
1514
1515 _.each(items.filter(':checked'), function (item) {
1516 tags.push($(item).prop('value'));
1517 });
1518
1519 // When no filters are checked, restore initial state and return
1520 if (tags.length === 0) {
1521 $('.filter-drawer .apply-filters').find('span').text('');
1522 $('.filter-drawer .clear-filters').hide();
1523 $('body').removeClass('filters-applied');
1524 return [];
1525 }
1526
1527 $('.filter-drawer .apply-filters').find('span').text(tags.length);
1528 $('.filter-drawer .clear-filters').css('display', 'inline-block');
1529
1530 return tags;
1531 },
1532
1533 activeClass: 'current',
1534
1535 // Overwrite search container class to append search
1536 // in new location
1537 searchContainer: $('#mainwp-search-themes-input-container'),
1538
1539 uploader: function () {
1540 $('a.upload').on('click', function (event) {
1541 event.preventDefault();
1542 $('.mainwp-bulk-install-showhide-content').hide();
1543 $('.mainwp-upload-theme').show();
1544 themes.router.navigate(themes.router.baseUrl('&upload'), { replace: true });
1545 $('a.mainwp-bulk-install-tabs-header-btn').removeClass('green');
1546 $(this).addClass('green');
1547 $('#mainwp_theme_bulk_install_btn').attr('bulk-action', 'upload');
1548 });
1549 $('a.browse-themes').on('click', function (event) {
1550 event.preventDefault();
1551 $('.mainwp-bulk-install-showhide-content').hide();
1552 $('#mainwp-search-themes-input-container').show();
1553 $('#theme-filter').show();
1554 themes.router.navigate(themes.router.baseUrl(''), { replace: true });
1555 $('a.mainwp-bulk-install-tabs-header-btn').removeClass('green');
1556 $(this).addClass('green');
1557 $('#mainwp_theme_bulk_install_btn').attr('bulk-action', 'install');
1558 });
1559 },
1560
1561 // Toggle the full filters navigation
1562 moreFilters: function (event) {
1563 event.preventDefault();
1564
1565 if ($('body').hasClass('filters-applied')) {
1566 return this.backToFilters();
1567 }
1568
1569 // If the filters section is opened and filters are checked
1570 // run the relevant query collapsing to filtered-by state
1571 if ($('body').hasClass('show-filters') && this.filtersChecked()) {
1572 return this.addFilter();
1573 }
1574
1575 this.clearSearch();
1576
1577 themes.router.navigate(themes.router.baseUrl(''));
1578 $('body').toggleClass('show-filters');
1579 },
1580
1581 // Clears all the checked filters
1582 // @uses filtersChecked()
1583 clearFilters: function (event) {
1584 let items = $('.filter-group').find(':checkbox')
1585 event.preventDefault();
1586
1587 _.each(items.filter(':checked'), (item) => {
1588 $(item).prop('checked', false);
1589 return this.filtersChecked();
1590 });
1591 },
1592
1593 backToFilters: function (event) {
1594 if (event) {
1595 event.preventDefault();
1596 }
1597
1598 $('body').removeClass('filters-applied');
1599 },
1600
1601 clearSearch: function () {
1602 $('#wp-filter-search-input').val('');
1603 }
1604 });
1605
1606 themes.InstallerRouter = Backbone.Router.extend({
1607 routes: {
1608 'admin.php?page=ThemesInstall&theme=:slug': 'preview',
1609 'admin.php?page=ThemesInstall&browse=:sort': 'sort',
1610 'admin.php?page=ThemesInstall&upload': 'upload',
1611 'admin.php?page=ThemesInstall&search=:query': 'search',
1612 'admin.php?page=ThemesInstall': 'sort'
1613 },
1614
1615 baseUrl: function (url) {
1616 return 'admin.php?page=ThemesInstall' + url;
1617 },
1618
1619 themePath: '&theme=',
1620 browsePath: '&browse=',
1621 searchPath: '&search=',
1622
1623 search: function (query) {
1624 $('.wp-filter-search').val(query);
1625 },
1626
1627 navigate: function () {
1628 if (Backbone.history._hasPushState) {
1629 Backbone.Router.prototype.navigate.apply(this, arguments);
1630 }
1631 }
1632 });
1633
1634
1635 themes.RunInstaller = {
1636
1637 init: function () {
1638 // Set up the view
1639 // Passes the default 'section' as an option
1640 this.view = new themes.view.Installer({
1641 section: 'featured',
1642 SearchView: themes.view.InstallerSearch
1643 });
1644
1645 // Render results
1646 this.render();
1647
1648 },
1649
1650 render: function () {
1651
1652 // Render results
1653 this.view.render();
1654 this.routes();
1655
1656 Backbone.history.start({
1657 root: themes.data.settings.adminUrl,
1658 pushState: true,
1659 hashChange: false
1660 });
1661 },
1662
1663 routes: function () {
1664 let request = {};
1665
1666 // Bind to our global `wp.themes` object
1667 // so that the router is available to sub-views
1668 themes.router = new themes.InstallerRouter();
1669
1670 // Handles `theme` route event
1671 // Queries the API for the passed theme slug
1672 themes.router.on('route:preview', (slug) => {
1673 request.theme = slug;
1674 this.view.collection.query(request);
1675 });
1676
1677 // Handles sorting / browsing routes
1678 // Also handles the root URL triggering a sort request
1679 // for `featured`, the default view
1680 themes.router.on('route:sort', (sort) => {
1681 if (!sort) {
1682 sort = 'featured';
1683 }
1684
1685 this.view.sort(sort);
1686 this.view.trigger('theme:close');
1687 });
1688
1689 // Support the `upload` route by going straight to upload section
1690 themes.router.on('route:upload', function () {
1691 $('a.upload').trigger('click');
1692 });
1693
1694 // The `search` route event. The router populates the input field.
1695 themes.router.on('route:search', function () {
1696 $('.wp-filter-search').trigger('focus').trigger('keyup');
1697 });
1698
1699 this.extraRoutes();
1700 },
1701
1702 extraRoutes: function () {
1703 return false;
1704 }
1705 };
1706
1707 // Ready...
1708 $(document).ready(function () {
1709 if (themes.isInstall) {
1710 themes.RunInstaller.init();
1711 } else {
1712 themes.Run.init();
1713 }
1714
1715 $('.broken-themes .delete-theme').on('click', function () {
1716 return confirm(_mainwpThemeSettings.settings.confirmDelete);
1717 });
1718 });
1719
1720 })(jQuery);
1721
1722 // Align theme browser thickbox
1723 jQuery(function($) {
1724
1725 globalThis.tb_position = function () {
1726 let tbWindow = $('#TB_window'),
1727 width = $(globalThis).width(),
1728 H = $(globalThis).height(),
1729 W = Math.min(1040, width),
1730 adminbar_height = 0;
1731
1732 if ($('#wpadminbar').length) {
1733 adminbar_height = Number.parseInt($('#wpadminbar').css('height'), 10);
1734 }
1735
1736 if (tbWindow.length) {
1737 tbWindow.width(W - 50).height(H - 45 - adminbar_height);
1738 $('#TB_iframeContent').width(W - 50).height(H - 75 - adminbar_height);
1739 tbWindow.css({ 'margin-left': '-' + Number.parseInt(((W - 50) / 2), 10) + 'px' });
1740 if ('maxWidth' in document.body.style) {
1741 tbWindow.css({
1742 top: `${20 + adminbar_height}px`,
1743 marginTop: 0
1744 });
1745 }
1746 }
1747 };
1748
1749 $(globalThis).on('resize', function () {
1750 globalThis.tb_position();
1751 });
1752 });
1753