/loggerhead/trunk

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/loggerhead/trunk

« back to all changes in this revision

Viewing changes to loggerhead/static/javascript/yui/build/dom/selector.js

  • Committer: Martin Albisetti
  • Date: 2008-10-20 21:18:06 UTC
  • mfrom: (229.1.3 add-yui-3)
  • Revision ID: martin.albisetti@canonical.com-20081020211806-rzs0ya40gz9wcpoz
Added yui library to the tree. Welcome to the future. (Paul Hummer)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*
 
2
Copyright (c) 2008, Yahoo! Inc. All rights reserved.
 
3
Code licensed under the BSD License:
 
4
http://developer.yahoo.net/yui/license.txt
 
5
version: 3.0.0pr1
 
6
*/
 
7
YUI.add('selector', function(Y) {
 
8
 
 
9
/**
 
10
 * Provides helper methods for collecting and filtering DOM elements.
 
11
 * @module dom
 
12
 * @submodule selector
 
13
 */
 
14
 
 
15
/**
 
16
 * Provides helper methods for collecting and filtering DOM elements.
 
17
 * @class Selector
 
18
 * @static
 
19
 */
 
20
 
 
21
var TAG = 'tag',
 
22
    PARENT_NODE = 'parentNode',
 
23
    PREVIOUS_SIBLING = 'previousSibling',
 
24
    LENGTH = 'length',
 
25
    NODE_TYPE = 'nodeType',
 
26
    TAG_NAME = 'tagName',
 
27
    ATTRIBUTES = 'attributes',
 
28
    PSEUDOS = 'pseudos',
 
29
    COMBINATOR = 'combinator';
 
30
 
 
31
var reNth = /^(?:([\-]?\d*)(n){1}|(odd|even)$)*([\-+]?\d*)$/;
 
32
 
 
33
var patterns = {
 
34
    tag: /^((?:-?[_a-z]+[\w\-]*)|\*)/i,
 
35
    attributes: /^\[([a-z]+\w*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
 
36
    pseudos: /^:([\-\w]+)(?:\(['"]?(.+)['"]?\))*/i,
 
37
    combinator: /^\s*([>+~]|\s)\s*/
 
38
};
 
39
 
 
40
var Selector = {
 
41
    /**
 
42
     * Default document for use queries 
 
43
     * @property document
 
44
     * @type object
 
45
     * @default window.document
 
46
     */
 
47
    document: Y.config.doc,
 
48
    /**
 
49
     * Mapping of attributes to aliases, normally to work around HTMLAttributes
 
50
     * that conflict with JS reserved words.
 
51
     * @property attrAliases
 
52
     * @type object
 
53
     */
 
54
    attrAliases: {},
 
55
 
 
56
    /**
 
57
     * Mapping of shorthand tokens to corresponding attribute selector 
 
58
     * @property shorthand
 
59
     * @type object
 
60
     */
 
61
    shorthand: {
 
62
        '\\#(-?[_a-z]+[-\\w]*)': '[id=$1]',
 
63
        '\\.(-?[_a-z]+[-\\w]*)': '[class~=$1]'
 
64
    },
 
65
 
 
66
    /**
 
67
     * List of operators and corresponding boolean functions. 
 
68
     * These functions are passed the attribute and the current node's value of the attribute.
 
69
     * @property operators
 
70
     * @type object
 
71
     */
 
72
    operators: {
 
73
        '=': function(attr, val) { return attr === val; }, // Equality
 
74
        '!=': function(attr, val) { return attr !== val; }, // Inequality
 
75
        '~=': function(attr, val) { // Match one of space seperated words 
 
76
            var s = ' ';
 
77
            return (s + attr + s).indexOf((s + val + s)) > -1;
 
78
        },
 
79
        '|=': function(attr, val) { return Y.DOM._getRegExp('^' + val + '[-]?').test(attr); }, // Match start with value followed by optional hyphen
 
80
        '^=': function(attr, val) { return attr.indexOf(val) === 0; }, // Match starts with value
 
81
        '$=': function(attr, val) { return attr.lastIndexOf(val) === attr[LENGTH] - val[LENGTH]; }, // Match ends with value
 
82
        '*=': function(attr, val) { return attr.indexOf(val) > -1; }, // Match contains value as substring 
 
83
        '': function(attr, val) { return attr; } // Just test for existence of attribute
 
84
    },
 
85
 
 
86
    /**
 
87
     * List of pseudo-classes and corresponding boolean functions. 
 
88
     * These functions are called with the current node, and any value that was parsed with the pseudo regex.
 
89
     * @property pseudos
 
90
     * @type object
 
91
     */
 
92
    pseudos: {
 
93
        'root': function(node) {
 
94
            return node === node.ownerDocument.documentElement;
 
95
        },
 
96
 
 
97
        'nth-child': function(node, val) {
 
98
            return Selector.getNth(node, val);
 
99
        },
 
100
 
 
101
        'nth-last-child': function(node, val) {
 
102
            return Selector.getNth(node, val, null, true);
 
103
        },
 
104
 
 
105
        'nth-of-type': function(node, val) {
 
106
            return Selector.getNth(node, val, node[TAG_NAME]);
 
107
        },
 
108
         
 
109
        'nth-last-of-type': function(node, val) {
 
110
            return Selector.getNth(node, val, node[TAG_NAME], true);
 
111
        },
 
112
         
 
113
        'first-child': function(node) {
 
114
            return Y.DOM.firstChild(node[PARENT_NODE]) === node;
 
115
        },
 
116
 
 
117
        'last-child': function(node) {
 
118
            return Y.DOM.lastChild(node[PARENT_NODE]) === node;
 
119
        },
 
120
 
 
121
        'first-of-type': function(node, val) {
 
122
            return Y.DOM.firstChildByTag(node[PARENT_NODE], node[TAG_NAME]) === node;
 
123
        },
 
124
         
 
125
        'last-of-type': function(node, val) {
 
126
            return Y.DOM.lastChildByTag(node[PARENT_NODE], node[TAG_NAME]) === node;
 
127
        },
 
128
         
 
129
        'only-child': function(node) {
 
130
            var children = Y.DOM.children(node[PARENT_NODE]);
 
131
            return children[LENGTH] === 1 && children[0] === node;
 
132
        },
 
133
 
 
134
        'only-of-type': function(node) {
 
135
            return Y.DOM.childrenByTag(node[PARENT_NODE], node[TAG_NAME])[LENGTH] === 1;
 
136
        },
 
137
 
 
138
        'empty': function(node) {
 
139
            return node.childNodes[LENGTH] === 0;
 
140
        },
 
141
 
 
142
        'not': function(node, simple) {
 
143
            return !Selector.test(node, simple);
 
144
        },
 
145
 
 
146
        'contains': function(node, str) {
 
147
            var text = node.innerText || node.textContent || '';
 
148
            return text.indexOf(str) > -1;
 
149
        },
 
150
        'checked': function(node) {
 
151
            return node.checked === true;
 
152
        }
 
153
    },
 
154
 
 
155
    /**
 
156
     * Test if the supplied node matches the supplied selector.
 
157
     * @method test
 
158
     *
 
159
     * @param {HTMLElement | String} node An id or node reference to the HTMLElement being tested.
 
160
     * @param {string} selector The CSS Selector to test the node against.
 
161
     * @return{boolean} Whether or not the node matches the selector.
 
162
     * @static
 
163
    
 
164
     */
 
165
    test: function(node, selector) {
 
166
        if (!node) {
 
167
            return false;
 
168
        }
 
169
 
 
170
        var groups = selector ? selector.split(',') : [];
 
171
        if (groups[LENGTH]) {
 
172
            for (var i = 0, len = groups[LENGTH]; i < len; ++i) {
 
173
                if ( Selector._testNode(node, groups[i]) ) { // passes if ANY group matches
 
174
                    return true;
 
175
                }
 
176
            }
 
177
            return false;
 
178
        }
 
179
        return Selector._testNode(node, selector);
 
180
    },
 
181
 
 
182
    /**
 
183
     * Filters a set of nodes based on a given CSS selector. 
 
184
     * @method filter
 
185
     *
 
186
     * @param {array} nodes A set of nodes/ids to filter. 
 
187
     * @param {string} selector The selector used to test each node.
 
188
     * @return{array} An array of nodes from the supplied array that match the given selector.
 
189
     * @static
 
190
     */
 
191
    filter: function(nodes, selector) {
 
192
        nodes = nodes || [];
 
193
 
 
194
        var result = Selector._filter(nodes, Selector._tokenize(selector)[0]);
 
195
        return result;
 
196
    },
 
197
 
 
198
    /**
 
199
     * Retrieves a set of nodes based on a given CSS selector. 
 
200
     * @method query
 
201
     *
 
202
     * @param {string} selector The CSS Selector to test the node against.
 
203
     * @param {HTMLElement | String} root optional An id or HTMLElement to start the query from. Defaults to Selector.document.
 
204
     * @param {Boolean} firstOnly optional Whether or not to return only the first match.
 
205
     * @return {Array} An array of nodes that match the given selector.
 
206
     * @static
 
207
     */
 
208
    query: function(selector, root, firstOnly) {
 
209
        var result = Selector._query(selector, root, firstOnly);
 
210
        return result;
 
211
    },
 
212
 
 
213
    _query: function(selector, root, firstOnly, deDupe) {
 
214
        var result =  (firstOnly) ? null : [];
 
215
        if (!selector) {
 
216
            return result;
 
217
        }
 
218
 
 
219
        root = root || Selector.document;
 
220
        var groups = selector.split(','); // TODO: handle comma in attribute/pseudo
 
221
 
 
222
        if (groups[LENGTH] > 1) {
 
223
            var found;
 
224
            for (var i = 0, len = groups[LENGTH]; i < len; ++i) {
 
225
                found = arguments.callee(groups[i], root, firstOnly, true);
 
226
                result = firstOnly ? found : result.concat(found); 
 
227
            }
 
228
            Selector._clearFoundCache();
 
229
            return result;
 
230
        }
 
231
 
 
232
        var tokens = Selector._tokenize(selector);
 
233
        var idToken = tokens[Selector._getIdTokenIndex(tokens)],
 
234
            nodes = [],
 
235
            node,
 
236
            id,
 
237
            token = tokens.pop() || {};
 
238
            
 
239
        if (idToken) {
 
240
            id = Selector._getId(idToken[ATTRIBUTES]);
 
241
        }
 
242
 
 
243
        // use id shortcut when possible
 
244
        if (id) {
 
245
            node = Selector.document.getElementById(id);
 
246
 
 
247
            if (node && (root[NODE_TYPE] === 9 || Y.DOM.contains(root, node))) {
 
248
                if ( Selector._testNode(node, null, idToken) ) {
 
249
                    if (idToken === token) {
 
250
                        nodes = [node]; // simple selector
 
251
                    } else {
 
252
                        root = node; // start from here
 
253
                    }
 
254
                }
 
255
            } else {
 
256
                return result;
 
257
            }
 
258
        }
 
259
 
 
260
        if (root && !nodes[LENGTH]) {
 
261
            nodes = root.getElementsByTagName(token[TAG]);
 
262
        }
 
263
 
 
264
        if (nodes[LENGTH]) {
 
265
            result = Selector._filter(nodes, token, firstOnly, deDupe); 
 
266
        }
 
267
        return result;
 
268
    },
 
269
 
 
270
    _filter: function(nodes, token, firstOnly, deDupe) {
 
271
        var result = firstOnly ? null : [];
 
272
 
 
273
        result = Y.DOM.filterElementsBy(nodes, function(node) {
 
274
            if (! Selector._testNode(node, '', token, deDupe)) {
 
275
                return false;
 
276
            }
 
277
 
 
278
            if (deDupe) {
 
279
                if (node._found) {
 
280
                    return false;
 
281
                }
 
282
                node._found = true;
 
283
                Selector._foundCache[Selector._foundCache[LENGTH]] = node;
 
284
            }
 
285
            return true;
 
286
        }, firstOnly);
 
287
 
 
288
        return result;
 
289
    },
 
290
 
 
291
    _testNode: function(node, selector, token, deDupe) {
 
292
        token = token || Selector._tokenize(selector).pop() || {};
 
293
        var ops = Selector.operators,
 
294
            pseudos = Selector.pseudos,
 
295
            prev = token.previous,
 
296
            i, len;
 
297
 
 
298
        if (!node[TAG_NAME] ||
 
299
            (token[TAG] !== '*' && node[TAG_NAME].toUpperCase() !== token[TAG]) ||
 
300
            (deDupe && node._found) ) {
 
301
            return false;
 
302
        }
 
303
 
 
304
        if (token[ATTRIBUTES][LENGTH]) {
 
305
            var attribute;
 
306
            for (i = 0, len = token[ATTRIBUTES][LENGTH]; i < len; ++i) {
 
307
                attribute = node.getAttribute(token[ATTRIBUTES][i][0], 2);
 
308
                if (attribute === undefined) {
 
309
                    attribute = node[token[ATTRIBUTES][i][0]];
 
310
                    if (attribute === undefined) {
 
311
                        return false;
 
312
                    }
 
313
                }
 
314
                if ( ops[token[ATTRIBUTES][i][1]] &&
 
315
                        !ops[token[ATTRIBUTES][i][1]](attribute, token[ATTRIBUTES][i][2])) {
 
316
                    return false;
 
317
                }
 
318
            }
 
319
        }
 
320
 
 
321
        if (token[PSEUDOS][LENGTH]) {
 
322
            for (i = 0, len = token[PSEUDOS][LENGTH]; i < len; ++i) {
 
323
                if (pseudos[token[PSEUDOS][i][0]] &&
 
324
                        !pseudos[token[PSEUDOS][i][0]](node, token[PSEUDOS][i][1])) {
 
325
                    return false;
 
326
                }
 
327
            }
 
328
        }
 
329
        return (prev && prev[COMBINATOR] !== ',') ?
 
330
                Selector.combinators[prev[COMBINATOR]](node, token) :
 
331
                true;
 
332
    },
 
333
 
 
334
 
 
335
    _foundCache: [],
 
336
    _regexCache: {},
 
337
 
 
338
    _clearFoundCache: function() {
 
339
        for (var i = 0, len = Selector._foundCache[LENGTH]; i < len; ++i) {
 
340
            try { // IE no like delete
 
341
                delete Selector._foundCache[i]._found;
 
342
            } catch(e) {
 
343
                Selector._foundCache[i].removeAttribute('_found');
 
344
            }
 
345
        }
 
346
        Selector._foundCache = [];
 
347
    },
 
348
 
 
349
    combinators: {
 
350
        ' ': function(node, token) {
 
351
            while ((node = node[PARENT_NODE])) {
 
352
                if (Selector._testNode(node, '', token.previous)) {
 
353
                    return true;
 
354
                }
 
355
            }  
 
356
            return false;
 
357
        },
 
358
 
 
359
        '>': function(node, token) {
 
360
            return Selector._testNode(node[PARENT_NODE], null, token.previous);
 
361
        },
 
362
        '+': function(node, token) {
 
363
            var sib = node[PREVIOUS_SIBLING];
 
364
            while (sib && sib[NODE_TYPE] !== 1) {
 
365
                sib = sib[PREVIOUS_SIBLING];
 
366
            }
 
367
 
 
368
            if (sib && Selector._testNode(sib, null, token.previous)) {
 
369
                return true; 
 
370
            }
 
371
            return false;
 
372
        },
 
373
 
 
374
        '~': function(node, token) {
 
375
            var sib = node[PREVIOUS_SIBLING];
 
376
            while (sib) {
 
377
                if (sib[NODE_TYPE] === 1 && Selector._testNode(sib, null, token.previous)) {
 
378
                    return true;
 
379
                }
 
380
                sib = sib[PREVIOUS_SIBLING];
 
381
            }
 
382
 
 
383
            return false;
 
384
        }
 
385
    },
 
386
 
 
387
 
 
388
    /*
 
389
        an+b = get every _a_th node starting at the _b_th
 
390
        0n+b = no repeat ("0" and "n" may both be omitted (together) , e.g. "0n+1" or "1", not "0+1"), return only the _b_th element
 
391
        1n+b =  get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
 
392
        an+0 = get every _a_th element, "0" may be omitted 
 
393
    */
 
394
    getNth: function(node, expr, tag, reverse) {
 
395
        reNth.test(expr);
 
396
 
 
397
        var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
 
398
            n = RegExp.$2, // "n"
 
399
            oddeven = RegExp.$3, // "odd" or "even"
 
400
            b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
 
401
            op, i, len, siblings;
 
402
 
 
403
        if (tag) {
 
404
            siblings = Y.DOM.childrenByTag(node[PARENT_NODE], tag);
 
405
        } else {
 
406
            siblings = Y.DOM.children(node[PARENT_NODE]);
 
407
        }
 
408
 
 
409
        if (oddeven) {
 
410
            a = 2; // always every other
 
411
            op = '+';
 
412
            n = 'n';
 
413
            b = (oddeven === 'odd') ? 1 : 0;
 
414
        } else if ( isNaN(a) ) {
 
415
            a = (n) ? 1 : 0; // start from the first or no repeat
 
416
        }
 
417
 
 
418
        if (a === 0) { // just the first
 
419
            if (reverse) {
 
420
                b = siblings[LENGTH] - b + 1; 
 
421
            }
 
422
 
 
423
            if (siblings[b - 1] === node) {
 
424
                return true;
 
425
            } else {
 
426
                return false;
 
427
            }
 
428
 
 
429
        } else if (a < 0) {
 
430
            reverse = !!reverse;
 
431
            a = Math.abs(a);
 
432
        }
 
433
 
 
434
        if (!reverse) {
 
435
            for (i = b - 1, len = siblings[LENGTH]; i < len; i += a) {
 
436
                if ( i >= 0 && siblings[i] === node ) {
 
437
                    return true;
 
438
                }
 
439
            }
 
440
        } else {
 
441
            for (i = siblings[LENGTH] - b, len = siblings[LENGTH]; i >= 0; i -= a) {
 
442
                if ( i < len && siblings[i] === node ) {
 
443
                    return true;
 
444
                }
 
445
            }
 
446
        }
 
447
        return false;
 
448
    },
 
449
 
 
450
    _getId: function(attr) {
 
451
        for (var i = 0, len = attr[LENGTH]; i < len; ++i) {
 
452
            if (attr[i][0] == 'id' && attr[i][1] === '=') {
 
453
                return attr[i][2];
 
454
            }
 
455
        }
 
456
    },
 
457
 
 
458
    _getIdTokenIndex: function(tokens) {
 
459
        for (var i = 0, len = tokens[LENGTH]; i < len; ++i) {
 
460
            if (Selector._getId(tokens[i][ATTRIBUTES])) {
 
461
                return i;
 
462
            }
 
463
        }
 
464
        return -1;
 
465
    },
 
466
 
 
467
    /**
 
468
        Break selector into token units per simple selector.
 
469
        Combinator is attached to left-hand selector.
 
470
     */
 
471
    _tokenize: function(selector) {
 
472
        var token = {},     // one token per simple selector (left selector holds combinator)
 
473
            tokens = [],    // array of tokens
 
474
            found = false,  // whether or not any matches were found this pass
 
475
            match;          // the regex match
 
476
 
 
477
        selector = Selector._replaceShorthand(selector); // convert ID and CLASS shortcuts to attributes
 
478
 
 
479
        /*
 
480
            Search for selector patterns, store, and strip them from the selector string
 
481
            until no patterns match (invalid selector) or we run out of chars.
 
482
 
 
483
            Multiple attributes and pseudos are allowed, in any order.
 
484
            for example:
 
485
                'form:first-child[type=button]:not(button)[lang|=en]'
 
486
        */
 
487
        do {
 
488
            found = false; // reset after full pass
 
489
            for (var re in patterns) {
 
490
                if (patterns.hasOwnProperty(re)) {
 
491
                    if (re != TAG && re != COMBINATOR) { // only one allowed
 
492
                        token[re] = token[re] || [];
 
493
                    }
 
494
                    if ((match = patterns[re].exec(selector))) { // note assignment
 
495
                        found = true;
 
496
                        if (re != TAG && re != COMBINATOR) { // only one allowed
 
497
                            //token[re] = token[re] || [];
 
498
 
 
499
                            // capture ID for fast path to element
 
500
                            if (re === ATTRIBUTES && match[1] === 'id') {
 
501
                                token.id = match[3];
 
502
                            }
 
503
 
 
504
                            token[re].push(match.slice(1));
 
505
                        } else { // single selector (tag, combinator)
 
506
                            token[re] = match[1];
 
507
                        }
 
508
                        selector = selector.replace(match[0], ''); // strip current match from selector
 
509
                        if (re === COMBINATOR || !selector[LENGTH]) { // next token or done
 
510
                            token[ATTRIBUTES] = Selector._fixAttributes(token[ATTRIBUTES]);
 
511
                            token[PSEUDOS] = token[PSEUDOS] || [];
 
512
                            token[TAG] = token[TAG] ? token[TAG].toUpperCase() : '*';
 
513
                            tokens.push(token);
 
514
 
 
515
                            token = { // prep next token
 
516
                                previous: token
 
517
                            };
 
518
                        }
 
519
                    }
 
520
                }
 
521
            }
 
522
        } while (found);
 
523
 
 
524
        return tokens;
 
525
    },
 
526
 
 
527
    _fixAttributes: function(attr) {
 
528
        var aliases = Selector.attrAliases;
 
529
        attr = attr || [];
 
530
        for (var i = 0, len = attr[LENGTH]; i < len; ++i) {
 
531
            if (aliases[attr[i][0]]) { // convert reserved words, etc
 
532
                attr[i][0] = aliases[attr[i][0]];
 
533
            }
 
534
            if (!attr[i][1]) { // use exists operator
 
535
                attr[i][1] = '';
 
536
            }
 
537
        }
 
538
        return attr;
 
539
    },
 
540
 
 
541
    _replaceShorthand: function(selector) {
 
542
        var shorthand = Selector.shorthand;
 
543
        var attrs = selector.match(patterns[ATTRIBUTES]); // pull attributes to avoid false pos on "." and "#"
 
544
        if (attrs) {
 
545
            selector = selector.replace(patterns[ATTRIBUTES], 'REPLACED_ATTRIBUTE');
 
546
        }
 
547
        for (var re in shorthand) {
 
548
            if (shorthand.hasOwnProperty(re)) {
 
549
                selector = selector.replace(Y.DOM._getRegExp(re, 'gi'), shorthand[re]);
 
550
            }
 
551
        }
 
552
 
 
553
        if (attrs) {
 
554
            for (var i = 0, len = attrs[LENGTH]; i < len; ++i) {
 
555
                selector = selector.replace('REPLACED_ATTRIBUTE', attrs[i]);
 
556
            }
 
557
        }
 
558
        return selector;
 
559
    }
 
560
 
 
561
};
 
562
 
 
563
if (Y.UA.ie) { // rewrite class for IE (others use getAttribute('class')
 
564
    Selector.attrAliases['class'] = 'className';
 
565
    Selector.attrAliases['for'] = 'htmlFor';
 
566
}
 
567
 
 
568
Y.Selector = Selector;
 
569
Y.Selector.patterns = patterns;
 
570
 
 
571
 
 
572
 
 
573
}, '3.0.0pr1' ,{skinnable:false, requires:['dom-base']});