简体   繁体   中英

Can not input to EditableElement of CKEditor5

In CKEditor5, I tried implementing custom element to convert model to view for editing. Then, editable element(@ckeditor/ckeditor5-engine/src/view/editableelement) in container element(@ckeditor/ckeditor5-engine/src/view/containerelement) is focused on the parent container element and can not be edited.

For example, if it is implemented as follows:

buildModelConverter().for(editing.modelToView)
            .fromElement('myElement')
            .toElement(new ContainerElement('div', {}, [new EditableElement('h4')]));

The result of actual editing dom after inserting 'myElement' and keydown "abc". (I hope inputting text of "abc" to h4 tag but...)

<div>​​​​​​​
  abc
  <h4>
    <br data-cke-filler="true">
  </h4>
</div>

I also tried using widget for applying contenteditable attribute. But, text couldn't be entered in h4.

<div class="ck-widget" contenteditable="false">​​​​​​​
  <h4 class="ck-editable" contenteditable="true">
    <br data-cke-filler="true">
  </h4>
</div>

Is this bug, or I made mistake understanding of container element?

[Additional details]

I am assuming to make a widget plugin for ranking list.

First, the ranking list is structured by <ol> and <li> tags because of having multiple items. I solved that by defining two schema such as "rankingList" and "rankingListItem", so I realized dynamic elements using nested model elements.

const item1 = new ModelElement('rankingListItem');
const item2 = new ModelElement('rankingListItem');
const model = new ModelElement('rankingList', {}, [item1, item2]);
// and insert

Next, the item of ranking list has link, image, title and note. Therefore, the ranking list item has the following DOM structure:

<ol><!-- apply toWidget -->
  <li>
    <a href="link[editable]">
      <img src="image[editable]">
      <h3>title[editable]</h3>
      <p>notes[editable]</p>
    </a>
  </li>
  ...
</ol>

I expect the view element is the following:

const {ref, src, title, notes} = data; // how to get data?
const view = new ContainerElement('a', {ref}, [
    new EmptyElement('img', {src}),
    new EditableElement('h3', {}, new Text(title)),
    new EditableElement('p', {}, new Text(title)),
  ]);
// maybe incorrect ...

In conclusion, I want to use editable view not to break defined DOM tree. How can I realize it?

Thank you for describing your case. It means a lot for us at the moment to know how developers are using the editor and what are your expectations.

Unfortunately, this looks like a very complex feature. It also looks like it would need custom UI (to edit link url and image src -- unless they do not change after added to the editor). It seems that you struggle with two problems:

  • position mapping between the model and the view,
  • nested editables.

First, to answer your question about EditableElement - it seems to be correct to use them for h3 and p elements.

However, such complex feature needs custom converters. Converter builders (which you used) are dedicated to being used in simple cases, like element-to-element conversion, or attribute-to-attribute conversion.

Behind the nice API, build converter is a function factory, that creates one or multiple functions. Those are then added as callbacks to ModelConversionDispatcher (which editor.editing.modelToView is an instance of). ModelConversionDispatcher fires a series of events during the conversion, which is a process of translating a change in the model to the view.

As I've mentioned, you would have to write those converting functions by yourself.

Since this is too big of a subject for a detailed and thorough answer, I'll just briefly present you what you should be interested in. Unfortunately, there are no guides yet about creating custom converters from scratch. This is a very broad subject.

First, let me explain you from where most of your problems come from. As you already know, the editor has three layers: model (data), view (DOM-like structure) and DOM. Model is converted to view and view is rendered to DOM. Also, the other way, when you load data, DOM is converted to view and view is converted to model. This is why you need to provide model-to-view converter and view-to-model converter for your feature.

The important piece of this puzzle is engine.conversion.Mapper . Its role is to map elements and positions from model to view. As you already might have seen, the model might be quite different than the view. Correct position mapping between those is key. When you type a letter at caret position (in DOM), this position is mapped to model and the letter is inserted in the model and only then converted back to the view and DOM. If view-to-model position conversion is wrong, you will not be able to type, or really do anything, at that place.

Mapper is pretty simple on its own. All it needs is that you specify which view elements are bound to which model elements. For example, in the model you might have:

<listItem type="bulleted" indent="0">Foo</listItem>
<listItem type="bulleted" indent="1">Bar</listItem>

While in the view you have:

