@eeacms/volto-tabs-block 5.0.1 → 5.0.2

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,446 @@
1
+ import React from 'react';
2
+ import { connect } from 'react-redux';
3
+ import { compose } from 'redux';
4
+ import { withRouter } from 'react-router';
5
+ import { v4 as uuid } from 'uuid';
6
+ import { isEmpty } from 'lodash';
7
+ import cx from 'classnames';
8
+ import { Menu, Tab, Container, Dropdown, Input } from 'semantic-ui-react';
9
+ import config from '@plone/volto/registry';
10
+ import { emptyBlocksForm } from '@plone/volto/helpers';
11
+ import { BlocksForm } from '@plone/volto/components';
12
+ import EditBlockWrapper from '@eeacms/volto-tabs-block/components/EditBlockWrapper';
13
+ import { TABS_BLOCK } from '@eeacms/volto-tabs-block/constants';
14
+ import {
15
+ SimpleMarkdown,
16
+ getMenuPosition,
17
+ positionedOffset,
18
+ toggleItem,
19
+ } from '@eeacms/volto-tabs-block/utils';
20
+
21
+ import '@eeacms/volto-tabs-block/less/menu.less';
22
+
23
+ import noop from 'lodash/noop';
24
+ const MenuItem = (props) => {
25
+ const inputRef = React.useRef(null);
26
+ const {
27
+ activeTab = null,
28
+ activeBlock = null,
29
+ block = null,
30
+ data = {},
31
+ editingTab = null,
32
+ selected = false,
33
+ tabData = {},
34
+ tabs = {},
35
+ tabsData = {},
36
+ tabsDescription,
37
+ tabsList = [],
38
+ tabsTitle,
39
+ emptyTab = noop,
40
+ setActiveBlock = noop,
41
+ setActiveTab = noop,
42
+ setEditingTab = noop,
43
+ onChangeBlock = noop,
44
+ } = props;
45
+ const { tab, index } = props;
46
+ const title = tabs[tab].title;
47
+ const tabIndex = index + 1;
48
+ const defaultTitle = `Tab ${tabIndex}`;
49
+
50
+ const addNewTab = () => {
51
+ const tabId = uuid();
52
+
53
+ onChangeBlock(block, {
54
+ ...data,
55
+ data: {
56
+ ...tabsData,
57
+ blocks: {
58
+ ...tabsData.blocks,
59
+ [tabId]: {
60
+ ...emptyTab(),
61
+ },
62
+ },
63
+ blocks_layout: {
64
+ items: [...tabsData.blocks_layout.items, tabId],
65
+ },
66
+ },
67
+ });
68
+ };
69
+
70
+ React.useEffect(() => {
71
+ if (editingTab === tab && inputRef.current) {
72
+ inputRef.current.focus();
73
+ }
74
+ /* eslint-disable-next-line */
75
+ }, [editingTab]);
76
+
77
+ return (
78
+ <>
79
+ {index === 0 && (tabsTitle || tabsDescription) && (
80
+ <Menu.Item className="menu-title">
81
+ <SimpleMarkdown md={tabsTitle} className="title" defaultTag="##" />
82
+ <SimpleMarkdown md={tabsDescription} className="description" />
83
+ </Menu.Item>
84
+ )}
85
+ <Menu.Item
86
+ item-data={tab}
87
+ name={defaultTitle}
88
+ active={tab === activeTab}
89
+ onClick={() => {
90
+ if (activeTab !== tab) {
91
+ setActiveTab(tab);
92
+ }
93
+ if (activeBlock) {
94
+ setActiveBlock(null);
95
+ }
96
+ if (editingTab !== tab) {
97
+ setEditingTab(null);
98
+ }
99
+ }}
100
+ onDoubleClick={() => {
101
+ setEditingTab(tab);
102
+ }}
103
+ >
104
+ {editingTab === tab && selected ? (
105
+ <Input
106
+ item-data={tab}
107
+ placeholder={defaultTitle}
108
+ ref={inputRef}
109
+ transparent
110
+ value={title}
111
+ onChange={(e) => {
112
+ onChangeBlock(block, {
113
+ ...data,
114
+ data: {
115
+ ...tabsData,
116
+ blocks: {
117
+ ...tabsData.blocks,
118
+ [tab]: {
119
+ ...(tabData || {}),
120
+ title: e.target.value,
121
+ },
122
+ },
123
+ },
124
+ });
125
+ }}
126
+ />
127
+ ) : (
128
+ <>
129
+ <span className={'menu-item-count'} item-data={tab}>
130
+ {tabIndex}
131
+ </span>
132
+ <p className={'menu-item-text'} item-data={tab}>
133
+ {title || defaultTitle}
134
+ </p>
135
+ </>
136
+ )}
137
+ </Menu.Item>
138
+ {index === tabsList.length - 1 ? (
139
+ <>
140
+ <Menu.Item
141
+ name="addition"
142
+ onClick={(e) => {
143
+ addNewTab();
144
+ setEditingTab(null);
145
+ e.preventDefault();
146
+ }}
147
+ item-data={'addition'}
148
+ >
149
+ +
150
+ </Menu.Item>
151
+ </>
152
+ ) : (
153
+ ''
154
+ )}
155
+ </>
156
+ );
157
+ };
158
+
159
+ const MenuWrapper = (props) => {
160
+ const {
161
+ data = {},
162
+ panes = [],
163
+ activeTab = null,
164
+ node = null,
165
+ screen = {},
166
+ tabs = {},
167
+ tabsList = [],
168
+ block = null,
169
+ emptyTab = noop,
170
+ setEditingTab = noop,
171
+ tabsData = {},
172
+ onChangeBlock = noop,
173
+ setActiveTab = noop,
174
+ } = props;
175
+ const [open, setOpen] = React.useState(false);
176
+ const addNewTab = () => {
177
+ const tabId = uuid();
178
+
179
+ onChangeBlock(block, {
180
+ ...data,
181
+ data: {
182
+ ...tabsData,
183
+ blocks: {
184
+ ...tabsData.blocks,
185
+ [tabId]: {
186
+ ...emptyTab(),
187
+ },
188
+ },
189
+ blocks_layout: {
190
+ items: [...tabsData.blocks_layout.items, tabId],
191
+ },
192
+ },
193
+ });
194
+ };
195
+ React.useEffect(() => {
196
+ if (false || !node?.current) return;
197
+ const items = node.current.querySelectorAll(
198
+ '.ui.menu > .menu-wrapper > .item:not(.menu-title)',
199
+ );
200
+ const underlineDropdown = node.current.querySelector('.ui.dropdown');
201
+ if (!underlineDropdown) return;
202
+ const overflowOffset = positionedOffset(underlineDropdown, node.current);
203
+ if (!overflowOffset) {
204
+ return;
205
+ }
206
+ let anyHidden = false;
207
+ for (const item of items) {
208
+ const itemOffset = positionedOffset(item, node.current);
209
+ if (itemOffset) {
210
+ const hidden =
211
+ itemOffset.left + item.offsetWidth >= overflowOffset.left;
212
+ toggleItem(node.current, item, hidden);
213
+ anyHidden = anyHidden || hidden;
214
+ }
215
+ }
216
+ underlineDropdown.style.visibility = anyHidden ? '' : 'hidden';
217
+ if (!anyHidden && open) {
218
+ setOpen(false);
219
+ }
220
+ }, [screen, node, open, data.isResponsive]);
221
+
222
+ return (
223
+ <React.Fragment>
224
+ <div className="menu-wrapper tabs-accessibility">
225
+ {panes.map((pane, index) => (
226
+ <React.Fragment key={`menu-item-${index}-${pane.id}`}>
227
+ {pane.menuItem}
228
+ </React.Fragment>
229
+ ))}
230
+ </div>
231
+ <Dropdown
232
+ icon="ellipsis horizontal"
233
+ className="item"
234
+ pointing="top right"
235
+ open={open}
236
+ onOpen={() => setOpen(true)}
237
+ onClose={() => setOpen(false)}
238
+ >
239
+ <Dropdown.Menu>
240
+ {[...tabsList, 'addition'].map((underlineTab, underlineIndex) => {
241
+ const title =
242
+ underlineTab === 'addition' ? '+' : tabs[underlineTab]?.title;
243
+ const defaultTitle = `Tab ${underlineIndex + 1}`;
244
+
245
+ return (
246
+ <Dropdown.Item
247
+ hidden
248
+ key={`underline-tab-${underlineIndex}-${underlineTab}`}
249
+ underline-item-data={underlineTab}
250
+ active={underlineTab === activeTab}
251
+ onClick={(e) => {
252
+ if (underlineTab === 'addition') {
253
+ addNewTab();
254
+ setEditingTab(null);
255
+ e.preventDefault();
256
+ } else if (activeTab !== underlineTab) {
257
+ setActiveTab(underlineTab);
258
+ }
259
+ e.preventDefault();
260
+ }}
261
+ >
262
+ <span className={'menu-item-count'}>{underlineIndex + 1}</span>
263
+ <p className={'menu-item-text'}>{title || defaultTitle}</p>
264
+ </Dropdown.Item>
265
+ );
266
+ })}
267
+ </Dropdown.Menu>
268
+ </Dropdown>
269
+ </React.Fragment>
270
+ );
271
+ };
272
+
273
+ const Edit = (props) => {
274
+ const {
275
+ metadata = {},
276
+ data = {},
277
+ tabsList = [],
278
+ tabs = {},
279
+ activeTab = null,
280
+ selected = false,
281
+ manage = false,
282
+ activeTabIndex = 0,
283
+ block = null,
284
+ activeBlock = null,
285
+ multiSelected = [],
286
+ screen,
287
+ tabsData = {},
288
+ emptyTab = noop,
289
+ onChangeBlock = noop,
290
+ onChangeTabData = noop,
291
+ onSelectBlock = noop,
292
+ setEditingTab = noop,
293
+ } = props;
294
+ const menuPosition = getMenuPosition(data);
295
+ const isContainer = data.align === 'full';
296
+ const tabsTitle = data.title;
297
+ const tabsDescription = data.description;
298
+ const schema = React.useMemo(
299
+ () =>
300
+ config.blocks.blocksConfig[TABS_BLOCK].templates?.['default']?.schema(
301
+ config,
302
+ props,
303
+ ) || {},
304
+ [props],
305
+ );
306
+
307
+ const getDataValue = React.useCallback(
308
+ (key) => {
309
+ return (
310
+ (schema.properties[key]?.value || data[key]) ??
311
+ schema.properties[key]?.defaultValue
312
+ );
313
+ },
314
+ [schema, data],
315
+ );
316
+
317
+ const panes = tabsList.map((tab, index) => {
318
+ return {
319
+ id: tab,
320
+ menuItem: (
321
+ <MenuItem
322
+ {...props}
323
+ key={tab}
324
+ tab={tab}
325
+ tabsList={tabsList}
326
+ blockId={props.id}
327
+ index={index}
328
+ lastIndex={tabsList.length - 1}
329
+ tabsTitle={tabsTitle}
330
+ tabsDescription={tabsDescription}
331
+ />
332
+ ),
333
+ pane: (
334
+ <Tab.Pane as={isContainer ? Container : undefined}>
335
+ <div tabIndex={0} role="tabpanel" id={'tab-pane-' + tab}>
336
+ <BlocksForm
337
+ allowedBlocks={data?.allowedBlocks}
338
+ description={data?.instrunctions?.data}
339
+ manage={manage}
340
+ metadata={metadata}
341
+ pathname={props.pathname}
342
+ properties={isEmpty(tabs[tab]) ? emptyBlocksForm() : tabs[tab]}
343
+ selected={selected && activeTab === tab && activeBlock}
344
+ selectedBlock={
345
+ selected && activeTab === tab && activeBlock
346
+ ? activeBlock
347
+ : null
348
+ }
349
+ title={data?.placeholder}
350
+ onChangeField={onChangeTabData}
351
+ onChangeFormData={(newFormData) => {
352
+ onChangeBlock(block, {
353
+ ...data,
354
+ data: {
355
+ ...tabsData,
356
+ blocks: {
357
+ ...tabsData.blocks,
358
+ [activeTab]: {
359
+ ...(newFormData.blocks_layout.items.length > 0
360
+ ? newFormData
361
+ : emptyTab()),
362
+ },
363
+ },
364
+ },
365
+ });
366
+ }}
367
+ onSelectBlock={(id, selected, e) => {
368
+ const isMultipleSelection = e
369
+ ? e.shiftKey || e.ctrlKey || e.metaKey
370
+ : false;
371
+ onSelectBlock(
372
+ id,
373
+ activeBlock === id ? false : isMultipleSelection,
374
+ e,
375
+ );
376
+ setEditingTab(null);
377
+ }}
378
+ >
379
+ {({ draginfo }, editBlock, blockProps) => {
380
+ return (
381
+ <EditBlockWrapper
382
+ blockProps={blockProps}
383
+ draginfo={draginfo}
384
+ multiSelected={multiSelected.includes(blockProps.block)}
385
+ >
386
+ {editBlock}
387
+ </EditBlockWrapper>
388
+ );
389
+ }}
390
+ </BlocksForm>
391
+ </div>
392
+ </Tab.Pane>
393
+ ),
394
+ };
395
+ });
396
+
397
+ return (
398
+ <>
399
+ <Tab
400
+ activeIndex={activeTabIndex}
401
+ className="horizontal-responsive tabs"
402
+ renderActiveOnly={false}
403
+ menu={{
404
+ attached: menuPosition.attached,
405
+ borderless: getDataValue('menuBorderless'),
406
+ color: getDataValue('menuColor'),
407
+ compact: getDataValue('menuCompact'),
408
+ fluid: getDataValue('menuFluid'),
409
+ inverted: getDataValue('menuInverted'),
410
+ pointing: getDataValue('menuPointing'),
411
+ secondary: getDataValue('menuSecondary'),
412
+ size: getDataValue('menuSize'),
413
+ stackable: getDataValue('menuStackable'),
414
+ tabular: getDataValue('menuTabular'),
415
+ text: getDataValue('menuText'),
416
+ vertical: menuPosition.vertical,
417
+ className: cx(data.menuAlign, { container: isContainer }),
418
+ children: (
419
+ <MenuWrapper
420
+ {...props}
421
+ panes={panes}
422
+ menuPosition={menuPosition}
423
+ screen={screen}
424
+ tabsList={tabsList}
425
+ blockId={props.id}
426
+ lastIndex={tabsList.length - 1}
427
+ tabsTitle={tabsTitle}
428
+ tabsDescription={tabsDescription}
429
+ />
430
+ ),
431
+ }}
432
+ menuPosition={menuPosition.direction}
433
+ panes={panes}
434
+ grid={{ paneWidth: 9, tabWidth: 3 }}
435
+ />
436
+ </>
437
+ );
438
+ };
439
+
440
+ export default compose(
441
+ connect((state) => {
442
+ return {
443
+ screen: state.screen,
444
+ };
445
+ }),
446
+ )(withRouter(Edit));
@@ -37,6 +37,7 @@ const MenuItem = (props) => {
37
37
  setTabChanged(false);
38
38
  }
