Last active
March 10, 2026 23:38
-
-
Save collei/fcc97dc85fa31fe14cfaf1f7a2070069 to your computer and use it in GitHub Desktop.
Simple way to limit number impout to certain formats
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * FILTRNI | |
| * | |
| * Applies input restrictions on input elements. | |
| * Add 'filtrni-input' CSS class and a 'filtrni-type' attribute to the input. | |
| * Supported types: | |
| * tinyint, smallint, int, bigint, decimal(n,p), numeric(n,p), real, float, double | |
| * Use 'unsigned' before name type to prevent signal (+ and -) | |
| * | |
| * @author github.com/collei <alarido.su@gmail.com> | |
| */ | |
| /** | |
| * Generate a constraint object from the input element. | |
| * @param element :HTMLInputElement|HTMLTextAreaElement | |
| * @returns object | |
| */ | |
| function generateConstraintsFor(element) { | |
| const attr = element.getAttribute('filtrni-type'); | |
| const brute_type = attr ? attr : 'varchar'; | |
| const signed = brute_type.indexOf('unsigned') < 0; | |
| const type = signed ? brute_type.trim() : brute_type.replace('unsigned','').trim(); | |
| let constraints = { | |
| field: { | |
| type: (type), | |
| name: element.name, | |
| tag: element.tagName, | |
| id: element.id | |
| }, | |
| isNumber: true, | |
| isExact: true, | |
| isSigned: (signed), | |
| isFixed: ('numeric' == type.substring(0,7)), | |
| isPercentage: false, | |
| length: 0, | |
| decimalLength: 0 | |
| }; | |
| switch (type) { | |
| case 'real': | |
| constraints.length = 8; | |
| constraints.decimalLength = 7; | |
| constraints.isExact = false; | |
| break; | |
| case 'float': | |
| case 'double': | |
| constraints.length = 16; | |
| constraints.decimalLength = 15; | |
| constraints.isExact = false; | |
| break; | |
| case 'tinyint': | |
| constraints.length = 3; | |
| constraints.decimalLength = 0; | |
| break; | |
| case 'smallint': | |
| constraints.length = 5; | |
| constraints.decimalLength = 0; | |
| break; | |
| case 'int': | |
| constraints.length = 10; | |
| constraints.decimalLength = 0; | |
| break; | |
| case 'bigint': | |
| constraints.length = 19; | |
| constraints.decimalLength = 0; | |
| break; | |
| default: | |
| if (['numeric','decimal'].includes(type.substring(0,7))) { | |
| let sublengths = type.substring(7).replace(/[^0-9,]/, '').split(','); | |
| if (sublengths.length > 1) { | |
| constraints.length = parseInt('0'+sublengths[0]); | |
| constraints.decimalLength = parseInt('0'+sublengths[1]); | |
| } else if (sublengths.length > 0) { | |
| constraints.length = parseInt('0'+sublengths[0]) + 1; | |
| constraints.decimalLength = parseInt('0'+sublengths[0]); | |
| } | |
| } else if ('percentage' == type.substring(0,10)) { | |
| constraints.isPercentage = true; | |
| constraints.length = 5; | |
| let sublengths = type.substring(10).replace(/[^0-9,]/, '').split(','); | |
| if (sublengths.length > 0) { | |
| constraints.decimalLength = parseInt('0'+sublengths[0]); | |
| } else { | |
| constraints.decimalLength = 2; | |
| } | |
| } else { | |
| constraints.isNumber = false; | |
| } | |
| } | |
| return constraints; | |
| } | |
| /** | |
| * Display a message in a validation tooltip besides the input. | |
| * @param input : HTMLInputElement | |
| * @param message : string | |
| * @returns void | |
| */ | |
| function showTipMessage(input, message) { | |
| let timer; | |
| input.setCustomValidity(message); | |
| input.reportValidity(); | |
| timer = window.setTimeout(() => { | |
| input.setCustomValidity(''); | |
| }, 2500); | |
| } | |
| /** | |
| * Intuitive helper to cancel the event | |
| * @param event : Event | |
| * @returns boolean | |
| */ | |
| function cancel(event) { | |
| event.preventDefault(); | |
| return false; | |
| } | |
| /** | |
| * Apply on finish page loading. | |
| */ | |
| $(document).ready(function(){ | |
| /** | |
| * Configure every marked input. | |
| */ | |
| $('.filtrni-input').each(function(){ | |
| let constraints = generateConstraintsFor(this); | |
| $(this).attr('filtrni-constraints', JSON.stringify(constraints)); | |
| }); | |
| /** | |
| * Event to handle input and apply input restrictions. | |
| */ | |
| $('.filtrni-input').on('keydown', function(event){ | |
| // loads and parses input configurations | |
| const jso = this.getAttribute('filtrni-constraints'); | |
| const cst = jso ? JSON.parse(jso) : generateConstraintsFor(this); | |
| // obtains the character and its code | |
| const key = event.key; | |
| const keyCode = event.which; | |
| // selection state and pre-value | |
| const selStart = this.selectionStart; | |
| const selEnd = this.selectionEnd; | |
| const selLength = this.selectionEnd - this.selectionStart; | |
| const previousValue = this.value; | |
| // Checks for selected parts in the value, removing it if any. | |
| // It keeps the cursor in the right spot. | |
| if (['0','1','2','3','4','5','6','7','8','9','+','-',',','.'].includes(key)) { | |
| if (selLength > 0) { | |
| this.value = previousValue.slice(0, selStart) + previousValue.slice(selEnd); | |
| // Restore focus and set the cursor position to where the deletion ended | |
| this.focus(); | |
| this.selectionStart = selStart; | |
| this.selectionEnd = selStart; | |
| } | |
| } | |
| // cursor position | |
| const cursor = this.selectionStart; | |
| // current value of input | |
| const currentValue = this.value; | |
| // position of decimal separator, if any | |
| const dot = currentValue.search(/[\.\,]/); | |
| // position of decimal separator, if any | |
| const signal = currentValue.search(/[\+\-]/); | |
| // total length of current value | |
| const charLength = currentValue.length; | |
| // digit length of current value | |
| const digitLength = currentValue.replace(/[^\d]/,'').length; | |
| // maximum allowed length | |
| const maxDigitLength = cst.length; | |
| // length of integer part | |
| const integers = (dot >= 0) ? dot : charLength; | |
| // length of decimal part | |
| const decimals = (dot >= 0) ? (charLength - dot - 1) : dot; | |
| // allows BACKSPACE, TAB, ENTER, DELETE and arrow keys to function | |
| if ((keyCode < 32) || [37,38,39,40,46].includes(keyCode)) { | |
| return true; | |
| } | |
| // Try preview the number before altered in input, | |
| // so value-based restrictions can be enforced | |
| let futureValue = this.value; | |
| if (['0','1','2','3','4','5','6','7','8','9','.',','].includes(key)) { | |
| if (cursor == (futureValue.length-1)) { | |
| futureValue = futureValue.substring(0,cursor) + key; | |
| } else if ((signal == 0) && (cursor == 1)) { | |
| futureValue = futureValue.substring(0,cursor) + key + futureValue.substring(cursor); | |
| } else { | |
| futureValue = futureValue.substring(0,cursor) + key + futureValue.substring(cursor); | |
| } | |
| } else if (['+','-'].includes(key)) { | |
| if ((signal < 0) && (cursor == 0)) { | |
| futureValue = key + futureValue.substring(cursor); | |
| } | |
| } | |
| // Avoid percentage values beyond 100 or less than -100 | |
| if (cst.isPercentage) { | |
| let num = Math.abs(parseFloat(futureValue.replace(',','.'))); | |
| if (num > 100) { | |
| showTipMessage(this, 'Percentage values most not exceed 100 (one hundred).'); | |
| return cancel(event); | |
| } | |
| } | |
| // non-digit characters | |
| if (! ['0','1','2','3','4','5','6','7','8','9'].includes(key)) { | |
| // decimal separators (supports both '.' and ',') | |
| if ([',','.'].includes(key)) { | |
| if (cst.decimalLength == 0) { | |
| showTipMessage(this, 'This field accepts only whole numbers.'); | |
| return cancel(event); | |
| } else if ((currentValue.indexOf(',') >= 0) || (currentValue.indexOf('.') >= 0)) { | |
| showTipMessage(this, 'This field already contains a decimal separator.'); | |
| return cancel(event); | |
| } else if ((currentValue.length - cursor) > cst.decimalLength) { | |
| showTipMessage(this, 'Inserting a decimal separator here is not allowed.'); | |
| return cancel(event); | |
| } else if (cst.isPercentage && (currentValue.length >= 3) && (cursor == currentValue.length)) { | |
| showTipMessage(this, 'Even with decimal places, percentages must not exceed 100 (one hundred) !!'); | |
| return cancel(event); | |
| } else { | |
| return true; | |
| } | |
| } | |
| // signal indicator '+' (positive) and '-' (negative) | |
| if (['+','-'].includes(key)) { | |
| if ((currentValue.indexOf('-') == 0) || (currentValue.indexOf('+') == 0)) { | |
| showTipMessage(this, 'This field contains a signal.'); | |
| return cancel(event); | |
| } else if (! cst.isSigned) { | |
| showTipMessage(this, 'This field accepts whole numbers only.'); | |
| return cancel(event); | |
| } else if (cursor > 0) { | |
| showTipMessage(this, 'The signal must be inserted at the start of the number.'); | |
| return cancel(event); | |
| } else { | |
| return true; | |
| } | |
| } | |
| // allows CTRL, ALT and SHIFT key combos | |
| if (event.ctrlKey || event.altKey || event.shiftKey) { | |
| return true; | |
| } | |
| // every other non-digit printable character is not allowed | |
| showTipMessage(this, 'This is a numeric field.'); | |
| return cancel(event); | |
| } | |
| // prevents adding digits before signal | |
| if ((cursor == 0) && (signal == 0)) { | |
| showTipMessage(this, 'Inserting a digit before signal is not allowed.'); | |
| return cancel(event); | |
| } | |
| // checks for digit (integer plus decimal) length | |
| if (digitLength >= maxDigitLength) { | |
| showTipMessage(this, 'The maximum digit length was reached.'); | |
| return cancel(event); | |
| } | |
| // apply further restrictions for numbers with decimal part | |
| if (cst.decimalLength > 0) { | |
| // checks for integer part length if type is numeric | |
| if (cst.isFixed) { | |
| if (cursor <= dot) if (integers >= (cst.length - cst.decimalLength)) { | |
| // disallow adding more digits on integer part | |
| showTipMessage(this, 'The maximum length of whole number part was reached.'); | |
| return cancel(event); | |
| } | |
| if (dot < 0) if (integers >= (cst.length - cst.decimalLength)) { | |
| // disallow adding more digits on integer part | |
| showTipMessage(this, 'The maximum length of whole number part was reached.'); | |
| return cancel(event); | |
| } | |
| } | |
| // checks for decimal length | |
| if (decimals >= cst.decimalLength) { | |
| // allows adding digits to integer part only | |
| if ((digitLength < maxDigitLength) && (cursor <= dot)) { | |
| return true; | |
| } | |
| // disallow adding more digits on decimal whem maximum limit was hit | |
| showTipMessage(this, 'The maximum length of decimal places was reached.'); | |
| return cancel(event); | |
| } | |
| } | |
| }); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment