├── .nvmrc
├── requirements.txt
├── wagtail_react_streamfield
├── blocks
│ ├── __init__.py
│ ├── static_block.py
│ ├── field_block.py
│ ├── stream_block.py
│ ├── block.py
│ ├── struct_block.py
│ └── list_block.py
├── exceptions.py
├── __init__.py
├── static
│ ├── wagtailadmin
│ │ ├── fonts
│ │ │ └── wagtail.woff
│ │ └── js
│ │ │ ├── page-chooser.js
│ │ │ └── hallo-bootstrap.js
│ ├── wagtaildocs
│ │ └── js
│ │ │ └── document-chooser.js
│ ├── wagtailsnippets
│ │ └── js
│ │ │ └── snippet-chooser.js
│ ├── wagtailimages
│ │ └── js
│ │ │ └── image-chooser.js
│ └── table_block
│ │ └── js
│ │ └── table.js
├── edit_handlers.py
├── apps.py
├── templates
│ └── wagtailadmin
│ │ └── block_forms
│ │ └── blocks_container.html
├── static_src
│ ├── js
│ │ └── entry.js
│ └── scss
│ │ └── entry.scss
├── monkey_patch.py
└── widgets.py
├── MANIFEST.in
├── wagtail-react-streamfield-screenshot-1.png
├── wagtail-react-streamfield-screenshot-2.png
├── wagtail-react-streamfield-screenshot-3.png
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── CONTRIBUTING.md
├── SPONSORS.md
├── webpack.config.js
├── LICENSE
├── setup.py
├── package.json
├── README.rst
└── CHANGELOG.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 14
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | wagtail>=2.6
2 |
--------------------------------------------------------------------------------
/wagtail_react_streamfield/blocks/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/wagtail_react_streamfield/exceptions.py:
--------------------------------------------------------------------------------
1 | class RemovedError(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst LICENSE CHANGELOG.md requirements.txt
2 | recursive-include wagtail_react_streamfield *.js *.css *.html *.woff
3 |
--------------------------------------------------------------------------------
/wagtail-react-streamfield-screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wagtail-deprecated/wagtail-react-streamfield/HEAD/wagtail-react-streamfield-screenshot-1.png
--------------------------------------------------------------------------------
/wagtail-react-streamfield-screenshot-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wagtail-deprecated/wagtail-react-streamfield/HEAD/wagtail-react-streamfield-screenshot-2.png
--------------------------------------------------------------------------------
/wagtail-react-streamfield-screenshot-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wagtail-deprecated/wagtail-react-streamfield/HEAD/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/static/wagtailadmin/fonts/wagtail.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wagtail-deprecated/wagtail-react-streamfield/HEAD/wagtail_react_streamfield/static/wagtailadmin/fonts/wagtail.woff
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------