Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow linking images #702

Closed
davidpolberger opened this issue Dec 4, 2017 · 51 comments
Closed

Allow linking images #702

davidpolberger opened this issue Dec 4, 2017 · 51 comments
Labels
domain:accessibility This issue reports an accessibility problem. Epic package:link support:2 An issue reported by a commercially licensed client. type:feature This issue reports a feature request (an idea for a new functionality or a missing option).

Comments

@davidpolberger
Copy link

davidpolberger commented Dec 4, 2017

🐞 Is this a bug report or feature request? (choose one)

  • Feature request

💻 Version of CKEditor

1.0.0-alpha.2

📋 Steps to reproduce

  1. Go to https://ckeditor5.github.io/
  2. Drag and drop an image into the editor to upload it.
  3. Select the image by clicking it.

✅ Expected result

The link button should be enabled.

❎ Actual result

The link button is disabled. Selecting the surrounding text in addition to the image enables the link button, but no link is created for the image when the button is pressed.

📃 Other details that might be useful

This scenario works in CKEditor 4.

👍 If you need this

(Edited by @Reinmar:) We need to know how important is this feature for you. Please react with 👍 if you need this feature.

@Reinmar Reinmar added status:confirmed type:feature This issue reports a feature request (an idea for a new functionality or a missing option). labels Dec 5, 2017
@Reinmar
Copy link
Member

Reinmar commented Dec 5, 2017

I thought there was a ticket for it already, but I can't find it. Definitely needed.

@oleq
Copy link
Member

oleq commented Dec 12, 2017

@Reinmar I guess you were referring to https://github.com/ckeditor/ckeditor5-link/issues/85, right?

@Reinmar
Copy link
Member

Reinmar commented Dec 12, 2017

Ah, right. I forgot it was the opposite issue in the past. So let's keep this one open too.

@Reinmar
Copy link
Member

Reinmar commented Dec 17, 2017

BTW, I'm curious what are the use cases for linking images.

The thing is that the only time when I wanted to link an image was when I wanted to allow opening a high-res version of that image. But then, as a user I must know the link to that high-res version. So it seems that cases like this one should rather be handled by the system, not the content author. For example, GH automatically links to the full version of images, even if you'll create a normal image in CommonMark.

The other case which comes to my mind is linking e.g. lead images in a newsletter. There may be images linking to blog posts or some other landing pages. This kinda starts falling into a structured content case and most likely should not be done inside the editor. But it's a case in which one will want to link the image manually if it's not handled by the system. If it's handled by the system, then e.g. the system may decide to use a block-level <a> to wrap the image and some additional text which follows it (the entire section of the content) which might give a better result.

So, what are the other scenarios in which the user will want to link an image which is a part of the content?

@davidpolberger
Copy link
Author

I agree with you, @Reinmar, for images that are photographs. But there are other types of images too, including line art and text. Examples include logos and highly stylized calls to action. I'd say that images that mostly consist of (stylized) text are quite likely to be turned into links.

@mwadams
Copy link

mwadams commented Jan 9, 2018

Similarly, an image that is e.g. a thumbnail for an article is frequently linked to the article itself.

@long-lazuli
Copy link

long-lazuli commented Jan 28, 2018

I got the need for an image to be linking to a product.
An ad on a banner.

My current idea, is to use <map><area></map>, which can placed after the <img>.
Instead of using a surrounding <a> element, which has already a meaning in CK.

I'm cloning the <figcaption> plugin for this.
It's far from perfect. As the area must have coords, and I want my images to be responsive.
I also need to generate an id for the <map>, referenced on <img>.

Here is an exemple of result : (as plain html)
https://codepen.io/long-lazuli/pen/644a741b5574b452dea02e1d61591aa1?editors=1100

@Reinmar Reinmar changed the title Allow images to serve as links Allow linking images Jan 29, 2018
@Reinmar Reinmar added this to the backlog milestone Apr 5, 2018
@stnor
Copy link

stnor commented May 15, 2018

We're using ckeditor to send email newsletters.
Some newsletters will contain videos and we'd like add an image of the player and surround it with an href to the actual video.

