Fix Notepad synchronization across pages and external updates
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s
This commit is contained in:
parent
898fd5286f
commit
b0e340efc7
7 changed files with 148 additions and 77 deletions
|
|
@ -14,14 +14,13 @@ import {
|
|||
TEXT_COLORS,
|
||||
formatUpdatedAt,
|
||||
getPlainText,
|
||||
getSelectionTextOffset,
|
||||
isHtmlEmpty,
|
||||
queryBlockFormat,
|
||||
queryCommandState,
|
||||
restoreSelectionTextOffset
|
||||
queryCommandState
|
||||
} from "./notepadUtils.js";
|
||||
|
||||
const EMPTY_NOTEPAD_DATA = { text: "" };
|
||||
const EMPTY_FORMATS = { bold: false, italic: false, underline: false, strikeThrough: false, heading: false, bulletList: false, numberList: false };
|
||||
|
||||
function readTemporaryStrokes(sessionKey) {
|
||||
try {
|
||||
|
|
@ -41,9 +40,9 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
|
|||
const activePage = normalizedData.pages.find((page) => page.id === normalizedData.activePageId) || normalizedData.pages[0];
|
||||
const editorRef = useRef(null);
|
||||
const surfaceRef = useRef(null);
|
||||
const selectionOffsetRef = useRef(null);
|
||||
const localSaveUpdatedAtRef = useRef("");
|
||||
const pageKeyRef = useRef(`${toolboxId}:${moduleId}:${activePage.id}`);
|
||||
const pageKey = `${toolboxId}:${moduleId}:${activePage.id}`;
|
||||
const [editorFocused, setEditorFocused] = useState(false);
|
||||
const [previousSource, setPreviousSource] = useState({ pageKey, page: activePage, focused: false });
|
||||
const temporaryStrokesDirtyRef = useRef(false);
|
||||
const sessionStorageFailureRef = useRef("");
|
||||
const [html, setHtml] = useState(activePage.html);
|
||||
|
|
@ -58,52 +57,40 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
|
|||
const [highlightColor, setHighlightColor] = useState(HIGHLIGHT_COLORS[0]);
|
||||
const [editingPageId, setEditingPageId] = useState("");
|
||||
const [pageTitleDrafts, setPageTitleDrafts] = useState(() => Object.fromEntries(normalizedData.pages.map((page) => [page.id, page.title])));
|
||||
const [activeFormats, setActiveFormats] = useState({
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
strikeThrough: false,
|
||||
heading: false,
|
||||
bulletList: false,
|
||||
numberList: false
|
||||
});
|
||||
const [activeFormats, setActiveFormats] = useState(EMPTY_FORMATS);
|
||||
const sessionKey = `sokkog:notepad-drawings:${toolboxId}:${moduleId}:${activePage.id}`;
|
||||
const [temporaryStrokes, setTemporaryStrokes] = useKeyedState(sessionKey, readTemporaryStrokes);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
notepadDataRef.current = normalizedData;
|
||||
}, [normalizedData]);
|
||||
|
||||
useEffect(() => {
|
||||
const pageKey = `${toolboxId}:${moduleId}:${activePage.id}`;
|
||||
const pageChanged = pageKeyRef.current !== pageKey;
|
||||
if (pageChanged) {
|
||||
pageKeyRef.current = pageKey;
|
||||
localSaveUpdatedAtRef.current = "";
|
||||
}
|
||||
if (!pageChanged && document.activeElement === editorRef.current) return;
|
||||
if (activePage.updatedAt && activePage.updatedAt === localSaveUpdatedAtRef.current) return;
|
||||
const editor = editorRef.current;
|
||||
if (document.activeElement === editor && getPlainText(editor) === activePage.text) {
|
||||
const pageChanged = previousSource.pageKey !== pageKey;
|
||||
const sourceChanged = ["html", "text", "updatedAt", "drawingMode"].some((field) => previousSource.page[field] !== activePage[field]);
|
||||
if (pageChanged || sourceChanged || previousSource.focused !== editorFocused) {
|
||||
setPreviousSource({ pageKey, page: activePage, focused: editorFocused });
|
||||
// Une page différente remplace l'éditeur ; une mise à jour de la page courante attend le blur.
|
||||
if (pageChanged || !editorFocused) {
|
||||
setHtml(activePage.html);
|
||||
setText(activePage.text);
|
||||
setUpdatedAt(activePage.updatedAt);
|
||||
setDrawingMode(activePage.drawingMode);
|
||||
return;
|
||||
if (pageChanged || activePage.html !== html || activePage.text !== text || activePage.drawingMode !== drawingMode) {
|
||||
setDrawingActive(false);
|
||||
setEraseActive(false);
|
||||
}
|
||||
}
|
||||
setHtml(activePage.html);
|
||||
setText(activePage.text);
|
||||
setUpdatedAt(activePage.updatedAt);
|
||||
setDrawingMode(activePage.drawingMode);
|
||||
setDrawingActive(false);
|
||||
setEraseActive(false);
|
||||
}, [toolboxId, moduleId, activePage.id, activePage.html, activePage.text, activePage.updatedAt, activePage.drawingMode]);
|
||||
if (pageChanged) {
|
||||
setEditorFocused(false);
|
||||
setActiveFormats(EMPTY_FORMATS);
|
||||
}
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!editorRef.current || editorRef.current.innerHTML === html) return;
|
||||
if (document.activeElement === editorRef.current) return;
|
||||
editorRef.current.innerHTML = html;
|
||||
restoreSelectionTextOffset(editorRef.current, selectionOffsetRef.current);
|
||||
}, [html, text]);
|
||||
}, [html, text, pageKey, editorFocused]);
|
||||
|
||||
useEffect(() => {
|
||||
function updateActiveFormats() {
|
||||
|
|
@ -167,7 +154,6 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
|
|||
|
||||
function saveContent(nextHtml, nextText) {
|
||||
const nextUpdatedAt = new Date().toISOString();
|
||||
localSaveUpdatedAtRef.current = nextUpdatedAt;
|
||||
setHtml(nextHtml);
|
||||
setText(nextText);
|
||||
setUpdatedAt(nextUpdatedAt);
|
||||
|
|
@ -214,7 +200,6 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
|
|||
}
|
||||
|
||||
function handleEditorInput() {
|
||||
selectionOffsetRef.current = getSelectionTextOffset(editorRef.current);
|
||||
const nextHtml = editorRef.current?.innerHTML || "";
|
||||
const nextText = getPlainText(editorRef.current);
|
||||
saveContent(nextHtml, nextText);
|
||||
|
|
@ -421,6 +406,7 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
|
|||
|
||||
<div className={`notepad-surface ${drawingActive ? "is-drawing" : ""}`} ref={surfaceRef}>
|
||||
<div
|
||||
key={pageKey}
|
||||
ref={editorRef}
|
||||
className={`notepad-editor ${isHtmlEmpty(html, text) ? "is-empty" : ""}`}
|
||||
contentEditable={!drawingActive}
|
||||
|
|
@ -428,6 +414,8 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
|
|||
role="textbox"
|
||||
aria-multiline="true"
|
||||
data-placeholder={textContent.placeholder || "Bloc Note..."}
|
||||
onFocus={() => setEditorFocused(true)}
|
||||
onBlur={() => setEditorFocused(false)}
|
||||
onInput={handleEditorInput}
|
||||
onPaste={handlePaste}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,43 +6,6 @@ export function getPlainText(element) {
|
|||
return (element?.innerText || "").replace(/\n{3,}/g, "\n\n").trim();
|
||||
}
|
||||
|
||||
export function getSelectionTextOffset(container) {
|
||||
const selection = document.getSelection();
|
||||
if (!container || !selection?.rangeCount || !container.contains(selection.anchorNode)) return null;
|
||||
const range = selection.getRangeAt(0);
|
||||
const prefix = range.cloneRange();
|
||||
prefix.selectNodeContents(container);
|
||||
prefix.setEnd(range.startContainer, range.startOffset);
|
||||
return prefix.toString().length;
|
||||
}
|
||||
|
||||
export function restoreSelectionTextOffset(container, offset) {
|
||||
if (!container || offset == null) return;
|
||||
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
||||
let remaining = offset;
|
||||
let node = walker.nextNode();
|
||||
while (node) {
|
||||
const length = node.textContent.length;
|
||||
if (remaining <= length) {
|
||||
const range = document.createRange();
|
||||
range.setStart(node, remaining);
|
||||
range.collapse(true);
|
||||
const selection = document.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
return;
|
||||
}
|
||||
remaining -= length;
|
||||
node = walker.nextNode();
|
||||
}
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(container);
|
||||
range.collapse(false);
|
||||
const selection = document.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
export function isHtmlEmpty(html, text) {
|
||||
return !text && !String(html || "").replace(/<br\s*\/?>/gi, "").replace(/<[^>]*>/g, "").replace(/ /gi, " ").trim();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue