panther 4.3.7__py3-none-any.whl → 5.0.0b2__py3-none-any.whl

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.
Files changed (59) hide show
  1. panther/__init__.py +1 -1
  2. panther/_load_configs.py +78 -64
  3. panther/_utils.py +1 -1
  4. panther/app.py +126 -60
  5. panther/authentications.py +26 -9
  6. panther/base_request.py +27 -2
  7. panther/base_websocket.py +26 -27
  8. panther/cli/create_command.py +1 -0
  9. panther/cli/main.py +19 -27
  10. panther/cli/monitor_command.py +8 -4
  11. panther/cli/template.py +11 -6
  12. panther/cli/utils.py +3 -2
  13. panther/configs.py +7 -9
  14. panther/db/cursor.py +23 -7
  15. panther/db/models.py +26 -19
  16. panther/db/queries/base_queries.py +1 -1
  17. panther/db/queries/mongodb_queries.py +177 -13
  18. panther/db/queries/pantherdb_queries.py +5 -5
  19. panther/db/queries/queries.py +1 -1
  20. panther/events.py +10 -4
  21. panther/exceptions.py +24 -2
  22. panther/generics.py +2 -2
  23. panther/main.py +90 -117
  24. panther/middlewares/__init__.py +1 -1
  25. panther/middlewares/base.py +15 -19
  26. panther/middlewares/monitoring.py +42 -0
  27. panther/openapi/__init__.py +1 -0
  28. panther/openapi/templates/openapi.html +27 -0
  29. panther/openapi/urls.py +5 -0
  30. panther/openapi/utils.py +167 -0
  31. panther/openapi/views.py +101 -0
  32. panther/pagination.py +1 -1
  33. panther/panel/middlewares.py +10 -0
  34. panther/panel/templates/base.html +14 -0
  35. panther/panel/templates/create.html +21 -0
  36. panther/panel/templates/create.js +1270 -0
  37. panther/panel/templates/detail.html +55 -0
  38. panther/panel/templates/home.html +9 -0
  39. panther/panel/templates/home.js +30 -0
  40. panther/panel/templates/login.html +47 -0
  41. panther/panel/templates/sidebar.html +13 -0
  42. panther/panel/templates/table.html +73 -0
  43. panther/panel/templates/table.js +339 -0
  44. panther/panel/urls.py +10 -5
  45. panther/panel/utils.py +98 -0
  46. panther/panel/views.py +143 -0
  47. panther/request.py +3 -0
  48. panther/response.py +91 -53
  49. panther/routings.py +7 -2
  50. panther/serializer.py +1 -1
  51. panther/utils.py +34 -26
  52. panther/websocket.py +3 -0
  53. {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/METADATA +19 -17
  54. panther-5.0.0b2.dist-info/RECORD +75 -0
  55. {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/WHEEL +1 -1
  56. panther-4.3.7.dist-info/RECORD +0 -57
  57. {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/entry_points.txt +0 -0
  58. {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/licenses/LICENSE +0 -0
  59. {panther-4.3.7.dist-info → panther-5.0.0b2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1270 @@
1
+ const schema = JSON.parse(`{{fields|tojson|safe}}`);
2
+ function toggleObjectVisibility(checkbox, contentId) {
3
+ const content = document.getElementById(contentId);
4
+ content.classList.toggle("hidden", !checkbox.checked);
5
+
6
+ // Disable/enable all inputs within the container
7
+ const inputs = content.querySelectorAll("input, select, textarea");
8
+ inputs.forEach((input) => {
9
+ input.disabled = !checkbox.checked;
10
+ });
11
+ }
12
+
13
+ function createObjectInputs(objectSchema, container, prefix = "") {
14
+ if (!objectSchema || !objectSchema.fields) return;
15
+
16
+ Object.entries(objectSchema.fields).forEach(([fieldName, field]) => {
17
+ const fullFieldName = prefix ? `${prefix}.${fieldName}` : fieldName;
18
+
19
+ // Check if it's an array type
20
+ if (field.type.includes("array")) {
21
+ // If items is specified, use it
22
+ if (field.items) {
23
+ const itemType = field.items.replace("$", "");
24
+ createArrayField(fieldName, itemType, container, fullFieldName, field);
25
+ } else {
26
+ // Handle array without items specification (simple types)
27
+ createSimpleArrayField(fieldName, container, fullFieldName, field);
28
+ }
29
+ } else if (
30
+ Array.isArray(field.type) &&
31
+ field.type.some((t) => t.startsWith("$"))
32
+ ) {
33
+ const objectType = field.type
34
+ .find((t) => t.startsWith("$"))
35
+ .replace("$", "");
36
+ createNestedObjectField(
37
+ fieldName,
38
+ objectType,
39
+ field,
40
+ container,
41
+ fullFieldName
42
+ );
43
+ } else {
44
+ createBasicInput(fieldName, field, container, fullFieldName);
45
+ }
46
+ });
47
+ }
48
+
49
+ function toggleArrayVisibility(checkbox, contentId) {
50
+ const content = document.getElementById(contentId);
51
+ if (content) {
52
+ content.classList.toggle("hidden", !checkbox.checked);
53
+
54
+ // Also enable/disable inputs
55
+ const inputs = content.querySelectorAll("input, select, textarea");
56
+ inputs.forEach((input) => {
57
+ input.disabled = !checkbox.checked;
58
+ });
59
+ }
60
+ }
61
+
62
+ function createSimpleArrayField(fieldName, container, fullFieldName, field) {
63
+ const arrayContainer = document.createElement("div");
64
+ arrayContainer.className = "border border-gray-400 p-4 rounded-lg space-y-2";
65
+
66
+ const spreadsheetId = `${fullFieldName}-container`;
67
+
68
+ const header = document.createElement("div");
69
+ header.className = "flex items-center justify-between mb-4";
70
+ header.innerHTML = `
71
+ <h3 class="text-lg font-medium">${field.title || fieldName}</h3>
72
+ ${
73
+ field.type.includes("null")
74
+ ? `
75
+ <label class="flex items-center space-x-3">
76
+ <input type="checkbox"
77
+ id="${fullFieldName}_toggle"
78
+ class="form-checkbox h-5 w-5 text-blue-600 bg-gray-700 border-gray-600 rounded"
79
+ ${!field.required ? "" : "checked disabled"}
80
+ onchange="toggleArrayVisibility(this, '${spreadsheetId}')">
81
+ <span class="text-sm font-medium">Include ${
82
+ field.title || fieldName
83
+ }</span>
84
+ </label>
85
+ `
86
+ : ""
87
+ }
88
+ `;
89
+
90
+ const content = document.createElement("div");
91
+ content.innerHTML = `
92
+ <div class="flex justify-between items-center mb-4">
93
+ <button type="button"
94
+ class="bg-green-600 px-3 py-1 rounded text-sm"
95
+ onclick="addSimpleArrayRow('${fullFieldName}', '${spreadsheetId}')">
96
+ Add Item
97
+ </button>
98
+ </div>
99
+ <div id="${spreadsheetId}" class="array-items space-y-4">
100
+ </div>
101
+ `;
102
+
103
+ if (field.type.includes("null") && !field.required) {
104
+ content.classList.add("hidden");
105
+ }
106
+
107
+ arrayContainer.appendChild(header);
108
+ arrayContainer.appendChild(content);
109
+ container.appendChild(arrayContainer);
110
+ }
111
+
112
+ function addSimpleArrayRow(arrayName, containerId) {
113
+ const container = document.getElementById(containerId);
114
+ if (!container) {
115
+ console.error("Invalid container");
116
+ return;
117
+ }
118
+
119
+ const rowIndex = container.children.length;
120
+ const rowContainer = document.createElement("div");
121
+ rowContainer.className = "flex items-center space-x-4";
122
+
123
+ const input = document.createElement("input");
124
+ input.type = "text";
125
+ input.name = `${arrayName}[${rowIndex}]`;
126
+ input.className =
127
+ "flex-grow bg-gray-700 border border-gray-600 rounded px-3 py-2";
128
+
129
+ const deleteButton = document.createElement("button");
130
+ deleteButton.className = "bg-red-600 px-3 py-1 rounded text-sm";
131
+ deleteButton.textContent = "Delete";
132
+ deleteButton.onclick = () => {
133
+ rowContainer.remove();
134
+ reindexArrayItems(containerId);
135
+ };
136
+ rowContainer.appendChild(input);
137
+ rowContainer.appendChild(deleteButton);
138
+ container.appendChild(rowContainer);
139
+ }
140
+
141
+ function reindexArrayItems(containerId) {
142
+ const container = document.getElementById(containerId);
143
+ const items = container.children;
144
+
145
+ // Update the index for each remaining item
146
+ Array.from(items).forEach((item, newIndex) => {
147
+ const input = item.querySelector("input");
148
+ if (input) {
149
+ const oldName = input.name;
150
+ const baseName = oldName.split("[")[0];
151
+ input.name = `${baseName}[${newIndex}]`;
152
+ }
153
+ });
154
+ }
155
+ function createNestedObjectField(
156
+ fieldName,
157
+ objectType,
158
+ field,
159
+ container,
160
+ fullFieldName
161
+ ) {
162
+ const objectWrapper = document.createElement("div");
163
+ objectWrapper.className = "space-y-4 border border-gray-400 p-4 rounded-lg";
164
+
165
+ const cleanObjectType = objectType.replace(/^\$/, "");
166
+
167
+ const header = document.createElement("div");
168
+ header.className = "flex items-center justify-between mb-4";
169
+ header.innerHTML = `<h3 class="text-lg font-medium">${
170
+ field.title || fieldName
171
+ }</h3>`;
172
+
173
+ const contentContainer = document.createElement("div");
174
+ contentContainer.id = `${fullFieldName}_content`;
175
+ contentContainer.className = "space-y-4";
176
+
177
+ // Add toggle for optional objects
178
+ if (!field.required) {
179
+ const toggleContainer = document.createElement("div");
180
+ toggleContainer.className = "flex items-center space-x-3";
181
+
182
+ const checkbox = document.createElement("input");
183
+ checkbox.type = "checkbox";
184
+ checkbox.id = `${fullFieldName}_toggle`;
185
+ checkbox.className = "form-checkbox h-5 w-5 text-blue-600";
186
+
187
+ const label = document.createElement("label");
188
+ label.htmlFor = `${fullFieldName}_toggle`;
189
+ label.textContent = `Include ${field.title || fieldName}`;
190
+
191
+ toggleContainer.appendChild(checkbox);
192
+ toggleContainer.appendChild(label);
193
+ header.appendChild(toggleContainer);
194
+
195
+ // Toggle handler to enable/disable validation
196
+ checkbox.addEventListener("change", (e) => {
197
+ const fields = contentContainer.querySelectorAll(
198
+ "input, select, textarea"
199
+ );
200
+ fields.forEach((field) => {
201
+ if (!e.target.checked) {
202
+ // When unchecked, disable and remove required attribute
203
+ field.disabled = true;
204
+ field.required = false;
205
+ field.value = ""; // Clear the value
206
+ } else {
207
+ // When checked, enable and restore required if it was originally required
208
+ field.disabled = false;
209
+ field.required = field.dataset.originalRequired === "true";
210
+ }
211
+ });
212
+ });
213
+
214
+ // Initialize as unchecked
215
+ checkbox.checked = false;
216
+ setTimeout(() => checkbox.dispatchEvent(new Event("change")), 0);
217
+ }
218
+
219
+ const nestedSchema = schema.$[cleanObjectType];
220
+ createObjectInputs(nestedSchema, contentContainer, fullFieldName);
221
+
222
+ // Store original required state for all nested fields
223
+ contentContainer
224
+ .querySelectorAll("input, select, textarea")
225
+ .forEach((field) => {
226
+ field.dataset.originalRequired = field.required;
227
+ if (!field.required) {
228
+ field.required = false;
229
+ }
230
+ });
231
+
232
+ objectWrapper.appendChild(header);
233
+ objectWrapper.appendChild(contentContainer);
234
+ container.appendChild(objectWrapper);
235
+ }
236
+
237
+ function createObjectInputs(objectSchema, container, prefix = "") {
238
+ if (!objectSchema || !objectSchema.fields) return;
239
+
240
+ Object.entries(objectSchema.fields).forEach(([fieldName, field]) => {
241
+ const fullFieldName = prefix ? `${prefix}.${fieldName}` : fieldName;
242
+
243
+ // Check if it's an array type
244
+ if (field.type.includes("array")) {
245
+ // If items is specified, use it
246
+ if (field.items) {
247
+ const itemType = field.items.replace("$", "");
248
+ createArrayField(fieldName, itemType, container, fullFieldName, field);
249
+ } else {
250
+ // Handle array without items specification (simple types)
251
+ createSimpleArrayField(fieldName, container, fullFieldName, field);
252
+ }
253
+ } else if (
254
+ // Check if type is an array and contains a reference to another type
255
+ (Array.isArray(field.type) &&
256
+ field.type.some((t) => t.startsWith("$"))) ||
257
+ // Or if type is a string and is a reference
258
+ (typeof field.type === "string" && field.type.startsWith("$"))
259
+ ) {
260
+ const objectType = Array.isArray(field.type)
261
+ ? field.type.find((t) => t.startsWith("$"))
262
+ : field.type;
263
+ createNestedObjectField(
264
+ fieldName,
265
+ objectType,
266
+ field,
267
+ container,
268
+ fullFieldName
269
+ );
270
+ } else {
271
+ createBasicInput(fieldName, field, container, fullFieldName);
272
+ }
273
+ });
274
+ }
275
+
276
+ function createBasicInput(fieldName, field, container, fullFieldName) {
277
+ const inputWrapper = document.createElement("div");
278
+ inputWrapper.className = "space-y-2";
279
+ if (fieldName === "id" && typeof isUpdate === "undefined") {
280
+ return;
281
+ }
282
+
283
+ // Hide ID field in update mode
284
+ if (fieldName === "_id" && isUpdate) {
285
+ const inputWrapper = document.createElement("div");
286
+ inputWrapper.className = "space-y-2";
287
+ inputWrapper.style.display = "none"; // Hide the entire wrapper
288
+
289
+ inputWrapper.innerHTML = `
290
+ <label class="block">
291
+ <span class="text-sm font-medium">ID</span>
292
+ <input type="text" name="_id" readonly
293
+ class="w-full mt-1 p-2 bg-slate-900 rounded text-gray-300">
294
+ </label>
295
+ `;
296
+
297
+ container.appendChild(inputWrapper);
298
+ return;
299
+ }
300
+
301
+ let inputHTML = "";
302
+ const defaultValue =
303
+ field.default !== undefined ? `value="${field.default}"` : "";
304
+ const requiredText = field.required
305
+ ? `<span class="text-red-500 text-sm ml-2">* Required</span>`
306
+ : "";
307
+ if (field.type.includes("boolean")) {
308
+ inputHTML = `
309
+ <label class="flex items-center space-x-3">
310
+ <input type="checkbox" name="${fullFieldName}"
311
+ ${field.default ? "checked" : ""}
312
+ class="form-checkbox h-5 w-5 text-blue-600 bg-gray-700 border-gray-600 rounded">
313
+ <span class="text-sm font-medium">${field.title || fieldName}</span>
314
+ ${requiredText}
315
+ </label>
316
+ `;
317
+ } else if (field.type.includes("string")) {
318
+ inputHTML = `
319
+ <label class="block">
320
+ <span class="text-sm font-medium">${
321
+ field.title || fieldName
322
+ } ${requiredText}</span>
323
+ <input type="text" name="${fullFieldName}"
324
+ ${defaultValue}
325
+ ${field.required ? "required" : ""}
326
+ class="w-full mt-1 p-2 bg-slate-900 rounded text-gray-300">
327
+ </label>
328
+ `;
329
+ } else if (field.type.includes("integer")) {
330
+ const min = field.min !== undefined ? `min="${field.min}"` : "";
331
+ const max = field.max !== undefined ? `max="${field.max}"` : "";
332
+
333
+ inputHTML = `
334
+ <label class="block">
335
+ <span class="text-sm font-medium">${
336
+ field.title || fieldName
337
+ } ${requiredText}</span>
338
+ <input type="number" name="${fullFieldName}"
339
+ ${defaultValue} ${min} ${max}
340
+ ${field.required ? "required" : ""}
341
+ class="w-full mt-1 p-2 bg-slate-900 rounded text-gray-300">
342
+ </label>
343
+ `;
344
+ }
345
+
346
+ inputWrapper.innerHTML = inputHTML;
347
+ container.appendChild(inputWrapper);
348
+ }
349
+
350
+ function createArrayField(fieldName, itemType, container, fullFieldName) {
351
+ const arrayContainer = document.createElement("div");
352
+ arrayContainer.className = "border border-gray-700 p-4 rounded-lg space-y-4";
353
+
354
+ const spreadsheetId = `${fullFieldName}-container`;
355
+
356
+ // Make sure itemType exists in schema.$
357
+ if (!schema.$ || !schema.$[itemType]) {
358
+ console.error(`Schema type ${itemType} not found`);
359
+ return;
360
+ }
361
+
362
+ arrayContainer.innerHTML = `
363
+ <div class="flex justify-between items-center mb-4">
364
+ <h3 class="text-lg font-medium">${fieldName}</h3>
365
+ <button type="button"
366
+ class="bg-green-600 px-3 py-1 rounded text-sm"
367
+ onclick="addArrayRow('${fullFieldName}', '${itemType}', '${spreadsheetId}')">
368
+ Add Item
369
+ </button>
370
+ </div>
371
+ <div id="${spreadsheetId}" class="array-items space-y-4"></div>
372
+ `;
373
+
374
+ container.appendChild(arrayContainer);
375
+ }
376
+
377
+ function addArrayRow(arrayName, itemType, containerId) {
378
+ const container = document.getElementById(containerId);
379
+ if (!container || !schema.$ || !schema.$[itemType]) {
380
+ console.error("Invalid container or schema type");
381
+ return;
382
+ }
383
+
384
+ const itemSchema = schema.$[itemType];
385
+ const rowIndex = container.children.length;
386
+ const rowContainer = document.createElement("div");
387
+ rowContainer.className =
388
+ "flex gap-2 items-start space-x-4 py-4 pl-3 border pr-2 bg-gray-800/80 border-gray-500 rounded-lg";
389
+
390
+ const itemContent = document.createElement("div");
391
+ itemContent.className = "flex-grow flex flex-col gap-8 w-full";
392
+
393
+ createObjectInputs(itemSchema, itemContent, `${arrayName}[${rowIndex}]`);
394
+
395
+ const deleteButton = document.createElement("button");
396
+ deleteButton.className = "bg-red-600 m-1 p-1 min-w-8 min-h-8 rounded text-sm";
397
+ deleteButton.textContent = "X";
398
+ deleteButton.onclick = () => {
399
+ rowContainer.remove();
400
+ reindexComplexArrayItems(containerId);
401
+ };
402
+ rowContainer.appendChild(itemContent);
403
+ rowContainer.appendChild(deleteButton);
404
+ container.appendChild(rowContainer);
405
+ }
406
+ function reindexComplexArrayItems(containerId) {
407
+ const container = document.getElementById(containerId);
408
+ const items = container.children;
409
+
410
+ Array.from(items).forEach((item, newIndex) => {
411
+ const inputs = item.querySelectorAll("input, select, textarea");
412
+ inputs.forEach((input) => {
413
+ const oldName = input.name;
414
+ const fieldPart = oldName.split("]")[1]; // Get the part after the index
415
+ const baseName = oldName.split("[")[0];
416
+ input.name = `${baseName}[${newIndex}]${fieldPart || ""}`;
417
+ });
418
+ });
419
+ }
420
+ function openObjectModal(fieldName, objectType) {
421
+ // Create modal for editing nested object
422
+ const modal = document.createElement("div");
423
+ modal.className =
424
+ "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center";
425
+ modal.innerHTML = `
426
+ <div class="bg-gray-800 p-6 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
427
+ <h3 class="text-lg font-medium mb-4">Edit ${fieldName}</h3>
428
+ <div class="object-inputs space-y-4"></div>
429
+ <div class="mt-4 flex justify-end space-x-2">
430
+ <button type="button" class="bg-gray-600 px-4 py-2 rounded" onclick="this.closest('.fixed').remove()">
431
+ Cancel
432
+ </button>
433
+ <button type="button" class="bg-blue-600 px-4 py-2 rounded" onclick="saveObjectModal(this)">
434
+ Save
435
+ </button>
436
+ </div>
437
+ </div>
438
+ `;
439
+
440
+ const objectSchema = schema.$[objectType];
441
+ createObjectInputs(
442
+ objectSchema,
443
+ modal.querySelector(".object-inputs"),
444
+ fieldName
445
+ );
446
+
447
+ document.body.appendChild(modal);
448
+ }
449
+
450
+ const dynamicInputs = document.getElementById("dynamicInputs");
451
+
452
+ createObjectInputs(schema, dynamicInputs);
453
+
454
+ // Check if the page is in update mode
455
+ if (typeof isUpdate !== "undefined" && isUpdate) {
456
+ // Populate the form with existing data for update mode
457
+ populateFormWithExistingData(existingData);
458
+ } else {
459
+ console.log("Create mode: No existing data to populate.");
460
+ }
461
+
462
+ // Function to populate the form with existing data
463
+ function populateFormWithExistingData(data) {
464
+ console.log("Populating form with data:", data);
465
+
466
+ // Handle simple fields first
467
+ Object.entries(data).forEach(([key, value]) => {
468
+ const input = document.querySelector(`[name="${key}"]`);
469
+ if (input) {
470
+ if (input.type === "checkbox") {
471
+ input.checked = Boolean(value);
472
+ } else {
473
+ input.value = value !== null ? value : "";
474
+ }
475
+ }
476
+ });
477
+
478
+ // Special case for the `_id` field
479
+ // Find the ID input field
480
+ const idInput = document.querySelector(`[name="_id"]`);
481
+ if (idInput) {
482
+ // If we're in update mode and have an ID, set the value
483
+ if (data && data.id) {
484
+ idInput.value = data.id;
485
+ }
486
+
487
+ // Hide the input itself
488
+ idInput.style.display = "none";
489
+
490
+ // Also hide the parent container (which likely contains the label)
491
+ const parentContainer =
492
+ idInput.closest(".form-group") || idInput.parentElement;
493
+ if (parentContainer) {
494
+ parentContainer.style.display = "none";
495
+ }
496
+
497
+ // Keep the field in the form data by making it readonly but not disabled
498
+ idInput.setAttribute("readonly", true);
499
+ }
500
+
501
+ // Handle nested objects and arrays
502
+ populateNestedData(data);
503
+ }
504
+
505
+ function populateNestedData(data, prefix = "") {
506
+ Object.entries(data).forEach(([key, value]) => {
507
+ const fullPath = prefix ? `${prefix}.${key}` : key;
508
+
509
+ // Handle arrays
510
+ if (Array.isArray(value)) {
511
+ // Find the array container
512
+ const containerID = `${fullPath}-container`;
513
+ const container = document.getElementById(containerID);
514
+
515
+ if (container) {
516
+ // Ensure toggle is checked for this array if it exists
517
+ const toggle = document.getElementById(`${fullPath}_toggle`);
518
+ if (toggle) {
519
+ toggle.checked = true;
520
+ toggleArrayVisibility(toggle, containerID);
521
+ }
522
+
523
+ // Clear any existing items
524
+ container.innerHTML = "";
525
+
526
+ // Recreate each array item
527
+ value.forEach((item, index) => {
528
+ if (typeof item === "object" && item !== null) {
529
+ // Complex object in array
530
+ const itemType = determineItemType(item);
531
+ if (itemType) {
532
+ addArrayRow(fullPath, itemType, containerID);
533
+
534
+ // After adding the row, populate its fields
535
+ // The new row should be the last child of the container
536
+ const newRow = container.lastElementChild;
537
+ if (newRow) {
538
+ // Find all inputs in this row and set their values
539
+ Object.entries(item).forEach(([itemKey, itemValue]) => {
540
+ const itemPath = `${fullPath}[${index}].${itemKey}`;
541
+ populateItemField(newRow, itemPath, itemValue);
542
+ });
543
+ }
544
+ }
545
+ } else {
546
+ // Simple value in array
547
+ addSimpleArrayRow(fullPath, containerID);
548
+ // Get the last added row and set its value
549
+ const rowInput = container.lastElementChild.querySelector("input");
550
+ if (rowInput) {
551
+ rowInput.value = item;
552
+ }
553
+ }
554
+ });
555
+ }
556
+ }
557
+ // Handle nested objects
558
+ else if (typeof value === "object" && value !== null) {
559
+ // Find the content container for this object
560
+ const contentId = `${fullPath}_content`;
561
+ const content = document.getElementById(contentId);
562
+
563
+ if (content) {
564
+ // Enable the toggle if it exists
565
+ const toggle = document.getElementById(`${fullPath}_toggle`);
566
+ if (toggle) {
567
+ toggle.checked = true;
568
+ // Manually enable all fields in this container
569
+ const inputs = content.querySelectorAll("input, select, textarea");
570
+ inputs.forEach((input) => {
571
+ input.disabled = false;
572
+ });
573
+ }
574
+
575
+ // Populate the fields in this object
576
+ Object.entries(value).forEach(([nestedKey, nestedValue]) => {
577
+ const nestedPath = `${fullPath}.${nestedKey}`;
578
+ populateItemField(content, nestedPath, nestedValue);
579
+ });
580
+ }
581
+ }
582
+ });
583
+ }
584
+
585
+ function populateItemField(container, fieldPath, value) {
586
+ const input = container.querySelector(`[name="${fieldPath}"]`);
587
+ if (input) {
588
+ if (input.type === "checkbox") {
589
+ input.checked = Boolean(value);
590
+ } else {
591
+ input.value = value !== null ? value : "";
592
+ }
593
+ }
594
+ }
595
+
596
+ function determineItemType(item) {
597
+ // Try to match the item structure with schema definitions
598
+ for (const [typeName, typeSchema] of Object.entries(schema.$)) {
599
+ if (typeSchema && typeSchema.fields) {
600
+ const fieldNames = Object.keys(typeSchema.fields);
601
+ // If most of the item keys match the schema fields, assume it's this type
602
+ const matchingFields = fieldNames.filter((field) => field in item);
603
+ if (
604
+ matchingFields.length > 0 &&
605
+ matchingFields.length / fieldNames.length >= 0.5
606
+ ) {
607
+ return typeName;
608
+ }
609
+ }
610
+ }
611
+ return null;
612
+ }
613
+
614
+ document
615
+ .getElementById(isUpdate ? "updateForm" : "createForm")
616
+ .addEventListener("submit", async (e) => {
617
+ e.preventDefault();
618
+
619
+ // Create an object to hold our updated data
620
+ const updatedData = {};
621
+
622
+ // Extract the current form values
623
+ const formData = new FormData(e.target);
624
+
625
+ // Debug log
626
+ console.log("Form data entries:");
627
+ for (let [key, value] of formData.entries()) {
628
+ console.log(`FormData Key: ${key}, Value: ${value}`);
629
+ }
630
+
631
+ // Process each form field
632
+ for (let [key, value] of formData.entries()) {
633
+ // Skip disabled fields (they won't be included in FormData anyway)
634
+ const field = e.target.querySelector(`[name="${key}"]`);
635
+ if (field && field.disabled) continue;
636
+
637
+ // Parse the key to handle nested structures
638
+ const parts = key.split(/[\[\].]/).filter(Boolean);
639
+
640
+ // Start at the root of our data object
641
+ let current = updatedData;
642
+
643
+ // Build the nested structure
644
+ for (let i = 0; i < parts.length; i++) {
645
+ const part = parts[i];
646
+
647
+ if (i === parts.length - 1) {
648
+ // We're at the leaf node, set the actual value
649
+
650
+ // Handle boolean values from checkboxes
651
+ if (field && field.type === "checkbox") {
652
+ value = field.checked;
653
+ }
654
+ // Convert string "true"/"false" to boolean
655
+ else if (value === "true" || value === "false") {
656
+ value = value === "true";
657
+ }
658
+ // Convert numeric strings to numbers
659
+ else if (!isNaN(value) && value !== "") {
660
+ value = Number(value);
661
+ }
662
+
663
+ // Set the value in our data structure
664
+ current[part] = value;
665
+ } else {
666
+ // We're building the nested structure
667
+
668
+ // Check if the next part is a number (array index)
669
+ if (/^\d+$/.test(parts[i + 1])) {
670
+ // Create array if it doesn't exist
671
+ current[part] = current[part] || [];
672
+ } else {
673
+ // Create object if it doesn't exist
674
+ current[part] = current[part] || {};
675
+ }
676
+
677
+ // Move deeper into the structure
678
+ current = current[part];
679
+ }
680
+ }
681
+ }
682
+
683
+ // Process unchecked checkboxes (they don't appear in FormData)
684
+ const allCheckboxes = e.target.querySelectorAll('input[type="checkbox"]');
685
+ allCheckboxes.forEach((checkbox) => {
686
+ if (checkbox.disabled) return; // Skip disabled checkboxes
687
+
688
+ const name = checkbox.name;
689
+ const parts = name.split(/[\[\].]/).filter(Boolean);
690
+
691
+ // Check if this checkbox's value is already in updatedData
692
+ // If not, it means it was unchecked
693
+ let current = updatedData;
694
+ let exists = true;
695
+
696
+ for (let i = 0; i < parts.length - 1; i++) {
697
+ if (!current[parts[i]]) {
698
+ exists = false;
699
+ break;
700
+ }
701
+ current = current[parts[i]];
702
+ }
703
+
704
+ const lastPart = parts[parts.length - 1];
705
+ if (exists && !(lastPart in current)) {
706
+ current[lastPart] = false;
707
+ }
708
+ });
709
+
710
+ // Copy over the ID field to ensure it's included
711
+ if (isUpdate && data && data.id) {
712
+ updatedData.id = data.id;
713
+ }
714
+
715
+ try {
716
+ const response = await fetch(window.location.pathname, {
717
+ method: isUpdate ? "PUT" : "POST",
718
+ headers: {
719
+ "Content-Type": "application/json",
720
+ },
721
+ body: JSON.stringify(updatedData),
722
+ });
723
+
724
+ if (response.ok) {
725
+ const result = await response.json();
726
+ console.log("Success:", result);
727
+ showToast(
728
+ "Success",
729
+ `Your data has been ${
730
+ isUpdate ? "updated" : "submitted"
731
+ } successfully!`,
732
+ "success"
733
+ );
734
+ } else {
735
+ const errorText = await response.text();
736
+ console.error("Error:", response.status, response.statusText);
737
+ showToast("Error", `Error ${response.status}: ${errorText}`, "error");
738
+ }
739
+ } catch (error) {
740
+ console.error("Fetch error:", error);
741
+ showToast(
742
+ "Error",
743
+ "An unexpected error occurred. Please try again.",
744
+ "error"
745
+ );
746
+ }
747
+ });
748
+
749
+ // Toast function
750
+ function showToast(title, message, type) {
751
+ const toastContainer =
752
+ document.getElementById("toastContainer") || createToastContainer();
753
+ const toast = document.createElement("div");
754
+ toast.className = `toast ${
755
+ type === "success" ? "border-green-600" : "border-red-600"
756
+ } p-4 mb-4 rounded shadow-lg bg-gray-900 text-gray-100 border-l-4 p-4 rounded-lg shadow-md animate-fadeIn`;
757
+ toast.innerHTML = `
758
+ <strong class="block text-lg">${title}</strong>
759
+ <span class="block text-sm">${message}</span>
760
+ `;
761
+ toastContainer.appendChild(toast);
762
+
763
+ // Automatically remove the toast after 5 seconds
764
+ setTimeout(() => {
765
+ toast.remove();
766
+ }, 5000);
767
+ }
768
+
769
+ function createToastContainer() {
770
+ const container = document.createElement("div");
771
+ container.id = "toastContainer";
772
+ container.className = "fixed top-4 left-4 z-50 space-y-4";
773
+ document.body.appendChild(container);
774
+ return container;
775
+ }
776
+
777
+ document.getElementById("deleteButton").addEventListener("click", async () => {
778
+ if (!data.id) {
779
+ console.error("No ID found for deletion.");
780
+ showToast("Error", "No ID found for deletion.", "error");
781
+ return;
782
+ }
783
+
784
+ const confirmDelete = confirm("Are you sure you want to delete this record?");
785
+ if (!confirmDelete) return;
786
+
787
+ try {
788
+ const response = await fetch(window.location.pathname, {
789
+ method: "DELETE",
790
+ headers: {
791
+ "Content-Type": "application/json",
792
+ },
793
+ });
794
+
795
+ if (response.ok) {
796
+ console.log("Record deleted successfully.");
797
+ showToast("Success", "Record deleted successfully!", "success");
798
+ const currentUrl = window.location.pathname;
799
+ const urlParts = currentUrl.split("/").filter((part) => part !== "");
800
+ urlParts.pop();
801
+ const redirectUrl = "/" + urlParts.join("/") + "/";
802
+
803
+ console.log("Redirecting to:", redirectUrl);
804
+
805
+ setTimeout(() => {
806
+ window.location.href = redirectUrl;
807
+ }, 2000);
808
+ } else {
809
+ const errorText = await response.text();
810
+ console.error("Error:", response.status, response.statusText);
811
+ showToast("Error", `Error ${response.status}: ${errorText}`, "error");
812
+ }
813
+ } catch (error) {
814
+ console.error("Fetch error:", error);
815
+ showToast(
816
+ "Error",
817
+ "An unexpected error occurred. Please try again.",
818
+ "error"
819
+ );
820
+ }
821
+ });
822
+
823
+ // Form Update Real-Time Logger and Debugger
824
+
825
+ // Create a logging container
826
+ function createLogContainer() {
827
+ const existingContainer = document.getElementById("formDataLogger");
828
+ if (existingContainer) return existingContainer;
829
+
830
+ const logContainer = document.createElement("div");
831
+ logContainer.id = "formDataLogger";
832
+ logContainer.className =
833
+ "fixed bottom-4 right-4 w-96 max-h-96 bg-gray-800 border border-gray-600 rounded-lg shadow-lg p-3 overflow-y-auto z-50";
834
+ logContainer.style.maxHeight = "400px";
835
+
836
+ logContainer.innerHTML = `
837
+ <div class="flex justify-between items-center mb-2 pb-2 border-b border-gray-600">
838
+ <h3 class="text-sm font-bold text-white">Form Data Logger</h3>
839
+ <div class="flex gap-2">
840
+ <button id="captureFormState" class="text-xs bg-blue-600 px-2 py-1 rounded hover:bg-blue-700">Capture State</button>
841
+ <button id="clearLogBtn" class="text-xs bg-gray-700 px-2 py-1 rounded hover:bg-gray-600">Clear</button>
842
+ <button id="toggleLogBtn" class="text-xs bg-gray-700 px-2 py-1 rounded hover:bg-gray-600">Hide</button>
843
+ </div>
844
+ </div>
845
+ <div id="logEntries" class="space-y-2 text-xs"></div>
846
+ `;
847
+
848
+ document.body.appendChild(logContainer);
849
+
850
+ // Add button event listeners
851
+ document.getElementById("clearLogBtn").addEventListener("click", () => {
852
+ document.getElementById("logEntries").innerHTML = "";
853
+ });
854
+
855
+ document.getElementById("toggleLogBtn").addEventListener("click", (e) => {
856
+ const logEntries = document.getElementById("logEntries");
857
+ if (logEntries.style.display === "none") {
858
+ logEntries.style.display = "block";
859
+ e.target.textContent = "Hide";
860
+ } else {
861
+ logEntries.style.display = "none";
862
+ e.target.textContent = "Show";
863
+ }
864
+ });
865
+
866
+ document.getElementById("captureFormState").addEventListener("click", () => {
867
+ const currentState = captureFormState();
868
+ logMessage("Form State Snapshot", currentState, "snapshot");
869
+ console.log("Current Form State:", currentState);
870
+ });
871
+
872
+ return logContainer;
873
+ }
874
+
875
+ // Capture current form state (all fields)
876
+ function captureFormState() {
877
+ const form = document.getElementById(isUpdate ? "updateForm" : "createForm");
878
+ if (!form) return null;
879
+
880
+ const formState = {};
881
+
882
+ // Get all input elements
883
+ const inputs = form.querySelectorAll("input, select, textarea");
884
+ inputs.forEach((input) => {
885
+ if (!input.name) return;
886
+
887
+ // Skip disabled inputs if we want to capture only enabled fields
888
+ // if (input.disabled) return;
889
+
890
+ if (input.type === "checkbox") {
891
+ formState[input.name] = input.checked;
892
+ } else {
893
+ formState[input.name] = input.value;
894
+ }
895
+ });
896
+
897
+ // Process the form data into a nested structure
898
+ const structuredData = {};
899
+
900
+ Object.entries(formState).forEach(([key, value]) => {
901
+ // Parse the key to handle nested structures
902
+ const parts = key.split(/[\[\].]/).filter(Boolean);
903
+
904
+ // Start at the root of our data object
905
+ let current = structuredData;
906
+
907
+ // Build the nested structure
908
+ for (let i = 0; i < parts.length; i++) {
909
+ const part = parts[i];
910
+
911
+ if (i === parts.length - 1) {
912
+ // We're at the leaf node, set the actual value
913
+
914
+ // Handle special value conversions
915
+ if (typeof value === "string") {
916
+ if (value === "true" || value === "false") {
917
+ value = value === "true";
918
+ } else if (!isNaN(value) && value !== "") {
919
+ value = Number(value);
920
+ }
921
+ }
922
+
923
+ // Set the value in our data structure
924
+ current[part] = value;
925
+ } else {
926
+ // We're building the nested structure
927
+
928
+ // Check if the next part is a number (array index)
929
+ if (/^\d+$/.test(parts[i + 1])) {
930
+ // Create array if it doesn't exist
931
+ current[part] = current[part] || [];
932
+ } else {
933
+ // Create object if it doesn't exist
934
+ current[part] = current[part] || {};
935
+ }
936
+
937
+ // Move deeper into the structure
938
+ current = current[part];
939
+ }
940
+ }
941
+ });
942
+
943
+ return structuredData;
944
+ }
945
+
946
+ // Log a message to the log container
947
+ function logMessage(title, data, type = "info") {
948
+ const logEntries = document.getElementById("logEntries");
949
+ if (!logEntries) return;
950
+
951
+ const timestamp = new Date().toLocaleTimeString();
952
+ const logEntry = document.createElement("div");
953
+
954
+ // Different styling based on type
955
+ if (type === "error") {
956
+ logEntry.className = "p-2 rounded bg-red-900/50 border-l-4 border-red-600";
957
+ } else if (type === "warning") {
958
+ logEntry.className =
959
+ "p-2 rounded bg-yellow-900/50 border-l-4 border-yellow-600";
960
+ } else if (type === "success") {
961
+ logEntry.className =
962
+ "p-2 rounded bg-green-900/50 border-l-4 border-green-600";
963
+ } else if (type === "snapshot") {
964
+ logEntry.className =
965
+ "p-2 rounded bg-blue-900/50 border-l-4 border-blue-600";
966
+ } else {
967
+ logEntry.className = "p-2 rounded bg-gray-800 border border-gray-700";
968
+ }
969
+
970
+ // Format data object to string
971
+ let dataHtml = "";
972
+ if (data === null || data === undefined) {
973
+ dataHtml = '<span class="text-gray-400">null or undefined</span>';
974
+ } else if (typeof data === "object") {
975
+ try {
976
+ const jsonStr = JSON.stringify(data, null, 2);
977
+ if (jsonStr === "{}") {
978
+ dataHtml = '<span class="text-orange-400">Empty object {}</span>';
979
+ } else {
980
+ dataHtml = `<pre class="text-xs mt-1 p-2 bg-gray-900 rounded overflow-x-auto">${escapeHtml(
981
+ jsonStr
982
+ )}</pre>`;
983
+ }
984
+ } catch (e) {
985
+ dataHtml = `<span class="text-red-400">Error stringifying: ${e.message}</span>`;
986
+ }
987
+ } else {
988
+ dataHtml = `<span>${escapeHtml(String(data))}</span>`;
989
+ }
990
+
991
+ logEntry.innerHTML = `
992
+ <div class="flex justify-between">
993
+ <span class="font-medium text-white">${escapeHtml(title)}</span>
994
+ <span class="text-gray-400 text-xs">${timestamp}</span>
995
+ </div>
996
+ <div class="mt-1">${dataHtml}</div>
997
+ `;
998
+
999
+ logEntries.prepend(logEntry);
1000
+
1001
+ // Limit the number of log entries
1002
+ const maxEntries = 50;
1003
+ while (logEntries.children.length > maxEntries) {
1004
+ logEntries.removeChild(logEntries.lastChild);
1005
+ }
1006
+ }
1007
+
1008
+ // Escape HTML to prevent XSS
1009
+ function escapeHtml(str) {
1010
+ return String(str)
1011
+ .replace(/&/g, "&amp;")
1012
+ .replace(/</g, "&lt;")
1013
+ .replace(/>/g, "&gt;")
1014
+ .replace(/"/g, "&quot;")
1015
+ .replace(/'/g, "&#039;");
1016
+ }
1017
+
1018
+ // Monitor form element changes
1019
+ function attachFormMonitors() {
1020
+ const form = document.getElementById(isUpdate ? "updateForm" : "createForm");
1021
+ if (!form) {
1022
+ logMessage(
1023
+ "Form not found",
1024
+ `Could not find form with ID: ${isUpdate ? "updateForm" : "createForm"}`,
1025
+ "error"
1026
+ );
1027
+ return;
1028
+ }
1029
+
1030
+ // Monitor input changes
1031
+ form.addEventListener("input", (e) => {
1032
+ if (!e.target.name) return;
1033
+
1034
+ const fieldName = e.target.name;
1035
+ const newValue =
1036
+ e.target.type === "checkbox" ? e.target.checked : e.target.value;
1037
+
1038
+ logMessage(`Field Changed: ${fieldName}`, { value: newValue }, "info");
1039
+ });
1040
+
1041
+ // Monitor checkbox toggles
1042
+ form.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => {
1043
+ if (checkbox.id && checkbox.id.includes("_toggle")) {
1044
+ checkbox.addEventListener("change", () => {
1045
+ const targetId = checkbox.id.replace("_toggle", "_content");
1046
+ logMessage(`Toggle Section: ${targetId}`, {
1047
+ checked: checkbox.checked,
1048
+ visible: checkbox.checked,
1049
+ });
1050
+ });
1051
+ }
1052
+ });
1053
+
1054
+ // Intercept form submission
1055
+ const originalSubmit = form.onsubmit;
1056
+ form.onsubmit = async function (e) {
1057
+ e.preventDefault();
1058
+
1059
+ logMessage(
1060
+ "Form Submission Started",
1061
+ {
1062
+ time: new Date().toISOString(),
1063
+ isUpdate: isUpdate || false,
1064
+ },
1065
+ "info"
1066
+ );
1067
+
1068
+ // Capture and log the complete form state
1069
+ const formState = captureFormState();
1070
+ logMessage(
1071
+ "Form Submission Data",
1072
+ formState,
1073
+ formState && Object.keys(formState).length ? "success" : "warning"
1074
+ );
1075
+
1076
+ // Collect FormData entries for debugging
1077
+ const formData = new FormData(form);
1078
+ const formDataEntries = {};
1079
+ for (let [key, value] of formData.entries()) {
1080
+ formDataEntries[key] = value;
1081
+ }
1082
+
1083
+ logMessage("Raw FormData Entries", formDataEntries);
1084
+
1085
+ // Check for empty submission
1086
+ if (!formState || Object.keys(formState).length === 0) {
1087
+ logMessage(
1088
+ "Empty Form Submission",
1089
+ "The form data is empty! Check for form field naming issues.",
1090
+ "error"
1091
+ );
1092
+ }
1093
+
1094
+ // Call the original submit handler if it exists
1095
+ if (originalSubmit) {
1096
+ return originalSubmit.call(this, e);
1097
+ }
1098
+ };
1099
+
1100
+ // Override the existing updateData function if it exists
1101
+ if (typeof window.updateData === "function") {
1102
+ const originalUpdateData = window.updateData;
1103
+ window.updateData = function (data) {
1104
+ logMessage("updateData Called", data, "info");
1105
+ return originalUpdateData.apply(this, arguments);
1106
+ };
1107
+ }
1108
+
1109
+ // Override the populateFormWithExistingData function
1110
+ if (typeof window.populateFormWithExistingData === "function") {
1111
+ const originalPopulate = window.populateFormWithExistingData;
1112
+ window.populateFormWithExistingData = function (data) {
1113
+ logMessage("Populating Form with Data", data, data ? "info" : "warning");
1114
+
1115
+ if (!data || Object.keys(data).length === 0) {
1116
+ logMessage(
1117
+ "Empty Data for Form Population",
1118
+ "This might explain missing form values",
1119
+ "warning"
1120
+ );
1121
+ }
1122
+
1123
+ // Call original function
1124
+ const result = originalPopulate.apply(this, arguments);
1125
+
1126
+ // Capture the form state after population
1127
+ setTimeout(() => {
1128
+ const formState = captureFormState();
1129
+ logMessage("Form State After Population", formState, "snapshot");
1130
+
1131
+ // Check if fields are disabled when they shouldn't be
1132
+ const disabledInputs = form.querySelectorAll(
1133
+ "input:disabled, select:disabled, textarea:disabled"
1134
+ );
1135
+ if (disabledInputs.length > 0) {
1136
+ const disabledFields = Array.from(disabledInputs)
1137
+ .map((el) => el.name)
1138
+ .filter(Boolean);
1139
+ if (disabledFields.length > 0) {
1140
+ logMessage("Found Disabled Fields", disabledFields, "warning");
1141
+ }
1142
+ }
1143
+ }, 100);
1144
+
1145
+ return result;
1146
+ };
1147
+ }
1148
+
1149
+ // Debug the fetch call
1150
+ const originalFetch = window.fetch;
1151
+ window.fetch = function (...args) {
1152
+ const [url, options] = args;
1153
+
1154
+ if (
1155
+ options &&
1156
+ options.method &&
1157
+ (options.method === "POST" || options.method === "PUT")
1158
+ ) {
1159
+ logMessage(
1160
+ `Fetch Request (${options.method})`,
1161
+ {
1162
+ url: url,
1163
+ method: options.method,
1164
+ bodyData: options.body ? JSON.parse(options.body) : null,
1165
+ },
1166
+ "info"
1167
+ );
1168
+ }
1169
+
1170
+ const fetchPromise = originalFetch.apply(this, args);
1171
+
1172
+ fetchPromise
1173
+ .then((response) => {
1174
+ if (!response.ok) {
1175
+ logMessage(
1176
+ "Fetch Error",
1177
+ {
1178
+ status: response.status,
1179
+ statusText: response.statusText,
1180
+ },
1181
+ "error"
1182
+ );
1183
+ } else {
1184
+ logMessage(
1185
+ "Fetch Success",
1186
+ {
1187
+ status: response.status,
1188
+ },
1189
+ "success"
1190
+ );
1191
+ }
1192
+ })
1193
+ .catch((error) => {
1194
+ logMessage(
1195
+ "Fetch Failed",
1196
+ {
1197
+ error: error.message,
1198
+ },
1199
+ "error"
1200
+ );
1201
+ });
1202
+
1203
+ return fetchPromise;
1204
+ };
1205
+ }
1206
+
1207
+ // Initialize the logger
1208
+ function initFormLogger() {
1209
+ createLogContainer();
1210
+ attachFormMonitors();
1211
+
1212
+ // Log initial system state
1213
+ logMessage(
1214
+ "Form Logger Initialized",
1215
+ {
1216
+ url: window.location.href,
1217
+ isUpdate: typeof isUpdate !== "undefined" ? isUpdate : false,
1218
+ time: new Date().toISOString(),
1219
+ },
1220
+ "info"
1221
+ );
1222
+
1223
+ // Check if existingData is available
1224
+ if (typeof existingData !== "undefined") {
1225
+ logMessage(
1226
+ "Existing Data Found",
1227
+ existingData,
1228
+ existingData ? "info" : "warning"
1229
+ );
1230
+ } else {
1231
+ logMessage("No Existing Data", "existingData is not defined", "warning");
1232
+ }
1233
+
1234
+ // Log initial schema
1235
+ if (typeof schema !== "undefined") {
1236
+ logMessage("Schema Structure", {
1237
+ hasFields: schema.fields ? true : false,
1238
+ nestedTypes: schema.$ ? Object.keys(schema.$) : [],
1239
+ });
1240
+ }
1241
+
1242
+ // Check form action is correctly set
1243
+ setTimeout(() => {
1244
+ const form = document.getElementById(
1245
+ isUpdate ? "updateForm" : "createForm"
1246
+ );
1247
+ if (form) {
1248
+ logMessage("Form Configuration", {
1249
+ action: form.action || window.location.pathname,
1250
+ method: form.method || "POST/PUT via JS",
1251
+ id: form.id,
1252
+ containsInputs:
1253
+ form.querySelectorAll("input, select, textarea").length > 0,
1254
+ });
1255
+ }
1256
+ }, 500);
1257
+ }
1258
+
1259
+ // Run on page load
1260
+ document.addEventListener("DOMContentLoaded", function () {
1261
+ setTimeout(initFormLogger, 100); // Slight delay to ensure page is fully loaded
1262
+ });
1263
+
1264
+ // For immediate execution if the page is already loaded
1265
+ if (
1266
+ document.readyState === "complete" ||
1267
+ document.readyState === "interactive"
1268
+ ) {
1269
+ setTimeout(initFormLogger, 100);
1270
+ }