| 1 |
<?php |
| 2 |
/* |
| 3 |
* Plugin Name: Auto Submenu - Free |
| 4 |
* Plugin URI: https://wordpress.telodelic.nl/auto-submenu |
| 5 |
* Description: Dynamic menus: Add a page to your menu and then let WordPress automatically add the child pages. |
| 6 |
* Version: 1.0.6 |
| 7 |
* Author: Diana van de Laarschot |
| 8 |
* Author URI: https://wordpress.telodelic.nl |
| 9 |
* License: GPL-2.0-or-later |
| 10 |
* License URI: https://www.gnu.org/licenses/gpl-2.0.html |
| 11 |
* Requires at least: 5.4 |
| 12 |
* Tested up to: 7.0 |
| 13 |
* Requires PHP: 7.4 |
| 14 |
* Text Domain: auto-submenu |
| 15 |
*/ |
| 16 |
|
| 17 |
/* Copyright 2024 Diana van de Laarschot (email : mail@telodelic.nl) |
| 18 |
|
| 19 |
This program is free software; you can redistribute it and/or modify |
| 20 |
it under the terms of the GNU General Public License, version 2, as |
| 21 |
published by the Free Software Foundation. |
| 22 |
|
| 23 |
This program is distributed in the hope that it will be useful, |
| 24 |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 25 |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 26 |
GNU General Public License for more details. |
| 27 |
|
| 28 |
You should have received a copy of the GNU General Public License |
| 29 |
along with this program; if not, write to the Free Software |
| 30 |
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| 31 |
*/ |
| 32 |
|
| 33 |
require('src/asm-functions.php'); |
| 34 |
|
| 35 |
class ASM_Manager_Free |
| 36 |
{ |
| 37 |
const OPTION_NAME = 'asm_options'; |
| 38 |
const VERSION = '1.0.0'; |
| 39 |
const CSS_VERSION = '1.0.0'; |
| 40 |
const JS_VERSION = '1.0.0'; |
| 41 |
|
| 42 |
protected $options = null; |
| 43 |
protected $defaults = array('version' => self::VERSION); |
| 44 |
|
| 45 |
/** |
| 46 |
* Constructor |
| 47 |
*/ |
| 48 |
function __construct() |
| 49 |
{ |
| 50 |
// For use in Appearance > Menus and Theme > Customize: |
| 51 |
add_filter('gettext', array(&$this, 'asm_gettext'), 10, 3); |
| 52 |
add_action('publish_page', array(&$this, 'asm_publish_page')); // Checkbox: Automatically add new top-level and all its child pages to this menu (Auto Submenu) |
| 53 |
|
| 54 |
// For use via Block Editor: |
| 55 |
add_action('enqueue_block_editor_assets', array(&$this, 'asm_enqueue_block_editor_assets_js')); |
| 56 |
add_action('block_core_navigation_render_inner_blocks', array(&$this, 'asm_block_core_navigation_render_inner_blocks')); |
| 57 |
|
| 58 |
// Add button to plugin description |
| 59 |
add_filter('plugin_row_meta', array(&$this, 'asm_plugin_row_meta'), 10, 2); |
| 60 |
|
| 61 |
// Auto-update if a new plugin version is available via central wordpress.org repository |
| 62 |
add_filter('auto_update_plugin', array(&$this, 'asm_auto_update_plugin'), 10, 2); |
| 63 |
} // function |
| 64 |
|
| 65 |
function ASM_Manager_Free() |
| 66 |
{ |
| 67 |
$this->get_options(); |
| 68 |
$this->__construct(); |
| 69 |
} // function |
| 70 |
|
| 71 |
function asm_block_core_navigation_render_inner_blocks_recursive(&$result_parent, &$original_parent, &$original_item, &$taxonomies) |
| 72 |
{ |
| 73 |
// We need to work on the inner array. (Array methods will throw if it's a WP_Block) |
| 74 |
if (is_a($original_item, 'WP_Block')) { |
| 75 |
$original_item = $original_item->parsed_block; |
| 76 |
} |
| 77 |
$result_item = array_merge($original_item, array("innerBlocks" => array())); |
| 78 |
if (array_key_exists("attrs", $original_item)) { |
| 79 |
$get_attribute_value = fn($i, $k, $def) => is_array($i) && array_key_exists("attrs", $i) && array_key_exists($k, $i["attrs"]) ? $i["attrs"][$k] : $def; |
| 80 |
// Augment page object with a list of its posts: Append posts to $result |
| 81 |
if ($get_attribute_value($original_item, "asm_unfold", false)) { |
| 82 |
$id = $get_attribute_value($original_item, "id", null); |
| 83 |
$asm_item_depth = $get_attribute_value($original_item, "asm_item_depth", 10); |
| 84 |
$asm_item_depth = ($asm_item_depth <= 0 || $asm_item_depth >= 3) ? 3 : $asm_item_depth; |
| 85 |
|
| 86 |
// Add child pages as submenu under page |
| 87 |
(new ASM_Functions_Free())->asm_block_editor_add_submenu_child_pages($result_parent, $original_item, $id, $asm_item_depth); |
| 88 |
} else { |
| 89 |
// Append to correct parent |
| 90 |
$original_parent["innerBlocks"][] = &$result_item; |
| 91 |
} |
| 92 |
|
| 93 |
if (is_array($original_item["innerBlocks"])) { |
| 94 |
foreach ($original_item["innerBlocks"] as $key => &$innerBlock) { |
| 95 |
$this->asm_block_core_navigation_render_inner_blocks_recursive($result_item, $original_item, $innerBlock, $taxonomies); |
| 96 |
} |
| 97 |
} |
| 98 |
} |
| 99 |
} |
| 100 |
|
| 101 |
// Workaround. Dropdown carets are not generated for parent items |
| 102 |
// Reported here: https://core.trac.wordpress.org/ticket/60572#ticket |
| 103 |
function decorate_block_recursive(&$menu_item) |
| 104 |
{ |
| 105 |
$menu_item = new WP_Block($menu_item, array("showSubmenuIcon" => 1)); // This is the actual fix, 'activate' the context attribute to make WordPress honor it. |
| 106 |
|
| 107 |
if (count($menu_item->parsed_block["innerBlocks"]) > 0) { |
| 108 |
foreach ($menu_item->parsed_block["innerBlocks"] as $key => &$innerBlock) { |
| 109 |
$this->decorate_block_recursive($innerBlock); |
| 110 |
} |
| 111 |
} |
| 112 |
} |
| 113 |
|
| 114 |
function asm_block_core_navigation_render_inner_blocks($items) |
| 115 |
{ |
| 116 |
$this->get_options(); |
| 117 |
|
| 118 |
$result = array("innerBlocks" => array()); |
| 119 |
|
| 120 |
$taxonomies = get_taxonomies(array('show_in_nav_menus' => true), 'objects'); |
| 121 |
foreach ($items as $key => $item) { |
| 122 |
$this->asm_block_core_navigation_render_inner_blocks_recursive($result, $result, $item->parsed_block, $taxonomies); |
| 123 |
} |
| 124 |
|
| 125 |
// Workaround. Dropdown carets are not generated for parent items |
| 126 |
// Reported here: https://core.trac.wordpress.org/ticket/60572#ticket |
| 127 |
foreach ($result["innerBlocks"] as $key => &$innerBlock) { |
| 128 |
$this->decorate_block_recursive($innerBlock); |
| 129 |
} |
| 130 |
|
| 131 |
return new WP_Block_List($result["innerBlocks"]); |
| 132 |
} |
| 133 |
|
| 134 |
function asm_auto_update_plugin($update, $item) |
| 135 |
{ |
| 136 |
return($item->slug == 'auto-submenu') ? true : $update; |
| 137 |
} // function |
| 138 |
|
| 139 |
private function get_options() |
| 140 |
{ |
| 141 |
// already did the checks |
| 142 |
if (isset($this->options)) { |
| 143 |
return $this->options; |
| 144 |
} |
| 145 |
|
| 146 |
// first call, get the options |
| 147 |
$options = get_option(self::OPTION_NAME); |
| 148 |
|
| 149 |
// options exist |
| 150 |
if ($options !== false) { |
| 151 |
$new_version = version_compare($options['version'], self::VERSION, '<'); |
| 152 |
$desync = array_diff_key($this->defaults, $options) !== array_diff_key($options, $this->defaults); |
| 153 |
|
| 154 |
// update options if version changed, or we have missing/extra (out of sync) option entries |
| 155 |
if ($new_version || $desync) { |
| 156 |
$new_options = array(); |
| 157 |
|
| 158 |
// check for new options and set defaults if necessary |
| 159 |
foreach ($this->defaults as $option => $value) { |
| 160 |
$new_options[$option] = isset($options[$option]) ? $options[$option] : $value; |
| 161 |
} |
| 162 |
|
| 163 |
// update version info |
| 164 |
$new_options['version'] = self::VERSION; |
| 165 |
|
| 166 |
update_option(self::OPTION_NAME, $new_options); |
| 167 |
$this->options = $new_options; |
| 168 |
} else // no update required |
| 169 |
{ |
| 170 |
$this->options = $options; |
| 171 |
} |
| 172 |
} else // either new install or version from before versioning existed |
| 173 |
{ |
| 174 |
update_option(self::OPTION_NAME, $this->defaults); |
| 175 |
$this->options = $this->defaults; |
| 176 |
} |
| 177 |
|
| 178 |
return $this->options; |
| 179 |
} |
| 180 |
|
| 181 |
static function asm_uninstall() |
| 182 |
{ |
| 183 |
// We're uninstalling, so delete all custom fields on nav_menu_items that the ASM plugin added |
| 184 |
// Unless the premium version is active |
| 185 |
$installed_plugins = get_plugins(); |
| 186 |
if (!(array_key_exists('auto-submenu-premium', $installed_plugins) || in_array('auto-submenu-premium', $installed_plugins, true))) { |
| 187 |
// Premium version not installed, safe to delete all fields |
| 188 |
delete_metadata('nav_menu_item', null, '_asm-unfold', '', true); |
| 189 |
delete_metadata('nav_menu_item', null, '_asm-orderby', '', true); |
| 190 |
delete_metadata('nav_menu_item', null, '_asm-order', '', true); |
| 191 |
delete_metadata('nav_menu_item', null, '_asm-item-depth', '', true); |
| 192 |
delete_metadata('nav_menu_item', null, '_asm-item-titles', '', true); |
| 193 |
} |
| 194 |
} // function |
| 195 |
|
| 196 |
/* |
| 197 |
* Add JS for ASM fields site-editor.php (Appearance > Editor, the block editor) |
| 198 |
*/ |
| 199 |
function asm_enqueue_block_editor_assets_js( /* no $hook parameter in this case */) |
| 200 |
{ |
| 201 |
wp_enqueue_script( |
| 202 |
'asm_enqueue_block_editor_assets_core_navigation_link_js', |
| 203 |
plugins_url('src/blocks/asm_core_navigation_link/build/index.js', __FILE__), |
| 204 |
['wp-blocks'], |
| 205 |
self::JS_VERSION, |
| 206 |
true |
| 207 |
); |
| 208 |
} // function |
| 209 |
|
| 210 |
function asm_gettext($translated, $original, $textdomain) |
| 211 |
{ |
| 212 |
// Some users expect to see a checkbox for this option. |
| 213 |
// Change the text of the checkbox, clearly showing that the plugin is active and the functionality behaves differently now. |
| 214 |
// https://wordpress.org/support/topic/no-automatically-add-new-child-pages-option-in-menus-when-plugin-activated/ |
| 215 |
if ('Automatically add new top-level pages to this menu' == $translated) { |
| 216 |
$translated = 'When publishing a top-level page, automatically add it to this menu. When publishing a child page, add it if the parent is in the menu (Auto Submenu)'; |
| 217 |
} |
| 218 |
|
| 219 |
return $translated; |
| 220 |
} |
| 221 |
|
| 222 |
/** |
| 223 |
* When publishing a top-level page or child page, automatically add it to this menu (Auto Submenu) |
| 224 |
*/ |
| 225 |
function asm_publish_page($post_id) |
| 226 |
{ |
| 227 |
// Theme supports custom menus? |
| 228 |
if (!current_theme_supports('menus')) { |
| 229 |
return; |
| 230 |
} |
| 231 |
|
| 232 |
// Published page has parent? |
| 233 |
$post = get_post( $post_id ); |
| 234 |
if ( ! $post->post_parent ) { |
| 235 |
return; |
| 236 |
} |
| 237 |
|
| 238 |
// Get menus with auto_add enabled |
| 239 |
$auto_add = get_option('nav_menu_options'); |
| 240 |
if (empty($auto_add) || !is_array($auto_add) || !isset($auto_add['auto_add'])) { |
| 241 |
return; |
| 242 |
} |
| 243 |
$auto_add = $auto_add['auto_add']; |
| 244 |
if (empty($auto_add) || !is_array($auto_add)) { |
| 245 |
return; |
| 246 |
} |
| 247 |
|
| 248 |
// Loop through the menus to find page parent |
| 249 |
foreach ($auto_add as $menu_id) { |
| 250 |
$menu_items = wp_get_nav_menu_items($menu_id, array('post_status' => 'publish,draft')); |
| 251 |
if (!is_array($menu_items)) { |
| 252 |
continue; |
| 253 |
} |
| 254 |
|
| 255 |
// Assumption of current implementation: Top level pages are added to the menu only once. |
| 256 |
$menu_child = NULL; |
| 257 |
$menu_parent = NULL; |
| 258 |
|
| 259 |
// Find the child menu item and the parent menu item for this child page |
| 260 |
foreach ($menu_items as $menu_item) { |
| 261 |
if ($menu_item->object_id == $post->ID) { |
| 262 |
$menu_child = $menu_item; |
| 263 |
} |
| 264 |
if ($menu_item->object_id == $post->post_parent) { |
| 265 |
$menu_parent = $menu_item; |
| 266 |
} |
| 267 |
} |
| 268 |
|
| 269 |
// Parent not in menu, abort. |
| 270 |
if ( !$menu_parent ){ |
| 271 |
break; |
| 272 |
} |
| 273 |
|
| 274 |
if ( $menu_child ) { |
| 275 |
// Is child menu item already under correct parent? (If so, do nothing) |
| 276 |
if ( $menu_child->menu_item_parent != $menu_parent->ID ) { |
| 277 |
// Update the existing child menu item, move to correct parent |
| 278 |
wp_update_nav_menu_item( $menu_id, $menu_child->db_id, array( |
| 279 |
'menu-item-position' => $post->menu_order, |
| 280 |
'menu-item-object-id' => $post->ID, |
| 281 |
'menu-item-object' => $post->post_type, |
| 282 |
'menu-item-parent-id' => $menu_parent->ID, |
| 283 |
'menu-item-type' => 'post_type', |
| 284 |
'menu-item-status' => 'publish' |
| 285 |
) ); |
| 286 |
} |
| 287 |
} else { |
| 288 |
// No menu item yet. Add new menu item for child page |
| 289 |
wp_update_nav_menu_item( $menu_id, 0, array( |
| 290 |
'menu-item-position' => $post->menu_order, |
| 291 |
'menu-item-object-id' => $post->ID, |
| 292 |
'menu-item-object' => $post->post_type, |
| 293 |
'menu-item-parent-id' => $menu_parent->ID, |
| 294 |
'menu-item-type' => 'post_type', |
| 295 |
'menu-item-status' => 'publish' |
| 296 |
) ); |
| 297 |
} |
| 298 |
} |
| 299 |
} |
| 300 |
|
| 301 |
/* |
| 302 |
* Add a button to the plugin description on the plugins page |
| 303 |
*/ |
| 304 |
function asm_plugin_row_meta($links, $file) |
| 305 |
{ |
| 306 |
if (plugin_basename(__FILE__) == $file) { |
| 307 |
$row_meta = array( |
| 308 |
'docs' => '<a href="' . esc_url('https://wordpress.telodelic.nl/auto-submenu/') . '" target="_blank" aria-label="' . esc_attr__('Go Premium', 'auto-submenu') . '" style="color:green;">' . esc_html__('Go Premium', 'auto-submenu') . '</a>' |
| 309 |
); |
| 310 |
return array_merge($links, $row_meta); |
| 311 |
} |
| 312 |
|
| 313 |
return $links; |
| 314 |
} |
| 315 |
} |
| 316 |
|
| 317 |
$asm_manager_free = new ASM_Manager_Free(); |
| 318 |
|
| 319 |
// Register the uninstall hook. Should be done after the class has been defined. |
| 320 |
register_uninstall_hook(__FILE__, array('ASM_Manager_Free', 'asm_uninstall')); |
| 321 |
|
| 322 |
?> |