Skip to content

Commit

Permalink
Merge pull request #51 from studiopress/add/editor-ui-autoslugging
Browse files Browse the repository at this point in the history
Auto-slug block and field names, make Repeater work
  • Loading branch information
kienstra authored Dec 17, 2020
2 parents 0c86640 + 20001f6 commit 9711172
Show file tree
Hide file tree
Showing 39 changed files with 3,181 additions and 2,469 deletions.
5 changes: 5 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
"globals": {
"wp": "readonly"
},
"settings": {
"jsdoc": {
"mode": "typescript"
}
},
"plugins": [
"jsx-a11y",
"testing-library",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Contributors: lukecarbis, ryankienstra, Stino11, rheinardkorf, studiopress, wpengine
Tags: gutenberg, blocks, block editor, fields, template
Requires at least: 5.0
Requires at least: 5.4
Tested up to: 5.5
Requires PHP: 5.6
Stable tag: 1.0.3
Expand Down
2 changes: 1 addition & 1 deletion js/src/block-editor/components/fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const getClassName = ( field ) => {

/**
* @typedef {Object} FieldsProps The component props.
* @property {Array} fields The fields to render.
* @property {Array} fields The fields to render.
* @property {Object} parentBlock The block where the fields are.
* @property {Object} parentBlockProps The props to pass to the control function.
* @property {number} [rowIndex] The index of the repeater row, if this field is in one (optional).
Expand Down
2 changes: 1 addition & 1 deletion js/src/common/components/svg-container.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as React from 'react';

/**
* @typedef {Object} SvgContainerProps The component props.
* @property {React.children} children The children of this component.
* @property {React.ReactElement[]} children The children of this component.
*/

/**
Expand Down
4 changes: 4 additions & 0 deletions js/src/edit-block/components/block-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as React from 'react';
/**
* WordPress dependencies
*/
import { PostTrash } from '@wordpress/editor';
import { __ } from '@wordpress/i18n';

/**
Expand Down Expand Up @@ -34,6 +35,9 @@ const BlockPanel = () => (
<CategorySection />
<KeywordsSection />
<PostTypesSection />
<div className="mt-4">
<PostTrash />
</div>
</div>
);

Expand Down
2 changes: 1 addition & 1 deletion js/src/edit-block/components/category-section.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ const CategorySection = () => {
}
</select>
<button
className="text-sm text-blue-600 focus:outline-none md:underline mt-2"
className="text-sm text-blue-700 focus:outline-none md:underline mt-2"
onClick={ () => {
setShowNewCategoryForm( ( previousValue ) => ! previousValue );
} }
Expand Down
51 changes: 26 additions & 25 deletions js/src/edit-block/components/clipboard-copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,21 @@ import { __, sprintf } from '@wordpress/i18n';
* Copies text to the clipboard, and shows feedback on copying.
*
* Forked from the Gutenberg component ClipboardButton.
*
* https://github.com/WordPress/gutenberg/blob/50eaa95881ddc2f0f93045721f541a96bae5cfa8/packages/components/src/clipboard-button/index.js
*
* @param {ClipboardCopyProps} props The component props.
* @return {React.ReactElement} Copies text to the clipboard.
*/
const ClipboardCopy = ( { text } ) => {
const ref = useRef();
const hasCopied = useCopyOnClick( ref, text );
// Backwards compatibility for before useCopyOnClick() existed.
const hasCopied = useCopyOnClick ? useCopyOnClick( ref, text ) : false; /* eslint-disable-line react-hooks/rules-of-hooks */
const lastHasCopied = useRef( hasCopied );
const label = sprintf(
/* translators: %1$s: the field name */
__( 'Copy the field name of %1$s', 'genesis-custom-blocks' ),
text
);

useEffect( () => {
if ( lastHasCopied.current === hasCopied ) {
Expand All @@ -40,31 +45,27 @@ const ClipboardCopy = ( { text } ) => {
lastHasCopied.current = hasCopied;
}, [ hasCopied ] );

const handleCopy = () => speak( __( 'Copied the text', 'genesis-custom-blocks' ) );

return (
<>
<button
ref={ ref }
onCopy={ handleCopy }
aria-describedby={ `clipboard-copy-${ text }` }
>
{ hasCopied
? <Icon size={ 20 } icon={ check } />
: <svg className="h-4 w-4 fill-current ml-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M8 2a1 1 0 000 2h2a1 1 0 100-2H8z" />
<path d="M3 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v6h-4.586l1.293-1.293a1 1 0 00-1.414-1.414l-3 3a1 1 0 000 1.414l3 3a1 1 0 001.414-1.414L10.414 13H15v3a2 2 0 01-2 2H5a2 2 0 01-2-2V5zM15 11h2a1 1 0 110 2h-2v-2z" />
</svg>
}
</button>
<span id={ `clipboard-copy-${ text }` } className="hidden">
{ sprintf(
/* translators: %1$s: the field name */
__( 'Copy the field name of %1$s', 'genesis-custom-blocks' ),
<button
aria-label={ label }
ref={ ref }
onCopy={ ( event ) => {
event.stopPropagation();
speak( sprintf(
/* translators: %1$s: the text that was copied */
__( 'Copied the text %1$s', 'genesis-custom-blocks' ),
text
) }
</span>
</>
) );
} }
>
{ hasCopied
? <Icon size={ 20 } icon={ check } />
: <svg className="h-4 w-4 fill-current ml-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M8 2a1 1 0 000 2h2a1 1 0 100-2H8z" />
<path d="M3 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v6h-4.586l1.293-1.293a1 1 0 00-1.414-1.414l-3 3a1 1 0 000 1.414l3 3a1 1 0 001.414-1.414L10.414 13H15v3a2 2 0 01-2 2H5a2 2 0 01-2-2V5zM15 11h2a1 1 0 110 2h-2v-2z" />
</svg>
}
</button>
);
};

Expand Down
87 changes: 87 additions & 0 deletions js/src/edit-block/components/editor-provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* External dependencies
*/
import * as React from 'react';

/**
* WordPress dependencies
*/
import { useEffect, useLayoutEffect } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
// @ts-ignore type declaration not available
import { EntityProvider } from '@wordpress/core-data';
// @ts-ignore type declaration not available
import { store as noticesStore } from '@wordpress/notices';

/**
* Internal dependencies
*/
import { useEditor } from '../hooks';

/**
* @typedef {Object} EditorProviderProps The props of the component.
* @property {Object} post The post for the editor.
* @property {Object} settings The editor settings.
* @property {React.ReactElement} children The component's children.
*/

/**
* The editor provider, forked from Gutenberg.
*
* This is forked so we can remove the <BlockEditorProvider>.
* That adds .blocks to the editedEntityRecord,
* which causes getEditedPostContent() to return ''.
*
* @see https://github.com/WordPress/gutenberg/blob/60ad1e320436a55e74fb41cc1735301da187f61e/packages/editor/src/components/provider/index.js
* @param {EditorProviderProps} props
*/
const EditorProvider = ( {
post,
settings,
children,
} ) => {
const { setupEditor } = useEditor();
const {
updatePostLock,
updateEditorSettings,
} = useDispatch( 'core/editor' );
// @ts-ignore type declaration not available
const { createWarningNotice } = useDispatch( noticesStore );

// Iniitialize the editor.
// Ideally this should be synced on each change and not just something you do once.
useLayoutEffect( () => {
updatePostLock( settings.postLock );
setupEditor( post );
if ( settings.autosave ) {
createWarningNotice(
__( 'There is an autosave of this post that is more recent than the version below.', 'genesis-custom-blocks' ),
{
id: 'autosave-exists',
actions: [
{
label: __( 'View the autosave', 'genesis-custom-blocks' ),
url: settings.autosave.editLink,
},
],
}
);
}
}, [] ); /* eslint-disable-line react-hooks/exhaustive-deps */

// Synchronize the editor settings as they change
useEffect( () => {
updateEditorSettings( settings );
}, [ settings, updateEditorSettings ] );

return (
<EntityProvider kind="root" type="site">
<EntityProvider kind="postType" type={ post.type } id={ post.id }>
{ children }
</EntityProvider>
</EntityProvider>
);
};

export default EditorProvider;
79 changes: 46 additions & 33 deletions js/src/edit-block/components/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,23 @@ import * as React from 'react';
/**
* WordPress dependencies
*/
import { useDispatch, useSelect } from '@wordpress/data';
import { useSelect } from '@wordpress/data';
import {
EditorNotices,
EditorProvider,
ErrorBoundary,
UnsavedChangesWarning,
} from '@wordpress/editor';
import { StrictMode, useEffect, useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import { BrowserURL, Header, Main, Side } from './';
import { BLOCK_PANEL, DEFAULT_LOCATION, NO_FIELD_SELECTED } from '../constants';
import { BrowserURL, EditorProvider, Header, Main, Side } from './';
import {
BLOCK_PANEL,
DEFAULT_LOCATION,
NO_FIELD_SELECTED,
} from '../constants';
import { getDefaultBlock } from '../helpers';
import { useBlock } from '../hooks';

Expand All @@ -29,34 +33,57 @@ import { useBlock } from '../hooks';

/**
* @typedef {Object} EditorProps The component props.
* @property {Object|null} initialEdits The initial edits, if any.
* @property {onErrorType} onError Handler for errors.
* @property {number} postId The current post ID.
* @property {string} postType The current post type.
* @property {Object} settings The editor settings.
*/

/**
* @typedef {Object} SelectedField A field to change.
* @property {string} name The name of the field.
* @property {string} [parent] The name of the field's parent, if any.
*/

/** @typedef {string} CurrentLocation The currently selected location. */
/** @typedef {boolean} IsNewField Whether there is a new field. */
/** @typedef {string} PanelDisplaying The panel currently displaying in the side, like 'block'. */
/** @typedef {function(string):void} SetCurrentLocation Sets the currently selected location */
/** @typedef {function(boolean):void} SetIsNewField Sets whether there is a new field. */
/** @typedef {function(string):void} SetPanelDisplaying Sets the current panel displaying. */
/** @typedef {function(SelectedField|import('../constants').NoFieldSelected):void} SetSelectedField Sets the selected field. */

/**
* @typedef {Object} Field A block field, can have more properties depending on its settings.
* @property {string} name The name of the field.
* @property {string} label The label of the field.
* @property {string} control The control type, like 'text' or 'textarea'.
* @property {string} location The location, like 'editor'.
* @property {string} type The data type for its value, like string.
* @property {number} order Its order relative to other fields in its location, like 0, 1, 2...
* @property {string} [parent] The name of its parent field, like a Repeater control.
* @property {Object} [sub_fields] Fields that this field has, like for the Repeater control.
* @property {string|number} [width] The width, like '25'.
*/

/**
* The editor component.
*
* @param {EditorProps} props The component props.
* @return {React.ReactElement} The editor.
*/
const Editor = ( { initialEdits, onError, postId, postType, settings } ) => {
const Editor = ( { onError, postId, postType, settings } ) => {
const { block, changeBlockName } = useBlock();
const [ selectedField, setSelectedField ] = useState( NO_FIELD_SELECTED );
const [ currentLocation, setCurrentLocation ] = useState( DEFAULT_LOCATION );
const [ isNewField, setIsNewField ] = useState( false );
const [ panelDisplaying, setPanelDisplaying ] = useState( BLOCK_PANEL );
const [ selectedField, setSelectedField ] = useState( NO_FIELD_SELECTED );

const post = useSelect(
( select ) => select( 'core' ).getEntityRecord( 'postType', postType, postId ),
[ postId, postType ]
);
const isSavingPost = useSelect(
( select ) => select( 'core/editor' ).isSavingPost()
);
// @ts-ignore
const { editEntityRecord } = useDispatch( 'core' );
const isSavingPost = useSelect( ( select ) => select( 'core/editor' ).isSavingPost() );

useEffect( () => {
if ( isSavingPost && ! block.name ) {
Expand All @@ -65,36 +92,18 @@ const Editor = ( { initialEdits, onError, postId, postType, settings } ) => {
}
}, [ block, changeBlockName, isSavingPost, postId ] );

useEffect( () => {
if ( ! post ) {
return;
}

// A hack to remove blocks from the edited entity.
// The stores use getEditedPostContent(), which gets the blocks if the .blocks property exists.
// This change makes getEditedPostContent() return the post content, instead of
// parsing [] blocks and returning ''.
editEntityRecord(
'postType',
postType,
postId,
{ blocks: null }
);
}, [ editEntityRecord, post, postId, postType ] );

if ( ! post ) {
return null;
}

return (
<StrictMode>
<UnsavedChangesWarning />
<div className="h-screen flex flex-col items-center text-black">
<BrowserURL />
<EditorProvider
settings={ settings }
post={ post }
initialEdits={ initialEdits }
useSubRegistry={ false }
settings={ settings }
>
<ErrorBoundary onError={ onError }>
<EditorNotices />
Expand All @@ -104,14 +113,18 @@ const Editor = ( { initialEdits, onError, postId, postType, settings } ) => {
currentLocation={ currentLocation }
selectedField={ selectedField }
setCurrentLocation={ setCurrentLocation }
setIsNewField={ setIsNewField }
setPanelDisplaying={ setPanelDisplaying }
setSelectedField={ setSelectedField }
/>
<Side
currentLocation={ currentLocation }
isNewField={ isNewField }
panelDisplaying={ panelDisplaying }
setPanelDisplaying={ setPanelDisplaying }
selectedField={ selectedField }
setPanelDisplaying={ setPanelDisplaying }
setCurrentLocation={ setCurrentLocation }
setIsNewField={ setIsNewField }
setSelectedField={ setSelectedField }
/>
</div>
Expand Down
Loading

0 comments on commit 9711172

Please sign in to comment.