File "editorNumeric.gpl.js"

Full Path: /home/adniftyx/public_html/wp-content/plugins/fc-loan-calculator/src/js/common/editorNumeric.gpl.js
File size: 14.73 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
 * -----------------------------------------------------------------------------
 * number editor
 * -----------------------------------------------------------------------------
 */

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


// translation strings
import { GlobalStrings } from '../strings/strs.GLOBAL.gpl.js';

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

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

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

const RATE_PRECISION = 4;
const NUM_PRECISION = 2;

//
// Start Numeric Editor
//
export class NE {
	conventions;

	/**
	 * Numeric Editor - event listeners for custom text input
	 * @constructor
	 * @param {string} id
	 * @param {Object} new_conventions
	 * @param {number} precision allows for override of conventions.precision
	 * @param {boolean} isTableEditor
	 * @return {Object}
	 */
	constructor (id, new_conventions, precision, isTableEditor = false) {
		let conventions;

		// assume not a valid object
		this.isValid = false;
		this.element = document.getElementById(id);
		if (this.element !== null) {
			this.id = id;
			if (!new_conventions || new_conventions === null) {
				// use defaults or user selected settings
				conventions = Locales.moneyConventions;
			} else {
				conventions = new_conventions;
			}
			this.ccy_format = conventions.ccy_format;
			this.precision = precision !== undefined ? precision : conventions.precision;
			this.sep = conventions.sep;
			this.dPnt = conventions.dPnt;
			this.ccy = conventions.ccy;
			this.ccy_r = conventions.ccy_r;
			this.PCT = '%';

			// Initialize handlers storage if not already present. If present then element has been initialize.
			if (!this.handlers) {
				this.handlers = {};
			}
			// if editor is contained in a html table element, then allow up/down keys etc.
			// Don't show instruction message and disable focus and blur events
			this.isTableEditor = isTableEditor;
			this.isValid = true;
			// required so resetConventions() works
			this.isNumEditor = false;
			this.isRateEditor = false;
			this.init();
		}
	}


	/**
	 * Numeric Editor - change field to interest rate editor (this.PCT appended to the right)
	 * Note, this preserves the local setting for sep & dPnt
	 */
	initRateEditor (precision = RATE_PRECISION) {
		this.ccy = '';
		this.ccy_r = this.PCT;
		this.precision = precision;
		this.isRateEditor = true;
	}


	/**
	 * Numeric Editor - change field to be a plain number editor (no currency or %)
	 * Note, this preserves the local setting for sep & dPnt
	 */
	initNumEditor (precision = NUM_PRECISION) {
		this.ccy = '';
		this.ccy_r = '';
		this.precision = precision;
		this.isNumEditor = true;
	};


	/**
	 * Numeric Editor - Initialize an event listener and store the handler
	 */
	addEvent (event, callback, caller) {
		const handler = (e) => callback.call(caller, e);

		this.element.addEventListener(event, handler, false);
		// Store handlers to enable removal
		if (!this.handlers) {
			this.handlers = {};
		}
		this.handlers[event] = handler;
	};


	/**
	 * Numeric Editor - init object
	 */
	init () {
		this.addEvent('focus', this.onCustomFocus, this);
		this.addEvent('blur', this.onCustomBlur, this);
		this.addEvent('keydown', this.onCustomKeyDown, this);
		this.addEvent('input', this.onCustomInput, this);
		this.addEvent('mouseup', this.onCustomMouseUp, this);
	};


	/**
	 * Numeric Editor - detach event listeners
	 */
	removeEvent (event) {
		if (this.handlers && this.handlers[event]) {
			this.element.removeEventListener(event, this.handlers[event], false);
			delete this.handlers[event]; // Remove the handler reference after removal
		}
	};


	/**
	 * Numeric Editor - deallocate object and clean up
	 */
	destroy () {
		// List all events to remove
		const events = ['focus', 'mouseup', 'keydown', 'input', 'blur'];

		// Loop through and remove each event
		events.forEach((event) => {
			this.removeEvent(event);
		});

		// Additional cleanup
		this.element = null;
		// Delete the handlers property - we may want to detect if element has already been initialized.
		delete this.handlers;
	};


	/**
	 * Numeric Editor - Remove local conventions from input's value. Convert to a number (with computer decimal '.').
	 * @return {number}
	 */
	parseNumStr () {
		return Utils.parseNumStr(this.element.value, this.precision, this.dPnt);
	};


	/**
	 * Numeric Editor - Remove local conventions from input's value. Convert to a number (with computer decimal '.').
	 * @return {number}
	 */
	getNumber () {
		return this.parseNumStr(this.element.value, this.precision, this.dPnt);
	};


