mce-view.js 25 KB

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