# easy-invoice/2.4.1/assets/js/document.js

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.4.1. 475 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.4.1/code/assets/js/document.js
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.4.1/raw/assets/js/document.js
- Modified: 2026-09-15T12:31:20+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.4.1/code/assets/js/document.js#L10-L20`.

```javascript
/**
 * Public invoice / quote page.
 *
 * Everything the action bar does: print, PDF, send by email, the payment
 * panel, accepting or declining a quote, and the administrator's per-document
 * CSS editor. Configuration arrives in `easyInvoiceDocument` (localised by
 * DocumentPage::enqueue).
 */
(function ($) {
    'use strict';

    var cfg = window.easyInvoiceDocument || {};
    var i18n = cfg.i18n || {};
    var isQuote = cfg.type === 'quote';

    /* ------------------------------------------------------------------ */
    /* Modal                                                                */
    /* ------------------------------------------------------------------ */

    function closeModal(overlay) {
        if (!overlay) { return; }
        overlay.classList.remove('show');
        var modal = overlay.querySelector('.ei-modal');
        if (modal) { modal.classList.remove('show'); }
        setTimeout(function () {
            if (overlay.parentNode) { overlay.parentNode.removeChild(overlay); }
        }, 200);
    }

    function closeAnyModal() {
        var existing = document.querySelector('.ei-modal-overlay');
        if (existing) { closeModal(existing); }
    }

    function iconFor(kind) {
        return { info: '?', danger: '!', success: '✓', error: '✕' }[kind] || '?';
    }

    /**
     * Build a modal.
     *
     * @param {Object} o title, message, kind (info|danger|success|error),
     *   confirmText, cancelText, reasonLabel (adds a textarea), onConfirm(reason, buttons)
     */
    function openModal(o) {
        closeAnyModal();

        var overlay = document.createElement('div');
        overlay.className = 'ei-modal-overlay';
        overlay.setAttribute('role', 'dialog');
        overlay.setAttribute('aria-modal', 'true');

        var reasonField = o.reasonLabel
            ? '<label class="ei-modal-label" for="ei-modal-reason">' + escapeHtml(o.reasonLabel) + '</label>' +
              '<textarea id="ei-modal-reason" class="ei-modal-textarea"></textarea>'
            : '';

        var buttons = o.confirmText
            ? '<button type="button" class="ei-modal-btn ei-modal-btn-secondary" data-modal="cancel">' + escapeHtml(o.cancelText || i18n.cancel || 'Cancel') + '</button>' +
              '<button type="button" class="ei-modal-btn ' + (o.kind === 'danger' ? 'ei-modal-btn-danger' : 'ei-modal-btn-primary') + '" data-modal="confirm">' + escapeHtml(o.confirmText) + '</button>'
            : '<button type="button" class="ei-modal-btn ei-modal-btn-primary" data-modal="cancel">' + escapeHtml(o.cancelText || i18n.close || 'Close') + '</button>';

        overlay.innerHTML =
            '<div class="ei-modal">' +
                '<div class="ei-modal-header">' +
                    '<div class="ei-modal-icon ' + escapeHtml(o.kind || 'info') + '">' + iconFor(o.kind) + '</div>' +
                    (o.title ? '<h3 class="ei-modal-title">' + escapeHtml(o.title) + '</h3>' : '') +
                '</div>' +
                '<div class="ei-modal-body">' +
                    (o.message ? '<p class="ei-modal-message">' + escapeHtml(o.message) + '</p>' : '') +
                    reasonField +
                '</div>' +
                '<div class="ei-modal-actions">' + buttons + '</div>' +
            '</div>';

        document.body.appendChild(overlay);
        requestAnimationFrame(function () {
            overlay.classList.add('show');
            overlay.querySelector('.ei-modal').classList.add('show');
        });

        var confirmBtn = overlay.querySelector('[data-modal="confirm"]');
        var cancelBtn = overlay.querySelector('[data-modal="cancel"]');

        function onKey(e) {
            if (e.key === 'Escape') { finish(); }
        }
        function finish() {
            document.removeEventListener('keydown', onKey);
            closeModal(overlay);
            if (typeof o.onClose === 'function') { o.onClose(); }
        }

        cancelBtn.addEventListener('click', finish);
        overlay.addEventListener('click', function (e) { if (e.target === overlay) { finish(); } });
        document.addEventListener('keydown', onKey);

        if (confirmBtn) {
            confirmBtn.addEventListener('click', function () {
                var reason = '';
                var textarea = overlay.querySelector('#ei-modal-reason');
                if (textarea) {
                    reason = textarea.value.trim();
                    if (o.reasonRequired && !reason) {
                        textarea.classList.add('error');
                        textarea.focus();
                        return;
                    }
                }
                o.onConfirm(reason, {
                    busy: function (label) {
                        confirmBtn.disabled = true;
                        cancelBtn.disabled = true;
                        confirmBtn.innerHTML = '<span class="ei-btn__spinner"></span> ' + escapeHtml(label || '');
                    },
                    close: function () {
                        document.removeEventListener('keydown', onKey);
                        closeModal(overlay);
                    }
                });
            });
            (overlay.querySelector('#ei-modal-reason') || confirmBtn).focus();
        } else {
            cancelBtn.focus();
        }

        return overlay;
    }

    function showResult(kind, message, onClose) {
        openModal({ kind: kind, message: message, onClose: onClose });
    }

    function escapeHtml(s) {
        return String(s === undefined || s === null ? '' : s)
            .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
    }

    function toast(kind, message) {
        if (window.EasyInvoiceToast && typeof window.EasyInvoiceToast[kind] === 'function') {
            window.EasyInvoiceToast[kind](message);
        } else {
            showResult(kind === 'success' ? 'success' : 'error', message);
        }
    }

    /* ------------------------------------------------------------------ */
    /* AJAX                                                                 */
    /* ------------------------------------------------------------------ */

    function post(data) {
        data = $.extend({ access_token: cfg.accessToken || '' }, data);
        return $.ajax({ url: cfg.ajaxUrl, type: 'POST', dataType: 'json', data: data });
    }

    function errorMessage(response, fallback) {
        if (response && response.data) {
            if (typeof response.data === 'string') { return response.data; }
            if (response.data.message) { return response.data.message; }
        }
        return fallback;
    }

    /* ------------------------------------------------------------------ */
    /* Actions                                                              */
    /* ------------------------------------------------------------------ */

    function printDocument() {
        window.print();
    }

    /**
     * In-browser PDF: html2canvas + jsPDF over the page exactly as it is drawn
     * — the design, a custom template, the watermark layer. The default for
     * the Download button; also the fallback when the server cannot render.
     */
    function browserPdf() {
        if (typeof window.DocumentPdfGenerator === 'undefined') {
            toast('error', i18n.pdfUnavailable || 'PDF generator not available.');
            return;
        }
        try {
            new window.DocumentPdfGenerator(cfg.type).generatePDF();
        } catch (e) {
            toast('error', i18n.pdfUnavailable || 'PDF generator not available.');
        }
    }

    function sendByEmail() {
        openModal({
            kind: 'info',
            title: i18n.sendTitle,
            message: i18n.sendMessage,
            confirmText: i18n.send,
            onConfirm: function (_reason, modal) {
                modal.busy(i18n.sending);
                var data = { action: isQuote ? 'easy_invoice_send_quote_email' : 'easy_invoice_send_invoice_email', nonce: (cfg.nonces || {}).sendEmail };
                data[isQuote ? 'quote_id' : 'invoice_id'] = cfg.id;
                post(data).done(function (r) {
                    modal.close();
                    if (r && r.success) {
                        showResult('success', errorMessage(r, i18n.sent));
                    } else {
                        showResult('error', errorMessage(r, i18n.sendFailed));
                    }
                }).fail(function () {
                    modal.close();
                    showResult('error', i18n.networkError);
                });
            }
        });
    }

    /* Payment panel (invoices) */
    function paymentPanel() { return document.getElementById('payment-panel'); }
    function payToggle() { return document.getElementById('toggle-payment-panel'); }

    function openPayment() {
        var panel = paymentPanel(), btn = payToggle();
        if (!panel) { return; }
        panel.hidden = false;
        requestAnimationFrame(function () { panel.classList.add('is-open'); });
        if (btn) { btn.classList.add('is-open'); btn.textContent = i18n.hidePayment || 'Hide payment'; }
    }

    function closePayment() {
        var panel = paymentPanel(), btn = payToggle();
        if (!panel) { return; }
        panel.classList.remove('is-open');
        setTimeout(function () { panel.hidden = true; }, 300);
        if (btn) { btn.classList.remove('is-open'); btn.textContent = btn.getAttribute('data-label') || 'Pay Now'; }
    }

    function togglePayment() {
        var panel = paymentPanel();
        if (!panel) { return; }
        if (panel.hidden || !panel.classList.contains('is-open')) { openPayment(); } else { closePayment(); }
    }

    /* Signature pad (only when cfg.signature is set by an addon) */
    function signaturePad(overlay) {
        var sig = cfg.signature || null;
        if (!sig) { return null; }
        var body = overlay.querySelector('.ei-modal-body');
        var wrap = document.createElement('div');
        wrap.className = 'ei-signature';
        wrap.innerHTML =
            '<label class="ei-modal-label" for="ei-signer-name">' + escapeHtml(sig.nameLabel || 'Your name') + '</label>' +
            '<input type="text" id="ei-signer-name" class="ei-modal-input" autocomplete="name">' +
            '<label class="ei-modal-label">' + escapeHtml(sig.padLabel || 'Sign here') + '</label>' +
            '<canvas class="ei-signature__pad" width="440" height="140" aria-label="' + escapeHtml(sig.padLabel || 'Sign here') + '"></canvas>' +
            '<div class="ei-signature__tools"><button type="button" class="ei-modal-btn ei-modal-btn-secondary" data-sig="clear">' + escapeHtml(sig.clearLabel || 'Clear') + '</button>' +
            '<span class="ei-signature__hint">' + escapeHtml(sig.hint || '') + '</span></div>';
        body.appendChild(wrap);

        var canvas = wrap.querySelector('canvas');
        var ctx = canvas.getContext('2d');
        var drawing = false, drawn = false, last = null;
        ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = '#111';
        function pos(e) {
            var r = canvas.getBoundingClientRect();
            var p = e.touches ? e.touches[0] : e;
            return { x: (p.clientX - r.left) * (canvas.width / r.width), y: (p.clientY - r.top) * (canvas.height / r.height) };
        }
        function start(e) { drawing = true; last = pos(e); e.preventDefault(); }
        function move(e) {
            if (!drawing) { return; }
            var p = pos(e);
            ctx.beginPath(); ctx.moveTo(last.x, last.y); ctx.lineTo(p.x, p.y); ctx.stroke();
            last = p; drawn = true; e.preventDefault();
        }
        function end() { drawing = false; }
        canvas.addEventListener('mousedown', start); canvas.addEventListener('mousemove', move); window.addEventListener('mouseup', end);
        canvas.addEventListener('touchstart', start, { passive: false }); canvas.addEventListener('touchmove', move, { passive: false }); canvas.addEventListener('touchend', end);
        wrap.querySelector('[data-sig=clear]').addEventListener('click', function () { ctx.clearRect(0, 0, canvas.width, canvas.height); drawn = false; });

        return {
            required: !!sig.required,
            value: function () { return drawn ? canvas.toDataURL('image/png') : ''; },
            name: function () { return wrap.querySelector('#ei-signer-name').value.trim(); },
            invalid: function () {
                var bad = (sig.required && !drawn) || (sig.required && !this.name());
                canvas.classList.toggle('error', sig.required && !drawn);
                wrap.querySelector('#ei-signer-name').classList.toggle('error', sig.required && !this.name());
                return bad;
            }
        };
    }

    /* Quote accept / decline */
    function acceptQuote(button) {
        var description = button.getAttribute('data-description') || '';
        var pad = null;
        var overlay = openModal({
            kind: 'info',
            title: i18n.acceptTitle,
            message: description,
            confirmText: i18n.accept,
            onConfirm: function (_reason, modal) {
                if (pad && pad.invalid()) { return; }
                modal.busy(i18n.accepting);
                var data = { action: 'easy_invoice_accept_quote', quote_id: cfg.id, nonce: (cfg.nonces || {}).quoteAction };
                if (pad) { data.signature = pad.value(); data.signer_name = pad.name(); }
                post(data).done(function (r) {
                    modal.close();
                    if (!(r && r.success)) {
                        showResult('error', errorMessage(r, i18n.actionFailed));
                        return;
                    }
                    var url = r.data && (r.data.secure_url || r.data.invoice_url);
                    if (r.data && r.data.invoice_id && url) {
                        showResult('success', errorMessage(r, i18n.acceptedRedirect), function () { window.location.href = url; });
                        setTimeout(function () { window.location.href = url; }, 1500);
                    } else {
                        showResult('success', errorMessage(r, i18n.accepted), function () { window.location.reload(); });
                        setTimeout(function () { window.location.reload(); }, 1500);
                    }
                }).fail(function () {
                    modal.close();
                    showResult('error', i18n.networkError);
                });
            }
        });
        pad = signaturePad(overlay);
    }

    function declineQuote(button) {
        var message = button.getAttribute('data-message') || i18n.declineMessage || '';
        var reasonRequired = button.getAttribute('data-reason-required') === '1';
        openModal({
            kind: 'danger',
            title: i18n.declineTitle,
            message: message,
            confirmText: i18n.decline,
            reasonLabel: i18n.declineReason,
            reasonRequired: reasonRequired,
            onConfirm: function (reason, modal) {
                modal.busy(i18n.declining);
                var data = { action: 'easy_invoice_decline_quote', quote_id: cfg.id, nonce: (cfg.nonces || {}).quoteAction };
                if (reason) { data.decline_reason = reason; }
                post(data).done(function (r) {
                    modal.close();
                    if (r && r.success) {
                        showResult('success', errorMessage(r, i18n.declined), function () { window.location.reload(); });
                        setTimeout(function () { window.location.reload(); }, 1500);
                    } else {
                        showResult('error', errorMessage(r, i18n.actionFailed));
                    }
                }).fail(function () {
                    modal.close();
                    showResult('error', i18n.networkError);
                });
            }
        });
    }

    /* Administrator: per-document CSS */
    function initCssPanel() {
        var toggle = document.getElementById('additional-css-btn');
        var panel = document.getElementById('css-panel');
        var overlay = document.getElementById('css-overlay');
        if (!toggle || !panel || !overlay) { return; }

        var textarea = document.getElementById('additional-css-textarea');
        var status = document.getElementById('css-status');
        var styleId = 'custom-' + cfg.type + '-css';

        function applyCss(css) {
            var el = document.getElementById(styleId);
            if (!el) {
                el = document.createElement('style');
                el.id = styleId;
                document.head.appendChild(el);
            }
            el.textContent = css;
        }
        function say(kind, message) {
            status.hidden = false;
            status.className = 'ei-css-panel__status ' + (kind === 'success' ? 'is-success' : 'is-error');
            status.textContent = message;
            setTimeout(function () { status.hidden = true; }, 3000);
        }
        function open() { panel.hidden = false; overlay.hidden = false; textarea.focus(); }
        function close() { panel.hidden = true; overlay.hidden = true; }
        function save(css) {
            return post({ action: 'save_additional_css', post_id: cfg.id, css: css, nonce: (cfg.nonces || {}).additionalCss });
        }

        toggle.addEventListener('click', open);
        document.getElementById('close-css-panel').addEventListener('click', close);
        overlay.addEventListener('click', close);

        document.getElementById('preview-css-btn').addEventListener('click', function () { applyCss(textarea.value); });
        document.getElementById('save-css-btn').addEventListener('click', function () {
            save(textarea.value).done(function (r) {
                if (r && r.success) {
                    applyCss(textarea.value);
                    toggle.classList.toggle('has-css', textarea.value.trim() !== '');
                    say('success', i18n.cssSaved);
                } else {
                    say('error', errorMessage(r, i18n.cssFailed));
                }
            }).fail(function () { say('error', i18n.networkError); });
        });
        document.getElementById('clear-css-btn').addEventListener('click', function () {
            textarea.value = '';
            save('').done(function (r) {
                if (r && r.success) {
                    applyCss('');
                    toggle.classList.remove('has-css');
                    say('success', i18n.cssCleared);
                } else {
                    say('error', errorMessage(r, i18n.cssFailed));
                }
            }).fail(function () { say('error', i18n.networkError); });
        });
    }

    /* ------------------------------------------------------------------ */
    /* Wiring                                                               */
    /* ------------------------------------------------------------------ */

    document.addEventListener('click', function (e) {
        var el = e.target.closest('[data-ei-action]');
        if (!el) { return; }
        switch (el.getAttribute('data-ei-action')) {
            case 'print': e.preventDefault(); printDocument(); break;
            case 'email': e.preventDefault(); sendByEmail(); break;
            case 'pay': e.preventDefault(); togglePayment(); break;
            case 'close-payment': e.preventDefault(); closePayment(); break;
            case 'accept': e.preventDefault(); acceptQuote(el); break;
            case 'decline': e.preventDefault(); declineQuote(el); break;
            case 'pdf':
                // Browser method: capture the page as drawn, right here, no
                // reload. Server method: the link itself serves the file.
                if (cfg.pdfMethod !== 'server') { e.preventDefault(); browserPdf(); }
                break;
        }
    });

    document.addEventListener('DOMContentLoaded', function () {
        initCssPanel();

        // The server serves ?auto_download_pdf=1 itself; reaching this page
        // with the flag means it could not, so fall back to the browser.
        if (cfg.autoDownload) {
            setTimeout(browserPdf, 800);
        }
        // A link that lands the client straight on the payment panel.
        if (cfg.openPayment) {
            setTimeout(openPayment, 300);
        }
    });

    // Names the previous templates defined globally, kept for customised
    // design templates that call them.
    window.printInvoiceContent = printDocument;
    window.printQuoteContent = printDocument;
    window.downloadInvoicePdf = browserPdf;
    window.downloadQuotePdf = browserPdf;
    window.handleDownloadPDF = browserPdf;
    window.togglePaymentPanel = togglePayment;
    window.closePaymentPanel = closePayment;
    window.confirmSendEmail = sendByEmail;
    window.sendInvoiceEmail = sendByEmail;
    window.EasyInvoiceDocument = {
        modal: openModal,
        print: printDocument,
        sendByEmail: sendByEmail,
        openPayment: openPayment,
        closePayment: closePayment
    };
})(jQuery);

```