	resetConventions (ccy_format) {
		let conventions;

		let current_value = this.getNumber();

		if (!ccy_format) {
		// no ccy_format, use global values
			this.ccy_format = Locales.moneyConventions.ccy_format;
		} else if (this.ccy_format !== ccy_format && Locales.CCY_CONVENTIONS[ccy_format] !== undefined) {
		// changing to user specified date_format
			this.ccy_format = ccy_format;
		} else {
			return; // nothing to do
		}

		conventions = Locales.CCY_CONVENTIONS[this.ccy_format];

		// make sure rate and number editors follow our rules
		if (this.isRateEditor) {
			conventions.ccy = '';
			conventions.ccy_r = '%';
		} else if (this.isNumEditor) {
			conventions.ccy = '';
			conventions.ccy_r = '';
		} else {
			// update precision for currency only
			this.precision = conventions.precision;
		}

		// Always set separators and decimal points as these are not editor-specific
		this.sep = conventions.sep;
		this.dPnt = conventions.dPnt;

		this.element.value = Utils.formatNumericValueWithSym(current_value, conventions, this.precision, this.ccy === Globals.INDIAN_RUPEE);

		return this.element.value;

	};


	/**
	 * Common numeric string formatting.
	 * Same results as: formatNumericValue
	 * Include formatting for Indian Rupee.
	 * Will format a string as it is being typed.
	 * That is, logic is applied on a character by character basis.
	 * No error checking. Assumes that 'value' is a valid number.
	 * Used by numeric editor. 'keydown' event handler validates 'value'.
	 */
	// formatNumericValueOnInput (value, sep, dPnt, precision, isIndianRupee = false) {
	formatNumericValueOnInput (value, sep, dPnt, precision) {
		// Direct return for lone minus sign or empty string to handle negative input initiation and user deleting value.
		if (value === '-' || value === '') {
			return value;
		}

		const isNegative = value.startsWith('-');

		value = isNegative ? value.substring(1) : value; // Remove minus if present for processing

		let [integerPart, decimalPart] = value.split(dPnt);

		// Remove non-digit characters safely
		integerPart = integerPart.replace(/\D/g, '');

		// Remove leading zeros from integerPart if it's longer than 1
		if (integerPart.length > 1) {
			integerPart = integerPart.replace(/^0+/, '');
		}

		// Default to '0' if integerPart is empty after removing leading zeros
		if (integerPart === '') {
			integerPart = '0';
		}

		let formattedIntegerPart = integerPart;
		let formattedValue = formattedIntegerPart;

		// Adjust logic for showing decimal point based on presence in input
		if (value.includes(dPnt) || decimalPart) {
			formattedValue += dPnt; // Show the decimal point if present in the input
			if (decimalPart) {
				decimalPart = decimalPart.substring(0, precision);
				formattedValue += decimalPart;
			}
		}

		// Prepend minus if the original value was negative, ensuring correct format for negative decimals
		formattedValue = isNegative ? '-' + formattedValue : formattedValue;

		return formattedValue;
	};


	/**
	 * val - element's value
	 * resonsible for handling the formating of the '<input>' element.
	 * This formats string in real time as user types
	 * 'val' is validated during 'keydown' event.
	 * Produces same results as 'formatNumericValue()' (which is not for char-by-char, realtime formatting)
	 */
	onCustomInputHelper (val) {
		// [KT] 09/17/2024 - logic changed for 'Plus' plugins
		// no value entered - just return, nothing to format.
		if (val === '') {
			return '';
		}
		// Continue with numeric formatting if the first character is not special or the logic above hasn't returned (e.g., for mid-string changes)
		return this.formatNumericValueOnInput(val, this.sep, this.dPnt, this.precision, this.ccy === Globals.INDIAN_RUPEE);
	};


	onCustomInput (e) {
		e.target.value = this.onCustomInputHelper(e.target.value);
	};


