File "interface.LOAN.gpl.js"

Full Path: /home/adniftyx/public_html/wp-content/plugins/fc-loan-calculator/src/js/interfaces/interface.LOAN.gpl.js
File size: 16.03 KB
MIME-type: text/plain
Charset: utf-8

/**
 * -----------------------------------------------------------------------------
 * (c) 2016-2025 Pine Grove Software, LLC -- All rights reserved.
 * Contact: webmaster@AccurateCalculators.com
 * License: Commercial
 * www.AccurateCalculators.com
 * -----------------------------------------------------------------------------
 * interface for HTML loan calculator plus plugin
 * -----------------------------------------------------------------------------
 */

/**
 * @preserve Copyright 2016-2025 Pine Grove Software, LLC
 * AccurateCalculators.com
 * License: Commercial
 * interface.LOAN-PLUS.gpl.js
 */

import { LoanCalculatorStrings as loanStrings } from '../strings/strs.LOAN.gpl.js';

// localization conventions
import { Locales } from '../common/locales.gpl.js';

// global constants
import { Globals } from '../common/globals.gpl.js';

// equation methods
import { LoanCalculation as lc } from '../calculations/eq.LOAN.gpl.js';

// utility functions
import { Utils } from '../common/utils.gpl.js';

// numeric editor
import { NE } from '../common/editorNumeric.gpl.js';

/**
 * This plugin uses Bootstrap modals for its interface. To optimize performance and avoid duplicate
 * DOM entries, all plugins that rely on these modals share the same modal instances.
 *
 * If multiple plugins are installed on the same page, the modals are loaded only once. A shared
 * property, `Modals.modals`, is used to track modal instances and prevent duplicates.
 *
 * While this approach minimizes redundancy, it introduces additional complexity in the interface
 * modules. Event handlers and the call chain must account for shared modals and properly manage
 * execution contexts.
 *
 * To achieve this, each modal sets its `activeContext` property to the calling interface class
 * when opened. Event handlers are scoped to the `activeContext`, ensuring the code executes only
 * within the context of the class that initiated the modal.
 */
// modal initialization code
import Modals from '../common/modals.gpl.js';

// printable loan schedule
import { HtmlLoanSchedule as ls } from '../schedules/sch.LOAN.gpl.js';

// charts
import { LoanCharts } from '../charting/ch.LOAN.gpl.js';

// const DEFAULT_AMOUNT = 350_000.00;
// const DEFAULT_NUM_PAYMENTS = 360;
// const DEFAULT_RATE = 5.25

export class LoanCalculatorHtmlInterface {

	static pvInput = null;
	static numPmtsInput = null;
	static rateInput = null;
	static schedule = []; // raw schedule data
	static displayScheduleData = []; // a formatted schedule
	static current_ccy_format = null;
	static current_date_format = null;

	static modalConfigs = [
		{
			modalId: 'CURRENCYDATE',
			initModal: { buttonId: 'CCY-ln', modalElementId: 'CURRENCYDATE', callback: this.showCURRENCYDATEModal, context: this, initCallback: null },
			eventHandlers: [
				{ elementId: 'CURRENCYDATE_save', eventType: 'click', callback: this.onCURRENCYDATESaveClick, context: this }
			]
		},
		{
			// example for configuring a modal that will be opened and inialized using JavaScript - not in response to a 'click'
			modalId: 'RPT',
			initModal: { buttonId: 'btnSchedule-ln', modalElementId: 'RPT', callback: this.showSchedule.bind(this), context: this }
		},
		{
			modalId: 'CHART',
			initModal: { buttonId: 'btnCharts-ln', modalElementId: 'CHART', callback: this.showCharts.bind(this), context: this }
		},
		{
			modalId: 'HLP',
			// optional initCallback: property for static data initialization callback (help text is not static if more than one plugin is used on a page)
			initModal: { buttonId: 'btnHelp-ln', modalElementId: 'HLP', callback: this.showHLPModal, context: this, initCallback: null }
		},
		{
			modalId: 'MSG',
			initModal: { buttonId: null, modalElementId: 'MSG', callback: null },
			eventHandlers: [
				{ elementId: 'MSG_close', eventType: 'click', callback: this.onMSGCloseClick }
			]
		}
	];


