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';
|
||||
} 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 = {};
|
||||
|
|
|
@ -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