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

Render html in post titles in visual mode and edit HTML in post title in code view #54718

Merged
merged 5 commits into from
Nov 27, 2023
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
16 changes: 16 additions & 0 deletions packages/e2e-test-utils/src/toggle-more-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@
export async function toggleMoreMenu( waitFor ) {
const menuSelector = '.interface-more-menu-dropdown [aria-label="Options"]';

const menuToggle = await page.waitForSelector( menuSelector );

const isOpen = await menuToggle.evaluate( ( el ) =>
el.getAttribute( 'aria-expanded' )
);

// If opening and it's already open then exit early.
if ( isOpen === 'true' && waitFor === 'open' ) {
return;
}

// If closing and it's already closed then exit early.
if ( isOpen === 'false' && waitFor === 'close' ) {
return;
}

await page.click( menuSelector );

if ( waitFor ) {
Expand Down
22 changes: 20 additions & 2 deletions packages/edit-post/src/components/text-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
*/
import {
PostTextEditor,
PostTitle,
PostTitleRaw,
store as editorStore,
} from '@wordpress/editor';
import { Button } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { displayShortcut } from '@wordpress/keycodes';
import { useEffect, useRef } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -22,6 +23,23 @@ export default function TextEditor() {
}, [] );
const { switchEditorMode } = useDispatch( editPostStore );

const { isWelcomeGuideVisible } = useSelect( ( select ) => {
const { isFeatureActive } = select( editPostStore );

return {
isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ),
};
}, [] );

const titleRef = useRef();

useEffect( () => {
if ( isWelcomeGuideVisible ) {
return;
}
titleRef?.current?.focus();
}, [ isWelcomeGuideVisible ] );

