是否可以创建导航祖先的自定义 jQuery 选择器?例如:closest 或 :parents 选择器

Posted

技术标签:

【中文标题】是否可以创建导航祖先的自定义 jQuery 选择器?例如:closest 或 :parents 选择器【英文标题】:Is it possible to create custom jQuery selectors that navigate ancestors? e.g. a :closest or :parents selector 【发布时间】:2014-11-01 12:04:57 【问题描述】:

我编写了很多 jQuery 插件,并拥有我一直使用的自定义 jQuery 选择器,例如 :focusable:closeto 来提供常用的过滤器。

例如:focusable 看起来像这样

jQuery.extend(jQuery.expr[':'], 
    focusable: function (el, index, selector) 
        return $(el).is('a, button, :input[type!=hidden], [tabindex]');
    ;
);

和其他选择器一样使用:

$(':focusable').css('color', 'red');  // color all focusable elements red

我注意到没有可用的 jQuery 选择器可以导航回祖先。我认为这是因为它们被设计为遵循向下钻取的基本 CSS 选择器规则。

举个例子:它会为一个有焦点的输入找到标签:

$('input:focus').closest('.form-group').find('.label');

我需要插件的等效类型的复杂选择器,因此将这样的选择器提供为单个字符串会很有用(因此它们可以作为插件的选项提供)。

例如类似:

$('input:focus < .form-group .label');

$('input:focus:closest(.form-group) .label');

注意:请假设更复杂的操作,并且需要祖先导航(我意识到这个特定示例可以使用has 完成,但这没有帮助)。

例如它还需要支持这一点:

options.selector = ':closest(".form-group") .label';

$('input').click(function()
    var label = $(this).find(options.selector);
);

是否可以扩展 jQuery 选择器来扩展搜索行为(而不仅仅是添加更多布尔过滤器)?如何扩展自定义搜索行为?

更新:

看起来一个完整的自定义选择器(如&lt;)并不像在 jQuery 的 Sizzle 解析器中添加一个伪选择器那么简单。我目前正在查看此Sizzle documentation,但我发现与 jQuery 版本不一致。 (例如,运行时不存在 Sizzle.selectors.order 属性)。

作为参考,jQuery 将 Sizzle 存储在其 jQuery.find 属性上,并将 Sizzle.selectors 存储在其 jQuery.expr 属性上。

到目前为止,我已经添加了这个:

 jQuery.expr.match.closest = /^:(?:closest)$/;
 jQuery.expr.find.closest = function (match, context, isXML)
     console.log("jQuery.expr.find.closest");
 ;

并通过简单的测试调用它:http://jsfiddle.net/z3vwk1ko/2/

但它永远不会到达 console.log 语句,我仍然得到"Syntax error, unrecognized expression: unsupported pseudo: closest"。在 jQuery 内部进行跟踪时,它试图将其应用为 filter 而不是 find,所以我缺少一些关键部分。

更新 2:

选择器的处理从右到左(参见下面的 jQuery 1.11.1 摘录)所以如果上下文中不存在最后一个参数,它会提前中止。这意味着在我们想要在祖先的另一个 DOM 分支中查找元素的常见情况下,当前的 jQuery Sizzle 代码不会发生向上导航:

// Fetch a seed set for right-to-left matching
i = matchExpr["needsContext"].test(selector) ? 0 : tokens.length;
while (i--) 
    token = tokens[i];

    // Abort if we hit a combinator
    if (Expr.relative[(type = token.type)]) 
        break;
    
    if ((find = Expr.find[type])) 
        // Search, expanding context for leading sibling combinators
        if ((seed = find(
            token.matches[0].replace(runescape, funescape),
            rsibling.test(tokens[0].type) && testContext(context.parentNode) || context
        ))) 

            // If seed is empty or no tokens remain, we can return early
            tokens.splice(i, 1);
            selector = seed.length && toSelector(tokens);
            if (!selector) 
                push.apply(results, seed);
                return results;
            

            break;
        
    

看到这一点我很惊讶,但现在意识到它使规则引擎更容易编写。这确实意味着我们需要确保选择器的右端尽可能具体,因为它首先被评估。之后发生的所有事情都只是对结果集进行渐进式修剪(我一直认为选择器中的第一个项目必须更具体以提高效率)。

【问题讨论】:

