fix: prosemirror adds two extra spaces when paste

news2024/12/24 16:46:51

bug

项目使用 prosemirror,复制 NodeSelection 时,会在末尾多出两个空格。


NodeSelection

prosemirror 的 Selection 是抽象类,它有三个子类

  • TextSelection 最常见的
  • NodeSelection 指向单一节点的选区。设置了 selectable = true 的节点,点击选中为 NodeSelection
  • AllSelection 全选整个文档

对于设置了 selectable = true 的节点,点击时是 NodeSelection,拖选时是 TextSelection。用 prosemirror 的 dino例子来举例说明:

点击红色恐龙

在这里插入图片描述

控制台查看 dom,是 selectednode

在这里插入图片描述

此时选区是 NodeSelection

在这里插入图片描述

拖选红色恐龙

在这里插入图片描述

查看 dom,不是 selectednode

在这里插入图片描述

此时选区是 TextSelection

在这里插入图片描述


debug

定位事件处理函数

在这里插入图片描述
控制台 - Elements - 选中 prosemirror 容器 - Event Listeners - copy & paste,找到 prosemirror-view 的 input.ts 文件:

handlers.copy = editHandlers.cut = (view, _event) => {
  let event = _event as ClipboardEvent
  let sel = view.state.selection, cut = event.type == "cut"
  if (sel.empty) return

  // IE and Edge's clipboard interface is completely broken
  let data = brokenClipboardAPI ? null : event.clipboardData
  let slice = sel.content(), {dom, text} = serializeForClipboard(view, slice)
  if (data) {
    event.preventDefault()
    data.clearData()
    data.setData("text/html", dom.innerHTML)
    data.setData("text/plain", text)
  } else {
    captureCopy(view, dom)
  }
  if (cut) view.dispatch(view.state.tr.deleteSelection().scrollIntoView().setMeta("uiEvent", "cut"))
}

editHandlers.paste = (view, _event) => {
  let event = _event as ClipboardEvent
  // Handling paste from JavaScript during composition is very poorly
  // handled by browsers, so as a dodgy but preferable kludge, we just
  // let the browser do its native thing there, except on Android,
  // where the editor is almost always composing.
  if (view.composing && !browser.android) return
  let data = brokenClipboardAPI ? null : event.clipboardData
  let plain = view.input.shiftKey && view.input.lastKeyCode != 45
  if (data && doPaste(view, data.getData("text/plain"), data.getData("text/html"), plain, event))
    event.preventDefault()
  else
    capturePaste(view, event)
}

复制

简化一下代码:

handlers.copy = editHandlers.cut = (view, e) => {
  if (空选区) { return }

  const {dom, text} = serializeForClipboard(view, view.state.selection.content())
  event.preventDefault()
  e.clipboardData.clearData()
  e.clipboardData.setData("text/html", dom.innerHTML)
  e.clipboardData.setData("text/plain", text)

  if (是剪切) { 删除选区 }
}

核心操作是,通过 serializeForClipboard 获取 domtext,然后 set 到 clipboardData 中。

点击 dino 节点,复制(此时是NodeSelection)。打断点查看,set 进去的 html 是:

"<img dino-type="stegosaurus" src="/img/dino/stegosaurus.png" title="stegosaurus" class="dinosaur" data-pm-slice="0 0 []">"

set 进去的 text 是空字符串。

到这一步还没有问题

粘贴

接下来看粘贴,同样简化一下代码:

editHandlers.paste = (view, event) => {
  // ctrl+v 和 shift+insert 粘贴;ctrl+shift+v 粘贴为纯文本
  let plain = view.input.shiftKey && view.input.lastKeyCode != 45	// 粘贴为纯文本flag
  doPaste(view, event.clipboardData.getData("text/plain"), event.clipboardData.getData("text/html"), plain, event)
  event.preventDefault() 
}

核心操作是,把 clipboardData 中的数据取出来,调用 doPaste

粘贴。打断点查看:

在这里插入图片描述
文本数据是空字符串,没问题。html 数据变了,多套了 html > body > startFragment

