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