Skip to content

Instantly share code, notes, and snippets.

@GLWalker
Last active March 7, 2022 17:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save GLWalker/eb025c726cbac3631510928539ddc172 to your computer and use it in GitHub Desktop.
Save GLWalker/eb025c726cbac3631510928539ddc172 to your computer and use it in GitHub Desktop.
Bootstrap 5 WP Page Walker Widget Classes
<?php
/**
* Post API: Walker_Page class
*
* @package ClassicPress
* @subpackage Template
* @since WP-4.4.0
*
* Modified for Bootstrap 5 List Group with Accordian
*/
/**
* Core walker class used to create an HTML list of pages.
*
* @since WP-2.1.0
*
* @see Walker
*/
class WT_Walker_Page extends Walker_Page
{
/**
* What the class handles.
*
* @since WP-2.1.0
* @var string
*
* @see Walker::$tree_type
*/
public $tree_type = 'page';
/**
* Database fields to use.
*
* @since WP-2.1.0
* @var array
*
* @see Walker::$db_fields
* @todo Decouple this.
*/
public $db_fields = array('parent' => 'post_parent', 'id' => 'ID');
/**
* Set the current in items inside start_el()
* to use on start_lvl()
*
* @var String
*/
private $id_submenu;
private $toggle_submenu;
/**
* Outputs the beginning of the current level in the tree before elements are output.
*
* @since WP-2.1.0
*
* @see Walker::start_lvl()
*
* @param string $output Used to append additional content (passed by reference).
* @param int $depth Optional. Depth of page. Used for padding. Default 0.
* @param array $args Optional. Arguments for outputting the next level.
* Default empty array.
*/
public function start_lvl(&$output, $depth = 0, $args = array())
{
if (isset($args['item_spacing']) && 'preserve' === $args['item_spacing']) {
$t = "\t";
$n = "\n";
} else {
$t = '';
$n = '';
}
$indent = str_repeat($t, $depth);
$class_names = 'class="children submenu list-group list-group-flush ' . $this->toggle_submenu . '"';
$aria_labelledby = 'aria-labelledby="' . $this->id_submenu . '"';
$id = 'id="' . $this->id_submenu . '"';
$data_parent = 'data-bs-parent="#page_list_accordion"';
$output .= "{$n}{$indent}<ul $class_names $aria_labelledby $id $data_parent >{$n}";
}
/**
* Outputs the end of the current level in the tree after elements are output.
*
* @since WP-2.1.0
*
* @see Walker::end_lvl()
*
* @param string $output Used to append additional content (passed by reference).
* @param int $depth Optional. Depth of page. Used for padding. Default 0.
* @param array $args Optional. Arguments for outputting the end of the current level.
* Default empty array.
*/
public function end_lvl(&$output, $depth = 0, $args = array())
{
if (isset($args['item_spacing']) && 'preserve' === $args['item_spacing']) {
$t = "\t";
$n = "\n";
} else {
$t = '';
$n = '';
}
$indent = str_repeat($t, $depth);
$output .= "{$indent}</ul>{$n}";
}
/**
* Outputs the beginning of the current element in the tree.
*
* @see Walker::start_el()
* @since WP-2.1.0
*
* @param string $output Used to append additional content. Passed by reference.
* @param WP_Post $page Page data object.
* @param int $depth Optional. Depth of page. Used for padding. Default 0.
* @param array $args Optional. Array of arguments. Default empty array.
* @param int $current_page Optional. Page ID. Default 0.
*/
public function start_el(&$output, $page, $depth = 0, $args = array(), $current_page = 0)
{
if (isset($args['item_spacing']) && 'preserve' === $args['item_spacing']) {
$t = "\t";
$n = "\n";
} else {
$t = '';
$n = '';
}
if ($depth) {
$indent = str_repeat($t, $depth);
} else {
$indent = '';
}
$css_class = array('page_item', 'page-item-' . $page->ID . ' list-group-item list-group-item-action');
//https://getbootstrap.com/docs/5.1/components/list-group/#contextual-classes
$css_class[] = 'list-group-item-primary';
$toggle = '';
$toggle = ' collapse';
$this->toggle_switch($toggle);
if (isset($args['pages_with_children'][$page->ID])) {
$css_class[] = 'page_item_has_children';
}
if (!empty($current_page)) {
$_current_page = get_post($current_page);
if ($_current_page && in_array($page->ID, $_current_page->ancestors)) {
$css_class[] = 'current_page_ancestor';
// $css_class[] = 'active';
$toggle = 'show ';
$this->toggle_switch($toggle);
}
if ($page->ID == $current_page) {
$css_class[] = 'current_page_item';
$css_class[] = 'active';
} elseif ($_current_page && $page->ID == $_current_page->post_parent) {
$css_class[] = 'current_page_parent';
$toggle = 'show ';
$this->toggle_switch($toggle);
}
} elseif ($page->ID == get_option('page_for_posts')) {
$css_class[] = 'current_page_parent';
}
/**
* Filters the list of CSS classes to include with each page item in the list.
*
* @since WP-2.8.0
*
* @see wp_list_pages()
*
* @param array $css_class An array of CSS classes to be applied
* to each list item.
* @param WP_Post $page Page data object.
* @param int $depth Depth of page, used for padding.
* @param array $args An array of arguments.
* @param int $current_page ID of the current page.
*/
$css_classes = implode(' ', apply_filters('page_css_class', $css_class, $page, $depth, $args, $current_page));
if ('' === $page->post_title) {
/* translators: %d: ID of a post */
$page->post_title = sprintf(__('#%d (no title)'), $page->ID);
}
# access the right property later in start_lvl (->title is made up)
$submenu_id = $this->create_sub_menu_id($page);
$args['link_before'] = empty($args['link_before']) ? '' : $args['link_before'];
$args['link_after'] = empty($args['link_after']) ? '' : $args['link_after'];
$atts = array();
$atts['href'] = get_permalink($page->ID);
$atts['aria-current'] = ($page->ID == $current_page) ? 'page' : '';
$atts['id'] = $submenu_id;
if (isset($args['pages_with_children'][$page->ID])) {
$atts['class'] = 'accordion-button ' . $this->toggle_submenu . 'd';
$atts['data-bs-toggle'] = 'collapse';
$atts['data-bs-target'] = '#' . $this->id_submenu;
}
/**
* Filters the HTML attributes applied to a page menu item's anchor element.
*
* @since WP-4.8.0
*
* @param array $atts {
* The HTML attributes applied to the menu item's `<a>` element, empty strings are ignored.
*
* @type string $href The href attribute.
* @type string $aria_current The aria-current attribute.
* }
* @param WP_Post $page Page data object.
* @param int $depth Depth of page, used for padding.
* @param array $args An array of arguments.
* @param int $current_page ID of the current page.
*/
$atts = apply_filters('page_menu_link_attributes', $atts, $page, $depth, $args, $current_page);
$attributes = '';
foreach ($atts as $attr => $value) {
if (!empty($value)) {
$value = esc_attr($value);
$attributes .= ' ' . $attr . '="' . $value . '"';
}
}
$output .= $indent . sprintf(
'<li class="%s"><a%s>%s%s%s</a>',
$css_classes,
$attributes,
$args['link_before'],
/** This filter is documented in wp-includes/post-template.php */
apply_filters('the_title', $page->post_title, $page->ID),
$args['link_after']
);
if (!empty($args['show_date'])) {
if ('modified' == $args['show_date']) {
$time = $page->post_modified;
} else {
$time = $page->post_date;
}
$date_format = empty($args['date_format']) ? '' : $args['date_format'];
$output .= " " . mysql2date($date_format, $time);
}
}
/**
* Outputs the end of the current element in the tree.
*
* @since WP-2.1.0
*
* @see Walker::end_el()
*
* @param string $output Used to append additional content. Passed by reference.
* @param WP_Post $page Page data object. Not used.
* @param int $depth Optional. Depth of page. Default 0 (unused).
* @param array $args Optional. Array of arguments. Default empty array.
*/
public function end_el(&$output, $page, $depth = 0, $args = array())
{
if (isset($args['item_spacing']) && 'preserve' === $args['item_spacing']) {
$t = "\t";
$n = "\n";
} else {
$t = '';
$n = '';
}
$output .= "</li>{$n}";
}
/**
* Create a submenu ID based on
* menu post type
*
* @param WP_Post $item Menu item data object.
*/
private function create_sub_menu_id($item)
{
return $this->id_submenu = "submenu-" . $item->post_name;
}
/**
* Create a switch to toggle accordian open
* if menu link is active
*
* @param WP_Post $item Menu item data object.
*/
private function toggle_switch($val)
{
return $this->toggle_submenu = $val;
}
}
<?php
/**
* Widget API: WP_Widget_Pages class
*
* @package WordPress
* @subpackage Widgets
* @since 4.4.0
*
* Modified for Bootstrap 5 List Group with Accordian
*/
/**
* Core class used to implement a Pages widget.
*
* @since 2.8.0
*
* @see WP_Widget
*/
//class WT_Widget_Pages extends WP_Widget_Pages
// Need to only extend WP_Widget in this case
class WT_Widget_Pages extends WP_Widget
{
/**
* Sets up a new Pages widget instance.
*
* @since 2.8.0
*/
public function __construct()
{
$widget_ops = array(
'classname' => 'widget_pages',
'description' => __('A list of your site&#8217;s Pages.'),
'customize_selective_refresh' => true,
'show_instance_in_rest' => true,
);
parent::__construct('pages', __('Pages'), $widget_ops);
}
/**
* Outputs the content for the current Pages widget instance.
*
* @since 2.8.0
*
* @param array $args Display arguments including 'before_title', 'after_title',
* 'before_widget', and 'after_widget'.
* @param array $instance Settings for the current Pages widget instance.
*/
public function widget($args, $instance)
{
$default_title = __('Pages');
$title = !empty($instance['title']) ? $instance['title'] : $default_title;
/**
* Filters the widget title.
*
* @since 2.6.0
*
* @param string $title The widget title. Default 'Pages'.
* @param array $instance Array of settings for the current widget.
* @param mixed $id_base The widget ID.
*/
$title = apply_filters('widget_title', $title, $instance, $this->id_base);
$sortby = empty($instance['sortby']) ? 'menu_order' : $instance['sortby'];
$exclude = empty($instance['exclude']) ? '' : $instance['exclude'];
if ('menu_order' === $sortby) {
$sortby = 'menu_order, post_title';
}
$output = wp_list_pages(
/**
* Filters the arguments for the Pages widget.
*
* @since 2.8.0
* @since 4.9.0 Added the `$instance` parameter.
*
* @see wp_list_pages()
*
* @param array $args An array of arguments to retrieve the pages list.
* @param array $instance Array of settings for the current widget.
*/
apply_filters(
'widget_pages_args',
array(
'title_li' => '',
'echo' => 0,
'sort_column' => $sortby,
'exclude' => $exclude,
'item_spacing' => '',
),
$instance
)
);
if (!empty($output)) {
echo $args['before_widget'];
if ($title) {
echo $args['before_title'] . $title . $args['after_title'];
}
// The title may be filtered: Strip out HTML and make sure the aria-label is never empty.
$title = trim(strip_tags($title));
$aria_label = $title ? $title : $default_title;
echo '<nav role="navigation" aria-label="' . esc_attr($aria_label) . '">';
?>
<ul id="page_list_accordion" class="list-group">
<?php echo $output; ?>
</ul>
<?php
echo '</nav>';
echo $args['after_widget'];
}
}
/**
* Handles updating settings for the current Pages widget instance.
*
* @since 2.8.0
*
* @param array $new_instance New settings for this instance as input by the user via
* WP_Widget::form().
* @param array $old_instance Old settings for this instance.
* @return array Updated settings to save.
*/
public function update($new_instance, $old_instance)
{
$instance = $old_instance;
$instance['title'] = sanitize_text_field($new_instance['title']);
if (in_array($new_instance['sortby'], array('post_title', 'menu_order', 'ID'), true)) {
$instance['sortby'] = $new_instance['sortby'];
} else {
$instance['sortby'] = 'menu_order';
}
$instance['exclude'] = sanitize_text_field($new_instance['exclude']);
return $instance;
}
/**
* Outputs the settings form for the Pages widget.
*
* @since 2.8.0
*
* @param array $instance Current settings.
*/
public function form($instance)
{
// Defaults.
$instance = wp_parse_args(
(array) $instance,
array(
'sortby' => 'post_title',
'title' => '',
'exclude' => '',
)
);
?>
<p>
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php _e('Title:'); ?></label>
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($instance['title']); ?>" />
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('sortby')); ?>"><?php _e('Sort by:'); ?></label>
<select name="<?php echo esc_attr($this->get_field_name('sortby')); ?>" id="<?php echo esc_attr($this->get_field_id('sortby')); ?>" class="widefat">
<option value="post_title" <?php selected($instance['sortby'], 'post_title'); ?>><?php _e('Page title'); ?></option>
<option value="menu_order" <?php selected($instance['sortby'], 'menu_order'); ?>><?php _e('Page order'); ?></option>
<option value="ID" <?php selected($instance['sortby'], 'ID'); ?>><?php _e('Page ID'); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('exclude')); ?>"><?php _e('Exclude:'); ?></label>
<input type="text" value="<?php echo esc_attr($instance['exclude']); ?>" name="<?php echo esc_attr($this->get_field_name('exclude')); ?>" id="<?php echo esc_attr($this->get_field_id('exclude')); ?>" class="widefat" />
<br />
<small><?php _e('Page IDs, separated by commas.'); ?></small>
</p>
<?php
}
}
<?php
/**
* Your theme should have BootStrap5 CSS and the javascript enqued.
*
* This creates a WordPress pages widget with BootStrap 5 list group menu with accordian dropdown for submenus
* Accordian works for all levels, but is quirky after the 2cnd level becuase the top level accordian button likes to close
* onClick of the second level - once reopend, the deeper levels do show,
* can probably be modified with some JS if it's really that important.
*
* Add the following two class files to your theme and call theme using whatever your prefered method is:
*
* /class-wt-walker-page.php
* /class-wt-widget-pages.php
*
* Add the following code to your functions:
*/
/**
* Pages Widget
*
*/
function wt_widget_pages_args($args)
{
$args = array_merge($args, array('walker' => new WT_Walker_Page()));
return $args;
}
add_filter('widget_pages_args', 'wt_widget_pages_args');
/**
* Add inline styles widget
* change wp_add_inline_style('bootstrap', $css_output);
* to match whatever your bootsrap stylesheet handle is
*
* @return css
*/
function wt_pla_inline_style()
{
$css_input = '
/* Pages List Accordian Widget */
#page_list_accordion li a{display:block;color:inherit!important}#page_list_accordion a:hover{color:inherit!important}#page_list_accordion li ul{padding-top:1rem;margin-bottom:-.5rem}#page_list_accordion li ul li{width:calc(100% + 2rem)!important;padding:.5rem 1rem;margin-left:-1rem;margin-right:-1rem;overflow:hidden!important}#page_list_accordion .accordion-button{display:flex!important;padding:.25rem 0 .33rem!important;font-size:inherit!important;color:inherit!important;background-color:inherit!important;box-shadow:none}#page_list_accordion .accordion-button:not(.collapsed){color:inherit!important;background-color:inherit!important;box-shadow:inherit!important}#page_list_accordion .accordion-button:focus{border-color:inherit!important;box-shadow:none}#page_list_accordion li a{display:block;color:inherit!important}#page_list_accordion a:hover{color:inherit!important}#page_list_accordion li ul{padding-top:1rem;margin-bottom:-.5rem}#page_list_accordion li ul li{width:calc(100% + 2rem)!important;padding:.5rem 1rem;margin-left:-1rem;margin-right:-1rem;overflow:hidden!important}#page_list_accordion .accordion-button{display:flex!important;padding:.25rem 0 .33rem!important;font-size:inherit!important;color:inherit!important;background-color:inherit!important;box-shadow:none}#page_list_accordion .accordion-button:not(.collapsed){color:inherit!important;background-color:inherit!important;box-shadow:inherit!important}#page_list_accordion .accordion-button:focus{border-color:inherit!important;box-shadow:none}
';
$css_output = esc_html($css_input);
wp_add_inline_style('bootstrap', $css_output);
wp_enqueue_style('bootstrap-inline-styles', get_stylesheet_uri());
}
add_action('wp_enqueue_scripts', 'wt_pla_inline_style');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment