PluginProbe
Polylang / trunk
Polylang vtrunk
3.8.9 3.8.8 3.8.7 3.8.6 3.8.5 3.8.4 3.8.3 2.7 2.7.0.1 2.7.1 2.7.2 2.7.3 2.7.4 2.8 2.8.1 2.8.2 2.8.3 2.8.4 2.9 2.9.1 2.9.2 3.0 3.0.1 3.0.2 3.0.3 All 233 releases
polylang / src / frontend / frontend-nav-menu.php

frontend-nav-menu.php in Polylang trunk, at src/frontend/frontend-nav-menu.php

344 lines 10.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Polylang
4 */
5
6 use WP_Syntex\Polylang\Switcher\Switcher;
7 use WP_Syntex\Polylang\Switcher\Element\Nav;
8 use WP_Syntex\Polylang\Switcher\Fields\Menu as Fields;
9 use WP_Syntex\Polylang\Switcher\Settings\Menu as Settings;
10
11 /**
12 * Manages custom menus translations as well as the language switcher menu item on frontend
13 *
14 * @since 1.2
15 */
16 class PLL_Frontend_Nav_Menu extends PLL_Nav_Menu {
17 /**
18 * Current language.
19 *
20 * @var PLL_Language|null|false
21 */
22 public $curlang;
23
24 /**
25 * @var PLL_Links|null
26 */
27 private ?PLL_Links $links;
28
29 /**
30 * Constructor
31 *
32 * @since 1.2
33 *
34 * @param PLL_Frontend|PLL_REST_Request $polylang The Polylang object.
35 */
36 public function __construct( &$polylang ) {
37 parent::__construct( $polylang );
38
39 $this->curlang = &$polylang->curlang;
40 $this->links = &$polylang->links;
41
42 // Split the language switcher menu item in several language menu items
43 add_filter( 'wp_get_nav_menu_items', array( $this, 'wp_get_nav_menu_items' ), 20 ); // after the customizer menus
44 add_filter( 'wp_nav_menu_objects', array( $this, 'wp_nav_menu_objects' ) );
45 add_filter( 'nav_menu_link_attributes', array( $this, 'nav_menu_link_attributes' ), 10, 2 );
46
47 // Filters menus by language
48 add_filter( 'theme_mod_nav_menu_locations', array( $this, 'nav_menu_locations' ), 20 );
49 add_filter( 'wp_nav_menu_args', array( $this, 'wp_nav_menu_args' ) );
50
51 // The customizer
52 if ( isset( $_POST['wp_customize'], $_POST['customized'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
53 add_filter( 'wp_nav_menu_args', array( $this, 'filter_args_before_customizer' ) );
54 add_filter( 'wp_nav_menu_args', array( $this, 'filter_args_after_customizer' ), 2000 );
55 }
56 }
57
58 /**
59 * Sorts menu items by menu order.
60 *
61 * @since 1.7.9
62 *
63 * @param stdClass $a The first object to compare.
64 * @param stdClass $b The second object to compare.
65 * @return int -1 or 1 if $a is considered to be respectively less than or greater than $b.
66 */
67 protected function usort_menu_items( $a, $b ) {
68 return ( $a->menu_order < $b->menu_order ) ? -1 : 1;
69 }
70
71 /**
72 * Splits the one language switcher menu item of backend in several menu items on frontend.
73 * Takes care to menu_order as it is used later in wp_nav_menu().
74 *
75 * @since 1.1.1
76 *
77 * @param stdClass[] $items Menu items.
78 * @return stdClass[] Modified menu items.
79 */
80 public function wp_get_nav_menu_items( $items ) {
81 if ( empty( $this->curlang ) || empty( $this->links ) ) {
82 return $items;
83 }
84
85 if ( doing_action( 'customize_register' ) ) { // needed since WP 4.3, doing_action available since WP 3.9
86 return $items;
87 }
88
89 // The customizer menus does not sort the items and we need them to be sorted before splitting the language switcher
90 usort( $items, array( $this, 'usort_menu_items' ) );
91
92 $new_items = array();
93 $offset = 0;
94
95 foreach ( $items as $item ) {
96 $options = get_post_meta( $item->ID, '_pll_menu_item', true );
97
98 if ( empty( $options ) || ! is_array( $options ) ) {
99 $item->menu_order += $offset;
100 $new_items[] = $item;
101 continue;
102 }
103
104 $settings = new Settings( Fields::remove_legacy_settings( $options ) );
105 $elements = ( new Switcher( $settings, $this->links ) )->get_elements();
106
107 if ( empty( $elements ) ) {
108 continue;
109 }
110
111 if ( 'dropdown' === $settings->layout ) {
112 // Parent item for dropdown.
113 $element = new Nav( $this->curlang, $settings, $this->links );
114
115 $item->title = $element->get_label();
116 $item->attr_title = '';
117 $item->url = $element->url;
118 $item->classes = array_merge( $item->classes, $element->item_classes, array( 'pll-parent-menu-item' ) );
119 $item->menu_order += $offset;
120
121 if ( $settings->show_flags ) {
122 // Since it is added to the parent `<li>`, no need to add it to the child elements too.
123 $item->classes[] = 'pll-aspect-ratio-' . str_replace( ':', '', $settings->flag_aspect_ratio );
124 }
125
126 $new_items[] = $item;
127 ++$offset;
128 }
129
130 $i = 0; // For incrementation of menu order only in case of dropdown.
131 foreach ( $elements as $element ) {
132 ++$i;
133 $lang_item = clone $item;
134
135 $lang_item->ID = "{$lang_item->ID}-{$element->slug}"; // A unique ID.
136 $lang_item->title = $element->get_label();
137 $lang_item->attr_title = '';
138 $lang_item->url = $element->url;
139 $lang_item->lang = $element->locale; // Save this for use in nav_menu_link_attributes.
140 $lang_item->classes = $element->item_classes;
141
142 if ( 'dropdown' === $settings->layout ) {
143 $lang_item->menu_order = $item->menu_order + $i;
144 $lang_item->menu_item_parent = $item->db_id;
145 $lang_item->db_id = 0; // To avoid recursion.
146 } else {
147 $lang_item->classes = array_diff( $lang_item->classes, array( 'lang-item-first' ) ); // Doesn't mean anything here.
148 $lang_item->classes[] = 'pll-aspect-ratio-' . str_replace( ':', '', $settings->flag_aspect_ratio );
149 $lang_item->menu_order += $offset;
150 }
151
152 $new_items[] = $lang_item;
153 ++$offset;
154 }
155 --$offset;
156 }
157
158 return $new_items;
159 }
160
161 /**
162 * Returns the ancestors of a menu item.
163 *
164 * @since 1.1.1
165 *
166 * @param stdClass $item Menu item.
167 * @return int[] Ancestors ids.
168 */
169 public function get_ancestors( $item ) {
170 $ids = array();
171 $ancestor_id = (int) $item->db_id;
172 while ( ( $ancestor_id = get_post_meta( $ancestor_id, '_menu_item_menu_item_parent', true ) ) && ! in_array( $ancestor_id, $ids ) ) {
173 $ids[] = $ancestor_id;
174 }
175 return $ids;
176 }
177
178 /**
179 * Removes current-menu and current-menu-ancestor classes to lang switcher when not on the home page.
180 *
181 * @since 1.1.1
182 *
183 * @param stdClass[] $items An array of menu items.
184 * @return stdClass[]
185 */
186 public function wp_nav_menu_objects( $items ) {
187 $k_ids = array();
188 $r_ids = array();
189
190 foreach ( $items as $item ) {
191 if ( ! empty( $item->classes ) && is_array( $item->classes ) ) {
192 if ( in_array( 'current-lang', $item->classes ) ) {
193 $item->current = false;
194 $item->classes = array_diff( $item->classes, array( 'current-menu-item' ) );
195 $r_ids = array_merge( $r_ids, $this->get_ancestors( $item ) ); // Remove the classes for these ancestors.
196 } elseif ( in_array( 'current-menu-item', $item->classes ) ) {
197 $k_ids = array_merge( $k_ids, $this->get_ancestors( $item ) ); // Keep the classes for these ancestors.
198 }
199 }
200 }
201
202 $r_ids = array_diff( $r_ids, $k_ids );
203
204 foreach ( $items as $item ) {
205 if ( ! empty( $item->db_id ) && in_array( $item->db_id, $r_ids ) ) {
206 $item->classes = array_diff( $item->classes, array( 'current-menu-ancestor', 'current-menu-parent', 'current_page_parent', 'current_page_ancestor' ) );
207 }
208 }
209
210 return $items;
211 }
212
213 /**
214 * Adds hreflang attribute for the language switcher menu items.
215 * available since WP 3.6.
216 *
217 * @since 1.1
218 *
219 * @param string[] $atts HTML attributes applied to the menu item's `<a>` element.
220 * @param stdClass $item Menu item.
221 * @return string[] Modified attributes.
222 */
223 public function nav_menu_link_attributes( $atts, $item ) {
224 if ( isset( $item->lang ) ) {
225 $atts['lang'] = esc_attr( $item->lang );
226 $atts['hreflang'] = $atts['lang'];
227 }
228 return $atts;
229 }
230
231 /**
232 * Fills the theme nav menus locations with the right menu in the right language
233 * Needs to wait for the language to be defined
234 *
235 * @since 1.2
236 *
237 * @param array|bool $menus list of nav menus locations, false if menu locations have not been filled yet
238 * @return array|bool modified list of nav menus locations
239 */
240 public function nav_menu_locations( $menus ) {
241 if ( is_array( $menus ) && ! empty( $this->curlang ) ) {
242 // First get multilingual menu locations from DB
243 $theme = get_option( 'stylesheet' );
244
245 foreach ( array_keys( $menus ) as $loc ) {
246 $menus[ $loc ] = empty( $this->options['nav_menus'][ $theme ][ $loc ][ $this->curlang->slug ] ) ? 0 : $this->options['nav_menus'][ $theme ][ $loc ][ $this->curlang->slug ];
247 }
248
249 // Support for theme customizer
250 if ( is_customize_preview() ) {
251 global $wp_customize;
252 foreach ( $wp_customize->unsanitized_post_values() as $key => $value ) {
253 if ( false !== strpos( $key, 'nav_menu_locations[' ) ) {
254 $loc = substr( trim( $key, ']' ), 19 );
255 $infos = $this->explode_location( $loc );
256 if ( $infos['lang'] === $this->curlang->slug ) {
257 $menus[ $infos['location'] ] = (int) $value;
258 } elseif ( $this->curlang->is_default ) {
259 $menus[ $loc ] = (int) $value;
260 }
261 }
262 }
263 }
264 }
265 return $menus;
266 }
267
268 /**
269 * Attempts to translate the nav menu when it is hardcoded or when no location is defined in wp_nav_menu().
270 *
271 * @since 1.7.10
272 *
273 * @param array $args Array of `wp_nav_menu()` arguments.
274 * @return array
275 */
276 public function wp_nav_menu_args( $args ) {
277 $theme = get_option( 'stylesheet' );
278
279 if ( empty( $this->curlang ) || empty( $this->options['nav_menus'][ $theme ] ) ) {
280 return $args;
281 }
282
283 // Get the nav menu based on the requested menu
284 $menu = wp_get_nav_menu_object( $args['menu'] );
285
286 // Attempt to find a translation of this menu
287 // This obviously does not work if the nav menu has no associated theme location
288 if ( $menu ) {
289 foreach ( $this->options['nav_menus'][ $theme ] as $menus ) {
290 if ( in_array( $menu->term_id, $menus ) && ! empty( $menus[ $this->curlang->slug ] ) ) {
291 $args['menu'] = $menus[ $this->curlang->slug ];
292 return $args;
293 }
294 }
295 }
296
297 // Get the first menu that has items and and is in the current language if we still can't find a menu
298 if ( ! $menu && ! $args['theme_location'] ) {
299 $menus = wp_get_nav_menus();
300 foreach ( $menus as $menu_maybe ) {
301 if ( wp_get_nav_menu_items( $menu_maybe->term_id, array( 'update_post_term_cache' => false ) ) ) {
302 foreach ( $this->options['nav_menus'][ $theme ] as $menus ) {
303 if ( in_array( $menu_maybe->term_id, $menus ) && ! empty( $menus[ $this->curlang->slug ] ) ) {
304 $args['menu'] = $menus[ $this->curlang->slug ];
305 return $args;
306 }
307 }
308 }
309 }
310 }
311
312 return $args;
313 }
314
315 /**
316 * Filters the nav menu location before the customizer so that it matches the temporary location in the customizer
317 *
318 * @since 1.8
319 *
320 * @param array $args wp_nav_menu $args
321 * @return array modified $args
322 */
323 public function filter_args_before_customizer( $args ) {
324 if ( ! empty( $this->curlang ) ) {
325 $args['theme_location'] = $this->combine_location( $args['theme_location'], $this->curlang );
326 }
327 return $args;
328 }
329
330 /**
331 * Filters the nav menu location after the customizer to get back the true nav menu location for the theme
332 *
333 * @since 1.8
334 *
335 * @param array $args wp_nav_menu $args
336 * @return array modified $args
337 */
338 public function filter_args_after_customizer( $args ) {
339 $infos = $this->explode_location( $args['theme_location'] );
340 $args['theme_location'] = $infos['location'];
341 return $args;
342 }
343 }
344