	/**
	 * 'keydown' applies logic for filtering the user's keystrokes. Rules are situational specific.
	 * @param {*} e
	 * @returns
	 */
	onCustomKeyDown (e) {
		const decimalChar = this.dPnt; // Ensure this is correctly set (e.g., '.')
		const navigationKeys = ['PageUp', 'PageDown', 'End', 'Home', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Enter'];

		// Block the decimal point if precision is set to 0
		if (this.precision === 0 && e.key === decimalChar) {
			e.preventDefault(); // Prevent the decimal point from being entered
			//  could show user a message here
			return; // Exit early
		}

		// Handle the minus sign as a "change of intent" key
		if (e.key === '-') {
			return; // Exit early
		}

		// Clear the input if backspace is pressed
		if (e.key === 'Backspace') {
			this.element.value = ''; // Clear the input
			return;
		}

		if (e.key === 'Delete') {
			e.preventDefault(); // Block this key
			requestAnimationFrame(() => {
				setTimeout(() => {
					Utils.showMessageModal('<p>' + Globals.ERR_MSGS.noDelKey + '</p>');
				}, 0);
			});
			return;
		}

		if (e.key === this.sep) {
			e.preventDefault(); // Block this key
			requestAnimationFrame(() => {
				setTimeout(() => {
					// [KT] 04/22/2012 -- new message. NOTE: period key and decimal point key on numeric keypad return different keycode values
					Utils.showMessageModal('<p>' + Globals.ERR_MSGS.noSeparators + '</p>');
				}, 0);
			});
			return;
		}

		// Allow control keys, decimal character, and navigation keys if in table editor mode
		if (['Backspace', 'ArrowRight', 'Tab'].includes(e.key) ||
        e.key === decimalChar || (this.isTableEditor && navigationKeys.includes(e.key))) {
			return; // Allow these inputs
		}

		// Block navigation keys when not in table editor mode and display a warning
		if (navigationKeys.includes(e.key) && !this.isTableEditor) {
			e.preventDefault(); // Block these keys
			requestAnimationFrame(() => {
				setTimeout(() => {
					Utils.showMessageModal('<p>' + Globals.ERR_MSGS.noCurKeys + '</p>'); // Display warning
				}, 0);
			});
			return;
		}

		// Block non-numeric keys
		if (isNaN(parseFloat(e.key)) || !isFinite(e.key)) {
			e.preventDefault(); // Blocks any input that's not numeric or specifically allowed
		}
	};


	/**
	 * Capture input element's original value
	 */
	onCustomFocus () {
		// Store the current value of the input on focus for later comparison
		requestAnimationFrame(() => {
			setTimeout(() => {
				this.element.setAttribute('data-original-value', this.element.value);
			}, 0);
		});
	};


	/**
	 * Numeric Editor - onblur event handler
	 * -- assures precision
	 * -- append currency symbol
	 * Precision Adjustment: Ensures numeric inputs conform to the specified precision, padding with zeros where necessary,
	 * and guarantees an integer part is present to avoid formatting like '$.99'.
	 * Currency Symbol Application: Thoughtfully appends or prepends currency symbols,
	 * considering changes to the value since focus to avoid unnecessary repetitions.
	 * Enhanced Value Integrity: The added check for the integer part's presence before the decimal separator enhances
	 *  the function's reliability in producing correctly formatted output.
	 */
	onCustomBlurHelper (val) {

		// Adjust the input value based on precision...
		// Ensure a default value if empty, directly applying '0' if precision is not required,
		// or '0.00' (or equivalent based on precision) if precision is specified.
		if (val.length === 0) {
			val = this.precision > 0 ? '0' + this.dPnt + '0'.repeat(this.precision) : '0';
		}

		val = Utils.formatNumericValue(val, this.sep, this.dPnt, this.precision, this.ccy === Globals.INDIAN_RUPEE);

		// Concise approach to append/prepend currency symbols based on their presence.
		// Note, 'ccy_r' may equal '%'. See: initRateEditor
		return (this.ccy + val + this.ccy_r).trim();
	};


	onCustomBlur () {
		// FYI: setTimeout() does not work when <input> is in a table since 'this.element.value' will have changed.
		// Retrieve the original value stored at focus
		const originalValue = this.element.getAttribute('data-original-value');

		if (this.element.value !== originalValue) {
			this.element.value = this.onCustomBlurHelper(this.element.value);
		}
		return true;
	};


	/**
	 * Numeric Editor - onmouseup event hander - select content of input
	 * The handler sets the selectionStart and selectionEnd properties of the input element to select all its text.
	 */
	onCustomMouseUp (e) {
		// Select the text within the input element
		this.element.selectionStart = 0;
		this.element.selectionEnd = this.element.value.length;
		// Prevent the default mouse up action to ensure the selection is not disrupted
		e.preventDefault();
	};  // onCustomMouseUp


	/**
	 * Numeric Editor - Set a input element's value. Format using local conventions.
	 * Converts a US number to local number before formatting.
	 * @param {number|string} v string
	 */
	setValue (v) {
		if (typeof v === 'number') {
		// if v is a string, the string's conventions must match the editor's conventions
			v = Utils.formatNumericValue(v, this.sep, this.dPnt, this.precision, this.ccy === Globals.INDIAN_RUPEE);
			// Concise approach to append/prepend currency symbols based on their presence.
			// Note, 'ccy_r' may equal '%'. See: NE.prototype.initRateEditor
			this.element.value = (this.ccy + v + this.ccy_r).trim();
		} else {
			// if v is a string, the string's conventions must match the editor's conventions
			v = Utils.formatNumericValue(v, this.sep, this.dPnt, this.precision, this.ccy === Globals.INDIAN_RUPEE);
			// Concise approach to append/prepend currency symbols based on their presence.
			// Note, 'ccy_r' may equal '%'. See: NE.prototype.initRateEditor
			this.element.value = (this.ccy + v + this.ccy_r).trim();
		}
		return this.element.value;
	};
}