| @@ -83,8 +83,11 @@ | ||
| 83 | 83 | // PreviewRest::maybe_enable_preview), could never show a newly enabled |
| 84 | 84 | // menu switch until saved. The filter runs at render time, after `init`, |
| 85 | 85 | // so re-reading the option in the callback sees the preview values. |
| 86 | 86 | add_filter('wp_nav_menu_items', array($this, 'darkify_switch_in_menu'), 10, 2); |
| 87 | + // Block-theme and Kadence navigations never fire `wp_nav_menu_items` — | |
| 88 | + // they are blocks, not wp_nav_menu() calls — so they need their own hook. | |
| 89 | + add_filter('render_block', array($this, 'darkify_switch_in_block_menu'), 10, 2); | |
| 87 | 90 | |
| 88 | 91 | add_action('wp_enqueue_scripts', array($this, 'darkify_client_enqueue'), 100); |
| 89 | 92 | add_action('login_enqueue_scripts', array($this, 'darkify_client_enqueue')); |
| 90 | 93 | add_action('register_enqueue_scripts', array($this, 'darkify_client_enqueue')); |
| @@ -511,6 +514,147 @@ | ||
| 511 | 514 | } |
| 512 | 515 | } |
| 513 | 516 | |
| 514 | 517 | return $items; |
| 518 | + } | |
| 519 | + | |
| 520 | + /** | |
| 521 | + * Put the switch inside a block-theme or Kadence navigation. | |
| 522 | + * | |
| 523 | + * `wp_nav_menu_items` only fires for classic wp_nav_menu() output, so on a | |
| 524 | + * block theme (and on any site using Kadence Navigation) the Menu Switch | |
| 525 | + * feature had nothing to hook: the menu rendered, the setting was saved, and | |
| 526 | + * no switch ever appeared. These navigations are blocks, so this is the | |
| 527 | + * equivalent hook for them. | |
| 528 | + * | |
| 529 | + * The selection is matched on the namespaced `<post_type>:<id>` key written | |
| 530 | + * by the picker (see AbstractRestController::darkify_block_menu_sources()), | |
| 531 | + * which is what keeps block and Kadence menu POST ids from being confused | |
| 532 | + * with classic menu TERM ids. | |
| 533 | + * | |
| 534 | + * @param string $block_content Rendered HTML for this block. | |
| 535 | + * @param array $block Parsed block, including its attributes. | |
| 536 | + * @return string | |
| 537 | + */ | |
| 538 | + function darkify_switch_in_block_menu($block_content, $block) | |
| 539 | + { | |
| 540 | + // render_block fires for every block on the page, so reject as early and | |
| 541 | + // as cheaply as possible: two string comparisons for the overwhelming | |
| 542 | + // majority of blocks, and nothing else. | |
| 543 | + if (empty($block['blockName'])) { | |
| 544 | + return $block_content; | |
| 545 | + } | |
| 546 | + | |
| 547 | + if ($block['blockName'] === 'core/navigation') { | |
| 548 | + $source = 'wp_navigation'; | |
| 549 | + $ref = isset($block['attrs']['ref']) ? (int) $block['attrs']['ref'] : 0; | |
| 550 | + } elseif ($block['blockName'] === 'kadence/navigation') { | |
| 551 | + $source = 'kadence_navigation'; | |
| 552 | + $ref = isset($block['attrs']['id']) ? (int) $block['attrs']['id'] : 0; | |
| 553 | + } else { | |
| 554 | + return $block_content; | |
| 555 | + } | |
| 556 | + | |
| 557 | + // A navigation block with no saved menu behind it (unsaved, or rendering | |
| 558 | + // its fallback) has nothing this setting could have been pointed at. | |
| 559 | + if ($ref <= 0) { | |
| 560 | + return $block_content; | |
| 561 | + } | |
| 562 | + | |
| 563 | + $options = get_option('darkify'); | |
| 564 | + $show_in_menu = isset($options['show_in_menu']) ? $options['show_in_menu'] : ''; | |
| 565 | + | |
| 566 | + // Gated here rather than at filter registration for the same reason as | |
| 567 | + // the classic path: the live preview toggles this without saving. | |
| 568 | + if (empty($show_in_menu['enable_switch_in_menu'])) { | |
| 569 | + return $block_content; | |
| 570 | + } | |
| 571 | + | |
| 572 | + if (! $this->darkify_is_dark_mode_allowed()) { | |
| 573 | + return $block_content; | |
| 574 | + } | |
| 575 | + | |
| 576 | + $select_menus = isset($show_in_menu['select_menus']) ? $show_in_menu['select_menus'] : []; | |
| 577 | + $needle = $source . ':' . $ref; | |
| 578 | + $shortcode = ''; | |
| 579 | + $position = 'after'; | |
| 580 | + | |
| 581 | + foreach ((array) $select_menus as $select_menu) { | |
| 582 | + if (($select_menu['switch_in_menu_location'] ?? '') !== $needle) { | |
| 583 | + continue; | |
| 584 | + } | |
| 585 | + // Last match wins, matching the classic path's behaviour. Unlike | |
| 586 | + // that path the position is read from the row that actually | |
| 587 | + // matched, so it cannot be taken from an unrelated row. | |
| 588 | + $shortcode = $select_menu['darkify_menu_shortcode_helper'] ?? ''; | |
| 589 | + $position = $select_menu['select_menu_position'] ?? 'after'; | |
| 590 | + } | |
| 591 | + | |
| 592 | + if (! $shortcode) { | |
| 593 | + return $block_content; | |
| 594 | + } | |
| 595 | + | |
| 596 | + $item = '<li class="menu-item">' . do_shortcode($shortcode) . '</li>'; | |
| 597 | + | |
| 598 | + $injected = $this->darkify_inject_menu_item($block_content, $item, $position); | |
| 599 | + | |
| 600 | + // Unrecognised markup is left exactly as the block rendered it: a | |
| 601 | + // navigation with no switch is a far better outcome than a broken one. | |
| 602 | + return ($injected === null) ? $block_content : $injected; | |
| 603 | + } | |
| 604 | + | |
| 605 | + /** | |
| 606 | + * A `<ul>` or `</ul>` tag, with capture 1 set to "/" on a closing tag. | |
| 607 | + * | |
| 608 | + * The alternation skips over quoted attribute values rather than using a | |
| 609 | + * plain `[^>]*`: an attribute may legitimately contain a `>` (`data-x="a>b"`), | |
| 610 | + * and a bare `[^>]*` ends the match in the middle of that value. The open | |
| 611 | + * tag would then appear to end mid-attribute, so a "before" insertion landed | |
| 612 | + * inside the attribute and broke the markup, and a `>` inside a submenu's | |
| 613 | + * own `<ul>` attributes miscounted the nesting depth below. | |
| 614 | + */ | |
| 615 | + const DARKIFY_UL_TAG = '/<(\/?)ul\b(?:[^>"\']|"[^"]*"|\'[^\']*\')*>/i'; | |
| 616 | + | |
| 617 | + /** | |
| 618 | + * Insert a list item into the first top-level `<ul>` of a rendered block. | |
| 619 | + * | |
| 620 | + * The closing tag is found by balancing `<ul>`/`</ul>` rather than by | |
| 621 | + * searching for the last `</ul>`: navigations nest submenus, so the last one | |
| 622 | + * closes a submenu and the switch would land inside a dropdown. | |
| 623 | + * | |
| 624 | + * @return string|null Null when the markup has no usable list. | |
| 625 | + */ | |
| 626 | + private function darkify_inject_menu_item($html, $item, $position) | |
| 627 | + { | |
| 628 | + if (! preg_match(self::DARKIFY_UL_TAG, $html, $open, PREG_OFFSET_CAPTURE)) { | |
| 629 | + return null; | |
| 630 | + } | |
| 631 | + | |
| 632 | + // A fragment that opens on a closing tag has no list to add to. | |
| 633 | + if ($open[1][0] === '/') { | |
| 634 | + return null; | |
| 635 | + } | |
| 636 | + | |
| 637 | + $after_open = $open[0][1] + strlen($open[0][0]); | |
| 638 | + | |
| 639 | + if ($position === 'before') { | |
| 640 | + return substr($html, 0, $after_open) . $item . substr($html, $after_open); | |
| 641 | + } | |
| 642 | + | |
| 643 | + $depth = 1; | |
| 644 | + $offset = $after_open; | |
| 645 | + $tag = null; | |
| 646 | + | |
| 647 | + while ($depth > 0 && preg_match(self::DARKIFY_UL_TAG, $html, $tag, PREG_OFFSET_CAPTURE, $offset)) { | |
| 648 | + $depth += ($tag[1][0] === '/') ? -1 : 1; | |
| 649 | + $offset = $tag[0][1] + strlen($tag[0][0]); | |
| 650 | + } | |
| 651 | + | |
| 652 | + if ($depth !== 0 || $tag === null) { | |
| 653 | + return null; | |
| 654 | + } | |
| 655 | + | |
| 656 | + $before_close = $offset - strlen($tag[0][0]); | |
| 657 | + | |
| 658 | + return substr($html, 0, $before_close) . $item . substr($html, $before_close); | |
| 515 | 659 | } |
| 516 | 660 | } |