0d557ba2fb277571274d48f1377099335d06892f
[moodle.git] / lib / editor / atto / plugins / table / yui / build / moodle-atto_table-button / moodle-atto_table-button-debug.js
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.
14 //
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_captionposition" class="sameline">{{get_string "captionposition" component}}</label>' +
44             '<select class="{{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +
45                 '<option value=""></option>' +
46                 '<option value="top">{{get_string "top" "editor"}}</option>' +
47                 '<option value="bottom">{{get_string "bottom" "editor"}}</option>' +
48             '</select>' +
49             '<br/>' +
50             '<label for="{{elementid}}_atto_table_headers" class="sameline">{{get_string "headers" component}}</label>' +
51             '<select class="{{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
52                 '<option value="columns">{{get_string "columns" component}}' + '</option>' +
53                 '<option value="rows">{{get_string "rows" component}}' + '</option>' +
54                 '<option value="both">{{get_string "both" component}}' + '</option>' +
55             '</select>' +
56             '<br/>' +
57             '<div class="mdl-align">' +
58                 '<br/>' +
59                 '<button class="submit" type="submit">{{get_string "updatetable" component}}</button>' +
60             '</div>' +
61         '</form>',
62     TEMPLATE = '' +
63         '<form class="{{CSS.FORM}}">' +
64             '<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' +
65             '<input class="{{CSS.CAPTION}} fullwidth" id="{{elementid}}_atto_table_caption" required />' +
66             '<br/>' +
67             '<br/>' +
68             '<label for="{{elementid}}_atto_table_captionposition" class="sameline">{{get_string "captionposition" component}}</label>' +
69             '<select class="{{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +
70                 '<option value=""></option>' +
71                 '<option value="top">{{get_string "top" "editor"}}</option>' +
72                 '<option value="bottom">{{get_string "bottom" "editor"}}</option>' +
73             '</select>' +
74             '<br/>' +
75             '<label for="{{elementid}}_atto_table_headers" class="sameline">{{get_string "headers" component}}</label>' +
76             '<select class="{{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
77                 '<option value="columns">{{get_string "columns" component}}' + '</option>' +
78                 '<option value="rows">{{get_string "rows" component}}' + '</option>' +
79                 '<option value="both">{{get_string "both" component}}' + '</option>' +
80             '</select>' +
81             '<br/>' +
82             '<label for="{{elementid}}_atto_table_rows" class="sameline">{{get_string "numberofrows" component}}</label>' +
83             '<input class="{{CSS.ROWS}}" type="number" value="3" id="{{elementid}}_atto_table_rows" size="8" min="1" max="50"/>' +
84             '<br/>' +
85             '<label for="{{elementid}}_atto_table_columns" class="sameline">{{get_string "numberofcolumns" component}}</label>' +
86             '<input class="{{CSS.COLUMNS}}" type="number" value="3" id="{{elementid}}_atto_table_columns" ' +
87                     'size="8" min="1" max="20"/>' +
88             '<br/>' +
89             '<div class="mdl-align">' +
90                 '<br/>' +
91                 '<button class="{{CSS.SUBMIT}}" type="submit">{{get_string "createtable" component}}</button>' +
92             '</div>' +
93         '</form>',
94     CSS = {
95         CAPTION: 'caption',
96         CAPTIONPOSITION: 'captionposition',
97         HEADERS: 'headers',
98         ROWS: 'rows',
99         COLUMNS: 'columns',
100         SUBMIT: 'submit',
101         FORM: 'atto_form'
102     },
103     SELECTORS = {
104         CAPTION: '.' + CSS.CAPTION,
105         CAPTIONPOSITION: '.' + CSS.CAPTIONPOSITION,
106         HEADERS: '.' + CSS.HEADERS,
107         ROWS: '.' + CSS.ROWS,
108         COLUMNS: '.' + CSS.COLUMNS,
109         SUBMIT: '.' + CSS.SUBMIT,
110         FORM: '.atto_form'
111     };
113 Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
115     /**
116      * A reference to the current selection at the time that the dialogue
117      * was opened.
118      *
119      * @property _currentSelection
120      * @type Range
121      * @private
122      */
123     _currentSelection: null,
125     /**
126      * The contextual menu that we can open.
127      *
128      * @property _contextMenu
129      * @type M.editor_atto.Menu
130      * @private
131      */
132     _contextMenu: null,
134     /**
135      * The last modified target.
136      *
137      * @property _lastTarget
138      * @type Node
139      * @private
140      */
141     _lastTarget: null,
143     /**
144      * The list of menu items.
145      *
146      * @property _menuOptions
147      * @type Object
148      * @private
149      */
150     _menuOptions: null,
152     initializer: function() {
153         this.addButton({
154             icon: 'e/table',
155             callback: this._displayTableEditor,
156             tags: 'table'
157         });
159         // Disable mozilla table controls.
160         if (Y.UA.gecko) {
161             document.execCommand("enableInlineTableEditing", false, false);
162             document.execCommand("enableObjectResizing", false, false);
163         }
164     },
166     /**
167      * Display the table tool.
168      *
169      * @method _displayDialogue
170      * @private
171      */
172     _displayDialogue: function() {
173         // Store the current cursor position.
174         this._currentSelection = this.get('host').getSelection();
176         if (this._currentSelection !== false && (!this._currentSelection.collapsed)) {
177             var dialogue = this.getDialogue({
178                 headerContent: M.util.get_string('createtable', COMPONENT),
179                 focusAfterHide: true,
180                 focusOnShowSelector: SELECTORS.CAPTION
181             });
183             // Set the dialogue content, and then show the dialogue.
184             dialogue.set('bodyContent', this._getDialogueContent())
185                     .show();
186         }
187     },
189     /**
190      * Display the appropriate table editor.
191      *
192      * If the current selection includes a table, then we show the
193      * contextual menu, otherwise show the table creation dialogue.
194      *
195      * @method _displayTableEditor
196      * @param {EventFacade} e
197      * @private
198      */
199     _displayTableEditor: function(e) {
200         var cell = this._getSuitableTableCell();
201         if (cell) {
202             // Add the cell to the EventFacade to save duplication in when showing the menu.
203             e.tableCell = cell;
204             return this._showTableMenu(e);
205         }
206         return this._displayDialogue(e);
207     },
209     /**
210      * Returns whether or not the parameter node exists within the editor.
211      *
212      * @method _stopAtContentEditableFilter
213      * @param  {Node} node
214      * @private
215      * @return {boolean} whether or not the parameter node exists within the editor.
216      */
217     _stopAtContentEditableFilter: function(node) {
218         this.editor.contains(node);
219     },
221     /**
222      * Return the edit table dialogue content, attaching any required
223      * events.
224      *
225      * @method _getEditDialogueContent
226      * @private
227      * @return {Node} The content to place in the dialogue.
228      */
229     _getEditDialogueContent: function() {
230         var template = Y.Handlebars.compile(EDITTEMPLATE);
231         this._content = Y.Node.create(template({
232                 CSS: CSS,
233                 elementid: this.get('host').get('elementid'),
234                 component: COMPONENT
235             }));
237         // Handle table setting.
238         this._content.one('.submit').on('click', this._updateTable, this);
240         return this._content;
241     },
243     /**
244      * Return the dialogue content for the tool, attaching any required
245      * events.
246      *
247      * @method _getDialogueContent
248      * @private
249      * @return {Node} The content to place in the dialogue.
250      */
251     _getDialogueContent: function() {
252         var template = Y.Handlebars.compile(TEMPLATE);
253         this._content = Y.Node.create(template({
254                 CSS: CSS,
255                 elementid: this.get('host').get('elementid'),
256                 component: COMPONENT
257             }));
259         // Handle table setting.
260         this._content.one('.submit').on('click', this._setTable, this);
262         return this._content;
263     },
265     /**
266      * Given the current selection, return a table cell suitable for table editing
267      * purposes, i.e. the first table cell selected, or the first cell in the table
268      * that the selection exists in, or null if not within a table.
269      *
270      * @method _getSuitableTableCell
271      * @private
272      * @return {Node} suitable target cell, or null if not within a table
273      */
274     _getSuitableTableCell: function() {
275         var targetcell = null,
276             host = this.get('host');
278         host.getSelectedNodes().some(function (node) {
279             if (node.ancestor('td, th, caption', true, this._stopAtContentEditableFilter)) {
280                 targetcell = node;
282                 var caption = node.ancestor('caption', true, this._stopAtContentEditableFilter);
283                 if (caption) {
284                     var table = caption.get('parentNode');
285                     if (table) {
286                         targetcell = table.one('td, th');
287                     }
288                 }
290                 // Once we've found a cell to target, we shouldn't need to keep looking.
291                 return true;
292             }
293         });
295         if (targetcell) {
296             var selection = host.getSelectionFromNode(targetcell);
297             host.setSelection(selection);
298         }
300         return targetcell;
301     },
303     /**
304      * Change a node from one type to another, copying all attributes and children.
305      *
306      * @method _changeNodeType
307      * @param {Y.Node} node
308      * @param {String} new node type
309      * @private
310      * @chainable
311      */
312     _changeNodeType: function(node, newType) {
313         var newNode = Y.Node.create('<' + newType + '></' + newType + '>');
314         newNode.setAttrs(node.getAttrs());
315         node.get('childNodes').each(function(child) {
316             newNode.append(child.remove());
317         });
318         node.replace(newNode);
319         return newNode;
320     },
322     /**
323      * Handle updating an existing table.
324      *
325      * @method _updateTable
326      * @param {EventFacade} e
327      * @private
328      */
329     _updateTable: function(e) {
330         var caption,
331             captionposition,
332             headers,
333             table,
334             captionnode;
336         e.preventDefault();
337         // Hide the dialogue.
338         this.getDialogue({
339             focusAfterHide: null
340         }).hide();
342         // Add/update the caption.
343         caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
344         captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
345         headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
347         table = this._lastTarget.ancestor('table');
349         captionnode = table.one('caption');
350         if (!captionnode) {
351             captionnode = Y.Node.create('<caption></caption>');
352             table.insert(captionnode, 0);
353         }
354         captionnode.setHTML(caption.get('value'));
355         captionnode.setStyle('caption-side', captionposition.get('value'));
356         if (!captionnode.getAttribute('style')) {
357             captionnode.removeAttribute('style');
358         }
360         // Add the row headers.
361         if (headers.get('value') === 'rows' || headers.get('value') === 'both') {
362             table.all('tr').each(function (row) {
363                 var cells = row.all('th, td'),
364                     firstCell = cells.shift(),
365                     newCell;
367                 if (firstCell.get('tagName') === 'TD') {
368                     // Cell is a td but should be a th - change it.
369                     newCell = this._changeNodeType(firstCell, 'th');
370                     newCell.setAttribute('scope', 'row');
371                 } else {
372                     firstCell.setAttribute('scope', 'row');
373                 }
375                 // Now make sure all other cells in the row are td.
376                 cells.each(function (cell) {
377                     if (cell.get('tagName') === 'TH') {
378                         newCell = this._changeNodeType(cell, 'td');
379                         newCell.removeAttribute('scope');
380                     }
381                 }, this);
383             }, this);
384         }
385         // Add the col headers. These may overrule the row headers in the first cell.
386         if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
387             var rows = table.all('tr'),
388                 firstRow = rows.shift(),
389                 newCell;
391             firstRow.all('td, th').each(function (cell) {
392                 if (cell.get('tagName') === 'TD') {
393                     // Cell is a td but should be a th - change it.
394                     newCell = this._changeNodeType(cell, 'th');
395                     newCell.setAttribute('scope', 'col');
396                 } else {
397                     cell.setAttribute('scope', 'col');
398                 }
399             }, this);
400             // Change all the cells in the rest of the table to tds (unless they are row headers).
401             rows.each(function(row) {
402                 var cells = row.all('th, td');
404                 if (headers.get('value') === 'both') {
405                     // Ignore the first cell because it's a row header.
406                     cells.shift();
407                 }
408                 cells.each(function(cell) {
409                     if (cell.get('tagName') === 'TH') {
410                         newCell = this._changeNodeType(cell, 'td');
411                         newCell.removeAttribute('scope');
412                     }
413                 }, this);
415             }, this);
416         }
417         // Clean the HTML.
418         this.markUpdated();
419     },
421     /**
422      * Handle creation of a new table.
423      *
424      * @method _setTable
425      * @param {EventFacade} e
426      * @private
427      */
428     _setTable: function(e) {
429         var caption,
430             captionposition,
431             rows,
432             cols,
433             headers,
434             tablehtml,
435             i, j;
437         e.preventDefault();
439         // Hide the dialogue.
440         this.getDialogue({
441             focusAfterHide: null
442         }).hide();
444         caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
445         captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
446         rows = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.ROWS);
447         cols = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.COLUMNS);
448         headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
450         // Set the selection.
451         this.get('host').setSelection(this._currentSelection);
453         // Note there are some spaces inserted in the cells and before and after, so that users have somewhere to click.
454         var nl = "\n";
455         tablehtml = '<br/>' + nl + '<table>' + nl;
457         var captionstyle = '';
458         if (captionposition.get('value')) {
459             captionstyle = ' style="caption-side: ' + captionposition.get('value') + '"';
460         }
461         tablehtml += '<caption' + captionstyle + '>' + Y.Escape.html(caption.get('value')) + '</caption>' + nl;
462         i = 0;
463         if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
464             i = 1;
465             tablehtml += '<thead>' + nl + '<tr>' + nl;
466             for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
467                 tablehtml += '<th scope="col"></th>' + nl;
468             }
469             tablehtml += '</tr>' + nl + '</thead>' + nl;
470         }
471         tablehtml += '<tbody>' + nl;
472         for (; i < parseInt(rows.get('value'), 10); i++) {
473             tablehtml += '<tr>' + nl;
474             for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
475                 if (j === 0 && (headers.get('value') === 'rows' || headers.get('value') === 'both')) {
476                     tablehtml += '<th scope="row"></th>' + nl;
477                 } else {
478                     tablehtml += '<td></td>' + nl;
479                 }
480             }
481             tablehtml += '</tr>' + nl;
482         }
483         tablehtml += '</tbody>' + nl;
484         tablehtml += '</table>' + nl + '<br/>';
486         this.get('host').insertContentAtFocusPoint(tablehtml);
488         // Mark the content as updated.
489         this.markUpdated();
490     },
492     /**
493      * Search for all the cells in the current, next and previous columns.
494      *
495      * @method _findColumnCells
496      * @private
497      * @return {Object} containing current, prev and next {Y.NodeList}s
498      */
499     _findColumnCells: function() {
500         var columnindex = this._getColumnIndex(this._lastTarget),
501             rows = this._lastTarget.ancestor('table').all('tr'),
502             currentcells = new Y.NodeList(),
503             prevcells = new Y.NodeList(),
504             nextcells = new Y.NodeList();
506         rows.each(function(row) {
507             var cells = row.all('td, th'),
508                 cell = cells.item(columnindex),
509                 cellprev = cells.item(columnindex-1),
510                 cellnext = cells.item(columnindex+1);
511             currentcells.push(cell);
512             if (cellprev) {
513                 prevcells.push(cellprev);
514             }
515             if (cellnext) {
516                 nextcells.push(cellnext);
517             }
518         });
520         return {
521             current: currentcells,
522             prev: prevcells,
523             next: nextcells
524         };
525     },
527     /**
528      * Hide the entries in the context menu that don't make sense with the
529      * current selection.
530      *
531      * @method _hideInvalidEntries
532      * @param {Y.Node} node - The node containing the menu.
533      * @private
534      */
535     _hideInvalidEntries: function(node) {
536         // Moving rows.
537         var table = this._lastTarget.ancestor('table'),
538             row = this._lastTarget.ancestor('tr'),
539             rows = table.all('tr'),
540             rowindex = rows.indexOf(row),
541             prevrow = rows.item(rowindex - 1),
542             prevrowhascells = prevrow ? prevrow.one('td') : null;
544         if (!row || !prevrowhascells) {
545             node.one('[data-change="moverowup"]').hide();
546         } else {
547             node.one('[data-change="moverowup"]').show();
548         }
550         var nextrow = rows.item(rowindex + 1),
551             rowhascell = row ? row.one('td') : false;
553         if (!row || !nextrow || !rowhascell) {
554             node.one('[data-change="moverowdown"]').hide();
555         } else {
556             node.one('[data-change="moverowdown"]').show();
557         }
559         // Moving columns.
560         var cells = this._findColumnCells();
561         if (cells.prev.filter('td').size() > 0) {
562             node.one('[data-change="movecolumnleft"]').show();
563         } else {
564             node.one('[data-change="movecolumnleft"]').hide();
565         }
567         var colhascell = cells.current.filter('td').size() > 0;
568         if ((cells.next.size() > 0) && colhascell) {
569             node.one('[data-change="movecolumnright"]').show();
570         } else {
571             node.one('[data-change="movecolumnright"]').hide();
572         }
574         // Delete col
575         if (cells.current.filter('td').size() > 0) {
576             node.one('[data-change="deletecolumn"]').show();
577         } else {
578             node.one('[data-change="deletecolumn"]').hide();
579         }
580         // Delete row
581         if (!row || !row.one('td')) {
582             node.one('[data-change="deleterow"]').hide();
583         } else {
584             node.one('[data-change="deleterow"]').show();
585         }
586     },
588     /**
589      * Display the table menu.
590      *
591      * @method _showTableMenu
592      * @param {EventFacade} e
593      * @private
594      */
595     _showTableMenu: function(e) {
596         e.preventDefault();
598         var boundingBox;
600         if (!this._contextMenu) {
601             this._menuOptions = [
602                 {
603                     text: M.util.get_string("addcolumnafter", COMPONENT),
604                     data: {
605                         change: "addcolumnafter"
606                     }
607                 }, {
608                     text: M.util.get_string("addrowafter", COMPONENT),
609                     data: {
610                         change: "addrowafter"
611                     }
612                 }, {
613                     text: M.util.get_string("moverowup", COMPONENT),
614                     data: {
615                         change: "moverowup"
616                     }
617                 }, {
618                     text: M.util.get_string("moverowdown", COMPONENT),
619                     data: {
620                         change: "moverowdown"
621                     }
622                 }, {
623                     text: M.util.get_string("movecolumnleft", COMPONENT),
624                     data: {
625                         change: "movecolumnleft"
626                     }
627                 }, {
628                     text: M.util.get_string("movecolumnright", COMPONENT),
629                     data: {
630                         change: "movecolumnright"
631                     }
632                 }, {
633                     text: M.util.get_string("deleterow", COMPONENT),
634                     data: {
635                         change: "deleterow"
636                     }
637                 }, {
638                     text: M.util.get_string("deletecolumn", COMPONENT),
639                     data: {
640                         change: "deletecolumn"
641                     }
642                 }, {
643                     text: M.util.get_string("edittable", COMPONENT),
644                     data: {
645                         change: "edittable"
646                     }
647                 }
648             ];
650             this._contextMenu = new Y.M.editor_atto.Menu({
651                 items: this._menuOptions
652             });
654             // Add event handlers for table control menus.
655             boundingBox = this._contextMenu.get('boundingBox');
656             boundingBox.delegate('click', this._handleTableChange, 'a', this);
657         }
659         boundingBox = this._contextMenu.get('boundingBox');
661         // We store the cell of the last click (the control node is transient).
662         this._lastTarget = e.tableCell.ancestor('.editor_atto_content td, .editor_atto_content th', true);
664         this._hideInvalidEntries(boundingBox);
666         // Clear the focusAfterHide for any other menus which may be open.
667         Y.Array.each(this.get('host').openMenus, function(menu) {
668             menu.set('focusAfterHide', null);
669         });
671         // Ensure that we focus on the button in the toolbar when we tab back to the menu.
672         var creatorButton = this.buttons[this.name];
673         this.get('host')._setTabFocus(creatorButton);
675         // Show the context menu, and align to the current position.
676         this._contextMenu.show();
677         this._contextMenu.align(this.buttons.table, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
678         this._contextMenu.set('focusAfterHide', creatorButton);
680         // If there are any anchors in the bounding box, focus on the first.
681         if (boundingBox.one('a')) {
682             boundingBox.one('a').focus();
683         }
685         // Add this menu to the list of open menus.
686         this.get('host').openMenus = [this._contextMenu];
687     },
689     /**
690      * Handle a selection from the table control menu.
691      *
692      * @method _handleTableChange
693      * @param {EventFacade} e
694      * @private
695      */
696     _handleTableChange: function(e) {
697         e.preventDefault();
699         this._contextMenu.set('focusAfterHide', this.get('host').editor);
700         // Hide the context menu.
701         this._contextMenu.hide(e);
703         // Make our changes.
704         switch (e.target.getData('change')) {
705             case 'addcolumnafter':
706                 this._addColumnAfter();
707                 break;
708             case 'addrowafter':
709                 this._addRowAfter();
710                 break;
711             case 'deleterow':
712                 this._deleteRow();
713                 break;
714             case 'deletecolumn':
715                 this._deleteColumn();
716                 break;
717             case 'edittable':
718                 this._editTable();
719                 break;
720             case 'moverowdown':
721                 this._moveRowDown();
722                 break;
723             case 'moverowup':
724                 this._moveRowUp();
725                 break;
726             case 'movecolumnleft':
727                 this._moveColumnLeft();
728                 break;
729             case 'movecolumnright':
730                 this._moveColumnRight();
731                 break;
732         }
733     },
735     /**
736      * Determine the index of a row in a table column.
737      *
738      * @method _getRowIndex
739      * @param {Node} cell
740      * @private
741      */
742     _getRowIndex: function(cell) {
743         var tablenode = cell.ancestor('table'),
744             rownode = cell.ancestor('tr');
746         if (!tablenode || !rownode) {
747             return;
748         }
750         var rows = tablenode.all('tr');
752         return rows.indexOf(rownode);
753     },
755     /**
756      * Determine the index of a column in a table row.
757      *
758      * @method _getColumnIndex
759      * @param {Node} cellnode
760      * @private
761      */
762     _getColumnIndex: function(cellnode) {
763         var rownode = cellnode.ancestor('tr');
765         if (!rownode) {
766             return;
767         }
769         var cells = rownode.all('td, th');
771         return cells.indexOf(cellnode);
772     },
774     /**
775      * Delete the current row.
776      *
777      * @method _deleteRow
778      * @private
779      */
780     _deleteRow: function() {
781         var row = this._lastTarget.ancestor('tr');
783         if (row && row.one('td')) {
784             // Only delete rows with at least one non-header cell.
785             row.remove(true);
786         }
788         // Clean the HTML.
789         this.markUpdated();
790     },
792     /**
793      * Move row up
794      *
795      * @method _moveRowUp
796      * @private
797      */
798     _moveRowUp: function() {
799         var row = this._lastTarget.ancestor('tr'),
800             prevrow = row.previous('tr');
801         if (!row || !prevrow) {
802             return;
803         }
805         row.swap(prevrow);
806         // Clean the HTML.
807         this.markUpdated();
808     },
810     /**
811      * Move column left
812      *
813      * @method _moveColumnLeft
814      * @private
815      */
816     _moveColumnLeft: function() {
817         var cells = this._findColumnCells();
819         if (cells.current.size() > 0 && cells.prev.size() > 0 && cells.current.size() === cells.prev.size()) {
820             var i = 0;
821             for (i = 0; i < cells.current.size(); i++) {
822                 var cell = cells.current.item(i),
823                     prevcell = cells.prev.item(i);
825                 cell.swap(prevcell);
826             }
827         }
828         // Cleanup.
829         this.markUpdated();
830     },
832     /**
833      * Add a caption to the table if it doesn't have one.
834      *
835      * @method _addCaption
836      * @private
837      */
838     _addCaption: function() {
839         var table = this._lastTarget.ancestor('table'),
840             caption = table.one('caption');
842         if (!caption) {
843             table.insert(Y.Node.create('<caption>&nbsp;</caption>'), 1);
844         }
845     },
847     /**
848      * Remove a caption from the table if has one.
849      *
850      * @method _removeCaption
851      * @private
852      */
853     _removeCaption: function() {
854         var table = this._lastTarget.ancestor('table'),
855             caption = table.one('caption');
857         if (caption) {
858             caption.remove(true);
859         }
860     },
862     /**
863      * Move column right.
864      *
865      * @method _moveColumnRight
866      * @private
867      */
868     _moveColumnRight: function() {
869         var cells = this._findColumnCells();
871         // Check we have some tds in this column, and one exists to the right.
872         if ( (cells.next.size() > 0) &&
873                 (cells.current.size() === cells.next.size()) &&
874                 (cells.current.filter('td').size() > 0)) {
875             var i = 0;
876             for (i = 0; i < cells.current.size(); i++) {
877                 var cell = cells.current.item(i),
878                     nextcell = cells.next.item(i);
880                 cell.swap(nextcell);
881             }
882         }
883         // Cleanup.
884         this.markUpdated();
885     },
887     /**
888      * Move row down.
889      *
890      * @method _moveRowDown
891      * @private
892      */
893     _moveRowDown: function() {
894         var row = this._lastTarget.ancestor('tr'),
895             nextrow = row.next('tr');
896         if (!row || !nextrow || !row.one('td')) {
897             return;
898         }
900         row.swap(nextrow);
901         // Clean the HTML.
902         this.markUpdated();
903     },
905     /**
906      * Edit table (show the dialogue).
907      *
908      * @method _editTable
909      * @private
910      */
911     _editTable: function() {
912         var dialogue = this.getDialogue({
913             headerContent: M.util.get_string('edittable', COMPONENT),
914             focusAfterHide: false,
915             focusOnShowSelector: SELECTORS.CAPTION
916         });
918         // Set the dialogue content, and then show the dialogue.
919         var node = this._getEditDialogueContent(),
920             captioninput = node.one(SELECTORS.CAPTION),
921             captionpositioninput = node.one(SELECTORS.CAPTIONPOSITION),
922             headersinput = node.one(SELECTORS.HEADERS),
923             table = this._lastTarget.ancestor('table'),
924             captionnode = table.one('caption');
926         if (captionnode) {
927             captioninput.set('value', captionnode.getHTML());
928         } else {
929             captioninput.set('value', '');
930         }
932         if (captionpositioninput && captionnode.getAttribute('style')) {
933             captionpositioninput.set('value', captionnode.getStyle('caption-side'));
934         } else {
935             // Default to none.
936             captionpositioninput.set('value', '');
937         }
939         var headersvalue = 'columns';
940         if (table.one('th[scope="row"]')) {
941             headersvalue = 'rows';
942             if (table.one('th[scope="col"]')) {
943                 headersvalue = 'both';
944             }
945         }
946         headersinput.set('value', headersvalue);
947         dialogue.set('bodyContent', node).show();
948     },
951     /**
952      * Delete the current column.
953      *
954      * @method _deleteColumn
955      * @private
956      */
957     _deleteColumn: function() {
958         var columnindex = this._getColumnIndex(this._lastTarget),
959             table = this._lastTarget.ancestor('table'),
960             rows = table.all('tr'),
961             columncells = new Y.NodeList(),
962             hastd = false;
964         rows.each(function(row) {
965             var cells = row.all('td, th');
966             var cell = cells.item(columnindex);
967             if (cell.get('tagName') === 'TD') {
968                 hastd = true;
969             }
970             columncells.push(cell);
971         });
973         // Do not delete all the headers.
974         if (hastd) {
975             columncells.remove(true);
976         }
978         // Clean the HTML.
979         this.markUpdated();
980     },
982     /**
983      * Add a row after the current row.
984      *
985      * @method _addRowAfter
986      * @private
987      */
988     _addRowAfter: function() {
989         var target = this._lastTarget.ancestor('tr'),
990             tablebody = this._lastTarget.ancestor('table').one('tbody');
991         if (!tablebody) {
992             // Not all tables have tbody.
993             tablebody = this._lastTarget.ancestor('table');
994         }
996         var firstrow = tablebody.one('tr');
997         if (!firstrow) {
998             firstrow = this._lastTarget.ancestor('table').one('tr');
999         }
1000         if (!firstrow) {
1001             // Table has no rows. Boo.
1002             return;
1003         }
1004         var newrow = firstrow.cloneNode(true);
1005         newrow.all('th, td').each(function (tablecell) {
1006             if (tablecell.get('tagName') === 'TH') {
1007                 if (tablecell.getAttribute('scope') !== 'row') {
1008                     var newcell = Y.Node.create('<td></td>');
1009                     tablecell.replace(newcell);
1010                     tablecell = newcell;
1011                 }
1012             }
1013             tablecell.setHTML('&nbsp;');
1014         });
1016         if (target.ancestor('thead')) {
1017             target = firstrow;
1018             tablebody.insert(newrow, target);
1019         } else {
1020             target.insert(newrow, 'after');
1021         }
1023         // Clean the HTML.
1024         this.markUpdated();
1025     },
1027     /**
1028      * Add a column after the current column.
1029      *
1030      * @method _addColumnAfter
1031      * @private
1032      */
1033     _addColumnAfter: function() {
1034         var cells = this._findColumnCells(),
1035             before = true,
1036             clonecells = cells.next;
1037         if (cells.next.size() <= 0) {
1038             before = false;
1039             clonecells = cells.current;
1040         }
1042         Y.each(clonecells, function(cell) {
1043             var newcell = cell.cloneNode();
1044             // Clear the content of the cell.
1045             newcell.setHTML('&nbsp;');
1047             if (before) {
1048                 cell.get('parentNode').insert(newcell, cell);
1049             } else {
1050                 cell.get('parentNode').insert(newcell, cell);
1051                 cell.swap(newcell);
1052             }
1053         }, this);
1055         // Clean the HTML.
1056         this.markUpdated();
1057     }
1059 });
1062 }, '@VERSION@', {"requires": ["moodle-editor_atto-plugin", "moodle-editor_atto-menu", "event", "event-valuechange"]});