PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 5.0.3.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v5.0.3.2
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 5.2.2 5.3 All 153 releases
mainwp / assets / js / mainwp-theme.js

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

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