[ Index ] |
WordPress Cross Reference |
[Summary view] [Print] [Text view]
1 /* global _wpThemeSettings, confirm */ 2 window.wp = window.wp || {}; 3 4 ( function($) { 5 6 // Set up our namespace... 7 var themes, l10n; 8 themes = wp.themes = wp.themes || {}; 9 10 // Store the theme data and settings for organized and quick access 11 // themes.data.settings, themes.data.themes, themes.data.l10n 12 themes.data = _wpThemeSettings; 13 l10n = themes.data.l10n; 14 15 // Setup app structure 16 _.extend( themes, { model: {}, view: {}, routes: {}, router: {}, template: wp.template }); 17 18 themes.model = Backbone.Model.extend({}); 19 20 // Main view controller for themes.php 21 // Unifies and renders all available views 22 themes.view.Appearance = wp.Backbone.View.extend({ 23 24 el: '#wpbody-content .wrap .theme-browser', 25 26 window: $( window ), 27 // Pagination instance 28 page: 0, 29 30 // Sets up a throttler for binding to 'scroll' 31 initialize: function() { 32 // Scroller checks how far the scroll position is 33 _.bindAll( this, 'scroller' ); 34 35 // Bind to the scroll event and throttle 36 // the results from this.scroller 37 this.window.bind( 'scroll', _.throttle( this.scroller, 300 ) ); 38 }, 39 40 // Main render control 41 render: function() { 42 // Setup the main theme view 43 // with the current theme collection 44 this.view = new themes.view.Themes({ 45 collection: this.collection, 46 parent: this 47 }); 48 49 // Render search form. 50 this.search(); 51 52 // Render and append 53 this.view.render(); 54 this.$el.empty().append( this.view.el ).addClass('rendered'); 55 this.$el.append( '<br class="clear"/>' ); 56 }, 57 58 // Search input and view 59 // for current theme collection 60 search: function() { 61 var view, 62 self = this; 63 64 // Don't render the search if there is only one theme 65 if ( themes.data.themes.length === 1 ) { 66 return; 67 } 68 69 view = new themes.view.Search({ collection: self.collection }); 70 71 // Render and append after screen title 72 view.render(); 73 $('#wpbody h2:first') 74 .append( $.parseHTML( '<label class="screen-reader-text" for="theme-search-input">' + l10n.search + '</label>' ) ) 75 .append( view.el ); 76 }, 77 78 // Checks when the user gets close to the bottom 79 // of the mage and triggers a theme:scroll event 80 scroller: function() { 81 var self = this, 82 bottom, threshold; 83 84 bottom = this.window.scrollTop() + self.window.height(); 85 threshold = self.$el.offset().top + self.$el.outerHeight( false ) - self.window.height(); 86 threshold = Math.round( threshold * 0.9 ); 87 88 if ( bottom > threshold ) { 89 this.trigger( 'theme:scroll' ); 90 } 91 } 92 }); 93 94 // Set up the Collection for our theme data 95 // @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ... 96 themes.Collection = Backbone.Collection.extend({ 97 98 model: themes.model, 99 100 // Search terms 101 terms: '', 102 103 // Controls searching on the current theme collection 104 // and triggers an update event 105 doSearch: function( value ) { 106 107 // Don't do anything if we've already done this search 108 // Useful because the Search handler fires multiple times per keystroke 109 if ( this.terms === value ) { 110 return; 111 } 112 113 // Updates terms with the value passed 114 this.terms = value; 115 116 // If we have terms, run a search... 117 if ( this.terms.length > 0 ) { 118 this.search( this.terms ); 119 } 120 121 // If search is blank, show all themes 122 // Useful for resetting the views when you clean the input 123 if ( this.terms === '' ) { 124 this.reset( themes.data.themes ); 125 } 126 127 // Trigger an 'update' event 128 this.trigger( 'update' ); 129 }, 130 131 // Performs a search within the collection 132 // @uses RegExp 133 search: function( term ) { 134 var match, results, haystack; 135 136 // Start with a full collection 137 this.reset( themes.data.themes, { silent: true } ); 138 139 // The RegExp object to match 140 // 141 // Consider spaces as word delimiters and match the whole string 142 // so matching terms can be combined 143 term = term.replace( ' ', ')(?=.*' ); 144 match = new RegExp( '^(?=.*' + term + ').+', 'i' ); 145 146 // Find results 147 // _.filter and .test 148 results = this.filter( function( data ) { 149 haystack = _.union( data.get( 'name' ), data.get( 'id' ), data.get( 'description' ), data.get( 'author' ), data.get( 'tags' ) ); 150 151 if ( match.test( data.get( 'author' ) ) && term.length > 2 ) { 152 data.set( 'displayAuthor', true ); 153 } 154 155 return match.test( haystack ); 156 }); 157 158 this.reset( results ); 159 }, 160 161 // Paginates the collection with a helper method 162 // that slices the collection 163 paginate: function( instance ) { 164 var collection = this; 165 instance = instance || 0; 166 167 // Themes per instance are set at 15 168 collection = _( collection.rest( 15 * instance ) ); 169 collection = _( collection.first( 15 ) ); 170 171 return collection; 172 } 173 }); 174 175 // This is the view that controls each theme item 176 // that will be displayed on the screen 177 themes.view.Theme = wp.Backbone.View.extend({ 178 179 // Wrap theme data on a div.theme element 180 className: 'theme', 181 182 // Reflects which theme view we have 183 // 'grid' (default) or 'detail' 184 state: 'grid', 185 186 // The HTML template for each element to be rendered 187 html: themes.template( 'theme' ), 188 189 events: { 190 'click': 'expand', 191 'keydown': 'expand', 192 'touchend': 'expand', 193 'touchmove': 'preventExpand' 194 }, 195 196 touchDrag: false, 197 198 render: function() { 199 var data = this.model.toJSON(); 200 // Render themes using the html template 201 this.$el.html( this.html( data ) ).attr({ 202 tabindex: 0, 203 'aria-describedby' : data.id + '-action ' + data.id + '-name' 204 }); 205 206 // Renders active theme styles 207 this.activeTheme(); 208 209 if ( this.model.get( 'displayAuthor' ) ) { 210 this.$el.addClass( 'display-author' ); 211 } 212 }, 213 214 // Adds a class to the currently active theme 215 // and to the overlay in detailed view mode 216 activeTheme: function() { 217 if ( this.model.get( 'active' ) ) { 218 this.$el.addClass( 'active' ); 219 } 220 }, 221 222 // Single theme overlay screen 223 // It's shown when clicking a theme 224 expand: function( event ) { 225 var self = this; 226 227 event = event || window.event; 228 229 // 'enter' and 'space' keys expand the details view when a theme is :focused 230 if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) { 231 return; 232 } 233 234 // Bail if the user scrolled on a touch device 235 if ( this.touchDrag === true ) { 236 return this.touchDrag = false; 237 } 238 239 // Prevent the modal from showing when the user clicks 240 // one of the direct action buttons 241 if ( $( event.target ).is( '.theme-actions a' ) ) { 242 return; 243 } 244 245 // Set focused theme to current element 246 themes.focusedTheme = this.$el; 247 248 this.trigger( 'theme:expand', self.model.cid ); 249 }, 250 251 preventExpand: function() { 252 this.touchDrag = true; 253 } 254 }); 255 256 // Theme Details view 257 // Set ups a modal overlay with the expanded theme data 258 themes.view.Details = wp.Backbone.View.extend({ 259 260 // Wrap theme data on a div.theme element 261 className: 'theme-overlay', 262 263 events: { 264 'click': 'collapse', 265 'click .delete-theme': 'deleteTheme', 266 'click .left': 'previousTheme', 267 'click .right': 'nextTheme' 268 }, 269 270 // The HTML template for the theme overlay 271 html: themes.template( 'theme-single' ), 272 273 render: function() { 274 var data = this.model.toJSON(); 275 this.$el.html( this.html( data ) ); 276 // Renders active theme styles 277 this.activeTheme(); 278 // Set up navigation events 279 this.navigation(); 280 // Checks screenshot size 281 this.screenshotCheck( this.$el ); 282 // Contain "tabbing" inside the overlay 283 this.containFocus( this.$el ); 284 }, 285 286 // Adds a class to the currently active theme 287 // and to the overlay in detailed view mode 288 activeTheme: function() { 289 // Check the model has the active property 290 this.$el.toggleClass( 'active', this.model.get( 'active' ) ); 291 }, 292 293 // Keeps :focus within the theme details elements 294 containFocus: function( $el ) { 295 var $target; 296 297 // Move focus to the primary action 298 _.delay( function() { 299 $( '.theme-wrap a.button-primary:visible' ).focus(); 300 }, 500 ); 301 302 $el.on( 'keydown.wp-themes', function( event ) { 303 304 // Tab key 305 if ( event.which === 9 ) { 306 $target = $( event.target ); 307 308 // Keep focus within the overlay by making the last link on theme actions 309 // switch focus to button.left on tabbing and vice versa 310 if ( $target.is( 'button.left' ) && event.shiftKey ) { 311 $el.find( '.theme-actions a:last-child' ).focus(); 312 event.preventDefault(); 313 } else if ( $target.is( '.theme-actions a:last-child' ) ) { 314 $el.find( 'button.left' ).focus(); 315 event.preventDefault(); 316 } 317 } 318 }); 319 }, 320 321 // Single theme overlay screen 322 // It's shown when clicking a theme 323 collapse: function( event ) { 324 var self = this, 325 scroll; 326 327 event = event || window.event; 328 329 // Prevent collapsing detailed view when there is only one theme available 330 if ( themes.data.themes.length === 1 ) { 331 return; 332 } 333 334 // Detect if the click is inside the overlay 335 // and don't close it unless the target was 336 // the div.back button 337 if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) { 338 339 // Add a temporary closing class while overlay fades out 340 $( 'body' ).addClass( 'closing-overlay' ); 341 342 // With a quick fade out animation 343 this.$el.fadeOut( 130, function() { 344 // Clicking outside the modal box closes the overlay 345 $( 'body' ).removeClass( 'theme-overlay-open closing-overlay' ); 346 // Handle event cleanup 347 self.closeOverlay(); 348 349 // Get scroll position to avoid jumping to the top 350 scroll = document.body.scrollTop; 351 352 // Clean the url structure 353 themes.router.navigate( themes.router.baseUrl( '' ), { replace: true } ); 354 355 // Restore scroll position 356 document.body.scrollTop = scroll; 357 358 // Return focus to the theme div 359 if ( themes.focusedTheme ) { 360 themes.focusedTheme.focus(); 361 } 362 }); 363 } 364 }, 365 366 // Handles .disabled classes for next/previous buttons 367 navigation: function() { 368 369 // Disable Left/Right when at the start or end of the collection 370 if ( this.model.cid === this.model.collection.at(0).cid ) { 371 this.$el.find( '.left' ).addClass( 'disabled' ); 372 } 373 if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) { 374 this.$el.find( '.right' ).addClass( 'disabled' ); 375 } 376 }, 377 378 // Performs the actions to effectively close 379 // the theme details overlay 380 closeOverlay: function() { 381 this.remove(); 382 this.unbind(); 383 this.trigger( 'theme:collapse' ); 384 }, 385 386 // Confirmation dialoge for deleting a theme 387 deleteTheme: function() { 388 return confirm( themes.data.settings.confirmDelete ); 389 }, 390 391 nextTheme: function() { 392 var self = this; 393 self.trigger( 'theme:next', self.model.cid ); 394 }, 395 396 previousTheme: function() { 397 var self = this; 398 self.trigger( 'theme:previous', self.model.cid ); 399 }, 400 401 // Checks if the theme screenshot is the old 300px width version 402 // and adds a corresponding class if it's true 403 screenshotCheck: function( el ) { 404 var screenshot, image; 405 406 screenshot = el.find( '.screenshot img' ); 407 image = new Image(); 408 image.src = screenshot.attr( 'src' ); 409 410 // Width check 411 if ( image.width && image.width <= 300 ) { 412 el.addClass( 'small-screenshot' ); 413 } 414 } 415 }); 416 417 // Controls the rendering of div.themes, 418 // a wrapper that will hold all the theme elements 419 themes.view.Themes = wp.Backbone.View.extend({ 420 421 className: 'themes', 422 $overlay: $( 'div.theme-overlay' ), 423 424 // Number to keep track of scroll position 425 // while in theme-overlay mode 426 index: 0, 427 428 // The theme count element 429 count: $( '.theme-count' ), 430 431 initialize: function( options ) { 432 var self = this; 433 434 // Set up parent 435 this.parent = options.parent; 436 437 // Set current view to [grid] 438 this.setView( 'grid' ); 439 440 // Move the active theme to the beginning of the collection 441 self.currentTheme(); 442 443 // When the collection is updated by user input... 444 this.listenTo( self.collection, 'update', function() { 445 self.parent.page = 0; 446 self.currentTheme(); 447 self.render( this ); 448 }); 449 450 this.listenTo( this.parent, 'theme:scroll', function() { 451 self.renderThemes( self.parent.page ); 452 }); 453 454 // Bind keyboard events. 455 $('body').on( 'keyup', function( event ) { 456 if ( ! self.overlay ) { 457 return; 458 } 459 460 // Pressing the right arrow key fires a theme:next event 461 if ( event.keyCode === 39 ) { 462 self.overlay.nextTheme(); 463 } 464 465 // Pressing the left arrow key fires a theme:previous event 466 if ( event.keyCode === 37 ) { 467 self.overlay.previousTheme(); 468 } 469 470 // Pressing the escape key fires a theme:collapse event 471 if ( event.keyCode === 27 ) { 472 self.overlay.collapse( event ); 473 } 474 }); 475 }, 476 477 // Manages rendering of theme pages 478 // and keeping theme count in sync 479 render: function() { 480 // Clear the DOM, please 481 this.$el.html( '' ); 482 483 // If the user doesn't have switch capabilities 484 // or there is only one theme in the collection 485 // render the detailed view of the active theme 486 if ( themes.data.themes.length === 1 ) { 487 488 // Constructs the view 489 this.singleTheme = new themes.view.Details({ 490 model: this.collection.models[0] 491 }); 492 493 // Render and apply a 'single-theme' class to our container 494 this.singleTheme.render(); 495 this.$el.addClass( 'single-theme' ); 496 this.$el.append( this.singleTheme.el ); 497 } 498 499 // Generate the themes 500 // Using page instance 501 this.renderThemes( this.parent.page ); 502 503 // Display a live theme count for the collection 504 this.count.text( this.collection.length ); 505 }, 506 507 // Iterates through each instance of the collection 508 // and renders each theme module 509 renderThemes: function( page ) { 510 var self = this; 511 512 self.instance = self.collection.paginate( page ); 513 514 // If we have no more themes bail 515 if ( self.instance.length === 0 ) { 516 return; 517 } 518 519 // Make sure the add-new stays at the end 520 if ( page >= 1 ) { 521 $( '.add-new-theme' ).remove(); 522 } 523 524 // Loop through the themes and setup each theme view 525 self.instance.each( function( theme ) { 526 self.theme = new themes.view.Theme({ 527 model: theme 528 }); 529 530 // Render the views... 531 self.theme.render(); 532 // and append them to div.themes 533 self.$el.append( self.theme.el ); 534 535 // Binds to theme:expand to show the modal box 536 // with the theme details 537 self.listenTo( self.theme, 'theme:expand', self.expand, self ); 538 }); 539 540 // 'Add new theme' element shown at the end of the grid 541 if ( themes.data.settings.canInstall ) { 542 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>' ); 543 } 544 545 this.parent.page++; 546 }, 547 548 // Grabs current theme and puts it at the beginning of the collection 549 currentTheme: function() { 550 var self = this, 551 current; 552 553 current = self.collection.findWhere({ active: true }); 554 555 // Move the active theme to the beginning of the collection 556 if ( current ) { 557 self.collection.remove( current ); 558 self.collection.add( current, { at:0 } ); 559 } 560 }, 561 562 // Sets current view 563 setView: function( view ) { 564 return view; 565 }, 566 567 // Renders the overlay with the ThemeDetails view 568 // Uses the current model data 569 expand: function( id ) { 570 var self = this; 571 572 // Set the current theme model 573 this.model = self.collection.get( id ); 574 575 // Trigger a route update for the current model 576 themes.router.navigate( themes.router.baseUrl( '?theme=' + this.model.id ), { replace: true } ); 577 578 // Sets this.view to 'detail' 579 this.setView( 'detail' ); 580 $( 'body' ).addClass( 'theme-overlay-open' ); 581 582 // Set up the theme details view 583 this.overlay = new themes.view.Details({ 584 model: self.model 585 }); 586 587 this.overlay.render(); 588 this.$overlay.html( this.overlay.el ); 589 590 // Bind to theme:next and theme:previous 591 // triggered by the arrow keys 592 // 593 // Keep track of the current model so we 594 // can infer an index position 595 this.listenTo( this.overlay, 'theme:next', function() { 596 // Renders the next theme on the overlay 597 self.next( [ self.model.cid ] ); 598 599 }) 600 .listenTo( this.overlay, 'theme:previous', function() { 601 // Renders the previous theme on the overlay 602 self.previous( [ self.model.cid ] ); 603 }); 604 }, 605 606 // This method renders the next theme on the overlay modal 607 // based on the current position in the collection 608 // @params [model cid] 609 next: function( args ) { 610 var self = this, 611 model, nextModel; 612 613 // Get the current theme 614 model = self.collection.get( args[0] ); 615 // Find the next model within the collection 616 nextModel = self.collection.at( self.collection.indexOf( model ) + 1 ); 617 618 // Sanity check which also serves as a boundary test 619 if ( nextModel !== undefined ) { 620 621 // We have a new theme... 622 // Close the overlay 623 this.overlay.closeOverlay(); 624 625 // Trigger a route update for the current model 626 self.theme.trigger( 'theme:expand', nextModel.cid ); 627 628 } 629 }, 630 631 // This method renders the previous theme on the overlay modal 632 // based on the current position in the collection 633 // @params [model cid] 634 previous: function( args ) { 635 var self = this, 636 model, previousModel; 637 638 // Get the current theme 639 model = self.collection.get( args[0] ); 640 // Find the previous model within the collection 641 previousModel = self.collection.at( self.collection.indexOf( model ) - 1 ); 642 643 if ( previousModel !== undefined ) { 644 645 // We have a new theme... 646 // Close the overlay 647 this.overlay.closeOverlay(); 648 649 // Trigger a route update for the current model 650 self.theme.trigger( 'theme:expand', previousModel.cid ); 651 652 } 653 } 654 }); 655 656 // Search input view controller. 657 themes.view.Search = wp.Backbone.View.extend({ 658 659 tagName: 'input', 660 className: 'theme-search', 661 id: 'theme-search-input', 662 663 attributes: { 664 placeholder: l10n.searchPlaceholder, 665 type: 'search' 666 }, 667 668 events: { 669 'input': 'search', 670 'keyup': 'search', 671 'change': 'search', 672 'search': 'search' 673 }, 674 675 // Runs a search on the theme collection. 676 search: function( event ) { 677 // Clear on escape. 678 if ( event.type === 'keyup' && event.which === 27 ) { 679 event.target.value = ''; 680 } 681 682 this.collection.doSearch( event.target.value ); 683 684 // Update the URL hash 685 if ( event.target.value ) { 686 themes.router.navigate( themes.router.baseUrl( '?search=' + event.target.value ), { replace: true } ); 687 } else { 688 themes.router.navigate( themes.router.baseUrl( '' ), { replace: true } ); 689 } 690 } 691 }); 692 693 // Sets up the routes events for relevant url queries 694 // Listens to [theme] and [search] params 695 themes.routes = Backbone.Router.extend({ 696 697 initialize: function() { 698 this.routes = _.object([ 699 ]); 700 }, 701 702 baseUrl: function( url ) { 703 return themes.data.settings.root + url; 704 } 705 }); 706 707 // Execute and setup the application 708 themes.Run = { 709 init: function() { 710 // Initializes the blog's theme library view 711 // Create a new collection with data 712 this.themes = new themes.Collection( themes.data.themes ); 713 714 // Set up the view 715 this.view = new themes.view.Appearance({ 716 collection: this.themes 717 }); 718 719 this.render(); 720 }, 721 722 render: function() { 723 // Render results 724 this.view.render(); 725 this.routes(); 726 727 // Set the initial theme 728 if ( 'undefined' !== typeof themes.data.settings.theme && '' !== themes.data.settings.theme ){ 729 this.view.view.theme.trigger( 'theme:expand', this.view.collection.findWhere( { id: themes.data.settings.theme } ) ); 730 } 731 732 // Set the initial search 733 if ( 'undefined' !== typeof themes.data.settings.search && '' !== themes.data.settings.search ){ 734 $( '.theme-search' ).val( themes.data.settings.search ); 735 this.themes.doSearch( themes.data.settings.search ); 736 } 737 738 // Start the router if browser supports History API 739 if ( window.history && window.history.pushState ) { 740 // Calls the routes functionality 741 Backbone.history.start({ pushState: true, silent: true }); 742 } 743 }, 744 745 routes: function() { 746 // Bind to our global thx object 747 // so that the object is available to sub-views 748 themes.router = new themes.routes(); 749 } 750 }; 751 752 // Ready... 753 jQuery( document ).ready( 754 755 // Bring on the themes 756 _.bind( themes.Run.init, themes.Run ) 757 758 ); 759 760 })( jQuery ); 761 762 // Align theme browser thickbox 763 var tb_position; 764 jQuery(document).ready( function($) { 765 tb_position = function() { 766 var tbWindow = $('#TB_window'), 767 width = $(window).width(), 768 H = $(window).height(), 769 W = ( 1040 < width ) ? 1040 : width, 770 adminbar_height = 0; 771 772 if ( $('body.admin-bar').length ) { 773 adminbar_height = parseInt( jQuery('#wpadminbar').css('height'), 10 ); 774 } 775 776 if ( tbWindow.size() ) { 777 tbWindow.width( W - 50 ).height( H - 45 - adminbar_height ); 778 $('#TB_iframeContent').width( W - 50 ).height( H - 75 - adminbar_height ); 779 tbWindow.css({'margin-left': '-' + parseInt( ( ( W - 50 ) / 2 ), 10 ) + 'px'}); 780 if ( typeof document.body.style.maxWidth !== 'undefined' ) { 781 tbWindow.css({'top': 20 + adminbar_height + 'px', 'margin-top': '0'}); 782 } 783 } 784 }; 785 786 $(window).resize(function(){ tb_position(); }); 787 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Tue Mar 25 01:41:18 2014 | WordPress honlapkészítés: online1.hu |