Insert an HTML element in a contentEditable element - javascript

I have a contentEditable div where I want to insert HTML tags (a simple span element).
Is there a cross browser solution that allows me to insert those tags over my div selection or cursor position. If something else is selected on the page (not in the div), I want to append the tag to the end of the div.
Thanks

Here is a kickstart
// get the selection range (or cursor position)
var range = window.getSelection().getRangeAt(0);
// create a span
var newElement = document.createElement('span');
newElement.id = 'myId';
newElement.innerHTML = 'Hello World!';
// if the range is in #myDiv ;)
if(range.startContainer.parentNode.id==='myDiv') {
// delete whatever is on the range
range.deleteContents();
// place your span
range.insertNode(newElement);
}
I don't have IE but works fine on firefox, chrome and safari. Maybe you want to play with range.startContainer to proceed only if the selection is made on the contentEditable div.
EDIT: According to quirksmode range intro you have to change the window.getSelection() part to be IE compatible.
var userSelection;
if (window.getSelection) {
userSelection = window.getSelection();
}
else if (document.selection) { // should come last; Opera!
userSelection = document.selection.createRange();
}

The following will do this in all major browsers (including IE 6). It will also handle cases where the end of the selection is outside your <div> and cases where the selection is contained within a child (or more deeply nested) element inside the <div>.
2019 addendum: The second branch of insertNodeOverSelection is for IE <= 8 only and could be removed now.
function isOrContainsNode(ancestor, descendant) {
var node = descendant;
while (node) {
if (node === ancestor) return true;
node = node.parentNode;
}
return false;
}
function insertNodeOverSelection(node, containerNode) {
var sel, range, html;
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
if (isOrContainsNode(containerNode, range.commonAncestorContainer)) {
range.deleteContents();
range.insertNode(node);
} else {
containerNode.appendChild(node);
}
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
if (isOrContainsNode(containerNode, range.parentElement())) {
html = (node.nodeType == 3) ? node.data : node.outerHTML;
range.pasteHTML(html);
} else {
containerNode.appendChild(node);
}
}
}
<input type="button" onmousedown="insertNodeOverSelection(document.createTextNode('[NODE]'), document.getElementById('test'));" value="insert">
<div contenteditable="true">
<div id="test" style="background-color: lightgoldenrodyellow">
This is the editable element where the insertion will happen. Select something or place the cursor in here, then hit the button above
</div>
<div>
No insertion will happen here
</div>
</div>

Related

Getting wrong caret position in javascript

I am trying to insert token after selecting from context menu at the caret position inside a contenteditable div. I have managed to do that if the caret position is in a single line.
In my case the range offset value will set to 0 whenever other HTML tags appear i.e when the line changes. I am using these two functions for getting the range which I've found somewhere in stackoverflow.
Any help is appreciated!
function saveSelection() {
var range = null;
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
return sel.getRangeAt(0);
}
} else if (document.selection && document.selection.createRange) {
return document.selection.createRange();
}
return null;
}
I think the problem is not with the no of line of content in your element which is contenteditable. Because you are using context menu with contenteditable div as you have mentioned. And when ever you click on the context menu it will get range value of that context menu.
So you should store the range value on certain variable before clicking on certain menu.
Since your code is incomplete I cannot relate any example with your code.
Here is one example hope this will help you:
function pasteHtmlAtCaret(html) {
var sel, range;
if (window.getSelection) {
// IE9 and non-IE
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
// Range.createContextualFragment() would be useful here but is
// non-standard and not supported in all browsers (IE9, for one)
var el = document.createElement("div");
el.innerHTML = html;
var frag = document.createDocumentFragment(), node, lastNode;
while ( (node = el.firstChild) ) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
// Preserve the selection
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
} else if (document.selection && document.selection.type != "Control") {
// IE < 9
document.selection.createRange().pasteHTML(html);
}
}
<input type="button" value="Paste HTML" onclick="document.getElementById('test').focus(); pasteHtmlAtCaret('<b>INSERTED</b>'); ">
<div id="test" contenteditable="true">
Here is some nice text
</div>
jsfiddle link for the above mentioned code.

Add element w/ text node after user selection

