PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 5.1.1
MainWP Dashboard: Self-hosted WordPress Management for Agencies v5.1.1
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 5.1.1, at assets/js/mainwp-theme.js

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