@unvired/turboforms-embed-sdk 2.0.73 → 2.0.74

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.
@@ -6758,13 +6758,21 @@
6758
6758
  font-weight: 600 !important;
6759
6759
  transition: all 0.2s ease !important;
6760
6760
  display: inline-block !important;
6761
+ padding: 8px 16px !important;
6762
+ margin-top: 4px !important;
6763
+ border-radius: 6px !important;
6764
+ touch-action: manipulation !important;
6765
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1) !important;
6766
+ user-select: none !important;
6767
+ -webkit-user-select: none !important;
6761
6768
  }
6762
6769
 
6763
- .tf-embed-sdk .formio-component-signature .signature-click-hint .signature-click-action:hover {
6770
+ .tf-embed-sdk .formio-component-signature .signature-click-hint .signature-click-action:hover,
6771
+ .tf-embed-sdk .formio-component-signature .signature-click-hint .signature-click-action:active {
6764
6772
  color: var(--tf-primary-color) !important;
6765
6773
  transform: scale(1.05) !important;
6766
6774
  opacity: 0.9 !important;
6767
- /* Slight pop effect */
6775
+ background: rgba(0, 0, 0, 0.05) !important;
6768
6776
  }
6769
6777
 
6770
6778
  /* ── ICON REMAPPING (Bootstrap Icons fallback) ── */
@@ -27232,9 +27240,9 @@ function showLocationError(message) {
27232
27240
  </script>
27233
27241
  <script>
27234
27242
  document.addEventListener("openCamera", function (event) {
27235
- logger.info('[TF_SDK:229] openCamera event received', { controlId: event.detail.controlId });
27243
+ const controlId = event.detail ? event.detail.controlId : null;
27244
+ logger.info('[TF_SDK:229] ✅ openCamera event received', { controlId });
27236
27245
 
27237
- const { controlId } = event.detail;
27238
27246
  window.compId = controlId;
27239
27247
 
27240
27248
  // Check if we're in React Native WebView on iOS or Android
@@ -27244,13 +27252,14 @@ document.addEventListener("openCamera", function (event) {
27244
27252
 
27245
27253
  // If React Native on iOS/Android, send event to native side
27246
27254
  if (isReactNative) {
27247
- logger.info('[TF_SDK:230] ✅ Forwarding to React Native side');
27255
+ logger.info('[TF_SDK:230] ✅ Forwarding to React Native WebView');
27248
27256
  window.ReactNativeWebView.postMessage(JSON.stringify({
27249
27257
  type: 'openCamera',
27250
27258
  controlId: controlId
27251
27259
  }));
27252
27260
  return;
27253
27261
  }
27262
+
27254
27263
  logger.info('[TF_SDK:231] ✅ Opening browser camera modal');
27255
27264
  openCameraModal();
27256
27265
  });
@@ -27273,15 +27282,14 @@ function openCameraModal() {
27273
27282
  const modalHTML = `
27274
27283
  <div id="cameraModal">
27275
27284
  <div id="cameraContainer">
27276
- <video id="cameraVideo" playsinline webkit-playsinline muted style="opacity: 0; pointer-events: none;"></video>
27277
27285
  <div id="cameraLoader">
27278
27286
  <div class="loader-spinner"></div>
27279
27287
  <p class="loader-text">Starting camera...</p>
27280
27288
  </div>
27281
27289
  <div id="cameraOverlay">
27282
- <button id="closeCameraBtn" class="camera-close-btn">
27290
+ <button id="closeCameraBtn" class="camera-close-btn" type="button" aria-label="Close">
27283
27291
  <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
27284
- <path d="M18 6L6 18M6 6L18 18" stroke="white" stroke-width="2" stroke-linecap="round"/>
27292
+ <path d="M18 6L6 18M6 6L18 18" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
27285
27293
  </svg>
27286
27294
  </button>
27287
27295
  <div class="camera-select-container">
@@ -27305,8 +27313,7 @@ function openCameraModal() {
27305
27313
  startCameraStream(e.target.value);
27306
27314
  });
27307
27315
 
27308
- // Small delay to ensure DOM is ready
27309
- setTimeout(startCameraStream, 100);
27316
+ startCameraStream();
27310
27317
  }
27311
27318
 
27312
27319
  async function startCameraStream(deviceId = null) {
@@ -27320,9 +27327,6 @@ async function startCameraStream(deviceId = null) {
27320
27327
  }
27321
27328
 
27322
27329
  try {
27323
- const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (window.platform && window.platform.iosPlatform);
27324
-
27325
- // Check if API is supported
27326
27330
  if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
27327
27331
  const errorMsg = "Camera API is not supported. Please ensure you are using HTTPS or localhost.";
27328
27332
  alert(errorMsg);
@@ -27330,37 +27334,22 @@ async function startCameraStream(deviceId = null) {
27330
27334
  return;
27331
27335
  }
27332
27336
 
27333
- const video = document.getElementById("cameraVideo");
27334
- if (!video) {
27335
- alert("Camera video element not found.");
27337
+ const container = document.getElementById("cameraContainer");
27338
+ if (!container) {
27336
27339
  return;
27337
27340
  }
27338
27341
 
27339
- // Simplified approach: Ask for camera directly.
27340
- // This allows the browser to handle permissions correctly on all platforms (Windows/Android/iOS)
27341
- // and defaults to the system default camera (webcam) on laptops.
27342
- // Simplified approach: Ask for camera directly.
27343
- // We race against a timeout because WebViews often hang indefinitely if permissions aren't handled.
27344
- // OPTIMIZATION: Request 16:9 aspect ratio (1280x720) instead of 4:3 (640x480).
27345
- // This reduces the "zoom" effect on modern mobile screens which are typically 16:9 or taller.
27346
27342
  const constraints = {
27347
- video: {
27348
- width: { ideal: 1280 },
27349
- height: { ideal: 720 }
27350
- }
27343
+ video: deviceId
27344
+ ? { deviceId: { exact: deviceId } }
27345
+ : { facingMode: { ideal: "environment" } }
27351
27346
  };
27352
27347
 
27353
- if (deviceId) {
27354
- constraints.video.deviceId = { exact: deviceId };
27355
- } else {
27356
- constraints.video.facingMode = "environment";
27357
- }
27358
-
27359
- const streamPromise = navigator.mediaDevices.getUserMedia(constraints);
27360
27348
  logger.info('[TF_SDK:233] ✅ Requesting camera stream...', { deviceId });
27361
27349
 
27350
+ const streamPromise = navigator.mediaDevices.getUserMedia(constraints);
27362
27351
  const timeoutPromise = new Promise((_, reject) =>
27363
- setTimeout(() => reject(new Error("PermissionTimeout")), 6000)
27352
+ setTimeout(() => reject(new Error("PermissionTimeout")), 10000)
27364
27353
  );
27365
27354
 
27366
27355
  try {
@@ -27372,19 +27361,60 @@ async function startCameraStream(deviceId = null) {
27372
27361
  throw err;
27373
27362
  }
27374
27363
 
27375
- // iOS Specific: Ensure inline playback setup BEFORE stream assignment
27376
- if (isIOS) {
27377
- video.setAttribute("playsinline", "");
27378
- video.setAttribute("webkit-playsinline", "");
27379
- video.playsInline = true; // Set property as well
27364
+ // Remove any previous video element
27365
+ let video = document.getElementById("cameraVideo");
27366
+ if (video) {
27367
+ video.remove();
27380
27368
  }
27381
27369
 
27382
- // Mute video to prevent audio feedback and allow autoplay in restrictive policies
27370
+ // Dynamically create <video> element with playsinline and muted attributes for iOS WebKit
27371
+ video = document.createElement("video");
27372
+ video.id = "cameraVideo";
27373
+ video.setAttribute("playsinline", "");
27374
+ video.setAttribute("webkit-playsinline", "");
27375
+ video.setAttribute("autoplay", "");
27376
+ video.setAttribute("muted", "");
27377
+ video.setAttribute("disablepictureinpicture", "true");
27378
+ video.setAttribute("controlslist", "nodownload nofullscreen noremoteplayback");
27379
+ video.style.cssText = "width: 100%; height: 100%; object-fit: cover; background: #000; transition: opacity 0.3s ease; opacity: 0; pointer-events: none;";
27380
+ video.playsInline = true;
27383
27381
  video.muted = true;
27382
+ video.autoplay = true;
27383
+ video.controls = false;
27384
+ if ('disablePictureInPicture' in video) {
27385
+ video.disablePictureInPicture = true;
27386
+ }
27387
+
27388
+ // Intercept and enforce inline mode for iOS WebKit
27389
+ video.addEventListener("playing", () => {
27390
+ if (typeof video.webkitSetPresentationMode === 'function' && video.webkitPresentationMode !== 'inline') {
27391
+ video.webkitSetPresentationMode('inline');
27392
+ }
27393
+ });
27394
+ video.addEventListener("webkitbeginfullscreen", () => {
27395
+ if (typeof video.webkitExitFullscreen === 'function') {
27396
+ video.webkitExitFullscreen();
27397
+ }
27398
+ if (typeof video.webkitSetPresentationMode === 'function') {
27399
+ video.webkitSetPresentationMode('inline');
27400
+ }
27401
+ });
27402
+ video.addEventListener("webkitpresentationmodechanged", () => {
27403
+ if (video.webkitPresentationMode === 'fullscreen') {
27404
+ if (typeof video.webkitSetPresentationMode === 'function') {
27405
+ video.webkitSetPresentationMode('inline');
27406
+ }
27407
+ }
27408
+ });
27409
+
27410
+ // Insert video element at top of cameraContainer
27411
+ container.insertBefore(video, container.firstChild);
27412
+
27413
+ let playInitiated = false;
27414
+ const handleStreamLive = () => {
27415
+ if (playInitiated) return;
27416
+ playInitiated = true;
27384
27417
 
27385
- // Wait for video to start playing, then hide loader
27386
- video.onloadedmetadata = () => {
27387
- // Once permission is granted and stream starts, listing devices will return labels
27388
27418
  populateCameraDevices();
27389
27419
 
27390
27420
  const showUI = () => {
@@ -27409,29 +27439,34 @@ async function startCameraStream(deviceId = null) {
27409
27439
  cameraStarted = true;
27410
27440
  logger.info('[TF_SDK:235] ✅ Camera stream live');
27411
27441
  }).catch(err => {
27412
- logger.error("[TF_SDK:236] Video play error:", err);
27413
- if (!video.paused) {
27414
- logger.info("[TF_SDK:237] Video is playing despite error. Continuing.");
27415
- showUI();
27416
- cameraStarted = true;
27417
- } else {
27418
- alert("Failed to start video preview: " + err.message);
27419
- closeCameraModal();
27420
- }
27442
+ logger.warn("[TF_SDK:236] ⚠️ Video play rejection handled:", err);
27443
+ showUI();
27444
+ cameraStarted = true;
27421
27445
  });
27422
27446
  };
27423
27447
 
27424
- // Ensure video doesn't stay paused if it somehow exits fullscreen or gets interrupted
27448
+ video.onloadedmetadata = handleStreamLive;
27449
+ video.onloadeddata = handleStreamLive;
27450
+ video.oncanplay = handleStreamLive;
27451
+
27452
+ video.srcObject = videoStream;
27453
+
27454
+ if (video.readyState >= 1) {
27455
+ handleStreamLive();
27456
+ } else {
27457
+ video.play().then(() => {
27458
+ handleStreamLive();
27459
+ }).catch(e => {
27460
+ logger.info("[TF_SDK:234b] Direct play attempt promise caught:", e?.message);
27461
+ });
27462
+ }
27463
+
27425
27464
  video.onpause = () => {
27426
- if (cameraStarted && video.srcObject) {
27427
- // Only try to resume if we expect it to be playing
27428
- video.play().catch(e => logger.info("[TF_SDK:234] ✅ Resume error", e));
27465
+ if (cameraStarted && video.srcObject && video.paused) {
27466
+ video.play().catch(e => logger.info("[TF_SDK:234] Resume handled:", e?.message));
27429
27467
  }
27430
27468
  };
27431
27469
 
27432
- // Assign stream
27433
- video.srcObject = videoStream;
27434
-
27435
27470
  } catch (err) {
27436
27471
  logger.error("[TF_SDK:238] ❌ Camera start error:", err);
27437
27472
  let errorMessage = err.message || "Failed to start camera";
@@ -27683,12 +27718,13 @@ function injectCameraModalStyles() {
27683
27718
  position: absolute;
27684
27719
  top: 20px;
27685
27720
  left: 20px;
27686
- width: 44px;
27687
- height: 44px;
27721
+ width: 48px;
27722
+ height: 48px;
27688
27723
  border: none;
27689
27724
  border-radius: 50%;
27690
- background: rgba(0, 0, 0, 0.5);
27725
+ background: rgba(0, 0, 0, 0.6);
27691
27726
  backdrop-filter: blur(10px);
27727
+ -webkit-backdrop-filter: blur(10px);
27692
27728
  cursor: pointer;
27693
27729
  display: flex;
27694
27730
  align-items: center;
@@ -27696,9 +27732,10 @@ function injectCameraModalStyles() {
27696
27732
  transition: background 0.2s;
27697
27733
  pointer-events: auto;
27698
27734
  z-index: 10001;
27735
+ -webkit-tap-highlight-color: transparent;
27699
27736
  }
27700
- .camera-close-btn:hover {
27701
- background: rgba(0, 0, 0, 0.7);
27737
+ .camera-close-btn:hover, .camera-close-btn:active {
27738
+ background: rgba(0, 0, 0, 0.8);
27702
27739
  }
27703
27740
 
27704
27741
  .camera-select-container {
@@ -27718,6 +27755,7 @@ function injectCameraModalStyles() {
27718
27755
  border-radius: 20px;
27719
27756
  font-size: 14px;
27720
27757
  backdrop-filter: blur(10px);
27758
+ -webkit-backdrop-filter: blur(10px);
27721
27759
  outline: none;
27722
27760
  cursor: pointer;
27723
27761
  appearance: none;
@@ -27739,6 +27777,7 @@ function injectCameraModalStyles() {
27739
27777
  bottom: 40px;
27740
27778
  left: 50%;
27741
27779
  transform: translateX(-50%);
27780
+ -webkit-transform: translateX(-50%);
27742
27781
  cursor: pointer;
27743
27782
  pointer-events: auto;
27744
27783
  z-index: 10001;
@@ -27753,12 +27792,13 @@ function injectCameraModalStyles() {
27753
27792
  .capture-ring {
27754
27793
  width: 80px;
27755
27794
  height: 80px;
27756
- border: 4px solid rgba(255, 255, 255, 0.3);
27795
+ border: 4px solid rgba(255, 255, 255, 0.4);
27757
27796
  border-radius: 50%;
27758
27797
  display: flex;
27759
27798
  align-items: center;
27760
27799
  justify-content: center;
27761
27800
  transition: all 0.2s ease;
27801
+ background: rgba(0, 0, 0, 0.2);
27762
27802
  }
27763
27803
 
27764
27804
  .capture-inner {
@@ -27769,13 +27809,15 @@ function injectCameraModalStyles() {
27769
27809
  transition: all 0.2s ease;
27770
27810
  }
27771
27811
 
27772
- #captureButton:hover .capture-ring {
27773
- border-color: rgba(255, 255, 255, 0.6);
27774
- transform: scale(1.1);
27812
+ #captureButton:hover .capture-ring, #captureButton:active .capture-ring {
27813
+ border-color: rgba(255, 255, 255, 0.8);
27814
+ transform: scale(1.05);
27815
+ -webkit-transform: scale(1.05);
27775
27816
  }
27776
27817
 
27777
- #captureButton:hover .capture-inner {
27818
+ #captureButton:hover .capture-inner, #captureButton:active .capture-inner {
27778
27819
  transform: scale(0.9);
27820
+ -webkit-transform: scale(0.9);
27779
27821
  }
27780
27822
 
27781
27823
  #captureButton.capturing .capture-ring {
@@ -27786,6 +27828,7 @@ function injectCameraModalStyles() {
27786
27828
  #captureButton.capturing .capture-inner {
27787
27829
  background: #4CAF50;
27788
27830
  transform: scale(0.7);
27831
+ -webkit-transform: scale(0.7);
27789
27832
  }
27790
27833
 
27791
27834
  @keyframes captureFlash {
@@ -27794,21 +27837,24 @@ function injectCameraModalStyles() {
27794
27837
  100% { transform: scale(1); }
27795
27838
  }
27796
27839
 
27797
- /* Mobile optimizations */
27840
+ /* Mobile & safe area inset optimizations */
27798
27841
  @media (max-width: 768px) {
27799
27842
  .camera-close-btn {
27800
- top: env(safe-area-inset-top, 20px);
27801
- top: max(20px, env(safe-area-inset-top));
27843
+ top: 20px;
27844
+ top: calc(20px + constant(safe-area-inset-top));
27845
+ top: calc(20px + env(safe-area-inset-top));
27802
27846
  }
27803
27847
 
27804
27848
  #captureButton {
27805
- bottom: env(safe-area-inset-bottom, 40px);
27806
- bottom: max(40px, env(safe-area-inset-bottom));
27849
+ bottom: 40px;
27850
+ bottom: calc(40px + constant(safe-area-inset-bottom));
27851
+ bottom: calc(40px + env(safe-area-inset-bottom));
27807
27852
  }
27808
27853
 
27809
27854
  .camera-select-container {
27810
- top: env(safe-area-inset-top, 20px);
27811
- top: max(20px, env(safe-area-inset-top));
27855
+ top: 20px;
27856
+ top: calc(20px + constant(safe-area-inset-top));
27857
+ top: calc(20px + env(safe-area-inset-top));
27812
27858
  }
27813
27859
  }
27814
27860
 
@@ -29120,8 +29166,6 @@ class SmartStorage {
29120
29166
  }
29121
29167
  };
29122
29168
 
29123
- reader.onerror = (e) => reject(e);
29124
-
29125
29169
  reader.readAsDataURL(file);
29126
29170
  });
29127
29171
  }
@@ -29136,17 +29180,23 @@ class SmartStorage {
29136
29180
  const trans = db.transaction(["Files"], "readwrite");
29137
29181
  const store = trans.objectStore("Files");
29138
29182
 
29139
- const req = store.delete(Id);
29183
+ const getReq = store.get(Id);
29140
29184
 
29141
- req.onsuccess = () => {
29142
- trans.oncomplete = () => {
29143
- logger.info("[TF_SDK:217] ✅ File deleted from IndexedDB", { Id });
29144
- flag = true;
29145
- resolve(true);
29146
- };
29185
+ getReq.onsuccess = () => {
29186
+ const record = getReq.result;
29187
+ if (record) {
29188
+ record.mode = "D"; // Mark as Deleted ('D')
29189
+ store.put(record, Id);
29190
+ } else {
29191
+ store.put({ id: Id, mode: "D" }, Id);
29192
+ }
29193
+ };
29194
+
29195
+ trans.oncomplete = () => {
29196
+ logger.info("[TF_SDK:217] ✅ File marked as deleted (mode: D) in IndexedDB", { Id });
29197
+ resolve(true);
29147
29198
  };
29148
29199
 
29149
- req.onerror = (e) => reject(e);
29150
29200
  trans.onerror = (e) => reject(e);
29151
29201
  } else {
29152
29202
  resolve(true);
@@ -29154,6 +29204,86 @@ class SmartStorage {
29154
29204
  });
29155
29205
  }
29156
29206
 
29207
+ /**
29208
+ * Update an existing file in IndexedDB in-place using the same key.
29209
+ * Used when an image is edited (annotated/cropped) — keeps the same id
29210
+ * and sets mode to 'A'.
29211
+ */
29212
+ async updateFile(existingId, file, fileName, progressCallback) {
29213
+ logger.info("[TF_SDK:223] ✅ updateFile() called", { existingId, fileName, size: (file.size / 1024).toFixed(1) + " KB", type: file.type });
29214
+ if (!existingId) {
29215
+ logger.warn("[TF_SDK:223a] ⚠️ updateFile: no existingId provided, falling back to uploadFile");
29216
+ return this.uploadFile(file, fileName, null, progressCallback);
29217
+ }
29218
+
29219
+ const db = await this.createDb();
29220
+ const reader = new FileReader();
29221
+
29222
+ return new Promise((resolve, reject) => {
29223
+ if (typeof progressCallback === "function") {
29224
+ progressCallback({ loaded: 0, total: file.size });
29225
+ }
29226
+
29227
+ reader.onload = (e) => {
29228
+ const blobObject = new Blob([file], { type: file.type });
29229
+ const url = e.target.result;
29230
+
29231
+ const data = {
29232
+ id: existingId,
29233
+ data: blobObject,
29234
+ name: fileName,
29235
+ size: file.size,
29236
+ type: file.type,
29237
+ url,
29238
+ mode: "A", // Mode 'A' for added/updated
29239
+ };
29240
+
29241
+ if (this.isPlatform()) {
29242
+ const trans = db.transaction(["Files"], "readwrite");
29243
+ const store = trans.objectStore("Files");
29244
+ const putReq = store.put(data, existingId);
29245
+
29246
+ putReq.onerror = (e) => {
29247
+ logger.error("[TF_SDK:224] ❌ Error updating file in IndexedDB:", e);
29248
+ reject(e);
29249
+ };
29250
+
29251
+ trans.onerror = (e) => reject(e);
29252
+
29253
+ trans.oncomplete = () => {
29254
+ logger.info("[TF_SDK:225] ✅ File updated in IndexedDB", { existingId, fileName });
29255
+ if (typeof progressCallback === "function") {
29256
+ progressCallback({ loaded: file.size, total: file.size });
29257
+ }
29258
+ resolve({
29259
+ storage: "SmartStorage",
29260
+ name: fileName,
29261
+ size: file.size,
29262
+ type: file.type,
29263
+ url,
29264
+ id: existingId,
29265
+ mode: "A",
29266
+ });
29267
+ };
29268
+ } else {
29269
+ resolve({
29270
+ storage: "SmartStorage",
29271
+ name: fileName,
29272
+ size: file.size,
29273
+ type: file.type,
29274
+ url,
29275
+ id: existingId,
29276
+ mode: "A",
29277
+ });
29278
+ }
29279
+ };
29280
+
29281
+ reader.onerror = (e) => reject(e);
29282
+
29283
+ reader.readAsDataURL(file);
29284
+ });
29285
+ }
29286
+
29157
29287
  // Download File from indexedDb
29158
29288
  async downloadFile(file) {
29159
29289
  logger.info("[TF_SDK:218] ✅ downloadFile() called", { id: file.id, name: file.originalName || file.name });
@@ -29351,6 +29481,168 @@ const getComponentSavedTypes = FormioUtils.getComponentSavedTypes;
29351
29481
  const componentValueTypes = FormioUtils.componentValueTypes;
29352
29482
  let replace = false;
29353
29483
 
29484
+ /**
29485
+ * Directly update an existing file entry in the Forms-Attachments IndexedDB store
29486
+ * using the same key (existingId), so the edited image overwrites the original in-place.
29487
+ * This bypasses Formio's FileService wrapper which doesn't expose an update method.
29488
+ *
29489
+ * @param {string} existingId - The UUID key of the record to overwrite
29490
+ * @param {Blob} blob - The new (edited) image blob
29491
+ * @param {string} fileName - Original file name
29492
+ * @returns {Promise<{id, url, name, size, type, mode}>}
29493
+ */
29494
+ function updateFileInIndexedDB(existingId, blob, fileName) {
29495
+ logger.info(`[TF_SDK:IDB-U0] ✅ updateFileInIndexedDB called for id: ${existingId}, file: ${fileName}`);
29496
+
29497
+ if (!existingId) {
29498
+ return Promise.reject(new Error('[TF_SDK] existingId is required for updateFileInIndexedDB'));
29499
+ }
29500
+ if (!blob) {
29501
+ return Promise.reject(new Error('[TF_SDK] blob is required for updateFileInIndexedDB'));
29502
+ }
29503
+
29504
+ return new Promise((resolve, reject) => {
29505
+ const openReq = indexedDB.open('Forms-Attachments', 3);
29506
+
29507
+ openReq.onupgradeneeded = (e) => {
29508
+ const db = e.target.result;
29509
+ if (!db.objectStoreNames.contains('Files')) {
29510
+ db.createObjectStore('Files');
29511
+ }
29512
+ };
29513
+
29514
+ openReq.onerror = () => reject(openReq.error);
29515
+
29516
+ openReq.onsuccess = (e) => {
29517
+ const db = e.target.result;
29518
+
29519
+ if (!db.objectStoreNames.contains('Files')) {
29520
+ db.close();
29521
+ return reject(new Error('[TF_SDK] Files store not found in IndexedDB'));
29522
+ }
29523
+
29524
+ const reader = new FileReader();
29525
+ reader.onerror = () => reject(reader.error);
29526
+
29527
+ reader.onload = (evt) => {
29528
+ const url = evt.target.result;
29529
+
29530
+ const data = {
29531
+ id: existingId,
29532
+ data: blob,
29533
+ name: fileName,
29534
+ size: blob.size,
29535
+ type: blob.type,
29536
+ url: url,
29537
+ mode: 'A', // Mode 'A' for added/edited
29538
+ };
29539
+
29540
+ try {
29541
+ const tx = db.transaction(['Files'], 'readwrite');
29542
+ const store = tx.objectStore('Files');
29543
+
29544
+ const putReq = store.put(data, existingId);
29545
+
29546
+ putReq.onerror = () => {
29547
+ logger.error('[TF_SDK:IDB-U1] ❌ updateFileInIndexedDB put failed:', putReq.error);
29548
+ db.close();
29549
+ reject(putReq.error);
29550
+ };
29551
+
29552
+ tx.oncomplete = () => {
29553
+ logger.info(`[TF_SDK:IDB-U2] ✅ IndexedDB record updated in-place (mode=A): id=${existingId}, file=${fileName}`);
29554
+ db.close();
29555
+ resolve({ id: existingId, url, name: fileName, size: blob.size, type: blob.type, mode: 'A' });
29556
+ };
29557
+
29558
+ tx.onerror = () => {
29559
+ logger.error('[TF_SDK:IDB-U3] ❌ updateFileInIndexedDB transaction error:', tx.error);
29560
+ db.close();
29561
+ reject(tx.error);
29562
+ };
29563
+
29564
+ tx.onabort = () => {
29565
+ db.close();
29566
+ reject(tx.error || new Error('Transaction aborted'));
29567
+ };
29568
+
29569
+ } catch (err) {
29570
+ logger.error('[TF_SDK:IDB-U4] ❌ Exception during transaction:', err);
29571
+ db.close();
29572
+ reject(err);
29573
+ }
29574
+ };
29575
+
29576
+ reader.readAsDataURL(blob);
29577
+ };
29578
+ });
29579
+ }
29580
+
29581
+ function deleteFileInIndexedDB(existingId) {
29582
+ logger.info(`[TF_SDK:IDB-D0] ✅ deleteFileInIndexedDB called for id: ${existingId}`);
29583
+
29584
+ if (!existingId) {
29585
+ return Promise.reject(new Error('[TF_SDK] existingId is required for deleteFileInIndexedDB'));
29586
+ }
29587
+
29588
+ return new Promise((resolve, reject) => {
29589
+ const openReq = indexedDB.open('Forms-Attachments', 3);
29590
+
29591
+ openReq.onupgradeneeded = (e) => {
29592
+ const db = e.target.result;
29593
+ if (!db.objectStoreNames.contains('Files')) {
29594
+ db.createObjectStore('Files');
29595
+ }
29596
+ };
29597
+
29598
+ openReq.onerror = () => reject(openReq.error);
29599
+
29600
+ openReq.onsuccess = (e) => {
29601
+ const db = e.target.result;
29602
+
29603
+ if (!db.objectStoreNames.contains('Files')) {
29604
+ db.close();
29605
+ return reject(new Error('[TF_SDK] Files store not found in IndexedDB'));
29606
+ }
29607
+
29608
+ try {
29609
+ const tx = db.transaction(['Files'], 'readwrite');
29610
+ const store = tx.objectStore('Files');
29611
+ const getReq = store.get(existingId);
29612
+
29613
+ getReq.onsuccess = () => {
29614
+ const record = getReq.result || { id: existingId };
29615
+ record.mode = 'D'; // Mark mode as Deleted ('D')
29616
+ store.put(record, existingId);
29617
+ };
29618
+
29619
+ tx.oncomplete = () => {
29620
+ logger.info(`[TF_SDK:IDB-D2] ✅ IndexedDB record marked mode='D': id=${existingId}`);
29621
+ db.close();
29622
+ resolve({ id: existingId, mode: 'D' });
29623
+ };
29624
+
29625
+ tx.onerror = () => {
29626
+ logger.error('[TF_SDK:IDB-D3] ❌ deleteFileInIndexedDB transaction error:', tx.error);
29627
+ db.close();
29628
+ reject(tx.error);
29629
+ };
29630
+
29631
+ } catch (err) {
29632
+ logger.error('[TF_SDK:IDB-D4] ❌ Exception during transaction:', err);
29633
+ db.close();
29634
+ reject(err);
29635
+ }
29636
+ };
29637
+ });
29638
+ }
29639
+
29640
+ if (typeof window !== 'undefined') {
29641
+ window.updateFileInIndexedDB = updateFileInIndexedDB;
29642
+ window.deleteFileInIndexedDB = deleteFileInIndexedDB;
29643
+ }
29644
+
29645
+
29354
29646
  let htmlCanvasElement;
29355
29647
  if (typeof window !== 'undefined') {
29356
29648
  htmlCanvasElement = window.HTMLCanvasElement;
@@ -29611,31 +29903,8 @@ class SmartFileComponent extends FieldComponent {
29611
29903
  }
29612
29904
 
29613
29905
  startVideo() {
29614
-
29615
- this.getVideoStream()
29616
- .then((stream) => {
29617
- this.videoStream = stream;
29618
- const {
29619
- videoPlayer
29620
- } = this.refs;
29621
- if (!videoPlayer) {
29622
- logger.warn('[TF_SDK:72] ⚠️ Video player not found in template.');
29623
- this.cameraMode = false;
29624
- this.redraw();
29625
- return;
29626
- }
29627
-
29628
- videoPlayer.srcObject = stream;
29629
- const width = parseInt(this.component.webcamSize) || 320;
29630
- videoPlayer.setAttribute('width', width);
29631
- videoPlayer.play();
29632
-
29633
- })
29634
- .catch((err) => {
29635
- logger.error(err);
29636
- this.cameraMode = false;
29637
- this.redraw();
29638
- });
29906
+ // Disabled in favor of CustomCamera modal to prevent iOS WebKit native video player popup
29907
+ this._cameraMode = false;
29639
29908
  }
29640
29909
 
29641
29910
  stopVideo() {
@@ -29643,27 +29912,16 @@ class SmartFileComponent extends FieldComponent {
29643
29912
  this.stopVideoStream(this.videoStream);
29644
29913
  this.videoStream = null;
29645
29914
  }
29915
+ this._cameraMode = false;
29646
29916
  }
29647
29917
 
29648
29918
  takePicture() {
29649
- const {
29650
- videoPlayer
29651
- } = this.refs;
29652
- if (!videoPlayer) {
29653
- logger.warn('[TF_SDK:73] ⚠️ Video player not found in template.');
29654
- this.cameraMode = false;
29655
- this.redraw();
29656
- return;
29657
- }
29658
-
29659
- this.getFrame(videoPlayer)
29660
- .then((frame) => {
29661
- frame.name = `photo-${Date.now()}.png`;
29662
- replace = false;
29663
- this.processAndUpload([frame]);
29664
- this.cameraMode = false;
29665
- this.redraw();
29666
- });
29919
+ // Forward to CustomCamera modal
29920
+ let id = this.id;
29921
+ let eventCustom = new CustomEvent("openCamera", {
29922
+ detail: { controlId: id }
29923
+ });
29924
+ document.dispatchEvent(eventCustom);
29667
29925
  }
29668
29926
 
29669
29927
  browseFiles(attrs = {}) {
@@ -29693,16 +29951,11 @@ class SmartFileComponent extends FieldComponent {
29693
29951
  }
29694
29952
 
29695
29953
  set cameraMode(value) {
29696
- this._cameraMode = value;
29697
- if (value) {
29698
- this.startVideo();
29699
- } else {
29700
- this.stopVideo();
29701
- }
29954
+ this._cameraMode = false;
29702
29955
  }
29703
29956
 
29704
29957
  get cameraMode() {
29705
- return this._cameraMode;
29958
+ return false;
29706
29959
  }
29707
29960
 
29708
29961
  get useWebViewCamera() {
@@ -29819,8 +30072,17 @@ class SmartFileComponent extends FieldComponent {
29819
30072
  this.processAndUpload(processedFiles);
29820
30073
  });
29821
30074
  this.addEventListener(this.refs.fileDrop, 'click', (event) => {
29822
- // Ignore clicks if they were directly on the browse link or other anchors to avoid duplicate triggers
29823
- if (event.target.tagName === 'A' || event.target.closest('a')) {
30075
+ // Ignore clicks if they were directly on links, buttons or camera controls to avoid duplicate triggers
30076
+ if (
30077
+ event.target.tagName === 'A' ||
30078
+ event.target.closest('a') ||
30079
+ event.target.closest('button') ||
30080
+ (event.target.closest && (
30081
+ event.target.closest('[ref="cameraButton"]') ||
30082
+ event.target.closest('[ref="toggleCameraMode"]') ||
30083
+ event.target.closest('[ref="galleryButton"]')
30084
+ ))
30085
+ ) {
29824
30086
  return;
29825
30087
  }
29826
30088
  event.preventDefault();
@@ -29930,32 +30192,50 @@ class SmartFileComponent extends FieldComponent {
29930
30192
  if (this.refs.cropImage) {
29931
30193
  this.refs.cropImage.forEach((cropImage, index) => {
29932
30194
  this.addEventListener(cropImage, 'click', (event) => {
29933
- const fileInfo = this.dataValue[index];
29934
- logger.info("[TF_SDK:79] ✅ cropImage clicked")
29935
30195
  let that = this;
29936
- const sampleImage = document.getElementsByClassName('fileImageData')
29937
- for (var i = 0; i < sampleImage.length; i++) {
29938
- if (sampleImage[i].alt == fileInfo.originalName && (sampleImage[i].id == fileInfo.compnentId || this.component.key == fileInfo.key)) {
29939
- showCropArea(sampleImage[i]);
30196
+ const fileInfo = this.dataValue[index];
30197
+ logger.info("[TF_SDK:79] cropImage clicked");
30198
+
30199
+ let targetImage = null;
30200
+ if (that.refs.fileImage && that.refs.fileImage[index]) {
30201
+ targetImage = that.refs.fileImage[index];
30202
+ } else {
30203
+ const sampleImage = document.getElementsByClassName('fileImageData');
30204
+ for (var i = 0; i < sampleImage.length; i++) {
30205
+ if (sampleImage[i].alt == fileInfo.originalName && (sampleImage[i].id == fileInfo.compnentId || that.component.key == fileInfo.key)) {
30206
+ targetImage = sampleImage[i];
30207
+ break;
30208
+ }
30209
+ }
30210
+ if (!targetImage && sampleImage.length > 0) {
30211
+ targetImage = sampleImage[0];
29940
30212
  }
29941
30213
  }
29942
30214
 
30215
+ if (targetImage) {
30216
+ showCropArea(targetImage);
30217
+ } else {
30218
+ logger.error('[TF_SDK:79a] ❌ Target image element not found for crop');
30219
+ }
30220
+
29943
30221
  function showCropArea(target) {
29944
30222
  new cropro.Activator.addKey('CRPR-P238-M097-0451');
29945
30223
  const cropArea = new cropro.CropArea(target);
29946
30224
  cropArea.displayMode = "popup";
30225
+ cropArea.renderImageType = 'image/jpeg';
30226
+ cropArea.renderImageQuality = 0.8;
29947
30227
  cropArea.styles.settings.toolbarBackgroundColor = '#2586c7';
29948
30228
  cropArea.styles.settings.toolbarBackgroundHoverColor = '#286f9f';
29949
- cropArea.addRenderEventListener((imgURL) => (
29950
- target.src = imgURL,
29951
- logger.info('[TF_SDK:80] ✅ cropped imgURL length =' + imgURL.length),
29952
- that.dataValue[index].url = imgURL,
29953
- b64CroppedImagetoBlob(imgURL.split(",")[1], 'image/jpeg', that.dataValue[index].originalName)
29954
- ));
30229
+ cropArea.addRenderEventListener((imgURL) => {
30230
+ target.src = imgURL;
30231
+ logger.info('[TF_SDK:80] ✅ cropped imgURL length =' + imgURL.length);
30232
+ that.dataValue[index].url = imgURL;
30233
+ b64CroppedImagetoBlob(imgURL.split(",")[1], 'image/jpeg', that.dataValue[index].originalName);
30234
+ });
29955
30235
  cropArea.show();
29956
30236
  }
29957
30237
 
29958
- function b64CroppedImagetoBlob(b64Data, contentType, originalName) {
30238
+ async function b64CroppedImagetoBlob(b64Data, contentType, originalName) {
29959
30239
  var sliceSize = 512;
29960
30240
  var byteCharacters = atob(b64Data);
29961
30241
  var byteArrays = [];
@@ -29973,12 +30253,47 @@ class SmartFileComponent extends FieldComponent {
29973
30253
  }
29974
30254
 
29975
30255
  var blob = new Blob(byteArrays, {
29976
- type: contentType
30256
+ type: contentType || 'image/jpeg'
29977
30257
  });
29978
- blob.name = originalName
29979
- logger.info(`[TF_SDK:81] ✅ Cropped Image file size ${blob.size / 1024 / 1024} MB`);
29980
- replace = true;
29981
- that.handleFilesToUpload([blob]);
30258
+ blob.name = originalName;
30259
+
30260
+ if (typeof imageCompression === 'function') {
30261
+ try {
30262
+ const compressedBlob = await imageCompression(blob, { maxSizeMB: 0.2, maxWidthOrHeight: 1600, useWebWorker: true });
30263
+ compressedBlob.name = originalName;
30264
+ logger.info(`[TF_SDK:81] ⚡ Compressed cropped image: ${blob.size} -> ${compressedBlob.size} bytes`);
30265
+ blob = compressedBlob;
30266
+ } catch (e) {
30267
+ logger.warn('[TF_SDK:81a] ⚠️ Compression failed:', e);
30268
+ }
30269
+ }
30270
+
30271
+ logger.info(`[TF_SDK:81b] ✅ Cropped Image file size ${blob.size / 1024 / 1024} MB`);
30272
+
30273
+ // 🔄 Update IndexedDB in-place with the same key so the id in dataValue stays valid
30274
+ const existingFileInfo = that.dataValue[index];
30275
+ const existingId = existingFileInfo && existingFileInfo.id;
30276
+
30277
+ if (existingId) {
30278
+ updateFileInIndexedDB(existingId, blob, originalName).then((updatedInfo) => {
30279
+ logger.info(`[TF_SDK:81c] ✅ IndexedDB updated in-place (crop) for: ${originalName}`);
30280
+ // Keep same id, just patch url/size/mode so dataValue stays consistent
30281
+ that.dataValue[index] = {
30282
+ ...that.dataValue[index],
30283
+ url: updatedInfo.url,
30284
+ size: updatedInfo.size,
30285
+ mode: updatedInfo.mode,
30286
+ originalName: originalName,
30287
+ };
30288
+ that.triggerChange?.({ modified: true });
30289
+ that.redraw();
30290
+ }).catch((err) => {
30291
+ logger.error(`[TF_SDK:81d] ❌ updateFileInIndexedDB (crop) failed:`, err);
30292
+ });
30293
+ } else {
30294
+ logger.warn(`[TF_SDK:81e] ⚠️ No existingId found for crop update (index=${index})`);
30295
+ }
30296
+
29982
30297
  }
29983
30298
  });
29984
30299
  });
@@ -29988,62 +30303,110 @@ class SmartFileComponent extends FieldComponent {
29988
30303
  if (this.refs.annotateImage) {
29989
30304
  this.refs.annotateImage.forEach((annotateImage, index) => {
29990
30305
  this.addEventListener(annotateImage, 'click', (event) => {
30306
+ let that = this; // ✅ FIX: capture 'this' so inner functions can reference it
29991
30307
  const fileInfo = this.dataValue[index];
29992
- logger.info("[TF_SDK:82] ✅ annotateImage clicked")
29993
- const sampleImage = document.getElementsByClassName('fileImageData')
29994
- for (var i = 0; i < sampleImage.length; i++) {
29995
- if (sampleImage[i].alt == fileInfo.originalName && (sampleImage[i].id == fileInfo.compnentId || this.component.key == fileInfo.key)) {
29996
- showMarkerArea(sampleImage[i]);
30308
+ logger.info("[TF_SDK:82] ✅ annotateImage clicked");
30309
+
30310
+ let targetImage = null;
30311
+ if (that.refs.fileImage && that.refs.fileImage[index]) {
30312
+ targetImage = that.refs.fileImage[index];
30313
+ } else {
30314
+ const sampleImage = document.getElementsByClassName('fileImageData');
30315
+ for (var i = 0; i < sampleImage.length; i++) {
30316
+ if (sampleImage[i].alt == fileInfo.originalName && (sampleImage[i].id == fileInfo.compnentId || that.component.key == fileInfo.key)) {
30317
+ targetImage = sampleImage[i];
30318
+ break;
30319
+ }
30320
+ }
30321
+ if (!targetImage && sampleImage.length > 0) {
30322
+ targetImage = sampleImage[0];
29997
30323
  }
29998
30324
  }
29999
30325
 
30000
- }, false)
30326
+ if (targetImage) {
30327
+ showMarkerArea(targetImage);
30328
+ } else {
30329
+ logger.error('[TF_SDK:82a] ❌ Target image element not found for annotate');
30330
+ }
30001
30331
 
30002
- function showMarkerArea(target) {
30003
- new markerjs2.Activator.addKey('MJS2-P013-M397-4100');
30004
- const markerArea = new markerjs2.MarkerArea(target);
30005
- // open the editor in a popup
30006
- markerArea.settings.displayMode = 'popup';
30007
- markerArea.uiStyleSettings.toolbarBackgroundColor = '#2586c7';
30008
- markerArea.uiStyleSettings.toolbarBackgroundHoverColor = '#286f9f';
30009
- markerArea.addRenderEventListener((imgURL) =>
30010
- (
30011
- target.src = imgURL,
30012
- that.dataValue[index].url = imgURL,
30013
- b64AnnotatedImagetoBlob(imgURL.split(",")[1], 'image/jpeg', that.dataValue[index].originalName)
30014
- ));
30015
- markerArea.show();
30016
- }
30332
+ function showMarkerArea(target) {
30333
+ new markerjs2.Activator.addKey('MJS2-P013-M397-4100');
30334
+ const markerArea = new markerjs2.MarkerArea(target);
30335
+ // open the editor in a popup
30336
+ markerArea.settings.displayMode = 'popup';
30337
+ markerArea.uiStyleSettings.toolbarBackgroundColor = '#2586c7';
30338
+ markerArea.uiStyleSettings.toolbarBackgroundHoverColor = '#286f9f';
30339
+ markerArea.addRenderEventListener((imgURL) => {
30340
+ target.src = imgURL;
30341
+ that.dataValue[index].url = imgURL;
30342
+ b64AnnotatedImagetoBlob(imgURL.split(",")[1], 'image/jpeg', that.dataValue[index].originalName);
30343
+ });
30344
+ markerArea.show();
30345
+ }
30017
30346
 
30018
30347
 
30019
- function b64AnnotatedImagetoBlob(b64Data, contentType, originalName) {
30020
- contentType = contentType || '';
30021
- var sliceSize = 512;
30022
- var byteCharacters = atob(b64Data);
30023
- var byteArrays = [];
30348
+ async function b64AnnotatedImagetoBlob(b64Data, contentType, originalName) {
30349
+ contentType = contentType || 'image/jpeg';
30350
+ var sliceSize = 512;
30351
+ var byteCharacters = atob(b64Data);
30352
+ var byteArrays = [];
30024
30353
 
30025
- for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
30026
- var slice = byteCharacters.slice(offset, offset + sliceSize);
30354
+ for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
30355
+ var slice = byteCharacters.slice(offset, offset + sliceSize);
30027
30356
 
30028
- var byteNumbers = new Array(slice.length);
30029
- for (var i = 0; i < slice.length; i++) {
30030
- byteNumbers[i] = slice.charCodeAt(i);
30357
+ var byteNumbers = new Array(slice.length);
30358
+ for (var i = 0; i < slice.length; i++) {
30359
+ byteNumbers[i] = slice.charCodeAt(i);
30360
+ }
30361
+
30362
+ var byteArray = new Uint8Array(byteNumbers);
30363
+
30364
+ byteArrays.push(byteArray);
30031
30365
  }
30032
30366
 
30033
- var byteArray = new Uint8Array(byteNumbers);
30367
+ var blob = new Blob(byteArrays, {
30368
+ type: contentType
30369
+ });
30370
+ blob.name = originalName;
30371
+
30372
+ if (typeof imageCompression === 'function') {
30373
+ try {
30374
+ const compressedBlob = await imageCompression(blob, { maxSizeMB: 0.2, maxWidthOrHeight: 1600, useWebWorker: true });
30375
+ compressedBlob.name = originalName;
30376
+ logger.info(`[TF_SDK:83] ⚡ Compressed annotated image: ${blob.size} -> ${compressedBlob.size} bytes`);
30377
+ blob = compressedBlob;
30378
+ } catch (e) {
30379
+ logger.warn('[TF_SDK:83a] ⚠️ Compression failed:', e);
30380
+ }
30381
+ }
30034
30382
 
30035
- byteArrays.push(byteArray);
30036
- }
30383
+ logger.info(`[TF_SDK:83b] ✅ Annotated Image file size ${blob.size / 1024 / 1024} MB`);
30037
30384
 
30038
- var blob = new Blob(byteArrays, {
30039
- type: contentType
30040
- });
30041
- blob.name = originalName;
30042
- logger.info(`[TF_SDK:83] ✅ Annotatted Image file size ${blob.size / 1024 / 1024} MB`);
30043
- replace = true;
30044
- that.handleFilesToUpload([blob]);
30385
+ // 🔄 Update IndexedDB in-place with the same key so the id in dataValue stays valid
30386
+ const existingFileInfo = that.dataValue[index];
30387
+ const existingId = existingFileInfo && existingFileInfo.id;
30045
30388
 
30046
- }
30389
+ if (existingId) {
30390
+ updateFileInIndexedDB(existingId, blob, originalName).then((updatedInfo) => {
30391
+ logger.info(`[TF_SDK:83c] ✅ IndexedDB updated in-place (annotate) for: ${originalName}`);
30392
+ // Keep same id, just patch url/size/mode
30393
+ that.dataValue[index] = {
30394
+ ...that.dataValue[index],
30395
+ url: updatedInfo.url,
30396
+ size: updatedInfo.size,
30397
+ mode: updatedInfo.mode,
30398
+ originalName: originalName,
30399
+ };
30400
+ that.triggerChange?.({ modified: true });
30401
+ that.redraw();
30402
+ }).catch((err) => {
30403
+ logger.error(`[TF_SDK:83d] ❌ updateFileInIndexedDB (annotate) failed:`, err);
30404
+ });
30405
+ } else {
30406
+ logger.warn(`[TF_SDK:83e] ⚠️ No existingId found for annotate update (index=${index})`);
30407
+ }
30408
+ }
30409
+ });
30047
30410
  });
30048
30411
  }
30049
30412
 
@@ -30114,68 +30477,50 @@ class SmartFileComponent extends FieldComponent {
30114
30477
  if (this.refs.cameraButton) {
30115
30478
  this.addEventListener(this.refs.cameraButton, 'click', (event) => {
30116
30479
  event.preventDefault();
30117
- if (webViewCamera) {
30118
- webViewCamera.getPicture((success) => {
30119
- window.resolveLocalFileSystemURL(success, (fileEntry) => {
30120
- fileEntry.file((file) => {
30121
- const reader = new FileReader();
30122
- reader.onloadend = (evt) => {
30123
- const blob = new Blob([new Uint8Array(evt.target.result)], {
30124
- type: file.type
30125
- });
30126
- blob.name = file.name;
30127
- replace = false;
30128
- this.processAndUpload([blob]);
30129
- };
30130
- reader.readAsArrayBuffer(file);
30131
- });
30132
- });
30133
- }, (err) => {
30134
- logger.error(err);
30135
- }, {
30136
- sourceType: webViewCamera.PictureSourceType.CAMERA,
30137
- encodingType: webViewCamera.EncodingType.PNG,
30138
- mediaType: webViewCamera.MediaType.PICTURE,
30139
- saveToPhotoAlbum: true,
30140
- correctOrientation: false,
30141
- });
30142
- } else {
30143
- let id = this.id;
30144
- isfileBrowse = false;
30145
- window.isListenerSet = false;
30146
- let eventCustom = new CustomEvent("openCamera", {
30147
- detail: {
30148
- controlId: id,
30149
- },
30150
- });
30151
-
30152
- if (window.parent && window.parent !== window) {
30153
- window.parent.postMessage(
30154
- {
30155
- type: "OPEN_CAMERA",
30156
- data: { controlId: id },
30157
- status: "sucess",
30158
- message: "camera triggerd",
30159
- },
30160
- "*"
30161
- );
30162
- }
30480
+ event.stopPropagation();
30481
+ if (typeof event.stopImmediatePropagation === 'function') {
30482
+ event.stopImmediatePropagation();
30483
+ }
30484
+ let id = this.id;
30485
+ isfileBrowse = false;
30486
+ window.isListenerSet = false;
30487
+ let eventCustom = new CustomEvent("openCamera", {
30488
+ detail: {
30489
+ controlId: id,
30490
+ },
30491
+ });
30163
30492
 
30164
- logger.info("[TF_SDK] Dispatching openCamera event for controlId:", id);
30165
- document.dispatchEvent(eventCustom);
30493
+ if (window.parent && window.parent !== window) {
30494
+ window.parent.postMessage(
30495
+ {
30496
+ type: "OPEN_CAMERA",
30497
+ data: { controlId: id },
30498
+ status: "sucess",
30499
+ message: "camera triggerd",
30500
+ },
30501
+ "*"
30502
+ );
30166
30503
  }
30504
+
30505
+ logger.info("[TF_SDK:84a] ✅ Dispatching openCamera event for controlId:", id);
30506
+ document.dispatchEvent(eventCustom);
30167
30507
  });
30168
30508
  }
30169
30509
 
30170
30510
  if (this.refs.takePictureButton) {
30171
30511
  this.addEventListener(this.refs.takePictureButton, 'click', (event) => {
30172
30512
  event.preventDefault();
30513
+ event.stopPropagation();
30173
30514
  this.takePicture();
30174
30515
  });
30175
30516
  }
30176
30517
  if (this.refs.toggleCameraMode) {
30177
30518
  this.addEventListener(this.refs.toggleCameraMode, 'click', (event) => {
30178
30519
  event.preventDefault();
30520
+ event.stopPropagation();
30521
+ if (typeof event.stopImmediatePropagation === 'function') {
30522
+ event.stopImmediatePropagation();
30523
+ }
30179
30524
  let id = this.id;
30180
30525
  isfileBrowse = false;
30181
30526
  window.isListenerSet = false;
@@ -30246,8 +30591,7 @@ class SmartFileComponent extends FieldComponent {
30246
30591
  }
30247
30592
  let that = this;
30248
30593
  this.addEventListener(document, 'capureImageData', function (event) {
30249
- logger.info("[TF_SDK:84] ✅ event.detail.controlId =" + event.detail.controlId)
30250
- if (that.id == event.detail.controlId) {
30594
+ if (that.id == event.detail?.controlId) {
30251
30595
  if (!window.isListenerSet) {
30252
30596
  const isBase64 = typeof event.detail.imageData === 'string' && event.detail.imageData.startsWith('data:');
30253
30597
  if (isBase64 || typeof window.resolveLocalFileSystemURL !== 'function') {
@@ -30871,6 +31215,12 @@ class SmartFileComponent extends FieldComponent {
30871
31215
  : this.t('Preparing file to remove'),
30872
31216
  });
30873
31217
 
31218
+ if (fileInfo && fileInfo.id) {
31219
+ deleteFileInIndexedDB(fileInfo.id).catch((err) => {
31220
+ logger.warn(`[TF_SDK:102c] ⚠️ Failed to mark file mode='D' in IndexedDB:`, err);
31221
+ });
31222
+ }
31223
+
30874
31224
  const index = this.dataValue.findIndex((file) =>
30875
31225
  file === fileInfo ||
30876
31226
  (file.id && file.id === fileInfo.id) ||
@@ -31120,23 +31470,33 @@ class SmartFileComponent extends FieldComponent {
31120
31470
 
31121
31471
  logger.info(`[TF_SDK:110] ✅ syncFiles succeeded for ${data.length} new files`);
31122
31472
  if (replace) {
31123
- let ind;
31124
- for (let i = 0; i < this.dataValue.length; i++) {
31125
- if (this.dataValue[i].originalName == fileInfo.originalName) {
31126
- logger.info("[TF_SDK:111] original name matches")
31127
- ind = i;
31128
- }
31129
- }
31130
- if (ind !== -1) {
31131
- let originalFileId = this.dataValue[ind].id;
31132
- logger.info("[TF_SDK:112] ✅ originalFileId =" + originalFileId)
31133
- this.dataValue[ind] = fileInfo;
31134
- let eventCustom = new CustomEvent('deleteOriginalFileFromDB', {
31135
- detail: {
31136
- fileId: originalFileId,
31473
+ replace = false; // Reset flag immediately to avoid stale state on future uploads
31474
+ const editedFileInfo = data[0]; // The newly stored edited image from IndexedDB
31475
+ if (!editedFileInfo) {
31476
+ logger.warn("[TF_SDK:111a] ⚠️ replace=true but no fileInfo available — skipping replace");
31477
+ } else {
31478
+ let ind = undefined;
31479
+ for (let i = 0; i < this.dataValue.length; i++) {
31480
+ if (this.dataValue[i].originalName == editedFileInfo.originalName) {
31481
+ logger.info("[TF_SDK:111] original name matches at index " + i);
31482
+ ind = i;
31137
31483
  }
31138
- });
31139
- document.dispatchEvent(eventCustom);
31484
+ }
31485
+ if (ind !== undefined) {
31486
+ let originalFileId = this.dataValue[ind].id;
31487
+ logger.info("[TF_SDK:112] ✅ originalFileId =" + originalFileId + ", replacing with new id =" + editedFileInfo.id);
31488
+ this.dataValue[ind] = editedFileInfo;
31489
+ // Delete the old IndexedDB entry since the edited image is stored under a new id
31490
+ let eventCustom = new CustomEvent('deleteOriginalFileFromDB', {
31491
+ detail: {
31492
+ fileId: originalFileId,
31493
+ }
31494
+ });
31495
+ document.dispatchEvent(eventCustom);
31496
+ } else {
31497
+ logger.warn("[TF_SDK:112a] ⚠️ Could not find original file to replace in dataValue for: " + editedFileInfo.originalName);
31498
+ this.dataValue.push(editedFileInfo);
31499
+ }
31140
31500
  }
31141
31501
  } else {
31142
31502
  this.dataValue.push(...data);
@@ -33799,47 +34159,11 @@ class SmartSelectComponent extends SmartSelectField {
33799
34159
  });
33800
34160
 
33801
34161
  this.addEventListener(input, 'showDropdown', () => {
33802
- const container = document.getElementById('form-container');
33803
- const currentScroll = container ? container.scrollTop : 0;
33804
-
33805
34162
  if (!this.activated) {
33806
34163
  this.update();
33807
34164
  }
33808
34165
  this.positionDropdown();
33809
34166
 
33810
- // Ensure select is visible above keyboard on iOS
33811
- if (window.platform && window.platform.iosPlatform) {
33812
- // We removed native scrollIntoView as it interferes with manual scroll control in focus.js
33813
- // focus.js handles the centering now.
33814
- }
33815
-
33816
- // Restore and lock scroll on iPad to prevent "flicker" shifts during keyboard toggles
33817
- if (container && currentScroll > 0 && window.platform && window.platform.iosPlatform) {
33818
- // Immediate restoration
33819
- container.scrollTop = currentScroll;
33820
-
33821
- // Follow-up restoration for async layout shifts
33822
- requestAnimationFrame(() => {
33823
- container.scrollTop = currentScroll;
33824
- });
33825
-
33826
- // Periodical restoration during the typical keyboard transition time (300ms)
33827
- let count = 0;
33828
- const interval = setInterval(() => {
33829
- if (container.scrollTop !== currentScroll) {
33830
- container.scrollTop = currentScroll;
33831
- }
33832
- if (++count > 10) clearInterval(interval);
33833
- }, 30);
33834
- } else if (container && currentScroll > 0) {
33835
- // Standard restoration for other platforms
33836
- requestAnimationFrame(() => {
33837
- if (container.scrollTop === 0) {
33838
- container.scrollTop = currentScroll;
33839
- }
33840
- });
33841
- }
33842
-
33843
34167
  if ((this.component.dataSrc === 'masterdata') && this.component.masterdata) {
33844
34168
  this.component.dropdownOpened = true;
33845
34169
  // Force reloading the full list if we currently only have a partial list (e.g. filtered down on hideDropdown)
@@ -33854,6 +34178,22 @@ class SmartSelectComponent extends SmartSelectField {
33854
34178
  this.positionDropdown();
33855
34179
  });
33856
34180
  }
34181
+
34182
+ this.addEventListener(input, 'hideDropdown', () => {
34183
+ if ((this.component.dataSrc === 'masterdata') && this.component.masterdata) {
34184
+ this.component.dropdownOpened = false;
34185
+ }
34186
+ });
34187
+
34188
+ this.addEventListener(input, 'search', (event) => {
34189
+ if ((this.component.dataSrc === 'masterdata') && this.component.masterdata) {
34190
+ this.searchDownloadedResources = this.filteredMasterdata.filter(data => {
34191
+ return data[this.component.searchField].toLowerCase().includes(event.detail.value.toLowerCase())
34192
+ })
34193
+ this.isFromSearch = true;
34194
+ }
34195
+ });
34196
+
33857
34197
  if (this.choices && choicesOptions.placeholderValue && this.choices._isSelectOneElement) {
33858
34198
  this.addPlaceholderItem(choicesOptions.placeholderValue);
33859
34199
 
@@ -33922,14 +34262,38 @@ class SmartSelectComponent extends SmartSelectField {
33922
34262
  }
33923
34263
 
33924
34264
  if (!dropdown || !container) {
34265
+ console.warn('[TF_SELECT_LOG:ERR] ⚠️ dropdown or container element not found in setDropdownPosition');
33925
34266
  return;
33926
34267
  }
33927
34268
 
33928
34269
  const containerPosition = container.getBoundingClientRect();
33929
- const isFlipped = container.classList.contains('is-flipped');
33930
-
33931
34270
  const vpH = window.visualViewport ? window.visualViewport.height : window.innerHeight;
33932
- const top = isFlipped ? containerPosition.top - dropdown.offsetHeight : containerPosition.top + containerPosition.height;
34271
+ const spaceBelow = vpH - (containerPosition.top + containerPosition.height);
34272
+ const spaceAbove = containerPosition.top;
34273
+
34274
+ // Auto-flip upwards if not enough room below and more room above
34275
+ let isFlipped = container.classList.contains('is-flipped');
34276
+ if (!isFlipped && spaceBelow < 180 && spaceAbove > spaceBelow) {
34277
+ isFlipped = true;
34278
+ container.classList.add('is-flipped');
34279
+ dropdown.classList.add('is-flipped');
34280
+ } else if (isFlipped && spaceBelow >= 200) {
34281
+ isFlipped = false;
34282
+ container.classList.remove('is-flipped');
34283
+ dropdown.classList.remove('is-flipped');
34284
+ }
34285
+
34286
+ const dropdownHeight = dropdown.offsetHeight || 220;
34287
+ let top;
34288
+ let maxHeight;
34289
+
34290
+ if (isFlipped) {
34291
+ maxHeight = Math.min(dropdownHeight, Math.max(120, spaceAbove - 20));
34292
+ top = Math.max(10, containerPosition.top - maxHeight);
34293
+ } else {
34294
+ top = containerPosition.top + containerPosition.height;
34295
+ maxHeight = Math.max(120, Math.min(dropdownHeight, vpH - top - 15));
34296
+ }
33933
34297
 
33934
34298
  const style = {
33935
34299
  left: `${containerPosition.left}px`,
@@ -33938,18 +34302,9 @@ class SmartSelectComponent extends SmartSelectField {
33938
34302
  bottom: 'unset',
33939
34303
  right: 'unset',
33940
34304
  top: `${top}px`,
34305
+ maxHeight: `${maxHeight}px`,
33941
34306
  };
33942
34307
 
33943
- // On iOS, we limit the max-height of the fixed dropdown to stay within the viewport.
33944
- // We also use a scroll listener on the wrapper (added in attach()) to ensure this fixed
33945
- // element re-positions during scrolling, making it behave like an absolute element.
33946
- if (window.platform && window.platform.iosPlatform) {
33947
- // Limit max-height to available space but ensure at least 300px for a good scroll area.
33948
- // We subtract 30px to leave a small gap at the bottom for better visibility.
33949
- const availableHeight = isFlipped ? top - 20 : vpH - top - 30;
33950
- style.maxHeight = `${Math.max(availableHeight, 300)}px`;
33951
- }
33952
-
33953
34308
  _.assign(dropdown.style, style);
33954
34309
  }
33955
34310
 
@@ -38249,49 +38604,39 @@ let isKeyboardOpen = false;
38249
38604
  let activeElement = null;
38250
38605
  const INITIAL_WINDOW_HEIGHT = window.innerHeight;
38251
38606
 
38252
- logger.info("[TF_SDK:254] ✅ Script initialized. Initial Height:", INITIAL_WINDOW_HEIGHT);
38607
+ function isIOSDevice() {
38608
+ return /iPad|iPhone|iPod/.test(navigator.userAgent) ||
38609
+ (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) ||
38610
+ Boolean(window.platform && window.platform.iosPlatform);
38611
+ }
38253
38612
 
38254
- // SOFT LOCK
38255
- window.addEventListener('scroll', () => {
38256
- if (isKeyboardOpen && Math.abs(window.scrollY) > 20) {
38257
- window.scrollTo(0, 0);
38258
- }
38259
- }, { passive: true });
38613
+ logger.info("[TF_SDK:254] Focus script initialized. Initial Height:", INITIAL_WINDOW_HEIGHT, "isIOS:", isIOSDevice());
38260
38614
 
38261
38615
  // Close flatpickr (date components) if user scrolls any container (like ion-content in Ionic)
38262
38616
  function closeOpenFlatpickrs(e) {
38263
- // Ignore scrolls inside the flatpickr calendar itself (e.g., year scroll)
38264
38617
  if (e && e.target && e.target.closest && e.target.closest('.flatpickr-calendar')) {
38265
38618
  return;
38266
38619
  }
38267
38620
  const openPicker = document.querySelector('.flatpickr-calendar.open, .flatpickr-calendar.show');
38268
38621
  if (openPicker) {
38269
- // Method 1: Blur active element
38270
38622
  if (document.activeElement && typeof document.activeElement.blur === 'function') {
38271
38623
  document.activeElement.blur();
38272
38624
  }
38273
- // Method 2: Call flatpickr instance close() directly
38274
38625
  document.querySelectorAll('input').forEach(input => {
38275
38626
  if (input._flatpickr && typeof input._flatpickr.close === 'function') {
38276
38627
  input._flatpickr.close();
38277
38628
  }
38278
38629
  });
38279
- // Method 3: Simulate click outside
38280
38630
  document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
38281
38631
  }
38282
38632
  }
38283
38633
 
38284
38634
  window.addEventListener('scroll', closeOpenFlatpickrs, true);
38285
- // Ionic's ion-content uses Shadow DOM, meaning native 'scroll' events do not bubble out of it.
38286
- // To reliably detect scrolling in Ionic, we use wheel and touchmove events globally.
38287
38635
  window.addEventListener('wheel', closeOpenFlatpickrs, { capture: true, passive: true });
38288
38636
  window.addEventListener('touchmove', closeOpenFlatpickrs, { capture: true, passive: true });
38289
38637
 
38290
38638
  function updateFooterVisibility(keyboardOpen) {
38291
- // Only hide the footer on iOS to make room for the software keyboard.
38292
- // On Windows/Desktop, we should keep the footer visible at all times.
38293
- const isIOS = (window.platform && window.platform.iosPlatform);
38294
- if (!isIOS) return;
38639
+ if (!isIOSDevice()) return;
38295
38640
 
38296
38641
  const footer = document.getElementById('sticky-footer');
38297
38642
  if (!footer) return;
@@ -38303,126 +38648,127 @@ function updateFooterVisibility(keyboardOpen) {
38303
38648
  }
38304
38649
  }
38305
38650
 
38651
+ function centerActiveElement() {
38652
+ if (!isIOSDevice() || !activeElement) return;
38653
+
38654
+ const target = activeElement.closest('.formio-component') || activeElement;
38655
+ const vpH = window.visualViewport ? window.visualViewport.height : window.innerHeight;
38656
+ const rect = target.getBoundingClientRect();
38657
+
38658
+ // If this is a select component, check if its dropdown is active or opening
38659
+ const dropdown = target.querySelector('.choices__list--dropdown.is-active, .choices__list--dropdown');
38660
+ const isSelect = Boolean(dropdown || target.querySelector('.choices'));
38661
+ const dropdownHeight = (isSelect && dropdown) ? Math.max(140, dropdown.offsetHeight || 140) : 0;
38662
+
38663
+ const effectiveBottom = rect.bottom + dropdownHeight;
38664
+ const isHiddenUnderKeyboard = effectiveBottom > (vpH - 15);
38665
+ const isScrolledAboveTop = rect.top < 0;
38666
+
38667
+ // If element is already fully visible in the viewport, do NOT scroll
38668
+ if (!isHiddenUnderKeyboard && !isScrolledAboveTop) {
38669
+ return;
38670
+ }
38671
+
38672
+ if (typeof target.scrollIntoView === 'function') {
38673
+ const align = (isSelect && isHiddenUnderKeyboard) ? 'center' : 'nearest';
38674
+ target.scrollIntoView({ behavior: 'smooth', block: align, inline: 'nearest' });
38675
+ }
38676
+
38677
+ const wrapper = document.getElementById('formio-wrapper');
38678
+ if (wrapper && wrapper.scrollHeight > wrapper.clientHeight) {
38679
+ let offsetTop = 0;
38680
+ let curr = target;
38681
+ while (curr && curr !== wrapper && curr !== null) {
38682
+ offsetTop += curr.offsetTop;
38683
+ curr = curr.offsetParent;
38684
+ }
38685
+ if (isHiddenUnderKeyboard) {
38686
+ const targetScroll = offsetTop - (vpH - target.offsetHeight - dropdownHeight - 20);
38687
+ wrapper.scrollTo({ top: Math.max(0, targetScroll), behavior: 'smooth' });
38688
+ } else if (isScrolledAboveTop) {
38689
+ wrapper.scrollTo({ top: Math.max(0, offsetTop - 20), behavior: 'smooth' });
38690
+ }
38691
+ }
38692
+ }
38693
+
38306
38694
  // Track focused input
38307
38695
  document.addEventListener('focusin', (e) => {
38696
+ if (!isIOSDevice()) return;
38697
+
38308
38698
  const el = e.target;
38309
38699
  if (!el || !['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName)) return;
38310
38700
 
38311
- logger.info("[TF_SDK:255] ✅ focusin on:", el.name || el.id || el.tagName);
38312
38701
  updateFooterVisibility(true);
38313
38702
 
38314
38703
  activeElement = el;
38315
38704
 
38316
- // Special handling for Choices.js: if an input inside choices is focused,
38317
- // we want to ensure the whole container is considered the active element for centering
38318
- if (el.classList.contains('choices__input')) {
38319
- const container = el.closest('.choices');
38705
+ if (el.classList.contains('choices__input') || el.closest('.choices')) {
38706
+ const container = el.closest('.choices') || el.closest('.formio-component-barcodeselect') || el.closest('.formio-component-select');
38320
38707
  if (container) {
38321
38708
  activeElement = container;
38322
- logger.info("[TF_SDK:256] ✅ Choices.js detected, centering container.");
38323
38709
  }
38324
38710
  }
38325
38711
 
38326
- if (isKeyboardOpen) {
38327
- logger.info("[TF_SDK:257] ✅ Already open, centering now.");
38328
- centerActiveElement();
38329
- }
38712
+ // Scroll element into view with multiple staggered triggers for smooth iOS animation
38713
+ setTimeout(centerActiveElement, 100);
38714
+ setTimeout(centerActiveElement, 350);
38330
38715
  });
38331
38716
 
38332
38717
  // Restore footer when focus is lost and keyboard is not open
38333
38718
  document.addEventListener('focusout', (e) => {
38719
+ if (!isIOSDevice()) return;
38720
+
38334
38721
  setTimeout(() => {
38335
38722
  const currentlyFocused = document.activeElement;
38336
38723
  const isInputFocused = currentlyFocused && ['INPUT', 'TEXTAREA', 'SELECT'].includes(currentlyFocused.tagName);
38337
38724
 
38338
38725
  if (!isInputFocused && !isKeyboardOpen) {
38339
- logger.info("[TF_SDK:258] ✅ focusout and keyboard closed, restoring footer.");
38340
38726
  updateFooterVisibility(false);
38341
38727
  }
38342
- }, 200);
38728
+ }, 250);
38343
38729
  });
38344
38730
 
38345
- // Detect keyboard open/close
38731
+ // Detect keyboard open/close via visualViewport
38346
38732
  if (window.visualViewport) {
38347
38733
  window.visualViewport.addEventListener('resize', () => {
38348
- const isIOS = (window.platform && window.platform.iosPlatform);
38349
- if (!isIOS) return;
38734
+ if (!isIOSDevice()) return;
38350
38735
 
38351
38736
  const viewport = window.visualViewport;
38352
38737
  const vpH = viewport.height;
38353
- const isNowOpen = vpH < INITIAL_WINDOW_HEIGHT - 100;
38738
+ const isNowOpen = (vpH < INITIAL_WINDOW_HEIGHT - 60) || (vpH < screen.height - 150);
38354
38739
 
38355
38740
  if (isNowOpen) {
38356
- const wasAlreadyOpen = isKeyboardOpen;
38357
38741
  isKeyboardOpen = true;
38358
38742
  updateFooterVisibility(true);
38359
38743
 
38360
- if (activeElement && !wasAlreadyOpen) {
38361
- logger.info("[TF_SDK:259] ✅ Keyboard sliding. Following...");
38362
- startFollowingElement();
38744
+ if (activeElement) {
38745
+ setTimeout(centerActiveElement, 150);
38746
+ setTimeout(centerActiveElement, 350);
38363
38747
  }
38364
38748
  } else {
38365
38749
  isKeyboardOpen = false;
38366
38750
  updateFooterVisibility(false);
38367
- window.scrollTo(0, 0);
38368
38751
  }
38369
38752
  });
38370
38753
  }
38371
38754
 
38372
- // Manually follow the element for a short duration to ensure it stays centered during animation
38373
- function startFollowingElement() {
38374
- let start = Date.now();
38375
- const duration = 400; // Follow for 400ms (typical iOS keyboard duration)
38376
-
38377
- function step() {
38378
- centerActiveElement();
38379
- if (Date.now() - start < duration) {
38380
- requestAnimationFrame(step);
38381
- }
38382
- }
38383
- requestAnimationFrame(step);
38384
- }
38385
-
38386
- function centerActiveElement() {
38387
- if (!activeElement) return;
38388
- const wrapper = document.getElementById('formio-wrapper');
38389
- if (!wrapper) return;
38390
-
38391
- const vpH = window.visualViewport ? window.visualViewport.height : window.innerHeight;
38392
-
38393
- // Calculate absolute offset of element relative to wrapper
38394
- let offsetTop = 0;
38395
- let curr = activeElement;
38396
- while (curr && curr !== wrapper && curr !== null) {
38397
- offsetTop += curr.offsetTop;
38398
- curr = curr.offsetParent;
38399
- }
38400
-
38401
- // For Choices.js, if the dropdown is open, we might want to adjust centering
38402
- // to ensure the dropdown is visible. However, centering the container is usually best.
38403
- const elementHeight = activeElement.offsetHeight;
38755
+ // Listen for Choices.js events to ensure dropdown options are fully visible
38756
+ document.addEventListener('showDropdown', (e) => {
38757
+ if (!isIOSDevice()) return;
38404
38758
 
38405
- // targetScroll = (distance from top) - (half of available space) + (half of element height)
38406
- const targetScroll = offsetTop - (vpH / 2) + (elementHeight / 2);
38407
-
38408
- // Apply instantly but accurately
38409
- if (Math.abs(wrapper.scrollTop - targetScroll) > 5) {
38410
- wrapper.scrollTop = targetScroll;
38411
- }
38412
- }
38413
-
38414
- // Listen for Choices.js events to re-center when dropdown opens/closes
38415
- document.addEventListener('showDropdown', () => {
38416
- if (isKeyboardOpen) {
38417
- logger.info("[TF_SDK:260] ✅ showDropdown detected, re-centering.");
38418
- setTimeout(centerActiveElement, 100);
38759
+ const el = e.target;
38760
+ const comp = (el && el.closest) ? (el.closest('.formio-component-barcodeselect, .formio-component-select, .formio-component') || el) : activeElement;
38761
+ if (comp) {
38762
+ activeElement = comp;
38763
+ setTimeout(centerActiveElement, 50);
38764
+ setTimeout(centerActiveElement, 200);
38765
+ setTimeout(centerActiveElement, 400);
38419
38766
  }
38420
38767
  }, true);
38421
38768
 
38422
38769
  document.addEventListener('hideDropdown', () => {
38423
- logger.info("[TF_SDK:261] ✅ hideDropdown detected, resetting view.");
38424
- // When dropdown hides, we might want to center the closed select again
38425
- setTimeout(centerActiveElement, 100);
38770
+ if (!isIOSDevice()) return;
38771
+ setTimeout(centerActiveElement, 80);
38426
38772
  }, true);
38427
38773
 
38428
38774
  // ============================================ AUTOFOCUS FIX =======================================================
@@ -40691,6 +41037,8 @@ function processFilePreviewsAndErrors(comp) {
40691
41037
  tempImg.onload = () => {
40692
41038
  const cropArea = new cropro.CropArea(tempImg);
40693
41039
  cropArea.displayMode = 'popup';
41040
+ cropArea.renderImageType = 'image/jpeg';
41041
+ cropArea.renderImageQuality = 0.8;
40694
41042
  cropArea.renderAtNaturalSize = true;
40695
41043
  cropArea.popupMargin = 60; // Increase margin to reduce modal size
40696
41044
 
@@ -40723,11 +41071,72 @@ function processFilePreviewsAndErrors(comp) {
40723
41071
  }
40724
41072
 
40725
41073
 
40726
- cropArea.addRenderEventListener((dataUrl) => {
41074
+ cropArea.addRenderEventListener(async (dataUrl) => {
41075
+ logger.info('[TF_SDK:35a] 🎨 Crop render event fired');
40727
41076
  // Update UI
40728
41077
  const previewImg = row.querySelector('.file-preview-img, [ref="fileImage"]');
40729
41078
  if (previewImg) previewImg.src = dataUrl;
40730
41079
 
41080
+ // Find Form.io component instance and row index
41081
+ const componentInstance = comp.componentInstance || comp.component;
41082
+ const rowIndex = fileRows.indexOf(row);
41083
+
41084
+ let fileInfo = null;
41085
+ if (componentInstance && Array.isArray(componentInstance.dataValue)) {
41086
+ fileInfo = componentInstance.dataValue[rowIndex];
41087
+ }
41088
+
41089
+ const existingId = fileInfo ? fileInfo.id : null;
41090
+ const originalName = fileInfo ? (fileInfo.originalName || fileInfo.name) : 'edited_image.jpg';
41091
+
41092
+ // Convert dataUrl to Blob
41093
+ var arr = dataUrl.split(','),
41094
+ mime = (arr[0].match(/:(.*?);/) || [])[1] || 'image/jpeg',
41095
+ bstr = atob(arr[1]),
41096
+ n = bstr.length,
41097
+ u8arr = new Uint8Array(n);
41098
+ while (n--) {
41099
+ u8arr[n] = bstr.charCodeAt(n);
41100
+ }
41101
+ let blob = new Blob([u8arr], { type: mime });
41102
+ blob.name = originalName;
41103
+
41104
+ // ⚡ OPTIMIZE / COMPRESS CROPPED BLOB
41105
+ if (typeof imageCompression === 'function' && mime.startsWith('image/')) {
41106
+ try {
41107
+ const compressOptions = {
41108
+ maxSizeMB: 0.2, // max ~200KB
41109
+ maxWidthOrHeight: 1600,
41110
+ initialQuality: 0.8,
41111
+ useWebWorker: true
41112
+ };
41113
+ const compressedBlob = await imageCompression(blob, compressOptions);
41114
+ compressedBlob.name = originalName;
41115
+ logger.info(`[TF_SDK:35b] ⚡ Compressed cropped image: ${blob.size} -> ${compressedBlob.size} bytes`);
41116
+ blob = compressedBlob;
41117
+ } catch (compressErr) {
41118
+ logger.warn('[TF_SDK:35c] ⚠️ Compression skipped/failed:', compressErr);
41119
+ }
41120
+ }
41121
+
41122
+ if (existingId && typeof window.updateFileInIndexedDB === 'function') {
41123
+ window.updateFileInIndexedDB(existingId, blob, originalName).then((updatedInfo) => {
41124
+ logger.info('[TF_SDK:35d] ✅ IndexedDB update successful for id:', existingId);
41125
+ if (fileInfo) {
41126
+ fileInfo.url = updatedInfo.url;
41127
+ fileInfo.size = updatedInfo.size;
41128
+ fileInfo.mode = 'M';
41129
+ }
41130
+ if (componentInstance && typeof componentInstance.triggerChange === 'function') {
41131
+ componentInstance.triggerChange({ modified: true });
41132
+ }
41133
+ }).catch((err) => {
41134
+ logger.error('[TF_SDK:35e] ❌ IndexedDB update failed:', err);
41135
+ });
41136
+ } else {
41137
+ logger.warn('[TF_SDK:35f] ⚠️ Cannot update IndexedDB: existingId missing or updateFileInIndexedDB function not available');
41138
+ }
41139
+
40731
41140
  // Notify app of changes
40732
41141
  const updateEvent = new CustomEvent('formio-file-updated', {
40733
41142
  detail: { element: comp, row: row, dataUrl: dataUrl },
@@ -41097,7 +41506,7 @@ window.applySignatureLayout = function(form) {
41097
41506
  const h = document.createElement('span');
41098
41507
  h.className = 'signature-click-hint';
41099
41508
  h.style.pointerEvents = 'none';
41100
- h.innerHTML = `Sign Here<br>Or<br><span class="signature-click-action">Click To Sign</span>`;
41509
+ h.innerHTML = `Sign Here<br>Or<br><span class="signature-click-action" role="button" tabindex="0">Click To Sign</span>`;
41101
41510
  padBody.appendChild(h);
41102
41511
  return h;
41103
41512
  };
@@ -41155,10 +41564,21 @@ window.applySignatureLayout = function(form) {
41155
41564
  }
41156
41565
 
41157
41566
  // ── "CLICK TO SIGN" → Trigger the component's internal logic ──────────
41158
- padBody.addEventListener('click', (e) => {
41159
- if (!e.target.classList.contains('signature-click-action')) return;
41567
+ let lastTriggerTime = 0;
41568
+ const triggerClickToSign = (e) => {
41569
+ const actionBtn = e.target && e.target.closest ? e.target.closest('.signature-click-action') : null;
41570
+ if (!actionBtn) return;
41160
41571
  if (comp.options && comp.options.readOnly) return;
41161
-
41572
+
41573
+ // Debounce touch vs click firing on mobile
41574
+ const now = Date.now();
41575
+ if (now - lastTriggerTime < 300) return;
41576
+ lastTriggerTime = now;
41577
+
41578
+ if (e.cancelable) {
41579
+ e.preventDefault();
41580
+ }
41581
+
41162
41582
  // Call the new robust method directly
41163
41583
  if (typeof comp.drawSignatureFromName === 'function') {
41164
41584
  comp.drawSignatureFromName();
@@ -41168,7 +41588,10 @@ window.applySignatureLayout = function(form) {
41168
41588
  comp.refs.clicktoSign.click();
41169
41589
  hideHint(padBody);
41170
41590
  }
41171
- });
41591
+ };
41592
+
41593
+ padBody.addEventListener('click', triggerClickToSign);
41594
+ padBody.addEventListener('touchend', triggerClickToSign, { passive: false });
41172
41595
 
41173
41596
  // ── CANVAS: immediate fade on touch/draw ──────────────────────────────
41174
41597
  if (canvas) {
@@ -41757,7 +42180,7 @@ initThemeHook();
41757
42180
  </div>
41758
42181
 
41759
42182
  <div id="sticky-footer">
41760
- <div class="build-version">SDK v2.0.73</div>
42183
+ <div class="build-version">SDK v2.0.74</div>
41761
42184
 
41762
42185
  <button class="footer-btn footer-btn-secondary" id="prevBtn" style="display:none" onclick="FormOnPrevious()">
41763
42186
  <i class="bi bi-chevron-left"></i> Previous