I have the following function that creates a new text node at the end of a document when called, using the value of text within a text field:
function addAfter () {
var elem = document.getElementById('textfield').value;
var h = document.createElement('span');
var t = document.createTextNode(elem);
h.appendChild(t);
document.body.appendChild(h);
}
What I would like it to do would be to add the text immediately after user-selected text (like when you click and drag to select text). What needs to replace the document.body.appendChild(h); for this to work?
Try This JS Fiddle
This should do it on anything but IE edge(I believe)
function getSelectionText() {
var text = "";
if (window.getSelection) {
text = window.getSelection().toString();
} else if (document.selection && document.selection.type != "Control") {
text = document.selection.createRange().text;
}
return text;
}
document.body.addEventListener('mouseup', function() {
(window.getSelection()) ? addText(window.getSelection().toString(), window.getSelection().baseNode.parentNode) : addText(document.selection.toString(), document.selection.baseNode.parentNode);
});
function addText(text, parent) {
parent.appendChild(document.createTextNode(text));
}
The way that this works is it uses the mouseUp event to determine whether or not text MAY have been selected. Selected text is stored underneath window.getSelected() OR document.selected() - it then passes that value as well as the parent of the selected text to the addText function. It uses the document appendChild and createTextNode methods to append the captured text to the DOM.
In Previous Versions of IE they used document.select(), but in Edge they switched over to getSelection(like everyone else) BUT they didn't implement the same value that's returned when you get text, ergo you can't really grab the parentNode and easily append to that node.
So, in short, this will give you what you're looking for, but it's not cross-browser and there doesn't appear to be a way to do that easily.
I manipulated a solution that I found on another feed.
function addAfter (isBefore) {
var sel, range, node;
var changeText = document.getElementById('textfield').value;
var textAndCode = "<span class=\"correction\"> " + changeText + " </span>";
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = window.getSelection().getRangeAt(0);
range.collapse(isBefore);
// Range.createContextualFragment() would be useful here but was
// until recently non-standard and not supported in all browsers
// (IE9, for one)
var el = document.createElement("div");
el.innerHTML = textAndCode;
var frag = document.createDocumentFragment(), node, lastNode;
while ( (node = el.firstChild) ) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
range.collapse(isBefore);
range.pasteHTML(textAndCode);
}
}
You just have to pass in the value for isBefore as false if you want it to appear after.

Javascript: how to un-surroundContents range

This function uses the range object to return user selection and wrap it in bold tags. is there a method that allows me to remove the tags? As in <b>text<b> = text.
I actually need a toggle function that wraps the selection in tags & un-wraps it if already contains tags. Similar to what text editors do when you toggle the bold button.
if "text" then "<b>text</b>"
else "<b>text</b>" then "text"
...
function makeBold() {
//create variable from selection
var selection = window.getSelection();
if (selection.rangeCount) {
var range = selection.getRangeAt(0).cloneRange();
var newNode = document.createElement("b");
//wrap selection in tags
range.surroundContents(newNode);
//return the user selection
selection.removeAllRanges();
selection.addRange(range);
}
}
I didn't mention this in your previous question about this because it sounded like you wanted a generic means of surrounding a range within an element, but for this particular application (i.e. bolding/unbolding text), and assuming you don't mind a little cross-browser variation in the precise tags used (<strong> versus <bold> versus possibly <span style="font-weight: bold">), you're best off using document.execCommand(), which will toggle boldness:
function toggleBold() {
document.execCommand("bold", false, null);
}
This will work in all browsers when the selected content is editable, and even when it's not editable in IE. If you need it to work on non-editable content in other browsers, you'll need to temporarily make the document editable:
function toggleBold() {
var range, sel;
if (window.getSelection) {
// Non-IE case
sel = window.getSelection();
if (sel.getRangeAt) {
range = sel.getRangeAt(0);
}
document.designMode = "on";
if (range) {
sel.removeAllRanges();
sel.addRange(range);
}
document.execCommand("bold", false, null);
document.designMode = "off";
} else if (document.selection && document.selection.createRange &&
document.selection.type != "None") {
// IE case
range = document.selection.createRange();
range.execCommand("bold", false, null);
}
}
wrapping & un-wrapping the text selection using the same button click event handler:
boldBtn.addEventListener('click', () => {
let selection = document.getSelection();
const isAllowedContainer = selection.baseNode.parentElement?.closest?.('#editor');
// do not continue if no text selection or this is not the desired element container
if( selection.rangeCount < 1 || !isAllowedContainer ) return;
const range = selection.getRangeAt(0);
const selParent = selection.anchorNode?.parentElement;
const selectedElem = selParent?.nodeType == 1 && selParent?.children.length < 2 && selParent;
// un-wrap
if(selectedElem.tagName === 'B') {
selectedElem.replaceWith(...selectedElem.childNodes)
}
// wrap with <b>
else {
range.surroundContents(document.createElement("b"));
selection.removeAllRanges();
selection.addRange(range);
range.collapse(); // removes selected and places caret at the end of the injected node
}
})
<button id="boldBtn">B</button><br/><br/>
<div id='editor' contenteditable="true">Select some text and click the button</div>

