feat: add tiptap in diffviewer

This commit is contained in:
thecodrr
2022-06-30 11:14:24 +05:00
parent 2b2da1609d
commit ad7e85bfad
6 changed files with 112 additions and 221 deletions

View File

@@ -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;
}

View File

@@ -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 });
};
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}>

View File

@@ -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 };