UNPKG

@wordpress/editor

Version:
215 lines (185 loc) 5.75 kB
/** * External dependencies */ import clsx from 'clsx'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { forwardRef, useState } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { ENTER } from '@wordpress/keycodes'; import { pasteHandler } from '@wordpress/blocks'; import { privateApis as richTextPrivateApis, create, insert, } from '@wordpress/rich-text'; import { useMergeRefs } from '@wordpress/compose'; import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; /** * Internal dependencies */ 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'; import { unlock } from '../../lock-unlock'; const { useRichText } = unlock( richTextPrivateApis ); const PostTitle = forwardRef( ( _, forwardedRef ) => { const { placeholder, isEditingContentOnlySection, isPreview } = useSelect( ( select ) => { const { getSettings, getEditedContentOnlySection } = unlock( select( blockEditorStore ) ); const { titlePlaceholder, isPreviewMode } = getSettings(); return { placeholder: titlePlaceholder, isEditingContentOnlySection: !! getEditedContentOnlySection(), isPreview: isPreviewMode, }; }, [] ); const [ isSelected, setIsSelected ] = useState( false ); const { ref: focusRef } = usePostTitleFocus( forwardedRef ); const { title, setTitle: onUpdate } = usePostTitle(); const [ selection, setSelection ] = useState( {} ); const { clearSelectedBlock, insertBlocks, insertDefaultBlock } = useDispatch( blockEditorStore ); const decodedPlaceholder = decodeEntities( placeholder ) || __( 'Add title' ); const { value, onChange, ref: richTextRef, } = useRichText( { value: title, onChange( newValue ) { onUpdate( newValue.replace( REGEXP_NEWLINES, ' ' ) ); }, 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: false, } ); function onInsertBlockAfter( blocks ) { insertBlocks( blocks, 0 ); } function onSelect() { setIsSelected( true ); clearSelectedBlock(); } function onUnselect() { setIsSelected( false ); setSelection( {} ); } function onEnterPress() { insertDefaultBlock( undefined, undefined, 0 ); } function onKeyDown( event ) { if ( event.keyCode === ENTER ) { event.preventDefault(); onEnterPress(); } } function onPaste( event ) { const clipboardData = event.clipboardData; let plainText = ''; let html = ''; try { plainText = clipboardData.getData( 'text/plain' ); html = clipboardData.getData( 'text/html' ); } catch ( error ) { // Some browsers like UC Browser paste plain text by default and // don't support clipboardData at all, so allow default // behaviour. return; } 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' ) ) { // 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 ); } } else { // 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 ); onChange( insert( value, create( { html: contentNoHTML } ) ) ); } } // The wp-block className is important for editor styles. // This same block is used in both the visual and the code editor. const className = clsx( DEFAULT_CLASSNAMES, { 'is-selected': isSelected, } ); // Because the title is within the editor iframe, we can't use scss styles. // Instead use an inline style to dim the block when it's disabled. const style = isEditingContentOnlySection ? { opacity: 0.2 } : undefined; return ( /* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */ <h1 ref={ useMergeRefs( [ richTextRef, focusRef ] ) } contentEditable={ ! isEditingContentOnlySection && ! isPreview } className={ className } aria-label={ decodedPlaceholder } role="textbox" aria-multiline="true" onFocus={ onSelect } onBlur={ onUnselect } onKeyDown={ onKeyDown } onPaste={ onPaste } style={ style } /> /* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */ ); } ); /** * Renders the `PostTitle` component. * * @param {Object} _ Unused parameter. * @param {Element} forwardedRef Forwarded ref for the component. * * @return {React.ReactNode} The rendered PostTitle component. */ export default forwardRef( ( _, forwardedRef ) => ( <PostTypeSupportCheck supportKeys="title"> <PostTitle ref={ forwardedRef } /> </PostTypeSupportCheck> ) );