0

I want to add "decoration" (non semantic) elements into the editing view at specific positions.

I made a small change in the Block-Widget Plugin given in the CKEditor5 tutorial https://ckeditor.com/docs/ckeditor5/latest/framework/guides/tutorials/implementing-a-block-widget.html

I added 3 lines ("const badge" etc...) into the downcast converter code:

_defineConverters() {
    const conversion = this.editor.conversion;

    // <simpleBox> 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' } );

            const badge = viewWriter.createContainerElement( 'span', { class: 'cause-badge', style: 'font-weight: bold' } );
            viewWriter.insert( viewWriter.createPositionAt( badge, 0 ), viewWriter.createText('Simple Box') );
            viewWriter.insert( viewWriter.createPositionAt( section, 0), badge );

            return toWidget( section, viewWriter, { label: 'simple box widget' } );
        }
    } );
//....

I was expecting the "Simple box" at the begining (top) of the widget, since I use createPositionAt( section, 0). But instead, I get it at the end (bottom) of the widget (on Firefox). screenshot on Firefox

  • Hi. Its probably due to the time of the conversion happens. When the simpleBox is converted the badge is inserted inside at the beginning but later on the children of the simpleBox are converted and they are inserted on the positions as in the model. Now to overcome this there are couple of ways but one of them is to create different structure for the main box (ie wrap the children in some additional div or use a view post-fixer to move the badge to the end. – jodator Jun 12 at 14:37
  • Another way is to create a model element for the badge and define a conversion for it - it might be usable if it represents some editable value. – jodator Jun 12 at 14:41
  • Thanks @jodator for your answer. I agree with you about the explanation of the bad result. For me this behavior is counterintuitive. There is maybe an opportunity to improve the CKEditor-5 behaviour ? Unfortunately, I have not yet achieved to solve my issue. 1/ I need the badge as part of the simple-box view, so putting it in the upper box is not a solution. 2/ I do not want to put view elements in the model, neither. 3/ I tried to use a writer post-fixer. The first display is ok. But each click on the interface calls the post-fixer and inserts a new badge again and again. – Je Ge Jun 13 at 9:41
  • In the post-fixer you can check if the element is already added and then do not add it again. – jodator yesterday
  • Also if a badge contains any data you can put it in the model to represent it somehow. Also the conversion would be straightforward. – jodator yesterday
0

I think that the easiest approach is to define this star rating element in the model and add proper conversion for it.

For editing pipeline you'd need and UIElement that allows to render in the editor editing area arbitrary HTML using render function. You can check some examples of using render function in custom UI elements POC.

Simple stub of such functionality below:

// Define schema element
schema.register( 'simpleBoxStars', {
    allowIn: 'simpleBox',
    allowAttributes: [ 'rating' ]
} );

conversion.for( 'upcast' ).elementToElement( {
    model: ( viewElement, writer ) => {
        return writer.createElement( 'simpleBoxStars', { rating: viewElement.getAttribute( 'data-rating' ) } );
    },
    view: {
        name: 'span',
        classes: 'simple-box-stars'
    }
} );

conversion.for( 'editingDowncast' ).elementToElement( {
    model: 'simpleBoxStars',
    view: ( modelElement, writer ) => {
        const uiSpan = writer.createUIElement( 'span', { class: 'simple-box-stars' }, function( domDocument ) {
            const domElement = this.toDomElement( domDocument );

            // Customize this for you needs
            domElement.innerHTML = `This many stars: <span class="stars">${ modelElement.getAttribute( 'rating' ) }</span>`;


            //
            domElement.addEventListener( 'click', evt => {
                // detect which star was clicked
                editor.model.change( writer => {
                    // Change model elment value, ie by using .setAttribute()
                } );
            } );

            return domElement;
        } );

        return uiSpan;
    }
} );

// For editor.getData() just output the data normally:
conversion.for( 'dataDowncast' ).elementToElement( {
    model: 'simpleBoxStars',
    view: {
        name: 'span',
        classes: 'simple-box-stars'
    }
} );

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.