Contribute to this guide

指南提及(自动完成)

提及功能支持基于用户输入的智能自动完成。当您键入预先配置的标记(例如 @#)时,将显示一个面板,其中包含自动完成建议。

# 演示

您可以键入“@”字符来调用提及自动完成 UI。下面的演示配置为建议一个静态名称列表(BarneyLilyMarry AnnMarshallRobinTed)。

你好 @Ted

此演示展示了一组有限的功能。访问 功能丰富的编辑器示例 以查看更多实际操作。

您也可以查看在聊天应用程序中使用的提及功能的 更高级的示例

您可以在 专门的博客文章 中阅读有关提及功能的可能实现的更多信息。

# 安装

⚠️ 新的导入路径

版本 42.0.0 开始,我们更改了导入路径的格式。本指南使用新的、更短的格式。如果您使用的是较旧版本的 CKEditor 5,请参考 旧版设置中的包 指南。

安装编辑器 后,将该功能添加到插件列表和工具栏配置中

import { ClassicEditor, Mention } from 'ckeditor5';

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        plugins: [ Mention, /* ... */ ],
        mention: {
            // Configuration.
            // ...
        }
    } )
    .then( /* ... */ )
    .catch( /* ... */ );

# 配置

提及功能的最小配置要求定义 feedmarker。您还可以定义 minimumCharacters 参数,设置自动完成面板显示的字母数。此外,提要项的 ID 可能包含空格。

下面的代码片段用于配置上面的演示。它定义了用户在键入“@”字符后编辑器将自动完成的名称列表。

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        // This feature is available in the superbuild only.
        // See the "Installation" section.
        plugins: [ Mention, /* ... */ ],

        mention: {
            feeds: [
                {
                    marker: '@',
                    feed: [ '@Barney', '@Lily', '@Marry Ann', '@Marshall', '@Robin', '@Ted' ],
                    minimumCharacters: 1
                }
            ]
        }
    } )
    .then( /* ... */ )
    .catch( /* ... */ );

此外,您还可以配置

  • 如何在自动完成面板中渲染项目(通过设置 itemRenderer)。请参阅 自定义自动完成列表
  • 如何在 转换 期间转换项目。请参阅 自定义输出
  • 多个提要。上面的演示仅使用一个提要,由 '@' 字符触发。您可以定义多个提要,但它们必须使用不同的标记。例如,您可以将 '@' 用于人,将 '#' 用于标签。

# 提供提要

可以将 feed 提供为

  • 静态数组 - 适用于自动完成项集相对较小的场景。
  • 回调 - 提供对返回的项目列表的更多控制。

当使用回调时,您可以返回一个 Promise,它解析为 匹配的提要项 列表。这些可以是简单的字符串或包含至少 name 属性的纯对象。该对象的其余属性可以在以后 自定义自动完成列表自定义输出 时使用。

当使用外部资源来获取提要时,建议添加一些缓存机制,以便后续对相同建议的调用可以更快地加载。

您还可以考虑将 minimumCharacters 选项添加到提要配置中,以便编辑器在键入一些最小字符数后而不是仅在标记上执行操作后才调用提要回调。

回调接收查询文本,应使用该文本过滤项目建议。它应该返回一个 Promise 并将其解析为与提要文本匹配的项目数组。

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        // This feature is available in the superbuild only.
        // See the "Installation" section.
        plugins: [ Mention, /* ... */ ],

        mention: {
            feeds: [
                {
                    marker: '@',
                    feed: getFeedItems
                }
            }
        ]
    } )
    .then( /* ... */ )
    .catch( /* ... */ );

const items = [
    { id: '@swarley', userId: '1', name: 'Barney Stinson', link: 'https://www.imdb.com/title/tt0460649/characters/nm0000439' },
    { id: '@lilypad', userId: '2', name: 'Lily Aldrin', link: 'https://www.imdb.com/title/tt0460649/characters/nm0004989' },
    { id: '@marry', userId: '3', name: 'Marry Ann Lewis', link: 'https://www.imdb.com/title/tt0460649/characters/nm1130627' },
    { id: '@marshmallow', userId: '4', name: 'Marshall Eriksen', link: 'https://www.imdb.com/title/tt0460649/characters/nm0781981' },
    { id: '@rsparkles', userId: '5', name: 'Robin Scherbatsky', link: 'https://www.imdb.com/title/tt0460649/characters/nm1130627' },
    { id: '@tdog', userId: '6', name: 'Ted Mosby', link: 'https://www.imdb.com/title/tt0460649/characters/nm1102140' }
];

