[ Index ]

PHP Cross Reference of WordPress

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


Generated: Thu Dec 5 01:00:03 2024 Cross-referenced by PHPXref 0.7.1