Use javascript to extend a DOM Range to cover partially selected nodes - javascript

I'm working on a rich text editor like web application, basically a XML editor written in javascript.
My javascript code needs to wrap a selection of nodes from the contentEditable div container.
I'm using the methods described at MDC. But since I need to synchronize the div containers content to my XML DOM I would like to avoid partial selections as described in w3c ranges:
<BODY><H1>Title</H1><P>Blah xyz.</P></BODY
............^----------------^............
This selection starts inside H1 and ends inside P, I'd like it to include H1,P completely.
Is there an easy way to extend the selection to cover partially selected children completely?
Basically I want to use range.surroundContents() without running into an exception.
(The code doesn't need to work with opera/IE)

Looking at the MDC documentation, I manage do something like this:
Selection.prototype.coverAll = function() {
var ranges = [];
for(var i=0; i<this.rangeCount; i++) {
var range = this.getRangeAt(i);
while(range.startContainer.nodeType == 3
|| range.startContainer.childNodes.length == 1)
range.setStartBefore(range.startContainer);
while(range.endContainer.nodeType == 3
|| range.endContainer.childNodes.length == 1)
range.setEndAfter(range.endContainer);
ranges.push(range);
}
this.removeAllRanges();
for(var i=0; i<ranges.length; i++) {
this.addRange(ranges[i]);
}
return;
};
You can try it here : http://jsfiddle.net/GFuX6/9/
edit:
Updated to have the browser display correctly the augmented selection. It does what you asked for, even if the selection contains several ranges (with Ctrl).
To make several partial nodes Bold, here is a solution:
Selection.prototype.boldinize = function() {
this.coverAll();
for(var i=0; i<this.rangeCount; i++) {
var range = this.getRangeAt(i);
var parent = range.commonAncestorContainer;
var b = document.createElement('b');
if(parent.nodeType == 3) {
range.surroundContents(b);
} else {
var content = range.extractContents();
b.appendChild(content);
range.insertNode(b);
}
}
};

Thanks to Alsciende I finally came up with the code at http://jsfiddle.net/wesUV/21/.
This method isn't as greedy as the other one. After coverAll(), surroundContents() should always work.
Selection.prototype.coverAll = function() {
var ranges = [];
for(var i=0; i<this.rangeCount; i++) {
var range = this.getRangeAt(i);
var ancestor = range.commonAncestorContainer;
if (ancestor.nodeType == 1) {
if (range.startContainer.parentNode != ancestor && this.containsNode(range.startContainer.parentNode, true)) {
range.setStartBefore(range.startContainer.parentNode);
}
if (range.endContainer.parentNode != ancestor && this.containsNode(range.endContainer.parentNode, true)) {
range.setEndAfter(range.endContainer.parentNode);
}
}
ranges.push(range);
}
this.removeAllRanges();
for(var i=0; i<ranges.length; i++) {
this.addRange(ranges[i]);
}
return;
};
And the boldinize function:
Selection.prototype.boldinize = function() {
for(var i=0; i<this.rangeCount; i++) {
var range = this.getRangeAt(i);
var b = document.createElement('b');
try {
range.surroundContents(b);
} catch (e) {
alert(e);
}
}
};

If you mean you want to include the tags H1 and P (i.e., the valid markup), don't worry. You get that for free. If you mean you want to it to include all the content within the (partial) selection, you need to access the Selection object. Read about it on Quirksmode's introduction to Range.

Related

Javascript get more than one id at a time

I'm just wondering if its possible in javascript to get more than one id at a time, without the use of JQuery. I'm checking the background color of each cell in a dynamically created table. For instance, I have this code:
var black = "rgb(0, 0, 0)";
if(document.getElementById("cell1").style.backgroundColor == black &&
document.getElementById("cell2").style.backgroundColor == black)
{
alert("Two cells are black!");
}
Would it be possible to do something like this:
var black = "rgb(0, 0, 0)";
if(document.getElementById("cell1","cell2").style.backgroundColor == black)
{
alert("Two cells are black!");
}
I'm trying not to use JQuery at all as I'm not too familiar with it.
With modern browsers you can do something similar using querySelectorAll (compatibility matrix), but you'd still have to loop over the resulting NodeList:
var nodes = document.querySelectorAll("#cell1, #cell2");
var count = 0;
for (var index = 0; index < nodes.length; ++index) {
if (nodes[index].style.backgroundColor == black) {
++count;
}
}
if (nodes.length === count) {
alert("Both are black");
}
Doesn't really buy you anything over, say:
var cells = ["cell1", "cell2"];
var count = 0;
for (var index = 0; index < cells.length; ++index) {
if (document.getElementById(cells[index]).style.backgroundColor == black) {
++count;
}
}
if (cells.length === count) {
alert("All cells are black");
}
So in short: No, there isn't really anything more useful you can do.
Not natively. You could write your own fairly easily, though:
function getElementsById(elements)
{
var to_return = [ ];
for(var i = 0; i < elements.length; i++)
{
to_return.push(document.getElementById(elements[i]));
}
return to_return;
}
This will accept an array of IDs as the parameter, and return the elements in an array. You might also want to look into the document.querySelector method.
No,
without using jQuery or other javascript helper libraries.
querySelector is not supported by IE7 and below which still represents a fairly large proportion of the traffice http://caniuse.com/#feat=queryselector
I have upvoted #BenM's answer, but I'd like to suggest another option.
Your issue:
I'm checking the background color of each cell
In this case, it would make more sense to attach the id to the table itself. Your selector becomes (assuming no nested table):
var cells = document.getElementById("myTable").getElementsByTagName("td");
or for recent browsers:
var cells = querySelectorAll("#myTable>tbody>tr>td");
For the record, here is another way if all your cells have a similar id "cellxxx":
var cells = querySelectorAll("td[id^='cell']");

Rangy range contained within a jquery selector

I'm working on a JavaScript wrapper around the Rangy JavaScript plugin. What I'm trying to do: given a jQuery selector and a range, detect if the range is contained within the selector. This is for a space where a user will read a document and be able to make comments about particular sections. So I have a div with id="viewer" that contains the document, and I have an area of buttons that do things after a user selects some text. Here is the (broken) function:
function selectedRangeInRegion(selector) {
var selectionArea = $(selector);
var range = rangy.getSelection().getRangeAt(0);
var inArea = (selectionArea.has(range.startContainer).length > 0);
if (inArea) {
return range;
} else {
return null;
}
}
It appears that selectionArea.has(range.startContainer) returns an array of size 0. I have tried wrapping like: $(range.startContainer). Any tips?
I developed a solution for this problem. This assumes you have a div selector and that your content does not have any divs:
function containsLegalRange(selector, range) {
var foundContainingNode = false;
var container = range.commonAncestorContainer
var nearestDiv = $(container).closest("div");
if (nearestDiv.attr("id") == selector) {
return true
}
else {
return false
}
}
That's not how has() works: the parameter you pass to it is either a selector string or a DOM element, whereas range.startContainer is a DOM node that may in practice be a text node or an element.
I don't think there will be a way that's as easy as you're hoping. The following is as simple as I can think of off the top of my head.
jsFiddle: http://jsfiddle.net/TRVCm/
Code:
function containsRange(selector, range, allowPartiallySelected) {
var foundContainingNode = false;
$(selector).each(function() {
if (range.containsNode(this, allowPartiallySelected)) {
foundContainingNode = true;
return false;
}
});
return foundContainingNode;
}
.has() can be weird sometimes and produce .length == 0 when it is not supposed to. Try this way instead:
function selectedRangeInRegion(selector) {
var range = rangy.getSelection().getRangeAt(0);
var selectionArea = selector + ':has(\'' + range.startContainer + '\')';
var inArea = $(selectionArea).length > 0);
if (inArea) {
return range;
}
else {
return null;
}
}