Insert HTML after a selection

I want to be able to double-click to select some text in a div, which then triggers a JavaScript function that inserts some HTML after the selected text, much like the 'edit/reply' feature in Google Wave.
I have already established how to trigger a function on double-clicking, it is locating the selection and subsequently inserting HTML after it that is the problem.
The following function will insert a DOM node (element or text node) at the end of the selection in all major browsers (note that Firefox now allows multiple selections by default; the following uses only the first selection):
function insertNodeAfterSelection(node) {
var sel, range, html;
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
range.collapse(false);
range.insertNode(node);
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
range.collapse(false);
html = (node.nodeType == 3) ? node.data : node.outerHTML;
range.pasteHTML(html);
}
}
If you would rather insert an HTML string:
function insertHtmlAfterSelection(html) {
var sel, range, node;
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = window.getSelection().getRangeAt(0);
range.collapse(false);
// Range.createContextualFragment() would be useful here but is
// non-standard and not supported in all browsers (IE9, for one)
var el = document.createElement("div");
el.innerHTML = html;
var frag = document.createDocumentFragment(), node, lastNode;
while ( (node = el.firstChild) ) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
range.collapse(false);
range.pasteHTML(html);
}
}
Update 18 January 2012
Finally, here's a version that inserts HTML and preserves the selection (i.e. expands the selection to include the originally selected content plus the inserted content).
Live demo: http://jsfiddle.net/timdown/JPb75/1/
Code:
function insertHtmlAfterSelection(html) {
var sel, range, expandedSelRange, node;
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = window.getSelection().getRangeAt(0);
expandedSelRange = range.cloneRange();
range.collapse(false);
// Range.createContextualFragment() would be useful here but is
// non-standard and not supported in all browsers (IE9, for one)
var el = document.createElement("div");
el.innerHTML = html;
var frag = document.createDocumentFragment(), node, lastNode;
while ( (node = el.firstChild) ) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
// Preserve the selection
if (lastNode) {
expandedSelRange.setEndAfter(lastNode);
sel.removeAllRanges();
sel.addRange(expandedSelRange);
}
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
expandedSelRange = range.duplicate();
range.collapse(false);
range.pasteHTML(html);
expandedSelRange.setEndPoint("EndToEnd", range);
expandedSelRange.select();
}
}
window.getSelection() will get you the selected text (Documentation).
You can use the jQuery.after() (Documentation) to insert new html after an element.
On your selected text function call you will have to loop through all your DOM elements checking their x&y positions to the current cursor position - get the closest element and insert after or use the function .elementFromPoint(x,y) (Documentation)
This is the best way I can currently think of. It's not perfect, but it is somewhere to start.
I don't know if it is possible, but I though of a partial solution.
On your double click event you can get the event object and access the property target.
Having the target element, you can get the selected text.
Now get the target element html and find the index of the selected text and it's length, inject your html on the index + length position.
I think you already saw the problem, it could match the text in multiple places. To solve it you might have to find the position of the cursor and compare to the element, I don't know how to do that...
Hope I could help a bit.

How do I find out the DOM node at cursor in a browser's editable content window using Javascript?

I am looking for a solution that works cross browser i.e. IE, Firefox and Safari.
By "editable content window" I'm going to assume you mean an element with contenteditable turned on or a document with designMode turned on.
There are also two cases to consider: the case when the user has made a selection and the case where there is just a caret. The code below will work in both cases, and will give you the innermost element that completely contains the selection. If the selection is completely contained within a text node it's slightly complicated to get that text node in IE (trivial in other browsers), so I haven't provided that code here. If you need it, I can dig it out.
function getSelectionContainerElement() {
var range, sel, container;
if (document.selection && document.selection.createRange) {
// IE case
range = document.selection.createRange();
return range.parentElement();
} else if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt) {
if (sel.rangeCount > 0) {
range = sel.getRangeAt(0);
}
} else {
// Old WebKit selection object has no getRangeAt, so
// create a range from other selection properties
range = document.createRange();
range.setStart(sel.anchorNode, sel.anchorOffset);
range.setEnd(sel.focusNode, sel.focusOffset);
// Handle the case when the selection was selected backwards (from the end to the start in the document)
if (range.collapsed !== sel.isCollapsed) {
range.setStart(sel.focusNode, sel.focusOffset);
range.setEnd(sel.anchorNode, sel.anchorOffset);
}
}
if (range) {
container = range.commonAncestorContainer;
// Check if the container is a text node and return its parent if so
return container.nodeType === 3 ? container.parentNode : container;
}
}
}
You can also use the Rangy Library:
elementAtCursor = rangy.getSelection().anchorNode.parentNode

Categories

Resources