`, and `
` elements have the `contentEditable` attribute on them (plus some classes). This attribute tells the browser whether an element is considered "editable". Passing the element through `toWidget()` will make its content non-editable. Conversely, passing it through `toWidgetEditable()` will make its content editable again.
* You can now click the widget (the gray area) to select it. Once it is selected, it is easier to copy-paste it.
* The widget and its nested editable regions react to hovering, selection, and focus (outline).
In other words, the simple box instance became much more responsive.
Additionally, if you call `editor.getData()` you will get the same HTML as before "widgetizing" the simple box. This is thanks to using `toWidget()` and `toNestedEditable()` only in the `editingDowncast` pipeline.
This is all that you need from the model and the view layers for now. In terms of "editability" and data input/output it is fully functional. Now find a way to insert new simple boxes into the document!
## Creating a command
A {@link framework/guides/architecture/core-editor-architecture#commands command} is a combination of an action and a state. You can interact with most of the editor features by commands that they expose. This allows not only executing these features (e.g. bolding a fragment of text) but also checking if this action can be executed in the selection's current location as well as observing other state properties (such as whether the currently selected text is bolded).
In case of the simple box the situation is simple:
* you need an "insert a new simple box" action,
* and "can you insert a new simple box here (at the current selection position)".
Create a new file `insertsimpleboxcommand.js` in the `simplebox/` directory. You will use the {@link module:engine/model/model~Model#insertContent `model.insertContent()`} method which will be able to, for example, split a paragraph if you try to insert a simple box in the middle of it (which is not allowed by the schema).
```js
// simplebox/insertsimpleboxcommand.js
import Command from '@ckeditor/ckeditor5-core/src/command';
export default class InsertSimpleBoxCommand extends Command {
execute() {
this.editor.model.change( writer => {
// Insert * at the current selection position
// in a way that will result in creating a valid model structure.
this.editor.model.insertContent( createSimpleBox( writer ) );
} );
}
refresh() {
const model = this.editor.model;
const selection = model.document.selection;
const allowedIn = model.schema.findAllowedParent( selection.getFirstPosition(), 'simpleBox' );
this.isEnabled = allowedIn !== null;
}
}
function createSimpleBox( writer ) {
const simpleBox = writer.createElement( 'simpleBox' );
const simpleBoxTitle = writer.createElement( 'simpleBoxTitle' );
const simpleBoxDescription = writer.createElement( 'simpleBoxDescription' );
writer.append( simpleBoxTitle, simpleBox );
writer.append( simpleBoxDescription, simpleBox );
// There must be at least one paragraph for the description to be editable.
// See https://github.com/ckeditor/ckeditor5/issues/1464.
writer.appendElement( 'paragraph', simpleBoxDescription );
return simpleBox;
}
```
Import the command and register it in the `SimpleBoxEditing` plugin:
```js
// simplebox/simpleboxediting.js
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import { toWidget, toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils';
import Widget from '@ckeditor/ckeditor5-widget/src/widget';
import InsertSimpleBoxCommand from './insertsimpleboxcommand'; // ADDED
export default class SimpleBoxEditing extends Plugin {
static get requires() {
return [ Widget ];
}
init() {
console.log( 'SimpleBoxEditing#init() got called' );
this._defineSchema();
this._defineConverters();
// ADDED
this.editor.commands.add( 'insertSimpleBox', new InsertSimpleBoxCommand( this.editor ) );
}
_defineSchema() {
// ...
}
_defineConverters() {
// ...
}
}
```
You can now execute this command in order to insert a new simple box. Calling:
```js
editor.execute( 'insertSimpleBox' );
```
Should result in:
{@img assets/img/tutorial-implementing-a-widget-6.png Screenshot of a simple box instance inserted at the beginning of the editor content.}
You can also try inspecting the `isEnabled` property value (or just checking it in CKEditor 5 inspector):
```js
console.log( editor.commands.get( 'insertSimpleBox' ).isEnabled );
```
It is always `true` except when the selection is in one place — in other simple box's title. You can also observe that executing the command when the selection is in that place takes no effect.
Change one more thing before you move forward — disallow `simpleBox` inside `simpleBoxDescription`, too. This can be done by {@link module:engine/model/schema~Schema#addChildCheck defining a custom child check}:
```js
// simplebox/simpleboxediting.js
// ... imports
export default class SimpleBoxEditing extends Plugin {
static get requires() {
return [ Widget ];
}
init() {
console.log( 'SimpleBoxEditing#init() got called' );
this._defineSchema();
this._defineConverters();
this.editor.commands.add( 'insertSimpleBox', new InsertSimpleBoxCommand( this.editor ) );
}
_defineSchema() {
const schema = this.editor.model.schema;
schema.register( 'simpleBox', {
// Behaves like a self-contained object (e.g. an image).
isObject: true,
// Allow in places where other blocks are allowed (e.g. directly in the root).
allowWhere: '$block'
} );
schema.register( 'simpleBoxTitle', {
// Cannot be split or left by the caret.
isLimit: true,
allowIn: 'simpleBox',
// Allow content which is allowed in blocks (i.e. text with attributes).
allowContentOf: '$block'
} );
schema.register( 'simpleBoxDescription', {
// Cannot be split or left by the caret.
isLimit: true,
allowIn: 'simpleBox',
// Allow content which is allowed in the root (e.g. paragraphs).
allowContentOf: '$root'
} );
// ADDED
schema.addChildCheck( ( context, childDefinition ) => {
if ( context.endsWith( 'simpleBoxDescription' ) && childDefinition.name == 'simpleBox' ) {
return false;
}
} );
}
_defineConverters() {
// ...
}
}
```
Now the command should be disabled also when the selection is inside the description of another simple box instance.
## Creating a button
It is time to allow the editor users to insert the widget into the content. The best way to do that is through a UI button in the toolbar. You can quickly create one using the {@link module:ui/button/buttonview~ButtonView `ButtonView`} class brought by the {@link framework/guides/architecture/ui-library UI framework} of CKEditor 5.
The button should execute the [command](#creating-a-command) when clicked and become inactive if the widget cannot be inserted into some particular position of the selection ([as defined in the schema](#defining-the-schema)).
See what it looks like in practice and extend the `SimpleBoxUI` plugin [created earlier](#plugin-structure):
```js
// simplebox/simpleboxui.js
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
export default class SimpleBoxUI extends Plugin {
init() {
console.log( 'SimpleBoxUI#init() got called' );
const editor = this.editor;
const t = editor.t;
// The "simpleBox" button must be registered among the UI components of the editor
// to be displayed in the toolbar.
editor.ui.componentFactory.add( 'simpleBox', locale => {
// The state of the button will be bound to the widget command.
const command = editor.commands.get( 'insertSimpleBox' );
// The button will be an instance of ButtonView.
const buttonView = new ButtonView( locale );
buttonView.set( {
// The t() function helps localize the editor. All strings enclosed in t() can be
// translated and change when the language of the editor changes.
label: t( 'Simple Box' ),
withText: true,
tooltip: true
} );
// Bind the state of the button to the command.
buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );
// Execute the command when the button is clicked (executed).
this.listenTo( buttonView, 'execute', () => editor.execute( 'insertSimpleBox' ) );
return buttonView;
} );
}
}
```
The last thing you need to do is tell the editor to display the button in the toolbar. To do that, you will need to slightly modify the code that runs the editor instance and include the button in the {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar configuration}:
```js
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Essentials, Paragraph, Heading, List, Bold, Italic, SimpleBox ],
// Insert the "simpleBox" button into the editor toolbar.
toolbar: [ 'heading', 'bold', 'italic', 'numberedList', 'bulletedList', 'simpleBox' ]
} )
.then( editor => {
// ...
} )
.catch( error => {
// ...
} );
```
Refresh the web page and try it yourself:
{@img assets/img/tutorial-implementing-a-widget-7.png Screenshot of the simple box widget being inserted using the toolbar button.}
## Demo
You can see the block widget implementation in action in the editor below. You can also check out the full [source code](#full-source-code) of this tutorial if you want to develop your own block widgets.
{@snippet framework/tutorials/block-widget}
## Full source code
The following code contains a complete implementation of the `SimpleBox` plugin (and all its dependencies) and the code to run the editor. You can paste it into the [`app.js`](#plugin-structure) file and it will run out–of–the–box:
```js
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
import List from '@ckeditor/ckeditor5-list/src/list';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import { toWidget, toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils';
import Widget from '@ckeditor/ckeditor5-widget/src/widget';
import Command from '@ckeditor/ckeditor5-core/src/command';
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
class SimpleBox extends Plugin {
static get requires() {
return [ SimpleBoxEditing, SimpleBoxUI ];
}
}
class SimpleBoxUI extends Plugin {
init() {
console.log( 'SimpleBoxUI#init() got called' );
const editor = this.editor;
const t = editor.t;
// The "simpleBox" button must be registered among the UI components of the editor
// to be displayed in the toolbar.
editor.ui.componentFactory.add( 'simpleBox', locale => {
// The state of the button will be bound to the widget command.
const command = editor.commands.get( 'insertSimpleBox' );
// The button will be an instance of ButtonView.
const buttonView = new ButtonView( locale );
buttonView.set( {
// The t() function helps localize the editor. All strings enclosed in t() can be
// translated and change when the language of the editor changes.
label: t( 'Simple Box' ),
withText: true,
tooltip: true
} );
// Bind the state of the button to the command.
buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );
// Execute the command when the button is clicked (executed).
this.listenTo( buttonView, 'execute', () => editor.execute( 'insertSimpleBox' ) );
return buttonView;
} );
}
}
class SimpleBoxEditing extends Plugin {
static get requires() {
return [ Widget ];
}
init() {
console.log( 'SimpleBoxEditing#init() got called' );
this._defineSchema();
this._defineConverters();
this.editor.commands.add( 'insertSimpleBox', new InsertSimpleBoxCommand( this.editor ) );
}
_defineSchema() {
const schema = this.editor.model.schema;
schema.register( 'simpleBox', {
// Behaves like a self-contained object (e.g. an image).
isObject: true,
// Allow in places where other blocks are allowed (e.g. directly in the root).
allowWhere: '$block'
} );
schema.register( 'simpleBoxTitle', {
// Cannot be split or left by the caret.
isLimit: true,
allowIn: 'simpleBox',
// Allow content which is allowed in blocks (i.e. text with attributes).
allowContentOf: '$block'
} );
schema.register( 'simpleBoxDescription', {
// Cannot be split or left by the caret.
isLimit: true,
allowIn: 'simpleBox',
// Allow content which is allowed in the root (e.g. paragraphs).
allowContentOf: '$root'
} );
schema.addChildCheck( ( context, childDefinition ) => {
if ( context.endsWith( 'simpleBoxDescription' ) && childDefinition.name == 'simpleBox' ) {
return false;
}
} );
}
_defineConverters() {
const conversion = this.editor.conversion;
// converters
conversion.for( 'upcast' ).elementToElement( {
model: 'simpleBox',
view: {
name: 'section',
classes: 'simple-box'
}
} );
conversion.for( 'dataDowncast' ).elementToElement( {
model: 'simpleBox',
view: {
name: 'section',
classes: 'simple-box'
}
} );
conversion.for( 'editingDowncast' ).elementToElement( {
model: 'simpleBox',
view: ( modelElement, viewWriter ) => {
const section = viewWriter.createContainerElement( 'section', { class: 'simple-box' } );
return toWidget( section, viewWriter, { label: 'simple box widget' } );
}
} );
// converters
conversion.for( 'upcast' ).elementToElement( {
model: 'simpleBoxTitle',
view: {
name: 'h1',
classes: 'simple-box-title'
}
} );
conversion.for( 'dataDowncast' ).elementToElement( {
model: 'simpleBoxTitle',
view: {
name: 'h1',
classes: 'simple-box-title'
}
} );
conversion.for( 'editingDowncast' ).elementToElement( {
model: 'simpleBoxTitle',
view: ( modelElement, viewWriter ) => {
// Note: You use a more specialized createEditableElement() method here.
const h1 = viewWriter.createEditableElement( 'h1', { class: 'simple-box-title' } );
return toWidgetEditable( h1, viewWriter );
}
} );
// converters
conversion.for( 'upcast' ).elementToElement( {
model: 'simpleBoxDescription',
view: {
name: 'div',
classes: 'simple-box-description'
}
} );
conversion.for( 'dataDowncast' ).elementToElement( {
model: 'simpleBoxDescription',
view: {
name: 'div',
classes: 'simple-box-description'
}
} );
conversion.for( 'editingDowncast' ).elementToElement( {
model: 'simpleBoxDescription',
view: ( modelElement, viewWriter ) => {
// Note: You use a more specialized createEditableElement() method here.
const div = viewWriter.createEditableElement( 'div', { class: 'simple-box-description' } );
return toWidgetEditable( div, viewWriter );
}
} );
}
}
class InsertSimpleBoxCommand extends Command {
execute() {
this.editor.model.change( writer => {
// Insert * at the current selection position
// in a way that will result in creating a valid model structure.
this.editor.model.insertContent( createSimpleBox( writer ) );
} );
}
refresh() {
const model = this.editor.model;
const selection = model.document.selection;
const allowedIn = model.schema.findAllowedParent( selection.getFirstPosition(), 'simpleBox' );
this.isEnabled = allowedIn !== null;
}
}
function createSimpleBox( writer ) {
const simpleBox = writer.createElement( 'simpleBox' );
const simpleBoxTitle = writer.createElement( 'simpleBoxTitle' );
const simpleBoxDescription = writer.createElement( 'simpleBoxDescription' );
writer.append( simpleBoxTitle, simpleBox );
writer.append( simpleBoxDescription, simpleBox );
// There must be at least one paragraph for the description to be editable.
// See https://github.com/ckeditor/ckeditor5/issues/1464.
writer.appendElement( 'paragraph', simpleBoxDescription );
return simpleBox;
}
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Essentials, Bold, Italic, Heading, List, Paragraph, SimpleBox ],
toolbar: [ 'heading', '|', 'bold', 'italic', 'numberedList', 'bulletedList', 'simpleBox' ],
} )
.then( editor => {
console.log( 'Editor was initialized', editor );
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
```