@ckeditor/ckeditor5-watchdog 46.0.2 → 46.1.0-alpha.0

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/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
4
  */
5
- import { throttle, isElement, cloneDeepWith } from 'es-toolkit/compat';
5
+ import { throttle, isElement, cloneDeepWith, isPlainObject } from 'es-toolkit/compat';
6
6
 
7
7
  /**
8
8
  * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
@@ -672,27 +672,37 @@ function isObject(structure) {
672
672
  // `as any` to avoid linking from external private repo.
673
673
  const parsedCommentThreads = JSON.parse(this._data.commentThreads);
674
674
  const parsedSuggestions = JSON.parse(this._data.suggestions);
675
- parsedCommentThreads.forEach((commentThreadData)=>{
676
- const channelId = this.editor.config.get('collaboration.channelId');
675
+ if (this.editor.plugins.has('CommentsRepository')) {
677
676
  const commentsRepository = this.editor.plugins.get('CommentsRepository');
678
- if (commentsRepository.hasCommentThread(commentThreadData.threadId)) {
679
- const commentThread = commentsRepository.getCommentThread(commentThreadData.threadId);
680
- commentThread.remove();
677
+ // First, remove the existing comments that were created by integration plugins during initialization.
678
+ // These comments may be outdated, and new instances will be created in the next step based on the saved data.
679
+ for (const commentThread of commentsRepository.getCommentThreads()){
680
+ // Use the internal API since it removes the comment thread directly and does not trigger events
681
+ // that could cause side effects, such as removing markers.
682
+ commentsRepository._removeCommentThread({
683
+ threadId: commentThread.id
684
+ });
681
685
  }
682
- commentsRepository.addCommentThread({
683
- channelId,
684
- ...commentThreadData
686
+ parsedCommentThreads.forEach((commentThreadData)=>{
687
+ const channelId = this.editor.config.get('collaboration.channelId');
688
+ const commentsRepository = this.editor.plugins.get('CommentsRepository');
689
+ commentsRepository.addCommentThread({
690
+ channelId,
691
+ ...commentThreadData
692
+ });
685
693
  });
686
- });
687
- parsedSuggestions.forEach((suggestionData)=>{
694
+ }
695
+ if (this.editor.plugins.has('TrackChangesEditing')) {
688
696
  const trackChangesEditing = this.editor.plugins.get('TrackChangesEditing');
689
- if (trackChangesEditing.hasSuggestion(suggestionData.id)) {
690
- const suggestion = trackChangesEditing.getSuggestion(suggestionData.id);
691
- suggestion.attributes = suggestionData.attributes;
692
- } else {
693
- trackChangesEditing.addSuggestionData(suggestionData);
697
+ // First, remove the existing suggestions that were created by integration plugins during initialization.
698
+ // These suggestions may be outdated, and new instances will be created in the next step based on the saved data.
699
+ for (const suggestion of trackChangesEditing.getSuggestions()){
700
+ trackChangesEditing._removeSuggestion(suggestion);
694
701
  }
695
- });
702
+ parsedSuggestions.forEach((suggestionData)=>{
703
+ trackChangesEditing.addSuggestionData(suggestionData);
704
+ });
705
+ }
696
706
  }
697
707
  }
698
708
 
@@ -1080,5 +1090,622 @@ const mainQueueId = Symbol('MainQueueId');
1080
1090
  ];
1081
1091
  }
1082
1092
 
1083
- export { ContextWatchdog, EditorWatchdog, Watchdog };
1093
+ /**
1094
+ * A plugin that records user actions and editor state changes for debugging purposes. It tracks commands execution, model operations,
1095
+ * UI interactions, and document events. It just collects data locally, and does not send it anywhere, integrator is responsible
1096
+ * for gathering data from this plugin for further processing.
1097
+ *
1098
+ * **Important! `ActionsRecorder` is an experimental feature, and may become deprecated.**
1099
+ *
1100
+ * By default, plugin stores latest 1000 action entries. Integrator can register an `onError` callback to collect those entries
1101
+ * in case of exception. Integrator should augment this data with application specific data such as page-id or session-id,
1102
+ * depending on the application. Augmented data should be processed by the integrator, for example integrator should send it
1103
+ * to some data collecting endpoint for later analysis.
1104
+ *
1105
+ * Example:
1106
+ *
1107
+ * ```ts
1108
+ * ClassicEditor
1109
+ * .create( editorElement, {
1110
+ * plugins: [ ActionsRecorder, ... ],
1111
+ * actionsRecorder: {
1112
+ * maxEntries: 1000, // This is the default value and could be adjusted.
1113
+ *
1114
+ * onError( error, entries ) {
1115
+ * console.error( 'ActionsRecorder - Error detected:', error );
1116
+ * console.warn( 'Actions recorded before error:', entries );
1117
+ *
1118
+ * this.flushEntries();
1119
+ *
1120
+ * // Integrator should send and store the entries. The error is already in the last entry in serializable form.
1121
+ * }
1122
+ * }
1123
+ * } )
1124
+ * .then( ... )
1125
+ * .catch( ... );
1126
+ * ```
1127
+ *
1128
+ * Alternatively integrator could continuously collect actions in batches and send them to theirs endpoint for later analysis:
1129
+ *
1130
+ * ```ts
1131
+ * ClassicEditor
1132
+ * .create( editorElement, {
1133
+ * plugins: [ ActionsRecorder, ... ],
1134
+ * actionsRecorder: {
1135
+ * maxEntries: 50, // This is the batch size.
1136
+ *
1137
+ * onMaxEntries() {
1138
+ * const entries = this.getEntries();
1139
+ *
1140
+ * this.flushEntries();
1141
+ *
1142
+ * console.log( 'ActionsRecorder - Batch of entries:', entries );
1143
+ *
1144
+ * // Integrator should send and store the entries.
1145
+ * },
1146
+ *
1147
+ * onError( error, entries ) {
1148
+ * console.error( 'ActionsRecorder - Error detected:', error );
1149
+ * console.warn( 'Actions recorded before error:', entries );
1150
+ *
1151
+ * this.flushEntries();
1152
+ *
1153
+ * // Integrator should send and store the entries. The error is already in the last entry in serializable form.
1154
+ * }
1155
+ * }
1156
+ * } )
1157
+ * .then( ... )
1158
+ * .catch( ... );
1159
+ * ```
1160
+ *
1161
+ * See {@link module:watchdog/actionsrecorderconfig~ActionsRecorderConfig plugin configuration} for more details.
1162
+ *
1163
+ */ class ActionsRecorder {
1164
+ /**
1165
+ * The editor instance.
1166
+ */ editor;
1167
+ /**
1168
+ * Array storing all recorded action entries with their context and state snapshots.
1169
+ */ _entries = [];
1170
+ /**
1171
+ * Stack tracking nested action frames to maintain call hierarchy.
1172
+ */ _frameStack = [];
1173
+ /**
1174
+ * Set of already reported errors used to notify only once for each error (not on every try-catch nested block).
1175
+ */ _errors = new Set();
1176
+ /**
1177
+ * Maximum number of action entries to keep in memory.
1178
+ */ _maxEntries;
1179
+ /**
1180
+ * Error callback.
1181
+ */ _errorCallback;
1182
+ /**
1183
+ * Filter function to determine which entries should be stored.
1184
+ */ _filterCallback;
1185
+ /**
1186
+ * Callback triggered every time count of recorded entries reaches maxEntries.
1187
+ */ _maxEntriesCallback;
1188
+ /**
1189
+ * @inheritDoc
1190
+ */ static get pluginName() {
1191
+ return 'ActionsRecorder';
1192
+ }
1193
+ /**
1194
+ * @inheritDoc
1195
+ */ static get isOfficialPlugin() {
1196
+ return true;
1197
+ }
1198
+ /**
1199
+ * @inheritDoc
1200
+ */ constructor(editor){
1201
+ this.editor = editor;
1202
+ editor.config.define('actionsRecorder.maxEntries', 1000);
1203
+ const config = editor.config.get('actionsRecorder');
1204
+ this._maxEntries = config.maxEntries;
1205
+ this._filterCallback = config.onFilter;
1206
+ this._errorCallback = config.onError;
1207
+ this._maxEntriesCallback = config.onMaxEntries || this._maxEntriesDefaultHandler;
1208
+ this._tapCommands();
1209
+ this._tapOperationApply();
1210
+ this._tapModelMethods();
1211
+ this._tapModelSelection();
1212
+ this._tapComponentFactory();
1213
+ this._tapViewDocumentEvents();
1214
+ }
1215
+ /**
1216
+ * Returns all recorded action entries.
1217
+ */ getEntries() {
1218
+ // Return a shallow copy instead of reference as this array could be modified.
1219
+ return this._entries.slice();
1220
+ }
1221
+ /**
1222
+ * Flushes all recorded entries.
1223
+ */ flushEntries() {
1224
+ this._entries = [];
1225
+ }
1226
+ /**
1227
+ * Creates a new action frame and adds it to the recording stack.
1228
+ *
1229
+ * @param action The name/type of the action being recorded.
1230
+ * @param params Optional parameters associated with the event.
1231
+ * @returns The created call frame object.
1232
+ */ _enterFrame(action, params) {
1233
+ const callFrame = {
1234
+ timeStamp: new Date().toISOString(),
1235
+ ...this._frameStack.length && {
1236
+ parentEntry: this._frameStack.at(-1)
1237
+ },
1238
+ action,
1239
+ params: params?.map((param)=>serializeValue(param)),
1240
+ before: this._buildStateSnapshot()
1241
+ };
1242
+ // Apply filter if configured, only add to entries if filter passes.
1243
+ if (!this._filterCallback || this._filterCallback(callFrame, this._entries)) {
1244
+ // Add the call frame to the entries.
1245
+ this._entries.push(callFrame);
1246
+ }
1247
+ this._frameStack.push(callFrame);
1248
+ return callFrame;
1249
+ }
1250
+ /**
1251
+ * Closes an action frame and records its final state and results.
1252
+ *
1253
+ * @param callFrame The call frame to close.
1254
+ * @param result Optional result value from the action.
1255
+ * @param error Optional error that occurred during the action.
1256
+ */ _leaveFrame(callFrame, result, error) {
1257
+ const topFrame = this._frameStack.pop();
1258
+ // Handle scenario when the stack has been cleared in the meantime
1259
+ // or the callFrame is not the top frame.
1260
+ if (!topFrame || topFrame !== callFrame) {
1261
+ return;
1262
+ }
1263
+ if (result !== undefined) {
1264
+ topFrame.result = serializeValue(result);
1265
+ }
1266
+ if (error) {
1267
+ topFrame.error = serializeValue(error);
1268
+ }
1269
+ topFrame.after = this._buildStateSnapshot();
1270
+ if (error) {
1271
+ this._callErrorCallback(error);
1272
+ }
1273
+ if (this._frameStack.length == 0) {
1274
+ this._errors.clear();
1275
+ }
1276
+ // Enforce max entries limit after leaving the frame so that complete entry is provided.
1277
+ if (this._entries.length >= this._maxEntries) {
1278
+ this._maxEntriesCallback();
1279
+ }
1280
+ }
1281
+ /**
1282
+ * Builds a snapshot of the current editor state including document version,
1283
+ * read-only status, focus state, and model selection.
1284
+ *
1285
+ * @returns An object containing the current editor state snapshot.
1286
+ */ _buildStateSnapshot() {
1287
+ const { model, isReadOnly, editing } = this.editor;
1288
+ return {
1289
+ documentVersion: model.document.version,
1290
+ editorReadOnly: isReadOnly,
1291
+ editorFocused: editing.view.document.isFocused,
1292
+ modelSelection: serializeValue(model.document.selection)
1293
+ };
1294
+ }
1295
+ /**
1296
+ * Sets up recording for all editor commands, both existing and future ones.
1297
+ * Taps into the command execution to track when commands are run.
1298
+ */ _tapCommands() {
1299
+ // Tap already registered commands.
1300
+ for (const [commandName, command] of this.editor.commands){
1301
+ this._tapCommand(commandName, command);
1302
+ }
1303
+ // Tap commands registered after the constructor was called.
1304
+ tapObjectMethod(this.editor.commands, 'add', {
1305
+ before: (callContext, [commandName, command])=>{
1306
+ this._tapCommand(commandName, command);
1307
+ return false;
1308
+ }
1309
+ });
1310
+ }
1311
+ /**
1312
+ * Sets up recording for model operation applications.
1313
+ * Tracks when operations are applied to the model document.
1314
+ */ _tapOperationApply() {
1315
+ tapObjectMethod(this.editor.model, 'applyOperation', {
1316
+ before: (callContext, [operation])=>{
1317
+ // Ignore operations applied to document fragments.
1318
+ if (operation.baseVersion === null) {
1319
+ return false;
1320
+ }
1321
+ callContext.callFrame = this._enterFrame('model.applyOperation', [
1322
+ operation
1323
+ ]);
1324
+ return true;
1325
+ },
1326
+ after: (callContext)=>{
1327
+ this._leaveFrame(callContext.callFrame);
1328
+ },
1329
+ error: (callContext, error)=>{
1330
+ this._leaveFrame(callContext.callFrame, undefined, error);
1331
+ }
1332
+ });
1333
+ }
1334
+ /**
1335
+ * Sets up recording for key model methods like insertContent, insertObject, and deleteContent.
1336
+ * These methods represent high-level model manipulation operations.
1337
+ */ _tapModelMethods() {
1338
+ for (const methodName of [
1339
+ 'insertContent',
1340
+ 'insertObject',
1341
+ 'deleteContent'
1342
+ ]){
1343
+ tapObjectMethod(this.editor.model, methodName, {
1344
+ before: (callContext, ...params)=>{
1345
+ callContext.callFrame = this._enterFrame(`model.${methodName}`, params);
1346
+ return true;
1347
+ },
1348
+ after: (callContext, result)=>{
1349
+ this._leaveFrame(callContext.callFrame, result);
1350
+ },
1351
+ error: (callContext, error)=>{
1352
+ this._leaveFrame(callContext.callFrame, undefined, error);
1353
+ }
1354
+ });
1355
+ }
1356
+ }
1357
+ /**
1358
+ * Sets up recording for model selection changes.
1359
+ * Tracks when the selection range, attributes, or markers change.
1360
+ */ _tapModelSelection() {
1361
+ const events = [
1362
+ 'change:range',
1363
+ 'change:attribute',
1364
+ 'change:marker'
1365
+ ];
1366
+ this._tapFireMethod(this.editor.model.document.selection, events, {
1367
+ eventSource: 'model-selection'
1368
+ });
1369
+ }
1370
+ /**
1371
+ * Sets up recording for a specific command execution.
1372
+ *
1373
+ * @param commandName The name of the command to record.
1374
+ * @param command The command instance to tap into.
1375
+ */ _tapCommand(commandName, command) {
1376
+ tapObjectMethod(command, 'execute', {
1377
+ before: (callContext, params)=>{
1378
+ callContext.callFrame = this._enterFrame(`commands.${commandName}:execute`, params);
1379
+ return true;
1380
+ },
1381
+ after: (callContext, result)=>{
1382
+ this._leaveFrame(callContext.callFrame, result);
1383
+ },
1384
+ error: (callContext, error)=>{
1385
+ this._leaveFrame(callContext.callFrame, undefined, error);
1386
+ }
1387
+ });
1388
+ }
1389
+ /**
1390
+ * Sets up recording for UI component factory creation and component interactions.
1391
+ * Tracks when components are created and their execute events.
1392
+ */ _tapComponentFactory() {
1393
+ tapObjectMethod(this.editor.ui.componentFactory, 'create', {
1394
+ before: (callContext, [componentName])=>{
1395
+ callContext.componentName = componentName;
1396
+ callContext.callFrame = this._enterFrame(`component-factory.create:${componentName}`);
1397
+ return true;
1398
+ },
1399
+ after: (callContext, componentInstance)=>{
1400
+ const executeContext = {
1401
+ ...callContext,
1402
+ eventSource: `component.${callContext.componentName}`
1403
+ };
1404
+ if (typeof componentInstance.fire == 'function') {
1405
+ this._tapFireMethod(componentInstance, [
1406
+ 'execute'
1407
+ ], executeContext);
1408
+ }
1409
+ if (typeof componentInstance.panelView?.fire == 'function') {
1410
+ this._tapFireMethod(componentInstance.panelView, [
1411
+ 'execute'
1412
+ ], executeContext);
1413
+ }
1414
+ if (typeof componentInstance.buttonView?.actionView?.fire == 'function') {
1415
+ this._tapFireMethod(componentInstance.buttonView.actionView, [
1416
+ 'execute'
1417
+ ], executeContext);
1418
+ }
1419
+ this._leaveFrame(callContext.callFrame);
1420
+ },
1421
+ error: (callContext, error)=>{
1422
+ this._leaveFrame(callContext.callFrame, undefined, error);
1423
+ }
1424
+ });
1425
+ }
1426
+ /**
1427
+ * Sets up recording for view document events like clicks, keyboard input,
1428
+ * selection changes, and other user interactions.
1429
+ */ _tapViewDocumentEvents() {
1430
+ const events = [
1431
+ 'click',
1432
+ 'mousedown',
1433
+ 'mouseup',
1434
+ 'pointerdown',
1435
+ 'pointerup',
1436
+ 'focus',
1437
+ 'blur',
1438
+ 'keydown',
1439
+ 'keyup',
1440
+ 'selectionChange',
1441
+ 'compositionstart',
1442
+ 'compositionend',
1443
+ 'beforeinput',
1444
+ 'mutations',
1445
+ 'enter',
1446
+ 'delete',
1447
+ 'insertText',
1448
+ 'paste',
1449
+ 'copy',
1450
+ 'cut',
1451
+ 'dragstart',
1452
+ 'drop'
1453
+ ];
1454
+ this._tapFireMethod(this.editor.editing.view.document, events, {
1455
+ eventSource: 'observers'
1456
+ });
1457
+ }
1458
+ /**
1459
+ * Sets up recording for specific events fired by an emitter object.
1460
+ *
1461
+ * @param emitter The object that fires events to be recorded.
1462
+ * @param eventNames Array of event names to record.
1463
+ * @param context Additional context to include with recorded events.
1464
+ */ _tapFireMethod(emitter, eventNames, context = {}) {
1465
+ tapObjectMethod(emitter, 'fire', {
1466
+ before: (callContext, [eventInfoOrName, ...params])=>{
1467
+ const eventName = typeof eventInfoOrName == 'string' ? eventInfoOrName : eventInfoOrName.name;
1468
+ if (!eventNames.includes(eventName)) {
1469
+ return false;
1470
+ }
1471
+ callContext.callFrame = this._enterFrame(`${callContext.eventSource}:${eventName}`, params);
1472
+ return true;
1473
+ },
1474
+ after: (callContext, result)=>{
1475
+ this._leaveFrame(callContext.callFrame, result);
1476
+ },
1477
+ error: (callContext, error)=>{
1478
+ this._leaveFrame(callContext.callFrame, undefined, error);
1479
+ }
1480
+ }, context);
1481
+ }
1482
+ /**
1483
+ * Triggers error callback.
1484
+ */ _callErrorCallback(error) {
1485
+ if (!this._errorCallback || this._errors.has(error)) {
1486
+ return;
1487
+ }
1488
+ this._errors.add(error);
1489
+ try {
1490
+ // Provide a shallow copy of entries as it might be modified before error handler serializes it.
1491
+ this._errorCallback(error, this.getEntries());
1492
+ } catch (observerError) {
1493
+ // Silently catch observer errors to prevent them from affecting the recording.
1494
+ console.error('ActionsRecorder onError callback error:', observerError);
1495
+ }
1496
+ }
1497
+ /**
1498
+ * The default handler for maxEntries callback.
1499
+ */ _maxEntriesDefaultHandler() {
1500
+ this._entries.shift();
1501
+ }
1502
+ }
1503
+ /**
1504
+ * Creates a wrapper around a method to record its calls, results, and errors.
1505
+ *
1506
+ * @internal
1507
+ *
1508
+ * @param object The object containing the method to tap.
1509
+ * @param methodName The name of the method to tap.
1510
+ * @param tap The tap configuration with before/after/error hooks.
1511
+ * @param context Additional context to include with the method calls.
1512
+ */ function tapObjectMethod(object, methodName, tap, context = {}) {
1513
+ const originalMethod = object[methodName];
1514
+ if (originalMethod[Symbol.for('Tapped method')]) {
1515
+ return;
1516
+ }
1517
+ object[methodName] = (...args)=>{
1518
+ const callContext = Object.assign({}, context);
1519
+ let shouldHandle;
1520
+ try {
1521
+ shouldHandle = tap.before?.(callContext, args);
1522
+ const result = originalMethod.apply(object, args);
1523
+ if (shouldHandle) {
1524
+ tap.after?.(callContext, result);
1525
+ }
1526
+ return result;
1527
+ } catch (error) {
1528
+ if (shouldHandle) {
1529
+ tap.error?.(callContext, error);
1530
+ }
1531
+ throw error;
1532
+ }
1533
+ };
1534
+ object[methodName][Symbol.for('Tapped method')] = originalMethod;
1535
+ }
1536
+ /**
1537
+ * Serializes a value into a JSON-serializable format.
1538
+ *
1539
+ * @internal
1540
+ *
1541
+ * @param value The value to serialize.
1542
+ * @param visited Set of already serialized objects to avoid circular references.
1543
+ * @returns A JSON-serializable representation of the value.
1544
+ */ function serializeValue(value, visited = new WeakSet()) {
1545
+ if (!value || [
1546
+ 'boolean',
1547
+ 'number',
1548
+ 'string'
1549
+ ].includes(typeof value)) {
1550
+ return value;
1551
+ }
1552
+ if (typeof value.toJSON == 'function') {
1553
+ const jsonData = value.toJSON();
1554
+ // Make sure that toJSON returns plain object, otherwise it could be just a clone with circular references.
1555
+ if (isPlainObject(jsonData) || Array.isArray(jsonData) || [
1556
+ 'string',
1557
+ 'number',
1558
+ 'boolean'
1559
+ ].includes(typeof jsonData)) {
1560
+ return serializeValue(jsonData, visited);
1561
+ }
1562
+ }
1563
+ if (value instanceof Error) {
1564
+ return {
1565
+ name: value.name,
1566
+ message: value.message,
1567
+ stack: value.stack
1568
+ };
1569
+ }
1570
+ // Most TypeCheckable should implement toJSON method so this is a fallback for other TypeCheckable objects.
1571
+ if (isTypeCheckable(value) || typeof value != 'object') {
1572
+ return {
1573
+ type: typeof value,
1574
+ constructor: value.constructor?.name || 'unknown',
1575
+ string: String(value)
1576
+ };
1577
+ }
1578
+ if (value instanceof File || value instanceof Blob || value instanceof FormData || value instanceof DataTransfer) {
1579
+ return String(value);
1580
+ }
1581
+ if (visited.has(value)) {
1582
+ return;
1583
+ }
1584
+ visited.add(value);
1585
+ // Arrays.
1586
+ if (Array.isArray(value)) {
1587
+ return value.length ? value.map((item)=>serializeValue(item, visited)) : undefined;
1588
+ }
1589
+ // Other objects (plain, instances of classes, or events).
1590
+ const result = {};
1591
+ const ignoreFields = [];
1592
+ // DOM event additional fields.
1593
+ if (value.domEvent) {
1594
+ ignoreFields.push('domEvent', 'domTarget', 'view', 'document');
1595
+ result.domEvent = serializeDomEvent(value.domEvent);
1596
+ result.target = serializeValue(value.target);
1597
+ if (value.dataTransfer) {
1598
+ result.dataTransfer = {
1599
+ types: value.dataTransfer.types,
1600
+ htmlData: value.dataTransfer.getData('text/html'),
1601
+ files: serializeValue(value.dataTransfer.files)
1602
+ };
1603
+ }
1604
+ }
1605
+ // Other object types.
1606
+ for (const [key, val] of Object.entries(value)){
1607
+ // Ignore private fields, DOM events serialized above, and decorated methods.
1608
+ if (key.startsWith('_') || ignoreFields.includes(key) || typeof val == 'function') {
1609
+ continue;
1610
+ }
1611
+ const serializedValue = serializeValue(val, visited);
1612
+ if (serializedValue !== undefined) {
1613
+ result[key] = serializedValue;
1614
+ }
1615
+ }
1616
+ if (Symbol.iterator in value) {
1617
+ const items = Array.from(value[Symbol.iterator]()).map((item)=>serializeValue(item, visited));
1618
+ if (items.length) {
1619
+ result._items = items;
1620
+ }
1621
+ }
1622
+ return Object.keys(result).length ? result : undefined;
1623
+ }
1624
+ /**
1625
+ * Serializes a DOM event into a plain object representation.
1626
+ *
1627
+ * Extracts common properties from DOM events such as type, target information,
1628
+ * coordinates, key codes, and other relevant event data for debugging purposes.
1629
+ *
1630
+ * @param event The DOM event to serialize.
1631
+ * @returns A serialized object containing the event's key properties.
1632
+ */ function serializeDomEvent(event) {
1633
+ let serialized = {
1634
+ type: event.type,
1635
+ target: serializeDOMTarget(event.target)
1636
+ };
1637
+ // Add mouse event properties.
1638
+ if (event instanceof MouseEvent) {
1639
+ serialized = {
1640
+ ...serialized,
1641
+ button: event.button,
1642
+ buttons: event.buttons,
1643
+ ctrlKey: event.ctrlKey,
1644
+ shiftKey: event.shiftKey,
1645
+ altKey: event.altKey,
1646
+ metaKey: event.metaKey
1647
+ };
1648
+ }
1649
+ // Add keyboard event properties.
1650
+ if (event instanceof KeyboardEvent) {
1651
+ serialized = {
1652
+ ...serialized,
1653
+ key: event.key,
1654
+ code: event.code,
1655
+ keyCode: event.keyCode,
1656
+ ctrlKey: event.ctrlKey,
1657
+ shiftKey: event.shiftKey,
1658
+ altKey: event.altKey,
1659
+ metaKey: event.metaKey,
1660
+ repeat: event.repeat
1661
+ };
1662
+ }
1663
+ // Add input event properties.
1664
+ if (event instanceof InputEvent) {
1665
+ serialized = {
1666
+ ...serialized,
1667
+ data: event.data,
1668
+ inputType: event.inputType,
1669
+ isComposing: event.isComposing
1670
+ };
1671
+ }
1672
+ // Add pointer event properties.
1673
+ if (event instanceof PointerEvent) {
1674
+ serialized = {
1675
+ ...serialized,
1676
+ isPrimary: event.isPrimary
1677
+ };
1678
+ }
1679
+ /**
1680
+ * Serializes a DOM event target into a plain object representation.
1681
+ *
1682
+ * @param target The DOM event target to serialize.
1683
+ * @returns A serialized object containing the target's information.
1684
+ */ function serializeDOMTarget(target) {
1685
+ if (!target) {
1686
+ return null;
1687
+ }
1688
+ if (target instanceof Element) {
1689
+ return {
1690
+ tagName: target.tagName,
1691
+ className: target.className,
1692
+ id: target.id
1693
+ };
1694
+ }
1695
+ if (target instanceof Window || target instanceof Document) {
1696
+ return {
1697
+ type: target.constructor.name
1698
+ };
1699
+ }
1700
+ return {};
1701
+ }
1702
+ return serialized;
1703
+ }
1704
+ /**
1705
+ * Checks if a value is type-checkable, meaning it has an `is` method.
1706
+ */ function isTypeCheckable(value) {
1707
+ return value && typeof value.is === 'function';
1708
+ }
1709
+
1710
+ export { ActionsRecorder, ContextWatchdog, EditorWatchdog, Watchdog };
1084
1711
  //# sourceMappingURL=index.js.map