diff --git a/blocks/toc/editor.scss b/blocks/toc/editor.scss new file mode 100644 index 00000000..e69de29b diff --git a/blocks/toc/index.php b/blocks/toc/index.php new file mode 100644 index 00000000..6b688bbd --- /dev/null +++ b/blocks/toc/index.php @@ -0,0 +1,7 @@ + 'wpcom-blocks', + 'style' => 'wpcom-blocks', + 'editor_style' => 'wpcom-blocks-editor', +] ); diff --git a/blocks/toc/src/block.json b/blocks/toc/src/block.json new file mode 100644 index 00000000..ad8d0894 --- /dev/null +++ b/blocks/toc/src/block.json @@ -0,0 +1,13 @@ +{ + "name": "jetpack/toc", + "category": "layout", + "attributes": { + "ListType": { + "type": "string", + "default": "ol" + }, + "nodes": { + "type": "array" + } + } +} \ No newline at end of file diff --git a/blocks/toc/src/edit.js b/blocks/toc/src/edit.js new file mode 100644 index 00000000..f6d540dd --- /dev/null +++ b/blocks/toc/src/edit.js @@ -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 ( + <> + + + + + + ); +}; diff --git a/blocks/toc/src/icons.js b/blocks/toc/src/icons.js new file mode 100644 index 00000000..ef3ba4c1 --- /dev/null +++ b/blocks/toc/src/icons.js @@ -0,0 +1,22 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from "@wordpress/primitives"; + +export const formatListBullets = ( + + + +); + +export const formatListNumbered = ( + + + +); + +export const icon = ( + + + +); diff --git a/blocks/toc/src/index.js b/blocks/toc/src/index.js new file mode 100644 index 00000000..6f373f13 --- /dev/null +++ b/blocks/toc/src/index.js @@ -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, + }); diff --git a/blocks/toc/src/save.js b/blocks/toc/src/save.js new file mode 100644 index 00000000..e0be80cf --- /dev/null +++ b/blocks/toc/src/save.js @@ -0,0 +1,10 @@ +/** + * Internal dependencies + */ +import Tree from "./tree"; + +export default ({ attributes }) => { + const { nodes, ordered } = attributes; + const ListType = ordered ? "ol" : "ul"; + return ; +}; diff --git a/blocks/toc/src/tree.js b/blocks/toc/src/tree.js new file mode 100644 index 00000000..50fea9b2 --- /dev/null +++ b/blocks/toc/src/tree.js @@ -0,0 +1,22 @@ +const Tree = ({ nodes, ListType }) => { + if (!nodes || !nodes.length) { + return null; + } + + return ( + + {nodes.map(node => { + const { children, anchor, content } = node; + + return ( +
  • + {content} + +
  • + ); + })} +
    + ); +}; + +export default Tree; diff --git a/blocks/toc/style.scss b/blocks/toc/style.scss new file mode 100644 index 00000000..e69de29b diff --git a/package.json b/package.json index 9417e7f6..4293ff11 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.js b/src/index.js index 072bffa5..1cfca60e 100644 --- a/src/index.js +++ b/src/index.js @@ -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(); @@ -38,3 +39,5 @@ layoutGridBlock.registerBlock(); motionBackgroundBlock.registerBlock(); richImageTools.registerBlock(); starscapeBlock.registerBlock(); +tocBlock.registerBlock(); + diff --git a/yarn.lock b/yarn.lock index a4c49070..7b30cd5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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" @@ -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" @@ -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" @@ -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" @@ -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== @@ -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"