Add initial Telepath code and integrate with BlockWidget

This is sufficient to render a CharBlock with `StreamField(blocks.CharBlock())`
pull/6931/head
Matt Westcott 2020-11-30 17:56:57 +00:00
rodzic 8d0eff1717
commit 1bb4c62cd8
9 zmienionych plików z 306 dodań i 13 usunięć

Wyświetl plik

@ -0,0 +1,128 @@
/* eslint-disable */
function initBlockWidget(id) {
/*
Initialises the top-level element of a BlockWidget
(i.e. the form widget for a StreamField).
Receives the ID of a DOM element with the attributes:
data-block: JSON-encoded block definition to be passed to telepath.unpack
to obtain a Javascript representation of the block
(i.e. an instance of one of the Block classes below)
data-value: JSON-encoded value for this block
*/
var body = document.getElementById(id);
// unpack the block definition and value
var blockDefData = JSON.parse(body.dataset.block);
var blockDef = telepath.unpack(blockDefData);
var blockValue = JSON.parse(body.dataset.value);
// replace the 'body' element with the unpopulated HTML structure for the block
var block = blockDef.render(body, id);
// populate the block HTML with the value
block.setState(blockValue);
}
window.initBlockWidget = initBlockWidget;
class FieldBlock {
constructor(name, widget, meta) {
this.name = name;
this.widget = telepath.unpack(widget);
this.meta = meta;
}
render(placeholder, prefix) {
var html =$(`
<div>
<div class="field-content">
<div class="input">
<div data-streamfield-widget></div>
<span></span>
</div>
<p class="help"></p>
<p class="error-message"></p>
</div>
</div>
`);
var dom = $(html);
$(placeholder).replaceWith(dom);
var widgetElement = dom.find('[data-streamfield-widget]').get(0);
var boundWidget = this.widget.render(widgetElement, prefix, prefix);
return {
'setState': function(state) {
boundWidget.setState(state);
},
'getState': function() {
boundWidget.getState();
},
'getValue': function() {
boundWidget.getValue();
},
};
}
}
telepath.register('wagtail.blocks.FieldBlock', FieldBlock);
class StructBlock {
constructor(name, childBlocks, meta) {
this.name = name;
this.childBlocks = childBlocks.map((child) => {return telepath.unpack(child);});
this.meta = meta;
}
render(placeholder, prefix) {
var html = $(`
<div class="{{ classname }}">
<span>
<div class="help">
<span class="icon-help-inverse" aria-hidden="true"></span>
</div>
</span>
</div>
`);
var dom = $(html);
$(placeholder).replaceWith(dom);
var boundBlocks = {};
this.childBlocks.forEach(childBlock => {
var childHtml = $(`
<div class="field">
<label class="field__label"></label>
<div data-streamfield-block></div>
</div>
`);
var childDom = $(childHtml);
dom.append(childDom);
var label = childDom.find('.field__label');
label.text(childBlock.meta.label);
var childBlockElement = childDom.find('[data-streamfield-block]').get(0);
var boundBlock = childBlock.render(childBlockElement, prefix + '-' + childBlock.name);
boundBlocks[childBlock.name] = boundBlock;
});
return {
'setState': function(state) {
for (name in state) {
boundBlocks[name].setState(state[name]);
}
},
'getState': function() {
var state = {};
for (name in boundBlocks) {
state[name] = boundBlocks[name].getState();
}
return state;
},
'getValue': function() {
var value = {};
for (name in boundBlocks) {
value[name] = boundBlocks[name].getValue();
}
return value;
},
};
}
}
telepath.register('wagtail.blocks.StructBlock', StructBlock);

Wyświetl plik

@ -0,0 +1,12 @@
/* eslint-disable */
window.telepath = {
constructors: {},
register: function(name, constructor) {
this.constructors[name] = constructor;
},
unpack: function(objData) {
var [constructorName, ...args] = objData;
var constructor = this.constructors[constructorName];
return new constructor(...args);
}
};

Wyświetl plik

@ -0,0 +1,56 @@
/* eslint-disable */
class BoundWidget {
constructor(element, name) {
var selector = ':input[name="' + name + '"]';
this.input = element.find(selector).addBack(selector); // find, including element itself
}
getValue() {
return this.input.val();
}
getState() {
return this.input.val();
}
setState(state) {
this.input.val(state);
}
}
class Widget {
constructor(html, idForLabel) {
this.html = html;
this.idForLabel = idForLabel;
}
boundWidgetClass = BoundWidget;
render(placeholder, name, id) {
var html = this.html.replace(/__NAME__/g, name).replace(/__ID__/g, id);
var dom = $(html);
$(placeholder).replaceWith(dom);
return new this.boundWidgetClass(dom, name);
}
}
telepath.register('wagtail.widgets.Widget', Widget);
class BoundRadioSelect {
constructor(element, name) {
this.element = element;
this.name = name;
this.selector = 'input[name="' + name + '"]:checked';
}
getValue() {
return this.element.find(this.selector).val();
}
getState() {
return this.element.find(this.selector).val();
}
setState(state) {
this.element.find('input[name="' + this.name + '"]').val([state]);
}
}
class RadioSelect extends Widget {
boundWidgetClass = BoundRadioSelect;
}
telepath.register('wagtail.widgets.RadioSelect', RadioSelect);

