CKEditor 5 的标准 UI 库是@ckeditor/ckeditor5-ui。其提供了基础类和辅助工具,用于构建模块化用户界面,这些界面能够与生态系统中的其它组件无缝集成。

视图​

视图使用模板来构建用户界面。它们还提供了可观察接口,其它功能(如 Plugin 或 Command)可以通过这些接口来修改 DOM,无需直接与原生 DOM API 进行交互。一般地,创建视图时传入一个locale实例,即可对相关视图进行本地化。参阅本地化指南,可以了解如何使用locale实例中提供的t()函数。

定义​

定义一个简单的输入视图类:

JavaScript
class SimpleInputView extends View {
    constructor( locale ) {
        super( locale );

        // 将可观察对象(observables)与 DOM 属性、事件和文本节点绑定的入口点
        const bind = this.bindTemplate;

        // 视图(Views)通过可观察属性(observable properties)来定义其接口(状态)
        this.set( {
            isEnabled: false,
            placeholder: ''
        } );

        this.setTemplate( {
            tag: 'input',
            attributes: {
                class: [
                    'foo',
                    // view#isEnabled 的值将控制该类名的存在与否
                    bind.if( 'isEnabled', 'ck-enabled' ),
                ],

                // HTML 的 placeholder 属性也由该可观察对象控制
                placeholder: bind.to( 'placeholder' ),
                type: 'text'
            },
            on: {
                // DOM 的 keydown 事件将触发 view#input 事件
                keydown: bind.to( 'input' )
            }
        } );
    }

    setValue( newValue ) {
        this.element.value = newValue;
    }
}

视图封装了它们所渲染的 DOM。由于 UI 是按照“每个树对应一个视图”(view-per-tree)的规则组织的,因此可以清楚地知道哪个视图负责 UI 的哪一部分。两个功能写入同一个 DOM 节点时,其发生冲突的可能性很小。通常情况下,视图会成为其他视图(集合)的子节点,即 UI 视图树中的节点:

JavaScript
class ParentView extends View {
    constructor( locale ) {
        super( locale );

        const childA = new SimpleInputView( locale );
        const childB = new SimpleInputView( locale );

        this.setTemplate( {
            tag: 'div',
            children: [
                childA,
                childB
            ]
        } );
    }
}

const parent = new ParentView( locale );

parent.render();

// 实际发生的插入:<div><input .. /><input .. /></div>.
document.body.appendChild( parent.element );

也可以创建不属于任何集合的独立视图(standalone views)。这些视图在注入到 DOM 之前必须先进行渲染:

JavaScript
const view = new SimpleInputView( locale );

view.render();

// 实际发生的插入:<input class="foo" type="text" placeholder="" />
document.body.appendChild( view.element );

交互​

功能(Features)可以通过视图的可观察属性与 DOM 的状态进行交互,因此可以实现以下操作:

JavaScript
view.isEnabled = true;
view.placeholder = 'Type some text';

实际输出则为:

HTML
<input class="foo ck-enabled" type="text" placeholder="Type some text" />

或者它们可以直接将这些属性绑定到自己的可观察属性上:

JavaScript
view.bind( 'placeholder', 'isEnabled' ).to( observable, 'placeholderText', 'isEnabled' );

// 以下内容将自动反映到 DOM 中的 view#placeholder 和 view.element#placeholder HTML 属性上
observable.placeholderText = 'Some placeholder';

此外,由于视图会传播 DOM 事件,功能现在可以对用户操作作出响应:

JavaScript
// 输入框中的每个 keydown 事件都会执行一个命令(command)
view.on( 'input', () => {
    editor.execute( 'myCommand' );
} );

实践​

一个完整的视图应为功能提供一个接口,以封装 DOM 节点和属性。

功能不应使用原生 API 直接操作视图的 DOM。任何类型的交互都必须由拥有该元素的视图处理,以避免冲突:

JavaScript
// 改变输入框的值
view.setValue( 'A new value of the input.' );

