kopia lustrzana https://github.com/wagtail/wagtail
Add support for merging cells in TableBlock.
Handsontable has support for merging table cells using the `mergeCells` plugin but there was no support in Wagtail for storing which cells have been merged or rendering them in the output template. The client JavaScript will now save which cells have been merged and the output template will merge or hide cells. See https://handsontable.com/docs/6.2.0/Options.html#mergeCellspull/10878/head
rodzic
28d55f8c24
commit
a63689869e
|
@ -21,6 +21,7 @@ Changelog
|
|||
* Allow overriding `IndexView.export_headings` via `ModelViewSet` (Christer Jensen, Sage Abdullah)
|
||||
* Support specifying a `get_object_list` method on `ChooserViewSet` (Matt Westcott)
|
||||
* Add `linked_fields` mechanism on chooser widgets to allow choices to be limited by fields on the calling page (Matt Westcott)
|
||||
* Add support for merging cells within `TableBlock` with the `mergedCells` option (Gareth Palmer)
|
||||
* Fix: Ensure that StreamField's `FieldBlock`s correctly set the `required` and `aria-describedby` attributes (Storm Heg)
|
||||
* Fix: Avoid an error when the moderation panel (admin dashboard) contains both snippets and private pages (Matt Westcott)
|
||||
* Fix: When deleting collections, ensure the collection name is correctly shown in the success message (LB (Ben) Johnston)
|
||||
|
|
|
@ -726,6 +726,7 @@
|
|||
* Shreshth Srivastava
|
||||
* Sandeep Choudhary
|
||||
* Antoni Martyniuk
|
||||
* Gareth Palmer
|
||||
|
||||
## Translators
|
||||
|
||||
|
|
|
@ -77,26 +77,52 @@ function initTable(id, tableOptions) {
|
|||
});
|
||||
}
|
||||
|
||||
const getCellsClassnames = function () {
|
||||
const meta = hot.getCellsMeta();
|
||||
const cellsClassnames = [];
|
||||
for (let i = 0; i < meta.length; i += 1) {
|
||||
if (hasOwn(meta[i], 'className')) {
|
||||
cellsClassnames.push({
|
||||
row: meta[i].row,
|
||||
col: meta[i].col,
|
||||
className: meta[i].className,
|
||||
const persist = function () {
|
||||
const cell = [];
|
||||
const mergeCells = [];
|
||||
const cellsMeta = hot.getCellsMeta();
|
||||
|
||||
cellsMeta.forEach((meta) => {
|
||||
let className;
|
||||
let hidden;
|
||||
|
||||
if (hasOwn(meta, 'className')) {
|
||||
className = meta.className;
|
||||
}
|
||||
if (hasOwn(meta, 'hidden')) {
|
||||
// Cells are hidden if they have been merged
|
||||
hidden = true;
|
||||
}
|
||||
|
||||
// Undefined values won't be included in the output
|
||||
if (className !== undefined || hidden) {
|
||||
cell.push({
|
||||
row: meta.row,
|
||||
col: meta.col,
|
||||
className: className,
|
||||
hidden: hidden,
|
||||
});
|
||||
}
|
||||
}
|
||||
return cellsClassnames;
|
||||
};
|
||||
});
|
||||
|
||||
if (hot.getPlugin('mergeCells').isEnabled()) {
|
||||
const collection = hot.getPlugin('mergeCells').mergedCellsCollection;
|
||||
|
||||
collection.mergedCells.forEach((merge) => {
|
||||
mergeCells.push({
|
||||
row: merge.row,
|
||||
col: merge.col,
|
||||
rowspan: merge.rowspan,
|
||||
colspan: merge.colspan,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const persist = function () {
|
||||
hiddenStreamInput.val(
|
||||
JSON.stringify({
|
||||
data: hot.getData(),
|
||||
cell: getCellsClassnames(),
|
||||
cell: cell,
|
||||
mergeCells: mergeCells,
|
||||
first_row_is_table_header: tableHeaderCheckbox.prop('checked'),
|
||||
first_col_is_header: colHeaderCheckbox.prop('checked'),
|
||||
table_caption: tableCaption.val(),
|
||||
|
@ -105,7 +131,7 @@ function initTable(id, tableOptions) {
|
|||
};
|
||||
|
||||
const cellEvent = function (change, source) {
|
||||
if (source === 'loadData') {
|
||||
if (!isInitialized || source === 'loadData' || source === 'MergeCells') {
|
||||
return; // don't save this change
|
||||
}
|
||||
|
||||
|
@ -119,6 +145,20 @@ function initTable(id, tableOptions) {
|
|||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const mergeEvent = function (cellRange, mergeParent, auto) {
|
||||
if (isInitialized) {
|
||||
persist();
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const unmergeEvent = function (cellRange, auto) {
|
||||
if (isInitialized) {
|
||||
persist();
|
||||
}
|
||||
};
|
||||
|
||||
const initEvent = function () {
|
||||
isInitialized = true;
|
||||
};
|
||||
|
@ -148,6 +188,8 @@ function initTable(id, tableOptions) {
|
|||
afterRemoveCol: structureEvent,
|
||||
afterRemoveRow: structureEvent,
|
||||
afterSetCellMeta: metaEvent,
|
||||
afterMergeCells: mergeEvent,
|
||||
afterUnmergeCells: unmergeEvent,
|
||||
afterInit: initEvent,
|
||||
// contextMenu set via init, from server defaults
|
||||
};
|
||||
|
@ -169,6 +211,13 @@ function initTable(id, tableOptions) {
|
|||
finalOptions[key] = tableOptions[key];
|
||||
});
|
||||
|
||||
if (hasOwn(finalOptions, 'mergeCells') && finalOptions.mergeCells === true) {
|
||||
// If mergeCells is enabled and true then use the value from the database
|
||||
if (dataForForm !== null && hasOwn(dataForForm, 'mergeCells')) {
|
||||
finalOptions.mergeCells = dataForForm.mergeCells;
|
||||
}
|
||||
}
|
||||
|
||||
hot = new Handsontable(document.getElementById(containerId), finalOptions);
|
||||
window.addEventListener('load', () => {
|
||||
// Render the table. Calling render also removes 'null' literals from empty cells.
|
||||
|
|
|
@ -85,6 +85,8 @@ default_table_options = {
|
|||
}
|
||||
```
|
||||
|
||||
(table_block_options)=
|
||||
|
||||
### Configuration Options
|
||||
|
||||
Every key in the `table_options` dictionary maps to a [handsontable](https://handsontable.com/) option. These settings can be changed to alter the behaviour of tables in Wagtail. The following options are available:
|
||||
|
@ -101,6 +103,7 @@ Every key in the `table_options` dictionary maps to a [handsontable](https://han
|
|||
- [language](https://handsontable.com/docs/6.2.2/Options.html#language) - The default language setting. By default TableBlock tries to get the language from `django.utils.translation.get_language`. If needed, this setting can be overridden here.
|
||||
- [renderer](https://handsontable.com/docs/6.2.2/Options.html#renderer) - The default setting Handsontable uses to render the content of table cells.
|
||||
- [autoColumnSize](https://handsontable.com/docs/6.2.2/Options.html#autoColumnSize) - Enables or disables the `autoColumnSize` plugin. The TableBlock default setting is `False`.
|
||||
- [mergedCells](https://handsontable.com/docs/6.2.0/Options.html#mergeCells) - Can be set to `True` or `False`, determined if merging cells is allowed. Remember to add `'mergeCells'` to the `'contextMenu'` option also.
|
||||
|
||||
A [complete list of handsontable options](https://handsontable.com/docs/6.2.2/Options.html) can be found on the Handsontable website.
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ depth: 1
|
|||
* Allow overriding `IndexView.export_headings` via `ModelViewSet` (Christer Jensen, Sage Abdullah)
|
||||
* Support specifying a `get_object_list` method on `ChooserViewSet` (Matt Westcott)
|
||||
* Add `linked_fields` mechanism on chooser widgets to allow choices to be limited by fields on the calling page (Matt Westcott)
|
||||
* Add support for merging cells within `TableBlock` with the [`mergedCells` option](table_block_options) (Gareth Palmer)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
|
|
|
@ -164,11 +164,24 @@ class TableBlock(FieldBlock):
|
|||
|
||||
if value.get("cell"):
|
||||
new_context["classnames"] = {}
|
||||
new_context["hidden"] = {}
|
||||
for meta in value["cell"]:
|
||||
if "className" in meta:
|
||||
new_context["classnames"][(meta["row"], meta["col"])] = meta[
|
||||
"className"
|
||||
]
|
||||
if "hidden" in meta:
|
||||
new_context["hidden"][(meta["row"], meta["col"])] = meta[
|
||||
"hidden"
|
||||
]
|
||||
|
||||
if value.get("mergeCells"):
|
||||
new_context["spans"] = {}
|
||||
for merge in value["mergeCells"]:
|
||||
new_context["spans"][(merge["row"], merge["col"])] = {
|
||||
"rowspan": merge["rowspan"],
|
||||
"colspan": merge["colspan"],
|
||||
}
|
||||
|
||||
return render_to_string(template, new_context)
|
||||
else:
|
||||
|
|
|
@ -9,15 +9,18 @@
|
|||
<tr>
|
||||
{% for column in table_header %}
|
||||
{% with forloop.counter0 as col_index %}
|
||||
<th scope="col" {% cell_classname 0 col_index %}>
|
||||
{% if column.strip %}
|
||||
{% if html_renderer %}
|
||||
{{ column.strip|safe|linebreaksbr }}
|
||||
{% else %}
|
||||
{{ column.strip|linebreaksbr }}
|
||||
{% cell_hidden 0 col_index as is_hidden %}
|
||||
{% if not is_hidden %}
|
||||
<th scope="col" {% cell_classname 0 col_index %} {% cell_span 0 col_index %}>
|
||||
{% if column.strip %}
|
||||
{% if html_renderer %}
|
||||
{{ column.strip|safe|linebreaksbr }}
|
||||
{% else %}
|
||||
{{ column.strip|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</th>
|
||||
</th>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
|
@ -29,26 +32,29 @@
|
|||
<tr>
|
||||
{% for column in row %}
|
||||
{% with forloop.counter0 as col_index %}
|
||||
{% if first_col_is_header and forloop.first %}
|
||||
<th scope="row" {% cell_classname row_index col_index table_header %}>
|
||||
{% if column.strip %}
|
||||
{% if html_renderer %}
|
||||
{{ column.strip|safe|linebreaksbr }}
|
||||
{% else %}
|
||||
{{ column.strip|linebreaksbr }}
|
||||
{% cell_hidden row_index col_index table_header as is_hidden %}
|
||||
{% if not is_hidden %}
|
||||
{% if first_col_is_header and forloop.first %}
|
||||
<th scope="row" {% cell_classname row_index col_index table_header %} {% cell_span row_index col_index table_header %}>
|
||||
{% if column.strip %}
|
||||
{% if html_renderer %}
|
||||
{{ column.strip|safe|linebreaksbr }}
|
||||
{% else %}
|
||||
{{ column.strip|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% else %}
|
||||
<td {% cell_classname row_index col_index table_header %}>
|
||||
{% if column.strip %}
|
||||
{% if html_renderer %}
|
||||
{{ column.strip|safe|linebreaksbr }}
|
||||
{% else %}
|
||||
{{ column.strip|linebreaksbr }}
|
||||
</th>
|
||||
{% else %}
|
||||
<td {% cell_classname row_index col_index table_header %} {% cell_span row_index col_index table_header %}>
|
||||
{% if column.strip %}
|
||||
{% if html_renderer %}
|
||||
{{ column.strip|safe|linebreaksbr }}
|
||||
{% else %}
|
||||
{{ column.strip|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
|
|
|
@ -15,3 +15,32 @@ def cell_classname(context, row_index, col_index, table_header=None):
|
|||
if cell_class:
|
||||
return mark_safe(f'class="{cell_class}"')
|
||||
return ""
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def cell_hidden(context, row_index, col_index, table_header=None):
|
||||
hidden = context.get("hidden")
|
||||
if hidden:
|
||||
if table_header is not None:
|
||||
row_index += 1
|
||||
index = (row_index, col_index)
|
||||
return hidden.get(index, False)
|
||||
return False
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def cell_span(context, row_index, col_index, table_header=None):
|
||||
spans = context.get("spans")
|
||||
if spans:
|
||||
if table_header is not None:
|
||||
row_index += 1
|
||||
index = (row_index, col_index)
|
||||
cell_span = spans.get(index)
|
||||
if cell_span:
|
||||
return mark_safe(
|
||||
'rowspan="{}" colspan="{}"'.format(
|
||||
cell_span["rowspan"],
|
||||
cell_span["colspan"],
|
||||
)
|
||||
)
|
||||
return ""
|
||||
|
|
|
@ -348,6 +348,40 @@ class TestTableBlock(TestCase):
|
|||
self.assertHTMLEqual(result, expected)
|
||||
self.assertNotIn("None", result)
|
||||
|
||||
def test_merge_cells_render(self):
|
||||
"""
|
||||
Test that merged table cells are rendered.
|
||||
"""
|
||||
value = {
|
||||
"first_row_is_table_header": False,
|
||||
"first_col_is_header": False,
|
||||
"data": [
|
||||
["one", None, "two"],
|
||||
["three", "four", "five"],
|
||||
["six", "seven", None],
|
||||
],
|
||||
"cell": [
|
||||
{"row": 0, "col": 1, "hidden": True},
|
||||
{"row": 2, "col": 2, "hidden": True},
|
||||
],
|
||||
"mergeCells": [
|
||||
{"row": 0, "col": 0, "rowspan": 1, "colspan": 2},
|
||||
{"row": 1, "col": 2, "rowspan": 2, "colspan": 1},
|
||||
],
|
||||
}
|
||||
block = TableBlock()
|
||||
result = block.render(value)
|
||||
expected = """
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td rowspan="1" colspan="2">one</td><td>two</td></tr>
|
||||
<tr><td>three</td><td>four</td><td rowspan="2" colspan="1">five</td></tr>
|
||||
<tr><td>six</td><td>seven</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
"""
|
||||
self.assertHTMLEqual(result, expected)
|
||||
|
||||
|
||||
class TestTableBlockForm(WagtailTestUtils, SimpleTestCase):
|
||||
def setUp(self):
|
||||
|
|
Ładowanie…
Reference in New Issue