1 YUI.add('moodle-atto_table-button', function (Y, NAME) {
3 // This file is part of Moodle - http://moodle.org/
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
20 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 * @module moodle-atto_table-button
29 * Atto text editor table plugin.
31 * @namespace M.atto_table
33 * @extends M.editor_atto.EditorPlugin
36 var COMPONENT = 'atto_table',
45 '<form class="{{CSS.FORM}}">' +
46 '<div class="mb-1 form-group row-fluid">' +
47 '<div class="col-sm-4">' +
48 '<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' +
49 '</div><div class="col-sm-8">' +
50 '<input type="text" class="form-control {{CSS.CAPTION}}" id="{{elementid}}_atto_table_caption" required />' +
53 '<div class="mb-1 form-group row-fluid">' +
54 '<div class="col-sm-4">' +
55 '<label for="{{elementid}}_atto_table_captionposition">' +
56 '{{get_string "captionposition" component}}</label>' +
57 '</div><div class="col-sm-8">' +
58 '<select class="custom-select {{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +
59 '<option value=""></option>' +
60 '<option value="top">{{get_string "top" "editor"}}</option>' +
61 '<option value="bottom">{{get_string "bottom" "editor"}}</option>' +
65 '<div class="mb-1 form-group row-fluid">' +
66 '<div class="col-sm-4">' +
67 '<label for="{{elementid}}_atto_table_headers">{{get_string "headers" component}}</label>' +
68 '</div><div class="col-sm-8">' +
69 '<select class="custom-select {{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
70 '<option value="columns">{{get_string "columns" component}}' + '</option>' +
71 '<option value="rows">{{get_string "rows" component}}' + '</option>' +
72 '<option value="both">{{get_string "both" component}}' + '</option>' +
77 '<div class="mb-1 form-group row-fluid">' +
78 '<div class="col-sm-4">' +
79 '<label for="{{elementid}}_atto_table_rows">{{get_string "numberofrows" component}}</label>' +
80 '</div><div class="col-sm-8">' +
81 '<input class="form-control w-auto {{CSS.ROWS}}" type="number" value="3" ' +
82 'id="{{elementid}}_atto_table_rows" size="8" min="1" max="50"/>' +
85 '<div class="mb-1 form-group row-fluid">' +
86 '<div class="col-sm-4">' +
87 '<label for="{{elementid}}_atto_table_columns" ' +
88 '>{{get_string "numberofcolumns" component}}</label>' +
89 '</div><div class="col-sm-8">' +
90 '<input class="form-control w-auto {{CSS.COLUMNS}}" type="number" value="3" ' +
91 'id="{{elementid}}_atto_table_columns"' +
92 'size="8" min="1" max="20"/>' +
96 '{{#if allowStyling}}' +
98 '<legend class="mdl-align">{{get_string "appearance" component}}</legend>' +
99 '{{#if allowBorders}}' +
100 '<div class="mb-1 form-group row-fluid">' +
101 '<div class="col-sm-4">' +
102 '<label for="{{elementid}}_atto_table_borders">{{get_string "borders" component}}</label>' +
103 '</div><div class="col-sm-8">' +
104 '<select name="borders" class="custom-select {{CSS.BORDERS}}" id="{{elementid}}_atto_table_borders">' +
105 '<option value="default">{{get_string "themedefault" component}}' + '</option>' +
106 '<option value="outer">{{get_string "outer" component}}' + '</option>' +
107 '<option value="all">{{get_string "all" component}}' + '</option>' +
111 '<div class="mb-1 form-group row-fluid">' +
112 '<div class="col-sm-4">' +
113 '<label for="{{elementid}}_atto_table_borderstyle">' +
114 '{{get_string "borderstyles" component}}</label>' +
115 '</div><div class="col-sm-8">' +
116 '<select name="borderstyles" class="custom-select {{CSS.BORDERSTYLE}}" ' +
117 'id="{{elementid}}_atto_table_borderstyle">' +
118 '{{#each borderStyles}}' +
119 '<option value="' + '{{this}}' + '">' + '{{get_string this ../component}}' + '</option>' +
124 '<div class="mb-1 form-group row-fluid">' +
125 '<div class="col-sm-4">' +
126 '<label for="{{elementid}}_atto_table_bordersize">' +
127 '{{get_string "bordersize" component}}</label>' +
128 '</div><div class="col-sm-8">' +
129 '<div class="form-inline">' +
130 '<input name="bordersize" id="{{elementid}}_atto_table_bordersize" ' +
131 'class="form-control w-auto mr-1 {{CSS.BORDERSIZE}}"' +
132 'type="number" value="1" size="8" min="1" max="50"/>' +
133 '<label>{{CSS.BORDERSIZEUNIT}}</label>' +
137 '<div class="mb-1 form-group row-fluid">' +
138 '<div class="col-sm-4">' +
139 '<label for="{{elementid}}_atto_table_bordercolour">' +
140 '{{get_string "bordercolour" component}}</label>' +
141 '</div><div class="col-sm-8">' +
142 '<div id="{{elementid}}_atto_table_bordercolour"' +
143 'class="form-inline {{CSS.BORDERCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
144 '<div class="tablebordercolor" style="background-color:transparent;color:transparent">' +
145 '<input id="{{../elementid}}_atto_table_bordercolour_-1"' +
146 'type="radio" class="m-0" name="borderColour" value="none" checked="checked"' +
147 'title="{{get_string "themedefault" component}}"></input>' +
148 '<label for="{{../elementid}}_atto_table_bordercolour_-1" class="accesshide">' +
149 '{{get_string "themedefault" component}}</label>' +
151 '{{#each availableColours}}' +
152 '<div class="tablebordercolor" style="background-color:{{this}};color:{{this}}">' +
153 '<input id="{{../elementid}}_atto_table_bordercolour_{{@index}}"' +
154 'type="radio" class="m-0" name="borderColour" value="' + '{{this}}' + '" title="{{this}}">' +
155 '<label for="{{../elementid}}_atto_table_bordercolour_{{@index}}" class="accesshide">' +
163 '{{#if allowBackgroundColour}}' +
164 '<div class="mb-1 form-group row-fluid">' +
165 '<div class="col-sm-4">' +
166 '<label for="{{elementid}}_atto_table_backgroundcolour">' +
167 '{{get_string "backgroundcolour" component}}</label>' +
168 '</div><div class="col-sm-8">' +
169 '<div id="{{elementid}}_atto_table_backgroundcolour"' +
170 'class="form-inline {{CSS.BACKGROUNDCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
171 '<div class="tablebackgroundcolor" style="background-color:transparent;color:transparent">' +
172 '<input id="{{../elementid}}_atto_table_backgroundcolour_-1"' +
173 'type="radio" class="m-0" name="backgroundColour" value="none" checked="checked"' +
174 'title="{{get_string "themedefault" component}}"></input>' +
175 '<label for="{{../elementid}}_atto_table_backgroundcolour_-1" class="accesshide">' +
176 '{{get_string "themedefault" component}}</label>' +
179 '{{#each availableColours}}' +
180 '<div class="tablebackgroundcolor" style="background-color:{{this}};color:{{this}}">' +
181 '<input id="{{../elementid}}_atto_table_backgroundcolour_{{@index}}"' +
182 'type="radio" class="m-0" name="backgroundColour" value="' + '{{this}}' + '" title="{{this}}">' +
183 '<label for="{{../elementid}}_atto_table_backgroundcolour_{{@index}}" class="accesshide">' +
191 '{{#if allowWidth}}' +
192 '<div class="mb-1 form-group row-fluid">' +
193 '<div class="col-sm-4">' +
194 '<label for="{{elementid}}_atto_table_width">' +
195 '{{get_string "width" component}}</label>' +
196 '</div><div class="col-sm-8">' +
197 '<div class="form-inline">' +
198 '<input name="width" id="{{elementid}}_atto_table_width" ' +
199 'class="form-control w-auto mr-1 {{CSS.WIDTH}}" size="8" ' +
200 'type="number" min="0" max="100"/>' +
201 '<label>{{CSS.WIDTHUNIT}}</label>' +
208 '<div class="mdl-align">' +
211 '<button class="btn btn-secondary submit" type="submit">{{get_string "updatetable" component}}</button>' +
214 '<button class="btn btn-secondary submit" type="submit">{{get_string "createtable" component}}</button>' +
220 CAPTIONPOSITION: 'captionposition',
227 BORDERSIZE: 'bordersize',
228 BORDERSIZEUNIT: 'px',
229 BORDERCOLOUR: 'bordercolour',
230 BORDERSTYLE: 'borderstyle',
231 BACKGROUNDCOLOUR: 'backgroundcolour',
232 WIDTH: 'customwidth',
234 AVAILABLECOLORS: 'availablecolors',
235 COLOURROW: 'colourrow'
238 CAPTION: '.' + CSS.CAPTION,
239 CAPTIONPOSITION: '.' + CSS.CAPTIONPOSITION,
240 HEADERS: '.' + CSS.HEADERS,
241 ROWS: '.' + CSS.ROWS,
242 COLUMNS: '.' + CSS.COLUMNS,
243 SUBMIT: '.' + CSS.SUBMIT,
244 BORDERS: '.' + CSS.BORDERS,
245 BORDERSIZE: '.' + CSS.BORDERSIZE,
246 BORDERCOLOURS: '.' + CSS.BORDERCOLOUR + ' input[name="borderColour"]',
247 SELECTEDBORDERCOLOUR: '.' + CSS.BORDERCOLOUR + ' input[name="borderColour"]:checked',
248 BORDERSTYLE: '.' + CSS.BORDERSTYLE,
249 BACKGROUNDCOLOURS: '.' + CSS.BACKGROUNDCOLOUR + ' input[name="backgroundColour"]',
250 SELECTEDBACKGROUNDCOLOUR: '.' + CSS.BACKGROUNDCOLOUR + ' input[name="backgroundColour"]:checked',
252 WIDTH: '.' + CSS.WIDTH,
253 AVAILABLECOLORS: '.' + CSS.AVAILABLECOLORS
256 Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
259 * A reference to the current selection at the time that the dialogue
262 * @property _currentSelection
266 _currentSelection: null,
269 * The contextual menu that we can open.
271 * @property _contextMenu
272 * @type M.editor_atto.Menu
278 * The last modified target.
280 * @property _lastTarget
287 * The list of menu items.
289 * @property _menuOptions
295 initializer: function() {
298 callback: this._displayTableEditor,
301 // Disable mozilla table controls.
303 document.execCommand("enableInlineTableEditing", false, false);
304 document.execCommand("enableObjectResizing", false, false);
309 * Display the table tool.
311 * @method _displayDialogue
314 _displayDialogue: function() {
315 // Store the current cursor position.
316 this._currentSelection = this.get('host').getSelection();
318 if (this._currentSelection !== false && (!this._currentSelection.collapsed)) {
319 var dialogue = this.getDialogue({
320 headerContent: M.util.get_string('createtable', COMPONENT),
321 focusAfterHide: true,
322 focusOnShowSelector: SELECTORS.CAPTION,
323 width: DIALOGUE.WIDTH
326 // Set the dialogue content, and then show the dialogue.
327 dialogue.set('bodyContent', this._getDialogueContent(false))
330 this._updateAvailableSettings();
335 * Display the appropriate table editor.
337 * If the current selection includes a table, then we show the
338 * contextual menu, otherwise show the table creation dialogue.
340 * @method _displayTableEditor
341 * @param {EventFacade} e
344 _displayTableEditor: function(e) {
345 var cell = this._getSuitableTableCell();
347 // Add the cell to the EventFacade to save duplication in when showing the menu.
349 return this._showTableMenu(e);
351 return this._displayDialogue(e);
355 * Returns whether or not the parameter node exists within the editor.
357 * @method _stopAtContentEditableFilter
360 * @return {boolean} whether or not the parameter node exists within the editor.
362 _stopAtContentEditableFilter: function(node) {
363 return this.editor.contains(node);
367 * Return the dialogue content for the tool, attaching any required
370 * @method _getDialogueContent
372 * @return {Node} The content to place in the dialogue.
374 _getDialogueContent: function(edit) {
375 var template = Y.Handlebars.compile(TEMPLATE);
376 var allowBorders = this.get('allowBorders');
378 this._content = Y.Node.create(template({
380 elementid: this.get('host').get('elementid'),
381 component: COMPONENT,
384 allowStyling: this.get('allowStyling'),
385 allowBorders: allowBorders,
386 borderStyles: this.get('borderStyles'),
387 allowBackgroundColour: this.get('allowBackgroundColour'),
388 availableColours: this.get('availableColors'),
389 allowWidth: this.get('allowWidth')
392 // Handle table setting.
394 this._content.one('.submit').on('click', this._updateTable, this);
396 this._content.one('.submit').on('click', this._setTable, this);
400 this._content.one('[name="borders"]').on('change', this._updateAvailableSettings, this);
403 return this._content;
407 * Disables options within the dialogue if they shouldn't be available.
409 * If borders are set to "Theme default" then the border size, style and
410 * colour options are disabled.
412 * @method _updateAvailableSettings
415 _updateAvailableSettings: function() {
416 var tableForm = this._content,
417 enableBorders = tableForm.one('[name="borders"]'),
418 borderStyle = tableForm.one('[name="borderstyles"]'),
419 borderSize = tableForm.one('[name="bordersize"]'),
420 borderColour = tableForm.all('[name="borderColour"]'),
421 disabledValue = 'removeAttribute';
423 if (!enableBorders) {
427 if (enableBorders.get('value') === 'default') {
428 disabledValue = 'setAttribute';
432 borderStyle[disabledValue]('disabled');
436 borderSize[disabledValue]('disabled');
440 borderColour[disabledValue]('disabled');
446 * Given the current selection, return a table cell suitable for table editing
447 * purposes, i.e. the first table cell selected, or the first cell in the table
448 * that the selection exists in, or null if not within a table.
450 * @method _getSuitableTableCell
452 * @return {Node} suitable target cell, or null if not within a table
454 _getSuitableTableCell: function() {
455 var targetcell = null,
456 host = this.get('host');
457 var stopAtContentEditableFilter = Y.bind(this._stopAtContentEditableFilter, this);
459 host.getSelectedNodes().some(function(node) {
460 if (node.ancestor('td, th, caption', true, stopAtContentEditableFilter)) {
463 var caption = node.ancestor('caption', true, stopAtContentEditableFilter);
465 var table = caption.get('parentNode');
467 targetcell = table.one('td, th');
471 // Once we've found a cell to target, we shouldn't need to keep looking.
477 var selection = host.getSelectionFromNode(targetcell);
478 host.setSelection(selection);
485 * Change a node from one type to another, copying all attributes and children.
487 * @method _changeNodeType
488 * @param {Y.Node} node
489 * @param {String} new node type
493 _changeNodeType: function(node, newType) {
494 var newNode = Y.Node.create('<' + newType + '></' + newType + '>');
495 newNode.setAttrs(node.getAttrs());
496 node.get('childNodes').each(function(child) {
497 newNode.append(child.remove());
499 node.replace(newNode);
504 * Handle updating an existing table.
506 * @method _updateTable
507 * @param {EventFacade} e
510 _updateTable: function(e) {
524 // Hide the dialogue.
529 // Add/update the caption.
530 caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
531 captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
532 headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
533 borders = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERS);
534 bordersize = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSIZE);
535 bordercolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBORDERCOLOUR);
536 borderstyle = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSTYLE);
537 backgroundcolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBACKGROUNDCOLOUR);
538 width = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.WIDTH);
540 table = this._lastTarget.ancestor('table');
541 this._setAppearance(table, {
544 borderColour: bordercolour,
545 borderSize: bordersize,
546 borderStyle: borderstyle,
547 backgroundColour: backgroundcolour
550 captionnode = table.one('caption');
552 captionnode = Y.Node.create('<caption></caption>');
553 table.insert(captionnode, 0);
555 captionnode.setHTML(caption.get('value'));
556 captionnode.setStyle('caption-side', captionposition.get('value'));
557 if (!captionnode.getAttribute('style')) {
558 captionnode.removeAttribute('style');
561 // Add the row headers.
562 if (headers.get('value') === 'rows' || headers.get('value') === 'both') {
563 table.all('tr').each(function(row) {
564 var cells = row.all('th, td'),
565 firstCell = cells.shift(),
568 if (firstCell.get('tagName') === 'TD') {
569 // Cell is a td but should be a th - change it.
570 newCell = this._changeNodeType(firstCell, 'th');
571 newCell.setAttribute('scope', 'row');
573 firstCell.setAttribute('scope', 'row');
576 // Now make sure all other cells in the row are td.
577 cells.each(function(cell) {
578 if (cell.get('tagName') === 'TH') {
579 newCell = this._changeNodeType(cell, 'td');
580 newCell.removeAttribute('scope');
586 // Add the col headers. These may overrule the row headers in the first cell.
587 if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
588 var rows = table.all('tr'),
589 firstRow = rows.shift(),
592 firstRow.all('td, th').each(function(cell) {
593 if (cell.get('tagName') === 'TD') {
594 // Cell is a td but should be a th - change it.
595 newCell = this._changeNodeType(cell, 'th');
596 newCell.setAttribute('scope', 'col');
598 cell.setAttribute('scope', 'col');
601 // Change all the cells in the rest of the table to tds (unless they are row headers).
602 rows.each(function(row) {
603 var cells = row.all('th, td');
605 if (headers.get('value') === 'both') {
606 // Ignore the first cell because it's a row header.
609 cells.each(function(cell) {
610 if (cell.get('tagName') === 'TH') {
611 newCell = this._changeNodeType(cell, 'td');
612 newCell.removeAttribute('scope');
623 * Handle creation of a new table.
626 * @param {EventFacade} e
629 _setTable: function(e) {
646 // Hide the dialogue.
651 caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
652 captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
653 borders = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERS);
654 bordersize = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSIZE);
655 bordercolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBORDERCOLOUR);
656 borderstyle = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSTYLE);
657 backgroundcolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBACKGROUNDCOLOUR);
658 rows = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.ROWS);
659 cols = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.COLUMNS);
660 headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
661 width = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.WIDTH);
663 // Set the selection.
664 this.get('host').setSelection(this._currentSelection);
666 // Note there are some spaces inserted in the cells and before and after, so that users have somewhere to click.
668 var tableId = Y.guid();
669 tablehtml = '<br/>' + nl + '<table id="' + tableId + '">' + nl;
671 var captionstyle = '';
672 if (captionposition.get('value')) {
673 captionstyle = ' style="caption-side: ' + captionposition.get('value') + '"';
675 tablehtml += '<caption' + captionstyle + '>' + Y.Escape.html(caption.get('value')) + '</caption>' + nl;
677 if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
679 tablehtml += '<thead>' + nl + '<tr>' + nl;
680 for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
681 tablehtml += '<th scope="col"></th>' + nl;
683 tablehtml += '</tr>' + nl + '</thead>' + nl;
685 tablehtml += '<tbody>' + nl;
686 for (; i < parseInt(rows.get('value'), 10); i++) {
687 tablehtml += '<tr>' + nl;
688 for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
689 if (j === 0 && (headers.get('value') === 'rows' || headers.get('value') === 'both')) {
690 tablehtml += '<th scope="row"></th>' + nl;
692 tablehtml += '<td ></td>' + nl;
695 tablehtml += '</tr>' + nl;
697 tablehtml += '</tbody>' + nl;
698 tablehtml += '</table>' + nl + '<br/>';
700 this.get('host').insertContentAtFocusPoint(tablehtml);
702 var tableNode = Y.one('#' + tableId);
703 this._setAppearance(tableNode, {
706 borderColour: bordercolour,
707 borderSize: bordersize,
708 borderStyle: borderstyle,
709 backgroundColour: backgroundcolour
711 tableNode.removeAttribute('id');
713 // Mark the content as updated.
718 * Search for all the cells in the current, next and previous columns.
720 * @method _findColumnCells
722 * @return {Object} containing current, prev and next {Y.NodeList}s
724 _findColumnCells: function() {
725 var columnindex = this._getColumnIndex(this._lastTarget),
726 rows = this._lastTarget.ancestor('table').all('tr'),
727 currentcells = new Y.NodeList(),
728 prevcells = new Y.NodeList(),
729 nextcells = new Y.NodeList();
731 rows.each(function(row) {
732 var cells = row.all('td, th'),
733 cell = cells.item(columnindex),
734 cellprev = cells.item(columnindex - 1),
735 cellnext = cells.item(columnindex + 1);
736 currentcells.push(cell);
738 prevcells.push(cellprev);
741 nextcells.push(cellnext);
746 current: currentcells,
753 * Hide the entries in the context menu that don't make sense with the
756 * @method _hideInvalidEntries
757 * @param {Y.Node} node - The node containing the menu.
760 _hideInvalidEntries: function(node) {
762 var table = this._lastTarget.ancestor('table'),
763 row = this._lastTarget.ancestor('tr'),
764 rows = table.all('tr'),
765 rowindex = rows.indexOf(row),
766 prevrow = rows.item(rowindex - 1),
767 prevrowhascells = prevrow ? prevrow.one('td') : null;
769 if (!row || !prevrowhascells) {
770 node.one('[data-change="moverowup"]').hide();
772 node.one('[data-change="moverowup"]').show();
775 var nextrow = rows.item(rowindex + 1),
776 rowhascell = row ? row.one('td') : false;
778 if (!row || !nextrow || !rowhascell) {
779 node.one('[data-change="moverowdown"]').hide();
781 node.one('[data-change="moverowdown"]').show();
785 var cells = this._findColumnCells();
786 if (cells.prev.filter('td').size() > 0) {
787 node.one('[data-change="movecolumnleft"]').show();
789 node.one('[data-change="movecolumnleft"]').hide();
792 var colhascell = cells.current.filter('td').size() > 0;
793 if ((cells.next.size() > 0) && colhascell) {
794 node.one('[data-change="movecolumnright"]').show();
796 node.one('[data-change="movecolumnright"]').hide();
800 if (cells.current.filter('td').size() > 0) {
801 node.one('[data-change="deletecolumn"]').show();
803 node.one('[data-change="deletecolumn"]').hide();
806 if (!row || !row.one('td')) {
807 node.one('[data-change="deleterow"]').hide();
809 node.one('[data-change="deleterow"]').show();
814 * Display the table menu.
816 * @method _showTableMenu
817 * @param {EventFacade} e
820 _showTableMenu: function(e) {
825 if (!this._contextMenu) {
826 this._menuOptions = [
828 text: M.util.get_string("addcolumnafter", COMPONENT),
830 change: "addcolumnafter"
833 text: M.util.get_string("addrowafter", COMPONENT),
835 change: "addrowafter"
838 text: M.util.get_string("moverowup", COMPONENT),
843 text: M.util.get_string("moverowdown", COMPONENT),
845 change: "moverowdown"
848 text: M.util.get_string("movecolumnleft", COMPONENT),
850 change: "movecolumnleft"
853 text: M.util.get_string("movecolumnright", COMPONENT),
855 change: "movecolumnright"
858 text: M.util.get_string("deleterow", COMPONENT),
863 text: M.util.get_string("deletecolumn", COMPONENT),
865 change: "deletecolumn"
868 text: M.util.get_string("edittable", COMPONENT),
875 this._contextMenu = new Y.M.editor_atto.Menu({
876 items: this._menuOptions
879 // Add event handlers for table control menus.
880 boundingBox = this._contextMenu.get('boundingBox');
881 boundingBox.delegate('click', this._handleTableChange, 'a', this);
884 boundingBox = this._contextMenu.get('boundingBox');
886 // We store the cell of the last click (the control node is transient).
887 this._lastTarget = e.tableCell.ancestor('.editor_atto_content td, .editor_atto_content th', true);
889 this._hideInvalidEntries(boundingBox);
891 // Clear the focusAfterHide for any other menus which may be open.
892 Y.Array.each(this.get('host').openMenus, function(menu) {
893 menu.set('focusAfterHide', null);
896 // Ensure that we focus on the button in the toolbar when we tab back to the menu.
897 var creatorButton = this.buttons[this.name];
898 this.get('host')._setTabFocus(creatorButton);
900 // Show the context menu, and align to the current position.
901 this._contextMenu.show();
902 this._contextMenu.align(this.buttons.table, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
903 this._contextMenu.set('focusAfterHide', creatorButton);
905 // If there are any anchors in the bounding box, focus on the first.
906 if (boundingBox.one('a')) {
907 boundingBox.one('a').focus();
910 // Add this menu to the list of open menus.
911 this.get('host').openMenus = [this._contextMenu];
915 * Handle a selection from the table control menu.
917 * @method _handleTableChange
918 * @param {EventFacade} e
921 _handleTableChange: function(e) {
924 this._contextMenu.set('focusAfterHide', this.get('host').editor);
925 // Hide the context menu.
926 this._contextMenu.hide(e);
929 switch (e.target.getData('change')) {
930 case 'addcolumnafter':
931 this._addColumnAfter();
940 this._deleteColumn();
951 case 'movecolumnleft':
952 this._moveColumnLeft();
954 case 'movecolumnright':
955 this._moveColumnRight();
961 * Determine the index of a row in a table column.
963 * @method _getRowIndex
967 _getRowIndex: function(cell) {
968 var tablenode = cell.ancestor('table'),
969 rownode = cell.ancestor('tr');
971 if (!tablenode || !rownode) {
975 var rows = tablenode.all('tr');
977 return rows.indexOf(rownode);
981 * Determine the index of a column in a table row.
983 * @method _getColumnIndex
984 * @param {Node} cellnode
987 _getColumnIndex: function(cellnode) {
988 var rownode = cellnode.ancestor('tr');
994 var cells = rownode.all('td, th');
996 return cells.indexOf(cellnode);
1000 * Delete the current row.
1002 * @method _deleteRow
1005 _deleteRow: function() {
1006 var row = this._lastTarget.ancestor('tr');
1008 if (row && row.one('td')) {
1009 // Only delete rows with at least one non-header cell.
1020 * @method _moveRowUp
1023 _moveRowUp: function() {
1024 var row = this._lastTarget.ancestor('tr'),
1025 prevrow = row.previous('tr');
1026 if (!row || !prevrow) {
1038 * @method _moveColumnLeft
1041 _moveColumnLeft: function() {
1042 var cells = this._findColumnCells();
1044 if (cells.current.size() > 0 && cells.prev.size() > 0 && cells.current.size() === cells.prev.size()) {
1046 for (i = 0; i < cells.current.size(); i++) {
1047 var cell = cells.current.item(i),
1048 prevcell = cells.prev.item(i);
1050 cell.swap(prevcell);
1058 * Add a caption to the table if it doesn't have one.
1060 * @method _addCaption
1063 _addCaption: function() {
1064 var table = this._lastTarget.ancestor('table'),
1065 caption = table.one('caption');
1068 table.insert(Y.Node.create('<caption> </caption>'), 1);
1073 * Remove a caption from the table if has one.
1075 * @method _removeCaption
1078 _removeCaption: function() {
1079 var table = this._lastTarget.ancestor('table'),
1080 caption = table.one('caption');
1083 caption.remove(true);
1088 * Move column right.
1090 * @method _moveColumnRight
1093 _moveColumnRight: function() {
1094 var cells = this._findColumnCells();
1096 // Check we have some tds in this column, and one exists to the right.
1097 if ((cells.next.size() > 0) &&
1098 (cells.current.size() === cells.next.size()) &&
1099 (cells.current.filter('td').size() > 0)) {
1101 for (i = 0; i < cells.current.size(); i++) {
1102 var cell = cells.current.item(i),
1103 nextcell = cells.next.item(i);
1105 cell.swap(nextcell);
1115 * @method _moveRowDown
1118 _moveRowDown: function() {
1119 var row = this._lastTarget.ancestor('tr'),
1120 nextrow = row.next('tr');
1121 if (!row || !nextrow || !row.one('td')) {
1131 * Obtain values for the table borders
1133 * @method _getBorderConfiguration
1134 * @param {Node} node
1136 * @return {Array} or {Boolean} Returns the settings, if presents, or else returns false
1138 _getBorderConfiguration: function(node) {
1139 // We need to make a clone of the node in order to avoid grabbing any
1140 // of the computed styles from the DOM. We only want inline styles set by us.
1141 var shadowNode = node.cloneNode(true);
1142 var borderStyle = shadowNode.getStyle('borderStyle'),
1143 borderColor = shadowNode.getStyle('borderColor'),
1144 borderWidth = shadowNode.getStyle('borderWidth');
1146 if (borderStyle || borderColor || borderWidth) {
1147 var hexColour = Y.Color.toHex(borderColor);
1148 var width = parseInt(borderWidth, 10);
1150 borderStyle: borderStyle,
1151 borderColor: hexColour === "#" ? null : hexColour,
1152 borderWidth: isNaN(width) ? null : width
1160 * Set the appropriate styles on the given table node according to
1161 * the provided configuration.
1163 * @method _setAppearance
1164 * @param {Node} The table node to be modified.
1165 * @param {Object} Configuration object (associative array) containing the form nodes for
1169 _setAppearance: function(tableNode, configuration) {
1173 backgroundcolourvalue;
1175 if (configuration.borderColour) {
1176 borderhex = configuration.borderColour.get('value');
1179 if (configuration.borderSize) {
1180 borderSizeValue = configuration.borderSize.get('value');
1183 if (configuration.borderStyle) {
1184 borderStyleValue = configuration.borderStyle.get('value');
1187 if (configuration.backgroundColour) {
1188 backgroundcolourvalue = configuration.backgroundColour.get('value');
1191 // Clear the inline border styling
1192 tableNode.removeAttribute('style');
1193 tableNode.all('td, th').each(function(cell) {
1194 cell.removeAttribute('style');
1197 if (configuration.borders) {
1198 if (configuration.borders.get('value') === 'outer') {
1199 tableNode.setStyle('borderWidth', borderSizeValue + CSS.BORDERSIZEUNIT);
1200 tableNode.setStyle('borderStyle', borderStyleValue);
1202 if (borderhex !== 'none') {
1203 tableNode.setStyle('borderColor', borderhex);
1205 } else if (configuration.borders.get('value') === 'all') {
1206 tableNode.all('td, th').each(function(cell) {
1207 cell.setStyle('borderWidth', borderSizeValue + CSS.BORDERSIZEUNIT);
1208 cell.setStyle('borderStyle', borderStyleValue);
1210 if (borderhex !== 'none') {
1211 cell.setStyle('borderColor', borderhex);
1217 if (backgroundcolourvalue !== 'none') {
1218 tableNode.setStyle('backgroundColor', backgroundcolourvalue);
1221 if (configuration.width && configuration.width.get('value')) {
1222 tableNode.setStyle('width', configuration.width.get('value') + CSS.WIDTHUNIT);
1227 * Edit table (show the dialogue).
1229 * @method _editTable
1232 _editTable: function() {
1233 var dialogue = this.getDialogue({
1234 headerContent: M.util.get_string('edittable', COMPONENT),
1235 focusAfterHide: false,
1236 focusOnShowSelector: SELECTORS.CAPTION,
1237 width: DIALOGUE.WIDTH
1240 // Set the dialogue content, and then show the dialogue.
1241 var node = this._getDialogueContent(true),
1242 captioninput = node.one(SELECTORS.CAPTION),
1243 captionpositioninput = node.one(SELECTORS.CAPTIONPOSITION),
1244 headersinput = node.one(SELECTORS.HEADERS),
1245 borderinput = node.one(SELECTORS.BORDERS),
1246 borderstyle = node.one(SELECTORS.BORDERSTYLE),
1247 bordercolours = node.all(SELECTORS.BORDERCOLOURS),
1248 bordersize = node.one(SELECTORS.BORDERSIZE),
1249 backgroundcolours = node.all(SELECTORS.BACKGROUNDCOLOURS),
1250 width = node.one(SELECTORS.WIDTH),
1251 table = this._lastTarget.ancestor('table'),
1252 captionnode = table.one('caption'),
1257 captioninput.set('value', captionnode.getHTML());
1259 captioninput.set('value', '');
1262 if (width && table.getStyle('width').indexOf('px') === -1) {
1263 width.set('value', parseInt(table.getStyle('width'), 10));
1266 if (captionpositioninput && captionnode && captionnode.getAttribute('style')) {
1267 captionpositioninput.set('value', captionnode.getStyle('caption-side'));
1270 captionpositioninput.set('value', '');
1273 if (table.getStyle('backgroundColor') && this.get('allowBackgroundColour')) {
1274 hexColour = Y.Color.toHex(table.getStyle('backgroundColor'));
1275 matchedInput = backgroundcolours.filter('[value="' + hexColour + '"]');
1278 matchedInput.set("checked", true);
1282 if (this.get('allowBorders')) {
1283 var borderValue = 'default',
1284 borderConfiguration = this._getBorderConfiguration(table);
1286 if (borderConfiguration) {
1287 borderValue = 'outer';
1289 borderConfiguration = this._getBorderConfiguration(table.one('td'));
1290 if (borderConfiguration) {
1291 borderValue = 'all';
1295 if (borderConfiguration) {
1296 var borderStyle = borderConfiguration.borderStyle || DEFAULT.BORDERSTYLE;
1297 var borderSize = borderConfiguration.borderWidth || DEFAULT.BORDERWIDTH;
1298 borderstyle.set('value', borderStyle);
1299 bordersize.set('value', borderSize);
1300 borderinput.set('value', borderValue);
1302 hexColour = borderConfiguration.borderColor;
1303 matchedInput = bordercolours.filter('[value="' + hexColour + '"]');
1306 matchedInput.set("checked", true);
1311 var headersvalue = 'columns';
1312 if (table.one('th[scope="row"]')) {
1313 headersvalue = 'rows';
1314 if (table.one('th[scope="col"]')) {
1315 headersvalue = 'both';
1318 headersinput.set('value', headersvalue);
1319 dialogue.set('bodyContent', node).show();
1320 this._updateAvailableSettings();
1325 * Delete the current column.
1327 * @method _deleteColumn
1330 _deleteColumn: function() {
1331 var columnindex = this._getColumnIndex(this._lastTarget),
1332 table = this._lastTarget.ancestor('table'),
1333 rows = table.all('tr'),
1334 columncells = new Y.NodeList(),
1337 rows.each(function(row) {
1338 var cells = row.all('td, th');
1339 var cell = cells.item(columnindex);
1340 if (cell.get('tagName') === 'TD') {
1343 columncells.push(cell);
1346 // Do not delete all the headers.
1348 columncells.remove(true);
1356 * Add a row after the current row.
1358 * @method _addRowAfter
1361 _addRowAfter: function() {
1362 var target = this._lastTarget.ancestor('tr'),
1363 tablebody = this._lastTarget.ancestor('table').one('tbody');
1365 // Not all tables have tbody.
1366 tablebody = this._lastTarget.ancestor('table');
1369 var firstrow = tablebody.one('tr');
1371 firstrow = this._lastTarget.ancestor('table').one('tr');
1374 // Table has no rows. Boo.
1377 var newrow = firstrow.cloneNode(true);
1378 newrow.all('th, td').each(function(tablecell) {
1379 if (tablecell.get('tagName') === 'TH') {
1380 if (tablecell.getAttribute('scope') !== 'row') {
1381 var newcell = Y.Node.create('<td></td>');
1382 tablecell.replace(newcell);
1383 tablecell = newcell;
1386 tablecell.setHTML(' ');
1389 if (target.ancestor('thead')) {
1391 tablebody.insert(newrow, target);
1393 target.insert(newrow, 'after');
1401 * Add a column after the current column.
1403 * @method _addColumnAfter
1406 _addColumnAfter: function() {
1407 var cells = this._findColumnCells(),
1409 clonecells = cells.next;
1410 if (cells.next.size() <= 0) {
1412 clonecells = cells.current;
1415 Y.each(clonecells, function(cell) {
1416 var newcell = cell.cloneNode();
1417 // Clear the content of the cell.
1418 newcell.setHTML(' ');
1421 cell.get('parentNode').insert(newcell, cell);
1423 cell.get('parentNode').insert(newcell, cell);
1435 * Whether or not to allow borders
1437 * @attribute allowBorder
1445 * What border styles to allow
1447 * @attribute borderStyles
1460 * Whether or not to allow colourizing the background
1462 * @attribute allowBackgroundColour
1465 allowBackgroundColour: {
1470 * Whether or not to allow setting the table width
1472 * @attribute allowWidth
1480 * Whether we allow styling
1481 * @attribute allowStyling
1486 getter: function() {
1487 return this.get('allowBorders') || this.get('allowBackgroundColour') || this.get('allowWidth');
1493 * @attribute availableColors
1511 }, '@VERSION@', {"requires": ["moodle-editor_atto-plugin", "moodle-editor_atto-menu", "event", "event-valuechange"]});