Skip to content

Commit e3e702a

Browse files
committed
improvement(rich-md-editor): stabilize the bubble-menu plugin key and move inline rationale into TSDoc
1 parent 4bf7917 commit e3e702a

4 files changed

Lines changed: 8 additions & 23 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/extensions.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ const InlineCode = Code.extend({ excludes: '' })
3030
* Table that escapes interior `|` characters when serializing cells. The upstream serializer
3131
* joins cells with `|` without escaping, so a cell containing a literal pipe silently splits
3232
* into phantom columns on round-trip (data loss). Escaping must happen on the `table` node —
33-
* `tableCell`/`tableHeader` have no markdown renderer; the table renders cell children directly.
33+
* `tableCell`/`tableHeader` have no markdown renderer; the table renders cell children directly. Only
34+
* `|` is escaped — `renderChildren` already escapes backslashes, so escaping them again would
35+
* double-escape and break round-trip idempotency (CodeQL's "missing backslash escape" is a false
36+
* positive here; covered by the table round-trip tests).
3437
*
3538
* The upstream serializer also wraps the table in its own leading/trailing blank lines; left in,
3639
* the block joiner adds another, so an interior table churns its surrounding whitespace to
@@ -42,9 +45,6 @@ const PipeSafeTable = Table.extend({
4245
renderMarkdown: (node: JSONContent, h: MarkdownRendererHelpers) =>
4346
renderTableToMarkdown(node, {
4447
...h,
45-
// `renderChildren` already markdown-escapes backslashes; here we only add the table-specific
46-
// pipe escaping on top. (CodeQL flags the missing backslash escape, but escaping it again would
47-
// double-escape and break round-trip idempotency — see the table round-trip tests.)
4848
renderChildren: (nodes, separator) =>
4949
h.renderChildren(nodes, separator).replace(/\|/g, '\\|'),
5050
})

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu.tsx

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
1+
import { useCallback, useEffect, useRef, useState } from 'react'
22
import { posToDOMRect } from '@tiptap/core'
33
import { PluginKey } from '@tiptap/pm/state'
44
import type { Editor } from '@tiptap/react'
@@ -32,12 +32,10 @@ function hasFormattableSelection(editor: Editor, from: number, to: number): bool
3232
return editor.state.doc.textBetween(from, to, ' ').trim().length > 0
3333
}
3434

35-
// Pin the toolbar to the viewport (fixed) and never attach a scroll listener, so once it's placed for
36-
// a selection it stays put while the document scrolls instead of tracking the text — matching Linear.
35+
/** Pins the toolbar to the viewport so it stays put while the document scrolls instead of tracking the text. */
3736
const FLOATING_OPTIONS = { strategy: 'fixed' } as const
3837

39-
// Render into the body so a transformed/clipping ancestor (e.g. the mothership panels) can't reparent
40-
// the fixed-positioned toolbar and shift it off the selection.
38+
/** Renders into the body so a transformed/clipping ancestor can't reparent the fixed toolbar and shift it. */
4139
const APPEND_TO_BODY = () => document.body
4240

4341
interface EditorBubbleMenuProps {
@@ -58,8 +56,7 @@ export function EditorBubbleMenu({ editor, scrollContainerRef }: EditorBubbleMen
5856
const linkRangeRef = useRef<{ from: number; to: number } | null>(null)
5957
const isEditingLink = linkValue !== null
6058

61-
// Explicit key so `setMeta` can target this menu to reveal it after a drag-select.
62-
const bubbleMenuKey = useMemo(() => new PluginKey('markdownBubbleMenu'), [])
59+
const [bubbleMenuKey] = useState(() => new PluginKey('markdownBubbleMenu'))
6360
const isPointerDownRef = useRef(false)
6461

6562
const active = useEditorState({
@@ -94,8 +91,6 @@ export function EditorBubbleMenu({ editor, scrollContainerRef }: EditorBubbleMen
9491
}
9592
}, [editor])
9693

97-
// Reveal the toolbar only once a drag-select finishes (Linear-style); `shouldShow` keeps it hidden
98-
// while the pointer is down. Keyboard selection has no pointer, so it still shows live.
9994
useEffect(() => {
10095
const dom = editor.view.dom
10196
const onPointerDown = () => {
@@ -106,13 +101,10 @@ export function EditorBubbleMenu({ editor, scrollContainerRef }: EditorBubbleMen
106101
isPointerDownRef.current = false
107102
const { from, to } = editor.state.selection
108103
if (hasFormattableSelection(editor, from, to)) {
109-
// `show` alone leaves the bar visible-but-unpositioned (its updatePosition no-ops until shown),
110-
// so a second `updatePosition` anchors it. Both are step-free, so the doc isn't marked dirty.
111104
editor.commands.setMeta(bubbleMenuKey, 'show')
112105
editor.commands.setMeta(bubbleMenuKey, 'updatePosition')
113106
}
114107
}
115-
// A release outside the window delivers no mouseup; clear the flag on blur so it can't stay wedged.
116108
const onWindowBlur = () => {
117109
isPointerDownRef.current = false
118110
}
@@ -175,10 +167,6 @@ export function EditorBubbleMenu({ editor, scrollContainerRef }: EditorBubbleMen
175167
setLinkValue(null)
176168
}
177169

178-
// Freeze the anchor per selection: the rect is computed once (in viewport coordinates) and reused on
179-
// every scroll/resize reposition, so the toolbar stays where it first appeared instead of tracking
180-
// the moving text — matching Linear. A new selection recomputes it. A selection taller than the
181-
// viewport (e.g. select-all) is clamped into the visible area so the bar isn't placed off-screen.
182170
const anchorCacheRef = useRef<{ key: string; rect: DOMRect } | null>(null)
183171
const resolveAnchor = useCallback(() => {
184172
const { view, state } = editor
@@ -218,7 +206,6 @@ export function EditorBubbleMenu({ editor, scrollContainerRef }: EditorBubbleMen
218206
// can't be applied to a doc that must not mutate.
219207
if (!e.isEditable) return false
220208
if (isEditingLink) return true
221-
// Suppressed mid-drag; the pointer-release handler forces it back open once the selection sticks.
222209
if (isPointerDownRef.current) return false
223210
return hasFormattableSelection(e, from, to)
224211
}}

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,6 @@ export function LoadedRichMarkdownEditor({
206206
shouldRerenderOnTransaction: false,
207207
content: initialContent,
208208
editorProps: {
209-
// Claim Mod+K so the global command registry yields it to the editor's link shortcut.
210209
attributes: { class: 'rich-markdown-prose', 'data-owned-shortcuts': 'Mod+K' },
211210
handleKeyDown: (_view, event) => {
212211
const isSaveShortcut = (event.metaKey || event.ctrlKey) && event.key?.toLowerCase() === 's'

apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@ export function GlobalCommandsProvider({ children }: { children: ReactNode }) {
151151
}
152152

153153
if (matchesShortcut(e, cmd.parsed)) {
154-
// A focused rich editor that owns this shortcut (e.g. Mod+K for links) handles it itself.
155154
if (focusedElementOwnsShortcut(cmd.parsed, isMac)) continue
156155
e.preventDefault()
157156
e.stopPropagation()

0 commit comments

Comments
 (0)