Table of Contents block

See Building a Block with Advanced Custom Fields for more information.

acf-blocks.php

<?php
/**
 * ACF Blocks
 *
 * @package      ClientName
 * @author       Bill Erickson
 * @since        1.0.0
 * @license      GPL-2.0+
**/
/**
 * Register ACF Blocks
 * @link https://www.advancedcustomfields.com/resources/acf_register_block/
 */
function ea_register_acf_blocks() {
	// check function exists
	if( ! function_exists('acf_register_block_type') )
		return;
	acf_register_block_type( array(
		'name'			=> 'table-of-contents',
		'title'			=> __( 'Table of Contents', 'client-name' ),
		'description'		=> '',
		'render_template'	=> 'partials/block-table-of-contents.php',
		'category'		=> 'widgets',
		'icon'			=> 'list-view',
	));
}
add_action( 'acf/init', 'ea_register_acf_blocks' );

/**
 * Table of Contents
 *
 */
function ea_table_of_contents( $count = false ) {
	global $post;
	$content = apply_filters( 'ea_anchor_header', $post->post_content );
	$doc = new DOMDocument();
	// START LibXML error management.
	// Modify state
	$libxml_previous_state = libxml_use_internal_errors( true );
	$doc->loadHTML( mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' ) );
	// handle errors
	libxml_clear_errors();
	// restore
	libxml_use_internal_errors( $libxml_previous_state );
	// END LibXML error management.
	$headings = $doc->getElementsByTagName( 'h2' );
	$output = array();
	foreach( $headings as $heading ) {
		$output[] = '<li><a href="#' . sanitize_title( $heading->nodeValue ) . '">' . $heading->nodeValue . '</a></li>';
	}
	if( empty( $output ) )
		return;
	if( false !== $count )
		$output = array_slice( $output, 0, $count );
	echo '<div class="table-of-contents">';
	echo '<ol>' . join( PHP_EOL, $output ) . '</ol>';
	echo '</div>';
}

block-table-of-contents.php

<?php
/**
 * Block Name: Table of Contents
 *
 * @package      ClientName
 * @author       Bill Erickson
 * @since        1.0.0
 * @license      GPL-2.0+
**/



if( is_admin() ) {
	echo '<div class="table-of-contents admin">Table of Contents will appear here</div>';
} else {
	ea_table_of_contents();
}

anchor-header.php

<?php
/**
 * Anchor Header
 *
 * @package      CoreFunctionality
 * @author       Bill Erickson
 * @since        1.0.0
 * @license      GPL-2.0+
**/

if ( defined( 'ABSPATH' ) ) {
	EA_Anchor_Header::instance();
}


class EA_Anchor_Header {

	/**
	 * Refers to a single instance of this class
	 *
	 * @var null
	 */
	private static $instance = null;

	/**
	 * Creates or returns an instance of this class.
	 *
	 * @since 0.1.8
	 * @return object EA_Anchor_Header, a single instance of this class.
	 */
	public static function instance() {
		if ( null == self::$instance ) {
			self::$instance = new self;
		}
		return self::$instance;
	}

	/**
	 * Load style and attach $this->the_content to the the_content filter
	 *
	 * @since 0.1.0
	 */
	function __construct() {
		add_filter( 'the_content', array( $this, 'the_content' ) );
		add_filter( 'ea_anchor_header', array( $this, 'the_content' ) );
	}

	/**
	 * Using DOMDocument, parse the content and add anchors to headers (H1-H6)
	 *
	 * @since 0.1.0
	 *
	 * @param string  $content The content
	 * @return string          the content, updated if the content has H1-H6
	 */
	function the_content( $content ) {

		if ( ! is_singular() || '' == $content ) {
			return $content;
		}

		global $post;
		if( false === strpos( $post->post_content, 'wp:acf/table-of-contents' ) )
			return $content;

		$anchors = array();
		$doc = new DOMDocument();
		// START LibXML error management.
		// Modify state
		$libxml_previous_state = libxml_use_internal_errors( true );
		$doc->loadHTML( mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' ) );
		// handle errors
		libxml_clear_errors();
		// restore
		libxml_use_internal_errors( $libxml_previous_state );
		// END LibXML error management.

//		foreach ( array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ) as $h ) {
//			$headings = $doc->getElementsByTagName( $h );
			$headings = $doc->getElementsByTagName( 'h2' );
			foreach ( $headings as $heading ) {
				$a = $doc->createElement( 'a' );
				$newnode = $heading->appendChild( $a );
				$newnode->setAttribute( 'class', 'anchorlink' );
				// @codingStandardsIgnoreStart
				// $heading->nodeValue is from an external libray. Ignore the standard check sinice it doesn't fit the WordPress-Core standard

				$slug = $heading->getAttribute( 'id' );
				if( empty( $slug ) ) {
					$slug = $tmpslug = sanitize_title( $heading->nodeValue );
					// @codingStandardsIgnoreEnd
					$i = 2;
					while ( false !== in_array( $slug, $anchors ) ) {
						$slug = sprintf( '%s-%d', $tmpslug, $i++ );
					}
					$heading->setAttribute( 'id', $slug );
				}
				$anchors[] = $slug;
				$newnode->setAttribute( 'href', '#' . $slug );
			}
//		}
		return $doc->saveHTML();
	}

} // class EA_Anchor_Header

Bill Erickson

Bill Erickson is a freelance WordPress developer and a contributing developer to the Genesis framework. For the past 14 years he has worked with attorneys, publishers, corporations, and non-profits, building custom websites tailored to their needs and goals.

Ready to upgrade your website?

I build custom WordPress websites that look great and are easy to manage.

Let's Talk