Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
YUI.add('datatable-body', function (Y, NAME) {
2
 
3
/**
4
View class responsible for rendering the `<tbody>` section of a table. Used as
5
the default `bodyView` for `Y.DataTable.Base` and `Y.DataTable` classes.
6
 
7
@module datatable
8
@submodule datatable-body
9
@since 3.5.0
10
**/
11
var Lang             = Y.Lang,
12
    isArray          = Lang.isArray,
13
    isNumber         = Lang.isNumber,
14
    isString         = Lang.isString,
15
    fromTemplate     = Lang.sub,
16
    htmlEscape       = Y.Escape.html,
17
    toArray          = Y.Array,
18
    bind             = Y.bind,
19
    YObject          = Y.Object,
20
    valueRegExp      = /\{value\}/g,
21
    EV_CONTENT_UPDATE = 'contentUpdate',
22
 
23
    shiftMap = {
24
        above:    [-1, 0],
25
        below:    [1, 0],
26
        next:     [0, 1],
27
        prev:     [0, -1],
28
        previous: [0, -1]
29
    };
30
 
31
/**
32
View class responsible for rendering the `<tbody>` section of a table. Used as
33
the default `bodyView` for `Y.DataTable.Base` and `Y.DataTable` classes.
34
 
35
Translates the provided `modelList` into a rendered `<tbody>` based on the data
36
in the constituent Models, altered or amended by any special column
37
configurations.
38
 
39
The `columns` configuration, passed to the constructor, determines which
40
columns will be rendered.
41
 
42
The rendering process involves constructing an HTML template for a complete row
43
of data, built by concatenating a customized copy of the instance's
44
`CELL_TEMPLATE` into the `ROW_TEMPLATE` once for each column.  This template is
45
then populated with values from each Model in the `modelList`, aggregating a
46
complete HTML string of all row and column data.  A `<tbody>` Node is then created from the markup and any column `nodeFormatter`s are applied.
47
 
48
Supported properties of the column objects include:
49
 
50
  * `key` - Used to link a column to an attribute in a Model.
51
  * `name` - Used for columns that don't relate to an attribute in the Model
52
    (`formatter` or `nodeFormatter` only) if the implementer wants a
53
    predictable name to refer to in their CSS.
54
  * `cellTemplate` - Overrides the instance's `CELL_TEMPLATE` for cells in this
55
    column only.
56
  * `formatter` - Used to customize or override the content value from the
57
    Model.  These do not have access to the cell or row Nodes and should
58
    return string (HTML) content.
59
  * `nodeFormatter` - Used to provide content for a cell as well as perform any
60
    custom modifications on the cell or row Node that could not be performed by
61
    `formatter`s.  Should be used sparingly for better performance.
62
  * `emptyCellValue` - String (HTML) value to use if the Model data for a
63
    column, or the content generated by a `formatter`, is the empty string,
64
    `null`, or `undefined`.
65
  * `allowHTML` - Set to `true` if a column value, `formatter`, or
66
    `emptyCellValue` can contain HTML.  This defaults to `false` to protect
67
    against XSS.
68
  * `className` - Space delimited CSS classes to add to all `<td>`s in a column.
69
 
70
A column `formatter` can be:
71
 
72
  * a function, as described below.
73
  * a string which can be:
74
      * the name of a pre-defined formatter function
75
        which can be located in the `Y.DataTable.BodyView.Formatters` hash using the
76
        value of the `formatter` property as the index.
77
      * A template that can use the `{value}` placeholder to include the value
78
        for the current cell or the name of any field in the underlaying model
79
        also enclosed in curly braces.  Any number and type of these placeholders
80
        can be used.
81
 
82
Column `formatter`s are passed an object (`o`) with the following properties:
83
 
84
  * `value` - The current value of the column's associated attribute, if any.
85
  * `data` - An object map of Model keys to their current values.
86
  * `record` - The Model instance.
87
  * `column` - The column configuration object for the current column.
88
  * `className` - Initially empty string to allow `formatter`s to add CSS
89
    classes to the cell's `<td>`.
90
  * `rowIndex` - The zero-based row number.
91
  * `rowClass` - Initially empty string to allow `formatter`s to add CSS
92
    classes to the cell's containing row `<tr>`.
93
 
94
They may return a value or update `o.value` to assign specific HTML content.  A
95
returned value has higher precedence.
96
 
97
Column `nodeFormatter`s are passed an object (`o`) with the following
98
properties:
99
 
100
  * `value` - The current value of the column's associated attribute, if any.
101
  * `td` - The `<td>` Node instance.
102
  * `cell` - The `<div>` liner Node instance if present, otherwise, the `<td>`.
103
    When adding content to the cell, prefer appending into this property.
104
  * `data` - An object map of Model keys to their current values.
105
  * `record` - The Model instance.
106
  * `column` - The column configuration object for the current column.
107
  * `rowIndex` - The zero-based row number.
108
 
109
They are expected to inject content into the cell's Node directly, including
110
any "empty" cell content.  Each `nodeFormatter` will have access through the
111
Node API to all cells and rows in the `<tbody>`, but not to the `<table>`, as
112
it will not be attached yet.
113
 
114
If a `nodeFormatter` returns `false`, the `o.td` and `o.cell` Nodes will be
115
`destroy()`ed to remove them from the Node cache and free up memory.  The DOM
116
elements will remain as will any content added to them.  _It is highly
117
advisable to always return `false` from your `nodeFormatter`s_.
118
 
119
@class BodyView
120
@namespace DataTable
121
@extends View
122
@since 3.5.0
123
**/
124
Y.namespace('DataTable').BodyView = Y.Base.create('tableBody', Y.View, [], {
125
    // -- Instance properties -------------------------------------------------
126
 
127
    /**
128
    HTML template used to create table cells.
129
 
130
    @property CELL_TEMPLATE
131
    @type {String}
132
    @default '<td {headers} class="{className}">{content}</td>'
133
    @since 3.5.0
134
    **/
135
    CELL_TEMPLATE: '<td {headers} class="{className}">{content}</td>',
136
 
137
    /**
138
    CSS class applied to even rows.  This is assigned at instantiation.
139
 
140
    For DataTable, this will be `yui3-datatable-even`.
141
 
142
    @property CLASS_EVEN
143
    @type {String}
144
    @default 'yui3-table-even'
145
    @since 3.5.0
146
    **/
147
    //CLASS_EVEN: null
148
 
149
    /**
150
    CSS class applied to odd rows.  This is assigned at instantiation.
151
 
152
    When used by DataTable instances, this will be `yui3-datatable-odd`.
153
 
154
    @property CLASS_ODD
155
    @type {String}
156
    @default 'yui3-table-odd'
157
    @since 3.5.0
158
    **/
159
    //CLASS_ODD: null
160
 
161
    /**
162
    HTML template used to create table rows.
163
 
164
    @property ROW_TEMPLATE
165
    @type {String}
166
    @default '<tr id="{rowId}" data-yui3-record="{clientId}" class="{rowClass}">{content}</tr>'
167
    @since 3.5.0
168
    **/
169
    ROW_TEMPLATE : '<tr id="{rowId}" data-yui3-record="{clientId}" class="{rowClass}">{content}</tr>',
170
 
171
    /**
172
    The object that serves as the source of truth for column and row data.
173
    This property is assigned at instantiation from the `host` property of
174
    the configuration object passed to the constructor.
175
 
176
    @property host
177
    @type {Object}
178
    @default (initially unset)
179
    @since 3.5.0
180
    **/
181
    //TODO: should this be protected?
182
    //host: null,
183
 
184
    /**
185
    HTML templates used to create the `<tbody>` containing the table rows.
186
 
187
    @property TBODY_TEMPLATE
188
    @type {String}
189
    @default '<tbody class="{className}">{content}</tbody>'
190
    @since 3.6.0
191
    **/
192
    TBODY_TEMPLATE: '<tbody class="{className}"></tbody>',
193
 
194
    // -- Public methods ------------------------------------------------------
195
 
196
    /**
197
    Returns the `<td>` Node from the given row and column index.  Alternately,
198
    the `seed` can be a Node.  If so, the nearest ancestor cell is returned.
199
    If the `seed` is a cell, it is returned.  If there is no cell at the given
200
    coordinates, `null` is returned.
201
 
202
    Optionally, include an offset array or string to return a cell near the
203
    cell identified by the `seed`.  The offset can be an array containing the
204
    number of rows to shift followed by the number of columns to shift, or one
205
    of "above", "below", "next", or "previous".
206
 
207
    <pre><code>// Previous cell in the previous row
208
    var cell = table.getCell(e.target, [-1, -1]);
209
 
210
    // Next cell
211
    var cell = table.getCell(e.target, 'next');
212
    var cell = table.getCell(e.target, [0, 1];</pre></code>
213
 
214
    @method getCell
215
    @param {Number[]|Node} seed Array of row and column indexes, or a Node that
216
        is either the cell itself or a descendant of one.
217
    @param {Number[]|String} [shift] Offset by which to identify the returned
218
        cell Node
219
    @return {Node}
220
    @since 3.5.0
221
    **/
222
    getCell: function (seed, shift) {
223
        var tbody = this.tbodyNode,
224
            row, cell, index, rowIndexOffset;
225
 
226
        if (seed && tbody) {
227
            if (isArray(seed)) {
228
                row = tbody.get('children').item(seed[0]);
229
                cell = row && row.get('children').item(seed[1]);
230
            } else if (seed._node) {
231
                cell = seed.ancestor('.' + this.getClassName('cell'), true);
232
            }
233
 
234
            if (cell && shift) {
235
                rowIndexOffset = tbody.get('firstChild.rowIndex');
236
                if (isString(shift)) {
237
                    if (!shiftMap[shift]) {
238
                        Y.error('Unrecognized shift: ' + shift, null, 'datatable-body');
239
                    }
240
                    shift = shiftMap[shift];
241
                }
242
 
243
                if (isArray(shift)) {
244
                    index = cell.get('parentNode.rowIndex') +
245
                                shift[0] - rowIndexOffset;
246
                    row   = tbody.get('children').item(index);
247
 
248
                    index = cell.get('cellIndex') + shift[1];
249
                    cell  = row && row.get('children').item(index);
250
                }
251
            }
252
        }
253
 
254
        return cell || null;
255
    },
256
 
257
    /**
258
    Returns the generated CSS classname based on the input.  If the `host`
259
    attribute is configured, it will attempt to relay to its `getClassName`
260
    or use its static `NAME` property as a string base.
261
 
262
    If `host` is absent or has neither method nor `NAME`, a CSS classname
263
    will be generated using this class's `NAME`.
264
 
265
    @method getClassName
266
    @param {String} token* Any number of token strings to assemble the
267
        classname from.
268
    @return {String}
269
    @protected
270
    @since 3.5.0
271
    **/
272
    getClassName: function () {
273
        var host = this.host,
274
            args;
275
 
276
        if (host && host.getClassName) {
277
            return host.getClassName.apply(host, arguments);
278
        } else {
279
            args = toArray(arguments);
280
            args.unshift(this.constructor.NAME);
281
            return Y.ClassNameManager.getClassName
282
                .apply(Y.ClassNameManager, args);
283
        }
284
    },
285
 
286
    /**
287
    Returns the Model associated to the row Node or id provided. Passing the
288
    Node or id for a descendant of the row also works.
289
 
290
    If no Model can be found, `null` is returned.
291
 
292
    @method getRecord
293
    @param {String|Node} seed Row Node or `id`, or one for a descendant of a row
294
    @return {Model}
295
    @since 3.5.0
296
    **/
297
    getRecord: function (seed) {
298
        var modelList = this.get('modelList'),
299
            tbody     = this.tbodyNode,
300
            row       = null,
301
            record;
302
 
303
        if (tbody) {
304
            if (isString(seed)) {
305
                seed = tbody.one('#' + seed);
306
            }
307
 
308
            if (seed && seed._node) {
309
                row = seed.ancestor(function (node) {
310
                    return node.get('parentNode').compareTo(tbody);
311
                }, true);
312
 
313
                record = row &&
314
                    modelList.getByClientId(row.getData('yui3-record'));
315
            }
316
        }
317
 
318
        return record || null;
319
    },
320
 
321
    /**
322
    Returns the `<tr>` Node from the given row index, Model, or Model's
323
    `clientId`.  If the rows haven't been rendered yet, or if the row can't be
324
    found by the input, `null` is returned.
325
 
326
    @method getRow
327
    @param {Number|String|Model} id Row index, Model instance, or clientId
328
    @return {Node}
329
    @since 3.5.0
330
    **/
331
    getRow: function (id) {
332
        var tbody = this.tbodyNode,
333
            row = null;
334
 
335
        if (tbody) {
336
            if (id) {
337
                id = this._idMap[id.get ? id.get('clientId') : id] || id;
338
            }
339
 
340
            row = isNumber(id) ?
341
                tbody.get('children').item(id) :
342
                tbody.one('#' + id);
343
        }
344
 
345
        return row;
346
    },
347
 
348
    /**
349
    Creates the table's `<tbody>` content by assembling markup generated by
350
    populating the `ROW\_TEMPLATE`, and `CELL\_TEMPLATE` templates with content
351
    from the `columns` and `modelList` attributes.
352
 
353
    The rendering process happens in three stages:
354
 
355
    1. A row template is assembled from the `columns` attribute (see
356
       `_createRowTemplate`)
357
 
358
    2. An HTML string is built up by concatenating the application of the data in
359
       each Model in the `modelList` to the row template. For cells with
360
       `formatter`s, the function is called to generate cell content. Cells
361
       with `nodeFormatter`s are ignored. For all other cells, the data value
362
       from the Model attribute for the given column key is used.  The
363
       accumulated row markup is then inserted into the container.
364
 
365
    3. If any column is configured with a `nodeFormatter`, the `modelList` is
366
       iterated again to apply the `nodeFormatter`s.
367
 
368
    Supported properties of the column objects include:
369
 
370
      * `key` - Used to link a column to an attribute in a Model.
371
      * `name` - Used for columns that don't relate to an attribute in the Model
372
        (`formatter` or `nodeFormatter` only) if the implementer wants a
373
        predictable name to refer to in their CSS.
374
      * `cellTemplate` - Overrides the instance's `CELL_TEMPLATE` for cells in
375
        this column only.
376
      * `formatter` - Used to customize or override the content value from the
377
        Model.  These do not have access to the cell or row Nodes and should
378
        return string (HTML) content.
379
      * `nodeFormatter` - Used to provide content for a cell as well as perform
380
        any custom modifications on the cell or row Node that could not be
381
        performed by `formatter`s.  Should be used sparingly for better
382
        performance.
383
      * `emptyCellValue` - String (HTML) value to use if the Model data for a
384
        column, or the content generated by a `formatter`, is the empty string,
385
        `null`, or `undefined`.
386
      * `allowHTML` - Set to `true` if a column value, `formatter`, or
387
        `emptyCellValue` can contain HTML.  This defaults to `false` to protect
388
        against XSS.
389
      * `className` - Space delimited CSS classes to add to all `<td>`s in a
390
        column.
391
 
392
    Column `formatter`s are passed an object (`o`) with the following
393
    properties:
394
 
395
      * `value` - The current value of the column's associated attribute, if
396
        any.
397
      * `data` - An object map of Model keys to their current values.
398
      * `record` - The Model instance.
399
      * `column` - The column configuration object for the current column.
400
      * `className` - Initially empty string to allow `formatter`s to add CSS
401
        classes to the cell's `<td>`.
402
      * `rowIndex` - The zero-based row number.
403
      * `rowClass` - Initially empty string to allow `formatter`s to add CSS
404
        classes to the cell's containing row `<tr>`.
405
 
406
    They may return a value or update `o.value` to assign specific HTML
407
    content.  A returned value has higher precedence.
408
 
409
    Column `nodeFormatter`s are passed an object (`o`) with the following
410
    properties:
411
 
412
      * `value` - The current value of the column's associated attribute, if
413
        any.
414
      * `td` - The `<td>` Node instance.
415
      * `cell` - The `<div>` liner Node instance if present, otherwise, the
416
        `<td>`.  When adding content to the cell, prefer appending into this
417
        property.
418
      * `data` - An object map of Model keys to their current values.
419
      * `record` - The Model instance.
420
      * `column` - The column configuration object for the current column.
421
      * `rowIndex` - The zero-based row number.
422
 
423
    They are expected to inject content into the cell's Node directly, including
424
    any "empty" cell content.  Each `nodeFormatter` will have access through the
425
    Node API to all cells and rows in the `<tbody>`, but not to the `<table>`,
426
    as it will not be attached yet.
427
 
428
    If a `nodeFormatter` returns `false`, the `o.td` and `o.cell` Nodes will be
429
    `destroy()`ed to remove them from the Node cache and free up memory.  The
430
    DOM elements will remain as will any content added to them.  _It is highly
431
    advisable to always return `false` from your `nodeFormatter`s_.
432
 
433
    @method render
434
    @chainable
435
    @since 3.5.0
436
    **/
437
    render: function () {
438
        var table   = this.get('container'),
439
            data    = this.get('modelList'),
440
            displayCols = this.get('columns'),
441
            tbody   = this.tbodyNode ||
442
                      (this.tbodyNode = this._createTBodyNode());
443
 
444
        // Needed for mutation
445
        this._createRowTemplate(displayCols);
446
 
447
        if (data) {
448
            tbody.setHTML(this._createDataHTML(displayCols));
449
 
450
            this._applyNodeFormatters(tbody, displayCols);
451
        }
452
 
453
        if (tbody.get('parentNode') !== table) {
454
            table.appendChild(tbody);
455
        }
456
 
457
        this.bindUI();
458
 
459
        return this;
460
    },
461
 
462
    /**
463
     Refreshes the provided row against the provided model and the Array of
464
     columns to be updated.
465
 
466
     @method refreshRow
467
     @param {Node} row
468
     @param {Model} model Y.Model representation of the row
469
     @param {String[]} colKeys Array of column keys
470
 
471
     @chainable
472
     */
473
    refreshRow: function (row, model, colKeys) {
474
        var col,
475
            cell,
476
            len = colKeys.length,
477
            i;
478
 
479
        for (i = 0; i < len; i++) {
480
            col = this.getColumn(colKeys[i]);
481
 
482
            if (col !== null) {
483
                cell = row.one('.' + this.getClassName('col', col._id || col.key));
484
                this.refreshCell(cell, model);
485
            }
486
        }
487
 
488
        return this;
489
    },
490
 
491
    /**
492
     Refreshes the given cell with the provided model data and the provided
493
     column configuration.
494
 
495
     Uses the provided column formatter if aviable.
496
 
497
     @method refreshCell
498
     @param {Node} cell Y.Node pointer to the cell element to be updated
499
     @param {Model} [model] Y.Model representation of the row
500
     @param {Object} [col] Column configuration object for the cell
501
 
502
     @chainable
503
     */
504
    refreshCell: function (cell, model, col) {
505
        var content,
506
            formatterFn,
507
            formatterData,
508
            data = model.toJSON();
509
 
510
        cell = this.getCell(cell);
511
        /* jshint -W030 */
512
        model || (model = this.getRecord(cell));
513
        col || (col = this.getColumn(cell));
514
        /* jshint +W030 */
515
 
516
        if (col.nodeFormatter) {
517
            formatterData = {
518
                cell: cell.one('.' + this.getClassName('liner')) || cell,
519
                column: col,
520
                data: data,
521
                record: model,
522
                rowIndex: this._getRowIndex(cell.ancestor('tr')),
523
                td: cell,
524
                value: data[col.key]
525
            };
526
 
527
            keep = col.nodeFormatter.call(host,formatterData);
528
 
529
            if (keep === false) {
530
                // Remove from the Node cache to reduce
531
                // memory footprint.  This also purges events,
532
                // which you shouldn't be scoping to a cell
533
                // anyway.  You've been warned.  Incidentally,
534
                // you should always return false. Just sayin.
535
                cell.destroy(true);
536
            }
537
 
538
        } else if (col.formatter) {
539
            if (!col._formatterFn) {
540
                col = this._setColumnsFormatterFn([col])[0];
541
            }
542
 
543
            formatterFn = col._formatterFn || null;
544
 
545
            if (formatterFn) {
546
                formatterData = {
547
                    value    : data[col.key],
548
                    data     : data,
549
                    column   : col,
550
                    record   : model,
551
                    className: '',
552
                    rowClass : '',
553
                    rowIndex : this._getRowIndex(cell.ancestor('tr'))
554
                };
555
 
556
                // Formatters can either return a value ...
557
                content = formatterFn.call(this.get('host'), formatterData);
558
 
559
                // ... or update the value property of the data obj passed
560
                if (content === undefined) {
561
                    content = formatterData.value;
562
                }
563
            }
564
 
565
            if (content === undefined || content === null || content === '') {
566
                content = col.emptyCellValue || '';
567
            }
568
 
569
        } else {
570
            content = data[col.key] || col.emptyCellValue || '';
571
        }
572
 
573
        cell.setHTML(col.allowHTML ? content : Y.Escape.html(content));
574
 
575
        return this;
576
    },
577
 
578
    /**
579
     Returns column data from this.get('columns'). If a Y.Node is provided as
580
     the key, will try to determine the key from the classname
581
     @method getColumn
582
     @param {String|Node} name
583
     @return {Object} Returns column configuration
584
     */
585
    getColumn: function (name) {
586
        if (name && name._node) {
587
            // get column name from node
588
            name = name.get('className').match(
589
                new RegExp( this.getClassName('col') +'-([^ ]*)' )
590
            )[1];
591
        }
592
 
593
        if (this.host) {
594
            return this.host._columnMap[name] || null;
595
        }
596
        var displayCols = this.get('columns'),
597
            col = null;
598
 
599
        Y.Array.some(displayCols, function (_col) {
600
            if ((_col._id || _col.key) === name) {
601
                col = _col;
602
                return true;
603
            }
604
        });
605
 
606
        return col;
607
    },
608
 
609
    // -- Protected and private methods ---------------------------------------
610
    /**
611
    Handles changes in the source's columns attribute.  Redraws the table data.
612
 
613
    @method _afterColumnsChange
614
    @param {EventFacade} e The `columnsChange` event object
615
    @protected
616
    @since 3.5.0
617
    **/
618
    // TODO: Preserve existing DOM
619
    // This will involve parsing and comparing the old and new column configs
620
    // and reacting to four types of changes:
621
    // 1. formatter, nodeFormatter, emptyCellValue changes
622
    // 2. column deletions
623
    // 3. column additions
624
    // 4. column moves (preserve cells)
625
    _afterColumnsChange: function () {
626
        this.render();
627
    },
628
 
629
    /**
630
    Handles modelList changes, including additions, deletions, and updates.
631
 
632
    Modifies the existing table DOM accordingly.
633
 
634
    @method _afterDataChange
635
    @param {EventFacade} e The `change` event from the ModelList
636
    @protected
637
    @since 3.5.0
638
    **/
639
    _afterDataChange: function (e) {
640
        var type = (e.type.match(/:(add|change|remove)$/) || [])[1],
641
            index = e.index,
642
            displayCols = this.get('columns'),
643
            col,
644
            changed = e.changed && Y.Object.keys(e.changed),
645
            key,
646
            row,
647
            i,
648
            len;
649
 
650
        for (i = 0, len = displayCols.length; i < len; i++ ) {
651
            col = displayCols[i];
652
 
653
            // since nodeFormatters typcially make changes outside of it's
654
            // cell, we need to see if there are any columns that have a
655
            // nodeFormatter and if so, we need to do a full render() of the
656
            // tbody
657
            if (col.hasOwnProperty('nodeFormatter')) {
658
                this.render();
659
                this.fire(EV_CONTENT_UPDATE);
660
                return;
661
            }
662
        }
663
 
664
        // TODO: if multiple rows are being added/remove/swapped, can we avoid the restriping?
665
        switch (type) {
666
            case 'change':
667
                for (i = 0, len = displayCols.length; i < len; i++) {
668
                    col = displayCols[i];
669
                    key = col.key;
670
                    if (col.formatter && !e.changed[key]) {
671
                        changed.push(key);
672
                    }
673
                }
674
                this.refreshRow(this.getRow(e.target), e.target, changed);
675
                break;
676
            case 'add':
677
                // we need to make sure we don't have an index larger than the data we have
678
                index =  Math.min(index, this.get('modelList').size() - 1);
679
 
680
                // updates the columns with formatter functions
681
                this._setColumnsFormatterFn(displayCols);
682
                row = Y.Node.create(this._createRowHTML(e.model, index, displayCols));
683
                this.tbodyNode.insert(row, index);
684
                this._restripe(index);
685
                break;
686
            case 'remove':
687
                this.getRow(index).remove(true);
688
                // we removed a row, so we need to back up our index to stripe
689
                this._restripe(index - 1);
690
                break;
691
            default:
692
                this.render();
693
        }
694
 
695
        // Event fired to tell users when we are done updating after the data
696
        // was changed
697
        this.fire(EV_CONTENT_UPDATE);
698
    },
699
 
700
    /**
701
     Toggles the odd/even classname of the row after the given index. This method
702
     is used to update rows after a row is inserted into or removed from the table.
703
     Note this event is delayed so the table is only restriped once when multiple
704
     rows are updated at one time.
705
 
706
     @protected
707
     @method _restripe
708
     @param {Number} [index] Index of row to start restriping after
709
     @since 3.11.0
710
     */
711
    _restripe: function (index) {
712
        var task = this._restripeTask,
713
            self;
714
 
715
        // index|0 to force int, avoid NaN. Math.max() to avoid neg indexes.
716
        index = Math.max((index|0), 0);
717
 
718
        if (!task) {
719
            self = this;
720
 
721
            this._restripeTask = {
722
                timer: setTimeout(function () {
723
                    // Check for self existence before continuing
724
                    if (!self || self.get('destroy') || !self.tbodyNode || !self.tbodyNode.inDoc()) {
725
                        self._restripeTask = null;
726
                        return;
727
                    }
728
 
729
                    var odd  = [self.CLASS_ODD, self.CLASS_EVEN],
730
                        even = [self.CLASS_EVEN, self.CLASS_ODD],
731
                        index = self._restripeTask.index;
732
 
733
                    self.tbodyNode.get('childNodes')
734
                        .slice(index)
735
                        .each(function (row, i) { // TODO: each vs batch
736
                            row.replaceClass.apply(row, (index + i) % 2 ? even : odd);
737
                        });
738
 
739
                    self._restripeTask = null;
740
                }, 0),
741
 
742
                index: index
743
            };
744
        } else {
745
            task.index = Math.min(task.index, index);
746
        }
747
 
748
    },
749
 
750
    /**
751
    Handles replacement of the modelList.
752
 
753
    Rerenders the `<tbody>` contents.
754
 
755
    @method _afterModelListChange
756
    @param {EventFacade} e The `modelListChange` event
757
    @protected
758
    @since 3.6.0
759
    **/
760
    _afterModelListChange: function () {
761
        var handles = this._eventHandles;
762
 
763
        if (handles.dataChange) {
764
            handles.dataChange.detach();
765
            delete handles.dataChange;
766
            this.bindUI();
767
        }
768
 
769
        if (this.tbodyNode) {
770
            this.render();
771
        }
772
    },
773
 
774
    /**
775
    Iterates the `modelList`, and calls any `nodeFormatter`s found in the
776
    `columns` param on the appropriate cell Nodes in the `tbody`.
777
 
778
    @method _applyNodeFormatters
779
    @param {Node} tbody The `<tbody>` Node whose columns to update
780
    @param {Object[]} displayCols The column configurations
781
    @protected
782
    @since 3.5.0
783
    **/
784
    _applyNodeFormatters: function (tbody, displayCols) {
785
        var host = this.host || this,
786
            data = this.get('modelList'),
787
            formatters = [],
788
            linerQuery = '.' + this.getClassName('liner'),
789
            rows, i, len;
790
 
791
        // Only iterate the ModelList again if there are nodeFormatters
792
        for (i = 0, len = displayCols.length; i < len; ++i) {
793
            if (displayCols[i].nodeFormatter) {
794
                formatters.push(i);
795
            }
796
        }
797
 
798
        if (data && formatters.length) {
799
            rows = tbody.get('childNodes');
800
 
801
            data.each(function (record, index) {
802
                var formatterData = {
803
                        data      : record.toJSON(),
804
                        record    : record,
805
                        rowIndex  : index
806
                    },
807
                    row = rows.item(index),
808
                    i, len, col, key, cells, cell, keep;
809
 
810
 
811
                if (row) {
812
                    cells = row.get('childNodes');
813
                    for (i = 0, len = formatters.length; i < len; ++i) {
814
                        cell = cells.item(formatters[i]);
815
 
816
                        if (cell) {
817
                            col = formatterData.column = displayCols[formatters[i]];
818
                            key = col.key || col.id;
819
 
820
                            formatterData.value = record.get(key);
821
                            formatterData.td    = cell;
822
                            formatterData.cell  = cell.one(linerQuery) || cell;
823
 
824
                            keep = col.nodeFormatter.call(host,formatterData);
825
 
826
                            if (keep === false) {
827
                                // Remove from the Node cache to reduce
828
                                // memory footprint.  This also purges events,
829
                                // which you shouldn't be scoping to a cell
830
                                // anyway.  You've been warned.  Incidentally,
831
                                // you should always return false. Just sayin.
832
                                cell.destroy(true);
833
                            }
834
                        }
835
                    }
836
                }
837
            });
838
        }
839
    },
840
 
841
    /**
842
    Binds event subscriptions from the UI and the host (if assigned).
843
 
844
    @method bindUI
845
    @protected
846
    @since 3.5.0
847
    **/
848
    bindUI: function () {
849
        var handles     = this._eventHandles,
850
            modelList   = this.get('modelList'),
851
            changeEvent = modelList.model.NAME + ':change';
852
 
853
        if (!handles.columnsChange) {
854
            handles.columnsChange = this.after('columnsChange',
855
                bind('_afterColumnsChange', this));
856
        }
857
 
858
        if (modelList && !handles.dataChange) {
859
            handles.dataChange = modelList.after(
860
                ['add', 'remove', 'reset', changeEvent],
861
                bind('_afterDataChange', this));
862
        }
863
    },
864
 
865
    /**
866
    Iterates the `modelList` and applies each Model to the `_rowTemplate`,
867
    allowing any column `formatter` or `emptyCellValue` to override cell
868
    content for the appropriate column.  The aggregated HTML string is
869
    returned.
870
 
871
    @method _createDataHTML
872
    @param {Object[]} displayCols The column configurations to customize the
873
                generated cell content or class names
874
    @return {String} The markup for all Models in the `modelList`, each applied
875
                to the `_rowTemplate`
876
    @protected
877
    @since 3.5.0
878
    **/
879
    _createDataHTML: function (displayCols) {
880
        var data = this.get('modelList'),
881
            html = '';
882
 
883
        if (data) {
884
            data.each(function (model, index) {
885
                html += this._createRowHTML(model, index, displayCols);
886
            }, this);
887
        }
888
 
889
        return html;
890
    },
891
 
892
    /**
893
    Applies the data of a given Model, modified by any column formatters and
894
    supplemented by other template values to the instance's `_rowTemplate` (see
895
    `_createRowTemplate`).  The generated string is then returned.
896
 
897
    The data from Model's attributes is fetched by `toJSON` and this data
898
    object is appended with other properties to supply values to {placeholders}
899
    in the template.  For a template generated from a Model with 'foo' and 'bar'
900
    attributes, the data object would end up with the following properties
901
    before being used to populate the `_rowTemplate`:
902
 
903
      * `clientID` - From Model, used the assign the `<tr>`'s 'id' attribute.
904
      * `foo` - The value to populate the 'foo' column cell content.  This
905
        value will be the value stored in the Model's `foo` attribute, or the
906
        result of the column's `formatter` if assigned.  If the value is '',
907
        `null`, or `undefined`, and the column's `emptyCellValue` is assigned,
908
        that value will be used.
909
      * `bar` - Same for the 'bar' column cell content.
910
      * `foo-className` - String of CSS classes to apply to the `<td>`.
911
      * `bar-className` - Same.
912
      * `rowClass`      - String of CSS classes to apply to the `<tr>`. This
913
        will be the odd/even class per the specified index plus any additional
914
        classes assigned by column formatters (via `o.rowClass`).
915
 
916
    Because this object is available to formatters, any additional properties
917
    can be added to fill in custom {placeholders} in the `_rowTemplate`.
918
 
919
    @method _createRowHTML
920
    @param {Model} model The Model instance to apply to the row template
921
    @param {Number} index The index the row will be appearing
922
    @param {Object[]} displayCols The column configurations
923
    @return {String} The markup for the provided Model, less any `nodeFormatter`s
924
    @protected
925
    @since 3.5.0
926
    **/
927
    _createRowHTML: function (model, index, displayCols) {
928
        var data     = model.toJSON(),
929
            clientId = model.get('clientId'),
930
            values   = {
931
                rowId   : this._getRowId(clientId),
932
                clientId: clientId,
933
                rowClass: (index % 2) ? this.CLASS_ODD : this.CLASS_EVEN
934
            },
935
            host = this.host || this,
936
            i, len, col, token, value, formatterData;
937
 
938
        for (i = 0, len = displayCols.length; i < len; ++i) {
939
            col   = displayCols[i];
940
            value = data[col.key];
941
            token = col._id || col.key;
942
 
943
            values[token + '-className'] = '';
944
 
945
            if (col._formatterFn) {
946
                formatterData = {
947
                    value    : value,
948
                    data     : data,
949
                    column   : col,
950
                    record   : model,
951
                    className: '',
952
                    rowClass : '',
953
                    rowIndex : index
954
                };
955
 
956
                // Formatters can either return a value
957
                value = col._formatterFn.call(host, formatterData);
958
 
959
                // or update the value property of the data obj passed
960
                if (value === undefined) {
961
                    value = formatterData.value;
962
                }
963
 
964
                values[token + '-className'] = formatterData.className;
965
                values.rowClass += ' ' + formatterData.rowClass;
966
            }
967
 
968
            // if the token missing OR is the value a legit value
969
            if (!values.hasOwnProperty(token) || data.hasOwnProperty(col.key)) {
970
                if (value === undefined || value === null || value === '') {
971
                    value = col.emptyCellValue || '';
972
                }
973
 
974
                values[token] = col.allowHTML ? value : htmlEscape(value);
975
            }
976
        }
977
 
978
        // replace consecutive whitespace with a single space
979
        values.rowClass = values.rowClass.replace(/\s+/g, ' ');
980
 
981
        return fromTemplate(this._rowTemplate, values);
982
    },
983
 
984
    /**
985
     Locates the row within the tbodyNode and returns the found index, or Null
986
     if it is not found in the tbodyNode
987
     @param {Node} row
988
     @return {Number} Index of row in tbodyNode
989
     */
990
    _getRowIndex: function (row) {
991
        var tbody = this.tbodyNode,
992
            index = 1;
993
 
994
        if (tbody && row) {
995
 
996
            //if row is not in the tbody, return
997
            if (row.ancestor('tbody') !== tbody) {
998
                return null;
999
            }
1000
 
1001
            // increment until we no longer have a previous node
1002
            /*jshint boss: true*/
1003
            while (row = row.previous()) { // NOTE: assignment
1004
            /*jshint boss: false*/
1005
                index++;
1006
            }
1007
        }
1008
 
1009
        return index;
1010
    },
1011
 
1012
    /**
1013
    Creates a custom HTML template string for use in generating the markup for
1014
    individual table rows with {placeholder}s to capture data from the Models
1015
    in the `modelList` attribute or from column `formatter`s.
1016
 
1017
    Assigns the `_rowTemplate` property.
1018
 
1019
    @method _createRowTemplate
1020
    @param {Object[]} displayCols Array of column configuration objects
1021
    @protected
1022
    @since 3.5.0
1023
    **/
1024
    _createRowTemplate: function (displayCols) {
1025
        var html         = '',
1026
            cellTemplate = this.CELL_TEMPLATE,
1027
            i, len, col, key, token, headers, tokenValues, formatter;
1028
 
1029
        this._setColumnsFormatterFn(displayCols);
1030
 
1031
        for (i = 0, len = displayCols.length; i < len; ++i) {
1032
            col     = displayCols[i];
1033
            key     = col.key;
1034
            token   = col._id || key;
1035
            formatter = col._formatterFn;
1036
            // Only include headers if there are more than one
1037
            headers = (col._headers || []).length > 1 ?
1038
                        'headers="' + col._headers.join(' ') + '"' : '';
1039
 
1040
            tokenValues = {
1041
                content  : '{' + token + '}',
1042
                headers  : headers,
1043
                className: this.getClassName('col', token) + ' ' +
1044
                           (col.className || '') + ' ' +
1045
                           this.getClassName('cell') +
1046
                           ' {' + token + '-className}'
1047
            };
1048
            if (!formatter && col.formatter) {
1049
                tokenValues.content = col.formatter.replace(valueRegExp, tokenValues.content);
1050
            }
1051
 
1052
            if (col.nodeFormatter) {
1053
                // Defer all node decoration to the formatter
1054
                tokenValues.content = '';
1055
            }
1056
 
1057
            html += fromTemplate(col.cellTemplate || cellTemplate, tokenValues);
1058
        }
1059
 
1060
        this._rowTemplate = fromTemplate(this.ROW_TEMPLATE, {
1061
            content: html
1062
        });
1063
    },
1064
 
1065
    /**
1066
     Parses the columns array and defines the column's _formatterFn if there
1067
     is a formatter available on the column
1068
     @protected
1069
     @method _setColumnsFormatterFn
1070
     @param {Object[]} displayCols Array of column configuration objects
1071
 
1072
     @return {Object[]} Returns modified displayCols configuration Array
1073
     */
1074
    _setColumnsFormatterFn: function (displayCols) {
1075
        var Formatters = Y.DataTable.BodyView.Formatters,
1076
            formatter,
1077
            col,
1078
            i,
1079
            len;
1080
 
1081
        for (i = 0, len = displayCols.length; i < len; i++) {
1082
            col = displayCols[i];
1083
            formatter = col.formatter;
1084
 
1085
            if (!col._formatterFn && formatter) {
1086
                if (Lang.isFunction(formatter)) {
1087
                    col._formatterFn = formatter;
1088
                } else if (formatter in Formatters) {
1089
                    col._formatterFn = Formatters[formatter].call(this.host || this, col);
1090
                }
1091
            }
1092
        }
1093
 
1094
        return displayCols;
1095
    },
1096
 
1097
    /**
1098
    Creates the `<tbody>` node that will store the data rows.
1099
 
1100
    @method _createTBodyNode
1101
    @return {Node}
1102
    @protected
1103
    @since 3.6.0
1104
    **/
1105
    _createTBodyNode: function () {
1106
        return Y.Node.create(fromTemplate(this.TBODY_TEMPLATE, {
1107
            className: this.getClassName('data')
1108
        }));
1109
    },
1110
 
1111
    /**
1112
    Destroys the instance.
1113
 
1114
    @method destructor
1115
    @protected
1116
    @since 3.5.0
1117
    **/
1118
    destructor: function () {
1119
        (new Y.EventHandle(YObject.values(this._eventHandles))).detach();
1120
    },
1121
 
1122
    /**
1123
    Holds the event subscriptions needing to be detached when the instance is
1124
    `destroy()`ed.
1125
 
1126
    @property _eventHandles
1127
    @type {Object}
1128
    @default undefined (initially unset)
1129
    @protected
1130
    @since 3.5.0
1131
    **/
1132
    //_eventHandles: null,
1133
 
1134
    /**
1135
    Returns the row ID associated with a Model's clientId.
1136
 
1137
    @method _getRowId
1138
    @param {String} clientId The Model clientId
1139
    @return {String}
1140
    @protected
1141
    **/
1142
    _getRowId: function (clientId) {
1143
        return this._idMap[clientId] || (this._idMap[clientId] = Y.guid());
1144
    },
1145
 
1146
    /**
1147
    Map of Model clientIds to row ids.
1148
 
1149
    @property _idMap
1150
    @type {Object}
1151
    @protected
1152
    **/
1153
    //_idMap,
1154
 
1155
    /**
1156
    Initializes the instance. Reads the following configuration properties in
1157
    addition to the instance attributes:
1158
 
1159
      * `columns` - (REQUIRED) The initial column information
1160
      * `host`    - The object to serve as source of truth for column info and
1161
                    for generating class names
1162
 
1163
    @method initializer
1164
    @param {Object} config Configuration data
1165
    @protected
1166
    @since 3.5.0
1167
    **/
1168
    initializer: function (config) {
1169
        this.host = config.host;
1170
 
1171
        this._eventHandles = {
1172
            modelListChange: this.after('modelListChange',
1173
                bind('_afterModelListChange', this))
1174
        };
1175
        this._idMap = {};
1176
 
1177
        this.CLASS_ODD  = this.getClassName('odd');
1178
        this.CLASS_EVEN = this.getClassName('even');
1179
 
1180
    }
1181
 
1182
    /**
1183
    The HTML template used to create a full row of markup for a single Model in
1184
    the `modelList` plus any customizations defined in the column
1185
    configurations.
1186
 
1187
    @property _rowTemplate
1188
    @type {String}
1189
    @default (initially unset)
1190
    @protected
1191
    @since 3.5.0
1192
    **/
1193
    //_rowTemplate: null
1194
},{
1195
    /**
1196
    Hash of formatting functions for cell contents.
1197
 
1198
    This property can be populated with a hash of formatting functions by the developer
1199
    or a set of pre-defined functions can be loaded via the `datatable-formatters` module.
1200
 
1201
    See: [DataTable.BodyView.Formatters](./DataTable.BodyView.Formatters.html)
1202
    @property Formatters
1203
    @type Object
1204
    @since 3.8.0
1205
    @static
1206
    **/
1207
    Formatters: {}
1208
});
1209
 
1210
 
1211
}, '3.18.1', {"requires": ["datatable-core", "view", "classnamemanager"]});