Search code examples
ckeditor5

ckeditor5 list item start number


Trying to build a plugin to set start attribute for ol in ckEditor5.

As best I can tell, the model contains a collection of listItems.

The start attribute would need to be set on the ol however, the parent of the list item, not on the list item itself.. is there a way to access the ol from the model?

I can get the current li with

first(editor.model.document.selection.getSelectedBlocks())

is there a way set an attribute on the ol element?

EDIT -- code to get attribute on LI:

editor.model.schema.extend('listItem', { allowAttributes: 'listStart' });

editor.conversion.attributeToAttribute({ 
  model: 'listStart', 
  view: 'start'
});

enables in the model: <listItem type="numbered" listStart="4">, which will translate to this in the view:

<ol>
  <li start="4">words</li>
</ol>

what I am trying to achieve is

<ol start="4">
  <li>words</li>
</ol>

When I examine source, its looks as though the ol (or ul) is automatically created here:

function generateLiInUl( modelItem, conversionApi ) {

    const mapper = conversionApi.mapper;
    const viewWriter = conversionApi.writer;
    const listType = modelItem.getAttribute( 'listType' ) == 'numbered' ? 'ol' : 'ul';
    const viewItem = createViewListItemElement( viewWriter );
  // ** OL or UL created here -->
    const viewList = viewWriter.createContainerElement( listType, null );
        viewWriter.insert( ViewPosition.createAt( viewList ), viewItem );

    mapper.bindElements( modelItem, viewItem );

    return viewItem;
}

link to source

Is there an event I could observe? Or is there a way in the conversion definition to target the attribute on the parent?

UPDATE 2

If we are going to mod source, we can intercept the downcast by adding this to the generateLiInUl function (thanks MTilsted):

    const listStart = modelItem.getAttribute('listStart');
    if (listStart) {
        viewWriter.setAttribute('start', listStart, viewList);
    }

and to facilitate upcast add this to the viewModelConverter function

    const listStart = data.viewItem.parent.getAttribute('start');
    if (listStart) {
        writer.setAttribute( 'listStart', listStart, listItem );
    }

It's a little ugly in that we are modifying source which is a pita for maintenance, and on upcast we are adding the listStart attribute to every listItem element in the model.. but its a start.

I looked briefly into adding dispatchers.. eg:

data.upcastDispatcher.on( 'element:li', myCustomUpcastFunction );

but could not figure out how to get a reference to listItem element that was added to the model in the viewModelConverter function mentioned above.


Solution

  • The answer that worked for me was very simple and I feel kinda silly not seeing it earlier: Use the LI value attribute instead of the OL start attribute.

    <ol>
      <li value="4">words</li>
    </ol>
    

    instead of:

    <ol start="4">
      <li>words</li>
    </ol>
    

    This keeps the attribute on the listItem and avoids all the complication (I completely missed the existence of the LI's value attribute in my original attempt):

    editor.model.schema.extend('listItem', { allowAttributes: 'value' });
    
    editor.conversion.attributeToAttribute({ 
      model: 'value', 
      view: 'value'
    });
    

    The command to apply the value:

    execute(arg) {
    
      let val = arg.value;
      const model = this.editor.model;
      const block = first(model.document.selection.getSelectedBlocks());
    
      model.change(writer => {
        if (+val) {
          writer.setAttribute('value', val, block);
        } else {
          writer.removeAttribute('value', block);
        }
      });
    
    }
    

    and to prevent the value being copied to next LI on enter:

    editor.commands.get('enter').on('afterExecute', () => {
      const block = first(editor.model.document.selection.getSelectedBlocks());
      if ( block.name == 'listItem' && block.hasAttribute('value')) {
        editor.model.change( writer => {
          writer.removeAttribute('value', block);
        });
      }
    });