8
0

imagecaptionengine.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module image/imagecaption/imagecaptionengine
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import ModelTreeWalker from '@ckeditor/ckeditor5-engine/src/model/treewalker';
  10. import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
  11. import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
  12. import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
  13. import viewWriter from '@ckeditor/ckeditor5-engine/src/view/writer';
  14. import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
  15. import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
  16. import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
  17. import { isImage, isImageWidget } from '../image/utils';
  18. import {
  19. captionElementCreator,
  20. isCaption,
  21. getCaptionFromImage,
  22. matchImageCaption
  23. } from './utils';
  24. /**
  25. * The image caption engine plugin.
  26. *
  27. * Registers proper converters. Takes care of adding caption element if image without it is inserted to model document.
  28. *
  29. * @extends module:core/plugin~Plugin
  30. */
  31. export default class ImageCaptionEngine extends Plugin {
  32. /**
  33. * @inheritDoc
  34. */
  35. init() {
  36. const editor = this.editor;
  37. const document = editor.document;
  38. const viewDocument = editor.editing.view;
  39. const schema = document.schema;
  40. const data = editor.data;
  41. const editing = editor.editing;
  42. /**
  43. * Last selected caption editable.
  44. * It is used for hiding editable when is empty and image widget is no longer selected.
  45. *
  46. * @private
  47. * @member {module:engine/view/editableelement~EditableElement} #_lastSelectedCaption
  48. */
  49. /**
  50. * Function used to create editable caption element in the editing view.
  51. *
  52. * @private
  53. * @member {Function}
  54. */
  55. this._createCaption = captionElementCreator( viewDocument );
  56. // Schema configuration.
  57. schema.registerItem( 'caption' );
  58. schema.allow( { name: '$inline', inside: 'caption' } );
  59. schema.allow( { name: 'caption', inside: 'image' } );
  60. schema.limits.add( 'caption' );
  61. // Add caption element to each image inserted without it.
  62. document.on( 'change', insertMissingModelCaptionElement );
  63. // View to model converter for the data pipeline.
  64. buildViewConverter()
  65. .for( data.viewToModel )
  66. .from( matchImageCaption )
  67. .toElement( 'caption' );
  68. // Model to view converter for the data pipeline.
  69. data.modelToView.on( 'insert:caption', captionModelToView( new ViewContainerElement( 'figcaption' ), false ) );
  70. // Model to view converter for the editing pipeline.
  71. editing.modelToView.on( 'insert:caption', captionModelToView( this._createCaption ) );
  72. // Update view before each rendering.
  73. this.listenTo( viewDocument, 'render', () => this._updateCaptionVisibility(), { priority: 'high' } );
  74. }
  75. /**
  76. * Updates view before each rendering, making sure that empty captions (so unnecessary ones) are hidden
  77. * and then visible when the image is selected.
  78. *
  79. * @private
  80. */
  81. _updateCaptionVisibility() {
  82. const mapper = this.editor.editing.mapper;
  83. const viewSelection = this.editor.editing.view.selection;
  84. const selectedElement = viewSelection.getSelectedElement();
  85. let viewCaption;
  86. // Hide last selected caption if have no child elements.
  87. if ( this._lastSelectedCaption && !this._lastSelectedCaption.childCount ) {
  88. this._lastSelectedCaption.addClass( 'ck-hidden' );
  89. }
  90. // If whole image widget is selected.
  91. if ( selectedElement && isImageWidget( selectedElement ) ) {
  92. const modelImage = mapper.toModelElement( selectedElement );
  93. const modelCaption = getCaptionFromImage( modelImage );
  94. viewCaption = mapper.toViewElement( modelCaption );
  95. }
  96. // If selection is placed inside caption.
  97. if ( isCaption( viewSelection.editableElement ) ) {
  98. viewCaption = viewSelection.editableElement;
  99. }
  100. if ( viewCaption ) {
  101. viewCaption.removeClass( 'ck-hidden' );
  102. this._lastSelectedCaption = viewCaption;
  103. }
  104. }
  105. }
  106. // Checks whether data inserted to the model document have image element that has no caption element inside it.
  107. // If there is none - adds it to the image element.
  108. //
  109. // @private
  110. function insertMissingModelCaptionElement( evt, changeType, data, batch ) {
  111. if ( changeType !== 'insert' ) {
  112. return;
  113. }
  114. const walker = new ModelTreeWalker( {
  115. boundaries: data.range,
  116. ignoreElementEnd: true
  117. } );
  118. for ( let value of walker ) {
  119. const item = value.item;
  120. if ( value.type == 'elementStart' && isImage( item ) && !getCaptionFromImage( item ) ) {
  121. batch.document.enqueueChanges( () => {
  122. batch.insert( ModelPosition.createAt( item, 'end' ), new ModelElement( 'caption' ) );
  123. } );
  124. }
  125. }
  126. }
  127. // Creates a converter that converts image caption model element to view element.
  128. //
  129. // @private
  130. // @param {Function|module:engine/view/element~Element} elementCreator
  131. // @param {Boolean} [hide=true] When set to `false` view element will not be inserted when it's empty.
  132. // @return {Function}
  133. function captionModelToView( elementCreator, hide = true ) {
  134. return ( evt, data, consumable, conversionApi ) => {
  135. const captionElement = data.item;
  136. // Return if element shouldn't be present when empty.
  137. if ( !captionElement.childCount && !hide ) {
  138. return;
  139. }
  140. if ( isImage( captionElement.parent ) ) {
  141. if ( !consumable.consume( data.item, 'insert' ) ) {
  142. return;
  143. }
  144. const viewImage = conversionApi.mapper.toViewElement( data.range.start.parent );
  145. const viewCaption = ( elementCreator instanceof ViewElement ) ?
  146. elementCreator.clone( true ) :
  147. elementCreator();
  148. // Hide if empty.
  149. if ( !captionElement.childCount ) {
  150. viewCaption.addClass( 'ck-hidden' );
  151. }
  152. insertViewCaptionAndBind( viewCaption, data.item, viewImage, conversionApi.mapper );
  153. }
  154. };
  155. }
  156. // Inserts `viewCaption` at the end of `viewImage` and binds it to `modelCaption`.
  157. //
  158. // @private
  159. // @param {module:engine/view/containerelement~ContainerElement} viewCaption
  160. // @param {module:engine/model/element~Element} modelCaption
  161. // @param {module:engine/view/containerelement~ContainerElement} viewImage
  162. // @param {module:engine/conversion/mapper~Mapper} mapper
  163. function insertViewCaptionAndBind( viewCaption, modelCaption, viewImage, mapper ) {
  164. const viewPosition = ViewPosition.createAt( viewImage, 'end' );
  165. viewWriter.insert( viewPosition, viewCaption );
  166. mapper.bindElements( modelCaption, viewCaption );
  167. }