/wp-includes/js/mce-view.js

https://bitbucket.org/skyarch-iijima/wordpress · JavaScript · 974 lines · 586 code · 144 blank · 244 comment · 91 complexity · f2e51a6cc3a8ea3f6d149602645bcb91 MD5 · raw file

  1. /* global tinymce */
  2. /*
  3. * The TinyMCE view API.
  4. *
  5. * Note: this API is "experimental" meaning that it will probably change
  6. * in the next few releases based on feedback from 3.9.0.
  7. * If you decide to use it, please follow the development closely.
  8. *
  9. * Diagram
  10. *
  11. * |- registered view constructor (type)
  12. * | |- view instance (unique text)
  13. * | | |- editor 1
  14. * | | | |- view node
  15. * | | | |- view node
  16. * | | | |- ...
  17. * | | |- editor 2
  18. * | | | |- ...
  19. * | |- view instance
  20. * | | |- ...
  21. * |- registered view
  22. * | |- ...
  23. */
  24. ( function( window, wp, shortcode, $ ) {
  25. 'use strict';
  26. var views = {},
  27. instances = {};
  28. wp.mce = wp.mce || {};
  29. /**
  30. * wp.mce.views
  31. *
  32. * A set of utilities that simplifies adding custom UI within a TinyMCE editor.
  33. * At its core, it serves as a series of converters, transforming text to a
  34. * custom UI, and back again.
  35. */
  36. wp.mce.views = {
  37. /**
  38. * Registers a new view type.
  39. *
  40. * @param {String} type The view type.
  41. * @param {Object} extend An object to extend wp.mce.View.prototype with.
  42. */
  43. register: function( type, extend ) {
  44. views[ type ] = wp.mce.View.extend( _.extend( extend, { type: type } ) );
  45. },
  46. /**
  47. * Unregisters a view type.
  48. *
  49. * @param {String} type The view type.
  50. */
  51. unregister: function( type ) {
  52. delete views[ type ];
  53. },
  54. /**
  55. * Returns the settings of a view type.
  56. *
  57. * @param {String} type The view type.
  58. *
  59. * @return {Function} The view constructor.
  60. */
  61. get: function( type ) {
  62. return views[ type ];
  63. },
  64. /**
  65. * Unbinds all view nodes.
  66. * Runs before removing all view nodes from the DOM.
  67. */
  68. unbind: function() {
  69. _.each( instances, function( instance ) {
  70. instance.unbind();
  71. } );
  72. },
  73. /**
  74. * Scans a given string for each view's pattern,
  75. * replacing any matches with markers,
  76. * and creates a new instance for every match.
  77. *
  78. * @param {String} content The string to scan.
  79. * @param {tinymce.Editor} editor The editor.
  80. *
  81. * @return {String} The string with markers.
  82. */
  83. setMarkers: function( content, editor ) {
  84. var pieces = [ { content: content } ],
  85. self = this,
  86. instance, current;
  87. _.each( views, function( view, type ) {
  88. current = pieces.slice();
  89. pieces = [];
  90. _.each( current, function( piece ) {
  91. var remaining = piece.content,
  92. result, text;
  93. // Ignore processed pieces, but retain their location.
  94. if ( piece.processed ) {
  95. pieces.push( piece );
  96. return;
  97. }
  98. // Iterate through the string progressively matching views
  99. // and slicing the string as we go.
  100. while ( remaining && ( result = view.prototype.match( remaining ) ) ) {
  101. // Any text before the match becomes an unprocessed piece.
  102. if ( result.index ) {
  103. pieces.push( { content: remaining.substring( 0, result.index ) } );
  104. }
  105. result.options.editor = editor;
  106. instance = self.createInstance( type, result.content, result.options );
  107. text = instance.loader ? '.' : instance.text;
  108. // Add the processed piece for the match.
  109. pieces.push( {
  110. content: instance.ignore ? text : '<p data-wpview-marker="' + instance.encodedText + '">' + text + '</p>',
  111. processed: true
  112. } );
  113. // Update the remaining content.
  114. remaining = remaining.slice( result.index + result.content.length );
  115. }
  116. // There are no additional matches.
  117. // If any content remains, add it as an unprocessed piece.
  118. if ( remaining ) {
  119. pieces.push( { content: remaining } );
  120. }
  121. } );
  122. } );
  123. content = _.pluck( pieces, 'content' ).join( '' );
  124. return content.replace( /<p>\s*<p data-wpview-marker=/g, '<p data-wpview-marker=' ).replace( /<\/p>\s*<\/p>/g, '</p>' );
  125. },
  126. /**
  127. * Create a view instance.
  128. *
  129. * @param {String} type The view type.
  130. * @param {String} text The textual representation of the view.
  131. * @param {Object} options Options.
  132. * @param {Boolean} force Recreate the instance. Optional.
  133. *
  134. * @return {wp.mce.View} The view instance.
  135. */
  136. createInstance: function( type, text, options, force ) {
  137. var View = this.get( type ),
  138. encodedText,
  139. instance;
  140. if ( text.indexOf( '[' ) !== -1 && text.indexOf( ']' ) !== -1 ) {
  141. // Looks like a shortcode? Remove any line breaks from inside of shortcodes
  142. // or autop will replace them with <p> and <br> later and the string won't match.
  143. text = text.replace( /\[[^\]]+\]/g, function( match ) {
  144. return match.replace( /[\r\n]/g, '' );
  145. });
  146. }
  147. if ( ! force ) {
  148. instance = this.getInstance( text );
  149. if ( instance ) {
  150. return instance;
  151. }
  152. }
  153. encodedText = encodeURIComponent( text );
  154. options = _.extend( options || {}, {
  155. text: text,
  156. encodedText: encodedText
  157. } );
  158. return instances[ encodedText ] = new View( options );
  159. },
  160. /**
  161. * Get a view instance.
  162. *
  163. * @param {(String|HTMLElement)} object The textual representation of the view or the view node.
  164. *
  165. * @return {wp.mce.View} The view instance or undefined.
  166. */
  167. getInstance: function( object ) {
  168. if ( typeof object === 'string' ) {
  169. return instances[ encodeURIComponent( object ) ];
  170. }
  171. return instances[ $( object ).attr( 'data-wpview-text' ) ];
  172. },
  173. /**
  174. * Given a view node, get the view's text.
  175. *
  176. * @param {HTMLElement} node The view node.
  177. *
  178. * @return {String} The textual representation of the view.
  179. */
  180. getText: function( node ) {
  181. return decodeURIComponent( $( node ).attr( 'data-wpview-text' ) || '' );
  182. },
  183. /**
  184. * Renders all view nodes that are not yet rendered.
  185. *
  186. * @param {Boolean} force Rerender all view nodes.
  187. */
  188. render: function( force ) {
  189. _.each( instances, function( instance ) {
  190. instance.render( null, force );
  191. } );
  192. },
  193. /**
  194. * Update the text of a given view node.
  195. *
  196. * @param {String} text The new text.
  197. * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
  198. * @param {HTMLElement} node The view node to update.
  199. * @param {Boolean} force Recreate the instance. Optional.
  200. */
  201. update: function( text, editor, node, force ) {
  202. var instance = this.getInstance( node );
  203. if ( instance ) {
  204. instance.update( text, editor, node, force );
  205. }
  206. },
  207. /**
  208. * Renders any editing interface based on the view type.
  209. *
  210. * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
  211. * @param {HTMLElement} node The view node to edit.
  212. */
  213. edit: function( editor, node ) {
  214. var instance = this.getInstance( node );
  215. if ( instance && instance.edit ) {
  216. instance.edit( instance.text, function( text, force ) {
  217. instance.update( text, editor, node, force );
  218. } );
  219. }
  220. },
  221. /**
  222. * Remove a given view node from the DOM.
  223. *
  224. * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
  225. * @param {HTMLElement} node The view node to remove.
  226. */
  227. remove: function( editor, node ) {
  228. var instance = this.getInstance( node );
  229. if ( instance ) {
  230. instance.remove( editor, node );
  231. }
  232. }
  233. };
  234. /**
  235. * A Backbone-like View constructor intended for use when rendering a TinyMCE View.
  236. * The main difference is that the TinyMCE View is not tied to a particular DOM node.
  237. *
  238. * @param {Object} options Options.
  239. */
  240. wp.mce.View = function( options ) {
  241. _.extend( this, options );
  242. this.initialize();
  243. };
  244. wp.mce.View.extend = Backbone.View.extend;
  245. _.extend( wp.mce.View.prototype, /** @lends wp.mce.View.prototype */{
  246. /**
  247. * The content.
  248. *
  249. * @type {*}
  250. */
  251. content: null,
  252. /**
  253. * Whether or not to display a loader.
  254. *
  255. * @type {Boolean}
  256. */
  257. loader: true,
  258. /**
  259. * Runs after the view instance is created.
  260. */
  261. initialize: function() {},
  262. /**
  263. * Returns the content to render in the view node.
  264. *
  265. * @return {*}
  266. */
  267. getContent: function() {
  268. return this.content;
  269. },
  270. /**
  271. * Renders all view nodes tied to this view instance that are not yet rendered.
  272. *
  273. * @param {String} content The content to render. Optional.
  274. * @param {Boolean} force Rerender all view nodes tied to this view instance. Optional.
  275. */
  276. render: function( content, force ) {
  277. if ( content != null ) {
  278. this.content = content;
  279. }
  280. content = this.getContent();
  281. // If there's nothing to render an no loader needs to be shown, stop.
  282. if ( ! this.loader && ! content ) {
  283. return;
  284. }
  285. // We're about to rerender all views of this instance, so unbind rendered views.
  286. force && this.unbind();
  287. // Replace any left over markers.
  288. this.replaceMarkers();
  289. if ( content ) {
  290. this.setContent( content, function( editor, node ) {
  291. $( node ).data( 'rendered', true );
  292. this.bindNode.call( this, editor, node );
  293. }, force ? null : false );
  294. } else {
  295. this.setLoader();
  296. }
  297. },
  298. /**
  299. * Binds a given node after its content is added to the DOM.
  300. */
  301. bindNode: function() {},
  302. /**
  303. * Unbinds a given node before its content is removed from the DOM.
  304. */
  305. unbindNode: function() {},
  306. /**
  307. * Unbinds all view nodes tied to this view instance.
  308. * Runs before their content is removed from the DOM.
  309. */
  310. unbind: function() {
  311. this.getNodes( function( editor, node ) {
  312. this.unbindNode.call( this, editor, node );
  313. }, true );
  314. },
  315. /**
  316. * Gets all the TinyMCE editor instances that support views.
  317. *
  318. * @param {Function} callback A callback.
  319. */
  320. getEditors: function( callback ) {
  321. _.each( tinymce.editors, function( editor ) {
  322. if ( editor.plugins.wpview ) {
  323. callback.call( this, editor );
  324. }
  325. }, this );
  326. },
  327. /**
  328. * Gets all view nodes tied to this view instance.
  329. *
  330. * @param {Function} callback A callback.
  331. * @param {Boolean} rendered Get (un)rendered view nodes. Optional.
  332. */
  333. getNodes: function( callback, rendered ) {
  334. this.getEditors( function( editor ) {
  335. var self = this;
  336. $( editor.getBody() )
  337. .find( '[data-wpview-text="' + self.encodedText + '"]' )
  338. .filter( function() {
  339. var data;
  340. if ( rendered == null ) {
  341. return true;
  342. }
  343. data = $( this ).data( 'rendered' ) === true;
  344. return rendered ? data : ! data;
  345. } )
  346. .each( function() {
  347. callback.call( self, editor, this, this /* back compat */ );
  348. } );
  349. } );
  350. },
  351. /**
  352. * Gets all marker nodes tied to this view instance.
  353. *
  354. * @param {Function} callback A callback.
  355. */
  356. getMarkers: function( callback ) {
  357. this.getEditors( function( editor ) {
  358. var self = this;
  359. $( editor.getBody() )
  360. .find( '[data-wpview-marker="' + this.encodedText + '"]' )
  361. .each( function() {
  362. callback.call( self, editor, this );
  363. } );
  364. } );
  365. },
  366. /**
  367. * Replaces all marker nodes tied to this view instance.
  368. */
  369. replaceMarkers: function() {
  370. this.getMarkers( function( editor, node ) {
  371. var selected = node === editor.selection.getNode();
  372. var $viewNode;
  373. if ( ! this.loader && $( node ).text() !== tinymce.DOM.decode( this.text ) ) {
  374. editor.dom.setAttrib( node, 'data-wpview-marker', null );
  375. return;
  376. }
  377. $viewNode = editor.$(
  378. '<div class="wpview wpview-wrap" data-wpview-text="' + this.encodedText + '" data-wpview-type="' + this.type + '" contenteditable="false"></div>'
  379. );
  380. editor.$( node ).replaceWith( $viewNode );
  381. if ( selected ) {
  382. setTimeout( function() {
  383. editor.selection.select( $viewNode[0] );
  384. editor.selection.collapse();
  385. } );
  386. }
  387. } );
  388. },
  389. /**
  390. * Removes all marker nodes tied to this view instance.
  391. */
  392. removeMarkers: function() {
  393. this.getMarkers( function( editor, node ) {
  394. editor.dom.setAttrib( node, 'data-wpview-marker', null );
  395. } );
  396. },
  397. /**
  398. * Sets the content for all view nodes tied to this view instance.
  399. *
  400. * @param {*} content The content to set.
  401. * @param {Function} callback A callback. Optional.
  402. * @param {Boolean} rendered Only set for (un)rendered nodes. Optional.
  403. */
  404. setContent: function( content, callback, rendered ) {
  405. if ( _.isObject( content ) && ( content.sandbox || content.head || content.body.indexOf( '<script' ) !== -1 ) ) {
  406. this.setIframes( content.head || '', content.body, callback, rendered );
  407. } else if ( _.isString( content ) && content.indexOf( '<script' ) !== -1 ) {
  408. this.setIframes( '', content, callback, rendered );
  409. } else {
  410. this.getNodes( function( editor, node ) {
  411. content = content.body || content;
  412. if ( content.indexOf( '<iframe' ) !== -1 ) {
  413. content += '<span class="mce-shim"></span>';
  414. }
  415. editor.undoManager.transact( function() {
  416. node.innerHTML = '';
  417. node.appendChild( _.isString( content ) ? editor.dom.createFragment( content ) : content );
  418. editor.dom.add( node, 'span', { 'class': 'wpview-end' } );
  419. } );
  420. callback && callback.call( this, editor, node );
  421. }, rendered );
  422. }
  423. },
  424. /**
  425. * Sets the content in an iframe for all view nodes tied to this view instance.
  426. *
  427. * @param {String} head HTML string to be added to the head of the document.
  428. * @param {String} body HTML string to be added to the body of the document.
  429. * @param {Function} callback A callback. Optional.
  430. * @param {Boolean} rendered Only set for (un)rendered nodes. Optional.
  431. */
  432. setIframes: function( head, body, callback, rendered ) {
  433. var self = this;
  434. if ( body.indexOf( '[' ) !== -1 && body.indexOf( ']' ) !== -1 ) {
  435. var shortcodesRegExp = new RegExp( '\\[\\/?(?:' + window.mceViewL10n.shortcodes.join( '|' ) + ')[^\\]]*?\\]', 'g' );
  436. // Escape tags inside shortcode previews.
  437. body = body.replace( shortcodesRegExp, function( match ) {
  438. return match.replace( /</g, '&lt;' ).replace( />/g, '&gt;' );
  439. } );
  440. }
  441. this.getNodes( function( editor, node ) {
  442. var dom = editor.dom,
  443. styles = '',
  444. bodyClasses = editor.getBody().className || '',
  445. editorHead = editor.getDoc().getElementsByTagName( 'head' )[0],
  446. iframe, iframeWin, iframeDoc, MutationObserver, observer, i, block;
  447. tinymce.each( dom.$( 'link[rel="stylesheet"]', editorHead ), function( link ) {
  448. if ( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1 &&
  449. link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 ) {
  450. styles += dom.getOuterHTML( link );
  451. }
  452. } );
  453. if ( self.iframeHeight ) {
  454. dom.add( node, 'span', {
  455. 'data-mce-bogus': 1,
  456. style: {
  457. display: 'block',
  458. width: '100%',
  459. height: self.iframeHeight
  460. }
  461. }, '\u200B' );
  462. }
  463. editor.undoManager.transact( function() {
  464. node.innerHTML = '';
  465. iframe = dom.add( node, 'iframe', {
  466. /* jshint scripturl: true */
  467. src: tinymce.Env.ie ? 'javascript:""' : '',
  468. frameBorder: '0',
  469. allowTransparency: 'true',
  470. scrolling: 'no',
  471. 'class': 'wpview-sandbox',
  472. style: {
  473. width: '100%',
  474. display: 'block'
  475. },
  476. height: self.iframeHeight
  477. } );
  478. dom.add( node, 'span', { 'class': 'mce-shim' } );
  479. dom.add( node, 'span', { 'class': 'wpview-end' } );
  480. } );
  481. // Bail if the iframe node is not attached to the DOM.
  482. // Happens when the view is dragged in the editor.
  483. // There is a browser restriction when iframes are moved in the DOM. They get emptied.
  484. // The iframe will be rerendered after dropping the view node at the new location.
  485. if ( ! iframe.contentWindow ) {
  486. return;
  487. }
  488. iframeWin = iframe.contentWindow;
  489. iframeDoc = iframeWin.document;
  490. iframeDoc.open();
  491. iframeDoc.write(
  492. '<!DOCTYPE html>' +
  493. '<html>' +
  494. '<head>' +
  495. '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
  496. head +
  497. styles +
  498. '<style>' +
  499. 'html {' +
  500. 'background: transparent;' +
  501. 'padding: 0;' +
  502. 'margin: 0;' +
  503. '}' +
  504. 'body#wpview-iframe-sandbox {' +
  505. 'background: transparent;' +
  506. 'padding: 1px 0 !important;' +
  507. 'margin: -1px 0 0 !important;' +
  508. '}' +
  509. 'body#wpview-iframe-sandbox:before,' +
  510. 'body#wpview-iframe-sandbox:after {' +
  511. 'display: none;' +
  512. 'content: "";' +
  513. '}' +
  514. 'iframe {' +
  515. 'max-width: 100%;' +
  516. '}' +
  517. '</style>' +
  518. '</head>' +
  519. '<body id="wpview-iframe-sandbox" class="' + bodyClasses + '">' +
  520. body +
  521. '</body>' +
  522. '</html>'
  523. );
  524. iframeDoc.close();
  525. function resize() {
  526. var $iframe;
  527. if ( block ) {
  528. return;
  529. }
  530. // Make sure the iframe still exists.
  531. if ( iframe.contentWindow ) {
  532. $iframe = $( iframe );
  533. self.iframeHeight = $( iframeDoc.body ).height();
  534. if ( $iframe.height() !== self.iframeHeight ) {
  535. $iframe.height( self.iframeHeight );
  536. editor.nodeChanged();
  537. }
  538. }
  539. }
  540. if ( self.iframeHeight ) {
  541. block = true;
  542. setTimeout( function() {
  543. block = false;
  544. resize();
  545. }, 3000 );
  546. }
  547. function reload() {
  548. if ( ! editor.isHidden() ) {
  549. $( node ).data( 'rendered', null );
  550. setTimeout( function() {
  551. wp.mce.views.render();
  552. } );
  553. }
  554. }
  555. function addObserver() {
  556. observer = new MutationObserver( _.debounce( resize, 100 ) );
  557. observer.observe( iframeDoc.body, {
  558. attributes: true,
  559. childList: true,
  560. subtree: true
  561. } );
  562. }
  563. $( iframeWin ).on( 'load', resize ).on( 'unload', reload );
  564. MutationObserver = iframeWin.MutationObserver || iframeWin.WebKitMutationObserver || iframeWin.MozMutationObserver;
  565. if ( MutationObserver ) {
  566. if ( ! iframeDoc.body ) {
  567. iframeDoc.addEventListener( 'DOMContentLoaded', addObserver, false );
  568. } else {
  569. addObserver();
  570. }
  571. } else {
  572. for ( i = 1; i < 6; i++ ) {
  573. setTimeout( resize, i * 700 );
  574. }
  575. }
  576. callback && callback.call( self, editor, node );
  577. }, rendered );
  578. },
  579. /**
  580. * Sets a loader for all view nodes tied to this view instance.
  581. */
  582. setLoader: function( dashicon ) {
  583. this.setContent(
  584. '<div class="loading-placeholder">' +
  585. '<div class="dashicons dashicons-' + ( dashicon || 'admin-media' ) + '"></div>' +
  586. '<div class="wpview-loading"><ins></ins></div>' +
  587. '</div>'
  588. );
  589. },
  590. /**
  591. * Sets an error for all view nodes tied to this view instance.
  592. *
  593. * @param {String} message The error message to set.
  594. * @param {String} dashicon A dashicon ID. Optional. {@link https://developer.wordpress.org/resource/dashicons/}
  595. */
  596. setError: function( message, dashicon ) {
  597. this.setContent(
  598. '<div class="wpview-error">' +
  599. '<div class="dashicons dashicons-' + ( dashicon || 'no' ) + '"></div>' +
  600. '<p>' + message + '</p>' +
  601. '</div>'
  602. );
  603. },
  604. /**
  605. * Tries to find a text match in a given string.
  606. *
  607. * @param {String} content The string to scan.
  608. *
  609. * @return {Object}
  610. */
  611. match: function( content ) {
  612. var match = shortcode.next( this.type, content );
  613. if ( match ) {
  614. return {
  615. index: match.index,
  616. content: match.content,
  617. options: {
  618. shortcode: match.shortcode
  619. }
  620. };
  621. }
  622. },
  623. /**
  624. * Update the text of a given view node.
  625. *
  626. * @param {String} text The new text.
  627. * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
  628. * @param {HTMLElement} node The view node to update.
  629. * @param {Boolean} force Recreate the instance. Optional.
  630. */
  631. update: function( text, editor, node, force ) {
  632. _.find( views, function( view, type ) {
  633. var match = view.prototype.match( text );
  634. if ( match ) {
  635. $( node ).data( 'rendered', false );
  636. editor.dom.setAttrib( node, 'data-wpview-text', encodeURIComponent( text ) );
  637. wp.mce.views.createInstance( type, text, match.options, force ).render();
  638. editor.selection.select( node );
  639. editor.nodeChanged();
  640. editor.focus();
  641. return true;
  642. }
  643. } );
  644. },
  645. /**
  646. * Remove a given view node from the DOM.
  647. *
  648. * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
  649. * @param {HTMLElement} node The view node to remove.
  650. */
  651. remove: function( editor, node ) {
  652. this.unbindNode.call( this, editor, node );
  653. editor.dom.remove( node );
  654. editor.focus();
  655. }
  656. } );
  657. } )( window, window.wp, window.wp.shortcode, window.jQuery );
  658. /*
  659. * The WordPress core TinyMCE views.
  660. * Views for the gallery, audio, video, playlist and embed shortcodes,
  661. * and a view for embeddable URLs.
  662. */
  663. ( function( window, views, media, $ ) {
  664. var base, gallery, av, embed,
  665. schema, parser, serializer;
  666. function verifyHTML( string ) {
  667. var settings = {};
  668. if ( ! window.tinymce ) {
  669. return string.replace( /<[^>]+>/g, '' );
  670. }
  671. if ( ! string || ( string.indexOf( '<' ) === -1 && string.indexOf( '>' ) === -1 ) ) {
  672. return string;
  673. }
  674. schema = schema || new window.tinymce.html.Schema( settings );
  675. parser = parser || new window.tinymce.html.DomParser( settings, schema );
  676. serializer = serializer || new window.tinymce.html.Serializer( settings, schema );
  677. return serializer.serialize( parser.parse( string, { forced_root_block: false } ) );
  678. }
  679. base = {
  680. state: [],
  681. edit: function( text, update ) {
  682. var type = this.type,
  683. frame = media[ type ].edit( text );
  684. this.pausePlayers && this.pausePlayers();
  685. _.each( this.state, function( state ) {
  686. frame.state( state ).on( 'update', function( selection ) {
  687. update( media[ type ].shortcode( selection ).string(), type === 'gallery' );
  688. } );
  689. } );
  690. frame.on( 'close', function() {
  691. frame.detach();
  692. } );
  693. frame.open();
  694. }
  695. };
  696. gallery = _.extend( {}, base, {
  697. state: [ 'gallery-edit' ],
  698. template: media.template( 'editor-gallery' ),
  699. initialize: function() {
  700. var attachments = media.gallery.attachments( this.shortcode, media.view.settings.post.id ),
  701. attrs = this.shortcode.attrs.named,
  702. self = this;
  703. attachments.more()
  704. .done( function() {
  705. attachments = attachments.toJSON();
  706. _.each( attachments, function( attachment ) {
  707. if ( attachment.sizes ) {
  708. if ( attrs.size && attachment.sizes[ attrs.size ] ) {
  709. attachment.thumbnail = attachment.sizes[ attrs.size ];
  710. } else if ( attachment.sizes.thumbnail ) {
  711. attachment.thumbnail = attachment.sizes.thumbnail;
  712. } else if ( attachment.sizes.full ) {
  713. attachment.thumbnail = attachment.sizes.full;
  714. }
  715. }
  716. } );
  717. self.render( self.template( {
  718. verifyHTML: verifyHTML,
  719. attachments: attachments,
  720. columns: attrs.columns ? parseInt( attrs.columns, 10 ) : media.galleryDefaults.columns
  721. } ) );
  722. } )
  723. .fail( function( jqXHR, textStatus ) {
  724. self.setError( textStatus );
  725. } );
  726. }
  727. } );
  728. av = _.extend( {}, base, {
  729. action: 'parse-media-shortcode',
  730. initialize: function() {
  731. var self = this, maxwidth = null;
  732. if ( this.url ) {
  733. this.loader = false;
  734. this.shortcode = media.embed.shortcode( {
  735. url: this.text
  736. } );
  737. }
  738. // Obtain the target width for the embed.
  739. if ( self.editor ) {
  740. maxwidth = self.editor.getBody().clientWidth;
  741. }
  742. wp.ajax.post( this.action, {
  743. post_ID: media.view.settings.post.id,
  744. type: this.shortcode.tag,
  745. shortcode: this.shortcode.string(),
  746. maxwidth: maxwidth
  747. } )
  748. .done( function( response ) {
  749. self.render( response );
  750. } )
  751. .fail( function( response ) {
  752. if ( self.url ) {
  753. self.ignore = true;
  754. self.removeMarkers();
  755. } else {
  756. self.setError( response.message || response.statusText, 'admin-media' );
  757. }
  758. } );
  759. this.getEditors( function( editor ) {
  760. editor.on( 'wpview-selected', function() {
  761. self.pausePlayers();
  762. } );
  763. } );
  764. },
  765. pausePlayers: function() {
  766. this.getNodes( function( editor, node, content ) {
  767. var win = $( 'iframe.wpview-sandbox', content ).get( 0 );
  768. if ( win && ( win = win.contentWindow ) && win.mejs ) {
  769. _.each( win.mejs.players, function( player ) {
  770. try {
  771. player.pause();
  772. } catch ( e ) {}
  773. } );
  774. }
  775. } );
  776. }
  777. } );
  778. embed = _.extend( {}, av, {
  779. action: 'parse-embed',
  780. edit: function( text, update ) {
  781. var frame = media.embed.edit( text, this.url ),
  782. self = this;
  783. this.pausePlayers();
  784. frame.state( 'embed' ).props.on( 'change:url', function( model, url ) {
  785. if ( url && model.get( 'url' ) ) {
  786. frame.state( 'embed' ).metadata = model.toJSON();
  787. }
  788. } );
  789. frame.state( 'embed' ).on( 'select', function() {
  790. var data = frame.state( 'embed' ).metadata;
  791. if ( self.url ) {
  792. update( data.url );
  793. } else {
  794. update( media.embed.shortcode( data ).string() );
  795. }
  796. } );
  797. frame.on( 'close', function() {
  798. frame.detach();
  799. } );
  800. frame.open();
  801. }
  802. } );
  803. views.register( 'gallery', _.extend( {}, gallery ) );
  804. views.register( 'audio', _.extend( {}, av, {
  805. state: [ 'audio-details' ]
  806. } ) );
  807. views.register( 'video', _.extend( {}, av, {
  808. state: [ 'video-details' ]
  809. } ) );
  810. views.register( 'playlist', _.extend( {}, av, {
  811. state: [ 'playlist-edit', 'video-playlist-edit' ]
  812. } ) );
  813. views.register( 'embed', _.extend( {}, embed ) );
  814. views.register( 'embedURL', _.extend( {}, embed, {
  815. match: function( content ) {
  816. var re = /(^|<p>)(https?:\/\/[^\s"]+?)(<\/p>\s*|$)/gi,
  817. match = re.exec( content );
  818. if ( match ) {
  819. return {
  820. index: match.index + match[1].length,
  821. content: match[2],
  822. options: {
  823. url: true
  824. }
  825. };
  826. }
  827. }
  828. } ) );
  829. } )( window, window.wp.mce.views, window.wp.media, window.jQuery );