linkimageediting.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module link/linkimageediting
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
  10. import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher';
  11. import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
  12. import LinkEditing from './linkediting';
  13. import linkIcon from '../theme/icons/link.svg';
  14. /**
  15. * The link image engine feature.
  16. *
  17. * It accepts the `linkHref="url"` attribute in the model for the {@link module:image/image~Image `<image>`} element
  18. * which allows linking images.
  19. *
  20. * @extends module:core/plugin~Plugin
  21. */
  22. export default class LinkImageEditing extends Plugin {
  23. /**
  24. * @inheritDoc
  25. */
  26. static get requires() {
  27. return [ ImageEditing, LinkEditing ];
  28. }
  29. /**
  30. * @inheritDoc
  31. */
  32. static get pluginName() {
  33. return 'LinkImageEditing';
  34. }
  35. init() {
  36. const editor = this.editor;
  37. editor.model.schema.extend( 'image', { allowAttributes: [ 'linkHref' ] } );
  38. editor.conversion.for( 'upcast' ).add( upcastLink() );
  39. editor.conversion.for( 'editingDowncast' ).add( downcastImageLink( { attachIconIndicator: true } ) );
  40. editor.conversion.for( 'dataDowncast' ).add( downcastImageLink( { attachIconIndicator: false } ) );
  41. // Definitions for decorators are provided by the `link` command and the `LinkEditing` plugin.
  42. this._enableAutomaticDecorators();
  43. this._enableManualDecorators();
  44. }
  45. /**
  46. * Processes {@link module:link/link~LinkDecoratorAutomaticDefinition automatic decorators} definitions and
  47. * attaches proper converters that will work when linking an image.`
  48. *
  49. * @private
  50. */
  51. _enableAutomaticDecorators() {
  52. const editor = this.editor;
  53. const command = editor.commands.get( 'link' );
  54. const automaticDecorators = command.automaticDecorators;
  55. if ( automaticDecorators.length ) {
  56. editor.conversion.for( 'downcast' ).add( automaticDecorators.getDispatcherForLinkedImage() );
  57. }
  58. }
  59. /**
  60. * Processes transformed {@link module:link/utils~ManualDecorator} instances and attaches proper converters
  61. * that will work when linking an image.
  62. *
  63. * @private
  64. */
  65. _enableManualDecorators() {
  66. const editor = this.editor;
  67. const command = editor.commands.get( 'link' );
  68. const manualDecorators = command.manualDecorators;
  69. for ( const decorator of command.manualDecorators ) {
  70. editor.model.schema.extend( 'image', { allowAttributes: decorator.id } );
  71. editor.conversion.for( 'downcast' ).add( downcastImageLinkManualDecorator( manualDecorators, decorator ) );
  72. editor.conversion.for( 'upcast' ).add( upcastImageLinkManualDecorator( manualDecorators, decorator ) );
  73. }
  74. }
  75. }
  76. // Returns a converter that consumes the 'href' attribute if a link contains an image.
  77. //
  78. // @private
  79. // @returns {Function}
  80. function upcastLink() {
  81. return dispatcher => {
  82. dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
  83. const viewLink = data.viewItem;
  84. const imageInLink = Array.from( viewLink.getChildren() ).find( child => child.name === 'img' );
  85. if ( !imageInLink ) {
  86. return;
  87. }
  88. // There's an image inside an <a> element - we consume it so it won't be picked up by the Link plugin.
  89. const consumableAttributes = { attributes: [ 'href' ] };
  90. // Consume the `href` attribute so the default one will not convert it to $text attribute.
  91. if ( !conversionApi.consumable.consume( viewLink, consumableAttributes ) ) {
  92. // Might be consumed by something else - i.e. other converter with priority=highest - a standard check.
  93. return;
  94. }
  95. const linkHref = viewLink.getAttribute( 'href' );
  96. // Missing the 'href' attribute.
  97. if ( !linkHref ) {
  98. return;
  99. }
  100. // A full definition of the image feature.
  101. // figure > a > img: parent of the link element is an image element.
  102. let modelElement = data.modelCursor.parent;
  103. if ( !modelElement.is( 'image' ) ) {
  104. // a > img: parent of the link is not the image element. We need to convert it manually.
  105. const conversionResult = conversionApi.convertItem( imageInLink, data.modelCursor );
  106. // Set image range as conversion result.
  107. data.modelRange = conversionResult.modelRange;
  108. // Continue conversion where image conversion ends.
  109. data.modelCursor = conversionResult.modelCursor;
  110. modelElement = data.modelCursor.nodeBefore;
  111. }
  112. if ( modelElement && modelElement.is( 'image' ) ) {
  113. // Set the linkHref attribute from link element on model image element.
  114. conversionApi.writer.setAttribute( 'linkHref', linkHref, modelElement );
  115. }
  116. }, { priority: 'high' } );
  117. // Using the same priority that `upcastImageLinkManualDecorator()` converter guarantees
  118. // that manual decorators will decorate the proper element.
  119. };
  120. }
  121. // Return a converter that adds the `<a>` element to data.
  122. //
  123. // @private
  124. // @params {Object} options
  125. // @params {Boolean} options.attachIconIndicator=false If set to `true`, an icon that informs about the linked image will be added.
  126. // @returns {Function}
  127. function downcastImageLink( options ) {
  128. return dispatcher => {
  129. dispatcher.on( 'attribute:linkHref:image', ( evt, data, conversionApi ) => {
  130. // The image will be already converted - so it will be present in the view.
  131. const viewFigure = conversionApi.mapper.toViewElement( data.item );
  132. const writer = conversionApi.writer;
  133. // But we need to check whether the link element exists.
  134. const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' );
  135. let linkIconIndicator;
  136. if ( options.attachIconIndicator ) {
  137. // Create an icon indicator for a linked image.
  138. linkIconIndicator = writer.createUIElement( 'span', { class: 'ck ck-link-image_icon' }, function( domDocument ) {
  139. const domElement = this.toDomElement( domDocument );
  140. domElement.innerHTML = linkIcon;
  141. return domElement;
  142. } );
  143. }
  144. // If so, update the attribute if it's defined or remove the entire link if the attribute is empty.
  145. if ( linkInImage ) {
  146. if ( data.attributeNewValue ) {
  147. writer.setAttribute( 'href', data.attributeNewValue, linkInImage );
  148. } else {
  149. const viewImage = Array.from( linkInImage.getChildren() ).find( child => child.name === 'img' );
  150. writer.move( writer.createRangeOn( viewImage ), writer.createPositionAt( viewFigure, 0 ) );
  151. writer.remove( linkInImage );
  152. }
  153. } else {
  154. // But if it does not exist. Let's wrap already converted image by newly created link element.
  155. // 1. Create an empty link element.
  156. const linkElement = writer.createContainerElement( 'a', { href: data.attributeNewValue } );
  157. // 2. Insert link inside the associated image.
  158. writer.insert( writer.createPositionAt( viewFigure, 0 ), linkElement );
  159. // 3. Move the image to the link.
  160. writer.move( writer.createRangeOn( viewFigure.getChild( 1 ) ), writer.createPositionAt( linkElement, 0 ) );
  161. // 4. Inset the linked image icon indicator while downcast to editing.
  162. if ( linkIconIndicator ) {
  163. writer.insert( writer.createPositionAt( linkElement, 'end' ), linkIconIndicator );
  164. }
  165. }
  166. } );
  167. };
  168. }
  169. // Returns a converter that decorates the `<a>` element when the image is the link label.
  170. //
  171. // @private
  172. // @returns {Function}
  173. function downcastImageLinkManualDecorator( manualDecorators, decorator ) {
  174. return dispatcher => {
  175. dispatcher.on( `attribute:${ decorator.id }:image`, ( evt, data, conversionApi ) => {
  176. const attributes = manualDecorators.get( decorator.id ).attributes;
  177. const viewFigure = conversionApi.mapper.toViewElement( data.item );
  178. const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' );
  179. for ( const [ key, val ] of toMap( attributes ) ) {
  180. conversionApi.writer.setAttribute( key, val, linkInImage );
  181. }
  182. } );
  183. };
  184. }
  185. // Returns a converter that checks whether manual decorators should be applied to the link.
  186. //
  187. // @private
  188. // @returns {Function}
  189. function upcastImageLinkManualDecorator( manualDecorators, decorator ) {
  190. return dispatcher => {
  191. dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
  192. const viewLink = data.viewItem;
  193. const consumableAttributes = {
  194. attributes: manualDecorators.get( decorator.id ).attributes
  195. };
  196. const matcher = new Matcher( consumableAttributes );
  197. const result = matcher.match( viewLink );
  198. // The link element does not have required attributes or/and proper values.
  199. if ( !result ) {
  200. return;
  201. }
  202. // Check whether we can consume those attributes.
  203. if ( !conversionApi.consumable.consume( viewLink, result.match ) ) {
  204. return;
  205. }
  206. // At this stage we can assume that we have the `<image>` element.
  207. const modelElement = data.modelCursor.parent;
  208. conversionApi.writer.setAttribute( decorator.id, true, modelElement );
  209. }, { priority: 'high' } );
  210. // Using the same priority that `upcastLink()` converter guarantees that the linked image was properly converted.
  211. };
  212. }