home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress2 / wp-includes / js / mce-view.js < prev    next >
Encoding:
JavaScript  |  2017-12-12  |  25.1 KB  |  975 lines

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