第三方 UI
CKEditor 5 是一个模块化的编辑框架,允许多种灵活的配置。 这包括在基本编辑器类之上使用第三方用户界面。
在本指南中,一个 类似经典的 编辑器将绑定到一个完全独立的、现有的在 Bootstrap 中创建的 UI,提供启动编辑所需的必要的基本结构和工具栏项目。
# 准备编辑器端
编辑器类型,例如 经典 或 内联编辑器,具有专用的默认用户界面和主题。 但是,要创建一个绑定到 Bootstrap UI 的编辑器实例,只需要有限的功能子集。 你需要先导入它们
// Basic classes to create an editor.
import {
Editor,
ComponentFactory,
EditorUI,
EditorUIView,
InlineEditableUIView,
ElementReplacer,
FocusTracker,
// Interfaces to extend the basic Editor API.
ElementApiMixin,
// Helper function for adding interfaces to the Editor class.
mix,
// Helper function that gets the data from an HTML element that the Editor is attached to.
getDataFromElement,
// Helper function that binds the editor with an HTMLForm element.
attachToForm,
// Basic features that every editor should enable.
Clipboard,
Enter,
Paragraph,
Typing,
UndoEditing,
// Basic features associated with the edited content.
BoldEditing,
ItalicEditing,
UnderlineEditing,
HeadingEditing
} from 'ckeditor5';
请注意,不是 Bold
,它加载默认的粗体 UI 和粗体编辑功能,而是只导入 BoldEditing
。 它提供了与编辑任何粗体文本相关的 引擎 功能,但没有实际的 UI。
同样,还导入了 ItalicEditing
、UnderlineEditing
、HeadingEditing
和 UndoEditing
。
导入基本编辑器组件后,你可以定义自定义的 BootstrapEditor
类,该类扩展了 Editor
// Extending the Editor class, which brings the base editor API.
export default class BootstrapEditor extends ElementApiMixin( Editor ) {
constructor( element, config ) {
super( config );
// Remember the element the editor is created with.
this.sourceElement = element;
// Create the ("main") root element of the model tree.
this.model.document.createRoot();
// The UI layer of the editor.
this.ui = new BootstrapEditorUI( this );
// When editor#element is a textarea inside a form element,
// the content of this textarea will be updated on form submit.
attachToForm( this );
}
destroy() {
// When destroyed, the editor sets the output of editor#getData() into editor#element...
this.updateSourceElement();
// ...and destroys the UI.
this.ui.destroy();
return super.destroy();
}
static create( element, config ) {
return new Promise( resolve => {
const editor = new this( element, config );
resolve(
editor.initPlugins()
// Initialize the UI first. See the BootstrapEditorUI class to learn more.
.then( () => editor.ui.init( element ) )
// Fill the editable with the initial data.
.then( () => editor.data.init( getDataFromElement( element ) ) )
// Fire the `editor#ready` event that announce the editor is complete and ready to use.
.then( () => editor.fire( 'ready' ) )
.then( () => editor )
);
} );
}
}
# 创建 Bootstrap UI
虽然编辑器已准备好使用,但它只是一个裸露的可编辑区域——这对用户来说没有多大用处。 你需要给它一个实际的界面,包括工具栏和按钮。
请参阅 Bootstrap 的 入门 指南,了解如何在你的网页中包含 Bootstrap。
在网页中加载 Bootstrap 框架后,你可以在 HTML 中定义编辑器的实际 UI
<!-- The outermost container of the editor. -->
<div class="ck-editor">
<!-- The toolbar of the editor. -->
<div class="btn-toolbar" role="toolbar" aria-label="Editor toolbar">
<!-- The headings dropdown. -->
<div class="btn-group mr-2" role="group" aria-label="Headings">
<div class="dropdown" id="heading">
<button class="btn btn-primary btn-sm dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span>Headings</span></button>
<div class="dropdown-menu" aria-labelledby="heading-button"></div>
</div>
</div>
<!-- Basic styles buttons. -->
<div class="btn-group mr-2" role="group" aria-label="Basic styles">
<button type="button" class="btn btn-primary btn-sm" id="bold">B</button>
<button type="button" class="btn btn-primary btn-sm" id="italic">I</button>
<button type="button" class="btn btn-primary btn-sm" id="underline">U</button>
</div>
<!-- Undo and redo buttons. -->
<div class="btn-group mr-2" role="group" aria-label="Undo">
<button type="button" class="btn btn-primary btn-sm" id="undo">←</button>
<button type="button" class="btn btn-primary btn-sm" id="redo">→</button>
</div>
</div>
<!-- The container with the data of the editor. -->
<div id="editor">
<p>Hello world!</p>
</div>
</div>
虽然 Bootstrap 提供了大部分 CSS,但它并没有提供专门针对 所见即所得 文本编辑器的样式,需要进行一些调整
/* Give the editor some space and limits using a border. */
.ck-editor {
margin: 1em 0;
border: 1px solid hsla(0, 0%, 0%, 0.1);
border-radius: 4px;
}
/* Adding internal spacing, border and background to the toolbar. */
.ck-editor .btn-toolbar {
padding: .5rem;
background: hsl(240, 14%, 97%);
border-bottom: 1px solid hsla(0, 0%, 0%, 0.1);
}
/* Tweaking the editable area for better readability. */
.ck-editor .ck-editor__editable {
padding: 2em 2em 1em;
overflow: auto;
}
/* When in read–only mode, the editable should fade out. */
.ck-editor .ck-editor__editable.ck-read-only {
background: hsl(0, 0%, 98%);
color: hsl(0, 0%, 47%);
}
/* Make sure the headings dropdown button does not change its size
as different headings are selected. */
.ck-editor .dropdown-toggle span {
display: inline-block;
width: 100px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: bottom;
}
/* Make the headings dropdown items visually distinctive. */
.ck-editor .heading-item_heading1 { font-size: 1.5em; }
.ck-editor .heading-item_heading2 { font-size: 1.3em; }
.ck-editor .heading-item_heading3 { font-size: 1.1em; }
.ck-editor [class*="heading-item_"] {
line-height: 22px;
padding: 10px;
}
.ck-editor [class*="heading-item_heading"] {
font-weight: bold;
}
/* Give the basic styles buttons the icon–like look and feel. */
.ck-editor #bold { font-weight: bold; }
.ck-editor #italic { font-style: italic; }
.ck-editor #underline { text-decoration: underline; }
# 绑定 UI 与编辑器
在此阶段,你应该将本指南开头创建的编辑器与 HTML 中定义的 Bootstrap UI 绑定。 所有 UI 逻辑都将封装在一个与 EditorUI
接口 匹配的单独类中。 你可能在 BootstrapEditor
的构造函数中注意到了这一行
this.ui = new BootstrapEditorUI( this );
定义 BootstrapEditorUI
,然后仔细查看该类的内容
// The class organizing the UI of the editor, binding it with existing Bootstrap elements in the DOM.
class BootstrapEditorUI extends EditorUI {
constructor( editor ) {
super( editor );
// A helper to easily replace the editor#element with editor.editable#element.
this._elementReplacer = new ElementReplacer();
// The global UI view of the editor. It aggregates various Bootstrap DOM elements.
const view = this._view = new EditorUIView( editor.locale );
// This is the main editor element in the DOM.
view.element = $( '.ck-editor' );
// This is the editable view in the DOM. It will replace the data container in the DOM.
view.editable = new InlineEditableUIView( editor.locale, editor.editing.view );
// References to the dropdown elements for further usage. See #_setupBootstrapHeadingDropdown.
view.dropdownMenu = view.element.find( '.dropdown-menu' );
view.dropdownToggle = view.element.find( '.dropdown-toggle' );
// References to the toolbar buttons for further usage. See #_setupBootstrapToolbarButtons.
view.toolbarButtons = {};
[ 'bold', 'italic', 'underline', 'undo', 'redo' ].forEach( name => {
// Retrieve the jQuery object corresponding with the button in the DOM.
view.toolbarButtons[ name ] = view.element.find( `#${ name }` );
} );
}
// All EditorUI subclasses should expose their view instance
// so other UI classes can access it if necessary.
get view() {
return this._view;
}
init( replacementElement ) {
const editor = this.editor;
const view = this.view;
const editingView = editor.editing.view;
// Make sure the EditorUIView is rendered. This will, for instance, create a place for UI elements
// like floating panels detached from the main editor UI in DOM.
this._view.render();
// Create an editing root in the editing layer. It will correspond with the
// document root created in the constructor().
const editingRoot = editingView.document.getRoot();
// The editable UI and editing root should share the same name.
view.editable.name = editingRoot.rootName;
// Render the editable component in the DOM first.
view.editable.render();
const editableElement = view.editable.element;
// Register editable element so it is available via getEditableElement() method.
this.setEditableElement( view.editable.name, editableElement );
// Let the editable UI element respond to the changes in the global editor focus tracker
// and let the focus tracker know about the editable element.
this.focusTracker.add( editableElement );
view.editable.bind( 'isFocused' ).to( this.focusTracker );
// Bind the editable UI element to the editing view, making it an end– and entry–point
// of the editor's engine. This is where the engine meets the UI.
editingView.attachDomRoot( editableElement );
// Setup the existing, external Bootstrap UI so it works with the rest of the editor.
this._setupBootstrapToolbarButtons();
this._setupBootstrapHeadingDropdown();
// Replace the editor#element with editor.editable#element.
this._elementReplacer.replace( replacementElement, editableElement );
// Tell the world that the UI of the editor is ready to use.
this.fire( 'ready' );
}
destroy() {
super.destroy();
// Restore the original editor#element.
this._elementReplacer.restore();
// Destroy the view.
this._view.editable.destroy();
this._view.destroy();
}
// This method activates Bold, Italic, Underline, Undo and Redo buttons in the toolbar.
_setupBootstrapToolbarButtons() {
// Implementation details are in the following snippets.
// ...
}
// This method activates the headings dropdown in the toolbar.
_setupBootstrapHeadingDropdown() {
// Implementation details are in the following snippets.
// ...
}
}
编辑器中的几乎每个功能都定义了一些命令,例如 HeadingCommand
或 UndoCommand
。 命令可以执行
editor.execute( 'undo' );
它们还带有默认的可观察属性,如 value
和 isEnabled
。 这些是创建自定义用户界面的切入点,因为它们的值代表了编辑器的实际状态。 你可以在简单的事件监听器中跟踪它们
const command = editor.commands.get( 'undo' );
command.on( 'change:isEnabled', ( evt, name, isEnabled ) => {
if ( isEnabled ) {
console.log( 'Whoa, you can undo some stuff now.' );
} else {
console.log( 'There is nothing to undo in the editor.' );
}
} );
要了解有关编辑器命令的更多信息,请查看 Command
API。 你也可以 console.log
活跃编辑器的 editor.commands
集合,以了解它提供了哪些命令。
了解这一点后,请填写 BootstrapEditorUI
的缺失方法。
# 将按钮绑定到编辑器命令
_setupBootstrapToolbarButtons()
是一个将 Bootstrap 工具栏按钮绑定到编辑器功能(命令)的方法。 它在点击时激活相应的编辑器命令,并使按钮监听命令的状态以更新其 CSS 类
// This method activates Bold, Italic, Underline, Undo and Redo buttons in the toolbar.
_setupBootstrapToolbarButtons() {
const editor = this.editor;
for ( const name in this.view.toolbarButtons ) {
// Retrieve the editor command corresponding with the ID of the button in the DOM.
const command = editor.commands.get( name );
const button = this.view.toolbarButtons[ name ];
// Clicking the buttons should execute the editor command...
button.click( () => editor.execute( name ) );
// ...but it should not steal the focus so the editing is uninterrupted.
button.mousedown( evt => evt.preventDefault() );
const onValueChange = () => {
button.toggleClass( 'active', command.value );
};
const onIsEnabledChange = () => {
button.attr( 'disabled', () => !command.isEnabled );
};
// Commands can become disabled, e.g. when the editor is read-only.
// Make sure the buttons reflect this state change.
command.on( 'change:isEnabled', onIsEnabledChange );
onIsEnabledChange();
// Bold, Italic and Underline commands have a value that changes
// when the selection starts in an element the command creates.
// The button should indicate that e.g. you are editing text which is already bold.
if ( !new Set( [ 'undo', 'redo' ] ).has( name ) ) {
command.on( 'change:value', onValueChange );
onValueChange();
}
}
}
# 将下拉菜单绑定到标题命令
工具栏中的下拉菜单是一个更复杂的情况。
首先,它必须填充标题选项,以便用户可以选择。 然后,单击每个选项必须在编辑器中执行相关的标题命令。 最后,下拉菜单按钮和下拉菜单项必须反映编辑器的状态,例如,当选择落在标题中时,应该激活适当的菜单项,并且按钮应该显示标题级别的名称。
// This method activates the headings dropdown in the toolbar.
_setupBootstrapHeadingDropdown() {
const editor = this.editor;
const dropdownMenu = this.view.dropdownMenu;
const dropdownToggle = this.view.dropdownToggle;
// Retrieve the editor commands for heading and paragraph.
const headingCommand = editor.commands.get( 'heading' );
const paragraphCommand = editor.commands.get( 'paragraph' );
// Create a dropdown menu entry for each heading configuration option.
editor.config.get( 'heading.options' ).map( option => {
// Check if options is a paragraph or a heading as their commands differ slightly.
const isParagraph = option.model === 'paragraph';
// Create the menu item DOM element.
const menuItem = $(
`<a href="#" class="dropdown-item heading-item_${ option.model }">` +
`${ option.title }` +
'</a>'
);
// Upon click, the dropdown menu item should execute the command and focus
// the editing view to keep the editing process uninterrupted.
menuItem.click( () => {
const commandName = isParagraph ? 'paragraph' : 'heading';
const commandValue = isParagraph ? undefined : { value: option.model };
editor.execute( commandName, commandValue );
editor.editing.view.focus();
} );
dropdownMenu.append( menuItem );
const command = isParagraph ? paragraphCommand : headingCommand;
// Make sure the dropdown and its items reflect the state of the
// currently active command.
const onValueChange = isParagraph ? onValueChangeParagraph : onValueChangeHeading;
command.on( 'change:value', onValueChange );
onValueChange();
// Heading commands can become disabled, e.g. when the editor is read-only.
// Make sure the UI reflects this state change.
command.on( 'change:isEnabled', onIsEnabledChange );
onIsEnabledChange();
function onValueChangeHeading() {
const isActive = !isParagraph && command.value === option.model;
if ( isActive ) {
dropdownToggle.children( ':first' ).text( option.title );
}
menuItem.toggleClass( 'active', isActive );
}
function onValueChangeParagraph() {
if ( command.value ) {
dropdownToggle.children( ':first' ).text( option.title );
}
menuItem.toggleClass( 'active', command.value );
}
function onIsEnabledChange() {
dropdownToggle.attr( 'disabled', () => !command.isEnabled );
}
} );
}
# 运行编辑器
当编辑器类和用户界面准备就绪时,就该运行编辑器了。 只要确保所有插件都已加载,并将正确的 DOM 元素传递给 BootstrapEditor#create
BootstrapEditor.create( $( '#editor' ).get( 0 ), {
plugins: [
Clipboard, Enter, Typing, Paragraph,
BoldEditing, ItalicEditing, UnderlineEditing, HeadingEditing, UndoEditing
]
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
一旦一切按预期工作,你可能希望创建一个自定义的编辑器预设,以便在应用程序之间发布它。 要了解有关此方面的更多信息,请查看 创建自定义构建指南。
我们每天都在努力使我们的文档保持完整。 你是否发现了过时信息? 是否缺少某些内容? 请通过我们的 问题跟踪器 报告它。
随着 42.0.0 版的发布,我们重新编写了大部分文档以反映新的导入路径和功能。 感谢您的反馈,帮助我们确保文档的准确性和完整性。