将跟踪更改集成到您的应用程序中
跟踪更改插件提供了一个 API,可让您管理添加到文档中的建议。要将建议保存到您的数据库并从数据库中访问建议,您首先需要集成此功能。
本指南介绍了如何将跟踪更改集成到独立插件中(异步版本)。如果您使用的是实时协作,请参考 实时协作功能集成 指南。
# 集成方法
本指南将讨论两种将 CKEditor 5 与您的建议数据源集成的方法
- 简单的“加载和保存”集成 使用
TrackChanges
插件 API。 - 适配器集成,它会立即将建议数据保存到数据库中。
适配器集成是推荐的方法,因为它可以让您更好地控制数据。
还建议将跟踪更改插件与评论插件一起使用。 查看如何将评论插件 与您的所见即所得编辑器集成。
# 开始之前
除了本指南之外,我们还提供了 可供下载的现成示例。您可以将这些示例用作集成示例或起点。
# 准备自定义编辑器设置
要使用跟踪更改插件,请准备一个自定义编辑器设置,其中包含异步版本的跟踪更改功能。
最简单的方法是使用 构建器。选择一个预设并开始自定义您的编辑器。
构建器允许您选择首选的发布方法和框架。在本指南中,我们将使用“Vanilla JS”选项与“npm”和一个基于“经典编辑器(基本)”预设的简单设置,以及启用的评论功能。
在构建器的“功能”部分(第二步)中,请确保
- 关闭“协作”组旁边的“实时”切换按钮。
- 启用“协作 → 跟踪更改”功能。
完成设置后,构建器将为您提供必要的 HTML、CSS 和 JavaScript 代码片段。我们将在下一步中使用这些代码片段。
# 设置示例项目
有了自定义编辑器设置后,我们需要一个简单的 JavaScript 项目来运行它。为此,我们建议您从我们的存储库中克隆基本项目模板
npx -y degit ckeditor/ckeditor5-tutorials-examples/sample-project sample-project
cd sample-project
npm install
然后,安装必要的依赖项
npm install ckeditor5
npm install ckeditor5-premium-features
此项目模板在后台使用 Vite,并包含我们将使用的 3 个源文件:index.html
、style.css
和 main.js
。
现在是时候使用我们的自定义编辑器设置了。转到 Builder 的“安装”部分,并将生成的代码片段复制到这 3 个文件中。
# 激活功能
要使用此高级功能,您需要使用许可证密钥激活它。有关详细信息,请参阅 许可证密钥和激活 指南。
成功获取许可证密钥后,打开 main.js
文件,并将 your-license-key
字符串更新为您的许可证密钥。
# 构建项目
最后,通过运行以下命令构建项目:
npm run dev
在浏览器中打开示例时,您应该会看到带有修订跟踪插件的所见即所得编辑器。但是,它仍然不加载或保存任何数据。您将在本指南的后面部分了解如何向修订跟踪插件添加数据。
现在让我们更深入地了解此设置的结构。
# 基本设置的解剖
以下示例实现了 宽侧边栏显示模式,用于修订跟踪注释。如果您想使用内联显示模式,请删除设置侧边栏的代码片段部分。
现在让我们一起看看此基本设置的关键部分。
# HTML 结构
页面的 HTML 和 CSS 结构创建了两列
<div class="editor-container__editor">
是编辑器使用的容器。<div class="editor-container__sidebar">
是侧边栏使用的容器,它包含注释(即修订跟踪)。
# JavaScript
main.js
文件设置编辑器实例
- 加载所有必要的编辑器插件(包括
TrackChanges
插件)。 - 设置
licenseKey
配置选项。 - 将
sidebar.container
配置选项设置为上面提到的容器。 - 将
trackChanges
按钮添加到编辑器工具栏。
# 评论
修订跟踪使用 评论插件 允许在建议中进行讨论。在开始集成建议之前,您应该熟悉 评论集成 指南。
因此,main.js
文件 在上一步骤中获取 在修订跟踪插件设置之上执行以下操作
- 加载
Comments
插件(TrackChanges
插件的依赖项)。 - 定义插件模板
- 用于
CommentsIntegration
插件(了解如何 保存评论)。 - 用于
UsersIntegrations
插件,该插件由修订跟踪和评论功能共享,我们将在本教程的后续步骤中使用。
- 用于
- 将
comment
和commentsArchive
按钮添加到编辑器工具栏。
# 下一步
我们已经设置了一个简单的 JavaScript 项目,它运行一个基本的 CKEditor 实例,该实例具有修订跟踪功能的异步版本。但是,它还没有处理加载或保存数据。接下来的两节将介绍两种可用的集成方法。
# 简单“加载和保存”集成
在此解决方案中,用户和建议数据在编辑器初始化期间加载,建议数据在您完成使用编辑器后保存(例如,当您提交包含所见即所得编辑器的表单时)。
仅当您信任用户或提供对提交数据的额外验证以确保用户仅更改了他们的建议时,才建议使用此方法。
作为对本指南的补充,我们提供 可供下载的现成示例。您可以将示例用作集成示例或起点。
以下集成使用修订跟踪 API。熟悉 API 可能有助于您理解代码片段。如有任何问题,请参阅 修订跟踪 API 文档。
# 加载数据
当修订跟踪插件已包含在编辑器中时,您需要创建插件来初始化用户和现有建议。
首先,将用户和建议数据转储到一个变量中,该变量将可供您的插件使用。
如果您的应用程序需要从服务器异步请求建议数据,而不是将数据放在 HTML 源代码中,您可以创建一个插件从数据库中获取数据。在这种情况下,您的插件应该 从 Plugin.init()
方法返回一个 Promise
,以确保编辑器初始化等待您的数据。
// Application data will be available under a global variable `appData`.
const appData = {
// Users data.
users: [
{
id: 'user-1',
name: 'Mex Haddox'
},
{
id: 'user-2',
name: 'Zee Croce'
}
],
// The ID of the current user.
userId: 'user-1',
// Comment threads data.
commentThreads: [
{
threadId: 'thread-1',
comments: [
{
commentId: 'comment-1',
authorId: 'user-1',
content: '<p>Are we sure we want to use a made-up disorder name?</p>',
createdAt: new Date( '09/20/2018 14:21:53' ),
attributes: {}
},
{
commentId: 'comment-2',
authorId: 'user-2',
content: '<p>Why not?</p>',
createdAt: new Date( '09/21/2018 08:17:01' ),
attributes: {}
}
],
context: {
type: 'text',
value: 'Bilingual Personality Disorder'
},
unlinkedAt: null,
resolvedAt: null,
resolvedBy: null,
attributes: {}
}
],
// Suggestions data.
suggestions: [
{
id: 'suggestion-1',
type: 'insertion',
authorId: 'user-2',
createdAt: new Date( 2019, 1, 13, 11, 20, 48 ),
data: null,
attributes: {}
},
{
id: 'suggestion-2',
type: 'deletion',
authorId: 'user-1',
createdAt: new Date( 2019, 1, 14, 12, 7, 20 ),
data: null,
attributes: {}
},
{
id: 'suggestion-3',
type: 'attribute:bold|ci1tcnk0lkep',
authorId: 'user-1',
createdAt: new Date( 2019, 2, 8, 10, 2, 7 ),
data: {
key: 'bold',
oldValue: null,
newValue: true
},
attributes: {
groupId: 'e29adbb2f3963e522da4d2be03bc5345f'
}
}
],
// Editor initial data.
initialData:
`<h2>
<comment-start name="thread-1"></comment-start>
Bilingual Personality Disorder
<comment-end name="thread-1"></comment-end>
</h2>
<p>
This may be the first time you hear about this
<suggestion-start name="insertion:suggestion-1:user-2"></suggestion-start>
made-up<suggestion-end name="insertion:suggestion-1:user-2"></suggestion-end>
disorder but it actually is not that far from the truth.
As recent studies show, the language you speak has more effects on you than you realize.
According to the studies, the language a person speaks affects their cognition,
<suggestion-start name="deletion:suggestion-2:user-1"></suggestion-start>
feelings, <suggestion-end name="deletion:suggestion-2:user-1"></suggestion-end>
behavior, emotions and hence <strong>their personality</strong>.
</p>
<p>
This shouldn’t come as a surprise
<a href="https://en.wikipedia.org/wiki/Lateralization_of_brain_function">since we already know</a>
that different regions of the brain become more active depending on the activity.
The structure, information and especially
<suggestion-start name="attribute:bold|ci1tcnk0lkep:suggestion-3:user-1"></suggestion-start><strong>the
culture of languages<suggestion-end name="attribute:bold|ci1tcnk0lkep:suggestion-3:user-1"></strong></suggestion-end>
varies substantially
and the language a person speaks is an essential element of daily life.
</p>`
};
Builder 的输出示例已经提供了三个插件的模板:UsersIntegration
、CommentsIntegration
和 TrackChangesIntegration
。用从 appData
读取数据并使用 Users
、CommentsRepository
和 TrackChanges
API 的插件替换它们
class UsersIntegration extends Plugin {
static get requires() {
return [ 'Users' ];
}
static get pluginName() {
return 'UsersIntegration';
}
init() {
const usersPlugin = this.editor.plugins.get( 'Users' );
// Load the users data.
for ( const user of appData.users ) {
usersPlugin.addUser( user );
}
// Set the current user.
usersPlugin.defineMe( appData.userId );
}
}
class CommentsIntegration extends Plugin {
static get requires() {
return [ 'CommentsRepository', 'UsersIntegration' ];
}
static get pluginName() {
return 'CommentsIntegration';
}
init() {
const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' );
// Load the comment threads data.
for ( const commentThread of appData.commentThreads ) {
commentsRepositoryPlugin.addCommentThread( commentThread );
}
}
}
class TrackChangesIntegration extends Plugin {
static get requires() {
return [ 'TrackChanges', 'UsersIntegration' ];
}
static get pluginName() {
return 'TrackChangesIntegration';
}
init() {
const trackChangesPlugin = this.editor.plugins.get( 'TrackChanges' );
// Load the suggestions data.
for ( const suggestion of appData.suggestions ) {
trackChangesPlugin.addSuggestion( suggestion );
}
}
}
更新 editorConfig.initialData
属性以使用 appData.initialData
值
const editorConfig = {
// ...
initialData: appData.initialData
// ...
};
然后构建项目
npm run dev
现在您应该看到一个具有一个评论线程和多个修订跟踪建议的编辑器实例。
# 保存数据
要保存建议数据,您首先需要从 TrackChanges
API 中获取它。为此,请使用 getSuggestions()
方法。
然后,使用建议数据以您喜欢的方式将其保存到数据库中。请参见以下示例。
在 index.html
中添加
<button id="get-data">Get data</button>
在 main.js
中使用链接的 then()
更新 ClassicEditor.create()
调用
ClassicEditor
.create( /* ... */ )
.then( editor => {
// After the editor is initialized, add an action to be performed after a button is clicked.
const trackChanges = editor.plugins.get( 'TrackChanges' );
// Get the data on demand.
document.querySelector( '#get-data' ).addEventListener( 'click', () => {
const editorData = editor.data.get();
const suggestionsData = trackChanges.getSuggestions( {
skipNotAttached: true,
toJSON: true
} );
// Now, use `editorData` and `suggestionsData` to save the data in your application.
// For example, you can set them as values of hidden input fields.
console.log( editorData );
console.log( suggestionsData );
} );
} )
.catch( error => console.error( error ) );
建议将 attributes
值字符串化为 JSON,将其作为字符串保存在数据库中,然后在加载建议时从 JSON 中解析该值。
# 演示
控制台
// Use the `Save data with track changes` button to see the result...
# 适配器集成
适配器集成使用您提供的适配器对象立即将建议保存到您的数据存储区。这是将修订跟踪与您的应用程序集成的推荐方式,因为它允许您更安全地处理客户端-服务器通信。例如,您可以检查用户权限,验证发送的数据或使用从服务器端获得的信息(例如建议创建日期)更新数据。在接下来的步骤中,您将看到如何处理服务器响应。
作为对本指南的补充,我们提供 可供下载的现成示例。您可以将示例用作集成示例或起点。
# 实现
首先,使用 TrackChanges#adapter
设置程序定义适配器。Adapter
方法 允许您在数据库中加载和保存更改。
在 UI 方面,建议中的每个更改都会立即执行,但是所有适配器操作都是异步的,并在后台执行。因此,所有适配器方法都需要返回一个 Promise
。当 promise 解析时,表示一切顺利,本地更改已成功保存到数据存储区。当 promise 被拒绝时,编辑器会抛出一个 CKEditorError 错误,它与 看门狗 功能配合得很好。当您处理服务器响应时,您可以决定是否应解析或拒绝 promise。
当适配器正在保存建议数据时,一个挂起操作会自动添加到编辑器的 PendingActions
插件中,因此您不必担心编辑器会在适配器操作完成之前被销毁。
请注意,使用 addSuggestion()
方法保存建议时,正确处理 suggestionData.originalSuggestionId
属性至关重要。否则,建议数据将不正确,这会导致某些情况下出现错误。
当您使用 addSuggestion()
方法保存建议时,应使用 suggestionData.originalSuggestionId
属性来设置正确的建议作者。请考虑以下示例
- 用户 A 创建了一个插入建议。
- 然后,用户 B 在该建议中开始输入,但修订跟踪模式处于关闭状态。
- 在这种情况下,原始建议被分成两部分,从而创建了一个新的建议。
- 虽然新建议是由用户 B 创建的,但实际作者是用户 A。
- 将新建议发送到数据库时,应使用正确的作者 ID(在本例中为用户 A)保存它。
- 作者应从原始建议中获取(使用
originalSuggestionId
)。
现在您已准备好实现适配器。
如果您已按照 “入门”部分中的建议 设置了示例项目,请打开 main.js
文件,并在导入语句之后添加以下代码
// Application data will be available under a global variable `appData`.
const appData = {
// Users data.
users: [
{
id: 'user-1',
name: 'Mex Haddox'
},
{
id: 'user-2',
name: 'Zee Croce'
}
],
// The ID of the current user.
userId: 'user-1',
// Comment threads data.
commentThreads: [
{
threadId: 'thread-1',
comments: [
{
commentId: 'comment-1',
authorId: 'user-1',
content: '<p>Are we sure we want to use a made-up disorder name?</p>',
createdAt: new Date( '09/20/2018 14:21:53' ),
attributes: {}
},
{
commentId: 'comment-2',
authorId: 'user-2',
content: '<p>Why not?</p>',
createdAt: new Date( '09/21/2018 08:17:01' ),
attributes: {}
}
],
context: {
type: 'text',
value: 'Bilingual Personality Disorder'
},
unlinkedAt: null,
resolvedAt: null,
resolvedBy: null,
attributes: {}
}
],
// Editor initial data.
initialData:
`<h2>
<comment-start name="thread-1"></comment-start>
Bilingual Personality Disorder
<comment-end name="thread-1"></comment-end>
</h2>
<p>
This may be the first time you hear about this
<suggestion-start name="insertion:suggestion-1:user-2"></suggestion-start>
made-up<suggestion-end name="insertion:suggestion-1:user-2"></suggestion-end>
disorder but it actually is not that far from the truth.
As recent studies show, the language you speak has more effects on you than you realize.
According to the studies, the language a person speaks affects their cognition,
<suggestion-start name="deletion:suggestion-2:user-1"></suggestion-start>
feelings, <suggestion-end name="deletion:suggestion-2:user-1"></suggestion-end>
behavior, emotions and hence <strong>their personality</strong>.
</p>
<p>
This shouldn’t come as a surprise
<a href="https://en.wikipedia.org/wiki/Lateralization_of_brain_function">since we already know</a>
that different regions of the brain become more active depending on the activity.
The structure, information and especially
<suggestion-start name="attribute:bold|ci1tcnk0lkep:suggestion-3:user-1"></suggestion-start><strong>the
culture of languages<suggestion-end name="attribute:bold|ci1tcnk0lkep:suggestion-3:user-1"></strong></suggestion-end>
varies substantially
and the language a person speaks is an essential element of daily life.
</p>`
};
Builder 的输出示例已经提供了三个插件的模板:UsersIntegration
、CommentsIntegration
和 TrackChangesIntegration
。用从 appData
读取数据并使用 Users
、CommentsRepository
和 TrackChanges
API 的插件替换它们
class UsersIntegration extends Plugin {
static get requires() {
return [ 'Users' ];
}
static get pluginName() {
return 'UsersIntegration';
}
init() {
const usersPlugin = this.editor.plugins.get( 'Users' );
// Load the users data.
for ( const user of appData.users ) {
usersPlugin.addUser( user );
}
// Set the current user.
usersPlugin.defineMe( appData.userId );
}
}
class CommentsIntegration extends Plugin {
static get requires() {
return [ 'CommentsRepository', 'UsersIntegration' ];
}
static get pluginName() {
return 'CommentsIntegration';
}
init() {
const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' );
// Load the comment threads data.
for ( const commentThread of appData.commentThreads ) {
commentsRepositoryPlugin.addCommentThread( commentThread );
}
}
}
class TrackChangesIntegration extends Plugin {
static get requires() {
return [ 'TrackChanges', 'UsersIntegration' ];
}
static get pluginName() {
return 'TrackChangesIntegration';
}
init() {
const trackChangesPlugin = this.editor.plugins.get( 'TrackChanges' );
// Set the adapter to the `TrackChanges#adapter` property.
trackChangesPlugin.adapter = {
getSuggestion: suggestionId => {
console.log( 'Getting suggestion', suggestionId );
// Write a request to your database here.
// The returned `Promise` should be resolved with the suggestion
// data object when the request has finished.
switch ( suggestionId ) {
case 'suggestion-1':
return Promise.resolve( {
id: suggestionId,
type: 'insertion',
authorId: 'user-2',
createdAt: new Date(),
data: null,
attributes: {}
} );
case 'suggestion-2':
return Promise.resolve( {
id: suggestionId,
type: 'deletion',
authorId: 'user-1',
createdAt: new Date(),
data: null,
attributes: {}
} );
case 'suggestion-3':
return Promise.resolve( {
id: 'suggestion-3',
type: 'attribute:bold|ci1tcnk0lkep',
authorId: 'user-1',
createdAt: new Date( 2019, 2, 8, 10, 2, 7 ),
data: {
key: 'bold',
oldValue: null,
newValue: true
},
attributes: {
groupId: 'e29adbb2f3963e522da4d2be03bc5345f'
}
} );
}
},
addSuggestion: suggestionData => {
console.log( 'Suggestion added', suggestionData );
// Write a request to your database here.
// The returned `Promise` should be resolved when the request
// has finished. When the promise resolves with the suggestion data
// object, it will update the editor suggestion using the provided data.
return Promise.resolve( {
createdAt: new Date() // Should be set on the server side.
} );
},
updateSuggestion: ( id, suggestionData ) => {
console.log( 'Suggestion updated', id, suggestionData );
// Write a request to your database here.
// The returned `Promise` should be resolved when the request
// has finished.
return Promise.resolve();
}
};
// In order to load comments added to suggestions, you
// should also integrate the comments adapter.
}
}
更新 editorConfig.initialData
属性以使用 appData.initialData
值
const editorConfig = {
// ...
initialData: appData.initialData
// ...
};
然后构建项目
npm run dev
您现在应该看到一个具有一个评论线程和多个修订跟踪建议的编辑器实例。适配器现在已准备好与您的富文本编辑器一起使用。
建议将 attributes
值字符串化为 JSON,将其作为字符串保存在数据库中,然后在加载建议时从 JSON 中解析该值。
请注意,此示例不包含评论适配器。查看评论集成 指南以了解如何构建完整的解决方案。另外,请注意这两个代码片段都定义了相同的用户列表。请确保对该代码进行去重,并且只定义一次用户列表,以避免出现错误。
# 演示
挂起适配器操作控制台
// Add a suggestion to see the result...
由于跟踪更改适配器会在执行建议后立即保存建议,因此建议使用 Autosave 插件在每次更改后保存编辑器内容。
# 为什么我在接受或放弃建议时没有事件?
请注意,当您放弃或接受建议时,适配器中不会触发任何事件。这是因为在编辑会话期间建议永远不会从编辑器中删除。您可以使用撤销(Cmd+Z 或 Ctrl+Z)恢复它。当您删除包含建议的段落时,也会发生同样的事情 - 不会触发任何事件,因为没有真正删除数据。
但是,为了确保您不会在数据库中保留过时的建议,您应该在编辑器被销毁或关闭时进行清理。您可以将存储在编辑器数据中的建议与存储在数据库中的建议进行比较,并从数据库中删除不再存在于编辑器数据中的所有建议。
# 跟踪更改示例
请访问 ckeditor5-collaboration-samples
GitHub 存储库以查找跟踪更改功能的多个示例集成。
我们每天都在努力使我们的文档保持完整。您是否发现了过时信息?是否缺少某些内容?请通过我们的 问题跟踪器 报告。
随着 42.0.0 版的发布,我们重新编写了许多文档以反映新的导入路径和功能。感谢您的反馈,帮助我们确保其准确性和完整性。