Wyświetl plik

@ -39,6 +39,9 @@ module.exports = function exports() {
'privacy-switch',
'task-chooser-modal',
'task-chooser',
'telepath/blocks',
'telepath/telepath',
'telepath/widgets',
'userbar',
'wagtailadmin',
'workflow-action',

Wyświetl plik

@ -10,3 +10,5 @@ class WagtailCoreAppConfig(AppConfig):
def ready(self):
from wagtail.core.signal_handlers import register_signal_handlers
register_signal_handlers()
from wagtail.core import widget_adapters # noqa

Wyświetl plik

@ -1,4 +1,5 @@
import collections
import json
import re
from importlib import import_module
@ -8,9 +9,12 @@ from django.core import checks
from django.core.exceptions import ImproperlyConfigured
from django.template.loader import render_to_string
from django.utils.encoding import force_str
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from wagtail.core.telepath import JSContext
__all__ = ['BaseBlock', 'Block', 'BoundBlock', 'DeclarativeSubBlocksMetaclass', 'BlockWidget', 'BlockField']
@ -549,29 +553,33 @@ class BlockWidget(forms.Widget):
def __init__(self, block_def, attrs=None):
super().__init__(attrs=attrs)
self.block_def = block_def
self.js_context = JSContext()
self.block_json = json.dumps(self.js_context.pack(self.block_def))
def render_with_errors(self, name, value, attrs=None, errors=None, renderer=None):
bound_block = self.block_def.bind(value, prefix=name, errors=errors)
js_initializer = self.block_def.js_initializer()
if js_initializer:
js_snippet = """
value_json = json.dumps("Hello world!")
return format_html(
"""
<div id="{id}" data-block="{block_json}" data-value="{value_json}"></div>
<script>
$(function() {
var initializer = %s;
initializer('%s');
})
initBlockWidget('{id}');
</script>
""" % (js_initializer, name)
else:
js_snippet = ''
return mark_safe(bound_block.render_form() + js_snippet)
""",
id=name, block_json=self.block_json, value_json=value_json
)
def render(self, name, value, attrs=None, renderer=None):
return self.render_with_errors(name, value, attrs=attrs, errors=None, renderer=renderer)
@property
def media(self):
return self.block_def.all_media() + forms.Media(
return self.js_context.media + forms.Media(
js=[
# needed for initBlockWidget, although these will almost certainly be
# pulled in by the block adapters too
'wagtailadmin/js/telepath/telepath.js',
'wagtailadmin/js/telepath/blocks.js',
],
css={'all': [
'wagtailadmin/css/panels/streamfield.css',
]}

Wyświetl plik

@ -10,7 +10,9 @@ from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from wagtail.admin.staticfiles import versioned_static
from wagtail.core.rich_text import RichText, get_text_for_indexing
from wagtail.core.telepath import Adapter, register
from wagtail.core.utils import resolve_model_string
from .base import Block
@ -94,6 +96,23 @@ class FieldBlock(Block):
default = None
class FieldBlockAdapter(Adapter):
js_constructor = 'wagtail.blocks.FieldBlock'
def js_args(self, block, context):
return [
block.name,
context.pack(block.field.widget),
{'label': block.label, 'required': block.required, 'icon': block.meta.icon},
]
class Media:
js = [versioned_static('wagtailadmin/js/telepath/blocks.js')]
register(FieldBlockAdapter(), FieldBlock)
class CharBlock(FieldBlock):
def __init__(self, required=True, help_text=None, max_length=None, min_length=None, validators=(), **kwargs):

Wyświetl plik

@ -0,0 +1,31 @@
from django import forms
from django.forms import MediaDefiningClass
adapters = {}
def register(adapter, cls):
adapters[cls] = adapter
class JSContext:
def __init__(self):
self.media = forms.Media(js=['wagtailadmin/js/telepath/telepath.js'])
self.objects = {}
def pack(self, obj):
for cls in type(obj).__mro__:
adapter = adapters.get(cls)
if adapter:
break
if adapter is None:
raise Exception("don't know how to add object to JS context: %r" % obj)
self.media += adapter.media
return [adapter.js_constructor, *adapter.js_args(obj, self)]
class Adapter(metaclass=MediaDefiningClass):
pass

Wyświetl plik

@ -0,0 +1,34 @@
"""
Register Telepath adapters for core Django form widgets, so that they can
have corresponding Javascript objects with the ability to render new instances
and extract field values.
"""
from django import forms
from wagtail.core.telepath import Adapter, register
class WidgetAdapter(Adapter):
js_constructor = 'wagtail.widgets.Widget'
def js_args(self, widget, context):
return [
widget.render('__NAME__', None, attrs={'id': '__ID__'}),
widget.id_for_label('__ID__'),
]
class Media:
js = ['wagtailadmin/js/telepath/widgets.js']
register(WidgetAdapter(), forms.widgets.Input)
register(WidgetAdapter(), forms.Textarea)
register(WidgetAdapter(), forms.Select)
class RadioSelectAdapter(WidgetAdapter):
js_constructor = 'wagtail.widgets.RadioSelect'
register(RadioSelectAdapter(), forms.RadioSelect)