Find the "Greatest Common Div" of user selection - javascript

Hello all you ridiculously genius people!
So I'm creating a basic word processor, and I need to have an option to clear formatting in a certain section. The place the user is typing is split up into several different divs, and whenever they hit enter it starts a new div. When they add a style option (Bold, italics, etc.) it ads the correct html tag.
Well, I want the user to be able to clear all formatting on text they highlight. To do this, I need to find the element that all of them share. so if it displayed "Here is my text" in a structure like this:
<div id="1">
<div>Here</div>
<div>is</div>
<div>some</div>
<div>text</div>
</div>
and the user highlighted all 4 words, I would need to computer to give me the div with the id "1" because that is what they all share in common.
I am completely stumped on this, and have absolutely no clue where to start, so I really don't have any code for you. I will be using window.getSelected(); to get the actual text, but I have no idea how to go about finding the divs around it.
Thanks for your help!

If you are using window.getSelection(), it returns a selection object which contains properties .anchorNode and .focusNode which are the nodes at the start and end of the selection. You can then use those nodes to walk up the parent chain to find any level of parent you want.
The closest common parent could be found by getting the immediate parent of the first object and then seeing if it is also a parent of the second object. If not, go up one parent higher until you eventually find a common parent.
function findCommonParentElement(startNode, endNode) {
// see if node has a particular parent
// by walking up the parent chain and comparing parents
function hasParent(node, parent) {
while (node && node !== parent && node !== document.body) {
node = node.parentNode;
}
return node === parent;
}
// for text nodes, get the containing element
// for elements, just return what was passed in
function getElement(node) {
while (node && node.nodeType !== 1) {
node = node.parentNode;
}
return node;
}
// go up the parent chain of endNode looking for a node
// that startNode has as a parent
while (endNode && !hasParent(startNode, endNode)) {
endNode = endNode.parentNode;
}
// return the containing element - so it won't return a textnode
return getElement(endNode);
}
// Usage:
var sel = window.getSelection();
var commonParent = findCommonParentElement(sel.anchorNode, sel.focusNode);
Working demo: http://jsfiddle.net/jfriend00/GV96w/

Related

Javascript get all first children elements [duplicate]

I'm writing a Chrome content script extension and I need to be able to target a specific element that, unfortunately, has no unique identifiers except its parent element.
I need to target the immediate first child element of parentElement. console.log(parentElement) reports both of the child elements/nodes perfectly, but the succeeding console logs (the ones that target the childNodes) always return an undefined value no matter what I do.
This is my code so far
(I have excluded the actual names to avoid confusion and extra, unnecessary explanation)
function injectCode() {
var parentElement = document.getElementsByClassName("uniqueClassName");
if (parentElement && parentElement.innerHTML != "") {
console.log(parentElement);
console.log(parentElement.firstElementChild);
console.log(parentElement.firstChild);
console.log(parentElement.childNodes);
console.log(parentElement.childNodes[0]);
console.log(parentElement.childNodes[1]);
} else {
setTimeout(injectCode, 250);
}
}
How do I select the first child element/node of parentElement?
Update:
parentElement.children[0] also has the same error as parentElement.childNodes[0].
Both these will give you the first child node:
console.log(parentElement.firstChild); // or
console.log(parentElement.childNodes[0]);
If you need the first child that is an element node then use:
console.log(parentElement.children[0]);
Edit
Ah, I see your problem now; parentElement is an array.
If you know that getElementsByClassName will only return one result, which it seems you do, you should use [0] to dearray (yes, I made that word up) the element:
var parentElement = document.getElementsByClassName("uniqueClassName")[0];

Comparing a parent and a child node for similarity

Given the following structure
<p>
<p>
<span>
<span class="a">
<p>
I want to turn it into
<p>
<span>
<span class="a">
Yes, the first block is invalid, we'll ignore that. It's just an example.
Basically, what I want to do is check if any child is necessary, and if not, remove it, keeping all of its children. So, all <p>'s are identical, straight elements, therefore only the top one is really doing anything (in my code, I realize that's not always the case). However, the spans, though identical in name, are not the same, as one has class="a" and the other has no class, thus they both stay.
The expansion of what I'm looking for would not just work if class name differs, but any of the properties that might make it actually different.
I was thinking that I could use node1.attributes === node2.attributes, but that doesn't work, even though node1 and node2 attributes have a length of zero. Furthermore, node1 === node2, node1.isEqualNode(node2), and node1.isSameNode(node2) all fail as well. And rightly so, as they're not the same node.
So how can I rightly check to see if the node is eligible for removal?
Another example, of when this would actually be useful
<p>
<b>
Some text
<u>that is
<b>bolded</b> <!-- << That <b> is totally useless, and should be removed. -->
</u>
</b>
</p>
No Jquery please.
This is what I ended up with:
it = doc.createNodeIterator(doc.firstChild, NodeFilter.SHOW_ALL, null, false);
node = it.nextNode();
while(node){
// Remove any identical children
// Get the node we can move around. We need to go to the parent first, or everything will be true
var tempNode = node.parentNode;
// While we haven't hit the top - This checks for identical children
while(tempNode && tempNode !== div && tempNode !== doc){
// If they're the same name, which isn't not on the noDel list, and they have the same number of attributes
if(node.nodeName === tempNode.nodeName && noDel.indexOf(node.nodeName) < 0 && node.attributes.length === tempNode.attributes.length){
// Set flag that is used to determine if we want to remove or not
var remove = true;
// Loop through all the attributes
for(var i = 0; i < node.attributes.length; ++i){
// If we find a mismatch, make the flag false and leave
if(node.attributes[i] !== tempNode.attributes[i]){
remove = false;
break;
}
}
// If we want to remove it
if(remove){
// Create a new fragment
var frag = doc.createDocumentFragment();
// Place all of nodes children into the fragment
while(node.firstChild){
frag.appendChild(node.firstChild);
}
// Replace the node with the fragment
node.parentNode.replaceChild(frag, node);
}
}
// Otherwise, look at the next parent
tempNode = tempNode.parentNode;
}
// Next node
node = it.nextNode();
}
Oh this would be an interesting interview question!
Regarding the solution- make a function to traverse the DOM any which way you like, which receives a predicate function as an argument. Then collect an array of items which pass the predicate, and do your 'fixing' afterwards.
In your case, the predicate is obviously some isChildNecessary function, which you will need to implement.
How would you plan on handling a second pass, and a third, etc? When do you stop? After removing some nodes, you may end up having another 'invalid' state.. Just something to think about.

How to limit Javascript's window.find to a particular DIV?

Is it possible to use Javascript in Safari/Firefox/Chrome to search a particular div container for a given text string. I know you can use window.find(str) to search the entire page but is it possible to limit the search area to the div only?
Thanks!
Once you look up your div (which you might do via document.getElementById or any of the other DOM functions, various specs here), you can use either textContent or innerText to find the text of that div. Then you can use indexOf to find the string in that.
Alternately, at a lower level, you can use a recursive function to search through all text nodes in the window, which sounds a lot more complicated than it is. Basically, starting from your target div (which is an Element), you can loop through its childNodes and search their nodeValue string (if they're Texts) or recurse into them (if they're Elements).
The trick is that a naive version would fail to find "foo" in this markup:
<p><span>fo</span>o</p>
...since neither of the two Text nodes there has a nodeValue with "foo" in it (one of them has "fo", the other "o").
Depending on what you are trying to do, there is an interesting way of doing this that does work (does require some work).
First, searching starts at the location where the user last clicked. So to get to the correct context, you can force a click on the div. This will place the internal pointer at the beginning of the div.
Then, you can use window.find as usual to find the element. It will highlight and move toward the next item found. You could create your own dialog and handle the true or false returned by find, as well as check the position. So for example, you could save the current scroll position, and if the next returned result is outside of the div, you restore the scroll. Also, if it returns false, then you can say there were no results found.
You could also show the default search box. In that case, you would be able to specify the starting position, but not the ending position because you lose control.
Some example code to help you get started. I could also try putting up a jsfiddle if there is enough interest.
Syntax:
window.find(aStringToFind, bCaseSensitive, bBackwards, bWrapAround, bWholeWord, bSearchInFrames, bShowDialog);
For example, to start searching inside of myDiv, try
document.getElementById("myDiv").click(); //Place cursor at the beginning
window.find("t", 0, 0, 0, 0, 0, 0); //Go to the next location, no wrap around
You could set a blur (lose focus) event handler to let you know when you leave the div so you can stop the search.
To save the current scroll position, use document.body.scrollTop. You can then set it back if it trys to jump outside of the div.
Hope this helps!
~techdude
As per the other answer you won't be able to use the window.find functionality for this. The good news is, you won't have to program this entirely yourself, as there nowadays is a library called rangy which helps a lot with this. So, as the code itself is a bit too much to copy paste into this answer I will just refer to a code example of the rangy library that can be found here. Looking in the code you will find
searchScopeRange.selectNodeContents(document.body);
which you can replace with
searchScopeRange.selectNodeContents(document.getElementById("content"));
To search only specifically in the content div.
If you are still looking for someting I think I found a pretty nice solution;
Here it is : https://www.aspforums.net/Threads/211834/How-to-search-text-on-web-page-similar-to-CTRL-F-using-jQuery/
And I'm working on removing jQuery (wip) : codepen.io/eloiletagant/pen/MBgOPB
Hope it's not too late :)
You can make use of Window.find() to search for all occurrences in a page and Node.contains() to filter out unsuitable search results.
Here is an example of how to find and highlight all occurrences of a string in a particular element:
var searchText = "something"
var container = document.getElementById("specificContainer");
// selection object
var sel = window.getSelection()
sel.collapse(document.body, 0)
// array to store ranges found
var ranges = []
// find all occurrences in a page
while (window.find(searchText)) {
// filter out search results outside of a specific element
if (container.contains(sel.anchorNode)){
ranges.push(sel.getRangeAt(sel.rangeCount - 1))
}
}
// remove selection
sel.collapseToEnd()
// Handle ranges outside of the while loop above.
// Otherwise Safari freezes for some reason (Chrome doesn't).
if (ranges.length == 0){
alert("No results for '" + searchText + "'")
} else {
for (var i = 0; i < ranges.length; i++){
var range = ranges[i]
if (range.startContainer == range.endContainer){
// Range includes just one node
highlight(i, range)
} else {
// More complex case: range includes multiple nodes
// Get all the text nodes in the range
var textNodes = getTextNodesInRange(
range.commonAncestorContainer,
range.startContainer,
range.endContainer)
var startOffset = range.startOffset
var endOffset = range.endOffset
for (var j = 0; j < textNodes.length; j++){
var node = textNodes[j]
range.setStart(node, j==0? startOffset : 0)
range.setEnd(node, j==textNodes.length-1?
endOffset : node.nodeValue.length)
highlight(i, range)
}
}
}
}
function highlight(index, range){
var newNode = document.createElement("span")
// TODO: define CSS class "highlight"
// or use <code>newNode.style.backgroundColor = "yellow"</code> instead
newNode.className = "highlight"
range.surroundContents(newNode)
// scroll to the first match found
if (index == 0){
newNode.scrollIntoView()
}
}
function getTextNodesInRange(rootNode, firstNode, lastNode){
var nodes = []
var startNode = null, endNode = lastNode
var walker = document.createTreeWalker(
rootNode,
// search for text nodes
NodeFilter.SHOW_TEXT,
// Logic to determine whether to accept, reject or skip node.
// In this case, only accept nodes that are between
// <code>firstNode</code> and <code>lastNode</code>
{
acceptNode: function(node) {
if (!startNode) {
if (firstNode == node){
startNode = node
return NodeFilter.FILTER_ACCEPT
}
return NodeFilter.FILTER_REJECT
}
if (endNode) {
if (lastNode == node){
endNode = null
}
return NodeFilter.FILTER_ACCEPT
}
return NodeFilter.FILTER_REJECT
}
},
false
)
while(walker.nextNode()){
nodes.push(walker.currentNode);
}
return nodes;
}
For the Range object, see https://developer.mozilla.org/en-US/docs/Web/API/Range.
For the TreeWalker object, see https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
var elements = [];
$(document).find("*").filter(function () {
if($(this).text().contains(yourText))
elements.push($(this));
});
console.log(elements);
I didn't try it, but according the jQuery documentation it should work.
Here is how I am doing with jquery:
var result = $('#elementid').text().indexOf('yourtext') > -1
it will return true or false
Maybe you are trying to not use jquery...but if not, you can use this $('div:contains(whatyouarelookingfor)') the only gotcha is that it could return parent elements that also contain the child div that matches.

