Skip to content
Merged
38 changes: 38 additions & 0 deletions .github/workflows/phpcs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: PHPCS

on:
pull_request:
paths:
- '**.php'
- 'composer.json'
- 'phpcs.xml.dist'
- '.github/workflows/phpcs.yml'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
phpcs:
name: "Lint PHP ${{ matrix.php }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [ '8.2', '8.3', '8.4' ]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ matrix.php }}
tools: composer, cs2pr

- run: composer install --no-progress --prefer-dist

# Only the first (lowest) version annotates the PR, to avoid duplicate comments.
- if: strategy.job-index == 0
run: composer phpcs -- -q --report=checkstyle | cs2pr

- if: strategy.job-index != 0
run: composer phpcs -- --no-colors
15 changes: 15 additions & 0 deletions .wp-env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"port": 6368,
"phpVersion": "8.2",
"testsEnvironment": false,
"config": {
"SCRIPT_DEBUG": true,
"WP_DEBUG_DISPLAY": false,
"WP_DEBUG": true,
"WP_DEVELOPMENT_MODE": "plugin"
},
"plugins": [
".",
"https://downloads.wordpress.org/plugin/query-monitor.zip"
]
}
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ This guide explains how to create and customize a mega menu using the HM Mega Me

This plugin provides a flexible, code-light way to build and customize mega menus in WordPress!

## Local Environment