When pasting the newsletter contents (from Wordpress) into ckeditor ("@ckeditor/ckeditor5-build-classic": "10.0.0") the links on the images are removed.

What are the ways of working around this behavior?

@stnor
Copy link

stnor commented May 21, 2018

@Reinmar Is there any way for me to workaround or add this feature using a plugin or otherwise? We're in dire need.

Thanks!

@Reinmar
Copy link
Member

Reinmar commented May 21, 2018

I asked @jodator to take a look on this. Sadly, the image plugin is one of the more complicated ones and there's no quick recipe how to add conversion for links in them. But to unblock you guys, we'll at least try now to find a way how such conversion could be added (either by a separate plugin or by modifying the image plugin).

@stnor
Copy link

stnor commented May 21, 2018

Excellent, much appreciated!

@jodator
Copy link
Contributor

jodator commented May 22, 2018

@stnor & @Reinmar So far I've managed to preserve existing links that wraps <img> or <figure>.

I've tested this on "caption" manual test from image plugin. It works with CKEditor's Image & Link plugins. It preserves links and allows to edit images. It does not allow to edit those links and ImageStyle plugin stopped to work on those images also.

I think that one way to enhance this code is to change its behavior so it will convert whole link with image so it will not create <figure> and will not create a widget for such images. But to propose that I'd need to know if that is what you need :) as right now

<a href=""><img src=""></a>

will be transformed to:

<a href=""><figure><img src=""></figure></a>

The current implementation is below:

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';
import Typing from '@ckeditor/ckeditor5-typing/src/typing';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
import Image from '../../src/image';
import ImageCaption from '../../src/imagecaption';
import Undo from '@ckeditor/ckeditor5-undo/src/undo';
import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
import ImageToolbar from '../../src/imagetoolbar';
import ImageStyle from '../../src/imagestyle';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
import List from '@ckeditor/ckeditor5-list/src/list';

import Link from '@ckeditor/ckeditor5-link/src/link';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import Range from '../../../ckeditor5-engine/src/view/range';
import Position from '../../../ckeditor5-engine/src/view/position';

class ImageLink extends Plugin {
	init() {
		const editor = this.editor;

		editor.model.schema.extend( 'image', { allowAttributes: [ 'href' ] } );

		editor.conversion.for( 'upcast' ).add( upcastLink() );
		editor.conversion.for( 'upcast' ).add( upcastImageLink( 'img' ) );
		editor.conversion.for( 'upcast' ).add( upcastImageLink( 'figure' ) );
		editor.conversion.for( 'downcast' ).add( downcastImageLink() );
	}
}

ClassicEditor
	.create( document.querySelector( '#editor' ), {
		plugins: [
			Enter, Typing, Paragraph, Heading, Image, ImageToolbar, Link, ImageCaption,
			Undo, Clipboard, ImageStyle, Bold, Italic, Heading, List, ImageLink
		],
		toolbar: [ 'heading', '|', 'undo', 'redo', 'bold', 'italic', 'bulletedList', 'numberedList', 'link' ],
		image: {
			toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
		}
	} )
	.then( editor => {
		window.editor = editor;
	} )
	.catch( err => {
		console.error( err.stack );
	} );

/**
 * Returns converter for links that wraps <img> or <figure> elements.
 *
 * @returns {Function}
 */
function upcastLink() {
	return dispatcher => {
		dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
			const viewLink = data.viewItem;

			const imageInLink = Array.from( viewLink.getChildren() ).find( child => child.name === 'img' || child.name === 'figure' );

			if ( imageInLink ) {
				// There's an image (or figure) inside an <a> element - we consume it so it won't be picked up by Link plugin.
				const consumableAttributes = { attributes: [ 'href' ] };

				// Consume the link so the default one will not convert it to $text attribute.
				if ( !conversionApi.consumable.test( viewLink, consumableAttributes ) ) {
					// Might be consumed by something else - ie other converter with priority=highest - a standard check.
					return;
				}

				// Consume 'href' attribute from link element.
				conversionApi.consumable.consume( viewLink, consumableAttributes );
			}
		}, { priority: 'high' } );
	};
}

