Blog

Code Snippet: Admin Quick Nav

Version: 1.6.1 (22.07.2024)

Einführung

Bei der Erstellung und Pflege von Websites mit WordPress muss man regelmäßig zwischen den Inhaltstypen (Seiten, Beiträge, Custom Posts) hin- und herspringen. In der normalen WordPress Admin Navigation springt man hierzu zunächst auf die Liste, wartet bis die Liste angezeigt wird, und klickt dann auf den gewünschten Eintrag zur Bearbeitung.
Der Wechsel zwischen verschiedenen Inhalten benötigt dadurch einige Klicks und auch Zeit.

Lösung

Das hier beschriebene Code Snippet stellt eine Schnellnavigation in der oberen Admin-Leiste zur Verfügung.
Die Navigation ist sehr individuell konfigurierbar, unterstützt neben den WordPress Standard-Inhaltstypen "Beiträge" und "Seiten" auch eigene Inhaltstypen (Custom Posts), und bietet auch erweiterte Funktionalität für die Builder Bricks und Oxygen.

Einstellungen

Die Konfiguration erfolgt über Einstellungen > Quick Nav in der deutschen WordPress Oberfläche, bzw. Settings > Quick Nav in der englischen.

Inhaltstypen

Es werden alle Inhaltstypen aufgelistet, die auch in der linken Admin-Leiste auftauchen.
Die Inhaltstypen können individuell zur Anzeige in der Schnellnavigation ausgewählt werden.

Reihenfolge ändern

Mit dem Symbol können die Inhaltstypen per Drag & Drop in die gewünschte Reihenfolge gebracht werden.
Einfach das Symbol mit der Maus halten und an die gewünschte Stelle in der Liste ziehen.

Sortierung der Einträge

Die Auswahllisten "Sort Order" und "Order" erlauben die Festlegung der gewünschten Sortierung für die Einträge in der Schnellnavigation. Es kann nach Titel, Erstellungsdatum, oder auch der im Inhaltstyp selbst festgelegten Reihenfolge, auf- oder absteigend sortiert werden.

Limit

Mit der Einstellung "Limit" kann die Anzahl der in der Schnellnavigation angezeigten Einträge begrenzt werden.
Ein leeres Feld erlaubt die Auflistung aller Einträge im System. Ein gesetzter Wert begrenzt die Anzahl der gelisteten Einträge, und zwar in Bezug auf die aktuell gewählte Sortierung.

Hierarchische Darstellung

Einige Inhaltstypen, wie beispielsweise "Seiten", erlauben die Festlegung einer Hierarchie.
Mit der Option "Show hierarchy" werden die Einträge in der Schnellnavigation entsprechend ihrer Hierarchie-Ebene eingerückt dargestellt.

HappyFiles Ordner

Falls Sie das Plugin HappyFiles installiert haben, um Ihre Inhaltstypen zu organisieren, können Sie mit der Einstellung "HappyFiles Folders" diese Organisations-Ordner auch in den Listen der Schnellnavigation anzeigen lassen.

Diese Einstellung steht für Inhaltstypen zur Verfügung, die in den HappyFiles Einstellungen aktiviert wurden.

Die Kombination "HappyFiles Folders" und "Hierarchische Darstellung" wird derzeit nicht zuverlässig unterstützt.

Allgemeine Einstellungen

Menü verschlanken

Die Einstellung "Collapse" legt fest, ob die Inhaltstypen nebeneinander in der Menüzeile angezeigt werden, oder in einem gemeinsamen Menü "Quick Nav" zusammen gefasst werden.

Hier der Vergleich zwischen den Einstellungsvarianten:

Schnellsuche

Mit der Einstellung "Enable Quick Search" kann die Schnellsuche aktiviert werden.
Dabei wird ein Eingabefeld über jeder Liste von Inhaltstypen angeboten, die bei Text-Eingabe eine Filterung der angezeigten Einträge vornimmt.
Das ist besonders hilfreich, wenn die Liste der Beiträge lang ist, und die eingestellte Sortierung eine Auswahl erschwert.

Bei der Suche muss die Groß-/Kleinschreibung nicht beachtet werden.
Die Suche berücksichtigt auch das Sprachkennzeichen und den Status, falls diese angezeigt werden.

Sprache anzeigen

Falls sie Polylang installiert haben, kann in den Listen optional die Sprache angezeigt werden.

Admin-Bar in Builder

Falls Sie Bricks oder Oxygen installiert haben, können Sie mit dieser Einstellung festlegen, ob die obere WordPress Admin-Leiste auch im Builder angezeigt werden soll.

Status

In der Standard-Konfiguration werden nur Inhalte mit Status "Veröffentlicht" in der Navigation aufgelistet.
Sie können hier festlegen, ob auch Inhalte mit Status "Entwurf" (draft), "Ausstehende Überprüfung" (pending) und "Privat" (private) angezeigt werden sollen.
In den Listen werden diese Einträge entsprechend gekennzeichnet.

Unterstützung von Bricks und Oxygen

Das Snippet unterstützt die beiden Builder Bricks und Oxygen.

Für unterstützte Inhaltstypen kann die Bearbeitung mit dem jeweiligen Builder aktiviert werden.

Hier der Vergleich der Einstellungsmöglichkeiten bei Verwendung von Bricks und Oxygen:

In den allgemeinen Einstellungen gibt es die Option, die obere Admin Leiste auch im Builder anzuzeigen.

So sieht das in Bricks bzw. Oxygen aus:

Bei aktivierter Builder Unterstützung zeigen die Listen in der Schnellnavigation Symbole für die Bearbeitung in WordPress und im jeweiligen Builder.
Dunklere oder helle Symbole zeigen an, ob Inhalt in WordPress bzw. dem Builder vorhanden ist.

Hier zum Vergleich:

FAQ – Häufig gestellte Fragen

Natürlich. Einfach auf die übergeordnete Bezeichnung "Seiten/Beiträge/..." klicken.

Das Snippet stellt keine Option für "öffnen in neuem Tab" zur Verfügung.
Eine generelle Einstellung ist nicht sinnvoll, da meist je nach Anwendungsfall entschieden wird, ob der Link in einem neuen oder im gleichen Tab geöffnet werden soll. Und zusätzliche Symbole je Link würden die Oberfläche unnötig überladen.

Es gibt eine viel einfachere und flexiblere Lösung:
Jeder moderne Browser (Chrome, Firefox, Safari) erlaubt es, einen Link in einem neuen Tab zu öffnen.
Entweder per Rechtsklick > In neuem Tab öffnen, oder über eine Tastenkombination:

Windows: Strg-Klick
macOS: Cmd-Klick

Die Schnellnavigation berücksichtigt Berechtigungen in vollem Umfang.

  • Dem Benutzer werden nur Inhaltstypen und Beiträge angezeigt, die er auch bearbeiten kann.
  • Dies gilt insbesondere auch für die beiden Builder Bricks und Oxygen, die eine eigene Berechtigungsverwaltung zur Verfügung stellen.
  • Die Konfiguration der Schnellnavigation ist Benutzern mit Admin-Rechten vorbehalten.

Die Schnellnavigation listet in der Standard-Einstellung nur Inhalte mit dem Status "Veröffentlicht".
In den Einstellungen können auch andere Status gewählt werden.

Download

Dieser JSON-Download kann direkt in die Plugins Code Snippets oder Advanced Scripts importiert werden. Vergiss nicht, das Snippet nach dem Import zu aktivieren.
Falls Du ein anderes Snippet Plugin verwendest, kannst Du stattdessen den Source Code kopieren und damit selbst ein neues Snippet anlegen.

ma-admin-quick-nav.code-snippets.json
Version 1.6.1, 2024-07-22

Spenden

Es macht mir viel Freude, Code Snippets zu entwickeln und damit Anforderungen zu lösen. Die Snippets stelle ich kostenfrei zur Verfügung.

Wenn Du möchtest, kannst Du meine vielen Stunden Arbeit mit einer kleinen Kaffee-Spende über PayPal honorieren.

  Bei Klick auf den Button wird eine Verbindung zu PayPal aufgebaut.

Spenden werden selbstverständlich ordnungsgemäß durch mich versteuert.

Disclaimer

Das Code Snippet habe ich nach bestem Wissen und Gewissen mit den aktuellsten WordPress und Builder Version entwickelt und getestet.
Ich stelle das Code Snippet zur freien Verwendung zur Verfügung.
Eine Garantie für die Funktionalität in allen denkbaren WordPress Umgebungen kann ich nicht geben. 
Download und Nutzung dieses Code Snippets erfolgen auf eigene Gefahr und Verantwortung.

Change Log

Siehe "Version History" in Source Code

Source Code

<?php
/*
Plugin Name:  MA Admin Quick Nav
Description:  Admin Bar Quick Navigation for Post Types
Author:       <a href="https://www.altmann.de/">Matthias Altmann</a>
Project:      Code Snippet: MA Admin Quick Nav
Version:      1.6.1
Plugin URI:   https://www.altmann.de/en/blog-en/code-snippet-admin-quick-nav/
Description:  en: https://www.altmann.de/en/blog-en/code-snippet-admin-quick-nav/
              de: https://www.altmann.de/blog/code-snippet-admin-schnellnavigation/
Copyright:    © 2024, Matthias Altmann

TESTED WITH:
Product		Versions
--------------------------------------------------------------------------------------------------------------
PHP 		8.1, 8.2
WordPress	6.4.2 ... 6.6
Bricks		1.9.5 ... 1.9.9
Oxygen		4.8.1 ... 4.8.3
--------------------------------------------------------------------------------------------------------------

VERSION HISTORY:
Date		Version		Description
--------------------------------------------------------------------------------------------------------------
2024-07-22	1.6.1		Fixes:
						- Prevent fatal errors if no HappyFiles post types have been configured
						 (Thanks to Pascal Dörflinger for reporting)
2024-07-17	1.6.0		New Features:
						- Support for HappyFiles folders
						  (Requested by Sebastian Berger)
2024-07-07	1.5.0		New Features:
						- Quick Search
						  (Requested by John Kirker)
						Changes:
						- Optimized post limit detection (now checking all selected states)
2024-05-23	1.4.0		New Features:
						- Optional display of Polylang language in Quick Nav lists
2024-05-21	1.3.1		Changes:
						- Compatibility to Oxygen 4.8.3 with changed post meta keys
2024-04-09	1.3.0		New Features:
						- Support for selecting status of posts to be listed
2024-04-05	1.2.0		Changes:
						- Disabled Polylang language filter to show all languages, also in frontend
						- Changed some direct Bricks function calls to call_user_func() to avoid warnings in 
						  code editor on non-Bricks setups
2024-02-07	1.1.0		New Features:
						- New action "View" (except for Oxygen/Bricks templates)
2024-02-03	1.0.2		Fixes:
						- Corrected semantical issue by moving svg symbols from head to footer
						- Corrected action icon width.
2024-01-30	1.0.1		Fixes:
						- Added additional permission check for Bricks templates
						Tested:
2024-01-28	1.0.0		Changes:
						- Stretch sub menu item links
						- Improved translation
						- Empty limit now means unlimited
						Inspired by Michael Pucher:
						- Sortable post types
						- Collapsed or individual menus for post types
						- Allow sorting by menu_order
						- Removed title attribute from top menu because that tooltip is distracting
2024-01-26	0.0.1		Internal test version supporting Oxygen and Bricks
2024-01-20	0.0.0		Development start
--------------------------------------------------------------------------------------------------------------

Icon Arrow:	https://css.gg/arrow-top-right-o
Icon Eye:	https://css.gg/eye
*/

if (!class_exists('MA_Admin_Quick_Nav')) :

class MA_Admin_Quick_Nav {
	const TITLE							= 'Quick Nav';
	const SLUG							= 'ma-admin-quick-nav';
	const SHRT 							= 'maqn';
	const VERSION						= '1.6.1';

	// ===== CONFIGURATION ==============================================================================================
	public static $timing				= false; 	// Write timing info to wordpress debug.log if WP_DEBUG also enabled. 
													// false/0:	Disabled, true/1: Enabled, 2: Extended.
	public static $expert_mode			= false;	// enables some additional settings for the experts, like menu_position


	// ===== INTERNAL ===================================================================================================
	private const DEFAULT_MENU_POSITION	= 80; 
	private const DEFAULT_POST_TYPES 	= ['page'=>[],'post'=>[]];
	private const DEFAULT_LIMIT			= 20;
	private const DEFAULT_ORDERBY		= 'post_date';
	private const DEFAULT_ORDER			= 'DESC';
	private const DEFAULT_STATUS		= ['publish'];
	private const VALID_ORDERBY			= ['post_date','post_title','menu_order'];
	private const VALID_ORDER			= ['ASC','DESC'];
	private const MINIMIZE_CSS			= true;
	private const MINIMIZE_SVG			= true;
	private const MINIMIZE_JS			= true;

	public static $total_runtime		= 0;
	private $settings 					= null;