This project uses [wp-env](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/) to run a lightweight, containerized WordPress instance at [localhost:6368](http://localhost:6368) for testing purposes. The default username for the localhost environment is `admin`, with the password `password`. Run `npm run build` (or `npm start`) first so the block assets exist.

These commands can be used to interact with the environment:

Command | Purpose
---- | ----
`npm run env:start` | Start the local environment at http://localhost:6368
`npm run env:bootstrap` | Seed demo "Products" and "Resources" mega menus into the Twenty Twenty-Five header (safe to re-run)
`npm run env:stop` | Turn off the local environment
`npm run env:cli -- wp ...` | Run WP-CLI commands within the environment
`npm run env:logs` | Open (and tail) the error logs for the application<sup>&ddagger;</sup>
`npm run env:db` | Open the database in the mysql command line
`npm run env:destroy` | Fully destroy the local environment (deletes container database)

<sup>&ddagger;</sup> This command deliberately filters out GET/OPTIONS/HEAD/POST/PUT access log entries

## Release Process

Merges to `main` will automatically build to the `release` branch.
Expand Down
151 changes: 151 additions & 0 deletions bin/bootstrap-env.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php
/**
* Seeds the wp-env site with demo mega menus in the active theme's header.
*
* Usage: npm run env:bootstrap
*
* Safe to re-run: content is matched by slug and updated in place.
*
* @package hm-blocks
*/

if ( ! defined( 'WP_CLI' ) ) {
exit;
}

/**
* Create or update a post, matched by post type and slug.
*
* @param array $postarr Post data, including post_type and post_name.
* @return int Post ID.
*/
function hm_mega_menu_bootstrap_upsert_post( array $postarr ) : int {
$existing = get_posts( [
'post_type' => $postarr['post_type'],
'name' => $postarr['post_name'],
'post_status' => 'any',
'posts_per_page' => 1,
'fields' => 'ids',
] );

if ( $existing ) {
$postarr['ID'] = $existing[0];
}

$post_id = wp_insert_post( wp_slash( $postarr + [ 'post_status' => 'publish' ] ), true );

if ( is_wp_error( $post_id ) ) {
WP_CLI::error( $post_id );
}

return $post_id;
}

/**
* Build demo template part content: one column of links per heading.
*
* @param array $columns Map of column heading => list of link labels.
* @return string Block markup.
*/
function hm_mega_menu_bootstrap_menu_content( array $columns ) : string {
$markup = '';

foreach ( $columns as $heading => $links ) {
$items = '';
foreach ( $links as $link ) {
$items .= sprintf( '<!-- wp:list-item --><li><a href="#">%s</a></li><!-- /wp:list-item -->', esc_html( $link ) );
}

$markup .= sprintf(
'<!-- wp:column --><div class="wp-block-column"><!-- wp:heading {"level":3} --><h3 class="wp-block-heading">%s</h3><!-- /wp:heading --><!-- wp:list --><ul class="wp-block-list">%s</ul><!-- /wp:list --></div><!-- /wp:column -->',
esc_html( $heading ),
$items
);
}

return '<!-- wp:group {"style":{"spacing":{"padding":{"top":"var:preset|spacing|30","bottom":"var:preset|spacing|30","left":"var:preset|spacing|30","right":"var:preset|spacing|30"}}},"backgroundColor":"base","layout":{"type":"constrained"}} --><div class="wp-block-group has-base-background-color has-background" style="padding-top:var(--wp--preset--spacing--30);padding-right:var(--wp--preset--spacing--30);padding-bottom:var(--wp--preset--spacing--30);padding-left:var(--wp--preset--spacing--30)"><!-- wp:columns --><div class="wp-block-columns">'
. $markup
. '</div><!-- /wp:columns --></div><!-- /wp:group -->';
}

$theme = get_stylesheet();

$menus = [
'mega-menu-products' => [
'title' => 'Mega Menu: Products',
'block' => [
'label' => 'Products',
'width' => 'wide',
],
'columns' => [
'Hardware' => [ 'Laptops', 'Phones', 'Accessories' ],
'Software' => [ 'Editor', 'Hosting', 'Analytics' ],
'Services' => [ 'Support', 'Training' ],
],
],
'mega-menu-resources' => [
'title' => 'Mega Menu: Resources',
'block' => [
'label' => 'Resources',
'labelColor' => '#c0392b',
'width' => 'content',
'justifyMenu' => 'right',
],
'columns' => [
'Learn' => [ 'Documentation', 'Tutorials' ],
'Community' => [ 'Forums', 'Events' ],
],
],
];

$nav_items = '<!-- wp:navigation-link {"label":"Home","url":"/","kind":"custom"} /-->';

foreach ( $menus as $slug => $menu ) {
$part_id = hm_mega_menu_bootstrap_upsert_post( [
'post_type' => 'wp_template_part',
'post_name' => $slug,
'post_title' => $menu['title'],
'post_content' => hm_mega_menu_bootstrap_menu_content( $menu['columns'] ),
] );
wp_set_object_terms( $part_id, $theme, 'wp_theme' );
wp_set_object_terms( $part_id, 'menu', 'wp_template_part_area' );

$nav_items .= sprintf(
'<!-- wp:hm-blocks/hm-mega-menu-block %s /-->',
wp_json_encode( $menu['block'] + [ 'menuSlug' => $slug ] )
);

WP_CLI::log( sprintf( 'Template part "%s": %d', $slug, $part_id ) );
}

$nav_items .= '<!-- wp:navigation-link {"label":"Contact","url":"#contact","kind":"custom"} /-->';

$nav_id = hm_mega_menu_bootstrap_upsert_post( [
'post_type' => 'wp_navigation',
'post_name' => 'mega-menu-demo',
'post_title' => 'Mega Menu Demo',
'post_content' => $nav_items,
] );
WP_CLI::log( sprintf( 'Navigation "mega-menu-demo": %d', $nav_id ) );

// Override the theme header, pointing its Navigation block at the demo menu.
$header = WP_Block_Patterns_Registry::get_instance()->get_registered( $theme . '/header' );
if ( ! $header ) {
WP_CLI::error( sprintf( 'No "%s/header" pattern found; the bootstrap targets Twenty Twenty-Five.', $theme ) );
}

$header_content = preg_replace( '/<!-- wp:navigation \{/', '<!-- wp:navigation {"ref":' . $nav_id . ',', $header['content'], 1, $count );
if ( ! $count ) {
WP_CLI::error( 'No Navigation block found in the header pattern.' );
}

$header_id = hm_mega_menu_bootstrap_upsert_post( [
'post_type' => 'wp_template_part',
'post_name' => 'header',
'post_title' => 'Header',
'post_content' => $header_content,
] );
wp_set_object_terms( $header_id, $theme, 'wp_theme' );
wp_set_object_terms( $header_id, 'header', 'wp_template_part_area' );

WP_CLI::success( sprintf( 'Header %d now uses the mega menu demo navigation.', $header_id ) );
13 changes: 12 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@
"license": "GPL-2.0-or-later",

"require": {
"php": ">=7.4"
"php": ">=8.2"
},
"require-dev": {
"humanmade/coding-standards": "^2.5"
},
"scripts": {
"phpcs": "phpcs"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"autoload": {
"psr-4": {
Expand Down
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build --experimental-modules",
"env:bootstrap": "npx @wordpress/env run cli -- wp eval-file wp-content/plugins/hm-mega-menu-block/bin/bootstrap-env.php",
"env:cli": "npx @wordpress/env run cli",
"env:db": "npx @wordpress/env run cli -- mysql -uroot -ppassword wordpress --host=mysql",
"env:destroy": "npx @wordpress/env destroy",
"env:logs": "npx @wordpress/env logs development | grep -v -E '\"(GET|OPTIONS|HEAD|POST|PUT)'",
"env:start": "npx @wordpress/env start",
"env:stop": "npx @wordpress/env stop",
"format": "wp-scripts format",
"lint:css": "wp-scripts lint-style",
"lint:js": "wp-scripts lint-js",
"lint:php": "composer phpcs",
"test": "wp-scripts test-unit-js",
"packages-update": "wp-scripts packages-update",
"plugin-zip": "wp-scripts plugin-zip",
Expand Down
24 changes: 24 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0"?>
<ruleset name="HM Mega Menu Block">
<description>Human Made coding standards for Mega Menu Block plugin.</description>

<!-- What to scan -->
<file>.</file>

<!-- Exclude paths -->
<exclude-pattern>*/node_modules/*</exclude-pattern>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/build/*</exclude-pattern>

<!-- How to scan -->
<arg value="sp"/> <!-- Show sniff and progress -->
<arg name="colors"/>
<arg name="extensions" value="php"/>
<arg name="parallel" value="8"/>

<!-- Rules: Check PHP version compatibility -->
<config name="testVersion" value="8.2-"/>

<!-- Rules: Use HM Coding Standards -->
<rule ref="HM-Minimum"/>
</ruleset>
37 changes: 20 additions & 17 deletions src/render.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
<?php
$label = esc_html($attributes['label'] ?? '');
$color_label = esc_attr($attributes['labelColor'] ?? '#000');
$menu_slug = esc_attr($attributes['menuSlug'] ?? '');
$justify_menu = esc_attr($attributes['justifyMenu'] ?? '');
$menu_width = esc_attr($attributes['width'] ?? 'content');
$label = $attributes['label'] ?? '';
$label_color = $attributes['labelColor'] ?? '';
$menu_slug = $attributes['menuSlug'] ?? '';
$justify_menu = $attributes['justifyMenu'] ?? '';
$menu_width = $attributes['width'] ?? 'content';

// Accept a single color value (hex, name, rgb(), var(), etc.); anything else,
// such as ";" or an entity, could inject further declarations into the style.
if ( ! preg_match( '/^[#a-zA-Z0-9(),.%\/\s-]+$/', $label_color ) ) {
$label_color = '#000';
}

$menu_classes = 'hm-mega-menu wp-block-hm-mega-menu__menu-container';
$menu_classes .= ' menu-width-' . $menu_width;
Expand All @@ -21,24 +27,21 @@
},
]
);

$wrapper_attributes = get_block_wrapper_attributes();

$close_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path></svg>';
$toggle_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false" fill="none"><path d="M1.50002 4L6.00002 8L10.5 4" stroke-width="1.5"></path></svg>';
?>

<li <?php echo $wrapper_attributes; ?> data-wp-interactive='{"namespace": "hm-blocks/hm-mega-menu-block" }' data-wp-context='{ "menuOpenedBy": {} }' data-wp-on-document--keydown="actions.handleMenuKeydown" data-wp-on-document--click="actions.handleOutsideClick" data-wp-on-document--focusin="actions.handleOutsideFocus" data-wp-watch="callbacks.initMenu">
<li <?php echo get_block_wrapper_attributes(); ?> data-wp-interactive='{"namespace": "hm-blocks/hm-mega-menu-block" }' data-wp-context='{ "menuOpenedBy": {} }' data-wp-on-document--keydown="actions.handleMenuKeydown" data-wp-on-document--click="actions.handleOutsideClick" data-wp-on-document--focusin="actions.handleOutsideFocus" data-wp-watch="callbacks.initMenu">

<button aria-controls="<?php echo esc_attr( $menu_id ); ?>" aria-expanded="false" class="wp-block-hm-mega-menu__toggle" data-wp-on--click="actions.toggleMenuOnClick" data-wp-bind--aria-expanded="state.isMenuOpen" style="color:<?php echo $color_label; ?>">
<?php echo $label; ?><span class="wp-block-hm-mega-menu__toggle-icon"><?php echo $toggle_icon; ?></span>
<button aria-controls="<?php echo esc_attr( $menu_id ); ?>" aria-expanded="false" class="wp-block-hm-mega-menu__toggle" data-wp-on--click="actions.toggleMenuOnClick" data-wp-bind--aria-expanded="state.isMenuOpen" style="color:<?php echo esc_attr( $label_color ); ?>">
<?php echo esc_html( $label ); ?><span class="wp-block-hm-mega-menu__toggle-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false" fill="none"><path d="M1.50002 4L6.00002 8L10.5 4" stroke-width="1.5"></path></svg>
</span>
</button>

<div class="<?php echo $menu_classes; ?>" id="<?php echo esc_attr( $menu_id ); ?>">
<?php echo block_template_part($menu_slug); ?>
<div class="<?php echo esc_attr( $menu_classes ); ?>" id="<?php echo esc_attr( $menu_id ); ?>">
<?php block_template_part( $menu_slug ); ?>

<button aria-label="<?php echo __('Close menu', 'mega-menu'); ?>" class="menu-container__close-button" data-wp-on--click="actions.closeMenuOnClick" type="button">
<?php echo $close_icon; ?>
<button aria-label="<?php esc_attr_e( 'Close menu', 'hm-mega-menu-block' ); ?>" class="menu-container__close-button" data-wp-on--click="actions.closeMenuOnClick" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path></svg>
</button>
</div>

Expand Down
Loading