🚀. 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.18
to
0.3.19
+2
-0
dist/index.d.cts

@@ -9,2 +9,4 @@ /**

onNavigateTo?: (path: string) => void;
/** Hue (0–359) of the overlay accent color. Default: 17 (orange). */
hue?: number;
/**

@@ -11,0 +13,0 @@ * Whether to strip stega-encoded invisible characters from text content after stamping.

@@ -9,2 +9,4 @@ /**

onNavigateTo?: (path: string) => void;
/** Hue (0–359) of the overlay accent color. Default: 17 (orange). */
hue?: number;
/**

@@ -11,0 +13,0 @@ * Whether to strip stega-encoded invisible characters from text content after stamping.

+1
-1
{
"name": "@datocms/content-link",
"version": "0.3.18",
"version": "0.3.19",
"description": "Lightweight library for DatoCMS visual editing overlays and content links.",

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

@@ -11,4 +11,2 @@ # DatoCMS Content Link

![Usage demo](./docs/usage.gif)
## Quick start

@@ -60,3 +58,6 @@

// Optional: strip stega-encoded invisible characters from text content (default: false)
stripStega: false
stripStega: false,
// Optional: hue (0–359) of the overlay accent color (default: 17, orange)
hue: 200
});

@@ -79,2 +80,3 @@

- `root?: ParentNode`: Limit scanning to a specific container (default: `document`)
- `hue?: number`: Hue angle (0–359) of the overlay accent color (default: `17`, orange). The library automatically computes a lightness value that guarantees readable white text on the overlay label at any hue.
- `stripStega?: boolean`: Whether to strip stega-encoded invisible characters from text content after stamping (default: `false`). Stega embeds invisible, zero-width UTF-8 characters into text content to encode editing metadata.

@@ -81,0 +83,0 @@ - When `false` (default): Stega encoding remains in the DOM, allowing controllers to be disposed and recreated on the same page. The invisible characters don't affect display but preserve the source of truth.

@@ -12,6 +12,7 @@ /**

resolveDocument,
toCompletePath,
toCompletePath
} from '../utils/dom.js';
import { extractInfo, extractItemIdsPerEnvironment } from '../utils/editUrl.js';
import { ClickToEditManager } from './clickToEdit/ClickToEditManager.js';
import { DEFAULT_HUE, buildOverlayColors, type OverlayColors } from './clickToEdit/constants.js';
import { DomStampingManager } from './domStamping/DomStampingManager.js';

@@ -21,3 +22,3 @@ import {

MANUAL_TARGET_STAMP_ATTRIBUTE,
STAMPED_ELEMENTS_SELECTOR,
STAMPED_ELEMENTS_SELECTOR
} from './domStamping/constants.js';

@@ -27,7 +28,3 @@ import { EventsManager } from './events/EventsManager.js';

import { FlashItemManager } from './flash/FlashItemManager.js';
import type {
Controller,
CreateControllerOptions,
StampSummary,
} from './types.js';
import type { Controller, CreateControllerOptions, StampSummary } from './types.js';
import type { WebPreviewsPluginMethods } from './webPreviewsPlugin/types.js';

@@ -38,2 +35,3 @@

private readonly onNavigateTo?: (path: string) => void;
private readonly overlayColors: OverlayColors;
private readonly eventsManager: EventsManager;

@@ -60,5 +58,6 @@ private readonly clickToEditManager: ClickToEditManager;

this.onNavigateTo = options.onNavigateTo;
this.overlayColors = buildOverlayColors(options.hue ?? DEFAULT_HUE);
this.eventsManager = new EventsManager({
doc: this.document,
doc: this.document
});

@@ -70,2 +69,3 @@

() => this.webPreviewsPluginConnection === null,
this.overlayColors
);

@@ -78,21 +78,17 @@

(summary) => this.handleStampResult(summary),
options.stripStega ?? false,
options.stripStega ?? false
);
this.flashAllManager = new FlashAllManager(this.wrapperElement);
this.flashAllManager = new FlashAllManager(this.wrapperElement, this.overlayColors);
this.listenerAbortController = new AbortController();
this.document.addEventListener(
'keydown',
(event) => this.onKeyDown(event),
{
capture: true,
signal: this.listenerAbortController.signal,
},
);
this.document.addEventListener('keydown', (event) => this.onKeyDown(event), {
capture: true,
signal: this.listenerAbortController.signal
});
this.document.addEventListener('keyup', (event) => this.onKeyUp(event), {
capture: true,
signal: this.listenerAbortController.signal,
signal: this.listenerAbortController.signal
});

@@ -102,3 +98,3 @@

capture: true,
signal: this.listenerAbortController.signal,
signal: this.listenerAbortController.signal
});

@@ -113,3 +109,3 @@

},
{ signal: this.listenerAbortController.signal },
{ signal: this.listenerAbortController.signal }
);

@@ -122,3 +118,3 @@

},
{ signal: this.listenerAbortController.signal },
{ signal: this.listenerAbortController.signal }
);

@@ -202,2 +198,3 @@ }

this.webPreviewsPluginConnection.editUrlRegExp,
this.overlayColors
);

@@ -222,5 +219,3 @@ const flashed = flashSingleManager.flash(scrollToNearestTarget);

const stampedElements = this.wrapperElement.querySelectorAll(
STAMPED_ELEMENTS_SELECTOR,
);
const stampedElements = this.wrapperElement.querySelectorAll(STAMPED_ELEMENTS_SELECTOR);

@@ -243,4 +238,4 @@ // Collect all edit URLs from stamped elements

Array.from(editUrls),
this.webPreviewsPluginConnection.editUrlRegExp,
),
this.webPreviewsPluginConnection.editUrlRegExp
)
});

@@ -251,6 +246,3 @@ }

if (this.webPreviewsPluginConnection) {
const info = extractInfo(
editUrl,
this.webPreviewsPluginConnection.editUrlRegExp,
);
const info = extractInfo(editUrl, this.webPreviewsPluginConnection.editUrlRegExp);

@@ -262,5 +254,3 @@ if (info) {

// Fallback: open in new tab
const opener =
this.document.defaultView ??
(typeof window !== 'undefined' ? window : null);
const opener = this.document.defaultView ?? (typeof window !== 'undefined' ? window : null);

@@ -292,5 +282,3 @@ opener?.open(editUrl, '_blank', 'noopener,noreferrer');

setClickToEditEnabled: (
payload:
| { enabled: true; flash: { scrollToNearestTarget: boolean } }
| { enabled: false },
payload: { enabled: true; flash: { scrollToNearestTarget: boolean } } | { enabled: false }
) => {

@@ -302,4 +290,4 @@ if (payload.enabled) {

}
},
},
}
}
});

@@ -326,3 +314,3 @@

},
editUrlRegExp: new RegExp(editUrlRegExp.source, editUrlRegExp.flags),
editUrlRegExp: new RegExp(editUrlRegExp.source, editUrlRegExp.flags)
};

@@ -373,3 +361,3 @@

cancelable: true,
view: window,
view: window
});

@@ -410,10 +398,6 @@

private get isTopLevelWindowOrInWebPreviewsIframe() {
const opener =
this.document.defaultView ??
(typeof window !== 'undefined' ? window : null);
const opener = this.document.defaultView ?? (typeof window !== 'undefined' ? window : null);
return (
this.webPreviewsPluginConnection || (opener && opener.parent === opener)
);
return this.webPreviewsPluginConnection || (opener && opener.parent === opener);
}
}
import { HighlightOverlay } from '../../utils/HighlightOverlay.js';
import type { OverlayColors } from './constants.js';
/**

@@ -21,3 +22,4 @@ * Manages click-to-edit functionality: highlights editable regions under the pointer

private readonly onEditClick: (editUrl: string) => void,
private readonly shouldShowLabel: () => boolean = () => false
private readonly shouldShowLabel: () => boolean = () => false,
private readonly overlayColors?: OverlayColors
) {}

@@ -142,3 +144,4 @@

},
showLabel: this.shouldShowLabel()
showLabel: this.shouldShowLabel(),
overlayColors: this.overlayColors
});

@@ -145,0 +148,0 @@ this.highlightOverlay.show();

@@ -6,9 +6,51 @@ /**

export const OVERLAY_Z_INDEX = '2147483646';
/**
* Default presentation values for HighlightOverlay.
*/
export const DEFAULT_BORDER_COLOR = '#ff7751';
export const DEFAULT_BORDER_WIDTH = '2px';
export const DEFAULT_BORDER_RADIUS = '6px';
export const DEFAULT_BACKGROUND_COLOR = 'rgba(255, 119, 81, 0.12)';
export const DEFAULT_HUE = 17;
export type OverlayColors = {
borderColor: string;
backgroundColor: string;
};
// HSL→RGB with S fixed at 100%, used only for luminance calculation.
function hslToRgb(h: number, l: number): [number, number, number] {
const a = Math.min(l, 1 - l); // s=1, so a = s * min(l, 1-l)
const f = (n: number) => {
const k = (n + h / 30) % 12;
return l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)));
};
return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
}
function relativeLuminance(r: number, g: number, b: number): number {
const lin = (c: number) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
return 0.2126 * lin(r / 255) + 0.7152 * lin(g / 255) + 0.0722 * lin(b / 255);
}
// Binary search: highest HSL lightness (S=100%) that achieves ≥3.5:1 contrast against white.
function contrastSafeLightness(hue: number): number {
let lo = 0,
hi = 0.5,
lightness = 0;
while (hi - lo > 0.001) {
const mid = (lo + hi) / 2;
const [r, g, b] = hslToRgb(hue, mid);
const contrast = 1.05 / (relativeLuminance(r, g, b) + 0.05);
if (contrast >= 3.5) {
lightness = mid;
lo = mid + 0.001;
} else {
hi = mid - 0.001;
}
}
return lightness;
}
export function buildOverlayColors(hue: number): OverlayColors {
const l = Math.round(contrastSafeLightness(hue) * 100);
return {
borderColor: `hsl(${hue}, 100%, ${l}%)`,
backgroundColor: `hsla(${hue}, 100%, ${l}%, 0.15)`
};
}

