Details
-
Bug
-
Resolution: Unresolved
-
Major
-
None
-
18.6.0-rc-1
-
None
-
Unknown
-
Description
XWIKI-24237 added a button to edit the selection macro. Currently, this works only for block-level macros. We need to find a solution for inline macros as well. This is not as easy as it seems. I got this plan from Claude:
# Activate the edit-macro toolbar button for selected INLINE macros
## Context
The edit-macro toolbar button (`CustomMacroEditButton.tsx`) now works for **block** macros: it resolves
the node-selected block via `editor.getTextCursorPosition().block` and matches `xwikiMacroBlock`
(XWiki) or a `Macro_<id>` block (Cristal). It does **not** handle **inline** macros
(`xwikiInlineMacro`, or Cristal inline `Macro_<id>` content), so there's no toolbar affordance to edit
an inline macro (only double-click works, via the inline spec's render in `macro.tsx`).
Goal: show + wire the edit button when an inline macro is **selected**. Per decision, scope is
**selection-based only** — the inline macro is node-selected or a non-empty text selection
touches/covers it (both are non-empty selections, so the formatting toolbar already appears). The
collapsed-caret case is explicitly out of scope (it would require overriding BlockNote's toolbar
visibility, which hides on empty selection).
### Key facts (from investigation)
- BlockNote exposes **no** high-level "inline content at selection" API. The sanctioned way is raw
ProseMirror access via `editor.transact((tr) => …)` — mirroring BlockNote's own `getSelectedLinkUrl`.
`editor.pmSchema` (public `readonly Schema`) and `editor.transact` are public/typed.
- A custom inline content node's **PM type name equals the BlockNote type string** (`"xwikiInlineMacro"`
/ `"Macro_<id>"`) and its **props are the node attrs** (`node.attrs.call`, `node.attrs.output`).
- Write-back mirrors the React inline node-view: `editor.transact((tr) => tr.replaceWith(pos,
pos + nodeSize, inlineContentToNodes([{ type, props }], editor.pmSchema)))`. `inlineContentToNodes`
is a public export of `@blocknote/core`.
- Reuse existing helpers in `src/blocknote/utils.tsx`: `macroCallToInvocation(call, "inline")`,
`invocationToMacroCall`, `MACRO_NAME_PREFIX`. `MacroEdit` in `CustomMacroEditButton.tsx` already
permits `InlineMacroInvocation`, so no type change there.
## Change
### 1. `selection.ts` — add inline-macro detection + replacement (centralize raw PM access here)
File: `.../blocknote-react/src/selection.ts` (already created for `selectionHasInlineContent`).
Add (importing `MACRO_NAME_PREFIX` from `./blocknote/utils` and `inlineContentToNodes` from
`@blocknote/core`):
```ts
type SelectedInlineMacro = {
type: string;
props: Record<string, unknown>;
pos: number;
nodeSize: number;
};
function isInlineMacroType(name: string): boolean {
return name === "xwikiInlineMacro" || name.startsWith(MACRO_NAME_PREFIX);
}
/** The inline macro node covered by the current (non-empty) selection, or null. */
function getSelectedInlineMacro(editor: EditorType): SelectedInlineMacro | null {
return editor.transact((tr) => {
const { from, to } = tr.selection;
if (from === to) {
return null; // collapsed caret — out of scope
}
let found: SelectedInlineMacro | null = null;
tr.doc.nodesBetween(from, to, (node, pos) => {
if (found) return false;
if (node.type.isInline && isInlineMacroType(node.type.name)) {
found = { type: node.type.name, props: { ...node.attrs }, pos, nodeSize: node.nodeSize };
return false;
}
return true;
});
return found;
});
}
/** Replace the inline content node at [pos, pos+nodeSize) with the given BlockNote inline content. */
function replaceInlineContent(
editor: EditorType,
pos: number,
nodeSize: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
content: { type: string; props: any },
): void {
editor.transact((tr) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tr.replaceWith(pos, pos + nodeSize, inlineContentToNodes([content as any], editor.pmSchema)),
);
}
export { getSelectedInlineMacro, replaceInlineContent };
export type { SelectedInlineMacro };
```
`nodesBetween(from, to, …)` covers both a NodeSelection (from=pos, to=pos+size) and a text selection
that wraps/touches the atom, so no separate `NodeSelection.node` handling is needed. It matches only
inline nodes (`node.type.isInline`), so block macros (`xwikiMacroBlock`, Cristal `Macro_<id>` block)
are correctly excluded.
### 2. `CustomMacroEditButton.tsx` — add an inline resolver, try it after the block resolver
File: `.../src/components/links/CustomMacroEditButton.tsx`
- Import `getSelectedInlineMacro`, `replaceInlineContent` from `../../selection`, and
`invocationToMacroCall`/`macroCallToInvocation`/`MACRO_NAME_PREFIX` (already imported) plus
`MacroCall`, `MacroCallParams` types (already imported).
- Add `resolveInlineMacroEdit(editor, inlineMacro)`:
```ts
function resolveInlineMacroEdit(
editor: ReturnType<typeof useEditor>,
inlineMacro: SelectedInlineMacro | null,
): MacroEdit | null {
if (!inlineMacro) return null;
const { type, props, pos, nodeSize } = inlineMacro;
if (type === "xwikiInlineMacro") {
return {
invocation: macroCallToInvocation(JSON.parse(props.call as string) as MacroCall, "inline"),
writeBack: (updated) =>
replaceInlineContent(editor, pos, nodeSize, {
type,
props: { call: JSON.stringify(invocationToMacroCall(updated)), output: "[]" },
}),
};
}
// Cristal client-rendered inline Macro_<id>: params are stored directly as node attrs.
return {
invocation: {
kind: "inline",
id: type.slice(MACRO_NAME_PREFIX.length),
params: props as MacroCallParams,
body: { type: "none" },
},
writeBack: (updated) => replaceInlineContent(editor, pos, nodeSize, { type, props: updated.params }),
};
}
```
- In the component, resolve block first, then inline:
```tsx
const block = editor.getTextCursorPosition().block;
const inlineMacro = getSelectedInlineMacro(editor);
const openMacroEditor = useMemo(() => {
const openParamsEditor = ctxForMacros.openParamsEditor;
if (!openParamsEditor) return null;
const edit = resolveMacroEdit(editor, block) ?? resolveInlineMacroEdit(editor, inlineMacro);
if (!edit) return null;
return () => openParamsEditor(edit.invocation, edit.writeBack);
}, [block, inlineMacro, ctxForMacros, editor]);
```
Block detection keeps priority (a node-selected block macro → block path; inline path only fires when
the selection covers an inline macro inside an ordinary block). `pos`/`nodeSize` are captured at
button-render time; the params modal only edits parameters (doc unchanged), so they stay valid at
write-back — mirroring how the block path captures `block.id`.
## Verification
1. Lint/type-check: `cd .../editors/blocknote-react && pnpm lint` and `npx tsc --noEmit`.
2. Rebuild the blocknote webjar (use the `xwiki-build` skill) so the running XWiki serves the new JS.
3. Manual smoke test:
- Select an inline macro (click to node-select it, or drag to select text over it) → toolbar shows
the **Edit macro** (pencil) button → clicking it opens the editor prefilled with the inline
macro's call; submitting updates it (placeholder `macro:<name>` until the next server round-trip).
- Normal inline text selection (no macro) → no edit button. Block macro → still works.
4. Functional test (recommended): extend `MacroIT` with an inline case. The load-bearing bit is
selecting the inline macro deterministically — reuse `BlockNoteRichTextArea.selectMacro(index)`
(its locator already includes `.bn-inline-content-section[data-inline-content-type="xwikiInlineMacro"]`)
if a single click node-selects the atom; otherwise select via a drag/`Shift`+arrow. Then
`editor.getToolBar().editMacro()`, change a parameter, submit, save, assert the updated wiki syntax.
Flag: the inline selection technique may need iteration during implementation — validate against the
real editor before locking the assertions.
Attachments
Issue Links
- is related to
-
XWIKI-24237 Provide a button on the context toolbar to edit the selected macro
-
- Closed
-