function upcastImageLink( elementName ) {
	return dispatcher => {
		dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
			const viewImage = data.viewItem;
			const parent = viewImage.parent;

			// Check only <img>/<figure> that are direct children of a link.
			if ( parent.name === 'a' ) {
				const modelImage = data.modelCursor.nodeBefore;
				const linkHref = parent.getAttribute( 'href' );

				if ( modelImage && linkHref ) {
					// Set the href attribute from link element on model image element.
					conversionApi.writer.setAttribute( 'href', linkHref, modelImage );
				}
			}
		}, { priority: 'normal' } );
	};
}

function downcastImageLink() {
	return dispatcher => {
		dispatcher.on( 'attribute:href:image', ( evt, data, conversionApi ) => {
			const href = data.attributeNewValue;
			// The image will be already converted - so it will be present in the view.
			const viewImage = conversionApi.mapper.toViewElement( data.item );

			// Below will wrap already converted image by newly created link element.

			// 1. Create empty link element.
			const linkElement = conversionApi.writer.createContainerElement( 'a', { href } );

			// 2. Insert link before associated image.
			conversionApi.writer.insert( Position.createBefore( viewImage ), linkElement );

			// 3. Move whole converted image to a link.
			conversionApi.writer.move( Range.createOn( viewImage ), new Position( linkElement, 0 ) );
		}, { priority: 'normal' } );
	};
}

@Reinmar
Copy link
Member

Reinmar commented May 22, 2018

Cool! :)

Do you think it would be possible to render <a> elements inside the <figure> (like in https://sdk.ckeditor.com/samples/image2.html) so only <img> is wrapped.

@jodator
Copy link
Contributor

jodator commented May 23, 2018

@Reinmar Yeah - actually it was a bit harder to make it wrap <figure/> instead of an <image/>. AFAIR I've used there AttributeAlement and conversionApi.writer.wrap() to wrap image with ''.

@stnor
Copy link

stnor commented May 23, 2018

Hi.
Thanks for this.
However, I cant get this to work for me.
I added some logging in the dispatchers and two are invoked (upcastLink and upcastImageLink) when I paste in the contents.

When is the downcast method supposed to run?

As you can see I'm using angularjs and synching the model on 'change'.

My code:

import CkEditor from "@ckeditor/ckeditor5-build-classic";

export default class CkEditorDirective {

  constructor() {
    this.restrict = 'A';
    this.require = 'ngModel';
  }

  static create() {
    return new CkEditorDirective();
  }

  link(scope, elem, attr, ngModel) {
    CkEditor.create(elem[0]).then((editor) => {

      function upcastLink() {
        return dispatcher => {
          dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
            console.log('upcastLink', evt, data, conversionApi)

            const viewLink = data.viewItem;

            const imageInLink = Array.from( viewLink.getChildren() ).find( child => child.name === 'img' || child.name === 'figure' );

            if ( imageInLink ) {
              // There's an image (or figure) inside an <a> element - we consume it so it won't be picked up by Link plugin.
              const consumableAttributes = { attributes: [ 'href' ] };

              // Consume the link so the default one will not convert it to $text attribute.
              if ( !conversionApi.consumable.test( viewLink, consumableAttributes ) ) {
                // Might be consumed by something else - ie other converter with priority=highest - a standard check.
                return;
              }

              // Consume 'href' attribute from link element.
              conversionApi.consumable.consume( viewLink, consumableAttributes );
            }
          }, { priority: 'high' } );
        };
      }

      function upcastImageLink( elementName ) {
        return dispatcher => {
          dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
            console.log('upcastImageLink', evt, data, conversionApi)
            const viewImage = data.viewItem;
            const parent = viewImage.parent;

            // Check only <img>/<figure> that are direct children of a link.
            if ( parent.name === 'a' ) {
              const modelImage = data.modelCursor.nodeBefore;
              const linkHref = parent.getAttribute( 'href' );

              if ( modelImage && linkHref ) {
                // Set the href attribute from link element on model image element.
                conversionApi.writer.setAttribute( 'href', linkHref, modelImage );
              }
            }
          }, { priority: 'normal' } );
        };
      }

      function downcastImageLink() {
        return dispatcher => {
          dispatcher.on( 'attribute:href:image', ( evt, data, conversionApi ) => {
            console.log('downcastImageLink', evt, data, conversionApi)
            const href = data.attributeNewValue;
            // The image will be already converted - so it will be present in the view.
            const viewImage = conversionApi.mapper.toViewElement( data.item );

            // Below will wrap already converted image by newly created link element.

            // 1. Create empty link element.
            const linkElement = conversionApi.writer.createContainerElement( 'a', { href } );

            // 2. Insert link before associated image.
            conversionApi.writer.insert( Position.createBefore( viewImage ), linkElement );

            // 3. Move whole converted image to a link.
            conversionApi.writer.move( Range.createOn( viewImage ), new Position( linkElement, 0 ) );
          }, { priority: 'normal' } );
        };
      }


      editor.model.schema.extend( 'image', { allowAttributes: [ 'href' ] } );
      editor.conversion.for( 'upcast' ).add( upcastLink() );
      editor.conversion.for( 'upcast' ).add( upcastImageLink( 'img' ) );
      editor.conversion.for( 'upcast' ).add( upcastImageLink( 'figure' ) );
      editor.conversion.for( 'downcast' ).add( downcastImageLink() );


      editor.model.document.on('change', () => {
        scope.$apply(() => {
          ngModel.$setViewValue(editor.getData());
        });
      });

      ngModel.$render = () => {
        editor.setData(ngModel.$modelValue);
      };

      scope.$on('$destroy', () => {
        editor.destroy();
      });
    });
  }
}

