Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
YUI.add('datatable-keynav', function (Y, NAME) {
2
 
3
/**
4
 Provides keyboard navigation of DataTable cells and support for adding other
5
 keyboard actions.
6
 
7
 @module datatable
8
 @submodule datatable-keynav
9
*/
10
var arrEach = Y.Array.each,
11
 
12
/**
13
 A DataTable class extension that provides navigation via keyboard, based on
14
 WAI-ARIA recommendation for the [Grid widget](http://www.w3.org/WAI/PF/aria-practices/#grid)
15
 and extensible to support other actions.
16
 
17
 
18
 @class DataTable.KeyNav
19
 @for DataTable
20
*/
21
    DtKeyNav = function (){};
22
 
23
/**
24
Mapping of key codes to friendly key names that can be used in the
25
[keyActions](#property_keyActions) property and [ARIA_ACTIONS](#property_ARIA_ACTIONS)
26
property.
27
 
28
It contains aliases for the following keys:
29
    <ul>
30
    <li>backspace</li>
31
    <li>tab</li>
32
    <li>enter</li>
33
    <li>esc</li>
34
    <li>space</li>
35
    <li>pgup</li>
36
    <li>pgdown</li>
37
    <li>end</li>
38
    <li>home</li>
39
    <li>left</li>
40
    <li>up</li>
41
    <li>right</li>
42
    <li>down</li>
43
    <li>f1 .. f12</li>
44
    </ul>
45
 
46
 
47
@property KEY_NAMES
48
@type {Object}
49
@static
50
**/
51
DtKeyNav.KEY_NAMES = {
52
     8: 'backspace',
53
     9: 'tab',
54
    13: 'enter',
55
    27: 'esc',
56
    32: 'space',
57
    33: 'pgup',
58
    34: 'pgdown',
59
    35: 'end',
60
    36: 'home',
61
    37: 'left',
62
    38: 'up',
63
    39: 'right',
64
    40: 'down',
65
    112:'f1',
66
    113:'f2',
67
    114:'f3',
68
    115:'f4',
69
    116:'f5',
70
    117:'f6',
71
    118:'f7',
72
    119:'f8',
73
    120:'f9',
74
    121:'f10',
75
    122:'f11',
76
    123:'f12'
77
};
78
 
79
/**
80
Mapping of key codes to actions according to the WAI-ARIA suggestion for the
81
[Grid Widget](http://www.w3.org/WAI/PF/aria-practices/#grid).
82
 
83
The key for each entry is a key-code or [keyName](#property_KEY_NAMES) while the
84
value can be a function that performs the action or a string.  If a string,
85
it can either correspond to the name of a method in this module (or  any
86
method in a DataTable instance) or the name of an event to fire.
87
@property ARIA_ACTIONS
88
@type Object
89
@static
90
 */
91
DtKeyNav.ARIA_ACTIONS = {
92
    left:   '_keyMoveLeft',
93
    right:  '_keyMoveRight',
94
    up:     '_keyMoveUp',
95
    down:   '_keyMoveDown',
96
    home:   '_keyMoveRowStart',
97
    end:    '_keyMoveRowEnd',
98
    pgup:   '_keyMoveColTop',
99
    pgdown: '_keyMoveColBottom'
100
};
101
 
102
DtKeyNav.ATTRS = {
103
    /**
104
    Cell that's currently either focused or
105
    focusable when the DataTable gets the focus.
106
 
107
    @attribute focusedCell
108
    @type Node
109
    @default first cell in the table.
110
    **/
111
    focusedCell: {
112
        setter: '_focusedCellSetter'
113
    },
114
 
115
    /**
116
    Determines whether it is possible to navigate into the header area.
117
    The examples referenced in the document show both behaviors so it seems
118
    it is optional.
119
 
120
    @attribute keyIntoHeaders
121
    @type Boolean
122
    @default true
123
     */
124
    keyIntoHeaders: {
125
        value: true
126
    }
127
 
128
};
129
 
130
Y.mix( DtKeyNav.prototype, {
131
 
132
    /**
133
    Table of actions to be performed for each key.  It is loaded with a clone
134
    of [ARIA_ACTIONS](#property_ARIA_ACTIONS) by default.
135
 
136
    The key for each entry is either a key-code or an alias from the
137
    [KEY_NAMES](#property_KEY_NAMES) table. They can be prefixed with any combination
138
    of the modifier keys `alt`, `ctrl`, `meta` or `shift` each followed by a hyphen,
139
    such as `"ctrl-shift-up"` (modifiers, if more than one, should appear in alphabetical order).
140
 
141
    The value for each entry should be a function or the name of a method in
142
    the DataTable instance.  The method will receive the original keyboard
143
    EventFacade as its only argument.
144
 
145
    If the value is a string and it cannot be resolved into a method,
146
    it will be assumed to be the name of an event to fire. The listener for that
147
    event will receive an EventFacade containing references to the cell that has the focus,
148
    the row, column and, unless it is a header row, the record it corresponds to.
149
    The second argument will be the original EventFacade for the keyboard event.
150
 
151
     @property keyActions
152
     @type {Object}
153
     @default Y.DataTable.keyNav.ARIA_ACTIONS
154
     */
155
 
156
    keyActions: null,
157
 
158
    /**
159
    Array containing the event handles to any event that might need to be detached
160
    on destruction.
161
    @property _keyNavSubscr
162
    @type Array
163
    @default null,
164
    @private
165
     */
166
    _keyNavSubscr: null,
167
 
168
    /**
169
    Reference to the THead section that holds the headers for the datatable.
170
    For a Scrolling DataTable, it is the one visible to the user.
171
    @property _keyNavTHead
172
    @type Node
173
    @default: null
174
    @private
175
     */
176
    _keyNavTHead: null,
177
 
178
    /**
179
    Indicates if the headers of the table are nested or not.
180
    Nested headers makes navigation in the headers much harder.
181
    @property _keyNavNestedHeaders
182
    @default false
183
    @private
184
     */
185
    _keyNavNestedHeaders: false,
186
 
187
    /**
188
    CSS class name prefix for columns, used to search for a cell by key.
189
    @property _keyNavColPrefix
190
    @type String
191
    @default null (initialized via getClassname() )
192
    @private
193
     */
194
    _keyNavColPrefix:null,
195
 
196
    /**
197
    Regular expression to extract the column key from a cell via its CSS class name.
198
    @property _keyNavColRegExp
199
    @type RegExp
200
    @default null (initialized based on _keyNavColPrefix)
201
    @private
202
     */
203
    _keyNavColRegExp:null,
204
 
205
    initializer: function () {
206
        this.onceAfter('render', this._afterKeyNavRender);
207
        this._keyNavSubscr = [
208
            this.after('focusedCellChange', this._afterKeyNavFocusedCellChange),
209
            this.after('focusedChange', this._afterKeyNavFocusedChange)
210
        ];
211
        this._keyNavColPrefix = this.getClassName('col', '');
212
        this._keyNavColRegExp = new RegExp(this._keyNavColPrefix + '(.+?)(\\s|$)');
213
        this.keyActions = Y.clone(DtKeyNav.ARIA_ACTIONS);
214
 
215
    },
216
 
217
    destructor: function () {
218
        arrEach(this._keyNavSubscr, function (evHandle) {
219
            if (evHandle && evHandle.detach) {
220
                evHandle.detach();
221
            }
222
        });
223
    },
224
 
225
    /**
226
    Sets the tabIndex on the focused cell and, if the DataTable has the focus,
227
    sets the focus on it.
228
 
229
    @method _afterFocusedCellChange
230
    @param e {EventFacade}
231
    @private
232
    */
233
    _afterKeyNavFocusedCellChange: function (e) {
234
        var newVal  = e.newVal,
235
            prevVal = e.prevVal;
236
 
237
        if (prevVal) {
238
            prevVal.set('tabIndex', -1);
239
        }
240
 
241
        if (newVal) {
242
            newVal.set('tabIndex', 0);
243
 
244
            if (this.get('focused')) {
245
                newVal.scrollIntoView();
246
                newVal.focus();
247
            }
248
        } else {
249
            this.set('focused', null);
250
        }
251
    },
252
 
253
    /**
254
    When the DataTable gets the focus, it ensures the correct cell regains
255
    the focus.
256
 
257
    @method _afterKeyNavFocusedChange
258
    @param e {EventFacade}
259
    @private
260
    */
261
    _afterKeyNavFocusedChange: function (e) {
262
        var cell = this.get('focusedCell');
263
        if (e.newVal) {
264
            if (cell) {
265
                cell.scrollIntoView();
266
                cell.focus();
267
            } else {
268
                this._keyMoveFirst();
269
            }
270
        } else {
271
            if (cell) {
272
                cell.blur();
273
            }
274
        }
275
    },
276
 
277
    /**
278
    Subscribes to the events on the DataTable elements once they have been rendered,
279
    finds out the header section and makes the top-left element focusable.
280
 
281
    @method _afterKeyNavRender
282
    @private
283
     */
284
    _afterKeyNavRender: function () {
285
        var cbx = this.get('contentBox');
286
        this._keyNavSubscr.push(
287
            cbx.on('keydown', this._onKeyNavKeyDown, this),
288
            cbx.on('click', this._onKeyNavClick, this)
289
        );
290
        this._keyNavTHead = (this._yScrollHeader || this._tableNode).one('thead');
291
        this._keyMoveFirst();
292
 
293
        // determine if we have nested headers
294
        this._keyNavNestedHeaders = (this.get('columns').length !== this.head.theadNode.all('th').size());
295
    },
296
 
297
    /**
298
    In response to a click event, it sets the focus on the clicked cell
299
 
300
    @method _onKeyNavClick
301
    @param e {EventFacade}
302
    @private
303
     */
304
    _onKeyNavClick: function (e) {
305
        var cell = e.target.ancestor((this.get('keyIntoHeaders') ? 'td, th': 'td'), true);
306
        if (cell) {
307
            this.focus();
308
            this.set('focusedCell', cell);
309
        }
310
    },
311
 
312
    /**
313
    Responds to a key down event by executing the action set in the
314
    [keyActions](#property_keyActions) table.
315
 
316
    @method _onKeyNavKeyDown
317
    @param e {EventFacade}
318
    @private
319
    */
320
    _onKeyNavKeyDown: function (e) {
321
        var keyCode = e.keyCode,
322
            keyName = DtKeyNav.KEY_NAMES[keyCode] || keyCode,
323
            action;
324
 
325
        arrEach(['alt', 'ctrl', 'meta', 'shift'], function (modifier) {
326
            if (e[modifier + 'Key']) {
327
                keyCode = modifier + '-' + keyCode;
328
                keyName = modifier + '-' + keyName;
329
            }
330
        });
331
        action = this.keyActions[keyCode] || this.keyActions[keyName];
332
 
333
        if (typeof action === 'string') {
334
            if (this[action]) {
335
                this[action].call(this, e);
336
            } else {
337
                this._keyNavFireEvent(action, e);
338
            }
339
        } else {
340
            action.call(this, e);
341
        }
342
    },
343
 
344
    /**
345
    If the action associated to a key combination is a string and no method
346
    by that name was found in this instance, this method will
347
    fire an event using that string and provides extra information
348
    to the listener.
349
 
350
    @method _keyNavFireEvent
351
    @param action {String} Name of the event to fire
352
    @param e {EventFacade} Original facade from the keydown event.
353
    @private
354
     */
355
    _keyNavFireEvent: function (action, e) {
356
        var cell = e.target.ancestor('td, th', true);
357
        if (cell) {
358
            this.fire(action, {
359
                cell: cell,
360
                row: cell.ancestor('tr'),
361
                record: this.getRecord(cell),
362
                column: this.getColumn(cell.get('cellIndex'))
363
            }, e);
364
        }
365
    },
366
 
367
    /**
368
    Sets the focus on the very first cell in the header of the table.
369
 
370
    @method _keyMoveFirst
371
    @private
372
     */
373
    _keyMoveFirst: function () {
374
        this.set('focusedCell' , (this.get('keyIntoHeaders') ? this._keyNavTHead.one('th') : this._tbodyNode.one('td')), {src:'keyNav'});
375
    },
376
 
377
    /**
378
    Sets the focus on the cell to the left of the currently focused one.
379
    Does not wrap, following the WAI-ARIA recommendation.
380
 
381
    @method _keyMoveLeft
382
    @param e {EventFacade} Event Facade for the keydown event
383
    @private
384
    */
385
    _keyMoveLeft: function (e) {
386
        var cell = this.get('focusedCell'),
387
            index = cell.get('cellIndex'),
388
            row = cell.ancestor();
389
 
390
        e.preventDefault();
391
 
392
        if (index === 0) {
393
            return;
394
        }
395
        cell = row.get('cells').item(index - 1);
396
        this.set('focusedCell', cell , {src:'keyNav'});
397
    },
398
 
399
    /**
400
    Sets the focus on the cell to the right of the currently focused one.
401
    Does not wrap, following the WAI-ARIA recommendation.
402
 
403
    @method _keyMoveRight
404
    @param e {EventFacade} Event Facade for the keydown event
405
    @private
406
    */
407
    _keyMoveRight: function (e) {
408
        var cell = this.get('focusedCell'),
409
            row = cell.ancestor('tr'),
410
            section = row.ancestor(),
411
            inHead = section === this._keyNavTHead,
412
            nextCell,
413
            parent;
414
 
415
        e.preventDefault();
416
 
417
        // a little special with nested headers
418
        /*
419
            +-------------+-------+
420
            | ABC         | DE    |
421
            +-------+-----+---+---+
422
            | AB    |     |   |   |
423
            +---+---+     |   |   |
424
            | A | B |  C  | D | E |
425
            +---+---+-----+---+---+
426
        */
427
 
428
        nextCell = cell.next();
429
 
430
        if (row.get('rowIndex') !== 0 && inHead && this._keyNavNestedHeaders) {
431
            if (nextCell) {
432
                cell = nextCell;
433
            } else { //-- B -> C
434
                parent = this._getTHParent(cell);
435
 
436
                if (parent && parent.next()) {
437
                    cell = parent.next();
438
                } else { //-- E -> ...
439
                    return;
440
                }
441
            }
442
 
443
        } else {
444
            if (!nextCell) {
445
                return;
446
            } else {
447
                cell = nextCell;
448
            }
449
        }
450
 
451
        this.set('focusedCell', cell, { src:'keyNav' });
452
 
453
    },
454
 
455
    /**
456
    Sets the focus on the cell above the currently focused one.
457
    It will move into the headers when the top of the data rows is reached.
458
    Does not wrap, following the WAI-ARIA recommendation.
459
 
460
    @method _keyMoveUp
461
    @param e {EventFacade} Event Facade for the keydown event
462
    @private
463
    */
464
    _keyMoveUp: function (e) {
465
        var cell = this.get('focusedCell'),
466
            cellIndex = cell.get('cellIndex'),
467
            row = cell.ancestor('tr'),
468
            rowIndex = row.get('rowIndex'),
469
            section = row.ancestor(),
470
            sectionRows = section.get('rows'),
471
            inHead = section === this._keyNavTHead,
472
            parent;
473
 
474
        e.preventDefault();
475
 
476
        if (!inHead) {
477
            rowIndex -= section.get('firstChild').get('rowIndex');
478
        }
479
 
480
        if (rowIndex === 0) {
481
            if (inHead || !this.get('keyIntoHeaders')) {
482
                return;
483
            }
484
 
485
            section = this._keyNavTHead;
486
            sectionRows = section.get('rows');
487
 
488
            if (this._keyNavNestedHeaders) {
489
                key = this._getCellColumnName(cell);
490
                cell = section.one('.' + this._keyNavColPrefix + key);
491
                cellIndex = cell.get('cellIndex');
492
                row = cell.ancestor('tr');
493
            } else {
494
                row = section.get('firstChild');
495
                cell = row.get('cells').item(cellIndex);
496
            }
497
        } else {
498
            if (inHead && this._keyNavNestedHeaders) {
499
                key = this._getCellColumnName(cell);
500
                parent = this._columnMap[key]._parent;
501
                if (parent) {
502
                    cell = section.one('#' + parent.id);
503
                }
504
            } else {
505
                row = sectionRows.item(rowIndex -1);
506
                cell = row.get('cells').item(cellIndex);
507
            }
508
        }
509
        this.set('focusedCell', cell);
510
    },
511
 
512
    /**
513
    Sets the focus on the cell below the currently focused one.
514
    It will move into the data rows when the bottom of the header rows is reached.
515
    Does not wrap, following the WAI-ARIA recommendation.
516
 
517
    @method _keyMoveDown
518
    @param e {EventFacade} Event Facade for the keydown event
519
    @private
520
    */
521
    _keyMoveDown: function (e) {
522
        var cell = this.get('focusedCell'),
523
            cellIndex = cell.get('cellIndex'),
524
            row = cell.ancestor('tr'),
525
            rowIndex = row.get('rowIndex') + 1,
526
            section = row.ancestor(),
527
            inHead = section === this._keyNavTHead,
528
            tbody = (this.body && this.body.tbodyNode),
529
            sectionRows = section.get('rows'),
530
            key,
531
            children;
532
 
533
        e.preventDefault();
534
 
535
        if (inHead) { // focused cell is in the header
536
            if (this._keyNavNestedHeaders) { // the header is nested
537
                key = this._getCellColumnName(cell);
538
                children = this._columnMap[key].children;
539
 
540
                rowIndex += (cell.getAttribute('rowspan') || 1) - 1;
541
 
542
                if (children) {
543
                    // stay in thead
544
                    cell = section.one('#' + children[0].id);
545
                } else {
546
                    // moving into tbody
547
                    cell = tbody.one('.' + this._keyNavColPrefix + key);
548
                    section = tbody;
549
                    sectionRows = section.get('rows');
550
                }
551
                cellIndex = cell.get('cellIndex');
552
 
553
            } else { // the header is not nested
554
                row = tbody.one('tr');
555
                cell = row.get('cells').item(cellIndex);
556
            }
557
        }
558
 
559
        // offset row index to tbody
560
        rowIndex -= sectionRows.item(0).get('rowIndex');
561
 
562
 
563
        if (rowIndex >= sectionRows.size()) {
564
            if (!inHead) { // last row in tbody
565
                return;
566
            }
567
            section = tbody;
568
            row = section.one('tr');
569
 
570
        } else {
571
            row = sectionRows.item(rowIndex);
572
        }
573
 
574
        this.set('focusedCell', row.get('cells').item(cellIndex));
575
    },
576
 
577
    /**
578
    Sets the focus on the left-most cell of the row containing the currently focused cell.
579
 
580
    @method _keyMoveRowStart
581
    @param e {EventFacade} Event Facade for the keydown event
582
    @private
583
     */
584
    _keyMoveRowStart: function (e) {
585
        var row = this.get('focusedCell').ancestor();
586
        this.set('focusedCell', row.get('firstChild'), {src:'keyNav'});
587
        e.preventDefault();
588
    },
589
 
590
    /**
591
    Sets the focus on the right-most cell of the row containing the currently focused cell.
592
 
593
    @method _keyMoveRowEnd
594
    @param e {EventFacade} Event Facade for the keydown event
595
    @private
596
     */
597
    _keyMoveRowEnd: function (e) {
598
        var row = this.get('focusedCell').ancestor();
599
        this.set('focusedCell', row.get('lastChild'), {src:'keyNav'});
600
        e.preventDefault();
601
    },
602
 
603
    /**
604
    Sets the focus on the top-most cell of the column containing the currently focused cell.
605
    It would normally be a header cell.
606
 
607
    @method _keyMoveColTop
608
    @param e {EventFacade} Event Facade for the keydown event
609
    @private
610
     */
611
    _keyMoveColTop: function (e) {
612
        var cell = this.get('focusedCell'),
613
            cellIndex = cell.get('cellIndex'),
614
            key, header;
615
 
616
        e.preventDefault();
617
 
618
        if (this._keyNavNestedHeaders && this.get('keyIntoHeaders')) {
619
            key = this._getCellColumnName(cell);
620
            header = this._columnMap[key];
621
            while (header._parent) {
622
                header = header._parent;
623
            }
624
            cell = this._keyNavTHead.one('#' + header.id);
625
 
626
        } else {
627
            cell = (this.get('keyIntoHeaders') ? this._keyNavTHead: this._tbodyNode).get('firstChild').get('cells').item(cellIndex);
628
        }
629
        this.set('focusedCell', cell , {src:'keyNav'});
630
    },
631
 
632
    /**
633
    Sets the focus on the last cell of the column containing the currently focused cell.
634
 
635
    @method _keyMoveColBottom
636
    @param e {EventFacade} Event Facade for the keydown event
637
    @private
638
     */
639
    _keyMoveColBottom: function (e) {
640
        var cell = this.get('focusedCell'),
641
            cellIndex = cell.get('cellIndex');
642
 
643
        this.set('focusedCell', this._tbodyNode.get('lastChild').get('cells').item(cellIndex), {src:'keyNav'});
644
        e.preventDefault();
645
 
646
    },
647
 
648
    /**
649
    Setter method for the [focusedCell](#attr_focusedCell) attribute.
650
    Checks that the passed value is a Node, either a TD or TH and is
651
    contained within the DataTable contentBox.
652
 
653
    @method _focusedCellSetter
654
    @param cell {Node} DataTable cell to receive the focus
655
    @return cell or Y.Attribute.INVALID_VALUE
656
    @private
657
     */
658
    _focusedCellSetter: function (cell) {
659
        if (cell instanceof Y.Node) {
660
            var tag = cell.get('tagName').toUpperCase();
661
            if ((tag === 'TD' || tag === 'TH') && this.get('contentBox').contains(cell) ) {
662
                return cell;
663
            }
664
        } else if (cell === null) {
665
            return cell;
666
        }
667
        return Y.Attribute.INVALID_VALUE;
668
    },
669
 
670
    /**
671
     Retrieves the parent cell of the given TH cell. If there is no parent for
672
     the provided cell, null is returned.
673
     @protected
674
     @method _getTHParent
675
     @param {Node} thCell Cell to find parent of
676
     @return {Node} Parent of the cell provided or null
677
     */
678
    _getTHParent: function (thCell) {
679
        var key = this._getCellColumnName(thCell),
680
            parent = this._columnMap[key] && this._columnMap[key]._parent;
681
 
682
        if (parent) {
683
            return thCell.ancestor().ancestor().one('.' + this._keyNavColPrefix + parent.key);
684
        }
685
 
686
        return null;
687
    },
688
 
689
    /**
690
     Retrieves the column name based from the data attribute on the cell if
691
     available. Other wise, extracts the column name from the classname
692
     @protected
693
     @method _getCellColumnName
694
     @param {Node} cell Cell to get column name from
695
     @return String Column name of the provided cell
696
     */
697
    _getCellColumnName: function (cell) {
698
        return cell.getData('yui3-col-id') || this._keyNavColRegExp.exec(cell.get('className'))[1];
699
    }
700
});
701
 
702
Y.DataTable.KeyNav = DtKeyNav;
703
Y.Base.mix(Y.DataTable, [DtKeyNav]);
704
 
705
 
706
}, '3.18.1', {"requires": ["datatable-base"]});