@eeacms/volto-eea-website-theme 0.5.2 → 0.6.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,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);
@@ -18,8 +18,8 @@ import { Portal } from 'react-portal';
18
18
  import { connect } from 'react-redux';
19
19
  import { compose } from 'redux';
20
20
  import { Button, Comment, Container, Icon } from 'semantic-ui-react';
21
- import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
22
- // import { Button, Grid, Segment, Container } from 'semantic-ui-react';
21
+ import { formatRelativeDate } from '@plone/volto/helpers/Utils/Date';
22
+ import config from '@plone/volto/registry';
23
23
 
24
24
  const messages = defineMessages({
25
25
  comment: {
@@ -297,7 +297,6 @@ class Comments extends Component {
297
297
  */
298
298
  render() {
299
299
  const { items } = this.props;
300
- const moment = this.props.moment.default;
301
300
  const { collapsedComments } = this.state;
302
301
  // object with comment ids, to easily verify if any comment has children
303
302
  const allCommentsWithCildren = this.addRepliesAsChildrenToComments(items);
@@ -318,8 +317,11 @@ class Comments extends Component {
318
317
  <Comment.Metadata>
319
318
  <span>
320
319
  {' '}
321
- <span title={moment(comment.creation_date).format('LLLL')}>
322
- {moment(comment.creation_date).fromNow()}
320
+ <span title={comment.creation_date}>
321
+ {formatRelativeDate({
322
+ date: comment.creation_date,
323
+ locale: config.settings.dateLocale || 'en-gb',
324
+ })}
323
325
  </span>
324
326
  </span>
325
327
  </Comment.Metadata>
@@ -472,7 +474,6 @@ class Comments extends Component {
472
474
 
473
475
  export default compose(
474
476
  injectIntl,
475
- injectLazyLibs(['moment']),
476
477
  connect(
477
478
  (state) => ({
478
479
  items: state.comments.items,
package/src/index.js CHANGED
@@ -14,6 +14,7 @@ const applyConfig = (config) => {
14
14
  if (config.blocks.blocksConfig.accordion) {
15
15
  config.blocks.blocksConfig.accordion.semanticIcon = 'ri-arrow-down-s-line';
16
16
  }
17
+
17
18
  // apply inPage navigation
18
19
  config.settings.appExtras = [
19
20
  ...(config.settings.appExtras || []),
@@ -105,6 +105,8 @@
105
105
  @newTabLabeledIcon : 'eea';
106
106
  @labeledIconGroup : 'eea';
107
107
  @languageLabeledIcon : 'eea';
108
+ @callout : 'eea';
109
+ @quote : 'eea';
108
110
 
109
111
  /*******************************
110
112
  Folders