Skip to content

Instantly share code, notes, and snippets.

@collei
Last active March 10, 2026 23:38
Show Gist options
  • Select an option

  • Save collei/fcc97dc85fa31fe14cfaf1f7a2070069 to your computer and use it in GitHub Desktop.

Select an option

Save collei/fcc97dc85fa31fe14cfaf1f7a2070069 to your computer and use it in GitHub Desktop.
Simple way to limit number impout to certain formats
/**
* 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