Blog

Code Snippet: Admin Quick Nav

Version: 1.3.0 (Apr 09, 2024)

Introduction

When creating and maintaining websites with WordPress, you regularly have to switch back and forth between the content types (pages, posts, custom posts). In the normal WordPress admin navigation, you first jump to the list, wait until the list is displayed and then click on the desired entry for editing.
Switching between different content requires a few clicks and also time.

Solution

The code snippet described here provides a quick navigation in the top admin bar.
The navigation is highly customizable, supports the WordPress standard content types "Posts" and "Pages" as well as your own content types (custom posts), and also offers extended functionality for the Bricks and Oxygen builders.

Settings

The configuration can be reached via Settings > Quick Nav.

Post Types

The Settings page lists all post types that also appear in the left-hand admin menu.
The post types can be selected individually to be displayed in the quick navigation.

Change the order of the post types

The icon is used to drag and drop the post types into the desired order.
Simply hold the icon with the mouse and drag it to the desired position in the list.

Change the order of the list entries

The "Sort Order" and "Order" selection lists allow you to specify the desired sorting for the entries in the quick navigation. They can be sorted by title, creation date or the order defined in the content type itself, in ascending or descending order.

Limit

The "Limit" setting can be used to limit the number of entries displayed in the quick navigation.
An empty field allows all entries in the system to be listed. A set value limits the number of entries listed in relation to the currently selected sorting.

Hierarchical representation

Some post types, such as "Pages", allow you to define a hierarchy.
With the "Show hierarchy" option, the entries in the quick navigation are displayed indented according to their hierarchy level.

General Settings

Collapse menus

The "Collapse" setting determines whether the post types are displayed next to each other in the menu bar or combined in a common "Quick Nav" menu.

Here is the comparison between the setting variants:

Admin Bar in Builder

If you have Bricks or Oxygen installed, this setting defines if the WordPress Admin Bar should also be shown in the builder.

Status

by default, only content with the status "Published" is listed in the navigation.
You can specify here whether content with the status "Draft", "Pending" and "Private" should also be displayed.
These entries are marked accordingly in the lists.

Support for Bricks and Oxygen

The snippet supports the two builders Bricks and Oxygen.

For supported content types, editing with the respective builder can be activated.

Here is a comparison of the setting options when using Bricks and Oxygen:

In the general settings there is an option to display the top admin bar in the builder as well.

This is how that looks in in Bricks or Oxygen:

If builder support is activated, the lists in the quick navigation show icons for editing in WordPress and in the respective builder.
Darker or lighter icons indicate whether content is available in WordPress or the builder.

Here for comparison:

FAQ – Frequently Asked Questions

Sure. Simply click on the heading "Pages/Posts/...".

The snippet does not provide an option for "open in new tab".
A general setting is not useful, as the decision about opening in a new or the same tab is usually made depending on the use case. And additional icons for each link would unnecessarily clutter the interface.

There is a much simpler and more flexible solution:
Every modern browser (Chrome, Firefox, Safari) allows you to open a link in a new tab.
Either via right-click > Open in new Tab, or with a simple keyboard shortcut.

Windows: Ctrl-click
macOS: Cmd-click

The quick navigation takes full account of permissions.

  • Users are only shown content types and posts that they can edit.
  • This also applies in particular to the two builders Bricks and Oxygen, which provide their own permission management.
  • The configuration of the quick navigation is reserved for users with admin rights.

With default settings, the quick navigation only lists posts with the status "Publish".
On the Settings page you can also select other status.

Download

The code snippet is available for download here:

ma-admin-quick-nav.code-snippets.json
Version 1.3.0, 2024-04-09

For installation and use of the downloaded JSON file you will need the plugin Code Snippets or Advanced Scripts.
You can install the JSON file using the "Import" function of the plugin. 
Don't forget to activate the snippet after import.

Alternative: At the end of this page you can view and copy the complete source code of the snippet.

New functionalities and bug fixes are documented in the change log.

Donation

I enjoy developing code snippets and solving requirements with them. I provide the snippets free of charge.

If you like, you can honor my many hours of work with a small coffee donation via PayPal.

  When clicking the button, a connection to PayPal is established.

Your donation will of course be taxed properly by me.

Disclaimer

I developed and tested the snippet to the best of my knowledge with the most current versions of WordPress, Bricks and Oxygen.
I provide the code snippet for free use.
I cannot give any guarantee for the functionality because of the countless possible variations in WordPress environments.
Download and use of this code snippet is at your own risk and responsibility.

Change Log

See "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.3.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, 6.4.3, 6.5
Bricks		1.9.5, 1.9.6, 1.9.7
Oxygen		4.8.1
--------------------------------------------------------------------------------------------------------------

VERSION HISTORY:
Date		Version		Description
--------------------------------------------------------------------------------------------------------------
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:
						- 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.3.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 DEFAULT_STATUS		= ['publish'];
	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'	=> [],
			'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
	//-------------------------------------------------------------------------------------------------------------------
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * 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');

		// 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,
							'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>
						<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><?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;
		}
		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 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_status = 'font-family:monospace;font-style:italic;';
		
		$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;}

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

		// 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, 
					]
				]);

			// 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_pages() returns hierachical results
				$args = [
					'post_type'			=> $ptn,
					'post_status'		=> $this->settings['status'],
					'sort_column'		=> $pto['orderby'],
					'sort_order'		=> $pto['order'],
					'number'			=> $pto['limit'] === '' ? -1 : $pto['limit'],
				];
				if (defined('POLYLANG_VERSION') ) {
					$args['lang'] = ''; // Deactivate Polylamg language filter, @see https://polylang.pro/doc/developpers-how-to/	
				}
				$posts = get_pages($args);
			} else {
				// get_posts() doesn't provide hierachical results
				$args = [
					'post_type'			=> $ptn,
					'post_status'		=> $this->settings['status'],
					'orderby'			=> $pto['orderby'],
					'order'				=> $pto['order'],
					'posts_per_page'	=> $pto['limit'] === '' ? -1 : $pto['limit'],
				];
				if (defined('POLYLANG_VERSION') ) {
					$args['lang'] = ''; // Deactivate Polylang language filter, @see https://polylang.pro/doc/developpers-how-to/	
				}
				$posts = get_posts($args);
			}
			$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);
				}

				$title_appendix = ($post->post_status == 'publish') ? '' : ' <span class="status">['.$post_statuses[$post->post_status].']</span>';
				// 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 . $title_appendix, 
					'href'	=> admin_url(sprintf('post.php?post=%d&action=edit', $post->ID)),  
					'meta'	=> [
						'title' => __('Edit').' in 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').' 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;

		// 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'=>sprintf(__('Edit').' in %s Builder',$this->builder()), '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'=>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;
	}
}

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

endif;
First published: Jan 28, 2024 on Code Snippet: Admin Quick Nav
magnifier