UNPKG

autonumeric

Version:

autoNumeric is a standalone Javascript library that provides live *as-you-type* formatting for international numbers and currencies. It supports most international numeric formats and currencies including those used in Europe, Asia, and North and South Am

1,452 lines (1,304 loc) 54.6 kB
/** * Helper functions for AutoNumeric.js * @author Alexandre Bonneau <alexandre.bonneau@linuxfr.eu> * @copyright © 2023 Alexandre Bonneau * * The MIT License (http://www.opensource.org/licenses/mit-license.php) * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sub license, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ import AutoNumericEnum from './AutoNumericEnum'; /** * Static class that holds all the helper functions autoNumeric uses. * Note : none of the functions in there are aware of any autoNumeric internals (which means there are no references to autoNumeric-specific info like options names or data structures). */ export default class AutoNumericHelper { /** * Return `true` if the `value` is null * * @static * @param {*} value The value to test * @returns {boolean} Return `true` if the `value` is null, FALSE otherwise */ static isNull(value) { return value === null; } /** * Return `true` if the `value` is undefined * * @static * @param {*} value The value to test * @returns {boolean} Return `true` if the `value` is undefined, FALSE otherwise */ static isUndefined(value) { return value === void(0); } /** * Return `true` if the `value` is undefined, null or empty * * @param {*} value * @returns {boolean} */ static isUndefinedOrNullOrEmpty(value) { return value === null || value === void(0) || '' === value; } /** * Return `true` if the given parameter is a String * * @param {*} str * @returns {boolean} */ static isString(str) { return (typeof str === 'string' || str instanceof String); } /** * Return `true` if the `value` is an empty string '' * * @static * @param {*} value The value to test * @returns {boolean} Return `true` if the `value` is an empty string '', FALSE otherwise */ static isEmptyString(value) { return value === ''; } /** * Return `true` if the parameter is a boolean * * @static * @param {*} value * @returns {boolean} */ static isBoolean(value) { return typeof(value) === 'boolean'; } /** * Return `true` if the parameter is a string 'true' or 'false' * * This function accepts any cases for those strings. * @param {string} value * @returns {boolean} */ static isTrueOrFalseString(value) { const lowercaseValue = String(value).toLowerCase(); return lowercaseValue === 'true' || lowercaseValue === 'false'; } /** * Return `true` if the parameter is an object * * @param {*} reference * @returns {boolean} */ static isObject(reference) { return typeof reference === 'object' && reference !== null && !Array.isArray(reference); } /** * Return `true` if the given object is empty * cf. http://stackoverflow.com/questions/679915/how-do-i-test-for-an-empty-javascript-object and http://jsperf.com/empty-object-test * * @param {object} obj * @returns {boolean} */ static isEmptyObj(obj) { for (const prop in obj) { if (Object.prototype.hasOwnProperty.call(obj, prop)) { return false; } } return true; } /** * Return `true` if the parameter is a real number (and not a numeric string). * * @param {*} n * @returns {boolean} */ static isNumberStrict(n) { return typeof n === 'number'; } /** * Return `true` if the parameter is a number (or a number written as a string). * * @param {*} n * @returns {boolean} */ static isNumber(n) { return !this.isArray(n) && !isNaN(parseFloat(n)) && isFinite(n); } /** * Return `true` if the given character is a number (0 to 9) * * @param {char} char * @returns {boolean} */ static isDigit(char) { return /\d/.test(char); } /** * Return `true` if the parameter is a number (or a number written as a string). * This version also accepts Arabic and Persian numbers. * * @param {*} n * @returns {boolean} */ static isNumberOrArabic(n) { const latinConvertedNumber = this.arabicToLatinNumbers(n, false, true, true); return this.isNumber(latinConvertedNumber); } /** * Return `true` if the parameter is an integer (and not a float). * * @param {*} n * @returns {boolean} */ static isInt(n) { return typeof n === 'number' && parseFloat(n) === parseInt(n, 10) && !isNaN(n); } /** * Return `true` if the parameter is a function. * * @param {function} func * @returns {boolean} */ static isFunction(func) { return typeof func === 'function'; } /** * Return `true` is the string `str` contains the string `needle` * Note: this function does not coerce the parameters types * * @param {string} str * @param {string} needle * @returns {boolean} */ static contains(str, needle) { //TODO Use `Array.prototype.includes()` when available (cf. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes) if (!this.isString(str) || !this.isString(needle) || str === '' || needle === '') { return false; } return str.indexOf(needle) !== -1; } /** * Return `true` if the `needle` is in the array * * @param {*} needle * @param {Array} array * @returns {boolean} */ static isInArray(needle, array) { if (!this.isArray(array) || array === [] || this.isUndefined(needle)) { return false; } return array.indexOf(needle) !== -1; } /** * Return `true` if the parameter is an Array * //TODO Replace this by the default `Array.isArray()` function? * * @param {*} arr * @throws Error * @returns {*|boolean} */ static isArray(arr) { if (Object.prototype.toString.call([]) === '[object Array]') { // Make sure an array has a class attribute of [object Array] // Test passed, now check if is an Array return Array.isArray(arr) || (typeof arr === 'object' && Object.prototype.toString.call(arr) === '[object Array]'); } else { throw new Error('toString message changed for Object Array'); // Verify that the string returned by `toString` does not change in the future (cf. http://stackoverflow.com/a/8365215) } } /** * Return `true` if the parameter is a DOM element * cf. http://stackoverflow.com/a/4754104/2834898 * * @param {*} obj * @returns {boolean} */ static isElement(obj) { // return !!(obj && obj.nodeName); // return obj && 'nodeType' in obj; // return obj instanceof Element || obj instanceof HTMLInputElement || obj instanceof HTMLElement; if (typeof Element === 'undefined') { // This test is needed in environnements where the Element object does not exist (ie. in web workers) return false; } return obj instanceof Element; } /** * Return `true` in the given DOM element is an <input>. * * @param {HTMLElement|HTMLInputElement} domElement * @returns {boolean} * @private */ static isInputElement(domElement) { return this.isElement(domElement) && domElement.tagName.toLowerCase() === 'input'; } /** * Return `true` if the parameter is a string that represents a float number, and that number has a decimal part * * @param {string} str * @returns {boolean} */ // static hasDecimals(str) { // const [, decimalPart] = str.split('.'); // return !isUndefined(decimalPart); // } /** * Return the number of decimal places if the parameter is a string that represents a float number, and that number has a decimal part. * * @param {string} str * @returns {int} */ static decimalPlaces(str) { const [, decimalPart] = str.split('.'); if (!this.isUndefined(decimalPart)) { return decimalPart.length; } return 0; } /** * Return the index of the first non-zero decimal place in the given value. * The index starts after the decimal point, if any, and begins at '1'. * If no decimal places are found in the value, this function returns `0`. * * @example * indexFirstNonZeroDecimalPlace('0.00') -> 0 * indexFirstNonZeroDecimalPlace('1.00') -> 0 * indexFirstNonZeroDecimalPlace('0.12') -> 1 * indexFirstNonZeroDecimalPlace('0.1234') -> 1 * indexFirstNonZeroDecimalPlace('0.01234') -> 2 * indexFirstNonZeroDecimalPlace('0.001234') -> 3 * indexFirstNonZeroDecimalPlace('0.0001234') -> 4 * * @param {number} value * @returns {Number|number} */ static indexFirstNonZeroDecimalPlace(value) { const [, decimalPart] = String(Math.abs(value)).split('.'); if (this.isUndefined(decimalPart)) { return 0; } let result = decimalPart.lastIndexOf('0'); if (result === -1) { result = 0; } else { result += 2; } return result; } /** * Return the code for the key used to generate the given event. * * @param {Event} event * @returns {string|Number} */ static keyCodeNumber(event) { // `event.keyCode` and `event.which` are deprecated, `KeyboardEvent.key` (https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key) must be used now // Also, do note that Firefox generate a 'keypress' event (e.keyCode === 0) for the keys that do not print a character (ie. 'Insert', 'Delete', 'Fn' keys, 'PageUp', 'PageDown' etc.). 'Shift' on the other hand does not generate a keypress event. return (typeof event.which === 'undefined')?event.keyCode:event.which; } /** * Return the character from the event key code. * If the KeyboardEvent does not represent a printable character, then the key name is used (ie. 'Meta', 'Shift', 'F1', etc.) * @example character(50) => '2' * * @param {KeyboardEvent} event * @returns {string} */ static character(event) { let result; if (event.key === 'Unidentified' || event.key === void(0) || this.isSeleniumBot()) { //XXX The selenium geckodriver does not understand `event.key`, hence when using it, we need to rely on the old deprecated `keyCode` attribute, cf. upstream issue https://github.com/mozilla/geckodriver/issues/440 // Use the old deprecated keyCode property, if the new `key` one is not supported const keyCode = this.keyCodeNumber(event); if (keyCode === 229) { // Android Chrome returns the same keycode number 229 for all keys pressed return AutoNumericEnum.keyName.AndroidDefault; } const potentialResult = AutoNumericEnum.fromCharCodeKeyCode[keyCode]; if (!AutoNumericHelper.isUndefinedOrNullOrEmpty(potentialResult)) { // Since `String.fromCharCode` do not return named keys for some keys ('Escape' and 'Enter' for instance), we convert the characters to the key names result = potentialResult; } else { result = String.fromCharCode(keyCode); } } else { switch (event.key) { // Manages all the special cases for obsolete browsers that return the non-standard names case 'Add': result = AutoNumericEnum.keyName.NumpadPlus; break; case 'Apps': result = AutoNumericEnum.keyName.ContextMenu; break; case 'Crsel': result = AutoNumericEnum.keyName.CrSel; break; case 'Decimal': if (event.char) { // this fixes #602 result = event.char; } else { result = AutoNumericEnum.keyName.NumpadDot; } break; case 'Del': result = AutoNumericEnum.keyName.Delete; break; case 'Divide': result = AutoNumericEnum.keyName.NumpadSlash; break; case 'Down': result = AutoNumericEnum.keyName.DownArrow; break; case 'Esc': result = AutoNumericEnum.keyName.Esc; break; case 'Exsel': result = AutoNumericEnum.keyName.ExSel; break; case 'Left': result = AutoNumericEnum.keyName.LeftArrow; break; case 'Meta': case 'Super': result = AutoNumericEnum.keyName.OSLeft; break; case 'Multiply': result = AutoNumericEnum.keyName.NumpadMultiply; break; case 'Right': result = AutoNumericEnum.keyName.RightArrow; break; case 'Spacebar': result = AutoNumericEnum.keyName.Space; break; case 'Subtract': result = AutoNumericEnum.keyName.NumpadMinus; break; case 'Up': result = AutoNumericEnum.keyName.UpArrow; break; default: // The normal case result = event.key; } } return result; } /** * Return an object containing the name and version of the current browser. * @example `browserVersion()` => { name: 'Firefox', version: '42' } * Based on http://stackoverflow.com/a/38080051/2834898 * * @returns {{ name: string, version: string }} */ static browser() { const ua = navigator.userAgent; let tem; let M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; if (/trident/i.test(M[1])) { tem = /\brv[ :]+(\d+)/g.exec(ua) || []; return { name: 'ie', version: (tem[1] || '') }; } if (M[1] === 'Chrome') { tem = ua.match(/\b(OPR|Edge)\/(\d+)/); if (tem !== null) { return { name: tem[1].replace('OPR', 'opera'), version: tem[2] }; } } M = M[2]?[M[1], M[2]]:[navigator.appName, navigator.appVersion, '-?']; if ((tem = ua.match(/version\/(\d+)/i)) !== null) { M.splice(1, 1, tem[1]); } return { name: M[0].toLowerCase(), version: M[1] }; } /** * Check if the browser is controlled by Selenium. * Note: This only works within the geckodriver. * cf. http://stackoverflow.com/questions/33225947/can-a-website-detect-when-you-are-using-selenium-with-chromedriver * * @returns {boolean} */ static isSeleniumBot() { // noinspection JSUnresolvedVariable return window.navigator.webdriver === true; } /** * Return `true` if the given number is negative, or if the given string contains a negative sign : * - everywhere in the string (by default), or * - on the first character only if the `checkEverywhere` parameter is set to `false`. * * Note: `-0` is not a negative number since it's equal to `0`. * * @param {number|string} numberOrNumericString A Number, or a number represented by a string * @param {string} negativeSignCharacter The single character that represent the negative sign * @param {boolean} checkEverywhere If TRUE, then the negative sign is search everywhere in the numeric string (this is needed for instance if the string is '1234.56-') * @returns {boolean} */ static isNegative(numberOrNumericString, negativeSignCharacter = '-', checkEverywhere = true) { if (numberOrNumericString === negativeSignCharacter) { return true; } if (numberOrNumericString === '') { return false; } if (AutoNumericHelper.isNumber(numberOrNumericString)) { return numberOrNumericString < 0; } if (checkEverywhere) { return this.contains(numberOrNumericString, negativeSignCharacter); } return this.isNegativeStrict(numberOrNumericString, negativeSignCharacter); } /** * Return `true` if the given string contains a negative sign on the first character (on the far left). * * @example isNegativeStrict('1234.56') => false * @example isNegativeStrict('1234.56-') => false * @example isNegativeStrict('-1234.56') => true * @example isNegativeStrict('-1,234.56 €') => true * * @param {string} numericString * @param {string} negativeSignCharacter The single character that represent the negative sign * @returns {boolean} */ static isNegativeStrict(numericString, negativeSignCharacter = '-') { return numericString.charAt(0) === negativeSignCharacter; } /** * Return `true` if the very first character is the opening bracket, and if the rest of the `valueString` also has the closing bracket. * * @param {string} valueString * @param {string} leftBracket * @param {string} rightBracket * @returns {boolean} */ static isNegativeWithBrackets(valueString, leftBracket, rightBracket) { return valueString.charAt(0) === leftBracket && this.contains(valueString, rightBracket); } /** * Return `true` if the formatted or unformatted numeric string represent the value 0 (ie. '0,00 €'), or is empty (' €'). * This works since we test if there are any numbers from 1 to 9 in the string. If there is none, then the number is zero (or the string is empty). * * @param {string} numericString * @returns {boolean} */ static isZeroOrHasNoValue(numericString) { return !(/[1-9]/g).test(numericString); } /** * Return the negative version of the value (represented as a string) given as a parameter. * The numeric string is a valid Javascript number when typecast to a `Number`. * * @param {string} value * @returns {*} */ static setRawNegativeSign(value) { if (!this.isNegativeStrict(value, '-')) { return `-${value}`; } return value; } /** * Replace the character at the position `index` in the string `string` by the character(s) `newCharacter`. * * @param {string} string * @param {int} index * @param {string} newCharacter * @returns {string} */ static replaceCharAt(string, index, newCharacter) { return `${string.substr(0, index)}${newCharacter}${string.substr(index + newCharacter.length)}`; } /** * Return the value clamped to the nearest minimum/maximum value, as defined in the settings. * * @param {string|number} value * @param {object} settings * @returns {number} */ static clampToRangeLimits(value, settings) { //XXX This function always assume `settings.minimumValue` is lower than `settings.maximumValue` return Math.max(settings.minimumValue, Math.min(settings.maximumValue, value)); } /** * Return the number of number or dot characters on the left side of the caret, in a formatted number. * * @param {string} formattedNumberString * @param {int} caretPosition This must be a positive integer * @param {string} decimalCharacter * @returns {number} */ static countNumberCharactersOnTheCaretLeftSide(formattedNumberString, caretPosition, decimalCharacter) { // Here we count the dot and report it as a number character too, since it will 'stay' in the Javascript number when unformatted const numberDotOrNegativeSign = new RegExp(`[0-9${decimalCharacter}-]`); // No need to escape the decimal character here, since it's in `[]` let numberDotAndNegativeSignCount = 0; for (let i = 0; i < caretPosition; i++) { // Test if the character is a number, a dot or an hyphen. If it is, count it, otherwise ignore it if (numberDotOrNegativeSign.test(formattedNumberString[i])) { numberDotAndNegativeSignCount++; } } return numberDotAndNegativeSignCount; } /** * Walk the `formattedNumberString` from left to right, one char by one, counting the `formattedNumberStringIndex`. * If the char is in the `rawNumberString` (starting at index 0), then `rawNumberStringIndex++`, and continue until * there is no more characters in `rawNumberString`) or that `rawNumberStringIndex === caretPositionInRawValue`. * When you stop, the `formattedNumberStringIndex` is the position where the caret should be set. * * @example * 1234567|89.01 : position 7 (rawNumberString) * 123.456.7|89,01 : position 9 (formattedNumberString) * * @param {string} rawNumberString * @param {int} caretPositionInRawValue * @param {string} formattedNumberString * @param {string} decimalCharacter * @returns {*} */ static findCaretPositionInFormattedNumber(rawNumberString, caretPositionInRawValue, formattedNumberString, decimalCharacter) { const formattedNumberStringSize = formattedNumberString.length; const rawNumberStringSize = rawNumberString.length; let formattedNumberStringIndex; let rawNumberStringIndex = 0; for (formattedNumberStringIndex = 0; formattedNumberStringIndex < formattedNumberStringSize && rawNumberStringIndex < rawNumberStringSize && rawNumberStringIndex < caretPositionInRawValue; formattedNumberStringIndex++) { if (rawNumberString[rawNumberStringIndex] === formattedNumberString[formattedNumberStringIndex] || (rawNumberString[rawNumberStringIndex] === '.' && formattedNumberString[formattedNumberStringIndex] === decimalCharacter)) { rawNumberStringIndex++; } } return formattedNumberStringIndex; } /** * Count the number of occurrence of the given character, in the given text. * * @param {string} character * @param {string} text * @returns {number} */ static countCharInText(character, text) { let charCounter = 0; for (let i = 0; i < text.length; i++) { if (text[i] === character) { charCounter++; } } return charCounter; } /** * Return the index that can be used to set the caret position. * This takes into account that the position is starting at '0', not 1. * * @param {int} characterCount * @returns {number} */ static convertCharacterCountToIndexPosition(characterCount) { return Math.max(characterCount, characterCount - 1); } /** * Cross browser routine for getting selected range/cursor position. * Note: this also works with edge cases like contenteditable-enabled elements, and hidden inputs. * * @param {HTMLInputElement|EventTarget} element * @returns {{}} */ static getElementSelection(element) { const position = {}; let isSelectionStartUndefined; try { isSelectionStartUndefined = this.isUndefined(element.selectionStart); } catch (error) { isSelectionStartUndefined = false; } try { if (isSelectionStartUndefined) { const selection = window.getSelection(); const selectionInfo = selection.getRangeAt(0); position.start = selectionInfo.startOffset; position.end = selectionInfo.endOffset; position.length = position.end - position.start; } else { position.start = element.selectionStart; position.end = element.selectionEnd; position.length = position.end - position.start; } } catch (error) { // Manages the cases where : // - the 'contenteditable' elements that have no selections // - the <input> element is of type 'hidden' position.start = 0; position.end = 0; position.length = 0; } return position; } /** * Cross browser routine for setting selected range/cursor position * * @param {HTMLInputElement|EventTarget} element * @param {int} start * @param {int|null} end */ static setElementSelection(element, start, end = null) { if (this.isUndefinedOrNullOrEmpty(end)) { end = start; } if (this.isInputElement(element)) { element.setSelectionRange(start, end); } else if (!AutoNumericHelper.isNull(element.firstChild)) { const range = document.createRange(); range.setStart(element.firstChild, start); range.setEnd(element.firstChild, end); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); } } /** * Function that throw error messages * * @param {string} message * @throws */ static throwError(message) { throw new Error(message); } /** * Function that display a warning messages, according to the debug level. * * @param {string} message * @param {boolean} showWarning If FALSE, then the warning message is not displayed */ static warning(message, showWarning = true) { if (showWarning) { /* eslint no-console: 0 */ console.warn(`Warning: ${message}`); } } /** * Return `true` if the given event is an instance of WheelEvent * * @static * @param {event} event The event to test * @returns {boolean} Return `true` if the event is an instance of WheelEvent, FALSE otherwise */ static isWheelEvent(event) { return event instanceof WheelEvent; } /** * Return `true` if the given event is a wheelup event * * @param {WheelEvent} wheelEvent * @returns {boolean} */ static isWheelUpEvent(wheelEvent) { if (!this.isWheelEvent(wheelEvent) || this.isUndefinedOrNullOrEmpty(wheelEvent.deltaY)) { this.throwError(`The event passed as a parameter is not a valid wheel event, '${wheelEvent.type}' given.`); } return wheelEvent.deltaY < 0; } /** * Return `true` if the given event is a wheeldown event * * @param {WheelEvent} wheelEvent * @returns {boolean} */ static isWheelDownEvent(wheelEvent) { if (!this.isWheelEvent(wheelEvent) || this.isUndefinedOrNullOrEmpty(wheelEvent.deltaY)) { this.throwError(`The event passed as a parameter is not a valid wheel event, '${wheelEvent.type}' given.`); } return wheelEvent.deltaY > 0; } /** * Return `true` if the given event is an instance of WheelEvent and the deltaY value is equal to zero * * @param {WheelEvent} wheelEvent The event to test * @returns {boolean} Return `true` if the event is an instance of WheelEvent and the deltaY value is equal to zero, FALSE otherwise */ static isWheelEventWithZeroDeltaY(wheelEvent) { return this.isWheelEvent(wheelEvent) && !this.isUndefinedOrNullOrEmpty(wheelEvent.deltaY) && wheelEvent.deltaY === 0; } /** * Return the given raw value truncated at the given number of decimal places `decimalPlaces`. * This function does not round the value. * * @example * forceDecimalPlaces(123.45678, 0) -> '123.45678' * forceDecimalPlaces(123.45678, 1) -> '123.4' * forceDecimalPlaces(123.45678, 2) -> '123.45' * forceDecimalPlaces(123.45678, 3) -> '123.456' * * @param {number} value * @param {int} decimalPlaces * @returns {number|string} */ static forceDecimalPlaces(value, decimalPlaces) { // We could make sure `decimalPlaces` is an integer and positive, but we'll leave that to the dev calling this function. const [integerPart, decimalPart] = String(value).split('.'); if (!decimalPart) { return value; } return `${integerPart}.${decimalPart.substr(0, decimalPlaces)}`; } /** * Return the 'nearest rounded' value, according to the given step size. * @example roundToNearest(264789, 10000)) => 260000 * * @param {number} value * @param {number} stepPlace * @returns {*} */ static roundToNearest(value, stepPlace = 1000) { if (0 === value) { return 0; } if (stepPlace === 0) { this.throwError('The `stepPlace` used to round is equal to `0`. This value must not be equal to zero.'); } return Math.round(value / stepPlace) * stepPlace; } /** * Return the 'nearest rounded' value by automatically adding or subtracting the calculated offset to the initial value. * This is done without having to pass a step to this function, and based on the size of the given `value`. * * @example Calculated offset * 1 -> 1 (1) * 14 -> 10 (10) * 143 -> 140 (10) * 1.278 -> 1.300 (100) * 28.456 -> 28.500 (100) * 276.345 -> 276.000 (1.000) * 4.534.061 -> 4.530.000 (10.000) * 66.723.844 -> 66.700.000 (100.000) * 257.833.411 -> 258.000.000 (1.000.000) * * Initial Added Offset * 2 decimalPlacesRawValue : 1.12 -> 2.00 (1) * 3 decimalPlacesRawValue : 1.123 -> 2.000 (1) * * Special case when the `value` to round is between -1 and 1, excluded : * @example * Number of Initial Result Calculated * decimal places value (add) offset * 2 decimalPlacesRawValue : 0.12 -> 0.13 (0.01) : Math.pow(10, -2) * 2 decimalPlacesRawValue : 0.01 -> 0.02 (0.01) * 2 decimalPlacesRawValue : 0.00 -> 0.01 (0.01) * * 3 decimalPlacesRawValue : 0.123 -> 0.133 (0.01) : Math.pow(10, -2) * 3 decimalPlacesRawValue : 0.012 -> 0.013 (0.001) : Math.pow(10, -3) * 3 decimalPlacesRawValue : 0.001 -> 0.001 (0.001) * 3 decimalPlacesRawValue : 0.000 -> 0.001 (0.001) * * 4 decimalPlacesRawValue : 0.4123 -> 0.4200 (0.01) : Math.pow(10, -2) * 4 decimalPlacesRawValue : 0.0412 -> 0.0420 (0.001) : Math.pow(10, -3) * 4 decimalPlacesRawValue : 0.0041 -> 0.0042 (0.0001) : Math.pow(10, -4) * 4 decimalPlacesRawValue : 0.0004 -> 0.0005 (0.0001) * 4 decimalPlacesRawValue : 0.0000 -> 0.0001 (0.0001) * * @param {number} value * @param {boolean} isAddition * @param {int} decimalPlacesRawValue The precision needed by the `rawValue` * @returns {*} */ static modifyAndRoundToNearestAuto(value, isAddition, decimalPlacesRawValue) { value = Number(this.forceDecimalPlaces(value, decimalPlacesRawValue)); // Make sure that '0.13000000001' is converted to the number of rawValue decimal places '0.13' const absValue = Math.abs(value); if (absValue >= 0 && absValue < 1) { const rawValueMinimumOffset = Math.pow(10, -decimalPlacesRawValue); if (value === 0) { // 4 decimalPlacesRawValue : 0.0000 -> 0.0001 (0.0001) return (isAddition)?rawValueMinimumOffset:-rawValueMinimumOffset; } let offset; const minimumOffsetFirstDecimalPlaceIndex = decimalPlacesRawValue; // Find where is the first non-zero decimal places const indexFirstNonZeroDecimalPlace = this.indexFirstNonZeroDecimalPlace(value); if (indexFirstNonZeroDecimalPlace >= minimumOffsetFirstDecimalPlaceIndex - 1) { /* 4 decimalPlacesRawValue : 0.0041 -> 0.0042 (0.0001) : Math.pow(10, -4) * 4 decimalPlacesRawValue : 0.0004 -> 0.0005 (0.0001) */ offset = rawValueMinimumOffset; } else { offset = Math.pow(10, -(indexFirstNonZeroDecimalPlace + 1)); } let result; if (isAddition) { result = value + offset; } else { result = value - offset; } return this.roundToNearest(result, offset); } else { // For values >= 1 value = parseInt(value, 10); const lengthValue = Math.abs(value).toString().length; // `Math.abs()` is needed here to omit the negative sign '-' in case of a negative value let pow; switch (lengthValue) { // Special cases for small numbers case 1: pow = 0; break; case 2: case 3: pow = 1; break; case 4: case 5: pow = 2; break; // Default behavior default: pow = lengthValue - 3; } const offset = Math.pow(10, pow); let result; if (isAddition) { result = value + offset; } else { result = value - offset; } if (result <= 10 && result >= -10) { return result; } return this.roundToNearest(result, offset); } } /** * Return the 'nearest rounded' value automatically by adding the calculated offset to the initial value. * This will limit the result to the given number of decimal places `decimalPlacesLimit`. * * @param {number} value * @param {int} decimalPlacesLimit * @returns {*} */ static addAndRoundToNearestAuto(value, decimalPlacesLimit) { return this.modifyAndRoundToNearestAuto(value, true, decimalPlacesLimit); } /** * Return the 'nearest rounded' value automatically by subtracting the calculated offset to the initial value. * This will limit the result to the given number of decimal places `decimalPlacesLimit`. * * @param {number} value * @param {int} decimalPlacesLimit * @returns {*} */ static subtractAndRoundToNearestAuto(value, decimalPlacesLimit) { return this.modifyAndRoundToNearestAuto(value, false, decimalPlacesLimit); } /** * Take an arabic number as a string and return a javascript number. * By default, this function does not try to convert the arabic decimal and thousand separator characters. * This returns `NaN` is the conversion is not possible. * Based on http://stackoverflow.com/a/17025392/2834898 * * @param {string} arabicNumbers * @param {boolean} returnANumber If `true`, return a Number, otherwise return a String * @param {boolean} parseDecimalCharacter * @param {boolean} parseThousandSeparator * @returns {string|number|NaN} */ static arabicToLatinNumbers(arabicNumbers, returnANumber = true, parseDecimalCharacter = false, parseThousandSeparator = false) { if (this.isNull(arabicNumbers)) { return arabicNumbers; } let result = arabicNumbers.toString(); if (result === '') { return arabicNumbers; } if (result.match(/[٠١٢٣٤٥٦٧٨٩۴۵۶]/g) === null) { // If no Arabic/Persian numbers are found, return the numeric string or number directly if (returnANumber) { result = Number(result); } return result; } if (parseDecimalCharacter) { result = result.replace(/٫/, '.'); // Decimal character } if (parseThousandSeparator) { result = result.replace(/٬/g, ''); // Thousand separator } // Replace the numbers only result = result.replace(/[٠١٢٣٤٥٦٧٨٩]/g, d => d.charCodeAt(0) - 1632) // Arabic numbers .replace(/[۰۱۲۳۴۵۶۷۸۹]/g, d => d.charCodeAt(0) - 1776); // Persian numbers // `NaN` has precedence over the string `'NaN'` const resultAsNumber = Number(result); if (isNaN(resultAsNumber)) { return resultAsNumber; } if (returnANumber) { result = resultAsNumber; } return result; } /** * Create a custom event and immediately sent it from the given element. * By default, if no element is given, the event is thrown from `document`. * * @param {string} eventName * @param {HTMLElement|HTMLDocument|EventTarget} element * @param {object} detail * @param {boolean} bubbles Set to `true` if the event must bubble up * @param {boolean} cancelable Set to `true` if the event must be cancelable */ static triggerEvent(eventName, element = document, detail = null, bubbles = true, cancelable = true) { let event; if (window.CustomEvent) { event = new CustomEvent(eventName, { detail, bubbles , cancelable }); // This is not supported by default by IE ; We use the polyfill for IE9 and later. } else { event = document.createEvent('CustomEvent'); event.initCustomEvent(eventName, bubbles, cancelable, { detail }); } element.dispatchEvent(event); } /** * Function to parse minimumValue, maximumValue & the input value to prepare for testing to determine if the value falls within the min / max range. * Return an object example: minimumValue: "999999999999999.99" returns the following "{s: -1, e: 12, c: Array[15]}". * * This function is adapted from Big.js https://github.com/MikeMcl/big.js/. Many thanks to Mike. * * @param {number|string} n A numeric value. * @returns {{}} */ static parseStr(n) { const x = {}; // A Big number instance. let e; let i; let nL; let j; // Minus zero? if (n === 0 && 1 / n < 0) { n = '-0'; } // Determine sign. 1 positive, -1 negative n = n.toString(); if (this.isNegativeStrict(n, '-')) { n = n.slice(1); x.s = -1; } else { x.s = 1; } // Decimal point? e = n.indexOf('.'); if (e > -1) { n = n.replace('.', ''); } // Length of string if no decimal character if (e < 0) { // Integer e = n.length; } // Determine leading zeros i = (n.search(/[1-9]/i) === -1) ? n.length : n.search(/[1-9]/i); nL = n.length; if (i === nL) { // Zero x.e = 0; x.c = [0]; } else { // Determine trailing zeros for (j = nL - 1; n.charAt(j) === '0'; j -= 1) { nL -= 1; } nL -= 1; // Decimal location x.e = e - i - 1; x.c = []; // Convert string to array of digits without leading/trailing zeros for (e = 0; i <= nL; i += 1) { x.c[e] = +n.charAt(i); e += 1; } } return x; } /** * Function to test if the input value falls with the Min / Max settings. * This uses the parsed strings for the above parseStr function. * * This function is adapted from Big.js https://github.com/MikeMcl/big.js/. Many thanks to Mike. * * @param {object} y Big number instance * @param {object} x Big number instance * @returns {*} */ static testMinMax(y, x) { const xc = x.c; const yc = y.c; let i = x.s; let j = y.s; let k = x.e; let l = y.e; // Either zero? if (!xc[0] || !yc[0]) { let result; if (!xc[0]) { result = !yc[0]?0:-j; } else { result = i; } return result; } // Signs differ? if (i !== j) { return i; } const xNeg = i < 0; // Compare exponents if (k !== l) { return (k > l ^ xNeg)?1:-1; } i = -1; k = xc.length; l = yc.length; j = (k < l) ? k : l; // Compare digit by digit for (i += 1; i < j; i += 1) { if (xc[i] !== yc[i]) { return (xc[i] > yc[i] ^ xNeg)?1:-1; } } // Compare lengths let result; if (k === l) { result = 0; } else { result = (k > l ^ xNeg)?1:-1; } return result; } /** * Generate a random string. * cf. http://stackoverflow.com/a/8084248/2834898 * * @param {Number} strLength Length of the generated string (in character count) * @returns {string} */ static randomString(strLength = 5) { return Math.random() .toString(36) .substr(2, strLength); } /** * Return the DOM element when passed either a DOM element or a selector string. * * @param {HTMLElement|string} domElementOrSelector * @returns {HTMLElement} */ static domElement(domElementOrSelector) { let domElement; if (AutoNumericHelper.isString(domElementOrSelector)) { domElement = document.querySelector(domElementOrSelector); } else { domElement = domElementOrSelector; } return domElement; } /** * Retrieve the current element value. * * @param {HTMLElement|HTMLInputElement|EventTarget} element * @returns {number|string|null} */ static getElementValue(element) { if (element.tagName.toLowerCase() === 'input') { return element.value; } return this.text(element); } /** * Modify the element value directly. * * @param {HTMLElement|HTMLInputElement} element * @param {number|string|null} value */ static setElementValue(element, value = null) { if (element.tagName.toLowerCase() === 'input') { element.value = value; } else { element.textContent = value; } } /** * Set the invalid state for the given element. * A custom message can be passed as the second argument. * Note: This does not work with contenteditable elements * * @param {HTMLElement|HTMLInputElement} element * @param {string|null} message * @throws Error */ static setInvalidState(element, message = 'Invalid') { if (message === '' || this.isNull(message)) this.throwError('Cannot set the invalid state with an empty message.'); element.setCustomValidity(message); } /** * Set the valid state for the given element. * Note: This does not work with contenteditable elements * * @param {HTMLElement|HTMLInputElement} element */ static setValidState(element) { element.setCustomValidity(''); } /** * This clone the given object, and return it. * WARNING: This does not do a deep cloning. * cf. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Examples * //TODO Add a `deep` option to clone object with more than one depth * * @param {object} obj * @returns {object} */ static cloneObject(obj) { return Object.assign({}, obj); } /** * Return a 'camelized' version of the given string. * By default, this assume that : * - the separators are hyphens '-', * - the 'data-' string should be removed, and * - that the very first word should not be capitalized. * * @example camelize('data-currency-symbol') => 'currencySymbol' * * @param {string} str Text to camelize * @param {string} separator Character that separate each word * @param {boolean} removeData If set to `true`, remove the `data-` part that you can find on some html attributes * @param {boolean} skipFirstWord If set to `true`, do not capitalize the very first word * @returns {string|null} */ static camelize(str, separator = '-', removeData = true, skipFirstWord = true) { if (this.isNull(str)) { return null; } if (removeData) { str = str.replace(/^data-/, ''); } // Cut the string into words const words = str.split(separator); // Capitalize each word let result = words.map(word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`); // Then concatenate them back result = result.join(''); if (skipFirstWord) { // Skip the very first letter result = `${result.charAt(0).toLowerCase()}${result.slice(1)}`; } return result; } /** * Return the text component of the given DOM element. * * @param {Element} domElement * @returns {string} */ static text(domElement) { const nodeType = domElement.nodeType; let result; // cf. https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType if (nodeType === Node.ELEMENT_NODE || nodeType === Node.DOCUMENT_NODE || nodeType === Node.DOCUMENT_FRAGMENT_NODE) { result = domElement.textContent; } else if (nodeType === Node.TEXT_NODE) { result = domElement.nodeValue; } else { result = ''; } return result; } /** * Set the text content of the given DOM element. * @param {Element} domElement * @param {string} text */ static setText(domElement, text) { const nodeType = domElement.nodeType; if (nodeType === Node.ELEMENT_NODE || nodeType === Node.DOCUMENT_NODE || nodeType === Node.DOCUMENT_FRAGMENT_NODE) { domElement.textContent = text; } //TODO Display a warning if that function does not do anything? } /** * Filter out the given `arr` array with the elements found in `excludedElements`. * This returns a new array and does not modify the source. * cf. verification here : http://codepen.io/AnotherLinuxUser/pen/XpvrMg?editors=0012 * * @param {Array} arr * @param {Array} excludedElements * @returns {*|Array.<T>} */ static filterOut(arr, excludedElements) { return arr.filter(element => !this.isInArray(element, excludedElements)); } /** * Remove the trailing zeros in the decimal part of a number. * * @param {string} numericString * @returns {*} */ static trimPaddedZerosFromDecimalPlaces(numericString) { numericString = String(numericString); if (numericString === '') { return ''; } const [integerPart, decimalPart] = numericString.split('.'); if (this.isUndefinedOrNullOrEmpty(decimalPart)) { return integerPart; } const trimmedDecimalPart = decimalPart.replace(/0+$/g, ''); let result; if (trimmedDecimalPart === '') { result = integerPart; } else { result = `${integerPart}.${trimmedDecimalPart}`; } return result; } /** * Return the top-most hovered item by the mouse cursor. * * @returns {*} */ static getHoveredElement() { const hoveredElements = [...document.querySelectorAll(':hover')]; return hoveredElements[hoveredElements.length - 1]; } /** * Return the given array trimmed to the given length. * @example arrayTrim([1, 2, 3, 4], 2) -> [1, 2] * * @param {Array} array * @param {Number} length * @returns {*} */ static arrayTrim(array, length) { const arrLength =