What kind of arguments does this function take?

function countChars(elm) {
if (elm.nodeType == 3) { // TEXT_NODE
return elm.nodeValue.length;
}
var count = 0;
for (var i = 0, child; child = elm.childNodes[i]; i++) {
count += countChars(child);
}
return count;
}
I tried passing this function a string like countChars("hello"); but that didn't work. What are examples of elements that I can pass?
It expects a reference to a DOM node. That could be a text node (nodeType == 3) or an element node (nodeType == 1) by the look of the function. For example:
countChars(document.getElementById("someId"));
The following HTML would cause the above call to return 5:
<span id="someId">Hello</span>
If the argument is a text node the function returns the number of characters that make up that node. If the argument is an element node the function recursively counts the number of characters that make up the descendant text nodes of the element.
The following HTML would cause the above call to return 10 (the child node is included):
<div id="someId">Outer<span>inner</span></div>
You can see the full list of node types on MDN.
It expects DOM nodes, like something you'd get from document.getElementById() or the list of child nodes on another DOM node.
What it does is find all the text nodes in the list of child nodes of an element and count up how many characters they contain. It does it recursively, so it will find all the text within a container, no matter how far separated in the element graph.
Also it should be noted that this code expects a node to be either a text node or else an element. There are other types of nodes, however, most notably comment nodes.
Looks like you should pass a dom element.
An element (node, actually, hence why it checks if nodeType is a text node), e.g.:
countChars(document.getElementById("myid"));
You can get nodeType over a DOM fragment, so... you must enter a DOM element as input
https://developer.mozilla.org/en/nodeType
The example from that link shows it
var node = document.documentElement.firstChild;
if(node.nodeType != Node.COMMENT_NODE)
alert("You should comment your code well!");

Copying children elements of one division to another along with all associated events

I am trying to replace a certain div element parent with another one newparent. I want to copy only some of parent's children and put them in newparent, and then replace the parent by newparent.
Here is a snippet of my code:
var sb_button = parent.firstChild;
var temp;
while(sb_button) {
console.log("loop: ");
console.log(sb_button.id);
temp = sb_button;
if(sb_button.id != curr_button.id && sb_button.id != prev_button.id) {
console.log("if");
newparent.appendChild(temp);
}
else if(sb_button.id == curr_button.id) {
console.log("elseif");
newparent.appendChild(temp);
newparent.appendChild(prev_button);
}
else {
console.log("else");
}
sb_button.parentNode = parent;
console.log(sb_button.id)
console.log(sb_button.parentNode.children);
sb_button = sb_button.nextSibling;
}
parent.parentNode.replaceChild(newparent,parent);
EDIT :
So when I do newparent.appendChild(temp) it modifies sb_button. What's the workaround for this?
I haven't run your code, but there's a few weird things, perhaps one of which may cause the issue or help clear up the code so the issue is more obvious.
the variable temp seems to be an alias for sb_button: you could remove the variable declaration and replace all references with temp
sb_button is a confusing name for an arbitrary child node
you're appending the node in sb_button to newparent within the if statement, but right after you're trying to set sb_button_.parentNode to parent - that's not possible since parentNode is readonly and it certainly doesn't make sense - you can't append the element to one element but have a different parent.
are you trying to copy or move nodes?
Edit: given that you want to copy nodes, I believe you're looking for cloneNode: make a copy of the node and append that copy, not the original node.
As a matter of clean design, when things get complicated, I'd avoid this kind of hard-to-reason-about while loop. Instead, simply make an array of the nodes, order those the way you want (you could even do this using sort to make it immediately obvious you're just rearranging things), and then make a function that takes a newparent and the array and appends copies of all elements in array order to newparent. Your example isn't that complex, but even here, I'd change the order of the if-clauses to have the "default" case in the final else. e.g.:
for(var child = parent.firstChild; child; child = child.nextSibling)
if(child.id == curr_button.id) { //insert prev_button after curr_button
newparent.appendChild(child.cloneNode(true));
newparent.appendChild(prev_button.cloneNode(true));
} else if(child.id != prev_button.id) {
newparent.appendChild(child.cloneNode(true));
}
parent.parentNode.replaceChild(newparent, parent);
The idea being to make it instantly obvious to the reader that all children are processed exactly once.

Categories