1
0
Fork 0

Adding edittable version 2023-01-14 (66785d9).

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2024-12-01 20:32:06 +01:00
parent 51b386fcf7
commit 778f9ac0bf
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
101 changed files with 56770 additions and 0 deletions

View file

@ -0,0 +1,210 @@
/* global LANG */
window.edittable = window.edittable || {};
(function (edittable) {
'use strict';
/**
* create an iterable array of selected cells from the selection object
*
* @param {object} selection the selection object
*
* @returns {Array} an array of the rows/columns of the cells in the selection
*/
edittable.cellArray = function (selection) {
var selectionArray = [];
for (var currentRow = selection.start.row; currentRow <= selection.end.row; currentRow += 1) {
for (var currentCol = selection.start.col; currentCol <= selection.end.col; currentCol += 1) {
selectionArray.push({row: currentRow, col: currentCol});
}
}
return selectionArray;
};
/**
* Defines our own contextMenu with custom callbacks
*
* @param {function} getData get the current data array
* @param {function} getMeta get the current meta array
* @returns {object} the context menu object
*/
edittable.getEditTableContextMenu = function (getData, getMeta) {
return {
items: {
toggle_header: {
name: LANG.plugins.edittable.toggle_header,
callback: function (key, selection) {
var meta = getMeta();
jQuery.each(edittable.cellArray(selection), function (index, cell) {
var col = cell.col;
var row = cell.row;
if (meta[row][col].tag && meta[row][col].tag === 'th') {
meta[row][col].tag = 'td';
} else {
meta[row][col].tag = 'th';
}
});
this.render();
}
},
align_left: {
name: LANG.plugins.edittable.align_left,
callback: function (key, selection) {
var meta = getMeta();
jQuery.each(edittable.cellArray(selection), function (index, cell) {
var col = cell.col;
var row = cell.row;
meta[row][col].align = 'left';
});
this.render();
},
disabled: function () {
var meta = getMeta();
var selection = this.getSelected();
var row = selection[0];
var col = selection[1];
return (!meta[row][col].align || meta[row][col].align === 'left');
}
},
align_center: {
name: LANG.plugins.edittable.align_center,
callback: function (key, selection) {
var meta = getMeta();
jQuery.each(edittable.cellArray(selection), function (index, cell) {
var col = cell.col;
var row = cell.row;
meta[row][col].align = 'center';
});
this.render();
},
disabled: function () {
var meta = getMeta();
var selection = this.getSelected();
var row = selection[0];
var col = selection[1];
return (meta[row][col].align && meta[row][col].align === 'center');
}
},
align_right: {
name: LANG.plugins.edittable.align_right,
callback: function (key, selection) {
var meta = getMeta();
jQuery.each(edittable.cellArray(selection), function (index, cell) {
var col = cell.col;
var row = cell.row;
meta[row][col].align = 'right';
});
this.render();
},
disabled: function () {
var meta = getMeta();
var selection = this.getSelected();
var row = selection[0];
var col = selection[1];
return (meta[row][col].align && meta[row][col].align === 'right');
}
},
hsep1: '---------',
row_above: {
name: LANG.plugins.edittable.row_above
},
remove_row: {
name: LANG.plugins.edittable.remove_row,
/**
* The same as the default action, but with confirmation
*
* @param {string} key key of the menu item
* @param {object} selection the selection object
*
* @return {void}
*/
callback: function (key, selection) {
if (window.confirm(LANG.plugins.edittable.confirmdeleterow)) {
var amount = selection.end.row - selection.start.row + 1;
this.alter('remove_row', selection.start.row, amount);
}
},
/**
* do not show when this is the last row
*
* @return {boolean} true if the entry is to be disabled, false otherwise
*/
disabled: function () {
var rowsInTable = this.countRows();
var firstSelectedRow = this.getSelected()[0];
var lastSelectedRow = this.getSelected()[2]; // fix magic number with destructuring once we drop IE11
var allRowsSelected = firstSelectedRow === 0 && lastSelectedRow === rowsInTable - 1;
return (rowsInTable <= 1 || allRowsSelected);
}
},
row_below: {
name: LANG.plugins.edittable.row_below
},
hsep2: '---------',
col_left: {
name: LANG.plugins.edittable.col_left
},
remove_col: {
name: LANG.plugins.edittable.remove_col,
/**
* The same as the default action, but with confirmation
*
* @param {string} key key of the menu item
* @param {object} selection the selection object
*
* @return {void}
*/
callback: function (key, selection) {
if (window.confirm(LANG.plugins.edittable.confirmdeletecol)) {
var amount = selection.end.col - selection.start.col + 1;
this.alter('remove_col', selection.start.col, amount);
}
},
/**
* do not show when this is the last row
*
* @return {boolean} true if the entry is to be disabled, false otherwise
*/
disabled: function () {
var colsInTable = this.countCols();
var firstSelectedColumn = this.getSelected()[1];
var lastSelectedColumn = this.getSelected()[3]; // fix magic number with destructuring once we drop IE11
var allColsSelected = firstSelectedColumn === 0 && lastSelectedColumn === colsInTable - 1;
return (colsInTable <= 1 || allColsSelected);
}
},
col_right: {
name: LANG.plugins.edittable.col_right
},
hsep3: '---------',
mergeCells: {
name: function () {
var sel = this.getSelected();
var info = this.mergeCells.mergedCellInfoCollection.getInfo(sel[0], sel[1]);
if (info) {
return '<div class="unmerge">' + LANG.plugins.edittable.unmerge_cells + '</div>';
} else {
return '<div class="merge">' + LANG.plugins.edittable.merge_cells + '</div>';
}
},
/**
* disable if only one cell is selected
*
* @return {boolean} true if the entry is to be disabled, false otherwise
*/
disabled: function () {
var selection = this.getSelected();
var startRow = selection[0];
var startCol = selection[1];
var endRow = selection[2];
var endCol = selection[3];
return startRow === endRow && startCol === endCol;
}
}
}
};
};
}(window.edittable));

