开发者

How can I recurse up a DOM tree?

开发者 https://www.devze.com 2023-02-02 00:53 出处:网络
So I have a series of nested ul elements as part of a tree like below: <ul> <li> <ul> <li>1.1</li>

So I have a series of nested ul elements as part of a tree like below:

<ul>
<li>
    <ul>
        <li>1.1</li>
        <li>1开发者_如何学JAVA.2</li>
    </ul>
    <ul>
        <li>2.1</li>
        <li>
            <ul>
                <li>2.2</li>
            </ul>
        </li>
    </ul>
    <ul>
        <li>3.1</li>
        <li>3.2</li>
    </ul>
</li>
</ul>

Let's say when 3.1 is the selected node and when the user clicks previous the selected node should then be 2.2. The bad news is that there could be any number of levels deep. How can I find the previous node (li) in relationship to the currently selected node using jquery?


DOM has defined document traversal classes that allow for sequential processing of tree content. The TreeWalker, and NodeIterator classes for perfect candidates for this.

First we create a TreeWalker instance that can sequentially iterate through <li> nodes.

var walker = document.createTreeWalker(
    document.body, 
    NodeFilter.SHOW_ELEMENT,
    function(node) {
            var hasNoElements = node.getElementsByTagName('*').length == 0;
            return (node.nodeName == "LI" && hasNoElements) ? 
                        NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
    },
    false
);

Next we iterate up to the current node in this TreeWalker. Let's say we had a reference to our current node in myNode. We will traverse this walker until we reach myNode or a null value.

while(walker.nextNode() != myNode && walker.currentNode);

Having reached our node in this TreeWalker, getting the previous node is a piece of cake.

var previousNode = walker.previousNode();

You can try an example here.


Here is a generic version of a backwards tree walker. It's a tiny wrapper around the TreeWalker interface.

function backwardsIterator(startingNode, nodeFilter) {
    var walker = document.createTreeWalker(
        document.body, 
        NodeFilter.SHOW_ELEMENT,
        function(node) {
            return nodeFilter(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
        },
        false
    );

    walker.currentNode = startingNode;

    return walker;
}

It takes a starting node, and a custom filter function to remove unwanted elements. Here is an example usage of starting at a given node, and only including leaf li elements.

var startNode = document.getElementById("2.1.1");

// creates a backwards iterator with a given start node, and a function to filter out unwanted elements.
var iterator = backwardsIterator(startNode, function(node) {
    var hasNoChildElements = node.childElementCount == 0;
    var isListItemNode = node.nodeName == "LI";

    return isListItemNode && hasNoChildElements;
});

// Call previousNode() on the iterator to walk backwards by one node.
// Can keep calling previousNode() to keep iterating backwards until the beginning.
iterator.previousNode()

Updated with an interactive example. Tap on a list item to highlight the previous list item.


You can do this like this in jQuery:

http://jsfiddle.net/haP5c/

$('li').click(function() {
    var $this = $(this);

    if ($this.children().length == 0) {
        var $lis = $('#myUL').find('li'),
            indCount = 0,
            prevLI = null;

        $lis.each(function(ind, el) {
            if (el == $this[0]) {
                indCount = ind;
                return false;
            }
        });

        for (indCount=indCount-1; indCount >= 0; indCount--) {
            if ($($lis[indCount]).children().size() == 0) {
                prevLI = $lis[indCount];
                break;
            }
        }

        if (prevLI) {
            alert($(prevLI).text());
        }
    }
});

Basically, if you click an element, it'll search for the previous LI node that doesn't have any children. If it does, it considers it the previous one in the chain. As you can see on the jsfiddle, it works perfectly.


A rather simple approach that would work without any JavaScript-framework:

var lis = document.getElementsByTagName("li"), prev;

for (var i = 0; i < lis.length; i++) {
    if (lis[i] == myCurLI) break;
    prev = lis[i];
}

Now prev holds the previous LI.


EDIT: Please see my other answer for a solution using XPath that doesn't have the flaws mentioned by treeface.


Here's how to do it as a jQuery plug-in (licensed under the MIT License). Load it just after jQuery has loaded:

/*
 * jQuery Previous/Next by Tag Name Plugin.
 * Copyright (c) 2010 idealmachine
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

(function($) {
    $.fn.prevByTagName = function(containerSelector) {
        return this.map(function() {
            var container = containerSelector ? $(this).parents(containerSelector).last()[0] : document,
                others = container.getElementsByTagName(this.tagName),
                result = null;
            for(var i = 0; i < others.length; ++i) {
                if(others[i] === this) {
                    break;
                }
                result = others[i];
            }
            return result;
        });
    };
    $.fn.nextByTagName = function(containerSelector) {
        return this.map(function() {
            var container = containerSelector ? $(this).parents(containerSelector).last()[0] : document,
                others = container.getElementsByTagName(this.tagName),
                result = null;
            for(var i = others.length; i--;) {
                if(others[i] === this) {
                    break;
                }
                result = others[i];
            }
            return result;
        });
    };
})(jQuery);

// End of plugin

To find the last previous li element (the outermost ul having the id myUL) that has no ul children, you could use the plug-in on $(this) like this:

var result = $(this);
while((result = result.prevByTagName('#myUL')).children('ul').length);

You can try it out on jsFiddle.


Mildly "annoyed" by the fact that my simple approach had flaws, here's one solution with XPath:

$('li').click(function() {
    var lis = document.getElementsByTagName("li"), 
        prev = null,
        myCurLI = this;

    if ($(this).children().length == 0) {
        // collect all LI-elements that are leaf nodes
        var expr = "//li[count(child::*) = 0]", xp, cur;

        xp = document.evaluate(expr, document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);

        while (cur = xp.iterateNext()) {
            if (cur == myCurLI) break;
            prev = cur;
        }

        if (prev) alert($(prev).text());
        else alert("No previous list element found");
    }
});

NOTE: I copied the event handling code from treeface's answer as I'm not that familiar with that framework.

0

精彩评论

暂无评论...
验证码 换一张
取 消