AutorÃa | Ultima modificación | Ver Log |
// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** @package atto_table* @copyright 2013 Damyon Wiese <damyon@moodle.com>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*//*** @module moodle-atto_table-button*//*** Atto text editor table plugin.** @namespace M.atto_table* @class Button* @extends M.editor_atto.EditorPlugin*/var COMPONENT = 'atto_table',DEFAULT = {BORDERSTYLE: 'none',BORDERWIDTH: '1'},DIALOGUE = {WIDTH: '480px'},TEMPLATE = '' +'<form class="{{CSS.FORM}}">' +'<div class="mb-1 mb-3 row">' +'<div class="col-sm-4">' +'<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' +'</div><div class="col-sm-8">' +'<input type="text" class="form-control {{CSS.CAPTION}}" id="{{elementid}}_atto_table_caption" required />' +'</div>' +'</div>' +'<div class="mb-1 mb-3 row">' +'<div class="col-sm-4">' +'<label for="{{elementid}}_atto_table_captionposition">' +'{{get_string "captionposition" component}}</label>' +'</div><div class="col-sm-8">' +'<select class="custom-select {{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +'<option value=""></option>' +'<option value="top">{{get_string "top" "editor"}}</option>' +'<option value="bottom">{{get_string "bottom" "editor"}}</option>' +'</select>' +'</div>' +'</div>' +'<div class="mb-1 mb-3 row">' +'<div class="col-sm-4">' +'<label for="{{elementid}}_atto_table_headers">{{get_string "headers" component}}</label>' +'</div><div class="col-sm-8">' +'<select class="custom-select {{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +'<option value="columns">{{get_string "columns" component}}' + '</option>' +'<option value="rows">{{get_string "rows" component}}' + '</option>' +'<option value="both">{{get_string "both" component}}' + '</option>' +'</select>' +'</div>' +'</div>' +'{{#if nonedit}}' +'<div class="mb-1 mb-3 row">' +'<div class="col-sm-4">' +'<label for="{{elementid}}_atto_table_rows">{{get_string "numberofrows" component}}</label>' +'</div><div class="col-sm-8">' +'<input class="form-control w-auto {{CSS.ROWS}}" type="number" value="3" ' +'id="{{elementid}}_atto_table_rows" size="8" min="1" max="50"/>' +'</div>' +'</div>' +'<div class="mb-1 mb-3 row">' +'<div class="col-sm-4">' +'<label for="{{elementid}}_atto_table_columns" ' +'>{{get_string "numberofcolumns" component}}</label>' +'</div><div class="col-sm-8">' +'<input class="form-control w-auto {{CSS.COLUMNS}}" type="number" value="3" ' +'id="{{elementid}}_atto_table_columns"' +'size="8" min="1" max="20"/>' +'</div>' +'</div>' +'{{/if}}' +'{{#if allowStyling}}' +'<fieldset>' +'<legend class="mdl-align">{{get_string "appearance" component}}</legend>' +'{{#if allowBorders}}' +'<div class="mb-1 mb-3 row">' +'<div class="col-sm-4">' +'<label for="{{elementid}}_atto_table_borders">{{get_string "borders" component}}</label>' +'</div><div class="col-sm-8">' +'<select name="borders" class="custom-select {{CSS.BORDERS}}" id="{{elementid}}_atto_table_borders">' +'<option value="default">{{get_string "themedefault" component}}' + '</option>' +'<option value="outer">{{get_string "outer" component}}' + '</option>' +'<option value="all">{{get_string "all" component}}' + '</option>' +'</select>' +'</div>' +'</div>' +'<div class="mb-1 mb-3 row">' +'<div class="col-sm-4">' +'<label for="{{elementid}}_atto_table_borderstyle">' +'{{get_string "borderstyles" component}}</label>' +'</div><div class="col-sm-8">' +'<select name="borderstyles" class="custom-select {{CSS.BORDERSTYLE}}" ' +'id="{{elementid}}_atto_table_borderstyle">' +'{{#each borderStyles}}' +'<option value="' + '{{this}}' + '">' + '{{get_string this ../component}}' + '</option>' +'{{/each}}' +'</select>' +'</div>' +'</div>' +'<div class="mb-1 mb-3 row">' +'<div class="col-sm-4">' +'<label for="{{elementid}}_atto_table_bordersize">' +'{{get_string "bordersize" component}}</label>' +'</div><div class="col-sm-8">' +'<div class="d-flex flex-wrap align-items-center">' +'<input name="bordersize" id="{{elementid}}_atto_table_bordersize" ' +'class="form-control w-auto mr-1 {{CSS.BORDERSIZE}}"' +'type="number" value="1" size="8" min="1" max="50"/>' +'<label>{{CSS.BORDERSIZEUNIT}}</label>' +'</div>' +'</div>' +'</div>' +'<div class="mb-1 mb-3 row">' +'<div class="col-sm-4">' +'<label for="{{elementid}}_atto_table_bordercolour">' +'{{get_string "bordercolour" component}}</label>' +'</div><div class="col-sm-8">' +'<div id="{{elementid}}_atto_table_bordercolour"' +'class="d-flex flex-wrap align-items-center {{CSS.BORDERCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +'<div class="tablebordercolor" style="background-color:transparent;color:transparent">' +'<input id="{{../elementid}}_atto_table_bordercolour_-1"' +'type="radio" class="m-0" name="borderColour" value="none" checked="checked"' +'title="{{get_string "themedefault" component}}"></input>' +'<label for="{{../elementid}}_atto_table_bordercolour_-1" class="accesshide">' +'{{get_string "themedefault" component}}</label>' +'</div>' +'{{#each availableColours}}' +'<div class="tablebordercolor" style="background-color:{{this}};color:{{this}}">' +'<input id="{{../elementid}}_atto_table_bordercolour_{{@index}}"' +'type="radio" class="m-0" name="borderColour" value="' + '{{this}}' + '" title="{{this}}">' +'<label for="{{../elementid}}_atto_table_bordercolour_{{@index}}" class="accesshide">' +'{{this}}</label>' +'</div>' +'{{/each}}' +'</div>' +'</div>' +'</div>' +'{{/if}}' +'{{#if allowBackgroundColour}}' +'<div class="mb-1 mb-3 row">' +'<div class="col-sm-4">' +'<label for="{{elementid}}_atto_table_backgroundcolour">' +'{{get_string "backgroundcolour" component}}</label>' +'</div><div class="col-sm-8">' +'<div id="{{elementid}}_atto_table_backgroundcolour"' +'class="d-flex flex-wrap align-items-center {{CSS.BACKGROUNDCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +'<div class="tablebackgroundcolor" style="background-color:transparent;color:transparent">' +'<input id="{{../elementid}}_atto_table_backgroundcolour_-1"' +'type="radio" class="m-0" name="backgroundColour" value="none" checked="checked"' +'title="{{get_string "themedefault" component}}"></input>' +'<label for="{{../elementid}}_atto_table_backgroundcolour_-1" class="accesshide">' +'{{get_string "themedefault" component}}</label>' +'</div>' +'{{#each availableColours}}' +'<div class="tablebackgroundcolor" style="background-color:{{this}};color:{{this}}">' +'<input id="{{../elementid}}_atto_table_backgroundcolour_{{@index}}"' +'type="radio" class="m-0" name="backgroundColour" value="' + '{{this}}' + '" title="{{this}}">' +'<label for="{{../elementid}}_atto_table_backgroundcolour_{{@index}}" class="accesshide">' +'{{this}}</label>' +'</div>' +'{{/each}}' +'</div>' +'</div>' +'</div>' +'{{/if}}' +'{{#if allowWidth}}' +'<div class="mb-1 mb-3 row">' +'<div class="col-sm-4">' +'<label for="{{elementid}}_atto_table_width">' +'{{get_string "width" component}}</label>' +'</div><div class="col-sm-8">' +'<div class="d-flex flex-wrap align-items-center">' +'<input name="width" id="{{elementid}}_atto_table_width" ' +'class="form-control w-auto mr-1 {{CSS.WIDTH}}" size="8" ' +'type="number" min="0" max="100"/>' +'<label>{{CSS.WIDTHUNIT}}</label>' +'</div>' +'</div>' +'</div>' +'{{/if}}' +'</fieldset>' +'{{/if}}' +'<div class="mdl-align">' +'<br/>' +'{{#if edit}}' +'<button class="btn btn-secondary submit" type="submit">{{get_string "updatetable" component}}</button>' +'{{/if}}' +'{{#if nonedit}}' +'<button class="btn btn-secondary submit" type="submit">{{get_string "createtable" component}}</button>' +'{{/if}}' +'</div>' +'</form>',CSS = {CAPTION: 'caption',CAPTIONPOSITION: 'captionposition',HEADERS: 'headers',ROWS: 'rows',COLUMNS: 'columns',SUBMIT: 'submit',FORM: 'atto_form',BORDERS: 'borders',BORDERSIZE: 'bordersize',BORDERSIZEUNIT: 'px',BORDERCOLOUR: 'bordercolour',BORDERSTYLE: 'borderstyle',BACKGROUNDCOLOUR: 'backgroundcolour',WIDTH: 'customwidth',WIDTHUNIT: '%',AVAILABLECOLORS: 'availablecolors',COLOURROW: 'colourrow'},SELECTORS = {CAPTION: '.' + CSS.CAPTION,CAPTIONPOSITION: '.' + CSS.CAPTIONPOSITION,HEADERS: '.' + CSS.HEADERS,ROWS: '.' + CSS.ROWS,COLUMNS: '.' + CSS.COLUMNS,SUBMIT: '.' + CSS.SUBMIT,BORDERS: '.' + CSS.BORDERS,BORDERSIZE: '.' + CSS.BORDERSIZE,BORDERCOLOURS: '.' + CSS.BORDERCOLOUR + ' input[name="borderColour"]',SELECTEDBORDERCOLOUR: '.' + CSS.BORDERCOLOUR + ' input[name="borderColour"]:checked',BORDERSTYLE: '.' + CSS.BORDERSTYLE,BACKGROUNDCOLOURS: '.' + CSS.BACKGROUNDCOLOUR + ' input[name="backgroundColour"]',SELECTEDBACKGROUNDCOLOUR: '.' + CSS.BACKGROUNDCOLOUR + ' input[name="backgroundColour"]:checked',FORM: '.atto_form',WIDTH: '.' + CSS.WIDTH,AVAILABLECOLORS: '.' + CSS.AVAILABLECOLORS};Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {/*** A reference to the current selection at the time that the dialogue* was opened.** @property _currentSelection* @type Range* @private*/_currentSelection: null,/*** The contextual menu that we can open.** @property _contextMenu* @type M.editor_atto.Menu* @private*/_contextMenu: null,/*** The last modified target.** @property _lastTarget* @type Node* @private*/_lastTarget: null,/*** The list of menu items.** @property _menuOptions* @type Object* @private*/_menuOptions: null,initializer: function() {var button = this.addButton({icon: 'e/table',callback: this._displayTableEditor,tags: 'table'});// Listen for toggled highlighting.require(['editor_atto/events'], function(attoEvents) {var domButton = button.getDOMNode();domButton.addEventListener(attoEvents.eventTypes.attoButtonHighlightToggled, function(e) {this._setAriaAttributes(e.detail.buttonName, e.detail.highlight);}.bind(this));}.bind(this));// Disable mozilla table controls.if (Y.UA.gecko) {document.execCommand("enableInlineTableEditing", false, false);document.execCommand("enableObjectResizing", false, false);}},/*** Sets the appropriate ARIA attributes for the table button when it switches roles between a button and a menu button.** @param {String} buttonName The button name.* @param {Boolean} highlight True when the button was highlighted. False, otherwise.* @private*/_setAriaAttributes: function(buttonName, highlight) {var menuButton = this.buttons[buttonName];if (menuButton) {if (highlight) {// This button becomes a menu button. Add appropriate ARIA attributes.var id = menuButton.getAttribute('id');menuButton.setAttribute('aria-haspopup', true);menuButton.setAttribute('aria-controls', id + '_menu');menuButton.setAttribute('aria-expanded', true);} else {menuButton.removeAttribute('aria-haspopup');menuButton.removeAttribute('aria-controls');menuButton.removeAttribute('aria-expanded');}}},/*** Display the table tool.** @method _displayDialogue* @private*/_displayDialogue: function() {// Store the current cursor position.this._currentSelection = this.get('host').getSelection();if (this._currentSelection !== false && (!this._currentSelection.collapsed)) {var dialogue = this.getDialogue({headerContent: M.util.get_string('createtable', COMPONENT),focusAfterHide: true,focusOnShowSelector: SELECTORS.CAPTION,width: DIALOGUE.WIDTH});// Set the dialogue content, and then show the dialogue.dialogue.set('bodyContent', this._getDialogueContent(false)).show();this._updateAvailableSettings();}},/*** Display the appropriate table editor.** If the current selection includes a table, then we show the* contextual menu, otherwise show the table creation dialogue.** @method _displayTableEditor* @param {EventFacade} e* @private*/_displayTableEditor: function(e) {var cell = this._getSuitableTableCell();var menuButton = e.currentTarget.ancestor('button', true);if (cell) {var id = menuButton.getAttribute('id');// Indicate that the menu is expanded.menuButton.setAttribute('aria-expanded', true);// Add the cell to the EventFacade to save duplication in when showing the menu.e.tableCell = cell;return this._showTableMenu(e, id);} else {// Dialog mode. Remove aria-expanded attribute.menuButton.removeAttribute('aria-expanded');}return this._displayDialogue(e);},/*** Returns whether or not the parameter node exists within the editor.** @method _stopAtContentEditableFilter* @param {Node} node* @private* @return {boolean} whether or not the parameter node exists within the editor.*/_stopAtContentEditableFilter: function(node) {return this.editor.contains(node);},/*** Return the dialogue content for the tool, attaching any required* events.** @method _getDialogueContent* @private* @return {Node} The content to place in the dialogue.*/_getDialogueContent: function(edit) {var template = Y.Handlebars.compile(TEMPLATE);var allowBorders = this.get('allowBorders');this._content = Y.Node.create(template({CSS: CSS,elementid: this.get('host').get('elementid'),component: COMPONENT,edit: edit,nonedit: !edit,allowStyling: this.get('allowStyling'),allowBorders: allowBorders,borderStyles: this.get('borderStyles'),allowBackgroundColour: this.get('allowBackgroundColour'),availableColours: this.get('availableColors'),allowWidth: this.get('allowWidth')}));// Handle table setting.if (edit) {this._content.one('.submit').on('click', this._updateTable, this);} else {this._content.one('.submit').on('click', this._setTable, this);}if (allowBorders) {this._content.one('[name="borders"]').on('change', this._updateAvailableSettings, this);}return this._content;},/*** Disables options within the dialogue if they shouldn't be available.* E.g.* If borders are set to "Theme default" then the border size, style and* colour options are disabled.** @method _updateAvailableSettings* @private*/_updateAvailableSettings: function() {var tableForm = this._content,enableBorders = tableForm.one('[name="borders"]'),borderStyle = tableForm.one('[name="borderstyles"]'),borderSize = tableForm.one('[name="bordersize"]'),borderColour = tableForm.all('[name="borderColour"]'),disabledValue = 'removeAttribute';if (!enableBorders) {return;}if (enableBorders.get('value') === 'default') {disabledValue = 'setAttribute';}if (borderStyle) {borderStyle[disabledValue]('disabled');}if (borderSize) {borderSize[disabledValue]('disabled');}if (borderColour) {borderColour[disabledValue]('disabled');}},/*** Given the current selection, return a table cell suitable for table editing* purposes, i.e. the first table cell selected, or the first cell in the table* that the selection exists in, or null if not within a table.** @method _getSuitableTableCell* @private* @return {Node} suitable target cell, or null if not within a table*/_getSuitableTableCell: function() {var targetcell = null,host = this.get('host');var stopAtContentEditableFilter = Y.bind(this._stopAtContentEditableFilter, this);host.getSelectedNodes().some(function(node) {if (node.ancestor('td, th, caption', true, stopAtContentEditableFilter)) {targetcell = node;var caption = node.ancestor('caption', true, stopAtContentEditableFilter);if (caption) {var table = caption.get('parentNode');if (table) {targetcell = table.one('td, th');}}// Once we've found a cell to target, we shouldn't need to keep looking.return true;}});if (targetcell) {var selection = host.getSelectionFromNode(targetcell);host.setSelection(selection);}return targetcell;},/*** Change a node from one type to another, copying all attributes and children.** @method _changeNodeType* @param {Y.Node} node* @param {String} new node type* @private* @chainable*/_changeNodeType: function(node, newType) {var newNode = Y.Node.create('<' + newType + '></' + newType + '>');newNode.setAttrs(node.getAttrs());node.get('childNodes').each(function(child) {newNode.append(child.remove());});node.replace(newNode);return newNode;},/*** Handle updating an existing table.** @method _updateTable* @param {EventFacade} e* @private*/_updateTable: function(e) {var caption,captionposition,headers,borders,bordersize,borderstyle,bordercolour,backgroundcolour,table,width,captionnode;e.preventDefault();// Hide the dialogue.this.getDialogue({focusAfterHide: null}).hide();// Add/update the caption.caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);borders = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERS);bordersize = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSIZE);bordercolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBORDERCOLOUR);borderstyle = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSTYLE);backgroundcolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBACKGROUNDCOLOUR);width = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.WIDTH);table = this._lastTarget.ancestor('table');this._setAppearance(table, {width: width,borders: borders,borderColour: bordercolour,borderSize: bordersize,borderStyle: borderstyle,backgroundColour: backgroundcolour});captionnode = table.one('caption');if (!captionnode) {captionnode = Y.Node.create('<caption></caption>');table.insert(captionnode, 0);}captionnode.setHTML(caption.get('value'));captionnode.setStyle('caption-side', captionposition.get('value'));if (!captionnode.getAttribute('style')) {captionnode.removeAttribute('style');}// Add the row headers.if (headers.get('value') === 'rows' || headers.get('value') === 'both') {table.all('tr').each(function(row) {var cells = row.all('th, td'),firstCell = cells.shift(),newCell;if (firstCell.get('tagName') === 'TD') {// Cell is a td but should be a th - change it.newCell = this._changeNodeType(firstCell, 'th');newCell.setAttribute('scope', 'row');} else {firstCell.setAttribute('scope', 'row');}// Now make sure all other cells in the row are td.cells.each(function(cell) {if (cell.get('tagName') === 'TH') {newCell = this._changeNodeType(cell, 'td');newCell.removeAttribute('scope');}}, this);}, this);}// Add the col headers. These may overrule the row headers in the first cell.if (headers.get('value') === 'columns' || headers.get('value') === 'both') {var rows = table.all('tr'),firstRow = rows.shift(),newCell;firstRow.all('td, th').each(function(cell) {if (cell.get('tagName') === 'TD') {// Cell is a td but should be a th - change it.newCell = this._changeNodeType(cell, 'th');newCell.setAttribute('scope', 'col');} else {cell.setAttribute('scope', 'col');}}, this);// Change all the cells in the rest of the table to tds (unless they are row headers).rows.each(function(row) {var cells = row.all('th, td');if (headers.get('value') === 'both') {// Ignore the first cell because it's a row header.cells.shift();}cells.each(function(cell) {if (cell.get('tagName') === 'TH') {newCell = this._changeNodeType(cell, 'td');newCell.removeAttribute('scope');}}, this);}, this);}// Clean the HTML.this.markUpdated();},/*** Handle creation of a new table.** @method _setTable* @param {EventFacade} e* @private*/_setTable: function(e) {var caption,captionposition,borders,bordersize,borderstyle,bordercolour,rows,cols,headers,tablehtml,backgroundcolour,width,i, j;e.preventDefault();// Hide the dialogue.this.getDialogue({focusAfterHide: null}).hide();caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION);borders = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERS);bordersize = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSIZE);bordercolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBORDERCOLOUR);borderstyle = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSTYLE);backgroundcolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBACKGROUNDCOLOUR);rows = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.ROWS);cols = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.COLUMNS);headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);width = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.WIDTH);// Set the selection.this.get('host').setSelection(this._currentSelection);// Note there are some spaces inserted in the cells and before and after, so that users have somewhere to click.var nl = "\n";var tableId = Y.guid();tablehtml = '<br/>' + nl + '<table id="' + tableId + '">' + nl;var captionstyle = '';if (captionposition.get('value')) {captionstyle = ' style="caption-side: ' + captionposition.get('value') + '"';}tablehtml += '<caption' + captionstyle + '>' + Y.Escape.html(caption.get('value')) + '</caption>' + nl;i = 0;if (headers.get('value') === 'columns' || headers.get('value') === 'both') {i = 1;tablehtml += '<thead>' + nl + '<tr>' + nl;for (j = 0; j < parseInt(cols.get('value'), 10); j++) {tablehtml += '<th scope="col"></th>' + nl;}tablehtml += '</tr>' + nl + '</thead>' + nl;}tablehtml += '<tbody>' + nl;for (; i < parseInt(rows.get('value'), 10); i++) {tablehtml += '<tr>' + nl;for (j = 0; j < parseInt(cols.get('value'), 10); j++) {if (j === 0 && (headers.get('value') === 'rows' || headers.get('value') === 'both')) {tablehtml += '<th scope="row"></th>' + nl;} else {tablehtml += '<td ></td>' + nl;}}tablehtml += '</tr>' + nl;}tablehtml += '</tbody>' + nl;tablehtml += '</table>' + nl + '<br/>';this.get('host').insertContentAtFocusPoint(tablehtml);var tableNode = Y.one('#' + tableId);this._setAppearance(tableNode, {width: width,borders: borders,borderColour: bordercolour,borderSize: bordersize,borderStyle: borderstyle,backgroundColour: backgroundcolour});tableNode.removeAttribute('id');// Mark the content as updated.this.markUpdated();},/*** Search for all the cells in the current, next and previous columns.** @method _findColumnCells* @private* @return {Object} containing current, prev and next {Y.NodeList}s*/_findColumnCells: function() {var columnindex = this._getColumnIndex(this._lastTarget),rows = this._lastTarget.ancestor('table').all('tr'),currentcells = new Y.NodeList(),prevcells = new Y.NodeList(),nextcells = new Y.NodeList();rows.each(function(row) {var cells = row.all('td, th'),cell = cells.item(columnindex),cellprev = cells.item(columnindex - 1),cellnext = cells.item(columnindex + 1);currentcells.push(cell);if (cellprev) {prevcells.push(cellprev);}if (cellnext) {nextcells.push(cellnext);}});return {current: currentcells,prev: prevcells,next: nextcells};},/*** Hide the entries in the context menu that don't make sense with the* current selection.** @method _hideInvalidEntries* @param {Y.Node} node - The node containing the menu.* @private*/_hideInvalidEntries: function(node) {// Moving rows.var table = this._lastTarget.ancestor('table'),row = this._lastTarget.ancestor('tr'),rows = table.all('tr'),rowindex = rows.indexOf(row),prevrow = rows.item(rowindex - 1),prevrowhascells = prevrow ? prevrow.one('td') : null;if (!row || !prevrowhascells) {node.one('[data-change="moverowup"]').hide();} else {node.one('[data-change="moverowup"]').show();}var nextrow = rows.item(rowindex + 1),rowhascell = row ? row.one('td') : false;if (!row || !nextrow || !rowhascell) {node.one('[data-change="moverowdown"]').hide();} else {node.one('[data-change="moverowdown"]').show();}// Moving columns.var cells = this._findColumnCells();if (cells.prev.filter('td').size() > 0) {node.one('[data-change="movecolumnleft"]').show();} else {node.one('[data-change="movecolumnleft"]').hide();}var colhascell = cells.current.filter('td').size() > 0;if ((cells.next.size() > 0) && colhascell) {node.one('[data-change="movecolumnright"]').show();} else {node.one('[data-change="movecolumnright"]').hide();}// Delete colif (cells.current.filter('td').size() > 0) {node.one('[data-change="deletecolumn"]').show();} else {node.one('[data-change="deletecolumn"]').hide();}// Delete rowif (!row || !row.one('td')) {node.one('[data-change="deleterow"]').hide();} else {node.one('[data-change="deleterow"]').show();}},/*** Display the table menu.** @method _showTableMenu* @param {EventFacade} e* @param {String} buttonId The ID of the menu button associated with this menu.* @private*/_showTableMenu: function(e, buttonId) {e.preventDefault();var boundingBox;var creatorButton = this.buttons[this.name];if (!this._contextMenu) {this._menuOptions = [{text: M.util.get_string("addcolumnafter", COMPONENT),data: {change: "addcolumnafter"}}, {text: M.util.get_string("addrowafter", COMPONENT),data: {change: "addrowafter"}}, {text: M.util.get_string("moverowup", COMPONENT),data: {change: "moverowup"}}, {text: M.util.get_string("moverowdown", COMPONENT),data: {change: "moverowdown"}}, {text: M.util.get_string("movecolumnleft", COMPONENT),data: {change: "movecolumnleft"}}, {text: M.util.get_string("movecolumnright", COMPONENT),data: {change: "movecolumnright"}}, {text: M.util.get_string("deleterow", COMPONENT),data: {change: "deleterow"}}, {text: M.util.get_string("deletecolumn", COMPONENT),data: {change: "deletecolumn"}}, {text: M.util.get_string("edittable", COMPONENT),data: {change: "edittable"}}];creatorButton.insert(Y.Node.create('<div class="menuplaceholder" id="' + buttonId + '_menu"></div>'), 'after');this._contextMenu = new Y.M.editor_atto.Menu({items: this._menuOptions,buttonId: buttonId,attachmentPoint: '#' + buttonId + '_menu'});// Add event handlers for table control menus.boundingBox = this._contextMenu.get('boundingBox');boundingBox.delegate('click', this._handleTableChange, 'a', this);}boundingBox = this._contextMenu.get('boundingBox');// We store the cell of the last click (the control node is transient).this._lastTarget = e.tableCell.ancestor('.editor_atto_content td, .editor_atto_content th', true);this._hideInvalidEntries(boundingBox);// Clear the focusAfterHide for any other menus which may be open.Y.Array.each(this.get('host').openMenus, function(menu) {menu.set('focusAfterHide', null);});// Ensure that we focus on the button in the toolbar when we tab back to the menu.this.get('host')._setTabFocus(creatorButton);// Show the context menu, and align to the current position.this._contextMenu.show();this._contextMenu.align(this.buttons.table, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);this._contextMenu.set('focusAfterHide', creatorButton);// If there are any anchors in the bounding box, focus on the first.if (boundingBox.one('a')) {boundingBox.one('a').focus();}// Add this menu to the list of open menus.this.get('host').openMenus = [this._contextMenu];},/*** Handle a selection from the table control menu.** @method _handleTableChange* @param {EventFacade} e* @private*/_handleTableChange: function(e) {e.preventDefault();this._contextMenu.set('focusAfterHide', this.get('host').editor);// Hide the context menu.this._contextMenu.hide(e);// Make our changes.switch (e.target.getData('change')) {case 'addcolumnafter':this._addColumnAfter();break;case 'addrowafter':this._addRowAfter();break;case 'deleterow':this._deleteRow();break;case 'deletecolumn':this._deleteColumn();break;case 'edittable':this._editTable();break;case 'moverowdown':this._moveRowDown();break;case 'moverowup':this._moveRowUp();break;case 'movecolumnleft':this._moveColumnLeft();break;case 'movecolumnright':this._moveColumnRight();break;}},/*** Determine the index of a row in a table column.** @method _getRowIndex* @param {Node} cell* @private*/_getRowIndex: function(cell) {var tablenode = cell.ancestor('table'),rownode = cell.ancestor('tr');if (!tablenode || !rownode) {return;}var rows = tablenode.all('tr');return rows.indexOf(rownode);},/*** Determine the index of a column in a table row.** @method _getColumnIndex* @param {Node} cellnode* @private*/_getColumnIndex: function(cellnode) {var rownode = cellnode.ancestor('tr');if (!rownode) {return;}var cells = rownode.all('td, th');return cells.indexOf(cellnode);},/*** Delete the current row.** @method _deleteRow* @private*/_deleteRow: function() {var row = this._lastTarget.ancestor('tr');if (row && row.one('td')) {// Only delete rows with at least one non-header cell.row.remove(true);}// Clean the HTML.this.markUpdated();},/*** Move row up** @method _moveRowUp* @private*/_moveRowUp: function() {var row = this._lastTarget.ancestor('tr'),prevrow = row.previous('tr');if (!row || !prevrow) {return;}row.swap(prevrow);// Clean the HTML.this.markUpdated();},/*** Move column left** @method _moveColumnLeft* @private*/_moveColumnLeft: function() {var cells = this._findColumnCells();if (cells.current.size() > 0 && cells.prev.size() > 0 && cells.current.size() === cells.prev.size()) {var i = 0;for (i = 0; i < cells.current.size(); i++) {var cell = cells.current.item(i),prevcell = cells.prev.item(i);cell.swap(prevcell);}}// Cleanup.this.markUpdated();},/*** Add a caption to the table if it doesn't have one.** @method _addCaption* @private*/_addCaption: function() {var table = this._lastTarget.ancestor('table'),caption = table.one('caption');if (!caption) {table.insert(Y.Node.create('<caption> </caption>'), 1);}},/*** Remove a caption from the table if has one.** @method _removeCaption* @private*/_removeCaption: function() {var table = this._lastTarget.ancestor('table'),caption = table.one('caption');if (caption) {caption.remove(true);}},/*** Move column right.** @method _moveColumnRight* @private*/_moveColumnRight: function() {var cells = this._findColumnCells();// Check we have some tds in this column, and one exists to the right.if ((cells.next.size() > 0) &&(cells.current.size() === cells.next.size()) &&(cells.current.filter('td').size() > 0)) {var i = 0;for (i = 0; i < cells.current.size(); i++) {var cell = cells.current.item(i),nextcell = cells.next.item(i);cell.swap(nextcell);}}// Cleanup.this.markUpdated();},/*** Move row down.** @method _moveRowDown* @private*/_moveRowDown: function() {var row = this._lastTarget.ancestor('tr'),nextrow = row.next('tr');if (!row || !nextrow || !row.one('td')) {return;}row.swap(nextrow);// Clean the HTML.this.markUpdated();},/*** Obtain values for the table borders** @method _getBorderConfiguration* @param {Node} node* @private* @return {Array} or {Boolean} Returns the settings, if presents, or else returns false*/_getBorderConfiguration: function(node) {// We need to make a clone of the node in order to avoid grabbing any// of the computed styles from the DOM. We only want inline styles set by us.var shadowNode = node.cloneNode(true);var borderStyle = shadowNode.getStyle('borderStyle'),borderColor = shadowNode.getStyle('borderColor'),borderWidth = shadowNode.getStyle('borderWidth');if (borderStyle || borderColor || borderWidth) {var hexColour = Y.Color.toHex(borderColor);var width = parseInt(borderWidth, 10);return {borderStyle: borderStyle,borderColor: hexColour === "#" ? null : hexColour,borderWidth: isNaN(width) ? null : width};}return false;},/*** Set the appropriate styles on the given table node according to* the provided configuration.** @method _setAppearance* @param {Node} The table node to be modified.* @param {Object} Configuration object (associative array) containing the form nodes for* border styling.* @private*/_setAppearance: function(tableNode, configuration) {var borderhex,borderSizeValue,borderStyleValue,backgroundcolourvalue;if (configuration.borderColour) {borderhex = configuration.borderColour.get('value');}if (configuration.borderSize) {borderSizeValue = configuration.borderSize.get('value');}if (configuration.borderStyle) {borderStyleValue = configuration.borderStyle.get('value');}if (configuration.backgroundColour) {backgroundcolourvalue = configuration.backgroundColour.get('value');}// Clear the inline border stylingtableNode.removeAttribute('style');tableNode.all('td, th').each(function(cell) {cell.removeAttribute('style');}, this);if (configuration.borders) {if (configuration.borders.get('value') === 'outer') {tableNode.setStyle('borderWidth', borderSizeValue + CSS.BORDERSIZEUNIT);tableNode.setStyle('borderStyle', borderStyleValue);if (borderhex !== 'none') {tableNode.setStyle('borderColor', borderhex);}} else if (configuration.borders.get('value') === 'all') {tableNode.all('td, th').each(function(cell) {cell.setStyle('borderWidth', borderSizeValue + CSS.BORDERSIZEUNIT);cell.setStyle('borderStyle', borderStyleValue);if (borderhex !== 'none') {cell.setStyle('borderColor', borderhex);}}, this);}}if (backgroundcolourvalue !== 'none') {tableNode.setStyle('backgroundColor', backgroundcolourvalue);}if (configuration.width && configuration.width.get('value')) {tableNode.setStyle('width', configuration.width.get('value') + CSS.WIDTHUNIT);}},/*** Edit table (show the dialogue).** @method _editTable* @private*/_editTable: function() {var dialogue = this.getDialogue({headerContent: M.util.get_string('edittable', COMPONENT),focusAfterHide: false,focusOnShowSelector: SELECTORS.CAPTION,width: DIALOGUE.WIDTH});// Set the dialogue content, and then show the dialogue.var node = this._getDialogueContent(true),captioninput = node.one(SELECTORS.CAPTION),captionpositioninput = node.one(SELECTORS.CAPTIONPOSITION),headersinput = node.one(SELECTORS.HEADERS),borderinput = node.one(SELECTORS.BORDERS),borderstyle = node.one(SELECTORS.BORDERSTYLE),bordercolours = node.all(SELECTORS.BORDERCOLOURS),bordersize = node.one(SELECTORS.BORDERSIZE),backgroundcolours = node.all(SELECTORS.BACKGROUNDCOLOURS),width = node.one(SELECTORS.WIDTH),table = this._lastTarget.ancestor('table'),captionnode = table.one('caption'),hexColour,matchedInput;if (captionnode) {captioninput.set('value', captionnode.getHTML());} else {captioninput.set('value', '');}if (width && table.getStyle('width').indexOf('px') === -1) {width.set('value', parseInt(table.getStyle('width'), 10));}if (captionpositioninput && captionnode && captionnode.getAttribute('style')) {captionpositioninput.set('value', captionnode.getStyle('caption-side'));} else {// Default to none.captionpositioninput.set('value', '');}if (table.getStyle('backgroundColor') && this.get('allowBackgroundColour')) {hexColour = Y.Color.toHex(table.getStyle('backgroundColor'));matchedInput = backgroundcolours.filter('[value="' + hexColour + '"]');if (matchedInput) {matchedInput.set("checked", true);}}if (this.get('allowBorders')) {var borderValue = 'default',borderConfiguration = this._getBorderConfiguration(table);if (borderConfiguration) {borderValue = 'outer';} else {borderConfiguration = this._getBorderConfiguration(table.one('td'));if (borderConfiguration) {borderValue = 'all';}}if (borderConfiguration) {var borderStyle = borderConfiguration.borderStyle || DEFAULT.BORDERSTYLE;var borderSize = borderConfiguration.borderWidth || DEFAULT.BORDERWIDTH;borderstyle.set('value', borderStyle);bordersize.set('value', borderSize);borderinput.set('value', borderValue);hexColour = borderConfiguration.borderColor;matchedInput = bordercolours.filter('[value="' + hexColour + '"]');if (matchedInput) {matchedInput.set("checked", true);}}}var headersvalue = 'columns';if (table.one('th[scope="row"]')) {headersvalue = 'rows';if (table.one('th[scope="col"]')) {headersvalue = 'both';}}headersinput.set('value', headersvalue);dialogue.set('bodyContent', node).show();this._updateAvailableSettings();},/*** Delete the current column.** @method _deleteColumn* @private*/_deleteColumn: function() {var columnindex = this._getColumnIndex(this._lastTarget),table = this._lastTarget.ancestor('table'),rows = table.all('tr'),columncells = new Y.NodeList(),hastd = false;rows.each(function(row) {var cells = row.all('td, th');var cell = cells.item(columnindex);if (cell.get('tagName') === 'TD') {hastd = true;}columncells.push(cell);});// Do not delete all the headers.if (hastd) {columncells.remove(true);}// Clean the HTML.this.markUpdated();},/*** Add a row after the current row.** @method _addRowAfter* @private*/_addRowAfter: function() {var target = this._lastTarget.ancestor('tr'),tablebody = this._lastTarget.ancestor('table').one('tbody');if (!tablebody) {// Not all tables have tbody.tablebody = this._lastTarget.ancestor('table');}var firstrow = tablebody.one('tr');if (!firstrow) {firstrow = this._lastTarget.ancestor('table').one('tr');}if (!firstrow) {// Table has no rows. Boo.return;}var newrow = firstrow.cloneNode(true);newrow.all('th, td').each(function(tablecell) {if (tablecell.get('tagName') === 'TH') {if (tablecell.getAttribute('scope') !== 'row') {var newcell = Y.Node.create('<td></td>');tablecell.replace(newcell);tablecell = newcell;}}tablecell.setHTML(' ');});if (target.ancestor('thead')) {target = firstrow;tablebody.insert(newrow, target);} else {target.insert(newrow, 'after');}// Clean the HTML.this.markUpdated();},/*** Add a column after the current column.** @method _addColumnAfter* @private*/_addColumnAfter: function() {var cells = this._findColumnCells(),before = true,clonecells = cells.next;if (cells.next.size() <= 0) {before = false;clonecells = cells.current;}Y.each(clonecells, function(cell) {var newcell = cell.cloneNode();// Clear the content of the cell.newcell.setHTML(' ');if (before) {cell.get('parentNode').insert(newcell, cell);} else {cell.get('parentNode').insert(newcell, cell);cell.swap(newcell);}}, this);// Clean the HTML.this.markUpdated();}}, {ATTRS: {/*** Whether or not to allow borders** @attribute allowBorder* @type Boolean*/allowBorders: {value: true},/*** What border styles to allow** @attribute borderStyles* @type Array*/borderStyles: {value: ['none','solid','dashed','dotted']},/*** Whether or not to allow colourizing the background** @attribute allowBackgroundColour* @type Boolean*/allowBackgroundColour: {value: true},/*** Whether or not to allow setting the table width** @attribute allowWidth* @type Boolean*/allowWidth: {value: true},/*** Whether we allow styling* @attribute allowStyling* @type Boolean*/allowStyling: {readOnly: true,getter: function() {return this.get('allowBorders') || this.get('allowBackgroundColour') || this.get('allowWidth');}},/*** Available colors* @attribute availableColors* @type Array*/availableColors: {value: ['#FFFFFF','#EF4540','#FFCF35','#98CA3E','#7D9FD3','#333333'],readOnly: true}}});