@pushwoosh/dumb-components 1.1.220 → 1.1.221
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EmailTemplatePreview/EmailTemplatePreview.d.ts +3 -1
- package/EmailTemplatePreview/EmailTemplatePreview.js +30 -7
- package/EmailTemplatePreview/__tests__/clickMap.test.d.ts +1 -0
- package/EmailTemplatePreview/__tests__/clickMap.test.js +107 -0
- package/EmailTemplatePreview/clickMap.d.ts +25 -0
- package/EmailTemplatePreview/clickMap.js +128 -0
- package/EmailTemplatePreview/index.d.ts +2 -0
- package/EmailTemplatePreview/index.js +2 -1
- package/EmailTemplatePreview/types.d.ts +33 -0
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
|
@@ -2,9 +2,11 @@ import { type ReactElement } from 'react';
|
|
|
2
2
|
import type { PreviewIframeProps } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* Renders an email HTML template in a sandboxed iframe, resolving Liquid, localization
|
|
5
|
-
* and placeholder values before display.
|
|
5
|
+
* and placeholder values before display. With `clickMap` it also draws click counts over
|
|
6
|
+
* the anchors they were recorded on, see {@link decorateClickMap}.
|
|
6
7
|
*
|
|
7
8
|
* @example
|
|
8
9
|
* <EmailTemplatePreview template={html} localization={l10n} values={vars} scale={0.5} />
|
|
10
|
+
* <EmailTemplatePreview template={html} localization={l10n} values={vars} clickMap={{ items }} />
|
|
9
11
|
*/
|
|
10
12
|
export declare function EmailTemplatePreview(props: PreviewIframeProps): ReactElement;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Liquid } from 'liquidjs';
|
|
3
|
-
import { useMemo, useState } from 'react';
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
import { decorateClickMap } from './clickMap';
|
|
4
5
|
import { replaceLocalization, replacePlaceholders } from './helpers';
|
|
5
6
|
import { IframeStyled } from './styles';
|
|
7
|
+
import { SandboxedHtmlFrame } from '../SandboxedHtmlFrame';
|
|
6
8
|
import { LIGHT_ISLAND_PROPS } from '../shared/lightIsland';
|
|
7
9
|
const createLiquidInstance = () => {
|
|
8
10
|
const liquidInstance = new Liquid();
|
|
@@ -21,10 +23,12 @@ const createLiquidInstance = () => {
|
|
|
21
23
|
};
|
|
22
24
|
/**
|
|
23
25
|
* Renders an email HTML template in a sandboxed iframe, resolving Liquid, localization
|
|
24
|
-
* and placeholder values before display.
|
|
26
|
+
* and placeholder values before display. With `clickMap` it also draws click counts over
|
|
27
|
+
* the anchors they were recorded on, see {@link decorateClickMap}.
|
|
25
28
|
*
|
|
26
29
|
* @example
|
|
27
30
|
* <EmailTemplatePreview template={html} localization={l10n} values={vars} scale={0.5} />
|
|
31
|
+
* <EmailTemplatePreview template={html} localization={l10n} values={vars} clickMap={{ items }} />
|
|
28
32
|
*/
|
|
29
33
|
export function EmailTemplatePreview(props) {
|
|
30
34
|
const {
|
|
@@ -33,8 +37,14 @@ export function EmailTemplatePreview(props) {
|
|
|
33
37
|
height,
|
|
34
38
|
template,
|
|
35
39
|
localization,
|
|
36
|
-
values
|
|
40
|
+
values,
|
|
41
|
+
clickMap,
|
|
42
|
+
onReady
|
|
37
43
|
} = props;
|
|
44
|
+
const clickMapItems = clickMap?.items;
|
|
45
|
+
const clickMapActiveKey = clickMap?.activeKey;
|
|
46
|
+
const clickMapOnSelect = clickMap?.onSelect;
|
|
47
|
+
const clickMapOnPlaced = clickMap?.onPlaced;
|
|
38
48
|
const [liquidInstance] = useState(createLiquidInstance);
|
|
39
49
|
const src = useMemo(() => {
|
|
40
50
|
let withDynamicContent = template;
|
|
@@ -47,15 +57,28 @@ export function EmailTemplatePreview(props) {
|
|
|
47
57
|
return withDynamicContent;
|
|
48
58
|
}
|
|
49
59
|
}, [template, localization, values, liquidInstance]);
|
|
60
|
+
const handleReady = useCallback(frameDocument => {
|
|
61
|
+
const cleanups = [onReady?.(frameDocument), clickMapItems && decorateClickMap(frameDocument, {
|
|
62
|
+
items: clickMapItems,
|
|
63
|
+
activeKey: clickMapActiveKey,
|
|
64
|
+
scale,
|
|
65
|
+
onSelect: clickMapOnSelect,
|
|
66
|
+
onPlaced: clickMapOnPlaced
|
|
67
|
+
})];
|
|
68
|
+
return () => {
|
|
69
|
+
cleanups.forEach(cleanup => cleanup?.());
|
|
70
|
+
};
|
|
71
|
+
}, [onReady, clickMapItems, clickMapActiveKey, clickMapOnSelect, clickMapOnPlaced, scale]);
|
|
50
72
|
return _jsx(IframeStyled, {
|
|
51
73
|
...LIGHT_ISLAND_PROPS,
|
|
52
74
|
"$scale": scale,
|
|
53
75
|
"$width": width,
|
|
54
76
|
"$height": height,
|
|
55
|
-
children: _jsx(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
children: _jsx(SandboxedHtmlFrame, {
|
|
78
|
+
mode: "inspectable",
|
|
79
|
+
title: "preview",
|
|
80
|
+
html: src,
|
|
81
|
+
onReady: handleReady
|
|
59
82
|
})
|
|
60
83
|
});
|
|
61
84
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { CLICK_MAP_BADGE_ATTRIBUTE, decorateClickMap, matchClickMapItems } from '../clickMap';
|
|
2
|
+
const PROMO = 'http://spartak.com/matchtowin';
|
|
3
|
+
const TICKETS = 'https://tickets.spartak.com/zones/18956';
|
|
4
|
+
const HTML = `
|
|
5
|
+
<a href="${PROMO}?utm_source=email&utm_content=img"><img src="banner.png" alt=""></a>
|
|
6
|
+
<p>Подробности на <a href="${PROMO}?utm_source=email">странице акции</a>.</p>
|
|
7
|
+
<a href="${PROMO}?utm_source=email"><strong>Перейти на страницу акции</strong></a>
|
|
8
|
+
<a href="${TICKETS}?utm_source=email">Купить билеты</a>
|
|
9
|
+
<a href="${TICKETS}?utm_source=email">Купить билеты</a>
|
|
10
|
+
<a href="[% param.url_unsub %]">Отписаться</a>
|
|
11
|
+
`;
|
|
12
|
+
const item = (key, link, title, clicks) => ({
|
|
13
|
+
key,
|
|
14
|
+
link,
|
|
15
|
+
title,
|
|
16
|
+
clicks
|
|
17
|
+
});
|
|
18
|
+
function createDocument() {
|
|
19
|
+
const frameDocument = document.implementation.createHTMLDocument('preview');
|
|
20
|
+
frameDocument.body.innerHTML = HTML;
|
|
21
|
+
return frameDocument;
|
|
22
|
+
}
|
|
23
|
+
describe('matchClickMapItems', () => {
|
|
24
|
+
it('tells anchors with the same link apart by their text and lands image links on the text-less anchor', () => {
|
|
25
|
+
const frameDocument = createDocument();
|
|
26
|
+
const items = [item('button', PROMO, 'Перейти на страницу акции', 1631), item('text', PROMO, 'странице акции', 398), item('image', PROMO, '', 172)];
|
|
27
|
+
const {
|
|
28
|
+
matches,
|
|
29
|
+
placement
|
|
30
|
+
} = matchClickMapItems(frameDocument, items);
|
|
31
|
+
expect(placement).toEqual({
|
|
32
|
+
placed: ['button', 'text', 'image'],
|
|
33
|
+
pooled: [],
|
|
34
|
+
unplaced: []
|
|
35
|
+
});
|
|
36
|
+
expect(matches.map(({
|
|
37
|
+
anchor,
|
|
38
|
+
items: matched
|
|
39
|
+
}) => [anchor.textContent?.trim(), matched.map(row => row.key)])).toEqual([['Перейти на страницу акции', ['button']], ['странице акции', ['text']], ['', ['image']]]);
|
|
40
|
+
});
|
|
41
|
+
it('ignores the query string of both sides when matching links', () => {
|
|
42
|
+
const {
|
|
43
|
+
placement
|
|
44
|
+
} = matchClickMapItems(createDocument(), [item('text', `${PROMO}?utm_source=other&x=1`, 'странице акции', 1)]);
|
|
45
|
+
expect(placement.placed).toEqual(['text']);
|
|
46
|
+
});
|
|
47
|
+
it('pools a row over every anchor that shares link and text', () => {
|
|
48
|
+
const {
|
|
49
|
+
matches,
|
|
50
|
+
placement
|
|
51
|
+
} = matchClickMapItems(createDocument(), [item('tickets', TICKETS, 'Купить билеты', 258)]);
|
|
52
|
+
expect(placement).toEqual({
|
|
53
|
+
placed: [],
|
|
54
|
+
pooled: ['tickets'],
|
|
55
|
+
unplaced: []
|
|
56
|
+
});
|
|
57
|
+
expect(matches).toHaveLength(2);
|
|
58
|
+
});
|
|
59
|
+
it('falls back to every anchor of the link when the recorded text matches none of them', () => {
|
|
60
|
+
const {
|
|
61
|
+
matches,
|
|
62
|
+
placement
|
|
63
|
+
} = matchClickMapItems(createDocument(), [item('promo', PROMO, 'Use this template →', 10)]);
|
|
64
|
+
expect(placement.pooled).toEqual(['promo']);
|
|
65
|
+
expect(matches).toHaveLength(3);
|
|
66
|
+
});
|
|
67
|
+
it('reports rows whose link is not in the template as unplaced', () => {
|
|
68
|
+
const {
|
|
69
|
+
matches,
|
|
70
|
+
placement
|
|
71
|
+
} = matchClickMapItems(createDocument(), [item('gone', 'https://old.example.com/', 'x', 5)]);
|
|
72
|
+
expect(placement).toEqual({
|
|
73
|
+
placed: [],
|
|
74
|
+
pooled: [],
|
|
75
|
+
unplaced: ['gone']
|
|
76
|
+
});
|
|
77
|
+
expect(matches).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('decorateClickMap', () => {
|
|
81
|
+
it('draws one badge per matched anchor, highlights the active row and cleans up after itself', () => {
|
|
82
|
+
const frameDocument = createDocument();
|
|
83
|
+
const onPlaced = jest.fn();
|
|
84
|
+
const onSelect = jest.fn();
|
|
85
|
+
const items = [item('button', PROMO, 'Перейти на страницу акции', 1631), item('image', PROMO, '', 172), item('gone', 'https://old.example.com/', 'x', 5)];
|
|
86
|
+
const cleanup = decorateClickMap(frameDocument, {
|
|
87
|
+
items,
|
|
88
|
+
activeKey: 'button',
|
|
89
|
+
onPlaced,
|
|
90
|
+
onSelect
|
|
91
|
+
});
|
|
92
|
+
const badges = [...frameDocument.querySelectorAll(`[${CLICK_MAP_BADGE_ATTRIBUTE}]`)];
|
|
93
|
+
expect(badges.map(badge => [badge.getAttribute(CLICK_MAP_BADGE_ATTRIBUTE), badge.textContent])).toEqual([['button', '1.6K'], ['image', '172']]);
|
|
94
|
+
expect(badges[0].style.backgroundColor).not.toBe(badges[1].style.backgroundColor);
|
|
95
|
+
expect(frameDocument.querySelector('a strong')?.parentElement?.style.outline).toContain('solid');
|
|
96
|
+
expect(onPlaced).toHaveBeenCalledWith({
|
|
97
|
+
placed: ['button', 'image'],
|
|
98
|
+
pooled: [],
|
|
99
|
+
unplaced: ['gone']
|
|
100
|
+
});
|
|
101
|
+
badges[1].click();
|
|
102
|
+
expect(onSelect).toHaveBeenCalledWith('image');
|
|
103
|
+
cleanup();
|
|
104
|
+
expect(frameDocument.querySelectorAll(`[${CLICK_MAP_BADGE_ATTRIBUTE}]`)).toHaveLength(0);
|
|
105
|
+
expect(frameDocument.querySelector('a strong')?.parentElement?.style.outline).toBe('');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EmailClickMapItem, EmailClickMapPlacement } from './types';
|
|
2
|
+
/** An anchor of the rendered template together with the rows recorded on it. */
|
|
3
|
+
export type EmailClickMapMatch = {
|
|
4
|
+
anchor: HTMLAnchorElement;
|
|
5
|
+
items: EmailClickMapItem[];
|
|
6
|
+
};
|
|
7
|
+
/** Outcome of {@link matchClickMapItems}. */
|
|
8
|
+
export type EmailClickMapMatchResult = {
|
|
9
|
+
matches: EmailClickMapMatch[];
|
|
10
|
+
placement: EmailClickMapPlacement;
|
|
11
|
+
};
|
|
12
|
+
/** Options of {@link decorateClickMap}. */
|
|
13
|
+
export type EmailClickMapDecorateOptions = {
|
|
14
|
+
items: EmailClickMapItem[];
|
|
15
|
+
activeKey?: string | null;
|
|
16
|
+
scale?: number;
|
|
17
|
+
onSelect?: (key: string) => void;
|
|
18
|
+
onPlaced?: (placement: EmailClickMapPlacement) => void;
|
|
19
|
+
};
|
|
20
|
+
/** Attribute every badge carries, so tests and consumers can find them. */
|
|
21
|
+
export declare const CLICK_MAP_BADGE_ATTRIBUTE = "data-pw-click-map-badge";
|
|
22
|
+
/** Lands click-tracking rows on template anchors by (link, text), falling back to every anchor of the link. */
|
|
23
|
+
export declare function matchClickMapItems(document: Document, items: EmailClickMapItem[]): EmailClickMapMatchResult;
|
|
24
|
+
/** Draws click-count badges over matched anchors, outlines `activeKey`; returns a cleanup. */
|
|
25
|
+
export declare function decorateClickMap(document: Document, options: EmailClickMapDecorateOptions): () => void;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** Attribute every badge carries, so tests and consumers can find them. */
|
|
2
|
+
export const CLICK_MAP_BADGE_ATTRIBUTE = 'data-pw-click-map-badge';
|
|
3
|
+
const ACTIVE_COLOR = '#00b2ff';
|
|
4
|
+
const IDLE_COLOR = 'rgba(36, 48, 66, 0.82)';
|
|
5
|
+
const compactNumber = new Intl.NumberFormat('en', {
|
|
6
|
+
notation: 'compact',
|
|
7
|
+
maximumFractionDigits: 1
|
|
8
|
+
});
|
|
9
|
+
const stripQuery = url => url.split('?')[0].trim();
|
|
10
|
+
const normalizeText = text => text.replace(/\s+/g, ' ').trim();
|
|
11
|
+
const anchorText = anchor => normalizeText(anchor.textContent ?? '');
|
|
12
|
+
const anchorLink = anchor => stripQuery(anchor.getAttribute('href') ?? '');
|
|
13
|
+
/** Lands click-tracking rows on template anchors by (link, text), falling back to every anchor of the link. */
|
|
14
|
+
export function matchClickMapItems(document, items) {
|
|
15
|
+
const anchorsByLink = new Map();
|
|
16
|
+
document.querySelectorAll('a[href]').forEach(anchor => {
|
|
17
|
+
const link = anchorLink(anchor);
|
|
18
|
+
if (link) {
|
|
19
|
+
anchorsByLink.set(link, [...(anchorsByLink.get(link) ?? []), anchor]);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const itemsByAnchor = new Map();
|
|
23
|
+
const placement = {
|
|
24
|
+
placed: [],
|
|
25
|
+
pooled: [],
|
|
26
|
+
unplaced: []
|
|
27
|
+
};
|
|
28
|
+
items.forEach(item => {
|
|
29
|
+
const candidates = anchorsByLink.get(stripQuery(item.link)) ?? [];
|
|
30
|
+
const title = normalizeText(item.title);
|
|
31
|
+
const exact = candidates.filter(anchor => anchorText(anchor) === title);
|
|
32
|
+
const targets = exact.length > 0 ? exact : candidates;
|
|
33
|
+
if (targets.length === 0) {
|
|
34
|
+
placement.unplaced.push(item.key);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
(targets.length === 1 ? placement.placed : placement.pooled).push(item.key);
|
|
38
|
+
targets.forEach(anchor => {
|
|
39
|
+
itemsByAnchor.set(anchor, [...(itemsByAnchor.get(anchor) ?? []), item]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
matches: [...itemsByAnchor].map(([anchor, matched]) => ({
|
|
44
|
+
anchor,
|
|
45
|
+
items: matched
|
|
46
|
+
})),
|
|
47
|
+
placement
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function anchorRect(anchor) {
|
|
51
|
+
const rects = [anchor, ...anchor.querySelectorAll('img')].map(node => node.getBoundingClientRect());
|
|
52
|
+
const top = Math.min(...rects.map(rect => rect.top));
|
|
53
|
+
const left = Math.min(...rects.map(rect => rect.left));
|
|
54
|
+
const right = Math.max(...rects.map(rect => rect.right));
|
|
55
|
+
const bottom = Math.max(...rects.map(rect => rect.bottom));
|
|
56
|
+
return new DOMRect(left, top, right - left, bottom - top);
|
|
57
|
+
}
|
|
58
|
+
function createBadge(document, match, options) {
|
|
59
|
+
const scale = options.scale ?? 1;
|
|
60
|
+
const keys = match.items.map(item => item.key);
|
|
61
|
+
const isActive = options.activeKey != null && keys.includes(options.activeKey);
|
|
62
|
+
const clicks = match.items.reduce((sum, item) => sum + item.clicks, 0);
|
|
63
|
+
const badge = document.createElement('span');
|
|
64
|
+
badge.setAttribute(CLICK_MAP_BADGE_ATTRIBUTE, keys.join(' '));
|
|
65
|
+
badge.textContent = compactNumber.format(clicks);
|
|
66
|
+
badge.title = match.items.map(item => `${item.title || item.link}: ${item.clicks}`).join('\n');
|
|
67
|
+
Object.assign(badge.style, {
|
|
68
|
+
position: 'absolute',
|
|
69
|
+
zIndex: '2147483647',
|
|
70
|
+
transform: 'translateY(-50%)',
|
|
71
|
+
padding: `${3 * scale}px ${8 * scale}px`,
|
|
72
|
+
borderRadius: '999px',
|
|
73
|
+
backgroundColor: isActive ? ACTIVE_COLOR : IDLE_COLOR,
|
|
74
|
+
color: '#fff',
|
|
75
|
+
font: `600 ${12 * scale}px/1 sans-serif`,
|
|
76
|
+
whiteSpace: 'nowrap',
|
|
77
|
+
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
|
|
78
|
+
cursor: options.onSelect ? 'pointer' : 'default',
|
|
79
|
+
userSelect: 'none'
|
|
80
|
+
});
|
|
81
|
+
badge.addEventListener('click', event => {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
event.stopPropagation();
|
|
84
|
+
options.onSelect?.(keys[0]);
|
|
85
|
+
});
|
|
86
|
+
return badge;
|
|
87
|
+
}
|
|
88
|
+
/** Draws click-count badges over matched anchors, outlines `activeKey`; returns a cleanup. */
|
|
89
|
+
export function decorateClickMap(document, options) {
|
|
90
|
+
const view = document.defaultView;
|
|
91
|
+
const {
|
|
92
|
+
matches,
|
|
93
|
+
placement
|
|
94
|
+
} = matchClickMapItems(document, options.items);
|
|
95
|
+
const badges = matches.map(match => createBadge(document, match, options));
|
|
96
|
+
const outlined = matches.filter(match => options.activeKey != null && match.items.some(item => item.key === options.activeKey)).map(match => [match.anchor, match.anchor.style.outline]);
|
|
97
|
+
const isInlineText = anchor => view?.getComputedStyle(anchor).display === 'inline' && !anchor.querySelector('img');
|
|
98
|
+
const layout = () => {
|
|
99
|
+
const maxRight = document.documentElement.clientWidth;
|
|
100
|
+
matches.forEach((match, index) => {
|
|
101
|
+
const badge = badges[index];
|
|
102
|
+
const rect = anchorRect(match.anchor);
|
|
103
|
+
const right = Math.min(rect.right, maxRight);
|
|
104
|
+
const inline = isInlineText(match.anchor);
|
|
105
|
+
const left = inline ? Math.min(right, maxRight - badge.offsetWidth) : right - badge.offsetWidth;
|
|
106
|
+
const top = inline ? rect.top + rect.height / 2 : rect.top;
|
|
107
|
+
badge.style.top = `${top + (view?.scrollY ?? 0)}px`;
|
|
108
|
+
badge.style.left = `${left + (view?.scrollX ?? 0)}px`;
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
badges.forEach(badge => document.body.appendChild(badge));
|
|
112
|
+
outlined.forEach(([anchor]) => {
|
|
113
|
+
anchor.style.outline = `${2 * (options.scale ?? 1)}px solid ${ACTIVE_COLOR}`;
|
|
114
|
+
});
|
|
115
|
+
layout();
|
|
116
|
+
options.onPlaced?.(placement);
|
|
117
|
+
const observer = view && 'ResizeObserver' in view ? new view.ResizeObserver(layout) : undefined;
|
|
118
|
+
observer?.observe(document.documentElement);
|
|
119
|
+
view?.addEventListener('resize', layout);
|
|
120
|
+
return () => {
|
|
121
|
+
observer?.disconnect();
|
|
122
|
+
view?.removeEventListener('resize', layout);
|
|
123
|
+
badges.forEach(badge => badge.remove());
|
|
124
|
+
outlined.forEach(([anchor, outline]) => {
|
|
125
|
+
anchor.style.outline = outline;
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { EmailTemplatePreview } from './EmailTemplatePreview';
|
|
2
2
|
export { NoEmailTemplatePreview } from './components/NoEmailTemplatePreview';
|
|
3
3
|
export { replaceLocalization, replacePlaceholders } from './helpers';
|
|
4
|
+
export { matchClickMapItems, decorateClickMap, CLICK_MAP_BADGE_ATTRIBUTE, type EmailClickMapMatch, type EmailClickMapMatchResult, type EmailClickMapDecorateOptions, } from './clickMap';
|
|
5
|
+
export type { EmailClickMapItem, EmailClickMapPlacement, EmailClickMapProps, PreviewIframeProps, } from './types';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { EmailTemplatePreview } from './EmailTemplatePreview';
|
|
2
2
|
export { NoEmailTemplatePreview } from './components/NoEmailTemplatePreview';
|
|
3
|
-
export { replaceLocalization, replacePlaceholders } from './helpers';
|
|
3
|
+
export { replaceLocalization, replacePlaceholders } from './helpers';
|
|
4
|
+
export { matchClickMapItems, decorateClickMap, CLICK_MAP_BADGE_ATTRIBUTE } from './clickMap';
|
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
/** One click-tracking row to be drawn over the anchor it was recorded on. */
|
|
2
|
+
export type EmailClickMapItem = {
|
|
3
|
+
/** Row identity, passed back through {@link EmailClickMapProps.onSelect}. */
|
|
4
|
+
key: string;
|
|
5
|
+
/** Destination URL as recorded by click tracking; the query string is ignored when matching. */
|
|
6
|
+
link: string;
|
|
7
|
+
/** Anchor text recorded with the click; empty for image links. */
|
|
8
|
+
title: string;
|
|
9
|
+
clicks: number;
|
|
10
|
+
};
|
|
11
|
+
/** How the rows of {@link EmailClickMapProps.items} landed on the rendered template. */
|
|
12
|
+
export type EmailClickMapPlacement = {
|
|
13
|
+
/** Keys drawn on exactly one anchor. */
|
|
14
|
+
placed: string[];
|
|
15
|
+
/** Keys drawn on several anchors that share link and text, so the count repeats. */
|
|
16
|
+
pooled: string[];
|
|
17
|
+
/** Keys with no matching anchor in the current template. */
|
|
18
|
+
unplaced: string[];
|
|
19
|
+
};
|
|
20
|
+
/** Click counts to overlay on the rendered email, see {@link EmailTemplatePreview}. */
|
|
21
|
+
export type EmailClickMapProps = {
|
|
22
|
+
items: EmailClickMapItem[];
|
|
23
|
+
/** Key whose badge and anchor are highlighted. */
|
|
24
|
+
activeKey?: string | null;
|
|
25
|
+
/** Called with the key of the badge the user clicked. */
|
|
26
|
+
onSelect?: (key: string) => void;
|
|
27
|
+
/** Called after every layout with the placement outcome. */
|
|
28
|
+
onPlaced?: (placement: EmailClickMapPlacement) => void;
|
|
29
|
+
};
|
|
1
30
|
/** Props of {@link EmailTemplatePreview}. */
|
|
2
31
|
export type PreviewIframeProps = {
|
|
3
32
|
/** CSS `transform: scale()` factor applied to the rendered email. */
|
|
@@ -12,4 +41,8 @@ export type PreviewIframeProps = {
|
|
|
12
41
|
localization: Record<string, string>;
|
|
13
42
|
/** Placeholder values resolved by {@link replacePlaceholders}. */
|
|
14
43
|
values: Record<string, any>;
|
|
44
|
+
/** Click counts drawn over the anchors of the rendered template. */
|
|
45
|
+
clickMap?: EmailClickMapProps;
|
|
46
|
+
/** Called with the frame document once rendered; may return a cleanup run before the next call. */
|
|
47
|
+
onReady?: (document: Document) => void | (() => void);
|
|
15
48
|
};
|
package/index.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export { Tooltip, type TooltipPosition, TooltipTrigger } from './Tooltip';
|
|
|
32
32
|
export { Tab, TabBar, TabPanel, Tabs, type TabCode } from './Tabs';
|
|
33
33
|
export { Link, type LinkPropsWithoutAs, type LinkPropsWithAsTag, type LinkPropsWithAsComponent, } from './Link';
|
|
34
34
|
export { StatisticCard } from './StatisticCard';
|
|
35
|
-
export { EmailTemplatePreview, NoEmailTemplatePreview, replaceLocalization, replacePlaceholders } from './EmailTemplatePreview';
|
|
35
|
+
export { EmailTemplatePreview, NoEmailTemplatePreview, replaceLocalization, replacePlaceholders, matchClickMapItems, decorateClickMap, CLICK_MAP_BADGE_ATTRIBUTE, type EmailClickMapItem, type EmailClickMapPlacement, type EmailClickMapProps, type PreviewIframeProps, } from './EmailTemplatePreview';
|
|
36
36
|
export { PushPreview } from './PushPreview';
|
|
37
37
|
export { WhatsAppPreview } from './WhatsAppPreview';
|
|
38
38
|
export type { WhatsAppAttachment, WhatsAppAttachmentKind, WhatsAppMessage, WhatsAppMessageDirection, WhatsAppMessageStatus, WhatsAppPreviewProps, } from './WhatsAppPreview';
|
package/index.js
CHANGED
|
@@ -32,7 +32,7 @@ export { Tooltip, TooltipTrigger } from './Tooltip';
|
|
|
32
32
|
export { Tab, TabBar, TabPanel, Tabs } from './Tabs';
|
|
33
33
|
export { Link } from './Link';
|
|
34
34
|
export { StatisticCard } from './StatisticCard';
|
|
35
|
-
export { EmailTemplatePreview, NoEmailTemplatePreview, replaceLocalization, replacePlaceholders } from './EmailTemplatePreview';
|
|
35
|
+
export { EmailTemplatePreview, NoEmailTemplatePreview, replaceLocalization, replacePlaceholders, matchClickMapItems, decorateClickMap, CLICK_MAP_BADGE_ATTRIBUTE } from './EmailTemplatePreview';
|
|
36
36
|
export { PushPreview } from './PushPreview';
|
|
37
37
|
export { WhatsAppPreview } from './WhatsAppPreview';
|
|
38
38
|
export { Toast, ToastProvider, useToast } from './Toast';
|