@plone/volto 17.0.0-alpha.17 → 17.0.0-alpha.19
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.
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +57 -0
- package/cypress/support/commands.js +17 -0
- package/locales/ca/LC_MESSAGES/volto.po +39 -0
- package/locales/ca.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +39 -0
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +39 -0
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +39 -0
- package/locales/es.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +39 -0
- package/locales/eu.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +39 -0
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +39 -0
- package/locales/fr.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +40 -1
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +39 -0
- package/locales/ja.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +39 -0
- package/locales/nl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +39 -0
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +39 -0
- package/locales/pt_BR.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +39 -0
- package/locales/ro.json +1 -1
- package/locales/volto.pot +39 -0
- package/locales/zh_CN/LC_MESSAGES/volto.po +39 -0
- package/locales/zh_CN.json +1 -1
- package/package.json +2 -2
- package/packages/volto-slate/package.json +1 -1
- package/packages/volto-slate/src/blocks/Table/TableBlockEdit.jsx +21 -212
- package/packages/volto-slate/src/blocks/Table/schema.js +122 -0
- package/packages/volto-slate/src/editor/plugins/StyleMenu/utils.js +14 -5
- package/packages/volto-slate/src/utils/blocks.js +7 -0
- package/packages/volto-slate/src/widgets/RichTextWidget.jsx +15 -8
- package/src/components/index.js +1 -0
- package/src/components/manage/Blocks/Search/components/Facets.jsx +6 -2
- package/src/components/manage/Blocks/Search/components/SearchInput.jsx +9 -2
- package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +12 -1
- package/src/components/manage/Blocks/ToC/Schema.jsx +5 -1
- package/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx +142 -8
- package/src/components/manage/LinksToItem/LinksToItem.jsx +209 -0
- package/src/components/manage/LinksToItem/LinksToItem.test.jsx +97 -0
- package/src/components/manage/Toolbar/More.jsx +15 -0
- package/src/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx +1 -1
- package/src/components/theme/Breadcrumbs/Breadcrumbs.jsx +52 -99
- package/src/components/theme/Breadcrumbs/Breadcrumbs.stories.jsx +14 -13
- package/src/components/theme/Comments/CommentEditModal.jsx +63 -115
- package/src/components/theme/ContactForm/ContactForm.jsx +108 -192
- package/src/components/theme/ContactForm/ContactForm.stories.jsx +1 -1
- package/src/components/theme/ContactForm/ContactForm.test.jsx +2 -3
- package/src/components/theme/Login/Login.jsx +1 -1
- package/src/components/theme/SearchWidget/SearchWidget.jsx +38 -98
- package/src/components/theme/View/LinkView.jsx +51 -79
- package/src/config/NonContentRoutes.jsx +1 -0
- package/src/config/index.js +2 -0
- package/src/config/server.js +2 -0
- package/src/express-middleware/ok.js +16 -0
- package/src/hooks/client/useClient.js +11 -0
- package/src/hooks/index.js +1 -0
- package/src/routes.js +9 -0
- package/theme/themes/pastanaga/extras/main.less +2 -1
- package/theme/themes/pastanaga/extras/toc.less +29 -0
- package/news/4351.bugfix +0 -1
- package/news/4725.bugfix +0 -1
- package/news/4932.bugfix +0 -1
- package/news/4941.documentation +0 -1
- package/news/4951.breaking +0 -1
- package/news/4962.feature +0 -1
- package/news/4964.bugfix +0 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { defineMessages } from 'react-intl';
|
|
2
|
+
|
|
3
|
+
const messages = defineMessages({
|
|
4
|
+
hideHeaders: {
|
|
5
|
+
id: 'Hide headers',
|
|
6
|
+
defaultMessage: 'Hide headers',
|
|
7
|
+
},
|
|
8
|
+
sortable: {
|
|
9
|
+
id: 'Make the table sortable',
|
|
10
|
+
defaultMessage: 'Make the table sortable',
|
|
11
|
+
},
|
|
12
|
+
sortableDescription: {
|
|
13
|
+
id: 'Visible only in view mode',
|
|
14
|
+
defaultMessage: 'Visible only in view mode',
|
|
15
|
+
},
|
|
16
|
+
fixed: {
|
|
17
|
+
id: 'Fixed width table cells',
|
|
18
|
+
defaultMessage: 'Fixed width table cells',
|
|
19
|
+
},
|
|
20
|
+
compact: {
|
|
21
|
+
id: 'Make the table compact',
|
|
22
|
+
defaultMessage: 'Make the table compact',
|
|
23
|
+
},
|
|
24
|
+
basic: {
|
|
25
|
+
id: 'Reduce complexity',
|
|
26
|
+
defaultMessage: 'Reduce complexity',
|
|
27
|
+
},
|
|
28
|
+
celled: {
|
|
29
|
+
id: 'Divide each row into separate cells',
|
|
30
|
+
defaultMessage: 'Divide each row into separate cells',
|
|
31
|
+
},
|
|
32
|
+
inverted: {
|
|
33
|
+
id: 'Table color inverted',
|
|
34
|
+
defaultMessage: 'Table color inverted',
|
|
35
|
+
},
|
|
36
|
+
striped: {
|
|
37
|
+
id: 'Stripe alternate rows with color',
|
|
38
|
+
defaultMessage: 'Stripe alternate rows with color',
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function TableSchema(props) {
|
|
43
|
+
const { intl } = props;
|
|
44
|
+
return {
|
|
45
|
+
title: 'Table block',
|
|
46
|
+
fieldsets: [
|
|
47
|
+
{
|
|
48
|
+
id: 'default',
|
|
49
|
+
title: 'Default',
|
|
50
|
+
fields: [
|
|
51
|
+
'hideHeaders',
|
|
52
|
+
'sortable',
|
|
53
|
+
'fixed',
|
|
54
|
+
'celled',
|
|
55
|
+
'striped',
|
|
56
|
+
'compact',
|
|
57
|
+
'basic',
|
|
58
|
+
'inverted',
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
properties: {
|
|
63
|
+
hideHeaders: {
|
|
64
|
+
title: intl.formatMessage(messages.hideHeaders),
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
},
|
|
67
|
+
sortable: {
|
|
68
|
+
title: intl.formatMessage(messages.sortable),
|
|
69
|
+
type: 'boolean',
|
|
70
|
+
},
|
|
71
|
+
fixed: {
|
|
72
|
+
title: intl.formatMessage(messages.fixed),
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
},
|
|
75
|
+
celled: {
|
|
76
|
+
title: intl.formatMessage(messages.celled),
|
|
77
|
+
type: 'boolean',
|
|
78
|
+
},
|
|
79
|
+
striped: {
|
|
80
|
+
title: intl.formatMessage(messages.striped),
|
|
81
|
+
type: 'boolean',
|
|
82
|
+
},
|
|
83
|
+
compact: {
|
|
84
|
+
title: intl.formatMessage(messages.compact),
|
|
85
|
+
type: 'boolean',
|
|
86
|
+
},
|
|
87
|
+
basic: {
|
|
88
|
+
title: intl.formatMessage(messages.basic),
|
|
89
|
+
type: 'boolean',
|
|
90
|
+
},
|
|
91
|
+
inverted: {
|
|
92
|
+
title: intl.formatMessage(messages.inverted),
|
|
93
|
+
type: 'boolean',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
required: [],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function TableBlockSchema(props) {
|
|
101
|
+
return {
|
|
102
|
+
title: 'Table block',
|
|
103
|
+
fieldsets: [
|
|
104
|
+
{
|
|
105
|
+
id: 'default',
|
|
106
|
+
title: 'Default',
|
|
107
|
+
fields: ['table'],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
properties: {
|
|
111
|
+
table: {
|
|
112
|
+
title: 'Table block',
|
|
113
|
+
widget: 'object',
|
|
114
|
+
schema: TableSchema(props),
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
required: [],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default TableBlockSchema;
|
|
@@ -72,19 +72,28 @@ export const toggleInlineStyle = (editor, style) => {
|
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
export const isBlockStyleActive = (editor, style) => {
|
|
75
|
+
const keyName = `style-${style}`;
|
|
75
76
|
const sn = Array.from(
|
|
76
77
|
Editor.nodes(editor, {
|
|
77
|
-
match: (n) =>
|
|
78
|
-
|
|
78
|
+
match: (n) => {
|
|
79
|
+
const isStyle = typeof n.styleName === 'string' || n[keyName];
|
|
80
|
+
return !Editor.isEditor(n) && isStyle;
|
|
81
|
+
},
|
|
82
|
+
mode: 'all',
|
|
79
83
|
}),
|
|
80
84
|
);
|
|
81
85
|
|
|
82
86
|
for (const [n] of sn) {
|
|
83
|
-
if (n.styleName
|
|
87
|
+
if (typeof n.styleName === 'string') {
|
|
88
|
+
if (n.styleName.split(' ').filter((x) => x === style).length > 0) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
} else if (
|
|
92
|
+
n[keyName] &&
|
|
93
|
+
keyName.split('-').filter((x) => x === style).length > 0
|
|
94
|
+
)
|
|
84
95
|
return true;
|
|
85
|
-
}
|
|
86
96
|
}
|
|
87
|
-
|
|
88
97
|
return false;
|
|
89
98
|
};
|
|
90
99
|
|
|
@@ -125,6 +125,13 @@ export function createEmptyParagraph() {
|
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
export function createParagraph(text) {
|
|
129
|
+
return {
|
|
130
|
+
type: config.settings.slate.defaultBlockType,
|
|
131
|
+
children: [{ text }],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
128
135
|
export const isSingleBlockTypeActive = (editor, format) => {
|
|
129
136
|
const [match] = Editor.nodes(editor, {
|
|
130
137
|
match: (n) => n.type === format,
|
|
@@ -4,13 +4,26 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React from 'react';
|
|
7
|
+
import isUndefined from 'lodash/isUndefined';
|
|
8
|
+
import isString from 'lodash/isString';
|
|
7
9
|
import { FormFieldWrapper } from '@plone/volto/components';
|
|
8
10
|
import SlateEditor from '@plone/volto-slate/editor/SlateEditor';
|
|
9
11
|
|
|
10
|
-
import { createEmptyParagraph } from '../utils/blocks';
|
|
12
|
+
import { createEmptyParagraph, createParagraph } from '../utils/blocks';
|
|
11
13
|
|
|
12
14
|
import './style.css';
|
|
13
15
|
|
|
16
|
+
const getValue = (value) => {
|
|
17
|
+
if (isUndefined(value) || !isUndefined(value?.data)) {
|
|
18
|
+
return [createEmptyParagraph()];
|
|
19
|
+
}
|
|
20
|
+
// Previously this was a text field
|
|
21
|
+
if (isString(value)) {
|
|
22
|
+
return [createParagraph(value)];
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
|
|
14
27
|
const SlateRichTextWidget = (props) => {
|
|
15
28
|
const {
|
|
16
29
|
id,
|
|
@@ -42,13 +55,7 @@ const SlateRichTextWidget = (props) => {
|
|
|
42
55
|
readOnly={readOnly}
|
|
43
56
|
id={id}
|
|
44
57
|
name={id}
|
|
45
|
-
value={
|
|
46
|
-
typeof value === 'undefined' ||
|
|
47
|
-
typeof value?.data !==
|
|
48
|
-
'undefined' /* previously this was a Draft block */
|
|
49
|
-
? [createEmptyParagraph()]
|
|
50
|
-
: value
|
|
51
|
-
}
|
|
58
|
+
value={getValue(value)}
|
|
52
59
|
onChange={(newValue) => {
|
|
53
60
|
onChange(id, newValue);
|
|
54
61
|
}}
|
package/src/components/index.js
CHANGED
|
@@ -108,6 +108,7 @@ export History from '@plone/volto/components/manage/History/History';
|
|
|
108
108
|
export Sharing from '@plone/volto/components/manage/Sharing/Sharing';
|
|
109
109
|
export Rules from '@plone/volto/components/manage/Rules/Rules';
|
|
110
110
|
export Aliases from '@plone/volto/components/manage/Aliases/Aliases';
|
|
111
|
+
export LinksToItem from '@plone/volto/components/manage/LinksToItem/LinksToItem';
|
|
111
112
|
export Workflow from '@plone/volto/components/manage/Workflow/Workflow';
|
|
112
113
|
export Messages from '@plone/volto/components/manage/Messages/Messages';
|
|
113
114
|
export BlockChooser from '@plone/volto/components/manage/BlockChooser/BlockChooser';
|
|
@@ -12,7 +12,7 @@ const messages = defineMessages({
|
|
|
12
12
|
hideFilters: { id: 'Hide filters', defaultMessage: 'Hide filters' },
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const defaultShowFacet = (index) => {
|
|
16
16
|
const { values } = index;
|
|
17
17
|
return index
|
|
18
18
|
? hasNonValueOperation(index.operations || []) ||
|
|
@@ -91,7 +91,11 @@ const Facets = (props) => {
|
|
|
91
91
|
|
|
92
92
|
// TODO :handle changing the type of facet (multi/nonmulti)
|
|
93
93
|
|
|
94
|
-
const {
|
|
94
|
+
const {
|
|
95
|
+
view: FacetWidget,
|
|
96
|
+
stateToValue,
|
|
97
|
+
showFacet = defaultShowFacet,
|
|
98
|
+
} = resolveExtension(
|
|
95
99
|
'type',
|
|
96
100
|
search.extensions.facetWidgets.types,
|
|
97
101
|
facetSettings,
|
|
@@ -13,7 +13,14 @@ const messages = defineMessages({
|
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
const SearchInput = (props) => {
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
data,
|
|
18
|
+
searchText,
|
|
19
|
+
setSearchText,
|
|
20
|
+
isLive,
|
|
21
|
+
onTriggerSearch,
|
|
22
|
+
removeSearchQuery,
|
|
23
|
+
} = props;
|
|
17
24
|
const intl = useIntl();
|
|
18
25
|
|
|
19
26
|
return (
|
|
@@ -44,7 +51,7 @@ const SearchInput = (props) => {
|
|
|
44
51
|
className="search-input-clear-icon-button"
|
|
45
52
|
onClick={() => {
|
|
46
53
|
setSearchText('');
|
|
47
|
-
|
|
54
|
+
removeSearchQuery();
|
|
48
55
|
}}
|
|
49
56
|
>
|
|
50
57
|
<Icon name={clearSVG} />
|
|
@@ -301,7 +301,7 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
301
301
|
id,
|
|
302
302
|
query: data.query || {},
|
|
303
303
|
facets: toSearchFacets || facets,
|
|
304
|
-
searchText: toSearchText
|
|
304
|
+
searchText: toSearchText ? toSearchText.trim() : '',
|
|
305
305
|
sortOn: toSortOn || sortOn,
|
|
306
306
|
sortOrder: toSortOrder || sortOrder,
|
|
307
307
|
facetSettings,
|
|
@@ -329,6 +329,16 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
329
329
|
],
|
|
330
330
|
);
|
|
331
331
|
|
|
332
|
+
const removeSearchQuery = () => {
|
|
333
|
+
searchData.query = searchData.query.reduce(
|
|
334
|
+
// Remove SearchableText from query
|
|
335
|
+
(acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
|
|
336
|
+
[],
|
|
337
|
+
);
|
|
338
|
+
setSearchData(searchData);
|
|
339
|
+
setLocationSearchData(getSearchFields(searchData));
|
|
340
|
+
};
|
|
341
|
+
|
|
332
342
|
const querystringResults = useSelector(
|
|
333
343
|
(state) => state.querystringsearch.subrequests,
|
|
334
344
|
);
|
|
@@ -347,6 +357,7 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
347
357
|
sortOrder={sortOrder}
|
|
348
358
|
searchedText={urlSearchText}
|
|
349
359
|
searchText={searchText}
|
|
360
|
+
removeSearchQuery={removeSearchQuery}
|
|
350
361
|
setSearchText={setSearchText}
|
|
351
362
|
onTriggerSearch={onTriggerSearch}
|
|
352
363
|
totalItems={totalItems}
|
|
@@ -10,7 +10,7 @@ const TableOfContentsSchema = ({ data }) => {
|
|
|
10
10
|
fields: [
|
|
11
11
|
'title',
|
|
12
12
|
'hide_title',
|
|
13
|
-
...(variation === 'default' ? ['ordered'] : []),
|
|
13
|
+
...(variation === 'default' ? ['ordered'] : ['sticky']),
|
|
14
14
|
'levels',
|
|
15
15
|
],
|
|
16
16
|
},
|
|
@@ -39,6 +39,10 @@ const TableOfContentsSchema = ({ data }) => {
|
|
|
39
39
|
title: 'Ordered',
|
|
40
40
|
type: 'boolean',
|
|
41
41
|
},
|
|
42
|
+
sticky: {
|
|
43
|
+
title: 'Sticky',
|
|
44
|
+
type: 'boolean',
|
|
45
|
+
},
|
|
42
46
|
},
|
|
43
47
|
required: [],
|
|
44
48
|
};
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
* View toc block.
|
|
3
|
-
* @module components/manage/Blocks/ToC/View
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
7
2
|
import PropTypes from 'prop-types';
|
|
8
3
|
import { map } from 'lodash';
|
|
9
|
-
import { Menu } from 'semantic-ui-react';
|
|
4
|
+
import { Menu, Dropdown } from 'semantic-ui-react';
|
|
10
5
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
|
11
6
|
import AnchorLink from 'react-anchor-link-smooth-scroll';
|
|
12
7
|
import Slugger from 'github-slugger';
|
|
@@ -36,6 +31,131 @@ const RenderMenuItems = ({ items }) => {
|
|
|
36
31
|
* @extends Component
|
|
37
32
|
*/
|
|
38
33
|
const View = ({ data, tocEntries }) => {
|
|
34
|
+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
35
|
+
// When the page is resized to prevent items from the TOC from going out of the viewport,
|
|
36
|
+
// a dropdown menu is added containing all the items that don't fit.
|
|
37
|
+
const handleResize = () => {
|
|
38
|
+
const menuElement = document.querySelector('.responsive-menu');
|
|
39
|
+
const containerWidth = menuElement.offsetWidth;
|
|
40
|
+
|
|
41
|
+
// Get all divs that contain the items from the TOC, except the dropdown button
|
|
42
|
+
const nested = document.querySelectorAll(
|
|
43
|
+
'.responsive-menu .item:not(.dropdown)',
|
|
44
|
+
);
|
|
45
|
+
const nestedArray = Object.values(nested);
|
|
46
|
+
const middle = Math.ceil(nestedArray.length / 2);
|
|
47
|
+
const firstHalfNested = nestedArray.slice(0, middle);
|
|
48
|
+
const secondHalfNested = nestedArray.slice(middle);
|
|
49
|
+
|
|
50
|
+
const dropdown = document.querySelector('.dropdown');
|
|
51
|
+
const dropdownWidth = dropdown.offsetWidth;
|
|
52
|
+
|
|
53
|
+
const firstHalfNestedHiddenItems = [];
|
|
54
|
+
|
|
55
|
+
// Add a 'hidden' class for the items that should be in the dropdown
|
|
56
|
+
firstHalfNested.forEach((item) => {
|
|
57
|
+
const itemOffsetLeft = item.offsetLeft;
|
|
58
|
+
const itemOffsetWidth = item.offsetWidth;
|
|
59
|
+
if (itemOffsetLeft + itemOffsetWidth > containerWidth - dropdownWidth) {
|
|
60
|
+
item.classList.add('hidden');
|
|
61
|
+
firstHalfNestedHiddenItems.push(item);
|
|
62
|
+
} else {
|
|
63
|
+
item.classList.remove('hidden');
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
secondHalfNested.forEach((item) => item.classList.add('hidden-dropdown'));
|
|
68
|
+
|
|
69
|
+
const diff = firstHalfNested.length - firstHalfNestedHiddenItems.length;
|
|
70
|
+
const secondHalfNestedShownItems = secondHalfNested.slice(diff);
|
|
71
|
+
secondHalfNestedShownItems.forEach((item) =>
|
|
72
|
+
item.classList.remove('hidden-dropdown'),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// If there are elements that should be displayed in the dropdown, show the dropdown button
|
|
76
|
+
if (secondHalfNestedShownItems.length > 0)
|
|
77
|
+
dropdown.classList.remove('hidden-dropdown');
|
|
78
|
+
else {
|
|
79
|
+
dropdown.classList.add('hidden-dropdown');
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleDropdownKeyDown = (event) => {
|
|
84
|
+
const dropdownMenu = document.querySelector('.menu.transition');
|
|
85
|
+
if (event.key === 'ArrowDown' && isDropdownOpen) {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
const menuItems = dropdownMenu.querySelectorAll(
|
|
88
|
+
'.item:not(.hidden-dropdown)',
|
|
89
|
+
);
|
|
90
|
+
const focusedItem = dropdownMenu.querySelector('.item.focused');
|
|
91
|
+
const focusedIndex = Array.from(menuItems).indexOf(focusedItem);
|
|
92
|
+
|
|
93
|
+
if (focusedIndex === -1) {
|
|
94
|
+
// No item is currently focused, so focus the first item
|
|
95
|
+
menuItems[0].classList.add('focused');
|
|
96
|
+
} else if (focusedIndex === menuItems.length - 1) {
|
|
97
|
+
// Remove focus from the currently focused item and close the dropdown
|
|
98
|
+
focusedItem.classList.remove('focused');
|
|
99
|
+
setIsDropdownOpen(false);
|
|
100
|
+
|
|
101
|
+
// Focus the next element on the page
|
|
102
|
+
const nextElement = dropdownMenu.nextElementSibling;
|
|
103
|
+
if (nextElement) {
|
|
104
|
+
nextElement.focus();
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
// Remove focus from the currently focused item
|
|
108
|
+
focusedItem.classList.remove('focused');
|
|
109
|
+
|
|
110
|
+
// Focus the next item or wrap around to the first item
|
|
111
|
+
const nextIndex = (focusedIndex + 1) % menuItems.length;
|
|
112
|
+
menuItems[nextIndex].classList.add('focused');
|
|
113
|
+
}
|
|
114
|
+
} else if (event.key === 'Enter' && isDropdownOpen) {
|
|
115
|
+
const focusedItem = dropdownMenu.querySelector('.item.focused');
|
|
116
|
+
if (focusedItem) {
|
|
117
|
+
focusedItem.querySelector('a').click();
|
|
118
|
+
focusedItem.classList.remove('focused');
|
|
119
|
+
}
|
|
120
|
+
} else if (event.key === 'Tab') {
|
|
121
|
+
const focusedItem = dropdownMenu.querySelector('.item.focused');
|
|
122
|
+
if (focusedItem) {
|
|
123
|
+
focusedItem.classList.remove('focused');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (data.sticky) {
|
|
130
|
+
const toc = document.querySelector('.horizontalMenu');
|
|
131
|
+
const tocPos = toc ? toc.offsetTop : 0;
|
|
132
|
+
|
|
133
|
+
const handleScroll = () => {
|
|
134
|
+
let scrollPos = window.scrollY;
|
|
135
|
+
if (scrollPos > tocPos && toc) {
|
|
136
|
+
toc.classList.add('sticky-toc');
|
|
137
|
+
} else if (scrollPos <= tocPos && toc) {
|
|
138
|
+
toc.classList.remove('sticky-toc');
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
window.addEventListener('scroll', handleScroll);
|
|
143
|
+
|
|
144
|
+
return () => {
|
|
145
|
+
window.removeEventListener('scroll', handleScroll);
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}, [data.sticky]);
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
handleResize();
|
|
152
|
+
window.addEventListener('resize', handleResize);
|
|
153
|
+
|
|
154
|
+
return () => {
|
|
155
|
+
window.removeEventListener('resize', handleResize);
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
|
|
39
159
|
return (
|
|
40
160
|
<>
|
|
41
161
|
{data.title && !data.hide_title ? (
|
|
@@ -50,8 +170,22 @@ const View = ({ data, tocEntries }) => {
|
|
|
50
170
|
) : (
|
|
51
171
|
''
|
|
52
172
|
)}
|
|
53
|
-
<Menu>
|
|
173
|
+
<Menu className="responsive-menu">
|
|
54
174
|
<RenderMenuItems items={tocEntries} />
|
|
175
|
+
<Dropdown
|
|
176
|
+
item
|
|
177
|
+
text="More"
|
|
178
|
+
className="hidden-dropdown"
|
|
179
|
+
open={isDropdownOpen}
|
|
180
|
+
onOpen={() => setIsDropdownOpen(true)}
|
|
181
|
+
onClose={() => setIsDropdownOpen(false)}
|
|
182
|
+
tabIndex={0}
|
|
183
|
+
onKeyDown={handleDropdownKeyDown}
|
|
184
|
+
>
|
|
185
|
+
<Dropdown.Menu>
|
|
186
|
+
<RenderMenuItems items={tocEntries} />
|
|
187
|
+
</Dropdown.Menu>
|
|
188
|
+
</Dropdown>
|
|
55
189
|
</Menu>
|
|
56
190
|
</>
|
|
57
191
|
);
|