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: Table of Contents Block #66

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Empty file added blocks/toc/editor.scss
Empty file.
7 changes: 7 additions & 0 deletions blocks/toc/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

register_block_type( 'jetpack/toc', [
'editor_script' => 'wpcom-blocks',
'style' => 'wpcom-blocks',
'editor_style' => 'wpcom-blocks-editor',
] );
13 changes: 13 additions & 0 deletions blocks/toc/src/block.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "jetpack/toc",
"category": "layout",
"attributes": {
"ListType": {
"type": "string",
"default": "ol"
},
"nodes": {
"type": "array"
}
}
}
137 changes: 137 additions & 0 deletions blocks/toc/src/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* WordPress dependencies
*/
import { BlockControls } from "@wordpress/block-editor";
import { ToolbarGroup } from "@wordpress/components";
import { useDispatch, useSelect } from "@wordpress/data";
import { useEffect } from "@wordpress/element";
import { __ } from "@wordpress/i18n";
// import { formatListBullets, formatListNumbered } from "@wordpress/icons";
import { SVG, Path } from "@wordpress/primitives";

/**
* Internal dependencies
*/
import Tree from "./tree";
import { formatListBullets, formatListNumbered } from "./icons";

/**
* External dependencies
*/
import { kebabCase, last } from "lodash";

const node = (block) => ({
children: [],
level: block.attributes.level,
anchor: block.attributes.anchor,
content: block.attributes.content,
});

const parent = (stack) => last(stack);
const sibling = (stack) => last(parent(stack).children);

const nestTree = (blocks) => {
const rootNode = node({ attributes: { level: 0 } });
rootNode.children = [node(blocks[0])];
const nodeStack = [rootNode];
for (let i = 1; i < blocks.length; i++) {
const currentNode = node(blocks[i]);
let parentNode = parent(nodeStack);
let siblingNode = sibling(nodeStack);

if (currentNode.level === siblingNode.level) {
parentNode.children.push(currentNode);
} else if (siblingNode.level < currentNode.level) {
nodeStack.push(siblingNode);
siblingNode.children.push(currentNode);
} else if (currentNode.level < siblingNode.level) {
while (currentNode.level < siblingNode.level) {
siblingNode = nodeStack.pop();
parentNode = parent(nodeStack);
}
parentNode.children.push(currentNode);
}
}

return rootNode.children;
};

const makeAnchor = (title, usedAnchors) => {
const kebabTitle = kebabCase(title);
let titleCandidate = kebabTitle,
i = 0;
while (usedAnchors.has(titleCandidate)) {
titleCandidate = kebabTitle + "-" + i++;
}
return titleCandidate;
};

export default ({ attributes, setAttributes }) => {
const { updateBlockAttributes } = useDispatch("core/editor");

const { headings, anchors } = useSelect((select) => {
const headings = select("core/block-editor")
.getBlocks()
.filter((block) => block.name === "core/heading");

const anchors = select("core/block-editor")
.getBlocks()
// ignore uncommitted anchors so they continue to update
.filter(
(block) => block.attributes.anchor && !block.attributes.tempAnchor
)
.map((block) => block.attributes.anchor);

return { headings, anchors };
});

const usedAnchors = new Set(anchors);

// We are storing a temp anchor name on anchors we've created so they get updated
// as the content changes
headings
.filter((block) => block.attributes.tempAnchor || !block.attributes.anchor)
.forEach((block) => {
const newAnchor = makeAnchor(block.attributes.content, usedAnchors);
updateBlockAttributes(block.clientId, {
anchor: newAnchor,
tempAnchor: newAnchor,
});
usedAnchors.add(newAnchor);
});

const nodes = nestTree(headings);
useEffect(() => {
setAttributes({ nodes });
}, [...headings]);

const ListType = attributes.ordered ? "ol" : "ul";

return (
<>
<BlockControls>
<ToolbarGroup
controls={[
{
icon: formatListBullets,
title: __("Convert to unordered list"),
isActive: ListType === "ul",
onClick() {
setAttributes({ ordered: false });
},
},
{
icon: formatListNumbered,
title: __("Convert to ordered list"),
isActive: ListType === "ol",
onClick() {
setAttributes({ ordered: true });
},
},
]}
/>
</BlockControls>
<Tree nodes={nodes} ListType={ListType} />
</>
);
};
22 changes: 22 additions & 0 deletions blocks/toc/src/icons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* WordPress dependencies
*/
import { SVG, Path } from "@wordpress/primitives";

