insertHTML.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import {hasClosestBlock, hasClosestByAttribute, hasClosestByClassName, hasClosestByMatchTag} from "./hasClosest";
  2. import * as dayjs from "dayjs";
  3. import {transaction, updateTransaction} from "../wysiwyg/transaction";
  4. import {getContenteditableElement, hasNextSibling, hasPreviousSibling} from "../wysiwyg/getBlock";
  5. import {fixTableRange, focusBlock, focusByWbr, getEditorRange} from "./selection";
  6. import {mathRender} from "../render/mathRender";
  7. import {Constants} from "../../constants";
  8. import {highlightRender} from "../render/highlightRender";
  9. import {scrollCenter} from "../../util/highlightById";
  10. import {updateAVName} from "../render/av/action";
  11. import {readText} from "./compatibility";
  12. export const insertHTML = (html: string, protyle: IProtyle, isBlock = false,
  13. // 移动端插入嵌入块时,获取到的 range 为旧值
  14. useProtyleRange = false) => {
  15. if (html === "") {
  16. return;
  17. }
  18. const range = useProtyleRange ? protyle.toolbar.range : getEditorRange(protyle.wysiwyg.element);
  19. fixTableRange(range);
  20. let tableInlineHTML;
  21. if (hasClosestByAttribute(range.startContainer, "data-type", "NodeTable") && !isBlock) {
  22. if (hasClosestByMatchTag(range.startContainer, "table")) {
  23. tableInlineHTML = protyle.lute.BlockDOM2InlineBlockDOM(html);
  24. } else {
  25. // https://github.com/siyuan-note/siyuan/issues/9411
  26. isBlock = true;
  27. }
  28. }
  29. let blockElement = hasClosestBlock(range.startContainer) as Element;
  30. if (!blockElement) {
  31. // 使用鼠标点击选则模版提示列表后 range 丢失
  32. if (protyle.toolbar.range) {
  33. blockElement = hasClosestBlock(protyle.toolbar.range.startContainer) as Element;
  34. } else {
  35. blockElement = protyle.wysiwyg.element.firstElementChild as Element;
  36. }
  37. }
  38. if (!blockElement) {
  39. return;
  40. }
  41. if (blockElement.classList.contains("av")) {
  42. range.deleteContents();
  43. const text = readText();
  44. if (typeof text === "string") {
  45. range.insertNode(document.createTextNode(text));
  46. range.collapse(false);
  47. updateAVName(protyle, blockElement);
  48. } else {
  49. text.then((t) => {
  50. range.insertNode(document.createTextNode(t));
  51. range.collapse(false);
  52. updateAVName(protyle, blockElement);
  53. });
  54. }
  55. return;
  56. }
  57. let id = blockElement.getAttribute("data-node-id");
  58. range.insertNode(document.createElement("wbr"));
  59. let oldHTML = blockElement.outerHTML;
  60. const isNodeCodeBlock = blockElement.getAttribute("data-type") === "NodeCodeBlock";
  61. if (!isBlock &&
  62. (isNodeCodeBlock || protyle.toolbar.getCurrentType(range).includes("code"))) {
  63. range.deleteContents();
  64. range.insertNode(document.createTextNode(html.replace(/\r\n|\r|\u2028|\u2029/g, "\n")));
  65. range.collapse(false);
  66. range.insertNode(document.createElement("wbr"));
  67. if (isNodeCodeBlock) {
  68. getContenteditableElement(blockElement).removeAttribute("data-render");
  69. highlightRender(blockElement);
  70. } else {
  71. focusByWbr(blockElement, range);
  72. }
  73. blockElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss"));
  74. updateTransaction(protyle, id, blockElement.outerHTML, oldHTML);
  75. setTimeout(() => {
  76. scrollCenter(protyle, blockElement, false, "smooth");
  77. }, Constants.TIMEOUT_LOAD);
  78. return;
  79. }
  80. const undoOperation: IOperation[] = [];
  81. const doOperation: IOperation[] = [];
  82. if (range.toString() !== "") {
  83. const inlineMathElement = hasClosestByAttribute(range.commonAncestorContainer, "data-type", "inline-math");
  84. if (inlineMathElement) {
  85. // 表格内选中数学公式 https://ld246.com/article/1631708573504
  86. inlineMathElement.remove();
  87. } else if (range.startContainer.nodeType === 3 && range.startContainer.parentElement.getAttribute("data-type")?.indexOf("block-ref") > -1) {
  88. // ref 选中处理 https://ld246.com/article/1629214377537
  89. range.startContainer.parentElement.remove();
  90. // 选中 ref**bbb** 后 alt+[
  91. range.deleteContents();
  92. } else {
  93. range.deleteContents();
  94. }
  95. range.insertNode(document.createElement("wbr"));
  96. undoOperation.push({
  97. action: "update",
  98. id,
  99. data: oldHTML
  100. });
  101. doOperation.push({
  102. action: "update",
  103. id,
  104. data: blockElement.outerHTML
  105. });
  106. }
  107. const tempElement = document.createElement("template");
  108. // 需要再 spin 一次 https://github.com/siyuan-note/siyuan/issues/7118
  109. tempElement.innerHTML = tableInlineHTML // 在 table 中插入需要使用转换好的行内元素 https://github.com/siyuan-note/siyuan/issues/9358
  110. || protyle.lute.SpinBlockDOM(html) ||
  111. html; // 空格会被 Spin 不再,需要使用原文
  112. const editableElement = getContenteditableElement(blockElement);
  113. // 使用 lute 方法会添加 p 元素,只有一个 p 元素或者只有一个字符串或者为 <u>b</u> 时的时候只拷贝内部
  114. if (!isBlock) {
  115. if (tempElement.content.firstChild.nodeType === 3 ||
  116. (tempElement.content.firstChild.nodeType !== 3 &&
  117. ((tempElement.content.firstElementChild.classList.contains("p") && tempElement.content.childElementCount === 1) ||
  118. tempElement.content.firstElementChild.tagName !== "DIV"))) {
  119. if (tempElement.content.firstChild.nodeType !== 3 && tempElement.content.firstElementChild.classList.contains("p")) {
  120. tempElement.innerHTML = tempElement.content.firstElementChild.firstElementChild.innerHTML.trim();
  121. }
  122. // 粘贴带样式的行内元素到另一个行内元素中需进行切割
  123. const spanElement = range.startContainer.nodeType === 3 ? range.startContainer.parentElement : range.startContainer as HTMLElement;
  124. if (spanElement.tagName === "SPAN" && spanElement.isSameNode(range.endContainer.nodeType === 3 ? range.endContainer.parentElement : range.endContainer) &&
  125. // 粘贴纯文本不需切割 https://ld246.com/article/1665556907936
  126. // emoji 图片需要切割 https://github.com/siyuan-note/siyuan/issues/9370
  127. tempElement.content.querySelector("span, img")
  128. ) {
  129. const afterElement = document.createElement("span");
  130. const attributes = spanElement.attributes;
  131. for (let i = 0; i < attributes.length; i++) {
  132. afterElement.setAttribute(attributes[i].name, attributes[i].value);
  133. }
  134. range.setEnd(spanElement.lastChild, spanElement.lastChild.textContent.length);
  135. afterElement.append(range.extractContents());
  136. spanElement.after(afterElement);
  137. range.setStartBefore(afterElement);
  138. range.collapse(true);
  139. }
  140. range.insertNode(tempElement.content.cloneNode(true));
  141. range.collapse(false);
  142. blockElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss"));
  143. // 使用 innerHTML,避免行内元素为代码块
  144. const trimStartText = editableElement ? editableElement.innerHTML.trimStart() : "";
  145. if (editableElement && (trimStartText.startsWith("```") || trimStartText.startsWith("~~~") || trimStartText.startsWith("···") ||
  146. trimStartText.indexOf("\n```") > -1 || trimStartText.indexOf("\n~~~") > -1 || trimStartText.indexOf("\n···") > -1)) {
  147. if (trimStartText.indexOf("\n") === -1 && trimStartText.replace(/·|~/g, "`").replace(/^`{3,}/g, "").indexOf("`") > -1) {
  148. // ```test` 不处理
  149. } else {
  150. let replaceInnerHTML = editableElement.innerHTML.replace(/^(~|·|`){3,}/g, "```").replace(/\n(~|·|`){3,}/g, "\n```").trim();
  151. if (!replaceInnerHTML.endsWith("\n```")) {
  152. replaceInnerHTML += "\n```";
  153. }
  154. const languageIndex = replaceInnerHTML.indexOf("```") + 3;
  155. replaceInnerHTML = replaceInnerHTML.substring(0, languageIndex) + (localStorage["local-codelang"] || "") + replaceInnerHTML.substring(languageIndex);
  156. editableElement.innerHTML = replaceInnerHTML;
  157. }
  158. }
  159. const editWbrElement = editableElement.querySelector("wbr");
  160. if (editWbrElement && editableElement && !trimStartText.endsWith("\n")) {
  161. // 数学公式后无换行,后期渲染后添加导致 rang 错误,中文输入错误 https://github.com/siyuan-note/siyuan/issues/9054
  162. const previousElement = hasPreviousSibling(editWbrElement) as HTMLElement;
  163. if (previousElement && previousElement.nodeType !== 3 && (previousElement.dataset.type || "").indexOf("inline-math") > -1 &&
  164. !hasNextSibling(editWbrElement)) {
  165. editWbrElement.insertAdjacentText("afterend", "\n");
  166. }
  167. }
  168. mathRender(blockElement);
  169. updateTransaction(protyle, id, blockElement.outerHTML, oldHTML);
  170. focusByWbr(protyle.wysiwyg.element, range);
  171. return;
  172. }
  173. }
  174. const cursorLiElement = hasClosestByClassName(blockElement, "li");
  175. // 列表项不能单独进行粘贴 https://ld246.com/article/1628681120576/comment/1628681209731#comments
  176. if (tempElement.content.children[0]?.getAttribute("data-type") === "NodeListItem") {
  177. if (cursorLiElement) {
  178. blockElement = cursorLiElement;
  179. id = blockElement.getAttribute("data-node-id");
  180. oldHTML = blockElement.outerHTML;
  181. } else {
  182. const liItemElement = tempElement.content.children[0];
  183. const subType = liItemElement.getAttribute("data-subtype");
  184. tempElement.innerHTML = `<div${subType === "o" ? " data-marker=\"1.\"" : ""} data-subtype="${subType}" data-node-id="${Lute.NewNodeID()}" data-type="NodeList" class="list">${html}<div class="protyle-attr" contenteditable="false">${Constants.ZWSP}</div></div>`;
  185. }
  186. }
  187. let lastElement: Element;
  188. Array.from(tempElement.content.children).reverse().forEach((item) => {
  189. let addId = item.getAttribute("data-node-id");
  190. if (addId === id) {
  191. doOperation.push({
  192. action: "update",
  193. data: item.outerHTML,
  194. id: addId,
  195. });
  196. undoOperation.push({
  197. action: "update",
  198. id: addId,
  199. data: oldHTML,
  200. });
  201. } else {
  202. if (item.classList.contains("li") && !blockElement.parentElement.classList.contains("list")) {
  203. // https://github.com/siyuan-note/siyuan/issues/6534
  204. addId = Lute.NewNodeID();
  205. const liElement = document.createElement("div");
  206. liElement.setAttribute("data-subtype", item.getAttribute("data-subtype"));
  207. liElement.setAttribute("data-node-id", addId);
  208. liElement.setAttribute("data-type", "NodeList");
  209. liElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss"));
  210. liElement.classList.add("list");
  211. liElement.append(item);
  212. item = liElement;
  213. }
  214. doOperation.push({
  215. action: "insert",
  216. data: item.outerHTML,
  217. id: addId,
  218. previousID: id
  219. });
  220. undoOperation.push({
  221. action: "delete",
  222. id: addId,
  223. });
  224. }
  225. blockElement.after(item);
  226. if (!lastElement) {
  227. lastElement = item;
  228. }
  229. });
  230. if (editableElement && editableElement.textContent === "" && blockElement.classList.contains("p")) {
  231. // 选中当前块所有内容粘贴再撤销会导致异常 https://ld246.com/article/1662542137636
  232. doOperation.find((item, index) => {
  233. if (item.id === id) {
  234. doOperation.splice(index, 1);
  235. return true;
  236. }
  237. });
  238. doOperation.push({
  239. action: "delete",
  240. id
  241. });
  242. // 选中当前块所有内容粘贴再撤销会导致异常 https://ld246.com/article/1662542137636
  243. undoOperation.find((item, index) => {
  244. if (item.id === id && item.action === "update") {
  245. undoOperation.splice(index, 1);
  246. return true;
  247. }
  248. });
  249. undoOperation.push({
  250. action: "insert",
  251. data: oldHTML,
  252. id,
  253. previousID: blockElement.previousElementSibling ? blockElement.previousElementSibling.getAttribute("data-node-id") : "",
  254. parentID: blockElement.parentElement.getAttribute("data-node-id") || protyle.block.parentID
  255. });
  256. blockElement.remove();
  257. }
  258. if (lastElement) {
  259. // https://github.com/siyuan-note/siyuan/issues/5591
  260. focusBlock(lastElement, undefined, false);
  261. }
  262. const wbrElement = protyle.wysiwyg.element.querySelector("wbr");
  263. if (wbrElement) {
  264. wbrElement.remove();
  265. }
  266. transaction(protyle, doOperation, undoOperation);
  267. };