	//-------------------------------------------------------------------------------------------------------------------
	function __construct() {
		$st = microtime(true);

		// read settings from db and merge to default settings
		$this->settings = array_merge([
			'menu_position'	=> self::DEFAULT_MENU_POSITION,
			'post_types'	=> [],
			'status'		=> self::DEFAULT_STATUS,
		], get_option(self::SLUG, ['post_types'=>self::DEFAULT_POST_TYPES]));


		// emit CSS for backend and frontend 
		add_action('admin_enqueue_scripts',	[$this, 'css']);
		add_action('wp_enqueue_scripts', 	[$this, 'css']);

		// emit SVG for backend and frontend 
		add_action('admin_footer',			[$this, 'svg']);
		add_action('wp_footer', 			[$this, 'svg']);

		add_action('admin_enqueue_scripts',	[$this, 'admin_scripts']);
	

		// display menu only for wp-admin area
		if (is_admin()) {
			// register admin menus (must be registered with hook, otherwise appears on top of menu)
			add_action('admin_menu', [$this, 'admin_menu'], 100 );
			// register settings
			add_action('admin_init', [$this, 'settings_register'] );

			// TODO: custom translations
			//add_action('after_setup_theme',[$this,'l10n']);
		} 



		// builder admin bar?
		if ($this->settings['builder_admin_bar']??false) {
			// must hook before init
			add_action('after_setup_theme',[$this,'builder_admin_bar']);
		}

		// register admin bar menu items
		add_action('admin_bar_menu',[$this, 'admin_bar_items'], intval($this->settings['menu_position']??self::DEFAULT_MENU_POSITION));

		// add a handler for logging total runtime
		add_action('shutdown', [$this,'total_runtime']);

		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s() Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, $et-$st));}
		self::$total_runtime += $et-$st;
	}

	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Logs total timing.
	 */
	public static function total_runtime(){
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s() Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, self::$total_runtime));}
	}

	//-------------------------------------------------------------------------------------------------------------------
	public function l10n(){
		global $l10n;
		// TODO: custom translations
	}

	//===================================================================================================================
	// SETTINGS
	//-------------------------------------------------------------------------------------------------------------------
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Creates the Settings menu.
	 */
	public function admin_menu(){
		add_options_page(	
			self::TITLE.' '.__('Settings'), // page title
			self::TITLE,					// menu title
			'manage_options', 				// capabilitiy
			self::SLUG, 					// menu slug
			[$this, 'settings'] 			// function
		);

	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Register the settings and validation method.
	 */
	public function settings_register() {
		register_setting(	
			self::SLUG, 					// option group
			self::SLUG, 					// option name
			[
				'type'				=> 'array',
				'sanitize_callback'	=> [$this, 'settings_validate'],
			]
		);
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Outputs the settings page.
	 */
	public function settings() {
		// check user capabilities
		if (!current_user_can('manage_options')) return;

		// show error/update messages
		settings_errors(self::SLUG . '_messages');

		// get the registered post types
		$registered_post_types = get_post_types([],'OBJECT');

		// get the registered post statuses
		$post_statuses = get_post_statuses();

		?>
		<div class="wrap <?php echo self::SLUG;?>-settings" id="<?php echo self::SLUG;?>-settings">

			<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
			<style>
				.ptl {border:1px solid lightgray; border-collapse:collapse;}
				.ptl thead {background-color:lightgray;}
				.ptl tr {border-bottom:1px solid lightgray;}
				.ptl :where(th,td) {text-align:left; vertical-align:top; padding:5px;}
				.ptl :where(th,td).sort {width:20px;}
				.ptl :where(th,td).sort svg.sort-handle {color:gray; width:1em; height:1em; padding-top:5px; cursor:ns-resize;}
				.ptl input[type='number'] {width:10ch;}
				.ptl .details * {visibility:hidden;}
				.ptl .active .details * {visibility:visible;}
				.general input[type='number'] {width:10ch;}
			</style>
			<form method="post" action="options.php">

				<?php settings_fields(self::SLUG); ?>

				<h2><?php _e('Post Types'); ?></h2>
				<p>Enable Admin Bar Menu for Post Types:</p>
				<table class="ptl" role="presentation">
					<thead>
						<tr>
							<th class="sort"></th>
							<th><?php _e('Post Type'); ?></th>
							<th><?php _e('Sort Order:'); ?></th>
							<th><?php _e('Order'); ?></th>
							<th><?php _ex('Count','Number/count of items'); ?></th>
							<th><?php _e('Advanced Options'); ?></th>
						</tr>
					</thead>

					<tbody>
					<?php 
					// get post types from settings plus other registered post types
					$post_types = array_unique(array_merge(array_keys($this->settings['post_types']), array_keys(wp_list_pluck($registered_post_types,'name'))));
					foreach ($post_types as $post_type):
						// check if configured post type is still a registered post type
						if (!$reg_post_type = $registered_post_types[$post_type]) continue;
						// skip post types that are not configured to be listed in nav menus
						if (!$reg_post_type->show_in_nav_menus) continue;

						// loop valid post types
						$ptn = $reg_post_type->name;
						$ptl = $reg_post_type->label;
						$pts = $this->settings['post_types']??[];
						$pto = array_merge([
							'orderby'		=> self::DEFAULT_ORDERBY,
							'order'			=> self::DEFAULT_ORDER,
							'limit'			=> self::DEFAULT_LIMIT,
							'hierarchical'	=> false,
							'happyfiles'	=> false,
							'builder' 		=> false, 
						],$pts[$ptn] ?? []);

						$label = $ptl . ' (<code>'.$ptn.'</code>)'; 
						// label adaption for special cases
						if ($ptn == 'ct_template') {$label = 'Oxygen '.$label;}
						?>
						<tr class="post_type_row <?php echo in_array($ptn,array_keys($pts)) ? 'active' : '';?>">
							<td class="sort">
								<svg class="sort-handle"><use xlink:href="#icon-sort"></use></svg>
							</td>
							<td>
								<input type="checkbox" name="post_types[]" id="post_type_<?php echo $ptn;?>" value="<?php echo $ptn;?>" <?php checked(in_array($ptn,array_keys($pts)), true);?>>
								<label for="post_type_<?php echo $ptn;?>"><?php echo $label;?></label>
							</td>
							<td class="details">
								<select name="<?php echo $ptn;?>-orderby" id="<?php echo $ptn;?>-orderby">
									<option value="post_date" <?php selected($pto['orderby'], 'post_date'); ?>><?php _e('Created');?></option>
									<option value="post_title" <?php selected($pto['orderby'], 'post_title'); ?>><?php _e('Title');?></option>
									<option value="menu_order" <?php selected($pto['orderby'], 'menu_order'); ?>><?php _e('Menu Order');?></option>
								</select> 
							</td>
							<td class="details">
								<select name="<?php echo $ptn;?>-order" id="<?php echo $ptn;?>-order">
									<option value="ASC" <?php selected($pto['order'], 'ASC'); ?>><?php _e('Ascending');?></option>
									<option value="DESC" <?php selected($pto['order'], 'DESC'); ?>><?php _e('Descending');?></option>
								</select> 
							</td>
							<td class="details">
								<input type="number" name="<?php echo $ptn;?>-limit" id="<?php echo $ptn;?>-limit" value="<?php echo $pto['limit'];?>">
							</td>
							<td class="details">
								<?php if ($reg_post_type->hierarchical === true): 
									$hint = 'Show hierarchy by indenting list'; ?>
									<input type="checkbox" name="<?php echo $ptn;?>-hierarchical" id="<?php echo $ptn;?>-hierarchical" title="<?php echo $hint;?>" value="1" <?php checked($pto['hierarchical'],true);?>>
									<label for="<?php echo $ptn;?>-hierarchical" title="<?php echo $hint;?>"><?php _e('Show hierarchy');?></label>
									<br>
								<?php endif; ?>
								<?php if (defined('HAPPYFILES_VERSION') && in_array($ptn, $this->get_happyfiles_post_types())):
										$hint = 'Group by HappyFiles folders'; ?>
									<input type="checkbox" name="<?php echo $ptn;?>-happyfiles" id="<?php echo $ptn;?>-happyfiles" title="<?php echo $hint;?>" value="1" <?php checked($pto['happyfiles'],true);?>>
									<label for="<?php echo $ptn;?>-happyfiles" title="<?php echo $hint;?>"><?php echo 'HappyFiles '.esc_html__( 'Folders', 'happyfiles' );?></label>
									<br>
								<?php endif; ?>
								<?php if ($this->builder()): 
									$hint = 'Show icon to allow editing in '.$this->builder(); ?>
									<input type="checkbox" name="<?php echo $ptn;?>-builder" id="<?php echo $ptn;?>-builder" title="<?php echo $hint;?>" value="1" <?php checked($pto['builder']??false,true);?>>
									<label for="<?php echo $ptn;?>-builder" title="<?php echo $hint;?>"><?php printf(__('Edit').' in %s Builder',$this->builder());?></label>
								<?php endif; ?>
							</td>
						</tr>
						<?php
					endforeach;
					?>
					</tbody>
				</table>
				<script>
				document.querySelectorAll('input[name^="post_types[]"]').forEach($c=>{
					$c.addEventListener('change',($e)=>{
						let $r = $e.currentTarget.closest('.post_type_row');
						if ($e.currentTarget.checked) {$r.classList.add('active');} 
						else {$r.classList.remove('active');}
					});
				});
				jQuery(document).ready(function(){
					jQuery('table.ptl tbody').sortable({
						handle: '.sort-handle',
						cursor: 'ns-resize',
						axis:   'y',
					});
				});
				</script>

				<h2><?php _e('General'); ?></h2>
				<div class="general">

					<?php if (self::$expert_mode):  ?>
					<div>
						<?php $hint = sprintf('Set the position for the %s menu(s)', self::TITLE); ?>
						<label for="menu_position" title="<?php echo $hint;?>"><?php printf(__('Menu Location'), self::TITLE);?></label>
						<input type="number" name="menu_position" id="menu_position" value="<?php echo $this->settings['menu_position']?>">
					</div>
					<?php endif; ?>
					
					<div>
						<?php $hint = sprintf('Collapse all post type menus into one menu "%s"', self::TITLE); ?>
						<input type="checkbox" name="collapsed_menu" id="collapsed_menu" title="<?php echo $hint;?>" value="1" <?php checked($this->settings['collapsed_menu']??false,true);?>>
						<label for="collapsed_menu" title="<?php echo $hint;?>"><?php printf(__('Collapse menu').' in "%s"', self::TITLE);?></label>
					</div>

					<div>
						<?php $hint = sprintf('Enable quick search bar'); ?>
						<input type="checkbox" name="quick_search" id="quick_search" title="<?php echo $hint;?>" value="1" <?php checked($this->settings['quick_search']??false,true);?>>
						<label for="quick_search" title="<?php echo $hint;?>"><?php printf(__('Enable Quick Search'));?></label>
					</div>

					<?php if (defined('POLYLANG_VERSION')): ?>
					<div>
						<?php $hint = sprintf('Show language for items if defined'); ?>
						<input type="checkbox" name="show_language" id="show_language" title="<?php echo $hint;?>" value="1" <?php checked($this->settings['show_language']??false,true);?>>
						<label for="show_language" title="<?php echo $hint;?>"><?php printf(__('Show language').' in "%s"', self::TITLE);?></label>
					</div>
					<?php endif; ?>
					
					<?php if ($this->builder()): ?>
					<div>
						<?php $hint = sprintf('Enable admin bar in %s builder',$this->builder()); ?>
						<input type="checkbox" name="builder_admin_bar" id="builder_admin_bar" title="<?php echo $hint;?>" value="1" <?php checked($this->settings['builder_admin_bar']??false,true);?>>
						<label for="builder_admin_bar" title="<?php echo $hint;?>"><?php printf(__('Admin Bar in %s Builder'),$this->builder());?></label>
					</div>
					<?php endif; ?>

					<div>
						<h3>Status</h3>
						<?php foreach ($post_statuses as $status => $status_label): ?>
							<?php $hint = sprintf('List items with status "%s"', $status_label); ?>
							<input type="checkbox" name="status[]" title="<?php echo $hint;?>" value="<?php echo $status;?>" <?php checked(in_array($status,$this->settings['status']),true);?>> <?php echo $status_label;?><br/>
						<?php endforeach;?>
					</div>

				</div>

				<?php submit_button(); ?>
			</form>
		</div>
		<div style="margin-top:3em;border-top:1px solid lightgray;font-size:70%;">
			© 2024, <a href="https://www.altmann.de/" target="_blank">Matthias Altmann</a> 
			• <?php echo basename(__FILE__) == 'ma-admin-quick-nav.php' ? 'Plugin' : 'Code Snippet';?> Version <?php echo self::VERSION;?> 
			• <a href="https://www.altmann.de/en/blog-en/code-snippet-admin-quick-nav/" target="_blank">Downloads & Documentation</a>
		</div>
		<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1">
			<defs>
				<symbol id="icon-sort" viewBox="0 0 500 500">
					<title><?php _e('Reorder'); ?></title>
					<path fill="currentColor" d="M285.714 303.571q0 7.254-5.301 12.556l-125 125q-5.301 5.301-12.556 5.301t-12.556-5.301l-125-125q-5.301-5.301-5.301-12.556t5.301-12.556 12.556-5.301h250q7.254 0 12.556 5.301t5.301 12.556zM285.714 196.429q0 7.254-5.301 12.556t-12.556 5.301h-250q-7.254 0-12.556-5.301t-5.301-12.556 5.301-12.556l125-125q5.301-5.301 12.556-5.301t12.556 5.301l125 125q5.301 5.301 5.301 12.556z" />
				</symbol>
			</defs>
		</svg>
		<?php
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Validates the submitted settings.
	 */
	public function settings_validate() {
		$input = $_REQUEST;
		$valid = ['post_types'=>[]];

		// check selected post types
		$allowed_post_types = wp_list_pluck(get_post_types([],'OBJECT'),'name');
		foreach(($input['post_types']??[]) as $ptn) {
			if (in_array($ptn, $allowed_post_types)) {
				$pto = [];
				if (in_array($input[$ptn.'-orderby']??'',self::VALID_ORDERBY)) {
					$pto['orderby'] = $input[$ptn.'-orderby'];
				}
				if (in_array($input[$ptn.'-order']??'',self::VALID_ORDER)) {
					$pto['order'] = $input[$ptn.'-order'];
				}
				$limit = $input[$ptn.'-limit'] ?? null;
				if (isset($limit)) {
					$pto['limit'] = '';
					if ($limit !== '') {
						$limit = intval($limit);
						if ($limit !== 0) {
							$pto['limit'] = $limit;
						}
					}
				}
				if ($input[$ptn.'-hierarchical']??'') {
					$pto['hierarchical'] = true;
				}
				if ($input[$ptn.'-happyfiles']??'') {
					$pto['happyfiles'] = true;
				}
				if ($input[$ptn.'-builder']??'') {
					$pto['builder'] = true;
				}
				$valid['post_types'][$ptn] = $pto;
			}
		}
		if ($menu_position = $input['menu_position'] ?? null) {
			$valid['menu_position'] = intval($menu_position);
		}
		if ($input['quick_search']??'' == '1') {
			$valid['quick_search'] = true;
		}
		if ($input['collapsed_menu']??'' == '1') {
			$valid['collapsed_menu'] = true;
		}
		if ($input['show_language']??'' == '1') {
			$valid['show_language'] = true;
		}
		if ($input['builder_admin_bar']??'' == '1') {
			$valid['builder_admin_bar'] = true;
		}
		if ($input['status']??null) {
			$valid['status'] = $input['status'];
		}
		return $valid;
	}




	//===================================================================================================================
	// ADMIN BAR
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Enables admin bar in builders.
	 */
	public function builder_admin_bar() {
		if ($this->builder('Oxygen')) {
			// disable Oxygen's admin bar removal
			remove_action('init','ct_hide_admin_bar');
			// adds Oxygen builder style adaptions if necessary
			add_action('wp_enqueue_scripts', [$this,'builder_css_oxygen']);
			add_action('init', function(){
				// don't show admin bar in builder iframe
				if (defined('OXYGEN_IFRAME')) {
					add_filter('show_admin_bar','__return_false');
				}
			});
		}
		if ($this->builder('Bricks')) {
			// adds Bricks builder style adaptions if necessary
			add_action('wp_head', [$this,'builder_css_bricks']);
			// show admin bar in Bricks
			add_action('init', function () {
				// only if Bricks editor active
				if (function_exists('bricks_is_builder_main') && call_user_func('bricks_is_builder_main')) {
					add_filter('show_admin_bar', '__return_true');
				}
			});
		}
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Emits CSS to adapt Oxygen builder.
	 */
	public function builder_css_oxygen() {
		// only for loading Oxygen UI, but not in preview iframe
		if (!defined('SHOW_CT_BUILDER')) return;
		if (defined('OXYGEN_IFRAME'))	return;

		// checked with Oxygen 4.8.1
		$builder_css = <<<STYLE_END
			/* new top */
			#oxygen-ui .oxygen-add-section-library-flyout-panel,
			#oxygen-ui .ct-panel-elements-managers,
			#oxygen-ui .oxygen-global-settings {
				top: calc(40px + var(--wp-admin--admin-bar--height,0px));
			}
			#ct-viewport-container #ct-viewport-ruller-wrap {
				top: calc(100vh - 40px -  var(--wp-admin--admin-bar--height,0px));
			}
			/* new height */
			#oxygen-ui .oxygen-add-section-library-flyout-panel,
			#oxygen-ui #oxygen-sidebar,
			#ct-viewport-container #ct-viewport-ruller-wrap {
				height: calc(100vh - 40px - var(--wp-admin--admin-bar--height,0px));
			}
			#ct-dialog-window .oxygen-data-dialog {
				height: 80%;
			}
			#oxygen-ui #ct-dom-tree-2 {
				height: calc(100vh - 119px - var(--wp-admin--admin-bar--height,0px));
			}
