PageSourceSearch

https://ppoagroup.com/js/forms-pdfjs-interop.js?v=2026-04-29-r5

js ppoagroup.com collected 2026-09-24 14:50:17 UTC 28,128 bytes, 617 lines download raw bytes

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.