(function() {
'use strict';
var IE_NON_DIRECTLY_EDITABLE_ELEMENT = {
'table': 1,
'col': 1,
'colgroup': 1,
'tbody': 1,
'td': 1,
'tfoot': 1,
'th': 1,
'thead': 1,
'tr': 1
};
/**
* Table class utility. Provides methods for create, delete and update tables.
*
* @class CKEDITOR.Table
* @constructor
* @param {Object} editor The CKEditor instance.
*/
function Table(editor) {
this._editor = editor;
}
Table.HEADING_BOTH = 'Both';
Table.HEADING_COL = 'Column';
Table.HEADING_NONE = 'None';
Table.HEADING_ROW = 'Row';
Table.prototype = {
constructor: Table,
/**
* Creates a table.
*
* @method create
* @param {Object} config Table configuration object
* @return {Object} The created table
*/
create: function(config) {
var editor = this._editor;
var table = this._createElement('table');
config = config || {};
// Generate the rows and cols.
var tbody = table.append(this._createElement('tbody'));
var rows = config.rows || 1;
var cols = config.cols || 1;
for (var i = 0; i < rows; i++) {
var row = tbody.append(this._createElement('tr'));
for (var j = 0; j < cols; j++) {
var cell = row.append(this._createElement('td'));
cell.appendBogus();
}
}
this.setAttributes(table, config.attrs);
this.setHeading(table, config.heading);
// Insert the table element if we're creating one.
editor.insertElement(table);
var firstCell = new CKEDITOR.dom.element(table.$.rows[0].cells[0]);
var range = editor.createRange();
range.moveToPosition(firstCell, CKEDITOR.POSITION_AFTER_START);
range.select();
return table;
},
/**
* Retrieves a table from the current selection.
*
* @method getFromSelection
* @return {CKEDITOR.dom.element} The retrieved table or null if not found.
*/
getFromSelection: function() {
var table;
var selection = this._editor.getSelection();
var selected = selection.getSelectedElement();
if (selected && selected.is('table')) {
table = selected;
} else {
var ranges = selection.getRanges();
if (ranges.length > 0) {
// Webkit could report the following range on cell selection (#4948):
// <table><tr><td>[ </td></tr></table>]
/* istanbul ignore else */
if (CKEDITOR.env.webkit) {
ranges[0].shrink(CKEDITOR.NODE_ELEMENT);
}
table = this._editor.elementPath(ranges[0].getCommonAncestor(true)).contains('table', 1);
}
}
return table;
},
/**
* Checks if a given table can be considered as editable. This method
* workarounds a limitation of IE where for some elements (like table),
* `isContentEditable` returns always false. This is because IE does not support
* `contenteditable` on such elements. However, despite such elements
* cannot be set as content editable directly, a content editable SPAN,
* or DIV element can be placed inside the individual table cells.
* See https://msdn.microsoft.com/en-us/library/ms537837%28v=VS.85%29.aspx
*
* @method isEditable
* @param {CKEDITOR.dom.element} el The table element to test if editable
* @return {Boolean}
*/
isEditable: function (el) {
if (!CKEDITOR.env.ie || !el.is(IE_NON_DIRECTLY_EDITABLE_ELEMENT)) {
return !el.isReadOnly();
}
if (el.hasAttribute('contenteditable')) {
return (el.getAttribute('contenteditable') !== 'false');
}
return this.isEditable(el.getParent());
},
/**
* Returns which heading style is set for the given table.
*
* @method getHeading
* @param {CKEDITOR.dom.element} table The table to gather the heading from. If null, it will be retrieved from the current selection.
* @return {String} The heading of the table. Expected values are `CKEDITOR.Table.NONE`, `CKEDITOR.Table.ROW`, `CKEDITOR.Table.COL` and `CKEDITOR.Table.BOTH`.
*/
getHeading: function(table) {
table = table || this.getFromSelection();
if (!table) {
return null;
}
var rowHeadingSettings = table.$.tHead !== null;
var colHeadingSettings = true;
// Check if all of the first cells in every row are TH
for (var row = 0; row < table.$.rows.length; row++) {
// If just one cell isn't a TH then it isn't a header column
var cell = table.$.rows[row].cells[0];
if (cell && cell.nodeName.toLowerCase() !== 'th') {
colHeadingSettings = false;
break;
}
}
var headingSettings = Table.HEADING_NONE;
if (rowHeadingSettings) {
headingSettings = Table.HEADING_ROW;
}
if (colHeadingSettings) {
headingSettings = (headingSettings === Table.HEADING_ROW ? Table.HEADING_BOTH : Table.HEADING_COL);
}
return headingSettings;
},
/**
* Removes a table from the editor.
*
* @method remove
* @param {CKEDITOR.dom.element} table The table element which table style should be removed.
*/
remove: function(table) {
var editor = this._editor;
if (table) {
table.remove();
} else {
table = editor.elementPath().contains('table', 1);
if (table) {
// If the table's parent has only one child remove it as well (unless it's a table cell, or the editable element) (#5416, #6289, #12110)
var parent = table.getParent();
var editable = editor.editable();
if (parent.getChildCount() === 1 && !parent.is('td', 'th') && !parent.equals(editable)) {
table = parent;
}
var range = editor.createRange();
range.moveToPosition(table, CKEDITOR.POSITION_BEFORE_START);
table.remove();
range.select();
}
}
},
/**
* Assigns provided attributes to a table.
*
* @method setAttributes
* @param {Object} table The table to which the attributes should be assigned
* @param {Object} attrs The attributes which have to be assigned to the table
*/
setAttributes: function(table, attrs) {
if (attrs) {
Object.keys(attrs).forEach(function(attr) {
table.setAttribute(attr, attrs[attr]);
});
}
},
/**
* Sets the appropriate table heading style to a table.
*
* @method setHeading
* @param {CKEDITOR.dom.element} table The table element to which the heading should be set. If null, it will be retrieved from the current selection.
* @param {String} heading The table heading to be set. Accepted values are: `CKEDITOR.Table.NONE`, `CKEDITOR.Table.ROW`, `CKEDITOR.Table.COL` and `CKEDITOR.Table.BOTH`.
*/
setHeading: function(table, heading) {
table = table || this.getFromSelection();
var i, newCell;
var tableHead;
var tableBody = table.getElementsByTag('tbody').getItem(0);
var tableHeading = this.getHeading(table);
var hadColHeading = (tableHeading === Table.HEADING_COL || tableHeading === Table.HEADING_BOTH);
var needColHeading = heading === Table.HEADING_COL || heading === Table.HEADING_BOTH;
var needRowHeading = heading === Table.HEADING_ROW || heading === Table.HEADING_BOTH;
// If we need row heading and don't have a <thead> element yet, move the
// first row of the table to the head and convert the nodes to <th> ones.
if (!table.$.tHead && needRowHeading) {
var tableFirstRow = tableBody.getElementsByTag('tr').getItem(0);
var tableFirstRowChildCount = tableFirstRow.getChildCount();
// Change TD to TH:
for (i = 0; i < tableFirstRowChildCount; i++) {
var cell = tableFirstRow.getChild(i);
// Skip bookmark nodes. (#6155)
if (cell.type === CKEDITOR.NODE_ELEMENT && !cell.data('cke-bookmark')) {
cell.renameNode('th');
cell.setAttribute('scope', 'col');
}
}
tableHead = this._createElement(table.$.createTHead());
tableHead.append(tableFirstRow.remove());
}
// If we don't need row heading and we have a <thead> element, move the
// row out of there and into the <tbody> element.
if (table.$.tHead !== null && !needRowHeading) {
// Move the row out of the THead and put it in the TBody:
tableHead = this._createElement(table.$.tHead);
var previousFirstRow = tableBody.getFirst();
while (tableHead.getChildCount() > 0) {
var newFirstRow = tableHead.getFirst();
var newFirstRowChildCount = newFirstRow.getChildCount();
for (i = 0; i < newFirstRowChildCount; i++) {
newCell = newFirstRow.getChild(i);
if (newCell.type === CKEDITOR.NODE_ELEMENT) {
newCell.renameNode('td');
newCell.removeAttribute('scope');
}
}
newFirstRow.insertBefore(previousFirstRow);
}
tableHead.remove();
}
tableHeading = this.getHeading(table);
var hasColHeading = (tableHeading === Table.HEADING_COL || tableHeading === Table.HEADING_BOTH);
// If we need column heading and the table doesn't have it, convert every first cell in
// every row into a `<th scope="row">` element.
if (!hasColHeading && needColHeading) {
for (i = 0; i < table.$.rows.length; i++) {
if (table.$.rows[i].cells[0].nodeName.toLowerCase() !== 'th') {
newCell = new CKEDITOR.dom.element(table.$.rows[i].cells[0]);
newCell.renameNode('th');
newCell.setAttribute('scope', 'row');
}
}
}
// If we don't need column heading but the table has it, convert every first cell in every
// row back into a `<td>` element.
if (hadColHeading && !needColHeading) {
for (i = 0; i < table.$.rows.length; i++) {
var row = new CKEDITOR.dom.element(table.$.rows[i]);
if (row.getParent().getName() === 'tbody') {
newCell = new CKEDITOR.dom.element(row.$.cells[0]);
newCell.renameNode('td');
newCell.removeAttribute('scope');
}
}
}
},
/**
* Creates a new CKEDITOR.dom.element using the passed tag name.
*
* @protected
* @method _createElement
* @param {String} name The tag name from which an element should be created
* @return {CKEDITOR.dom.element} Instance of CKEDITOR DOM element class
*/
_createElement: function(name) {
return new CKEDITOR.dom.element(name, this._editor.document);
}
};
CKEDITOR.on('instanceReady', function(event) {
var headingCommands = [Table.HEADING_NONE, Table.HEADING_ROW, Table.HEADING_COL, Table.HEADING_BOTH];
var tableUtils = new Table(event.editor);
headingCommands.forEach(function(heading) {
event.editor.addCommand('tableHeading' + heading, {
exec: function(editor) {
tableUtils.setHeading(null, heading);
}
});
});
});
CKEDITOR.Table = CKEDITOR.Table || Table;
}());