1// PPOAGroup Forms â PDF.js + designer interop. 2// 3-canvas stack inside one position:relative parent: 3// z=0 PDF canvas (passed in by Blazor) â PDF.js render target 4// z=10 .forms-overlay-canvas (pointer-events:none) â field rectangles + labels 5// z=20 .forms-handle-layer (catches mouse events) â resize handles + drag dispatch 6// 7// All overlay/handle math is done in CSS pixels; canvas pixel buffers are 8// DPR-scaled separately so high-DPI displays stay sharp without misaligning 9// the hit-tests. Pattern ported from TowSmart's pdfjs-interop.js. 10// 11// Field types (server enum order): 0=Signature 1=Initial 2=TypedName 3=Date 4=FreeText 5=Checkbox 6=StaticText 12// 13// Blazor â JS: 14// await formsPdfInterop.getPageCount(pdfBase64) -> number 15// await formsPdfInterop.renderPage(canvas, pdfBase64, pageNum, scale=1.5) -> string (doc cache key) 16// Throws "renderPage: canvas element is null." if the Blazor ElementReference 17// hasn't been resolved yet â caller is expected to retry with a short delay. 18// Responsive scaling is done in CSS (canvas style="width:100%; max-width:..."), 19// not in this module â render at fixed scale, let the layout shrink the canvas. 20// formsPdfInterop.setPageFields(canvas, fields, selectedIndex) // call any time fields/selection change 21// formsPdfInterop.initInteractions(canvas, dotnetRef) // install handle layer + listeners 22// formsPdfInterop.destroyInteractions() 23// formsPdfInterop.clearOverlay(canvas) 24// formsPdfInterop.dispose() 25// 26// JS â Blazor [JSInvokable] (on dotnetRef): 27// OnFieldClicked(pageLocalIndex) 28// OnCanvasClickAtPct(xPct, yPct) 29// OnFieldGeometryChanged(pageLocalIndex, xPct, yPct, widthPct, heightPct) 30(function () { 31 'use strict'; 32 if (window.formsPdfInterop) { return; } 33 34 const PDFJS_SRC = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js'; 35 const PDFJS_WORKER = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; 36 37 // Brand-aligned palette (matches PPOAGroup theme tokens). 0..6 in enum order. 38 const FIELD_COLORS = [ 39 'rgba(90, 167, 255, 0.85)', // Signature â brand blue 40 'rgba(143, 170, 255, 0.85)', // Initial â brand blue-secondary 41 'rgba(34, 211, 238, 0.85)', // TypedName â cyan 42 'rgba(245, 158, 11, 0.85)', // Date â amber 43 'rgba(34, 197, 94, 0.85)', // FreeText â success green 44 'rgba(236, 72, 153, 0.85)', // Checkbox â pink 45 'rgba(153, 153, 153, 0.85)' // StaticText â text-dim 46 ]; 47 const FIELD_FILL_OPACITY = 0.18; 48 const SELECTED_BORDER = 'rgba(90, 167, 255, 1)'; 49 const FIELD_LABELS = ['Signature', 'Initial', 'Typed Name', 'Date', 'Text', 'Checkbox', 'Static Text']; 50 const HANDLE_PX = 8; 51 const HANDLE_HIT_PAD = 4; // extra hit area around handles 52 const MIN_SIZE_PCT = 1.5; 53 54 let _pdfDoc = null; 55 let _pdfBase64 = null; 56 57 // Single-canvas-at-a-time state (the designer only renders one page canvas). 58 let _state = { 59 pdfCanvas: null, 60 overlay: null, 61 handleLayer: null, 62 fields: [], 63 selectedIndex: -1, 64 dotnetRef: null, 65 listeners: null, 66 drag: null 67 }; 68 69 // ââ PDF.js loading ââââââââââââââââââââââââââââââââââââââââââââ 70 71 async function ensureLoaded(timeoutMs) { 72 if (window.pdfjsLib) return; 73 await new Promise(function (resolve, reject) { 74 const existing = document.querySelector('script[data-forms-pdfjs]'); 75 if (existing) { 76 if (window.pdfjsLib) return resolve(); 77 existing.addEventListener('load', () => resolve()); 78 existing.addEventListener('error', () => reject(new Error('pdf.js failed to load'))); 79 return; 80 } 81 const s = document.createElement('script'); 82 s.src = PDFJS_SRC; 83 s.async = true; 84 s.dataset.formsPdfjs = '1'; 85 s.onload = function () { resolve(); }; 86 s.onerror = function () { reject(new Error('pdf.js failed to load')); }; 87 document.head.appendChild(s); 88 setTimeout(function () { reject(new Error('pdf.js load timeout')); }, timeoutMs || 15000); 89 }); 90 if (window.pdfjsLib && window.pdfjsLib.GlobalWorkerOptions) { 91 window.pdfjsLib.GlobalWorkerOptions.workerSrc = PDFJS_WORKER;
92 } 93 } 94 95 function base64ToUint8Array(b64) { 96 const bin = atob(b64); 97 const len = bin.length; 98 const out = new Uint8Array(len); 99 for (let i = 0; i < len; i++) out[i] = bin.charCodeAt(i); 100 return out; 101 } 102 103 async function loadDoc(pdfBase64) { 104 if (_pdfDoc && _pdfBase64 === pdfBase64) return _pdfDoc; 105 await ensureLoaded(15000); 106 const data = base64ToUint8Array(pdfBase64); 107 _pdfDoc = await window.pdfjsLib.getDocument({ data }).promise; 108 _pdfBase64 = pdfBase64; 109 return _pdfDoc; 110 } 111 112 async function getPageCount(pdfBase64) { 113 const doc = await loadDoc(pdfBase64); 114 return doc.numPages; 115 } 116 117 // ââ Canvas helpers (CSS-pixel math, DPR-aware) âââââââââââââââ 118 119 function getDpr() { return window.devicePixelRatio || 1; } 120 121 function cssSize(canvas) { 122 if (!canvas) return { w: 0, h: 0 }; 123 const rect = canvas.getBoundingClientRect(); 124 const w = rect.width || parseFloat(canvas.style.width) || canvas.width || 0; 125 const h = rect.height || parseFloat(canvas.style.height) || canvas.height || 0; 126 return { w, h }; 127 } 128 129 function sizeCanvasToCss(canvas, cssW, cssH, dpr, responsive) { 130 if (!canvas) return; 131 canvas.width = Math.round(cssW * dpr); 132 canvas.height = Math.round(cssH * dpr); 133 canvas.style.aspectRatio = cssW + ' / ' + cssH; 134 if (responsive) { 135 // Signing mode: fill the parent (.canvas-inner) at any width. 136 // No explicit pixel dimensions â they'd defeat the responsive 137 // sizing on phones. 138 canvas.style.width = '100%'; 139 canvas.style.height = 'auto'; 140 canvas.style.maxWidth = '100%'; 141 } else { 142 // Designer mode: explicit pixel dimensions matching the PDF canvas. 143 canvas.style.width = cssW + 'px'; 144 canvas.style.height = 'auto'; 145 canvas.style.maxWidth = ''; 146 } 147 const ctx = canvas.getContext('2d'); 148 if (ctx && typeof ctx.setTransform === 'function') { 149 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 150 } 151 } 152 153 function getOrCreateOverlay(pdfCanvas) { 154 if (!pdfCanvas || !pdfCanvas.parentElement) return null; 155 const parent = pdfCanvas.parentElement; 156 if (parent.style.position !== 'relative' && parent.style.position !== 'absolute') { 157 parent.style.position = 'relative'; 158 } 159 let overlay = parent.querySelector('.forms-overlay-canvas'); 160 if (overlay) return overlay; 161 overlay = document.createElement('canvas'); 162 overlay.className = 'forms-overlay-canvas'; 163 overlay.style.position = 'absolute'; 164 overlay.style.top = '0'; 165 overlay.style.left = '0'; 166 overlay.style.pointerEvents = 'none'; 167 overlay.style.zIndex = '10'; 168 parent.appendChild(overlay); 169 return overlay; 170 } 171 172 function getOrCreateHandleLayer(pdfCanvas) { 173 if (!pdfCanvas || !pdfCanvas.parentElement) return null; 174 const parent = pdfCanvas.parentElement; 175 if (parent.style.position !== 'relative' && parent.style.position !== 'absolute') { 176 parent.style.position = 'relative'; 177 } 178 let layer = parent.querySelector('.forms-handle-layer'); 179 if (layer) return layer; 180 layer = document.createElement('canvas'); 181 layer.className = 'forms-handle-layer'; 182 layer.style.position = 'absolute'; 183 layer.style.top = '0'; 184 layer.style.left = '0'; 185 layer.style.zIndex = '20'; 186 layer.style.cursor = 'crosshair'; 187 // CRITICAL â default to pointer-events:none so the layer never absorbs 188 // clicks meant for the per-field <button>s underneath. The designer's 189 // initInteractions() flips this to 'auto' when it binds mouse-down / 190 // mouse-move handlers; destroyInteractions() flips it back. On the 191 // signing page (where initInteractions is never called) this stays 192 // 'none' for the lifetime of the layer, so the click-to-place buttons 193 // actually receive their @onclick events. 194 layer.style.pointerEvents = 'none'; 195 parent.appendChild(layer); 196 return layer; 197 } 198 199 // ââ Drawing âââââââââââââââââââââââââââââââââââââââââââââââââââ 200 201 // Render a single page at a fixed numeric scale. Caller is responsible for 202 // responsive sizing via CSS on the canvas element (e.g. `width:100%; 203 // max-width: 612px; height: auto;`). The intent here mirrors TowSmart's 204 // pdfjs-interop: keep this function simple and idempotent so re-entrancy is 205 // guarded on the C# side via a `_rendering` bool â much less brittle than 206 // tracking pdf.js RenderTasks across an async boundary. 207 //
208 // Returns a stable string "doc key" so callers see a positive return value 209 // (avoids any chance of NaN / undefined deserialization on the Blazor side). 210 async function renderPage(canvasEl, pdfBase64, pageNumber, scale, options) { 211 if (!canvasEl) throw new Error('renderPage: canvas element is null.'); 212 options = options || {}; 213 const responsive = options.responsive === true; 214 const doc = await loadDoc(pdfBase64); 215 const page = await doc.getPage(pageNumber); 216 const userScale = (typeof scale === 'number' && scale > 0) ? scale : 1.5; 217 const dpr = getDpr(); 218 219 const cssViewport = page.getViewport({ scale: userScale }); 220 const cssW = cssViewport.width; 221 const cssH = cssViewport.height; 222 223 // PDF render at userScale * dpr for sharpness on retina/zoom; viewport 224 // already bakes in the transform â do NOT also scale the context. 225 const renderViewport = page.getViewport({ scale: userScale * dpr }); 226 canvasEl.width = Math.round(renderViewport.width); 227 canvasEl.height = Math.round(renderViewport.height); 228 // Aspect-ratio is always set so the browser keeps the canvas's 229 // display proportions correct when CSS constrains either dimension. 230 canvasEl.style.aspectRatio = cssW + ' / ' + cssH; 231 232 if (responsive) { 233 // Signing-viewer mode: CSS in FormSigningViewer.razor controls 234 // the canvas display via `width: 100%` on the canvas inside a 235 // .canvas-inner that has `max-width: 612px`. We must NOT set 236 // explicit pixel width/height here â that would override the 237 // responsive CSS and force the canvas to its natural size on 238 // phones (the bug the user just reported). 239 canvasEl.style.width = ''; 240 canvasEl.style.height = ''; 241 } else { 242 // Designer mode: explicit pixel size for precise field placement 243 // and drag/resize math. height:auto + aspect-ratio still keeps 244 // proportions correct if the parent width is constrained. 245 canvasEl.style.width = cssW + 'px'; 246 canvasEl.style.height = 'auto'; 247 } 248 249 const ctx = canvasEl.getContext('2d'); 250 if (typeof ctx.setTransform === 'function') ctx.setTransform(1, 0, 0, 1, 0, 0); 251 await page.render({ canvasContext: ctx, viewport: renderViewport }).promise; 252 253 // Overlay + handle layers track the PDF canvas's display box. 254 const overlay = getOrCreateOverlay(canvasEl); 255 if (overlay) sizeCanvasToCss(overlay, cssW, cssH, dpr, responsive); 256 const handleLayer = getOrCreateHandleLayer(canvasEl); 257 if (handleLayer) sizeCanvasToCss(handleLayer, cssW, cssH, dpr, responsive); 258 259 // Stash references so subsequent draws/hit-tests don't need to re-query. 260 _state.pdfCanvas = canvasEl; 261 _state.overlay = overlay; 262 _state.handleLayer = handleLayer; 263 264 return 'pdf-' + (pdfBase64 ? pdfBase64.length : 0) + ':p' + pageNumber + ':s' + userScale; 265 } 266 267 function fieldColor(t) { 268 const i = (typeof t === 'number') ? t : 0; 269 return FIELD_COLORS[i] || FIELD_COLORS[FIELD_COLORS.length - 1]; 270 } 271 272 function fieldLabel(t) { return FIELD_LABELS[t] || ('Type ' + t); } 273 274 function rgbaWithAlpha(rgba, alpha) { 275 // 'rgba(r, g, b, _)' â 'rgba(r, g, b, alpha)' 276 return rgba.replace(/rgba\(([^,]+),([^,]+),([^,]+),[^)]+\)/, function (_m, r, g, b) { 277 return 'rgba(' + r.trim() + ',' + g.trim() + ',' + b.trim() + ',' + alpha + ')'; 278 }); 279 } 280 281 function drawFields(overlay, fields, selectedIndex) { 282 const ctx = overlay.getContext('2d'); 283 const { w, h } = cssSize(overlay); 284 ctx.clearRect(0, 0, w, h); 285 286 if (!fields || !fields.length) return; 287 ctx.font = '11px system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'; 288 ctx.textBaseline = 'top'; 289 290 for (let i = 0; i < fields.length; i++) { 291 const f = fields[i]; 292 const x = (f.xPct / 100) * w; 293 const y = (f.yPct / 100) * h; 294 const fw = (f.widthPct / 100) * w; 295 const fh = (f.heightPct / 100) * h; 296 const isSelected = (i === selectedIndex); 297 const stroke = fieldColor(f.fieldType); 298 const fill = rgbaWithAlpha(stroke, FIELD_FILL_OPACITY); 299 300 ctx.fillStyle = fill; 301 ctx.fillRect(x, y, fw, fh); 302 303 ctx.strokeStyle = isSelected ? SELECTED_BORDER : stroke; 304 ctx.lineWidth = isSelected ? 2 : 1.25; 305 if (isSelected) ctx.setLineDash([5, 3]); else ctx.setLineDash([]); 306 ctx.strokeRect(x + 0.5, y + 0.5, Math.max(0, fw - 1), Math.max(0, fh - 1)); 307 ctx.setLineDash([]); 308 309 // Label tab above the field â clipped to field width. 310 const text = (f.label && f.label.length ? f.label : (f.fieldName || fieldLabel(f.fieldType))) + (f.isRequired ? ' *' : ''); 311 const tabH = 14; 312 const padX = 4; 313 const tw = Math.min(fw, ctx.measureText(text).width + padX * 2); 314 const tabY = Math.max(0, y - tabH); 315 ctx.fillStyle = isSelected ? SELECTED_BORDER : stroke; 316 ctx.fillRect(x, tabY, tw, tabH); 317 ctx.fillStyle = '#000';
318 ctx.fillText(text, x + padX, tabY + 2, fw - padX * 2); 319 } 320 } 321 322 function handlePositions(x, y, w, h) { 323 return [ 324 { name: 'nw', cx: x, cy: y, cursor: 'nwse-resize' }, 325 { name: 'n', cx: x + w / 2, cy: y, cursor: 'ns-resize' }, 326 { name: 'ne', cx: x + w, cy: y, cursor: 'nesw-resize' }, 327 { name: 'e', cx: x + w, cy: y + h / 2, cursor: 'ew-resize' }, 328 { name: 'se', cx: x + w, cy: y + h, cursor: 'nwse-resize' }, 329 { name: 's', cx: x + w / 2, cy: y + h, cursor: 'ns-resize' }, 330 { name: 'sw', cx: x, cy: y + h, cursor: 'nesw-resize' }, 331 { name: 'w', cx: x, cy: y + h / 2, cursor: 'ew-resize' } 332 ]; 333 } 334 335 function drawHandles(handleLayer, fields, selectedIndex) { 336 const ctx = handleLayer.getContext('2d'); 337 const { w, h } = cssSize(handleLayer); 338 ctx.clearRect(0, 0, w, h); 339 if (selectedIndex < 0 || selectedIndex >= fields.length) return; 340 341 const f = fields[selectedIndex]; 342 const x = (f.xPct / 100) * w; 343 const y = (f.yPct / 100) * h; 344 const fw = (f.widthPct / 100) * w; 345 const fh = (f.heightPct / 100) * h; 346 const positions = handlePositions(x, y, fw, fh); 347 const hs = HANDLE_PX; 348 349 ctx.fillStyle = '#fff'; 350 ctx.strokeStyle = SELECTED_BORDER; 351 ctx.lineWidth = 1.5; 352 for (const p of positions) { 353 ctx.fillRect(p.cx - hs / 2, p.cy - hs / 2, hs, hs); 354 ctx.strokeRect(p.cx - hs / 2, p.cy - hs / 2, hs, hs); 355 } 356 } 357 358 function hitTestHandle(field, mx, my, cssW, cssH) { 359 const x = (field.xPct / 100) * cssW; 360 const y = (field.yPct / 100) * cssH; 361 const w = (field.widthPct / 100) * cssW; 362 const h = (field.heightPct / 100) * cssH; 363 const r = HANDLE_PX / 2 + HANDLE_HIT_PAD; 364 for (const p of handlePositions(x, y, w, h)) { 365 if (Math.abs(p.cx - mx) <= r && Math.abs(p.cy - my) <= r) return p; 366 } 367 return null; 368 } 369 370 function hitTestFieldBody(field, mx, my, cssW, cssH) { 371 const x = (field.xPct / 100) * cssW; 372 const y = (field.yPct / 100) * cssH; 373 const w = (field.widthPct / 100) * cssW; 374 const h = (field.heightPct / 100) * cssH; 375 return mx >= x && mx <= x + w && my >= y && my <= y + h; 376 } 377 378 function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); } 379 380 function applyResize(f, o, handle, dxPct, dyPct) { 381 let x = o.xPct, y = o.yPct, w = o.widthPct, h = o.heightPct; 382 if (handle.indexOf('e') !== -1) w = clamp(o.widthPct + dxPct, MIN_SIZE_PCT, 100 - o.xPct); 383 if (handle.indexOf('s') !== -1) h = clamp(o.heightPct + dyPct, MIN_SIZE_PCT, 100 - o.yPct); 384 if (handle.indexOf('w') !== -1) { 385 const newX = clamp(o.xPct + dxPct, 0, o.xPct + o.widthPct - MIN_SIZE_PCT); 386 w = o.widthPct + (o.xPct - newX); 387 x = newX; 388 } 389 if (handle.indexOf('n') !== -1) { 390 const newY = clamp(o.yPct + dyPct, 0, o.yPct + o.heightPct - MIN_SIZE_PCT); 391 h = o.heightPct + (o.yPct - newY); 392 y = newY; 393 } 394 f.xPct = x; f.yPct = y; f.widthPct = w; f.heightPct = h; 395 } 396 397 function redraw() { 398 if (_state.overlay) drawFields(_state.overlay, _state.fields, _state.selectedIndex); 399 if (_state.handleLayer) drawHandles(_state.handleLayer, _state.fields, _state.selectedIndex); 400 } 401 402 // ââ Public API ââââââââââââââââââââââââââââââââââââââââââââââââ 403 404 function setPageFields(canvasEl, fields, selectedIndex) { 405 _state.pdfCanvas = canvasEl; 406 if (!_state.overlay) _state.overlay = getOrCreateOverlay(canvasEl); 407 if (!_state.handleLayer) _state.handleLayer = getOrCreateHandleLayer(canvasEl); 408 // Deep copy so JS can mutate during drag without aliasing the .NET payload. 409 _state.fields = (fields || []).map(f => Object.assign({}, f)); 410 _state.selectedIndex = (typeof selectedIndex === 'number') ? selectedIndex : -1; 411 redraw(); 412 } 413 414 function clearOverlayFor(canvasEl) { 415 const parent = canvasEl && canvasEl.parentElement; 416 if (!parent) return;
417 const overlay = parent.querySelector('.forms-overlay-canvas'); 418 if (overlay) { 419 const ctx = overlay.getContext('2d'); 420 const { w, h } = cssSize(overlay); 421 ctx.clearRect(0, 0, w, h); 422 } 423 const layer = parent.querySelector('.forms-handle-layer'); 424 if (layer) { 425 const ctx = layer.getContext('2d'); 426 const { w, h } = cssSize(layer); 427 ctx.clearRect(0, 0, w, h); 428 } 429 } 430 431 function destroyInteractions() { 432 if (_state.listeners && _state.handleLayer) { 433 _state.handleLayer.removeEventListener('mousedown', _state.listeners.mousedown); 434 document.removeEventListener('mousemove', _state.listeners.mousemove); 435 document.removeEventListener('mouseup', _state.listeners.mouseup); 436 } 437 // Restore click-through so the layer doesn't keep eating events after 438 // the designer tears down its listeners. 439 if (_state.handleLayer) _state.handleLayer.style.pointerEvents = 'none'; 440 _state.listeners = null; 441 _state.drag = null; 442 } 443 444 function initInteractions(canvasEl, dotnetRef) { 445 destroyInteractions(); 446 _state.pdfCanvas = canvasEl; 447 _state.overlay = getOrCreateOverlay(canvasEl); 448 _state.handleLayer = getOrCreateHandleLayer(canvasEl); 449 _state.dotnetRef = dotnetRef; 450 // Designer mode is binding listeners â the layer needs to receive 451 // pointer events for drag/resize to work. 452 _state.handleLayer.style.pointerEvents = 'auto'; 453 454 // Re-sync handle layer size in case initInteractions ran before renderPage. 455 const pdfCss = cssSize(canvasEl); 456 if (pdfCss.w > 0 && pdfCss.h > 0) { 457 sizeCanvasToCss(_state.handleLayer, pdfCss.w, pdfCss.h, getDpr()); 458 sizeCanvasToCss(_state.overlay, pdfCss.w, pdfCss.h, getDpr()); 459 } 460 461 const layer = _state.handleLayer; 462 463 const onMouseDown = function (ev) { 464 if (ev.button !== 0) return; // primary button only 465 const rect = layer.getBoundingClientRect(); 466 if (rect.width <= 0 || rect.height <= 0) return; 467 const mx = ev.clientX - rect.left; 468 const my = ev.clientY - rect.top; 469 const cssW = rect.width; 470 const cssH = rect.height; 471 472 // 1) Resize handle of the currently-selected field? 473 if (_state.selectedIndex >= 0 && _state.selectedIndex < _state.fields.length) { 474 const sf = _state.fields[_state.selectedIndex]; 475 const hit = hitTestHandle(sf, mx, my, cssW, cssH); 476 if (hit) { 477 _state.drag = { 478 type: 'resize', 479 handle: hit.name, 480 index: _state.selectedIndex, 481 startX: mx, 482 startY: my, 483 original: { xPct: sf.xPct, yPct: sf.yPct, widthPct: sf.widthPct, heightPct: sf.heightPct } 484 }; 485 ev.preventDefault(); 486 ev.stopPropagation(); 487 return; 488 } 489 } 490 491 // 2) Body of any field, top-down (last-drawn is on top). 492 for (let i = _state.fields.length - 1; i >= 0; i--) { 493 const f = _state.fields[i]; 494 if (hitTestFieldBody(f, mx, my, cssW, cssH)) { 495 _state.selectedIndex = i; 496 _state.drag = { 497 type: 'move', 498 index: i, 499 startX: mx, 500 startY: my, 501 original: { xPct: f.xPct, yPct: f.yPct, widthPct: f.widthPct, heightPct: f.heightPct } 502 }; 503 redraw(); 504 if (_state.dotnetRef) { 505 try { _state.dotnetRef.invokeMethodAsync('OnFieldClicked', i); } catch (_e) { /* ignore */ } 506 } 507 ev.preventDefault(); 508 ev.stopPropagation(); 509 return; 510 } 511 } 512 513 // 3) Empty area â place a new field at click position. 514 const xPct = (mx / cssW) * 100; 515 const yPct = (my / cssH) * 100; 516 if (_state.dotnetRef) { 517 try { _state.dotnetRef.invokeMethodAsync('OnCanvasClickAtPct', xPct, yPct); } catch (_e) { /* ignore */ } 518 } 519 ev.preventDefault(); 520 }; 521 522 const onMouseMove = function (ev) { 523 const rect = layer.getBoundingClientRect(); 524 if (rect.width <= 0 || rect.height <= 0) return; 525 const mx = ev.clientX - rect.left; 526 const my = ev.clientY - rect.top; 527 const cssW = rect.width; 528 const cssH = rect.height; 529 530 // Cursor hint when not dragging. 531 if (!_state.drag) { 532 if (mx < 0 || my < 0 || mx > cssW || my > cssH) { 533 layer.style.cursor = 'crosshair'; 534 return; 535 } 536 let cur = 'crosshair'; 537 if (_state.selectedIndex >= 0 && _state.selectedIndex < _state.fields.length) { 538 const sf = _state.fields[_state.selectedIndex]; 539 const hit = hitTestHandle(sf, mx, my, cssW, cssH); 540 if (hit) cur = hit.cursor; 541 else if (hitTestFieldBody(sf, mx, my, cssW, cssH)) cur = 'move'; 542 } 543 if (cur === 'crosshair') { 544 for (let i = _state.fields.length - 1; i >= 0; i--) { 545 if (hitTestFieldBody(_state.fields[i], mx, my, cssW, cssH)) { cur = 'move'; break; } 546 } 547 } 548 layer.style.cursor = cur; 549 return; 550 } 551 552 ev.preventDefault(); 553 const dxPct = ((mx - _state.drag.startX) / cssW) * 100; 554 const dyPct = ((my - _state.drag.startY) / cssH) * 100; 555 const f = _state.fields[_state.drag.index]; 556 if (!f) return; 557 const o = _state.drag.original; 558 559 if (_state.drag.type === 'move') { 560 f.xPct = clamp(o.xPct + dxPct, 0, 100 - o.widthPct); 561 f.yPct = clamp(o.yPct + dyPct, 0, 100 - o.heightPct); 562 f.widthPct = o.widthPct; 563 f.heightPct = o.heightPct; 564 } else { 565 applyResize(f, o, _state.drag.handle, dxPct, dyPct); 566 } 567 // Round to 2 decimals to match the .NET model precision. 568 f.xPct = Math.round(f.xPct * 100) / 100; 569 f.yPct = Math.round(f.yPct * 100) / 100; 570 f.widthPct = Math.round(f.widthPct * 100) / 100; 571 f.heightPct = Math.round(f.heightPct * 100) / 100; 572 redraw(); 573 }; 574 575 const onMouseUp = function () { 576 if (!_state.drag) return; 577 const idx = _state.drag.index; 578 const f = _state.fields[idx]; 579 _state.drag = null; 580 if (!f || !_state.dotnetRef) return; 581 try { 582 _state.dotnetRef.invokeMethodAsync('OnFieldGeometryChanged', idx, f.xPct, f.yPct, f.widthPct, f.heightPct); 583 } catch (_e) { /* ignore */ } 584 }; 585 586 layer.addEventListener('mousedown', onMouseDown);
587 document.addEventListener('mousemove', onMouseMove); 588 document.addEventListener('mouseup', onMouseUp); 589 590 _state.listeners = { mousedown: onMouseDown, mousemove: onMouseMove, mouseup: onMouseUp }; 591 } 592 593 function dispose() { 594 destroyInteractions(); 595 _pdfDoc = null; 596 _pdfBase64 = null; 597 _state = { 598 pdfCanvas: null, overlay: null, handleLayer: null, 599 fields: [], selectedIndex: -1, dotnetRef: null, 600 listeners: null, drag: null 601 }; 602 } 603 604 window.formsPdfInterop = { 605 ensureLoaded, 606 getPageCount, 607 renderPage, 608 setPageFields, 609 clearOverlay: clearOverlayFor, 610 initInteractions, 611 destroyInteractions, 612 // Back-compat aliases for any older call sites. 613 initDragResize: initInteractions, 614 destroyDragResize: destroyInteractions, 615 dispose 616 }; 617})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.