-
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 new Categories block #2102
Add new Categories block #2102
Changes from all commits
03789b3
6f49fc2
3f546d0
2893607
2dfb411
2cc2804
86afe7a
515dad4
30ba95e
0fa3f29
428d791
aa5d4ad
f1f7f0b
2af85c8
f23a5da
c501a4b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.wp-block-categories { | ||
&.alignleft { | ||
margin-right: 2em; | ||
} | ||
&.alignright { | ||
margin-left: 2em; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/** | ||
* Returns a jqXHR object with the categories or an error on failure. | ||
* | ||
* @returns {wp.api.collections.Categories} Returns a jqXHR object with all categories. | ||
*/ | ||
export function getCategories() { | ||
const categoriesCollection = new wp.api.collections.Categories(); | ||
|
||
const categories = categoriesCollection.fetch(); | ||
|
||
return categories; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,250 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { Component } from '@wordpress/element'; | ||
import { Placeholder, Spinner } from '@wordpress/components'; | ||
import { __ } from '@wordpress/i18n'; | ||
import { times, unescape } from 'lodash'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import './style.scss'; | ||
import './block.scss'; | ||
import { registerBlockType } from '../../api'; | ||
import { getCategories } from './data.js'; | ||
import InspectorControls from '../../inspector-controls'; | ||
import ToggleControl from '../../inspector-controls/toggle-control'; | ||
import BlockDescription from '../../block-description'; | ||
import BlockControls from '../../block-controls'; | ||
import BlockAlignmentToolbar from '../../block-alignment-toolbar'; | ||
|
||
registerBlockType( 'core/categories', { | ||
title: __( 'Categories' ), | ||
|
||
icon: 'list-view', | ||
|
||
category: 'widgets', | ||
|
||
defaultAttributes: { | ||
showPostCounts: false, | ||
displayAsDropdown: false, | ||
showHierarchy: false, | ||
}, | ||
|
||
getEditWrapperProps( attributes ) { | ||
const { align } = attributes; | ||
if ( 'left' === align || 'right' === align || 'full' === align ) { | ||
return { 'data-align': align }; | ||
} | ||
}, | ||
|
||
edit: class extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
|
||
this.state = { | ||
categories: [], | ||
}; | ||
|
||
this.categoriesRequest = getCategories(); | ||
|
||
this.categoriesRequest | ||
.then( categories => this.setState( { categories } ) ); | ||
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. Can we confirm whether this callback will be invoked if the request is aborted while in-flight? At that point the component would no longer be mounted either, so 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. Sure - it will not be invoked, as the deferred |
||
|
||
this.toggleDisplayAsDropdown = this.toggleDisplayAsDropdown.bind( this ); | ||
this.toggleShowPostCounts = this.toggleShowPostCounts.bind( this ); | ||
this.toggleShowHierarchy = this.toggleShowHierarchy.bind( this ); | ||
} | ||
|
||
componentWillUnmount() { | ||
if ( this.categoriesRequest.state() === 'pending' ) { | ||
this.categoriesRequest.abort(); | ||
} | ||
} | ||
|
||
toggleDisplayAsDropdown() { | ||
const { attributes, setAttributes } = this.props; | ||
const { displayAsDropdown } = attributes; | ||
|
||
setAttributes( { displayAsDropdown: ! displayAsDropdown } ); | ||
} | ||
|
||
toggleShowPostCounts() { | ||
const { attributes, setAttributes } = this.props; | ||
const { showPostCounts } = attributes; | ||
|
||
setAttributes( { showPostCounts: ! showPostCounts } ); | ||
} | ||
|
||
toggleShowHierarchy() { | ||
const { attributes, setAttributes } = this.props; | ||
const { showHierarchy } = attributes; | ||
|
||
setAttributes( { showHierarchy: ! showHierarchy } ); | ||
} | ||
|
||
getCategories( parentId = null ) { | ||
const { categories } = this.state; | ||
if ( ! categories.length ) { | ||
return categories; | ||
} | ||
|
||
if ( parentId === null ) { | ||
return categories; | ||
} | ||
|
||
return categories.filter( category => category.parent === parentId ); | ||
} | ||
|
||
getCategoryListClassName( level ) { | ||
const { className } = this.props; | ||
return `${ className }__list ${ className }__list-level-${ level }`; | ||
} | ||
|
||
renderCategoryName( category ) { | ||
if ( ! category.name ) { | ||
return __( '(Untitled)' ); | ||
} | ||
|
||
return unescape( category.name ).trim(); | ||
} | ||
|
||
renderCategoryList() { | ||
const { showHierarchy } = this.props.attributes; | ||
const parentId = showHierarchy ? 0 : null; | ||
const categories = this.getCategories( parentId ); | ||
|
||
return ( | ||
<ul className={ this.getCategoryListClassName( 0 ) }> | ||
{ categories.map( category => this.renderCategoryListItem( category, 0 ) ) } | ||
</ul> | ||
); | ||
} | ||
|
||
renderCategoryListItem( category, level ) { | ||
const { showHierarchy, showPostCounts } = this.props.attributes; | ||
const childCategories = this.getCategories( category.id ); | ||
|
||
return ( | ||
<li key={ category.id }> | ||
<a href={ category.link } target="_blank">{ this.renderCategoryName( category ) }</a> | ||
{ showPostCounts && | ||
<span className={ `${ this.props.className }__post-count` }> | ||
{ ' ' }({ category.count }) | ||
</span> | ||
} | ||
|
||
{ | ||
showHierarchy && | ||
!! childCategories.length && ( | ||
<ul className={ this.getCategoryListClassName( level + 1 ) }> | ||
{ childCategories.map( childCategory => this.renderCategoryListItem( childCategory, level + 1 ) ) } | ||
</ul> | ||
) | ||
} | ||
</li> | ||
); | ||
} | ||
|
||
renderCategoryDropdown() { | ||
const { showHierarchy } = this.props.attributes; | ||
const parentId = showHierarchy ? 0 : null; | ||
const categories = this.getCategories( parentId ); | ||
|
||
return ( | ||
<select className={ `${ this.props.className }__dropdown` }> | ||
{ categories.map( category => this.renderCategoryDropdownItem( category, 0 ) ) } | ||
</select> | ||
); | ||
} | ||
|
||
renderCategoryDropdownItem( category, level ) { | ||
const { showHierarchy, showPostCounts } = this.props.attributes; | ||
const childCategories = this.getCategories( category.id ); | ||
|
||
return [ | ||
<option key={ category.id }> | ||
{ times( level * 3, () => '\xa0' ) } | ||
{ this.renderCategoryName( category ) } | ||
{ | ||
!! showPostCounts | ||
? ` (${ category.count })` | ||
: '' | ||
} | ||
</option>, | ||
showHierarchy && | ||
!! childCategories.length && ( | ||
childCategories.map( childCategory => this.renderCategoryDropdownItem( childCategory, level + 1 ) ) | ||
), | ||
]; | ||
} | ||
|
||
render() { | ||
const { setAttributes } = this.props; | ||
const categories = this.getCategories(); | ||
|
||
if ( ! categories.length ) { | ||
return ( | ||
<Placeholder | ||
icon="admin-post" | ||
label={ __( 'Categories' ) } | ||
> | ||
<Spinner /> | ||
</Placeholder> | ||
); | ||
} | ||
|
||
const { focus } = this.props; | ||
const { align, displayAsDropdown, showHierarchy, showPostCounts } = this.props.attributes; | ||
|
||
return [ | ||
focus && ( | ||
<BlockControls key="controls"> | ||
<BlockAlignmentToolbar | ||
value={ align } | ||
onChange={ ( nextAlign ) => { | ||
setAttributes( { align: nextAlign } ); | ||
} } | ||
controls={ [ 'left', 'center', 'right', 'full' ] } | ||
/> | ||
</BlockControls> | ||
), | ||
focus && ( | ||
<InspectorControls key="inspector"> | ||
<BlockDescription> | ||
<p>{ __( 'Shows a list of your site\'s categories.' ) }</p> | ||
</BlockDescription> | ||
<h3>{ __( 'Categories Settings' ) }</h3> | ||
<ToggleControl | ||
label={ __( 'Display as dropdown' ) } | ||
checked={ displayAsDropdown } | ||
onChange={ this.toggleDisplayAsDropdown } | ||
/> | ||
<ToggleControl | ||
label={ __( 'Show post counts' ) } | ||
checked={ showPostCounts } | ||
onChange={ this.toggleShowPostCounts } | ||
/> | ||
<ToggleControl | ||
label={ __( 'Show hierarchy' ) } | ||
checked={ showHierarchy } | ||
onChange={ this.toggleShowHierarchy } | ||
/> | ||
</InspectorControls> | ||
), | ||
<div key="categories" className={ this.props.className }> | ||
{ | ||
displayAsDropdown | ||
? this.renderCategoryDropdown() | ||
: this.renderCategoryList() | ||
} | ||
</div>, | ||
]; | ||
} | ||
}, | ||
|
||
save() { | ||
return null; | ||
}, | ||
} ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
<?php | ||
/** | ||
* Server-side rendering of the `core/categories` block. | ||
* | ||
* @package gutenberg | ||
*/ | ||
|
||
/** | ||
* Renders the `core/categories` block on server. | ||
* | ||
* @param array $attributes The block attributes. | ||
* | ||
* @return string Returns the categories list/dropdown markup. | ||
*/ | ||
function gutenberg_render_block_core_categories( $attributes ) { | ||
static $block_id = 0; | ||
$block_id++; | ||
|
||
$align = 'center'; | ||
if ( isset( $attributes['align'] ) && in_array( $attributes['align'], array( 'left', 'right', 'full' ), true ) ) { | ||
$align = $attributes['align']; | ||
} | ||
|
||
$args = array( | ||
'echo' => false, | ||
'hierarchical' => ! empty( $attributes['showHierarchy'] ), | ||
'orderby' => 'name', | ||
'show_count' => ! empty( $attributes['showPostCounts'] ), | ||
'title_li' => '', | ||
); | ||
|
||
if ( ! empty( $attributes['displayAsDropdown'] ) ) { | ||
$id = 'wp-block-categories-' . $block_id; | ||
$args['id'] = $id; | ||
$args['show_option_none'] = __( 'Select Category', 'gutenberg' ); | ||
$wrapper_markup = '<div class="%1$s">%2$s</div>'; | ||
$items_markup = wp_dropdown_categories( $args ); | ||
$type = 'dropdown'; | ||
|
||
if ( ! is_admin() ) { | ||
$wrapper_markup .= gutenberg_build_dropdown_script_block_core_categories( $id ); | ||
} | ||
} else { | ||
$wrapper_markup = '<div class="%1$s"><ul>%2$s</ul></div>'; | ||
$items_markup = wp_list_categories( $args ); | ||
$type = 'list'; | ||
} | ||
|
||
$class = "wp-block-categories wp-block-categories-{$type} align{$align}"; | ||
|
||
$block_content = sprintf( | ||
$wrapper_markup, | ||
esc_attr( $class ), | ||
$items_markup | ||
); | ||
|
||
return $block_content; | ||
} | ||
|
||
/** | ||
* Generates the inline script for a categories dropdown field. | ||
* | ||
* @param string $dropdown_id ID of the dropdown field. | ||
* | ||
* @return string Returns the dropdown onChange redirection script. | ||
*/ | ||
function gutenberg_build_dropdown_script_block_core_categories( $dropdown_id ) { | ||
ob_start(); | ||
?> | ||
<script type='text/javascript'> | ||
/* <![CDATA[ */ | ||
(function() { | ||
var dropdown = document.getElementById( '<?php echo esc_js( $dropdown_id ); ?>' ); | ||
function onCatChange() { | ||
if ( dropdown.options[ dropdown.selectedIndex ].value > 0 ) { | ||
location.href = "<?php echo home_url(); ?>/?cat=" + dropdown.options[ dropdown.selectedIndex ].value; | ||
} | ||
} | ||
dropdown.onchange = onCatChange; | ||
})(); | ||
/* ]]> */ | ||
</script> | ||
<?php | ||
return ob_get_clean(); | ||
} | ||
|
||
register_block_type( 'core/categories', array( | ||
'render_callback' => 'gutenberg_render_block_core_categories', | ||
) ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
|
||
.editor-visual-editor__block[data-type="core/categories"] { | ||
|
||
.wp-block-categories ul { | ||
padding-left: 2.5em; | ||
|
||
ul { | ||
margin-top: 6px; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<!-- wp:core/categories {"showPostCounts":false,"displayAsDropdown":false,"showHierarchy":false} /--> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
[ | ||
{ | ||
"uid": "_uid_0", | ||
"name": "core/categories", | ||
"isValid": true, | ||
"attributes": { | ||
"showPostCounts": false, | ||
"displayAsDropdown": false, | ||
"showHierarchy": false | ||
} | ||
} | ||
] |
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.
Will this not introduce the possibility of there being many requests when there is a large deeply-nested category tree?
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.
Not really - this one will be called only per a Categories block.
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 see. I was confusing this
getCategories
function with thegetCategories
method on the component, and the latter is called recursively.