var range = document.createRange();
//start range at a point somewhere in the first #text node
range.setStart(document.getElementById('my_textarea').childNodes[0], 4);
//end range outside of span
range.setEnd(document.getElementById('my_textarea').childNodes[2], 0);
range.startContainer.innerHTML = "hi";
<div id="my_textarea" contenteditable>
000000000<span id="test_span" style="font-weight:bold;">000000000</span>
</div>
I have a range and I would like to insert an opening span tag in the startContainer at startOffset. I am unsure of how to even alter the html of the startContainer. My problem may be arising because startContainer is #text which is a weird browser implementation, not sure. The following code has no effect:
range.startContainer.outerHTML = "<b>This is an example of modifying the startContainers outerHTML</b>";
This does not produce the expected results. I have a codepen however the issue may be a little difficult to recreate if your not familiar with the code and also it may only work in firefox, have yet to test it in other browsers.
https://codepen.io/justinhdevelopment/pen/GRZzEom
Sorry about all the if statements but the area of interest would be:
} //End for loop
}else if(range.commonAncestorContainer == textarea && (range.startContainer.nodeName === "#text" && range.endContainer.nodeName === "#text")){
if(range.startContainer.parentNode.nodeName === "SPAN"){ console.log("Start Container Parent Node is SPAN");}else{
console.log("OOOOOOOOOOOOOMMMMMMMGGGGGGGGG");
To recreate the problem, type a string (example: 00000000000000000000000) highlight a portion of the end and click bold. Then highlight another portion of the end that contains the bold part as well as non bold text ( the point here is to make a selection that fully contains the bold span and also contains non styled text) This will set the commonAncestorContainer to the textarea and the start and end container to #text node. Now with this Range I can splice in an opening span tag and end tag, but I can't seem to alter the HTML of the startContainer. I apologize if I dont make sense, but if any clarification is needed I will gratefully explain. Thank you for your help and time.
Since the text node doesn't contain any HTML, outerHTML does nothing for the text node.
Consider using a span instead.
var span = document.getElementById("span");
span.outerHTML = '<span class="blue">changed!</span>'
.blue {
color: blue;
}
<body style="font-family:sans-serif;margin:0">
<p>Let's change <span id="span">this</span> word blue.</p>
</body>
Looking at the methods available on Range, it seems like what you want to do is deleteContents(), then insertNode() to add your new text.
var range = document.createRange();
//start range at a point somewhere in the first #text node
range.setStart(document.getElementById('my_textarea').childNodes[0], 4);
//end range outside of span
range.setEnd(document.getElementById('my_textarea').childNodes[2], 0);
range.deleteContents();
range.insertNode(document.createTextNode("hi"));
<div id="my_textarea" contenteditable>
000000000<span id="test_span" style="font-weight:bold;">000000000</span>
</div>
If, however, you just want to surround the contents of the range with a tag, use surroundContents()
var range = document.createRange();
//start range at a point somewhere in the first #text node
range.setStart(document.getElementById('my_textarea').childNodes[0], 4);
//end range outside of span
range.setEnd(document.getElementById('my_textarea').childNodes[2], 0);
var newSpan = document.createElement("span");
newSpan.classList.add("red");
range.surroundContents(newSpan);
.red { color: red; }
<div id="my_textarea" contenteditable>
000000000<span id="test_span" style="font-weight:bold;">000000000</span>
</div>
Range.surroundContents()
is the correct answer due to the inability to bypass the node insertion method javascript uses to manipulate the DOM.
surroundContents will not work if your range endContainer or startContainer is a sub-child of the commonAncestorContainer. So you will need to use setStartBefore and setEndAfter to properly surround the range.
Related
The behavior I'm looking to implement would wrap each word the user types in <div class="word"></div> as they type their message. The parent div they're typing into has contenteditable="true".
One complicating factor is that for this use case one "word" div may contain two words (imagine a name would be considered one "word" so e.g. <div class="word">Bob Smith</div> may occur). This means I can't just grab all of the textContent when the user presses space and split(" ") into an array to build the div.word DOM elements.
I'm thinking when the user presses space I can get all the child nodes of the contenteditable div and loop through them to check which is a textNode and which is not (i.e. a word already wrapped in div.word). Then for the text nodes only I can build a div.word DOM element and append all of these to the contenteditable div.
I hope that's clear. Here's sort of where I'm at:
<div id="editor" contenteditable></div>
#editor {
border: 1px solid #333;
padding: 10px;
}
#editor .word {
background: yellow;
display: inline-block;
}
const editorElement = document.getElementById('editor');
function handleSpacebarPress() {
// Get all editor child nodes
let editorChildNodes = [...editor.childNodes];
// Clear editor of all child nodes
editor.innerHTML = '';
editorChildNodes.forEach(node => {
// If node is a text node (not a div.word element)
if (node.nodeType === 3) {
// Create div.word
let wordDiv = document.createElement('div');
wordDiv.className = 'word';
wordDiv.textContent = node.textContent;
editor.appendChild(wordDiv);
}
// Else node is already a div.word element
else {
editor.appendChild(node);
}
});
// Return caret to end of editor
const editorLength = editor.childNodes.length;
const lastNode = editor.childNodes[editorLength - 1]; // Last editor node
const range = document.createRange();
const selection = window.getSelection();
range.setStart(lastNode, 1);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
editorElement.addEventListener('keydown', e => {
if (e.code === 'Space') {
handleSpacebarPress();
}
});
You can view this on jsfiddle here.
Right now it seems to just place all words directly into the one div.word element and I can't seem to handle creating a new one for each word.
Any ideas?
I guess you want to achieve something like this:
Note how the spaces between the words are not highlighted and therefore have to be inside the main editor. You could fix this (as you pointed out in the comments) by adding spaces after each word in the editor and trim()-ming each word so that there are no spaces at the start or end of inner nodes.
You might to rethink your code, because it has some disadvantages.
Basically you are reimplementing what basic editors with custom syntax highlighting already do.
Manually setting the cursor position does not work, if you are editing in the middle of the editor.
Copy & pasting text wont trigger handleSpacebarPress
If you want to program the editor yourself, I suggest you use a function highlightWords that gets triggered after every change event (maybe with some artificial delay). In that function you can use a regular expression to find all the words according to your criteria and replace them with <span class=word>$1</span>.
I want a contenteditable div to call a function which constantly reviews its contents and replaces patterns within it (think Markdown). My attempts resulted in the cursor constantly positioning itself to the start of the div with the text being inserted in backwards order. Additionally, even if a (regex style replacement) pattern matches, replacements with HTML tags (such as bold) ends up not terminating, leaving the rest of the div bold. A simple code sample duplicating these issues follows:
Jsfiddle here (fixed)
<body>
<div id = "content" contenteditable = "true" oninput = "reformat()"></div>
</body>
// JS //
var content = document.getElementById("content");
function reformat()
{
content.innerHTML = content.innerHTML.replace(/dogs/g, "<b>cat</b>");
}
Basically, I want it to replace "dogs" with "cat" in bold and nothing else abnormal as the user is typing. My last resort is to convert non-contenteditable divs to act as contenteditable ones to avoid issues, but it involves reinventing the wheel for something that should be seemingly trivial. How should I approach this?
I have visited other posts such as replace string in contenteditable div but the cursor is still stuck and I would like my answer in vanilla JS if possible. If there's a platform other than HTML/JS/CSS (Visual Studio, Python tkinter, etc) that this is more suited towards, please let me know.
Try this, its around but i didn't found better solution.
I replacing event with setinterval so i dont need to distrub to content, second one i check if match, if so i replace the match string and return the caret to end of string by lastchild
<html>
<body>
<body>
<div onkeypress="reformat()" id="content" contenteditable="true"></div>
</body>
<script>
var content = document.getElementById("content");
function reformat()
{
var dogs = /dogs/g;
var content1=content.innerHTML+" ";
var found = content1.match(dogs);
if(found){
content.innerHTML = content1.replace(/dogs/g, "<b>cat</b>");
var range = document.createRange()
var sel = window.getSelection()
range.setStart(content.lastChild, 0)
range.collapse(true)
sel.removeAllRanges()
sel.addRange(range)
}
}
</script>
</html>
<style>
#content{
border: 1px solid black;
}
</style>
I'm trying to write a basic text editor using contenteditable. In this MCVE, it only has one function, which is that selected text is given a red highlight.†
The code I'm using is here:
function createSpan() {
let selection = document.getSelection();
let range = selection.getRangeAt(0);
let element = document.createElement("span");
element.className = "inline-equation";
range.surroundContents(element);
let newRange = new Range();
newRange.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(newRange);
}
$("button").click(createSpan)
.inline-equation {
background-color: red;
display: inline-block;
}
#editor {
width: 100%;
height: 100%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button>
Create Span
</button>
<div id="editor" contenteditable="true">
This is a contenteditable area.
</div>
I'm having trouble with the idea that the user may move out of the highlight area and continue typing in unformatted text. To experience this issue:
Run the Stack Snippet above
Select the text from somewhere in the middle til the end, then click Create Span
Type some new text at the end of the line
This new text has the red highlight too, even if you attempt to move out of the inserted span by pressing the right arrow key.
I'd still like to give the user the option to append new text which is formatted, but then also allow the user to navigate out of the span so that they may continue to type normal text.
In other words, the span should act as a completely separate editable object which may be moved into or out of. This includes the ability to move out of the span even if it's at the end of the document, so that the user can continue typing in non-formatted text.
The best example I am able to give of what I'd like is Microsoft Word's inline equations. Notice how, in the GIF below, the equation acts as a separate object, which I may navigate out of so that I can type normal text to the right of it. The is how I'd like my span to act.
I've tried replacing the span with a div with inline-block formatting to see if that affected the behaviour, but it didn't. How should I achieve the effect I'm looking for?
† In the actual use case, the 'highlight' actually denotes LaTeX-formatted mathematics which are rendered later. I'm writing what is essentially an editor for a proprietary markup language which supports inline LaTeX.
The issue is that you need something editable at end for this to work. There are lot of existing SO thread for the same. You can see below
Why Is My Contenteditable Cursor Jumping to the End in Chrome?
contenteditable put caret outside inserted span
contenteditable with nested span. Who has the focus?
Focusing on nested contenteditable element
Combining knowledge from above thread the simplest thing I could think of was adding below keyup handler
$("#editor").on('keyup',(e) => {
var editor = $("#editor").get(0)
var cn = editor.childNodes;
if (cn[cn.length - 1].nodeType !== Node.TEXT_NODE)
{
empty = document.createTextNode( '\uFEFF' );
editor.appendChild(empty);
}
if (cn[0].nodeType !== Node.TEXT_NODE)
{
empty = document.createTextNode( '\uFEFF' );
editor.prepend(empty);
}
})
Which makes sure there is one text node to step out of the div. You can do the same thing for the starting div if you want. Below is JSFiddle for the same
https://jsfiddle.net/4tcLr0qa/1/
I can not come up with a suitable solution...
<p class="session" contenteditable="true">
Everything contained <b>**within**</b> this div is editable in browsers
that support <code>`HTML5`</code>. Go on, give it a try: click it and
start typing.
</p>
I want to add the appropriate HTML-tags while typing.
Of course it is no problem doing it by simple regex replacements. But I have problems setting the caret at the right place.
Say, in the example above, I want to emphasize the word "try". While typing the right * first and then the left star *, the text is converted to <i>*try*</i> but I want to preserve the position of the caret (set the the caret after the left *). And of cource vice versa, where the right * is set at last.
I find it more difficult to set the part contained <b>**within**</b> this say underlined. _contained <b>**within**</b> this_ becomes <u>_contained <b>**within**</b> this_</u>. So we are not in the same node.
I can think of complicated solutions, but since I am no expert in these text conversions techniques (range, window selection etc.) I wonder if there is a well known pratice of doing it.
Check out the Fiddle
I have tried to get the caret position as a first step and in the second step i have tried to put the caret back to the position .
$('.session').keyup(function(){
//alert("happening");
var sel = window.getSelection();
var offset=sel.anchorOffset;
// you can change the text in div by uncommeting the line below and replace your text with mathcing regex .
//$('.session').text($('.session').text().replace('/(\*.\*)+/','<i>$1<i>'));
setCaret(offset);
$('.session')
});
function setCaret(offset) {
var el = $('.session');
//alert();
var range = document.createRange();
var sel = window.getSelection();
range.setStart(el[0].childNodes[0],offset);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
el.focus();
}
You need to figure out how to change the text in the div as per your needs. While replacing the text inside the div also maintain the formatting so that the solution works.
Consider following DOM fragment:
<div id="div-1">foo</div>
<div id="div-2">bar</div>
Is it possible to insert HTML string (EDIT: that contains tags to render) between divs without wrapping it in another div (EDIT: or some other tag) created via document.createElement and setting its innerHTML property?
Most browsers support element#insertAdjacentHTML(), which finally became standard in the HTML5 specification. Unfortunately, Firefox 7 and lower don't support it, but I managed to find a workaround that uses ranges to insert the HTML. I've adapted it below to work for your scenario:
var el = document.getElementById("div-2"),
html = "<span>Some HTML <b>here</b></span>";
// Internet Explorer, Opera, Chrome, Firefox 8+ and Safari
if (el.insertAdjacentHTML)
el.insertAdjacentHTML ("beforebegin", html);
else {
var range = document.createRange();
var frag = range.createContextualFragment(html);
el.parentNode.insertBefore(frag, el);
}
Live example: http://jsfiddle.net/AndyE/jARTf/
This does it for straight text, which is how I read your original question (see below for an update for strings that include tags):
var div = document.getElementById('div-2');
var textNode = document.createTextNode('your text');
div.parentNode.insertBefore(textNode, div);
Live example
If you start with:
<div id="div-1">foo</div>div id="div-2">bar</div>
(note that there's no whitespace between them) then the result of the above is exactly what you would get with this HTML:
<div id="div-1">foo</div>your text<div id="div-2">bar</div>
If you really have that whitespace between the divs, you'll already have a text node there and the above will insert another one next to it. For virtually all intents and purposes, that doesn't matter, but if that bothers you, you can append to the existing text node instead if you like:
var text = 'your text';
var div = document.getElementById('div-2');
var prev = div.previousSibling;
if (prev && prev.nodeType == 3) { // 3 == TEXT_NODE
// Prev is a text node, append to it
prev.nodeValue = prev.nodeValue + text;
}
else {
// Prev isn't a text node, insert one
var textNode = document.createTextNode(text);
div.parentNode.insertBefore(textNode, div);
}
Live example
Links to W3C docs: insertBefore, createTextNode
Including HTML tags
In your revised question you've said you want to include tags to be interpreted in doing all this. It's possible, but it's roundabout. First you put the HTML string into an element, then you move the stuff over, like this:
var text, div, tempdiv, node, next, parent;
// The text
text = 'your text <em>with</em> <strong>tags</strong> in it';
// Get the element to insert in front of, and its parent
div = document.getElementById('div-2');
parent = div.parentNode;
// Create a temporary container and render the HTML to it
tempdiv = document.createElement('div');
tempdiv.innerHTML = text;
// Walk through the children of the container, moving them
// to the desired target. Note that we have to be sure to
// grab the node's next sibling *before* we move it, because
// these things are live and when we moev it, well, the next
// sibling will become div-2!
node = tempdiv.firstChild;
next = node.nextSibling;
while (node) {
parent.insertBefore(node, div);
node = next;
next = node ? node.nextSibling : undefined;
}
Live example
But here there be dragons, you have to select the container element as appropriate to the content you're inserting. For instance, we couldn't use a <tr> in your text with the code above because we're inserting it into a div, not a tbody, and so that's invalid and the results are implementation-specific. These sorts of complexities are why we have libraries to help us out. You've asked for a raw DOM answer and that's what the above is, but I really would check out jQuery, Closure, Prototype, YUI, or any of several others. They'll smooth a lot of stuff over for you.
var neuB = document.createElement("b");
var neuBText = document.createTextNode("mit fettem Text ");
neuB.appendChild(neuBText);
document.getElementById("derText").insertBefore(neuB, document.getElementById("derKursiveText"));
You search for: insertBefore
Using jquery it is very simple:
$("#div-1").after("Other tag here!!!");
See: jquery.after
It is obvious that javascript is not a pure javascript solution.