PageRenderTime 38ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-admin/js/theme.js

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