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 new Categories block #2102

Merged
merged 16 commits into from
Aug 7, 2017
Merged
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
8 changes: 8 additions & 0 deletions blocks/library/categories/block.scss
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;
}
}
13 changes: 13 additions & 0 deletions blocks/library/categories/data.js
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();
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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 the getCategories method on the component, and the latter is called recursively.


return categories;
}

250 changes: 250 additions & 0 deletions blocks/library/categories/index.js
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 } ) );
Copy link
Member

Choose a reason for hiding this comment

The 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 setState could cause trouble.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure - it will not be invoked, as the deferred .then is executed only if the request is resolved.


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;
},
} );
89 changes: 89 additions & 0 deletions blocks/library/categories/index.php
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',
) );
11 changes: 11 additions & 0 deletions blocks/library/categories/style.scss
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;
}
}
}
1 change: 1 addition & 0 deletions blocks/library/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import './code';
import './html';
import './freeform';
import './latest-posts';
import './categories';
import './cover-image';
import './cover-text';
import './verse';
1 change: 1 addition & 0 deletions blocks/test/fixtures/core__categories.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!-- wp:core/categories {"showPostCounts":false,"displayAsDropdown":false,"showHierarchy":false} /-->
12 changes: 12 additions & 0 deletions blocks/test/fixtures/core__categories.json
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
}
}
]
Loading