1
0
Fork 0
polaris-task-force/src/components/frontend/wiki/useEditorHistory.ts
Z8MB1E 55a03e8094 feat(wiki): add editor undo/redo, template dialog, highlighting, and formatting
- useEditorHistory: undo/redo stack over the textarea body
- WikiTemplateDialog: insert {{Template}} transclusions from the
  toolbar, with per-template {{param}} placeholder prompts
- WikiEditorHighlightLayer: mirrored overlay highlighting headings,
  directives, and template placeholders over the textarea
- editorHighlight.ts: tokenizer driving the overlay
- formatMarkdown.ts + createFormatUpdate: auto-format the document
- editorFormatting.ts: Tab/Shift+Tab indent/outdent, list
  continuation on Enter, inline bold/italic/code formatting, infobox
  snippet
- WikiEditor: editor and live preview side by side, preview renders
  template CSS/HTML via WikiPreparedBody
2026-09-06 05:32:51 -04:00

91 lines
2.6 KiB
TypeScript

"use client";
import { type RefObject, useCallback, useEffect, useRef } from "react";
export interface HistoryEntry {
readonly value: string;
readonly selectionStart: number;
readonly selectionEnd: number;
}
const COALESCE_WINDOW_MS = 400;
const COALESCE_MAX_DELTA = 8;
const MAX_HISTORY = 200;
function countLines(value: string): number {
let lines = 1;
for (let i = 0; i < value.length; i += 1) {
if (value.charCodeAt(i) === 10) lines += 1;
}
return lines;
}
export function useEditorHistory(
body: string,
bodyRef: RefObject<HTMLTextAreaElement | null>,
) {
const stackRef = useRef<HistoryEntry[]>([]);
const indexRef = useRef(-1);
const lastRecordAtRef = useRef(0);
if (stackRef.current.length === 0) {
stackRef.current = [{ value: body, selectionStart: 0, selectionEnd: 0 }];
indexRef.current = 0;
}
useEffect(() => {
const top = stackRef.current[indexRef.current];
if (top && top.value === body) return;
stackRef.current = [
{
value: body,
selectionStart: bodyRef.current?.selectionStart ?? 0,
selectionEnd: bodyRef.current?.selectionEnd ?? 0,
},
];
indexRef.current = 0;
lastRecordAtRef.current = 0;
}, [body, bodyRef]);
const record = useCallback(
(value: string, selectionStart: number, selectionEnd: number, coalesce: boolean) => {
const stack = stackRef.current;
const top = stack[indexRef.current];
const now = Date.now();
const canCoalesce =
coalesce &&
top !== undefined &&
now - lastRecordAtRef.current <= COALESCE_WINDOW_MS &&
Math.abs(value.length - top.value.length) <= COALESCE_MAX_DELTA &&
countLines(value) === countLines(top.value);
if (canCoalesce && top) {
stack[indexRef.current] = { value, selectionStart, selectionEnd };
} else {
stack.length = indexRef.current + 1;
stack.push({ value, selectionStart, selectionEnd });
if (stack.length > MAX_HISTORY) stack.shift();
indexRef.current = stack.length - 1;
}
lastRecordAtRef.current = now;
},
[],
);
const undo = useCallback((): HistoryEntry | null => {
if (indexRef.current <= 0) return null;
indexRef.current -= 1;
lastRecordAtRef.current = 0;
return stackRef.current[indexRef.current];
}, []);
const redo = useCallback((): HistoryEntry | null => {
const stack = stackRef.current;
if (indexRef.current >= stack.length - 1) return null;
indexRef.current += 1;
lastRecordAtRef.current = 0;
return stack[indexRef.current];
}, []);
return { record, undo, redo };
}