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',
43 '<form class="{{CSS.FORM}}">' +
44 '<div class="mb-1 form-group row-fluid">' +
45 '<div class="col-sm-4">' +
46 '<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' +
47 '</div><div class="col-sm-8">' +
48 '<input type="text" class="form-control {{CSS.CAPTION}}" id="{{elementid}}_atto_table_caption" required />' +
51 '<div class="mb-1 form-group row-fluid">' +
52 '<div class="col-sm-4">' +
53 '<label for="{{elementid}}_atto_table_captionposition">' +
54 '{{get_string "captionposition" component}}</label>' +
55 '</div><div class="col-sm-8">' +
56 '<select class="custom-select {{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +
57 '<option value=""></option>' +
58 '<option value="top">{{get_string "top" "editor"}}</option>' +
59 '<option value="bottom">{{get_string "bottom" "editor"}}</option>' +
63 '<div class="mb-1 form-group row-fluid">' +
64 '<div class="col-sm-4">' +
65 '<label for="{{elementid}}_atto_table_headers">{{get_string "headers" component}}</label>' +
66 '</div><div class="col-sm-8">' +
67 '<select class="custom-select {{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
68 '<option value="columns">{{get_string "columns" component}}' + '</option>' +
69 '<option value="rows">{{get_string "rows" component}}' + '</option>' +
70 '<option value="both">{{get_string "both" component}}' + '</option>' +
75 '<div class="mb-1 form-group row-fluid">' +
76 '<div class="col-sm-4">' +
77 '<label for="{{elementid}}_atto_table_rows">{{get_string "numberofrows" component}}</label>' +
78 '</div><div class="col-sm-8">' +
79 '<input class="form-control w-auto {{CSS.ROWS}}" type="number" value="3" ' +
80 'id="{{elementid}}_atto_table_rows" size="8" min="1" max="50"/>' +
83 '<div class="mb-1 form-group row-fluid">' +
84 '<div class="col-sm-4">' +
85 '<label for="{{elementid}}_atto_table_columns" ' +
86 '>{{get_string "numberofcolumns" component}}</label>' +
87 '</div><div class="col-sm-8">' +
88 '<input class="form-control w-auto {{CSS.COLUMNS}}" type="number" value="3" ' +
89 'id="{{elementid}}_atto_table_columns"' +
90 'size="8" min="1" max="20"/>' +
94 '{{#if allowStyling}}' +
96 '<legend class="mdl-align">{{get_string "appearance" component}}</legend>' +
97 '{{#if allowBorders}}' +
98 '<div class="mb-1 form-group row-fluid">' +
99 '<div class="col-sm-4">' +
100 '<label for="{{elementid}}_atto_table_borders">{{get_string "borders" component}}</label>' +
101 '</div><div class="col-sm-8">' +
102 '<select name="borders" class="custom-select {{CSS.BORDERS}}" id="{{elementid}}_atto_table_borders">' +
103 '<option value="default">{{get_string "themedefault" component}}' + '</option>' +
104 '<option value="outer">{{get_string "outer" component}}' + '</option>' +
105 '<option value="all">{{get_string "all" component}}' + '</option>' +
109 '<div class="mb-1 form-group row-fluid">' +
110 '<div class="col-sm-4">' +
111 '<label for="{{elementid}}_atto_table_borderstyle">' +
112 '{{get_string "borderstyles" component}}</label>' +
113 '</div><div class="col-sm-8">' +
114 '<select name="borderstyles" class="custom-select {{CSS.BORDERSTYLE}}" ' +
115 'id="{{elementid}}_atto_table_borderstyle">' +
116 '{{#each borderStyles}}' +
117 '<option value="' + '{{this}}' + '">' + '{{get_string this ../component}}' + '</option>' +
122 '<div class="mb-1 form-group row-fluid">' +
123 '<div class="col-sm-4">' +
124 '<label for="{{elementid}}_atto_table_bordersize">' +
125 '{{get_string "bordersize" component}}</label>' +
126 '</div><div class="col-sm-8">' +
127 '<div class="form-inline">' +
128 '<input name="bordersize" id="{{elementid}}_atto_table_bordersize" ' +
129 'class="form-control w-auto mr-1 {{CSS.BORDERSIZE}}"' +
130 'type="number" value="1" size="8" min="1" max="50"/>' +
131 '<label>{{CSS.BORDERSIZEUNIT}}</label>' +
135 '<div class="mb-1 form-group row-fluid">' +
136 '<div class="col-sm-4">' +
137 '<label for="{{elementid}}_atto_table_bordercolour">' +
138 '{{get_string "bordercolour" component}}</label>' +
139 '</div><div class="col-sm-8">' +
140 '<div id="{{elementid}}_atto_table_bordercolour"' +
141 'class="form-inline {{CSS.BORDERCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
142 '<div class="tablebordercolor" style="background-color:transparent;color:transparent">' +
143 '<input id="{{../elementid}}_atto_table_bordercolour_-1"' +
144 'type="radio" class="m-0" name="borderColour" value="none" checked="checked"' +
145 'title="{{get_string "themedefault" component}}"></input>' +
146 '<label for="{{../elementid}}_atto_table_bordercolour_-1" class="accesshide">' +
147 '{{get_string "themedefault" component}}</label>' +
149 '{{#each availableColours}}' +
150 '<div class="tablebordercolor" style="background-color:{{this}};color:{{this}}">' +
151 '<input id="{{../elementid}}_atto_table_bordercolour_{{@index}}"' +
152 'type="radio" class="m-0" name="borderColour" value="' + '{{this}}' + '" title="{{this}}">' +
153 '<label for="{{../elementid}}_atto_table_bordercolour_{{@index}}" class="accesshide">' +
161 '{{#if allowBackgroundColour}}' +
162 '<div class="mb-1 form-group row-fluid">' +
163 '<div class="col-sm-4">' +
164 '<label for="{{elementid}}_atto_table_backgroundcolour">' +
165 '{{get_string "backgroundcolour" component}}</label>' +
166 '</div><div class="col-sm-8">' +
167 '<div id="{{elementid}}_atto_table_backgroundcolour"' +
168 'class="form-inline {{CSS.BACKGROUNDCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
169 '<div class="tablebackgroundcolor" style="background-color:transparent;color:transparent">' +
170 '<input id="{{../elementid}}_atto_table_backgroundcolour_-1"' +
171 'type="radio" class="m-0" name="backgroundColour" value="none" checked="checked"' +
172 'title="{{get_string "themedefault" component}}"></input>' +
173 '<label for="{{../elementid}}_atto_table_backgroundcolour_-1" class="accesshide">' +
174 '{{get_string "themedefault" component}}</label>' +
177 '{{#each availableColours}}' +
178 '<div class="tablebackgroundcolor" style="background-color:{{this}};color:{{this}}">' +
179 '<input id="{{../elementid}}_atto_table_backgroundcolour_{{@index}}"' +
180 'type="radio" class="m-0" name="backgroundColour" value="' + '{{this}}' + '" title="{{this}}">' +
181 '<label for="{{../elementid}}_atto_table_backgroundcolour_{{@index}}" class="accesshide">' +
189 '{{#if allowWidth}}' +
190 '<div class="mb-1 form-group row-fluid">' +
191 '<div class="col-sm-4">' +
192 '<label for="{{elementid}}_atto_table_width">' +
193 '{{get_string "width" component}}</label>' +
194 '</div><div class="col-sm-8">' +
195 '<div class="form-inline">' +
196 '<input name="width" id="{{elementid}}_atto_table_width" ' +
197 'class="form-control w-auto mr-1 {{CSS.WIDTH}}" size="8" ' +
198 'type="number" min="0" max="100"/>' +
199 '<label>{{CSS.WIDTHUNIT}}</label>' +
206 '<div class="mdl-align">' +
209 '<button class="btn btn-secondary submit" type="submit">{{get_string "updatetable" component}}</button>' +
212 '<button class="btn btn-secondary submit" type="submit">{{get_string "createtable" component}}</button>' +
218 CAPTIONPOSITION: 'captionposition',
225 BORDERSIZE: 'bordersize',
226 BORDERSIZEUNIT: 'px',
227 BORDERCOLOUR: 'bordercolour',
228 BORDERSTYLE: 'borderstyle',
229 BACKGROUNDCOLOUR: 'backgroundcolour',
230 WIDTH: 'customwidth',
232 AVAILABLECOLORS: 'availablecolors',
233 COLOURROW: 'colourrow'
236 CAPTION: '.' + CSS.CAPTION,
237 CAPTIONPOSITION: '.' + CSS.CAPTIONPOSITION,
238 HEADERS: '.' + CSS.HEADERS,
239 ROWS: '.' + CSS.ROWS,
240 COLUMNS: '.' + CSS.COLUMNS,
241 SUBMIT: '.' + CSS.SUBMIT,
242 BORDERS: '.' + CSS.BORDERS,
243 BORDERSIZE: '.' + CSS.BORDERSIZE,
244 BORDERCOLOURS: '.' + CSS.BORDERCOLOUR + ' input[name="borderColour"]',
245 SELECTEDBORDERCOLOUR: '.' + CSS.BORDERCOLOUR + ' input[name="borderColour"]:checked',
246 BORDERSTYLE: '.' + CSS.BORDERSTYLE,
247 BACKGROUNDCOLOURS: '.' + CSS.BACKGROUNDCOLOUR + ' input[name="backgroundColour"]',
248 SELECTEDBACKGROUNDCOLOUR: '.' + CSS.BACKGROUNDCOLOUR + ' input[name="backgroundColour"]:checked',
250 WIDTH: '.' + CSS.WIDTH,
251 AVAILABLECOLORS: '.' + CSS.AVAILABLECOLORS
254 Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
257 * A reference to the current selection at the time that the dialogue
260 * @property _currentSelection
264 _currentSelection: null,
267 * The contextual menu that we can open.
269 * @property _contextMenu
270 * @type M.editor_atto.Menu
276 * The last modified target.
278 * @property _lastTarget
285 * The list of menu items.
287 * @property _menuOptions
293 initializer: function() {
296 callback: this._displayTableEditor,
299 // Disable mozilla table controls.
301 document.execCommand("enableInlineTableEditing", false, false);
302 document.execCommand("enableObjectResizing", false, false);
307 * Display the table tool.
309 * @method _displayDialogue
312 _displayDialogue: function() {
313 // Store the current cursor position.
314 this._currentSelection = this.get('host').getSelection();
316 if (this._currentSelection !== false && (!this._currentSelection.collapsed)) {
317 var dialogue = this.getDialogue({
318 headerContent: M.util.get_string('createtable', COMPONENT),
319 focusAfterHide: true,
320 focusOnShowSelector: SELECTORS.CAPTION,
321 width: DIALOGUE.WIDTH
324 // Set the dialogue content, and then show the dialogue.
325 dialogue.set('bodyContent', this._getDialogueContent(false))
328 this._updateAvailableSettings();
333 * Display the appropriate table editor.
335 * If the current selection includes a table, then we show the
336 * contextual menu, otherwise show the table creation dialogue.
338 * @method _displayTableEditor
339 * @param {EventFacade} e
342 _displayTableEditor: function(e) {
343 var cell = this._getSuitableTableCell();
345 // Add the cell to the EventFacade to save duplication in when showing the menu.
347 return this._showTableMenu(e);
349 return this._displayDialogue(e);
353 * Returns whether or not the parameter node exists within the editor.
355 * @method _stopAtContentEditableFilter
358 * @return {boolean} whether or not the parameter node exists within the editor.
360 _stopAtContentEditableFilter: function(node) {
361 return this.editor.contains(node);
365 * Return the dialogue content for the tool, attaching any required
368 * @method _getDialogueContent
370 * @return {Node} The content to place in the dialogue.
372 _getDialogueContent: function(edit) {
373 var template = Y.Handlebars.compile(TEMPLATE);
374 var allowBorders = this.get('allowBorders');
376 this._content = Y.Node.create(template({
378 elementid: this.get('host').get('elementid'),
379 component: COMPONENT,
382 allowStyling: this.get('allowStyling'),
383 allowBorders: allowBorders,
384 borderStyles: this.get('borderStyles'),
385 allowBackgroundColour: this.get('allowBackgroundColour'),
386 availableColours: this.get('availableColors'),
387 allowWidth: this.get('allowWidth')
390 // Handle table setting.
392 this._content.one('.submit').on('click', this._updateTable, this);
394 this._content.one('.submit').on('click', this._setTable, this);
398 this._content.one('[name="borders"]').on('change', this._updateAvailableSettings, this);
401 return this._content;
405 * Disables options within the dialogue if they shouldn't be available.
407 * If borders are set to "Theme default" then the border size, style and
408 * colour options are disabled.
410 * @method _updateAvailableSettings
413 _updateAvailableSettings: function() {
414 var tableForm = this._content,
415 enableBorders = tableForm.one('[name="borders"]'),
416 borderStyle = tableForm.one('[name="borderstyles"]'),
417 borderSize = tableForm.one('[name="bordersize"]'),
418 borderColour = tableForm.all('[name="borderColour"]'),
419 disabledValue = 'removeAttribute';
421 if (!enableBorders) {
425 if (enableBorders.get('value') === 'default') {
426 disabledValue = 'setAttribute';
430 borderStyle[disabledValue]('disabled');
434 borderSize[disabledValue]('disabled');
438 borderColour[disabledValue]('disabled');
444 * Given the current selection, return a table cell suitable for table editing
445 * purposes, i.e. the first table cell selected, or the first cell in the table
446 * that the selection exists in, or null if not within a table.
448 * @method _getSuitableTableCell
450 * @return {Node} suitable target cell, or null if not within a table
452 _getSuitableTableCell: function() {
453 var targetcell = null,
454 host = this.get('host');
455 var stopAtContentEditableFilter = Y.bind(this._stopAtContentEditableFilter, this);
457 host.getSelectedNodes().some(function(node) {
458 if (node.ancestor('td, th, caption', true, stopAtContentEditableFilter)) {
461 var caption = node.ancestor('caption', true, stopAtContentEditableFilter);
463 var table = caption.get('parentNode');
465 targetcell = table.one('td, th');
469 // Once we've found a cell to target, we shouldn't need to keep looking.
475 var selection = host.getSelectionFromNode(targetcell);
476 host.setSelection(selection);
483 * Change a node from one type to another, copying all attributes and children.
485 * @method _changeNodeType
486 * @param {Y.Node} node
487 * @param {String} new node type
491 _changeNodeType: function(node, newType) {
492 var newNode = Y.Node.create('<' + newType + '></' + newType + '>');
493 newNode.setAttrs(node.getAttrs());
494 node.get('childNodes').each(function(child) {
495 newNode.append(child.remove());
497 node.replace(newNode);
502 * Handle updating an existing table.
504 * @method _updateTable
505 * @param {EventFacade} e
508 _updateTable: function(e) {
522 // Hide the dialogue.
527 // Add/update the caption.
528 caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
529 captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
530 headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
531 borders = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERS);
532 bordersize = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSIZE);
533 bordercolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBORDERCOLOUR);
534 borderstyle = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSTYLE);
535 backgroundcolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBACKGROUNDCOLOUR);
536 width = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.WIDTH);
538 table = this._lastTarget.ancestor('table');
539 this._setAppearance(table, {
542 borderColour: bordercolour,
543 borderSize: bordersize,
544 borderStyle: borderstyle,
545 backgroundColour: backgroundcolour
548 captionnode = table.one('caption');
550 captionnode = Y.Node.create('<caption></caption>');
551 table.insert(captionnode, 0);
553 captionnode.setHTML(caption.get('value'));
554 captionnode.setStyle('caption-side', captionposition.get('value'));
555 if (!captionnode.getAttribute('style')) {
556 captionnode.removeAttribute('style');
559 // Add the row headers.
560 if (headers.get('value') === 'rows' || headers.get('value') === 'both') {
561 table.all('tr').each(function(row) {
562 var cells = row.all('th, td'),
563 firstCell = cells.shift(),
566 if (firstCell.get('tagName') === 'TD') {
567 // Cell is a td but should be a th - change it.
568 newCell = this._changeNodeType(firstCell, 'th');
569 newCell.setAttribute('scope', 'row');
571 firstCell.setAttribute('scope', 'row');
574 // Now make sure all other cells in the row are td.
575 cells.each(function(cell) {
576 if (cell.get('tagName') === 'TH') {
577 newCell = this._changeNodeType(cell, 'td');
578 newCell.removeAttribute('scope');
584 // Add the col headers. These may overrule the row headers in the first cell.
585 if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
586 var rows = table.all('tr'),
587 firstRow = rows.shift(),
590 firstRow.all('td, th').each(function(cell) {
591 if (cell.get('tagName') === 'TD') {
592 // Cell is a td but should be a th - change it.
593 newCell = this._changeNodeType(cell, 'th');
594 newCell.setAttribute('scope', 'col');
596 cell.setAttribute('scope', 'col');
599 // Change all the cells in the rest of the table to tds (unless they are row headers).
600 rows.each(function(row) {
601 var cells = row.all('th, td');
603 if (headers.get('value') === 'both') {
604 // Ignore the first cell because it's a row header.
607 cells.each(function(cell) {
608 if (cell.get('tagName') === 'TH') {
609 newCell = this._changeNodeType(cell, 'td');
610 newCell.removeAttribute('scope');
621 * Handle creation of a new table.
624 * @param {EventFacade} e
627 _setTable: function(e) {
644 // Hide the dialogue.
649 caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
650 captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
651 borders = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERS);
652 bordersize = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSIZE);
653 bordercolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBORDERCOLOUR);
654 borderstyle = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSTYLE);
655 backgroundcolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBACKGROUNDCOLOUR);
656 rows = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.ROWS);
657 cols = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.COLUMNS);
658 headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
659 width = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.WIDTH);
661 // Set the selection.
662 this.get('host').setSelection(this._currentSelection);
664 // Note there are some spaces inserted in the cells and before and after, so that users have somewhere to click.
666 var tableId = Y.guid();
667 tablehtml = '<br/>' + nl + '<table id="' + tableId + '">' + nl;
669 var captionstyle = '';
670 if (captionposition.get('value')) {
671 captionstyle = ' style="caption-side: ' + captionposition.get('value') + '"';
673 tablehtml += '<caption' + captionstyle + '>' + Y.Escape.html(caption.get('value')) + '</caption>' + nl;
675 if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
677 tablehtml += '<thead>' + nl + '<tr>' + nl;
678 for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
679 tablehtml += '<th scope="col"></th>' + nl;
681 tablehtml += '</tr>' + nl + '</thead>' + nl;
683 tablehtml += '<tbody>' + nl;
684 for (; i < parseInt(rows.get('value'), 10); i++) {
685 tablehtml += '<tr>' + nl;
686 for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
687 if (j === 0 && (headers.get('value') === 'rows' || headers.get('value') === 'both')) {
688 tablehtml += '<th scope="row"></th>' + nl;
690 tablehtml += '<td ></td>' + nl;
693 tablehtml += '</tr>' + nl;
695 tablehtml += '</tbody>' + nl;
696 tablehtml += '</table>' + nl + '<br/>';
698 this.get('host').insertContentAtFocusPoint(tablehtml);
700 var tableNode = Y.one('#' + tableId);
701 this._setAppearance(tableNode, {
704 borderColour: bordercolour,
705 borderSize: bordersize,
706 borderStyle: borderstyle,
707 backgroundColour: backgroundcolour
709 tableNode.removeAttribute('id');
711 // Mark the content as updated.
716 * Search for all the cells in the current, next and previous columns.
718 * @method _findColumnCells
720 * @return {Object} containing current, prev and next {Y.NodeList}s
722 _findColumnCells: function() {
723 var columnindex = this._getColumnIndex(this._lastTarget),
724 rows = this._lastTarget.ancestor('table').all('tr'),
725 currentcells = new Y.NodeList(),
726 prevcells = new Y.NodeList(),
727 nextcells = new Y.NodeList();
729 rows.each(function(row) {
730 var cells = row.all('td, th'),
731 cell = cells.item(columnindex),
732 cellprev = cells.item(columnindex - 1),
733 cellnext = cells.item(columnindex + 1);
734 currentcells.push(cell);
736 prevcells.push(cellprev);
739 nextcells.push(cellnext);
744 current: currentcells,
751 * Hide the entries in the context menu that don't make sense with the
754 * @method _hideInvalidEntries
755 * @param {Y.Node} node - The node containing the menu.
758 _hideInvalidEntries: function(node) {
760 var table = this._lastTarget.ancestor('table'),
761 row = this._lastTarget.ancestor('tr'),
762 rows = table.all('tr'),
763 rowindex = rows.indexOf(row),
764 prevrow = rows.item(rowindex - 1),
765 prevrowhascells = prevrow ? prevrow.one('td') : null;
767 if (!row || !prevrowhascells) {
768 node.one('[data-change="moverowup"]').hide();
770 node.one('[data-change="moverowup"]').show();
773 var nextrow = rows.item(rowindex + 1),
774 rowhascell = row ? row.one('td') : false;
776 if (!row || !nextrow || !rowhascell) {
777 node.one('[data-change="moverowdown"]').hide();
779 node.one('[data-change="moverowdown"]').show();
783 var cells = this._findColumnCells();
784 if (cells.prev.filter('td').size() > 0) {
785 node.one('[data-change="movecolumnleft"]').show();
787 node.one('[data-change="movecolumnleft"]').hide();
790 var colhascell = cells.current.filter('td').size() > 0;
791 if ((cells.next.size() > 0) && colhascell) {
792 node.one('[data-change="movecolumnright"]').show();
794 node.one('[data-change="movecolumnright"]').hide();
798 if (cells.current.filter('td').size() > 0) {
799 node.one('[data-change="deletecolumn"]').show();
801 node.one('[data-change="deletecolumn"]').hide();
804 if (!row || !row.one('td')) {
805 node.one('[data-change="deleterow"]').hide();
807 node.one('[data-change="deleterow"]').show();
812 * Display the table menu.
814 * @method _showTableMenu
815 * @param {EventFacade} e
818 _showTableMenu: function(e) {
823 if (!this._contextMenu) {
824 this._menuOptions = [
826 text: M.util.get_string("addcolumnafter", COMPONENT),
828 change: "addcolumnafter"
831 text: M.util.get_string("addrowafter", COMPONENT),
833 change: "addrowafter"
836 text: M.util.get_string("moverowup", COMPONENT),
841 text: M.util.get_string("moverowdown", COMPONENT),
843 change: "moverowdown"
846 text: M.util.get_string("movecolumnleft", COMPONENT),
848 change: "movecolumnleft"
851 text: M.util.get_string("movecolumnright", COMPONENT),
853 change: "movecolumnright"
856 text: M.util.get_string("deleterow", COMPONENT),
861 text: M.util.get_string("deletecolumn", COMPONENT),
863 change: "deletecolumn"
866 text: M.util.get_string("edittable", COMPONENT),
873 this._contextMenu = new Y.M.editor_atto.Menu({
874 items: this._menuOptions
877 // Add event handlers for table control menus.
878 boundingBox = this._contextMenu.get('boundingBox');
879 boundingBox.delegate('click', this._handleTableChange, 'a', this);
882 boundingBox = this._contextMenu.get('boundingBox');
884 // We store the cell of the last click (the control node is transient).
885 this._lastTarget = e.tableCell.ancestor('.editor_atto_content td, .editor_atto_content th', true);
887 this._hideInvalidEntries(boundingBox);
889 // Clear the focusAfterHide for any other menus which may be open.
890 Y.Array.each(this.get('host').openMenus, function(menu) {
891 menu.set('focusAfterHide', null);
894 // Ensure that we focus on the button in the toolbar when we tab back to the menu.
895 var creatorButton = this.buttons[this.name];
896 this.get('host')._setTabFocus(creatorButton);
898 // Show the context menu, and align to the current position.
899 this._contextMenu.show();
900 this._contextMenu.align(this.buttons.table, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
901 this._contextMenu.set('focusAfterHide', creatorButton);
903 // If there are any anchors in the bounding box, focus on the first.
904 if (boundingBox.one('a')) {
905 boundingBox.one('a').focus();
908 // Add this menu to the list of open menus.
909 this.get('host').openMenus = [this._contextMenu];
913 * Handle a selection from the table control menu.
915 * @method _handleTableChange
916 * @param {EventFacade} e
919 _handleTableChange: function(e) {
922 this._contextMenu.set('focusAfterHide', this.get('host').editor);
923 // Hide the context menu.
924 this._contextMenu.hide(e);
927 switch (e.target.getData('change')) {
928 case 'addcolumnafter':
929 this._addColumnAfter();
938 this._deleteColumn();
949 case 'movecolumnleft':
950 this._moveColumnLeft();
952 case 'movecolumnright':
953 this._moveColumnRight();
959 * Determine the index of a row in a table column.
961 * @method _getRowIndex
965 _getRowIndex: function(cell) {
966 var tablenode = cell.ancestor('table'),
967 rownode = cell.ancestor('tr');
969 if (!tablenode || !rownode) {
973 var rows = tablenode.all('tr');
975 return rows.indexOf(rownode);
979 * Determine the index of a column in a table row.
981 * @method _getColumnIndex
982 * @param {Node} cellnode
985 _getColumnIndex: function(cellnode) {
986 var rownode = cellnode.ancestor('tr');
992 var cells = rownode.all('td, th');
994 return cells.indexOf(cellnode);
998 * Delete the current row.
1000 * @method _deleteRow
1003 _deleteRow: function() {
1004 var row = this._lastTarget.ancestor('tr');
1006 if (row && row.one('td')) {
1007 // Only delete rows with at least one non-header cell.
1018 * @method _moveRowUp
1021 _moveRowUp: function() {
1022 var row = this._lastTarget.ancestor('tr'),
1023 prevrow = row.previous('tr');
1024 if (!row || !prevrow) {
1036 * @method _moveColumnLeft
1039 _moveColumnLeft: function() {
1040 var cells = this._findColumnCells();
1042 if (cells.current.size() > 0 && cells.prev.size() > 0 && cells.current.size() === cells.prev.size()) {
1044 for (i = 0; i < cells.current.size(); i++) {
1045 var cell = cells.current.item(i),
1046 prevcell = cells.prev.item(i);
1048 cell.swap(prevcell);
1056 * Add a caption to the table if it doesn't have one.
1058 * @method _addCaption
1061 _addCaption: function() {
1062 var table = this._lastTarget.ancestor('table'),
1063 caption = table.one('caption');
1066 table.insert(Y.Node.create('<caption> </caption>'), 1);
1071 * Remove a caption from the table if has one.
1073 * @method _removeCaption
1076 _removeCaption: function() {
1077 var table = this._lastTarget.ancestor('table'),
1078 caption = table.one('caption');
1081 caption.remove(true);
1086 * Move column right.
1088 * @method _moveColumnRight
1091 _moveColumnRight: function() {
1092 var cells = this._findColumnCells();
1094 // Check we have some tds in this column, and one exists to the right.
1095 if ((cells.next.size() > 0) &&
1096 (cells.current.size() === cells.next.size()) &&
1097 (cells.current.filter('td').size() > 0)) {
1099 for (i = 0; i < cells.current.size(); i++) {
1100 var cell = cells.current.item(i),
1101 nextcell = cells.next.item(i);
1103 cell.swap(nextcell);
1113 * @method _moveRowDown
1116 _moveRowDown: function() {
1117 var row = this._lastTarget.ancestor('tr'),
1118 nextrow = row.next('tr');
1119 if (!row || !nextrow || !row.one('td')) {
1129 * Obtain values for the table borders
1131 * @method _getBorderConfiguration
1132 * @param {Node} node
1134 * @return {Array} or {Boolean} Returns the settings, if presents, or else returns false
1136 _getBorderConfiguration: function(node) {
1137 // We need to make a clone of the node in order to avoid grabbing any
1138 // of the computed styles from the DOM. We only want inline styles set by us.
1139 var shadowNode = node.cloneNode(true);
1140 var borderStyle = shadowNode.getStyle('borderStyle'),
1141 borderColor = shadowNode.getStyle('borderColor'),
1142 borderWidth = shadowNode.getStyle('borderWidth');
1144 if (borderStyle || borderColor || borderWidth) {
1145 var hexColour = Y.Color.toHex(borderColor);
1146 var width = parseInt(borderWidth, 10);
1148 borderStyle: borderStyle,
1149 borderColor: hexColour === "#" ? null : hexColour,
1150 borderWidth: isNaN(width) ? null : width
1158 * Set the appropriate styles on the given table node according to
1159 * the provided configuration.
1161 * @method _setAppearance
1162 * @param {Node} The table node to be modified.
1163 * @param {Object} Configuration object (associative array) containing the form nodes for
1167 _setAppearance: function(tableNode, configuration) {
1171 backgroundcolourvalue;
1173 if (configuration.borderColour) {
1174 borderhex = configuration.borderColour.get('value');
1177 if (configuration.borderSize) {
1178 borderSizeValue = configuration.borderSize.get('value');
1181 if (configuration.borderStyle) {
1182 borderStyleValue = configuration.borderStyle.get('value');
1185 if (configuration.backgroundColour) {
1186 backgroundcolourvalue = configuration.backgroundColour.get('value');
1189 // Clear the inline border styling
1190 tableNode.removeAttribute('style');
1191 tableNode.all('td, th').each(function(cell) {
1192 cell.removeAttribute('style');
1195 if (configuration.borders) {
1196 if (configuration.borders.get('value') === 'outer') {
1197 tableNode.setStyle('borderWidth', borderSizeValue + CSS.BORDERSIZEUNIT);
1198 tableNode.setStyle('borderStyle', borderStyleValue);
1200 if (borderhex !== 'none') {
1201 tableNode.setStyle('borderColor', borderhex);
1203 } else if (configuration.borders.get('value') === 'all') {
1204 tableNode.all('td, th').each(function(cell) {
1205 cell.setStyle('borderWidth', borderSizeValue + CSS.BORDERSIZEUNIT);
1206 cell.setStyle('borderStyle', borderStyleValue);
1208 if (borderhex !== 'none') {
1209 cell.setStyle('borderColor', borderhex);
1215 if (backgroundcolourvalue !== 'none') {
1216 tableNode.setStyle('backgroundColor', backgroundcolourvalue);
1219 if (configuration.width && configuration.width.get('value')) {
1220 tableNode.setStyle('width', configuration.width.get('value') + CSS.WIDTHUNIT);
1225 * Edit table (show the dialogue).
1227 * @method _editTable
1230 _editTable: function() {
1231 var dialogue = this.getDialogue({
1232 headerContent: M.util.get_string('edittable', COMPONENT),
1233 focusAfterHide: false,
1234 focusOnShowSelector: SELECTORS.CAPTION,
1235 width: DIALOGUE.WIDTH
1238 // Set the dialogue content, and then show the dialogue.
1239 var node = this._getDialogueContent(true),
1240 captioninput = node.one(SELECTORS.CAPTION),
1241 captionpositioninput = node.one(SELECTORS.CAPTIONPOSITION),
1242 headersinput = node.one(SELECTORS.HEADERS),
1243 borderinput = node.one(SELECTORS.BORDERS),
1244 borderstyle = node.one(SELECTORS.BORDERSTYLE),
1245 bordercolours = node.all(SELECTORS.BORDERCOLOURS),
1246 bordersize = node.one(SELECTORS.BORDERSIZE),
1247 backgroundcolours = node.all(SELECTORS.BACKGROUNDCOLOURS),
1248 width = node.one(SELECTORS.WIDTH),
1249 table = this._lastTarget.ancestor('table'),
1250 captionnode = table.one('caption'),
1255 captioninput.set('value', captionnode.getHTML());
1257 captioninput.set('value', '');
1260 if (width && table.getStyle('width').indexOf('px') === -1) {
1261 width.set('value', parseInt(table.getStyle('width'), 10));
1264 if (captionpositioninput && captionnode && captionnode.getAttribute('style')) {
1265 captionpositioninput.set('value', captionnode.getStyle('caption-side'));
1268 captionpositioninput.set('value', '');
1271 if (table.getStyle('backgroundColor') && this.get('allowBackgroundColour')) {
1272 hexColour = Y.Color.toHex(table.getStyle('backgroundColor'));
1273 matchedInput = backgroundcolours.filter('[value="' + hexColour + '"]');
1276 matchedInput.set("checked", true);
1280 if (this.get('allowBorders')) {
1281 var borderValue = 'default',
1282 borderConfiguration = this._getBorderConfiguration(table);
1284 if (borderConfiguration) {
1285 borderValue = 'outer';
1287 borderConfiguration = this._getBorderConfiguration(table.one('td'));
1288 if (borderConfiguration) {
1289 borderValue = 'all';
1293 if (borderConfiguration) {
1294 var borderStyle = borderConfiguration.borderStyle || DEFAULT.BORDERSTYLE;
1295 var borderSize = borderConfiguration.borderWidth || DEFAULT.BORDERWIDTH;
1296 borderstyle.set('value', borderStyle);
1297 bordersize.set('value', borderSize);
1298 borderinput.set('value', borderValue);
1300 hexColour = borderConfiguration.borderColor;
1301 matchedInput = bordercolours.filter('[value="' + hexColour + '"]');
1304 matchedInput.set("checked", true);
1309 var headersvalue = 'columns';
1310 if (table.one('th[scope="row"]')) {
1311 headersvalue = 'rows';
1312 if (table.one('th[scope="col"]')) {
1313 headersvalue = 'both';
1316 headersinput.set('value', headersvalue);
1317 dialogue.set('bodyContent', node).show();
1318 this._updateAvailableSettings();
1323 * Delete the current column.
1325 * @method _deleteColumn
1328 _deleteColumn: function() {
1329 var columnindex = this._getColumnIndex(this._lastTarget),
1330 table = this._lastTarget.ancestor('table'),
1331 rows = table.all('tr'),
1332 columncells = new Y.NodeList(),
1335 rows.each(function(row) {
1336 var cells = row.all('td, th');
1337 var cell = cells.item(columnindex);
1338 if (cell.get('tagName') === 'TD') {
1341 columncells.push(cell);
1344 // Do not delete all the headers.
1346 columncells.remove(true);
1354 * Add a row after the current row.
1356 * @method _addRowAfter
1359 _addRowAfter: function() {
1360 var target = this._lastTarget.ancestor('tr'),
1361 tablebody = this._lastTarget.ancestor('table').one('tbody');
1363 // Not all tables have tbody.
1364 tablebody = this._lastTarget.ancestor('table');
1367 var firstrow = tablebody.one('tr');
1369 firstrow = this._lastTarget.ancestor('table').one('tr');
1372 // Table has no rows. Boo.
1375 var newrow = firstrow.cloneNode(true);
1376 newrow.all('th, td').each(function(tablecell) {
1377 if (tablecell.get('tagName') === 'TH') {
1378 if (tablecell.getAttribute('scope') !== 'row') {
1379 var newcell = Y.Node.create('<td></td>');
1380 tablecell.replace(newcell);
1381 tablecell = newcell;
1384 tablecell.setHTML(' ');
1387 if (target.ancestor('thead')) {
1389 tablebody.insert(newrow, target);
1391 target.insert(newrow, 'after');
1399 * Add a column after the current column.
1401 * @method _addColumnAfter
1404 _addColumnAfter: function() {
1405 var cells = this._findColumnCells(),
1407 clonecells = cells.next;
1408 if (cells.next.size() <= 0) {
1410 clonecells = cells.current;
1413 Y.each(clonecells, function(cell) {
1414 var newcell = cell.cloneNode();
1415 // Clear the content of the cell.
1416 newcell.setHTML(' ');
1419 cell.get('parentNode').insert(newcell, cell);
1421 cell.get('parentNode').insert(newcell, cell);
1433 * Whether or not to allow borders
1435 * @attribute allowBorder
1443 * What border styles to allow
1445 * @attribute borderStyles
1458 * Whether or not to allow colourizing the background
1460 * @attribute allowBackgroundColour
1463 allowBackgroundColour: {
1468 * Whether or not to allow setting the table width
1470 * @attribute allowWidth
1478 * Whether we allow styling
1479 * @attribute allowStyling
1484 getter: function() {
1485 return this.get('allowBorders') || this.get('allowBackgroundColour') || this.get('allowWidth');
1491 * @attribute availableColors