// 错误!这不是与 DOM 交互的正确方式,因为它会与绑定至 #placeholderText 的可观察对象生冲突
// 当可观察对象的状态发生变化时,该值将被永久覆盖。
view.element.placeholder = 'A new placeholder';

模板​

模板用于在 UI 库中渲染 DOM 元素和文本节点。它们主要由视图使用,是连接应用程序与网页的 UI 底层。

参阅TemplateDefinition以了解更多关于模板语法和其它高级概念的内容。

模板支持可观察属性绑定,并处理原生 DOM 事件。一个简单的 Template 可以如下所示:

JavaScript
new Template( {
    tag: 'p',
    attributes: {
        class: [
            'foo',
            bind.to( 'class' )
        ],
        style: {
            backgroundColor: 'yellow'
        }
    },
    on: {
        click: bind.to( 'clicked' )
    },
    children: [
        'A paragraph.'
    ]
} ).render();

实际渲染的 HTML 元素内容:

HTML
<p class="foo bar" style="background-color: yellow;">A paragraph.</p>

其中observable#class的值为"bar"。上例中的observable可以是视图或任何可观察的对象。当class属性的值发生变化时,模板会更新 DOM 中的class属性。从此时起,该元素将永久绑定到应用程序的状态。类似地,在渲染时,模板还会处理 DOM 事件。定义中对click事件的绑定会使可观察对象在 DOM 中发生点击操作时始终触发clicked事件。通过这种方式,可观察对象提供了 DOM 元素的事件接口,所有通信都应通过它进行。

视图集合与 UI 树​

视图被组织成集合(collections),这些集合管理它们的元素并进一步传播 DOM 事件。在集合中添加或移除视图会移动视图的 DOM 元素以反映其位置。

每个编辑器 UI 都有一个“根视图”,例如ClassicEditor#view,其可以在editor.ui.view下找到。此类视图通常定义编辑器的容器元素以及可供其他功能填充的最底层视图集合。例如,BoxedEditorUiView类定义了两个集合:

  1. top:一个包含工具栏的集合;

  2. main:一个包含编辑器可编辑区域的集合。

它还继承了直接位于网页<body>中的body集合。该集合存储浮动元素,例如气泡型面板(balloon panels)。

插件(Plugins)可以通过其子视图填充根视图集合。这些子视图将成为 UI 树的一部分,并由编辑器管理,其意味着它们将与编辑器一起初始化和销毁。

JavaScript
class MyPlugin extends Plugin {
    init() {
        const editor = this.editor;
        const view = new MyPluginView();

        editor.ui.top.add( view );
    }
}

MyPluginView可以创建其视图集合(view collections),并在编辑器的生命周期内填充它们。UI 树的深度没有限制,其结构通常如下所示:

代码
EditorUIView
    ├── "top" collection
    │    └── ToolbarView
    │        └── "items" collection
    │            ├── DropdownView
    │            │    ├── ButtonView
    │            │    └── PanelView
    │            ├── ButtonViewA
    │            ├── ButtonViewB
    │            └── ...
    ├── "main" collection
    │    └── InlineEditableUIView
    └── "body" collection
        ├── BalloonPanelView
        │    └── "content" collection
        │        └── ToolbarView
        ├── BalloonPanelView
        │    └── "content" collection
        │        └── ...
        └── ...

内置组件​

CKEditor 内部提供了一些常用组件,如ButtonViewToolbarView。在开发新用户界面时,其可能会很有帮助。

例如,要创建一个包含多个按钮的工具栏,可以先导入ToolbarViewButtonView类:

JavaScript
import { ButtonView, ToolbarView } from 'ckeditor5';

首先创建工具栏和几个带标签的按钮,然后将这些按钮添加到工具栏中:

JavaScript
const toolbar = new ToolbarView();
const buttonFoo = new ButtonView();
const buttonBar = new ButtonView();

buttonFoo.set( {
    label: 'Foo',
    withText: true
} );