Pasted contents (from wordpress, wysiwyg) source:

Test of link...

<a href="https://nomp.se"><img class="aligncenter size-large wp-image-3525" src="https://blog.nomp.se/wp-content/uploads/2018/05/gdpr-1024x445.png" alt="" width="640" height="278" /></a>

@stnor
Copy link

stnor commented May 23, 2018

screen shot 2018-05-23 at 11 42 33

@jodator
Copy link
Contributor

jodator commented May 23, 2018

@stnor The code provided by me must be implemented as a CKEditor plugin in order to properly extend upcast (from view to the model) and downcast (from model to the view) conversions.

You'll need a custom build for that.

@stnor
Copy link

stnor commented May 23, 2018

@jodator ok, thanks! i'll check that out.

@stnor
Copy link

stnor commented May 23, 2018

I created a new factory for CkEditor in my project:

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';
import Typing from '@ckeditor/ckeditor5-typing/src/typing';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
import Image from '@ckeditor/ckeditor5-image/src/image';
import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
import Undo from '@ckeditor/ckeditor5-undo/src/undo';
import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar';
import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
import List from '@ckeditor/ckeditor5-list/src/list';
import Link from '@ckeditor/ckeditor5-link/src/link';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import Range from '@ckeditor/ckeditor5-engine/src/view/range';
import Position from '@ckeditor/ckeditor5-engine/src/view/position';

export default class CustomCkEditorFactory {

  static create(element) {
    return ClassicEditor
        .create( element, {
          plugins: [
            Enter, Typing, Paragraph, Heading, Image, ImageToolbar, Link, ImageCaption,
            Undo, Clipboard, ImageStyle, Bold, Italic, Heading, List, ImageLink
          ],
          toolbar: [ 'heading', '|', 'undo', 'redo', 'bold', 'italic', 'bulletedList', 'numberedList', 'link' ],
          image: {
            toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
          }
        } );
  }
}

class ImageLink extends Plugin {
  init() {
    console.log('Plugin in init()')
    const editor = this.editor;
    editor.model.schema.extend( 'image', { allowAttributes: [ 'href' ] } );
    editor.conversion.for( 'upcast' ).add( upcastLink() );
    editor.conversion.for( 'upcast' ).add( upcastImageLink( 'img' ) );
    editor.conversion.for( 'upcast' ).add( upcastImageLink( 'figure' ) );
    editor.conversion.for( 'downcast' ).add( downcastImageLink() );
  }
}

