Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion app/src/protyle/undo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Constants} from "../../constants";
import {hideElements} from "../ui/hideElements";
import {scrollCenter} from "../../util/highlightById";
import {matchHotKey} from "../util/hotKey";
import {fetchSyncPost} from "../../util/fetch";
import {ipcRenderer} from "electron";

interface IOperations {
Expand Down Expand Up @@ -64,9 +65,72 @@ export class Undo {
}
}

private render(protyle: IProtyle, state: IOperations, redo: boolean) {
// 重放 insert 操作前检查块 ID 是否已被占用(例如剪切后粘贴到其他编辑器时保留了原 ID,
// 此时撤销剪切会插入重复 ID 的块),冲突时在两个栈中统一替换为新 ID
private async resolveDuplicateIds(operations: IOperation[]) {
const ids = new Set<string>();
operations.forEach(op => {
if (op.action === "insert" && typeof op.data === "string") {
if (op.id) {
ids.add(op.id);
}
op.data.match(/data-node-id="[^"]+"/g)?.forEach((match: string) => {
ids.add(match.substring(14, match.length - 1));
});
}
});
if (ids.size === 0) {
return;
}
let existResponse: IWebSocketData;
try {
existResponse = await fetchSyncPost("/api/block/checkBlocksExist", {ids: Array.from(ids)});
} catch (e) {
return;
}
if (!existResponse?.data) {
return;
}
const replacements: [string, string][] = [];
ids.forEach(id => {
if (existResponse.data[id] === true) {
replacements.push([id, Lute.NewNodeID()]);
}
});
if (replacements.length === 0) {
return;
}
[this.undoStack, this.redoStack].forEach(stack => {
stack.forEach(item => {
[item.doOperations, item.undoOperations].forEach(ops => {
ops.forEach(op => {
replacements.forEach(([oldId, newId]) => {
if (op.id === oldId) {
op.id = newId;
}
if (op.parentID === oldId) {
op.parentID = newId;
}
if (op.previousID === oldId) {
op.previousID = newId;
}
if (op.nextID === oldId) {
op.nextID = newId;
}
if (typeof op.data === "string") {
op.data = op.data.split(`data-node-id="${oldId}"`).join(`data-node-id="${newId}"`);
}
});
});
});
});
});
}

private async render(protyle: IProtyle, state: IOperations, redo: boolean) {
hideElements(["hint", "gutter"], protyle);
protyle.wysiwyg.lastHTMLs = {};
await this.resolveDuplicateIds(redo ? state.doOperations : state.undoOperations);
if (!redo) {
Comment on lines +93 to 97
for (let i = state.undoOperations.length - 1; i >= 0; i--) {
if (state.undoOperations[i].action === "insert") {
Expand Down
23 changes: 17 additions & 6 deletions app/src/protyle/util/paste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {hasClosestBlock, hasClosestByAttribute, hasClosestByClassName} from "./h
import {getEditorRange} from "./selection";
import {blockRender} from "../render/blockRender";
import {highlightRender} from "../render/highlightRender";
import {fetchPost} from "../../util/fetch";
import {fetchPost, fetchSyncPost} from "../../util/fetch";
import {isDynamicRef, isFileAnnotation} from "../../util/functions";
import {insertHTML} from "./insertHTML";
import {scrollCenter} from "../../util/highlightById";
Expand Down Expand Up @@ -457,12 +457,23 @@ export const paste = async (protyle: IProtyle, event: (ClipboardEvent | DragEven
}
}
let isBlock = false;
tempElement.querySelectorAll("[data-node-id]").forEach((e) => {
const newId = Lute.NewNodeID();
e.setAttribute("data-node-id", newId);
clearBlockElement(e);
const pastedBlockElements = tempElement.querySelectorAll("[data-node-id]");
if (pastedBlockElements.length > 0) {
isBlock = true;
});
// 剪切后粘贴时原块已被删除,保留原 ID 可避免该块被其他位置的引用失效;
// 仅当 ID 仍存在(复制粘贴)时才生成新 ID
const oldIds: string[] = [];
pastedBlockElements.forEach((e) => {
oldIds.push(e.getAttribute("data-node-id"));
});
const existResponse = await fetchSyncPost("/api/block/checkBlocksExist", {ids: oldIds});
pastedBlockElements.forEach((e) => {
if (existResponse.data[e.getAttribute("data-node-id")] !== false) {
e.setAttribute("data-node-id", Lute.NewNodeID());
}
clearBlockElement(e);
});
Comment on lines +490 to +500
}
if (nodeElement.classList.contains("table")) {
isBlock = false;
}
Expand Down
20 changes: 20 additions & 0 deletions kernel/api/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"strings"

"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/html"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/logging"
Expand Down Expand Up @@ -382,6 +383,25 @@ func checkBlockExist(c *gin.Context) {
ret.Data = nil != b
}

func checkBlocksExist(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)

arg, ok := util.JsonArg(c, ret)
if !ok {
return
}

idsArg := arg["ids"].([]interface{})
var ids []string
for _, idArg := range idsArg {
if id, idOk := idArg.(string); idOk && ast.IsNodeIDPattern(id) {
ids = append(ids, id)
}
}
ret.Data = treenode.ExistBlockTrees(ids)
Comment on lines +389 to +396
}

func getDocInfo(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
Expand Down
1 change: 1 addition & 0 deletions kernel/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ func ServeAPI(ginServer *gin.Engine) {
ginServer.Handle("POST", "/api/block/getDocInfo", model.CheckAuth, getDocInfo)
ginServer.Handle("POST", "/api/block/getDocsInfo", model.CheckAuth, getDocsInfo)
ginServer.Handle("POST", "/api/block/checkBlockExist", model.CheckAuth, checkBlockExist)
ginServer.Handle("POST", "/api/block/checkBlocksExist", model.CheckAuth, checkBlocksExist)
ginServer.Handle("POST", "/api/block/getUnfoldedParentID", model.CheckAuth, getUnfoldedParentID)
ginServer.Handle("POST", "/api/block/checkBlockFold", model.CheckAuth, checkBlockFold)
ginServer.Handle("POST", "/api/block/insertBlock", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, insertBlock)
Expand Down
Loading