buttonBar.set( {
    label: 'Bar',
    withText: true
} );

toolbar.items.add( buttonFoo );
toolbar.items.add( buttonBar );

工具栏既可以打入 UI 树,也可以直接注入到 DOM 中。为了简化示例,我们采用后一种方案:

JavaScript
toolbar.render();

document.body.appendChild( toolbar.element );

此时已渲染的工具栏还没有什么实际功能。要在点击按钮时执行操作,需要定义一个监听器。按钮可以将execute事件委派给它们的父级:

JavaScript
buttonFoo.delegate( 'execute' ).to( toolbar );
buttonBar.delegate( 'execute' ).to( toolbar );

toolbar.on( 'execute', evt => {
    console.log( `按钮 "${ evt.source.label }" 已被点击!` );
} );

下拉栏​

该框架实现了下拉组件,其面板中可以承载任何类型的 UI。它由一个按钮(用于打开下拉菜单)和一个面板(容器)组成。

按钮可以是以下两种之一:

  1. 标准的ButtonView

  2. 更复杂场景下的SplitButtonView

下拉栏暴露了其子视图集合。最常见的视图包括:

  1. ListView - 下拉列表

  2. ToolbarView - 下拉工具栏

  3. DropdownMenuRootListView - 下拉菜单

该框架提供了一组辅助工具,以简化下拉菜单的创建过程。尽管您仍然可以使用基础类从头开始构建自定义下拉菜单。然而对于大多数需求,我们强烈建议使用内置提供的辅助函数。例如,createDropdown辅助函数可以创建一个带有ButtonViewSplitButtonViewDropdownView

JavaScript
import { createDropdown, SplitButtonView } from 'ckeditor5';

const dropdownView = createDropdown( locale, SplitButtonView );

这种下拉菜单默认附带一组行为:

  1. 失去焦点时(例如,用户将焦点移动到其他地方),关闭面板;

  2. 触发execute事件时,关闭面板;

  3. 使用键盘导航工具栏时,聚焦面板中托管的视图。

设置 labelicontootip

要自定义下拉菜单的按钮,应使用buttonView属性,其允许直接访问下拉菜单使用的ButtonView实例。

如果下拉菜单是使用SplitButtonView创建的,请使用actionView来访问其主区域。例如:dropdownView.buttonView.actionView.set( /* ... */ )

要控制下拉菜单的标签,首先使用withText属性使其可见,然后设置标签的文本:

JavaScript
const dropdownView = createDropdown( locale );

dropdownView.buttonView.set( {
    withText: true,
    label: '按钮的标签',
} );

下拉按钮也可以显示图标。首先导入 SVG 文件。然后将其传递给按钮的icon属性:

JavaScript
import iconFile from 'path/to/icon.svg';

// 用于创建下拉视图的代码
// ...

dropdownView.buttonView.set( {
    icon: iconFile
} );

可以使用编辑器提供的内置图标,也可以像本例一样,通过提供图标的整个 XML 字符串,在下拉菜单中添加自定义icon

HTML
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10.187 17H5.773c-.637 0-1.092-.138-1.364-.415-.273-.277-.409-.718-.409-1.323V4.738c0-.617.14-1.062.419-1.332.279-.27.73-.406 1.354-.406h4.68c.69 0 1.288.041 1.793.124.506.083.96.242 1.36.478.341.197.644.447.906.75a3.262 3.262 0 0 1 .808 2.162c0 1.401-.722 2.426-2.167 3.075C15.05 10.175 16 11.315 16 13.01a3.756 3.756 0 0 1-2.296 3.504 6.1 6.1 0 0 1-1.517.377c-.571.073-1.238.11-2 .11zm-.217-6.217H7v4.087h3.069c1.977 0 2.965-.69 2.965-2.072 0-.707-.256-1.22-.768-1.537-.512-.319-1.277-.478-2.296-.478zM7 5.13v3.619h2.606c.729 0 1.292-.067 1.69-.2a1.6 1.6 0 0 0 .91-.765c.165-.267.247-.566.247-.897 0-.707-.26-1.176-.778-1.409-.519-.232-1.31-.348-2.375-.348H7z"/></svg>

