From 7ab272b5da0144440bd4688e416470332ee789a3 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 10 Jul 2023 17:35:39 +0200 Subject: [PATCH] Footnotes: save numbering through the entity provider (#52423) * Footnotes: save numbering through the entity provider * Add sup so no styling is needed at all * Migrate old format * Restore old styles, fix nested attribute queries * Fix anchor selection * Migrate markup in entity provider instead * Fix tests * Fix typo * Fix comment --------- Co-authored-by: Miguel Fonseca <150562+mcsf@users.noreply.github.com> --- .../block-library/src/footnotes/format.js | 8 +- .../block-library/src/footnotes/style.scss | 6 +- packages/core-data/src/entity-provider.js | 77 +++++++++++++++++-- .../src/component/use-select-object.js | 7 +- packages/rich-text/src/to-dom.js | 4 + packages/rich-text/src/to-tree.js | 8 +- .../specs/editor/various/footnotes.spec.js | 16 ++-- 7 files changed, 102 insertions(+), 24 deletions(-) diff --git a/packages/block-library/src/footnotes/format.js b/packages/block-library/src/footnotes/format.js index 7c1b190fc42af..40de6a132ea99 100644 --- a/packages/block-library/src/footnotes/format.js +++ b/packages/block-library/src/footnotes/format.js @@ -24,11 +24,9 @@ import { name } from './block.json'; export const formatName = 'core/footnote'; export const format = { title: __( 'Footnote' ), - tagName: 'a', + tagName: 'sup', className: 'fn', attributes: { - id: 'id', - href: 'href', 'data-fn': 'data-fn', }, contentEditable: false, @@ -50,11 +48,9 @@ export const format = { { type: formatName, attributes: { - href: '#' + id, - id: `${ id }-link`, 'data-fn': id, }, - innerHTML: '*', + innerHTML: `*`, }, value.end, value.end diff --git a/packages/block-library/src/footnotes/style.scss b/packages/block-library/src/footnotes/style.scss index ad49bc1cf2911..aa7ab8b6951dd 100644 --- a/packages/block-library/src/footnotes/style.scss +++ b/packages/block-library/src/footnotes/style.scss @@ -1,9 +1,11 @@ +// These styles are for backwards compatibility with the old footnotes anchors. +// Can be removed in the future. .editor-styles-wrapper, .entry-content { counter-reset: footnotes; } -[data-fn].fn { +a[data-fn].fn { vertical-align: super; font-size: smaller; counter-increment: footnotes; @@ -12,7 +14,7 @@ text-indent: -9999999px; } -[data-fn].fn::after { +a[data-fn].fn::after { content: "[" counter(footnotes) "]"; text-indent: 0; float: left; diff --git a/packages/core-data/src/entity-provider.js b/packages/core-data/src/entity-provider.js index da048944f1498..6cc1e021841b4 100644 --- a/packages/core-data/src/entity-provider.js +++ b/packages/core-data/src/entity-provider.js @@ -191,9 +191,10 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { const updateFootnotes = useCallback( ( _blocks ) => { - if ( ! meta ) return; + const output = { blocks: _blocks }; + if ( ! meta ) return output; // If meta.footnotes is empty, it means the meta is not registered. - if ( meta.footnotes === undefined ) return {}; + if ( meta.footnotes === undefined ) return output; const { getRichTextValues } = unlock( blockEditorPrivateApis ); const _content = getRichTextValues( _blocks ).join( '' ) || ''; @@ -215,7 +216,8 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { : []; const currentOrder = footnotes.map( ( fn ) => fn.id ); - if ( currentOrder.join( '' ) === newOrder.join( '' ) ) return; + if ( currentOrder.join( '' ) === newOrder.join( '' ) ) + return output; const newFootnotes = newOrder.map( ( fnId ) => @@ -226,6 +228,71 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { } ); + function updateAttributes( attributes ) { + attributes = { ...attributes }; + + for ( const key in attributes ) { + const value = attributes[ key ]; + + if ( Array.isArray( value ) ) { + attributes[ key ] = value.map( updateAttributes ); + continue; + } + + if ( typeof value !== 'string' ) { + continue; + } + + if ( value.indexOf( 'data-fn' ) === -1 ) { + continue; + } + + // When we store rich text values, this would no longer + // require a regex. + const regex = + /(]+data-fn="([^"]+)"[^>]*>]*>)[\d*]*<\/a><\/sup>/g; + + attributes[ key ] = value.replace( + regex, + ( match, opening, fnId ) => { + const index = newOrder.indexOf( fnId ); + return `${ opening }${ index + 1 }`; + } + ); + + const compatRegex = + /]+data-fn="([^"]+)"[^>]*>\*<\/a>/g; + + attributes[ key ] = attributes[ key ].replace( + compatRegex, + ( match, fnId ) => { + const index = newOrder.indexOf( fnId ); + return `${ + index + 1 + }`; + } + ); + } + + return attributes; + } + + function updateBlocksAttributes( __blocks ) { + return __blocks.map( ( block ) => { + return { + ...block, + attributes: updateAttributes( block.attributes ), + innerBlocks: updateBlocksAttributes( + block.innerBlocks + ), + }; + } ); + } + + // We need to go through all block attributs deeply and update the + // footnote anchor numbering (textContent) to match the new order. + const newBlocks = updateBlocksAttributes( _blocks ); + oldFootnotes = { ...oldFootnotes, ...footnotes.reduce( ( acc, fn ) => { @@ -241,6 +308,7 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { ...meta, footnotes: JSON.stringify( newFootnotes ), }, + blocks: newBlocks, }; }, [ meta ] @@ -258,7 +326,6 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { // to make sure the edit makes the post dirty and creates // a new undo level. const edits = { - blocks: newBlocks, selection, content: ( { blocks: blocksForSerialization = [] } ) => __unstableSerializeAndClean( blocksForSerialization ), @@ -282,7 +349,7 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { ( newBlocks, options ) => { const { selection } = options; const footnotesChanges = updateFootnotes( newBlocks ); - const edits = { blocks: newBlocks, selection, ...footnotesChanges }; + const edits = { selection, ...footnotesChanges }; editEntityRecord( kind, name, id, edits, { isCached: true } ); }, diff --git a/packages/rich-text/src/component/use-select-object.js b/packages/rich-text/src/component/use-select-object.js index 9ecc7ed9f147c..e5db313494f48 100644 --- a/packages/rich-text/src/component/use-select-object.js +++ b/packages/rich-text/src/component/use-select-object.js @@ -25,8 +25,13 @@ export function useSelectObject() { if ( selection.containsNode( target ) ) return; const range = ownerDocument.createRange(); + // If the target is within a non editable element, select the non + // editable element. + const nodeToSelect = target.isContentEditable + ? target + : target.closest( '[contenteditable]' ); - range.selectNode( target ); + range.selectNode( nodeToSelect ); selection.removeAllRanges(); selection.addRange( range ); diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js index 828e3a4e3f6cb..305eebaf3e4a6 100644 --- a/packages/rich-text/src/to-dom.js +++ b/packages/rich-text/src/to-dom.js @@ -57,6 +57,10 @@ function getNodeByPath( node, path ) { } function append( element, child ) { + if ( child.html !== undefined ) { + return ( element.innerHTML += child.html ); + } + if ( typeof child === 'string' ) { child = element.ownerDocument.createTextNode( child ); } diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.js index b390954b79672..8a1c3ff074a55 100644 --- a/packages/rich-text/src/to-tree.js +++ b/packages/rich-text/src/to-tree.js @@ -108,7 +108,7 @@ function fromFormat( { } return { - type: formatType.tagName === '*' ? tagName : formatType.tagName, + type: tagName || formatType.tagName, object: formatType.object, attributes: restoreOnAttributes( elementAttributes, isEditableTree ), }; @@ -326,7 +326,11 @@ export function toTree( { } ) ); - if ( innerHTML ) append( pointer, innerHTML ); + if ( innerHTML ) { + append( pointer, { + html: innerHTML, + } ); + } } else { pointer = append( getParent( pointer ), diff --git a/test/e2e/specs/editor/various/footnotes.spec.js b/test/e2e/specs/editor/various/footnotes.spec.js index 1f2fd33f23f73..376962b5c99ba 100644 --- a/test/e2e/specs/editor/various/footnotes.spec.js +++ b/test/e2e/specs/editor/various/footnotes.spec.js @@ -48,7 +48,7 @@ test.describe( 'Footnotes', () => { { name: 'core/paragraph', attributes: { - content: `second paragraph*`, + content: `second paragraph1`, }, }, { @@ -72,13 +72,13 @@ test.describe( 'Footnotes', () => { { name: 'core/paragraph', attributes: { - content: `first paragraph*`, + content: `first paragraph1`, }, }, { name: 'core/paragraph', attributes: { - content: `second paragraph*`, + content: `second paragraph2`, }, }, { @@ -106,13 +106,13 @@ test.describe( 'Footnotes', () => { { name: 'core/paragraph', attributes: { - content: `second paragraph*`, + content: `second paragraph1`, }, }, { name: 'core/paragraph', attributes: { - content: `first paragraph*`, + content: `first paragraph2`, }, }, { @@ -138,7 +138,7 @@ test.describe( 'Footnotes', () => { { name: 'core/paragraph', attributes: { - content: `second paragraph*`, + content: `second paragraph1`, }, }, { @@ -202,7 +202,7 @@ test.describe( 'Footnotes', () => { { name: 'core/list-item', attributes: { - content: `1*`, + content: `11`, }, }, ], @@ -242,7 +242,7 @@ test.describe( 'Footnotes', () => { { cells: [ { - content: `1*`, + content: `11`, tag: 'td', }, {