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 3 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;
247 changes: 35 additions & 212 deletions packages/editor/src/components/post-title/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,242 +7,65 @@ 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 { useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import {
__unstableUseRichText as useRichText,
create,
toHTMLString,
insert,
} from '@wordpress/rich-text';
import { useMergeRefs } from '@wordpress/compose';
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 PostTitleRich from './post-title-rich';
import { DEFAULT_CLASSNAMES, REGEXP_NEWLINES } from './constants';
import usePostTitleFocus from './use-post-title-focus';
import usePostTitle from './use-post-title';

function PostTitle( _, forwardedRef ) {
getdave marked this conversation as resolved.
Show resolved Hide resolved
const ref = useRef();
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();
},
} ) );

useEffect( () => {
if ( ! ref.current ) {
return;
}

const { defaultView } = ref.current.ownerDocument;
const { name, parent } = defaultView;
const ownerDocument =
name === 'editor-canvas' ? parent.document : defaultView.document;
const { activeElement, body } = ownerDocument;
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,
};
}, [] );

// 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 ] );

function onEnterPress() {
insertDefaultBlock( undefined, undefined, 0 );
}

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

function onUpdate( newTitle ) {
editPost( { title: newTitle } );
}
const [ isSelected, setIsSelected ] = useState( false );

const [ selection, setSelection ] = useState( {} );
const { ref: focusRef } = usePostTitleFocus( forwardedRef );

function onSelect() {
setIsSelected( true );
clearSelectedBlock();
}

function onUnselect() {
setIsSelected( false );
setSelection( {} );
}
const { title, setTitle: onUpdate } = usePostTitle();

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

function onKeyDown( event ) {
if ( event.keyCode === ENTER ) {
event.preventDefault();
onEnterPress();
}
}

function onPaste( event ) {
const clipboardData = event.clipboardData;

let plainText = '';
let html = '';

// IE11 only supports `Text` as an argument for `getData` and will
// otherwise throw an invalid argument error, so we try the standard
// arguments first, then fallback to `Text` if they fail.
try {
plainText = clipboardData.getData( 'text/plain' );
html = clipboardData.getData( 'text/html' );
} catch ( error1 ) {
try {
html = clipboardData.getData( 'Text' );
} catch ( error2 ) {
// Some browsers like UC Browser paste plain text by default and
// don't support clipboardData at all, so allow default
// behaviour.
return;
}
}

// Allows us to ask for this information when we get a report.
window.console.log( 'Received HTML:\n\n', html );
window.console.log( 'Received plain text:\n\n', plainText );

const content = pasteHandler( {
HTML: html,
plainText,
} );

event.preventDefault();

if ( ! content.length ) {
return;
}

if ( typeof content !== 'string' ) {
const [ firstBlock ] = content;

if (
! title &&
( firstBlock.name === 'core/heading' ||
firstBlock.name === 'core/paragraph' )
) {
onUpdate( stripHTML( firstBlock.attributes.content ) );
onInsertBlockAfter( content.slice( 1 ) );
} else {
onInsertBlockAfter( content );
}
} else {
const value = {
...create( { html: title } ),
...selection,
};
const newValue = insert(
value,
create( { html: stripHTML( content ) } )
);
onUpdate( toHTMLString( { value: newValue } ) );
setSelection( {
start: newValue.start,
end: newValue.end,
} );
}
}

// 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 className = classnames( DEFAULT_CLASSNAMES, {
'is-selected': isSelected,
'has-fixed-toolbar': hasFixedToolbar,
} );

const decodedPlaceholder =
decodeEntities( placeholder ) || __( 'Add title' );
const { ref: richTextRef } = useRichText( {
value: title,
onChange,
placeholder: decodedPlaceholder,
selectionStart: selection.start,
selectionEnd: selection.end,
onSelectionChange( newStart, newEnd ) {
setSelection( ( sel ) => {
const { start, end } = sel;
if ( start === newStart && end === newEnd ) {
return sel;
}
return {
start: newStart,
end: newEnd,
};
} );
},
__unstableDisableFormats: true,
} );

/* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
return (
<PostTypeSupportCheck supportKeys="title">
<h1
ref={ useMergeRefs( [ richTextRef, ref ] ) }
contentEditable
className={ className }
aria-label={ decodedPlaceholder }
role="textbox"
aria-multiline="true"
onFocus={ onSelect }
onBlur={ onUnselect }
onKeyDown={ onKeyDown }
onKeyPress={ onUnselect }
onPaste={ onPaste }
/>
</PostTypeSupportCheck>
<PostTitleRich
ref={ focusRef }
title={ title }
onChange={ onChange }
onUpdate={ onUpdate }
className={ className }
placeholder={ decodedPlaceholder }
setIsSelected={ setIsSelected }
/>
);
/* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */
}

export default forwardRef( PostTitle );
Loading