如果想让图标根据按钮的状态改变颜色,则应删除所有fillstroke属性。

属性withTexticon是互相独立的,因此下拉菜单可以:

  1. 只有一个文本标签;

  2. 只有一个图标;

  3. 同时拥有标签和图标。

即使下拉菜单没有可见标签(withTextfalse),也建议设置一个label属性。屏幕阅读器等无障碍辅助技术需要它才能与编辑器正常工作。

下拉菜单还可以在鼠标悬停时显示小型提示文本。使用按钮的tooltip属性可启用此功能。可以在 tooltip 中包含按键信息或创建自定义 tooltip。

JavaScript
dropdownView.buttonView.set( {
    // Tooltip 文本将会重复 label 的内容
    tooltip: true
} );

向下拉栏中添加列表​

可以使用addListToDropdown辅助函数将一个ListView添加到下拉栏中。

JavaScript
import { ViewModel, addListToDropdown, createDropdown, Collection } from 'ckeditor5';

// 默认下拉栏
const dropdownView = createDropdown( locale );

// 列表元素的集合
const items = new Collection();

items.add( {
    type: 'button',
    model: new ViewModel( {
        withText: true,
        label: 'Foo'
    } )
} );

items.add( {
    type: 'button',
    model: new ViewModel( {
        withText: true,
        label: 'Bar'
    } )
} );

// 在面板内部创建一个带列表的下拉栏
addListToDropdown( dropdownView, items );

向下拉栏中添加工具栏​

可以使用addToolbarToDropdown辅助函数将工具栏视图添加到下拉栏中。

JavaScript
import { ButtonView, SplitButtonView, addToolbarToDropdown, createDropdown } from 'ckeditor5';

const buttons = [];

// 向工具栏元件数组中添加一个简单的按钮
buttons.push( new ButtonView() );

// 向工具栏元件列表中添加另一个小组件
buttons.push( componentFactory.create( 'componentName' ) );

const dropdownView = createDropdown( locale, SplitButtonView );

// 在面板内部创建一个带工具栏的下拉栏
addToolbarToDropdown( dropdownView, buttons );

一种常见的做法是:当工具栏上的某个项目启用时,主下拉栏的关联按钮也会被启用:

JavaScript
// 下拉栏中的任意元素被启用时,将下拉栏的主唤起按钮置为活跃状态
dropdownView.bind( 'isEnabled' ).toMany( buttons, 'isEnabled',
    ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled )
);

向下拉栏中添加菜单​

可以使用addMenuToDropdown辅助函数将多级菜单添加到下拉栏中。

JavaScript
import { addMenuToDropdown, createDropdown } from 'ckeditor5';

// 默认下拉栏
const dropdownView = createDropdown( editor.locale );

// 菜单元素的 Definition
const definition = [
    {
        id: 'menu_1',
        menu: 'Menu 1',
        children: [
            {
                id: 'menu_1_a',
                label: '选项 A'
            },
            {
                id: 'menu_1_b',
                label: '选项 B'
            }
        ]
    },
    {
        id: 'top_a',
        label: '顶级选项 A'
    },
    {
        id: 'top_b',
        label: '顶级选项 B'
    }
];

addMenuToDropdown( dropdownView, editor.body.ui.view, definition );

在按下某个已定义的按钮时执行某些操作:

JavaScript
dropdownView.on( 'execute', evt => {
    const id = evt.source.id;


    // 例如按下 "Item A" 时打印 "menu_1_a"
    console.log( id );
} );

对话框和模态框​

该框架提供 UI 可交互对话框组件。CKEditor 5 中的对话框系统由 Dialog 插件提供,它提供了在对话框中显示视图的 API。从某种意义上来说,该插件对应于另一个在整个用户界面中管理 Balloon 弹窗视图的插件(ContextualBalloon 插件)。

