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 col
if (cells.current.filter('td').size() > 0) {
node.one('[data-change="deletecolumn"]').show();
} else {
node.one('[data-change="deletecolumn"]').hide();
}
// Delete row
if (!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 styling
tableNode.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
}
}
});