From 791305a109f974d75f4e93ff788d6507e521e59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Wed, 2 May 2018 21:33:56 +0200 Subject: [PATCH 01/36] Add footnotes control button to paragraph block Adds a button to the toolbar to add footnotes. When clicked, a SUP tag is added the paragraph content. For now, the button is only activated in paragraphs blocks. --- core-blocks/paragraph/index.js | 3 +++ .../rich-text/format-toolbar/index.js | 18 ++++++++++++++++++ editor/components/rich-text/index.js | 11 +++++++++++ 3 files changed, 32 insertions(+) diff --git a/core-blocks/paragraph/index.js b/core-blocks/paragraph/index.js index d7df95c47dc7ef..8cc56818e9abf5 100644 --- a/core-blocks/paragraph/index.js +++ b/core-blocks/paragraph/index.js @@ -163,6 +163,8 @@ class ParagraphBlock extends Component { const fontSize = this.getFontSize(); + const formattingControls = [ 'bold', 'italic', 'strikethrough', 'link', 'footnote' ]; + return ( @@ -251,6 +253,7 @@ class ParagraphBlock extends Component { onReplace={ this.onReplace } onRemove={ () => onReplace( [] ) } placeholder={ placeholder || __( 'Add text or type / to add content' ) } + formattingControls={ formattingControls } /> diff --git a/editor/components/rich-text/format-toolbar/index.js b/editor/components/rich-text/format-toolbar/index.js index 62d48671769de4..a49280152601df 100644 --- a/editor/components/rich-text/format-toolbar/index.js +++ b/editor/components/rich-text/format-toolbar/index.js @@ -47,6 +47,11 @@ const FORMATTING_CONTROLS = [ shortcut: displayShortcut.primary( 'k' ), format: 'link', }, + { + icon: 'editor-textcolor', // TODO: Need proper footnote icon + title: __( 'Footnote' ), + format: 'footnote', + }, ]; // Default controls shown if no `enabledControls` prop provided @@ -114,6 +119,12 @@ class FormatToolbar extends Component { }; } + addFootnote() { + return () => { + this.props.onAddFootnote(); + }; + } + toggleLinkSettingsVisibility() { this.setState( ( state ) => ( { settingsVisible: ! state.settingsVisible } ) ); } @@ -185,6 +196,13 @@ class FormatToolbar extends Component { }; } + if ( control.format === 'footnote' ) { + return { + ...control, + onClick: this.addFootnote(), + }; + } + return { ...control, onClick: this.toggleFormat( control.format ), diff --git a/editor/components/rich-text/index.js b/editor/components/rich-text/index.js index d1d54bf312d3eb..d611b40279d7ab 100644 --- a/editor/components/rich-text/index.js +++ b/editor/components/rich-text/index.js @@ -1,6 +1,7 @@ /** * External dependencies */ +import uuid from 'uuid/v4'; import classnames from 'classnames'; import { last, @@ -119,6 +120,7 @@ export class RichText extends Component { this.onKeyDown = this.onKeyDown.bind( this ); this.onKeyUp = this.onKeyUp.bind( this ); this.changeFormats = this.changeFormats.bind( this ); + this.addFootnote = this.addFootnote.bind( this ); this.onPropagateUndo = this.onPropagateUndo.bind( this ); this.onPastePreProcess = this.onPastePreProcess.bind( this ); this.onPaste = this.onPaste.bind( this ); @@ -829,6 +831,14 @@ export class RichText extends Component { } ) ); } + addFootnote() { + this.editor.selection.collapse(); + if ( this.editor.selection.getNode().tagName === 'SUP' ) { + return; + } + this.editor.insertContent( '* ' ); + } + /** * Calling onSplit means we need to abort the change done by TinyMCE. * we need to call updateContent to restore the initial content before calling onSplit. @@ -875,6 +885,7 @@ export class RichText extends Component { focusPosition={ this.state.focusPosition } formats={ this.state.formats } onChange={ this.changeFormats } + onAddFootnote={ this.addFootnote } enabledControls={ formattingControls } customControls={ formatters } /> From 533aa51007c20b2cdff7ec258d3271f2f3fe1dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Thu, 3 May 2018 01:56:17 +0200 Subject: [PATCH 02/36] Store footnote ids in the block object Everytime the block is modified, it parses its code and stores the footnotes in the block object. --- blocks/api/index.js | 1 + blocks/api/parser.js | 28 ++++++++++++++++++++++++++++ blocks/api/test/parser.js | 26 ++++++++++++++++++++++++++ editor/store/reducer.js | 3 ++- 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/blocks/api/index.js b/blocks/api/index.js index 897aba32f31761..5d93473c79f193 100644 --- a/blocks/api/index.js +++ b/blocks/api/index.js @@ -9,6 +9,7 @@ export { export { default as parse, getBlockAttributes, + parseFootnotesFromContent, parseWithAttributeSchema, } from './parser'; export { default as rawHandler, getPhrasingContentSchema } from './raw-handling'; diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 99d13fbb83e8fb..8d1e95a304e2f2 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -205,6 +205,29 @@ export function getAttributesAndInnerBlocksFromDeprecatedVersion( blockType, inn } } +/** + * Parses the content and extracts the list of footnotes. + * + * @param {?Array} content The content to parse. + * + * @return {Array} Array of footnote ids. + */ +export function parseFootnotesFromContent( content ) { + if ( ! content || ! Array.isArray( content ) ) { + return []; + } + + return content.reduce( ( footnotes, element ) => { + if ( element.type === 'sup' && + element.props.className === 'footnote' && + element.props[ 'data-footnote-id' ] ) { + return footnotes.concat( { id: element.props[ 'data-footnote-id' ] } ); + } + + return footnotes; + }, [] ); +} + /** * Creates a block with fallback to the unknown type handler. * @@ -297,6 +320,11 @@ export function createBlockWithFallback( blockNode ) { } } + const footnotes = parseFootnotesFromContent( block.attributes.content ); + if ( footnotes.length ) { + block.footnotes = footnotes; + } + return block; } diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index 1eacdc317e37f2..10756e5d8fafc0 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -5,6 +5,7 @@ import { getBlockAttribute, getBlockAttributes, asType, + parseFootnotesFromContent, createBlockWithFallback, getAttributesAndInnerBlocksFromDeprecatedVersion, default as parse, @@ -312,6 +313,31 @@ describe( 'block parser', () => { } ); } ); + describe( 'parseFootnotesFromContent', () => { + it( 'should return empty array if there is no content', () => { + const footnotes = parseFootnotesFromContent(); + + expect( footnotes ).toEqual( [] ); + } ); + it( 'should parse content and return footnote ids', () => { + const content = [ + 'Lorem ipsum', + { + type: 'sup', + props: { + className: 'footnote', + 'data-footnote-id': '12345', + }, + }, + 'is a text', + ]; + + const footnotes = parseFootnotesFromContent( content ); + + expect( footnotes ).toEqual( [ { id: '12345' } ] ); + } ); + } ); + describe( 'createBlockWithFallback', () => { it( 'should create the requested block if it exists', () => { registerBlockType( 'core/test-block', defaultBlockSettings ); diff --git a/editor/store/reducer.js b/editor/store/reducer.js index 64d530c74116a8..808af51c430c47 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -23,7 +23,7 @@ import { /** * WordPress dependencies */ -import { isSharedBlock } from '@wordpress/blocks'; +import { isSharedBlock, parseFootnotesFromContent } from '@wordpress/blocks'; import { combineReducers } from '@wordpress/data'; /** @@ -321,6 +321,7 @@ export const editor = flow( [ [ action.uid ]: { ...state[ action.uid ], attributes: nextAttributes, + footnotes: parseFootnotesFromContent( nextAttributes.content ), }, }; From 95237fc8f4cdda588be87c46e8987e0be5f1bec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Fri, 4 May 2018 19:22:41 +0200 Subject: [PATCH 03/36] Create footnotes block on editor setup and update footnotes on block attributes update On editor setup create the footnotes block if it doesn't exist and when any block attributes are updated, update the footnotes block with the new parsed footnotes. This commit depends on a followup that creates the footnotes block. --- blocks/api/parser.js | 2 +- blocks/api/test/parser.js | 2 +- editor/store/effects.js | 13 ++++++ editor/store/reducer.js | 84 +++++++++++++++++++++++++++++++++++- editor/store/test/effects.js | 32 ++++++++++++-- editor/store/test/reducer.js | 62 ++++++++++++++++++++++++++ 6 files changed, 188 insertions(+), 7 deletions(-) diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 8d1e95a304e2f2..2c627d76e303c3 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -221,7 +221,7 @@ export function parseFootnotesFromContent( content ) { if ( element.type === 'sup' && element.props.className === 'footnote' && element.props[ 'data-footnote-id' ] ) { - return footnotes.concat( { id: element.props[ 'data-footnote-id' ] } ); + return footnotes.concat( element.props[ 'data-footnote-id' ] ); } return footnotes; diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index 10756e5d8fafc0..a51e463e079fc2 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -334,7 +334,7 @@ describe( 'block parser', () => { const footnotes = parseFootnotesFromContent( content ); - expect( footnotes ).toEqual( [ { id: '12345' } ] ); + expect( footnotes ).toEqual( [ '12345' ] ); } ); } ); diff --git a/editor/store/effects.js b/editor/store/effects.js index 3570b8fedf64d4..be804478f6a583 100644 --- a/editor/store/effects.js +++ b/editor/store/effects.js @@ -350,6 +350,19 @@ export default { blocks = []; } + // Append footnotes block if not present + let hasFootnotesBlock = false; + for ( let i = 0; i < blocks.length; i++ ) { + if ( blocks[ i ].name === 'core/footnotes' ) { + hasFootnotesBlock = true; + break; + } + } + + if ( ! hasFootnotesBlock ) { + blocks.push( createBlock( 'core/footnotes' ) ); + } + // Include auto draft title in edits while not flagging post as dirty const edits = {}; if ( post.status === 'auto-draft' ) { diff --git a/editor/store/reducer.js b/editor/store/reducer.js index 808af51c430c47..a257412281b3f1 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -193,6 +193,84 @@ const withInnerBlocksRemoveCascade = ( reducer ) => ( state, action ) => { return reducer( state, action ); }; +/** + * Returns the footnotes contained in the blocks. + * + * @param {Object} blocksByUid Object containing the blocks where to extract + * the footnotes from. + * + * @return {Array} Footnote ids contained in the blocks. + */ +const getFootnotes = ( blocksByUid ) => { + return Object.keys( blocksByUid ).reduce( + ( footnotes, blockUid ) => + footnotes.concat( blocksByUid[ blockUid ].footnotes || [] ), + [] + ); +}; + +/** + * Returns the footnotes block. + * + * @param {Object} blocksByUid Object containing the blocks where to search + * for the footnotes block. + * + * @return {Object|null} Footnotes block or null if there isn't a footnotes + * block. + */ +const getFootnotesBlock = ( blocksByUid ) => { + for ( let i = 0; i < Object.keys( blocksByUid ).length; i++ ) { + const block = blocksByUid[ Object.keys( blocksByUid )[ i ] ]; + if ( block.name === 'core/footnotes' ) { + return block; + } + } + + return null; +}; + +/** + * Updates footnotes block with new footnotes. + * + * @param {Object} footnotesBlock Object of the footnotes block. + * @param {Array} footnotes Array of new footnotes. + * + * @return {Object} Footnotes block updated with the new footnotes. + */ +const updateFootnotesBlock = ( footnotesBlock, footnotes ) => { + return { + ...footnotesBlock, + attributes: { + ...footnotesBlock.attributes, + footnotes, + }, + }; +}; + +/** + * Updates footnotes block if it exists in the list of blocks. + * + * @param {Object} blocksByUid Object containing the blocks. + * + * @return {Object} New blocks with the footnotes block updated with the + * footnotes from the other blocks. + */ +const updateFootnotes = ( blocksByUid ) => { + const footnotesBlock = getFootnotesBlock( blocksByUid ); + + if ( ! footnotesBlock ) { + return blocksByUid; + } + + const footnotes = getFootnotes( blocksByUid ); + const updatedFootnotesBlock = updateFootnotesBlock( footnotesBlock, footnotes ); + + return { + ...blocksByUid, + [ footnotesBlock.uid ]: updatedFootnotesBlock, + }; +}; + /** * Undoable reducer returning the editor post state, including blocks parsed * from current HTML markup. @@ -281,7 +359,7 @@ export const editor = flow( [ switch ( action.type ) { case 'RESET_BLOCKS': case 'SETUP_EDITOR_STATE': - return getFlattenedBlocks( action.blocks ); + return updateFootnotes( getFlattenedBlocks( action.blocks ) ); case 'RECEIVE_BLOCKS': return { @@ -316,7 +394,7 @@ export const editor = flow( [ } // Otherwise merge attributes into state - return { + const newState = { ...state, [ action.uid ]: { ...state[ action.uid ], @@ -325,6 +403,8 @@ export const editor = flow( [ }, }; + return updateFootnotes( newState ); + case 'MOVE_BLOCK_TO_POSITION': // Avoid creating a new instance if the layout didn't change. if ( state[ action.uid ].attributes.layout === action.layout ) { diff --git a/editor/store/test/effects.js b/editor/store/test/effects.js index b80b3893bc60c1..8f15fb77741bd8 100644 --- a/editor/store/test/effects.js +++ b/editor/store/test/effects.js @@ -501,6 +501,18 @@ describe( 'effects', () => { describe( '.SETUP_EDITOR', () => { const handler = effects.SETUP_EDITOR; + beforeEach( () => { + registerBlockType( 'core/footnotes', { + title: 'Footnotes', + category: 'common', + save: () => null, + attributes: { + footnotes: [], + names: {}, + }, + } ); + } ); + afterEach( () => { getBlockTypes().forEach( ( block ) => { unregisterBlockType( block.name ); @@ -526,10 +538,17 @@ describe( 'effects', () => { } ); const result = handler( { post, settings: {} }, { getState } ); + const footnotesBlock = { + attributes: {}, + innerBlocks: [], + isValid: true, + name: 'core/footnotes', + uid: result[ 1 ].blocks[ 0 ].uid, + }; expect( result ).toEqual( [ setTemplateValidity( true ), - setupEditorState( post, [], {} ), + setupEditorState( post, [ footnotesBlock ], {} ), ] ); } ); @@ -554,7 +573,7 @@ describe( 'effects', () => { const result = handler( { post }, { getState } ); - expect( result[ 1 ].blocks ).toHaveLength( 1 ); + expect( result[ 1 ].blocks ).toHaveLength( 2 ); expect( result ).toEqual( [ setTemplateValidity( true ), setupEditorState( post, result[ 1 ].blocks, {} ), @@ -580,10 +599,17 @@ describe( 'effects', () => { } ); const result = handler( { post }, { getState } ); + const footnotesBlock = { + attributes: {}, + innerBlocks: [], + isValid: true, + name: 'core/footnotes', + uid: result[ 1 ].blocks[ 0 ].uid, + }; expect( result ).toEqual( [ setTemplateValidity( true ), - setupEditorState( post, [], { title: 'A History of Pork', status: 'draft' } ), + setupEditorState( post, [ footnotesBlock ], { title: 'A History of Pork', status: 'draft' } ), ] ); } ); } ); diff --git a/editor/store/test/reducer.js b/editor/store/test/reducer.js index 542b904792d77b..9522a0c9fa6d6e 100644 --- a/editor/store/test/reducer.js +++ b/editor/store/test/reducer.js @@ -1170,6 +1170,68 @@ describe( 'state', () => { expect( state.present.blocksByUID ).toBe( state.present.blocksByUID ); } ); + + it( 'should update footnotes when block attributes are updated', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + uid: 'kumquat', + attributes: { + content: [ + 'Lorem ipsum', + ], + }, + innerBlocks: [], + }, { + uid: 'footnotes', + name: 'core/footnotes', + attributes: { + footnotes: [], + }, + innerBlocks: [], + } ], + } ) ); + const newFootnote = 'fn-1234'; + const newContent = [ + 'Lorem ipsum', + { + type: 'sup', + props: { + className: 'footnote', + 'data-footnote-id': newFootnote, + }, + }, + 'is a text', + ]; + + const state = editor( original, { + type: 'UPDATE_BLOCK_ATTRIBUTES', + uid: 'kumquat', + attributes: { + content: newContent, + }, + } ); + + const expectedKumquatBlock = { + uid: 'kumquat', + attributes: { + content: newContent, + }, + footnotes: [ newFootnote ], + }; + const expectedFootnotesBlock = { + uid: 'footnotes', + name: 'core/footnotes', + attributes: { + footnotes: [ newFootnote ], + }, + }; + + expect( state.present.blocksByUid ).toEqual( { + kumquat: expectedKumquatBlock, + footnotes: expectedFootnotesBlock, + } ); + } ); } ); describe( 'withHistory', () => { From 213150ee05e5d699c232883c46fc1611e71dcf0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Sat, 5 May 2018 10:29:13 +0200 Subject: [PATCH 04/36] Create Footnotes block Create Footnotes block which renders the list of footnotes texts when saved and allows editing them in the post editor. --- core-blocks/footnotes/editor.scss | 3 + core-blocks/footnotes/index.js | 94 +++++++++++++++++++ .../test/__snapshots__/index.js.snap | 7 ++ core-blocks/footnotes/test/index.js | 13 +++ core-blocks/index.js | 2 + .../test/fixtures/core__footnotes.html | 3 + .../test/fixtures/core__footnotes.json | 16 ++++ .../test/fixtures/core__footnotes.parsed.json | 18 ++++ .../fixtures/core__footnotes.serialized.html | 8 ++ 9 files changed, 164 insertions(+) create mode 100644 core-blocks/footnotes/editor.scss create mode 100644 core-blocks/footnotes/index.js create mode 100644 core-blocks/footnotes/test/__snapshots__/index.js.snap create mode 100644 core-blocks/footnotes/test/index.js create mode 100644 core-blocks/test/fixtures/core__footnotes.html create mode 100644 core-blocks/test/fixtures/core__footnotes.json create mode 100644 core-blocks/test/fixtures/core__footnotes.parsed.json create mode 100644 core-blocks/test/fixtures/core__footnotes.serialized.html diff --git a/core-blocks/footnotes/editor.scss b/core-blocks/footnotes/editor.scss new file mode 100644 index 00000000000000..cdb6a814ceb8c9 --- /dev/null +++ b/core-blocks/footnotes/editor.scss @@ -0,0 +1,3 @@ +.edit-post-visual-editor .blocks-footnotes__footnotes-list { + padding-left: 1.3em; +} diff --git a/core-blocks/footnotes/index.js b/core-blocks/footnotes/index.js new file mode 100644 index 00000000000000..2e21084715b65d --- /dev/null +++ b/core-blocks/footnotes/index.js @@ -0,0 +1,94 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { withState } from '@wordpress/components'; +import { RichText } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import './editor.scss'; + +export const name = 'core/footnotes'; + +const getFootnotesText = ( text ) => { + if ( ! text ) { + return []; + } + if ( ! Array.isArray( text ) ) { + return [ text ]; + } + return text; +}; + +export const settings = { + title: __( 'Footnotes' ), + description: __( 'List of footnotes from the article' ), + category: 'common', + keywords: [ __( 'footnotes' ), __( 'references' ) ], + + attributes: { + footnotes: { + type: 'array', + default: [], + }, + texts: { + type: 'object', + default: [], + }, + }, + + edit: withState( { + editable: null, + } )( ( { attributes, editable, isSelected, setAttributes, setState } ) => { + const { footnotes, texts } = attributes; + const onSetActiveEditable = ( index ) => () => { + setState( { editable: index } ); + }; + const footnotesBlock = footnotes.map( ( footnote, i ) => ( +
  • + { + setAttributes( { + texts: { + ...texts, + [ footnote ]: nextValue, + }, + } ); + } + } + isSelected={ isSelected && editable === i } + placeholder={ __( 'Write footnote…' ) } + onFocus={ onSetActiveEditable( i ) } + /> +
  • + ) ); + + return ( +
      + { footnotesBlock } +
    + ); + } ), + + save( { attributes } ) { + const { footnotes, texts } = attributes; + const footnotesBlock = footnotes.map( ( footnote ) => ( +
  • + { getFootnotesText( texts[ footnote ] ) } +
  • + ) ); + + return ( +
    +
      + { footnotesBlock } +
    +
    + ); + }, +}; diff --git a/core-blocks/footnotes/test/__snapshots__/index.js.snap b/core-blocks/footnotes/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000000..819a2eeb733fe0 --- /dev/null +++ b/core-blocks/footnotes/test/__snapshots__/index.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`core/footnotes block edit matches snapshot 1`] = ` +
      +`; diff --git a/core-blocks/footnotes/test/index.js b/core-blocks/footnotes/test/index.js new file mode 100644 index 00000000000000..da58257ad72d8c --- /dev/null +++ b/core-blocks/footnotes/test/index.js @@ -0,0 +1,13 @@ +/** + * Internal dependencies + */ +import { name, settings } from '../'; +import { blockEditRender } from '../../test/helpers'; + +describe( 'core/footnotes', () => { + test( 'block edit matches snapshot', () => { + const wrapper = blockEditRender( name, settings ); + + expect( wrapper ).toMatchSnapshot(); + } ); +} ); diff --git a/core-blocks/index.js b/core-blocks/index.js index a6c83b08da036f..0a71e8db3e16cd 100644 --- a/core-blocks/index.js +++ b/core-blocks/index.js @@ -25,6 +25,7 @@ import * as columns from './columns'; import * as coverImage from './cover-image'; import * as embed from './embed'; import * as freeform from './freeform'; +import * as footnotes from './footnotes'; import * as html from './html'; import * as latestPosts from './latest-posts'; import * as list from './list'; @@ -65,6 +66,7 @@ export const registerCoreBlocks = () => { ...embed.common, ...embed.others, freeform, + footnotes, html, latestPosts, more, diff --git a/core-blocks/test/fixtures/core__footnotes.html b/core-blocks/test/fixtures/core__footnotes.html new file mode 100644 index 00000000000000..813f3989d2d8d6 --- /dev/null +++ b/core-blocks/test/fixtures/core__footnotes.html @@ -0,0 +1,3 @@ + +
      1. Reference 1
      2. Reference 2
      + diff --git a/core-blocks/test/fixtures/core__footnotes.json b/core-blocks/test/fixtures/core__footnotes.json new file mode 100644 index 00000000000000..b84363cc8c9dd8 --- /dev/null +++ b/core-blocks/test/fixtures/core__footnotes.json @@ -0,0 +1,16 @@ +[ + { + "uid": "_uid_0", + "name": "core/footnotes", + "isValid": true, + "attributes": { + "footnotes": [ "7edc47cb-3fe1-4ce0-ae6f-5e23e1e67aa2", "2e5f8a19-d9cd-4898-8686-cb6518450dc5" ], + "texts": { + "7edc47cb-3fe1-4ce0-ae6f-5e23e1e67aa2": "Reference 1", + "2e5f8a19-d9cd-4898-8686-cb6518450dc5": "Reference 2" + } + }, + "innerBlocks": [], + "originalContent": "
      1. Reference 1
      2. Reference 2
      " + } +] diff --git a/core-blocks/test/fixtures/core__footnotes.parsed.json b/core-blocks/test/fixtures/core__footnotes.parsed.json new file mode 100644 index 00000000000000..51b59c02ea646b --- /dev/null +++ b/core-blocks/test/fixtures/core__footnotes.parsed.json @@ -0,0 +1,18 @@ +[ + { + "blockName": "core/footnotes", + "attrs": { + "footnotes": [ "7edc47cb-3fe1-4ce0-ae6f-5e23e1e67aa2", "2e5f8a19-d9cd-4898-8686-cb6518450dc5" ], + "texts": { + "7edc47cb-3fe1-4ce0-ae6f-5e23e1e67aa2": "Reference 1", + "2e5f8a19-d9cd-4898-8686-cb6518450dc5": "Reference 2" + } + }, + "innerBlocks": [], + "innerHTML": "\n
      1. Reference 1
      2. Reference 2
      \n" + }, + { + "attrs": {}, + "innerHTML": "\n" + } +] diff --git a/core-blocks/test/fixtures/core__footnotes.serialized.html b/core-blocks/test/fixtures/core__footnotes.serialized.html new file mode 100644 index 00000000000000..c95c16df69e3ff --- /dev/null +++ b/core-blocks/test/fixtures/core__footnotes.serialized.html @@ -0,0 +1,8 @@ + +
      +
        +
      1. Reference 1
      2. +
      3. Reference 2
      4. +
      +
      + From 2e6de5ede565d04fbc756205feae5592586fbd22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Sat, 5 May 2018 11:46:28 +0200 Subject: [PATCH 05/36] Use auto-incrementing numbers instead of asterisks for footnotes Make footnotes be numbers instead of asterisks and link to the relevant footnote. --- blocks/api/parser.js | 4 +--- core-blocks/footnotes/index.js | 1 + core-blocks/footnotes/style.scss | 9 +++++++++ editor/components/rich-text/index.js | 4 +++- editor/store/test/reducer.js | 1 - 5 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 core-blocks/footnotes/style.scss diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 2c627d76e303c3..bd78c6d41af4a5 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -218,9 +218,7 @@ export function parseFootnotesFromContent( content ) { } return content.reduce( ( footnotes, element ) => { - if ( element.type === 'sup' && - element.props.className === 'footnote' && - element.props[ 'data-footnote-id' ] ) { + if ( element.type === 'sup' && element.props[ 'data-footnote-id' ] ) { return footnotes.concat( element.props[ 'data-footnote-id' ] ); } diff --git a/core-blocks/footnotes/index.js b/core-blocks/footnotes/index.js index 2e21084715b65d..a8aa8d3fabce83 100644 --- a/core-blocks/footnotes/index.js +++ b/core-blocks/footnotes/index.js @@ -8,6 +8,7 @@ import { RichText } from '@wordpress/blocks'; /** * Internal dependencies */ +import './style.scss'; import './editor.scss'; export const name = 'core/footnotes'; diff --git a/core-blocks/footnotes/style.scss b/core-blocks/footnotes/style.scss new file mode 100644 index 00000000000000..08954e1229d1c4 --- /dev/null +++ b/core-blocks/footnotes/style.scss @@ -0,0 +1,9 @@ +.post, +.edit-post-visual-editor { + counter-reset: footnotes; +} + +.wp-footnote::before { + counter-increment: footnotes; + content: counter(footnotes); +} diff --git a/editor/components/rich-text/index.js b/editor/components/rich-text/index.js index d611b40279d7ab..d0941900764490 100644 --- a/editor/components/rich-text/index.js +++ b/editor/components/rich-text/index.js @@ -19,6 +19,7 @@ import 'element-closest'; /** * WordPress dependencies */ +import { __ } from '@wordpress/i18n'; import { Component, Fragment, compose, RawHTML, createRef } from '@wordpress/element'; import { isHorizontalEdge, @@ -836,7 +837,8 @@ export class RichText extends Component { if ( this.editor.selection.getNode().tagName === 'SUP' ) { return; } - this.editor.insertContent( '* ' ); + const uid = uuid(); + this.editor.insertContent( '' + __( 'See footnote' ) + ' ' ); } /** diff --git a/editor/store/test/reducer.js b/editor/store/test/reducer.js index 9522a0c9fa6d6e..e5f11ac43b741c 100644 --- a/editor/store/test/reducer.js +++ b/editor/store/test/reducer.js @@ -1197,7 +1197,6 @@ describe( 'state', () => { { type: 'sup', props: { - className: 'footnote', 'data-footnote-id': newFootnote, }, }, From 40dc6861e06c34cd6d85c368117cca0b7e293a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Sat, 5 May 2018 19:56:27 +0200 Subject: [PATCH 06/36] Update footnotes block when inserting or removing blocks In the case a block is inserted or removed from the list of blocks, footnotes might change. Because of that, we must update the list of footnotes in those cases. --- editor/store/reducer.js | 19 +++++-- editor/store/test/reducer.js | 106 +++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 4 deletions(-) diff --git a/editor/store/reducer.js b/editor/store/reducer.js index a257412281b3f1..78ffcc83b6d56e 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -437,10 +437,21 @@ export const editor = flow( [ }; case 'INSERT_BLOCKS': - return { + const newBlocks = action.blocks.map( ( block ) => { + if ( ! block.attributes || ! block.attributes.content ) { + return block; + } + + return { + ...block, + footnotes: parseFootnotesFromContent( block.attributes.content ), + }; + } ); + + return updateFootnotes( { ...state, - ...getFlattenedBlocks( action.blocks ), - }; + ...getFlattenedBlocks( newBlocks ), + } ); case 'REPLACE_BLOCKS': if ( ! action.blocks ) { @@ -453,7 +464,7 @@ export const editor = flow( [ }; case 'REMOVE_BLOCKS': - return omit( state, action.uids ); + return updateFootnotes( omit( state, action.uids ) ); case 'SAVE_SHARED_BLOCK_SUCCESS': { const { id, updatedId } = action; diff --git a/editor/store/test/reducer.js b/editor/store/test/reducer.js index e5f11ac43b741c..bffc27cb2c8314 100644 --- a/editor/store/test/reducer.js +++ b/editor/store/test/reducer.js @@ -1231,6 +1231,112 @@ describe( 'state', () => { footnotes: expectedFootnotesBlock, } ); } ); + + it( 'should update footnotes when a block is inserted', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + uid: 'kumquat1', + attributes: { + content: [ + 'Lorem ipsum', + { + type: 'sup', + props: { + 'data-footnote-id': 'footnote-1', + }, + }, + ], + }, + innerBlocks: [], + footnotes: [ 'footnote-1' ], + }, { + uid: 'footnotes', + name: 'core/footnotes', + attributes: { + footnotes: [ 'footnote-1' ], + }, + innerBlocks: [], + } ], + } ) ); + + const state = editor( original, { + type: 'INSERT_BLOCKS', + uid: 'kumquat2', + blocks: [ { + uid: 'kumquat2', + attributes: { + content: [ + 'is a text', + { + type: 'sup', + props: { + 'data-footnote-id': 'footnote-2', + }, + }, + ], + }, + innerBlocks: [], + } ], + } ); + + expect( state.present.blocksByUid.footnotes.attributes ).toEqual( { + footnotes: [ 'footnote-1', 'footnote-2' ], + } ); + } ); + + it( 'should update footnotes when a block is removed', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + uid: 'kumquat1', + attributes: { + content: [ + 'Lorem ipsum', + { + type: 'sup', + props: { + 'data-footnote-id': 'footnote-1', + }, + }, + ], + }, + innerBlocks: [], + footnotes: [ 'footnote-1' ], + }, { + uid: 'kumquat2', + attributes: { + content: [ + { + type: 'sup', + props: { + 'data-footnote-id': 'footnote-2', + }, + }, + 'is a text', + ], + }, + innerBlocks: [], + footnotes: [ 'footnote-2' ], + }, { + uid: 'footnotes', + name: 'core/footnotes', + attributes: { + footnotes: [ 'footnote-1', 'footnote-2' ], + }, + innerBlocks: [], + } ], + } ) ); + + const state = editor( original, { + type: 'REMOVE_BLOCKS', + uids: [ 'kumquat2' ], + } ); + + expect( state.present.blocksByUid.footnotes.attributes ).toEqual( { + footnotes: [ 'footnote-1' ], + } ); + } ); } ); describe( 'withHistory', () => { From b5cee89f24c7d7868a19c13b12f26f22826495d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Sat, 5 May 2018 20:49:17 +0200 Subject: [PATCH 07/36] Make footnotes block to render nothing if there are no footnotes Until now, the footnotes block was always rendering OL tags even if there were no footnotes. --- core-blocks/footnotes/index.js | 10 ++++++++++ core-blocks/footnotes/test/__snapshots__/index.js.snap | 6 +----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core-blocks/footnotes/index.js b/core-blocks/footnotes/index.js index a8aa8d3fabce83..1098d239afdeea 100644 --- a/core-blocks/footnotes/index.js +++ b/core-blocks/footnotes/index.js @@ -44,6 +44,11 @@ export const settings = { editable: null, } )( ( { attributes, editable, isSelected, setAttributes, setState } ) => { const { footnotes, texts } = attributes; + + if ( ! footnotes.length ) { + return null; + } + const onSetActiveEditable = ( index ) => () => { setState( { editable: index } ); }; @@ -78,6 +83,11 @@ export const settings = { save( { attributes } ) { const { footnotes, texts } = attributes; + + if ( ! footnotes.length ) { + return null; + } + const footnotesBlock = footnotes.map( ( footnote ) => (
    1. { getFootnotesText( texts[ footnote ] ) } diff --git a/core-blocks/footnotes/test/__snapshots__/index.js.snap b/core-blocks/footnotes/test/__snapshots__/index.js.snap index 819a2eeb733fe0..be0a83f3830658 100644 --- a/core-blocks/footnotes/test/__snapshots__/index.js.snap +++ b/core-blocks/footnotes/test/__snapshots__/index.js.snap @@ -1,7 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`core/footnotes block edit matches snapshot 1`] = ` -
        -`; +exports[`core/footnotes block edit matches snapshot 1`] = `null`; From 3941d183ff23867f1cd899d9c12355b205d88885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Sun, 6 May 2018 16:35:01 +0200 Subject: [PATCH 08/36] Rename variables like 'newState' to 'nextState' to keep consistency with the other functions --- editor/store/reducer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/editor/store/reducer.js b/editor/store/reducer.js index 78ffcc83b6d56e..7d6147293c07d8 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -394,7 +394,7 @@ export const editor = flow( [ } // Otherwise merge attributes into state - const newState = { + const nextState = { ...state, [ action.uid ]: { ...state[ action.uid ], @@ -403,7 +403,7 @@ export const editor = flow( [ }, }; - return updateFootnotes( newState ); + return updateFootnotes( nextState ); case 'MOVE_BLOCK_TO_POSITION': // Avoid creating a new instance if the layout didn't change. @@ -437,7 +437,7 @@ export const editor = flow( [ }; case 'INSERT_BLOCKS': - const newBlocks = action.blocks.map( ( block ) => { + const nextBlocks = action.blocks.map( ( block ) => { if ( ! block.attributes || ! block.attributes.content ) { return block; } @@ -450,7 +450,7 @@ export const editor = flow( [ return updateFootnotes( { ...state, - ...getFlattenedBlocks( newBlocks ), + ...getFlattenedBlocks( nextBlocks ), } ); case 'REPLACE_BLOCKS': From 55889c2b00a70eff7b0d5230fad9d1a7d129935b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Sun, 6 May 2018 16:36:34 +0200 Subject: [PATCH 09/36] Use a template literal for the footnotes markup --- editor/components/rich-text/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/components/rich-text/index.js b/editor/components/rich-text/index.js index d0941900764490..815dd418ecdd25 100644 --- a/editor/components/rich-text/index.js +++ b/editor/components/rich-text/index.js @@ -838,7 +838,7 @@ export class RichText extends Component { return; } const uid = uuid(); - this.editor.insertContent( '' + __( 'See footnote' ) + ' ' ); + this.editor.insertContent( `${ __( 'See footnote' ) } ` ); } /** From 202dfd9d8e7f28d6c3538d79059cd5f14b41ef1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Sun, 6 May 2018 19:27:13 +0200 Subject: [PATCH 10/36] Save block footnotes in the block attributes instead of a block property object That allows us to parse the block texts when updating its attributes intead of doing it when the new attributes are handled by the reducer. This will make it easier to expand the footnotes feature to blocks other than the paragraph block. --- core-blocks/paragraph/index.js | 15 ++++++- editor/store/reducer.js | 25 ++++------- editor/store/test/reducer.js | 79 +++------------------------------- 3 files changed, 29 insertions(+), 90 deletions(-) diff --git a/core-blocks/paragraph/index.js b/core-blocks/paragraph/index.js index 8cc56818e9abf5..d8251fb8cc5b5c 100644 --- a/core-blocks/paragraph/index.js +++ b/core-blocks/paragraph/index.js @@ -24,6 +24,7 @@ import { import { getColorClass, withColors, + parseFootnotesFromContent, AlignmentToolbar, BlockControls, ContrastChecker, @@ -231,18 +232,25 @@ class ParagraphBlock extends Component { onChange={ ( nextContent ) => { setAttributes( { content: nextContent, + blockFootnotes: parseFootnotesFromContent( nextContent ), } ); } } onSplit={ insertBlocksAfter ? ( before, after, ...blocks ) => { if ( after ) { - blocks.push( createBlock( name, { content: after } ) ); + blocks.push( createBlock( name, { + content: after, + blockFootnotes: parseFootnotesFromContent( after ), + } ) ); } insertBlocksAfter( blocks ); if ( before ) { - setAttributes( { content: before } ); + setAttributes( { + content: before, + blockFootnotes: parseFootnotesFromContent( before ), + } ); } else { onReplace( [] ); } @@ -300,6 +308,9 @@ const schema = { customFontSize: { type: 'number', }, + blockFootnotes: { + type: 'array', + }, }; export const name = 'core/paragraph'; diff --git a/editor/store/reducer.js b/editor/store/reducer.js index 7d6147293c07d8..345fdbce009031 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -23,7 +23,7 @@ import { /** * WordPress dependencies */ -import { isSharedBlock, parseFootnotesFromContent } from '@wordpress/blocks'; +import { isSharedBlock } from '@wordpress/blocks'; import { combineReducers } from '@wordpress/data'; /** @@ -203,8 +203,13 @@ const withInnerBlocksRemoveCascade = ( reducer ) => ( state, action ) => { */ const getFootnotes = ( blocksByUid ) => { return Object.keys( blocksByUid ).reduce( - ( footnotes, blockUid ) => - footnotes.concat( blocksByUid[ blockUid ].footnotes || [] ), + ( footnotes, blockUid ) => { + if ( ! blocksByUid[ blockUid ].attributes || + ! blocksByUid[ blockUid ].attributes.blockFootnotes ) { + return footnotes; + } + return footnotes.concat( blocksByUid[ blockUid ].attributes.blockFootnotes ); + }, [] ); }; @@ -399,7 +404,6 @@ export const editor = flow( [ [ action.uid ]: { ...state[ action.uid ], attributes: nextAttributes, - footnotes: parseFootnotesFromContent( nextAttributes.content ), }, }; @@ -437,20 +441,9 @@ export const editor = flow( [ }; case 'INSERT_BLOCKS': - const nextBlocks = action.blocks.map( ( block ) => { - if ( ! block.attributes || ! block.attributes.content ) { - return block; - } - - return { - ...block, - footnotes: parseFootnotesFromContent( block.attributes.content ), - }; - } ); - return updateFootnotes( { ...state, - ...getFlattenedBlocks( nextBlocks ), + ...getFlattenedBlocks( action.blocks ), } ); case 'REPLACE_BLOCKS': diff --git a/editor/store/test/reducer.js b/editor/store/test/reducer.js index bffc27cb2c8314..875b086b87670f 100644 --- a/editor/store/test/reducer.js +++ b/editor/store/test/reducer.js @@ -1176,11 +1176,7 @@ describe( 'state', () => { type: 'RESET_BLOCKS', blocks: [ { uid: 'kumquat', - attributes: { - content: [ - 'Lorem ipsum', - ], - }, + attributes: {}, innerBlocks: [], }, { uid: 'footnotes', @@ -1192,43 +1188,17 @@ describe( 'state', () => { } ], } ) ); const newFootnote = 'fn-1234'; - const newContent = [ - 'Lorem ipsum', - { - type: 'sup', - props: { - 'data-footnote-id': newFootnote, - }, - }, - 'is a text', - ]; const state = editor( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', uid: 'kumquat', attributes: { - content: newContent, + blockFootnotes: [ newFootnote ], }, } ); - const expectedKumquatBlock = { - uid: 'kumquat', - attributes: { - content: newContent, - }, + expect( state.present.blocksByUid.footnotes.attributes ).toEqual( { footnotes: [ newFootnote ], - }; - const expectedFootnotesBlock = { - uid: 'footnotes', - name: 'core/footnotes', - attributes: { - footnotes: [ newFootnote ], - }, - }; - - expect( state.present.blocksByUid ).toEqual( { - kumquat: expectedKumquatBlock, - footnotes: expectedFootnotesBlock, } ); } ); @@ -1238,18 +1208,9 @@ describe( 'state', () => { blocks: [ { uid: 'kumquat1', attributes: { - content: [ - 'Lorem ipsum', - { - type: 'sup', - props: { - 'data-footnote-id': 'footnote-1', - }, - }, - ], + blockFootnotes: [ 'footnote-1' ], }, innerBlocks: [], - footnotes: [ 'footnote-1' ], }, { uid: 'footnotes', name: 'core/footnotes', @@ -1266,15 +1227,7 @@ describe( 'state', () => { blocks: [ { uid: 'kumquat2', attributes: { - content: [ - 'is a text', - { - type: 'sup', - props: { - 'data-footnote-id': 'footnote-2', - }, - }, - ], + blockFootnotes: [ 'footnote-2' ], }, innerBlocks: [], } ], @@ -1291,33 +1244,15 @@ describe( 'state', () => { blocks: [ { uid: 'kumquat1', attributes: { - content: [ - 'Lorem ipsum', - { - type: 'sup', - props: { - 'data-footnote-id': 'footnote-1', - }, - }, - ], + blockFootnotes: [ 'footnote-1' ], }, innerBlocks: [], - footnotes: [ 'footnote-1' ], }, { uid: 'kumquat2', attributes: { - content: [ - { - type: 'sup', - props: { - 'data-footnote-id': 'footnote-2', - }, - }, - 'is a text', - ], + blockFootnotes: [ 'footnote-2' ], }, innerBlocks: [], - footnotes: [ 'footnote-2' ], }, { uid: 'footnotes', name: 'core/footnotes', From e8597a0259425a9e3b157138a897eb2ec70dceca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Mon, 7 May 2018 01:32:54 +0200 Subject: [PATCH 11/36] Use withSelect HOC in Footnotes block so it's automatically updated when footnotes change This allows us to clean a lot of code from the reducer. --- core-blocks/footnotes/index.js | 64 ++++++++++++++++++--- editor/store/reducer.js | 95 ++---------------------------- editor/store/test/reducer.js | 102 --------------------------------- 3 files changed, 60 insertions(+), 201 deletions(-) diff --git a/core-blocks/footnotes/index.js b/core-blocks/footnotes/index.js index 1098d239afdeea..1df65b07b83b2e 100644 --- a/core-blocks/footnotes/index.js +++ b/core-blocks/footnotes/index.js @@ -3,6 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { withState } from '@wordpress/components'; +import { withSelect } from '@wordpress/data'; import { RichText } from '@wordpress/blocks'; /** @@ -13,16 +14,50 @@ import './editor.scss'; export const name = 'core/footnotes'; +/** + * Returns the text to be used for the footnotes. + * + * @param {?Array|string} text Text which might be a string or an array of + * strings and nodes. + * + * @return {Array} Same text formatted as an array. + */ const getFootnotesText = ( text ) => { if ( ! text ) { return []; } + if ( ! Array.isArray( text ) ) { return [ text ]; } + return text; }; +/** + * Returns the footnotes contained in the blocks. + * + * @param {Object} blocksByUid Object containing the blocks where to extract + * the footnotes from. + * + * @return {Array} Footnote ids contained in the blocks. + */ +const getFootnotesFromBlocks = ( blocksByUid ) => { + return Object.keys( blocksByUid ).reduce( + ( footnotes, blockUid ) => { + const block = blocksByUid[ blockUid ]; + + if ( ! block.attributes || + ! block.attributes.blockFootnotes ) { + return footnotes; + } + + return footnotes.concat( block.attributes.blockFootnotes ); + }, + [] + ); +}; + export const settings = { title: __( 'Footnotes' ), description: __( 'List of footnotes from the article' ), @@ -40,15 +75,18 @@ export const settings = { }, }, - edit: withState( { + edit: withSelect( ( select ) => ( { + blocks: select( 'core/editor' ) ? select( 'core/editor' ).getBlocks() : [], + } ) )( withState( { editable: null, - } )( ( { attributes, editable, isSelected, setAttributes, setState } ) => { - const { footnotes, texts } = attributes; + } )( ( { attributes, blocks, editable, isSelected, setAttributes, setState } ) => { + const footnotes = getFootnotesFromBlocks( blocks ); if ( ! footnotes.length ) { return null; } + const { texts } = attributes; const onSetActiveEditable = ( index ) => () => { setState( { editable: index } ); }; @@ -79,18 +117,26 @@ export const settings = { { footnotesBlock }
      ); - } ), + } ) ), save( { attributes } ) { - const { footnotes, texts } = attributes; + const { texts } = attributes; + const footnoteIds = Object.keys( texts ); - if ( ! footnotes.length ) { + if ( ! footnoteIds.length ) { return null; } - const footnotesBlock = footnotes.map( ( footnote ) => ( -
    2. - { getFootnotesText( texts[ footnote ] ) } + const footnotesToPrint = footnoteIds.map( ( footnoteId ) => { + return { + id: footnoteId, + text: texts[ footnoteId ], + }; + } ); + + const footnotesBlock = footnotesToPrint.map( ( footnote ) => ( +
    3. + { getFootnotesText( footnote.text ) }
    4. ) ); diff --git a/editor/store/reducer.js b/editor/store/reducer.js index 345fdbce009031..64d530c74116a8 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -193,89 +193,6 @@ const withInnerBlocksRemoveCascade = ( reducer ) => ( state, action ) => { return reducer( state, action ); }; -/** - * Returns the footnotes contained in the blocks. - * - * @param {Object} blocksByUid Object containing the blocks where to extract - * the footnotes from. - * - * @return {Array} Footnote ids contained in the blocks. - */ -const getFootnotes = ( blocksByUid ) => { - return Object.keys( blocksByUid ).reduce( - ( footnotes, blockUid ) => { - if ( ! blocksByUid[ blockUid ].attributes || - ! blocksByUid[ blockUid ].attributes.blockFootnotes ) { - return footnotes; - } - return footnotes.concat( blocksByUid[ blockUid ].attributes.blockFootnotes ); - }, - [] - ); -}; - -/** - * Returns the footnotes block. - * - * @param {Object} blocksByUid Object containing the blocks where to search - * for the footnotes block. - * - * @return {Object|null} Footnotes block or null if there isn't a footnotes - * block. - */ -const getFootnotesBlock = ( blocksByUid ) => { - for ( let i = 0; i < Object.keys( blocksByUid ).length; i++ ) { - const block = blocksByUid[ Object.keys( blocksByUid )[ i ] ]; - if ( block.name === 'core/footnotes' ) { - return block; - } - } - - return null; -}; - -/** - * Updates footnotes block with new footnotes. - * - * @param {Object} footnotesBlock Object of the footnotes block. - * @param {Array} footnotes Array of new footnotes. - * - * @return {Object} Footnotes block updated with the new footnotes. - */ -const updateFootnotesBlock = ( footnotesBlock, footnotes ) => { - return { - ...footnotesBlock, - attributes: { - ...footnotesBlock.attributes, - footnotes, - }, - }; -}; - -/** - * Updates footnotes block if it exists in the list of blocks. - * - * @param {Object} blocksByUid Object containing the blocks. - * - * @return {Object} New blocks with the footnotes block updated with the - * footnotes from the other blocks. - */ -const updateFootnotes = ( blocksByUid ) => { - const footnotesBlock = getFootnotesBlock( blocksByUid ); - - if ( ! footnotesBlock ) { - return blocksByUid; - } - - const footnotes = getFootnotes( blocksByUid ); - const updatedFootnotesBlock = updateFootnotesBlock( footnotesBlock, footnotes ); - - return { - ...blocksByUid, - [ footnotesBlock.uid ]: updatedFootnotesBlock, - }; -}; - /** * Undoable reducer returning the editor post state, including blocks parsed * from current HTML markup. @@ -364,7 +281,7 @@ export const editor = flow( [ switch ( action.type ) { case 'RESET_BLOCKS': case 'SETUP_EDITOR_STATE': - return updateFootnotes( getFlattenedBlocks( action.blocks ) ); + return getFlattenedBlocks( action.blocks ); case 'RECEIVE_BLOCKS': return { @@ -399,7 +316,7 @@ export const editor = flow( [ } // Otherwise merge attributes into state - const nextState = { + return { ...state, [ action.uid ]: { ...state[ action.uid ], @@ -407,8 +324,6 @@ export const editor = flow( [ }, }; - return updateFootnotes( nextState ); - case 'MOVE_BLOCK_TO_POSITION': // Avoid creating a new instance if the layout didn't change. if ( state[ action.uid ].attributes.layout === action.layout ) { @@ -441,10 +356,10 @@ export const editor = flow( [ }; case 'INSERT_BLOCKS': - return updateFootnotes( { + return { ...state, ...getFlattenedBlocks( action.blocks ), - } ); + }; case 'REPLACE_BLOCKS': if ( ! action.blocks ) { @@ -457,7 +372,7 @@ export const editor = flow( [ }; case 'REMOVE_BLOCKS': - return updateFootnotes( omit( state, action.uids ) ); + return omit( state, action.uids ); case 'SAVE_SHARED_BLOCK_SUCCESS': { const { id, updatedId } = action; diff --git a/editor/store/test/reducer.js b/editor/store/test/reducer.js index 875b086b87670f..542b904792d77b 100644 --- a/editor/store/test/reducer.js +++ b/editor/store/test/reducer.js @@ -1170,108 +1170,6 @@ describe( 'state', () => { expect( state.present.blocksByUID ).toBe( state.present.blocksByUID ); } ); - - it( 'should update footnotes when block attributes are updated', () => { - const original = deepFreeze( editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - uid: 'kumquat', - attributes: {}, - innerBlocks: [], - }, { - uid: 'footnotes', - name: 'core/footnotes', - attributes: { - footnotes: [], - }, - innerBlocks: [], - } ], - } ) ); - const newFootnote = 'fn-1234'; - - const state = editor( original, { - type: 'UPDATE_BLOCK_ATTRIBUTES', - uid: 'kumquat', - attributes: { - blockFootnotes: [ newFootnote ], - }, - } ); - - expect( state.present.blocksByUid.footnotes.attributes ).toEqual( { - footnotes: [ newFootnote ], - } ); - } ); - - it( 'should update footnotes when a block is inserted', () => { - const original = deepFreeze( editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - uid: 'kumquat1', - attributes: { - blockFootnotes: [ 'footnote-1' ], - }, - innerBlocks: [], - }, { - uid: 'footnotes', - name: 'core/footnotes', - attributes: { - footnotes: [ 'footnote-1' ], - }, - innerBlocks: [], - } ], - } ) ); - - const state = editor( original, { - type: 'INSERT_BLOCKS', - uid: 'kumquat2', - blocks: [ { - uid: 'kumquat2', - attributes: { - blockFootnotes: [ 'footnote-2' ], - }, - innerBlocks: [], - } ], - } ); - - expect( state.present.blocksByUid.footnotes.attributes ).toEqual( { - footnotes: [ 'footnote-1', 'footnote-2' ], - } ); - } ); - - it( 'should update footnotes when a block is removed', () => { - const original = deepFreeze( editor( undefined, { - type: 'RESET_BLOCKS', - blocks: [ { - uid: 'kumquat1', - attributes: { - blockFootnotes: [ 'footnote-1' ], - }, - innerBlocks: [], - }, { - uid: 'kumquat2', - attributes: { - blockFootnotes: [ 'footnote-2' ], - }, - innerBlocks: [], - }, { - uid: 'footnotes', - name: 'core/footnotes', - attributes: { - footnotes: [ 'footnote-1', 'footnote-2' ], - }, - innerBlocks: [], - } ], - } ) ); - - const state = editor( original, { - type: 'REMOVE_BLOCKS', - uids: [ 'kumquat2' ], - } ); - - expect( state.present.blocksByUid.footnotes.attributes ).toEqual( { - footnotes: [ 'footnote-1' ], - } ); - } ); } ); describe( 'withHistory', () => { From ec802ab50635dad5ac96b9712706a112648e138a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Mon, 7 May 2018 01:54:10 +0200 Subject: [PATCH 12/36] Clean up parser code no longer being used --- blocks/api/parser.js | 5 ----- blocks/api/test/parser.js | 8 +------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/blocks/api/parser.js b/blocks/api/parser.js index bd78c6d41af4a5..35d3bf20bd120c 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -318,11 +318,6 @@ export function createBlockWithFallback( blockNode ) { } } - const footnotes = parseFootnotesFromContent( block.attributes.content ); - if ( footnotes.length ) { - block.footnotes = footnotes; - } - return block; } diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index a51e463e079fc2..6c03a166d75999 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -322,13 +322,7 @@ describe( 'block parser', () => { it( 'should parse content and return footnote ids', () => { const content = [ 'Lorem ipsum', - { - type: 'sup', - props: { - className: 'footnote', - 'data-footnote-id': '12345', - }, - }, + { type: 'sup', props: { 'data-footnote-id': '12345' } }, 'is a text', ]; From d2309af3ab09f7334ac8a0a974796cbe8fb04cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Mon, 7 May 2018 01:59:26 +0200 Subject: [PATCH 13/36] Simplify the code which rendersthe footnotes block --- core-blocks/footnotes/index.js | 57 ++++++++++++++++------------------ 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/core-blocks/footnotes/index.js b/core-blocks/footnotes/index.js index 1df65b07b83b2e..db628127bb9b56 100644 --- a/core-blocks/footnotes/index.js +++ b/core-blocks/footnotes/index.js @@ -90,31 +90,30 @@ export const settings = { const onSetActiveEditable = ( index ) => () => { setState( { editable: index } ); }; - const footnotesBlock = footnotes.map( ( footnote, i ) => ( -
    5. - { - setAttributes( { - texts: { - ...texts, - [ footnote ]: nextValue, - }, - } ); - } - } - isSelected={ isSelected && editable === i } - placeholder={ __( 'Write footnote…' ) } - onFocus={ onSetActiveEditable( i ) } - /> -
    6. - ) ); return (
        - { footnotesBlock } + { footnotes.map( ( footnote, i ) => ( +
      1. + { + setAttributes( { + texts: { + ...texts, + [ footnote ]: nextValue, + }, + } ); + } + } + isSelected={ isSelected && editable === i } + placeholder={ __( 'Write footnote…' ) } + onFocus={ onSetActiveEditable( i ) } + /> +
      2. + ) ) }
      ); } ) ), @@ -127,23 +126,21 @@ export const settings = { return null; } - const footnotesToPrint = footnoteIds.map( ( footnoteId ) => { + const footnotes = footnoteIds.map( ( footnoteId ) => { return { id: footnoteId, text: texts[ footnoteId ], }; } ); - const footnotesBlock = footnotesToPrint.map( ( footnote ) => ( -
    7. - { getFootnotesText( footnote.text ) } -
    8. - ) ); - return (
        - { footnotesBlock } + { footnotes.map( ( footnote ) => ( +
      1. + { getFootnotesText( footnote.text ) } +
      2. + ) ) }
      ); From e7dc2bc71a0b52944e6c15f00f4e9d7798b744d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Tue, 8 May 2018 20:09:39 +0200 Subject: [PATCH 14/36] Create insertFootnotesBlock action to create the footnotes block when required This allows us to create the block only if footnotes are present, before we were attaching the footnotes block on editor setup. --- editor/components/block-list/block.js | 14 +++++++-- editor/store/actions.js | 13 +++++++++ editor/store/effects.js | 13 --------- editor/store/selectors.js | 20 +++++++++++++ editor/store/test/actions.js | 42 +++++++++++++++++++++++++++ editor/store/test/effects.js | 32 ++------------------ editor/store/test/selectors.js | 40 +++++++++++++++++++++++++ 7 files changed, 130 insertions(+), 44 deletions(-) diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index 580b22c3d87657..25727bd6ce4ef1 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -26,7 +26,7 @@ import { } from '@wordpress/blocks'; import { withFilters } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; -import { withDispatch, withSelect } from '@wordpress/data'; +import { select as selectFunction, withDispatch, withSelect } from '@wordpress/data'; import { withViewportMatch } from '@wordpress/viewport'; /** @@ -190,8 +190,14 @@ export class BlockListBlock extends Component { } setAttributes( attributes ) { - const { block, onChange } = this.props; + const { block, onAddFirstFootnote, onChange } = this.props; const type = getBlockType( block.name ); + + if ( attributes.blockFootnotes && attributes.blockFootnotes.length > 0 && + ! selectFunction( 'core/editor' ).getFootnotesBlockUid() ) { + onAddFirstFootnote(); + } + onChange( block.uid, attributes ); const metaAttributes = reduce( attributes, ( result, value, key ) => { @@ -652,6 +658,7 @@ const applyWithSelect = withSelect( ( select, { uid, rootUID } ) => { const applyWithDispatch = withDispatch( ( dispatch, ownProps ) => { const { updateBlockAttributes, + insertFootnotesBlock, selectBlock, insertBlocks, removeBlock, @@ -665,6 +672,9 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps ) => { onChange( uid, attributes ) { updateBlockAttributes( uid, attributes ); }, + onAddFirstFootnote() { + insertFootnotesBlock(); + }, onSelect( uid = ownProps.uid, initialPosition ) { selectBlock( uid, initialPosition ); }, diff --git a/editor/store/actions.js b/editor/store/actions.js index 9028f989452dda..e499f2a685d5e9 100644 --- a/editor/store/actions.js +++ b/editor/store/actions.js @@ -285,6 +285,18 @@ export function insertBlocks( blocks, index, rootUID ) { }; } +/** + * Returns an action object used in signalling that a footnotes block should be + * added to the block list. + * + * @return {Object} Action object + */ +export function insertFootnotesBlock() { + return { + ...insertBlock( createBlock( 'core/footnotes', {} ) ), + }; +} + /** * Returns an action object used in signalling that the insertion point should * be shown. @@ -646,6 +658,7 @@ export function convertBlockToShared( uid ) { uid, }; } + /** * Returns an action object used in signalling that a new block of the default * type should be added to the block list. diff --git a/editor/store/effects.js b/editor/store/effects.js index be804478f6a583..3570b8fedf64d4 100644 --- a/editor/store/effects.js +++ b/editor/store/effects.js @@ -350,19 +350,6 @@ export default { blocks = []; } - // Append footnotes block if not present - let hasFootnotesBlock = false; - for ( let i = 0; i < blocks.length; i++ ) { - if ( blocks[ i ].name === 'core/footnotes' ) { - hasFootnotesBlock = true; - break; - } - } - - if ( ! hasFootnotesBlock ) { - blocks.push( createBlock( 'core/footnotes' ) ); - } - // Include auto draft title in edits while not flagging post as dirty const edits = {}; if ( post.status === 'auto-draft' ) { diff --git a/editor/store/selectors.js b/editor/store/selectors.js index cc0707801aebd7..2063620f721b57 100644 --- a/editor/store/selectors.js +++ b/editor/store/selectors.js @@ -713,6 +713,26 @@ export function getNextBlockUid( state, startUID ) { return getAdjacentBlockUid( state, startUID, 1 ); } +/** + * Returns the UID from the footnotes block if it exists, or null if there isn't + * any footnotes block. + * + * @param {Object} state Global application state. + * + * @return {string|null} Footnotes block's UID, or null if none exists. + */ +export function getFootnotesBlockUid( state ) { + const blocks = getBlocks( state ); + + for ( let i = 0; i < blocks.length; i++ ) { + if ( blocks[ i ].name === 'core/footnotes' ) { + return blocks[ i ].uid; + } + } + + return null; +} + /** * Returns the initial caret position for the selected block. * This position is to used to position the caret properly when the selected block changes. diff --git a/editor/store/test/actions.js b/editor/store/test/actions.js index 050ff2473015b3..7eb93cafa1e9cc 100644 --- a/editor/store/test/actions.js +++ b/editor/store/test/actions.js @@ -1,3 +1,11 @@ +/** + * WordPress dependencies + */ +import { + unregisterBlockType, + registerBlockType, +} from '@wordpress/blocks'; + /** * Internal dependencies */ @@ -24,6 +32,7 @@ import { replaceBlock, insertBlock, insertBlocks, + insertFootnotesBlock, showInsertionPoint, hideInsertionPoint, editPost, @@ -213,6 +222,39 @@ describe( 'actions', () => { } ); } ); + describe( 'insertFootnotesBlock', () => { + beforeEach( () => { + registerBlockType( 'core/footnotes', { + title: 'Footnotes', + category: 'common', + save: () => null, + attributes: { + footnotes: [], + names: {}, + }, + } ); + } ); + + afterEach( () => { + unregisterBlockType( 'core/footnotes' ); + } ); + + it( 'should return the INSERT_BLOCKS action', () => { + const block = { + attributes: {}, + innerBlocks: [], + isValid: true, + name: 'core/footnotes', + uid: expect.any( String ), + }; + expect( insertFootnotesBlock() ).toEqual( { + type: 'INSERT_BLOCKS', + blocks: [ block ], + time: expect.any( Number ), + } ); + } ); + } ); + describe( 'showInsertionPoint', () => { it( 'should return the SHOW_INSERTION_POINT action', () => { expect( showInsertionPoint() ).toEqual( { diff --git a/editor/store/test/effects.js b/editor/store/test/effects.js index 8f15fb77741bd8..b80b3893bc60c1 100644 --- a/editor/store/test/effects.js +++ b/editor/store/test/effects.js @@ -501,18 +501,6 @@ describe( 'effects', () => { describe( '.SETUP_EDITOR', () => { const handler = effects.SETUP_EDITOR; - beforeEach( () => { - registerBlockType( 'core/footnotes', { - title: 'Footnotes', - category: 'common', - save: () => null, - attributes: { - footnotes: [], - names: {}, - }, - } ); - } ); - afterEach( () => { getBlockTypes().forEach( ( block ) => { unregisterBlockType( block.name ); @@ -538,17 +526,10 @@ describe( 'effects', () => { } ); const result = handler( { post, settings: {} }, { getState } ); - const footnotesBlock = { - attributes: {}, - innerBlocks: [], - isValid: true, - name: 'core/footnotes', - uid: result[ 1 ].blocks[ 0 ].uid, - }; expect( result ).toEqual( [ setTemplateValidity( true ), - setupEditorState( post, [ footnotesBlock ], {} ), + setupEditorState( post, [], {} ), ] ); } ); @@ -573,7 +554,7 @@ describe( 'effects', () => { const result = handler( { post }, { getState } ); - expect( result[ 1 ].blocks ).toHaveLength( 2 ); + expect( result[ 1 ].blocks ).toHaveLength( 1 ); expect( result ).toEqual( [ setTemplateValidity( true ), setupEditorState( post, result[ 1 ].blocks, {} ), @@ -599,17 +580,10 @@ describe( 'effects', () => { } ); const result = handler( { post }, { getState } ); - const footnotesBlock = { - attributes: {}, - innerBlocks: [], - isValid: true, - name: 'core/footnotes', - uid: result[ 1 ].blocks[ 0 ].uid, - }; expect( result ).toEqual( [ setTemplateValidity( true ), - setupEditorState( post, [ footnotesBlock ], { title: 'A History of Pork', status: 'draft' } ), + setupEditorState( post, [], { title: 'A History of Pork', status: 'draft' } ), ] ); } ); } ); diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js index 602672368f41d6..4fff3e012b4019 100644 --- a/editor/store/test/selectors.js +++ b/editor/store/test/selectors.js @@ -63,6 +63,7 @@ const { getBlockIndex, getPreviousBlockUid, getNextBlockUid, + getFootnotesBlockUid, isBlockSelected, isBlockWithinSelection, hasMultiSelection, @@ -2030,6 +2031,45 @@ describe( 'selectors', () => { } ); } ); + describe( 'getFootnotesBlockUid', () => { + it( 'should return the footnotes block\'s UID', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocksByUID: { + uid1: { + uid: 'uid1', + name: 'core/footnotes', + attributes: {}, + }, + }, + blockOrder: { + '': [ 'uid1' ], + uid1: [], + }, + edits: {}, + }, + }, + }; + + expect( getFootnotesBlockUid( state ) ).toEqual( 'uid1' ); + } ); + + it( 'should return null if there isn\'t any footnotes block', () => { + const state = { + editor: { + present: { + blocksByUID: [], + blockOrder: { '': [] }, + }, + }, + }; + + expect( getFootnotesBlockUid( state ) ).toBeNull(); + } ); + } ); + describe( 'isBlockSelected', () => { it( 'should return true if the block is selected', () => { const state = { From 23a303766ea19e3a1044e24a17744b1ac8022e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Tue, 8 May 2018 22:27:52 +0200 Subject: [PATCH 15/36] Remove footnotes block when the last footnote is deleted --- core-blocks/footnotes/index.js | 38 +------------- .../test/__snapshots__/index.js.snap | 6 ++- editor/components/block-list/block.js | 20 ++++++-- editor/store/selectors.js | 25 ++++++++++ editor/store/test/selectors.js | 50 +++++++++++++++++++ 5 files changed, 99 insertions(+), 40 deletions(-) diff --git a/core-blocks/footnotes/index.js b/core-blocks/footnotes/index.js index db628127bb9b56..3e8fea9995b39b 100644 --- a/core-blocks/footnotes/index.js +++ b/core-blocks/footnotes/index.js @@ -34,30 +34,6 @@ const getFootnotesText = ( text ) => { return text; }; -/** - * Returns the footnotes contained in the blocks. - * - * @param {Object} blocksByUid Object containing the blocks where to extract - * the footnotes from. - * - * @return {Array} Footnote ids contained in the blocks. - */ -const getFootnotesFromBlocks = ( blocksByUid ) => { - return Object.keys( blocksByUid ).reduce( - ( footnotes, blockUid ) => { - const block = blocksByUid[ blockUid ]; - - if ( ! block.attributes || - ! block.attributes.blockFootnotes ) { - return footnotes; - } - - return footnotes.concat( block.attributes.blockFootnotes ); - }, - [] - ); -}; - export const settings = { title: __( 'Footnotes' ), description: __( 'List of footnotes from the article' ), @@ -76,16 +52,10 @@ export const settings = { }, edit: withSelect( ( select ) => ( { - blocks: select( 'core/editor' ) ? select( 'core/editor' ).getBlocks() : [], + footnotes: select( 'core/editor' ) ? select( 'core/editor' ).getFootnotes() : [], } ) )( withState( { editable: null, - } )( ( { attributes, blocks, editable, isSelected, setAttributes, setState } ) => { - const footnotes = getFootnotesFromBlocks( blocks ); - - if ( ! footnotes.length ) { - return null; - } - + } )( ( { attributes, editable, footnotes, isSelected, setAttributes, setState } ) => { const { texts } = attributes; const onSetActiveEditable = ( index ) => () => { setState( { editable: index } ); @@ -122,10 +92,6 @@ export const settings = { const { texts } = attributes; const footnoteIds = Object.keys( texts ); - if ( ! footnoteIds.length ) { - return null; - } - const footnotes = footnoteIds.map( ( footnoteId ) => { return { id: footnoteId, diff --git a/core-blocks/footnotes/test/__snapshots__/index.js.snap b/core-blocks/footnotes/test/__snapshots__/index.js.snap index be0a83f3830658..819a2eeb733fe0 100644 --- a/core-blocks/footnotes/test/__snapshots__/index.js.snap +++ b/core-blocks/footnotes/test/__snapshots__/index.js.snap @@ -1,3 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`core/footnotes block edit matches snapshot 1`] = `null`; +exports[`core/footnotes block edit matches snapshot 1`] = ` +
        +`; diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index 25727bd6ce4ef1..f35492c2983007 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -190,12 +190,23 @@ export class BlockListBlock extends Component { } setAttributes( attributes ) { - const { block, onAddFirstFootnote, onChange } = this.props; + const { block, onAddFirstFootnote, onChange, onRemoveLastFootnote } = this.props; const type = getBlockType( block.name ); + const footnotesBlockUid = selectFunction( 'core/editor' ).getFootnotesBlockUid(); + const nextBlockFootnotes = attributes.blockFootnotes ? + attributes.blockFootnotes.length : 0; - if ( attributes.blockFootnotes && attributes.blockFootnotes.length > 0 && - ! selectFunction( 'core/editor' ).getFootnotesBlockUid() ) { + if ( ! footnotesBlockUid && nextBlockFootnotes > 0 ) { onAddFirstFootnote(); + } else if ( footnotesBlockUid && nextBlockFootnotes === 0 ) { + const postFootnotes = selectFunction( 'core/editor' ).getFootnotes().length; + const currentBlockFootnotes = block.attributes.blockFootnotes ? + block.attributes.blockFootnotes.length : 0; + const footnotesBlockChange = nextBlockFootnotes - currentBlockFootnotes; + + if ( postFootnotes + footnotesBlockChange === 0 ) { + onRemoveLastFootnote( footnotesBlockUid ); + } } onChange( block.uid, attributes ); @@ -675,6 +686,9 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps ) => { onAddFirstFootnote() { insertFootnotesBlock(); }, + onRemoveLastFootnote( footnotesBlockUid ) { + removeBlock( footnotesBlockUid ); + }, onSelect( uid = ownProps.uid, initialPosition ) { selectBlock( uid, initialPosition ); }, diff --git a/editor/store/selectors.js b/editor/store/selectors.js index 2063620f721b57..c77c4d008839e1 100644 --- a/editor/store/selectors.js +++ b/editor/store/selectors.js @@ -733,6 +733,31 @@ export function getFootnotesBlockUid( state ) { return null; } +/** + * Returns an array with all footnotes UIDs. + * + * @param {Object} state Global application state. + * + * @return {Array} Footnote ids. + */ +export function getFootnotes( state ) { + const blocks = getBlocks( state ); + + return Object.keys( blocks ).reduce( + ( footnotes, blockUid ) => { + const block = blocks[ blockUid ]; + + if ( ! block.attributes || + ! block.attributes.blockFootnotes ) { + return footnotes; + } + + return footnotes.concat( block.attributes.blockFootnotes ); + }, + [] + ); +} + /** * Returns the initial caret position for the selected block. * This position is to used to position the caret properly when the selected block changes. diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js index 4fff3e012b4019..40e6036f0fe74e 100644 --- a/editor/store/test/selectors.js +++ b/editor/store/test/selectors.js @@ -64,6 +64,7 @@ const { getPreviousBlockUid, getNextBlockUid, getFootnotesBlockUid, + getFootnotes, isBlockSelected, isBlockWithinSelection, hasMultiSelection, @@ -2070,6 +2071,55 @@ describe( 'selectors', () => { } ); } ); + describe( 'getFootnotes', () => { + it( 'should return the footnotes array ordered', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocksByUid: { + uid1: { + uid: 'uid1', + name: 'core/paragraph', + attributes: { + blockFootnotes: [ '123', '456' ], + }, + }, + uid2: { + uid: 'uid2', + name: 'core/paragraph', + attributes: { + blockFootnotes: [ '789' ], + }, + }, + }, + blockOrder: { + '': [ 'uid2', 'uid1' ], + uid1: [], + uid2: [], + }, + edits: {}, + }, + }, + }; + + expect( getFootnotes( state ) ).toEqual( [ '789', '123', '456' ] ); + } ); + + it( 'should return empty array if there isn\'t any footnote', () => { + const state = { + editor: { + present: { + blocksByUid: [], + blockOrder: { '': [] }, + }, + }, + }; + + expect( getFootnotes( state ) ).toEqual( [] ); + } ); + } ); + describe( 'isBlockSelected', () => { it( 'should return true if the block is selected', () => { const state = { From f519cacdbdfba7f3b0f6453438f735f799fcdf6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Tue, 8 May 2018 22:30:09 +0200 Subject: [PATCH 16/36] Import parseFootnotesFromContent from @wordpress/editor instead of @wordpress/blocks --- core-blocks/paragraph/index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core-blocks/paragraph/index.js b/core-blocks/paragraph/index.js index d8251fb8cc5b5c..2b50a07ccc428e 100644 --- a/core-blocks/paragraph/index.js +++ b/core-blocks/paragraph/index.js @@ -24,7 +24,6 @@ import { import { getColorClass, withColors, - parseFootnotesFromContent, AlignmentToolbar, BlockControls, ContrastChecker, @@ -32,7 +31,11 @@ import { PanelColor, RichText, } from '@wordpress/editor'; -import { createBlock, getPhrasingContentSchema } from '@wordpress/blocks'; +import { + createBlock, + getPhrasingContentSchema, + parseFootnotesFromContent, +} from '@wordpress/blocks'; /** * Internal dependencies From e7715cda7e26fceb333d9a7872db640de1190a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Tue, 8 May 2018 22:42:48 +0200 Subject: [PATCH 17/36] Update paragraph snapshots with blockFootnotes attribute --- test/e2e/specs/__snapshots__/adding-blocks.test.js.snap | 4 ++-- test/e2e/specs/__snapshots__/splitting-merging.test.js.snap | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/e2e/specs/__snapshots__/adding-blocks.test.js.snap b/test/e2e/specs/__snapshots__/adding-blocks.test.js.snap index adcd4803278805..df99b46ad510ca 100644 --- a/test/e2e/specs/__snapshots__/adding-blocks.test.js.snap +++ b/test/e2e/specs/__snapshots__/adding-blocks.test.js.snap @@ -1,11 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`adding blocks Should insert content using the placeholder and the regular inserter 1`] = ` -" +"

        Paragraph block

        - +

        Second paragraph

        diff --git a/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap b/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap index 98d163a480fbd4..996334dee27cd9 100644 --- a/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap +++ b/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap @@ -1,17 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`splitting and merging blocks Should split and merge paragraph blocks using Enter and Backspace 1`] = ` -" +"

        First

        - +

        Second

        " `; exports[`splitting and merging blocks Should split and merge paragraph blocks using Enter and Backspace 2`] = ` -" +"

        FirstSecond

        " `; From a9ddeee66bf5f1379c3027f4eb4b43880610505b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Wed, 9 May 2018 20:55:41 +0200 Subject: [PATCH 18/36] Flatten the blocks before searching for footnotes Otherwise, getFootnotes wasn't returning the footnotes of inner blocks. --- editor/store/reducer.js | 27 +-------------------------- editor/store/selectors.js | 7 ++++++- editor/store/test/selectors.js | 34 ++++++++++++++++++++++++++++++++-- editor/utils/block-list.js | 26 ++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 29 deletions(-) diff --git a/editor/store/reducer.js b/editor/store/reducer.js index 64d530c74116a8..b76907dcde1d35 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -31,6 +31,7 @@ import { combineReducers } from '@wordpress/data'; */ import withHistory from '../utils/with-history'; import withChangeDetection from '../utils/with-change-detection'; +import { getFlattenedBlocks } from '../utils/block-list'; import { PREFERENCES_DEFAULTS, EDITOR_SETTINGS_DEFAULTS } from './defaults'; import { insertAt, moveTo } from './array'; @@ -74,32 +75,6 @@ function mapBlockOrder( blocks, rootUID = '' ) { return result; } -/** - * Given an array of blocks, returns an object containing all blocks, recursing - * into inner blocks. Keys correspond to the block UID, the value of which is - * the block object. - * - * @param {Array} blocks Blocks to flatten. - * - * @return {Object} Flattened blocks object. - */ -function getFlattenedBlocks( blocks ) { - const flattenedBlocks = {}; - - const stack = [ ...blocks ]; - while ( stack.length ) { - // `innerBlocks` is redundant data which can fall out of sync, since - // this is reflected in `blockOrder`, so exclude from appended block. - const { innerBlocks, ...block } = stack.shift(); - - stack.push( ...innerBlocks ); - - flattenedBlocks[ block.uid ] = block; - } - - return flattenedBlocks; -} - /** * Returns true if the two object arguments have the same keys, or false * otherwise. diff --git a/editor/store/selectors.js b/editor/store/selectors.js index c77c4d008839e1..9789f0aa9a12c6 100644 --- a/editor/store/selectors.js +++ b/editor/store/selectors.js @@ -27,6 +27,11 @@ import { __ } from '@wordpress/i18n'; import { addQueryArgs } from '@wordpress/url'; import { moment } from '@wordpress/date'; +/** + * Internal dependencies + */ +import { getFlattenedBlocks } from '../utils/block-list'; + /*** * Module constants */ @@ -741,7 +746,7 @@ export function getFootnotesBlockUid( state ) { * @return {Array} Footnote ids. */ export function getFootnotes( state ) { - const blocks = getBlocks( state ); + const blocks = getFlattenedBlocks( getBlocks( state ) ); return Object.keys( blocks ).reduce( ( footnotes, blockUid ) => { diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js index 40e6036f0fe74e..5a9c8c6c293a6d 100644 --- a/editor/store/test/selectors.js +++ b/editor/store/test/selectors.js @@ -2077,7 +2077,7 @@ describe( 'selectors', () => { currentPost: {}, editor: { present: { - blocksByUid: { + blocksByUID: { uid1: { uid: 'uid1', name: 'core/paragraph', @@ -2106,11 +2106,41 @@ describe( 'selectors', () => { expect( getFootnotes( state ) ).toEqual( [ '789', '123', '456' ] ); } ); + it( 'should return the footnotes from inner blocks', () => { + const state = { + currentPost: {}, + editor: { + present: { + blocksByUID: { + uid1: { + uid: 'uid1', + name: 'core/columns', + }, + uid2: { + uid: 'uid2', + name: 'core/paragraph', + attributes: { + blockFootnotes: [ '123' ], + }, + }, + }, + blockOrder: { + '': [ 'uid1' ], + uid1: [ 'uid2' ], + }, + edits: {}, + }, + }, + }; + + expect( getFootnotes( state ) ).toEqual( [ '123' ] ); + } ); + it( 'should return empty array if there isn\'t any footnote', () => { const state = { editor: { present: { - blocksByUid: [], + blocksByUID: [], blockOrder: { '': [] }, }, }, diff --git a/editor/utils/block-list.js b/editor/utils/block-list.js index 2bbfe34cdc326e..0f906148dfb21b 100644 --- a/editor/utils/block-list.js +++ b/editor/utils/block-list.js @@ -125,3 +125,29 @@ export function createInnerBlockList( uid, renderBlockMenu = noop ) { return INNER_BLOCK_LIST_CACHE[ uid ][ 0 ]; } + +/** + * Given an array of blocks, returns an object containing all blocks, recursing + * into inner blocks. Keys correspond to the block UID, the value of which is + * the block object. + * + * @param {Array} blocks Blocks to flatten. + * + * @return {Object} Flattened blocks object. + */ +export function getFlattenedBlocks( blocks ) { + const flattenedBlocks = {}; + + const stack = [ ...blocks ]; + while ( stack.length ) { + // `innerBlocks` is redundant data which can fall out of sync, since + // this is reflected in `blockOrder`, so exclude from appended block. + const { innerBlocks, ...block } = stack.shift(); + + stack.push( ...innerBlocks ); + + flattenedBlocks[ block.uid ] = block; + } + + return flattenedBlocks; +} From db6e61d4e496ea58d75e001f34eab8f6c7ea7453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Wed, 9 May 2018 21:51:46 +0200 Subject: [PATCH 19/36] Clean up unnecessary properties from footnotes tests --- editor/store/test/selectors.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js index 5a9c8c6c293a6d..64772a014287a1 100644 --- a/editor/store/test/selectors.js +++ b/editor/store/test/selectors.js @@ -2080,14 +2080,12 @@ describe( 'selectors', () => { blocksByUID: { uid1: { uid: 'uid1', - name: 'core/paragraph', attributes: { blockFootnotes: [ '123', '456' ], }, }, uid2: { uid: 'uid2', - name: 'core/paragraph', attributes: { blockFootnotes: [ '789' ], }, @@ -2114,11 +2112,9 @@ describe( 'selectors', () => { blocksByUID: { uid1: { uid: 'uid1', - name: 'core/columns', }, uid2: { uid: 'uid2', - name: 'core/paragraph', attributes: { blockFootnotes: [ '123' ], }, From 991a67a7e0454d07929a379cca67d4b552cb558c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Thu, 10 May 2018 20:26:42 +0200 Subject: [PATCH 20/36] Allow only one instance of footnotes block --- core-blocks/footnotes/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core-blocks/footnotes/index.js b/core-blocks/footnotes/index.js index 3e8fea9995b39b..0741a2dc0fe276 100644 --- a/core-blocks/footnotes/index.js +++ b/core-blocks/footnotes/index.js @@ -38,6 +38,7 @@ export const settings = { title: __( 'Footnotes' ), description: __( 'List of footnotes from the article' ), category: 'common', + useOnce: true, keywords: [ __( 'footnotes' ), __( 'references' ) ], attributes: { From bed9b39e91a27628c535e2d3c15153769fa0502a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Tue, 15 May 2018 23:03:39 +0200 Subject: [PATCH 21/36] Remove check if core/editor selector is defined in footnotes block Until there was a check if select( 'core/editor' ) was undefined to avoid tests failing. This commit removes this check and instead imports the editor in the test. --- core-blocks/footnotes/index.js | 2 +- core-blocks/footnotes/test/index.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core-blocks/footnotes/index.js b/core-blocks/footnotes/index.js index 0741a2dc0fe276..72602e199b4d4c 100644 --- a/core-blocks/footnotes/index.js +++ b/core-blocks/footnotes/index.js @@ -53,7 +53,7 @@ export const settings = { }, edit: withSelect( ( select ) => ( { - footnotes: select( 'core/editor' ) ? select( 'core/editor' ).getFootnotes() : [], + footnotes: select( 'core/editor' ).getFootnotes(), } ) )( withState( { editable: null, } )( ( { attributes, editable, footnotes, isSelected, setAttributes, setState } ) => { diff --git a/core-blocks/footnotes/test/index.js b/core-blocks/footnotes/test/index.js index da58257ad72d8c..928f10c8713f3f 100644 --- a/core-blocks/footnotes/test/index.js +++ b/core-blocks/footnotes/test/index.js @@ -3,6 +3,7 @@ */ import { name, settings } from '../'; import { blockEditRender } from '../../test/helpers'; +import '@wordpress/editor'; describe( 'core/footnotes', () => { test( 'block edit matches snapshot', () => { From 33d004557b9493ca6ff97fe90be0ca7098af1690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Wed, 16 May 2018 23:34:17 +0200 Subject: [PATCH 22/36] Add/remove the footnotes blocks every time the first footnote is added/the last one is removed The previous implementation was quite simple and had bugs in some edge cases (removing the last block with footnotes, splitting a block in a post when the only footnotes were in the second half of the block, etc.). This new implementation should handle these cases much better. --- editor/components/block-list/block.js | 40 +++---- .../block-remove-button.js | 6 + editor/utils/footnotes.js | 109 ++++++++++++++++++ 3 files changed, 130 insertions(+), 25 deletions(-) create mode 100644 editor/utils/footnotes.js diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index f35492c2983007..ae0f8a852e1f04 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -26,7 +26,7 @@ import { } from '@wordpress/blocks'; import { withFilters } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; -import { select as selectFunction, withDispatch, withSelect } from '@wordpress/data'; +import { withDispatch, withSelect } from '@wordpress/data'; import { withViewportMatch } from '@wordpress/viewport'; /** @@ -51,6 +51,7 @@ import InserterWithShortcuts from '../inserter-with-shortcuts'; import Inserter from '../inserter'; import withHoverAreas from './with-hover-areas'; import { createInnerBlockList } from '../../utils/block-list'; +import { updateFootnotesBlockVisibility } from '../../utils/footnotes'; const { BACKSPACE, DELETE, ENTER } = keycodes; @@ -189,24 +190,14 @@ export class BlockListBlock extends Component { } } - setAttributes( attributes ) { - const { block, onAddFirstFootnote, onChange, onRemoveLastFootnote } = this.props; + setAttributes( attributes, shouldUpdateFootnotesBlockVisibilty = true ) { + const { block, onChange } = this.props; const type = getBlockType( block.name ); - const footnotesBlockUid = selectFunction( 'core/editor' ).getFootnotesBlockUid(); - const nextBlockFootnotes = attributes.blockFootnotes ? - attributes.blockFootnotes.length : 0; - - if ( ! footnotesBlockUid && nextBlockFootnotes > 0 ) { - onAddFirstFootnote(); - } else if ( footnotesBlockUid && nextBlockFootnotes === 0 ) { - const postFootnotes = selectFunction( 'core/editor' ).getFootnotes().length; - const currentBlockFootnotes = block.attributes.blockFootnotes ? - block.attributes.blockFootnotes.length : 0; - const footnotesBlockChange = nextBlockFootnotes - currentBlockFootnotes; - - if ( postFootnotes + footnotesBlockChange === 0 ) { - onRemoveLastFootnote( footnotesBlockUid ); - } + + if ( shouldUpdateFootnotesBlockVisibilty ) { + updateFootnotesBlockVisibility( { + [ block.uid ]: attributes.blockFootnotes, + } ); } onChange( block.uid, attributes ); @@ -290,6 +281,12 @@ export class BlockListBlock extends Component { } insertBlocksAfter( blocks ) { + const footnotes = {}; + blocks.forEach( ( block ) => { + footnotes[ block.uid ] = get( block, [ 'attributes', 'blockFootnotes' ] ); + } ); + + updateFootnotesBlockVisibility( footnotes ); this.props.onInsertBlocks( blocks, this.props.order + 1 ); } @@ -669,7 +666,6 @@ const applyWithSelect = withSelect( ( select, { uid, rootUID } ) => { const applyWithDispatch = withDispatch( ( dispatch, ownProps ) => { const { updateBlockAttributes, - insertFootnotesBlock, selectBlock, insertBlocks, removeBlock, @@ -683,12 +679,6 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps ) => { onChange( uid, attributes ) { updateBlockAttributes( uid, attributes ); }, - onAddFirstFootnote() { - insertFootnotesBlock(); - }, - onRemoveLastFootnote( footnotesBlockUid ) { - removeBlock( footnotesBlockUid ); - }, onSelect( uid = ownProps.uid, initialPosition ) { selectBlock( uid, initialPosition ); }, diff --git a/editor/components/block-settings-menu/block-remove-button.js b/editor/components/block-settings-menu/block-remove-button.js index a9fe8f121eb373..e7d46afb27f60f 100644 --- a/editor/components/block-settings-menu/block-remove-button.js +++ b/editor/components/block-settings-menu/block-remove-button.js @@ -11,6 +11,11 @@ import { IconButton } from '@wordpress/components'; import { compose } from '@wordpress/element'; import { withDispatch, withSelect } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { updateFootnotesBlockVisibility } from '../../utils/footnotes'; + export function BlockRemoveButton( { onRemove, onClick = noop, isLocked, role, ...props } ) { if ( isLocked ) { return null; @@ -33,6 +38,7 @@ export function BlockRemoveButton( { onRemove, onClick = noop, isLocked, role, . export default compose( withDispatch( ( dispatch, { uids } ) => ( { onRemove() { + updateFootnotesBlockVisibility( {}, uids ); dispatch( 'core/editor' ).removeBlocks( uids ); }, } ) ), diff --git a/editor/utils/footnotes.js b/editor/utils/footnotes.js new file mode 100644 index 00000000000000..e34a285afa5c52 --- /dev/null +++ b/editor/utils/footnotes.js @@ -0,0 +1,109 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; + +/** + * WordPress dependencies + */ +import { dispatch, select } from '@wordpress/data'; + +/** + * Checks if the updated blocks contain footnotes. + * + * @param {Object} updatedBlocks Object containing the updated blocks in a hierarchically + * form with a blockFootnotes property which contains the new footnotes. + * + * @return {boolean} True if the updated blocks contain footnotes and false if they don't. + */ +const doUpdatedBlocksContainFootnotes = function( updatedBlocks ) { + for ( let i = 0; i < Object.keys( updatedBlocks ).length; i++ ) { + const uid = Object.keys( updatedBlocks )[ i ]; + + if ( updatedBlocks[ uid ] && updatedBlocks[ uid ].length ) { + return true; + } + } + + return false; +}; + +/** + * Checks if the provided list of blocks contain footnotes. If a block is in the list of + * updatedBlocks, the list of footnotes from updatedBlocks takes precedence. If a block + * uid matches the removedBlock parameter, the footnotes of that block and its children + * are ignored. + * + * @param {Array} blocks Array of blocks from the post + * @param {Object} updatedBlocks Object containing the updated blocks in a hierarchically + * form with a blockFootnotes property which contains the new footnotes. + * @param {?string} removedBlock Uid of the removed block. + * + * @return {boolean} True if the blocks contain footnotes and false if they don't. It + * also returns false if the array of blocks is empty. + */ +const doBlocksContainFootnotes = function( blocks, updatedBlocks, removedBlock ) { + if ( ! blocks ) { + return false; + } + + for ( let i = 0; i < blocks.length; i++ ) { + const block = blocks[ i ]; + + if ( block.uid === removedBlock ) { + continue; + } + + const blockFootnotes = updatedBlocks.hasOwnProperty( block.uid ) ? + updatedBlocks[ block.uid ] : get( block, [ 'attributes', 'blockFootnotes' ] ); + if ( blockFootnotes && blockFootnotes.length ) { + return true; + } + + if ( doBlocksContainFootnotes( block.innerBlocks, updatedBlocks, removedBlock ) ) { + return true; + } + } + + return false; +}; + +/** + * Checks if post being edited contains footnotes. + * + * @param {Object} updatedBlocks Object containing the updated blocks in a hierarchically + * form with a blockFootnotes property which contains the new footnotes. + * @param {?string} removedBlock Uid of the removed block. + * + * @return {boolean} True if the current edited post contains footnotes and + * false if it doesn't. + */ +const doesPostContainFootnotes = function( updatedBlocks, removedBlock ) { + if ( doUpdatedBlocksContainFootnotes( updatedBlocks ) ) { + return true; + } + + const blocks = select( 'core/editor' ).getBlocks(); + + return doBlocksContainFootnotes( blocks, updatedBlocks, removedBlock ); +}; + +/** + * Inserts the footnotes block or removes it depending on if the post blocks contain + * footnotes or not. + * + * @param {Object} updatedBlocks Object containing the updated blocks in a hierarchically + * form with a blockFootnotes property which contains the new footnotes. + * @param {?string} removedBlock Uid of the removed block. + */ +export function updateFootnotesBlockVisibility( updatedBlocks, removedBlock = null ) { + const { insertFootnotesBlock, removeBlock } = dispatch( 'core/editor' ); + const footnotesBlockUid = select( 'core/editor' ).getFootnotesBlockUid(); + const shouldFootnotesBlockBeVisible = doesPostContainFootnotes( updatedBlocks, removedBlock ); + + if ( ! footnotesBlockUid && shouldFootnotesBlockBeVisible ) { + insertFootnotesBlock(); + } else if ( footnotesBlockUid && ! shouldFootnotesBlockBeVisible ) { + removeBlock( footnotesBlockUid ); + } +} From d06aff60d83fc828243ea2de20edbc6bb5eba6a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Sun, 20 May 2018 12:53:44 +0200 Subject: [PATCH 23/36] Rename footnote SUP element attribute from data-footnote-id to data-wp-footnote-id This way we might prevent collisions if a similar data attribute is used. --- blocks/api/parser.js | 4 ++-- blocks/api/test/parser.js | 2 +- editor/components/rich-text/index.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 35d3bf20bd120c..c221abfc542a39 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -218,8 +218,8 @@ export function parseFootnotesFromContent( content ) { } return content.reduce( ( footnotes, element ) => { - if ( element.type === 'sup' && element.props[ 'data-footnote-id' ] ) { - return footnotes.concat( element.props[ 'data-footnote-id' ] ); + if ( element.type === 'sup' && element.props[ 'data-wp-footnote-id' ] ) { + return footnotes.concat( element.props[ 'data-wp-footnote-id' ] ); } return footnotes; diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index 6c03a166d75999..411f9058f81260 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -322,7 +322,7 @@ describe( 'block parser', () => { it( 'should parse content and return footnote ids', () => { const content = [ 'Lorem ipsum', - { type: 'sup', props: { 'data-footnote-id': '12345' } }, + { type: 'sup', props: { 'data-wp-footnote-id': '12345' } }, 'is a text', ]; diff --git a/editor/components/rich-text/index.js b/editor/components/rich-text/index.js index 815dd418ecdd25..a679c9b6b9ccae 100644 --- a/editor/components/rich-text/index.js +++ b/editor/components/rich-text/index.js @@ -838,7 +838,7 @@ export class RichText extends Component { return; } const uid = uuid(); - this.editor.insertContent( `${ __( 'See footnote' ) } ` ); + this.editor.insertContent( `${ __( 'See footnote' ) } ` ); } /** From 75fc957b8d09c36867f4278c4b3d0903f23df887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Sun, 20 May 2018 12:54:30 +0200 Subject: [PATCH 24/36] Clean up footnotes block attributes Now all attributes are extracted from the markup. --- core-blocks/footnotes/index.js | 109 +++++++++--------- .../test/fixtures/core__footnotes.json | 15 ++- .../fixtures/core__footnotes.serialized.html | 2 +- 3 files changed, 63 insertions(+), 63 deletions(-) diff --git a/core-blocks/footnotes/index.js b/core-blocks/footnotes/index.js index 72602e199b4d4c..138731823f3a92 100644 --- a/core-blocks/footnotes/index.js +++ b/core-blocks/footnotes/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; + /** * WordPress dependencies */ @@ -14,26 +19,6 @@ import './editor.scss'; export const name = 'core/footnotes'; -/** - * Returns the text to be used for the footnotes. - * - * @param {?Array|string} text Text which might be a string or an array of - * strings and nodes. - * - * @return {Array} Same text formatted as an array. - */ -const getFootnotesText = ( text ) => { - if ( ! text ) { - return []; - } - - if ( ! Array.isArray( text ) ) { - return [ text ]; - } - - return text; -}; - export const settings = { title: __( 'Footnotes' ), description: __( 'List of footnotes from the article' ), @@ -44,68 +29,78 @@ export const settings = { attributes: { footnotes: { type: 'array', - default: [], - }, - texts: { - type: 'object', + source: 'query', + selector: 'li', + query: { + id: { + source: 'attribute', + attribute: 'id', + }, + text: { + source: 'children', + }, + }, default: [], }, }, edit: withSelect( ( select ) => ( { - footnotes: select( 'core/editor' ).getFootnotes(), + footnotesOrder: select( 'core/editor' ).getFootnotes(), } ) )( withState( { editable: null, - } )( ( { attributes, editable, footnotes, isSelected, setAttributes, setState } ) => { - const { texts } = attributes; + } )( ( { attributes, editable, footnotesOrder, isSelected, setAttributes, setState } ) => { + const { footnotes } = attributes; const onSetActiveEditable = ( index ) => () => { setState( { editable: index } ); }; return (
          - { footnotes.map( ( footnote, i ) => ( -
        1. - { - setAttributes( { - texts: { - ...texts, - [ footnote ]: nextValue, - }, - } ); + { footnotesOrder.map( ( footnoteUid, i ) => { + const filteredFootnotes = footnotes.filter( + ( footnote ) => footnote.id === footnoteUid ); + const value = get( filteredFootnotes, [ 0, 'text' ] ); + + return ( +
        2. + { + const nextFootnotes = footnotes.filter( + ( footnote ) => footnote.id !== footnoteUid ); + + nextFootnotes.push( { + id: footnoteUid, + text: nextValue, + } ); + + setAttributes( { + footnotes: nextFootnotes, + } ); + } } - } - isSelected={ isSelected && editable === i } - placeholder={ __( 'Write footnote…' ) } - onFocus={ onSetActiveEditable( i ) } - /> -
        3. - ) ) } + isSelected={ isSelected && editable === i } + placeholder={ __( 'Write footnote…' ) } + onFocus={ onSetActiveEditable( i ) } + /> + + ); + } ) }
        ); } ) ), save( { attributes } ) { - const { texts } = attributes; - const footnoteIds = Object.keys( texts ); - - const footnotes = footnoteIds.map( ( footnoteId ) => { - return { - id: footnoteId, - text: texts[ footnoteId ], - }; - } ); + const { footnotes } = attributes; return (
          { footnotes.map( ( footnote ) => (
        1. - { getFootnotesText( footnote.text ) } + { footnote.text }
        2. ) ) }
        diff --git a/core-blocks/test/fixtures/core__footnotes.json b/core-blocks/test/fixtures/core__footnotes.json index b84363cc8c9dd8..0b6034c10216dc 100644 --- a/core-blocks/test/fixtures/core__footnotes.json +++ b/core-blocks/test/fixtures/core__footnotes.json @@ -4,11 +4,16 @@ "name": "core/footnotes", "isValid": true, "attributes": { - "footnotes": [ "7edc47cb-3fe1-4ce0-ae6f-5e23e1e67aa2", "2e5f8a19-d9cd-4898-8686-cb6518450dc5" ], - "texts": { - "7edc47cb-3fe1-4ce0-ae6f-5e23e1e67aa2": "Reference 1", - "2e5f8a19-d9cd-4898-8686-cb6518450dc5": "Reference 2" - } + "footnotes": [ + { + "id": "7edc47cb-3fe1-4ce0-ae6f-5e23e1e67aa2", + "text": [ "Reference 1" ] + }, + { + "id": "2e5f8a19-d9cd-4898-8686-cb6518450dc5", + "text": [ "Reference 2" ] + } + ] }, "innerBlocks": [], "originalContent": "
        1. Reference 1
        2. Reference 2
        " diff --git a/core-blocks/test/fixtures/core__footnotes.serialized.html b/core-blocks/test/fixtures/core__footnotes.serialized.html index c95c16df69e3ff..5f6c80ff1eb28e 100644 --- a/core-blocks/test/fixtures/core__footnotes.serialized.html +++ b/core-blocks/test/fixtures/core__footnotes.serialized.html @@ -1,4 +1,4 @@ - +
        1. Reference 1
        2. From 089dd9dc4e806aac976651f78283b8723a1e46c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Sun, 20 May 2018 21:11:35 +0200 Subject: [PATCH 25/36] Optimize checking if footnotes block must be added/removed Only check if footnotes block must be added or removed when the footnotes of a block change. Until now, the check was made everytime a block changed its attributes or was split. --- core-blocks/paragraph/index.js | 26 ++++++++++++++++++++------ editor/components/block-list/block.js | 25 ++++++++++++------------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/core-blocks/paragraph/index.js b/core-blocks/paragraph/index.js index 2b50a07ccc428e..a352549e253ffe 100644 --- a/core-blocks/paragraph/index.js +++ b/core-blocks/paragraph/index.js @@ -2,7 +2,7 @@ * External dependencies */ import classnames from 'classnames'; -import { isFinite, find, omit } from 'lodash'; +import { isEqual, isFinite, find, omit } from 'lodash'; /** * WordPress dependencies @@ -144,6 +144,7 @@ class ParagraphBlock extends Component { render() { const { attributes, + updateFootnotes, setAttributes, insertBlocksAfter, mergeBlocks, @@ -160,6 +161,7 @@ class ParagraphBlock extends Component { const { align, + blockFootnotes, content, dropCap, placeholder, @@ -233,18 +235,26 @@ class ParagraphBlock extends Component { } } value={ content } onChange={ ( nextContent ) => { + const footnotes = parseFootnotesFromContent( nextContent ); + setAttributes( { content: nextContent, - blockFootnotes: parseFootnotesFromContent( nextContent ), + blockFootnotes: footnotes, } ); + if ( ! isEqual( blockFootnotes, footnotes ) ) { + updateFootnotes( footnotes ); + } } } onSplit={ insertBlocksAfter ? ( before, after, ...blocks ) => { + const beforeFootnotes = parseFootnotesFromContent( before ); + const afterFootnotes = parseFootnotesFromContent( after ); + const afterBlock = createBlock( name, { + content: after, + blockFootnotes: parseFootnotesFromContent( after ), + } ); if ( after ) { - blocks.push( createBlock( name, { - content: after, - blockFootnotes: parseFootnotesFromContent( after ), - } ) ); + blocks.push( afterBlock ); } insertBlocksAfter( blocks ); @@ -257,6 +267,10 @@ class ParagraphBlock extends Component { } else { onReplace( [] ); } + + if ( ! isEqual( blockFootnotes, beforeFootnotes ) && afterFootnotes.length ) { + updateFootnotes( beforeFootnotes, { [ afterBlock.uid ]: afterFootnotes } ); + } } : undefined } diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index ae0f8a852e1f04..58bd064472a6be 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -61,6 +61,7 @@ export class BlockListBlock extends Component { this.setBlockListRef = this.setBlockListRef.bind( this ); this.bindBlockNode = this.bindBlockNode.bind( this ); + this.updateFootnotes = this.updateFootnotes.bind( this ); this.setAttributes = this.setAttributes.bind( this ); this.maybeHover = this.maybeHover.bind( this ); this.hideHoverEffects = this.hideHoverEffects.bind( this ); @@ -190,16 +191,19 @@ export class BlockListBlock extends Component { } } - setAttributes( attributes, shouldUpdateFootnotesBlockVisibilty = true ) { + updateFootnotes( currentBlockFootnotes, updatedBlocks ) { + const { block } = this.props; + + updateFootnotesBlockVisibility( { + ...updatedBlocks, + [ block.uid ]: currentBlockFootnotes, + } ); + } + + setAttributes( attributes ) { const { block, onChange } = this.props; const type = getBlockType( block.name ); - if ( shouldUpdateFootnotesBlockVisibilty ) { - updateFootnotesBlockVisibility( { - [ block.uid ]: attributes.blockFootnotes, - } ); - } - onChange( block.uid, attributes ); const metaAttributes = reduce( attributes, ( result, value, key ) => { @@ -281,12 +285,6 @@ export class BlockListBlock extends Component { } insertBlocksAfter( blocks ) { - const footnotes = {}; - blocks.forEach( ( block ) => { - footnotes[ block.uid ] = get( block, [ 'attributes', 'blockFootnotes' ] ); - } ); - - updateFootnotesBlockVisibility( footnotes ); this.props.onInsertBlocks( blocks, this.props.order + 1 ); } @@ -570,6 +568,7 @@ export class BlockListBlock extends Component { name={ blockName } isSelected={ isSelected } attributes={ block.attributes } + updateFootnotes={ this.updateFootnotes } setAttributes={ this.setAttributes } insertBlocksAfter={ isLocked ? undefined : this.insertBlocksAfter } onReplace={ isLocked ? undefined : onReplace } From c33beaca412b53af387b959f184a79a694b39ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Mon, 21 May 2018 03:12:29 +0200 Subject: [PATCH 26/36] Update footnotes block fixtures --- core-blocks/test/fixtures/core__footnotes.html | 2 +- core-blocks/test/fixtures/core__footnotes.parsed.json | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/core-blocks/test/fixtures/core__footnotes.html b/core-blocks/test/fixtures/core__footnotes.html index 813f3989d2d8d6..ed37491b35295a 100644 --- a/core-blocks/test/fixtures/core__footnotes.html +++ b/core-blocks/test/fixtures/core__footnotes.html @@ -1,3 +1,3 @@ - +
          1. Reference 1
          2. Reference 2
          diff --git a/core-blocks/test/fixtures/core__footnotes.parsed.json b/core-blocks/test/fixtures/core__footnotes.parsed.json index 51b59c02ea646b..e40bce6ab20697 100644 --- a/core-blocks/test/fixtures/core__footnotes.parsed.json +++ b/core-blocks/test/fixtures/core__footnotes.parsed.json @@ -1,13 +1,7 @@ [ { "blockName": "core/footnotes", - "attrs": { - "footnotes": [ "7edc47cb-3fe1-4ce0-ae6f-5e23e1e67aa2", "2e5f8a19-d9cd-4898-8686-cb6518450dc5" ], - "texts": { - "7edc47cb-3fe1-4ce0-ae6f-5e23e1e67aa2": "Reference 1", - "2e5f8a19-d9cd-4898-8686-cb6518450dc5": "Reference 2" - } - }, + "attrs": null, "innerBlocks": [], "innerHTML": "\n
          1. Reference 1
          2. Reference 2
          \n" }, From 8c2a1a2c2db2dabf108d83d3176c1e91a5025945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Mon, 21 May 2018 03:46:52 +0200 Subject: [PATCH 27/36] Raise paragraph key in pullquote test so it doesn't collide with recently added tests A better solution will be needed in order to ignore the key during tests so adding more tests doesn't break the previous ones. --- core-blocks/test/fixtures/core__pullquote__multi-paragraph.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-blocks/test/fixtures/core__pullquote__multi-paragraph.json b/core-blocks/test/fixtures/core__pullquote__multi-paragraph.json index c2a6c0d770de73..3ce42dbb1e3aee 100644 --- a/core-blocks/test/fixtures/core__pullquote__multi-paragraph.json +++ b/core-blocks/test/fixtures/core__pullquote__multi-paragraph.json @@ -15,7 +15,7 @@ "Paragraph ", { "type": "strong", - "key": "_domReact71", + "key": "_domReact73", "ref": null, "props": { "children": "one" From b3c303d3108da8d7f43ab1947087e1e2db7c07dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Mon, 21 May 2018 16:18:26 +0200 Subject: [PATCH 28/36] Extract blockFootnotes paragraph attribute from the markup --- blocks/api/parser.js | 5 ++++- blocks/api/test/parser.js | 2 +- core-blocks/footnotes/index.js | 10 +++++----- core-blocks/paragraph/index.js | 18 +++++++++++++++--- core-blocks/test/fixtures/core__columns.json | 4 ++++ .../fixtures/core__paragraph__align-right.json | 1 + .../fixtures/core__paragraph__deprecated.json | 1 + .../core__text__converts-to-paragraph.json | 1 + editor/store/test/selectors.js | 11 ++++++----- .../__snapshots__/adding-blocks.test.js.snap | 4 ++-- .../splitting-merging.test.js.snap | 6 +++--- 11 files changed, 43 insertions(+), 20 deletions(-) diff --git a/blocks/api/parser.js b/blocks/api/parser.js index c221abfc542a39..f64c0a87375ec3 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -219,7 +219,10 @@ export function parseFootnotesFromContent( content ) { return content.reduce( ( footnotes, element ) => { if ( element.type === 'sup' && element.props[ 'data-wp-footnote-id' ] ) { - return footnotes.concat( element.props[ 'data-wp-footnote-id' ] ); + return [ + ...footnotes, + { id: element.props[ 'data-wp-footnote-id' ] }, + ]; } return footnotes; diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index 411f9058f81260..b039a66bac0720 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -328,7 +328,7 @@ describe( 'block parser', () => { const footnotes = parseFootnotesFromContent( content ); - expect( footnotes ).toEqual( [ '12345' ] ); + expect( footnotes ).toEqual( [ { id: '12345' } ] ); } ); } ); diff --git a/core-blocks/footnotes/index.js b/core-blocks/footnotes/index.js index 138731823f3a92..c36f123304b78b 100644 --- a/core-blocks/footnotes/index.js +++ b/core-blocks/footnotes/index.js @@ -56,23 +56,23 @@ export const settings = { return (
            - { footnotesOrder.map( ( footnoteUid, i ) => { + { footnotesOrder.map( ( footnotesOrderItem, i ) => { const filteredFootnotes = footnotes.filter( - ( footnote ) => footnote.id === footnoteUid ); + ( footnote ) => footnote.id === footnotesOrderItem.id ); const value = get( filteredFootnotes, [ 0, 'text' ] ); return ( -
          1. +
          2. { const nextFootnotes = footnotes.filter( - ( footnote ) => footnote.id !== footnoteUid ); + ( footnote ) => footnote.id !== footnotesOrderItem.id ); nextFootnotes.push( { - id: footnoteUid, + id: footnotesOrderItem.id, text: nextValue, } ); diff --git a/core-blocks/paragraph/index.js b/core-blocks/paragraph/index.js index a352549e253ffe..a2f6f21a61f010 100644 --- a/core-blocks/paragraph/index.js +++ b/core-blocks/paragraph/index.js @@ -235,24 +235,28 @@ class ParagraphBlock extends Component { } } value={ content } onChange={ ( nextContent ) => { + const previousFootnotes = blockFootnotes || []; const footnotes = parseFootnotesFromContent( nextContent ); setAttributes( { content: nextContent, blockFootnotes: footnotes, } ); - if ( ! isEqual( blockFootnotes, footnotes ) ) { + + if ( ! isEqual( previousFootnotes, footnotes ) ) { updateFootnotes( footnotes ); } } } onSplit={ insertBlocksAfter ? ( before, after, ...blocks ) => { const beforeFootnotes = parseFootnotesFromContent( before ); + const afterFootnotes = parseFootnotesFromContent( after ); const afterBlock = createBlock( name, { content: after, - blockFootnotes: parseFootnotesFromContent( after ), + blockFootnotes: afterFootnotes, } ); + if ( after ) { blocks.push( afterBlock ); } @@ -262,7 +266,7 @@ class ParagraphBlock extends Component { if ( before ) { setAttributes( { content: before, - blockFootnotes: parseFootnotesFromContent( before ), + blockFootnotes: beforeFootnotes, } ); } else { onReplace( [] ); @@ -327,6 +331,14 @@ const schema = { }, blockFootnotes: { type: 'array', + source: 'query', + selector: 'sup', + query: { + id: { + source: 'attribute', + attribute: 'data-wp-footnote-id', + }, + }, }, }; diff --git a/core-blocks/test/fixtures/core__columns.json b/core-blocks/test/fixtures/core__columns.json index baf28cfd7f291b..c08d20bead8bdf 100644 --- a/core-blocks/test/fixtures/core__columns.json +++ b/core-blocks/test/fixtures/core__columns.json @@ -12,6 +12,7 @@ "name": "core/paragraph", "isValid": true, "attributes": { + "blockFootnotes": [], "content": [ "Column One, Paragraph One" ], @@ -26,6 +27,7 @@ "name": "core/paragraph", "isValid": true, "attributes": { + "blockFootnotes": [], "content": [ "Column One, Paragraph Two" ], @@ -40,6 +42,7 @@ "name": "core/paragraph", "isValid": true, "attributes": { + "blockFootnotes": [], "content": [ "Column Two, Paragraph One" ], @@ -54,6 +57,7 @@ "name": "core/paragraph", "isValid": true, "attributes": { + "blockFootnotes": [], "content": [ "Column Three, Paragraph One" ], diff --git a/core-blocks/test/fixtures/core__paragraph__align-right.json b/core-blocks/test/fixtures/core__paragraph__align-right.json index 731657d06705f8..1690e7a7927aa8 100644 --- a/core-blocks/test/fixtures/core__paragraph__align-right.json +++ b/core-blocks/test/fixtures/core__paragraph__align-right.json @@ -4,6 +4,7 @@ "name": "core/paragraph", "isValid": true, "attributes": { + "blockFootnotes": [], "content": [ "... like this one, which is separate from the above and right aligned." ], diff --git a/core-blocks/test/fixtures/core__paragraph__deprecated.json b/core-blocks/test/fixtures/core__paragraph__deprecated.json index 7fe227a9e14cb4..1685fb1a1dfef2 100644 --- a/core-blocks/test/fixtures/core__paragraph__deprecated.json +++ b/core-blocks/test/fixtures/core__paragraph__deprecated.json @@ -4,6 +4,7 @@ "name": "core/paragraph", "isValid": true, "attributes": { + "blockFootnotes": [], "content": [ { "key": "html", diff --git a/core-blocks/test/fixtures/core__text__converts-to-paragraph.json b/core-blocks/test/fixtures/core__text__converts-to-paragraph.json index f9ff727423d80a..62899a2cc2697f 100644 --- a/core-blocks/test/fixtures/core__text__converts-to-paragraph.json +++ b/core-blocks/test/fixtures/core__text__converts-to-paragraph.json @@ -4,6 +4,7 @@ "name": "core/paragraph", "isValid": true, "attributes": { + "blockFootnotes": [], "content": [ "This is an old-style text block. Changed to ", { diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js index 64772a014287a1..4b5fd5e3f48f19 100644 --- a/editor/store/test/selectors.js +++ b/editor/store/test/selectors.js @@ -2081,13 +2081,13 @@ describe( 'selectors', () => { uid1: { uid: 'uid1', attributes: { - blockFootnotes: [ '123', '456' ], + blockFootnotes: [ { id: '123' }, { id: '456' } ], }, }, uid2: { uid: 'uid2', attributes: { - blockFootnotes: [ '789' ], + blockFootnotes: [ { id: '789' } ], }, }, }, @@ -2101,7 +2101,8 @@ describe( 'selectors', () => { }, }; - expect( getFootnotes( state ) ).toEqual( [ '789', '123', '456' ] ); + expect( getFootnotes( state ) ).toEqual( + [ { id: '789' }, { id: '123' }, { id: '456' } ] ); } ); it( 'should return the footnotes from inner blocks', () => { @@ -2116,7 +2117,7 @@ describe( 'selectors', () => { uid2: { uid: 'uid2', attributes: { - blockFootnotes: [ '123' ], + blockFootnotes: [ { id: '123' } ], }, }, }, @@ -2129,7 +2130,7 @@ describe( 'selectors', () => { }, }; - expect( getFootnotes( state ) ).toEqual( [ '123' ] ); + expect( getFootnotes( state ) ).toEqual( [ { id: '123' } ] ); } ); it( 'should return empty array if there isn\'t any footnote', () => { diff --git a/test/e2e/specs/__snapshots__/adding-blocks.test.js.snap b/test/e2e/specs/__snapshots__/adding-blocks.test.js.snap index df99b46ad510ca..adcd4803278805 100644 --- a/test/e2e/specs/__snapshots__/adding-blocks.test.js.snap +++ b/test/e2e/specs/__snapshots__/adding-blocks.test.js.snap @@ -1,11 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`adding blocks Should insert content using the placeholder and the regular inserter 1`] = ` -" +"

            Paragraph block

            - +

            Second paragraph

            diff --git a/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap b/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap index 996334dee27cd9..98d163a480fbd4 100644 --- a/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap +++ b/test/e2e/specs/__snapshots__/splitting-merging.test.js.snap @@ -1,17 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`splitting and merging blocks Should split and merge paragraph blocks using Enter and Backspace 1`] = ` -" +"

            First

            - +

            Second

            " `; exports[`splitting and merging blocks Should split and merge paragraph blocks using Enter and Backspace 2`] = ` -" +"

            FirstSecond

            " `; From 14b9b2d979ed8fff1222bcf95ece8937b2847cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Mon, 21 May 2018 17:02:13 +0200 Subject: [PATCH 29/36] Refactor getFootnotes selector Code continues working as usual but it has been optimized, made simpler and the dependency to getFlattenedBlocks has been removed, so it no longer has to be stored in the utils. --- editor/store/reducer.js | 27 ++++++++++++++++++++++++++- editor/store/selectors.js | 38 ++++++++++++++++---------------------- editor/utils/block-list.js | 26 -------------------------- 3 files changed, 42 insertions(+), 49 deletions(-) diff --git a/editor/store/reducer.js b/editor/store/reducer.js index b76907dcde1d35..64d530c74116a8 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -31,7 +31,6 @@ import { combineReducers } from '@wordpress/data'; */ import withHistory from '../utils/with-history'; import withChangeDetection from '../utils/with-change-detection'; -import { getFlattenedBlocks } from '../utils/block-list'; import { PREFERENCES_DEFAULTS, EDITOR_SETTINGS_DEFAULTS } from './defaults'; import { insertAt, moveTo } from './array'; @@ -75,6 +74,32 @@ function mapBlockOrder( blocks, rootUID = '' ) { return result; } +/** + * Given an array of blocks, returns an object containing all blocks, recursing + * into inner blocks. Keys correspond to the block UID, the value of which is + * the block object. + * + * @param {Array} blocks Blocks to flatten. + * + * @return {Object} Flattened blocks object. + */ +function getFlattenedBlocks( blocks ) { + const flattenedBlocks = {}; + + const stack = [ ...blocks ]; + while ( stack.length ) { + // `innerBlocks` is redundant data which can fall out of sync, since + // this is reflected in `blockOrder`, so exclude from appended block. + const { innerBlocks, ...block } = stack.shift(); + + stack.push( ...innerBlocks ); + + flattenedBlocks[ block.uid ] = block; + } + + return flattenedBlocks; +} + /** * Returns true if the two object arguments have the same keys, or false * otherwise. diff --git a/editor/store/selectors.js b/editor/store/selectors.js index 9789f0aa9a12c6..9603243764c17f 100644 --- a/editor/store/selectors.js +++ b/editor/store/selectors.js @@ -27,11 +27,6 @@ import { __ } from '@wordpress/i18n'; import { addQueryArgs } from '@wordpress/url'; import { moment } from '@wordpress/date'; -/** - * Internal dependencies - */ -import { getFlattenedBlocks } from '../utils/block-list'; - /*** * Module constants */ @@ -739,27 +734,26 @@ export function getFootnotesBlockUid( state ) { } /** - * Returns an array with all footnotes UIDs. + * Returns an array with all footnote UIDs contained in the specified block + * including its children. If no block UID is specified, it returns the array + * of footnotes of the entire post. * - * @param {Object} state Global application state. + * @param {Object} state Global application state. + * @param {?string} rootUID Optional root UID of the block to search footnotes in. * * @return {Array} Footnote ids. */ -export function getFootnotes( state ) { - const blocks = getFlattenedBlocks( getBlocks( state ) ); - - return Object.keys( blocks ).reduce( - ( footnotes, blockUid ) => { - const block = blocks[ blockUid ]; - - if ( ! block.attributes || - ! block.attributes.blockFootnotes ) { - return footnotes; - } - - return footnotes.concat( block.attributes.blockFootnotes ); - }, - [] +export function getFootnotes( state, rootUID = '' ) { + const blockFootnotes = get( state.editor.present.blocksByUID[ rootUID ], + [ 'attributes', 'blockFootnotes' ], [] ); + const innerBlocksUids = getBlockOrder( state, rootUID ); + + return innerBlocksUids.reduce( + ( footnotes, blockUid ) => [ + ...footnotes, + ...getFootnotes( state, blockUid ), + ], + blockFootnotes ); } diff --git a/editor/utils/block-list.js b/editor/utils/block-list.js index 0f906148dfb21b..2bbfe34cdc326e 100644 --- a/editor/utils/block-list.js +++ b/editor/utils/block-list.js @@ -125,29 +125,3 @@ export function createInnerBlockList( uid, renderBlockMenu = noop ) { return INNER_BLOCK_LIST_CACHE[ uid ][ 0 ]; } - -/** - * Given an array of blocks, returns an object containing all blocks, recursing - * into inner blocks. Keys correspond to the block UID, the value of which is - * the block object. - * - * @param {Array} blocks Blocks to flatten. - * - * @return {Object} Flattened blocks object. - */ -export function getFlattenedBlocks( blocks ) { - const flattenedBlocks = {}; - - const stack = [ ...blocks ]; - while ( stack.length ) { - // `innerBlocks` is redundant data which can fall out of sync, since - // this is reflected in `blockOrder`, so exclude from appended block. - const { innerBlocks, ...block } = stack.shift(); - - stack.push( ...innerBlocks ); - - flattenedBlocks[ block.uid ] = block; - } - - return flattenedBlocks; -} From 0646693ffd22c07a7942dde4fa10ebf273489b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Mon, 21 May 2018 20:52:14 +0200 Subject: [PATCH 30/36] Several minor fixes and typos corrected --- editor/components/block-list/block.js | 1 - editor/store/actions.js | 2 +- editor/store/test/actions.js | 1 - editor/utils/footnotes.js | 10 +++++----- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index 58bd064472a6be..f32fd8dbb05077 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -203,7 +203,6 @@ export class BlockListBlock extends Component { setAttributes( attributes ) { const { block, onChange } = this.props; const type = getBlockType( block.name ); - onChange( block.uid, attributes ); const metaAttributes = reduce( attributes, ( result, value, key ) => { diff --git a/editor/store/actions.js b/editor/store/actions.js index e499f2a685d5e9..8c157e64bdec8e 100644 --- a/editor/store/actions.js +++ b/editor/store/actions.js @@ -293,7 +293,7 @@ export function insertBlocks( blocks, index, rootUID ) { */ export function insertFootnotesBlock() { return { - ...insertBlock( createBlock( 'core/footnotes', {} ) ), + ...insertBlock( createBlock( 'core/footnotes' ) ), }; } diff --git a/editor/store/test/actions.js b/editor/store/test/actions.js index 7eb93cafa1e9cc..af4c38e87cf96e 100644 --- a/editor/store/test/actions.js +++ b/editor/store/test/actions.js @@ -230,7 +230,6 @@ describe( 'actions', () => { save: () => null, attributes: { footnotes: [], - names: {}, }, } ); } ); diff --git a/editor/utils/footnotes.js b/editor/utils/footnotes.js index e34a285afa5c52..94934d4a406699 100644 --- a/editor/utils/footnotes.js +++ b/editor/utils/footnotes.js @@ -11,7 +11,7 @@ import { dispatch, select } from '@wordpress/data'; /** * Checks if the updated blocks contain footnotes. * - * @param {Object} updatedBlocks Object containing the updated blocks in a hierarchically + * @param {Object} updatedBlocks Object containing the updated blocks in a hierarchical * form with a blockFootnotes property which contains the new footnotes. * * @return {boolean} True if the updated blocks contain footnotes and false if they don't. @@ -35,7 +35,7 @@ const doUpdatedBlocksContainFootnotes = function( updatedBlocks ) { * are ignored. * * @param {Array} blocks Array of blocks from the post - * @param {Object} updatedBlocks Object containing the updated blocks in a hierarchically + * @param {Object} updatedBlocks Object containing the updated blocks in a hierarchical * form with a blockFootnotes property which contains the new footnotes. * @param {?string} removedBlock Uid of the removed block. * @@ -71,7 +71,7 @@ const doBlocksContainFootnotes = function( blocks, updatedBlocks, removedBlock ) /** * Checks if post being edited contains footnotes. * - * @param {Object} updatedBlocks Object containing the updated blocks in a hierarchically + * @param {Object} updatedBlocks Object containing the updated blocks in a hierarchical * form with a blockFootnotes property which contains the new footnotes. * @param {?string} removedBlock Uid of the removed block. * @@ -89,10 +89,10 @@ const doesPostContainFootnotes = function( updatedBlocks, removedBlock ) { }; /** - * Inserts the footnotes block or removes it depending on if the post blocks contain + * Inserts the footnotes block or removes it depending on whether the post blocks contain * footnotes or not. * - * @param {Object} updatedBlocks Object containing the updated blocks in a hierarchically + * @param {Object} updatedBlocks Object containing the updated blocks in a hierarchical * form with a blockFootnotes property which contains the new footnotes. * @param {?string} removedBlock Uid of the removed block. */ From ac2e5af31b6f860556f2bb9fd69310d804aed335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Mon, 21 May 2018 20:59:20 +0200 Subject: [PATCH 31/36] Don't trigger updateFootnotes when spliting a paragraph block --- core-blocks/paragraph/index.js | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/core-blocks/paragraph/index.js b/core-blocks/paragraph/index.js index a2f6f21a61f010..d3cc887e14547f 100644 --- a/core-blocks/paragraph/index.js +++ b/core-blocks/paragraph/index.js @@ -249,16 +249,11 @@ class ParagraphBlock extends Component { } } onSplit={ insertBlocksAfter ? ( before, after, ...blocks ) => { - const beforeFootnotes = parseFootnotesFromContent( before ); - - const afterFootnotes = parseFootnotesFromContent( after ); - const afterBlock = createBlock( name, { - content: after, - blockFootnotes: afterFootnotes, - } ); - if ( after ) { - blocks.push( afterBlock ); + blocks.push( createBlock( name, { + content: after, + blockFootnotes: parseFootnotesFromContent( after ), + } ) ); } insertBlocksAfter( blocks ); @@ -266,15 +261,11 @@ class ParagraphBlock extends Component { if ( before ) { setAttributes( { content: before, - blockFootnotes: beforeFootnotes, + blockFootnotes: parseFootnotesFromContent( before ), } ); } else { onReplace( [] ); } - - if ( ! isEqual( blockFootnotes, beforeFootnotes ) && afterFootnotes.length ) { - updateFootnotes( beforeFootnotes, { [ afterBlock.uid ]: afterFootnotes } ); - } } : undefined } From 72277b51ff79794090285368854dd3d58db838ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Tue, 22 May 2018 23:56:35 +0200 Subject: [PATCH 32/36] Update footnotes attribute when the order is changed There was a bug that when reordering blocks with footnotes, those were not reordered when saving the post. --- core-blocks/footnotes/editor.js | 107 ++++++++++++++++++++++++++++++++ core-blocks/footnotes/index.js | 58 +---------------- 2 files changed, 109 insertions(+), 56 deletions(-) create mode 100644 core-blocks/footnotes/editor.js diff --git a/core-blocks/footnotes/editor.js b/core-blocks/footnotes/editor.js new file mode 100644 index 00000000000000..07ba19b9515205 --- /dev/null +++ b/core-blocks/footnotes/editor.js @@ -0,0 +1,107 @@ +/** + * External dependencies + */ +import { get, isEqual } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { withSelect } from '@wordpress/data'; +import { RichText } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import './editor.scss'; + +class FootnotesEditor extends Component { + constructor( props ) { + super( ...arguments ); + + this.setFootnotesInOrder( props.footnotesOrder ); + this.state = { + editable: null, + }; + } + + setFootnotesInOrder( footnotesOrder ) { + const { attributes, setAttributes } = this.props; + + const footnotes = footnotesOrder.map( ( { id } ) => { + return this.getFootnoteById( attributes.footnotes, id ); + } ); + + setAttributes( { footnotes } ); + } + + getFootnoteById( footnotes, footnoteUid ) { + const filteredFootnotes = footnotes.filter( + ( footnote ) => footnote.id === footnoteUid ); + + return get( filteredFootnotes, [ 0 ], { id: footnoteUid, text: '' } ); + } + + onChange( footnoteUid ) { + return ( nextValue ) => { + const { attributes, footnotesOrder, setAttributes } = this.props; + + const nextFootnotes = footnotesOrder.map( ( { id } ) => { + if ( id === footnoteUid ) { + return { + id, + text: nextValue, + }; + } + + return this.getFootnoteById( attributes.footnotes, id ); + } ); + + setAttributes( { + footnotes: nextFootnotes, + } ); + }; + } + + onSetActiveEditable( id ) { + return () => { + this.setState( { editable: id } ); + }; + } + + componentWillReceiveProps( nextProps ) { + const { footnotesOrder } = this.props; + const nextFootnotesOrder = nextProps.footnotesOrder; + + if ( ! isEqual( footnotesOrder, nextFootnotesOrder ) ) { + this.setFootnotesInOrder( nextFootnotesOrder ); + } + } + + render() { + const { attributes, editable, isSelected } = this.props; + const { footnotes } = attributes; + + return ( +
              + { footnotes.map( ( footnote ) => ( +
            1. + +
            2. + ) ) } +
            + ); + } +} + +export default withSelect( ( select ) => ( { + footnotesOrder: select( 'core/editor' ).getFootnotes(), +} ) )( FootnotesEditor ); diff --git a/core-blocks/footnotes/index.js b/core-blocks/footnotes/index.js index c36f123304b78b..bfd9b590ad5633 100644 --- a/core-blocks/footnotes/index.js +++ b/core-blocks/footnotes/index.js @@ -1,21 +1,13 @@ -/** - * External dependencies - */ -import { get } from 'lodash'; - /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { withState } from '@wordpress/components'; -import { withSelect } from '@wordpress/data'; -import { RichText } from '@wordpress/blocks'; /** * Internal dependencies */ +import FootnotesEditor from './editor.js'; import './style.scss'; -import './editor.scss'; export const name = 'core/footnotes'; @@ -44,53 +36,7 @@ export const settings = { }, }, - edit: withSelect( ( select ) => ( { - footnotesOrder: select( 'core/editor' ).getFootnotes(), - } ) )( withState( { - editable: null, - } )( ( { attributes, editable, footnotesOrder, isSelected, setAttributes, setState } ) => { - const { footnotes } = attributes; - const onSetActiveEditable = ( index ) => () => { - setState( { editable: index } ); - }; - - return ( -
              - { footnotesOrder.map( ( footnotesOrderItem, i ) => { - const filteredFootnotes = footnotes.filter( - ( footnote ) => footnote.id === footnotesOrderItem.id ); - const value = get( filteredFootnotes, [ 0, 'text' ] ); - - return ( -
            1. - { - const nextFootnotes = footnotes.filter( - ( footnote ) => footnote.id !== footnotesOrderItem.id ); - - nextFootnotes.push( { - id: footnotesOrderItem.id, - text: nextValue, - } ); - - setAttributes( { - footnotes: nextFootnotes, - } ); - } - } - isSelected={ isSelected && editable === i } - placeholder={ __( 'Write footnote…' ) } - onFocus={ onSetActiveEditable( i ) } - /> -
            2. - ); - } ) } -
            - ); - } ) ), + edit: FootnotesEditor, save( { attributes } ) { const { footnotes } = attributes; From 11794cc28056a76e5e116bf0ed0e6ff0b77cfd60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Sun, 27 May 2018 19:07:19 +0200 Subject: [PATCH 33/36] Order footnotes when saving footnotes block This way we don't have to update the footnotes block attributes everytime the order changes. --- core-blocks/footnotes/editor.js | 49 ++++--------------- core-blocks/footnotes/footnotes-utils.js | 40 +++++++++++++++ core-blocks/footnotes/index.js | 7 ++- core-blocks/footnotes/test/footnotes-utils.js | 34 +++++++++++++ 4 files changed, 89 insertions(+), 41 deletions(-) create mode 100644 core-blocks/footnotes/footnotes-utils.js create mode 100644 core-blocks/footnotes/test/footnotes-utils.js diff --git a/core-blocks/footnotes/editor.js b/core-blocks/footnotes/editor.js index 07ba19b9515205..b923c44eeacda7 100644 --- a/core-blocks/footnotes/editor.js +++ b/core-blocks/footnotes/editor.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { get, isEqual } from 'lodash'; - /** * WordPress dependencies */ @@ -14,40 +9,23 @@ import { RichText } from '@wordpress/blocks'; /** * Internal dependencies */ +import { getFootnoteByUid, orderFootnotes } from './footnotes-utils.js'; import './editor.scss'; class FootnotesEditor extends Component { - constructor( props ) { + constructor() { super( ...arguments ); - this.setFootnotesInOrder( props.footnotesOrder ); this.state = { editable: null, }; } - setFootnotesInOrder( footnotesOrder ) { - const { attributes, setAttributes } = this.props; - - const footnotes = footnotesOrder.map( ( { id } ) => { - return this.getFootnoteById( attributes.footnotes, id ); - } ); - - setAttributes( { footnotes } ); - } - - getFootnoteById( footnotes, footnoteUid ) { - const filteredFootnotes = footnotes.filter( - ( footnote ) => footnote.id === footnoteUid ); - - return get( filteredFootnotes, [ 0 ], { id: footnoteUid, text: '' } ); - } - onChange( footnoteUid ) { return ( nextValue ) => { - const { attributes, footnotesOrder, setAttributes } = this.props; + const { attributes, orderedFootnoteUids, setAttributes } = this.props; - const nextFootnotes = footnotesOrder.map( ( { id } ) => { + const nextFootnotes = orderedFootnoteUids.map( ( { id } ) => { if ( id === footnoteUid ) { return { id, @@ -55,7 +33,7 @@ class FootnotesEditor extends Component { }; } - return this.getFootnoteById( attributes.footnotes, id ); + return getFootnoteByUid( attributes.footnotes, id ); } ); setAttributes( { @@ -70,22 +48,13 @@ class FootnotesEditor extends Component { }; } - componentWillReceiveProps( nextProps ) { - const { footnotesOrder } = this.props; - const nextFootnotesOrder = nextProps.footnotesOrder; - - if ( ! isEqual( footnotesOrder, nextFootnotesOrder ) ) { - this.setFootnotesInOrder( nextFootnotesOrder ); - } - } - render() { - const { attributes, editable, isSelected } = this.props; - const { footnotes } = attributes; + const { attributes, editable, orderedFootnoteUids, isSelected } = this.props; + const orderedFootnotes = orderFootnotes( attributes.footnotes, orderedFootnoteUids ); return (
              - { footnotes.map( ( footnote ) => ( + { orderedFootnotes.map( ( footnote ) => (
            1. ( { - footnotesOrder: select( 'core/editor' ).getFootnotes(), + orderedFootnoteUids: select( 'core/editor' ).getFootnotes(), } ) )( FootnotesEditor ); diff --git a/core-blocks/footnotes/footnotes-utils.js b/core-blocks/footnotes/footnotes-utils.js new file mode 100644 index 00000000000000..14a6de0f99134c --- /dev/null +++ b/core-blocks/footnotes/footnotes-utils.js @@ -0,0 +1,40 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; + +/** + * Given an array of footnotes and a UID, returns the footnote object associated + * with that UID. If the array doesn't contain the footnote, a footnote object is + * returned with the given ID and an empty text. + * + * @param {Array} footnotes Array of footnotes. + * @param {Array} footnoteUID UID of the footnote to return. + * + * @return {Object} Footnote object with the id and the text of the footnote. If + * the footnote doesn't exist in the array, the text is an empty string. + */ +const getFootnoteByUid = function( footnotes, footnoteUID ) { + const filteredFootnotes = footnotes.filter( + ( footnote ) => footnote.id === footnoteUID ); + + return get( filteredFootnotes, [ 0 ], { id: footnoteUID, text: '' } ); +}; + +/** + * Orders an array of footnotes based on another array with the footnote UIDs + * ordered. + * + * @param {Array} footnotes Array of unordered footnotes. + * @param {Array} orderedFootnotesUids Array of ordered footnotes UIDs. Every + * element of the array must be an object with an id property, like the one + * returned after parsing the attributes. + * + * @return {Array} Array of footnotes ordered. + */ +const orderFootnotes = function( footnotes, orderedFootnotesUids ) { + return orderedFootnotesUids.map( + ( { id } ) => getFootnoteByUid( footnotes, id ) ); +}; + +export { getFootnoteByUid, orderFootnotes }; diff --git a/core-blocks/footnotes/index.js b/core-blocks/footnotes/index.js index bfd9b590ad5633..4e8d7b320683c6 100644 --- a/core-blocks/footnotes/index.js +++ b/core-blocks/footnotes/index.js @@ -2,11 +2,13 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { select } from '@wordpress/data'; /** * Internal dependencies */ import FootnotesEditor from './editor.js'; +import { orderFootnotes } from './footnotes-utils.js'; import './style.scss'; export const name = 'core/footnotes'; @@ -39,7 +41,10 @@ export const settings = { edit: FootnotesEditor, save( { attributes } ) { - const { footnotes } = attributes; + const orderedFootnoteUids = select( 'core/editor' ).getFootnotes(); + const footnotes = orderedFootnoteUids && orderedFootnoteUids.length ? + orderFootnotes( attributes.footnotes, orderedFootnoteUids ) : + attributes.footnotes; return (
              diff --git a/core-blocks/footnotes/test/footnotes-utils.js b/core-blocks/footnotes/test/footnotes-utils.js new file mode 100644 index 00000000000000..4a4fde18c219a1 --- /dev/null +++ b/core-blocks/footnotes/test/footnotes-utils.js @@ -0,0 +1,34 @@ +/** + * Internal dependencies + */ +import { getFootnoteByUid, orderFootnotes } from '../footnotes-utils.js'; + +describe( 'footnotes utils', () => { + describe( 'getFootnoteByUid', () => { + it( 'should return footnote associated with the id', () => { + const footnote = { id: 'abcd', text: 'ABCD' }; + const id = 'abcd'; + + expect( getFootnoteByUid( [ footnote ], id ) ).toEqual( footnote ); + } ); + + it( 'should return a footnote without text when the id is not found', () => { + const emptyFootnote = { id: 'abcd', text: '' }; + const id = 'abcd'; + + expect( getFootnoteByUid( [], id ) ).toEqual( emptyFootnote ); + } ); + } ); + + describe( 'orderFootnotes', () => { + it( 'should return ordered footnotes', () => { + const footnote1 = { id: 'abcd1', text: 'ABCD1' }; + const footnote2 = { id: 'abcd2', text: 'ABCD2' }; + const footnotes = [ footnote1, footnote2 ]; + const orderedFootnoteUids = [ { id: footnote2.id }, { id: footnote1.id } ]; + + expect( orderFootnotes( footnotes, orderedFootnoteUids ) ) + .toEqual( [ footnote2, footnote1 ] ); + } ); + } ); +} ); From c958ccad24967c7899b27f84e1fa9d7b37fe9c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Mon, 4 Jun 2018 19:56:29 +0200 Subject: [PATCH 34/36] Rename FootnotesEditor to FootnotesEdit to keep consistency with other blocks --- core-blocks/footnotes/{editor.js => edit.js} | 4 ++-- core-blocks/footnotes/index.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename core-blocks/footnotes/{editor.js => edit.js} (96%) diff --git a/core-blocks/footnotes/editor.js b/core-blocks/footnotes/edit.js similarity index 96% rename from core-blocks/footnotes/editor.js rename to core-blocks/footnotes/edit.js index b923c44eeacda7..3e952ce4e46db0 100644 --- a/core-blocks/footnotes/editor.js +++ b/core-blocks/footnotes/edit.js @@ -12,7 +12,7 @@ import { RichText } from '@wordpress/blocks'; import { getFootnoteByUid, orderFootnotes } from './footnotes-utils.js'; import './editor.scss'; -class FootnotesEditor extends Component { +class FootnotesEdit extends Component { constructor() { super( ...arguments ); @@ -73,4 +73,4 @@ class FootnotesEditor extends Component { export default withSelect( ( select ) => ( { orderedFootnoteUids: select( 'core/editor' ).getFootnotes(), -} ) )( FootnotesEditor ); +} ) )( FootnotesEdit ); diff --git a/core-blocks/footnotes/index.js b/core-blocks/footnotes/index.js index 4e8d7b320683c6..b9927fd39edbfc 100644 --- a/core-blocks/footnotes/index.js +++ b/core-blocks/footnotes/index.js @@ -7,7 +7,7 @@ import { select } from '@wordpress/data'; /** * Internal dependencies */ -import FootnotesEditor from './editor.js'; +import FootnotesEdit from './edit.js'; import { orderFootnotes } from './footnotes-utils.js'; import './style.scss'; @@ -38,7 +38,7 @@ export const settings = { }, }, - edit: FootnotesEditor, + edit: FootnotesEdit, save( { attributes } ) { const orderedFootnoteUids = select( 'core/editor' ).getFootnotes(); From 578d71a5de16b2f9a5418430710613312a75ff4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Mon, 4 Jun 2018 19:58:15 +0200 Subject: [PATCH 35/36] Replace spaces with tabs in footnotes style sheets --- core-blocks/footnotes/style.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core-blocks/footnotes/style.scss b/core-blocks/footnotes/style.scss index 08954e1229d1c4..027f8f644c5717 100644 --- a/core-blocks/footnotes/style.scss +++ b/core-blocks/footnotes/style.scss @@ -1,9 +1,9 @@ .post, .edit-post-visual-editor { - counter-reset: footnotes; + counter-reset: footnotes; } .wp-footnote::before { - counter-increment: footnotes; - content: counter(footnotes); + counter-increment: footnotes; + content: counter(footnotes); } From 400140fb5cba1069ec528e67df4fd814dc133b73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Sun, 10 Jun 2018 18:00:20 +0200 Subject: [PATCH 36/36] Make the footnote number non-editable Adding contenteditable="false" the user can't edit inside the SUP tag, which makes interacting with footnote numbers much easier because the cursor can't get caught inside its tags. --- editor/components/rich-text/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/components/rich-text/index.js b/editor/components/rich-text/index.js index a679c9b6b9ccae..bab62ff23717a7 100644 --- a/editor/components/rich-text/index.js +++ b/editor/components/rich-text/index.js @@ -838,7 +838,7 @@ export class RichText extends Component { return; } const uid = uuid(); - this.editor.insertContent( `${ __( 'See footnote' ) } ` ); + this.editor.insertContent( `${ __( 'See footnote' ) } ` ); } /**