Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Neos Conference 2019: Neos + CKEditor 5 = Love

Neos Conference 2019: Neos + CKEditor 5 = Love

Sebastian Kurfürst

May 10, 2019
Tweet

More Decks by Sebastian Kurfürst

Other Decks in Technology

Transcript

  1. class MyFeature extends Plugin { init() { // I'm a

    plugin! } } function MyFeature( editor ) { // I'm a plugin as well! }
  2. paragraph paragraph image caption This is very { bold: true

    } important. Nice view! { src: … } Model MV architecture
  3. p p figure img This is very important. { src:

    … } View strong figcaption Nice view! { class: ‘image’ } paragraph paragraph image caption This is very { bold: true } important. Nice view! { src: … } Model
  4. br p p figure img This is very important. {

    src: … } DOM strong figcaption Nice   view! { class: ‘image’ } p p figure img This is very important. { src: … } View strong figcaption Nice view! { class: ‘image’ } paragraph paragraph image caption This is very { bold: true } important. Nice view! { src: … } Model
  5. br p p figure img This is very important. {

    src: … } DOM strong figcaption Nice   view! { class: ‘image’ } p p figure img This is very important. { src: … } View strong figcaption Nice view! { class: ‘image’ } paragraph paragraph image caption This is very { bold: true } important. Nice view! { src: … } Model
  6. MyFeatureEditing == ? • Schema • Converters (model ←→ view)

    • Commands • Misc (keystrokes, observers, ...)
  7. Schema schema.register( 'myBlock', { allowIn: [ '$root', 'section' ], allowAttributes:

    'textAlignment' } ); schema.extend( '$text', { allowAttributes: 'myHighlight' } );
  8. Converters model → view • for editing • getting data

    view → model • loading data • pasting
  9. import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import AttributeCommand from '@ckeditor/ckeditor5-basic-styles/src/attributecommand'; export default

    class Example extends Plugin { init() { this.editor.model.schema.extend('$text', { allowAttributes: ‘highlight' }); const config = { model: 'highlight', view: { name: 'span', classes: 'example-class', styles: { 'background-color': '#00adee', } } }; this.editor.conversion.attributeToElement(config); this.editor.commands.add( ‘highlight', new AttributeCommand(this.editor, ‘highlight’) ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
  10. { "scripts": { "build": "neos-react-scripts build", "watch": "neos-react-scripts watch" },

    "devDependencies": { "@neos-project/neos-ui-extensibility": "*" }, "neos": { "buildTargetDirectory": "../../Public/CustomStylingForCkEditor" }, "dependencies": { "@ckeditor/ckeditor5-basic-styles": "^10.0.0", "@ckeditor/ckeditor5-core": "^10.0.0" } } package.json
  11. UI Bootstrap Sequence Registry time UI plugins loaded plugin manifest

    backend loaded core manifest start user interface registry frozen read from registry immutable
  12. // 1. register CKEditor Plugin, but only if the Node

    Type Formatting option is enabled const addExamplePluginToCkEditorConfiguration = (ckEditorConfiguration, options) => { if ($get(['formatting', 'Neos.Neos.Ui.ExtensibilityExamples:MyCustomSpan'], options.editorOptions)) { ckEditorConfiguration.plugins = ckEditorConfiguration.plugins || []; return $add('plugins', ExamplePlugin, ckEditorConfiguration); } return ckEditorConfiguration; }; const config = globalRegistry.get('ckEditor5').get('config'); config.set('exampleExtension', addExamplePluginToCkEditorConfiguration); manifest.js 1 2 3 4 5 6 7 8 9 10
  13. manifest.js // 2. Add Button to toolbar const richtextToolbar =

    globalRegistry.get('ckEditor5').get('richtextToolbar'); richtextToolbar.set('exampleExtension', { // the path in isActive must match the commandName, to ensure the active state // of the button automatically toggles. isActive: $get('highlightCommand'), isVisible: $get(['formatting', 'Neos.Neos.Ui.ExtensibilityExamples:MyCustomSpan']), component: ExampleButton, icon: 'plus-square', tooltip: 'Mark a span', }, 'before strong'); 1 2 3 4 5 6 7 8 9 10 11
  14. import React, {PureComponent} from 'react'; import {IconButton} from '@neos-project/react-ui-components'; import

    {executeCommand} from '@neos-project/neos-ui-ckeditor5-bindings'; export default class ExampleButton extends PureComponent { handleClick = () => { executeCommand('highlight'); }; render() { return <IconButton onClick={this.handleClick} isActive={Boolean(this.props.isActive)} icon={this.props.icon} />; } } ExampleButton.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
  15. ExamplePlugin.js 1 2 3 4 5 6 7 8 9

    10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import AttributeCommand from '@ckeditor/ckeditor5-basic-styles/src/attributecommand'; export default class Example extends Plugin { init() { this.editor.model.schema.extend('$text', { allowAttributes: ‘highlight' }); const config = { model: 'highlight', view: { name: 'span', classes: 'example-class', styles: { 'background-color': '#00adee', } } }; this.editor.conversion.attributeToElement(config); this.editor.commands.add( ‘highlight', new AttributeCommand(this.editor, ‘highlight’) ); } }
  16. Neos UX issues No shared undo stack No “big selection”

    (copy paste, styling) No caret jumping from node to node 3-step “I want to insert image in the middle of this text node”
  17. Easy wins ✅ Shared undo stack ✅ Big selection possible

    ✅ Caret jumping between nodes ✅ Easy to insert image in the middle of a text
  18. Challenges ⚠ Much closer integration ⚠ Fusion-controlled rendering ⚠ Roundtrips

    to the server (asynchronous conversion) ⚠ Transparent node boundaries
  19. Caption ContentCollection Headline Text Image <p>…</p> <p>…</p> <h1>…</h1> paragraph …

    heading1 … neos-headline neos-text neos-image neos-caption Caption paragraph … Editor model
  20. Caption ContentCollection Headline Text Image <p>…</p> <p>…</p> <h1>…</h1> paragraph …

    heading1 … neos-headline neos-text neos-image neos-caption Caption paragraph … Editor model
  21. const NodeTypes = { // ... Text: { type: ‘textBlock’,

    render() { return parse( ` <section class="node-text"> <div data-editable="main"></div> </section> ` ); } }, // ... };
  22. const NodeTypes = { // ... Image: { type: ‘objectBlock',

    render( properties ) { return parse( ` <figure class="node-image align-${ properties.align }"> <img src="${ properties.url }" alt="${ properties.alt }"> <figcaption data-editable="caption"></figcaption> </figure> ` ); } }, // ... };
  23. [ { name: 'Text', uid: 'fc48c911-f1c5-4145-a4b7-f2d11534efbb', editables: { main: `<h3>Lorem

    ipsum"</h3><p>Dolor""..."</p>` } }, { name: 'Image', uid: '74b269ab-587d-411c-b4b9-2f4cda3d74f4', editables: { caption: '<p>A photo of a kitten."</p>' }, properties: { url: 'http:"//placekitten.com/800/300', alt: 'Random kitten' } } ]