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 a Table of Contents block #2871

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions blocks/editable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ export default class Editable extends Component {
const isPlaceholderVisible = placeholder && ( ! focus || keepPlaceholderOnFocus ) && this.state.empty;
const classes = classnames( wrapperClassname, 'blocks-editable' );

const formatToolbar = (
const formatToolbar = formattingControls && (
<FormatToolbar
selectedNodeId={ this.state.selectedNodeId }
focusPosition={ this.state.focusPosition }
Expand All @@ -625,12 +625,12 @@ export default class Editable extends Component {

return (
<div className={ classes }>
{ focus &&
{ focus && formatToolbar &&
<Fill name="Formatting.Toolbar">
{ ! inlineToolbar && formatToolbar }
</Fill>
}
{ focus && inlineToolbar &&
{ focus && inlineToolbar && formatToolbar &&
<div className="block-editable__inline-toolbar">
{ formatToolbar }
</div>
Expand Down
1 change: 1 addition & 0 deletions blocks/library/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ import './text-columns';
import './verse';
import './video';
import './audio';
import './table-of-contents';
199 changes: 199 additions & 0 deletions blocks/library/table-of-contents/class-block-table-of-contents.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<?php
/**
* Table of Contents block.
*
* @package gutenberg
*/

/**
* Handles rendering the table of contents.
*/
class Block_Table_Of_Contents {
/**
* Cache of TOC placeholder IDs.
*
* @var array $ids
*/
private $ids = array();

/**
* Cache of TOC Titles.
*
* @var array $titles
*/
private $titles = array();

/**
* Cache of TOC "numbered" settings.
*
* @var array $numbered
*/
private $numbered = array();

/**
* Cache of heading blocks found.
*
* @var array $headings
*/
private $headings = array();

/**
* Constructor.
*/
function __construct() {
register_block_type( 'core/table-of-contents', array(
'attributes' => array(
'title' => array(
'type' => 'string',
'default' => __( 'Table of Contents', 'gutenberg' ),
),
'numbered' => array(
'type' => 'bool',
'default' => true,
),
),
'render_callback' => array( $this, 'add_placeholder' ),
) );

add_filter( 'raw_block_content', array( $this, 'add_id_to_heading_blocks' ), 10, 2 );
add_filter( 'the_content', array( $this, 'insert_toc' ) );
}

/**
* Initialises an instance of the class.
*
* @return Block_Table_Of_Contents Instance of the class.
*/
static public function init() {
static $instance;
if ( ! $instance ) {
$instance = new Block_Table_Of_Contents();
}
return $instance;
}

/**
* Adds a placeholder for the Table of Contents to be rendered into, after
* all of the blocks have been processed.
*
* @param array $attributes The block attributes.
*
* @return string The placeholder.
*/
public function add_placeholder( $attributes ) {
$toc_id = '<!-- ' . uniqid( 'toc-', true ) . ' -->';

$this->ids[] = $toc_id;
$this->titles[] = $attributes['title'];
$this->numbered[] = $attributes['numbered'];

return $toc_id;
}

/**
* Replaces the Table of Contents placeholders with the actual Table of Contents
*
* @param string $content The HTML content of the post.
*
* @return string The post HTML, with Table of Contents inserted.
*/
public function insert_toc( $content ) {
foreach ( $this->ids as $count => $id ) {
$title = $this->titles[ $count ];
$numbered = $this->numbered[ $count ];

$html = "<h2>$title</h2>";

if ( ! $this->headings ) {
$html .= '<p><em>' . __( 'Empty', 'gutenberg' ) . '</e></p>';
$content = str_replace( $id, $html, $content );
continue;
}

$html .= '<ul class="wp-block-table-of-contents">';

$level_counts = array(
1 => 0,
2 => 0,
3 => 0,
4 => 0,
5 => 0,
6 => 0,
);

foreach ( $this->headings as $heading ) {
$level_string = '';
if ( $numbered ) {
$level_counts[ $heading['level'] ]++;
for ( $ii = $heading['level'] + 1; $ii <= 6; $ii++ ) {
$level_counts[ $ii ] = 0;
}
$level_string = $this->create_chapter_string( $level_counts, $heading['level'] ) . ' ';
}

$html .= "<li class='level{$heading['level']}'>$level_string<a href='#{$heading['id']}'>{$heading['heading']}</a></li>";
}

$content = str_replace( $id, $html, $content );
}

return $content;
}

/**
* Creates a TOC chapter string, based on where the parser is currently up to.
*
* @param array $level_counts The state of each chapter level.
* @param int $level The currently chapter's level.
*
* @return string The chapter string.
*/
private function create_chapter_string( $level_counts, $level ) {
$string = '';
for ( $ii = 2; $ii <= $level; $ii++ ) {
$string .= $level_counts[ $ii ];
if ( $ii != $level ) {
$string .= '.';
}
}
return $string;
}

/**
* When a heading block is processed, we need to add an ID attribute, so we can link to it.
*
* @param string $content The raw content of the block being processed.
* @param string $block_name The Block Name of the block being processed.
*
* @return string The HTML to replace $content with.
*/
public function add_id_to_heading_blocks( $content, $block_name ) {
if ( 'core/heading' !== $block_name ) {
return $content;
}

return preg_replace_callback( '|^(\s*)<h([1-6])>(.+)</h\2>|i', array( $this, 'add_id_to_heading_blocks_callback' ), $content );
}

/**
* Internal callback for add an ID to headers.
*
* @see Gutenberg_Table_of_Contents::add_id_to_heading_blocks()
*
* @param array $matches Array of matches.
*
* @return string The replacement string to use.
*/
private function add_id_to_heading_blocks_callback( $matches ) {
$heading = trim( wp_strip_all_tags( $matches[3], true ) );
$id = 'heading-' . preg_replace( '/[^a-z0-9_]+/i', '-', $heading );

$this->headings[] = array(
'level' => $matches[2],
'heading' => $heading,
'id' => $id,
);

return "{$matches[1]}<h{$matches[2]} id='$id'>{$matches[3]}</h{$matches[2]}>";
}
}
2 changes: 2 additions & 0 deletions blocks/library/table-of-contents/editor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
div[data-type="core/table-of-contents"] {
}
74 changes: 74 additions & 0 deletions blocks/library/table-of-contents/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* WordPress
*/
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import './editor.scss';
import './style.scss';
import { registerBlockType } from '../../api';
import Editable from '../../editable';
import InspectorControls from '../../inspector-controls';
import ToggleControl from '../../inspector-controls/toggle-control';
import BlockDescription from '../../block-description';

registerBlockType( 'core/table-of-contents', {
title: __( 'Table of Contents' ),

icon: 'list-view',

category: 'widgets',

attributes: {
title: {
type: 'string',
default: __( 'Table of Contents' ),
},
numbered: {
type: 'bool',
default: true,
},
},

edit( { attributes, setAttributes, focus, setFocus } ) {
const { title, numbered } = attributes;
return [
focus && (
<InspectorControls key="inspector">
<BlockDescription>
<p>{ __( 'This is a table of contents, y\'all' ) }</p>
</BlockDescription>

<h3>{ __( 'Table of Contents Settings' ) }</h3>

<ToggleControl
label={ __( 'Display chapter numbering' ) }
checked={ numbered }
onChange={
() => setAttributes( {
numbered: ! numbered,
} )
}
/>
</InspectorControls>
),
<Editable
key="editable"
tagName="h2"
value={ [ title ] }
focus={ focus }
onFocus={ setFocus }
onChange={ ( value ) => setAttributes( { title: value[ 0 ] } ) }
formattingControls={ false }
multiline={ false }
/>,
__( 'Here shall render ye table of contents' ),
];
},

save() {
return null;
},
} );
10 changes: 10 additions & 0 deletions blocks/library/table-of-contents/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
/**
* Table of Contents block.
*
* @package gutenberg
*/

// Load the Table of Contents class.
require_once dirname( __FILE__ ) . '/class-block-table-of-contents.php';
add_action( 'init', array( 'Block_Table_Of_Contents', 'init' ) );
28 changes: 28 additions & 0 deletions blocks/library/table-of-contents/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.wp-block-table-of-contents {
list-style: none;

.level1 {
margin-left: 10px;
}

.level2 {
margin-left: 20px;
}

.level3 {
margin-left: 30px;
}

.level4 {
margin-left: 40px;
}

.level5 {
margin-left: 50px;
}

.level6 {
margin-left: 60px;
}

}
1 change: 1 addition & 0 deletions blocks/test/fixtures/core__table-of-contents.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!-- wp:core/table-of-contents {"title":"Table of Contents"} /-->
12 changes: 12 additions & 0 deletions blocks/test/fixtures/core__table-of-contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"uid": "_uid_0",
"name": "core/table-of-contents",
"isValid": true,
"attributes": {
"title": "Table of Contents",
"numbered": true
},
"originalContent": ""
}
]
9 changes: 9 additions & 0 deletions blocks/test/fixtures/core__table-of-contents.parsed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"blockName": "core/table-of-contents",
"attrs": {
"title": "Table of Contents"
},
"rawContent": ""
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!-- wp:core/table-of-contents /-->
9 changes: 8 additions & 1 deletion lib/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,14 @@ function do_blocks( $content ) {
}

if ( $raw_content ) {
$content_after_blocks .= $raw_content;
/**
* Filters the raw HTML produced by an individual block.
*
* @param string $raw_content The raw HTML produced by the block.
* @param string $block_name The block name.
* @param array $attributes The block's attributes.
*/
$content_after_blocks .= apply_filters( 'raw_block_content', $raw_content, $block_name, $attributes );
}
}

Expand Down