Changeset 27408
- Timestamp:
- 03/05/2014 07:00:18 AM (11 years ago)
- Location:
- trunk/src/wp-includes
- Files:
-
- 7 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/class-wp-editor.php
r27391 r27408 243 243 'wplink', 244 244 'wpdialogs', 245 'wpview', 245 246 ) ) ); 246 247 … … 502 503 add_thickbox(); 503 504 wp_enqueue_script('media-upload'); 505 506 if ( self::$has_tinymce ) 507 wp_enqueue_script('mce-view'); 504 508 } 505 509 } -
trunk/src/wp-includes/js/mce-view.js
r22798 r27408 1 /* global tinymce */ 2 1 3 // Ensure the global `wp` object exists. 2 4 window.wp = window.wp || {}; … … 4 6 (function($){ 5 7 var views = {}, 6 instances = {}; 8 instances = {}, 9 media = wp.media, 10 viewOptions = ['encodedText']; 7 11 8 12 // Create the `wp.mce` object if necessary. 9 13 wp.mce = wp.mce || {}; 10 14 11 // wp.mce.view 12 // ----------- 13 // A set of utilities that simplifies adding custom UI within a TinyMCE editor. 14 // At its core, it serves as a series of converters, transforming text to a 15 // custom UI, and back again. 16 wp.mce.view = { 17 // ### defaults 18 defaults: { 19 // The default properties used for objects with the `pattern` key in 20 // `wp.mce.view.add()`. 21 pattern: { 22 view: Backbone.View, 23 text: function( instance ) { 24 return instance.options.original; 25 }, 26 27 toView: function( content ) { 28 if ( ! this.pattern ) 29 return; 30 31 this.pattern.lastIndex = 0; 32 var match = this.pattern.exec( content ); 33 34 if ( ! match ) 35 return; 36 37 return { 38 index: match.index, 39 content: match[0], 40 options: { 41 original: match[0], 42 results: match 43 } 44 }; 15 /** 16 * wp.mce.View 17 * 18 * A Backbone-like View constructor intended for use when rendering a TinyMCE View. The main difference is 19 * that the TinyMCE View is not tied to a particular DOM node. 20 */ 21 wp.mce.View = function( options ) { 22 options || (options = {}); 23 _.extend(this, _.pick(options, viewOptions)); 24 this.initialize.apply(this, arguments); 25 }; 26 27 _.extend( wp.mce.View.prototype, { 28 initialize: function() {}, 29 html: function() {}, 30 render: function() { 31 var html = this.getHtml(); 32 // Search all tinymce editor instances and update the placeholders 33 _.each( tinymce.editors, function( editor ) { 34 var doc; 35 if ( editor.plugins.wpview ) { 36 doc = editor.getDoc(); 37 $( doc ).find( '[data-wpview-text="' + this.encodedText + '"]' ).html( html ); 45 38 } 46 }, 47 48 // The default properties used for objects with the `shortcode` key in 49 // `wp.mce.view.add()`. 50 shortcode: { 51 view: Backbone.View, 52 text: function( instance ) { 53 return instance.options.shortcode.string(); 54 }, 55 56 toView: function( content ) { 57 var match = wp.shortcode.next( this.shortcode, content ); 58 59 if ( ! match ) 60 return; 61 62 return { 63 index: match.index, 64 content: match.content, 65 options: { 66 shortcode: match.shortcode 67 } 68 }; 69 } 70 } 71 }, 72 73 // ### add( id, options ) 74 // Registers a new TinyMCE view. 75 // 76 // Accepts a unique `id` and an `options` object. 77 // 78 // `options` accepts the following properties: 79 // 80 // * `pattern` is the regular expression used to scan the content and 81 // detect matching views. 82 // 83 // * `view` is a `Backbone.View` constructor. If a plain object is 84 // provided, it will automatically extend the parent constructor 85 // (usually `Backbone.View`). Views are instantiated when the `pattern` 86 // is successfully matched. The instance's `options` object is provided 87 // with the `original` matched value, the match `results` including 88 // capture groups, and the `viewType`, which is the constructor's `id`. 89 // 90 // * `extend` an existing view by passing in its `id`. The current 91 // view will inherit all properties from the parent view, and if 92 // `view` is set to a plain object, it will extend the parent `view` 93 // constructor. 94 // 95 // * `text` is a method that accepts an instance of the `view` 96 // constructor and transforms it into a text representation. 97 add: function( id, options ) { 98 var parent, remove, base, properties; 99 100 // Fetch the parent view or the default options. 101 if ( options.extend ) 102 parent = wp.mce.view.get( options.extend ); 103 else if ( options.shortcode ) 104 parent = wp.mce.view.defaults.shortcode; 105 else 106 parent = wp.mce.view.defaults.pattern; 107 108 // Extend the `options` object with the parent's properties. 109 _.defaults( options, parent ); 110 options.id = id; 111 112 // Create properties used to enhance the view for use in TinyMCE. 113 properties = { 114 // Ensure the wrapper element and references to the view are 115 // removed. Otherwise, removed views could randomly restore. 116 remove: function() { 117 delete instances[ this.el.id ]; 118 this.$el.parent().remove(); 119 120 // Trigger the inherited `remove` method. 121 if ( remove ) 122 remove.apply( this, arguments ); 123 124 return this; 125 } 126 }; 127 128 // If the `view` provided was an object, use the parent's 129 // `view` constructor as a base. If a `view` constructor 130 // was provided, treat that as the base. 131 if ( _.isFunction( options.view ) ) { 132 base = options.view; 133 } else { 134 base = parent.view; 135 remove = options.view.remove; 136 _.defaults( properties, options.view ); 137 } 138 139 // If there's a `remove` method on the `base` view that wasn't 140 // created by this method, inherit it. 141 if ( ! remove && ! base._mceview ) 142 remove = base.prototype.remove; 143 144 // Automatically create the new `Backbone.View` constructor. 145 options.view = base.extend( properties, { 146 // Flag that the new view has been created by `wp.mce.view`. 147 _mceview: true 148 }); 149 150 views[ id ] = options; 151 }, 152 153 // ### get( id ) 154 // Returns a TinyMCE view options object. 155 get: function( id ) { 156 return views[ id ]; 157 }, 158 159 // ### remove( id ) 160 // Unregisters a TinyMCE view. 161 remove: function( id ) { 162 delete views[ id ]; 163 }, 164 165 // ### toViews( content ) 166 // Scans a `content` string for each view's pattern, replacing any 167 // matches with wrapper elements, and creates a new view instance for 168 // every match. 169 // 170 // To render the views, call `wp.mce.view.render( scope )`. 39 }, this ); 40 } 41 } ); 42 43 // take advantage of the Backbone extend method 44 wp.mce.View.extend = Backbone.View.extend; 45 46 /** 47 * wp.mce.views 48 * 49 * A set of utilities that simplifies adding custom UI within a TinyMCE editor. 50 * At its core, it serves as a series of converters, transforming text to a 51 * custom UI, and back again. 52 */ 53 wp.mce.views = { 54 55 /** 56 * wp.mce.views.register( type, view ) 57 * 58 * Registers a new TinyMCE view. 59 * 60 * @param type 61 * @param constructor 62 * 63 */ 64 register: function( type, constructor ) { 65 views[ type ] = constructor; 66 }, 67 68 /** 69 * wp.mce.views.get( id ) 70 * 71 * Returns a TinyMCE view constructor. 72 */ 73 get: function( type ) { 74 return views[ type ]; 75 }, 76 77 /** 78 * wp.mce.views.unregister( type ) 79 * 80 * Unregisters a TinyMCE view. 81 */ 82 unregister: function( type ) { 83 delete views[ type ]; 84 }, 85 86 /** 87 * toViews( content ) 88 * Scans a `content` string for each view's pattern, replacing any 89 * matches with wrapper elements, and creates a new instance for 90 * every match, which triggers the related data to be fetched. 91 * 92 */ 171 93 toViews: function( content ) { 172 94 var pieces = [ { content: content } ], … … 191 113 while ( remaining && (result = view.toView( remaining )) ) { 192 114 // Any text before the match becomes an unprocessed piece. 193 if ( result.index ) 115 if ( result.index ) { 194 116 pieces.push({ content: remaining.substring( 0, result.index ) }); 117 } 195 118 196 119 // Add the processed piece for the match. 197 120 pieces.push({ 198 content: wp.mce.view.toView( viewType, result.options ),121 content: wp.mce.views.toView( viewType, result.content, result.options ), 199 122 processed: true 200 123 }); … … 206 129 // There are no additional matches. If any content remains, 207 130 // add it as an unprocessed piece. 208 if ( remaining ) 131 if ( remaining ) { 209 132 pieces.push({ content: remaining }); 133 } 210 134 }); 211 135 }); … … 214 138 }, 215 139 216 toView: function( viewType, options ) { 217 var view = wp.mce.view.get( viewType ), 218 instance, id; 219 220 if ( ! view ) 221 return ''; 222 223 // Create a new view instance. 224 instance = new view.view( _.extend( options || {}, { 225 viewType: viewType 226 }) ); 227 228 // Use the view's `id` if it already exists. Otherwise, 229 // create a new `id`. 230 id = instance.el.id = instance.el.id || _.uniqueId('__wpmce-'); 231 instances[ id ] = instance; 232 233 // Create a dummy `$wrapper` property to allow `$wrapper` to be 234 // called in the view's `render` method without a conditional. 235 instance.$wrapper = $(); 140 /** 141 * Create a placeholder for a particular view type 142 * 143 * @param viewType 144 * @param text 145 * @param options 146 * 147 */ 148 toView: function( viewType, text, options ) { 149 var view = wp.mce.views.get( viewType ), 150 encodedText = window.encodeURIComponent( text ), 151 instance, viewOptions; 152 153 154 if ( ! view ) { 155 return text; 156 } 157 158 if ( ! wp.mce.views.getInstance( encodedText ) ) { 159 viewOptions = options; 160 viewOptions.encodedText = encodedText; 161 instance = new view.View( viewOptions ); 162 instances[ encodedText ] = instance; 163 } 236 164 237 165 return wp.html.string({ 238 // If the view is a span, wrap it in a span. 239 tag: 'span' === instance.tagName ? 'span' : 'div', 166 tag: 'div', 240 167 241 168 attrs: { 242 'class': 'wp-view-wrap wp-view-type-' + viewType, 243 'data-wp-view': id, 244 'contenteditable': false 245 } 169 'class': 'wpview-wrap wpview-type-' + viewType, 170 'data-wpview-text': encodedText, 171 'data-wpview-type': viewType, 172 'contenteditable': 'false' 173 }, 174 175 content: '\u00a0' 246 176 }); 247 177 }, 248 178 249 // ### render( scope ) 250 // Renders any view instances inside a DOM node `scope`. 251 // 252 // View instances are detected by the presence of wrapper elements. 253 // To generate wrapper elements, pass your content through 254 // `wp.mce.view.toViews( content )`. 255 render: function( scope ) { 256 $( '.wp-view-wrap', scope ).each( function() { 257 var wrapper = $(this), 258 view = wp.mce.view.instance( this ); 259 260 if ( ! view ) 261 return; 262 263 // Link the real wrapper to the view. 264 view.$wrapper = wrapper; 265 // Render the view. 266 view.render(); 267 // Detach the view element to ensure events are not unbound. 268 view.$el.detach(); 269 270 // Empty the wrapper, attach the view element to the wrapper, 271 // and add an ending marker to the wrapper to help regexes 272 // scan the HTML string. 273 wrapper.empty().append( view.el ).append('<span data-wp-view-end class="wp-view-end"></span>'); 274 }); 275 }, 276 277 // ### toText( content ) 278 // Scans an HTML `content` string and replaces any view instances with 279 // their respective text representations. 280 toText: function( content ) { 281 return content.replace( /<(?:div|span)[^>]+data-wp-view="([^"]+)"[^>]*>.*?<span[^>]+data-wp-view-end[^>]*><\/span><\/(?:div|span)>/g, function( match, id ) { 282 var instance = instances[ id ], 283 view; 284 285 if ( instance ) 286 view = wp.mce.view.get( instance.options.viewType ); 287 288 return instance && view ? view.text( instance ) : ''; 289 }); 290 }, 291 292 // ### Remove internal TinyMCE attributes. 293 removeInternalAttrs: function( attrs ) { 294 var result = {}; 295 _.each( attrs, function( value, attr ) { 296 if ( -1 === attr.indexOf('data-mce') ) 297 result[ attr ] = value; 298 }); 299 return result; 300 }, 301 302 // ### Parse an attribute string and removes internal TinyMCE attributes. 303 attrs: function( content ) { 304 return wp.mce.view.removeInternalAttrs( wp.html.attrs( content ) ); 305 }, 306 307 // ### instance( scope ) 308 // 309 // Accepts a MCE view wrapper `node` (i.e. a node with the 310 // `wp-view-wrap` class). 311 instance: function( node ) { 312 var id = $( node ).data('wp-view'); 313 314 if ( id ) 315 return instances[ id ]; 316 }, 317 318 // ### Select a view. 319 // 320 // Accepts a MCE view wrapper `node` (i.e. a node with the 321 // `wp-view-wrap` class). 322 select: function( node ) { 323 var $node = $(node); 324 325 // Bail if node is already selected. 326 if ( $node.hasClass('selected') ) 327 return; 328 329 $node.addClass('selected'); 330 $( node.firstChild ).trigger('select'); 331 }, 332 333 // ### Deselect a view. 334 // 335 // Accepts a MCE view wrapper `node` (i.e. a node with the 336 // `wp-view-wrap` class). 337 deselect: function( node ) { 338 var $node = $(node); 339 340 // Bail if node is already selected. 341 if ( ! $node.hasClass('selected') ) 342 return; 343 344 $node.removeClass('selected'); 345 $( node.firstChild ).trigger('deselect'); 179 /** 180 * Refresh views after an update is made 181 * 182 * @param view {object} being refreshed 183 * @param text {string} textual representation of the view 184 */ 185 refreshView: function( view, text ) { 186 var encodedText = window.encodeURIComponent( text ), 187 viewOptions, 188 result, instance; 189 190 instance = wp.mce.views.getInstance( encodedText ); 191 192 if ( ! instance ) { 193 result = view.toView( text ); 194 viewOptions = result.options; 195 viewOptions.encodedText = encodedText; 196 instance = new view.View( viewOptions ); 197 instances[ encodedText ] = instance; 198 } 199 200 wp.mce.views.render(); 201 }, 202 203 getInstance: function( encodedText ) { 204 return instances[ encodedText ]; 205 }, 206 207 /** 208 * render( scope ) 209 * 210 * Renders any view instances inside a DOM node `scope`. 211 * 212 * View instances are detected by the presence of wrapper elements. 213 * To generate wrapper elements, pass your content through 214 * `wp.mce.view.toViews( content )`. 215 */ 216 render: function() { 217 _.each( instances, function( instance ) { 218 instance.render(); 219 } ); 220 }, 221 222 edit: function( node ) { 223 var viewType = $( node ).data('wpview-type'), 224 view = wp.mce.views.get( viewType ); 225 226 if ( view ) { 227 view.edit( node ); 228 } 346 229 } 347 230 }; 348 231 232 wp.mce.gallery = { 233 shortcode: 'gallery', 234 toView: function( content ) { 235 var match = wp.shortcode.next( this.shortcode, content ); 236 237 if ( ! match ) { 238 return; 239 } 240 241 return { 242 index: match.index, 243 content: match.content, 244 options: { 245 shortcode: match.shortcode 246 } 247 }; 248 }, 249 View: wp.mce.View.extend({ 250 className: 'editor-gallery', 251 template: media.template('editor-gallery'), 252 253 // The fallback post ID to use as a parent for galleries that don't 254 // specify the `ids` or `include` parameters. 255 // 256 // Uses the hidden input on the edit posts page by default. 257 postID: $('#post_ID').val(), 258 259 initialize: function( options ) { 260 this.shortcode = options.shortcode; 261 this.fetch(); 262 }, 263 264 fetch: function() { 265 this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID ); 266 this.attachments.more().done( _.bind( this.render, this ) ); 267 }, 268 269 getHtml: function() { 270 var attrs = this.shortcode.attrs.named, 271 options; 272 273 if ( ! this.attachments.length ) { 274 return; 275 } 276 277 options = { 278 attachments: this.attachments.toJSON(), 279 columns: attrs.columns ? parseInt( attrs.columns, 10 ) : 3 280 }; 281 282 return this.template( options ); 283 284 } 285 }), 286 287 edit: function( node ) { 288 var gallery = wp.media.gallery, 289 self = this, 290 frame, data; 291 292 data = window.decodeURIComponent( $( node ).data('wpview-text') ); 293 frame = gallery.edit( data ); 294 295 frame.state('gallery-edit').on( 'update', function( selection ) { 296 var shortcode = gallery.shortcode( selection ).string(); 297 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) ); 298 wp.mce.views.refreshView( self, shortcode ); 299 frame.detach(); 300 }); 301 } 302 303 }; 304 wp.mce.views.register( 'gallery', wp.mce.gallery ); 349 305 }(jQuery)); -
trunk/src/wp-includes/js/tinymce/plugins/wpgallery/plugin.js
r27276 r27408 60 60 } 61 61 62 // Check if the `wp.media .gallery` API exists.62 // Check if the `wp.media` API exists. 63 63 if ( typeof wp === 'undefined' || ! wp.media ) { 64 64 return; … … 167 167 168 168 editor.on( 'BeforeSetContent', function( event ) { 169 event.content = replaceGalleryShortcodes( event.content ); 169 // 'wpview' handles the gallery shortcode when present 170 if ( ! editor.plugins.wpview ) { 171 event.content = replaceGalleryShortcodes( event.content ); 172 } 173 170 174 event.content = replaceAVShortcodes( event.content ); 171 175 }); -
trunk/src/wp-includes/js/tinymce/plugins/wpview/plugin.js
r26880 r27408 3 3 * WordPress View plugin. 4 4 */ 5 6 (function() { 7 var VK = tinymce.VK,5 tinymce.PluginManager.add( 'wpview', function( editor ) { 6 var selected, 7 VK = tinymce.util.VK, 8 8 TreeWalker = tinymce.dom.TreeWalker, 9 selected; 10 11 tinymce.create('tinymce.plugins.wpView', { 12 init : function( editor ) { 13 var wpView = this; 14 15 // Check if the `wp.mce` API exists. 16 if ( typeof wp === 'undefined' || ! wp.mce ) { 9 toRemove = false; 10 11 function getParentView( node ) { 12 while ( node && node.nodeName !== 'BODY' ) { 13 if ( isView( node ) ) { 14 return node; 15 } 16 17 node = node.parentNode; 18 } 19 } 20 21 function isView( node ) { 22 return node && /\bwpview-wrap\b/.test( node.className ); 23 } 24 25 function createPadNode() { 26 return editor.dom.create( 'p', { 'data-wpview-pad': 1 }, 27 ( tinymce.Env.ie && tinymce.Env.ie < 11 ) ? '' : '<br data-mce-bogus="1" />' ); 28 } 29 30 /** 31 * Get the text/shortcode string for a view. 32 * 33 * @param view The view wrapper's HTML id or node 34 * @returns string The text/shoercode string of the view 35 */ 36 function getViewText( view ) { 37 view = getParentView( typeof view === 'string' ? editor.dom.get( view ) : view ); 38 39 if ( view ) { 40 return window.decodeURIComponent( editor.dom.getAttrib( view, 'data-wpview-text' ) || '' ); 41 } 42 return ''; 43 } 44 45 /** 46 * Set the view's original text/shortcode string 47 * 48 * @param view The view wrapper's HTML id or node 49 * @param text The text string to be set 50 */ 51 function setViewText( view, text ) { 52 view = getParentView( typeof view === 'string' ? editor.dom.get( view ) : view ); 53 54 if ( view ) { 55 editor.dom.setAttrib( view, 'data-wpview-text', window.encodeURIComponent( text || '' ) ); 56 return true; 57 } 58 return false; 59 } 60 61 function _stop( event ) { 62 event.stopPropagation(); 63 } 64 65 function select( viewNode ) { 66 var clipboard, 67 dom = editor.dom; 68 69 // Bail if node is already selected. 70 if ( viewNode === selected ) { 71 return; 72 } 73 74 deselect(); 75 selected = viewNode; 76 dom.addClass( viewNode, 'selected' ); 77 78 clipboard = dom.create( 'div', { 79 'class': 'wpview-clipboard', 80 'contenteditable': 'true' 81 }, getViewText( viewNode ) ); 82 83 viewNode.appendChild( clipboard ); 84 85 // Both of the following are necessary to prevent manipulating the selection/focus 86 editor.dom.bind( clipboard, 'beforedeactivate focusin focusout', _stop ); 87 editor.dom.bind( selected, 'beforedeactivate focusin focusout', _stop ); 88 89 // select the hidden div 90 editor.selection.select( clipboard, true ); 91 } 92 93 /** 94 * Deselect a selected view and remove clipboard 95 */ 96 function deselect() { 97 var clipboard, 98 dom = editor.dom; 99 100 if ( selected ) { 101 clipboard = editor.dom.select( '.wpview-clipboard', selected )[0]; 102 dom.unbind( clipboard ); 103 dom.remove( clipboard ); 104 105 dom.unbind( selected, 'beforedeactivate focusin focusout click mouseup', _stop ); 106 dom.removeClass( selected, 'selected' ); 107 108 editor.selection.select( selected.nextSibling ); 109 editor.selection.collapse(); 110 111 } 112 113 selected = null; 114 } 115 116 // Check if the `wp.mce` API exists. 117 if ( typeof wp === 'undefined' || ! wp.mce ) { 118 return; 119 } 120 121 editor.on( 'BeforeAddUndo', function( event ) { 122 if ( selected && ! toRemove ) { 123 event.preventDefault(); 124 } 125 }); 126 127 // When the editor's content changes, scan the new content for 128 // matching view patterns, and transform the matches into 129 // view wrappers. 130 editor.on( 'BeforeSetContent', function( e ) { 131 if ( ! e.content ) { 132 return; 133 } 134 135 e.content = wp.mce.views.toViews( e.content ); 136 }); 137 138 // When the editor's content has been updated and the DOM has been 139 // processed, render the views in the document. 140 editor.on( 'SetContent', function( event ) { 141 var body, padNode; 142 143 wp.mce.views.render(); 144 145 // Add padding <p> if the noneditable node is last 146 if ( event.load || ! event.set ) { 147 body = editor.getBody(); 148 149 if ( isView( body.lastChild ) ) { 150 padNode = createPadNode(); 151 body.appendChild( padNode ); 152 editor.selection.setCursorLocation( padNode, 0 ); 153 } 154 } 155 156 // refreshEmptyContentNode(); 157 }); 158 159 // Detect mouse down events that are adjacent to a view when a view is the first view or the last view 160 editor.on( 'click', function( event ) { 161 var body = editor.getBody(), 162 doc = editor.getDoc(), 163 scrollTop = doc.documentElement.scrollTop || body.scrollTop || 0, 164 x, y, firstNode, lastNode, padNode; 165 166 if ( event.target.nodeName === 'HTML' && ! event.metaKey && ! event.ctrlKey ) { 167 firstNode = body.firstChild; 168 lastNode = body.lastChild; 169 x = event.clientX; 170 y = event.clientY; 171 172 if ( isView( firstNode ) && ( ( x < firstNode.offsetLeft && y < ( firstNode.offsetHeight - scrollTop ) ) || 173 y < firstNode.offsetTop ) ) { 174 // detect events above or to the left of the first view 175 176 padNode = createPadNode(); 177 body.insertBefore( padNode, firstNode ); 178 } else if ( isView( lastNode ) && ( x > ( lastNode.offsetLeft + lastNode.offsetWidth ) || 179 ( ( scrollTop + y ) - ( lastNode.offsetTop + lastNode.offsetHeight ) ) > 0 ) ) { 180 // detect events to the right and below the last view 181 182 padNode = createPadNode(); 183 body.appendChild( padNode ); 184 } 185 186 if ( padNode ) { 187 editor.selection.setCursorLocation( padNode, 0 ); 188 } 189 } 190 }); 191 192 editor.on( 'init', function() { 193 var selection = editor.selection; 194 // When a view is selected, ensure content that is being pasted 195 // or inserted is added to a text node (instead of the view). 196 editor.on( 'BeforeSetContent', function() { 197 var walker, target, 198 view = getParentView( selection.getNode() ); 199 200 // If the selection is not within a view, bail. 201 if ( ! view ) { 17 202 return; 18 203 } 19 204 20 editor.on( 'PreInit', function() { 21 // Add elements so we can set `contenteditable` to false. 22 editor.schema.addValidElements('div[*],span[*]'); 23 }); 24 25 // When the editor's content changes, scan the new content for 26 // matching view patterns, and transform the matches into 27 // view wrappers. Since the editor's DOM is outdated at this point, 28 // we'll wait to render the views. 29 editor.on( 'BeforeSetContent', function( e ) { 30 if ( ! e.content ) { 31 return; 32 } 33 34 e.content = wp.mce.view.toViews( e.content ); 35 }); 36 37 // When the editor's content has been updated and the DOM has been 38 // processed, render the views in the document. 39 editor.on( 'SetContent', function() { 40 wp.mce.view.render( editor.getDoc() ); 41 }); 42 43 editor.on( 'init', function() { 44 var selection = editor.selection; 45 // When a view is selected, ensure content that is being pasted 46 // or inserted is added to a text node (instead of the view). 47 editor.on( 'BeforeSetContent', function() { 48 var walker, target, 49 view = wpView.getParentView( selection.getNode() ); 50 51 // If the selection is not within a view, bail. 52 if ( ! view ) { 53 return; 54 } 55 56 // If there are no additional nodes or the next node is a 57 // view, create a text node after the current view. 58 if ( ! view.nextSibling || wpView.isView( view.nextSibling ) ) { 59 target = editor.getDoc().createTextNode(''); 60 editor.dom.insertAfter( target, view ); 61 62 // Otherwise, find the next text node. 63 } else { 64 walker = new TreeWalker( view.nextSibling, view.nextSibling ); 65 target = walker.next(); 66 } 67 68 // Select the `target` text node. 69 selection.select( target ); 70 selection.collapse( true ); 71 }); 72 73 // When the selection's content changes, scan any new content 74 // for matching views and immediately render them. 75 // 76 // Runs on paste and on inserting nodes/html. 77 editor.on( 'SetContent', function( e ) { 78 if ( ! e.context ) { 79 return; 80 } 81 82 var node = selection.getNode(); 83 84 if ( ! node.innerHTML ) { 85 return; 86 } 87 88 node.innerHTML = wp.mce.view.toViews( node.innerHTML ); 89 wp.mce.view.render( node ); 90 }); 91 }); 92 93 // When the editor's contents are being accessed as a string, 94 // transform any views back to their text representations. 95 editor.on( 'PostProcess', function( e ) { 96 if ( ( ! e.get && ! e.save ) || ! e.content ) { 97 return; 98 } 99 100 e.content = wp.mce.view.toText( e.content ); 101 }); 102 103 // Triggers when the selection is changed. 104 // Add the event handler to the top of the stack. 105 editor.on( 'NodeChange', function( e ) { 106 var view = wpView.getParentView( e.element ); 107 108 // Update the selected view. 109 if ( view ) { 110 wpView.select( view ); 111 112 // Prevent the selection from propagating to other plugins. 113 return false; 114 115 // If we've clicked off of the selected view, deselect it. 116 } else { 117 wpView.deselect(); 118 } 119 }); 120 121 editor.on( 'keydown', function( event ) { 122 var keyCode = event.keyCode, 123 view, instance; 124 125 // If a view isn't selected, let the event go on its merry way. 126 if ( ! selected ) { 127 return; 128 } 129 130 // If the caret is not within the selected view, deselect the 131 // view and bail. 132 view = wpView.getParentView( editor.selection.getNode() ); 133 if ( view !== selected ) { 134 wpView.deselect(); 135 return; 136 } 137 138 // If delete or backspace is pressed, delete the view. 139 if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) { 140 if ( (instance = wp.mce.view.instance( selected )) ) { 141 instance.remove(); 142 wpView.deselect(); 205 if ( ! view.nextSibling || isView( view.nextSibling ) ) { 206 // If there are no additional nodes or the next node is a 207 // view, create a text node after the current view. 208 target = editor.getDoc().createTextNode(''); 209 editor.dom.insertAfter( target, view ); 210 } else { 211 // Otherwise, find the next text node. 212 walker = new TreeWalker( view.nextSibling, view.nextSibling ); 213 target = walker.next(); 214 } 215 216 // Select the `target` text node. 217 selection.select( target ); 218 selection.collapse( true ); 219 }); 220 221 // When the selection's content changes, scan any new content 222 // for matching views. 223 // 224 // Runs on paste and on inserting nodes/html. 225 editor.on( 'SetContent', function( e ) { 226 if ( ! e.context ) { 227 return; 228 } 229 230 var node = selection.getNode(); 231 232 if ( ! node.innerHTML ) { 233 return; 234 } 235 236 node.innerHTML = wp.mce.views.toViews( node.innerHTML ); 237 }); 238 239 editor.dom.bind( editor.getBody(), 'mousedown mouseup click', function( event ) { 240 var view = getParentView( event.target ); 241 242 // Contain clicks inside the view wrapper 243 if ( view ) { 244 event.stopPropagation(); 245 246 if ( event.type === 'click' ) { 247 if ( ! event.metaKey && ! event.ctrlKey ) { 248 if ( editor.dom.hasClass( event.target, 'edit' ) ) { 249 wp.mce.views.edit( view ); 250 } else if ( editor.dom.hasClass( event.target, 'remove' ) ) { 251 editor.dom.remove( view ); 252 } 143 253 } 144 254 } 145 146 // Let keypresses that involve the command or control keys through. 147 // Also, let any of the F# keys through. 148 if ( event.metaKey || event.ctrlKey || ( keyCode >= 112 && keyCode <= 123 ) ) { 149 return; 255 select( view ); 256 // returning false stops the ugly bars from appearing in IE11 and stops the view being selected as a range in FF 257 // unfortunately, it also inhibits the dragging fo views to a new location 258 return false; 259 } else { 260 if ( event.type === 'click' ) { 261 deselect(); 150 262 } 151 152 event.preventDefault(); 153 }); 154 }, 155 156 getParentView : function( node ) { 157 while ( node ) { 158 if ( this.isView( node ) ) { 159 return node; 263 } 264 }); 265 266 }); 267 268 editor.on( 'PreProcess', function( event ) { 269 var dom = editor.dom; 270 271 // Remove empty padding nodes 272 tinymce.each( dom.select( 'p[data-wpview-pad]', event.node ), function( node ) { 273 if ( dom.isEmpty( node ) ) { 274 dom.remove( node ); 275 } else { 276 dom.setAttrib( node, 'data-wpview-pad', null ); 277 } 278 }); 279 280 // Replace the wpview node with the wpview string/shortcode? 281 tinymce.each( dom.select( 'div[data-wpview-text]', event.node ), function( node ) { 282 // Empty the wrap node 283 if ( 'textContent' in node ) { 284 node.textContent = ''; 285 } else { 286 node.innerText = ''; 287 } 288 289 // TODO: that makes all views into block tags (as we use <div>). 290 // Can use 'PostProcess' and a regex instead. 291 dom.replace( dom.create( 'p', null, window.decodeURIComponent( dom.getAttrib( node, 'data-wpview-text' ) ) ), node ); 292 }); 293 }); 294 295 editor.on( 'keydown', function( event ) { 296 var keyCode = event.keyCode, 297 view; 298 299 // If a view isn't selected, let the event go on its merry way. 300 if ( ! selected ) { 301 return; 302 } 303 304 // Let keypresses that involve the command or control keys through. 305 // Also, let any of the F# keys through. 306 if ( event.metaKey || event.ctrlKey || ( keyCode >= 112 && keyCode <= 123 ) ) { 307 if ( ( event.metaKey || event.ctrlKey ) && keyCode === 88 ) { 308 toRemove = selected; 309 } 310 return; 311 } 312 313 // If the caret is not within the selected view, deselect the 314 // view and bail. 315 view = getParentView( editor.selection.getNode() ); 316 317 if ( view !== selected ) { 318 deselect(); 319 return; 320 } 321 322 // If delete or backspace is pressed, delete the view. 323 if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) { 324 editor.dom.remove( selected ); 325 } 326 327 event.preventDefault(); 328 }); 329 330 editor.on( 'keyup', function( event ) { 331 var padNode, 332 keyCode = event.keyCode, 333 body = editor.getBody(), 334 range; 335 336 if ( toRemove ) { 337 editor.dom.remove( toRemove ); 338 toRemove = false; 339 } 340 341 if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) { 342 // Make sure there is padding if the last element is a view 343 if ( isView( body.lastChild ) ) { 344 padNode = createPadNode(); 345 body.appendChild( padNode ); 346 347 if ( body.childNodes.length === 2 ) { 348 editor.selection.setCursorLocation( padNode, 0 ); 160 349 } 161 162 node = node.parentNode; 163 } 164 }, 165 166 isView : function( node ) { 167 return (/(?:^|\s)wp-view-wrap(?:\s|$)/).test( node.className ); 168 }, 169 170 select : function( view ) { 171 if ( view === selected ) { 172 return; 173 } 174 175 this.deselect(); 176 selected = view; 177 wp.mce.view.select( selected ); 178 }, 179 180 deselect : function() { 181 if ( selected ) { 182 wp.mce.view.deselect( selected ); 183 } 184 185 selected = null; 186 } 187 }); 188 189 // Register plugin 190 tinymce.PluginManager.add( 'wpview', tinymce.plugins.wpView ); 191 })(); 350 } 351 352 range = editor.selection.getRng(); 353 354 // Allow an initial element in the document to be removed when it is before a view 355 if ( body.firstChild === range.startContainer && range.collapsed === true && 356 isView( range.startContainer.nextSibling ) && range.startOffset === 0 ) { 357 358 editor.dom.remove( range.startContainer ); 359 } 360 } 361 }); 362 363 return { 364 getViewText: getViewText, 365 setViewText: setViewText 366 }; 367 }); -
trunk/src/wp-includes/js/tinymce/skins/wordpress/wp-content.css
r27387 r27408 199 199 } 200 200 201 202 /** 203 * WP Views 204 */ 205 206 /* IE hasLayout. Needed for all IE incl. 11 (ugh, not again!!) */ 207 .wpview-wrap { 208 width: 99.99%; 209 position: relative; 210 } 211 212 /* delegate the handling of the selection to the wpview tinymce plugin */ 213 .wpview-wrap, 214 .wpview-wrap * { 215 -moz-user-select: none; 216 -webkit-user-select: none; 217 -ms-user-select: none; 218 user-select: none; 219 } 220 221 /* hide the shortcode content, but allow the content to still be selected */ 222 .wpview-wrap .wpview-clipboard { 223 position: absolute; 224 top: 0; 225 left: 0; 226 z-index: -1; 227 clip: rect(1px, 1px, 1px, 1px); 228 overflow: hidden; 229 outline: 0; 230 } 231 232 /** 233 * Gallery preview 234 */ 235 .wpview-type-gallery { 236 position: relative; 237 padding: 0 0 12px; 238 margin-bottom: 16px; 239 cursor: pointer; 240 } 241 242 .wpview-type-gallery:after { 243 content: ''; 244 display: block; 245 height: 0; 246 clear: both; 247 visibility: hidden; 248 } 249 250 .wpview-type-gallery.selected { 251 background-color: #efefef; 252 } 253 254 .wpview-type-gallery .toolbar { 255 position: absolute; 256 top: 0; 257 left: 0; 258 background-color: #333; 259 color: white; 260 padding: 4px; 261 display: none; 262 } 263 264 .wpview-type-gallery.selected .toolbar { 265 display: block; 266 } 267 268 .wpview-type-gallery .toolbar span { 269 cursor: pointer; 270 } 271 272 .gallery img[data-mce-selected]:focus { 273 outline: none; 274 } 275 276 .gallery a { 277 cursor: default; 278 } 279 280 .gallery { 281 margin: auto; 282 line-height: 1; 283 } 284 285 .gallery .gallery-item { 286 float: left; 287 margin: 10px 0 0 0; 288 text-align: center; 289 } 290 291 .gallery .gallery-caption, 292 .gallery .gallery-icon { 293 margin: 0; 294 } 295 296 .gallery-columns-1 .gallery-item { 297 width: 99%; 298 } 299 300 .gallery-columns-2 .gallery-item { 301 width: 49.5%; 302 } 303 304 .gallery-columns-3 .gallery-item { 305 width: 33%; 306 } 307 308 .gallery-columns-4 .gallery-item { 309 width: 24.75%; 310 } 311 312 .gallery-columns-5 .gallery-item { 313 width: 19.825%; 314 } 315 316 .gallery-columns-6 .gallery-item { 317 width: 16%; 318 } 319 320 .gallery-columns-7 .gallery-item { 321 width: 14%; 322 } 323 324 .gallery-columns-8 .gallery-item { 325 width: 12%; 326 } 327 328 .gallery-columns-9 .gallery-item { 329 width: 11%; 330 } 331 332 .gallery img { 333 border: 1px solid #cfcfcf; 334 } 335 201 336 img.wp-oembed { 202 337 border: 1px dashed #888; -
trunk/src/wp-includes/media-template.php
r27400 r27408 651 651 <?php 652 652 653 //TODO: do we want to deal with the fact that the elements used for gallery items are filterable and can be overriden via shortcode attributes 654 // do we want to deal with the difference between display and edit context at all? (e.g. wptexturize() being applied to the caption. 655 ?> 656 657 <script type="text/html" id="tmpl-editor-gallery"> 658 <div class="toolbar"> 659 <div class="dashicons dashicons-format-gallery edit"></div> 660 <div class="dashicons dashicons-no-alt remove"></div> 661 </div> 662 <div class="gallery gallery-columns-{{{ data.columns }}}"> 663 <# _.each( data.attachments, function( attachment, index ) { #> 664 <dl class="gallery-item"> 665 <dt class="gallery-icon"> 666 <?php // TODO: need to figure out the best way to make sure that we have thumbnails ?> 667 <img src="{{{ attachment.sizes.thumbnail.url }}}" /> 668 </dt> 669 <dd class="wp-caption-text gallery-caption"> 670 {{ attachment.caption }} 671 </dd> 672 </dl> 673 <?php // this is kind silly, but copied from the gallery shortcode. Maybe it should be removed ?> 674 <# if ( index % data.columns === data.columns - 1 ) { #> 675 <br style="clear: both;"> 676 <# } #> 677 678 <# } ); #> 679 </div> 680 </script> 681 <?php 682 653 683 /** 654 684 * Prints the media manager custom media templates. -
trunk/src/wp-includes/version.php
r27387 r27408 19 19 * @global string $tinymce_version 20 20 */ 21 $tinymce_version = '4018-2014030 3';21 $tinymce_version = '4018-20140304'; 22 22 23 23 /**
Note: See TracChangeset
for help on using the changeset viewer.