@@ -8,3 +8,3 @@ /**

MANUAL_TARGET_STAMP_ATTRIBUTE,
STAMPED_ELEMENTS_SELECTOR,
STAMPED_ELEMENTS_SELECTOR
} from '../domStamping/constants.js';

@@ -17,5 +17,3 @@

export function findEditableTarget(
from: EventTarget | Element | null,
): EditableTarget | null {
export function findEditableTarget(from: EventTarget | Element | null): EditableTarget | null {
if (!from || !(from instanceof Element)) {

@@ -22,0 +20,0 @@ return null;

@@ -6,4 +6,3 @@ /**

*/
export const AUTOMATIC_TARGET_STAMP_ATTRIBUTE =
'data-datocms-auto-content-link-url';
export const AUTOMATIC_TARGET_STAMP_ATTRIBUTE = 'data-datocms-auto-content-link-url';
export const AUTOMATIC_STEGA_STAMP_ATTRIBUTE = 'data-datocms-contains-stega';

@@ -10,0 +9,0 @@ export const MANUAL_TARGET_STAMP_ATTRIBUTE = 'data-datocms-content-link-url';

@@ -16,3 +16,3 @@ /**

GROUP_BOUNDARY_ATTRIBUTE,
SOURCE_STAMP_ATTRIBUTE,
SOURCE_STAMP_ATTRIBUTE
} from './constants.js';

@@ -23,5 +23,3 @@

private readonly pendingElementsToStamp = new Set<ParentNode>();
private readonly scheduleStamping = createScheduler(() =>
this.instantStampPendingElements(),
);
private readonly scheduleStamping = createScheduler(() => this.instantStampPendingElements());

@@ -31,7 +29,5 @@ constructor(

private readonly onStamp: (summary: StampSummary) => void,
private readonly stripStega: boolean = false,
private readonly stripStega: boolean = false
) {
this.observer = new MutationObserver((mutations) =>
this.handleMutations(mutations),
);
this.observer = new MutationObserver((mutations) => this.handleMutations(mutations));

@@ -43,3 +39,3 @@ this.observer.observe(this.root, {

attributes: true,
attributeFilter: ['alt', SOURCE_STAMP_ATTRIBUTE],
attributeFilter: ['alt', SOURCE_STAMP_ATTRIBUTE]
});

@@ -54,5 +50,3 @@

const nodes = this.root.querySelectorAll<HTMLElement>(
`[${AUTOMATIC_TARGET_STAMP_ATTRIBUTE}]`,
);
const nodes = this.root.querySelectorAll<HTMLElement>(`[${AUTOMATIC_TARGET_STAMP_ATTRIBUTE}]`);

@@ -70,15 +64,8 @@ for (const el of nodes) {

const node = mutation.target as Node;
const parent = (node.parentElement ??
node.parentNode ??
this.root) as ParentNode;
const parent = (node.parentElement ?? node.parentNode ?? this.root) as ParentNode;
this.pendingElementsToStamp.add(parent);
hasChanges = true;
} else if (
mutation.type === 'attributes' &&
mutation.attributeName === 'alt'
) {
} else if (mutation.type === 'attributes' && mutation.attributeName === 'alt') {
const element = mutation.target as Element;
this.pendingElementsToStamp.add(
(element.parentElement ?? this.root) as ParentNode,
);
this.pendingElementsToStamp.add((element.parentElement ?? this.root) as ParentNode);
hasChanges = true;

@@ -90,5 +77,3 @@ } else if (

const element = mutation.target as Element;
this.pendingElementsToStamp.add(
(element.parentElement ?? this.root) as ParentNode,
);
this.pendingElementsToStamp.add((element.parentElement ?? this.root) as ParentNode);
hasChanges = true;

@@ -139,3 +124,3 @@ } else if (mutation.type === 'childList') {

}, new Map<Element, string>()),
scope: this.root,
scope: this.root
};

@@ -165,3 +150,3 @@

appliedStamps: new Map(),
scope: element,
scope: element
};

@@ -194,3 +179,3 @@ }

parent,
appliedStamps,
appliedStamps
);

@@ -212,3 +197,3 @@

img,
appliedStamps,
appliedStamps
);

@@ -222,12 +207,6 @@

// Third pass: inspect elements with data-datocms-content-link-source attribute
for (const el of element.querySelectorAll<HTMLElement>(
`[${SOURCE_STAMP_ATTRIBUTE}]`,
)) {
for (const el of element.querySelectorAll<HTMLElement>(`[${SOURCE_STAMP_ATTRIBUTE}]`)) {
const sourceValue = el.getAttribute(SOURCE_STAMP_ATTRIBUTE);
this.addStampingAttributesTargetAndReturnStrippedValue(
sourceValue,
el,
appliedStamps,
);
this.addStampingAttributesTargetAndReturnStrippedValue(sourceValue, el, appliedStamps);

@@ -242,3 +221,3 @@ // If stripStega is enabled, clear the source attribute after stamping

appliedStamps: appliedStamps,
scope: element,
scope: element
};

@@ -252,3 +231,3 @@

elementWithStega: Element | null,
appliedStamps: Map<Element, string>,
appliedStamps: Map<Element, string>
): string | undefined {

@@ -294,5 +273,3 @@ if (!value || !elementWithStega) {

// Stamp the attribute if it changed
const existingEditUrl = target.getAttribute(
AUTOMATIC_TARGET_STAMP_ATTRIBUTE,
);
const existingEditUrl = target.getAttribute(AUTOMATIC_TARGET_STAMP_ATTRIBUTE);

@@ -317,3 +294,3 @@ if (existingEditUrl !== decoded.href) {

incomingEl: Element,
incomingUrl: string,
incomingUrl: string
) {

@@ -320,0 +297,0 @@ const message = `[@datocms/content-link] Multiple stega-encoded payloads resolved to the same DOM element. Previous URL: ${originalUrl}. Incoming URL: ${incomingUrl}. Wrap each encoded block in its own element (for example by adding ${GROUP_ATTRIBUTE}).`;

import { HighlightOverlay } from '../../utils/HighlightOverlay.js';
import { maybeScrollToNearestTarget, sleep, waitTwoRafs } from '../../utils/dom.js';
import type { OverlayColors } from '../clickToEdit/constants.js';
import { STAMPED_ELEMENTS_SELECTOR } from '../domStamping/constants.js';

@@ -12,3 +13,6 @@

constructor(private readonly wrapperElement: ParentNode) {}
constructor(
private readonly wrapperElement: ParentNode,
private readonly overlayColors?: OverlayColors
) {}

@@ -51,3 +55,3 @@ async flash(scrollToNearestTarget: boolean) {

targets.map((target, index) => {
const overlay = new HighlightOverlay(target);
const overlay = new HighlightOverlay(target, { overlayColors: this.overlayColors });
overlay.fadeIn(targetsCount < 50 ? index * STAGGER_DELAY : 0, abortController);

@@ -54,0 +58,0 @@ this.overlays.push(overlay);

import { HighlightOverlay } from '../../utils/HighlightOverlay.js';
import {
maybeScrollToNearestTarget,
sleep,
waitTwoRafs,
} from '../../utils/dom.js';
import { maybeScrollToNearestTarget, sleep, waitTwoRafs } from '../../utils/dom.js';
import { extractInfo } from '../../utils/editUrl.js';
import type { OverlayColors } from '../clickToEdit/constants.js';
import {
AUTOMATIC_TARGET_STAMP_ATTRIBUTE,
MANUAL_TARGET_STAMP_ATTRIBUTE,
STAMPED_ELEMENTS_SELECTOR,
STAMPED_ELEMENTS_SELECTOR
} from '../domStamping/constants.js';

@@ -24,2 +21,3 @@ import { STAGGER_DELAY } from './FlashAllManager.js';

private readonly editUrlRegExp: RegExp,
private readonly overlayColors?: OverlayColors
) {}

@@ -37,5 +35,4 @@

if (this.disposed) return;
const stampedElements = this.wrapperElement.querySelectorAll<HTMLElement>(
STAMPED_ELEMENTS_SELECTOR,
);
const stampedElements =
this.wrapperElement.querySelectorAll<HTMLElement>(STAMPED_ELEMENTS_SELECTOR);

@@ -75,3 +72,3 @@ const targetsSet = new Set<HTMLElement>();

targets.map((target, index) => {
const overlay = new HighlightOverlay(target);
const overlay = new HighlightOverlay(target, { overlayColors: this.overlayColors });
overlay.fadeIn(index * STAGGER_DELAY, abortController);

@@ -93,4 +90,4 @@ this.overlays.push(overlay);

this.overlays.map((overlay, index) =>
overlay.disposeWithFadeOut(index * STAGGER_DELAY, abortController),
),
overlay.disposeWithFadeOut(index * STAGGER_DELAY, abortController)
)
);

@@ -97,0 +94,0 @@

@@ -10,2 +10,4 @@ /**

onNavigateTo?: (path: string) => void;
/** Hue (0–359) of the overlay accent color. Default: 17 (orange). */
hue?: number;
/**

@@ -12,0 +14,0 @@ * Whether to strip stega-encoded invisible characters from text content after stamping.

@@ -6,16 +6,11 @@ /**

import {
DEFAULT_BACKGROUND_COLOR,
DEFAULT_BORDER_COLOR,
DEFAULT_BORDER_RADIUS,
DEFAULT_BORDER_WIDTH,
DEFAULT_HUE,
DEFAULT_OVERLAY_PADDING,
OVERLAY_Z_INDEX,
buildOverlayColors,
type OverlayColors
} from '../createController/clickToEdit/constants.js';
import {
abortableSleep,
getDocumentWindow,
measure,
resolveDocument,
waitTwoRafs,
} from './dom.js';
import { abortableSleep, getDocumentWindow, measure, resolveDocument, waitTwoRafs } from './dom.js';
import { getScrollResizeCoordinator } from './scrollResizeCoordinator.js';

@@ -29,2 +24,3 @@ import { getSharedResizeObserver } from './sharedResizeObserver.js';

showLabel?: boolean;
overlayColors?: OverlayColors;
}

@@ -41,9 +37,11 @@

private readonly showLabel: boolean;
private readonly overlayColors: OverlayColors;
constructor(
readonly targetElement: HTMLElement,
options: HighlightOverlayOptions = {},
options: HighlightOverlayOptions = {}
) {
this.onDispose = options.onDispose;
this.showLabel = options.showLabel ?? false;
this.overlayColors = options.overlayColors ?? buildOverlayColors(DEFAULT_HUE);

@@ -89,9 +87,5 @@ this.overlayElement = this.createOverlayElement(this.showLabel);

async fadeIn(
afterDelay = 0,
abortController?: AbortController,
): Promise<void> {
async fadeIn(afterDelay = 0, abortController?: AbortController): Promise<void> {
this.cancelPendingAnimation();
this.pendingAnimationAbortController =
abortController || new AbortController();
this.pendingAnimationAbortController = abortController || new AbortController();
const { signal } = this.pendingAnimationAbortController;

@@ -109,9 +103,5 @@

async disposeWithFadeOut(
afterDelay = 0,
abortController?: AbortController,
): Promise<void> {
async disposeWithFadeOut(afterDelay = 0, abortController?: AbortController): Promise<void> {
this.cancelPendingAnimation();
this.pendingAnimationAbortController =
abortController || new AbortController();
this.pendingAnimationAbortController = abortController || new AbortController();
const { signal } = this.pendingAnimationAbortController;

@@ -137,7 +127,7 @@

overlay.style.height = '0';
overlay.style.border = `${DEFAULT_BORDER_WIDTH} solid ${DEFAULT_BORDER_COLOR}`;
overlay.style.border = `${DEFAULT_BORDER_WIDTH} solid ${this.overlayColors.borderColor}`;
overlay.style.borderRadius = withLabel
? `${DEFAULT_BORDER_RADIUS} 0 ${DEFAULT_BORDER_RADIUS} ${DEFAULT_BORDER_RADIUS}`
: DEFAULT_BORDER_RADIUS;
overlay.style.background = DEFAULT_BACKGROUND_COLOR;
overlay.style.background = this.overlayColors.backgroundColor;
overlay.style.boxSizing = 'border-box';

@@ -157,3 +147,3 @@ overlay.style.pointerEvents = 'none';

label.style.right = `-${DEFAULT_BORDER_WIDTH}`;
label.style.backgroundColor = DEFAULT_BORDER_COLOR;
label.style.backgroundColor = this.overlayColors.borderColor;
label.style.color = 'white';

@@ -180,5 +170,3 @@ label.style.padding = '4px 12px';

const rect = measure(this.targetElement);
this.overlayElement.style.zIndex = this.computeOverlayZIndex(
this.targetElement,
);
this.overlayElement.style.zIndex = this.computeOverlayZIndex(this.targetElement);

@@ -185,0 +173,0 @@ if (!rect) {

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