kopia lustrzana https://github.com/wagtail/wagtail
Add initial Telepath code and integrate with BlockWidget
This is sufficient to render a CharBlock with `StreamField(blocks.CharBlock())`pull/6931/head
rodzic
8d0eff1717
commit
1bb4c62cd8
client
src/entrypoints/admin/telepath
wagtail/core
|
@ -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);
|
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -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);
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
]}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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)
|
Ładowanie…
Reference in New Issue