当前位置: 首页 > news >正文

工作记录:举步维艰的在线 word 之旅 - tinymce

项目中需要实现 “在线编辑 word 模板” 的功能,我打算使用富文本组件 tinymce ,因为业务需求比较特殊,研究一下 tinymce 是否能实现。

如何在 vue 项目中引用 tinymce,可以看另一篇文章 《在 vue 项目中使用 tinymce》

(最后这个功能没有用 tinymce 实现,换了别的富文本库。但还是把这次的研究过程记录下来,给自己做一个总结回顾)


业务描述

使用场景

假设我需要写一个 word 文件:《2023年销售部工作汇报》

在这里插入图片描述

写完之后,我还要给开发部、采购部写同样的报告,格式相同,只有金额数值需要根据各部门实际情况填写。写完了今年的报告,我还要补上之前年份的报告。

这些都是重复的工作,我想省点力气。做一个模板,让程序给我自动生成多份 word 文件。

简单观察就能发现,这篇报告中只有两个变量:年度部门。收入支出的值都可以按照设定好的规则,根据这两个变量的值自动计算查询:

在这里插入图片描述
我使用这个模板时,只需要指明 年度部门 的值,程序就会自动把所有值替换好,生成 word 文件。年度和部门是直接替换文字,金额是根据年度和部门的值自动查询数据库后再替换。

上面是简单的描述,真实项目要复杂得多,这里不详细说明了。

解释

  • 年度、部门是 “全局参数”,导出 word 时用户可以设置这个值或者使用默认值(比如说年度默认取当前年)。
  • 总收入、总支出、净收入是“数据标签”,每个标签都需要配置好规则,这样导出 word 时就可以根据全局参数的值实时查询数据

数据标签的规则设置非常灵活,可配置项很多,大部分的业务场景都可以覆盖。这里不细说了,举个例子简单理解一下就够了:(很久不写sql了,不确定语法对不对,明白意思就行)

# [总收入]的规则:
SELECT SUM(income) FROM table_money WHERE year = 年度 and dept = 部门;

除了简单的数据标签,还可以插入多种类型的图表。这些内容也需要配置数据规则、设置想要的样式。导出 word 时查询完数据,会自动渲染:

在这里插入图片描述


用 tinymce 怎么实现

每个数据标签是一个整体

全局参数、数据标签等业务组件,它们其实都是占位符。在编辑模板的时候,这些占位符的名字是由配置决定的(会根据你的配置信息自动给标签起名字),不应该让用户在富文本中直接修改文字。而且每个标签应该是一个整体。

如下图:光标在数据标签内部了,这是不可以的,应该禁止光标点进去:

在这里插入图片描述

光标在净收入标签的后面。这时候用户点击 Backspace 键,应该把 [净收入] 标签整个删除:

在这里插入图片描述
这种效果其实在 CodeMirror 中可以实现,也很好实现,因为 CodeMirror 提供了支持。但 tinymce 中并没提供支持(其实大部分富文本组件都不支持,这对于富文本属于进阶功能了,普通使用者用不到)。我也试了很久,在 tinymce 中想要实现很麻烦,

灵机一动,我可以用 button 标签啊!最终全局参数、数据标签决定用:

<input type="button" value="[年度]">

PROBLEM SOLVED!

(后来在网上看到有人说,Web component 配合 tinymce custom_elements 属性也可以实现类似功能。就不去试了,这里提一下。而且Web component 也有兼容性问题,现在项目还没放弃兼容 IE,用不了)


编辑器中怎么显示图表

项目中的图表很多是用 echarts 实现的,比如柱状、饼图。但 echarts 图是不能放在 tinymce 编辑框中直接用的。所以我使用图片当占位符。
在屏幕外获取数据、渲染图表。渲染好之后,把图表转为图片,再更新占位符的 src。
因为最终导出到 word 后,用户看到的也是一张静态的图片(word里也放不了活的 echarts 图表哇)

给数据标签添加事件

编辑模板时,点击数据标签,要弹出配置弹窗。所以要给数据标签添加点击事件。

tinymce 所有的 set 方法只接受 string 类型的参数。所以不能把 element 直接传给 insertContent,只能传 element.outerHTML。这样绑定的事件就无效了。