<ul>
  <li>
    Foo
    <ul>
      <li>Bar</li>
    </ul>
  </li>
</ul>

If the mapper knows that first listItem is bound with first <li> and the second listItem is bound with second <li> , then it is able to translate positions correctly.

Back to your case. Each converter has to provide data for Mapper . Since you used converter builder, the converters build by it already do this. But they are simple, so when you provide:

buildModelConverter().for(editing.modelToView)
        .fromElement('myElement')
        .toElement(new ContainerElement('div', {}, [new EditableElement('h4')]));

it is assumed, that myElement is bound with the <div> . So anything that is written inside that <div> will go straight to myElement and then will be rendered at the beginning of myElement :

<div>
  <h4></h4>
</div>

Assuming that you just wrote x at <h4> , that position will be mapped to myElement offset 0 in the model and then rendered to view at the beginning of <div> .

Model:

<myElement>x</myElement>

View:

<div>x<h4></h4></div>

As you can see, in your case, it is <h4> which should be bound with myElement .

At the moment, we are during refactoring phase. One of the goals is providing more utility functions for converter builders. One of those utilities are converters for elements which have a wrapper element, like in that "div + h4" case above. This is also a case of image feature. The image is represented by <image> in model but it is <figure><img /></figure> in the view. You can look at ckeditor5-image to see how those converters look like now. We want to simplify them.

Unfortunately, your real case is even more complicated because you have multiple elements inside. CKE5 architecture should be able handle your case but you have to understand that this is almost impossible to write without proper guides.

If you want to tinker though, you should study ckeditor5-image repo. It won't be easy, but this is the best way to go. Image plugin together with ImageCaption are very similar to your case.

Model:

<image alt="x" src="y">
  <caption>Foo</caption>
</image>

View:

<figure class="image">
  <img alt="x" src="y" />
  <figcaption>Foo</caption>
</figure>

While in your case, I'd see the model somewhere between those lines:

<rankItem imageSrc="x" linkUrl="y">
  <rankTitle>Foo</rankTitle>
  <rankNotes>Bar</rankNotes>
</rankItem>

And I'd make the view a bit heavier but it will be easier to write converters:

<li contenteditable="false">
  <a href="y">
    <img src="x" />
    <span class="data">
      <span class="title" contenteditable="true">Foo</span>
      <span class="notes" contenteditable="true">Bar</span>
    </span>
  </a>
</li>

For rankTitle and rankNotes - base them on caption element ( ckeditor5-image/src/imagecaption/imagecaptionengine.js ).

For rankItem - base it on image element ( ckeditor5-image/src/image/ ).

Once again - keep in mind that we are in the process of simplifying all of this. We want people to write their own features, even those complicated ones like yours. However, we are aware of how complex it is right now. That's why there are no docs at the moment - we are looking to change and simplify things.

And lastly - you could create that ranking list simpler, using Link plugin and elements build with converter builder:

  • rankList -> <ol class="rank"> ,
  • rankItem -> <li> ,
  • rankImage -> <img /> ,
  • rankNotes -> <span class="notes"> ,
  • rankTitle -> <span class="title"> .

However, it will be possible to mess it up because the whole structure will be editable.

Model:

<rankList>
  <rankItem>
    <rankImage linkHref="" />
    <rankTitle>Foo</rankTitle>
    <rankNotes>Bar</rankNotes>
  </rankItem>
  ...
</rankList>

Where "Foo" and "Bar" also have linkHref attribute set.

View:

<ol class="rank">
  <li>
    <a href=""><img src="" /></a>
    <span class="title"><a href="">Title</a></span>
    <span class="notes"><a href="">Foo</a></span>
  </li>
  ...
</ol>

Something like this, while far from perfect, should be much easier to write as long as we are before the refactor and before writing guides.

Of course you will also have to provide view-to-model converters. You might want to write them on your own (take a look at ckeditor5-list/src/converters.js viewModelConverter() -- although yours will be easier because it will be flat, not nested list). Or you can generate them through converter builder.

Maybe it will be possible to use the approach above (simpler) but use contentEditable attribute to control the structure. rankList would have to be converted to <ol> with contentEditable="false" . Maybe you could somehow use toWidget for better selection handling, for example. rankNotes and rankTitle would have to be converted to element with contentEditable="true" (maybe use toWidgetEditable() ).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM