mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-03 04:31:46 +02:00
feat: add tiptap in diffviewer
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
span.diff-del {
|
||||
background-color: #fdb0c0;
|
||||
}
|
||||
|
||||
span.diff-ins {
|
||||
background-color: #cafffb;
|
||||
}
|
||||
|
||||
.diffviewer img {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
background-color: var(--bgSecondary);
|
||||
border-radius: 5px;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
export default class HTMLDiffer {
|
||||
constructor() {
|
||||
this.isReady = false;
|
||||
this.initializing = false;
|
||||
this.promiseQueue = [];
|
||||
}
|
||||
|
||||
async _initialize() {
|
||||
if (this.isReady) return;
|
||||
this.initializing = true;
|
||||
this.worker = new Worker("/diff.worker.js");
|
||||
this.isReady = true;
|
||||
this.initializing = false;
|
||||
}
|
||||
|
||||
_communicate(type, data, transferables = [], init = true) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (init) await this._initialize();
|
||||
const messageId = Math.random().toString(36).substr(2, 9);
|
||||
const onMessage = (e) => {
|
||||
const { type: _type, messageId: _mId, data } = e.data;
|
||||
if (_type === type && _mId === messageId) {
|
||||
this.worker.removeEventListener("message", onMessage);
|
||||
if (data.error) {
|
||||
console.error(data.error);
|
||||
return reject(data.error);
|
||||
}
|
||||
resolve(data);
|
||||
}
|
||||
};
|
||||
this.worker.addEventListener("message", onMessage);
|
||||
this.worker.postMessage(
|
||||
{
|
||||
type,
|
||||
data,
|
||||
messageId,
|
||||
},
|
||||
transferables
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
generate = (before, after) => {
|
||||
return this._communicate("generate", { before, after });
|
||||
};
|
||||
|
||||
clean = (html) => {
|
||||
return this._communicate("clean", { html });
|
||||
};
|
||||
}
|
||||
@@ -1,48 +1,20 @@
|
||||
import "./diff.css";
|
||||
import "../editor/plugins/attachmentshandler.css";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Flex, Box, Text, Button } from "rebass";
|
||||
import { Flex, Text, Button } from "rebass";
|
||||
import * as Icon from "../icons";
|
||||
import ContentToggle from "./content-toggle";
|
||||
import { store as notesStore } from "../../stores/note-store";
|
||||
import { db } from "../../common/db";
|
||||
import { useStore as useAppStore } from "../../stores/app-store";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
import { useStore as useEditorStore } from "../../stores/editor-store";
|
||||
import { hashNavigate } from "../../navigation";
|
||||
import HTMLDiffer from "./differ";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { ScrollSync, ScrollSyncPane } from "react-scroll-sync";
|
||||
import { injectCssSrc, removeCss } from "../../utils/css";
|
||||
import { EV, EVENTS } from "notes-core/common";
|
||||
|
||||
const differ = new HTMLDiffer();
|
||||
var conflicts = undefined;
|
||||
var currentConflict = undefined;
|
||||
|
||||
function navigateConflicts(prev) {
|
||||
if (!conflicts)
|
||||
conflicts = [
|
||||
...document.querySelectorAll("span.diff-ins"),
|
||||
...document.querySelectorAll("span.diff-del"),
|
||||
];
|
||||
let nextConflict;
|
||||
if (currentConflict) {
|
||||
const scrollTop = document.getElementById("diffViewAfter").scrollTop;
|
||||
nextConflict = conflicts.find((conflict) =>
|
||||
prev ? conflict.offsetTop < scrollTop : conflict.offsetTop > scrollTop
|
||||
);
|
||||
} else nextConflict = conflicts[0];
|
||||
if (!nextConflict) return false;
|
||||
currentConflict = nextConflict;
|
||||
currentConflict.scrollIntoView({ block: "center" });
|
||||
}
|
||||
import { Editor } from "../editor";
|
||||
|
||||
function DiffViewer(props) {
|
||||
const { noteId } = props;
|
||||
|
||||
const setIsEditorOpen = useAppStore((store) => store.setIsEditorOpen);
|
||||
const theme = useThemeStore((store) => store.theme);
|
||||
const sync = useAppStore((store) => store.sync);
|
||||
const clearSession = useEditorStore((store) => store.clearSession);
|
||||
const [conflictedNote, setConflictedNote] = useState();
|
||||
@@ -50,6 +22,7 @@ function DiffViewer(props) {
|
||||
const [localContent, setLocalContent] = useState();
|
||||
const [isDownloadingImages, setIsDownloadingImages] = useState(false);
|
||||
const [htmlDiff, setHtmlDiff] = useState({});
|
||||
const [selectedContent, setSelectedContent] = useState(-1);
|
||||
|
||||
const resolveConflict = useCallback(
|
||||
async ({ toKeep, toCopy, toKeepDateEdited, dateResolved }) => {
|
||||
@@ -125,39 +98,15 @@ function DiffViewer(props) {
|
||||
setLocalContent({ ...content, conflicted: false });
|
||||
setRemoteContent(content.conflicted);
|
||||
|
||||
differ
|
||||
.generate(content.data, content.conflicted.data)
|
||||
.then(async ({ before, after }) => {
|
||||
setHtmlDiff({ before, after });
|
||||
conflicts = undefined;
|
||||
currentConflict = undefined;
|
||||
});
|
||||
|
||||
setHtmlDiff({ before: content.data, after: content.conflicted.data });
|
||||
|
||||
conflicts = undefined;
|
||||
currentConflict = undefined;
|
||||
})();
|
||||
}, [noteId, resolveConflict]);
|
||||
|
||||
useEffect(() => {
|
||||
let cssPath = "";
|
||||
if (theme === "dark")
|
||||
cssPath = "/skins/notesnook-dark/content.inline.min.css";
|
||||
else cssPath = "/skins/notesnook/content.inline.min.css";
|
||||
injectCssSrc("tmce", cssPath);
|
||||
return () => {
|
||||
removeCss("tmce");
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
clearSession(false);
|
||||
setIsEditorOpen(true);
|
||||
}, [setIsEditorOpen, clearSession]);
|
||||
|
||||
const [selectedContent, setSelectedContent] = useState(-1);
|
||||
|
||||
if (!conflictedNote || !localContent || !remoteContent) return null;
|
||||
return (
|
||||
<Flex
|
||||
@@ -181,21 +130,6 @@ function DiffViewer(props) {
|
||||
{conflictedNote.title}
|
||||
</Text>
|
||||
<Flex alignSelf="center" justifySelf="center" mt={1}>
|
||||
<Button
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
variant="tool"
|
||||
mr={2}
|
||||
onClick={() => {
|
||||
navigateConflicts(true);
|
||||
}}
|
||||
>
|
||||
<Icon.ArrowLeft size={18} />
|
||||
<Text display={["none", "block", "block"]} fontSize="body" ml={1}>
|
||||
Prev conflict
|
||||
</Text>
|
||||
</Button>
|
||||
<Button
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
@@ -203,28 +137,19 @@ function DiffViewer(props) {
|
||||
variant="tool"
|
||||
onClick={async () => {
|
||||
setIsDownloadingImages(true);
|
||||
const event = EV.subscribe(
|
||||
EVENTS.mediaAttachmentDownloaded,
|
||||
({ hash, src }) => {
|
||||
const elements = document.querySelectorAll(
|
||||
`.diffviewer img[data-hash="${hash}"]`
|
||||
);
|
||||
if (!elements || !elements.length) return;
|
||||
for (let element of elements) element.setAttribute("src", src);
|
||||
}
|
||||
);
|
||||
try {
|
||||
await db.content.downloadMedia(noteId, {
|
||||
data: htmlDiff.before,
|
||||
type: localContent.type,
|
||||
});
|
||||
await db.content.downloadMedia(noteId, {
|
||||
data: htmlDiff.after,
|
||||
type: remoteContent.type,
|
||||
});
|
||||
await Promise.all([
|
||||
db.content.downloadMedia(noteId, {
|
||||
data: htmlDiff.before,
|
||||
type: localContent.type,
|
||||
}),
|
||||
db.content.downloadMedia(noteId, {
|
||||
data: htmlDiff.after,
|
||||
type: remoteContent.type,
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
setIsDownloadingImages(false);
|
||||
event.unsubscribe();
|
||||
}
|
||||
}}
|
||||
disabled={isDownloadingImages}
|
||||
@@ -239,20 +164,6 @@ function DiffViewer(props) {
|
||||
{isDownloadingImages ? "Downloading..." : "Load images"}
|
||||
</Text>
|
||||
</Button>
|
||||
<Button
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
variant="tool"
|
||||
onClick={() => {
|
||||
navigateConflicts();
|
||||
}}
|
||||
>
|
||||
<Text display={["none", "block", "block"]} fontSize="body" mr={1}>
|
||||
Next conflict
|
||||
</Text>
|
||||
<Icon.ArrowRight size={18} />
|
||||
</Button>
|
||||
</Flex>
|
||||
<ScrollSync>
|
||||
<Flex
|
||||
@@ -291,20 +202,24 @@ function DiffViewer(props) {
|
||||
}}
|
||||
/>
|
||||
<ScrollSyncPane>
|
||||
<Box
|
||||
id="diffViewBefore"
|
||||
px={2}
|
||||
flex={1}
|
||||
overflowY="auto"
|
||||
<Flex
|
||||
sx={{
|
||||
px: 2,
|
||||
overflowY: "auto",
|
||||
flex: 1,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 0,
|
||||
borderRightWidth: [0, 0, 1],
|
||||
borderBottomWidth: [1, 1, 0],
|
||||
borderColor: "border",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: htmlDiff.before }}
|
||||
/>
|
||||
>
|
||||
<Editor
|
||||
content={htmlDiff.before}
|
||||
nonce={0}
|
||||
options={{ readonly: true, headless: true }}
|
||||
/>
|
||||
</Flex>
|
||||
</ScrollSyncPane>
|
||||
</Flex>
|
||||
<Flex
|
||||
@@ -340,12 +255,13 @@ function DiffViewer(props) {
|
||||
}}
|
||||
/>
|
||||
<ScrollSyncPane>
|
||||
<Box
|
||||
id="diffViewAfter"
|
||||
px={2}
|
||||
overflowY="auto"
|
||||
dangerouslySetInnerHTML={{ __html: htmlDiff.after }}
|
||||
/>
|
||||
<Flex sx={{ px: 2 }}>
|
||||
<Editor
|
||||
content={htmlDiff.after}
|
||||
nonce={0}
|
||||
options={{ readonly: true, headless: true }}
|
||||
/>
|
||||
</Flex>
|
||||
</ScrollSyncPane>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { useEffect, useCallback, useState, useRef } from "react";
|
||||
import {
|
||||
useEffect,
|
||||
useCallback,
|
||||
useState,
|
||||
useRef,
|
||||
PropsWithChildren,
|
||||
} from "react";
|
||||
import { Box, Button, Flex, Text } from "rebass";
|
||||
import Properties from "../properties";
|
||||
import { useStore, store as editorstore } from "../../stores/editor-store";
|
||||
@@ -132,8 +138,10 @@ export default function EditorManager({
|
||||
nonce={timestamp}
|
||||
title={title}
|
||||
content={content}
|
||||
readonly={isReadonly}
|
||||
onRequestFocus={() => toggleProperties(false)}
|
||||
options={{
|
||||
readonly: isReadonly,
|
||||
onRequestFocus: () => toggleProperties(false),
|
||||
}}
|
||||
/>
|
||||
{arePropertiesVisible && <Properties />}
|
||||
<DropZone overlayRef={overlayRef} />
|
||||
@@ -141,18 +149,27 @@ export default function EditorManager({
|
||||
);
|
||||
}
|
||||
|
||||
type EditorProps = {
|
||||
title: string;
|
||||
type EditorOptions = {
|
||||
headless?: boolean;
|
||||
readonly?: boolean;
|
||||
focusMode?: boolean;
|
||||
nonce?: number;
|
||||
content: string;
|
||||
onRequestFocus?: () => void;
|
||||
};
|
||||
function Editor(props: EditorProps) {
|
||||
const { content, readonly, focusMode, onRequestFocus, title, nonce } = props;
|
||||
type EditorProps = {
|
||||
title?: string;
|
||||
nonce?: number;
|
||||
content: string;
|
||||
options?: EditorOptions;
|
||||
};
|
||||
export function Editor(props: EditorProps) {
|
||||
const { content, nonce, options } = props;
|
||||
const { readonly, headless } = options || {
|
||||
headless: false,
|
||||
readonly: false,
|
||||
focusMode: false,
|
||||
};
|
||||
|
||||
const editor = useEditorInstance();
|
||||
const isMobile = useMobile();
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
@@ -190,6 +207,44 @@ function Editor(props: EditorProps) {
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<EditorChrome {...props}>
|
||||
<Tiptap
|
||||
nonce={nonce}
|
||||
readonly={readonly}
|
||||
toolbarContainerId={headless ? undefined : "editorToolbar"}
|
||||
content={content}
|
||||
onChange={(content, counter) => {
|
||||
const { id, sessionId } = editorstore.get().session;
|
||||
debouncedOnEditorChange(sessionId, id, sessionId, content);
|
||||
if (counter) debouncedUpdateWordCount(counter);
|
||||
}}
|
||||
onDownloadAttachment={(attachment) =>
|
||||
downloadAttachment(attachment.hash)
|
||||
}
|
||||
onInsertAttachment={(type) => {
|
||||
const mime = type === "file" ? "*/*" : "image/*";
|
||||
insertAttachment(mime).then((file) => {
|
||||
if (!file) return;
|
||||
editor?.attachFile(file);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EditorChrome>
|
||||
);
|
||||
}
|
||||
|
||||
function EditorChrome(props: PropsWithChildren<EditorProps>) {
|
||||
const { title, nonce, options, children } = props;
|
||||
const { readonly, focusMode, headless, onRequestFocus } = options || {
|
||||
headless: false,
|
||||
readonly: false,
|
||||
focusMode: false,
|
||||
};
|
||||
const isMobile: boolean | null = useMobile();
|
||||
|
||||
if (headless) return <>{children}</>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbar />
|
||||
@@ -223,37 +278,19 @@ function Editor(props: EditorProps) {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Titlebox
|
||||
nonce={nonce}
|
||||
readonly={readonly || false}
|
||||
setTitle={(title) => {
|
||||
const { sessionId, id } = editorstore.get().session;
|
||||
debouncedOnTitleChange(sessionId, id, title);
|
||||
}}
|
||||
title={title}
|
||||
/>
|
||||
{title !== undefined ? (
|
||||
<Titlebox
|
||||
nonce={nonce}
|
||||
readonly={readonly || false}
|
||||
setTitle={(title) => {
|
||||
const { sessionId, id } = editorstore.get().session;
|
||||
debouncedOnTitleChange(sessionId, id, title);
|
||||
}}
|
||||
title={title}
|
||||
/>
|
||||
) : null}
|
||||
<Header readonly={readonly} />
|
||||
<Tiptap
|
||||
nonce={nonce}
|
||||
readonly={readonly}
|
||||
toolbarContainerId="editorToolbar"
|
||||
content={content}
|
||||
onChange={(content, counter) => {
|
||||
const { id, sessionId } = editorstore.get().session;
|
||||
debouncedOnEditorChange(sessionId, id, sessionId, content);
|
||||
if (counter) debouncedUpdateWordCount(counter);
|
||||
}}
|
||||
onDownloadAttachment={(attachment) =>
|
||||
downloadAttachment(attachment.hash)
|
||||
}
|
||||
onInsertAttachment={(type) => {
|
||||
const mime = type === "file" ? "*/*" : "image/*";
|
||||
insertAttachment(mime).then((file) => {
|
||||
if (!file) return;
|
||||
editor?.attachFile(file);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</Flex>
|
||||
</FlexScrollContainer>
|
||||
|
||||
|
||||
@@ -100,6 +100,8 @@ function TipTap(props: TipTapProps) {
|
||||
if (isSearching && !isEditorSearching) toggleSearch();
|
||||
}, [toggleSearch, editor?.storage.searchreplace?.isSearching]);
|
||||
|
||||
if (!toolbarContainerId) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Portal containerId={toolbarContainerId}>
|
||||
|
||||
@@ -24,10 +24,10 @@ function Home() {
|
||||
store.refresh();
|
||||
setIsLoading(false);
|
||||
}
|
||||
// const note = db.notes.note("f90f344ee3c13c2f686bd5c1").data;
|
||||
// const note = db.notes.note("62bc3f28a1a1a10000707077").data;
|
||||
// const data = await db.content.raw(note.contentId);
|
||||
|
||||
// const note2 = db.notes.note("3e9a515cc63199a101ec49bb").data;
|
||||
// const note2 = db.notes.note("62bc3f1ca1a1a10000707075").data;
|
||||
// const data2 = await db.content.raw(note2.contentId);
|
||||
|
||||
// const data3 = { ...data, conflicted: data2 };
|
||||
|
||||
Reference in New Issue
Block a user