diff --git a/includes/BlockStyles.php b/includes/BlockStyles.php index 8ea1d3c0..40bd38df 100644 --- a/includes/BlockStyles.php +++ b/includes/BlockStyles.php @@ -84,6 +84,21 @@ public function register_block_styles() { ), ); + $heading_styles = array( + array( + 'name' => 'nfd-heading-boxed', + 'label' => __('Boxed', 'nfd-wonder-blocks'), + ), + array( + 'name' => 'nfd-heading-highlight', + 'label' => __('Highlight', 'nfd-wonder-blocks'), + ), + array( + 'name' => 'nfd-heading-underline', + 'label' => __('Underline', 'nfd-wonder-blocks'), + ) + ); + foreach ( $image_styles as $image_style ) { register_block_style( array( 'core/group', 'core/image' ), @@ -97,5 +112,12 @@ public function register_block_styles() { $theme_style ); } + + foreach ( $heading_styles as $heading_style ) { + register_block_style( + 'core/heading', + $heading_style + ); + } } } diff --git a/src/blocks/heading.js b/src/blocks/heading.js new file mode 100644 index 00000000..8aae1ca9 --- /dev/null +++ b/src/blocks/heading.js @@ -0,0 +1,229 @@ +import { addFilter } from '@wordpress/hooks'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; +import { + create as createRT, + toHTMLString, + applyFormat, + registerFormatType +} from '@wordpress/rich-text'; +import {createHigherOrderComponent} from "@wordpress/compose"; +import { InspectorControls } from '@wordpress/block-editor'; +import { + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, + BorderControl, + PanelBody +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +const STYLE_CLASS = 'is-style-nfd-heading-highlight'; +const FORMAT_TYPE = 'nfd/heading-highlight'; +const FORMAT_CLASS = 'nfd-heading-highlight__text'; +import TitleWithLogo from "../components/TitleWithLogo"; + +const resolveHeadingColor = (slug, custom) => + slug ? `var(--wp--preset--color--${slug})` : (custom || undefined); + +const withHeadingHighlightSync = ( BlockEdit ) => ( props ) => { + if ( props.name !== 'core/heading' ) return ; + + const { attributes } = props; + const { updateBlockAttributes } = useDispatch('core/block-editor'); + + const { className, content } = useSelect( ( select ) => { + const attrs = select('core/block-editor').getBlockAttributes( props.clientId ) || {}; + return { className: attrs.className || '', content: attrs.content || '' }; + }, [ props.clientId ] ); + + const styled = className.includes( STYLE_CLASS ); + + useEffect( () => { + const html = content || ''; + const parser = new DOMParser(); + const doc = parser.parseFromString( html, 'text/html' ); + let changed = false; + + doc.querySelectorAll('span.nfd-heading-highlight__text').forEach( (node) => { + const parent = node.parentNode; + while ( node.firstChild ) parent.insertBefore( node.firstChild, node ); + parent.removeChild( node ); + changed = true; + } ); + + let normalizedHTML = doc.body.innerHTML || ''; + + let value = createRT( { html: normalizedHTML } ); + + const styled = (className || '').includes( STYLE_CLASS ); + if ( styled && value.text.length > 0 ) { + value = applyFormat( value, { type: FORMAT_TYPE, attributes: { class: FORMAT_CLASS } }, 0, value.text.length ); + } + + const nextHTML = toHTMLString( { value } ); + + if ( nextHTML !== content ) { + updateBlockAttributes( props.clientId, { content: nextHTML } ); + } + }, [ className, content ]); + + return ; +}; + +addFilter( 'editor.BlockEdit', 'nfd/heading-highlight/sync', withHeadingHighlightSync ); + + +registerFormatType( FORMAT_TYPE, { + title: 'Heading Highlight', + tagName: 'span', + className: FORMAT_CLASS, + edit: () => null +} ); + + +const listHeadingBlock = createHigherOrderComponent((BlockListBlock) => { + return (props) => { + if (props.name !== 'core/heading') return ; + const attr = props.attributes; + const styleVars = { + ...(props.wrapperProps?.style || {}), + '--nfd-heading-border': resolveHeadingColor(attr.nfdHeadingBorderColor, attr.nfdHeadingBorderColor), + }; + + return ( + + ); + }; +}, 'nfd-list-block'); + +addFilter('editor.BlockListBlock', 'nfd-wonder-blocks/utilities/listBlock', listHeadingBlock); + + +export const applyHeadingStylesInPlace = (props, blockType, atts) => { + if ( !blockType || blockType.name !== 'core/heading' ) return; + + const color = atts?.nfdHeadingBorderColor || ''; + const width = atts?.nfdHeadingBorderWidth || ''; + const style = atts?.nfdHeadingBorderStyle || ''; + + const resolveColor = ( value ) => { + if ( typeof value !== 'string' || !value ) return undefined; + const isLiteral = + value.startsWith('#') || value.startsWith('rgb') || value.startsWith('hsl'); + return isLiteral ? value : `var(--wp--preset--color--${ value })`; + }; + + const resolveWidth = ( value ) => { + if ( typeof value !== 'string' || !value ) return undefined; + if ( /(px|em|rem|vh|vw|%)$/i.test(value.trim()) ) return value.trim(); + if ( /^-?\d+(\.\d+)?$/.test(value.trim()) ) return `${value.trim()}px`; + return undefined; + }; + + const resolveStyle = ( value ) => { + const allowed = new Set([ 'solid', 'dashed', 'dotted', 'double', 'groove', 'ridge', 'inset', 'outset', 'none', 'hidden' ]); + if ( typeof value !== 'string' || !value ) return undefined; + const v = value.trim().toLowerCase(); + return allowed.has(v) ? v : undefined; + }; + + const borderColor = resolveColor(color); + const borderWidth = resolveWidth(width); + const borderStyle = resolveStyle(style) || 'solid'; + + const nextStyle = { ...(props.style || {}) }; + + if ( borderColor ) { + nextStyle['--nfd-heading-border'] = borderColor; + } + if ( borderWidth ) { + nextStyle['--nfd-heading-border-size'] = borderWidth; + } + if ( borderStyle ) { + nextStyle['--nfd-heading-border-style'] = borderStyle; + } + + if ( + !nextStyle['--nfd-heading-border'] && + !nextStyle['--nfd-heading-border-size'] && + !nextStyle['--nfd-heading-border-style'] + ) return; + + props.style = nextStyle; +} + + +const HeadingExtras = ( props ) => { + const { attributes, setAttributes, clientId } = props; + const { nfdHeadingBorderWidth, nfdHeadingBorderColor, nfdHeadingBorderStyle } = attributes; + + const borderValue = { + width: nfdHeadingBorderWidth || '1px', + color: nfdHeadingBorderColor || undefined, + style: nfdHeadingBorderStyle || 'solid', + }; + + const hasBorderValue = Boolean( + nfdHeadingBorderWidth || nfdHeadingBorderColor || nfdHeadingBorderStyle + ); + + const onBorderChange = ( border ) => { + setAttributes({ + nfdHeadingBorderWidth: border?.width || '1px', + nfdHeadingBorderColor: border?.color || '', + nfdHeadingBorderStyle: border?.style || 'solid', + }); + }; + + const resetBorder = () => { + setAttributes({ + nfdHeadingBorderWidth: undefined, + nfdHeadingBorderColor: undefined, + nfdHeadingBorderStyle: undefined, + }); + }; + + return ( + + } + initialOpen={ true } + > + + hasBorderValue } + onDeselect={ resetBorder } + resetAllFilter={ () => ({ + nfdHeadingBorderWidth: undefined, + nfdHeadingBorderColor: undefined, + nfdHeadingBorderStyle: undefined, + }) } + isShownByDefault + > + + + + + + + ); +}; + +export default HeadingExtras; diff --git a/src/blocks/inspector-control.js b/src/blocks/inspector-control.js index 0d35750e..d2f1e9d6 100644 --- a/src/blocks/inspector-control.js +++ b/src/blocks/inspector-control.js @@ -1,4 +1,6 @@ -import { InspectorControls } from "@wordpress/block-editor"; +import { + InspectorControls +} from "@wordpress/block-editor"; import { Button, Notice, @@ -16,6 +18,7 @@ import { __ } from "@wordpress/i18n"; import classnames from "classnames"; import TitleWithLogo from "../components/TitleWithLogo"; +import HeadingExtras, {applyHeadingStylesInPlace} from "./heading"; // These block types do not support custom attributes. const skipBlockTypes = [ @@ -46,6 +49,21 @@ function addAttributes(settings, name) { }; } + if (name === "core/heading") { + settings.attributes = { + ...settings.attributes, + nfdHeadingBorderColor: { + type: 'string' + }, + nfdHeadingBorderWidth: { + type: 'string' + }, + nfdHeadingBorderStyle: { + type: 'string' + } + }; + } + return { ...settings, attributes: { @@ -89,6 +107,8 @@ const withInspectorControls = createHigherOrderComponent((BlockEdit) => { } }; + const allClassNames = props?.attributes?.className || ""; + const selectedGroupDivider = props?.attributes?.nfdGroupDivider ?? "default"; const selectedGroupTheme = props?.attributes?.nfdGroupTheme ?? ""; const selectedGroupEffect = props?.attributes?.nfdGroupEffect ?? ""; @@ -312,6 +332,11 @@ const withInspectorControls = createHigherOrderComponent((BlockEdit) => { [] ); + const isNfdHeadingStyle = + allClassNames.includes('is-style-nfd-heading-boxed') || + allClassNames.includes('is-style-nfd-heading-highlight') || + allClassNames.includes('is-style-nfd-heading-underline'); + return ( <> @@ -491,6 +516,10 @@ const withInspectorControls = createHigherOrderComponent((BlockEdit) => { )} + + {name === "core/heading" && isNfdHeadingStyle && ( + + )} ); }; @@ -530,9 +559,13 @@ function addSaveProps(saveElementProps, blockType, attributes) { ...normalizeAsArray(classes), ]); - return Object.assign({}, saveElementProps, { - className: [...classesCombined].join(" "), + const nextProps = Object.assign({}, saveElementProps, { + className: [...classesCombined].join(' ').trim(), }); + + applyHeadingStylesInPlace(nextProps, blockType, attributes); + + return nextProps; } addFilter("blocks.registerBlockType", "nfd-wonder-blocks/utilities/attributes", addAttributes); diff --git a/src/wonder-blocks.js b/src/wonder-blocks.js index ea3c2ba6..956574a3 100644 --- a/src/wonder-blocks.js +++ b/src/wonder-blocks.js @@ -30,6 +30,7 @@ import { import "./blocks/block"; import "./blocks/inspector-control"; +import "./blocks/heading"; import "./blocks/register-category"; import Modal from "./components/Modal/Modal"; import ToolbarButton from "./components/ToolbarButton";