So I am trying to create an RTE environment.
I have a content editable div and I would like to allow the user to select text and then press a button which would wrap BBCode around it.
I have tried creating the following function, However, the selected text is just replaced.
It doesn't seem to be storing the proper valu ein selectedText
function wrap(tag)
{
var sel, range;
if (window.getSelection)
{
sel = window.getSelection();
if (sel.rangeCount)
{
range = sel.getRangeAt(0);
var selectedText = range;
range.deleteContents();
range.insertNode(document.createTextNode('['+tag+']'+selectedText+'[/'+tag+']'));
}
}
else if (document.selection && document.selection.createRange)
{
range = document.selection.createRange();
selectedText = document.selection.createRange().text;
console.log(text);
range.text = '['+tag+']'+text+'[/'+tag+']';
}
}
</script>
JQuery is acceptable but I'd prefer regular Javascript.
Change selectedText = range; to selectedText = range.toString();. The range is an object and when you do deleteContents it wipes out the data and so it doesn't wrape.
DEMO
JS:
function wrap(tag) {
var sel, range;
var selectedText;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount) {
range = sel.getRangeAt(0);
selectedText = range.toString();
range.deleteContents();
range.insertNode(document.createTextNode('[' + tag + ']' + selectedText + '[/' + tag + ']'));
}
}
else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
selectedText = document.selection.createRange().text + "";
range.text = '[' + tag + ']' + selectedText + '[/' + tag + ']';
}
}
You need
var selectedText = range.toString();
rather than
var selectedText = range;
... because the range will contain no text after its deleteContents() method has been called.
One other note: console.log(text); will throw in the IE branch if you run it in a version of IE without a console or with console disabled.
The other answer that was here does work, but only if there is no images in the selection. I've simplified it and made it work with images in the selection as well:
https://jsfiddle.net/skeets23/go1nxf06/
function wrap(tag) {
sel = window.getSelection();
if (sel.rangeCount) {
range = sel.getRangeAt(0);
range.insertNode(document.createTextNode("["+tag+"]"));
range.collapse();
range.insertNode(document.createTextNode("[/"+tag+"]"));
}
}
Since I'm working on an internal project, I didn't bother making it compatible with all browsers like in the other answer, but it works in the current versions of firefox and chrome. Does not work in IE.
Related
I am having a few issues with my code regarding caret positioning, content editable div and HTML tags in it.
What I am trying to achieve
I'd like to have a content editable div, which allows for line breaks and multiple HTML tags inserted by typing some sort of shortcut - double left bracket '{{' in my case.
What I have achieved so far
The div allows for a single HTML tag and only works in a single line of text.
The issues
1) When I break the line with the return key, the {{ no longer triggers the tag to show up. I assume that you have to somehow make the script to take line breaks (nodes?) into account when creating the range.
2) If you already have one HTML tag visible, you can't insert another one. Instead, you get the following error in browser's console.
Uncaught DOMException: Failed to execute 'setStart' on 'Range': The offset 56 is larger than the node's length (33).
I noticed that range offset goes to 0 (or starts with the end of HTML tag) which is probably at the culprit of the issue here.
Below is the code I have so far...
Everything is triggered on either keyup or mouseclick.
var tw_template_trigger = '{{';
var tw_template_tag = '<span class="tw-template-tag" contenteditable="false"><i class="tw-icon tw-icon-close"></i>Pick a tag</span>';
$('.tw-post-template-content').on( 'keyup mouseup', function() {
// Basically check if someone typed {{
// if yes, attempt to delete those two characters
// then paste tag HTML in that position
if( checkIfTagIsTriggered( this ) && deleteTagTrigger( this ) ) {
pasteTagAtCaret();
}
});
function pasteTagAtCaret(selectPastedContent) {
// Then add the tag
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
// only relatively recently standardized and is not supported in
// some browsers (IE9, for one)
var el = document.createElement("div");
el.innerHTML = tw_template_tag;
var frag = document.createDocumentFragment(), node, lastNode;
while ( (node = el.firstChild) ) {
lastNode = frag.appendChild(node);
}
var firstNode = frag.firstChild;
range.insertNode(frag);
// Preserve the selection
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
} else if ( (sel = document.selection) && sel.type != "Control") {
// IE < 9
var originalRange = sel.createRange();
originalRange.collapse(true);
sel.createRange().pasteHTML( tw_template_tag );
}
}
function checkIfTagIsTriggered(containerEl) {
var precedingChar = "", sel, range, precedingRange;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount > 0) {
range = sel.getRangeAt(0).cloneRange();
range.collapse(true);
range.setStart(containerEl, 0);
precedingChar = range.toString().slice(-2);
}
} else if ( (sel = document.selection) && sel.type != "Control") {
range = sel.createRange();
precedingRange = range.duplicate();
precedingRange.moveToElementText(containerEl);
precedingRange.setEndPoint("EndToStart", range);
precedingChar = precedingRange.text.slice(-2);
}
if( tw_template_trigger == precedingChar )
return true;
return false;
}
function deleteTagTrigger(containerEl) {
var preceding = "",
sel,
range,
precedingRange;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount > 0) {
range = sel.getRangeAt(0).cloneRange();
range.collapse(true);
range.setStart(containerEl, 0);
preceding = range.toString();
}
} else if ((sel = document.selection) && sel.type != "Control") {
range = sel.createRange();
precedingRange = range.duplicate();
precedingRange.moveToElementText(containerEl);
precedingRange.setEndPoint("EndToStart", range);
preceding = precedingRange.text;
}
// First Remove {{
var words = range.toString().trim().split(' '),
lastWord = words[words.length - 1];
if (lastWord && lastWord == tw_template_trigger ) {
/* Find word start and end */
var wordStart = range.toString().lastIndexOf(lastWord);
var wordEnd = wordStart + lastWord.length;
range.setStart(containerEl.firstChild, wordStart);
range.setEnd(containerEl.firstChild, wordEnd);
range.deleteContents();
range.insertNode(document.createTextNode(' '));
// delete That specific word and replace if with resultValue
return true;
}
return false;
}
I noticed that those two lines are causing the browser error in the second issue
range.setStart(containerEl.firstChild, wordStart);
range.setEnd(containerEl.firstChild, wordEnd);
Theoretically, I know what the issue is. I believe both issues could be solved by making the range-creating script to use parent node rather than children nodes and also to loop through text nodes which line breaks are. However, I don't have a clue how to implement it at this point.
Could you please point me into the right direction?
Edit
I've actually managed to upload a demo with the progress so far to make it more clear.
Demo
I solved the problem myself and merged all functions into one. Neat! Below is the final code. I removed the ability to press enter after further considering it.
Hope it helps someone
var tw_template_trigger = '{{';
var tw_template_tag = '<span class="tw-template-tag" contenteditable="false">Pick a tag</span>';
$(".tw-post-template-content").keypress(function(e){ return e.which != 13; });
$('.tw-post-template-content').on( 'keyup mouseup', function() {
triggerTag( this );
});
function triggerTag(containerEl) {
var sel,
range,
text;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount > 0) {
range = sel.getRangeAt(0).cloneRange(); // clone current range into another variable for manipulation#
range.collapse(true);
range.setStart(containerEl, 0);
text = range.toString();
}
}
if( text && text.slice(-2) == tw_template_trigger ) {
range.setStart( range.endContainer, range.endOffset - tw_template_trigger.length);
range.setEnd( range.endContainer, range.endOffset );
range.deleteContents();
range.insertNode(document.createTextNode(' '));
//
var el = document.createElement("div");
el.innerHTML = tw_template_tag;
var frag = document.createDocumentFragment(), node, lastNode;
while ( (node = el.firstChild) ) {
lastNode = frag.appendChild(node);
}
var firstNode = frag.firstChild;
range.insertNode(frag);
// Preserve the selection
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
return true;
}
return false;
}
I want to delete some particular text before and after the selected text.For example if the text is:
<p>This is a <random>sentence</random> that i am writing<p>
If the user selects text,it should remove <random> and </random> from the text and text will be like this.
This is a sentence that i am writing.
If the user selects anything other than 'sentence',nothing will happen.
I know how to select a particular text but i dont know the next step on how to remove text before and after a particular text.Is it possible?
function replaceSelection() {
var sel, range, fragment;
if (typeof window.getSelection != "undefined") {
// IE 9 and other non-IE browsers
sel = window.getSelection();
// Test that the Selection object contains at least one Range
if (sel.getRangeAt && sel.rangeCount) {
// Get the first Range (only Firefox supports more than one)
range = window.getSelection().getRangeAt(0);
var selectedText = range.toString();
var replacementText = selectedText.replace(/<\/?random>/, '');
range.deleteContents();
// Create a DocumentFragment to insert and populate it with HTML
// Need to test for the existence of range.createContextualFragment
// because it's non-standard and IE 9 does not support it
if (range.createContextualFragment) {
fragment = range.createContextualFragment(replacementText);
} else {
// In IE 9 we need to use innerHTML of a temporary element
var div = document.createElement("div"), child;
div.innerHTML = replacementText;
fragment = document.createDocumentFragment();
while ( (child = div.firstChild) ) {
fragment.appendChild(child);
}
}
var firstInsertedNode = fragment.firstChild;
var lastInsertedNode = fragment.lastChild;
range.insertNode(fragment);
if (selectInserted) {
if (firstInsertedNode) {
range.setStartBefore(firstInsertedNode);
range.setEndAfter(lastInsertedNode);
}
sel.removeAllRanges();
sel.addRange(range);
}
}
} else if (document.selection && document.selection.type != "Control") {
// IE 8 and below
range = document.selection.createRange();
var selectedText = range.text;
var replacementText = selectedText.replace(/<\/?random>/, '')
range.pasteHTML(replacementText);
}
}
<div onmouseup="replaceSelection()"><p>This is a <random>sentence</random> that i am writing<p></div>
I have the following function that tracks the cursor position in an editable div:
function getCaretPosition(editableDiv) {
var caretPos = 0, containerEl = null, sel, range;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount) {
range = sel.getRangeAt(0);
if (range.commonAncestorContainer.parentNode == editableDiv) {
caretPos = range.endOffset;
}
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
if (range.parentElement() == editableDiv) {
var tempEl = document.createElement("span");
editableDiv.insertBefore(tempEl, editableDiv.firstChild);
var tempRange = range.duplicate();
tempRange.moveToElementText(tempEl);
tempRange.setEndPoint("EndToEnd", range);
caretPos = tempRange.text.length;
}
}
return caretPos-1;
}
The issue is that if my editable div has the following html:
hello <span contenteditable='false'>Abderrahmane Benbachir</span>
and I put the cursor next to the span, it starts counting the cursor from 0. Why is this? How do I fix this?
In this case, to get the caret position use this Tim Down's code instead. Which also has restoreSelection function in it which restores the caret position.
I asked the same question recently, which you can check here.
I'm inserting an element into a contentEditable div but the browser sets the position of the cursor before the inserted element. Is it possible to set the cursor right after the inserted element so that the user keeps typing without having to re-adjust the cursor position?
The following function will do it. DOM Level 2 Range objects make this easy in most browsers. In IE, you need to insert a marker element after the node you're inserting, move the selection to it and then remove it.
Live example: http://jsfiddle.net/timdown/4N4ZD/
Code:
function insertNodeAtCaret(node) {
if (typeof window.getSelection != "undefined") {
var sel = window.getSelection();
if (sel.rangeCount) {
var range = sel.getRangeAt(0);
range.collapse(false);
range.insertNode(node);
range = range.cloneRange();
range.selectNodeContents(node);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
}
} else if (typeof document.selection != "undefined" && document.selection.type != "Control") {
var html = (node.nodeType == 1) ? node.outerHTML : node.data;
var id = "marker_" + ("" + Math.random()).slice(2);
html += '<span id="' + id + '"></span>';
var textRange = document.selection.createRange();
textRange.collapse(false);
textRange.pasteHTML(html);
var markerSpan = document.getElementById(id);
textRange.moveToElementText(markerSpan);
textRange.select();
markerSpan.parentNode.removeChild(markerSpan);
}
}
Alternatively, you could use my Rangy library. The equivalent code there would be
function insertNodeAtCaret(node) {
var sel = rangy.getSelection();
if (sel.rangeCount) {
var range = sel.getRangeAt(0);
range.collapse(false);
range.insertNode(node);
range.collapseAfter(node);
sel.setSingleRange(range);
}
}
If you're inserting an empty div, p or span, I believe there needs to be "something" inside the newly created element for the range to grab onto -- and in order to put the caret inside there.
Here's my hack that seems to work OK in Chrome. The idea is simply to put a temporary string inside the element, then remove it once the caret is in there.
// Get the selection and range
var idoc = document; // (In my case it's an iframe document)
var sel = idoc.getSelection();
var range = sel.getRangeAt(0);
// Create a node to insert
var p = idoc.createElement("p"); // Could be a div, span or whatever
// Add "something" to the node.
var temp = idoc.createTextNode("anything");
p.appendChild(temp);
// -- or --
//p.innerHTML = "anything";
// Do the magic (what rangy showed above)
range.collapse(false);
range.insertNode( p );
range = range.cloneRange();
range.selectNodeContents(p);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
// Clear the non
p.removeChild(p.firstChild);
// -- or --
//p.innerHTML = "";
Here's what worked for me, using Rangy, in a VueJS context.
// When the user clicks the button to open the popup to enter
// the URL, run this function to save the location of the user's
// selection and the selected text.
newSaveSel: function() {
if (this.savedSel) {
rangy.removeMarkers(this.savedSel);
}
// Save the location of the selected text
this.savedSel = rangy.saveSelection();
// Save the selected text
this.savedSelText = rangy.getSelection().toString();
this.showLinkPopup = true;
console.log('newSavedSel', this.savedSel);
},
surroundRange: function() {
// Restore the user's selected text. This is necessary since
// the selection is lost when the user stars entering text.
if (this.savedSel) {
rangy.restoreSelection(this.savedSel, true);
this.savedSel = null;
}
// Surround the selected text with the anchor element
var sel = rangy.getSelection();
var range = sel.rangeCount ? sel.getRangeAt(0) : null;
if (range) {
// Create the new anchor element
var el = document.createElement("a");
el.style.backgroundColor = "pink";
el.href = this.anchorHref;
el.innerHTML = this.savedSelText;
if (this.checked) {
el.target = "_blank";
}
// Delete the originally selected text
range.deleteContents();
// Insert the anchor tag
range.insertNode(el);
// Ensure that the caret appears at the end
sel.removeAllRanges();
range = range.cloneRange();
range.selectNode(el);
range.collapse(false);
sel.addRange(range);
this.showLinkPopup = false;
}
},
I use this javascript code to get the currently highlighted selection.
var selection = window.getSelection()
If the highlight is a section of text all within a <div>, how can I get the offset from the beginning of the <div> and the length of the highlight? (the length is not just the length of the text, it should be the actual length of the html code for that text)
You can get the length of the selected HTML as follows:
function getSelectionHtml() {
var sel, html = "";
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount) {
var frag = sel.getRangeAt(0).cloneContents();
var el = document.createElement("div");
el.appendChild(frag);
html = el.innerHTML;
}
} else if (document.selection && document.selection.type == "Text") {
html = document.selection.createRange().htmlText;
}
return html;
}
alert(getSelectionHtml().length);