onMounted(() => {tinymce.init({selector: "#target",toolbar: "addDataTag",setup: (editor) => {editor.ui.registry.addButton("addDataTag", {text: "插入数据标签",onAction: () => {const button = editor.getDoc().createElement("input"); button.setAttribute("id", "dataTag1");button.setAttribute("type", "button");button.setAttribute("value", "数据标签1");          button.addEventListener("click", function () { console.log("click");}); // 没用// insertContent 方法只接受 string 类型的参数,所以这里只能传 button.outerHTML。绑定的事件函数丢失了。editor.insertContent(button.outerHTML);	},});},});
});

要想加点击事件,只能先用 tinymce set。等页面中已经有这个元素后,再去找到它并绑定事件:

        onAction: () => {...// 绑不上事件!button.addEventListener("click", function () { console.log("before");	});editor.insertContent(button.outerHTML);const res = editor.getDoc().querySelector("#dataTag1");// 成功绑定事件!res.addEventListener("click", function () { console.log("after"); });	},

但是这样绑定的事件是临时的:每次页面回显数据时,都要给数据标签重新绑定一遍事件。

onMounted(() => {tinymce.init({...});setTimeout(() => {// 从服务器获取数据并回显tinymce.activeEditor.setContent(`<input id="dataTag1" type="button" value="数据标签1">`);// 绑定事件const widgets = tinymce.activeEditor.getDoc().querySelector("input[type='button']");widgets.addEventListener("click", function () { console.log("回显时绑定事件"); });}, 500);
});

回显后给已有的标签绑定事件,新增时给当前新增的这个绑定事件。很麻烦,不如 每次初始化时,给 editor.getDoc().body 绑定事件,根据 e.srcElement 判断触发元素

(这里只是根据麻烦程度来考虑。但其实给元素绑定事件是根本不可行的,只能给 body 绑,原因在下面会说明)

关联数据

模板内容是一段 html 代码。业务组件的配置项是 object 对象。

要把 html 中代表业务组件的 Element 和配置项关联起来,就需要在 Element 上绑定属性:

<input type="button" value="[年度]" biz-type="globalParams" biz-id="1">

自定义属性和事件被 cleanup 了

但是,我发现 tinymce 在每次获取、设置数据时,都会先执行 cleanup: 清除元素上的自定义属性、事件处理函数等。也就是说,上面的按钮在添加到文档中时, biz-typebiz-id 这两个自定义属性就已经丢失了。

显式调用 tinymce getContent setContent insertContent 方法时会被 cleanup。其他隐式触发 tinymce set 的操作也会悄悄执行 cleanup,防不胜防。举例:

我插入了一个数据标签,点击事件有,一切正常。光标的位置和数据标签在同一行,点击工具栏:插入水平线

在这里插入图片描述

变成:

在这里插入图片描述
再点击”数据标签1“,没有反应,说明绑定的事件已经丢了。我认为原因是:插入水平线功能的内部实现是先 get 了当前行,进行数据处理、计算,再把算好的结果 set 进去,取代以前的内容。所以这一行的内容都被替换了,只是看上去一样罢了。

我也验证了这个想法:用浏览器的调试工具,把之前的 element 存下来了。和之后的 element 进行比较,果然是不相等的:

在这里插入图片描述

前后已经不是同一个元素了:

在这里插入图片描述
这也说明,在上一部分 “给数据标签添加事件” 中,给具体元素绑定事件是不可行的,很容易就会因为 cleanup 而丢失。只能采用给整个 editor.body 绑定事件的做法。

丢失事件的问题用 “给 body 整体绑事件” 就解决了,下面该说丢失自定义属性的问题了。

我原本想看 tinymce 的源码,看看它 cleanup 时用的什么规则,但找了挺久没找到。我决定换个思路,想到 tinymce 自动清理应该是为了规范数据,所以它不认 ”乱七八糟“ 的自定义属性,那正规的属性他就应该认了吧。我试了试 HTML5 提供的自定义属性语法 :

<input type="button" value="[年度]" data-biz-type="globalParams" data-biz-id="1">

成功,tinymce 接受这两个属性了!

相关文章:

工作记录:举步维艰的在线 word 之旅 - tinymce

项目中需要实现 “在线编辑 word 模板” 的功能&#xff0c;我打算使用富文本组件 tinymce &#xff0c;因为业务需求比较特殊&#xff0c;研究一下 tinymce 是否能实现。 如何在 vue 项目中引用 tinymce&#xff0c;可以看另一篇文章 《在 vue 项目中使用 tinymce》 &#x…...

动态规划编译距离

583. 两个字符串的删除操作方法&#xff1a;dp状态表示&#xff1a;以i-1和j-1为结尾的字符串world1和world2&#xff0c;抵达相同的字符串所需的最少操作数属性&#xff1a;最小值状态计算&#xff1a;world1[i-1]和world2[j-1]相同dp[i][j] dp[i-1][j-1];world1[i-1]和world…...

Netty 教程 – 解码器详解

TCP以流的方式进行数据传输&#xff0c;上层的应用为了对消息进行区分&#xff0c;往往采用如下方式 固定消息长度&#xff0c;累计读取到长度和定长LEN的报文后&#xff0c;就认为读取到了个完整的消息&#xff0c;然后将计数器位置重置在读取下一个报文内容将回车换行符作为…...

Allegro如何自动添加测试点操作指导

Allegro如何自动添加测试点操作指导 在做PCB设计的时候,在一些应用场合下需要给PCB上的网络添加测试点,如下图 测试点除了可以手动逐个添加之外,Allegro还支持自动添加测试点,具体操作如下 点击Manufacture点击Testprep...

【CSS】CSS 背景设置 ③ ( 背景位置-长度值设置 | 背景位置-长度值方位值同时设置 )

文章目录一、背景位置-长度值设置二、背景位置-长度值方位值同时设置三、完整代码示例一、背景位置-长度值设置 长度值设置 效果展示 : 设置背景位置为具体值 10px 50px : 粉色区域是盒子的区域 , 图片背景位于盒子位置 x 轴方向 10 像素 , y 轴方向 50 像素 ; 在水平方向上 ,…...

AbTest —— 不同场景下的应用模式

文章目录不同人群眼中的 AbTestAbTest 不同的功能倚重用户关联性弱&#xff0c;经典场景为 Feed - 部门组织形式大多非垂直业务用户关联性强&#xff0c;经典场景为 垂类/工具类APP&#xff1b;部门组织形式大多为垂直业务康为定律-组织决定产品形态不同应用模式下服务构建开机…...

fast-api 一款快速将spring的bean发布成接口并生产对应swagger文档调试的轻量级工具

fast-api简介背景开发痛点:分析需求实战fast-api快速上手1. 引入依赖2. FastApiMapping标记service对象3. swagger2/knife4j 在线测试进阶使用开启调试模式支持指定类或包目录发布如何关闭fast-api自定义fast-api的前缀写在最后简介 fast-api 一款快速将spring的bean(service)发…...

以公益之名 让人类发现数学之美

目录 1.品牌理念高举高打 2.创新赛制 赋能品牌 3.全球化的品牌传播 9月26日&#xff0c;2022阿里巴巴全球数学竞赛获奖名单公布&#xff0c;4座金杯分别由平均年龄25岁&#xff0c;来自美国麻省理工学院、美国布朗大学、北京大学在读数学博士斩获。77位获奖者中00后超五成引热…...

JUC并发编程之HashMap(jdk1.7版本)-底层源码探究

目录 JUC并发编程之HashMap(jdk1.7版本)-底层源码探究 HashMap底层源码 - jdk1.7 基本概念 -采取层层递进&#xff0c;问答式 存储Key-Value的结构 常量和成员变量 构造方法 put方法 inflateTable方法 hash方法 indexFor方法 addEntry方法 resize方法 createEntry…...

QT Q_OBJECT 和 signals/slots

Q_OBJECT宏展开 #define Q_OBJECT \ public: \QT_WARNING_PUSH \Q_OBJECT_NO_OVERRIDE_WARNING \static const QMetaObject staticMetaObject; \virtual const QMetaObject *metaObject() const; \virtual void *qt_metacast(const char *); \virtual int qt_metacall(QMetaOb…...

APM新添加UAVCAN设备

简介 UAVCAN是一种轻量级协议,旨在通过CAN总线在航空航天和机器人应用中实现可靠通信。要实现通信&#xff0c;最基本需要data_type_ id, signature、数据结构、设备程序初始化。 添加设备数据结构文件(.uavcan格式) 1.在以下路径添加设备数据结构文件&#xff0c;根据设备类…...

【C++】string类基本用法

文章目录string类基本用法1. 为什么要学习string类&#xff1f;1.1 C语言中的字符串2. 标准库中的string类2.1 string类2.2 string类的常用接口说明小试牛刀1. 仅仅反转字母2. 字符串中第一个唯一字符3. 字符串中最后一个单词的长度string类基本用法 1. 为什么要学习string类&…...

KDZD耐电压高压击穿强度测试仪

一、技术参数 01、输入电压&#xff1a; 交流 220 V。 02、输出电压&#xff1a; 交流 0--50KV ; 直流 0—50kv 。 03、电器容量&#xff1a;3KVA。 04、高压分级&#xff1a;0—50KV&#xff0c;&#xff08;全程可调&#xff09;。 05、升压速率&#xff1a;0.1KV/s-…...

数组和指针面试题的补充(细的抠jio)

生命是一条艰险的峡谷&#xff0c;只有勇敢的人才能通过。 ——米歇潘 说明&#xff1a;用的vs都是x86的环境&#xff0c;也就是32位平台。 建议&#xff1a;对于难题来说&#xff0c;一定要配合画图来解决问题。 第一题&#xff1a; #include<stdio.h> int…...

Java多线程基础

文章目录Java多线程基础一、什么是进程与线程&#xff1f;二、线程和进程的区别【重点】三、线程的创建方式【重点】1. 继承Thread类2. 实现Runnable接口3. lambda 表达式四、Thread的常见属性线程中断自己定义一个标志位Thread类提供的静态方法线程的状态Java多线程基础 一、…...

爆品分析第5期 | 一条视频带货3700+,这款斋月不锈钢厨具套装火了!

俗话说民以食为天&#xff0c;吃在任何一种文化中都占据重要的位置&#xff0c;要做出一道美味佳肴&#xff0c;除了食材、烹饪者的自身厨艺之外&#xff0c;还少不了一口好锅。新冠疫情以来&#xff0c;全世界范围内的封闭让很多人养成了居家做饭的习惯&#xff0c;不仅为厨具…...

团队管理的七个要点

要掌握团队管理的要点和做好团队管理工作&#xff0c;不是一件容易的事&#xff0c;但也远非想象中那么难。首先&#xff0c;我个人比较推荐所有团队管理者都能阅读下《经理人参阅&#xff1a;团队管理》&#xff08;注意该书仅可其官网获得&#xff09;这本佳作。相信会为你带…...

Go语言容器之map、list和nil

一、map map和C中map一样&#xff0c;里面存放的是key-value键值对在Go中map是引用类型&#xff0c;声明语法&#xff1a;var map变量名 map[key的类型]value的类型package mainimport "fmt"func main() {var mp map[string]intmpls : map[string]int{"one&quo…...

软件测试的案例分析 - 闰年1

&#xff08;这是关于博客质量分的测试 https://www.csdn.net/qc) 我们谈了不少测试的名词, 软件是人写的, 测试计划和测试用例也是人写的, 人总会犯错误。错误发生之后, 总有人问: 为什么这个bug 没有测出来啊?! 我们看看一类简单的bug是如何发生的&#xff0c;以及如何预防…...

【强化学习】强化学习数学基础:值函数近似

值函数近似Value Function ApproximationMotivating examples: curve fittingAlgorithm for state value estimationObjective functionOptimization algorithmsSelection of function approximatorsIllustrative examplesSummary of the storyTheoretical analysisSarsa with …...

JVM系列——Java与线程,介绍线程原理和操作系统的关系

并发不一定要依赖多线程(如PHP中很常见的多进程并发)。 但是在Java里面谈论并发&#xff0c;基本上都与线程脱不开关系。因此我们讲一下从Java线程在虚拟机中的实现。 线程的实现 线程是比进程更轻量级的调度执行单位。 线程的引入&#xff0c;可以把一个进程的资源分配和执行调…...

C++打开文件夹对话框之BROWSEINFO

头文件 #include <shlobj.h> #include <windows.h> #include <stdio.h> using namespace std; 案例 string chooseFile(void) {//用户选择的路径&#xff0c;可以是TCHAR szBuffer[MAX_PATH] {0};然后再使用TCHAR 转char字符串&#xff0c;此处可以直接使…...

Nuxt项目配置、目录结构说明-实战教程基础-Day02

Nuxt项目配置、目录结构说明-实战教程基础-Day02一、Nuxt项目结构1.1资源目录1.2 组件目录1.3 布局目录1.4 中间件目录1.5 页面目录1.6 插件目录1.7 静态文件目录1.8 Store 目录1.9 nuxt.config.js 文件1.10 package.json 文件其他&#xff1a;别名二、项目配置2.1 build2.2 cs…...

单链表的头插,尾插,头删,尾删等操作

前言顺序表要求是具有连续的物理空间&#xff0c;并且数据的话是在这些空间当中是连续的存储。但这样会带来很多问题&#xff0c;比如说在头部或者说中间插入的话&#xff0c;效率不是很高&#xff1b;并且申请空间可能需要扩容&#xff0c;并且越往后一般来说都是异地扩容&…...

Qt扫盲-QProcess理论总结

QProcess理论使用总结一、概述二、使用三、通过 Channel 通道通信四、同步进程API五、注意事项1. 平台特性2. 不能实时读取一、概述 QProcess 其实更多的是与外面进程进行交互的一个工具类&#xff0c;通过这个类来启动外部进程&#xff0c;获取这个进程的标准输出&#xff0c…...

JAVA进阶 —— Steam流

目录 一、 引言 二、 Stream流概述 三、Stream流的使用步骤 1. 获取Stream流 1.1 单列集合 1.2 双列集合 1.3 数组 1.4 零散数据 2. Stream流的中间方法 3. Stream流的终结方法 四、 练习 1. 数据过滤 2. 数据操作 - 按年龄筛选 3. 数据操作 - 演员信息要求…...

Ubuntu Protobuf 安装(测试有效)

安装流程 下载软件 下载自己要安装的版本&#xff1a;https://github.com/protocolbuffers/protobuf 下载源码编译&#xff1a; 系统环境&#xff1a;Ubuntu16&#xff08;其它版本亦可&#xff09;&#xff0c;Protobuf-3.6.1 编译源码 cd protobuf# 当使用 git clone 下来的…...

驱动程序开发:FTP服务器和OpenSSH的移植与搭建、以及一些笔记

目录一、FTP服务器移植与搭建1、在ubuntu下安装vsftpd2、在window下安装FileZilla3、移植vsftpd到开发板上4、Filezilla 连接测试5、注意点二、开发板 OpenSSH 移植与使用1、移植 zlib 库2、移植 openssl 库3、移植 openssh 库4、openssh 使用测试三、关于u-boot上的操作及根文…...

优化改进YOLOv5算法之添加GIoU、DIoU、CIoU、EIoU、Wise-IoU模块(超详细)

目录 1、IoU 1.1 什么是IOU 1.2 IOU代码 2、GIOU 2.1 为什么提出GIOU 2.2 GIoU代码 3 DIoU 3.1 为什么提出DIOU 3.2 DIOU代码 4 CIOU 4.1 为什么提出CIOU 4.2 CIOU代码 5 EIOU 5.1 为什么提出EIOU 5.2 EIOU代码 6 Wise-IoU 7 YOLOv5中添加GIoU、DIoU、CIoU、…...

windows电脑pc如何使用svn获取文档和代码

一、安装svn 下载链接 也可通过其他方式下载 二、使用 2.1 随便找一个文件夹 2.2 点击右键&#xff0c;选择SVN Checkout 2.3输入网址 如当你在网页上访问时地址为https://10.197.78.78/!/#aaa/view/head/bbb 在这里不能直接填入&#xff0c;而是 https://10.197.78.78/sv…...

建筑网站制作/搜索引擎优化案例分析

线程只要分为&#xff1a;主线程和子线程&#xff0c;主线程主要处理和界面相关的事情&#xff0c;而子线程则往往用于执行耗时的操作&#xff0c;由于Android的特性&#xff0c;如果在主线程中执行耗时操作那么就会导致程序无法及时响应&#xff0c;因此耗时操作必须方法子线程…...

wordpress 枚举用户/网络营销运营策划

http://acm.hdu.edu.cn/showproblem.php?pid4549 Problem DescriptionM斐波那契数列F[n]是一种整数数列&#xff0c;它的定义如下&#xff1a;F[0] aF[1] bF[n] F[n-1] * F[n-2] ( n > 1 )现在给出a, b, n&#xff0c;你能求出F[n]的值吗&#xff1f;Input输入包含多组…...

公司网站改版要怎么做/百度网址

ecshop版权的修改&#xff0c;头部&#xff0c;底部 2011-03-28 23:33ecshop的title的修改 前面我们已经讲过如何删除ecshop的版权&#xff0c;但是还有很多人不会&#xff0c;今天就详细的讲下如何删除所有ecshop版权和logo 前台部分&#xff1a; 1:去掉头部TITLE部分的ECSHO…...

招聘网哪个真实可靠一些/免费关键词优化工具

当你大量使用innodb引擎的数据表时,有时你需要停止mysql数据库或者启动mysql数据库的时候会发现,时间相当的长,其实这个并不是完全由于你的数据库太大导致的,其实也与innodb_max_dirty_pages_pct这个选项有关的,我们可以来减少它的值来提高我们关闭mysql或者启动mysql所花费的时…...

欧米茄官方网站/优化公司网站

实例如下所示&#xff1a;/*** 获取某年第几周的开始日期和结束日期* param int $year* param int $week 第几周;*/public function weekday($year,$week1){$year_start mktime(0,0,0,1,1,$year);$year_end mktime(0,0,0,12,31,$year);// 判断第一天是否为第一周的开始if (in…...

企业网站免费建站/产品推广计划书怎么写

论文题目&#xff1a;Extracting Entities and Events as a Single Task Using a Transition-Based Neural Model 论文来源&#xff1a;IJCAI 2019 武汉大学, 东华大学, 西湖大学 论文链接&#xff1a;https://www.ijcai.org/Proceedings/2019/753 代码链接&#xff1a;http…...