Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
339 changes: 339 additions & 0 deletions frontend/user-profile-wysiwyg-editor.js

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heya! Is there any specific reason we need to duplicate the wysiwyg component here?
I also noticed there are a lot of imports not being used and a few function calls/refs that don't seem to exist in the scope of the script (e.g. isSafeUrl, parseMarkdownSafe and others..).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this was the best solution I found for the requirements of limiting the features on the wysiwig. I'm not super happy with this solution, but it is the best one I discovered.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing, no problem with that specifically. My comment is more related to the unused imports and the missing function references. Did you have the chance to take a look at those?

Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import Underline from "@tiptap/extension-underline";
import Link from "@tiptap/extension-link";
import Table from "@tiptap/extension-table";
import TableRow from "@tiptap/extension-table-row";
import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header";
import Image from "@tiptap/extension-image";
import TaskList from "@tiptap/extension-task-list";
import TaskItem from "@tiptap/extension-task-item";
import { common, createLowlight } from "lowlight";
import { toHtml } from "hast-util-to-html";
import { marked } from "marked";
import DOMPurify from "dompurify";
import TurndownService from "turndown";
import { gfm } from "turndown-plugin-gfm";
import {handleKeyDown, handlePaste, createToolbarButton, ICONS, setupMermaidEditMode, debounce} from './wysiwyg-editor';

const lowlight = createLowlight(common);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused lowlight initialization.

The lowlight constant is created but never used, since CodeBlockLowlight is not included in this editor's configuration.

🧹 Proposed fix
-const lowlight = createLowlight(common);
-
 const editorInstances = new Map();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const lowlight = createLowlight(common);
const editorInstances = new Map();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/user-profile-wysiwyg-editor.js` at line 21, Remove the unused
lowlight initialization: delete the const lowlight = createLowlight(common) line
(and remove the createLowlight and common imports if they are no longer used)
because CodeBlockLowlight is not included in this editor configuration; ensure
no other references to lowlight or createLowlight remain in the file (search for
lowlight, createLowlight, and common) and run the linter/TypeScript checks to
confirm no unused-import errors.


const editorInstances = new Map();

const buildToolbar = (editor, toolbarEl) => {
const left = document.createElement("div");
left.className = "wysiwyg-toolbar__left";
const right = document.createElement("div");
right.className = "wysiwyg-toolbar__right";


left.appendChild(
createToolbarButton(editor, {
label: "Bold", title: "Bold", html: "<strong>B</strong>",
onClick: () => editor.chain().focus().toggleBold().run(),
isActive: () => editor.isActive("bold"),
})
);
left.appendChild(
createToolbarButton(editor, {
label: "Italic", title: "Italic", html: "<em>I</em>",
onClick: () => editor.chain().focus().toggleItalic().run(),
isActive: () => editor.isActive("italic"),
})
);
left.appendChild(
createToolbarButton(editor, {
label: "Underline", title: "Underline", html: "<u>U</u>",
onClick: () => editor.chain().focus().toggleUnderline().run(),
isActive: () => editor.isActive("underline"),
})
);


left.appendChild(
createToolbarButton(editor, {
label: "Bullet list", title: "Bullet list", html: ICONS.bulletList,
onClick: () => editor.chain().focus().toggleBulletList().run(),
isActive: () => editor.isActive("bulletList"),
})
);
left.appendChild(
createToolbarButton(editor, {
label: "Ordered list", title: "Ordered list", html: ICONS.orderedList,
onClick: () => editor.chain().focus().toggleOrderedList().run(),
isActive: () => editor.isActive("orderedList"),
})
);


left.appendChild(
createToolbarButton(editor, {
label: "Link", title: "Insert link", html: ICONS.link,
onClick: async () => {
const result = await openModal("Insert Link", [
{ name: "url", label: "URL", type: "url", placeholder: "https://example.com" },
]);
if (!result || !result.url) return;
if (!isSafeUrl(result.url)) {
window.alert("Only http, https, and mailto URLs are allowed.");
return;
}
editor.chain().focus().setLink({ href: result.url }).run();
},
isActive: () => editor.isActive("link"),
})
);
Comment on lines +75 to +87

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that openModal and isSafeUrl are not exported from wysiwyg-editor.js

echo "=== Checking if openModal is exported ==="
rg -n "export.*openModal" frontend/wysiwyg-editor.js || echo "Not found as export"

echo ""
echo "=== Checking if isSafeUrl is exported ==="
rg -n "export.*isSafeUrl" frontend/wysiwyg-editor.js || echo "Not found as export"

echo ""
echo "=== Checking where openModal is defined ==="
rg -n "^(const|function|export const|export function)\s+openModal" frontend/wysiwyg-editor.js

echo ""
echo "=== Checking where isSafeUrl is defined ==="
rg -n "^(const|function|export const|export function)\s+isSafeUrl" frontend/wysiwyg-editor.js

Repository: boostorg/website-v2

Length of output: 350


Export/import openModal and isSafeUrl for the Link action to avoid runtime failures

frontend/user-profile-wysiwyg-editor.js (lines 75-87) uses openModal and isSafeUrl, but frontend/wysiwyg-editor.js only defines them (const isSafeUrl ... at 87 and const openModal ... at 96) and has no corresponding named exports, so these identifiers won’t be available in this module’s scope when the Link command runs.

Export openModal/isSafeUrl from frontend/wysiwyg-editor.js and import them here, or implement those helpers locally in frontend/user-profile-wysiwyg-editor.js.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/user-profile-wysiwyg-editor.js` around lines 75 - 87, The Link
action uses openModal and isSafeUrl but those helpers are only defined (const
isSafeUrl, const openModal) in another module without exports, causing runtime
failures; either export them from frontend/wysiwyg-editor.js and import them
into this file, or implement equivalents locally in
frontend/user-profile-wysiwyg-editor.js, then update the Link command to call
the imported/local openModal and isSafeUrl before calling
editor.chain().focus().setLink(...). Ensure the exported names match exactly:
openModal and isSafeUrl.


