mirror of
https://github.com/docmost/docmost.git
synced 2026-08-30 02:25:01 +08:00
Show actual history changes
This commit is contained in:
committed by
Philipinho
parent
5cd0ba6902
commit
669ff0435f
@@ -1,4 +1,5 @@
|
|||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
|
|
||||||
export const historyAtoms = atom<boolean>(false);
|
export const historyAtoms = atom<boolean>(false);
|
||||||
export const activeHistoryIdAtom = atom<string>('');
|
export const activeHistoryIdAtom = atom<string>("");
|
||||||
|
export const activeHistoryPrevIdAtom = atom<string>("");
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diffSummary {
|
||||||
|
border: rem(1px) solid
|
||||||
|
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||||
|
border-radius: rem(10px);
|
||||||
|
padding: rem(12px);
|
||||||
|
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.history-diff-added) {
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.history-diff-added)::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
inset: 0;
|
||||||
|
left: rem(-12px);
|
||||||
|
right: 0;
|
||||||
|
border-left: rem(4px) solid
|
||||||
|
light-dark(var(--mantine-color-green-6), var(--mantine-color-green-4));
|
||||||
|
background: light-dark(var(--mantine-color-green-0), rgba(0, 255, 0, 0.06));
|
||||||
|
border-radius: rem(6px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.history-diff-deleted) {
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.history-diff-deleted)::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
inset: 0;
|
||||||
|
left: rem(-12px);
|
||||||
|
right: 0;
|
||||||
|
border-left: rem(4px) solid
|
||||||
|
light-dark(var(--mantine-color-red-6), var(--mantine-color-red-4));
|
||||||
|
border-top: rem(1px) dashed
|
||||||
|
light-dark(var(--mantine-color-red-4), var(--mantine-color-red-6));
|
||||||
|
border-right: rem(1px) dashed
|
||||||
|
light-dark(var(--mantine-color-red-4), var(--mantine-color-red-6));
|
||||||
|
border-bottom: rem(1px) dashed
|
||||||
|
light-dark(var(--mantine-color-red-4), var(--mantine-color-red-6));
|
||||||
|
background: light-dark(var(--mantine-color-red-0), rgba(255, 0, 0, 0.08));
|
||||||
|
border-radius: rem(6px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,31 +1,112 @@
|
|||||||
import '@/features/editor/styles/index.css';
|
import "@/features/editor/styles/index.css";
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import { EditorContent, useEditor } from '@tiptap/react';
|
import { EditorContent, useEditor } from "@tiptap/react";
|
||||||
import { mainExtensions } from '@/features/editor/extensions/extensions';
|
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
||||||
import { Title } from '@mantine/core';
|
import { Badge, Divider, Group, Text, Title } from "@mantine/core";
|
||||||
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
||||||
|
import { computeHistoryBlockDiff } from "@/features/page-history/utils/history-diff";
|
||||||
|
import classes from "./history-diff.module.css";
|
||||||
|
|
||||||
export interface HistoryEditorProps {
|
export interface HistoryEditorProps {
|
||||||
title: string;
|
title: string;
|
||||||
content: any;
|
content: any;
|
||||||
|
previousContent?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HistoryEditor({ title, content }: HistoryEditorProps) {
|
export function HistoryEditor({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
previousContent,
|
||||||
|
}: HistoryEditorProps) {
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: mainExtensions,
|
extensions: mainExtensions,
|
||||||
editable: false,
|
editable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [diffCounts, setDiffCounts] = useState<{ added: number; deleted: number }>({
|
||||||
|
added: 0,
|
||||||
|
deleted: 0,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editor && content) {
|
if (editor && content) {
|
||||||
editor.commands.setContent(content);
|
let decorationSet = DecorationSet.empty;
|
||||||
|
let addedCount = 0;
|
||||||
|
let deletedCount = 0;
|
||||||
|
|
||||||
|
if (previousContent) {
|
||||||
|
try {
|
||||||
|
const currentDoc = editor.schema.nodeFromJSON(content);
|
||||||
|
const prevDoc = editor.schema.nodeFromJSON(previousContent);
|
||||||
|
const {
|
||||||
|
diffDoc,
|
||||||
|
addedNodeRanges,
|
||||||
|
deletedNodeRanges,
|
||||||
|
addedCount: aCount,
|
||||||
|
deletedCount: dCount,
|
||||||
|
} = computeHistoryBlockDiff(currentDoc, prevDoc);
|
||||||
|
|
||||||
|
editor.commands.setContent(diffDoc.toJSON());
|
||||||
|
|
||||||
|
addedCount = aCount;
|
||||||
|
deletedCount = dCount;
|
||||||
|
|
||||||
|
const decos = addedNodeRanges.map((r) =>
|
||||||
|
Decoration.node(r.from, r.to, { class: "history-diff-added" }),
|
||||||
|
);
|
||||||
|
const deletedDecos = deletedNodeRanges.map((r) =>
|
||||||
|
Decoration.node(r.from, r.to, { class: "history-diff-deleted" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
decorationSet = DecorationSet.create(diffDoc, [...decos, ...deletedDecos]);
|
||||||
|
} catch {
|
||||||
|
decorationSet = DecorationSet.empty;
|
||||||
|
addedCount = 0;
|
||||||
|
deletedCount = 0;
|
||||||
|
editor.commands.setContent(content);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
editor.commands.setContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDiffCounts({ added: addedCount, deleted: deletedCount });
|
||||||
|
|
||||||
|
const existingEditorProps = editor.options.editorProps ?? {};
|
||||||
|
editor.setOptions({
|
||||||
|
editorProps: {
|
||||||
|
...existingEditorProps,
|
||||||
|
decorations: () => decorationSet,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [title, content, editor]);
|
}, [title, content, editor, previousContent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div className={classes.container}>
|
||||||
<Title order={1}>{title}</Title>
|
<Title order={1}>{title}</Title>
|
||||||
|
|
||||||
|
{previousContent && (
|
||||||
|
<>
|
||||||
|
<Divider my="md" />
|
||||||
|
<div className={classes.diffSummary}>
|
||||||
|
<Group gap="xs" wrap="wrap">
|
||||||
|
<Text fw={600}>Changes</Text>
|
||||||
|
<Badge variant="light" color="green">
|
||||||
|
+{diffCounts.added} added
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="light" color="red">
|
||||||
|
-{diffCounts.deleted} deleted
|
||||||
|
</Badge>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
(added = green, deleted = red/strikethrough)
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
<Divider my="md" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{editor && <EditorContent editor={editor} />}
|
{editor && <EditorContent editor={editor} />}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
import HistoryItem from "@/features/page-history/components/history-item";
|
import HistoryItem from "@/features/page-history/components/history-item";
|
||||||
import {
|
import {
|
||||||
activeHistoryIdAtom,
|
activeHistoryIdAtom,
|
||||||
|
activeHistoryPrevIdAtom,
|
||||||
historyAtoms,
|
historyAtoms,
|
||||||
} from "@/features/page-history/atoms/history-atoms";
|
} from "@/features/page-history/atoms/history-atoms";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
@@ -32,6 +33,7 @@ interface Props {
|
|||||||
function HistoryList({ pageId }: Props) {
|
function HistoryList({ pageId }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [activeHistoryId, setActiveHistoryId] = useAtom(activeHistoryIdAtom);
|
const [activeHistoryId, setActiveHistoryId] = useAtom(activeHistoryIdAtom);
|
||||||
|
const [, setActiveHistoryPrevId] = useAtom(activeHistoryPrevIdAtom);
|
||||||
const {
|
const {
|
||||||
data: pageHistoryList,
|
data: pageHistoryList,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -86,8 +88,9 @@ function HistoryList({ pageId }: Props) {
|
|||||||
!activeHistoryId
|
!activeHistoryId
|
||||||
) {
|
) {
|
||||||
setActiveHistoryId(pageHistoryList.items[0].id);
|
setActiveHistoryId(pageHistoryList.items[0].id);
|
||||||
|
setActiveHistoryPrevId(pageHistoryList.items[1]?.id ?? "");
|
||||||
}
|
}
|
||||||
}, [pageHistoryList]);
|
}, [pageHistoryList, activeHistoryId, setActiveHistoryId, setActiveHistoryPrevId]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <></>;
|
return <></>;
|
||||||
@@ -107,9 +110,14 @@ function HistoryList({ pageId }: Props) {
|
|||||||
{pageHistoryList &&
|
{pageHistoryList &&
|
||||||
pageHistoryList.items.map((historyItem, index) => (
|
pageHistoryList.items.map((historyItem, index) => (
|
||||||
<HistoryItem
|
<HistoryItem
|
||||||
key={index}
|
key={historyItem.id}
|
||||||
historyItem={historyItem}
|
historyItem={historyItem}
|
||||||
onSelect={setActiveHistoryId}
|
onSelect={(id) => {
|
||||||
|
setActiveHistoryId(id);
|
||||||
|
setActiveHistoryPrevId(
|
||||||
|
pageHistoryList.items[index + 1]?.id ?? "",
|
||||||
|
);
|
||||||
|
}}
|
||||||
isActive={historyItem.id === activeHistoryId}
|
isActive={historyItem.id === activeHistoryId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { ScrollArea } from "@mantine/core";
|
|||||||
import HistoryList from "@/features/page-history/components/history-list";
|
import HistoryList from "@/features/page-history/components/history-list";
|
||||||
import classes from "./history.module.css";
|
import classes from "./history.module.css";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { activeHistoryIdAtom } from "@/features/page-history/atoms/history-atoms";
|
import {
|
||||||
|
activeHistoryIdAtom,
|
||||||
|
activeHistoryPrevIdAtom,
|
||||||
|
} from "@/features/page-history/atoms/history-atoms";
|
||||||
import HistoryView from "@/features/page-history/components/history-view";
|
import HistoryView from "@/features/page-history/components/history-view";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
@@ -12,9 +15,13 @@ interface Props {
|
|||||||
|
|
||||||
export default function HistoryModalBody({ pageId }: Props) {
|
export default function HistoryModalBody({ pageId }: Props) {
|
||||||
const [activeHistoryId, setActiveHistoryId] = useAtom(activeHistoryIdAtom);
|
const [activeHistoryId, setActiveHistoryId] = useAtom(activeHistoryIdAtom);
|
||||||
|
const [activeHistoryPrevId, setActiveHistoryPrevId] = useAtom(
|
||||||
|
activeHistoryPrevIdAtom,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveHistoryId("");
|
setActiveHistoryId("");
|
||||||
|
setActiveHistoryPrevId("");
|
||||||
}, [pageId]);
|
}, [pageId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -25,9 +32,14 @@ export default function HistoryModalBody({ pageId }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<ScrollArea h="650" w="100%" scrollbarSize={5}>
|
<ScrollArea h={650} w="100%" scrollbarSize={5}>
|
||||||
<div className={classes.sidebarRightSection}>
|
<div className={classes.sidebarRightSection}>
|
||||||
{activeHistoryId && <HistoryView historyId={activeHistoryId} />}
|
{activeHistoryId && (
|
||||||
|
<HistoryView
|
||||||
|
historyId={activeHistoryId}
|
||||||
|
prevHistoryId={activeHistoryPrevId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,24 +4,38 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
interface HistoryProps {
|
interface HistoryProps {
|
||||||
historyId: string;
|
historyId: string;
|
||||||
|
prevHistoryId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function HistoryView({ historyId }: HistoryProps) {
|
function HistoryView({ historyId, prevHistoryId }: HistoryProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data, isLoading, isError } = usePageHistoryQuery(historyId);
|
const {
|
||||||
|
data,
|
||||||
|
isLoading: isLoadingCurrent,
|
||||||
|
isError: isErrorCurrent,
|
||||||
|
} = usePageHistoryQuery(historyId);
|
||||||
|
const {
|
||||||
|
data: prevData,
|
||||||
|
isLoading: isLoadingPrev,
|
||||||
|
isError: isErrorPrev,
|
||||||
|
} = usePageHistoryQuery(prevHistoryId ?? "");
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoadingCurrent || isLoadingPrev) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !data) {
|
if (isErrorCurrent || !data) {
|
||||||
return <div>{t("Error fetching page data.")}</div>;
|
return <div>{t("Error fetching page data.")}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
data && (
|
data && (
|
||||||
<div>
|
<div>
|
||||||
<HistoryEditor content={data.content} title={data.title} />
|
<HistoryEditor
|
||||||
|
content={data.content}
|
||||||
|
title={data.title}
|
||||||
|
previousContent={!isErrorPrev ? prevData?.content : undefined}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,5 +33,5 @@
|
|||||||
|
|
||||||
.sidebarRightSection {
|
.sidebarRightSection {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: rem(16px) rem(40px);
|
padding: rem(16px) rem(40px) rem(96px);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import type { Node as PMNode } from "@tiptap/pm/model";
|
||||||
|
|
||||||
|
function stableStringify(value: unknown): string {
|
||||||
|
if (value === null || typeof value !== "object") {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return `[${value.map(stableStringify).join(",")}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = value as Record<string, unknown>;
|
||||||
|
const keys = Object.keys(obj).sort();
|
||||||
|
return `{${keys
|
||||||
|
.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`)
|
||||||
|
.join(",")}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiffOp =
|
||||||
|
| { type: "equal"; aIndex: number; bIndex: number }
|
||||||
|
| { type: "insert"; bIndex: number }
|
||||||
|
| { type: "delete"; aIndex: number };
|
||||||
|
|
||||||
|
function myersDiff(a: string[], b: string[]): DiffOp[] {
|
||||||
|
const N = a.length;
|
||||||
|
const M = b.length;
|
||||||
|
const max = N + M;
|
||||||
|
|
||||||
|
let v = new Map<number, number>();
|
||||||
|
v.set(1, 0);
|
||||||
|
const trace: Array<Map<number, number>> = [];
|
||||||
|
|
||||||
|
for (let d = 0; d <= max; d += 1) {
|
||||||
|
const vNew = new Map<number, number>();
|
||||||
|
for (let k = -d; k <= d; k += 2) {
|
||||||
|
const vKMinus = v.get(k - 1) ?? 0;
|
||||||
|
const vKPlus = v.get(k + 1) ?? 0;
|
||||||
|
|
||||||
|
let x: number;
|
||||||
|
if (k === -d || (k !== d && vKMinus < vKPlus)) {
|
||||||
|
x = vKPlus;
|
||||||
|
} else {
|
||||||
|
x = vKMinus + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let y = x - k;
|
||||||
|
while (x < N && y < M && a[x] === b[y]) {
|
||||||
|
x += 1;
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
vNew.set(k, x);
|
||||||
|
|
||||||
|
if (x >= N && y >= M) {
|
||||||
|
trace.push(vNew);
|
||||||
|
return backtrack(trace, a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trace.push(vNew);
|
||||||
|
v = vNew;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function backtrack(trace: Array<Map<number, number>>, a: string[], b: string[]) {
|
||||||
|
let x = a.length;
|
||||||
|
let y = b.length;
|
||||||
|
const ops: DiffOp[] = [];
|
||||||
|
|
||||||
|
for (let d = trace.length - 1; d > 0; d -= 1) {
|
||||||
|
const v = trace[d];
|
||||||
|
const prevV = trace[d - 1];
|
||||||
|
|
||||||
|
const k = x - y;
|
||||||
|
|
||||||
|
const prevK =
|
||||||
|
k === -d || (k !== d && (prevV.get(k - 1) ?? 0) < (prevV.get(k + 1) ?? 0))
|
||||||
|
? k + 1
|
||||||
|
: k - 1;
|
||||||
|
|
||||||
|
const prevX = prevV.get(prevK) ?? 0;
|
||||||
|
const prevY = prevX - prevK;
|
||||||
|
|
||||||
|
while (x > prevX && y > prevY) {
|
||||||
|
ops.push({ type: "equal", aIndex: x - 1, bIndex: y - 1 });
|
||||||
|
x -= 1;
|
||||||
|
y -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x === prevX) {
|
||||||
|
ops.push({ type: "insert", bIndex: y - 1 });
|
||||||
|
y -= 1;
|
||||||
|
} else {
|
||||||
|
ops.push({ type: "delete", aIndex: x - 1 });
|
||||||
|
x -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (x > 0 && y > 0) {
|
||||||
|
ops.push({ type: "equal", aIndex: x - 1, bIndex: y - 1 });
|
||||||
|
x -= 1;
|
||||||
|
y -= 1;
|
||||||
|
}
|
||||||
|
while (x > 0) {
|
||||||
|
ops.push({ type: "delete", aIndex: x - 1 });
|
||||||
|
x -= 1;
|
||||||
|
}
|
||||||
|
while (y > 0) {
|
||||||
|
ops.push({ type: "insert", bIndex: y - 1 });
|
||||||
|
y -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ops.reverse();
|
||||||
|
return ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HistoryBlockDiffResult {
|
||||||
|
diffDoc: PMNode;
|
||||||
|
addedNodeRanges: Array<{ from: number; to: number }>;
|
||||||
|
deletedNodeRanges: Array<{ from: number; to: number }>;
|
||||||
|
addedCount: number;
|
||||||
|
deletedCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeHistoryBlockDiff(
|
||||||
|
currentDoc: PMNode,
|
||||||
|
prevDoc: PMNode,
|
||||||
|
): HistoryBlockDiffResult {
|
||||||
|
const currentTop = Array.from({ length: currentDoc.childCount }, (_, i) =>
|
||||||
|
currentDoc.child(i),
|
||||||
|
);
|
||||||
|
const prevTop = Array.from({ length: prevDoc.childCount }, (_, i) =>
|
||||||
|
prevDoc.child(i),
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentHashes = currentTop.map((n) => stableStringify(n.toJSON()));
|
||||||
|
const prevHashes = prevTop.map((n) => stableStringify(n.toJSON()));
|
||||||
|
|
||||||
|
const ops = myersDiff(prevHashes, currentHashes);
|
||||||
|
|
||||||
|
const nodes: PMNode[] = [];
|
||||||
|
const addedIndices: number[] = [];
|
||||||
|
const deletedIndices: number[] = [];
|
||||||
|
|
||||||
|
for (const op of ops) {
|
||||||
|
if (op.type === "equal") {
|
||||||
|
nodes.push(currentTop[op.bIndex]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (op.type === "insert") {
|
||||||
|
addedIndices.push(nodes.length);
|
||||||
|
nodes.push(currentTop[op.bIndex]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
deletedIndices.push(nodes.length);
|
||||||
|
nodes.push(prevTop[op.aIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffDoc = currentDoc.type.create(null, nodes);
|
||||||
|
|
||||||
|
const addedNodeRanges: Array<{ from: number; to: number }> = [];
|
||||||
|
const deletedNodeRanges: Array<{ from: number; to: number }> = [];
|
||||||
|
|
||||||
|
let pos = 0;
|
||||||
|
for (let i = 0; i < nodes.length; i += 1) {
|
||||||
|
const node = nodes[i];
|
||||||
|
const from = pos;
|
||||||
|
const to = pos + node.nodeSize;
|
||||||
|
if (addedIndices.includes(i)) addedNodeRanges.push({ from, to });
|
||||||
|
if (deletedIndices.includes(i)) deletedNodeRanges.push({ from, to });
|
||||||
|
pos = to;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
diffDoc,
|
||||||
|
addedNodeRanges,
|
||||||
|
deletedNodeRanges,
|
||||||
|
addedCount: addedIndices.length,
|
||||||
|
deletedCount: deletedIndices.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user