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

RichText: Extend the AlignmentToolbar #14824

Closed
wants to merge 4 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
27 changes: 23 additions & 4 deletions packages/block-editor/src/components/alignment-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,30 @@ const DEFAULT_ALIGNMENT_CONTROLS = [
},
];

export function AlignmentToolbar( { isCollapsed, value, onChange, alignmentControls = DEFAULT_ALIGNMENT_CONTROLS } ) {
export function AlignmentToolbar( {
alignmentControls = DEFAULT_ALIGNMENT_CONTROLS,
customAlignmentTypes,
isCollapsed,
onChange,
value,
} ) {
function applyOrUnset( align ) {
return () => onChange( value === align ? undefined : align );
}

const activeAlignment = find( alignmentControls, ( control ) => control.align === value );
const extendedAlignmentControls = [
...alignmentControls,
...customAlignmentTypes,
];

const activeAlignment = find( extendedAlignmentControls, ( control ) => control.align === value );

return (
<Toolbar
isCollapsed={ isCollapsed }
icon={ activeAlignment ? activeAlignment.icon : 'editor-alignleft' }
label={ __( 'Change Text Alignment' ) }
controls={ alignmentControls.map( ( control ) => {
controls={ extendedAlignmentControls.map( ( control ) => {
const { align } = control;
const isActive = ( value === align );

Expand All @@ -69,12 +80,20 @@ export default compose(
} ),
withViewportMatch( { isLargeViewport: 'medium' } ),
withSelect( ( select, { clientId, isLargeViewport, isCollapsed } ) => {
const { getBlockRootClientId, getSettings } = select( 'core/block-editor' );
const { getSelectedBlock, getBlockRootClientId, getSettings } = select( 'core/block-editor' );
const { getCustomAlignmentTypesForBlock } = select( 'core/rich-text' );

const selectedBlock = getSelectedBlock();
const customAlignmentTypes = selectedBlock ?
getCustomAlignmentTypesForBlock( selectedBlock.name ) :
[];

return {
isCollapsed: isCollapsed || ! isLargeViewport || (
! getSettings().hasFixedToolbar &&
getBlockRootClientId( clientId )
),
customAlignmentTypes,
};
} ),
)( AlignmentToolbar );
9 changes: 9 additions & 0 deletions packages/edit-post/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { registerCoreBlocks } from '@wordpress/block-library';
import { render, unmountComponentAtNode } from '@wordpress/element';
import { dispatch } from '@wordpress/data';

import { registerCustomAlignmentType } from '@wordpress/rich-text';

/**
* Internal dependencies
*/
Expand Down Expand Up @@ -83,6 +85,13 @@ export function initializeEditor( id, postType, postId, settings, initialEdits )
'core/editor.publish',
] );

registerCustomAlignmentType( 'test/custom-align-center', {
align: 'center',
icon: 'editor-aligncenter',
title: 'Custom Align Center',
blockName: 'core/paragraph',
} );

render(
<Editor
settings={ settings }
Expand Down
18 changes: 18 additions & 0 deletions packages/rich-text/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,24 @@ _Returns_

- `Object`: A new combined value.

<a name="registerCustomAlignmentType" href="#registerCustomAlignmentType">#</a> **registerCustomAlignmentType**

Registers a new custom alignment type provided a unique name and an object defining its
behavior.

_Parameters_

- _name_ `string`: Custom alignment name.
- _settings_ `Object`: Custom alignment settings.
- _settings.blockName_ `string`: The block name this custom alignment will be added to the Rich Text alignment toolbar.
- _settings.align_ `string`: The alignment setting.
- _settings.title_ `string`: Name of the custom alignment.
- _settings.icon_ `string`: The icon to be displayed in the alignment toolbar.

_Returns_

- `(Object|undefined)`: The Custom alignment, if it has been successfully registered; otherwise `undefined`.

<a name="registerFormatType" href="#registerFormatType">#</a> **registerFormatType**

Registers a new format provided a unique name and an object defining its
Expand Down
1 change: 1 addition & 0 deletions packages/rich-text/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { getTextContent } from './get-text-content';
export { isCollapsed } from './is-collapsed';
export { isEmpty, isEmptyLine } from './is-empty';
export { join } from './join';
export { registerCustomAlignmentType } from './register-custom-alignment-type';
export { registerFormatType } from './register-format-type';
export { removeFormat } from './remove-format';
export { remove } from './remove';
Expand Down
88 changes: 88 additions & 0 deletions packages/rich-text/src/register-custom-alignment-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* WordPress dependencies
*/
import { dispatch, select } from '@wordpress/data';

/**
* Registers a new custom alignment type provided a unique name and an object defining its
* behavior.
*
* @param {string} name Custom alignment name.
* @param {Object} settings Custom alignment settings.
* @param {string} settings.blockName The block name this custom alignment will be added to the Rich Text alignment toolbar.
* @param {string} settings.align The alignment setting.
* @param {string} settings.title Name of the custom alignment.
* @param {string} settings.icon The icon to be displayed in the alignment toolbar.
*
* @return {Object|undefined} The Custom alignment, if it has been successfully registered;
* otherwise `undefined`.
*/
export function registerCustomAlignmentType( name, settings ) {
Copy link
Member

Choose a reason for hiding this comment

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

I know that this is a draft PR but I'd love to stress how valuable some inline documentation right here can be. people will look here to try and find the answer "what information is necessary to define an alignment type?" (if only we had some way to designate types and have the computer answer the question for us…)

looking through the whole PR I'm able to discern that it needs a name, a type, and control but I still don't know what those properties should be.

we could consider adding a JSDoc typedef

/**
 * @typedef {Object} AlignmentType
 * @property {string} type - blockType for which this will be available
 * @property {AlignmentControl} control - whatever this actually is, maybe another typedef
 */

/**
 * Registers a custom alignment type
 *
 * @example
 * registerCustomAlignmentType( 'justified', { type: 'core/paragraph', control: <Whatever /> } );
 *
 * @param {string} name - name of alignment, e.g. "left" or "justified"
 * @param {AlignmentType} settings - specifies the alignment
 */
export function registerCustomAlignmentType( name, settings ) {
  
}

even without the typedef though having an example use can clear up so much confusion, as can enumerating what properties of settings may exist and what they mean

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for pointing this out @dmsnell!

That file is a very stripped down version of register-format-type.js.
It doesn't have any docs, validations, etc, because this exploration is more about extending the toolbar than handling this registration.
I will add everything (and more!) back as soon as I get the extension part right. 👍

settings = {
name,
...settings,
};

if ( typeof settings.name !== 'string' ) {
window.console.error(
'Custom alignment names must be strings.'
);
return;
}

if ( ! /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/.test( settings.name ) ) {
window.console.error(
'Custom alignment names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-alignment'
);
return;
}

if (
typeof settings.blockName !== 'string' ||
settings.blockName === ''
) {
window.console.error(
`Custom alignment block name must be a string.`
);
return;
}

if ( ! select( 'core/blocks' ).getBlockType( settings.blockName ) ) {
window.console.error(
'Custom alignment block "' + settings.blockName + '" must be registered.'
);
return;
}

if ( select( 'core/rich-text' ).getCustomAlignmentType( settings.name, settings.blockName ) ) {
window.console.error(
'Custom alignment "' + settings.name + '" is already registered.'
);
return;
}

if ( ! ( 'title' in settings ) || settings.title === '' ) {
window.console.error(
'The custom alignment "' + settings.name + '" must have a title.'
);
return;
}

if ( ! ( 'icon' in settings ) || settings.icon === '' ) {
window.console.error(
'The custom alignment "' + settings.name + '" must have an icon.'
);
return;
}

if ( ! ( 'align' in settings ) || settings.align === '' ) {
window.console.error(
'The custom alignment "' + settings.name + '" must have an alignment setting.'
);
return;
}

dispatch( 'core/rich-text' ).addCustomAlignmentTypes( settings );

return settings;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd personally love us to move away from these register* global functions. That said, I'd like to understand the use-case a bit better. Why do you want a new alignment option, is this meant or custom blocks essentially or for Core Blocks.

Another downside of this approach is that if you disable the custom alignment, this might create invalid blocks in some cases (depending on how the alignment is applied to the block). And I think we should discourage the extensibility APIs that lead to this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The use-case would be to allow custom alignment options to be added to the alignment toolbar.
The most obvious one would be justify, which is a standard text-align value, so it should be straightforward enough to add back (with my implementation).
Less obvious ones would be additional block alignments like alignwide and alignfull.

Now, this is an exploratory PR opened by someone with close-to-zero familiarity with how extending Gutenberg works, so comments like yours about the risk of breaking a block are absolutely appreciated!
This said, I didn't expect to trigger a review request to seven different people when opening the PR, or otherwise I'd have tried to describe it a bit better, and clean it up a bit more 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

didn't expect to trigger a review request to seven different people when opening the PR

I guess, it means you're lucky to have early feedback.

About the justify alignment, I was thinking this is something we support by default "code-wise" but maybe just leave the UI hidden (it could be enabled using a shortcut). Is this a valid option?

Also, you're mixing both block and text alignment in the comments while it seems that at the moment, this PR is addressing text alignments only.

I wonder if this should be just another editor setting. The same way we have "colors" and "fontSizes" in the editor settings, we could include "blockAlignments" and "textAlignments". The block alignment option would be a little be redundunt with the "alignWide" setting but we can probably ensure BC. Thoughts?

28 changes: 28 additions & 0 deletions packages/rich-text/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,34 @@
*/
import { castArray } from 'lodash';

/**
* Returns an action object used in signalling that custom alignment types have been added.
*
* @param {Array|Object} customAlignmentTypes Alignment types received.
*
* @return {Object} Action object.
*/
export function addCustomAlignmentTypes( customAlignmentTypes ) {
return {
type: 'ADD_CUSTOM_ALIGNMENT_TYPES',
customAlignmentTypes: castArray( customAlignmentTypes ),
};
}

/**
* Returns an action object used to remove a registered custom alignment type.
*
* @param {string|Array} names Custom alignment name.
*
* @return {Object} Action object.
*/
export function removeCustomAlignmentTypes( names ) {
return {
type: 'REMOVE_CUSTOM_ALIGNMENT_TYPES',
names: castArray( names ),
};
}

/**
* Returns an action object used in signalling that format types have been
* added.
Expand Down
24 changes: 23 additions & 1 deletion packages/rich-text/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ import { keyBy, omit } from 'lodash';
*/
import { combineReducers } from '@wordpress/data';

/**
* Reducer managing the alignment types
*
* @param {Object} state Current state.
* @param {Object} action Dispatched action.
*
* @return {Object} Updated state.
*/
export function customAlignmentTypes( state = {}, action ) {
switch ( action.type ) {
case 'ADD_CUSTOM_ALIGNMENT_TYPES':
return {
...state,
...keyBy( action.customAlignmentTypes, 'name' ),
};
case 'REMOVE_CUSTOM_ALIGNMENT_TYPES':
return omit( state, action.names );
}

return state;
}

/**
* Reducer managing the format types
*
Expand All @@ -30,4 +52,4 @@ export function formatTypes( state = {}, action ) {
return state;
}

export default combineReducers( { formatTypes } );
export default combineReducers( { customAlignmentTypes, formatTypes } );
41 changes: 40 additions & 1 deletion packages/rich-text/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,46 @@
* External dependencies
*/
import createSelector from 'rememo';
import { find } from 'lodash';
import { filter, find } from 'lodash';

/**
* Returns all the available custom alignment types.
*
* @param {Object} state Data state.
*
* @return {Array} Custom alignment types.
*/
export const getCustomAlignmentTypes = createSelector(
( state ) => Object.values( state.customAlignmentTypes ),
( state ) => [
state.customAlignmentTypes,
]
);

/**
* Returns the custom alignment types available for a given block.
*
* @param {Object} state Data state.
* @param {string} blockName Block name.
*
* @return {Array} Custom alignment types.
*/
export function getCustomAlignmentTypesForBlock( state, blockName ) {
return filter( state.customAlignmentTypes, ( type ) => type.blockName === blockName );
}

/**
* Returns a custom alignment type.
*
* @param {Object} state Data state.
* @param {string} name Alignment name.
* @param {string} blockName Block name.
*
* @return {?Object} Custom alignment types.
*/
export function getCustomAlignmentType( state, name, blockName ) {
return find( state.customAlignmentTypes, ( type ) => type.name === name && type.blockName === blockName );
}

/**
* Returns all the available format types.
Expand Down