Changeset 27830
- Timestamp:
- 03/28/2014 09:48:52 PM (11 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-admin/js/theme.js
r27804 r27830 214 214 215 215 return collection; 216 } 217 }); 218 219 // This is the view that controls each theme item 220 // that will be displayed on the screen 221 themes.view.Theme = wp.Backbone.View.extend({ 222 223 // Wrap theme data on a div.theme element 224 className: 'theme', 225 226 // Reflects which theme view we have 227 // 'grid' (default) or 'detail' 228 state: 'grid', 229 230 // The HTML template for each element to be rendered 231 html: themes.template( 'theme' ), 232 233 events: { 234 'click': themes.isInstall ? 'preview': 'expand', 235 'click .preview': 'preview', 236 'keydown': themes.isInstall ? 'preview': 'expand', 237 'touchend': themes.isInstall ? 'preview': 'expand', 238 'keyup': 'addFocus', 239 'touchmove': 'preventExpand' 240 }, 241 242 touchDrag: false, 243 244 render: function() { 245 var data = this.model.toJSON(); 246 // Render themes using the html template 247 this.$el.html( this.html( data ) ).attr({ 248 tabindex: 0, 249 'aria-describedby' : data.id + '-action ' + data.id + '-name' 250 }); 251 252 // Renders active theme styles 253 this.activeTheme(); 254 255 if ( this.model.get( 'displayAuthor' ) ) { 256 this.$el.addClass( 'display-author' ); 257 } 258 }, 259 260 // Adds a class to the currently active theme 261 // and to the overlay in detailed view mode 262 activeTheme: function() { 263 if ( this.model.get( 'active' ) ) { 264 this.$el.addClass( 'active' ); 265 } 266 }, 267 268 // Add class of focus to the theme we are focused on. 269 addFocus: function() { 270 var $themeToFocus = ( $( ':focus' ).hasClass( 'theme' ) ) ? $( ':focus' ) : $(':focus').parents('.theme'); 271 272 $('.theme.focus').removeClass('focus'); 273 $themeToFocus.addClass('focus'); 274 }, 275 276 // Single theme overlay screen 277 // It's shown when clicking a theme 278 expand: function( event ) { 279 var self = this; 280 281 event = event || window.event; 282 283 // 'enter' and 'space' keys expand the details view when a theme is :focused 284 if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) { 285 return; 286 } 287 288 // Bail if the user scrolled on a touch device 289 if ( this.touchDrag === true ) { 290 return this.touchDrag = false; 291 } 292 293 // Prevent the modal from showing when the user clicks 294 // one of the direct action buttons 295 if ( $( event.target ).is( '.theme-actions a' ) ) { 296 return; 297 } 298 299 // Set focused theme to current element 300 themes.focusedTheme = this.$el; 301 302 this.trigger( 'theme:expand', self.model.cid ); 303 }, 304 305 preventExpand: function() { 306 this.touchDrag = true; 307 }, 308 309 preview: function( event ) { 310 // Bail if the user scrolled on a touch device 311 if ( this.touchDrag === true ) { 312 return this.touchDrag = false; 313 } 314 315 // 'enter' and 'space' keys expand the details view when a theme is :focused 316 if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) { 317 return; 318 } 319 320 // pressing enter while focused on the buttons shouldn't open the preview 321 if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) { 322 return; 323 } 324 325 event.preventDefault(); 326 327 event = event || window.event; 328 329 var preview = new themes.view.Preview({ 330 model: this.model 331 }); 332 333 preview.render(); 334 $( 'div.wrap' ).append( preview.el ); 335 } 336 }); 337 338 // Theme Details view 339 // Set ups a modal overlay with the expanded theme data 340 themes.view.Details = wp.Backbone.View.extend({ 341 342 // Wrap theme data on a div.theme element 343 className: 'theme-overlay', 344 345 events: { 346 'click': 'collapse', 347 'click .delete-theme': 'deleteTheme', 348 'click .left': 'previousTheme', 349 'click .right': 'nextTheme' 350 }, 351 352 // The HTML template for the theme overlay 353 html: themes.template( 'theme-single' ), 354 355 render: function() { 356 var data = this.model.toJSON(); 357 this.$el.html( this.html( data ) ); 358 // Renders active theme styles 359 this.activeTheme(); 360 // Set up navigation events 361 this.navigation(); 362 // Checks screenshot size 363 this.screenshotCheck( this.$el ); 364 // Contain "tabbing" inside the overlay 365 this.containFocus( this.$el ); 366 }, 367 368 // Adds a class to the currently active theme 369 // and to the overlay in detailed view mode 370 activeTheme: function() { 371 // Check the model has the active property 372 this.$el.toggleClass( 'active', this.model.get( 'active' ) ); 373 }, 374 375 // Keeps :focus within the theme details elements 376 containFocus: function( $el ) { 377 var $target; 378 379 // Move focus to the primary action 380 _.delay( function() { 381 $( '.theme-wrap a.button-primary:visible' ).focus(); 382 }, 500 ); 383 384 $el.on( 'keydown.wp-themes', function( event ) { 385 386 // Tab key 387 if ( event.which === 9 ) { 388 $target = $( event.target ); 389 390 // Keep focus within the overlay by making the last link on theme actions 391 // switch focus to button.left on tabbing and vice versa 392 if ( $target.is( 'button.left' ) && event.shiftKey ) { 393 $el.find( '.theme-actions a:last-child' ).focus(); 394 event.preventDefault(); 395 } else if ( $target.is( '.theme-actions a:last-child' ) ) { 396 $el.find( 'button.left' ).focus(); 397 event.preventDefault(); 398 } 216 }, 217 218 // Handles requests for more themes 219 // and caches results 220 // 221 // When we are missing a cache object we fire an apiCall() 222 // which triggers events of `query:success` or `query:fail` 223 query: function( request ) { 224 /** 225 * @static 226 * @type Array 227 */ 228 var queries = this.queries, 229 self = this, 230 query, isPaginated, count; 231 232 // Search the query cache for matches. 233 query = _.find( queries, function( query ) { 234 return _.isEqual( query.request, request ); 235 }); 236 237 // If the request matches the stored currentQuery.request 238 // it means we have a paginated request. 239 isPaginated = _.has( request, 'page' ); 240 241 // Reset the internal api page counter for non paginated queries. 242 if ( ! isPaginated ) { 243 this.currentQuery.page = 1; 244 } 245 246 // Otherwise, send a new API call and add it to the cache. 247 if ( ! query ) { 248 query = this.apiCall( request ).done( function( data ) { 249 // Update the collection with the queried data. 250 self.reset( data.themes ); 251 count = data.info.results; 252 253 // Trigger a collection refresh event 254 // and a `query:success` event with a `count` argument. 255 self.trigger( 'update' ); 256 self.trigger( 'query:success', count ); 257 258 // Store the results and the query request 259 queries.push( { themes: data.themes, request: request } ); 260 }).fail( function() { 261 self.trigger( 'query:fail' ); 262 }); 263 } else { 264 // If it's a paginated request we need to fetch more themes... 265 if ( isPaginated ) { 266 return this.apiCall( request, isPaginated ).done( function( data ) { 267 // Add the new themes to the current collection 268 // @todo update counter 269 self.add( data.themes ); 270 self.trigger( 'query:success' ); 271 272 }).fail( function() { 273 self.trigger( 'query:fail' ); 274 }); 399 275 } 400 }); 401 }, 402 403 // Single theme overlay screen 404 // It's shown when clicking a theme 405 collapse: function( event ) { 406 var self = this, 407 scroll; 408 409 event = event || window.event; 410 411 // Prevent collapsing detailed view when there is only one theme available 412 if ( themes.data.themes.length === 1 ) { 413 return; 414 } 415 416 // Detect if the click is inside the overlay 417 // and don't close it unless the target was 418 // the div.back button 419 if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) { 420 421 // Add a temporary closing class while overlay fades out 422 $( 'body' ).addClass( 'closing-overlay' ); 423 424 // With a quick fade out animation 425 this.$el.fadeOut( 130, function() { 426 // Clicking outside the modal box closes the overlay 427 $( 'body' ).removeClass( 'closing-overlay' ); 428 // Handle event cleanup 429 self.closeOverlay(); 430 431 // Get scroll position to avoid jumping to the top 432 scroll = document.body.scrollTop; 433 434 // Clean the url structure 435 themes.router.navigate( themes.router.baseUrl( '' ) ); 436 437 // Restore scroll position 438 document.body.scrollTop = scroll; 439 440 // Return focus to the theme div 441 if ( themes.focusedTheme ) { 442 themes.focusedTheme.focus(); 443 } 444 }); 445 } 446 }, 447 448 // Handles .disabled classes for next/previous buttons 449 navigation: function() { 450 451 // Disable Left/Right when at the start or end of the collection 452 if ( this.model.cid === this.model.collection.at(0).cid ) { 453 this.$el.find( '.left' ).addClass( 'disabled' ); 454 } 455 if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) { 456 this.$el.find( '.right' ).addClass( 'disabled' ); 457 } 458 }, 459 460 // Performs the actions to effectively close 461 // the theme details overlay 462 closeOverlay: function() { 463 $( 'body' ).removeClass( 'theme-overlay-open' ); 464 this.remove(); 465 this.unbind(); 466 this.trigger( 'theme:collapse' ); 467 }, 468 469 // Confirmation dialoge for deleting a theme 470 deleteTheme: function() { 471 return confirm( themes.data.settings.confirmDelete ); 472 }, 473 474 nextTheme: function() { 475 var self = this; 476 self.trigger( 'theme:next', self.model.cid ); 477 }, 478 479 previousTheme: function() { 480 var self = this; 481 self.trigger( 'theme:previous', self.model.cid ); 482 }, 483 484 // Checks if the theme screenshot is the old 300px width version 485 // and adds a corresponding class if it's true 486 screenshotCheck: function( el ) { 487 var screenshot, image; 488 489 screenshot = el.find( '.screenshot img' ); 490 image = new Image(); 491 image.src = screenshot.attr( 'src' ); 492 493 // Width check 494 if ( image.width && image.width <= 300 ) { 495 el.addClass( 'small-screenshot' ); 496 } 497 } 498 }); 499 500 // Theme Preview view 501 // Set ups a modal overlay with the expanded theme data 502 themes.view.Preview = wp.Backbone.View.extend({ 503 504 className: 'wp-full-overlay expanded', 505 el: '#theme-installer', 506 507 events: { 508 'click .close-full-overlay': 'close', 509 'click .collapse-sidebar': 'collapse' 510 }, 511 512 // The HTML template for the theme preview 513 html: themes.template( 'theme-preview' ), 514 515 render: function() { 516 var data = this.model.toJSON(); 517 this.$el.html( this.html( data ) ); 518 519 themes.router.navigate( themes.router.baseUrl( '?theme=' + this.model.get( 'id' ) ), { replace: true } ); 520 521 this.$el.fadeIn( 200, function() { 522 $( 'body' ).addClass( 'theme-installer-active full-overlay-active' ); 523 }); 524 }, 525 526 close: function() { 527 this.$el.fadeOut( 200, function() { 528 $( 'body' ).removeClass( 'theme-installer-active full-overlay-active' ); 529 }); 530 531 themes.router.navigate( themes.router.baseUrl( '' ) ); 532 return false; 533 }, 534 535 collapse: function() { 536 this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' ); 537 return false; 538 } 539 }); 540 541 // Controls the rendering of div.themes, 542 // a wrapper that will hold all the theme elements 543 themes.view.Themes = wp.Backbone.View.extend({ 544 545 className: 'themes', 546 $overlay: $( 'div.theme-overlay' ), 547 548 // Number to keep track of scroll position 549 // while in theme-overlay mode 550 index: 0, 551 552 // The theme count element 553 count: $( '.theme-count' ), 554 555 initialize: function( options ) { 556 var self = this; 557 558 // Set up parent 559 this.parent = options.parent; 560 561 // Set current view to [grid] 562 this.setView( 'grid' ); 563 564 // Move the active theme to the beginning of the collection 565 self.currentTheme(); 566 567 // When the collection is updated by user input... 568 this.listenTo( self.collection, 'update', function() { 569 self.parent.page = 0; 570 self.currentTheme(); 571 self.render( this ); 572 }); 573 574 this.listenTo( this.parent, 'theme:scroll', function() { 575 self.renderThemes( self.parent.page ); 576 }); 577 578 this.listenTo( this.parent, 'theme:close', function() { 579 if ( self.overlay ) { 580 self.overlay.closeOverlay(); 581 } 582 } ); 583 584 // Bind keyboard events. 585 $('body').on( 'keyup', function( event ) { 586 if ( ! self.overlay ) { 587 return; 588 } 589 590 // Pressing the right arrow key fires a theme:next event 591 if ( event.keyCode === 39 ) { 592 self.overlay.nextTheme(); 593 } 594 595 // Pressing the left arrow key fires a theme:previous event 596 if ( event.keyCode === 37 ) { 597 self.overlay.previousTheme(); 598 } 599 600 // Pressing the escape key fires a theme:collapse event 601 if ( event.keyCode === 27 ) { 602 self.overlay.collapse( event ); 603 } 604 }); 605 }, 606 607 // Manages rendering of theme pages 608 // and keeping theme count in sync 609 render: function() { 610 // Clear the DOM, please 611 this.$el.html( '' ); 612 613 // If the user doesn't have switch capabilities 614 // or there is only one theme in the collection 615 // render the detailed view of the active theme 616 if ( themes.data.themes.length === 1 ) { 617 618 // Constructs the view 619 this.singleTheme = new themes.view.Details({ 620 model: this.collection.models[0] 621 }); 622 623 // Render and apply a 'single-theme' class to our container 624 this.singleTheme.render(); 625 this.$el.addClass( 'single-theme' ); 626 this.$el.append( this.singleTheme.el ); 627 } 628 629 // Generate the themes 630 // Using page instance 631 this.renderThemes( this.parent.page ); 632 633 // Display a live theme count for the collection 634 this.count.text( this.collection.length ); 635 }, 636 637 // Iterates through each instance of the collection 638 // and renders each theme module 639 renderThemes: function( page ) { 640 var self = this; 641 642 self.instance = self.collection.paginate( page ); 643 644 // If we have no more themes bail 645 if ( self.instance.length === 0 ) { 646 return; 647 } 648 649 // Make sure the add-new stays at the end 650 if ( page >= 1 ) { 651 $( '.add-new-theme' ).remove(); 652 } 653 654 // Loop through the themes and setup each theme view 655 self.instance.each( function( theme ) { 656 self.theme = new themes.view.Theme({ 657 model: theme 658 }); 659 660 // Render the views... 661 self.theme.render(); 662 // and append them to div.themes 663 self.$el.append( self.theme.el ); 664 665 // Binds to theme:expand to show the modal box 666 // with the theme details 667 self.listenTo( self.theme, 'theme:expand', self.expand, self ); 668 }); 669 670 // 'Add new theme' element shown at the end of the grid 671 if ( themes.data.settings.canInstall ) { 672 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>' ); 673 } 674 675 this.parent.page++; 676 }, 677 678 // Grabs current theme and puts it at the beginning of the collection 679 currentTheme: function() { 680 var self = this, 681 current; 682 683 current = self.collection.findWhere({ active: true }); 684 685 // Move the active theme to the beginning of the collection 686 if ( current ) { 687 self.collection.remove( current ); 688 self.collection.add( current, { at:0 } ); 689 } 690 }, 691 692 // Sets current view 693 setView: function( view ) { 694 return view; 695 }, 696 697 // Renders the overlay with the ThemeDetails view 698 // Uses the current model data 699 expand: function( id ) { 700 var self = this; 701 702 // Set the current theme model 703 this.model = self.collection.get( id ); 704 705 // Trigger a route update for the current model 706 themes.router.navigate( themes.router.baseUrl( '?theme=' + this.model.id ) ); 707 708 // Sets this.view to 'detail' 709 this.setView( 'detail' ); 710 $( 'body' ).addClass( 'theme-overlay-open' ); 711 712 // Set up the theme details view 713 this.overlay = new themes.view.Details({ 714 model: self.model 715 }); 716 717 this.overlay.render(); 718 this.$overlay.html( this.overlay.el ); 719 720 // Bind to theme:next and theme:previous 721 // triggered by the arrow keys 722 // 723 // Keep track of the current model so we 724 // can infer an index position 725 this.listenTo( this.overlay, 'theme:next', function() { 726 // Renders the next theme on the overlay 727 self.next( [ self.model.cid ] ); 728 729 }) 730 .listenTo( this.overlay, 'theme:previous', function() { 731 // Renders the previous theme on the overlay 732 self.previous( [ self.model.cid ] ); 733 }); 734 }, 735 736 // This method renders the next theme on the overlay modal 737 // based on the current position in the collection 738 // @params [model cid] 739 next: function( args ) { 740 var self = this, 741 model, nextModel; 742 743 // Get the current theme 744 model = self.collection.get( args[0] ); 745 // Find the next model within the collection 746 nextModel = self.collection.at( self.collection.indexOf( model ) + 1 ); 747 748 // Sanity check which also serves as a boundary test 749 if ( nextModel !== undefined ) { 750 751 // We have a new theme... 752 // Close the overlay 753 this.overlay.closeOverlay(); 754 755 // Trigger a route update for the current model 756 self.theme.trigger( 'theme:expand', nextModel.cid ); 757 758 } 759 }, 760 761 // This method renders the previous theme on the overlay modal 762 // based on the current position in the collection 763 // @params [model cid] 764 previous: function( args ) { 765 var self = this, 766 model, previousModel; 767 768 // Get the current theme 769 model = self.collection.get( args[0] ); 770 // Find the previous model within the collection 771 previousModel = self.collection.at( self.collection.indexOf( model ) - 1 ); 772 773 if ( previousModel !== undefined ) { 774 775 // We have a new theme... 776 // Close the overlay 777 this.overlay.closeOverlay(); 778 779 // Trigger a route update for the current model 780 self.theme.trigger( 'theme:expand', previousModel.cid ); 781 782 } 783 } 784 }); 785 786 // Search input view controller. 787 themes.view.Search = wp.Backbone.View.extend({ 788 789 tagName: 'input', 790 className: 'theme-search', 791 id: 'theme-search-input', 792 searching: false, 793 794 attributes: { 795 placeholder: l10n.searchPlaceholder, 796 type: 'search' 797 }, 798 799 events: { 800 'input': 'search', 801 'keyup': 'search', 802 'change': 'search', 803 'search': 'search', 804 'blur': 'pushState' 805 }, 806 807 initialize: function( options ) { 808 809 this.parent = options.parent; 810 811 this.listenTo( this.parent, 'theme:close', function() { 812 this.searching = false; 813 } ); 814 815 }, 816 817 // Runs a search on the theme collection. 818 search: function( event ) { 819 var options = {}; 820 821 // Clear on escape. 822 if ( event.type === 'keyup' && event.which === 27 ) { 823 event.target.value = ''; 824 } 825 826 // Lose input focus when pressing enter 827 if ( event.which === 13 ) { 828 this.$el.trigger( 'blur' ); 829 } 830 831 this.collection.doSearch( event.target.value ); 832 833 // if search is initiated and key is not return 834 if ( this.searching && event.which !== 13 ) { 835 options.replace = true; 836 } else { 837 this.searching = true; 838 } 839 840 // Update the URL hash 841 if ( event.target.value ) { 842 themes.router.navigate( themes.router.baseUrl( '?search=' + event.target.value ), options ); 843 } else { 844 themes.router.navigate( themes.router.baseUrl( '' ) ); 845 } 846 }, 847 848 pushState: function( event ) { 849 var url = themes.router.baseUrl( '' ); 850 851 if ( event.target.value ) { 852 url = themes.router.baseUrl( '?search=' + event.target.value ); 853 } 854 855 this.searching = false; 856 themes.router.navigate( url ); 857 858 } 859 }); 860 861 // Sets up the routes events for relevant url queries 862 // Listens to [theme] and [search] params 863 themes.Router = Backbone.Router.extend({ 864 865 routes: { 866 'themes.php?theme=:slug': 'theme', 867 'themes.php?search=:query': 'search', 868 'themes.php?s=:query': 'search', 869 'themes.php': 'themes', 870 '': 'themes' 871 }, 872 873 baseUrl: function( url ) { 874 return 'themes.php' + url; 875 }, 876 877 search: function( query ) { 878 $( '.theme-search' ).val( query ); 879 }, 880 881 themes: function() { 882 $( '.theme-search' ).val( '' ); 883 } 884 885 }); 886 887 // Execute and setup the application 888 themes.Run = { 889 init: function() { 890 // Initializes the blog's theme library view 891 // Create a new collection with data 892 this.themes = new themes.Collection( themes.data.themes ); 893 894 // Set up the view 895 this.view = new themes.view.Appearance({ 896 collection: this.themes 897 }); 898 899 this.render(); 900 }, 901 902 render: function() { 903 904 // Render results 905 this.view.render(); 906 this.routes(); 907 908 Backbone.history.start({ 909 root: themes.data.settings.adminUrl, 910 pushState: true, 911 hashChange: false 912 }); 913 }, 914 915 routes: function() { 916 var self = this; 917 // Bind to our global thx object 918 // so that the object is available to sub-views 919 themes.router = new themes.Router(); 920 921 // Handles theme details route event 922 themes.router.on( 'route:theme', function( slug ) { 923 self.view.view.expand( slug ); 924 }); 925 926 themes.router.on( 'route:themes', function() { 927 self.themes.doSearch( '' ); 928 self.view.trigger( 'theme:close' ); 929 }); 930 931 // Handles search route event 932 themes.router.on( 'route:search', function( query ) { 933 self.view.trigger( 'theme:close' ); 934 self.themes.doSearch( query ); 935 }); 936 937 this.extraRoutes(); 938 }, 939 940 extraRoutes: function() { 941 return false; 942 } 943 }; 944 945 // Extend the main Search view 946 themes.view.InstallerSearch = themes.view.Search.extend({ 947 948 events: { 949 'keyup': 'search' 950 }, 951 952 // Handles Ajax request for searching through themes in public repo 953 search: function( event ) { 954 955 // Tabbing or reverse tabbing into the search input shouldn't trigger a search 956 if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) { 957 return; 958 } 959 960 this.collection = this.options.parent.view.collection; 961 962 // Clear on escape. 963 if ( event.type === 'keyup' && event.which === 27 ) { 964 event.target.value = ''; 965 } 966 967 _.debounce( _.bind( this.doSearch, this ), 300 )( event.target.value ); 968 }, 969 970 doSearch: function( value ) { 971 var request = {}, 972 self = this; 973 974 request.search = value; 975 976 // Intercept an [author] search. 977 // 978 // If input value starts with `author:` send a request 979 // for `author` instead of a regular `search` 980 if ( value.substring( 0, 7 ) === 'author:' ) { 981 request.search = ''; 982 request.author = value.slice( 7 ); 983 } 984 985 // Intercept a [tag] search. 986 // 987 // If input value starts with `tag:` send a request 988 // for `tag` instead of a regular `search` 989 if ( value.substring( 0, 4 ) === 'tag:' ) { 990 request.search = ''; 991 request.tag = [ value.slice( 4 ) ]; 992 } 993 994 // Send Ajax POST request to api.wordpress.org/themes 995 themes.view.Installer.prototype.apiCall( request ).done( function( data ) { 996 // Update the collection with the queried data 997 self.collection.reset( data.themes ); 998 // Trigger a collection refresh event to render the views 999 self.collection.trigger( 'update' ); 1000 1001 // Un-spin it 1002 $( 'body' ).removeClass( 'loading-themes' ); 1003 $( '.theme-browser' ).find( 'div.error' ).remove(); 1004 }).fail( function() { 1005 $( '.theme-browser' ).find( 'div.error' ).remove(); 1006 $( '.theme-browser' ).append( '<div class="error"><p>' + l10n.error + '</p></div>' ); 1007 }); 1008 1009 return false; 1010 } 1011 }); 1012 1013 themes.view.Installer = themes.view.Appearance.extend({ 1014 1015 el: '#wpbody-content .wrap', 1016 1017 // Register events for sorting and filters in theme-navigation 1018 events: { 1019 'click .theme-section': 'onSort', 1020 'click .theme-filter': 'onFilter', 1021 'click .more-filters': 'moreFilters', 1022 'click [type="checkbox"]': 'addFilter' 276 277 // Only trigger an update event since we already have the themes 278 // on our cached object 279 this.reset( query.themes ); 280 this.trigger( 'update' ); 281 } 282 }, 283 284 // Local cache array for API queries 285 queries: [], 286 287 // Keep track of current query so we can handle pagination 288 currentQuery: { 289 page: 1, 290 request: {} 1023 291 }, 1024 292 1025 293 // Send Ajax POST request to api.wordpress.org/themes 1026 apiCall: function( request ) { 294 apiCall: function( request, paginated ) { 295 // Store current query request args 296 // for later use with the event `theme:end` 297 this.currentQuery.request = request; 298 299 // Ajax request to .org API 1027 300 return $.ajax({ 1028 301 url: 'https://api.wordpress.org/themes/info/1.1/?action=query_themes', … … 1037 310 action: 'query_themes', 1038 311 request: _.extend({ 1039 per_page: 36,312 per_page: 72, 1040 313 fields: { 1041 314 description: true, … … 1053 326 1054 327 beforeSend: function() { 1055 // Spin it 1056 $( 'body' ).addClass( 'loading-themes' ); 328 if ( ! paginated ) { 329 // Spin it 330 $( 'body' ).addClass( 'loading-themes' ); 331 } 1057 332 } 1058 333 }); 334 } 335 }); 336 337 // This is the view that controls each theme item 338 // that will be displayed on the screen 339 themes.view.Theme = wp.Backbone.View.extend({ 340 341 // Wrap theme data on a div.theme element 342 className: 'theme', 343 344 // Reflects which theme view we have 345 // 'grid' (default) or 'detail' 346 state: 'grid', 347 348 // The HTML template for each element to be rendered 349 html: themes.template( 'theme' ), 350 351 events: { 352 'click': themes.isInstall ? 'preview': 'expand', 353 'click .preview': 'preview', 354 'keydown': themes.isInstall ? 'preview': 'expand', 355 'touchend': themes.isInstall ? 'preview': 'expand', 356 'keyup': 'addFocus', 357 'touchmove': 'preventExpand' 358 }, 359 360 touchDrag: false, 361 362 render: function() { 363 var data = this.model.toJSON(); 364 // Render themes using the html template 365 this.$el.html( this.html( data ) ).attr({ 366 tabindex: 0, 367 'aria-describedby' : data.id + '-action ' + data.id + '-name' 368 }); 369 370 // Renders active theme styles 371 this.activeTheme(); 372 373 if ( this.model.get( 'displayAuthor' ) ) { 374 this.$el.addClass( 'display-author' ); 375 } 376 }, 377 378 // Adds a class to the currently active theme 379 // and to the overlay in detailed view mode 380 activeTheme: function() { 381 if ( this.model.get( 'active' ) ) { 382 this.$el.addClass( 'active' ); 383 } 384 }, 385 386 // Add class of focus to the theme we are focused on. 387 addFocus: function() { 388 var $themeToFocus = ( $( ':focus' ).hasClass( 'theme' ) ) ? $( ':focus' ) : $(':focus').parents('.theme'); 389 390 $('.theme.focus').removeClass('focus'); 391 $themeToFocus.addClass('focus'); 392 }, 393 394 // Single theme overlay screen 395 // It's shown when clicking a theme 396 expand: function( event ) { 397 var self = this; 398 399 event = event || window.event; 400 401 // 'enter' and 'space' keys expand the details view when a theme is :focused 402 if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) { 403 return; 404 } 405 406 // Bail if the user scrolled on a touch device 407 if ( this.touchDrag === true ) { 408 return this.touchDrag = false; 409 } 410 411 // Prevent the modal from showing when the user clicks 412 // one of the direct action buttons 413 if ( $( event.target ).is( '.theme-actions a' ) ) { 414 return; 415 } 416 417 // Set focused theme to current element 418 themes.focusedTheme = this.$el; 419 420 this.trigger( 'theme:expand', self.model.cid ); 421 }, 422 423 preventExpand: function() { 424 this.touchDrag = true; 425 }, 426 427 preview: function( event ) { 428 // Bail if the user scrolled on a touch device 429 if ( this.touchDrag === true ) { 430 return this.touchDrag = false; 431 } 432 433 // 'enter' and 'space' keys expand the details view when a theme is :focused 434 if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) { 435 return; 436 } 437 438 // pressing enter while focused on the buttons shouldn't open the preview 439 if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) { 440 return; 441 } 442 443 event.preventDefault(); 444 445 event = event || window.event; 446 447 var preview = new themes.view.Preview({ 448 model: this.model 449 }); 450 451 preview.render(); 452 $( 'div.wrap' ).append( preview.el ); 453 } 454 }); 455 456 // Theme Details view 457 // Set ups a modal overlay with the expanded theme data 458 themes.view.Details = wp.Backbone.View.extend({ 459 460 // Wrap theme data on a div.theme element 461 className: 'theme-overlay', 462 463 events: { 464 'click': 'collapse', 465 'click .delete-theme': 'deleteTheme', 466 'click .left': 'previousTheme', 467 'click .right': 'nextTheme' 468 }, 469 470 // The HTML template for the theme overlay 471 html: themes.template( 'theme-single' ), 472 473 render: function() { 474 var data = this.model.toJSON(); 475 this.$el.html( this.html( data ) ); 476 // Renders active theme styles 477 this.activeTheme(); 478 // Set up navigation events 479 this.navigation(); 480 // Checks screenshot size 481 this.screenshotCheck( this.$el ); 482 // Contain "tabbing" inside the overlay 483 this.containFocus( this.$el ); 484 }, 485 486 // Adds a class to the currently active theme 487 // and to the overlay in detailed view mode 488 activeTheme: function() { 489 // Check the model has the active property 490 this.$el.toggleClass( 'active', this.model.get( 'active' ) ); 491 }, 492 493 // Keeps :focus within the theme details elements 494 containFocus: function( $el ) { 495 var $target; 496 497 // Move focus to the primary action 498 _.delay( function() { 499 $( '.theme-wrap a.button-primary:visible' ).focus(); 500 }, 500 ); 501 502 $el.on( 'keydown.wp-themes', function( event ) { 503 504 // Tab key 505 if ( event.which === 9 ) { 506 $target = $( event.target ); 507 508 // Keep focus within the overlay by making the last link on theme actions 509 // switch focus to button.left on tabbing and vice versa 510 if ( $target.is( 'button.left' ) && event.shiftKey ) { 511 $el.find( '.theme-actions a:last-child' ).focus(); 512 event.preventDefault(); 513 } else if ( $target.is( '.theme-actions a:last-child' ) ) { 514 $el.find( 'button.left' ).focus(); 515 event.preventDefault(); 516 } 517 } 518 }); 519 }, 520 521 // Single theme overlay screen 522 // It's shown when clicking a theme 523 collapse: function( event ) { 524 var self = this, 525 scroll; 526 527 event = event || window.event; 528 529 // Prevent collapsing detailed view when there is only one theme available 530 if ( themes.data.themes.length === 1 ) { 531 return; 532 } 533 534 // Detect if the click is inside the overlay 535 // and don't close it unless the target was 536 // the div.back button 537 if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) { 538 539 // Add a temporary closing class while overlay fades out 540 $( 'body' ).addClass( 'closing-overlay' ); 541 542 // With a quick fade out animation 543 this.$el.fadeOut( 130, function() { 544 // Clicking outside the modal box closes the overlay 545 $( 'body' ).removeClass( 'closing-overlay' ); 546 // Handle event cleanup 547 self.closeOverlay(); 548 549 // Get scroll position to avoid jumping to the top 550 scroll = document.body.scrollTop; 551 552 // Clean the url structure 553 themes.router.navigate( themes.router.baseUrl( '' ) ); 554 555 // Restore scroll position 556 document.body.scrollTop = scroll; 557 558 // Return focus to the theme div 559 if ( themes.focusedTheme ) { 560 themes.focusedTheme.focus(); 561 } 562 }); 563 } 564 }, 565 566 // Handles .disabled classes for next/previous buttons 567 navigation: function() { 568 569 // Disable Left/Right when at the start or end of the collection 570 if ( this.model.cid === this.model.collection.at(0).cid ) { 571 this.$el.find( '.left' ).addClass( 'disabled' ); 572 } 573 if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) { 574 this.$el.find( '.right' ).addClass( 'disabled' ); 575 } 576 }, 577 578 // Performs the actions to effectively close 579 // the theme details overlay 580 closeOverlay: function() { 581 $( 'body' ).removeClass( 'theme-overlay-open' ); 582 this.remove(); 583 this.unbind(); 584 this.trigger( 'theme:collapse' ); 585 }, 586 587 // Confirmation dialoge for deleting a theme 588 deleteTheme: function() { 589 return confirm( themes.data.settings.confirmDelete ); 590 }, 591 592 nextTheme: function() { 593 var self = this; 594 self.trigger( 'theme:next', self.model.cid ); 595 }, 596 597 previousTheme: function() { 598 var self = this; 599 self.trigger( 'theme:previous', self.model.cid ); 600 }, 601 602 // Checks if the theme screenshot is the old 300px width version 603 // and adds a corresponding class if it's true 604 screenshotCheck: function( el ) { 605 var screenshot, image; 606 607 screenshot = el.find( '.screenshot img' ); 608 image = new Image(); 609 image.src = screenshot.attr( 'src' ); 610 611 // Width check 612 if ( image.width && image.width <= 300 ) { 613 el.addClass( 'small-screenshot' ); 614 } 615 } 616 }); 617 618 // Theme Preview view 619 // Set ups a modal overlay with the expanded theme data 620 themes.view.Preview = wp.Backbone.View.extend({ 621 622 className: 'wp-full-overlay expanded', 623 el: '#theme-installer', 624 625 events: { 626 'click .close-full-overlay': 'close', 627 'click .collapse-sidebar': 'collapse' 628 }, 629 630 // The HTML template for the theme preview 631 html: themes.template( 'theme-preview' ), 632 633 render: function() { 634 var data = this.model.toJSON(); 635 this.$el.html( this.html( data ) ); 636 637 themes.router.navigate( themes.router.baseUrl( '?theme=' + this.model.get( 'id' ) ), { replace: true } ); 638 639 this.$el.fadeIn( 200, function() { 640 $( 'body' ).addClass( 'theme-installer-active full-overlay-active' ); 641 }); 642 }, 643 644 close: function() { 645 this.$el.fadeOut( 200, function() { 646 $( 'body' ).removeClass( 'theme-installer-active full-overlay-active' ); 647 }); 648 649 themes.router.navigate( themes.router.baseUrl( '' ) ); 650 return false; 651 }, 652 653 collapse: function() { 654 this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' ); 655 return false; 656 } 657 }); 658 659 // Controls the rendering of div.themes, 660 // a wrapper that will hold all the theme elements 661 themes.view.Themes = wp.Backbone.View.extend({ 662 663 className: 'themes', 664 $overlay: $( 'div.theme-overlay' ), 665 666 // Number to keep track of scroll position 667 // while in theme-overlay mode 668 index: 0, 669 670 // The theme count element 671 count: $( '.theme-count' ), 672 673 initialize: function( options ) { 674 var self = this; 675 676 // Set up parent 677 this.parent = options.parent; 678 679 // Set current view to [grid] 680 this.setView( 'grid' ); 681 682 // Move the active theme to the beginning of the collection 683 self.currentTheme(); 684 685 // When the collection is updated by user input... 686 this.listenTo( self.collection, 'update', function() { 687 self.parent.page = 0; 688 self.currentTheme(); 689 self.render( this ); 690 }); 691 692 // Update theme count to full result set when available. 693 this.listenTo( self.collection, 'query:success', function( count ) { 694 if ( _.isNumber( count ) ) { 695 self.count.text( count ); 696 } else { 697 self.count.text( self.collection.length ); 698 } 699 }); 700 701 this.listenTo( this.parent, 'theme:scroll', function() { 702 self.renderThemes( self.parent.page ); 703 }); 704 705 this.listenTo( this.parent, 'theme:close', function() { 706 if ( self.overlay ) { 707 self.overlay.closeOverlay(); 708 } 709 } ); 710 711 // Bind keyboard events. 712 $( 'body' ).on( 'keyup', function( event ) { 713 if ( ! self.overlay ) { 714 return; 715 } 716 717 // Pressing the right arrow key fires a theme:next event 718 if ( event.keyCode === 39 ) { 719 self.overlay.nextTheme(); 720 } 721 722 // Pressing the left arrow key fires a theme:previous event 723 if ( event.keyCode === 37 ) { 724 self.overlay.previousTheme(); 725 } 726 727 // Pressing the escape key fires a theme:collapse event 728 if ( event.keyCode === 27 ) { 729 self.overlay.collapse( event ); 730 } 731 }); 732 }, 733 734 // Manages rendering of theme pages 735 // and keeping theme count in sync 736 render: function() { 737 // Clear the DOM, please 738 this.$el.html( '' ); 739 740 // If the user doesn't have switch capabilities 741 // or there is only one theme in the collection 742 // render the detailed view of the active theme 743 if ( themes.data.themes.length === 1 ) { 744 745 // Constructs the view 746 this.singleTheme = new themes.view.Details({ 747 model: this.collection.models[0] 748 }); 749 750 // Render and apply a 'single-theme' class to our container 751 this.singleTheme.render(); 752 this.$el.addClass( 'single-theme' ); 753 this.$el.append( this.singleTheme.el ); 754 } 755 756 // Generate the themes 757 // Using page instance 758 // While checking the collection has items 759 if ( this.options.collection.size() > 0 ) { 760 this.renderThemes( this.parent.page ); 761 } 762 763 // Display a live theme count for the collection 764 this.count.text( this.collection.length ); 765 }, 766 767 // Iterates through each instance of the collection 768 // and renders each theme module 769 renderThemes: function( page ) { 770 var self = this; 771 772 self.instance = self.collection.paginate( page ); 773 774 // If we have no more themes bail 775 if ( self.instance.size() === 0 ) { 776 // Fire a no-more-themes event. 777 this.parent.trigger( 'theme:end' ); 778 return; 779 } 780 781 // Make sure the add-new stays at the end 782 if ( page >= 1 ) { 783 $( '.add-new-theme' ).remove(); 784 } 785 786 // Loop through the themes and setup each theme view 787 self.instance.each( function( theme ) { 788 self.theme = new themes.view.Theme({ 789 model: theme 790 }); 791 792 // Render the views... 793 self.theme.render(); 794 // and append them to div.themes 795 self.$el.append( self.theme.el ); 796 797 // Binds to theme:expand to show the modal box 798 // with the theme details 799 self.listenTo( self.theme, 'theme:expand', self.expand, self ); 800 }); 801 802 // 'Add new theme' element shown at the end of the grid 803 if ( themes.data.settings.canInstall ) { 804 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>' ); 805 } 806 807 this.parent.page++; 808 }, 809 810 // Grabs current theme and puts it at the beginning of the collection 811 currentTheme: function() { 812 var self = this, 813 current; 814 815 current = self.collection.findWhere({ active: true }); 816 817 // Move the active theme to the beginning of the collection 818 if ( current ) { 819 self.collection.remove( current ); 820 self.collection.add( current, { at:0 } ); 821 } 822 }, 823 824 // Sets current view 825 setView: function( view ) { 826 return view; 827 }, 828 829 // Renders the overlay with the ThemeDetails view 830 // Uses the current model data 831 expand: function( id ) { 832 var self = this; 833 834 // Set the current theme model 835 this.model = self.collection.get( id ); 836 837 // Trigger a route update for the current model 838 themes.router.navigate( themes.router.baseUrl( '?theme=' + this.model.id ) ); 839 840 // Sets this.view to 'detail' 841 this.setView( 'detail' ); 842 $( 'body' ).addClass( 'theme-overlay-open' ); 843 844 // Set up the theme details view 845 this.overlay = new themes.view.Details({ 846 model: self.model 847 }); 848 849 this.overlay.render(); 850 this.$overlay.html( this.overlay.el ); 851 852 // Bind to theme:next and theme:previous 853 // triggered by the arrow keys 854 // 855 // Keep track of the current model so we 856 // can infer an index position 857 this.listenTo( this.overlay, 'theme:next', function() { 858 // Renders the next theme on the overlay 859 self.next( [ self.model.cid ] ); 860 861 }) 862 .listenTo( this.overlay, 'theme:previous', function() { 863 // Renders the previous theme on the overlay 864 self.previous( [ self.model.cid ] ); 865 }); 866 }, 867 868 // This method renders the next theme on the overlay modal 869 // based on the current position in the collection 870 // @params [model cid] 871 next: function( args ) { 872 var self = this, 873 model, nextModel; 874 875 // Get the current theme 876 model = self.collection.get( args[0] ); 877 // Find the next model within the collection 878 nextModel = self.collection.at( self.collection.indexOf( model ) + 1 ); 879 880 // Sanity check which also serves as a boundary test 881 if ( nextModel !== undefined ) { 882 883 // We have a new theme... 884 // Close the overlay 885 this.overlay.closeOverlay(); 886 887 // Trigger a route update for the current model 888 self.theme.trigger( 'theme:expand', nextModel.cid ); 889 890 } 891 }, 892 893 // This method renders the previous theme on the overlay modal 894 // based on the current position in the collection 895 // @params [model cid] 896 previous: function( args ) { 897 var self = this, 898 model, previousModel; 899 900 // Get the current theme 901 model = self.collection.get( args[0] ); 902 // Find the previous model within the collection 903 previousModel = self.collection.at( self.collection.indexOf( model ) - 1 ); 904 905 if ( previousModel !== undefined ) { 906 907 // We have a new theme... 908 // Close the overlay 909 this.overlay.closeOverlay(); 910 911 // Trigger a route update for the current model 912 self.theme.trigger( 'theme:expand', previousModel.cid ); 913 914 } 915 } 916 }); 917 918 // Search input view controller. 919 themes.view.Search = wp.Backbone.View.extend({ 920 921 tagName: 'input', 922 className: 'theme-search', 923 id: 'theme-search-input', 924 searching: false, 925 926 attributes: { 927 placeholder: l10n.searchPlaceholder, 928 type: 'search' 929 }, 930 931 events: { 932 'input': 'search', 933 'keyup': 'search', 934 'change': 'search', 935 'search': 'search', 936 'blur': 'pushState' 937 }, 938 939 initialize: function( options ) { 940 941 this.parent = options.parent; 942 943 this.listenTo( this.parent, 'theme:close', function() { 944 this.searching = false; 945 } ); 946 947 }, 948 949 // Runs a search on the theme collection. 950 search: function( event ) { 951 var options = {}; 952 953 // Clear on escape. 954 if ( event.type === 'keyup' && event.which === 27 ) { 955 event.target.value = ''; 956 } 957 958 // Lose input focus when pressing enter 959 if ( event.which === 13 ) { 960 this.$el.trigger( 'blur' ); 961 } 962 963 this.collection.doSearch( event.target.value ); 964 965 // if search is initiated and key is not return 966 if ( this.searching && event.which !== 13 ) { 967 options.replace = true; 968 } else { 969 this.searching = true; 970 } 971 972 // Update the URL hash 973 if ( event.target.value ) { 974 themes.router.navigate( themes.router.baseUrl( '?search=' + event.target.value ), options ); 975 } else { 976 themes.router.navigate( themes.router.baseUrl( '' ) ); 977 } 978 }, 979 980 pushState: function( event ) { 981 var url = themes.router.baseUrl( '' ); 982 983 if ( event.target.value ) { 984 url = themes.router.baseUrl( '?search=' + event.target.value ); 985 } 986 987 this.searching = false; 988 themes.router.navigate( url ); 989 990 } 991 }); 992 993 // Sets up the routes events for relevant url queries 994 // Listens to [theme] and [search] params 995 themes.Router = Backbone.Router.extend({ 996 997 routes: { 998 'themes.php?theme=:slug': 'theme', 999 'themes.php?search=:query': 'search', 1000 'themes.php?s=:query': 'search', 1001 'themes.php': 'themes', 1002 '': 'themes' 1003 }, 1004 1005 baseUrl: function( url ) { 1006 return 'themes.php' + url; 1007 }, 1008 1009 search: function( query ) { 1010 $( '.theme-search' ).val( query ); 1011 }, 1012 1013 themes: function() { 1014 $( '.theme-search' ).val( '' ); 1015 } 1016 1017 }); 1018 1019 // Execute and setup the application 1020 themes.Run = { 1021 init: function() { 1022 // Initializes the blog's theme library view 1023 // Create a new collection with data 1024 this.themes = new themes.Collection( themes.data.themes ); 1025 1026 // Set up the view 1027 this.view = new themes.view.Appearance({ 1028 collection: this.themes 1029 }); 1030 1031 this.render(); 1032 }, 1033 1034 render: function() { 1035 1036 // Render results 1037 this.view.render(); 1038 this.routes(); 1039 1040 Backbone.history.start({ 1041 root: themes.data.settings.adminUrl, 1042 pushState: true, 1043 hashChange: false 1044 }); 1045 }, 1046 1047 routes: function() { 1048 var self = this; 1049 // Bind to our global thx object 1050 // so that the object is available to sub-views 1051 themes.router = new themes.Router(); 1052 1053 // Handles theme details route event 1054 themes.router.on( 'route:theme', function( slug ) { 1055 self.view.view.expand( slug ); 1056 }); 1057 1058 themes.router.on( 'route:themes', function() { 1059 self.themes.doSearch( '' ); 1060 self.view.trigger( 'theme:close' ); 1061 }); 1062 1063 // Handles search route event 1064 themes.router.on( 'route:search', function( query ) { 1065 self.view.trigger( 'theme:close' ); 1066 self.themes.doSearch( query ); 1067 }); 1068 1069 this.extraRoutes(); 1070 }, 1071 1072 extraRoutes: function() { 1073 return false; 1074 } 1075 }; 1076 1077 // Extend the main Search view 1078 themes.view.InstallerSearch = themes.view.Search.extend({ 1079 1080 events: { 1081 'keyup': 'search' 1082 }, 1083 1084 // Handles Ajax request for searching through themes in public repo 1085 search: function( event ) { 1086 1087 // Tabbing or reverse tabbing into the search input shouldn't trigger a search 1088 if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) { 1089 return; 1090 } 1091 1092 this.collection = this.options.parent.view.collection; 1093 1094 // Clear on escape. 1095 if ( event.type === 'keyup' && event.which === 27 ) { 1096 event.target.value = ''; 1097 } 1098 1099 _.debounce( _.bind( this.doSearch, this ), 300 )( event.target.value ); 1100 }, 1101 1102 doSearch: _.debounce( function( value ) { 1103 var request = {}, 1104 self = this; 1105 1106 request.search = value; 1107 1108 // Intercept an [author] search. 1109 // 1110 // If input value starts with `author:` send a request 1111 // for `author` instead of a regular `search` 1112 if ( value.substring( 0, 7 ) === 'author:' ) { 1113 request.search = ''; 1114 request.author = value.slice( 7 ); 1115 } 1116 1117 // Intercept a [tag] search. 1118 // 1119 // If input value starts with `tag:` send a request 1120 // for `tag` instead of a regular `search` 1121 if ( value.substring( 0, 4 ) === 'tag:' ) { 1122 request.search = ''; 1123 request.tag = [ value.slice( 4 ) ]; 1124 } 1125 1126 // Get the themes by sending Ajax POST request to api.wordpress.org/themes 1127 // or searching the local cache 1128 this.collection.query( request ); 1129 }, 300 ), 1130 }); 1131 1132 themes.view.Installer = themes.view.Appearance.extend({ 1133 1134 el: '#wpbody-content .wrap', 1135 1136 // Register events for sorting and filters in theme-navigation 1137 events: { 1138 'click .theme-section': 'onSort', 1139 'click .theme-filter': 'onFilter', 1140 'click .more-filters': 'moreFilters', 1141 'click [type="checkbox"]': 'addFilter' 1059 1142 }, 1060 1143 … … 1063 1146 var self = this; 1064 1147 1065 // @todo Cache the collection after fetching based on the section1066 1148 this.collection = new themes.Collection(); 1149 1150 // Bump `collection.currentQuery.page` and request more themes if we hit the end of the page. 1151 this.listenTo( this, 'theme:end', function() { 1152 self.collection.currentQuery.page++; 1153 _.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } ); 1154 self.collection.query( self.collection.currentQuery.request ); 1155 }); 1156 1157 this.listenTo( this.collection, 'query:success', function() { 1158 $( 'body' ).removeClass( 'loading-themes' ); 1159 $( '.theme-browser' ).find( 'div.error' ).remove(); 1160 }); 1161 1162 this.listenTo( this.collection, 'query:fail', function() { 1163 $( '.theme-browser' ).find( 'div.error' ).remove(); 1164 $( '.theme-browser' ).append( '<div class="error"><p>' + l10n.error + '</p></div>' ); 1165 }); 1067 1166 1068 1167 // Create a new collection with the proper theme data 1069 1168 // for each section 1070 this.apiCall({ browse: section }).done( function( data ) { 1071 // Update the collection with the queried data 1072 self.collection.reset( data.themes ); 1073 // Trigger a collection refresh event to render the views 1074 self.collection.trigger( 'update' ); 1075 1076 // Un-spin it 1077 $( 'body' ).removeClass( 'loading-themes' ); 1078 $( '.theme-browser' ).find( 'div.error' ).remove(); 1079 }); 1169 this.collection.query( { browse: section } ); 1080 1170 1081 1171 if ( this.view ) { … … 1154 1244 // Construct the filter request 1155 1245 // using the default values 1156 1157 // @todo Cache the collection after fetching based on the filter1158 1246 filter = _.union( filter, this.filtersChecked() ); 1159 1247 request = { tag: [ filter ] }; 1160 1248 1161 // Send Ajax POST request to api.wordpress.org/themes 1162 this.apiCall( request ).done( function( data ) { 1163 // Update the collection with the queried data 1164 self.collection.reset( data.themes ); 1165 // Trigger a collection refresh event to render the views 1166 self.collection.trigger( 'update' ); 1167 1168 // Un-spin it 1169 $( 'body' ).removeClass( 'loading-themes' ); 1170 $( '.theme-browser' ).find( 'div.error' ).remove(); 1171 }).fail( function() { 1172 $( '.theme-browser' ).find( 'div.error' ).remove(); 1173 $( '.theme-browser' ).append( '<div class="error"><p>' + l10n.error + '</p></div>' ); 1174 }); 1175 1176 return false; 1249 // Get the themes by sending Ajax POST request to api.wordpress.org/themes 1250 // or searching the local cache 1251 this.collection.query( request ); 1177 1252 }, 1178 1253 … … 1183 1258 request = { tag: tags }; 1184 1259 1185 // Send Ajax POST request to api.wordpress.org/themes 1186 this.apiCall( request ).done( function( data ) { 1187 // Update the collection with the queried data 1188 self.collection.reset( data.themes ); 1189 // Trigger a collection refresh event to render the views 1190 self.collection.trigger( 'update' ); 1191 1192 // Un-spin it 1193 $( 'body' ).removeClass( 'loading-themes' ); 1194 $( '.theme-browser' ).find( 'div.error' ).remove(); 1195 }).fail( function() { 1196 $( '.theme-browser' ).find( 'div.error' ).remove(); 1197 $( '.theme-browser' ).append( '<div class="error"><p>' + l10n.error + '</p></div>' ); 1198 }); 1260 // Get the themes by sending Ajax POST request to api.wordpress.org/themes 1261 // or searching the local cache 1262 this.collection.query( request ); 1199 1263 }, 1200 1264
Note: See TracChangeset
for help on using the changeset viewer.