jexcel
Version:
jExcel is a lightweight, vanilla javascript plugin to create amazing web-based interactive tables and spreadsheets compatible with Excel, Google Spreadsheets and any other spreadsheet software.
1,333 lines (1,210 loc) • 430 kB
JavaScript
/**
* (c) jExcel v3.0.1
*
* Author: Paul Hodel <paul.hodel@gmail.com>
* Website: https://bossanova.uk/jexcel/
* Description: Create amazing web based spreadsheets.
*
* This software is distribute under MIT License
*/
var jexcel = (function(el, options) {
// Create jexcel object
var obj = {};
obj.options = {};
// Loading default configuration
var defaults = {
// External data
url:null,
// Data
data:[[]],
// Rows and columns definitions
rows:[],
columns:[],
// Deprected legacy options
colHeaders:[],
colWidths:[],
colAlignments:[],
nestedHeaders:null,
// Column width that is used by default
defaultColWidth:50,
// Spare rows and columns
minSpareRows:0,
minSpareCols:0,
// Minimal table dimensions
minDimensions:[0,0],
// Allow column sorting
columnSorting:true,
// Allow column resizing
columnDrag:false,
// Allow column resizing
columnResize:true,
// Allow row resizing
rowResize:false,
// Allow row dragging
rowDrag:true,
// Allow table edition
editable:true,
// Allow new rows
allowInsertRow:true,
// Allow new rows
allowManualInsertRow:true,
// Allow new columns
allowInsertColumn:true,
// Allow new rows
allowManualInsertColumn:true,
// Allow row delete
allowDeleteRow:true,
// Allow column delete
allowDeleteColumn:true,
// Allow rename column
allowRenameColumn:true,
// Allow comments
allowComments:false,
// Global wrap
wordWrap:false,
// CSV source
csv:null,
// Filename
csvFileName:'jexcel',
// Consider first line as header
csvHeaders:true,
// Delimiters
csvDelimiter:',',
// Disable corner selection
selectionCopy:true,
// Merged cells
mergeCells:[],
// Create toolbar
toolbar:null,
// Allow search
search:false,
// Create pagination
pagination:false,
paginationOptions:null,
// Full screen
fullscreen:false,
// Lazy loading
lazyLoading:false,
loadingSpin:false,
// Table overflow
tableOverflow:false,
tableHeight:'300px',
tableWidth:null,
// Style
style:null,
// Event handles
onload:null,
onchange:null,
onbeforechange:null,
oninsertrow:null,
oninsertcolumn:null,
ondeleterow:null,
ondeletecolumn:null,
onmoverow:null,
onmovecolumn:null,
onresizerow:null,
onresizecolumn:null,
onsort:null,
onselection:null,
onpaste:null,
onmerge:null,
onfocus:null,
onblur:null,
// Customize any cell behavior
updateTable:null,
// Texts
text:{
noRecordsFound: 'No records found',
showingPage: 'Showing page {0} of {1} entries',
show: 'Show ',
entries: ' entries',
insertANewColumnBefore: 'Insert a new column before',
insertANewColumnAfter: 'Insert a new column after',
deleteSelectedColumns: 'Delete selected columns',
renameThisColumn: 'Rename this column',
orderAscending: 'Order ascending',
orderDescending: 'Order descending',
insertANewRowBefore: 'Insert a new row before',
insertANewRowAfter: 'Insert a new row after',
deleteSelectedRows: 'Delete selected rows',
editComments: 'Edit comments',
addComments: 'Add comments',
comments: 'Comments',
clearComments: 'Clear comments',
copy: 'Copy...',
paste: 'Paste...',
saveAs: 'Save as...',
about: 'About',
areYouSureToDeleteTheSelectedRows: 'Are you sure to delete the selected rows?',
areYouSureToDeleteTheSelectedColumns: 'Are you sure to delete the selected columns?',
thisActionWillDestroyAnyExistingMergedCellsAreYouSure: 'This action will destroy any existing merged cells. Are you sure?',
thisActionWillClearYourSearchResultsAreYouSure: 'This action will clear your search results. Are you sure?',
thereIsAConflictWithAnotherMergedCell: 'There is a conflict with another merged cell',
invalidMergeProperties: 'Invalid merged properties',
cellAlreadyMerged: 'Cell already merged',
noCellsSelected: 'No cells selected',
},
// About message
about:"jExcel Pro Spreadsheet\nVersion 3.0.1\nAuthor: Paul Hodel <paul.hodel@gmail.com>\nWebsite: https://jexcel.net/v3",
};
// Loading initial configuration from user
for (var property in defaults) {
if (options.hasOwnProperty(property)) {
obj.options[property] = options[property];
} else {
obj.options[property] = defaults[property];
}
}
// Global elements
obj.el = el;
obj.corner = null;
obj.contextMenu = null;
obj.textarea = null;
obj.ads = null;
obj.content = null;
obj.table = null;
obj.thead = null;
obj.tbody = null;
obj.rows = [];
obj.results = null;
obj.searchInput = null;
obj.toolbar = null;
obj.pagination = null;
obj.pageNumber = null;
obj.headerContainer = null;
obj.colgroupContainer = null;
// Containers
obj.headers = [];
obj.records = [];
obj.history = [];
obj.formula = [];
obj.formulaStack = 0;
obj.colgroup = [];
obj.selection = [];
obj.highlighted = [];
obj.selectedCell = null;
obj.selectedContainer = null;
obj.style = [];
obj.meta = [];
// Internal controllers
obj.cursor = null;
obj.historyIndex = -1;
obj.ignoreEvents = false;
obj.ignoreHistory = false;
obj.edition = null;
obj.hashString = null;
obj.resizing = null;
obj.dragging = null;
// Lazy loading
if (obj.options.lazyLoading == true && (obj.options.tableOverflow == false && obj.options.fullscreen == false)) {
console.error('JEXCEL: The lazyloading only works when tableOverflow = yes or fullscreen = yes');
obj.options.lazyLoading = false;
}
/**
* Prepare the jexcel table
*
* @Param config
*/
obj.prepareTable = function() {
// Loading initial data from remote sources
var results = [];
// Number of columns
var size = obj.options.columns.length;
if (obj.options.data[0].length > size) {
size = obj.options.data[0].length;
}
// Minimal dimensions
if (obj.options.minDimensions[0] > size) {
size = obj.options.minDimensions[0];
}
// Requests
var requests = [];
var requestsIndex = [];
// Preparations
for (var i = 0; i < size; i++) {
// Deprected options. You should use only columns
if (! obj.options.colHeaders[i]) {
obj.options.colHeaders[i] = '';
}
if (! obj.options.colWidths[i]) {
obj.options.colWidths[i] = obj.options.defaultColWidth || '50';
}
if (! obj.options.colAlignments[i]) {
obj.options.colAlignments[i] = 'center';
}
// Default column description
if (! obj.options.columns[i]) {
obj.options.columns[i] = { type:'text' };
} else if (! obj.options.columns[i]) {
obj.options.columns[i].type = 'text';
}
if (! obj.options.columns[i].source) {
obj.options.columns[i].source = [];
}
if (! obj.options.columns[i].options) {
obj.options.columns[i].options = [];
}
if (! obj.options.columns[i].editor) {
obj.options.columns[i].editor = null;
}
if (! obj.options.columns[i].allowEmpty) {
obj.options.columns[i].allowEmpty = false;
}
if (! obj.options.columns[i].title) {
obj.options.columns[i].title = obj.options.colHeaders[i] ? obj.options.colHeaders[i] : '';
}
if (! obj.options.columns[i].width) {
obj.options.columns[i].width = obj.options.colWidths[i] ? obj.options.colWidths[i] : '50';
}
if (! obj.options.columns[i].align) {
obj.options.columns[i].align = obj.options.colAlignments[i] ? obj.options.colAlignments[i] : 'center';
}
// Pre-load initial source for json autocomplete
if (obj.options.columns[i].type == 'autocomplete' || obj.options.columns[i].type == 'dropdown') {
// if remote content
if (obj.options.columns[i].url) {
requestsIndex.push(i);
requests.push(fetch(obj.options.columns[i].url, { headers: new Headers({ 'content-type': 'text/json' }) })
.then(function(data) {
return data.json();
}));
}
} else if (obj.options.columns[i].type == 'calendar') {
// Default format for date columns
if (! obj.options.columns[i].options.format) {
obj.options.columns[i].options.format = 'DD/MM/YYYY';
}
}
}
if (requests.length) {
Promise.all(requests).then(function(data) {
for (var i = 0; i < data.length; i++) {
obj.options.columns[i].source = data[i];
}
obj.createTable();
});
} else {
// Create table
obj.createTable();
}
}
obj.createTable = function() {
// Elements
obj.table = document.createElement('table');
obj.thead = document.createElement('thead');
obj.tbody = document.createElement('tbody');
// Create headers controllers
obj.headers = [];
obj.colgroup = [];
// Create table container
obj.content = document.createElement('div');
obj.content.classList.add('jexcel_content');
// Create toolbar object
obj.toolbar = document.createElement('div');
obj.toolbar.classList.add('jexcel_toolbar');
// Search
var searchContainer = document.createElement('div');
var searchText = document.createTextNode('Search: ');
obj.searchInput = document.createElement('input');
obj.searchInput.classList.add('jexcel_search');
searchContainer.appendChild(searchText);
searchContainer.appendChild(obj.searchInput);
obj.searchInput.onfocus = function() {
obj.resetSelection();
}
// Pagination select option
var paginationUpdateContainer = document.createElement('div');
if (obj.options.pagination > 0 && obj.options.paginationOptions && obj.options.paginationOptions.length > 0) {
obj.paginationDropdown = document.createElement('select');
obj.paginationDropdown.classList.add('jexcel_pagination_dropdown');
obj.paginationDropdown.onchange = function() {
obj.options.pagination = parseInt(this.value);
obj.page(0);
}
for (var i = 0; i < obj.options.paginationOptions.length; i++) {
var temp = document.createElement('option');
temp.value = obj.options.paginationOptions[i];
temp.innerHTML = obj.options.paginationOptions[i];
obj.paginationDropdown.appendChild(temp);
}
paginationUpdateContainer.appendChild(document.createTextNode(obj.options.text.show));
paginationUpdateContainer.appendChild(obj.paginationDropdown);
paginationUpdateContainer.appendChild(document.createTextNode(obj.options.text.entries));
}
// Filter and pagination container
obj.filter = document.createElement('div');
obj.filter.classList.add('jexcel_filter');
obj.filter.appendChild(paginationUpdateContainer);
obj.filter.appendChild(searchContainer);
// Colsgroup
obj.colgroupContainer = document.createElement('colgroup');
var tempCol = document.createElement('col');
tempCol.setAttribute('width', 50);
obj.colgroupContainer.appendChild(tempCol);
// Nested
if (obj.options.nestedHeaders && obj.options.nestedHeaders.length > 0) {
// Flexible way to handle nestedheaders
if (obj.options.nestedHeaders[0] && obj.options.nestedHeaders[0][0]) {
for (var j = 0; j < obj.options.nestedHeaders.length; j++) {
obj.thead.appendChild(obj.createNestedHeader(obj.options.nestedHeaders[j]));
}
} else {
obj.thead.appendChild(obj.createNestedHeader(obj.options.nestedHeaders));
}
}
// Row
obj.headerContainer = document.createElement('tr');
var tempCol = document.createElement('td');
obj.headerContainer.appendChild(tempCol);
for (var i = 0; i < obj.options.columns.length; i++) {
// Create header
obj.createCellHeader(i);
// Append cell to the container
obj.headerContainer.appendChild(obj.headers[i]);
obj.colgroupContainer.appendChild(obj.colgroup[i]);
}
obj.thead.appendChild(obj.headerContainer);
// Content table
obj.table = document.createElement('table');
obj.table.classList.add('jexcel');
obj.table.setAttribute('cellpadding', '0');
obj.table.setAttribute('cellspacing', '0');
obj.table.setAttribute('unselectable', 'yes');
obj.table.setAttribute('onselectstart', 'return false');
obj.table.appendChild(obj.colgroupContainer);
obj.table.appendChild(obj.thead);
obj.table.appendChild(obj.tbody);
// Spreasheet corner
obj.corner = document.createElement('div');
obj.corner.className = 'jexcel_corner';
obj.corner.setAttribute('unselectable', 'on');
obj.corner.setAttribute('onselectstart', 'return false');
if (obj.selectionCopy == false) {
obj.corner.style.display = 'none';
}
// Textarea helper
obj.textarea = document.createElement('textarea');
obj.textarea.className = 'jexcel_textarea';
obj.textarea.id = 'jexcel_textarea';
// Contextmenu container
obj.contextMenu = document.createElement('div');
obj.contextMenu.className = 'jexcel_contextmenu';
// Create element
jApp.contextmenu(obj.contextMenu, {
onclick:function() {
obj.contextMenu.contextmenu.close(false);
}
});
// Powered by jExcel
var ads = '<a href="https://bossanova.uk/jexcel/"><img src="//bossanova.uk/jexcel/logo.png">jExcel Spreadsheet</a>';
obj.ads = document.createElement('div');
obj.ads.className = 'jexcel_about';
if (typeof(sessionStorage) !== "undefined") {
if (! sessionStorage.getItem('jexcel')) {
sessionStorage.setItem('jexcel', true);
obj.ads.innerHTML = ads;
}
} else {
obj.ads.innerHTML = ads;
}
// Create table container TODO: frozen columns
var container = document.createElement('div');
container.classList.add('jexcel_table');
// Pagination
obj.pagination = document.createElement('div');
obj.pagination.classList.add('jexcel_pagination');
var paginationInfo = document.createElement('div');
var paginationPages = document.createElement('div');
obj.pagination.appendChild(paginationInfo);
obj.pagination.appendChild(paginationPages);
// Append containers to the table
if (obj.options.search == true) {
el.appendChild(obj.filter);
}
// Elements
obj.content.appendChild(obj.table);
obj.content.appendChild(obj.corner);
obj.content.appendChild(obj.textarea);
el.appendChild(obj.toolbar);
el.appendChild(obj.content);
el.appendChild(obj.pagination);
el.appendChild(obj.contextMenu);
el.appendChild(obj.ads);
el.classList.add('jexcel_container');
// Create toolbar
if (obj.options.toolbar && obj.options.toolbar.length) {
obj.createToolbar();
}
// Fullscreen
if (obj.options.fullscreen == true) {
el.classList.add('fullscreen');
if (obj.options.toolbar) {
el.classList.add('with-toolbar');
}
} else {
// Overflow
if (obj.options.tableOverflow == true) {
if (obj.options.tableHeight) {
obj.content.style['overflow-y'] = 'auto';
obj.content.style.height = obj.options.tableHeight;
}
if (obj.options.tableWidth) {
el.content.style['overflow-x'] = 'auto';
el.content.width = obj.options.tableWidth;
}
}
}
// Actions
if (obj.options.columnDrag == true) {
obj.thead.classList.add('draggable');
}
if (obj.options.columnResize == true) {
obj.thead.classList.add('resizable');
}
if (obj.options.rowDrag == true) {
obj.tbody.classList.add('draggable');
}
if (obj.options.rowResize == true) {
obj.tbody.classList.add('resizable');
}
// Load data
obj.setData();
// Style
if (obj.options.style) {
obj.setStyle(obj.options.style, null, null, 1, 1);
}
}
/**
* Set data
*
* @param array data In case no data is sent, default is reloaded
* @return void
*/
obj.setData = function(data) {
// Update data
if (data) {
if (typeof(data) == 'string') {
data = JSON.parse(data);
}
obj.options.data = data;
}
// Adjust minimal dimensions
var j = 0;
var i = 0;
var size_i = obj.options.columns.length;
var size_j = obj.options.data.length;
var min_i = obj.options.minDimensions[0];
var min_j = obj.options.minDimensions[1];
var max_i = min_i > size_i ? min_i : size_i;
var max_j = min_j > size_j ? min_j : size_j;
for (j = 0; j < max_j; j++) {
for (i = 0; i < max_i; i++) {
if (obj.options.data[j] == undefined) {
obj.options.data[j] = [];
}
if (obj.options.data[j][i] == undefined) {
obj.options.data[j][i] = '';
}
}
}
// Reset containers
obj.rows = [];
obj.results = null;
obj.records = [];
obj.history = [];
// Reset internal controllers
obj.historyIndex = -1;
// Reset data
obj.tbody.innerHTML = '';
// Lazy loading
if (obj.options.lazyLoading == true) {
// Load only 100 records
var startNumber = 0
var finalNumber = obj.options.data.length < 100 ? obj.options.data.length : 100;
if (obj.options.pagination) {
obj.options.pagination = false;
console.error('JEXCEL: Pagination will be disable due the lazyLoading');
}
} else if (obj.options.pagination) {
// Pagination
if (! obj.pageNumber) {
obj.pageNumber = 0;
}
var quantityPerPage = obj.options.pagination;
startNumber = (obj.options.pagination * obj.pageNumber);
finalNumber = (obj.options.pagination * obj.pageNumber) + obj.options.pagination;
if (obj.options.data.length < finalNumber) {
finalNumber = obj.options.data.length;
}
} else {
var startNumber = 0;
var finalNumber = obj.options.data.length;
}
// Append nodes to the HTML
for (j = 0; j < obj.options.data.length; j++) {
// Create row
var tr = obj.createRow(j, obj.options.data[j]);
// Append line to the table
if (j >= startNumber && j < finalNumber) {
obj.tbody.appendChild(tr);
}
}
if (obj.options.lazyLoading == true) {
// Do not create pagination with lazyloading activated
} else if (obj.options.pagination) {
obj.updatePagination();
}
// Merge cells
if (obj.options.mergeCells) {
var keys = Object.keys(obj.options.mergeCells);
for (var i = 0; i < keys.length; i++) {
var num = obj.options.mergeCells[keys[i]];
obj.setMerge(keys[i], num[0], num[1], 1);
}
}
// Updata table with custom configurations if applicable
obj.updateTable();
// Onload
if (! obj.ignoreEvents) {
if (typeof(obj.options.onload) == 'function') {
obj.options.onload(el);
}
}
}
/**
* Get the whole table data
*
* @param integer row number
* @return string value
*/
obj.getData = function(highlighted) {
// Control vars
var dataset = [];
var px = 0;
var py = 0;
// Column and row length
var x = obj.options.data[0].length
var y = obj.options.data.length
// Go through the columns to get the data
for (var j = 0; j < y; j++) {
px = 0;
for (var i = 0; i < x; i++) {
// Cell selected or fullset
if (! highlighted || obj.records[j][i].classList.contains('highlight')) {
// Get value
if (! dataset[py]) {
dataset[py] = [];
}
dataset[py][px] = obj.options.data[j][i];
px++;
}
}
if (px > 0) {
py++;
}
}
return dataset;
}
/**
* Get a row data by rowNumber
*/
obj.getRowData = function(rowNumber) {
return obj.options.data[rowNumber];
}
/**
* Get a column data by columnNumber
*/
obj.getColumnData = function(columnNumber) {
var dataset = [];
// Go through the rows to get the data
for (var j = 0; j < obj.options.data.length; j++) {
dataset.push(obj.options.data[j][columnNumber]);
}
return dataset;
}
/**
* Create row
*/
obj.createRow = function(j, data) {
// Create container
if (! obj.records[j]) {
obj.records[j] = [];
}
// New line of data to be append in the table
obj.rows[j] = document.createElement('tr');
obj.rows[j].setAttribute('data-y', j);
// Definitions
if (obj.options.rows[j]) {
if (obj.options.rows[j].height) {
obj.rows[j].style.height = obj.options.rows[j].height;
}
}
// Row number label
var td = document.createElement('td');
td.innerHTML = parseInt(j + 1);
td.setAttribute('data-y', j);
td.className = 'jexcel_row';
obj.rows[j].appendChild(td);
// Data columns
for (i = 0; i < obj.options.columns.length; i++) {
// New column of data to be append in the line
obj.records[j][i] = obj.createCell(i, j, data[i]);
// Add column to the row
obj.rows[j].appendChild(obj.records[j][i]);
}
// Add row to the table body
return obj.rows[j];
}
/**
* Create cell
*/
obj.createCell = function(i, j, value) {
// Create cell and properties
var td = document.createElement('td');
td.setAttribute('data-x', i);
td.setAttribute('data-y', j);
// Hidden column
if (obj.options.columns[i].type == 'hidden') {
td.style.display = 'none';
td.innerHTML = value;
} else if (obj.options.columns[i].type == 'checkbox' || obj.options.columns[i].type == 'radio') {
// Create input
var element = document.createElement('input');
element.type = obj.options.columns[i].type;
element.name = 'c' + i;
element.checked = (value == 1 || value == true || value == 'true') ? true : false;
element.onclick = function() {
obj.setValue(td, this.checked);
}
if (obj.options.columns[i].readOnly == true) {
element.setAttribute('disabled', 'disabled');
}
// Append to the table
td.appendChild(element);
// Make sure the values are correct
obj.options.data[j][i] = element.checked;
} else if (obj.options.columns[i].type == 'calendar') {
// Try formatted date
var formatted = jApp.calendar.extractDateFromString(value, obj.options.columns[i].options.format);
// Create calendar cell
td.innerHTML = jApp.calendar.getDateString(formatted ? formatted : value, obj.options.columns[i].options.format);
} else if (obj.options.columns[i].type == 'dropdown' || obj.options.columns[i].type == 'autocomplete') {
// Create dropdown cell
td.classList.add('dropdown');
td.innerHTML = obj.getDropDownValue(i, value);
} else if (obj.options.columns[i].type == 'color') {
if (obj.options.columns[i].render == 'square') {
var color = document.createElement('div');
color.className = 'color';
color.style.backgroundColor = value;
td.appendChild(color);
} else {
td.style.color = value;
td.innerHTML = value;
}
} else if (obj.options.columns[i].type == 'image') {
if (value && value.substr(0, 10) == 'data:image') {
var img = document.createElement('img');
img.src = value;
td.appendChild(img);
}
} else {
if ((''+value).substr(0,1) == '=') {
value = obj.executeFormula(value, i, j)
}
if (obj.options.columns[i].mask) {
var decimal = obj.options.columns[i].decimal || '.';
value = '' + jApp.mask.run(value, obj.options.columns[i].mask, decimal);
}
td.innerHTML = value;
}
// Readonly
if (obj.options.columns[i].readOnly == true) {
td.className = 'readonly';
}
// Text align
var colAlign = obj.options.columns[i].align ? obj.options.columns[i].align : 'center';
td.style.textAlign = colAlign;
// Wrap option
if (obj.options.wordWrap == true || obj.options.columns[i].wordWrap == true || td.innerHTML.length > 200) {
td.style.whiteSpace = 'pre-wrap';
}
// Overflow
if (i > 0) {
if (value || td.innerHTML) {
obj.records[j][i-1].style.overflow = 'hidden';
} else {
if (i == obj.options.columns.length - 1) {
td.style.overflow = 'hidden';
}
}
}
return td;
}
obj.createCellHeader = function(colNumber) {
// Create col global control
var colWidth = obj.options.columns[colNumber].width ? obj.options.columns[colNumber].width : obj.options.defaultColWidth;
var colAlign = obj.options.columns[colNumber].align ? obj.options.columns[colNumber].align : 'center';
// Create header cell
obj.headers[colNumber] = document.createElement('td');
obj.headers[colNumber].innerHTML = obj.options.columns[colNumber].title ? obj.options.columns[colNumber].title : jexcel.getColumnName(colNumber);
obj.headers[colNumber].setAttribute('data-x', colNumber);
obj.headers[colNumber].style.textAlign = colAlign;
if (obj.options.columns[colNumber].title) {
obj.headers[colNumber].setAttribute('title', obj.options.columns[colNumber].title);
}
// Width control
obj.colgroup[colNumber] = document.createElement('col');
obj.colgroup[colNumber].setAttribute('width', colWidth);
// Hidden column
if (obj.options.columns[colNumber].type == 'hidden') {
obj.headers[colNumber].style.display = 'none';
obj.colgroup[colNumber].style.display = 'none';
}
}
obj.createNestedHeader = function(nestedInformation) {
var tr = document.createElement('tr');
tr.classList.add('jexcel_nested');
var td = document.createElement('td');
tr.appendChild(td);
var headerIndex = 0;
for (var i = 0; i < nestedInformation.length; i++) {
// Default values
if (! nestedInformation[i].colspan) {
nestedInformation[i].colspan = 1;
}
if (! nestedInformation[i].align) {
nestedInformation[i].align = 'center';
}
if (! nestedInformation[i].title) {
nestedInformation[i].title = '';
}
// Classes container
var column = [];
// Header classes for this cell
for (var x = 0; x < nestedInformation[i].colspan; x++) {
column.push(headerIndex);
headerIndex++;
}
// Created the nested cell
var td = document.createElement('td');
td.setAttribute('data-column', column.join(','));
td.setAttribute('colspan', nestedInformation[i].colspan);
td.setAttribute('align', nestedInformation[i].align);
td.innerHTML = nestedInformation[i].title;
tr.appendChild(td);
}
return tr;
}
/**
* Create toolbar
*/
obj.createToolbar = function(toolbar) {
if (toolbar) {
obj.options.toolbar = toolbar;
} else {
var toolbar = obj.options.toolbar;
}
for (var i = 0; i < toolbar.length; i++) {
if (toolbar[i].type == 'i') {
var toolbarItem = document.createElement('i');
toolbarItem.classList.add('jexcel_toolbar_item');
toolbarItem.classList.add('material-icons');
toolbarItem.setAttribute('data-k', toolbar[i].k);
toolbarItem.setAttribute('data-v', toolbar[i].v);
// Handle click
if (toolbar[i].onclick && typeof(toolbar[i].onclick)) {
toolbarItem.onclick = toolbar[i].onclick;
} else {
toolbarItem.onclick = function() {
var k = this.getAttribute('data-k');
var v = this.getAttribute('data-v');
obj.setStyle(obj.highlighted, k, v);
}
}
// Append element
toolbarItem.innerHTML = toolbar[i].content;
obj.toolbar.appendChild(toolbarItem);
} else if (toolbar[i].type == 'select') {
var toolbarItem = document.createElement('select');
toolbarItem.classList.add('jexcel_toolbar_item');
toolbarItem.setAttribute('data-k', toolbar[i].k);
// Handle onchange
if (toolbar[i].onchange && typeof(toolbar[i].onchange)) {
toolbarItem.onchange = toolbar[i].onchange;
} else {
toolbarItem.onchange = function() {
var k = this.getAttribute('data-k');
obj.setStyle(obj.highlighted, k, this.value);
}
}
// Add options to the dropdown
for(var j = 0; j < toolbar[i].v.length; j++) {
var toolbarDropdownOption = document.createElement('option');
toolbarDropdownOption.value = toolbar[i].v[j];
toolbarDropdownOption.innerHTML = toolbar[i].v[j];
toolbarItem.appendChild(toolbarDropdownOption);
}
obj.toolbar.appendChild(toolbarItem);
} else if (toolbar[i].type == 'color') {
var toolbarItem = document.createElement('i');
toolbarItem.classList.add('jexcel_toolbar_item');
toolbarItem.classList.add('material-icons');
toolbarItem.setAttribute('data-k', toolbar[i].k);
toolbarItem.setAttribute('data-v', '');
obj.toolbar.appendChild(toolbarItem);
toolbarItem.onclick = function() {
this.color.open();
}
toolbarItem.innerHTML = toolbar[i].content;
jApp.color(toolbarItem, {
onchange:function(o, v) {
var k = o.getAttribute('data-k');
obj.setStyle(obj.highlighted, k, v);
}
});
}
}
}
/**
* Merge cells
* @param cellName
* @param colspan
* @param rowspan
* @param ignoreHistoryAndEvents
*/
obj.setMerge = function(cellName, colspan, rowspan, ignoreHistoryAndEvents) {
var test = false;
if (! cellName) {
if (! obj.highlighted.length) {
alert(obj.options.text.noCellsSelected);
return null;
} else {
var x1 = parseInt(obj.highlighted[0].getAttribute('data-x'));
var y1 = parseInt(obj.highlighted[0].getAttribute('data-y'));
var x2 = parseInt(obj.highlighted[obj.highlighted.length-1].getAttribute('data-x'));
var y2 = parseInt(obj.highlighted[obj.highlighted.length-1].getAttribute('data-y'));
var cellName = jexcel.getColumnNameFromId([ x1, y1 ]);
var colspan = (x2 - x1) + 1;
var rowspan = (y2 - y1) + 1;
}
}
var cell = jexcel.getIdFromColumnName(cellName, true);
if (obj.options.mergeCells[cellName] && obj.options.mergeCells[cellName][2]) {
test = obj.options.text.cellAlreadyMerged;
} else if ((! colspan || colspan < 2) && (! rowspan || rowspan < 2)) {
test = obj.options.text.invalidMergeProperties;
} else {
var cells = [];
for (var j = cell[1]; j < cell[1] + rowspan; j++) {
for (var i = cell[0]; i < cell[0] + colspan; i++) {
var columnName = jexcel.getColumnNameFromId([i, j]);
if (obj.options.mergeCells[columnName] && obj.options.mergeCells[columnName][2]) {
test = obj.options.text.thereIsAConflictWithAnotherMergedCell;
}
}
}
}
if (test) {
alert(test);
} else {
// Add property
if (colspan > 1) {
obj.records[cell[1]][cell[0]].setAttribute('colspan', colspan);
} else {
colspan = 1;
}
if (rowspan > 1) {
obj.records[cell[1]][cell[0]].setAttribute('rowspan', rowspan);
} else {
rowspan = 1;
}
// Keep links to the existing nodes
obj.options.mergeCells[cellName] = [ colspan, rowspan, [] ];
// Mark cell as merged
obj.records[cell[1]][cell[0]].setAttribute('data-merged', 'true');
// Overflow
obj.records[cell[1]][cell[0]].style.overflow = 'hidden';
// History data
var data = [];
// Adjust the nodes
for (var y = cell[1]; y < cell[1] + rowspan; y++) {
for (var x = cell[0]; x < cell[0] + colspan; x++) {
if (! (cell[0] == x && cell[1] == y)) {
data.push(obj.options.data[y][x]);
obj.updateCell(x, y, '', true);
obj.options.mergeCells[cellName][2].push(obj.records[y][x]);
obj.records[y][x].style.display = 'none';
obj.records[y][x] = obj.records[cell[1]][cell[0]];
}
}
}
// In the initialization is not necessary keep the history
obj.updateSelection(obj.records[cell[1]][cell[0]]);
if (! ignoreHistoryAndEvents) {
obj.setHistory({
action:'setMerge',
column:cellName,
colspan:colspan,
rowspan:rowspan,
data:data,
});
if (typeof(obj.options.onmerge) == 'function') {
obj.options.onmerge(el, column, width, oldWidth);
}
}
}
}
/**
* Merge cells
* @param cellName
* @param colspan
* @param rowspan
* @param ignoreHistoryAndEvents
*/
obj.getMerge = function(cellName) {
var data = {};
if (cellName) {
if (obj.options.mergeCells[cellName]) {
data = [ obj.options.mergeCells[cellName][0], obj.options.mergeCells[cellName][1] ];
} else {
data = null;
}
} else {
if (obj.options.mergeCells) {
var mergedCells = obj.options.mergeCells;
var keys = Object.keys(obj.options.mergeCells);
for (var i = 0; i < keys.length; i++) {
data[keys[i]] = [ obj.options.mergeCells[keys[i]][0], obj.options.mergeCells[keys[i]][1] ];
}
}
}
return data;
}
/**
* Remove merge by cellname
* @param cellName
*/
obj.removeMerge = function(cellName, data, keepOptions) {
if (obj.options.mergeCells[cellName]) {
var cell = jexcel.getIdFromColumnName(cellName, true);
obj.records[cell[1]][cell[0]].removeAttribute('colspan');
obj.records[cell[1]][cell[0]].removeAttribute('rowspan');
obj.records[cell[1]][cell[0]].removeAttribute('data-merged');
var info = obj.options.mergeCells[cellName];
var index = 0;
for (var j = 0; j < info[1]; j++) {
for (var i = 0; i < info[0]; i++) {
if (j > 0 || i > 0) {
obj.records[cell[1]+j][cell[0]+i] = info[2][index];
obj.records[cell[1]+j][cell[0]+i].style.display = '';
// Recover data
if (data && data[index]) {
obj.updateCell(cell[0]+i, cell[1]+j, data[index]);
}
index++;
}
}
}
// Update selection
obj.updateSelection(obj.records[cell[1]][cell[0]], obj.records[cell[1]+j-1][cell[0]+i-1]);
if (! keepOptions) {
delete(obj.options.mergeCells[cellName]);
}
}
}
/**
* Remove all merged cells
*/
obj.destroyMerged = function(keepOptions) {
// Remove any merged cells
if (obj.options.mergeCells) {
var mergedCells = obj.options.mergeCells;
var keys = Object.keys(obj.options.mergeCells);
for (var i = 0; i < keys.length; i++) {
obj.removeMerge(keys[i], null, keepOptions);
}
}
}
/**
* Is column merged
*/
obj.isColMerged = function(x, insertBefore) {
var cols = [];
// Remove any merged cells
if (obj.options.mergeCells) {
var keys = Object.keys(obj.options.mergeCells);
for (var i = 0; i < keys.length; i++) {
var info = jexcel.getIdFromColumnName(keys[i], true);
var colspan = obj.options.mergeCells[keys[i]][0];
var x1 = info[0];
var x2 = info[0] + (colspan > 1 ? colspan - 1 : 0);
if (insertBefore == null) {
if ((x1 <= x && x2 >= x)) {
cols.push(keys[i]);
}
} else {
if (insertBefore) {
if ((x1 < x && x2 >= x)) {
cols.push(keys[i]);
}
} else {
if ((x1 <= x && x2 > x)) {
cols.push(keys[i]);
}
}
}
}
}
return cols;
}
/**
* Is rows merged
*/
obj.isRowMerged = function(y, insertBefore) {
var rows = [];
// Remove any merged cells
if (obj.options.mergeCells) {
var keys = Object.keys(obj.options.mergeCells);
for (var i = 0; i < keys.length; i++) {
var info = jexcel.getIdFromColumnName(keys[i], true);
var rowspan = obj.options.mergeCells[keys[i]][1];
var y1 = info[1];
var y2 = info[1] + (rowspan > 1 ? rowspan - 1 : 0);
if (insertBefore == null) {
if ((y1 <= y && y2 >= y)) {
rows.push(keys[i]);
}
} else {
if (insertBefore) {
if ((y1 < y && y2 >= y)) {
rows.push(keys[i]);
}
} else {
if ((y1 <= y && y2 > y)) {
rows.push(keys[i]);
}
}
}
}
}
return rows;
}
/**
* Open the editor
*
* @param object cell
* @return void
*/
obj.openEditor = function(cell, empty, e) {
// Get cell position
var y = cell.getAttribute('data-y');
var x = cell.getAttribute('data-x');
// Overflow
if (x > 0) {
obj.records[y][x-1].style.overflow = 'hidden';
}
// Create editor
var createEditor = function(type) {
// Cell information
let info = cell.getBoundingClientRect();
// Create dropdown
var editor = document.createElement(type);
editor.style.width = (info.width) + 'px';
editor.style.height = (info.height - 2) + 'px';
editor.style.minHeight = (info.height - 2) + 'px';
// Edit cell
cell.classList.add('editor');
cell.innerHTML = '';
cell.appendChild(editor);
return editor;
}
// Readonly
if (cell.classList.contains('readonly') == true) {
// Do nothing
} else {
// Holder
obj.edition = [ obj.records[y][x], obj.records[y][x].innerHTML, x, y ];
// If there is a custom editor for it
if (obj.options.columns[x].editor) {
// Custom editors
obj.options.columns[x].editor.openEditor(cell, el);
} else {
// Native functions
if (obj.options.columns[x].type == 'hidden') {
// Do nothing
} else if (obj.options.columns[x].type == 'checkbox' || obj.options.columns[x].type == 'radio') {
// Get value
var value = cell.children[0].checked ? false : true;
// Toogle value
obj.setValue(cell, value);
// Do not keep edition open
obj.edition = null;
} else if (obj.options.columns[x].type == 'dropdown' || obj.options.columns[x].type == 'autocomplete') {
// Get current value
var value = obj.options.data[y][x];
// Create dropdown
if (typeof(obj.options.columns[x].filter) == 'function') {
var source = obj.options.columns[x].filter(el, cell, x, y, obj.options.columns[x].source);
} else {
var source = obj.options.columns[x].source;
}
// Create editor
var editor = createEditor('div');
var options = {
data: source,
multiple: obj.options.columns[x].multiple ? true : false,
autocomplete: obj.options.columns[x].autocomplete || obj.options.columns[x].type == 'autocomplete' ? true : false,
opened:true,
value: obj.options.columns[x].multiple ? value.split(';') : value,
width:'100%',
height:editor.style.minHeight,
position: (obj.options.tableOverflow == true || obj.options.fullscreen == true) ? true : false,
onclose:function() {
obj.closeEditor(cell, true);
}
};
if (obj.options.columns[x].options && obj.options.columns[x].options.type) {
options.type = obj.options.columns[x].options.type;
}
jApp.dropdown(editor, options);
} else if (obj.options.columns[x].type == 'calendar' || obj.options.columns[x].type == 'color') {
// Value
var value = obj.options.data[y][x];
// Create editor
var editor = createEditor('input');
editor.value = value;
if (obj.options.tableOverflow == true || obj.options.fullscreen == true) {
obj.options.columns[x].options.position = true;
}
obj.options.columns[x].options.value = obj.options.data[y][x];
obj.options.columns[x].options.onclose = function(el, value) {
obj.closeEditor(cell, true);
}
// Current value
if (obj.options.columns[x].type == 'color') {
jApp.color(editor, obj.options.columns[x].options);
} else {
var calendar = jApp.calendar(editor, obj.options.columns[x].options);
calendar.setValue(value);
}
// Focus on editor
editor.focus();
} else if (obj.options.columns[x].type == 'image') {
// Value
var img = cell.children[0];
// Create editor
var editor = createEditor('div');
editor.style.position = 'relative';
var div = document.createElement('div');
div.classList.add('jclose');
if (img && img.src) {
div.appendChild(img);
}
editor.appendChild(div);
jApp.image(div);
const rect = cell.getBoundingClientRect();
const rectContent = div.getBoundingClientRect();
if (window.innerHeight < rect.bottom + rectContent.height) {
div.style.top = rect.top - (rectContent.height + 2);
} else