423b9cc3a9563305b2f5d170c5382d5f3335d4ab
[moodle.git] / lib / editor / atto / plugins / table / yui / src / button / js / button.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * @package    atto_table
18  * @copyright  2013 Damyon Wiese  <damyon@moodle.com>
19  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
20  */
22 /**
23  * @module moodle-atto_table-button
24  */
26 /**
27  * Atto text editor table plugin.
28  *
29  * @namespace M.atto_table
30  * @class Button
31  * @extends M.editor_atto.EditorPlugin
32  */
34 var COMPONENT = 'atto_table',
35     EDITTEMPLATE = '' +
36         '<form class="{{CSS.FORM}}">' +
37             '<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' +
38             '<input class="{{CSS.CAPTION}} fullwidth" id="{{elementid}}_atto_table_caption" required />' +
39             '<br/>' +
40             '<br/>' +
41             '<label for="{{elementid}}_atto_table_captionposition" class="sameline">{{get_string "captionposition" component}}</label>' +
42             '<select class="{{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +
43                 '<option value=""></option>' +
44                 '<option value="top">{{get_string "top" "editor"}}</option>' +
45                 '<option value="bottom">{{get_string "bottom" "editor"}}</option>' +
46             '</select>' +
47             '<br/>' +
48             '<label for="{{elementid}}_atto_table_headers" class="sameline">{{get_string "headers" component}}</label>' +
49             '<select class="{{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
50                 '<option value="columns">{{get_string "columns" component}}' + '</option>' +
51                 '<option value="rows">{{get_string "rows" component}}' + '</option>' +
52                 '<option value="both">{{get_string "both" component}}' + '</option>' +
53             '</select>' +
54             '<br/>' +
55             '<div class="mdl-align">' +
56                 '<br/>' +
57                 '<button class="submit" type="submit">{{get_string "updatetable" component}}</button>' +
58             '</div>' +
59         '</form>',
60     TEMPLATE = '' +
61         '<form class="{{CSS.FORM}}">' +
62             '<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' +
63             '<input class="{{CSS.CAPTION}} fullwidth" id="{{elementid}}_atto_table_caption" required />' +
64             '<br/>' +
65             '<br/>' +
66             '<label for="{{elementid}}_atto_table_captionposition" class="sameline">{{get_string "captionposition" component}}</label>' +
67             '<select class="{{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +
68                 '<option value=""></option>' +
69                 '<option value="top">{{get_string "top" "editor"}}</option>' +
70                 '<option value="bottom">{{get_string "bottom" "editor"}}</option>' +
71             '</select>' +
72             '<br/>' +
73             '<label for="{{elementid}}_atto_table_headers" class="sameline">{{get_string "headers" component}}</label>' +
74             '<select class="{{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
75                 '<option value="columns">{{get_string "columns" component}}' + '</option>' +
76                 '<option value="rows">{{get_string "rows" component}}' + '</option>' +
77                 '<option value="both">{{get_string "both" component}}' + '</option>' +
78             '</select>' +
79             '<br/>' +
80             '<label for="{{elementid}}_atto_table_rows" class="sameline">{{get_string "numberofrows" component}}</label>' +
81             '<input class="{{CSS.ROWS}}" type="number" value="3" id="{{elementid}}_atto_table_rows" size="8" min="1" max="50"/>' +
82             '<br/>' +
83             '<label for="{{elementid}}_atto_table_columns" class="sameline">{{get_string "numberofcolumns" component}}</label>' +
84             '<input class="{{CSS.COLUMNS}}" type="number" value="3" id="{{elementid}}_atto_table_columns" ' +
85                     'size="8" min="1" max="20"/>' +
86             '<br/>' +
87             '<div class="mdl-align">' +
88                 '<br/>' +
89                 '<button class="{{CSS.SUBMIT}}" type="submit">{{get_string "createtable" component}}</button>' +
90             '</div>' +
91         '</form>',
92     CSS = {
93         CAPTION: 'caption',
94         CAPTIONPOSITION: 'captionposition',
95         HEADERS: 'headers',
96         ROWS: 'rows',
97         COLUMNS: 'columns',
98         SUBMIT: 'submit',
99         FORM: 'atto_form'
100     },
101     SELECTORS = {
102         CAPTION: '.' + CSS.CAPTION,
103         CAPTIONPOSITION: '.' + CSS.CAPTIONPOSITION,
104         HEADERS: '.' + CSS.HEADERS,
105         ROWS: '.' + CSS.ROWS,
106         COLUMNS: '.' + CSS.COLUMNS,
107         SUBMIT: '.' + CSS.SUBMIT,
108         FORM: '.atto_form'
109     };
111 Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
113     /**
114      * A reference to the current selection at the time that the dialogue
115      * was opened.
116      *
117      * @property _currentSelection
118      * @type Range
119      * @private
120      */
121     _currentSelection: null,
123     /**
124      * The contextual menu that we can open.
125      *
126      * @property _contextMenu
127      * @type M.editor_atto.Menu
128      * @private
129      */
130     _contextMenu: null,
132     /**
133      * The last modified target.
134      *
135      * @property _lastTarget
136      * @type Node
137      * @private
138      */
139     _lastTarget: null,
141     /**
142      * The list of menu items.
143      *
144      * @property _menuOptions
145      * @type Object
146      * @private
147      */
148     _menuOptions: null,
150     initializer: function() {
151         this.addButton({
152             icon: 'e/table',
153             callback: this._displayTableEditor,
154             tags: 'table'
155         });
157         // Disable mozilla table controls.
158         if (Y.UA.gecko) {
159             document.execCommand("enableInlineTableEditing", false, false);
160             document.execCommand("enableObjectResizing", false, false);
161         }
162     },
164     /**
165      * Display the table tool.
166      *
167      * @method _displayDialogue
168      * @private
169      */
170     _displayDialogue: function() {
171         // Store the current cursor position.
172         this._currentSelection = this.get('host').getSelection();
174         if (this._currentSelection !== false && (!this._currentSelection.collapsed)) {
175             var dialogue = this.getDialogue({
176                 headerContent: M.util.get_string('createtable', COMPONENT),
177                 focusAfterHide: true,
178                 focusOnShowSelector: SELECTORS.CAPTION
179             });
181             // Set the dialogue content, and then show the dialogue.
182             dialogue.set('bodyContent', this._getDialogueContent())
183                     .show();
184         }
185     },
187     /**
188      * Display the appropriate table editor.
189      *
190      * If the current selection includes a table, then we show the
191      * contextual menu, otherwise show the table creation dialogue.
192      *
193      * @method _displayTableEditor
194      * @param {EventFacade} e
195      * @private
196      */
197     _displayTableEditor: function(e) {
198         var cell = this._getSuitableTableCell();
199         if (cell) {
200             // Add the cell to the EventFacade to save duplication in when showing the menu.
201             e.tableCell = cell;
202             return this._showTableMenu(e);
203         }
204         return this._displayDialogue(e);
205     },
207     /**
208      * Returns whether or not the parameter node exists within the editor.
209      *
210      * @method _stopAtContentEditableFilter
211      * @param  {Node} node
212      * @private
213      * @return {boolean} whether or not the parameter node exists within the editor.
214      */
215     _stopAtContentEditableFilter: function(node) {
216         this.editor.contains(node);
217     },
219     /**
220      * Return the edit table dialogue content, attaching any required
221      * events.
222      *
223      * @method _getEditDialogueContent
224      * @private
225      * @return {Node} The content to place in the dialogue.
226      */
227     _getEditDialogueContent: function() {
228         var template = Y.Handlebars.compile(EDITTEMPLATE);
229         this._content = Y.Node.create(template({
230                 CSS: CSS,
231                 elementid: this.get('host').get('elementid'),
232                 component: COMPONENT
233             }));
235         // Handle table setting.
236         this._content.one('.submit').on('click', this._updateTable, this);
238         return this._content;
239     },
241     /**
242      * Return the dialogue content for the tool, attaching any required
243      * events.
244      *
245      * @method _getDialogueContent
246      * @private
247      * @return {Node} The content to place in the dialogue.
248      */
249     _getDialogueContent: function() {
250         var template = Y.Handlebars.compile(TEMPLATE);
251         this._content = Y.Node.create(template({
252                 CSS: CSS,
253                 elementid: this.get('host').get('elementid'),
254                 component: COMPONENT
255             }));
257         // Handle table setting.
258         this._content.one('.submit').on('click', this._setTable, this);
260         return this._content;
261     },
263     /**
264      * Given the current selection, return a table cell suitable for table editing
265      * purposes, i.e. the first table cell selected, or the first cell in the table
266      * that the selection exists in, or null if not within a table.
267      *
268      * @method _getSuitableTableCell
269      * @private
270      * @return {Node} suitable target cell, or null if not within a table
271      */
272     _getSuitableTableCell: function() {
273         var targetcell = null,
274             host = this.get('host');
276         host.getSelectedNodes().some(function (node) {
277             if (node.ancestor('td, th, caption', true, this._stopAtContentEditableFilter)) {
278                 targetcell = node;
280                 var caption = node.ancestor('caption', true, this._stopAtContentEditableFilter);
281                 if (caption) {
282                     var table = caption.get('parentNode');
283                     if (table) {
284                         targetcell = table.one('td, th');
285                     }
286                 }
288                 // Once we've found a cell to target, we shouldn't need to keep looking.
289                 return true;
290             }
291         });
293         if (targetcell) {
294             var selection = host.getSelectionFromNode(targetcell);
295             host.setSelection(selection);
296         }
298         return targetcell;
299     },
301     /**
302      * Change a node from one type to another, copying all attributes and children.
303      *
304      * @method _changeNodeType
305      * @param {Y.Node} node
306      * @param {String} new node type
307      * @private
308      * @chainable
309      */
310     _changeNodeType: function(node, newType) {
311         var newNode = Y.Node.create('<' + newType + '></' + newType + '>');
312         newNode.setAttrs(node.getAttrs());
313         node.get('childNodes').each(function(child) {
314             newNode.append(child.remove());
315         });
316         node.replace(newNode);
317         return newNode;
318     },
320     /**
321      * Handle updating an existing table.
322      *
323      * @method _updateTable
324      * @param {EventFacade} e
325      * @private
326      */
327     _updateTable: function(e) {
328         var caption,
329             captionposition,
330             headers,
331             table,
332             captionnode;
334         e.preventDefault();
335         // Hide the dialogue.
336         this.getDialogue({
337             focusAfterHide: null
338         }).hide();
340         // Add/update the caption.
341         caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
342         captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
343         headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
345         table = this._lastTarget.ancestor('table');
347         captionnode = table.one('caption');
348         if (!captionnode) {
349             captionnode = Y.Node.create('<caption></caption>');
350             table.insert(captionnode, 0);
351         }
352         captionnode.setHTML(caption.get('value'));
353         captionnode.setStyle('caption-side', captionposition.get('value'));
354         if (!captionnode.getAttribute('style')) {
355             captionnode.removeAttribute('style');
356         }
358         // Add the row headers.
359         if (headers.get('value') === 'rows' || headers.get('value') === 'both') {
360             table.all('tr').each(function (row) {
361                 var cells = row.all('th, td'),
362                     firstCell = cells.shift(),
363                     newCell;
365                 if (firstCell.get('tagName') === 'TD') {
366                     // Cell is a td but should be a th - change it.
367                     newCell = this._changeNodeType(firstCell, 'th');
368                     newCell.setAttribute('scope', 'row');
369                 } else {
370                     firstCell.setAttribute('scope', 'row');
371                 }
373                 // Now make sure all other cells in the row are td.
374                 cells.each(function (cell) {
375                     if (cell.get('tagName') === 'TH') {
376                         newCell = this._changeNodeType(cell, 'td');
377                         newCell.removeAttribute('scope');
378                     }
379                 }, this);
381             }, this);
382         }
383         // Add the col headers. These may overrule the row headers in the first cell.
384         if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
385             var rows = table.all('tr'),
386                 firstRow = rows.shift(),
387                 newCell;
389             firstRow.all('td, th').each(function (cell) {
390                 if (cell.get('tagName') === 'TD') {
391                     // Cell is a td but should be a th - change it.
392                     newCell = this._changeNodeType(cell, 'th');
393                     newCell.setAttribute('scope', 'col');
394                 } else {
395                     cell.setAttribute('scope', 'col');
396                 }
397             }, this);
398             // Change all the cells in the rest of the table to tds (unless they are row headers).
399             rows.each(function(row) {
400                 var cells = row.all('th, td');
402                 if (headers.get('value') === 'both') {
403                     // Ignore the first cell because it's a row header.
404                     cells.shift();
405                 }
406                 cells.each(function(cell) {
407                     if (cell.get('tagName') === 'TH') {
408                         newCell = this._changeNodeType(cell, 'td');
409                         newCell.removeAttribute('scope');
410                     }
411                 }, this);
413             }, this);
414         }
415         // Clean the HTML.
416         this.markUpdated();
417     },
419     /**
420      * Handle creation of a new table.
421      *
422      * @method _setTable
423      * @param {EventFacade} e
424      * @private
425      */
426     _setTable: function(e) {
427         var caption,
428             captionposition,
429             rows,
430             cols,
431             headers,
432             tablehtml,
433             i, j;
435         e.preventDefault();
437         // Hide the dialogue.
438         this.getDialogue({
439             focusAfterHide: null
440         }).hide();
442         caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
443         captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
444         rows = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.ROWS);
445         cols = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.COLUMNS);
446         headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
448         // Set the selection.
449         this.get('host').setSelection(this._currentSelection);
451         // Note there are some spaces inserted in the cells and before and after, so that users have somewhere to click.
452         var nl = "\n";
453         tablehtml = '<br/>' + nl + '<table>' + nl;
455         var captionstyle = '';
456         if (captionposition.get('value')) {
457             captionstyle = ' style="caption-side: ' + captionposition.get('value') + '"';
458         }
459         tablehtml += '<caption' + captionstyle + '>' + Y.Escape.html(caption.get('value')) + '</caption>' + nl;
460         i = 0;
461         if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
462             i = 1;
463             tablehtml += '<thead>' + nl + '<tr>' + nl;
464             for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
465                 tablehtml += '<th scope="col"></th>' + nl;
466             }
467             tablehtml += '</tr>' + nl + '</thead>' + nl;
468         }
469         tablehtml += '<tbody>' + nl;
470         for (; i < parseInt(rows.get('value'), 10); i++) {
471             tablehtml += '<tr>' + nl;
472             for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
473                 if (j === 0 && (headers.get('value') === 'rows' || headers.get('value') === 'both')) {
474                     tablehtml += '<th scope="row"></th>' + nl;
475                 } else {
476                     tablehtml += '<td></td>' + nl;
477                 }
478             }
479             tablehtml += '</tr>' + nl;
480         }
481         tablehtml += '</tbody>' + nl;
482         tablehtml += '</table>' + nl + '<br/>';
484         this.get('host').insertContentAtFocusPoint(tablehtml);
486         // Mark the content as updated.
487         this.markUpdated();
488     },
490     /**
491      * Search for all the cells in the current, next and previous columns.
492      *
493      * @method _findColumnCells
494      * @private
495      * @return {Object} containing current, prev and next {Y.NodeList}s
496      */
497     _findColumnCells: function() {
498         var columnindex = this._getColumnIndex(this._lastTarget),
499             rows = this._lastTarget.ancestor('table').all('tr'),
500             currentcells = new Y.NodeList(),
501             prevcells = new Y.NodeList(),
502             nextcells = new Y.NodeList();
504         rows.each(function(row) {
505             var cells = row.all('td, th'),
506                 cell = cells.item(columnindex),
507                 cellprev = cells.item(columnindex-1),
508                 cellnext = cells.item(columnindex+1);
509             currentcells.push(cell);
510             if (cellprev) {
511                 prevcells.push(cellprev);
512             }
513             if (cellnext) {
514                 nextcells.push(cellnext);
515             }
516         });
518         return {
519             current: currentcells,
520             prev: prevcells,
521             next: nextcells
522         };
523     },
525     /**
526      * Hide the entries in the context menu that don't make sense with the
527      * current selection.
528      *
529      * @method _hideInvalidEntries
530      * @param {Y.Node} node - The node containing the menu.
531      * @private
532      */
533     _hideInvalidEntries: function(node) {
534         // Moving rows.
535         var table = this._lastTarget.ancestor('table'),
536             row = this._lastTarget.ancestor('tr'),
537             rows = table.all('tr'),
538             rowindex = rows.indexOf(row),
539             prevrow = rows.item(rowindex - 1),
540             prevrowhascells = prevrow ? prevrow.one('td') : null;
542         if (!row || !prevrowhascells) {
543             node.one('[data-change="moverowup"]').hide();
544         } else {
545             node.one('[data-change="moverowup"]').show();
546         }
548         var nextrow = rows.item(rowindex + 1),
549             rowhascell = row ? row.one('td') : false;
551         if (!row || !nextrow || !rowhascell) {
552             node.one('[data-change="moverowdown"]').hide();
553         } else {
554             node.one('[data-change="moverowdown"]').show();
555         }
557         // Moving columns.
558         var cells = this._findColumnCells();
559         if (cells.prev.filter('td').size() > 0) {
560             node.one('[data-change="movecolumnleft"]').show();
561         } else {
562             node.one('[data-change="movecolumnleft"]').hide();
563         }
565         var colhascell = cells.current.filter('td').size() > 0;
566         if ((cells.next.size() > 0) && colhascell) {
567             node.one('[data-change="movecolumnright"]').show();
568         } else {
569             node.one('[data-change="movecolumnright"]').hide();
570         }
572         // Delete col
573         if (cells.current.filter('td').size() > 0) {
574             node.one('[data-change="deletecolumn"]').show();
575         } else {
576             node.one('[data-change="deletecolumn"]').hide();
577         }
578         // Delete row
579         if (!row || !row.one('td')) {
580             node.one('[data-change="deleterow"]').hide();
581         } else {
582             node.one('[data-change="deleterow"]').show();
583         }
584     },
586     /**
587      * Display the table menu.
588      *
589      * @method _showTableMenu
590      * @param {EventFacade} e
591      * @private
592      */
593     _showTableMenu: function(e) {
594         e.preventDefault();
596         var boundingBox;
598         if (!this._contextMenu) {
599             this._menuOptions = [
600                 {
601                     text: M.util.get_string("addcolumnafter", COMPONENT),
602                     data: {
603                         change: "addcolumnafter"
604                     }
605                 }, {
606                     text: M.util.get_string("addrowafter", COMPONENT),
607                     data: {
608                         change: "addrowafter"
609                     }
610                 }, {
611                     text: M.util.get_string("moverowup", COMPONENT),
612                     data: {
613                         change: "moverowup"
614                     }
615                 }, {
616                     text: M.util.get_string("moverowdown", COMPONENT),
617                     data: {
618                         change: "moverowdown"
619                     }
620                 }, {
621                     text: M.util.get_string("movecolumnleft", COMPONENT),
622                     data: {
623                         change: "movecolumnleft"
624                     }
625                 }, {
626                     text: M.util.get_string("movecolumnright", COMPONENT),
627                     data: {
628                         change: "movecolumnright"
629                     }
630                 }, {
631                     text: M.util.get_string("deleterow", COMPONENT),
632                     data: {
633                         change: "deleterow"
634                     }
635                 }, {
636                     text: M.util.get_string("deletecolumn", COMPONENT),
637                     data: {
638                         change: "deletecolumn"
639                     }
640                 }, {
641                     text: M.util.get_string("edittable", COMPONENT),
642                     data: {
643                         change: "edittable"
644                     }
645                 }
646             ];
648             this._contextMenu = new Y.M.editor_atto.Menu({
649                 items: this._menuOptions
650             });
652             // Add event handlers for table control menus.
653             boundingBox = this._contextMenu.get('boundingBox');
654             boundingBox.delegate('click', this._handleTableChange, 'a', this);
655         }
657         boundingBox = this._contextMenu.get('boundingBox');
659         // We store the cell of the last click (the control node is transient).
660         this._lastTarget = e.tableCell.ancestor('.editor_atto_content td, .editor_atto_content th', true);
662         this._hideInvalidEntries(boundingBox);
664         // Clear the focusAfterHide for any other menus which may be open.
665         Y.Array.each(this.get('host').openMenus, function(menu) {
666             menu.set('focusAfterHide', null);
667         });
669         // Ensure that we focus on the button in the toolbar when we tab back to the menu.
670         var creatorButton = this.buttons[this.name];
671         this.get('host')._setTabFocus(creatorButton);
673         // Show the context menu, and align to the current position.
674         this._contextMenu.show();
675         this._contextMenu.align(this.buttons.table, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
676         this._contextMenu.set('focusAfterHide', creatorButton);
678         // If there are any anchors in the bounding box, focus on the first.
679         if (boundingBox.one('a')) {
680             boundingBox.one('a').focus();
681         }
683         // Add this menu to the list of open menus.
684         this.get('host').openMenus = [this._contextMenu];
685     },
687     /**
688      * Handle a selection from the table control menu.
689      *
690      * @method _handleTableChange
691      * @param {EventFacade} e
692      * @private
693      */
694     _handleTableChange: function(e) {
695         e.preventDefault();
697         this._contextMenu.set('focusAfterHide', this.get('host').editor);
698         // Hide the context menu.
699         this._contextMenu.hide(e);
701         // Make our changes.
702         switch (e.target.getData('change')) {
703             case 'addcolumnafter':
704                 this._addColumnAfter();
705                 break;
706             case 'addrowafter':
707                 this._addRowAfter();
708                 break;
709             case 'deleterow':
710                 this._deleteRow();
711                 break;
712             case 'deletecolumn':
713                 this._deleteColumn();
714                 break;
715             case 'edittable':
716                 this._editTable();
717                 break;
718             case 'moverowdown':
719                 this._moveRowDown();
720                 break;
721             case 'moverowup':
722                 this._moveRowUp();
723                 break;
724             case 'movecolumnleft':
725                 this._moveColumnLeft();
726                 break;
727             case 'movecolumnright':
728                 this._moveColumnRight();
729                 break;
730         }
731     },
733     /**
734      * Determine the index of a row in a table column.
735      *
736      * @method _getRowIndex
737      * @param {Node} cell
738      * @private
739      */
740     _getRowIndex: function(cell) {
741         var tablenode = cell.ancestor('table'),
742             rownode = cell.ancestor('tr');
744         if (!tablenode || !rownode) {
745             return;
746         }
748         var rows = tablenode.all('tr');
750         return rows.indexOf(rownode);
751     },
753     /**
754      * Determine the index of a column in a table row.
755      *
756      * @method _getColumnIndex
757      * @param {Node} cellnode
758      * @private
759      */
760     _getColumnIndex: function(cellnode) {
761         var rownode = cellnode.ancestor('tr');
763         if (!rownode) {
764             return;
765         }
767         var cells = rownode.all('td, th');
769         return cells.indexOf(cellnode);
770     },
772     /**
773      * Delete the current row.
774      *
775      * @method _deleteRow
776      * @private
777      */
778     _deleteRow: function() {
779         var row = this._lastTarget.ancestor('tr');
781         if (row && row.one('td')) {
782             // Only delete rows with at least one non-header cell.
783             row.remove(true);
784         }
786         // Clean the HTML.
787         this.markUpdated();
788     },
790     /**
791      * Move row up
792      *
793      * @method _moveRowUp
794      * @private
795      */
796     _moveRowUp: function() {
797         var row = this._lastTarget.ancestor('tr'),
798             prevrow = row.previous('tr');
799         if (!row || !prevrow) {
800             return;
801         }
803         row.swap(prevrow);
804         // Clean the HTML.
805         this.markUpdated();
806     },
808     /**
809      * Move column left
810      *
811      * @method _moveColumnLeft
812      * @private
813      */
814     _moveColumnLeft: function() {
815         var cells = this._findColumnCells();
817         if (cells.current.size() > 0 && cells.prev.size() > 0 && cells.current.size() === cells.prev.size()) {
818             var i = 0;
819             for (i = 0; i < cells.current.size(); i++) {
820                 var cell = cells.current.item(i),
821                     prevcell = cells.prev.item(i);
823                 cell.swap(prevcell);
824             }
825         }
826         // Cleanup.
827         this.markUpdated();
828     },
830     /**
831      * Add a caption to the table if it doesn't have one.
832      *
833      * @method _addCaption
834      * @private
835      */
836     _addCaption: function() {
837         var table = this._lastTarget.ancestor('table'),
838             caption = table.one('caption');
840         if (!caption) {
841             table.insert(Y.Node.create('<caption>&nbsp;</caption>'), 1);
842         }
843     },
845     /**
846      * Remove a caption from the table if has one.
847      *
848      * @method _removeCaption
849      * @private
850      */
851     _removeCaption: function() {
852         var table = this._lastTarget.ancestor('table'),
853             caption = table.one('caption');
855         if (caption) {
856             caption.remove(true);
857         }
858     },
860     /**
861      * Move column right.
862      *
863      * @method _moveColumnRight
864      * @private
865      */
866     _moveColumnRight: function() {
867         var cells = this._findColumnCells();
869         // Check we have some tds in this column, and one exists to the right.
870         if ( (cells.next.size() > 0) &&
871                 (cells.current.size() === cells.next.size()) &&
872                 (cells.current.filter('td').size() > 0)) {
873             var i = 0;
874             for (i = 0; i < cells.current.size(); i++) {
875                 var cell = cells.current.item(i),
876                     nextcell = cells.next.item(i);
878                 cell.swap(nextcell);
879             }
880         }
881         // Cleanup.
882         this.markUpdated();
883     },
885     /**
886      * Move row down.
887      *
888      * @method _moveRowDown
889      * @private
890      */
891     _moveRowDown: function() {
892         var row = this._lastTarget.ancestor('tr'),
893             nextrow = row.next('tr');
894         if (!row || !nextrow || !row.one('td')) {
895             return;
896         }
898         row.swap(nextrow);
899         // Clean the HTML.
900         this.markUpdated();
901     },
903     /**
904      * Edit table (show the dialogue).
905      *
906      * @method _editTable
907      * @private
908      */
909     _editTable: function() {
910         var dialogue = this.getDialogue({
911             headerContent: M.util.get_string('edittable', COMPONENT),
912             focusAfterHide: false,
913             focusOnShowSelector: SELECTORS.CAPTION
914         });
916         // Set the dialogue content, and then show the dialogue.
917         var node = this._getEditDialogueContent(),
918             captioninput = node.one(SELECTORS.CAPTION),
919             captionpositioninput = node.one(SELECTORS.CAPTIONPOSITION),
920             headersinput = node.one(SELECTORS.HEADERS),
921             table = this._lastTarget.ancestor('table'),
922             captionnode = table.one('caption');
924         if (captionnode) {
925             captioninput.set('value', captionnode.getHTML());
926         } else {
927             captioninput.set('value', '');
928         }
930         if (captionpositioninput && captionnode.getAttribute('style')) {
931             captionpositioninput.set('value', captionnode.getStyle('caption-side'));
932         } else {
933             // Default to none.
934             captionpositioninput.set('value', '');
935         }
937         var headersvalue = 'columns';
938         if (table.one('th[scope="row"]')) {
939             headersvalue = 'rows';
940             if (table.one('th[scope="col"]')) {
941                 headersvalue = 'both';
942             }
943         }
944         headersinput.set('value', headersvalue);
945         dialogue.set('bodyContent', node).show();
946     },
949     /**
950      * Delete the current column.
951      *
952      * @method _deleteColumn
953      * @private
954      */
955     _deleteColumn: function() {
956         var columnindex = this._getColumnIndex(this._lastTarget),
957             table = this._lastTarget.ancestor('table'),
958             rows = table.all('tr'),
959             columncells = new Y.NodeList(),
960             hastd = false;
962         rows.each(function(row) {
963             var cells = row.all('td, th');
964             var cell = cells.item(columnindex);
965             if (cell.get('tagName') === 'TD') {
966                 hastd = true;
967             }
968             columncells.push(cell);
969         });
971         // Do not delete all the headers.
972         if (hastd) {
973             columncells.remove(true);
974         }
976         // Clean the HTML.
977         this.markUpdated();
978     },
980     /**
981      * Add a row after the current row.
982      *
983      * @method _addRowAfter
984      * @private
985      */
986     _addRowAfter: function() {
987         var target = this._lastTarget.ancestor('tr'),
988             tablebody = this._lastTarget.ancestor('table').one('tbody');
989         if (!tablebody) {
990             // Not all tables have tbody.
991             tablebody = this._lastTarget.ancestor('table');
992         }
994         var firstrow = tablebody.one('tr');
995         if (!firstrow) {
996             firstrow = this._lastTarget.ancestor('table').one('tr');
997         }
998         if (!firstrow) {
999             // Table has no rows. Boo.
1000             return;
1001         }
1002         var newrow = firstrow.cloneNode(true);
1003         newrow.all('th, td').each(function (tablecell) {
1004             if (tablecell.get('tagName') === 'TH') {
1005                 if (tablecell.getAttribute('scope') !== 'row') {
1006                     var newcell = Y.Node.create('<td></td>');
1007                     tablecell.replace(newcell);
1008                     tablecell = newcell;
1009                 }
1010             }
1011             tablecell.setHTML('&nbsp;');
1012         });
1014         if (target.ancestor('thead')) {
1015             target = firstrow;
1016             tablebody.insert(newrow, target);
1017         } else {
1018             target.insert(newrow, 'after');
1019         }
1021         // Clean the HTML.
1022         this.markUpdated();
1023     },
1025     /**
1026      * Add a column after the current column.
1027      *
1028      * @method _addColumnAfter
1029      * @private
1030      */
1031     _addColumnAfter: function() {
1032         var cells = this._findColumnCells(),
1033             before = true,
1034             clonecells = cells.next;
1035         if (cells.next.size() <= 0) {
1036             before = false;
1037             clonecells = cells.current;
1038         }
1040         Y.each(clonecells, function(cell) {
1041             var newcell = cell.cloneNode();
1042             // Clear the content of the cell.
1043             newcell.setHTML('&nbsp;');
1045             if (before) {
1046                 cell.get('parentNode').insert(newcell, cell);
1047             } else {
1048                 cell.get('parentNode').insert(newcell, cell);
1049                 cell.swap(newcell);
1050             }
1051         }, this);
1053         // Clean the HTML.
1054         this.markUpdated();
1055     }
1057 });