对话框是一个弹出式窗口,用户点击其外部时不会关闭。它允许在打开时与编辑器及其内容进行交互(除非它是一个模态窗口,在关闭前会阻止与页面其他部分的交互)。如果配置为显示标题,对话框还可以用鼠标或触摸拖动。一次只能打开一个对话框——打开另一个对话框则会关闭之前可见的对话框。

模态窗口​

模态窗口与对话框类似——它们共享相同的结构规则、API 等。主要区别在于当模态窗口打开时,用户无法与编辑器或页面的其它部分进行交互。它们被一个非完全透明的遮罩层所覆盖。通常使用模态来强制用户执行指定的操作。

要创建模态窗口,应使用Dialog::show()方法的可选isModal属性:

JavaScript
editor.plugins.get( 'Dialog' ).show( {
    isModal: true,

    // 对话框 Definition 的剩余部分
} );

要关闭一个模态窗口,有很多种方法:

  1. 点击右上角的关闭按钮(如果标题可见);

  2. 使用 Esc 键或某个操作按钮。

下面你将了解到禁用其中一些行为的方法。请记住一定要留下至少一个关闭模态的选项。否则用户就会被锁定在模态窗口中。

结构与行为​

对话框可由三个部分组成,每个部分都是可选的:

  1. 页眉,也用作拖动 Handler;

  2. 内容,作为对话框主体;

  3. 操作按钮区域,是一个按钮集合。

不能更改这些部分的顺序。

页眉​

页眉也即对话框的标题栏。其可以由以下三种元素任意组合而成:

  1. 图标icon

  2. 标题title

  3. 关闭按钮。

默认情况下,只要提供icontitle,关闭按钮就会被添加到页眉中。要隐藏该按钮,将hasCloseButton设为false即可:

JavaScript
import { icons } from 'ckeditor5';

// ...

editor.plugins.get( 'Dialog' ).show( {
    icon: icons.pencil,
    title: '我的第一个对话框',
    // 不显示关闭按钮
    hasCloseButton: false,

    // 对话框 Definition 的剩余部分
} );

如果决定隐藏“关闭 ”按钮,则请务必记住要留出其它关闭对话框的方法。Esc 键也可以关闭对话框,但触摸屏用户可能无法使用。

内容​

该部分可以是单个视图,也可以是视图的集合。它们将直接显示在对话框正文中。下面是如何在对话框中插入文本块的示例。

JavaScript
const textView = new View( locale );

textView.setTemplate( {
    tag: 'div',
    attributes: {
        style: {
            padding: 'var(--ck-spacing-large)',
            whiteSpace: 'initial',
            width: '100%',
            maxWidth: '500px'
        },
        tabindex: -1
    },
    children: [
        '这是对话框的内容示例。',
        '可以在这里放置文字、图片、输入框和按钮等元素。'
    ]
} );

editor.plugins.get( 'Dialog' ).show( {
    title: '带文本消息的对话框',
    content: textView,

    // 对话框 Definition 的剩余部分
} );

对话框的内容决定其尺寸。如果要以固定尺寸显示对话框,请确保其内容也有固定尺寸。如果要显示长文本,建议在内容容器(元素)上将max-height设置为固定值,将overflow设置为auto。它们会限制对话框在屏幕上使用的垂直空间。

操作按钮​

对话框的最后一部分是操作区,按钮都显示在这里。回调onCreate()onExecute()能够完全自定义它们的行为。下面是四个自定义按钮的配置示例:

  1. “OK”按钮用于关闭对话框,并设置了自定义 CSS 类。

  2. “设置自定义标题”按钮用于修改对话框标题。

  3. “此按钮将在...秒后启用...”按钮,可在几秒钟后改变其状态。

  4. “取消”按钮,用于关闭对话框。