一步步查看 doPaste -> parseFromClipboard ,发现 parser.parseSlice 返回值不对。它的返回值是 [dino节点,TextNode<两个空格>

继续查看 parseSlice 内部实现,发现它用 childNodes 获取子节点,得到[text, comment, img, comment, text ](如果用 children 只会得到一个子节点 img)。一头一尾的 text 内容都是 “\n\n”,两个 comment 节点分别是 startFragment 和 EndFragment。

循环处理子节点:开头的 \n\n 被忽略,startFragment 忽略,img 保留,endFragment 忽略,结尾的 \n\n 保留

换成 TextSelection,复制

对比看看 TextSelection。复制,打断点查看,set 进去的 html 数据 是:(和刚才 NodeSelection 时相比,外面多套了一层 p)

<p data-pm-slice="1 1 []"><img dino-type="stegosaurus" src="/img/dino/stegosaurus.png" title="stegosaurus" class="dinosaur"></p>

text 还是空字符串

TextSelection,粘贴

获取的 html 数据是:

<html>
<body>
<!--StartFragment--><p data-pm-slice="1 1 []"><img dino-type="stegosaurus" src="/img/dino/stegosaurus.png" title="stegosaurus" class="dinosaur"></p><!--EndFragment-->
</body>
</html>

doPaste -> parseFromClipboard -> parser.parseSlice,childNodes 是 [text, comment, p>img, comment, text]。循环处理子节点,和 NodeSelection 不同的是,处理到最后一项 \n\n 时,这一项被忽略了(简单看了一下,大概是循环到最后一项是,已生成的节点是[p>img],判断最后一项的根节点 p 是块级元素,就把当前的 \n\n 忽略掉了)

总结

  1. 复制时向 e.clipboardData 中设置数据。NodeSelection 设置的是 img,TextSelection 是 p>img
  2. 粘贴时从 e.clipboardData 中获取数据。NodeSelection 获取到的数据多套了 html>body>startFragment, TextSelection 也多套了
  3. 粘贴时,parseSlice 方法内部通过 childNodes 取子节点。NodeSelection 取到的是 [text, comment, img, comment, text],TextSelection 是 [text, comment, p>img, comment, text]。comment 都被去掉了,开头的 \n\n 都被去掉了,但是结尾的 \n\n 在NodeSelection 中被保留,Text Selection 中去掉了

通过看代码已经 debug 不下去了,改不动。不理解为什么要多套 html>body,弄得最后多俩空格,无语


discussion

又去 google,看到了 Space added on paste 。和我遇到的问题一样。有人回复:
在这里插入图片描述
居然是 windows 干的好事,完全没想到。我一直以为是 prosemirror 内部处理什么东西加的。。

Windows HTML Clipboard Format HTML: The fragment should be preceded and followed by the HTML comments and to indicate where the fragment starts and ends

解决方法上面也贴出来了,在 transformPastedHTML 中把 windows 加的这些东西给去掉就 ok 了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1098167.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++项目实战——基于多设计模式下的同步异步日志系统-⑪-日志器管理类与全局建造者类设计(单例模式)

文章目录 专栏导读日志器建造者类完善单例日志器管理类设计思想单例日志器管理类设计全局建造者类设计日志器类、建造者类整理日志器管理类测试 专栏导读 &#x1f338;作者简介&#xff1a;花想云 &#xff0c;在读本科生一枚&#xff0c;C/C领域新星创作者&#xff0c;新星计…

达梦数据库适配ServiceStack框架

注&#xff1a;达梦的驱动版本请使用2023第四季度及以后版本驱动才可以 ServiceStack介绍 ServiceStack官网&#xff1a; https://github.com/ServiceStack/ServiceStack ServiceStack是一个开源的十分流行的WebService框架&#xff0c;引用其官网的介绍&#xff1a;“Servic…

创建React Native的第一个hello world工程

创建React Native的第一个hello world工程 需要安装好node、npm环境 如果之前没有安装过react-native-cli脚手架的&#xff0c;可以按照下述步骤直接安装。如果已经安装过的&#xff0c;但是在使用这个脚手架初始化工程的时候遇到下述报错的话 cli.init(root, projectname);…

FPGA中的LUT查找表工作原理。

在RAM中填入1110,后续的不同AB组合Y输出对应的值&#xff0c;实现上面逻辑表达式的功能。

windows编译ollvm笔记

准备工作 1.找到Android SDK目录配置好cmake环境变量 E:\AndroidSDK\cmake\3.18.1&#xff08;E:\AndroidSDK为 Android SDK目录地址&#xff09;。 下载llvm-mingw编译环境(gcc编译器的windows版本&#xff0c;即可以在windows平台上使用gcc编译器)&#xff0c;下载地址&…

Linux安装rpm包在线安装mysql5.7

以前安装过mysql 前言&#xff1a;检查以前是否装有mysql rpm -qa|grep -i mysql安装了会显示&#xff1a;   bt-mysql57-5.7.31-1.el7.x86_64 停止mysql服务和删除之前安装的mysql rpm -e bt-mysql57-5.7.31-1.el7.x86_64查找并删除mysql相关目录 find / -name mysql/va…

QT开发工业自动化控制软件的几个常用模块

最近两年一直从事工业自动化制造企业的软件开发&#xff0c;发现跟以前开发网络软件还是有较大的区别&#xff0c;重点就是在一些细的方面&#xff0c;比如架构、模块、通讯之类的。下面举几个例子&#xff1a; 1、数字键盘&#xff08;替代普通键盘的小数字键盘&#xff09; …

Jenkins 内存占用

查看内存占用 # ps aux | grep 9090 root 130854 0.0 0.0 8900 708 pts/1 S 16:23 0:00 grep --colorauto 9090 root 4010748 0.2 30.7 5826500 2502884 ? Ssl Oct13 8:55 /usr/bin/java -Djava.awt.headlesstrue -jar /usr/share/java/jenkins…

数据在内存中的存储(2)

文章目录 3. 浮点型在内存中的存储3.1 一个例子3.2 浮点数存储规则 3. 浮点型在内存中的存储 常见的浮点数&#xff1a; 3.14159 1E10 ------ 1.0 * 10^10 浮点数家族包括&#xff1a; float、double、long double 类型 浮点数表示的范围&#xff1a;float.h中定义 3.1 一个例…

C++基本语法【恩培学习笔记(一)】

文章目录 1、C程序结构1.1 C程序的基本组成部分1.2 预处理指令1.3 注释1.4 main() 主函数1.5 命名空间 namespace 2、 C的变量和常量2.1 变量2.2 变量的声明2.3 变量的类型 3、C 数组和容器3.1 数组&#xff08;array&#xff09;3.2 容器&#xff08;vector&#xff09; 4、C …

创建scala项目并增加新的object试运行

一、创建scala项目 依赖配置&#xff1a; scala&#xff0c;jdk&#xff0c;maven 没有maven也可以创建 1.1 直接创建 1.1.1 创建 选择新project 路径、依赖配置、代码调试 1.1.2 项目结构 Scala项目中几个文件&#xff1a; .idea&#xff1a;这个文件夹是用来存储项目的…

Android酒店客房预订系统 后台管理+前端app 包含视频教程

【项目功能介绍】 功能列表: 本系统包含后台管理和前端app双端系统, 本系统包含三个角色: 管理员,员工,app用户。 后台管理员的功能包含: 登录, 退出, ,酒店管理,添加酒店,修改酒店,禁用启用酒店; 酒店客房管理,添加客房,修改客房,启用禁用客房; 订单管理,确定订单,拒绝订单,用…

Android视音频知识

Android视音频知识 视音频完整解码播放流程分析。 视音频完整录制编码流程分析。 为什么要编码&#xff0c;如何编码(编码原理) ?。 为什么要编码&#xff1f; 因为视频文件实在太大了&#xff0c;一部电影 200多个GB&#xff0c;编码&#xff1a;1G 视频是连续的图像序列&a…

【Linux】chmod 命令使用

chmod&#xff08;英文全拼&#xff1a;change mode&#xff09;命令是控制用户对文件的权限的命令。 chmod命令 -Linux手册页 著者 作者&#xff1a;David MacKenzie和Jim Meyering。 语法 chmod [选项] [模式] 文件或目录 Linux/Unix 的文件调用权限分为三级 : 文件所有者…

Spring-AOP-加强

目录 简略介绍 AOP是如何实现的 实现时机 实现原理 简略介绍 AOP(Aspect-Oriented Programming)&#xff0c;即面向切面编程&#xff0c;用人话说就是把公共的逻辑抽出来&#xff0c;让开发者可以更专注于业务逻辑开发和IOC一样&#xff0c;AOP也指的是一种思想AOP思想是OO…

自动化测试框架中如何记录日志更加已读 ?一文介绍使用loguru来管理日志的心得。

只要做代码开发&#xff0c;记录日志必不可少的 &#xff0c;对于像我这样的测试开发同学也是 &#xff0c;你在编写自动化时如何记录日志 &#xff1f;怎么要日志记录更容易已读 &#xff1f;如何备份日志文件 &#xff1f; 这都是我们在编写代码时要考虑的问题 &#xff0c;如…

JNI 的数据类型以及和Java层之间的数据转换

JNI的数据类型和类型签名 数据类型 JNI的数据类型包含两种&#xff1a;基本类型和引用类型。 基本类型主要有jboolean、jchar、jint等&#xff0c;它们和Java中的数据类型的对应关系如下表所示。 JNI中的引用类型主要有类、对象和数组&#xff0c;它们和Java中的引用类型的对…

ICC2:如何抓取“no net“的shape和via

我正在「拾陆楼」和朋友们讨论有趣的话题&#xff0c;你⼀起来吧&#xff1f; 拾陆楼知识星球入口 pr过程中(尤其是eco)会产生一些no net的shape或via&#xff0c;它们会造成drc和lvs问题&#xff0c;但是常规的办法无法把他们抓出来&#xff0c;下面分享可以获取no net的方法…

数字化时代下,汽车行业如何突破现有营销困境?

之前三年的“口罩”时期&#xff0c;给全球和中国汽车市场带来不小影响&#xff0c;汽车销售市场整体下滑&#xff0c;传统营销模式很难适应现阶段汽车营销需求&#xff0c;那么在当下&#xff0c;汽车行业应该如何突破现有营销困境呢&#xff1f;接下来就由媒介盒子跟大家聊聊…

如何同步 Github 和 Gitee的仓库代码

一、从github导入仓库&#xff0c;手动同步 在 Gitee 的项目主页&#xff0c;导入的仓库会会有一个同步的按钮&#xff0c;你只用点一下&#xff0c;即可与 Github 同步更新&#xff0c;但是注意这里的同步功能默认是强制同步。有点麻烦的是&#xff0c;我们需要在推送到 Githu…