-
Notifications
You must be signed in to change notification settings - Fork 4k
/
edit.js
245 lines (222 loc) · 6.6 KB
/
edit.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
/**
* External dependencies
*/
import { isEqual } from 'lodash';
/**
* WordPress dependencies
*/
import {
BlockControls,
BlockIcon,
InspectorControls,
store as blockEditorStore,
useBlockProps,
} from '@wordpress/block-editor';
import { createBlock, store as blocksStore } from '@wordpress/blocks';
import {
PanelBody,
Placeholder,
ToggleControl,
ToolbarButton,
ToolbarGroup,
} from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { renderToString, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { addQueryArgs, removeQueryArgs } from '@wordpress/url';
/**
* Internal dependencies
*/
import icon from './icon';
import TableOfContentsList from './list';
import { linearToNestedHeadingList } from './utils';
/**
* @typedef HeadingData
*
* @property {string} content The plain text content of the heading.
* @property {number} level The heading level.
* @property {string} link Link to the heading.
*/
/**
* Table of Contents block edit component.
*
* @param {Object} props The props.
* @param {Object} props.attributes The block attributes.
* @param {HeadingData[]} props.attributes.headings
* A list of data for each heading in the post.
* @param {boolean} props.attributes.onlyIncludeCurrentPage
* Whether to only include headings from the current page (if the post is
* paginated).
* @param {string} props.clientId
* @param {(attributes: Object) => void} props.setAttributes
*
* @return {WPComponent} The component.
*/
export default function TableOfContentsEdit( {
attributes: { headings = [], onlyIncludeCurrentPage },
clientId,
setAttributes,
} ) {
const blockProps = useBlockProps();
const [ headingTree, setHeadingTree ] = useState( [] );
const listBlockExists = useSelect(
( select ) => !! select( blocksStore ).getBlockType( 'core/list' ),
[]
);
const {
// __unstableMarkNextChangeAsNotPersistent,
replaceBlocks,
} = useDispatch( blockEditorStore );
// The page this block would be part of on the front-end. For performance
// reasons, this is only calculated when onlyIncludeCurrentPage is true.
useSelect(
( select ) => {
const {
getBlockAttributes,
getBlockIndex,
getBlockName,
getBlockOrder,
getGlobalBlockCount,
} = select( blockEditorStore );
const { getPermalink } = select( editorStore );
const isPaginated = getGlobalBlockCount( 'core/nextpage' ) !== 0;
const blockIndex = getBlockIndex( clientId );
const blockOrder = getBlockOrder();
const latestHeadings = [];
// The page (of a paginated post) the Table of Contents block will be
// part of.
let tocPage = 1;
// The page (of a paginated post) a heading will be part of.
let headingPage = 1;
// Link to post including pagination query if necessary.
const permalink = getPermalink();
let headingPageLink = isPaginated
? addQueryArgs( permalink, { page: headingPage } )
: permalink;
for ( const [ i, blockClientId ] of blockOrder.entries() ) {
const blockName = getBlockName( blockClientId );
if ( blockName === 'core/nextpage' ) {
headingPage++;
headingPageLink = addQueryArgs(
removeQueryArgs( permalink, [ 'page' ] ),
{ page: headingPage }
);
if ( i < blockIndex ) {
tocPage++;
}
} else if ( blockName === 'core/heading' ) {
// If we're only including headings from the current page (of a
// paginated post), then exit the loop if we've reached headings
// on the pages after the one with the Table of Contents block.
if ( onlyIncludeCurrentPage && headingPage > tocPage ) {
break;
}
// If we're including all headings or we've reached headings on
// the same page as the Table of Contents block, add them to the
// list.
if ( ! onlyIncludeCurrentPage || headingPage === tocPage ) {
const headingAttributes = getBlockAttributes(
blockClientId
);
const hasAnchor =
typeof headingAttributes.anchor === 'string' &&
headingAttributes.anchor !== '';
latestHeadings.push( {
content: headingAttributes.content,
level: headingAttributes.level,
link: hasAnchor
? `${ headingPageLink }#${ headingAttributes.anchor }`
: null,
// page: headingPage,
} );
}
}
}
if ( ! isEqual( headings, latestHeadings ) ) {
// __unstableMarkNextChangeAsNotPersistent();
setAttributes( { headings: latestHeadings } );
setHeadingTree( linearToNestedHeadingList( latestHeadings ) );
}
}
// ,[
// clientId,
// onlyIncludeCurrentPage,
// // __unstableMarkNextChangeAsNotPersistent,
// ]
);
const toolbarControls = listBlockExists && (
<BlockControls>
<ToolbarGroup>
<ToolbarButton
onClick={ () =>
replaceBlocks(
clientId,
createBlock( 'core/list', {
values: renderToString(
<TableOfContentsList
nestedHeadingList={ headingTree }
/>
),
} )
)
}
>
{ __( 'Convert to static list' ) }
</ToolbarButton>
</ToolbarGroup>
</BlockControls>
);
const inspectorControls = (
<InspectorControls>
<PanelBody title={ __( 'Table of Contents settings' ) }>
<ToggleControl
label={ __( 'Only include current page' ) }
checked={ onlyIncludeCurrentPage }
onChange={ ( value ) =>
setAttributes( { onlyIncludeCurrentPage: value } )
}
help={
onlyIncludeCurrentPage
? __(
'Only including headings from the current page (if the post is paginated).'
)
: __(
'Toggle to only include headings from the current page (if the post is paginated).'
)
}
/>
</PanelBody>
</InspectorControls>
);
// If there are no headings or the only heading is empty.
// Note that the toolbar controls are intentionally omitted since the
// "Convert to static list" option is useless to the placeholder state.
if ( headings.length === 0 ) {
return (
<>
<div { ...blockProps }>
<Placeholder
icon={ <BlockIcon icon={ icon } /> }
label="Table of Contents"
instructions={ __(
'Start adding Heading blocks to create a table of contents. Headings with HTML anchors will be linked here.'
) }
/>
</div>
{ inspectorControls }
</>
);
}
return (
<>
<nav { ...blockProps }>
<ul>
<TableOfContentsList nestedHeadingList={ headingTree } />
</ul>
</nav>
{ toolbarControls }
{ inspectorControls }
</>
);
}