STYLE_END;
		echo '<style id="'.self::SLUG.'-css-oxygen">'.$builder_css.'</style>';
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Emits CSS to adapt Bricks builder.
	 */
	public function builder_css_bricks() {
		if (!function_exists('bricks_is_builder_main') || !call_user_func('bricks_is_builder_main')) return;
		
		// checked with Bricks 1.9.5
		$builder_css = <<<STYLE_END
		body.admin-bar #bricks-toolbar {
		  top: var(--wp-admin--admin-bar--height,0px);
		}
		#bricks-panel, #bricks-preview, #bricks-structure {
			height: calc(100vh - 40px - var(--wp-admin--admin-bar--height,0px));
		}
		body.admin-bar #bricks-structure {
			top: calc(40px + var(--wp-admin--admin-bar--height,0px));
		}
STYLE_END;
		echo '<style id="'.self::SLUG.'-css-bricks">'.$builder_css.'</style>';
	}
	
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Enqueues jQuery sortable for settings page. 
	 */
	public function admin_scripts(){
		if (function_exists('get_current_screen')) {
			$screen = get_current_screen();
			if ($screen->id == 'settings_page_'.self::SLUG) {
				wp_enqueue_script('jquery-ui-sortable');
			}
		}
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Emits assets in footer. 
	 */
	public function footer_assets(){
		do_action('qm/start', __METHOD__); // Query Monitor Profiling
		$slug = self::SLUG;
		$shrt = self::SHRT;
		$script = <<<SCRIPT_END
		<script id="{$slug}-script">
		'use strict';
		const {$shrt}_menu_observer_callback = (mutationList, observer) => {
  			for (let mutation of mutationList) {
				if ((mutation.type==='attributes') && (mutation.attributeName==='class')) {
					if (mutation.target.classList.contains('hover')) {
						let search = mutation.target.querySelector('input[name=search]');
						setTimeout(s=>{s.focus();}, 100, search);
					}
				}
			}
		};
		const {$shrt}_observer = new MutationObserver({$shrt}_menu_observer_callback);
		document.querySelectorAll('.{$shrt}-post-list').forEach( post_list_menu =>{
			{$shrt}_observer.observe(post_list_menu, {attributes:true});
		});
		document.querySelectorAll('.{$shrt}-post-list input[name=search]').forEach( search_field =>{
			/* handle search input */
			search_field.addEventListener('keyup',function(evt){
				let term = this.value.toLowerCase();
				let plist = this.closest('ul.ab-submenu');
				if (!plist) return;
				plist.querySelectorAll('.{$shrt}-post a.ab-item').forEach( post_link =>{
					post_link.closest('li').style.display = (post_link.innerText.toLowerCase().indexOf(term)==-1) ? 'none' : 'flex';
				});
			});
		});
		</script>
SCRIPT_END;
		if (self::MINIMIZE_JS) {
			$script = preg_replace('/\/\*.*?\*\//s','',$script);  // remove comments 
			$script = preg_replace('/\r?\n */','',$script); // remove line breaks
			$script = preg_replace('/\t/','',$script); // remove tabs

		}
		echo $script.PHP_EOL;
		do_action('qm/stop', __METHOD__); // Query Monitor Profiling
	}

	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Emits CSS for menu styling.
	 */
	public function css(){
		$st = microtime(true);
		do_action('qm/start', __METHOD__); // Query Monitor Profiling

		// Get user's color scheme to pick some colors
		$color_scheme 	= get_user_option('admin_color', get_current_user_id());
		$color_hover	= $GLOBALS['_wp_admin_css_colors'][$color_scheme]->colors[0]??'';
		$color_line		= $GLOBALS['_wp_admin_css_colors'][$color_scheme]->colors[2]??'';
		$css_props_language = 'font-family:monospace;font-size:80%;vertical-align:super;';
		$css_props_status = 'font-family:monospace;font-style:italic;';
		/* Notes: 
		Color schemes define the colors for the backend. 
		Color scheme for frontend adminbar is always standard (dark).
		While there are proper definitions for text, highlight, icons, there's no color definition for input fields.
		So we always use dark background and light text for search input field
		*/

		
		$icon_size = '16px';
		
		$slug = self::SLUG;
		$shrt = self::SHRT;
		$style = <<<STYLE_END
			<style id="{$slug}-css">

			/* highlight row on hover */
			#wpadminbar .{$shrt} ul li:hover {background-color:{$color_hover}; }

			/* flexbox for icon+label */
			#wpadminbar .{$shrt} .{$shrt}-flexer {display:flex; flex-direction:row; align-items:center;}
			#wpadminbar .{$shrt} ul ul .{$shrt}-flexer {justify-content:space-between;}
			#wpadminbar .{$shrt} .ab-item {width:100%;}
			#wpadminbar .{$shrt} > .ab-item {width:fit-content;}

			/* search */
			#wpadminbar .{$shrt}-post-list .{$shrt}-search .ab-item {padding:0 5px;}
			#wpadminbar .{$shrt}-post-list .{$shrt}-search input {background-color:hsl(0,0%,10%); color:hsl(0,0%,70%); 
				width:-webkit-fill-available; width:-moz-available; width:fill; line-height:1; min-height:unset; 
				border: none; outline:1px dotted hsl(0,0%,70%); padding:5px;}

			/* post-list */
			#wpadminbar .{$shrt}-post-list .ab-sub-wrapper ul {max-height:80vh; overflow:hidden auto; padding-bottom:10px;}
			/* happyfiles */
			#wpadminbar .{$shrt}-post-list .{$shrt}-happyfiles-folder .ab-item {font-weight:700;background-color:dimgray;color:white;}

			/* language */
			#wpadminbar .{$shrt}-post-list .ab-sub-wrapper ul li span.language {{$css_props_language}}
			/* status */
			#wpadminbar .{$shrt}-post-list .ab-sub-wrapper ul li span.status {{$css_props_status}}

			/* action links */
			#wpadminbar .{$shrt}-post-list .{$shrt}-action-links {display:flex; flex-direction:row; margin:0 1em;}
			#wpadminbar .{$shrt}-post-list .{$shrt}-action-links a {width:{$icon_size}; height:{$icon_size}; line-height:normal; padding:0 5px; opacity:.5;}
			#wpadminbar .{$shrt}-post-list .{$shrt}-action-links a.{$shrt}-has-content {opacity:1;}

			/* icons & svg */
			#wpadminbar .{$shrt} .dashicons {font:{$icon_size} dashicons; line-height:normal; color:inherit;}
			#wpadminbar .{$shrt} svg {width:{$icon_size}; height:{$icon_size}; margin-right:6px;}
			#wpadminbar li.{$shrt} span.dashicon-before {color:inherit;}
			#wpadminbar li.{$shrt} span.dashicon-before::before {width:{$icon_size}; height:{$icon_size};  color:inherit;}

			/* divider */
			#wpadminbar :is(.{$shrt}-divider,#incspec) {margin-top: .5em; border-top:1px dotted {$color_line}; height:26px;}
			</style>
STYLE_END;
		if (self::MINIMIZE_CSS) {
			$style = preg_replace('/\/\*.*?\*\//', '', $style); // remove comments
			$style = preg_replace('/\r?\n */', '', $style); // remove line breaks
			$style = preg_replace('/\t/', '', $style); // remove tabs
		}
		echo $style.PHP_EOL;

		do_action('qm/stop', __METHOD__); // Query Monitor Profiling
		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('  %s%s::%s() Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, $et-$st));}
		self::$total_runtime += $et-$st;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Emits SVG for menu icons.
	 */
	public function svg(){
		$st = microtime(true);
		do_action('qm/start', __METHOD__); // Query Monitor Profiling

		$slug = self::SLUG;
		$shrt = self::SHRT;

		$builder_logo = '';
		if ($this->builder('Oxygen')) {
			$builder_logo = <<<LOGO_END
			<symbol id="logo-oxygen" viewBox="0 0 381 385">
				<path fill="currentColor" 
					d="M297.508,349.748 C275.443,349.748 257.556,331.86 257.556,309.796 C257.556,287.731 275.443,269.844 
					297.508,269.844 C319.573,269.844 337.46,287.731 337.46,309.796 C337.46,331.86 319.573,349.748 
					297.508,349.748 L297.508,349.748 Z M222.304,309.796 C222.304,312.039 222.447,314.247 222.639,316.441 
					C212.33,319.092 201.528,320.505 190.403,320.505 C119.01,320.505 60.929,262.423 60.929,191.031 
					C60.929,119.638 119.01,61.557 190.403,61.557 C261.794,61.557 319.877,119.638 319.877,191.031 
					C319.877,206.833 317.02,221.978 311.815,235.99 C307.179,235.097 302.404,234.592 297.508,234.592 
					C255.974,234.592 222.304,268.262 222.304,309.796 L222.304,309.796 Z M380.805,191.031 C380.805,86.042 
					295.392,0.628 190.403,0.628 C85.414,0.628 0,86.042 0,191.031 C0,296.02 85.414,381.433 190.403,381.433 
					C212.498,381.433 233.708,377.609 253.456,370.657 C265.845,379.641 281.034,385 297.508,385 C339.042,385 
					372.712,351.33 372.712,309.796 C372.712,296.092 368.988,283.283 362.584,272.219 C374.251,247.575 
					380.805,220.058 380.805,191.031 L380.805,191.031 Z"/>
			</symbol>
LOGO_END;
		}
		if ($this->builder('Bricks')) {
			$builder_logo = <<<LOGO_END
			<symbol id="logo-bricks"  viewBox="0 0 35 45">
				<path fill="currentColor" transform="translate(-16.000000, -11.000000)"
					d="M25.1875,11.34375 L25.9375,11.8125 L25.9375,24.84375 C28.5833466,23.0937413 31.5104006,22.21875 
					34.71875,22.21875 C39.3437731,22.21875 43.1770681,23.8333172 46.21875,27.0625 C49.218765,30.2916828 
					50.71875,34.2708097 50.71875,39 C50.71875,43.7500237 49.2083484,47.7291506 46.1875,50.9375 
					C43.1458181,54.1666828 39.3229397,55.78125 34.71875,55.78125 C30.6978966,55.78125 27.2604309,54.3437644 
					24.40625,51.46875 L24.40625,55 L16.03125,55 L16.03125,12.375 L25.1875,11.34375 Z M33.125,30.6875 
					C30.9166556,30.6875 29.0729241,31.4374925 27.59375,32.9375 C26.1145759,34.4791744 25.375,36.4999875 
					25.375,39 C25.375,41.5000125 26.1145759,43.5104091 27.59375,45.03125 C29.0520906,46.5520909 
					30.8958222,47.3125 33.125,47.3125 C35.4791784,47.3125 37.3854094,46.5208413 38.84375,44.9375 
					C40.2812572,43.3749922 41,41.3958453 41,39 C41,36.6041547 40.2708406,34.6145913 38.8125,33.03125 
					C37.3541594,31.4687422 35.458345,30.6875 33.125,30.6875 Z"/>
			</symbol>
LOGO_END;
		}

		$svg = <<<SVG_END
		<svg id="{$slug}-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1">
			<defs>
				<symbol id="logo-{$slug}" viewBox="0 0 24 24">
					<path fill="currentColor"
						d="M14 13.9633H16V7.96331H10V9.96331H12.5858L7.25623 15.2929L8.67044 16.7071L14 11.3775L14 13.9633Z"/>
					<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"
						d="M1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 
						1 18.0751 1 12ZM3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 
						21C7.02944 21 3 16.9706 3 12Z"/>
				</symbol>
				<symbol id="icon-{$slug}-eye" viewBox="0 0 24 24">
					<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"
						d="M16 12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12C8 9.79086 9.79086 8 12 8C14.2091 8 16 
						9.79086 16 12ZM14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 
						10C13.1046 10 14 10.8954 14 12Z"/>
			  		<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"
						d="M12 3C17.5915 3 22.2898 6.82432 23.6219 12C22.2898 17.1757 17.5915 21 12 21C6.40848 21 1.71018 17.1757 
						0.378052 12C1.71018 6.82432 6.40848 3 12 3ZM12 19C7.52443 19 3.73132 16.0581 2.45723 12C3.73132 7.94186 
						7.52443 5 12 5C16.4756 5 20.2687 7.94186 21.5428 12C20.2687 16.0581 16.4756 19 12 19Z"/>
				</symbol>
				{$builder_logo}
			</defs>
		</svg>
SVG_END;
		if (self::MINIMIZE_SVG) {
			$svg = preg_replace('/>.*?</s', '><', $svg); // remove formatting content (line breaks, tabs) between tags
			$svg = preg_replace('/\r?\n\t+/', ' ', $svg); // remove remaining line breaks and tabs
		}
		echo $svg.PHP_EOL;

		
		do_action('qm/stop', __METHOD__); // Query Monitor Profiling
		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('  %s%s::%s() Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, $et-$st));}
		self::$total_runtime += $et-$st;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Adds items to the admin bar.
	 */
	public function admin_bar_items (WP_Admin_Bar $wp_admin_bar) {
		$st = microtime(true);
		do_action('qm/start', __METHOD__.' (total)'); // Query Monitor Profiling

		// basic permission check
		if (!current_user_can('edit_posts')) return;

		// get the registered post types
		$registered_post_types = get_post_types([],'OBJECT');

		// get post statuses
		$post_statuses = get_post_statuses();


		$parent_menu_slug = null;
		if ($this->settings['collapsed_menu']??false) {
			$parent_menu_slug = self::SLUG;
			// create admin bar main item
			$label = 'Quick Nav';
			$icon = sprintf('<svg><use xlink:href="#logo-%1$s"></use></svg>', self::SLUG);
			$wp_admin_bar->add_menu([
				'id'	=> $parent_menu_slug, 
				'title'	=> sprintf('<span class="%s-flexer">%s</span>', self::SHRT, $icon.$label),
				'meta'	=> [
					'class' => self::SHRT,
					//	'title' => self::TITLE, // don't show because it's distracting
					]
				]);
				
		}
		// create sub menus for post types
		foreach ($this->settings['post_types']??[] as $ptn => $pto) {
			$stpt = microtime(true);
			do_action('qm/start', __METHOD__.' (post type "'.$ptn.'")'); // Query Monitor Profiling
			if (WP_DEBUG && self::$timing>1) {error_log(sprintf('    %s%s::%s() Post Type "%s" Loop Start', '', __CLASS__, __FUNCTION__, $ptn));}
			
			// permission check, either specific for this post type, or generic for posts
			$capability = $registered_post_types[$ptn]->capability_type ?? $ptn; 
			if (!current_user_can('edit_'.$capability.'s')) goto DONE_POSTTYPE;
			// check permission to access Oxygen templates
			if ($this->builder('Oxygen') && ($ptn=='ct_template') && function_exists('oxygen_vsb_current_user_can_access') && !oxygen_vsb_current_user_can_access()) {
				if (WP_DEBUG && self::$timing>1) {error_log(sprintf('    %s%s::%s() User has no permission to access "%s"', '', __CLASS__, __FUNCTION__, $ptn));}
				goto DONE_POSTTYPE;
			}
			// check permission to access Bricks templates
			if ($this->builder('Bricks') && ($ptn=='bricks_template') && method_exists('\Bricks\Capabilities','current_user_can_use_builder') && !call_user_func('\Bricks\Capabilities::current_user_can_use_builder')) {
				if (WP_DEBUG && self::$timing>1) {error_log(sprintf('    %s%s::%s() User has no permission to access "%s"', '', __CLASS__, __FUNCTION__, $ptn));}
				goto DONE_POSTTYPE;
			}

			// prepare label, title, icon
			$label = $registered_post_types[$ptn]->label;
			$title = $registered_post_types[$ptn]->labels->all_items ?? $label;
			$icon_label = '<span class="ab-item dashicon-before '.$registered_post_types[$ptn]->menu_icon.'">'.$label.'</span>';
			if ($this->builder('Oxygen') && $ptn == 'ct_template') { 
				//$label = 'Oxygen Templates';
				$icon = '<svg><use xlink:href="#logo-oxygen"></use></svg>';
				$icon_label = $icon.$label;
			}
			if ($this->builder('Bricks') && $ptn == 'bricks_template') { 
				//$label = 'Bricks Templates';
				$icon = '<svg><use xlink:href="#logo-bricks"></use></svg>';
				$icon_label = $icon.$label;
			}

			// create menu item for post type
			$wp_admin_bar->add_menu([
				'id'	=> sprintf('%s-%s', self::SLUG, $ptn), 
				'parent'=> $parent_menu_slug,
				'title'	=> sprintf('<span class="%s-flexer">%s</span>', self::SHRT, $icon_label),
				'href'	=> admin_url('edit.php?post_type='.$ptn),
				'meta'	=> [
					'class' => self::SHRT.' '.self::SHRT.'-post-list',
					'title' => $title, 
					]
				]);

			if ($this->settings['quick_search']??null) {
				// add search
				$search_html = sprintf('<input id="%1$s-%2$s-search" type="text" name="search" placeholder="%3$s">', self::SHRT, $ptn, __('Search'));
				$wp_admin_bar->add_node([
					'id'	=> sprintf('%s-%s-%s', self::SLUG, $ptn, 'search'), 
					'parent'=> sprintf('%s-%s', self::SLUG, $ptn),
					'group'	=> null,
					'title'	=> $search_html,
					'meta'	=> [
						//'title' => __('Search'), // distracting
						'class' => sprintf('%1$s-flexer %1$s-search', self::SHRT),
						]
					]);
				add_action('wp_footer',[$this, 'footer_assets']);
				add_action('admin_footer',[$this, 'footer_assets']);
			}


			// get config for post type list
			$pto = array_merge([ 
				'orderby'		=> self::DEFAULT_ORDERBY,
				'order'			=> self::DEFAULT_ORDER,
				'limit'			=> self::DEFAULT_LIMIT,
				'hierarchical'	=> false,
				'happyfiles'	=> false,
			],$this->settings['post_types'][$ptn] ?? []);

			$args = [
				'post_type'			=> $ptn,
				'post_status'		=> $this->settings['status'],
			];
			if (defined('POLYLANG_VERSION')) {
				$args['lang'] = ''; // Deactivate Polylang language filter, @see https://polylang.pro/doc/developpers-how-to/	
			}
			if ($pto['hierarchical']) {
				// get_pages() returns hierachical results
				$args['sort_column']	= $pto['orderby'];
				$args['sort_order']		= $pto['order'];
				$args['number']			= $pto['limit'] === '' ? -1 : $pto['limit'];
				$posts = get_pages($args);
			} else {
				// get_posts() doesn't provide hierachical results
				$args['orderby']		= $pto['orderby'];
				$args['order']			= $pto['order'];
				$args['posts_per_page']	= $pto['limit'] === '' ? -1 : $pto['limit'];
				$posts = get_posts($args);
			}
			if ($pto['happyfiles'] && in_array($ptn,$this->get_happyfiles_post_types())) {
				$happyfiles_terms = []; // to collect used happyfiles folders
				do_action('qm/start', __METHOD__.' HappyFiles (post type "'.$ptn.'")'); // Query Monitor Profiling
				// loop posts and evaluate HappyFiles terms
				foreach ($posts as $post) {
					if ($post->happyfiles_term??null) continue; // we already handled this post by an append
					// get assigned HappyFiles terms
					$happyfiles_terms_post = get_the_terms($post->ID, 'hf_cat_'.$ptn);
					if (($happyfiles_terms_post !== false) && (!is_wp_error($happyfiles_terms_post)) && count($happyfiles_terms_post)) {
						// get assigned HappyFiles term names
						foreach($happyfiles_terms_post as $idx => $happyfiles_term_post) {
							// multiple terms assigned? duplicate post
							if ($idx>0) {
								$new_post = clone $post;
								$new_post->happyfiles_term = $happyfiles_term_post; //->name;
								$posts[] = $new_post;
							} else {
								// assign HappyFiles term names to post
								$post->happyfiles_term = $happyfiles_term_post; //->name;
							}
						}
					} else {
						$post->happyfiles_term = (object)['term_id'=>0, 'name'=>' Uncategorized','parent'=>0]; // fake term
					}
					if (!in_array($post->happyfiles_term, $happyfiles_terms)) $happyfiles_terms[] = $post->happyfiles_term;
				}
				// --- care about HappyFiles Folder hierarchy
				// create simple happyfiles folder list for quick reference
				$happyfiles_terms_registry=[];
				foreach ($happyfiles_terms as $happyfiles_term) {
					$happyfiles_terms_registry[$happyfiles_term->term_id] = $happyfiles_term;
				}
				// store HappyFiles Folder path to posts
				foreach ($posts as $post) {
					$happyfiles_term = $post->happyfiles_term;
					$happyfiles_term_path = $happyfiles_term->name;
					while (($happyfiles_term->parent??0)!=0) {
						if ($happyfiles_term = $happyfiles_terms_registry[$happyfiles_term->parent]??null){
							$happyfiles_term_path = $happyfiles_term->name.' ▸ '.$happyfiles_term_path;
						}
					}
					// replace space by ­ (except leading space from ' Uncategorized') to assure correct sorting
					$post->happyfiles_term_path = preg_replace('/^([^ ]+) /','\1'."\u{00A0}",$happyfiles_term_path); 
				}

				// --- order posts by HappyFiles Folder, plus the defined sort field and order
				$orderby_key = $pto['orderby'];
				$order_key = $pto['order'];
				usort($posts,function($a,$b) use ($orderby_key, $order_key) {
					if ($order_key == 'ASC') {
						return strcmp($a->happyfiles_term_path.' '.$a->{$orderby_key},$b->happyfiles_term_path.' '.$b->{$orderby_key});
					} else {
						return strcmp($a->happyfiles_term_path.' '.$b->{$orderby_key},$b->happyfiles_term_path.' '.$a->{$orderby_key});
					}
				});
				do_action('qm/stop', __METHOD__.' HappyFiles (post type "'.$ptn.'")'); // Query Monitor Profiling
			}
			
			$hierarchy_parents = [];
			$happyfiles_previous_folder = null;
			// loop posts
			foreach ($posts as $post) {

				// permission check
				if (!current_user_can('edit_'.$capability, $post->ID)) continue;

				// hierarchy view				
				if ($pto['hierarchical']) {
					// indent post titles as necessary
					if ($post->post_parent??0) { 
						$parent_level = array_search($post->post_parent, $hierarchy_parents);
						if ($parent_level !== false) {
							$post->post_title = str_replace(' ','–',str_pad('',$parent_level+1,' ')).' '.$post->post_title;
							if ($parent_level < count($hierarchy_parents)-1) {
								$hierarchy_parents = array_slice($hierarchy_parents, 0, $parent_level+1);
							} 
							$hierarchy_parents[] = $post->ID;
						}
					} else {
						$hierarchy_parents = [$post->ID];
					}
				}
				
				$actions = array_merge([], $this->actions_wordpress($post));
				if ($pto['builder']??false) {
					if ($this->builder('Oxygen')) {$actions = array_merge($actions, $this->actions_oxygen($post));}
					if ($this->builder('Bricks')) {$actions = array_merge($actions, $this->actions_bricks($post));}
				}
				// provide view action, except for templates
				if (!in_array($ptn, ['ct_template','bricks_template'])) {
					$actions = array_merge($actions, $this->actions_frontend($post));
				} 

				// create builder links
				$action_links = '';
				foreach ($actions as $link) {
					$action_links .= sprintf('<a href="%s" title="%s" class="%s">%s</a>', 
										$link->href??'', $link->title??'', $link->has_content?self::SHRT.'-has-content':'', $link->icon??'');
				}
				if ($action_links) {
					$action_links = sprintf('<span class="%s-action-links">%s</span>', self::SHRT, $action_links);
				}

				$title_appendix = '';
				if (defined('POLYLANG_VERSION') && ($this->settings['show_language']??false)) {
					// language
					$pll_function_name = 'pll_get_post_language';
					if (function_exists($pll_function_name)) {
						$pll_post_language = call_user_func($pll_function_name, $post->ID);
						$title_appendix .= ' <span class="language">'.$pll_post_language.'</span>';
					}
				}
				// status
				$title_appendix .= ($post->post_status == 'publish') ? '' : ' <span class="status">['.$post_statuses[$post->post_status].']</span>';
				if ($pto['happyfiles'] && ($happyfiles_previous_folder != $post->happyfiles_term)) {
					// add HappyFiles Folder
					$wp_admin_bar->add_node([
						'id'	=> sprintf('%s-%s-hf-%d', self::SLUG, $ptn, $post->happyfiles_term->term_id??0), 
						'parent'=> sprintf('%s-%s', self::SLUG, $ptn),
						'group'	=> null,
						'title'	=> '▸ '.$post->happyfiles_term_path, 
						'meta'	=> [
							'title' => 'HappyFiles '.esc_html__( 'Folders', 'happyfiles' ), 
							'class' => sprintf('%1$s-flexer %1$s-happyfiles-folder', self::SHRT),
							]
						]);
					$happyfiles_previous_folder = $post->happyfiles_term;
				}
				// create menu item for post. append happyfiles term to menu ID to avoid duplicate menu entries
				$wp_admin_bar->add_menu([ 
					'id'	=> sprintf('%s-%s-%d-%d', self::SLUG, $ptn, $post->ID, $post->happyfiles_term->term_id??0), 
					'parent'=> sprintf('%s-%s', self::SLUG, $ptn),
					'group'	=> null,
					'title'	=> $post->post_title . $title_appendix, 
					'href'	=> admin_url(sprintf('post.php?post=%d&action=edit', $post->ID)),  
					'meta'	=> [
						'title' => __('Edit').' in WordPress', 
						'html'	=> $action_links,
						'class' => sprintf('%1$s-flexer %1$s-post', self::SHRT),
						]
					]);
			}
			$post_counts = wp_count_posts($ptn);
			$post_count = 0;
			foreach ($this->settings['status'] as $status) {
				$post_count += (int)$post_counts->{$status};
			}
			if ($pto['limit'] && $post_count > $pto['limit']) {
				// add a hint about limits
				$link = false;
				$hint = __('Number of entries exceeds limit.');
				if (current_user_can('manage_options')) {
					$link = admin_url('options-general.php?page='.self::SLUG);
					$hint .= ' '.__('Click to change the limit');
				}
				$wp_admin_bar->add_node([
					'id'	=> sprintf('%s-%s-%s', self::SLUG, $ptn, 'limit-hint'), 
					'parent'=> sprintf('%s-%s', self::SLUG, $ptn),
					'group'	=> null,
					'title'	=> 'Limit: '.$pto['limit'], 
					'href'	=> $link,  
					'meta'	=> [
						'title' => $hint,
						'class' => sprintf('%1$s-divider %1$s-limit', self::SHRT),
						]
					]);
			}
			DONE_POSTTYPE:
			do_action('qm/stop', __METHOD__.' (post type "'.$ptn.'")'); // Query Monitor Profiling
			$etpt = microtime(true);
			if (WP_DEBUG && self::$timing) {error_log(sprintf('    %s%s::%s() Post Type "%s" Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, $ptn, $etpt-$stpt));}
		}


		// create menu item for settings
		if (current_user_can('manage_options')) {
			$wp_admin_bar->add_menu([
				'id'	=> sprintf('%s-%s', self::SLUG, 'settings'), 
				'parent'=> self::SLUG,
				'title'	=> sprintf('<span class="%s-flexer">%s</span>', self::SHRT, __('Settings')),
				'href'	=> admin_url('options-general.php?page='.self::SLUG),
				'meta'	=> [
					'class' => sprintf('%1$s-divider', self::SHRT),
					'title' => __('Settings'), 
					]
				]);
			}
		do_action('qm/stop', __METHOD__.' (total)'); // Query Monitor Profiling
		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('  %s%s::%s() Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, $et-$st));}
		self::$total_runtime += $et-$st;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Returns action links for WordPress.
	 * @param WP_Post $post		The WP_Post to generate action links for. 
	 * @return array			An array of objects containing properties `href`, `icon`, `title`, `has_content`. 
	 */
	private function actions_wordpress(WP_Post $post):array {
		$st = microtime(true);
		$retval = [];

		$link = admin_url(sprintf('post.php?post=%d&action=edit', $post->ID));
		$icon = '<span class="dashicons dashicons-wordpress" ></span>';
		$retval[] = (object)['href'=>esc_url($link), 'icon'=>$icon, 'title'=>__('Edit').' in WordPress', 'has_content' => $post->post_content?1:0]; 

		DONE:
		$et = microtime(true);
		if (WP_DEBUG && self::$timing>1) {error_log(sprintf('      %s%s::%s(%s) Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, ($post->post_type??'').' '.($post->ID??''), $et-$st));}
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Returns action link for view in frontend.
	 * @param WP_Post $post		The WP_Post to generate action links for. 
	 * @return array			An array of objects containing properties `href`, `icon`, `title`, `has_content`. 
	 */
	private function actions_frontend(WP_Post $post):array {
		$st = microtime(true);
		$retval = [];

		$link = get_permalink($post->ID);
		$icon = sprintf('<svg><use xlink:href="#icon-%s-eye"></use></svg>',self::SLUG);
		$retval[] = (object)['href'=>esc_url($link), 'icon'=>$icon, 'title'=>__('View'), 'has_content' => 1]; 

		DONE:
		$et = microtime(true);
		if (WP_DEBUG && self::$timing>1) {error_log(sprintf('      %s%s::%s(%s) Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, ($post->post_type??'').' '.($post->ID??''), $et-$st));}
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Returns action links for Oxygen.
	 * @param WP_Post $post		The WP_Post to generate action links for. 
	 * @return array			An array of objects containing properties `href`, `icon`, `title`, `has_content`. 
	 */
	private function actions_oxygen(WP_Post $post):array {
		$st = microtime(true);
		$retval = [];
		if (!$this->builder('Oxygen')) goto DONE;
		// Oxygen hidden for this post type?
		if (get_option('oxygen_vsb_ignore_post_type_'.$post->post_type) == 'true') goto DONE;
		// permission check
		if (!function_exists('oxygen_vsb_current_user_can_access') || !oxygen_vsb_current_user_can_access()) goto DONE;

		// Oxygen pre or post 4.8.3?
		$meta_function_name = function_exists('oxy_get_post_meta') ? 'oxy_get_post_meta' : 'get_post_meta';

		// Edit a template or a post?
		if ($post->post_type == 'ct_template') {
			$parent_template_id = call_user_func($meta_function_name, $post->ID, 'ct_parent_template', true);
			$inner_content = false;
			if ($parent_template_id) {
				if ($json = call_user_func($meta_function_name, $parent_template_id, 'ct_builder_json', true)) {
					$inner_content = (strpos($json, '"name":"ct_inner_content"') !== false);
				}
				else {
					if ($shortcodes = call_user_func($meta_function_name, $parent_template_id, 'ct_builder_shortcodes', true)) {
						$inner_content = (strpos($shortcodes, '[ct_inner_content') !== false);
					}
				}
			}
			$edit_url = ct_get_post_builder_link($post->ID) . ($inner_content ? '&ct_inner=true' : '');
			$icon = '<svg><use xlink:href="#logo-oxygen"></use></svg>';
			$json = call_user_func($meta_function_name, $post->ID, 'ct_builder_json', true);

			$retval[] = (object)['href'=>esc_url($edit_url), 'icon'=>$icon, 'title'=>sprintf(__('Edit').' in %s Builder',$this->builder()), 'has_content'=>!empty($json)]; 
		} else {

			// -1: None, undefined or 0: Automatic, > 0: Manually assigned
			$template_id = (int)call_user_func($meta_function_name, $post->ID, 'ct_other_template', true);
			
			if ($template_id == -1) { // None
				// nothing to do
			} elseif ($template_id > 0) { // manually assigned 
				// get specified template post
				$template_post = get_post($template_id);
				$template_id = $template_post->ID ?? -1; // if template not found, assume None
			} else {
				// get template for single
				$template_post = ct_get_posts_template($post->ID);
				$template_id = $template_post->ID ?? -1; // if template not found, assume None
			}
			// if we have a template, check for inner content element
			$inner_content = false;
			if ($template_id && $template_id != -1) {
				if ($json = call_user_func($meta_function_name, $template_id, 'ct_builder_json', true)) {
					$inner_content = (strpos($json, '"name":"ct_inner_content"') !== false);
				}
				else { // only read shortcodes if we didn't find json; might be an old unmigrated post
					if ($shortcodes = call_user_func($meta_function_name, $template_id, 'ct_builder_shortcodes', true)) {
						$inner_content = (strpos($shortcodes, '[ct_inner_content') !== false);
					}
				}
			}
			// Check if post has Oxygen content
			$json = call_user_func($meta_function_name, $post->ID, 'ct_builder_json', true);
			// only read shortcodes if we didn't find json; might be an old unmigrated post
			$shortcodes = ''; 
			//$shortcodes = $json ? true : call_user_func($meta_function_name, $post->ID, 'ct_builder_shortcodes', true);
			$icon = '<svg><use xlink:href="#logo-oxygen"></use></svg>';
			$link = ct_get_post_builder_link($post->ID) . ($inner_content ? '&ct_inner=true' : '');
			$retval[] = (object)['href'=>esc_url($link), 'icon'=>$icon, 'title'=>sprintf(__('Edit').' in %s Builder',$this->builder()), 'has_content'=>($json||$shortcodes)]; 
		}
		DONE:
		$et = microtime(true);
		if (WP_DEBUG && self::$timing>1) {error_log(sprintf('      %s%s::%s(%s) Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, ($post->post_type??'').' '.($post->ID??''), $et-$st));}
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Returns action links for Bricks.
	 * @param WP_Post $post		The WP_Post to generate action links for. 
	 * @return array			An array of objects containing properties `href`, `icon`, `title`, `has_content`. 
	 */
	private function actions_bricks(WP_Post $post):array {
		$st = microtime(true);
		$retval = [];
		if (!$this->builder('Bricks')) goto DONE;
		// permission check
		if (!method_exists('\Bricks\Capabilities','current_user_can_use_builder') || !call_user_func('\Bricks\Capabilities::current_user_can_use_builder')) goto DONE;
		// create action link
		$icon = '<svg><use xlink:href="#logo-bricks"></use></svg>';
		$link = call_user_func('\Bricks\Helpers::get_builder_edit_link',$post->ID);
		$template_type = call_user_func('\Bricks\Templates::get_template_type',$post->ID);
		$template_data = call_user_func('\Bricks\Database::get_data',$post->ID, $template_type);
		$retval[] = (object)['href'=>esc_url($link), 'icon'=>$icon, 'title'=>sprintf(__('Edit').' in %s Builder',$this->builder()), 'has_content'=>!empty($template_data)]; 
		DONE:
		$et = microtime(true);
		if (WP_DEBUG && self::$timing>1) {error_log(sprintf('      %s%s::%s(%s) Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, ($post->post_type??'').' '.($post->ID??''), $et-$st));}
		return $retval;
	}

	//===================================================================================================================
	// UTILITIES
	//-------------------------------------------------------------------------------------------------------------------
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Returns builder name or boolean if builder matches specified name `Bricks` or `Oxygen`. 
	 * @param string $builder	[Optional] Checks if builder matches.
	 * @return mixed			Builder name, or true/false for builder name match 
	 */
	private function builder(string $builder = null) {
		$retval = '';
		if (defined('BRICKS_VERSION')) 	$retval = 'Bricks';
		if (defined('CT_VERSION')) 		$retval = 'Oxygen';
		if (isset($builder)) $retval = strtolower($retval) == strtolower($builder);
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Returns HappyFiles configured post types, or empty array if not configured
	 */
	private function get_happyfiles_post_types() {
		$retval = get_option('happyfiles_post_types',[]); // doesn't return empty array but string if none configured
		if (!is_array($retval)) {$retval = [];}
		return $retval;
	}
}

//===================================================================================================================
// Initialize
new MA_Admin_Quick_Nav();

endif;
Erstveröffentlichung: 28.01.2024 auf Code Snippet: Admin Quick Nav
magnifier