Add HTML element after text

I am looking for a way to add an HTML element using JavaScript. But the problem is that the new element might be in between some text. In all other cases I'm using the insertBefore() method.
I am using the following function to get the cursor position.
My initial approach was to split the target innerHTML and add the necessary tags but the cursor position provided does not take into account the character conversions such as space to . So if there are multiple continous spaces, the cursor position will not give the coreect position int he innerHTML.
function getCursorPos()
{
var cursorPos=-1;
if (window.getSelection)
{
var selObj = window.getSelection();
var selRange = selObj.getRangeAt(0);
cursorPos = findNode(selObj.anchorNode.parentNode.childNodes,
selObj.anchorNode) + selObj.anchorOffset;
/* FIXME the following works wrong in Opera when the document is longer than 32767 chars */
}
else if (document.selection)
{
var range = document.selection.createRange();
var bookmark = range.getBookmark();
/* FIXME the following works wrong when the document is longer than 65535 chars */
cursorPos = bookmark.charCodeAt(2) - 11; /* Undocumented function [3] */
}
return cursorPos;
}
function findNode(list, node)
{
for (var i = 0; i < list.length; i++)
{
if (list[i] == node)
{
return i;
}
}
return -1;
}
Is there any other method to do this?
The new element may be in the middle of the HTML ie, it may not be always at the end.
Thank You
You can do this:
var text = $("#container").html();//the target element has the id of container
Process text now, that is break up text the way you want or whatever you want to do with it and add html elements at the relevant position in this text using string addition.
Then do this ...
$("#container").html(text);
You can also take Keith's approach. It's important to realize what he said. Another way to look into it would be inserting a dom element (e.g., html tags) inside text may not be possible with insertAfter or insertBefore.
Here's part of the method I used. It works but I dont know if there are better ways.
function getCursorNode()
{
if (window.getSelection)
{
var selObj = window.getSelection();
return selObj.anchorNode;
}
}
function splitTextNode(pos)
{
selNode=getCursorNode();
if(selNode.nodeName=="#text")
{
var value=selNode.nodeValue;
if(value.length == pos)
{
return ({"node":selNode,"txt":value });
}
else if(pos==0)
{
return ({"node":selNode,"txt":""});
}
else
{
var splittxt1=value.slice(0,pos)
var tempsplit1=document.createTextNode(splittxt1);
var splittxt2=value.slice(pos)
var tempsplit2=document.createTextNode(splittxt2);
myInsertAfterMany([tempsplit1,tempsplit2],selNode);
child.parentNode.removeChild(selNode);
return ({"node":tempsplit1,"first":false,"txt":splittxt1});
}
}
}
But, it(getCursorNode) does not work in IE.
Now, I append the required node after "node" after checking for "first"

