如何将 "target" 属性添加到 ckeditor5 中的 `a` 标签?

How to add "target" attribute to `a` tag in ckeditor5?

我已经为 link 创建了自己的插件。现在我想向插件生成的 a 标签添加一些其他属性,例如 targetrel.

但是我无法完成它。这是转换器的我的插件代码。 我应该添加什么转换器以便 a 标签可以支持其他属性?

/**
 * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md.
 */

/**
 * @module link/linkediting
 */

import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting';
import {
    downcastAttributeToElement
} from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
import { upcastElementToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
import LinkCommand from './uclinkcommand';
import UnlinkCommand from './ucunlinkcommand';
import { createLinkElement } from '@ckeditor/ckeditor5-link/src/utils';
import { ensureSafeUrl } from './utils';
import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute';

/**
 * The link engine feature.
 *
 * It introduces the `linkHref="url"` attribute in the model which renders to the view as a `<a href="url">` element.
 *
 * @extends module:core/plugin~Plugin
 */
export default class UcLinkEditing extends LinkEditing {
    /**
     * @inheritDoc
     */
    init() {
        const editor = this.editor;

        // Allow link attribute on all inline nodes.
        editor.model.schema.extend( '$text', { allowAttributes: 'linkHref' } );

        editor.conversion.for( 'dataDowncast' )
            .add( downcastAttributeToElement( { model: 'linkHref', view: createLinkElement } ) );

        editor.conversion.for( 'editingDowncast' )
            .add( downcastAttributeToElement( { model: 'linkHref', view: ( href, writer ) => {
                return createLinkElement( ensureSafeUrl( href ), writer );
            } } ) );

        editor.conversion.for( 'upcast' )
            .add( upcastElementToAttribute( {
                view: {
                    name: 'a',
                    attribute: {
                        href: true
                    }
                },
                model: {
                    key: 'linkHref',
                    value: viewElement => viewElement.getAttribute( 'href' )
                }
            } ) );

        // Create linking commands.
        editor.commands.add( 'ucLink', new LinkCommand( editor ) );
        editor.commands.add( 'ucUnlink', new UnlinkCommand( editor ) );

        // Enable two-step caret movement for `linkHref` attribute.
        bindTwoStepCaretToAttribute( editor.editing.view, editor.model, this, 'linkHref' );

        // Setup highlight over selected link.
        this._setupLinkHighlight();
    }
}

简介

在开始编写代码之前,我想借此机会解释一下 CKEditor 5 内联元素(如 <a>)的方法,以便解决方案更容易理解。有了这些知识,以后类似的问题就不会麻烦了。以下是一个全面的教程,因此请阅读很长时间。

即使您可能了解理论部分的大部分内容,我还是建议您阅读它以全面了解 CKEditor 5 中的工作原理。

另外,请注意,我将为原始 CKEditor 5 插件提供一个解决方案,因为它对寻求有关此问题的教程的其他社区成员更有价值。不过,我希望通过本教程的洞察力,您将能够根据您的自定义插件调整代码示例。

此外,请记住,本教程不讨论此插件的 UI 部分,仅讨论应如何配置以进行转换。添加和删​​除属性是 UI 或代码其他部分的工作。这里我只讨论引擎的东西。

CKEditor 5 中的内联元素

首先,让我们确定哪些元素是内联的。通过内联元素,我理解 <strong><a><span> 等元素。与 <p><blockquote><div> 不同,内联元素不会构造数据。相反,它们以特定的(视觉和语义)方式标记一些文本。因此,在某种程度上,这些元素是文本给定部分的特征。因此,我们说文本的给定部分是粗体,或者文本的给定部分 is/has a link.

同样,在模型中,我们不直接将 <a><strong> 表示为元素。相反,我们允许将属性添加到文本的一部分。这就是文本特征(如粗体、斜体或 link)的表示方式。

例如,在模型中,我们可能有一个带有 Foo bar 文本的 <paragraph> 元素,其中 barbold 属性设置为 true.我们会这样记下来:<paragraph>Foo <$text bold="true">bar</$text></paragraph>。看,那里没有 <strong> 或任何其他附加元素。它只是一些带有属性的文本。稍后,bold 属性被转换为 <strong> 元素。

顺便说一下:来自模型属性的视图元素有它们自己的 class: view.AttributeElement 而不是内联元素也可以称为属性元素。可悲的是,名称与 "attribute" 作为视图元素的属性冲突(更糟糕的是,属性元素允许具有属性)。

当然,文本可能有多个属性,它们都被转换为各自的视图内联元素。请记住,在模型中,属性没有任何固定顺序。这与视图或 HTML 相反,其中内联元素一个接一个地嵌套。嵌套发生在从模型到视图的转换过程中。这使得模型中的工作更简单,因为特征不需要处理模型中的元素的破坏或重新排列。

考虑这个模型字符串:

<paragraph>
    <$text bold="true">Foo </$text>
    <$text bold="true" linkHref="bar.html">bar</$text>
    <$text bold="true"> baz</$text>
</paragraph>

它是粗体 Foo bar baz 文本,在 bar 上带有 link。转换时会转换为:

<p>
    <strong>Foo </strong><a href="bar.html"><strong>bar</strong></a><strong> baz</strong>
</p>

请注意,<a> 元素的转换方式始终是最顶层元素。这是故意的,这样 none 元素将永远破坏 <a> 元素。看到这个,不正确的 view/HTML 字符串:

<p>
    <a href="bar.html">Foo </a><strong><a href="bar.html">bar</a></strong>
</p>

生成的view/HTML有两个link元素相邻,这是错误的。

我们使用 priority 属性 of view.AttributeElement 来定义哪个元素应该在其他元素之上。大多数元素,如 <strong> 不关心它并保持默认优先级。但是,<a> 元素已更改优先级以保证 view/HTML.

中的正确顺序

复杂的内联元素和合并

到目前为止,我们主要讨论了更简单的内联元素,即没有属性的元素。例如 <strong><em>。相反,<a> 有额外的属性。

很容易想出需要 mark/style 文本的一部分但足够自定义的功能,因此仅使用标签是不够的。一个例子是字体系列功能。使用时,它会将 fontFamily 属性添加到文本,稍后将其转换为具有适当 style 属性的 <span> 元素。

此时,你要问如果在一个文本的同一部分设置多个这样的属性会怎样?以此模型为例:

<paragraph>
    <$text fontFamily="Tahoma" fontSize="big">Foo</$text>
</paragraph>

以上属性转换如下:

  • fontFamily="value" 转换为 <span style="font-family: value;">,
  • fontSize="value" 转换为 <span class="text-value">.

那么,我们可以期待什么样的view/HTML?

<p>
    <span style="font-family: Tahoma;">
        <span class="text-big">Foo</span>
    </span>
</p>

然而,这似乎是错误的。为什么不只有一个 <span> 元素?这样不是更好吗?

<p>
    <span style="font-family: Tahoma;" class="text-big">Foo</span>
</p>

为了解决这些情况,在CKEditor 5的转换机制中,我们实际上引入了合并机制。

在上面的场景中,我们有两个转换为 <span> 的属性。当第一个属性(比如 fontFamily 被转换时,视图中还没有 <span>。因此 <span> 添加了 style 属性。但是,当 fontSize被转换,视图中已经有<span>view.Writer识别这个并检查这些元素是否可以合并。规则是三个:

  • 元素必须具有相同的view.Element#name,
  • 元素必须具有相同的view.AttributeElement#priority,
  • 两个元素都不能设置 view.AttributeElement#id

我们还没有讨论 id 属性 但是,为了简单起见,我现在不会讨论它。足以说明一些属性元素防止合并很重要

正在向 link

添加另一个属性

至此,应该很清楚如何向 <a> 元素添加另一个属性了。

所有需要做的就是定义一个新的模型属性(linkTargetlinkRel)并使其转换为具有所需(target="...")的 <a> 元素或 rel="...") 属性。然后,它将与原始 <a href="..."> 元素合并。

请记住,来自原始 CKEditor 5 link 插件的 <a> 元素已指定自定义 priority。这意味着新插件生成的元素需要指定相同的优先级才能正确合并。

向上转换合并的属性元素

目前,我们只讨论了向下转型(即从模型转换为视图)。现在让我们谈谈向上转换(即从视图转换为模型)。幸运的是,它比上一部分更容易。

有两个 "things" 可以向上转换 - 元素和属性。这里没有魔法——元素就是元素(<p><a><strong> 等),属性就是属性(class=""href="" 等)。

元素可以向上转换为元素 (<p> -> <paragraph>) 或属性 (<strong> -> bold, <a> -> linkHref).属性可以向上转换为属性。

我们的示例显然需要从元素向上转型到属性。实际上,<a> 元素被转换为 linkHref 属性,并且 linkHref 属性值取自 <a> 元素的 href="" 属性。

自然地,人们会为他们的新 linkTargetlinkRel 属性定义相同的转换。然而,这里有一个陷阱。视图的每个部分只能转换("consumed")一次(向下转换时模型也是如此)。

这是什么意思?简单地说,如果给定 元素名称 或给定元素属性已经转换了一个特征,那么这两个特征都不能转换它。这样功能就可以正确地相互覆盖。这也意味着可以引入通用转换器(例如,如果没有其他功能将 <div> 识别为可以由该功能转换的内容,则 <div> 可以转换为 <paragraph>)。这也有助于发现冲突的转换器。

回到我们的例子。我们不能定义两个转换相同元素 (<a>) 的元素到属性转换器并期望它们同时一起工作。一个会覆盖另一个。

由于我们不想更改原始的 link 插件,因此我们需要保持该转换器不变。但是,新插件的向上转换转换器将是一个属性到属性转换器。由于该转换器不会转换元素(或者更确切地说,元素名称),它将与原始转换器一起工作。

代码示例

这里是 link 目标插件的代码示例。下面我会解释其中的一部分。

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import { downcastAttributeToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
import { upcastAttributeToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';

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

        editor.model.schema.extend( '$text', { allowAttributes: 'linkTarget' } );

        editor.conversion.for( 'downcast' ).add( downcastAttributeToElement( {
            model: 'linkTarget',
            view: ( attributeValue, writer ) => {
                return writer.createAttributeElement( 'a', { target: attributeValue }, { priority: 5 } );
            },
            converterPriority: 'low'
        } ) );

        editor.conversion.for( 'upcast' ).add( upcastAttributeToAttribute( {
            view: {
                name: 'a',
                key: 'target'
            },
            model: 'linkTarget',
            converterPriority: 'low'
        } ) );
    }
}

对于这么长的教程来说,它肯定是一个小片段。希望其中大部分是不言自明的。

首先,我们通过定义文本允许的新属性 linkTarget 来扩展 Schema

然后,我们定义向下转换。 downcastAttributeToElement 用于创建将与原始 <a> 元素合并的 <a target="..."> 元素。请记住,此处创建的 <a> 元素的优先级定义为 5,就像在原始 link 插件中一样。

最后一步是向上转换。 upcastAttributeToAttribute helper 被使用,如前所述。在view配置中,指定只转换<a>元素的target属性(name: 'a')。这并不意味着 <a> 元素将被转换!这只是转换器的过滤配置,因此它不会转换其他元素的 target 属性。

最后,两个转换器都以低于原始转换器的优先级添加,以防止任何假设性问题。

以上示例适用于 ckeditor5-engineckeditor5-link 的当前母版。

当我在 2022 年遇到同样的问题时,我发现这个答案非常有帮助,我想添加 id 属性但没有创建自己的插件,我只是编辑了 Link ckeditor5-build-classic 包中的插件然后我 re-builded 它。

1- 在 @module link/linkediting 中:

  • 允许所有内联节点上的 link 属性。
    • editor.model.schema.extend( '$text', { allowAttributes: ['linkHref', 'linkId'] } );
  • upcast 添加转换以保留现有 id 属性或创建新属性:
editor.conversion.for( 'upcast' ).attributeToAttribute( {
    view: {
        name: 'a'
    },
    model: {
        key: 'linkId',
        value: viewElement => {
            let id = viewElement.getAttribute( 'id' );
            if (id)
                return id;
            return 'id_'+Math.floor(Math.random() * 10000)
        }
    },
    converterPriority: 'low'
} ) ;  
  • editingDowncast 添加转换,以将模型转换为视图:
editor.conversion.for( 'editingDowncast' ).attributeToElement( {
        model: 'linkId',
        view: ( attributeValue, conversionApi ) => {
            return conversionApi.writer.createAttributeElement( 'a', { id: attributeValue }, { priority: 5 } );
        },
        converterPriority: 'low'
    } ) ;
  • dataDowncast 添加转换,以在调用 getDate() 时获取属性:
editor.conversion.for( 'dataDowncast' )
    .attributeToElement( {
        model: 'linkId',
        view: ( attributeValue, conversionApi ) => {
            return conversionApi.writer.createAttributeElement( 'a', { id: attributeValue }, { priority: 5 } );
        }
    } ) ;

2- in @module link/linkcommand :为了创建 id,我只想要一个随机字符串,所以我没有在表单中创建任何新字段,只是在该行之后添加了这一行负责linkHref属性

writer.setAttribute( 'linkId', 'id_'+Math.floor(Math.random() * 10000), range );