Commit realizado el 12:13:52 08-04-2024
This commit is contained in:
@@ -0,0 +1 @@
|
||||
.wp-block-rank-math-toc-block nav li,.wp-block-rank-math-toc-block nav div{position:relative;min-height:28px;margin-bottom:0}.wp-block-rank-math-toc-block nav li.disabled,.wp-block-rank-math-toc-block nav div.disabled{display:block !important;opacity:0.5}.wp-block-rank-math-toc-block nav li .components-base-control,.wp-block-rank-math-toc-block nav div .components-base-control{position:absolute;top:2px;left:-4px;right:-3px}.wp-block-rank-math-toc-block nav li .rank-math-block-actions,.wp-block-rank-math-toc-block nav div .rank-math-block-actions{position:absolute;top:1px;right:0;display:none;line-height:1}.wp-block-rank-math-toc-block nav li .rank-math-block-actions button.components-button,.wp-block-rank-math-toc-block nav div .rank-math-block-actions button.components-button{min-width:24px;width:24px;height:24px;line-height:34px}.wp-block-rank-math-toc-block nav li:hover .rank-math-block-actions,.wp-block-rank-math-toc-block nav li:focus .rank-math-block-actions,.wp-block-rank-math-toc-block nav li .components-base-control+.rank-math-block-actions .rank-math-block-actions,.wp-block-rank-math-toc-block nav div:hover .rank-math-block-actions,.wp-block-rank-math-toc-block nav div:focus .rank-math-block-actions,.wp-block-rank-math-toc-block nav div .components-base-control+.rank-math-block-actions .rank-math-block-actions{display:block}.rank-math-toc-exclude-headings{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.rank-math-toc-exclude-headings>div{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;margin-bottom:10px !important}
|
@@ -0,0 +1 @@
|
||||
.wp-block-rank-math-toc-block nav ol{counter-reset:item}.wp-block-rank-math-toc-block nav ol li{display:block}.wp-block-rank-math-toc-block nav ol li:before{content:counters(item, ".") ". ";counter-increment:item}
|
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* Block script dependencies.
|
||||
*
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Schema
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
return [
|
||||
'dependencies' => [
|
||||
'wp-blocks',
|
||||
'wp-element',
|
||||
'wp-components',
|
||||
'wp-block-editor',
|
||||
'wp-data',
|
||||
'wp-dom',
|
||||
'wp-url',
|
||||
'wp-i18n',
|
||||
'lodash',
|
||||
'wp-primitives',
|
||||
'wp-reusable-blocks',
|
||||
],
|
||||
'version' => rank_math()->version,
|
||||
];
|
File diff suppressed because one or more lines are too long
@@ -0,0 +1,52 @@
|
||||
.wp-block-rank-math-toc-block {
|
||||
nav {
|
||||
li, div {
|
||||
position: relative;
|
||||
min-height: 28px;
|
||||
margin-bottom: 0;
|
||||
|
||||
&.disabled {
|
||||
display: block !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.components-base-control {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: -4px;
|
||||
right: -3px;
|
||||
}
|
||||
|
||||
.rank-math-block-actions {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 0;
|
||||
display: none;
|
||||
line-height: 1;
|
||||
|
||||
button.components-button {
|
||||
min-width: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &:focus, .components-base-control + .rank-math-block-actions {
|
||||
.rank-math-block-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rank-math-toc-exclude-headings {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> div {
|
||||
flex: 0 0 50%;
|
||||
margin-bottom: 10px!important;
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
.wp-block-rank-math-toc-block {
|
||||
nav {
|
||||
ol {
|
||||
counter-reset: item;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
|
||||
li:before {
|
||||
content: counters(item, ".") ". ";
|
||||
counter-increment: item;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
import { linearToNestedHeadingList } from './utils'
|
||||
import { useBlockProps } from '@wordpress/block-editor'
|
||||
import List from './list'
|
||||
|
||||
const attributes = {
|
||||
title: {
|
||||
type: 'text',
|
||||
},
|
||||
headings: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
listStyle: {
|
||||
type: 'text',
|
||||
},
|
||||
titleWrapper: {
|
||||
type: 'text',
|
||||
default: 'h2',
|
||||
},
|
||||
excludeHeadings: {
|
||||
type: 'array',
|
||||
},
|
||||
}
|
||||
|
||||
const v1 = {
|
||||
attributes,
|
||||
save( { attributes } ) {
|
||||
if ( attributes.headings.length === 0 ) {
|
||||
return null
|
||||
}
|
||||
|
||||
const TitleWrapper = attributes.titleWrapper
|
||||
const headings = linearToNestedHeadingList( attributes.headings )
|
||||
const ListStyle = attributes.listStyle
|
||||
|
||||
return (
|
||||
<div { ...useBlockProps.save() }>
|
||||
{ attributes.title && <TitleWrapper dangerouslySetInnerHTML={ { __html: attributes.title } }></TitleWrapper> }
|
||||
<nav>
|
||||
<ListStyle>
|
||||
<List
|
||||
headings={ headings }
|
||||
ListStyle={ ListStyle }
|
||||
isSave={ true }
|
||||
/>
|
||||
</ListStyle>
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default [ v1 ]
|
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isUndefined, map, includes, remove } from 'lodash'
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import {
|
||||
useBlockProps,
|
||||
RichText,
|
||||
store as blockEditorStore,
|
||||
} from '@wordpress/block-editor'
|
||||
import { useDispatch } from '@wordpress/data'
|
||||
import { Placeholder } from '@wordpress/components'
|
||||
import { useEffect, useState } from '@wordpress/element'
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { GetLatestHeadings, linearToNestedHeadingList } from './utils'
|
||||
import List from './list'
|
||||
import InspectControls from './inspectControls'
|
||||
import Toolbar from './toolbar'
|
||||
|
||||
export default ( {
|
||||
attributes,
|
||||
setAttributes,
|
||||
} ) => {
|
||||
const blockProps = useBlockProps()
|
||||
|
||||
// State to monitor edit heading links.
|
||||
const [ edit, toggleEdit ] = useState( false )
|
||||
const [ excludeHeading, toggleExcludeHeading ] = useState( {} )
|
||||
if ( ! attributes.listStyle ) {
|
||||
setAttributes( { listStyle: rankMath.listStyle } )
|
||||
}
|
||||
|
||||
const ListStyle = attributes.listStyle
|
||||
const tocTitle = attributes.title ?? rankMath.tocTitle
|
||||
const excludeHeadings = ! isUndefined( attributes.excludeHeadings ) ? attributes.excludeHeadings : rankMath.tocExcludeHeadings
|
||||
|
||||
// Function to hide certain heading.
|
||||
const hideHeading = ( value, key ) => {
|
||||
const headings = map( attributes.headings, ( heading ) => {
|
||||
if ( heading.key === key ) {
|
||||
heading.disable = value
|
||||
}
|
||||
|
||||
return heading
|
||||
} )
|
||||
|
||||
setAttributes( { headings } )
|
||||
}
|
||||
|
||||
// Function to update Heading link.
|
||||
const onHeadingUpdate = ( value, key, isContent = false ) => {
|
||||
const headings = map( attributes.headings, ( heading ) => {
|
||||
if ( heading.key === key ) {
|
||||
if ( isContent ) {
|
||||
heading.content = value
|
||||
heading.isUpdated = true
|
||||
} else {
|
||||
heading.isGeneratedLink = false
|
||||
heading.link = value
|
||||
}
|
||||
}
|
||||
|
||||
return heading
|
||||
} )
|
||||
|
||||
setAttributes( { headings } )
|
||||
}
|
||||
|
||||
const setExcludeHeadings = ( headingLevel ) => {
|
||||
if ( includes( excludeHeadings, headingLevel ) ) {
|
||||
remove( excludeHeadings, ( heading ) => {
|
||||
return heading === headingLevel
|
||||
} )
|
||||
} else {
|
||||
excludeHeadings.push( headingLevel )
|
||||
}
|
||||
setAttributes( { excludeHeadings } )
|
||||
toggleExcludeHeading( ! excludeHeading )
|
||||
}
|
||||
|
||||
const { __unstableMarkNextChangeAsNotPersistent } = useDispatch( blockEditorStore )
|
||||
|
||||
// Get Latest headings from the content.
|
||||
const latestHeadings = GetLatestHeadings( attributes.headings, excludeHeadings )
|
||||
useEffect( () => {
|
||||
if ( latestHeadings !== null ) {
|
||||
__unstableMarkNextChangeAsNotPersistent();
|
||||
setAttributes( { headings: latestHeadings } )
|
||||
}
|
||||
}, [ latestHeadings ] )
|
||||
|
||||
const headingTree = linearToNestedHeadingList( attributes.headings )
|
||||
if ( isUndefined( attributes.headings ) || attributes.headings.length === 0 ) {
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<Placeholder
|
||||
label={ __( 'Table of Contents', 'rank-math' ) }
|
||||
instructions={ __( 'Add Heading blocks to this page to generate the Table of Contents.', 'rank-math' ) }
|
||||
/>
|
||||
<InspectControls attributes={ attributes } setAttributes={ setAttributes } excludeHeadings={ excludeHeadings } setExcludeHeadings={ setExcludeHeadings } />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<RichText
|
||||
tagName={ attributes.titleWrapper }
|
||||
value={ tocTitle }
|
||||
onChange={ ( newTitle ) => {
|
||||
setAttributes( { title: newTitle } )
|
||||
} }
|
||||
placeholder={ __( 'Enter a title', 'rank-math' ) }
|
||||
/>
|
||||
<nav>
|
||||
<ListStyle>
|
||||
<List
|
||||
headings={ headingTree }
|
||||
onHeadingUpdate={ onHeadingUpdate }
|
||||
edit={ edit }
|
||||
toggleEdit={ toggleEdit }
|
||||
hideHeading={ hideHeading }
|
||||
ListStyle={ ListStyle }
|
||||
/>
|
||||
</ListStyle>
|
||||
</nav>
|
||||
<Toolbar setAttributes={ setAttributes } />
|
||||
<InspectControls attributes={ attributes } setAttributes={ setAttributes } excludeHeadings={ excludeHeadings } setExcludeHeadings={ setExcludeHeadings } />
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { registerBlockType } from '@wordpress/blocks'
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import edit from './edit'
|
||||
import save from './save'
|
||||
import deprecated from './deprecated'
|
||||
|
||||
/**
|
||||
* Register TOC block
|
||||
*/
|
||||
registerBlockType(
|
||||
'rank-math/toc-block',
|
||||
{
|
||||
edit,
|
||||
save,
|
||||
deprecated,
|
||||
}
|
||||
)
|
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { map, includes, toUpper } from 'lodash'
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { InspectorControls } from '@wordpress/block-editor'
|
||||
import {
|
||||
PanelBody,
|
||||
SelectControl,
|
||||
CheckboxControl,
|
||||
} from '@wordpress/components'
|
||||
|
||||
export default ( { attributes, setAttributes, excludeHeadings, setExcludeHeadings } ) => {
|
||||
return (
|
||||
<InspectorControls>
|
||||
<PanelBody title={ __( 'Settings', 'rank-math' ) }>
|
||||
|
||||
<SelectControl
|
||||
label={ __( 'Title Wrapper', 'rank-math' ) }
|
||||
value={ attributes.titleWrapper }
|
||||
options={ [
|
||||
{ value: 'h2', label: __( 'H2', 'rank-math' ) },
|
||||
{ value: 'h3', label: __( 'H3', 'rank-math' ) },
|
||||
{ value: 'h4', label: __( 'H4', 'rank-math' ) },
|
||||
{ value: 'h5', label: __( 'H5', 'rank-math' ) },
|
||||
{ value: 'h6', label: __( 'H6', 'rank-math' ) },
|
||||
{ value: 'p', label: __( 'P', 'rank-math' ) },
|
||||
{ value: 'div', label: __( 'DIV', 'rank-math' ) },
|
||||
] }
|
||||
onChange={ ( titleWrapper ) => {
|
||||
setAttributes( { titleWrapper } )
|
||||
} }
|
||||
/>
|
||||
|
||||
<br />
|
||||
<h3>{ __( 'Exclude Headings', 'rank-math' ) }</h3>
|
||||
<div className="rank-math-toc-exclude-headings">
|
||||
{
|
||||
map( [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ], ( value ) => {
|
||||
return (
|
||||
<CheckboxControl
|
||||
key={ value }
|
||||
label={ __( 'Heading ', 'rank-math' ) + toUpper( value ) }
|
||||
checked={ includes( excludeHeadings, value ) }
|
||||
onChange={ ( newVlaue ) => setExcludeHeadings( value, newVlaue ) }
|
||||
/>
|
||||
)
|
||||
} )
|
||||
}
|
||||
</div>
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
)
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { Button, TextControl } from '@wordpress/components'
|
||||
import { RichText } from '@wordpress/block-editor'
|
||||
|
||||
export default function List( { headings = {}, onHeadingUpdate = {}, edit = {}, toggleEdit = {}, hideHeading = {}, ListStyle = 'ul', isSave = false } ) {
|
||||
if ( isEmpty( headings ) ) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ headings.map( ( heading ) => {
|
||||
if ( isSave && heading.heading.disable ) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { content, link, disable, key } = heading.heading
|
||||
const TagName = 'div' === ListStyle ? 'div' : 'li'
|
||||
return (
|
||||
<TagName key={ key } className={ disable ? 'disabled' : '' }>
|
||||
{
|
||||
isSave &&
|
||||
<a href={ link }>
|
||||
{ content }
|
||||
</a>
|
||||
}
|
||||
{
|
||||
! isSave &&
|
||||
<RichText
|
||||
tagName="a"
|
||||
value={ content }
|
||||
allowedFormats={ [] }
|
||||
onChange={ ( newContent ) => onHeadingUpdate( newContent, key, true ) }
|
||||
placeholder={ __( 'Heading text', 'rank-math' ) }
|
||||
/>
|
||||
}
|
||||
{
|
||||
heading.children &&
|
||||
<ListStyle>
|
||||
<List
|
||||
headings={ heading.children }
|
||||
onHeadingUpdate={ onHeadingUpdate }
|
||||
edit={ edit }
|
||||
toggleEdit={ toggleEdit }
|
||||
hideHeading={ hideHeading }
|
||||
ListStyle={ ListStyle }
|
||||
isSave={ isSave }
|
||||
/>
|
||||
</ListStyle>
|
||||
}
|
||||
{
|
||||
key === edit &&
|
||||
<TextControl
|
||||
placeholder={ __( 'Heading Link', 'rank-math' ) }
|
||||
value={ link }
|
||||
onChange={ ( newLink ) => onHeadingUpdate( newLink, key ) }
|
||||
/>
|
||||
}
|
||||
{
|
||||
! isSave &&
|
||||
<span className="rank-math-block-actions">
|
||||
<Button
|
||||
icon={ edit === key ? 'saved' : 'admin-links' }
|
||||
className="rank-math-item-visbility"
|
||||
onClick={ () => toggleEdit( edit === key ? false : key ) }
|
||||
title={ __( 'Edit Link', 'rank-math' ) }
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="rank-math-item-delete"
|
||||
icon={ ! disable ? 'visibility' : 'hidden' }
|
||||
onClick={ () => hideHeading( ! disable, key ) }
|
||||
title={ __( 'Hide', 'rank-math' ) }
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
</TagName>
|
||||
)
|
||||
} ) }
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isUndefined } from 'lodash'
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { useBlockProps } from '@wordpress/block-editor'
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { linearToNestedHeadingList } from './utils'
|
||||
import List from './list'
|
||||
|
||||
export default function save( { attributes } ) {
|
||||
if ( isUndefined( attributes.headings ) || attributes.headings.length === 0 ) {
|
||||
return null
|
||||
}
|
||||
|
||||
const TitleWrapper = attributes.titleWrapper
|
||||
const headings = linearToNestedHeadingList( attributes.headings )
|
||||
const ListStyle = attributes.listStyle
|
||||
|
||||
return (
|
||||
<div { ...useBlockProps.save() } id="rank-math-toc">
|
||||
{ attributes.title && <TitleWrapper dangerouslySetInnerHTML={ { __html: attributes.title } }></TitleWrapper> }
|
||||
<nav>
|
||||
<ListStyle>
|
||||
<List
|
||||
headings={ headings }
|
||||
ListStyle={ ListStyle }
|
||||
isSave={ true }
|
||||
/>
|
||||
</ListStyle>
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n'
|
||||
import { BlockControls } from '@wordpress/block-editor'
|
||||
import {
|
||||
Toolbar,
|
||||
ToolbarButton,
|
||||
} from '@wordpress/components'
|
||||
import { formatListBullets, formatListNumbered, alignLeft } from '@wordpress/icons'
|
||||
|
||||
export default ( { setAttributes } ) => {
|
||||
return (
|
||||
<BlockControls>
|
||||
<Toolbar label={ __( 'Table of Content Options', 'rank-math' ) }>
|
||||
<ToolbarButton
|
||||
icon={ formatListBullets }
|
||||
label={ __( 'Unordered List', 'rank-math' ) }
|
||||
onClick={ () => setAttributes( { listStyle: 'ul' } ) }
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={ formatListNumbered }
|
||||
label={ __( 'Ordered List', 'rank-math' ) }
|
||||
onClick={ () => setAttributes( { listStyle: 'ol' } ) }
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={ alignLeft }
|
||||
label={ __( 'None', 'rank-math' ) }
|
||||
onClick={ () => setAttributes( { listStyle: 'div' } ) }
|
||||
/>
|
||||
</Toolbar>
|
||||
</BlockControls>
|
||||
)
|
||||
}
|
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isEmpty, isUndefined, isString, kebabCase, includes, forEach, isEqual, map, isNull } from 'lodash'
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { store as blockEditorStore } from '@wordpress/block-editor'
|
||||
import { __unstableStripHTML as stripHTML } from '@wordpress/dom'
|
||||
import { useSelect, useDispatch } from '@wordpress/data'
|
||||
|
||||
/**
|
||||
* Get the headings from the content.
|
||||
*
|
||||
* @param {Array} headings Array of headings data
|
||||
* @param {Array} excludeHeadings Heading levels to exclude
|
||||
*/
|
||||
export function GetLatestHeadings( headings, excludeHeadings ) {
|
||||
return useSelect(
|
||||
( select ) => {
|
||||
const {
|
||||
getBlockAttributes,
|
||||
getBlockName,
|
||||
getClientIdsWithDescendants,
|
||||
} = select( blockEditorStore )
|
||||
const { __experimentalConvertBlockToStatic: convertBlockToStatic } = useDispatch( 'core/reusable-blocks' )
|
||||
|
||||
// Get the client ids of all blocks in the editor.
|
||||
const allBlockClientIds = getClientIdsWithDescendants()
|
||||
const _latestHeadings = []
|
||||
let i = 0
|
||||
const anchors = []
|
||||
for ( const blockClientId of allBlockClientIds ) {
|
||||
const blockName = getBlockName( blockClientId )
|
||||
if ( blockName === 'core/block' ) {
|
||||
const attrs = getBlockAttributes( blockClientId )
|
||||
if ( ! isNull( attrs.ref ) ) {
|
||||
const reusableBlock = wp.data.select( 'core' ).getEditedEntityRecord( 'postType', 'wp_block', attrs.ref )
|
||||
const blocks = map( reusableBlock.blocks, ( block ) => {
|
||||
return block.name
|
||||
} )
|
||||
|
||||
if ( includes( blocks, 'rank-math/toc-block' ) && ! isNull( getBlockAttributes( blockClientId ) ) ) {
|
||||
convertBlockToStatic( blockClientId )
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if ( ! includes( [ 'rank-math/faq-block', 'rank-math/howto-block', 'core/heading' ], blockName ) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
const headingAttributes = getBlockAttributes( blockClientId )
|
||||
if ( blockName === 'rank-math/faq-block' || blockName === 'rank-math/howto-block' ) {
|
||||
const titleWrapper = headingAttributes.titleWrapper
|
||||
if (
|
||||
includes( excludeHeadings, titleWrapper ) ||
|
||||
includes( [ 'div', 'p' ], titleWrapper )
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const data = blockName === 'rank-math/howto-block' ? headingAttributes.steps : headingAttributes.questions
|
||||
if ( isEmpty( data ) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
forEach( data, ( value ) => {
|
||||
const currentHeading = ! isUndefined( headings ) && ! isEmpty( headings[ _latestHeadings.length ] ) ? headings[ _latestHeadings.length ] : {
|
||||
content: '',
|
||||
level: '',
|
||||
disable: false,
|
||||
isUpdated: false,
|
||||
isGeneratedLink: true,
|
||||
}
|
||||
|
||||
const isGeneratedLink = ! isUndefined( currentHeading.isGeneratedLink ) && currentHeading.isGeneratedLink
|
||||
const content = ! isUndefined( currentHeading.isUpdated ) && currentHeading.isUpdated ? currentHeading.content : value.title
|
||||
|
||||
_latestHeadings.push( {
|
||||
key: value.id,
|
||||
content: stripHTML( content ),
|
||||
level: parseInt( headingAttributes.titleWrapper.replace( 'h', '' ) ),
|
||||
link: ! isGeneratedLink ? currentHeading.link : `#${ value.id }`,
|
||||
disable: currentHeading.disable ? currentHeading.disable : false,
|
||||
isUpdated: ! isUndefined( currentHeading.isUpdated ) ? currentHeading.isUpdated : false,
|
||||
isGeneratedLink,
|
||||
} )
|
||||
} )
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if ( blockName === 'core/heading' ) {
|
||||
if ( includes( excludeHeadings, 'h' + headingAttributes.level ) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
const currentHeading = ! isUndefined( headings ) && ! isEmpty( headings[ _latestHeadings.length ] ) ? headings[ _latestHeadings.length ] : {
|
||||
content: '',
|
||||
level: '',
|
||||
disable: false,
|
||||
isUpdated: false,
|
||||
isGeneratedLink: true,
|
||||
}
|
||||
|
||||
const isGeneratedLink = ! isUndefined( currentHeading.isGeneratedLink ) && currentHeading.isGeneratedLink
|
||||
|
||||
let anchor = headingAttributes.anchor
|
||||
const headingText = ! isEmpty( headingAttributes.content.text ) ? headingAttributes.content.text : headingAttributes.content
|
||||
if ( isEmpty( headingAttributes.anchor ) || isGeneratedLink ) {
|
||||
anchor = kebabCase( stripHTML( headingText ) )
|
||||
}
|
||||
|
||||
if ( includes( anchors, anchor ) ) {
|
||||
i += 1
|
||||
anchor = anchor + '-' + i
|
||||
}
|
||||
|
||||
anchors.push( anchor )
|
||||
headingAttributes.anchor = anchor
|
||||
const headingContent = isString( headingText ) ? stripHTML(
|
||||
headingText.replace(
|
||||
/(<br *\/?>)+/g,
|
||||
' '
|
||||
)
|
||||
) : ''
|
||||
|
||||
const content = ! isUndefined( currentHeading.isUpdated ) && currentHeading.isUpdated ? currentHeading.content : headingContent
|
||||
|
||||
_latestHeadings.push( {
|
||||
key: blockClientId,
|
||||
content: stripHTML( content ),
|
||||
level: headingAttributes.level,
|
||||
link: ! isGeneratedLink ? currentHeading.link : `#${ headingAttributes.anchor }`,
|
||||
disable: currentHeading.disable ? currentHeading.disable : false,
|
||||
isUpdated: ! isUndefined( currentHeading.isUpdated ) ? currentHeading.isUpdated : false,
|
||||
isGeneratedLink,
|
||||
} )
|
||||
}
|
||||
}
|
||||
|
||||
if ( isEqual( headings, _latestHeadings ) ) {
|
||||
return null
|
||||
}
|
||||
|
||||
return _latestHeadings
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Nest heading based on the Heading level.
|
||||
*
|
||||
* @param {Array} headingList The flat list of headings to nest.
|
||||
*
|
||||
* @return {Array} The nested list of headings.
|
||||
*/
|
||||
export function linearToNestedHeadingList( headingList = [] ) {
|
||||
const nestedHeadingList = []
|
||||
forEach( headingList, ( heading, key ) => {
|
||||
if ( isEmpty( heading.content ) ) {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure we are only working with the same level as the first iteration in our set.
|
||||
if ( heading.level === headingList[ 0 ].level ) {
|
||||
if ( headingList[ key + 1 ]?.level > heading.level ) {
|
||||
let endOfSlice = headingList.length
|
||||
for ( let i = key + 1; i < headingList.length; i++ ) {
|
||||
if ( headingList[ i ].level === heading.level ) {
|
||||
endOfSlice = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
nestedHeadingList.push( {
|
||||
heading,
|
||||
children: linearToNestedHeadingList(
|
||||
headingList.slice( key + 1, endOfSlice )
|
||||
),
|
||||
} )
|
||||
} else {
|
||||
nestedHeadingList.push( {
|
||||
heading,
|
||||
children: null,
|
||||
} )
|
||||
}
|
||||
}
|
||||
} )
|
||||
|
||||
return nestedHeadingList
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"apiVersion": 2,
|
||||
"title": "Table of Contents by Rank Math",
|
||||
"description": "Automatically generate the Table of Contents from the Headings added to this page.",
|
||||
"name": "rank-math/toc-block",
|
||||
"category": "rank-math-blocks",
|
||||
"icon": "rm-icon rm-icon-stories",
|
||||
"textdomain": "rank-math",
|
||||
"editorScript": "file:./assets/js/index.js",
|
||||
"editorStyle": "file:./assets/css/toc.css",
|
||||
"style": "file:./assets/css/toc_list_style.css",
|
||||
"attributes": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"headings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"listStyle": {
|
||||
"type": "string"
|
||||
},
|
||||
"titleWrapper": {
|
||||
"type": "string",
|
||||
"default": "h2"
|
||||
},
|
||||
"excludeHeadings": {
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
"color": {
|
||||
"link": true,
|
||||
"gradients": true
|
||||
},
|
||||
"multiple": false,
|
||||
"spacing": {
|
||||
"margin": true,
|
||||
"padding": true
|
||||
},
|
||||
"typography": {
|
||||
"fontSize": true,
|
||||
"lineHeight": true,
|
||||
"__experimentalDefaultControls": {
|
||||
"fontSize": true
|
||||
}
|
||||
},
|
||||
"align": true
|
||||
}
|
||||
}
|
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
/**
|
||||
* The TOC Block
|
||||
*
|
||||
* @since 1.0.104
|
||||
* @package RankMath
|
||||
* @subpackage RankMath\Schema
|
||||
* @author Rank Math <support@rankmath.com>
|
||||
*/
|
||||
|
||||
namespace RankMath\Schema;
|
||||
|
||||
use WP_Block_Type_Registry;
|
||||
use RankMath\Helper;
|
||||
use RankMath\Traits\Hooker;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* HowTo Block class.
|
||||
*/
|
||||
class Block_TOC extends Block {
|
||||
|
||||
use Hooker;
|
||||
|
||||
/**
|
||||
* Block type name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $block_type = 'rank-math/toc-block';
|
||||
|
||||
/**
|
||||
* The single instance of the class.
|
||||
*
|
||||
* @var Block_TOC
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Retrieve main Block_TOC instance.
|
||||
*
|
||||
* Ensure only one instance is loaded or can be loaded.
|
||||
*
|
||||
* @return Block_TOC
|
||||
*/
|
||||
public static function get() {
|
||||
if ( is_null( self::$instance ) && ! ( self::$instance instanceof Block_TOC ) ) {
|
||||
self::$instance = new Block_TOC();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( WP_Block_Type_Registry::get_instance()->is_registered( $this->block_type ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->filter( 'rank_math/schema/block/toc-block', 'add_graph', 10, 2 );
|
||||
$this->filter( 'render_block_rank-math/toc-block', 'render_toc_block_content', 10, 2 );
|
||||
$this->filter( 'rank_math/metabox/post/values', 'block_settings_metadata' );
|
||||
register_block_type( RANK_MATH_PATH . 'includes/modules/schema/blocks/toc/block.json' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta data to use in the TOC block.
|
||||
*
|
||||
* @param array $values Aray of tabs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function block_settings_metadata( $values ) {
|
||||
$values['tocTitle'] = Helper::get_settings( 'general.toc_block_title' );
|
||||
$values['tocExcludeHeadings'] = Helper::get_settings( 'general.toc_block_exclude_headings', [] );
|
||||
$values['listStyle'] = Helper::get_settings( 'general.toc_block_list_style', 'ul' );
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add default TOC title.
|
||||
*
|
||||
* @param string $block_content Block content.
|
||||
* @param array $parsed_block The full block, including name and attributes.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render_toc_block_content( $block_content, $parsed_block ) {
|
||||
if ( isset( $parsed_block['attrs']['title'] ) ) {
|
||||
return $block_content;
|
||||
}
|
||||
|
||||
$title = Helper::get_settings( 'general.toc_block_title' );
|
||||
if ( ! $title ) {
|
||||
return $block_content;
|
||||
}
|
||||
|
||||
$title_wrapper = $parsed_block['attrs']['titleWrapper'] ?? 'h2';
|
||||
|
||||
$block_content = preg_replace_callback(
|
||||
'/(<div class=".*?wp-block-rank-math-toc-block.*?"\>)/i',
|
||||
function( $value ) use ( $title, $block_content, $title_wrapper ) {
|
||||
if ( ! isset( $value[0] ) ) {
|
||||
return $block_content;
|
||||
}
|
||||
|
||||
$value[0] = str_replace( '>', ' id="rank-math-toc">', $value[0] );
|
||||
return $value[0] . '<' . tag_escape( $title_wrapper ) . '>' . esc_html( $title ) . '</' . tag_escape( $title_wrapper ) . '>';
|
||||
},
|
||||
$block_content
|
||||
);
|
||||
|
||||
return str_replace( 'class=""', '', $block_content );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add TOC schema data in JSON-LD array.
|
||||
*
|
||||
* @param array $data Array of JSON-LD data.
|
||||
* @param array $block JsonLD Instance.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function add_graph( $data, $block ) {
|
||||
$attributes = $block['attrs'];
|
||||
// Early bail.
|
||||
if ( empty( $attributes['headings'] ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if ( ! isset( $data['toc'] ) ) {
|
||||
$data['toc'] = [];
|
||||
}
|
||||
|
||||
foreach ( $attributes['headings'] as $heading ) {
|
||||
if ( ! empty( $heading['disable'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data['toc'][] = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'SiteNavigationElement',
|
||||
'@id' => '#rank-math-toc',
|
||||
'name' => $heading['content'],
|
||||
'url' => get_permalink() . $heading['link'],
|
||||
];
|
||||
}
|
||||
|
||||
if ( empty( $data['toc'] ) ) {
|
||||
unset( $data['toc'] );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user