Merge branch 'MDL-51914' of git://github.com/timhunt/moodle
[moodle.git] / lib / editor / atto / plugins / table / yui / src / button / js / button.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * @package    atto_table
18  * @copyright  2013 Damyon Wiese  <damyon@moodle.com>
19  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
20  */
22 /**
23  * @module moodle-atto_table-button
24  */
26 /**
27  * Atto text editor table plugin.
28  *
29  * @namespace M.atto_table
30  * @class Button
31  * @extends M.editor_atto.EditorPlugin
32  */
34 var COMPONENT = 'atto_table',
35     DEFAULT = {
36         BORDERSTYLE: 'inherit',
37         BORDERWIDTH: '1'
38     },
39     DIALOGUE = {
40         WIDTH: '480px'
41     },
42     TEMPLATE = '' +
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 />' +
46             '<br/>' +
47             '<br/>' +
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>' +
54             '</select>' +
55             '<br/>' +
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>' +
61             '</select>' +
62             '<br/>' +
63             '{{#if nonedit}}' +
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"/>' +
67                 '<br/>' +
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"/>' +
72                 '<br/>' +
73             '{{/if}}' +
74             '{{#if allowStyling}}' +
75                 '<fieldset>' +
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>' +
83                     '</select>' +
84                     '<br>' +
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>' +
92                             '{{/each}}' +
93                         '</select>' +
94                         '<br>' +
95                     '{{/if}}' +
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>' +
102                         '<br>' +
103                     '{{/if}}' +
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}}' +
117                             '</label>' +
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}}">' +
125                                     '{{this}}' +
126                                 '</label>' +
127                             '{{/each}}' +
128                         '</div>' +
129                         '<br>' +
130                     '{{/if}}' +
131                 '{{/if}}' +
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}}' +
145                         '</label>' +
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}}">' +
154                                 '{{this}}' +
155                             '</label>' +
156                         '{{/each}}' +
157                     '</div>' +
158                     '<br>' +
159                 '{{/if}}' +
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>' +
165                     '<br>' +
166                 '{{/if}}' +
167                 '</fieldset>' +
168             '{{/if}}' +
169             '<div class="mdl-align">' +
170             '<br/>' +
171             '{{#if edit}}' +
172                 '<button class="submit" type="submit">{{get_string "updatetable" component}}</button>' +
173             '{{/if}}' +
174             '{{#if nonedit}}' +
175                 '<button class="submit" type="submit">{{get_string "createtable" component}}</button>' +
176             '{{/if}}' +
177             '</div>' +
178         '</form>',
179     CSS = {
180         CAPTION: 'caption',
181         CAPTIONPOSITION: 'captionposition',
182         HEADERS: 'headers',
183         ROWS: 'rows',
184         COLUMNS: 'columns',
185         SUBMIT: 'submit',
186         FORM: 'atto_form',
187         BORDERS: 'borders',
188         BORDERSIZE: 'bordersize',
189         BORDERSIZEUNIT: 'px',
190         BORDERCOLOUR: 'bordercolour',
191         BORDERSTYLE: 'borderstyle',
192         BACKGROUNDCOLOUR: 'backgroundcolour',
193         WIDTH: 'customwidth',
194         WIDTHUNIT: '%',
195         AVAILABLECOLORS: 'availablecolors',
196         COLOURROW: 'colourrow'
197     },
198     SELECTORS = {
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',
212         FORM: '.atto_form',
213         WIDTH: '.' + CSS.WIDTH,
214         AVAILABLECOLORS: '.' + CSS.AVAILABLECOLORS
215     };
217 Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
219     /**
220      * A reference to the current selection at the time that the dialogue
221      * was opened.
222      *
223      * @property _currentSelection
224      * @type Range
225      * @private
226      */
227     _currentSelection: null,
229     /**
230      * The contextual menu that we can open.
231      *
232      * @property _contextMenu
233      * @type M.editor_atto.Menu
234      * @private
235      */
236     _contextMenu: null,
238     /**
239      * The last modified target.
240      *
241      * @property _lastTarget
242      * @type Node
243      * @private
244      */
245     _lastTarget: null,
247     /**
248      * The list of menu items.
249      *
250      * @property _menuOptions
251      * @type Object
252      * @private
253      */
254     _menuOptions: null,
256     initializer: function() {
257         this.addButton({
258             icon: 'e/table',
259             callback: this._displayTableEditor,
260             tags: 'table'
261         });
262         // Disable mozilla table controls.
263         if (Y.UA.gecko) {
264             document.execCommand("enableInlineTableEditing", false, false);
265             document.execCommand("enableObjectResizing", false, false);
266         }
267     },
269     /**
270      * Display the table tool.
271      *
272      * @method _displayDialogue
273      * @private
274      */
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
285             });
287             // Set the dialogue content, and then show the dialogue.
288             dialogue.set('bodyContent', this._getDialogueContent(false))
289                     .show();
291             this._updateAvailableSettings();
292         }
293     },
295     /**
296      * Display the appropriate table editor.
297      *
298      * If the current selection includes a table, then we show the
299      * contextual menu, otherwise show the table creation dialogue.
300      *
301      * @method _displayTableEditor
302      * @param {EventFacade} e
303      * @private
304      */
305     _displayTableEditor: function(e) {
306         var cell = this._getSuitableTableCell();
307         if (cell) {
308             // Add the cell to the EventFacade to save duplication in when showing the menu.
309             e.tableCell = cell;
310             return this._showTableMenu(e);
311         }
312         return this._displayDialogue(e);
313     },
315     /**
316      * Returns whether or not the parameter node exists within the editor.
317      *
318      * @method _stopAtContentEditableFilter
319      * @param  {Node} node
320      * @private
321      * @return {boolean} whether or not the parameter node exists within the editor.
322      */
323     _stopAtContentEditableFilter: function(node) {
324         this.editor.contains(node);
325     },
327     /**
328      * Return the dialogue content for the tool, attaching any required
329      * events.
330      *
331      * @method _getDialogueContent
332      * @private
333      * @return {Node} The content to place in the dialogue.
334      */
335     _getDialogueContent: function(edit) {
336         var template = Y.Handlebars.compile(TEMPLATE);
337         var allowBorders = this.get('allowBorders');
339         this._content = Y.Node.create(template({
340                 CSS: CSS,
341                 elementid: this.get('host').get('elementid'),
342                 component: COMPONENT,
343                 edit: edit,
344                 nonedit: !edit,
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')
354             }));
356         // Handle table setting.
357         if (edit) {
358             this._content.one('.submit').on('click', this._updateTable, this);
359         } else {
360             this._content.one('.submit').on('click', this._setTable, this);
361         }
363         if (allowBorders) {
364             this._content.one('[name="borders"]').on('change', this._updateAvailableSettings, this);
365         }
367         return this._content;
368     },
370     /**
371      * Disables options within the dialogue if they shouldn't be available.
372      * E.g.
373      * If borders are set to "Theme default" then the border size, style and
374      * colour options are disabled.
375      *
376      * @method _updateAvailableSettings
377      * @private
378      */
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';
389         }
391         if (borderStyle) {
392             borderStyle[disabledValue]('disabled');
393         }
395         if (borderSize) {
396             borderSize[disabledValue]('disabled');
397         }
399         if (borderColour) {
400             borderColour[disabledValue]('disabled');
401         }
403     },
405     /**
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.
409      *
410      * @method _getSuitableTableCell
411      * @private
412      * @return {Node} suitable target cell, or null if not within a table
413      */
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)) {
420                 targetcell = node;
422                 var caption = node.ancestor('caption', true, this._stopAtContentEditableFilter);
423                 if (caption) {
424                     var table = caption.get('parentNode');
425                     if (table) {
426                         targetcell = table.one('td, th');
427                     }
428                 }
430                 // Once we've found a cell to target, we shouldn't need to keep looking.
431                 return true;
432             }
433         });
435         if (targetcell) {
436             var selection = host.getSelectionFromNode(targetcell);
437             host.setSelection(selection);
438         }
440         return targetcell;
441     },
443     /**
444      * Change a node from one type to another, copying all attributes and children.
445      *
446      * @method _changeNodeType
447      * @param {Y.Node} node
448      * @param {String} new node type
449      * @private
450      * @chainable
451      */
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());
457         });
458         node.replace(newNode);
459         return newNode;
460     },
462     /**
463      * Handle updating an existing table.
464      *
465      * @method _updateTable
466      * @param {EventFacade} e
467      * @private
468      */
469     _updateTable: function(e) {
470         var caption,
471             captionposition,
472             headers,
473             borders,
474             bordersize,
475             borderstyle,
476             bordercolour,
477             backgroundcolour,
478             table,
479             width,
480             captionnode;
482         e.preventDefault();
483         // Hide the dialogue.
484         this.getDialogue({
485             focusAfterHide: null
486         }).hide();
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, {
501             width: width,
502             borders: borders,
503             borderColour: bordercolour,
504             borderSize: bordersize,
505             borderStyle: borderstyle,
506             backgroundColour: backgroundcolour
507         });
509         captionnode = table.one('caption');
510         if (!captionnode) {
511             captionnode = Y.Node.create('<caption></caption>');
512             table.insert(captionnode, 0);
513         }
514         captionnode.setHTML(caption.get('value'));
515         captionnode.setStyle('caption-side', captionposition.get('value'));
516         if (!captionnode.getAttribute('style')) {
517             captionnode.removeAttribute('style');
518         }
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(),
525                     newCell;
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');
531                 } else {
532                     firstCell.setAttribute('scope', 'row');
533                 }
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');
540                     }
541                 }, this);
543             }, this);
544         }
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(),
549                 newCell;
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');
556                 } else {
557                     cell.setAttribute('scope', 'col');
558                 }
559             }, this);
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.
566                     cells.shift();
567                 }
568                 cells.each(function(cell) {
569                     if (cell.get('tagName') === 'TH') {
570                         newCell = this._changeNodeType(cell, 'td');
571                         newCell.removeAttribute('scope');
572                     }
573                 }, this);
575             }, this);
576         }
577         // Clean the HTML.
578         this.markUpdated();
579     },
581     /**
582      * Handle creation of a new table.
583      *
584      * @method _setTable
585      * @param {EventFacade} e
586      * @private
587      */
588     _setTable: function(e) {
589         var caption,
590             captionposition,
591             borders,
592             bordersize,
593             borderstyle,
594             bordercolour,
595             rows,
596             cols,
597             headers,
598             tablehtml,
599             backgroundcolour,
600             width,
601             i, j;
603         e.preventDefault();
605         // Hide the dialogue.
606         this.getDialogue({
607             focusAfterHide: null
608         }).hide();
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.
626         var nl = "\n";
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') + '"';
633         }
634         tablehtml += '<caption' + captionstyle + '>' + Y.Escape.html(caption.get('value')) + '</caption>' + nl;
635         i = 0;
636         if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
637             i = 1;
638             tablehtml += '<thead>' + nl + '<tr>' + nl;
639             for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
640                 tablehtml += '<th scope="col"></th>' + nl;
641             }
642             tablehtml += '</tr>' + nl + '</thead>' + nl;
643         }
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;
650                 } else {
651                     tablehtml += '<td ></td>' + nl;
652                 }
653             }
654             tablehtml += '</tr>' + nl;
655         }
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, {
663             width: width,
664             borders: borders,
665             borderColour: bordercolour,
666             borderSize: bordersize,
667             borderStyle: borderstyle,
668             backgroundColour: backgroundcolour
669         });
670         tableNode.removeAttribute('id');
672         // Mark the content as updated.
673         this.markUpdated();
674     },
676     /**
677      * Search for all the cells in the current, next and previous columns.
678      *
679      * @method _findColumnCells
680      * @private
681      * @return {Object} containing current, prev and next {Y.NodeList}s
682      */
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);
696             if (cellprev) {
697                 prevcells.push(cellprev);
698             }
699             if (cellnext) {
700                 nextcells.push(cellnext);
701             }
702         });
704         return {
705             current: currentcells,
706             prev: prevcells,
707             next: nextcells
708         };
709     },
711     /**
712      * Hide the entries in the context menu that don't make sense with the
713      * current selection.
714      *
715      * @method _hideInvalidEntries
716      * @param {Y.Node} node - The node containing the menu.
717      * @private
718      */
719     _hideInvalidEntries: function(node) {
720         // Moving rows.
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();
730         } else {
731             node.one('[data-change="moverowup"]').show();
732         }
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();
739         } else {
740             node.one('[data-change="moverowdown"]').show();
741         }
743         // Moving columns.
744         var cells = this._findColumnCells();
745         if (cells.prev.filter('td').size() > 0) {
746             node.one('[data-change="movecolumnleft"]').show();
747         } else {
748             node.one('[data-change="movecolumnleft"]').hide();
749         }
751         var colhascell = cells.current.filter('td').size() > 0;
752         if ((cells.next.size() > 0) && colhascell) {
753             node.one('[data-change="movecolumnright"]').show();
754         } else {
755             node.one('[data-change="movecolumnright"]').hide();
756         }
758         // Delete col
759         if (cells.current.filter('td').size() > 0) {
760             node.one('[data-change="deletecolumn"]').show();
761         } else {
762             node.one('[data-change="deletecolumn"]').hide();
763         }
764         // Delete row
765         if (!row || !row.one('td')) {
766             node.one('[data-change="deleterow"]').hide();
767         } else {
768             node.one('[data-change="deleterow"]').show();
769         }
770     },
772     /**
773      * Display the table menu.
774      *
775      * @method _showTableMenu
776      * @param {EventFacade} e
777      * @private
778      */
779     _showTableMenu: function(e) {
780         e.preventDefault();
782         var boundingBox;
784         if (!this._contextMenu) {
785             this._menuOptions = [
786                 {
787                     text: M.util.get_string("addcolumnafter", COMPONENT),
788                     data: {
789                         change: "addcolumnafter"
790                     }
791                 }, {
792                     text: M.util.get_string("addrowafter", COMPONENT),
793                     data: {
794                         change: "addrowafter"
795                     }
796                 }, {
797                     text: M.util.get_string("moverowup", COMPONENT),
798                     data: {
799                         change: "moverowup"
800                     }
801                 }, {
802                     text: M.util.get_string("moverowdown", COMPONENT),
803                     data: {
804                         change: "moverowdown"
805                     }
806                 }, {
807                     text: M.util.get_string("movecolumnleft", COMPONENT),
808                     data: {
809                         change: "movecolumnleft"
810                     }
811                 }, {
812                     text: M.util.get_string("movecolumnright", COMPONENT),
813                     data: {
814                         change: "movecolumnright"
815                     }
816                 }, {
817                     text: M.util.get_string("deleterow", COMPONENT),
818                     data: {
819                         change: "deleterow"
820                     }
821                 }, {
822                     text: M.util.get_string("deletecolumn", COMPONENT),
823                     data: {
824                         change: "deletecolumn"
825                     }
826                 }, {
827                     text: M.util.get_string("edittable", COMPONENT),
828                     data: {
829                         change: "edittable"
830                     }
831                 }
832             ];
834             this._contextMenu = new Y.M.editor_atto.Menu({
835                 items: this._menuOptions
836             });
838             // Add event handlers for table control menus.
839             boundingBox = this._contextMenu.get('boundingBox');
840             boundingBox.delegate('click', this._handleTableChange, 'a', this);
841         }
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);
853         });
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();
867         }
869         // Add this menu to the list of open menus.
870         this.get('host').openMenus = [this._contextMenu];
871     },
873     /**
874      * Handle a selection from the table control menu.
875      *
876      * @method _handleTableChange
877      * @param {EventFacade} e
878      * @private
879      */
880     _handleTableChange: function(e) {
881         e.preventDefault();
883         this._contextMenu.set('focusAfterHide', this.get('host').editor);
884         // Hide the context menu.
885         this._contextMenu.hide(e);
887         // Make our changes.
888         switch (e.target.getData('change')) {
889             case 'addcolumnafter':
890                 this._addColumnAfter();
891                 break;
892             case 'addrowafter':
893                 this._addRowAfter();
894                 break;
895             case 'deleterow':
896                 this._deleteRow();
897                 break;
898             case 'deletecolumn':
899                 this._deleteColumn();
900                 break;
901             case 'edittable':
902                 this._editTable();
903                 break;
904             case 'moverowdown':
905                 this._moveRowDown();
906                 break;
907             case 'moverowup':
908                 this._moveRowUp();
909                 break;
910             case 'movecolumnleft':
911                 this._moveColumnLeft();
912                 break;
913             case 'movecolumnright':
914                 this._moveColumnRight();
915                 break;
916         }
917     },
919     /**
920      * Determine the index of a row in a table column.
921      *
922      * @method _getRowIndex
923      * @param {Node} cell
924      * @private
925      */
926     _getRowIndex: function(cell) {
927         var tablenode = cell.ancestor('table'),
928             rownode = cell.ancestor('tr');
930         if (!tablenode || !rownode) {
931             return;
932         }
934         var rows = tablenode.all('tr');
936         return rows.indexOf(rownode);
937     },
939     /**
940      * Determine the index of a column in a table row.
941      *
942      * @method _getColumnIndex
943      * @param {Node} cellnode
944      * @private
945      */
946     _getColumnIndex: function(cellnode) {
947         var rownode = cellnode.ancestor('tr');
949         if (!rownode) {
950             return;
951         }
953         var cells = rownode.all('td, th');
955         return cells.indexOf(cellnode);
956     },
958     /**
959      * Delete the current row.
960      *
961      * @method _deleteRow
962      * @private
963      */
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.
969             row.remove(true);
970         }
972         // Clean the HTML.
973         this.markUpdated();
974     },
976     /**
977      * Move row up
978      *
979      * @method _moveRowUp
980      * @private
981      */
982     _moveRowUp: function() {
983         var row = this._lastTarget.ancestor('tr'),
984             prevrow = row.previous('tr');
985         if (!row || !prevrow) {
986             return;
987         }
989         row.swap(prevrow);
990         // Clean the HTML.
991         this.markUpdated();
992     },
994     /**
995      * Move column left
996      *
997      * @method _moveColumnLeft
998      * @private
999      */
1000     _moveColumnLeft: function() {
1001         var cells = this._findColumnCells();
1003         if (cells.current.size() > 0 && cells.prev.size() > 0 && cells.current.size() === cells.prev.size()) {
1004             var i = 0;
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);
1010             }
1011         }
1012         // Cleanup.
1013         this.markUpdated();
1014     },
1016     /**
1017      * Add a caption to the table if it doesn't have one.
1018      *
1019      * @method _addCaption
1020      * @private
1021      */
1022     _addCaption: function() {
1023         var table = this._lastTarget.ancestor('table'),
1024             caption = table.one('caption');
1026         if (!caption) {
1027             table.insert(Y.Node.create('<caption>&nbsp;</caption>'), 1);
1028         }
1029     },
1031     /**
1032      * Remove a caption from the table if has one.
1033      *
1034      * @method _removeCaption
1035      * @private
1036      */
1037     _removeCaption: function() {
1038         var table = this._lastTarget.ancestor('table'),
1039             caption = table.one('caption');
1041         if (caption) {
1042             caption.remove(true);
1043         }
1044     },
1046     /**
1047      * Move column right.
1048      *
1049      * @method _moveColumnRight
1050      * @private
1051      */
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)) {
1059             var i = 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);
1065             }
1066         }
1067         // Cleanup.
1068         this.markUpdated();
1069     },
1071     /**
1072      * Move row down.
1073      *
1074      * @method _moveRowDown
1075      * @private
1076      */
1077     _moveRowDown: function() {
1078         var row = this._lastTarget.ancestor('tr'),
1079             nextrow = row.next('tr');
1080         if (!row || !nextrow || !row.one('td')) {
1081             return;
1082         }
1084         row.swap(nextrow);
1085         // Clean the HTML.
1086         this.markUpdated();
1087     },
1089     /**
1090      * Obtain values for the table borders
1091      *
1092      * @method _getBorderConfiguration
1093      * @param {Node} node
1094      * @private
1095      * @return {Array} or {Boolean} Returns the settings, if presents, or else returns false
1096      */
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);
1108             return {
1109                 borderStyle: borderStyle,
1110                 borderColor: hexColour === "#" ? null : hexColour,
1111                 borderWidth: isNaN(width) ? null : width
1112             };
1113         }
1115         return false;
1116     },
1118     /**
1119      * Set the appropriate styles on the given table node according to
1120      * the provided configuration.
1121      *
1122      * @method _setAppearance
1123      * @param {Node} The table node to be modified.
1124      * @param {Object} Configuration object (associative array) containing the form nodes for
1125      *                 border styling.
1126      * @private
1127      */
1128     _setAppearance: function(tableNode, configuration) {
1129         var borderhex,
1130             borderSizeValue,
1131             borderStyleValue,
1132             backgroundcolourvalue;
1134         if (configuration.borderColour) {
1135             borderhex = configuration.borderColour.get('value');
1136         }
1138         if (configuration.borderSize) {
1139             borderSizeValue = configuration.borderSize.get('value');
1140         }
1142         if (configuration.borderStyle) {
1143             borderStyleValue = configuration.borderStyle.get('value');
1144         }
1146         if (configuration.backgroundColour) {
1147             backgroundcolourvalue = configuration.backgroundColour.get('value');
1148         }
1150         // Clear the inline border styling
1151         tableNode.removeAttribute('style');
1152         tableNode.all('td, th').each(function(cell) {
1153             cell.removeAttribute('style');
1154         }, this);
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);
1163                 }
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);
1171                     }
1172                 }, this);
1173             }
1174         }
1176         if (backgroundcolourvalue !== 'none') {
1177             tableNode.setStyle('backgroundColor', backgroundcolourvalue);
1178         }
1180         if (configuration.width && configuration.width.get('value')) {
1181             tableNode.setStyle('width', configuration.width.get('value') + CSS.WIDTHUNIT);
1182         }
1183     },
1185     /**
1186      * Edit table (show the dialogue).
1187      *
1188      * @method _editTable
1189      * @private
1190      */
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
1197         });
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'),
1212             hexColour,
1213             matchedInput;
1215         if (captionnode) {
1216             captioninput.set('value', captionnode.getHTML());
1217         } else {
1218             captioninput.set('value', '');
1219         }
1221         if (width && table.getStyle('width').indexOf('px') === -1) {
1222             width.set('value', parseInt(table.getStyle('width'), 10));
1223         }
1225         if (captionpositioninput && captionnode && captionnode.getAttribute('style')) {
1226             captionpositioninput.set('value', captionnode.getStyle('caption-side'));
1227         } else {
1228             // Default to none.
1229             captionpositioninput.set('value', '');
1230         }
1232         if (table.getStyle('backgroundColor') && this.get('allowBackgroundColour')) {
1233             hexColour = Y.Color.toHex(table.getStyle('backgroundColor'));
1234             matchedInput = backgroundcolours.filter('[value="' + hexColour + '"]');
1236             if (matchedInput) {
1237                 matchedInput.set("checked", true);
1238             }
1239         }
1241         if (this.get('allowBorders')) {
1242             var borderValue = 'default',
1243                 borderConfiguration = this._getBorderConfiguration(table);
1245             if (borderConfiguration) {
1246                 borderValue = 'outer';
1247             } else {
1248                 borderConfiguration = this._getBorderConfiguration(table.one('td'));
1249                 if (borderConfiguration) {
1250                      borderValue = 'all';
1251                 }
1252             }
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 + '"]');
1264                 if (matchedInput) {
1265                     matchedInput.set("checked", true);
1266                 }
1267             }
1268         }
1270         var headersvalue = 'columns';
1271         if (table.one('th[scope="row"]')) {
1272             headersvalue = 'rows';
1273             if (table.one('th[scope="col"]')) {
1274                 headersvalue = 'both';
1275             }
1276         }
1277         headersinput.set('value', headersvalue);
1278         dialogue.set('bodyContent', node).show();
1279         this._updateAvailableSettings();
1280     },
1283     /**
1284      * Delete the current column.
1285      *
1286      * @method _deleteColumn
1287      * @private
1288      */
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(),
1294             hastd = false;
1296         rows.each(function(row) {
1297             var cells = row.all('td, th');
1298             var cell = cells.item(columnindex);
1299             if (cell.get('tagName') === 'TD') {
1300                 hastd = true;
1301             }
1302             columncells.push(cell);
1303         });
1305         // Do not delete all the headers.
1306         if (hastd) {
1307             columncells.remove(true);
1308         }
1310         // Clean the HTML.
1311         this.markUpdated();
1312     },
1314     /**
1315      * Add a row after the current row.
1316      *
1317      * @method _addRowAfter
1318      * @private
1319      */
1320     _addRowAfter: function() {
1321         var target = this._lastTarget.ancestor('tr'),
1322             tablebody = this._lastTarget.ancestor('table').one('tbody');
1323         if (!tablebody) {
1324             // Not all tables have tbody.
1325             tablebody = this._lastTarget.ancestor('table');
1326         }
1328         var firstrow = tablebody.one('tr');
1329         if (!firstrow) {
1330             firstrow = this._lastTarget.ancestor('table').one('tr');
1331         }
1332         if (!firstrow) {
1333             // Table has no rows. Boo.
1334             return;
1335         }
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;
1343                 }
1344             }
1345             tablecell.setHTML('&nbsp;');
1346         });
1348         if (target.ancestor('thead')) {
1349             target = firstrow;
1350             tablebody.insert(newrow, target);
1351         } else {
1352             target.insert(newrow, 'after');
1353         }
1355         // Clean the HTML.
1356         this.markUpdated();
1357     },
1359     /**
1360      * Add a column after the current column.
1361      *
1362      * @method _addColumnAfter
1363      * @private
1364      */
1365     _addColumnAfter: function() {
1366         var cells = this._findColumnCells(),
1367             before = true,
1368             clonecells = cells.next;
1369         if (cells.next.size() <= 0) {
1370             before = false;
1371             clonecells = cells.current;
1372         }
1374         Y.each(clonecells, function(cell) {
1375             var newcell = cell.cloneNode();
1376             // Clear the content of the cell.
1377             newcell.setHTML('&nbsp;');
1379             if (before) {
1380                 cell.get('parentNode').insert(newcell, cell);
1381             } else {
1382                 cell.get('parentNode').insert(newcell, cell);
1383                 cell.swap(newcell);
1384             }
1385         }, this);
1387         // Clean the HTML.
1388         this.markUpdated();
1389     }
1391 }, {
1392     ATTRS: {
1393         /**
1394          * Whether or not to allow borders
1395          *
1396          * @attribute allowBorder
1397          * @type Boolean
1398          */
1399         allowBorders: {
1400             value: true
1401         },
1403         /**
1404          * Whether or not to allow style of borders
1405          *
1406          * @attribute allowBorderStyle
1407          * @type Boolean
1408          */
1409         allowBorderStyles: {
1410             value: true
1411         },
1413         /**
1414          * What border styles to allow
1415          *
1416          * @attribute borderStyles
1417          * @type Array
1418          */
1419         borderStyles: {
1420             value: [
1421                 'solid',
1422                 'dashed',
1423                 'dotted'
1424             ],
1425             setter: function(value) {
1426                 if (value) {
1427                     return value.replace(/ /g,'').split(',');
1428                 } else {
1429                     // Not a valid value - revert to default value.
1430                     return Y.Attribute.INVALID_VALUE;
1431                 }
1432             }
1433         },
1435         /**
1436          * Whether or not to allow border size
1437          *
1438          * @attribute allowBorderSize
1439          * @type Boolean
1440          */
1441         allowBorderSize: {
1442             value: true
1443         },
1445         /**
1446          * Whether or not to allow colourizing borders
1447          *
1448          * @attribute allowBorderColours
1449          * @type Boolean
1450          */
1451         allowBorderColour: {
1452             value: true
1453         },
1455         /**
1456          * Whether or not to allow colourizing the background
1457          *
1458          * @attribute allowBackgroundColour
1459          * @type Boolean
1460          */
1461         allowBackgroundColour: {
1462             value: true
1463         },
1465         /**
1466          * Whether or not to allow setting the table width
1467          *
1468          * @attribute allowWidth
1469          * @type Boolean
1470          */
1471         allowWidth: {
1472             value: true
1473         },
1475         /**
1476          * Whether we allow styling
1477          * @attribute allowStyling
1478          * @type Boolean
1479          */
1480         allowStyling: {
1481             readOnly: true,
1482             getter: function() {
1483                 return this.get('allowBorders') || this.get('allowBackgroundColour') || this.get('allowWidth');
1484             }
1485         },
1487         /**
1488          * Available colors
1489          * @attribute availableColors
1490          * @type Array
1491          */
1492         availableColors: {
1493             value: [
1494                 '#FFFFFF',
1495                 '#EF4540',
1496                 '#FFCF35',
1497                 '#98CA3E',
1498                 '#7D9FD3',
1499                 '#333333'
1500             ],
1501             readOnly: true
1502         }
1503     }
1504 });