/**
 * Returns converter for links that wraps <img> or <figure> elements.
 *
 * @returns {Function}
 */
function upcastLink() {
  return dispatcher => {
    dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
      console.log('upcastLink')
      const viewLink = data.viewItem;

      const imageInLink = Array.from( viewLink.getChildren() ).find( child => child.name === 'img' || child.name === 'figure' );

      if ( imageInLink ) {
        // There's an image (or figure) inside an <a> element - we consume it so it won't be picked up by Link plugin.
        const consumableAttributes = { attributes: [ 'href' ] };

        // Consume the link so the default one will not convert it to $text attribute.
        if ( !conversionApi.consumable.test( viewLink, consumableAttributes ) ) {
          // Might be consumed by something else - ie other converter with priority=highest - a standard check.
          return;
        }

        // Consume 'href' attribute from link element.
        conversionApi.consumable.consume( viewLink, consumableAttributes );
      }
    }, { priority: 'high' } );
  };
}

function upcastImageLink( elementName ) {
  return dispatcher => {
    dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
      console.log('upcastImageLink')

      const viewImage = data.viewItem;
      const parent = viewImage.parent;

      // Check only <img>/<figure> that are direct children of a link.
      if ( parent.name === 'a' ) {
        const modelImage = data.modelCursor.nodeBefore;
        const linkHref = parent.getAttribute( 'href' );

        if ( modelImage && linkHref ) {
          // Set the href attribute from link element on model image element.
          conversionApi.writer.setAttribute( 'href', linkHref, modelImage );
        }
      }
    }, { priority: 'normal' } );
  };
}

function downcastImageLink() {
  return dispatcher => {
    dispatcher.on( 'attribute:href:image', ( evt, data, conversionApi ) => {
      console.log('downcastImageLink')

      const href = data.attributeNewValue;
      // The image will be already converted - so it will be present in the view.
      const viewImage = conversionApi.mapper.toViewElement( data.item );

      // Below will wrap already converted image by newly created link element.

      // 1. Create empty link element.
      const linkElement = conversionApi.writer.createContainerElement( 'a', { href } );

      // 2. Insert link before associated image.
      conversionApi.writer.insert( Position.createBefore( viewImage ), linkElement );

      // 3. Move whole converted image to a link.
      conversionApi.writer.move( Range.createOn( viewImage ), new Position( linkElement, 0 ) );
    }, { priority: 'normal' } );
  };
}

This produced an error when registering the plugin: "Class constructor Plugin cannot be invoked without 'new'", as in #649

So I copied the constructor from the Plugin class and removed the inheritance, as a workaround described in #649.

Unfortunately the outcome is the same as before, downcast is not run, even though the "ImageLink" is among the registered plugins.

´´´´
CustomCkEditorFactory.js:118 Plugin in init()
CustomCkEditorFactory.js:141 upcastLink
CustomCkEditorFactory.js:168 upcastImageLink

Any ideas?

@stnor
Copy link

stnor commented May 23, 2018

I just managed somehow to get downcast to run once, but I am not sure how I made that happen...

@Reinmar
Copy link
Member

Reinmar commented May 23, 2018

The code you posted works fine for me:

  1. I copied it 1 to 1 to build-classic's src/ckeditor.js,
  2. I executed npm run build-ckeditor
  3. And I opened the sample:

image

@stnor
Copy link

stnor commented May 23, 2018

Yes, but as you can see, downcast isn't being run, and the link is not in the HTML model when I access it using editor.getData()

@Reinmar
Copy link
Member

Reinmar commented May 23, 2018

WFM:

image

@Reinmar
Copy link
Member

Reinmar commented May 23, 2018

But anyway, the question is why do you get such a strange error:

Class constructor Plugin cannot be invoked without 'new'

Some issues with Babel? But why?

@Reinmar
Copy link
Member

Reinmar commented May 23, 2018

OK, according to #649 that's indeed Babel. Moving the plugin to node_modules (as proposed in that ticket) meant that Babel started to ignore this code (so it wasn't transpiled). Apparently, if part of the code gets transpiled and part doesn't problems appear.

