@eeacms/volto-eea-website-theme 0.5.2 → 0.5.3

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,426 @@
1
+ /**
2
+ * ObjectBrowserWidget component.
3
+ * @module components/manage/Widgets/ObjectBrowserWidget
4
+ */
5
+
6
+ import React, { Component } from 'react';
7
+ import PropTypes from 'prop-types';
8
+ import { compose } from 'redux';
9
+ import { compact, isArray, isEmpty, remove } from 'lodash';
10
+ import { connect } from 'react-redux';
11
+ import { Label, Popup, Button } from 'semantic-ui-react';
12
+ import {
13
+ flattenToAppURL,
14
+ isInternalURL,
15
+ isUrl,
16
+ normalizeUrl,
17
+ removeProtocol,
18
+ } from '@plone/volto/helpers/Url/Url';
19
+ import { searchContent } from '@plone/volto/actions/search/search';
20
+ import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
21
+ import { defineMessages, injectIntl } from 'react-intl';
22
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
23
+ import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
24
+
25
+ import navTreeSVG from '@plone/volto/icons/nav.svg';
26
+ import clearSVG from '@plone/volto/icons/clear.svg';
27
+ import homeSVG from '@plone/volto/icons/home.svg';
28
+ import aheadSVG from '@plone/volto/icons/ahead.svg';
29
+ import blankSVG from '@plone/volto/icons/blank.svg';
30
+ import { withRouter } from 'react-router';
31
+
32
+ const messages = defineMessages({
33
+ placeholder: {
34
+ id: 'No items selected',
35
+ defaultMessage: 'No items selected',
36
+ },
37
+ edit: {
38
+ id: 'Edit',
39
+ defaultMessage: 'Edit',
40
+ },
41
+ delete: {
42
+ id: 'Delete',
43
+ defaultMessage: 'Delete',
44
+ },
45
+ openObjectBrowser: {
46
+ id: 'Open object browser',
47
+ defaultMessage: 'Open object browser',
48
+ },
49
+ });
50
+
51
+ /**
52
+ * ObjectBrowserWidget component class.
53
+ * @class ObjectBrowserWidget
54
+ * @extends Component
55
+ */
56
+ export class ObjectBrowserWidgetComponent extends Component {
57
+ /**
58
+ * Property types.
59
+ * @property {Object} propTypes Property types.
60
+ * @static
61
+ */
62
+ static propTypes = {
63
+ id: PropTypes.string.isRequired,
64
+ title: PropTypes.string.isRequired,
65
+ description: PropTypes.string,
66
+ mode: PropTypes.string, // link, image, multiple
67
+ return: PropTypes.string, // single, multiple
68
+ required: PropTypes.bool,
69
+ error: PropTypes.arrayOf(PropTypes.string),
70
+ value: PropTypes.oneOfType([
71
+ PropTypes.arrayOf(PropTypes.object),
72
+ PropTypes.object,
73
+ ]),
74
+ onChange: PropTypes.func.isRequired,
75
+ openObjectBrowser: PropTypes.func.isRequired,
76
+ allowExternals: PropTypes.bool,
77
+ };
78
+
79
+ /**
80
+ * Default properties
81
+ * @property {Object} defaultProps Default properties.
82
+ * @static
83
+ */
84
+ static defaultProps = {
85
+ description: null,
86
+ required: false,
87
+ error: [],
88
+ value: [],
89
+ mode: 'multiple',
90
+ return: 'multiple',
91
+ allowExternals: false,
92
+ };
93
+
94
+ state = {
95
+ manualLinkInput: '',
96
+ validURL: false,
97
+ };
98
+
99
+ constructor(props) {
100
+ super(props);
101
+ this.selectedItemsRef = React.createRef();
102
+ this.placeholderRef = React.createRef();
103
+ }
104
+ renderLabel(item) {
105
+ const href = item['@id'];
106
+ return (
107
+ <Popup
108
+ key={flattenToAppURL(href)}
109
+ content={
110
+ <div style={{ display: 'flex' }}>
111
+ {isInternalURL(href) ? (
112
+ <Icon name={homeSVG} size="18px" />
113
+ ) : (
114
+ <Icon name={blankSVG} size="18px" />
115
+ )}
116
+ &nbsp;
117
+ {flattenToAppURL(href)}
118
+ </div>
119
+ }
120
+ trigger={
121
+ <Label>
122
+ <div className="item-title">{item.title}</div>
123
+ <div>
124
+ {this.props.mode === 'multiple' && (
125
+ <Icon
126
+ name={clearSVG}
127
+ size="12px"
128
+ className="right"
129
+ onClick={(event) => {
130
+ event.preventDefault();
131
+ this.removeItem(item);
132
+ }}
133
+ />
134
+ )}
135
+ </div>
136
+ </Label>
137
+ }
138
+ />
139
+ );
140
+ }
141
+
142
+ removeItem = (item) => {
143
+ let value = [...this.props.value];
144
+ remove(value, function (_item) {
145
+ return _item['@id'] === item['@id'];
146
+ });
147
+ this.props.onChange(this.props.id, value);
148
+ };
149
+
150
+ onChange = (item) => {
151
+ let value = this.props.mode === 'multiple' ? [...this.props.value] : [];
152
+ value = value.filter((item) => item != null);
153
+ const maxSize =
154
+ this.props.widgetOptions?.pattern_options?.maximumSelectionSize || -1;
155
+ if (maxSize === 1 && value.length === 1) {
156
+ value = []; //enable replace of selected item with another value, if maxsize is 1
157
+ }
158
+ let exists = false;
159
+ let index = -1;
160
+ value.forEach((_item, _index) => {
161
+ if (flattenToAppURL(_item['@id']) === flattenToAppURL(item['@id'])) {
162
+ exists = true;
163
+ index = _index;
164
+ }
165
+ });
166
+ //find(value, {
167
+ // '@id': flattenToAppURL(item['@id']),
168
+ // });
169
+ if (!exists) {
170
+ // add item
171
+ // Check if we want to filter the attributes of the selected item
172
+ let resultantItem = item;
173
+ if (this.props.selectedItemAttrs) {
174
+ const allowedItemKeys = [
175
+ ...this.props.selectedItemAttrs,
176
+ // Add the required attributes for the widget to work
177
+ '@id',
178
+ 'title',
179
+ ];
180
+ resultantItem = Object.keys(item)
181
+ .filter((key) => allowedItemKeys.includes(key))
182
+ .reduce((obj, key) => {
183
+ obj[key] = item[key];
184
+ return obj;
185
+ }, {});
186
+ }
187
+ // Add required @id field, just in case
188
+ resultantItem = { ...resultantItem, '@id': item['@id'] };
189
+ value.push(resultantItem);
190
+ if (this.props.return === 'single') {
191
+ this.props.onChange(this.props.id, value[0]);
192
+ } else {
193
+ this.props.onChange(this.props.id, value);
194
+ }
195
+ } else {
196
+ //remove item
197
+ value.splice(index, 1);
198
+ this.props.onChange(this.props.id, value);
199
+ }
200
+ };
201
+
202
+ onManualLinkInput = (e) => {
203
+ this.setState({ manualLinkInput: e.target.value });
204
+ if (this.validateManualLink(e.target.value)) {
205
+ this.setState({ validURL: true });
206
+ } else {
207
+ this.setState({ validURL: false });
208
+ }
209
+ };
210
+
211
+ validateManualLink = (url) => {
212
+ if (this.props.allowExternals) {
213
+ return isUrl(url);
214
+ } else {
215
+ return isInternalURL(url);
216
+ }
217
+ };
218
+
219
+ onSubmitManualLink = () => {
220
+ if (this.validateManualLink(this.state.manualLinkInput)) {
221
+ if (isInternalURL(this.state.manualLinkInput)) {
222
+ const link = this.state.manualLinkInput;
223
+ // convert it into an internal on if possible
224
+ this.props
225
+ .searchContent(
226
+ '/',
227
+ {
228
+ 'path.query': flattenToAppURL(this.state.manualLinkInput),
229
+ 'path.depth': '0',
230
+ sort_on: 'getObjPositionInParent',
231
+ metadata_fields: '_all',
232
+ b_size: 1000,
233
+ },
234
+ `${this.props.block}-${this.props.mode}`,
235
+ )
236
+ .then((resp) => {
237
+ if (resp.items?.length > 0) {
238
+ this.onChange(resp.items[0]);
239
+ } else {
240
+ this.props.onChange(this.props.id, [
241
+ {
242
+ '@id': normalizeUrl(link),
243
+ title: removeProtocol(link),
244
+ },
245
+ ]);
246
+ }
247
+ });
248
+ } else {
249
+ this.props.onChange(this.props.id, [
250
+ {
251
+ '@id': normalizeUrl(this.state.manualLinkInput),
252
+ title: removeProtocol(this.state.manualLinkInput),
253
+ },
254
+ ]);
255
+ }
256
+ this.setState({ validURL: true, manualLinkInput: '' });
257
+ }
258
+ };
259
+
260
+ onKeyDownManualLink = (e) => {
261
+ if (e.key === 'Enter') {
262
+ e.preventDefault();
263
+ e.stopPropagation();
264
+ this.onSubmitManualLink();
265
+ } else if (e.key === 'Escape') {
266
+ e.preventDefault();
267
+ e.stopPropagation();
268
+ // TODO: Do something on ESC key
269
+ }
270
+ };
271
+
272
+ showObjectBrowser = (ev) => {
273
+ ev.preventDefault();
274
+ this.props.openObjectBrowser({
275
+ mode: this.props.mode,
276
+ currentPath: this.props.location.pathname,
277
+ propDataName: 'value',
278
+ onSelectItem: (url, item) => {
279
+ this.onChange(item);
280
+ },
281
+ selectableTypes: this.props.widgetOptions?.pattern_options
282
+ ?.selectableTypes,
283
+ maximumSelectionSize: this.props.widgetOptions?.pattern_options
284
+ ?.maximumSelectionSize,
285
+ });
286
+ };
287
+
288
+ handleSelectedItemsRefClick = (e) => {
289
+ if (this.props.isDisabled) {
290
+ return;
291
+ }
292
+
293
+ if (
294
+ e.target.contains(this.selectedItemsRef.current) ||
295
+ e.target.contains(this.placeholderRef.current)
296
+ ) {
297
+ this.showObjectBrowser(e);
298
+ }
299
+ };
300
+
301
+ /**
302
+ * Render method.
303
+ * @method render
304
+ * @returns {string} Markup for the component.
305
+ */
306
+ render() {
307
+ const {
308
+ id,
309
+ description,
310
+ fieldSet,
311
+ value,
312
+ mode,
313
+ onChange,
314
+ isDisabled,
315
+ } = this.props;
316
+
317
+ let items = compact(!isArray(value) && value ? [value] : value || []);
318
+
319
+ let icon =
320
+ mode === 'multiple' || items.length === 0 ? navTreeSVG : clearSVG;
321
+ let iconAction =
322
+ mode === 'multiple' || items.length === 0
323
+ ? this.showObjectBrowser
324
+ : (e) => {
325
+ e.preventDefault();
326
+ onChange(id, this.props.return === 'single' ? null : []);
327
+ };
328
+
329
+ return (
330
+ <FormFieldWrapper
331
+ {...this.props}
332
+ className={description ? 'help text' : 'text'}
333
+ >
334
+ <div
335
+ className="objectbrowser-field"
336
+ aria-labelledby={`fieldset-${
337
+ fieldSet || 'default'
338
+ }-field-label-${id}`}
339
+ >
340
+ <div
341
+ className="selected-values"
342
+ onClick={this.handleSelectedItemsRefClick}
343
+ onKeyDown={this.handleSelectedItemsRefClick}
344
+ role="searchbox"
345
+ tabIndex={0}
346
+ ref={this.selectedItemsRef}
347
+ >
348
+ {items.map((item) => this.renderLabel(item))}
349
+
350
+ {items.length === 0 && this.props.mode === 'multiple' && (
351
+ <div className="placeholder" ref={this.placeholderRef}>
352
+ {this.props.intl.formatMessage(messages.placeholder)}
353
+ </div>
354
+ )}
355
+ {this.props.allowExternals &&
356
+ items.length === 0 &&
357
+ this.props.mode !== 'multiple' && (
358
+ <input
359
+ onKeyDown={this.onKeyDownManualLink}
360
+ onChange={this.onManualLinkInput}
361
+ value={this.state.manualLinkInput}
362
+ placeholder={this.props.intl.formatMessage(
363
+ messages.placeholder,
364
+ )}
365
+ />
366
+ )}
367
+ </div>
368
+ {this.state.manualLinkInput && isEmpty(items) && (
369
+ <Button.Group>
370
+ <Button
371
+ basic
372
+ icon
373
+ className="cancel"
374
+ onClick={(e) => {
375
+ e.stopPropagation();
376
+ this.setState({ manualLinkInput: '' });
377
+ }}
378
+ >
379
+ <Icon name={clearSVG} size="18px" color="#e40166" />
380
+ </Button>
381
+ <Button
382
+ basic
383
+ icon
384
+ primary
385
+ disabled={!this.state.validURL}
386
+ onClick={(e) => {
387
+ e.stopPropagation();
388
+ this.onSubmitManualLink();
389
+ }}
390
+ >
391
+ <Icon name={aheadSVG} size="18px" />
392
+ </Button>
393
+ </Button.Group>
394
+ )}
395
+ {!this.state.manualLinkInput && (
396
+ <Button
397
+ aria-label={this.props.intl.formatMessage(
398
+ messages.openObjectBrowser,
399
+ )}
400
+ onClick={iconAction}
401
+ className="action"
402
+ disabled={isDisabled}
403
+ >
404
+ <Icon name={icon} size="18px" />
405
+ </Button>
406
+ )}
407
+ </div>
408
+ </FormFieldWrapper>
409
+ );
410
+ }
411
+ }
412
+
413
+ const ObjectBrowserWidgetMode = (mode) =>
414
+ compose(
415
+ injectIntl,
416
+ withObjectBrowser,
417
+ withRouter,
418
+ connect(null, { searchContent }),
419
+ )((props) => <ObjectBrowserWidgetComponent {...props} mode={mode} />);
420
+ export { ObjectBrowserWidgetMode };
421
+ export default compose(
422
+ injectIntl,
423
+ withObjectBrowser,
424
+ withRouter,
425
+ connect(null, { searchContent }),
426
+ )(ObjectBrowserWidgetComponent);