View file

@ -0,0 +1,25 @@
/**
* Adjust the top margin and make buttons visible
*/
jQuery(function () {
'use strict';
var $editbutton = jQuery('.dokuwiki div.editbutton_table');
if (!$editbutton.length) {
return;
}
// unhide the buttons - we have JavaScript
$editbutton.show();
// determine the bottom margin of the table above and remove it from our button
var margin = 0;
var $tablediv = $editbutton.prev('div.table');
if (!$tablediv.length) {
return;
}
margin += parseFloat($tablediv.css('margin-bottom'));
margin += parseFloat($tablediv.find('table').css('margin-bottom'));
margin += 1; // for the border
$editbutton.css('margin-top', margin * -1);
});

View file

@ -0,0 +1,585 @@
/* 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));

View file

@ -0,0 +1,63 @@
/* exported addBtnActionNewTable */
/**
* Add button action for your toolbar button
*
* @param {jQuery} $btn Button element to add the action to
* @param {Array} props Associative array of button properties
* @param {string} edid ID of the editor textarea
* @return {string} If button should be appended return the id for in aria-controls,
* otherwise an empty string
*/
window.addBtnActionNewTable = function addBtnActionNewTable($btn, props, edid) {
'use strict';
$btn.click(function () {
var editform = jQuery('#dw__editform')[0];
var ed = jQuery('#' + edid)[0];
/**
* Add new textarea to the form
*
* @param {string} name the name attribute of the new field
* @param {string} val the value attribute of the new field
*
* @return {void}
*/
function addField(name, val) {
var pos_field = document.createElement('textarea');
pos_field.name = 'edittable__new[' + name + ']';
pos_field.value = val;
pos_field.style.display = 'none';
editform.appendChild(pos_field);
}
var sel;
if (window.DWgetSelection) {
sel = window.DWgetSelection(ed);
} else {
sel = window.getSelection(ed);
}
addField('pre', ed.value.substr(0, sel.start));
addField('text', ed.value.substr(sel.start, sel.end - sel.start));
addField('suf', ed.value.substr(sel.end));
// adora belle requires a range, even though we handle ranging ourselve here
var range = document.createElement('input');
range.name = 'range';
range.value = '0-0';
range.type = 'hidden';
editform.appendChild(range);
// Fake POST
var editbutton = document.createElement('input');
editbutton.name = 'do[edit]';
editbutton.type = 'submit';
editbutton.style.display = 'none';
editform.appendChild(editbutton);
// Prevent warning
window.textChanged = false;
editbutton.click();
});
return 'click';
};