-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Comments
I thought there was a ticket for it already, but I can't find it. Definitely needed. |
@Reinmar I guess you were referring to https://github.com/ckeditor/ckeditor5-link/issues/85, right? |
Ah, right. I forgot it was the opposite issue in the past. So let's keep this one open too. |
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 So, what are the other scenarios in which the user will want to link an image which is a part of the content? |
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. |
Similarly, an image that is e.g. a thumbnail for an article is frequently linked to the article itself. |
I got the need for an image to be linking to a product. My current idea, is to use I'm cloning the Here is an exemple of result : (as plain html) |
We're using ckeditor to send email newsletters. 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? |
@Reinmar Is there any way for me to workaround or add this feature using a plugin or otherwise? We're in dire need. Thanks! |
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). |
Excellent, much appreciated! |
@stnor & @Reinmar So far I've managed to preserve existing links that wraps 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 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 <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' } );
};
} |
Cool! :) Do you think it would be possible to render |
@Reinmar Yeah - actually it was a bit harder to make it wrap |
Hi. 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:
|
@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. |
@jodator ok, thanks! i'll check that out. |
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. ´´´´ Any ideas? |
I just managed somehow to get downcast to run once, but I am not sure how I made that happen... |
The code you posted works fine for me:
|
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() |
But anyway, the question is why do you get such a strange error:
Some issues with Babel? But why? |
OK, according to #649 that's indeed Babel. Moving the plugin to 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 |
This feature is more important than #436 issue that published recently. |
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? |
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! |
i have the same problem, its a vital function and we need it now! |
@stnor, @jodator My code is identical to yours in but two things:
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:
Still cant get it to work though, will post updates |
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? |
As we identified this might be a risky task to do all in one step let's split it to:
|
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. |
Thanks for reporting the issue. We're aware of it. See: #7519. |
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 |
🐞 Is this a bug report or feature request? (choose one)
💻 Version of CKEditor
1.0.0-alpha.2
📋 Steps to reproduce
✅ 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.
The text was updated successfully, but these errors were encountered: