将参数从命令传递到转换器

Passing parameters from Command to Converter

我定义了一种新型的模型元素作为插件;我们将其称为 Foo。模型中的 Foo 节点应转换为视图中的 section 元素。到目前为止,一切都很好。我设法通过定义简单的转换规则来做到这一点。我还设法定义了一个新的 FooCommand 将选定的块转换(重命名)为 Foo.

我试图将那些 Foo 模型节点上的属性转换为视图元素上的属性(反之亦然)。假设 Foos 有一个名为 fooClass 的属性,它应该映射到视图元素的 class 属性。

<Foo fooClass="green-foo"> should map to/from <section class="green-foo">

我可以在FooCommand中成功接收参数,但我似乎无法在命令正在处理的块上设置它们:

execute(options = {}) {
    const document = this.editor.document;
    const fooClass = options.fooClass;

    document.enqueueChanges(() => {
        const batch = options.batch || document.batch();
        const blocks = (options.selection || document.selection).getSelectedBlocks();

        for (const block of blocks) {
            if (!block.is('foo')) {
                batch.rename(block, 'foo');
                batch.setAttribute(block, 'fooClass', fooClass);
            }
        }
    });
}

下面是Foo插件中init函数的代码,包括模型→视图和视图→模型转换:

init() {
    const editor = this.editor;
    const doc = editor.document;
    const data = editor.data;
    const editing = editor.editing;

    editor.commands.add('foo', new FooCommand(editor));
    doc.schema.registerItem('foo', '$block');

    buildModelConverter().for(data.modelToView, editing.modelToView)
        .fromElement('foo')
        .toElement(modelElement => {
            const fooClass = modelElement.item.getAttribute('fooClass'));
            return new ContainerElement('section', {'class': fooClass});
        });

    buildViewConverter().for(data.viewToModel)
        .fromElement('section')
        .toElement(viewElement => {
            let classes = Array.from(viewElement.getClassNames());
            let modelElement = new ModelElement('foo', {'fooClass': classes[0]});
            return modelElement;
        });

}

当我尝试通过

运行 命令时
editor.execute('foo', { fooClass: 'green-foo' })

我可以看到green-foo值对FooCommand可用,但是模型→视图转换中的modelElement却没有fooClass属性。

我敢肯定我错过了这里的要点并且滥用了 APIs。如果有人能阐明这个问题,我将非常感激。我可以根据需要提供更多详细信息。

初步建议后跟进

感谢@Reinmar 和@jodator 关于配置文档架构以允许自定义属性的建议。我真的以为那会解决它,但没有。无论如何,这可能是一个必要的步骤,但在模型→视图转换过程中,我仍然无法从模型元素中获取属性值。

首先,让我补充一条我遗漏的重要信息:我正在使用的 CKEditor5 版本是 1.0.0-alpha2。我知道有几个 APIs 肯定会改变,但我仍然希望使用当前版本。

模型→视图转换

如果我理解正确,可以将 stringfunction 传递给 toElement 调用。关于使用后者的一个问题:传递给函数的参数到底是什么?我假设它是要转换的模型元素(节点?)。是这样吗?如果是这样,为什么在请求时无法通过 batch.setAttribute(在 document.enqueueChanges 内)在该节点上设置的属性?应该吗?

排序问题?

其他测试似乎表明存在某种 执行顺序 问题。我观察到,即使当我第一次尝试从 modelElement 参数读取该属性时该属性不可用,但如果我稍后再次读取它,它就会如此。让我试着说明下面的情况。首先,我将修改转换代码,使其使用一些虚拟值,以防读取时属性值不可用:

buildModelConverter().for(data.modelToView, editing.modelToView)
    .fromElement('foo')
    .toElement(modelElement => {
        let fooClass = modelElement.item.getAttribute('fooClass') || 'naught';
        let viewElement = new ContainerElement('section');
        viewElement.setAttribute('class', fooClass);
        return viewElement;
    });

现在我重新加载页面并在控制台上执行以下指令:

c = Array.from(editor.document.getRoot().getChildren());

c[1].is('paragraph'); // true

// Changing the node from 'paragraph' to 'foo' and adding an attribute
// 'fooClass' with value 'green-foo' to it.
editor.document.enqueueChanges(() => {
    const batch = editor.document.batch();
    batch.rename(c[1], 'foo');
    batch.setAttribute(c[1], 'fooClass', 'green-foo');
    return batch;
});

c[1].is('paragraph'); // false
c[1].is('foo'); // true

c[1].hasAttribute('fooClass'); // true
c[1].getAttribute('fooClass'); // 'green-foo'

尽管看起来正在产生预期的输出,但看一眼生成的视图元素就会发现问题所在:

<section class="naught"/>

最后,即使我尝试重置模型元素上的 fooClass 属性,更改也不会反映在视图元素上。这是为什么?通过 enqueueChanges 所做的更改不应该导致视图更新吗?

很抱歉拖了这么久 post,但我会尽力传达尽可能多的细节。希望有人会发现我的错误或对 CKEditor 5 API 实际工作方式的误解。

视图未更新?

我转向 Document's events 并尝试了 changesDone 事件。它成功地解决了 "timing" 问题,因为它始终只在处理完所有更改后才会触发。尽管如此,视图未响应模型更改而更新的问题仍然存在。明确地说,模型确实发生了变化,但视图并没有反映出这一点。这是电话:

editor.document.enqueueChanges(() => editor.document.batch().setAttribute(c[1], 'fooClass', 'red-foo'));

为了 100% 确定我自己编写了整个功能。我使用的是 1.0.0-beta.1 API,它与您使用的完全不同。

基本上 - 它有效。这还不是 100% 正确,但我会解决的。

如何转换元素+属性对?

实现需要转换元素 + 属性的功能时,需要单独处理元素和属性转换,因为它们在 CKEditor 5 中是分开处理的。

因此,在下面的代码中你会发现我使用了 elementToElement():

editor.conversion.elementToElement( {
    model: 'foo',
    view: 'section'
} );

模型的 <foo> 元素和视图的 <section> 元素之间的转换器。这是一个 two-way 转换器,因此它处理向上转换(视图 -> 模型)和向下转换(模型 -> 视图)转换。

注意:它不处理属性。

理论上,与 view 属性 一样,您可以编写一个回调来读取模型元素的属性并创建也设置了该属性的视图元素。但这行不通,因为这样的配置只有在向下转换(模型 -> 视图)的情况下才有意义。我们如何使用该回调来向下转换视图结构?

注意:您可以分别为向下转换和向上转换管道编写转换器(通过使用 editor.conversion.for()),在这种情况下您可以真正使用回调。但在这种情况下它并没有真正意义。

属性可以独立改变!

另一个问题是假设您编写了一个同时设置属性的元素转换器。 Tada,你加载 <section class=ohmy> 并在你的模型中得到 <foo class=ohmy>。

但是...如果模型中的属性会发生变化怎么办?

在向下转换管道中,CKEditor 5 将元素更改与属性更改分开处理。它将它们作为单独的事件触发。因此,当您的 FooCommand 在标题上执行时它调用 writer.rename() 并且我们在 DowncastDispatcher 中得到以下事件:

  1. remove<heading>
  2. insert:section

但是属性也改变了(writer.setAttribute()),所以我们也得到:

  1. setAttibute:class:section

elementToElement() 转换助手监听 insert:section 事件。所以是瞎了setAttribute:class:selection.

因此,当您改变属性的值时,您需要attributeToAttribute()转换。

测序

我不想在我们发布 1.0.0-beta.1 之前回答您的问题,因为 1.0.0-beta.1 带来了 Differ.

在 1.0.0-beta.1 之前,所有更改在应用时都会立即转换。因此,rename() 会立即导致 removeinsert:section 事件。此时,您在后一个中获得的元素还没有设置 class 属性。

多亏了 Differ,我们能够在应用所有更改后(在执行 change() 块之后)开始转换。这意味着一旦模型 <foo> 元素已经设置了 class 属性,就会触发 insert:section 事件。这就是为什么你可以写一个 callback-based 转换器......但是你不应该 :D

密码

import { downcastAttributeToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
import { upcastAttributeToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';

class FooCommand extends Command {
    execute( options = {} ) {
        const model = this.editor.model;
        const fooClass = options.class;

        model.change( writer => {
            const blocks = model.document.selection.getSelectedBlocks();

            for ( const block of blocks ) {
                if ( !block.is( 'foo' ) ) {
                    writer.rename( block, 'foo' );
                    writer.setAttribute( 'class', fooClass, block );
                }
            }
        } );
    }
}

class FooPlugin extends Plugin {
    init() {
        const editor = this.editor;

        editor.commands.add( 'foo', new FooCommand( editor ) );

        editor.model.schema.register( 'foo', {
            allowAttributes: 'class',
            inheritAllFrom: '$block'
        } );

        editor.conversion.elementToElement( {
            model: 'foo',
            view: 'section'
        } );

        editor.conversion.for( 'upcast' ).add(
            upcastAttributeToAttribute( {
                model: 'class',
                view: 'class'
            } )
        );

        editor.conversion.for( 'downcast' ).add(
            downcastAttributeToAttribute( {
                model: 'class',
                view: 'class'
            } )
        );

        // This should work but it does not due to https://github.com/ckeditor/ckeditor5-engine/issues/1379 :(((    
        // EDIT: The above issue is fixed and will be released in 1.0.0-beta.2.
        // editor.conversion.attributeToAttribute( {
        //  model: {
        //      name: 'foo',
        //      key: 'class'
        //  },
        //  view: {
        //      name: 'section',
        //      key: 'class'
        //  }
        // } );
    }
}

这段代码工作得很好,除了它在任何可能具有它的元素上转换 class 属性这一事实。那是因为我不得不使用非常通用的 downcastAttributeToAttribute()upcastAttributeToAttribute() 转换器,因为 bug that I found 编辑:它是固定的,将在 1.0.0-beta.2 中可用)。如果一切正常并且它将在 1.0.0-beta.2 中工作,则注释掉的代码是您应该如何定义它。

很遗憾我们错过了这样一个简单的案例,但这主要是因为我们所有的特征...都比这复杂得多。