Code Snippet: Admin Quick Nav

Einführung
Bei der Erstellung und Pflege von Websites mit WordPress muss man regelmäßig zwischen den Inhaltstypen (Seiten, Beiträge, Custom Posts) hin- und herspringen. In der normalen WordPress Admin Navigation springt man hierzu zunächst auf die Liste, wartet bis die Liste angezeigt wird, und klickt dann auf den gewünschten Eintrag zur Bearbeitung.
Der Wechsel zwischen verschiedenen Inhalten benötigt dadurch einige Klicks und auch Zeit.
Lösung
Das hier beschriebene Code Snippet stellt eine Schnellnavigation in der oberen Admin-Leiste zur Verfügung.
Die Navigation ist sehr individuell konfigurierbar, unterstützt neben den WordPress Standard-Inhaltstypen "Beiträge" und "Seiten" auch eigene Inhaltstypen (Custom Posts), und bietet auch erweiterte Funktionalität für die Builder Bricks und Oxygen.
Einstellungen
Die Konfiguration erfolgt über Einstellungen > Quick Nav in der deutschen WordPress Oberfläche, bzw. Settings > Quick Nav in der englischen.

Inhaltstypen
Es werden alle Inhaltstypen aufgelistet, die auch in der linken Admin-Leiste auftauchen.
Die Inhaltstypen können individuell zur Anzeige in der Schnellnavigation ausgewählt werden.
Reihenfolge ändern
Mit dem Symbol können die Inhaltstypen per Drag & Drop in die gewünschte Reihenfolge gebracht werden.
Einfach das Symbol mit der Maus halten und an die gewünschte Stelle in der Liste ziehen.
Sortierung der Einträge
Die Auswahllisten "Sort Order" und "Order" erlauben die Festlegung der gewünschten Sortierung für die Einträge in der Schnellnavigation. Es kann nach Titel, Erstellungsdatum, oder auch der im Inhaltstyp selbst festgelegten Reihenfolge, auf- oder absteigend sortiert werden.
Limit
Mit der Einstellung "Limit" kann die Anzahl der in der Schnellnavigation angezeigten Einträge begrenzt werden.
Ein leeres Feld erlaubt die Auflistung aller Einträge im System. Ein gesetzter Wert begrenzt die Anzahl der gelisteten Einträge, und zwar in Bezug auf die aktuell gewählte Sortierung.
Hierarchische Darstellung
Einige Inhaltstypen, wie beispielsweise "Seiten", erlauben die Festlegung einer Hierarchie.
Mit der Option "Show hierarchy" werden die Einträge in der Schnellnavigation entsprechend ihrer Hierarchie-Ebene eingerückt dargestellt.
HappyFiles Ordner
Falls Sie das Plugin HappyFiles installiert haben, um Ihre Inhaltstypen zu organisieren, können Sie mit der Einstellung "HappyFiles Folders" diese Organisations-Ordner auch in den Listen der Schnellnavigation anzeigen lassen.
Diese Einstellung steht für Inhaltstypen zur Verfügung, die in den HappyFiles Einstellungen aktiviert wurden.
Die Kombination "HappyFiles Folders" und "Hierarchische Darstellung" wird derzeit nicht zuverlässig unterstützt.

Allgemeine Einstellungen
Menü verschlanken
Die Einstellung "Collapse" legt fest, ob die Inhaltstypen nebeneinander in der Menüzeile angezeigt werden, oder in einem gemeinsamen Menü "Quick Nav" zusammen gefasst werden.
Hier der Vergleich zwischen den Einstellungsvarianten:


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

Bei der Suche muss die Groß-/Kleinschreibung nicht beachtet werden.
Die Suche berücksichtigt auch das Sprachkennzeichen und den Status, falls diese angezeigt werden.
Add new
Die Einstellung "Enable 'Add new …'" fügt den Listen einen Link zum Erstellen eines neuen Eintrages des jeweiligen Inhaltstyps hinzu.
Sprache anzeigen
Falls sie Polylang installiert haben, kann in den Listen optional die Sprache angezeigt werden.

Admin-Bar in Builder
Falls Sie Bricks oder Oxygen installiert haben, können Sie mit dieser Einstellung festlegen, ob die obere WordPress Admin-Leiste auch im Builder angezeigt werden soll.
Status
In der Standard-Konfiguration werden nur Inhalte mit Status "Veröffentlicht" in der Navigation aufgelistet.
Sie können hier festlegen, ob auch Inhalte mit Status "Entwurf" (draft), "Ausstehende Überprüfung" (pending) und "Privat" (private) angezeigt werden sollen.
In den Listen werden diese Einträge entsprechend gekennzeichnet.

Unterstützung von Bricks und Oxygen
Das Snippet unterstützt die beiden Builder Bricks und Oxygen.
Für unterstützte Inhaltstypen kann die Bearbeitung mit dem jeweiligen Builder aktiviert werden.
Hier der Vergleich der Einstellungsmöglichkeiten bei Verwendung von Bricks und Oxygen:


In den allgemeinen Einstellungen gibt es die Option, die obere Admin Leiste auch im Builder anzuzeigen.
So sieht das in Bricks bzw. Oxygen aus:


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


