noibu-react-native 0.2.12 → 0.2.13
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/CHANGELOG.md +21 -0
- package/README.md +69 -0
- package/dist/api/ClientConfig.d.ts +3 -0
- package/dist/api/ClientConfig.js +3 -0
- package/dist/api/MetroplexSocket.d.ts +1 -1
- package/dist/api/MetroplexSocket.js +11 -5
- package/dist/constants.js +1 -1
- package/dist/mobileTransformer/mobile-replay/index.d.ts +10 -0
- package/dist/mobileTransformer/mobile-replay/index.js +57 -0
- package/dist/mobileTransformer/mobile-replay/mobile.types.d.ts +312 -0
- package/dist/mobileTransformer/mobile-replay/mobile.types.js +11 -0
- package/dist/mobileTransformer/mobile-replay/rrweb.d.ts +573 -0
- package/dist/mobileTransformer/mobile-replay/rrweb.js +39 -0
- package/dist/mobileTransformer/mobile-replay/schema/mobile/rr-mobile-schema.json.js +1559 -0
- package/dist/mobileTransformer/mobile-replay/schema/web/rr-web-schema.json.js +1183 -0
- package/dist/mobileTransformer/mobile-replay/transformer/colors.d.ts +1 -0
- package/dist/mobileTransformer/mobile-replay/transformer/colors.js +43 -0
- package/dist/mobileTransformer/mobile-replay/transformer/screen-chrome.d.ts +13 -0
- package/dist/mobileTransformer/mobile-replay/transformer/screen-chrome.js +142 -0
- package/dist/mobileTransformer/mobile-replay/transformer/transformers.d.ts +59 -0
- package/dist/mobileTransformer/mobile-replay/transformer/transformers.js +1160 -0
- package/dist/mobileTransformer/mobile-replay/transformer/types.d.ts +40 -0
- package/dist/mobileTransformer/mobile-replay/transformer/wireframeStyle.d.ts +16 -0
- package/dist/mobileTransformer/mobile-replay/transformer/wireframeStyle.js +197 -0
- package/dist/mobileTransformer/utils.d.ts +1 -0
- package/dist/mobileTransformer/utils.js +5 -0
- package/dist/monitors/http-tools/GqlErrorValidator.js +4 -3
- package/dist/sessionRecorder/SessionRecorder.js +7 -4
- package/dist/sessionRecorder/nativeSessionRecorderSubscription.js +23 -3
- package/dist/sessionRecorder/types.d.ts +1 -1
- package/dist/utils/piiRedactor.js +15 -2
- package/dist/utils/piiRedactor.test.d.ts +1 -0
- package/ios/IOSPocEmitter.h +7 -0
- package/ios/IOSPocEmitter.m +33 -0
- package/noibu-react-native.podspec +50 -0
- package/package.json +17 -4
- package/android/.gitignore +0 -13
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
import { IncrementalSource, EventType } from '../rrweb.js';
|
|
2
|
+
import { isObject } from '../../utils.js';
|
|
3
|
+
import { NodeType } from '../mobile.types.js';
|
|
4
|
+
import { makeOpenKeyboardPlaceholder, makeStatusBar, makeNavigationBar } from './screen-chrome.js';
|
|
5
|
+
import { makeHTMLStyles, makeBodyStyles, makeStylesString, asStyleString, makeMinimalStyles, makeDeterminateProgressStyles, makeIndeterminateProgressStyles, makePositionStyles, makeColorStyles } from './wireframeStyle.js';
|
|
6
|
+
import { noibuLog } from '../../../utils/log.js';
|
|
7
|
+
|
|
8
|
+
const BACKGROUND = '#f3f4ef';
|
|
9
|
+
const FOREGROUND = '#35373e';
|
|
10
|
+
/**
|
|
11
|
+
* generates a sequence of ids
|
|
12
|
+
* from 100 to 9,999,999
|
|
13
|
+
* the transformer reserves ids in the range 0 to 9,999,999
|
|
14
|
+
* we reserve a range of ids because we need nodes to have stable ids across snapshots
|
|
15
|
+
* in order for incremental snapshots to work
|
|
16
|
+
* some mobile elements have to be wrapped in other elements in order to be styled correctly
|
|
17
|
+
* which means the web version of a mobile replay will use ids that don't exist in the mobile replay,
|
|
18
|
+
* and we need to ensure they don't clash
|
|
19
|
+
* -----
|
|
20
|
+
* id is typed as a number in rrweb
|
|
21
|
+
* and there's a few places in their code where rrweb uses a check for `id === -1` to bail out of processing
|
|
22
|
+
* so, it's safest to assume that id is expected to be a positive integer
|
|
23
|
+
*/
|
|
24
|
+
function* ids() {
|
|
25
|
+
let i = 100;
|
|
26
|
+
while (i < 9999999) {
|
|
27
|
+
yield i++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let globalIdSequence = ids();
|
|
31
|
+
// there are some fixed ids that we need to use for fixed elements or artificial mutations
|
|
32
|
+
const DOCUMENT_ID = 1;
|
|
33
|
+
const HTML_DOC_TYPE_ID = 2;
|
|
34
|
+
const HTML_ELEMENT_ID = 3;
|
|
35
|
+
const HEAD_ID = 4;
|
|
36
|
+
const BODY_ID = 5;
|
|
37
|
+
// the nav bar should always be the last item in the body so that it is at the top of the stack
|
|
38
|
+
const NAVIGATION_BAR_PARENT_ID = 7;
|
|
39
|
+
const NAVIGATION_BAR_ID = 8;
|
|
40
|
+
// the keyboard so that it is still before the nav bar
|
|
41
|
+
const KEYBOARD_PARENT_ID = 9;
|
|
42
|
+
const KEYBOARD_ID = 10;
|
|
43
|
+
const STATUS_BAR_PARENT_ID = 11;
|
|
44
|
+
const STATUS_BAR_ID = 12;
|
|
45
|
+
function isKeyboardEvent(x) {
|
|
46
|
+
return (isObject(x) &&
|
|
47
|
+
'data' in x &&
|
|
48
|
+
isObject(x.data) &&
|
|
49
|
+
'tag' in x.data &&
|
|
50
|
+
x.data.tag === 'keyboard');
|
|
51
|
+
}
|
|
52
|
+
function _isPositiveInteger(id) {
|
|
53
|
+
return typeof id === 'number' && id > 0 && id % 1 === 0;
|
|
54
|
+
}
|
|
55
|
+
function _isNullish(x) {
|
|
56
|
+
return x === null || x === undefined;
|
|
57
|
+
}
|
|
58
|
+
function isRemovedNodeMutation(x) {
|
|
59
|
+
return isObject(x) && 'id' in x;
|
|
60
|
+
}
|
|
61
|
+
const makeCustomEvent = (mobileCustomEvent) => {
|
|
62
|
+
if (isKeyboardEvent(mobileCustomEvent)) {
|
|
63
|
+
// keyboard events are handled as incremental snapshots to add or remove a keyboard from the DOM
|
|
64
|
+
// TODO eventually we can pass something to makeIncrementalEvent here
|
|
65
|
+
const adds = [];
|
|
66
|
+
const removes = [];
|
|
67
|
+
if (mobileCustomEvent.data.payload.open) {
|
|
68
|
+
const keyboardPlaceHolder = makeOpenKeyboardPlaceholder(mobileCustomEvent, {
|
|
69
|
+
timestamp: mobileCustomEvent.timestamp,
|
|
70
|
+
idSequence: globalIdSequence,
|
|
71
|
+
});
|
|
72
|
+
if (keyboardPlaceHolder) {
|
|
73
|
+
adds.push({
|
|
74
|
+
parentId: KEYBOARD_PARENT_ID,
|
|
75
|
+
nextId: null,
|
|
76
|
+
node: keyboardPlaceHolder.result,
|
|
77
|
+
});
|
|
78
|
+
// mutations seem not to want a tree of nodes to add
|
|
79
|
+
// so even though `keyboardPlaceholder` is a tree with content
|
|
80
|
+
// we have to add the text content as well
|
|
81
|
+
adds.push({
|
|
82
|
+
parentId: keyboardPlaceHolder.result.id,
|
|
83
|
+
nextId: null,
|
|
84
|
+
node: {
|
|
85
|
+
type: NodeType.Text,
|
|
86
|
+
id: globalIdSequence.next().value,
|
|
87
|
+
textContent: 'keyboard',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
//captureMessage('Failed to create keyboard placeholder', { extra: { mobileCustomEvent } })
|
|
93
|
+
console.error('Failed to create keyboard placeholder', mobileCustomEvent);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
removes.push(
|
|
98
|
+
//@ts-ignore
|
|
99
|
+
{
|
|
100
|
+
parentId: KEYBOARD_PARENT_ID,
|
|
101
|
+
id: KEYBOARD_ID,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const mutation = {
|
|
105
|
+
adds,
|
|
106
|
+
attributes: [],
|
|
107
|
+
removes,
|
|
108
|
+
source: IncrementalSource.Mutation,
|
|
109
|
+
texts: [],
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
type: EventType.IncrementalSnapshot,
|
|
113
|
+
data: mutation,
|
|
114
|
+
timestamp: mobileCustomEvent.timestamp,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return mobileCustomEvent;
|
|
118
|
+
};
|
|
119
|
+
const makeMetaEvent = (mobileMetaEvent) => ({
|
|
120
|
+
type: EventType.Meta,
|
|
121
|
+
data: {
|
|
122
|
+
href: mobileMetaEvent.data.href || '', // the replay doesn't use the href, so we safely ignore any absence
|
|
123
|
+
// mostly we need width and height in order to size the viewport
|
|
124
|
+
width: mobileMetaEvent.data.width,
|
|
125
|
+
height: mobileMetaEvent.data.height,
|
|
126
|
+
},
|
|
127
|
+
timestamp: mobileMetaEvent.timestamp,
|
|
128
|
+
});
|
|
129
|
+
function makeDivElement(wireframe, children, context) {
|
|
130
|
+
const _id = _isPositiveInteger(wireframe.id)
|
|
131
|
+
? wireframe.id
|
|
132
|
+
: context.idSequence.next().value;
|
|
133
|
+
return {
|
|
134
|
+
result: {
|
|
135
|
+
type: NodeType.Element,
|
|
136
|
+
tagName: 'div',
|
|
137
|
+
attributes: {
|
|
138
|
+
style: asStyleString([
|
|
139
|
+
makeStylesString(wireframe),
|
|
140
|
+
'overflow:hidden',
|
|
141
|
+
'white-space:nowrap',
|
|
142
|
+
]),
|
|
143
|
+
'data-rrweb-id': _id,
|
|
144
|
+
},
|
|
145
|
+
id: _id,
|
|
146
|
+
childNodes: children,
|
|
147
|
+
},
|
|
148
|
+
context,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function makeTextElement(wireframe, children, context) {
|
|
152
|
+
if (wireframe.type !== 'text') {
|
|
153
|
+
console.error('Passed incorrect wireframe type to makeTextElement');
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
// because we might have to style the text, we always wrap it in a div
|
|
157
|
+
// and apply styles to that
|
|
158
|
+
const id = context.idSequence.next().value;
|
|
159
|
+
const childNodes = [...children];
|
|
160
|
+
if (!_isNullish(wireframe.text)) {
|
|
161
|
+
childNodes.unshift({
|
|
162
|
+
type: NodeType.Text,
|
|
163
|
+
textContent: wireframe.text,
|
|
164
|
+
// since the text node is wrapped, we assign it a synthetic id
|
|
165
|
+
id,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
result: {
|
|
170
|
+
type: NodeType.Element,
|
|
171
|
+
tagName: 'div',
|
|
172
|
+
attributes: {
|
|
173
|
+
style: asStyleString([
|
|
174
|
+
makeStylesString(wireframe),
|
|
175
|
+
'overflow:hidden',
|
|
176
|
+
'white-space:normal',
|
|
177
|
+
]),
|
|
178
|
+
'data-rrweb-id': wireframe.id,
|
|
179
|
+
},
|
|
180
|
+
id: wireframe.id,
|
|
181
|
+
childNodes,
|
|
182
|
+
},
|
|
183
|
+
context,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function makeWebViewElement(wireframe, children, context) {
|
|
187
|
+
const labelledWireframe = Object.assign({}, wireframe);
|
|
188
|
+
if ('url' in wireframe) {
|
|
189
|
+
labelledWireframe.label = wireframe.url;
|
|
190
|
+
}
|
|
191
|
+
return makePlaceholderElement(labelledWireframe, children, context);
|
|
192
|
+
}
|
|
193
|
+
function makePlaceholderElement(wireframe, children, context) {
|
|
194
|
+
var _a, _b;
|
|
195
|
+
const txt = 'label' in wireframe && wireframe.label
|
|
196
|
+
? wireframe.label
|
|
197
|
+
: wireframe.type || 'PLACEHOLDER';
|
|
198
|
+
return {
|
|
199
|
+
result: {
|
|
200
|
+
type: NodeType.Element,
|
|
201
|
+
tagName: 'div',
|
|
202
|
+
attributes: {
|
|
203
|
+
style: makeStylesString(wireframe, Object.assign({ verticalAlign: 'center', horizontalAlign: 'center', backgroundColor: ((_a = wireframe.style) === null || _a === void 0 ? void 0 : _a.backgroundColor) || BACKGROUND, color: ((_b = wireframe.style) === null || _b === void 0 ? void 0 : _b.color) || FOREGROUND,
|
|
204
|
+
//backgroundImage: PLACEHOLDER_SVG_DATA_IMAGE_URL,
|
|
205
|
+
backgroundSize: 'auto', backgroundRepeat: 'unset' }, context.styleOverride)),
|
|
206
|
+
'data-rrweb-id': wireframe.id,
|
|
207
|
+
},
|
|
208
|
+
id: wireframe.id,
|
|
209
|
+
childNodes: [
|
|
210
|
+
{
|
|
211
|
+
type: NodeType.Text,
|
|
212
|
+
// since the text node is wrapped, we assign it a synthetic id
|
|
213
|
+
id: context.idSequence.next().value,
|
|
214
|
+
textContent: txt,
|
|
215
|
+
},
|
|
216
|
+
...children,
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
context,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function dataURIOrPNG(src) {
|
|
223
|
+
// replace all new lines in src
|
|
224
|
+
src = src.replace(/\r?\n|\r/g, '');
|
|
225
|
+
// if (!src.startsWith('data:image/')) {
|
|
226
|
+
// return 'data:image/png;base64,' + src;
|
|
227
|
+
// }
|
|
228
|
+
noibuLog("this is the url: ", src);
|
|
229
|
+
return src;
|
|
230
|
+
}
|
|
231
|
+
function makeImageElement(wireframe, children, context) {
|
|
232
|
+
if (!wireframe.base64) {
|
|
233
|
+
return makePlaceholderElement(wireframe, children, context);
|
|
234
|
+
}
|
|
235
|
+
const src = dataURIOrPNG(wireframe.base64);
|
|
236
|
+
return {
|
|
237
|
+
result: {
|
|
238
|
+
type: NodeType.Element,
|
|
239
|
+
tagName: 'img',
|
|
240
|
+
attributes: {
|
|
241
|
+
src: src,
|
|
242
|
+
width: wireframe.width,
|
|
243
|
+
height: wireframe.height,
|
|
244
|
+
style: makeStylesString(wireframe),
|
|
245
|
+
'data-rrweb-id': wireframe.id,
|
|
246
|
+
},
|
|
247
|
+
id: wireframe.id,
|
|
248
|
+
childNodes: children,
|
|
249
|
+
},
|
|
250
|
+
context,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function inputAttributes(wireframe) {
|
|
254
|
+
const attributes = Object.assign(Object.assign({ style: makeStylesString(wireframe), type: wireframe.inputType }, (wireframe.disabled ? { disabled: wireframe.disabled } : {})), { 'data-rrweb-id': wireframe.id });
|
|
255
|
+
switch (wireframe.inputType) {
|
|
256
|
+
case 'checkbox':
|
|
257
|
+
return Object.assign(Object.assign(Object.assign({}, attributes), { style: null }), (wireframe.checked ? { checked: wireframe.checked } : {}));
|
|
258
|
+
case 'toggle':
|
|
259
|
+
return Object.assign(Object.assign(Object.assign({}, attributes), { style: null }), (wireframe.checked ? { checked: wireframe.checked } : {}));
|
|
260
|
+
case 'radio':
|
|
261
|
+
return Object.assign(Object.assign(Object.assign({}, attributes), { style: null }), (wireframe.checked ? { checked: wireframe.checked } : {}));
|
|
262
|
+
case 'button':
|
|
263
|
+
return Object.assign({}, attributes);
|
|
264
|
+
case 'text_area':
|
|
265
|
+
return Object.assign(Object.assign({}, attributes), { value: wireframe.value || '' });
|
|
266
|
+
case 'progress':
|
|
267
|
+
return Object.assign(Object.assign({}, attributes), {
|
|
268
|
+
// indeterminate when omitted
|
|
269
|
+
value: wireframe.value || null,
|
|
270
|
+
// defaults to 1 when omitted
|
|
271
|
+
max: wireframe.max || null, type: null });
|
|
272
|
+
default:
|
|
273
|
+
return Object.assign(Object.assign({}, attributes), { value: wireframe.value || '' });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function makeButtonElement(wireframe, children, context) {
|
|
277
|
+
const buttonText = wireframe.value
|
|
278
|
+
? {
|
|
279
|
+
type: NodeType.Text,
|
|
280
|
+
textContent: wireframe.value,
|
|
281
|
+
}
|
|
282
|
+
: null;
|
|
283
|
+
return {
|
|
284
|
+
result: {
|
|
285
|
+
type: NodeType.Element,
|
|
286
|
+
tagName: 'button',
|
|
287
|
+
attributes: inputAttributes(wireframe),
|
|
288
|
+
id: wireframe.id,
|
|
289
|
+
childNodes: buttonText
|
|
290
|
+
? [
|
|
291
|
+
Object.assign(Object.assign({}, buttonText), { id: context.idSequence.next().value }),
|
|
292
|
+
...children,
|
|
293
|
+
]
|
|
294
|
+
: children,
|
|
295
|
+
},
|
|
296
|
+
context,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function makeSelectOptionElement(option, selected, context) {
|
|
300
|
+
const optionId = context.idSequence.next().value;
|
|
301
|
+
return {
|
|
302
|
+
result: {
|
|
303
|
+
type: NodeType.Element,
|
|
304
|
+
tagName: 'option',
|
|
305
|
+
attributes: Object.assign(Object.assign({}, (selected ? { selected: selected } : {})), { 'data-rrweb-id': optionId }),
|
|
306
|
+
id: optionId,
|
|
307
|
+
childNodes: [
|
|
308
|
+
{
|
|
309
|
+
type: NodeType.Text,
|
|
310
|
+
textContent: option,
|
|
311
|
+
id: context.idSequence.next().value,
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
},
|
|
315
|
+
context,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function makeSelectElement(wireframe, children, context) {
|
|
319
|
+
const selectOptions = [];
|
|
320
|
+
if (wireframe.options) {
|
|
321
|
+
let optionContext = context;
|
|
322
|
+
for (let i = 0; i < wireframe.options.length; i++) {
|
|
323
|
+
const option = wireframe.options[i];
|
|
324
|
+
const conversion = makeSelectOptionElement(option, wireframe.value === option, optionContext);
|
|
325
|
+
selectOptions.push(conversion.result);
|
|
326
|
+
optionContext = conversion.context;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
result: {
|
|
331
|
+
type: NodeType.Element,
|
|
332
|
+
tagName: 'select',
|
|
333
|
+
attributes: inputAttributes(wireframe),
|
|
334
|
+
id: wireframe.id,
|
|
335
|
+
childNodes: [...selectOptions, ...children],
|
|
336
|
+
},
|
|
337
|
+
context,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function groupRadioButtons(children, radioGroupName) {
|
|
341
|
+
return children.map(child => {
|
|
342
|
+
if (child.type === NodeType.Element &&
|
|
343
|
+
child.tagName === 'input' &&
|
|
344
|
+
child.attributes.type === 'radio') {
|
|
345
|
+
return Object.assign(Object.assign({}, child), { attributes: Object.assign(Object.assign({}, child.attributes), { name: radioGroupName, 'data-rrweb-id': child.id }) });
|
|
346
|
+
}
|
|
347
|
+
return child;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function makeRadioGroupElement(wireframe, children, context) {
|
|
351
|
+
const radioGroupName = 'radio_group_' + wireframe.id;
|
|
352
|
+
return {
|
|
353
|
+
result: {
|
|
354
|
+
type: NodeType.Element,
|
|
355
|
+
tagName: 'div',
|
|
356
|
+
attributes: {
|
|
357
|
+
style: makeStylesString(wireframe),
|
|
358
|
+
'data-rrweb-id': wireframe.id,
|
|
359
|
+
},
|
|
360
|
+
id: wireframe.id,
|
|
361
|
+
childNodes: groupRadioButtons(children, radioGroupName),
|
|
362
|
+
},
|
|
363
|
+
context,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function makeStar(title, path, context) {
|
|
367
|
+
const svgId = context.idSequence.next().value;
|
|
368
|
+
const titleId = context.idSequence.next().value;
|
|
369
|
+
const pathId = context.idSequence.next().value;
|
|
370
|
+
return {
|
|
371
|
+
type: NodeType.Element,
|
|
372
|
+
tagName: 'svg',
|
|
373
|
+
isSVG: true,
|
|
374
|
+
attributes: {
|
|
375
|
+
style: asStyleString([
|
|
376
|
+
'height: 100%',
|
|
377
|
+
'overflow-clip-margin: content-box',
|
|
378
|
+
'overflow:hidden',
|
|
379
|
+
]),
|
|
380
|
+
viewBox: '0 0 24 24',
|
|
381
|
+
fill: 'currentColor',
|
|
382
|
+
'data-rrweb-id': svgId,
|
|
383
|
+
},
|
|
384
|
+
id: svgId,
|
|
385
|
+
childNodes: [
|
|
386
|
+
{
|
|
387
|
+
type: NodeType.Element,
|
|
388
|
+
tagName: 'title',
|
|
389
|
+
isSVG: true,
|
|
390
|
+
attributes: {
|
|
391
|
+
'data-rrweb-id': titleId,
|
|
392
|
+
},
|
|
393
|
+
id: titleId,
|
|
394
|
+
childNodes: [
|
|
395
|
+
{
|
|
396
|
+
type: NodeType.Text,
|
|
397
|
+
textContent: title,
|
|
398
|
+
id: context.idSequence.next().value,
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
type: NodeType.Element,
|
|
404
|
+
tagName: 'path',
|
|
405
|
+
isSVG: true,
|
|
406
|
+
attributes: {
|
|
407
|
+
d: path,
|
|
408
|
+
'data-rrweb-id': pathId,
|
|
409
|
+
},
|
|
410
|
+
id: pathId,
|
|
411
|
+
childNodes: [],
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function filledStar(context) {
|
|
417
|
+
return makeStar('filled star', 'M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z', context);
|
|
418
|
+
}
|
|
419
|
+
function halfStar(context) {
|
|
420
|
+
return makeStar('half-filled star', 'M12,15.4V6.1L13.71,10.13L18.09,10.5L14.77,13.39L15.76,17.67M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z', context);
|
|
421
|
+
}
|
|
422
|
+
function emptyStar(context) {
|
|
423
|
+
return makeStar('empty star', 'M12,15.39L8.24,17.66L9.23,13.38L5.91,10.5L10.29,10.13L12,6.09L13.71,10.13L18.09,10.5L14.77,13.38L15.76,17.66M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z', context);
|
|
424
|
+
}
|
|
425
|
+
function makeRatingBar(wireframe, children, context) {
|
|
426
|
+
// max is the number of stars... and value is the number of stars to fill
|
|
427
|
+
// deliberate double equals, because we want to allow null and undefined
|
|
428
|
+
if (wireframe.value == null || wireframe.max == null) {
|
|
429
|
+
return makePlaceholderElement(wireframe, children, context);
|
|
430
|
+
}
|
|
431
|
+
const numberOfFilledStars = Math.floor(wireframe.value);
|
|
432
|
+
const numberOfHalfStars = wireframe.value - numberOfFilledStars > 0 ? 1 : 0;
|
|
433
|
+
const numberOfEmptyStars = wireframe.max - numberOfFilledStars - numberOfHalfStars;
|
|
434
|
+
const filledStars = Array(numberOfFilledStars)
|
|
435
|
+
.fill(undefined)
|
|
436
|
+
.map(() => filledStar(context));
|
|
437
|
+
const halfStars = Array(numberOfHalfStars)
|
|
438
|
+
.fill(undefined)
|
|
439
|
+
.map(() => halfStar(context));
|
|
440
|
+
const emptyStars = Array(numberOfEmptyStars)
|
|
441
|
+
.fill(undefined)
|
|
442
|
+
.map(() => emptyStar(context));
|
|
443
|
+
const ratingBarId = context.idSequence.next().value;
|
|
444
|
+
const ratingBar = {
|
|
445
|
+
type: NodeType.Element,
|
|
446
|
+
tagName: 'div',
|
|
447
|
+
id: ratingBarId,
|
|
448
|
+
attributes: {
|
|
449
|
+
style: asStyleString([
|
|
450
|
+
makeColorStyles(wireframe),
|
|
451
|
+
'position: relative',
|
|
452
|
+
'display: flex',
|
|
453
|
+
'flex-direction: row',
|
|
454
|
+
'padding: 2px 4px',
|
|
455
|
+
]),
|
|
456
|
+
'data-rrweb-id': ratingBarId,
|
|
457
|
+
},
|
|
458
|
+
childNodes: [...filledStars, ...halfStars, ...emptyStars],
|
|
459
|
+
};
|
|
460
|
+
return {
|
|
461
|
+
result: {
|
|
462
|
+
type: NodeType.Element,
|
|
463
|
+
tagName: 'div',
|
|
464
|
+
attributes: {
|
|
465
|
+
style: makeStylesString(wireframe),
|
|
466
|
+
'data-rrweb-id': wireframe.id,
|
|
467
|
+
},
|
|
468
|
+
id: wireframe.id,
|
|
469
|
+
childNodes: [ratingBar, ...children],
|
|
470
|
+
},
|
|
471
|
+
context,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function makeProgressElement(wireframe, children, context) {
|
|
475
|
+
var _a, _b, _c, _d;
|
|
476
|
+
if (((_a = wireframe.style) === null || _a === void 0 ? void 0 : _a.bar) === 'circular') {
|
|
477
|
+
// value needs to be expressed as a number between 0 and 100
|
|
478
|
+
const max = wireframe.max || 1;
|
|
479
|
+
let value = wireframe.value || null;
|
|
480
|
+
if (_isPositiveInteger(value) && value <= max) {
|
|
481
|
+
value = (value / max) * 100;
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
value = null;
|
|
485
|
+
}
|
|
486
|
+
const styleOverride = {
|
|
487
|
+
color: ((_b = wireframe.style) === null || _b === void 0 ? void 0 : _b.color) || FOREGROUND,
|
|
488
|
+
backgroundColor: ((_c = wireframe.style) === null || _c === void 0 ? void 0 : _c.backgroundColor) || BACKGROUND,
|
|
489
|
+
};
|
|
490
|
+
// if not _isPositiveInteger(value) then we render a spinner,
|
|
491
|
+
// so we need to add a style element with the spin keyframe
|
|
492
|
+
const stylingChildren = _isPositiveInteger(value)
|
|
493
|
+
? []
|
|
494
|
+
: [
|
|
495
|
+
{
|
|
496
|
+
type: NodeType.Element,
|
|
497
|
+
tagName: 'style',
|
|
498
|
+
attributes: {
|
|
499
|
+
type: 'text/css',
|
|
500
|
+
},
|
|
501
|
+
id: context.idSequence.next().value,
|
|
502
|
+
childNodes: [
|
|
503
|
+
{
|
|
504
|
+
type: NodeType.Text,
|
|
505
|
+
textContent: `@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }`,
|
|
506
|
+
id: context.idSequence.next().value,
|
|
507
|
+
},
|
|
508
|
+
],
|
|
509
|
+
},
|
|
510
|
+
];
|
|
511
|
+
const wrappingDivId = context.idSequence.next().value;
|
|
512
|
+
return {
|
|
513
|
+
result: {
|
|
514
|
+
type: NodeType.Element,
|
|
515
|
+
tagName: 'div',
|
|
516
|
+
attributes: {
|
|
517
|
+
style: makeMinimalStyles(wireframe),
|
|
518
|
+
'data-rrweb-id': wireframe.id,
|
|
519
|
+
},
|
|
520
|
+
id: wireframe.id,
|
|
521
|
+
childNodes: [
|
|
522
|
+
{
|
|
523
|
+
type: NodeType.Element,
|
|
524
|
+
tagName: 'div',
|
|
525
|
+
attributes: {
|
|
526
|
+
// with no provided value we render a spinner
|
|
527
|
+
style: _isPositiveInteger(value)
|
|
528
|
+
? makeDeterminateProgressStyles(wireframe, styleOverride)
|
|
529
|
+
: makeIndeterminateProgressStyles(wireframe, styleOverride),
|
|
530
|
+
'data-rrweb-id': wrappingDivId,
|
|
531
|
+
},
|
|
532
|
+
id: wrappingDivId,
|
|
533
|
+
childNodes: stylingChildren,
|
|
534
|
+
},
|
|
535
|
+
...children,
|
|
536
|
+
],
|
|
537
|
+
},
|
|
538
|
+
context,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
else if (((_d = wireframe.style) === null || _d === void 0 ? void 0 : _d.bar) === 'rating') {
|
|
542
|
+
return makeRatingBar(wireframe, children, context);
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
result: {
|
|
546
|
+
type: NodeType.Element,
|
|
547
|
+
tagName: 'progress',
|
|
548
|
+
attributes: inputAttributes(wireframe),
|
|
549
|
+
id: wireframe.id,
|
|
550
|
+
childNodes: children,
|
|
551
|
+
},
|
|
552
|
+
context,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function makeToggleParts(wireframe, context) {
|
|
556
|
+
var _a, _b, _c, _d;
|
|
557
|
+
const togglePosition = wireframe.checked ? 'right' : 'left';
|
|
558
|
+
const defaultColor = wireframe.checked ? '#1d4aff' : BACKGROUND;
|
|
559
|
+
const sliderPartId = context.idSequence.next().value;
|
|
560
|
+
const handlePartId = context.idSequence.next().value;
|
|
561
|
+
return [
|
|
562
|
+
{
|
|
563
|
+
type: NodeType.Element,
|
|
564
|
+
tagName: 'div',
|
|
565
|
+
attributes: {
|
|
566
|
+
'data-toggle-part': 'slider',
|
|
567
|
+
style: asStyleString([
|
|
568
|
+
'position:absolute',
|
|
569
|
+
'top:33%',
|
|
570
|
+
'left:5%',
|
|
571
|
+
'display:inline-block',
|
|
572
|
+
'width:75%',
|
|
573
|
+
'height:33%',
|
|
574
|
+
'opacity: 0.2',
|
|
575
|
+
'border-radius:7.5%',
|
|
576
|
+
`background-color:${((_a = wireframe.style) === null || _a === void 0 ? void 0 : _a.color) || defaultColor}`,
|
|
577
|
+
]),
|
|
578
|
+
'data-rrweb-id': sliderPartId,
|
|
579
|
+
},
|
|
580
|
+
id: sliderPartId,
|
|
581
|
+
childNodes: [],
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
type: NodeType.Element,
|
|
585
|
+
tagName: 'div',
|
|
586
|
+
attributes: {
|
|
587
|
+
'data-toggle-part': 'handle',
|
|
588
|
+
style: asStyleString([
|
|
589
|
+
'position:absolute',
|
|
590
|
+
'top:1.5%',
|
|
591
|
+
`${togglePosition}:5%`,
|
|
592
|
+
'display:flex',
|
|
593
|
+
'align-items:center',
|
|
594
|
+
'justify-content:center',
|
|
595
|
+
'width:40%',
|
|
596
|
+
'height:75%',
|
|
597
|
+
'cursor:inherit',
|
|
598
|
+
'border-radius:50%',
|
|
599
|
+
`background-color:${((_b = wireframe.style) === null || _b === void 0 ? void 0 : _b.color) || defaultColor}`,
|
|
600
|
+
`border:2px solid ${((_c = wireframe.style) === null || _c === void 0 ? void 0 : _c.borderColor) ||
|
|
601
|
+
((_d = wireframe.style) === null || _d === void 0 ? void 0 : _d.color) ||
|
|
602
|
+
defaultColor}`,
|
|
603
|
+
]),
|
|
604
|
+
'data-rrweb-id': handlePartId,
|
|
605
|
+
},
|
|
606
|
+
id: handlePartId,
|
|
607
|
+
childNodes: [],
|
|
608
|
+
},
|
|
609
|
+
];
|
|
610
|
+
}
|
|
611
|
+
function makeToggleElement(wireframe, context) {
|
|
612
|
+
const isLabelled = 'label' in wireframe;
|
|
613
|
+
const wrappingDivId = context.idSequence.next().value;
|
|
614
|
+
return {
|
|
615
|
+
result: {
|
|
616
|
+
type: NodeType.Element,
|
|
617
|
+
tagName: 'div',
|
|
618
|
+
attributes: {
|
|
619
|
+
// if labelled take up available space, otherwise use provided positioning
|
|
620
|
+
style: isLabelled
|
|
621
|
+
? asStyleString(['height:100%', 'flex:1'])
|
|
622
|
+
: makePositionStyles(wireframe),
|
|
623
|
+
'data-rrweb-id': wireframe.id,
|
|
624
|
+
},
|
|
625
|
+
id: wireframe.id,
|
|
626
|
+
childNodes: [
|
|
627
|
+
{
|
|
628
|
+
type: NodeType.Element,
|
|
629
|
+
tagName: 'div',
|
|
630
|
+
attributes: {
|
|
631
|
+
// relative position, fills parent
|
|
632
|
+
style: asStyleString([
|
|
633
|
+
'position:relative',
|
|
634
|
+
'width:100%',
|
|
635
|
+
'height:100%',
|
|
636
|
+
]),
|
|
637
|
+
'data-rrweb-id': wrappingDivId,
|
|
638
|
+
},
|
|
639
|
+
id: wrappingDivId,
|
|
640
|
+
childNodes: makeToggleParts(wireframe, context),
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
},
|
|
644
|
+
context,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function makeLabelledInput(wireframe, theInputElement, context) {
|
|
648
|
+
const theLabel = {
|
|
649
|
+
type: NodeType.Text,
|
|
650
|
+
textContent: wireframe.label || '',
|
|
651
|
+
id: context.idSequence.next().value,
|
|
652
|
+
};
|
|
653
|
+
const orderedChildren = wireframe.inputType === 'toggle'
|
|
654
|
+
? [theLabel, theInputElement]
|
|
655
|
+
: [theInputElement, theLabel];
|
|
656
|
+
const labelId = context.idSequence.next().value;
|
|
657
|
+
return {
|
|
658
|
+
result: {
|
|
659
|
+
type: NodeType.Element,
|
|
660
|
+
tagName: 'label',
|
|
661
|
+
attributes: {
|
|
662
|
+
style: makeStylesString(wireframe),
|
|
663
|
+
'data-rrweb-id': labelId,
|
|
664
|
+
},
|
|
665
|
+
id: labelId,
|
|
666
|
+
childNodes: orderedChildren,
|
|
667
|
+
},
|
|
668
|
+
context,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function makeInputElement(wireframe, children, context) {
|
|
672
|
+
if (!wireframe.inputType) {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
if (wireframe.inputType === 'button') {
|
|
676
|
+
return makeButtonElement(wireframe, children, context);
|
|
677
|
+
}
|
|
678
|
+
if (wireframe.inputType === 'select') {
|
|
679
|
+
return makeSelectElement(wireframe, children, context);
|
|
680
|
+
}
|
|
681
|
+
if (wireframe.inputType === 'progress') {
|
|
682
|
+
return makeProgressElement(wireframe, children, context);
|
|
683
|
+
}
|
|
684
|
+
const theInputElement = wireframe.inputType === 'toggle'
|
|
685
|
+
? makeToggleElement(wireframe, context)
|
|
686
|
+
: {
|
|
687
|
+
result: {
|
|
688
|
+
type: NodeType.Element,
|
|
689
|
+
tagName: 'input',
|
|
690
|
+
attributes: inputAttributes(wireframe),
|
|
691
|
+
id: wireframe.id,
|
|
692
|
+
childNodes: children,
|
|
693
|
+
},
|
|
694
|
+
context,
|
|
695
|
+
};
|
|
696
|
+
if (!theInputElement) {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
if ('label' in wireframe) {
|
|
700
|
+
return makeLabelledInput(wireframe, theInputElement.result, theInputElement.context);
|
|
701
|
+
}
|
|
702
|
+
// when labelled no styles are needed, when un-labelled as here - we add the styling in.
|
|
703
|
+
theInputElement.result.attributes.style =
|
|
704
|
+
makeStylesString(wireframe);
|
|
705
|
+
return theInputElement;
|
|
706
|
+
}
|
|
707
|
+
function makeRectangleElement(wireframe, children, context) {
|
|
708
|
+
return {
|
|
709
|
+
result: {
|
|
710
|
+
type: NodeType.Element,
|
|
711
|
+
tagName: 'div',
|
|
712
|
+
attributes: {
|
|
713
|
+
style: makeStylesString(wireframe),
|
|
714
|
+
'data-rrweb-id': wireframe.id,
|
|
715
|
+
},
|
|
716
|
+
id: wireframe.id,
|
|
717
|
+
childNodes: children,
|
|
718
|
+
},
|
|
719
|
+
context,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function chooseConverter(wireframe) {
|
|
723
|
+
// in theory type is always present
|
|
724
|
+
// but since this is coming over the wire we can't really be sure,
|
|
725
|
+
// and so we default to div
|
|
726
|
+
const converterType = wireframe.type || 'div';
|
|
727
|
+
const converterMapping = {
|
|
728
|
+
// KLUDGE: TS can't tell that the wireframe type of each function is safe based on the converter type
|
|
729
|
+
text: makeTextElement,
|
|
730
|
+
image: makeImageElement,
|
|
731
|
+
rectangle: makeRectangleElement,
|
|
732
|
+
div: makeDivElement,
|
|
733
|
+
input: makeInputElement,
|
|
734
|
+
radio_group: makeRadioGroupElement,
|
|
735
|
+
web_view: makeWebViewElement,
|
|
736
|
+
placeholder: makePlaceholderElement,
|
|
737
|
+
status_bar: makeStatusBar,
|
|
738
|
+
navigation_bar: makeNavigationBar,
|
|
739
|
+
screenshot: makeImageElement,
|
|
740
|
+
};
|
|
741
|
+
return converterMapping[converterType];
|
|
742
|
+
}
|
|
743
|
+
function convertWireframe(wireframe, context) {
|
|
744
|
+
var _a;
|
|
745
|
+
const children = convertWireframesFor(wireframe.childWireframes, context);
|
|
746
|
+
const converted = (_a = chooseConverter(wireframe)) === null || _a === void 0 ? void 0 : _a(wireframe, children.result, children.context);
|
|
747
|
+
return converted || null;
|
|
748
|
+
}
|
|
749
|
+
function convertWireframesFor(wireframes, context) {
|
|
750
|
+
if (!wireframes) {
|
|
751
|
+
return { result: [], context };
|
|
752
|
+
}
|
|
753
|
+
const result = [];
|
|
754
|
+
for (const wireframe of wireframes) {
|
|
755
|
+
const converted = convertWireframe(wireframe, context);
|
|
756
|
+
if (converted) {
|
|
757
|
+
result.push(converted.result);
|
|
758
|
+
context = converted.context;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return { result, context };
|
|
762
|
+
}
|
|
763
|
+
function isMobileIncrementalSnapshotEvent(x) {
|
|
764
|
+
const isIncrementalSnapshot = isObject(x) && 'type' in x && x.type === EventType.IncrementalSnapshot;
|
|
765
|
+
if (!isIncrementalSnapshot) {
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
const hasData = isObject(x) && 'data' in x;
|
|
769
|
+
const data = hasData ? x.data : null;
|
|
770
|
+
const hasMutationSource = isObject(data) &&
|
|
771
|
+
'source' in data &&
|
|
772
|
+
data.source === IncrementalSource.Mutation;
|
|
773
|
+
const adds = isObject(data) && 'adds' in data && Array.isArray(data.adds)
|
|
774
|
+
? data.adds
|
|
775
|
+
: null;
|
|
776
|
+
const updates = isObject(data) && 'updates' in data && Array.isArray(data.updates)
|
|
777
|
+
? data.updates
|
|
778
|
+
: null;
|
|
779
|
+
const hasUpdatedWireframe = !!updates &&
|
|
780
|
+
updates.length > 0 &&
|
|
781
|
+
isObject(updates[0]) &&
|
|
782
|
+
'wireframe' in updates[0];
|
|
783
|
+
const hasAddedWireframe = !!adds &&
|
|
784
|
+
adds.length > 0 &&
|
|
785
|
+
isObject(adds[0]) &&
|
|
786
|
+
'wireframe' in adds[0];
|
|
787
|
+
return hasMutationSource && (hasAddedWireframe || hasUpdatedWireframe);
|
|
788
|
+
}
|
|
789
|
+
function chooseParentId(nodeType, providedParentId) {
|
|
790
|
+
return nodeType === 'screenshot' ? BODY_ID : providedParentId;
|
|
791
|
+
}
|
|
792
|
+
function makeIncrementalAdd(add, context) {
|
|
793
|
+
const converted = convertWireframe(add.wireframe, context);
|
|
794
|
+
if (!converted) {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
const addition = {
|
|
798
|
+
parentId: chooseParentId(add.wireframe.type, add.parentId),
|
|
799
|
+
nextId: null,
|
|
800
|
+
node: converted.result,
|
|
801
|
+
};
|
|
802
|
+
const adds = [];
|
|
803
|
+
if (addition) {
|
|
804
|
+
const flattened = flattenMutationAdds(addition);
|
|
805
|
+
flattened.forEach(x => adds.push(x));
|
|
806
|
+
return adds;
|
|
807
|
+
}
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* When processing an update we remove the entire item, and then add it back in.
|
|
812
|
+
*/
|
|
813
|
+
function makeIncrementalRemoveForUpdate(update) {
|
|
814
|
+
return {
|
|
815
|
+
parentId: chooseParentId(update.wireframe.type, update.parentId),
|
|
816
|
+
id: update.wireframe.id,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function isNode(x) {
|
|
820
|
+
// KLUDGE: really we should check that x.type is valid, but we're safe enough already
|
|
821
|
+
return isObject(x) && 'type' in x && 'id' in x;
|
|
822
|
+
}
|
|
823
|
+
function isNodeWithChildren(x) {
|
|
824
|
+
return isNode(x) && 'childNodes' in x && Array.isArray(x.childNodes);
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* when creating incremental adds we have to flatten the node tree structure
|
|
828
|
+
* there's no point, then keeping those child nodes in place
|
|
829
|
+
*/
|
|
830
|
+
function cloneWithoutChildren(converted) {
|
|
831
|
+
const cloned = Object.assign({}, converted);
|
|
832
|
+
const clonedNode = Object.assign({}, converted.node);
|
|
833
|
+
if (isNodeWithChildren(clonedNode)) {
|
|
834
|
+
clonedNode.childNodes = [];
|
|
835
|
+
}
|
|
836
|
+
cloned.node = clonedNode;
|
|
837
|
+
return cloned;
|
|
838
|
+
}
|
|
839
|
+
function flattenMutationAdds(converted) {
|
|
840
|
+
const flattened = [];
|
|
841
|
+
flattened.push(cloneWithoutChildren(converted));
|
|
842
|
+
const node = converted.node;
|
|
843
|
+
const newParentId = converted.node.id;
|
|
844
|
+
if (isNodeWithChildren(node)) {
|
|
845
|
+
node.childNodes.forEach(child => {
|
|
846
|
+
flattened.push(cloneWithoutChildren({
|
|
847
|
+
parentId: newParentId,
|
|
848
|
+
nextId: null,
|
|
849
|
+
node: child,
|
|
850
|
+
}));
|
|
851
|
+
if (isNodeWithChildren(child)) {
|
|
852
|
+
flattened.push(...flattenMutationAdds({
|
|
853
|
+
parentId: newParentId,
|
|
854
|
+
nextId: null,
|
|
855
|
+
node: child,
|
|
856
|
+
}));
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
return flattened;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* each update wireframe carries the entire tree because we don't want to diff on the client
|
|
864
|
+
* that means that we might create multiple mutations for the same node
|
|
865
|
+
* we only want to add it once, so we dedupe the mutations
|
|
866
|
+
* the app guarantees that for a given ID that is present more than once in a single snapshot
|
|
867
|
+
* every instance of that ID is identical
|
|
868
|
+
* it might change in the next snapshot but for a single incremental snapshot there is one
|
|
869
|
+
* and only one version of any given ID
|
|
870
|
+
*/
|
|
871
|
+
function dedupeMutations(mutations) {
|
|
872
|
+
// KLUDGE: it's slightly yucky to stringify everything but since synthetic nodes
|
|
873
|
+
// introduce a new id, we can't just compare the id
|
|
874
|
+
const seen = new Set();
|
|
875
|
+
// in case later mutations are the ones we want to keep, we reverse the array
|
|
876
|
+
// this does help with the deduping, so, it's likely that the view for a single ID
|
|
877
|
+
// is not consistent over a snapshot, but it's cheap to reverse so :YOLO:
|
|
878
|
+
return mutations
|
|
879
|
+
.reverse()
|
|
880
|
+
.filter((mutation) => {
|
|
881
|
+
let toCompare;
|
|
882
|
+
if (isRemovedNodeMutation(mutation)) {
|
|
883
|
+
toCompare = JSON.stringify(mutation);
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
// if this is a synthetic addition, then we need to ignore the id,
|
|
887
|
+
// since duplicates won't have duplicate ids
|
|
888
|
+
toCompare = JSON.stringify(Object.assign(Object.assign({}, mutation.node), { id: 0 }));
|
|
889
|
+
}
|
|
890
|
+
if (seen.has(toCompare)) {
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
seen.add(toCompare);
|
|
894
|
+
return true;
|
|
895
|
+
})
|
|
896
|
+
.reverse();
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* We want to ensure that any events don't use id = 0.
|
|
900
|
+
* They must always represent a valid ID from the dom, so we swap in the body id when the id = 0.
|
|
901
|
+
*
|
|
902
|
+
* For "removes", we don't need to do anything, the id of the element to be removed remains valid. We won't try and remove other elements that we added during transformation in order to show that element.
|
|
903
|
+
*
|
|
904
|
+
* "adds" are converted from wireframes to nodes and converted to `incrementalSnapshotEvent.adds`
|
|
905
|
+
*
|
|
906
|
+
* "updates" are converted to a remove and an add.
|
|
907
|
+
*
|
|
908
|
+
*/
|
|
909
|
+
const makeIncrementalEvent = (mobileEvent) => {
|
|
910
|
+
const converted = mobileEvent;
|
|
911
|
+
if ('id' in converted.data && converted.data.id === 0) {
|
|
912
|
+
converted.data.id = BODY_ID;
|
|
913
|
+
}
|
|
914
|
+
if (isMobileIncrementalSnapshotEvent(mobileEvent)) {
|
|
915
|
+
const adds = [];
|
|
916
|
+
const removes = mobileEvent.data.removes || [];
|
|
917
|
+
if ('adds' in mobileEvent.data &&
|
|
918
|
+
Array.isArray(mobileEvent.data.adds)) {
|
|
919
|
+
const addsContext = {
|
|
920
|
+
timestamp: mobileEvent.timestamp,
|
|
921
|
+
idSequence: globalIdSequence,
|
|
922
|
+
};
|
|
923
|
+
mobileEvent.data.adds.forEach(add => {
|
|
924
|
+
var _a;
|
|
925
|
+
(_a = makeIncrementalAdd(add, addsContext)) === null || _a === void 0 ? void 0 : _a.forEach(x => adds.push(x));
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
if ('updates' in mobileEvent.data &&
|
|
929
|
+
Array.isArray(mobileEvent.data.updates)) {
|
|
930
|
+
const updatesContext = {
|
|
931
|
+
timestamp: mobileEvent.timestamp,
|
|
932
|
+
idSequence: globalIdSequence,
|
|
933
|
+
};
|
|
934
|
+
const updateAdditions = [];
|
|
935
|
+
mobileEvent.data.updates.forEach(update => {
|
|
936
|
+
var _a;
|
|
937
|
+
const removal = makeIncrementalRemoveForUpdate(update);
|
|
938
|
+
if (removal) {
|
|
939
|
+
removes.push(removal);
|
|
940
|
+
}
|
|
941
|
+
(_a = makeIncrementalAdd(update, updatesContext)) === null || _a === void 0 ? void 0 : _a.forEach(x => updateAdditions.push(x));
|
|
942
|
+
});
|
|
943
|
+
dedupeMutations(updateAdditions).forEach(x => adds.push(x));
|
|
944
|
+
}
|
|
945
|
+
converted.data = {
|
|
946
|
+
source: IncrementalSource.Mutation,
|
|
947
|
+
attributes: [],
|
|
948
|
+
texts: [],
|
|
949
|
+
adds: dedupeMutations(adds),
|
|
950
|
+
// TODO: this assumes that removes are processed before adds 🤞
|
|
951
|
+
removes: dedupeMutations(removes),
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
return converted;
|
|
955
|
+
};
|
|
956
|
+
function makeKeyboardParent() {
|
|
957
|
+
return {
|
|
958
|
+
type: NodeType.Element,
|
|
959
|
+
tagName: 'div',
|
|
960
|
+
attributes: {
|
|
961
|
+
'data-render-reason': 'a fixed placeholder to contain the keyboard in the correct stacking position',
|
|
962
|
+
'data-rrweb-id': KEYBOARD_PARENT_ID,
|
|
963
|
+
},
|
|
964
|
+
id: KEYBOARD_PARENT_ID,
|
|
965
|
+
childNodes: [],
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
function makeStatusBarNode(statusBar, context) {
|
|
969
|
+
const childNodes = statusBar
|
|
970
|
+
? convertWireframesFor([statusBar], context).result
|
|
971
|
+
: [];
|
|
972
|
+
return {
|
|
973
|
+
type: NodeType.Element,
|
|
974
|
+
tagName: 'div',
|
|
975
|
+
attributes: {
|
|
976
|
+
'data-rrweb-id': STATUS_BAR_PARENT_ID,
|
|
977
|
+
},
|
|
978
|
+
id: STATUS_BAR_PARENT_ID,
|
|
979
|
+
childNodes,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
function makeNavBarNode(navigationBar, context) {
|
|
983
|
+
const childNodes = navigationBar
|
|
984
|
+
? convertWireframesFor([navigationBar], context).result
|
|
985
|
+
: [];
|
|
986
|
+
return {
|
|
987
|
+
type: NodeType.Element,
|
|
988
|
+
tagName: 'div',
|
|
989
|
+
attributes: {
|
|
990
|
+
'data-rrweb-id': NAVIGATION_BAR_PARENT_ID,
|
|
991
|
+
},
|
|
992
|
+
id: NAVIGATION_BAR_PARENT_ID,
|
|
993
|
+
childNodes,
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
function stripBarsFromWireframe(wireframe) {
|
|
997
|
+
if (wireframe.type === 'status_bar') {
|
|
998
|
+
return {
|
|
999
|
+
wireframe: undefined,
|
|
1000
|
+
statusBar: wireframe,
|
|
1001
|
+
navBar: undefined,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
else if (wireframe.type === 'navigation_bar') {
|
|
1005
|
+
return {
|
|
1006
|
+
wireframe: undefined,
|
|
1007
|
+
statusBar: undefined,
|
|
1008
|
+
navBar: wireframe,
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
let statusBar;
|
|
1012
|
+
let navBar;
|
|
1013
|
+
const wireframeToReturn = Object.assign({}, wireframe);
|
|
1014
|
+
wireframeToReturn.childWireframes = [];
|
|
1015
|
+
for (const child of wireframe.childWireframes || []) {
|
|
1016
|
+
const { wireframe: childWireframe, statusBar: childStatusBar, navBar: childNavBar, } = stripBarsFromWireframe(child);
|
|
1017
|
+
statusBar = statusBar || childStatusBar;
|
|
1018
|
+
navBar = navBar || childNavBar;
|
|
1019
|
+
if (childWireframe) {
|
|
1020
|
+
wireframeToReturn.childWireframes.push(childWireframe);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return { wireframe: wireframeToReturn, statusBar, navBar };
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* We want to be able to place the status bar and navigation bar in the correct stacking order.
|
|
1027
|
+
* So, we lift them out of the tree, and return them separately.
|
|
1028
|
+
*/
|
|
1029
|
+
function stripBarsFromWireframes(wireframes) {
|
|
1030
|
+
let statusBar;
|
|
1031
|
+
let navigationBar;
|
|
1032
|
+
const copiedNodes = [];
|
|
1033
|
+
wireframes.forEach(w => {
|
|
1034
|
+
const matches = stripBarsFromWireframe(w);
|
|
1035
|
+
if (matches.statusBar) {
|
|
1036
|
+
statusBar = matches.statusBar;
|
|
1037
|
+
}
|
|
1038
|
+
if (matches.navBar) {
|
|
1039
|
+
navigationBar = matches.navBar;
|
|
1040
|
+
}
|
|
1041
|
+
if (matches.wireframe) {
|
|
1042
|
+
copiedNodes.push(matches.wireframe);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
return { statusBar, navigationBar, appNodes: copiedNodes };
|
|
1046
|
+
}
|
|
1047
|
+
const makeFullEvent = (mobileEvent) => {
|
|
1048
|
+
// we can restart the id sequence on each full snapshot
|
|
1049
|
+
globalIdSequence = ids();
|
|
1050
|
+
if (!('wireframes' in mobileEvent.data)) {
|
|
1051
|
+
return mobileEvent;
|
|
1052
|
+
}
|
|
1053
|
+
const conversionContext = {
|
|
1054
|
+
timestamp: mobileEvent.timestamp,
|
|
1055
|
+
idSequence: globalIdSequence,
|
|
1056
|
+
};
|
|
1057
|
+
const { statusBar, navigationBar, appNodes } = stripBarsFromWireframes(mobileEvent.data.wireframes);
|
|
1058
|
+
const nodeGroups = {
|
|
1059
|
+
appNodes: convertWireframesFor(appNodes, conversionContext).result || [],
|
|
1060
|
+
statusBarNode: makeStatusBarNode(statusBar, conversionContext),
|
|
1061
|
+
navBarNode: makeNavBarNode(navigationBar, conversionContext),
|
|
1062
|
+
};
|
|
1063
|
+
return {
|
|
1064
|
+
type: EventType.FullSnapshot,
|
|
1065
|
+
timestamp: mobileEvent.timestamp,
|
|
1066
|
+
data: {
|
|
1067
|
+
node: {
|
|
1068
|
+
type: NodeType.Document,
|
|
1069
|
+
childNodes: [
|
|
1070
|
+
{
|
|
1071
|
+
type: NodeType.DocumentType,
|
|
1072
|
+
name: 'html',
|
|
1073
|
+
publicId: '',
|
|
1074
|
+
systemId: '',
|
|
1075
|
+
id: HTML_DOC_TYPE_ID,
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
type: NodeType.Element,
|
|
1079
|
+
tagName: 'html',
|
|
1080
|
+
attributes: {
|
|
1081
|
+
style: makeHTMLStyles(),
|
|
1082
|
+
'data-rrweb-id': HTML_ELEMENT_ID,
|
|
1083
|
+
},
|
|
1084
|
+
id: HTML_ELEMENT_ID,
|
|
1085
|
+
childNodes: [
|
|
1086
|
+
{
|
|
1087
|
+
type: NodeType.Element,
|
|
1088
|
+
tagName: 'head',
|
|
1089
|
+
attributes: { 'data-rrweb-id': HEAD_ID },
|
|
1090
|
+
id: HEAD_ID,
|
|
1091
|
+
childNodes: [makeCSSReset(conversionContext)],
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
type: NodeType.Element,
|
|
1095
|
+
tagName: 'body',
|
|
1096
|
+
attributes: {
|
|
1097
|
+
style: makeBodyStyles(),
|
|
1098
|
+
'data-rrweb-id': BODY_ID,
|
|
1099
|
+
},
|
|
1100
|
+
id: BODY_ID,
|
|
1101
|
+
childNodes: [
|
|
1102
|
+
// in the order they should stack if they ever clash
|
|
1103
|
+
// lower is higher in the stacking context
|
|
1104
|
+
...nodeGroups.appNodes,
|
|
1105
|
+
makeKeyboardParent(),
|
|
1106
|
+
nodeGroups.navBarNode,
|
|
1107
|
+
nodeGroups.statusBarNode,
|
|
1108
|
+
],
|
|
1109
|
+
},
|
|
1110
|
+
],
|
|
1111
|
+
},
|
|
1112
|
+
],
|
|
1113
|
+
id: DOCUMENT_ID,
|
|
1114
|
+
},
|
|
1115
|
+
initialOffset: {
|
|
1116
|
+
top: 0,
|
|
1117
|
+
left: 0,
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
};
|
|
1121
|
+
};
|
|
1122
|
+
function makeCSSReset(context) {
|
|
1123
|
+
// we need to normalize CSS so browsers don't do unexpected things
|
|
1124
|
+
return {
|
|
1125
|
+
type: NodeType.Element,
|
|
1126
|
+
tagName: 'style',
|
|
1127
|
+
attributes: {
|
|
1128
|
+
type: 'text/css',
|
|
1129
|
+
},
|
|
1130
|
+
id: context.idSequence.next().value,
|
|
1131
|
+
childNodes: [
|
|
1132
|
+
{
|
|
1133
|
+
type: NodeType.Text,
|
|
1134
|
+
textContent: `
|
|
1135
|
+
body {
|
|
1136
|
+
margin: unset;
|
|
1137
|
+
}
|
|
1138
|
+
input, button, select, textarea {
|
|
1139
|
+
font: inherit;
|
|
1140
|
+
margin: 0;
|
|
1141
|
+
padding: 0;
|
|
1142
|
+
border: 0;
|
|
1143
|
+
outline: 0;
|
|
1144
|
+
background: transparent;
|
|
1145
|
+
padding-block: 0 !important;
|
|
1146
|
+
}
|
|
1147
|
+
.input:focus {
|
|
1148
|
+
outline: none;
|
|
1149
|
+
}
|
|
1150
|
+
img {
|
|
1151
|
+
border-style: none;
|
|
1152
|
+
}
|
|
1153
|
+
`,
|
|
1154
|
+
id: context.idSequence.next().value,
|
|
1155
|
+
},
|
|
1156
|
+
],
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
export { BACKGROUND, KEYBOARD_ID, NAVIGATION_BAR_ID, STATUS_BAR_ID, STATUS_BAR_PARENT_ID, _isPositiveInteger, dataURIOrPNG, makeCustomEvent, makeDivElement, makeFullEvent, makeIncrementalEvent, makeMetaEvent, makePlaceholderElement, stripBarsFromWireframes };
|