├── .editorconfig ├── .gitignore ├── .nvmrc ├── .storybook ├── addons.js ├── config.js ├── preview-head.html └── webpack.config.js ├── .stylelintrc.yaml ├── .travis.yml ├── README.md ├── SPONSORS.md ├── example ├── example.scss ├── favicon.ico ├── index.story.js └── samples.js ├── package-lock.json ├── package.json ├── public ├── example.scss ├── favicon.ico ├── iframe.html ├── index.html ├── index.story.js ├── main.256d76e5854b17cc3fe0.bundle.js ├── main.256d76e5854b17cc3fe0.bundle.js.map ├── main.f5c97f635de8325f65a9.bundle.js ├── runtime~main.256d76e5854b17cc3fe0.bundle.js ├── runtime~main.256d76e5854b17cc3fe0.bundle.js.map ├── runtime~main.ba735fcc62253c47f409.bundle.js ├── samples.js ├── sb_dll │ ├── storybook_ui-manifest.json │ ├── storybook_ui_dll.LICENCE │ └── storybook_ui_dll.js ├── vendors~main.256d76e5854b17cc3fe0.bundle.js ├── vendors~main.256d76e5854b17cc3fe0.bundle.js.map ├── vendors~main.4fc0f35c44893298bee2.bundle.js └── webpack-stats.html ├── react-streamfield-screenshot.png ├── rollup.config.js └── src ├── AddButton.js ├── Block.js ├── BlockActions.js ├── BlockContent.js ├── BlockHeader.js ├── BlocksContainer.js ├── FieldInput.js ├── RawHtmlFieldInput.js ├── StreamField.js ├── StructChildField.js ├── actions.js ├── index.js ├── processing ├── conversions.js ├── conversions.test.js ├── reducers.js ├── reducers.test.js ├── samples.js ├── utils.js └── utils.test.js ├── reducer.js ├── scss ├── _variables.scss ├── components │ ├── c-sf-add-button.scss │ ├── c-sf-add-panel.scss │ ├── c-sf-block.scss │ ├── c-sf-button.scss │ └── c-sf-container.scss └── index.scss └── types.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [*.{css,js,json,py,yml,rst}] 9 | indent_style = space 10 | 11 | [*.{js,py}] 12 | charset = utf-8 13 | 14 | [*.{css,py,scss}] 15 | indent_size = 4 16 | 17 | [*.{js,json,yml}] 18 | indent_size = 2 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.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 | 19 | ### JetBrains 20 | .idea/ 21 | *.iml 22 | *.ipr 23 | *.iws 24 | coverage/ 25 | 26 | ### vscode 27 | .vscode 28 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-viewport/register"; 2 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from "@storybook/react"; 2 | 3 | configure(() => { 4 | require("../example/example.scss"); 5 | require("../src/scss/index.scss"); 6 | 7 | require("../example/index.story"); 8 | }, module); 9 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const webpack = require("webpack"); 4 | const sass = require("sass"); 5 | const autoprefixer = require("autoprefixer"); 6 | const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); 7 | 8 | const pkg = require("../package.json"); 9 | 10 | module.exports = (baseConfig, env, defaultConfig) => { 11 | const isProduction = env === "PRODUCTION"; 12 | 13 | // See http://webpack.github.io/docs/configuration.html#devtool 14 | defaultConfig.devtool = "source-map"; 15 | 16 | defaultConfig.module.rules.push({ 17 | test: /\.(scss|css)$/, 18 | loaders: [ 19 | "style-loader", 20 | { 21 | loader: "css-loader", 22 | options: { 23 | sourceMap: true, 24 | minimize: true, 25 | }, 26 | }, 27 | { 28 | loader: "postcss-loader", 29 | options: { 30 | sourceMap: true, 31 | plugins: () => [autoprefixer()], 32 | }, 33 | }, 34 | { 35 | loader: "sass-loader", 36 | options: { 37 | sourceMap: true, 38 | implementation: sass, 39 | }, 40 | }, 41 | ], 42 | include: path.resolve(__dirname, "../"), 43 | }); 44 | 45 | defaultConfig.plugins.push( 46 | new webpack.DefinePlugin({ 47 | "process.env.NODE_ENV": JSON.stringify(env), 48 | }), 49 | ); 50 | 51 | defaultConfig.plugins.push( 52 | new BundleAnalyzerPlugin({ 53 | // Can be `server`, `static` or `disabled`. 54 | analyzerMode: "static", 55 | // Path to bundle report file that will be generated in `static` mode. 56 | reportFilename: path.join( 57 | __dirname, 58 | "..", 59 | "public", 60 | "webpack-stats.html", 61 | ), 62 | // Automatically open report in default browser 63 | openAnalyzer: false, 64 | logLevel: isProduction ? "info" : "warn", 65 | }), 66 | ); 67 | 68 | return defaultConfig; 69 | }; 70 | -------------------------------------------------------------------------------- /.stylelintrc.yaml: -------------------------------------------------------------------------------- 1 | ignoreFiles: 2 | - node_modules 3 | - public/**/* 4 | plugins: 5 | - stylelint-scss 6 | # See https://github.com/stylelint/stylelint/blob/master/docs/user-guide/rules.md 7 | rules: 8 | block-closing-brace-newline-after: 9 | - always 10 | - ignoreAtRules: 11 | # Ignore @if … @else in SCSS. 12 | - if 13 | - else 14 | block-no-empty: true 15 | block-opening-brace-space-before: always 16 | color-hex-case: lower 17 | color-hex-length: short 18 | color-named: never 19 | color-no-invalid-hex: true 20 | comment-no-empty: true 21 | declaration-bang-space-after: never 22 | declaration-bang-space-before: always 23 | declaration-block-no-duplicate-properties: true 24 | declaration-block-no-redundant-longhand-properties: true 25 | declaration-block-single-line-max-declarations: 1 26 | declaration-block-trailing-semicolon: always 27 | declaration-colon-space-after: always 28 | declaration-colon-space-before: never 29 | declaration-property-value-blacklist: 30 | - /^border/: [none] 31 | - severity: error 32 | declaration-no-important: true 33 | font-family-no-duplicate-names: true 34 | function-calc-no-unspaced-operator: true 35 | function-comma-space-after: always 36 | function-linear-gradient-no-nonstandard-direction: true 37 | function-parentheses-space-inside: never 38 | function-url-quotes: always 39 | indentation: 40 | - 4 41 | - severity: warning 42 | length-zero-no-unit: true 43 | max-nesting-depth: 3 44 | media-feature-name-no-unknown: true 45 | no-empty-source: true 46 | no-eol-whitespace: true 47 | no-extra-semicolons: true 48 | no-missing-end-of-source-newline: true 49 | number-no-trailing-zeros: true 50 | number-leading-zero: always 51 | property-case: lower 52 | property-no-unknown: true 53 | rule-empty-line-before: 54 | - always 55 | - except: 56 | - after-single-line-comment 57 | - first-nested 58 | scss/at-import-no-partial-leading-underscore: true 59 | scss/at-import-partial-extension-blacklist: 60 | - scss 61 | scss/at-else-empty-line-before: never 62 | selector-no-qualifying-type: 63 | - true 64 | - ignore: 65 | - attribute 66 | - class 67 | selector-list-comma-newline-after: always 68 | selector-max-id: 0 69 | selector-pseudo-element-no-unknown: true 70 | selector-type-no-unknown: true 71 | scss/at-rule-no-unknown: true 72 | scss/media-feature-value-dollar-variable: always 73 | scss/selector-no-redundant-nesting-selector: true 74 | string-no-newline: true 75 | string-quotes: single 76 | unit-no-unknown: true 77 | unit-case: lower 78 | value-no-vendor-prefix: true 79 | property-no-vendor-prefix: true 80 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | version: ~> 1.0 2 | os: linux 3 | dist: bionic 4 | language: node_js 5 | install: 6 | - npm ci 7 | script: 8 | - npm run build 9 | - npx jest --coverage 10 | # List the published package’s content. 11 | - npm pack --loglevel notice 2>&1 >/dev/null | sed -e 's/^npm notice //' && rm *.tgz 12 | notifications: 13 | email: false 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > :warning: **No longer maintained**: Features have been merged into Wagtail see [2.13 Release Notes](https://docs.wagtail.io/en/stable/releases/2.13.html#streamfield-performance-and-functionality-updates). 2 | 3 | # React StreamField [![npm](https://img.shields.io/npm/v/react-streamfield.svg)](https://www.npmjs.com/package/react-streamfield) [![Build Status](https://travis-ci.org/wagtail/react-streamfield.svg?branch=master)](https://travis-ci.org/wagtail/react-streamfield) 4 | 5 | Powerful field for inserting multiple blocks with nesting. 6 | 7 | Originally created for the [Wagtail CMS](https://wagtail.io/) 8 | thanks to [a Kickstarter campaign](https://kickstarter.com/projects/noripyt/wagtails-first-hatch). 9 | 10 | ![React StreamField screenshot](https://raw.github.com/wagtail/react-streamfield/master/react-streamfield-screenshot.png) 11 | 12 | 13 | ## Demo 14 | 15 | https://wagtail.github.io/react-streamfield/public/ 16 | 17 | 18 | ## Example usage 19 | 20 | To have an idea on how to fully integrate react-streamfield, please check 21 | [this CodeSandbox demo](https://codesandbox.io/s/lyz2k28jpm?fontsize=14). 22 | 23 | For more complex examples, see `example/index.story.js` and 24 | [the corresponding demos](https://wagtail.github.io/react-streamfield/public/) 25 | for more complex examples. 26 | 27 | **More documentation will arrive soon!** 28 | 29 | You can also check out 30 | [wagtail-react-streamfield](https://github.com/wagtail/wagtail-react-streamfield) 31 | to see what an integration of this field looks like! 32 | 33 | 34 | ## Internet Explorer 11 support 35 | 36 | These JavaScript features are used in react-streamfield that are not supported 37 | natively in Internet Explorer 11: 38 | 39 | - `Element.closest(…)` 40 | - `Array.find(…)` 41 | - `Object.entries(…)` 42 | - `CustomEvent` 43 | 44 | When using react-streamfield for Internet Explorer 11, you need to include 45 | the polyfills found in the section below, otherwise the package will not work 46 | properly. 47 | 48 | 49 | ## Polyfills 50 | 51 | React-streamfield uses some JavaScript features only available starting 52 | ECMAScript 2015. Some of these features are not handled by browsers such as 53 | Internet Explorer 11. 54 | 55 | To maintain compatibility when using react-streamfield, install and import 56 | these polyfills (a polyfill adds a missing JavaScript browser feature): 57 | 58 | ```json 59 | { 60 | "dependencies": { 61 | "core-js": "^2.6.5", 62 | "element-closest": "^3.0.1", 63 | "custom-event-polyfill": "^1.0.6" 64 | } 65 | } 66 | ``` 67 | 68 | ```javascript 69 | import 'core-js/shim' 70 | import 'element-closest'; 71 | import 'custom-event-polyfill'; 72 | ``` 73 | 74 | 75 | ## Webpack stats 76 | 77 | https://wagtail.github.io/react-streamfield/public/webpack-stats.html 78 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/example.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background: #F6F6F6; 3 | padding: 15px; 4 | } 5 | 6 | .c-sf-block .c-sf-block__content-inner { 7 | &.full { 8 | padding: 0; 9 | input, textarea { 10 | display: block; 11 | min-width: 100%; 12 | max-width: 100%; 13 | height: 100%; 14 | padding: 16px 24px; 15 | border: none; 16 | outline: none; 17 | } 18 | input { 19 | font-size: 30px; 20 | } 21 | } 22 | } 23 | 24 | input[type="text"], input[type="password"], input[type="email"], 25 | input[type="date"], input[type="time"], input[type="number"], select { 26 | padding: 5px 8px; 27 | border: 1px solid lightGrey; 28 | border-radius: 3px; 29 | } 30 | 31 | input[type="color"] { 32 | -webkit-appearance: none; 33 | -moz-appearance: none; 34 | appearance: none; 35 | border: none; 36 | background: none; 37 | } 38 | 39 | input, textarea { 40 | max-width: 100%; 41 | } 42 | -------------------------------------------------------------------------------- /example/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagtail-deprecated/react-streamfield/827362d39197bf07528739b28f59546637d8a1ae/example/favicon.ico -------------------------------------------------------------------------------- /example/index.story.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import React from 'react'; 3 | import { createStore, applyMiddleware } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import thunk from 'redux-thunk'; 6 | 7 | // Polyfills 8 | import 'core-js/shim' 9 | import 'element-closest'; 10 | import 'custom-event-polyfill'; 11 | 12 | import { StreamField, streamFieldReducer } from '../src'; 13 | 14 | import { complexNestedStreamField } from './samples' 15 | 16 | const store = createStore(streamFieldReducer, applyMiddleware(thunk)); 17 | 18 | storiesOf('React StreamField demo', module) 19 | .addDecorator(story => {story()}) 20 | .add('1 block type', () => { 21 | const props = { 22 | required: true, 23 | blockDefinitions: [ 24 | { 25 | key: 'title', 26 | icon: '', 27 | className: 'full title', 28 | titleTemplate: '${title}', 29 | html: '' 30 | } 31 | ], 32 | value: [{ type: 'title', value: 'Wagtail is awesome!' }] 33 | }; 34 | return ; 35 | }) 36 | .add('1 open block type', () => { 37 | const props = { 38 | required: true, 39 | blockDefinitions: [ 40 | { 41 | key: 'title', 42 | icon: '', 43 | className: 'full title', 44 | titleTemplate: '${title}', 45 | closed: false, 46 | html: '' 47 | } 48 | ], 49 | value: [{ type: 'title', value: 'Wagtail is awesome!' }] 50 | }; 51 | return ; 52 | }) 53 | .add('1 static block type', () => { 54 | const props = { 55 | required: true, 56 | blockDefinitions: [ 57 | { 58 | key: 'static', 59 | isStatic: true, 60 | html: 'Some static block' 61 | } 62 | ], 63 | value: [{ type: 'static' }] 64 | }; 65 | return ; 66 | }) 67 | .add('1 block type, default value', () => { 68 | const props = { 69 | required: true, 70 | blockDefinitions: [ 71 | { 72 | key: 'title', 73 | default: 'The default title', 74 | icon: '', 75 | className: 'full title', 76 | titleTemplate: '${title}', 77 | html: '' 78 | } 79 | ], 80 | value: [{ type: 'title', value: 'Wagtail is awesome!' }] 81 | }; 82 | return ; 83 | }) 84 | .add('1 block type, custom per-value HTML', () => { 85 | const props = { 86 | required: true, 87 | blockDefinitions: [ 88 | { 89 | key: 'title', 90 | icon: '', 91 | className: 'full title', 92 | titleTemplate: '${title}', 93 | html: '' 94 | } 95 | ], 96 | value: [ 97 | { 98 | type: 'title', 99 | html: 100 | '
Do you see it?
', 101 | value: 'Custom HTML for this value!', 102 | titleTemplate: '${title}', 103 | }, 104 | { type: 'title', value: 'This time, no custom HTML.' } 105 | ] 106 | }; 107 | return ; 108 | }) 109 | .add('2 block types', () => { 110 | const props = { 111 | required: true, 112 | blockDefinitions: [ 113 | { 114 | key: 'title', 115 | icon: '', 116 | className: 'full title', 117 | titleTemplate: '${title}', 118 | html: '' 119 | }, 120 | { 121 | key: 'text', 122 | icon: '', 123 | className: 'full', 124 | titleTemplate: '${text}', 125 | html: '' 126 | } 127 | ], 128 | value: [ 129 | { type: 'title', value: 'Wagtail is awesome!' }, 130 | { type: 'text', value: 'And it’s always getting better 😃' } 131 | ] 132 | }; 133 | return ; 134 | }) 135 | .add('List block, 1 child block type', () => { 136 | const props = { 137 | required: true, 138 | blockDefinitions: [ 139 | { 140 | key: 'list', 141 | children: [ 142 | { 143 | key: 'bool', 144 | html: '' 145 | } 146 | ] 147 | } 148 | ], 149 | value: [ 150 | { 151 | type: 'list', 152 | value: [{ type: 'bool', value: true }, { type: 'bool', value: false }] 153 | } 154 | ] 155 | }; 156 | 157 | return ; 158 | }) 159 | .add('List block, 1 child block type, default value', () => { 160 | const props = { 161 | required: true, 162 | blockDefinitions: [ 163 | { 164 | key: 'list', 165 | children: [ 166 | { 167 | key: 'bool', 168 | html: '' 169 | } 170 | ], 171 | default: [{ type: 'bool', value: true }] 172 | } 173 | ], 174 | value: [] 175 | }; 176 | return ; 177 | }) 178 | .add('List block, 1 child block type, custom HTML', () => { 179 | const props = { 180 | required: true, 181 | blockDefinitions: [ 182 | { 183 | key: 'list', 184 | children: [ 185 | { 186 | key: 'bool', 187 | html: '' 188 | } 189 | ], 190 | html: 191 | 'As you can see by this text, it’s possible to insert some HTML before or after the contained blocks. You can even have multiple times the same blocks container. Can’t think of a case where that would be useful, but still, it’s possible if you really want it.' 192 | } 193 | ], 194 | value: [] 195 | }; 196 | return ; 197 | }) 198 | .add('List block, 2 children block types with groups', () => { 199 | const props = { 200 | required: true, 201 | blockDefinitions: [ 202 | { 203 | key: 'list', 204 | children: [ 205 | { 206 | key: 'title', 207 | icon: '', 208 | className: 'full title', 209 | group: 'Text', 210 | html: '' 211 | }, 212 | { 213 | key: 'bool', 214 | group: 'Other', 215 | html: '' 216 | }, 217 | { 218 | key: 'second_bool', 219 | group: 'Other', 220 | html: '' 221 | }, 222 | { 223 | key: 'third_bool', 224 | html: '' 225 | } 226 | ], 227 | default: [ 228 | { type: 'title', value: 'Lorem ipsum' }, 229 | { type: 'bool', value: true } 230 | ] 231 | } 232 | ], 233 | value: [ 234 | { 235 | type: 'list', 236 | value: [ 237 | { type: 'title', value: 'NoriPyt rocks!' }, 238 | { type: 'bool', value: false } 239 | ] 240 | } 241 | ] 242 | }; 243 | return ; 244 | }) 245 | .add('Gutter of add buttons', () => { 246 | const props = { 247 | required: true, 248 | gutteredAdd: true, 249 | blockDefinitions: [ 250 | { 251 | key: 'text', 252 | } 253 | ], 254 | value: [] 255 | }; 256 | return 257 | }) 258 | .add('Maximum number of blocks', () => { 259 | const props = { 260 | required: true, 261 | minNum: null, 262 | maxNum: 2, 263 | blockDefinitions: [ 264 | { 265 | key: 'list', 266 | maxNum: 5, 267 | children: [ 268 | { 269 | key: 'bool', 270 | html: '' 271 | } 272 | ] 273 | } 274 | ], 275 | value: [ 276 | { 277 | type: 'list', 278 | value: [{ type: 'bool', value: true }, { type: 'bool', value: false }] 279 | } 280 | ] 281 | }; 282 | return ; 283 | }) 284 | .add('Error in one of the nested blocks', () => { 285 | const props = { 286 | required: true, 287 | blockDefinitions: [ 288 | { 289 | key: 'list', 290 | children: [ 291 | { 292 | key: 'bool', 293 | html: '' 294 | } 295 | ] 296 | } 297 | ], 298 | value: [ 299 | { 300 | type: 'list', 301 | value: [ 302 | { type: 'bool', value: true }, 303 | { type: 'bool', value: false, hasError: true } 304 | ] 305 | } 306 | ] 307 | }; 308 | return ; 309 | }) 310 | .add('Struct block', () => { 311 | const props = { 312 | blockDefinitions: [ 313 | { 314 | key: 'struct', 315 | isStruct: true, 316 | children: [ 317 | { 318 | key: 'some_field' 319 | }, 320 | { 321 | key: 'another_field' 322 | } 323 | ], 324 | label: 'Struct' 325 | } 326 | ], 327 | value: [] 328 | }; 329 | return ; 330 | }) 331 | .add('Struct block with default value', () => { 332 | const props = { 333 | blockDefinitions: [ 334 | { 335 | key: 'struct', 336 | isStruct: true, 337 | children: [ 338 | { 339 | key: 'some_field', 340 | default: 'Lorem' 341 | }, 342 | { 343 | key: 'another_field', 344 | default: 'Ipsum' 345 | } 346 | ], 347 | label: 'Struct' 348 | } 349 | ], 350 | value: [] 351 | }; 352 | 353 | return ; 354 | }) 355 | .add('Struct block with custom HTML', () => { 356 | const props = { 357 | blockDefinitions: [ 358 | { 359 | key: 'struct', 360 | isStruct: true, 361 | children: [ 362 | { 363 | key: 'some_field' 364 | }, 365 | { 366 | key: 'another_field' 367 | } 368 | ], 369 | label: 'Struct', 370 | html: 371 | 'Like for lists, we can add HTML before struct fields and after as well.' 372 | } 373 | ], 374 | value: [] 375 | }; 376 | return ; 377 | }) 378 | .add('Struct block as a struct block field', () => { 379 | const props = { 380 | blockDefinitions: [ 381 | { 382 | key: 'struct', 383 | isStruct: true, 384 | children: [ 385 | { 386 | key: 'some_field' 387 | }, 388 | { 389 | key: 'link', 390 | isStruct: true, 391 | collapsible: false, 392 | children: [ 393 | { 394 | key: 'url', 395 | label: 'URL', 396 | default: 'https://noripyt.com' 397 | }, 398 | { 399 | key: 'email', 400 | label: 'E-mail' 401 | } 402 | ] 403 | }, 404 | { 405 | key: 'another_field' 406 | } 407 | ], 408 | label: 'Struct' 409 | } 410 | ], 411 | value: [], 412 | }; 413 | return ; 414 | }) 415 | .add('Struct block as a struct block field collapsible', () => { 416 | const props = { 417 | blockDefinitions: [ 418 | { 419 | key: 'struct', 420 | isStruct: true, 421 | children: [ 422 | { 423 | key: 'some_field' 424 | }, 425 | { 426 | key: 'link', 427 | isStruct: true, 428 | collapsible: true, 429 | closed: true, 430 | titleTemplate: '${label}', 431 | children: [ 432 | { 433 | key: 'label', 434 | label: 'Label', 435 | default: 'label' 436 | }, 437 | { 438 | key: 'email', 439 | label: 'E-mail' 440 | } 441 | ], 442 | }, 443 | { 444 | key: 'another_field' 445 | } 446 | ], 447 | label: 'Struct' 448 | } 449 | ], 450 | value: [ 451 | {type: 'struct', value: [ 452 | {type: 'some_field', value: ''}, 453 | {type: 'link', value: []}, 454 | {type: 'another_field', value: ''}, 455 | ]}, 456 | ], 457 | }; 458 | return ; 459 | }) 460 | .add('StructBlock as a list block child', () => { 461 | const props = { 462 | required: true, 463 | blockDefinitions: [ 464 | { 465 | key: 'list', 466 | children: [ 467 | { 468 | key: 'link', 469 | isStruct: true, 470 | children: [ 471 | { 472 | key: 'url', 473 | label: 'URL' 474 | }, 475 | { 476 | key: 'email', 477 | label: 'E-mail' 478 | } 479 | ] 480 | } 481 | ] 482 | } 483 | ], 484 | value: [] 485 | }; 486 | return ; 487 | }) 488 | .add('Complex nested StreamField', () => { 489 | return ; 490 | }) 491 | .add('Custom action icons', () => { 492 | const props = { 493 | required: true, 494 | icons: { 495 | add: '', 496 | moveUp: '', 497 | moveDown: '', 498 | duplicate: '', 499 | delete: '', 500 | grip: '' 501 | }, 502 | blockDefinitions: [ 503 | { 504 | key: 'title', 505 | className: 'full title', 506 | html: '' 507 | } 508 | ], 509 | value: [{ type: 'title', value: 'Wagtail is awesome!' }] 510 | }; 511 | 512 | return ; 513 | }) 514 | .add('Radio buttons', () => { 515 | const props = { 516 | required: true, 517 | blockDefinitions: [ 518 | { 519 | key: 'date', 520 | dangerouslyRunInnerScripts: true, 521 | html: '', 522 | }, 523 | ], 524 | value: [], 525 | }; 526 | return ; 527 | }) 528 | .add('Checkboxes', () => { 529 | const props = { 530 | required: true, 531 | blockDefinitions: [ 532 | { 533 | key: 'date', 534 | dangerouslyRunInnerScripts: true, 535 | html: '', 536 | }, 537 | ], 538 | value: [], 539 | }; 540 | return ; 541 | }) 542 | .add('JavaScript widget', () => { 543 | const props = { 544 | required: true, 545 | blockDefinitions: [ 546 | { 547 | key: 'date', 548 | dangerouslyRunInnerScripts: true, 549 | html: '' + 550 | '', 551 | }, 552 | ], 553 | value: [], 554 | }; 555 | return ; 556 | }); 557 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-streamfield", 3 | "version": "0.9.6", 4 | "author": "Wagtail (https://wagtail.io)", 5 | "description": "Powerful field for inserting multiple blocks with nesting.", 6 | "keywords": [ 7 | "react", 8 | "react-component", 9 | "field" 10 | ], 11 | "homepage": "https://github.com/wagtail/react-streamfield", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/wagtail/react-streamfield" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/wagtail/react-streamfield/issues" 18 | }, 19 | "license": "BSD-3-Clause", 20 | "files": [ 21 | "dist", 22 | "src/scss" 23 | ], 24 | "main": "dist/react-streamfield.cjs.js", 25 | "module": "dist/react-streamfield.esm.js", 26 | "sideEffects": false, 27 | "peerDependencies": { 28 | "prop-types": "^15.6.0", 29 | "react": "^16.4.0", 30 | "react-dom": "^16.4.0", 31 | "react-redux": "^5.0.0", 32 | "redux": "^4.0.0", 33 | "redux-thunk": "^2.3.0" 34 | }, 35 | "dependencies": { 36 | "classnames": "^2.2.6", 37 | "react-animate-height": "^2.0.7", 38 | "react-beautiful-dnd": "^10.0.2", 39 | "uuid": "^3.3.2" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.1.6", 43 | "@babel/plugin-proposal-class-properties": "^7.1.0", 44 | "@babel/plugin-proposal-decorators": "^7.1.6", 45 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 46 | "@babel/preset-env": "^7.1.6", 47 | "@babel/preset-react": "^7.0.0", 48 | "@storybook/addon-viewport": "^4.1.7", 49 | "@storybook/react": "^4.1.7", 50 | "autoprefixer": "^9.4.5", 51 | "babel-core": "^7.0.0-bridge.0", 52 | "babel-jest": "^23.6.0", 53 | "babel-plugin-transform-react-remove-prop-types": "^0.4.21", 54 | "css-loader": "^1.0.1", 55 | "custom-event-polyfill": "^1.0.6", 56 | "element-closest": "^3.0.1", 57 | "jest": "^23.6.0", 58 | "npx": "^10.2.0", 59 | "postcss-cli": "^6.1.1", 60 | "postcss-loader": "^3.0.0", 61 | "prop-types": "^15.6.2", 62 | "react": "^16.7.0", 63 | "react-dom": "^16.7.0", 64 | "react-redux": "^5.1.1", 65 | "redux": "^4.0.1", 66 | "redux-thunk": "^2.3.0", 67 | "rollup": "^1.1.0", 68 | "rollup-plugin-babel": "^4.3.1", 69 | "sass": "^1.16.1", 70 | "sass-loader": "^7.1.0", 71 | "style-loader": "^0.23.1", 72 | "stylelint": "^8.4.0", 73 | "stylelint-scss": "^2.2.0", 74 | "webpack-bundle-analyzer": "^3.0.3" 75 | }, 76 | "browserslist": [ 77 | "Firefox ESR", 78 | "ie 11", 79 | "last 2 Chrome versions", 80 | "last 2 ChromeAndroid versions", 81 | "last 2 Edge versions", 82 | "last 1 Firefox version", 83 | "last 2 iOS versions", 84 | "last 2 Safari versions" 85 | ], 86 | "babel": { 87 | "presets": [ 88 | [ 89 | "@babel/preset-env", 90 | { 91 | "modules": false 92 | } 93 | ], 94 | "@babel/preset-react" 95 | ], 96 | "plugins": [ 97 | [ 98 | "@babel/plugin-proposal-decorators", 99 | { 100 | "legacy": true 101 | } 102 | ], 103 | [ 104 | "@babel/plugin-proposal-class-properties", 105 | { 106 | "loose": true 107 | } 108 | ], 109 | "@babel/plugin-proposal-object-rest-spread", 110 | [ 111 | "transform-react-remove-prop-types", 112 | { 113 | "mode": "unsafe-wrap", 114 | "ignoreFilenames": [ 115 | "node_modules" 116 | ] 117 | } 118 | ] 119 | ], 120 | "env": { 121 | "test": { 122 | "presets": [ 123 | "@babel/preset-env", 124 | "@babel/preset-react" 125 | ] 126 | } 127 | } 128 | }, 129 | "scripts": { 130 | "build": "rollup -c && build-storybook -c .storybook -s example -o public && sass src/scss/index.scss | npx postcss --use autoprefixer --no-map > dist/react-streamfield.css", 131 | "start": "start-storybook -c .storybook -s example -p 9001", 132 | "test": "jest", 133 | "test:watch": "jest --watch", 134 | "prepublishOnly": "npm run build -s" 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /public/example.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background: #F6F6F6; 3 | padding: 15px; 4 | } 5 | 6 | .c-sf-block .c-sf-block__content-inner { 7 | &.full { 8 | padding: 0; 9 | input, textarea { 10 | display: block; 11 | min-width: 100%; 12 | max-width: 100%; 13 | height: 100%; 14 | padding: 16px 24px; 15 | border: none; 16 | outline: none; 17 | } 18 | input { 19 | font-size: 30px; 20 | } 21 | } 22 | } 23 | 24 | input[type="text"], input[type="password"], input[type="email"], 25 | input[type="date"], input[type="time"], input[type="number"], select { 26 | padding: 5px 8px; 27 | border: 1px solid lightGrey; 28 | border-radius: 3px; 29 | } 30 | 31 | input[type="color"] { 32 | -webkit-appearance: none; 33 | -moz-appearance: none; 34 | appearance: none; 35 | border: none; 36 | background: none; 37 | } 38 | 39 | input, textarea { 40 | max-width: 100%; 41 | } 42 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagtail-deprecated/react-streamfield/827362d39197bf07528739b28f59546637d8a1ae/public/favicon.ico -------------------------------------------------------------------------------- /public/iframe.html: -------------------------------------------------------------------------------- 1 | Storybook

