I have 2 modules in active development with the purpose of learning how to develop custom plugins for the Ckeditor5 wysiwyg.
On both modules, I am stuck on the following problem. The error pasted below is from the second module :
ckeditor5-dll.js?v=35.4.0:5 toolbarview-item-unavailable {item: 'testPlugin'}
Read more: https://ckeditor.com/docs/ckeditor5/latest/support/error-codes.html#error-toolbarview-item-unavailable
Clearing indicated my plugin cannot be included in the toolbar.
With the module enabled, my testPlugin module is visible and added to my active toolbar in the text-formats configuration area as show below:
However, when on a node-edit form the plugin icon is not rendered in the toolbar due to the javascript error included above :
The full code for the second module is located here for your reference but I pasted some essential code below for anyone who wants to help or refer to this question in the future:
testPlugin.ckeditor5.yml
testplugin_embeddedContent:
provider: testplugin
ckeditor5:
plugins:
- embeddedContent.EmbeddedContent
config:
# embeddedContent:
testPlugin:
openDialog:
func:
name: Drupal.ckeditor5.openDialog
invoke: false
dialogSettings:
height: 75%
dialogClass: embedded-content-widget-modal
title: Create embedded content
drupal:
label: Test Plugin
elements:
- <embedded-content>
- <embedded-content data-plugin-config data-plugin-id>
admin_library: testplugin/admin.embedded_content
class: Drupal\testplugin\Plugin\CKEditor5Plugin\EmbeddedContent
library: testplugin/embedded_content
toolbar_items:
# embeddedContent:
testPlugin:
label: My Test Plugin
conditions:
filter: testplugin
index.js:
import EmbeddedContent from "./embeddedcontent";
/**
* @private
*/
export default {
EmbeddedContent: EmbeddedContent
};
embeddedcontent.js:
import { Plugin } from 'ckeditor5/src/core';
import EmbeddedContentEditing from './embeddedcontentediting';
import EmbeddedContentUI from './embeddedcontentui';
/**
* Main entry point to the embedded content.
*/
export default class EmbeddedContent extends Plugin {
/**
* @inheritdoc
*/
static get requires() {
return [
EmbeddedContentEditing,
EmbeddedContentUI
];
}
/**
* @inheritdoc
*/
static get pluginName() {
// return 'embeddedContent';
return 'testPlugin';
}
}
embeddedcontentediting.js:
import {Plugin} from 'ckeditor5/src/core';
import {toWidget, Widget} from 'ckeditor5/src/widget';
import EmbeddedContentCommand from './embeddedcontentcommand';
/**
* Embedded content editing functionality.
*/
export default class EmbeddedContentEditing extends Plugin {
/**
* @inheritdoc
*/
static get requires() {
return [Widget];
}
/**
* @inheritdoc
*/
init() {
this.attrs = {
embeddedContentPluginConfig: 'data-plugin-config',
embeddedContentPluginId: 'data-plugin-id',
};
const options = this.editor.config.get('embeddedContent');
if (!options) {
return;
}
const {previewURL, themeError} = options;
this.previewUrl = previewURL;
this.themeError =
themeError ||
`
<p>${Drupal.t(
'An error occurred while trying to preview the embedded content. Please save your work and reload this page.',
)}<p>
`;
this._defineSchema();
this._defineConverters();
this.editor.commands.add(
'embeddedContent',
new EmbeddedContentCommand(this.editor),
);
}
/**
* Fetches the preview.
*/
async _fetchPreview(modelElement) {
const query = {
plugin_id: modelElement.getAttribute('embeddedContentPluginId'),
plugin_config: modelElement.getAttribute('embeddedContentPluginConfig'),
};
const response = await fetch(
`${this.previewUrl}?${new URLSearchParams(query)}`
);
if (response.ok) {
return await response.text();
}
return this.themeError;
}
/**
* Registers embeddedContent as a block element in the DOM converter.
*/
_defineSchema() {
const schema = this.editor.model.schema;
schema.register('embeddedContent', {
allowWhere: '$block',
isObject: true,
isContent: true,
isBlock: true,
allowAttributes: Object.keys(this.attrs),
});
this.editor.editing.view.domConverter.blockElements.push('embedded-content');
}
/**
* Defines handling of drupal media element in the content lifecycle.
*
* @private
*/
_defineConverters() {
const conversion = this.editor.conversion;
conversion
.for('upcast')
.elementToElement({
view: {
name: 'embedded-content',
},
model: 'embeddedContent',
});
conversion
.for('dataDowncast')
.elementToElement({
model: 'embeddedContent',
view: {
name: 'embedded-content',
},
});
conversion
.for('editingDowncast')
.elementToElement({
model: 'embeddedContent',
view: (modelElement, {writer}) => {
const container = writer.createContainerElement('figure');
return toWidget(container, writer, {
label: Drupal.t('Embedded content'),
});
},
})
.add((dispatcher) => {
const converter = (event, data, conversionApi) => {
const viewWriter = conversionApi.writer;
const modelElement = data.item;
const container = conversionApi.mapper.toViewElement(data.item);
const embeddedContent = viewWriter.createRawElement('div', {
'data-embedded-content-preview': 'loading',
'class': 'embedded-content-preview'
});
viewWriter.insert(viewWriter.createPositionAt(container, 0), embeddedContent);
this._fetchPreview(modelElement).then((preview) => {
if (!embeddedContent) {
return;
}
this.editor.editing.view.change((writer) => {
const embeddedContentPreview = writer.createRawElement(
'div',
{'class': 'embedded-content-preview', 'data-embedded-content-preview': 'ready'},
(domElement) => {
domElement.innerHTML = preview;
},
);
writer.insert(writer.createPositionBefore(embeddedContent), embeddedContentPreview);
writer.remove(embeddedContent);
});
});
};
dispatcher.on('attribute:embeddedContentPluginId:embeddedContent', converter);
return dispatcher;
});
Object.keys(this.attrs).forEach((modelKey) => {
const attributeMapping = {
model: {
key: modelKey,
name: 'embeddedContent',
},
view: {
name: 'embedded-content',
key: this.attrs[modelKey],
},
};
conversion.for('dataDowncast').attributeToAttribute(attributeMapping);
conversion.for('upcast').attributeToAttribute(attributeMapping);
});
}
/**
* @inheritdoc
*/
static get pluginName() {
return 'embeddedContentEditing';
}
}
embeddedcontentui.js:
import { Plugin } from 'ckeditor5/src/core';
import { ButtonView } from 'ckeditor5/src/ui';
import EmbeddedContentIcon from '../theme/icons/embeddedcontent.svg';
import {DomEventObserver} from "ckeditor5/src/engine";
/**
* Ckeditor5 doesn't support double click out of the box.
* Register it here so we can use it.
*
* @Todo Replace double click with a balloon style popup menu to
* edit the embedded content item.
*/
class DoubleClickObserver extends DomEventObserver {
constructor( view ) {
super( view );
this.domEventType = 'dblclick';
}
onDomEvent( domEvent ) {
this.fire( domEvent.type, domEvent );
}
}
/**
* Provides the embedded content button and editing.
*/
export default class EmbeddedContentUI extends Plugin {
init() {
const editor = this.editor;
const options = this.editor.config.get('embeddedContent');
if (!options) {
return;
}
const { dialogURL, openDialog, dialogSettings = {} } = options;
if (!dialogURL || typeof openDialog !== 'function') {
return;
}
editor.ui.componentFactory.add('embeddedContent', (locale) => {
const command = editor.commands.get('embeddedContent');
const buttonView = new ButtonView(locale);
buttonView.set({
label: Drupal.t('Embedded content'),
icon: EmbeddedContentIcon,
tooltip: true,
});
// Bind the state of the button to the command.
buttonView.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled');
this.listenTo(buttonView, 'execute', () => {
openDialog(
dialogURL,
({ attributes }) => {
editor.execute('embeddedContent', attributes);
},
dialogSettings,
);
});
return buttonView;
});
const view = editor.editing.view;
const viewDocument = view.document;
view.addObserver( DoubleClickObserver );
editor.listenTo( viewDocument, 'dblclick', ( evt, data ) => {
const modelElement = editor.editing.mapper.toModelElement( data.target);
if(modelElement && typeof modelElement.name !== 'undefined' && modelElement.name === 'embeddedContent'){
const query = {
plugin_id: modelElement.getAttribute('embeddedContentPluginId'),
plugin_config: modelElement.getAttribute('embeddedContentPluginConfig'),
};
openDialog(
`${dialogURL}?${new URLSearchParams(query)}`,
({ attributes }) => {
editor.execute('embeddedContent', attributes);
},
dialogSettings,
);
}
} );
}
}
embeddedcontentcommand.js:
import { Command } from 'ckeditor5/src/core';
/**
* Creates embedded content
*/
function createEmbeddedContent(writer, attributes) {
return writer.createElement('embeddedContent', attributes);
}
/**
* Command for inserting <embedded-content> tag into ckeditor.
*/
export default class EmbeddedContentCommand extends Command {
execute(attributes) {
const embeddedContentEditing = this.editor.plugins.get('embeddedContentEditing');
// Create object that contains supported data-attributes in view data by
// flipping `DrupalMediaEditing.attrs` object (i.e. keys from object become
// values and values from object become keys).
const dataAttributeMapping = Object.entries(embeddedContentEditing.attrs).reduce(
(result, [key, value]) => {
result[value] = key;
return result;
},
{},
);
// \Drupal\media\Form\EditorMediaDialog returns data in keyed by
// data-attributes used in view data. This converts data-attribute keys to
// keys used in model.
const modelAttributes = Object.keys(attributes).reduce(
(result, attribute) => {
if (dataAttributeMapping[attribute]) {
result[dataAttributeMapping[attribute]] = attributes[attribute];
}
return result;
},
{},
);
this.editor.model.change((writer) => {
this.editor.model.insertContent(
createEmbeddedContent(writer, modelAttributes),
);
});
}
refresh() {
const model = this.editor.model;
const selection = model.document.selection;
const allowedIn = model.schema.findAllowedParent(
selection.getFirstPosition(),
'embeddedContent',
);
this.isEnabled = allowedIn !== null;
}
}
I would appreciate some help understanind what I'm missing or doing wrong.
Thanks in advance.