@pushwoosh/dumb-components 1.1.223 → 1.1.224
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.js +3 -1
- package/EmailTemplatePreview/__tests__/clickMap.test.js +31 -1
- package/EmailTemplatePreview/clickMap.d.ts +5 -2
- package/EmailTemplatePreview/clickMap.js +46 -6
- package/EmailTemplatePreview/index.d.ts +2 -2
- package/EmailTemplatePreview/index.js +1 -1
- package/EmailTemplatePreview/types.d.ts +3 -0
- package/package.json +1 -1
|
@@ -42,6 +42,7 @@ export function EmailTemplatePreview(props) {
|
|
|
42
42
|
onReady
|
|
43
43
|
} = props;
|
|
44
44
|
const clickMapItems = clickMap?.items;
|
|
45
|
+
const clickMapMode = clickMap?.mode;
|
|
45
46
|
const clickMapActiveKey = clickMap?.activeKey;
|
|
46
47
|
const clickMapOnSelect = clickMap?.onSelect;
|
|
47
48
|
const clickMapOnPlaced = clickMap?.onPlaced;
|
|
@@ -60,6 +61,7 @@ export function EmailTemplatePreview(props) {
|
|
|
60
61
|
const handleReady = useCallback(frameDocument => {
|
|
61
62
|
const cleanups = [onReady?.(frameDocument), clickMapItems && decorateClickMap(frameDocument, {
|
|
62
63
|
items: clickMapItems,
|
|
64
|
+
mode: clickMapMode,
|
|
63
65
|
activeKey: clickMapActiveKey,
|
|
64
66
|
scale,
|
|
65
67
|
onSelect: clickMapOnSelect,
|
|
@@ -68,7 +70,7 @@ export function EmailTemplatePreview(props) {
|
|
|
68
70
|
return () => {
|
|
69
71
|
cleanups.forEach(cleanup => cleanup?.());
|
|
70
72
|
};
|
|
71
|
-
}, [onReady, clickMapItems, clickMapActiveKey, clickMapOnSelect, clickMapOnPlaced, scale]);
|
|
73
|
+
}, [onReady, clickMapItems, clickMapMode, clickMapActiveKey, clickMapOnSelect, clickMapOnPlaced, scale]);
|
|
72
74
|
return _jsx(IframeStyled, {
|
|
73
75
|
...LIGHT_ISLAND_PROPS,
|
|
74
76
|
"$scale": scale,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CLICK_MAP_BADGE_ATTRIBUTE, decorateClickMap, matchClickMapItems } from '../clickMap';
|
|
1
|
+
import { CLICK_MAP_BADGE_ATTRIBUTE, CLICK_MAP_HEAT_ATTRIBUTE, decorateClickMap, matchClickMapItems } from '../clickMap';
|
|
2
2
|
const PROMO = 'http://spartak.com/matchtowin';
|
|
3
3
|
const TICKETS = 'https://tickets.spartak.com/zones/18956';
|
|
4
4
|
const SHOP = 'https://shop.spartak.com/';
|
|
@@ -135,4 +135,34 @@ describe('decorateClickMap', () => {
|
|
|
135
135
|
expect(frameDocument.querySelectorAll(`[${CLICK_MAP_BADGE_ATTRIBUTE}]`)).toHaveLength(0);
|
|
136
136
|
expect(frameDocument.querySelector('a strong')?.parentElement?.style.outline).toBe('');
|
|
137
137
|
});
|
|
138
|
+
});
|
|
139
|
+
describe('decorateClickMap in heat mode', () => {
|
|
140
|
+
it('tints every matched anchor by its share of clicks and counts only the active one', () => {
|
|
141
|
+
const frameDocument = createDocument();
|
|
142
|
+
const items = [item('button', PROMO, 'Перейти на страницу акции', 1000), item('image', PROMO, '', 250), item('gone', 'https://old.example.com/', 'x', 5)];
|
|
143
|
+
const cleanup = decorateClickMap(frameDocument, {
|
|
144
|
+
items,
|
|
145
|
+
mode: 'heat',
|
|
146
|
+
activeKey: 'image'
|
|
147
|
+
});
|
|
148
|
+
const heats = [...frameDocument.querySelectorAll(`[${CLICK_MAP_HEAT_ATTRIBUTE}]`)];
|
|
149
|
+
const badges = [...frameDocument.querySelectorAll(`[${CLICK_MAP_BADGE_ATTRIBUTE}]`)];
|
|
150
|
+
const alpha = heat => Number(heat.style.backgroundColor.match(/[\d.]+\)$/)?.[0].slice(0, -1));
|
|
151
|
+
expect(heats.map(heat => heat.getAttribute(CLICK_MAP_HEAT_ATTRIBUTE))).toEqual(['button', 'image']);
|
|
152
|
+
expect(alpha(heats[0])).toBeGreaterThan(alpha(heats[1]));
|
|
153
|
+
expect(badges.map(badge => [badge.getAttribute(CLICK_MAP_BADGE_ATTRIBUTE), badge.textContent])).toEqual([['image', '250']]);
|
|
154
|
+
cleanup();
|
|
155
|
+
expect(frameDocument.querySelectorAll(`[${CLICK_MAP_HEAT_ATTRIBUTE}], [${CLICK_MAP_BADGE_ATTRIBUTE}]`)).toHaveLength(0);
|
|
156
|
+
});
|
|
157
|
+
it('selects a row by clicking its heat overlay', () => {
|
|
158
|
+
const frameDocument = createDocument();
|
|
159
|
+
const onSelect = jest.fn();
|
|
160
|
+
decorateClickMap(frameDocument, {
|
|
161
|
+
items: [item('text', PROMO, 'странице акции', 1)],
|
|
162
|
+
mode: 'heat',
|
|
163
|
+
onSelect
|
|
164
|
+
});
|
|
165
|
+
frameDocument.querySelector(`[${CLICK_MAP_HEAT_ATTRIBUTE}]`)?.click();
|
|
166
|
+
expect(onSelect).toHaveBeenCalledWith('text');
|
|
167
|
+
});
|
|
138
168
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EmailClickMapItem, EmailClickMapPlacement } from './types';
|
|
1
|
+
import type { EmailClickMapItem, EmailClickMapMode, EmailClickMapPlacement } from './types';
|
|
2
2
|
/** An anchor of the rendered template together with the rows recorded on it. */
|
|
3
3
|
export type EmailClickMapMatch = {
|
|
4
4
|
anchor: HTMLAnchorElement;
|
|
@@ -12,6 +12,7 @@ export type EmailClickMapMatchResult = {
|
|
|
12
12
|
/** Options of {@link decorateClickMap}. */
|
|
13
13
|
export type EmailClickMapDecorateOptions = {
|
|
14
14
|
items: EmailClickMapItem[];
|
|
15
|
+
mode?: EmailClickMapMode;
|
|
15
16
|
activeKey?: string | null;
|
|
16
17
|
scale?: number;
|
|
17
18
|
onSelect?: (key: string) => void;
|
|
@@ -21,7 +22,9 @@ export type EmailClickMapDecorateOptions = {
|
|
|
21
22
|
export declare const CLICK_MAP_BADGE_ATTRIBUTE = "data-pw-click-map-badge";
|
|
22
23
|
/** Stable anchor id stamped into the template html on save; the click carries it as `linkId`. */
|
|
23
24
|
export declare const CLICK_MAP_LINK_ID_ATTRIBUTE = "data-pw-lid";
|
|
25
|
+
/** Attribute every heat overlay carries, so tests and consumers can find them. */
|
|
26
|
+
export declare const CLICK_MAP_HEAT_ATTRIBUTE = "data-pw-click-map-heat";
|
|
24
27
|
/** Lands click-tracking rows on template anchors by stamped `linkId`, else by (link, text), else every anchor of the link. */
|
|
25
28
|
export declare function matchClickMapItems(document: Document, items: EmailClickMapItem[]): EmailClickMapMatchResult;
|
|
26
|
-
/** Draws click
|
|
29
|
+
/** Draws the click map over matched anchors (counts in `badges` mode, tints in `heat`), outlines `activeKey`; returns a cleanup. */
|
|
27
30
|
export declare function decorateClickMap(document: Document, options: EmailClickMapDecorateOptions): () => void;
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
export const CLICK_MAP_BADGE_ATTRIBUTE = 'data-pw-click-map-badge';
|
|
3
3
|
/** Stable anchor id stamped into the template html on save; the click carries it as `linkId`. */
|
|
4
4
|
export const CLICK_MAP_LINK_ID_ATTRIBUTE = 'data-pw-lid';
|
|
5
|
+
/** Attribute every heat overlay carries, so tests and consumers can find them. */
|
|
6
|
+
export const CLICK_MAP_HEAT_ATTRIBUTE = 'data-pw-click-map-heat';
|
|
7
|
+
const HEAT_MIN_ALPHA = 0.18;
|
|
8
|
+
const HEAT_MAX_ALPHA = 0.7;
|
|
5
9
|
const ACTIVE_COLOR = '#00b2ff';
|
|
6
10
|
const IDLE_COLOR = 'rgba(36, 48, 66, 0.82)';
|
|
7
11
|
const compactNumber = new Intl.NumberFormat('en', {
|
|
@@ -96,29 +100,64 @@ function createBadge(document, match, options) {
|
|
|
96
100
|
});
|
|
97
101
|
return badge;
|
|
98
102
|
}
|
|
99
|
-
|
|
103
|
+
const matchClicks = match => match.items.reduce((sum, item) => sum + item.clicks, 0);
|
|
104
|
+
const isActiveMatch = (match, options) => options.activeKey != null && match.items.some(item => item.key === options.activeKey);
|
|
105
|
+
function createHeat(document, match, maxClicks, options) {
|
|
106
|
+
const share = maxClicks > 0 ? matchClicks(match) / maxClicks : 0;
|
|
107
|
+
const heat = document.createElement('span');
|
|
108
|
+
heat.setAttribute(CLICK_MAP_HEAT_ATTRIBUTE, match.items.map(item => item.key).join(' '));
|
|
109
|
+
heat.title = match.items.map(item => `${item.title || item.link}: ${item.clicks}`).join('\n');
|
|
110
|
+
Object.assign(heat.style, {
|
|
111
|
+
position: 'absolute',
|
|
112
|
+
zIndex: '2147483646',
|
|
113
|
+
borderRadius: `${4 * (options.scale ?? 1)}px`,
|
|
114
|
+
backgroundColor: `rgba(0, 178, 255, ${(HEAT_MIN_ALPHA + (HEAT_MAX_ALPHA - HEAT_MIN_ALPHA) * share).toFixed(2)})`,
|
|
115
|
+
cursor: options.onSelect ? 'pointer' : 'default'
|
|
116
|
+
});
|
|
117
|
+
heat.addEventListener('click', event => {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
event.stopPropagation();
|
|
120
|
+
options.onSelect?.(match.items[0].key);
|
|
121
|
+
});
|
|
122
|
+
return heat;
|
|
123
|
+
}
|
|
124
|
+
/** Draws the click map over matched anchors (counts in `badges` mode, tints in `heat`), outlines `activeKey`; returns a cleanup. */
|
|
100
125
|
export function decorateClickMap(document, options) {
|
|
101
126
|
const view = document.defaultView;
|
|
127
|
+
const heatMode = options.mode === 'heat';
|
|
102
128
|
const {
|
|
103
129
|
matches,
|
|
104
130
|
placement
|
|
105
131
|
} = matchClickMapItems(document, options.items);
|
|
106
|
-
const
|
|
107
|
-
const
|
|
132
|
+
const maxClicks = Math.max(0, ...matches.map(matchClicks));
|
|
133
|
+
const heats = heatMode ? matches.map(match => createHeat(document, match, maxClicks, options)) : [];
|
|
134
|
+
const badged = heatMode ? matches.filter(match => isActiveMatch(match, options)) : matches;
|
|
135
|
+
const badges = badged.map(match => createBadge(document, match, options));
|
|
136
|
+
const outlined = matches.filter(match => isActiveMatch(match, options)).map(match => [match.anchor, match.anchor.style.outline]);
|
|
108
137
|
const isInlineText = anchor => view?.getComputedStyle(anchor).display === 'inline' && !anchor.querySelector('img');
|
|
109
138
|
const layout = () => {
|
|
110
139
|
const maxRight = document.documentElement.clientWidth;
|
|
111
|
-
|
|
140
|
+
const scrollX = view?.scrollX ?? 0;
|
|
141
|
+
const scrollY = view?.scrollY ?? 0;
|
|
142
|
+
heats.forEach((heat, index) => {
|
|
143
|
+
const rect = anchorRect(matches[index].anchor);
|
|
144
|
+
heat.style.top = `${rect.top + scrollY}px`;
|
|
145
|
+
heat.style.left = `${rect.left + scrollX}px`;
|
|
146
|
+
heat.style.width = `${rect.width}px`;
|
|
147
|
+
heat.style.height = `${rect.height}px`;
|
|
148
|
+
});
|
|
149
|
+
badged.forEach((match, index) => {
|
|
112
150
|
const badge = badges[index];
|
|
113
151
|
const rect = anchorRect(match.anchor);
|
|
114
152
|
const right = Math.min(rect.right, maxRight);
|
|
115
153
|
const inline = isInlineText(match.anchor);
|
|
116
154
|
const left = inline ? Math.min(right, maxRight - badge.offsetWidth) : right - badge.offsetWidth;
|
|
117
155
|
const top = inline ? rect.top + rect.height / 2 : rect.top;
|
|
118
|
-
badge.style.top = `${top +
|
|
119
|
-
badge.style.left = `${left +
|
|
156
|
+
badge.style.top = `${top + scrollY}px`;
|
|
157
|
+
badge.style.left = `${left + scrollX}px`;
|
|
120
158
|
});
|
|
121
159
|
};
|
|
160
|
+
heats.forEach(heat => document.body.appendChild(heat));
|
|
122
161
|
badges.forEach(badge => document.body.appendChild(badge));
|
|
123
162
|
outlined.forEach(([anchor]) => {
|
|
124
163
|
anchor.style.outline = `${2 * (options.scale ?? 1)}px solid ${ACTIVE_COLOR}`;
|
|
@@ -131,6 +170,7 @@ export function decorateClickMap(document, options) {
|
|
|
131
170
|
return () => {
|
|
132
171
|
observer?.disconnect();
|
|
133
172
|
view?.removeEventListener('resize', layout);
|
|
173
|
+
heats.forEach(heat => heat.remove());
|
|
134
174
|
badges.forEach(badge => badge.remove());
|
|
135
175
|
outlined.forEach(([anchor, outline]) => {
|
|
136
176
|
anchor.style.outline = outline;
|
|
@@ -1,5 +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';
|
|
4
|
+
export { matchClickMapItems, decorateClickMap, CLICK_MAP_BADGE_ATTRIBUTE, CLICK_MAP_HEAT_ATTRIBUTE, CLICK_MAP_LINK_ID_ATTRIBUTE, type EmailClickMapMatch, type EmailClickMapMatchResult, type EmailClickMapDecorateOptions, } from './clickMap';
|
|
5
|
+
export type { EmailClickMapItem, EmailClickMapMode, EmailClickMapPlacement, EmailClickMapProps, PreviewIframeProps, } from './types';
|
|
@@ -1,4 +1,4 @@
|
|
|
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 } from './clickMap';
|
|
4
|
+
export { matchClickMapItems, decorateClickMap, CLICK_MAP_BADGE_ATTRIBUTE, CLICK_MAP_HEAT_ATTRIBUTE, CLICK_MAP_LINK_ID_ATTRIBUTE } from './clickMap';
|
|
@@ -19,9 +19,12 @@ export type EmailClickMapPlacement = {
|
|
|
19
19
|
/** Keys with no matching anchor in the current template. */
|
|
20
20
|
unplaced: string[];
|
|
21
21
|
};
|
|
22
|
+
/** `badges` draws a count on every anchor; `heat` tints anchors by their share and shows the count on the active one only. */
|
|
23
|
+
export type EmailClickMapMode = 'badges' | 'heat';
|
|
22
24
|
/** Click counts to overlay on the rendered email, see {@link EmailTemplatePreview}. */
|
|
23
25
|
export type EmailClickMapProps = {
|
|
24
26
|
items: EmailClickMapItem[];
|
|
27
|
+
mode?: EmailClickMapMode;
|
|
25
28
|
/** Key whose badge and anchor are highlighted. */
|
|
26
29
|
activeKey?: string | null;
|
|
27
30
|
/** Called with the key of the badge the user clicked. */
|