🚀. Socket Launch Week Day 2:Introducing Manifest Alerts.Learn more
Sign In

@datocms/content-link

Package Overview
Dependencies
Maintainers
7
Versions
31
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@datocms/content-link - npm Package Compare versions

Comparing version
0.3.15
to
0.3.16
+1
-1
package.json
{
"name": "@datocms/content-link",
"version": "0.3.15",
"version": "0.3.16",
"description": "DatoCMS visual editing overlays without Vercel dependencies.",

@@ -5,0 +5,0 @@ "type": "module",

+182
-73

@@ -210,88 +210,213 @@ # DatoCMS Content Link

### Edit groups with `data-datocms-content-link-group`
### Data attributes reference
In some cases, you may want to make a larger area clickable than the specific element containing the stega-encoded information. You can achieve this by adding the `data-datocms-content-link-group` attribute to a parent element.
This library uses several `data-datocms-*` attributes. Some are **developer-specified** (you add them to your markup), and some are **library-managed** (added automatically during DOM stamping). Here's a complete reference.
**Structured text fields**
#### Developer-specified attributes
This attribute is particularly useful when rendering **Structured Text** fields. The DatoCMS GraphQL CDA encodes stega information within a specific `span` node inside the structured text content. This means that by default, only that particular span would be clickable to open the editor.
These attributes are added by you in your templates/components to control how editable regions behave.
To provide a better editing experience, we recommend wrapping your structured text rendering component with a container that has the `data-datocms-content-link-group` attribute. This makes the entire structured text area clickable:
##### `data-datocms-content-link-url`
Manually marks an element as editable with an explicit edit URL. Use this for non-text fields (booleans, numbers, dates, JSON) that cannot contain stega encoding. The recommended approach is to use the `_editingUrl` field available on all records:
```graphql
query {
product {
id
price
isActive
_editingUrl
}
}
```
```tsx
<div data-datocms-content-link-group>
<StructuredText data={content.structuredTextField} />
<span data-datocms-content-link-url={product._editingUrl}>
${product.price}
</span>
```
##### `data-datocms-content-link-source`
Attaches stega-encoded metadata without the need to render it as content. Useful for structural elements that cannot contain text (like `<video>`, `<audio>`, `<iframe>`, etc.) or when stega encoding in visible text would be problematic:
```tsx
<div data-datocms-content-link-source={video.alt}>
<video src={video.url} poster={video.posterImage.url} controls />
</div>
```
This way, users can click anywhere within the structured text content to edit it, rather than having to precisely target a small span element.
The value must be a stega-encoded string (any text field from the API will work). The library decodes the stega metadata from the attribute value and makes the element clickable to edit.
**Edit boundaries with `data-datocms-content-link-boundary`**
##### `data-datocms-content-link-group`
By default, when the library encounters stega-encoded content, it searches up the DOM tree to find the nearest `data-datocms-content-link-group` attribute. However, you can stop this upward traversal at any point using the `data-datocms-content-link-boundary` attribute.
Expands the clickable area to a parent element. When the library encounters stega-encoded content, by default it makes the immediate parent of the text node clickable to edit. Adding this attribute to an ancestor makes that ancestor the clickable target instead:
This is particularly useful with **Structured Text** fields that contain embedded blocks: while the main structured text paragraphs, headings, and lists should open the structured text field editor, embedded blocks should open their own specific record editor instead:
```html
<article data-datocms-content-link-group>
<h2>Title with stega</h2>
<p>Description with no stega</p>
</article>
```
```tsx
Here, clicking anywhere in the `<article>` opens the editor, rather than requiring users to click precisely on the `<h2>`.
**Important:** A group should contain only one stega-encoded source. If multiple stega strings resolve to the same group, the library logs a collision warning and only the last URL wins.
##### `data-datocms-content-link-boundary`
Stops the upward DOM traversal that looks for a `data-datocms-content-link-group`, making the element where stega was found the clickable target instead. This creates an independent editable region that won't merge into a parent group (see [How group and boundary resolution works](#how-group-and-boundary-resolution-works) below for details):
```html
<div data-datocms-content-link-group>
<StructuredText
data={content.structuredTextField}
renderBlock={(block) => (
<div data-datocms-content-link-boundary>
<BlockComponent block={block} />
</div>
)}
/>
<h1>Title with stega (URL A)</h1>
<section data-datocms-content-link-boundary>
<span>Text with stega (URL B)</span>
</section>
</div>
```
In this example:
- The main structured text content will use the outer `div[data-datocms-content-link-group]` for editing
- Each embedded block will **not** traverse past its `div[data-datocms-content-link-boundary]`, creating its own independent editable region
Without the boundary, clicking "Text with stega" would open URL A (the outer group). With the boundary, the `<span>` becomes the clickable target opening URL B.
This ensures that clicking on the main text opens the structured text field editor, while clicking on an embedded block opens that specific block's editor.
The boundary can also be placed directly on the element that contains the stega text:
### Manual overlays with `data-datocms-content-link-url`
```html
<div data-datocms-content-link-group>
<h1>Title with stega (URL A)</h1>
<span data-datocms-content-link-boundary>Text with stega (URL B)</span>
</div>
```
For text-based fields (single-line text, structured text, markdown), the DatoCMS API automatically embeds stega-encoded information, which this library detects to create overlays. However, non-text fields like booleans, numbers, dates, and JSON cannot contain stega encoding.
Here, the `<span>` has the boundary and directly contains the stega text, so the `<span>` itself becomes the clickable target (since the starting element and the boundary element are the same).
For these cases, use the `data-datocms-content-link-url` attribute to manually specify the edit URL. The recommended approach is to use the `_editingUrl` field available on all records:
#### Library-managed attributes
```graphql
query {
product {
id
price
isActive
_editingUrl
}
These attributes are added automatically by the library during DOM stamping. You do not need to add them yourself, but you can target them in CSS or JavaScript.
##### `data-datocms-contains-stega`
Added to elements whose text content contains stega-encoded invisible characters. This attribute is only present when `stripStega` is `false` (the default), since with `stripStega: true` the characters are removed entirely. Useful for CSS workarounds — the zero-width characters can sometimes cause unexpected letter-spacing or text overflow:
```css
[data-datocms-contains-stega] {
letter-spacing: 0 !important;
}
```
Then add the attribute to your element:
##### `data-datocms-auto-content-link-url`
```tsx
<span data-datocms-content-link-url={product._editingUrl}>
${product.price}
</span>
Added automatically to elements that the library has identified as editable targets (through stega decoding and group/boundary resolution). Contains the resolved edit URL.
This is the automatic counterpart to the developer-specified `data-datocms-content-link-url`. The library adds `data-datocms-auto-content-link-url` wherever it can extract an edit URL from stega encoding, while `data-datocms-content-link-url` is needed for non-text fields (booleans, numbers, dates, etc.) where stega encoding cannot be embedded. Both attributes are used by the click-to-edit overlay system to determine which elements are clickable and where they link to.
### How group and boundary resolution works
When the library encounters stega-encoded content inside an element, it walks up the DOM tree from that element:
1. If it finds a `data-datocms-content-link-group`, it stops and stamps **that** element as the clickable target.
2. If it finds a `data-datocms-content-link-boundary`, it stops and stamps the **starting element** as the clickable target — further traversal is prevented.
3. If it reaches the root without finding either, it stamps the **starting element**.
Here are some concrete examples to illustrate:
**Example 1: Nested groups**
```html
<div data-datocms-content-link-group>
<h1>Title with stega (URL A)</h1>
<div data-datocms-content-link-group>
<p>Paragraph with stega (URL B)</p>
</div>
</div>
```
This ensures the URL format is always correct and adapts automatically to any future changes.
- **"Title with stega"**: walks up from `<h1>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **"Paragraph with stega"**: walks up from `<p>`, finds the inner group first → the **inner `<div>`** becomes clickable (opens URL B). The outer group is never reached.
### Stamping elements via `data-datocms-content-link-source`
Each nested group creates an independent clickable region. The innermost group always wins for its own content.
In some cases, you may want to provide stega-encoded metadata for an element without rendering any visible stega-encoded content. The `data-datocms-content-link-source` attribute allows you to attach stega metadata directly to any element.
**Example 2: Boundary preventing group propagation**
This is particularly useful when:
- You want to make a container element editable without stega-encoded text content
- You're rendering components where stega encoding in visible text would be problematic
- You need to provide metadata for structural elements that don't contain text (like `<video>`, `<audio>`, `<iframe>`, etc.)
```html
<div data-datocms-content-link-group>
<h1>Title with stega (URL A)</h1>
<section data-datocms-content-link-boundary>
<span>Text with stega (URL B)</span>
</section>
</div>
```
- **"Title with stega"**: walks up from `<h1>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **"Text with stega"**: walks up from `<span>`, hits the `<section>` boundary → traversal stops, the **`<span>`** itself becomes clickable (opens URL B). The outer group is not reached.
**Example 3: Boundary inside a group**
```html
<div data-datocms-content-link-group>
<p>Main content with stega (URL A)</p>
<div data-datocms-content-link-boundary>
<p>Isolated content with stega (URL B)</p>
</div>
</div>
```
- **"Main content with stega"**: walks up from `<p>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **"Isolated content with stega"**: walks up from `<p>`, hits the boundary → traversal stops, the **`<p>`** itself becomes clickable (opens URL B). The outer group is not reached.
**Example 4: Multiple stega strings without groups (collision warning)**
```html
<p>
Text with stega (URL A)
More text with stega (URL B)
</p>
```
Both stega-encoded strings resolve to the same `<p>` element. The library logs a console warning and the last URL wins. To fix this, wrap each piece of content in its own element:
```html
<p>
<span>Text with stega (URL A)</span>
<span>More text with stega (URL B)</span>
</p>
```
### Structured Text fields
Structured Text fields require special attention because of how stega encoding works within them:
- The DatoCMS API encodes stega information inside a single `<span>` within the structured text output. Without any configuration, only that small span would be clickable.
- Structured Text fields can contain **embedded blocks** and **inline records**, each with their own editing URL that should open a different record in the editor.
Here are the rules to follow:
#### Rule 1: Always wrap the Structured Text component in a group
This makes the entire structured text area clickable, instead of just the tiny stega-encoded span:
```tsx
// Use any stega-encoded text field as the source
<div data-datocms-content-link-source={video.alt}>
<video
src={video.url}
poster={video.posterImage.url}
controls
<div data-datocms-content-link-group>
<StructuredText data={page.content} />
</div>
```
#### Rule 2: Wrap embedded blocks and inline records in a boundary
Embedded blocks and inline records have their own edit URL (pointing to the block/record). Without a boundary, clicking them would bubble up to the parent group and open the structured text field editor instead. Add `data-datocms-content-link-boundary` to prevent them from merging into the parent group:
```tsx
<div data-datocms-content-link-group>
<StructuredText
data={page.content}
renderBlock={(block) => (
<div data-datocms-content-link-boundary>
<BlockComponent block={block} />
</div>
)}
renderInlineRecord={(record) => (
<span data-datocms-content-link-boundary>
<InlineRecordComponent record={record} />
</span>
)}
/>

@@ -301,2 +426,6 @@ </div>

With this setup:
- Clicking the main text (paragraphs, headings, lists) opens the **structured text field editor**
- Clicking an embedded block or inline record opens **that record's editor**
---

@@ -346,22 +475,2 @@

## Runtime & debugging
### Runtime behaviour
1. **DOM Stamping** (automatic): Walks text nodes, `<img alt>` values, and elements with `data-datocms-content-link-source` attribute inside `root`, decodes stega, stamps attributes (`data-datocms-stega`). By default, stega encoding is preserved in the DOM (invisible to users). If `stripStega: true` is set, the invisible characters are removed from content. MutationObserver watches for changes and rescans automatically.
2. **Click-to-Edit Overlays** (opt-in): When enabled via `enableClickToEdit()`, listens for hover/click/focus/keyboard events and highlights editable regions. Clicking opens the edit URL in the DatoCMS editor or a new tab. Can also be temporarily toggled by holding the Alt/Option key.
3. **Web Previews Plugin Connection** (automatic): When running inside the Web Previews plugin iframe, establishes bidirectional communication for state synchronization and remote control.
4. **Dispose**: Disconnects all observers, tears down listeners, clears stamps, and cleans up.
### Architecture
The controller orchestrates several independent managers:
- **DomStampingManager**: Handles DOM observation, mutation batching, stega decoding, and attribute stamping
- **ClickToEditManager**: Handles visual highlighting and user interactions (only active when enabled)
- **FlashAllManager**: Handles the animated flash-all highlighting feature
- **EventsManager**: Manages custom events for state changes and user interactions
- **WebPreviewsPluginConnection**: Handles bidirectional communication with the Web Previews plugin via iframe messaging (Penpal)
All managers can work independently - stamping continues even when click-to-edit is disabled, and the plugin connection is only established when running inside an iframe.
## Troubleshooting

@@ -368,0 +477,0 @@

@@ -335,4 +335,4 @@ /**

// If we hit an edit boundary, stop and return current element
if (current !== start && current.hasAttribute(GROUP_BOUNDARY_ATTRIBUTE)) {
// If we hit an edit boundary, stop and return the starting element
if (current.hasAttribute(GROUP_BOUNDARY_ATTRIBUTE)) {
return start;

@@ -339,0 +339,0 @@ }

@@ -61,3 +61,3 @@ import { type Options, compute as computeScrollIntoView } from 'compute-scroll-into-view';

/** Compute the bounding box for the target element, ignoring zero-size nodes. */
/** Compute the bounding box for the target element, ignoring zero-size or hidden nodes. */
export function measure(

@@ -73,2 +73,5 @@ el: Element

}
if (!isElementVisible(el as HTMLElement)) {
return null;
}
return {

@@ -75,0 +78,0 @@ top: rect.top,

@@ -46,3 +46,3 @@ /**

this.showLabel = options.showLabel ?? false;
this.overlayElement = this.createOverlayElement(this.showLabel);

@@ -157,5 +157,10 @@ document.body.appendChild(this.overlayElement);

label.style.fontSize = '13px';
label.style.fontWeight = '500';
label.style.fontWeight = '600';
label.style.fontFamily = 'system-ui, -apple-system, sans-serif';
label.style.whiteSpace = 'nowrap';
label.style.setProperty('-webkit-font-smoothing', 'antialiased');
label.style.letterSpacing = 'normal';
label.style.lineHeight = 'normal';
label.style.textTransform = 'none';
label.style.fontStyle = 'normal';
label.setAttribute('aria-hidden', 'true');

@@ -175,5 +180,7 @@ overlay.appendChild(label);

if (!rect) {
this.overlayElement.style.display = 'none';
return;
}
this.overlayElement.style.display = 'block';
this.overlayElement.style.top = `${rect.top - DEFAULT_OVERLAY_PADDING}px`;

@@ -183,2 +190,24 @@ this.overlayElement.style.left = `${rect.left - DEFAULT_OVERLAY_PADDING}px`;

this.overlayElement.style.height = `${rect.height + DEFAULT_OVERLAY_PADDING * 2}px`;
if (this.showLabel) {
const label = this.overlayElement.firstElementChild as HTMLElement | null;
if (label) {
const isNarrow = rect.width + DEFAULT_OVERLAY_PADDING * 2 < 150;
if (isNarrow) {
label.style.bottom = `calc(100% + 10px)`;
label.style.right = 'auto';
label.style.left = '50%';
label.style.transform = 'translateX(-50%)';
label.style.borderRadius = `${DEFAULT_BORDER_RADIUS}`;
this.overlayElement.style.borderRadius = DEFAULT_BORDER_RADIUS;
} else {
label.style.bottom = '100%';
label.style.right = `-${DEFAULT_BORDER_WIDTH}`;
label.style.left = 'auto';
label.style.transform = 'none';
label.style.borderRadius = `${DEFAULT_BORDER_RADIUS} ${DEFAULT_BORDER_RADIUS} 0 0`;
this.overlayElement.style.borderRadius = `${DEFAULT_BORDER_RADIUS} 0 ${DEFAULT_BORDER_RADIUS} ${DEFAULT_BORDER_RADIUS}`;
}
}
}
}

@@ -185,0 +214,0 @@

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display