How can I loop through ALL DOM elements on a page?

I'm trying to loop over ALL elements on a page, so I want to check every element that exists on this page for a special class.
So, how do I say that I want to check EVERY element?
You can pass a * to getElementsByTagName() so that it will return all elements in a page:
var all = document.getElementsByTagName("*");
for (var i=0, max=all.length; i < max; i++) {
// Do something with the element here
}
Note that you could use querySelectorAll(), if it's available (IE9+, CSS in IE8), to just find elements with a particular class.
if (document.querySelectorAll)
var clsElements = document.querySelectorAll(".mySpeshalClass");
else
// loop through all elements instead
This would certainly speed up matters for modern browsers.
Browsers now support foreach on NodeList. This means you can directly loop the elements instead of writing your own for loop.
document.querySelectorAll('*').forEach(function(node) {
// Do whatever you want with the node object.
});
Performance note - Do your best to scope what you're looking for by using a specific selector. A universal selector can return lots of nodes depending on the complexity of the page. Also, consider using document.body.querySelectorAll instead of document.querySelectorAll when you don’t care about <head> children.
Was looking for same. Well, not exactly. I only wanted to list all DOM Nodes.
var currentNode,
ni = document.createNodeIterator(document.documentElement, NodeFilter.SHOW_ELEMENT);
while(currentNode = ni.nextNode()) {
console.log(currentNode.nodeName);
}
To get elements with a specific class, we can use filter function.
var currentNode,
ni = document.createNodeIterator(
document.documentElement,
NodeFilter.SHOW_ELEMENT,
function(node){
return node.classList.contains('toggleable') ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
);
while(currentNode = ni.nextNode()) {
console.log(currentNode.nodeName);
}
Found solution on
MDN
As always the best solution is to use recursion:
loop(document);
function loop(node){
// do some thing with the node here
var nodes = node.childNodes;
for (var i = 0; i <nodes.length; i++){
if(!nodes[i]){
continue;
}
if(nodes[i].childNodes.length > 0){
loop(nodes[i]);
}
}
}
Unlike other suggestions, this solution does not require you to create an array for all the nodes, so its more light on the memory. More importantly, it finds more results. I am not sure what those results are, but when testing on chrome it finds about 50% more nodes compared to document.getElementsByTagName("*");
Here is another example on how you can loop through a document or an element:
function getNodeList(elem){
var l=new Array(elem),c=1,ret=new Array();
//This first loop will loop until the count var is stable//
for(var r=0;r<c;r++){
//This loop will loop thru the child element list//
for(var z=0;z<l[r].childNodes.length;z++){
//Push the element to the return array.
ret.push(l[r].childNodes[z]);
if(l[r].childNodes[z].childNodes[0]){
l.push(l[r].childNodes[z]);c++;
}//IF
}//FOR
}//FOR
return ret;
}
For those who are using Jquery
$("*").each(function(i,e){console.log(i+' '+e)});
Andy E. gave a good answer.
I would add, if you feel to select all the childs in some special selector (this need happened to me recently), you can apply the method "getElementsByTagName()" on any DOM object you want.
For an example, I needed to just parse "visual" part of the web page, so I just made this
var visualDomElts = document.body.getElementsByTagName('*');
This will never take in consideration the head part.
from this link
javascript reference
<html>
<head>
<title>A Simple Page</title>
<script language="JavaScript">
<!--
function findhead1()
{
var tag, tags;
// or you can use var allElem=document.all; and loop on it
tags = "The tags in the page are:"
for(i = 0; i < document.all.length; i++)
{
tag = document.all(i).tagName;
tags = tags + "\r" + tag;
}
document.write(tags);
}
// -->
</script>
</head>
<body onload="findhead1()">
<h1>Heading One</h1>
</body>
</html>
UPDATE:EDIT
since my last answer i found better simpler solution
function search(tableEvent)
{
clearResults()
document.getElementById('loading').style.display = 'block';
var params = 'formAction=SearchStocks';
var elemArray = document.mainForm.elements;
for (var i = 0; i < elemArray.length;i++)
{
var element = elemArray[i];
var elementName= element.name;
if(elementName=='formAction')
continue;
params += '&' + elementName+'='+ encodeURIComponent(element.value);
}
params += '&tableEvent=' + tableEvent;
createXmlHttpObject();
sendRequestPost(http_request,'Controller',false,params);
prepareUpdateTableContents();//function js to handle the response out of scope for this question
}
Getting all elements using var all = document.getElementsByTagName("*"); for (var i=0, max=all.length; i < max; i++); is ok if you need to check every element but will result in checking or looping repeating elements or text.
Below is a recursion implementation that checks or loop each element of all DOM elements only once and append:
(Credits to #George Reith for his recursion answer here: Map HTML to JSON)
function mapDOMCheck(html_string, json) {
treeObject = {}
dom = new jsdom.JSDOM(html_string) // use jsdom because DOMParser does not provide client-side Window for element access
document = dom.window.document
element = document.querySelector('html')
// Recurse and loop through DOM elements only once
function treeHTML(element, object) {
var nodeList = element.childNodes;
if (nodeList != null) {
if (nodeList.length) {
object[element.nodeName] = []; // IMPT: empty [] array for parent node to push non-text recursivable elements (see below)
for (var i = 0; i < nodeList.length; i++) {
console.log("nodeName", nodeList[i].nodeName);
if (nodeList[i].nodeType == 3) { // if child node is **final base-case** text node
console.log("nodeValue", nodeList[i].nodeValue);
} else { // else
object[element.nodeName].push({}); // push {} into empty [] array where {} for recursivable elements
treeHTML(nodeList[i], object[element.nodeName][object[element.nodeName].length - 1]);
}
}
}
}
}
treeHTML(element, treeObject);
}
Use *
var allElem = document.getElementsByTagName("*");
for (var i = 0; i < allElem.length; i++) {
// Do something with all element here
}
i think this is really quick
document.querySelectorAll('body,body *').forEach(function(e) {
You can try with
document.getElementsByClassName('special_class');

Get a CSS value from external style sheet with Javascript/jQuery

Is it possible to get a value from the external CSS of a page if the element that the style refers to has not been generated yet? (the element is to be generated dynamically).
The jQuery method I've seen is $('element').css('property');, but this relies on element being on the page. Is there a way of finding out what the property is set to within the CSS rather than the computed style of an element?
Will I have to do something ugly like add a hidden copy of the element to my page so that I can access its style attributes?
With jQuery:
// Scoping function just to avoid creating a global
(function() {
var $p = $("<p></p>").hide().appendTo("body");
console.log($p.css("color"));
$p.remove();
})();
p {color: blue}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Using the DOM directly:
// Scoping function just to avoid creating a global
(function() {
var p = document.createElement('p');
document.body.appendChild(p);
console.log(getComputedStyle(p).color);
document.body.removeChild(p);
})();
p {color: blue}
Note: In both cases, if you're loading external style sheets, you'll want to wait for them to load in order to see their effect on the element. Neither jQuery's ready nor the DOM's DOMContentLoaded event does that, you'd have to ensure it by watching for them to load.
Normally you should be let the browser apply all the rules and then ask the browser for the results, but for the rare case where you really need to get the value out of the style sheet you can use this: (JSFiddle)
function getStyleSheetPropertyValue(selectorText, propertyName) {
// search backwards because the last match is more likely the right one
for (var s= document.styleSheets.length - 1; s >= 0; s--) {
var cssRules = document.styleSheets[s].cssRules ||
document.styleSheets[s].rules || []; // IE support
for (var c=0; c < cssRules.length; c++) {
if (cssRules[c].selectorText === selectorText)
return cssRules[c].style[propertyName];
}
}
return null;
}
alert(getStyleSheetPropertyValue("p", "color"));
Note that this is pretty fragile, as you have to supply the full selector text that matches the rule you are looking up (it is not parsed) and it does not handle duplicate entries or any kind of precedence rules. It's hard for me to think of a case when using this would be a good idea, but here it is just as an example.
In response to Karim79, I just thought I'd toss out my function version of that answer. I've had to do it several times so this is what I wrote:
function getClassStyles(parentElem, selector, style){
elemstr = '<div '+ selector +'></div>';
var $elem = $(elemstr).hide().appendTo(parentElem);
val = $elem.css(style);
$elem.remove();
return val;
}
val = getClassStyles('.container:first', 'class="title"', 'margin-top');
console.warn(val);
This example assumes you have and element with class="container" and you're looking for the margin-top style of the title class in that element. Of course change up to fit your needs.
In the stylesheet:
.container .title{ margin-top:num; }
Let me know what you think - Would you modify it, and if so how? Thanks!
I have written a helper function that accepts an object with the css attributes to be retrieved from the given css class and fills in the actual css attribute values.
Example is included.
function getStyleSheetValues(colScheme) {
var tags='';
var obj= colScheme;
// enumerate css classes from object
for (var prop in obj) {
if (obj.hasOwnProperty(prop) && typeof obj[prop]=="object") {
tags+= '<span class="'+prop+'"></span>';
}
}
// generate an object that uses the given classes
tags= $('<div>'+tags+'</div>').hide().appendTo("body");
// read the class properties from the generated object
var idx= 0;
for (var prop in obj) {
if (obj.hasOwnProperty(prop) && typeof obj[prop]=="object") {
var nobj= obj[prop];
for (var nprop in nobj) {
if (nobj.hasOwnProperty(nprop) && typeof(nobj[nprop])=="string") {
nobj[nprop]= tags.find("span:eq("+idx+")").css(nobj[nprop]);
}
}
idx++;
}
}
tags.remove();
}
// build an object with css class names where each class name contains one
// or more properties with an arbitrary name and the css attribute name as its value.
// This value will be replaced by the actual css value for the respective class.
var colorScheme= { chart_wall: {wallColor:'background-color',wallGrid:'color'}, chart_line1: { color:'color'} };
$(document).ready(function() {
getStyleSheetValues(colorScheme);
// debug: write the property values to the console;
if (window.console) {
var obj= colorScheme;
for (var prop in obj) {
if (obj.hasOwnProperty(prop) && typeof obj[prop]=="object") {
var nobj= obj[prop];
for (var nprop in nobj) {
if (nobj.hasOwnProperty(nprop)) {
console.log(prop+'.'+nprop +':'+ nobj[nprop]);
}
}
}
}
// example of how to read an individual css attribute value
console.log('css value for chart_wall.wallGrid: '+colorScheme.chart_wall.wallGrid);
}
});
I wrote this js function, seems to be working for nested classes as well:
usage:
var style = get_css_property('.container-class .sub-container-class .child-class', 'margin');
console.log('style');
function get_css_property(class_name, property_name){
class_names = class_name.split(/\s+/);
var container = false;
var child_element = false;
for (var i = class_names.length - 1; i >= 0; i--) {
if(class_names[i].startsWith('.'))
class_names[i] = class_names[i].substring(1);
var new_element = $.parseHTML('<div class="' + class_names[i] + '"></div>');
if(!child_element)
child_element = new_element;
if(container)
$(new_element).append(container);
container = new_element;
}
$(container).hide().appendTo('body');
var style = $(child_element).css(property_name);
$(container).remove();
return style;
}

Categories