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
← All changes | js/build/nav-menu.js +308 -91 3.0.1 → trunk View file →
@@ -1,105 +1,322 @@
1 1 /**
2 2 * Handles the options in the language switcher nav menu metabox.
3 - *
4 - * @package Polylang
5 3 */
6 4
7 -jQuery(
8 - function( $ ) {
9 - $( '#update-nav-menu' ).on(
10 - 'click',
11 - function( e ) {
12 - if ( e.target && e.target.className && -1 != e.target.className.indexOf( 'item-edit' ) ) {
13 - $( "input[value='#pll_switcher'][type=text]" ).parent().parent().parent().each(
14 - function(){
15 - var item = $( this ).attr( 'id' ).substring( 19 );
16 - $( this ).children( 'p:not( .field-move )' ).remove(); // remove default fields we don't need
5 +const pllNavMenu = {
6 + /**
7 + * The element wrapping the menu elements.
8 + *
9 + * @member {HTMLElement|null}
10 + */
11 + wrapper: null,
17 12
18 - // item is a number part of id of parent menu item built by WordPress
19 - // pll_data is built server side with i18n strings without HTML and data retrieved from post meta
20 - // the usage of attr method is safe before append call.
21 - h = $( '<input>' ).attr(
22 - {
23 - type: 'hidden',
24 - id: 'edit-menu-item-title-' + item,
25 - name: 'menu-item-title[' + item + ']',
26 - value: pll_data.title
27 - }
28 - );
29 - $( this ).append( h ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
13 + /**
14 + * Init.
15 + */
16 + init: () => {
17 + if ( document.readyState !== 'loading' ) {
18 + pllNavMenu.ready();
19 + } else {
20 + document.addEventListener( 'DOMContentLoaded', pllNavMenu.ready );
21 + }
22 + },
30 23
31 - h = $( '<input>' ).attr(
32 - {
33 - type: 'hidden',
34 - id: 'edit-menu-item-url-' + item,
35 - name: 'menu-item-url[' + item + ']',
36 - value: '#pll_switcher'
37 - }
38 - );
39 - $( this ).append( h ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
24 + /**
25 + * Called when the DOM is ready. Attaches the events to the wrapper.
26 + */
27 + ready: () => {
28 + pllNavMenu.wrapper = document.getElementById( 'menu-to-edit' );
40 29
41 - // a hidden field which exits only if our jQuery code has been executed
42 - h = $( '<input>' ).attr(
43 - {
44 - type: 'hidden',
45 - id: 'edit-menu-item-pll-detect-' + item,
46 - name: 'menu-item-pll-detect[' + item + ']',
47 - value: 1
48 - }
49 - );
50 - $( this ).append( h ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
30 + if ( ! pllNavMenu.wrapper ) {
31 + return;
32 + }
51 33
52 - ids = Array( 'hide_if_no_translation', 'hide_current', 'force_home', 'show_flags', 'show_names', 'dropdown' ); // reverse order
34 + pllNavMenu.wrapper.addEventListener( 'click', pllNavMenu.printMetabox );
35 + pllNavMenu.wrapper.addEventListener( 'change', pllNavMenu.manageRowsAndValues );
36 + },
53 37
54 - // add the fields
55 - for ( var i = 0, idsLength = ids.length; i < idsLength; i++ ) {
56 - p = $( '<p>' ).attr( 'class', 'description' );
57 - // p is hardcoded just above by using attr method which is safe.
58 - $( this ).prepend( p ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend
59 - // item is a number part of id of parent menu item built by WordPress
60 - // pll_data is built server side with i18n strings without HTML
61 - label = $( '<label>' ).attr( 'for', 'edit-menu-item-' + ids[ i ] + '-' + item ).text( ' ' + pll_data.strings[ ids[ i ] ] );
62 - p.append( label ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
63 - cb = $( '<input>' ).attr(
64 - {
65 - type: 'checkbox',
66 - id: 'edit-menu-item-' + ids[ i ] + '-' + item,
67 - name: 'menu-item-' + ids[ i ] + '[' + item + ']',
68 - value: 1
69 - }
70 - );
71 - if ( ( typeof( pll_data.val[ item ] ) != 'undefined' && pll_data.val[ item ][ ids[ i ] ] == 1 ) || ( typeof( pll_data.val[ item ] ) == 'undefined' && ids[ i ] == 'show_names' ) ) { // show_names as default value
72 - cb.prop( 'checked', true );
73 - }
74 - // See reasons above. Checkbox are totaly hardcoded here with safe value
75 - label.prepend( cb ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend
76 - }
77 - }
38 + printMetabox: {
39 + /**
40 + * Event callback that prints our checkboxes in the language switcher.
41 + *
42 + * @param {Event} event The event.
43 + */
44 + handleEvent: event => {
45 + if ( ! event.target.classList.contains( 'item-edit' ) ) {
46 + // Not clicking on a Edit arrow button.
47 + return;
48 + }
49 +
50 + const metabox = event.target.closest( '.menu-item' )?.querySelector( '.menu-item-settings' );
51 +
52 + if ( ! metabox?.id ) {
53 + // Should not happen.
54 + return;
55 + }
56 +
57 + if ( ! metabox.querySelectorAll( 'input[value="#pll_switcher"][type=text]' ).length ) {
58 + // Not our metabox, or already replaced.
59 + return;
60 + }
61 +
62 + // Remove default fields we don't need.
63 + [ ...metabox.children ].forEach( el => {
64 + if ( 'P' === el.nodeName && ! el.classList.contains( 'field-move' ) ) {
65 + el.remove();
66 + }
67 + } );
68 +
69 + const thus = pllNavMenu.printMetabox;
70 + const itemId = Number( metabox.id.replace( 'menu-item-settings-', '' ) );
71 +
72 + metabox.append( thus.createHiddenInput( 'title', itemId, pll_data.title ) );
73 +
74 + metabox.append( thus.createHiddenInput( 'url', itemId, '#pll_switcher' ) );
75 + metabox.append( thus.createHiddenInput( 'pll-detect', itemId, 1 ) );
76 +
77 + const menuValues =
78 + typeof pll_data.val[ itemId ] !== 'undefined' ? pll_data.val[ itemId ] : {}; // eslint-disable-line security/detect-object-injection -- Data generated by PLL_Admin_Nav_Menu::admin_enqueue_scripts() in src/admin/admin-nav-menu.php.
79 +
80 + // Create a global wrapper for our settings.
81 + const settingsWrapper = thus.createElement( 'div', {
82 + class: 'polylang-language-switcher-menu-content',
83 + } );
84 + metabox.prepend( settingsWrapper );
85 +
86 + // Wrap sections in `fieldset` tags.
87 + let sectionWrapper = settingsWrapper; // This 1st value will never be used.
88 +
89 + // Add our settings.
90 + for ( const [ optionName, optionData ] of Object.entries( pll_data.data ) ) {
91 + const optionValue =
92 + typeof menuValues[ optionName ] !== 'undefined' // eslint-disable-line security/detect-object-injection -- Data generated by PLL_Admin_Nav_Menu::admin_enqueue_scripts() in src/admin/admin-nav-menu.php.
93 + ? menuValues[ optionName ] // eslint-disable-line security/detect-object-injection -- Data generated by PLL_Admin_Nav_Menu::admin_enqueue_scripts() in src/admin/admin-nav-menu.php.
94 + : optionData.default;
95 +
96 + // Create a section with title.
97 + if ( optionData.section ) {
98 + sectionWrapper = thus.createElement( 'fieldset' );
99 + settingsWrapper.append( sectionWrapper );
100 +
101 + const sectionTitle = thus.createElement( 'legend' );
102 + sectionTitle.innerText = optionData.section;
103 + sectionWrapper.append( sectionTitle );
104 + }
105 +
106 + // Create the row wrapper.
107 + const inputWrapper = thus.createWrapper( optionData, menuValues );
108 + sectionWrapper.append( inputWrapper );
109 +
110 + // Create the label.
111 + const label = thus.createElement( 'label', {
112 + for: `edit-menu-item-${ optionName }-${ itemId }`,
113 + } );
114 + label.innerText = ` ${ optionData.label } `;
115 + inputWrapper.append( label );
116 +
117 + // Create the input.
118 + if ( optionData.choices ) {
119 + const input = thus.createSelectInput(
120 + optionName,
121 + itemId,
122 + optionValue,
123 + optionData.choices
78 124 );
125 + label.append( input );
126 + } else {
127 + const input = thus.createCheckboxInput( optionName, itemId, optionValue );
128 + label.prepend( input );
129 + }
130 + }
131 + },
79 132
80 - // disallow unchecking both show names and show flags
81 - $( '.menu-item-data-object-id' ).each(
82 - function() {
83 - var id = $( this ).val();
84 - var options = ['names-', 'flags-'];
85 - $.each(
86 - options,
87 - function( i, v ) {
88 - $( '#edit-menu-item-show_' + v + id ).on(
89 - 'change',
90 - function() {
91 - if ( true != $( this ).prop( 'checked' ) ) {
92 - $( '#edit-menu-item-show_' + options[ 1 - i ] + id ).prop( 'checked', true );
93 - }
94 - }
95 - );
96 - }
97 - );
98 - }
99 - );
133 + /**
134 + * Creates and returns a `<input type="hidden"/>` element.
135 + *
136 + * @param {string} id An identifier for this input. It will be part of the final `id` attribute.
137 + * @param {number} itemId The ID of the menu element (post ID).
138 + * @param {string|number} value The input's value.
139 + * @return {HTMLElement} The input element.
140 + */
141 + createHiddenInput: ( id, itemId, value ) => {
142 + return pllNavMenu.printMetabox.createInput( id, itemId, value, 'hidden' );
143 + },
144 +
145 + /**
146 + * Creates and returns a wrapper row.
147 + *
148 + * @param {Object} optionData A field definition.
149 + * @param {Object} {optionData.hide_if} Optional. "Hide row" conditions.
150 + * @param {Object} menuValues The input values.
151 + * @return {HTMLElement} The input element, wrapped in a `<p>` tag.
152 + */
153 + createWrapper: ( optionData, menuValues ) => {
154 + const wrapperAtts = { class: 'description' };
155 +
156 + if ( ! optionData.hide_if ) {
157 + return pllNavMenu.printMetabox.createElement( 'p', wrapperAtts );
158 + }
159 +
160 + for ( const [ conditionName, conditionValue ] of Object.entries( optionData.hide_if ) ) {
161 + // eslint-disable-next-line security/detect-object-injection -- Data generated by PLL_Admin_Nav_Menu::admin_enqueue_scripts() in src/admin/admin-nav-menu.php.
162 + if ( typeof pll_data.data[ conditionName ] === 'undefined' ) {
163 + // Should not happen.
164 + continue;
100 165 }
166 +
167 + const menuValue =
168 + typeof menuValues[ conditionName ] !== 'undefined' // eslint-disable-line security/detect-object-injection -- Data generated by PLL_Admin_Nav_Menu::admin_enqueue_scripts() in src/admin/admin-nav-menu.php.
169 + ? menuValues[ conditionName ] // eslint-disable-line security/detect-object-injection -- Data generated by PLL_Admin_Nav_Menu::admin_enqueue_scripts() in src/admin/admin-nav-menu.php.
170 + : pll_data.data[ conditionName ].default; // eslint-disable-line security/detect-object-injection -- Data generated by PLL_Admin_Nav_Menu::admin_enqueue_scripts() in src/admin/admin-nav-menu.php.
171 + wrapperAtts.class += ` pll-hidden-if-${ conditionName }-${ conditionValue }`;
172 +
173 + if ( conditionValue === menuValue ) {
174 + wrapperAtts.class += ` pll-hidden-by-${ conditionName }`;
175 + }
101 176 }
102 - );
103 - }
104 -);
177 +
178 + return pllNavMenu.printMetabox.createElement( 'p', wrapperAtts );
179 + },
180 +
181 + /**
182 + * Creates and returns a `<input type="checkbox"/>` element.
183 + *
184 + * @param {string} id An identifier for this input. It will be part of the final `id` attribute.
185 + * @param {number} itemId The ID of the menu element (post ID).
186 + * @param {boolean|number} value The option value.
187 + * @return {HTMLElement} The input element.
188 + */
189 + createCheckboxInput: ( id, itemId, value ) => {
190 + const input = pllNavMenu.printMetabox.createInput( id, itemId, 1, 'checkbox' );
191 + input.setAttribute( 'data-key', id );
192 + if ( value ) {
193 + input.checked = true;
194 + }
195 + return input;
196 + },
197 +
198 + /**
199 + * Creates and returns a `<select/>` element.
200 + *
201 + * @param {string} id An identifier for this input. It will be part of the final `id` attribute.
202 + * @param {number} itemId The ID of the menu element (post ID).
203 + * @param {string|number} value The option value.
204 + * @param {Object} choices Choices.
205 + * @return {HTMLElement} The input element.
206 + */
207 + createSelectInput: ( id, itemId, value, choices ) => {
208 + const input = pllNavMenu.printMetabox.createElement( 'select', {
209 + id: `edit-menu-item-${ id }-${ itemId }`,
210 + name: `menu-item-${ id }[${ itemId }]`,
211 + 'data-key': id,
212 + } );
213 + for ( const [ optionValue, optionLabel ] of Object.entries( choices ) ) {
214 + const atts = { value: optionValue };
215 + if ( value === optionValue ) {
216 + atts.selected = 'selected';
217 + }
218 + const option = pllNavMenu.printMetabox.createElement( 'option', atts );
219 + option.innerText = optionLabel;
220 + input.append( option );
221 + }
222 + return input;
223 + },
224 +
225 + /**
226 + * Creates and returns a `<input/>` element.
227 + *
228 + * @param {string} id An identifier for this input. It will be part of the final `id` attribute.
229 + * @param {number} itemId The ID of the menu element (post ID).
230 + * @param {string|number} value The input's value.
231 + * @param {string} type The type of input.
232 + * @return {HTMLElement} The input element.
233 + */
234 + createInput: ( id, itemId, value, type ) => {
235 + return pllNavMenu.printMetabox.createElement( 'input', {
236 + type,
237 + id: `edit-menu-item-${ id }-${ itemId }`,
238 + name: `menu-item-${ id }[${ itemId }]`,
239 + value,
240 + } );
241 + },
242 +
243 + /**
244 + * Creates and returns an element.
245 + *
246 + * @param {string} type Element's type.
247 + * @param {Object} {atts} Optional. Element's attributes.
248 + * @return {HTMLElement} The element.
249 + */
250 + createElement: ( type, atts = {} ) => {
251 + const el = document.createElement( type );
252 + for ( const [ key, value ] of Object.entries( atts ) ) {
253 + el.setAttribute( key, value );
254 + }
255 + return el;
256 + },
257 + },
258 +
259 + manageRowsAndValues: {
260 + /**
261 + * Event callback that hides rows and forbids disabling both flag and name/code.
262 + *
263 + * @param {Event} event The event.
264 + */
265 + handleEvent: event => {
266 + let value = '';
267 +
268 + if ( 'SELECT' === event.target.nodeName ) {
269 + value = event.target.value;
270 + } else if ( 'INPUT' === event.target.nodeName && 'checkbox' === event.target.type ) {
271 + value = event.target.checked;
272 + } else {
273 + return;
274 + }
275 +
276 + const wrapper = event.target.closest( '.polylang-language-switcher-menu-content' );
277 +
278 + if ( ! wrapper ) {
279 + return;
280 + }
281 +
282 + // Hide rows.
283 + const key = event.target.getAttribute( 'data-key' );
284 +
285 + if ( ! key ) {
286 + return;
287 + }
288 +
289 + const valueSelector = typeof value === 'string' ? value.replaceAll( ':', '\\:' ) : value; // For flag format.
290 + wrapper
291 + .querySelectorAll(
292 + `[class*="pll-hidden-if-${ key }-"]:not(.pll-hidden-if-${ key }-${ valueSelector })`
293 + )
294 + .forEach( input => {
295 + input.classList.remove( `pll-hidden-by-${ key }` );
296 + } );
297 + wrapper.querySelectorAll( `.pll-hidden-if-${ key }-${ valueSelector }` ).forEach( input => {
298 + input.classList.add( `pll-hidden-by-${ key }` );
299 + } );
300 +
301 + // Forbid disabling both flag and name/code.
302 + if ( 'show_labels' === key && '' === value ) {
303 + const otherInput = wrapper.querySelector( '[data-key="show_flags"]' );
304 +
305 + if ( otherInput && true !== otherInput.checked ) {
306 + otherInput.checked = true;
307 + otherInput.dispatchEvent( new Event( 'change', { bubbles: true } ) );
308 + }
309 + } else if ( 'show_flags' === key && false === value ) {
310 + const otherInput = wrapper.querySelector( '[data-key="show_labels"]' );
311 +
312 + if ( otherInput && '' === otherInput.value ) {
313 + otherInput.value = 'names';
314 + otherInput.dispatchEvent( new Event( 'change', { bubbles: true } ) );
315 + }
316 + }
317 + },
318 + },
319 +};
320 +
321 +pllNavMenu.init();
105 322