function getFeedItems( queryText ) {
    // As an example of an asynchronous action, return a promise
    // that resolves after a 100ms timeout.
    // This can be a server request or any sort of delayed action.
    return new Promise( resolve => {
        setTimeout( () => {
            const itemsToDisplay = items
                // Filter out the full list of all items to only those matching the query text.
                .filter( isItemMatching )
                // Return 10 items max - needed for generic queries when the list may contain hundreds of elements.
                .slice( 0, 10 );

            resolve( itemsToDisplay );
        }, 100 );
    } );

    // Filtering function - it uses the `name` and `username` properties of an item to find a match.
    function isItemMatching( item ) {
        // Make the search case-insensitive.
        const searchString = queryText.toLowerCase();

        // Include an item in the search results if the name or username includes the current user input.
        return (
            item.name.toLowerCase().includes( searchString ) ||
            item.id.toLowerCase().includes( searchString )
        );
    }
}

包含所有可能的自定义项及其源代码的完整工作演示 在本文档末尾提供

# 自定义自动完成列表

# 样式

可以通过定义 itemRenderer 回调来自定义自动完成列表中显示的项目。

此回调接受提要项(它至少包含 name 属性)并且必须返回一个新的 DOM 元素。

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        plugins: [ Mention, /* ... */ ],
        mention: {
            feeds: [
                {
                    feed: [ /* ... */ ],
                    // Define the custom item renderer.
                    itemRenderer: customItemRenderer
                }
            ]
        }
    } )
    .then( /* ... */ )
    .catch( /* ... */ );

function customItemRenderer( item ) {
    const itemElement = document.createElement( 'span' );

    itemElement.classList.add( 'custom-item' );
    itemElement.id = `mention-list-item-id-${ item.userId }`;
    itemElement.textContent = `${ item.name } `;

    const usernameElement = document.createElement( 'span' );

    usernameElement.classList.add( 'custom-item-username' );
    usernameElement.textContent = item.id;

    itemElement.appendChild( usernameElement );

    return itemElement;
}

包含所有可能的自定义项及其源代码的完整工作演示 在本文档末尾提供

# 列表长度

可以通过定义 dropdownLimit 选项来自定义自动完成列表中显示的项目数。

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        plugins: [ Mention, /* ... */ ],
        mention: {
            // Define the custom number of visible mentions.
            dropdownLimit: 4
            feeds: [
                { /* ... */ }
                // More feeds.
                // ...
            ]
        }
    } )
    .then( /* ... */ )
    .catch( /* ... */ );

包含所有可能的自定义项及其源代码的完整工作演示 在本文档末尾提供

# 自定义插入到编辑器中的文本

您可以通过提及配置中的 text 属性来控制在创建提及时插入到编辑器中的文本。

ClassicEditor
    .create( editorElement, {
        plugins: [ Mention, ... ],
        mention: {
            feeds: [
                // Feed items as objects.
                {
                    marker: '@',
                    feed: [
                        {
                            id: '@Barney',
                            fullName: 'Barney Stinson',
                            // Custom text to be inserted into the editor
                            text: 'Swarley'
                        },
                        // ...
                    ]
                },
            ]
        }
    } )
    .then( /* ... */ )
    .catch( /* ... */ );

您在此属性中指定的字符串将在创建提及时显示在编辑器中。

# 自定义输出

要更改编辑器为提及生成的标记,您可以覆盖提及功能的默认转换器。为此,您必须使用 upcastdowncast 转换器指定 AttributeElement

下面的示例定义了一个覆盖默认输出的插件

<span data-mention="@Ted" class="mention">@Ted</span>

到一个链接

<a class="mention" data-mention="@Ted" data-user-id="5" href="https://www.imdb.com/title/tt0460649/characters/nm1102140">@tdog</a>

转换器必须使用 'high' 优先级定义,以便在 链接 功能的转换器和提及功能的默认转换器之前执行。提及在模型中存储为一个 文本属性,该属性存储一个对象(参见 MentionFeedItem)。

要控制提及元素如何被其他属性元素(如粗体、斜体等)包装,请设置其 priority。要复制默认插件行为,使提及被其他元素包装,请将优先级设置为 20

