Here's a suggestion for the style, I'll leave the final result here at the beginning:
var span = document.getElementsByTagName('span')[0];
function getCaretPosition(element) {
var caretOffset = 0;
if (typeof window.getSelection != "undefined") {
var range = window.getSelection().getRangeAt(0);
var preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
caretOffset = preCaretRange.toString().length;
} else if (typeof document.selection != "undefined" && document.selection.type != "Control") {
var textRange = document.selection.createRange();
var preCaretTextRange = document.body.createTextRange();
preCaretTextRange.moveToElementText(element);
preCaretTextRange.setEndPoint("EndToEnd", textRange);
caretOffset = preCaretTextRange.text.length;
}
return caretOffset;
}
function setCaret(el, pos) {
for (var i = 0; i < el.childNodes.length; i++) {
var node = el.childNodes[i];
if (node.nodeType == 3) {
if (node.length >= pos) {
var range = document.createRange(),
sel = window.getSelection();
range.setStart(node, pos);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
return -1;
} else {
pos -= node.length;
}
} else {
pos = setCaret(node, pos);
if (pos == -1) {
return -1;
}
}
}
return pos;
}
function detectIE() {
var ua = window.navigator.userAgent;
var msie = ua.indexOf('MSIE ');
if (msie > 0) return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
var trident = ua.indexOf('Trident/');
if (trident > 0) {
var rv = ua.indexOf('rv:');
return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
}
var edge = ua.indexOf('Edge/');
if (edge > 0) return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);
return false;
}
var caretPositions = [];
var contents = [];
var maxLength = 10;
var event;
var div = document.getElementsByTagName('div')[0];
if(detectIE() == false){
event = "input"
}else{
event = "keyup";
}
div.addEventListener(event, insertMark);
function insertMark(e) {
var self = e.target;
var content = self.textContent;
caretPositions.push(getCaretPosition(this));
contents.push(content);
if (contents[contents.length - 1] != contents[contents.length - 2]) {
if (content.length > maxLength) {
self.innerHTML = content.substring(0, maxLength) + "<span class='marker'>" + content.substring(maxLength, content.length) + "</span>";
} else {
self.innerHTML = content;
}
setCaret(self, (caretPositions[caretPositions.length - 1]));
}
}
div.addEventListener('click', function() {
caretPositions.push(getCaretPosition(this));
})
div {
box-sizing: border-box;
width: 100%;
height: 100px;
background: #fff;
border: 1px solid #666;
padding: 7px;
line-height: 20px;
border-radius: 3px;
}
div span {
background: rgba(230, 0, 0, 0.5);
border-radius: 3px;
line-height: 20px;
}
<div contenteditable></div>
Steps:
- Check the number of characters by ignoring tags
- Create a
<span>
with red background to put what you have after 10 chars inside it
- Assign concatenation to event
keyup
if user is using IE, and input
otherwise
- Capture the position of caret and "set it" so that replace does not move
The first two steps are very simple so I will not focus too much on them.
Function to capture the position of caret in a div contenteditable
:
function getCaretPosition(element) {
var caretOffset = 0;
if (typeof window.getSelection != "undefined") {
var range = window.getSelection().getRangeAt(0);
var preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
caretOffset = preCaretRange.toString().length;
} else if (typeof document.selection != "undefined" && document.selection.type != "Control") {
var textRange = document.selection.createRange();
var preCaretTextRange = document.body.createTextRange();
preCaretTextRange.moveToElementText(element);
preCaretTextRange.setEndPoint("EndToEnd", textRange);
caretOffset = preCaretTextRange.text.length;
}
return caretOffset;
}
Available in: link
SetCaret function:
function setCaret(el, pos) {
for (var node of el.childNodes) {
if (node.nodeType == 3) {
if (node.length >= pos) {
var range = document.createRange(),
sel = window.getSelection();
range.setStart(node, pos);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
return -1;
} else {
pos -= node.length;
}
} else {
pos = setCaret(node, pos);
if (pos == -1) {
return -1;
}
}
}
return pos;
}
Available at: link
Function to recognize IE:
function detectIE() {
var ua = window.navigator.userAgent;
var msie = ua.indexOf('MSIE ');
if (msie > 0) return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
var trident = ua.indexOf('Trident/');
if (trident > 0) {
var rv = ua.indexOf('rv:');
return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
}
var edge = ua.indexOf('Edge/');
if (edge > 0) return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);
return false;
}
Including <span>
:
var caretPositions = []; // array com as posições dos cursores
var contentLengths = []; // array com as conteudos da tag
var maxLength = 10; // máximo de caracteres
var event; // receberá o evento
var div = document.getElementsByTagName('div')[0]; // div contenteditable
if(detectIE() == false){
event = "input" // se não estiver usando o IE usar "input"
}else{
event = "keyup"; // se estiver usando o IE usar "keyup"
}
div.addEventListener(event, insertMark); // atribui a adição ao evento keyup ou input
function insertMark(e) {
var self = e.target; // captura o elemento em questão
var content = self.textContent; // captura somente o text escrito na div, sem tags
caretPositions.push(getCaretPosition(this));
contents.push(content); // adiciona ao array o conteúdo da tag a cada adição
// só executa o bloco de código de houver tido alguma mudança no tag (ignorando navegação com as setas, por exemplo)
if (contents[contents.length - 1] != contents[contents.length - 2]) {
if (content.length > maxLength) {
// seleciona do primeiro ao 10º char e concatena com uma <span> com estilo predefinido que irá conter do 10º ao último char
self.innerHTML = content.substring(0, maxLength) + "<span class='marker'>" + content.substring(maxLength, content.length) + "</span>";
} else {
self.innerHTML = content; // retira a marcação
}
// sempre que houver a mudança no conteúdo da div, o curso irá pro início do text, e como não se quer isso:
setCaret(self, (caretPositions[caretPositions.length - 1])); // colocar o cursor no mesmo lugar de antes
}
}
div.addEventListener('click', function() {
caretPositions.push(getCaretPosition(this)); // adiciona a posição do curso ao array definido a cada click
})
Obs :
- I'm using
textContent
to capture content, but there's also the option of innerText
, see the difference here
- I use a function to check if the user is in IE, the only difference is a small delay in rendering the markup, since IE does not support
input
in a div contenteditable