2024-06-05 22:50:53 +08:00
import { fetchPost , fetchSyncPost } from "../../util/fetch" ;
2022-10-12 08:31:15 +08:00
import { focusBlock , focusByWbr , focusSideBlock , getEditorRange } from "../util/selection" ;
2025-02-01 20:18:17 +08:00
import { getContenteditableElement , getFirstBlock , getTopAloneElement } from "./getBlock" ;
2022-05-26 15:18:53 +08:00
import { Constants } from "../../constants" ;
2023-06-07 10:36:20 +08:00
import { blockRender } from "../render/blockRender" ;
2022-05-26 15:18:53 +08:00
import { processRender } from "../util/processCode" ;
2023-06-07 10:36:20 +08:00
import { highlightRender } from "../render/highlightRender" ;
2025-04-02 17:51:44 +08:00
import { hasClosestBlock , hasClosestByAttribute , hasTopClosestByAttribute , isInEmbedBlock } from "../util/hasClosest" ;
2025-09-24 16:30:16 +08:00
import { setFold , zoomOut } from "../../menus/protyle" ;
2023-09-09 14:54:05 +08:00
import { disabledProtyle , enableProtyle , onGet } from "../util/onGet" ;
2022-06-29 20:25:30 +08:00
/// #if !MOBILE
2022-05-26 15:18:53 +08:00
import { getAllModels } from "../../layout/getAll" ;
2022-06-29 20:25:30 +08:00
/// #endif
2023-06-11 22:58:48 +08:00
import { avRender , refreshAV } from "../render/av/render" ;
2022-05-26 15:18:53 +08:00
import { removeFoldHeading } from "../util/heading" ;
import { genEmptyElement , genSBElement } from "../../block/util" ;
import { hideElements } from "../ui/hideElements" ;
2022-10-03 01:07:24 +08:00
import { reloadProtyle } from "../util/reload" ;
2022-10-05 10:12:21 +08:00
import { countBlockWord } from "../../layout/status" ;
2024-01-01 23:07:40 +08:00
import { isPaidUser , needSubscribe } from "../../util/needSubscribe" ;
2023-09-13 12:05:57 +08:00
import { resize } from "../util/resize" ;
2024-06-07 11:02:02 +08:00
import { processClonePHElement } from "../render/util" ;
2025-10-09 20:02:53 +08:00
import { scrollCenter } from "../../util/highlightById" ;
2022-05-26 15:18:53 +08:00
const removeTopElement = ( updateElement : Element , protyle : IProtyle ) = > {
// 移动到其他文档中,该块需移除
// TODO 文档没有打开时,需要通过后台获取 getTopAloneElement
const topAloneElement = getTopAloneElement ( updateElement ) ;
const doOperations : IOperation [ ] = [ ] ;
2025-07-23 12:21:59 +08:00
if ( topAloneElement !== updateElement ) {
2022-05-26 15:18:53 +08:00
updateElement . remove ( ) ;
doOperations . push ( {
action : "delete" ,
id : topAloneElement.getAttribute ( "data-node-id" )
} ) ;
}
topAloneElement . remove ( ) ;
2024-12-30 16:47:03 +08:00
if ( protyle . wysiwyg . element . childElementCount === 0 ) {
if ( protyle . block . rootID === protyle . block . id ) {
const newId = Lute . NewNodeID ( ) ;
const newElement = genEmptyElement ( false , false , newId ) ;
doOperations . push ( {
action : "insert" ,
data : newElement.outerHTML ,
id : newId ,
parentID : protyle.block.parentID
} ) ;
protyle . wysiwyg . element . innerHTML = newElement . outerHTML ;
} else {
zoomOut ( {
protyle ,
id : protyle.block.rootID ,
isPushBack : false ,
focusId : protyle.block.id ,
2024-12-31 08:58:53 +08:00
} ) ;
2024-12-30 16:47:03 +08:00
}
2022-05-26 15:18:53 +08:00
}
if ( doOperations . length > 0 ) {
transaction ( protyle , doOperations , [ ] ) ;
}
} ;
2024-05-10 23:27:04 +08:00
// 用于执行操作,外加处理当前编辑器中块引用、嵌入块的更新
2023-06-01 20:50:49 +08:00
const promiseTransaction = ( ) = > {
2023-06-09 21:39:49 +08:00
if ( window . siyuan . transactions . length === 0 ) {
return ;
}
2022-05-26 15:18:53 +08:00
const protyle = window . siyuan . transactions [ 0 ] . protyle ;
const doOperations = window . siyuan . transactions [ 0 ] . doOperations ;
const undoOperations = window . siyuan . transactions [ 0 ] . undoOperations ;
// 1. * ;2. * ;3. a
2023-06-09 22:01:36 +08:00
// 第一步请求没有返回前在 transaction 中会合并1、2步, 此时第一步请求返回将被以下代码删除, 在输入a时, 就会出现 block not found, 因此以下代码不能放入请求回调中
2022-05-26 15:18:53 +08:00
window . siyuan . transactions . splice ( 0 , 1 ) ;
fetchPost ( "/api/transactions" , {
session : protyle.id ,
app : Constants.SIYUAN_APPID ,
transactions : [ {
doOperations ,
undoOperations // 目前用于 ws 推送更新大纲
} ]
} , ( response ) = > {
2023-06-09 22:13:24 +08:00
if ( window . siyuan . transactions . length === 0 ) {
countBlockWord ( [ ] , protyle . block . rootID , true ) ;
} else {
2023-06-01 20:50:49 +08:00
promiseTransaction ( ) ;
2022-05-26 15:18:53 +08:00
}
2023-01-19 11:49:31 +08:00
/// #if MOBILE
2024-01-01 23:07:40 +08:00
if ( ( ( 0 !== window . siyuan . config . sync . provider && isPaidUser ( ) ) ||
2023-07-21 17:20:34 +08:00
( 0 === window . siyuan . config . sync . provider && ! needSubscribe ( "" ) ) ) &&
2023-01-19 11:49:31 +08:00
window . siyuan . config . repo . key && window . siyuan . config . sync . enabled ) {
2023-03-05 15:08:25 +08:00
document . getElementById ( "toolbarSync" ) . classList . remove ( "fn__none" ) ;
2023-01-19 11:49:31 +08:00
}
/// #endif
2022-10-03 16:22:18 +08:00
let range : Range ;
2022-10-03 01:07:24 +08:00
if ( getSelection ( ) . rangeCount > 0 ) {
range = getSelection ( ) . getRangeAt ( 0 ) ;
}
2022-11-22 00:20:34 +08:00
response . data [ 0 ] . doOperations . forEach ( ( operation : IOperation ) = > {
2022-11-21 23:36:49 +08:00
if ( operation . action === "unfoldHeading" || operation . action === "foldHeading" ) {
2024-06-24 10:52:15 +08:00
processFold ( operation , protyle ) ;
2022-11-21 23:36:49 +08:00
return ;
}
2022-05-26 15:18:53 +08:00
if ( operation . action === "update" ) {
2022-10-03 01:07:24 +08:00
if ( protyle . options . backlinkData ) {
// 反链中有多个相同块的情况
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) ) . forEach ( item = > {
2024-07-25 17:55:25 +08:00
if ( ! isInEmbedBlock ( item ) ) {
2025-07-23 13:08:38 +08:00
if ( range && ( item === range . startContainer || item . contains ( range . startContainer ) ) ) {
2022-10-03 01:07:24 +08:00
// 正在编辑的块不能进行更新
} else {
item . outerHTML = operation . data . replace ( "<wbr>" , "" ) ;
}
}
} ) ;
processRender ( protyle . wysiwyg . element ) ;
highlightRender ( protyle . wysiwyg . element ) ;
2023-09-09 23:21:46 +08:00
avRender ( protyle . wysiwyg . element , protyle ) ;
2022-10-03 01:07:24 +08:00
blockRender ( protyle , protyle . wysiwyg . element ) ;
}
2022-05-26 15:18:53 +08:00
// 当前编辑器中更新嵌入块
updateEmbed ( protyle , operation ) ;
return ;
}
if ( operation . action === "delete" || operation . action === "append" ) {
2022-10-03 01:07:24 +08:00
if ( protyle . options . backlinkData ) {
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) ) . forEach ( item = > {
2025-04-03 23:44:34 +08:00
if ( ! isInEmbedBlock ( item ) && ! item . contains ( range . startContainer ) ) {
2022-10-03 01:07:24 +08:00
item . remove ( ) ;
}
} ) ;
}
2022-05-26 15:18:53 +08:00
// 更新嵌入块
protyle . wysiwyg . element . querySelectorAll ( '[data-type="NodeBlockQueryEmbed"]' ) . forEach ( ( item ) = > {
if ( item . querySelector ( ` [data-node-id=" ${ operation . id } "] ` ) ) {
item . removeAttribute ( "data-render" ) ;
blockRender ( protyle , item ) ;
}
} ) ;
2024-04-14 11:11:18 +08:00
hideElements ( [ "gutter" ] , protyle ) ;
2022-05-26 15:18:53 +08:00
return ;
}
if ( operation . action === "move" ) {
2022-10-03 01:07:24 +08:00
if ( protyle . options . backlinkData ) {
2022-10-03 16:22:18 +08:00
const updateElements : Element [ ] = [ ] ;
2022-10-03 01:07:24 +08:00
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) ) . forEach ( item = > {
2025-04-03 01:52:29 +08:00
if ( ! isInEmbedBlock ( item ) ) {
const topElement = hasTopClosestByAttribute ( item , "data-node-id" , null ) ;
if ( topElement && ! topElement . contains ( range . startContainer ) ) {
// 当前操作块不再进行操作,否则光标丢失 https://github.com/siyuan-note/siyuan/issues/13946
updateElements . push ( item ) ;
}
2022-10-03 01:07:24 +08:00
}
} ) ;
2022-10-03 16:22:18 +08:00
let hasFind = false ;
2022-10-03 01:07:24 +08:00
if ( operation . previousID && updateElements . length > 0 ) {
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . previousID } "] ` ) ) . forEach ( item = > {
2025-02-01 20:18:17 +08:00
if ( ! isInEmbedBlock ( item ) && ! item . nextElementSibling . contains ( range . startContainer ) ) {
2024-06-07 11:02:02 +08:00
item . after ( processClonePHElement ( updateElements [ 0 ] . cloneNode ( true ) as Element ) ) ;
2022-10-03 01:07:24 +08:00
hasFind = true ;
}
} ) ;
} else if ( updateElements . length > 0 ) {
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . parentID } "] ` ) ) . forEach ( item = > {
2025-02-01 20:18:17 +08:00
if ( ! isInEmbedBlock ( item ) && ! getFirstBlock ( item ) . contains ( range . startContainer ) ) {
2025-12-01 19:30:33 +08:00
const cloneElement = processClonePHElement ( updateElements [ 0 ] . cloneNode ( true ) as Element ) ;
2022-10-03 01:07:24 +08:00
// 列表特殊处理
if ( item . firstElementChild ? . classList . contains ( "protyle-action" ) ) {
2025-12-01 19:30:33 +08:00
item . firstElementChild . after ( cloneElement ) ;
} else if ( item . classList . contains ( "callout" ) ) {
item . querySelector ( ".callout-content" ) . prepend ( cloneElement ) ;
2022-10-03 01:07:24 +08:00
} else {
2025-12-01 19:30:33 +08:00
item . prepend ( cloneElement ) ;
2022-10-03 01:07:24 +08:00
}
hasFind = true ;
}
} ) ;
}
updateElements . forEach ( item = > {
if ( hasFind ) {
item . remove ( ) ;
} else if ( ! hasFind && item . parentElement ) {
removeTopElement ( item , protyle ) ;
}
} ) ;
}
2022-05-26 15:18:53 +08:00
// 更新嵌入块
protyle . wysiwyg . element . querySelectorAll ( '[data-type="NodeBlockQueryEmbed"]' ) . forEach ( ( item ) = > {
2025-03-16 21:54:25 +08:00
if ( item . querySelector ( ` [data-node-id=" ${ operation . id } "],[data-node-id=" ${ operation . parentID } "],[data-node-id=" ${ operation . previousID } "] ` ) ) {
2022-05-26 15:18:53 +08:00
item . removeAttribute ( "data-render" ) ;
blockRender ( protyle , item ) ;
}
} ) ;
return ;
}
2022-10-03 01:07:24 +08:00
if ( operation . action === "insert" ) {
// insert
2022-10-03 11:28:22 +08:00
if ( protyle . options . backlinkData ) {
2022-10-03 16:22:18 +08:00
const cursorElements : Element [ ] = [ ] ;
2022-10-03 11:28:22 +08:00
if ( operation . previousID ) {
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . previousID } "] ` ) ) . forEach ( item = > {
if ( item . nextElementSibling ? . getAttribute ( "data-node-id" ) !== operation . id &&
2025-04-02 17:51:44 +08:00
! item . contains ( range . startContainer ) && // 当前操作块不再进行操作
2022-10-04 11:43:45 +08:00
! hasClosestByAttribute ( item , "data-node-id" , operation . id ) && // 段落转列表会在段落后插入新列表
2024-07-25 17:55:25 +08:00
! isInEmbedBlock ( item ) ) {
2022-10-03 11:28:22 +08:00
item . insertAdjacentHTML ( "afterend" , operation . data ) ;
2022-10-03 16:22:18 +08:00
cursorElements . push ( item . nextElementSibling ) ;
2022-10-03 11:28:22 +08:00
}
} ) ;
} else {
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . parentID } "] ` ) ) . forEach ( item = > {
2025-04-03 23:44:34 +08:00
if ( ! isInEmbedBlock ( item ) && ! item . contains ( range . startContainer ) ) {
2022-10-03 11:28:22 +08:00
// 列表特殊处理
if ( item . firstElementChild && item . firstElementChild . classList . contains ( "protyle-action" ) &&
2025-12-01 19:30:33 +08:00
item . firstElementChild . nextElementSibling ? . getAttribute ( "data-node-id" ) !== operation . id ) {
2022-10-03 11:28:22 +08:00
item . firstElementChild . insertAdjacentHTML ( "afterend" , operation . data ) ;
2022-10-03 16:22:18 +08:00
cursorElements . push ( item . firstElementChild . nextElementSibling ) ;
2025-12-01 19:30:33 +08:00
} else if ( item . classList . contains ( "callout" ) &&
item . querySelector ( '[data-node-id]' ) ? . getAttribute ( "data-node-id" ) !== operation . id ) {
item . querySelector ( ".callout-content" ) . insertAdjacentHTML ( "afterbegin" , operation . data ) ;
cursorElements . push ( item . querySelector ( '[data-node-id]' ) ) ;
} else if ( item . firstElementChild . getAttribute ( "data-node-id" ) !== operation . id ) {
2022-10-03 11:28:22 +08:00
item . insertAdjacentHTML ( "afterbegin" , operation . data ) ;
2022-10-03 16:22:18 +08:00
cursorElements . push ( item . firstElementChild ) ;
2022-10-03 11:28:22 +08:00
}
}
} ) ;
}
// https://github.com/siyuan-note/siyuan/issues/4420
protyle . wysiwyg . element . querySelectorAll ( '[data-type="NodeHeading"]' ) . forEach ( item = > {
if ( item . lastElementChild . getAttribute ( "spin" ) === "1" ) {
item . lastElementChild . remove ( ) ;
}
} ) ;
cursorElements . forEach ( item = > {
processRender ( item ) ;
highlightRender ( item ) ;
2023-09-09 23:21:46 +08:00
avRender ( item , protyle ) ;
2022-10-03 11:28:22 +08:00
blockRender ( protyle , item ) ;
const wbrElement = item . querySelector ( "wbr" ) ;
if ( wbrElement ) {
wbrElement . remove ( ) ;
}
} ) ;
}
2022-10-03 01:07:24 +08:00
// 不更新嵌入块:在快速删除时重新渲染嵌入块会导致滚动条产生滚动从而触发 getDoc 请求,此时删除的块还没有写库,会把已删除的块 append 到文档底部,最终导致查询块失败、光标丢失
// protyle.wysiwyg.element.querySelectorAll('[data-type="NodeBlockQueryEmbed"]').forEach((item) => {
// if (item.getAttribute("data-node-id") === operation.id) {
// item.removeAttribute("data-render");
// blockRender(protyle, item);
// }
// });
2025-10-10 13:26:01 +08:00
protyle . wysiwyg . element . querySelectorAll ( "[parent-heading]" ) . forEach ( item = > {
item . remove ( ) ;
} ) ;
2022-10-03 01:07:24 +08:00
}
2022-05-26 15:18:53 +08:00
} ) ;
2024-05-11 10:50:22 +08:00
// 删除仅有的折叠标题后展开内容为空
2024-09-02 00:29:28 +08:00
if ( protyle . wysiwyg . element . childElementCount === 0 &&
// 聚焦时不需要新增块,否则会导致 https://github.com/siyuan-note/siyuan/issues/12326 第一点
! protyle . block . showAll ) {
2024-05-11 10:50:22 +08:00
const newID = Lute . NewNodeID ( ) ;
const emptyElement = genEmptyElement ( false , true , newID ) ;
protyle . wysiwyg . element . insertAdjacentElement ( "afterbegin" , emptyElement ) ;
transaction ( protyle , [ {
action : "insert" ,
data : emptyElement.outerHTML ,
id : newID ,
parentID : protyle.block.parentID
} ] ) ;
// 不能撤销,否则就无限循环了
focusByWbr ( emptyElement , range ) ;
}
2022-05-26 15:18:53 +08:00
} ) ;
} ;
const updateEmbed = ( protyle : IProtyle , operation : IOperation ) = > {
let updatedEmbed = false ;
2025-04-02 22:30:46 +08:00
const updateHTML = ( item : Element , html : string ) = > {
2025-03-17 19:17:52 +08:00
const tempElement = document . createElement ( "template" ) ;
2025-05-16 09:59:34 +08:00
tempElement . innerHTML = protyle . lute . SpinBlockDOM ( html ) ;
2025-04-03 00:45:44 +08:00
tempElement . content . querySelectorAll ( '[contenteditable="true"]' ) . forEach ( editItem = > {
2022-05-26 15:18:53 +08:00
editItem . setAttribute ( "contenteditable" , "false" ) ;
} ) ;
2025-04-03 00:45:44 +08:00
tempElement . content . querySelectorAll ( ".protyle-wysiwyg--select" ) . forEach ( selectItem = > {
2025-04-01 12:00:36 +08:00
selectItem . classList . remove ( "protyle-wysiwyg--select" ) ;
2025-04-03 15:13:01 +08:00
} ) ;
2025-05-26 16:45:41 +08:00
const wbrElement = tempElement . content . querySelector ( "wbr" ) ;
2022-05-26 15:18:53 +08:00
if ( wbrElement ) {
wbrElement . remove ( ) ;
}
item . outerHTML = tempElement . innerHTML ;
updatedEmbed = true ;
2025-04-03 15:13:01 +08:00
} ;
2025-04-02 22:30:46 +08:00
const allTempElement = document . createElement ( "template" ) ;
allTempElement . innerHTML = operation . data ;
protyle . wysiwyg . element . querySelectorAll ( '[data-type="NodeBlockQueryEmbed"]' ) . forEach ( ( item ) = > {
const matchElement = item . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) ;
if ( matchElement . length > 0 ) {
matchElement . forEach ( embedItem = > {
updateHTML ( embedItem , operation . data ) ;
2025-04-03 15:13:01 +08:00
} ) ;
2025-04-02 22:30:46 +08:00
} else {
item . querySelectorAll ( ".protyle-wysiwyg__embed" ) . forEach ( embedBlockItem = > {
const newTempElement = allTempElement . content . querySelector ( ` [data-node-id=" ${ embedBlockItem . getAttribute ( "data-id" ) } "] ` ) ;
2025-06-27 11:42:19 +08:00
if ( newTempElement && ! isInEmbedBlock ( newTempElement ) ) {
2025-04-02 22:30:46 +08:00
updateHTML ( embedBlockItem . querySelector ( "[data-node-id]" ) , newTempElement . outerHTML ) ;
}
} ) ;
}
2022-05-26 15:18:53 +08:00
} ) ;
if ( updatedEmbed ) {
processRender ( protyle . wysiwyg . element ) ;
highlightRender ( protyle . wysiwyg . element ) ;
2023-09-09 23:21:46 +08:00
avRender ( protyle . wysiwyg . element , protyle ) ;
2022-05-26 15:18:53 +08:00
}
2022-05-29 10:06:02 +08:00
} ;
2022-05-26 15:18:53 +08:00
2023-09-01 19:52:55 +08:00
const deleteBlock = ( updateElements : Element [ ] , id : string , protyle : IProtyle , isUndo : boolean ) = > {
if ( isUndo ) {
2023-08-25 15:47:47 +08:00
focusSideBlock ( updateElements [ 0 ] ) ;
}
updateElements . forEach ( item = > {
2024-12-30 09:26:39 +08:00
if ( isUndo ) {
// https://github.com/siyuan-note/siyuan/issues/13617
item . remove ( ) ;
} else {
// 需移除顶层,否则删除唯一的列表项后列表无法清除干净 https://github.com/siyuan-note/siyuan/issues/12326 第一点
const topElement = getTopAloneElement ( item ) ;
if ( topElement ) {
topElement . remove ( ) ;
}
2024-09-02 00:29:28 +08:00
}
2023-08-25 15:47:47 +08:00
} ) ;
// 更新 ws 嵌入块
protyle . wysiwyg . element . querySelectorAll ( '[data-type="NodeBlockQueryEmbed"]' ) . forEach ( ( item ) = > {
if ( item . querySelector ( ` [data-node-id=" ${ id } "] ` ) ) {
item . removeAttribute ( "data-render" ) ;
blockRender ( protyle , item ) ;
}
} ) ;
2023-08-28 23:26:47 +08:00
} ;
2023-08-25 15:47:47 +08:00
2023-09-01 19:52:55 +08:00
const updateBlock = ( updateElements : Element [ ] , protyle : IProtyle , operation : IOperation , isUndo : boolean ) = > {
2023-08-25 15:47:47 +08:00
updateElements . forEach ( item = > {
2023-12-21 23:46:08 +08:00
// 图标撤销后无法渲染
if ( item . getAttribute ( "data-subtype" ) === "echarts" ) {
item . outerHTML = protyle . lute . SpinBlockDOM ( operation . data ) ;
} else {
item . outerHTML = operation . data ;
}
2023-08-25 15:47:47 +08:00
} ) ;
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) ) . find ( item = > {
2024-07-25 17:55:25 +08:00
if ( ! isInEmbedBlock ( item ) ) {
2023-08-25 15:47:47 +08:00
updateElements [ 0 ] = item ;
return true ;
}
} ) ;
const wbrElement = updateElements [ 0 ] . querySelector ( "wbr" ) ;
2023-09-01 19:52:55 +08:00
if ( isUndo ) {
2023-08-25 15:47:47 +08:00
const range = getEditorRange ( updateElements [ 0 ] ) ;
if ( wbrElement ) {
focusByWbr ( updateElements [ 0 ] , range ) ;
} else {
focusBlock ( updateElements [ 0 ] ) ;
}
} else if ( wbrElement ) {
wbrElement . remove ( ) ;
}
processRender ( updateElements . length === 1 ? updateElements [ 0 ] : protyle . wysiwyg . element ) ;
highlightRender ( updateElements . length === 1 ? updateElements [ 0 ] : protyle . wysiwyg . element ) ;
2023-09-09 23:21:46 +08:00
avRender ( updateElements . length === 1 ? updateElements [ 0 ] : protyle . wysiwyg . element , protyle ) ;
2023-08-25 15:47:47 +08:00
blockRender ( protyle , updateElements . length === 1 ? updateElements [ 0 ] : protyle . wysiwyg . element ) ;
// 更新 ws 嵌入块
updateEmbed ( protyle , operation ) ;
2023-08-28 23:26:47 +08:00
} ;
2023-08-25 15:47:47 +08:00
2022-05-26 15:18:53 +08:00
// 用于推送和撤销
2023-09-01 19:52:55 +08:00
export const onTransaction = ( protyle : IProtyle , operation : IOperation , isUndo : boolean ) = > {
2022-10-03 16:22:18 +08:00
const updateElements : Element [ ] = [ ] ;
2022-10-03 01:07:24 +08:00
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) ) . forEach ( item = > {
2024-07-25 17:55:25 +08:00
if ( ! isInEmbedBlock ( item ) ) {
2022-10-03 16:22:18 +08:00
updateElements . push ( item ) ;
2022-05-26 15:18:53 +08:00
}
} ) ;
if ( operation . action === "setAttrs" ) {
protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) . forEach ( item = > {
if ( JSON . parse ( operation . data ) . fold === "1" ) {
item . setAttribute ( "fold" , "1" ) ;
} else {
item . removeAttribute ( "fold" ) ;
}
} ) ;
return ;
}
if ( operation . action === "unfoldHeading" ) {
const scrollTop = protyle . contentElement . scrollTop ;
protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) . forEach ( item = > {
2024-10-09 00:16:03 +08:00
item . removeAttribute ( "fold" ) ;
// undo 会走 transaction
if ( isUndo ) {
return ;
}
2024-10-12 12:34:59 +08:00
const embedElement = isInEmbedBlock ( item ) ;
2024-10-09 00:16:03 +08:00
if ( embedElement ) {
embedElement . removeAttribute ( "data-render" ) ;
blockRender ( protyle , embedElement ) ;
2024-10-08 22:46:51 +08:00
return ;
}
2022-05-26 15:18:53 +08:00
if ( operation . retData ) { // undo 的时候没有 retData
2022-09-02 19:38:54 +08:00
removeUnfoldRepeatBlock ( operation . retData , protyle ) ;
2022-05-26 15:18:53 +08:00
item . insertAdjacentHTML ( "afterend" , operation . retData ) ;
}
if ( operation . data === "remove" ) {
item . remove ( ) ;
}
} ) ;
if ( operation . retData ) {
2024-04-09 18:05:02 +08:00
if ( protyle . disabled ) {
disabledProtyle ( protyle ) ;
}
2022-05-26 15:18:53 +08:00
processRender ( protyle . wysiwyg . element ) ;
highlightRender ( protyle . wysiwyg . element ) ;
2023-09-09 23:21:46 +08:00
avRender ( protyle . wysiwyg . element , protyle ) ;
2022-05-26 15:18:53 +08:00
blockRender ( protyle , protyle . wysiwyg . element ) ;
protyle . contentElement . scrollTop = scrollTop ;
protyle . scroll . lastScrollTop = scrollTop ;
}
return ;
}
if ( operation . action === "foldHeading" ) {
protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) . forEach ( item = > {
item . setAttribute ( "fold" , "1" ) ;
if ( ! operation . retData ) {
removeFoldHeading ( item ) ;
}
} ) ;
2024-10-09 00:16:03 +08:00
// undo 会走 transaction
if ( isUndo ) {
return ;
}
2022-05-26 15:18:53 +08:00
if ( operation . retData ) {
operation . retData . forEach ( ( item : string ) = > {
2024-11-17 17:54:26 +08:00
let embedElement : HTMLElement | false ;
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ item } "] ` ) ) . find ( itemElement = > {
2024-11-19 00:30:22 +08:00
embedElement = isInEmbedBlock ( itemElement ) ;
2024-11-17 17:54:26 +08:00
if ( embedElement ) {
return true ;
}
itemElement . remove ( ) ;
2022-05-26 15:18:53 +08:00
} ) ;
2024-11-17 17:54:26 +08:00
// 折叠嵌入块的父级
if ( embedElement ) {
embedElement . removeAttribute ( "data-render" ) ;
blockRender ( protyle , embedElement ) ;
}
2022-05-26 15:18:53 +08:00
} ) ;
2024-09-09 23:40:32 +08:00
if ( protyle . wysiwyg . element . childElementCount === 0 ) {
zoomOut ( {
protyle ,
id : protyle.block.rootID ,
isPushBack : false ,
focusId : operation.id ,
} ) ;
}
2022-05-26 15:18:53 +08:00
}
return ;
}
if ( operation . action === "delete" ) {
2024-11-19 17:25:33 +08:00
if ( updateElements . length > 0 || ! isUndo ) {
2023-09-01 19:52:55 +08:00
deleteBlock ( updateElements , operation . id , protyle , isUndo ) ;
2023-09-08 21:59:18 +08:00
} else if ( isUndo ) {
2023-08-25 15:47:47 +08:00
zoomOut ( {
protyle ,
id : protyle.block.rootID ,
isPushBack : false ,
focusId : operation.id ,
callback() {
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) ) . forEach ( item = > {
2024-07-25 17:55:25 +08:00
if ( ! isInEmbedBlock ( item ) ) {
2023-08-25 15:47:47 +08:00
updateElements . push ( item ) ;
}
} ) ;
2023-09-01 19:52:55 +08:00
deleteBlock ( updateElements , operation . id , protyle , isUndo ) ;
2023-08-25 15:47:47 +08:00
}
2022-10-03 01:07:24 +08:00
} ) ;
2022-05-26 15:18:53 +08:00
}
return ;
}
if ( operation . action === "update" ) {
2025-03-13 11:58:11 +08:00
// 缩放后仅更新局部 https://github.com/siyuan-note/siyuan/issues/14326
if ( updateElements . length === 0 ) {
2025-03-18 11:57:08 +08:00
const newUpdateElement = protyle . wysiwyg . element . querySelector ( "[data-node-id]" ) ;
2025-04-27 17:26:26 +08:00
if ( newUpdateElement ) {
const newUpdateId = newUpdateElement . getAttribute ( "data-node-id" ) ;
const tempElement = document . createElement ( "template" ) ;
tempElement . innerHTML = operation . data ;
const newTempElement = tempElement . content . querySelector ( ` [data-node-id=" ${ newUpdateId } "] ` ) ;
if ( newTempElement ) {
updateElements . push ( newUpdateElement ) ;
operation . data = newTempElement . outerHTML ;
operation . id = newUpdateId ;
// https://github.com/siyuan-note/siyuan/issues/14326#issuecomment-2746140335
for ( let i = 1 ; i < protyle . wysiwyg . element . childElementCount ; i ++ ) {
protyle . wysiwyg . element . childNodes [ i ] . remove ( ) ;
i -- ;
}
2025-03-24 09:18:55 +08:00
}
2025-03-13 11:58:11 +08:00
}
}
2022-10-03 01:07:24 +08:00
if ( updateElements . length > 0 ) {
2023-09-01 19:52:55 +08:00
updateBlock ( updateElements , protyle , operation , isUndo ) ;
} else if ( isUndo ) {
2023-08-25 15:47:47 +08:00
zoomOut ( {
protyle ,
id : protyle.block.rootID ,
isPushBack : false ,
focusId : operation.id ,
callback() {
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) ) . forEach ( item = > {
2024-07-25 17:55:25 +08:00
if ( ! isInEmbedBlock ( item ) ) {
2023-08-25 15:47:47 +08:00
updateElements . push ( item ) ;
}
} ) ;
2023-09-01 19:52:55 +08:00
updateBlock ( updateElements , protyle , operation , isUndo ) ;
2022-05-26 15:18:53 +08:00
}
} ) ;
2023-12-05 20:06:56 +08:00
} else { // updateElements 没有包含嵌入块,在悬浮层编辑嵌入块时,嵌入块也需要更新
2023-12-05 20:06:36 +08:00
// 更新 ws 嵌入块
updateEmbed ( protyle , operation ) ;
2022-05-26 15:18:53 +08:00
}
return ;
}
2022-09-09 19:46:54 +08:00
if ( operation . action === "updateAttrs" ) { // 调用接口才推送
2022-09-17 11:28:09 +08:00
const data = operation . data as any ;
const attrsResult : IObject = { } ;
2023-03-16 11:19:46 +08:00
let bookmarkHTML = "" ;
let nameHTML = "" ;
let aliasHTML = "" ;
let memoHTML = "" ;
2023-12-18 21:26:03 +08:00
let avHTML = "" ;
2022-09-17 11:28:09 +08:00
Object . keys ( data . new ) . forEach ( key = > {
attrsResult [ key ] = data . new [ key ] ;
2023-01-27 20:03:40 +08:00
const escapeHTML = Lute . EscapeHTMLStr ( data . new [ key ] ) ;
2022-09-17 11:28:09 +08:00
if ( key === "bookmark" ) {
2023-03-14 22:53:19 +08:00
bookmarkHTML = ` <div class="protyle-attr--bookmark"> ${ escapeHTML } </div> ` ;
2022-09-17 11:28:09 +08:00
} else if ( key === "name" ) {
2023-03-14 22:53:19 +08:00
nameHTML = ` <div class="protyle-attr--name"><svg><use xlink:href="#iconN"></use></svg> ${ escapeHTML } </div> ` ;
2022-09-17 11:28:09 +08:00
} else if ( key === "alias" ) {
2023-03-14 22:53:19 +08:00
aliasHTML = ` <div class="protyle-attr--alias"><svg><use xlink:href="#iconA"></use></svg> ${ escapeHTML } </div> ` ;
2022-09-17 11:28:09 +08:00
} else if ( key === "memo" ) {
2025-10-08 23:39:14 +08:00
memoHTML = ` <div class="protyle-attr--memo ariaLabel" aria-label=" ${ escapeHTML } " data-position="north"><svg><use xlink:href="#iconM"></use></svg></div> ` ;
2024-03-08 23:39:18 +08:00
} else if ( key === "custom-avs" && data . new [ "av-names" ] ) {
avHTML = ` <div class="protyle-attr--av"><svg><use xlink:href="#iconDatabase"></use></svg> ${ data . new [ "av-names" ] } </div> ` ;
2022-09-17 11:28:09 +08:00
}
} ) ;
2023-12-18 21:26:03 +08:00
let nodeAttrHTML = bookmarkHTML + nameHTML + aliasHTML + memoHTML + avHTML ;
2023-03-30 19:48:36 +08:00
if ( protyle . block . rootID === operation . id ) {
2022-09-17 11:28:09 +08:00
// 文档
2023-03-30 19:48:36 +08:00
if ( protyle . title ) {
2024-03-08 23:39:18 +08:00
if ( data . new [ "custom-avs" ] && ! data . new [ "av-names" ] ) {
nodeAttrHTML += protyle . title . element . querySelector ( ".protyle-attr--av" ) ? . outerHTML || "" ;
}
2023-03-30 19:48:36 +08:00
const refElement = protyle . title . element . querySelector ( ".protyle-attr--refcount" ) ;
if ( refElement ) {
nodeAttrHTML += refElement . outerHTML ;
}
2023-09-09 14:54:05 +08:00
if ( data . new [ Constants . CUSTOM_RIFF_DECKS ] && data . new [ Constants . CUSTOM_RIFF_DECKS ] !== data . old [ Constants . CUSTOM_RIFF_DECKS ] ) {
2023-05-02 23:38:38 +08:00
protyle . title . element . style . animation = "addCard 450ms linear" ;
2023-09-09 14:54:05 +08:00
protyle . title . element . setAttribute ( Constants . CUSTOM_RIFF_DECKS , data . new [ Constants . CUSTOM_RIFF_DECKS ] ) ;
2023-05-02 23:38:38 +08:00
setTimeout ( ( ) = > {
protyle . title . element . style . animation = "" ;
2023-05-05 14:04:43 +08:00
} , 450 ) ;
2023-09-09 14:54:05 +08:00
} else if ( ! data . new [ Constants . CUSTOM_RIFF_DECKS ] ) {
protyle . title . element . removeAttribute ( Constants . CUSTOM_RIFF_DECKS ) ;
2023-03-30 19:48:36 +08:00
}
protyle . title . element . querySelector ( ".protyle-attr" ) . innerHTML = nodeAttrHTML ;
2023-02-23 18:33:04 +08:00
}
2022-09-17 11:28:09 +08:00
protyle . wysiwyg . renderCustom ( attrsResult ) ;
2023-09-09 14:54:05 +08:00
if ( data . new [ Constants . CUSTOM_SY_FULLWIDTH ] !== data . old [ Constants . CUSTOM_SY_FULLWIDTH ] ) {
2023-09-13 12:05:57 +08:00
resize ( protyle ) ;
2023-09-08 21:59:18 +08:00
}
2023-09-09 14:54:05 +08:00
if ( data . new [ Constants . CUSTOM_SY_READONLY ] !== data . old [ Constants . CUSTOM_SY_READONLY ] ) {
let customReadOnly = data . new [ Constants . CUSTOM_SY_READONLY ] ;
if ( ! customReadOnly ) {
customReadOnly = window . siyuan . config . editor . readOnly ? "true" : "false" ;
}
if ( customReadOnly === "true" ) {
disabledProtyle ( protyle ) ;
} else {
enableProtyle ( protyle ) ;
}
}
2025-04-18 11:37:48 +08:00
if ( data . new . icon !== data . old . icon ||
data . new [ "title-img" ] !== data . old [ "title-img" ] ||
data . new . tags !== data . old . tags && protyle . background ) {
2023-02-09 17:38:19 +08:00
/// #if MOBILE
2025-04-18 11:37:48 +08:00
protyle = window . siyuan . mobile . editor . protyle ;
2023-02-09 17:38:19 +08:00
/// #endif
2025-04-18 11:37:48 +08:00
protyle . background . ial . icon = data . new . icon ;
protyle . background . ial . tags = data . new . tags ;
protyle . background . ial [ "title-img" ] = data . new [ "title-img" ] ;
protyle . background . render ( protyle . background . ial , protyle . block . rootID ) ;
protyle . model ? . parent . setDocIcon ( data . new . icon ) ;
2023-02-09 17:38:19 +08:00
}
2022-09-17 11:28:09 +08:00
return ;
}
2023-05-02 23:38:38 +08:00
protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) . forEach ( ( item : HTMLElement ) = > {
2022-12-26 11:05:46 +08:00
if ( item . getAttribute ( "data-type" ) === "NodeThematicBreak" ) {
return ;
}
2022-09-09 19:46:54 +08:00
Object . keys ( data . old ) . forEach ( key = > {
2022-09-14 11:18:44 +08:00
item . removeAttribute ( key ) ;
2025-11-26 18:58:24 +08:00
if ( key === "custom-avs" ) {
item . removeAttribute ( "av-names" ) ;
}
2022-09-14 11:18:44 +08:00
} ) ;
2023-09-09 14:54:05 +08:00
if ( data . new . style && data . new [ Constants . CUSTOM_RIFF_DECKS ] && data . new [ Constants . CUSTOM_RIFF_DECKS ] !== data . old [ Constants . CUSTOM_RIFF_DECKS ] ) {
2023-05-25 23:41:52 +08:00
data . new . style += ";animation:addCard 450ms linear" ;
}
2022-09-09 19:46:54 +08:00
Object . keys ( data . new ) . forEach ( key = > {
2025-11-26 18:58:24 +08:00
if ( "id" === key ) {
2025-07-18 18:35:46 +08:00
// 设置属性以后不应该给块元素添加 id 属性 No longer add the `id` attribute to block elements after setting the attribute https://github.com/siyuan-note/siyuan/issues/15327
return ;
}
2022-09-09 19:46:54 +08:00
item . setAttribute ( key , data . new [ key ] ) ;
2025-11-26 20:58:30 +08:00
if ( key === Constants . CUSTOM_RIFF_DECKS &&
data . new [ Constants . CUSTOM_RIFF_DECKS ] !== data . old [ Constants . CUSTOM_RIFF_DECKS ] ) {
2023-05-02 23:38:38 +08:00
item . style . animation = "addCard 450ms linear" ;
setTimeout ( ( ) = > {
2024-03-15 23:48:15 +08:00
if ( item . parentElement ) {
item . style . animation = "" ;
} else {
protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) . forEach ( ( realItem : HTMLElement ) = > {
realItem . style . animation = "" ;
2024-03-17 09:28:33 +08:00
} ) ;
2024-03-15 23:48:15 +08:00
}
2023-05-05 14:04:43 +08:00
} , 450 ) ;
2023-05-02 23:38:38 +08:00
}
2022-09-14 11:18:44 +08:00
} ) ;
2025-06-17 11:25:30 +08:00
if ( data [ "data-av-type" ] ) {
item . setAttribute ( "data-av-type" , data [ "data-av-type" ] ) ;
}
2025-06-02 22:51:00 +08:00
const attrElements = item . querySelectorAll ( ".protyle-attr" ) ;
const attrElement = attrElements [ attrElements . length - 1 ] ;
2024-03-08 23:39:18 +08:00
if ( data . new [ "custom-avs" ] && ! data . new [ "av-names" ] ) {
2025-05-12 11:10:29 +08:00
nodeAttrHTML += attrElement . querySelector ( ".protyle-attr--av" ) ? . outerHTML || "" ;
2024-03-08 23:39:18 +08:00
}
2025-05-12 11:10:29 +08:00
const refElement = attrElement . querySelector ( ".protyle-attr--refcount" ) ;
2022-09-09 19:46:54 +08:00
if ( refElement ) {
nodeAttrHTML += refElement . outerHTML ;
}
2025-05-12 11:10:29 +08:00
attrElement . innerHTML = nodeAttrHTML + Constants . ZWSP ;
2022-09-09 19:46:54 +08:00
} ) ;
return ;
}
2022-05-26 15:18:53 +08:00
if ( operation . action === "move" ) {
2025-09-15 23:38:49 +08:00
if ( operation . context ? . ignoreProcess === "true" ) {
return ;
}
2022-06-29 20:25:30 +08:00
/// #if !MOBILE
2022-10-03 01:07:24 +08:00
if ( updateElements . length === 0 ) {
2022-05-26 15:18:53 +08:00
// 打开两个相同的文档 A、A1, 从 A 拖拽块 B 到 A1, 在后续 ws 处理中,无法获取到拖拽出去的 B
getAllModels ( ) . editor . forEach ( editor = > {
const updateCloneElement = editor . editor . protyle . wysiwyg . element . querySelector ( ` [data-node-id=" ${ operation . id } "] ` ) ;
if ( updateCloneElement ) {
2022-10-03 01:07:24 +08:00
updateElements . push ( updateCloneElement . cloneNode ( true ) as Element ) ;
2022-05-26 15:18:53 +08:00
}
} ) ;
}
2022-11-21 11:17:00 +08:00
if ( updateElements . length === 0 ) {
// 页签拖入浮窗 https://github.com/siyuan-note/siyuan/issues/6647
window . siyuan . blockPanels . forEach ( ( item ) = > {
2022-11-22 21:54:52 +08:00
const updateCloneElement = item . element . querySelector ( ` [data-node-id=" ${ operation . id } "] ` ) ;
2022-11-21 11:17:00 +08:00
if ( updateCloneElement ) {
updateElements . push ( updateCloneElement . cloneNode ( true ) as Element ) ;
}
} ) ;
}
2022-06-29 20:25:30 +08:00
/// #endif
2025-03-29 09:35:19 +08:00
let range ;
if ( isUndo && getSelection ( ) . rangeCount > 0 ) {
range = getSelection ( ) . getRangeAt ( 0 ) ;
const rangeBlockElement = hasClosestBlock ( range . startContainer ) ;
if ( rangeBlockElement ) {
if ( getContenteditableElement ( rangeBlockElement ) ) {
range . insertNode ( document . createElement ( "wbr" ) ) ;
} else {
getContenteditableElement ( updateElements [ 0 ] ) . insertAdjacentHTML ( "afterbegin" , "<wbr>" ) ;
}
}
}
2022-10-03 16:22:18 +08:00
let hasFind = false ;
2022-10-03 01:07:24 +08:00
if ( operation . previousID && updateElements . length > 0 ) {
2025-04-03 15:13:01 +08:00
const previousElement = protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . previousID } "] ` ) ;
2025-04-03 01:52:29 +08:00
if ( previousElement . length === 0 && protyle . options . backlinkData && isUndo && getSelection ( ) . rangeCount > 0 ) {
// 反链面板删除超级块中的最后一个段落块后撤销重做
2025-04-03 15:13:01 +08:00
const blockElement = hasTopClosestByAttribute ( range . startContainer , "data-node-id" , null ) ;
2025-04-03 01:52:29 +08:00
if ( blockElement ) {
blockElement . before ( processClonePHElement ( updateElements [ 0 ] . cloneNode ( true ) as Element ) ) ;
2022-10-03 16:22:18 +08:00
hasFind = true ;
2022-05-26 15:18:53 +08:00
}
2025-04-03 01:52:29 +08:00
} else {
previousElement . forEach ( item = > {
if ( ! isInEmbedBlock ( item ) ) {
item . after ( processClonePHElement ( updateElements [ 0 ] . cloneNode ( true ) as Element ) ) ;
hasFind = true ;
}
} ) ;
}
2022-10-03 01:07:24 +08:00
} else if ( updateElements . length > 0 ) {
2025-04-03 15:13:01 +08:00
const parentElement = protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . parentID } "] ` ) ;
2025-10-04 17:10:35 +08:00
if ( ! protyle . options . backlinkData && operation . parentID === protyle . block . parentID && ! protyle . block . showAll ) {
2024-06-07 11:02:02 +08:00
protyle . wysiwyg . element . prepend ( processClonePHElement ( updateElements [ 0 ] . cloneNode ( true ) as Element ) ) ;
2022-10-03 16:22:18 +08:00
hasFind = true ;
2025-04-02 17:51:44 +08:00
} else if ( parentElement . length === 0 && protyle . options . backlinkData && isUndo && getSelection ( ) . rangeCount > 0 ) {
// 反链面板删除超级块中的段落块后撤销再重做 https://github.com/siyuan-note/siyuan/issues/14496#issuecomment-2771372486
2025-04-03 15:13:01 +08:00
const topBlockElement = hasTopClosestByAttribute ( getSelection ( ) . getRangeAt ( 0 ) . startContainer , "data-node-id" , null ) ;
2025-04-02 17:51:44 +08:00
if ( topBlockElement ) {
topBlockElement . before ( processClonePHElement ( updateElements [ 0 ] . cloneNode ( true ) as Element ) ) ;
hasFind = true ;
}
2022-10-03 01:07:24 +08:00
} else {
2025-04-02 17:51:44 +08:00
parentElement . forEach ( item = > {
2024-07-25 17:55:25 +08:00
if ( ! isInEmbedBlock ( item ) ) {
2025-12-01 19:30:33 +08:00
const cloneElement = processClonePHElement ( updateElements [ 0 ] . cloneNode ( true ) as Element ) ;
2022-10-03 01:07:24 +08:00
// 列表特殊处理
if ( item . firstElementChild ? . classList . contains ( "protyle-action" ) ) {
2025-12-01 19:30:33 +08:00
item . firstElementChild . after ( cloneElement ) ;
} else if ( item . classList . contains ( "callout" ) ) {
item . querySelector ( ".callout-content" ) . prepend ( cloneElement ) ;
2022-10-03 01:07:24 +08:00
} else {
2025-12-01 19:30:33 +08:00
item . prepend ( cloneElement ) ;
2022-10-03 01:07:24 +08:00
}
2022-10-03 16:22:18 +08:00
hasFind = true ;
2022-10-03 01:07:24 +08:00
}
} ) ;
2022-05-26 15:18:53 +08:00
}
}
2022-10-03 01:07:24 +08:00
updateElements . forEach ( item = > {
if ( hasFind ) {
item . remove ( ) ;
} else if ( ! hasFind && item . parentElement ) {
removeTopElement ( item , protyle ) ;
}
} ) ;
2023-09-01 19:52:55 +08:00
if ( isUndo && range ) {
2022-05-26 15:18:53 +08:00
if ( operation . data === "focus" ) {
2022-10-11 11:19:36 +08:00
// 标记需要 focus, https://ld246.com/article/1650018446988/comment/1650081404993?r=Vanessa#comments
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) ) . find ( item = > {
2024-07-25 17:55:25 +08:00
if ( ! isInEmbedBlock ( item ) ) {
2022-10-11 11:19:36 +08:00
focusBlock ( item ) ;
return true ;
}
} ) ;
2025-03-29 09:35:19 +08:00
document . querySelectorAll ( "wbr" ) . forEach ( item = > {
2025-03-29 10:29:01 +08:00
item . remove ( ) ;
} ) ;
2022-05-26 15:18:53 +08:00
} else {
2022-10-11 11:19:36 +08:00
focusByWbr ( protyle . wysiwyg . element , range ) ;
2022-05-26 15:18:53 +08:00
}
}
2025-03-16 21:54:25 +08:00
// 更新 ws 嵌入块, undo 会在 transaction 中更新
if ( ! isUndo ) {
protyle . wysiwyg . element . querySelectorAll ( '[data-type="NodeBlockQueryEmbed"]' ) . forEach ( ( item ) = > {
if ( item . querySelector ( ` [data-node-id=" ${ operation . id } "],[data-node-id=" ${ operation . parentID } "],[data-node-id=" ${ operation . previousID } "] ` ) ) {
item . removeAttribute ( "data-render" ) ;
blockRender ( protyle , item ) ;
}
} ) ;
}
2022-05-26 15:18:53 +08:00
return ;
}
if ( operation . action === "insert" ) {
2025-09-06 22:33:31 +08:00
if ( operation . context ? . ignoreProcess === "true" ) {
return ;
}
2022-10-03 11:28:22 +08:00
const cursorElements = [ ] ;
2022-05-26 15:18:53 +08:00
if ( operation . previousID ) {
2025-04-03 01:52:29 +08:00
const previousElement = protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . previousID } "] ` ) ;
2025-08-01 23:14:34 +08:00
if ( previousElement . length === 0 && isUndo && protyle . wysiwyg . element . childElementCount === 0 ) {
// https://github.com/siyuan-note/siyuan/issues/15396 操作后撤销
protyle . wysiwyg . element . innerHTML = operation . data ;
} else if ( previousElement . length === 0 && protyle . options . backlinkData && isUndo && getSelection ( ) . rangeCount > 0 ) {
2025-04-03 01:52:29 +08:00
// 反链面板删除超级块中的最后一个段落块后撤销
2025-04-03 15:13:01 +08:00
const blockElement = hasClosestBlock ( getSelection ( ) . getRangeAt ( 0 ) . startContainer ) ;
2025-04-03 01:52:29 +08:00
if ( blockElement ) {
blockElement . insertAdjacentHTML ( "beforebegin" , operation . data ) ;
cursorElements . push ( blockElement . previousElementSibling ) ;
2022-05-26 15:18:53 +08:00
}
2025-04-03 01:52:29 +08:00
} else {
previousElement . forEach ( item = > {
const embedElement = isInEmbedBlock ( item ) ;
if ( embedElement ) {
// https://github.com/siyuan-note/siyuan/issues/5524
embedElement . removeAttribute ( "data-render" ) ;
blockRender ( protyle , embedElement ) ;
} else {
item . insertAdjacentHTML ( "afterend" , operation . data ) ;
cursorElements . push ( item . nextElementSibling ) ;
}
} ) ;
}
2024-09-15 17:29:39 +08:00
} else if ( operation . nextID ) {
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . nextID } "] ` ) ) . forEach ( item = > {
const embedElement = isInEmbedBlock ( item ) ;
if ( embedElement ) {
// https://github.com/siyuan-note/siyuan/issues/5524
embedElement . removeAttribute ( "data-render" ) ;
blockRender ( protyle , embedElement ) ;
} else {
item . insertAdjacentHTML ( "beforebegin" , operation . data ) ;
cursorElements . push ( item . previousElementSibling ) ;
}
} ) ;
2022-05-26 15:18:53 +08:00
} else {
2025-04-03 01:52:29 +08:00
const parentElement = protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . parentID } "] ` ) ;
2025-10-04 17:10:35 +08:00
if ( ! protyle . options . backlinkData && operation . parentID === protyle . block . parentID && ! protyle . block . showAll ) {
2022-05-26 15:18:53 +08:00
protyle . wysiwyg . element . insertAdjacentHTML ( "afterbegin" , operation . data ) ;
2022-10-03 16:22:18 +08:00
cursorElements . push ( protyle . wysiwyg . element . firstElementChild ) ;
2025-04-02 13:08:20 +08:00
} else if ( parentElement . length === 0 && protyle . options . backlinkData && isUndo && getSelection ( ) . rangeCount > 0 ) {
// 反链面板删除超级块中的段落块后撤销
2025-04-03 15:13:01 +08:00
const blockElement = hasClosestBlock ( getSelection ( ) . getRangeAt ( 0 ) . startContainer ) ;
2025-04-02 13:08:20 +08:00
if ( blockElement ) {
blockElement . insertAdjacentHTML ( "beforebegin" , operation . data ) ;
cursorElements . push ( blockElement . previousElementSibling ) ;
}
2022-10-03 11:28:22 +08:00
} else {
2025-04-02 13:08:20 +08:00
parentElement . forEach ( item = > {
2024-07-25 17:55:25 +08:00
if ( ! isInEmbedBlock ( item ) ) {
2022-10-03 11:28:22 +08:00
// 列表特殊处理
if ( item . firstElementChild ? . classList . contains ( "protyle-action" ) ) {
item . firstElementChild . insertAdjacentHTML ( "afterend" , operation . data ) ;
2022-10-03 16:22:18 +08:00
cursorElements . push ( item . firstElementChild . nextElementSibling ) ;
2025-12-01 19:30:33 +08:00
} else if ( item . classList . contains ( "callout" ) ) {
item . querySelector ( ".callout-content" ) . insertAdjacentHTML ( "afterbegin" , operation . data ) ;
cursorElements . push ( item . querySelector ( '[data-node-id]' ) ) ;
2022-10-03 11:28:22 +08:00
} else {
item . insertAdjacentHTML ( "afterbegin" , operation . data ) ;
2022-10-03 16:22:18 +08:00
cursorElements . push ( item . firstElementChild ) ;
2022-10-03 11:28:22 +08:00
}
}
} ) ;
2022-05-26 15:18:53 +08:00
}
}
// https://github.com/siyuan-note/siyuan/issues/4420
protyle . wysiwyg . element . querySelectorAll ( '[data-type="NodeHeading"]' ) . forEach ( item = > {
if ( item . lastElementChild . getAttribute ( "spin" ) === "1" ) {
item . lastElementChild . remove ( ) ;
}
} ) ;
2022-10-03 11:28:22 +08:00
if ( cursorElements . length === 0 ) {
return ;
2022-05-26 15:18:53 +08:00
}
2022-10-03 11:28:22 +08:00
cursorElements . forEach ( item = > {
processRender ( item ) ;
highlightRender ( item ) ;
2023-09-09 23:21:46 +08:00
avRender ( item , protyle ) ;
2022-10-03 11:28:22 +08:00
blockRender ( protyle , item ) ;
const wbrElement = item . querySelector ( "wbr" ) ;
2023-09-01 19:52:55 +08:00
if ( isUndo ) {
2022-10-03 11:28:22 +08:00
const range = getEditorRange ( item ) ;
if ( wbrElement ) {
focusByWbr ( item , range ) ;
} else {
focusBlock ( item ) ;
}
} else if ( wbrElement ) {
wbrElement . remove ( ) ;
}
} ) ;
2025-10-10 13:26:01 +08:00
protyle . wysiwyg . element . querySelectorAll ( "[parent-heading]" ) . forEach ( item = > {
item . remove ( ) ;
} ) ;
2024-03-29 20:48:02 +08:00
return ;
}
if ( operation . action === "append" ) {
2024-04-20 11:18:25 +08:00
// 目前只有移动块的时候会调用,反连面板就自己点击刷新处理。
if ( ! protyle . options . backlinkData ) {
reloadProtyle ( protyle , false ) ;
}
2024-03-29 20:48:02 +08:00
return ;
}
2025-08-10 17:39:35 +08:00
if ( [ "addAttrViewCol" , "updateAttrViewCol" , "updateAttrViewColOptions" ,
2023-07-10 19:02:59 +08:00
"updateAttrViewColOption" , "updateAttrViewCell" , "sortAttrViewRow" , "sortAttrViewCol" , "setAttrViewColHidden" ,
2023-07-13 10:21:11 +08:00
"setAttrViewColWrap" , "setAttrViewColWidth" , "removeAttrViewColOption" , "setAttrViewName" , "setAttrViewFilters" ,
2023-10-11 11:30:00 +08:00
"setAttrViewSorts" , "setAttrViewColCalc" , "removeAttrViewCol" , "updateAttrViewColNumberFormat" , "removeAttrViewBlock" ,
2024-01-21 12:21:31 +08:00
"replaceAttrViewBlock" , "updateAttrViewColTemplate" , "setAttrViewColPin" , "addAttrViewView" , "setAttrViewColIcon" ,
2023-12-08 21:56:48 +08:00
"removeAttrViewView" , "setAttrViewViewName" , "setAttrViewViewIcon" , "duplicateAttrViewView" , "sortAttrViewView" ,
2025-06-13 10:39:27 +08:00
"updateAttrViewColRelation" , "setAttrViewPageSize" , "updateAttrViewColRollup" , "sortAttrViewKey" , "setAttrViewColDesc" ,
"duplicateAttrViewKey" , "setAttrViewViewDesc" , "setAttrViewCoverFrom" , "setAttrViewCoverFromAssetKeyID" ,
2025-06-30 15:46:22 +08:00
"setAttrViewBlockView" , "setAttrViewCardSize" , "setAttrViewCardAspectRatio" , "hideAttrViewName" , "setAttrViewShowIcon" ,
2025-07-27 11:23:58 +08:00
"setAttrViewWrapField" , "setAttrViewGroup" , "removeAttrViewGroup" , "hideAttrViewGroup" , "sortAttrViewGroup" ,
2025-08-23 22:09:21 +08:00
"foldAttrViewGroup" , "hideAttrViewAllGroups" , "setAttrViewFitImage" , "setAttrViewDisplayFieldName" ,
2025-11-10 18:37:07 +08:00
"insertAttrViewBlock" , "setAttrViewColDateFillSpecificTime" , "setAttrViewFillColBackgroundColor" , "setAttrViewUpdatedIncludeTime" ,
"setAttrViewCreatedIncludeTime" ] . includes ( operation . action ) ) {
2025-08-23 22:09:21 +08:00
// 撤销 transaction 会进行推送,需使用推送来进行刷新最新数据 https://github.com/siyuan-note/siyuan/issues/13607
2025-07-10 09:47:20 +08:00
if ( ! isUndo ) {
2024-12-28 00:05:33 +08:00
refreshAV ( protyle , operation ) ;
} else if ( operation . action === "setAttrViewName" ) {
// setAttrViewName 同文档不会推送,需手动刷新
2025-08-08 16:24:42 +08:00
Array . from ( protyle . wysiwyg . element . querySelectorAll ( ` .av[data-av-id=" ${ operation . id } "] ` ) ) . forEach ( ( item : HTMLElement ) = > {
2024-12-28 00:05:33 +08:00
const titleElement = item . querySelector ( ".av__title" ) as HTMLElement ;
if ( ! titleElement ) {
return ;
}
titleElement . textContent = operation . data ;
titleElement . dataset . title = operation . data ;
} ) ;
}
2024-03-29 20:48:02 +08:00
return ;
}
if ( operation . action === "doUpdateUpdated" ) {
2023-10-13 23:25:58 +08:00
updateElements . forEach ( item = > {
item . setAttribute ( "updated" , operation . data ) ;
2023-10-21 23:49:28 +08:00
} ) ;
2024-03-29 20:48:02 +08:00
return ;
2022-05-26 15:18:53 +08:00
}
} ;
2023-02-09 17:38:19 +08:00
export const turnsIntoOneTransaction = ( options : {
protyle : IProtyle ,
selectsElement : Element [ ] ,
2023-11-28 17:19:22 +08:00
type : TTurnIntoOne ,
level? : TTurnIntoOneSub
2023-02-09 17:38:19 +08:00
} ) = > {
2022-05-26 15:18:53 +08:00
let parentElement : Element ;
const id = Lute . NewNodeID ( ) ;
if ( options . type === "BlocksMergeSuperBlock" ) {
parentElement = genSBElement ( options . level , id ) ;
} else if ( options . type === "Blocks2Blockquote" ) {
parentElement = document . createElement ( "div" ) ;
parentElement . classList . add ( "bq" ) ;
parentElement . setAttribute ( "data-node-id" , id ) ;
2025-12-01 18:51:58 +08:00
parentElement . setAttribute ( "data-type" , "NodeBlockquote" ) ;
2025-12-01 19:30:33 +08:00
parentElement . innerHTML = ` <div class="protyle-attr" contenteditable="false"> ${ Constants . ZWSP } </div> ` ;
2025-12-01 18:51:58 +08:00
} else if ( options . type === "Blocks2Callout" ) {
parentElement = document . createElement ( "div" ) ;
2025-12-01 19:30:33 +08:00
parentElement . classList . add ( "callout" ) ;
2025-12-01 18:51:58 +08:00
parentElement . setAttribute ( "data-node-id" , id ) ;
2025-12-01 19:30:33 +08:00
parentElement . setAttribute ( "data-type" , "NodeCallout" ) ;
parentElement . setAttribute ( "contenteditable" , "false" ) ;
parentElement . setAttribute ( "data-subtype" , "NOTE" ) ;
parentElement . innerHTML = ` <div class="callout-info"><span class="callout-icon">✏️</span><span class="callout-title">Note</span></div><div class="callout-content"></div><div class="protyle-attr" contenteditable="false"> ${ Constants . ZWSP } </div> ` ;
2022-05-26 15:18:53 +08:00
} else if ( options . type . endsWith ( "Ls" ) ) {
parentElement = document . createElement ( "div" ) ;
parentElement . classList . add ( "list" ) ;
parentElement . setAttribute ( "data-node-id" , id ) ;
parentElement . setAttribute ( "data-type" , "NodeList" ) ;
if ( options . type === "Blocks2ULs" ) {
parentElement . setAttribute ( "data-subtype" , "u" ) ;
} else if ( options . type === "Blocks2OLs" ) {
parentElement . setAttribute ( "data-subtype" , "o" ) ;
} else {
parentElement . setAttribute ( "data-subtype" , "t" ) ;
}
let html = "" ;
options . selectsElement . forEach ( ( item , index ) = > {
if ( options . type === "Blocks2ULs" ) {
html += ` <div data-marker="*" data-subtype="u" data-node-id=" ${ Lute . NewNodeID ( ) } " data-type="NodeListItem" class="li"><div class="protyle-action" draggable="true"><svg><use xlink:href="#iconDot"></use></svg></div><div class="protyle-attr" contenteditable="false"></div></div> ` ;
} else if ( options . type === "Blocks2OLs" ) {
html += ` <div data-marker=" ${ index + 1 } ." data-subtype="o" data-node-id=" ${ Lute . NewNodeID ( ) } " data-type="NodeListItem" class="li"><div class="protyle-action protyle-action--order" contenteditable="false" draggable="true"> ${ index + 1 } .</div><div class="protyle-attr" contenteditable="false"></div></div> ` ;
} else {
2022-08-28 21:29:35 +08:00
html += ` <div data-marker="*" data-subtype="t" data-node-id=" ${ Lute . NewNodeID ( ) } " data-type="NodeListItem" class="li"><div class="protyle-action protyle-action--task" draggable="true"><svg><use xlink:href="#iconUncheck"></use></svg></div><div class="protyle-attr" contenteditable="false"></div></div> ` ;
2022-05-26 15:18:53 +08:00
}
} ) ;
parentElement . innerHTML = html + '<div class="protyle-attr" contenteditable="false"></div>' ;
}
2022-10-04 11:43:45 +08:00
const previousId = options . selectsElement [ 0 ] . getAttribute ( "data-node-id" ) ;
2022-05-26 15:18:53 +08:00
const parentId = options . selectsElement [ 0 ] . parentElement . getAttribute ( "data-node-id" ) || options . protyle . block . parentID ;
const doOperations : IOperation [ ] = [ {
action : "insert" ,
id ,
data : parentElement.outerHTML ,
2022-10-15 14:23:40 +08:00
nextID : previousId ,
2022-05-26 15:18:53 +08:00
parentID : parentId
} ] ;
const undoOperations : IOperation [ ] = [ ] ;
if ( options . selectsElement [ 0 ] . previousElementSibling ) {
options . selectsElement [ 0 ] . before ( parentElement ) ;
} else {
options . selectsElement [ 0 ] . parentElement . prepend ( parentElement ) ;
}
let itemPreviousId : string ;
options . selectsElement . forEach ( ( item , index ) = > {
item . classList . remove ( "protyle-wysiwyg--select" ) ;
2022-10-22 11:31:41 +08:00
item . removeAttribute ( "select-start" ) ;
item . removeAttribute ( "select-end" ) ;
2022-05-26 15:18:53 +08:00
const itemId = item . getAttribute ( "data-node-id" ) ;
undoOperations . push ( {
action : "move" ,
id : itemId ,
2022-10-04 11:43:45 +08:00
previousID : itemPreviousId || id ,
2022-05-26 15:18:53 +08:00
parentID : parentId
} ) ;
if ( options . type . endsWith ( "Ls" ) ) {
doOperations . push ( {
action : "move" ,
id : itemId ,
parentID : parentElement.children [ index ] . getAttribute ( "data-node-id" )
} ) ;
parentElement . children [ index ] . firstElementChild . after ( item ) ;
2025-12-01 19:30:33 +08:00
} else if ( options . type === "Blocks2Callout" ) {
doOperations . push ( {
action : "move" ,
id : itemId ,
previousID : itemPreviousId ,
parentID : id
} ) ;
parentElement . querySelector ( ".callout-content" ) . insertAdjacentElement ( "beforeend" , item ) ;
2022-05-26 15:18:53 +08:00
} else {
doOperations . push ( {
action : "move" ,
id : itemId ,
previousID : itemPreviousId ,
parentID : id
} ) ;
parentElement . lastElementChild . before ( item ) ;
}
itemPreviousId = item . getAttribute ( "data-node-id" ) ;
if ( index === options . selectsElement . length - 1 ) {
undoOperations . push ( {
action : "delete" ,
id ,
} ) ;
}
2023-03-05 14:18:23 +08:00
// 超级块内嵌入块无面包屑,需重新渲染 https://github.com/siyuan-note/siyuan/issues/7574
if ( item . getAttribute ( "data-type" ) === "NodeBlockQueryEmbed" ) {
2023-03-06 13:43:32 +08:00
item . removeAttribute ( "data-render" ) ;
blockRender ( options . protyle , item ) ;
2023-03-05 14:18:23 +08:00
}
2022-05-26 15:18:53 +08:00
} ) ;
transaction ( options . protyle , doOperations , undoOperations ) ;
focusBlock ( options . protyle . wysiwyg . element . querySelector ( ` [data-node-id=" ${ options . selectsElement [ 0 ] . getAttribute ( "data-node-id" ) } "] ` ) ) ;
hideElements ( [ "gutter" ] , options . protyle ) ;
} ;
2022-09-09 19:46:54 +08:00
const removeUnfoldRepeatBlock = ( html : string , protyle : IProtyle ) = > {
2022-09-02 19:41:59 +08:00
const temp = document . createElement ( "template" ) ;
temp . innerHTML = html ;
2022-09-02 19:38:54 +08:00
Array . from ( temp . content . children ) . forEach ( item = > {
2022-09-02 19:41:59 +08:00
protyle . wysiwyg . element . querySelector ( ` :scope > [data-node-id=" ${ item . getAttribute ( "data-node-id" ) } "] ` ) ? . remove ( ) ;
} ) ;
} ;
2022-09-02 19:38:54 +08:00
2022-08-29 20:57:33 +08:00
export const turnsIntoTransaction = ( options : {
protyle : IProtyle ,
selectsElement? : Element [ ] ,
nodeElement? : Element ,
2023-11-28 17:19:22 +08:00
type : TTurnInto ,
level? : number ,
2022-09-02 16:37:35 +08:00
isContinue? : boolean ,
2024-03-13 00:19:06 +08:00
range? : Range
2022-08-29 20:57:33 +08:00
} ) = > {
2025-10-08 15:58:14 +08:00
// https://github.com/siyuan-note/siyuan/issues/14505
2025-10-11 22:02:11 +08:00
options . protyle . observerLoad ? . disconnect ( ) ;
2022-08-29 20:57:33 +08:00
let selectsElement : Element [ ] = options . selectsElement ;
2022-09-02 19:41:59 +08:00
let range : Range ;
2022-08-29 20:57:33 +08:00
// 通过快捷键触发
if ( options . nodeElement ) {
2022-09-02 16:37:35 +08:00
range = getSelection ( ) . getRangeAt ( 0 ) ;
2022-09-02 19:41:59 +08:00
range . insertNode ( document . createElement ( "wbr" ) ) ;
2022-08-29 20:57:33 +08:00
selectsElement = Array . from ( options . protyle . wysiwyg . element . querySelectorAll ( ".protyle-wysiwyg--select" ) ) ;
if ( selectsElement . length === 0 ) {
selectsElement = [ options . nodeElement ] ;
2022-05-26 15:18:53 +08:00
}
2022-08-29 20:57:33 +08:00
let isContinue = false ;
let isList = false ;
selectsElement . find ( ( item , index ) = > {
if ( item . classList . contains ( "li" ) ) {
isList = true ;
return true ;
}
if ( item . nextElementSibling && selectsElement [ index + 1 ] &&
2025-07-23 13:08:38 +08:00
item . nextElementSibling === selectsElement [ index + 1 ] ) {
2022-08-29 20:57:33 +08:00
isContinue = true ;
} else if ( index !== selectsElement . length - 1 ) {
isContinue = false ;
return true ;
}
2022-05-26 15:18:53 +08:00
} ) ;
2025-09-29 11:36:26 +08:00
if ( isList ) {
2022-08-29 20:57:33 +08:00
return ;
}
if ( selectsElement . length === 1 && options . type === "Blocks2Hs" &&
selectsElement [ 0 ] . getAttribute ( "data-type" ) === "NodeHeading" &&
options . level === parseInt ( selectsElement [ 0 ] . getAttribute ( "data-subtype" ) . substr ( 1 ) ) ) {
// 快捷键同级转换,消除标题
options . type = "Blocks2Ps" ;
}
options . isContinue = isContinue ;
}
let html = "" ;
const doOperations : IOperation [ ] = [ ] ;
const undoOperations : IOperation [ ] = [ ] ;
2025-04-13 00:06:02 +08:00
let previousId : string ;
2025-10-10 22:15:10 +08:00
selectsElement . forEach ( ( item : HTMLElement , index ) = > {
2022-08-29 20:57:33 +08:00
item . classList . remove ( "protyle-wysiwyg--select" ) ;
2022-10-22 12:14:26 +08:00
item . removeAttribute ( "select-start" ) ;
item . removeAttribute ( "select-end" ) ;
2022-08-29 20:57:33 +08:00
html += item . outerHTML ;
const id = item . getAttribute ( "data-node-id" ) ;
2025-04-13 00:06:02 +08:00
const tempElement = document . createElement ( "template" ) ;
2023-11-28 17:19:22 +08:00
if ( ! options . isContinue ) {
2022-08-29 20:57:33 +08:00
// @ts-ignore
2025-10-10 22:15:10 +08:00
let newHTML = options . protyle . lute [ options . type ] ( item . outerHTML , options . level ) ;
2025-04-13 00:06:02 +08:00
tempElement . innerHTML = newHTML ;
if ( ! tempElement . content . querySelector ( ` [data-node-id=" ${ id } "] ` ) ) {
undoOperations . push ( {
action : "insert" ,
id ,
previousID : previousId || item . previousElementSibling ? . getAttribute ( "data-node-id" ) ,
data : item.outerHTML ,
parentID : item.parentElement?.getAttribute ( "data-node-id" ) || options . protyle . block . parentID || options . protyle . block . rootID ,
} ) ;
Array . from ( tempElement . content . children ) . forEach ( ( tempItem : HTMLElement ) = > {
const tempItemId = tempItem . getAttribute ( "data-node-id" ) ;
doOperations . push ( {
action : "insert" ,
id : tempItemId ,
previousID : tempItem.previousElementSibling?.getAttribute ( "data-node-id" ) || item . previousElementSibling ? . getAttribute ( "data-node-id" ) ,
data : tempItem.outerHTML ,
parentID : item.parentElement?.getAttribute ( "data-node-id" ) || options . protyle . block . parentID || options . protyle . block . rootID ,
} ) ;
undoOperations . splice ( 0 , 0 , {
action : "delete" ,
id : tempItemId ,
} ) ;
} ) ;
doOperations . push ( {
action : "delete" ,
id ,
} ) ;
2025-07-23 12:21:59 +08:00
if ( item === selectsElement [ index + 1 ] ? . previousElementSibling ) {
2025-04-13 00:06:02 +08:00
previousId = id ;
} else {
previousId = undefined ;
}
2022-08-29 20:57:33 +08:00
} else {
2025-10-10 22:15:10 +08:00
let foldData ;
if ( item . getAttribute ( "data-type" ) === "NodeHeading" && item . getAttribute ( "fold" ) === "1" &&
tempElement . content . firstElementChild . getAttribute ( "data-subtype" ) !== item . dataset . subtype ) {
foldData = setFold ( options . protyle , item , undefined , undefined , false , true ) ;
newHTML = newHTML . replace ( ' fold="1"' , "" ) ;
}
if ( foldData && foldData . doOperations ? . length > 0 ) {
doOperations . push ( . . . foldData . doOperations ) ;
}
2025-04-13 00:06:02 +08:00
undoOperations . push ( {
action : "update" ,
id ,
data : item.outerHTML ,
} ) ;
doOperations . push ( {
action : "update" ,
2025-04-16 18:46:34 +08:00
id ,
2025-04-13 00:06:02 +08:00
data : newHTML
} ) ;
2025-10-10 22:15:10 +08:00
if ( foldData && foldData . undoOperations ? . length > 0 ) {
undoOperations . push ( . . . foldData . undoOperations ) ;
}
2022-08-29 20:57:33 +08:00
}
2025-04-13 00:06:02 +08:00
item . outerHTML = newHTML ;
2023-12-20 09:31:40 +08:00
} else {
2025-04-13 00:06:02 +08:00
undoOperations . push ( {
2023-12-20 09:31:40 +08:00
action : "insert" ,
2025-04-13 00:06:02 +08:00
id ,
previousID : doOperations [ doOperations . length - 1 ] ? . id || item . previousElementSibling ? . getAttribute ( "data-node-id" ) ,
2023-12-20 09:31:40 +08:00
data : item.outerHTML ,
parentID : item.parentElement?.getAttribute ( "data-node-id" ) || options . protyle . block . parentID || options . protyle . block . rootID ,
} ) ;
2025-04-13 00:06:02 +08:00
doOperations . push ( {
2023-12-20 09:31:40 +08:00
action : "delete" ,
2025-04-13 00:06:02 +08:00
id ,
2023-12-21 09:07:53 +08:00
} ) ;
2025-04-13 00:06:02 +08:00
if ( index === selectsElement . length - 1 ) {
// @ts-ignore
const newHTML = options . protyle . lute [ options . type ] ( html , options . level ) ;
tempElement . innerHTML = newHTML ;
Array . from ( tempElement . content . children ) . forEach ( ( tempItem : HTMLElement ) = > {
const tempItemId = tempItem . getAttribute ( "data-node-id" ) ;
doOperations . push ( {
action : "insert" ,
id : tempItemId ,
previousID : tempItem.previousElementSibling?.getAttribute ( "data-node-id" ) || item . previousElementSibling ? . getAttribute ( "data-node-id" ) ,
data : tempItem.outerHTML ,
parentID : item.parentElement?.getAttribute ( "data-node-id" ) || options . protyle . block . parentID || options . protyle . block . rootID ,
} ) ;
undoOperations . splice ( 0 , 0 , {
action : "delete" ,
id : tempItemId ,
} ) ;
} ) ;
item . outerHTML = newHTML ;
} else {
item . remove ( ) ;
}
2023-12-20 09:31:40 +08:00
}
2023-12-21 09:07:53 +08:00
} ) ;
2022-08-29 20:57:33 +08:00
transaction ( options . protyle , doOperations , undoOperations ) ;
2022-05-26 15:18:53 +08:00
processRender ( options . protyle . wysiwyg . element ) ;
highlightRender ( options . protyle . wysiwyg . element ) ;
2023-09-09 23:21:46 +08:00
avRender ( options . protyle . wysiwyg . element , options . protyle ) ;
2022-08-29 20:57:33 +08:00
blockRender ( options . protyle , options . protyle . wysiwyg . element ) ;
2024-03-13 00:19:06 +08:00
if ( range || options . range ) {
focusByWbr ( options . protyle . wysiwyg . element , range || options . range ) ;
2022-09-02 16:37:35 +08:00
} else {
focusBlock ( options . protyle . wysiwyg . element . querySelector ( ` [data-node-id=" ${ selectsElement [ 0 ] . getAttribute ( "data-node-id" ) } "] ` ) ) ;
}
2022-08-29 20:57:33 +08:00
hideElements ( [ "gutter" ] , options . protyle ) ;
2022-05-26 15:18:53 +08:00
} ;
2024-06-05 22:50:53 +08:00
export const turnsOneInto = async ( options : {
protyle : IProtyle ,
nodeElement : Element ,
id : string ,
type : string ,
level? : number
} ) = > {
if ( ! options . nodeElement . querySelector ( "wbr" ) ) {
getContenteditableElement ( options . nodeElement ) ? . insertAdjacentHTML ( "afterbegin" , "<wbr>" ) ;
}
2025-12-01 12:13:49 +08:00
if ( [ "CancelBlockquote" , "CancelList" , "CancelCallout" ] . includes ( options . type ) ) {
for ( const item of options . nodeElement . querySelectorAll ( '[data-type="NodeHeading"][fold="1"]' ) ) {
2024-06-05 22:50:53 +08:00
const itemId = item . getAttribute ( "data-node-id" ) ;
item . removeAttribute ( "fold" ) ;
const response = await fetchSyncPost ( "/api/transactions" , {
session : options.protyle.id ,
app : Constants.SIYUAN_APPID ,
transactions : [ {
doOperations : [ {
action : "unfoldHeading" ,
id : itemId ,
} ] ,
undoOperations : [ {
action : "foldHeading" ,
id : itemId
} ] ,
} ]
} ) ;
options . protyle . undo . add ( [ {
action : "unfoldHeading" ,
id : itemId ,
} ] , [ {
action : "foldHeading" ,
id : itemId
} ] , options . protyle ) ;
item . insertAdjacentHTML ( "afterend" , response . data [ 0 ] . doOperations [ 0 ] . retData ) ;
}
}
const oldHTML = options . nodeElement . outerHTML ;
2025-08-01 23:14:34 +08:00
let previousId = options . nodeElement . previousElementSibling ? . getAttribute ( "data-node-id" ) ;
if ( ! options . nodeElement . previousElementSibling && options . protyle . block . showAll ) {
const response = await fetchSyncPost ( "/api/block/getBlockRelevantIDs" , { id : options.id } ) ;
previousId = response . data . previousID ;
}
2024-06-05 22:50:53 +08:00
const parentId = options . nodeElement . parentElement . getAttribute ( "data-node-id" ) || options . protyle . block . parentID ;
// @ts-ignore
const newHTML = options . protyle . lute [ options . type ] ( options . nodeElement . outerHTML , options . level ) ;
options . nodeElement . outerHTML = newHTML ;
2025-12-01 12:13:49 +08:00
if ( [ "CancelBlockquote" , "CancelList" , "CancelCallout" ] . includes ( options . type ) ) {
2024-06-05 22:50:53 +08:00
const tempElement = document . createElement ( "template" ) ;
tempElement . innerHTML = newHTML ;
const doOperations : IOperation [ ] = [ {
action : "delete" ,
id : options.id
} ] ;
const undoOperations : IOperation [ ] = [ ] ;
let tempPreviousId = previousId ;
Array . from ( tempElement . content . children ) . forEach ( ( item ) = > {
const tempId = item . getAttribute ( "data-node-id" ) ;
doOperations . push ( {
action : "insert" ,
data : item.outerHTML ,
id : tempId ,
previousID : tempPreviousId ,
parentID : parentId
} ) ;
undoOperations . push ( {
action : "delete" ,
id : tempId
} ) ;
tempPreviousId = tempId ;
} ) ;
undoOperations . push ( {
action : "insert" ,
data : oldHTML ,
id : options.id ,
previousID : previousId ,
parentID : parentId
} ) ;
transaction ( options . protyle , doOperations , undoOperations ) ;
} else {
updateTransaction ( options . protyle , options . id , newHTML , oldHTML ) ;
}
focusByWbr ( options . protyle . wysiwyg . element , getEditorRange ( options . protyle . wysiwyg . element ) ) ;
options . protyle . wysiwyg . element . querySelectorAll ( '[data-type~="block-ref"]' ) . forEach ( item = > {
if ( item . textContent === "" ) {
fetchPost ( "/api/block/getRefText" , { id : item.getAttribute ( "data-id" ) } , ( response ) = > {
item . innerHTML = response . data ;
} ) ;
}
} ) ;
blockRender ( options . protyle , options . protyle . wysiwyg . element ) ;
processRender ( options . protyle . wysiwyg . element ) ;
highlightRender ( options . protyle . wysiwyg . element ) ;
avRender ( options . protyle . wysiwyg . element , options . protyle ) ;
2024-06-07 00:19:09 +08:00
} ;
2024-06-05 22:50:53 +08:00
2023-06-09 21:39:49 +08:00
let transactionsTimeout : number ;
2022-05-26 15:18:53 +08:00
export const transaction = ( protyle : IProtyle , doOperations : IOperation [ ] , undoOperations? : IOperation [ ] ) = > {
2024-02-07 23:31:17 +08:00
if ( doOperations . length === 0 ) {
return ;
}
2023-09-24 17:53:28 +08:00
if ( ! protyle ) {
2024-04-25 12:19:24 +08:00
// 文档树中点开属性->数据库后的变更操作 & 文档树添加到数据库
2023-09-24 17:53:28 +08:00
fetchPost ( "/api/transactions" , {
session : Constants.SIYUAN_APPID ,
app : Constants.SIYUAN_APPID ,
transactions : [ {
doOperations
} ]
} ) ;
return ;
}
2022-05-26 15:18:53 +08:00
const lastTransaction = window . siyuan . transactions [ window . siyuan . transactions . length - 1 ] ;
let needDebounce = false ;
const time = new Date ( ) . getTime ( ) ;
if ( lastTransaction && lastTransaction . doOperations . length === 1 && lastTransaction . doOperations [ 0 ] . action === "update" &&
doOperations . length === 1 && doOperations [ 0 ] . action === "update" &&
lastTransaction . doOperations [ 0 ] . id === doOperations [ 0 ] . id &&
protyle . transactionTime - time < Constants . TIMEOUT_INPUT ) {
needDebounce = true ;
}
if ( undoOperations ) {
if ( window . siyuan . config . fileTree . openFilesUseCurrentTab && protyle . model ) {
protyle . model . headElement . classList . remove ( "item--unupdate" ) ;
}
protyle . updated = true ;
if ( needDebounce ) {
2024-02-02 11:52:53 +08:00
protyle . undo . replace ( doOperations , protyle ) ;
2022-05-26 15:18:53 +08:00
} else {
2024-02-02 11:52:53 +08:00
protyle . undo . add ( doOperations , undoOperations , protyle ) ;
2022-05-26 15:18:53 +08:00
}
}
2024-06-28 10:43:23 +08:00
// 加速折叠 https://github.com/siyuan-note/siyuan/issues/11828
2025-08-09 22:10:21 +08:00
if ( ( doOperations . length === 1 && (
2025-06-18 09:00:34 +08:00
doOperations [ 0 ] . action === "unfoldHeading" || doOperations [ 0 ] . action === "setAttrViewBlockView" ||
2024-06-28 10:43:23 +08:00
( doOperations [ 0 ] . action === "setAttrs" && doOperations [ 0 ] . data . startsWith ( '{"fold":' ) )
2025-08-09 22:10:21 +08:00
) ) || ( doOperations . length === 2 && doOperations [ 0 ] . action === "insertAttrViewBlock" ) ) {
2024-06-29 11:06:38 +08:00
// 防止 needDebounce 为 true
protyle . transactionTime = time + Constants . TIMEOUT_INPUT * 2 ;
2024-06-24 10:52:15 +08:00
fetchPost ( "/api/transactions" , {
session : protyle.id ,
app : Constants.SIYUAN_APPID ,
transactions : [ {
doOperations ,
undoOperations
} ]
} , ( response ) = > {
response . data [ 0 ] . doOperations . forEach ( ( operation : IOperation ) = > {
if ( operation . action === "unfoldHeading" || operation . action === "foldHeading" ) {
processFold ( operation , protyle ) ;
2024-06-28 10:43:23 +08:00
} else if ( operation . action === "setAttrs" ) {
const gutterFoldElement = protyle . gutter . element . querySelector ( '[data-type="fold"]' ) ;
if ( gutterFoldElement ) {
gutterFoldElement . removeAttribute ( "disabled" ) ;
}
// 仅在 alt+click 箭头折叠时才会触发
protyle . wysiwyg . element . querySelectorAll ( '[data-type="NodeBlockQueryEmbed"]' ) . forEach ( ( item ) = > {
if ( item . querySelector ( ` [data-node-id=" ${ operation . id } "] ` ) ) {
item . removeAttribute ( "data-render" ) ;
blockRender ( protyle , item ) ;
}
} ) ;
2024-06-24 10:52:15 +08:00
}
} ) ;
2024-07-06 09:57:00 +08:00
} ) ;
2024-06-24 10:52:15 +08:00
return ;
}
2025-08-10 17:49:07 +08:00
window . clearTimeout ( transactionsTimeout ) ;
2022-05-26 15:18:53 +08:00
if ( needDebounce ) {
// 不能覆盖 undoOperations https://github.com/siyuan-note/siyuan/issues/3727
window . siyuan . transactions [ window . siyuan . transactions . length - 1 ] . protyle = protyle ;
window . siyuan . transactions [ window . siyuan . transactions . length - 1 ] . doOperations = doOperations ;
} else {
window . siyuan . transactions . push ( {
protyle ,
doOperations ,
undoOperations
} ) ;
}
protyle . transactionTime = time ;
2023-06-09 21:39:49 +08:00
transactionsTimeout = window . setTimeout ( ( ) = > {
promiseTransaction ( ) ;
} , Constants . TIMEOUT_INPUT * 2 ) ;
2024-06-23 22:23:25 +08:00
// 插入块后会导致高度变化,从而产生再次定位 https://github.com/siyuan-note/siyuan/issues/11798
doOperations . find ( item = > {
if ( item . action === "insert" ) {
2024-07-06 09:57:00 +08:00
protyle . observerLoad ? . disconnect ( ) ;
2024-06-23 22:23:25 +08:00
return true ;
}
2024-07-06 09:57:00 +08:00
} ) ;
2022-05-26 15:18:53 +08:00
} ;
2024-06-24 10:52:15 +08:00
const processFold = ( operation : IOperation , protyle : IProtyle ) = > {
if ( operation . action === "unfoldHeading" || operation . action === "foldHeading" ) {
const gutterFoldElement = protyle . gutter . element . querySelector ( '[data-type="fold"]' ) ;
if ( gutterFoldElement ) {
gutterFoldElement . removeAttribute ( "disabled" ) ;
}
if ( operation . action === "unfoldHeading" ) {
const scrollTop = protyle . contentElement . scrollTop ;
protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) . forEach ( item = > {
2024-10-12 12:34:59 +08:00
const embedElement = isInEmbedBlock ( item ) ;
2024-10-09 00:16:03 +08:00
if ( embedElement ) {
embedElement . removeAttribute ( "data-render" ) ;
blockRender ( protyle , embedElement ) ;
2024-10-08 22:46:51 +08:00
return ;
}
2024-06-24 10:52:15 +08:00
if ( ! item . lastElementChild . classList . contains ( "protyle-attr" ) ) {
item . lastElementChild . remove ( ) ;
}
removeUnfoldRepeatBlock ( operation . retData , protyle ) ;
item . insertAdjacentHTML ( "afterend" , operation . retData ) ;
if ( operation . data === "remove" ) {
// https://github.com/siyuan-note/siyuan/issues/2188
const selection = getSelection ( ) ;
if ( selection . rangeCount > 0 && item . contains ( selection . getRangeAt ( 0 ) . startContainer ) ) {
focusBlock ( item . nextElementSibling , undefined , true ) ;
}
item . remove ( ) ;
}
} ) ;
if ( protyle . disabled ) {
disabledProtyle ( protyle ) ;
}
processRender ( protyle . wysiwyg . element ) ;
highlightRender ( protyle . wysiwyg . element ) ;
avRender ( protyle . wysiwyg . element , protyle ) ;
blockRender ( protyle , protyle . wysiwyg . element ) ;
2025-10-10 10:36:03 +08:00
if ( operation . context ? . focusId ) {
2025-10-09 20:02:53 +08:00
const focusElement = protyle . wysiwyg . element . querySelector ( ` [data-node-id=" ${ operation . context . focusId } "] ` ) ;
focusBlock ( focusElement ) ;
2025-11-09 10:46:35 +08:00
scrollCenter ( protyle , focusElement ) ;
2025-10-09 20:02:53 +08:00
} else {
protyle . contentElement . scrollTop = scrollTop ;
protyle . scroll . lastScrollTop = scrollTop ;
}
2024-06-24 10:52:15 +08:00
return ;
}
2024-10-09 00:16:03 +08:00
protyle . wysiwyg . element . querySelectorAll ( ` [data-node-id=" ${ operation . id } "] ` ) . forEach ( item = > {
2024-10-12 12:34:59 +08:00
const embedElement = isInEmbedBlock ( item ) ;
2024-10-09 00:16:03 +08:00
if ( embedElement ) {
embedElement . removeAttribute ( "data-render" ) ;
blockRender ( protyle , embedElement ) ;
}
} ) ;
2024-06-24 10:52:15 +08:00
// 折叠标题后未触发动态加载 https://github.com/siyuan-note/siyuan/issues/4168
if ( protyle . wysiwyg . element . lastElementChild . getAttribute ( "data-eof" ) !== "2" &&
! protyle . scroll . element . classList . contains ( "fn__none" ) &&
protyle . contentElement . scrollHeight - protyle . contentElement . scrollTop < protyle . contentElement . clientHeight * 2 // https://github.com/siyuan-note/siyuan/issues/7785
) {
fetchPost ( "/api/filetree/getDoc" , {
id : protyle.wysiwyg.element.lastElementChild.getAttribute ( "data-node-id" ) ,
mode : 2 ,
size : window.siyuan.config.editor.dynamicLoadBlocks ,
} , getResponse = > {
onGet ( {
data : getResponse ,
protyle ,
action : [ Constants . CB_GET_APPEND , Constants . CB_GET_UNCHANGEID ] ,
} ) ;
} ) ;
}
return ;
}
2024-07-06 09:57:00 +08:00
} ;
2024-06-24 10:52:15 +08:00
2022-05-26 15:18:53 +08:00
export const updateTransaction = ( protyle : IProtyle , id : string , newHTML : string , html : string ) = > {
if ( newHTML === html ) {
return ;
}
transaction ( protyle , [ {
id ,
data : newHTML ,
action : "update"
} ] , [ {
id ,
data : html ,
action : "update"
} ] ) ;
} ;
2022-08-01 11:59:22 +08:00
export const updateBatchTransaction = ( nodeElements : Element [ ] , protyle : IProtyle , cb : ( e : HTMLElement ) = > void ) = > {
const operations : IOperation [ ] = [ ] ;
const undoOperations : IOperation [ ] = [ ] ;
nodeElements . forEach ( ( element ) = > {
const id = element . getAttribute ( "data-node-id" ) ;
element . classList . remove ( "protyle-wysiwyg--select" ) ;
2022-10-22 11:31:41 +08:00
element . removeAttribute ( "select-start" ) ;
element . removeAttribute ( "select-end" ) ;
2022-08-01 11:59:22 +08:00
undoOperations . push ( {
action : "update" ,
id ,
data : element.outerHTML
} ) ;
cb ( element as HTMLElement ) ;
operations . push ( {
action : "update" ,
id ,
data : element.outerHTML
} ) ;
} ) ;
transaction ( protyle , operations , undoOperations ) ;
2022-08-02 11:15:18 +08:00
} ;