@eeacms/volto-eea-website-theme 1.4.1 → 1.5.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,121 @@
1
+ import { defineMessages } from 'react-intl';
2
+
3
+ const messages = defineMessages({
4
+ Source: {
5
+ id: 'Source',
6
+ defaultMessage: 'Source',
7
+ },
8
+ Image: {
9
+ id: 'Image',
10
+ defaultMessage: 'Image',
11
+ },
12
+ AltText: {
13
+ id: 'Alt text',
14
+ defaultMessage: 'Alt text',
15
+ },
16
+ Align: {
17
+ id: 'Alignment',
18
+ defaultMessage: 'Alignment',
19
+ },
20
+ size: {
21
+ id: 'Image size',
22
+ defaultMessage: 'Image size',
23
+ },
24
+ LinkTo: {
25
+ id: 'Link to',
26
+ defaultMessage: 'Link to',
27
+ },
28
+ openLinkInNewTab: {
29
+ id: 'Open in a new tab',
30
+ defaultMessage: 'Open in a new tab',
31
+ },
32
+ AltTextHint: {
33
+ id: 'Alt text hint',
34
+ defaultMessage: 'Leave empty if the image is purely decorative.',
35
+ },
36
+ AltTextHintLinkText: {
37
+ id: 'Alt text hint link text',
38
+ defaultMessage: 'Describe the purpose of the image.',
39
+ },
40
+ });
41
+
42
+ export function ImageSchema({ formData, intl }) {
43
+ return {
44
+ fieldsets: [
45
+ {
46
+ id: 'default',
47
+ title: 'Default',
48
+ fields: [...(formData.url ? ['url', 'alt', 'align', 'size'] : [])],
49
+ },
50
+ {
51
+ id: 'copyright',
52
+ title: 'Copyright',
53
+ fields: ['copyright', 'copyrightIcon', 'copyrightPosition'],
54
+ },
55
+ ...(formData.url
56
+ ? [
57
+ {
58
+ id: 'link_settings',
59
+ title: 'Link settings',
60
+ fields: ['href', 'openLinkInNewTab'],
61
+ },
62
+ ]
63
+ : []),
64
+ ],
65
+ properties: {
66
+ url: {
67
+ title: intl.formatMessage(messages.Source),
68
+ widget: 'url',
69
+ },
70
+ alt: {
71
+ title: intl.formatMessage(messages.AltText),
72
+ description: (
73
+ <>
74
+ <a
75
+ href="https://www.w3.org/WAI/tutorials/images/decision-tree/"
76
+ title={intl.formatMessage(messages.openLinkInNewTab)}
77
+ target="_blank"
78
+ rel="noopener noreferrer"
79
+ >
80
+ {intl.formatMessage(messages.AltTextHintLinkText)}
81
+ </a>{' '}
82
+ {intl.formatMessage(messages.AltTextHint)}
83
+ </>
84
+ ),
85
+ },
86
+ align: {
87
+ title: intl.formatMessage(messages.Align),
88
+ widget: 'align',
89
+ },
90
+ size: {
91
+ title: intl.formatMessage(messages.size),
92
+ widget: 'image_size',
93
+ },
94
+ href: {
95
+ title: intl.formatMessage(messages.LinkTo),
96
+ widget: 'object_browser',
97
+ mode: 'link',
98
+ selectedItemAttrs: ['Title', 'Description', 'hasPreviewImage'],
99
+ allowExternals: true,
100
+ },
101
+ openLinkInNewTab: {
102
+ title: intl.formatMessage(messages.openLinkInNewTab),
103
+ type: 'boolean',
104
+ },
105
+ copyright: {
106
+ title: 'Text',
107
+ },
108
+ copyrightIcon: {
109
+ title: 'Icon',
110
+ default: 'ri-copyright-line',
111
+ },
112
+ copyrightPosition: {
113
+ title: 'Align',
114
+ widget: 'align',
115
+ actions: ['left', 'right'],
116
+ defaultValue: 'left',
117
+ },
118
+ },
119
+ required: [],
120
+ };
121
+ }
@@ -0,0 +1,12 @@
1
+ .image-block {
2
+ position: relative;
3
+ }
4
+
5
+ .copyright-image {
6
+ position: absolute;
7
+ z-index: 1000;
8
+ top: 90%;
9
+ width: 100%;
10
+ padding-right: 10px;
11
+ padding-left: 10px;
12
+ }
@@ -0,0 +1,76 @@
1
+ import { defineMessages, useIntl } from 'react-intl';
2
+ import { Icon } from '@plone/volto/components';
3
+ import { Button } from 'semantic-ui-react';
4
+ import imageLeftSVG from '@plone/volto/icons/image-left.svg';
5
+ import imageRightSVG from '@plone/volto/icons/image-right.svg';
6
+ import imageFitSVG from '@plone/volto/icons/image-fit.svg';
7
+ import imageWideSVG from '@plone/volto/icons/image-wide.svg';
8
+ import imageFullSVG from '@plone/volto/icons/image-full.svg';
9
+
10
+ const messages = defineMessages({
11
+ left: {
12
+ id: 'Left',
13
+ defaultMessage: 'Left',
14
+ },
15
+ right: {
16
+ id: 'Right',
17
+ defaultMessage: 'Right',
18
+ },
19
+ center: {
20
+ id: 'Center',
21
+ defaultMessage: 'Center',
22
+ },
23
+ wide: {
24
+ id: 'Wide',
25
+ defaultMessage: 'Wide',
26
+ },
27
+ full: {
28
+ id: 'Full',
29
+ defaultMessage: 'Full',
30
+ },
31
+ });
32
+
33
+ const AlignChooser = ({
34
+ align,
35
+ onChangeBlock,
36
+ data,
37
+ block,
38
+ actions = ['left', 'right', 'center', 'full'],
39
+ }) => {
40
+ const intl = useIntl();
41
+
42
+ const ICON_MAP = {
43
+ left: imageLeftSVG,
44
+ right: imageRightSVG,
45
+ center: imageFitSVG,
46
+ wide: imageWideSVG,
47
+ full: imageFullSVG,
48
+ };
49
+
50
+ function onAlignBlock(align) {
51
+ onChangeBlock(block, {
52
+ ...data,
53
+ copyrightPosition: align,
54
+ });
55
+ }
56
+
57
+ return (
58
+ <div className="align-buttons">
59
+ {actions.map((action) => (
60
+ <Button.Group>
61
+ <Button
62
+ icon
63
+ basic
64
+ aria-label={intl.formatMessage(messages[action])}
65
+ onClick={() => onAlignBlock(action)}
66
+ active={data.copyrightPosition === action}
67
+ >
68
+ <Icon name={ICON_MAP[action]} size="24px" />
69
+ </Button>
70
+ </Button.Group>
71
+ ))}
72
+ </div>
73
+ );
74
+ };
75
+
76
+ export default AlignChooser;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Edit image block.
3
+ * @module components/manage/Blocks/Image/Edit
4
+ */
5
+
6
+ import React, { Component } from 'react';
7
+ import PropTypes from 'prop-types';
8
+ import { compose } from 'redux';
9
+ import { defineMessages, injectIntl } from 'react-intl';
10
+ import cx from 'classnames';
11
+ import { Message } from 'semantic-ui-react';
12
+ import { isEqual } from 'lodash';
13
+ import { Copyright } from '@eeacms/volto-eea-design-system/ui';
14
+ import './style.less';
15
+ import { Icon } from 'semantic-ui-react';
16
+ import { LeadImageSidebar, SidebarPortal } from '@plone/volto/components';
17
+ import { flattenToAppURL } from '@plone/volto/helpers';
18
+
19
+ import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
20
+
21
+ const messages = defineMessages({
22
+ ImageBlockInputPlaceholder: {
23
+ id: "Upload a lead image in the 'Lead Image' content field.",
24
+ defaultMessage: "Upload a lead image in the 'Lead Image' content field.",
25
+ },
26
+ });
27
+
28
+ /**
29
+ * Edit image block class.
30
+ * @class Edit
31
+ * @extends Component
32
+ */
33
+ class Edit extends Component {
34
+ /**
35
+ * Property types.
36
+ * @property {Object} propTypes Property types.
37
+ * @static
38
+ */
39
+ static propTypes = {
40
+ properties: PropTypes.objectOf(PropTypes.any).isRequired,
41
+ selected: PropTypes.bool.isRequired,
42
+ block: PropTypes.string.isRequired,
43
+ index: PropTypes.number.isRequired,
44
+ data: PropTypes.objectOf(PropTypes.any).isRequired,
45
+ pathname: PropTypes.string.isRequired,
46
+ onChangeBlock: PropTypes.func.isRequired,
47
+ openObjectBrowser: PropTypes.func.isRequired,
48
+ };
49
+
50
+ /**
51
+ * Align block handler
52
+ * @method onAlignBlock
53
+ * @param {string} align Alignment option
54
+ * @returns {undefined}
55
+ */
56
+ onAlignBlock(align) {
57
+ this.props.onChangeBlock(this.props.block, {
58
+ ...this.props.data,
59
+ align,
60
+ });
61
+ }
62
+
63
+ /**
64
+ * @param {*} nextProps
65
+ * @returns {boolean}
66
+ * @memberof Edit
67
+ */
68
+ shouldComponentUpdate(nextProps) {
69
+ return (
70
+ this.props.selected ||
71
+ nextProps.selected ||
72
+ !isEqual(this.props.data, nextProps.data)
73
+ );
74
+ }
75
+
76
+ node = React.createRef();
77
+
78
+ /**
79
+ * Render method.
80
+ * @method render
81
+ * @returns {string} Markup for the component.
82
+ */
83
+ render() {
84
+ const { data, properties } = this.props;
85
+ const placeholder =
86
+ this.props.data.placeholder ||
87
+ this.props.intl.formatMessage(messages.ImageBlockInputPlaceholder);
88
+ const { copyright, copyrightIcon, copyrightPosition } = data;
89
+
90
+ return (
91
+ <div
92
+ className={cx(
93
+ 'block image align',
94
+ {
95
+ center: !Boolean(data.align),
96
+ },
97
+ data.align,
98
+ )}
99
+ >
100
+ {!properties.image && (
101
+ <Message>
102
+ <center>
103
+ <img src={imageBlockSVG} alt="" />
104
+ <div className="message-text">{placeholder}</div>
105
+ </center>
106
+ </Message>
107
+ )}
108
+ {properties.image && (
109
+ <div className="image-block">
110
+ <img
111
+ className={cx({ 'full-width': data.align === 'full' })}
112
+ src={
113
+ properties.image.data
114
+ ? `data:${properties.image['content-type']};base64,${properties.image.data}`
115
+ : flattenToAppURL(properties.image.download)
116
+ }
117
+ alt={data.image_caption || ''}
118
+ />
119
+ <div className="copyright-image">
120
+ {copyright ? (
121
+ <Copyright copyrightPosition={copyrightPosition}>
122
+ <Copyright.Icon>
123
+ <Icon className={copyrightIcon} />
124
+ </Copyright.Icon>
125
+ <Copyright.Text>{copyright}</Copyright.Text>
126
+ </Copyright>
127
+ ) : (
128
+ ''
129
+ )}
130
+ </div>
131
+ </div>
132
+ )}
133
+ <SidebarPortal selected={this.props.selected}>
134
+ <LeadImageSidebar {...this.props} />
135
+ </SidebarPortal>
136
+ </div>
137
+ );
138
+ }
139
+ }
140
+
141
+ export default compose(injectIntl)(Edit);
@@ -0,0 +1,296 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Form } from 'semantic-ui-react';
4
+ import { Accordion, Grid, Segment } from 'semantic-ui-react';
5
+ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
6
+ import { CheckboxWidget, Icon, TextWidget } from '@plone/volto/components';
7
+ import { flattenToAppURL } from '@plone/volto/helpers';
8
+ import AlignBlock from '@plone/volto/components/manage/Sidebar/AlignBlock';
9
+ import AlignChooser from './AlignChooser';
10
+
11
+ import imageSVG from '@plone/volto/icons/image.svg';
12
+ import clearSVG from '@plone/volto/icons/clear.svg';
13
+ import upSVG from '@plone/volto/icons/up-key.svg';
14
+ import downSVG from '@plone/volto/icons/down-key.svg';
15
+ import navTreeSVG from '@plone/volto/icons/nav.svg';
16
+
17
+ const messages = defineMessages({
18
+ Image: {
19
+ id: 'Image',
20
+ defaultMessage: 'Image',
21
+ },
22
+ Origin: {
23
+ id: 'Origin',
24
+ defaultMessage: 'Origin',
25
+ },
26
+ AltText: {
27
+ id: 'Alt text',
28
+ defaultMessage: 'Alt text',
29
+ },
30
+ Copyright: {
31
+ id: 'Text',
32
+ defaultMessage: 'Text',
33
+ },
34
+ CopyrightIcon: {
35
+ id: 'Icon',
36
+ defaultMessage: 'Icon',
37
+ },
38
+ Align: {
39
+ id: 'Alignment',
40
+ defaultMessage: 'Alignment',
41
+ },
42
+ LinkTo: {
43
+ id: 'Link to',
44
+ defaultMessage: 'Link to',
45
+ },
46
+ openLinkInNewTab: {
47
+ id: 'Open in a new tab',
48
+ defaultMessage: 'Open in a new tab',
49
+ },
50
+ NoImageSelected: {
51
+ id: 'No image set in image content field',
52
+ defaultMessage: 'No image set in image content field',
53
+ },
54
+ externalURL: {
55
+ id: 'External URL',
56
+ defaultMessage: 'External URL',
57
+ },
58
+ });
59
+
60
+ const LeadImageSidebar = ({
61
+ properties,
62
+ data,
63
+ block,
64
+ onChangeBlock,
65
+ openObjectBrowser,
66
+ required = false,
67
+ onChangeField,
68
+ intl,
69
+ }) => {
70
+ const [activeAccIndex, setActiveAccIndex] = useState(0);
71
+ const defaultValueCopyrightIcon = 'ri-copyright-line';
72
+ const defaultValueCopyrightPosition = 'left';
73
+ function handleAccClick(e, titleProps) {
74
+ const { index } = titleProps;
75
+ const newIndex = activeAccIndex === index ? -1 : index;
76
+ setActiveAccIndex(newIndex);
77
+ }
78
+
79
+ useEffect(() => {
80
+ if (data.copyrightIcon === '' || data.copyrightIcon === undefined)
81
+ onChangeBlock(block, {
82
+ ...data,
83
+ copyrightIcon: defaultValueCopyrightIcon,
84
+ });
85
+ if (data.copyrightPosition === '' || data.copyrightPosition === undefined)
86
+ onChangeBlock(block, {
87
+ ...data,
88
+ copyrightPosition: defaultValueCopyrightPosition,
89
+ });
90
+ }, [block, data, onChangeBlock]);
91
+
92
+ return (
93
+ <Segment.Group raised>
94
+ <header className="header pulled">
95
+ <h2>
96
+ <FormattedMessage id="Lead Image" defaultMessage="Lead Image" />
97
+ </h2>
98
+ </header>
99
+
100
+ {!properties.image && (
101
+ <>
102
+ <Segment className="sidebar-metadata-container" secondary>
103
+ <FormattedMessage
104
+ id="No image set in Lead Image content field"
105
+ defaultMessage="No image set in Lead Image content field"
106
+ />
107
+ <Icon name={imageSVG} size="100px" color="#b8c6c8" />
108
+ </Segment>
109
+ </>
110
+ )}
111
+ {properties.image && (
112
+ <>
113
+ <Segment className="sidebar-metadata-container" secondary>
114
+ {properties.image.filename}
115
+ <img
116
+ src={
117
+ properties.image.data
118
+ ? `data:${properties.image['content-type']};base64,${properties.image.data}`
119
+ : flattenToAppURL(properties.image.scales.mini.download)
120
+ }
121
+ alt={properties.image_caption || ''}
122
+ />
123
+ </Segment>
124
+ <Segment className="form sidebar-image-data">
125
+ <TextWidget
126
+ id="alt"
127
+ title={intl.formatMessage(messages.AltText)}
128
+ required={false}
129
+ value={properties.image_caption}
130
+ icon={properties.image_caption ? clearSVG : null}
131
+ iconAction={() => onChangeField('image_caption', '')}
132
+ onChange={(name, value) => {
133
+ onChangeField('image_caption', value);
134
+ }}
135
+ />
136
+ <Form.Field inline required={required}>
137
+ <Grid>
138
+ <Grid.Row>
139
+ <Grid.Column width="4">
140
+ <div className="wrapper">
141
+ <label htmlFor="field-align">
142
+ <FormattedMessage
143
+ id="Alignment"
144
+ defaultMessage="Alignment"
145
+ />
146
+ </label>
147
+ </div>
148
+ </Grid.Column>
149
+ <Grid.Column width="8" className="align-tools">
150
+ <AlignBlock
151
+ align={data.align}
152
+ onChangeBlock={onChangeBlock}
153
+ data={data}
154
+ block={block}
155
+ />
156
+ </Grid.Column>
157
+ </Grid.Row>
158
+ </Grid>
159
+ </Form.Field>
160
+ </Segment>
161
+ <Accordion fluid styled className="form">
162
+ <Accordion.Title
163
+ active={activeAccIndex === 0}
164
+ index={0}
165
+ onClick={handleAccClick}
166
+ >
167
+ Link Settings
168
+ {activeAccIndex === 0 ? (
169
+ <Icon name={upSVG} size="20px" />
170
+ ) : (
171
+ <Icon name={downSVG} size="20px" />
172
+ )}
173
+ </Accordion.Title>
174
+ <Accordion.Content active={activeAccIndex === 0}>
175
+ <TextWidget
176
+ id="link"
177
+ title={intl.formatMessage(messages.LinkTo)}
178
+ required={false}
179
+ value={flattenToAppURL(data.href)}
180
+ icon={data.href ? clearSVG : navTreeSVG}
181
+ iconAction={
182
+ data.href
183
+ ? () => {
184
+ onChangeBlock(block, {
185
+ ...data,
186
+ href: '',
187
+ });
188
+ }
189
+ : () => openObjectBrowser({ mode: 'link' })
190
+ }
191
+ onChange={(name, value) => {
192
+ onChangeBlock(block, {
193
+ ...data,
194
+ href: flattenToAppURL(value),
195
+ });
196
+ }}
197
+ />
198
+ <CheckboxWidget
199
+ id="openLinkInNewTab"
200
+ title={intl.formatMessage(messages.openLinkInNewTab)}
201
+ value={data.openLinkInNewTab ? data.openLinkInNewTab : false}
202
+ onChange={(name, value) => {
203
+ onChangeBlock(block, {
204
+ ...data,
205
+ openLinkInNewTab: value,
206
+ });
207
+ }}
208
+ />
209
+ </Accordion.Content>
210
+ </Accordion>
211
+ <Accordion fluid styled className="form">
212
+ <Accordion.Title
213
+ active={activeAccIndex === 1}
214
+ index={1}
215
+ onClick={handleAccClick}
216
+ >
217
+ Copyright
218
+ {activeAccIndex === 1 ? (
219
+ <Icon name={upSVG} size="20px" />
220
+ ) : (
221
+ <Icon name={downSVG} size="20px" />
222
+ )}
223
+ </Accordion.Title>
224
+ <Accordion.Content active={activeAccIndex === 1}>
225
+ <Segment className="form">
226
+ <TextWidget
227
+ id="copyright"
228
+ title={intl.formatMessage(messages.Copyright)}
229
+ required={false}
230
+ value={data.copyright}
231
+ icon={data.copyright ? clearSVG : null}
232
+ iconAction={() => onChangeField('copyright', '')}
233
+ onChange={(name, value) => {
234
+ onChangeBlock(block, {
235
+ ...data,
236
+ copyright: value,
237
+ });
238
+ }}
239
+ />
240
+ <TextWidget
241
+ id="copyrightIcon"
242
+ title={intl.formatMessage(messages.CopyrightIcon)}
243
+ required={false}
244
+ value={data.copyrightIcon}
245
+ icon={data.copyrightIcon ? clearSVG : null}
246
+ iconAction={() => onChangeField('copyrightIcon', '')}
247
+ onChange={(name, value) => {
248
+ onChangeBlock(block, {
249
+ ...data,
250
+ copyrightIcon: value,
251
+ });
252
+ }}
253
+ />
254
+ <Form.Field inline required={required}>
255
+ <Grid>
256
+ <Grid.Row>
257
+ <Grid.Column width="4">
258
+ <div className="wrapper">
259
+ <label htmlFor="field-align">
260
+ <FormattedMessage
261
+ id="Alignment"
262
+ defaultMessage="Alignment"
263
+ />
264
+ </label>
265
+ </div>
266
+ </Grid.Column>
267
+ <Grid.Column width="8" className="align-tools">
268
+ <AlignChooser
269
+ align={data.copyrightPosition}
270
+ actions={['left', 'right']}
271
+ onChangeBlock={onChangeBlock}
272
+ block={block}
273
+ data={data}
274
+ />
275
+ </Grid.Column>
276
+ </Grid.Row>
277
+ </Grid>
278
+ </Form.Field>
279
+ </Segment>
280
+ </Accordion.Content>
281
+ </Accordion>
282
+ </>
283
+ )}
284
+ </Segment.Group>
285
+ );
286
+ };
287
+
288
+ LeadImageSidebar.propTypes = {
289
+ data: PropTypes.objectOf(PropTypes.any).isRequired,
290
+ block: PropTypes.string.isRequired,
291
+ onChangeBlock: PropTypes.func.isRequired,
292
+ openObjectBrowser: PropTypes.func.isRequired,
293
+ onChangeField: PropTypes.func.isRequired,
294
+ };
295
+
296
+ export default injectIntl(LeadImageSidebar);