├── .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 [](https://www.npmjs.com/package/react-streamfield) [](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 | 
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 | StorybookNo Preview
Sorry, but you either have no stories or none are selected somehow.
- Please check the storybook config.
- Try reloading the page.
--------------------------------------------------------------------------------
/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 |
129 |
133 | >
134 | :
135 | null}
136 |
140 |
143 | >
144 | }
145 |
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: '',
61 | moveDown: '',
62 | duplicate: '',
63 | delete: '',
64 | grip: '',
65 | },
66 | labels: {
67 | add: 'Add block',
68 | moveUp: 'Move up',
69 | moveDown: 'Move down',
70 | duplicate: 'Duplicate',
71 | delete: 'Delete',
72 | },
73 | };
74 |
75 |
76 | @connect((state, props) => {
77 | const {id} = props;
78 | const fieldData = state[id];
79 | return {
80 | generatedValue: fieldData === undefined ? '' : stateToValue(state, id),
81 | };
82 | }, (dispatch, props) => {
83 | const {id} = props;
84 | return bindActionCreators({
85 | initializeStreamField: data => initializeStreamField(id, data),
86 | moveBlock: (blockId, newIndex) => moveBlock(id, blockId, newIndex),
87 | }, dispatch);
88 | })
89 | class StreamField extends React.Component {
90 | static propTypes = {
91 | id: PropTypes.string.isRequired,
92 | required: PropTypes.bool,
93 | minNum: PropTypes.number,
94 | maxNum: PropTypes.number,
95 | icons: PropTypes.shape({
96 | add: PropTypes.string,
97 | moveUp: PropTypes.string,
98 | moveDown: PropTypes.string,
99 | duplicate: PropTypes.string,
100 | delete: PropTypes.string,
101 | grip: PropTypes.string,
102 | }),
103 | labels: PropTypes.shape({
104 | add: PropTypes.string,
105 | moveUp: PropTypes.string,
106 | moveDown: PropTypes.string,
107 | duplicate: PropTypes.string,
108 | delete: PropTypes.string,
109 | }),
110 | gutteredAdd: PropTypes.bool,
111 | blockDefinitions: PropTypes.arrayOf(BlockDefinitionType).isRequired,
112 | value: PropTypes.arrayOf(BlockValueType).isRequired,
113 | };
114 |
115 | static defaultProps = StreamFieldDefaultProps;
116 |
117 | onDragEnd = result => {
118 | const {draggableId, source, destination} = result;
119 | if (!destination || (result.reason === 'CANCEL')
120 | || (destination.droppableId !== source.droppableId)
121 | || (destination.index === source.index)) {
122 | return;
123 | }
124 | this.props.moveBlock(draggableId, destination.index);
125 | };
126 |
127 | componentWillMount() {
128 | // Removes the input with the same name if it exists.
129 | const input = document.querySelector(`[name="${this.props.id}"]`);
130 | if (input !== null) {
131 | input.parentNode.removeChild(input);
132 | }
133 | }
134 |
135 | componentDidMount() {
136 | const {
137 | initializeStreamField, required, minNum, maxNum, gutteredAdd,
138 | blockDefinitions, value,
139 | } = this.props;
140 | const defaultProps = StreamFieldDefaultProps;
141 | const icons = {...defaultProps.icons, ...this.props.icons};
142 | const labels = {...defaultProps.labels, ...this.props.labels};
143 | initializeStreamField({
144 | required, minNum, maxNum, icons, labels, gutteredAdd,
145 | blockDefinitions, value,
146 | });
147 | }
148 |
149 | render() {
150 | const {id, generatedValue} = this.props;
151 | return (
152 |
153 | {generatedValue ? : null}
154 |
155 |
157 |
158 | );
159 | }
160 | }
161 |
162 |
163 | export default StreamField;
164 |
--------------------------------------------------------------------------------
/src/StructChildField.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 {
6 | getFieldName, getLabel,
7 | getNestedBlockDefinition,
8 | } from './processing/utils';
9 | import FieldInput from './FieldInput';
10 |
11 |
12 | @connect((state, props) => {
13 | const {fieldId, parentBlockId, type} = props;
14 | const blocks = state[fieldId].blocks;
15 | const parentBlock = blocks[parentBlockId];
16 | const blockId = parentBlock.value.find(
17 | childBlockId => blocks[childBlockId].type === type);
18 | return {
19 | blockDefinition: getNestedBlockDefinition(state, fieldId, blockId),
20 | blockId,
21 | };
22 | })
23 | class StructChildField extends React.Component {
24 | static propTypes = {
25 | fieldId: PropTypes.string.isRequired,
26 | parentBlockId: PropTypes.string.isRequired,
27 | type: PropTypes.string.isRequired,
28 | };
29 |
30 | render() {
31 | const {fieldId, blockId, blockDefinition} = this.props;
32 | return (
33 |
35 |
38 |
39 |
40 | );
41 | }
42 | }
43 |
44 |
45 | export default StructChildField;
46 |
--------------------------------------------------------------------------------
/src/actions.js:
--------------------------------------------------------------------------------
1 | export const initializeStreamField = (id, data) => ({
2 | type: 'INITIALIZE_STREAM_FIELD',
3 | id, data,
4 | });
5 |
6 |
7 | export const setIsMobile = (id, isMobile) => ({
8 | type: 'SET_IS_MOBILE',
9 | id, isMobile,
10 | });
11 |
12 |
13 | export const blockUpdated = (fieldId, blockId) => ({
14 | type: 'BLOCK_UPDATED',
15 | fieldId, blockId,
16 | });
17 |
18 |
19 | export const changeBlockValue = (fieldId, blockId, value) => ({
20 | type: 'CHANGE_BLOCK_VALUES',
21 | fieldId, blockId,
22 | value,
23 | });
24 |
25 |
26 | export const toggleBlock = (fieldId, blockId) => ({
27 | type: 'TOGGLE_BLOCK',
28 | fieldId, blockId,
29 | });
30 |
31 |
32 | export const showBlock = (fieldId, blockId) => dispatch => {
33 | return new Promise(resolve => {
34 | setTimeout(resolve, 0.001);
35 | }).then(() => {
36 | dispatch({
37 | type: 'SHOW_BLOCK',
38 | fieldId, blockId,
39 | });
40 | });
41 | };
42 |
43 |
44 | export const hideBlock = (fieldId, blockId) => ({
45 | type: 'HIDE_BLOCK',
46 | fieldId, blockId,
47 | });
48 |
49 |
50 | export const addBlock = (fieldId, parentId, index, blockType) => ({
51 | type: 'ADD_BLOCK',
52 | fieldId, parentId, index, blockType,
53 | });
54 |
55 |
56 | export const duplicateBlock = (fieldId, blockId) => ({
57 | type: 'DUPLICATE_BLOCK',
58 | fieldId, blockId,
59 | });
60 |
61 |
62 | export const moveBlock = (fieldId, blockId, newIndex) => ({
63 | type: 'MOVE_BLOCK',
64 | fieldId, blockId, newIndex,
65 | });
66 |
67 |
68 | export const deleteBlock = (fieldId, blockId) => ({
69 | type: 'DELETE_BLOCK',
70 | fieldId, blockId,
71 | });
72 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as StreamField } from './StreamField';
2 | export { default as streamFieldReducer } from './reducer';
3 |
--------------------------------------------------------------------------------
/src/processing/conversions.js:
--------------------------------------------------------------------------------
1 | import {
2 | getBlockDefinition,
3 | getNestedBlockDefinition,
4 | isField,
5 | isStruct,
6 | getNewBlock, getNewId, isClosed, isNA,
7 | } from './utils';
8 |
9 |
10 | export const getNestedBlocksState = (parentBlockId, blockDefinitions,
11 | blocks) => {
12 | const childrenBlocksIds = [];
13 | let blocksState = {};
14 | let descendantsBlocksState = {};
15 |
16 | for (let block of blocks) {
17 | const blockId = block.id === undefined ? getNewId() : block.id;
18 | const blockDefinition = blockDefinitions.length === 1 ?
19 | blockDefinitions[0] : getBlockDefinition(blockDefinitions, block.type);
20 | const blockIsField = isField(blockDefinition);
21 | let value = block.value;
22 | if (!blockIsField) {
23 | if (isNA(value)) {
24 | value = [];
25 | }
26 | if (isStruct(blockDefinition)) {
27 | for (let childBlockDefinition of blockDefinition.children) {
28 | const childBlockType = childBlockDefinition.key;
29 | const childBlock = value.find(
30 | childBlock => childBlock.type === childBlockType);
31 | if (childBlock === undefined) {
32 | let [childBlockId, childBlock, extraBlocks] =
33 | getNewBlock(blockId, childBlockDefinition);
34 | blocksState = {
35 | ...blocksState,
36 | ...extraBlocks,
37 | [childBlockId]: childBlock,
38 | };
39 | value.push({id: childBlockId, ...childBlock});
40 | }
41 | }
42 | }
43 | [value, descendantsBlocksState] = getNestedBlocksState(
44 | blockId, blockDefinition.children, value);
45 | blocksState = {
46 | ...blocksState,
47 | ...descendantsBlocksState,
48 | };
49 | }
50 | childrenBlocksIds.push(blockId);
51 | blocksState[blockId] = {
52 | parent: parentBlockId,
53 | type: blockDefinition.key,
54 | html: block.html,
55 | hasError: block.hasError,
56 | value: value,
57 | hidden: false,
58 | closed: isClosed(blockDefinition),
59 | shouldUpdate: false,
60 | isField: blockIsField,
61 | };
62 | }
63 |
64 | return [childrenBlocksIds, blocksState];
65 | };
66 |
67 |
68 | export const valueToState = (prevState, fieldId, value) => {
69 | const [rootBlocks, blocks] = getNestedBlocksState(
70 | null, prevState[fieldId].blockDefinitions, value);
71 |
72 | // Delete internal field created only for browsing data.
73 | for (let block of Object.values(blocks)) {
74 | delete block['isField'];
75 | }
76 |
77 | return {
78 | ...prevState,
79 | [fieldId]: {
80 | ...prevState[fieldId],
81 | rootBlocks: rootBlocks,
82 | blocks: blocks,
83 | },
84 | };
85 | };
86 |
87 |
88 | export const extractValue = (state, fieldId, blockId) => {
89 | const blocks = state[fieldId].blocks;
90 | const block = blocks[blockId];
91 | let value = block.value;
92 | const blockDefinition = getNestedBlockDefinition(state, fieldId, blockId);
93 | if (!isField(blockDefinition)) {
94 | value = value.map(childBlockId => extractValue(state, fieldId,
95 | childBlockId));
96 | }
97 | return {
98 | id: blockId,
99 | type: block.type,
100 | value: value,
101 | };
102 | };
103 |
104 |
105 | export const stateToValue = (state, fieldId) => {
106 | const fieldData = state[fieldId];
107 | return fieldData.rootBlocks.map(
108 | blockId => extractValue(state, fieldId, blockId));
109 | };
110 |
--------------------------------------------------------------------------------
/src/processing/conversions.test.js:
--------------------------------------------------------------------------------
1 | import {stateToValue, valueToState} from './conversions';
2 | import {
3 | initialState,
4 | fieldId,
5 | rootBlock1,
6 | rootBlock2,
7 | listBlock,
8 | structBlock,
9 | streamBlock,
10 | rootBlock1Id,
11 | rootBlock2Id,
12 | listBlockId,
13 | listBlockImage1Id,
14 | listBlockImage2Id,
15 | structBlockId,
16 | structBlockHeightId,
17 | structBlockImagesId,
18 | structBlockImageCell1Id,
19 | structBlockImage1Id,
20 | structBlockWidth1Id,
21 | structBlockImageCell2Id,
22 | structBlockImage2Id,
23 | structBlockWidth2Id,
24 | structBlockRelatedPagesId,
25 | structBlockPageId,
26 | streamBlockId,
27 | streamBlockImageId,
28 | streamBlockTitleId,
29 | rootBlock1State,
30 | rootBlock2State,
31 | listBlockState,
32 | listBlockImage1State,
33 | listBlockImage2State,
34 | structBlockState,
35 | structBlockHeightState,
36 | structBlockImagesState,
37 | structBlockImage1State,
38 | structBlockWidth1State,
39 | structBlockImageCell1State,
40 | structBlockImage2State,
41 | structBlockWidth2State,
42 | structBlockImageCell2State,
43 | structBlockRelatedPagesState,
44 | structBlockPageState,
45 | streamBlockState,
46 | streamBlockImage1State,
47 | streamBlockImage2State,
48 | } from './samples';
49 |
50 |
51 | describe('valueToState', () => {
52 | test('Empty value', () => {
53 | expect(valueToState(initialState, fieldId, [])).toEqual({
54 | ...initialState,
55 | [fieldId]: {
56 | ...initialState[fieldId],
57 | rootBlocks: [],
58 | blocks: {},
59 | },
60 | });
61 | });
62 |
63 | test('Root blocks', () => {
64 | expect(valueToState(initialState, fieldId, [
65 | rootBlock1,
66 | rootBlock2,
67 | ])).toEqual({
68 | ...initialState,
69 | [fieldId]: {
70 | ...initialState[fieldId],
71 | rootBlocks: [rootBlock1Id, rootBlock2Id],
72 | blocks: {
73 | [rootBlock1Id]: rootBlock1State,
74 | [rootBlock2Id]: rootBlock2State,
75 | },
76 | },
77 | });
78 | });
79 |
80 | test('ListBlock', () => {
81 | const result = valueToState(initialState, fieldId, [listBlock]);
82 | expect(result).toEqual({
83 | ...initialState,
84 | [fieldId]: {
85 | ...initialState[fieldId],
86 | rootBlocks: [listBlockId],
87 | blocks: {
88 | [listBlockId]: listBlockState,
89 | [listBlockImage1Id]: listBlockImage1State,
90 | [listBlockImage2Id]: listBlockImage2State,
91 | }
92 | }
93 | })
94 | });
95 |
96 | test('StructBlock', () => {
97 | const result = valueToState(initialState, fieldId, [structBlock]);
98 | expect(result).toEqual({
99 | ...initialState,
100 | [fieldId]: {
101 | ...initialState[fieldId],
102 | rootBlocks: [structBlockId],
103 | blocks: {
104 | [structBlockId]: structBlockState,
105 | [structBlockHeightId]: structBlockHeightState,
106 | [structBlockImagesId]: structBlockImagesState,
107 | [structBlockImageCell1Id]: structBlockImageCell1State,
108 | [structBlockImage1Id]: structBlockImage1State,
109 | [structBlockWidth1Id]: structBlockWidth1State,
110 | [structBlockImageCell2Id]: structBlockImageCell2State,
111 | [structBlockImage2Id]: structBlockImage2State,
112 | [structBlockWidth2Id]: structBlockWidth2State,
113 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
114 | [structBlockPageId]: structBlockPageState,
115 | },
116 | },
117 | });
118 | });
119 |
120 | test('StructBlock with missing nested data', () => {
121 | const result = valueToState(initialState, fieldId, [
122 | {
123 | ...structBlock,
124 | value: [],
125 | },
126 | ]);
127 | const [childHeightId, childImagesId, childRelatedPagesId] =
128 | result[fieldId].blocks[structBlockId].value;
129 | expect(result).toEqual({
130 | ...initialState,
131 | [fieldId]: {
132 | ...initialState[fieldId],
133 | rootBlocks: [structBlockId],
134 | blocks: {
135 | [structBlockId]: {
136 | ...structBlockState,
137 | value: [childHeightId, childImagesId, childRelatedPagesId],
138 | },
139 | [childHeightId]: {
140 | parent: structBlockId,
141 | type: 'height',
142 | value: null,
143 | closed: true,
144 | hidden: false,
145 | shouldUpdate: false
146 | },
147 | [childImagesId]: {
148 | parent: structBlockId,
149 | type: 'images',
150 | value: [],
151 | closed: true,
152 | hidden: false,
153 | shouldUpdate: false
154 | },
155 | [childRelatedPagesId]: {
156 | parent: structBlockId,
157 | type: 'related_pages',
158 | value: [],
159 | closed: true,
160 | hidden: false,
161 | shouldUpdate: false
162 | },
163 | },
164 | },
165 | });
166 | });
167 |
168 | test('StreamBlock', () => {
169 | const result = valueToState(initialState, fieldId, [streamBlock]);
170 | expect(result).toEqual({
171 | ...initialState,
172 | [fieldId]: {
173 | ...initialState[fieldId],
174 | rootBlocks: [streamBlockId],
175 | blocks: {
176 | [streamBlockId]: streamBlockState,
177 | [streamBlockImageId]: streamBlockImage1State,
178 | [streamBlockTitleId]: streamBlockImage2State,
179 | },
180 | },
181 | });
182 | });
183 | });
184 |
185 |
186 | describe('stateToValue', () => {
187 | test('Empty value', () => {
188 | const value = [];
189 | expect(stateToValue(valueToState(initialState, fieldId, value), fieldId))
190 | .toEqual(value);
191 | });
192 |
193 | test('Root blocks', () => {
194 | const value = [
195 | rootBlock1,
196 | rootBlock2,
197 | ];
198 | expect(stateToValue(valueToState(initialState, fieldId, value), fieldId))
199 | .toEqual(value);
200 | });
201 |
202 | test('ListBlock', () => {
203 | const value = [listBlock];
204 | expect(stateToValue(valueToState(initialState, fieldId, value), fieldId))
205 | .toEqual(value);
206 | });
207 |
208 | test('StructBlock', () => {
209 | const value = [structBlock];
210 | expect(stateToValue(valueToState(initialState, fieldId, value), fieldId))
211 | .toEqual(value);
212 | });
213 |
214 | test('StreamBlock', () => {
215 | const value = [streamBlock];
216 | expect(stateToValue(valueToState(initialState, fieldId, value), fieldId))
217 | .toEqual(value);
218 | });
219 | });
220 |
--------------------------------------------------------------------------------
/src/processing/reducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | applyToBlocks,
3 | getBlockDefinition,
4 | getChildrenIds,
5 | getDescendantsIds,
6 | getNestedBlockDefinition,
7 | getNewBlock, getNewId,
8 | isField,
9 | isStruct,
10 | } from './utils';
11 |
12 |
13 | export const updateChildren = (state, fieldId, parentId) => {
14 | const childrenIds = getChildrenIds(state, fieldId, parentId);
15 | return applyToBlocks(state, fieldId, childrenIds, block => ({
16 | ...block,
17 | shouldUpdate: true,
18 | }));
19 | };
20 |
21 |
22 | export const setChildren = (state, fieldId, parentId, childrenIds) => {
23 | let fieldData = state[fieldId];
24 | let blocks = fieldData.blocks;
25 | if (parentId === null) {
26 | fieldData = {...fieldData, rootBlocks: childrenIds};
27 | } else {
28 | blocks = {
29 | ...blocks,
30 | [parentId]: {...blocks[parentId], value: childrenIds},
31 | };
32 | }
33 | return {
34 | ...state,
35 | [fieldId]: {
36 | ...fieldData,
37 | blocks: blocks,
38 | },
39 | };
40 | };
41 |
42 |
43 | export const insertBlock = (state, fieldId, parentId, index,
44 | blockId, block) => {
45 | const siblingsIds = [...getChildrenIds(state, fieldId, parentId)];
46 | siblingsIds.splice(index, 0, blockId);
47 | state = setChildren(state, fieldId, parentId, siblingsIds);
48 |
49 | const fieldData = state[fieldId];
50 | const blocks = fieldData.blocks;
51 | state = {
52 | ...state,
53 | [fieldId]: {
54 | ...fieldData,
55 | blocks: {
56 | ...blocks,
57 | [blockId]: block,
58 | },
59 | },
60 | };
61 | return updateChildren(state, fieldId, parentId);
62 | };
63 |
64 |
65 | export const moveBlock = (state, fieldId, blockId, newIndex) => {
66 | if (newIndex < 0) {
67 | throw new Error(`Index ${newIndex} is out of bounds.`);
68 | }
69 | const fieldData = state[fieldId];
70 | let blocks = fieldData.blocks;
71 | const block = blocks[blockId];
72 | const siblingsIds = [...getChildrenIds(state, fieldId, block.parent)];
73 |
74 | if (newIndex >= siblingsIds.length) {
75 | throw new Error(`Index ${newIndex} is out of bounds.`);
76 | }
77 |
78 | const oldIndex = siblingsIds.indexOf(blockId);
79 | siblingsIds.splice(oldIndex, 1);
80 | siblingsIds.splice(newIndex, 0, blockId);
81 |
82 | state = setChildren(state, fieldId, block.parent, siblingsIds);
83 | return updateChildren(state, fieldId, block.parent);
84 | };
85 |
86 |
87 | export const addBlock = (state, fieldId, parentId, index, type) => {
88 | const fieldData = state[fieldId];
89 | let blockDefinitions;
90 | if (parentId === null) {
91 | blockDefinitions = fieldData.blockDefinitions;
92 | } else {
93 | const parentBlockDefinition = getNestedBlockDefinition(state, fieldId,
94 | parentId);
95 | blockDefinitions = parentBlockDefinition.children;
96 | }
97 | const blockDefinition = getBlockDefinition(blockDefinitions, type);
98 | const [blockId, block, extraBlocks] = getNewBlock(parentId, blockDefinition);
99 | state = {
100 | ...state,
101 | [fieldId]: {
102 | ...fieldData,
103 | blocks: {
104 | ...fieldData.blocks,
105 | ...extraBlocks,
106 | },
107 | },
108 | };
109 | return insertBlock(state, fieldId, parentId, index, blockId, block);
110 | };
111 |
112 |
113 | export const getIndex = (state, fieldId, blockId) => {
114 | const block = state[fieldId].blocks[blockId];
115 | const siblingsIds = [...getChildrenIds(state, fieldId, block.parent)];
116 | return siblingsIds.indexOf(blockId);
117 | };
118 |
119 |
120 | export const cloneBlock = (state, fieldId, parentId, blockId) => {
121 | const fieldData = state[fieldId];
122 | const blocks = {...fieldData.blocks};
123 | const newBlockId = getNewId();
124 | const newBlock = {...blocks[blockId], parent: parentId};
125 | let newBlocks = {[newBlockId]: newBlock};
126 | const blockDefinition = getNestedBlockDefinition(state, fieldId, blockId);
127 | let value = newBlock.value;
128 |
129 | if (isStruct(blockDefinition)) {
130 | const newValue = [];
131 | for (let childBlockId of value) {
132 | let [newChildId, newChildrenBlocks] =
133 | cloneBlock(state, fieldId, newBlockId, childBlockId);
134 | newBlocks = {...newBlocks, ...newChildrenBlocks};
135 | newValue.push(newChildId);
136 | }
137 | value = newValue;
138 | } else if (!isField(blockDefinition)) {
139 | const newValue = [];
140 | for (let childBlockId of value) {
141 | const [newChildBlockId, newChildrenBlocks] =
142 | cloneBlock(state, fieldId, newBlockId, childBlockId);
143 | newValue.push(newChildBlockId);
144 | newBlocks = {...newBlocks, ...newChildrenBlocks};
145 | }
146 | value = newValue;
147 | }
148 | newBlock.value = value;
149 | return [newBlockId, newBlocks];
150 | };
151 |
152 |
153 | export const duplicateBlock = (state, fieldId, blockId) => {
154 | const fieldData = state[fieldId];
155 | const blocks = fieldData.blocks;
156 | const parentId = blocks[blockId].parent;
157 | const [newBlockId, newBlocks] =
158 | cloneBlock(state, fieldId, parentId, blockId);
159 | state = {
160 | ...state,
161 | [fieldId]: {
162 | ...fieldData,
163 | blocks: {
164 | ...blocks,
165 | ...newBlocks,
166 | },
167 | },
168 | };
169 | const block = newBlocks[newBlockId];
170 | block.hidden = true;
171 | const index = getIndex(state, fieldId, blockId) + 1; // + 1 to add after.
172 | return insertBlock(state, fieldId, parentId, index, newBlockId, block);
173 | };
174 |
175 |
176 | export const deleteBlock = (state, fieldId, blockId) => {
177 | const fieldData = state[fieldId];
178 | let rootBlocks = [...fieldData.rootBlocks];
179 | const blocks = {...fieldData.blocks};
180 | const block = blocks[blockId];
181 | let shouldUpdateSiblings = true;
182 | if (block.parent === null) {
183 | rootBlocks = rootBlocks.filter(childBlockId => childBlockId !== blockId);
184 | } else {
185 | const parentBlockDefinition = getNestedBlockDefinition(state, fieldId,
186 | block.parent);
187 | const parentBlock = blocks[block.parent];
188 | if (isStruct(parentBlockDefinition)) {
189 | shouldUpdateSiblings = false;
190 | }
191 | blocks[block.parent] = {
192 | ...parentBlock,
193 | closed: false, // We make sure it’s open for when we remove
194 | shouldUpdate: true, // an errored block from a list block, and we
195 | // force update the header color.
196 | value: parentBlock.value.filter(
197 | childBlockId => childBlockId !== blockId),
198 | };
199 | }
200 | for (let descendantBlockId of getDescendantsIds(state, fieldId, blockId,
201 | true)) {
202 | delete blocks[descendantBlockId];
203 | }
204 | state = {
205 | ...state,
206 | [fieldId]: {
207 | ...fieldData,
208 | rootBlocks,
209 | blocks,
210 | },
211 | };
212 | if (shouldUpdateSiblings) {
213 | return updateChildren(state, fieldId, block.parent);
214 | }
215 | return state;
216 | };
217 |
--------------------------------------------------------------------------------
/src/processing/reducers.test.js:
--------------------------------------------------------------------------------
1 | import {addBlock, deleteBlock, moveBlock} from './reducers';
2 | import {valueToState} from './conversions';
3 | import {
4 | initialState,
5 | fieldId,
6 | rootBlock1,
7 | rootBlock2,
8 | listBlock,
9 | structBlock,
10 | streamBlock,
11 | rootBlock1Id,
12 | rootBlock2Id,
13 | listBlockId,
14 | listBlockImage1Id,
15 | listBlockImage2Id,
16 | structBlockId,
17 | structBlockHeightId,
18 | structBlockImagesId,
19 | structBlockImageCell1Id,
20 | structBlockImage1Id,
21 | structBlockWidth1Id,
22 | structBlockImageCell2Id,
23 | structBlockImage2Id,
24 | structBlockWidth2Id,
25 | structBlockRelatedPagesId,
26 | structBlockPageId,
27 | streamBlockId,
28 | streamBlockImageId,
29 | streamBlockTitleId,
30 | rootBlock1State,
31 | rootBlock2State,
32 | listBlockState,
33 | listBlockImage1State,
34 | listBlockImage2State,
35 | structBlockState,
36 | structBlockHeightState,
37 | structBlockImagesState,
38 | structBlockImage1State,
39 | structBlockWidth1State,
40 | structBlockImageCell1State,
41 | structBlockImage2State,
42 | structBlockWidth2State,
43 | structBlockImageCell2State,
44 | structBlockRelatedPagesState,
45 | structBlockPageState,
46 | streamBlockState,
47 | streamBlockImage1State,
48 | streamBlockImage2State,
49 | } from './samples';
50 |
51 |
52 | describe('addBlock', () => {
53 | test('As root', () => {
54 | let state = valueToState(initialState, fieldId, []);
55 | state = addBlock(state, fieldId, null, 0, 'image');
56 | const id1 = state[fieldId].rootBlocks[0];
57 | expect(state).toEqual({
58 | ...state,
59 | [fieldId]: {
60 | ...state[fieldId],
61 | rootBlocks: [id1],
62 | blocks: {
63 | [id1]: {
64 | parent: null,
65 | type: 'image',
66 | value: null,
67 | closed: false,
68 | hidden: true,
69 | shouldUpdate: true,
70 | },
71 | },
72 | },
73 | });
74 | state = addBlock(state, fieldId, null, 0, 'carousel');
75 | const id2 = state[fieldId].rootBlocks[0];
76 | expect(state).toEqual({
77 | ...state,
78 | [fieldId]: {
79 | ...state[fieldId],
80 | rootBlocks: [id2, id1],
81 | blocks: {
82 | [id1]: {
83 | parent: null,
84 | type: 'image',
85 | value: null,
86 | closed: false,
87 | hidden: true,
88 | shouldUpdate: true,
89 | },
90 | [id2]: {
91 | parent: null,
92 | type: 'carousel',
93 | value: [],
94 | closed: false,
95 | hidden: true,
96 | shouldUpdate: true,
97 | },
98 | },
99 | },
100 | });
101 | state = addBlock(state, fieldId, null, 2, 'image_row');
102 | const id3 = state[fieldId].rootBlocks[2];
103 | const blocks = state[fieldId].blocks;
104 | const getId = key => {
105 | return blocks[id3].value.find(
106 | childBlockId => blocks[childBlockId].type === key)
107 | };
108 | const childHeightId = getId('height');
109 | const childImagesId = getId('images');
110 | const childRelatedPagesId = getId('related_pages');
111 | expect(state).toEqual({
112 | ...state,
113 | [fieldId]: {
114 | ...state[fieldId],
115 | rootBlocks: [id2, id1, id3],
116 | blocks: {
117 | [id1]: {
118 | parent: null,
119 | type: 'image',
120 | value: null,
121 | closed: false,
122 | hidden: true,
123 | shouldUpdate: true,
124 | },
125 | [id2]: {
126 | parent: null,
127 | type: 'carousel',
128 | value: [],
129 | closed: false,
130 | hidden: true,
131 | shouldUpdate: true,
132 | },
133 | [id3]: {
134 | parent: null,
135 | type: 'image_row',
136 | value: [
137 | childHeightId,
138 | childImagesId,
139 | childRelatedPagesId,
140 | ],
141 | closed: false,
142 | hidden: true,
143 | shouldUpdate: true,
144 | },
145 | [childHeightId]: {
146 | parent: id3,
147 | type: 'height',
148 | value: null,
149 | closed: false,
150 | hidden: true,
151 | shouldUpdate: false,
152 | },
153 | [childImagesId]: {
154 | parent: id3,
155 | type: 'images',
156 | value: [],
157 | closed: false,
158 | hidden: true,
159 | shouldUpdate: false,
160 | },
161 | [childRelatedPagesId]: {
162 | parent: id3,
163 | type: 'related_pages',
164 | value: [],
165 | closed: false,
166 | hidden: true,
167 | shouldUpdate: false,
168 | },
169 | },
170 | },
171 | });
172 | state = addBlock(state, fieldId, null, 1, 'rich_carousel');
173 | const id4 = state[fieldId].rootBlocks[1];
174 | expect(state).toEqual({
175 | ...state,
176 | [fieldId]: {
177 | ...state[fieldId],
178 | rootBlocks: [id2, id4, id1, id3],
179 | blocks: {
180 | [id1]: {
181 | parent: null,
182 | type: 'image',
183 | value: null,
184 | closed: false,
185 | hidden: true,
186 | shouldUpdate: true,
187 | },
188 | [id2]: {
189 | parent: null,
190 | type: 'carousel',
191 | value: [],
192 | closed: false,
193 | hidden: true,
194 | shouldUpdate: true,
195 | },
196 | [id3]: {
197 | parent: null,
198 | type: 'image_row',
199 | value: [
200 | childHeightId,
201 | childImagesId,
202 | childRelatedPagesId,
203 | ],
204 | closed: false,
205 | hidden: true,
206 | shouldUpdate: true,
207 | },
208 | [childHeightId]: {
209 | parent: id3,
210 | type: 'height',
211 | value: null,
212 | closed: false,
213 | hidden: true,
214 | shouldUpdate: false,
215 | },
216 | [childImagesId]: {
217 | parent: id3,
218 | type: 'images',
219 | value: [],
220 | closed: false,
221 | hidden: true,
222 | shouldUpdate: false,
223 | },
224 | [childRelatedPagesId]: {
225 | parent: id3,
226 | type: 'related_pages',
227 | value: [],
228 | closed: false,
229 | hidden: true,
230 | shouldUpdate: false,
231 | },
232 | [id4]: {
233 | parent: null,
234 | type: 'rich_carousel',
235 | value: [],
236 | closed: false,
237 | hidden: true,
238 | shouldUpdate: true,
239 | },
240 | },
241 | },
242 | });
243 | });
244 |
245 | test('As leaf', () => {
246 | let state = valueToState(initialState, fieldId, []);
247 | state = addBlock(state, fieldId, null, 0, 'carousel');
248 | const id1 = state[fieldId].rootBlocks[0];
249 | expect(state).toEqual({
250 | ...state,
251 | [fieldId]: {
252 | ...state[fieldId],
253 | rootBlocks: [id1],
254 | blocks: {
255 | [id1]: {
256 | parent: null,
257 | type: 'carousel',
258 | value: [],
259 | closed: false,
260 | hidden: true,
261 | shouldUpdate: true,
262 | },
263 | },
264 | },
265 | });
266 | state = addBlock(state, fieldId, id1, 0, 'image');
267 | const id2 = state[fieldId].blocks[id1].value[0];
268 | expect(state).toEqual({
269 | ...state,
270 | [fieldId]: {
271 | ...state[fieldId],
272 | rootBlocks: [id1],
273 | blocks: {
274 | [id1]: {
275 | parent: null,
276 | type: 'carousel',
277 | value: [id2],
278 | closed: false,
279 | hidden: true,
280 | shouldUpdate: true,
281 | },
282 | [id2]: {
283 | parent: id1,
284 | type: 'image',
285 | value: null,
286 | closed: false,
287 | hidden: true,
288 | shouldUpdate: true,
289 | },
290 | },
291 | },
292 | });
293 | state = addBlock(state, fieldId, id1, 0, 'image');
294 | const id3 = state[fieldId].blocks[id1].value[0];
295 | expect(state).toEqual({
296 | ...state,
297 | [fieldId]: {
298 | ...state[fieldId],
299 | rootBlocks: [id1],
300 | blocks: {
301 | [id1]: {
302 | parent: null,
303 | type: 'carousel',
304 | value: [id3, id2],
305 | closed: false,
306 | hidden: true,
307 | shouldUpdate: true,
308 | },
309 | [id2]: {
310 | parent: id1,
311 | type: 'image',
312 | value: null,
313 | closed: false,
314 | hidden: true,
315 | shouldUpdate: true,
316 | },
317 | [id3]: {
318 | parent: id1,
319 | type: 'image',
320 | value: null,
321 | closed: false,
322 | hidden: true,
323 | shouldUpdate: true,
324 | },
325 | },
326 | },
327 | });
328 | state = addBlock(state, fieldId, id1, 2, 'image');
329 | const id4 = state[fieldId].blocks[id1].value[2];
330 | expect(state).toEqual({
331 | ...state,
332 | [fieldId]: {
333 | ...state[fieldId],
334 | rootBlocks: [id1],
335 | blocks: {
336 | [id1]: {
337 | parent: null,
338 | type: 'carousel',
339 | value: [id3, id2, id4],
340 | closed: false,
341 | hidden: true,
342 | shouldUpdate: true,
343 | },
344 | [id2]: {
345 | parent: id1,
346 | type: 'image',
347 | value: null,
348 | closed: false,
349 | hidden: true,
350 | shouldUpdate: true,
351 | },
352 | [id3]: {
353 | parent: id1,
354 | type: 'image',
355 | value: null,
356 | closed: false,
357 | hidden: true,
358 | shouldUpdate: true,
359 | },
360 | [id4]: {
361 | parent: id1,
362 | type: 'image',
363 | value: null,
364 | closed: false,
365 | hidden: true,
366 | shouldUpdate: true,
367 | },
368 | },
369 | },
370 | });
371 | state = addBlock(state, fieldId, id1, 1, 'image');
372 | const id5 = state[fieldId].blocks[id1].value[1];
373 | expect(state).toEqual({
374 | ...state,
375 | [fieldId]: {
376 | ...state[fieldId],
377 | rootBlocks: [id1],
378 | blocks: {
379 | [id1]: {
380 | parent: null,
381 | type: 'carousel',
382 | value: [id3, id5, id2, id4],
383 | closed: false,
384 | hidden: true,
385 | shouldUpdate: true,
386 | },
387 | [id2]: {
388 | parent: id1,
389 | type: 'image',
390 | value: null,
391 | closed: false,
392 | hidden: true,
393 | shouldUpdate: true,
394 | },
395 | [id3]: {
396 | parent: id1,
397 | type: 'image',
398 | value: null,
399 | closed: false,
400 | hidden: true,
401 | shouldUpdate: true,
402 | },
403 | [id4]: {
404 | parent: id1,
405 | type: 'image',
406 | value: null,
407 | closed: false,
408 | hidden: true,
409 | shouldUpdate: true,
410 | },
411 | [id5]: {
412 | parent: id1,
413 | type: 'image',
414 | value: null,
415 | closed: false,
416 | hidden: true,
417 | shouldUpdate: true,
418 | },
419 | },
420 | },
421 | });
422 | });
423 | });
424 |
425 |
426 | describe('moveBlock', () => {
427 | test('As root', () => {
428 | let state = valueToState(initialState, fieldId, [
429 | rootBlock1, rootBlock2, structBlock,
430 | ]);
431 | expect(state).toEqual({
432 | ...state,
433 | [fieldId]: {
434 | ...state[fieldId],
435 | rootBlocks: [
436 | rootBlock1Id, rootBlock2Id, structBlockId,
437 | ],
438 | blocks: {
439 | [rootBlock1Id]: rootBlock1State,
440 | [rootBlock2Id]: rootBlock2State,
441 | [structBlockId]: structBlockState,
442 | [structBlockHeightId]: structBlockHeightState,
443 | [structBlockImagesId]: structBlockImagesState,
444 | [structBlockImageCell1Id]: structBlockImageCell1State,
445 | [structBlockImage1Id]: structBlockImage1State,
446 | [structBlockWidth1Id]: structBlockWidth1State,
447 | [structBlockImageCell2Id]: structBlockImageCell2State,
448 | [structBlockImage2Id]: structBlockImage2State,
449 | [structBlockWidth2Id]: structBlockWidth2State,
450 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
451 | [structBlockPageId]: structBlockPageState,
452 | },
453 | },
454 | });
455 | state = moveBlock(state, fieldId, rootBlock1Id, 0);
456 | expect(state).toEqual({
457 | ...state,
458 | [fieldId]: {
459 | ...state[fieldId],
460 | rootBlocks: [
461 | rootBlock1Id, rootBlock2Id, structBlockId,
462 | ],
463 | blocks: {
464 | [rootBlock1Id]: {
465 | ...rootBlock1State,
466 | shouldUpdate: true,
467 | },
468 | [rootBlock2Id]: {
469 | ...rootBlock2State,
470 | shouldUpdate: true,
471 | },
472 | [structBlockId]: {
473 | ...structBlockState,
474 | shouldUpdate: true,
475 | },
476 | [structBlockHeightId]: structBlockHeightState,
477 | [structBlockImagesId]: structBlockImagesState,
478 | [structBlockImageCell1Id]: structBlockImageCell1State,
479 | [structBlockImage1Id]: structBlockImage1State,
480 | [structBlockWidth1Id]: structBlockWidth1State,
481 | [structBlockImageCell2Id]: structBlockImageCell2State,
482 | [structBlockImage2Id]: structBlockImage2State,
483 | [structBlockWidth2Id]: structBlockWidth2State,
484 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
485 | [structBlockPageId]: structBlockPageState,
486 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
487 | [structBlockPageId]: structBlockPageState,
488 | },
489 | },
490 | });
491 | state = moveBlock(state, fieldId, rootBlock2Id, 1);
492 | expect(state).toEqual({
493 | ...state,
494 | [fieldId]: {
495 | ...state[fieldId],
496 | rootBlocks: [
497 | rootBlock1Id, rootBlock2Id, structBlockId,
498 | ],
499 | blocks: {
500 | [rootBlock1Id]: {
501 | ...rootBlock1State,
502 | shouldUpdate: true,
503 | },
504 | [rootBlock2Id]: {
505 | ...rootBlock2State,
506 | shouldUpdate: true,
507 | },
508 | [structBlockId]: {
509 | ...structBlockState,
510 | shouldUpdate: true,
511 | },
512 | [structBlockHeightId]: structBlockHeightState,
513 | [structBlockImagesId]: structBlockImagesState,
514 | [structBlockImageCell1Id]: structBlockImageCell1State,
515 | [structBlockImage1Id]: structBlockImage1State,
516 | [structBlockWidth1Id]: structBlockWidth1State,
517 | [structBlockImageCell2Id]: structBlockImageCell2State,
518 | [structBlockImage2Id]: structBlockImage2State,
519 | [structBlockWidth2Id]: structBlockWidth2State,
520 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
521 | [structBlockPageId]: structBlockPageState,
522 | },
523 | },
524 | });
525 | state = moveBlock(state, fieldId, structBlockId, 2);
526 | expect(state).toEqual({
527 | ...state,
528 | [fieldId]: {
529 | ...state[fieldId],
530 | rootBlocks: [
531 | rootBlock1Id, rootBlock2Id, structBlockId,
532 | ],
533 | blocks: {
534 | [rootBlock1Id]: {
535 | ...rootBlock1State,
536 | shouldUpdate: true,
537 | },
538 | [rootBlock2Id]: {
539 | ...rootBlock2State,
540 | shouldUpdate: true,
541 | },
542 | [structBlockId]: {
543 | ...structBlockState,
544 | shouldUpdate: true,
545 | },
546 | [structBlockHeightId]: structBlockHeightState,
547 | [structBlockImagesId]: structBlockImagesState,
548 | [structBlockImageCell1Id]: structBlockImageCell1State,
549 | [structBlockImage1Id]: structBlockImage1State,
550 | [structBlockWidth1Id]: structBlockWidth1State,
551 | [structBlockImageCell2Id]: structBlockImageCell2State,
552 | [structBlockImage2Id]: structBlockImage2State,
553 | [structBlockWidth2Id]: structBlockWidth2State,
554 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
555 | [structBlockPageId]: structBlockPageState,
556 | },
557 | },
558 | });
559 | state = moveBlock(state, fieldId, rootBlock1Id, 1);
560 | expect(state).toEqual({
561 | ...state,
562 | [fieldId]: {
563 | ...state[fieldId],
564 | rootBlocks: [
565 | rootBlock2Id, rootBlock1Id, structBlockId,
566 | ],
567 | blocks: {
568 | [rootBlock1Id]: {
569 | ...rootBlock1State,
570 | shouldUpdate: true,
571 | },
572 | [rootBlock2Id]: {
573 | ...rootBlock2State,
574 | shouldUpdate: true,
575 | },
576 | [structBlockId]: {
577 | ...structBlockState,
578 | shouldUpdate: true,
579 | },
580 | [structBlockHeightId]: structBlockHeightState,
581 | [structBlockImagesId]: structBlockImagesState,
582 | [structBlockImageCell1Id]: structBlockImageCell1State,
583 | [structBlockImage1Id]: structBlockImage1State,
584 | [structBlockWidth1Id]: structBlockWidth1State,
585 | [structBlockImageCell2Id]: structBlockImageCell2State,
586 | [structBlockImage2Id]: structBlockImage2State,
587 | [structBlockWidth2Id]: structBlockWidth2State,
588 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
589 | [structBlockPageId]: structBlockPageState,
590 | },
591 | },
592 | });
593 | state = moveBlock(state, fieldId, rootBlock1Id, 0);
594 | expect(state).toEqual({
595 | ...state,
596 | [fieldId]: {
597 | ...state[fieldId],
598 | rootBlocks: [
599 | rootBlock1Id, rootBlock2Id, structBlockId,
600 | ],
601 | blocks: {
602 | [rootBlock1Id]: {
603 | ...rootBlock1State,
604 | shouldUpdate: true,
605 | },
606 | [rootBlock2Id]: {
607 | ...rootBlock2State,
608 | shouldUpdate: true,
609 | },
610 | [structBlockId]: {
611 | ...structBlockState,
612 | shouldUpdate: true,
613 | },
614 | [structBlockHeightId]: structBlockHeightState,
615 | [structBlockImagesId]: structBlockImagesState,
616 | [structBlockImageCell1Id]: structBlockImageCell1State,
617 | [structBlockImage1Id]: structBlockImage1State,
618 | [structBlockWidth1Id]: structBlockWidth1State,
619 | [structBlockImageCell2Id]: structBlockImageCell2State,
620 | [structBlockImage2Id]: structBlockImage2State,
621 | [structBlockWidth2Id]: structBlockWidth2State,
622 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
623 | [structBlockPageId]: structBlockPageState,
624 | },
625 | },
626 | });
627 | state = moveBlock(state, fieldId, structBlockId, 0);
628 | expect(state).toEqual({
629 | ...state,
630 | [fieldId]: {
631 | ...state[fieldId],
632 | rootBlocks: [
633 | structBlockId, rootBlock1Id, rootBlock2Id,
634 | ],
635 | blocks: {
636 | [rootBlock1Id]: {
637 | ...rootBlock1State,
638 | shouldUpdate: true,
639 | },
640 | [rootBlock2Id]: {
641 | ...rootBlock2State,
642 | shouldUpdate: true,
643 | },
644 | [structBlockId]: {
645 | ...structBlockState,
646 | shouldUpdate: true,
647 | },
648 | [structBlockHeightId]: structBlockHeightState,
649 | [structBlockImagesId]: structBlockImagesState,
650 | [structBlockImageCell1Id]: structBlockImageCell1State,
651 | [structBlockImage1Id]: structBlockImage1State,
652 | [structBlockWidth1Id]: structBlockWidth1State,
653 | [structBlockImageCell2Id]: structBlockImageCell2State,
654 | [structBlockImage2Id]: structBlockImage2State,
655 | [structBlockWidth2Id]: structBlockWidth2State,
656 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
657 | [structBlockPageId]: structBlockPageState,
658 | },
659 | },
660 | });
661 | state = moveBlock(state, fieldId, rootBlock1Id, 2);
662 | expect(state).toEqual({
663 | ...state,
664 | [fieldId]: {
665 | ...state[fieldId],
666 | rootBlocks: [
667 | structBlockId, rootBlock2Id, rootBlock1Id,
668 | ],
669 | blocks: {
670 | [rootBlock1Id]: {
671 | ...rootBlock1State,
672 | shouldUpdate: true,
673 | },
674 | [rootBlock2Id]: {
675 | ...rootBlock2State,
676 | shouldUpdate: true,
677 | },
678 | [structBlockId]: {
679 | ...structBlockState,
680 | shouldUpdate: true,
681 | },
682 | [structBlockHeightId]: structBlockHeightState,
683 | [structBlockImagesId]: structBlockImagesState,
684 | [structBlockImageCell1Id]: structBlockImageCell1State,
685 | [structBlockImage1Id]: structBlockImage1State,
686 | [structBlockWidth1Id]: structBlockWidth1State,
687 | [structBlockImageCell2Id]: structBlockImageCell2State,
688 | [structBlockImage2Id]: structBlockImage2State,
689 | [structBlockWidth2Id]: structBlockWidth2State,
690 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
691 | [structBlockPageId]: structBlockPageState,
692 | },
693 | },
694 | });
695 | });
696 |
697 | test('As leaf', () => {
698 | let state = valueToState(initialState, fieldId, [
699 | listBlock,
700 | ]);
701 | expect(state).toEqual({
702 | ...state,
703 | [fieldId]: {
704 | ...state[fieldId],
705 | rootBlocks: [
706 | listBlockId,
707 | ],
708 | blocks: {
709 | [listBlockId]: listBlockState,
710 | [listBlockImage1Id]: listBlockImage1State,
711 | [listBlockImage2Id]: listBlockImage2State,
712 | },
713 | },
714 | });
715 | state = moveBlock(state, fieldId, listBlockImage1Id, 0);
716 | expect(state).toEqual({
717 | ...state,
718 | [fieldId]: {
719 | ...state[fieldId],
720 | rootBlocks: [
721 | listBlockId,
722 | ],
723 | blocks: {
724 | [listBlockId]: listBlockState,
725 | [listBlockImage1Id]: {
726 | ...listBlockImage1State,
727 | shouldUpdate: true,
728 | },
729 | [listBlockImage2Id]: {
730 | ...listBlockImage2State,
731 | shouldUpdate: true,
732 | },
733 | },
734 | },
735 | });
736 | state = moveBlock(state, fieldId, listBlockImage1Id, 1);
737 | expect(state).toEqual({
738 | ...state,
739 | [fieldId]: {
740 | ...state[fieldId],
741 | rootBlocks: [
742 | listBlockId,
743 | ],
744 | blocks: {
745 | [listBlockId]: {
746 | ...listBlockState,
747 | value: [listBlockImage2Id, listBlockImage1Id],
748 | },
749 | [listBlockImage1Id]: {
750 | ...listBlockImage1State,
751 | shouldUpdate: true,
752 | },
753 | [listBlockImage2Id]: {
754 | ...listBlockImage2State,
755 | shouldUpdate: true,
756 | },
757 | },
758 | },
759 | });
760 | state = moveBlock(state, fieldId, listBlockImage2Id, 1);
761 | expect(state).toEqual({
762 | ...state,
763 | [fieldId]: {
764 | ...state[fieldId],
765 | rootBlocks: [
766 | listBlockId,
767 | ],
768 | blocks: {
769 | [listBlockId]: listBlockState,
770 | [listBlockImage1Id]: {
771 | ...listBlockImage1State,
772 | shouldUpdate: true,
773 | },
774 | [listBlockImage2Id]: {
775 | ...listBlockImage2State,
776 | shouldUpdate: true,
777 | },
778 | },
779 | },
780 | });
781 | });
782 | });
783 |
784 |
785 | describe('deleteBlock', () => {
786 | test('As root', () => {
787 | let state = valueToState(initialState, fieldId, [
788 | rootBlock1, rootBlock2,
789 | listBlock, structBlock, streamBlock,
790 | ]);
791 |
792 | expect(state).toEqual({
793 | ...state,
794 | [fieldId]: {
795 | ...state[fieldId],
796 | rootBlocks: [
797 | rootBlock1Id, rootBlock2Id, listBlockId,
798 | structBlockId, streamBlockId,
799 | ],
800 | blocks: {
801 | [rootBlock1Id]: rootBlock1State,
802 | [rootBlock2Id]: rootBlock2State,
803 | [listBlockId]: listBlockState,
804 | [listBlockImage1Id]: listBlockImage1State,
805 | [listBlockImage2Id]: listBlockImage2State,
806 | [structBlockId]: structBlockState,
807 | [structBlockHeightId]: structBlockHeightState,
808 | [structBlockImagesId]: structBlockImagesState,
809 | [structBlockImageCell1Id]: structBlockImageCell1State,
810 | [structBlockImage1Id]: structBlockImage1State,
811 | [structBlockWidth1Id]: structBlockWidth1State,
812 | [structBlockImageCell2Id]: structBlockImageCell2State,
813 | [structBlockImage2Id]: structBlockImage2State,
814 | [structBlockWidth2Id]: structBlockWidth2State,
815 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
816 | [structBlockPageId]: structBlockPageState,
817 | [streamBlockId]: streamBlockState,
818 | [streamBlockImageId]: streamBlockImage1State,
819 | [streamBlockTitleId]: streamBlockImage2State,
820 | },
821 | },
822 | });
823 | state = deleteBlock(state, fieldId, rootBlock1Id);
824 | expect(state).toEqual({
825 | ...state,
826 | [fieldId]: {
827 | ...state[fieldId],
828 | rootBlocks: [
829 | rootBlock2Id, listBlockId,
830 | structBlockId, streamBlockId,
831 | ],
832 | blocks: {
833 | [rootBlock2Id]: {
834 | ...rootBlock2State,
835 | shouldUpdate: true,
836 | },
837 | [listBlockId]: {
838 | ...listBlockState,
839 | shouldUpdate: true,
840 | },
841 | [listBlockImage1Id]: listBlockImage1State,
842 | [listBlockImage2Id]: listBlockImage2State,
843 | [structBlockId]: {
844 | ...structBlockState,
845 | shouldUpdate: true,
846 | },
847 | [structBlockHeightId]: structBlockHeightState,
848 | [structBlockImagesId]: structBlockImagesState,
849 | [structBlockImageCell1Id]: structBlockImageCell1State,
850 | [structBlockImage1Id]: structBlockImage1State,
851 | [structBlockWidth1Id]: structBlockWidth1State,
852 | [structBlockImageCell2Id]: structBlockImageCell2State,
853 | [structBlockImage2Id]: structBlockImage2State,
854 | [structBlockWidth2Id]: structBlockWidth2State,
855 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
856 | [structBlockPageId]: structBlockPageState,
857 | [streamBlockId]: {
858 | ...streamBlockState,
859 | shouldUpdate: true,
860 | },
861 | [streamBlockImageId]: streamBlockImage1State,
862 | [streamBlockTitleId]: streamBlockImage2State,
863 | },
864 | },
865 | });
866 | state = deleteBlock(state, fieldId, rootBlock2Id);
867 | expect(state).toEqual({
868 | ...state,
869 | [fieldId]: {
870 | ...state[fieldId],
871 | rootBlocks: [
872 | listBlockId, structBlockId, streamBlockId,
873 | ],
874 | blocks: {
875 | [listBlockId]: {
876 | ...listBlockState,
877 | shouldUpdate: true,
878 | },
879 | [listBlockImage1Id]: listBlockImage1State,
880 | [listBlockImage2Id]: listBlockImage2State,
881 | [structBlockId]: {
882 | ...structBlockState,
883 | shouldUpdate: true,
884 | },
885 | [structBlockHeightId]: structBlockHeightState,
886 | [structBlockImagesId]: structBlockImagesState,
887 | [structBlockImageCell1Id]: structBlockImageCell1State,
888 | [structBlockImage1Id]: structBlockImage1State,
889 | [structBlockWidth1Id]: structBlockWidth1State,
890 | [structBlockImageCell2Id]: structBlockImageCell2State,
891 | [structBlockImage2Id]: structBlockImage2State,
892 | [structBlockWidth2Id]: structBlockWidth2State,
893 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
894 | [structBlockPageId]: structBlockPageState,
895 | [streamBlockId]: {
896 | ...streamBlockState,
897 | shouldUpdate: true,
898 | },
899 | [streamBlockImageId]: streamBlockImage1State,
900 | [streamBlockTitleId]: streamBlockImage2State,
901 | },
902 | },
903 | });
904 | state = deleteBlock(state, fieldId, listBlockId);
905 | expect(state).toEqual({
906 | ...state,
907 | [fieldId]: {
908 | ...state[fieldId],
909 | rootBlocks: [
910 | structBlockId, streamBlockId,
911 | ],
912 | blocks: {
913 | [structBlockId]: {
914 | ...structBlockState,
915 | shouldUpdate: true,
916 | },
917 | [structBlockHeightId]: structBlockHeightState,
918 | [structBlockImagesId]: structBlockImagesState,
919 | [structBlockImageCell1Id]: structBlockImageCell1State,
920 | [structBlockImage1Id]: structBlockImage1State,
921 | [structBlockWidth1Id]: structBlockWidth1State,
922 | [structBlockImageCell2Id]: structBlockImageCell2State,
923 | [structBlockImage2Id]: structBlockImage2State,
924 | [structBlockWidth2Id]: structBlockWidth2State,
925 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
926 | [structBlockPageId]: structBlockPageState,
927 | [streamBlockId]: {
928 | ...streamBlockState,
929 | shouldUpdate: true,
930 | },
931 | [streamBlockImageId]: streamBlockImage1State,
932 | [streamBlockTitleId]: streamBlockImage2State,
933 | },
934 | },
935 | });
936 | state = deleteBlock(state, fieldId, structBlockId);
937 | expect(state).toEqual({
938 | ...state,
939 | [fieldId]: {
940 | ...state[fieldId],
941 | rootBlocks: [
942 | streamBlockId,
943 | ],
944 | blocks: {
945 | [streamBlockId]: {
946 | ...streamBlockState,
947 | shouldUpdate: true,
948 | },
949 | [streamBlockImageId]: streamBlockImage1State,
950 | [streamBlockTitleId]: streamBlockImage2State,
951 | },
952 | },
953 | });
954 | state = deleteBlock(state, fieldId, streamBlockId);
955 | expect(state).toEqual({
956 | ...state,
957 | [fieldId]: {
958 | ...state[fieldId],
959 | rootBlocks: [],
960 | blocks: {},
961 | },
962 | });
963 | });
964 |
965 | test('As branch', () => {
966 | let state = valueToState(initialState, fieldId, [
967 | structBlock,
968 | ]);
969 | expect(state).toEqual({
970 | ...state,
971 | [fieldId]: {
972 | ...state[fieldId],
973 | rootBlocks: [structBlockId],
974 | blocks: {
975 | [structBlockId]: structBlockState,
976 | [structBlockHeightId]: structBlockHeightState,
977 | [structBlockImagesId]: structBlockImagesState,
978 | [structBlockImageCell1Id]: structBlockImageCell1State,
979 | [structBlockImage1Id]: structBlockImage1State,
980 | [structBlockWidth1Id]: structBlockWidth1State,
981 | [structBlockImageCell2Id]: structBlockImageCell2State,
982 | [structBlockImage2Id]: structBlockImage2State,
983 | [structBlockWidth2Id]: structBlockWidth2State,
984 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
985 | [structBlockPageId]: structBlockPageState,
986 | },
987 | },
988 | });
989 | state = deleteBlock(state, fieldId, structBlockImagesId);
990 | expect(state).toEqual({
991 | ...state,
992 | [fieldId]: {
993 | ...state[fieldId],
994 | rootBlocks: [structBlockId],
995 | blocks: {
996 | [structBlockId]: {
997 | ...structBlockState,
998 | closed: false,
999 | shouldUpdate: true,
1000 | value: [
1001 | structBlockHeightId,
1002 | structBlockRelatedPagesId,
1003 | ],
1004 | },
1005 | [structBlockHeightId]: structBlockHeightState,
1006 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
1007 | [structBlockPageId]: structBlockPageState,
1008 | },
1009 | },
1010 | });
1011 | state = deleteBlock(state, fieldId, structBlockRelatedPagesId);
1012 | expect(state).toEqual({
1013 | ...state,
1014 | [fieldId]: {
1015 | ...state[fieldId],
1016 | rootBlocks: [structBlockId],
1017 | blocks: {
1018 | [structBlockId]: {
1019 | ...structBlockState,
1020 | closed: false,
1021 | shouldUpdate: true,
1022 | value: [
1023 | structBlockHeightId,
1024 | ],
1025 | },
1026 | [structBlockHeightId]: structBlockHeightState,
1027 | },
1028 | },
1029 | });
1030 | });
1031 |
1032 | test('As leaf', () => {
1033 | let state = valueToState(initialState, fieldId, [
1034 | structBlock,
1035 | ]);
1036 | expect(state).toEqual({
1037 | ...state,
1038 | [fieldId]: {
1039 | ...state[fieldId],
1040 | rootBlocks: [structBlockId],
1041 | blocks: {
1042 | [structBlockId]: structBlockState,
1043 | [structBlockHeightId]: structBlockHeightState,
1044 | [structBlockImagesId]: structBlockImagesState,
1045 | [structBlockImageCell1Id]: structBlockImageCell1State,
1046 | [structBlockImage1Id]: structBlockImage1State,
1047 | [structBlockWidth1Id]: structBlockWidth1State,
1048 | [structBlockImageCell2Id]: structBlockImageCell2State,
1049 | [structBlockImage2Id]: structBlockImage2State,
1050 | [structBlockWidth2Id]: structBlockWidth2State,
1051 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
1052 | [structBlockPageId]: structBlockPageState,
1053 | },
1054 | },
1055 | });
1056 | state = deleteBlock(state, fieldId, structBlockImageCell1Id);
1057 | expect(state).toEqual({
1058 | ...state,
1059 | [fieldId]: {
1060 | ...state[fieldId],
1061 | rootBlocks: [structBlockId],
1062 | blocks: {
1063 | [structBlockId]: structBlockState,
1064 | [structBlockHeightId]: structBlockHeightState,
1065 | [structBlockImagesId]: {
1066 | ...structBlockImagesState,
1067 | closed: false,
1068 | shouldUpdate: true,
1069 | value: [structBlockImageCell2Id],
1070 | },
1071 | [structBlockImageCell2Id]: {
1072 | ...structBlockImageCell2State,
1073 | shouldUpdate: true,
1074 | },
1075 | [structBlockImage2Id]: structBlockImage2State,
1076 | [structBlockWidth2Id]: structBlockWidth2State,
1077 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
1078 | [structBlockPageId]: structBlockPageState,
1079 | },
1080 | },
1081 | });
1082 | state = deleteBlock(state, fieldId, structBlockImageCell2Id);
1083 | expect(state).toEqual({
1084 | ...state,
1085 | [fieldId]: {
1086 | ...state[fieldId],
1087 | rootBlocks: [structBlockId],
1088 | blocks: {
1089 | [structBlockId]: structBlockState,
1090 | [structBlockHeightId]: structBlockHeightState,
1091 | [structBlockImagesId]: {
1092 | ...structBlockImagesState,
1093 | closed: false,
1094 | shouldUpdate: true,
1095 | value: [],
1096 | },
1097 | [structBlockRelatedPagesId]: structBlockRelatedPagesState,
1098 | [structBlockPageId]: structBlockPageState,
1099 | },
1100 | },
1101 | });
1102 | state = deleteBlock(state, fieldId, structBlockPageId);
1103 | expect(state).toEqual({
1104 | ...state,
1105 | [fieldId]: {
1106 | ...state[fieldId],
1107 | rootBlocks: [structBlockId],
1108 | blocks: {
1109 | [structBlockId]: structBlockState,
1110 | [structBlockHeightId]: structBlockHeightState,
1111 | [structBlockImagesId]: {
1112 | ...structBlockImagesState,
1113 | closed: false,
1114 | shouldUpdate: true,
1115 | value: [],
1116 | },
1117 | [structBlockRelatedPagesId]: {
1118 | ...structBlockRelatedPagesState,
1119 | closed: false,
1120 | shouldUpdate: true,
1121 | value: [],
1122 | },
1123 | },
1124 | },
1125 | });
1126 | });
1127 | });
1128 |
--------------------------------------------------------------------------------
/src/processing/samples.js:
--------------------------------------------------------------------------------
1 | import uuidv4 from 'uuid';
2 |
3 |
4 | //
5 | // Definition
6 | //
7 |
8 |
9 | export const fieldId = uuidv4();
10 |
11 |
12 | export const rootBlockDefinition = {
13 | key: 'image',
14 | label: 'Image',
15 | icon: 'image',
16 | };
17 |
18 |
19 | export const listBlockImageDefinition = {
20 | key: 'image',
21 | label: 'Image',
22 | icon: 'image',
23 | };
24 |
25 |
26 | export const listBlockDefinition = {
27 | key: 'carousel',
28 | label: 'Carousel',
29 | children: [
30 | listBlockImageDefinition,
31 | ],
32 | };
33 |
34 |
35 | export const structBlockHeightDefinition = {
36 | key: 'height',
37 | label: 'Height',
38 | };
39 |
40 |
41 | export const structBlockWidthDefinition = {
42 | key: 'width',
43 | label: 'Width',
44 | };
45 |
46 |
47 | export const structBlockImageDefinition = {
48 | key: 'image',
49 | label: 'Image',
50 | };
51 |
52 |
53 | export const structBlockImageCellDefinition = {
54 | key: 'image_cell',
55 | label: 'Image',
56 | icon: 'image',
57 | isStruct: true,
58 | children: [
59 | structBlockWidthDefinition,
60 | structBlockImageDefinition,
61 | ],
62 | };
63 |
64 |
65 | export const structBlockImagesDefinition = {
66 | key: 'images',
67 | label: 'Images',
68 | children: [
69 | structBlockImageCellDefinition,
70 | ],
71 | };
72 |
73 |
74 | export const structBlockPageDefinition = {
75 | key: 'page',
76 | label: 'Page',
77 | };
78 |
79 |
80 | export const structBlockRelatedPagesDefinition = {
81 | key: 'related_pages',
82 | label: 'Related pages',
83 | children: [
84 | structBlockPageDefinition,
85 | ]
86 | };
87 |
88 |
89 | export const structBlockDefinition = {
90 | key: 'image_row',
91 | label: 'Image row',
92 | isStruct: true,
93 | children: [
94 | structBlockHeightDefinition,
95 | structBlockImagesDefinition,
96 | structBlockRelatedPagesDefinition,
97 | ],
98 | };
99 |
100 |
101 | export const streamBlockImageDefinition = {
102 | key: 'image',
103 | label: 'Image',
104 | icon: 'image',
105 | };
106 |
107 |
108 | export const streamBlockTitleDefinition = {
109 | key: 'title',
110 | label: 'Title',
111 | };
112 |
113 |
114 | export const streamBlockDefinition = {
115 | key: 'rich_carousel',
116 | label: 'Rich carousel',
117 | children: [
118 | streamBlockImageDefinition,
119 | streamBlockTitleDefinition,
120 | ],
121 | };
122 |
123 |
124 | export const blockDefinitions = [
125 | rootBlockDefinition,
126 | listBlockDefinition,
127 | structBlockDefinition,
128 | streamBlockDefinition,
129 | ];
130 |
131 |
132 | //
133 | // Value
134 | //
135 |
136 |
137 | export const rootBlock1Id = uuidv4();
138 | export const rootBlock1 = {
139 | id: rootBlock1Id,
140 | type: 'image',
141 | value: 1154,
142 | };
143 | export const rootBlock2Id = uuidv4();
144 | export const rootBlock2 = {
145 | id: rootBlock2Id,
146 | type: 'image',
147 | value: 57,
148 | };
149 |
150 |
151 | export const listBlockImage1Id = uuidv4();
152 | export const listBlockImage1 = {
153 | id: listBlockImage1Id,
154 | type: 'image',
155 | value: 1154,
156 | };
157 |
158 |
159 | export const listBlockImage2Id = uuidv4();
160 | export const listBlockImage2 = {
161 | id: listBlockImage2Id,
162 | type: 'image',
163 | value: 57,
164 | };
165 |
166 |
167 | export const listBlockId = uuidv4();
168 | export const listBlock = {
169 | id: listBlockId,
170 | type: 'carousel',
171 | value: [listBlockImage1, listBlockImage2],
172 | };
173 |
174 |
175 | export const structBlockHeightId = uuidv4();
176 | export const structBlockHeight = {
177 | id: structBlockHeightId,
178 | type: 'height',
179 | value: 'short',
180 | };
181 |
182 |
183 | export const structBlockWidth1Id = uuidv4();
184 | export const structBlockWidth1 = {
185 | id: structBlockWidth1Id,
186 | type: 'width',
187 | value: 'col-md-4',
188 | };
189 |
190 |
191 | export const structBlockImage1Id = uuidv4();
192 | export const structBlockImage1 = {
193 | id: structBlockImage1Id,
194 | type: 'image',
195 | value: 257,
196 | };
197 |
198 |
199 | export const structBlockImageCell1Id = uuidv4();
200 | export const structBlockImageCell1 = {
201 | id: structBlockImageCell1Id,
202 | type: 'image_cell',
203 | value: [structBlockImage1, structBlockWidth1],
204 | };
205 |
206 |
207 | export const structBlockWidth2Id = uuidv4();
208 | export const structBlockWidth2 = {
209 | id: structBlockWidth2Id,
210 | type: 'width',
211 | value: 'col-md-8',
212 | };
213 |
214 |
215 | export const structBlockImage2Id = uuidv4();
216 | export const structBlockImage2 = {
217 | id: structBlockImage2Id,
218 | type: 'image',
219 | value: 319,
220 | };
221 |
222 |
223 | export const structBlockImageCell2Id = uuidv4();
224 | export const structBlockImageCell2 = {
225 | id: structBlockImageCell2Id,
226 | type: 'image_cell',
227 | value: [structBlockImage2, structBlockWidth2],
228 | };
229 |
230 |
231 | export const structBlockImageCellAllSortedIds = [
232 | structBlockImageCell1Id, structBlockImage1Id, structBlockWidth1Id,
233 | structBlockImageCell2Id, structBlockImage2Id, structBlockWidth2Id];
234 |
235 |
236 | export const structBlockImagesId = uuidv4();
237 | export const structBlockImages = {
238 | id: structBlockImagesId,
239 | type: 'images',
240 | value: [structBlockImageCell1, structBlockImageCell2],
241 | };
242 |
243 |
244 | export const structBlockPageId = uuidv4();
245 | export const structBlockPage = {
246 | id: structBlockPageId,
247 | type: 'page',
248 | value: 8,
249 | };
250 |
251 |
252 | export const structBlockRelatedPagesId = uuidv4();
253 | export const structBlockRelatedPages = {
254 | id: structBlockRelatedPagesId,
255 | type: 'related_pages',
256 | value: [structBlockPage]
257 | };
258 |
259 |
260 | export const structBlockId = uuidv4();
261 | export const structBlock = {
262 | id: structBlockId,
263 | type: 'image_row',
264 | value: [
265 | structBlockHeight,
266 | structBlockImages,
267 | structBlockRelatedPages,
268 | ],
269 | };
270 |
271 |
272 | export const streamBlockImageId = uuidv4();
273 | export const streamBlockImage = {
274 | id: streamBlockImageId,
275 | type: 'image',
276 | value: 121,
277 | };
278 |
279 |
280 | export const streamBlockTitleId = uuidv4();
281 | export const streamBlockTitle = {
282 | id: streamBlockTitleId,
283 | type: 'title',
284 | value: 'Το Πυθαγόρειο ήταν χορτοφάγος',
285 | };
286 |
287 |
288 | export const streamBlockId = uuidv4();
289 | export const streamBlock = {
290 | id: streamBlockId,
291 | type: 'rich_carousel',
292 | value: [
293 | streamBlockImage,
294 | streamBlockTitle,
295 | ],
296 | };
297 |
298 |
299 | //
300 | // State
301 | //
302 |
303 |
304 | export const initialState = {
305 | [fieldId]: {
306 | blockDefinitions: blockDefinitions,
307 | },
308 | };
309 |
310 |
311 | export const rootBlock1State = {
312 | parent: null,
313 | type: 'image',
314 | value: 1154,
315 | closed: true,
316 | hidden: false,
317 | shouldUpdate: false,
318 | };
319 |
320 |
321 | export const rootBlock2State = {
322 | parent: null,
323 | type: 'image',
324 | value: 57,
325 | closed: true,
326 | hidden: false,
327 | shouldUpdate: false,
328 | };
329 |
330 |
331 | export const listBlockState = {
332 | parent: null,
333 | type: 'carousel',
334 | value: [listBlockImage1Id, listBlockImage2Id],
335 | closed: true,
336 | hidden: false,
337 | shouldUpdate: false,
338 | };
339 |
340 |
341 | export const listBlockImage1State = {
342 | parent: listBlockId,
343 | type: 'image',
344 | value: 1154,
345 | closed: true,
346 | hidden: false,
347 | shouldUpdate: false,
348 | };
349 |
350 |
351 | export const listBlockImage2State = {
352 | parent: listBlockId,
353 | type: 'image',
354 | value: 57,
355 | closed: true,
356 | hidden: false,
357 | shouldUpdate: false,
358 | };
359 |
360 |
361 | export const structBlockState = {
362 | parent: null,
363 | type: 'image_row',
364 | value: [
365 | structBlockHeightId,
366 | structBlockImagesId,
367 | structBlockRelatedPagesId,
368 | ],
369 | closed: true,
370 | hidden: false,
371 | shouldUpdate: false,
372 | };
373 |
374 |
375 | export const structBlockHeightState = {
376 | parent: structBlockId,
377 | type: 'height',
378 | value: 'short',
379 | closed: true,
380 | hidden: false,
381 | shouldUpdate: false,
382 | };
383 |
384 |
385 | export const structBlockImagesState = {
386 | parent: structBlockId,
387 | type: 'images',
388 | value: [structBlockImageCell1Id, structBlockImageCell2Id],
389 | closed: true,
390 | hidden: false,
391 | shouldUpdate: false,
392 | };
393 |
394 |
395 | export const structBlockImage1State = {
396 | parent: structBlockImageCell1Id,
397 | type: 'image',
398 | value: 257,
399 | closed: true,
400 | hidden: false,
401 | shouldUpdate: false,
402 | };
403 |
404 |
405 | export const structBlockWidth1State = {
406 | parent: structBlockImageCell1Id,
407 | type: 'width',
408 | value: 'col-md-4',
409 | closed: true,
410 | hidden: false,
411 | shouldUpdate: false,
412 | };
413 |
414 |
415 | export const structBlockImageCell1State = {
416 | parent: structBlockImagesId,
417 | type: 'image_cell',
418 | value: [structBlockImage1Id, structBlockWidth1Id],
419 | closed: true,
420 | hidden: false,
421 | shouldUpdate: false,
422 | };
423 |
424 |
425 | export const structBlockImage2State = {
426 | parent: structBlockImageCell2Id,
427 | type: 'image',
428 | value: 319,
429 | closed: true,
430 | hidden: false,
431 | shouldUpdate: false,
432 | };
433 |
434 |
435 | export const structBlockWidth2State = {
436 | parent: structBlockImageCell2Id,
437 | type: 'width',
438 | value: 'col-md-8',
439 | closed: true,
440 | hidden: false,
441 | shouldUpdate: false,
442 | };
443 |
444 |
445 | export const structBlockImageCell2State = {
446 | parent: structBlockImagesId,
447 | type: 'image_cell',
448 | value: [structBlockImage2Id, structBlockWidth2Id],
449 | closed: true,
450 | hidden: false,
451 | shouldUpdate: false,
452 | };
453 |
454 |
455 | export const structBlockRelatedPagesState = {
456 | parent: structBlockId,
457 | type: 'related_pages',
458 | value: [structBlockPageId],
459 | closed: true,
460 | hidden: false,
461 | shouldUpdate: false,
462 | };
463 |
464 |
465 | export const structBlockPageState = {
466 | parent: structBlockRelatedPagesId,
467 | type: 'page',
468 | value: 8,
469 | closed: true,
470 | hidden: false,
471 | shouldUpdate: false,
472 | };
473 |
474 |
475 | export const streamBlockState = {
476 | parent: null,
477 | type: 'rich_carousel',
478 | value: [streamBlockImageId, streamBlockTitleId],
479 | closed: true,
480 | hidden: false,
481 | shouldUpdate: false,
482 | };
483 |
484 |
485 | export const streamBlockImage1State = {
486 | parent: streamBlockId,
487 | type: 'image',
488 | value: 121,
489 | closed: true,
490 | hidden: false,
491 | shouldUpdate: false,
492 | };
493 |
494 |
495 | export const streamBlockImage2State = {
496 | parent: streamBlockId,
497 | type: 'title',
498 | value: 'Το Πυθαγόρειο ήταν χορτοφάγος',
499 | closed: true,
500 | hidden: false,
501 | shouldUpdate: false,
502 | };
503 |
--------------------------------------------------------------------------------
/src/processing/utils.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import uuidv4 from 'uuid';
3 |
4 |
5 | export const isNA = value => {
6 | return (value === null) || (value === undefined)
7 | };
8 |
9 |
10 | export const getNewId = () => {
11 | return uuidv4();
12 | };
13 |
14 |
15 | export const getFieldName = blockId => {
16 | return `field-${blockId}`;
17 | };
18 |
19 |
20 | export const isField = blockDefinition => {
21 | return (blockDefinition.children === undefined)
22 | || (blockDefinition.children.length === 0);
23 | };
24 |
25 |
26 | export const isStruct = blockDefinition => {
27 | return (blockDefinition.isStruct !== undefined)
28 | && blockDefinition.isStruct;
29 | };
30 |
31 |
32 | export const isStatic = blockDefinition => {
33 | return (blockDefinition.isStatic !== undefined)
34 | && blockDefinition.isStatic;
35 | };
36 |
37 |
38 | export const isClosed = blockDefinition => {
39 | return ((blockDefinition.closed === undefined)
40 | || blockDefinition.closed);
41 | };
42 |
43 |
44 | export const shouldRunInnerScripts = blockDefinition => {
45 | return (blockDefinition.dangerouslyRunInnerScripts !== undefined)
46 | && (blockDefinition.dangerouslyRunInnerScripts);
47 | };
48 |
49 |
50 | export const getLabel = blockDefinition => {
51 | let {key, label} = blockDefinition;
52 | if (label === undefined) {
53 | label = key.replace('_', ' ');
54 | label = `${label[0].toUpperCase()}${label.substring(1)}`;
55 | }
56 | return label;
57 | };
58 |
59 |
60 | export const getChildrenIds = (state, fieldId, parentId) => {
61 | const fieldData = state[fieldId];
62 | const blocks = fieldData.blocks;
63 | if (parentId === null) {
64 | return fieldData.rootBlocks;
65 | } else {
66 | return blocks[parentId].value;
67 | }
68 | };
69 |
70 |
71 | export const getSiblingsIds = (state, fieldId, blockId) => {
72 | const fieldData = state[fieldId];
73 | const blocks = fieldData.blocks;
74 | const block = blocks[blockId];
75 | const parentId = block.parent;
76 | if (parentId !== null) {
77 | const parentBlockDefinition = getNestedBlockDefinition(state, fieldId,
78 | parentId);
79 | if (isStruct(parentBlockDefinition)) {
80 | return [blockId];
81 | }
82 | }
83 | return getChildrenIds(state, fieldId, parentId);
84 | };
85 |
86 |
87 | export const getAncestorsIds = (state, fieldId, blockId,
88 | includeSelf=false) => {
89 | const blocks = state[fieldId].blocks;
90 | const ancestors = [];
91 | if (includeSelf) {
92 | ancestors.push(blockId);
93 | }
94 |
95 | let block = blocks[blockId];
96 | while (block.parent !== null) {
97 | blockId = block.parent;
98 | ancestors.push(blockId);
99 | block = blocks[blockId];
100 | }
101 | return ancestors.reverse();
102 | };
103 |
104 |
105 | export const getDescendantsIds = (state, fieldId, blockId,
106 | includeSelf=false) => {
107 | let descendants = [];
108 | if (includeSelf) {
109 | descendants.push(blockId);
110 | }
111 |
112 | const blockDefinition = getNestedBlockDefinition(state, fieldId, blockId);
113 |
114 | if (isField(blockDefinition)) {
115 | return descendants;
116 | }
117 |
118 | for (let childBlockId of state[fieldId].blocks[blockId].value) {
119 | descendants = [
120 | ...descendants,
121 | ...getDescendantsIds(state, fieldId, childBlockId, true),
122 | ];
123 | }
124 | return descendants;
125 | };
126 |
127 |
128 | export const getBlockDefinition = (blockDefinitions, type) => {
129 | const blockDefinition = blockDefinitions.find(
130 | blockDefinition => blockDefinition.key === type);
131 | if (blockDefinition === undefined) {
132 | throw new TypeError(`No block definition found for '${type}'`);
133 | }
134 | return blockDefinition;
135 | };
136 |
137 |
138 | export const getNestedBlockDefinition = (state, fieldId, blockId) => {
139 | const fieldData = state[fieldId];
140 | let {blockDefinitions, blocks} = fieldData;
141 | let blockDefinition;
142 | for (let ancestorId of getAncestorsIds(state, fieldId, blockId, true)) {
143 | const block = blocks[ancestorId];
144 | blockDefinition = getBlockDefinition(blockDefinitions, block.type);
145 | blockDefinitions = blockDefinition.children;
146 | }
147 | return blockDefinition;
148 | };
149 |
150 |
151 | export const structValueToObject = (state, fieldId, structValue) => {
152 | const blocks = state[fieldId].blocks;
153 | const obj = {};
154 | for (let childBlockId of structValue) {
155 | const childBlockDefinition = getNestedBlockDefinition(state, fieldId,
156 | childBlockId);
157 | let value;
158 | if (isField(childBlockDefinition)) {
159 | const childBlock = blocks[childBlockId];
160 | value = childBlock.value;
161 | } else {
162 | value = childBlockId;
163 | }
164 | obj[childBlockDefinition.key] = value;
165 | }
166 | return obj;
167 | };
168 |
169 |
170 | export const getNewBlock = (parentId, blockDefinition, value=null) => {
171 | let extraBlocks = {};
172 | let childBlockId, childBlock, childExtraBlocks;
173 | const blockId = getNewId();
174 |
175 | if (isNA(value) && (blockDefinition.default !== undefined)) {
176 | value = blockDefinition.default;
177 | }
178 |
179 | if (isStruct(blockDefinition)) {
180 | const newValue = [];
181 | for (let childBlockDefinition of blockDefinition.children) {
182 | let childDefaultValue = null;
183 | if (!isNA(value)) {
184 | for (let childDefault of value) {
185 | if (childDefault.type === childBlockDefinition.key) {
186 | childDefaultValue = childDefault.value;
187 | break;
188 | }
189 | }
190 | }
191 | [childBlockId, childBlock, childExtraBlocks] =
192 | getNewBlock(blockId, childBlockDefinition, childDefaultValue);
193 | newValue.push(childBlockId);
194 | extraBlocks = {
195 | ...extraBlocks,
196 | ...childExtraBlocks,
197 | [childBlockId]: childBlock,
198 | };
199 | }
200 | value = newValue;
201 | } else if (!isField(blockDefinition)) {
202 | const newValue = [];
203 | if (!isNA(value)) {
204 | for (let childBlock of value) {
205 | let childBlockDefinition;
206 | for (childBlockDefinition of blockDefinition.children) {
207 | if (childBlockDefinition.key === childBlock.type) {
208 | break;
209 | }
210 | }
211 | [childBlockId, childBlock, childExtraBlocks] =
212 | getNewBlock(blockId, childBlockDefinition, childBlock.value);
213 | newValue.push(childBlockId);
214 | extraBlocks = {
215 | ...extraBlocks,
216 | ...childExtraBlocks,
217 | [childBlockId]: childBlock,
218 | };
219 | }
220 | }
221 | value = newValue;
222 | }
223 | return [
224 | blockId,
225 | {
226 | parent: parentId,
227 | type: blockDefinition.key,
228 | value: value,
229 | hidden: true,
230 | closed: false,
231 | shouldUpdate: false,
232 | },
233 | extraBlocks,
234 | ];
235 | };
236 |
237 |
238 | export const deepCopy = data => {
239 | let copy;
240 | if (data instanceof FileList) {
241 | return data;
242 | }
243 | if (data instanceof Array) {
244 | return data.map(v => deepCopy(v));
245 | }
246 | if (data instanceof Object) {
247 | copy = {};
248 | for (const [key, value] of Object.entries(data)) {
249 | copy[key] = deepCopy(value);
250 | }
251 | return copy;
252 | }
253 | return data;
254 | };
255 |
256 |
257 | export const applyToBlocks = (state, fieldId, blocksIds, func) => {
258 | const fieldData = state[fieldId];
259 | const blocks = fieldData.blocks;
260 | for (let blockId of blocksIds) {
261 | const block = deepCopy(blocks[blockId]);
262 | blocks[blockId] = func(block);
263 | }
264 | return {
265 | ...state,
266 | [fieldId]: {
267 | ...fieldData,
268 | blocks: blocks,
269 | },
270 | };
271 | };
272 |
273 |
274 | export const applyToBlock = (state, fieldId, blockId, func) => {
275 | return applyToBlocks(state, fieldId, [blockId], func);
276 | };
277 |
278 |
279 | export const triggerKeyboardEvent = (element, key) => {
280 | let event = new Event('keydown', {bubbles: true, cancelable: true});
281 |
282 | event.key = key; // These four lines
283 | event.keyIdentifier = key; // are here
284 | event.keyCode = key; // to fix cross-browser
285 | event.which = key; // compatibility issues.
286 |
287 | element.dispatchEvent(event);
288 | };
289 |
290 |
291 | export const triggerCustomEvent = (element, name, data=null) => {
292 | if (data === null) {
293 | data = {};
294 | }
295 | const event = new CustomEvent(`streamfield:${name}`, {
296 | detail: {
297 | target: element,
298 | ...data,
299 | },
300 | });
301 | element.dispatchEvent(event);
302 | window.dispatchEvent(event);
303 | };
304 |
305 |
306 | export const replaceWithComponent = (string, placeholder, component) => {
307 | const parts = string.split(new RegExp(`(${placeholder})`));
308 | for (const i in parts) {
309 | let part = parts[i];
310 | if (part === placeholder) {
311 | parts[i] = {component}
312 | } else {
313 | parts[i] =
314 | }
315 | }
316 | return <>{parts}>;
317 | };
318 |
--------------------------------------------------------------------------------
/src/processing/utils.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | getAncestorsIds,
3 | getDescendantsIds,
4 | getNestedBlockDefinition
5 | } from './utils';
6 | import {valueToState} from './conversions';
7 | import {
8 | initialState,
9 | fieldId,
10 | rootBlock1,
11 | rootBlock2,
12 | listBlock,
13 | structBlock,
14 | streamBlock,
15 | rootBlockDefinition,
16 | listBlockDefinition,
17 | listBlockImageDefinition,
18 | structBlockDefinition,
19 | structBlockImagesDefinition,
20 | structBlockImageCellDefinition,
21 | streamBlockDefinition,
22 | streamBlockImageDefinition,
23 | streamBlockTitleDefinition,
24 | rootBlock1Id,
25 | rootBlock2Id,
26 | listBlockId,
27 | listBlockImage1Id,
28 | listBlockImage2Id,
29 | structBlockId,
30 | structBlockHeightId,
31 | structBlockImagesId,
32 | structBlockImageCellAllSortedIds,
33 | structBlockImageCell1Id,
34 | structBlockImage1Id,
35 | structBlockWidth1Id,
36 | structBlockImageCell2Id,
37 | structBlockImage2Id,
38 | structBlockWidth2Id,
39 | structBlockRelatedPagesId,
40 | structBlockPageId,
41 | streamBlockId,
42 | streamBlockImageId,
43 | streamBlockTitleId,
44 | } from './samples';
45 |
46 |
47 | describe('getAncestorsId', () => {
48 | const state = valueToState(initialState, fieldId, [
49 | rootBlock1, rootBlock2, listBlock, structBlock, streamBlock,
50 | ]);
51 |
52 | test('Root blocks', () => {
53 | expect(getAncestorsIds(state, fieldId, rootBlock1Id))
54 | .toEqual([]);
55 | expect(getAncestorsIds(state, fieldId, rootBlock1Id, true))
56 | .toEqual([rootBlock1Id,]);
57 | expect(getAncestorsIds(state, fieldId, rootBlock2Id))
58 | .toEqual([]);
59 | expect(getAncestorsIds(state, fieldId, rootBlock2Id, true))
60 | .toEqual([rootBlock2Id]);
61 | });
62 |
63 | test('ListBlock', () => {
64 | expect(getAncestorsIds(state, fieldId, listBlockId))
65 | .toEqual([]);
66 | expect(getAncestorsIds(state, fieldId, listBlockId, true))
67 | .toEqual([listBlockId]);
68 | expect(getAncestorsIds(state, fieldId, listBlockImage1Id))
69 | .toEqual([listBlockId]);
70 | expect(getAncestorsIds(state, fieldId, listBlockImage1Id, true))
71 | .toEqual([listBlockId, listBlockImage1Id]);
72 | expect(getAncestorsIds(state, fieldId, listBlockImage2Id))
73 | .toEqual([listBlockId]);
74 | expect(getAncestorsIds(state, fieldId, listBlockImage2Id, true))
75 | .toEqual([listBlockId, listBlockImage2Id]);
76 | });
77 |
78 | test('StructBlock', () => {
79 | expect(getAncestorsIds(state, fieldId, structBlockId))
80 | .toEqual([]);
81 | expect(getAncestorsIds(state, fieldId, structBlockId, true))
82 | .toEqual([structBlockId]);
83 | expect(getAncestorsIds(state, fieldId, structBlockImagesId))
84 | .toEqual([structBlockId]);
85 | expect(getAncestorsIds(state, fieldId, structBlockImagesId, true))
86 | .toEqual([structBlockId, structBlockImagesId]);
87 | expect(getAncestorsIds(state, fieldId, structBlockImageCell1Id))
88 | .toEqual([structBlockId, structBlockImagesId]);
89 | expect(getAncestorsIds(state, fieldId, structBlockImageCell1Id, true))
90 | .toEqual([structBlockId, structBlockImagesId, structBlockImageCell1Id]);
91 | expect(getAncestorsIds(state, fieldId, structBlockImageCell2Id))
92 | .toEqual([structBlockId, structBlockImagesId]);
93 | expect(getAncestorsIds(state, fieldId, structBlockImageCell2Id, true))
94 | .toEqual([structBlockId, structBlockImagesId, structBlockImageCell2Id]);
95 | });
96 |
97 | test('StreamBlock', () => {
98 | expect(getAncestorsIds(state, fieldId, streamBlockId))
99 | .toEqual([]);
100 | expect(getAncestorsIds(state, fieldId, streamBlockId, true))
101 | .toEqual([streamBlockId]);
102 | expect(getAncestorsIds(state, fieldId, streamBlockImageId))
103 | .toEqual([streamBlockId]);
104 | expect(getAncestorsIds(state, fieldId, streamBlockImageId, true))
105 | .toEqual([streamBlockId, streamBlockImageId]);
106 | expect(getAncestorsIds(state, fieldId, streamBlockTitleId))
107 | .toEqual([streamBlockId]);
108 | expect(getAncestorsIds(state, fieldId, streamBlockTitleId, true))
109 | .toEqual([streamBlockId, streamBlockTitleId]);
110 | });
111 | });
112 |
113 |
114 | describe('getDescendantsIds', () => {
115 | const state = valueToState(initialState, fieldId, [
116 | rootBlock1, rootBlock2, listBlock, structBlock, streamBlock,
117 | ]);
118 |
119 | test('Root blocks', () => {
120 | expect(getDescendantsIds(state, fieldId, rootBlock1Id))
121 | .toEqual([]);
122 | expect(getDescendantsIds(state, fieldId, rootBlock1Id, true))
123 | .toEqual([rootBlock1Id]);
124 | expect(getDescendantsIds(state, fieldId, rootBlock2Id))
125 | .toEqual([]);
126 | expect(getDescendantsIds(state, fieldId, rootBlock2Id, true))
127 | .toEqual([rootBlock2Id]);
128 | });
129 |
130 | test('ListBlock', () => {
131 | expect(getDescendantsIds(state, fieldId, listBlockId))
132 | .toEqual([listBlockImage1Id, listBlockImage2Id]);
133 | expect(getDescendantsIds(state, fieldId, listBlockId, true))
134 | .toEqual([listBlockId, listBlockImage1Id, listBlockImage2Id]);
135 | expect(getDescendantsIds(state, fieldId, listBlockImage1Id))
136 | .toEqual([]);
137 | expect(getDescendantsIds(state, fieldId, listBlockImage1Id, true))
138 | .toEqual([listBlockImage1Id]);
139 | expect(getDescendantsIds(state, fieldId, listBlockImage2Id))
140 | .toEqual([]);
141 | expect(getDescendantsIds(state, fieldId, listBlockImage2Id, true))
142 | .toEqual([listBlockImage2Id]);
143 | });
144 |
145 | test('StructBlock', () => {
146 | expect(getDescendantsIds(state, fieldId, structBlockId))
147 | .toEqual([structBlockHeightId, structBlockImagesId,
148 | ...structBlockImageCellAllSortedIds,
149 | structBlockRelatedPagesId, structBlockPageId]);
150 | expect(getDescendantsIds(state, fieldId, structBlockId, true))
151 | .toEqual([structBlockId,
152 | structBlockHeightId, structBlockImagesId,
153 | ...structBlockImageCellAllSortedIds,
154 | structBlockRelatedPagesId, structBlockPageId]);
155 | expect(getDescendantsIds(state, fieldId, structBlockImagesId))
156 | .toEqual([...structBlockImageCellAllSortedIds]);
157 | expect(getDescendantsIds(state, fieldId, structBlockImagesId, true))
158 | .toEqual([structBlockImagesId, ...structBlockImageCellAllSortedIds]);
159 | expect(getDescendantsIds(state, fieldId, structBlockImageCell1Id))
160 | .toEqual([structBlockImage1Id, structBlockWidth1Id]);
161 | expect(getDescendantsIds(state, fieldId, structBlockImageCell1Id, true))
162 | .toEqual([structBlockImageCell1Id,
163 | structBlockImage1Id, structBlockWidth1Id]);
164 | expect(getDescendantsIds(state, fieldId, structBlockImageCell2Id))
165 | .toEqual([structBlockImage2Id, structBlockWidth2Id]);
166 | expect(getDescendantsIds(state, fieldId, structBlockImageCell2Id, true))
167 | .toEqual([structBlockImageCell2Id,
168 | structBlockImage2Id, structBlockWidth2Id]);
169 | expect(getDescendantsIds(state, fieldId, structBlockRelatedPagesId))
170 | .toEqual([structBlockPageId]);
171 | expect(getDescendantsIds(state, fieldId, structBlockRelatedPagesId, true))
172 | .toEqual([structBlockRelatedPagesId, structBlockPageId]);
173 | expect(getDescendantsIds(state, fieldId, structBlockPageId))
174 | .toEqual([]);
175 | expect(getDescendantsIds(state, fieldId, structBlockPageId, true))
176 | .toEqual([structBlockPageId]);
177 | });
178 |
179 | test('StreamBlock', () => {
180 | expect(getDescendantsIds(state, fieldId, streamBlockId))
181 | .toEqual([streamBlockImageId, streamBlockTitleId]);
182 | expect(getDescendantsIds(state, fieldId, streamBlockId, true))
183 | .toEqual([streamBlockId, streamBlockImageId, streamBlockTitleId]);
184 | expect(getDescendantsIds(state, fieldId, streamBlockImageId))
185 | .toEqual([]);
186 | expect(getDescendantsIds(state, fieldId, streamBlockImageId, true))
187 | .toEqual([streamBlockImageId]);
188 | expect(getDescendantsIds(state, fieldId, streamBlockTitleId))
189 | .toEqual([]);
190 | expect(getDescendantsIds(state, fieldId, streamBlockTitleId, true))
191 | .toEqual([streamBlockTitleId]);
192 | });
193 | });
194 |
195 |
196 | describe('getNestedBlockDefinition', () => {
197 | const state = valueToState(initialState, fieldId, [
198 | rootBlock1, rootBlock2, listBlock, structBlock, streamBlock,
199 | ]);
200 |
201 | test('Root blocks', () => {
202 | expect(getNestedBlockDefinition(state, fieldId, rootBlock1Id))
203 | .toEqual(rootBlockDefinition);
204 | expect(getNestedBlockDefinition(state, fieldId, rootBlock2Id))
205 | .toEqual(rootBlockDefinition);
206 | });
207 |
208 | test('ListBlock', () => {
209 | expect(getNestedBlockDefinition(state, fieldId, listBlockId))
210 | .toEqual(listBlockDefinition);
211 | expect(getNestedBlockDefinition(state, fieldId, listBlockImage1Id))
212 | .toEqual(listBlockImageDefinition);
213 | expect(getNestedBlockDefinition(state, fieldId, listBlockImage2Id))
214 | .toEqual(listBlockImageDefinition);
215 | });
216 |
217 | test('StructBlock', () => {
218 | expect(getNestedBlockDefinition(state, fieldId, structBlockId))
219 | .toEqual(structBlockDefinition);
220 | expect(getNestedBlockDefinition(state, fieldId, structBlockImagesId))
221 | .toEqual(structBlockImagesDefinition);
222 | expect(getNestedBlockDefinition(state, fieldId, structBlockImageCell1Id))
223 | .toEqual(structBlockImageCellDefinition);
224 | expect(getNestedBlockDefinition(state, fieldId, structBlockImageCell2Id))
225 | .toEqual(structBlockImageCellDefinition);
226 | });
227 |
228 | test('StreamBlock', () => {
229 | expect(getNestedBlockDefinition(state, fieldId, streamBlockId))
230 | .toEqual(streamBlockDefinition);
231 | expect(getNestedBlockDefinition(state, fieldId, streamBlockImageId))
232 | .toEqual(streamBlockImageDefinition);
233 | expect(getNestedBlockDefinition(state, fieldId, streamBlockTitleId))
234 | .toEqual(streamBlockTitleDefinition);
235 | });
236 | });
237 |
--------------------------------------------------------------------------------
/src/reducer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | addBlock,
4 | deleteBlock,
5 | duplicateBlock,
6 | moveBlock
7 | } from './processing/reducers';
8 | import {valueToState} from './processing/conversions';
9 | import {
10 | applyToBlock,
11 | deepCopy,
12 | getNestedBlockDefinition, isStruct
13 | } from './processing/utils';
14 |
15 |
16 | const initialState = {};
17 |
18 |
19 | export default (state=initialState, action) => {
20 | switch (action.type) {
21 | case 'INITIALIZE_STREAM_FIELD': {
22 | const data = deepCopy(action.data);
23 | const {
24 | required, minNum, maxNum, icons, labels, gutteredAdd,
25 | blockDefinitions, isMobile, value,
26 | } = data;
27 | state = {
28 | ...state,
29 | [action.id]: {
30 | required, minNum, maxNum, icons, labels, gutteredAdd,
31 | blockDefinitions, isMobile,
32 | },
33 | };
34 | return valueToState(state, action.id, value);
35 | }
36 | case 'SET_IS_MOBILE': {
37 | return {
38 | ...state,
39 | [action.id]: {
40 | ...state[action.id],
41 | isMobile: action.isMobile,
42 | }
43 | };
44 | }
45 | case 'BLOCK_UPDATED': {
46 | const {fieldId, blockId} = action;
47 | return applyToBlock(state, fieldId, blockId, block => ({
48 | ...block,
49 | shouldUpdate: false,
50 | }));
51 | }
52 | case 'CHANGE_BLOCK_VALUES': {
53 | const {fieldId, blockId, value} = action;
54 | state = applyToBlock(state, fieldId, blockId, block => ({
55 | ...block,
56 | value: value,
57 | shouldUpdate: true,
58 | }));
59 | const blocks = state[fieldId].blocks;
60 | const block = blocks[blockId];
61 | const parentBlockId = block.parent;
62 | if (parentBlockId !== null) {
63 | const parentBlockDefinition = getNestedBlockDefinition(state, fieldId,
64 | parentBlockId);
65 | if (isStruct(parentBlockDefinition)) {
66 | state = applyToBlock(state, fieldId, parentBlockId, block => ({
67 | ...block,
68 | shouldUpdate: true,
69 | }));
70 | }
71 | }
72 | return state;
73 | }
74 | case 'TOGGLE_BLOCK': {
75 | const {fieldId, blockId} = action;
76 | return applyToBlock(state, fieldId, blockId, block => ({
77 | ...block,
78 | closed: block.closed === undefined ? false : !block.closed,
79 | shouldUpdate: true,
80 | }));
81 | }
82 | case 'SHOW_BLOCK': {
83 | const {fieldId, blockId} = action;
84 | return applyToBlock(state, fieldId, blockId, block => ({
85 | ...block,
86 | hidden: false,
87 | shouldUpdate: true,
88 | }));
89 | }
90 | case 'HIDE_BLOCK': {
91 | const {fieldId, blockId} = action;
92 | return applyToBlock(state, fieldId, blockId, block => ({
93 | ...block,
94 | hidden: true,
95 | shouldUpdate: true,
96 | }));
97 | }
98 | case 'ADD_BLOCK': {
99 | const {fieldId, parentId, index, blockType} = action;
100 | return addBlock(state, fieldId, parentId, index, blockType);
101 | }
102 | case 'DUPLICATE_BLOCK': {
103 | const {fieldId, blockId} = action;
104 | return duplicateBlock(state, fieldId, blockId);
105 | }
106 | case 'MOVE_BLOCK': {
107 | const {fieldId, blockId, newIndex} = action;
108 | return moveBlock(state, fieldId, blockId, newIndex);
109 | }
110 | case 'DELETE_BLOCK': {
111 | return deleteBlock(state, action.fieldId, action.blockId);
112 | }
113 | default: {
114 | return state;
115 | }
116 | }
117 | };
118 |
--------------------------------------------------------------------------------
/src/scss/_variables.scss:
--------------------------------------------------------------------------------
1 | $grid-gutter-width: 30px !default;
2 | $header-padding-horizontal: 4px !default;
3 | $header-padding-vertical: 4px !default;
4 | $header-gutter: 8px !default;
5 | $block-margin-vertical: 4px !default;
6 | $block-margin-horizontal: 0 !default;
7 | $add-button-size: 34px !default;
8 | $add-button-font-size: 24px !default;
9 | $type-button-padding-vertical: 10px !default;
10 | $type-button-padding-horizontal: 10px !default;
11 | $children-container-padding: $add-button-size / 2 !default;
12 | $content-padding-horizontal: 24px !default;
13 | $content-padding-vertical: 16px !default;
14 | $action-font-size: 15px !default;
15 | $add-transition-duration: 0.3s !default;
16 | $hover-transition-duration: 0.3s !default;
17 | $bounce-transition-timing: cubic-bezier(0.175, 0.885, 0.32, 1.275) !default;
18 | $border-radius: 3px !default;
19 | $teal: #007d7e !default;
20 | $header-text-color-focus: #4d4d4d !default;
21 | $header-text-color: #585858 !default;
22 | $block-border-color: #e6e6e6 !default;
23 | $block-border-color-focus: #bbbbbb !default;
24 | $block-hover-background: #f5f5f5 !default;
25 | $error-color: #cd3238 !default;
26 | $error-border-color: #dbc7c8 !default;
27 | $error-border-color-focus: #cdb2b3 !default;
28 | $error-background-color: #fbefef !default;
29 | $screen-xs-max: 799px !default;
30 | $screen-sm-min: 800px !default;
31 | $screen-l-min: 1075px !default;
32 | $add-panel-gutter: 8px !default;
33 |
--------------------------------------------------------------------------------
/src/scss/components/c-sf-add-button.scss:
--------------------------------------------------------------------------------
1 | .c-sf-add-button {
2 | width: 100%;
3 | height: $add-button-size;
4 | appearance: none;
5 | border: 0 none;
6 | color: $teal;
7 | font-weight: bold;
8 | background: none;
9 | padding: 0;
10 | cursor: pointer;
11 | outline: none;
12 | backface-visibility: hidden;
13 | overflow: hidden; // Makes the rotated i box not clickable.
14 | user-select: none;
15 | opacity: 0;
16 | pointer-events: none;
17 | transition: opacity 100ms ease-in-out;
18 |
19 | &:hover {
20 | opacity: 1;
21 | }
22 |
23 | i {
24 | display: block;
25 | transition: transform $add-transition-duration $bounce-transition-timing;
26 | font-style: normal;
27 | font-size: $add-button-font-size;
28 | line-height: $add-button-size;
29 | }
30 |
31 | &--visible {
32 | opacity: 0.8;
33 | pointer-events: unset;
34 | }
35 |
36 | &--close i {
37 | transform: rotate(45deg);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/scss/components/c-sf-add-panel.scss:
--------------------------------------------------------------------------------
1 | .c-sf-add-panel {
2 | position: relative;
3 | padding: $grid-gutter-width / 4
4 | 0
5 | $grid-gutter-width;
6 | border-radius: $border-radius;
7 | user-select: none;
8 |
9 | @media (min-width: $screen-l-min) {
10 | padding: $grid-gutter-width / 4
11 | $grid-gutter-width * 2
12 | $add-button-size - $grid-gutter-width / 2;
13 | }
14 |
15 | &__group-title {
16 | margin: 5px 0;
17 | font-weight: 600;
18 | }
19 |
20 | &__grid {
21 | display: flex;
22 | flex-flow: row wrap;
23 | margin-left: -$add-panel-gutter;
24 | margin-right: -$add-panel-gutter;
25 | margin-bottom: $grid-gutter-width / 2;
26 | &:last-child {
27 | margin-bottom: 0;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/scss/components/c-sf-block.scss:
--------------------------------------------------------------------------------
1 | // TODO: Reduce nesting further by splitting out more components.
2 | //
3 | // There's quite a lot of nesting in here which makes parsing some segments difficult.
4 | // Some of this is to deal with the fact that .c-sf-block can contain many .c-sf-block's and so is
5 | // legitimate. A lot of these would ideally be their own components (eg the actions) however there
6 | // is a lot of interdependency of the elements which makes this hard
7 | // without fairly intensive rethinking of the HTML.
8 | //
9 | // However, the new classes adequately sanitise streamfield only CSS so am leaving this for
10 | // now to avoid blocking the release of the new Streamfield. -@jonnyscholes
11 |
12 | .c-sf-block {
13 | flex: 1 1 auto;
14 | margin: $block-margin-vertical $block-margin-horizontal;
15 | border: 1px solid $block-border-color;
16 | border-radius: $border-radius;
17 | background: white;
18 | transition: border-color $hover-transition-duration ease-in-out;
19 | transition-property: border-color, box-shadow;
20 |
21 | &__header {
22 | display: flex;
23 | justify-content: space-between;
24 | align-items: center;
25 | padding: $header-padding-vertical $header-padding-horizontal;
26 | user-select: none;
27 | transition: background-color $hover-transition-duration ease-in-out;
28 | cursor: default;
29 | border-top-left-radius: $border-radius;
30 | border-top-right-radius: $border-radius;
31 | min-height: 30px;
32 |
33 | @media (min-width: $screen-sm-min) {
34 | padding-left: $content-padding-horizontal - $header-gutter;
35 | }
36 |
37 | &--collapsible {
38 | cursor: pointer;
39 | }
40 |
41 | &--sortable {
42 | cursor: grab;
43 | }
44 |
45 | &__title, &__icon {
46 | color: $header-text-color;
47 | }
48 |
49 | &__title {
50 | display: inline-block;
51 | flex: 1 10 auto;
52 | margin: 0;
53 | font-size: 12px;
54 | white-space: nowrap;
55 | text-overflow: ellipsis;
56 | overflow: hidden;
57 | }
58 |
59 | &__icon {
60 | margin: 0 $header-gutter;
61 | font-size: 20px;
62 | transition: color $hover-transition-duration ease-in-out;
63 | }
64 | }
65 |
66 | &__content {
67 | &-inner {
68 | padding: $content-padding-vertical $content-padding-horizontal;
69 | }
70 | }
71 |
72 | &__actions {
73 | flex: 0 1 auto;
74 | display: flex;
75 | align-items: center;
76 | white-space: nowrap;
77 | overflow-x: hidden;
78 |
79 | &__single {
80 | appearance: none;
81 | border: 0 none;
82 | background: none;
83 | cursor: pointer;
84 | color: $header-text-color;
85 | opacity: 1;
86 | transition: opacity $hover-transition-duration ease-in-out, background-color $hover-transition-duration ease-in-out;
87 | transition-property: opacity, color, background-color;
88 | font-size: $action-font-size;
89 | border-radius: 50%;
90 | width: 30px;
91 | height: 30px;
92 | text-align: center;
93 | padding: 0 8px;
94 |
95 | &:not(:last-of-type) {
96 | margin-right: 3px;
97 | }
98 |
99 | &:focus,
100 | &:hover {
101 | background-color: rgba(0, 0, 0, 0.05);
102 |
103 | i {
104 | color: #333;
105 | }
106 | }
107 |
108 | i {
109 | font-style: normal;
110 | transition: color $hover-transition-duration ease-in-out;
111 |
112 | &:before {
113 | vertical-align: top;
114 | margin: 0 0 0 -2px;
115 | }
116 | }
117 |
118 | &[disabled] {
119 | opacity: 0.2;
120 | pointer-events: none;
121 | }
122 | }
123 | }
124 |
125 | &__type {
126 | margin: 0 $header-gutter;
127 | text-align: right;
128 | font-size: 12px;
129 | color: $header-text-color;
130 | user-select: none;
131 | vertical-align: 2px;
132 | overflow-x: hidden;
133 | text-overflow: ellipsis;
134 | }
135 |
136 | &.c-sf-block--error {
137 | border-color: $error-border-color;
138 |
139 | > .c-sf-block__header {
140 | background: $error-background-color;
141 | }
142 |
143 | &:hover,
144 | &:focus {
145 | border-color: $error-border-color-focus;
146 |
147 | > .c-sf-block__header {
148 | background: $error-background-color;
149 | }
150 | }
151 |
152 | // Duplicated because of
153 | // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/16651302/
154 | &:focus-within {
155 | border-color: $error-border-color-focus;
156 |
157 | > .c-sf-block__header {
158 | background: $error-background-color;
159 | }
160 | }
161 | }
162 |
163 | &:hover,
164 | &:focus {
165 | border-color: $block-border-color-focus;
166 | box-shadow: 3px 2px 3px -1px rgba(0, 0, 0, 0.1);
167 |
168 | > .c-sf-block__header {
169 | background: $block-hover-background;
170 |
171 | .c-sf-block__header__title,
172 | .c-sf-block__actions__single {
173 | color: $header-text-color-focus;
174 | }
175 | }
176 | }
177 |
178 | // Duplicated because of
179 | // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/16651302/
180 | &:focus-within {
181 | border-color: $block-border-color-focus;
182 | box-shadow: 3px 2px 3px -1px rgba(0, 0, 0, 0.1);
183 |
184 | > .c-sf-block__header {
185 | background: $block-hover-background;
186 |
187 | .c-sf-block__header__title,
188 | .c-sf-block__actions__single {
189 | color: $header-text-color-focus;
190 | }
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/src/scss/components/c-sf-button.scss:
--------------------------------------------------------------------------------
1 | .c-sf-button {
2 | $root: &;
3 | display: flex;
4 | flex: 1 1 200px;
5 | margin: $add-panel-gutter;
6 | appearance: none;
7 | background: #eee;
8 | padding: $type-button-padding-vertical $type-button-padding-horizontal;
9 | border: 0 none;
10 | border-radius: $border-radius;
11 | outline: none;
12 | cursor: pointer;
13 | overflow: hidden;
14 | transition: background-color $hover-transition-duration ease-in-out;
15 |
16 | &:hover,
17 | &:focus {
18 | background-color: $teal;
19 |
20 | #{$root}__icon,
21 | #{$root}__label {
22 | color: white;
23 | }
24 | }
25 |
26 | // Fallback for no `grid` support in IE11
27 | @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
28 | display: inline-block;
29 | margin: 0 10px 10px 0;
30 | }
31 |
32 | &__icon,
33 | &__label {
34 | display: inline-block;
35 |
36 | // Fixes for label elements getting global `label` styles from Wagtail -@jonnyscholes
37 | color: #333;
38 | font-weight: 600;
39 | font-size: 12px;
40 | font-family: 'Open Sans', sans-serif;
41 | transition: color $hover-transition-duration ease-in-out;
42 | }
43 |
44 | &__icon {
45 | font-size: 16px;
46 | padding-right: $type-button-padding-horizontal;
47 |
48 | .icon {
49 | &::before {
50 | vertical-align: initial;
51 | }
52 | }
53 | }
54 |
55 | &__label {
56 | padding-top: 2px;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/scss/components/c-sf-container.scss:
--------------------------------------------------------------------------------
1 | .c-sf-container {
2 | position: relative;
3 | display: flex;
4 | flex-flow: column nowrap;
5 | padding: $children-container-padding 0;
6 | transition: padding $hover-transition-duration ease-in-out;
7 |
8 | &__block-container {
9 | position: relative;
10 | display: flex;
11 | flex-flow: column nowrap;
12 | }
13 |
14 | &--add-in-gutter {
15 | @media (min-width: $screen-sm-min) {
16 | padding-left: $add-button-size;
17 |
18 | .c-sf-add-button {
19 | width: $add-button-size;
20 | height: 0;
21 | transform: translate(-100%, -$add-button-size / 2);
22 | overflow: visible;
23 | }
24 | }
25 | }
26 |
27 | // TODO: Remove these references to classes that are styled by core Wagtail CSS. The best
28 | // opportunity for this is propably as part of Wagtails general CSS overhaul. -@jonnyscholes
29 | .field {
30 | + .field {
31 | padding-top: $grid-gutter-width / 2;
32 | }
33 |
34 | &__label {
35 | display: block;
36 | font-weight: bold;
37 | margin-bottom: $grid-gutter-width / 4;
38 | }
39 |
40 | &.required > label::after {
41 | content: '*';
42 | color: #cd3238;
43 | font-weight: 700;
44 | display: inline-block;
45 | margin-left: 0.5em;
46 | line-height: 1em;
47 | font-size: 13px;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/scss/index.scss:
--------------------------------------------------------------------------------
1 | @import 'variables';
2 |
3 | @import 'components/c-sf-add-button';
4 | @import 'components/c-sf-add-panel';
5 | @import 'components/c-sf-button';
6 | @import 'components/c-sf-block';
7 | @import 'components/c-sf-container';
8 |
--------------------------------------------------------------------------------
/src/types.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 |
4 | export const refType = PropTypes.oneOfType([
5 | PropTypes.func,
6 | PropTypes.shape({current: PropTypes.instanceOf(Element)}),
7 | ]);
8 |
--------------------------------------------------------------------------------