39
39
  }, [tabChanged, tab, blockId]);
40
+
40
41
  return (
41
42
  <React.Fragment>
42
43
  {index === 0 && (tabsTitle || tabsDescription) && (
@@ -193,7 +194,6 @@ const View = (props) => {
193
194
  const isContainer = data.align === 'full';
194
195
  const tabsTitle = data.title;
195
196
  const tabsDescription = data.description;
196
-
197
197
  const schema = React.useMemo(
198
198
  () =>
199
199
  config.blocks.blocksConfig[TABS_BLOCK].templates?.['default']?.schema(
@@ -231,8 +231,17 @@ const View = (props) => {
231
231
  ),
232
232
  pane: (
233
233
  <Tab.Pane as={isContainer ? Container : undefined}>
234
- <div tabIndex={0} role="tabpanel" id={'tab-pane-' + tab}>
235
- <RenderBlocks {...props} metadata={metadata} content={tabs[tab]} />
234
+ <div
235
+ id={tabs[tab]?.title || `Tab ${tabsList.indexOf(tab) + 1}`}
236
+ className="tab-name"
237
+ >
238
+ <div tabIndex={0} role="tabpanel" id={'tab-pane-' + tab}>
239
+ <RenderBlocks
240
+ {...props}
241
+ metadata={metadata}
242
+ content={tabs[tab]}
243
+ />
244
+ </div>
236
245
  </div>
237
246
  </Tab.Pane>
238
247
  ),
@@ -1,4 +1,8 @@
1
1
  import HorizontalResponsiveView from './View';
2
2
  import horizontalResponsiveSchema from './schema';
3
-
4
- export { HorizontalResponsiveView, horizontalResponsiveSchema };
3
+ import HorizontalResponsiveEdit from './Edit';
4
+ export {
5
+ HorizontalResponsiveEdit,
6
+ HorizontalResponsiveView,
7
+ horizontalResponsiveSchema,
8
+ };
package/src/index.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  DefaultView,
4
4
  AccordionEdit,
5
5
  AccordionView,
6
+ HorizontalResponsiveEdit,
6
7
  HorizontalResponsiveView,
7
8
  HorizontalCarouselView,
8
9
  VerticalCarouselView,
@@ -59,7 +60,7 @@ export default (config) => {
59
60
  },
60
61
  'horizontal-responsive': {
61
62
  title: 'Horizontal responsive',
62
- edit: DefaultEdit,
63
+ edit: HorizontalResponsiveEdit,
63
64
  view: HorizontalResponsiveView,
64
65
  schema: horizontalResponsiveSchema,
65
66
  },