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>
Related
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.
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 am wondering if it is possible to remove a tag but leave the content in tact? For example, is it possible to remove the SPAN tag but leave SPAN's content there?
<p>The weather is sure <span>sunny</span> today</p> //original
<p>The weather is sure sunny today</p> //turn it into this
I have tried using this method of using replaceWith(), but it it turned the HTML into
<p>
"The weather is sure "
"sunny"
" today"
</p>
EDIT : After testing all of your answers, I realized that my code is at fault. The reason why I keep getting three split text nodes is due to the insertion of the SPAN tag. I'll create another question to try to fix my problem.
<p>The weather is sure <span>sunny</span> today</p>;
var span=document.getElementsByTagName('span')[0]; // get the span
var pa=span.parentNode;
while(span.firstChild) pa.insertBefore(span.firstChild, span);
pa.removeChild(span);
jQuery has easier ways:
var spans = $('span');
spans.contents().unwrap();
With different selector methods, it is possible to remove deeply nested spans or just direct children spans of an element.
There are several ways to do it. Jquery is the most easy way:
//grab and store inner span html
var content = $('p span').html;
//"Re"set inner p html
$('p').html(content);
Javascript can do the same using element.replace. (I don't remember the regex to do the replace in one stroke, but this is the easy way)
paragraphElement.replace("<span>", "");
paragraphElement.replace("</span>", "");
It's just three text nodes instead of one. It doesn't make a visible difference does it?
If it's a problem, use the DOM normalize method to combine them:
$(...)[0].normalize();
$(function(){
var newLbl=$("p").clone().find("span").remove().end().html();
alert(newLbl);
});
Example : http://jsfiddle.net/7gWdM/6/
If you're not looking for a jQuery solution, here something that's a little more lightweight and focused on your scenario.
I created a function called getText() and I used it recursively. In short, you can get the child nodes of your p element and retrieve all the text nodes within that p node.
Just about everything in the DOM is a node of some sort. Looking up at the following links I found that text nodes have a numerical nodeType value of 3, and when you identify where your text nodes are, you get their nodeValueand return it to be concatenated to the entire, non-text-node-free value.
https://developer.mozilla.org/en/nodeType
https://developer.mozilla.org/En/DOM/Node.nodeValue
var para = document.getElementById('p1') // get your paragraphe
var texttext = getText(para); // pass the paragraph to the function
para.innerHTML = texttext // set the paragraph with the new text
function getText(pNode) {
if (pNode.nodeType == 3) return pNode.nodeValue;
var pNodes = pNode.childNodes // get the child nodes of the passed element
var nLen = pNodes.length // count how many there are
var text = "";
for (var idx=0; idx < nLen; idx++) { // loop through the child nodes
if (pNodes[idx].nodeType != 3 ) { // if the child not isn't a text node
text += getText(pNodes[idx]); // pass it to the function again and
// concatenate it's value to your text string
} else {
text += pNodes[idx].nodeValue // otherwise concatenate the value of the text
// to the entire text
}
}
return text
}
I haven't tested this for all scenarios, but it will do for what you're doing at the moment. It's a little more complex than a replace string since you're looking for the text node and not hardcoding to remove specific tags.
Good Luck.
If someone is still looking for that, the complete solution that has worked for me is:
Assuming we have:
<p>hello this is the <span class="highlight">text to unwrap</span></p>
the js is:
// get the parent
var parentElem = $(".highlight").parent();
// replacing with the same contents
$(".highlight").replaceWith(
function() {
return $(this).contents();
}
);
// normalize parent to strip extra text nodes
parentElem.each(function(element,index){
$(this)[0].normalize();
});
If it’s the only child span inside the parent, you could do something like this:
HTML:
<p class="parent">The weather is sure <span>sunny</span> today</p>;
JavaScript:
parent = document.querySelector('.parent');
parent.innerHTML = parent.innerText;
So just replace the HTML of the element with its text.
You can remove the span element and keep the HTML content or internal text intact. With jQuery’s unwrap() method.
<html>
<head>
<script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
<script type="text/javascript">
$(document).ready(function(){
$("button").click(function(){
$("p").find("span").contents().unwrap();
});
});
</script>
</head>
<body>
<p>The weather is sure <span style="background-color:blue">sunny</span> today</p>
<button type="button">Remove span</button>
</body>
</html>
You can see an example here: How to remove a tag without deleting its content with jQuery
I'm having some serious trouble getting my code to work in IE9, works fine in Chrome & Firefox but I throws some errors. Here are my 2 functions:
function insertHTML(content){
var body=document.getElementsByTagName('body');
body[0].appendChild(createElement(content));
}
function createElement(string){
var container=document.createElement('div');
container.innerHTML=string;
var element=container.firstChild.cloneNode(true);
return element;
}
I've tried severel methods for this and none seem to work, I'll explain exactly what I need to do...
...I need to create a new element from an html string, the string is sent back from an ajax call so my script will have almost no idea what it contains until it gets it.
I did try using element.innerHTML but this is no good, because if i have one html element (form) on the screen and the user enters data into it, and then when another element is inserted it will wipe all the user-entered data from the first form. I was doing element.innerHTML+=newData;
So basically, I need 2 things:
1) A way to create a new element from an html string.
2) A way to append the element to the document body.
It all needs to work cross-browser and I'm not allowed to use jQuery, also the new element cannot be contained in a div parent item, it has to have the body as its parent.
Thanks very much for your help,
Richard
innerHTML is read write and will destroy anything inside your div. use with extreme care
function insertHTML( htmlString ){
var bodEle = document.getElementsByTagName('body');
var divEle = createElement("div");
divEle.innerHTML = htmlString;
bodEle.appendChild(divEle);
}
So basically, I need 2 things:
A way to create a new element from an html string.
A way to append the element to the document body.
It all needs to work cross-browser and I'm not allowed to use jQuery, also the new element cannot be contained in a div parent item, it has to have the body as its parent.
The following was tested in IE8
<!DOCTYPE html>
<html>
<body>
<script>
var divBefore = document.createElement('div');
var divAfter = document.createElement('div');
var htmlBefore = '<span><span style="font-weight: bold">This bold text</span> was added before</span>';
var htmlAfter = '<span><span style="font-weight: bold">This bold text</span> was added after</span>';
divBefore.innerHTML = htmlBefore;
divAfter.innerHTML = htmlAfter;
document.body.appendChild(divBefore);
setTimeout(function() {
document.body.appendChild(divAfter);
}, 0);
</script>
<div>This content was here first</div>
</body>
</html>
Renders
This bold text was added before
This content was here first
This bold text was added after
https://www.browserstack.com/screenshots/7e166dc72b636d3dffdd3739a19ff8956e9cea96
In the above example, if you don't need to be able to prepend to the body (i.e. insert content before what already exists), then simply place the script tag after the original content and don't use setTimeout.
<!DOCTYPE html>
<html>
<body>
<div>This content was here first</div>
<script>
var divAfter = document.createElement('div');
var htmlAfter = '<span><span style="font-weight: bold">This bold text</span> was added after</span>';
divAfter.innerHTML = htmlAfter;
document.body.appendChild(divAfter);
</script>
</body>
</html>
Well,
I need to replace a word, in a div contentEdible property on, by the same word but formatted...
Like this:
<div> My balls are big </div>
To this (replace the word: balls):
<div> My <font style="color:blue;">balls</font> are big </div>
In a contentEditable this happens dinamically, while the user type the text the replacements happens. I think that a simple event onkeydown, onkeyup, or onkey press, can solve this part.
But, the trouble is with the caret, that after all that i tryed, it stay before the word replaced, when should be stay after. I tryed write some js code, tryed find some jquery scripts, but all them failed in this situation...
Any one has some ideia, or trick ?
I think:
--> Record the length of the word unformatted.
--> Delete this word
--> Put new word formatted.
--> Walk with the caret, to position based this formatted word length.
--> Is it?
PS: I have to considerate a word in any place of this div.
I don't know how to write this code that do what i say above.
Correct me, if i'm wrong.
Since yet, thanks!
Edit[1]: I want that this works on Mozilla Firefox, specificlly;
I only have IE6/7 on this machine, but maybe you can apply the concept to other browser versions of Ranges (or maybe this is cross-browser?).
Basically we store the cursor position, make our search/replacement, then put the cursor back where it was:
html:
<div id="content" contentEditable="true" onkeyup="highlight(this)">This is some area to type.</div>
and the script:
function highlight(elem) {
// store cursor position
var cursorPos=document.selection.createRange().duplicate();
var clickx = cursorPos.getBoundingClientRect().left;
var clicky = cursorPos.getBoundingClientRect().top;
// copy contents of div
var content = elem.innerHTML;
var replaceStart = '<font style="color:blue">';
var replaceEnd = '</font>';
// only replace/move cursor if any matches
// note the spacebands - this prevents duplicates
if(content.match(/ test /)) {
elem.innerHTML = content.replace(/ test /g,' '+replaceStart+'test'+replaceEnd+' ');
// reset cursor and focus
cursorPos = document.body.createTextRange();
cursorPos.moveToPoint(clickx, clicky);
cursorPos.select();
}
}