No Preview

Sorry, but you either have no stories or none are selected somehow.

  • Please check the storybook config.
  • Try reloading the page.
66 |     
67 |   
-------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | Storybook
-------------------------------------------------------------------------------- /public/index.story.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import React from 'react'; 3 | import { createStore, applyMiddleware } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import thunk from 'redux-thunk'; 6 | 7 | // Polyfills 8 | import 'core-js/shim' 9 | import 'element-closest'; 10 | import 'custom-event-polyfill'; 11 | 12 | import { StreamField, streamFieldReducer } from '../src'; 13 | 14 | import { complexNestedStreamField } from './samples' 15 | 16 | const store = createStore(streamFieldReducer, applyMiddleware(thunk)); 17 | 18 | storiesOf('React StreamField demo', module) 19 | .addDecorator(story => {story()}) 20 | .add('1 block type', () => { 21 | const props = { 22 | required: true, 23 | blockDefinitions: [ 24 | { 25 | key: 'title', 26 | icon: '', 27 | className: 'full title', 28 | titleTemplate: '${title}', 29 | html: '' 30 | } 31 | ], 32 | value: [{ type: 'title', value: 'Wagtail is awesome!' }] 33 | }; 34 | return ; 35 | }) 36 | .add('1 open block type', () => { 37 | const props = { 38 | required: true, 39 | blockDefinitions: [ 40 | { 41 | key: 'title', 42 | icon: '', 43 | className: 'full title', 44 | titleTemplate: '${title}', 45 | closed: false, 46 | html: '' 47 | } 48 | ], 49 | value: [{ type: 'title', value: 'Wagtail is awesome!' }] 50 | }; 51 | return ; 52 | }) 53 | .add('1 static block type', () => { 54 | const props = { 55 | required: true, 56 | blockDefinitions: [ 57 | { 58 | key: 'static', 59 | isStatic: true, 60 | html: 'Some static block' 61 | } 62 | ], 63 | value: [{ type: 'static' }] 64 | }; 65 | return ; 66 | }) 67 | .add('1 block type, default value', () => { 68 | const props = { 69 | required: true, 70 | blockDefinitions: [ 71 | { 72 | key: 'title', 73 | default: 'The default title', 74 | icon: '', 75 | className: 'full title', 76 | titleTemplate: '${title}', 77 | html: '' 78 | } 79 | ], 80 | value: [{ type: 'title', value: 'Wagtail is awesome!' }] 81 | }; 82 | return ; 83 | }) 84 | .add('1 block type, custom per-value HTML', () => { 85 | const props = { 86 | required: true, 87 | blockDefinitions: [ 88 | { 89 | key: 'title', 90 | icon: '', 91 | className: 'full title', 92 | titleTemplate: '${title}', 93 | html: '' 94 | } 95 | ], 96 | value: [ 97 | { 98 | type: 'title', 99 | html: 100 | '
Do you see it?
', 101 | value: 'Custom HTML for this value!', 102 | titleTemplate: '${title}', 103 | }, 104 | { type: 'title', value: 'This time, no custom HTML.' } 105 | ] 106 | }; 107 | return ; 108 | }) 109 | .add('2 block types', () => { 110 | const props = { 111 | required: true, 112 | blockDefinitions: [ 113 | { 114 | key: 'title', 115 | icon: '', 116 | className: 'full title', 117 | titleTemplate: '${title}', 118 | html: '' 119 | }, 120 | { 121 | key: 'text', 122 | icon: '', 123 | className: 'full', 124 | titleTemplate: '${text}', 125 | html: '' 126 | } 127 | ], 128 | value: [ 129 | { type: 'title', value: 'Wagtail is awesome!' }, 130 | { type: 'text', value: 'And it’s always getting better 😃' } 131 | ] 132 | }; 133 | return ; 134 | }) 135 | .add('List block, 1 child block type', () => { 136 | const props = { 137 | required: true, 138 | blockDefinitions: [ 139 | { 140 | key: 'list', 141 | children: [ 142 | { 143 | key: 'bool', 144 | html: '' 145 | } 146 | ] 147 | } 148 | ], 149 | value: [ 150 | { 151 | type: 'list', 152 | value: [{ type: 'bool', value: true }, { type: 'bool', value: false }] 153 | } 154 | ] 155 | }; 156 | 157 | return ; 158 | }) 159 | .add('List block, 1 child block type, default value', () => { 160 | const props = { 161 | required: true, 162 | blockDefinitions: [ 163 | { 164 | key: 'list', 165 | children: [ 166 | { 167 | key: 'bool', 168 | html: '' 169 | } 170 | ], 171 | default: [{ type: 'bool', value: true }] 172 | } 173 | ], 174 | value: [] 175 | }; 176 | return ; 177 | }) 178 | .add('List block, 1 child block type, custom HTML', () => { 179 | const props = { 180 | required: true, 181 | blockDefinitions: [ 182 | { 183 | key: 'list', 184 | children: [ 185 | { 186 | key: 'bool', 187 | html: '' 188 | } 189 | ], 190 | html: 191 | 'As you can see by this text, it’s possible to insert some HTML before or after the contained blocks. You can even have multiple times the same blocks container. Can’t think of a case where that would be useful, but still, it’s possible if you really want it.' 192 | } 193 | ], 194 | value: [] 195 | }; 196 | return ; 197 | }) 198 | .add('List block, 2 children block types with groups', () => { 199 | const props = { 200 | required: true, 201 | blockDefinitions: [ 202 | { 203 | key: 'list', 204 | children: [ 205 | { 206 | key: 'title', 207 | icon: '', 208 | className: 'full title', 209 | group: 'Text', 210 | html: '' 211 | }, 212 | { 213 | key: 'bool', 214 | group: 'Other', 215 | html: '' 216 | }, 217 | { 218 | key: 'second_bool', 219 | group: 'Other', 220 | html: '' 221 | }, 222 | { 223 | key: 'third_bool', 224 | html: '' 225 | } 226 | ], 227 | default: [ 228 | { type: 'title', value: 'Lorem ipsum' }, 229 | { type: 'bool', value: true } 230 | ] 231 | } 232 | ], 233 | value: [ 234 | { 235 | type: 'list', 236 | value: [ 237 | { type: 'title', value: 'NoriPyt rocks!' }, 238 | { type: 'bool', value: false } 239 | ] 240 | } 241 | ] 242 | }; 243 | return ; 244 | }) 245 | .add('Gutter of add buttons', () => { 246 | const props = { 247 | required: true, 248 | gutteredAdd: true, 249 | blockDefinitions: [ 250 | { 251 | key: 'text', 252 | } 253 | ], 254 | value: [] 255 | }; 256 | return 257 | }) 258 | .add('Maximum number of blocks', () => { 259 | const props = { 260 | required: true, 261 | minNum: null, 262 | maxNum: 2, 263 | blockDefinitions: [ 264 | { 265 | key: 'list', 266 | maxNum: 5, 267 | children: [ 268 | { 269 | key: 'bool', 270 | html: '' 271 | } 272 | ] 273 | } 274 | ], 275 | value: [ 276 | { 277 | type: 'list', 278 | value: [{ type: 'bool', value: true }, { type: 'bool', value: false }] 279 | } 280 | ] 281 | }; 282 | return ; 283 | }) 284 | .add('Error in one of the nested blocks', () => { 285 | const props = { 286 | required: true, 287 | blockDefinitions: [ 288 | { 289 | key: 'list', 290 | children: [ 291 | { 292 | key: 'bool', 293 | html: '' 294 | } 295 | ] 296 | } 297 | ], 298 | value: [ 299 | { 300 | type: 'list', 301 | value: [ 302 | { type: 'bool', value: true }, 303 | { type: 'bool', value: false, hasError: true } 304 | ] 305 | } 306 | ] 307 | }; 308 | return ; 309 | }) 310 | .add('Struct block', () => { 311 | const props = { 312 | blockDefinitions: [ 313 | { 314 | key: 'struct', 315 | isStruct: true, 316 | children: [ 317 | { 318 | key: 'some_field' 319 | }, 320 | { 321 | key: 'another_field' 322 | } 323 | ], 324 | label: 'Struct' 325 | } 326 | ], 327 | value: [] 328 | }; 329 | return ; 330 | }) 331 | .add('Struct block with default value', () => { 332 | const props = { 333 | blockDefinitions: [ 334 | { 335 | key: 'struct', 336 | isStruct: true, 337 | children: [ 338 | { 339 | key: 'some_field', 340 | default: 'Lorem' 341 | }, 342 | { 343 | key: 'another_field', 344 | default: 'Ipsum' 345 | } 346 | ], 347 | label: 'Struct' 348 | } 349 | ], 350 | value: [] 351 | }; 352 | 353 | return ; 354 | }) 355 | .add('Struct block with custom HTML', () => { 356 | const props = { 357 | blockDefinitions: [ 358 | { 359 | key: 'struct', 360 | isStruct: true, 361 | children: [ 362 | { 363 | key: 'some_field' 364 | }, 365 | { 366 | key: 'another_field' 367 | } 368 | ], 369 | label: 'Struct', 370 | html: 371 | 'Like for lists, we can add HTML before struct fields and after as well.' 372 | } 373 | ], 374 | value: [] 375 | }; 376 | return ; 377 | }) 378 | .add('Struct block as a struct block field', () => { 379 | const props = { 380 | blockDefinitions: [ 381 | { 382 | key: 'struct', 383 | isStruct: true, 384 | children: [ 385 | { 386 | key: 'some_field' 387 | }, 388 | { 389 | key: 'link', 390 | isStruct: true, 391 | collapsible: false, 392 | children: [ 393 | { 394 | key: 'url', 395 | label: 'URL', 396 | default: 'https://noripyt.com' 397 | }, 398 | { 399 | key: 'email', 400 | label: 'E-mail' 401 | } 402 | ] 403 | }, 404 | { 405 | key: 'another_field' 406 | } 407 | ], 408 | label: 'Struct' 409 | } 410 | ], 411 | value: [], 412 | }; 413 | return ; 414 | }) 415 | .add('Struct block as a struct block field collapsible', () => { 416 | const props = { 417 | blockDefinitions: [ 418 | { 419 | key: 'struct', 420 | isStruct: true, 421 | children: [ 422 | { 423 | key: 'some_field' 424 | }, 425 | { 426 | key: 'link', 427 | isStruct: true, 428 | collapsible: true, 429 | closed: true, 430 | titleTemplate: '${label}', 431 | children: [ 432 | { 433 | key: 'label', 434 | label: 'Label', 435 | default: 'label' 436 | }, 437 | { 438 | key: 'email', 439 | label: 'E-mail' 440 | } 441 | ], 442 | }, 443 | { 444 | key: 'another_field' 445 | } 446 | ], 447 | label: 'Struct' 448 | } 449 | ], 450 | value: [ 451 | {type: 'struct', value: [ 452 | {type: 'some_field', value: ''}, 453 | {type: 'link', value: []}, 454 | {type: 'another_field', value: ''}, 455 | ]}, 456 | ], 457 | }; 458 | return ; 459 | }) 460 | .add('StructBlock as a list block child', () => { 461 | const props = { 462 | required: true, 463 | blockDefinitions: [ 464 | { 465 | key: 'list', 466 | children: [ 467 | { 468 | key: 'link', 469 | isStruct: true, 470 | children: [ 471 | { 472 | key: 'url', 473 | label: 'URL' 474 | }, 475 | { 476 | key: 'email', 477 | label: 'E-mail' 478 | } 479 | ] 480 | } 481 | ] 482 | } 483 | ], 484 | value: [] 485 | }; 486 | return ; 487 | }) 488 | .add('Complex nested StreamField', () => { 489 | return ; 490 | }) 491 | .add('Custom action icons', () => { 492 | const props = { 493 | required: true, 494 | icons: { 495 | add: '', 496 | moveUp: '', 497 | moveDown: '', 498 | duplicate: '', 499 | delete: '', 500 | grip: '' 501 | }, 502 | blockDefinitions: [ 503 | { 504 | key: 'title', 505 | className: 'full title', 506 | html: '' 507 | } 508 | ], 509 | value: [{ type: 'title', value: 'Wagtail is awesome!' }] 510 | }; 511 | 512 | return ; 513 | }) 514 | .add('Radio buttons', () => { 515 | const props = { 516 | required: true, 517 | blockDefinitions: [ 518 | { 519 | key: 'date', 520 | dangerouslyRunInnerScripts: true, 521 | html: '', 522 | }, 523 | ], 524 | value: [], 525 | }; 526 | return ; 527 | }) 528 | .add('Checkboxes', () => { 529 | const props = { 530 | required: true, 531 | blockDefinitions: [ 532 | { 533 | key: 'date', 534 | dangerouslyRunInnerScripts: true, 535 | html: '', 536 | }, 537 | ], 538 | value: [], 539 | }; 540 | return ; 541 | }) 542 | .add('JavaScript widget', () => { 543 | const props = { 544 | required: true, 545 | blockDefinitions: [ 546 | { 547 | key: 'date', 548 | dangerouslyRunInnerScripts: true, 549 | html: '' + 550 | '', 551 | }, 552 | ], 553 | value: [], 554 | }; 555 | return ; 556 | }); 557 | -------------------------------------------------------------------------------- /public/main.f5c97f635de8325f65a9.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{320:function(n,o,p){p(321),p(402),n.exports=p(517)},402:function(n,o,p){"use strict";p.r(o);p(403)}},[[320,1,2]]]); -------------------------------------------------------------------------------- /public/runtime~main.256d76e5854b17cc3fe0.bundle.js: -------------------------------------------------------------------------------- 1 | !function(modules){function webpackJsonpCallback(data){for(var moduleId,chunkId,chunkIds=data[0],moreModules=data[1],executeModules=data[2],i=0,resolves=[];i { 16 | const {fieldId, parentId, blockId} = props; 17 | const field = state[fieldId]; 18 | let blockDefinitions; 19 | if (parentId) { 20 | blockDefinitions = getNestedBlockDefinition(state, fieldId, 21 | parentId).children; 22 | } else { 23 | blockDefinitions = field.blockDefinitions; 24 | } 25 | 26 | let index = 0; 27 | if (blockId !== undefined) { 28 | // Incremented by 1 to add after the current block. 29 | index = getSiblingsIds(state, fieldId, blockId).indexOf(blockId) + 1; 30 | } 31 | 32 | return { 33 | blockDefinitions, index, 34 | icons: field.icons, 35 | labels: field.labels, 36 | }; 37 | }, (dispatch, props) => { 38 | const {fieldId, parentId} = props; 39 | return bindActionCreators({ 40 | addBlock: (index, blockType) => addBlock(fieldId, parentId, 41 | index, blockType), 42 | }, dispatch); 43 | }) 44 | class AddButton extends React.Component { 45 | static propTypes = { 46 | fieldId: PropTypes.string.isRequired, 47 | parentId: PropTypes.string, 48 | blockId: PropTypes.string, 49 | open: PropTypes.bool, 50 | visible: PropTypes.bool, 51 | }; 52 | 53 | static defaultProps = { 54 | open: false, 55 | visible: true, 56 | }; 57 | 58 | constructor(props) { 59 | super(props); 60 | this.state = {open: props.open}; 61 | } 62 | 63 | get hasChoice() { 64 | return this.props.blockDefinitions.length !== 1; 65 | } 66 | 67 | toggle = event => { 68 | event.preventDefault(); 69 | event.stopPropagation(); 70 | if (this.hasChoice) { 71 | this.setState((state, props) => ({open: !state.open})); 72 | } else { 73 | this.props.addBlock(this.props.index, 74 | this.props.blockDefinitions[0].key); 75 | } 76 | }; 77 | 78 | addHandler = event => { 79 | this.props.addBlock(this.props.index, 80 | event.target.closest('button').value); 81 | this.toggle(event); 82 | }; 83 | 84 | getIcon(blockDefinition) { 85 | const {icon} = blockDefinition; 86 | if (isNA(icon)) { 87 | return null; 88 | } 89 | return ; 92 | } 93 | 94 | get panelHeight() { 95 | return this.state.open && this.props.visible ? 'auto' : 0; 96 | } 97 | 98 | get groupedBlockDefinitions() { 99 | const grouped = {}; 100 | for (const blockDefinition of this.props.blockDefinitions) { 101 | const key = blockDefinition.group || ''; 102 | const others = grouped[key] || []; 103 | others.push(blockDefinition); 104 | grouped[key] = others; 105 | } 106 | return grouped; 107 | } 108 | 109 | render() { 110 | const {visible, icons, labels} = this.props; 111 | const button = ( 112 | 119 | ); 120 | if (this.hasChoice) { 121 | return ( 122 | <> 123 | {button} 124 | 126 | {Object.entries(this.groupedBlockDefinitions).map( 127 | ([group, blockDefinitions]) => ( 128 | 129 | {group ?

