Typed table block - initial block class and client-side mechanism for adding columns/rows

pull/7590/head
Matt Westcott 2021-08-27 13:57:34 +01:00 zatwierdzone przez Matt Westcott
rodzic ff76931aa4
commit 614c23c9a0
5 zmienionych plików z 245 dodań i 0 usunięć

Wyświetl plik

@ -0,0 +1,160 @@
/* global $ */
import { escapeHtml as h } from '../../../utils/text';
export class TypedTableBlock {
constructor(blockDef, placeholder, prefix, initialState, initialError) {
const state = initialState || {};
this.blockDef = blockDef;
this.type = blockDef.name;
this.columns = [];
const dom = $(`
<div class="typed-table-block ${h(this.blockDef.meta.classname || '')}">
<table>
<thead>
<tr><th><button type="button" data-append-column>Add columns</button></th></tr>
</thead>
<tbody>
</tbody>
</table>
<button type="button" data-add-row>Add row</button>
</div>
`);
$(placeholder).replaceWith(dom);
this.thead = dom.find('table > thead').get(0);
this.tbody = dom.find('table > tbody').get(0);
this.appendColumnButton = dom.find('button[data-append-column]');
this.addRowButton = dom.find('button[data-add-row]');
this.addRowButton.hide();
if (this.blockDef.meta.helpText) {
// help text is left unescaped as per Django conventions
dom.append(`
<span>
<div class="help">
${this.blockDef.meta.helpIcon}
${this.blockDef.meta.helpText}
</div>
</span>
`);
}
this.addColumnCallback = null;
this.addColumnMenu = $('<ul></ul>');
this.blockDef.childBlockDefs.forEach(childBlockDef => {
const columnTypeButton = $('<button type="button"></button>').text(childBlockDef.meta.label);
columnTypeButton.on('click', () => {
if (this.addColumnCallback) this.addColumnCallback(childBlockDef);
this.hideAddColumnMenu();
});
const li = $('<li></li>').append(columnTypeButton);
this.addColumnMenu.append(li);
});
this.addColumnMenuBaseElement = null; // the element the add-column menu is attached to
this.appendColumnButton.on('click', () => {
this.toggleAddColumnMenu(this.appendColumnButton, (chosenBlockDef) => {
this.insertColumn(this.columns.length, chosenBlockDef);
});
});
this.addRowButton.on('click', () => {
this.addRow();
});
}
showAddColumnMenu(baseElement, callback) {
this.addColumnMenuBaseElement = baseElement;
baseElement.after(this.addColumnMenu);
this.addColumnMenu.show();
this.addColumnCallback = callback;
}
hideAddColumnMenu() {
this.addColumnMenu.hide();
this.addColumnMenuBaseElement = null;
}
toggleAddColumnMenu(baseElement, callback) {
if (this.addColumnMenuBaseElement === baseElement) {
this.hideAddColumnMenu();
} else {
this.showAddColumnMenu(baseElement, callback);
}
}
insertColumn(index, blockDef) {
const column = {
blockDef,
};
this.columns.splice(index, 0, column);
Array.from(this.thead.children).forEach(tr => {
const cells = tr.children;
const newCell = document.createElement('th');
tr.insertBefore(newCell, cells[index]);
});
Array.from(this.tbody.children).forEach(tr => {
const cells = tr.children;
const newCell = document.createElement('td');
tr.insertBefore(newCell, cells[index]);
this.initCell(newCell, blockDef);
});
/* after first column is added, enable adding rows */
this.addRowButton.show();
this.appendColumnButton.text('+');
/* if no rows exist, add an initial one */
if (this.tbody.children.length === 0) {
this.addRow();
}
}
addRow() {
const newRow = document.createElement('tr');
this.tbody.appendChild(newRow);
this.columns.forEach(column => {
const newCell = document.createElement('td');
newRow.appendChild(newCell);
this.initCell(newCell, column.blockDef);
});
}
initCell(cell, blockDef) {
const placeholder = document.createElement('div');
cell.appendChild(placeholder);
blockDef.render(placeholder, 'asdf', null, null);
}
setState(state) {
}
setError(errorList) {
if (errorList.length !== 1) {
return;
}
const error = errorList[0];
}
getState() {
}
getValue() {
}
getTextLabel(opts) {
// no usable label found
return null;
}
focus(opts) {
}
}
export class TypedTableBlockDefinition {
constructor(name, childBlockDefs, meta) {
this.name = name;
this.childBlockDefs = childBlockDefs;
this.meta = meta;
}
render(placeholder, prefix, initialState, initialError) {
return new TypedTableBlock(this, placeholder, prefix, initialState, initialError);
}
}
window.telepath.register('wagtail.contrib.typed_table_block.blocks.TypedTableBlock', TypedTableBlockDefinition);

