Blog

Code Snippet: Admin Quick Nav

Version: 1.1.0 (07.02.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.

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:

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, mit einer simplen Taste einen Link in einem neuen Tab zu öffnen.

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 derzeit nur Beiträge mit dem Status "Veröffentlicht".
Eine Auflistung von Einträgen mit Status "Entwurf" oder "Privat" wird in einer kommenden Version nachgereicht.

Download

Das Code Snippet steht hier zum Download zur Verfügung:

ma-admin-quick-nav.code-snippets..json
Version 1.1.0, 2024-02-07

Zur Installation und Nutzung dieser JSON Datei wird das Plugin Code Snippets oder Advanced Scripts benötigt.
Dort kann diese JSON Datei mit der Funktion "Import" hochgeladen und anschließend aktiviert werden.

Alternativ: Am Ende dieser Seite kann der vollständige Source Code des Snippets eingesehen und kopiert werden.

Im Change Log sind neue Funktionalitäten und Fehlerbehebungen dokumentiert.

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.1.0
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
Bricks		1.9.5
Oxygen		4.8.1
--------------------------------------------------------------------------------------------------------------

VERSION HISTORY:
Date		Version		Description
--------------------------------------------------------------------------------------------------------------
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:
						- PHP 8.2
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
						Tested:
						- PHP 8.1
						- WordPress 6.4.2
						- Bricks 1.9.5
						- Oxygen 4.8.1
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.1.0';

	// ===== 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 VALID_ORDERBY			= ['post_date','post_title','menu_order'];
	private const VALID_ORDER			= ['ASC','DESC'];

	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'	=> [],
		], 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
	//-------------------------------------------------------------------------------------------------------------------
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * 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'],
			]
		);
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * 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
		);

	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * 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');

		?>
		<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,
							'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 ($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'],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>
					<?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>

				<?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><?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.'-builder']??'') {
					$pto['builder'] = true;
				}
				$valid['post_types'][$ptn] = $pto;
			}
		}
		if ($menu_position = $input['menu_position'] ?? null) {
			$valid['menu_position'] = intval($menu_position);
		}
		if ($input['builder_admin_bar']??'' == '1') {
			$valid['builder_admin_bar'] = true;
		}
		if ($input['collapsed_menu']??'' == '1') {
			$valid['collapsed_menu'] = true;
		}
		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') && 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') || !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 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]??'';
		$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%;}

			/* post-list */
			#wpadminbar .{$shrt}-post-list .ab-sub-wrapper ul {max-height:80vh; overflow:hidden auto; padding-bottom:10px;}

			/* 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;
		// minimize
		$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;
		// minimize
		$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__); // 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');

		$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') && !\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 some data
			$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, 
					]
				]);

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

			if ($pto['hierarchical']) {
				// get posts hierachical
				$posts = get_pages([
					'post_type'			=> $ptn,
					'post_status'		=> 'publish',
					'sort_column'		=> $pto['orderby'],
					'sort_order'		=> $pto['order'],
					'number'			=> $pto['limit'] === '' ? -1 : $pto['limit'],
				]);
			} else {
				// get posts
				$posts = get_posts([
					'post_type'			=> $ptn,
					'post_status'		=> 'publish',
					'orderby'			=> $pto['orderby'],
					'order'				=> $pto['order'],
					'posts_per_page'	=> $pto['limit'] === '' ? -1 : $pto['limit'],
				]);
			}
			$hierarchy_parents = [];
			// 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);
				}

				// create menu item for post
				$wp_admin_bar->add_menu([
					'id'	=> sprintf('%s-%s-%s', self::SLUG, $ptn, $post->ID), 
					'parent'=> sprintf('%s-%s', self::SLUG, $ptn),
					'group'	=> null,
				//	'title'	=> sprintf('<span class="%s-flexer">%s</span>', self::SHRT, $post->post_title), 
					'title'	=> $post->post_title, 
					'href'	=> admin_url(sprintf('post.php?post=%d&action=edit', $post->ID)),  
					'meta'	=> [
						'title' => __('Edit with WordPress'), 
						'html'	=> $action_links,
						'class' => self::SHRT.'-flexer',
						]
					]);
			}
			if ($pto['limit'] && wp_count_posts($ptn)->publish > $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__); // 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 with 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;

		// Edit a template or a post?
		if ($post->post_type == 'ct_template') {
			$parent_template_id = get_post_meta($post->ID, 'ct_parent_template', true);
			$inner_content = false;
			if ($parent_template_id) {
				if ($json = get_post_meta($parent_template_id, 'ct_builder_json', true)) {
					$inner_content = (strpos($json, '"name":"ct_inner_content"') !== false);
				}
				else {
					if ($shortcodes = get_post_meta($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 = get_post_meta($post->ID, 'ct_builder_json', true);

			$retval[] = (object)['href'=>esc_url($edit_url), 'icon'=>$icon, 'title'=>'Edit with Oxygen', 'has_content'=>!empty($json)]; 
		} else {

			// -1: None, undefined or 0: Automatic, > 0: Manually assigned
			$template_id = (int)get_post_meta($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 = get_post_meta($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 = get_post_meta($template_id, 'ct_builder_shortcodes', true)) {
						$inner_content = (strpos($shortcodes, '[ct_inner_content') !== false);
					}
				}
			}
			// Check if post has Oxygen content
			$json = get_post_meta($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 : get_post_meta($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'=>'Edit with Oxygen', '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') || !\Bricks\Capabilities::current_user_can_use_builder()) goto DONE;
		// create action link
		$icon = '<svg><use xlink:href="#logo-bricks"></use></svg>';
		$link = \Bricks\Helpers::get_builder_edit_link($post->ID);
		$template_type = \Bricks\Templates::get_template_type($post->ID);
		$template_data = \Bricks\Database::get_data($post->ID, $template_type);
		$retval[] = (object)['href'=>esc_url($link), 'icon'=>$icon, 'title'=>'Edit with Bricks', '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;
	}
}

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

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