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