8
0

linkimageediting.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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 LinkEditing from './linkediting';
  11. import linkIcon from '../theme/icons/link.svg';
  12. /**
  13. * The link image engine feature.
  14. *
  15. * It accepts the `linkHref="url"` attribute in the model for the {@link module:image/image~Image `<image>`} element
  16. * which allows linking images.
  17. *
  18. * @extends module:core/plugin~Plugin
  19. */
  20. export default class LinkImageEditing extends Plugin {
  21. /**
  22. * @inheritDoc
  23. */
  24. static get requires() {
  25. return [ ImageEditing, LinkEditing ];
  26. }
  27. /**
  28. * @inheritDoc
  29. */
  30. static get pluginName() {
  31. return 'LinkImageEditing';
  32. }
  33. init() {
  34. const editor = this.editor;
  35. editor.model.schema.extend( 'image', { allowAttributes: [ 'linkHref' ] } );
  36. editor.conversion.for( 'upcast' ).add( upcastLink() );
  37. editor.conversion.for( 'downcast' ).add( downcastImageLink() );
  38. }
  39. }
  40. // Returns a converter that consumes the 'href' attribute if a link contains an image.
  41. //
  42. // @private
  43. // @returns {Function}
  44. //
  45. function upcastLink() {
  46. return dispatcher => {
  47. dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
  48. const viewLink = data.viewItem;
  49. const imageInLink = Array.from( viewLink.getChildren() ).find( child => child.name === 'img' );
  50. if ( !imageInLink ) {
  51. return;
  52. }
  53. // There's an image inside an <a> element - we consume it so it won't be picked up by the Link plugin.
  54. const consumableAttributes = { attributes: [ 'href' ] };
  55. // Consume the `href` attribute so the default one will not convert it to $text attribute.
  56. if ( !conversionApi.consumable.consume( viewLink, consumableAttributes ) ) {
  57. // Might be consumed by something else - i.e. other converter with priority=highest - a standard check.
  58. return;
  59. }
  60. const linkHref = viewLink.getAttribute( 'href' );
  61. // Missing the 'href' attribute.
  62. if ( !linkHref ) {
  63. return;
  64. }
  65. // A full definition of the image feature.
  66. // figure > a > img: parent of the link element is an image element.
  67. let modelElement = data.modelCursor.parent;
  68. if ( !modelElement.is( 'image' ) ) {
  69. // a > img: parent of the link is not the image element. We need to convert it manually.
  70. const conversionResult = conversionApi.convertItem( imageInLink, data.modelCursor );
  71. // Set image range as conversion result.
  72. data.modelRange = conversionResult.modelRange;
  73. // Continue conversion where image conversion ends.
  74. data.modelCursor = conversionResult.modelCursor;
  75. modelElement = data.modelCursor.nodeBefore;
  76. }
  77. if ( modelElement && modelElement.is( 'image' ) ) {
  78. // Set the linkHref attribute from link element on model image element.
  79. conversionApi.writer.setAttribute( 'linkHref', linkHref, modelElement );
  80. }
  81. }, { priority: 'high' } );
  82. };
  83. }
  84. // Return a converter that adds the `<a>` element to data.
  85. //
  86. // @private
  87. // @returns {Function}
  88. //
  89. function downcastImageLink() {
  90. return dispatcher => {
  91. dispatcher.on( 'attribute:linkHref:image', ( evt, data, conversionApi ) => {
  92. // The image will be already converted - so it will be present in the view.
  93. const viewFigure = conversionApi.mapper.toViewElement( data.item );
  94. const writer = conversionApi.writer;
  95. // But we need to check whether the link element exists.
  96. const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' );
  97. // Create an icon indicator for a linked image.
  98. const linkIconIndicator = writer.createUIElement( 'span', { class: 'ck ck-link-image_icon' }, function( domDocument ) {
  99. const domElement = this.toDomElement( domDocument );
  100. domElement.innerHTML = linkIcon;
  101. return domElement;
  102. } );
  103. // If so, update the attribute if it's defined or remove the entire link if the attribute is empty.
  104. if ( linkInImage ) {
  105. if ( data.attributeNewValue ) {
  106. writer.setAttribute( 'href', data.attributeNewValue, linkInImage );
  107. } else {
  108. const viewImage = Array.from( linkInImage.getChildren() ).find( child => child.name === 'img' );
  109. writer.move( writer.createRangeOn( viewImage ), writer.createPositionAt( viewFigure, 0 ) );
  110. writer.remove( linkInImage );
  111. }
  112. } else {
  113. // But if it does not exist. Let's wrap already converted image by newly created link element.
  114. // 1. Create an empty link element.
  115. const linkElement = writer.createContainerElement( 'a', { href: data.attributeNewValue } );
  116. // 2. Insert link inside the associated image.
  117. writer.insert( writer.createPositionAt( viewFigure, 0 ), linkElement );
  118. // 3. Move the image to the link.
  119. writer.move( writer.createRangeOn( viewFigure.getChild( 1 ) ), writer.createPositionAt( linkElement, 0 ) );
  120. // 4. Inset the linked image icon indicator.
  121. writer.insert( writer.createPositionAt( linkElement, 'end' ), linkIconIndicator );
  122. }
  123. } );
  124. };
  125. }