是否可以创建导航祖先的自定义 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 选择器来扩展搜索行为(而不仅仅是添加更多布尔过滤器)?如何扩展自定义搜索行为?
更新:
看起来一个完整的自定义选择器(如<
)并不像在 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 > child')
在本机工作,所有 jQuery 要做的就是将它传递给支持它的浏览器的 querySelector
。 $('child < 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
之前检查选择器并拆分<
等来完成。
这个答案根本没有真正回答任何问题,它只是在关于伪选择器如何工作的问题下突出显示 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
属性(这不会导致问题,但不像我想要的那样整洁)
以下第一个版本:
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 选择器的主要内容,如果未能解决你的问题,请参考以下文章