From d23184dfa5461a8322d2d3abcf86cf065cc99ee8 Mon Sep 17 00:00:00 2001 From: Osong Agberndifor Date: Wed, 8 Jan 2025 13:51:44 +0100 Subject: [PATCH] PLANET-6530 Add new Secondary Navigation Block - Added all necessary files for the new block - Added tests for the new block --- .../SecondaryNavigationBlock.js | 39 ++++++++++ .../SecondaryNavigationEditor.js | 66 +++++++++++++++++ .../SecondaryNavigationFrontend.js | 32 ++++++++ .../src/blocks/SecondaryNavigation/example.js | 27 +++++++ .../generateHeadingsForBlock.js | 30 ++++++++ .../TableOfContents/getHeadingsFromDom.js | 4 +- assets/src/editorIndex.js | 2 + assets/src/frontendIndex.js | 4 +- assets/src/scss/blocks.scss | 1 + .../SecondaryNavigationStyle.scss | 38 ++++++++++ src/BlockSettings.php | 2 + src/Blocks/SecondaryNavigation.php | 73 +++++++++++++++++++ src/Loader.php | 1 + 13 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 assets/src/blocks/SecondaryNavigation/SecondaryNavigationBlock.js create mode 100644 assets/src/blocks/SecondaryNavigation/SecondaryNavigationEditor.js create mode 100644 assets/src/blocks/SecondaryNavigation/SecondaryNavigationFrontend.js create mode 100644 assets/src/blocks/SecondaryNavigation/example.js create mode 100644 assets/src/blocks/SecondaryNavigation/generateHeadingsForBlock.js create mode 100644 assets/src/scss/blocks/SecondaryNavigation/SecondaryNavigationStyle.scss create mode 100644 src/Blocks/SecondaryNavigation.php diff --git a/assets/src/blocks/SecondaryNavigation/SecondaryNavigationBlock.js b/assets/src/blocks/SecondaryNavigation/SecondaryNavigationBlock.js new file mode 100644 index 0000000000..e5e189a4aa --- /dev/null +++ b/assets/src/blocks/SecondaryNavigation/SecondaryNavigationBlock.js @@ -0,0 +1,39 @@ +import {SecondaryNavigationEditor} from './SecondaryNavigationEditor'; +import {example} from './example'; + +const {__} = wp.i18n; + +const BLOCK_NAME = 'planet4-blocks/secondary-navigation'; + +export const registerSecondaryNavigationBlock = () => { + const {registerBlockType} = wp.blocks; + + registerBlockType(BLOCK_NAME, { + title: 'Secondary Navigation Menu', + description: __('Inserts a secondary navigation menu to the page that leads to different sections of the same page.', 'planet4-blocks-backend'), + icon: 'welcome-widgets-menus', + category: 'planet4-blocks', + attributes: { + levels: { + type: 'array', + default: [{heading: 2, link: true}], + }, + exampleMenuItems: { // Used for the block's preview, which can't extract items from anything. + type: 'array', + }, + }, + isExample: { + type: 'boolean', + default: false, + }, + supports: { + multiple: false, // Use the block just once per post. + html: false, + }, + edit: SecondaryNavigationEditor, + save() { + return null; + }, + example, + }); +}; diff --git a/assets/src/blocks/SecondaryNavigation/SecondaryNavigationEditor.js b/assets/src/blocks/SecondaryNavigation/SecondaryNavigationEditor.js new file mode 100644 index 0000000000..39b9248665 --- /dev/null +++ b/assets/src/blocks/SecondaryNavigation/SecondaryNavigationEditor.js @@ -0,0 +1,66 @@ +import {getHeadingsFromBlocks} from './generateHeadingsForBlock'; + +const {useSelect} = wp.data; +const {InspectorControls} = wp.blockEditor; +const {PanelBody} = wp.components; +const {__} = wp.i18n; + +const renderEdit = () => { + return ( + + +

+ + P4 Handbook - P4 Secondary Navigation Menu + + {' '} 📋 +

+
+
+ ); +}; + +const renderView = attributes => { + const { + levels, + isExample, + exampleMenuItems, + } = attributes; + + const blocks = useSelect(select => select('core/block-editor').getBlocks(), []); + + const flatHeadings = getHeadingsFromBlocks(blocks, levels); + + const menuItems = isExample ? exampleMenuItems : flatHeadings; + + return ( +
+ {menuItems.length > 0 ? +
+ +
: +
+ {__('There are not any pre-established headings that this block can display in the form of a secondary navigation menu. Please add headings to your page.', 'planet4-blocks-backend')} +
+ } +
+ ); +}; + +export const SecondaryNavigationEditor = ({attributes, isSelected}) => ( + <> + {isSelected && renderEdit()} + {renderView(attributes)} + +); diff --git a/assets/src/blocks/SecondaryNavigation/SecondaryNavigationFrontend.js b/assets/src/blocks/SecondaryNavigation/SecondaryNavigationFrontend.js new file mode 100644 index 0000000000..26d399ab83 --- /dev/null +++ b/assets/src/blocks/SecondaryNavigation/SecondaryNavigationFrontend.js @@ -0,0 +1,32 @@ +import {getHeadingsFromDom} from '../TableOfContents/getHeadingsFromDom'; + +export const SecondaryNavigationFrontend = ({levels}) => { + const headings = getHeadingsFromDom(levels); + const setActive = event => { + const allLinks = document.querySelectorAll('.secondary-navigation-link'); + allLinks.forEach(link => link.classList.remove('active')); + event.target.classList.add('active'); + }; + + return ( +
+
+ +
+
+ ); +}; diff --git a/assets/src/blocks/SecondaryNavigation/example.js b/assets/src/blocks/SecondaryNavigation/example.js new file mode 100644 index 0000000000..5d4fde92f0 --- /dev/null +++ b/assets/src/blocks/SecondaryNavigation/example.js @@ -0,0 +1,27 @@ +export const example = { + viewportWidth: 992, + attributes: { + isExample: true, + exampleMenuItems: [ + { + content: 'Title 1', + anchor: 'title-1', + level: 2, + shouldLink: true, + }, + { + content: 'Title 2', + anchor: 'title-2', + level: 2, + shouldLink: true, + }, + { + content: 'Title 3', + anchor: 'title-3', + level: 2, + shouldLink: true, + }, + ], + }, +}; + \ No newline at end of file diff --git a/assets/src/blocks/SecondaryNavigation/generateHeadingsForBlock.js b/assets/src/blocks/SecondaryNavigation/generateHeadingsForBlock.js new file mode 100644 index 0000000000..b9de962652 --- /dev/null +++ b/assets/src/blocks/SecondaryNavigation/generateHeadingsForBlock.js @@ -0,0 +1,30 @@ +import {generateAnchor} from '../TableOfContents/generateAnchor'; +import {unescape} from '../../functions/unescape'; + +const stripTags = str => str.replace(/(<([^>]+)>)/ig, ''); //NOSONAR + +export const getHeadingsFromBlocks = (blocks, selectedLevels) => { + const headings = []; + blocks.forEach(block => { + if (block.name === 'core/heading') { + const blockLevel = block.attributes.level; + + const levelConfig = selectedLevels.find(selected => selected.heading === blockLevel); + + if (!levelConfig) { + return; + } + + const anchor = block.attributes.anchor || generateAnchor(block.attributes.content, headings.map(h => h.anchor)); + + headings.push({ + level: blockLevel, + content: unescape(stripTags(block.attributes.content)), + anchor, + shouldLink: levelConfig.link, + }); + } + }); + + return headings; +}; \ No newline at end of file diff --git a/assets/src/blocks/TableOfContents/getHeadingsFromDom.js b/assets/src/blocks/TableOfContents/getHeadingsFromDom.js index cbe5783d09..1f7369fac8 100644 --- a/assets/src/blocks/TableOfContents/getHeadingsFromDom.js +++ b/assets/src/blocks/TableOfContents/getHeadingsFromDom.js @@ -9,7 +9,9 @@ export const getHeadingsFromDom = selectedLevels => { } // Get all heading tags that we need to query - const headingsSelector = selectedLevels.map(level => `:not(.table-of-contents-block) > h${level.heading}`); + const headingsSelector = selectedLevels.map( + level => `:not(.table-of-contents-block):not(.secondary-navigation-block) > h${level.heading}` + ); const usedAnchors = []; diff --git a/assets/src/editorIndex.js b/assets/src/editorIndex.js index 728bc7a7c7..22ae54d1d5 100644 --- a/assets/src/editorIndex.js +++ b/assets/src/editorIndex.js @@ -12,6 +12,7 @@ import {registerColumnsBlock} from './blocks/Columns/ColumnsBlock'; import {registerBlockStyles} from './block-styles'; import {registerBlockVariations} from './block-variations'; import {registerActionButtonTextBlock} from './blocks/ActionCustomButtonText'; +import {registerSecondaryNavigationBlock} from './blocks/SecondaryNavigation/SecondaryNavigationBlock'; wp.domReady(() => { // Blocks @@ -21,6 +22,7 @@ wp.domReady(() => { registerHappyPointBlock(); registerSocialMediaBlock(); registerTimelineBlock(); + registerSecondaryNavigationBlock(); // Block Templates registerBlockTemplates(); diff --git a/assets/src/frontendIndex.js b/assets/src/frontendIndex.js index 00610e2987..df7eb7b641 100644 --- a/assets/src/frontendIndex.js +++ b/assets/src/frontendIndex.js @@ -8,12 +8,14 @@ import {TableOfContentsFrontend} from './blocks/TableOfContents/TableOfContentsF import {HappyPointFrontend} from './blocks/HappyPoint/HappyPointFrontend'; import {ColumnsFrontend} from './blocks/Columns/ColumnsFrontend'; import {setupLightboxForImages} from './blocks/components/Lightbox/setupLightboxForImages'; +import {SecondaryNavigationFrontend} from './blocks/SecondaryNavigation/SecondaryNavigationFrontend'; // Render React components const COMPONENTS = { 'planet4-blocks/submenu': TableOfContentsFrontend, 'planet4-blocks/happypoint': HappyPointFrontend, 'planet4-blocks/columns': ColumnsFrontend, + 'planet4-blocks/secondary-navigation': SecondaryNavigationFrontend, }; document.addEventListener('DOMContentLoaded', () => { @@ -35,4 +37,4 @@ document.addEventListener('DOMContentLoaded', () => { ); setupLightboxForImages(); -}); +}); \ No newline at end of file diff --git a/assets/src/scss/blocks.scss b/assets/src/scss/blocks.scss index 404dd70073..0c919dc8c6 100644 --- a/assets/src/scss/blocks.scss +++ b/assets/src/scss/blocks.scss @@ -1,6 +1,7 @@ // Beta blocks @import "blocks/ActionsList/ActionsListStyle"; @import "blocks/PostsList/PostsListStyle"; +@import "blocks/SecondaryNavigation/SecondaryNavigationStyle"; // P4 Blocks @import "blocks/Accordion/AccordionStyle"; diff --git a/assets/src/scss/blocks/SecondaryNavigation/SecondaryNavigationStyle.scss b/assets/src/scss/blocks/SecondaryNavigation/SecondaryNavigationStyle.scss new file mode 100644 index 0000000000..f94ca1ec8b --- /dev/null +++ b/assets/src/scss/blocks/SecondaryNavigation/SecondaryNavigationStyle.scss @@ -0,0 +1,38 @@ +.secondary-navigation-block { + margin-left: calc(-50vw - -50%); + width: 100vw; + height: 55px; + padding: 10px; + background: var(--color-background-navigation_bar); + border-bottom: 0 transparent; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); +} + +.secondary-navigation-item { + list-style: none; + display: flex; + justify-content: space-around; + + li { + padding: 5px; + + &:has(a.active) { + border-bottom: 4px solid var(--grey-900); + + a { + color: var(--grey-900) !important; + } + } + } +} + +.secondary-navigation-link { + color: var(--grey-600) !important; + font-weight: var(--font-weight-bold); + font-family: var(--font-family-primary); + + &:hover { + text-decoration: none; + color: var(--grey-900) !important + } +} \ No newline at end of file diff --git a/src/BlockSettings.php b/src/BlockSettings.php index 95f6d8cbba..cd3512a767 100644 --- a/src/BlockSettings.php +++ b/src/BlockSettings.php @@ -54,6 +54,7 @@ class BlockSettings self::P4_BLOCKS_PREFIX . '/take-action-boxout', self::P4_BLOCKS_PREFIX . '/timeline', self::P4_BLOCKS_PREFIX . '/guestbook', + self::P4_BLOCKS_PREFIX . '/secondary-navigation', self::HUBSPOT_FORMS_BLOCK, self::GRAVITY_FORMS_BLOCK, ]; @@ -92,6 +93,7 @@ class BlockSettings self::P4_BLOCKS_PREFIX . '/take-action-boxout', self::P4_BLOCKS_PREFIX . '/timeline', self::P4_BLOCKS_PREFIX . '/guestbook', + self::P4_BLOCKS_PREFIX . '/secondary-navigation', self::HUBSPOT_FORMS_BLOCK, self::GRAVITY_FORMS_BLOCK, ]; diff --git a/src/Blocks/SecondaryNavigation.php b/src/Blocks/SecondaryNavigation.php new file mode 100644 index 0000000000..d95e78ee45 --- /dev/null +++ b/src/Blocks/SecondaryNavigation.php @@ -0,0 +1,73 @@ +register_secondary_navigation_block(); + } + + /** + * Register SecondaryNavigation block. + */ + public function register_secondary_navigation_block(): void + { + register_block_type( + self::get_full_block_name(), + [ + 'render_callback' => [ self::class, 'render_frontend' ], + 'attributes' => [ + 'levels' => [ + 'type' => 'array', + 'default' => [ + [ + 'heading' => 2, + 'link' => true, + ], + ], + ], + ], + ] + ); + + add_action('enqueue_block_editor_assets', [ self::class, 'enqueue_editor_assets' ]); + add_action('wp_enqueue_scripts', [ self::class, 'enqueue_frontend_assets' ]); + } + + /** + * Required by the `Base_Block` class. + * + * @param array $fields Unused, required by the abstract function. + * @phpcs:disable SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter + */ + public function prepare_data(array $fields): array + { + return []; + } + // @phpcs:enable SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter +} diff --git a/src/Loader.php b/src/Loader.php index f2e5130ff5..002f0ec2ff 100644 --- a/src/Loader.php +++ b/src/Loader.php @@ -203,6 +203,7 @@ public static function add_blocks(): void new Blocks\TableOfContents();//NOSONAR new Blocks\TakeActionBoxout();//NOSONAR new Blocks\Timeline();//NOSONAR + new Blocks\SecondaryNavigation();//NOSONAR register_block_pattern_category( 'page-headers',