提高可访问性和添加命令
您已经完成了缩写插件教程的最后部分。在本部分中,我们将改进插件的可访问性。我们还将处理一个命令,它将另外从用户的选择中获取文本,并将其插入到我们的表单中。等等!
我们从 第二部分 结束的地方开始,因此请确保您已经完成了它,或者使用以下命令获取本部分的入门文件。
npx -y degit ckeditor/ckeditor5-tutorials-examples/abbreviation-plugin/part-2 abbreviation-plugin
cd abbreviation-plugin
npm install
npm run dev
如果您想在深入了解之前查看本教程的最终产品,请查看 实时演示。
# 提高可访问性
首先,我们使插件对依赖键盘导航的用户可访问。我们希望确保按下 Tab 和 Shift + Tab 将在表单视图中移动焦点,并且按下 Esc 将关闭它。
我们有一些现成的选项可以帮助我们——KeystrokeHandler、FocusTracker 和 FocusCycler 辅助类。
# 添加按键处理程序和焦点跟踪器
我们首先将 KeystrokeHandler
和 FocusTracker
类导入到我们的表单视图中,并在 constructor()
中创建它们的实例。
现在,在 render()
方法中,我们将 childViews
视图集合中的每个元素添加到 focusTracker
中。在那里,我们还可以开始监听来自渲染的视图元素的按键。
最后,让我们添加 destroy()
方法,并销毁焦点跟踪器和按键处理程序。它将确保当用户关闭编辑器时,我们的助手也“死亡”,防止任何内存泄漏。
// abbreviation/abbreviationview.js
import {
View,
LabeledFieldView,
createLabeledInputText,
ButtonView,
submitHandler,
icons,
FocusTracker, // ADDED
KeystrokeHandler, // ADDED
} from 'ckeditor5';
export default class FormView extends View {
constructor( locale ) {
// View class constructor invoke.
// ...
this.focusTracker = new FocusTracker();
this.keystrokes = new KeystrokeHandler();
// Code prepared in the previous part.
// ...
}
render() {
// View.render() invocation and adding a submit handler.
// ...
this.childViews._items.forEach( view => {
// Register the view in the focus tracker.
this.focusTracker.add( view.element );
} );
// Start listening for the keystrokes coming from #element.
this.keystrokes.listenTo( this.element );
}
destroy() {
super.destroy();
this.focusTracker.destroy();
this.keystrokes.destroy();
}
// Previously declared helper methods.
// ...
}
# 添加焦点循环器
FocusCycler
将允许用户在表单视图的所有子元素中导航,并在它们之间循环。检查我们的表单视图中的导航是如何工作的——我们可以使用 Tab 从第一个输入字段移动到第二个,但随后焦点将离开表单,以及编辑器本身。让我们解决这个问题。
我们导入 FocusCycler
类,并在表单视图 constructor()
中创建它的实例。我们需要传入一个包含可聚焦元素(即我们的 childViews
集合)、焦点跟踪器、按键处理程序以及与不同按键相关的操作的对象。
// abbreviation/abbreviationview.js
import {
View,
LabeledFieldView,
createLabeledInputText,
ButtonView,
submitHandler,
FocusTracker,
KeystrokeHandler,
icons,
FocusCycler // ADDED
} from 'ckeditor5';
export default class FormView extends View {
constructor( locale ) {
// Previous code from the constructor.
// ...
this._focusCycler = new FocusCycler( {
focusables: this.childViews,
focusTracker: this.focusTracker,
keystrokeHandler: this.keystrokes,
actions: {
// Navigate form fields backwards using the Shift + Tab keystroke.
focusPrevious: 'shift + tab',
// Navigate form fields forwards using the Tab key.
focusNext: 'tab'
}
} );
this.setTemplate( {
// Setting the form template.
// ...
} );
}
// Previously declared methods.
// ...
}
现在,我们可以在 _createFromView()
函数中添加 Esc 按钮处理程序,它将隐藏 UI 并触发表单上的 cancel
。
// abbreviation/abbreviationui.js
// Previously imported packages.
// ...
export default class AbbreviationUI extends Plugin {
// Previously declared methods.
// ...
_createFormView() {
// Form view initialization.
// ...
// Close the panel on esc key press when the form has focus.
formView.keystrokes.set( 'Esc', ( data, cancel ) => {
this._hideUI();
cancel();
} );
return formView;
}
// Previously declared helper methods.
// ...
}
我们已经完成了为仅使用键盘的用户改进可访问性。通过在表单中按下 Tab、Shift + Tab 和 Esc 来自行尝试。
# 改进 UI 功能
当用户选择一个范围(一个字母、一个词或整个文档片段)并按下缩写按钮时,他们可能希望其选定的文本自动出现在缩写输入字段中。让我们将此功能添加到我们的表单中。
由于我们将使用文档中的用户选择,因此了解它在编辑器的模型中到底意味着什么非常重要。阅读我们关于 位置、范围和选择 的介绍,以扩展您在这方面的知识。
要将用户选择中的文本显示在表单字段中,我们需要首先获取并连接从选定范围中的所有文本。如果用户选择了一些段落、一个标题和一个图像,我们需要遍历所有节点,并且只使用包含文本的节点。
让我们在单独的 /utils.js
文件中创建一个辅助 getRangeText()
函数。它将使用其 getItems()
方法获取范围中的所有项目。然后,它将连接来自 text
和 textProxy
节点的所有文本,并跳过所有其他节点。
// abbreviation/utils.js
// A helper function that retrieves and concatenates all text within the model range.
export default function getRangeText( range ) {
return Array.from( range.getItems() ).reduce( ( rangeText, node ) => {
if ( !( node.is( 'text' ) || node.is( 'textProxy' ) ) ) {
return rangeText;
}
return rangeText + node.data;
}, '' );
}
现在,在 AbbreviationUI
中,我们可以调整 _showUI()
方法以将选定文本显示在缩写输入字段中。我们导入 getRangeText
并将选择中的第一个范围(使用 getFirstRange()
方法)作为参数传入。
我们还将在选择未折叠时禁用输入字段,因为如果选择跨越多个段落,则很难更改缩写的文本。
// abbreviation/abbreviationui.js
// Previously imported packages.
// ...
import getRangeText from './utils.js';
export default class AbbreviationUI extends Plugin {
// Previously declared methods.
// ...
_showUI() {
const selection = this.editor.model.document.selection;
this._balloon.add( {
view: this.formView,
position: this._getBalloonPositionData()
} );
// Disable the input when the selection is not collapsed.
this.formView.abbrInputView.isEnabled = selection.getFirstRange().isCollapsed;
const selectedText = getRangeText( selection.getFirstRange() );
this.formView.abbrInputView.fieldView.value = selectedText;
this.formView.titleInputView.fieldView.value = '';
this.formView.focus();
}
// Previously declared helper methods.
// ...
}
由于我们在某些情况下禁用了第一个输入字段,让我们相应地更新表单视图中的 focus()
方法。
// abbreviation/abbreviationview.js
// Previously imported packages.
// ...
export default class FormView extends View {
// Previously declared constructor and other methods.
// ...
focus() {
// If the abbreviation text field is enabled, focus it.
if ( this.abbrInputView.isEnabled ) {
this.abbrInputView.focus();
}
// Focus the abbreviation title field if the former is disabled.
else {
this.titleInputView.focus();
}
}
// Previously declared helper methods.
// ...
}
我们的新功能现在应该可以工作了,自己尝试一下!它不识别选定的文本是否已经是缩写,因此如果您选择“WYSIWYG”,则完整的标题尚未出现在标题输入字段中。我们将在接下来的步骤中更改它。
# 添加命令
我们的插件完成了我们想要它做的事情,那么为什么通过添加命令来使事情复杂化呢?嗯,命令不仅执行操作,而且还会在对模型应用任何更改时自动做出反应。
CKEditor 5 中的命令是操作和状态的组合。无论何时对模型进行任何更改,命令的状态都会刷新。我们强烈建议您 阅读有关命令的信息,然后再继续。
当用户在编辑器中进行选择时,命令将自动检查该选择中是否包含缩写。它还将确保只有在当前模型选择(例如,不在图像上)可以设置“缩写”属性的情况下,命令才被启用。
# 创建命令
让我们从创建命令开始,并将现有的操作逻辑移动到那里。
在 /abbreviationcommand.js
文件中,我们导入 Command
类并创建它的实例。
我们将从简单地将我们之前为 submit
创建的操作移动到 _createFormView()
方法中开始,将标题和缩写文本传递到命令的 execute()
方法中。
// abbreviation/abbreviationcommand.js
import { Command } from 'ckeditor5';
export default class AbbreviationCommand extends Command {
execute( { title, abbr } ) {
const model = this.editor.model;
model.change( writer => {
model.insertContent(
writer.createText( abbr, { abbreviation: title } )
);
} );
}
}
现在,让我们初始化 AbbreviationCommand
,通过将其添加到编辑器命令列表中的 AbbreviationEditing
中。我们还将传递一个名称,我们将使用它来调用我们的命令。
// abreviation/abbreviationediting.js
import { Plugin } from 'ckeditor5';
import AbbreviationCommand from './abbreviationcommand'; // ADDED
export default class AbbreviationEditing extends Plugin {
init() {
this._defineSchema();
this._defineConverters();
this.editor.commands.add(
'addAbbreviation', new AbbreviationCommand( this.editor )
);
}
// Previously declared methods.
// ...
}
我们现在可以用新的命令替换 submit
上调用的操作,将其传入编辑器的 execute()
方法中,以及缩写和标题值。
// abbreviation/abbreviationui.js
// Previously imported packages.
// ...
export default class AbbreviationUI extends Plugin {
// Previously declared methods.
// ...
_createFormView() {
const editor = this.editor;
const formView = new FormView( editor.locale );
// Execute the command after clicking the "Save" button.
this.listenTo( formView, 'submit', () => {
const value = {
abbr: formView.abbrInputView.fieldView.element.value,
title: formView.titleInputView.fieldView.element.value
};
editor.execute( 'addAbbreviation', value );
this._hideUI();
} );
// Handle clicking outside the balloon and on the "Cancel" button.
// ...
}
// Previously declared helper methods.
// ...
}
命令现在应该可以工作了,按下 Submit 按钮应该与之前具有相同的效果。我们现在可以探索一些额外的功能。您现在可以在 CKEditor 5 检查器中查看它。
# 刷新状态
由于命令的 refresh()
方法,我们可以观察命令的状态和值,不仅是在用户按下按钮时,而是在编辑器中进行任何更改时。我们将使用它来检查用户的选择是否已经具有缩写模型属性。
在我们这样做之前,我们可能想检查命令是否可以在给定的选择上使用。如果用户选择了一个图像,则命令应该被禁用。让我们使用其 checkAttributeInSelection()
方法来检查我们的 abbreviation
属性是否在模式中被允许。
// abbreviation/abbreviationcommand.js
import { Command } from 'ckeditor5';
export default class AbbreviationCommand extends Command {
refresh() {
const model = this.editor.model;
const selection = model.document.selection;
// The command is enabled when the "abbreviation" attribute
// can be set on the current model selection.
this.isEnabled = model.schema.checkAttributeInSelection(
selection, 'abbreviation'
);
}
execute( { title, abbr } ) {
// The code runs after command execution.
// ...
}
}
我们现在可以检查选定范围是否折叠。如果是,我们将检查光标是否在缩写中,并获取包含它的整个范围。我们可以使用 findAttributeRange
辅助函数轻松地做到这一点。我们需要将选择的首个位置、我们的属性名称和值以及模型传递给它。
然后,我们更改命令的值。我们将使用我们的 getRangeText
辅助函数获取缩写文本。我们还添加一个范围值,我们将在执行命令时使用它。
// abbreviation/abbreviationcommand.js
import {
Command,
findAttributeRange // ADDED
} from 'ckeditor5';
import getRangeText from './utils.js'; // ADDED
export default class AbbreviationCommand extends Command {
refresh() {
const model = this.editor.model;
const selection = model.document.selection;
const firstRange = selection.getFirstRange();
// When the selection is collapsed, the command has a value
// if the caret is in an abbreviation.
if ( firstRange.isCollapsed ) {
if ( selection.hasAttribute( 'abbreviation' ) ) {
const attributeValue = selection.getAttribute( 'abbreviation' );
// Find the entire range containing the abbreviation
// under the caret position.
const abbreviationRange = findAttributeRange(
selection.getFirstPosition(), 'abbreviation', attributeValue, model
);
this.value = {
abbr: getRangeText( abbreviationRange ),
title: attributeValue,
range: abbreviationRange
};
} else {
this.value = null;
}
}
// The code that enables the command.
// ...
}
execute( { title, abbr } ) {
// The code runs after command execution.
// ...
}
}
如果选择未折叠,我们将检查它是否具有 abbreviation
模型属性。如果是,我们将再次获取缩写的完整范围,并将其与用户选择进行比较。
当用户选择具有缩写属性的文本的一部分,以及没有缩写属性的一部分时,我们不希望更改命令的值。因此,我们将使用 containsRange()
方法来查看选定范围是否在缩写范围内。第二个参数使其成为 loose
检查,这意味着选定范围可以与缩写范围开始、结束或相等。
// abbreviation/abbreviationcommand.js
// Previously imported packages.
//...
export default class AbbreviationCommand extends Command {
refresh() {
const model = this.editor.model;
const selection = model.document.selection;
const firstRange = selection.getFirstRange();
if ( firstRange.isCollapsed ) {
// When the selection is collapsed, the command has a value
// if the caret is in an abbreviation.
// ...
}
// When the selection is not collapsed, the command has a value if the selection
// contains a subset of a single abbreviation or an entire abbreviation.
else {
if ( selection.hasAttribute( 'abbreviation' ) ) {
const attributeValue = selection.getAttribute( 'abbreviation' );
// Find the entire range containing the abbreviation
// under the caret position.
const abbreviationRange = findAttributeRange(
selection.getFirstPosition(), 'abbreviation', attributeValue, model
);
if ( abbreviationRange.containsRange( firstRange, true ) ) {
this.value = {
abbr: getRangeText( firstRange ),
title: attributeValue,
range: firstRange
};
} else {
this.value = null;
}
} else {
this.value = null;
}
}
// The code that enables the command.
// ...
}
execute( { title, abbr } ) {
// The code runs after command execution.
// ...
}
}
您可以在检查器中查看命令及其当前值。
我们现在可以检查用户按下工具栏缩写按钮时的命令值,并将缩写文本和标题值都插入到表单的输入字段中。
在 AbbreviationUI
中添加一个简单的 if
语句,使用命令的值或选定文本(如我们之前所做的那样)填充表单。
// abbreviation/abbreviationui.js
// Previously imported packages.
// ...
export default class AbbreviationUI extends Plugin {
// Previously declared methods.
// ...
_showUI() {
const selection = this.editor.model.document.selection;
// Check the value of the command.
const commandValue = this.editor.commands.get( 'addAbbreviation' ).value;
this._balloon.add( {
view: this.formView,
position: this._getBalloonPositionData()
} );
// Disable the input when the selection is not collapsed.
this.formView.abbrInputView.isEnabled = selection.getFirstRange().isCollapsed;
// Fill the form using the state (value) of the command.
if ( commandValue ) {
this.formView.abbrInputView.fieldView.value = commandValue.abbr;
this.formView.titleInputView.fieldView.value = commandValue.title;
}
// If the command has no value, put the currently selected text (not collapsed)
// in the first field and empty the second in that case.
else {
const selectedText = getRangeText( selection.getFirstRange() );
this.formView.abbrInputView.fieldView.value = selectedText;
this.formView.titleInputView.fieldView.value = '';
}
this.formView.focus();
}
// Previously declared helper methods.
// ...
}
# 改进 execute()
方法
现在我们应该在 execute()
方法中添加更多用例。首先,如果用户的选择没有折叠,我们只需要将缩写属性添加到他们的选择中,而不是将缩写文本插入模型中。
如果选择没有折叠,我们将使用模式的 getValidRanges()
方法收集所有允许使用 abbreviation
模式属性的范围。然后我们将使用 setAttribute()
将标题值添加到每个范围。
如果选择已折叠,我们将保留我们之前使用的 insertContent()
模式方法。然后,我们需要使用 removeSelectionAttribute
方法,以防止在用户开始输入时向缩写添加新内容。
// abbreviation/abbreviationcommand.js
// Previously imported packages.
// ...
export default class AbbreviationCommand extends Command {
refresh() {
// The code runs after the command refresh.
// ...
}
execute( { abbr, title } ) {
const model = this.editor.model;
const selection = model.document.selection;
model.change( writer => {
// If selection is collapsed then update the selected abbreviation
// or insert a new one at the place of caret.
if ( selection.isCollapsed ) {
model.insertContent(
writer.createText( abbr, { abbreviation: title } )
);
// Remove the "abbreviation" attribute attribute from the selection.
writer.removeSelectionAttribute( 'abbreviation' );
} else {
// If the selection has non-collapsed ranges,
// change the attribute on nodes inside those ranges
// omitting nodes where the "abbreviation" attribute is disallowed.
const ranges = model.schema.getValidRanges(
selection.getRanges(), 'abbreviation'
);
for ( const range of ranges ) {
writer.setAttribute( 'abbreviation', title, range );
}
}
} );
}
}
现在我们可以使用命令的状态来检查选择是否位于现有缩写内。如果命令的值不为 null
,我们将获取整个范围并更新其文本和标题。
我们将在插入的缩写末尾创建一个位置,并在那里设置选择。insertContent()
方法返回一个范围,我们获取其结束位置以定义我们的 positionAfter
。
// abbreviation/abbreviationcommand.js
// Previously imported packages.
// ...
export default class AbbreviationCommand extends Command {
refresh() {
// The code runs after the command refresh.
// ...
}
execute( { abbr, title } ) {
const model = this.editor.model;
const selection = model.document.selection;
model.change( writer => {
// If selection is collapsed then update the selected abbreviation
// or insert a new one at the place of caret.
if ( selection.isCollapsed ) {
// When a collapsed selection is inside text with the "abbreviation" attribute,
// update its text and title.
if ( this.value ) {
const { end: positionAfter } = model.insertContent(
writer.createText( abbr, { abbreviation: title } ),
this.value.range
);
// Put the selection at the end of the inserted abbreviation.
writer.setSelection( positionAfter );
}
writer.removeSelectionAttribute( 'abbreviation' );
} else {
// When the selection has non-collapsed node ranges, change the attribute on nodes inside those ranges
// omitting nodes where the "abbreviation" attribute is disallowed.
// ...
}
} );
}
}
如果折叠的选择不在现有缩写内,我们将在光标处插入一个带有 "abbreviation" 属性的文本节点。
用户可能会将缩写放在文本中,该文本已经具有其他一些模型属性,如 "bold" 或 "italic"。我们应该首先将它们与我们的缩写属性一起收集起来,并在将缩写插入文档时使用整个列表。我们将使用我们的 toMap
辅助函数来收集所有属性。
// abbreviation/abbreviationcommand.js
// More imports.
// ...
import { toMap } from 'ckeditor5'; // ADDED
export default class AbbreviationCommand extends Command {
refresh() {
// The code runs after the command refresh.
// ...
}
execute( { abbr, title } ) {
const model = this.editor.model;
const selection = model.document.selection;
model.change( writer => {
if ( selection.isCollapsed ) {
if ( this.value ) {
// When a collapsed selection is inside text
// with the "abbreviation" attribute, update texts.
// ...
}
// If the collapsed selection is not in an existing abbreviation,
// insert a text node with the "abbreviation" attribute
// in place of the caret.
// If the abbreviation is empty, don't do anything.
else if ( abbr !== '' ) {
const firstPosition = selection.getFirstPosition();
// Collect all attributes of the user selection.
const attributes = toMap( selection.getAttributes() );
// Put the new attribute to the map of attributes.
attributes.set( 'abbreviation', title );
// Inject the new text node with the abbreviation text
// with all selection attributes.
const { end: positionAfter } = model.insertContent(
writer.createText( abbr, attributes ), firstPosition
);
// Put the selection at the end of the inserted abbreviation.
writer.setSelection( positionAfter );
}
writer.removeSelectionAttribute( 'abbreviation' );
} else {
// When the selection has non-collapsed node ranges, change the attribute on nodes inside those ranges
// omitting nodes where the "abbreviation" attribute is disallowed.
// ...
}
} );
}
}
命令现在已经完成,通过尝试所有不同的用例来检查它的工作方式 - 选择已折叠、未折叠、位于现有缩写内等。
# 演示
查看实际效果。
缩写插件
CKEditor 5 是一款现代、功能丰富、世界一流的 WYSIWYG 编辑器。
# 最终代码
如果您在任何地方迷路了,这是 插件的最终实现。您可以将来自不同文件的代码粘贴到您的项目中,或者克隆并安装整个代码,它将开箱即用。
下一步
就这样,您已经完成了本教程!您现在已经准备好创建自己的插件。如果您想继续学习,请继续学习我们更高级的教程,从 实现一个块小部件 指南开始。
我们每天都在努力使我们的文档保持完整。您是否发现了过时的信息?是否缺少什么?请通过我们的 问题追踪器 报告它。
随着 42.0.0 版本的发布,我们重写了大部分文档以反映新的导入路径和功能。感谢您的反馈,帮助我们确保其准确性和完整性。