浏览代码

Add initial Telepath code and integrate with BlockWidget

This is sufficient to render a CharBlock with `StreamField(blocks.CharBlock())`
Matt Westcott 4 年之前
父节点
当前提交
1bb4c62cd8

+ 128 - 0
client/src/entrypoints/admin/telepath/blocks.js

@@ -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);

+ 12 - 0
client/src/entrypoints/admin/telepath/telepath.js

@@ -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);
+    }
+};

+ 56 - 0
client/src/entrypoints/admin/telepath/widgets.js

@@ -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);

+ 3 - 0
client/webpack.config.js

@@ -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',

+ 2 - 0
wagtail/core/apps.py

@@ -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

+ 21 - 13
wagtail/core/blocks/base.py

@@ -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',
             ]}

+ 19 - 0
wagtail/core/blocks/field_block.py

@@ -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):

+ 31 - 0
wagtail/core/telepath.py

@@ -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

+ 34 - 0
wagtail/core/widget_adapters.py

@@ -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)