YUI.add('moodle-atto_table-button', function (Y, NAME) {
// 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 .
/**
* @package atto_table
* @copyright 2013 Damyon Wiese
* @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 = '' +
'',
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() {
this.addButton({
icon: 'e/table',
callback: this._displayTableEditor,
tags: 'table'
});
// Disable mozilla table controls.
if (Y.UA.gecko) {
document.execCommand("enableInlineTableEditing", false, false);
document.execCommand("enableObjectResizing", false, false);
}
},
/**
* 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();
if (cell) {
// Add the cell to the EventFacade to save duplication in when showing the menu.
e.tableCell = cell;
return this._showTableMenu(e);
}
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) {
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');
host.getSelectedNodes().some(function(node) {
if (node.ancestor('td, th, caption', true, this._stopAtContentEditableFilter)) {
targetcell = node;
var caption = node.ancestor('caption', true, this._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('
');
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 = ' ' + nl + '