	static initializeModalEvents() {
		// Set up show/hide event handlers for the RPT modal
		const rptModalElement = document.getElementById('RPT');

		if (rptModalElement) {
			rptModalElement.addEventListener('hide.bs.modal', this.onRPTModalClosing.bind(this));
		}

		// Set up show/hide event handlers for the CHART modal
		const chartModalElement = document.getElementById('CHART');

		if (chartModalElement) {
			chartModalElement.addEventListener('hide.bs.modal', this.onCHARTModalClosing.bind(this));
		}

		const helpModalElement = document.getElementById('HLP');

		if (helpModalElement) {
			helpModalElement.addEventListener('hide.bs.modal', this.onHLPModalClosing.bind(this));
		}

		const ccyDateModalElement = document.getElementById('CURRENCYDATE');

		if (ccyDateModalElement) {
			ccyDateModalElement.addEventListener('show.bs.modal', this.onCURRENCYDATEModalOpening.bind(this));
			ccyDateModalElement.addEventListener('hide.bs.modal', this.onCURRENCYDATEModalClosing.bind(this));
			ccyDateModalElement.addEventListener('hidden.bs.modal', this.onCURRENCYDATEModalClosed.bind(this));
		}
	}


	static onRPTModalClosing() {
		const rptModalElement = document.getElementById('RPT');

		if (document.activeElement instanceof HTMLElement) {
			document.activeElement.blur();
		}

		// Only remove event listener if this instance is the active context
		if (rptModalElement.activeContext === this) {

			// Clear the active context on close
			delete rptModalElement.activeContext; // Unset the active context
		}
	}


	static onCHARTModalClosing() {
		const chartModalElement = document.getElementById('CHART');

		if (document.activeElement instanceof HTMLElement) {
			document.activeElement.blur();
		}

		if (chartModalElement.activeContext === this) {
			LoanCharts.destroy();

			// Clear the active context on close
			delete chartModalElement.activeContext; // Unset the active context
		}
	}


	static clearResults() {
		document.getElementById('edPmt-ln').value = Utils.formatNumericValueWithSym(0.0);
		document.getElementById('edInterest-ln').value = Utils.formatNumericValueWithSym(0.0);
		document.getElementById('edTotalPI-ln').value = Utils.formatNumericValueWithSym(0.0);
	}

	/**
	 * clearGUI() -- reset GUI's values
	 */
	static clearGUI() {
		// main window
		this.pvInput.setValue(0.0);
		this.numPmtsInput.setValue(0);
		this.rateInput.setValue(0.0);
		document.getElementById('selPmtMthd-ln').value = Globals.PMT_METHOD.ARREARS;
		this.clearResults();
	} // clearGUI


	// print the calculator `div` element
	static print() {
		Utils.printCalculator('loan-plugin');
	}


	/**
	 * Initialize currency and date format selection in modal.
	 * Show the modal.
	 * @param {*} modal
	 */
	static showCURRENCYDATEModal(modal) {
		const ccyModalElement = document.getElementById('CURRENCYDATE');

		ccyModalElement.activeContext = this;
		document.getElementById('ccy-select').value = Locales.moneyConventions.ccy_format;
		document.getElementById('date-select').value = Locales.dateConventions.date_format;
		modal.show();
	}


	/**
	 * showHLPModal() -- open help modal
	 * @param {*} modal
	 */
	static showHLPModal(modal) {
		let txt = '';

		const hlpModalElement = document.getElementById('HLP');

		hlpModalElement.activeContext = this;

		// initialize modal content here
		document.getElementById('hlp-title').innerHTML = loanStrings.strs.s4082;
		txt += loanStrings.strs.s4092;
		txt += loanStrings.strs.s410b;
		txt += loanStrings.strs.s411b;
		txt += loanStrings.strs.s412b;
		txt += '<br>';
		txt += loanStrings.strs.s413b; // for GPL plugin


		// update DOM once
		document.getElementById('hlp-content').innerHTML = txt;

		modal.show();
	}


	/**
	 * onCloseHLPModal()
	 * modal close, remove the text
	 * @param {*} modal
	 */
	static onHLPModalClosing() {
		const hlpModalElement = document.getElementById('HLP');

		if (document.activeElement instanceof HTMLElement) {
			document.activeElement.blur();
		}

		// clear DOM
		if (hlpModalElement.activeContext === this) {
			document.getElementById('hlp-content').innerHTML = '';
			// Clear the active context on close
			delete hlpModalElement.activeContext; // Unset the active context
		}
	}


