I am fixing an editor and I have highlighting text problem.
I got this code for checking what have the user highlighted (more details below the code):
function getSelectionHTML()
{
var iframe = document.getElementById(theEditorID);
var win, doc = iframe.contentDocument;
if(doc)
win = doc.defaultView;
else
{
win = iframe.contentWindow;
doc = win.document;
}
var userSelection;
if(win.getSelection)
{
userSelection = win.getSelection();
if(userSelection.getRangeAt)
var range = userSelection.getRangeAt(0);
else
{
var range = document.createRange();
range.setStart(userSelection.anchorNode, userSelection.anchorOffset);
range.setEnd(userSelection.focusNode, userSelection.focusOffset);
}
var clonedSelection = range.cloneContents();
var div = document.createElement('div');
div.appendChild(clonedSelection);
callIcons(div.innerHTML);
}
else if(doc.selection)
{
userSelection = document.selection.createRange();
callIcons(userSelection.htmlText);
}
};
When the user highlight some bold text and some other italic text i got this output:
<b>some text</b><i>some other text</i>
But when the user highlight only bold text i got this output (there is no 'bold' tag):
some text
You can check that, live, here - http://brownfolder.com/06/
You'll see an alert after highlighting some text.
Do you have any idea how can I fix this?
Thanks in advance.
Browsers vary over whether they include the surrounding tags in the selection. They usually don't include the surrounding tags if the selection is entirely contained within the tags. It's possible to walk up the DOM from the start of the selection and check if each element is a <b> tag. But if the iframe you're working with uses contenteditable, there's no guarantee it will bold with <b> tags. There are other ways the text might be bolded: IE usually adds <STRONG> tags to bold, and Firefox and WebKit may use <span style="font-weight:bold>.
It's probably better to use queryCommandState('bold') on the iframe document instead.
Related
I am really struggling with this.
I have two div on a page. The first one has got contents (mainly text). On the second div, I want to display the content of first div based on the position. for example if i select line 30, then the content of that line will be displayed in second div. Is there any idea to do that?
Thank you
This answer assumes you only want to copy the selected text to the new div and provices a basic idea how you can do so
In order to achieve that kind of behaviour you need to listen to the mouseup event in your container where you want text to be selected. That way we assume, that the user was selecting something and ended the selection.
I have prepared this JS fiddle for you: https://jsfiddle.net/djzkcw0m/
Code for the prove of concept:
HTML
Highlight some text with mouse in this container:
<div id="test">
This is some text and you can highlight it with your mouse
</div>
Result div:
<div id="result"></div>
JS
const testDiv = document.getElementById('test');
const resultDiv = document.getElementById('result');
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;
}
testDiv.addEventListener('mouseup', function(e) {
const selectedText = getSelectionText();
resultDiv.textContent = selectedText;
})
note
Method "getSelectionText()" is found from a related question Get the Highlighted/Selected text
HTML:
Our formula is this: We go out, we hit people in the mouth.<sup id="cite_ref-9" class="reference">[9]</sup> The team went 5–4 overall
I'm trying to match:
in the mouth. The team went
Basically trying to ignore the text inside the <sup> tag
I've adapted code from this answer.
document.designMode = "on";
var sel = window.getSelection();
sel.collapse(document.body, 0);
while (window.find(search_string)) {
document.execCommand("backColor", false, "yellow");
}
sel.collapseToEnd();
document.designMode = "off";
While this ignores the tag, it doesn't ignore text within the tag.
If you know it's always a sup that you want to remove then you should be able to do something like
function stripSupFromText(text) {
var div = document.createElement('div');
div.innerHTML = text;
div.removeChild(div.getElementsByTagName('sup')[0]);
return div.innerText;
}
I'm working on a little experimental editor where I would like to visualize the time between typed characters. Therefore I'm using javascript and a contenteditable div to wrap every character with a SPAN and a timestamp attribute. I build a little function with the help of rangy:
function insertAtCursor(char, timestamp) {
var sel = rangy.getSelection();
var range = sel.rangeCount ? sel.getRangeAt(0) : null;
if (range) {
var el = document.createElement("span");
$(el).attr('time', timestamp);
el.appendChild(document.createTextNode(char));
range.insertNode(el);
range.setStartAfter(el);
rangy.getSelection().setSingleRange(range);
}
}
Now I'm facing two problems with this concept where I would appreciate some help:
a. With the above function the output ends in nested span's like seen here:
<span time="12345">a
<span time="12345">b
<span time="12345">c</span>
</span>
</span>
b. Even if I could get the above function running, a copy&paste or drag&drop action would possibly also end in some nested span's ... and I wonder if there is a way to avoid that at all?
Thanks,
Andreas
I'm not convinced this a good idea overall, particularly if the text could get large. A couple of improvements:
time should probably be data-time to validate as HTML5
you need to handle the case where some content is selected (adding range.deleteContents() would do).
However, if you are going to do this, I would suggest checking if the cursor is at the end of a text node inside an existing <span> and appending the new <span> after the text node's parent. Something like this:
Live demo: http://jsfiddle.net/kWL82/1/
Code:
function insertAtCursor(char, timestamp) {
var sel = rangy.getSelection();
var range = sel.rangeCount ? sel.getRangeAt(0) : null;
var parent;
if (range) {
var el = document.createElement("span");
$(el).attr('data-time', timestamp);
el.appendChild(document.createTextNode(char));
// Check if the cursor is at the end of the text in an existing span
if (range.endContainer.nodeType == 3
&& (parent = range.endContainer.parentNode)
&& (parent.tagName == "SPAN")) {
range.setStartAfter(parent);
}
range.insertNode(el);
range.setStartAfter(el);
rangy.getSelection().setSingleRange(range);
}
}
I am writing a script on which the user needs to be able to select some text which is sent via ajax to the backend script for further process.
I can select plain text nodes fine or text nodes that have bold, italic or underlined text inside it.
For e.g
<p>This is <strong>some</strong> cool <em>italic</em> text, <u>really!</u></p>
So, that works, that is cool.
However, the issue is, if the text node starts with hsome bold, italic or underlined text OR even headings it outputs the following error on firefox console:
The boundary-points of a range does not meet specific requirements." code: "1 range.surroundContents($('<span...wAnno_'+newLen+'"></span>').get(0));
The error is output when the user selects something like:
<strong>Mark says</strong> Hi
OR
<em>Mark says</em> Hi
OR
<u>Mark says</u> Hi
The same error outputs even if a text is enclosed inside heading tags e.g <h2>test</h2>
My code looks like:
var select = window.getSelection();
var parents = $(select.focusNode).parents('.the-content');
if($(select.focusNode).parent().hasClass('.highlighted')) {
alert('This text is already highlighted');
} else {
for(var i = 0; i < select.rangeCount; i++) {
var range = select.getRangeAt(i);
range.surroundContents($('<span class="newHighlight" id="newHigh_'+newLen+'"></span>').get(0));
}
}
var selectedText = select.toString();
I need help with fixing this.
Help with the code will be awesome.
The problem is that the surroundContents method of Range can't work on a Range where the start and end boundaries lie within different elements, because surrounding the contents of such a Range within an element would not produce valid HTML. If changing the background colour of your Range is all you need to do, you could use the following trick with document.execCommand:
function highlight(colour) {
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);
}
// Use HiliteColor since some browsers apply BackColor to the whole block
if ( !document.execCommand("HiliteColor", false, colour) ) {
document.execCommand("BackColor", false, colour);
}
document.designMode = "off";
} else if (document.selection && document.selection.createRange) {
// IE case
range = document.selection.createRange();
range.execCommand("BackColor", false, colour);
}
}
Otherwise, you'll need to walk through the text nodes within the range and surround each with a <span>, which is not trivial. I've been working on a cross-browser range and selection library that includes a module for applying a CSS class to the contents of a selection or Range at http://code.google.com/p/rangy/, although that module is a few days away from being documented and released.
I'm working on a jQuery plugin that will allow you to do #username style tags, like Facebook does in their status update input box.
My problem is, that even after hours of researching and experimenting, it seems REALLY hard to simply move the caret. I've managed to inject the <a> tag with someone's name, but placing the caret after it seems like rocket science, specially if it's supposed work in all browsers.
And I haven't even looked into replacing the typed #username text with the tag yet, rather than just injecting it as I'm doing right now... lol
There's a ton of questions about working with contenteditable here on Stack Overflow, and I think I've read all of them, but they don't really cover properly what I need. So any more information anyone can provide would be great :)
You could use my Rangy library, which attempts with some success to normalize browser range and selection implementations. If you've managed to insert the <a> as you say and you've got it in a variable called aElement, you can do the following:
var range = rangy.createRange();
range.setStartAfter(aElement);
range.collapse(true);
var sel = rangy.getSelection();
sel.removeAllRanges();
sel.addRange(range);
I got interested in this, so I've written the starting point for a full solution. The following uses my Rangy library with its selection save/restore module to save and restore the selection and normalize cross browser issues. It surrounds all matching text (#whatever in this case) with a link element and positions the selection where it had been previously. This is triggered after there has been no keyboard activity for one second. It should be quite reusable.
function createLink(matchedTextNode) {
var el = document.createElement("a");
el.style.backgroundColor = "yellow";
el.style.padding = "2px";
el.contentEditable = false;
var matchedName = matchedTextNode.data.slice(1); // Remove the leading #
el.href = "http://www.example.com/?name=" + matchedName;
matchedTextNode.data = matchedName;
el.appendChild(matchedTextNode);
return el;
}
function shouldLinkifyContents(el) {
return el.tagName != "A";
}
function surroundInElement(el, regex, surrounderCreateFunc, shouldSurroundFunc) {
var child = el.lastChild;
while (child) {
if (child.nodeType == 1 && shouldSurroundFunc(el)) {
surroundInElement(child, regex, surrounderCreateFunc, shouldSurroundFunc);
} else if (child.nodeType == 3) {
surroundMatchingText(child, regex, surrounderCreateFunc);
}
child = child.previousSibling;
}
}
function surroundMatchingText(textNode, regex, surrounderCreateFunc) {
var parent = textNode.parentNode;
var result, surroundingNode, matchedTextNode, matchLength, matchedText;
while ( textNode && (result = regex.exec(textNode.data)) ) {
matchedTextNode = textNode.splitText(result.index);
matchedText = result[0];
matchLength = matchedText.length;
textNode = (matchedTextNode.length > matchLength) ?
matchedTextNode.splitText(matchLength) : null;
surroundingNode = surrounderCreateFunc(matchedTextNode.cloneNode(true));
parent.insertBefore(surroundingNode, matchedTextNode);
parent.removeChild(matchedTextNode);
}
}
function updateLinks() {
var el = document.getElementById("editable");
var savedSelection = rangy.saveSelection();
surroundInElement(el, /#\w+/, createLink, shouldLinkifyContents);
rangy.restoreSelection(savedSelection);
}
var keyTimer = null, keyDelay = 1000;
function keyUpLinkifyHandler() {
if (keyTimer) {
window.clearTimeout(keyTimer);
}
keyTimer = window.setTimeout(function() {
updateLinks();
keyTimer = null;
}, keyDelay);
}
HTML:
<p contenteditable="true" id="editable" onkeyup="keyUpLinkifyHandler()">
Some editable content for #someone or other
</p>
As you say you can already insert an tag at the caret, I'm going to start from there. The first thing to do is to give your tag an id when you insert it. You should then have something like this:
<div contenteditable='true' id='status'>I went shopping with <a href='#' id='atagid'>Jane</a></div>
Here is a function that should place the cursor just after the tag.
function setCursorAfterA()
{
var atag = document.getElementById("atagid");
var parentdiv = document.getElementById("status");
var range,selection;
if(window.getSelection) //FF,Chrome,Opera,Safari,IE9+
{
parentdiv.appendChild(document.createTextNode(""));//FF wont allow cursor to be placed directly between <a> tag and the end of the div, so a space is added at the end (this can be trimmed later)
range = document.createRange();//create range object (like an invisible selection)
range.setEndAfter(atag);//set end of range selection to just after the <a> tag
range.setStartAfter(atag);//set start of range selection to just after the <a> tag
selection = window.getSelection();//get selection object (list of current selections/ranges)
selection.removeAllRanges();//remove any current selections (FF can have more than one)
parentdiv.focus();//Focuses contenteditable div (necessary for opera)
selection.addRange(range);//add our range object to the selection list (make our range visible)
}
else if(document.selection)//IE 8 and lower
{
range = document.body.createRange();//create a "Text Range" object (like an invisible selection)
range.moveToElementText(atag);//select the contents of the a tag (i.e. "Jane")
range.collapse(false);//collapse selection to end of range (between "e" and "</a>").
while(range.parentElement() == atag)//while ranges cursor is still inside <a> tag
{
range.move("character",1);//move cursor 1 character to the right
}
range.move("character",-1);//move cursor 1 character to the left
range.select()//move the actual cursor to the position of the ranges cursor
}
/*OPTIONAL:
atag.id = ""; //remove id from a tag
*/
}
EDIT:
Tested and fixed script. It definitely works in IE6, chrome 8, firefox 4, and opera 11. Don't have other browsers on hand to test, but it doesn't use any functions that have changed recently so it should work in anything that supports contenteditable.
This button is handy for testing:
<input type='button' onclick='setCursorAfterA()' value='Place Cursor After <a/> tag' >
Nico