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)
|
* Allow overriding `IndexView.export_headings` via `ModelViewSet` (Christer Jensen, Sage Abdullah)
|
||||||
* Support specifying a `get_object_list` method on `ChooserViewSet` (Matt Westcott)
|
* 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 `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: 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: 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)
|
* Fix: When deleting collections, ensure the collection name is correctly shown in the success message (LB (Ben) Johnston)
|
||||||
|
|
|
@ -726,6 +726,7 @@
|
||||||
* Shreshth Srivastava
|
* Shreshth Srivastava
|
||||||
* Sandeep Choudhary
|
* Sandeep Choudhary
|
||||||
* Antoni Martyniuk
|
* Antoni Martyniuk
|
||||||
|
* Gareth Palmer
|
||||||
|
|
||||||
## Translators
|
## Translators
|
||||||
|
|
||||||
|
|
|
@ -77,26 +77,52 @@ function initTable(id, tableOptions) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCellsClassnames = function () {
|
const persist = function () {
|
||||||
const meta = hot.getCellsMeta();
|
const cell = [];
|
||||||
const cellsClassnames = [];
|
const mergeCells = [];
|
||||||
for (let i = 0; i < meta.length; i += 1) {
|
const cellsMeta = hot.getCellsMeta();
|
||||||
if (hasOwn(meta[i], 'className')) {
|
|
||||||
cellsClassnames.push({
|
cellsMeta.forEach((meta) => {
|
||||||
row: meta[i].row,
|
let className;
|
||||||
col: meta[i].col,
|
let hidden;
|
||||||
className: meta[i].className,
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return cellsClassnames;
|
|
||||||
};
|
|
||||||
|
|
||||||
const persist = function () {
|
|
||||||
hiddenStreamInput.val(
|
hiddenStreamInput.val(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
data: hot.getData(),
|
data: hot.getData(),
|
||||||
cell: getCellsClassnames(),
|
cell: cell,
|
||||||
|
mergeCells: mergeCells,
|
||||||
first_row_is_table_header: tableHeaderCheckbox.prop('checked'),
|
first_row_is_table_header: tableHeaderCheckbox.prop('checked'),
|
||||||
first_col_is_header: colHeaderCheckbox.prop('checked'),
|
first_col_is_header: colHeaderCheckbox.prop('checked'),
|
||||||
table_caption: tableCaption.val(),
|
table_caption: tableCaption.val(),
|
||||||
|
@ -105,7 +131,7 @@ function initTable(id, tableOptions) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const cellEvent = function (change, source) {
|
const cellEvent = function (change, source) {
|
||||||
if (source === 'loadData') {
|
if (!isInitialized || source === 'loadData' || source === 'MergeCells') {
|
||||||
return; // don't save this change
|
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 () {
|
const initEvent = function () {
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
};
|
};
|
||||||
|
@ -148,6 +188,8 @@ function initTable(id, tableOptions) {
|
||||||
afterRemoveCol: structureEvent,
|
afterRemoveCol: structureEvent,
|
||||||
afterRemoveRow: structureEvent,
|
afterRemoveRow: structureEvent,
|
||||||
afterSetCellMeta: metaEvent,
|
afterSetCellMeta: metaEvent,
|
||||||
|
afterMergeCells: mergeEvent,
|
||||||
|
afterUnmergeCells: unmergeEvent,
|
||||||
afterInit: initEvent,
|
afterInit: initEvent,
|
||||||
// contextMenu set via init, from server defaults
|
// contextMenu set via init, from server defaults
|
||||||
};
|
};
|
||||||
|
@ -169,6 +211,13 @@ function initTable(id, tableOptions) {
|
||||||
finalOptions[key] = tableOptions[key];
|
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);
|
hot = new Handsontable(document.getElementById(containerId), finalOptions);
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
// Render the table. Calling render also removes 'null' literals from empty cells.
|
// Render the table. Calling render also removes 'null' literals from empty cells.
|
||||||
|
|
|
@ -85,6 +85,8 @@ default_table_options = {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(table_block_options)=
|
||||||
|
|
||||||
### Configuration 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:
|
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.
|
- [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.
|
- [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`.
|
- [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.
|
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)
|
* Allow overriding `IndexView.export_headings` via `ModelViewSet` (Christer Jensen, Sage Abdullah)
|
||||||
* Support specifying a `get_object_list` method on `ChooserViewSet` (Matt Westcott)
|
* 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 `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
|
### Bug fixes
|
||||||
|
|
||||||
|
|
|
@ -164,11 +164,24 @@ class TableBlock(FieldBlock):
|
||||||
|
|
||||||
if value.get("cell"):
|
if value.get("cell"):
|
||||||
new_context["classnames"] = {}
|
new_context["classnames"] = {}
|
||||||
|
new_context["hidden"] = {}
|
||||||
for meta in value["cell"]:
|
for meta in value["cell"]:
|
||||||
if "className" in meta:
|
if "className" in meta:
|
||||||
new_context["classnames"][(meta["row"], meta["col"])] = meta[
|
new_context["classnames"][(meta["row"], meta["col"])] = meta[
|
||||||
"className"
|
"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)
|
return render_to_string(template, new_context)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -9,7 +9,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
{% for column in table_header %}
|
{% for column in table_header %}
|
||||||
{% with forloop.counter0 as col_index %}
|
{% with forloop.counter0 as col_index %}
|
||||||
<th scope="col" {% cell_classname 0 col_index %}>
|
{% 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 column.strip %}
|
||||||
{% if html_renderer %}
|
{% if html_renderer %}
|
||||||
{{ column.strip|safe|linebreaksbr }}
|
{{ column.strip|safe|linebreaksbr }}
|
||||||
|
@ -18,6 +20,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</th>
|
</th>
|
||||||
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -29,8 +32,10 @@
|
||||||
<tr>
|
<tr>
|
||||||
{% for column in row %}
|
{% for column in row %}
|
||||||
{% with forloop.counter0 as col_index %}
|
{% with forloop.counter0 as col_index %}
|
||||||
|
{% cell_hidden row_index col_index table_header as is_hidden %}
|
||||||
|
{% if not is_hidden %}
|
||||||
{% if first_col_is_header and forloop.first %}
|
{% if first_col_is_header and forloop.first %}
|
||||||
<th scope="row" {% cell_classname row_index col_index table_header %}>
|
<th scope="row" {% cell_classname row_index col_index table_header %} {% cell_span row_index col_index table_header %}>
|
||||||
{% if column.strip %}
|
{% if column.strip %}
|
||||||
{% if html_renderer %}
|
{% if html_renderer %}
|
||||||
{{ column.strip|safe|linebreaksbr }}
|
{{ column.strip|safe|linebreaksbr }}
|
||||||
|
@ -40,7 +45,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</th>
|
</th>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td {% cell_classname row_index col_index table_header %}>
|
<td {% cell_classname row_index col_index table_header %} {% cell_span row_index col_index table_header %}>
|
||||||
{% if column.strip %}
|
{% if column.strip %}
|
||||||
{% if html_renderer %}
|
{% if html_renderer %}
|
||||||
{{ column.strip|safe|linebreaksbr }}
|
{{ column.strip|safe|linebreaksbr }}
|
||||||
|
@ -50,6 +55,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -15,3 +15,32 @@ def cell_classname(context, row_index, col_index, table_header=None):
|
||||||
if cell_class:
|
if cell_class:
|
||||||
return mark_safe(f'class="{cell_class}"')
|
return mark_safe(f'class="{cell_class}"')
|
||||||
return ""
|
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.assertHTMLEqual(result, expected)
|
||||||
self.assertNotIn("None", result)
|
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):
|
class TestTableBlockForm(WagtailTestUtils, SimpleTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
Ładowanie…
Reference in New Issue