diff --git a/lib/init.php b/lib/init.php index 88dcba4525f6e..f91d169fa4d21 100644 --- a/lib/init.php +++ b/lib/init.php @@ -57,3 +57,58 @@ function gutenberg_menu() { ); } add_action( 'admin_menu', 'gutenberg_menu', 9 ); + +register_meta( + 'post', + 'isbn', + array( + 'object_subtype' => 'post', + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'revisions_enabled' => true + ) +); + +add_filter( + 'the_content', + function ( $content ) { + $sources = get_all_registered_block_bindings_sources(); + // To do: use HTML API. + return preg_replace_callback( + '/<\/\/([^>]*)>/', + function ( $matches ) use ( $sources ) { + $attributes = explode(' ', $matches[1]); + $key = array_shift($attributes); + if ( ! isset( $sources["core/$key"] ) ) { + return ''; + } + $attributes = array_reduce( + $attributes, + function ( $carry, $item ) { + $parts = explode('=', $item); + $carry[ $parts[0] ] = $parts[1]; + return $carry; + }, + array() + ); + // We need to change this function so it doesn't rely on a block + // instance. + return $sources["core/$key"]->get_value( + $attributes, + new WP_Block( + array( + 'blockName' => 'core/paragraph', + ), + array( + 'postId' => get_the_ID(), + 'postType' => get_post_type(), + ) + ), + '' + ); + }, + $content + ); + }, +); diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index fac1a594b1c95..d44dcb4b27a66 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -11,6 +11,8 @@ import { useCallback, forwardRef, createContext, + createPortal, + useContext, } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { useMergeRefs, useInstanceId } from '@wordpress/compose'; @@ -48,6 +50,7 @@ import { Content, valueToHTMLString } from './content'; import { withDeprecations } from './with-deprecations'; import { unlock } from '../../lock-unlock'; import { canBindBlock } from '../../hooks/use-bindings-attributes'; +import BlockContext from '../block-context'; export const keyboardShortcutContext = createContext(); export const inputEventContext = createContext(); @@ -321,6 +324,7 @@ export function RichTextWrapper( getValue, onChange, ref: richTextRef, + replacementRefs, } = useRichText( { value: adjustedValue, onChange( html, { __unstableFormats, __unstableText } ) { @@ -464,10 +468,54 @@ export function RichTextWrapper( } data-wp-block-attribute-key={ identifier } /> + { replacementRefs.map( ( ref ) => { + return ( + ref && + createPortal( + , + ref + ) + ); + } ) } ); } +function Binding( { content } ) { + const context = useContext( BlockContext ); + const blockBindingsSources = useSelect( ( select ) => { + return unlock( select( blocksStore ) ).getAllBlockBindingsSources(); + } ); + + if ( ! content.startsWith( '/wp:' ) ) { + return null; + } + + const fakeHTML = '<' + content.slice( 4 ) + '>'; + const body = document.implementation.createHTMLDocument( '' ).body; + body.innerHTML = fakeHTML; + const element = body.firstElementChild; + const tag = 'core/' + element.tagName.toLowerCase(); + const source = blockBindingsSources[ tag ]; + + if ( ! source ) { + return null; + } + + const value = source.useSource( + { + context, + }, + { key: element.getAttribute( 'key' ) } + ); + + return value.value.toString() || value.placeholder; +} + // This is the private API for the RichText component. // It allows access to all props, not just the public ones. export const PrivateRichText = withDeprecations( diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index 6767498548a64..cc9e7921921d6 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -38,6 +38,7 @@ export function useRichText( { const registry = useRegistry(); const [ , forceRender ] = useReducer( () => ( {} ) ); const ref = useRef(); + const replacementRefs = useRef( [] ); function createRecord() { const { @@ -62,6 +63,15 @@ export function useRichText( { __unstableDomOnly: domOnly, placeholder, } ); + + ref.current + .querySelectorAll( '[data-rich-text-comment]' ) + .forEach( ( node, i ) => { + if ( replacementRefs.current[ i ] !== node ) { + replacementRefs.current[ i ] = node; + forceRender(); + } + } ); } // Internal values are updated synchronously, unlike props and state. @@ -218,6 +228,7 @@ export function useRichText( { getValue: () => record.current, onChange: handleChange, ref: mergedRefs, + replacementRefs: replacementRefs.current, }; } diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index 8ac79799b22ef..b712fcc6ceae0 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -145,10 +145,7 @@ export class RichTextData { // We could expose `toHTMLElement` at some point as well, but we'd only use // it internally. toHTMLString( { preserveWhiteSpace } = {} ) { - return ( - this.originalHTML || - toHTMLString( { value: this.#value, preserveWhiteSpace } ) - ); + return toHTMLString( { value: this.#value, preserveWhiteSpace } ); } valueOf() { return this.toHTMLString(); @@ -469,6 +466,34 @@ function createFromElement( { element, range, isEditableTree } ) { continue; } + if ( + node.nodeType === node.COMMENT_NODE || + ( node.nodeType === node.ELEMENT_NODE && + node.tagName === 'SPAN' && + node.hasAttribute( 'data-rich-text-comment' ) ) + ) { + const value = { + formats: [ , ], + replacements: [ + { + type: '#comment', + attributes: { + 'data-rich-text-comment': + node.nodeType === node.COMMENT_NODE + ? node.nodeValue + : node.getAttribute( + 'data-rich-text-comment' + ), + }, + }, + ], + text: OBJECT_REPLACEMENT_CHARACTER, + }; + accumulateSelection( accumulator, node, range, value ); + mergePair( accumulator, value ); + continue; + } + if ( node.nodeType !== node.ELEMENT_NODE ) { continue; } diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js index e7288e4ba1633..d8eb3babe6682 100644 --- a/packages/rich-text/src/to-dom.js +++ b/packages/rich-text/src/to-dom.js @@ -68,10 +68,16 @@ function append( element, child ) { const { type, attributes } = child; if ( type ) { - child = element.ownerDocument.createElement( type ); + if ( type === '#comment' ) { + child = element.ownerDocument.createComment( + attributes[ 'data-rich-text-comment' ] + ); + } else { + child = element.ownerDocument.createElement( type ); - for ( const key in attributes ) { - child.setAttribute( key, attributes[ key ] ); + for ( const key in attributes ) { + child.setAttribute( key, attributes[ key ] ); + } } } @@ -238,7 +244,9 @@ export function applyValue( future, current ) { } } - applyValue( futureChild, currentChild ); + if ( ! currentChild.hasAttribute( 'data-rich-text-comment' ) ) { + applyValue( futureChild, currentChild ); + } future.removeChild( futureChild ); } } else { diff --git a/packages/rich-text/src/to-html-string.js b/packages/rich-text/src/to-html-string.js index 35089003f0b3f..d1f7b0f4d8fa2 100644 --- a/packages/rich-text/src/to-html-string.js +++ b/packages/rich-text/src/to-html-string.js @@ -88,6 +88,10 @@ function remove( object ) { } function createElementHTML( { type, attributes, object, children } ) { + if ( type === '#comment' ) { + return ``; + } + let attributeString = ''; for ( const key in attributes ) { diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.js index c380570db561d..49c6844030b2f 100644 --- a/packages/rich-text/src/to-tree.js +++ b/packages/rich-text/src/to-tree.js @@ -227,7 +227,17 @@ export function toTree( { const { type, attributes, innerHTML } = replacement; const formatType = getFormatType( type ); - if ( ! isEditableTree && type === 'script' ) { + if ( isEditableTree && type === '#comment' ) { + pointer = append( getParent( pointer ), { + type: 'span', + attributes: { + contenteditable: 'false', + 'data-rich-text-comment': + attributes[ 'data-rich-text-comment' ], + style: 'background:yellow', + }, + } ); + } else if ( ! isEditableTree && type === 'script' ) { pointer = append( getParent( pointer ), fromFormat( {