Edit: if self-closing tags arent "possible" in html I wonder how this tutorial does it with react: https://ckeditor.com/docs/ckeditor5/latest/framework/tutorials/using-react-in-a-widget.html
I've read through various threads including this where it is recommended to create a custom widget but it doesnt matter what I try, my custom html tag is never self closing. All of the examples below will cause CKE to turn <x-mycomponent />
into <x-mycomponent></x-mycomponent>
. Since I am not using any module bundler it might be because of that, but since my plugin is recognized I doubt that.
I am using CKSource.Editor.builtinPlugins.push(BladeComponentPlugin);
at the end of all examples since I am working without modules. Array.from( editor.ui.componentFactory.names() )
lists my plugin and console.log
s from within my plugin also fire.
I've started with this:
function BladeComponentPlugin(editor) {
editor.ui.componentFactory.add('bladeComponent', locale => {
const viewTemplate = '<span class="blade-component">[x-mycomponent]</span>';
const modelSchema = editor.model.schema;
modelSchema.register('bladeComponent', {
allowWhere: '$block',
isSelfClosing: true
});
return {
init() {
const view = editor.editing.view;
const model = editor.model;
// Convert <x-mycomponent /> to 'bladeComponent' model
model.schema.extend(view.elementToElement({
model: 'bladeComponent',
view: {
name: 'span',
classes: 'blade-component'
}
}));
// Convert 'bladeComponent' model to <x-mycomponent />
model.schema.extend(model.understand(viewTemplate));
editor.editing.mapper.on(
'viewToModelPosition',
(evt, data) => {
if (data.modelRange && data.modelRange.start) {
const { start } = data.modelRange;
if (start.nodeBefore && start.nodeBefore.name === 'span' && start.nodeBefore.hasClass('blade-component')) {
data.modelRange.start = model.createPositionAt(start.nodeBefore, 1);
}
}
}
);
},
render() {
...
}
};
});
}
CKSource.Editor.builtinPlugins.push(BladeComponentPlugin);
But self-closing wouldnt work. I've tried various things in the modelSchema.register options but no luck. I've then tried to somehow make the self closing happen in the data processing:
function BladeComponentPlugin(editor) {
editor.ui.componentFactory.add('bladeComponent', locale => {
const viewTemplate = '<span class="blade-component">[x-mycomponent /]</span>';
editor.conversion.for('upcast').elementToElement({
view: {
name: 'span',
classes: 'blade-component'
},
model: (viewElement, { writer }) => {
const modelElement = writer.createElement('bladeComponent');
return modelElement;
}
});
editor.conversion.for('downcast').elementToElement({
model: 'bladeComponent',
view: (modelElement, { writer }) => {
const viewElement = writer.createContainerElement('span', { class: 'blade-component' });
return viewElement;
}
});
editor.data.processor.htmlFilter.addRules({
elements: {
span: (element, { writer }) => {
if (element.hasClass('blade-component')) {
// Replace the self-closing syntax with the view template
return writer.createText(viewTemplate);
}
}
}
});
// Handle clicks on the component
editor.editing.view.document.on('click', 'span.blade-component', evt => {
const component = evt.target;
// Open dialog or perform action on component click
const componentName = 'x-mycomponent'; // Replace with the actual component name
console.log('Clicked component:', componentName);
});
});
}
But still, self closing wouldnt work. Then I tried the block widget approach (I guess):
function BladeComponentPlugin(editor) {
editor.ui.componentFactory.add('bladeComponent', locale => {
editor.plugins.get('BlockToolbar').add({
model: 'bladeComponent',
view: 'span',
label: 'Blade Component',
icon: 'x-mycomponent'
});
editor.conversion.for('upcast').elementToElement({
view: {
name: 'span',
classes: 'blade-component'
},
model: 'bladeComponent'
});
editor.conversion.for('dataDowncast').elementToElement({
model: 'bladeComponent',
view: (modelElement, { writer }) => {
const span = writer.createContainerElement('span', { class: 'blade-component' });
return span;
}
});
editor.conversion.for('editingDowncast').elementToElement({
model: 'bladeComponent',
view: (modelElement, { writer }) => {
const span = writer.createContainerElement('span', { class: 'blade-component' });
return span;
}
});
return {
init() {
editor.model.schema.extend('bladeComponent', { isObject: true, isBlock: true });
editor.editing.mapper.on(
'viewToModelPosition',
evt => {
const viewElement = evt.mapper.fromViewElement(evt.position.element);
if (viewElement.hasClass('blade-component')) {
evt.position.object = editor.model.document.getRoot();
evt.position.path = [evt.position.object];
}
},
{ priority: 'high' }
);
// Handle clicks on the component
editor.editing.view.document.on('click', 'span.blade-component', evt => {
const component = evt.target;
// Open dialog or perform action on component click
const componentName = 'x-mycomponent'; // Replace with the actual component name
console.log('Clicked component:', componentName);
});
}
};
});
}
And I've tried to somehow utilize the General HTML Support plugin:
function BladeComponentPlugin(editor) {
editor.ui.componentFactory.add('bladeComponent', locale => {
const viewTemplate = '<span class="blade-component"><x-mycomponent /></span>';
editor.data.processor.htmlFilter.addRules({
elements: {
span: (element, { writer }) => {
if (element.hasClass('blade-component')) {
// Replace the self-closing syntax with the view template
return writer.createText(viewTemplate);
}
}
}
});
editor.conversion.for('upcast').elementToElement({
view: {
name: 'span',
classes: 'blade-component'
},
model: (viewElement, { writer }) => {
const modelElement = writer.createElement('bladeComponent');
return modelElement;
}
});
editor.conversion.for('dataDowncast').elementToElement({
model: 'bladeComponent',
view: (modelElement, { writer }) => {
const span = writer.createContainerElement('span', { class: 'blade-component' });
return span;
}
});
editor.conversion.for('editingDowncast').elementToElement({
model: 'bladeComponent',
view: (modelElement, { writer }) => {
const span = writer.createContainerElement('span', { class: 'blade-component' });
return span;
}
});
// Handle clicks on the component
editor.editing.view.document.on('click', 'span.blade-component', evt => {
const component = evt.target;
// Open dialog or perform action on component click
const componentName = 'x-mycomponent'; // Replace with the actual component name
console.log('Clicked component:', componentName);
});
});
}
// Initialize the plugin when CKEditor is ready
ClassicEditor.builtinPlugins.push(BladeComponentPlugin);
Still no luck. Before I try another editor I hope to somehow find a solution or answer why my self-closing approach doesnt work.
I noticed that whatever markup I type, like <x-foobar />
, it would always add a separate closing tag. I dont know if that means my plugin doesnt work or load or whatever.