	/**
	 * onCURRENCYDATESaveClick() -- save button click event handler
	 */
	static onCURRENCYDATESaveClick() {
		// const ccyModalElement = document.getElementById('CURRENCYDATE');
		let ccy_format = document.getElementById('ccy-select').value;
		let date_format = document.getElementById('date-select').value;
		let isCcyFormatChanged = (this.current_ccy_format !== ccy_format);
		let isDateFormatChanged = (this.current_date_format !== date_format);

		// We need to update the conventions for any on page plugin
		if (isCcyFormatChanged) {
			// resets the numConventions and rateConventions too.
			Locales.resetCcyConventions(ccy_format);
		}
		if (isDateFormatChanged) {
			Locales.resetDateConventions(date_format);
		}

		// Having this outside the activeContext test allows all plugins on a page to have their conventions updated.
		if (isCcyFormatChanged) {
			this.clearGUI();
			this.pvInput.resetConventions(ccy_format);
			this.numPmtsInput.resetConventions(ccy_format);
			this.rateInput.resetConventions(ccy_format);
		}
	} // onCURRENCYDATESaveClick


	static onCURRENCYDATEModalOpening() {
		this.current_ccy_format = document.getElementById('ccy-select').value;
		this.current_date_format = document.getElementById('date-select').value;
	}


	static onCURRENCYDATEModalClosing() {
		if (document.activeElement instanceof HTMLElement) {
			document.activeElement.blur();
		}
	}


	static onCURRENCYDATEModalClosed() {
		const ccyModalElement = document.getElementById('CURRENCYDATE');

		if (ccyModalElement.activeContext === this) {
			// Clear the active context on close
			delete ccyModalElement.activeContext; // Unset the active context
		}
	}

	/**
	 * onRPTPrintClick() -- print button click event handler
	 */
	static onRPTPrintClick() {
		Utils.printSchedule('rptFrame');
	}


	/**
	 * onMSGCloseClick() -- close button click event handler
	 * clear the message content
	 */
	static onMSGCloseClick() {
		const modalBody = document.getElementById('msg-content');

		modalBody.innerHTML = '';
	}

	// end modal coded.

	/**
	 * getInputs() -- get user inputs and initialize obj equation interface object
	 */
	static getInputs() {
		var selPmtMthd;

		let obj = JSON.parse(JSON.stringify(lc.loan_params));

		obj.pv = this.pvInput.getNumber();

		obj.n = this.numPmtsInput.getNumber();

		obj.nominalRate = this.rateInput.getNumber() / 100;

		obj.cf = 0;

		// enumerated value for annual cash flow frequency
		obj.pmtFreq = Globals.PMT_FREQUENCY.MONTHLY;

		// enumerated value for annual compounding periods
		obj.cmpFreq = Globals.CMP_FREQUENCY.MONTHLY;

		selPmtMthd = document.getElementById('selPmtMthd-ln');
		obj.pmtMthd = parseInt(selPmtMthd.value, 10);
		obj.amortMthd = Globals.AMORT_MTHD.AM_NORMAL;

		return obj;

	} // getInputs()


	/**
	 * calc() -- initialize CashInputs data structures for equation classes
	 */
	static calc() {
		let totPI, totI;

		this.clearResults();

		let obj = this.getInputs();

		if (obj.pv === 0 || obj.n === 0 || obj.nominalRate === 0) {
			// There are too many unknown values.
			// Enter "Loan Amount", "Number of Payments"\nand "Annual Interest Rate".
			Utils.showMessageModal('<p>' + loanStrings.strs.s4052 + '</p><p>' + loanStrings.strs.s4072 + '</p>');
			return []; // validation checks for an empty array
		}

		if (obj.cf === 0) {
			obj.cf = Utils.roundNumber(lc.calc(obj));
			if (obj.cf !== Infinity) {
				document.getElementById('edPmt-ln').value = Utils.formatNumericValueWithSym(-obj.cf);
			} else {
				obj.cf = 0;
			}
		}

		// schedule array
		this.schedule = lc.sourceScheduleData;

		if (this.schedule.length > 0) {
			totPI = this.schedule[this.schedule.length - 1][5];
			totI = this.schedule[this.schedule.length - 1][8];

			// results to GUI
			if (obj.cf !== 0) {
				document.getElementById('edInterest-ln').value = Utils.formatNumericValueWithSym(totI);
				document.getElementById('edTotalPI-ln').value = Utils.formatNumericValueWithSym(totPI);
			}
		}
		return this.schedule;
	} // calc()