Wyświetl plik

@ -9,6 +9,8 @@ const getOutputPath = (app, filename) => {
appLabel = 'wagtaildocs';
} else if (app === 'contrib/table_block') {
appLabel = 'table_block';
} else if (app === 'contrib/typed_table_block') {
appLabel = 'typed_table_block';
}
return path.join('wagtail', app, 'static', appLabel, 'js', filename);
@ -70,6 +72,9 @@ module.exports = function exports() {
'contrib/table_block': [
'table',
],
'contrib/typed_table_block': [
'typed_table_block',
],
};
const entry = {};

Wyświetl plik

@ -0,0 +1 @@
static

Wyświetl plik

@ -0,0 +1,79 @@
from django import forms
from django.utils.functional import cached_property
from wagtail.admin.staticfiles import versioned_static
from wagtail.core.blocks.base import Block, DeclarativeSubBlocksMetaclass, get_help_icon
from wagtail.core.telepath import Adapter, register
class BaseTypedTableBlock(Block):
def __init__(self, local_blocks=None, **kwargs):
self._constructor_kwargs = kwargs
super().__init__(**kwargs)
# create a local (shallow) copy of base_blocks so that it can be supplemented by local_blocks
self.child_blocks = self.base_blocks.copy()
if local_blocks:
for name, block in local_blocks:
block.set_name(name)
self.child_blocks[name] = block
def deconstruct(self):
"""
Always deconstruct TypedTableBlock instances as if they were plain TypedTableBlock with all
of the field definitions passed to the constructor - even if in reality this is a subclass
with the fields defined declaratively, or some combination of the two.
This ensures that the field definitions get frozen into migrations, rather than leaving a
reference to a custom subclass in the user's models.py that may or may not stick around.
"""
path = 'wagtail.contrib.typed_table_block.blocks.TypedTableBlock'
args = [list(self.child_blocks.items())]
kwargs = self._constructor_kwargs
return (path, args, kwargs)
def check(self, **kwargs):
errors = super().check(**kwargs)
for name, child_block in self.child_blocks.items():
errors.extend(child_block.check(**kwargs))
errors.extend(child_block._check_name(**kwargs))
return errors
class Meta:
default = None
icon = "table"
class TypedTableBlock(BaseTypedTableBlock, metaclass=DeclarativeSubBlocksMetaclass):
pass
class TypedTableBlockAdapter(Adapter):
js_constructor = 'wagtail.contrib.typed_table_block.blocks.TypedTableBlock'
def js_args(self, block):
meta = {
'label': block.label, 'required': block.required, 'icon': block.meta.icon,
}
help_text = getattr(block.meta, 'help_text', None)
if help_text:
meta['helpText'] = help_text
meta['helpIcon'] = get_help_icon()
return [
block.name,
block.child_blocks.values(),
meta,
]
@cached_property
def media(self):
return forms.Media(js=[
versioned_static('typed_table_block/js/typed_table_block.js'),
])
register(TypedTableBlockAdapter(), TypedTableBlock)