@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.
- package/dist/turboforms.esm.js +6 -6
- package/dist/turboforms.html +831 -408
- package/dist/turboforms.js +7 -7
- package/package.json +1 -1
package/dist/turboforms.html
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
27334
|
-
if (!
|
|
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
|
-
|
|
27349
|
-
|
|
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")),
|
|
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
|
-
//
|
|
27376
|
-
|
|
27377
|
-
|
|
27378
|
-
video.
|
|
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
|
-
//
|
|
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.
|
|
27413
|
-
|
|
27414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
27687
|
-
height:
|
|
27721
|
+
width: 48px;
|
|
27722
|
+
height: 48px;
|
|
27688
27723
|
border: none;
|
|
27689
27724
|
border-radius: 50%;
|
|
27690
|
-
background: rgba(0, 0, 0, 0.
|
|
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.
|
|
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.
|
|
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.
|
|
27774
|
-
transform: scale(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:
|
|
27801
|
-
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:
|
|
27806
|
-
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:
|
|
27811
|
-
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
|
|
29183
|
+
const getReq = store.get(Id);
|
|
29140
29184
|
|
|
29141
|
-
|
|
29142
|
-
|
|
29143
|
-
|
|
29144
|
-
|
|
29145
|
-
|
|
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.
|
|
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
|
-
|
|
29650
|
-
|
|
29651
|
-
|
|
29652
|
-
|
|
29653
|
-
|
|
29654
|
-
|
|
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 =
|
|
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
|
|
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
|
|
29823
|
-
if (
|
|
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
|
|
29937
|
-
|
|
29938
|
-
|
|
29939
|
-
|
|
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
|
-
|
|
29980
|
-
|
|
29981
|
-
|
|
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
|
-
|
|
29994
|
-
|
|
29995
|
-
|
|
29996
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30003
|
-
|
|
30004
|
-
|
|
30005
|
-
|
|
30006
|
-
|
|
30007
|
-
|
|
30008
|
-
|
|
30009
|
-
|
|
30010
|
-
|
|
30011
|
-
|
|
30012
|
-
|
|
30013
|
-
|
|
30014
|
-
|
|
30015
|
-
|
|
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
|
-
|
|
30020
|
-
|
|
30021
|
-
|
|
30022
|
-
|
|
30023
|
-
|
|
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
|
-
|
|
30026
|
-
|
|
30354
|
+
for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
|
30355
|
+
var slice = byteCharacters.slice(offset, offset + sliceSize);
|
|
30027
30356
|
|
|
30028
|
-
|
|
30029
|
-
|
|
30030
|
-
|
|
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
|
|
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
|
-
|
|
30036
|
-
}
|
|
30383
|
+
logger.info(`[TF_SDK:83b] ✅ Annotated Image file size ${blob.size / 1024 / 1024} MB`);
|
|
30037
30384
|
|
|
30038
|
-
|
|
30039
|
-
|
|
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
|
-
|
|
30118
|
-
|
|
30119
|
-
|
|
30120
|
-
|
|
30121
|
-
|
|
30122
|
-
|
|
30123
|
-
|
|
30124
|
-
|
|
30125
|
-
|
|
30126
|
-
|
|
30127
|
-
|
|
30128
|
-
|
|
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
|
-
|
|
30165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31124
|
-
|
|
31125
|
-
|
|
31126
|
-
|
|
31127
|
-
|
|
31128
|
-
|
|
31129
|
-
|
|
31130
|
-
|
|
31131
|
-
|
|
31132
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38317
|
-
|
|
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
|
-
|
|
38327
|
-
|
|
38328
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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 -
|
|
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
|
|
38361
|
-
|
|
38362
|
-
|
|
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
|
-
//
|
|
38373
|
-
|
|
38374
|
-
|
|
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
|
-
|
|
38406
|
-
const
|
|
38407
|
-
|
|
38408
|
-
|
|
38409
|
-
|
|
38410
|
-
|
|
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
|
-
|
|
38424
|
-
|
|
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
|
-
|
|
41159
|
-
|
|
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.
|
|
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
|