A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 576 lines 28 kB view raw
1import {hasClosestBlock, hasClosestByAttribute, hasClosestByClassName, hasClosestByTag} from "./hasClosest"; 2import * as dayjs from "dayjs"; 3import {transaction, updateTransaction} from "../wysiwyg/transaction"; 4import {getContenteditableElement} from "../wysiwyg/getBlock"; 5import { 6 fixTableRange, 7 focusBlock, 8 focusByRange, 9 focusByWbr, 10 getEditorRange, 11 getSelectionOffset, 12 setLastNodeRange, 13} from "./selection"; 14import {Constants} from "../../constants"; 15import {highlightRender} from "../render/highlightRender"; 16import {scrollCenter} from "../../util/highlightById"; 17import {updateAttrViewCellAnimation, updateAVName} from "../render/av/action"; 18import {updateCellsValue} from "../render/av/cell"; 19import {input} from "../wysiwyg/input"; 20import {fetchPost} from "../../util/fetch"; 21import {isIncludeCell} from "./table"; 22import {getFieldIdByCellElement} from "../render/av/row"; 23import {processClonePHElement} from "../render/util"; 24import {setFold} from "../../menus/protyle"; 25 26const processAV = (range: Range, html: string, protyle: IProtyle, blockElement: HTMLElement) => { 27 const tempElement = document.createElement("template"); 28 tempElement.innerHTML = html; 29 let values: string[][] = []; 30 if (html.endsWith("]") && html.startsWith("[")) { 31 try { 32 values = JSON.parse(html); 33 } catch (e) { 34 console.warn("insert cell: JSON.parse error"); 35 } 36 } else if (tempElement.content.querySelector("table")) { 37 tempElement.content.querySelectorAll("tr").forEach(item => { 38 values.push([]); 39 Array.from(item.children).forEach(cell => { 40 values[values.length - 1].push(cell.textContent); 41 }); 42 }); 43 } 44 const avID = blockElement.dataset.avId; 45 fetchPost("/api/av/getAttributeViewKeysByAvID", {avID}, async (response) => { 46 const columns: IAVColumn[] = response.data; 47 const cellElements: HTMLElement[] = Array.from(blockElement.querySelectorAll(".av__cell--active, .av__cell--select")) || []; 48 if (values && Array.isArray(values) && values.length > 0) { 49 if (cellElements.length === 0) { 50 blockElement.querySelectorAll(".av__row--select:not(.av__row--header)").forEach(rowElement => { 51 rowElement.querySelectorAll(".av__cell").forEach((cellElement: HTMLElement) => { 52 cellElements.push(cellElement); 53 }); 54 }); 55 } 56 if (cellElements.length === 0) { 57 cellElements.push(blockElement.querySelector(".av__row:not(.av__row--header) .av__cell")); 58 } 59 const doOperations: IOperation[] = []; 60 const undoOperations: IOperation[] = []; 61 62 const id = blockElement.dataset.nodeId; 63 let currentRowElement: Element; 64 const firstColIndex = cellElements[0].getAttribute("data-col-id"); 65 for (let i = 0; i < values.length; i++) { 66 if (!currentRowElement) { 67 currentRowElement = hasClosestByClassName(cellElements[0].parentElement, "av__row") as HTMLElement; 68 } else { 69 currentRowElement = currentRowElement.nextElementSibling; 70 } 71 if (!currentRowElement.classList.contains("av__row")) { 72 break; 73 } 74 let cellElement: HTMLElement; 75 for (let j = 0; j < values[i].length; j++) { 76 const cellValue = values[i][j]; 77 if (!cellElement) { 78 cellElement = currentRowElement.querySelector(`.av__cell[data-col-id="${firstColIndex}"]`) as HTMLElement; 79 } else { 80 if (cellElement.nextElementSibling) { 81 cellElement = cellElement.nextElementSibling as HTMLElement; 82 } else if (cellElement.parentElement.classList.contains("av__colsticky")) { 83 cellElement = cellElement.parentElement.nextElementSibling as HTMLElement; 84 } 85 } 86 if (!cellElement.classList.contains("av__cell")) { 87 break; 88 } 89 const operations = await updateCellsValue(protyle, blockElement as HTMLElement, 90 cellValue, [cellElement], columns, html, true); 91 if (operations.doOperations.length > 0) { 92 doOperations.push(...operations.doOperations); 93 undoOperations.push(...operations.undoOperations); 94 } 95 } 96 } 97 if (doOperations.length > 0) { 98 doOperations.push({ 99 action: "doUpdateUpdated", 100 id, 101 data: dayjs().format("YYYYMMDDHHmmss"), 102 }); 103 undoOperations.push({ 104 action: "doUpdateUpdated", 105 id, 106 data: blockElement.getAttribute("updated"), 107 }); 108 transaction(protyle, doOperations, undoOperations); 109 } 110 return; 111 } 112 113 const contenteditableElement = getContenteditableElement(tempElement.content.firstElementChild); 114 if (contenteditableElement && contenteditableElement.childNodes.length === 1 && contenteditableElement.firstElementChild?.getAttribute("data-type") === "block-ref") { 115 const selectCellElement = blockElement.querySelector(".av__cell--select") as HTMLElement; 116 if (selectCellElement) { 117 const sourceId = contenteditableElement.firstElementChild.getAttribute("data-id"); 118 const previousID = getFieldIdByCellElement(selectCellElement, blockElement.getAttribute("data-av-type") as TAVView); 119 transaction(protyle, [{ 120 action: "replaceAttrViewBlock", 121 avID, 122 previousID, 123 nextID: sourceId, 124 isDetached: false, 125 }], [{ 126 action: "replaceAttrViewBlock", 127 avID, 128 previousID: sourceId, 129 nextID: previousID, 130 isDetached: selectCellElement.dataset.detached === "true", 131 }]); 132 updateAttrViewCellAnimation(selectCellElement, { 133 type: "block", 134 isDetached: false, 135 block: {content: contenteditableElement.firstElementChild.textContent, id: sourceId} 136 }); 137 return; 138 } 139 } 140 141 const text = protyle.lute.BlockDOM2Content(html); 142 const rowsElement = blockElement.querySelectorAll(".av__row--select"); 143 144 const textJSON: string[][] = []; 145 text.split("\n").forEach(row => { 146 textJSON.push(row.split("\t")); 147 }); 148 if (rowsElement.length > 0 && textJSON.length === 1 && textJSON[0].length === 1) { 149 updateCellsValue(protyle, blockElement as HTMLElement, text, undefined, columns, html); 150 return; 151 } 152 if (rowsElement.length > 0) { 153 rowsElement.forEach(rowElement => { 154 rowElement.querySelectorAll(".av__cell").forEach((cellElement: HTMLElement) => { 155 cellElements.push(cellElement); 156 }); 157 }); 158 } 159 if (cellElements.length > 0) { 160 if (textJSON.length === 1 && textJSON[0].length === 1) { 161 updateCellsValue(protyle, blockElement as HTMLElement, text, cellElements, columns, html); 162 } else { 163 let currentRowElement: Element; 164 const doOperations: IOperation[] = []; 165 const undoOperations: IOperation[] = []; 166 const firstColIndex = cellElements[0].getAttribute("data-col-id"); 167 for (let i = 0; i < textJSON.length; i++) { 168 if (!currentRowElement) { 169 currentRowElement = hasClosestByClassName(cellElements[0].parentElement, "av__row") as HTMLElement; 170 } else { 171 currentRowElement = currentRowElement.nextElementSibling; 172 } 173 if (!currentRowElement.classList.contains("av__row")) { 174 break; 175 } 176 let cellElement: HTMLElement; 177 for (let j = 0; j < textJSON[i].length; j++) { 178 if (!cellElement) { 179 cellElement = currentRowElement.querySelector(`.av__cell[data-col-id="${firstColIndex}"]`) as HTMLElement; 180 } else { 181 if (cellElement.nextElementSibling) { 182 cellElement = cellElement.nextElementSibling as HTMLElement; 183 } else if (cellElement.parentElement.classList.contains("av__colsticky")) { 184 cellElement = cellElement.parentElement.nextElementSibling as HTMLElement; 185 } 186 } 187 if (!cellElement.classList.contains("av__cell")) { 188 break; 189 } 190 const cellValue = textJSON[i][j]; 191 const operations = await updateCellsValue(protyle, blockElement as HTMLElement, cellValue, [cellElement], columns, html, true); 192 if (operations.doOperations.length > 0) { 193 doOperations.push(...operations.doOperations); 194 undoOperations.push(...operations.undoOperations); 195 } 196 } 197 } 198 if (doOperations.length > 0) { 199 const id = blockElement.getAttribute("data-node-id"); 200 doOperations.push({ 201 action: "doUpdateUpdated", 202 id, 203 data: dayjs().format("YYYYMMDDHHmmss"), 204 }); 205 undoOperations.push({ 206 action: "doUpdateUpdated", 207 id, 208 data: blockElement.getAttribute("updated"), 209 }); 210 transaction(protyle, doOperations, undoOperations); 211 } 212 } 213 document.querySelector(".av__panel")?.remove(); 214 } else if (hasClosestByClassName(range.startContainer, "av__title")) { 215 const node = document.createTextNode(text); 216 range.insertNode(node); 217 range.setEnd(node, text.length); 218 range.collapse(false); 219 focusByRange(range); 220 updateAVName(protyle, blockElement); 221 } 222 }); 223}; 224 225const processTable = (range: Range, html: string, protyle: IProtyle, blockElement: HTMLElement) => { 226 const tempElement = document.createElement("template"); 227 tempElement.innerHTML = html; 228 const copyCellElements = tempElement.content.querySelectorAll("th, td"); 229 if (copyCellElements.length === 0) { 230 return false; 231 } 232 const scrollLeft = blockElement.firstElementChild.scrollLeft; 233 const scrollTop = blockElement.querySelector("table").scrollTop; 234 const tableSelectElement = blockElement.querySelector(".table__select") as HTMLElement; 235 let index = 0; 236 const matchCellsElement: HTMLTableCellElement[] = []; 237 blockElement.querySelectorAll("th, td").forEach((item: HTMLTableCellElement) => { 238 if (!item.classList.contains("fn__none") && copyCellElements.length > index && 239 isIncludeCell({ 240 tableSelectElement, 241 scrollLeft, 242 scrollTop, 243 item, 244 })) { 245 matchCellsElement.push(item); 246 index++; 247 } 248 }); 249 tableSelectElement.removeAttribute("style"); 250 const oldHTML = blockElement.outerHTML; 251 blockElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss")); 252 matchCellsElement.forEach((item, matchIndex) => { 253 item.innerHTML = copyCellElements[matchIndex].innerHTML; 254 if (matchIndex === matchCellsElement.length - 1) { 255 setLastNodeRange(item, range, false); 256 } 257 }); 258 range.collapse(false); 259 updateTransaction(protyle, blockElement.getAttribute("data-node-id"), blockElement.outerHTML, oldHTML); 260 return true; 261}; 262 263export const insertHTML = (html: string, protyle: IProtyle, isBlock = false, 264 // 移动端插入嵌入块时,获取到的 range 为旧值 265 useProtyleRange = false, 266 // 在开头粘贴块则插入上方 267 insertByCursor = false) => { 268 if (html === "") { 269 return; 270 } 271 const range = useProtyleRange ? protyle.toolbar.range : getEditorRange(protyle.wysiwyg.element); 272 fixTableRange(range); 273 let unSpinHTML; 274 if (hasClosestByAttribute(range.startContainer, "data-type", "NodeTable") && !isBlock) { 275 if (hasClosestByTag(range.startContainer, "TABLE")) { 276 unSpinHTML = protyle.lute.BlockDOM2InlineBlockDOM(html); 277 } else { 278 // https://github.com/siyuan-note/siyuan/issues/9411 279 isBlock = true; 280 } 281 } 282 let blockElement = hasClosestBlock(range.startContainer) as HTMLElement; 283 if (!blockElement) { 284 // 使用鼠标点击选则模版提示列表后 range 丢失 285 if (protyle.toolbar.range) { 286 blockElement = hasClosestBlock(protyle.toolbar.range.startContainer) as HTMLElement; 287 } else { 288 blockElement = protyle.wysiwyg.element.firstElementChild as HTMLElement; 289 } 290 } 291 if (!blockElement) { 292 return; 293 } 294 295 if (blockElement.classList.contains("av")) { 296 range.deleteContents(); 297 processAV(range, html, protyle, blockElement as HTMLElement); 298 return; 299 } 300 if (blockElement.classList.contains("table") && blockElement.querySelector(".table__select").clientWidth > 0 && 301 processTable(range, html, protyle, blockElement)) { 302 return; 303 } 304 305 let id = blockElement.getAttribute("data-node-id"); 306 range.insertNode(document.createElement("wbr")); 307 let oldHTML = blockElement.outerHTML; 308 const type = blockElement.getAttribute("data-type"); 309 const isNodeCodeBlock = type === "NodeCodeBlock"; 310 const editableElement = getContenteditableElement(blockElement); 311 if (!isBlock && 312 (isNodeCodeBlock || protyle.toolbar.getCurrentType(range).includes("code"))) { 313 range.deleteContents(); 314 // 代码块需保持至少一个 \n https://github.com/siyuan-note/siyuan/pull/13271#issuecomment-2502672155 315 let codeBlockIsEmpty = false; 316 if (isNodeCodeBlock && editableElement.textContent === "") { 317 codeBlockIsEmpty = true; 318 } 319 range.insertNode(document.createTextNode(html.replace(/\r\n|\r|\u2028|\u2029/g, "\n"))); 320 range.collapse(false); 321 range.insertNode(document.createElement("wbr")); 322 if (codeBlockIsEmpty) { 323 // 代码块为空添加的 \n 需放在最后 https://github.com/siyuan-note/siyuan/issues/15399 324 range.collapse(false); 325 range.insertNode(document.createTextNode("\n")); 326 } 327 if (isNodeCodeBlock) { 328 blockElement.querySelector('[data-render="true"]')?.removeAttribute("data-render"); 329 highlightRender(blockElement); 330 } else { 331 focusByWbr(blockElement, range); 332 } 333 blockElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss")); 334 updateTransaction(protyle, id, blockElement.outerHTML, oldHTML); 335 setTimeout(() => { 336 scrollCenter(protyle, blockElement, false, "smooth"); 337 }, Constants.TIMEOUT_LOAD); 338 return; 339 } 340 341 const undoOperation: IOperation[] = []; 342 const doOperation: IOperation[] = []; 343 if (range.toString() !== "") { 344 const inlineMathElement = hasClosestByAttribute(range.commonAncestorContainer, "data-type", "inline-math"); 345 if (inlineMathElement) { 346 // 表格内选中数学公式 https://ld246.com/article/1631708573504 347 inlineMathElement.remove(); 348 } else if (range.startContainer.nodeType === 3 && range.startContainer.parentElement.getAttribute("data-type")?.indexOf("block-ref") > -1) { 349 // 选中 ref**bbb** 后 alt+[ 350 range.deleteContents(); 351 // https://github.com/siyuan-note/siyuan/issues/14035 352 if (range.startContainer.nodeType !== 3 && range.startContainer.textContent === "") { 353 // ref 选中处理 https://ld246.com/article/1629214377537 354 (range.startContainer as HTMLElement).remove(); 355 } 356 } else { 357 range.deleteContents(); 358 } 359 range.insertNode(document.createElement("wbr")); 360 undoOperation.push({ 361 action: "update", 362 id, 363 data: oldHTML 364 }); 365 doOperation.push({ 366 action: "update", 367 id, 368 data: blockElement.outerHTML 369 }); 370 } 371 const tempElement = document.createElement("template"); 372 373 // https://github.com/siyuan-note/siyuan/issues/14162 & https://github.com/siyuan-note/siyuan/issues/14965 374 if (/^\s*&gt;|\*|-|\+|\d*.|\[ \]|[x]/.test(html) && 375 editableElement.textContent.replace(Constants.ZWSP, "") !== "") { 376 unSpinHTML = html; 377 } 378 379 let innerHTML = unSpinHTML || // 在 table 中插入需要使用转换好的行内元素 https://github.com/siyuan-note/siyuan/issues/9358 380 html; // 空格会被 Spin 不再,需要使用原文 381 // 粘贴纯文本时会进行内部转义,这里需要进行反转义 https://github.com/siyuan-note/siyuan/issues/10620 382 innerHTML = innerHTML.replace(/;;;lt;;;/g, "&lt;").replace(/;;;gt;;;/g, "&gt;"); 383 tempElement.innerHTML = innerHTML; 384 385 let block2text = false; 386 if (( 387 editableElement.textContent.replace(Constants.ZWSP, "") !== "" || 388 type === "NodeHeading" 389 ) && 390 tempElement.content.childElementCount === 1 && 391 tempElement.content.firstChild.nodeType !== 3 && 392 tempElement.content.firstElementChild.getAttribute("data-type") === "NodeHeading") { 393 // https://github.com/siyuan-note/siyuan/issues/14114 394 isBlock = false; 395 block2text = true; 396 } 397 // 使用 lute 方法会添加 p 元素,只有一个 p 元素或者只有一个字符串或者为 <u>b</u> 时的时候只拷贝内部 398 if (!isBlock) { 399 if (tempElement.content.firstChild.nodeType === 3 || block2text || 400 (tempElement.content.firstChild.nodeType !== 3 && 401 ((tempElement.content.firstElementChild.classList.contains("p") && tempElement.content.childElementCount === 1) || 402 tempElement.content.firstElementChild.tagName !== "DIV"))) { 403 if (tempElement.content.firstChild.nodeType !== 3 && tempElement.content.firstElementChild.classList.contains("p")) { 404 tempElement.innerHTML = tempElement.content.firstElementChild.firstElementChild.innerHTML.trim(); 405 } 406 // 粘贴带样式的行内元素到另一个行内元素中需进行切割 407 const spanElement = range.startContainer.nodeType === 3 ? range.startContainer.parentElement : range.startContainer as HTMLElement; 408 if (spanElement.tagName === "SPAN" && spanElement === (range.endContainer.nodeType === 3 ? range.endContainer.parentElement : range.endContainer) && 409 // 粘贴纯文本不需切割 https://ld246.com/article/1665556907936 410 // emoji 图片需要切割 https://github.com/siyuan-note/siyuan/issues/9370 411 tempElement.content.querySelector("span, img") 412 ) { 413 const afterElement = document.createElement("span"); 414 const attributes = spanElement.attributes; 415 for (let i = 0; i < attributes.length; i++) { 416 afterElement.setAttribute(attributes[i].name, attributes[i].value); 417 } 418 range.setEnd(spanElement.lastChild, spanElement.lastChild.textContent.length); 419 afterElement.append(range.extractContents()); 420 spanElement.after(afterElement); 421 range.setStartBefore(afterElement); 422 range.collapse(true); 423 } 424 range.insertNode(tempElement.content.cloneNode(true)); 425 range.collapse(false); 426 blockElement.querySelector("wbr")?.remove(); 427 protyle.wysiwyg.lastHTMLs[id] = oldHTML; 428 input(protyle, blockElement as HTMLElement, range); 429 return; 430 } 431 } 432 const cursorLiElement = hasClosestByClassName(blockElement, "li"); 433 // 列表项不能单独进行粘贴 https://ld246.com/article/1628681120576/comment/1628681209731#comments 434 if (tempElement.content.children[0]?.getAttribute("data-type") === "NodeListItem") { 435 if (cursorLiElement) { 436 blockElement = cursorLiElement; 437 id = blockElement.getAttribute("data-node-id"); 438 oldHTML = blockElement.outerHTML; 439 } else { 440 const liItemElement = tempElement.content.children[0]; 441 const subType = liItemElement.getAttribute("data-subtype"); 442 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>`; 443 } 444 } 445 let lastElement: Element; 446 let insertBefore = false; 447 if (!range.toString() && insertByCursor) { 448 const positon = getSelectionOffset(blockElement, protyle.wysiwyg.element, range); 449 if (positon.start === 0 && editableElement.textContent !== "") { 450 insertBefore = true; 451 } 452 } 453 // https://github.com/siyuan-note/siyuan/issues/15768 454 if (tempElement.content.firstChild.nodeType === 3 || (tempElement.content.firstChild.nodeType === 1 && tempElement.content.firstElementChild.tagName !== "DIV")) { 455 tempElement.innerHTML = protyle.lute.SpinBlockDOM(tempElement.innerHTML); 456 } 457 (insertBefore ? Array.from(tempElement.content.children) : Array.from(tempElement.content.children).reverse()).find((item) => { 458 let addId = item.getAttribute("data-node-id"); 459 const hasParentHeading = item.getAttribute("parent-heading"); 460 if (addId === id) { 461 doOperation.push({ 462 action: "update", 463 data: item.outerHTML, 464 id: addId, 465 }); 466 undoOperation.push({ 467 action: "update", 468 id: addId, 469 data: oldHTML, 470 }); 471 } else { 472 if (item.classList.contains("li") && !blockElement.parentElement.classList.contains("list")) { 473 // https://github.com/siyuan-note/siyuan/issues/6534 474 addId = Lute.NewNodeID(); 475 const liElement = document.createElement("div"); 476 liElement.setAttribute("data-subtype", item.getAttribute("data-subtype")); 477 liElement.setAttribute("data-node-id", addId); 478 liElement.setAttribute("data-type", "NodeList"); 479 liElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss")); 480 liElement.classList.add("list"); 481 liElement.append(item); 482 item = liElement; 483 } 484 item.removeAttribute("parent-heading"); 485 doOperation.push({ 486 action: "insert", 487 data: item.outerHTML, 488 id: addId, 489 context: {ignoreProcess: hasParentHeading ? "true" : "false"}, 490 nextID: insertBefore ? id : undefined, 491 previousID: insertBefore ? undefined : id 492 }); 493 undoOperation.push({ 494 action: "delete", 495 id: addId, 496 }); 497 } 498 if (!hasParentHeading) { 499 const rendersElement = []; 500 if (item.classList.contains("render-node") && item.getAttribute("data-type") === "NodeCodeBlock") { 501 rendersElement.push(item); 502 } else { 503 rendersElement.push(...item.querySelectorAll('.render-node[data-type="NodeCodeBlock"]')); 504 } 505 rendersElement.forEach((renderItem) => { 506 renderItem.querySelector(".protyle-icons")?.remove(); 507 const spinElement = renderItem.querySelector('[spin="1"]'); 508 if (spinElement) { 509 spinElement.innerHTML = ""; 510 } 511 renderItem.removeAttribute("data-render"); 512 }); 513 processClonePHElement(item); 514 if (insertBefore) { 515 blockElement.before(item); 516 } else { 517 blockElement.after(item); 518 } 519 } 520 if (!lastElement) { 521 lastElement = item; 522 } 523 }); 524 if (editableElement && editableElement.textContent === "" && blockElement.classList.contains("p")) { 525 // 选中当前块所有内容粘贴再撤销会导致异常 https://ld246.com/article/1662542137636 526 doOperation.find((item, index) => { 527 if (item.id === id) { 528 doOperation.splice(index, 1); 529 return true; 530 } 531 }); 532 doOperation.push({ 533 action: "delete", 534 id 535 }); 536 // 选中当前块所有内容粘贴再撤销会导致异常 https://ld246.com/article/1662542137636 537 undoOperation.find((item, index) => { 538 if (item.id === id && item.action === "update") { 539 undoOperation.splice(index, 1); 540 return true; 541 } 542 }); 543 undoOperation.push({ 544 action: "insert", 545 data: oldHTML, 546 id, 547 previousID: blockElement.previousElementSibling ? blockElement.previousElementSibling.getAttribute("data-node-id") : "", 548 parentID: blockElement.parentElement.getAttribute("data-node-id") || protyle.block.parentID 549 }); 550 blockElement.remove(); 551 } 552 if (lastElement) { 553 // https://github.com/siyuan-note/siyuan/issues/5591 554 focusBlock(lastElement, undefined, false); 555 } 556 const wbrElement = protyle.wysiwyg.element.querySelector("wbr"); 557 if (wbrElement) { 558 wbrElement.remove(); 559 } 560 let foldData; 561 if (blockElement.getAttribute("data-type") === "NodeHeading" && 562 blockElement.getAttribute("fold") === "1") { 563 foldData = setFold(protyle, blockElement, true, false, false, true); 564 doOperation.reverse(); 565 foldData.doOperations[0].context = { 566 focusId: lastElement?.getAttribute("data-node-id"), 567 }; 568 doOperation.push(...foldData.doOperations); 569 undoOperation.push(...foldData.undoOperations); 570 } 571 transaction(protyle, doOperation, undoOperation); 572 // 复制容器块中包含折叠标题块 573 protyle.wysiwyg.element.querySelectorAll("[parent-heading]").forEach(item => { 574 item.remove(); 575 }); 576};