{group}

: null} 130 |
131 | {blockDefinitions.map(blockDefinition => 132 | 137 | )} 138 |
139 |
140 | ))} 141 |
142 | 143 | ); 144 | } 145 | return button; 146 | } 147 | } 148 | 149 | 150 | export default AddButton; 151 | -------------------------------------------------------------------------------- /src/Block.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import AnimateHeight from 'react-animate-height'; 5 | import {Draggable} from 'react-beautiful-dnd'; 6 | import {bindActionCreators} from 'redux'; 7 | import {connect} from 'react-redux'; 8 | import { 9 | blockUpdated, 10 | deleteBlock, 11 | showBlock, 12 | } from './actions'; 13 | import { 14 | getDescendantsIds, 15 | getNestedBlockDefinition, 16 | getSiblingsIds, 17 | triggerCustomEvent, 18 | } from './processing/utils'; 19 | import AddButton from './AddButton'; 20 | import BlockContent from './BlockContent'; 21 | import BlockHeader from './BlockHeader'; 22 | 23 | 24 | @connect((state, props) => { 25 | const {fieldId, id} = props; 26 | const fieldData = state[fieldId]; 27 | const blocks = fieldData.blocks; 28 | const block = blocks[id]; 29 | const siblings = getSiblingsIds(state, fieldId, id); 30 | const blockDefinition = getNestedBlockDefinition(state, fieldId, id); 31 | const hasDescendantError = getDescendantsIds(state, fieldId, id, true) 32 | .some(descendantBlockId => blocks[descendantBlockId].hasError); 33 | return { 34 | blockDefinition, 35 | parentId: block.parent, 36 | hasError: hasDescendantError, 37 | closed: block.closed, 38 | hidden: block.hidden, 39 | shouldUpdate: block.shouldUpdate, 40 | index: siblings.indexOf(id), 41 | }; 42 | }, (dispatch, props) => { 43 | const {fieldId, id} = props; 44 | return bindActionCreators({ 45 | blockUpdated: () => blockUpdated(fieldId, id), 46 | showBlock: () => showBlock(fieldId, id), 47 | deleteBlock: () => deleteBlock(fieldId, id), 48 | }, dispatch); 49 | }) 50 | class Block extends React.Component { 51 | static propTypes = { 52 | fieldId: PropTypes.string.isRequired, 53 | id: PropTypes.string.isRequired, 54 | standalone: PropTypes.bool, 55 | collapsible: PropTypes.bool, 56 | sortable: PropTypes.bool, 57 | canAdd: PropTypes.bool, 58 | }; 59 | 60 | 61 | static defaultProps = { 62 | standalone: false, 63 | collapsible: true, 64 | sortable: true, 65 | canAdd: true, 66 | }; 67 | 68 | constructor(props) { 69 | super(props); 70 | this.dragHandleRef = React.createRef(); 71 | this.contentRef = React.createRef(); 72 | } 73 | 74 | shouldComponentUpdate(nextProps, nextState, nextContext) { 75 | return nextProps.shouldUpdate; 76 | } 77 | 78 | componentDidUpdate(prevProps, prevState, snapshot) { 79 | if (!prevProps.shouldUpdate) { 80 | this.props.blockUpdated(); 81 | } 82 | } 83 | 84 | triggerCustomEvent(name, data=null) { 85 | triggerCustomEvent(ReactDOM.findDOMNode(this), name, data); 86 | } 87 | 88 | onDraggableContainerAnimationEnd = () => { 89 | if (this.props.hidden) { 90 | this.triggerCustomEvent('delete'); 91 | this.props.deleteBlock(); 92 | } 93 | }; 94 | 95 | get draggableHeight() { 96 | return this.props.hidden ? 0 : 'auto'; 97 | } 98 | 99 | componentDidMount() { 100 | if (this.props.hidden) { 101 | this.props.showBlock(); 102 | } 103 | } 104 | 105 | wrapSortable(blockContent) { 106 | const { 107 | fieldId, id, parentId, index, hasError, 108 | collapsible, sortable, canAdd, standalone, 109 | } = this.props; 110 | const blockClassName = 111 | `c-sf-block ${hasError ? 'c-sf-block--error' : ''}`; 112 | const addButton = ( 113 | 115 | ); 116 | if (sortable) { 117 | return ( 118 | 120 | {(provided, snapshot) => ( 121 |
123 |
124 | 131 | {blockContent} 132 |
133 | {addButton} 134 |
135 | )} 136 |
137 | ); 138 | } 139 | return ( 140 |
141 |
142 | 148 | {blockContent} 149 |
150 | {standalone ? null : addButton} 151 |
152 | ); 153 | } 154 | 155 | render() { 156 | const {fieldId, id, standalone, collapsible} = this.props; 157 | const blockContent = ( 158 | 160 | ); 161 | if (standalone && !collapsible) { 162 | return ( 163 |
164 |
165 | {blockContent} 166 |
167 |
168 | ); 169 | } 170 | return ( 171 | 173 | {this.wrapSortable(blockContent)} 174 | 175 | ); 176 | } 177 | } 178 | 179 | 180 | export default Block; 181 | -------------------------------------------------------------------------------- /src/BlockActions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import {bindActionCreators} from 'redux'; 5 | import {connect} from 'react-redux'; 6 | import { 7 | getLabel, 8 | getNestedBlockDefinition, 9 | getSiblingsIds, 10 | triggerCustomEvent, triggerKeyboardEvent 11 | } from './processing/utils'; 12 | import {duplicateBlock, hideBlock} from './actions'; 13 | import {refType} from './types'; 14 | 15 | 16 | @connect((state, props) => { 17 | const {fieldId, blockId} = props; 18 | const blockDefinition = getNestedBlockDefinition(state, fieldId, blockId); 19 | const siblings = getSiblingsIds(state, fieldId, blockId); 20 | const field = state[fieldId]; 21 | return { 22 | blockDefinition, 23 | siblings, 24 | icons: field.icons, 25 | labels: field.labels, 26 | index: siblings.indexOf(blockId), 27 | }; 28 | }, (dispatch, props) => { 29 | const {fieldId, blockId} = props; 30 | return bindActionCreators({ 31 | hideBlock: () => hideBlock(fieldId, blockId), 32 | duplicateBlock: () => duplicateBlock(fieldId, blockId), 33 | }, dispatch); 34 | }) 35 | class BlockActions extends React.Component { 36 | static propTypes = { 37 | fieldId: PropTypes.string.isRequired, 38 | blockId: PropTypes.string.isRequired, 39 | sortableBlock: PropTypes.bool, 40 | canDuplicate: PropTypes.bool, 41 | standalone: PropTypes.bool, 42 | dragHandleRef: refType, 43 | }; 44 | 45 | static defaultProps = { 46 | sortableBlock: true, 47 | canDuplicate: true, 48 | standalone: false, 49 | }; 50 | 51 | get isFirst() { 52 | return this.props.index === 0; 53 | } 54 | 55 | get isLast() { 56 | return this.props.index === (this.props.siblings.length - 1); 57 | } 58 | 59 | triggerCustomEvent(name, data=null) { 60 | triggerCustomEvent(ReactDOM.findDOMNode(this), name, data); 61 | } 62 | 63 | sendKeyToDragHandle = key => { 64 | const dragHandle = ReactDOM.findDOMNode(this.props.dragHandleRef.current); 65 | triggerKeyboardEvent(dragHandle, 32); // 32 for spacebar, to drag 66 | return new Promise(resolve => { 67 | setTimeout(() => { 68 | triggerKeyboardEvent(dragHandle, key); 69 | setTimeout(() => { 70 | triggerKeyboardEvent(dragHandle, 32); // Drop at the new position 71 | resolve(); 72 | }, 100); // 100 ms is the duration of a move in react-beautiful-dnd 73 | }, 0); 74 | }); 75 | }; 76 | 77 | moveUpHandler = event => { 78 | event.preventDefault(); 79 | event.stopPropagation(); 80 | this.sendKeyToDragHandle(38) // 38 for up arrow 81 | .then(() => { 82 | this.triggerCustomEvent('move', {index: this.props.index}); 83 | }); 84 | }; 85 | 86 | moveDownHandler = event => { 87 | event.preventDefault(); 88 | event.stopPropagation(); 89 | this.sendKeyToDragHandle(40) // 40 for down arrow 90 | .then(() => { 91 | this.triggerCustomEvent('move', {index: this.props.index}); 92 | }); 93 | }; 94 | 95 | duplicateHandler = event => { 96 | event.preventDefault(); 97 | event.stopPropagation(); 98 | this.props.duplicateBlock(); 99 | this.triggerCustomEvent('duplicate'); 100 | }; 101 | 102 | deleteHandler = event => { 103 | event.preventDefault(); 104 | event.stopPropagation(); 105 | this.props.hideBlock(); 106 | }; 107 | 108 | render() { 109 | const { 110 | blockDefinition, sortableBlock, canDuplicate, standalone, 111 | icons, labels, 112 | } = this.props; 113 | return ( 114 |
115 | 116 | {getLabel(blockDefinition)} 117 | 118 | 119 | {standalone ? 120 | null 121 | : 122 | <> 123 | {sortableBlock ? 124 | <> 125 |
146 | ); 147 | } 148 | } 149 | 150 | 151 | export default BlockActions; 152 | -------------------------------------------------------------------------------- /src/BlockContent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | import {connect} from 'react-redux'; 5 | import AnimateHeight from 'react-animate-height'; 6 | import { 7 | getNestedBlockDefinition, 8 | isStruct, 9 | getDescendantsIds, replaceWithComponent, isNA, 10 | } from './processing/utils'; 11 | import StructChildField from './StructChildField'; 12 | import FieldInput from './FieldInput'; 13 | 14 | 15 | @connect((state, props) => { 16 | const {fieldId, blockId} = props; 17 | const fieldData = state[fieldId]; 18 | const blocks = fieldData.blocks; 19 | const block = blocks[blockId]; 20 | const blockDefinition = getNestedBlockDefinition(state, fieldId, blockId); 21 | const hasDescendantError = getDescendantsIds(state, fieldId, blockId, true) 22 | .some(descendantBlockId => blocks[descendantBlockId].hasError); 23 | return { 24 | blockDefinition, 25 | html: block.html, 26 | closed: block.closed && !hasDescendantError, 27 | }; 28 | }) 29 | class BlockContent extends React.Component { 30 | static propTypes = { 31 | fieldId: PropTypes.string.isRequired, 32 | blockId: PropTypes.string.isRequired, 33 | collapsible: PropTypes.bool, 34 | }; 35 | 36 | static defaultProps = { 37 | collapsible: true, 38 | }; 39 | 40 | get html() { 41 | const {fieldId, blockDefinition, blockId} = this.props; 42 | if (isStruct(blockDefinition)) { 43 | const blocksContainer = blockDefinition.children.map( 44 | childBlockDefinition => 45 | 48 | ); 49 | let html = this.props.html; 50 | if (isNA(html)) { 51 | html = blockDefinition.html; 52 | } 53 | if (isNA(html)) { 54 | return blocksContainer; 55 | } 56 | return replaceWithComponent( 57 | html, '', 58 | blocksContainer); 59 | } 60 | return ; 61 | } 62 | 63 | get height() { 64 | return this.props.closed ? 0 : 'auto'; 65 | } 66 | 67 | render() { 68 | const {blockDefinition, collapsible} = this.props; 69 | const content = this.html; 70 | const className = classNames('c-sf-block__content-inner', blockDefinition.className); 71 | if (collapsible) { 72 | return ( 73 | 76 | {content} 77 | 78 | ); 79 | } 80 | return ( 81 |
82 |
83 | {content} 84 |
85 |
86 | ); 87 | } 88 | } 89 | 90 | 91 | export default BlockContent; 92 | -------------------------------------------------------------------------------- /src/BlockHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import classNames from 'classnames'; 5 | import {bindActionCreators} from 'redux'; 6 | import {connect} from 'react-redux'; 7 | import { 8 | getNestedBlockDefinition, isNA, 9 | isStruct, structValueToObject, triggerCustomEvent 10 | } from './processing/utils'; 11 | import {toggleBlock} from './actions'; 12 | import BlockActions from './BlockActions'; 13 | import {refType} from './types'; 14 | 15 | 16 | @connect((state, props) => { 17 | const {fieldId, blockId} = props; 18 | const fieldData = state[fieldId]; 19 | const blocks = fieldData.blocks; 20 | const block = blocks[blockId]; 21 | const blockDefinition = getNestedBlockDefinition(state, fieldId, blockId); 22 | const value = block.value; 23 | return { 24 | blockDefinition, 25 | icons: fieldData.icons, 26 | value: isStruct(blockDefinition) ? 27 | structValueToObject(state, fieldId, value) 28 | : 29 | value, 30 | }; 31 | }, (dispatch, props) => { 32 | const {fieldId, blockId} = props; 33 | return bindActionCreators({ 34 | toggleBlock: () => toggleBlock(fieldId, blockId), 35 | }, dispatch); 36 | }) 37 | class BlockHeader extends React.Component { 38 | static propTypes = { 39 | fieldId: PropTypes.string.isRequired, 40 | blockId: PropTypes.string.isRequired, 41 | collapsibleBlock: PropTypes.bool, 42 | sortableBlock: PropTypes.bool, 43 | canDuplicate: PropTypes.bool, 44 | standalone: PropTypes.bool, 45 | dragHandleRef: refType, 46 | dragHandleProps: PropTypes.object, 47 | }; 48 | 49 | static defaultProps = { 50 | collapsibleBlock: true, 51 | sortableBlock: true, 52 | canDuplicate: true, 53 | standalone: false, 54 | }; 55 | 56 | get title() { 57 | const {title, blockDefinition, value} = this.props; 58 | if ((title !== undefined) && (title !== null)) { 59 | return title; 60 | } 61 | if (blockDefinition.titleTemplate !== undefined) { 62 | let hasVariables = false; 63 | let isEmpty = true; 64 | let renderedTitle = blockDefinition.titleTemplate.replace( 65 | /\${([^}]+)}/g, (match, varName) => { 66 | if (isStruct(blockDefinition)) { 67 | let childValue = value[varName]; 68 | if (isNA(childValue)) { 69 | childValue = ''; 70 | } else if (childValue !== '') { 71 | isEmpty = false; 72 | } 73 | hasVariables = true; 74 | return childValue || ''; 75 | } else { 76 | if (varName === blockDefinition.key) { 77 | return value || ''; 78 | } 79 | return ''; 80 | } 81 | }); 82 | if (!hasVariables || !isEmpty) { 83 | return renderedTitle; 84 | } 85 | } 86 | return null; 87 | } 88 | 89 | triggerCustomEvent(name, data=null) { 90 | triggerCustomEvent(ReactDOM.findDOMNode(this), name, data); 91 | } 92 | 93 | toggle = () => { 94 | const {toggleBlock, closed} = this.props; 95 | toggleBlock(); 96 | this.triggerCustomEvent('toggle', {closed: !closed}); 97 | }; 98 | 99 | render() { 100 | const { 101 | blockDefinition, fieldId, blockId, dragHandleProps, collapsibleBlock, 102 | sortableBlock, canDuplicate, standalone, dragHandleRef, 103 | } = this.props; 104 | return ( 105 |
110 | 112 |

{this.title || ''}

113 | 118 |
119 | ); 120 | } 121 | } 122 | 123 | 124 | export default BlockHeader; 125 | -------------------------------------------------------------------------------- /src/BlocksContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | import {connect} from 'react-redux'; 5 | import {Droppable} from 'react-beautiful-dnd'; 6 | import Block from './Block'; 7 | import AddButton from './AddButton'; 8 | import {getNestedBlockDefinition, isNA} from './processing/utils'; 9 | 10 | 11 | @connect((state, props) => { 12 | const {fieldId, id} = props; 13 | const fieldData = state[fieldId]; 14 | const blocksIds = id === null ? 15 | fieldData.rootBlocks 16 | : 17 | fieldData.blocks[id].value; 18 | let minNum, maxNum; 19 | if (id === null) { 20 | minNum = fieldData.minNum; 21 | maxNum = fieldData.maxNum; 22 | } else { 23 | const blockDefinition = getNestedBlockDefinition(state, fieldId, id); 24 | minNum = blockDefinition.minNum; 25 | maxNum = blockDefinition.maxNum; 26 | } 27 | if (isNA(minNum)) { 28 | minNum = 0; 29 | } 30 | if (isNA(maxNum)) { 31 | maxNum = Infinity; 32 | } 33 | return { 34 | minNum, maxNum, 35 | gutteredAdd: fieldData.gutteredAdd, 36 | blocksIds: blocksIds, 37 | }; 38 | }) 39 | class BlocksContainer extends React.Component { 40 | static propTypes = { 41 | fieldId: PropTypes.string.isRequired, 42 | id: PropTypes.string, 43 | }; 44 | 45 | renderBlock(blockId, canAdd=true) { 46 | return ( 47 | 50 | ); 51 | } 52 | 53 | render() { 54 | const {fieldId, id, blocksIds, maxNum, gutteredAdd} = this.props; 55 | const droppableId = `${fieldId}-${id}`; 56 | const num = blocksIds.length; 57 | const canAdd = num < maxNum; 58 | return ( 59 | 60 | {(provided, snapshot) => ( 61 |
66 | 68 | {blocksIds.map(blockId => this.renderBlock(blockId, canAdd))} 69 | {provided.placeholder} 70 |
71 | )} 72 |
73 | ); 74 | } 75 | } 76 | 77 | 78 | export default BlocksContainer; 79 | -------------------------------------------------------------------------------- /src/FieldInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {bindActionCreators} from 'redux'; 4 | import {connect} from 'react-redux'; 5 | import { 6 | getFieldName, 7 | getNestedBlockDefinition, 8 | isField, isNA, 9 | isStruct, replaceWithComponent 10 | } from './processing/utils'; 11 | import {changeBlockValue} from './actions'; 12 | import Block from './Block'; 13 | import BlocksContainer from './BlocksContainer'; 14 | import RawHtmlFieldInput from './RawHtmlFieldInput'; 15 | 16 | 17 | @connect((state, props) => { 18 | const {fieldId, blockId} = props; 19 | const block = state[fieldId].blocks[blockId]; 20 | return { 21 | blockDefinition: getNestedBlockDefinition(state, fieldId, blockId), 22 | html: block.html, 23 | value: block.value, 24 | }; 25 | }, (dispatch, props) => { 26 | const {fieldId, blockId} = props; 27 | return bindActionCreators({ 28 | changeBlockValue: value => changeBlockValue(fieldId, blockId, value), 29 | }, dispatch); 30 | }) 31 | class FieldInput extends React.Component { 32 | static propTypes = { 33 | fieldId: PropTypes.string.isRequired, 34 | blockId: PropTypes.string.isRequired, 35 | }; 36 | 37 | render() { 38 | const {fieldId, blockDefinition, blockId, value} = this.props; 39 | if (isStruct(blockDefinition)) { // Nested StructBlock 40 | return ( 41 | 43 | ); 44 | } 45 | let html = this.props.html; 46 | if (isNA(html)) { 47 | html = blockDefinition.html; 48 | } 49 | if (isField(blockDefinition)) { 50 | if (isNA(html)) { 51 | html = ``; 53 | } 54 | return ( 55 | 58 | ); 59 | } 60 | const blocksContainer = ; 61 | if (isNA(html)) { 62 | return blocksContainer; 63 | } 64 | return replaceWithComponent( 65 | html, '', blocksContainer); 66 | } 67 | } 68 | 69 | 70 | export default FieldInput; 71 | -------------------------------------------------------------------------------- /src/RawHtmlFieldInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import { 5 | getFieldName, 6 | isStatic, 7 | shouldRunInnerScripts 8 | } from './processing/utils'; 9 | 10 | 11 | class RawHtmlFieldInput extends React.Component { 12 | static propTypes = { 13 | fieldId: PropTypes.string.isRequired, 14 | blockDefinition: PropTypes.object.isRequired, 15 | blockId: PropTypes.string.isRequired, 16 | html: PropTypes.string.isRequired, 17 | value: PropTypes.any, 18 | changeBlockValue: PropTypes.func.isRequired, 19 | }; 20 | 21 | runInnerScripts() { 22 | if (shouldRunInnerScripts(this.props.blockDefinition)) { 23 | for (let script 24 | of ReactDOM.findDOMNode(this).querySelectorAll('script')) { 25 | script.parentNode.removeChild(script); 26 | window.eval(script.innerHTML); 27 | } 28 | } 29 | } 30 | 31 | setValue(input) { 32 | const {value} = this.props; 33 | if ((value !== undefined) && (value !== null)) { 34 | if (input.type === 'file') { 35 | input.files = value; 36 | } else if ((input.type === 'checkbox') || (input.type === 'radio')) { 37 | input.checked = value === null ? false : ( 38 | typeof value === 'boolean' ? value : value.includes(input.value)); 39 | } else if (input.type === 'hidden') { 40 | input.value = value; 41 | input.dispatchEvent(new Event('change')); 42 | } else { 43 | input.value = value; 44 | } 45 | } 46 | } 47 | 48 | bindChange(input) { 49 | if (input.type === 'hidden') { 50 | const observer = new MutationObserver(() => { 51 | input.dispatchEvent(new Event('change')); 52 | }); 53 | observer.observe(input, { 54 | attributes: true, attributeFilter: ['value'], 55 | }); 56 | this.mutationObservers.push(observer); 57 | } 58 | input.addEventListener('change', this.onChange); 59 | } 60 | 61 | unbindChange(input) { 62 | input.removeEventListener('change', this.onChange); 63 | } 64 | 65 | componentDidMount() { 66 | const {blockDefinition, blockId} = this.props; 67 | if (!isStatic(blockDefinition)) { 68 | const name = getFieldName(blockId); 69 | this.inputs = [ 70 | ...ReactDOM.findDOMNode(this).querySelectorAll(`[name="${name}"]`)]; 71 | if (this.inputs.length === 0) { 72 | throw Error(`Could not find input with name "${name}"`); 73 | } 74 | this.mutationObservers = []; 75 | for (let input of this.inputs) { 76 | this.setValue(input); 77 | this.bindChange(input); 78 | // We remove the name attribute to remove inputs from the submitted form. 79 | input.removeAttribute('name'); 80 | } 81 | } 82 | this.runInnerScripts(); 83 | } 84 | 85 | componentWillUnmount() { 86 | if (!isStatic(this.props.blockDefinition)) { 87 | for (let observer of this.mutationObservers) { 88 | observer.disconnect(); 89 | } 90 | for (let input of this.inputs) { 91 | this.unbindChange(input); 92 | } 93 | } 94 | } 95 | 96 | onChange = event => { 97 | const input = event.target; 98 | let value; 99 | if (input.type === 'file') { 100 | value = input.files; 101 | } else if (input.type === 'checkbox' || input.type === 'radio') { 102 | const boxes = this.inputs; 103 | value = boxes.filter(box => box.checked).map(box => box.value); 104 | const previousValue = this.props.value; 105 | if (input.type === 'radio') { 106 | if (previousValue) { 107 | // Makes it possible to select only one radio button at a time. 108 | boxes.filter(box => box.value === previousValue)[0].checked = false; 109 | const index = value.indexOf(previousValue); 110 | if (index > -1) { 111 | value.splice(index, 1); 112 | } 113 | } 114 | value = value.length > 0 ? value[0] : null; 115 | } 116 | } else if (input.tagName === 'SELECT') { 117 | value = input.options[input.selectedIndex].value; 118 | } else { 119 | value = input.value; 120 | } 121 | this.props.changeBlockValue(value); 122 | }; 123 | 124 | get html() { 125 | const {blockDefinition, html, blockId} = this.props; 126 | if (isStatic(blockDefinition)) { 127 | return html; 128 | } 129 | return html.replace(/__ID__/g, blockId); 130 | } 131 | 132 | render() { 133 | return ( 134 |
135 | ); 136 | } 137 | } 138 | 139 | 140 | export default RawHtmlFieldInput; 141 | -------------------------------------------------------------------------------- /src/StreamField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {bindActionCreators} from 'redux'; 4 | import {connect} from 'react-redux'; 5 | import {DragDropContext} from 'react-beautiful-dnd'; 6 | import { 7 | moveBlock, 8 | initializeStreamField, 9 | } from './actions'; 10 | import {stateToValue} from './processing/conversions'; 11 | import BlocksContainer from './BlocksContainer'; 12 | 13 | 14 | function lazyFunction(f) { 15 | return function () { 16 | return f().apply(this, arguments); 17 | }; 18 | } 19 | 20 | 21 | const BlockDefinitionType = PropTypes.shape({ 22 | key: PropTypes.string.isRequired, 23 | label: PropTypes.string, 24 | required: PropTypes.bool, 25 | default: PropTypes.any, 26 | icon: PropTypes.string, 27 | group: PropTypes.string, 28 | className: PropTypes.string, 29 | minNum: PropTypes.number, 30 | maxNum: PropTypes.number, 31 | closed: PropTypes.bool, 32 | titleTemplate: PropTypes.string, 33 | html: PropTypes.string, 34 | isStruct: PropTypes.bool, 35 | isStatic: PropTypes.bool, 36 | dangerouslyRunInnerScripts: PropTypes.bool, 37 | children: PropTypes.arrayOf(lazyFunction(() => BlockDefinitionType)), 38 | }); 39 | 40 | 41 | const BlockValueType = PropTypes.shape({ 42 | type: PropTypes.string.isRequired, 43 | html: PropTypes.string, 44 | hasError: PropTypes.bool, 45 | value: PropTypes.oneOfType([ 46 | PropTypes.arrayOf(lazyFunction(() => BlockValueType)), 47 | PropTypes.string, 48 | PropTypes.number, 49 | PropTypes.bool, 50 | ]), 51 | }); 52 | 53 | 54 | const StreamFieldDefaultProps = { 55 | required: false, 56 | minNum: 0, 57 | maxNum: Infinity, 58 | icons: { 59 | add: '', 60 | moveUp: '