I have a toggle button that changes a bit of text. The problem I run into is if I have 2 words and I want to change the text of one it changes one but when I toggle it off style is removed from both spans instead of the span of the selected text.
How can I remove the span from the specific text selected and leave the span on the other text?
function headuppercase(e) {
tags('span', 'sC');
}
function tags(tag, clas) {
var ele = document.createElement(tag);
ele.classList.add(clas);
wrap(ele);
}
function wrap(tags) {
var el = document.querySelector('span.sC');
sel = window.getSelection();
if (!el) {
if (sel.rangeCount && sel.getRangeAt) {
range = sel.getRangeAt(0);
}
document.designMode = "on";
if (range) {
sel.removeAllRanges();
sel.addRange(range);
}
range.surroundContents(tags);
} else {
var parent = el.parentNode;
while (el.firstChild) parent.insertBefore(el.firstChild, el);
parent.removeChild(el);
}
document.designMode = "off";
}
.ourbutton {
padding: 5px;
float: left;
font-variant: small-caps;
}
.container {
width: 200px;
height: 300px;
float: left;
}
.spanA {
width: 100px;
height: 80px;
max-width: 200px;
max-height: 300px;
float: left;
border: thin blue solid;
}
.sC {
font-variant: small-caps;
}
<button class="ourbutton" type="button" onclick="headuppercase();">Tt</button>
<div class="container">
<span class="spanA" contenteditable="true"></span>
</div>
No jQuery please. Thanks You!
The issue is with this document.querySelector('span.sC'). In all the case it will select the first span with sC which is not good as you have to deal with the current one.
Here is an idea of fix:
function headuppercase(e) {
tags('span', 'sC');
}
function tags(tag, clas) {
var ele = document.createElement(tag);
ele.classList.add(clas);
wrap(ele);
}
function wrap(tags) {
sel = window.getSelection();
if (sel.rangeCount && sel.getRangeAt) {
range = sel.getRangeAt(0);
}
document.designMode = "on";
if (range) {
sel.removeAllRanges();
sel.addRange(range);
}
range.surroundContents(tags);
if(tags.querySelector('.sC')) {
tags.classList.remove('sC');
tags.innerHTML=tags.querySelector('.sC').innerHTML;
}
document.designMode = "off";
}
.ourbutton {
padding: 5px;
float: left;
font-variant: small-caps;
}
.container {
width: 200px;
height: 300px;
float: left;
}
.spanA {
width: 100px;
height: 80px;
max-width: 200px;
max-height: 300px;
float: left;
border: thin blue solid;
}
.sC {
font-variant: small-caps;
}
<button class="ourbutton" type="button" onclick="headuppercase();">Tt</button>
<div class="container">
<span class="spanA" contenteditable="true"></span>
</div>
This is not so easily achievable without a library because of all the edge cases you need to handle. I will first touch on some edge cases and then give you an example on how to implement them.
Simple case
Say we have the following string inside a text Node
"Nobody wants too complex code because it becomes unmanageable."
Consider a user who selected the words "Nobody wants" and pressed the toggle small caps button. You should end up with something looking like this (where the bold segments represent text in small-caps):
"Nobody wants too complex code because it becomes unmanageable."
This is an easy case. Just wrap the segment "Nobody wants" inside a <span> and give it the sC class. The same goes for all other segments that are not yet in small caps. Nothing too hard here.
Edge case 1
But say you are in the following state (again, bold segments represent text in small-caps) :
"Nobody wants too complex code because it becomes unmanageable."
When the user selects and toggles the word "becomes" things become complicated. You have to:
Remove the last two words from their containing <span class="sC"> element
Add the word "becomes" after the span (from step 1.) and make sure it is not contained in a <span> that has the className sC.
Add the word "unmanageable" inside a new <span class="sC"> element after the text Node "becomes" (that was inserted in step 2.)
Edge case 2
Or say you are in the following state (again, bold segments represent text in small-caps) :
"Nobody wants too complex code because it becomes unmanageable."
What should happen when somebody selects and toggles the segment "wants too complex code because" ? One could say: make every character in this segment
small-caps : when it is not
back to normal : when it is currently small-caps
It's easy to see that you will again need a lot of splitting existing span elements, creating new text nodes, and so forth
Edge case N
Say you start with a nested list
A
B
C
D
and a user selects the two last items at once. Then you need to wrap each item separately into a span.
First step towards a solution
While it is not clear in your question how all the edge cases should be handled, here is a first step towards a solution.
const allCapsClass = 'sC'
function toggleCase() {
const selection = window.getSelection()
if (selection && selection.rangeCount>0) {
const spans = wrapInNonNestedSpanners(selection.getRangeAt(0))
for (const span of spans) {
const classes = span.classList
const action = classes.contains(allCapsClass) ? 'remove' : 'add'
classes[action](allCapsClass)
}
}
}
const spannerClassName = "non-nested-spanner"
const spannerQuerySelector = `.${spannerClassName}`
function wrapInNonNestedSpanners(range) {
const containingSpanner = getContainingSpanner(range)
const result = []
if (containingSpanner != null) { // Edge case 1
const endRange = document.createRange() // contents of the span after range
endRange.selectNode(containingSpanner)
endRange.setStart(range.endContainer, range.endOffset)
const endContents = endRange.cloneContents()
const wrappedSelectionContents = containingSpanner.cloneNode(false)
wrappedSelectionContents.appendChild(range.cloneContents())
endRange.deleteContents()
range.deleteContents()
const parent = containingSpanner.parentNode
const next = containingSpanner.nextSibling
parent.insertBefore(wrappedSelectionContents, next)
result.push(wrappedSelectionContents)
if (!isEmptySpanner(endContents.childNodes[0])) parent.insertBefore(endContents, next)
if (isEmptySpanner(containingSpanner)) parent.removeChild(containingSpanner)
const newSelection = document.createRange()
newSelection.selectNode(wrappedSelectionContents)
window.getSelection().removeAllRanges()
window.getSelection().addRange(newSelection)
} else { // Edge case 2
const contents = range.extractContents()
const spanners = contents.querySelectorAll(spannerQuerySelector)
let endRange = document.createRange() // range before the span
for (let index = spanners.length-1; index>=0; index--) {
const spanner = spanners[index]
endRange.selectNodeContents(contents)
endRange.setStartAfter(spanner)
if (!endRange.collapsed) {
const wrappedEndContents = createSpannerWrapping(endRange.extractContents())
range.insertNode(wrappedEndContents)
result.unshift(wrappedEndContents)
}
range.insertNode(spanner)
result.unshift(spanner)
}
const rest = createSpannerWrapping(contents)
if (!isEmptySpanner(rest)) {
range.insertNode(rest)
result.unshift(rest)
}
}
return result
}
function getContainingSpanner(range) {
let cursor = range.commonAncestorContainer
if (cursor.classList == undefined) cursor = cursor.parentElement
while (cursor.parentElement != null) {
if (cursor.classList.contains(spannerClassName)) return cursor
cursor = cursor.parentElement
}
return null
}
function createSpannerWrapping(childNode) {
const spanner = document.createElement('span')
spanner.classList.add(spannerClassName)
spanner.appendChild(childNode)
return spanner
}
function isEmptySpanner(spanner) {
if (spanner.childNodes.length == 0) return true
else if (spanner.childNodes.length == 1) {
const node = spanner.childNodes[0]
return node instanceof Text && node.length == 0
}
return false
}
.sC {
font-variant: small-caps;
}
<section contenteditable>
Hello world this is some text
</section>
<button onclick="toggleCase()">Toggle case</button>
Related
I have a div that acts as a WYSIWYG editor. This acts as a text box but renders markdown syntax within it, to show live changes.
Problem: When a letter is typed, the caret position is reset to the start of the div.
const editor = document.querySelector('div');
editor.innerHTML = parse('**dlob** *cilati*');
editor.addEventListener('input', () => {
editor.innerHTML = parse(editor.innerText);
});
function parse(text) {
return text
.replace(/\*\*(.*)\*\*/gm, '**<strong>$1</strong>**') // bold
.replace(/\*(.*)\*/gm, '*<em>$1</em>*'); // italic
}
div {
height: 100vh;
width: 100vw;
}
<div contenteditable />
Codepen: https://codepen.io/ADAMJR/pen/MWvPebK
Markdown editors like QuillJS seem to edit child elements without editing the parent element. This avoids the problem but I'm now sure how to recreate that logic with this setup.
Question: How would I get the caret position to not reset when typing?
Update:
I have managed to send the caret position to the end of the div, on each input. However, this still essentially resets the position. https://codepen.io/ADAMJR/pen/KKvGNbY
You need to get position of the cursor first then process and set the content. Then restore the cursor position.
Restoring cursor position is a tricky part when there are nested elements. Also you are creating new <strong> and <em> elements every time, old ones are being discarded.
const editor = document.querySelector(".editor");
editor.innerHTML = parse(
"For **bold** two stars.\nFor *italic* one star. Some more **bold**."
);
editor.addEventListener("input", () => {
//get current cursor position
const sel = window.getSelection();
const node = sel.focusNode;
const offset = sel.focusOffset;
const pos = getCursorPosition(editor, node, offset, { pos: 0, done: false });
if (offset === 0) pos.pos += 0.5;
editor.innerHTML = parse(editor.innerText);
// restore the position
sel.removeAllRanges();
const range = setCursorPosition(editor, document.createRange(), {
pos: pos.pos,
done: false,
});
range.collapse(true);
sel.addRange(range);
});
function parse(text) {
//use (.*?) lazy quantifiers to match content inside
return (
text
.replace(/\*{2}(.*?)\*{2}/gm, "**<strong>$1</strong>**") // bold
.replace(/(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)/gm, "*<em>$1</em>*") // italic
// handle special characters
.replace(/\n/gm, "<br>")
.replace(/\t/gm, " ")
);
}
// get the cursor position from .editor start
function getCursorPosition(parent, node, offset, stat) {
if (stat.done) return stat;
let currentNode = null;
if (parent.childNodes.length == 0) {
stat.pos += parent.textContent.length;
} else {
for (let i = 0; i < parent.childNodes.length && !stat.done; i++) {
currentNode = parent.childNodes[i];
if (currentNode === node) {
stat.pos += offset;
stat.done = true;
return stat;
} else getCursorPosition(currentNode, node, offset, stat);
}
}
return stat;
}
//find the child node and relative position and set it on range
function setCursorPosition(parent, range, stat) {
if (stat.done) return range;
if (parent.childNodes.length == 0) {
if (parent.textContent.length >= stat.pos) {
range.setStart(parent, stat.pos);
stat.done = true;
} else {
stat.pos = stat.pos - parent.textContent.length;
}
} else {
for (let i = 0; i < parent.childNodes.length && !stat.done; i++) {
currentNode = parent.childNodes[i];
setCursorPosition(currentNode, range, stat);
}
}
return range;
}
.editor {
height: 100px;
width: 400px;
border: 1px solid #888;
padding: 0.5rem;
white-space: pre;
}
em, strong{
font-size: 1.3rem;
}
<div class="editor" contenteditable ></div>
The API window.getSelection returns Node and position relative to it. Every time you are creating brand new elements so we can't restore position using old node objects. So to keep it simple and have more control, we are getting position relative to the .editor using getCursorPosition function. And, after we set innerHTML content we restore the cursor position using setCursorPosition.
Both functions work with nested elements.
Also, improved the regular expressions: used (.*?) lazy quantifiers and lookahead and behind for better matching. You can find better expressions.
Note:
I've tested the code on Chrome 97 on Windows 10.
Used recursive solution in getCursorPosition and setCursorPosition for the demo and to keep it simple.
Special characters like newline require conversion to their equivalent HTML form, e.g. <br>. Tab characters require white-space: pre set on the editable element. I've tried to handled \n, \t in the demo.
The way most rich text editors does it is by keeping their own internal state, updating it on key down events and rendering a custom visual layer. For example like this:
const $editor = document.querySelector('.editor');
const state = {
cursorPosition: 0,
contents: 'hello world'.split(''),
isFocused: false,
};
const $cursor = document.createElement('span');
$cursor.classList.add('cursor');
$cursor.innerText = ''; // Mongolian vowel separator
const renderEditor = () => {
const $contents = state.contents
.map(char => {
const $span = document.createElement('span');
$span.innerText = char;
return $span;
});
$contents.splice(state.cursorPosition, 0, $cursor);
$editor.innerHTML = '';
$contents.forEach(el => $editor.append(el));
}
document.addEventListener('click', (ev) => {
if (ev.target === $editor) {
$editor.classList.add('focus');
state.isFocused = true;
} else {
$editor.classList.remove('focus');
state.isFocused = false;
}
});
document.addEventListener('keydown', (ev) => {
if (!state.isFocused) return;
switch(ev.key) {
case 'ArrowRight':
state.cursorPosition = Math.min(
state.contents.length,
state.cursorPosition + 1
);
renderEditor();
return;
case 'ArrowLeft':
state.cursorPosition = Math.max(
0,
state.cursorPosition - 1
);
renderEditor();
return;
case 'Backspace':
if (state.cursorPosition === 0) return;
delete state.contents[state.cursorPosition-1];
state.contents = state.contents.filter(Boolean);
state.cursorPosition = Math.max(
0,
state.cursorPosition - 1
);
renderEditor();
return;
default:
// This is very naive
if (ev.key.length > 1) return;
state.contents.splice(state.cursorPosition, 0, ev.key);
state.cursorPosition += 1;
renderEditor();
return;
}
});
renderEditor();
.editor {
position: relative;
min-height: 100px;
max-height: max-content;
width: 100%;
border: black 1px solid;
}
.editor.focus {
border-color: blue;
}
.editor.focus .cursor {
position: absolute;
border: black solid 1px;
border-top: 0;
border-bottom: 0;
animation-name: blink;
animation-duration: 1s;
animation-iteration-count: infinite;
}
#keyframes blink {
from {opacity: 0;}
50% {opacity: 1;}
to {opacity: 0;}
}
<div class="editor"></div>
You need to keep the state of the position and restore it on each input. There is no other way. You can look at how content editable is handled in my project jQuery Terminal (the links point to specific lines in source code and use commit hash, current master when I've written this, so they will always point to those lines).
insert method that is used when user type something (or on copy-paste).
fix_textarea - the function didn't changed after I've added content editable. The function makes sure that textarea or contenteditable (that are hidden) have the same state as the visible cursor.
clip object (that is textarea or content editable - another not refactored name that in beginning was only for clipboard).
For position I use jQuery Caret that is the core of moving the cursor. You can easily modify this code and make it work as you want. jQuery plugin can be easily refactored into a function move_cursor.
This should give you an idea how to implement this on your own in your project.
You can use window.getSelection to get the current position and, after parsing, move the cursor to again this position with sel.modify.
const editor = document.querySelector('div')
editor.innerHTML = parse('**dlob** *cilati*')
sel = window.getSelection()
editor.addEventListener('input', () => {
sel.extend(editor, 0)
pos = sel.toString().length
editor.innerHTML = parse(editor.innerText)
while (pos-->0)
sel.modify('move', 'forward', "character")
})
function parse(text) {
return text
.replace(/\*\*(.*)\*\*/gm, '**<strong>$1</strong>**') // bold
.replace(/\*(.*)\*/gm, '*<em>$1</em>*'); // italic
}
div {
height: 100vh;
width: 100vw;
}
<div contenteditable />
That said, note the edit history is gone (i.e. no undo), when using editor.innerHTML = ....
As other indicated, it seems better to separate editing and rendering.
I call this pseudo-contenteditable. I asked a question related to this
Pseudo contenteditable: how does codemirror works?. Still waiting for an answer.
But the basic idea might look this https://jsfiddle.net/Lfbt4c7p.
I want to insert a HTML tag in a keypress event. I am new to jQuery and JavaScript, and I've also searched for this thing, but got no answer for this thing
I know about the append and selectionStart methods but i want to insert at the position of text cursor
For E.g., if i type the letter 'a', it should send <img src='a.png'> to the div, at the cursor position
$("#textcontent").contentEditable = "true";
$(document).ready(function(){
$("#textcontent").keydown(function(event){
$("#textcontent").append("<img src='" + event.key +".png'>");
But this (JS append code at the top) is not I want, but I want to insert a HTML tag at the current text cursor position
My HTML:
<div class="textcontent" contenteditable="true">
<img src='h.png'>
... // more image tags goes here
</div>
Can anybody pls give some answers for this problem?
Check this out:
$("#textcontent").keydown(function(event) {
if (event.key === "Backspace") return; // allow backspace to clear image
if (!/^[a-zA-Z0-9]{1,1}$/.test(event.key)) return false; // just allow a-z and 0-9
const position = document.getSelection().anchorOffset; // get position
const newElement = $(`<img alt="${event.key}" src="https://eu.ui-avatars.com/api/?name=${event.key}">`); // create new element
const element = [...$("#textcontent img")][position]; // get previous element on index "position"
if (element)
textcontent.insertBefore(newElement.get(0), element); // prepend element
else $("#textcontent").append(newElement); // append element
/* OPTIONAL! place caret at the end */
textcontent.focus();
const range = document.createRange();
range.selectNodeContents(textcontent);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
/**/
return false; // cancel event to avoid text inside div
});
#textcontent {
border: 2px solid gray;
padding: 10px;
border-radius: 10px;
}
img {
margin-left: 7px;
width: 30px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="textcontent" contenteditable="true"></div>
I've made some changes on #jns's code and this is what i've came up with...
Added the function to use lowercase characters, space and also for some other symbols...
Thank you #jns!
$("#textcontent").keydown(function(event) {
if ((event.key === "Backspace")||(event.ctrlKey)||(event.altKey)||(event.keyCode == 46)||(event.which>=33 && event.which <=40)) return; // Allow other keys to do their normal functions (not for Enter key)
if (!/^[a-zA-Z0-9\/`~=\[\]'\\; ]$/.test(event.key)) return false; // Just allows A-Z, a-z, 0-9, and also characters like `~=[];'\/
var letter_name = event.key;
var uppercase = "false";
if (event.key === ' ' || event.key === 'Spacebar') // To add spacebar input feature ('Spacebar' for IE9 and Firefox < 37)
letter_name = '';
else
letter_name = event.key;
if (/^[A-Z]$/.test(letter_name)) // To chech whether its an uppercase letter
uppercase = "true";
else
uppercase = "false";
const position = document.getSelection().anchorOffset; // get position
const newElement = $(`<img alt="${letter_name}" src="https://eu.ui-avatars.com/api/?uppercase=${uppercase}&name=${letter_name}">`); // create new element
const element = [...$("#textcontent img")][position]; // get previous element on index "position"
if (element)
textcontent.insertBefore(newElement.get(0), element); // prepend element
else $("#textcontent").append(newElement); // append element
/* OPTIONAL! place caret at the end */
textcontent.focus();
const range = document.createRange();
range.selectNodeContents(textcontent);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
/**/
return false; // cancel event to avoid text inside div
});
#textcontent {
border: 2px solid gray;
padding: 10px;
border-radius: 10px;
}
img {
margin-left: 7px;
width: 30px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="textcontent" contenteditable="true"></div>
How can I keep the cursor in the right place when typing inside of a <div id="richTextBox" contenteditable="true"></div> whose innerHTML changes on each keystroke? The act of replacing the innerHTML messes up the cursor position.
The reason I change the innerHTML is because I am adding <span> tags. It's part of a code highlighting program. The span tags allow me to place the correct color highlights.
I am using the below code from a StackOverflow answer as a band aid for the moment, but it has a significant bug. If you hit enter, the cursor stays at the old spot, or goes to a random spot. That's because the algorithm counts how many characters from the beginning the cursor is. But it doesn't count HTML tags or line breaks as characters. And the richTextBox inserts <br> to make enters.
Ideas for fixing:
Fix the below code? See Fiddle
Replace with simpler code? I tried a bunch of simpler stuff involving window.getSelection() and document.createRange(), but I could not get that to work.
Replace with a richTextBox library or module that doesn't have this bug?
Screenshot
// Credit to Liam (Stack Overflow)
// https://stackoverflow.com/a/41034697/3480193
class Cursor {
static getCurrentCursorPosition(parentElement) {
var selection = window.getSelection(),
charCount = -1,
node;
if (selection.focusNode) {
if (Cursor._isChildOf(selection.focusNode, parentElement)) {
node = selection.focusNode;
charCount = selection.focusOffset;
while (node) {
if (node === parentElement) {
break;
}
if (node.previousSibling) {
node = node.previousSibling;
charCount += node.textContent.length;
} else {
node = node.parentNode;
if (node === null) {
break;
}
}
}
}
}
return charCount;
}
static setCurrentCursorPosition(chars, element) {
if (chars >= 0) {
var selection = window.getSelection();
let range = Cursor._createRange(element, { count: chars });
if (range) {
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
}
}
static _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 = Cursor._createRange(node.childNodes[lp], chars, range);
if (chars.count === 0) {
break;
}
}
}
}
return range;
}
static _isChildOf(node, parentElement) {
while (node !== null) {
if (node === parentElement) {
return true;
}
node = node.parentNode;
}
return false;
}
}
window.addEventListener('DOMContentLoaded', (e) => {
let richText = document.getElementById('rich-text');
richText.addEventListener('input', function(e) {
let offset = Cursor.getCurrentCursorPosition(richText);
// Pretend we do stuff with innerHTML here. The innerHTML will end up getting replaced with slightly changed code.
let s = richText.innerHTML;
richText.innerHTML = "";
richText.innerHTML = s;
Cursor.setCurrentCursorPosition(offset, richText);
richText.focus(); // blinks the cursor
});
});
body {
margin: 1em;
}
#rich-text {
width: 100%;
height: 450px;
border: 1px solid black;
cursor: text;
overflow: scroll;
resize: both;
/* in Chrome, must have display: inline-block for contenteditable=true to prevent it from adding <div> <p> and <span> when you type. */
display: inline-block;
}
<p>
Click somewhere in the middle of line 1. Hit enter. Start typing. Cursor is in the wrong place.
</p>
<p>
Reset. Click somewhere in the middle of line 1. Hit enter. Hit enter again. Cursor goes to some random place.
</p>
<div id="rich-text" contenteditable="true">Testing 123<br />Testing 456</div>
Browser
Google Chrome v83, Windows 7
The issue seems to be that adding a new line adds a <br>, but as you are still in the parent element, previous DOM children are not taken into account, and the selection.focusOffset only gives the value of 4.
It may help to add a newline to the end of the innerHtml, as it is being stripped when you remove and re-add it. + "\n" to the end of line 100 on the Fiddle would do.
Your main problem though is that getCurrentCursorPosition you copied from that other StackOverflow question doesn't actually work.
I'd suggest you go through some of the other answers to this question:
Get contentEditable caret index position, and console.log what they output and see which one works best for your edge-cases.
If you don't want to write it yourself, then Caret.js (part of the At.js editor library) would be useful.
I am regenerating innerHTML with every keystroke in order to wrap words from innerText with spans. I do this to search for keywords that have different style.
process() {
this.content.nativeElement.innerHTML = this.text.split(" ").map((word, index) => {
let wrapped;
if (this.elements.map(e => e.name).includes(word)) {
wrapped = `<span style="color: darkblue; font-weight: bold; text-decoration: underline; cursor: pointer;">${word}</span>`
} else {
wrapped = `<span style="cursor: pointer;">${word}</span>`
}
if (word === this.word) {
this.spanIndex = index;
}
return wrapped;
}).join(" ");
this.setCaretPosition();
this.addInnerHTMLListeners();
}
Within the process function I set back the caret position.
setCaretPosition() {
this.target.focus();
var lastNode = this.content.nativeElement.childNodes[this.nodeIndex];
var range = document.createRange();
const letterIndex = this.letterIndex;
console.log(letterIndex, lastNode.childNodes.length)
range.setStart(lastNode, letterIndex); //lastNode.childNodes.length
range.setEnd(lastNode, letterIndex);
range.collapse(true);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
It works great with lastNode.childNodes.length when the cursor should be at the end of a word.
But when I am in middle of it I dont know how to do that.
Since when I am at position 2 (third letter) then setting it at 2 will result in
ERROR DOMException: Failed to execute 'setStart' on 'Range': There is no child at offset 2.
I figured it out. Didnt realize the DOM was more complicated.
The innerHTML of the div consists of spans like this:
<span style="color: darkblue; font-weight: bold; text-decoration: underline; cursor: pointer;">${word}</span>
I knew which span it is
var node = this.content.nativeElement.childNodes[this.nodeIndex];
But to set the caret inside the word I need to get its first child which is probably text
var range = document.createRange();
console.log(node, node.firstChild, this.letterIndex)
range.setStart(node.firstChild, this.letterIndex);
range.setEnd(node.firstChild, this.letterIndex);
This may be a stupid question that might be easy to find but i'm quite new to all of this and i can't seem to find what i'm looking for or atleast i don't know what i need to look for, thus I'm here.
So what I'm trying to do is create a kind of Linux terminal... This is what i got so far.
What I'm stuck on is the actual entering text part...
I've been trying to create a div with contenteditable=true as well as trying out Input elements but neither seems to be working how i want it to.
The current structure that i'm using for this is:
<div class="title" contenteditable="false" >
admin#localhost:~$
<div class="write-point" contenteditable="true" ></div>
<div class="linux-cursor" contenteditable="false"></div>
However this only deletes the whole line of text. "admin#localhost:~$" as well as the cursor.
I've also tried using JavaScript to put the cursor after the text but its not working at all.
function forStackOverFlow() {
var textInput = document.getElementsByClassName('write-point');
textInput.onkeydown = function(e) {
console.log(textInput.value);
var childTag = document.getElementsByClassName("write-point");
childTag.parentNode.insertBefore(textInput.value, childTag.nextSibling);
}};
So my main questions are:
How and what is needed to move a div(cursor element) to the end of input text(user input)
Is it possible to allow a user to type immediately once the webpage has loaded?
Thanks, any help would be great :)
You can do that in a better way with some CSS to make sure the "caret" element always comes after the contenteditable one and some JS to make sure the contenteditable element is always focused. You might try to do this last thing by adding autofocus to the contenteditable element and using a <label> for the caret element, but that doesn't work on contenteditable elements. Note no keyboard event listeners are needed:
const input = document.getElementById('input');
const caret = document.getElementById('caret');
// Move the focus back to the input if it moves away from it:
input.addEventListener('blur', (e) => {
input.focus();
});
// Set the focus to the input so that you can start typing straight away:
input.focus();
body {
background: #000;
color: #0F0;
font-family: monospace;
height: 100vh;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: scroll;
margin: 0;
padding: 16px;
}
#input {
display: inline;
word-break: break-all;
outline: none;
visibility: visible;
}
#caret {
border: 0;
padding: 0;
outline: none;
background-color: #0F0;
display: inline-block;
font-family: monospace;
}
admin#localhost:~$
<div id="input" contenteditable="true"></div><button id="caret" for="input"> </button>
In a more realistic example, you might want to:
Avoid trapping the focus in the contenteditable element, as that would prevent selecting previous commands. Instead, focus to the contenteditable element only once the user presses some key.
Show a different caret depending on its position: square if it's at the end of the input, line if it's somewhere else (unless overtype mode is enabled with the Ins key).
Add a new command/entry if ↵ is pressed.
Prevent entering formatted text and automatically split it up into multiple commands/entries when needed.
const history = document.getElementById('history');
const input = document.getElementById('input');
const cursor = document.getElementById('cursor');
function focusAndMoveCursorToTheEnd(e) {
input.focus();
const range = document.createRange();
const selection = window.getSelection();
const { childNodes } = input;
const lastChildNode = childNodes && childNodes.length - 1;
range.selectNodeContents(lastChildNode === -1 ? input : childNodes[lastChildNode]);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
function handleCommand(command) {
const line = document.createElement('DIV');
line.textContent = `admin#localhost:~$ ${ command }`;
history.appendChild(line);
}
// Every time the selection changes, add or remove the .noCursor
// class to show or hide, respectively, the bug square cursor.
// Note this function could also be used to enforce showing always
// a big square cursor by always selecting 1 chracter from the current
// cursor position, unless it's already at the end, in which case the
// #cursor element should be displayed instead.
document.addEventListener('selectionchange', () => {
if (document.activeElement.id !== 'input') return;
const range = window.getSelection().getRangeAt(0);
const start = range.startOffset;
const end = range.endOffset;
const length = input.textContent.length;
if (end < length) {
input.classList.add('noCaret');
} else {
input.classList.remove('noCaret');
}
});
input.addEventListener('input', () => {
// If we paste HTML, format it as plain text and break it up
// input individual lines/commands:
if (input.childElementCount > 0) {
const lines = input.innerText.replace(/\n$/, '').split('\n');
const lastLine = lines[lines.length - 1];
for (let i = 0; i <= lines.length - 2; ++i) {
handleCommand(lines[i]);
}
input.textContent = lastLine;
focusAndMoveCursorToTheEnd();
}
// If we delete everything, display the square caret again:
if (input.innerText.length === 0) {
input.classList.remove('noCaret');
}
});
document.addEventListener('keydown', (e) => {
// If some key is pressed outside the input, focus it and move the cursor
// to the end:
if (e.target !== input) focusAndMoveCursorToTheEnd();
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleCommand(input.textContent);
input.textContent = '';
focusAndMoveCursorToTheEnd();
}
});
// Set the focus to the input so that you can start typing straigh away:
input.focus();
body {
background: #000;
color: #0F0;
font-family: monospace;
height: 100vh;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: scroll;
word-break: break-all;
margin: 0;
padding: 16px;
}
#input {
display: inline;
outline: none;
visibility: visible;
}
/*
If you press the Insert key, the vertical line caret will automatically
be replaced by a one-character selection.
*/
#input::selection {
color: #000;
background: #0F0;
}
#input:empty::before {
content: ' ';
}
#keyframes blink {
to {
visibility: hidden;
}
}
#input:focus + #caret {
animation: blink 1s steps(5, start) infinite;
}
#input.noCaret + #caret {
visibility: hidden;
}
#caret {
border: 0;
padding: 0;
outline: none;
background-color: #0F0;
display: inline-block;
font-family: monospace;
}
<div id="history"></div>
admin#localhost:~$
<div id="input" contenteditable="true"></div><button id="caret" for="input"> </button>
In general, it's usually a bad idea to listen for keyboard events (keydown / keypress / keyup) to handle text input or cursors, as the value of the input can also be updated by pasting or dropping text into it and there are many edge cases, such as arrows, delete, escape, shortcuts such as select all, copy, paste... so trying to come up with an exhaustive list of all the keys we should take care of is probably not the best approach.
Moreover, that won't work on mobile, where most keys emit the same values e.key = 'Unidentified', e.which== 229 and e.keyCode = 229.
Instead, it's usually better to rely on other events such as input and use KeyboardEvents to handle very specific keys, like ↵ in this case.
If you need to check KeyboardEvent's properties values such as e.key, e.code, e.which or e.keyCode you can use https://keyjs.dev. I will add information about these kinds of cross-browser incompatibilities soon!
Disclaimer: I'm the author.
I suggest to use span instead of div because it's an inline element, more easy to manage in your case.
Next you can catch eatch keyboard's entry with a listener :
document.body.onkeydown
and tell him to append each key into a variable that you can display.
I let you think about all the functionnality you have to manage, like an enter or a backspace event.
you can play and find code you will need here : http://keycode.info/
Here is a working snippet :
var command = "";
document.body.onkeydown = function(e){
var writePoint = document.getElementById('writePoint');
command += String.fromCharCode(e.keyCode);
writePoint.innerHTML = command;
};
.linux-cursor{
width:7px;
height:15px;
background-color:green;
display:inline-block;
}
<span class="title" contenteditable="false" >
admin#localhost:~$
</span>
<span class="write-point" id="writePoint" contenteditable="true" ></span>
<span class="linux-cursor" contenteditable="false"></span>
Hope this help you, it looks like a nice project.