export const formatListBullets = (
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<Path d="M4 7.2v1.5h16V7.2H4zm7.1 8.6H20v-1.5h-8.9v1.5zM6 13c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
</SVG>
);

export const formatListNumbered = (
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<Path d="M11.1 15.8H20v-1.5h-8.9v1.5zM4 7.2v1.5h16V7.2H4zm.2 6.8l.8-.3V17h1v-4.7l-2.2.7.4 1z" />
</SVG>
);

export const icon = (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M4 15.8h8.9v-1.5H4v1.5zm0-7h8.9V7.2H4v1.6zM18 13c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z" />
</svg>
);
25 changes: 25 additions & 0 deletions blocks/toc/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* WordPress dependencies
*/
import { registerBlockType } from "@wordpress/blocks";
import { __ } from "@wordpress/i18n";

/**
* Internal dependencies
*/
import metadata from "./block.json";
import edit from "./edit";
import save from "./save";
import { icon } from "./icons";

const { name, category, attributes } = metadata;

export const registerBlock = () =>
registerBlockType(name, {
title: __("Table of Contents"),
icon,
category,
attributes,
edit,
save,
});
10 changes: 10 additions & 0 deletions blocks/toc/src/save.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Internal dependencies
*/
import Tree from "./tree";

export default ({ attributes }) => {
const { nodes, ordered } = attributes;
const ListType = ordered ? "ol" : "ul";
return <Tree nodes={nodes} ListType={ListType} />;
};
22 changes: 22 additions & 0 deletions blocks/toc/src/tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const Tree = ({ nodes, ListType }) => {
if (!nodes || !nodes.length) {
return null;
}

return (
<ListType>
{nodes.map(node => {
const { children, anchor, content } = node;

return (
<li key={anchor}>
<a href={"#" + anchor}>{content}</a>
<Tree nodes={children} ListType={ListType} />
</li>
);
})}
</ListType>
);
};

export default Tree;
Empty file added blocks/toc/style.scss
Empty file.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@wordpress/icons": "^1.1.0",
"@wordpress/icons": "^1.2.0",
"@wordpress/scripts": "^6.0.0",
"autoprefixer": "^9.7.4",
"eslint": "^6.7.2",
Expand Down
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import * as layoutGridBlock from '../blocks/layout-grid/src';
import * as motionBackgroundBlock from '../blocks/motion-background/src';
import * as richImageTools from '../blocks/rich-image/src';
import * as starscapeBlock from '../blocks/starscape/src';
import * as tocBlock from '../blocks/toc/src';

// Instantiate the blocks, adding them to our block category
bauhausCentenaryBlock.registerBlock();
Expand All @@ -38,3 +39,5 @@ layoutGridBlock.registerBlock();
motionBackgroundBlock.registerBlock();
richImageTools.registerBlock();
starscapeBlock.registerBlock();
tocBlock.registerBlock();

50 changes: 49 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,13 @@
dependencies:
regenerator-runtime "^0.13.2"

"@babel/runtime@^7.8.3":
version "7.9.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==
dependencies:
regenerator-runtime "^0.13.4"

"@babel/template@^7.4.0", "@babel/template@^7.7.4":
version "7.7.4"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.4.tgz#428a7d9eecffe27deac0a98e23bf8e3675d2a77b"
Expand Down Expand Up @@ -1297,6 +1304,17 @@
webpack "^4.8.3"
webpack-sources "^1.3.0"

"@wordpress/element@^2.12.0":
version "2.12.0"
resolved "https://registry.yarnpkg.com/@wordpress/element/-/element-2.12.0.tgz#356ed5dcc812f4f26cea8ea254163724f0e6d378"
integrity sha512-XVTEHkqvkaTv6W2vNENnXHPJdc9BGn1vqsbPnfDKC7UzSyr7Vn2yFiJxLgv8wZmkia720afAKOHcaseXoXlYsw==
dependencies:
"@babel/runtime" "^7.8.3"
"@wordpress/escape-html" "^1.7.0"
lodash "^4.17.15"
react "^16.9.0"
react-dom "^16.9.0"

"@wordpress/element@^2.9.0":
version "2.9.0"
resolved "https://registry.yarnpkg.com/@wordpress/element/-/element-2.9.0.tgz#5f374d6ea7ec18d4064b57e3e790f10354e29732"
Expand All @@ -1315,6 +1333,13 @@
dependencies:
"@babel/runtime" "^7.4.4"

"@wordpress/escape-html@^1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@wordpress/escape-html/-/escape-html-1.7.0.tgz#8f3e1798a23c16deac7bd0e416f11b90f3f31e89"
integrity sha512-xDOBo0P3Jnbdbb/UypsQaplsD2k4UXgd/EpKhMAKhDa2m20GxWWmEKW9IB3/5bS4Rh2YZjVM9WL4JyWPUo4hEA==
dependencies:
"@babel/runtime" "^7.8.3"

"@wordpress/eslint-plugin@^3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@wordpress/eslint-plugin/-/eslint-plugin-3.2.0.tgz#e3958c0b8e9a5421ed0d88a5c92040d230b3a0da"
Expand All @@ -1329,6 +1354,15 @@
globals "^12.0.0"
requireindex "^1.2.0"

"@wordpress/icons@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@wordpress/icons/-/icons-1.2.0.tgz#734c8d684c8a02d688e8d96e5ce57de574d5c388"
integrity sha512-9QuXr1FA4byrlQ5SQWoVE/BMzoDqhX1AYuzZeZh9aa9dlHLo8cMQ0hGhbgtrv8GsztRniRZ/ms7UqlkFxXgD8g==
dependencies:
"@babel/runtime" "^7.8.3"
"@wordpress/element" "^2.12.0"
"@wordpress/primitives" "^1.2.0"

"@wordpress/jest-console@^3.4.0":
version "3.4.0"
resolved "https://registry.yarnpkg.com/@wordpress/jest-console/-/jest-console-3.4.0.tgz#3022e947d3c2c3aaf6e419bd4bbe4c648a41c390"
Expand All @@ -1354,6 +1388,15 @@
resolved "https://registry.yarnpkg.com/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-2.1.0.tgz#961f42a3f047abe5463a4cb0474a4423bf8b510e"
integrity sha512-NSwcK7GtlmW5O5ZMG7elRKBa9sPws17Sadjlztig6ShOuhlLFeHYk99tUenpmJ/PYOZex4fSJ5e9mqjPyKunjw==

"@wordpress/primitives@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@wordpress/primitives/-/primitives-1.2.0.tgz#b7aec50761f9b8cc89f0a2176709e6500567d283"
integrity sha512-tUs2h9Pq0WW5THlD3L+gBN6reio3bpF01igDmTHYFopzQjhnZTEu3oIRjQWH6gqphBLILcknjhW19EL9ISh1TA==
dependencies:
"@babel/runtime" "^7.8.3"
"@wordpress/element" "^2.12.0"
classnames "^2.2.5"

"@wordpress/scripts@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@wordpress/scripts/-/scripts-6.0.0.tgz#4f900d92f7b20d2219a0a27635559d47307aa447"
Expand Down Expand Up @@ -2461,7 +2504,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"

classnames@^2.2.6:
classnames@^2.2.5, classnames@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
Expand Down Expand Up @@ -7867,6 +7910,11 @@ regenerator-runtime@^0.13.2:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==

regenerator-runtime@^0.13.4:
version "0.13.5"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==

regenerator-transform@^0.14.0:
version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb"
Expand Down