创建一个功能相对完善的插件
1、概述
上篇那个只是一个简单插件的demo,这篇我们来创建一个可正常编辑,换行的插件,因为按上篇那种方式创建出来,设置默认的文字之类的然后去进行编辑会有一些问题。
比如你想插入一个文字然后渲染成一下结构,文字可正常编辑换行
<div><p>default word</p></div>
但是你执行writer.insertText('default word', parent);
之后插入的元素会渲染成下面的 (这是在command文件内执行的)
<div>default word</div>
这个时候如果你编辑或者干嘛,会生成下面的结构
<div>default word<p>你编辑的内容</p></div>
还有创建一个插件想选中或者怎样上篇是不能实现的,我们在这个插件里都会一一解决。
2、创建项目
初始化项目:
npm init -y
安装项目依赖
npm install --save postcss-loader@3 raw-loader@3 style-loader@1 webpack@4 webpack-cli@3
创建webpack.config.js
文件
'use strict';
const path = require('path');
const { styles } = require('@ckeditor/ckeditor5-dev-utils');
module.exports = {
// https://webpack.js.org/configuration/entry-context/
entry: './app.js',
// https://webpack.js.org/configuration/output/
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /ckeditor5-[^/\\]+[/\\]theme[/\\]icons[/\\][^/\\]+\.svg$/,
use: ['raw-loader']
},
{
test: /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/,
use: [
{
loader: 'style-loader',
options: {
injectType: 'singletonStyleTag'
}
},
{
loader: 'postcss-loader',
options: styles.getPostCssConfig({
themeImporter: {
themePath: require.resolve('@ckeditor/ckeditor5-theme-lark')
},
minify: true
})
}
]
}
]
},
// Useful for debugging.
devtool: 'source-map',
// By default webpack logs warnings if the bundle is bigger than 200kb.
performance: { hints: false }
};
安装编辑器依赖
npm install --save @ckeditor/ckeditor5-dev-utils @ckeditor/ckeditor5-editor-classic @ckeditor/ckeditor5-essentials @ckeditor/ckeditor5-paragraph @ckeditor/ckeditor5-basic-styles @ckeditor/ckeditor5-theme-lark
创建APP.js
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
ClassicEditor
.create(document.querySelector('#editor'), {
plugins: [Essentials, Paragraph, Bold, Italic],
toolbar: ['bold', 'italic']
})
.then(editor => {
console.log('Editor was initialized', editor);
})
.catch(error => {
console.error(error.stack);
});
修改package.json
,因为要经常编译,所以我加了--watch
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --mode development --watch"
}
然后构建
npm run build
创建HTML文件index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>CKEditor 5 Framework – Quick start</title>
</head>
<body>
<div id="editor">
<p>Editor content goes here.</p>
</div>
<script src="dist/bundle.js"></script>
</body>
</html>
然后浏览器打开HTML,这个时候就展示出了一个基本的编辑器最初的模样~以上都是基于文档:https://ckeditor.com/docs/ckeditor5/latest/framework/guides/quick-start.html 现在我们来正式开始创建插件
3、创建插件
我们来创建一个提示框插件,插入一个提示框,上面有一个提示的标题,中间部分内容可编辑,输入想要的文字
首先我们来创建一个tip
文件夹,在里面创建四个文件tip.js
,tipEdit.js
,tipUi.js
,tipCommand.js
,不清楚为什么要创建这四个的可以看前面的。
3.1 编写tip.js
import ui from './tipUi'
import edit from './tipEdit'
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import Widget from '@ckeditor/ckeditor5-widget/src/widget';
export default class writeBox extends Plugin {
static get requires() {
return [ui, edit, Widget];
}
}
3.2 确定HTML结构
在编辑edit.js文件之前,我们来确定一下HTML结构,方便写模型及转换器,如果脑子好的话也可以不先写HTML模板,直接开始操作,我脑子不行,所以先写个HTML结构来明确DOM结构
<div class="edit-item-box box">
<div class="tip-content-box">
<div class="tip-content" >
<p>1. 多喝水</p>
<p>2. 多喝热水</p>
</div>
<div class="close"><span class="closeIcon">x</span></div>
<div class="tip-title">
<p>提示的标题</p>
</div>
</div>
</div>
CSS样式,真正写插件的时候写成单独的文件,然后引入到edit.js
里面即可,这里我就直接扔index.html
文件里了
<style>
.tip-content-box{
width: 100%;
position: relative;
border: 1px solid #ccc;
padding: 6px 10px;
box-sizing: border-box;
border-radius: 4px;
cursor: pointer;
}
.tip-content-box:hover .close{
display: block;
}
.tip-title{
background: white;
text-align: center;
font-size: 14px;
position: absolute;
top: -10px;
cursor: text;
height: 20px;
left: 50%;
transform: translateX(-50%);
}
.edit-item-box{
margin: 20px 0;
}
.tip-content{
font-size: 14px;
cursor: text;
}
.close{
position: absolute;
top: -7px;
right: -4px;
width: 14px;
height: 14px;
color:white;
background:#ccc;
text-align: center;
line-height: 12px;
border-radius: 50%;
display: none;
z-index: 99;
cursor: pointer;
}
.tip-title p{
padding: 0 10px;
color: #365f93;
font-weight: bold;
transform: translateY(-15px);
}
.closeIcon{
transform: scaleX(1.2) translateX(0px) translateY(-1.5px);
display: inline-block;
font-size: 12px;
}
</style>
3.3 编写edit.js
先来安装一个插件npm install @ckeditor/ckeditor5-widget --save
,安装完之后记得重新build
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import command from './tipCommand';
import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
export default class tipEdit extends Plugin {
init() {
this._defineSchema();
this._defineConverters();
this.editor.commands.add('tip', new command(this.editor));
}
_defineSchema() {
const schema = this.editor.model.schema;
// 注册模型
schema.register('editItemBox', {
// 是否为一个完整的对象,不可被回车拆分,意思是回车等行为都是在它自身容器内进行
isObject: true,
allowWhere: '$block',
})
schema.register('tipContentBox', {
allowWhere: '$block',
isLimit: true,
allowIn: 'editItemBox',
})
schema.register('tipTitle', {
isLimit: true,
allowIn: 'tipContentBox',
isObject: true,
allowContentOf: '$root'
})
schema.register('tipContent', {
isLimit: true,
allowIn: 'tipContentBox',
isObject: true,
allowContentOf: '$root'
})
schema.register('tipClose', {
isLimit: true,
allowIn: 'tipContentBox',
allowContentOf: '$block'
});
schema.register('tipSpan', {
isLimit: true,
allowIn: 'tipClose',
allowContentOf: '$block'
});
}
_defineConverters() {
const conversion = this.editor.conversion;
// 使用editingDowncast数据管道
// 创建一个元件
// model的名称整个项目需要唯一
conversion.for('editingDowncast').elementToElement({
model: 'editItemBox',
view: (modelItem, viewWriter) => {
const viewWrapper = viewWriter.createContainerElement('div');
viewWriter.addClass('edit-item-box', viewWrapper);
return toHorizontalLineWidget(viewWrapper, viewWriter);
}
});
// 创建一个普通的转换器
conversion.elementToElement({
model: 'tipContentBox',
view: {
name: 'div',
classes: 'tip-content-box'
}
});
conversion.elementToElement({
model: 'tipClose',
view: {
name: 'div',
classes: 'close'
}
});
conversion.elementToElement({
model: 'tipSpan',
view: {
name: 'span',
classes: 'closeIcon'
}
});
// 创建一个普通的可编辑可选择的转换器
conversion.for('editingDowncast').elementToElement({
model: 'tipTitle',
view: (modelItem, writer) => {
const nested = writer.createEditableElement('div', { class: 'tip-title' });
writer.setAttribute('contenteditable', nested.isReadOnly ? 'false' : 'true', nested);
nested.on('change:isReadOnly', (evt, property, is) => {
writer.setAttribute('contenteditable', is ? 'false' : 'true', nested);
});
return nested;
}
});
conversion.for('editingDowncast').elementToElement({
model: 'tipContent',
view: (modelItem, writer) => {
const nested = writer.createEditableElement('div', { class: 'tip-content' });
writer.setAttribute('contenteditable', nested.isReadOnly ? 'false' : 'true', nested);
nested.on('change:isReadOnly', (evt, property, is) => {
writer.setAttribute('contenteditable', is ? 'false' : 'true', nested);
});
return nested;
}
});
}
}
// 这个地方转换原件之后就可以进行选择之类的操作,但是这个方法是不能编辑的,因为源码里面给设置false了,具体操作可以看源码
// 源码:node_modules/@ckeditor/ckeditor5-widget/src/utils.js => export function toWidget( element, writer, options = {} ) {}
function toHorizontalLineWidget(viewElement, writer, label) {
// 在当前元素上设置自定义属性, key value targetElement
writer.setCustomProperty('tip', true, viewElement);
// toWidget 转化为元件
return toWidget(viewElement, writer, { label });
}
3.4 编写tipUi.js
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
// import icon from './icons/insert.svg';
export default class tipUi extends Plugin {
init() {
const editor = this.editor;
const t = editor.t;
editor.ui.componentFactory.add('tip', locale => {
const view = editor.editing.view;
const command = editor.commands.get('tip');
view.addObserver(ClickObserver);
const buttonView = new ButtonView(locale);
buttonView.set({
label: t('插入tip'),
// 如果要用icon可以把这里打开,等会儿再说icon的问题
// icon: icon,
tooltip: true,
// true表示使用文字,false表示用icon,可以一起用,但是会并列
withText:true
});
buttonView.bind('isOn', 'tip').to(command, 'value', 'isEnabled');
this.listenTo(buttonView, 'execute', () => editor.execute('tip'));
return buttonView;
});
}
}
3.5 编写tipCommand.js
import Command from '@ckeditor/ckeditor5-core/src/command';
export default class tipCommand extends Command {
execute() {
this.editor.model.change(writer => {
this.editor.model.insertContent(createTip(writer));
});
}
}
function createTip(writer) {
const editItemBox = writer.createElement('editItemBox');
const tipContentBox = writer.createElement('tipContentBox');
const tipTitle = writer.createElement('tipTitle');
const tipContent = writer.createElement('tipContent');
// 创建关闭按钮
const tipClose = writer.createElement('tipClose');
const span = writer.createElement('tipSpan');
writer.insertText('x', writer.createPositionAt(span, 0));
writer.append(span, tipClose);
// 创建标题
const paragraph = writer.createElement('paragraph');
writer.appendText('提示的标题', paragraph);
// 创建内容
const paragraph1 = writer.createElement('paragraph');
writer.appendText('1. 多喝水', paragraph1);
const paragraph2 = writer.createElement('paragraph');
writer.appendText('2. 多喝热水', paragraph2);
writer.insert(paragraph, tipTitle, 0);
writer.insert(paragraph1, tipContent, 0);
writer.insert(paragraph2, tipContent, 1);
writer.append(tipContent, tipContentBox);
writer.append(tipClose, tipContentBox);
writer.append(tipTitle, tipContentBox);
writer.append(tipContentBox, editItemBox);
return editItemBox;
}
3.5 注册tip插件
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
import tip from './tip/tip'
ClassicEditor
.create(document.querySelector('#editor'), {
plugins: [Essentials, Paragraph, Bold, tip, Italic, tip],
toolbar: ['bold', 'italic', 'tip']
})
.then(editor => {
// 注册删除事件
editor.editing.view.document.on('click', (evt, data) => {
if (data.domTarget.className == 'closeIcon' || data.domTarget.className == 'close' ) {
const selection = editor.model.document.selection;
editor.model.document.model.deleteContent(selection)
};
});
})
.catch(error => {
console.error(error.stack);
});
4、重置样式
在style标签里面加入下面代码就重置了
/* 选中及鼠标移入样式重置 */
.ck .ck-widget_selected{
background: rgba(72,145,255,.16);
}
.ck .ck-widget{
outline-style: none;
outline-color:transparent !important;
transition: none !important;
}
.ck .ck-widget:hover{
outline-style:dashed;
outline-color:#ccc !important;
outline-width:1.2px;
}
.ck .ck-widget_selected.ck-widget{
outline-style:dashed !important;
outline-color:#ccc !important;
outline-width:1.2px !important;
}
.tip-title, .tip-content, .tip-content-box{
outline:none !important;
}
/* 功能按钮区域移入变色 */
.ck.ck-button:not(.ck-disabled):hover, a.ck.ck-button:not(.ck-disabled):hover{
color: green;
}
/* 重置宽高 */
.ck-editor__editable {
min-height: 300px;
width: 800px !important;
}
.ck-toolbar, .ck-sticky-panel__content{
width: 821px !important;
}