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