$('input:focus:closest(.form-group) .label'); 应该很容易实现 @TrueBlueAussie 如果你制作了这样的插件,请分享它们,因为它将帮助包括我在内的其他人参与项目.. @Ehsan Sajjad:我提到的插件用于将普通的 Web 应用程序转换为 SPA(实际上使它们可以在移动设备和桌面设备上使用)。它们很可能在完成后被释放,但这个问题是关于简单的辅助扩展:) 我认为这是可能的,我记得我实现了一个选择器,您可以将 td 元素传递给它,它会返回 td 元素所在的整个列。如果您正在寻找类似的东西,请告诉我,我会添加更多详细信息的答案。 $('parent &gt; child') 在本机工作,所有 jQuery 要做的就是将它传递给支持它的浏览器的 querySelector$('child &lt; parent') 但是没有任何东西支持,所以你必须挂钩到选择器的解析。伪选择器要容易得多,将.closest() 转换为:closest() 应该不会很难。 【参考方案1】:

不可能,我会告诉你原因

为了创建自定义选择器,您应该需要这样的东西:

jQuery.extend(jQuery.expr[':'], 
  focusable: function (el, index, selector) 
    return $(el).is('a, button, :input[type!=hidden], [tabindex]');
  ;
)

选择器逻辑允许您从当前选择中选择元素的子集,例如,如果 $("div") 选择页面上的所有 div,那么,以下两个示例将选择所有没有子元素的 div:

$("div:empty")
$("div").filter(":empty")

根据 jQuery 允许您编写自定义选择器的方式,无法返回原始集合之外的元素,您只能在询问时为集合中的每个元素选择 true 或 false。例如,“:contains”选择器在 2.1.1 版本的 jQuery 上就是这样实现的:

function ( text ) 
        return function( elem ) 
            return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
        ;

但是…… (也许你想先看最后的编辑) 也许,您会发现给定元素的所有祖先的选择器很有用,例如,想象一下这种情况:

<element1>
  <div>
    <span id="myelementid"></span>
  </div>
  <div>
    <p>Lorem ipsum sir amet dolor... </p>
  </div>
</element1>
<element2>
</element2>

而且,你想要一些涉及 myelementid 的 div 父级、element1 和所有祖先等的东西...,你可以使用“has”选择器,这样:

$("*:has('#myelementid')")

第一个选择器将返回所有元素,由“has”过滤将返回所有具有#myelementid 作为后代的元素,因此,所有祖先 的#myelementid。 如果你担心性能(也许你应该),你需要用更具体的东西替换“*”,也许你正在搜索 div

$("div:has('#myelementid')")

或者对于给定的类

$(".agivenclass:has('#myelementid')")

希望对你有帮助

编辑

您可以使用 jquery 的 parents() “选择器”,因为它返回元素的所有祖先,甚至包括文档的根元素(即 html 标记),这样:

$('#myelementid').parents()

如果你想过滤某些元素(例如,div),你可以使用这个:

$('#myelementid').parents(null, "div")

或者这个:

$('#myelementid').parents().filter("div")

【讨论】:

