decap-cms-widget-list 2.11.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,680 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import ImmutablePropTypes from 'react-immutable-proptypes';
4
+ import styled from '@emotion/styled';
5
+ import { css, ClassNames } from '@emotion/core';
6
+ import { List, Map, fromJS } from 'immutable';
7
+ import { partial, isEmpty, uniqueId } from 'lodash';
8
+ import uuid from 'uuid/v4';
9
+ import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
10
+ import DecapCmsWidgetObject from 'decap-cms-widget-object';
11
+ import {
12
+ ListItemTopBar,
13
+ ObjectWidgetTopBar,
14
+ colors,
15
+ lengths,
16
+ FieldLabel,
17
+ } from 'decap-cms-ui-default';
18
+ import { stringTemplate, validations } from 'decap-cms-lib-widgets';
19
+
20
+ import {
21
+ TYPES_KEY,
22
+ getTypedFieldForValue,
23
+ resolveFieldKeyType,
24
+ getErrorMessageForTypedFieldAndValue,
25
+ } from './typedListHelpers';
26
+
27
+ const ObjectControl = DecapCmsWidgetObject.controlComponent;
28
+
29
+ const ListItem = styled.div();
30
+
31
+ const SortableListItem = SortableElement(ListItem);
32
+
33
+ const StyledListItemTopBar = styled(ListItemTopBar)`
34
+ background-color: ${colors.textFieldBorder};
35
+ `;
36
+
37
+ const NestedObjectLabel = styled.div`
38
+ display: ${props => (props.collapsed ? 'block' : 'none')};
39
+ border-top: 0;
40
+ color: ${props => (props.error ? colors.errorText : 'inherit')};
41
+ background-color: ${colors.textFieldBorder};
42
+ padding: 13px;
43
+ border-radius: 0 0 ${lengths.borderRadius} ${lengths.borderRadius};
44
+ `;
45
+
46
+ const styleStrings = {
47
+ collapsedObjectControl: `
48
+ display: none;
49
+ `,
50
+ objectWidgetTopBarContainer: `
51
+ padding: ${lengths.objectWidgetTopBarContainerPadding};
52
+ `,
53
+ };
54
+
55
+ const styles = {
56
+ listControlItem: css`
57
+ margin-top: 18px;
58
+
59
+ &:first-of-type {
60
+ margin-top: 26px;
61
+ }
62
+ `,
63
+ listControlItemCollapsed: css`
64
+ padding-bottom: 0;
65
+ `,
66
+ };
67
+
68
+ const SortableList = SortableContainer(({ items, renderItem }) => {
69
+ return <div>{items.map(renderItem)}</div>;
70
+ });
71
+
72
+ const valueTypes = {
73
+ SINGLE: 'SINGLE',
74
+ MULTIPLE: 'MULTIPLE',
75
+ MIXED: 'MIXED',
76
+ };
77
+
78
+ function handleSummary(summary, entry, label, item) {
79
+ const data = stringTemplate.addFileTemplateFields(
80
+ entry.get('path'),
81
+ item.set('fields.label', label),
82
+ );
83
+ return stringTemplate.compileStringTemplate(summary, null, '', data);
84
+ }
85
+
86
+ function validateItem(field, item) {
87
+ if (!Map.isMap(item)) {
88
+ console.warn(
89
+ `'${field.get('name')}' field item value value should be a map but is a '${typeof item}'`,
90
+ );
91
+ return false;
92
+ }
93
+
94
+ return true;
95
+ }
96
+ function LabelComponent({ field, isActive, hasErrors, uniqueFieldId, isFieldOptional, t }) {
97
+ const label = `${field.get('label', field.get('name'))}`;
98
+ return (
99
+ <FieldLabel isActive={isActive} hasErrors={hasErrors} htmlFor={uniqueFieldId}>
100
+ {label} {`${isFieldOptional ? ` (${t('editor.editorControl.field.optional')})` : ''}`}
101
+ </FieldLabel>
102
+ );
103
+ }
104
+
105
+ export default class ListControl extends React.Component {
106
+ validations = [];
107
+
108
+ static propTypes = {
109
+ metadata: ImmutablePropTypes.map,
110
+ onChange: PropTypes.func.isRequired,
111
+ onChangeObject: PropTypes.func.isRequired,
112
+ onValidateObject: PropTypes.func.isRequired,
113
+ validate: PropTypes.func.isRequired,
114
+ value: ImmutablePropTypes.list,
115
+ field: PropTypes.object,
116
+ forID: PropTypes.string,
117
+ controlRef: PropTypes.func,
118
+ mediaPaths: ImmutablePropTypes.map.isRequired,
119
+ getAsset: PropTypes.func.isRequired,
120
+ onOpenMediaLibrary: PropTypes.func.isRequired,
121
+ onAddAsset: PropTypes.func.isRequired,
122
+ onRemoveInsertedMedia: PropTypes.func.isRequired,
123
+ classNameWrapper: PropTypes.string.isRequired,
124
+ setActiveStyle: PropTypes.func.isRequired,
125
+ setInactiveStyle: PropTypes.func.isRequired,
126
+ editorControl: PropTypes.elementType.isRequired,
127
+ resolveWidget: PropTypes.func.isRequired,
128
+ clearFieldErrors: PropTypes.func.isRequired,
129
+ fieldsErrors: ImmutablePropTypes.map.isRequired,
130
+ entry: ImmutablePropTypes.map.isRequired,
131
+ t: PropTypes.func,
132
+ };
133
+
134
+ static defaultProps = {
135
+ value: List(),
136
+ parentIds: [],
137
+ };
138
+
139
+ constructor(props) {
140
+ super(props);
141
+ const { field, value } = props;
142
+ const listCollapsed = field.get('collapsed', true);
143
+ const itemsCollapsed = (value && Array(value.size).fill(listCollapsed)) || [];
144
+ const keys = (value && Array.from({ length: value.size }, () => uuid())) || [];
145
+
146
+ this.state = {
147
+ listCollapsed,
148
+ itemsCollapsed,
149
+ value: this.valueToString(value),
150
+ keys,
151
+ };
152
+ }
153
+
154
+ valueToString = value => {
155
+ let stringValue;
156
+ if (List.isList(value) || Array.isArray(value)) {
157
+ stringValue = value.join(',');
158
+ } else {
159
+ console.warn(
160
+ `Expected List value to be an array but received '${value}' with type of '${typeof value}'. Please check the value provided to the '${this.props.field.get(
161
+ 'name',
162
+ )}' field`,
163
+ );
164
+ stringValue = String(value);
165
+ }
166
+ return stringValue.replace(/,([^\s]|$)/g, ', $1');
167
+ };
168
+
169
+ getValueType = () => {
170
+ const { field } = this.props;
171
+ if (field.get('fields')) {
172
+ return valueTypes.MULTIPLE;
173
+ } else if (field.get('field')) {
174
+ return valueTypes.SINGLE;
175
+ } else if (field.get(TYPES_KEY)) {
176
+ return valueTypes.MIXED;
177
+ } else {
178
+ return null;
179
+ }
180
+ };
181
+
182
+ uniqueFieldId = uniqueId(`${this.props.field.get('name')}-field-`);
183
+ /**
184
+ * Always update so that each nested widget has the option to update. This is
185
+ * required because ControlHOC provides a default `shouldComponentUpdate`
186
+ * which only updates if the value changes, but every widget must be allowed
187
+ * to override this.
188
+ */
189
+ shouldComponentUpdate() {
190
+ return true;
191
+ }
192
+
193
+ handleChange = e => {
194
+ const { onChange } = this.props;
195
+ const oldValue = this.state.value;
196
+ const newValue = e.target.value.trim();
197
+ const listValue = newValue ? newValue.split(',') : [];
198
+ if (newValue.match(/,$/) && oldValue.match(/, $/)) {
199
+ listValue.pop();
200
+ }
201
+
202
+ const parsedValue = this.valueToString(listValue);
203
+ this.setState({ value: parsedValue });
204
+ onChange(List(listValue.map(val => val.trim())));
205
+ };
206
+
207
+ handleFocus = () => {
208
+ this.props.setActiveStyle();
209
+ };
210
+
211
+ handleBlur = e => {
212
+ const listValue = e.target.value
213
+ .split(',')
214
+ .map(el => el.trim())
215
+ .filter(el => el);
216
+ this.setState({ value: this.valueToString(listValue) });
217
+ this.props.setInactiveStyle();
218
+ };
219
+
220
+ handleAdd = e => {
221
+ e.preventDefault();
222
+ const { field } = this.props;
223
+ const parsedValue =
224
+ this.getValueType() === valueTypes.SINGLE
225
+ ? this.singleDefault()
226
+ : fromJS(this.multipleDefault(field.get('fields')));
227
+ this.addItem(parsedValue);
228
+ };
229
+
230
+ singleDefault = () => {
231
+ return this.props.field.getIn(['field', 'default'], null);
232
+ };
233
+
234
+ multipleDefault = fields => {
235
+ return this.getFieldsDefault(fields);
236
+ };
237
+
238
+ handleAddType = (type, typeKey) => {
239
+ const parsedValue = fromJS(this.mixedDefault(typeKey, type));
240
+ this.addItem(parsedValue);
241
+ };
242
+
243
+ mixedDefault = (typeKey, type) => {
244
+ const selectedType = this.props.field.get(TYPES_KEY).find(f => f.get('name') === type);
245
+ const fields = selectedType.get('fields') || [selectedType.get('field')];
246
+
247
+ return this.getFieldsDefault(fields, { [typeKey]: type });
248
+ };
249
+
250
+ getFieldsDefault = (fields, initialValue = {}) => {
251
+ return fields.reduce((acc, item) => {
252
+ const subfields = item.get('field') || item.get('fields');
253
+ const object = item.get('widget') == 'object';
254
+ const name = item.get('name');
255
+ const defaultValue = item.get('default', null);
256
+
257
+ if (List.isList(subfields) && object) {
258
+ const subDefaultValue = this.getFieldsDefault(subfields);
259
+ !isEmpty(subDefaultValue) && (acc[name] = subDefaultValue);
260
+ return acc;
261
+ }
262
+
263
+ if (Map.isMap(subfields) && object) {
264
+ const subDefaultValue = this.getFieldsDefault([subfields]);
265
+ !isEmpty(subDefaultValue) && (acc[name] = subDefaultValue);
266
+ return acc;
267
+ }
268
+
269
+ if (defaultValue !== null) {
270
+ acc[name] = defaultValue;
271
+ }
272
+
273
+ return acc;
274
+ }, initialValue);
275
+ };
276
+
277
+ addItem = parsedValue => {
278
+ const { value, onChange, field } = this.props;
279
+ const addToTop = field.get('add_to_top', false);
280
+
281
+ const itemKey = uuid();
282
+ this.setState({
283
+ itemsCollapsed: addToTop
284
+ ? [false, ...this.state.itemsCollapsed]
285
+ : [...this.state.itemsCollapsed, false],
286
+ keys: addToTop ? [itemKey, ...this.state.keys] : [...this.state.keys, itemKey],
287
+ });
288
+
289
+ const listValue = value || List();
290
+ if (addToTop) {
291
+ onChange(listValue.unshift(parsedValue));
292
+ } else {
293
+ onChange(listValue.push(parsedValue));
294
+ }
295
+ };
296
+
297
+ processControlRef = ref => {
298
+ if (!ref) return;
299
+ const {
300
+ validate,
301
+ props: { validationKey: key },
302
+ } = ref;
303
+ this.validations.push({ key, validate });
304
+ };
305
+
306
+ validate = () => {
307
+ if (this.getValueType()) {
308
+ this.validations.forEach(item => {
309
+ item.validate();
310
+ });
311
+ } else {
312
+ this.props.validate();
313
+ }
314
+ this.props.onValidateObject(this.props.forID, this.validateSize());
315
+ };
316
+
317
+ validateSize = () => {
318
+ const { field, value, t } = this.props;
319
+ const min = field.get('min');
320
+ const max = field.get('max');
321
+ const required = field.get('required', true);
322
+
323
+ if (!required && !value?.size) {
324
+ return [];
325
+ }
326
+
327
+ const error = validations.validateMinMax(
328
+ t,
329
+ field.get('label', field.get('name')),
330
+ value,
331
+ min,
332
+ max,
333
+ );
334
+
335
+ return error ? [error] : [];
336
+ };
337
+
338
+ /**
339
+ * In case the `onChangeObject` function is frozen by a child widget implementation,
340
+ * e.g. when debounced, always get the latest object value instead of using
341
+ * `this.props.value` directly.
342
+ */
343
+ getObjectValue = idx => this.props.value.get(idx) || Map();
344
+
345
+ handleChangeFor(index) {
346
+ return (f, newValue, newMetadata) => {
347
+ const { value, metadata, onChange, field } = this.props;
348
+ const collectionName = field.get('name');
349
+ const listFieldObjectWidget = field.getIn(['field', 'widget']) === 'object';
350
+ const withNameKey =
351
+ this.getValueType() !== valueTypes.SINGLE ||
352
+ (this.getValueType() === valueTypes.SINGLE && listFieldObjectWidget);
353
+ const newObjectValue = withNameKey
354
+ ? this.getObjectValue(index).set(f.get('name'), newValue)
355
+ : newValue;
356
+ const parsedMetadata = {
357
+ [collectionName]: Object.assign(metadata ? metadata.toJS() : {}, newMetadata || {}),
358
+ };
359
+ onChange(value.set(index, newObjectValue), parsedMetadata);
360
+ };
361
+ }
362
+
363
+ handleRemove = (index, event) => {
364
+ event.preventDefault();
365
+ const { itemsCollapsed } = this.state;
366
+ const { value, metadata, onChange, field, clearFieldErrors } = this.props;
367
+ const collectionName = field.get('name');
368
+ const isSingleField = this.getValueType() === valueTypes.SINGLE;
369
+
370
+ const metadataRemovePath = isSingleField ? value.get(index) : value.get(index).valueSeq();
371
+ const parsedMetadata =
372
+ metadata && !metadata.isEmpty()
373
+ ? { [collectionName]: metadata.removeIn(metadataRemovePath) }
374
+ : metadata;
375
+
376
+ itemsCollapsed.splice(index, 1);
377
+ // clear validations
378
+ this.validations = [];
379
+
380
+ this.setState({
381
+ itemsCollapsed: [...itemsCollapsed],
382
+ keys: Array.from({ length: value.size - 1 }, () => uuid()),
383
+ });
384
+
385
+ onChange(value.remove(index), parsedMetadata);
386
+ clearFieldErrors();
387
+ };
388
+
389
+ handleItemCollapseToggle = (index, event) => {
390
+ event.preventDefault();
391
+ const { itemsCollapsed } = this.state;
392
+ const newItemsCollapsed = itemsCollapsed.map((collapsed, itemIndex) => {
393
+ if (index === itemIndex) {
394
+ return !collapsed;
395
+ }
396
+ return collapsed;
397
+ });
398
+ this.setState({
399
+ itemsCollapsed: newItemsCollapsed,
400
+ });
401
+ };
402
+
403
+ handleCollapseAllToggle = e => {
404
+ e.preventDefault();
405
+ const { value, field } = this.props;
406
+ const { itemsCollapsed, listCollapsed } = this.state;
407
+ const minimizeCollapsedItems = field.get('minimize_collapsed', false);
408
+ const listCollapsedByDefault = field.get('collapsed', true);
409
+ const allItemsCollapsed = itemsCollapsed.every(val => val === true);
410
+
411
+ if (minimizeCollapsedItems) {
412
+ let updatedItemsCollapsed = itemsCollapsed;
413
+ // Only allow collapsing all items in this mode but not opening all at once
414
+ if (!listCollapsed || !listCollapsedByDefault) {
415
+ updatedItemsCollapsed = Array(value.size).fill(!listCollapsed);
416
+ }
417
+ this.setState({ listCollapsed: !listCollapsed, itemsCollapsed: updatedItemsCollapsed });
418
+ } else {
419
+ this.setState({ itemsCollapsed: Array(value.size).fill(!allItemsCollapsed) });
420
+ }
421
+ };
422
+
423
+ objectLabel(item) {
424
+ const { field, entry } = this.props;
425
+ const valueType = this.getValueType();
426
+ switch (valueType) {
427
+ case valueTypes.MIXED: {
428
+ if (!validateItem(field, item)) {
429
+ return;
430
+ }
431
+ const itemType = getTypedFieldForValue(field, item);
432
+ const label = itemType.get('label', itemType.get('name'));
433
+ // each type can have its own summary, but default to the list summary if exists
434
+ const summary = itemType.get('summary', field.get('summary'));
435
+ const labelReturn = summary ? handleSummary(summary, entry, label, item) : label;
436
+ return labelReturn;
437
+ }
438
+ case valueTypes.SINGLE: {
439
+ const singleField = field.get('field');
440
+ const label = singleField.get('label', singleField.get('name'));
441
+ const summary = field.get('summary');
442
+ const data = fromJS({ [singleField.get('name')]: item });
443
+ const labelReturn = summary ? handleSummary(summary, entry, label, data) : label;
444
+ return labelReturn;
445
+ }
446
+ case valueTypes.MULTIPLE: {
447
+ if (!validateItem(field, item)) {
448
+ return;
449
+ }
450
+ const multiFields = field.get('fields');
451
+ const labelField = multiFields && multiFields.first();
452
+ const value = item.get(labelField.get('name'));
453
+ const summary = field.get('summary');
454
+ const labelReturn = summary ? handleSummary(summary, entry, value, item) : value;
455
+ return (labelReturn || `No ${labelField.get('name')}`).toString();
456
+ }
457
+ }
458
+ return '';
459
+ }
460
+
461
+ onSortEnd = ({ oldIndex, newIndex }) => {
462
+ const { value, clearFieldErrors } = this.props;
463
+ const { itemsCollapsed, keys } = this.state;
464
+
465
+ // Update value
466
+ const item = value.get(oldIndex);
467
+ const newValue = value.delete(oldIndex).insert(newIndex, item);
468
+ this.props.onChange(newValue);
469
+
470
+ // Update collapsing
471
+ const collapsed = itemsCollapsed[oldIndex];
472
+ itemsCollapsed.splice(oldIndex, 1);
473
+ const updatedItemsCollapsed = [...itemsCollapsed];
474
+ updatedItemsCollapsed.splice(newIndex, 0, collapsed);
475
+
476
+ // Reset item to ensure updated state
477
+ const updatedKeys = keys.map((key, keyIndex) => {
478
+ if (keyIndex === oldIndex || keyIndex === newIndex) {
479
+ return uuid();
480
+ }
481
+ return key;
482
+ });
483
+ this.setState({ itemsCollapsed: updatedItemsCollapsed, keys: updatedKeys });
484
+
485
+ //clear error fields and remove old validations
486
+ clearFieldErrors();
487
+ this.validations = this.validations.filter(item => updatedKeys.includes(item.key));
488
+ };
489
+
490
+ hasError = index => {
491
+ const { fieldsErrors } = this.props;
492
+ if (fieldsErrors && fieldsErrors.size > 0) {
493
+ return Object.values(fieldsErrors.toJS()).some(arr =>
494
+ arr.some(err => err.parentIds && err.parentIds.includes(this.state.keys[index])),
495
+ );
496
+ }
497
+ };
498
+
499
+ // eslint-disable-next-line react/display-name
500
+ renderItem = (item, index) => {
501
+ const {
502
+ classNameWrapper,
503
+ editorControl,
504
+ onValidateObject,
505
+ metadata,
506
+ clearFieldErrors,
507
+ fieldsErrors,
508
+ controlRef,
509
+ resolveWidget,
510
+ parentIds,
511
+ forID,
512
+ t,
513
+ } = this.props;
514
+
515
+ const { itemsCollapsed, keys } = this.state;
516
+ const collapsed = itemsCollapsed[index];
517
+ const key = keys[index];
518
+ let field = this.props.field;
519
+ const hasError = this.hasError(index);
520
+ const isVariableTypesList = this.getValueType() === valueTypes.MIXED;
521
+ if (isVariableTypesList) {
522
+ field = getTypedFieldForValue(field, item);
523
+ if (!field) {
524
+ return this.renderErroneousTypedItem(index, item);
525
+ }
526
+ }
527
+ return (
528
+ <SortableListItem
529
+ css={[styles.listControlItem, collapsed && styles.listControlItemCollapsed]}
530
+ index={index}
531
+ key={key}
532
+ >
533
+ {isVariableTypesList && (
534
+ <LabelComponent
535
+ field={field}
536
+ isActive={false}
537
+ hasErrors={hasError}
538
+ uniqueFieldId={this.uniqueFieldId}
539
+ isFieldOptional={field.get('required') === false}
540
+ t={t}
541
+ />
542
+ )}
543
+ <StyledListItemTopBar
544
+ collapsed={collapsed}
545
+ onCollapseToggle={partial(this.handleItemCollapseToggle, index)}
546
+ onRemove={partial(this.handleRemove, index)}
547
+ dragHandleHOC={SortableHandle}
548
+ data-testid={`styled-list-item-top-bar-${key}`}
549
+ />
550
+ <NestedObjectLabel collapsed={collapsed} error={hasError}>
551
+ {this.objectLabel(item)}
552
+ </NestedObjectLabel>
553
+ <ClassNames>
554
+ {({ css, cx }) => (
555
+ <ObjectControl
556
+ classNameWrapper={cx(classNameWrapper, {
557
+ [css`
558
+ ${styleStrings.collapsedObjectControl};
559
+ `]: collapsed,
560
+ })}
561
+ value={item}
562
+ field={field}
563
+ onChangeObject={this.handleChangeFor(index)}
564
+ editorControl={editorControl}
565
+ resolveWidget={resolveWidget}
566
+ metadata={metadata}
567
+ forList
568
+ onValidateObject={onValidateObject}
569
+ clearFieldErrors={clearFieldErrors}
570
+ fieldsErrors={fieldsErrors}
571
+ ref={this.processControlRef}
572
+ controlRef={controlRef}
573
+ validationKey={key}
574
+ collapsed={collapsed}
575
+ data-testid={`object-control-${key}`}
576
+ hasError={hasError}
577
+ parentIds={[...parentIds, forID, key]}
578
+ />
579
+ )}
580
+ </ClassNames>
581
+ </SortableListItem>
582
+ );
583
+ };
584
+
585
+ renderErroneousTypedItem(index, item) {
586
+ const field = this.props.field;
587
+ const errorMessage = getErrorMessageForTypedFieldAndValue(field, item);
588
+ const key = `item-${index}`;
589
+ return (
590
+ <SortableListItem
591
+ css={[styles.listControlItem, styles.listControlItemCollapsed]}
592
+ index={index}
593
+ key={key}
594
+ >
595
+ <StyledListItemTopBar
596
+ onCollapseToggle={null}
597
+ onRemove={partial(this.handleRemove, index, key)}
598
+ dragHandleHOC={SortableHandle}
599
+ />
600
+ <NestedObjectLabel collapsed={true} error={true}>
601
+ {errorMessage}
602
+ </NestedObjectLabel>
603
+ </SortableListItem>
604
+ );
605
+ }
606
+
607
+ renderListControl() {
608
+ const { value, forID, field, classNameWrapper, t } = this.props;
609
+ const { itemsCollapsed, listCollapsed } = this.state;
610
+ const items = value || List();
611
+ const label = field.get('label', field.get('name'));
612
+ const labelSingular = field.get('label_singular') || field.get('label', field.get('name'));
613
+ const listLabel = items.size === 1 ? labelSingular.toLowerCase() : label.toLowerCase();
614
+ const minimizeCollapsedItems = field.get('minimize_collapsed', false);
615
+ const allItemsCollapsed = itemsCollapsed.every(val => val === true);
616
+ const selfCollapsed = allItemsCollapsed && (listCollapsed || !minimizeCollapsedItems);
617
+
618
+ return (
619
+ <ClassNames>
620
+ {({ cx, css }) => (
621
+ <div
622
+ id={forID}
623
+ className={cx(
624
+ classNameWrapper,
625
+ css`
626
+ ${styleStrings.objectWidgetTopBarContainer}
627
+ `,
628
+ )}
629
+ >
630
+ <ObjectWidgetTopBar
631
+ allowAdd={field.get('allow_add', true)}
632
+ onAdd={this.handleAdd}
633
+ types={field.get(TYPES_KEY, null)}
634
+ onAddType={type => this.handleAddType(type, resolveFieldKeyType(field))}
635
+ heading={`${items.size} ${listLabel}`}
636
+ label={labelSingular.toLowerCase()}
637
+ onCollapseToggle={this.handleCollapseAllToggle}
638
+ collapsed={selfCollapsed}
639
+ t={t}
640
+ />
641
+ {(!selfCollapsed || !minimizeCollapsedItems) && (
642
+ <SortableList
643
+ items={items}
644
+ renderItem={this.renderItem}
645
+ onSortEnd={this.onSortEnd}
646
+ useDragHandle
647
+ lockAxis="y"
648
+ />
649
+ )}
650
+ </div>
651
+ )}
652
+ </ClassNames>
653
+ );
654
+ }
655
+
656
+ renderInput() {
657
+ const { forID, classNameWrapper } = this.props;
658
+ const { value } = this.state;
659
+
660
+ return (
661
+ <input
662
+ type="text"
663
+ id={forID}
664
+ value={value}
665
+ onChange={this.handleChange}
666
+ onFocus={this.handleFocus}
667
+ onBlur={this.handleBlur}
668
+ className={classNameWrapper}
669
+ />
670
+ );
671
+ }
672
+
673
+ render() {
674
+ if (this.getValueType() !== null) {
675
+ return this.renderListControl();
676
+ } else {
677
+ return this.renderInput();
678
+ }
679
+ }
680
+ }