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

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