JavaScript
editor.plugins.get( 'Dialog' ).show( {
    // ...

    actionButtons: [
        {
            label: 'OK',
            class: 'ck-button-action',
            withText: true,
            onExecute: () => dialog.hide()
        },
        {
            label: '设置自定义标题',
            withText: true,
            onExecute: () => {
                dialog.view.headerView.label = '新的标题!';
            }
        },
        {
            label: '此按钮将在 5 秒后启用...',
            withText: true,
            onCreate: buttonView => {
                buttonView.isEnabled = false;
                let counter = 5;

                const interval = setInterval( () => {
                    buttonView.label = `此按钮将在 ${ --counter } 秒后启用...`;

                    if ( counter === 0 ) {
                        clearInterval( interval );
                        buttonView.label = '此按钮现已启用!';
                        buttonView.isEnabled = true;
                    }
                }, 1000 );
            }
        },
        {
            label: '取消',
            withText: true,
            onExecute: () => dialog.hide()
        }
    ]
} );

还可以将按钮状态与内容绑定,以确保用户执行了某些必要的操作。参见下方的 “是/否模式 ”Definition 示例,其要求用户标记复选框以关闭 Dialog:

JavaScript
// 首先创建要注入模态对话框的内容
// 本例中使用一个简单的开关按钮
const switchButtonView = new SwitchButtonView( locale );

switchButtonView.set( {
    label: t( '我同意使用条款' ),
    withText: true
} );

// 管理用户点击开关按钮时的状态
switchButtonView.on( 'execute', () => {
    switchButtonView.isOn = !switchButtonView.isOn;
} );

// 然后显示模态窗口
editor.plugins.get( 'Dialog' ).show( {
    id: 'yesNoModal',
    isModal: true,
    title: '同意使用条款以继续',
    hasCloseButton: false,
    content: switchButtonView,
    actionButtons: [
        {
            label: t( '继续' ),
            class: 'ck-button-action',
            withText: true,
            onExecute: () => dialog.hide(),
            onCreate: buttonView => {
                // 默认情况下此按钮应为禁用
                buttonView.isEnabled = false;

                // 用户切换开关按钮后才会启用
                switchButtonView.on( 'change:isOn', () => {
                    buttonView.isEnabled = switchButtonView.isOn;
                } );
            }
        },
        {
            label: t( '否' ),
            withText: true,
            onExecute: () => dialog.hide()
        }
    ],
    // 禁用 Esc 退出策略
    onShow: dialog => {
        dialog.view.on( 'close', ( evt, data ) => {
            if ( data.source === 'escKeyPress' ) {
                evt.stop();
            }
        }, { priority: 'high' } );
    }
} );

无障碍​

对话框提供全面的键盘辅助功能。

当对话框打开时,按下 Ctrl+F6 即可在编辑器和对话框之间移动焦点,也可以随时按 Esc 键关闭对话框。

要在对话框之间导航,请使用 Tab 和 Shift+Tab 键。对话框的内容也可供屏幕阅读器使用。

API​

对话框的生命周期(创建和销毁)由 Dialog 插件管理。后者提供了两个公共方法:show()hide()

这两个方法都会触发相应的事件(显示和隐藏),因此可以在它们之后或之前执行 Hook。

Dialog::show()

隐藏所有当前对话框,并显示一个新对话框。此方法接受一个对话框定义,该定义允许自由塑造对话框的结构和行为。参阅DialogDefinition API 以了解其提供的更多功能。

Dialog::show:[id] 事件​

调用Dialog::show()函数时,会触发一个名为show:[id]的事件。这样就可以自定义对话框的行为。

例如,可以使用以下代码将“查找和替换 ”对话框的默认渲染位置从编辑器右上角改为底部:

JavaScript
import { DialogViewPosition } from 'ckeditor5';

// ...

editor.plugins.get( 'Dialog' ).on( 'show:findAndReplace', ( evt, data ) => {
    Object.assign( data, { position: DialogViewPosition.EDITOR_BOTTOM_CENTER } );
}, { priority: 'high' } );