const handleDocClick = (e) => {
if (!tableWrapper.contains(e.target)) gridPopup.style.display = "none";
};
Comment on lines +89 to +91

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove dead code referencing undefined variables.

This handleDocClick function references tableWrapper and gridPopup which are not defined anywhere in the buildToolbar function scope. This appears to be leftover code from the full editor that wasn't cleaned up during the simplification.

🗑️ Proposed fix
   });
 
-  const handleDocClick = (e) => {
-    if (!tableWrapper.contains(e.target)) gridPopup.style.display = "none";
-  };
-
   const mdBtn = document.createElement("button");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleDocClick = (e) => {
if (!tableWrapper.contains(e.target)) gridPopup.style.display = "none";
};
});
const mdBtn = document.createElement("button");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/user-profile-wysiwyg-editor.js` around lines 89 - 91, The function
handleDocClick in buildToolbar references undefined variables tableWrapper and
gridPopup; remove this dead code (delete the handleDocClick definition and any
registrations like document.addEventListener('click', handleDocClick)) or, if
behavior is required, replace it with a safe implementation that uses existing
toolbar elements instead; ensure no leftover event listeners or references to
tableWrapper/gridPopup remain in buildToolbar.


const mdBtn = document.createElement("button");
mdBtn.type = "button";
mdBtn.className = "wysiwyg-toolbar__btn wysiwyg-toolbar__btn--md";
mdBtn.setAttribute("aria-label", "Markdown");
mdBtn.setAttribute("title", "Toggle Markdown mode");
mdBtn.innerHTML = ICONS.markdown;
left.appendChild(mdBtn);

const previewBtn = document.createElement("button");
previewBtn.type = "button";
previewBtn.className = "wysiwyg-toolbar__btn wysiwyg-toolbar__btn--preview-toggle";
previewBtn.setAttribute("aria-label", "Preview");
previewBtn.setAttribute("title", "Toggle preview");
previewBtn.innerHTML = ICONS.preview;
previewBtn.style.display = "none";

right.appendChild(previewBtn);
right.appendChild(
createToolbarButton(editor, {
label: "Undo", title: "Undo", html: "&#8630;",
onClick: () => editor.chain().focus().undo().run(),
isActive: () => false,
})
);
right.appendChild(
createToolbarButton(editor, {
label: "Redo", title: "Redo", html: "&#8631;",
onClick: () => editor.chain().focus().redo().run(),
isActive: () => false,
})
);

toolbarEl.appendChild(left);
toolbarEl.appendChild(right);

return { mdBtn, previewBtn, handleDocClick };
};

export const initWysiwyg = (textareaId) => {
const prev = editorInstances.get(textareaId);
if (prev) {
prev.editor.destroy();
prev.cleanup();
editorInstances.delete(textareaId);
}

const textarea = document.getElementById(textareaId);
if (!textarea) return null;
const wrapper = textarea.closest('[data-wysiwyg="v3"]');
if (!wrapper) return null;

const toolbarEl = wrapper.querySelector(".wysiwyg-editor__toolbar");
const editorEl = wrapper.querySelector(".wysiwyg-editor__body");
if (!toolbarEl || !editorEl) return null;

/* Ensure toolbar is empty and remove any previous table-context bar after re-init (e.g. Fill demo content) to avoid duplicate bars */
toolbarEl.innerHTML = "";
wrapper.querySelectorAll(".wysiwyg-table-context").forEach((el) => el.remove());

const rawContent = textarea.value ? textarea.value.trim() : "";
const isHtml = rawContent.startsWith("<") && rawContent.includes(">");
let initialContent = rawContent;
if (initialContent && !isHtml) {
try {
initialContent = parseMarkdownSafe(initialContent);
} catch (_) {
initialContent = rawContent;
}
Comment on lines +157 to +160

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if these required functions are exported from wysiwyg-editor.js

echo "=== Checking exports ==="
for func in parseMarkdownSafe highlightPreviewCodeBlocks renderMermaidPreview turndown; do
  echo "Checking $func:"
  rg -n "export.*(const|function)\s+$func" frontend/wysiwyg-editor.js || echo "  ❌ Not exported"
done

echo ""
echo "=== Where they are defined in wysiwyg-editor.js ==="
rg -n "^(const|function)\s+(parseMarkdownSafe|highlightPreviewCodeBlocks|renderMermaidPreview|turndown)" frontend/wysiwyg-editor.js

Repository: boostorg/website-v2

Length of output: 514


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== user-profile-wysiwyg-editor.js imports/exports near top ==="
sed -n '1,220p' frontend/user-profile-wysiwyg-editor.js | nl -ba | sed -n '1,220p'

echo ""
echo "=== user-profile-wysiwyg-editor.js: locate references ==="
rg -n "\bparseMarkdownSafe\b|\bhighlightPreviewCodeBlocks\b|\brenderMermaidPreview\b|\bturndown\b" frontend/user-profile-wysiwyg-editor.js -n

echo ""
echo "=== user-profile-wysiwyg-editor.js: check if identifiers are defined locally ==="
rg -n "function\s+parseMarkdownSafe|const\s+parseMarkdownSafe|let\s+parseMarkdownSafe|var\s+parseMarkdownSafe" frontend/user-profile-wysiwyg-editor.js || true
rg -n "function\s+highlightPreviewCodeBlocks|const\s+highlightPreviewCodeBlocks|let\s+highlightPreviewCodeBlocks|var\s+highlightPreviewCodeBlocks" frontend/user-profile-wysiwyg-editor.js || true
rg -n "function\s+renderMermaidPreview|const\s+renderMermaidPreview|let\s+renderMermaidPreview|var\s+renderMermaidPreview" frontend/user-profile-wysiwyg-editor.js || true
rg -n "const\s+turndown|let\s+turndown|var\s+turndown" frontend/user-profile-wysiwyg-editor.js || true

echo ""
echo "=== wysiwyg-editor.js: check exports mechanism ==="
rg -n "export\s+(default\s+)?|module\.exports|exports\." frontend/wysiwyg-editor.js

echo ""
echo "=== wysiwyg-editor.js: show definitions for referenced identifiers ==="
rg -n "function\s+parseMarkdownSafe|const\s+turndown\s*=|const\s+highlightPreviewCodeBlocks\s*=|const\s+renderMermaidPreview\s*=" frontend/wysiwyg-editor.js -n

echo ""
echo "=== wysiwyg-editor.js: show surrounding code for the definitions ==="
for line in 35 53 655 669; do
  echo "--- context around line $line ---"
  sed -n "$((line-5)),$((line+20))p" frontend/wysiwyg-editor.js | nl -ba | sed -n '1,80p'
done

Repository: boostorg/website-v2

Length of output: 188


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== user-profile-wysiwyg-editor.js: show first ~240 lines with line numbers ==="
awk '{printf "%d:%s\n", NR, $0}' frontend/user-profile-wysiwyg-editor.js | sed -n '1,240p'

echo ""
echo "=== user-profile-wysiwyg-editor.js: locate references to the identifiers ==="
rg -n "\bparseMarkdownSafe\b|\bhighlightPreviewCodeBlocks\b|\brenderMermaidPreview\b|\bturndown\b" frontend/user-profile-wysiwyg-editor.js

echo ""
echo "=== user-profile-wysiwyg-editor.js: locate any imports for wysiwyg-editor.js ==="
rg -n "from\s+['\"].*wysiwyg-editor['\"]|require\(['\"].*wysiwyg-editor['\"]\)" frontend/user-profile-wysiwyg-editor.js || true

echo ""
echo "=== user-profile-wysiwyg-editor.js: check whether any of these identifiers are declared locally ==="
rg -n "(function|const|let|var)\s+parseMarkdownSafe\b" frontend/user-profile-wysiwyg-editor.js || true
rg -n "(function|const|let|var)\s+highlightPreviewCodeBlocks\b" frontend/user-profile-wysiwyg-editor.js || true
rg -n "(function|const|let|var)\s+renderMermaidPreview\b" frontend/user-profile-wysiwyg-editor.js || true
rg -n "(const|let|var)\s+turndown\b" frontend/user-profile-wysiwyg-editor.js || true

echo ""
echo "=== wysiwyg-editor.js: check how these identifiers are declared and exported ==="
rg -n "function\s+parseMarkdownSafe|const\s+turndown\s*=|const\s+highlightPreviewCodeBlocks\s*=|const\s+renderMermaidPreview\s*=" frontend/wysiwyg-editor.js

echo ""
echo "=== wysiwyg-editor.js: check for exports of these identifiers ==="
rg -n "export\s*\{[^}]*\b(parseMarkdownSafe|highlightPreviewCodeBlocks|renderMermaidPreview|turndown)\b[^}]*\}|export\s+(const|function)\s+(parseMarkdownSafe|highlightPreviewCodeBlocks|renderMermaidPreview|turndown)\b|module\.exports|exports\." frontend/wysiwyg-editor.js || true

echo ""
echo "=== wysiwyg-editor.js: show context around each definition/export area ==="
for start in 1 20 40 620 640 650 660 670; do
  :
done

for line in 35 53 655 669; do
  echo "--- context around wysiwyg-editor.js:$line ---"
  awk -v s="$((line-10))" -v e="$((line+25))" 'NR>=s && NR<=e {printf "%d:%s\n", NR, $0}' frontend/wysiwyg-editor.js
done

Repository: boostorg/website-v2

Length of output: 15757


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== user-profile-wysiwyg-editor.js: check for any local declarations (whole file) ==="
rg -n "(function|const|let|var)\s+parseMarkdownSafe\b" frontend/user-profile-wysiwyg-editor.js || true
rg -n "(function|const|let|var)\s+highlightPreviewCodeBlocks\b" frontend/user-profile-wysiwyg-editor.js || true
rg -n "(function|const|let|var)\s+renderMermaidPreview\b" frontend/user-profile-wysiwyg-editor.js || true
rg -n "(const|let|var)\s+turndown\b" frontend/user-profile-wysiwyg-editor.js || true

echo ""
echo "=== user-profile-wysiwyg-editor.js: show context around reported usages (150-220) ==="
awk 'NR>=140 && NR<=220 {printf "%d:%s\n", NR, $0}' frontend/user-profile-wysiwyg-editor.js

echo ""
echo "=== user-profile-wysiwyg-editor.js: show context around reported usages (230-320) ==="
awk 'NR>=220 && NR<=340 {printf "%d:%s\n", NR, $0}' frontend/user-profile-wysiwyg-editor.js

echo ""
echo "=== wysiwyg-editor.js: show exports section (if any) ==="
rg -n "export\s*\{|export\s+default|module\.exports|exports\." frontend/wysiwyg-editor.js || true

Repository: boostorg/website-v2

Length of output: 8029


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for global assignments of these identifiers ==="
for sym in parseMarkdownSafe highlightPreviewCodeBlocks renderMermaidPreview turndown; do
  echo "--- $sym ---"
  rg -n "globalThis\.${sym}|window\.${sym}|global\.\${sym}|self\.${sym}" -S . || true
done

echo ""
echo "=== Search for re-exports from wysiwyg-editor.js ==="
rg -n "wysiwyg-editor['\"]" frontend -S || true
rg -n "export\s*\{[^}]*parseMarkdownSafe|export\s*\{[^}]*highlightPreviewCodeBlocks|export\s*\{[^}]*renderMermaidPreview|export\s*\{[^}]*turndown" frontend/wysiwyg-editor.js -S || true

echo ""
echo "=== Search for any import specifiers of parseMarkdownSafe etc from wysiwyg-editor ==="
rg -n "from\s+['\"].*wysiwyg-editor['\"]" frontend/user-profile-wysiwyg-editor.js -n
rg -n "parseMarkdownSafe|highlightPreviewCodeBlocks|renderMermaidPreview|turndown" frontend/user-profile-wysiwyg-editor.js -n

Repository: boostorg/website-v2

Length of output: 2403


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Check for any ES-module exports of these symbols in wysiwyg-editor.js
echo "=== wysiwyg-editor.js: export statements referencing these symbols ==="
for sym in parseMarkdownSafe highlightPreviewCodeBlocks renderMermaidPreview turndown; do
  echo "--- $sym ---"
  rg -n "export\s+(const|function)\s+$sym\b" frontend/wysiwyg-editor.js || true
  rg -n "export\s*\{[^}]*\b$sym\b[^}]*\}" frontend/wysiwyg-editor.js || true
  rg -n "\bexport\s*\*\s*from\s*['\"][^'\"]*['\"]" frontend/wysiwyg-editor.js || true
done

echo ""
echo "=== wysiwyg-editor.js: all occurrences of 'export' (context) ==="
rg -n "\bexport\b" frontend/wysiwyg-editor.js

echo ""
# 2) Check for global/window assignments in frontend for these symbols
echo "=== frontend/: global assignments for these symbols ==="
for sym in parseMarkdownSafe highlightPreviewCodeBlocks renderMermaidPreview turndown; do
  echo "--- $sym ---"
  rg -n "globalThis\.${sym}\b|window\.${sym}\b|global\.${sym}\b|self\.${sym}\b" frontend -S || true
done

echo ""
echo "=== user-profile-wysiwyg-editor.js: any imports of these symbols from wysiwyg-editor ==="
rg -n "from\s+['\"]\./wysiwyg-editor['\"]" frontend/user-profile-wysiwyg-editor.js
rg -n "import\s*\{[^}]*\b(parseMarkdownSafe|highlightPreviewCodeBlocks|renderMermaidPreview|turndown)\b[^}]*\}\s*from\s+['\"]\./wysiwyg-editor['\"]" frontend/user-profile-wysiwyg-editor.js || true

echo ""
echo "=== user-profile-wysiwyg-editor.js: whether these symbols are declared locally ==="
for sym in parseMarkdownSafe highlightPreviewCodeBlocks renderMermaidPreview turndown; do
  rg -n "(function|const|let|var)\s+$sym\b" frontend/user-profile-wysiwyg-editor.js || true
done

Repository: boostorg/website-v2

Length of output: 1010


Fix undefined identifiers in user-profile-wysiwyg-editor.js (will crash with ReferenceError)

frontend/user-profile-wysiwyg-editor.js calls parseMarkdownSafe(), highlightPreviewCodeBlocks(), renderMermaidPreview(), and uses turndown, but none of these are imported or defined in this file. They are defined in frontend/wysiwyg-editor.js but are not exported, so these references will throw at runtime when the code paths run.

Snippet at issue location (final state):

      initialContent = parseMarkdownSafe(initialContent);
    } catch (_) {
      initialContent = rawContent;
    }

Impacted identifiers/usages:

  • parseMarkdownSafe() (157, 199, 239, 267, 287)
  • highlightPreviewCodeBlocks() (240, 288)
  • renderMermaidPreview() (241, 289)
  • turndown (253, 308)

Solution: Export parseMarkdownSafe, highlightPreviewCodeBlocks, renderMermaidPreview, and turndown (or a helper that returns turndown) from frontend/wysiwyg-editor.js and import them here, or implement/instantiate the same logic locally in frontend/user-profile-wysiwyg-editor.js.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/user-profile-wysiwyg-editor.js` around lines 157 - 160, The file
references undefined symbols parseMarkdownSafe, highlightPreviewCodeBlocks,
renderMermaidPreview, and turndown causing ReferenceErrors; fix by exporting
those helpers (or a single helper that returns turndown) from
frontend/wysiwyg-editor.js and importing them into
frontend/user-profile-wysiwyg-editor.js, or alternatively implement equivalent
local versions in user-profile-wysiwyg-editor.js that match the behavior used
around initialContent parsing and preview rendering (update the catch block
usage of parseMarkdownSafe, the preview calls to
highlightPreviewCodeBlocks/renderMermaidPreview, and any turndown
instantiation/usage to use the imported or locally defined identifiers).

}

