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