1import { Plugin, Matcher, toMap } from 'ckeditor5'; 2import CustomLinkEditing from './customLinkEditing.js'; 3 4export default class LinkImageEditing extends Plugin { 5 /** 6 * @inheritDoc 7 */ 8 static get requires() { 9 return [ 'ImageEditing', 'ImageUtils', CustomLinkEditing ]; 10 } 11 12 /** 13 * @inheritDoc 14 */ 15 static get pluginName() { 16 return 'CustomLinkImageEditing'; 17 } 18 19 init() { 20 const editor = this.editor; 21 const schema = editor.model.schema; 22 23 if ( editor.plugins.has( 'ImageBlockEditing' ) ) { 24 schema.extend( 'imageBlock', { allowAttributes: [ 'linkHref', 'linkTitle' ] } ); 25 } 26 27 editor.conversion.for( 'upcast' ).add( upcastLink( editor ) ); 28 editor.conversion.for( 'downcast' ).add( downcastImageLink( editor ) ); 29 30 // Definitions for decorators are provided by the `link` command and the `LinkEditing` plugin. 31 this._enableAutomaticDecorators(); 32 this._enableManualDecorators(); 33 } 34 35 /** 36 * Processes {@link module:link/link~LinkDecoratorAutomaticDefinition automatic decorators} definitions and 37 * attaches proper converters that will work when linking an image.` 38 * 39 * @private 40 */ 41 _enableAutomaticDecorators() { 42 const editor = this.editor; 43 const command = editor.commands.get( 'link' ); 44 const automaticDecorators = command.automaticDecorators; 45 46 if ( automaticDecorators.length ) { 47 editor.conversion.for( 'downcast' ).add( automaticDecorators.getDispatcherForLinkedImage() ); 48 } 49 } 50 51 /** 52 * Processes transformed {@link module:link/utils~ManualDecorator} instances and attaches proper converters 53 * that will work when linking an image. 54 * 55 * @private 56 */ 57 _enableManualDecorators() { 58 const editor = this.editor; 59 const command = editor.commands.get( 'link' ); 60 61 for ( const decorator of command.manualDecorators ) { 62 if ( editor.plugins.has( 'ImageBlockEditing' ) ) { 63 editor.model.schema.extend( 'imageBlock', { allowAttributes: decorator.id } ); 64 } 65 66 if ( editor.plugins.has( 'ImageInlineEditing' ) ) { 67 editor.model.schema.extend( 'imageInline', { allowAttributes: decorator.id } ); 68 } 69 70 editor.conversion.for( 'downcast' ).add( downcastImageLinkManualDecorator( decorator ) ); 71 editor.conversion.for( 'upcast' ).add( upcastImageLinkManualDecorator( editor, decorator ) ); 72 } 73 } 74} 75 76// Returns a converter for linked block images that consumes the "href" attribute 77// if a link contains an image. 78// 79// @private 80// @param {module:core/editor/editor~Editor} editor The editor instance. 81// @returns {Function} 82function upcastLink( editor ) { 83 const isImageInlinePluginLoaded = editor.plugins.has( 'ImageInlineEditing' ); 84 const imageUtils = editor.plugins.get( 'ImageUtils' ); 85 86 return dispatcher => { 87 dispatcher.on( 'element:a', ( evt, data, conversionApi ) => { 88 const viewLink = data.viewItem; 89 const imageInLink = imageUtils.findViewImgElement( viewLink ); 90 91 if ( !imageInLink ) { 92 return; 93 } 94 95 const blockImageView = imageInLink.findAncestor( element => imageUtils.isBlockImageView( element ) ); 96 97 // There are four possible cases to consider here 98 // 99 // 1. A "root > ... > figure.image > a > img" structure. 100 // 2. A "root > ... > figure.image > a > picture > img" structure. 101 // 3. A "root > ... > block > a > img" structure. 102 // 4. A "root > ... > block > a > picture > img" structure. 103 // 104 // but the last 2 cases should only be considered by this converter when the inline image plugin 105 // is NOT loaded in the editor (because otherwise, that would be a plain, linked inline image). 106 if ( isImageInlinePluginLoaded && !blockImageView ) { 107 return; 108 } 109 110 // There's an image inside an <a> element - we consume it so it won't be picked up by the Link plugin. 111 const consumableAttributes = { attributes: [ 'href', 'title' ] }; 112 113 // Consume the `href` attribute so the default one will not convert it to $text attribute. 114 if ( !conversionApi.consumable.consume( viewLink, consumableAttributes ) ) { 115 // Might be consumed by something else - i.e. other converter with priority=highest - a standard check. 116 return; 117 } 118 119 const linkHref = viewLink.getAttribute( 'href' ); 120 const linkTitle = viewLink.getAttribute( 'title' ); 121 122 // Missing the 'href' attribute. 123 if ( !linkHref ) { 124 return; 125 } 126 127 // A full definition of the image feature. 128 // figure > a > img: parent of the view link element is an image element (figure). 129 let modelElement = data.modelCursor.parent; 130 131 if ( !modelElement.is( 'element', 'imageBlock' ) ) { 132 // a > img: parent of the view link is not the image (figure) element. We need to convert it manually. 133 const conversionResult = conversionApi.convertItem( imageInLink, data.modelCursor ); 134 135 // Set image range as conversion result. 136 data.modelRange = conversionResult.modelRange; 137 138 // Continue conversion where image conversion ends. 139 data.modelCursor = conversionResult.modelCursor; 140 141 modelElement = data.modelCursor.nodeBefore; 142 } 143 144 if ( modelElement && modelElement.is( 'element', 'imageBlock' ) ) { 145 // Set the linkHref attribute from link element on model image element. 146 conversionApi.writer.setAttribute( 'linkHref', linkHref, modelElement ); 147 conversionApi.writer.setAttribute( 'linkTitle', linkTitle, modelElement ); 148 } 149 }, { priority: 'high' } ); 150 // Using the same priority that `upcastImageLinkManualDecorator()` converter guarantees 151 // that manual decorators will decorate the proper element. 152 }; 153} 154 155// Creates a converter that adds `<a>` to linked block image view elements. 156// 157// @private 158function downcastImageLink( editor ) { 159 const imageUtils = editor.plugins.get( 'ImageUtils' ); 160
161 return dispatcher => { 162 dispatcher.on( 'attribute:linkHref:imageBlock', ( evt, data, conversionApi ) => { 163 if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { 164 return; 165 } 166 167 // The image will be already converted - so it will be present in the view. 168 const viewFigure = conversionApi.mapper.toViewElement( data.item ); 169 const writer = conversionApi.writer; 170 171 // But we need to check whether the link element exists. 172 const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' ); 173 const viewImage = imageUtils.findViewImgElement( viewFigure ); 174 // <picture>...<img/></picture> or <img/> 175 const viewImgOrPicture = viewImage.parent.is( 'element', 'picture' ) ? viewImage.parent : viewImage; 176 177 // If so, update the attribute if it's defined or remove the entire link if the attribute is empty. 178 if ( linkInImage ) { 179 if ( data.attributeNewValue ) { 180 writer.setAttribute( 'href', data.attributeNewValue, linkInImage ); 181 writer.setAttribute( 'title', data.item.getAttribute( 'linkTitle' ), linkInImage ); 182 } else { 183 writer.move( writer.createRangeOn( viewImgOrPicture ), writer.createPositionAt( viewFigure, 0 ) ); 184 writer.remove( linkInImage ); 185 } 186 } else { 187 // But if it does not exist. Let's wrap already converted image by newly created link element. 188 // 1. Create an empty link element. 189 const linkElement = writer.createContainerElement( 'a', { href: data.attributeNewValue, title: data.item.getAttribute( 'linkTitle' ) } ); 190 191 // 2. Insert link inside the associated image. 192 writer.insert( writer.createPositionAt( viewFigure, 0 ), linkElement ); 193 194 // 3. Move the image to the link. 195 writer.move( writer.createRangeOn( viewImgOrPicture ), writer.createPositionAt( linkElement, 0 ) ); 196 } 197 }, { priority: 'high' } ); 198 }; 199} 200 201// Returns a converter that decorates the `<a>` element when the image is the link label. 202// 203// @private 204// @returns {Function} 205function downcastImageLinkManualDecorator( decorator ) { 206 return dispatcher => { 207 dispatcher.on( `attribute:${ decorator.id }:imageBlock`, ( evt, data, conversionApi ) => { 208 const viewFigure = conversionApi.mapper.toViewElement( data.item ); 209 const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' ); 210 211 // The <a> element was removed by the time this converter is executed. 212 // It may happen when the base `linkHref` and decorator attributes are removed 213 // at the same time (see #8401). 214 if ( !linkInImage ) { 215 return; 216 } 217 218 for ( const [ key, val ] of toMap( decorator.attributes ) ) { 219 conversionApi.writer.setAttribute( key, val, linkInImage ); 220 } 221 222 if ( decorator.classes ) { 223 conversionApi.writer.addClass( decorator.classes, linkInImage ); 224 } 225 226 for ( const key in decorator.styles ) { 227 conversionApi.writer.setStyle( key, decorator.styles[ key ], linkInImage ); 228 } 229 conversionApi.writer.setAttribute( 'title', data.item.getAttribute( 'linkTitle' ), linkInImage ); 230 } ); 231 }; 232} 233 234// Returns a converter that checks whether manual decorators should be applied to the link. 235// 236// @private 237// @returns {Function} 238function upcastImageLinkManualDecorator( editor, decorator ) { 239 const isImageInlinePluginLoaded = editor.plugins.has( 'ImageInlineEditing' ); 240 const imageUtils = editor.plugins.get( 'ImageUtils' ); 241 242 return dispatcher => { 243 dispatcher.on( 'element:a', ( evt, data, conversionApi ) => { 244 const viewLink = data.viewItem; 245 const imageInLink = imageUtils.findViewImgElement( viewLink ); 246 247 // We need to check whether an image is inside a link because the converter handles 248 // only manual decorators for linked images. See #7975. 249 if ( !imageInLink ) { 250 return; 251 } 252 253 const blockImageView = imageInLink.findAncestor( element => imageUtils.isBlockImageView( element ) ); 254 255 if ( isImageInlinePluginLoaded && !blockImageView ) { 256 return; 257 } 258 259 const matcher = new Matcher( decorator._createPattern() ); 260 const result = matcher.match( viewLink ); 261 262 // The link element does not have required attributes or/and proper values. 263 if ( !result ) { 264 return; 265 } 266 267 // Check whether we can consume those attributes. 268 if ( !conversionApi.consumable.consume( viewLink, result.match ) ) { 269 return; 270 } 271 272 // At this stage we can assume that we have the `<imageBlock>` element. 273 // `nodeBefore` comes after conversion: `<a><img></a>`. 274 // `parent` comes with full image definition: `<figure><a><img></a></figure>. 275 // See the body of the `upcastLink()` function.
276 const modelElement = data.modelCursor.nodeBefore || data.modelCursor.parent; 277 278 conversionApi.writer.setAttribute( decorator.id, true, modelElement ); 279 }, { priority: 'high' } ); 280 // Using the same priority that `upcastLink()` converter guarantees that the linked image was properly converted. 281 }; 282}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.