Skip to content
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

Merged
merged 13 commits into from
Sep 25, 2017
Merged
2 changes: 2 additions & 0 deletions editor/modes/visual-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import './style.scss';
import VisualEditorBlockList from './block-list';
import PostTitle from '../../post-title';
import WritingFlow from '../../writing-flow';
import TableOfContents from '../../table-of-contents';
import { getBlockUids, getMultiSelectedBlockUids } from '../../selectors';
import { clearSelectedBlock, multiSelect, redo, undo, removeBlocks } from '../../actions';

Expand Down Expand Up @@ -103,6 +104,7 @@ class VisualEditor extends Component {
<PostTitle />
<VisualEditorBlockList ref={ this.bindBlocksContainer } />
</WritingFlow>
<TableOfContents />
</div>
);
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */
Expand Down
2 changes: 0 additions & 2 deletions editor/sidebar/post-settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import PostTaxonomies from '../post-taxonomies';
import FeaturedImage from '../featured-image';
import DiscussionPanel from '../discussion-panel';
import LastRevision from '../last-revision';
import TableOfContents from '../table-of-contents';
import PageAttributes from '../page-attributes';

const panel = (
Expand All @@ -25,7 +24,6 @@ const panel = (
<PostExcerpt />
<DiscussionPanel />
<PageAttributes />
<TableOfContents />
</Panel>
);

Expand Down
123 changes: 0 additions & 123 deletions editor/sidebar/table-of-contents/index.js

This file was deleted.

157 changes: 157 additions & 0 deletions editor/table-of-contents/index.js
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':
Copy link
Member

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: #1916

Copy link
Member

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 nodeNames, I believe this was just some defensive coding that seemed to have minimal cost.

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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we could split out the TableOfContent to its own subcomponent and rename this one PostInfo or something?

Copy link
Member Author

Choose a reason for hiding this comment

The 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 );
Original file line number Diff line number Diff line change
@@ -1,3 +1,62 @@
.table-of-contents {
position: fixed;
right: 16px;
top: 100px;

.is-sidebar-opened & {
right: $sidebar-width + 16px;
}
}

.table-of-contents__toggle {
background: none;
border: none;
box-shadow: none;
color: $dark-gray-300;
cursor: pointer;
font-family: $default-font;
font-size: 12px;

.dashicon {
width: 20px;
height: 20px;
position: relative;
top: 5px;
}
}

.table-of-contents__popover .components-popover__content {
padding: 16px;
}

.table-of-contents__counts {
display: flex;
flex-wrap: wrap;
margin-bottom: 10px;
}

.table-of-contents__count {
width: 50%;
display: flex;
flex-direction: column;
margin-bottom: 10px;
}

.table-of-contents__number,
.table-of-contents__popover .word-count {
font-size: 40px;
font-weight: 100;
line-height: 50px;
color: $dark-gray-300;
}

.table-of-contents__title {
display: block;
margin-top: 20px;
font-size: 15px;
font-weight: 600;
}

.table-of-contents__items {
margin: 20px 0;
}
Expand Down
Loading