For a chat app, I need to change the color of words starting with a special character, @word
inside an input tag, i.e:
Hello @John how are you?
the part @John
should change color inside the input, i.e:
Hello @John how are you?
It works after it's posted, but that's obviously another code. I want it to highlight words (@mention) by changing its color right in the input or textarea Element.
CodePudding user response:
Color text portion inside an input-alike element or textarea
Two examples ahead:
1. Input-alike field:
One 5y old idea would be to:
- Make a Textarea with CSS
color: transparent
text, but with visible caret:caret-color: black
- Overlay the Textarea over an underlying Pre element
- On "input" event do some
String.prototype.replace()
with a regular expression that matches @mentions on the textarea value and replace i.e:@John
with<span >@John</span>
- Write the result as HTML to the underlying PRE element.
const colorMention = (elText, elPre) => {
elPre.innerHTML = elText.value.replace(/(?<=^| )@\p{L} /gu, "<span class='mention'>$&</span>");
};
const scrollMirror = (elText, elPre) => {
elPre.scrollTo(elText.scrollLeft, elText.scrollTop);
};
const handleKey = (ev, elText, elPre) => {
if (ev.key === "Enter") ev.preventDefault();
scrollMirror(elText, elPre);
};
const handlePaste = (ev, elText, elPre) => {
ev.preventDefault();
const value = ev.clipboardData.getData('Text');
elText.value = value.replace(/(\r\n|\n|\r)/gm, "");
elPre.innerHTML = elText.value;
};
document.querySelectorAll(".message").forEach(el => {
const elText = el.querySelector("textarea");
const elPre = el.querySelector("pre");
elText.addEventListener("keyup", () => scrollMirror(elText, elPre));
elText.addEventListener("input", () => colorMention(elText, elPre));
elText.addEventListener("keydown", (ev) => handleKey(ev, elText, elPre));
elText.addEventListener("paste", (ev) => handlePaste(ev, elText, elPre));
// Init:
colorMention(elText, elPre);
scrollMirror(elText, elPre);
});
* {
margin: 0;
box-sizing: border-box;
}
.message {
font: 16px/1 sans-serif;
display: inline-block;
border: 1px solid #888;
position: relative;
}
.message>* {
position: relative;
display: block;
overflow: hidden;
font: inherit;
padding: 0.5rem;
width: 13rem;
resize: none;
}
/* The overlay TEXTAREA with transparent text but visible caret */
.message>textarea {
border: 0;
white-space: nowrap;
background: transparent;
/* Make text invisible */
color: transparent;
/* But keep caret visible */
caret-color: black;
}
/* The underlaying PRE with colors */
.message>pre {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
/* Prevent text selections */
user-select: none;
}
.mention {
color: fuchsia;
}
<div >
<pre></pre>
<textarea cols="50" rows="1" spellcheck="false">Hi @test, how are you?</textarea>
</div>
2. Textarea field:
Till now the above code just prevents the "Enter"
key to create new lines and the <textarea>
tries to emulate the standard behavior of <input>
. If instead you do need a textarea like i.e: for a chat app messaging area:
const colorMention = (elText, elPre) => {
elPre.innerHTML = elText.value.replace(/(?<=^| )@\p{L} /gu, "<span class='mention'>$&</span>");
};
const scrollMirror = (elText, elPre) => {
elPre.scrollTo(elText.scrollLeft, elText.scrollTop);
};
const handlePaste = (ev, elText, elPre) => {
ev.preventDefault();
const value = ev.clipboardData.getData('Text');
elText.value = value.replace(/(\r\n|\n|\r)/gm, "");
elText.previousElementSibling.innerHTML = elText.value;
};
const handleKey = (ev, elText, elPre) => {
if (ev.key === "Enter" && !ev.shiftKey) {
// prevent Enter key behavior (new-line)
ev.preventDefault();
// Check message to send...
const message = elText.value.trim(); // (trim whitespaces)
if (!message) {
return; // Nothing to submit. Message is empty
}
// YOUR SUBMIT LOGIC GOES HERE:
console.log(message);
// Clear fields:
elText.value = "";
elPre.innerHTML = "";
} else {
// Any other key: (scroll areas is necessary)
scrollMirror(elText, elPre);
}
};
document.querySelectorAll(".message").forEach(el => {
const elText = el.querySelector("textarea");
const elPre = el.querySelector("pre");
elText.addEventListener("scroll", () => scrollMirror(elText, elPre));
elText.addEventListener("keyup", () => scrollMirror(elText, elPre));
elText.addEventListener("input", () => colorMention(elText, elPre));
elText.addEventListener("keydown", (ev) => handleKey(ev, elText, elPre));
elText.addEventListener("paste", (ev) => handlePaste(ev, elText, elPre));
// Init:
colorMention(elText, elPre);
scrollMirror(elText, elPre);
});
* {
margin: 0;
box-sizing: border-box;
}
.message {
font: 16px/1.3 sans-serif;
display: block;
border: 1px solid #888;
position: relative;
}
.message>* {
position: relative;
display: block;
overflow-y: scroll;
font: inherit;
padding: 0.5rem;
height: 80px;
resize: none;
width: 100%;
}
/* The overlay TEXTAREA with transparent text but visible caret */
.message>textarea {
border: 0;
background: transparent;
/* Make text invisible */
color: transparent;
/* But keep caret visible */
caret-color: black;
}
/* The underlaying PRE with colors */
.message>pre {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
/* Prevent text selections */
user-select: none;
}
.mention {
color: fuchsia;
}
<div >
<pre></pre>
<textarea spellcheck="false">Hi @Anna, how are you?
Hope @JohnDoe called you today!
</textarea>
</div>
Chat tips:<br> Use <code>@username</code> to mention a user.<br> Use <kbd>Enter</kbd> to send.<br> Use <kbd>Shift</kbd> <kbd>Enter</kbd> to go to a new line.<br>
Regarding the regular expression for matching mentions:
/(?<=^| )@\p{L} /gu
here's a Regex101.com example with description, and a related answer.