79ca755393da81789b9d612a4d0c05659ec107c8
[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: 'none',
37         BORDERWIDTH: '1'
38     },
39     DIALOGUE = {
40         WIDTH: '480px'
41     },
42     TEMPLATE = '' +
43         '<form class="{{CSS.FORM}}">' +
44             '<div class="mb-1 form-group row-fluid">' +
45             '<div class="col-sm-4 span4">' +
46             '<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' +
47             '</div><div class="col-sm-8 span8">' +
48             '<input type="text" class="form-control {{CSS.CAPTION}}" id="{{elementid}}_atto_table_caption" required />' +
49             '</div>' +
50             '</div>' +
51             '<div class="mb-1 form-group row-fluid">' +
52             '<div class="col-sm-4 span4">' +
53             '<label for="{{elementid}}_atto_table_captionposition">' +
54             '{{get_string "captionposition" component}}</label>' +
55             '</div><div class="col-sm-8 span8">' +
56             '<select class="custom-select {{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +
57                 '<option value=""></option>' +
58                 '<option value="top">{{get_string "top" "editor"}}</option>' +
59                 '<option value="bottom">{{get_string "bottom" "editor"}}</option>' +
60             '</select>' +
61             '</div>' +
62             '</div>' +
63             '<div class="mb-1 form-group row-fluid">' +
64             '<div class="col-sm-4 span4">' +
65             '<label for="{{elementid}}_atto_table_headers">{{get_string "headers" component}}</label>' +
66             '</div><div class="col-sm-8 span8">' +
67             '<select class="custom-select {{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
68                 '<option value="columns">{{get_string "columns" component}}' + '</option>' +
69                 '<option value="rows">{{get_string "rows" component}}' + '</option>' +
70                 '<option value="both">{{get_string "both" component}}' + '</option>' +
71             '</select>' +
72             '</div>' +
73             '</div>' +
74             '{{#if nonedit}}' +
75                 '<div class="mb-1 form-group row-fluid">' +
76                 '<div class="col-sm-4 span4">' +
77                 '<label for="{{elementid}}_atto_table_rows">{{get_string "numberofrows" component}}</label>' +
78                 '</div><div class="col-sm-8 span8">' +
79                 '<input class="form-control w-auto {{CSS.ROWS}}" type="number" value="3" ' +
80                 'id="{{elementid}}_atto_table_rows" size="8" min="1" max="50"/>' +
81                 '</div>' +
82                 '</div>' +
83                 '<div class="mb-1 form-group row-fluid">' +
84                 '<div class="col-sm-4 span4">' +
85                 '<label for="{{elementid}}_atto_table_columns" ' +
86                 '>{{get_string "numberofcolumns" component}}</label>' +
87                 '</div><div class="col-sm-8 span8">' +
88                 '<input class="form-control w-auto {{CSS.COLUMNS}}" type="number" value="3" ' +
89                     'id="{{elementid}}_atto_table_columns"' +
90                 'size="8" min="1" max="20"/>' +
91                 '</div>' +
92                 '</div>' +
93             '{{/if}}' +
94             '{{#if allowStyling}}' +
95                 '<fieldset>' +
96                 '<legend class="mdl-align">{{get_string "appearance" component}}</legend>' +
97                 '{{#if allowBorders}}' +
98                     '<div class="mb-1 form-group row-fluid">' +
99                     '<div class="col-sm-4 span4">' +
100                     '<label for="{{elementid}}_atto_table_borders">{{get_string "borders" component}}</label>' +
101                     '</div><div class="col-sm-8 span8">' +
102                     '<select name="borders" class="custom-select {{CSS.BORDERS}}" id="{{elementid}}_atto_table_borders">' +
103                         '<option value="default">{{get_string "themedefault" component}}' + '</option>' +
104                         '<option value="outer">{{get_string "outer" component}}' + '</option>' +
105                         '<option value="all">{{get_string "all" component}}' + '</option>' +
106                     '</select>' +
107                     '</div>' +
108                     '</div>' +
109                     '<div class="mb-1 form-group row-fluid">' +
110                     '<div class="col-sm-4 span4">' +
111                     '<label for="{{elementid}}_atto_table_borderstyle">' +
112                     '{{get_string "borderstyles" component}}</label>' +
113                     '</div><div class="col-sm-8 span8">' +
114                     '<select name="borderstyles" class="custom-select {{CSS.BORDERSTYLE}}" ' +
115                         'id="{{elementid}}_atto_table_borderstyle">' +
116                         '{{#each borderStyles}}' +
117                             '<option value="' + '{{this}}' + '">' + '{{get_string this ../component}}' + '</option>' +
118                         '{{/each}}' +
119                     '</select>' +
120                     '</div>' +
121                     '</div>' +
122                     '<div class="mb-1 form-group row-fluid">' +
123                     '<div class="col-sm-4 span4">' +
124                     '<label for="{{elementid}}_atto_table_bordersize">' +
125                     '{{get_string "bordersize" component}}</label>' +
126                     '</div><div class="col-sm-8 span8">' +
127                     '<div class="form-inline">' +
128                     '<input name="bordersize" id="{{elementid}}_atto_table_bordersize" ' +
129                     'class="form-control w-auto mr-1 {{CSS.BORDERSIZE}}"' +
130                     'type="number" value="1" size="8" min="1" max="50"/>' +
131                     '<label>{{CSS.BORDERSIZEUNIT}}</label>' +
132                     '</div>' +
133                     '</div>' +
134                     '</div>' +
135                     '<div class="mb-1 form-group row-fluid">' +
136                     '<div class="col-sm-4 span4">' +
137                     '<label for="{{elementid}}_atto_table_bordercolour">' +
138                     '{{get_string "bordercolour" component}}</label>' +
139                     '</div><div class="col-sm-8 span8">' +
140                     '<div id="{{elementid}}_atto_table_bordercolour"' +
141                     'class="form-inline {{CSS.BORDERCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
142                         '<div class="tablebordercolor" style="background-color:transparent;color:transparent">' +
143                             '<input id="{{../elementid}}_atto_table_bordercolour_-1"' +
144                             'type="radio" class="m-0" name="borderColour" value="none" checked="checked"' +
145                             'title="{{get_string "themedefault" component}}"></input>' +
146                             '<label for="{{../elementid}}_atto_table_bordercolour_-1" class="accesshide">' +
147                             '{{get_string "themedefault" component}}</label>' +
148                         '</div>' +
149                         '{{#each availableColours}}' +
150                             '<div class="tablebordercolor" style="background-color:{{this}};color:{{this}}">' +
151                                 '<input id="{{../elementid}}_atto_table_bordercolour_{{@index}}"' +
152                                 'type="radio" class="m-0" name="borderColour" value="' + '{{this}}' + '" title="{{this}}">' +
153                                 '<label for="{{../elementid}}_atto_table_bordercolour_{{@index}}" class="accesshide">' +
154                                 '{{this}}</label>' +
155                             '</div>' +
156                         '{{/each}}' +
157                     '</div>' +
158                     '</div>' +
159                     '</div>' +
160                 '{{/if}}' +
161                 '{{#if allowBackgroundColour}}' +
162                     '<div class="mb-1 form-group row-fluid">' +
163                     '<div class="col-sm-4 span4">' +
164                     '<label for="{{elementid}}_atto_table_backgroundcolour">' +
165                     '{{get_string "backgroundcolour" component}}</label>' +
166                     '</div><div class="col-sm-8 span8">' +
167                     '<div id="{{elementid}}_atto_table_backgroundcolour"' +
168                     'class="form-inline {{CSS.BACKGROUNDCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
169                         '<div class="tablebackgroundcolor" style="background-color:transparent;color:transparent">' +
170                             '<input id="{{../elementid}}_atto_table_backgroundcolour_-1"' +
171                             'type="radio" class="m-0" name="backgroundColour" value="none" checked="checked"' +
172                             'title="{{get_string "themedefault" component}}"></input>' +
173                             '<label for="{{../elementid}}_atto_table_backgroundcolour_-1" class="accesshide">' +
174                             '{{get_string "themedefault" component}}</label>' +
175                         '</div>' +
177                         '{{#each availableColours}}' +
178                             '<div class="tablebackgroundcolor" style="background-color:{{this}};color:{{this}}">' +
179                                 '<input id="{{../elementid}}_atto_table_backgroundcolour_{{@index}}"' +
180                                 'type="radio" class="m-0" name="backgroundColour" value="' + '{{this}}' + '" title="{{this}}">' +
181                                 '<label for="{{../elementid}}_atto_table_backgroundcolour_{{@index}}" class="accesshide">' +
182                                 '{{this}}</label>' +
183                             '</div>' +
184                         '{{/each}}' +
185                     '</div>' +
186                     '</div>' +
187                     '</div>' +
188                 '{{/if}}' +
189                 '{{#if allowWidth}}' +
190                     '<div class="mb-1 form-group row-fluid">' +
191                     '<div class="col-sm-4 span4">' +
192                     '<label for="{{elementid}}_atto_table_width">' +
193                     '{{get_string "width" component}}</label>' +
194                     '</div><div class="col-sm-8 span8">' +
195                     '<div class="form-inline">' +
196                     '<input name="width" id="{{elementid}}_atto_table_width" ' +
197                         'class="form-control w-auto mr-1 {{CSS.WIDTH}}" size="8" ' +
198                         'type="number" min="0" max="100"/>' +
199                     '<label>{{CSS.WIDTHUNIT}}</label>' +
200                     '</div>' +
201                     '</div>' +
202                     '</div>' +
203                 '{{/if}}' +
204                 '</fieldset>' +
205             '{{/if}}' +
206             '<div class="mdl-align">' +
207             '<br/>' +
208             '{{#if edit}}' +
209                 '<button class="btn btn-secondary submit" type="submit">{{get_string "updatetable" component}}</button>' +
210             '{{/if}}' +
211             '{{#if nonedit}}' +
212                 '<button class="btn btn-secondary submit" type="submit">{{get_string "createtable" component}}</button>' +
213             '{{/if}}' +
214             '</div>' +
215         '</form>',
216     CSS = {
217         CAPTION: 'caption',
218         CAPTIONPOSITION: 'captionposition',
219         HEADERS: 'headers',
220         ROWS: 'rows',
221         COLUMNS: 'columns',
222         SUBMIT: 'submit',
223         FORM: 'atto_form',
224         BORDERS: 'borders',
225         BORDERSIZE: 'bordersize',
226         BORDERSIZEUNIT: 'px',
227         BORDERCOLOUR: 'bordercolour',
228         BORDERSTYLE: 'borderstyle',
229         BACKGROUNDCOLOUR: 'backgroundcolour',
230         WIDTH: 'customwidth',
231         WIDTHUNIT: '%',
232         AVAILABLECOLORS: 'availablecolors',
233         COLOURROW: 'colourrow'
234     },
235     SELECTORS = {
236         CAPTION: '.' + CSS.CAPTION,
237         CAPTIONPOSITION: '.' + CSS.CAPTIONPOSITION,
238         HEADERS: '.' + CSS.HEADERS,
239         ROWS: '.' + CSS.ROWS,
240         COLUMNS: '.' + CSS.COLUMNS,
241         SUBMIT: '.' + CSS.SUBMIT,
242         BORDERS: '.' + CSS.BORDERS,
243         BORDERSIZE: '.' + CSS.BORDERSIZE,
244         BORDERCOLOURS: '.' + CSS.BORDERCOLOUR + ' input[name="borderColour"]',
245         SELECTEDBORDERCOLOUR: '.' + CSS.BORDERCOLOUR + ' input[name="borderColour"]:checked',
246         BORDERSTYLE: '.' + CSS.BORDERSTYLE,
247         BACKGROUNDCOLOURS: '.' + CSS.BACKGROUNDCOLOUR + ' input[name="backgroundColour"]',
248         SELECTEDBACKGROUNDCOLOUR: '.' + CSS.BACKGROUNDCOLOUR + ' input[name="backgroundColour"]:checked',
249         FORM: '.atto_form',
250         WIDTH: '.' + CSS.WIDTH,
251         AVAILABLECOLORS: '.' + CSS.AVAILABLECOLORS
252     };
254 Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
256     /**
257      * A reference to the current selection at the time that the dialogue
258      * was opened.
259      *
260      * @property _currentSelection
261      * @type Range
262      * @private
263      */
264     _currentSelection: null,
266     /**
267      * The contextual menu that we can open.
268      *
269      * @property _contextMenu
270      * @type M.editor_atto.Menu
271      * @private
272      */
273     _contextMenu: null,
275     /**
276      * The last modified target.
277      *
278      * @property _lastTarget
279      * @type Node
280      * @private
281      */
282     _lastTarget: null,
284     /**
285      * The list of menu items.
286      *
287      * @property _menuOptions
288      * @type Object
289      * @private
290      */
291     _menuOptions: null,
293     initializer: function() {
294         this.addButton({
295             icon: 'e/table',
296             callback: this._displayTableEditor,
297             tags: 'table'
298         });
299         // Disable mozilla table controls.
300         if (Y.UA.gecko) {
301             document.execCommand("enableInlineTableEditing", false, false);
302             document.execCommand("enableObjectResizing", false, false);
303         }
304     },
306     /**
307      * Display the table tool.
308      *
309      * @method _displayDialogue
310      * @private
311      */
312     _displayDialogue: function() {
313         // Store the current cursor position.
314         this._currentSelection = this.get('host').getSelection();
316         if (this._currentSelection !== false && (!this._currentSelection.collapsed)) {
317             var dialogue = this.getDialogue({
318                 headerContent: M.util.get_string('createtable', COMPONENT),
319                 focusAfterHide: true,
320                 focusOnShowSelector: SELECTORS.CAPTION,
321                 width: DIALOGUE.WIDTH
322             });
324             // Set the dialogue content, and then show the dialogue.
325             dialogue.set('bodyContent', this._getDialogueContent(false))
326                     .show();
328             this._updateAvailableSettings();
329         }
330     },
332     /**
333      * Display the appropriate table editor.
334      *
335      * If the current selection includes a table, then we show the
336      * contextual menu, otherwise show the table creation dialogue.
337      *
338      * @method _displayTableEditor
339      * @param {EventFacade} e
340      * @private
341      */
342     _displayTableEditor: function(e) {
343         var cell = this._getSuitableTableCell();
344         if (cell) {
345             // Add the cell to the EventFacade to save duplication in when showing the menu.
346             e.tableCell = cell;
347             return this._showTableMenu(e);
348         }
349         return this._displayDialogue(e);
350     },
352     /**
353      * Returns whether or not the parameter node exists within the editor.
354      *
355      * @method _stopAtContentEditableFilter
356      * @param  {Node} node
357      * @private
358      * @return {boolean} whether or not the parameter node exists within the editor.
359      */
360     _stopAtContentEditableFilter: function(node) {
361         return this.editor.contains(node);
362     },
364     /**
365      * Return the dialogue content for the tool, attaching any required
366      * events.
367      *
368      * @method _getDialogueContent
369      * @private
370      * @return {Node} The content to place in the dialogue.
371      */
372     _getDialogueContent: function(edit) {
373         var template = Y.Handlebars.compile(TEMPLATE);
374         var allowBorders = this.get('allowBorders');
376         this._content = Y.Node.create(template({
377                 CSS: CSS,
378                 elementid: this.get('host').get('elementid'),
379                 component: COMPONENT,
380                 edit: edit,
381                 nonedit: !edit,
382                 allowStyling: this.get('allowStyling'),
383                 allowBorders: allowBorders,
384                 borderStyles: this.get('borderStyles'),
385                 allowBackgroundColour: this.get('allowBackgroundColour'),
386                 availableColours: this.get('availableColors'),
387                 allowWidth: this.get('allowWidth')
388             }));
390         // Handle table setting.
391         if (edit) {
392             this._content.one('.submit').on('click', this._updateTable, this);
393         } else {
394             this._content.one('.submit').on('click', this._setTable, this);
395         }
397         if (allowBorders) {
398             this._content.one('[name="borders"]').on('change', this._updateAvailableSettings, this);
399         }
401         return this._content;
402     },
404     /**
405      * Disables options within the dialogue if they shouldn't be available.
406      * E.g.
407      * If borders are set to "Theme default" then the border size, style and
408      * colour options are disabled.
409      *
410      * @method _updateAvailableSettings
411      * @private
412      */
413     _updateAvailableSettings: function() {
414         var tableForm = this._content,
415             enableBorders = tableForm.one('[name="borders"]'),
416             borderStyle = tableForm.one('[name="borderstyles"]'),
417             borderSize = tableForm.one('[name="bordersize"]'),
418             borderColour = tableForm.all('[name="borderColour"]'),
419             disabledValue = 'removeAttribute';
421         if (!enableBorders) {
422             return;
423         }
425         if (enableBorders.get('value') === 'default') {
426             disabledValue = 'setAttribute';
427         }
429         if (borderStyle) {
430             borderStyle[disabledValue]('disabled');
431         }
433         if (borderSize) {
434             borderSize[disabledValue]('disabled');
435         }
437         if (borderColour) {
438             borderColour[disabledValue]('disabled');
439         }
441     },
443     /**
444      * Given the current selection, return a table cell suitable for table editing
445      * purposes, i.e. the first table cell selected, or the first cell in the table
446      * that the selection exists in, or null if not within a table.
447      *
448      * @method _getSuitableTableCell
449      * @private
450      * @return {Node} suitable target cell, or null if not within a table
451      */
452     _getSuitableTableCell: function() {
453         var targetcell = null,
454             host = this.get('host');
455         var stopAtContentEditableFilter = Y.bind(this._stopAtContentEditableFilter, this);
457         host.getSelectedNodes().some(function(node) {
458             if (node.ancestor('td, th, caption', true, stopAtContentEditableFilter)) {
459                 targetcell = node;
461                 var caption = node.ancestor('caption', true, stopAtContentEditableFilter);
462                 if (caption) {
463                     var table = caption.get('parentNode');
464                     if (table) {
465                         targetcell = table.one('td, th');
466                     }
467                 }
469                 // Once we've found a cell to target, we shouldn't need to keep looking.
470                 return true;
471             }
472         });
474         if (targetcell) {
475             var selection = host.getSelectionFromNode(targetcell);
476             host.setSelection(selection);
477         }
479         return targetcell;
480     },
482     /**
483      * Change a node from one type to another, copying all attributes and children.
484      *
485      * @method _changeNodeType
486      * @param {Y.Node} node
487      * @param {String} new node type
488      * @private
489      * @chainable
490      */
491     _changeNodeType: function(node, newType) {
492         var newNode = Y.Node.create('<' + newType + '></' + newType + '>');
493         newNode.setAttrs(node.getAttrs());
494         node.get('childNodes').each(function(child) {
495             newNode.append(child.remove());
496         });
497         node.replace(newNode);
498         return newNode;
499     },
501     /**
502      * Handle updating an existing table.
503      *
504      * @method _updateTable
505      * @param {EventFacade} e
506      * @private
507      */
508     _updateTable: function(e) {
509         var caption,
510             captionposition,
511             headers,
512             borders,
513             bordersize,
514             borderstyle,
515             bordercolour,
516             backgroundcolour,
517             table,
518             width,
519             captionnode;
521         e.preventDefault();
522         // Hide the dialogue.
523         this.getDialogue({
524             focusAfterHide: null
525         }).hide();
527         // Add/update the caption.
528         caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
529         captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
530         headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
531         borders = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERS);
532         bordersize = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSIZE);
533         bordercolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBORDERCOLOUR);
534         borderstyle = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSTYLE);
535         backgroundcolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBACKGROUNDCOLOUR);
536         width = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.WIDTH);
538         table = this._lastTarget.ancestor('table');
539         this._setAppearance(table, {
540             width: width,
541             borders: borders,
542             borderColour: bordercolour,
543             borderSize: bordersize,
544             borderStyle: borderstyle,
545             backgroundColour: backgroundcolour
546         });
548         captionnode = table.one('caption');
549         if (!captionnode) {
550             captionnode = Y.Node.create('<caption></caption>');
551             table.insert(captionnode, 0);
552         }
553         captionnode.setHTML(caption.get('value'));
554         captionnode.setStyle('caption-side', captionposition.get('value'));
555         if (!captionnode.getAttribute('style')) {
556             captionnode.removeAttribute('style');
557         }
559         // Add the row headers.
560         if (headers.get('value') === 'rows' || headers.get('value') === 'both') {
561             table.all('tr').each(function(row) {
562                 var cells = row.all('th, td'),
563                     firstCell = cells.shift(),
564                     newCell;
566                 if (firstCell.get('tagName') === 'TD') {
567                     // Cell is a td but should be a th - change it.
568                     newCell = this._changeNodeType(firstCell, 'th');
569                     newCell.setAttribute('scope', 'row');
570                 } else {
571                     firstCell.setAttribute('scope', 'row');
572                 }
574                 // Now make sure all other cells in the row are td.
575                 cells.each(function(cell) {
576                     if (cell.get('tagName') === 'TH') {
577                         newCell = this._changeNodeType(cell, 'td');
578                         newCell.removeAttribute('scope');
579                     }
580                 }, this);
582             }, this);
583         }
584         // Add the col headers. These may overrule the row headers in the first cell.
585         if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
586             var rows = table.all('tr'),
587                 firstRow = rows.shift(),
588                 newCell;
590             firstRow.all('td, th').each(function(cell) {
591                 if (cell.get('tagName') === 'TD') {
592                     // Cell is a td but should be a th - change it.
593                     newCell = this._changeNodeType(cell, 'th');
594                     newCell.setAttribute('scope', 'col');
595                 } else {
596                     cell.setAttribute('scope', 'col');
597                 }
598             }, this);
599             // Change all the cells in the rest of the table to tds (unless they are row headers).
600             rows.each(function(row) {
601                 var cells = row.all('th, td');
603                 if (headers.get('value') === 'both') {
604                     // Ignore the first cell because it's a row header.
605                     cells.shift();
606                 }
607                 cells.each(function(cell) {
608                     if (cell.get('tagName') === 'TH') {
609                         newCell = this._changeNodeType(cell, 'td');
610                         newCell.removeAttribute('scope');
611                     }
612                 }, this);
614             }, this);
615         }
616         // Clean the HTML.
617         this.markUpdated();
618     },
620     /**
621      * Handle creation of a new table.
622      *
623      * @method _setTable
624      * @param {EventFacade} e
625      * @private
626      */
627     _setTable: function(e) {
628         var caption,
629             captionposition,
630             borders,
631             bordersize,
632             borderstyle,
633             bordercolour,
634             rows,
635             cols,
636             headers,
637             tablehtml,
638             backgroundcolour,
639             width,
640             i, j;
642         e.preventDefault();
644         // Hide the dialogue.
645         this.getDialogue({
646             focusAfterHide: null
647         }).hide();
649         caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
650         captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);
651         borders = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERS);
652         bordersize = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSIZE);
653         bordercolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBORDERCOLOUR);
654         borderstyle = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSTYLE);
655         backgroundcolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBACKGROUNDCOLOUR);
656         rows = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.ROWS);
657         cols = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.COLUMNS);
658         headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
659         width = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.WIDTH);
661         // Set the selection.
662         this.get('host').setSelection(this._currentSelection);
664         // Note there are some spaces inserted in the cells and before and after, so that users have somewhere to click.
665         var nl = "\n";
666         var tableId = Y.guid();
667         tablehtml = '<br/>' + nl + '<table id="' + tableId + '">' + nl;
669         var captionstyle = '';
670         if (captionposition.get('value')) {
671             captionstyle = ' style="caption-side: ' + captionposition.get('value') + '"';
672         }
673         tablehtml += '<caption' + captionstyle + '>' + Y.Escape.html(caption.get('value')) + '</caption>' + nl;
674         i = 0;
675         if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
676             i = 1;
677             tablehtml += '<thead>' + nl + '<tr>' + nl;
678             for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
679                 tablehtml += '<th scope="col"></th>' + nl;
680             }
681             tablehtml += '</tr>' + nl + '</thead>' + nl;
682         }
683         tablehtml += '<tbody>' + nl;
684         for (; i < parseInt(rows.get('value'), 10); i++) {
685             tablehtml += '<tr>' + nl;
686             for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
687                 if (j === 0 && (headers.get('value') === 'rows' || headers.get('value') === 'both')) {
688                     tablehtml += '<th scope="row"></th>' + nl;
689                 } else {
690                     tablehtml += '<td ></td>' + nl;
691                 }
692             }
693             tablehtml += '</tr>' + nl;
694         }
695         tablehtml += '</tbody>' + nl;
696         tablehtml += '</table>' + nl + '<br/>';
698         this.get('host').insertContentAtFocusPoint(tablehtml);
700         var tableNode = Y.one('#' + tableId);
701         this._setAppearance(tableNode, {
702             width: width,
703             borders: borders,
704             borderColour: bordercolour,
705             borderSize: bordersize,
706             borderStyle: borderstyle,
707             backgroundColour: backgroundcolour
708         });
709         tableNode.removeAttribute('id');
711         // Mark the content as updated.
712         this.markUpdated();
713     },
715     /**
716      * Search for all the cells in the current, next and previous columns.
717      *
718      * @method _findColumnCells
719      * @private
720      * @return {Object} containing current, prev and next {Y.NodeList}s
721      */
722     _findColumnCells: function() {
723         var columnindex = this._getColumnIndex(this._lastTarget),
724             rows = this._lastTarget.ancestor('table').all('tr'),
725             currentcells = new Y.NodeList(),
726             prevcells = new Y.NodeList(),
727             nextcells = new Y.NodeList();
729         rows.each(function(row) {
730             var cells = row.all('td, th'),
731                 cell = cells.item(columnindex),
732                 cellprev = cells.item(columnindex - 1),
733                 cellnext = cells.item(columnindex + 1);
734             currentcells.push(cell);
735             if (cellprev) {
736                 prevcells.push(cellprev);
737             }
738             if (cellnext) {
739                 nextcells.push(cellnext);
740             }
741         });
743         return {
744             current: currentcells,
745             prev: prevcells,
746             next: nextcells
747         };
748     },
750     /**
751      * Hide the entries in the context menu that don't make sense with the
752      * current selection.
753      *
754      * @method _hideInvalidEntries
755      * @param {Y.Node} node - The node containing the menu.
756      * @private
757      */
758     _hideInvalidEntries: function(node) {
759         // Moving rows.
760         var table = this._lastTarget.ancestor('table'),
761             row = this._lastTarget.ancestor('tr'),
762             rows = table.all('tr'),
763             rowindex = rows.indexOf(row),
764             prevrow = rows.item(rowindex - 1),
765             prevrowhascells = prevrow ? prevrow.one('td') : null;
767         if (!row || !prevrowhascells) {
768             node.one('[data-change="moverowup"]').hide();
769         } else {
770             node.one('[data-change="moverowup"]').show();
771         }
773         var nextrow = rows.item(rowindex + 1),
774             rowhascell = row ? row.one('td') : false;
776         if (!row || !nextrow || !rowhascell) {
777             node.one('[data-change="moverowdown"]').hide();
778         } else {
779             node.one('[data-change="moverowdown"]').show();
780         }
782         // Moving columns.
783         var cells = this._findColumnCells();
784         if (cells.prev.filter('td').size() > 0) {
785             node.one('[data-change="movecolumnleft"]').show();
786         } else {
787             node.one('[data-change="movecolumnleft"]').hide();
788         }
790         var colhascell = cells.current.filter('td').size() > 0;
791         if ((cells.next.size() > 0) && colhascell) {
792             node.one('[data-change="movecolumnright"]').show();
793         } else {
794             node.one('[data-change="movecolumnright"]').hide();
795         }
797         // Delete col
798         if (cells.current.filter('td').size() > 0) {
799             node.one('[data-change="deletecolumn"]').show();
800         } else {
801             node.one('[data-change="deletecolumn"]').hide();
802         }
803         // Delete row
804         if (!row || !row.one('td')) {
805             node.one('[data-change="deleterow"]').hide();
806         } else {
807             node.one('[data-change="deleterow"]').show();
808         }
809     },
811     /**
812      * Display the table menu.
813      *
814      * @method _showTableMenu
815      * @param {EventFacade} e
816      * @private
817      */
818     _showTableMenu: function(e) {
819         e.preventDefault();
821         var boundingBox;
823         if (!this._contextMenu) {
824             this._menuOptions = [
825                 {
826                     text: M.util.get_string("addcolumnafter", COMPONENT),
827                     data: {
828                         change: "addcolumnafter"
829                     }
830                 }, {
831                     text: M.util.get_string("addrowafter", COMPONENT),
832                     data: {
833                         change: "addrowafter"
834                     }
835                 }, {
836                     text: M.util.get_string("moverowup", COMPONENT),
837                     data: {
838                         change: "moverowup"
839                     }
840                 }, {
841                     text: M.util.get_string("moverowdown", COMPONENT),
842                     data: {
843                         change: "moverowdown"
844                     }
845                 }, {
846                     text: M.util.get_string("movecolumnleft", COMPONENT),
847                     data: {
848                         change: "movecolumnleft"
849                     }
850                 }, {
851                     text: M.util.get_string("movecolumnright", COMPONENT),
852                     data: {
853                         change: "movecolumnright"
854                     }
855                 }, {
856                     text: M.util.get_string("deleterow", COMPONENT),
857                     data: {
858                         change: "deleterow"
859                     }
860                 }, {
861                     text: M.util.get_string("deletecolumn", COMPONENT),
862                     data: {
863                         change: "deletecolumn"
864                     }
865                 }, {
866                     text: M.util.get_string("edittable", COMPONENT),
867                     data: {
868                         change: "edittable"
869                     }
870                 }
871             ];
873             this._contextMenu = new Y.M.editor_atto.Menu({
874                 items: this._menuOptions
875             });
877             // Add event handlers for table control menus.
878             boundingBox = this._contextMenu.get('boundingBox');
879             boundingBox.delegate('click', this._handleTableChange, 'a', this);
880         }
882         boundingBox = this._contextMenu.get('boundingBox');
884         // We store the cell of the last click (the control node is transient).
885         this._lastTarget = e.tableCell.ancestor('.editor_atto_content td, .editor_atto_content th', true);
887         this._hideInvalidEntries(boundingBox);
889         // Clear the focusAfterHide for any other menus which may be open.
890         Y.Array.each(this.get('host').openMenus, function(menu) {
891             menu.set('focusAfterHide', null);
892         });
894         // Ensure that we focus on the button in the toolbar when we tab back to the menu.
895         var creatorButton = this.buttons[this.name];
896         this.get('host')._setTabFocus(creatorButton);
898         // Show the context menu, and align to the current position.
899         this._contextMenu.show();
900         this._contextMenu.align(this.buttons.table, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
901         this._contextMenu.set('focusAfterHide', creatorButton);
903         // If there are any anchors in the bounding box, focus on the first.
904         if (boundingBox.one('a')) {
905             boundingBox.one('a').focus();
906         }
908         // Add this menu to the list of open menus.
909         this.get('host').openMenus = [this._contextMenu];
910     },
912     /**
913      * Handle a selection from the table control menu.
914      *
915      * @method _handleTableChange
916      * @param {EventFacade} e
917      * @private
918      */
919     _handleTableChange: function(e) {
920         e.preventDefault();
922         this._contextMenu.set('focusAfterHide', this.get('host').editor);
923         // Hide the context menu.
924         this._contextMenu.hide(e);
926         // Make our changes.
927         switch (e.target.getData('change')) {
928             case 'addcolumnafter':
929                 this._addColumnAfter();
930                 break;
931             case 'addrowafter':
932                 this._addRowAfter();
933                 break;
934             case 'deleterow':
935                 this._deleteRow();
936                 break;
937             case 'deletecolumn':
938                 this._deleteColumn();
939                 break;
940             case 'edittable':
941                 this._editTable();
942                 break;
943             case 'moverowdown':
944                 this._moveRowDown();
945                 break;
946             case 'moverowup':
947                 this._moveRowUp();
948                 break;
949             case 'movecolumnleft':
950                 this._moveColumnLeft();
951                 break;
952             case 'movecolumnright':
953                 this._moveColumnRight();
954                 break;
955         }
956     },
958     /**
959      * Determine the index of a row in a table column.
960      *
961      * @method _getRowIndex
962      * @param {Node} cell
963      * @private
964      */
965     _getRowIndex: function(cell) {
966         var tablenode = cell.ancestor('table'),
967             rownode = cell.ancestor('tr');
969         if (!tablenode || !rownode) {
970             return;
971         }
973         var rows = tablenode.all('tr');
975         return rows.indexOf(rownode);
976     },
978     /**
979      * Determine the index of a column in a table row.
980      *
981      * @method _getColumnIndex
982      * @param {Node} cellnode
983      * @private
984      */
985     _getColumnIndex: function(cellnode) {
986         var rownode = cellnode.ancestor('tr');
988         if (!rownode) {
989             return;
990         }
992         var cells = rownode.all('td, th');
994         return cells.indexOf(cellnode);
995     },
997     /**
998      * Delete the current row.
999      *
1000      * @method _deleteRow
1001      * @private
1002      */
1003     _deleteRow: function() {
1004         var row = this._lastTarget.ancestor('tr');
1006         if (row && row.one('td')) {
1007             // Only delete rows with at least one non-header cell.
1008             row.remove(true);
1009         }
1011         // Clean the HTML.
1012         this.markUpdated();
1013     },
1015     /**
1016      * Move row up
1017      *
1018      * @method _moveRowUp
1019      * @private
1020      */
1021     _moveRowUp: function() {
1022         var row = this._lastTarget.ancestor('tr'),
1023             prevrow = row.previous('tr');
1024         if (!row || !prevrow) {
1025             return;
1026         }
1028         row.swap(prevrow);
1029         // Clean the HTML.
1030         this.markUpdated();
1031     },
1033     /**
1034      * Move column left
1035      *
1036      * @method _moveColumnLeft
1037      * @private
1038      */
1039     _moveColumnLeft: function() {
1040         var cells = this._findColumnCells();
1042         if (cells.current.size() > 0 && cells.prev.size() > 0 && cells.current.size() === cells.prev.size()) {
1043             var i = 0;
1044             for (i = 0; i < cells.current.size(); i++) {
1045                 var cell = cells.current.item(i),
1046                     prevcell = cells.prev.item(i);
1048                 cell.swap(prevcell);
1049             }
1050         }
1051         // Cleanup.
1052         this.markUpdated();
1053     },
1055     /**
1056      * Add a caption to the table if it doesn't have one.
1057      *
1058      * @method _addCaption
1059      * @private
1060      */
1061     _addCaption: function() {
1062         var table = this._lastTarget.ancestor('table'),
1063             caption = table.one('caption');
1065         if (!caption) {
1066             table.insert(Y.Node.create('<caption>&nbsp;</caption>'), 1);
1067         }
1068     },
1070     /**
1071      * Remove a caption from the table if has one.
1072      *
1073      * @method _removeCaption
1074      * @private
1075      */
1076     _removeCaption: function() {
1077         var table = this._lastTarget.ancestor('table'),
1078             caption = table.one('caption');
1080         if (caption) {
1081             caption.remove(true);
1082         }
1083     },
1085     /**
1086      * Move column right.
1087      *
1088      * @method _moveColumnRight
1089      * @private
1090      */
1091     _moveColumnRight: function() {
1092         var cells = this._findColumnCells();
1094         // Check we have some tds in this column, and one exists to the right.
1095         if ((cells.next.size() > 0) &&
1096                 (cells.current.size() === cells.next.size()) &&
1097                 (cells.current.filter('td').size() > 0)) {
1098             var i = 0;
1099             for (i = 0; i < cells.current.size(); i++) {
1100                 var cell = cells.current.item(i),
1101                     nextcell = cells.next.item(i);
1103                 cell.swap(nextcell);
1104             }
1105         }
1106         // Cleanup.
1107         this.markUpdated();
1108     },
1110     /**
1111      * Move row down.
1112      *
1113      * @method _moveRowDown
1114      * @private
1115      */
1116     _moveRowDown: function() {
1117         var row = this._lastTarget.ancestor('tr'),
1118             nextrow = row.next('tr');
1119         if (!row || !nextrow || !row.one('td')) {
1120             return;
1121         }
1123         row.swap(nextrow);
1124         // Clean the HTML.
1125         this.markUpdated();
1126     },
1128     /**
1129      * Obtain values for the table borders
1130      *
1131      * @method _getBorderConfiguration
1132      * @param {Node} node
1133      * @private
1134      * @return {Array} or {Boolean} Returns the settings, if presents, or else returns false
1135      */
1136     _getBorderConfiguration: function(node) {
1137         // We need to make a clone of the node in order to avoid grabbing any
1138         // of the computed styles from the DOM. We only want inline styles set by us.
1139         var shadowNode = node.cloneNode(true);
1140         var borderStyle = shadowNode.getStyle('borderStyle'),
1141             borderColor = shadowNode.getStyle('borderColor'),
1142             borderWidth = shadowNode.getStyle('borderWidth');
1144         if (borderStyle || borderColor || borderWidth) {
1145             var hexColour = Y.Color.toHex(borderColor);
1146             var width = parseInt(borderWidth, 10);
1147             return {
1148                 borderStyle: borderStyle,
1149                 borderColor: hexColour === "#" ? null : hexColour,
1150                 borderWidth: isNaN(width) ? null : width
1151             };
1152         }
1154         return false;
1155     },
1157     /**
1158      * Set the appropriate styles on the given table node according to
1159      * the provided configuration.
1160      *
1161      * @method _setAppearance
1162      * @param {Node} The table node to be modified.
1163      * @param {Object} Configuration object (associative array) containing the form nodes for
1164      *                 border styling.
1165      * @private
1166      */
1167     _setAppearance: function(tableNode, configuration) {
1168         var borderhex,
1169             borderSizeValue,
1170             borderStyleValue,
1171             backgroundcolourvalue;
1173         if (configuration.borderColour) {
1174             borderhex = configuration.borderColour.get('value');
1175         }
1177         if (configuration.borderSize) {
1178             borderSizeValue = configuration.borderSize.get('value');
1179         }
1181         if (configuration.borderStyle) {
1182             borderStyleValue = configuration.borderStyle.get('value');
1183         }
1185         if (configuration.backgroundColour) {
1186             backgroundcolourvalue = configuration.backgroundColour.get('value');
1187         }
1189         // Clear the inline border styling
1190         tableNode.removeAttribute('style');
1191         tableNode.all('td, th').each(function(cell) {
1192             cell.removeAttribute('style');
1193         }, this);
1195         if (configuration.borders) {
1196             if (configuration.borders.get('value') === 'outer') {
1197                 tableNode.setStyle('borderWidth', borderSizeValue + CSS.BORDERSIZEUNIT);
1198                 tableNode.setStyle('borderStyle', borderStyleValue);
1200                 if (borderhex !== 'none') {
1201                     tableNode.setStyle('borderColor', borderhex);
1202                 }
1203             } else if (configuration.borders.get('value') === 'all') {
1204                 tableNode.all('td, th').each(function(cell) {
1205                     cell.setStyle('borderWidth', borderSizeValue + CSS.BORDERSIZEUNIT);
1206                     cell.setStyle('borderStyle', borderStyleValue);
1208                     if (borderhex !== 'none') {
1209                         cell.setStyle('borderColor', borderhex);
1210                     }
1211                 }, this);
1212             }
1213         }
1215         if (backgroundcolourvalue !== 'none') {
1216             tableNode.setStyle('backgroundColor', backgroundcolourvalue);
1217         }
1219         if (configuration.width && configuration.width.get('value')) {
1220             tableNode.setStyle('width', configuration.width.get('value') + CSS.WIDTHUNIT);
1221         }
1222     },
1224     /**
1225      * Edit table (show the dialogue).
1226      *
1227      * @method _editTable
1228      * @private
1229      */
1230     _editTable: function() {
1231         var dialogue = this.getDialogue({
1232             headerContent: M.util.get_string('edittable', COMPONENT),
1233             focusAfterHide: false,
1234             focusOnShowSelector: SELECTORS.CAPTION,
1235             width: DIALOGUE.WIDTH
1236         });
1238         // Set the dialogue content, and then show the dialogue.
1239         var node = this._getDialogueContent(true),
1240             captioninput = node.one(SELECTORS.CAPTION),
1241             captionpositioninput = node.one(SELECTORS.CAPTIONPOSITION),
1242             headersinput = node.one(SELECTORS.HEADERS),
1243             borderinput = node.one(SELECTORS.BORDERS),
1244             borderstyle = node.one(SELECTORS.BORDERSTYLE),
1245             bordercolours = node.all(SELECTORS.BORDERCOLOURS),
1246             bordersize = node.one(SELECTORS.BORDERSIZE),
1247             backgroundcolours = node.all(SELECTORS.BACKGROUNDCOLOURS),
1248             width = node.one(SELECTORS.WIDTH),
1249             table = this._lastTarget.ancestor('table'),
1250             captionnode = table.one('caption'),
1251             hexColour,
1252             matchedInput;
1254         if (captionnode) {
1255             captioninput.set('value', captionnode.getHTML());
1256         } else {
1257             captioninput.set('value', '');
1258         }
1260         if (width && table.getStyle('width').indexOf('px') === -1) {
1261             width.set('value', parseInt(table.getStyle('width'), 10));
1262         }
1264         if (captionpositioninput && captionnode && captionnode.getAttribute('style')) {
1265             captionpositioninput.set('value', captionnode.getStyle('caption-side'));
1266         } else {
1267             // Default to none.
1268             captionpositioninput.set('value', '');
1269         }
1271         if (table.getStyle('backgroundColor') && this.get('allowBackgroundColour')) {
1272             hexColour = Y.Color.toHex(table.getStyle('backgroundColor'));
1273             matchedInput = backgroundcolours.filter('[value="' + hexColour + '"]');
1275             if (matchedInput) {
1276                 matchedInput.set("checked", true);
1277             }
1278         }
1280         if (this.get('allowBorders')) {
1281             var borderValue = 'default',
1282                 borderConfiguration = this._getBorderConfiguration(table);
1284             if (borderConfiguration) {
1285                 borderValue = 'outer';
1286             } else {
1287                 borderConfiguration = this._getBorderConfiguration(table.one('td'));
1288                 if (borderConfiguration) {
1289                      borderValue = 'all';
1290                 }
1291             }
1293             if (borderConfiguration) {
1294                 var borderStyle = borderConfiguration.borderStyle || DEFAULT.BORDERSTYLE;
1295                 var borderSize = borderConfiguration.borderWidth || DEFAULT.BORDERWIDTH;
1296                 borderstyle.set('value', borderStyle);
1297                 bordersize.set('value', borderSize);
1298                 borderinput.set('value', borderValue);
1300                 hexColour = borderConfiguration.borderColor;
1301                 matchedInput = bordercolours.filter('[value="' + hexColour + '"]');
1303                 if (matchedInput) {
1304                     matchedInput.set("checked", true);
1305                 }
1306             }
1307         }
1309         var headersvalue = 'columns';
1310         if (table.one('th[scope="row"]')) {
1311             headersvalue = 'rows';
1312             if (table.one('th[scope="col"]')) {
1313                 headersvalue = 'both';
1314             }
1315         }
1316         headersinput.set('value', headersvalue);
1317         dialogue.set('bodyContent', node).show();
1318         this._updateAvailableSettings();
1319     },
1322     /**
1323      * Delete the current column.
1324      *
1325      * @method _deleteColumn
1326      * @private
1327      */
1328     _deleteColumn: function() {
1329         var columnindex = this._getColumnIndex(this._lastTarget),
1330             table = this._lastTarget.ancestor('table'),
1331             rows = table.all('tr'),
1332             columncells = new Y.NodeList(),
1333             hastd = false;
1335         rows.each(function(row) {
1336             var cells = row.all('td, th');
1337             var cell = cells.item(columnindex);
1338             if (cell.get('tagName') === 'TD') {
1339                 hastd = true;
1340             }
1341             columncells.push(cell);
1342         });
1344         // Do not delete all the headers.
1345         if (hastd) {
1346             columncells.remove(true);
1347         }
1349         // Clean the HTML.
1350         this.markUpdated();
1351     },
1353     /**
1354      * Add a row after the current row.
1355      *
1356      * @method _addRowAfter
1357      * @private
1358      */
1359     _addRowAfter: function() {
1360         var target = this._lastTarget.ancestor('tr'),
1361             tablebody = this._lastTarget.ancestor('table').one('tbody');
1362         if (!tablebody) {
1363             // Not all tables have tbody.
1364             tablebody = this._lastTarget.ancestor('table');
1365         }
1367         var firstrow = tablebody.one('tr');
1368         if (!firstrow) {
1369             firstrow = this._lastTarget.ancestor('table').one('tr');
1370         }
1371         if (!firstrow) {
1372             // Table has no rows. Boo.
1373             return;
1374         }
1375         var newrow = firstrow.cloneNode(true);
1376         newrow.all('th, td').each(function(tablecell) {
1377             if (tablecell.get('tagName') === 'TH') {
1378                 if (tablecell.getAttribute('scope') !== 'row') {
1379                     var newcell = Y.Node.create('<td></td>');
1380                     tablecell.replace(newcell);
1381                     tablecell = newcell;
1382                 }
1383             }
1384             tablecell.setHTML('&nbsp;');
1385         });
1387         if (target.ancestor('thead')) {
1388             target = firstrow;
1389             tablebody.insert(newrow, target);
1390         } else {
1391             target.insert(newrow, 'after');
1392         }
1394         // Clean the HTML.
1395         this.markUpdated();
1396     },
1398     /**
1399      * Add a column after the current column.
1400      *
1401      * @method _addColumnAfter
1402      * @private
1403      */
1404     _addColumnAfter: function() {
1405         var cells = this._findColumnCells(),
1406             before = true,
1407             clonecells = cells.next;
1408         if (cells.next.size() <= 0) {
1409             before = false;
1410             clonecells = cells.current;
1411         }
1413         Y.each(clonecells, function(cell) {
1414             var newcell = cell.cloneNode();
1415             // Clear the content of the cell.
1416             newcell.setHTML('&nbsp;');
1418             if (before) {
1419                 cell.get('parentNode').insert(newcell, cell);
1420             } else {
1421                 cell.get('parentNode').insert(newcell, cell);
1422                 cell.swap(newcell);
1423             }
1424         }, this);
1426         // Clean the HTML.
1427         this.markUpdated();
1428     }
1430 }, {
1431     ATTRS: {
1432         /**
1433          * Whether or not to allow borders
1434          *
1435          * @attribute allowBorder
1436          * @type Boolean
1437          */
1438         allowBorders: {
1439             value: true
1440         },
1442         /**
1443          * What border styles to allow
1444          *
1445          * @attribute borderStyles
1446          * @type Array
1447          */
1448         borderStyles: {
1449             value: [
1450                 'none',
1451                 'solid',
1452                 'dashed',
1453                 'dotted'
1454             ]
1455         },
1457         /**
1458          * Whether or not to allow colourizing the background
1459          *
1460          * @attribute allowBackgroundColour
1461          * @type Boolean
1462          */
1463         allowBackgroundColour: {
1464             value: true
1465         },
1467         /**
1468          * Whether or not to allow setting the table width
1469          *
1470          * @attribute allowWidth
1471          * @type Boolean
1472          */
1473         allowWidth: {
1474             value: true
1475         },
1477         /**
1478          * Whether we allow styling
1479          * @attribute allowStyling
1480          * @type Boolean
1481          */
1482         allowStyling: {
1483             readOnly: true,
1484             getter: function() {
1485                 return this.get('allowBorders') || this.get('allowBackgroundColour') || this.get('allowWidth');
1486             }
1487         },
1489         /**
1490          * Available colors
1491          * @attribute availableColors
1492          * @type Array
1493          */
1494         availableColors: {
1495             value: [
1496                 '#FFFFFF',
1497                 '#EF4540',
1498                 '#FFCF35',
1499                 '#98CA3E',
1500                 '#7D9FD3',
1501                 '#333333'
1502             ],
1503             readOnly: true
1504         }
1505     }
1506 });