├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── SPONSORS.md ├── package-lock.json ├── package.json ├── requirements.txt ├── setup.py ├── wagtail-react-streamfield-screenshot-1.png ├── wagtail-react-streamfield-screenshot-2.png ├── wagtail-react-streamfield-screenshot-3.png ├── wagtail_react_streamfield ├── __init__.py ├── apps.py ├── blocks │ ├── __init__.py │ ├── block.py │ ├── field_block.py │ ├── list_block.py │ ├── static_block.py │ ├── stream_block.py │ └── struct_block.py ├── edit_handlers.py ├── exceptions.py ├── monkey_patch.py ├── static │ ├── table_block │ │ └── js │ │ │ └── table.js │ ├── wagtailadmin │ │ ├── fonts │ │ │ └── wagtail.woff │ │ └── js │ │ │ ├── hallo-bootstrap.js │ │ │ └── page-chooser.js │ ├── wagtaildocs │ │ └── js │ │ │ └── document-chooser.js │ ├── wagtailimages │ │ └── js │ │ │ └── image-chooser.js │ └── wagtailsnippets │ │ └── js │ │ └── snippet-chooser.js ├── static_src │ ├── js │ │ └── entry.js │ └── scss │ │ └── entry.scss ├── templates │ └── wagtailadmin │ │ └── block_forms │ │ └── blocks_container.html └── widgets.py └── webpack.config.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: actions/setup-node@v1 9 | with: 10 | node-version: 14.x 11 | - run: npm ci --no-optional --no-audit --progress=false --no-fund 12 | - run: npm run build 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | .DS_Store 4 | /.coverage 5 | /dist/ 6 | /build/ 7 | /MANIFEST 8 | /wagtail.egg-info/ 9 | /docs/_build/ 10 | /.tox/ 11 | /venv 12 | /node_modules/ 13 | npm-debug.log* 14 | *.idea/ 15 | /*.egg/ 16 | /.cache/ 17 | /.pytest_cache/ 18 | /wagtail_react_streamfield.egg-info/ 19 | 20 | ### JetBrains 21 | .idea/ 22 | *.iml 23 | *.ipr 24 | *.iws 25 | coverage/ 26 | client/node_modules 27 | 28 | ### vscode 29 | .vscode 30 | 31 | wagtail_react_streamfield/static/js 32 | wagtail_react_streamfield/static/css 33 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## 1.3.6 6 | 7 | - Add vendor prefixes to the project’s stylesheet, to ensure styles are cross-browser-compatible (`user-select`, `appearance`, and `backface-visibility`). 8 | - Fix Django 3.0 compatibility issue due to removed `django.utils.six` ([#55](https://github.com/wagtail/wagtail-react-streamfield/issues/55), [#56](https://github.com/wagtail/wagtail-react-streamfield/pull/56)). Thanks to [@lennsa](https://github.com/lennsa), [@colinappnovation](https://github.com/colinappnovation), and [@zerolab](https://github.com/zerolab). 9 | 10 | ## 1.3.5 11 | 12 | Fixes validation errors in nested StructBlocks or ListBlocks 13 | 14 | ## 1.3.4 15 | 16 | Fixes the broken JavaScript from TableBlock 17 | 18 | ## 1.3.3 19 | 20 | Fixes a packaging issue due to a setuptools bug (it doesn’t clean its 21 | workspace before rebuilding, leading to extra unwanted files in the package) 22 | 23 | ## 1.3.2 24 | 25 | Fixes an issue with collapsible `FieldPanel` 26 | (nothing related to StreamFields) no longer working properly due to an 27 | overridden JavaScript file lagging behind the official version 28 | 29 | ## 1.3.1 30 | 31 | Fixes another issue with radio buttons (the serialization was not right) 32 | 33 | ## 1.3.0 34 | 35 | - Dropped Wagtail < 2.6 support 36 | - Removes layouts, only a single layout exists now, a mix of the best parts of 37 | the former `SIMPLE` of `COLLAPSIBLE` 38 | - Adds `closed` attribute in the `Meta` of blocks to set by block type 39 | whether they should be closed on page editor load (defaults to `False`) 40 | - Adds support for collapsible nested struct blocks 41 | - Speeds up server-side page editor load 42 | - Fixes two issues with radio buttons 43 | - Fixes an issue with missing IDs in StreamBlocks 44 | (including the root StreamBlock) 45 | - Fixes callable default values 46 | - Fixes the dynamic title of the children of a `ListBlock` 47 | - Fixes minor CSS details 48 | - Renamed `Block.get_definition()` to a `Block.definition` cached property 49 | (overriding it with a simple `@property` works, but it will be faster 50 | with `@cached_property` from `django.utils.functional`) 51 | 52 | ## 1.2.0 53 | 54 | - Dropped Wagtail 2.2 & 2.3 support, but it may work perfectly with both these 55 | versions, it was not tested on these 56 | - Moves the plusses between blocks (they were in the left gutter) 57 | - Adds the duplicate icon that was missing from the MANIFEST.in 58 | - Fixes `COLLAPSIBLE` block content previews on non-struct blocks 59 | - Rewrites the SCSS using BEM to avoid clashes with external CSS 60 | - Countless minor visual fixes 61 | - Major rewrite to improve code quality and extensibility 62 | 63 | ## 1.1.1 64 | 65 | - Fixes a bug was introduced in 1.1.0 when validation fails while saving 66 | - Removes a rectangle visible on Safari due to a weird unicode character 67 | added by accident 68 | 69 | ## 1.1.0 70 | 71 | - Automatically collapses blocks on small/mobile devices 72 | - Increases the size of block type selection buttons while making the labels 73 | uppercase 74 | - Adds support for help text in StreamBlock, ListBlock & StructBlock 75 | - Adds support for default values in StreamBlock, ListBlock & StructBlock 76 | - Adds support for non-block errors in non-root StreamBlock, ListBlock 77 | & StructBlock 78 | - Applies default values to missing sub-blocks of already saved StructBlocks 79 | - Fixes the remaining edge cases found by updating tests upstream, 80 | in https://github.com/wagtail/wagtail/pull/4942 81 | 82 | ## 1.0.6 83 | 84 | Fixes a formatting issue with `AdminDateInput` & `AdminDateTimeInput` 85 | due to Wagtail’s custom formatting 86 | (`WAGTAIL_DATE_FORMAT` & `WAGTAIL_DATETIME_FORMAT`) 87 | 88 | ## 1.0.5 89 | 90 | - Adds a real duplicate icon 91 | - Fix a recently introduced bug raising a 500 error when saving a page 92 | with validation errors 93 | 94 | ## 1.0.4 95 | 96 | - Makes block type styling consistent 97 | - Maximizes the action buttons padding 98 | 99 | ## 1.0.3 100 | 101 | - Changes the teal color to the new color from Wagtail 2.3 102 | - Improves margins and paddings consistency with Wagtail 103 | - Fixes an issue on mobile devices with the panel for adding new blocks 104 | that jumps during its transition 105 | 106 | ## 1.0.2 107 | 108 | - Improves mobile layout 109 | - Enlarges the clickable area of add buttons 110 | 111 | ## 1.0.1 112 | 113 | - Fixes a bug where `COLLAPSIBLE` blocks 114 | couldn’t be defined as open by default 115 | - Fixes the version number 116 | 117 | ## 1.0.0 118 | 119 | - Changes the overall look to match latest design decisions 120 | - Adds the `SIMPLE` layout 121 | - Makes `SIMPLE` the new default layout 122 | for a better continuity with the old StreamField 123 | - Allows to customize the layout by overwriting 124 | the `Block.get_layout()` method 125 | - Use Wagtail icons instead of FontAwesome icons 126 | - Fixes the remaining CSS integration issues 127 | 128 | ## 0.9.0 129 | 130 | - Adds Wagtail 2.3 support 131 | - Adds support for block groups 132 | - Adds support for static blocks 133 | - Upgrades to react-beautiful-dnd 10, improving fluidity by 30% 134 | 135 | ## 0.8.6 136 | 137 | - Fixes default values support 138 | - Removes Wagtail 2.0 & 2.1 support to fix chooser blocks 139 | 140 | ## 0.8.5 141 | 142 | - Adds `min_num` and `max_num` support for `ListBlock` 143 | - Fixes duplication of remaining unsupported blocks: `ChooserBlock` & `DateBlock` 144 | - Fixes rendering of errors on non-chooser blocks 145 | - Fixes a Python error when migrations use combinations of `ListBlock` with `StructBlock` 146 | - Removes the confirm dialog shown when leaving the page without changes 147 | 148 | ## 0.8.4 149 | 150 | - Fixes loading of Draftail RichTextBlock in some _scenarii_ 151 | 152 | ## 0.8.3 153 | 154 | - Fixes loading and duplication of TableBlock, Hallo.js RichTextBlocks 155 | - Fixes Draftail RichTextBlock duplication 156 | - Avoids showing a confirm when exiting an unmodified page 157 | - Fixes handling of custom empty block values 158 | - Fixes handling of extra undefined data 159 | 160 | ## 0.8.2 161 | 162 | - Adds `max_num` support 163 | - Adds a transition when using move arrows 164 | - Adds a transition on the panel listing the block types to add 165 | - Fixes StructBlock as a StructBlock field 166 | 167 | ## 0.8.1 168 | 169 | - Automatically opens blocks with errors while adding a red highlight 170 | - Fixes the load of JavaScript widgets such as RichTextField & ChooserPanels 171 | 172 | ## 0.8.0 173 | 174 | First working version with all essential features 175 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | ## Development 4 | 5 | ### Installation 6 | 7 | > Requirements: `nvm` 8 | 9 | ```sh 10 | git clone git@github.com:wagtail/wagtail-react-streamfield.git 11 | cd wagtail-react-streamfield/ 12 | # Use the correct Node version. 13 | nvm use 14 | # Run the static files’ build. 15 | npm run build 16 | ``` 17 | 18 | ## Releases 19 | 20 | - Update the [CHANGELOG](https://github.com/wagtail/wagtail-react-streamfield/blob/master/CHANGELOG.md). 21 | - Update the version number in `wagtail-react-streamfield/__init__.py`. 22 | - Commit 23 | - `rm dist/* ; python setup.py sdist bdist_wheel; twine upload dist/*` 24 | - Finally, go to GitHub and create a release and a tag for the new version. 25 | - Done! 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, NoriPyt 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of wagtail-react-streamfield nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE CHANGELOG.md requirements.txt 2 | recursive-include wagtail_react_streamfield *.js *.css *.html *.woff 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **No longer maintained**: Features have been merged into Wagtail see `2.13 Release Notes `_. 2 | 3 | Wagtail React StreamField 4 | ========================= 5 | 6 | .. image:: http://img.shields.io/pypi/v/wagtail-react-streamfield.svg 7 | :target: https://pypi.python.org/pypi/wagtail-react-streamfield 8 | .. image:: https://img.shields.io/pypi/dm/wagtail-react-streamfield.svg 9 | :target: https://pypi.python.org/pypi/wagtail-react-streamfield 10 | .. image:: https://github.com/wagtail/wagtail-react-streamfield/workflows/CI/badge.svg 11 | :target: https://github.com/wagtail/wagtail-react-streamfield/actions 12 | 13 | Drop-in replacement for the StreamField in `Wagtail `_. 14 | 15 | This work was funded thanks to 16 | `a Kickstarter campaign `_! 17 | 18 | It relies on `react-streamfield `_, 19 | a React package created for the occasion. 20 | 21 | **This work is currently in beta phase and will in the end be merged in Wagtail.** 22 | You should be careful and manually check that it works for your own StreamField 23 | and report any bug you find. 24 | 25 | 26 | Requirements 27 | ------------ 28 | 29 | Wagtail 2.6 or above. 30 | 31 | 32 | Getting started 33 | --------------- 34 | 35 | It’s really easy to setup, like most NoriPyt packages: 36 | 37 | - ``pip install wagtail-react-streamfield`` 38 | - Add ``'wagtail_react_streamfield',`` to your ``INSTALLED_APPS`` 39 | **before** ``'wagtail.admin'``, ``'wagtail.images'``, ``'wagtail.docs'`` 40 | & ``'wagtail.snippets'`` 41 | 42 | That’s it! 43 | 44 | 45 | Usage 46 | ----- 47 | 48 | wagtail-react-streamfield has the same class API as the regular StreamField. 49 | What changes: 50 | 51 | ``Meta`` attributes (or passed to __init__) 52 | ........................................... 53 | 54 | ``closed`` 55 | Set to ``True`` to close all blocks of this type when loading the page. 56 | Defaults to ``False``. 57 | 58 | 59 | Screenshots 60 | ----------- 61 | 62 | .. image:: https://raw.github.com/noripyt/wagtail-react-streamfield/master/wagtail-react-streamfield-screenshot-1.png 63 | .. image:: https://raw.github.com/noripyt/wagtail-react-streamfield/master/wagtail-react-streamfield-screenshot-2.png 64 | .. image:: https://raw.github.com/noripyt/wagtail-react-streamfield/master/wagtail-react-streamfield-screenshot-3.png 65 | -------------------------------------------------------------------------------- /SPONSORS.md: -------------------------------------------------------------------------------- 1 | # Sponsors 2 | 3 | These companies sponsored this work during 4 | [a Kickstarter campaign](https://kickstarter.com/projects/noripyt/wagtails-first-hatch): 5 | 6 | - [Springload](https://springload.nz/) 7 | - [NetFM](https://netfm.org/) 8 | - [Ambient Innovation](https://ambient-innovation.com/) 9 | - [Shenberger Technology](http://shenbergertech.com/) 10 | - [Type/Code](https://typecode.com/) 11 | - [SharperTool](http://sharpertool.com/) 12 | - [Overcast Software](https://www.overcast.io/) 13 | - [Octave](https://octave.nz/) 14 | - [Taywa](https://www.taywa.ch/) 15 | - [Rock Kitchen Harris](https://www.rkh.co.uk/) 16 | - [The Motley Fool](http://www.fool.com/) 17 | - [R Strother Scott](https://twitter.com/rstrotherscott) 18 | - [Beyond Media](http://beyond.works/) 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "core-js": "^2.6.5", 5 | "custom-event-polyfill": "^1.0.6", 6 | "element-closest": "^3.0.1", 7 | "prop-types": "^15.6.0", 8 | "react": "^16.7.0", 9 | "react-dom": "^16.7.0", 10 | "react-redux": "^5.1.1", 11 | "react-streamfield": "^0.9.5", 12 | "redux": "^4.0.1", 13 | "redux-thunk": "^2.3.0" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "^7.1.6", 17 | "@babel/plugin-proposal-class-properties": "^7.1.0", 18 | "@babel/plugin-proposal-decorators": "^7.1.6", 19 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 20 | "@babel/preset-env": "^7.1.6", 21 | "@babel/preset-react": "^7.0.0", 22 | "autoprefixer": "^9.4.5", 23 | "babel-loader": "8.0.4", 24 | "babel-plugin-transform-react-remove-prop-types": "^0.4.21", 25 | "css-loader": "1.0.1", 26 | "mini-css-extract-plugin": "0.5.0", 27 | "postcss-loader": "^3.0.0", 28 | "sass": "^1.26.9", 29 | "sass-loader": "^8.0.2", 30 | "webpack": "^4.28.2", 31 | "webpack-cli": "^3.1.2" 32 | }, 33 | "browserslist": [ 34 | "Firefox ESR", 35 | "ie 11", 36 | "last 2 Chrome versions", 37 | "last 2 ChromeAndroid versions", 38 | "last 2 Edge versions", 39 | "last 1 Firefox version", 40 | "last 2 iOS versions", 41 | "last 2 Safari versions" 42 | ], 43 | "babel": { 44 | "presets": [ 45 | "@babel/preset-env", 46 | "@babel/preset-react" 47 | ], 48 | "plugins": [ 49 | "@babel/plugin-proposal-class-properties", 50 | [ 51 | "@babel/plugin-proposal-decorators", 52 | { 53 | "legacy": true 54 | } 55 | ], 56 | "@babel/plugin-proposal-object-rest-spread", 57 | [ 58 | "transform-react-remove-prop-types", 59 | { 60 | "mode": "remove", 61 | "removeImport": true, 62 | "ignoreFilenames": [ 63 | "node_modules" 64 | ] 65 | } 66 | ] 67 | ] 68 | }, 69 | "scripts": { 70 | "build": "webpack --config webpack.config.js --mode production", 71 | "start": "webpack --config webpack.config.js --mode development --watch" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | wagtail>=2.6 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from setuptools import setup, find_packages 5 | from wagtail_react_streamfield import __version__ 6 | 7 | 8 | CURRENT_PATH = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | with open(os.path.join(CURRENT_PATH, 'requirements.txt')) as f: 11 | required = f.read().splitlines() 12 | 13 | 14 | setup( 15 | name='wagtail-react-streamfield', 16 | version=__version__, 17 | author='NoriPyt', 18 | author_email='contact@noripyt.com', 19 | url='https://github.com/noripyt/wagtail-react-streamfield', 20 | description='The brand new Wagtail StreamField!', 21 | long_description=open('README.rst').read(), 22 | classifiers=[ 23 | 'Development Status :: 4 - Beta', 24 | 'Programming Language :: Python', 25 | 'Programming Language :: Python :: 3', 26 | 'Programming Language :: Python :: 3.5', 27 | 'Programming Language :: Python :: 3.6', 28 | 'Programming Language :: Python :: 3.7', 29 | 'Framework :: Django', 30 | 'Framework :: Django :: 2.0', 31 | 'Framework :: Django :: 2.1', 32 | 'Framework :: Django :: 2.2', 33 | 'Framework :: Django :: 3.0', 34 | 'Framework :: Wagtail :: 2', 35 | 'Intended Audience :: Developers', 36 | 'License :: OSI Approved :: BSD License', 37 | 'Operating System :: OS Independent', 38 | 'Topic :: Internet :: WWW/HTTP', 39 | ], 40 | license='BSD', 41 | packages=find_packages(), 42 | install_requires=required, 43 | include_package_data=True, 44 | zip_safe=False, 45 | ) 46 | -------------------------------------------------------------------------------- /wagtail-react-streamfield-screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagtail-deprecated/wagtail-react-streamfield/9e7e11bfb7eace1946c5005bd2f3bf5d8f7be260/wagtail-react-streamfield-screenshot-1.png -------------------------------------------------------------------------------- /wagtail-react-streamfield-screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagtail-deprecated/wagtail-react-streamfield/9e7e11bfb7eace1946c5005bd2f3bf5d8f7be260/wagtail-react-streamfield-screenshot-2.png -------------------------------------------------------------------------------- /wagtail-react-streamfield-screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagtail-deprecated/wagtail-react-streamfield/9e7e11bfb7eace1946c5005bd2f3bf5d8f7be260/wagtail-react-streamfield-screenshot-3.png -------------------------------------------------------------------------------- /wagtail_react_streamfield/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (1, 3, 6) 2 | __version__ = '.'.join(map(str, VERSION)) 3 | 4 | default_app_config = 'wagtail_react_streamfield.apps.WagtailReactStreamFieldConfig' 5 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | from .monkey_patch import patch 4 | 5 | 6 | class WagtailReactStreamFieldConfig(AppConfig): 7 | name = 'wagtail_react_streamfield' 8 | 9 | def ready(self): 10 | patch() 11 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/blocks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagtail-deprecated/wagtail-react-streamfield/9e7e11bfb7eace1946c5005bd2f3bf5d8f7be260/wagtail_react_streamfield/blocks/__init__.py -------------------------------------------------------------------------------- /wagtail_react_streamfield/blocks/block.py: -------------------------------------------------------------------------------- 1 | from django.template.loader import render_to_string 2 | from django.utils.functional import cached_property 3 | from django.utils.text import capfirst 4 | from wagtail.core.blocks import Block 5 | 6 | from wagtail_react_streamfield.exceptions import RemovedError 7 | from wagtail_react_streamfield.widgets import get_non_block_errors 8 | 9 | 10 | class NewBlock(Block): 11 | FIELD_NAME_TEMPLATE = 'field-__ID__' 12 | 13 | def get_default(self): 14 | default = self.meta.default 15 | if callable(default): 16 | default = default() 17 | return default 18 | 19 | def get_children_errors(self, errors): 20 | if errors: 21 | if len(errors) > 1: 22 | # We rely on Block.get_children_errors throwing 23 | # a single ValidationError with a specially crafted 'params' 24 | # attribute that we can pull apart and distribute 25 | # to the child blocks. 26 | raise TypeError( 27 | 'Block.get_children_errors unexpectedly ' 28 | 'received multiple errors.' 29 | ) 30 | return errors.as_data()[0].params 31 | 32 | def prepare_value(self, value, errors=None): 33 | """ 34 | Returns the value as it will be displayed in react-streamfield. 35 | """ 36 | return value 37 | 38 | def get_instance_html(self, value, errors=None): 39 | """ 40 | Returns the HTML template generated for a given value. 41 | 42 | That HTML will be displayed as the block content panel 43 | in react-streamfield. It is usually not rendered 44 | """ 45 | help_text = getattr(self.meta, 'help_text', None) 46 | non_block_errors = get_non_block_errors(errors) 47 | if help_text or non_block_errors: 48 | return render_to_string( 49 | 'wagtailadmin/block_forms/blocks_container.html', 50 | { 51 | 'help_text': help_text, 52 | 'non_block_errors': non_block_errors, 53 | } 54 | ) 55 | 56 | @cached_property 57 | def definition(self): 58 | definition = { 59 | 'key': self.name, 60 | 'label': capfirst(self.label), 61 | 'required': self.required, 62 | 'closed': self.meta.closed, 63 | 'dangerouslyRunInnerScripts': True, 64 | } 65 | if self.meta.icon != Block._meta_class.icon: 66 | definition['icon'] = ('' 67 | % self.meta.icon) 68 | classname = getattr(self.meta, 'form_classname', self.meta.classname) 69 | if classname is not None: 70 | definition['className'] = classname 71 | if self.meta.group: 72 | definition['group'] = str(self.meta.group) 73 | if self.meta.default: 74 | definition['default'] = self.prepare_value(self.get_default()) 75 | return definition 76 | 77 | def all_html_declarations(self): 78 | raise RemovedError 79 | 80 | def html_declarations(self): 81 | raise RemovedError 82 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/blocks/field_block.py: -------------------------------------------------------------------------------- 1 | from django.utils.functional import cached_property 2 | from wagtail.core.blocks import ( 3 | FieldBlock, CharBlock, TextBlock, FloatBlock, DecimalBlock, RegexBlock, 4 | URLBlock, DateBlock, TimeBlock, DateTimeBlock, EmailBlock, IntegerBlock, 5 | RichTextBlock, Block, 6 | ) 7 | 8 | 9 | class NewFieldBlock(FieldBlock): 10 | def prepare_value(self, value, errors=None): 11 | from wagtail.admin.rich_text import DraftailRichTextArea 12 | from wagtail.admin.widgets import AdminDateInput, AdminDateTimeInput 13 | 14 | value = self.value_for_form(self.field.prepare_value(value)) 15 | widget = self.field.widget 16 | if isinstance(self, RichTextBlock) \ 17 | and isinstance(widget, DraftailRichTextArea): 18 | value = widget.format_value(value) 19 | if isinstance(widget, (AdminDateInput, AdminDateTimeInput)): 20 | value = widget.format_value(value) 21 | return value 22 | 23 | def get_instance_html(self, value, errors=None): 24 | if errors: 25 | return self.render_form(value, prefix=Block.FIELD_NAME_TEMPLATE, 26 | errors=errors) 27 | 28 | @cached_property 29 | def definition(self): 30 | definition = super(FieldBlock, self).definition 31 | definition['html'] = self.render_form(self.get_default(), 32 | prefix=self.FIELD_NAME_TEMPLATE) 33 | title_template = self.get_title_template() 34 | if title_template is not None: 35 | definition['titleTemplate'] = title_template 36 | return definition 37 | 38 | def get_title_template(self): 39 | if isinstance(self, (CharBlock, TextBlock, FloatBlock, 40 | DecimalBlock, RegexBlock, URLBlock, 41 | DateBlock, TimeBlock, DateTimeBlock, 42 | EmailBlock, IntegerBlock)) and self.name: 43 | return '${%s}' % self.name 44 | 45 | def value_from_datadict(self, data, files, prefix): 46 | return self.value_from_form( 47 | self.field.widget.value_from_datadict( 48 | {'value': data.get('value', self.get_default())}, 49 | files, 'value')) 50 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/blocks/list_block.py: -------------------------------------------------------------------------------- 1 | from django.utils.functional import cached_property 2 | from uuid import uuid4 3 | 4 | from django.core.exceptions import ValidationError 5 | from django.forms.utils import ErrorList 6 | from django.utils.translation import ugettext_lazy as _ 7 | from wagtail.core.blocks import ListBlock, Block 8 | 9 | from ..exceptions import RemovedError 10 | from ..widgets import BlockData 11 | 12 | 13 | class NewListBlock(ListBlock): 14 | def __init__(self, child_block, **kwargs): 15 | Block.__init__(self, **kwargs) 16 | 17 | self.child_block = (child_block() if isinstance(child_block, type) 18 | else child_block) 19 | 20 | if not hasattr(self.meta, 'default'): 21 | self.meta.default = [self.child_block.get_default()] 22 | 23 | self.dependencies = [self.child_block] 24 | 25 | @cached_property 26 | def definition(self): 27 | definition = super(ListBlock, self).definition 28 | definition.update( 29 | children=[self.child_block.definition], 30 | minNum=self.meta.min_num, 31 | maxNum=self.meta.max_num, 32 | ) 33 | html = self.get_instance_html([]) 34 | if html is not None: 35 | definition['html'] = html 36 | return definition 37 | 38 | def render_list_member(self, *args, **kwargs): 39 | raise RemovedError 40 | 41 | def html_declarations(self): 42 | raise RemovedError 43 | 44 | def js_initializer(self): 45 | raise RemovedError 46 | 47 | def render_form(self, *args, **kwargs): 48 | raise RemovedError 49 | 50 | def value_from_datadict(self, data, files, prefix): 51 | return [ 52 | self.child_block.value_from_datadict(child_block_data, files, 53 | prefix) 54 | for child_block_data in data['value']] 55 | 56 | def prepare_value(self, value, errors=None): 57 | children_errors = self.get_children_errors(errors) 58 | if children_errors is None: 59 | children_errors = [None] * len(value) 60 | prepared_value = [] 61 | for child_value, child_errors in zip(value, children_errors): 62 | html = self.child_block.get_instance_html(child_value, 63 | errors=child_errors) 64 | child_value = BlockData({ 65 | 'id': str(uuid4()), 66 | 'type': self.child_block.name, 67 | 'hasError': bool(child_errors), 68 | 'value': self.child_block.prepare_value(child_value, 69 | errors=child_errors), 70 | }) 71 | if html is not None: 72 | child_value['html'] = html 73 | prepared_value.append(child_value) 74 | return prepared_value 75 | 76 | def value_omitted_from_data(self, *args, **kwargs): 77 | raise RemovedError 78 | 79 | def clean(self, value): 80 | result = [] 81 | errors = [] 82 | for child_val in value: 83 | try: 84 | result.append(self.child_block.clean(child_val)) 85 | except ValidationError as e: 86 | errors.append(ErrorList([e])) 87 | else: 88 | errors.append(None) 89 | 90 | if any(errors): 91 | raise ValidationError('Validation error in ListBlock', 92 | params=errors) 93 | 94 | if self.meta.min_num is not None and self.meta.min_num > len(value): 95 | raise ValidationError( 96 | _('The minimum number of items is %d') % self.meta.min_num 97 | ) 98 | elif self.required and len(value) == 0: 99 | raise ValidationError(_('This field is required.')) 100 | 101 | if self.meta.max_num is not None and self.meta.max_num < len(value): 102 | raise ValidationError( 103 | _('The maximum number of items is %d') % self.meta.max_num 104 | ) 105 | 106 | return result 107 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/blocks/static_block.py: -------------------------------------------------------------------------------- 1 | from django.utils.functional import cached_property 2 | from wagtail.core.blocks import StaticBlock, Block 3 | 4 | 5 | class NewStaticBlock(StaticBlock): 6 | @cached_property 7 | def definition(self): 8 | definition = Block.definition.func(self) 9 | definition.update( 10 | isStatic=True, 11 | html=self.render_form(self.get_default(), 12 | prefix=self.FIELD_NAME_TEMPLATE), 13 | ) 14 | return definition 15 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/blocks/stream_block.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | from django.utils.functional import cached_property 4 | from wagtail.core.blocks import BaseStreamBlock, StreamValue 5 | 6 | from ..exceptions import RemovedError 7 | from ..widgets import BlockData 8 | 9 | 10 | class NewBaseStreamBlock(BaseStreamBlock): 11 | @cached_property 12 | def definition(self): 13 | definition = super(BaseStreamBlock, self).definition 14 | definition.update( 15 | children=[ 16 | child_block.definition 17 | for child_block in self.child_blocks.values() 18 | ], 19 | minNum=self.meta.min_num, 20 | maxNum=self.meta.max_num, 21 | ) 22 | html = self.get_instance_html([]) 23 | if html is not None: 24 | definition['html'] = html 25 | return definition 26 | 27 | def sorted_child_blocks(self): 28 | raise RemovedError 29 | 30 | def render_list_member(self, *args, **kwargs): 31 | raise RemovedError 32 | 33 | def html_declarations(self): 34 | raise RemovedError 35 | 36 | def js_initializer(self): 37 | raise RemovedError 38 | 39 | def render_form(self, *args, **kwargs): 40 | raise RemovedError 41 | 42 | def value_from_datadict(self, data, files, prefix): 43 | return StreamValue(self, [ 44 | (child_block_data['type'], 45 | self.child_blocks[child_block_data['type']].value_from_datadict( 46 | child_block_data, files, prefix, 47 | ), 48 | child_block_data.get('id', str(uuid4()))) 49 | for child_block_data in data['value'] 50 | if child_block_data['type'] in self.child_blocks 51 | ]) 52 | 53 | def prepare_value(self, value, errors=None): 54 | if value is None: 55 | return [] 56 | children_errors = self.get_children_errors(errors) 57 | if children_errors is None: 58 | children_errors = {} 59 | prepared_value = [] 60 | for i, stream_child in enumerate(value): 61 | child_errors = children_errors.get(i) 62 | child_block = stream_child.block 63 | child_value = stream_child.value 64 | html = child_block.get_instance_html(child_value, 65 | errors=child_errors) 66 | child_value = BlockData({ 67 | 'id': stream_child.id or str(uuid4()), 68 | 'type': child_block.name, 69 | 'hasError': bool(child_errors), 70 | 'value': child_block.prepare_value(child_value, 71 | errors=child_errors), 72 | }) 73 | if html is not None: 74 | child_value['html'] = html 75 | prepared_value.append(child_value) 76 | return prepared_value 77 | 78 | def value_omitted_from_data(self, data, files, prefix): 79 | return data.get('value') is None 80 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/blocks/struct_block.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | from django.utils.functional import cached_property 4 | from wagtail.core.blocks import BaseStructBlock, Block 5 | 6 | from ..exceptions import RemovedError 7 | from ..widgets import BlockData 8 | 9 | 10 | class NewBaseStructBlock(BaseStructBlock): 11 | def __init__(self, local_blocks=None, **kwargs): 12 | self._constructor_kwargs = kwargs 13 | 14 | Block.__init__(self, **kwargs) 15 | 16 | self.child_blocks = self.base_blocks.copy() 17 | if local_blocks: 18 | for name, block in local_blocks: 19 | block.set_name(name) 20 | self.child_blocks[name] = block 21 | 22 | self.dependencies = self.child_blocks.values() 23 | 24 | @cached_property 25 | def definition(self): 26 | definition = super(BaseStructBlock, self).definition 27 | definition.update( 28 | isStruct=True, 29 | children=[child_block.definition 30 | for child_block in self.child_blocks.values()], 31 | ) 32 | html = self.get_instance_html({}) 33 | if html is not None: 34 | definition['html'] = html 35 | for child_definition in definition['children']: 36 | if 'titleTemplate' in child_definition: 37 | definition['titleTemplate'] = child_definition['titleTemplate'] 38 | break 39 | return definition 40 | 41 | def js_initializer(self): 42 | raise RemovedError 43 | 44 | def get_form_context(self, *args, **kwargs): 45 | raise RemovedError 46 | 47 | def render_form(self, *args, **kwargs): 48 | raise RemovedError 49 | 50 | def value_from_datadict(self, data, files, prefix): 51 | return self._to_struct_value([ 52 | (child_block_data['type'], 53 | self.child_blocks[child_block_data['type']].value_from_datadict( 54 | child_block_data, files, prefix, 55 | )) 56 | for child_block_data in data['value'] 57 | if child_block_data['type'] in self.child_blocks 58 | ]) 59 | 60 | def prepare_value(self, value, errors=None): 61 | children_errors = self.get_children_errors(errors) 62 | if children_errors is None: 63 | children_errors = {} 64 | prepared_value = [] 65 | for k, child_block in self.child_blocks.items(): 66 | child_errors = (None if children_errors is None 67 | else children_errors.get(k)) 68 | child_value = value.get(k, child_block.get_default()) 69 | html = child_block.get_instance_html(child_value, 70 | errors=child_errors) 71 | child_value = BlockData({ 72 | 'id': str(uuid4()), 73 | 'type': k, 74 | 'hasError': bool(child_errors), 75 | 'value': child_block.prepare_value(child_value, 76 | errors=child_errors), 77 | }) 78 | if html is not None: 79 | child_value['html'] = html 80 | prepared_value.append(child_value) 81 | return prepared_value 82 | 83 | def value_omitted_from_data(self, *args, **kwargs): 84 | raise RemovedError 85 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/edit_handlers.py: -------------------------------------------------------------------------------- 1 | from wagtail.admin.edit_handlers import StreamFieldPanel 2 | 3 | 4 | class NewStreamFieldPanel(StreamFieldPanel): 5 | def html_declarations(self): 6 | return '' 7 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/exceptions.py: -------------------------------------------------------------------------------- 1 | class RemovedError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/monkey_patch.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from wagtail.core.blocks import ( 4 | BlockField, Block, BaseStreamBlock, ListBlock, BaseStructBlock, FieldBlock, 5 | StaticBlock) 6 | 7 | from wagtail_react_streamfield.blocks.static_block import NewStaticBlock 8 | from .blocks.block import NewBlock 9 | from .blocks.field_block import NewFieldBlock 10 | from .blocks.list_block import NewListBlock 11 | from .blocks.stream_block import NewBaseStreamBlock 12 | from .blocks.struct_block import NewBaseStructBlock 13 | from .widgets import NewBlockWidget 14 | 15 | 16 | def _patch_with(original_class, new_class, *method_names): 17 | def patch_original(original_method, new_method): 18 | @wraps(original_method) 19 | def inner(*args, **kwargs): 20 | return new_method(*args, **kwargs) 21 | 22 | return inner 23 | 24 | for method_name in method_names: 25 | original_method = getattr(original_class, method_name, None) 26 | new_method = getattr(new_class, method_name) 27 | 28 | if original_method is not None and callable(original_method): 29 | new_method = patch_original(original_method, new_method) 30 | 31 | setattr(original_class, method_name, new_method) 32 | 33 | 34 | def _patch_streamfield_panel(): 35 | from wagtail.admin.edit_handlers import StreamFieldPanel 36 | from .edit_handlers import NewStreamFieldPanel 37 | 38 | _patch_with(StreamFieldPanel, NewStreamFieldPanel, 'html_declarations') 39 | 40 | 41 | def _patch_block_widget(): 42 | def patch_init(original): 43 | @wraps(original) 44 | def inner(self, block=None, **kwargs): 45 | if 'widget' not in kwargs: 46 | kwargs['widget'] = NewBlockWidget(block) 47 | original(self, block=block, **kwargs) 48 | return inner 49 | 50 | BlockField.__init__ = patch_init(BlockField.__init__) 51 | 52 | 53 | def _patch_list_block(): 54 | _patch_with(ListBlock, NewListBlock, 55 | '__init__', 'definition', 'render_list_member', 56 | 'html_declarations', 'js_initializer', 'render_form', 57 | 'value_from_datadict', 'prepare_value', 58 | 'value_omitted_from_data', 'clean') 59 | ListBlock._meta_class.min_num = None 60 | ListBlock._meta_class.max_num = None 61 | 62 | 63 | def patch(): 64 | _patch_streamfield_panel() 65 | _patch_block_widget() 66 | _patch_with(Block, NewBlock, 67 | 'FIELD_NAME_TEMPLATE', 'get_default', 'get_children_errors', 68 | 'prepare_value', 'get_instance_html', 'definition', 69 | 'html_declarations', 'all_html_declarations') 70 | Block._meta_class.closed = False 71 | _patch_with(BaseStreamBlock, NewBaseStreamBlock, 72 | 'definition', 'sorted_child_blocks', 'render_list_member', 73 | 'html_declarations', 'js_initializer', 'prepare_value', 74 | 'render_form', 'value_from_datadict', 'value_omitted_from_data') 75 | _patch_list_block() 76 | _patch_with(BaseStructBlock, NewBaseStructBlock, 77 | '__init__', 'definition', 'js_initializer', 78 | 'get_form_context', 'render_form', 'value_from_datadict', 79 | 'prepare_value', 'value_omitted_from_data') 80 | _patch_with(FieldBlock, NewFieldBlock, 81 | 'prepare_value', 'definition', 'get_instance_html', 82 | 'get_title_template', 'value_from_datadict') 83 | _patch_with(StaticBlock, NewStaticBlock, 'definition') 84 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/static/table_block/js/table.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function initTable(id, tableOptions) { 4 | var containerId = id + '-handsontable-container'; 5 | var tableHeaderCheckboxId = id + '-handsontable-header'; 6 | var colHeaderCheckboxId = id + '-handsontable-col-header'; 7 | var hiddenStreamInput = $('#' + id); 8 | var tableHeaderCheckbox = $('#' + tableHeaderCheckboxId); 9 | var colHeaderCheckbox = $('#' + colHeaderCheckboxId); 10 | var hot; 11 | var defaultOptions; 12 | var finalOptions = {}; 13 | var getCellsClassnames; 14 | var persist; 15 | var cellEvent; 16 | var metaEvent; 17 | var initEvent; 18 | var structureEvent; 19 | var isInitialized = false; 20 | var dataForForm = null; 21 | var getHeight = function() { 22 | var tableParent = $('#' + id).parent(); 23 | return tableParent.find('.htCore').height() + (tableParent.find('.input').height() * 2); 24 | }; 25 | var resizeTargets = ['.input > .handsontable', '.wtHider', '.wtHolder']; 26 | var resizeHeight = function(height) { 27 | var currTable = $('#' + id); 28 | $.each(resizeTargets, function() { 29 | currTable.closest('.field-content').find(this).height(height); 30 | }); 31 | }; 32 | 33 | try { 34 | dataForForm = JSON.parse(hiddenStreamInput.val()); 35 | } catch (e) { 36 | // do nothing 37 | } 38 | 39 | if (dataForForm !== null) { 40 | if (dataForForm.hasOwnProperty('first_row_is_table_header')) { 41 | tableHeaderCheckbox.prop('checked', dataForForm.first_row_is_table_header); 42 | } 43 | if (dataForForm.hasOwnProperty('first_col_is_header')) { 44 | colHeaderCheckbox.prop('checked', dataForForm.first_col_is_header); 45 | } 46 | } 47 | 48 | if (!tableOptions.hasOwnProperty('width') || !tableOptions.hasOwnProperty('height')) { 49 | $(window).on('resize', function() { 50 | hot.updateSettings({ 51 | width: '100%', 52 | height: getHeight() 53 | }); 54 | }); 55 | } 56 | 57 | getCellsClassnames = function() { 58 | var meta = hot.getCellsMeta(); 59 | var cellsClassnames = [] 60 | for (var i = 0; i < meta.length; i++) { 61 | if (meta[i].hasOwnProperty('className')) { 62 | cellsClassnames.push({ 63 | row: meta[i].row, 64 | col: meta[i].col, 65 | className: meta[i].className 66 | }); 67 | } 68 | } 69 | return cellsClassnames; 70 | }; 71 | 72 | persist = function() { 73 | hiddenStreamInput.val(JSON.stringify({ 74 | data: hot.getData(), 75 | cell: getCellsClassnames(), 76 | first_row_is_table_header: tableHeaderCheckbox.prop('checked'), 77 | first_col_is_header: colHeaderCheckbox.prop('checked') 78 | })); 79 | }; 80 | 81 | cellEvent = function(change, source) { 82 | if (source === 'loadData') { 83 | return; //don't save this change 84 | } 85 | 86 | persist(); 87 | }; 88 | 89 | metaEvent = function(row, column, key, value) { 90 | if (isInitialized && key === 'className') { 91 | persist(); 92 | } 93 | }; 94 | 95 | initEvent = function() { 96 | isInitialized = true; 97 | }; 98 | 99 | structureEvent = function(index, amount) { 100 | resizeHeight(getHeight()); 101 | persist(); 102 | }; 103 | 104 | tableHeaderCheckbox.on('change', function() { 105 | persist(); 106 | }); 107 | 108 | colHeaderCheckbox.on('change', function() { 109 | persist(); 110 | }); 111 | 112 | defaultOptions = { 113 | afterChange: cellEvent, 114 | afterCreateCol: structureEvent, 115 | afterCreateRow: structureEvent, 116 | afterRemoveCol: structureEvent, 117 | afterRemoveRow: structureEvent, 118 | afterSetCellMeta: metaEvent, 119 | afterInit: initEvent, 120 | // contextMenu set via init, from server defaults 121 | }; 122 | 123 | if (dataForForm !== null) { 124 | // Overrides default value from tableOptions (if given) with value from database 125 | if (dataForForm.hasOwnProperty('data')) { 126 | defaultOptions.data = dataForForm.data; 127 | } 128 | if (dataForForm.hasOwnProperty('cell')) { 129 | defaultOptions.cell = dataForForm.cell; 130 | } 131 | } 132 | 133 | Object.keys(defaultOptions).forEach(function (key) { 134 | finalOptions[key] = defaultOptions[key]; 135 | }); 136 | Object.keys(tableOptions).forEach(function (key) { 137 | finalOptions[key] = tableOptions[key]; 138 | }); 139 | 140 | hot = new Handsontable(document.getElementById(containerId), finalOptions); 141 | hot.render(); // Call to render removes 'null' literals from empty cells 142 | 143 | // Apply resize after document is finished loading 144 | if ('resize' in $(window)) { 145 | resizeHeight(getHeight()); 146 | $(window).on('load', function() { 147 | $(window).trigger('resize'); 148 | }); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/static/wagtailadmin/fonts/wagtail.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagtail-deprecated/wagtail-react-streamfield/9e7e11bfb7eace1946c5005bd2f3bf5d8f7be260/wagtail_react_streamfield/static/wagtailadmin/fonts/wagtail.woff -------------------------------------------------------------------------------- /wagtail_react_streamfield/static/wagtailadmin/js/hallo-bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function makeHalloRichTextEditable(id, plugins) { 4 | var input = $('#' + id); 5 | var editor = $('
').html(input.val()); 6 | editor.insertBefore(input); 7 | input.hide(); 8 | 9 | var removeStylingPending = false; 10 | function removeStyling() { 11 | /* Strip the 'style' attribute from spans that have no other attributes. 12 | (we don't remove the span entirely as that messes with the cursor position, 13 | and spans will be removed anyway by our whitelisting) 14 | */ 15 | $('span[style]', editor).filter(function() { 16 | return this.attributes.length === 1; 17 | }).removeAttr('style'); 18 | removeStylingPending = false; 19 | } 20 | 21 | /* Workaround for faulty change-detection in hallo */ 22 | function setModified() { 23 | var hallo = editor.data('IKS-hallo'); 24 | if (hallo) { 25 | hallo.setModified(); 26 | } 27 | } 28 | 29 | var closestObj = input.closest('.object'); 30 | 31 | editor.hallo({ 32 | toolbar: 'halloToolbarFixed', 33 | toolbarCssClass: (closestObj.hasClass('full')) ? 'full' : '', 34 | /* use the passed-in plugins arg */ 35 | plugins: plugins 36 | }).on('hallomodified', function(event, data) { 37 | input.val(data.content); 38 | input.attr('value', data.content); 39 | if (!removeStylingPending) { 40 | setTimeout(removeStyling, 100); 41 | removeStylingPending = true; 42 | } 43 | }).on('paste drop', function(event, data) { 44 | setTimeout(function() { 45 | removeStyling(); 46 | setModified(); 47 | }, 1); 48 | /* Animate the fields open when you click into them. */ 49 | }).on('halloactivated', function(event, data) { 50 | $(event.target).addClass('expanded', 200, function(e) { 51 | /* Hallo's toolbar will reposition itself on the scroll event. 52 | This is useful since animating the fields can cause it to be 53 | positioned badly initially. */ 54 | $(window).trigger('scroll'); 55 | }); 56 | }).on('hallodeactivated', function(event, data) { 57 | $(event.target).removeClass('expanded', 200, function(e) { 58 | $(window).trigger('scroll'); 59 | }); 60 | }); 61 | 62 | setupLinkTooltips(editor); 63 | } 64 | 65 | function setupLinkTooltips(elem) { 66 | elem.tooltip({ 67 | animation: false, 68 | title: function() { 69 | return $(this).attr('href'); 70 | }, 71 | trigger: 'hover', 72 | placement: 'bottom', 73 | selector: 'a' 74 | }); 75 | } 76 | 77 | function insertRichTextDeleteControl(elem) { 78 | var a = $('Delete'); 79 | $(elem).addClass('halloembed').prepend(a); 80 | a.on('click', function() { 81 | var widget = $(elem).parent('[data-hallo-editor]').data('IKS-hallo'); 82 | $(elem).fadeOut(function() { 83 | $(elem).remove(); 84 | if (widget != undefined && widget.options.editable) { 85 | widget.element.trigger('change'); 86 | } 87 | }); 88 | }); 89 | } 90 | 91 | $(function() { 92 | $('[data-hallo-editor] [contenteditable="false"]').each(function() { 93 | insertRichTextDeleteControl(this); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/static/wagtailadmin/js/page-chooser.js: -------------------------------------------------------------------------------- 1 | function createPageChooser(id, pageTypes, openAtParentId, canChooseRoot, userPerms) { 2 | var chooserElement = $('#' + id + '-chooser'); 3 | var pageTitle = chooserElement.find('.title'); 4 | var input = $('#' + id); 5 | var editLink = chooserElement.find('.edit-link'); 6 | 7 | function pageChosen(pageData, initial) { 8 | if (!initial) { 9 | input.val(pageData.id); 10 | } 11 | openAtParentId = pageData.parentId; 12 | pageTitle.text(pageData.title); 13 | chooserElement.removeClass('blank'); 14 | editLink.attr('href', pageData.editUrl); 15 | } 16 | 17 | $('.action-choose', chooserElement).on('click', function() { 18 | var initialUrl = window.chooserUrls.pageChooser; 19 | if (openAtParentId) { 20 | initialUrl += openAtParentId + '/'; 21 | } 22 | var urlParams = {page_type: pageTypes.join(',')}; 23 | if (canChooseRoot) { 24 | urlParams.can_choose_root = 'true'; 25 | } 26 | if (userPerms) { 27 | urlParams.user_perms = userPerms; 28 | } 29 | 30 | ModalWorkflow({ 31 | url: initialUrl, 32 | urlParams: urlParams, 33 | onload: PAGE_CHOOSER_MODAL_ONLOAD_HANDLERS, 34 | responses: { 35 | pageChosen: pageChosen, 36 | } 37 | }); 38 | }); 39 | 40 | $('.action-clear', chooserElement).on('click', function() { 41 | input.val(''); 42 | openAtParentId = null; 43 | chooserElement.addClass('blank'); 44 | }); 45 | 46 | if (input.val()) { 47 | $.ajax({ 48 | url: window.wagtailConfig.ADMIN_API.PAGES + encodeURIComponent(input.val()) + '/', 49 | }).done(function (data) { 50 | pageChosen({ 51 | id: data.id, 52 | title: data.admin_display_title, 53 | parentId: (data.meta.parent && data.meta.parent.id) ? data.meta.parent.id : null, 54 | editUrl: window.wagtailConfig.ADMIN_URLS.PAGES 55 | + data.id + '/edit/'}, true); 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/static/wagtaildocs/js/document-chooser.js: -------------------------------------------------------------------------------- 1 | function createDocumentChooser(id) { 2 | var chooserElement = $('#' + id + '-chooser'); 3 | var docTitle = chooserElement.find('.title'); 4 | var input = $('#' + id); 5 | var editLink = chooserElement.find('.edit-link'); 6 | 7 | function documentChosen(docData, initial) { 8 | if (!initial) { 9 | input.val(docData.id); 10 | } 11 | docTitle.text(docData.title); 12 | chooserElement.removeClass('blank'); 13 | editLink.attr('href', docData.edit_link); 14 | } 15 | 16 | $('.action-choose', chooserElement).on('click', function() { 17 | ModalWorkflow({ 18 | url: window.chooserUrls.documentChooser, 19 | onload: DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS, 20 | responses: { 21 | documentChosen: documentChosen, 22 | } 23 | }); 24 | }); 25 | 26 | $('.action-clear', chooserElement).on('click', function() { 27 | input.val(''); 28 | chooserElement.addClass('blank'); 29 | }); 30 | 31 | if (input.val()) { 32 | $.ajax(window.chooserUrls.documentChooser + encodeURIComponent(input.val()) + '/') 33 | .done(function (data) { 34 | documentChosen(data.result, true); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/static/wagtailimages/js/image-chooser.js: -------------------------------------------------------------------------------- 1 | function createImageChooser(id) { 2 | var chooserElement = $('#' + id + '-chooser'); 3 | var previewImage = chooserElement.find('.preview-image img'); 4 | var input = $('#' + id); 5 | var editLink = chooserElement.find('.edit-link'); 6 | 7 | function imageChosen(imageData, initial) { 8 | if (!initial) { 9 | input.val(imageData.id); 10 | } 11 | previewImage.attr({ 12 | src: imageData.preview.url, 13 | width: imageData.preview.width, 14 | height: imageData.preview.height, 15 | alt: imageData.title 16 | }); 17 | chooserElement.removeClass('blank'); 18 | editLink.attr('href', imageData.edit_link); 19 | } 20 | 21 | $('.action-choose', chooserElement).on('click', function() { 22 | ModalWorkflow({ 23 | url: window.chooserUrls.imageChooser, 24 | onload: IMAGE_CHOOSER_MODAL_ONLOAD_HANDLERS, 25 | responses: { 26 | imageChosen: imageChosen, 27 | } 28 | }); 29 | }); 30 | 31 | $('.action-clear', chooserElement).on('click', function() { 32 | input.val(''); 33 | chooserElement.addClass('blank'); 34 | }); 35 | 36 | if (input.val()) { 37 | $.ajax(window.chooserUrls.imageChooser + encodeURIComponent(input.val()) + '/') 38 | .done(function (data) { 39 | imageChosen(data.result, true); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/static/wagtailsnippets/js/snippet-chooser.js: -------------------------------------------------------------------------------- 1 | function createSnippetChooser(id, modelString) { 2 | var chooserElement = $('#' + id + '-chooser'); 3 | var docTitle = chooserElement.find('.title'); 4 | var input = $('#' + id); 5 | var editLink = chooserElement.find('.edit-link'); 6 | 7 | function snippetChosen(snippetData, initial) { 8 | if (!initial) { 9 | input.val(snippetData.id); 10 | } 11 | docTitle.text(snippetData.string); 12 | chooserElement.removeClass('blank'); 13 | editLink.attr('href', snippetData.edit_link); 14 | } 15 | 16 | $('.action-choose', chooserElement).on('click', function() { 17 | ModalWorkflow({ 18 | url: window.chooserUrls.snippetChooser + modelString + '/', 19 | onload: SNIPPET_CHOOSER_MODAL_ONLOAD_HANDLERS, 20 | responses: { 21 | snippetChosen: snippetChosen, 22 | } 23 | }); 24 | }); 25 | 26 | $('.action-clear', chooserElement).on('click', function() { 27 | input.val(''); 28 | chooserElement.addClass('blank'); 29 | }); 30 | 31 | if (input.val()) { 32 | $.ajax(window.chooserUrls.snippetChooser + modelString + '/' 33 | + encodeURIComponent(input.val()) + '/') 34 | .done(function (data) { 35 | snippetChosen(data.result, true); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/static_src/js/entry.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, applyMiddleware } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import thunk from 'redux-thunk'; 6 | import { StreamField, streamFieldReducer } from 'react-streamfield'; 7 | 8 | 9 | /** 10 | * Polyfills for Wagtail's admin. 11 | */ 12 | 13 | // IE11. 14 | import 'core-js/shim'; 15 | // IE11. 16 | import 'element-closest'; 17 | // IE11 18 | import 'custom-event-polyfill'; 19 | 20 | 21 | const store = createStore(streamFieldReducer, applyMiddleware(thunk)); 22 | 23 | const init = (name, options, currentScript) => { 24 | // document.currentScript is not available in IE11. Use a fallback instead. 25 | const context = currentScript ? currentScript.parentNode : document.body; 26 | // If the field is not in the current context, look for it in the whole body. 27 | // Fallback for sequence.js jQuery eval-ed scripts running in document.head. 28 | const selector = `[name="${name}"]`; 29 | const field = (context.querySelector(selector) 30 | || document.body.querySelector(selector)); 31 | 32 | const wrapper = document.createElement('div'); 33 | 34 | field.parentNode.appendChild(wrapper); 35 | 36 | ReactDOM.render( 37 | 38 | 42 | , 43 | wrapper 44 | ); 45 | }; 46 | 47 | window.streamField = {init}; 48 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/static_src/scss/entry.scss: -------------------------------------------------------------------------------- 1 | $header-padding-vertical: 6px; 2 | $action-font-size: 18px; 3 | 4 | 5 | @import '../../../node_modules/react-streamfield/src/scss/index'; 6 | 7 | 8 | .icon-duplicate:before { 9 | content: '\e902'; 10 | } 11 | 12 | 13 | .c-sf-container { 14 | // TODO: #CSSoverhaul the bse icon style here - the margin - should be reconsidered when 15 | // re-building the icon component as part of the CSS These styles come from global 16 | // label styles in css/mixins/_general.scss -@jonnyscholes 17 | .c-sf-button__icon { 18 | .icon::before { 19 | margin: unset; 20 | } 21 | } 22 | 23 | // TODO: #CSSoverhaul global label styles need to be removed. These styles come from global 24 | // label styles in css/_grid.scss -@jonnyscholes 25 | .field label { 26 | float: unset; // LEGIT 27 | width: unset; // LEGIT 28 | max-width: unset; // LEGIT 29 | padding: 0; // LEGIT 30 | 31 | &::after { 32 | content: ''; // LEGIT 33 | } 34 | } 35 | 36 | // TODO: #CSSoverhaul This should be fixed as part of Wagtail CSS overhaul. The default 37 | // `.field`/`.field-componet` (or whatever they become) should be full width by default. 38 | // -@jonnyscholes 39 | .field-content { 40 | float: unset; 41 | display: block; 42 | width: unset; 43 | 44 | textarea { 45 | max-width: 100%; 46 | } 47 | } 48 | 49 | // TODO: #CSSoverhaul This should be fixed as part of Wagtail CSS overhaul. Whatever we do with 50 | // `.field`/`.field-content` should take into account help text -@jonnyscholes 51 | .help { 52 | margin: 8px 0; 53 | } 54 | } 55 | 56 | 57 | .object { 58 | &.stream-field { 59 | background-color: #f6f6f6; 60 | padding-left: 20px; 61 | padding-right: 20px; 62 | 63 | &.required .field > label:after { 64 | display: none; 65 | } 66 | 67 | .object-layout_big-part { 68 | max-width: 100%; 69 | } 70 | 71 | fieldset { 72 | padding-bottom: 0; 73 | max-width: unset; 74 | // Workaround to make sure blocks do not overflow horizontally. 75 | min-width: 0; 76 | input[type="radio"] { 77 | margin-bottom: 1.1em; 78 | vertical-align: middle; 79 | &::before { 80 | top: unset; 81 | } 82 | } 83 | } 84 | 85 | .block_field > .field-content { 86 | width: 100%; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/templates/wagtailadmin/block_forms/blocks_container.html: -------------------------------------------------------------------------------- 1 | {% if non_block_errors %} 2 | {% for error in non_block_errors %} 3 |
{{ error }}
4 | {% endfor %} 5 | {% endif %} 6 | {% if help_text %} 7 |
8 | 9 | {{ help_text }} 10 |
11 | {% endif %} 12 | {# Special tag for react-streamfield #} 13 | -------------------------------------------------------------------------------- /wagtail_react_streamfield/widgets.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.core.exceptions import NON_FIELD_ERRORS 4 | from django.core.serializers.json import DjangoJSONEncoder 5 | from django.forms import Media 6 | from django.forms.utils import ErrorList 7 | from django.utils.safestring import mark_safe 8 | from django.utils.translation import ugettext_lazy as _ 9 | from wagtail.core.blocks import BlockWidget 10 | 11 | 12 | class ConfigJSONEncoder(DjangoJSONEncoder): 13 | def default(self, o): 14 | if isinstance(o, BlockData): 15 | return o.data 16 | return super().default(o) 17 | 18 | 19 | class InputJSONEncoder(DjangoJSONEncoder): 20 | def default(self, o): 21 | if isinstance(o, BlockData): 22 | return {'id': o['id'], 23 | 'type': o['type'], 24 | 'value': o['value']} 25 | return super().default(o) 26 | 27 | 28 | def to_json_script(data, encoder=ConfigJSONEncoder): 29 | return json.dumps( 30 | data, separators=(',', ':'), cls=encoder 31 | ).replace('<', '\\u003c') 32 | 33 | 34 | class BlockData: 35 | def __init__(self, data): 36 | self.data = data 37 | 38 | def __getitem__(self, item): 39 | return self.data[item] 40 | 41 | def __setitem__(self, key, value): 42 | self.data[key] = value 43 | 44 | def __repr__(self): 45 | return '' % self.data 46 | 47 | 48 | def get_non_block_errors(errors): 49 | if errors is None: 50 | return () 51 | errors_data = errors.as_data() 52 | if isinstance(errors, ErrorList): 53 | errors_data = errors_data[0].params 54 | if errors_data is None: 55 | return errors 56 | if isinstance(errors_data, dict): 57 | return errors_data.get(NON_FIELD_ERRORS, ()) 58 | return () 59 | 60 | 61 | class NewBlockWidget(BlockWidget): 62 | def get_action_labels(self): 63 | return { 64 | 'add': _('Add'), 65 | 'moveUp': _('Move up'), 66 | 'moveDown': _('Move down'), 67 | 'duplicate': _('Duplicate'), 68 | 'delete': _('Delete'), 69 | } 70 | 71 | def get_actions_icons(self): 72 | return { 73 | 'add': '', 74 | 'moveUp': '', 75 | 'moveDown': '', 76 | 'duplicate': '', 77 | 'delete': '', 78 | 'grip': '', 79 | } 80 | 81 | def get_streamfield_config(self, value, errors=None): 82 | return { 83 | 'required': self.block_def.required, 84 | 'minNum': self.block_def.meta.min_num, 85 | 'maxNum': self.block_def.meta.max_num, 86 | 'icons': self.get_actions_icons(), 87 | 'labels': self.get_action_labels(), 88 | 'blockDefinitions': self.block_def.definition['children'], 89 | 'value': self.block_def.prepare_value(value, errors=errors), 90 | } 91 | 92 | def render_with_errors(self, name, value, attrs=None, errors=None, 93 | renderer=None): 94 | streamfield_config = self.get_streamfield_config(value, errors=errors) 95 | escaped_value = to_json_script(streamfield_config['value'], 96 | encoder=InputJSONEncoder) 97 | non_block_errors = get_non_block_errors(errors) 98 | non_block_errors = ''.join([ 99 | mark_safe('
%s
') % error 100 | for error in non_block_errors]) 101 | return mark_safe(""" 102 | 103 | 104 | %s 105 | """ % (name, escaped_value, 106 | name, to_json_script(streamfield_config), 107 | non_block_errors)) 108 | 109 | @property 110 | def media(self): 111 | return self.block_def.all_media() + Media( 112 | js=['js/wagtail-react-streamfield.js'], 113 | css={'all': [ 114 | 'css/wagtail-react-streamfield.css', 115 | ]}) 116 | 117 | def value_from_datadict(self, data, files, name): 118 | stream_field_data = json.loads(data.get(name)) 119 | return super().value_from_datadict({'value': stream_field_data}, 120 | files, name) 121 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const autoprefixer = require('autoprefixer'); 4 | 5 | 6 | module.exports = (env, argv) => { 7 | const config = { 8 | entry: {'wagtail-react-streamfield': [ 9 | './wagtail_react_streamfield/static_src/js/entry.js', 10 | './wagtail_react_streamfield/static_src/scss/entry.scss', 11 | ]}, 12 | output: { 13 | path: path.resolve('wagtail_react_streamfield/static'), 14 | filename: 'js/[name].js', 15 | publicPath: '/static/' 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.jsx?$/, 21 | loader: 'babel-loader', 22 | exclude: /node_modules/, 23 | }, 24 | { 25 | test: /\.scss$/, 26 | use: [ 27 | MiniCssExtractPlugin.loader, 28 | 'css-loader', 29 | { 30 | loader: 'postcss-loader', 31 | options: { plugins: () => [autoprefixer()] }, 32 | }, 33 | { 34 | loader: 'sass-loader', 35 | options: { sassOptions: { outputStyle: 'compressed' }}, 36 | }, 37 | ] 38 | }, 39 | ], 40 | }, 41 | plugins: [ 42 | new MiniCssExtractPlugin({ 43 | filename: 'css/[name].css', 44 | chunkFilename: 'css/[id].css', 45 | }), 46 | ], 47 | }; 48 | return config; 49 | }; 50 | --------------------------------------------------------------------------------