const editorRef = { current: null };
const editor = new Editor({
element: editorEl,
extensions: [
StarterKit.configure({ codeBlock: false }),
Underline,
TaskList,
TaskItem.configure({ nested: true }),
],
content: initialContent,
editorProps: {
attributes: {
class: "wysiwyg-editor__prose",
},
handleKeyDown(view, event) {
if (event.key === "Tab") {
const { $from } = view.state.selection;
if ($from.parent.type.name === "codeBlock") {
event.preventDefault();
editorRef.current?.chain().focus().insertContent("\t").run();
return true;
}
}
return false;
},
handlePaste(_view, event) {
const pastedText = event.clipboardData?.getData("text/plain") || "";
if (!pastedText.trim() || !editorRef.current) return false;
const trimmed = pastedText.trim();
const looksLikeMarkdown =
(!trimmed.startsWith("<") &&
(/^#|^\*\*|^\- |^\d+\. |^`|^\[|^>|^\||^\- \[ \]|^\- \[x\]/i.test(trimmed) ||
/\n```|\n#{1,6}\s|\n\*\*|\n\- |\n\d+\. |\n\|---|\n\- \[ \]/.test(pastedText)));
if (looksLikeMarkdown) {
try {
event.preventDefault();
const html = parseMarkdownSafe(pastedText);
editorRef.current.chain().focus().insertContent(html).run();
return true;
} catch (_) {
return false;
}
}
return false;
},
},
});
editorRef.current = editor;

const state = { mode: "wysiwyg", markdownText: "", previewOn: false };

const { mdBtn, previewBtn, handleDocClick } = buildToolbar(editor, toolbarEl);
setupMermaidEditMode(editor, editorEl);

const markdownPane = document.createElement("div");
markdownPane.className = "wysiwyg-editor__markdown-pane";
markdownPane.style.display = "none";

const mdTextarea = document.createElement("textarea");
mdTextarea.className = "wysiwyg-markdown__textarea";
mdTextarea.setAttribute("aria-label", "Markdown source");
mdTextarea.setAttribute("placeholder", "Write markdown here...");

const mdPreview = document.createElement("div");
mdPreview.className = "wysiwyg-markdown__preview wysiwyg-editor__prose";

markdownPane.appendChild(mdTextarea);
markdownPane.appendChild(mdPreview);
editorEl.after(markdownPane);

const previewEl = document.createElement("div");
previewEl.className = "wysiwyg-editor__preview wysiwyg-editor__prose";
previewEl.style.display = "none";
markdownPane.after(previewEl);

const updateMdPreview = () => {
mdPreview.innerHTML = parseMarkdownSafe(state.markdownText);
highlightPreviewCodeBlocks(mdPreview);
renderMermaidPreview(mdPreview);
};
const debouncedMdPreview = debounce(updateMdPreview, 300);

mdTextarea.addEventListener("input", () => {
state.markdownText = mdTextarea.value;
debouncedMdPreview();
});

mdBtn.addEventListener("click", (e) => {
e.preventDefault();
if (state.mode === "wysiwyg") {
state.markdownText = turndown.turndown(editor.getHTML());
state.mode = "markdown";
state.previewOn = false;
mdBtn.classList.add("wysiwyg-toolbar__btn--active");
toolbarEl.classList.add("wysiwyg-editor__toolbar--markdown");
editorEl.style.display = "none";
markdownPane.style.display = "";
previewEl.style.display = "none";
previewBtn.style.display = "";
previewBtn.classList.remove("wysiwyg-toolbar__btn--active");
mdTextarea.value = state.markdownText;
updateMdPreview();
mdTextarea.focus();
} else {
editor.commands.setContent(parseMarkdownSafe(state.markdownText));
state.mode = "wysiwyg";
state.previewOn = false;
mdBtn.classList.remove("wysiwyg-toolbar__btn--active");
toolbarEl.classList.remove("wysiwyg-editor__toolbar--markdown");
editorEl.style.display = "";
markdownPane.style.display = "none";
previewEl.style.display = "none";
previewBtn.style.display = "none";
previewBtn.classList.remove("wysiwyg-toolbar__btn--active");
}
});

previewBtn.addEventListener("click", (e) => {
e.preventDefault();
state.previewOn = !state.previewOn;
previewBtn.classList.toggle("wysiwyg-toolbar__btn--active", state.previewOn);
if (state.previewOn) {
markdownPane.style.display = "none";
previewEl.style.display = "";
previewEl.innerHTML = parseMarkdownSafe(state.markdownText);
highlightPreviewCodeBlocks(previewEl);
renderMermaidPreview(previewEl);
} else {
markdownPane.style.display = "";
previewEl.style.display = "none";
}
});

textarea.style.position = "absolute";
textarea.style.left = "-9999px";
textarea.style.width = "1px";
textarea.style.height = "1px";
textarea.setAttribute("aria-hidden", "true");
textarea.tabIndex = -1;

const form = wrapper.closest("form");
const syncTextarea = () => {
if (state.mode === "markdown") {
textarea.value = state.markdownText;
} else {
textarea.value = turndown.turndown(editor.getHTML());
}
};
if (form) {
form.addEventListener("submit", syncTextarea, true);
}

editorInstances.set(textareaId, {
editor,
cleanup: () => {
document.removeEventListener("click", handleDocClick);
if (form) form.removeEventListener("submit", syncTextarea, true);
},
});
return editor;
};

const autoInit = () => {
if (typeof document === "undefined" || !document.querySelector) return;
document.querySelectorAll('[data-wysiwyg="v3"]').forEach((wrapper) => {
const ta = wrapper.querySelector("textarea[id]");
if (ta && ta.id) initWysiwyg(ta.id);
});
};

if (typeof document !== "undefined") {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", autoInit);
} else {
autoInit();
}
}
8 changes: 4 additions & 4 deletions frontend/wysiwyg-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,15 @@ const getMermaid = async () => {
return mermaidModule;
};

const debounce = (fn, ms) => {
export const debounce = (fn, ms) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
};

const createToolbarButton = (editor, opts) => {
export const createToolbarButton = (editor, opts) => {
const { label, onClick, isActive, title } = opts;
const btn = document.createElement("button");
btn.type = "button";
Expand Down Expand Up @@ -261,7 +261,7 @@ const createHeadingDropdown = (editor) => {
return select;
};

const ICONS = {
export const ICONS = {
bulletList:
'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>',
orderedList:
Expand Down Expand Up @@ -604,7 +604,7 @@ const buildToolbar = (editor, toolbarEl) => {
return { mdBtn, previewBtn, handleDocClick };
};

const setupMermaidEditMode = (editor, editorEl) => {
export const setupMermaidEditMode = (editor, editorEl) => {
const renderMermaid = debounce(async () => {
const pres = editorEl.querySelectorAll("pre");
const activePreviews = new Set();
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"builddocs": "NODE_ENV=production tailwindcss -c ./docstailwind.config.js -i frontend/docsstyles.css -o static/css/docsstyles.css --minify",
"builduserguide": "NODE_ENV=production tailwindcss -c ./userguidetailwind.config.js -i frontend/userguidestyles.css -o static/css/userguidestyles.css --minify",
"build:wysiwyg": "esbuild frontend/wysiwyg-editor.js --bundle --minify --format=esm --outfile=static/js/v3/wysiwyg-editor.js --external:mermaid",
"build:up-wysiwyg": "esbuild frontend/user-profile-wysiwyg-editor.js --bundle --minify --format=esm --outfile=static/js/v3/user-profile-wysiwyg-editor.js --external:mermaid",
"build:fuse": "esbuild frontend/fuse-entry.js --bundle --minify --format=iife --outfile=static/js/v3/fuse.min.js"
},
"dependencies": {
Expand Down
Loading
Loading