I think that you need to adjust your webpack config to make sure than entire CKEditor 5's source (and code which uses it) gets transpiled or that none of it is transpiled.

@szymonkups proposed this recently, when working on a React component: https://github.com/ckeditor/ckeditor5-react/tree/t/1#changes-in-webpackconfigprodjs-only

@TuguldurJ
Copy link

This feature is more important than #436 issue that published recently.

@Reinmar Reinmar modified the milestones: nice-to-have, next Dec 10, 2019
@lslowikowska lslowikowska added the support:1 An issue reported by a commercially licensed client. label Dec 11, 2019
@JLeeC
Copy link

JLeeC commented Dec 18, 2019

We are also in great need of this too. This is a feature being asked of by several of my customers as well. Any ETA?

@jayli66
Copy link

jayli66 commented Dec 31, 2019

We upgraded our RTE and chose ckeditor5 between TinyMCE, QuillJS and some others. We were very pleased with ckeditor5 except found out recently that it can't link images. It's a surprise cause this use case is very common. Especially when creating email or web page blocks. We had to rollback to old editor because of missing this feature.

This ticket is open for 2 years now and lots of people asked for this feature. Really looking forward to seeing it in the next release.

Happy New Year!

@ghost
Copy link

ghost commented Feb 28, 2020

i have the same problem, its a vital function and we need it now!

@mvrska
Copy link

mvrska commented Feb 29, 2020

Thanks again @jodator.

Just for the sake of documentation, the working workaround for this issue is now:

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';
import Typing from '@ckeditor/ckeditor5-typing/src/typing';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
import Image from '@ckeditor/ckeditor5-image/src/image';
import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
import Undo from '@ckeditor/ckeditor5-undo/src/undo';
import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar';
import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
import List from '@ckeditor/ckeditor5-list/src/list';
import Link from '@ckeditor/ckeditor5-link/src/link';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import Range from '@ckeditor/ckeditor5-engine/src/view/range';
import Position from '@ckeditor/ckeditor5-engine/src/view/position';

export default class CustomCkEditorFactory {

  static create(element) {
    return ClassicEditor
        .create( element, {
          plugins: [
            Enter, Typing, Paragraph, Heading, Image, ImageToolbar, Link, ImageCaption,
            Undo, Clipboard, ImageStyle, Bold, Italic, Heading, List, ImageLink
          ],
          toolbar: [ 'heading', '|', 'undo', 'redo', 'bold', 'italic', 'bulletedList', 'numberedList', 'link' ],
          image: {
            toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
          }
        } );
  }
}

class ImageLink extends Plugin {

  init() {
    const editor = this.editor;
    editor.model.schema.extend( 'image', { allowAttributes: [ 'href' ] } );
    editor.conversion.for( 'upcast' ).add( upcastLink() );
    editor.conversion.for( 'upcast' ).add( upcastImageLink( 'img' ) );
    editor.conversion.for( 'upcast' ).add( upcastImageLink( 'figure' ) );
    editor.conversion.for( 'downcast' ).add( downcastImageLink() );
  }
}

/**
 * Returns converter for links that wraps <img> or <figure> elements.
 *
 * @returns {Function}
 */
function upcastLink() {
  return dispatcher => {
    dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
      const viewLink = data.viewItem;
      const imageInLink = Array.from( viewLink.getChildren() ).find( child => child.name === 'img' || child.name === 'figure' );

      if ( imageInLink ) {
        // There's an image (or figure) inside an <a> element - we consume it so it won't be picked up by Link plugin.
        const consumableAttributes = { attributes: [ 'href' ] };

        // Consume the link so the default one will not convert it to $text attribute.
        if ( !conversionApi.consumable.test( viewLink, consumableAttributes ) ) {
          // Might be consumed by something else - ie other converter with priority=highest - a standard check.
          return;
        }

        // Consume 'href' attribute from link element.
        conversionApi.consumable.consume( viewLink, consumableAttributes );
      }
    }, { priority: 'high' } );
  };
}