return (
<div className="edit-post-text-editor">
{ isRichEditingEnabled && (
Expand All @@ -37,7 +55,7 @@ export default function TextEditor() {
</div>
) }
<div className="edit-post-text-editor__body">
<PostTitle />
<PostTitleRaw ref={ titleRef } />
<PostTextEditor />
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export { HierarchicalTermSelector as PostTaxonomiesHierarchicalTermSelector } fr
export { default as PostTaxonomiesCheck } from './post-taxonomies/check';
export { default as PostTextEditor } from './post-text-editor';
export { default as PostTitle } from './post-title';
export { default as PostTitleRaw } from './post-title/post-title-raw';
export { default as PostTrash } from './post-trash';
export { default as PostTrashCheck } from './post-trash/check';
export { default as PostTypeSupportCheck } from './post-type-support-check';
Expand Down
4 changes: 4 additions & 0 deletions packages/editor/src/components/post-title/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const DEFAULT_CLASSNAMES =
'wp-block wp-block-post-title block-editor-block-list__block editor-post-title editor-post-title__input rich-text';

export const REGEXP_NEWLINES = /[\r\n]+/g;
145 changes: 57 additions & 88 deletions packages/editor/src/components/post-title/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,12 @@ import classnames from 'classnames';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from '@wordpress/element';
import { forwardRef, useState } from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
import { ENTER } from '@wordpress/keycodes';
import { useSelect, useDispatch } from '@wordpress/data';
import { pasteHandler } from '@wordpress/blocks';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { ENTER } from '@wordpress/keycodes';
import { pasteHandler } from '@wordpress/blocks';
import {
__unstableUseRichText as useRichText,
create,
Expand All @@ -31,78 +25,45 @@ import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
/**
* Internal dependencies
*/
import PostTypeSupportCheck from '../post-type-support-check';
import { store as editorStore } from '../../store';

/**
* Constants
*/
const REGEXP_NEWLINES = /[\r\n]+/g;
import { DEFAULT_CLASSNAMES, REGEXP_NEWLINES } from './constants';
import usePostTitleFocus from './use-post-title-focus';
import usePostTitle from './use-post-title';
import PostTypeSupportCheck from '../post-type-support-check';

function PostTitle( _, forwardedRef ) {
getdave marked this conversation as resolved.
Show resolved Hide resolved
const ref = useRef();
const { placeholder, hasFixedToolbar } = useSelect( ( select ) => {
const { getEditedPostAttribute } = select( editorStore );
const { getSettings } = select( blockEditorStore );
const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } =
getSettings();

return {
title: getEditedPostAttribute( 'title' ),
placeholder: titlePlaceholder,
hasFixedToolbar: _hasFixedToolbar,
};
}, [] );

const [ isSelected, setIsSelected ] = useState( false );
const { editPost } = useDispatch( editorStore );
const { insertDefaultBlock, clearSelectedBlock, insertBlocks } =
useDispatch( blockEditorStore );
const { isCleanNewPost, title, placeholder, hasFixedToolbar } = useSelect(
( select ) => {
const { getEditedPostAttribute, isCleanNewPost: _isCleanNewPost } =
select( editorStore );
const { getSettings } = select( blockEditorStore );
const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } =
getSettings();

return {
isCleanNewPost: _isCleanNewPost(),
title: getEditedPostAttribute( 'title' ),
placeholder: titlePlaceholder,
hasFixedToolbar: _hasFixedToolbar,
};
},
[]
);

useImperativeHandle( forwardedRef, () => ( {
focus: () => {
ref?.current?.focus();
},
} ) );
const { ref: focusRef } = usePostTitleFocus( forwardedRef );

useEffect( () => {
if ( ! ref.current ) {
return;
}
const { title, setTitle: onUpdate } = usePostTitle();

const { defaultView } = ref.current.ownerDocument;
const { name, parent } = defaultView;
const ownerDocument =
name === 'editor-canvas' ? parent.document : defaultView.document;
const { activeElement, body } = ownerDocument;

// Only autofocus the title when the post is entirely empty. This should
// only happen for a new post, which means we focus the title on new
// post so the author can start typing right away, without needing to
// click anything.
if ( isCleanNewPost && ( ! activeElement || body === activeElement ) ) {
ref.current.focus();
}
}, [ isCleanNewPost ] );
const [ selection, setSelection ] = useState( {} );

function onEnterPress() {
insertDefaultBlock( undefined, undefined, 0 );
const { clearSelectedBlock, insertBlocks, insertDefaultBlock } =
useDispatch( blockEditorStore );

function onChange( value ) {
onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) );
}

function onInsertBlockAfter( blocks ) {
insertBlocks( blocks, 0 );
}

function onUpdate( newTitle ) {
editPost( { title: newTitle } );
}

const [ selection, setSelection ] = useState( {} );

function onSelect() {
setIsSelected( true );
clearSelectedBlock();
Expand All @@ -113,8 +74,8 @@ function PostTitle( _, forwardedRef ) {
setSelection( {} );
}

function onChange( value ) {
onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) );
function onEnterPress() {
insertDefaultBlock( undefined, undefined, 0 );
}

function onKeyDown( event ) {
Expand Down Expand Up @@ -170,7 +131,13 @@ function PostTitle( _, forwardedRef ) {
( firstBlock.name === 'core/heading' ||
firstBlock.name === 'core/paragraph' )
) {
onUpdate( stripHTML( firstBlock.attributes.content ) );
// Strip HTML to avoid unwanted HTML being added to the title.
// In the majority of cases it is assumed that HTML in the title
// is undesirable.
const contentNoHTML = stripHTML(
firstBlock.attributes.content
);
onUpdate( contentNoHTML );
onInsertBlockAfter( content.slice( 1 ) );
} else {
onInsertBlockAfter( content );
Expand All @@ -180,10 +147,13 @@ function PostTitle( _, forwardedRef ) {
...create( { html: title } ),
...selection,
};
const newValue = insert(
value,
create( { html: stripHTML( content ) } )
);

// Strip HTML to avoid unwanted HTML being added to the title.
// In the majority of cases it is assumed that HTML in the title
// is undesirable.
const contentNoHTML = stripHTML( content );

const newValue = insert( value, create( { html: contentNoHTML } ) );
onUpdate( toHTMLString( { value: newValue } ) );
setSelection( {
start: newValue.start,
Expand All @@ -192,21 +162,13 @@ function PostTitle( _, forwardedRef ) {
}
}

// The wp-block className is important for editor styles.
// This same block is used in both the visual and the code editor.
const className = classnames(
'wp-block wp-block-post-title block-editor-block-list__block editor-post-title editor-post-title__input rich-text',
{
'is-selected': isSelected,
'has-fixed-toolbar': hasFixedToolbar,
}
);
const decodedPlaceholder =
decodeEntities( placeholder ) || __( 'Add title' );

const { ref: richTextRef } = useRichText( {
value: title,
onChange,
placeholder: decodedPlaceholder,
decodedPlaceholder,
Copy link
Contributor

Choose a reason for hiding this comment

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

@getdave This change makes the placeholder text now show in the post editor.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll look into it

selectionStart: selection.start,
selectionEnd: selection.end,
onSelectionChange( newStart, newEnd ) {
Expand All @@ -221,14 +183,21 @@ function PostTitle( _, forwardedRef ) {
};
} );
},
__unstableDisableFormats: true,
__unstableDisableFormats: false,
Comment on lines -224 to +186
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is what allows formats (HTML) to render in the field when in Visual mode.

Copy link
Member

Choose a reason for hiding this comment

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

Then this prop can be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeh it can. Leaving it in for explanation purposes but now it can go.

} );

// The wp-block className is important for editor styles.
// This same block is used in both the visual and the code editor.
const className = classnames( DEFAULT_CLASSNAMES, {
'is-selected': isSelected,
'has-fixed-toolbar': hasFixedToolbar,
} );

/* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
return (
/* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
<PostTypeSupportCheck supportKeys="title">
<h1
ref={ useMergeRefs( [ richTextRef, ref ] ) }
ref={ useMergeRefs( [ richTextRef, focusRef ] ) }
contentEditable
className={ className }
aria-label={ decodedPlaceholder }
Expand All @@ -241,8 +210,8 @@ function PostTitle( _, forwardedRef ) {
onPaste={ onPaste }
/>
</PostTypeSupportCheck>
/* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
);
/* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
}

export default forwardRef( PostTitle );
81 changes: 81 additions & 0 deletions packages/editor/src/components/post-title/post-title-raw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { TextareaControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
import { useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { useState, forwardRef } from '@wordpress/element';

/**
* Internal dependencies
*/
import { DEFAULT_CLASSNAMES, REGEXP_NEWLINES } from './constants';
import usePostTitleFocus from './use-post-title-focus';
import usePostTitle from './use-post-title';

function PostTitleRaw( _, forwardedRef ) {
const { placeholder, hasFixedToolbar } = useSelect( ( select ) => {
const { getSettings } = select( blockEditorStore );
const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } =
getSettings();

return {
placeholder: titlePlaceholder,
hasFixedToolbar: _hasFixedToolbar,
};
}, [] );

const [ isSelected, setIsSelected ] = useState( false );

const { title, setTitle: onUpdate } = usePostTitle();
const { ref: focusRef } = usePostTitleFocus( forwardedRef );

function onChange( value ) {
onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) );
}

function onSelect() {
setIsSelected( true );
}

function onUnselect() {
setIsSelected( false );
}

// The wp-block className is important for editor styles.
// This same block is used in both the visual and the code editor.
const className = classnames( DEFAULT_CLASSNAMES, {
'is-selected': isSelected,
'has-fixed-toolbar': hasFixedToolbar,
'is-raw-text': true,
} );

const decodedPlaceholder =
decodeEntities( placeholder ) || __( 'Add title' );

return (
<TextareaControl
getdave marked this conversation as resolved.
Show resolved Hide resolved
ref={ focusRef }
value={ title }
onChange={ onChange }
onFocus={ onSelect }
onBlur={ onUnselect }
label={ placeholder }
className={ className }
placeholder={ decodedPlaceholder }
hideLabelFromVision={ true }
autoComplete="off"
dir="auto"
__nextHasNoMarginBottom
/>
);
}

export default forwardRef( PostTitleRaw );
Loading
Loading