@lvce-editor/renderer-process 30.59.0 → 30.60.1
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/dist/rendererProcessMain.js +63 -18
- package/dist/sessionReplayPlayer.js +243 -443
- package/dist/sessionReplayWorkerMain.js +11962 -94
- package/package.json +1 -1
|
@@ -1,40 +1,3 @@
|
|
|
1
|
-
const createActivityChart = document => {
|
|
2
|
-
const namespace = 'http://www.w3.org/2000/svg';
|
|
3
|
-
const element = document.createElementNS(namespace, 'svg');
|
|
4
|
-
element.classList.add('SessionReplayActivity');
|
|
5
|
-
element.setAttribute('viewBox', '0 0 240 48');
|
|
6
|
-
element.setAttribute('preserveAspectRatio', 'none');
|
|
7
|
-
element.setAttribute('role', 'img');
|
|
8
|
-
element.setAttribute('aria-label', 'Session replay activity');
|
|
9
|
-
const title = document.createElementNS(namespace, 'title');
|
|
10
|
-
title.textContent = 'Recorded activity over time. Click to seek, or use the position slider with the keyboard.';
|
|
11
|
-
const path = document.createElementNS(namespace, 'path');
|
|
12
|
-
const cursor = document.createElementNS(namespace, 'line');
|
|
13
|
-
cursor.classList.add('SessionReplayActivityCursor');
|
|
14
|
-
cursor.setAttribute('y1', '0');
|
|
15
|
-
cursor.setAttribute('y2', '48');
|
|
16
|
-
cursor.setAttribute('vector-effect', 'non-scaling-stroke');
|
|
17
|
-
element.append(title, path, cursor);
|
|
18
|
-
return {
|
|
19
|
-
element,
|
|
20
|
-
setActivity(counts) {
|
|
21
|
-
const maximum = Math.max(1, ...counts);
|
|
22
|
-
const width = 240 / counts.length;
|
|
23
|
-
const segments = counts.map((count, index) => {
|
|
24
|
-
if (!count) return '';
|
|
25
|
-
const height = count / maximum * 44;
|
|
26
|
-
return `M${index * width} 48v-${height}h${width}v${height}z`;
|
|
27
|
-
});
|
|
28
|
-
path.setAttribute('d', segments.join(' '));
|
|
29
|
-
},
|
|
30
|
-
setPosition(fraction) {
|
|
31
|
-
const x = String(fraction * 240);
|
|
32
|
-
cursor.setAttribute('x1', x);
|
|
33
|
-
cursor.setAttribute('x2', x);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
|
|
38
1
|
// Walk the structured-clone graph, including collections and shared buffer views.
|
|
39
2
|
const getTransferrables = value => {
|
|
40
3
|
const seen = [];
|
|
@@ -113,176 +76,6 @@ const createClient = url => {
|
|
|
113
76
|
};
|
|
114
77
|
};
|
|
115
78
|
|
|
116
|
-
const playerStyles = `
|
|
117
|
-
.SessionReplay > .SessionReplayControls {
|
|
118
|
-
box-sizing: border-box;
|
|
119
|
-
display: grid;
|
|
120
|
-
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
121
|
-
flex: none;
|
|
122
|
-
align-items: center;
|
|
123
|
-
gap: 0 18px;
|
|
124
|
-
margin: 12px 16px 16px;
|
|
125
|
-
padding: 10px 16px 10px 10px;
|
|
126
|
-
border: 1px solid #ffffff1f;
|
|
127
|
-
border-radius: 12px;
|
|
128
|
-
background: #181a1f;
|
|
129
|
-
color: #f4f4f5;
|
|
130
|
-
box-shadow: 0 4px 20px #00000040;
|
|
131
|
-
font: 13px/1.4 system-ui, sans-serif;
|
|
132
|
-
color-scheme: dark;
|
|
133
|
-
}
|
|
134
|
-
.SessionReplayControls > .SessionReplayPlay {
|
|
135
|
-
grid-column: 1;
|
|
136
|
-
grid-row: 2;
|
|
137
|
-
appearance: none;
|
|
138
|
-
display: grid;
|
|
139
|
-
place-items: center;
|
|
140
|
-
flex: none;
|
|
141
|
-
width: 52px;
|
|
142
|
-
height: 44px;
|
|
143
|
-
padding: 0;
|
|
144
|
-
border: 0;
|
|
145
|
-
border-radius: 8px;
|
|
146
|
-
background: #00adef;
|
|
147
|
-
color: #07141c;
|
|
148
|
-
cursor: pointer;
|
|
149
|
-
}
|
|
150
|
-
.SessionReplayControls > .SessionReplayPlay:hover:not(:disabled) {
|
|
151
|
-
background: #43caff;
|
|
152
|
-
}
|
|
153
|
-
.SessionReplayControls > .SessionReplayPlay:active:not(:disabled) {
|
|
154
|
-
background: #0095ce;
|
|
155
|
-
}
|
|
156
|
-
.SessionReplayPlay > svg {
|
|
157
|
-
display: block;
|
|
158
|
-
width: 22px;
|
|
159
|
-
height: 22px;
|
|
160
|
-
fill: currentColor;
|
|
161
|
-
pointer-events: none;
|
|
162
|
-
}
|
|
163
|
-
.SessionReplayControls > .SessionReplayPosition {
|
|
164
|
-
grid-column: 2;
|
|
165
|
-
grid-row: 2;
|
|
166
|
-
appearance: none;
|
|
167
|
-
box-sizing: border-box;
|
|
168
|
-
flex: 1;
|
|
169
|
-
min-width: 0;
|
|
170
|
-
width: 100%;
|
|
171
|
-
height: 28px;
|
|
172
|
-
margin: 0;
|
|
173
|
-
padding: 0;
|
|
174
|
-
border: 0;
|
|
175
|
-
border-radius: 3px;
|
|
176
|
-
background: transparent;
|
|
177
|
-
cursor: pointer;
|
|
178
|
-
--replay-progress: 0%;
|
|
179
|
-
}
|
|
180
|
-
.SessionReplayPosition::-webkit-slider-runnable-track {
|
|
181
|
-
height: 5px;
|
|
182
|
-
border-radius: 3px;
|
|
183
|
-
background: linear-gradient(to right, #00adef var(--replay-progress), #454950 var(--replay-progress));
|
|
184
|
-
}
|
|
185
|
-
.SessionReplayPosition::-moz-range-track {
|
|
186
|
-
height: 5px;
|
|
187
|
-
border-radius: 3px;
|
|
188
|
-
background: #454950;
|
|
189
|
-
}
|
|
190
|
-
.SessionReplayPosition::-moz-range-progress {
|
|
191
|
-
height: 5px;
|
|
192
|
-
border-radius: 3px;
|
|
193
|
-
background: #00adef;
|
|
194
|
-
}
|
|
195
|
-
.SessionReplayPosition::-webkit-slider-thumb {
|
|
196
|
-
appearance: none;
|
|
197
|
-
width: 13px;
|
|
198
|
-
height: 13px;
|
|
199
|
-
margin-top: -4px;
|
|
200
|
-
border: 0;
|
|
201
|
-
border-radius: 50%;
|
|
202
|
-
background: #ffffff;
|
|
203
|
-
box-shadow: 0 1px 5px #00000066;
|
|
204
|
-
}
|
|
205
|
-
.SessionReplayPosition::-moz-range-thumb {
|
|
206
|
-
width: 13px;
|
|
207
|
-
height: 13px;
|
|
208
|
-
border: 0;
|
|
209
|
-
border-radius: 50%;
|
|
210
|
-
background: #ffffff;
|
|
211
|
-
box-shadow: 0 1px 5px #00000066;
|
|
212
|
-
}
|
|
213
|
-
.SessionReplayControls > .SessionReplayActivity {
|
|
214
|
-
grid-column: 2;
|
|
215
|
-
grid-row: 1;
|
|
216
|
-
display: block;
|
|
217
|
-
width: calc(100% - 13px);
|
|
218
|
-
min-width: 0;
|
|
219
|
-
height: 48px;
|
|
220
|
-
margin: 0 6.5px;
|
|
221
|
-
border-bottom: 1px solid #454950;
|
|
222
|
-
fill: #00adef80;
|
|
223
|
-
cursor: pointer;
|
|
224
|
-
}
|
|
225
|
-
.SessionReplayControls > .SessionReplayActivity:hover {
|
|
226
|
-
fill: #00adefb3;
|
|
227
|
-
}
|
|
228
|
-
.SessionReplayActivityCursor {
|
|
229
|
-
stroke: #ffffff;
|
|
230
|
-
stroke-width: 1;
|
|
231
|
-
pointer-events: none;
|
|
232
|
-
}
|
|
233
|
-
.SessionReplayControls > :focus-visible {
|
|
234
|
-
outline: 2px solid #ffffff;
|
|
235
|
-
outline-offset: 4px;
|
|
236
|
-
}
|
|
237
|
-
.SessionReplayControls > :disabled {
|
|
238
|
-
opacity: 0.45;
|
|
239
|
-
cursor: default;
|
|
240
|
-
}
|
|
241
|
-
.SessionReplayControls > .SessionReplayTime {
|
|
242
|
-
grid-column: 3;
|
|
243
|
-
grid-row: 2;
|
|
244
|
-
flex: none;
|
|
245
|
-
font: inherit;
|
|
246
|
-
font-variant-numeric: tabular-nums;
|
|
247
|
-
white-space: nowrap;
|
|
248
|
-
}
|
|
249
|
-
.SessionReplayControls > .SessionReplayTime[role='alert'] {
|
|
250
|
-
flex: 1;
|
|
251
|
-
min-width: 0;
|
|
252
|
-
color: #ffb4ab;
|
|
253
|
-
white-space: normal;
|
|
254
|
-
overflow-wrap: anywhere;
|
|
255
|
-
}
|
|
256
|
-
@media (max-width: 480px) {
|
|
257
|
-
.SessionReplay > .SessionReplayControls {
|
|
258
|
-
column-gap: 12px;
|
|
259
|
-
margin: 8px;
|
|
260
|
-
padding: 8px;
|
|
261
|
-
font-size: 12px;
|
|
262
|
-
}
|
|
263
|
-
.SessionReplayControls > .SessionReplayPlay {
|
|
264
|
-
width: 44px;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
@media (forced-colors: active) {
|
|
268
|
-
.SessionReplayControls > .SessionReplayActivity {
|
|
269
|
-
fill: Highlight;
|
|
270
|
-
border-color: CanvasText;
|
|
271
|
-
}
|
|
272
|
-
.SessionReplayActivityCursor {
|
|
273
|
-
stroke: CanvasText;
|
|
274
|
-
}
|
|
275
|
-
.SessionReplayControls > .SessionReplayPlay {
|
|
276
|
-
border: 1px solid ButtonText;
|
|
277
|
-
background: ButtonFace;
|
|
278
|
-
color: ButtonText;
|
|
279
|
-
}
|
|
280
|
-
.SessionReplayControls > .SessionReplayPosition {
|
|
281
|
-
appearance: auto;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
`;
|
|
285
|
-
|
|
286
79
|
var __create = Object.create;
|
|
287
80
|
var __defProp = Object.defineProperty;
|
|
288
81
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -10986,7 +10779,7 @@ var walker_default = {
|
|
|
10986
10779
|
};
|
|
10987
10780
|
|
|
10988
10781
|
// ../../node_modules/css-tree/lib/syntax/index.js
|
|
10989
|
-
|
|
10782
|
+
create_default({
|
|
10990
10783
|
...lexer_default,
|
|
10991
10784
|
...parser_default,
|
|
10992
10785
|
...walker_default
|
|
@@ -11058,122 +10851,7 @@ function encode3(str) {
|
|
|
11058
10851
|
return encoded;
|
|
11059
10852
|
}
|
|
11060
10853
|
|
|
11061
|
-
// ../../node_modules/css-tree/lib/index.js
|
|
11062
|
-
var {
|
|
11063
|
-
parse: parse51,
|
|
11064
|
-
generate: generate51,
|
|
11065
|
-
walk: walk2} = syntax_default;
|
|
11066
|
-
|
|
11067
|
-
// ../session-replay-worker/src/parts/AssetUrls/AssetUrls.ts
|
|
11068
|
-
var resolveAssetUrl$1 = (value, assetBaseUrl) => {
|
|
11069
|
-
if (!assetBaseUrl) return void 0;
|
|
11070
|
-
value = value.replace("/extensions/builtin.vscode-icons/icons/", "/file-icons/");
|
|
11071
|
-
const match = /\/(icons|fonts|file-icons)\/([\w./-]+)(?:[?#].*)?$/.exec(value);
|
|
11072
|
-
if (!match || match[2].split("/").some(part => !part || part === "." || part === "..")) return void 0;
|
|
11073
|
-
if (!/\.(svg|png|jpg|jpeg|gif|webp|woff2?|ttf|otf)$/.test(match[2])) return void 0;
|
|
11074
|
-
return `${assetBaseUrl}${match[1]}/${match[2]}`;
|
|
11075
|
-
};
|
|
11076
|
-
|
|
11077
|
-
// ../session-replay-worker/src/parts/ReplayCss/ReplayCss.ts
|
|
11078
|
-
var allowedRules = ["media", "supports", "container", "layer", "font-face", "keyframes", "-webkit-keyframes", "property"];
|
|
11079
|
-
var blockedFunctions = ["url", "image-set", "-webkit-image-set", "image", "src", "attr", "paint"];
|
|
11080
|
-
var replayCss = (css, assetBaseUrl, inline = false) => {
|
|
11081
|
-
try {
|
|
11082
|
-
const ast = parse51(css, {
|
|
11083
|
-
context: inline ? "declarationList" : "stylesheet",
|
|
11084
|
-
parseCustomProperty: true
|
|
11085
|
-
});
|
|
11086
|
-
walk2(ast, {
|
|
11087
|
-
enter(node, item, list) {
|
|
11088
|
-
if (node.type === "Atrule" && !allowedRules.includes(ident_exports.decode(node.name).toLowerCase())) {
|
|
11089
|
-
list.remove(item);
|
|
11090
|
-
return this.skip;
|
|
11091
|
-
}
|
|
11092
|
-
if (node.type === "Raw") {
|
|
11093
|
-
list.remove(item);
|
|
11094
|
-
return this.skip;
|
|
11095
|
-
}
|
|
11096
|
-
if (node.type !== "Declaration") return void 0;
|
|
11097
|
-
let unsafe = false;
|
|
11098
|
-
walk2(node.value, value => {
|
|
11099
|
-
if (value.type === "Raw" || value.type === "Function" && blockedFunctions.includes(ident_exports.decode(value.name).toLowerCase())) unsafe = true;
|
|
11100
|
-
if (value.type === "Url") {
|
|
11101
|
-
const resolved = resolveAssetUrl$1(value.value, assetBaseUrl);
|
|
11102
|
-
if (resolved) value.value = resolved;else if (!/^data:(?:image\/(?:png|jpeg|gif|webp)|font\/[\w-]+);base64,[a-z\d+/=\s]+$/i.test(value.value) && !/^#[\w-]+$/.test(value.value)) unsafe = true;
|
|
11103
|
-
}
|
|
11104
|
-
});
|
|
11105
|
-
if (unsafe) list.remove(item);
|
|
11106
|
-
return this.skip;
|
|
11107
|
-
}
|
|
11108
|
-
});
|
|
11109
|
-
return generate51(ast);
|
|
11110
|
-
} catch {
|
|
11111
|
-
return "";
|
|
11112
|
-
}
|
|
11113
|
-
};
|
|
11114
|
-
|
|
11115
|
-
// Only known editor asset paths can be remapped into the host's trusted asset directory.
|
|
11116
|
-
const resolveAssetUrl = (value, assetBaseUrl) => {
|
|
11117
|
-
if (!assetBaseUrl) return undefined;
|
|
11118
|
-
value = value.replace('/extensions/builtin.vscode-icons/icons/', '/file-icons/');
|
|
11119
|
-
const match = /\/(icons|fonts|file-icons)\/([\w./-]+)(?:[?#].*)?$/.exec(value);
|
|
11120
|
-
if (!match || match[2].split('/').some(part => !part || part === '.' || part === '..')) return undefined;
|
|
11121
|
-
if (!/\.(svg|png|jpg|jpeg|gif|webp|woff2?|ttf|otf)$/.test(match[2])) return undefined;
|
|
11122
|
-
return `${assetBaseUrl}${match[1]}/${match[2]}`;
|
|
11123
|
-
};
|
|
11124
|
-
|
|
11125
|
-
const tags = 'body div span p pre code main section article header footer nav aside h1 h2 h3 h4 h5 h6 ul ol li table thead tbody tr td th button input textarea select option label form fieldset legend a img br hr strong em b i u s small details summary svg path rect circle ellipse line polyline polygon g defs clipPath text tspan'.split(' ');
|
|
11126
|
-
const attributes = /^(class|style|id|title|role|type|checked|disabled|selected|placeholder|width|height|viewBox|d|fill|stroke|cx|cy|r|x|y|x1|x2|y1|y2|points|transform|xmlns|data-[\w-]+|aria-[\w-]+)$/i;
|
|
11127
10854
|
const svgNamespace = 'http://www.w3.org/2000/svg';
|
|
11128
|
-
const attributeValue = (name, value, assetBaseUrl) => {
|
|
11129
|
-
if (name === 'style') return replayCss(value, assetBaseUrl, true);
|
|
11130
|
-
if (name !== 'fill' && name !== 'stroke') return value;
|
|
11131
|
-
const css = replayCss(`${name}:${value}`, assetBaseUrl, true);
|
|
11132
|
-
return css ? css.slice(css.indexOf(':') + 1) : '';
|
|
11133
|
-
};
|
|
11134
|
-
const imageSource = (attrs, assetBaseUrl) => {
|
|
11135
|
-
const value = attrs.src;
|
|
11136
|
-
if (typeof value === 'string') {
|
|
11137
|
-
if (/^data:image\/(png|jpeg|gif|webp);base64,/.test(value)) return value;
|
|
11138
|
-
const src = resolveAssetUrl(value, assetBaseUrl);
|
|
11139
|
-
if (src) return src;
|
|
11140
|
-
}
|
|
11141
|
-
// Older captures omitted image sources; the editor logo has a stable identity.
|
|
11142
|
-
return attrs.class?.split(/\s+/).includes('TitleBarIconIcon') ? resolveAssetUrl('/icons/icon.svg', assetBaseUrl) : undefined;
|
|
11143
|
-
};
|
|
11144
|
-
const normalizeAttributes = (value, tag, assetBaseUrl) => {
|
|
11145
|
-
const attrs = Object.create(null);
|
|
11146
|
-
const entries = Object.entries(value.attrs || {});
|
|
11147
|
-
for (const [key, val] of entries) {
|
|
11148
|
-
if (typeof val !== 'string') continue;
|
|
11149
|
-
if (attributes.test(key)) attrs[key] = attributeValue(key.toLowerCase(), val, assetBaseUrl);
|
|
11150
|
-
}
|
|
11151
|
-
if (tag === 'img') {
|
|
11152
|
-
const src = imageSource(value.attrs || {}, assetBaseUrl);
|
|
11153
|
-
if (src) attrs.src = src;
|
|
11154
|
-
}
|
|
11155
|
-
return attrs;
|
|
11156
|
-
};
|
|
11157
|
-
const normalizeDom = (root, assetBaseUrl) => {
|
|
11158
|
-
let count = 0;
|
|
11159
|
-
const visit = (value, depth) => {
|
|
11160
|
-
if (++count > 100_000 || depth > 150 || !value || typeof value !== 'object') throw new Error('Invalid replay DOM');
|
|
11161
|
-
if (typeof value.text === 'string') return {
|
|
11162
|
-
text: value.text
|
|
11163
|
-
};
|
|
11164
|
-
const tag = value.tag && tags.includes(value.tag) ? value.tag : 'div';
|
|
11165
|
-
return {
|
|
11166
|
-
attrs: normalizeAttributes(value, tag, assetBaseUrl),
|
|
11167
|
-
checked: value.checked === true,
|
|
11168
|
-
children: (value.children || []).map(child => visit(child, depth + 1)),
|
|
11169
|
-
scroll: Array.isArray(value.scroll) ? [value.scroll[0], value.scroll[1]] : [0, 0],
|
|
11170
|
-
svg: value.svg === true,
|
|
11171
|
-
tag,
|
|
11172
|
-
value: typeof value.value === 'string' ? value.value : undefined
|
|
11173
|
-
};
|
|
11174
|
-
};
|
|
11175
|
-
return visit(root, 0);
|
|
11176
|
-
};
|
|
11177
10855
|
const key = value => value.attrs?.['data-uid'] || value.attrs?.id;
|
|
11178
10856
|
const compatible = (before, after) => typeof before.text === typeof after.text && before.tag === after.tag && before.svg === after.svg;
|
|
11179
10857
|
const createNode = (document, value) => {
|
|
@@ -11237,8 +10915,7 @@ const createDomRenderer = root => {
|
|
|
11237
10915
|
patchAttributes(element, old?.value.attrs || {}, value.attrs);
|
|
11238
10916
|
const children = patchChildren(element, old?.children || [], value.children);
|
|
11239
10917
|
patchProperties(element, old?.value, value);
|
|
11240
|
-
|
|
11241
|
-
scrolls.push([element, x, y]);
|
|
10918
|
+
if (value.scroll) scrolls.push([element, ...value.scroll]);
|
|
11242
10919
|
return {
|
|
11243
10920
|
children,
|
|
11244
10921
|
node,
|
|
@@ -11272,30 +10949,30 @@ const createRenderer = target => {
|
|
|
11272
10949
|
const render = createDomRenderer(surface);
|
|
11273
10950
|
let sheets = [];
|
|
11274
10951
|
let previousStyles = [];
|
|
11275
|
-
let previousBase;
|
|
11276
10952
|
let previousClass = originalClass;
|
|
11277
10953
|
let previousTheme;
|
|
11278
10954
|
let appliedTheme = originalTheme;
|
|
11279
10955
|
const removeSheets = () => document.adoptedStyleSheets.filter(sheet => !sheets.includes(sheet));
|
|
11280
|
-
const update =
|
|
11281
|
-
const
|
|
10956
|
+
const update = frame => {
|
|
10957
|
+
const {
|
|
10958
|
+
dom
|
|
10959
|
+
} = frame;
|
|
11282
10960
|
const styles = (frame.styles || []).filter(value => typeof value === 'string');
|
|
11283
|
-
if (
|
|
10961
|
+
if (styles.length !== previousStyles.length || styles.some((css, index) => css !== previousStyles[index])) {
|
|
11284
10962
|
const others = removeSheets();
|
|
11285
10963
|
sheets = styles.map((css, index) => {
|
|
11286
|
-
if (
|
|
10964
|
+
if (css === previousStyles[index]) return sheets[index];
|
|
11287
10965
|
const sheet = new Sheet();
|
|
11288
|
-
sheet.replaceSync(
|
|
10966
|
+
sheet.replaceSync(css);
|
|
11289
10967
|
return sheet;
|
|
11290
10968
|
});
|
|
11291
10969
|
document.adoptedStyleSheets = [...others, ...sheets];
|
|
11292
10970
|
previousStyles = styles;
|
|
11293
|
-
previousBase = assetBaseUrl;
|
|
11294
10971
|
}
|
|
11295
10972
|
const className = typeof frame.documentElement?.className === 'string' ? frame.documentElement.className : '';
|
|
11296
10973
|
if (root.className !== className) root.className = className;
|
|
11297
10974
|
previousClass = className;
|
|
11298
|
-
const theme =
|
|
10975
|
+
const theme = frame.documentElement?.style || '';
|
|
11299
10976
|
if (theme !== previousTheme) root.style.cssText = theme;
|
|
11300
10977
|
previousTheme = theme;
|
|
11301
10978
|
appliedTheme = root.style.cssText;
|
|
@@ -11327,18 +11004,66 @@ const disposeFrame = target => {
|
|
|
11327
11004
|
renderers.get(target)?.dispose();
|
|
11328
11005
|
renderers.delete(target);
|
|
11329
11006
|
};
|
|
11330
|
-
const
|
|
11007
|
+
const renderPreparedFrame = (target, frame) => {
|
|
11331
11008
|
let render = renderers.get(target);
|
|
11332
11009
|
if (!render) {
|
|
11333
11010
|
render = createRenderer(target);
|
|
11334
11011
|
renderers.set(target, render);
|
|
11335
11012
|
}
|
|
11336
|
-
render.render(frame
|
|
11013
|
+
render.render(frame);
|
|
11337
11014
|
};
|
|
11338
11015
|
|
|
11016
|
+
const createTimelinePreview = container => {
|
|
11017
|
+
const document = container.ownerDocument;
|
|
11018
|
+
const view = document.defaultView;
|
|
11019
|
+
const popup = container.querySelector(':scope > .SessionReplay > .SessionReplayPreview');
|
|
11020
|
+
const image = popup.querySelector('.SessionReplayPreviewImage');
|
|
11021
|
+
let frame;
|
|
11022
|
+
const removeFrame = () => {
|
|
11023
|
+
if (frame?.contentDocument) disposeFrame(frame.contentDocument);
|
|
11024
|
+
frame?.remove();
|
|
11025
|
+
frame = undefined;
|
|
11026
|
+
};
|
|
11027
|
+
return {
|
|
11028
|
+
dispose() {
|
|
11029
|
+
removeFrame();
|
|
11030
|
+
popup.remove();
|
|
11031
|
+
},
|
|
11032
|
+
render(preview, enabled) {
|
|
11033
|
+
if (!enabled) removeFrame();
|
|
11034
|
+
if (!preview) return;
|
|
11035
|
+
if (!frame) {
|
|
11036
|
+
frame = document.createElement('iframe');
|
|
11037
|
+
frame.title = 'Session replay preview';
|
|
11038
|
+
frame.tabIndex = -1;
|
|
11039
|
+
frame.setAttribute('sandbox', 'allow-same-origin');
|
|
11040
|
+
image.append(frame);
|
|
11041
|
+
}
|
|
11042
|
+
const {
|
|
11043
|
+
height,
|
|
11044
|
+
scale,
|
|
11045
|
+
width,
|
|
11046
|
+
x,
|
|
11047
|
+
y
|
|
11048
|
+
} = preview;
|
|
11049
|
+
frame.style.width = `${width}px`;
|
|
11050
|
+
frame.style.height = `${height}px`;
|
|
11051
|
+
frame.style.transform = `scale(${scale})`;
|
|
11052
|
+
image.style.width = `${width * scale}px`;
|
|
11053
|
+
image.style.height = `${height * scale}px`;
|
|
11054
|
+
renderPreparedFrame(frame.contentDocument, preview.frame);
|
|
11055
|
+
const bounds = popup.getBoundingClientRect();
|
|
11056
|
+
popup.style.left = `${Math.max(8, Math.min(view.innerWidth - bounds.width - 8, x - bounds.width / 2))}px`;
|
|
11057
|
+
popup.style.top = `${Math.max(8, y - bounds.height - 8)}px`;
|
|
11058
|
+
}
|
|
11059
|
+
};
|
|
11060
|
+
};
|
|
11061
|
+
|
|
11062
|
+
const settingKey = 'sessionReplay.timelinePreviewEnabled';
|
|
11339
11063
|
const mountPlayer = async (container, {
|
|
11340
11064
|
assetBaseUrl,
|
|
11341
11065
|
source,
|
|
11066
|
+
timelinePreviewEnabled,
|
|
11342
11067
|
workerUrl
|
|
11343
11068
|
}) => {
|
|
11344
11069
|
const {
|
|
@@ -11351,131 +11076,206 @@ const mountPlayer = async (container, {
|
|
|
11351
11076
|
if (!assets.pathname.endsWith('/')) assets.pathname += '/';
|
|
11352
11077
|
}
|
|
11353
11078
|
const client = createClient(workerUrl);
|
|
11079
|
+
const document = ownerDocument;
|
|
11080
|
+
const view = document.defaultView;
|
|
11081
|
+
let enabled = timelinePreviewEnabled ?? true;
|
|
11082
|
+
if (timelinePreviewEnabled === undefined) {
|
|
11083
|
+
try {
|
|
11084
|
+
enabled = view.localStorage.getItem(settingKey) !== 'false';
|
|
11085
|
+
} catch {
|
|
11086
|
+
/* Storage may be unavailable in embedded players. */
|
|
11087
|
+
}
|
|
11088
|
+
}
|
|
11354
11089
|
container.replaceChildren();
|
|
11355
|
-
|
|
11356
|
-
|
|
11357
|
-
const document = container.ownerDocument;
|
|
11358
|
-
const style = document.createElement('style');
|
|
11359
|
-
style.textContent = playerStyles;
|
|
11360
|
-
const viewport = document.createElement('div');
|
|
11361
|
-
viewport.style.cssText = 'flex:1;min-height:0;overflow:auto;position:relative';
|
|
11362
|
-
const surface = createReplayRoot(document);
|
|
11363
|
-
viewport.append(surface);
|
|
11364
|
-
const controls = document.createElement('div');
|
|
11365
|
-
controls.className = 'SessionReplayControls';
|
|
11366
|
-
controls.setAttribute('role', 'group');
|
|
11367
|
-
controls.setAttribute('aria-label', 'Session replay controls');
|
|
11368
|
-
const play = document.createElement('button');
|
|
11369
|
-
play.type = 'button';
|
|
11370
|
-
play.className = 'SessionReplayPlay';
|
|
11371
|
-
const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
11372
|
-
icon.setAttribute('viewBox', '0 0 24 24');
|
|
11373
|
-
icon.setAttribute('aria-hidden', 'true');
|
|
11374
|
-
icon.setAttribute('focusable', 'false');
|
|
11375
|
-
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
11376
|
-
icon.append(path);
|
|
11377
|
-
play.append(icon);
|
|
11378
|
-
const updatePlayButton = playing => {
|
|
11379
|
-
const label = playing ? 'Pause' : 'Play';
|
|
11380
|
-
play.setAttribute('aria-label', label);
|
|
11381
|
-
play.title = label;
|
|
11382
|
-
path.setAttribute('d', playing ? 'M6 4h4v16H6zM14 4h4v16h-4z' : 'M8 4v16l12-8z');
|
|
11383
|
-
};
|
|
11384
|
-
updatePlayButton(false);
|
|
11385
|
-
const slider = document.createElement('input');
|
|
11386
|
-
slider.type = 'range';
|
|
11387
|
-
slider.min = '0';
|
|
11388
|
-
slider.step = '1';
|
|
11389
|
-
slider.value = '0';
|
|
11390
|
-
slider.setAttribute('aria-label', 'Session replay position');
|
|
11391
|
-
slider.className = 'SessionReplayPosition';
|
|
11392
|
-
const status = document.createElement('output');
|
|
11393
|
-
status.className = 'SessionReplayTime';
|
|
11394
|
-
// Playback updates frequently; announce the position only when the slider is used.
|
|
11395
|
-
status.setAttribute('aria-live', 'off');
|
|
11396
|
-
const activity = createActivityChart(document);
|
|
11397
|
-
controls.append(play, activity.element, slider, status);
|
|
11398
|
-
container.append(style, viewport, controls);
|
|
11090
|
+
const setDom = createDomRenderer(container);
|
|
11091
|
+
let preview;
|
|
11399
11092
|
let disposed = false;
|
|
11400
|
-
let
|
|
11093
|
+
let sequence = 0;
|
|
11401
11094
|
let timer;
|
|
11402
|
-
let
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
(
|
|
11407
|
-
|
|
11408
|
-
|
|
11409
|
-
|
|
11410
|
-
|
|
11411
|
-
|
|
11412
|
-
|
|
11413
|
-
|
|
11414
|
-
|
|
11415
|
-
|
|
11416
|
-
|
|
11417
|
-
|
|
11418
|
-
|
|
11419
|
-
|
|
11420
|
-
|
|
11421
|
-
|
|
11422
|
-
updatePlayButton(false);
|
|
11423
|
-
};
|
|
11424
|
-
const seek = async time => {
|
|
11425
|
-
const id = ++requestId;
|
|
11426
|
-
const result = await client.invoke('seek', time);
|
|
11427
|
-
if (!disposed && id === requestId) show(result);
|
|
11095
|
+
let previewsEnabled = enabled;
|
|
11096
|
+
const apply = result => {
|
|
11097
|
+
if (disposed) return;
|
|
11098
|
+
setDom(result.dom);
|
|
11099
|
+
if (result.frame) renderPreparedFrame(container.querySelector('.SessionReplaySurface'), result.frame);
|
|
11100
|
+
previewsEnabled = result.previewEnabled;
|
|
11101
|
+
preview ||= createTimelinePreview(container);
|
|
11102
|
+
preview.render(result.preview, result.previewEnabled);
|
|
11103
|
+
if (result.delay === undefined) {
|
|
11104
|
+
clearTimeout(timer);
|
|
11105
|
+
timer = undefined;
|
|
11106
|
+
} else if (timer === undefined) {
|
|
11107
|
+
timer = setTimeout(() => {
|
|
11108
|
+
timer = undefined;
|
|
11109
|
+
void send({
|
|
11110
|
+
now: performance.now(),
|
|
11111
|
+
type: 'tick'
|
|
11112
|
+
});
|
|
11113
|
+
}, result.delay);
|
|
11114
|
+
}
|
|
11428
11115
|
};
|
|
11429
11116
|
const report = error => {
|
|
11430
|
-
|
|
11117
|
+
if (disposed) return;
|
|
11118
|
+
clearTimeout(timer);
|
|
11119
|
+
timer = undefined;
|
|
11120
|
+
const status = container.querySelector(':scope > .SessionReplay > .SessionReplayControls > output') || document.createElement('output');
|
|
11121
|
+
if (!status.parentNode) container.append(status);
|
|
11431
11122
|
status.setAttribute('role', 'alert');
|
|
11432
11123
|
status.setAttribute('aria-live', 'assertive');
|
|
11433
11124
|
status.textContent = error instanceof Error ? error.message : String(error);
|
|
11434
11125
|
};
|
|
11435
|
-
|
|
11436
|
-
|
|
11437
|
-
|
|
11438
|
-
|
|
11439
|
-
|
|
11440
|
-
|
|
11441
|
-
if (!bounds.width || slider.disabled) return;
|
|
11442
|
-
pause();
|
|
11443
|
-
slider.focus();
|
|
11444
|
-
const fraction = Math.max(0, Math.min(1, (event.clientX - bounds.left) / bounds.width));
|
|
11445
|
-
void seek(fraction * duration).catch(report);
|
|
11446
|
-
};
|
|
11447
|
-
play.onclick = () => {
|
|
11448
|
-
if (playing) {
|
|
11449
|
-
pause();
|
|
11450
|
-
return;
|
|
11126
|
+
const send = async event => {
|
|
11127
|
+
if (disposed) return;
|
|
11128
|
+
try {
|
|
11129
|
+
apply(await client.invoke('SessionReplay.dispatch', 1, event, ++sequence));
|
|
11130
|
+
} catch (error) {
|
|
11131
|
+
report(error);
|
|
11451
11132
|
}
|
|
11452
|
-
playing = true;
|
|
11453
|
-
updatePlayButton(true);
|
|
11454
|
-
const origin = performance.now() - (position >= duration ? 0 : position);
|
|
11455
|
-
const tick = async () => {
|
|
11456
|
-
if (!playing || disposed) return;
|
|
11457
|
-
try {
|
|
11458
|
-
await seek(performance.now() - origin);
|
|
11459
|
-
if (position >= duration) pause();else if (playing) timer = setTimeout(() => {
|
|
11460
|
-
void tick();
|
|
11461
|
-
}, 50);
|
|
11462
|
-
} catch (error) {
|
|
11463
|
-
report(error);
|
|
11464
|
-
}
|
|
11465
|
-
};
|
|
11466
|
-
void tick();
|
|
11467
11133
|
};
|
|
11468
11134
|
try {
|
|
11469
|
-
|
|
11470
|
-
|
|
11471
|
-
show(initial);
|
|
11135
|
+
apply(await client.invoke('SessionReplay.create', 1, enabled, assets?.href));
|
|
11136
|
+
apply(await client.invoke('SessionReplay.loadContent', 1, source));
|
|
11472
11137
|
} catch (error) {
|
|
11473
|
-
play.disabled = slider.disabled = true;
|
|
11474
11138
|
report(error);
|
|
11475
11139
|
}
|
|
11140
|
+
if (!container.querySelector(':scope > .SessionReplay > .SessionReplayControls')) {
|
|
11141
|
+
client.dispose();
|
|
11142
|
+
return () => container.replaceChildren();
|
|
11143
|
+
}
|
|
11144
|
+
let previewTimer;
|
|
11145
|
+
let pendingPreview;
|
|
11146
|
+
let previewBusy = false;
|
|
11147
|
+
let previewGeneration = 0;
|
|
11148
|
+
const surface = container.querySelector('.SessionReplaySurface');
|
|
11149
|
+
const controls = container.querySelector(':scope > .SessionReplay > .SessionReplayControls');
|
|
11150
|
+
const slider = controls.querySelector('.SessionReplayPosition');
|
|
11151
|
+
const chart = controls.querySelector('.SessionReplayActivity');
|
|
11152
|
+
const play = controls.querySelector('.SessionReplayPlay');
|
|
11153
|
+
const checkbox = controls.querySelector('[type="checkbox"]');
|
|
11154
|
+
const seek = event => {
|
|
11155
|
+
clearTimeout(timer);
|
|
11156
|
+
timer = undefined;
|
|
11157
|
+
void send(event);
|
|
11158
|
+
};
|
|
11159
|
+
play.onclick = () => seek({
|
|
11160
|
+
now: performance.now(),
|
|
11161
|
+
type: 'togglePlay'
|
|
11162
|
+
});
|
|
11163
|
+
slider.oninput = () => seek({
|
|
11164
|
+
now: performance.now(),
|
|
11165
|
+
position: Number(slider.value),
|
|
11166
|
+
type: 'seek'
|
|
11167
|
+
});
|
|
11168
|
+
const point = event => {
|
|
11169
|
+
const target = event.currentTarget;
|
|
11170
|
+
const bounds = target.getBoundingClientRect();
|
|
11171
|
+
return {
|
|
11172
|
+
left: bounds.left,
|
|
11173
|
+
pointerId: event.pointerId,
|
|
11174
|
+
pointerType: event.pointerType,
|
|
11175
|
+
slider: target === slider,
|
|
11176
|
+
top: chart.getBoundingClientRect().top,
|
|
11177
|
+
width: bounds.width,
|
|
11178
|
+
windowWidth: view.innerWidth,
|
|
11179
|
+
x: event.clientX
|
|
11180
|
+
};
|
|
11181
|
+
};
|
|
11182
|
+
chart.onpointerdown = event => {
|
|
11183
|
+
if (event.button !== 0 || !event.isPrimary || slider.disabled) return;
|
|
11184
|
+
event.preventDefault();
|
|
11185
|
+
chart.setPointerCapture(event.pointerId);
|
|
11186
|
+
slider.focus();
|
|
11187
|
+
seek({
|
|
11188
|
+
now: performance.now(),
|
|
11189
|
+
point: point(event),
|
|
11190
|
+
type: 'pointerDown'
|
|
11191
|
+
});
|
|
11192
|
+
};
|
|
11193
|
+
chart.onpointermove = event => {
|
|
11194
|
+
if (chart.hasPointerCapture(event.pointerId)) seek({
|
|
11195
|
+
now: performance.now(),
|
|
11196
|
+
point: point(event),
|
|
11197
|
+
type: 'pointerMove'
|
|
11198
|
+
});
|
|
11199
|
+
};
|
|
11200
|
+
const endDrag = event => {
|
|
11201
|
+
if (chart.hasPointerCapture(event.pointerId)) chart.releasePointerCapture(event.pointerId);
|
|
11202
|
+
void send({
|
|
11203
|
+
pointerId: event.pointerId,
|
|
11204
|
+
type: 'pointerUp'
|
|
11205
|
+
});
|
|
11206
|
+
};
|
|
11207
|
+
chart.onpointerup = endDrag;
|
|
11208
|
+
chart.onpointercancel = endDrag;
|
|
11209
|
+
chart.onlostpointercapture = endDrag;
|
|
11210
|
+
const updatePreview = async () => {
|
|
11211
|
+
previewTimer = undefined;
|
|
11212
|
+
if (!pendingPreview || previewBusy || disposed) return;
|
|
11213
|
+
const event = {
|
|
11214
|
+
point: pendingPreview,
|
|
11215
|
+
type: 'preview'
|
|
11216
|
+
};
|
|
11217
|
+
pendingPreview = undefined;
|
|
11218
|
+
previewBusy = true;
|
|
11219
|
+
const generation = previewGeneration;
|
|
11220
|
+
const id = ++sequence;
|
|
11221
|
+
try {
|
|
11222
|
+
const result = await client.invoke('SessionReplay.dispatch', 1, event, id);
|
|
11223
|
+
if (!disposed && generation === previewGeneration) apply(result);
|
|
11224
|
+
} catch {
|
|
11225
|
+
/* Preview errors must not interrupt playback. */
|
|
11226
|
+
} finally {
|
|
11227
|
+
previewBusy = false;
|
|
11228
|
+
if (pendingPreview && !disposed) previewTimer = setTimeout(() => void updatePreview(), 60);
|
|
11229
|
+
}
|
|
11230
|
+
};
|
|
11231
|
+
const hover = event => {
|
|
11232
|
+
if (!previewsEnabled || slider.disabled || event.pointerType === 'touch') return;
|
|
11233
|
+
previewGeneration++;
|
|
11234
|
+
pendingPreview = point(event);
|
|
11235
|
+
if (!previewBusy && previewTimer === undefined) previewTimer = setTimeout(() => void updatePreview(), 60);
|
|
11236
|
+
};
|
|
11237
|
+
const hide = () => {
|
|
11238
|
+
previewGeneration++;
|
|
11239
|
+
pendingPreview = undefined;
|
|
11240
|
+
clearTimeout(previewTimer);
|
|
11241
|
+
previewTimer = undefined;
|
|
11242
|
+
void send({
|
|
11243
|
+
type: 'hidePreview'
|
|
11244
|
+
});
|
|
11245
|
+
};
|
|
11246
|
+
for (const target of [slider, chart]) {
|
|
11247
|
+
target.addEventListener('pointermove', hover);
|
|
11248
|
+
target.addEventListener('pointerleave', hide);
|
|
11249
|
+
target.addEventListener('pointercancel', hide);
|
|
11250
|
+
}
|
|
11251
|
+
const keydown = event => {
|
|
11252
|
+
if (event.key === 'Escape') hide();
|
|
11253
|
+
};
|
|
11254
|
+
view.addEventListener('resize', hide);
|
|
11255
|
+
view.addEventListener('blur', hide);
|
|
11256
|
+
document.addEventListener('keydown', keydown);
|
|
11257
|
+
checkbox.onchange = () => {
|
|
11258
|
+
hide();
|
|
11259
|
+
const enabled = checkbox.checked;
|
|
11260
|
+
void send({
|
|
11261
|
+
enabled,
|
|
11262
|
+
type: 'setPreviewEnabled'
|
|
11263
|
+
});
|
|
11264
|
+
try {
|
|
11265
|
+
view.localStorage.setItem(settingKey, String(enabled));
|
|
11266
|
+
} catch {
|
|
11267
|
+
/* The setting still works without storage. */
|
|
11268
|
+
}
|
|
11269
|
+
};
|
|
11476
11270
|
return () => {
|
|
11477
11271
|
disposed = true;
|
|
11478
|
-
|
|
11272
|
+
clearTimeout(timer);
|
|
11273
|
+
clearTimeout(previewTimer);
|
|
11274
|
+
pendingPreview = undefined;
|
|
11275
|
+
view.removeEventListener('resize', hide);
|
|
11276
|
+
view.removeEventListener('blur', hide);
|
|
11277
|
+
document.removeEventListener('keydown', keydown);
|
|
11278
|
+
preview?.dispose();
|
|
11479
11279
|
client.dispose();
|
|
11480
11280
|
disposeFrame(surface);
|
|
11481
11281
|
container.replaceChildren();
|