在 Web 应用的评论区中集成富文本编辑器可以极大地提升用户体验。CKEditor 是一款成熟且可扩展的所见即所得编辑器,以下将以 id_content 为目标元素,介绍如何一步步集成并增强 CKEditor 功能。


🧩 基本集成

首先确保页面已正确引入 CKEditor 核心脚本,并准备好一个 <textarea id="id_content"> 作为绑定目标。

<script src="/static/ckeditor/ckeditor/ckeditor.js"></script>

然后用如下代码初始化编辑器:

if (document.getElementById('id_content')) {
  CKEDITOR.replace('id_content', {
    width: 'auto',
    height: '250px',
    skin: 'moono',
    resize_enabled: false,
    tabSpaces: 4,
    removePlugins: 'elementspath',

    // 工具栏配置
    toolbar: 'Custom',
    toolbar_Custom: [
      { name: 'document', items: ['Source', '-', 'Preview', '-', 'Templates'] },
      { name: 'clipboard', items: ['Undo', 'Redo'] },
      { name: 'editing', items: ['Find', 'Replace', '-', 'SelectAll'] },
      { name: 'forms', items: ['Form', 'TextField', 'Textarea', 'Select', 'Button'] },
      '/',
      { name: 'basicstyles', items: ['Bold', 'Italic', 'Underline', 'RemoveFormat'] },
      { name: 'paragraph', items: ['NumberedList', 'BulletedList', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight'] },
      { name: 'links', items: ['Link', 'Unlink'] },
      { name: 'insert', items: ['Image', 'Table', 'CodeSnippet', 'SpecialChar', 'Iframe'] },
      '/',
      { name: 'styles', items: ['Font', 'FontSize'] },
      { name: 'colors', items: ['TextColor', 'BGColor'] },
      { name: 'tools', items: ['Maximize'] },
      { name: 'customplugins', items: ['dotblock', 'echartsblock', 'selectblock'] }
    ],

    extraPlugins: 'codesnippet,prism,widget,lineutils,clipboard,dialog,mathjax,markdown,dotblock,echartsblock,uploadimage,selectblock',
    uploadUrl: '/admin/upload/ckeditor-image',
    filebrowserImageUploadUrl: '/admin/upload/ckeditor-image',

    extraAllowedContent: 'div.echarts-placeholder[*]{*}(*)',
    allowedContent: true,

    mathJaxLib: '//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'
  });
} else {
  console.error('CKEditor 4: Element with ID "id_content" not found.');
}

📊 自定义图表插件支持(ECharts)

集成自定义插件如 echartsblock

CKEDITOR.plugins.addExternal('echartsblock', '/static/ckeditor/plugins/echartsblock/', 'plugin.js');

在插件中定义图表 JSON 配置面板,并在插入占位符时使用 data-option 存储配置。

例如默认图表配置可写成:

{
  "title": { "text": "2024 年各月网站访问量统计" },
  "xAxis": { "type": "category", "data": ["1月", ..., "12月"] },
  "yAxis": { "type": "value" },
  "series": [{ "type": "line", "data": [820, ..., 1750], "smooth": true }]
}

🧠 添加快捷键支持

通过 keystrokes 可以添加键盘快捷方式,增强效率:

CKEDITOR.config.keystrokes = [
  [CKEDITOR.ALT + 121, 'toolbarFocus'],
  [CKEDITOR.ALT + 67, 'codeSnippet'],
  [CKEDITOR.ALT + 77, 'mathjax'],
  [CKEDITOR.ALT + 122, 'maximize'],
  [CKEDITOR.CTRL + CKEDITOR.SHIFT + 121, 'contextMenu']
];

🪄 工具栏自动隐藏与焦点交互

为了在评论区节省空间,可以设置工具栏在焦点时显示、失焦后隐藏:

document.addEventListener('DOMContentLoaded', function () {
  CKEDITOR.on('instanceReady', function (ev) {
    var editor = ev.editor;

    editor.ui.space('top').setStyle('display', 'none');
    editor.ui.space('contents').setStyle('height', '243px');

    editor.on('focus', function () {
      editor.ui.space('top').setStyle('display', '');
      editor.ui.space('contents').setStyle('height', '200px');
    });

    editor.on('blur', function () {
      editor.ui.space('top').setStyle('display', 'none');
      editor.ui.space('contents').setStyle('height', '243px');
    });
  });
});

🎨 提示

  • 如需支持自动高度,可考虑监听 changeinput 事件,在 iframe 或 editor.document.$.body.scrollHeight 基础上调整编辑区域高度。
  • 插件如 echartsblockdotblock 必须在 /static/ckeditor/plugins/ 目录下,并含 plugin.js 文件才可被识别。
  • 对上传图片,需确保 uploadUrl 对应后端上传处理接口已启用。

 

在 CKEditor 中集成 ECharts 插件教程

—— 实现图表插入与预览功能

1. 项目结构和文件介绍

假设你的项目目录结构大致如下:

D:\Nextcloud\go\blog\
  ├─ static\
  │    ├─ ckeditor\
  │    │    ├─ ckeditor\plugins\echartsblock\plugin.js
  │    │    ├─ ckeditor\plugins\echartsblock\dialogs\echartsblock.js
  │    │    └─ ckeditor\plugins\echartsblock\icons\echartsblock.png
  ├─ templates\
  │    └─ write_blog.html
  └─ adminpanel\
       └─ templates\write_blog.html
  • plugin.js:插件入口,负责注册插件和UI按钮。
  • dialogs/echartsblock.js:弹窗对话框定义,编辑图表配置 JSON。
  • write_blog.html:包含 CKEditor 初始化与插件调用的页面模板。
  • 另外,页面还包含 renderAllCharts 函数,实现 ECharts 渲染预览。

2. 插件核心代码解析

2.1 插件注册 (plugin.js)

CKEDITOR.plugins.add('echartsblock', {
  requires: 'dialog',
  icons: 'echartsblock',
  init: function(editor) {
    var pluginName = 'echartsblock';
    CKEDITOR.dialog.add(pluginName, this.path + 'dialogs/echartsblock.js');
    editor.addCommand(pluginName, new CKEDITOR.dialogCommand(pluginName));
    editor.ui.addButton('echartsblock', {
      label: '插入 ECharts 图表',
      command: pluginName,
      toolbar: 'insert',
      icon: this.path + 'icons/echartsblock.png'
    });
  }
});
  • 注册插件名称为 echartsblock
  • 依赖对话框组件 dialog
  • 在工具栏添加按钮,点击时弹出配置对话框。

2.2 插件弹窗定义 (dialogs/echartsblock.js)

CKEDITOR.dialog.add('echartsblock', function(editor) {
    // 简单柱状图默认配置示例,格式是 JSON 字符串
    var defaultExample = JSON.stringify({
      title: {
        text: '示例柱状图'
      },
      tooltip: {},
      xAxis: {
        data: ['苹果', '香蕉', '橘子', '梨子', '葡萄']
      },
      yAxis: {},
      series: [{
        name: '销量',
        type: 'bar',
        data: [5, 20, 36, 10, 10]
      }]
    }, null, 2); // pretty print 2 空格缩进
  
    return {
      title: '插入/编辑 ECharts 图表',
      minWidth: 400,
      minHeight: 200,
      contents: [{
        id: 'tab-basic',
        label: '基本',
        elements: [{
          type: 'textarea',
          id: 'option',
          label: 'ECharts 配置 (JSON 格式)',
          
          validate: CKEDITOR.dialog.validate.notEmpty("请填写配置"),
          setup: function(element) {
            this.setValue(element.getAttribute('data-option') || defaultExample);
          },
          commit: function(element) {
            element.setAttribute('data-option', this.getValue());
          }
        }]
      }],
      onShow: function() {
        var optionTextarea = this.getContentElement('tab-basic', 'option');
        optionTextarea.getInputElement().setStyle('height', '500px');
      
        var selection = editor.getSelection();
        var element = selection.getSelectedElement();
        if (element && element.hasClass('echarts-placeholder')) {
          this.setupContent(element);
          this._.selectedElement = element;
        } else {
          // 默认填充示例 JSON
          this.getContentElement('tab-basic', 'option').setValue(defaultExample);
          delete this._.selectedElement;
        }
      },
      onOk: function() {
        var dialog = this;
        var json = dialog.getValueOf('tab-basic', 'option');
        var encodedJson = CKEDITOR.tools.htmlEncode(json);
  
        var elementToModify = this._.selectedElement;
  
        if (elementToModify && elementToModify.hasClass('echarts-placeholder')) {
          this.commitContent(elementToModify);
          elementToModify.setText('ECharts 图表占位符 (点击编辑或预览): ' + encodedJson.substring(0, 100) + '...');
        } else {
          var html = '<div class="echarts-placeholder" contenteditable="false" ' +
                     'data-option=\'' + encodedJson + '\' ' +
                     'data-width="600px" data-height="400px" ' +
                     'style="width:600px;height:400px;' +
                           'border:1px dashed #999;' +
                           'background:#f9f9f9;' +
                           'display:flex; align-items:center; justify-content:center; text-align:center; font-size:14px; color:#888; cursor:pointer;">' +
                     'ECharts 图表占位符 (点击编辑或预览)<br>' + 
                     encodedJson.substring(0, 100) + '...' +
                     '</div>';
          editor.insertHtml(html);
        }
  
        if (typeof renderAllCharts === 'function') {
          setTimeout(function() {
            renderAllCharts(editor);
          }, 50);
        }
      }
    };
  });
  
  • 弹窗内包含一个 textarea 用于输入 ECharts JSON 配置。
  • 支持编辑已插入图表占位符,或新插入一个带 data-option 属性的占位符 div
  • 插入后调用 renderAllCharts 函数渲染图表预览。

 

2.3 ckeditor插件配置(id_content)


<script>
    if (document.getElementById('id_content')) {
    CKEDITOR.replace('id_content', {

        toolbar_Custom: [ 
            
            { name: 'customplugins', items: [ 'echartsblock'] } 
        ],
        // 额外插件
        extraPlugins: 'echartsblock', 
       
    });
} else {
}
    </script>

 


3. 编辑器中图表的插入与编辑流程

  1. 点击工具栏的“插入 ECharts 图表”按钮。
  2. 弹出 JSON 配置编辑框,输入符合 ECharts 配置规范的 JSON 字符串。
  3. 确认后,编辑器插入一个不可编辑的 div.echarts-placeholder,携带配置数据。
  4. 触发 renderAllCharts(editor),将配置渲染为静态图片形式插入,防止编辑时占用大量资源。
  5. 点击占位符可再次编辑配置。

    

注意事项:ECharts 图表编辑json技巧

  • 当光标位于 ECharts 占位符的下方(紧贴图表占位符)时,按删除键可以逐步清空该位置的文本。

  • 如果删除到光标“消失”或文档尾部无内容时,请不要继续按删除键。

  • 继续删除会导致图表占位符被误删,从而使后续点击 echartsblock 插件按钮时,编辑器在错误位置插入重复或异常的图表代码。

  • 建议保持编辑器中至少存在一个空段落,以避免插入位置异常和图表丢失。

ckeditor 中渲染,可用再次编辑


<script>
    function renderAllCharts(editor) {
      // 确保编辑器处于可视化模式且已就绪
      if (!editor || !editor.editable || editor.mode !== 'wysiwyg') {
          console.log('ECharts 渲染跳过:编辑器未就绪或不在可视化模式。');
          return;
      }
    
      const editable = editor.editable();
    
      // 遍历所有 ECharts 占位符
      editable.find('.echarts-placeholder').toArray().forEach(function (widget) {
        const el = widget.$; // 获取原始的 .echarts-placeholder div 的 DOM 元素
    
        // 避免重复渲染:如果这个占位符已经包含了一个 ECharts 预览图片,就跳过
        if (el.querySelector('img[alt^="ECharts 图表预览"]')) {
            // console.log('ECharts 渲染跳过:已存在预览图片。');
            return;
        }
    
        // 清除占位符内的任何旧内容(如文本提示或旧图片),准备渲染新内容
        el.innerHTML = ''; 
    
        // 获取 ECharts 配置 JSON
        const rawOption = el.getAttribute('data-option');
        let option;
        try {
          option = JSON.parse(rawOption);
        } catch (e) {
          console.warn('ECharts 渲染失败:无效的 ECharts 配置 JSON。', e, '原始配置:', rawOption);
          el.innerHTML = '<div style="width:100%;height:100%; border:1px dashed #f00; background:#fff0f0; display:flex; align-items:center; justify-content:center; font-size:14px; color:#f00;">ECharts 配置错误!</div>';
          return;
        }
    
        try {
          // 创建一个临时 div 用于离屏渲染 ECharts
          const tempDiv = document.createElement('div');
          // 确保 tempDiv 继承原占位符的尺寸,如果 style 属性未设置,则使用默认值
          tempDiv.style.width = el.style.width || el.getAttribute('data-width') || '600px';
          tempDiv.style.height = el.style.height || el.getAttribute('data-height') || '400px';
          // 将 tempDiv 放置到屏幕外,不影响页面布局
          tempDiv.style.position = 'absolute';
          tempDiv.style.left = '-9999px';
          tempDiv.style.top = '-9999px';
          document.body.appendChild(tempDiv); // 将临时 div 添加到 body
    
          const chart = echarts.init(tempDiv); // 在临时 div 上初始化 ECharts 实例
    
          // 监听 'finished' 事件,该事件在图表渲染完成后触发,是捕获图片的最佳时机
          chart.on('finished', function() {
              const canvas = tempDiv.querySelector('canvas');
              if (canvas) {
                const img = document.createElement('img');
                img.src = canvas.toDataURL('image/png'); // 将 canvas 内容转换为图片 URL
                img.style.maxWidth = '100%'; // 确保图片不会超出容器
                img.style.height = 'auto'; // 保持图片宽高比
                img.style.pointerEvents = 'none'; // 防止图片干扰编辑器交互
                img.alt = 'ECharts 图表预览';
                
                // 将生成的图片插入到原始的 .echarts-placeholder div 中
                el.innerHTML = ''; // 再次清空,确保只显示图片
                el.appendChild(img);
              }
              chart.dispose(); // 销毁 ECharts 实例,释放资源
              document.body.removeChild(tempDiv); // 移除临时 div
              // console.log('ECharts 渲染完成并已转换为图片。');
          });
    
          // 设置 ECharts 配置并强制重绘
          chart.setOption(option, true); // true 表示不合并配置,确保完全重新渲染
          chart.resize(); // 强制 ECharts 重新计算和绘制尺寸
    
          // 添加一个回退计时器,以防 'finished' 事件因某种原因未触发(例如,数据为空的图表)
          setTimeout(() => {
              if (!chart.isDisposed()) { // 检查 ECharts 实例是否已经被处理过
                  console.warn("ECharts 'finished' 事件未触发,执行超时回退捕获。");
                  const canvas = tempDiv.querySelector('canvas');
                  if (canvas) {
                      const img = document.createElement('img');
                      img.src = canvas.toDataURL('image/png');
                      img.style.maxWidth = '100%';
                      img.style.height = 'auto';
                      img.style.pointerEvents = 'none';
                      img.alt = 'ECharts 图表预览 (回退)';
                      el.innerHTML = '';
                      el.appendChild(img);
                  }
                  chart.dispose();
                  document.body.removeChild(tempDiv);
              }
          }, 2000); // 延长回退超时时间
          
        } catch (e) {
          console.error('ECharts 渲染过程中发生错误:', e);
          el.innerHTML = '<div style="width:100%;height:100%; border:1px dashed #f00; background:#fff0f0; display:flex; align-items:center; justify-content:center; font-size:14px; color:#f00;">ECharts 渲染过程中发生错误!<br>详情请查看控制台。</div>';
        }
      });
    }
    </script>

 


4. ECharts 图表渲染机制说明

4.1 renderAllCharts 函数

  • 遍历所有 .echarts-placeholder 元素。
  • 读取 data-option JSON,初始化离屏临时 div
  • 使用 echarts.init 创建图表实例,监听 finished 事件后将 canvas 转为图片。
  • 替换原占位符内容为生成的图片,减少编辑器渲染压力。
  • 设置回退定时器防止事件未触发导致无法显示。

4.2 页面首次加载时自动渲染(detail.html)

<script>
    document.addEventListener('DOMContentLoaded', function() {
      // 查找所有带有 data-option 属性的 div,无论是 placeholder 还是已渲染的 div
      document
        .querySelectorAll('div[data-option]') // <-- 修改此处选择器
        .forEach(function(el) {
          try {
            // 避免重复渲染已经激活的 ECharts 实例
            if (el._echarts_instance_) {
                return;
            }

            var option = JSON.parse(el.getAttribute('data-option'));
            var chartDom = document.createElement('div');
            // 从 data-属性获取尺寸
            chartDom.style.width  = el.getAttribute('data-width') || '600px';
            chartDom.style.height = el.getAttribute('data-height') || '400px';
            
            // 复制 data-* 属性到新的容器 (可选,但有助于调试和一致性)
            chartDom.setAttribute('data-option', el.getAttribute('data-option'));
            if (el.hasAttribute('data-width')) chartDom.setAttribute('data-width', el.getAttribute('data-width'));
            if (el.hasAttribute('data-height')) chartDom.setAttribute('data-height', el.getAttribute('data-height'));

            // 替换占位节点或已渲染但未激活的节点
            el.parentNode.replaceChild(chartDom, el);
            
            echarts.init(chartDom).setOption(option);
          } catch (e) {
            console.error('ECharts 渲染失败:', e);
          }
        });
    });
</script>
  
  • 页面中所有带 data-option 属性的元素将直接被渲染成 ECharts 图表。

5. 使用示例与效果展示

5.1 插入示例

编辑器中点击按钮,输入如下 JSON 配置:

{
  "title": { "text": "示例折线图" },
  "tooltip": {},
  "xAxis": {
    "data": ["衬衫","羊毛衫","雪纺衫","裤子","高跟鞋","袜子"]
  },
  "yAxis": {},
  "series": [{
    "name": "销量",
    "type": "line",
    "data": [5, 20, 36, 10, 10, 20]
  }]
}

确认后会插入带有图表预览的占位符,显示图表快照。

5.2 编辑示例

点击图表占位符,会弹出对话框,显示 JSON 配置,允许修改。

5.3 效果截图

插入时显示占位符,渲染完成后显示图表图片,编辑时弹出 JSON 配置框。


6. 结语与扩展建议

  • 本方案将 ECharts 配置以 JSON 字符串形式存储于元素属性,简洁直观。
  • 通过将图表渲染为图片预览,提升编辑器性能和稳定性。
  • 可扩展支持图表交互和动态更新。
  • 推荐对输入 JSON 进行格式校验和美化,提升用户体验。
  • 适合用于技术内容编辑、后台数据展示、报告生成等多种应用场景。