I am trying to insert some HTML and some text in a contenteditable.
However, when I'm in this configuration:
Some text <span>A span tag</span>
If I have my caret at the end of the text and try to insert some text like this:
document.execCommand('insertHTML', false, ' my text');
Instead of getting what I expect:
Some text <span>A span tag</span> my text
I get this:
Some text <span>A span tag my text</span>
How can I force insertHTML to insert at the highest level?
You basically need to set the carretPosition at the end of the selection and then insert your html, I made a small snippet demonstrating this behavior
One you set the range based on a node, you can then choose to selection.collapseToEnd() like so:
currentSelection.collapseToEnd();
The snippet uses the mousedown event to make sure the focus stays on the contenteditable item
document.querySelector('#addContentButton').addEventListener('mousedown', (e) => {
var currentSelection = window.getSelection();
// make sure there is a range available
if (currentSelection.rangeCount > 0) {
// get the container from where it started
var target = currentSelection.getRangeAt(0).startContainer;
var range = document.createRange();
// check if it has any childNodes, if so take the last node otherwise choose the container itself
var targetNode = target.childNodes && target.childNodes.length ? target.childNodes[target.childNodes.length - 1] : target;
// select the node
range.selectNode( targetNode );
// remove all ranges from the window
currentSelection.removeAllRanges();
// add the new range
currentSelection.addRange( range );
// set the position of the carret at the end
currentSelection.collapseToEnd();
// focus the element if posible
target.focus && target.focus();
// insert the html at the end
document.execCommand('insertHTML', false, 'test');
}
e.preventDefault();
});
document.querySelector('#insertCurrentPosition').addEventListener('mousedown', (e) => {
var currentSelection = window.getSelection();
// make sure there is a range available
if (currentSelection.rangeCount > 0) {
// get the container from where it started
var target = currentSelection.getRangeAt(0).startContainer;
// focus the element if posible
target.focus && target.focus();
// insert the html at the end
document.execCommand('insertHTML', false, 'test');
}
e.preventDefault();
});
span {
margin: 0 5px;
}
<div contenteditable="true">This is <span>my span</span><span>and another one</span></div>
<button id="addContentButton">Append at end of editable element</button>
<button id="insertCurrentPosition">Insert at current position</button>
Not sure I 100% understand your question / problem, but inserting text into a span element is as simple as this:
HTML:
<span id="mySpan"></span>
JavaScript:
document.getElementById('mySpan').innerHTML = "Text to put inside span";
'contenteditable' simple returns a true/false indicating whether an object can or cannot be edited.
Hope this helped
Related
I have a content editable field, in which I can enter and html format som text (select text and click a button to add <span class="word"></span> around it).
I use the following function:
function highlightSelection() {
if (window.getSelection) {
let sel = window.getSelection();
if (sel.rangeCount > 0) {
if(sel.anchorNode.parentElement.classList.value) {
let range = sel.getRangeAt(0).cloneRange();
let newParent = document.createElement('span');
newParent.classList.add("word");
range.surroundContents(newParent);
sel.removeAllRanges();
sel.addRange(range);
} else {
console.log("Already in span!");
// end span - start new.
}
}
}
}
But in the case where I have:
Hello my
<span class="word">name is</span>
Benny
AND I select "is" and click my button I need to prevent the html from nesting, so instead of
Hello my
<span class="word">name <span class="word">is</span></span> Benny
I need:
Hello my
<span class="word">name</span>
<span class="word">is</span>
Benny
I try to check the parent class, to see if it is set, but how do I prevent nested html - close span tag at caret start position, add and at the end of caret position add so the html will not nest?
It should also take into account if there are elements after selection which are included in the span:
So:
Hello my
<span class="word">name is Benny</span>
selecting IS again and clicking my button gives:
Hello my
<span class="word">name</span>
<span class="word">is</span>
<span class="word">Benny</span>
Any help is appreciated!
Thanks in advance.
One way would be to do this in multiple pass.
First you wrap your content, almost blindly like you are currently doing.
Then, you check if in this content there were some .word content. If so, you extract its content inside the new wrapper you just created.
Then, you check if your new wrapper is itself in a .word container.
If so, you get the content that was before the selection and wrap it in its own new wrapper. You do the same with the content after the selection.
At this stage we may have three .word containers inside the initial one. We thus have to extract the content of the initial one, and remove it. Our three wrappers are now independent.
function highlightSelection() {
if (window.getSelection) {
const sel = window.getSelection();
if (sel.rangeCount > 0) {
const range = sel.getRangeAt(0).cloneRange();
if (range.collapsed) { // nothing to do
return;
}
// first pass, wrap (almost) carelessly
wrapRangeInWordSpan(range);
// second pass, find the nested .word
// and wrap the content before and after it in their own spans
const inner = document.querySelector(".word .word");
if (!inner) {
// there is a case I couldn't identify correctly
// when selecting two .word start to end, where the empty spans stick around
// we remove them here
range.startContainer.parentNode.querySelectorAll(".word:empty")
.forEach((node) => node.remove());
return;
}
const parent = inner.closest(".word:not(:scope)");
const extractingRange = document.createRange();
// wrap the content before
extractingRange.selectNode(parent);
extractingRange.setEndBefore(inner);
wrapRangeInWordSpan(extractingRange);
// wrap the content after
extractingRange.selectNode(parent);
extractingRange.setStartAfter(inner);
wrapRangeInWordSpan(extractingRange);
// finally, extract all the contents from parent
// to its own parent and remove it, now that it's empty
moveContentBefore(parent)
}
}
}
document.querySelector("button").onclick = highlightSelection;
function wrapRangeInWordSpan(range) {
if (
!range.toString().length && // empty text content
!range.cloneContents().querySelector("img") // and not an <img>
) {
return; // empty range, do nothing (collapsed may not work)
}
const content = range.extractContents();
const newParent = document.createElement('span');
newParent.classList.add("word");
newParent.appendChild(content);
range.insertNode(newParent);
// if our content was wrapping .word spans,
// move their content in the new parent
// and remove them now that they're empty
newParent.querySelectorAll(".word").forEach(moveContentBefore);
}
function moveContentBefore(parent) {
const iterator = document.createNodeIterator(parent);
let currentNode;
// walk through all nodes
while ((currentNode = iterator.nextNode())) {
// move them to the grand-parent
parent.before(currentNode);
}
// remove the now empty parent
parent.remove();
}
.word {
display: inline-block;
border: 1px solid red;
}
[contenteditable] {
white-space: pre; /* leading spaces will get ignored once highlighted */
}
<button>highlight</button>
<div contenteditable
>Hello my <span class="word">name is</span> Benny</div>
But beware, this is just a rough proof of concept, I didn't do any heavy testings and there may very well be odd cases where it will just fail (content-editable is a nightmare).
Also, this doesn't handle cases where one would copy-paste or drag & drop HTML content.
I modified your code to a working approach:
document.getElementById('btn').addEventListener('click', () => {
if (window.getSelection) {
var sel = window.getSelection(),
range = sel.getRangeAt(0).cloneRange();
sel.anchorNode.parentElement.className != 'word' ?
addSpan(range) : console.log('Already tagged');
}
});
const addSpan = (range) => {
var newParent = document.createElement('span');
newParent.classList.add("word");
range.surroundContents(newParent);
}
.word{color:orange}button{margin:10px 0}
<div id="text">lorem ipsum Benny</div>
<button id="btn">Add span tag to selection</button>
I want a feature where clicking the Edit button will make the text content inside span tags editable. I was able to do that but couldn't figure out how to get the blinking cursor at the end of text.
Below is the simplified version of my code.
document.querySelector('button').addEventListener('click', function(){
const span=document.querySelector('span');
span.setAttribute('contentEditable', 'true');
span.focus();
let val=span.innerText;
span.innerText='';
span.innerText=val
})
<span>this is the span tag</span> <button>Edit</button>
Create a new range object set the node you wish to set the range selection to addRange using getSelection... See notes in code snippit
https://developer.mozilla.org/en-US/docs/Web/API/Document/createRange
https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection
https://developer.mozilla.org/en-US/docs/Web/API/Range/selectNodeContents
https://developer.mozilla.org/en-US/docs/Web/API/Selection/removeAllRanges
document.querySelector('button').addEventListener('click', function() {
const span = document.querySelector('span');
span.setAttribute('contentEditable', 'true');
span.focus();
let val = span.innerHtml;
span.innerHtml = '';
span.innerHtml = val;
//set a new range object
let caret = document.createRange();
//return the text selected or that will be appended to eventually
let sel = window.getSelection();
//get the node you wish to set range to
caret.selectNodeContents(span);
//set collapse to null or false to set at end of node
caret.collapse(null);
//make sure all ranges are removed from the selection object
sel.removeAllRanges();
//set all ranges to null and append it to the new range object
sel.addRange(caret);
})
<span>this is the span tag</span> <button>Edit</button>
*This post may also be helpful to you...
How to set caret(cursor) position in contenteditable element (div)?
I'm looking into text selection and ranges in JavaScript.
I need to get any nodes that surround the selected text exactly, for example:
<div>this is <span>some simple</span> text</div>
When the user selects the words 'some simple' i need to know that it sits entirely within the node .
Yet if they select just 'some' then this is not entirely within the node as the word 'simple' is NOT selected.
The end requirement is to be able to amend the class on the node only if the whole text within the node is selected.
jquery is also viable. thanks
To add some more context to this, when a user selects some text we add some sytling to it, let's say 'bold'. the user can edit the text in the parent div as often as they wish so each edit could add a new span enclosing the selected text. We could end up with something like this:
<div><span class="text-bold">Hi</span>, <span class="text-red">this <span class="text-italic">is</span></span> a sample text item</div>
So the spans can come and go dependant on what the user wants.
You are looking to get the DOM each time. So you can set id to your HTML Element Objects and get the value from them. For example:
<span id="f_span">some simple</span>
<script>
var x = document.getElementById("f_span");
</script>
Then you can check if x value equals with the value that user had selected.
You can use setInterval to get the selected text every x second. With the joined function you can get the selected text.
For the selection of user :
function getSelected() {
if(window.getSelection) { return window.getSelection(); }
else if(document.getSelection) { return document.getSelection(); }
else {
var selection = document.selection && document.selection.createRange();
if(selection.text) { return selection.text; }
return false;
}
return false;
}
It return an object that give you the offset of the selection. If result.anchorOffset = 0 and result.focusOffset = result.anchorNode.length (if it start at the begining of the node and it have the length of the whole node), then the user selected all your node.
Thanks for your replies, it allowed me to cobble together my solution:
function applyTextFormatClass(className) {
var selection = getSelected();
var parent = selection.getRangeAt(0).commonAncestorContainer; //see comment and link below
if (parent.nodeType !== 1) {
parent = parent.parentNode; //we want the parent node
}
var tagText = parent.innerText;
var selectText = selection.toString();
if (tagText.length !== selectText.length) {
addNodeAroundSelectedText(selection, className); //create new node
} else {
addClass(parent, className); //add class to existing node
}
}
commonAncestorContainer: https://developer.mozilla.org/en-US/docs/Web/API/Range/commonAncestorContainer
Is it possible to select specific text inside a div using the code. I have a div of text, and I need to iterate through each word selecting it individually, then deslecting and onto the next word.
I'm not talking about simulating a select by changing the background css of the words requiring highlighting, but actually selecting it so the outcome is the same as if the user used the mouse to select it.
I know it's possible inside a text area input, but is it possible on a Div?
------------------------UPDATE------------------------------------------
Ok this is where I'm at with it after having another look at it today. I can select all the text in a span, but not specifically a range of words within that span. The closest I have come ( code shown below... ) is selecting the range manually, then removing the selection, then reapplying it.
<div contentEditable = 'true' id="theDiv">
A selection of words but only from here to here to be selected
</div>
<script type="text/javascript">
setInterval( function() {
var currSelection = window.getSelection();
var storedSelections = [];
if ( currSelection ) {
for (var i = 0; i < currSelection.rangeCount; i++) {
storedSelections.push (currSelection.getRangeAt (i));
}
currSelection.removeAllRanges ();
}
if ( storedSelections.length != 0 ) {
currSelection.addRange( storedSelections[0] )
}
}, 1000 );
</script>
The stored selection range object has a startOffset and endOffset property. My question is how do I set this alongside the initial selection via the code ( not via a mouse select ) ?
Please read this article, it's difficult to summarise, so better read it:
http://www.quirksmode.org/dom/range_intro.html
This answer here can be useful too:
Can you set and/or change the user’s text selection in JavaScript?
Turns out it's fairly straightforward...
<div id = "theDiv">adsf asdf asdf asdf</div>
<script type="text/javascript">
var theDiv = document.getElementById('theDiv')
theDivFirstChild = theDiv.firstChild;
var range = document.createRange();
range.setStart( theDivFirstChild, 2 );
range.setEnd( theDivFirstChild, 8);
window.getSelection().addRange(range);
</script>
I am working on a WYSIWYG editor in which I want to add <p>'s when hit Enter / Return key and then the user will write to this new <p>.
Right now I am having issue setting the caret to this new <p>.
$('#content').on('keypress', function(e){
if(e.which === 13){
console.log('enter pressed');
e.preventDefault();
var range = window.getSelection().getRangeAt(0);
var element = document.createElement('p');
// element.textContent = 'lorem'; // gets added in the right position
range.insertNode(element);
// range.setStart(element); // doesn't work
}
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p id='content' contentEditable=true>test</p>
I need to get this working in Chrome for now.
How do I fix this?
A simple solution if you don't absolutely need the top contenteditable element to be a p element, is to add a contenteditable div as parent of your p element. Enter will automatically add p elements.
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id='content' contentEditable=true><p>test</p></div>