Instantly share code, notes, and snippets.
Created
July 1, 2026 09:29
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save gbyat/d9dbe28c1cae4cb9038b49b3bfa24000 to your computer and use it in GitHub Desktop.
WordPress Navigation Breakpoint for Menublocks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ( function ( wp ) { | |
| if ( | |
| ! wp || | |
| ! wp.hooks || | |
| ! wp.compose || | |
| ! wp.element || | |
| ! wp.components || | |
| ! wp.i18n | |
| ) { | |
| return; | |
| } | |
| var addFilter = wp.hooks.addFilter; | |
| var createHigherOrderComponent = wp.compose.createHigherOrderComponent; | |
| var createElement = wp.element.createElement; | |
| var Fragment = wp.element.Fragment; | |
| var editorApi = wp.blockEditor || wp.editor; | |
| if ( ! editorApi || ! editorApi.InspectorControls ) { | |
| return; | |
| } | |
| var InspectorControls = editorApi.InspectorControls; | |
| var PanelBody = wp.components.PanelBody; | |
| var TextControl = wp.components.TextControl; | |
| var SelectControl = wp.components.SelectControl; | |
| var NumberControl = wp.components.__experimentalNumberControl; | |
| var __ = wp.i18n.__; | |
| var ATTRIBUTE_NAME = 'webentwicklerinBreakpoint'; | |
| var ALIGN_ATTRIBUTE_NAME = 'webentwicklerinMobileIconAlign'; | |
| var DEFAULT_BREAKPOINT = 600; | |
| var DEFAULT_ALIGNMENT = 'inherit'; | |
| var EDITOR_PREVIEW_STYLE_ID = 'webentwicklerin-navigation-alignment-preview'; | |
| function getOverlayMenuMode( attributes ) { | |
| if ( ! attributes || typeof attributes.overlayMenu === 'undefined' ) { | |
| return 'mobile'; | |
| } | |
| return attributes.overlayMenu; | |
| } | |
| function sanitizeValue( value ) { | |
| var parsed = parseInt( value, 10 ); | |
| if ( Number.isNaN( parsed ) ) { | |
| return DEFAULT_BREAKPOINT; | |
| } | |
| if ( parsed < 320 ) { | |
| return 320; | |
| } | |
| if ( parsed > 1920 ) { | |
| return 1920; | |
| } | |
| return parsed; | |
| } | |
| function addNavigationBreakpointAttribute( settings, blockName ) { | |
| if ( blockName !== 'core/navigation' ) { | |
| return settings; | |
| } | |
| settings.attributes = Object.assign( {}, settings.attributes, { | |
| webentwicklerinBreakpoint: { | |
| type: 'number', | |
| default: DEFAULT_BREAKPOINT, | |
| }, | |
| webentwicklerinMobileIconAlign: { | |
| type: 'string', | |
| default: DEFAULT_ALIGNMENT, | |
| }, | |
| } ); | |
| return settings; | |
| } | |
| function sanitizeAlignment( value ) { | |
| if ( value === 'inherit' || value === 'left' || value === 'center' || value === 'right' ) { | |
| return value; | |
| } | |
| return DEFAULT_ALIGNMENT; | |
| } | |
| function ensureEditorPreviewStyles() { | |
| if ( ! document || document.getElementById( EDITOR_PREVIEW_STYLE_ID ) ) { | |
| return; | |
| } | |
| var style = document.createElement( 'style' ); | |
| style.id = EDITOR_PREVIEW_STYLE_ID; | |
| style.textContent = | |
| '.block-editor-block-list__block.webentwicklerin-mobile-icon-align-left nav.wp-block-navigation,' + | |
| '.block-editor-block-list__block.webentwicklerin-mobile-icon-align-center nav.wp-block-navigation,' + | |
| '.block-editor-block-list__block.webentwicklerin-mobile-icon-align-right nav.wp-block-navigation{width:100%!important;}' + | |
| '.block-editor-block-list__block.webentwicklerin-mobile-icon-align-left nav.wp-block-navigation .wp-block-navigation__responsive-container-open{margin-left:0!important;margin-right:auto!important;}' + | |
| '.block-editor-block-list__block.webentwicklerin-mobile-icon-align-center nav.wp-block-navigation .wp-block-navigation__responsive-container-open{margin-left:auto!important;margin-right:auto!important;}' + | |
| '.block-editor-block-list__block.webentwicklerin-mobile-icon-align-right nav.wp-block-navigation .wp-block-navigation__responsive-container-open{margin-left:auto!important;margin-right:0!important;}'; | |
| document.head.appendChild( style ); | |
| } | |
| function createBreakpointControl( value, onChange ) { | |
| var label = __( 'Responsive navigation breakpoint (px)', 'we-navigation-breakpoint' ); | |
| var help = __( 'Defines when the menu switches to the mobile toggle.', 'we-navigation-breakpoint' ); | |
| if ( NumberControl ) { | |
| return createElement( NumberControl, { | |
| label: label, | |
| help: help, | |
| value: value, | |
| min: 320, | |
| max: 1920, | |
| step: 1, | |
| onChange: onChange, | |
| } ); | |
| } | |
| return createElement( TextControl, { | |
| label: label, | |
| help: help, | |
| type: 'number', | |
| value: value, | |
| min: 320, | |
| max: 1920, | |
| step: 1, | |
| onChange: onChange, | |
| } ); | |
| } | |
| function createAlignmentControl( value, onChange ) { | |
| return createElement( SelectControl, { | |
| label: __( 'Mobile menu icon alignment', 'we-navigation-breakpoint' ), | |
| help: __( 'Aligns the visible mobile menu icon independently from the menu alignment.', 'we-navigation-breakpoint' ), | |
| value: value, | |
| options: [ | |
| { | |
| label: __( 'Inherit (native behavior)', 'we-navigation-breakpoint' ), | |
| value: 'inherit', | |
| }, | |
| { | |
| label: __( 'Left', 'we-navigation-breakpoint' ), | |
| value: 'left', | |
| }, | |
| { | |
| label: __( 'Center', 'we-navigation-breakpoint' ), | |
| value: 'center', | |
| }, | |
| { | |
| label: __( 'Right', 'we-navigation-breakpoint' ), | |
| value: 'right', | |
| }, | |
| ], | |
| onChange: onChange, | |
| } ); | |
| } | |
| var withNavigationBreakpointControl = createHigherOrderComponent( | |
| function ( BlockEdit ) { | |
| return function ( props ) { | |
| if ( props.name !== 'core/navigation' ) { | |
| return createElement( BlockEdit, props ); | |
| } | |
| var currentValue = props.attributes[ ATTRIBUTE_NAME ]; | |
| var safeValue = Number.isFinite( currentValue ) ? currentValue : DEFAULT_BREAKPOINT; | |
| var currentAlignment = props.attributes[ ALIGN_ATTRIBUTE_NAME ]; | |
| var safeAlignment = sanitizeAlignment( currentAlignment ); | |
| var overlayMode = getOverlayMenuMode( props.attributes ); | |
| var hasMobileOverlay = overlayMode === 'mobile'; | |
| var hasToggleIcon = overlayMode === 'mobile' || overlayMode === 'always'; | |
| function onChangeBreakpoint( nextValue ) { | |
| props.setAttributes( { | |
| webentwicklerinBreakpoint: sanitizeValue( nextValue ), | |
| } ); | |
| } | |
| function onChangeAlignment( nextValue ) { | |
| props.setAttributes( { | |
| webentwicklerinMobileIconAlign: sanitizeAlignment( nextValue ), | |
| } ); | |
| } | |
| return createElement( | |
| Fragment, | |
| null, | |
| createElement( BlockEdit, props ), | |
| createElement( | |
| InspectorControls, | |
| null, | |
| createElement( | |
| PanelBody, | |
| { | |
| title: __( 'Responsive Settings', 'we-navigation-breakpoint' ), | |
| initialOpen: true, | |
| }, | |
| hasMobileOverlay | |
| ? createBreakpointControl( safeValue, onChangeBreakpoint ) | |
| : null, | |
| hasToggleIcon | |
| ? createAlignmentControl( safeAlignment, onChangeAlignment ) | |
| : createElement( 'p', null, __( 'Available when Overlay menu is set to Mobile or Always.', 'we-navigation-breakpoint' ) ) | |
| ) | |
| ) | |
| ); | |
| }; | |
| }, | |
| 'withNavigationBreakpointControl' | |
| ); | |
| var withNavigationAlignmentPreview = createHigherOrderComponent( | |
| function ( BlockListBlock ) { | |
| return function ( props ) { | |
| if ( props.name !== 'core/navigation' ) { | |
| return createElement( BlockListBlock, props ); | |
| } | |
| ensureEditorPreviewStyles(); | |
| var attributes = props.attributes || {}; | |
| var overlayMode = getOverlayMenuMode( attributes ); | |
| var hasToggleIcon = overlayMode === 'mobile' || overlayMode === 'always'; | |
| var alignment = sanitizeAlignment( attributes[ ALIGN_ATTRIBUTE_NAME ] ); | |
| var alignmentClass = hasToggleIcon && alignment !== 'inherit' | |
| ? 'webentwicklerin-mobile-icon-align-' + alignment | |
| : ''; | |
| var className = props.className ? props.className + ' ' + alignmentClass : alignmentClass; | |
| return createElement( | |
| BlockListBlock, | |
| Object.assign( {}, props, { | |
| className: className, | |
| } ) | |
| ); | |
| }; | |
| }, | |
| 'withNavigationAlignmentPreview' | |
| ); | |
| addFilter( | |
| 'blocks.registerBlockType', | |
| 'we-navigation-breakpoint/navigation-breakpoint-attribute', | |
| addNavigationBreakpointAttribute | |
| ); | |
| addFilter( | |
| 'editor.BlockEdit', | |
| 'we-navigation-breakpoint/navigation-breakpoint-control', | |
| withNavigationBreakpointControl | |
| ); | |
| addFilter( | |
| 'editor.BlockListBlock', | |
| 'we-navigation-breakpoint/navigation-alignment-preview', | |
| withNavigationAlignmentPreview | |
| ); | |
| } )( window.wp ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Plugin Name: WE Navigation Breakpoint | |
| * Description: Adds a configurable responsive breakpoint and mobile icon alignment for the Navigation block. | |
| * Version: 1.0.1 | |
| * Requires at least: 6.5 | |
| * Requires PHP: 7.4 | |
| * Author: webentwicklerin, Gabriele Laesser | |
| * Author URI: https://webentwicklerin.at | |
| * License: GPL-2.0-or-later | |
| * License URI: https://www.gnu.org/licenses/gpl-2.0.html | |
| * Text Domain: we-navigation-breakpoint | |
| * | |
| * @package Webentwicklerin\NavigationBreakpoint | |
| */ | |
| namespace Webentwicklerin\NavigationBreakpoint; | |
| if ( ! defined( 'ABSPATH' ) ) { | |
| exit; | |
| } | |
| const VERSION = '1.0.1'; | |
| const SCRIPT_HANDLE = 'we-navigation-breakpoint-control'; | |
| /** | |
| * Registers server-side attributes for the Navigation block. | |
| * | |
| * @return void | |
| */ | |
| function register_navigation_attributes() { | |
| if ( ! class_exists( '\WP_Block_Type_Registry' ) ) { | |
| return; | |
| } | |
| $registry_class = '\WP_Block_Type_Registry'; | |
| $navigation_block = $registry_class::get_instance()->get_registered( 'core/navigation' ); | |
| if ( ! $navigation_block ) { | |
| return; | |
| } | |
| $navigation_block->attributes['webentwicklerinBreakpoint'] = array( | |
| 'type' => 'number', | |
| 'default' => 600, | |
| ); | |
| $navigation_block->attributes['webentwicklerinMobileIconAlign'] = array( | |
| 'type' => 'string', | |
| 'default' => 'inherit', | |
| ); | |
| } | |
| /** | |
| * Enqueues block editor assets for Navigation controls. | |
| * | |
| * @return void | |
| */ | |
| function enqueue_block_editor_assets() { | |
| $script_path = plugin_dir_path( __FILE__ ) . 'assets/js/navigation-breakpoint-control.js'; | |
| $script_url = plugin_dir_url( __FILE__ ) . 'assets/js/navigation-breakpoint-control.js'; | |
| if ( ! file_exists( $script_path ) ) { | |
| return; | |
| } | |
| wp_enqueue_script( | |
| SCRIPT_HANDLE, | |
| $script_url, | |
| array( 'wp-blocks', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-element', 'wp-hooks', 'wp-i18n' ), | |
| (string) filemtime( $script_path ), | |
| true | |
| ); | |
| wp_set_script_translations( | |
| SCRIPT_HANDLE, | |
| 'we-navigation-breakpoint', | |
| plugin_dir_path( __FILE__ ) . 'languages' | |
| ); | |
| } | |
| /** | |
| * Enqueues editor assets for editors that do not trigger enqueue_block_editor_assets. | |
| * | |
| * @return void | |
| */ | |
| function enqueue_block_assets_fallback() { | |
| if ( ! is_admin() ) { | |
| return; | |
| } | |
| enqueue_block_editor_assets(); | |
| } | |
| /** | |
| * Returns a safe breakpoint value. | |
| * | |
| * @param mixed $value Breakpoint value. | |
| * @return int | |
| */ | |
| function sanitize_breakpoint( $value ) { | |
| $breakpoint = absint( $value ); | |
| if ( $breakpoint < 320 ) { | |
| return 320; | |
| } | |
| if ( $breakpoint > 1920 ) { | |
| return 1920; | |
| } | |
| return $breakpoint; | |
| } | |
| /** | |
| * Returns a safe mobile icon alignment value. | |
| * | |
| * @param mixed $value Alignment value. | |
| * @return string | |
| */ | |
| function sanitize_mobile_icon_alignment( $value ) { | |
| $allowed_values = array( 'inherit', 'left', 'center', 'right' ); | |
| if ( is_string( $value ) && in_array( $value, $allowed_values, true ) ) { | |
| return $value; | |
| } | |
| return 'inherit'; | |
| } | |
| /** | |
| * Returns CSS declarations for mobile icon alignment. | |
| * | |
| * @param string $alignment Alignment value. | |
| * @return string | |
| */ | |
| function get_mobile_icon_alignment_css( $alignment ) { | |
| if ( 'left' === $alignment ) { | |
| return 'margin-left:0!important;margin-right:auto!important;'; | |
| } | |
| if ( 'center' === $alignment ) { | |
| return 'margin-left:auto!important;margin-right:auto!important;'; | |
| } | |
| if ( 'right' !== $alignment ) { | |
| return ''; | |
| } | |
| return 'margin-left:auto!important;margin-right:0!important;'; | |
| } | |
| /** | |
| * Returns the overlay menu mode used by the Navigation block. | |
| * | |
| * @param array $attributes Block attributes. | |
| * @return string | |
| */ | |
| function get_overlay_menu_mode( $attributes ) { | |
| if ( empty( $attributes['overlayMenu'] ) ) { | |
| // Core defaults to mobile overlay when the setting is not explicitly saved. | |
| return 'mobile'; | |
| } | |
| return (string) $attributes['overlayMenu']; | |
| } | |
| /** | |
| * Adds custom responsive breakpoint and icon alignment CSS to Navigation block output. | |
| * | |
| * @param string $block_content Rendered block HTML. | |
| * @param array $block Parsed block data. | |
| * @return string | |
| */ | |
| function render_navigation_with_breakpoint( $block_content, $block ) { | |
| if ( empty( $block['blockName'] ) || 'core/navigation' !== $block['blockName'] ) { | |
| return $block_content; | |
| } | |
| $attributes = isset( $block['attrs'] ) ? $block['attrs'] : array(); | |
| $has_custom_breakpoint = isset( $attributes['webentwicklerinBreakpoint'] ); | |
| $has_custom_alignment = isset( $attributes['webentwicklerinMobileIconAlign'] ); | |
| $alignment = sanitize_mobile_icon_alignment( | |
| $has_custom_alignment ? $attributes['webentwicklerinMobileIconAlign'] : 'inherit' | |
| ); | |
| $has_effective_alignment = $has_custom_alignment && 'inherit' !== $alignment; | |
| $overlay_mode = get_overlay_menu_mode( $attributes ); | |
| $has_mobile_overlay = 'mobile' === $overlay_mode; | |
| $has_toggle_icon = $has_mobile_overlay || 'always' === $overlay_mode; | |
| $breakpoint = sanitize_breakpoint( | |
| $has_custom_breakpoint ? $attributes['webentwicklerinBreakpoint'] : 600 | |
| ); | |
| // Keep core behavior untouched when no custom value is saved. | |
| if ( ( ! $has_mobile_overlay && ! $has_toggle_icon ) || ( ! $has_custom_breakpoint && ! $has_effective_alignment ) ) { | |
| return $block_content; | |
| } | |
| if ( ! class_exists( '\WP_HTML_Tag_Processor' ) ) { | |
| return $block_content; | |
| } | |
| $processor_class = '\WP_HTML_Tag_Processor'; | |
| $processor = new $processor_class( $block_content ); | |
| if ( ! $processor->next_tag( 'nav' ) ) { | |
| return $block_content; | |
| } | |
| $nav_id = $processor->get_attribute( 'id' ); | |
| if ( empty( $nav_id ) ) { | |
| $nav_id = 'nav-' . wp_unique_id(); | |
| $processor->set_attribute( 'id', $nav_id ); | |
| } | |
| $updated_content = $processor->get_updated_html(); | |
| $escaped_nav_id = esc_attr( $nav_id ); | |
| $css_rules = ''; | |
| if ( $has_mobile_overlay && $has_custom_breakpoint ) { | |
| $max_breakpoint = max( 0, $breakpoint - 1 ); | |
| $css_rules .= sprintf( | |
| '@media (max-width:%1$dpx){#%2$s .wp-block-navigation__responsive-container-open:not(.always-shown){display:flex!important;}#%2$s .wp-block-navigation__responsive-container:not(.is-menu-open):not(.hidden-by-default){display:none!important;}}@media (min-width:%3$dpx){#%2$s .wp-block-navigation__responsive-container-open:not(.always-shown){display:none!important;}#%2$s .wp-block-navigation__responsive-container:not(.is-menu-open):not(.hidden-by-default){display:block!important;}}', | |
| $max_breakpoint, | |
| $escaped_nav_id, | |
| $breakpoint | |
| ); | |
| } | |
| if ( $has_toggle_icon && $has_effective_alignment ) { | |
| $button_css = get_mobile_icon_alignment_css( $alignment ); | |
| $alignment_rule = sprintf( | |
| '#%1$s{width:100%%!important;}#%1$s .wp-block-navigation__responsive-container-open{%2$s}', | |
| $escaped_nav_id, | |
| $button_css | |
| ); | |
| if ( $has_mobile_overlay ) { | |
| $max_breakpoint = max( 0, $breakpoint - 1 ); | |
| $css_rules .= sprintf( | |
| '@media (max-width:%1$dpx){%2$s}', | |
| $max_breakpoint, | |
| $alignment_rule | |
| ); | |
| } else { | |
| $css_rules .= $alignment_rule; | |
| } | |
| } | |
| return '<style>' . $css_rules . '</style>' . $updated_content; | |
| } | |
| add_action( 'init', __NAMESPACE__ . '\register_navigation_attributes', 100 ); | |
| add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\enqueue_block_editor_assets' ); | |
| add_action( 'enqueue_block_assets', __NAMESPACE__ . '\enqueue_block_assets_fallback', 20 ); | |
| add_filter( 'render_block', __NAMESPACE__ . '\render_navigation_with_breakpoint', 10, 2 ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment