Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add footnotes support to Gutenberg #6549

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
791305a
Add footnotes control button to paragraph block
Aljullu May 2, 2018
533aa51
Store footnote ids in the block object
Aljullu May 2, 2018
95237fc
Create footnotes block on editor setup and update footnotes on block …
Aljullu May 4, 2018
213150e
Create Footnotes block
Aljullu May 5, 2018
2e6de5e
Use auto-incrementing numbers instead of asterisks for footnotes
Aljullu May 5, 2018
40dc686
Update footnotes block when inserting or removing blocks
Aljullu May 5, 2018
b5cee89
Make footnotes block to render nothing if there are no footnotes
Aljullu May 5, 2018
3941d18
Rename variables like 'newState' to 'nextState' to keep consistency w…
Aljullu May 6, 2018
55889c2
Use a template literal for the footnotes markup
Aljullu May 6, 2018
202dfd9
Save block footnotes in the block attributes instead of a block prope…
Aljullu May 6, 2018
e8597a0
Use withSelect HOC in Footnotes block so it's automatically updated w…
Aljullu May 6, 2018
ec802ab
Clean up parser code no longer being used
Aljullu May 6, 2018
d2309af
Simplify the code which rendersthe footnotes block
Aljullu May 6, 2018
e7dc2bc
Create insertFootnotesBlock action to create the footnotes block when…
Aljullu May 8, 2018
23a3037
Remove footnotes block when the last footnote is deleted
Aljullu May 8, 2018
f519cac
Import parseFootnotesFromContent from @wordpress/editor instead of @w…
Aljullu May 8, 2018
e7715cd
Update paragraph snapshots with blockFootnotes attribute
Aljullu May 8, 2018
a9ddeee
Flatten the blocks before searching for footnotes
Aljullu May 9, 2018
db6e61d
Clean up unnecessary properties from footnotes tests
Aljullu May 9, 2018
991a67a
Allow only one instance of footnotes block
Aljullu May 10, 2018
bed9b39
Remove check if core/editor selector is defined in footnotes block
Aljullu May 15, 2018
33d0045
Add/remove the footnotes blocks every time the first footnote is adde…
Aljullu May 16, 2018
d06aff6
Rename footnote SUP element attribute from data-footnote-id to data-w…
Aljullu May 20, 2018
75fc957
Clean up footnotes block attributes
Aljullu May 20, 2018
089dd9d
Optimize checking if footnotes block must be added/removed
Aljullu May 20, 2018
c33beac
Update footnotes block fixtures
Aljullu May 21, 2018
8c2a1a2
Raise paragraph key in pullquote test so it doesn't collide with rece…
Aljullu May 21, 2018
b3c303d
Extract blockFootnotes paragraph attribute from the markup
Aljullu May 21, 2018
14b9b2d
Refactor getFootnotes selector
Aljullu May 21, 2018
0646693
Several minor fixes and typos corrected
Aljullu May 21, 2018
ac2e5af
Don't trigger updateFootnotes when spliting a paragraph block
Aljullu May 21, 2018
72277b5
Update footnotes attribute when the order is changed
Aljullu May 22, 2018
11794cc
Order footnotes when saving footnotes block
Aljullu May 27, 2018
c958cca
Rename FootnotesEditor to FootnotesEdit to keep consistency with othe…
Aljullu Jun 4, 2018
578d71a
Replace spaces with tabs in footnotes style sheets
Aljullu Jun 4, 2018
400140f
Make the footnote number non-editable
Aljullu Jun 10, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions blocks/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {
export {
default as parse,
getBlockAttributes,
parseFootnotesFromContent,
parseWithAttributeSchema,
} from './parser';
export { default as rawHandler, getPhrasingContentSchema } from './raw-handling';
Expand Down
24 changes: 24 additions & 0 deletions blocks/api/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,30 @@ 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[ 'data-wp-footnote-id' ] ) {
return [
...footnotes,
{ id: element.props[ 'data-wp-footnote-id' ] },
];
}

return footnotes;
}, [] );
}

/**
* Creates a block with fallback to the unknown type handler.
*
Expand Down
20 changes: 20 additions & 0 deletions blocks/api/test/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getBlockAttribute,
getBlockAttributes,
asType,
parseFootnotesFromContent,
createBlockWithFallback,
getAttributesAndInnerBlocksFromDeprecatedVersion,
default as parse,
Expand Down Expand Up @@ -312,6 +313,25 @@ 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: { 'data-wp-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 );
Expand Down
76 changes: 76 additions & 0 deletions core-blocks/footnotes/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { withSelect } from '@wordpress/data';
import { RichText } from '@wordpress/blocks';

/**
* Internal dependencies
*/
import { getFootnoteByUid, orderFootnotes } from './footnotes-utils.js';
import './editor.scss';

class FootnotesEdit extends Component {
constructor() {
super( ...arguments );

this.state = {
editable: null,
};
}

onChange( footnoteUid ) {
return ( nextValue ) => {
const { attributes, orderedFootnoteUids, setAttributes } = this.props;

const nextFootnotes = orderedFootnoteUids.map( ( { id } ) => {
if ( id === footnoteUid ) {
return {
id,
text: nextValue,
};
}

return getFootnoteByUid( attributes.footnotes, id );
} );

setAttributes( {
footnotes: nextFootnotes,
} );
};
}

onSetActiveEditable( id ) {
return () => {
this.setState( { editable: id } );
};
}

render() {
const { attributes, editable, orderedFootnoteUids, isSelected } = this.props;
const orderedFootnotes = orderFootnotes( attributes.footnotes, orderedFootnoteUids );

return (
<ol className="blocks-footnotes__footnotes-list">
{ orderedFootnotes.map( ( footnote ) => (
<li key={ footnote.id }>
<RichText
tagName="span"
value={ footnote.text }
onChange={ this.onChange( footnote.id ) }
isSelected={ isSelected && editable === footnote.id }
placeholder={ __( 'Write footnote…' ) }
onFocus={ this.onSetActiveEditable( footnote.id ) }
/>
</li>
) ) }
</ol>
);
}
}

export default withSelect( ( select ) => ( {
orderedFootnoteUids: select( 'core/editor' ).getFootnotes(),
} ) )( FootnotesEdit );
3 changes: 3 additions & 0 deletions core-blocks/footnotes/editor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.edit-post-visual-editor .blocks-footnotes__footnotes-list {
padding-left: 1.3em;
}
40 changes: 40 additions & 0 deletions core-blocks/footnotes/footnotes-utils.js
Original file line number Diff line number Diff line change
@@ -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 };
61 changes: 61 additions & 0 deletions core-blocks/footnotes/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { select } from '@wordpress/data';

/**
* Internal dependencies
*/
import FootnotesEdit from './edit.js';
import { orderFootnotes } from './footnotes-utils.js';
import './style.scss';

export const name = 'core/footnotes';

export const settings = {
title: __( 'Footnotes' ),
description: __( 'List of footnotes from the article' ),
category: 'common',
useOnce: true,
keywords: [ __( 'footnotes' ), __( 'references' ) ],

attributes: {
footnotes: {
type: 'array',
source: 'query',
selector: 'li',
query: {
id: {
source: 'attribute',
attribute: 'id',
},
text: {
source: 'children',
},
},
default: [],
},
},

edit: FootnotesEdit,

save( { attributes } ) {
const orderedFootnoteUids = select( 'core/editor' ).getFootnotes();
const footnotes = orderedFootnoteUids && orderedFootnoteUids.length ?
orderFootnotes( attributes.footnotes, orderedFootnoteUids ) :
attributes.footnotes;

return (
<div>
<ol>
{ footnotes.map( ( footnote ) => (
<li id={ footnote.id } key={ footnote.id }>
{ footnote.text }
</li>
) ) }
</ol>
</div>
);
},
};
9 changes: 9 additions & 0 deletions core-blocks/footnotes/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.post,
.edit-post-visual-editor {
counter-reset: footnotes;
}

.wp-footnote::before {
counter-increment: footnotes;
content: counter(footnotes);
}
7 changes: 7 additions & 0 deletions core-blocks/footnotes/test/__snapshots__/index.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`core/footnotes block edit matches snapshot 1`] = `
<ol
class="blocks-footnotes__footnotes-list"
/>
`;
34 changes: 34 additions & 0 deletions core-blocks/footnotes/test/footnotes-utils.js
Original file line number Diff line number Diff line change
@@ -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 ] );
} );
} );
} );
14 changes: 14 additions & 0 deletions core-blocks/footnotes/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Internal dependencies
*/
import { name, settings } from '../';
import { blockEditRender } from '../../test/helpers';
import '@wordpress/editor';

describe( 'core/footnotes', () => {
test( 'block edit matches snapshot', () => {
const wrapper = blockEditRender( name, settings );

expect( wrapper ).toMatchSnapshot();
} );
} );
2 changes: 2 additions & 0 deletions core-blocks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -65,6 +66,7 @@ export const registerCoreBlocks = () => {
...embed.common,
...embed.others,
freeform,
footnotes,
html,
latestPosts,
more,
Expand Down
Loading