1 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
19 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 * @module moodle-atto_table-button
27 * Atto text editor table plugin.
29 * @namespace M.atto_table
31 * @extends M.editor_atto.EditorPlugin
34 var COMPONENT = 'atto_table',
36 BORDERSTYLE: 'inherit',
43 '<form class="{{CSS.FORM}}">' +
44 '<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' +
45 '<input class="{{CSS.CAPTION}} fullwidth" id="{{elementid}}_atto_table_caption" required />' +
48 '<label for="{{elementid}}_atto_table_captionposition" class="sameline">' +
49 '{{get_string "captionposition" component}}</label>' +
50 '<select class="{{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +
51 '<option value=""></option>' +
52 '<option value="top">{{get_string "top" "editor"}}</option>' +
53 '<option value="bottom">{{get_string "bottom" "editor"}}</option>' +
56 '<label for="{{elementid}}_atto_table_headers" class="sameline">{{get_string "headers" component}}</label>' +
57 '<select class="{{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
58 '<option value="columns">{{get_string "columns" component}}' + '</option>' +
59 '<option value="rows">{{get_string "rows" component}}' + '</option>' +
60 '<option value="both">{{get_string "both" component}}' + '</option>' +
64 '<label for="{{elementid}}_atto_table_rows" class="sameline">{{get_string "numberofrows" component}}</label>' +
65 '<input class="{{CSS.ROWS}}" type="number" value="3" ' +
66 'id="{{elementid}}_atto_table_rows" size="8" min="1" max="50"/>' +
68 '<label for="{{elementid}}_atto_table_columns" ' +
69 'class="sameline">{{get_string "numberofcolumns" component}}</label>' +
70 '<input class="{{CSS.COLUMNS}}" type="number" value="3" id="{{elementid}}_atto_table_columns"' +
71 'size="8" min="1" max="20"/>' +
74 '{{#if allowStyling}}' +
76 '<legend class="mdl-align">{{get_string "appearance" component}}</legend>' +
77 '{{#if allowBorders}}' +
78 '<label for="{{elementid}}_atto_table_borders" class="sameline">{{get_string "borders" component}}</label>' +
79 '<select name="borders" class="{{CSS.BORDERS}}" id="{{elementid}}_atto_table_borders">' +
80 '<option value="default">{{get_string "themedefault" component}}' + '</option>' +
81 '<option value="outer">{{get_string "outer" component}}' + '</option>' +
82 '<option value="all">{{get_string "all" component}}' + '</option>' +
85 '{{#if allowBorderStyles}}' +
86 '<label for="{{elementid}}_atto_table_borderstyle" class="sameline">' +
87 '{{get_string "borderstyles" component}}</label>' +
88 '<select name="borderstyles" class="{{CSS.BORDERSTYLE}}" id="{{elementid}}_atto_table_borderstyle">' +
89 '<option value="inherit">{{get_string "inherit" component}}</option>' +
90 '{{#each borderStyles}}' +
91 '<option value="' + '{{this}}' + '">' + '{{get_string this ../component}}' + '</option>' +
96 '{{#if allowBorderSize}}' +
97 '<label for="{{elementid}}_atto_table_bordersize" class="sameline">' +
98 '{{get_string "bordersize" component}}</label>' +
99 '<input name="bordersize" id="{{elementid}}_atto_table_bordersize" class="{{CSS.BORDERSIZE}}"' +
100 'type="number" value="1" size="8" min="1" max="50"/>' +
101 '<label style="display: inline-block;">{{CSS.BORDERSIZEUNIT}}</label>' +
104 '{{#if allowBorderColour}}' +
105 '<label for="{{elementid}}_atto_table_bordercolour" class="sameline">' +
106 '{{get_string "bordercolour" component}}</label>' +
107 '<div id="{{elementid}}_atto_table_bordercolour"' +
108 'class="{{CSS.BORDERCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
109 '<label class="hideborder" for="{{../elementid}}_atto_table_bordercolour_-1"' +
110 'style="background-color:transparent;color:transparent">' +
112 '<input id="{{../elementid}}_atto_table_bordercolour_-1"' +
113 'type="radio" name="borderColour" value="none" checked="checked"'+
114 'title="{{get_string "themedefault" component}}"></input>' +
116 '{{get_string "themedefault" component}}' +
118 '{{#each availableColours}}' +
119 '<label for="{{../elementid}}_atto_table_bordercolour_{{@index}}"' +
120 'style="background-color:{{this}};color:{{this}}">' +
122 '<input id="{{../elementid}}_atto_table_bordercolour_{{@index}}"' +
123 'type="radio" name="borderColour" value="' + '{{this}}' + '" title="{{this}}">' +
132 '{{#if allowBackgroundColour}}' +
133 '<label for="{{elementid}}_atto_table_backgroundcolour" class="sameline">' +
134 '{{get_string "backgroundcolour" component}}</label>' +
135 '<div id="{{elementid}}_atto_table_backgroundcolour"' +
136 'class="{{CSS.BACKGROUNDCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
137 '<label class="hideborder" for="{{../elementid}}_atto_table_backgroundcolour_-1"' +
138 'style="background-color:transparent;color:transparent">' +
140 '<input id="{{../elementid}}_atto_table_backgroundcolour_-1"' +
141 'type="radio" name="backgroundColour" value="none" checked="checked"'+
142 'title="{{get_string "themedefault" component}}"></input>' +
144 '{{get_string "themedefault" component}}' +
147 '{{#each availableColours}}' +
148 '<label for="{{../elementid}}_atto_table_backgroundcolour_{{@index}}"' +
149 'style="background-color:{{this}};color:{{this}}">'+
151 '<input id="{{../elementid}}_atto_table_backgroundcolour_{{@index}}"' +
152 'type="radio" name="backgroundColour" value="' + '{{this}}' + '" title="{{this}}">' +
160 '{{#if allowWidth}}' +
161 '<label for="{{elementid}}_atto_table_width" class="sameline">' +
162 '{{get_string "width" component}}</label>' +
163 '<input name="width" id="{{elementid}}_atto_table_width" class="{{CSS.WIDTH}}" size="8" type="number" min="0" max="100"/>' +
164 '<label style="display: inline-block;">{{CSS.WIDTHUNIT}}</label>' +
169 '<div class="mdl-align">' +
172 '<button class="submit" type="submit">{{get_string "updatetable" component}}</button>' +
175 '<button class="submit" type="submit">{{get_string "createtable" component}}</button>' +
181 CAPTIONPOSITION: 'captionposition',
188 BORDERSIZE: 'bordersize',
189 BORDERSIZEUNIT: 'px',
190 BORDERCOLOUR: 'bordercolour',
191 BORDERSTYLE: 'borderstyle',
192 BACKGROUNDCOLOUR: 'backgroundcolour',
193 WIDTH: 'customwidth',
195 AVAILABLECOLORS: 'availablecolors',
196 COLOURROW: 'colourrow'
199 CAPTION: '.' + CSS.CAPTION,
200 CAPTIONPOSITION: '.' + CSS.CAPTIONPOSITION,
201 HEADERS: '.' + CSS.HEADERS,
202 ROWS: '.' + CSS.ROWS,
203 COLUMNS: '.' + CSS.COLUMNS,
204 SUBMIT: '.' + CSS.SUBMIT,
205 BORDERS: '.' + CSS.BORDERS,
206 BORDERSIZE: '.' + CSS.BORDERSIZE,
207 BORDERCOLOURS: '.' + CSS.BORDERCOLOUR + ' input[name="borderColour"]',
208 SELECTEDBORDERCOLOUR: '.' + CSS.BORDERCOLOUR + ' input[name="borderColour"]:checked',
209 BORDERSTYLE: '.' + CSS.BORDERSTYLE,
210 BACKGROUNDCOLOURS: '.' + CSS.BACKGROUNDCOLOUR + ' input[name="backgroundColour"]',
211 SELECTEDBACKGROUNDCOLOUR: '.' + CSS.BACKGROUNDCOLOUR + ' input[name="backgroundColour"]:checked',
213 WIDTH: '.' + CSS.WIDTH,
214 AVAILABLECOLORS: '.' + CSS.AVAILABLECOLORS
217 Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
220 * A reference to the current selection at the time that the dialogue
223 * @property _currentSelection
227 _currentSelection: null,
230 * The contextual menu that we can open.
232 * @property _contextMenu
233 * @type M.editor_atto.Menu
239 * The last modified target.
241 * @property _lastTarget
248 * The list of menu items.
250 * @property _menuOptions
256 initializer: function() {
259 callback: this._displayTableEditor,
262 // Disable mozilla table controls.
264 document.execCommand("enableInlineTableEditing", false, false);
265 document.execCommand("enableObjectResizing", false, false);
270 * Display the table tool.
272 * @method _displayDialogue
275 _displayDialogue: function() {
276 // Store the current cursor position.
277 this._currentSelection = this.get('host').getSelection();
279 if (this._currentSelection !== false && (!this._currentSelection.collapsed)) {
280 var dialogue = this.getDialogue({
281 headerContent: M.util.get_string('createtable', COMPONENT),
282 focusAfterHide: true,
283 focusOnShowSelector: SELECTORS.CAPTION,
284 width: DIALOGUE.WIDTH
287 // Set the dialogue content, and then show the dialogue.
288 dialogue.set('bodyContent', this._getDialogueContent(false))
291 this._updateAvailableSettings();
296 * Display the appropriate table editor.
298 * If the current selection includes a table, then we show the
299 * contextual menu, otherwise show the table creation dialogue.
301 * @method _displayTableEditor
302 * @param {EventFacade} e
305 _displayTableEditor: function(e) {
306 var cell = this._getSuitableTableCell();
308 // Add the cell to the EventFacade to save duplication in when showing the menu.
310 return this._showTableMenu(e);
312 return this._displayDialogue(e);
316 * Returns whether or not the parameter node exists within the editor.
318 * @method _stopAtContentEditableFilter
321 * @return {boolean} whether or not the parameter node exists within the editor.
323 _stopAtContentEditableFilter: function(node) {
324 this.editor.contains(node);
328 * Return the dialogue content for the tool, attaching any required
331 * @method _getDialogueContent
333 * @return {Node} The content to place in the dialogue.
335 _getDialogueContent: function(edit) {
336 var template = Y.Handlebars.compile(TEMPLATE);
337 var allowBorders = this.get('allowBorders');
339 this._content = Y.Node.create(template({
341 elementid: this.get('host').get('elementid'),
342 component: COMPONENT,
345 allowStyling: this.get('allowStyling'),
346 allowBorders: allowBorders,
347 allowBorderStyles: this.get('allowBorderStyles'),
348 borderStyles: this.get('borderStyles'),
349 allowBorderSize: this.get('allowBorderSize'),
350 allowBorderColour: this.get('allowBorderColour'),
351 allowBackgroundColour: this.get('allowBackgroundColour'),
352 availableColours: this.get('availableColors'),
353 allowWidth: this.get('allowWidth')
356 // Handle table setting.
358 this._content.one('.submit').on('click', this._updateTable, this);
360 this._content.one('.submit').on('click', this._setTable, this);
364 this._content.one('[name="borders"]').on('change', this._updateAvailableSettings, this);
367 return this._content;
371 * Disables options within the dialogue if they shouldn't be available.
373 * If borders are set to "Theme default" then the border size, style and
374 * colour options are disabled.
376 * @method _updateAvailableSettings
379 _updateAvailableSettings: function() {
380 var tableForm = this._content,
381 enableBorders = tableForm.one('[name="borders"]'),
382 borderStyle = tableForm.one('[name="borderstyles"]'),
383 borderSize = tableForm.one('[name="bordersize"]'),
384 borderColour = tableForm.all('[name="borderColour"]'),
385 disabledValue = 'removeAttribute';
387 if (enableBorders.get('value') === 'default') {
388 disabledValue = 'setAttribute';
392 borderStyle[disabledValue]('disabled');
396 borderSize[disabledValue]('disabled');
400 borderColour[disabledValue]('disabled');
406 * Given the current selection, return a table cell suitable for table editing
407 * purposes, i.e. the first table cell selected, or the first cell in the table
408 * that the selection exists in, or null if not within a table.
410 * @method _getSuitableTableCell
412 * @return {Node} suitable target cell, or null if not within a table
414 _getSuitableTableCell: function() {
415 var targetcell = null,
416 host = this.get('host');
418 host.getSelectedNodes().some(function (node) {
419 if (node.ancestor('td, th, caption', true, this._stopAtContentEditableFilter)) {
422 var caption = node.ancestor('caption', true, this._stopAtContentEditableFilter);
424 var table = caption.get('parentNode');
426 targetcell = table.one('td, th');
430 // Once we've found a cell to target, we shouldn't need to keep looking.
436 var selection = host.getSelectionFromNode(targetcell);
437 host.setSelection(selection);
444 * Change a node from one type to another, copying all attributes and children.
446 * @method _changeNodeType
447 * @param {Y.Node} node
448 * @param {String} new node type
452 _changeNodeType: function(node, newType) {
453 var newNode = Y.Node.create('<' + newType + '></' + newType + '>');
454 newNode.setAttrs(node.getAttrs());
455 node.get('childNodes').each(function(child) {
456 newNode.append(child.remove());
458 node.replace(newNode);
463 * Handle updating an existing table.
465 * @method _updateTable
466 * @param {EventFacade} e
469 _updateTable: function(e) {
483 // Hide the dialogue.
488 // Add/update the caption.
489 caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
490 captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
491 headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
492 borders = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERS);
493 bordersize = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSIZE);
494 bordercolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBORDERCOLOUR);
495 borderstyle = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSTYLE);
496 backgroundcolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBACKGROUNDCOLOUR);
497 width = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.WIDTH);
499 table = this._lastTarget.ancestor('table');
500 this._setAppearance(table, {
503 borderColour: bordercolour,
504 borderSize: bordersize,
505 borderStyle: borderstyle,
506 backgroundColour: backgroundcolour
509 captionnode = table.one('caption');
511 captionnode = Y.Node.create('<caption></caption>');
512 table.insert(captionnode, 0);
514 captionnode.setHTML(caption.get('value'));
515 captionnode.setStyle('caption-side', captionposition.get('value'));
516 if (!captionnode.getAttribute('style')) {
517 captionnode.removeAttribute('style');
520 // Add the row headers.
521 if (headers.get('value') === 'rows' || headers.get('value') === 'both') {
522 table.all('tr').each(function (row) {
523 var cells = row.all('th, td'),
524 firstCell = cells.shift(),
527 if (firstCell.get('tagName') === 'TD') {
528 // Cell is a td but should be a th - change it.
529 newCell = this._changeNodeType(firstCell, 'th');
530 newCell.setAttribute('scope', 'row');
532 firstCell.setAttribute('scope', 'row');
535 // Now make sure all other cells in the row are td.
536 cells.each(function (cell) {
537 if (cell.get('tagName') === 'TH') {
538 newCell = this._changeNodeType(cell, 'td');
539 newCell.removeAttribute('scope');
545 // Add the col headers. These may overrule the row headers in the first cell.
546 if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
547 var rows = table.all('tr'),
548 firstRow = rows.shift(),
551 firstRow.all('td, th').each(function (cell) {
552 if (cell.get('tagName') === 'TD') {
553 // Cell is a td but should be a th - change it.
554 newCell = this._changeNodeType(cell, 'th');
555 newCell.setAttribute('scope', 'col');
557 cell.setAttribute('scope', 'col');
560 // Change all the cells in the rest of the table to tds (unless they are row headers).
561 rows.each(function(row) {
562 var cells = row.all('th, td');
564 if (headers.get('value') === 'both') {
565 // Ignore the first cell because it's a row header.
568 cells.each(function(cell) {
569 if (cell.get('tagName') === 'TH') {
570 newCell = this._changeNodeType(cell, 'td');
571 newCell.removeAttribute('scope');
582 * Handle creation of a new table.
585 * @param {EventFacade} e
588 _setTable: function(e) {
605 // Hide the dialogue.
610 caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
611 captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
612 borders = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERS);
613 bordersize = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSIZE);
614 bordercolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBORDERCOLOUR);
615 borderstyle = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSTYLE);
616 backgroundcolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBACKGROUNDCOLOUR);
617 rows = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.ROWS);
618 cols = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.COLUMNS);
619 headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
620 width = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.WIDTH);
622 // Set the selection.
623 this.get('host').setSelection(this._currentSelection);
625 // Note there are some spaces inserted in the cells and before and after, so that users have somewhere to click.
627 var tableId = Y.guid();
628 tablehtml = '<br/>' + nl + '<table id="' + tableId + '">' + nl;
630 var captionstyle = '';
631 if (captionposition.get('value')) {
632 captionstyle = ' style="caption-side: ' + captionposition.get('value') + '"';
634 tablehtml += '<caption' + captionstyle + '>' + Y.Escape.html(caption.get('value')) + '</caption>' + nl;
636 if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
638 tablehtml += '<thead>' + nl + '<tr>' + nl;
639 for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
640 tablehtml += '<th scope="col"></th>' + nl;
642 tablehtml += '</tr>' + nl + '</thead>' + nl;
644 tablehtml += '<tbody>' + nl;
645 for (; i < parseInt(rows.get('value'), 10); i++) {
646 tablehtml += '<tr>' + nl;
647 for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
648 if (j === 0 && (headers.get('value') === 'rows' || headers.get('value') === 'both')) {
649 tablehtml += '<th scope="row"></th>' + nl;
651 tablehtml += '<td ></td>' + nl;
654 tablehtml += '</tr>' + nl;
656 tablehtml += '</tbody>' + nl;
657 tablehtml += '</table>' + nl + '<br/>';
659 this.get('host').insertContentAtFocusPoint(tablehtml);
661 var tableNode = Y.one('#' + tableId);
662 this._setAppearance(tableNode, {
665 borderColour: bordercolour,
666 borderSize: bordersize,
667 borderStyle: borderstyle,
668 backgroundColour: backgroundcolour
670 tableNode.removeAttribute('id');
672 // Mark the content as updated.
677 * Search for all the cells in the current, next and previous columns.
679 * @method _findColumnCells
681 * @return {Object} containing current, prev and next {Y.NodeList}s
683 _findColumnCells: function() {
684 var columnindex = this._getColumnIndex(this._lastTarget),
685 rows = this._lastTarget.ancestor('table').all('tr'),
686 currentcells = new Y.NodeList(),
687 prevcells = new Y.NodeList(),
688 nextcells = new Y.NodeList();
690 rows.each(function(row) {
691 var cells = row.all('td, th'),
692 cell = cells.item(columnindex),
693 cellprev = cells.item(columnindex-1),
694 cellnext = cells.item(columnindex+1);
695 currentcells.push(cell);
697 prevcells.push(cellprev);
700 nextcells.push(cellnext);
705 current: currentcells,
712 * Hide the entries in the context menu that don't make sense with the
715 * @method _hideInvalidEntries
716 * @param {Y.Node} node - The node containing the menu.
719 _hideInvalidEntries: function(node) {
721 var table = this._lastTarget.ancestor('table'),
722 row = this._lastTarget.ancestor('tr'),
723 rows = table.all('tr'),
724 rowindex = rows.indexOf(row),
725 prevrow = rows.item(rowindex - 1),
726 prevrowhascells = prevrow ? prevrow.one('td') : null;
728 if (!row || !prevrowhascells) {
729 node.one('[data-change="moverowup"]').hide();
731 node.one('[data-change="moverowup"]').show();
734 var nextrow = rows.item(rowindex + 1),
735 rowhascell = row ? row.one('td') : false;
737 if (!row || !nextrow || !rowhascell) {
738 node.one('[data-change="moverowdown"]').hide();
740 node.one('[data-change="moverowdown"]').show();
744 var cells = this._findColumnCells();
745 if (cells.prev.filter('td').size() > 0) {
746 node.one('[data-change="movecolumnleft"]').show();
748 node.one('[data-change="movecolumnleft"]').hide();
751 var colhascell = cells.current.filter('td').size() > 0;
752 if ((cells.next.size() > 0) && colhascell) {
753 node.one('[data-change="movecolumnright"]').show();
755 node.one('[data-change="movecolumnright"]').hide();
759 if (cells.current.filter('td').size() > 0) {
760 node.one('[data-change="deletecolumn"]').show();
762 node.one('[data-change="deletecolumn"]').hide();
765 if (!row || !row.one('td')) {
766 node.one('[data-change="deleterow"]').hide();
768 node.one('[data-change="deleterow"]').show();
773 * Display the table menu.
775 * @method _showTableMenu
776 * @param {EventFacade} e
779 _showTableMenu: function(e) {
784 if (!this._contextMenu) {
785 this._menuOptions = [
787 text: M.util.get_string("addcolumnafter", COMPONENT),
789 change: "addcolumnafter"
792 text: M.util.get_string("addrowafter", COMPONENT),
794 change: "addrowafter"
797 text: M.util.get_string("moverowup", COMPONENT),
802 text: M.util.get_string("moverowdown", COMPONENT),
804 change: "moverowdown"
807 text: M.util.get_string("movecolumnleft", COMPONENT),
809 change: "movecolumnleft"
812 text: M.util.get_string("movecolumnright", COMPONENT),
814 change: "movecolumnright"
817 text: M.util.get_string("deleterow", COMPONENT),
822 text: M.util.get_string("deletecolumn", COMPONENT),
824 change: "deletecolumn"
827 text: M.util.get_string("edittable", COMPONENT),
834 this._contextMenu = new Y.M.editor_atto.Menu({
835 items: this._menuOptions
838 // Add event handlers for table control menus.
839 boundingBox = this._contextMenu.get('boundingBox');
840 boundingBox.delegate('click', this._handleTableChange, 'a', this);
843 boundingBox = this._contextMenu.get('boundingBox');
845 // We store the cell of the last click (the control node is transient).
846 this._lastTarget = e.tableCell.ancestor('.editor_atto_content td, .editor_atto_content th', true);
848 this._hideInvalidEntries(boundingBox);
850 // Clear the focusAfterHide for any other menus which may be open.
851 Y.Array.each(this.get('host').openMenus, function(menu) {
852 menu.set('focusAfterHide', null);
855 // Ensure that we focus on the button in the toolbar when we tab back to the menu.
856 var creatorButton = this.buttons[this.name];
857 this.get('host')._setTabFocus(creatorButton);
859 // Show the context menu, and align to the current position.
860 this._contextMenu.show();
861 this._contextMenu.align(this.buttons.table, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
862 this._contextMenu.set('focusAfterHide', creatorButton);
864 // If there are any anchors in the bounding box, focus on the first.
865 if (boundingBox.one('a')) {
866 boundingBox.one('a').focus();
869 // Add this menu to the list of open menus.
870 this.get('host').openMenus = [this._contextMenu];
874 * Handle a selection from the table control menu.
876 * @method _handleTableChange
877 * @param {EventFacade} e
880 _handleTableChange: function(e) {
883 this._contextMenu.set('focusAfterHide', this.get('host').editor);
884 // Hide the context menu.
885 this._contextMenu.hide(e);
888 switch (e.target.getData('change')) {
889 case 'addcolumnafter':
890 this._addColumnAfter();
899 this._deleteColumn();
910 case 'movecolumnleft':
911 this._moveColumnLeft();
913 case 'movecolumnright':
914 this._moveColumnRight();
920 * Determine the index of a row in a table column.
922 * @method _getRowIndex
926 _getRowIndex: function(cell) {
927 var tablenode = cell.ancestor('table'),
928 rownode = cell.ancestor('tr');
930 if (!tablenode || !rownode) {
934 var rows = tablenode.all('tr');
936 return rows.indexOf(rownode);
940 * Determine the index of a column in a table row.
942 * @method _getColumnIndex
943 * @param {Node} cellnode
946 _getColumnIndex: function(cellnode) {
947 var rownode = cellnode.ancestor('tr');
953 var cells = rownode.all('td, th');
955 return cells.indexOf(cellnode);
959 * Delete the current row.
964 _deleteRow: function() {
965 var row = this._lastTarget.ancestor('tr');
967 if (row && row.one('td')) {
968 // Only delete rows with at least one non-header cell.
982 _moveRowUp: function() {
983 var row = this._lastTarget.ancestor('tr'),
984 prevrow = row.previous('tr');
985 if (!row || !prevrow) {
997 * @method _moveColumnLeft
1000 _moveColumnLeft: function() {
1001 var cells = this._findColumnCells();
1003 if (cells.current.size() > 0 && cells.prev.size() > 0 && cells.current.size() === cells.prev.size()) {
1005 for (i = 0; i < cells.current.size(); i++) {
1006 var cell = cells.current.item(i),
1007 prevcell = cells.prev.item(i);
1009 cell.swap(prevcell);
1017 * Add a caption to the table if it doesn't have one.
1019 * @method _addCaption
1022 _addCaption: function() {
1023 var table = this._lastTarget.ancestor('table'),
1024 caption = table.one('caption');
1027 table.insert(Y.Node.create('<caption> </caption>'), 1);
1032 * Remove a caption from the table if has one.
1034 * @method _removeCaption
1037 _removeCaption: function() {
1038 var table = this._lastTarget.ancestor('table'),
1039 caption = table.one('caption');
1042 caption.remove(true);
1047 * Move column right.
1049 * @method _moveColumnRight
1052 _moveColumnRight: function() {
1053 var cells = this._findColumnCells();
1055 // Check we have some tds in this column, and one exists to the right.
1056 if ( (cells.next.size() > 0) &&
1057 (cells.current.size() === cells.next.size()) &&
1058 (cells.current.filter('td').size() > 0)) {
1060 for (i = 0; i < cells.current.size(); i++) {
1061 var cell = cells.current.item(i),
1062 nextcell = cells.next.item(i);
1064 cell.swap(nextcell);
1074 * @method _moveRowDown
1077 _moveRowDown: function() {
1078 var row = this._lastTarget.ancestor('tr'),
1079 nextrow = row.next('tr');
1080 if (!row || !nextrow || !row.one('td')) {
1090 * Obtain values for the table borders
1092 * @method _getBorderConfiguration
1093 * @param {Node} node
1095 * @return {Array} or {Boolean} Returns the settings, if presents, or else returns false
1097 _getBorderConfiguration: function(node) {
1098 // We need to make a clone of the node in order to avoid grabbing any
1099 // of the computed styles from the DOM. We only want inline styles set by us.
1100 var shadowNode = node.cloneNode(true);
1101 var borderStyle = shadowNode.getStyle('borderStyle'),
1102 borderColor = shadowNode.getStyle('borderColor'),
1103 borderWidth = shadowNode.getStyle('borderWidth');
1105 if (borderStyle || borderColor || borderWidth) {
1106 var hexColour = Y.Color.toHex(borderColor);
1107 var width = parseInt(borderWidth, 10);
1109 borderStyle: borderStyle,
1110 borderColor: hexColour === "#" ? null : hexColour,
1111 borderWidth: isNaN(width) ? null : width
1119 * Set the appropriate styles on the given table node according to
1120 * the provided configuration.
1122 * @method _setAppearance
1123 * @param {Node} The table node to be modified.
1124 * @param {Object} Configuration object (associative array) containing the form nodes for
1128 _setAppearance: function(tableNode, configuration) {
1132 backgroundcolourvalue;
1134 if (configuration.borderColour) {
1135 borderhex = configuration.borderColour.get('value');
1138 if (configuration.borderSize) {
1139 borderSizeValue = configuration.borderSize.get('value');
1142 if (configuration.borderStyle) {
1143 borderStyleValue = configuration.borderStyle.get('value');
1146 if (configuration.backgroundColour) {
1147 backgroundcolourvalue = configuration.backgroundColour.get('value');
1150 // Clear the inline border styling
1151 tableNode.removeAttribute('style');
1152 tableNode.all('td, th').each(function(cell) {
1153 cell.removeAttribute('style');
1156 if (configuration.borders) {
1157 if (configuration.borders.get('value') === 'outer') {
1158 tableNode.setStyle('borderWidth', borderSizeValue + CSS.BORDERSIZEUNIT);
1159 tableNode.setStyle('borderStyle', borderStyleValue);
1161 if (borderhex !== 'none') {
1162 tableNode.setStyle('borderColor', borderhex);
1164 } else if (configuration.borders.get('value') === 'all') {
1165 tableNode.all('td, th').each(function(cell) {
1166 cell.setStyle('borderWidth', borderSizeValue + CSS.BORDERSIZEUNIT);
1167 cell.setStyle('borderStyle', borderStyleValue);
1169 if (borderhex !== 'none') {
1170 cell.setStyle('borderColor', borderhex);
1176 if (backgroundcolourvalue !== 'none') {
1177 tableNode.setStyle('backgroundColor', backgroundcolourvalue);
1180 if (configuration.width && configuration.width.get('value')) {
1181 tableNode.setStyle('width', configuration.width.get('value') + CSS.WIDTHUNIT);
1186 * Edit table (show the dialogue).
1188 * @method _editTable
1191 _editTable: function() {
1192 var dialogue = this.getDialogue({
1193 headerContent: M.util.get_string('edittable', COMPONENT),
1194 focusAfterHide: false,
1195 focusOnShowSelector: SELECTORS.CAPTION,
1196 width: DIALOGUE.WIDTH
1199 // Set the dialogue content, and then show the dialogue.
1200 var node = this._getDialogueContent(true),
1201 captioninput = node.one(SELECTORS.CAPTION),
1202 captionpositioninput = node.one(SELECTORS.CAPTIONPOSITION),
1203 headersinput = node.one(SELECTORS.HEADERS),
1204 borderinput = node.one(SELECTORS.BORDERS),
1205 borderstyle = node.one(SELECTORS.BORDERSTYLE),
1206 bordercolours = node.all(SELECTORS.BORDERCOLOURS),
1207 bordersize = node.one(SELECTORS.BORDERSIZE),
1208 backgroundcolours = node.all(SELECTORS.BACKGROUNDCOLOURS),
1209 width = node.one(SELECTORS.WIDTH),
1210 table = this._lastTarget.ancestor('table'),
1211 captionnode = table.one('caption'),
1216 captioninput.set('value', captionnode.getHTML());
1218 captioninput.set('value', '');
1221 if (width && table.getStyle('width').indexOf('px') === -1) {
1222 width.set('value', parseInt(table.getStyle('width'), 10));
1225 if (captionpositioninput && captionnode && captionnode.getAttribute('style')) {
1226 captionpositioninput.set('value', captionnode.getStyle('caption-side'));
1229 captionpositioninput.set('value', '');
1232 if (table.getStyle('backgroundColor') && this.get('allowBackgroundColour')) {
1233 hexColour = Y.Color.toHex(table.getStyle('backgroundColor'));
1234 matchedInput = backgroundcolours.filter('[value="' + hexColour + '"]');
1237 matchedInput.set("checked", true);
1241 if (this.get('allowBorders')) {
1242 var borderValue = 'default',
1243 borderConfiguration = this._getBorderConfiguration(table);
1245 if (borderConfiguration) {
1246 borderValue = 'outer';
1248 borderConfiguration = this._getBorderConfiguration(table.one('td'));
1249 if (borderConfiguration) {
1250 borderValue = 'all';
1254 if (borderConfiguration) {
1255 var borderStyle = borderConfiguration.borderStyle || DEFAULT.BORDERSTYLE;
1256 var borderSize = borderConfiguration.borderWidth || DEFAULT.BORDERWIDTH;
1257 borderstyle.set('value', borderStyle);
1258 bordersize.set('value', borderSize);
1259 borderinput.set('value', borderValue);
1261 hexColour = borderConfiguration.borderColor;
1262 matchedInput = bordercolours.filter('[value="' + hexColour + '"]');
1265 matchedInput.set("checked", true);
1270 var headersvalue = 'columns';
1271 if (table.one('th[scope="row"]')) {
1272 headersvalue = 'rows';
1273 if (table.one('th[scope="col"]')) {
1274 headersvalue = 'both';
1277 headersinput.set('value', headersvalue);
1278 dialogue.set('bodyContent', node).show();
1279 this._updateAvailableSettings();
1284 * Delete the current column.
1286 * @method _deleteColumn
1289 _deleteColumn: function() {
1290 var columnindex = this._getColumnIndex(this._lastTarget),
1291 table = this._lastTarget.ancestor('table'),
1292 rows = table.all('tr'),
1293 columncells = new Y.NodeList(),
1296 rows.each(function(row) {
1297 var cells = row.all('td, th');
1298 var cell = cells.item(columnindex);
1299 if (cell.get('tagName') === 'TD') {
1302 columncells.push(cell);
1305 // Do not delete all the headers.
1307 columncells.remove(true);
1315 * Add a row after the current row.
1317 * @method _addRowAfter
1320 _addRowAfter: function() {
1321 var target = this._lastTarget.ancestor('tr'),
1322 tablebody = this._lastTarget.ancestor('table').one('tbody');
1324 // Not all tables have tbody.
1325 tablebody = this._lastTarget.ancestor('table');
1328 var firstrow = tablebody.one('tr');
1330 firstrow = this._lastTarget.ancestor('table').one('tr');
1333 // Table has no rows. Boo.
1336 var newrow = firstrow.cloneNode(true);
1337 newrow.all('th, td').each(function (tablecell) {
1338 if (tablecell.get('tagName') === 'TH') {
1339 if (tablecell.getAttribute('scope') !== 'row') {
1340 var newcell = Y.Node.create('<td></td>');
1341 tablecell.replace(newcell);
1342 tablecell = newcell;
1345 tablecell.setHTML(' ');
1348 if (target.ancestor('thead')) {
1350 tablebody.insert(newrow, target);
1352 target.insert(newrow, 'after');
1360 * Add a column after the current column.
1362 * @method _addColumnAfter
1365 _addColumnAfter: function() {
1366 var cells = this._findColumnCells(),
1368 clonecells = cells.next;
1369 if (cells.next.size() <= 0) {
1371 clonecells = cells.current;
1374 Y.each(clonecells, function(cell) {
1375 var newcell = cell.cloneNode();
1376 // Clear the content of the cell.
1377 newcell.setHTML(' ');
1380 cell.get('parentNode').insert(newcell, cell);
1382 cell.get('parentNode').insert(newcell, cell);
1394 * Whether or not to allow borders
1396 * @attribute allowBorder
1404 * Whether or not to allow style of borders
1406 * @attribute allowBorderStyle
1409 allowBorderStyles: {
1414 * What border styles to allow
1416 * @attribute borderStyles
1425 setter: function(value) {
1427 return value.replace(/ /g,'').split(',');
1429 // Not a valid value - revert to default value.
1430 return Y.Attribute.INVALID_VALUE;
1436 * Whether or not to allow border size
1438 * @attribute allowBorderSize
1446 * Whether or not to allow colourizing borders
1448 * @attribute allowBorderColours
1451 allowBorderColour: {
1456 * Whether or not to allow colourizing the background
1458 * @attribute allowBackgroundColour
1461 allowBackgroundColour: {
1466 * Whether or not to allow setting the table width
1468 * @attribute allowWidth
1476 * Whether we allow styling
1477 * @attribute allowStyling
1482 getter: function() {
1483 return this.get('allowBorders') || this.get('allowBackgroundColour') || this.get('allowWidth');
1489 * @attribute availableColors