	/**
	 * showSchedule() -- display the loan schedule
	 */
	static showSchedule() {
		const rptModalElement = document.getElementById('RPT');

		rptModalElement.activeContext = this;

		this.displayScheduleData = [];
		this.schedule = this.calc();

		if (this.schedule.length > 0) {

			this.displayScheduleData = ls.formatLoanSchedule(this.schedule, lc.summary);

			// Open report modal
			const rpt = Modals.modals['RPT'];

			if (rpt) {
				let oIframe = document.getElementById('rptFrame'); // modal's iframe

				oIframe.srcdoc = this.displayScheduleData;
				rpt.show();
			}
		}
	} // showSchedule


	static showCharts() {

		this.schedule = this.calc();

		if (this.schedule.length > 0) {
			LoanCharts.showCharts(this.schedule);

			// Open chart modal
			const charts = Modals.modals['CHART'];

			if (charts) {
				// Set this class as the active context on the modal element
				const chartModalElement = document.getElementById('CHART');

				chartModalElement.activeContext = this;
				charts.show();
			}
		}
	}


	/**
	 * initInputs() -- initialize the GUI's input fields - considers localization
	 * @static
	 * @memberof LoanCalculatorHtmlInterface
	 * @returns {void}
	 */
	static initInputs() {

		// for development purposes
		// this.pvInput.setValue(DEFAULT_AMOUNT);
		// this.numPmtsInput.setValue(DEFAULT_NUM_PAYMENTS);
		// this.rateInput.setValue(DEFAULT_RATE);

		// for production - pickup site's configuration
		this.pvInput.setValue(parseFloat(document.getElementById('edPV-ln').value) || 0);
		this.numPmtsInput.setValue(parseFloat(document.getElementById('edNumPmts-ln').value) || 0);
		this.rateInput.setValue(parseFloat(document.getElementById('edRate-ln').value) || 0);

		document.getElementById('selPmtMthd-ln').value = Globals.PMT_METHOD.ARREARS;
		this.clearResults();
	}


	/**
	 * init() -- init the GUI
	 */
	static initGUI() {

		// create new editors per GUI requirements
		this.pvInput = new NE('edPV-ln', Locales.moneyConventions, Locales.moneyConventions.precision);

		this.numPmtsInput = new NE('edNumPmts-ln', Locales.numConventions, 0);
		this.numPmtsInput.isNumEditor = true;

		this.rateInput = new NE('edRate-ln', Locales.rateConventions, Locales.rateConventions.precision);
		this.rateInput.isRateEditor = true;

		// add click event handlers
		Utils.addEventListenerToElement('click', 'btnCalc-ln', this.calc, this);
		Utils.addEventListenerToElement('click', 'btnClear-ln', this.clearGUI, this);
		Utils.addEventListenerToElement('click', 'btnPrint-ln', this.print, this);

		// add change event handlers
		Utils.addEventListenerToElement('change', 'selPmtMthd-ln', Utils.updateSelectedAttribute, this);

		Modals.initializeModals(this.modalConfigs);
		this.initializeModalEvents();

		Utils.initTooltips();

		this.initInputs();

		// Initialize the zoom buttons with specific IDs
		Utils.setupZoomButtons('shrink-ln', 'grow-ln', 'original-ln');

	} // initGUI

} // class LoanCalculatorHtmlInterface


// Initialize when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', function () {
	// don't try to initialize the wrong plugin
	if (!document.getElementById('loan-plugin')) {
		return;
	}

	// 08/05/2025 v2.1
	// explicitly to make the plugin compatible with Elementor, but probably works for making it compatible with other themes too.
	document.querySelectorAll('.modal').forEach(function (modal) {
		if (modal.parentElement !== document.body) {
			document.body.appendChild(modal);
		}
	});

	// 08/05/2025 v2.1
	document.addEventListener('shown.bs.modal', function () {
		// Find the last backdrop element added to the DOM
		const backdrop = document.querySelector('.modal-backdrop:last-of-type');

		if (backdrop) {
			backdrop.classList.add('ac-modal-backdrop');
		}
	});

	LoanCalculatorHtmlInterface.initGUI();
});