[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/js/ -> mce-view.js (source)

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


Generated : Thu Sep 24 08:20:34 2026 Cross-referenced by PHPXref