-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add word and block count to table of contents #2684
Changes from 4 commits
17eed7a
a43bb7a
3b390a6
d1c5002
493d97a
09dca0a
e9c630e
e51468a
7e3cc89
671ddf0
88dae46
c739a20
7c35f64
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { connect } from 'react-redux'; | ||
import { filter } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { __ } from '@wordpress/i18n'; | ||
import { Dashicon, Popover } from '@wordpress/components'; | ||
import { Component } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import './style.scss'; | ||
import TableOfContentsItem from './item'; | ||
import WordCount from '../word-count'; | ||
import { getBlocks } from '../selectors'; | ||
import { selectBlock } from '../actions'; | ||
|
||
/** | ||
* Module constants | ||
*/ | ||
const emptyHeadingContent = <em>{ __( '(Empty heading)' ) }</em>; | ||
const incorrectLevelContent = [ | ||
<br key="incorrect-break" />, | ||
<em key="incorrect-message">{ __( '(Incorrect heading level)' ) }</em>, | ||
]; | ||
|
||
const getHeadingLevel = heading => { | ||
switch ( heading.attributes.nodeName ) { | ||
case 'h1': | ||
case 'H1': | ||
return 1; | ||
case 'h2': | ||
case 'H2': | ||
return 2; | ||
case 'h3': | ||
case 'H3': | ||
return 3; | ||
case 'h4': | ||
case 'H4': | ||
return 4; | ||
case 'h5': | ||
case 'H5': | ||
return 5; | ||
case 'h6': | ||
case 'H6': | ||
return 6; | ||
} | ||
}; | ||
|
||
const isEmptyHeading = heading => ! heading.attributes.content || heading.attributes.content.length === 0; | ||
|
||
class TableOfContents extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
this.state = { | ||
showPopover: false, | ||
}; | ||
} | ||
|
||
render() { | ||
const { blocks, onSelect } = this.props; | ||
const headings = filter( blocks, ( block ) => block.name === 'core/heading' ); | ||
|
||
if ( headings.length <= 1 ) { | ||
return null; | ||
} | ||
|
||
let prevHeadingLevel = 1; | ||
|
||
// Select the corresponding block in the main editor | ||
// when clicking on a heading item from the list. | ||
const onSelectHeading = ( uid ) => onSelect( uid ); | ||
|
||
const tocItems = headings.map( ( heading, index ) => { | ||
const headingLevel = getHeadingLevel( heading ); | ||
const isEmpty = isEmptyHeading( heading ); | ||
|
||
// Headings remain the same, go up by one, or down by any amount. | ||
// Otherwise there are missing levels. | ||
const isIncorrectLevel = headingLevel > prevHeadingLevel + 1; | ||
|
||
const isValid = ( | ||
! isEmpty && | ||
! isIncorrectLevel && | ||
headingLevel | ||
); | ||
|
||
prevHeadingLevel = headingLevel; | ||
|
||
return ( | ||
<TableOfContentsItem | ||
key={ index } | ||
level={ headingLevel } | ||
isValid={ isValid } | ||
onClick={ () => onSelectHeading( heading.uid ) } | ||
> | ||
{ isEmpty ? emptyHeadingContent : heading.attributes.content } | ||
{ isIncorrectLevel && incorrectLevelContent } | ||
</TableOfContentsItem> | ||
); | ||
} ); | ||
|
||
return ( | ||
<div className="table-of-contents"> | ||
<button | ||
className="table-of-contents__toggle" | ||
onClick={ () => this.setState( { showPopover: ! this.state.showPopover } ) } | ||
> | ||
<Dashicon icon="marker" /> { __( 'Info' ) } | ||
</button> | ||
<Popover | ||
isOpen={ this.state.showPopover } | ||
position="bottom" | ||
className="table-of-contents__popover" | ||
> | ||
<div className="table-of-contents__counts"> | ||
<div className="table-of-contents__count"> | ||
<WordCount /> | ||
{ __( 'Word Count' ) } | ||
</div> | ||
<div className="table-of-contents__count"> | ||
<span className="table-of-contents__number">{ blocks.length }</span> | ||
{ __( 'Blocks' ) } | ||
</div> | ||
<div className="table-of-contents__count"> | ||
<span className="table-of-contents__number">{ headings.length }</span> | ||
{ __( 'Headings' ) } | ||
</div> | ||
</div> | ||
<hr /> | ||
<span className="table-of-contents__title">{ __( 'Table of Contents' ) }</span> | ||
<div className="table-of-contents__items"> | ||
<ul>{ tocItems }</ul> | ||
</div> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe we could split out the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Document Outline may fit. |
||
</Popover> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
export default connect( | ||
( state ) => { | ||
return { | ||
blocks: getBlocks( state ), | ||
}; | ||
}, | ||
{ | ||
onSelect( uid ) { | ||
return selectBlock( uid ); | ||
}, | ||
} | ||
)( TableOfContents ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd never expect
nodeName
to be lowercase. Why were these additional cases added originally? cc @sirreal re: #1916There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't recall actually encountering lowercase
nodeName
s, I believe this was just some defensive coding that seemed to have minimal cost.