mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-14 13:49:18 +02:00
Replace Tiptap's BubbleMenu plugin with @floating-ui/react-dom for
all floating editor UI (formatting toolbar, link preview cards).
Architecture:
- useFloating({ strategy:"fixed" }) + createPortal(body) escapes
all overflow:hidden ancestors (Card component, scroll containers)
- autoUpdate + contextElement monitors all scroll ancestors for
repositioning; manual update() on transaction for virtual ref changes
- open prop resets isPositioned on visibility change (no stale-position
flash at 0,0)
- display:none for hiding (not return null which causes blur/focus
cycle, not visibility:hidden which leaves transition artifacts)
- No blur listener — portal DOM updates cause false editor blurs;
outside-click + scroll + resize + Escape handle all close cases
Bug fixes:
- BubbleMenu: remove all custom visibility hacks, let selection state
drive show/hide
- Link preview: new shared card (Copy + Open) for editable editor and
readonly markdown, portaled to body with fixed positioning
- TitleEditor: use JSON content format (not HTML interpolation that
loses < > characters)
- Blob URLs: strip from getMarkdown output during upload
- Markdown paste: check clipboard.files first to avoid intercepting
file paste events
- FileCard: escape HTML attributes in preprocessing
- Link extension: enable linkOnPaste, set defaultProtocol to https,
switch URL normalization to protocol blocklist (only block
javascript:/data:/vbscript:)
Dependencies: add @floating-ui/react-dom, remove @tiptap/extension-bubble-menu
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
53 lines
2.1 KiB
TypeScript
53 lines
2.1 KiB
TypeScript
import { preprocessLinks } from "@multica/ui/markdown";
|
|
import { preprocessMentionShortcodes } from "@multica/ui/markdown";
|
|
import { isFileCardUrl } from "../extensions/file-card";
|
|
|
|
/**
|
|
* Preprocess a markdown string before loading into Tiptap via contentType: 'markdown'.
|
|
*
|
|
* This is the ONLY transform applied before @tiptap/markdown parses the content.
|
|
* It does NOT convert to HTML — that was the old markdownToHtml.ts pipeline which
|
|
* was deleted in the April 2026 refactor.
|
|
*
|
|
* Three string→string transforms on raw Markdown:
|
|
* 1. Legacy mention shortcodes [@ id="..." label="..."] → [@Label](mention://member/id)
|
|
* (old serialization format in database, migrated on read)
|
|
* 2. Raw URLs → markdown links via linkify-it (so they render as clickable Link nodes)
|
|
* 3. CDN file links on their own line → HTML div for fileCard node parsing
|
|
*/
|
|
export function preprocessMarkdown(markdown: string): string {
|
|
if (!markdown) return "";
|
|
const step1 = preprocessMentionShortcodes(markdown);
|
|
const step2 = preprocessLinks(step1);
|
|
const step3 = preprocessFileCards(step2);
|
|
return step3;
|
|
}
|
|
|
|
/**
|
|
* Convert standalone `[name](cdnUrl)` lines into HTML that Tiptap's fileCard
|
|
* parseHTML can recognise. Only matches non-image CDN URLs on their own line.
|
|
*
|
|
* Input: `[report.pdf](https://multica-static.copilothub.ai/xxx.pdf)`
|
|
* Output: `<div data-type="fileCard" data-href="url" data-filename="report.pdf"></div>`
|
|
*/
|
|
const FILE_LINK_LINE = /^\[([^\]]+)\]\((https?:\/\/[^)]+)\)$/;
|
|
|
|
function escapeAttr(s: string): string {
|
|
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
}
|
|
|
|
function preprocessFileCards(markdown: string): string {
|
|
return markdown
|
|
.split("\n")
|
|
.map((line) => {
|
|
const trimmed = line.trim();
|
|
const match = trimmed.match(FILE_LINK_LINE);
|
|
if (!match) return line;
|
|
const filename = match[1]!;
|
|
const url = match[2]!;
|
|
if (!isFileCardUrl(url)) return line;
|
|
return `<div data-type="fileCard" data-href="${escapeAttr(url)}" data-filename="${escapeAttr(filename)}"></div>`;
|
|
})
|
|
.join("\n");
|
|
}
|