Skip to content

Allow tip format functions to return DOM nodes for rich text - #2444

Open
kirthi-b wants to merge 2 commits into
observablehq:mainfrom
kirthi-b:feat/tip-title-markup
Open

Allow tip format functions to return DOM nodes for rich text#2444
kirthi-b wants to merge 2 commits into
observablehq:mainfrom
kirthi-b:feat/tip-title-markup

Conversation

@kirthi-b

Copy link
Copy Markdown

Closes the long-standing request for rich text and markup in tips (#1767).

This implements the "relatively easy" direction that mbostock outlined in the thread: a custom format function for the title channel, or any other channel, may now return a DOM node instead of a string. The returned node is appended directly to the tip rather than being wrapped in a text node, so you can style and hyperlink tip content, for example with an SVG tspan or anchor element built with Hypertext Literal:

Plot.tip(data, {
  title: "name",
  format: {
    title: (name) => htl.svg`<tspan><tspan font-weight="bold">${name}</tspan> <a fill="blue" href="/details/${name}">details</a></tspan>`
  }
})

Safety: only a DOM node returned by a format function is treated as rich content. Plain string formats are unchanged and always rendered as text, so no HTML is ever parsed and this adds no injection surface. Because a returned node is appended as-is rather than measured, the lineWidth and textOverflow options do not apply to it.

Changes:

  • src/marks/tip.js: append a returned node directly in renderLine, and route a title-channel node through renderLine; small isNode helper.
  • src/marks/tip.d.ts: TipFormat may now return string | Node.
  • docs/marks/tip.md: document the behavior, safety note, and an example.
  • test/plots/tip-format.ts: snapshot tests for a title node and a name-value channel node.

I scoped this to the format-function approach only, and did not add the separate content channel or the href channel also floated in the thread, since those are larger API surfaces you may want to design yourselves. The VersionBadge version in the docs is a placeholder for you to set to whatever you plan to release.

Fixes #1767

A custom format function for the title channel or any other channel may
now return a DOM node, such as an SVG tspan or anchor element, instead of
a string. The returned node is appended directly to the tip rather than
being wrapped in a text node, allowing styled and hyperlinked rich text.
Plain string formats are unchanged and still rendered as text, so this
adds no HTML injection surface.

Fixes observablehq#1767

@Fil Fil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you for the PR. I'm sorry I haven't found enough time to fully review, but this looks promising. I noticed that the channel markup case is missing a space between the field name+colon and the contents (as you can see in tipFormatChannelMarkup.svg).

Comment thread docs/marks/tip.md Outdated

The order and formatting of channels in the tip can be customized with the **format** option <VersionBadge version="0.6.11" pr="1823" />, which accepts a key-value object mapping channel names to formats. Each [format](../features/formats.md) can be a string (for number or time formats), a function that receives the value as input and returns a string, true to use the default format, and null or false to suppress. The order of channels in the tip follows their order in the format object followed by any additional channels. When using the **title** channel, the **format** option may be specified as a string or a function; the given format will then apply to the **title** channel. <VersionBadge version="0.6.15" pr="2074" />

A format function may also return a DOM node instead of a string, such as an SVG tspan or anchor element (say generated with [Hypertext Literal](https://github.com/observablehq/htl)); the returned node is appended directly to the tip, allowing styled and hyperlinked rich text. <VersionBadge version="0.6.18" pr="1767" /> For example, to render the **title** channel in bold followed by a link:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
A format function may also return a DOM node instead of a string, such as an SVG tspan or anchor element (say generated with [Hypertext Literal](https://github.com/observablehq/htl)); the returned node is appended directly to the tip, allowing styled and hyperlinked rich text. <VersionBadge version="0.6.18" pr="1767" /> For example, to render the **title** channel in bold followed by a link:
A format function may also return a DOM node, such as an SVG tspan or anchor element (say generated with [Hypertext Literal](https://github.com/observablehq/htl)); the returned node is appended directly to the tip, allowing styled and hyperlinked rich text. <VersionBadge pr="2444" /> For example, to render the **title** channel in bold followed by a link:

Comment thread docs/marks/tip.md Outdated
})
```

Only DOM nodes returned by a format function are treated as rich content; plain strings are always rendered as text, so this does not introduce any HTML injection. Because a returned node is appended as-is rather than measured, the **lineWidth** and **textOverflow** options do not apply to it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
Only DOM nodes returned by a format function are treated as rich content; plain strings are always rendered as text, so this does not introduce any HTML injection. Because a returned node is appended as-is rather than measured, the **lineWidth** and **textOverflow** options do not apply to it.
When returning nodes, the **lineWidth** and **textOverflow** options are ignored.

Comment thread src/marks/tip.js Outdated
Comment on lines +160 to +163
if (isNode(lines)) {
// A format function may return a DOM node (e.g., an SVG tspan
// or anchor) to display styled or hyperlinked rich text.
renderLine(that, {value: lines});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (isNode(lines)) {
// A format function may return a DOM node (e.g., an SVG tspan
// or anchor) to display styled or hyperlinked rich text.
renderLine(that, {value: lines});
if (lines?.ownerDocument) {
// A format function may return a DOM node (e.g., an SVG tspan
// or anchor) to display styled or hyperlinked rich text.
renderLine(that, {value: lines});

Precedent for this simpler test:

if (contents.ownerDocument) return contents;

Also I would like to avoid testing twice if this is a node.

Apply the suggested wording in docs/marks/tip.md and use the ownerDocument
test for DOM nodes, matching createTitleElement in plot.js. The node path
now adds a space between the channel label and the node, as the string
path already did; the channel markup snapshot is updated accordingly.

Node rendering moves into its own renderNodeLine so a node value is only
tested once rather than again inside renderLine.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow the tip title channel to contain rich text or markup

2 participants