diff --git a/package-lock.json b/package-lock.json index 00dbbd2..16c8403 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "superlist-block", - "version": "0.1.1", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "superlist-block", - "version": "0.1.1", + "version": "0.2.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/block-editor": "^8.0.11", diff --git a/package.json b/package.json index 2011d91..5d1dbc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "superlist-block", - "version": "0.1.1", + "version": "0.2.0", "description": "A list block that allows you to nest other blocks in each list item.", "author": "Aurooba Ahmed", "license": "GPL-2.0-or-later", diff --git a/readme.txt b/readme.txt index 76770f7..0a18398 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: block, list, nesting, repeater, superlist Requires at least: 6.3 Tested up to: 6.9 Requires PHP: 7.0 -Stable tag: 0.1.4 +Stable tag: 0.2.0 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html Donate link: https://github.com/sponsors/aurooba @@ -58,6 +58,18 @@ https://youtu.be/wqzw-XrwuQE == Changelog == += 0.2.0 = +* Fix critical editor crash caused by missing `sprintf` import in the list-item block (#60). +* Update block icons to a simpler black-on-transparent style for better cohesion with core blocks (#44). +* Rename "List Orientation" to "List Style" and the previous "List Style" (ul/ol/none) to "List Type", with clearer icons (#36). +* Add background image support via the standard block-supports API (#35). +* Add support for ordered list `start` value and reverse numbering (#25). +* Add optional list title rendered as `
` + `
` (#13). +* Surface the list-item max-width control on the list-item block as well (#9). +* Add a "double-Enter on an empty trailing paragraph" shortcut to create a new list item (#7). +* Add a custom marker character (e.g. ★, →, •) for unordered lists (#3). +* Update transforms to/from the modern `core/list` block, which uses nested `core/list-item` inner blocks (#54). + = 0.1.4 = * Fix issue where you can't add new list items * When adding a new Super List, only one list item is added by default diff --git a/src/superlist-item/edit.js b/src/superlist-item/edit.js index 3a15f37..84ff673 100644 --- a/src/superlist-item/edit.js +++ b/src/superlist-item/edit.js @@ -3,7 +3,7 @@ * * @see https://developer.wordpress.org/block-editor/packages/packages-i18n/ */ -import { __ } from "@wordpress/i18n"; +import { __, sprintf } from "@wordpress/i18n"; /** * React hook that is used to mark the block wrapper element. @@ -15,13 +15,21 @@ import { useBlockProps, InnerBlocks, BlockControls, + InspectorControls, useInnerBlocksProps, store as blockEditorStore, } from "@wordpress/block-editor"; -import { ToolbarButton, ToolbarGroup } from "@wordpress/components"; +import { + ToolbarButton, + ToolbarGroup, + PanelBody, + PanelRow, + __experimentalUnitControl as UnitControl, +} from "@wordpress/components"; import { useSelect, useDispatch } from "@wordpress/data"; import { createBlock, store as blocksStore } from "@wordpress/blocks"; import { plusCircle } from "@wordpress/icons"; +import { useCallback } from "@wordpress/element"; /** * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. @@ -32,6 +40,7 @@ import { plusCircle } from "@wordpress/icons"; import "./editor.scss"; const LISTITEM_TEMPLATE = [["core/paragraph"]]; + /** * The edit function describes the structure of your block in the context of the * editor. This represents what the editor will render when the block is used. @@ -42,28 +51,121 @@ const LISTITEM_TEMPLATE = [["core/paragraph"]]; */ export default function Edit(props) { const { clientId, name } = props; - const { insertBlock, selectBlock } = useDispatch("core/block-editor"); - const { parentBlock, parentClientId, parentBlockType, hasInnerBlocks } = useSelect((select) => { - const parentClientId = select(blockEditorStore).getBlockParentsByBlockName( - clientId, - "createwithrani/superlist-block", - )[0]; - const parentBlock = select(blockEditorStore).getBlock(parentClientId); - const parentBlockType = select(blocksStore).getBlockType( - parentBlock ? parentBlock.name : "", - ); - const { getBlock } = select(blockEditorStore); - const block = getBlock(clientId); - return { - parentClientId, - parentBlock, - parentBlockType, - hasInnerBlocks: !!(block && block.innerBlocks.length), - }; - }, [clientId, name]); - - // set up block properties and inner blocks settings - const blockProps = useBlockProps({}); + const { insertBlock, selectBlock, removeBlock, updateBlockAttributes } = + useDispatch(blockEditorStore); + + const { + parentBlock, + parentClientId, + parentBlockType, + parentItemWidth, + parentOrientation, + hasInnerBlocks, + innerBlocks, + selectedBlockClientId, + } = useSelect( + (select) => { + const store = select(blockEditorStore); + const blocks = select(blocksStore); + + const parents = store.getBlockParentsByBlockName( + clientId, + "createwithrani/superlist-block" + ); + const parentId = parents[0]; + const parent = parentId ? store.getBlock(parentId) : null; + const parentType = blocks.getBlockType( + parent ? parent.name : "" + ); + const block = store.getBlock(clientId); + + return { + parentClientId: parentId, + parentBlock: parent, + parentBlockType: parentType, + parentItemWidth: parent ? parent.attributes.itemWidth : undefined, + parentOrientation: parent ? parent.attributes.orientation : undefined, + hasInnerBlocks: !!(block && block.innerBlocks.length), + innerBlocks: block ? block.innerBlocks : [], + selectedBlockClientId: store.getSelectedBlockClientId(), + }; + }, + [clientId] + ); + + // Insert a new list item block after the current block. The toolbar "Add" + // button leaves the new item empty (matching core's list behavior); the + // double-enter shortcut prefills it with an empty paragraph so the cursor + // has somewhere to land for typing. + const insertListItem = useCallback( + ({ withParagraph = false } = {}) => { + if (!parentBlock || !parentClientId) { + return null; + } + const inner = withParagraph ? [createBlock("core/paragraph")] : []; + const newListItem = createBlock(name, {}, inner); + + const currentIndex = parentBlock.innerBlocks.findIndex( + (block) => block.clientId === clientId + ); + const insertIndex = + currentIndex === -1 + ? parentBlock.innerBlocks.length + : currentIndex + 1; + insertBlock(newListItem, insertIndex, parentClientId); + return newListItem.clientId; + }, + [parentBlock, parentClientId, clientId, name, insertBlock] + ); + + const blockProps = useBlockProps({ + // When the user presses Enter inside an empty trailing paragraph of + // this list-item, escape out and start a new list-item. The first + // Enter (in a non-empty paragraph) splits the paragraph as usual; the + // second Enter — now in the resulting empty paragraph — fires this. + onKeyDown: (event) => { + if ( + event.key !== "Enter" || + event.shiftKey || + event.metaKey || + event.ctrlKey || + event.isDefaultPrevented() + ) { + return; + } + if (!innerBlocks || innerBlocks.length === 0) { + return; + } + + const lastChild = innerBlocks[innerBlocks.length - 1]; + if ( + !lastChild || + lastChild.name !== "core/paragraph" || + lastChild.clientId !== selectedBlockClientId + ) { + return; + } + + const content = lastChild.attributes && lastChild.attributes.content; + const contentString = + content == null + ? "" + : typeof content === "string" + ? content + : typeof content.toString === "function" + ? content.toString() + : ""; + + if (contentString.replace(//gi, "").trim() !== "") { + return; + } + + event.preventDefault(); + removeBlock(lastChild.clientId, false); + insertListItem({ withParagraph: true }); + }, + }); + const innerBlockProps = useInnerBlocksProps(blockProps, { templateInsertUpdateSelection: true, renderAppender: hasInnerBlocks @@ -71,19 +173,14 @@ export default function Edit(props) { : InnerBlocks.ButtonBlockAppender, }); - // Insert a new list item block after the current block - const insertListItem = () => { - const newListItem = createBlock(name); - - // Get the index of the current block in the parent block's inner blocks - const currentIndex = parentBlock.innerBlocks.findIndex( - (block) => block.clientId === clientId, - ); - - // Insert the new list item block after the current block - const insertIndex = currentIndex === -1 ? parentBlock.innerBlocks.length : currentIndex + 1; - insertBlock(newListItem, insertIndex, parentClientId); - }; + const updateParentItemWidth = useCallback( + (value) => { + if (parentClientId) { + updateBlockAttributes(parentClientId, { itemWidth: value }); + } + }, + [parentClientId, updateBlockAttributes] + ); return ( <> @@ -104,12 +201,32 @@ export default function Edit(props) { insertListItem()} icon={plusCircle} label={__("Add another list item", "superlist-block")} /> + {parentOrientation === "horizontal" && ( + + + + + + + + )}
  • ); diff --git a/src/superlist-item/icons.js b/src/superlist-item/icons.js index 63ab622..2a28177 100644 --- a/src/superlist-item/icons.js +++ b/src/superlist-item/icons.js @@ -1,13 +1,20 @@ +/** + * Monochrome list-item icon: a single bold square + line, on transparent + * background, to match the core block icon style. + */ export const ListItem = ( + fillRule="evenodd" + clipRule="evenodd" + d="M3 11a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-2zm6.5.25a.75.75 0 0 1 .75-.75h9.5a.75.75 0 0 1 0 1.5h-9.5a.75.75 0 0 1-.75-.75z" + fill="currentColor" + /> ); diff --git a/src/superlist/block.json b/src/superlist/block.json index 9a6d345..07252b2 100644 --- a/src/superlist/block.json +++ b/src/superlist/block.json @@ -2,13 +2,13 @@ "$schema": "https://json.schemastore.org/block.json", "apiVersion": 3, "name": "createwithrani/superlist-block", - "version": "0.1.3", + "version": "0.2.0", "title": "Super List", "category": "design", "description": "Nest multiple blocks inside list items in any kind of list (ordered, unordered, no marker, etc)", "allowedBlocks": ["createwithrani/superlist-item"], "attributes": { - "listStyle":{ + "listStyle": { "type": "string", "default": "ul" }, @@ -20,6 +20,18 @@ }, "verticalAlignment": { "type": "string" + }, + "start": { + "type": "number" + }, + "reversed": { + "type": "boolean" + }, + "caption": { + "type": "string" + }, + "customMarker": { + "type": "string" } }, "supports": { @@ -32,6 +44,10 @@ "text": true, "link": true }, + "background": { + "backgroundImage": true, + "backgroundSize": true + }, "spacing": { "margin": true, "padding": true diff --git a/src/superlist/edit/edit.js b/src/superlist/edit/edit.js index 9e12e79..d5b7ccf 100644 --- a/src/superlist/edit/edit.js +++ b/src/superlist/edit/edit.js @@ -23,13 +23,17 @@ import { InspectorControls, useSettings, BlockVerticalAlignmentToolbar, + RichText, } from "@wordpress/block-editor"; import { PanelBody, PanelRow, + ToggleControl, + TextControl, + __experimentalNumberControl as NumberControl, __experimentalUnitControl as UnitControl, } from "@wordpress/components"; -import { useState } from "@wordpress/element"; +import { useState, useCallback } from "@wordpress/element"; /** * Internal Dependencies @@ -61,7 +65,16 @@ const LIST_TEMPLATE = [ * @return {WPElement} Element to render. */ export default function Edit({ attributes, setAttributes }) { - const { listStyle, orientation, itemWidth, verticalAlignment } = attributes; + const { + listStyle, + orientation, + itemWidth, + verticalAlignment, + start, + reversed, + caption, + customMarker, + } = attributes; // check if theme.json has set a preferred list orientation const [themeListOrientation] = useSettings( @@ -72,7 +85,7 @@ export default function Edit({ attributes, setAttributes }) { const defaultListOrientation = undefined === themeListOrientation ? "vertical" : themeListOrientation; - // set up a state variable for list orientation, use the orientation attribute if it is set, otherwise use the smart default + // set up a state variable for list orientation, use the orientation attribute if it is set, otherwise use the smart default const [listOrientation, setListOrientation] = useState( undefined !== orientation ? orientation : defaultListOrientation ); @@ -80,37 +93,71 @@ export default function Edit({ attributes, setAttributes }) { // set state for list item width from the attribute const [width, setWidth] = useState(itemWidth); - // set inline CSS Custom Property for list item width - const subItemWidth = { - "--wp--custom--superlist-block--list-settings--width": width, - }; + const isOrdered = listStyle === "ol"; + const showCaption = + typeof caption === "string" && caption.length > 0; - const blockProps = useBlockProps({ - // add class names to set list style, list orientation, and vertical alignment - className: classnames(listStyle, listOrientation, { - [`is-vertically-aligned-${verticalAlignment}`]: verticalAlignment, - }), - // add inline style if list orientation is horizontal and the user has set a custom list item width - style: "horizontal" === listOrientation ? subItemWidth : {}, + // Inline style applied to the list element (not the figure wrapper). + const listInlineStyle = {}; + if ("horizontal" === listOrientation) { + listInlineStyle["--wp--custom--superlist-block--list-settings--width"] = width; + } + if (customMarker) { + listInlineStyle["--superlist-marker"] = `"${customMarker}"`; + } + + const listClasses = classnames(listStyle, listOrientation, { + [`is-vertically-aligned-${verticalAlignment}`]: verticalAlignment, + "has-custom-marker": !!customMarker, }); - const innerBlockProps = useInnerBlocksProps(blockProps, { - // we only allow one kind of inner block, and we automatically populate this block with two inner blocks - allowedBlocks: ALLOWED_BLOCKS, - template: LIST_TEMPLATE, - // we also set the inner block orientation based on the list orientation, this affects the in-between appender position - orientation: `${listOrientation}`, - // we also set the focus to newly added inner block when it's added - templateInsertUpdateSelection: true, + // useBlockProps applies to the outermost element. In caption mode that's + // the
    ; otherwise it's the list itself. + const blockProps = useBlockProps({ + className: showCaption + ? "has-caption" + : listClasses, + style: showCaption ? undefined : listInlineStyle, }); + // useInnerBlocksProps wires the inner-blocks behavior to a wrapper element. + // In caption mode the list is a separate element (not the block wrapper); + // otherwise it IS the block wrapper, so we extend blockProps. + const innerBlockProps = useInnerBlocksProps( + showCaption + ? { + className: classnames( + "wp-block-createwithrani-superlist-block__list", + listClasses + ), + style: listInlineStyle, + } + : blockProps, + { + allowedBlocks: ALLOWED_BLOCKS, + template: LIST_TEMPLATE, + orientation: `${listOrientation}`, + templateInsertUpdateSelection: true, + } + ); + /** * onChange function to switch the list style * @param {string} style ul || ol || none */ - function switchStyle(style) { - setAttributes({ listStyle: style }); - } + const switchStyle = useCallback( + (style) => { + const next = { listStyle: style }; + // Reset ordered-list-only attributes when switching away from ol so + // they don't silently affect a future
      conversion. + if (style !== "ol") { + next.start = undefined; + next.reversed = undefined; + } + setAttributes(next); + }, + [setAttributes] + ); /** * onChange function to set the user inputted item width including unit @@ -123,26 +170,37 @@ export default function Edit({ attributes, setAttributes }) { /** * onChange function to set the vertical alignment attribute - * @param {string} verticalAlignment */ - function updateAlignment(verticalAlignment) { - setAttributes({ verticalAlignment: verticalAlignment }); + function updateAlignment(nextVerticalAlignment) { + setAttributes({ verticalAlignment: nextVerticalAlignment }); } /** * onChange function to update the list orientation - * @param {string} orientation horizontal || vertical + * @param {string} nextOrientation horizontal || vertical */ - function updateOrientation(orientation) { - setListOrientation(orientation); - setAttributes({ orientation: orientation }); + function updateOrientation(nextOrientation) { + setListOrientation(nextOrientation); + setAttributes({ orientation: nextOrientation }); } /** - * Set container tag name based on list style, if the list style is none, set it to `ol` + * Set container tag name based on list style. When markers are off we still + * use a
        so the structure stays semantic. */ const ListContainer = "none" !== listStyle ? listStyle : "ul"; + const containerExtraProps = isOrdered + ? { + ...(typeof start === "number" ? { start } : {}), + ...(reversed ? { reversed: true } : {}), + } + : {}; + + const listElement = ( + + ); + return ( <> @@ -166,28 +224,12 @@ export default function Edit({ attributes, setAttributes }) { initialOpen={true} title={__("List Settings", "superlist-block")} > - { - /** - * Only show list item setting if the list orientation is - * horizontal, because this setting is specifically for the basic - * grid mode this block offers. - */ - listOrientation === "horizontal" && ( - - - - ) - } -
        @@ -197,9 +239,93 @@ export default function Edit({ attributes, setAttributes }) { placement="inspector" /> + {listOrientation === "horizontal" && ( + + + + )} + {isOrdered && ( + <> + + { + if (value === "" || value === undefined) { + setAttributes({ start: undefined }); + return; + } + const parsed = parseInt(value, 10); + setAttributes({ + start: Number.isNaN(parsed) ? undefined : parsed, + }); + }} + /> + + + setAttributes({ reversed: value })} + /> + + + )} + {listStyle === "ul" && ( + + + setAttributes({ customMarker: value || undefined }) + } + /> + + )} + + + + with this caption as
        .", + "superlist-block" + )} + value={caption || ""} + onChange={(value) => + setAttributes({ caption: value || undefined }) + } + /> + - + {showCaption ? ( +
        + setAttributes({ caption: value })} + placeholder={__("Add a list title…", "superlist-block")} + aria-label={__("List title", "superlist-block")} + /> + {listElement} +
        + ) : ( + listElement + )} ); } diff --git a/src/superlist/edit/editor.scss b/src/superlist/edit/editor.scss index 150cead..844a0fd 100644 --- a/src/superlist/edit/editor.scss +++ b/src/superlist/edit/editor.scss @@ -1,10 +1,9 @@ /** * The following styles get applied inside the editor only. - * - * Replace them with your own styles or remove the file completely. */ -.editor-styles-wrapper .wp-block-createwithrani-superlist-block { +.editor-styles-wrapper .wp-block-createwithrani-superlist-block, +.editor-styles-wrapper .wp-block-createwithrani-superlist-block__list { &.horizontal { li { margin: 0; @@ -17,3 +16,10 @@ } } } + +.editor-styles-wrapper figure.wp-block-createwithrani-superlist-block.has-caption { + .wp-block-createwithrani-superlist-block__caption { + font-style: italic; + margin-bottom: 0.5em; + } +} diff --git a/src/superlist/edit/list-style.js b/src/superlist/edit/list-style.js index 27a5391..1c56c24 100644 --- a/src/superlist/edit/list-style.js +++ b/src/superlist/edit/list-style.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { find } from "lodash"; - /** * WordPress dependencies */ @@ -18,7 +13,7 @@ const DEFAULT_LIST_CONTROLS = [ }, { icon: formatListNumbered, - title: __("Ordered List", "superlist-block"), + title: __("Ordered list", "superlist-block"), listStyle: "ol", }, { @@ -37,7 +32,7 @@ function ListStyleUI({ value, onChange, listControls = DEFAULT_LIST_CONTROLS, - describedBy = __("Change list style"), + describedBy = __("Change list type", "superlist-block"), isCollapsed = true, placement, }) { @@ -45,8 +40,7 @@ function ListStyleUI({ return () => onChange(value === listStyle ? undefined : listStyle); } - const activeStyle = find( - listControls, + const activeStyle = listControls.find( (control) => control.listStyle === value ); @@ -57,7 +51,7 @@ function ListStyleUI({ return "toolbar" === placement ? ( { @@ -74,13 +68,13 @@ function ListStyleUI({ /> ) : (
        - {__(`${describedBy}`, "superlist-block")} + {describedBy}
        {listControls.map(({ icon, listStyle, title }) => { return (
        -
        + + ); + } + + return ( + updateOrientation(value)} + isBlock + __nextHasNoMarginBottom + > + + + ); }; diff --git a/src/superlist/icons.js b/src/superlist/icons.js index 6d1d970..b829f35 100644 --- a/src/superlist/icons.js +++ b/src/superlist/icons.js @@ -1,24 +1,21 @@ +/** + * A simpler, monochrome icon for the Super List block to feel cohesive + * with the core block library where most icons are a single black glyph + * on a transparent background. + */ export const SuperList = ( - - - - + ); diff --git a/src/superlist/index.js b/src/superlist/index.js index 2e2e05c..6334f00 100644 --- a/src/superlist/index.js +++ b/src/superlist/index.js @@ -39,5 +39,5 @@ registerBlockType("createwithrani/superlist-block", { edit, save, example, - // transforms, + transforms, }); diff --git a/src/superlist/save.js b/src/superlist/save.js index 161c158..9faae23 100644 --- a/src/superlist/save.js +++ b/src/superlist/save.js @@ -2,12 +2,6 @@ * External dependencies */ import classnames from "classnames"; -/** - * Retrieves the translation of text. - * - * @see https://developer.wordpress.org/block-editor/packages/packages-i18n/ - */ -import { __ } from "@wordpress/i18n"; /** * React hook that is used to mark the block wrapper element. @@ -15,7 +9,7 @@ import { __ } from "@wordpress/i18n"; * * @see https://developer.wordpress.org/block-editor/packages/packages-block-editor/#useBlockProps */ -import { useBlockProps, InnerBlocks } from "@wordpress/block-editor"; +import { useBlockProps, InnerBlocks, RichText } from "@wordpress/block-editor"; /** * The save function defines the way in which the different attributes should @@ -27,19 +21,71 @@ import { useBlockProps, InnerBlocks } from "@wordpress/block-editor"; * @return {WPElement} Element to render. */ export default function save({ attributes }) { - const { listStyle, orientation, itemWidth, verticalAlignment } = attributes; - const subItemWidth = { - gridTemplateColumns: `repeat(auto-fill, minmax(${itemWidth}, 1fr))`, - }; + const { + listStyle, + orientation, + itemWidth, + verticalAlignment, + start, + reversed, + caption, + customMarker, + } = attributes; + + const isOrdered = listStyle === "ol"; + const showCaption = typeof caption === "string" && caption.length > 0; + + const listInlineStyle = {}; + if ("horizontal" === orientation) { + listInlineStyle["--wp--custom--superlist-block--list-settings--width"] = itemWidth; + } + if (customMarker) { + listInlineStyle["--superlist-marker"] = `"${customMarker}"`; + } + + const listClasses = classnames(listStyle, orientation, { + [`is-vertically-aligned-${verticalAlignment}`]: verticalAlignment, + "has-custom-marker": !!customMarker, + }); + const ListContainer = "none" !== listStyle ? listStyle : "ul"; + + const containerExtraProps = isOrdered + ? { + ...(typeof start === "number" ? { start } : {}), + ...(reversed ? { reversed: true } : {}), + } + : {}; + + if (showCaption) { + return ( +
        + + + + +
        + ); + } + return ( diff --git a/src/superlist/style.scss b/src/superlist/style.scss index ac3f482..67e2020 100644 --- a/src/superlist/style.scss +++ b/src/superlist/style.scss @@ -1,10 +1,9 @@ /** * The following styles get applied both on the front of your site * and in the editor. - * - * Replace them with your own styles or remove the file completely. */ -.wp-block-createwithrani-superlist-block { +.wp-block-createwithrani-superlist-block, +.wp-block-createwithrani-superlist-block__list { --listItemWidth: var( --wp--custom--superlist-block--list-settings--width, 250px @@ -38,6 +37,9 @@ --margin: var(--wp--custom--superlist-block--spacing--margin, inherit); --padding: var(--wp--custom--superlist-block--spacing--padding, inherit); +} + +.wp-block-createwithrani-superlist-block { color: var(--textColor); background-color: var(--backgroundColor); font-size: var(--fontSize); @@ -50,32 +52,74 @@ a { color: var(--linkColor); } - &.horizontal { - display: grid; - gap: 2rem; - grid-template-rows: auto; - grid-template-columns: repeat(auto-fill, minmax(var(--listItemWidth), 1fr)); - } - &.none { - list-style: none; - li { - margin-top: 0; - margin-bottom: 0; - } +} + +// Layout/list-style rules apply both to the block itself (no-caption mode) +// and to the inner __list element (caption mode). +.wp-block-createwithrani-superlist-block.horizontal, +.wp-block-createwithrani-superlist-block__list.horizontal { + display: grid; + gap: 2rem; + grid-template-rows: auto; + grid-template-columns: repeat(auto-fill, minmax(var(--listItemWidth), 1fr)); +} + +.wp-block-createwithrani-superlist-block.none, +.wp-block-createwithrani-superlist-block__list.none { + list-style: none; + padding-inline-start: 0; + li { + margin-top: 0; + margin-bottom: 0; } - &.is-vertically-aligned-center { - align-items: center; +} + +.wp-block-createwithrani-superlist-block.is-vertically-aligned-center, +.wp-block-createwithrani-superlist-block__list.is-vertically-aligned-center { + align-items: center; +} +.wp-block-createwithrani-superlist-block.is-vertically-aligned-top, +.wp-block-createwithrani-superlist-block__list.is-vertically-aligned-top { + align-items: flex-start; +} +.wp-block-createwithrani-superlist-block.is-vertically-aligned-bottom, +.wp-block-createwithrani-superlist-block__list.is-vertically-aligned-bottom { + align-items: flex-end; +} + +// Custom marker support: render the user-supplied character via ::marker. +.wp-block-createwithrani-superlist-block.has-custom-marker, +.wp-block-createwithrani-superlist-block__list.has-custom-marker { + list-style: none; + padding-inline-start: 1.5em; + li { + position: relative; } - &.is-vertically-aligned-top { - align-items: flex-start; + li::before { + content: var(--superlist-marker, "•"); + position: absolute; + left: -1.25em; + top: 0; } - &.is-vertically-aligned-bottom { - align-items: flex-end; +} + +// Caption / figure wrapper. +figure.wp-block-createwithrani-superlist-block.has-caption { + display: block; + margin: var(--margin); + padding: var(--padding); + + .wp-block-createwithrani-superlist-block__caption { + margin-bottom: 0.5em; + font-style: italic; + text-align: inherit; } } -// allow indentation via padding to occur when a super list is nested inside a super list item, otherwise, remove the unnecessary padding-left. -:not(li) > ul.wp-block-createwithrani-superlist-block { +// allow indentation via padding to occur when a super list is nested inside a +// super list item, otherwise, remove the unnecessary padding-left. +:not(li) > ul.wp-block-createwithrani-superlist-block, +:not(li) > ol.wp-block-createwithrani-superlist-block { padding-inline-start: 0; margin-inline-start: 0; li { @@ -83,7 +127,9 @@ } } -ul.wp-block-createwithrani-superlist-block { +ul.wp-block-createwithrani-superlist-block, +ol.wp-block-createwithrani-superlist-block, +.wp-block-createwithrani-superlist-block__list { li { flex: 1; } diff --git a/src/superlist/transforms.js b/src/superlist/transforms.js index 136cc9b..5afdb4b 100644 --- a/src/superlist/transforms.js +++ b/src/superlist/transforms.js @@ -1,116 +1,192 @@ /** * Block Transforms. * + * Modern @wordpress/block-library/list stores list items as nested + * core/list-item blocks (and recursively nested core/list blocks for + * sub-lists). The previous transform read a now-removed `values` HTML + * string attribute and is no longer compatible. + * * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-transforms/ */ import { createBlock } from "@wordpress/blocks"; -// TODO: replace with LINE_SEPARATOR from @wordpress/rich-text when it is no longer unstable (__UNSTABLE_LINE_SEPARATOR). -const LINE_SEPARATOR = "\u2028"; - -const Transforms = { - from: [ - { - type: "block", - blocks: ["core/list"], - transform: ({ ordered, values, ...rest }) => { - // Parse list HTML string so we can natively traverse nested lists. - const listDOM = new DOMParser().parseFromString(values, "text/html"); - const innerBlocks = nodeToInnerBlocks(listDOM.body); // DOMParser creates an entire virtual document, the list elements are in `body`. +/** + * Convert a tree of core/list (with nested core/list-item / core/list) + * innerBlocks into the equivalent superlist-block / superlist-item tree. + * + * @param {Object[]} blocks Array of core/list-item innerBlocks. + * @return {Object[]} Array of superlist-item blocks. + */ +function listItemsToSuperItems(blocks) { + const result = []; + for (const block of blocks) { + if (!block || !block.name) continue; - return createBlock( - "createwithrani/superlist-block", - { - listStyle: ordered ? "ol" : "ul", - /** - * Apply the rest of the original list attributes to the - * super list (for typography settings, etc). - */ - ...rest, - }, - innerBlocks + if (block.name === "core/list-item") { + const inner = []; + const { content } = block.attributes || {}; + if (content && content !== "") { + inner.push( + createBlock("core/paragraph", { content }) ); - }, + } + // A list-item may itself contain a nested core/list (for sub-lists). + for (const child of block.innerBlocks || []) { + if (child.name === "core/list") { + inner.push(coreListToSuperList(child)); + } else { + // Anything else: keep as-is. + inner.push( + createBlock(child.name, child.attributes, child.innerBlocks) + ); + } + } + result.push( + createBlock("createwithrani/superlist-item", {}, inner) + ); + } else if (block.name === "core/list") { + // A loose core/list directly inside another list — preserve as a + // nested superlist by wrapping in a single superlist-item. + result.push( + createBlock( + "createwithrani/superlist-item", + {}, + [coreListToSuperList(block)] + ) + ); + } + } + return result; +} + +/** + * Convert a single core/list block (with attributes + innerBlocks) into a + * superlist-block. + * + * @param {Object} listBlock core/list block object. + * @return {Object} createwithrani/superlist-block block. + */ +function coreListToSuperList(listBlock) { + const { + ordered, + start, + reversed, + ...rest + } = listBlock.attributes || {}; + const innerSuperItems = listItemsToSuperItems(listBlock.innerBlocks || []); + return createBlock( + "createwithrani/superlist-block", + { + listStyle: ordered ? "ol" : "ul", + ...(typeof start === "number" ? { start } : {}), + ...(reversed ? { reversed: true } : {}), + // Preserve typography / color / etc. attributes from the source. + ...rest, }, - ], -}; + innerSuperItems + ); +} /** - * Recursively traverse child nodes and decide whether they can be stitched - * together into a single core/paragraph block or if a superlist-block or - * superlist-item with nested innerBlocks is output. + * Extract a content string from a superlist-item by concatenating the text + * content of any inner paragraph/heading blocks. Used by the to-core/list + * transform, which is necessarily lossy (it can't preserve arbitrary nested + * blocks inside a single list-item). * - * @param {Node} parentNode Parent node to traverse child nodes. - * @return {array} Array of InnerBlocks. + * @param {Object} item superlist-item block. + * @return {string} Plain HTML content for the corresponding core/list-item. */ -function nodeToInnerBlocks(parentNode) { - const nodes = parentNode.childNodes.values(); +function superItemToListItemContent(item) { + const parts = []; + for (const inner of item.innerBlocks || []) { + if (!inner || !inner.attributes) continue; + const { content } = inner.attributes; + if (content && content !== "") { + parts.push(typeof content === "string" ? content : content.toString()); + } + } + return parts.join("
        "); +} - const innerBlocks = []; - let stitching = []; +/** + * Recursively convert a superlist-block into a core/list block (with nested + * core/list-item and core/list blocks for sub-lists). + * + * @param {Object} superBlock createwithrani/superlist-block block. + * @return {Object} core/list block. + */ +function superListToCoreList(superBlock) { + // `orientation` and `itemWidth` are deliberately discarded — they have no + // equivalent on core/list. The rest pass through (color, typography, etc.). + const { + listStyle, + start, + reversed, + // eslint-disable-next-line no-unused-vars + orientation, + // eslint-disable-next-line no-unused-vars + itemWidth, + ...rest + } = superBlock.attributes || {}; - /** - * Combine the nodes in `stitching` as an HTML string, add to a paragraph - * block, and empty the array. - */ - const stitch = () => { - if (stitching.length) { - const content = stitching - .map((n) => (n.nodeName === "#text" ? n.nodeValue : n.outerHTML)) - .join(""); - // Create a paragraph block with the HTML string as content. - innerBlocks.push(createBlock("core/paragraph", { content })); - // Reset stitching. - stitching = []; - } - }; + const listItems = []; + for (const item of superBlock.innerBlocks || []) { + if (!item || item.name !== "createwithrani/superlist-item") continue; - // Walk through child nodes and take action based on whether they are a list, list item, or anything else. - for (const node of nodes) { - switch (node.nodeName) { - case "LI": - case "OL": - case "UL": - // If we've reached one of these elements, stitch together previous nodes in `stitching` and return a paragraph block. - stitch(); + // Promote any nested superlist-block inside this item into a nested + // core/list child of the corresponding core/list-item. + const nestedLists = (item.innerBlocks || []) + .filter((b) => b.name === "createwithrani/superlist-block") + .map((b) => superListToCoreList(b)); - // Create either a superlist-block or a superlist-item, and recurse to create their innerBlocks. - switch (node.nodeName) { - case "OL": - case "UL": - innerBlocks.push( - createBlock( - "createwithrani/superlist-block", - { - listStyle: node.nodeName === "OL" ? "ol" : "ul", - }, - nodeToInnerBlocks(node) - ) - ); - break; - case "LI": - innerBlocks.push( - createBlock( - "createwithrani/superlist-item", - {}, - nodeToInnerBlocks(node) - ) - ); - break; - } - break; - default: - // Add non-LI/OL/UL nodes to `stitching` to combine as a single paragraph block. - stitching.push(node); - break; - } + listItems.push( + createBlock( + "core/list-item", + { content: superItemToListItemContent(item) }, + nestedLists + ) + ); } - // Stitch together any lingering text nodes. - stitch(); - - return innerBlocks; + return createBlock( + "core/list", + { + ordered: listStyle === "ol", + ...(typeof start === "number" ? { start } : {}), + ...(reversed ? { reversed: true } : {}), + // Pass through generic attributes (typography, color, etc.) that + // both blocks support; orientation/itemWidth are dropped because + // core/list has no equivalent. + ...rest, + }, + listItems + ); } +const Transforms = { + from: [ + { + type: "block", + blocks: ["core/list"], + transform: (attributes, innerBlocks) => + coreListToSuperList({ + attributes, + innerBlocks: innerBlocks || [], + }), + }, + ], + to: [ + { + type: "block", + blocks: ["core/list"], + transform: (attributes, innerBlocks) => + superListToCoreList({ + attributes, + innerBlocks: innerBlocks || [], + }), + }, + ], +}; + export default Transforms; diff --git a/superlist-block.php b/superlist-block.php index b9e4b3a..c139a6d 100644 --- a/superlist-block.php +++ b/superlist-block.php @@ -4,7 +4,7 @@ * Description: Nest multiple blocks inside lists of any kind of list (ordered, unordered, no marker, etc), or do away with list markers and use it like a repeater! * Requires at least: 5.9 * Requires PHP: 7.0 - * Version: 0.1.4 + * Version: 0.2.0 * Author: Aurooba Ahmed * Author URI: https://aurooba.com * License: GPL-2.0-or-later