dd2113bef80f9833bc618237e574a171922c3bbe
[autoinstalls/wordpress.git] / wp-includes / js / tinymce / plugins / wpeditimage / plugin.js
1 /* global tinymce */
2 tinymce.PluginManager.add( 'wpeditimage', function( editor ) {
3         var floatingToolbar, serializer,
4                 DOM = tinymce.DOM,
5                 settings = editor.settings,
6                 Factory = tinymce.ui.Factory,
7                 each = tinymce.each,
8                 iOS = tinymce.Env.iOS,
9                 toolbarIsHidden = true,
10                 editorWrapParent = tinymce.$( '#postdivrich' );
11
12         function isPlaceholder( node ) {
13                 return !! ( editor.dom.getAttrib( node, 'data-mce-placeholder' ) || editor.dom.getAttrib( node, 'data-mce-object' ) );
14         }
15
16         editor.addButton( 'wp_img_remove', {
17                 tooltip: 'Remove',
18                 icon: 'dashicon dashicons-no',
19                 onclick: function() {
20                         removeImage( editor.selection.getNode() );
21                 }
22         } );
23
24         editor.addButton( 'wp_img_edit', {
25                 tooltip: 'Edit ', // trailing space is needed, used for context
26                 icon: 'dashicon dashicons-edit',
27                 onclick: function() {
28                         editImage( editor.selection.getNode() );
29                 }
30         } );
31
32         each( {
33                 alignleft: 'Align left',
34                 aligncenter: 'Align center',
35                 alignright: 'Align right',
36                 alignnone: 'No alignment'
37         }, function( tooltip, name ) {
38                 var direction = name.slice( 5 );
39
40                 editor.addButton( 'wp_img_' + name, {
41                         tooltip: tooltip,
42                         icon: 'dashicon dashicons-align-' + direction,
43                         cmd: 'alignnone' === name ? 'wpAlignNone' : 'Justify' + direction.slice( 0, 1 ).toUpperCase() + direction.slice( 1 ),
44                         onPostRender: function() {
45                                 var self = this;
46
47                                 editor.on( 'NodeChange', function( event ) {
48                                         var node;
49
50                                         // Don't bother.
51                                         if ( event.element.nodeName !== 'IMG' ) {
52                                                 return;
53                                         }
54
55                                         node = editor.dom.getParent( event.element, '.wp-caption' ) || event.element;
56
57                                         if ( 'alignnone' === name ) {
58                                                 self.active( ! /\balign(left|center|right)\b/.test( node.className ) );
59                                         } else {
60                                                 self.active( editor.dom.hasClass( node, name ) );
61                                         }
62                                 } );
63                         }
64                 } );
65         } );
66
67         function toolbarConfig() {
68                 var toolbarItems = [],
69                         buttonGroup;
70
71                 each( [ 'wp_img_alignleft', 'wp_img_aligncenter', 'wp_img_alignright', 'wp_img_alignnone', 'wp_img_edit', 'wp_img_remove' ], function( item ) {
72                         var itemName;
73
74                         function bindSelectorChanged() {
75                                 var selection = editor.selection;
76
77                                 if ( item.settings.stateSelector ) {
78                                         selection.selectorChanged( item.settings.stateSelector, function( state ) {
79                                                 item.active( state );
80                                         }, true );
81                                 }
82
83                                 if ( item.settings.disabledStateSelector ) {
84                                         selection.selectorChanged( item.settings.disabledStateSelector, function( state ) {
85                                                 item.disabled( state );
86                                         } );
87                                 }
88                         }
89
90                         if ( item === '|' ) {
91                                 buttonGroup = null;
92                         } else {
93                                 if ( Factory.has( item ) ) {
94                                         item = {
95                                                 type: item
96                                         };
97
98                                         if ( settings.toolbar_items_size ) {
99                                                 item.size = settings.toolbar_items_size;
100                                         }
101
102                                         toolbarItems.push( item );
103
104                                         buttonGroup = null;
105                                 } else {
106                                         if ( ! buttonGroup ) {
107                                                 buttonGroup = {
108                                                         type: 'buttongroup',
109                                                         items: []
110                                                 };
111
112                                                 toolbarItems.push( buttonGroup );
113                                         }
114
115                                         if ( editor.buttons[ item ] ) {
116                                                 itemName = item;
117                                                 item = editor.buttons[ itemName ];
118
119                                                 if ( typeof item === 'function' ) {
120                                                         item = item();
121                                                 }
122
123                                                 item.type = item.type || 'button';
124
125                                                 if ( settings.toolbar_items_size ) {
126                                                         item.size = settings.toolbar_items_size;
127                                                 }
128
129                                                 item = Factory.create( item );
130                                                 buttonGroup.items.push( item );
131
132                                                 if ( editor.initialized ) {
133                                                         bindSelectorChanged();
134                                                 } else {
135                                                         editor.on( 'init', bindSelectorChanged );
136                                                 }
137                                         }
138                                 }
139                         }
140                 } );
141
142                 return {
143                         type: 'panel',
144                         layout: 'stack',
145                         classes: 'toolbar-grp inline-toolbar-grp wp-image-toolbar',
146                         ariaRoot: true,
147                         ariaRemember: true,
148                         items: [
149                                 {
150                                         type: 'toolbar',
151                                         layout: 'flow',
152                                         items: toolbarItems
153                                 }
154                         ]
155                 };
156         }
157
158         floatingToolbar = Factory.create( toolbarConfig() ).renderTo( document.body ).hide();
159
160         floatingToolbar.reposition = function() {
161                 var top, left, minTop, className,
162                         windowPos, adminbar, mceToolbar, boundary,
163                         boundaryMiddle, boundaryVerticalMiddle, spaceTop,
164                         spaceBottom, windowWidth, toolbarWidth, toolbarHalf,
165                         iframe, iframePos, iframeWidth, iframeHeigth,
166                         toolbarNodeHeight, verticalSpaceNeeded,
167                         toolbarNode = this.getEl(),
168                         buffer = 5,
169                         margin = 8,
170                         adminbarHeight = 0,
171                         imageNode = editor.selection.getNode();
172
173                 if ( ! imageNode || imageNode.nodeName !== 'IMG' ) {
174                         return this;
175                 }
176
177                 windowPos = window.pageYOffset || document.documentElement.scrollTop;
178                 adminbar = tinymce.$( '#wpadminbar' )[0];
179                 mceToolbar = tinymce.$( '.mce-tinymce .mce-toolbar-grp' )[0];
180                 boundary = imageNode.getBoundingClientRect();
181                 boundaryMiddle = ( boundary.left + boundary.right ) / 2;
182                 boundaryVerticalMiddle = ( boundary.top + boundary.bottom ) / 2;
183                 spaceTop = boundary.top;
184                 spaceBottom = iframeHeigth - boundary.bottom;
185                 windowWidth = window.innerWidth;
186                 toolbarWidth = toolbarNode.offsetWidth;
187                 toolbarHalf = toolbarWidth / 2;
188                 iframe = editor.getContentAreaContainer().firstChild;
189                 iframePos = DOM.getPos( iframe );
190                 iframeWidth = iframe.offsetWidth;
191                 iframeHeigth = iframe.offsetHeight;
192                 toolbarNodeHeight = toolbarNode.offsetHeight;
193                 verticalSpaceNeeded = toolbarNodeHeight + margin + buffer;
194
195                 if ( iOS ) {
196                         top = boundary.top + iframePos.y + margin;
197                 } else {
198                         if ( spaceTop >= verticalSpaceNeeded ) {
199                                 className = ' mce-arrow-down';
200                                 top = boundary.top + iframePos.y - toolbarNodeHeight - margin;
201                         } else if ( spaceBottom >= verticalSpaceNeeded ) {
202                                 className = ' mce-arrow-up';
203                                 top = boundary.bottom + iframePos.y;
204                         } else {
205                                 top = buffer;
206
207                                 if ( boundaryVerticalMiddle >= verticalSpaceNeeded ) {
208                                         className = ' mce-arrow-down';
209                                 } else {
210                                         className = ' mce-arrow-up';
211                                 }
212                         }
213                 }
214
215                 // Make sure the image toolbar is below the main toolbar.
216                 if ( mceToolbar ) {
217                         minTop = DOM.getPos( mceToolbar ).y + mceToolbar.clientHeight;
218                 } else {
219                         minTop = iframePos.y;
220                 }
221
222                 // Make sure the image toolbar is below the adminbar (if visible) or below the top of the window.
223                 if ( windowPos ) {
224                         if ( adminbar && adminbar.getBoundingClientRect().top === 0 ) {
225                                 adminbarHeight = adminbar.clientHeight;
226                         }
227
228                         if ( windowPos + adminbarHeight > minTop ) {
229                                 minTop = windowPos + adminbarHeight;
230                         }
231                 }
232
233                 if ( top && minTop && ( minTop + buffer > top ) ) {
234                         top = minTop + buffer;
235                         className = '';
236                 }
237
238                 left = boundaryMiddle - toolbarHalf;
239                 left += iframePos.x;
240
241                 if ( toolbarWidth >= windowWidth ) {
242                         className += ' mce-arrow-full';
243                         left = 0;
244                 } else if ( ( left < 0 && boundary.left + toolbarWidth > windowWidth ) ||
245                         ( left + toolbarWidth > windowWidth && boundary.right - toolbarWidth < 0 ) ) {
246
247                         left = ( windowWidth - toolbarWidth ) / 2;
248                 } else if ( left < iframePos.x ) {
249                         className += ' mce-arrow-left';
250                         left = boundary.left + iframePos.x;
251                 } else if ( left + toolbarWidth > iframeWidth + iframePos.x ) {
252                         className += ' mce-arrow-right';
253                         left = boundary.right - toolbarWidth + iframePos.x;
254                 }
255
256                 if ( ! iOS ) {
257                         toolbarNode.className = toolbarNode.className.replace( / ?mce-arrow-[\w]+/g, '' );
258                         toolbarNode.className += className;
259                 }
260
261                 DOM.setStyles( toolbarNode, { 'left': left, 'top': top } );
262
263                 return this;
264         };
265
266         if ( iOS ) {
267                 // Safari on iOS fails to select image nodes in contentEditoble mode on touch/click.
268                 // Select them again.
269                 editor.on( 'click', function( event ) {
270                         if ( event.target.nodeName === 'IMG' ) {
271                                 var node = event.target;
272
273                                 window.setTimeout( function() {
274                                         editor.selection.select( node );
275                                 }, 200 );
276                         } else {
277                                 floatingToolbar.hide();
278                         }
279                 });
280         }
281
282         editor.on( 'nodechange', function( event ) {
283                 var delay = iOS ? 350 : 100;
284
285                 if ( event.element.nodeName !== 'IMG' || isPlaceholder( event.element ) ) {
286                         floatingToolbar.hide();
287                         return;
288                 }
289
290                 setTimeout( function() {
291                         var element = editor.selection.getNode();
292
293                         if ( element.nodeName === 'IMG' && ! isPlaceholder( element ) ) {
294                                 if ( floatingToolbar._visible ) {
295                                         floatingToolbar.reposition();
296                                 } else {
297                                         floatingToolbar.show();
298                                 }
299                         } else {
300                                 floatingToolbar.hide();
301                         }
302                 }, delay );
303         } );
304
305         function hide() {
306                 if ( ! toolbarIsHidden ) {
307                         floatingToolbar.hide();
308                 }
309         }
310
311         floatingToolbar.on( 'show', function() {
312                 toolbarIsHidden = false;
313
314                 if ( this._visible ) {
315                         this.reposition();
316                         DOM.addClass( this.getEl(), 'mce-inline-toolbar-grp-active' );
317                 }
318         } );
319
320         floatingToolbar.on( 'hide', function() {
321                 toolbarIsHidden = true;
322                 DOM.removeClass( this.getEl(), 'mce-inline-toolbar-grp-active' );
323         } );
324
325         floatingToolbar.on( 'keydown', function( event ) {
326                 if ( event.keyCode === 27 ) {
327                         hide();
328                         editor.focus();
329                 }
330         } );
331
332         DOM.bind( window, 'resize scroll', function() {
333                 if ( ! toolbarIsHidden && editorWrapParent.hasClass( 'wp-editor-expand' ) ) {
334                         hide();
335                 }
336         });
337
338         editor.on( 'init', function() {
339                 editor.dom.bind( editor.getWin(), 'scroll', hide );
340         });
341
342         editor.on( 'blur hide', hide );
343
344         // 119 = F8
345         editor.shortcuts.add( 'Alt+119', '', function() {
346                 var node = floatingToolbar.find( 'toolbar' )[0];
347
348                 if ( node ) {
349                         node.focus( true );
350                 }
351         });
352
353         function parseShortcode( content ) {
354                 return content.replace( /(?:<p>)?\[(?:wp_)?caption([^\]]+)\]([\s\S]+?)\[\/(?:wp_)?caption\](?:<\/p>)?/g, function( a, b, c ) {
355                         var id, align, classes, caption, img, width,
356                                 trim = tinymce.trim;
357
358                         id = b.match( /id=['"]([^'"]*)['"] ?/ );
359                         if ( id ) {
360                                 b = b.replace( id[0], '' );
361                         }
362
363                         align = b.match( /align=['"]([^'"]*)['"] ?/ );
364                         if ( align ) {
365                                 b = b.replace( align[0], '' );
366                         }
367
368                         classes = b.match( /class=['"]([^'"]*)['"] ?/ );
369                         if ( classes ) {
370                                 b = b.replace( classes[0], '' );
371                         }
372
373                         width = b.match( /width=['"]([0-9]*)['"] ?/ );
374                         if ( width ) {
375                                 b = b.replace( width[0], '' );
376                         }
377
378                         c = trim( c );
379                         img = c.match( /((?:<a [^>]+>)?<img [^>]+>(?:<\/a>)?)([\s\S]*)/i );
380
381                         if ( img && img[2] ) {
382                                 caption = trim( img[2] );
383                                 img = trim( img[1] );
384                         } else {
385                                 // old captions shortcode style
386                                 caption = trim( b ).replace( /caption=['"]/, '' ).replace( /['"]$/, '' );
387                                 img = c;
388                         }
389
390                         id = ( id && id[1] ) ? id[1].replace( /[<>&]+/g,  '' ) : '';
391                         align = ( align && align[1] ) ? align[1] : 'alignnone';
392                         classes = ( classes && classes[1] ) ? ' ' + classes[1].replace( /[<>&]+/g,  '' ) : '';
393
394                         if ( ! width && img ) {
395                                 width = img.match( /width=['"]([0-9]*)['"]/ );
396                         }
397
398                         if ( width && width[1] ) {
399                                 width = width[1];
400                         }
401
402                         if ( ! width || ! caption ) {
403                                 return c;
404                         }
405
406                         width = parseInt( width, 10 );
407                         if ( ! editor.getParam( 'wpeditimage_html5_captions' ) ) {
408                                 width += 10;
409                         }
410
411                         return '<div class="mceTemp"><dl id="' + id + '" class="wp-caption ' + align + classes + '" style="width: ' + width + 'px">' +
412                                 '<dt class="wp-caption-dt">'+ img +'</dt><dd class="wp-caption-dd">'+ caption +'</dd></dl></div>';
413                 });
414         }
415
416         function getShortcode( content ) {
417                 return content.replace( /<div (?:id="attachment_|class="mceTemp)[^>]*>([\s\S]+?)<\/div>/g, function( a, b ) {
418                         var out = '';
419
420                         if ( b.indexOf('<img ') === -1 ) {
421                                 // Broken caption. The user managed to drag the image out?
422                                 // Try to return the caption text as a paragraph.
423                                 out = b.match( /<dd [^>]+>([\s\S]+?)<\/dd>/i );
424
425                                 if ( out && out[1] ) {
426                                         return '<p>' + out[1] + '</p>';
427                                 }
428
429                                 return '';
430                         }
431
432                         out = b.replace( /\s*<dl ([^>]+)>\s*<dt [^>]+>([\s\S]+?)<\/dt>\s*<dd [^>]+>([\s\S]*?)<\/dd>\s*<\/dl>\s*/gi, function( a, b, c, caption ) {
433                                 var id, classes, align, width;
434
435                                 width = c.match( /width="([0-9]*)"/ );
436                                 width = ( width && width[1] ) ? width[1] : '';
437
438                                 if ( ! width || ! caption ) {
439                                         return c;
440                                 }
441
442                                 id = b.match( /id="([^"]*)"/ );
443                                 id = ( id && id[1] ) ? id[1] : '';
444
445                                 classes = b.match( /class="([^"]*)"/ );
446                                 classes = ( classes && classes[1] ) ? classes[1] : '';
447
448                                 align = classes.match( /align[a-z]+/i ) || 'alignnone';
449                                 classes = classes.replace( /wp-caption ?|align[a-z]+ ?/gi, '' );
450
451                                 if ( classes ) {
452                                         classes = ' class="' + classes + '"';
453                                 }
454
455                                 caption = caption.replace( /\r\n|\r/g, '\n' ).replace( /<[a-zA-Z0-9]+( [^<>]+)?>/g, function( a ) {
456                                         // no line breaks inside HTML tags
457                                         return a.replace( /[\r\n\t]+/, ' ' );
458                                 });
459
460                                 // convert remaining line breaks to <br>
461                                 caption = caption.replace( /\s*\n\s*/g, '<br />' );
462
463                                 return '[caption id="' + id + '" align="' + align + '" width="' + width + '"' + classes + ']' + c + ' ' + caption + '[/caption]';
464                         });
465
466                         if ( out.indexOf('[caption') === -1 ) {
467                                 // the caption html seems broken, try to find the image that may be wrapped in a link
468                                 // and may be followed by <p> with the caption text.
469                                 out = b.replace( /[\s\S]*?((?:<a [^>]+>)?<img [^>]+>(?:<\/a>)?)(<p>[\s\S]*<\/p>)?[\s\S]*/gi, '<p>$1</p>$2' );
470                         }
471
472                         return out;
473                 });
474         }
475
476         function extractImageData( imageNode ) {
477                 var classes, extraClasses, metadata, captionBlock, caption, link, width, height,
478                         captionClassName = [],
479                         dom = editor.dom,
480                         isIntRegExp = /^\d+$/;
481
482                 // default attributes
483                 metadata = {
484                         attachment_id: false,
485                         size: 'custom',
486                         caption: '',
487                         align: 'none',
488                         extraClasses: '',
489                         link: false,
490                         linkUrl: '',
491                         linkClassName: '',
492                         linkTargetBlank: false,
493                         linkRel: '',
494                         title: ''
495                 };
496
497                 metadata.url = dom.getAttrib( imageNode, 'src' );
498                 metadata.alt = dom.getAttrib( imageNode, 'alt' );
499                 metadata.title = dom.getAttrib( imageNode, 'title' );
500
501                 width = dom.getAttrib( imageNode, 'width' );
502                 height = dom.getAttrib( imageNode, 'height' );
503
504                 if ( ! isIntRegExp.test( width ) || parseInt( width, 10 ) < 1 ) {
505                         width = imageNode.naturalWidth || imageNode.width;
506                 }
507
508                 if ( ! isIntRegExp.test( height ) || parseInt( height, 10 ) < 1 ) {
509                         height = imageNode.naturalHeight || imageNode.height;
510                 }
511
512                 metadata.customWidth = metadata.width = width;
513                 metadata.customHeight = metadata.height = height;
514
515                 classes = tinymce.explode( imageNode.className, ' ' );
516                 extraClasses = [];
517
518                 tinymce.each( classes, function( name ) {
519
520                         if ( /^wp-image/.test( name ) ) {
521                                 metadata.attachment_id = parseInt( name.replace( 'wp-image-', '' ), 10 );
522                         } else if ( /^align/.test( name ) ) {
523                                 metadata.align = name.replace( 'align', '' );
524                         } else if ( /^size/.test( name ) ) {
525                                 metadata.size = name.replace( 'size-', '' );
526                         } else {
527                                 extraClasses.push( name );
528                         }
529
530                 } );
531
532                 metadata.extraClasses = extraClasses.join( ' ' );
533
534                 // Extract caption
535                 captionBlock = dom.getParents( imageNode, '.wp-caption' );
536
537                 if ( captionBlock.length ) {
538                         captionBlock = captionBlock[0];
539
540                         classes = captionBlock.className.split( ' ' );
541                         tinymce.each( classes, function( name ) {
542                                 if ( /^align/.test( name ) ) {
543                                         metadata.align = name.replace( 'align', '' );
544                                 } else if ( name && name !== 'wp-caption' ) {
545                                         captionClassName.push( name );
546                                 }
547                         } );
548
549                         metadata.captionClassName = captionClassName.join( ' ' );
550
551                         caption = dom.select( 'dd.wp-caption-dd', captionBlock );
552                         if ( caption.length ) {
553                                 caption = caption[0];
554
555                                 metadata.caption = editor.serializer.serialize( caption )
556                                         .replace( /<br[^>]*>/g, '$&\n' ).replace( /^<p>/, '' ).replace( /<\/p>$/, '' );
557                         }
558                 }
559
560                 // Extract linkTo
561                 if ( imageNode.parentNode && imageNode.parentNode.nodeName === 'A' ) {
562                         link = imageNode.parentNode;
563                         metadata.linkUrl = dom.getAttrib( link, 'href' );
564                         metadata.linkTargetBlank = dom.getAttrib( link, 'target' ) === '_blank' ? true : false;
565                         metadata.linkRel = dom.getAttrib( link, 'rel' );
566                         metadata.linkClassName = link.className;
567                 }
568
569                 return metadata;
570         }
571
572         function hasTextContent( node ) {
573                 return node && !! ( node.textContent || node.innerText );
574         }
575
576         // Verify HTML in captions
577         function verifyHTML( caption ) {
578                 if ( ! caption || ( caption.indexOf( '<' ) === -1 && caption.indexOf( '>' ) === -1 ) ) {
579                         return caption;
580                 }
581
582                 if ( ! serializer ) {
583                         serializer = new tinymce.html.Serializer( {}, editor.schema );
584                 }
585
586                 return serializer.serialize( editor.parser.parse( caption, { forced_root_block: false } ) );
587         }
588
589         function updateImage( imageNode, imageData ) {
590                 var classes, className, node, html, parent, wrap, linkNode,
591                         captionNode, dd, dl, id, attrs, linkAttrs, width, height, align,
592                         dom = editor.dom;
593
594                 classes = tinymce.explode( imageData.extraClasses, ' ' );
595
596                 if ( ! classes ) {
597                         classes = [];
598                 }
599
600                 if ( ! imageData.caption ) {
601                         classes.push( 'align' + imageData.align );
602                 }
603
604                 if ( imageData.attachment_id ) {
605                         classes.push( 'wp-image-' + imageData.attachment_id );
606                         if ( imageData.size && imageData.size !== 'custom' ) {
607                                 classes.push( 'size-' + imageData.size );
608                         }
609                 }
610
611                 width = imageData.width;
612                 height = imageData.height;
613
614                 if ( imageData.size === 'custom' ) {
615                         width = imageData.customWidth;
616                         height = imageData.customHeight;
617                 }
618
619                 attrs = {
620                         src: imageData.url,
621                         width: width || null,
622                         height: height || null,
623                         alt: imageData.alt,
624                         title: imageData.title || null,
625                         'class': classes.join( ' ' ) || null
626                 };
627
628                 dom.setAttribs( imageNode, attrs );
629
630                 linkAttrs = {
631                         href: imageData.linkUrl,
632                         rel: imageData.linkRel || null,
633                         target: imageData.linkTargetBlank ? '_blank': null,
634                         'class': imageData.linkClassName || null
635                 };
636
637                 if ( imageNode.parentNode && imageNode.parentNode.nodeName === 'A' && ! hasTextContent( imageNode.parentNode ) ) {
638                         // Update or remove an existing link wrapped around the image
639                         if ( imageData.linkUrl ) {
640                                 dom.setAttribs( imageNode.parentNode, linkAttrs );
641                         } else {
642                                 dom.remove( imageNode.parentNode, true );
643                         }
644                 } else if ( imageData.linkUrl ) {
645                         if ( linkNode = dom.getParent( imageNode, 'a' ) ) {
646                                 // The image is inside a link together with other nodes,
647                                 // or is nested in another node, move it out
648                                 dom.insertAfter( imageNode, linkNode );
649                         }
650
651                         // Add link wrapped around the image
652                         linkNode = dom.create( 'a', linkAttrs );
653                         imageNode.parentNode.insertBefore( linkNode, imageNode );
654                         linkNode.appendChild( imageNode );
655                 }
656
657                 captionNode = editor.dom.getParent( imageNode, '.mceTemp' );
658
659                 if ( imageNode.parentNode && imageNode.parentNode.nodeName === 'A' && ! hasTextContent( imageNode.parentNode ) ) {
660                         node = imageNode.parentNode;
661                 } else {
662                         node = imageNode;
663                 }
664
665                 if ( imageData.caption ) {
666                         imageData.caption = verifyHTML( imageData.caption );
667
668                         id = imageData.attachment_id ? 'attachment_' + imageData.attachment_id : null;
669                         align = 'align' + ( imageData.align || 'none' );
670                         className = 'wp-caption ' + align;
671
672                         if ( imageData.captionClassName ) {
673                                 className += ' ' + imageData.captionClassName.replace( /[<>&]+/g,  '' );
674                         }
675
676                         if ( ! editor.getParam( 'wpeditimage_html5_captions' ) ) {
677                                 width = parseInt( width, 10 );
678                                 width += 10;
679                         }
680
681                         if ( captionNode ) {
682                                 dl = dom.select( 'dl.wp-caption', captionNode );
683
684                                 if ( dl.length ) {
685                                         dom.setAttribs( dl, {
686                                                 id: id,
687                                                 'class': className,
688                                                 style: 'width: ' + width + 'px'
689                                         } );
690                                 }
691
692                                 dd = dom.select( '.wp-caption-dd', captionNode );
693
694                                 if ( dd.length ) {
695                                         dom.setHTML( dd[0], imageData.caption );
696                                 }
697
698                         } else {
699                                 id = id ? 'id="'+ id +'" ' : '';
700
701                                 // should create a new function for generating the caption markup
702                                 html =  '<dl ' + id + 'class="' + className +'" style="width: '+ width +'px">' +
703                                         '<dt class="wp-caption-dt"></dt><dd class="wp-caption-dd">'+ imageData.caption +'</dd></dl>';
704
705                                 wrap = dom.create( 'div', { 'class': 'mceTemp' }, html );
706
707                                 if ( parent = dom.getParent( node, 'p' ) ) {
708                                         parent.parentNode.insertBefore( wrap, parent );
709
710                                         if ( dom.isEmpty( parent ) ) {
711                                                 dom.remove( parent );
712                                         }
713                                 } else {
714                                         node.parentNode.insertBefore( wrap, node );
715                                 }
716
717                                 editor.$( wrap ).find( 'dt.wp-caption-dt' ).append( node );
718                         }
719                 } else if ( captionNode ) {
720                         // Remove the caption wrapper and place the image in new paragraph
721                         parent = dom.create( 'p' );
722                         captionNode.parentNode.insertBefore( parent, captionNode );
723                         parent.appendChild( node );
724                         dom.remove( captionNode );
725                 }
726
727                 if ( wp.media.events ) {
728                         wp.media.events.trigger( 'editor:image-update', {
729                                 editor: editor,
730                                 metadata: imageData,
731                                 image: imageNode
732                         } );
733                 }
734
735                 editor.nodeChanged();
736         }
737
738         function editImage( img ) {
739                 var frame, callback, metadata;
740
741                 if ( typeof wp === 'undefined' || ! wp.media ) {
742                         editor.execCommand( 'mceImage' );
743                         return;
744                 }
745
746                 metadata = extractImageData( img );
747
748                 // Manipulate the metadata by reference that is fed into the PostImage model used in the media modal
749                 wp.media.events.trigger( 'editor:image-edit', {
750                         editor: editor,
751                         metadata: metadata,
752                         image: img
753                 } );
754
755                 frame = wp.media({
756                         frame: 'image',
757                         state: 'image-details',
758                         metadata: metadata
759                 } );
760
761                 wp.media.events.trigger( 'editor:frame-create', { frame: frame } );
762
763                 callback = function( imageData ) {
764                         editor.focus();
765                         editor.undoManager.transact( function() {
766                                 updateImage( img, imageData );
767                         } );
768                         frame.detach();
769                 };
770
771                 frame.state('image-details').on( 'update', callback );
772                 frame.state('replace-image').on( 'replace', callback );
773                 frame.on( 'close', function() {
774                         editor.focus();
775                         frame.detach();
776                 });
777
778                 frame.open();
779         }
780
781         function removeImage( node ) {
782                 var wrap;
783
784                 if ( node.nodeName === 'DIV' && editor.dom.hasClass( node, 'mceTemp' ) ) {
785                         wrap = node;
786                 } else if ( node.nodeName === 'IMG' || node.nodeName === 'DT' || node.nodeName === 'A' ) {
787                         wrap = editor.dom.getParent( node, 'div.mceTemp' );
788                 }
789
790                 if ( wrap ) {
791                         if ( wrap.nextSibling ) {
792                                 editor.selection.select( wrap.nextSibling );
793                         } else if ( wrap.previousSibling ) {
794                                 editor.selection.select( wrap.previousSibling );
795                         } else {
796                                 editor.selection.select( wrap.parentNode );
797                         }
798
799                         editor.selection.collapse( true );
800                         editor.dom.remove( wrap );
801                 } else {
802                         editor.dom.remove( node );
803                 }
804
805                 editor.nodeChanged();
806                 editor.undoManager.add();
807         }
808
809         editor.on( 'init', function() {
810                 var dom = editor.dom,
811                         captionClass = editor.getParam( 'wpeditimage_html5_captions' ) ? 'html5-captions' : 'html4-captions';
812
813                 dom.addClass( editor.getBody(), captionClass );
814
815                 // Add caption field to the default image dialog
816                 editor.on( 'wpLoadImageForm', function( event ) {
817                         if ( editor.getParam( 'wpeditimage_disable_captions' ) ) {
818                                 return;
819                         }
820
821                         var captionField = {
822                                 type: 'textbox',
823                                 flex: 1,
824                                 name: 'caption',
825                                 minHeight: 60,
826                                 multiline: true,
827                                 scroll: true,
828                                 label: 'Image caption'
829                         };
830
831                         event.data.splice( event.data.length - 1, 0, captionField );
832                 });
833
834                 // Fix caption parent width for images added from URL
835                 editor.on( 'wpNewImageRefresh', function( event ) {
836                         var parent, captionWidth;
837
838                         if ( parent = dom.getParent( event.node, 'dl.wp-caption' ) ) {
839                                 if ( ! parent.style.width ) {
840                                         captionWidth = parseInt( event.node.clientWidth, 10 ) + 10;
841                                         captionWidth = captionWidth ? captionWidth + 'px' : '50%';
842                                         dom.setStyle( parent, 'width', captionWidth );
843                                 }
844                         }
845                 });
846
847                 editor.on( 'wpImageFormSubmit', function( event ) {
848                         var data = event.imgData.data,
849                                 imgNode = event.imgData.node,
850                                 caption = event.imgData.caption,
851                                 captionId = '',
852                                 captionAlign = '',
853                                 captionWidth = '',
854                                 wrap, parent, node, html, imgId;
855
856                         // Temp image id so we can find the node later
857                         data.id = '__wp-temp-img-id';
858                         // Cancel the original callback
859                         event.imgData.cancel = true;
860
861                         if ( ! data.style ) {
862                                 data.style = null;
863                         }
864
865                         if ( ! data.src ) {
866                                 // Delete the image and the caption
867                                 if ( imgNode ) {
868                                         if ( wrap = dom.getParent( imgNode, 'div.mceTemp' ) ) {
869                                                 dom.remove( wrap );
870                                         } else if ( imgNode.parentNode.nodeName === 'A' ) {
871                                                 dom.remove( imgNode.parentNode );
872                                         } else {
873                                                 dom.remove( imgNode );
874                                         }
875
876                                         editor.nodeChanged();
877                                 }
878                                 return;
879                         }
880
881                         if ( caption ) {
882                                 caption = caption.replace( /\r\n|\r/g, '\n' ).replace( /<\/?[a-zA-Z0-9]+( [^<>]+)?>/g, function( a ) {
883                                         // No line breaks inside HTML tags
884                                         return a.replace( /[\r\n\t]+/, ' ' );
885                                 });
886
887                                 // Convert remaining line breaks to <br>
888                                 caption = caption.replace( /(<br[^>]*>)\s*\n\s*/g, '$1' ).replace( /\s*\n\s*/g, '<br />' );
889                                 caption = verifyHTML( caption );
890                         }
891
892                         if ( ! imgNode ) {
893                                 // New image inserted
894                                 html = dom.createHTML( 'img', data );
895
896                                 if ( caption ) {
897                                         node = editor.selection.getNode();
898
899                                         if ( data.width ) {
900                                                 captionWidth = parseInt( data.width, 10 );
901
902                                                 if ( ! editor.getParam( 'wpeditimage_html5_captions' ) ) {
903                                                         captionWidth += 10;
904                                                 }
905
906                                                 captionWidth = ' style="width: ' + captionWidth + 'px"';
907                                         }
908
909                                         html = '<dl class="wp-caption alignnone"' + captionWidth + '>' +
910                                                 '<dt class="wp-caption-dt">'+ html +'</dt><dd class="wp-caption-dd">'+ caption +'</dd></dl>';
911
912                                         if ( node.nodeName === 'P' ) {
913                                                 parent = node;
914                                         } else {
915                                                 parent = dom.getParent( node, 'p' );
916                                         }
917
918                                         if ( parent && parent.nodeName === 'P' ) {
919                                                 wrap = dom.create( 'div', { 'class': 'mceTemp' }, html );
920                                                 parent.parentNode.insertBefore( wrap, parent );
921                                                 editor.selection.select( wrap );
922                                                 editor.nodeChanged();
923
924                                                 if ( dom.isEmpty( parent ) ) {
925                                                         dom.remove( parent );
926                                                 }
927                                         } else {
928                                                 editor.selection.setContent( '<div class="mceTemp">' + html + '</div>' );
929                                         }
930                                 } else {
931                                         editor.selection.setContent( html );
932                                 }
933                         } else {
934                                 // Edit existing image
935
936                                 // Store the original image id if any
937                                 imgId = imgNode.id || null;
938                                 // Update the image node
939                                 dom.setAttribs( imgNode, data );
940                                 wrap = dom.getParent( imgNode, 'dl.wp-caption' );
941
942                                 if ( caption ) {
943                                         if ( wrap ) {
944                                                 if ( parent = dom.select( 'dd.wp-caption-dd', wrap )[0] ) {
945                                                         parent.innerHTML = caption;
946                                                 }
947                                         } else {
948                                                 if ( imgNode.className ) {
949                                                         captionId = imgNode.className.match( /wp-image-([0-9]+)/ );
950                                                         captionAlign = imgNode.className.match( /align(left|right|center|none)/ );
951                                                 }
952
953                                                 if ( captionAlign ) {
954                                                         captionAlign = captionAlign[0];
955                                                         imgNode.className = imgNode.className.replace( /align(left|right|center|none)/g, '' );
956                                                 } else {
957                                                         captionAlign = 'alignnone';
958                                                 }
959
960                                                 captionAlign = ' class="wp-caption ' + captionAlign + '"';
961
962                                                 if ( captionId ) {
963                                                         captionId = ' id="attachment_' + captionId[1] + '"';
964                                                 }
965
966                                                 captionWidth = data.width || imgNode.clientWidth;
967
968                                                 if ( captionWidth ) {
969                                                         captionWidth = parseInt( captionWidth, 10 );
970
971                                                         if ( ! editor.getParam( 'wpeditimage_html5_captions' ) ) {
972                                                                 captionWidth += 10;
973                                                         }
974
975                                                         captionWidth = ' style="width: '+ captionWidth +'px"';
976                                                 }
977
978                                                 if ( imgNode.parentNode && imgNode.parentNode.nodeName === 'A' ) {
979                                                         node = imgNode.parentNode;
980                                                 } else {
981                                                         node = imgNode;
982                                                 }
983
984                                                 html = '<dl ' + captionId + captionAlign + captionWidth + '>' +
985                                                         '<dt class="wp-caption-dt"></dt><dd class="wp-caption-dd">'+ caption +'</dd></dl>';
986
987                                                 wrap = dom.create( 'div', { 'class': 'mceTemp' }, html );
988
989                                                 if ( parent = dom.getParent( node, 'p' ) ) {
990                                                         parent.parentNode.insertBefore( wrap, parent );
991
992                                                         if ( dom.isEmpty( parent ) ) {
993                                                                 dom.remove( parent );
994                                                         }
995                                                 } else {
996                                                         node.parentNode.insertBefore( wrap, node );
997                                                 }
998
999                                                 editor.$( wrap ).find( 'dt.wp-caption-dt' ).append( node );
1000                                         }
1001                                 } else {
1002                                         if ( wrap ) {
1003                                                 // Remove the caption wrapper and place the image in new paragraph
1004                                                 if ( imgNode.parentNode.nodeName === 'A' ) {
1005                                                         html = dom.getOuterHTML( imgNode.parentNode );
1006                                                 } else {
1007                                                         html = dom.getOuterHTML( imgNode );
1008                                                 }
1009
1010                                                 parent = dom.create( 'p', {}, html );
1011                                                 dom.insertAfter( parent, wrap.parentNode );
1012                                                 editor.selection.select( parent );
1013                                                 editor.nodeChanged();
1014                                                 dom.remove( wrap.parentNode );
1015                                         }
1016                                 }
1017                         }
1018
1019                         imgNode = dom.get('__wp-temp-img-id');
1020                         dom.setAttrib( imgNode, 'id', imgId );
1021                         event.imgData.node = imgNode;
1022                 });
1023
1024                 editor.on( 'wpLoadImageData', function( event ) {
1025                         var parent,
1026                                 data = event.imgData.data,
1027                                 imgNode = event.imgData.node;
1028
1029                         if ( parent = dom.getParent( imgNode, 'dl.wp-caption' ) ) {
1030                                 parent = dom.select( 'dd.wp-caption-dd', parent )[0];
1031
1032                                 if ( parent ) {
1033                                         data.caption = editor.serializer.serialize( parent )
1034                                                 .replace( /<br[^>]*>/g, '$&\n' ).replace( /^<p>/, '' ).replace( /<\/p>$/, '' );
1035                                 }
1036                         }
1037                 });
1038
1039                 dom.bind( editor.getDoc(), 'dragstart', function( event ) {
1040                         var node = editor.selection.getNode();
1041
1042                         // Prevent dragging images out of the caption elements
1043                         if ( node.nodeName === 'IMG' && dom.getParent( node, '.wp-caption' ) ) {
1044                                 event.preventDefault();
1045                         }
1046                 });
1047
1048                 // Prevent IE11 from making dl.wp-caption resizable
1049                 if ( tinymce.Env.ie && tinymce.Env.ie > 10 ) {
1050                         // The 'mscontrolselect' event is supported only in IE11+
1051                         dom.bind( editor.getBody(), 'mscontrolselect', function( event ) {
1052                                 if ( event.target.nodeName === 'IMG' && dom.getParent( event.target, '.wp-caption' ) ) {
1053                                         // Hide the thick border with resize handles around dl.wp-caption
1054                                         editor.getBody().focus(); // :(
1055                                 } else if ( event.target.nodeName === 'DL' && dom.hasClass( event.target, 'wp-caption' ) ) {
1056                                         // Trigger the thick border with resize handles...
1057                                         // This will make the caption text editable.
1058                                         event.target.focus();
1059                                 }
1060                         });
1061                 }
1062         });
1063
1064         editor.on( 'ObjectResized', function( event ) {
1065                 var node = event.target;
1066
1067                 if ( node.nodeName === 'IMG' ) {
1068                         editor.undoManager.transact( function() {
1069                                 var parent, width,
1070                                         dom = editor.dom;
1071
1072                                 node.className = node.className.replace( /\bsize-[^ ]+/, '' );
1073
1074                                 if ( parent = dom.getParent( node, '.wp-caption' ) ) {
1075                                         width = event.width || dom.getAttrib( node, 'width' );
1076
1077                                         if ( width ) {
1078                                                 width = parseInt( width, 10 );
1079
1080                                                 if ( ! editor.getParam( 'wpeditimage_html5_captions' ) ) {
1081                                                         width += 10;
1082                                                 }
1083
1084                                                 dom.setStyle( parent, 'width', width + 'px' );
1085                                         }
1086                                 }
1087                         });
1088                 }
1089     });
1090
1091         editor.on( 'BeforeExecCommand', function( event ) {
1092                 var node, p, DL, align, replacement,
1093                         cmd = event.command,
1094                         dom = editor.dom;
1095
1096                 if ( cmd === 'mceInsertContent' ) {
1097                         // When inserting content, if the caret is inside a caption create new paragraph under
1098                         // and move the caret there
1099                         if ( node = dom.getParent( editor.selection.getNode(), 'div.mceTemp' ) ) {
1100                                 p = dom.create( 'p' );
1101                                 dom.insertAfter( p, node );
1102                                 editor.selection.setCursorLocation( p, 0 );
1103                                 editor.nodeChanged();
1104                         }
1105                 } else if ( cmd === 'JustifyLeft' || cmd === 'JustifyRight' || cmd === 'JustifyCenter' || cmd === 'wpAlignNone' ) {
1106                         node = editor.selection.getNode();
1107                         align = 'align' + cmd.slice( 7 ).toLowerCase();
1108                         DL = editor.dom.getParent( node, '.wp-caption' );
1109
1110                         if ( node.nodeName !== 'IMG' && ! DL ) {
1111                                 return;
1112                         }
1113
1114                         node = DL || node;
1115
1116                         if ( editor.dom.hasClass( node, align ) ) {
1117                                 replacement = ' alignnone';
1118                         } else {
1119                                 replacement = ' ' + align;
1120                         }
1121
1122                         node.className = node.className.replace( / ?align(left|center|right|none)/g, '' ) + replacement;
1123
1124                         editor.nodeChanged();
1125                         event.preventDefault();
1126
1127                         if ( floatingToolbar ) {
1128                                 floatingToolbar.reposition();
1129                         }
1130
1131                         editor.fire( 'ExecCommand', {
1132                                 command: cmd,
1133                                 ui: event.ui,
1134                                 value: event.value
1135                         } );
1136                 }
1137         });
1138
1139         editor.on( 'keydown', function( event ) {
1140                 var node, wrap, P, spacer,
1141                         selection = editor.selection,
1142                         keyCode = event.keyCode,
1143                         dom = editor.dom,
1144                         VK = tinymce.util.VK;
1145
1146                 if ( keyCode === VK.ENTER ) {
1147                         // When pressing Enter inside a caption move the caret to a new parapraph under it
1148                         node = selection.getNode();
1149                         wrap = dom.getParent( node, 'div.mceTemp' );
1150
1151                         if ( wrap ) {
1152                                 dom.events.cancel( event ); // Doesn't cancel all :(
1153
1154                                 // Remove any extra dt and dd cleated on pressing Enter...
1155                                 tinymce.each( dom.select( 'dt, dd', wrap ), function( element ) {
1156                                         if ( dom.isEmpty( element ) ) {
1157                                                 dom.remove( element );
1158                                         }
1159                                 });
1160
1161                                 spacer = tinymce.Env.ie && tinymce.Env.ie < 11 ? '' : '<br data-mce-bogus="1" />';
1162                                 P = dom.create( 'p', null, spacer );
1163
1164                                 if ( node.nodeName === 'DD' ) {
1165                                         dom.insertAfter( P, wrap );
1166                                 } else {
1167                                         wrap.parentNode.insertBefore( P, wrap );
1168                                 }
1169
1170                                 editor.nodeChanged();
1171                                 selection.setCursorLocation( P, 0 );
1172                         }
1173                 } else if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) {
1174                         node = selection.getNode();
1175
1176                         if ( node.nodeName === 'DIV' && dom.hasClass( node, 'mceTemp' ) ) {
1177                                 wrap = node;
1178                         } else if ( node.nodeName === 'IMG' || node.nodeName === 'DT' || node.nodeName === 'A' ) {
1179                                 wrap = dom.getParent( node, 'div.mceTemp' );
1180                         }
1181
1182                         if ( wrap ) {
1183                                 dom.events.cancel( event );
1184                                 removeImage( node );
1185                                 return false;
1186                         }
1187                 }
1188         });
1189
1190         // After undo/redo FF seems to set the image height very slowly when it is set to 'auto' in the CSS.
1191         // This causes image.getBoundingClientRect() to return wrong values and the resize handles are shown in wrong places.
1192         // Collapse the selection to remove the resize handles.
1193         if ( tinymce.Env.gecko ) {
1194                 editor.on( 'undo redo', function() {
1195                         if ( editor.selection.getNode().nodeName === 'IMG' ) {
1196                                 editor.selection.collapse();
1197                         }
1198                 });
1199         }
1200
1201         editor.wpSetImgCaption = function( content ) {
1202                 return parseShortcode( content );
1203         };
1204
1205         editor.wpGetImgCaption = function( content ) {
1206                 return getShortcode( content );
1207         };
1208
1209         editor.on( 'BeforeSetContent', function( event ) {
1210                 if ( event.format !== 'raw' ) {
1211                         event.content = editor.wpSetImgCaption( event.content );
1212                 }
1213         });
1214
1215         editor.on( 'PostProcess', function( event ) {
1216                 if ( event.get ) {
1217                         event.content = editor.wpGetImgCaption( event.content );
1218                 }
1219         });
1220
1221         return {
1222                 _do_shcode: parseShortcode,
1223                 _get_shcode: getShortcode
1224         };
1225 });