还可以监听通配show事件,以一次性自定义所有对话框。

Dialog::hide()

执行Dialog::hide()方法将会隐藏对话框。如果在相应的Dialog::show()方法调用中提供了onHide()回调,则其还将调用该回调。

Dialog::hide:[id] 事件​

show:[id]事件类似,调用Dialog::hide()时也会触发hide:[id]事件,其允许执行自定义操作:

JavaScript
// 隐藏“查找和替换”对话框之后的日志
editor.plugins.get( 'Dialog' ).on( 'hide:findAndReplace', () => {
    console.log( '“查找和替换”对话框已被隐藏。' );
} );

还可以监听通配hide事件,以便同时对所有对话框做出反应。

DialogView::close事件​

在对话框打开的情况下按下 Esc 键或关闭按钮时,Dialog 的 View 会触发带有相应源参数的关闭事件。在这之后,Dialog 插件便会隐藏(并销毁)对话框。

这种行为可以自定义控制,例如禁用 Esc 键的 Handler:

JavaScript
editor.plugins.get( 'Dialog' ).view.on( 'close', ( evt, data ) => {
    if ( data.source === 'escKeyPress' ) {
        evt.stop();
    }
} );

也可以直接在onShow回调中的show()方法调用中传递此类代码:

JavaScript
editor.plugins.get( 'Dialog' ).show( {
    onShow: dialog => {
        dialog.view.on( 'close', ( evt, data ) => {
            if ( data.source === 'escKeyPress' ) {
                evt.stop();
            }
        }, { priority: 'high' } );
    }
    // 对话框 Definition 的剩余部分
} );

阻断 Esc 键的使用会限制无障碍功能,因此这是一种不好的做法。如果需要这样做,请记住一定要至少留出一种关闭对话框或模态的方法。

使用onShowonHide回调​

DialogDefinition接受两个回调,它们允许自定义对话框显示onShow()和隐藏onHide()后的操作。

回调onShow可以操作对话框值或设置额外的监听器。在DialogView::event:close事件部分,可以找到如何禁用 Esc 键的示例。

下面的代码展示了如何引导动态字段填充:

JavaScript
// 导入必要类
import { View, InputTextView } from 'ckeditor5';

// 创建一个输入框
const input = new InputTextView();
const dialogBody = new View();

dialogBody.setTemplate( {
    tag: 'div',
    children: [
        // 还包含视图中的其它元素
        input
    ]
    // 模板的剩余部分
} );

editor.plugins.get( 'Dialog' ).show( {
    onShow: () => {
        // 动态设置输入框的初始值
        input.value = getDataFromExternalSource();
    },
    // 对话框 Definition 的剩余部分
} );

回调onHide有助于在对话框关闭时重置组件或其控制器的状态。

JavaScript
// 在带有 controller 属性的类中执行,将该类的状态与对话框相连接
editor.plugins.get( 'Dialog' ).show( {
    onHide: dialog => {
        // 隐藏对话框之后,重置控制器的状态
        this.controller.reset();
    },
    // 对话框 Definition 的剩余部分
} );

可见性和定位​

同一时间只能渲染一个对话框。(从同一个或另一个编辑器实例)打开另一个对话框则会关闭上一个对话框。

若未特殊指定,对话框将显示在编辑器编辑区的中央,模态对话框会显示在屏幕中央。自定义对话框的位置可设置为预设选项之一(请参阅DialogViewPosition)。一旦用户手动拖动对话框,相对定位就会禁用。在开发对话框时,可以在传递给Dialog::show()方法的定义中直接指定位置属性:

JavaScript
import { DialogViewPosition } from 'ckeditor5';

// ...

const dialog = editor.plugins.get( 'Dialog' );

dialog.show( {
    // ...

    // 修改对话框的默认渲染位置
    position: DialogViewPosition.EDITOR_BOTTOM_CENTER
} );

