kopia lustrzana https://github.com/wagtail/wagtail
Typed table block - initial block class and client-side mechanism for adding columns/rows
rodzic
ff76931aa4
commit
614c23c9a0
|
@ -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);
|
|
@ -9,6 +9,8 @@ const getOutputPath = (app, filename) => {
|
||||||
appLabel = 'wagtaildocs';
|
appLabel = 'wagtaildocs';
|
||||||
} else if (app === 'contrib/table_block') {
|
} else if (app === 'contrib/table_block') {
|
||||||
appLabel = '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);
|
return path.join('wagtail', app, 'static', appLabel, 'js', filename);
|
||||||
|
@ -70,6 +72,9 @@ module.exports = function exports() {
|
||||||
'contrib/table_block': [
|
'contrib/table_block': [
|
||||||
'table',
|
'table',
|
||||||
],
|
],
|
||||||
|
'contrib/typed_table_block': [
|
||||||
|
'typed_table_block',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const entry = {};
|
const entry = {};
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
static
|
|
@ -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)
|
Ładowanie…
Reference in New Issue