FAQ – Häufig gestellte Fragen
Download
Dieser JSON-Download kann direkt in die Plugins Code Snippets oder Advanced Scripts importiert werden. Vergiss nicht, das Snippet nach dem Import zu aktivieren.
Falls Du ein anderes Snippet Plugin verwendest, kannst Du stattdessen den Source Code kopieren und damit selbst ein neues Snippet anlegen.
ma-admin-quick-nav.code-snippets.json
Version 1.7.3, 2024-11-13
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.
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.7.3 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.7 Bricks 1.9.5 ... 1.10.3 Oxygen 4.8.1 ... 4.9 -------------------------------------------------------------------------------------------------------------- VERSION HISTORY: Date Version Description -------------------------------------------------------------------------------------------------------------- 2024-11-13 1.7.3 Compatibility WordPress 6.7: - Changed permission check based on cap instead of capability_type (which is now array) Fixes: - CSS adaptions for Advanced Themer 2.9 on Bricks sites: - Padding for search field on AT settings page 2024-10-28 1.7.2 Fixes: - Fix for quick search in Builder and frontend 2024-10-11 1.7.1 Fixes: - CSS adaptions for Advanced Themer 2.9 on Bricks sites: - Additional spacing at top for full screen modes with Admin bar enabled - Padding for search field 2024-10-06 1.7.0 New Features: - Added "Add new ..." link (Requested by Jennifer Cisneros) 2024-08-23 1.6.3 Fixes: - CSS adaptions for Admin Bar in Oxygen 4.9 Builder 2024-07-28 1.6.2 Fixes: - When building menu sections/items, skip unregistered post types. This situation can occur if a post type was configured in Quick Nav settings, but related plugin has been deactivated. 2024-07-22 1.6.1 Fixes: - Prevent fatal errors if no HappyFiles post types have been configured (Thanks to Pascal Dörflinger for reporting) 2024-07-17 1.6.0 New Features: - Support for HappyFiles folders (Requested by Sebastian Berger) 2024-07-07 1.5.0 New Features: - Quick Search (Requested by John Kirker) Changes: - Optimized post limit detection (now checking all selected states) 2024-05-23 1.4.0 New Features: - Optional display of Polylang language in Quick Nav lists 2024-05-21 1.3.1 Changes: - Compatibility to Oxygen 4.8.3 with changed post meta keys 2024-04-09 1.3.0 New Features: - Support for selecting status of posts to be listed 2024-04-05 1.2.0 Changes: - Disabled Polylang language filter to show all languages, also in frontend - Changed some direct Bricks function calls to call_user_func() to avoid warnings in code editor on non-Bricks setups 2024-02-07 1.1.0 New Features: - New action "View" (except for Oxygen/Bricks templates) 2024-02-03 1.0.2 Fixes: - Corrected semantical issue by moving svg symbols from head to footer - Corrected action icon width. 2024-01-30 1.0.1 Fixes: - Added additional permission check for Bricks templates Tested: 2024-01-28 1.0.0 Changes: - Stretch sub menu item links - Improved translation - Empty limit now means unlimited Inspired by Michael Pucher: - Sortable post types - Collapsed or individual menus for post types - Allow sorting by menu_order - Removed title attribute from top menu because that tooltip is distracting 2024-01-26 0.0.1 Internal test version supporting Oxygen and Bricks 2024-01-20 0.0.0 Development start -------------------------------------------------------------------------------------------------------------- Icon Arrow: https://css.gg/arrow-top-right-o Icon Eye: https://css.gg/eye */ if (!class_exists('MA_Admin_Quick_Nav')) : class MA_Admin_Quick_Nav { const TITLE = 'Quick Nav'; const SLUG = 'ma-admin-quick-nav'; const SHRT = 'maqn'; const VERSION = '1.7.3'; // ===== CONFIGURATION ============================================================================================== public static $timing = false; // Write timing info to wordpress debug.log if WP_DEBUG also enabled. // false/0: Disabled, true/1: Enabled, 2: Extended. public static $expert_mode = false; // enables some additional settings for the experts, like menu_position // ===== INTERNAL =================================================================================================== private const DEFAULT_MENU_POSITION = 80; private const DEFAULT_POST_TYPES = ['page'=>[],'post'=>[]]; private const DEFAULT_LIMIT = 20; private const DEFAULT_ORDERBY = 'post_date'; private const DEFAULT_ORDER = 'DESC'; private const DEFAULT_STATUS = ['publish']; private const VALID_ORDERBY = ['post_date','post_title','menu_order']; private const VALID_ORDER = ['ASC','DESC']; private const MINIMIZE_CSS = true; private const MINIMIZE_SVG = true; private const MINIMIZE_JS = true; public static $total_runtime = 0; private $settings = null; //------------------------------------------------------------------------------------------------------------------- function __construct() { $st = microtime(true); // read settings from db and merge to default settings $this->settings = array_merge([ 'menu_position' => self::DEFAULT_MENU_POSITION, 'post_types' => [], 'status' => self::DEFAULT_STATUS, ], get_option(self::SLUG, ['post_types'=>self::DEFAULT_POST_TYPES])); // emit CSS for backend and frontend add_action('admin_enqueue_scripts', [$this, 'css']); add_action('wp_enqueue_scripts', [$this, 'css']); // emit SVG for backend and frontend add_action('admin_footer', [$this, 'svg']); add_action('wp_footer', [$this, 'svg']); add_action('wp_footer', [$this, 'debug_info']); // emit search script if ($this->settings['quick_search']??null) { add_action('wp_footer',[$this, 'search_script']); add_action('admin_footer',[$this, 'search_script']); } add_action('admin_enqueue_scripts', [$this, 'admin_scripts']); // display menu only for wp-admin area if (is_admin()) { // register admin menus (must be registered with hook, otherwise appears on top of menu) add_action('admin_menu', [$this, 'admin_menu'], 100 ); // register settings add_action('admin_init', [$this, 'settings_register'] ); // TODO: custom translations //add_action('after_setup_theme',[$this,'l10n']); } // builder admin bar? if ($this->settings['builder_admin_bar']??false) { // must hook before init add_action('after_setup_theme',[$this,'builder_admin_bar']); } // register admin bar menu items add_action('admin_bar_menu',[$this, 'admin_bar_items'], intval($this->settings['menu_position']??self::DEFAULT_MENU_POSITION)); // add a handler for logging total runtime add_action('shutdown', [$this,'total_runtime']); $et = microtime(true); if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s() Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, $et-$st));} self::$total_runtime += $et-$st; } //------------------------------------------------------------------------------------------------------------------- /** * Logs total timing. */ public static function total_runtime(){ if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s() Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, self::$total_runtime));} } //------------------------------------------------------------------------------------------------------------------- public function l10n(){ global $l10n; // TODO: custom translations } //=================================================================================================================== // SETTINGS //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- /** * Creates the Settings menu. */ public function admin_menu(){ add_options_page( self::TITLE.' '.__('Settings'), // page title self::TITLE, // menu title 'manage_options', // capabilitiy self::SLUG, // menu slug [$this, 'settings'] // function ); } //------------------------------------------------------------------------------------------------------------------- /** * Register the settings and validation method. */ public function settings_register() { register_setting( self::SLUG, // option group self::SLUG, // option name [ 'type' => 'array', 'sanitize_callback' => [$this, 'settings_validate'], ] ); } //------------------------------------------------------------------------------------------------------------------- /** * Outputs the settings page. */ public function settings() { // check user capabilities if (!current_user_can('manage_options')) return; // show error/update messages settings_errors(self::SLUG . '_messages'); // get the registered post types $registered_post_types = get_post_types([],'OBJECT'); // get the registered post statuses $post_statuses = get_post_statuses(); ?> <div class="wrap <?php echo self::SLUG;?>-settings" id="<?php echo self::SLUG;?>-settings"> <h1><?php echo esc_html(get_admin_page_title()); ?></h1> <style> .ptl {border:1px solid lightgray; border-collapse:collapse;} .ptl thead {background-color:lightgray;} .ptl tr {border-bottom:1px solid lightgray;} .ptl :where(th,td) {text-align:left; vertical-align:top; padding:5px;} .ptl :where(th,td).sort {width:20px;} .ptl :where(th,td).sort svg.sort-handle {color:gray; width:1em; height:1em; padding-top:5px; cursor:ns-resize;} .ptl input[type='number'] {width:10ch;} .ptl .details * {visibility:hidden;} .ptl .active .details * {visibility:visible;} .general input[type='number'] {width:10ch;} </style> <form method="post" action="options.php"> <?php settings_fields(self::SLUG); ?> <h2><?php _e('Post Types'); ?></h2> <p>Enable Admin Bar Menu for Post Types:</p> <table class="ptl" role="presentation"> <thead> <tr> <th class="sort"></th> <th><?php _e('Post Type'); ?></th> <th><?php _e('Sort Order:'); ?></th> <th><?php _e('Order'); ?></th> <th><?php _ex('Count','Number/count of items'); ?></th> <th><?php _e('Advanced Options'); ?></th> </tr> </thead> <tbody> <?php // get post types from settings plus other registered post types $post_types = array_unique(array_merge(array_keys($this->settings['post_types']), array_keys(wp_list_pluck($registered_post_types,'name')))); foreach ($post_types as $post_type): // check if configured post type is still a registered post type if (!$reg_post_type = $registered_post_types[$post_type]) continue; // skip post types that are not configured to be listed in nav menus if (!$reg_post_type->show_in_nav_menus) continue; // loop valid post types $ptn = $reg_post_type->name; $ptl = $reg_post_type->label; $pts = $this->settings['post_types']??[]; $pto = array_merge([ 'orderby' => self::DEFAULT_ORDERBY, 'order' => self::DEFAULT_ORDER, 'limit' => self::DEFAULT_LIMIT, 'hierarchical' => false, 'happyfiles' => false, 'builder' => false, ],$pts[$ptn] ?? []); $label = $ptl . ' (<code>'.$ptn.'</code>)'; // label adaption for special cases if ($ptn == 'ct_template') {$label = 'Oxygen '.$label;} ?> <tr class="post_type_row <?php echo in_array($ptn,array_keys($pts)) ? 'active' : '';?>"> <td class="sort"> <svg class="sort-handle"><use xlink:href="#icon-sort"></use></svg> </td> <td> <input type="checkbox" name="post_types[]" id="post_type_<?php echo $ptn;?>" value="<?php echo $ptn;?>" <?php checked(in_array($ptn,array_keys($pts)), true);?>> <label for="post_type_<?php echo $ptn;?>"><?php echo $label;?></label> </td> <td class="details"> <select name="<?php echo $ptn;?>-orderby" id="<?php echo $ptn;?>-orderby"> <option value="post_date" <?php selected($pto['orderby'], 'post_date'); ?>><?php _e('Created');?></option> <option value="post_title" <?php selected($pto['orderby'], 'post_title'); ?>><?php _e('Title');?></option> <option value="menu_order" <?php selected($pto['orderby'], 'menu_order'); ?>><?php _e('Menu Order');?></option> </select> </td> <td class="details"> <select name="<?php echo $ptn;?>-order" id="<?php echo $ptn;?>-order"> <option value="ASC" <?php selected($pto['order'], 'ASC'); ?>><?php _e('Ascending');?></option> <option value="DESC" <?php selected($pto['order'], 'DESC'); ?>><?php _e('Descending');?></option> </select> </td> <td class="details"> <input type="number" name="<?php echo $ptn;?>-limit" id="<?php echo $ptn;?>-limit" value="<?php echo $pto['limit'];?>"> </td> <td class="details"> <?php if ($reg_post_type->hierarchical === true): $hint = 'Show hierarchy by indenting list'; ?> <input type="checkbox" name="<?php echo $ptn;?>-hierarchical" id="<?php echo $ptn;?>-hierarchical" title="<?php echo $hint;?>" value="1" <?php checked($pto['hierarchical'],true);?>> <label for="<?php echo $ptn;?>-hierarchical" title="<?php echo $hint;?>"><?php _e('Show hierarchy');?></label> <br> <?php endif; ?> <?php if (defined('HAPPYFILES_VERSION') && in_array($ptn, $this->get_happyfiles_post_types())): $hint = 'Group by HappyFiles folders'; ?> <input type="checkbox" name="<?php echo $ptn;?>-happyfiles" id="<?php echo $ptn;?>-happyfiles" title="<?php echo $hint;?>" value="1" <?php checked($pto['happyfiles'],true);?>> <label for="<?php echo $ptn;?>-happyfiles" title="<?php echo $hint;?>"><?php echo 'HappyFiles '.esc_html__( 'Folders', 'happyfiles' );?></label> <br> <?php endif; ?> <?php if ($this->builder()): $hint = 'Show icon to allow editing in '.$this->builder(); ?> <input type="checkbox" name="<?php echo $ptn;?>-builder" id="<?php echo $ptn;?>-builder" title="<?php echo $hint;?>" value="1" <?php checked($pto['builder']??false,true);?>> <label for="<?php echo $ptn;?>-builder" title="<?php echo $hint;?>"><?php printf(__('Edit').' in %s Builder',$this->builder());?></label> <?php endif; ?> </td> </tr> <?php endforeach; ?> </tbody> </table> <script> document.querySelectorAll('input[name^="post_types[]"]').forEach($c=>{ $c.addEventListener('change',($e)=>{ let $r = $e.currentTarget.closest('.post_type_row'); if ($e.currentTarget.checked) {$r.classList.add('active');} else {$r.classList.remove('active');} }); }); jQuery(document).ready(function(){ jQuery('table.ptl tbody').sortable({ handle: '.sort-handle', cursor: 'ns-resize', axis: 'y', }); }); </script> <h2><?php _e('General'); ?></h2> <div class="general"> <?php if (self::$expert_mode): ?> <div> <?php $hint = sprintf('Set the position for the %s menu(s)', self::TITLE); ?> <label for="menu_position" title="<?php echo $hint;?>"><?php printf(__('Menu Location'), self::TITLE);?></label> <input type="number" name="menu_position" id="menu_position" value="<?php echo $this->settings['menu_position']?>"> </div> <?php endif; ?> <div> <?php $hint = sprintf('Collapse all post type menus into one menu "%s"', self::TITLE); ?> <input type="checkbox" name="collapsed_menu" id="collapsed_menu" title="<?php echo $hint;?>" value="1" <?php checked($this->settings['collapsed_menu']??false,true);?>> <label for="collapsed_menu" title="<?php echo $hint;?>"><?php printf(__('Collapse menu').' in "%s"', self::TITLE);?></label> </div> <div> <?php $hint = sprintf('Enable quick search bar'); ?> <input type="checkbox" name="quick_search" id="quick_search" title="<?php echo $hint;?>" value="1" <?php checked($this->settings['quick_search']??false,true);?>> <label for="quick_search" title="<?php echo $hint;?>"><?php esc_html_e('Enable Quick Search');?></label> </div> <div> <?php $hint = esc_html('Enable "Add new..."'); ?> <input type="checkbox" name="add_new" id="add_new" title="<?php echo $hint;?>" value="1" <?php checked($this->settings['add_new']??false,true);?>> <label for="add_new" title="<?php echo $hint;?>"><?php esc_html_e('Enable "Add new..."');?></label> </div> <?php if (defined('POLYLANG_VERSION')): ?> <div> <?php $hint = sprintf('Show language for items if defined'); ?> <input type="checkbox" name="show_language" id="show_language" title="<?php echo $hint;?>" value="1" <?php checked($this->settings['show_language']??false,true);?>> <label for="show_language" title="<?php echo $hint;?>"><?php printf(__('Show language').' in "%s"', self::TITLE);?></label> </div> <?php endif; ?> <?php if ($this->builder()): ?> <div> <?php $hint = sprintf('Enable admin bar in %s builder',$this->builder()); ?> <input type="checkbox" name="builder_admin_bar" id="builder_admin_bar" title="<?php echo $hint;?>" value="1" <?php checked($this->settings['builder_admin_bar']??false,true);?>> <label for="builder_admin_bar" title="<?php echo $hint;?>"><?php printf(__('Admin Bar in %s Builder'),$this->builder());?></label> </div> <?php endif; ?> <div> <h3>Status</h3> <?php foreach ($post_statuses as $status => $status_label): ?> <?php $hint = sprintf('List items with status "%s"', $status_label); ?> <input type="checkbox" name="status[]" title="<?php echo $hint;?>" value="<?php echo $status;?>" <?php checked(in_array($status,$this->settings['status']),true);?>> <?php echo $status_label;?><br/> <?php endforeach;?> </div> </div> <?php submit_button(); ?> </form> </div> <div style="margin-top:3em;border-top:1px solid lightgray;font-size:70%;"> © 2024, <a href="https://www.altmann.de/" target="_blank">Matthias Altmann</a> • <?php echo basename(__FILE__) == 'ma-admin-quick-nav.php' ? 'Plugin' : 'Code Snippet';?> Version <?php echo self::VERSION;?> • <a href="https://www.altmann.de/en/blog-en/code-snippet-admin-quick-nav/" target="_blank">Downloads & Documentation</a> </div> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1"> <defs> <symbol id="icon-sort" viewBox="0 0 500 500"> <title><?php _e('Reorder'); ?></title> <path fill="currentColor" d="M285.714 303.571q0 7.254-5.301 12.556l-125 125q-5.301 5.301-12.556 5.301t-12.556-5.301l-125-125q-5.301-5.301-5.301-12.556t5.301-12.556 12.556-5.301h250q7.254 0 12.556 5.301t5.301 12.556zM285.714 196.429q0 7.254-5.301 12.556t-12.556 5.301h-250q-7.254 0-12.556-5.301t-5.301-12.556 5.301-12.556l125-125q5.301-5.301 12.556-5.301t12.556 5.301l125 125q5.301 5.301 5.301 12.556z" /> </symbol> </defs> </svg> <?php } //------------------------------------------------------------------------------------------------------------------- /** * Validates the submitted settings. */ public function settings_validate() { $input = $_REQUEST; $valid = ['post_types'=>[]]; // check selected post types $allowed_post_types = wp_list_pluck(get_post_types([],'OBJECT'),'name'); foreach(($input['post_types']??[]) as $ptn) { if (in_array($ptn, $allowed_post_types)) { $pto = []; if (in_array($input[$ptn.'-orderby']??'',self::VALID_ORDERBY)) { $pto['orderby'] = $input[$ptn.'-orderby']; } if (in_array($input[$ptn.'-order']??'',self::VALID_ORDER)) { $pto['order'] = $input[$ptn.'-order']; } $limit = $input[$ptn.'-limit'] ?? null; if (isset($limit)) { $pto['limit'] = ''; if ($limit !== '') { $limit = intval($limit); if ($limit !== 0) { $pto['limit'] = $limit; } } } if ($input[$ptn.'-hierarchical']??'') { $pto['hierarchical'] = true; } if ($input[$ptn.'-happyfiles']??'') { $pto['happyfiles'] = true; } if ($input[$ptn.'-builder']??'') { $pto['builder'] = true; } $valid['post_types'][$ptn] = $pto; } } if ($menu_position = $input['menu_position'] ?? null) { $valid['menu_position'] = intval($menu_position); } if ($input['quick_search']??'' == '1') { $valid['quick_search'] = true; } if ($input['add_new']??'' == '1') { $valid['add_new'] = true; } if ($input['collapsed_menu']??'' == '1') { $valid['collapsed_menu'] = true; } if ($input['show_language']??'' == '1') { $valid['show_language'] = true; } if ($input['builder_admin_bar']??'' == '1') { $valid['builder_admin_bar'] = true; } if ($input['status']??null) { $valid['status'] = $input['status']; } return $valid; } //=================================================================================================================== // ADMIN BAR //------------------------------------------------------------------------------------------------------------------- /** * Enables admin bar in builders. */ public function builder_admin_bar() { if ($this->builder('Oxygen')) { // disable Oxygen's admin bar removal remove_action('init','ct_hide_admin_bar'); // adds Oxygen builder style adaptions if necessary add_action('wp_enqueue_scripts', [$this,'builder_css_oxygen']); add_action('init', function(){ // don't show admin bar in builder iframe if (defined('OXYGEN_IFRAME')) { add_filter('show_admin_bar','__return_false'); } }); } if ($this->builder('Bricks')) { // adds Bricks builder style adaptions if necessary add_action('wp_head', [$this,'builder_css_bricks']); // show admin bar in Bricks add_action('init', function () { // only if Bricks editor active if (function_exists('bricks_is_builder_main') && call_user_func('bricks_is_builder_main')) { add_filter('show_admin_bar', '__return_true'); } }); } } //------------------------------------------------------------------------------------------------------------------- /** * Emits CSS to adapt Oxygen builder. */ public function builder_css_oxygen() { // only for loading Oxygen UI, but not in preview iframe if (!defined('SHOW_CT_BUILDER')) return; if (defined('OXYGEN_IFRAME')) return; // Oxygen pre 4.9 if (version_compare(CT_VERSION,'4.9','<')) { $builder_css = <<<STYLE_END /* ma-admin-quick-nav for Oxygen .. 4.8.3 */ /* 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; } if (version_compare(CT_VERSION,'4.9','>=')) { $builder_css = <<<STYLE_END /* ma-admin-quick-nav for Oxygen 4.9 .. */ #oxygen-ui { position: fixed; width: 100%; } #oxygen-ui #oxygen-topbar .oxygen-zoom-control .oxygen-zoom-icon { height: 40px; } #oxygen-ui #oxygen-sidebar { height: calc(100vh - 36px - var(--wp-admin--admin-bar--height,0px)); } #oxygen-ui .oxygen-add-section-library-flyout-panel { top: calc(58px + var(--wp-admin--admin-bar--height,0px)); height: calc(100vh - 58px - var(--wp-admin--admin-bar--height,0px)); } #oxygen-ui .ct-panel-elements-managers, #oxygen-ui .oxygen-global-settings { top: calc(40px + var(--wp-admin--admin-bar--height,0px)); } #oxygen-ui .oxygen-add-sidebar { height: calc(100% - var(--wp-admin--admin-bar--height,0px)); } body.admin-bar #ct-viewport-container { position: fixed; margin-top: 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; $slug = self::SLUG; $shrt = self::SHRT; // checked with Bricks 1.9.5 $builder_css = <<<STYLE_END /* ma-admin-quick-nav for Bricks */ 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)); } /* adaptions for Advanced Themer 2.9 */ html body[data-superpower-css="true"] [data-controlkey].full-screen, html #advancedCSSUI__panel.full-screen { top: var(--wp-admin--admin-bar--height,0px); } #wpadminbar .{$shrt}-post-list .{$shrt}-search input {padding: 5px !important;} 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 script for search */ public function search_script(){ do_action('qm/start', __METHOD__); // Query Monitor Profiling $slug = self::SLUG; $shrt = self::SHRT; $script = <<<SCRIPT_END <script id="{$slug}-script"> 'use strict'; document.addEventListener('DOMContentLoaded',(evt) => { {$shrt}_search_init(); }); function {$shrt}_search_init() { const {$shrt}_menu_observer_callback = (mutationList, observer) => { for (let mutation of mutationList) { if ((mutation.type==='attributes') && (mutation.attributeName==='class')) { if (mutation.target.classList.contains('hover')) { let search = mutation.target.querySelector('input[name=search]'); setTimeout(s=>{s.focus();}, 100, search); } } } }; const {$shrt}_observer = new MutationObserver({$shrt}_menu_observer_callback); document.querySelectorAll('.{$shrt}-post-list').forEach( post_list_menu =>{ {$shrt}_observer.observe(post_list_menu, {attributes:true}); }); document.querySelectorAll('.{$shrt}-post-list input[name=search]').forEach( search_field =>{ /* handle search input */ search_field.addEventListener('keyup',function(evt){ let term = this.value.toLowerCase(); let plist = this.closest('ul.ab-submenu'); if (!plist) return; plist.querySelectorAll('.{$shrt}-post a.ab-item').forEach( post_link =>{ post_link.closest('li').style.display = (post_link.innerText.toLowerCase().indexOf(term)==-1) ? 'none' : 'flex'; }); }); }); } </script> SCRIPT_END; if (self::MINIMIZE_JS) { $script = preg_replace('/\/\*.*?\*\//s','',$script); // remove comments $script = preg_replace('/\r?\n */','',$script); // remove line breaks $script = preg_replace('/\t/','',$script); // remove tabs } echo $script.PHP_EOL; do_action('qm/stop', __METHOD__); // Query Monitor Profiling } //------------------------------------------------------------------------------------------------------------------- /** * Emits CSS for menu styling. */ public function css(){ $st = microtime(true); do_action('qm/start', __METHOD__); // Query Monitor Profiling // Get user's color scheme to pick some colors $color_scheme = get_user_option('admin_color', get_current_user_id()); $color_hover = $GLOBALS['_wp_admin_css_colors'][$color_scheme]->colors[0]??''; $color_line = $GLOBALS['_wp_admin_css_colors'][$color_scheme]->colors[2]??''; $css_props_language = 'font-family:monospace;font-size:80%;vertical-align:super;'; $css_props_status = 'font-family:monospace;font-style:italic;'; /* Notes: Color schemes define the colors for the backend. Color scheme for frontend adminbar is always standard (dark). While there are proper definitions for text, highlight, icons, there's no color definition for input fields. So we always use dark background and light text for search input field */ $icon_size = '16px'; $slug = self::SLUG; $shrt = self::SHRT; $style = <<<STYLE_END <style id="{$slug}-css"> /* highlight row on hover */ #wpadminbar .{$shrt} ul li:hover {background-color:{$color_hover}; } /* flexbox for icon+label */ #wpadminbar .{$shrt} .{$shrt}-flexer {display:flex; flex-direction:row; align-items:center;} #wpadminbar .{$shrt} ul ul .{$shrt}-flexer {justify-content:space-between;} #wpadminbar .{$shrt} .ab-item {width:100%;} #wpadminbar .{$shrt} > .ab-item {width:fit-content;} /* search */ #wpadminbar .{$shrt}-post-list .{$shrt}-search .ab-item {padding:0 5px;} #wpadminbar .{$shrt}-post-list .{$shrt}-search input {background-color:hsl(0,0%,10%); color:hsl(0,0%,70%); width:-webkit-fill-available; width:-moz-available; width:fill; line-height:1; min-height:unset; border: none; outline:1px dotted hsl(0,0%,70%); padding:5px !important;} /* add new */ #wpadminbar .{$shrt}-post-list .{$shrt}-add_new {border-bottom:1px dotted {$color_line};} /* post-list */ #wpadminbar .{$shrt}-post-list .ab-sub-wrapper ul {max-height:80vh; overflow:hidden auto; padding-bottom:10px;} /* happyfiles */ #wpadminbar .{$shrt}-post-list .{$shrt}-happyfiles-folder .ab-item {font-weight:700;background-color:dimgray;color:white;} /* language */ #wpadminbar .{$shrt}-post-list .ab-sub-wrapper ul li span.language {{$css_props_language}} /* status */ #wpadminbar .{$shrt}-post-list .ab-sub-wrapper ul li span.status {{$css_props_status}} /* action links */ #wpadminbar .{$shrt}-post-list .{$shrt}-action-links {display:flex; flex-direction:row; margin:0 1em;} #wpadminbar .{$shrt}-post-list .{$shrt}-action-links a {width:{$icon_size}; height:{$icon_size}; line-height:normal; padding:0 5px; opacity:.5;} #wpadminbar .{$shrt}-post-list .{$shrt}-action-links a.{$shrt}-has-content {opacity:1;} /* icons & svg */ #wpadminbar .{$shrt} .dashicons {font:{$icon_size} dashicons; line-height:normal; color:inherit;} #wpadminbar .{$shrt} svg {width:{$icon_size}; height:{$icon_size}; margin-right:6px;} #wpadminbar li.{$shrt} span.dashicon-before {color:inherit;} #wpadminbar li.{$shrt} span.dashicon-before::before {width:{$icon_size}; height:{$icon_size}; color:inherit;} /* divider */ #wpadminbar :is(.{$shrt}-divider,#incspec) {margin-top: .5em; border-top:1px dotted {$color_line}; height:26px;} </style> STYLE_END; if (self::MINIMIZE_CSS) { $style = preg_replace('/\/\*.*?\*\//', '', $style); // remove comments $style = preg_replace('/\r?\n */', '', $style); // remove line breaks $style = preg_replace('/\t/', '', $style); // remove tabs } echo $style.PHP_EOL; do_action('qm/stop', __METHOD__); // Query Monitor Profiling $et = microtime(true); if (WP_DEBUG && self::$timing) {error_log(sprintf(' %s%s::%s() Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, $et-$st));} self::$total_runtime += $et-$st; } //------------------------------------------------------------------------------------------------------------------- /** * Emits SVG for menu icons. */ public function svg(){ $st = microtime(true); do_action('qm/start', __METHOD__); // Query Monitor Profiling $slug = self::SLUG; $shrt = self::SHRT; $builder_logo = ''; if ($this->builder('Oxygen')) { $builder_logo = <<<LOGO_END <symbol id="logo-oxygen" viewBox="0 0 381 385"> <path fill="currentColor" d="M297.508,349.748 C275.443,349.748 257.556,331.86 257.556,309.796 C257.556,287.731 275.443,269.844 297.508,269.844 C319.573,269.844 337.46,287.731 337.46,309.796 C337.46,331.86 319.573,349.748 297.508,349.748 L297.508,349.748 Z M222.304,309.796 C222.304,312.039 222.447,314.247 222.639,316.441 C212.33,319.092 201.528,320.505 190.403,320.505 C119.01,320.505 60.929,262.423 60.929,191.031 C60.929,119.638 119.01,61.557 190.403,61.557 C261.794,61.557 319.877,119.638 319.877,191.031 C319.877,206.833 317.02,221.978 311.815,235.99 C307.179,235.097 302.404,234.592 297.508,234.592 C255.974,234.592 222.304,268.262 222.304,309.796 L222.304,309.796 Z M380.805,191.031 C380.805,86.042 295.392,0.628 190.403,0.628 C85.414,0.628 0,86.042 0,191.031 C0,296.02 85.414,381.433 190.403,381.433 C212.498,381.433 233.708,377.609 253.456,370.657 C265.845,379.641 281.034,385 297.508,385 C339.042,385 372.712,351.33 372.712,309.796 C372.712,296.092 368.988,283.283 362.584,272.219 C374.251,247.575 380.805,220.058 380.805,191.031 L380.805,191.031 Z"/> </symbol> LOGO_END; } if ($this->builder('Bricks')) { $builder_logo = <<<LOGO_END <symbol id="logo-bricks" viewBox="0 0 35 45"> <path fill="currentColor" transform="translate(-16.000000, -11.000000)" d="M25.1875,11.34375 L25.9375,11.8125 L25.9375,24.84375 C28.5833466,23.0937413 31.5104006,22.21875 34.71875,22.21875 C39.3437731,22.21875 43.1770681,23.8333172 46.21875,27.0625 C49.218765,30.2916828 50.71875,34.2708097 50.71875,39 C50.71875,43.7500237 49.2083484,47.7291506 46.1875,50.9375 C43.1458181,54.1666828 39.3229397,55.78125 34.71875,55.78125 C30.6978966,55.78125 27.2604309,54.3437644 24.40625,51.46875 L24.40625,55 L16.03125,55 L16.03125,12.375 L25.1875,11.34375 Z M33.125,30.6875 C30.9166556,30.6875 29.0729241,31.4374925 27.59375,32.9375 C26.1145759,34.4791744 25.375,36.4999875 25.375,39 C25.375,41.5000125 26.1145759,43.5104091 27.59375,45.03125 C29.0520906,46.5520909 30.8958222,47.3125 33.125,47.3125 C35.4791784,47.3125 37.3854094,46.5208413 38.84375,44.9375 C40.2812572,43.3749922 41,41.3958453 41,39 C41,36.6041547 40.2708406,34.6145913 38.8125,33.03125 C37.3541594,31.4687422 35.458345,30.6875 33.125,30.6875 Z"/> </symbol> LOGO_END; } $svg = <<<SVG_END <svg id="{$slug}-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1"> <defs> <symbol id="logo-{$slug}" viewBox="0 0 24 24"> <path fill="currentColor" d="M14 13.9633H16V7.96331H10V9.96331H12.5858L7.25623 15.2929L8.67044 16.7071L14 11.3775L14 13.9633Z"/> <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12ZM3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12Z"/> </symbol> <symbol id="icon-{$slug}-eye" viewBox="0 0 24 24"> <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M16 12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12C8 9.79086 9.79086 8 12 8C14.2091 8 16 9.79086 16 12ZM14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12Z"/> <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M12 3C17.5915 3 22.2898 6.82432 23.6219 12C22.2898 17.1757 17.5915 21 12 21C6.40848 21 1.71018 17.1757 0.378052 12C1.71018 6.82432 6.40848 3 12 3ZM12 19C7.52443 19 3.73132 16.0581 2.45723 12C3.73132 7.94186 7.52443 5 12 5C16.4756 5 20.2687 7.94186 21.5428 12C20.2687 16.0581 16.4756 19 12 19Z"/> </symbol> {$builder_logo} </defs> </svg> SVG_END; if (self::MINIMIZE_SVG) { $svg = preg_replace('/>.*?</s', '><', $svg); // remove formatting content (line breaks, tabs) between tags $svg = preg_replace('/\r?\n\t+/', ' ', $svg); // remove remaining line breaks and tabs } echo $svg.PHP_EOL; do_action('qm/stop', __METHOD__); // Query Monitor Profiling $et = microtime(true); if (WP_DEBUG && self::$timing) {error_log(sprintf(' %s%s::%s() Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, $et-$st));} self::$total_runtime += $et-$st; } //------------------------------------------------------------------------------------------------------------------- /** @since 1.6.3 */ public function debug_info(){ echo sprintf('<span id="%s-info" data-nosnippet style="display:none">%s %s</span>', self::SLUG, self::SLUG, self::VERSION); } //------------------------------------------------------------------------------------------------------------------- /** * Adds items to the admin bar. */ public function admin_bar_items (WP_Admin_Bar $wp_admin_bar) { $st = microtime(true); do_action('qm/start', __METHOD__.' (total)'); // Query Monitor Profiling // basic permission check if (!current_user_can('edit_posts')) return; // get the registered post types $registered_post_types = get_post_types([],'OBJECT'); // get post statuses $post_statuses = get_post_statuses(); $parent_menu_slug = null; if ($this->settings['collapsed_menu']??false) { $parent_menu_slug = self::SLUG; // create admin bar main item $label = 'Quick Nav'; $icon = sprintf('<svg><use xlink:href="#logo-%1$s"></use></svg>', self::SLUG); $wp_admin_bar->add_menu([ 'id' => $parent_menu_slug, 'title' => sprintf('<span class="%s-flexer">%s</span>', self::SHRT, $icon.$label), 'meta' => [ 'class' => self::SHRT, // 'title' => self::TITLE, // don't show because it's distracting ] ]); } // create sub menus for post types foreach ($this->settings['post_types']??[] as $ptn => $pto) { // skip if post type not registered. Could happen if it was configured in Quick Nav settings, but has deactivated since. if (!$registered_post_types[$ptn]??null) continue; $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));} // get post object to evaluate capabilities and labels $ptobj = get_post_type_object($ptn); // permission check, either specific for this post type, or generic for posts if (!current_user_can($ptobj->cap->edit_posts??false)) goto DONE_POSTTYPE; // check permission to access Oxygen templates if ($this->builder('Oxygen') && ($ptn=='ct_template') && function_exists('oxygen_vsb_current_user_can_access') && !oxygen_vsb_current_user_can_access()) { if (WP_DEBUG && self::$timing>1) {error_log(sprintf(' %s%s::%s() User has no permission to access "%s"', '', __CLASS__, __FUNCTION__, $ptn));} goto DONE_POSTTYPE; } // check permission to access Bricks templates if ($this->builder('Bricks') && ($ptn=='bricks_template') && method_exists('\Bricks\Capabilities','current_user_can_use_builder') && !call_user_func('\Bricks\Capabilities::current_user_can_use_builder')) { if (WP_DEBUG && self::$timing>1) {error_log(sprintf(' %s%s::%s() User has no permission to access "%s"', '', __CLASS__, __FUNCTION__, $ptn));} goto DONE_POSTTYPE; } // prepare label, title, icon $label = $registered_post_types[$ptn]->label; $title = $registered_post_types[$ptn]->labels->all_items ?? $label; $icon_label = '<span class="ab-item dashicon-before '.$registered_post_types[$ptn]->menu_icon.'">'.$label.'</span>'; if ($this->builder('Oxygen') && $ptn == 'ct_template') { //$label = 'Oxygen Templates'; $icon = '<svg><use xlink:href="#logo-oxygen"></use></svg>'; $icon_label = $icon.$label; } if ($this->builder('Bricks') && $ptn == 'bricks_template') { //$label = 'Bricks Templates'; $icon = '<svg><use xlink:href="#logo-bricks"></use></svg>'; $icon_label = $icon.$label; } // create menu item for post type $wp_admin_bar->add_menu([ 'id' => sprintf('%s-%s', self::SLUG, $ptn), 'parent'=> $parent_menu_slug, 'title' => sprintf('<span class="%s-flexer">%s</span>', self::SHRT, $icon_label), 'href' => admin_url('edit.php?post_type='.$ptn), 'meta' => [ 'class' => self::SHRT.' '.self::SHRT.'-post-list', 'title' => $title, ] ]); if ($this->settings['quick_search']??null) { // add search $search_html = sprintf('<input id="%1$s-%2$s-search" type="text" name="search" placeholder="%3$s">', self::SHRT, $ptn, __('Search')); $wp_admin_bar->add_node([ 'id' => sprintf('%s-%s-%s', self::SLUG, $ptn, 'search'), 'parent'=> sprintf('%s-%s', self::SLUG, $ptn), 'group' => null, 'title' => $search_html, 'meta' => [ //'title' => __('Search'), // distracting 'class' => sprintf('%1$s-flexer %1$s-search', self::SHRT), ] ]); } if ($this->settings['add_new']??null) { // Add new... if (current_user_can($ptobj->cap->create_posts??false)) { if ($add_new_url = admin_url('post-new.php?post_type='.$ptn)) { $wp_admin_bar->add_node([ 'id' => sprintf('%s-%s-%s', self::SLUG, $ptn, 'add_new'), 'parent'=> sprintf('%s-%s', self::SLUG, $ptn), 'group' => null, 'title' => esc_html($ptobj->labels->add_new_item??'Add new ...').' ...', 'href' => $add_new_url, 'meta' => [ 'class' => sprintf('%1$s-flexer %1$s-add_new', self::SHRT), ] ]); } } } // get config for post type list $pto = array_merge([ 'orderby' => self::DEFAULT_ORDERBY, 'order' => self::DEFAULT_ORDER, 'limit' => self::DEFAULT_LIMIT, 'hierarchical' => false, 'happyfiles' => false, ],$this->settings['post_types'][$ptn] ?? []); $args = [ 'post_type' => $ptn, 'post_status' => $this->settings['status'], ]; if (defined('POLYLANG_VERSION')) { $args['lang'] = ''; // Deactivate Polylang language filter, @see https://polylang.pro/doc/developpers-how-to/ } if ($pto['hierarchical']) { // get_pages() returns hierachical results $args['sort_column'] = $pto['orderby']; $args['sort_order'] = $pto['order']; $args['number'] = $pto['limit'] === '' ? -1 : $pto['limit']; $posts = get_pages($args); } else { // get_posts() doesn't provide hierachical results $args['orderby'] = $pto['orderby']; $args['order'] = $pto['order']; $args['posts_per_page'] = $pto['limit'] === '' ? -1 : $pto['limit']; $posts = get_posts($args); } if ($pto['happyfiles'] && in_array($ptn,$this->get_happyfiles_post_types())) { $happyfiles_terms = []; // to collect used happyfiles folders do_action('qm/start', __METHOD__.' HappyFiles (post type "'.$ptn.'")'); // Query Monitor Profiling // loop posts and evaluate HappyFiles terms foreach ($posts as $post) { if ($post->happyfiles_term??null) continue; // we already handled this post by an append // get assigned HappyFiles terms $happyfiles_terms_post = get_the_terms($post->ID, 'hf_cat_'.$ptn); if (($happyfiles_terms_post !== false) && (!is_wp_error($happyfiles_terms_post)) && count($happyfiles_terms_post)) { // get assigned HappyFiles term names foreach($happyfiles_terms_post as $idx => $happyfiles_term_post) { // multiple terms assigned? duplicate post if ($idx>0) { $new_post = clone $post; $new_post->happyfiles_term = $happyfiles_term_post; //->name; $posts[] = $new_post; } else { // assign HappyFiles term names to post $post->happyfiles_term = $happyfiles_term_post; //->name; } } } else { $post->happyfiles_term = (object)['term_id'=>0, 'name'=>' Uncategorized','parent'=>0]; // fake term } if (!in_array($post->happyfiles_term, $happyfiles_terms)) $happyfiles_terms[] = $post->happyfiles_term; } // --- care about HappyFiles Folder hierarchy // create simple happyfiles folder list for quick reference $happyfiles_terms_registry=[]; foreach ($happyfiles_terms as $happyfiles_term) { $happyfiles_terms_registry[$happyfiles_term->term_id] = $happyfiles_term; } // store HappyFiles Folder path to posts foreach ($posts as $post) { $happyfiles_term = $post->happyfiles_term; $happyfiles_term_path = $happyfiles_term->name; while (($happyfiles_term->parent??0)!=0) { if ($happyfiles_term = $happyfiles_terms_registry[$happyfiles_term->parent]??null){ $happyfiles_term_path = $happyfiles_term->name.' ▸ '.$happyfiles_term_path; } } // replace space by (except leading space from ' Uncategorized') to assure correct sorting $post->happyfiles_term_path = preg_replace('/^([^ ]+) /','\1'."\u{00A0}",$happyfiles_term_path); } // --- order posts by HappyFiles Folder, plus the defined sort field and order $orderby_key = $pto['orderby']; $order_key = $pto['order']; usort($posts,function($a,$b) use ($orderby_key, $order_key) { if ($order_key == 'ASC') { return strcmp($a->happyfiles_term_path.' '.$a->{$orderby_key},$b->happyfiles_term_path.' '.$b->{$orderby_key}); } else { return strcmp($a->happyfiles_term_path.' '.$b->{$orderby_key},$b->happyfiles_term_path.' '.$a->{$orderby_key}); } }); do_action('qm/stop', __METHOD__.' HappyFiles (post type "'.$ptn.'")'); // Query Monitor Profiling } $hierarchy_parents = []; $happyfiles_previous_folder = null; // loop posts foreach ($posts as $post) { // permission check if (!current_user_can($ptobj->cap->edit_post??false, $post->ID)) continue; // hierarchy view if ($pto['hierarchical']) { // indent post titles as necessary if ($post->post_parent??0) { $parent_level = array_search($post->post_parent, $hierarchy_parents); if ($parent_level !== false) { $post->post_title = str_replace(' ','–',str_pad('',$parent_level+1,' ')).' '.$post->post_title; if ($parent_level < count($hierarchy_parents)-1) { $hierarchy_parents = array_slice($hierarchy_parents, 0, $parent_level+1); } $hierarchy_parents[] = $post->ID; } } else { $hierarchy_parents = [$post->ID]; } } $actions = array_merge([], $this->actions_wordpress($post)); if ($pto['builder']??false) { if ($this->builder('Oxygen')) {$actions = array_merge($actions, $this->actions_oxygen($post));} if ($this->builder('Bricks')) {$actions = array_merge($actions, $this->actions_bricks($post));} } // provide view action, except for templates if (!in_array($ptn, ['ct_template','bricks_template'])) { $actions = array_merge($actions, $this->actions_frontend($post)); } // create builder links $action_links = ''; foreach ($actions as $link) { $action_links .= sprintf('<a href="%s" title="%s" class="%s">%s</a>', $link->href??'', $link->title??'', $link->has_content?self::SHRT.'-has-content':'', $link->icon??''); } if ($action_links) { $action_links = sprintf('<span class="%s-action-links">%s</span>', self::SHRT, $action_links); } $title_appendix = ''; if (defined('POLYLANG_VERSION') && ($this->settings['show_language']??false)) { // language $pll_function_name = 'pll_get_post_language'; if (function_exists($pll_function_name)) { $pll_post_language = call_user_func($pll_function_name, $post->ID); $title_appendix .= ' <span class="language">'.$pll_post_language.'</span>'; } } // status $title_appendix .= ($post->post_status == 'publish') ? '' : ' <span class="status">['.$post_statuses[$post->post_status].']</span>'; if ($pto['happyfiles'] && ($happyfiles_previous_folder != $post->happyfiles_term)) { // add HappyFiles Folder $wp_admin_bar->add_node([ 'id' => sprintf('%s-%s-hf-%d', self::SLUG, $ptn, $post->happyfiles_term->term_id??0), 'parent'=> sprintf('%s-%s', self::SLUG, $ptn), 'group' => null, 'title' => '▸ '.$post->happyfiles_term_path, 'meta' => [ 'title' => 'HappyFiles '.esc_html__( 'Folders', 'happyfiles' ), 'class' => sprintf('%1$s-flexer %1$s-happyfiles-folder', self::SHRT), ] ]); $happyfiles_previous_folder = $post->happyfiles_term; } // create menu item for post. append happyfiles term to menu ID to avoid duplicate menu entries $wp_admin_bar->add_menu([ 'id' => sprintf('%s-%s-%d-%d', self::SLUG, $ptn, $post->ID, $post->happyfiles_term->term_id??0), 'parent'=> sprintf('%s-%s', self::SLUG, $ptn), 'group' => null, 'title' => $post->post_title . $title_appendix, 'href' => admin_url(sprintf('post.php?post=%d&action=edit', $post->ID)), 'meta' => [ 'title' => __('Edit').' in WordPress', 'html' => $action_links, 'class' => sprintf('%1$s-flexer %1$s-post', self::SHRT), ] ]); } $post_counts = wp_count_posts($ptn); $post_count = 0; foreach ($this->settings['status'] as $status) { $post_count += (int)$post_counts->{$status}; } if ($pto['limit'] && $post_count > $pto['limit']) { // add a hint about limits $link = false; $hint = __('Number of entries exceeds limit.'); if (current_user_can('manage_options')) { $link = admin_url('options-general.php?page='.self::SLUG); $hint .= ' '.__('Click to change the limit'); } $wp_admin_bar->add_node([ 'id' => sprintf('%s-%s-%s', self::SLUG, $ptn, 'limit-hint'), 'parent'=> sprintf('%s-%s', self::SLUG, $ptn), 'group' => null, 'title' => 'Limit: '.$pto['limit'], 'href' => $link, 'meta' => [ 'title' => $hint, 'class' => sprintf('%1$s-divider %1$s-limit', self::SHRT), ] ]); } DONE_POSTTYPE: do_action('qm/stop', __METHOD__.' (post type "'.$ptn.'")'); // Query Monitor Profiling $etpt = microtime(true); if (WP_DEBUG && self::$timing) {error_log(sprintf(' %s%s::%s() Post Type "%s" Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, $ptn, $etpt-$stpt));} } // create menu item for settings if (current_user_can('manage_options')) { $wp_admin_bar->add_menu([ 'id' => sprintf('%s-%s', self::SLUG, 'settings'), 'parent'=> self::SLUG, 'title' => sprintf('<span class="%s-flexer">%s</span>', self::SHRT, __('Settings')), 'href' => admin_url('options-general.php?page='.self::SLUG), 'meta' => [ 'class' => sprintf('%1$s-divider', self::SHRT), 'title' => __('Settings'), ] ]); } do_action('qm/stop', __METHOD__.' (total)'); // Query Monitor Profiling $et = microtime(true); if (WP_DEBUG && self::$timing) {error_log(sprintf(' %s%s::%s() Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, $et-$st));} self::$total_runtime += $et-$st; } //------------------------------------------------------------------------------------------------------------------- /** * Returns action links for WordPress. * @param WP_Post $post The WP_Post to generate action links for. * @return array An array of objects containing properties `href`, `icon`, `title`, `has_content`. */ private function actions_wordpress(WP_Post $post):array { $st = microtime(true); $retval = []; $link = admin_url(sprintf('post.php?post=%d&action=edit', $post->ID)); $icon = '<span class="dashicons dashicons-wordpress" ></span>'; $retval[] = (object)['href'=>esc_url($link), 'icon'=>$icon, 'title'=>__('Edit').' in WordPress', 'has_content' => $post->post_content?1:0]; DONE: $et = microtime(true); if (WP_DEBUG && self::$timing>1) {error_log(sprintf(' %s%s::%s(%s) Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, ($post->post_type??'').' '.($post->ID??''), $et-$st));} return $retval; } //------------------------------------------------------------------------------------------------------------------- /** * Returns action link for view in frontend. * @param WP_Post $post The WP_Post to generate action links for. * @return array An array of objects containing properties `href`, `icon`, `title`, `has_content`. */ private function actions_frontend(WP_Post $post):array { $st = microtime(true); $retval = []; $link = get_permalink($post->ID); $icon = sprintf('<svg><use xlink:href="#icon-%s-eye"></use></svg>',self::SLUG); $retval[] = (object)['href'=>esc_url($link), 'icon'=>$icon, 'title'=>__('View'), 'has_content' => 1]; DONE: $et = microtime(true); if (WP_DEBUG && self::$timing>1) {error_log(sprintf(' %s%s::%s(%s) Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, ($post->post_type??'').' '.($post->ID??''), $et-$st));} return $retval; } //------------------------------------------------------------------------------------------------------------------- /** * Returns action links for Oxygen. * @param WP_Post $post The WP_Post to generate action links for. * @return array An array of objects containing properties `href`, `icon`, `title`, `has_content`. */ private function actions_oxygen(WP_Post $post):array { $st = microtime(true); $retval = []; if (!$this->builder('Oxygen')) goto DONE; // Oxygen hidden for this post type? if (get_option('oxygen_vsb_ignore_post_type_'.$post->post_type) == 'true') goto DONE; // permission check if (!function_exists('oxygen_vsb_current_user_can_access') || !oxygen_vsb_current_user_can_access()) goto DONE; // Oxygen pre or post 4.8.3? $meta_function_name = function_exists('oxy_get_post_meta') ? 'oxy_get_post_meta' : 'get_post_meta'; // Edit a template or a post? if ($post->post_type == 'ct_template') { $parent_template_id = call_user_func($meta_function_name, $post->ID, 'ct_parent_template', true); $inner_content = false; if ($parent_template_id) { if ($json = call_user_func($meta_function_name, $parent_template_id, 'ct_builder_json', true)) { $inner_content = (strpos($json, '"name":"ct_inner_content"') !== false); } else { if ($shortcodes = call_user_func($meta_function_name, $parent_template_id, 'ct_builder_shortcodes', true)) { $inner_content = (strpos($shortcodes, '[ct_inner_content') !== false); } } } $edit_url = ct_get_post_builder_link($post->ID) . ($inner_content ? '&ct_inner=true' : ''); $icon = '<svg><use xlink:href="#logo-oxygen"></use></svg>'; $json = call_user_func($meta_function_name, $post->ID, 'ct_builder_json', true); $retval[] = (object)['href'=>esc_url($edit_url), 'icon'=>$icon, 'title'=>sprintf(__('Edit').' in %s Builder',$this->builder()), 'has_content'=>!empty($json)]; } else { // -1: None, undefined or 0: Automatic, > 0: Manually assigned $template_id = (int)call_user_func($meta_function_name, $post->ID, 'ct_other_template', true); if ($template_id == -1) { // None // nothing to do } elseif ($template_id > 0) { // manually assigned // get specified template post $template_post = get_post($template_id); $template_id = $template_post->ID ?? -1; // if template not found, assume None } else { // get template for single $template_post = ct_get_posts_template($post->ID); $template_id = $template_post->ID ?? -1; // if template not found, assume None } // if we have a template, check for inner content element $inner_content = false; if ($template_id && $template_id != -1) { if ($json = call_user_func($meta_function_name, $template_id, 'ct_builder_json', true)) { $inner_content = (strpos($json, '"name":"ct_inner_content"') !== false); } else { // only read shortcodes if we didn't find json; might be an old unmigrated post if ($shortcodes = call_user_func($meta_function_name, $template_id, 'ct_builder_shortcodes', true)) { $inner_content = (strpos($shortcodes, '[ct_inner_content') !== false); } } } // Check if post has Oxygen content $json = call_user_func($meta_function_name, $post->ID, 'ct_builder_json', true); // only read shortcodes if we didn't find json; might be an old unmigrated post $shortcodes = ''; //$shortcodes = $json ? true : call_user_func($meta_function_name, $post->ID, 'ct_builder_shortcodes', true); $icon = '<svg><use xlink:href="#logo-oxygen"></use></svg>'; $link = ct_get_post_builder_link($post->ID) . ($inner_content ? '&ct_inner=true' : ''); $retval[] = (object)['href'=>esc_url($link), 'icon'=>$icon, 'title'=>sprintf(__('Edit').' in %s Builder',$this->builder()), 'has_content'=>($json||$shortcodes)]; } DONE: $et = microtime(true); if (WP_DEBUG && self::$timing>1) {error_log(sprintf(' %s%s::%s(%s) Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, ($post->post_type??'').' '.($post->ID??''), $et-$st));} return $retval; } //------------------------------------------------------------------------------------------------------------------- /** * Returns action links for Bricks. * @param WP_Post $post The WP_Post to generate action links for. * @return array An array of objects containing properties `href`, `icon`, `title`, `has_content`. */ private function actions_bricks(WP_Post $post):array { $st = microtime(true); $retval = []; if (!$this->builder('Bricks')) goto DONE; // permission check if (!method_exists('\Bricks\Capabilities','current_user_can_use_builder') || !call_user_func('\Bricks\Capabilities::current_user_can_use_builder')) goto DONE; // create action link $icon = '<svg><use xlink:href="#logo-bricks"></use></svg>'; $link = call_user_func('\Bricks\Helpers::get_builder_edit_link',$post->ID); $template_type = call_user_func('\Bricks\Templates::get_template_type',$post->ID); $template_data = call_user_func('\Bricks\Database::get_data',$post->ID, $template_type); $retval[] = (object)['href'=>esc_url($link), 'icon'=>$icon, 'title'=>sprintf(__('Edit').' in %s Builder',$this->builder()), 'has_content'=>!empty($template_data)]; DONE: $et = microtime(true); if (WP_DEBUG && self::$timing>1) {error_log(sprintf(' %s%s::%s(%s) Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, ($post->post_type??'').' '.($post->ID??''), $et-$st));} return $retval; } //=================================================================================================================== // UTILITIES //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- /** * Returns builder name or boolean if builder matches specified name `Bricks` or `Oxygen`. * @param string $builder [Optional] Checks if builder matches. * @return mixed Builder name, or true/false for builder name match */ private function builder(string $builder = null) { $retval = ''; if (defined('BRICKS_VERSION')) $retval = 'Bricks'; if (defined('CT_VERSION')) $retval = 'Oxygen'; if (isset($builder)) $retval = strtolower($retval) == strtolower($builder); return $retval; } //------------------------------------------------------------------------------------------------------------------- /** * Returns HappyFiles configured post types, or empty array if not configured */ private function get_happyfiles_post_types() { $retval = get_option('happyfiles_post_types',[]); // doesn't return empty array but string if none configured if (!is_array($retval)) {$retval = [];} return $retval; } } //=================================================================================================================== // Initialize new MA_Admin_Quick_Nav(); endif;