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";