The following code restricts input using regexp pattern, works on input type='text', content editable divs and textareas, prevents pasting and drag/dropping of non-matching text. Just add pattern='^.{0,10}$' property to restrict maxlength (or ^[\s\S]{0,10}$ to support new lines). You can copy this code into test.html file and open it in your browser for a quick test. I would like to know in which use cases it wouldn't work, please give a feedback in the comments.
<html>
<script language='javascript'>
function patternify() {
//Restricts input using regexp pattern
for (const el of document.querySelectorAll("[pattern]")) {
el.dataset.beforeinputvalue = el.innerText.replaceAll("\n", "&n;")
format = function(evt) {
const pattern = el.getAttribute("pattern")
const accept = new RegExp(pattern, "g")
var input_match
var el_div_value
if (el.tagName == "DIV") {
el_div_value = el.innerText
input_match = el_div_value.match(accept)
} else {
input_match = el.value.match(accept)
}
if (input_match == null) {
if (el.tagName == "DIV") {
var sel = document.getSelection();
//var bo = sel.baseOffset
var eo = sel.extentOffset
var cp = getCaretPosition(el)[0];
el.innerHTML = el.dataset.beforeinputvalue.replaceAll("&n;","<br>")
var antienter=0
if (window.fix_pattern_key === "Enter") antienter=1
setCurrentCursorPosition(el, cp-1+antienter)
} else {
var ss = el.selectionStart
var se = el.selectionEnd
el.value = el.dataset.beforeinputvalue
el.selectionStart = ss - 1
el.selectionEnd = se - 1
}
} else {
if (el.tagName == "DIV") {
el.dataset.beforeinputvalue = el_div_value.replaceAll("\n","&n;")
} else {
el.dataset.beforeinputvalue = el.value
}
}
};
//let back = false;
el.addEventListener("keydown", function(e) { window.fix_pattern_key=e.key });
el.addEventListener("input", format);
//el.addEventListener("focus", format);
//el.addEventListener("blur", () => el.value === pattern && (el.value = ""));
//format()
}
}
// node_walk: walk the element tree, stop when func(node) returns false
function node_walk(node, func) {
var result = func(node);
for(node = node.firstChild; result !== false && node; node = node.nextSibling)
result = node_walk(node, func);
return result;
};
// getCaretPosition: return [start, end] as offsets to elem.textContent that
// correspond to the selected portion of text
// (if start == end, caret is at given position and no text is selected)
function getCaretPosition(elem) {
var sel = window.getSelection();
var cum_length = [0, 0];
if(sel.anchorNode == elem)
cum_length = [sel.anchorOffset, sel.extentOffset];
else {
var nodes_to_find = [sel.anchorNode, sel.extentNode];
if(!elem.contains(sel.anchorNode) || !elem.contains(sel.extentNode))
return undefined;
else {
var found = [0,0];
var i;
node_walk(elem, function(node) {
for(i = 0; i < 2; i++) {
if(node == nodes_to_find[i]) {
found[i] = true;
if(found[i == 0 ? 1 : 0])
return false; // all done
}
}
if(node.textContent && !node.firstChild) {
for(i = 0; i < 2; i++) {
if(!found[i])
cum_length[i] += node.textContent.length;
}
}
});
cum_length[0] += sel.anchorOffset;
cum_length[1] += sel.extentOffset;
}
}
if(cum_length[0] <= cum_length[1])
return cum_length;
return [cum_length[1], cum_length[0]];
}
function setCurrentCursorPosition(el, chars) {
if (chars >= 0) {
var selection = window.getSelection();
range = createRange(el, { count: chars });
if (range) {
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
}
};
function createRange(node, chars, range) {
if (!range) {
range = document.createRange()
range.selectNode(node);
range.setStart(node, 0);
}
if (chars.count === 0) {
range.setEnd(node, chars.count);
} else if (node && chars.count > 0) {
if (node.nodeType === Node.TEXT_NODE) {
if (node.textContent.length < chars.count) {
chars.count -= node.textContent.length;
} else {
range.setEnd(node, chars.count);
chars.count = 0;
}
} else {
for (var lp = 0; lp < node.childNodes.length; lp++) {
range = createRange(node.childNodes[lp], chars, range);
if (chars.count === 0) {
break;
}
}
}
}
return range;
};
document.addEventListener('DOMContentLoaded', function () {
patternify();
}, false);
</script>
<body>
input type='text' (maxlength=5)<br>
<input type='text' pattern='^.{0,5}$'><br>
div contenteditable (maxlength=10)
<div contenteditable='true' pattern='^[\s\S]{0,10}$' style='width:500px;height:200px;border:1px solid black'></div>
textarea (maxlength=15)<br>
<textarea pattern='^[\s\S]{0,15}$' style='width:500px;height:200px;'></textarea>
</body>
</html>