(function (window, document) { "use strict"; function qs(root, selector) { return root ? root.querySelector(selector) : null; } function qsa(root, selector) { return root ? Array.prototype.slice.call(root.querySelectorAll(selector)) : []; } function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function uid() { return "sig_" + Date.now().toString(36) + Math.random().toString(36).slice(2); } function parseJson(value, fallback) { try { return JSON.parse(value || ""); } catch (ignore) { return fallback; } } function parseState(value) { var state = parseJson(value, {}); state.CurrentPage = state.CurrentPage || 1; state.Scale = state.Scale || 1; state.Placements = state.Placements || []; state.SignMode = !!state.SignMode; state.SelectedPlacementId = state.SelectedPlacementId || ""; state.ActiveTab = state.ActiveTab || "draw"; state.IsFullscreen = !!state.IsFullscreen; var hasPropertiesPanelState = typeof state.PropertiesPanelOpen === "boolean"; state.PropertiesPanelOpen = hasPropertiesPanelState ? state.PropertiesPanelOpen : !!state.SelectedPlacementId; return state; } function executePostback(code) { if (code) { window.eval(code); } } function formatSize(size) { var value = Number(size || 0); if (value < 1024) { return value + " B"; } if (value < 1024 * 1024) { return (value / 1024).toFixed(1) + " KB"; } return (value / (1024 * 1024)).toFixed(1) + " MB"; } function postFile(url, kind, file, callback) { var form = new FormData(); form.append("kind", kind); form.append("file", file, file.name); var request = new XMLHttpRequest(); request.open("POST", url, true); request.onreadystatechange = function () { if (request.readyState !== 4) { return; } var response = parseJson(request.responseText, null); if (request.status >= 200 && request.status < 300 && response && response.success) { callback(null, response); } else { callback(new Error(response && response.message ? response.message : "No fue posible cargar el archivo.")); } }; request.send(form); } function postCommand(url, kind, id, callback) { var form = new FormData(); form.append("kind", kind); form.append("id", id || ""); var request = new XMLHttpRequest(); request.open("POST", url, true); request.onreadystatechange = function () { if (request.readyState !== 4) { return; } callback(request.status >= 200 && request.status < 300); }; request.send(form); } function PdfControl(root) { this.root = root; this.uploadUrl = root.getAttribute("data-upload-url"); this.actionField = document.getElementById(root.getAttribute("data-action-id")); this.stateField = document.getElementById(root.getAttribute("data-state-id")); this.uploads = []; this.viewer = null; this.bindUploadScreen(); this.bindViewer(); } PdfControl.prototype.setAction = function (action) { if (this.actionField) { this.actionField.value = JSON.stringify(action || {}); } }; PdfControl.prototype.bindUploadScreen = function () { var self = this; var zone = qs(this.root, ".sig-pdf-upload-dropzone"); var input = qs(this.root, ".sig-pdf-upload-input"); if (!zone || !input) { return; } function addFiles(files) { var max = Number(self.root.getAttribute("data-max-pdf-files") || 20); var allowMultiple = self.root.getAttribute("data-allow-multiple") === "true"; var list = Array.prototype.slice.call(files || []); if (!allowMultiple && list.length > 1) { list = [list[0]]; } list.forEach(function (file) { if (self.uploads.length >= max) { return; } if (!/\.pdf$/i.test(file.name) && file.type !== "application/pdf") { window.alert("Solo se permiten archivos PDF."); return; } postFile(self.uploadUrl, "pdf", file, function (error, response) { if (error) { window.alert(error.message); return; } self.uploads.push({ id: response.id, name: response.name || file.name, size: response.size || file.size }); self.renderUploads(); }); }); } var usingDropzone = false; if (window.Dropzone) { usingDropzone = true; window.Dropzone.autoDiscover = false; input.style.display = "none"; var pdfDropzone = new window.Dropzone(zone, { url: self.uploadUrl, paramName: "file", params: { kind: "pdf" }, acceptedFiles: "application/pdf,.pdf", maxFilesize: 50, maxFiles: self.root.getAttribute("data-allow-multiple") === "true" ? Number(self.root.getAttribute("data-max-pdf-files") || 20) : 1, uploadMultiple: false, parallelUploads: 1, previewsContainer: false, createImageThumbnails: false, clickable: true, accept: function (file, done) { var max = Number(self.root.getAttribute("data-max-pdf-files") || 20); if (self.uploads.length >= max) { done("Se alcanzó el límite de archivos."); } else { done(); } } }); pdfDropzone.on("success", function (file, response) { var data = typeof response === "string" ? parseJson(response, null) : response; if (!data || !data.success) { window.alert(data && data.message ? data.message : "No fue posible cargar el PDF."); return; } self.uploads.push({ id: data.id, name: data.name || file.name, size: data.size || file.size }); self.renderUploads(); pdfDropzone.removeFile(file); }); pdfDropzone.on("error", function (file, message) { var data = typeof message === "string" ? parseJson(message, null) : message; window.alert(data && data.message ? data.message : (typeof message === "string" ? message : "No fue posible cargar el PDF.")); pdfDropzone.removeFile(file); }); } if (!usingDropzone) { zone.addEventListener("click", function (event) { if (event.target !== input) { input.click(); } }); input.addEventListener("change", function () { addFiles(this.files); this.value = ""; }); zone.addEventListener("dragover", function (event) { event.preventDefault(); zone.classList.add("is-dragover"); }); zone.addEventListener("dragleave", function () { zone.classList.remove("is-dragover"); }); zone.addEventListener("drop", function (event) { event.preventDefault(); zone.classList.remove("is-dragover"); addFiles(event.dataTransfer.files); }); } var clearButton = qs(this.root, ".sig-pdf-upload-clear"); var combineButton = qs(this.root, ".sig-pdf-upload-combine"); if (clearButton) { clearButton.addEventListener("click", function () { self.uploads.forEach(function (item) { postCommand(self.uploadUrl, "remove-pdf", item.id, function () {}); }); self.uploads = []; self.renderUploads(); }); } if (combineButton) { combineButton.addEventListener("click", function () { if (!self.uploads.length) { return; } self.setAction({ Action: "combine-pdfs", UploadIds: self.uploads.map(function (item) { return item.id; }) }); executePostback(self.root.getAttribute("data-postback-combine")); }); } self.renderUploads(); }; PdfControl.prototype.renderUploads = function () { var self = this; var list = qs(this.root, ".sig-pdf-upload-list"); var combineButton = qs(this.root, ".sig-pdf-upload-combine"); if (!list) { return; } list.innerHTML = ""; this.uploads.forEach(function (item, index) { var row = document.createElement("div"); row.className = "sig-pdf-upload-item"; row.innerHTML = "" + (index + 1) + "" + "" + "" + "" + "" + "" + ""; qs(row, "strong").textContent = item.name; qs(row, "small").textContent = formatSize(item.size); qs(row, ".sig-upload-up").disabled = index === 0; qs(row, ".sig-upload-down").disabled = index === self.uploads.length - 1; qs(row, ".sig-upload-up").addEventListener("click", function () { if (index <= 0) { return; } var temp = self.uploads[index - 1]; self.uploads[index - 1] = self.uploads[index]; self.uploads[index] = temp; self.renderUploads(); }); qs(row, ".sig-upload-down").addEventListener("click", function () { if (index >= self.uploads.length - 1) { return; } var temp = self.uploads[index + 1]; self.uploads[index + 1] = self.uploads[index]; self.uploads[index] = temp; self.renderUploads(); }); qs(row, ".sig-upload-remove").addEventListener("click", function () { postCommand(self.uploadUrl, "remove-pdf", item.id, function () {}); self.uploads.splice(index, 1); self.renderUploads(); }); list.appendChild(row); }); if (combineButton) { combineButton.disabled = this.uploads.length === 0; } }; PdfControl.prototype.bindViewer = function () { var viewerRoot = qs(this.root, ".sig-pdf-viewer"); if (!viewerRoot) { return; } this.viewer = new Viewer(this, viewerRoot); }; function Viewer(control, root) { this.control = control; this.root = root; this.pdfUrl = root.getAttribute("data-pdf-url"); this.bestUrl = root.getAttribute("data-best-url"); this.downloadUrl = root.getAttribute("data-download-url"); this.workerUrl = root.getAttribute("data-worker-url"); this.state = parseState(control.stateField ? control.stateField.value : ""); this.currentPage = Number(this.state.CurrentPage || 1); this.scale = Number(this.state.Scale || 1); this.pdf = null; this.renderTask = null; this.pageWidth = 0; this.pageHeight = 0; this.selectedId = this.state.SelectedPlacementId || null; this.signMode = !!this.state.SignMode; this.propertiesPanelOpen = !!this.state.PropertiesPanelOpen; this.currentSignature = null; this.uploadedSignature = null; this.strokes = []; this.redoStrokes = []; this.currentStroke = null; this.drawColor = "#111111"; this.pendingDrawColor = this.drawColor; this.bind(); this.load(); } Viewer.prototype.bind = function () { var self = this; this.canvas = qs(this.root, ".sig-pdf-canvas"); this.layer = qs(this.root, ".sig-signature-layer"); this.shell = qs(this.root, ".sig-page-shell"); this.loading = qs(this.root, ".sig-loading"); this.leftPanel = qs(this.root, ".sig-left-panel"); this.rightPanel = qs(this.root, ".sig-right-panel"); this.startButton = qs(this.root, ".sig-start-signing"); this.finishButton = qs(this.root, ".sig-finish-signing"); this.propertiesToggle = qs(this.root, ".sig-toggle-properties"); this.modeLabel = qs(this.root, ".sig-mode-label"); this.pageInput = qs(this.root, ".sig-page-number"); this.pageCount = qs(this.root, ".sig-page-count"); this.zoomSelect = qs(this.root, ".sig-zoom-select"); this.drawCanvas = qs(this.root, ".sig-draw-canvas"); this.bindClick(".sig-prev-page", function () { self.goToPage(self.currentPage - 1); }); this.bindClick(".sig-next-page", function () { self.goToPage(self.currentPage + 1); }); this.bindClick(".sig-zoom-in", function () { self.setScale(self.scale + 0.25); }); this.bindClick(".sig-zoom-out", function () { self.setScale(self.scale - 0.25); }); this.bindClick(".sig-print", function () { window.open(self.bestUrl, "_blank", "noopener"); }); this.bindClick(".sig-download", function () { window.location.href = self.downloadUrl; }); this.bindClick(".sig-start-signing", function () { self.enterSignMode(); }); this.bindClick(".sig-finish-signing", function () { self.postback("data-postback-finalize"); }); this.bindClick(".sig-toggle-properties", function () { self.togglePropertiesPanel(); }); this.bindClick(".sig-refresh-document", function () { self.reloadDocument(); }); this.bindClick(".sig-change-documents", function () { self.postback("data-postback-reset-upload"); }); this.bindClick(".sig-expand-viewer", function () { self.setFullscreen(true); }); this.bindClick(".sig-collapse-viewer", function () { self.setFullscreen(false); }); this.bindClick(".sig-delete-signature", function () { self.deleteSelected(); }); this.bindClick(".sig-clear-all-signatures", function () { self.clearAll(); }); this.bindClick(".sig-download-signed", function () { self.postback("data-postback-download-signed"); }); this.bindClick(".sig-save-signed", function () { self.postback("data-postback-save-signed"); }); this.bindClick(".sig-save-exit", function () { self.postback("data-postback-save-exit"); }); if (this.pageInput) { this.pageInput.addEventListener("change", function () { self.goToPage(parseInt(this.value, 10)); }); } if (this.zoomSelect) { this.zoomSelect.addEventListener("change", function () { self.setScale(parseFloat(this.value)); }); } if (this.shell) { this.shell.addEventListener("click", function () { self.select(null); }); } this.bindTabs(); this.bindDrawPad(); this.bindDrawColorPicker(); this.bindSignatureUpload(); this.bindSavedSignatures(); this.bindPropertyEditors(); this.restoreUiState(); document.addEventListener("keydown", function (event) { if (event.keyCode === 27 && self.root.classList.contains("is-fullscreen")) { self.setFullscreen(false); } }); }; Viewer.prototype.bindClick = function (selector, handler) { var element = qs(this.root, selector); if (element) { element.addEventListener("click", handler); } }; Viewer.prototype.postback = function (attributeName) { this.syncState(); executePostback(this.control.root.getAttribute(attributeName)); }; Viewer.prototype.updatePropertiesToggle = function () { if (!this.propertiesToggle) { return; } var isOpen = this.signMode && this.propertiesPanelOpen; this.propertiesToggle.hidden = !this.signMode; this.propertiesToggle.classList.toggle( "is-active", isOpen ); this.propertiesToggle.setAttribute( "aria-expanded", isOpen ? "true" : "false" ); this.propertiesToggle.setAttribute( "title", isOpen ? "Ocultar propiedades de la firma" : "Mostrar propiedades de la firma" ); this.propertiesToggle.setAttribute( "aria-label", isOpen ? "Ocultar propiedades de la firma" : "Mostrar propiedades de la firma" ); }; Viewer.prototype.applyPropertiesPanelState = function () { var visible = this.signMode && this.propertiesPanelOpen; if (this.rightPanel) { this.rightPanel.classList.toggle( "is-open", visible ); } this.root.classList.toggle( "properties-open", visible ); this.updatePropertiesToggle(); }; Viewer.prototype.setPropertiesPanel = function ( open, sync ) { this.propertiesPanelOpen = !!open; this.state.PropertiesPanelOpen = this.propertiesPanelOpen; this.applyPropertiesPanelState(); if (sync !== false) { this.syncState(); } }; Viewer.prototype.showPropertiesForSignature = function () { this.setPropertiesPanel( true, false ); }; Viewer.prototype.togglePropertiesPanel = function () { if (!this.signMode) { return; } this.setPropertiesPanel( !this.propertiesPanelOpen, true ); }; Viewer.prototype.restoreUiState = function () { var self = this; var activeTab = this.state.ActiveTab || "draw"; qsa(this.root, ".sig-signature-tab").forEach(function (button) { button.classList.toggle( "is-active", button.getAttribute("data-tab") === activeTab ); }); qsa(this.root, ".sig-signature-tab-panel").forEach(function (panel) { panel.classList.toggle( "is-active", panel.classList.contains("sig-tab-" + activeTab) ); }); if (this.signMode) { this.root.classList.add("is-signing"); if (this.leftPanel) { this.leftPanel.classList.add("is-open"); } if (this.startButton) { this.startButton.hidden = true; } if (this.finishButton) { this.finishButton.hidden = false; } if (this.modeLabel) { this.modeLabel.textContent = "Modo de firma activo"; } } if (this.state.IsFullscreen) { this.root.classList.add("is-fullscreen"); document.body.classList.add( "sig-viewer-fullscreen-open" ); var expand = qs(this.root, ".sig-expand-viewer"); var collapse = qs(this.root, ".sig-collapse-viewer"); if (expand) { expand.hidden = true; } if (collapse) { collapse.hidden = false; } } if ( !this.selectedId || !this.state.Placements.some( function (placement) { return placement.Id === self.selectedId; } ) ) { this.selectedId = null; this.state.SelectedPlacementId = ""; } /* * Respeta la decisión manual del usuario. * Un postback no vuelve a abrir el panel * si el usuario lo había cerrado. */ this.applyPropertiesPanelState(); }; Viewer.prototype.finishSigningClient = function () { this.signMode = false; this.selectedId = null; this.state.SignMode = false; this.state.SelectedPlacementId = ""; this.root.classList.remove("is-signing"); this.root.classList.remove("properties-open"); if (this.leftPanel) { this.leftPanel.classList.remove("is-open"); } if (this.rightPanel) { this.rightPanel.classList.remove("is-open"); } if (this.startButton) { this.startButton.hidden = false; } if (this.finishButton) { this.finishButton.hidden = true; } if (this.modeLabel) { this.modeLabel.textContent = "Modo lectura"; } this.updatePropertiesToggle(); this.renderPlacements(); this.syncState(); }; Viewer.prototype.reloadDocument = function () { var cleanUrl = this.pdfUrl .replace(/([?&])_sigRefresh=\d+/g, "$1") .replace(/[?&]$/, ""); var separator = cleanUrl.indexOf("?") >= 0 ? "&" : "?"; this.pdfUrl = cleanUrl + separator + "_sigRefresh=" + new Date().getTime(); if (this.pdf && typeof this.pdf.destroy === "function") { try { this.pdf.destroy(); } catch (ignore) {} } this.pdf = null; this.load(); }; Viewer.prototype.addStoredSignatureClient = function (item) { if (!item || !item.Id) { return; } var list = qs(this.root, ".sig-saved-signature-list"); if (!list) { return; } var existing = qs( list, '.sig-saved-signature-item[data-asset-id="' + item.Id + '"]' ); if (existing) { return; } var button = document.createElement("button"); button.type = "button"; button.className = "sig-saved-signature-item"; button.setAttribute("data-asset-id", item.Id); button.setAttribute("data-image-url", item.ImageUrl || ""); button.setAttribute("data-name", item.Name || "Firma"); button.innerHTML = '' + ''; qs(button, ".sig-saved-signature-name").textContent = item.Name || "Firma"; var self = this; button.addEventListener("click", function () { var signature = { assetId: button.getAttribute("data-asset-id"), name: button.getAttribute("data-name"), imageUrl: button.getAttribute("data-image-url"), dataUrl: "" }; self.currentSignature = signature; self.previewSavedSignature(signature); self.placeCurrentSignature(); }); list.appendChild(button); }; Viewer.prototype.bindTabs = function () { var self = this; qsa(this.root, ".sig-signature-tab").forEach(function (button) { button.addEventListener("click", function () { var tab = button.getAttribute("data-tab"); self.state.ActiveTab = tab || "draw"; self.syncState(); qsa(self.root, ".sig-signature-tab").forEach(function (item) { item.classList.toggle("is-active", item === button); }); qsa(self.root, ".sig-signature-tab-panel").forEach(function (panel) { panel.classList.toggle("is-active", panel.classList.contains("sig-tab-" + tab)); }); }); }); }; Viewer.prototype.bindDrawPad = function () { var self = this; var canvas = this.drawCanvas; if (!canvas) { return; } var context = canvas.getContext("2d"); var drawing = false; function position(event) { var rect = canvas.getBoundingClientRect(); var source = event.touches && event.touches.length ? event.touches[0] : event; return { x: (source.clientX - rect.left) * canvas.width / rect.width, y: (source.clientY - rect.top) * canvas.height / rect.height }; } function begin(event) { event.preventDefault(); drawing = true; self.redoStrokes = []; self.currentStroke = { color: self.drawColor, width: Number(qs(self.root, ".sig-draw-width").value || 3), points: [position(event)] }; } function move(event) { if (!drawing || !self.currentStroke) { return; } event.preventDefault(); self.currentStroke.points.push(position(event)); self.redrawPad(); } function end(event) { if (!drawing) { return; } if (event) { event.preventDefault(); } drawing = false; if (self.currentStroke && self.currentStroke.points.length > 1) { self.strokes.push(self.currentStroke); } self.currentStroke = null; self.redrawPad(); } canvas.addEventListener("mousedown", begin); canvas.addEventListener("mousemove", move); document.addEventListener("mouseup", end); canvas.addEventListener("touchstart", begin, { passive: false }); canvas.addEventListener("touchmove", move, { passive: false }); canvas.addEventListener("touchend", end, { passive: false }); var widthInput = qs(this.root, ".sig-draw-width"); var widthOutput = qs(this.root, ".sig-draw-width-output"); if (widthInput && widthOutput) { widthInput.addEventListener("input", function () { widthOutput.textContent = this.value + "px"; }); } this.bindClick(".sig-draw-undo", function () { if (self.strokes.length) { self.redoStrokes.push(self.strokes.pop()); self.redrawPad(); } }); this.bindClick(".sig-draw-redo", function () { if (self.redoStrokes.length) { self.strokes.push(self.redoStrokes.pop()); self.redrawPad(); } }); this.bindClick(".sig-clear-signature-pad", function () { self.strokes = []; self.redoStrokes = []; self.currentStroke = null; self.currentSignature = null; self.redrawPad(); }); this.bindClick(".sig-use-signature-pad", function () { var dataUrl = self.getDrawnSignatureDataUrl(); if (!dataUrl) { window.alert("Dibuja una firma antes de continuar."); return; } self.currentSignature = { name: "Firma", dataUrl: dataUrl, assetId: "", imageUrl: dataUrl }; self.placeCurrentSignature(); }); this.bindClick(".sig-save-current-signature", function () { var dataUrl = self.getDrawnSignatureDataUrl(); if (!dataUrl) { window.alert("Dibuja una firma antes de guardarla."); return; } var nameInput = qs(self.root, ".sig-signature-name"); self.control.setAction({ Action: "save-signature", Name: nameInput ? nameInput.value : "Firma", DataUrl: dataUrl, AssetId: "" }); self.syncState(); executePostback(self.control.root.getAttribute("data-postback-save-signature")); }); }; Viewer.prototype.normalizeHexColor = function (value) { var color = String(value || "").replace(/\s+/g, "").toUpperCase(); if (/^[0-9A-F]{6}$/.test(color)) { color = "#" + color; } if (!/^#[0-9A-F]{6}$/.test(color)) { return ""; } return color; }; Viewer.prototype.setPendingDrawColor = function (value) { var color = this.normalizeHexColor(value); var input = qs(this.root, ".sig-draw-color-hex"); var preview = qs(this.root, ".sig-draw-color-pending-preview"); var error = qs(this.root, ".sig-draw-color-error"); if (input && input.value !== String(value || "").toUpperCase()) { input.value = String(value || "").toUpperCase(); } if (!color) { if (error) { error.hidden = false; } return false; } this.pendingDrawColor = color; if (input) { input.value = color; } if (preview) { preview.style.backgroundColor = color; } if (error) { error.hidden = true; } qsa(this.root, ".sig-draw-color-swatch").forEach(function (button) { button.classList.toggle( "is-selected", String(button.getAttribute("data-color") || "").toUpperCase() === color ); }); return true; }; Viewer.prototype.openDrawColorPicker = function () { var popover = qs(this.root, ".sig-draw-color-popover"); var trigger = qs(this.root, ".sig-draw-color-trigger"); if (!popover || !trigger) { return; } this.pendingDrawColor = this.drawColor; this.setPendingDrawColor(this.drawColor); popover.hidden = false; trigger.setAttribute("aria-expanded", "true"); var input = qs(this.root, ".sig-draw-color-hex"); if (input) { window.setTimeout(function () { input.focus(); input.select(); }, 0); } }; Viewer.prototype.closeDrawColorPicker = function (restore) { var popover = qs(this.root, ".sig-draw-color-popover"); var trigger = qs(this.root, ".sig-draw-color-trigger"); if (!popover || !trigger) { return; } if (restore) { this.pendingDrawColor = this.drawColor; this.setPendingDrawColor(this.drawColor); } popover.hidden = true; trigger.setAttribute("aria-expanded", "false"); }; Viewer.prototype.applyDrawColor = function () { var input = qs(this.root, ".sig-draw-color-hex"); var value = input ? input.value : this.pendingDrawColor; if (!this.setPendingDrawColor(value)) { return; } this.drawColor = this.pendingDrawColor; var preview = qs(this.root, ".sig-draw-color-preview"); var label = qs(this.root, ".sig-draw-color-value"); if (preview) { preview.style.backgroundColor = this.drawColor; } if (label) { label.textContent = this.drawColor; } this.closeDrawColorPicker(false); }; Viewer.prototype.bindDrawColorPicker = function () { var self = this; var picker = qs(this.root, ".sig-draw-color-picker"); var trigger = qs(this.root, ".sig-draw-color-trigger"); var input = qs(this.root, ".sig-draw-color-hex"); if (!picker || !trigger || !input) { return; } trigger.addEventListener("click", function (event) { event.preventDefault(); event.stopPropagation(); var popover = qs(self.root, ".sig-draw-color-popover"); if (popover && !popover.hidden) { self.closeDrawColorPicker(true); } else { self.openDrawColorPicker(); } }); qsa(this.root, ".sig-draw-color-swatch").forEach(function (button) { button.addEventListener("click", function (event) { event.preventDefault(); event.stopPropagation(); self.setPendingDrawColor(button.getAttribute("data-color")); }); }); input.addEventListener("input", function () { self.setPendingDrawColor(this.value); }); input.addEventListener("keydown", function (event) { if (event.keyCode === 13) { event.preventDefault(); self.applyDrawColor(); } else if (event.keyCode === 27) { event.preventDefault(); self.closeDrawColorPicker(true); } }); this.bindClick(".sig-draw-color-accept", function () { self.applyDrawColor(); }); this.bindClick(".sig-draw-color-cancel", function () { self.closeDrawColorPicker(true); }); picker.addEventListener("click", function (event) { event.stopPropagation(); }); this.root.addEventListener("click", function (event) { if (!picker.contains(event.target)) { self.closeDrawColorPicker(true); } }); }; Viewer.prototype.redrawPad = function () { var canvas = this.drawCanvas; if (!canvas) { return; } var context = canvas.getContext("2d"); context.clearRect(0, 0, canvas.width, canvas.height); var strokes = this.strokes.slice(0); if (this.currentStroke) { strokes.push(this.currentStroke); } strokes.forEach(function (stroke) { if (!stroke.points || stroke.points.length < 2) { return; } context.beginPath(); context.strokeStyle = stroke.color; context.lineWidth = stroke.width; context.lineCap = "round"; context.lineJoin = "round"; context.moveTo(stroke.points[0].x, stroke.points[0].y); for (var index = 1; index < stroke.points.length; index += 1) { context.lineTo(stroke.points[index].x, stroke.points[index].y); } context.stroke(); context.closePath(); }); }; Viewer.prototype.getDrawnSignatureDataUrl = function () { if (!this.drawCanvas || !this.strokes.length) { return ""; } return this.drawCanvas.toDataURL("image/png"); }; Viewer.prototype.bindSignatureUpload = function () { var self = this; var zone = qs(this.root, ".sig-signature-upload-dropzone"); var input = qs(this.root, ".sig-signature-upload-input"); if (!zone || !input) { return; } function upload(file) { if (!file) { return; } if (!/\.png$/i.test(file.name) && file.type !== "image/png") { window.alert("La firma debe ser un archivo PNG."); return; } postFile(self.control.uploadUrl, "signature", file, function (error, response) { if (error) { window.alert(error.message); return; } self.uploadedSignature = { assetId: response.id, name: response.name || file.name, imageUrl: response.url, dataUrl: "" }; self.renderUploadedSignature(); }); } var usingDropzone = false; if (window.Dropzone) { usingDropzone = true; window.Dropzone.autoDiscover = false; input.style.display = "none"; var signatureDropzone = new window.Dropzone(zone, { url: self.control.uploadUrl, paramName: "file", params: { kind: "signature" }, acceptedFiles: "image/png,.png", maxFilesize: 5, maxFiles: 1, uploadMultiple: false, previewsContainer: false, createImageThumbnails: false, clickable: true }); signatureDropzone.on("success", function (file, response) { var data = typeof response === "string" ? parseJson(response, null) : response; if (!data || !data.success) { window.alert(data && data.message ? data.message : "No fue posible cargar la firma."); return; } self.uploadedSignature = { assetId: data.id, name: data.name || file.name, imageUrl: data.url, dataUrl: "" }; self.renderUploadedSignature(); signatureDropzone.removeAllFiles(true); }); signatureDropzone.on("error", function (file, message) { var data = typeof message === "string" ? parseJson(message, null) : message; window.alert(data && data.message ? data.message : (typeof message === "string" ? message : "No fue posible cargar la firma.")); signatureDropzone.removeAllFiles(true); }); } if (!usingDropzone) { zone.addEventListener("click", function (event) { if (event.target !== input) { input.click(); } }); input.addEventListener("change", function () { upload(this.files && this.files[0]); this.value = ""; }); zone.addEventListener("dragover", function (event) { event.preventDefault(); zone.classList.add("is-dragover"); }); zone.addEventListener("dragleave", function () { zone.classList.remove("is-dragover"); }); zone.addEventListener("drop", function (event) { event.preventDefault(); zone.classList.remove("is-dragover"); upload(event.dataTransfer.files && event.dataTransfer.files[0]); }); } this.bindClick(".sig-use-uploaded-signature", function () { if (!self.uploadedSignature) { return; } self.currentSignature = self.uploadedSignature; self.placeCurrentSignature(); }); this.bindClick(".sig-save-uploaded-signature", function () { if (!self.uploadedSignature) { return; } var inputName = qs(self.root, ".sig-upload-signature-name"); self.control.setAction({ Action: "save-signature", Name: inputName && inputName.value ? inputName.value : self.uploadedSignature.name, DataUrl: "", AssetId: self.uploadedSignature.assetId }); self.syncState(); executePostback(self.control.root.getAttribute("data-postback-save-signature")); }); }; Viewer.prototype.renderUploadedSignature = function () { var preview = qs(this.root, ".sig-uploaded-signature-preview"); var useButton = qs(this.root, ".sig-use-uploaded-signature"); var saveButton = qs(this.root, ".sig-save-uploaded-signature"); if (!preview) { return; } preview.innerHTML = ""; if (this.uploadedSignature) { var image = document.createElement("img"); image.src = this.uploadedSignature.imageUrl; image.alt = this.uploadedSignature.name; preview.appendChild(image); } if (useButton) { useButton.disabled = !this.uploadedSignature; } if (saveButton) { saveButton.disabled = !this.uploadedSignature; } }; Viewer.prototype.bindSavedSignatures = function () { var self = this; qsa(this.root, ".sig-saved-signature-item").forEach(function (button) { button.addEventListener("click", function () { var signature = { assetId: button.getAttribute("data-asset-id"), name: button.getAttribute("data-name"), imageUrl: button.getAttribute("data-image-url"), dataUrl: "" }; self.currentSignature = signature; self.previewSavedSignature(signature); self.placeCurrentSignature(); }); }); var filter = qs(this.root, ".sig-saved-signature-filter"); if (filter) { filter.addEventListener("input", function () { var value = this.value.toLowerCase(); qsa(self.root, ".sig-saved-signature-item").forEach(function (item) { var name = (item.getAttribute("data-name") || "").toLowerCase(); item.style.display = name.indexOf(value) >= 0 ? "" : "none"; }); }); } }; Viewer.prototype.previewSavedSignature = function (signature) { var preview = qs(this.root, ".sig-saved-signature-preview"); if (!preview) { return; } preview.innerHTML = ""; var image = document.createElement("img"); image.src = signature.imageUrl; image.alt = signature.name; preview.appendChild(image); }; Viewer.prototype.bindPropertyEditors = function () { var self = this; ["width", "height", "x", "y"].forEach(function (name) { var input = qs(self.root, ".sig-prop-" + name); if (input) { input.addEventListener("input", function () { self.updateSelectedFromInputs(name); }); } }); var rotation = qs(this.root, ".sig-prop-rotation"); var opacity = qs(this.root, ".sig-prop-opacity"); if (rotation) { rotation.addEventListener("input", function () { qs(self.root, ".sig-rotation-output").textContent = this.value + "°"; self.updateSelectedFromInputs("rotation"); }); } if (opacity) { opacity.addEventListener("input", function () { qs(self.root, ".sig-opacity-output").textContent = this.value + "%"; self.updateSelectedFromInputs("opacity"); }); } qsa(this.root, ".sig-pages-fieldset input[type=radio]").forEach(function (radio) { radio.addEventListener("change", function () { var specific = qs(self.root, ".sig-specific-pages"); if (specific) { specific.disabled = this.value !== "specific"; } self.updateSelectedFromInputs("pages"); }); }); var pages = qs(this.root, ".sig-specific-pages"); if (pages) { pages.addEventListener("input", function () { self.updateSelectedFromInputs("pages"); }); } }; Viewer.prototype.load = function () { var self = this; if (!window.pdfjsLib) { this.showError("No se cargó PDF.js."); return; } window.pdfjsLib.GlobalWorkerOptions.workerSrc = this.workerUrl; window.pdfjsLib.getDocument(this.pdfUrl).promise.then(function (pdf) { self.pdf = pdf; self.pageCount.textContent = "/ " + pdf.numPages; self.pageInput.max = pdf.numPages; self.goToPage(clamp(self.currentPage, 1, pdf.numPages)); }).catch(function (error) { self.showError("No fue posible abrir el PDF: " + error.message); }); }; Viewer.prototype.goToPage = function (pageNumber) { if (!this.pdf) { return; } this.currentPage = clamp(Number(pageNumber) || 1, 1, this.pdf.numPages); this.pageInput.value = this.currentPage; this.state.CurrentPage = this.currentPage; this.renderPage(); }; Viewer.prototype.setScale = function (value) { this.scale = clamp(Number(value) || 1, 0.5, 3); this.state.Scale = this.scale; this.zoomSelect.value = String(this.scale); this.renderPage(); }; Viewer.prototype.renderPage = function () { var self = this; this.loading.hidden = false; this.pdf.getPage(this.currentPage).then(function (page) { var viewport = page.getViewport({ scale: self.scale }); self.pageWidth = viewport.width / self.scale; self.pageHeight = viewport.height / self.scale; self.canvas.width = Math.floor(viewport.width); self.canvas.height = Math.floor(viewport.height); self.canvas.style.width = viewport.width + "px"; self.canvas.style.height = viewport.height + "px"; self.shell.style.width = viewport.width + "px"; self.shell.style.height = viewport.height + "px"; self.layer.style.width = viewport.width + "px"; self.layer.style.height = viewport.height + "px"; if (self.renderTask) { try { self.renderTask.cancel(); } catch (ignore) {} } self.renderTask = page.render({ canvasContext: self.canvas.getContext("2d"), viewport: viewport }); return self.renderTask.promise; }).then(function () { self.loading.hidden = true; self.renderPlacements(); if (self.selectedId) { self.loadInputsFromSelected(); } self.syncState(); }).catch(function (error) { if (error && error.name === "RenderingCancelledException") { return; } self.showError(error.message || "No fue posible mostrar la página."); }); }; Viewer.prototype.enterSignMode = function () { this.signMode = true; this.root.classList.add("is-signing"); this.leftPanel.classList.add("is-open"); this.startButton.hidden = true; this.finishButton.hidden = false; this.modeLabel.textContent = "Modo de firma activo"; this.state.SignMode = true; this.applyPropertiesPanelState(); this.renderPlacements(); this.syncState(); }; Viewer.prototype.setFullscreen = function (expanded) { this.root.classList.toggle("is-fullscreen", !!expanded); var expand = qs(this.root, ".sig-expand-viewer"); var collapse = qs(this.root, ".sig-collapse-viewer"); if (expand) { expand.hidden = !!expanded; } if (collapse) { collapse.hidden = !expanded; } document.body.classList.toggle("sig-viewer-fullscreen-open", !!expanded); this.state.IsFullscreen = !!expanded; this.syncState(); if (this.pdf) { window.setTimeout(this.renderPage.bind(this), 40); } }; Viewer.prototype.placeCurrentSignature = function () { if (!this.currentSignature) { return; } if (!this.signMode) { this.enterSignMode(); } var width = 180; var height = 70; var x = Math.max(0, (this.pageWidth - width) / 2); var y = Math.max(0, this.pageHeight - height - 45); var placement = { Id: uid(), AssetId: this.currentSignature.assetId || "", Name: this.currentSignature.name || "Firma", PageNumber: this.currentPage, X: x, Y: y, Width: width, Height: height, PageWidth: this.pageWidth, PageHeight: this.pageHeight, Rotation: 0, Opacity: 1, ApplyToAllPages: false, SpecificPages: "", ImageDataUrl: this.currentSignature.dataUrl || "", ImageUrl: this.currentSignature.imageUrl || "" }; this.state.Placements.push(placement); this.select(placement.Id); this.syncState(); }; Viewer.prototype.renderPlacements = function () { var self = this; this.layer.innerHTML = ""; this.state.Placements.filter(function (placement) { return Number(placement.PageNumber) === self.currentPage; }).forEach(function (placement) { var element = document.createElement("div"); element.className = "sig-placement" + (placement.Id === self.selectedId ? " is-selected" : ""); element.style.left = Number(placement.X) * self.scale + "px"; element.style.top = Number(placement.Y) * self.scale + "px"; element.style.width = Number(placement.Width) * self.scale + "px"; element.style.height = Number(placement.Height) * self.scale + "px"; element.style.transform = "rotate(" + Number(placement.Rotation || 0) + "deg)"; element.style.opacity = Number(placement.Opacity || 1); element.setAttribute("data-id", placement.Id); var image = document.createElement("img"); image.draggable = false; image.src = placement.ImageDataUrl || placement.ImageUrl || ""; element.appendChild(image); var handle = document.createElement("span"); handle.className = "sig-resize-handle"; element.appendChild(handle); element.addEventListener("mousedown", function (event) { self.startPointer(event, placement, event.target === handle ? "resize" : "move"); }); element.addEventListener("click", function (event) { event.stopPropagation(); self.select(placement.Id); }); self.layer.appendChild(element); }); this.renderPlacementList(); }; Viewer.prototype.renderPlacementList = function () { var self = this; var list = qs(this.root, ".sig-placement-list"); if (!list) { return; } list.innerHTML = ""; this.state.Placements.forEach(function (placement, index) { var item = document.createElement("div"); item.className = "sig-placement-list-item" + (placement.Id === self.selectedId ? " is-selected" : ""); item.innerHTML = ""; qs(item, "span").textContent = placement.Name || ("Firma " + (index + 1)); qs(item, "small").textContent = "Página " + placement.PageNumber; qs(item, ".sig-placement-select").addEventListener("click", function () { self.goToPage(placement.PageNumber); self.select(placement.Id); }); qs(item, ".sig-placement-remove").addEventListener("click", function () { self.removePlacement(placement.Id); }); list.appendChild(item); }); if (!this.state.Placements.length) { list.innerHTML = "No hay firmas colocadas."; } }; Viewer.prototype.startPointer = function (event, placement, mode) { if (!this.signMode) { return; } event.preventDefault(); event.stopPropagation(); this.select(placement.Id); var self = this; var start = { x: event.clientX, y: event.clientY, px: Number(placement.X), py: Number(placement.Y), w: Number(placement.Width), h: Number(placement.Height) }; var keepRatio = qs(this.root, ".sig-keep-ratio").checked; function move(currentEvent) { var dx = (currentEvent.clientX - start.x) / self.scale; var dy = (currentEvent.clientY - start.y) / self.scale; if (mode === "move") { placement.X = clamp(start.px + dx, 0, Math.max(0, self.pageWidth - placement.Width)); placement.Y = clamp(start.py + dy, 0, Math.max(0, self.pageHeight - placement.Height)); } else { var ratio = start.w / Math.max(1, start.h); placement.Width = clamp(start.w + dx, 20, Math.max(20, self.pageWidth - placement.X)); placement.Height = keepRatio ? placement.Width / ratio : clamp(start.h + dy, 20, Math.max(20, self.pageHeight - placement.Y)); } self.renderPlacements(); self.loadInputsFromSelected(); } function up() { document.removeEventListener("mousemove", move); document.removeEventListener("mouseup", up); self.syncState(); } document.addEventListener("mousemove", move); document.addEventListener("mouseup", up); }; Viewer.prototype.select = function (id) { this.selectedId = id; this.state.SelectedPlacementId = id || ""; /* * Seleccionar o colocar una firma abre el panel. * Deseleccionar no lo cierra. */ if (id) { this.showPropertiesForSignature(); } this.renderPlacements(); this.loadInputsFromSelected(); this.syncState(); }; Viewer.prototype.selected = function () { var id = this.selectedId; return this.state.Placements.filter(function (placement) { return placement.Id === id; })[0] || null; }; Viewer.prototype.loadInputsFromSelected = function () { var placement = this.selected(); if (!placement) { return; } qs(this.root, ".sig-prop-width").value = Math.round(Number(placement.Width)); qs(this.root, ".sig-prop-height").value = Math.round(Number(placement.Height)); qs(this.root, ".sig-prop-x").value = Math.round(Number(placement.X)); qs(this.root, ".sig-prop-y").value = Math.round(Number(placement.Y)); qs(this.root, ".sig-prop-rotation").value = Number(placement.Rotation || 0); qs(this.root, ".sig-prop-opacity").value = Math.round(Number(placement.Opacity || 1) * 100); qs(this.root, ".sig-rotation-output").textContent = Number(placement.Rotation || 0) + "°"; qs(this.root, ".sig-opacity-output").textContent = Math.round(Number(placement.Opacity || 1) * 100) + "%"; var mode = placement.ApplyToAllPages ? "all" : (placement.SpecificPages ? "specific" : "current"); var radio = qs(this.root, ".sig-pages-fieldset input[value=\"" + mode + "\"]"); if (radio) { radio.checked = true; } var specific = qs(this.root, ".sig-specific-pages"); if (specific) { specific.value = placement.SpecificPages || ""; specific.disabled = mode !== "specific"; } }; Viewer.prototype.updateSelectedFromInputs = function (changed) { var placement = this.selected(); if (!placement) { return; } var keep = qs(this.root, ".sig-keep-ratio").checked; var ratio = Number(placement.Width) / Math.max(1, Number(placement.Height)); var width = Number(qs(this.root, ".sig-prop-width").value) || 1; var height = Number(qs(this.root, ".sig-prop-height").value) || 1; if (changed === "width" && keep) { height = width / ratio; qs(this.root, ".sig-prop-height").value = Math.round(height); } if (changed === "height" && keep) { width = height * ratio; qs(this.root, ".sig-prop-width").value = Math.round(width); } placement.Width = clamp(width, 1, this.pageWidth); placement.Height = clamp(height, 1, this.pageHeight); placement.X = clamp(Number(qs(this.root, ".sig-prop-x").value) || 0, 0, Math.max(0, this.pageWidth - placement.Width)); placement.Y = clamp(Number(qs(this.root, ".sig-prop-y").value) || 0, 0, Math.max(0, this.pageHeight - placement.Height)); placement.Rotation = Number(qs(this.root, ".sig-prop-rotation").value) || 0; placement.Opacity = (Number(qs(this.root, ".sig-prop-opacity").value) || 100) / 100; var checked = qs(this.root, ".sig-pages-fieldset input[type=radio]:checked"); var mode = checked ? checked.value : "current"; placement.ApplyToAllPages = mode === "all"; placement.SpecificPages = mode === "specific" ? qs(this.root, ".sig-specific-pages").value : ""; this.renderPlacements(); this.syncState(); }; Viewer.prototype.deleteSelected = function () { if (!this.selectedId) { return; } this.removePlacement(this.selectedId); }; Viewer.prototype.removePlacement = function (id) { this.state.Placements = this.state.Placements.filter( function (placement) { return placement.Id !== id; } ); if (this.selectedId === id) { this.selectedId = null; this.state.SelectedPlacementId = ""; } /* * El panel permanece en el estado elegido * por el usuario. */ this.renderPlacements(); this.syncState(); }; Viewer.prototype.clearAll = function () { if (!this.state.Placements.length) { return; } if ( !window.confirm( "¿Deseas eliminar todas las firmas colocadas?" ) ) { return; } this.state.Placements = []; this.selectedId = null; this.state.SelectedPlacementId = ""; /* * Limpiar las firmas no cambia la visibilidad * del panel de propiedades. */ this.renderPlacements(); this.syncState(); }; Viewer.prototype.syncState = function () { this.state.CurrentPage = this.currentPage; this.state.Scale = this.scale; this.state.SignMode = !!this.signMode; this.state.SelectedPlacementId = this.selectedId || ""; this.state.IsFullscreen = this.root.classList.contains("is-fullscreen"); this.state.PropertiesPanelOpen = !!this.propertiesPanelOpen; this.state.Placements.forEach(function (placement) { delete placement.ImageUrl; }); if (this.control.stateField) { this.control.stateField.value = JSON.stringify(this.state); } }; Viewer.prototype.showError = function (message) { if (!this.loading) { return; } this.loading.hidden = false; this.loading.textContent = message; this.loading.classList.add("is-error"); }; function initialize(root) { if (!root || root.__sigPdfControl) { return; } root.__sigPdfControl = new PdfControl(root); } function initializeAll() { qsa(document, ".sig-pdf-control").forEach(initialize); } window.SigPdfViewer = { init: function (id) { initialize(document.getElementById(id)); }, initAll: initializeAll, command: function (controlId, commandName, payload) { var controlRoot = document.getElementById(controlId); if (!controlRoot) { return; } initialize(controlRoot); var control = controlRoot.__sigPdfControl; var viewer = control ? control.viewer : null; if (!viewer) { return; } if (commandName === "finish-signing") { viewer.finishSigningClient(); } else if (commandName === "add-stored-signature") { viewer.addStoredSignatureClient(payload || {}); } else if (commandName === "close-drawer") { var drawerId = controlRoot.getAttribute("data-host-drawer-id"); if (drawerId && window.SIGPdfDrawer) { window.SIGPdfDrawer.close(drawerId); } } } }; if (window.Sys && window.Sys.Application && typeof window.Sys.Application.add_load === "function") { window.Sys.Application.add_load(initializeAll); } else if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initializeAll); } else { initializeAll(); } }(window, document));