585 lines
22 KiB
JavaScript
585 lines
22 KiB
JavaScript
/* global initToolbar */
|
|
|
|
window.edittable = window.edittable || {};
|
|
window.edittable_plugins = window.edittable_plugins || {};
|
|
|
|
(function (edittable, edittable_plugins) {
|
|
'use strict';
|
|
|
|
/**
|
|
*
|
|
*
|
|
* @param {Array} movingRowIndexes the indices of the rows to be moved
|
|
* @param {int} target the row where the rows will be inserted
|
|
* @param {Array} dmarray the data or meta array
|
|
*
|
|
* @return {Array} the new data or meta array
|
|
*/
|
|
edittable.moveRow = function moveRow(movingRowIndexes, target, dmarray) {
|
|
var startIndex = movingRowIndexes[0];
|
|
var endIndex = movingRowIndexes[movingRowIndexes.length - 1];
|
|
var moveForward = target < startIndex;
|
|
|
|
var first = dmarray.slice(0, Math.min(startIndex, target));
|
|
var moving = dmarray.slice(startIndex, endIndex + 1);
|
|
var between;
|
|
if (moveForward) {
|
|
between = dmarray.slice(target, startIndex);
|
|
} else {
|
|
between = dmarray.slice(endIndex + 1, target);
|
|
}
|
|
var last = dmarray.slice(Math.max(endIndex + 1, target));
|
|
if (moveForward) {
|
|
return [].concat(first, moving, between, last);
|
|
}
|
|
return [].concat(first, between, moving, last);
|
|
};
|
|
|
|
edittable.addRowToMeta = function (index, amount, metaArray) {
|
|
var i;
|
|
var cols = 1; // minimal number of cells
|
|
if (metaArray[0]) {
|
|
cols = metaArray[0].length;
|
|
}
|
|
|
|
// insert into meta array
|
|
for (i = 0; i < amount; i += 1) {
|
|
var newrow = Array.apply(null, new Array(cols)).map(function initializeRowMeta() {
|
|
return { rowspan: 1, colspan: 1 };
|
|
});
|
|
metaArray.splice(index, 0, newrow);
|
|
}
|
|
|
|
return metaArray;
|
|
};
|
|
|
|
|
|
/**
|
|
*
|
|
* @param {Array} movingColIndexes the indices of the columns to be moved
|
|
* @param {int} target the column where the columns will be inserted
|
|
* @param {Array} dmarray the data or meta array
|
|
*
|
|
* @return {Array} the new data or meta array
|
|
*/
|
|
edittable.moveCol = function moveCol(movingColIndexes, target, dmarray) {
|
|
return dmarray.map(function (row) {
|
|
return edittable.moveRow(movingColIndexes, target, row);
|
|
});
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {Array} meta the meta array
|
|
* @returns {Array} an array of the cells with a rowspan or colspan larger than 1
|
|
*/
|
|
edittable.getMerges = function (meta) {
|
|
var merges = [];
|
|
for (var row = 0; row < meta.length; row += 1) {
|
|
for (var col = 0; col < meta[0].length; col += 1) {
|
|
if (meta[row][col].hasOwnProperty('rowspan') && meta[row][col].rowspan > 1 ||
|
|
meta[row][col].hasOwnProperty('colspan') && meta[row][col].colspan > 1) {
|
|
var merge = {};
|
|
merge.row = row;
|
|
merge.col = col;
|
|
merge.rowspan = meta[row][col].rowspan;
|
|
merge.colspan = meta[row][col].colspan;
|
|
merges.push(merge);
|
|
}
|
|
}
|
|
}
|
|
return merges;
|
|
};
|
|
|
|
|
|
/**
|
|
*
|
|
* @param {Array} merges an array of the cells that are part of a merge
|
|
* @param {int} target the target column or row
|
|
* @param {string} direction whether we're trying to move a col or row
|
|
*
|
|
* @return {bool} wether the target col/row is part of a merge
|
|
*/
|
|
edittable.isTargetInMerge = function isTargetInMerge(merges, target, direction) {
|
|
return merges.some(function (merge) {
|
|
return (merge[direction] < target && target < merge[direction] + merge[direction + 'span']);
|
|
});
|
|
};
|
|
|
|
edittable.loadEditor = function () {
|
|
var $container = jQuery('#edittable__editor');
|
|
if (!$container.length) {
|
|
return;
|
|
}
|
|
|
|
var $form = jQuery('#dw__editform');
|
|
var $datafield = $form.find('input[name=edittable_data]');
|
|
var $metafield = $form.find('input[name=edittable_meta]');
|
|
|
|
var data = JSON.parse($datafield.val());
|
|
var meta = JSON.parse($metafield.val());
|
|
|
|
/**
|
|
* Get the current meta array
|
|
*
|
|
* @return {array} the current meta array as array of rows with arrays of columns with objects
|
|
*/
|
|
function getMeta() {return meta;}
|
|
|
|
/**
|
|
* Get the current data array
|
|
*
|
|
* @return {array} the current data array as array of rows with arrays of columns with strings
|
|
*/
|
|
function getData() {return data;}
|
|
|
|
var merges = edittable.getMerges(meta);
|
|
if (merges === []) {
|
|
merges = true;
|
|
}
|
|
var lastselect = { row: 0, col: 0 };
|
|
|
|
var handsontable_config = {
|
|
data: data,
|
|
startRows: 5,
|
|
startCols: 5,
|
|
colHeaders: true,
|
|
rowHeaders: true,
|
|
manualColumnResize: true,
|
|
outsideClickDeselects: false,
|
|
contextMenu: edittable.getEditTableContextMenu(getData, getMeta),
|
|
manualColumnMove: true,
|
|
manualRowMove: true,
|
|
mergeCells: merges,
|
|
|
|
|
|
/**
|
|
* Attach pointers to our raw data structures in the instance
|
|
*
|
|
* @return {void}
|
|
*/
|
|
afterLoadData: function () {
|
|
var i;
|
|
this.raw = {
|
|
data: data,
|
|
meta: meta,
|
|
colinfo: [],
|
|
rowinfo: []
|
|
};
|
|
for (i = 0; i < data.length; i += 1) {
|
|
this.raw.rowinfo[i] = {};
|
|
}
|
|
for (i = 0; i < data[0].length; i += 1) {
|
|
this.raw.colinfo[i] = {};
|
|
}
|
|
},
|
|
|
|
/**
|
|
* initialize cell properties
|
|
*
|
|
* properties are stored in extra array
|
|
*
|
|
* @param {int} row the row of the desired column
|
|
* @param {int} col the col of the desired column
|
|
* @returns {Array} the respective cell from the meta array
|
|
*/
|
|
cells: function (row, col) {
|
|
return meta[row][col];
|
|
},
|
|
|
|
/**
|
|
* Custom cell renderer
|
|
*
|
|
* It handles all our custom meta attributes like alignments and rowspans
|
|
*
|
|
* @param {object} instance the handsontable instance
|
|
* @param {HTMLTableCellElement} td the dom node of the cell
|
|
* @param {int} row the row of the cell to be rendered
|
|
* @param {int} col the column of the cell to be rendered
|
|
*
|
|
* @return {void}
|
|
*/
|
|
renderer: function (instance, td, row, col) {
|
|
// for some reason, neither cellProperties nor instance.getCellMeta() give the right data
|
|
var cellMeta = meta[row][col];
|
|
var $td = jQuery(td);
|
|
|
|
if (cellMeta.colspan) {
|
|
$td.attr('colspan', cellMeta.colspan);
|
|
} else {
|
|
$td.removeAttr('colspan');
|
|
}
|
|
|
|
if (cellMeta.rowspan) {
|
|
$td.attr('rowspan', cellMeta.rowspan);
|
|
} else {
|
|
$td.removeAttr('rowspan');
|
|
}
|
|
|
|
if (cellMeta.hide) {
|
|
$td.hide();
|
|
} else {
|
|
$td.show();
|
|
}
|
|
|
|
if (cellMeta.align === 'right') {
|
|
$td.addClass('right');
|
|
$td.removeClass('center');
|
|
} else if (cellMeta.align === 'center') {
|
|
$td.addClass('center');
|
|
$td.removeClass('right');
|
|
} else {
|
|
$td.removeClass('center');
|
|
$td.removeClass('right');
|
|
}
|
|
|
|
if (cellMeta.tag === 'th') {
|
|
$td.addClass('header');
|
|
} else {
|
|
$td.removeClass('header');
|
|
}
|
|
|
|
/* globals Handsontable */
|
|
Handsontable.renderers.TextRenderer.apply(this, arguments);
|
|
},
|
|
|
|
/**
|
|
* Initialization after the Editor loaded
|
|
*
|
|
* @return {void}
|
|
*/
|
|
afterInit: function () {
|
|
// select first cell
|
|
this.selectCell(0, 0);
|
|
|
|
// we need an ID on the input field
|
|
jQuery('textarea.handsontableInput').attr('id', 'handsontable__input');
|
|
|
|
// we're ready to intialize the toolbar now
|
|
initToolbar('tool__bar', 'handsontable__input', window.toolbar, false);
|
|
|
|
// we wrap DokuWiki's pasteText() here to get notified when the toolbar inserted something into our editor
|
|
var original_pasteText = window.pasteText;
|
|
window.pasteText = function (selection, text, opts) {
|
|
original_pasteText(selection, text, opts); // do what pasteText does
|
|
// trigger resize
|
|
jQuery('#handsontable__input').data('AutoResizer').check();
|
|
};
|
|
window.pasteText = original_pasteText;
|
|
|
|
/*
|
|
This is a workaround to rerender the table. It serves two functions:
|
|
1: On wide tables with linebreaks in columns with no pre-defined table widths (via the tablelayout plugin)
|
|
reset the width of the table columns to what is needed by its no narrower content
|
|
2: On table with some rows fixed at the top, ensure that the content of these rows stays at the top as well,
|
|
not only the lefthand rownumbers
|
|
Attaching this to the event 'afterRenderer' did not have the desired results, as it seemed not to work for
|
|
usecase 1 at all and for usecase 2 only with a delay.
|
|
*/
|
|
var _this = this;
|
|
this.addHookOnce('afterOnCellMouseOver', function () {
|
|
_this.updateSettings({});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* This recalculates the col and row spans and makes sure all correct cells are hidden
|
|
*
|
|
* @return {void}
|
|
*/
|
|
beforeRender: function () {
|
|
var row, r, c, col, i;
|
|
|
|
// reset row and column infos - we store spanning info there
|
|
this.raw.rowinfo = [];
|
|
this.raw.colinfo = [];
|
|
for (i = 0; i < data.length; i += 1) {
|
|
this.raw.rowinfo[i] = {};
|
|
}
|
|
for (i = 0; i < data[0].length; i += 1) {
|
|
this.raw.colinfo[i] = {};
|
|
}
|
|
|
|
// unhide all cells
|
|
for (row = 0; row < data.length; row += 1) {
|
|
for (col = 0; col < data[0].length; col += 1) {
|
|
if (meta[row][col].hide) {
|
|
meta[row][col].hide = false;
|
|
data[row][col] = '';
|
|
}
|
|
// unset all row/colspans
|
|
meta[row][col].colspan = 1;
|
|
meta[row][col].rowspan = 1;
|
|
|
|
// make sure no data cell is undefined/null
|
|
if (!data[row][col]) {
|
|
data[row][col] = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var merge = 0; merge < this.mergeCells.mergedCellInfoCollection.length; merge += 1) {
|
|
row = this.mergeCells.mergedCellInfoCollection[merge].row;
|
|
col = this.mergeCells.mergedCellInfoCollection[merge].col;
|
|
var colspan = this.mergeCells.mergedCellInfoCollection[merge].colspan;
|
|
var rowspan = this.mergeCells.mergedCellInfoCollection[merge].rowspan;
|
|
meta[row][col].colspan = colspan;
|
|
meta[row][col].rowspan = rowspan;
|
|
|
|
// hide the cells hidden by the row/colspan
|
|
|
|
for (r = row; r < row + rowspan; r += 1) {
|
|
for (c = col; c < col + colspan; c += 1) {
|
|
if (r === row && c === col) {
|
|
continue;
|
|
}
|
|
meta[r][c].hide = true;
|
|
meta[r][c].rowspan = 1;
|
|
meta[r][c].colspan = 1;
|
|
if (data[r][c] && data[r][c] !== ':::') {
|
|
data[row][col] += ' ' + data[r][c];
|
|
}
|
|
if (r === row) {
|
|
data[r][c] = '';
|
|
} else {
|
|
data[r][c] = ':::';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clone data object
|
|
// Since we can't use real line breaks (\n) inside table cells, this object is used to store all cell values with DokuWiki's line breaks (\\) instead of actual ones.
|
|
var dataLBFixed = jQuery.extend(true, {}, data);
|
|
|
|
// In dataLBFixed, replace all actual line breaks with DokuWiki line breaks
|
|
// In data, replace all DokuWiki line breaks with actual ones so the editor displays line breaks properly
|
|
for (row = 0; row < data.length; row += 1) {
|
|
for (col = 0; col < data[0].length; col += 1) {
|
|
dataLBFixed[row][col] = data[row][col].replace(/(\r\n|\n|\r)/g, '\\\\ ');
|
|
data[row][col] = data[row][col].replace(/\\\\\s/g, '\n');
|
|
}
|
|
}
|
|
|
|
// Store dataFixed and meta back in the form
|
|
$datafield.val(JSON.stringify(dataLBFixed));
|
|
$metafield.val(JSON.stringify(meta));
|
|
},
|
|
|
|
/**
|
|
* Disable key handling while the link wizard or any other dialog is visible
|
|
*
|
|
* @param {event} e the keydown event object
|
|
*
|
|
* @return {void}
|
|
*/
|
|
beforeKeyDown: function (e) {
|
|
if (jQuery('.ui-dialog:visible').length) {
|
|
e.stopImmediatePropagation();
|
|
e.preventDefault();
|
|
}
|
|
},
|
|
|
|
beforeColumnMove: function (movingCols, target) {
|
|
var disallowMove = edittable.isTargetInMerge(this.mergeCells.mergedCellInfoCollection, target, 'col');
|
|
if (disallowMove) {
|
|
return false;
|
|
}
|
|
meta = edittable.moveCol(movingCols, target, meta);
|
|
data = edittable.moveCol(movingCols, target, data);
|
|
this.updateSettings({ mergeCells: edittable.getMerges(meta), data: data });
|
|
return false;
|
|
},
|
|
|
|
beforeRowMove: function (movingRows, target) {
|
|
var disallowMove = edittable.isTargetInMerge(this.mergeCells.mergedCellInfoCollection, target, 'row');
|
|
if (disallowMove) {
|
|
return false;
|
|
}
|
|
meta = edittable.moveRow(movingRows, target, meta);
|
|
data = edittable.moveRow(movingRows, target, data);
|
|
this.updateSettings({ mergeCells: edittable.getMerges(meta), data: data });
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Update meta data array when rows are added
|
|
*
|
|
* @param {int} index the index where the new rows are created
|
|
* @param {int} amount the number of new rows that are created
|
|
*
|
|
* @return {void}
|
|
*/
|
|
afterCreateRow: function (index, amount) {
|
|
meta = edittable.addRowToMeta(index, amount, meta);
|
|
},
|
|
|
|
/**
|
|
* Set id for toolbar to current handsontable input textarea
|
|
*
|
|
* For some reason (bug?), handsontable creates a new div.handsontableInputHolder with a new textarea and
|
|
* ignores the old one. For the toolbar to keep working we need make sure the currently used textarea has
|
|
* also the id `handsontable__input`.
|
|
*
|
|
* @return {void}
|
|
*/
|
|
afterBeginEditing: function () {
|
|
if (jQuery('textarea.handsontableInput').length > 1) {
|
|
jQuery('textarea.handsontableInput:not(:last)').remove();
|
|
jQuery('textarea.handsontableInput').attr('id', 'handsontable__input');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update meta data array when rows are removed
|
|
*
|
|
* @param {int} index the index where the rows are removed
|
|
* @param {int} amount the number of rows that are removed
|
|
*
|
|
* @return {void}
|
|
*/
|
|
afterRemoveRow: function (index, amount) {
|
|
meta.splice(index, amount);
|
|
},
|
|
|
|
/**
|
|
* Update meta data array when columns are added
|
|
*
|
|
* @param {int} index the index where the new columns are created
|
|
* @param {int} amount the number of new columns that are created
|
|
*
|
|
* @return {void}
|
|
*/
|
|
afterCreateCol: function (index, amount) {
|
|
for (var row = 0; row < data.length; row += 1) {
|
|
for (var i = 0; i < amount; i += 1) {
|
|
meta[row].splice(index, 0, { rowspan: 1, colspan: 1 });
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update meta data array when columns are removed
|
|
*
|
|
* @param {int} index the index where the columns are removed
|
|
* @param {int} amount the number of columns that are removed
|
|
*
|
|
* @return {void}
|
|
*/
|
|
afterRemoveCol: function (index, amount) {
|
|
for (var row = 0; row < data.length; row += 1) {
|
|
meta[row].splice(index, amount);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Skip hidden cells for selection
|
|
*
|
|
* @param {int} r the row of the selected cell
|
|
* @param {int} c the column of the selected cell
|
|
*
|
|
* @return {void}
|
|
*/
|
|
afterSelection: function (r, c) {
|
|
if (meta[r][c].hide) {
|
|
// user navigated into a hidden cell! we need to find the next selectable cell
|
|
var x = 0;
|
|
|
|
var v = r - lastselect.row;
|
|
if (v > 0) {
|
|
v = 1;
|
|
}
|
|
if (v < 0) {
|
|
v = -1;
|
|
}
|
|
|
|
var h = c - lastselect.col;
|
|
if (h > 0) {
|
|
h = 1;
|
|
}
|
|
if (h < 0) {
|
|
h = -1;
|
|
}
|
|
|
|
if (v !== 0) {
|
|
x = r;
|
|
// user navigated vertically
|
|
do {
|
|
x += v;
|
|
if (!meta[x][c].hide) {
|
|
// cell is selectable, do it
|
|
this.selectCell(x, c);
|
|
return;
|
|
}
|
|
|
|
} while (x > 0 && x < data.length);
|
|
// found no suitable cell
|
|
this.deselectCell();
|
|
} else if (h !== 0) {
|
|
x = c;
|
|
// user navigated horizontally
|
|
do {
|
|
x += h;
|
|
if (!meta[r][x].hide) {
|
|
// cell is selectable, do it
|
|
this.selectCell(r, x);
|
|
return;
|
|
}
|
|
|
|
} while (x > 0 && x < data[0].length);
|
|
// found no suitable cell
|
|
this.deselectCell();
|
|
}
|
|
} else {
|
|
// remember this selection
|
|
lastselect.row = r;
|
|
lastselect.col = c;
|
|
}
|
|
},
|
|
|
|
/**
|
|
*
|
|
* @param {Array} pasteData An array of arrays which contains data to paste.
|
|
* @param {Array} coords An array of objects with ranges of the visual indexes (startRow, startCol, endRow, endCol)
|
|
* that correspond to the previously selected area.
|
|
* @return {true} always allowing the pasting
|
|
*/
|
|
beforePaste: function (pasteData, coords) {
|
|
var startRow = coords[0].startRow;
|
|
var startCol = coords[0].startCol;
|
|
var totalRows = this.countRows();
|
|
var totalCols = this.countCols();
|
|
|
|
var missingRows = (startRow + pasteData.length) - totalRows;
|
|
var missingCols = (startCol + pasteData[0].length) - totalCols;
|
|
if (missingRows > 0) {
|
|
this.alter('insert_row', undefined, missingRows, 'paste');
|
|
}
|
|
if (missingCols > 0) {
|
|
this.alter('insert_col', undefined, missingCols, 'paste');
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
if (window.JSINFO.plugins.edittable['default columnwidth']) {
|
|
handsontable_config.colWidths = window.JSINFO.plugins.edittable['default columnwidth'];
|
|
}
|
|
|
|
|
|
for (var plugin in edittable_plugins) {
|
|
if (edittable_plugins.hasOwnProperty(plugin)) {
|
|
if (typeof edittable_plugins[plugin].modifyHandsontableConfig === 'function') {
|
|
edittable_plugins[plugin].modifyHandsontableConfig(handsontable_config, $form);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
$container.handsontable(handsontable_config);
|
|
|
|
};
|
|
|
|
jQuery(document).ready(edittable.loadEditor);
|
|
|
|
}(window.edittable, window.edittable_plugins));
|