ckeditor5 自定义构建+上传图片+wps 复制图文粘贴到编辑器空白丢失+去掉右下角logo+当前输入字数展示 解决记录

首先 在官网 CKEditor 5 Online Builder | Create your own editor in 5 steps      定制你想要的编辑器 包括编辑器的工具栏样式、工具栏的功能插件手动添加;最后选择语言 chiness,下载压缩包到本地得到一个定制的编辑器 如下:

解压缩后初始化插件 

npm install

支持 wps 复制图文粘贴到编辑器:

ckeditor5-block-new\node_modules\@ckeditor\ckeditor5-paste-from-office\src\filters

找到 image.js 中 extractImageDataFromRtf 方法 修改如下:添加 if(!images){}

function extractImageDataFromRtf( rtfData ) {
    if ( !rtfData ) {
        return [];
    }

    const regexPictureHeader = /{\\pict[\s\S]+?\\bliptag-?\d+(\\blipupi-?\d+)?({\\\*\\blipuid\s?[\da-fA-F]+)?[\s}]*?/;
    const regexPicture = new RegExp( '(?:(' + regexPictureHeader.source + '))([\\da-fA-F\\s]+)\\}', 'g' );
    const images = rtfData.match( regexPicture );
    const result = [];

    // 针对 wps 添加的判断
    if (!images) {
        regexPictureHeader = /{\\pict[\s\S]+?(\\pngblip-?\d+)?(\\wmetafile8-?\d+)?{\\\*\\blipuid\s?[\da-fA-F]+[\s}]*?/;
        regexPicture = new RegExp( '(?:(' + regexPictureHeader.source + '))([\\da-fA-F\\s]+)\\}', 'g' );
        images = rtfData.match( regexPicture );
    }

    if ( images ) {
        for ( const image of images ) {
            let imageType = false;

            if ( image.includes( '\\pngblip' ) ) {
                imageType = 'image/png';
            } else if ( image.includes( '\\jpegblip' ) ) {
                imageType = 'image/jpeg';
            }

            if ( imageType ) {
                result.push( {
                    hex: image.replace( regexPictureHeader, '' ).replace( /[^\da-fA-F]/g, '' ),
                    type: imageType
                } );
            }
         }
    }

    return result;
}

space.js修改:

export function normalizeSpacing( htmlString ) {
    // Run normalizeSafariSpaceSpans() two times to cover nested spans.
    return normalizeSafariSpaceSpans( normalizeSafariSpaceSpans( htmlString ) )
        // Remove all \r\n from "spacerun spans" so the last replace line doesn't strip all whitespaces.
        .replace( /(<span\s+style=['"]mso-spacerun:yes['"]>[^\S\r\n]*?)[\r\n]+([^\S\r\n]*<\/span>)/g, '$1$2' )
        .replace( /<span\s+style=['"]mso-spacerun:yes['"]><\/span>/g, '' )
        .replace( / <\//g, '\u00A0</' )
        .replace( / <o:p><\/o:p>/g, '\u00A0<o:p></o:p>' )
        // Remove <o:p> block filler from empty paragraph. Safari uses \u00A0 instead of &nbsp;.
        .replace( /<o:p>(&nbsp;|\u00A0)<\/o:p>/g, '' )
        // Remove all whitespaces when they contain any \r or \n.
        .replace( />([^\S\r\n]*[\r\n]\s*)</g, '><' )
        // 针对WPS的修改,去除空格
        .replace( />(\s+)</g, '><' );
}
export function normalizeSpacerunSpans( htmlDocument ) {
    htmlDocument.querySelectorAll( 'span[style*=spacerun]' ).forEach( el => {
        // 针对 wps 添加的判断
        if ( el.childNodes[ 0 ] && el.childNodes[ 0 ].data ) {
            const innerTextLength = el.innerText.length || 0;
            el.innerHTML = Array( innerTextLength + 1 ).join( '\u00A0 ' ).substr( 0, innerTextLength );
        }
    });
}

在这修改完依赖包中的代码后!本人的做法是:打开webpack.config.js 修改

执行命令 npm run build  打包后的build文件 剪切到 ckeditor5-block-new\node_modules\@ckeditor\ckeditor5-paste-from-office\build

 再把webpack文件 恢复原来的代码 再进行打包命令 npm run build,成功后 就是修改后支持粘贴wps 文字+图片的编辑器依赖包了。

正常来说 直接新建一个plugins 文件夹 放入依赖包,然后根据文件地址引用就可以了,后面再说如何把修改代码后的依赖包上传到npm仓库 可以通过npm 直接下载,应用到项目中;

下面先说 图片上传功能:

ckeditor5 的图片上传功能 需要自定义上传适配器,以下是我的功能代码 MyUploadAdapter.js

import {
	option, $base_share
} from '../../static/config' //看项目的上传接口是否需要
import { uuid } from "../../utils/commonTool";//看项目的上传接口是否需要

export default class MyUploadAdapter {
  constructor( loader ) {
    this.loader = loader;
  }

  // Starts the upload process.
  upload() {
    return new Promise( ( resolve, reject ) => {
      this.loader.file.then( file => {
        // Instantiate a FormData object to send the file.
        const data = new FormData();
        // 上传的参数
        data.append('file', file);
        data.append("dataId", uuid());
        data.append("dataTable", 'sys_base_filepath');
        data.append('AccessTimestamp', new Date().getTime())
        // 获取 用户的token,按需修改
        const vt = window.localStorage.getItem($base_share.ACCESS_TOKEN);
        // 使用 fetch 或 XMLHttpRequest 发送文件
        fetch( '/apixxx/system/upload', {
          method: 'POST',
          body: data,
          headers:{
            // "Content-Type": "multipart/form-data" ,
            [$base_share.ACCESS_TOKEN]:vt,
          },
        })
          .then( response => {
            if (response.ok) {
              console.log('ok')
              // 如果响应是 JSON 格式
              return response.json();
            } else {
              // 如果响应状态码不是 2xx,抛出错误
              throw new Error('Network response was not ok.');
            }
            // Handle server response here.
            // For example, you can return a URL to the file.
          } )
          .then(data => {
            // 在这里处理解析后的数据
            var resData = {}
            resData.default = data.result.filePath //获取图片地址 
            // resData.default ='/api'+ data.result.filePath+"?filepath="+ data.result.bkUrl+"&token="+token   //或者接口返回的不是http 而是服务器的文件路径,需要自己去拼接
            //注意这里的resData一定要是一个对象,而且外面一定要有一个default设置为图片上传后的url,这是ckeditor的规定格式
            resolve(resData);
          })
          .catch( error => {
            console.log(error,'?er')
            reject( error );
          } );
      } );
    } );
  }
}

在vue页面中的使用

<template>
  <div class="content">
    <div style="width: 95%;margin: auto;">
      <div id="ckeditor-classic" @ready="onEditorReady"
        style="width: 100%;min-height: 500px;padding: 20px 30px;box-sizing: border-box;">
      </div>
    </div>
  </div>
</template>

<script>

import Editor from 'zdy-ckeditor5-build-classic'; //我的自定义依赖包
import 'zdy-ckeditor5-build-classic/build/translations/zh' //中文包
import MyUploadAdapter from '../../../assets/js/MyUploadAdapter' //上传图片的js
export default {
  props: {
    value: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      editor: null, //editor 对象
      editorData: '', //内容
      title: '',
    };
  },
  watch: {
    value: {
      handler(newValue) {
        if (this.editor) {
          // 监听修改回显内容
          this.editor.setData(newValue);
          this.editorData = newValue
        }
      }
    }
  },
  mounted() {
    this.initEditor();
  },
  beforeDestroy() {
    if (this.editor) {
      this.editor.destroy();
      this.editor = null;
    }
  },
  methods: {
    initEditor() {
      Editor
        .create(document.querySelector('#ckeditor-classic'), {
          language: 'zh-cn',
          extraPlugins: [MyUploadAdapter],   // 添加上传适配器
          mediaEmbed: {
            previews: true,
            providers: [
              // 配置支持的媒体提供商
              { name: 'YouTube', url: /https?:\/\/(www\.)?youtube\.com\// },
              { name: 'Vimeo', url: /https?:\/\/(www\.)?vimeo\.com\// },
            ],
          },
        })
        .then(editor => {
          this.editor = editor;
          // editor.setData(this.value);
          //监听编辑器内容的变化 发送给父组件;
          editor.model.document.on('change:data', () => {
            this.editorData = editor.getData();
            this.$emit('input', this.editorData); // 发射 input 事件来更新 v-model
          });
          editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
            // You need to return an instance of UploadAdapter.
            return new MyUploadAdapter(loader);
          };

        })
        .catch(error => {
          console.log(error)
          console.error(error.stack);
        });
    },
    // editor
    onEditorReady() {
      // 确保 'editor' 是一个有效的实例
      // editor.ui.viewport.on('paste', (event, data) => {
      //   // 你的逻辑...
      //   console.log(data.item,data.item.type,'/====')
      // });
    }
  }
};
</script>
<style lang="scss" scoped>
.content {
  width: 100%;
  background: #fff;
}

.input {
  width: 100%;
  height: 40px;
  border: none;
  font-size: 20px;
}

.input:focus-visible {
  outline: none;
}

.ck.ck-editor__editable:not(.ck-editor__nested-editable) {
  border-radius: 0;
}

#ckeditor-classic {
  height: 300px;
}

/deep/ .ck-blurred.ck.ck-content.ck-editor__editable.ck-rounded-corners.ck-editor__editable_inline {
  min-height: 300px !important;
}

/deep/ .ckeditor__editable {
  min-height: 300px !important;
}

/deep/.ck-rounded-corners .ck.ck-editor__main>.ck-ediotr__editable {
  min-height: 300px !important;
}

/deep/.ck.ck-editor__main>.ck-editor__editable.ck-rounded-corners {
  min-height: 300px !important;
}
</style>

以上是目前我涉及到的插件问题解决;下面是把修改代码后的ckeditor5 上传到 npm仓库中去

首先 要在github 创建一个自己的远程仓库,把本地的代码 git init 初始化,以及关联到git 远程仓库去,具体 git 命令:

当你已经创建了一个Git的远程仓库,并且想要上传你的代码到这个仓库中,你可以按照以下步骤操作:

  1. 初始化本地仓库: 如果你的本地项目目录还没有被Git跟踪,你需要首先在项目目录中运行 git init 来初始化一个新的Git仓库。

  2. 添加远程仓库: 将远程仓库的URL添加到你的本地仓库中。你需要使用 git remote add 命令,例如:

     

    git remote add origin <远程仓库的URL>

    这里的 <远程仓库的URL> 应该替换成你的远程仓库的实际URL。

  3. 添加文件到本地仓库: 使用 git add 命令将你的文件添加到本地仓库的暂存区。你可以添加单个文件或整个目录:

     

    git add <文件名> # 或者 git add .

  4. 提交更改: 使用 git commit 命令将暂存区的更改提交到本地仓库:

     

    git commit -m "你的提交信息"

    提交信息应该简短描述你所做的更改。

  5. 推送到远程仓库: 使用 git push 命令将你的提交推送到远程仓库。如果你是第一次推送到远程仓库,可能需要进行身份验证:

     

    git push -u origin <分支名>

    这里的 <分支名> 通常是 mastermain,取决于你的远程仓库的默认分支名称。

打开git bash here 

执行命令:

npm init  初始化创建你的npm 仓库,其中name : 你的插件名称(npm install 时的插件名字);需要复制你的git远程仓库地址;

 package.json 中 删掉 private 字段****否则不被发布到npm仓库;每次更新包后,都需要手动修改version 版本号,否则上传包 失败

npm init 命令用于初始化一个新的Node.js项目,并创建一个package.json文件,该文件包含了项目的元数据和依赖信息。当你运行 npm init 时,npm会提示你输入一些信息来填充package.json文件。下面是一些常见的字段及其意义:

  1. name: 项目的名称,通常遵循/的格式,例如@my-scope/my-package。如果你没有注册npm scope,可以只填写项目名称。

  2. version: 项目的当前版本,遵循语义版本控制(Semantic Versioning),格式为主版本号.次版本号.修订号,例如1.0.0

  3. description: 项目的简短描述。

  4. entry point(或main): 项目的入口文件,通常是你的模块的默认导出文件,例如index.js

  5. repository: 项目的代码仓库URL,可以是Git仓库。

  6. test command: 运行测试的命令,例如"test": "echo \"Error: no test specified\" && exit 1"

  7. git repository: Git仓库的URL,用于关联npm包和代码仓库。

  8. keywords: 与项目相关的关键词,有助于npm搜索和用户发现。

  9. author: 项目作者的姓名。

  10. license: 项目的许可证,决定了其他人如何使用、修改和分发你的代码。常见的许可证包括MIT、ISC、Apache 2.0等。

  11. files: 指定要包含在npm包中的文件列表。

  12. bin: 指定命令行工具的入口点,允许你将JavaScript文件作为命令行工具发布。

  13. scripts: 定义可以运行的脚本命令,例如"start": "node index.js"

  14. dependencies: 项目运行时依赖的包列表。

  15. devDependencies: 仅在开发时需要的包列表,例如测试框架。

  16. peerDependencies: 定义对项目运行时依赖的包的版本要求,通常用于插件或库。

  17. engines: 指定项目运行所需的Node.js版本范围。

  18. os / cpu: 指定项目运行所需的操作系统或CPU架构。

  19. private: 如果设置为true,表示这个包不应该被发布到npm公共仓库。*************(重要)

当你运行 npm init 时,你可以选择回答这些问题,或者简单地按回车键接受默认值。如果你选择跳过某些字段,可以在任何时候手动编辑生成的package.json文件。此外,你还可以使用 -y--yes 标志来自动接受所有默认值并创建package.json文件。

 init后 执行npm login 登录你的npm 账号;登录成功后 npm publish 推送你的插件包到仓库;成功后就可以在项目中 通过npm install xxxx 下载并使用啦;如果项目是vue3 的 可以直接通过pnpm 下载 按照插件;

注:下载自定义插件后,就不需要再执行npm install @ckeditor/ckeditor5-build-classic 等等了;

后续如果再添加功能,后续再更新;欢迎一起学习探讨 ckeditor5 的使用

后续问题:段落缩进无法使用以及标签样式丢失问题解决: 

段落缩进按钮功能一直置灰禁用的状态,无法点击,需要在ckeditor.js 中添加IndentBlock

重新npm run build 打包插件后生效


粘贴到编辑器的内容 html标签内联样式丢失问题解决:

添加GeneralHtmlSupport 插件在ckeditor.js中

import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
import { GeneralHtmlSupport } from '@ckeditor/ckeditor5-html-support';
import { Alignment } from '@ckeditor/ckeditor5-alignment';
import { Autoformat } from '@ckeditor/ckeditor5-autoformat';
import { Bold, Code, Italic, Underline } from '@ckeditor/ckeditor5-basic-styles';
import { BlockQuote } from '@ckeditor/ckeditor5-block-quote';
import { CKBox, CKBoxImageEdit } from '@ckeditor/ckeditor5-ckbox';
import { CloudServices } from '@ckeditor/ckeditor5-cloud-services';
import { CodeBlock } from '@ckeditor/ckeditor5-code-block';
import type { EditorConfig } from '@ckeditor/ckeditor5-core';
import { Essentials } from '@ckeditor/ckeditor5-essentials';
import { FontBackgroundColor, FontColor, FontFamily, FontSize } from '@ckeditor/ckeditor5-font';
import { Heading } from '@ckeditor/ckeditor5-heading';
import { Highlight } from '@ckeditor/ckeditor5-highlight';

重新npm run build 打包插件;注意在package.json 添加GeneralHtmlSupport

 在vue页面 初始化editor实例中

initEditor() {
      Editor
        .create(document.querySelector('#ckeditor-classic'), {
          language: 'zh-cn',
          extraPlugins: [MyUploadAdapter],   // 添加上传图片适配器
          fontSize: {
            options: ['12px', '14px', '16px', '18px', '20px','22px','24px','28px','32px']
          },
          //允许粘贴携带样式
          htmlSupport: {
            allow: [//哪些功能被支持
                {
                    name: /.*/,//需要启用的元素名称
                    attributes: true,
                    classes: true,//允许所有标签的样式
                    styles: true,//允许的内联样式
                }
            ],
            disallow:[//不支持哪些功能
                {name:'script'},//不允许创建script元素
                {
                    name:/.*/,//不允许所有元素执行点击事件
                    attributes:['onclick'],
                }
            ]
          },
          //字数回调
          wordCount: {
            onUpdate: stats => {
              this.characters = stats.characters
            // Prints the current content statistics.
            // console.log( `Characters: ${ stats.characters }\nWords: ${ stats.words }` );
            }
          },
          mediaEmbed: {
            previews: true,
            providers: [
              // 配置支持的媒体提供商
              { name: 'YouTube', url: /https?:\/\/(www\.)?youtube\.com\// },
              { name: 'Vimeo', url: /https?:\/\/(www\.)?vimeo\.com\// },
            ],
          },
        })
        .then(editor => {
          this.editor = editor;
          editor.model.document.on('change:data', () => {
            this.editorData = editor.getData();
            this.$emit('input', this.editorData); // 发射 input 事件来更新 v-model
          });
          editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
            // You need to return an instance of UploadAdapter.
            return new MyUploadAdapter(loader);
          };
          

        })
        .catch(error => {
          console.log(error)
          console.error(error.stack);
        });
    },

隐藏右下角的logo

<style>
 .ck.ck-balloon-panel.ck-balloon-panel_visible {
      display: none !important;
  }
</style>

 设置展示当前输入的字数:

<template>
  <div class="content">
    <div id="ckeditor-classic" @ready="onEditorReady"
        style="width: 100%;min-height: 500px;padding: 20px 10px;box-sizing: border-box;">
    </div>

//这是字数的容器和数据
    <div class="ck ck-word-count">
      <!-- 行数 -->
      <!-- <div class="ck-word-count__words">Words: %%</div>  -->
      <div class="ck-word-count__characters">字数:{{ characters }} </div>
    </div>
  </div>
</template>

//设置样式

<style>
 .ck.ck-balloon-panel.ck-balloon-panel_visible {
      display: none !important;
  }
.ck-word-count{
  justify-content: flex-end;
  display: flex;
  background:#fff;
  padding:calc(0.6em*0.5) 0.6em;
  border: 1px solid #ccced1;
  border-top-width: 0;
  border-radius:0 0 2px;
  font: normal normal normal 13px / 1.84615 / Helvetica,Arial,Tahoma,Verdana,Sans-Serif
}
</style>

效果图:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值