要修改现有对话框的位置,或动态管理其位置,则应使用显示事件监听器。

有时当对话框的内容或环境发生变化时(例如编辑器的大小被调整),则开发者可能希望强制更新对话框的位置——使其位置恢复到配置的默认值。此操作还将重置用户之前进行过的所有手动定位(与拖动)操作。这种情况对应updatePosition方法。

JavaScript
editor.plugins.get( 'Dialog' ).view.updatePosition();

最佳实践​

为了获得最佳的用户体验,编辑视图应在用户执行任何操作(如执行命令)时聚焦,以确保编辑器保留焦点:

JavaScript
// 在 dropdown#execute 事件上执行一些操作
dropdownView.buttonView.on( 'execute', () => {
    editor.execute( 'command', { value: "command-value" } );
    editor.editing.view.focus();
} );

按键与焦点管理​

此框架提供的内置类可帮助管理用户界面中的按键和焦点,尤其能优化无障碍体验。

焦点跟踪器​

FocusTracker类可以观察一些 HTML 元素,并确定其中一个元素是否被用户(点击、键入)或使用HTMLElement.focus() DOM 方法聚焦。

JavaScript
import { FocusTracker } from 'ckeditor5';

// 其它导入
// ...

const focusTracker = new FocusTracker();

要在跟踪器中注册元素,应使用add()方法:

JavaScript
focusTracker.add( document.querySelector( '.some-element' ) );
focusTracker.add( viewInstance.element );

通过观察焦点跟踪器的isFocused可观察属性,可以确定某个注册元素当前是否处于焦点状态:

JavaScript
focusTracker.on( 'change:isFocused', ( evt, name, isFocused ) => {
    if ( isFocused ) {
        console.log( '元素 ', focusTracker.focusedElement, ' 现已被聚焦。' );
    } else {
        console.log( '元素已失焦。' );
    }
} );

当用户界面的行为取决于焦点时,该信息就会派上用场。例如:包含表单的上下文面板和浮动 Balloon 应在用户决定放弃相关操作时隐藏。

按键处理程序​

按键处理程序(KeystrokeHandler)会监听 HTML 元素或其任何子元素触发的按键事件。按下按键时,它会执行预定义的操作。通常每个视图都会创建自己的按键处理程序实例,它负责处理视图渲染的元素触发的键盘事件。

JavaScript
import { KeystrokeHandler } from 'ckeditor5';

// 其它导入
// ...

const keystrokeHandler = new KeystrokeHandler();

要在 DOM 中定义按键处理程序的 scope,应使用listenTo()方法:

JavaScript
keystrokeHandler.listenTo( document.querySelector( '.some-element' ) );
keystrokeHandler.listenTo( viewInstance.element );

按键的操作回调是一个函数。要阻止按键的默认操作及其进一步传播,请使用回调中提供的cancel()函数。

JavaScript
keystrokeHandler.set( 'Tab', ( keyEvtData, cancel ) => {
    console.log( 'TAB 键已被按下!' );

    // 该按键已被处理,可以取消
    cancel();
} );

还有一个EditingKeystrokeHandler类,其 API 与KeystrokeHandler相同,但它提供了与编辑器 Command 的直接按键绑定。

编辑器在editor.keystrokes属性下提供了这样一个按键处理程序,因此任何插件都可以注册与编辑器命令相关的按键。例如撤销插件内置注册了editor.keystrokes.set( 'Ctrl+Z', 'undo' )用以执行撤销命令。

为同一个按键指定多个回调时,可以使用优先级来决定先处理哪个回调,以及是否执行其它回调:

JavaScript
keystrokeHandler.set( 'Ctrl+A', ( keyEvtData ) => {
    console.log( '一个常规优先级的监听器。' );
} );

keystrokeHandler.set( 'Ctrl+A', ( keyEvtData ) => {
    console.log( '一个高优先级的监听器。' );

    // 常规优先级的监听器不会被执行
    cancel();
}, { priority: 'high' } );