我不能同意这是不可能的,只是认为这可能很困难。我并没有将解决方案限制为使用标准/简单的 jQuery 方式来实现自定义选择器(这只是应用过滤器的几行代码)。 Sizzle 由 jQuery 公开,允许其他解决方案。这可能涉及替换它的一些方法,或者在最坏的情况下,完全替换 Sizzle,但这项任务当然不是不可能的。谢谢 关于$("div:has('#myelementid')") 示例,目标元素(即#myelementid)需要是应用查找选择器的元素,因为它可能是动态的(例如,单击的元素等) 并且可能没有 id。然而,你给了我一些新的想法。 @adeneo 提供了一个有趣的 hack,证明它确实是可能的:jsfiddle.net/z3vwk1ko/4 现在只需要找到一种更简洁的方法 :) 这很容易解决,只是为了表明它可以做到。然而,它应该通过以某种方式连接到jQuery.fn.init,在调用new 之前检查选择器并拆分&lt; 等来完成。 这个答案根本没有真正回答任何问题,它只是在关于伪选择器如何工作的问题下突出显示 cmets 中已有的内容,顺便说一句,在 jQuery 中定义自定义表达式的方式已更改1.8,这里贴的方式是定义伪选择器的老方法?【参考方案2】:

基于大量的 cmets,以及关于为什么这是不可能的的详细解释,我突然想到,我想要的目标可以通过 $(document).find() 来实现,但有一些 目标元素。也就是说,在选择器中以某种方式定位原始查询元素。

为此,我想出了以下:this 选择器,它的工作原理是这样的(不是双关语):

// Find all labels under .level3 classes that have the .starthere class beneath them
$('.starthere').findThis('.level3:has(:this) .label')

这使我们现在可以有效地在单个选择器字符串中向上搜索 DOM,然后向下搜索到相邻的分支! 即它执行与此相同的工作(但在单个选择器中):

$('.starthere').parents('.level3').find('.label')

步骤:

1 - 添加一个新的jQuery.findThis 方法

2 - 如果选择器有 :this,则替换为 id 搜索并从 document 进行搜索

3 - 如果选择器不包含:this 进程,通常使用原始find

4 - 使用 $('.target').find('.ancestor:has(:this) .label') 之类的选择器进行测试,以在目标元素的祖先中选择标签

这是基于 cmets 的修订版本,它不会替换现有的 find 并使用生成的唯一 ID。

JSFiddle:http://jsfiddle.net/TrueBlueAussie/z3vwk1ko/36/

// Add findThis method to jQuery (with a custom :this check)
jQuery.fn.findThis = function (selector) 
    // If we have a :this selector
    if (selector.indexOf(':this') > 0) 
        var ret = $();
        for (var i = 0; i < this.length; i++) 
            var el = this[i];
            var id = el.id;
            // If not id already, put in a temp (unique) id
            el.id = 'id'+ new Date().getTime();
            var selector2 = selector.replace(':this', '#' + el.id);
            ret = ret.add(jQuery(selector2, document));
            // restore any original id
            el.id = id;
        
        ret.selector = selector;
        return ret;
    
    // do a normal find instead
    return this.find(selector);


// Test case
$(function () 
    $('.starthere').findThis('.level3:has(:this) .label').css(
        color: 'red'
    );
);

已知问题:

这会在没有id 属性开头的目标元素上留下一个空白的id 属性(这不会​​导致问题,但不像我想要的那样整洁)

1234563这段代码。

以下第一个版本:

1 - 保存 jQuery 的 find 方法以供参考

2 - 替换一个新的jQuery.find 方法

3 - 如果选择器有 :this,则替换为 id 搜索并从 document 进行搜索

4 - 如果选择器不包含:this 进程,通常使用原始find

5 - 使用 $('.target').find('.ancestor:has(:this)') 之类的选择器进行测试,以选择目标元素的祖先

JSFiddle:http://jsfiddle.net/TrueBlueAussie/z3vwk1ko/24/

// Save the original jQuery find we are replacing
jQuery.fn.findorig = jQuery.fn.find

// Replace jQuery find with a custom :this hook
jQuery.fn.find = function (selector) 
    // If we have a :this selector
    if (selector.indexOf(':this') > 0) 
        var self = this;
        var ret = $();
        for (var i = 0; i < this.length; i++) 
            // Save any existing id on the targetted element
            var id = self[i].id;
            if (!id) 
                // If not id already, put in a temp (unique) one
                self[i].id = 'findme123';
            
            var selector2 = selector.replace(':this', '#findme123');
            ret = ret.add(jQuery(selector2, document));
            // restore any original id
            self[i].id = id;
        
        ret.selector = selector;
        return ret;
    
    return this.findorig(selector);


// Test case
$(function () 
   $('.starthere').find('.level3:has(:this)').css(
        color: 'red'
    );
);

这是基于对 jQuery/Sizzle 源代码的 6 小时奴役,所以要温柔。总是很高兴听到改进这个替换 find 的方法,因为我是 jQuery 内部的新手 :)

现在意味着我可以解决如何做原始标签示例的初始问题了:

options.selector = ".form-group:has(:this) .label";

$('input').click(function()
    var label = $(this).find(options.selector);
);

例如http://jsfiddle.net/TrueBlueAussie/z3vwk1ko/25/

【讨论】:

我不建议这样做,你在搞乱 jQuery 内部,甚至更改元素 ID!,你可能会破坏依赖 jQuery 的东西(例如引导程序),除非你准备好应对意外生产错误...我将发布另一个答案,避免这种黑客攻击,更接近您正在寻找的内容 替换find 可能是合理的,尽管引入一种新方法会更好。但是,使用任意 id findme123 是非常有问题的,应该用适当的 .parent(…).find(…) 调用来代替。 @dseminara:如果您检查代码,您将看到 id 立即恢复。您还会注意到,除非选择器包含 :this,否则它只会调用通常的查找代码。如果你能找到一个实际的问题,或者更好的解决方案,请列出来:) @Bergi:添加新方法可能更可取。 “找到这个”怎么样?开销是对所有其他发现的 indexOf 调用很小,但我同意你的观点。 id is 意味着是唯一的(因此它旁边的注释),但我没有时间添加生成的。将更正这些问题并发布更新。谢谢。 异常不安全,如果在恢复id之前发生异常,会一直报错:(

以上是关于是否可以创建导航祖先的自定义 jQuery 选择器?例如:closest 或 :parents 选择器的主要内容,如果未能解决你的问题,请参考以下文章

不为空的自定义数据属性上的 jQuery 选择器

我的自定义 UIView 没有显示在屏幕上

SwiftUI:可以访问祖先自定义`@EnvironmentObject`?如果是,如何?

带有身份检查器的自定义 Xcode 选择器

你写过哪些有用的自定义 jQuery 选择器?

使用 jQuery 和 CSS 选择器选择嵌套元素的共同父/祖先