默认情况下,相邻且具有相同值的属性元素将被渲染为单个 HTML 元素。要防止这种情况,模型属性值对象将每个插入的提及的唯一 ID 作为 uid 公开给模型。要防止合并后续的提及,请将其设置为 id

注意:该功能防止复制现有提及的片段。如果只选择提及的一部分,它将被复制为纯文本。具有 'highest' 优先级 的内部转换器控制此行为。我们不建议添加具有 'highest' 优先级的提及转换器,以避免冲突和奇特的结果。

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        plugins: [ Mention, MentionCustomization, /* ... */ ], // Add the custom mention plugin function.
        mention: {
            // Configuration.
            // ...
        }
    } )
    .then( /* ... */ )
    .catch( /* ... */ );

function MentionCustomization( editor ) {
    // The upcast converter will convert view <a class="mention" href="" data-user-id="">
    // elements to the model 'mention' text attribute.
    editor.conversion.for( 'upcast' ).elementToAttribute( {
        view: {
            name: 'a',
            key: 'data-mention',
            classes: 'mention',
            attributes: {
                href: true,
                'data-user-id': true
            }
        },
        model: {
            key: 'mention',
            value: viewItem => {
                // The mention feature expects that the mention attribute value
                // in the model is a plain object with a set of additional attributes.
                // In order to create a proper object use the toMentionAttribute() helper method:
                const mentionAttribute = editor.plugins.get( 'Mention' ).toMentionAttribute( viewItem, {
                    // Add any other properties that you need.
                    link: viewItem.getAttribute( 'href' ),
                    userId: viewItem.getAttribute( 'data-user-id' )
                } );

                return mentionAttribute;
            }
        },
        converterPriority: 'high'
    } );

    // Downcast the model 'mention' text attribute to a view <a> element.
    editor.conversion.for( 'downcast' ).attributeToElement( {
        model: 'mention',
        view: ( modelAttributeValue, { writer } ) => {
            // Do not convert empty attributes (lack of value means no mention).
            if ( !modelAttributeValue ) {
                return;
            }

            return writer.createAttributeElement( 'a', {
                class: 'mention',
                'data-mention': modelAttributeValue.id,
                'data-user-id': modelAttributeValue.userId,
                'href': modelAttributeValue.link
            }, {
                // Make mention attribute to be wrapped by other attribute elements.
                priority: 20,
                // Prevent merging mentions together.
                id: modelAttributeValue.uid
            } );
        },
        converterPriority: 'high'
    } );
}

包含所有可能的自定义项及其源代码的完整工作演示 在本文档末尾提供

# 完全自定义的提及提要

以下是一个自定义提及功能的示例,该功能

  • 使用具有附加属性(idusernamelink)的项目提要。
  • 在自动完成面板中渲染自定义项目视图。
  • 将提及转换为 <a> 元素而不是 <span>
  • 将提及的数量限制为四个元素。

你好 @tdog

# 源代码

ClassicEditor
    .create( document.querySelector( '#snippet-mention-customization' ), {
        plugins: [ Mention, MentionCustomization, /* ... */ ],
        mention: {
            dropdownLimit: 4,
            feeds: [
                {
                    marker: '@',
                    feed: getFeedItems,
                    itemRenderer: customItemRenderer
                }
            ]
        }
    } )
    .then( editor => {
        window.editor = editor;
    } )
    .catch( err => {
        console.error( err.stack );
    } );

function MentionCustomization( editor ) {
    // The upcast converter will convert <a class="mention" href="" data-user-id="">
    // elements to the model 'mention' attribute.
    editor.conversion.for( 'upcast' ).elementToAttribute( {
        view: {
            name: 'a',
            key: 'data-mention',
            classes: 'mention',
            attributes: {
                href: true,
                'data-user-id': true
            }
        },
        model: {
            key: 'mention',
            value: viewItem => {
                // The mention feature expects that the mention attribute value
                // in the model is a plain object with a set of additional attributes.
                // In order to create a proper object, use the toMentionAttribute helper method:
                const mentionAttribute = editor.plugins.get( 'Mention' ).toMentionAttribute( viewItem, {
                    // Add any other properties that you need.
                    link: viewItem.getAttribute( 'href' ),
                    userId: viewItem.getAttribute( 'data-user-id' )
                } );

                return mentionAttribute;
            }
        },
        converterPriority: 'high'
    } );

    // Downcast the model 'mention' text attribute to a view <a> element.
    editor.conversion.for( 'downcast' ).attributeToElement( {
        model: 'mention',
        view: ( modelAttributeValue, { writer } ) => {
            // Do not convert empty attributes (lack of value means no mention).
            if ( !modelAttributeValue ) {
                return;
            }

            return writer.createAttributeElement( 'a', {
                class: 'mention',
                'data-mention': modelAttributeValue.id,
                'data-user-id': modelAttributeValue.userId,
                'href': modelAttributeValue.link
            }, {
                // Make mention attribute to be wrapped by other attribute elements.
                priority: 20,
                // Prevent merging mentions together.
                id: modelAttributeValue.uid
            } );
        },
        converterPriority: 'high'
    } );
}

