jQueryのセレクタ文法は簡単に拡張できます。みんな知ってるね。
え?知らない?ココを参照すべし。
http://james.padolsey.com/javascript/extending-jquerys-selector-capabilities/
僕は、こんな風にして
(function($) { $.extend($.expr.pseudos, { 'all-parents': function(el, indx, args) { var cond = args[3]; for (var $parent = $(el).parent(); !$parent.is('html'); $parent = $parent.parent()) { if (!$parent.is(cond)) { return false; } } return true; } }); })(jQuery);
「全ての親要素がXXXを満たす」という擬似クラスを追加しています。
これにより、例えば
<section> <div class="a"> <div class="b"> <span class="c">NG</span> </div> </div> <div class="d"> <div class="b"> <span class="c">OK</span> </div> </div> </section>
に対して、
.b .c:all-parents(:not(.a))
と指定することで、OKを含むspanだけを選択できるようになるわけです。
本題
さて、親を取得するセレクタを実装できないか調べてみました。
例えばこんな感じ。
.c < .a
これは、クラスcを持つ要素の親で、クラスaを持つものを指します。
前述のものとの決定的な違いは、これはフィルタではなくコンビネータ*1だということです。
従って、上記のように擬似クラスでは実現できず、コンビネータの定義を追加する必要があります。
jQuery(というかSizzle)の実装を見てみる
言うまでもありませんが、jQueryのセレクタ実装の実体はSizzleです。みんな知っ(略)
ちなみに、jQueryのバージョン2.1.0での話。
さて、早速ですがコンビネータの定義について、まず注目すべきなのは1513行目の
Expr = Sizzle.selectors = { // ...(略) relative: { ">": { dir: "parentNode", first: true }, " ": { dir: "parentNode" }, "+": { dir: "previousSibling", first: true }, "~": { dir: "previousSibling" } }, // ...(略) }
です。ここで、コンビネータのトークンが宣言されています。たぶん。
次に、2562行目を見ると
jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; // ←ここに注目 jQuery.expr[":"] = jQuery.expr.pseudos; jQuery.unique = Sizzle.uniqueSort; jQuery.text = Sizzle.getText; jQuery.isXMLDoc = Sizzle.isXML; jQuery.contains = Sizzle.contains;
とのことなので、relativeをいじるには
// jQuery.expr === $.expr に注意 $.extend($.expr.relative, { "<": { dir: 'parentNode' } });
とすればよいはずです。
さて、ここまでは良いのですが、追加した"<"コンビネータに対して振る舞いを追加する方法がわかりません。
もう少しコードを掘ってみると、2239行目辺りの
function matcherFromTokens( tokens ) { var checkContext, matcher, j, len = tokens.length, leadingRelative = Expr.relative[ tokens[0].type ], implicitRelative = leadingRelative || Expr.relative[" "], i = leadingRelative ? 1 : 0, // ...(略)
に辿り着きます。どうも、tokensという引数がセレクタ文字列のパース結果のようです。
ではtokensは何処で作られているのかというと、1981行目のtokenizeという関数がその責務を負っているようです。
function tokenize( selector, parseOnly ) { var matched, match, tokens, type, soFar, groups, preFilters, cached = tokenCache[ selector + " " ]; if ( cached ) { return parseOnly ? 0 : cached.slice( 0 ); } soFar = selector; groups = []; preFilters = Expr.preFilter; while ( soFar ) { // Comma and first run if ( !matched || (match = rcomma.exec( soFar )) ) { if ( match ) { // Don't consume trailing commas as valid soFar = soFar.slice( match[0].length ) || soFar; } groups.push( (tokens = []) ); } matched = false; // Combinators if ( (match = rcombinators.exec( soFar )) ) { matched = match.shift(); tokens.push({ value: matched, // Cast descendant combinators to space type: match[0].replace( rtrim, " " ) }); soFar = soFar.slice( matched.length ); } // Filters for ( type in Expr.filter ) { if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || (match = preFilters[ type ]( match ))) ) { matched = match.shift(); tokens.push({ value: matched, type: type, matches: match }); soFar = soFar.slice( matched.length ); } } if ( !matched ) { break; } } // Return the length of the invalid excess // if we're just parsing // Otherwise, throw an error or return tokens return parseOnly ? soFar.length : soFar ? Sizzle.error( selector ) : // Cache the tokens tokenCache( selector, groups ).slice( 0 ); }
さて、このtokenizeですが、よく見てみると2008行目で
// Combinators if ( (match = rcombinators.exec( soFar )) )
とあるので、正規表現を用いてコンビネータをパースしていることがわかります。
ではこの定義は何処にあるのかというと、653行目で
rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" )
となっています。ならば、この定義を/[<>+~]/に置き換えれば…
…ん?
var Sizzle = /*! * Sizzle CSS Selector Engine v1.10.16 * http://sizzlejs.com/ * * Copyright 2013 jQuery Foundation, Inc. and other contributors * Released under the MIT license * http://jquery.org/license * * Date: 2014-01-13 */ (function( window ) { var i, support, Expr, getText, isXML, compile, outermostContext, sortInput, rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), // ...(略)
\(^o^)/オワタ
見ての通りrcombinatorは不可視なところで宣言されているため、どうにもなりませぬ。
他のところでどれだけ頑張ろうと、無常にも
> $(".b < .a") Error: Syntax error, unrecognized expression: .b < .a
と言われて先に進めないままです。