ref cleanup
This commit is contained in:
@@ -309,7 +309,7 @@ export function LeftBar() {
|
|||||||
{/* Hamburger menu button - positioned at right edge of navbar */}
|
{/* Hamburger menu button - positioned at right edge of navbar */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setLeftBarVisible(!leftBarVisible())}
|
onClick={() => setLeftBarVisible(!leftBarVisible())}
|
||||||
class="hamburger-menu-btn absolute top-4 -right-14 z-10 rounded-md p-2 shadow-md backdrop-blur-2xl transition-transform duration-600 ease-in-out hover:scale-110"
|
class="hamburger-menu-btn absolute top-4 -right-14 z-200 rounded-md p-2 shadow-md backdrop-blur-2xl transition-transform duration-600 ease-in-out hover:scale-110"
|
||||||
classList={{
|
classList={{
|
||||||
hidden: leftBarVisible()
|
hidden: leftBarVisible()
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ mermaid.initialize({
|
|||||||
|
|
||||||
export default function MermaidRenderer() {
|
export default function MermaidRenderer() {
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Find all mermaid diagrams and render them
|
|
||||||
const mermaidPres = document.querySelectorAll('pre[data-type="mermaid"]');
|
const mermaidPres = document.querySelectorAll('pre[data-type="mermaid"]');
|
||||||
|
|
||||||
mermaidPres.forEach(async (pre, index) => {
|
mermaidPres.forEach(async (pre, index) => {
|
||||||
|
|||||||
@@ -99,8 +99,6 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
|||||||
const processReferences = () => {
|
const processReferences = () => {
|
||||||
if (!contentRef) return;
|
if (!contentRef) return;
|
||||||
|
|
||||||
const foundRefs = new Map<string, HTMLElement>();
|
|
||||||
|
|
||||||
const supElements = contentRef.querySelectorAll("sup");
|
const supElements = contentRef.querySelectorAll("sup");
|
||||||
|
|
||||||
supElements.forEach((sup) => {
|
supElements.forEach((sup) => {
|
||||||
@@ -278,6 +276,7 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
|||||||
return (
|
return (
|
||||||
<div class="mx-auto max-w-4xl px-4 pt-32 md:pt-40">
|
<div class="mx-auto max-w-4xl px-4 pt-32 md:pt-40">
|
||||||
<div
|
<div
|
||||||
|
id="post-content-body"
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
class="text-text prose dark:prose-invert max-w-none"
|
class="text-text prose dark:prose-invert max-w-none"
|
||||||
innerHTML={props.body}
|
innerHTML={props.body}
|
||||||
|
|||||||
@@ -349,6 +349,9 @@ declare module "@tiptap/core" {
|
|||||||
setReference: (options: { refId: string; refNum: number }) => ReturnType;
|
setReference: (options: { refId: string; refNum: number }) => ReturnType;
|
||||||
updateReferenceNumber: (refId: string, newNum: number) => ReturnType;
|
updateReferenceNumber: (refId: string, newNum: number) => ReturnType;
|
||||||
};
|
};
|
||||||
|
referenceSectionMarker: {
|
||||||
|
setReferenceSectionMarker: (heading: string) => ReturnType;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,47 +425,6 @@ const Reference = Mark.create({
|
|||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
refId: {
|
|
||||||
default: null,
|
|
||||||
parseHTML: (element) => element.getAttribute("data-ref-id"),
|
|
||||||
renderHTML: (attributes) => {
|
|
||||||
if (!attributes.refId) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"data-ref-id": attributes.refId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
refNum: {
|
|
||||||
default: 1,
|
|
||||||
parseHTML: (element) => {
|
|
||||||
const text = element.textContent || "";
|
|
||||||
const match = text.match(/^\[(\d+)\]$/);
|
|
||||||
return match ? parseInt(match[1]) : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
parseHTML() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
tag: "sup[data-ref-id]"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
|
||||||
return [
|
|
||||||
"sup",
|
|
||||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
|
||||||
0
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
addCommands() {
|
addCommands() {
|
||||||
return {
|
return {
|
||||||
setReference:
|
setReference:
|
||||||
@@ -523,6 +485,65 @@ const Reference = Mark.create({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Custom ReferenceSectionMarker node - invisible marker to identify references section
|
||||||
|
const ReferenceSectionMarker = Node.create({
|
||||||
|
name: "referenceSectionMarker",
|
||||||
|
group: "inline",
|
||||||
|
inline: true,
|
||||||
|
atom: true,
|
||||||
|
selectable: false,
|
||||||
|
draggable: false,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
heading: {
|
||||||
|
default: "References",
|
||||||
|
parseHTML: (element) =>
|
||||||
|
element.getAttribute("data-heading") || "References",
|
||||||
|
renderHTML: (attributes) => {
|
||||||
|
return {
|
||||||
|
"data-heading": attributes.heading
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: "span[id='references-section-start']"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
"span",
|
||||||
|
mergeAttributes(HTMLAttributes, {
|
||||||
|
id: "references-section-start",
|
||||||
|
style:
|
||||||
|
"display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.125rem 0.5rem; margin: 0 0.25rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 600; font-family: system-ui, -apple-system, sans-serif; user-select: none; cursor: default; vertical-align: middle;",
|
||||||
|
contenteditable: "false"
|
||||||
|
}),
|
||||||
|
"📌 References Section"
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setReferenceSectionMarker:
|
||||||
|
(heading: string) =>
|
||||||
|
({ commands }) => {
|
||||||
|
return commands.insertContent({
|
||||||
|
type: this.name,
|
||||||
|
attrs: { heading }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export interface TextEditorProps {
|
export interface TextEditorProps {
|
||||||
updateContent: (content: string) => void;
|
updateContent: (content: string) => void;
|
||||||
preSet?: string;
|
preSet?: string;
|
||||||
@@ -559,6 +580,20 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
|
|
||||||
const [showKeyboardHelp, setShowKeyboardHelp] = createSignal(false);
|
const [showKeyboardHelp, setShowKeyboardHelp] = createSignal(false);
|
||||||
|
|
||||||
|
// References section heading customization
|
||||||
|
const [referencesHeading, setReferencesHeading] = createSignal(
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? localStorage.getItem("editor-references-heading") || "References"
|
||||||
|
: "References"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Persist heading changes to localStorage
|
||||||
|
createEffect(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
localStorage.setItem("editor-references-heading", referencesHeading());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const [showConditionalConfig, setShowConditionalConfig] = createSignal(false);
|
const [showConditionalConfig, setShowConditionalConfig] = createSignal(false);
|
||||||
const [conditionalConfigPosition, setConditionalConfigPosition] =
|
const [conditionalConfigPosition, setConditionalConfigPosition] =
|
||||||
createSignal({
|
createSignal({
|
||||||
@@ -622,7 +657,10 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
const editor = createTiptapEditor(() => ({
|
const editor = createTiptapEditor(() => ({
|
||||||
element: editorRef,
|
element: editorRef,
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit,
|
StarterKit.configure({
|
||||||
|
// Disable these since we're adding them separately with custom config
|
||||||
|
codeBlock: false
|
||||||
|
}),
|
||||||
CodeBlockLowlight.configure({ lowlight }),
|
CodeBlockLowlight.configure({ lowlight }),
|
||||||
Link.configure({
|
Link.configure({
|
||||||
openOnClick: true
|
openOnClick: true
|
||||||
@@ -678,14 +716,14 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}),
|
}),
|
||||||
Superscript,
|
Superscript,
|
||||||
Subscript,
|
Subscript,
|
||||||
Reference
|
Reference,
|
||||||
|
ReferenceSectionMarker
|
||||||
],
|
],
|
||||||
content: props.preSet || `<p><em><b>Hello!</b> World</em></p>`,
|
content: props.preSet || `<p><em><b>Hello!</b> World</em></p>`,
|
||||||
onCreate: ({ editor }) => {
|
onCreate: ({ editor }) => {
|
||||||
// Migrate legacy references on initial load
|
// Migrate legacy references on initial load
|
||||||
if (props.preSet) {
|
if (props.preSet) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log("Running migration on initial load");
|
|
||||||
const doc = editor.state.doc;
|
const doc = editor.state.doc;
|
||||||
let refCount = 0;
|
let refCount = 0;
|
||||||
let legacyCount = 0;
|
let legacyCount = 0;
|
||||||
@@ -697,9 +735,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
);
|
);
|
||||||
if (refMark) {
|
if (refMark) {
|
||||||
refCount++;
|
refCount++;
|
||||||
console.log(
|
|
||||||
`Found Reference mark: [${refMark.attrs.refNum}] with ID: ${refMark.attrs.refId}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const superMark = node.marks.find(
|
const superMark = node.marks.find(
|
||||||
(mark: any) => mark.type.name === "superscript"
|
(mark: any) => mark.type.name === "superscript"
|
||||||
@@ -708,16 +743,11 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
const match = node.text?.match(/^\[(\d+)\]$/);
|
const match = node.text?.match(/^\[(\d+)\]$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
legacyCount++;
|
legacyCount++;
|
||||||
console.log(`Found legacy superscript: ${node.text}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Total Reference marks: ${refCount}, Legacy superscript: ${legacyCount}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (legacyCount > 0) {
|
if (legacyCount > 0) {
|
||||||
migrateLegacyReferences(editor);
|
migrateLegacyReferences(editor);
|
||||||
}
|
}
|
||||||
@@ -850,11 +880,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
|
||||||
"All superscript nodes:",
|
|
||||||
allSuperscriptNodes.map((n) => `"${n.text}" at ${n.pos}`)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Second pass: identify complete references (might be split)
|
// Second pass: identify complete references (might be split)
|
||||||
let i = 0;
|
let i = 0;
|
||||||
while (i < allSuperscriptNodes.length) {
|
while (i < allSuperscriptNodes.length) {
|
||||||
@@ -868,9 +893,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
(mark: any) =>
|
(mark: any) =>
|
||||||
mark.type.name !== "superscript" && mark.type.name !== "reference"
|
mark.type.name !== "superscript" && mark.type.name !== "reference"
|
||||||
);
|
);
|
||||||
console.log(
|
|
||||||
`Found complete legacy ref [${completeMatch[1]}] at pos ${node.pos}, hasOtherMarks: ${hasOtherMarks}, text: "${text}"`
|
|
||||||
);
|
|
||||||
legacyRefs.push({
|
legacyRefs.push({
|
||||||
pos: node.pos,
|
pos: node.pos,
|
||||||
num: parseInt(completeMatch[1]),
|
num: parseInt(completeMatch[1]),
|
||||||
@@ -892,10 +914,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
const totalLength =
|
const totalLength =
|
||||||
text.length + nextNode.text.length + afterNode.text.length;
|
text.length + nextNode.text.length + afterNode.text.length;
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Found split reference [${refNum}] starting at pos ${node.pos} (split into "${text}", "${nextNode.text}", "${afterNode.text}")`
|
|
||||||
);
|
|
||||||
|
|
||||||
// We need to handle split references differently - remove all three nodes and create one
|
// We need to handle split references differently - remove all three nodes and create one
|
||||||
legacyRefs.push({
|
legacyRefs.push({
|
||||||
pos: node.pos,
|
pos: node.pos,
|
||||||
@@ -913,31 +931,21 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (legacyRefs.length === 0) {
|
if (legacyRefs.length === 0) {
|
||||||
console.log("No legacy references found to migrate");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Migrating ${legacyRefs.length} legacy references to Reference marks`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sort by position (process from end to start to avoid position shifts)
|
|
||||||
legacyRefs.sort((a, b) => b.pos - a.pos);
|
legacyRefs.sort((a, b) => b.pos - a.pos);
|
||||||
|
|
||||||
// Build a single transaction to convert all legacy refs
|
|
||||||
const tr = editorInstance.state.tr;
|
const tr = editorInstance.state.tr;
|
||||||
|
|
||||||
legacyRefs.forEach((ref) => {
|
legacyRefs.forEach((ref) => {
|
||||||
// Generate unique ID for this reference
|
|
||||||
const refId = `ref-migrated-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
const refId = `ref-migrated-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
// Create Reference mark (only the reference mark, no other marks)
|
|
||||||
const newMark = editorInstance.schema.marks.reference.create({
|
const newMark = editorInstance.schema.marks.reference.create({
|
||||||
refId: refId,
|
refId: refId,
|
||||||
refNum: ref.num
|
refNum: ref.num
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace with Reference mark (preserve only the reference mark, remove other marks like links)
|
|
||||||
tr.replaceWith(
|
tr.replaceWith(
|
||||||
ref.pos,
|
ref.pos,
|
||||||
ref.pos + ref.textLength,
|
ref.pos + ref.textLength,
|
||||||
@@ -945,10 +953,7 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dispatch the transaction
|
|
||||||
editorInstance.view.dispatch(tr);
|
editorInstance.view.dispatch(tr);
|
||||||
|
|
||||||
console.log("Migration complete");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renumberAllReferences = (editorInstance: any) => {
|
const renumberAllReferences = (editorInstance: any) => {
|
||||||
@@ -1050,34 +1055,49 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (foundRefs.size === 0) {
|
if (foundRefs.size === 0) {
|
||||||
let hasReferencesSection = false;
|
// No references found - remove the entire section if it exists
|
||||||
|
let markerPos = -1;
|
||||||
let hrPos = -1;
|
let hrPos = -1;
|
||||||
let sectionStartPos = -1;
|
let sectionEndPos = -1;
|
||||||
|
|
||||||
doc.descendants((node: any, pos: number) => {
|
doc.descendants((node: any, pos: number) => {
|
||||||
if (node.type.name === "heading" && node.textContent === "References") {
|
// Find marker first
|
||||||
hasReferencesSection = true;
|
if (node.type.name === "referenceSectionMarker") {
|
||||||
sectionStartPos = pos;
|
markerPos = pos;
|
||||||
|
}
|
||||||
|
// Find HR before marker
|
||||||
|
if (markerPos === -1 && node.type.name === "horizontalRule") {
|
||||||
|
hrPos = pos;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasReferencesSection && sectionStartPos > 0) {
|
// Find the end of the references section
|
||||||
doc.nodesBetween(
|
if (markerPos >= 0) {
|
||||||
Math.max(0, sectionStartPos - 50),
|
let foundEnd = false;
|
||||||
sectionStartPos,
|
doc.descendants((node: any, pos: number) => {
|
||||||
(node: any, pos: number) => {
|
if (foundEnd || pos <= markerPos) return;
|
||||||
if (node.type.name === "horizontalRule") {
|
|
||||||
hrPos = pos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hrPos >= 0) {
|
// Section ends at next HR or H2 heading
|
||||||
const tr = editorInstance.state.tr;
|
if (
|
||||||
tr.delete(hrPos, doc.content.size);
|
node.type.name === "horizontalRule" ||
|
||||||
editorInstance.view.dispatch(tr);
|
(node.type.name === "heading" && node.attrs.level <= 2)
|
||||||
|
) {
|
||||||
|
sectionEndPos = pos;
|
||||||
|
foundEnd = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no end found, section goes to end of document
|
||||||
|
if (!foundEnd) {
|
||||||
|
sectionEndPos = doc.content.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hrPos >= 0 && sectionEndPos > hrPos) {
|
||||||
|
const tr = editorInstance.state.tr;
|
||||||
|
tr.delete(hrPos, sectionEndPos);
|
||||||
|
editorInstance.view.dispatch(tr);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1090,31 +1110,117 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let markerPos = -1;
|
||||||
|
let markerHeading = "";
|
||||||
let referencesHeadingPos = -1;
|
let referencesHeadingPos = -1;
|
||||||
let existingRefs = new Set<string>();
|
let sectionEndPos = -1;
|
||||||
|
let existingRefs = new Map<
|
||||||
|
string,
|
||||||
|
{ pos: number; isPlaceholder: boolean }
|
||||||
|
>();
|
||||||
|
|
||||||
|
// Look for the marker first
|
||||||
doc.descendants((node: any, pos: number) => {
|
doc.descendants((node: any, pos: number) => {
|
||||||
if (node.type.name === "heading" && node.textContent === "References") {
|
if (node.type.name === "referenceSectionMarker") {
|
||||||
|
markerPos = pos;
|
||||||
|
markerHeading = node.attrs.heading || referencesHeading();
|
||||||
|
}
|
||||||
|
// If marker found, look for heading after it
|
||||||
|
if (
|
||||||
|
markerPos >= 0 &&
|
||||||
|
referencesHeadingPos === -1 &&
|
||||||
|
node.type.name === "heading" &&
|
||||||
|
pos > markerPos &&
|
||||||
|
pos < markerPos + 50
|
||||||
|
) {
|
||||||
referencesHeadingPos = pos;
|
referencesHeadingPos = pos;
|
||||||
}
|
}
|
||||||
if (referencesHeadingPos >= 0 && node.type.name === "paragraph") {
|
// Find section end (next HR or H2)
|
||||||
const match = node.textContent.match(/^\[(.+?)\]/);
|
if (
|
||||||
|
referencesHeadingPos >= 0 &&
|
||||||
|
sectionEndPos === -1 &&
|
||||||
|
pos > referencesHeadingPos &&
|
||||||
|
(node.type.name === "horizontalRule" ||
|
||||||
|
(node.type.name === "heading" && node.attrs.level <= 2))
|
||||||
|
) {
|
||||||
|
sectionEndPos = pos;
|
||||||
|
}
|
||||||
|
// Collect existing reference numbers within the section
|
||||||
|
if (
|
||||||
|
referencesHeadingPos >= 0 &&
|
||||||
|
pos > referencesHeadingPos &&
|
||||||
|
(sectionEndPos === -1 || pos < sectionEndPos) &&
|
||||||
|
node.type.name === "paragraph"
|
||||||
|
) {
|
||||||
|
const text = node.textContent;
|
||||||
|
const match = text.match(/^\[(.+?)\]/);
|
||||||
if (match) {
|
if (match) {
|
||||||
existingRefs.add(match[1]);
|
const isPlaceholder = text.includes("Add your reference text here");
|
||||||
|
existingRefs.set(match[1], { pos, isPlaceholder });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (referencesHeadingPos === -1) {
|
// If no section end found, it goes to document end
|
||||||
|
if (referencesHeadingPos >= 0 && sectionEndPos === -1) {
|
||||||
|
sectionEndPos = doc.content.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update marker heading if it changed
|
||||||
|
if (markerPos >= 0 && markerHeading !== referencesHeading()) {
|
||||||
|
const tr = editorInstance.state.tr;
|
||||||
|
const markerNode = doc.nodeAt(markerPos);
|
||||||
|
if (markerNode) {
|
||||||
|
tr.replaceWith(
|
||||||
|
markerPos,
|
||||||
|
markerPos + markerNode.nodeSize,
|
||||||
|
editorInstance.schema.nodes.referenceSectionMarker.create({
|
||||||
|
heading: referencesHeading()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
editorInstance.view.dispatch(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update heading text if it changed
|
||||||
|
if (referencesHeadingPos >= 0 && markerHeading !== referencesHeading()) {
|
||||||
|
const tr = editorInstance.state.tr;
|
||||||
|
const headingNode = doc.nodeAt(referencesHeadingPos);
|
||||||
|
if (headingNode) {
|
||||||
|
tr.replaceWith(
|
||||||
|
referencesHeadingPos,
|
||||||
|
referencesHeadingPos + headingNode.nodeSize,
|
||||||
|
editorInstance.schema.nodes.heading.create(
|
||||||
|
{ level: 2 },
|
||||||
|
editorInstance.schema.text(referencesHeading())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
editorInstance.view.dispatch(tr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create section if marker not found
|
||||||
|
if (markerPos === -1) {
|
||||||
const content: any[] = [
|
const content: any[] = [
|
||||||
{ type: "horizontalRule" },
|
{ type: "horizontalRule" },
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "referenceSectionMarker",
|
||||||
|
attrs: { heading: referencesHeading() }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "heading",
|
type: "heading",
|
||||||
attrs: { level: 2 },
|
attrs: { level: 2 },
|
||||||
content: [{ type: "text", text: "References" }]
|
content: [{ type: "text", text: referencesHeading() }]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Add placeholder paragraphs for each reference
|
||||||
refNumbers.forEach((refNum) => {
|
refNumbers.forEach((refNum) => {
|
||||||
content.push({
|
content.push({
|
||||||
type: "paragraph",
|
type: "paragraph",
|
||||||
@@ -1123,7 +1229,7 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
type: "text",
|
type: "text",
|
||||||
text: `[${refNum}] `,
|
text: `[${refNum}] `,
|
||||||
marks: [{ type: "bold" }]
|
marks: [{ type: "bold" }]
|
||||||
} as any,
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Add your reference text here"
|
text: "Add your reference text here"
|
||||||
@@ -1138,47 +1244,75 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
editorInstance.schema.nodeFromJSON({ type: "doc", content }).content
|
editorInstance.schema.nodeFromJSON({ type: "doc", content }).content
|
||||||
);
|
);
|
||||||
editorInstance.view.dispatch(tr);
|
editorInstance.view.dispatch(tr);
|
||||||
} else {
|
return;
|
||||||
const newRefs = refNumbers.filter((ref) => !existingRefs.has(ref));
|
}
|
||||||
|
|
||||||
if (newRefs.length > 0) {
|
// Section exists - manage placeholders
|
||||||
let insertPos = referencesHeadingPos;
|
const tr = editorInstance.state.tr;
|
||||||
doc.nodesBetween(
|
let hasChanges = false;
|
||||||
referencesHeadingPos,
|
|
||||||
doc.content.size,
|
|
||||||
(node: any, pos: number) => {
|
|
||||||
if (pos > insertPos) {
|
|
||||||
insertPos = pos + node.nodeSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const content: any[] = [];
|
// Step 1: Remove placeholders for references that no longer exist
|
||||||
newRefs.forEach((refNum) => {
|
const toDelete: Array<{ pos: number; nodeSize: number }> = [];
|
||||||
content.push({
|
existingRefs.forEach((info, refNum) => {
|
||||||
|
if (info.isPlaceholder && !refNumbers.includes(refNum)) {
|
||||||
|
const node = doc.nodeAt(info.pos);
|
||||||
|
if (node) {
|
||||||
|
toDelete.push({ pos: info.pos, nodeSize: node.nodeSize });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete in reverse order to maintain positions
|
||||||
|
toDelete
|
||||||
|
.sort((a, b) => b.pos - a.pos)
|
||||||
|
.forEach(({ pos, nodeSize }) => {
|
||||||
|
tr.delete(pos, pos + nodeSize);
|
||||||
|
hasChanges = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2: Add placeholders for new references
|
||||||
|
if (referencesHeadingPos >= 0) {
|
||||||
|
// Find insertion point (after heading, before any content or at section end)
|
||||||
|
let insertPos = referencesHeadingPos;
|
||||||
|
const headingNode = doc.nodeAt(referencesHeadingPos);
|
||||||
|
if (headingNode) {
|
||||||
|
insertPos = referencesHeadingPos + headingNode.nodeSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add missing references in order
|
||||||
|
const nodesToInsert: any[] = [];
|
||||||
|
refNumbers.forEach((refNum) => {
|
||||||
|
if (!existingRefs.has(refNum)) {
|
||||||
|
nodesToInsert.push({
|
||||||
type: "paragraph",
|
type: "paragraph",
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `[${refNum}] `,
|
text: `[${refNum}] `,
|
||||||
marks: [{ type: "bold" }]
|
marks: [{ type: "bold" }]
|
||||||
} as any,
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Add your reference text here"
|
text: "Add your reference text here"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const tr = editorInstance.state.tr;
|
if (nodesToInsert.length > 0) {
|
||||||
content.forEach((item) => {
|
nodesToInsert.forEach((nodeData) => {
|
||||||
tr.insert(insertPos, editorInstance.schema.nodeFromJSON(item));
|
const node = editorInstance.schema.nodeFromJSON(nodeData);
|
||||||
insertPos += editorInstance.schema.nodeFromJSON(item).nodeSize;
|
tr.insert(insertPos, node);
|
||||||
|
insertPos += node.nodeSize;
|
||||||
});
|
});
|
||||||
editorInstance.view.dispatch(tr);
|
hasChanges = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
editorInstance.view.dispatch(tr);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setLink = () => {
|
const setLink = () => {
|
||||||
@@ -1627,11 +1761,14 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
document.addEventListener("click", handleClickOutside);
|
document.addEventListener("click", handleClickOutside);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return () => document.removeEventListener("click", handleClickOutside);
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
document.removeEventListener("click", handleClickOutside);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1647,11 +1784,14 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
document.addEventListener("click", handleClickOutside);
|
document.addEventListener("click", handleClickOutside);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return () => document.removeEventListener("click", handleClickOutside);
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
document.removeEventListener("click", handleClickOutside);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1667,11 +1807,14 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
document.addEventListener("click", handleClickOutside);
|
document.addEventListener("click", handleClickOutside);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return () => document.removeEventListener("click", handleClickOutside);
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
document.removeEventListener("click", handleClickOutside);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1687,11 +1830,14 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
document.addEventListener("click", handleClickOutside);
|
document.addEventListener("click", handleClickOutside);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return () => document.removeEventListener("click", handleClickOutside);
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
document.removeEventListener("click", handleClickOutside);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2111,7 +2257,7 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
<Show when={showBubbleMenu()}>
|
<Show when={showBubbleMenu()}>
|
||||||
<div
|
<div
|
||||||
ref={bubbleMenuRef}
|
ref={bubbleMenuRef}
|
||||||
class="bg-crust text-text fixed z-110 w-fit rounded p-2 text-sm whitespace-nowrap shadow-xl"
|
class="bg-crust text-text fixed z-[120] w-fit rounded p-2 text-sm whitespace-nowrap shadow-xl"
|
||||||
style={{
|
style={{
|
||||||
top: `${bubbleMenuPosition().top}px`,
|
top: `${bubbleMenuPosition().top}px`,
|
||||||
left: `${bubbleMenuPosition().left}px`,
|
left: `${bubbleMenuPosition().left}px`,
|
||||||
@@ -2345,7 +2491,7 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
{/* Language Selector Dropdown */}
|
{/* Language Selector Dropdown */}
|
||||||
<Show when={showLanguageSelector()}>
|
<Show when={showLanguageSelector()}>
|
||||||
<div
|
<div
|
||||||
class="language-selector bg-mantle text-text border-surface2 fixed z-110 max-h-64 w-48 overflow-y-auto rounded border shadow-lg"
|
class="language-selector bg-mantle text-text border-surface2 fixed z-[120] max-h-64 w-48 overflow-y-auto rounded border shadow-lg"
|
||||||
style={{
|
style={{
|
||||||
top: `${languageSelectorPosition().top}px`,
|
top: `${languageSelectorPosition().top}px`,
|
||||||
left: `${languageSelectorPosition().left}px`
|
left: `${languageSelectorPosition().left}px`
|
||||||
@@ -2368,7 +2514,7 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
{/* Table Grid Selector */}
|
{/* Table Grid Selector */}
|
||||||
<Show when={showTableMenu()}>
|
<Show when={showTableMenu()}>
|
||||||
<div
|
<div
|
||||||
class="table-menu fixed z-110"
|
class="table-menu fixed z-[120]"
|
||||||
style={{
|
style={{
|
||||||
top: `${tableMenuPosition().top}px`,
|
top: `${tableMenuPosition().top}px`,
|
||||||
left: `${tableMenuPosition().left}px`
|
left: `${tableMenuPosition().left}px`
|
||||||
@@ -2381,7 +2527,7 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
{/* Mermaid Template Selector */}
|
{/* Mermaid Template Selector */}
|
||||||
<Show when={showMermaidTemplates()}>
|
<Show when={showMermaidTemplates()}>
|
||||||
<div
|
<div
|
||||||
class="mermaid-menu bg-mantle text-text border-surface2 fixed z-110 max-h-96 w-56 overflow-y-auto rounded border shadow-lg"
|
class="mermaid-menu bg-mantle text-text border-surface2 fixed z-[120] max-h-96 w-56 overflow-y-auto rounded border shadow-lg"
|
||||||
style={{
|
style={{
|
||||||
top: `${mermaidMenuPosition().top}px`,
|
top: `${mermaidMenuPosition().top}px`,
|
||||||
left: `${mermaidMenuPosition().left}px`
|
left: `${mermaidMenuPosition().left}px`
|
||||||
@@ -2409,7 +2555,7 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
{/* Conditional Configurator */}
|
{/* Conditional Configurator */}
|
||||||
<Show when={showConditionalConfig()}>
|
<Show when={showConditionalConfig()}>
|
||||||
<div
|
<div
|
||||||
class="conditional-config fixed z-110"
|
class="conditional-config fixed z-[120]"
|
||||||
style={{
|
style={{
|
||||||
top: `${conditionalConfigPosition().top}px`,
|
top: `${conditionalConfigPosition().top}px`,
|
||||||
left: `${conditionalConfigPosition().left}px`
|
left: `${conditionalConfigPosition().left}px`
|
||||||
@@ -2533,6 +2679,27 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
>
|
>
|
||||||
[n]
|
[n]
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const newHeading = window.prompt(
|
||||||
|
"Enter heading for references section:",
|
||||||
|
referencesHeading()
|
||||||
|
);
|
||||||
|
if (newHeading && newHeading.trim()) {
|
||||||
|
setReferencesHeading(newHeading.trim());
|
||||||
|
// Update existing section if it exists
|
||||||
|
const instance = editor();
|
||||||
|
if (instance) {
|
||||||
|
updateReferencesSection(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
class="hover:bg-surface1 touch-manipulation rounded px-2 py-1 text-xs select-none"
|
||||||
|
title={`Change references heading (current: "${referencesHeading()}")`}
|
||||||
|
>
|
||||||
|
📑
|
||||||
|
</button>
|
||||||
<div class="border-surface2 mx-1 border-l"></div>
|
<div class="border-surface2 mx-1 border-l"></div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -2896,7 +3063,7 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
class="prose prose-sm prose-invert sm:prose-base md:prose-xl lg:prose-xl xl:prose-2xl mx-auto max-w-full transition-all duration-300 focus:outline-none"
|
class="prose prose-sm prose-invert sm:prose-base md:prose-lg mx-auto max-w-full transition-all duration-300 focus:outline-none md:px-8"
|
||||||
classList={{
|
classList={{
|
||||||
"h-[80dvh] overflow-scroll": !isFullscreen(),
|
"h-[80dvh] overflow-scroll": !isFullscreen(),
|
||||||
"flex-1 h-full overflow-y-auto": isFullscreen()
|
"flex-1 h-full overflow-y-auto": isFullscreen()
|
||||||
@@ -2909,7 +3076,7 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
{/* Keyboard Help Modal */}
|
{/* Keyboard Help Modal */}
|
||||||
<Show when={showKeyboardHelp()}>
|
<Show when={showKeyboardHelp()}>
|
||||||
<div
|
<div
|
||||||
class="bg-opacity-50 fixed inset-0 z-110 flex items-center justify-center bg-black"
|
class="bg-opacity-50 fixed inset-0 z-150 flex items-center justify-center bg-black"
|
||||||
onClick={() => setShowKeyboardHelp(false)}
|
onClick={() => setShowKeyboardHelp(false)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -727,3 +727,8 @@ button:active,
|
|||||||
[role="button"]:active {
|
[role="button"]:active {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide reference section marker in published posts (only show in editor) */
|
||||||
|
#post-content-body span#references-section-start {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user