const items = [
    { id: '@swarley', userId: '1', name: 'Barney Stinson', link: 'https://www.imdb.com/title/tt0460649/characters/nm0000439' },
    { id: '@lilypad', userId: '2', name: 'Lily Aldrin', link: 'https://www.imdb.com/title/tt0460649/characters/nm0004989' },
    { id: '@marry', userId: '3', name: 'Marry Ann Lewis', link: 'https://www.imdb.com/title/tt0460649/characters/nm1130627' },
    { id: '@marshmallow', userId: '4', name: 'Marshall Eriksen', link: 'https://www.imdb.com/title/tt0460649/characters/nm0781981' },
    { id: '@rsparkles', userId: '5', name: 'Robin Scherbatsky', link: 'https://www.imdb.com/title/tt0460649/characters/nm1130627' },
    { id: '@tdog', userId: '6', name: 'Ted Mosby', link: 'https://www.imdb.com/title/tt0460649/characters/nm1102140' }
];

function getFeedItems( queryText ) {
    // As an example of an asynchronous action, return a promise
    // that resolves after a 100ms timeout.
    // This can be a server request or any sort of delayed action.
    return new Promise( resolve => {
        setTimeout( () => {
            const itemsToDisplay = items
                // Filter out the full list of all items to only those matching the query text.
                .filter( isItemMatching )
                // Return 10 items max - needed for generic queries when the list may contain hundreds of elements.
                .slice( 0, 10 );

            resolve( itemsToDisplay );
        }, 100 );
    } );

    // Filtering function - it uses `name` and `username` properties of an item to find a match.
    function isItemMatching( item ) {
        // Make the search case-insensitive.
        const searchString = queryText.toLowerCase();

        // Include an item in the search results if name or username includes the current user input.
        return (
            item.name.toLowerCase().includes( searchString ) ||
            item.id.toLowerCase().includes( searchString )
        );
    }
}

function customItemRenderer( item ) {
    const itemElement = document.createElement( 'span' );

    itemElement.classList.add( 'custom-item' );
    itemElement.id = `mention-list-item-id-${ item.userId }`;
    itemElement.textContent = `${ item.name } `;

    const usernameElement = document.createElement( 'span' );

    usernameElement.classList.add( 'custom-item-username' );
    usernameElement.textContent = item.id;

    itemElement.appendChild( usernameElement );

    return itemElement;
}

# 颜色和样式

# 使用 CSS 变量

提及功能利用了 CSS 变量 的强大功能,这些变量在 Lark 主题样式表 中定义。因此,提及样式可以 轻松定制

:root {
    /* Make the mention background blue. */
    --ck-color-mention-background: hsla(220, 100%, 54%, 0.4);

    /* Make the mention text dark grey. */
    --ck-color-mention-text: hsl(0, 0%, 15%);
}

你好 @Ted

# 含有提及的评论

可以配置提及功能以与 评论功能 配合使用。您可以在 此处找到有关此问题的详细指南

除了启用提及之外,您可能还想查看以下生产力功能

  • 自动文本转换 – 让您自动将 (tm) 之类的片段转换为 ,并将 "foo" 转换为 “foo”
  • 自动链接 – 将在编辑器中键入或粘贴的链接和电子邮件地址转换为活动 URL。
  • 自动格式化 – 让您快速将格式应用于您正在编写的文本。

# 通用 API

Mention 插件注册

  • MentionCommand 实现的 'mention' 命令。

    您可以通过执行以下代码来插入提及元素

    editor.execute( 'mention', { marker: '@', mention: '@John' } );
    

我们建议使用官方的 CKEditor 5 检查器 进行开发和调试。它将为您提供有关编辑器状态的大量有用信息,例如内部数据结构、选择、命令等等。

# 贡献

该功能的源代码在 GitHub 上 https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-mention 提供。