on-demand release 3.0dev+
[moodle.git] / lib / editor / atto / plugins / table / yui / build / moodle-atto_table-button / moodle-atto_table-button.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/>' +
556decf1
DM
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/>' +
353473aa
DW
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>',
62467795 62 TEMPLATE = '' +
353473aa 63 '<form class="{{CSS.FORM}}">' +
62467795 64 '<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' +
353473aa
DW
65 '<input class="{{CSS.CAPTION}} fullwidth" id="{{elementid}}_atto_table_caption" required />' +
66 '<br/>' +
62467795 67 '<br/>' +
556decf1
DM
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/>' +
62467795 75 '<label for="{{elementid}}_atto_table_headers" class="sameline">{{get_string "headers" component}}</label>' +
353473aa 76 '<select class="{{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
62467795
AN
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>' +
353473aa 83 '<input class="{{CSS.ROWS}}" type="number" value="3" id="{{elementid}}_atto_table_rows" size="8" min="1" max="50"/>' +
62467795
AN
84 '<br/>' +
85 '<label for="{{elementid}}_atto_table_columns" class="sameline">{{get_string "numberofcolumns" component}}</label>' +
557f44d9
AN
86 '<input class="{{CSS.COLUMNS}}" type="number" value="3" id="{{elementid}}_atto_table_columns" ' +
87 'size="8" min="1" max="20"/>' +
62467795
AN
88 '<br/>' +
89 '<div class="mdl-align">' +
90 '<br/>' +
353473aa 91 '<button class="{{CSS.SUBMIT}}" type="submit">{{get_string "createtable" component}}</button>' +
62467795 92 '</div>' +
23cead68 93 '</form>',
62467795 94 CSS = {
353473aa 95 CAPTION: 'caption',
556decf1 96 CAPTIONPOSITION: 'captionposition',
353473aa
DW
97 HEADERS: 'headers',
98 ROWS: 'rows',
99 COLUMNS: 'columns',
100 SUBMIT: 'submit',
101 FORM: 'atto_form'
102 },
103 SELECTORS = {
104 CAPTION: '.' + CSS.CAPTION,
556decf1 105 CAPTIONPOSITION: '.' + CSS.CAPTIONPOSITION,
353473aa
DW
106 HEADERS: '.' + CSS.HEADERS,
107 ROWS: '.' + CSS.ROWS,
108 COLUMNS: '.' + CSS.COLUMNS,
109 SUBMIT: '.' + CSS.SUBMIT,
110 FORM: '.atto_form'
62467795
AN
111 };
112
113Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
adca7326
DW
114
115 /**
62467795
AN
116 * A reference to the current selection at the time that the dialogue
117 * was opened.
adca7326 118 *
62467795
AN
119 * @property _currentSelection
120 * @type Range
121 * @private
adca7326 122 */
62467795 123 _currentSelection: null,
adca7326
DW
124
125 /**
62467795 126 * The contextual menu that we can open.
adca7326 127 *
62467795
AN
128 * @property _contextMenu
129 * @type M.editor_atto.Menu
130 * @private
131 */
132 _contextMenu: null,
133
134 /**
135 * The last modified target.
136 *
137 * @property _lastTarget
138 * @type Node
139 * @private
adca7326 140 */
62467795
AN
141 _lastTarget: null,
142
ee616cff
AN
143 /**
144 * The list of menu items.
145 *
146 * @property _menuOptions
147 * @type Object
148 * @private
149 */
150 _menuOptions: null,
151
62467795
AN
152 initializer: function() {
153 this.addButton({
154 icon: 'e/table',
155 callback: this._displayTableEditor,
156 tags: 'table'
157 });
158
159 // Disable mozilla table controls.
160 if (Y.UA.gecko) {
161 document.execCommand("enableInlineTableEditing", false, false);
162 document.execCommand("enableObjectResizing", false, false);
163 }
164 },
adca7326
DW
165
166 /**
62467795 167 * Display the table tool.
adca7326 168 *
62467795
AN
169 * @method _displayDialogue
170 * @private
adca7326 171 */
62467795
AN
172 _displayDialogue: function() {
173 // Store the current cursor position.
174 this._currentSelection = this.get('host').getSelection();
175
176 if (this._currentSelection !== false && (!this._currentSelection.collapsed)) {
177 var dialogue = this.getDialogue({
178 headerContent: M.util.get_string('createtable', COMPONENT),
e5ddec38 179 focusAfterHide: true,
c1660772 180 focusOnShowSelector: SELECTORS.CAPTION
62467795
AN
181 });
182
183 // Set the dialogue content, and then show the dialogue.
184 dialogue.set('bodyContent', this._getDialogueContent())
185 .show();
186 }
187 },
adca7326
DW
188
189 /**
62467795 190 * Display the appropriate table editor.
adca7326 191 *
62467795
AN
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
adca7326 198 */
62467795 199 _displayTableEditor: function(e) {
f0ddce4d 200 var cell = this._getSuitableTableCell();
62467795
AN
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 }
7b280674 206 return this._displayDialogue(e);
3a6511a5
JC
207 },
208
f0ddce4d
JC
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 },
220
353473aa
DW
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 }));
236
237 // Handle table setting.
238 this._content.one('.submit').on('click', this._updateTable, this);
239
240 return this._content;
241 },
242
adca7326 243 /**
62467795
AN
244 * Return the dialogue content for the tool, attaching any required
245 * events.
adca7326 246 *
62467795
AN
247 * @method _getDialogueContent
248 * @private
249 * @return {Node} The content to place in the dialogue.
adca7326 250 */
62467795
AN
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 }));
258
259 // Handle table setting.
260 this._content.one('.submit').on('click', this._setTable, this);
261
262 return this._content;
263 },
adca7326 264
f0ddce4d
JC
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');
277
278 host.getSelectedNodes().some(function (node) {
279 if (node.ancestor('td, th, caption', true, this._stopAtContentEditableFilter)) {
280 targetcell = node;
281
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 }
289
290 // Once we've found a cell to target, we shouldn't need to keep looking.
291 return true;
292 }
293 });
294
295 if (targetcell) {
296 var selection = host.getSelectionFromNode(targetcell);
297 host.setSelection(selection);
298 }
299
300 return targetcell;
301 },
302
353473aa
DW
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 },
321
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,
556decf1 331 captionposition,
353473aa
DW
332 headers,
333 table,
334 captionnode;
335
336 e.preventDefault();
337 // Hide the dialogue.
338 this.getDialogue({
339 focusAfterHide: null
340 }).hide();
341
342 // Add/update the caption.
343 caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
556decf1 344 captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
353473aa
DW
345 headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
346
347 table = this._lastTarget.ancestor('table');
348
349 captionnode = table.one('caption');
350 if (!captionnode) {
556decf1 351 captionnode = Y.Node.create('<caption></caption>');
353473aa
DW
352 table.insert(captionnode, 0);
353 }
354 captionnode.setHTML(caption.get('value'));
556decf1
DM
355 captionnode.setStyle('caption-side', captionposition.get('value'));
356 if (!captionnode.getAttribute('style')) {
357 captionnode.removeAttribute('style');
358 }
353473aa
DW
359
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;
366
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 }
374
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);
382
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;
390
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');
403
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);
414
415 }, this);
416 }
7d8f825b
DW
417 // Clean the HTML.
418 this.markUpdated();
353473aa
DW
419 },
420
adca7326 421 /**
62467795 422 * Handle creation of a new table.
adca7326 423 *
62467795
AN
424 * @method _setTable
425 * @param {EventFacade} e
426 * @private
adca7326 427 */
62467795
AN
428 _setTable: function(e) {
429 var caption,
556decf1 430 captionposition,
62467795
AN
431 rows,
432 cols,
433 headers,
434 tablehtml,
435 i, j;
436
adca7326 437 e.preventDefault();
adca7326 438
62467795
AN
439 // Hide the dialogue.
440 this.getDialogue({
441 focusAfterHide: null
442 }).hide();
443
48dc9f01 444 caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
556decf1 445 captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
353473aa
DW
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);
62467795
AN
449
450 // Set the selection.
451 this.get('host').setSelection(this._currentSelection);
452
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;
adca7326 456
556decf1
DM
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;
62467795
AN
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;
adca7326 482 }
62467795
AN
483 tablehtml += '</tbody>' + nl;
484 tablehtml += '</table>' + nl + '<br/>';
adca7326 485
62467795
AN
486 this.get('host').insertContentAtFocusPoint(tablehtml);
487
488 // Mark the content as updated.
489 this.markUpdated();
adca7326
DW
490 },
491
353473aa
DW
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();
505
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 });
519
520 return {
521 current: currentcells,
522 prev: prevcells,
523 next: nextcells
524 };
525 },
526
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;
543
544 if (!row || !prevrowhascells) {
545 node.one('[data-change="moverowup"]').hide();
546 } else {
547 node.one('[data-change="moverowup"]').show();
548 }
549
550 var nextrow = rows.item(rowindex + 1),
551 rowhascell = row ? row.one('td') : false;
552
553 if (!row || !nextrow || !rowhascell) {
554 node.one('[data-change="moverowdown"]').hide();
555 } else {
556 node.one('[data-change="moverowdown"]').show();
557 }
558
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 }
566
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 }
573
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 },
587
adca7326 588 /**
62467795 589 * Display the table menu.
adca7326 590 *
62467795
AN
591 * @method _showTableMenu
592 * @param {EventFacade} e
593 * @private
adca7326 594 */
62467795 595 _showTableMenu: function(e) {
adca7326
DW
596 e.preventDefault();
597
62467795
AN
598 var boundingBox;
599
600 if (!this._contextMenu) {
ee616cff
AN
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 }
92810c07
DW
642 }, {
643 text: M.util.get_string("edittable", COMPONENT),
644 data: {
645 change: "edittable"
646 }
ee616cff
AN
647 }
648 ];
62467795
AN
649
650 this._contextMenu = new Y.M.editor_atto.Menu({
ee616cff 651 items: this._menuOptions
adca7326 652 });
62467795
AN
653
654 // Add event handlers for table control menus.
655 boundingBox = this._contextMenu.get('boundingBox');
656 boundingBox.delegate('click', this._handleTableChange, 'a', this);
adca7326 657 }
ee616cff 658
62467795
AN
659 boundingBox = this._contextMenu.get('boundingBox');
660
adca7326 661 // We store the cell of the last click (the control node is transient).
62467795
AN
662 this._lastTarget = e.tableCell.ancestor('.editor_atto_content td, .editor_atto_content th', true);
663
353473aa
DW
664 this._hideInvalidEntries(boundingBox);
665
c63f9053
AN
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 });
670
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);
674
62467795
AN
675 // Show the context menu, and align to the current position.
676 this._contextMenu.show();
f8c3af13 677 this._contextMenu.align(this.buttons.table, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
c63f9053 678 this._contextMenu.set('focusAfterHide', creatorButton);
62467795
AN
679
680 // If there are any anchors in the bounding box, focus on the first.
681 if (boundingBox.one('a')) {
682 boundingBox.one('a').focus();
26f8822d 683 }
c63f9053
AN
684
685 // Add this menu to the list of open menus.
686 this.get('host').openMenus = [this._contextMenu];
62467795
AN
687 },
688
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();
adca7326 698
ee616cff 699 this._contextMenu.set('focusAfterHide', this.get('host').editor);
62467795 700 // Hide the context menu.
ee616cff 701 this._contextMenu.hide(e);
62467795
AN
702
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;
353473aa
DW
717 case 'edittable':
718 this._editTable();
719 break;
62467795
AN
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;
adca7326
DW
732 }
733 },
734
735 /**
736 * Determine the index of a row in a table column.
737 *
62467795
AN
738 * @method _getRowIndex
739 * @param {Node} cell
740 * @private
adca7326 741 */
62467795 742 _getRowIndex: function(cell) {
adca7326
DW
743 var tablenode = cell.ancestor('table'),
744 rownode = cell.ancestor('tr');
745
746 if (!tablenode || !rownode) {
747 return;
748 }
749
750 var rows = tablenode.all('tr');
751
752 return rows.indexOf(rownode);
753 },
754
755 /**
756 * Determine the index of a column in a table row.
757 *
62467795
AN
758 * @method _getColumnIndex
759 * @param {Node} cellnode
760 * @private
adca7326 761 */
62467795 762 _getColumnIndex: function(cellnode) {
adca7326
DW
763 var rownode = cellnode.ancestor('tr');
764
765 if (!rownode) {
766 return;
767 }
768
769 var cells = rownode.all('td, th');
770
771 return cells.indexOf(cellnode);
772 },
773
774 /**
62467795 775 * Delete the current row.
adca7326 776 *
62467795
AN
777 * @method _deleteRow
778 * @private
adca7326 779 */
62467795
AN
780 _deleteRow: function() {
781 var row = this._lastTarget.ancestor('tr');
adca7326 782
353473aa
DW
783 if (row && row.one('td')) {
784 // Only delete rows with at least one non-header cell.
785 row.remove(true);
adca7326
DW
786 }
787
788 // Clean the HTML.
62467795 789 this.markUpdated();
adca7326
DW
790 },
791
792 /**
793 * Move row up
794 *
62467795
AN
795 * @method _moveRowUp
796 * @private
adca7326 797 */
62467795 798 _moveRowUp: function() {
353473aa
DW
799 var row = this._lastTarget.ancestor('tr'),
800 prevrow = row.previous('tr');
adca7326
DW
801 if (!row || !prevrow) {
802 return;
803 }
804
805 row.swap(prevrow);
806 // Clean the HTML.
62467795 807 this.markUpdated();
adca7326
DW
808 },
809
810 /**
811 * Move column left
812 *
62467795
AN
813 * @method _moveColumnLeft
814 * @private
adca7326 815 */
62467795 816 _moveColumnLeft: function() {
353473aa 817 var cells = this._findColumnCells();
adca7326 818
353473aa 819 if (cells.current.size() > 0 && cells.prev.size() > 0 && cells.current.size() === cells.prev.size()) {
adca7326 820 var i = 0;
353473aa
DW
821 for (i = 0; i < cells.current.size(); i++) {
822 var cell = cells.current.item(i),
823 prevcell = cells.prev.item(i);
adca7326
DW
824
825 cell.swap(prevcell);
826 }
827 }
828 // Cleanup.
62467795 829 this.markUpdated();
adca7326
DW
830 },
831
353473aa
DW
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');
841
842 if (!caption) {
843 table.insert(Y.Node.create('<caption>&nbsp;</caption>'), 1);
844 }
845 },
846
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');
856
857 if (caption) {
858 caption.remove(true);
859 }
860 },
861
adca7326 862 /**
62467795 863 * Move column right.
adca7326 864 *
62467795
AN
865 * @method _moveColumnRight
866 * @private
adca7326 867 */
62467795 868 _moveColumnRight: function() {
353473aa 869 var cells = this._findColumnCells();
adca7326 870
353473aa
DW
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)) {
adca7326 875 var i = 0;
353473aa
DW
876 for (i = 0; i < cells.current.size(); i++) {
877 var cell = cells.current.item(i),
878 nextcell = cells.next.item(i);
adca7326
DW
879
880 cell.swap(nextcell);
881 }
882 }
883 // Cleanup.
62467795 884 this.markUpdated();
adca7326
DW
885 },
886
887 /**
62467795 888 * Move row down.
adca7326 889 *
62467795
AN
890 * @method _moveRowDown
891 * @private
adca7326 892 */
62467795 893 _moveRowDown: function() {
353473aa
DW
894 var row = this._lastTarget.ancestor('tr'),
895 nextrow = row.next('tr');
896 if (!row || !nextrow || !row.one('td')) {
adca7326
DW
897 return;
898 }
899
900 row.swap(nextrow);
901 // Clean the HTML.
62467795 902 this.markUpdated();
adca7326
DW
903 },
904
353473aa
DW
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),
e5ddec38 914 focusAfterHide: false,
c1660772 915 focusOnShowSelector: SELECTORS.CAPTION
353473aa
DW
916 });
917
918 // Set the dialogue content, and then show the dialogue.
919 var node = this._getEditDialogueContent(),
920 captioninput = node.one(SELECTORS.CAPTION),
556decf1 921 captionpositioninput = node.one(SELECTORS.CAPTIONPOSITION),
353473aa
DW
922 headersinput = node.one(SELECTORS.HEADERS),
923 table = this._lastTarget.ancestor('table'),
924 captionnode = table.one('caption');
925
926 if (captionnode) {
927 captioninput.set('value', captionnode.getHTML());
928 } else {
929 captioninput.set('value', '');
930 }
931
bcd58d9f 932 if (captionpositioninput && captionnode && captionnode.getAttribute('style')) {
556decf1
DM
933 captionpositioninput.set('value', captionnode.getStyle('caption-side'));
934 } else {
935 // Default to none.
936 captionpositioninput.set('value', '');
937 }
938
353473aa
DW
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 },
949
950
adca7326 951 /**
62467795 952 * Delete the current column.
adca7326 953 *
62467795
AN
954 * @method _deleteColumn
955 * @private
adca7326 956 */
62467795 957 _deleteColumn: function() {
353473aa
DW
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;
adca7326
DW
963
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 });
972
353473aa 973 // Do not delete all the headers.
adca7326
DW
974 if (hastd) {
975 columncells.remove(true);
976 }
977
978 // Clean the HTML.
62467795 979 this.markUpdated();
adca7326
DW
980 },
981
982 /**
983 * Add a row after the current row.
984 *
62467795
AN
985 * @method _addRowAfter
986 * @private
adca7326 987 */
62467795 988 _addRowAfter: function() {
aa65ecea 989 var target = this._lastTarget.ancestor('tr'),
990 tablebody = this._lastTarget.ancestor('table').one('tbody');
adca7326
DW
991 if (!tablebody) {
992 // Not all tables have tbody.
62467795 993 tablebody = this._lastTarget.ancestor('table');
adca7326
DW
994 }
995
996 var firstrow = tablebody.one('tr');
997 if (!firstrow) {
62467795 998 firstrow = this._lastTarget.ancestor('table').one('tr');
adca7326
DW
999 }
1000 if (!firstrow) {
1001 // Table has no rows. Boo.
1002 return;
1003 }
557f44d9 1004 var newrow = firstrow.cloneNode(true);
adca7326
DW
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 });
1015
aa65ecea 1016 if (target.ancestor('thead')) {
1017 target = firstrow;
1018 tablebody.insert(newrow, target);
1019 } else {
1020 target.insert(newrow, 'after');
1021 }
adca7326
DW
1022
1023 // Clean the HTML.
62467795 1024 this.markUpdated();
adca7326
DW
1025 },
1026
1027 /**
1028 * Add a column after the current column.
1029 *
62467795
AN
1030 * @method _addColumnAfter
1031 * @private
adca7326 1032 */
62467795 1033 _addColumnAfter: function() {
353473aa
DW
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 }
adca7326 1041
353473aa
DW
1042 Y.each(clonecells, function(cell) {
1043 var newcell = cell.cloneNode();
adca7326
DW
1044 // Clear the content of the cell.
1045 newcell.setHTML('&nbsp;');
1046
353473aa
DW
1047 if (before) {
1048 cell.get('parentNode').insert(newcell, cell);
1049 } else {
1050 cell.get('parentNode').insert(newcell, cell);
1051 cell.swap(newcell);
1052 }
adca7326
DW
1053 }, this);
1054
1055 // Clean the HTML.
62467795 1056 this.markUpdated();
adca7326 1057 }
353473aa 1058
62467795 1059});
adca7326
DW
1060
1061
62467795 1062}, '@VERSION@', {"requires": ["moodle-editor_atto-plugin", "moodle-editor_atto-menu", "event", "event-valuechange"]});