function upcastImageLink( elementName ) {
  return dispatcher => {
    dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
      const viewImage = data.viewItem;
      const parent = viewImage.parent;

      // Check only <img>/<figure> that are direct children of a link.
      if ( parent.name === 'a' ) {
        const modelImage = Array.from( data.modelRange.getItems() ).find( item => item.is( 'image' ) );
        const linkHref = parent.getAttribute( 'href' );

        if ( modelImage && linkHref ) {
          // Set the href attribute from link element on model image element.
          conversionApi.writer.setAttribute( 'href', linkHref, modelImage );
        }
      }
    }, { priority: 'normal' } );
  };
}

function downcastImageLink() {
  return dispatcher => {
    dispatcher.on( 'attribute:href:image', ( evt, data, conversionApi ) => {
      const href = data.attributeNewValue;
      // The image will be already converted - so it will be present in the view.
      const viewImage = conversionApi.mapper.toViewElement( data.item );

      // Below will wrap already converted image by newly created link element.

      // 1. Create empty link element.
      const linkElement = conversionApi.writer.createContainerElement( 'a', { href } );

      // 2. Insert link before associated image.
      conversionApi.writer.insert( Position.createBefore( viewImage ), linkElement );

      // 3. Move whole converted image to a link.
      conversionApi.writer.move( Range.createOn( viewImage ), new Position( linkElement, 0 ) );
    }, { priority: 'normal' } );
  };
}

@stnor, @jodator
Hi, I was able to successfully use this code to create a new custom build but have a single issue = the ImageLink plugin is showing in console.log(InlineEditor.builtinPlugins.map( plugin => plugin.pluginName )); as undefined.

My code is identical to yours in but two things:

  1. I added also the Alignement plugin, which works fine in the build
  2. Im using the inline editor, not the classic one

Would anyone please be so kind and advise, what could be the issue? TIA

@mvrska
Copy link

mvrska commented Mar 4, 2020

Hi, I was able to successfully use this code to create a new custom build but have a single issue = the ImageLink plugin is showing in console.log(InlineEditor.builtinPlugins.map( plugin => plugin.pluginName )); as undefined.

My code is identical to yours in but two things:

I added also the Alignement plugin, which works fine in the build
Im using the inline editor, not the classic one
Would anyone please be so kind and advise, what could be the issue? TIA

I was able to make the undefined component load successfully by modifying above code like so:

/**
 * @extends module:core/plugin~Plugin
 */
class ImageLink extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'ImageLink';
	}

Still cant get it to work though, will post updates

@lslowikowska lslowikowska added support:2 An issue reported by a commercially licensed client. and removed support:1 An issue reported by a commercially licensed client. labels Apr 16, 2020
@rshipp
Copy link
Contributor

rshipp commented May 1, 2020

I'm using this workaround in my project, but the link toolbar button not working on images is causing issues. Is there an easy way to enable that button?

@jodator
Copy link
Contributor

jodator commented May 29, 2020

As we identified this might be a risky task to do all in one step let's split it to:

@Reinmar
Copy link
Member

Reinmar commented Jun 22, 2020

The guide is not ready yet, but the feature is merged. It will be an opt-in feature for now. It may be enabled by default in all builds in the future.

@Reinmar Reinmar closed this as completed Jun 22, 2020
@wc-matteo
Copy link

I noticed that link decorators like this one:
image
show up in the ui but don't change anything in the markup for linkImage. Is this a known issue?

@pomek
Copy link
Member

pomek commented Jul 15, 2020

Thanks for reporting the issue. We're aware of it. See: #7519.

@robclancy
Copy link

Incase someone finds themself in this issue where it says no docs yet from the changelog... here is the docs now. https://ckeditor.com/docs/ckeditor5/latest/features/image.html#linking-images

@Reinmar Reinmar added the domain:accessibility This issue reports an accessibility problem. label Oct 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain:accessibility This issue reports an accessibility problem. Epic package:link support:2 An issue reported by a commercially licensed client. type:feature This issue reports a feature request (an idea for a new functionality or a missing option).
Projects
None yet
Development

No branches or pull requests