I have this function which removes selected (cursor) highlighted elements:
function deleteSelection() {
if (window.getSelection) {
// Mozilla
var selection = window.getSelection();
if (selection.rangeCount > 0) {
window.getSelection().deleteFromDocument();
window.getSelection().removeAllRanges();
}
} else if (document.selection) {
// Internet Explorer
var ranges = document.selection.createRangeCollection();
for (var i = 0; i < ranges.length; i++) {
ranges[i].text = "";
}
}
}
What I actually want to do is remove all the cursor- highlighted elements, except for the input#cursor element.
edit:
so lets say if I highlighted these elements with my cursor:
<span>a</span>
<span>b</span>
<input type='text' id = 'cursor' />
<span>c</span>
<span>d</span>
on a keyup, this function should not remove the <input> ...only the <span>
Phew, that was a little harder than expected!
Here's the code, I've tested it in IE8 and Chrome 16/FF5. The JSFiddle I used to test is available here. AFAIK, this is probably the best performance-wise you'll get.
The docs I used for Moz: Selection, Range, DocumentFragment. IE: TextRange
function deleteSelection() {
// get cursor element
var cursor = document.getElementById('cursor');
// mozilla
if(window.getSelection) {
var selection = window.getSelection();
var containsCursor = selection.containsNode(cursor, true);
if(containsCursor) {
var cursorFound = false;
for(var i=0; i < selection.rangeCount; i++) {
var range = selection.getRangeAt(i);
if(!cursorFound) {
// extracts tree from DOM and gives back a fragment
var contents = range.extractContents();
// check if tree fragment contains our cursor
cursorFound = containsChildById(contents, 'cursor');
if(cursorFound) range.insertNode(cursor); // put back in DOM
}
else {
// deletes everything in range
range.deleteContents();
}
}
}
else {
selection.deleteFromDocument();
}
// removes highlight
selection.removeAllRanges();
}
// ie
else if(document.selection) {
var ranges = document.selection.createRangeCollection();
var cursorFound = false;
for(var i=0; i < ranges.length; i++) {
if(!cursorFound) {
// hacky but it will work
cursorFound = (ranges[i].htmlText.indexOf('id=cursor') != -1);
if(cursorFound)
ranges[i].pasteHTML(cursor.outerHTML); // replaces html with parameter
else
ranges[i].text = '';
}
else {
ranges[i].text = '';
}
}
}
}
// simple BFS to find an id in a tree, not sure if you have a
// library function that does this or not
function containsChildById(source, id) {
q = [];
q.push(source);
while(q.length > 0) {
var current = q.shift();
if(current.id == id)
return true;
for(var i=0; i < current.childNodes.length; i++) {
q.push(current.childNodes[i]);
}
}
return false;
}
I would suggest removing the element you want keep, extracting the contents of the selection using Range's extractContents() method and then reinserting the element you want to keep using the range's insertNode() method.
For IE < 9, which doesn't support DOM Range, the code is trickier, so for simplicity you could use my Rangy library, which also adds convenience methods such as the containsNode() method of Range.
Live demo: http://jsfiddle.net/DFxH8/1/
Code:
function deleteSelectedExcept(node) {
var sel = rangy.getSelection();
if (!sel.isCollapsed) {
for (var i = 0, len = sel.rangeCount, range, nodeInRange; i < len; ++i) {
range = sel.getRangeAt(i);
nodeInRange = range.containsNode(node);
range.extractContents();
if (nodeInRange) {
range.insertNode(node);
}
}
}
}
deleteSelectedExcept(document.getElementById("cursor"));
Related
I want to select the HTML of whatever the user selects in a contenteditable div. I found some code to retrieve the HTML of the selection, but it's not limited to just the div.
What I want to do is copy the selected HTML, wrap tags around it, and then replace the selection with it. So, 'test' would become 'test' for instance.
<div contenteditable="true" class="body" id="bodydiv"></div>
function getSelectionHtml() {
var html = "";
if (typeof window.getSelection != "undefined") {
var sel = window.getSelection();
if (sel.rangeCount) {
var container = document.createElement("div");
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
container.appendChild(sel.getRangeAt(i).cloneContents());
}
html = container.innerHTML;
}
} else if (typeof document.selection != "undefined") {
if (document.selection.type == "Text") {
html = document.selection.createRange().htmlText;
}
}
}
HI for getting content(text) inside a selected div I have create a little code you can check it out if it works for you :
$(document).ready(function(){
$(document).bind('mouseup', function(){
var content = getSelected();
content = "<b>"+content+"</b>";
$('#selected').html(content);
});
});
function getSelected(){
var t;
if(window.getSelection){
t = window.getSelection();
var start = t.focusOffset;
var end = t.baseOffset;
t = t.anchorNode.data;
t = t.slice(start, end);
} else if (document.selection) {
t = document.selection.createRange().text;
}
return t;
}
And
<div id="selected"></div>
Hope this helps.
I am trying to make a web app which allows the user to select some text on a page, and then with the click of another button highlight it. When the user goes back to the page, I want the highlight to show up in the same spot as well.
So I've gotten as far as:
var selectedRange = document.getSelection().getRangeAt(0);
highlightRange(selectedRange);
Where highlightRange is a function that highlights the range. This works so far.
The problem is, I need a way to save the selectedRange into a database so that it can be fetched again later. After that, I need to re-create the range from this data and highlight it again. I've found this method:
document.createRange();
From this page here: https://developer.mozilla.org/en-US/docs/Web/API/range
But I'm not really sure how I can make this work
UPDATE:
I know I'll need to re-create the range from scratch afterwards. And to do that I will use something like this:
var range = document.createRange();
range.setStart(startNode,startOffset);
range.setEnd(endNode,endOffset);
I can easily store startOffset and endOffset because those are just numbers. But startNode and endNode are node objects. I don't know how to store this in a database?
More specifically, I need to store the reference to the node in a database.
I solved this problem by saving 5 pieces of information from the range into the database:
var saveNode = range.startContainer;
var startOffset = range.startOffset; // where the range starts
var endOffset = range.endOffset; // where the range ends
var nodeData = saveNode.data; // the actual selected text
var nodeHTML = saveNode.parentElement.innerHTML; // parent element innerHTML
var nodeTagName = saveNode.parentElement.tagName; // parent element tag name
And then to build the range from the database, I have this function:
function buildRange(startOffset, endOffset, nodeData, nodeHTML, nodeTagName){
var cDoc = document.getElementById('content-frame').contentDocument;
var tagList = cDoc.getElementsByTagName(nodeTagName);
// find the parent element with the same innerHTML
for (var i = 0; i < tagList.length; i++) {
if (tagList[i].innerHTML == nodeHTML) {
var foundEle = tagList[i];
}
}
// find the node within the element by comparing node data
var nodeList = foundEle.childNodes;
for (var i = 0; i < nodeList.length; i++) {
if (nodeList[i].data == nodeData) {
var foundNode = nodeList[i];
}
}
// create the range
var range = cDoc.createRange();
range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);
return range;
}
From there, I can just use my highlightRange function again to highlight the text.
Update 2022-01
Didn't know this was actually still being used. Thought I might as well give two cents about the two for loops and how we can improve them with modern syntax:
const foundEle = tagList.find(x => x.innerHTML === nodeHTML);
const foundNode = nodeList.find(x => x.data === nodeData);
run the following script when user select texts
let sel = window.getSelection();
let range = sel.getRangeAt(0);
let startNode = range.startContainer;
let endNode = range.endContainer;
if (startNode.nodeType == 3) {
var startIsText = true;
var startFlag = startNode.parentNode;
startNode = startNode.nodeValue;
} else {
var startIsText = false;
var startFlag = startNode;
}
if (endNode.nodeType == 3) {
var endIsText = true;
var endFlag = endNode.parentNode;
endNode = endNode.nodeValue;
} else {
var endIsText = false;
var endFlag = endNode;
}
let startOffset = range.startOffset;
let endOffset = range.endOffset;
let startTagName = startFlag.nodeName;
let startHTML = startFlag.innerHTML;
let endTagName = endFlag.nodeName;
let endHTML = endFlag.innerHTML;
//you can store this in database and use it
let rInfo = {
startNode: startNode,
startOffset: startOffset,
startIsText: startIsText,
startTagName: startTagName,
startHTML: startHTML,
endNode: endNode,
endOffset: endOffset,
endIsText: endIsText,
endTagName: endTagName,
endHTML: endHTML
};
window.localStorage.setItem("r", JSON.stringify(rInfo));
then use the following scripts when user go back to the page
function findEle(tagName, innerHTML) {
let list = document.getElementsByTagName(tagName);
for (let i = 0; i < list.length; i++) {
if (list[i].innerHTML == innerHTML) {
return list[i];
}
}
}
function show(startNode,startIsText,startOffset,
endNode,endIsText,endOffset,sP,eP) {
var s, e;
if (startIsText) {
let childs = sP.childNodes;
console.log(childs);
for (let i = 0; i < childs.length; i++) {
console.log(childs[i].nodeValue);
console.log(startNode);
if (childs[i].nodeType == 3 && childs[i].nodeValue == startNode)
s = childs[i];
console.log(s);
}
} else {
s = startNode;
}
if (endIsText) {
let childs = eP.childNodes;
console.log(childs);
for (let i = 0; i < childs.length; i++) {
if (childs[i].nodeType == 3 && childs[i].nodeValue == endNode)
e = childs[i];
console.log(e);
}
} else {
e = endNode;
}
let range = document.createRange();
range.setStart(s, startOffset);
range.setEnd(e, endOffset);
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
function use(obj) {
let sP = findEle(obj.startTagName, obj.startHTML);
let eP = findEle(obj.endTagName, obj.endHTML);
show(
obj.startNode,
obj.startIsText,
obj.startOffset,
obj.endNode,
obj.endIsText,
obj.endOffset,
sP,
eP
);
}
let a = window.localStorage.getItem("r");
use(JSON.parse(a));
I have a script to wrap html tags around the selection.
function wrap(tagName)
{
var selection;
var elements = [];
var ranges = [];
var rangeCount = 0;
if (window.getSelection)
{
selection = window.getSelection();
if (selection.rangeCount)
{
rangeCount = selection.rangeCount;
for (var i=0; i<rangeCount; i++)
{
ranges[i] = selection.getRangeAt(i).cloneRange();
elements[i] = document.createElement(tagName);
elements[i].appendChild(ranges[i].cloneContents());
ranges[i].deleteContents();
ranges[i].insertNode(elements[i]);
ranges[i].selectNode(elements[i]);
}
selection.removeAllRanges();
for (var i=0; i<ranges.length; i++)
{
selection.addRange(ranges[i]);
}
}
}
}
It works fine, but when I try to select the following text and wrap tags around the following code
<strong>W</strong>elcom<strong>e</strong>
The code changes to
<strong></strong><u><strong>W</strong>elcom<strong>e</strong></u><strong></strong>
instead of
<u><strong>W</strong>elcom<strong>e</strong></u>
I have inspected the code with firebug and it goes wrong at the function deleteContents(). The function deleteContents() leaves two empty nodes <strong></strong> so it doesn't delete the whole content. How does this happen?
I have a code to wrap elements around text it works fine until i try the following format in my editor:
<u><strong>T</strong>es<strong>t</strong></u>
It automatic adds two empty strong elements before the underlined element and after like this:
<strong></strong>
<u><strong>T</strong>es<strong>t</strong></u>
<strong></strong>
Here's the code that i use and i have buttons that have actions like wrap('strong'):
function wrap(tagName)
{
var selection;
var elements = [];
var ranges = [];
var rangeCount = 0;
if (window.getSelection)
{
selection = window.getSelection();
if (selection.rangeCount)
{
rangeCount = selection.rangeCount;
for (var i=0; i<rangeCount; i++)
{
ranges[i] = selection.getRangeAt(i).cloneRange();
elements[i] = document.createElement(tagName);
elements[i].appendChild(ranges[i].extractContents());
ranges[i].insertNode(elements[i]);
ranges[i].selectNode(elements[i]);
}
selection.removeAllRanges();
for (var i=0; i<ranges.length; i++)
{
selection.addRange(ranges[i]);
}
}
}
}
WYSIWYG is hard. Especially with HTML, which is nothing what it looks like.
I'm just guessing here, but if you start with
<u>Test</u>
ant you select T in WYSIWYG then the actual selected code will probably be <u>T. Since you can't wrap that in strong (because <strong><u>T</strong> is not valid markup then the editor will wrap everything before the tag in strong and everything after tag in strong, which results in
<strong></strong>
<u><strong>T</strong>es<strong>t</strong></u>
<strong></strong>
that you are getting.
I to avoid that you could check if the text that you are wrapping has the lenght of 0, end then if so - not wrap it with anything.
I'd suggest using DOM manipulation to wrap text nodes within the selection individually. My Rangy library can help a little with this, providing splitBoundaries() and getNodes() extension methods to its Range objects.
Live demo: http://jsfiddle.net/5cdMn/
Code:
function isNodeInsideElementWithTagName(node, tagName) {
tagName = tagName.toLowerCase();
while (node) {
if (node.nodeType == 1 && node.tagName.toLowerCase() == tagName) {
return true;
}
node = node.parentNode;
}
return false;
}
function wrapSelection(tagName) {
var range, textNode, i, len, j, jLen, el;
var ranges = rangy.getSelection().getAllRanges();
for (i = 0, len = ranges.length; i < len; ++i) {
range = ranges[i];
range.splitBoundaries();
textNodes = range.getNodes([3]/* Array of node types to retrieve */);
for (j = 0, jLen = textNodes.length; j < jLen; ++j) {
textNode = textNodes[j];
if (!isNodeInsideElementWithTagName(textNode, tagName)) {
el = document.createElement(tagName);
textNode.parentNode.insertBefore(el, textNode);
el.appendChild(textNode);
}
}
}
}
it has any way for bind execcommand with a div element not for whole document , i try this :
document.getElementById('div').execcommand(...)
but it has an error :
execcommand is not a function
it has any way for bind the execcommand with just div element not whole document !!
i don't like use iframe method .
This is easier to do in IE than other browsers because IE's TextRange objects have an execCommand() method, meaning that a command can be executed on a section of the document without needing to change the selection and temporarily enable designMode (which is what you have to do in other browsers). Here's a function to do what you want cleanly:
function execCommandOnElement(el, commandName, value) {
if (typeof value == "undefined") {
value = null;
}
if (typeof window.getSelection != "undefined") {
// Non-IE case
var sel = window.getSelection();
// Save the current selection
var savedRanges = [];
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
savedRanges[i] = sel.getRangeAt(i).cloneRange();
}
// Temporarily enable designMode so that
// document.execCommand() will work
document.designMode = "on";
// Select the element's content
sel = window.getSelection();
var range = document.createRange();
range.selectNodeContents(el);
sel.removeAllRanges();
sel.addRange(range);
// Execute the command
document.execCommand(commandName, false, value);
// Disable designMode
document.designMode = "off";
// Restore the previous selection
sel = window.getSelection();
sel.removeAllRanges();
for (var i = 0, len = savedRanges.length; i < len; ++i) {
sel.addRange(savedRanges[i]);
}
} else if (typeof document.body.createTextRange != "undefined") {
// IE case
var textRange = document.body.createTextRange();
textRange.moveToElementText(el);
textRange.execCommand(commandName, false, value);
}
}
Examples:
var testDiv = document.getElementById("test");
execCommandOnElement(testDiv, "Bold");
execCommandOnElement(testDiv, "ForeColor", "red");
Perhaps this helps you out:
Example code 1 on this page has the execcommand on a div by using a function. Not sure if that's what you're after? Good luck!
Edit: I figured out how to put the code here :o
<head>
<script type="text/javascript">
function SetToBold () {
document.execCommand ('bold', false, null);
}
</script>
</head>
<body>
<div contenteditable="true" onmouseup="SetToBold ();">
Select a part of this text!
</div>
</body>
Javascript:
var editableFocus = null;
window.onload = function() {
var editableElements = document.querySelectorAll("[contenteditable=true]");
for (var i = 0; i<editableElements.length; i++) {
editableElements[i].onfocus = function() {
editableFocus = this.id;
}
};
}
function setPreviewFocus(obj){
editableFocus = obj.id
}
function formatDoc(sCmd, sValue) {
if(editableFocus == "textBox"){
document.execCommand(sCmd, false, sValue);
}
}
HTML:
<div id="textBox" contenteditable="true">
Texto fora do box
</div>
<div contenteditable="true">
Texto fora do box
</div>
<button title="Bold" onclick="formatDoc('bold');">Bold</button>
You can keep it Simple, and add this to your div.
<div contenteditable="true" onmouseup="document.execCommand('bold',false,null);">