From 01e319a69c64f8e4e3c820eb93f1b0c9a45a8cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Mon, 24 Apr 2023 19:23:00 +0300 Subject: [PATCH 01/44] wip --- docs/reference-guides/core-blocks.md | 9 ++ packages/block-library/package.json | 3 +- .../block-library/src/footnotes/block.json | 36 ++++++ packages/block-library/src/footnotes/edit.js | 72 ++++++++++++ .../block-library/src/footnotes/format.js | 105 ++++++++++++++++++ packages/block-library/src/footnotes/index.js | 29 +++++ packages/block-library/src/footnotes/save.js | 19 ++++ packages/block-library/src/index.js | 2 + packages/blocks/README.md | 2 + packages/blocks/src/api/serializer.js | 21 ++-- .../src/component/use-select-object.js | 6 +- 11 files changed, 295 insertions(+), 9 deletions(-) create mode 100644 packages/block-library/src/footnotes/block.json create mode 100644 packages/block-library/src/footnotes/edit.js create mode 100644 packages/block-library/src/footnotes/format.js create mode 100644 packages/block-library/src/footnotes/index.js create mode 100644 packages/block-library/src/footnotes/save.js diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 24cb7cfeefded1..475b6fb72afdc3 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -269,6 +269,15 @@ Add a link to a downloadable file. ([Source](https://github.com/WordPress/gutenb - **Supports:** align, anchor, color (background, gradients, link, ~~text~~) - **Attributes:** displayPreview, downloadButtonText, fileId, fileName, href, id, previewHeight, showDownloadButton, textLinkHref, textLinkTarget +## Footnotes + + ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/footnotes)) + +- **Name:** core/footnotes +- **Category:** text +- **Supports:** ~~html~~ +- **Attributes:** footnotes + ## Classic Use the classic WordPress editor. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/freeform)) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 91ccb7552f6b42..8d145f51d4a760 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -73,7 +73,8 @@ "memize": "^2.1.0", "micromodal": "^0.4.10", "preact": "^10.13.2", - "remove-accents": "^0.4.2" + "remove-accents": "^0.4.2", + "uuid": "^8.3.0" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/block-library/src/footnotes/block.json b/packages/block-library/src/footnotes/block.json new file mode 100644 index 00000000000000..ac5f8b4b2b204a --- /dev/null +++ b/packages/block-library/src/footnotes/block.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "core/footnotes", + "title": "Footnotes", + "category": "text", + "description": "", + "keywords": [ "references" ], + "textdomain": "default", + "attributes": { + "footnotes": { + "type": "array", + "default": [], + "source": "query", + "selector": "ol", + "query": { + "content": { + "type": "string", + "source": "html", + "selector": "li span" + }, + "id": { + "type": "string", + "source": "attribute", + "attribute": "id", + "selector": "li" + } + } + } + }, + "supports": { + "html": false + }, + "editorStyle": "wp-block-footnotes-editor", + "style": "wp-block-footnotes" +} diff --git a/packages/block-library/src/footnotes/edit.js b/packages/block-library/src/footnotes/edit.js new file mode 100644 index 00000000000000..f1e5e5c122ae54 --- /dev/null +++ b/packages/block-library/src/footnotes/edit.js @@ -0,0 +1,72 @@ +/** + * WordPress dependencies + */ +import { RichText, useBlockProps } from '@wordpress/block-editor'; +import { useRefEffect } from '@wordpress/compose'; + +export default function FootnotesEdit( { attributes, setAttributes } ) { + const ref = useRefEffect( + ( element ) => { + const { ownerDocument } = element; + const { defaultView } = ownerDocument; + const config = { childList: true, subtree: true }; + const observer = new defaultView.MutationObserver( () => { + const newOrder = Array.from( + ownerDocument.querySelectorAll( 'a.fn' ) + ).map( ( node ) => { + return node.getAttribute( 'href' ).slice( 1 ); + } ); + const currentOrder = attributes.footnotes.map( + ( footnote ) => footnote.id + ); + + if ( newOrder.join( '' ) === currentOrder.join( '' ) ) { + return; + } + + const newFootnotes = attributes.footnotes.filter( ( a ) => { + return newOrder.indexOf( a.id ) !== -1; + } ); + newFootnotes.sort( ( a, b ) => { + return newOrder.indexOf( a.id ) - newOrder.indexOf( b.id ); + } ); + setAttributes( { footnotes: newFootnotes } ); + } ); + + observer.observe( ownerDocument, config ); + return () => { + observer.disconnect(); + }; + }, + [ attributes.footnotes ] + ); + return ( + + ); +} diff --git a/packages/block-library/src/footnotes/format.js b/packages/block-library/src/footnotes/format.js new file mode 100644 index 00000000000000..894793656496ba --- /dev/null +++ b/packages/block-library/src/footnotes/format.js @@ -0,0 +1,105 @@ +/** + * External dependencies + */ +import { v4 as createId } from 'uuid'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { group as icon } from '@wordpress/icons'; +import { insert, applyFormat } from '@wordpress/rich-text'; +import { + RichTextToolbarButton, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { useSelect, useDispatch, useRegistry } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { createBlock } from '@wordpress/blocks'; + +export const format = { + title: __( 'Footnote' ), + tagName: 'a', + className: 'fn', + edit: function Edit( { value, onChange, isActive } ) { + const registry = useRegistry(); + const { + getSelectedBlockClientId, + getBlockRootClientId, + getBlockName, + getBlocks, + } = useSelect( blockEditorStore ); + const { insertBlock, selectBlock, updateBlockAttributes } = + useDispatch( blockEditorStore ); + function onClick() { + registry.batch( () => { + const id = createId(); + let newValue = insert( value, '*' ); + newValue.start = newValue.end - 1; + newValue = applyFormat( newValue, { + type: 'core/footnote', + attributes: { + href: '#' + id, + id: `${ id }-link`, + contenteditable: 'false', + }, + } ); + + const flattenBlocks = ( blocks ) => + blocks.reduce( + ( acc, block ) => [ + ...acc, + block, + ...flattenBlocks( block.innerBlocks ), + ], + [] + ); + + let fnBlock = flattenBlocks( getBlocks() ).find( + ( block ) => block.name === 'core/footnotes' + ); + + if ( ! fnBlock ) { + const clientId = getSelectedBlockClientId(); + let rootClientId = getBlockRootClientId( clientId ); + + while ( + rootClientId && + getBlockName( rootClientId ) !== 'core/post-content' + ) { + rootClientId = getBlockRootClientId( rootClientId ); + } + + fnBlock = createBlock( 'core/footnotes' ); + + insertBlock( fnBlock, undefined, rootClientId ); + } + + updateBlockAttributes( fnBlock.clientId, { + footnotes: [ + ...fnBlock.attributes.footnotes, + { + id, + content: '', + }, + ], + } ); + + onChange( newValue ); + selectBlock( fnBlock.clientId, -1 ); + } ); + } + + return ( + + ); + }, +}; diff --git a/packages/block-library/src/footnotes/index.js b/packages/block-library/src/footnotes/index.js new file mode 100644 index 00000000000000..b5856933dac2c9 --- /dev/null +++ b/packages/block-library/src/footnotes/index.js @@ -0,0 +1,29 @@ +/** + * WordPress dependencies + */ +import { group as icon } from '@wordpress/icons'; +import { registerFormatType } from '@wordpress/rich-text'; + +/** + * Internal dependencies + */ +import initBlock from '../utils/init-block'; +import edit from './edit'; +import metadata from './block.json'; +import save from './save'; +import { format } from './format'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + icon, + edit, + save, +}; + +export const init = () => { + initBlock( { name, metadata, settings } ); + registerFormatType( 'core/footnote', format ); +}; diff --git a/packages/block-library/src/footnotes/save.js b/packages/block-library/src/footnotes/save.js new file mode 100644 index 00000000000000..8892b17832ffff --- /dev/null +++ b/packages/block-library/src/footnotes/save.js @@ -0,0 +1,19 @@ +/** + * WordPress dependencies + */ +import { RichText, useBlockProps } from '@wordpress/block-editor'; + +export default function Save( { attributes } ) { + return ( +
+
    + { attributes.footnotes.map( ( { id, content } ) => ( +
  1. + { ' ' } + ↩︎ +
  2. + ) ) } +
+
+ ); +} diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 73c2f1eb1140a2..2b4a0f2fb95f2a 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -116,6 +116,7 @@ import * as termDescription from './term-description'; import * as textColumns from './text-columns'; import * as verse from './verse'; import * as video from './video'; +import * as footnotes from './footnotes'; import isBlockMetadataExperimental from './utils/is-block-metadata-experimental'; @@ -176,6 +177,7 @@ const getAllBlocks = () => { textColumns, verse, video, + footnotes, // theme blocks navigation, diff --git a/packages/blocks/README.md b/packages/blocks/README.md index 91cfec30c6a726..f58cdcca9aeca2 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -259,6 +259,7 @@ _Parameters_ - _blockTypeOrName_ `string|Object`: Block type or name. - _attributes_ `Object`: Block attributes. - _innerBlocks_ `?Array`: Nested blocks. +- _clientId_ `string`: _Returns_ @@ -273,6 +274,7 @@ _Parameters_ - _blockTypeOrName_ `string|Object`: Block type or name. - _attributes_ `Object`: Block attributes. - _innerBlocks_ `?Array`: Nested blocks. +- _clientId_ `string`: _Returns_ diff --git a/packages/blocks/src/api/serializer.js b/packages/blocks/src/api/serializer.js index 3300e0893d2459..89705bc2198e02 100644 --- a/packages/blocks/src/api/serializer.js +++ b/packages/blocks/src/api/serializer.js @@ -111,13 +111,14 @@ export function getInnerBlocksProps( props = {} ) { * @param {string|Object} blockTypeOrName Block type or name. * @param {Object} attributes Block attributes. * @param {?Array} innerBlocks Nested blocks. - * + * @param {string} clientId * @return {Object|string} Save element or raw HTML string. */ export function getSaveElement( blockTypeOrName, attributes, - innerBlocks = [] + innerBlocks = [], + clientId ) { const blockType = normalizeBlockType( blockTypeOrName ); let { save } = blockType; @@ -134,7 +135,7 @@ export function getSaveElement( blockPropsProvider.attributes = attributes; innerBlocksPropsProvider.innerBlocks = innerBlocks; - let element = save( { attributes, innerBlocks } ); + let element = save( { attributes, innerBlocks, clientId } ); if ( element !== null && @@ -183,14 +184,19 @@ export function getSaveElement( * @param {string|Object} blockTypeOrName Block type or name. * @param {Object} attributes Block attributes. * @param {?Array} innerBlocks Nested blocks. - * + * @param {string} clientId * @return {string} Save content. */ -export function getSaveContent( blockTypeOrName, attributes, innerBlocks ) { +export function getSaveContent( + blockTypeOrName, + attributes, + innerBlocks, + clientId +) { const blockType = normalizeBlockType( blockTypeOrName ); return renderToString( - getSaveElement( blockType, attributes, innerBlocks ) + getSaveElement( blockType, attributes, innerBlocks, clientId ) ); } @@ -287,7 +293,8 @@ export function getBlockInnerHTML( block ) { saveContent = getSaveContent( block.name, block.attributes, - block.innerBlocks + block.innerBlocks, + block.clientId ); } catch ( error ) {} } diff --git a/packages/rich-text/src/component/use-select-object.js b/packages/rich-text/src/component/use-select-object.js index 0866815be15758..be8c17d9fd1fc4 100644 --- a/packages/rich-text/src/component/use-select-object.js +++ b/packages/rich-text/src/component/use-select-object.js @@ -9,7 +9,10 @@ export function useSelectObject() { const { target } = event; // If the child element has no text content, it must be an object. - if ( target === element || target.textContent ) { + if ( + target === element || + ( target.textContent && target.isContentEditable ) + ) { return; } @@ -21,6 +24,7 @@ export function useSelectObject() { range.selectNode( target ); selection.removeAllRanges(); selection.addRange( range ); + event.preventDefault(); } element.addEventListener( 'click', onClick ); From 9a14fa97d2dafc5bf80bde57bb81c16900662f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Tue, 25 Apr 2023 08:23:35 +0300 Subject: [PATCH 02/44] wip --- packages/block-library/src/footnotes/edit.js | 81 ++++++++++++-------- packages/block-library/src/footnotes/save.js | 24 +++++- 2 files changed, 68 insertions(+), 37 deletions(-) diff --git a/packages/block-library/src/footnotes/edit.js b/packages/block-library/src/footnotes/edit.js index f1e5e5c122ae54..95a940dcaddc2d 100644 --- a/packages/block-library/src/footnotes/edit.js +++ b/packages/block-library/src/footnotes/edit.js @@ -3,51 +3,64 @@ */ import { RichText, useBlockProps } from '@wordpress/block-editor'; import { useRefEffect } from '@wordpress/compose'; +import { useReducer } from '@wordpress/element'; -export default function FootnotesEdit( { attributes, setAttributes } ) { - const ref = useRefEffect( - ( element ) => { - const { ownerDocument } = element; - const { defaultView } = ownerDocument; - const config = { childList: true, subtree: true }; - const observer = new defaultView.MutationObserver( () => { - const newOrder = Array.from( - ownerDocument.querySelectorAll( 'a.fn' ) - ).map( ( node ) => { - return node.getAttribute( 'href' ).slice( 1 ); - } ); - const currentOrder = attributes.footnotes.map( - ( footnote ) => footnote.id - ); +export const order = new Map(); - if ( newOrder.join( '' ) === currentOrder.join( '' ) ) { - return; - } - - const newFootnotes = attributes.footnotes.filter( ( a ) => { - return newOrder.indexOf( a.id ) !== -1; - } ); - newFootnotes.sort( ( a, b ) => { - return newOrder.indexOf( a.id ) - newOrder.indexOf( b.id ); - } ); - setAttributes( { footnotes: newFootnotes } ); +export default function FootnotesEdit( { + clientId, + attributes, + setAttributes, +} ) { + const [ , forceRender ] = useReducer( () => ( {} ) ); + const ref = useRefEffect( ( element ) => { + const { ownerDocument } = element; + const { defaultView } = ownerDocument; + const config = { childList: true, subtree: true }; + const observer = new defaultView.MutationObserver( () => { + const newOrder = Array.from( + ownerDocument.querySelectorAll( 'a.fn' ) + ).map( ( node ) => { + return node.getAttribute( 'href' ).slice( 1 ); + } ); + const footnotes = Object.fromEntries( + attributes.footnotes.map( ( { content, id } ) => [ + id, + content, + ] ) + ); + attributes.footnotes = newOrder.map( ( id ) => { + return { + content: footnotes[ id ], + id, + }; } ); + forceRender(); + } ); - observer.observe( ownerDocument, config ); - return () => { - observer.disconnect(); - }; - }, - [ attributes.footnotes ] + observer.observe( ownerDocument, config ); + return () => { + observer.disconnect(); + }; + }, [] ); + const footnotes = Object.fromEntries( + attributes.footnotes.map( ( { content, id } ) => [ id, content ] ) + ); + order.set( + clientId, + new Set( [ + ...( order.get( clientId ) || [] ), + ...Object.keys( footnotes ), + ] ) ); return (