PluginProbe
Polylang / 3.8.6
Polylang v3.8.6
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 3.8.6, at src/frontend/frontend-nav-menu.php

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