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 +192 -90 3.8.9 → trunk View file →
@@ -1,8 +1,6 @@
1 1 /**
2 2 * Handles the options in the language switcher nav menu metabox.
3 - *
4 - * @package Polylang
5 3 */
6 4
7 5 const pllNavMenu = {
8 6 /**
@@ -33,10 +31,9 @@
33 31 return;
34 32 }
35 33
36 34 pllNavMenu.wrapper.addEventListener( 'click', pllNavMenu.printMetabox );
37 - pllNavMenu.wrapper.addEventListener( 'change', pllNavMenu.ensureContent );
38 - pllNavMenu.wrapper.addEventListener( 'change', pllNavMenu.showHideRows );
35 + pllNavMenu.wrapper.addEventListener( 'change', pllNavMenu.manageRowsAndValues );
39 36 },
40 37
41 38 printMetabox: {
42 39 /**
@@ -43,15 +40,15 @@
43 40 * Event callback that prints our checkboxes in the language switcher.
44 41 *
45 42 * @param {Event} event The event.
46 43 */
47 - handleEvent: ( event ) => {
44 + handleEvent: event => {
48 45 if ( ! event.target.classList.contains( 'item-edit' ) ) {
49 46 // Not clicking on a Edit arrow button.
50 47 return;
51 48 }
52 49
53 - const metabox = event.target.closest( '.menu-item' ).querySelector( '.menu-item-settings' );
50 + const metabox = event.target.closest( '.menu-item' )?.querySelector( '.menu-item-settings' );
54 51
55 52 if ( ! metabox?.id ) {
56 53 // Should not happen.
57 54 return;
@@ -62,72 +59,185 @@
62 59 return;
63 60 }
64 61
65 62 // Remove default fields we don't need.
66 - [ ...metabox.children ].forEach( ( el ) => {
63 + [ ...metabox.children ].forEach( el => {
67 64 if ( 'P' === el.nodeName && ! el.classList.contains( 'field-move' ) ) {
68 65 el.remove();
69 66 }
70 67 } );
71 68
72 - const t = pllNavMenu.printMetabox;
69 + const thus = pllNavMenu.printMetabox;
73 70 const itemId = Number( metabox.id.replace( 'menu-item-settings-', '' ) );
74 71
75 - metabox.append( t.createHiddenInput( 'title', itemId, pll_data.title ) ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
76 - metabox.append( t.createHiddenInput( 'url', itemId, '#pll_switcher' ) ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
77 - metabox.append( t.createHiddenInput( 'pll-detect', itemId, 1 ) ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
72 + metabox.append( thus.createHiddenInput( 'title', itemId, pll_data.title ) );
78 73
79 - const ids = Array( 'hide_if_no_translation', 'hide_current', 'force_home', 'show_flags', 'show_names', 'dropdown' ); // Reverse order.
80 - const isValDefined = typeof( pll_data.val[ itemId ] ) !== 'undefined';
74 + metabox.append( thus.createHiddenInput( 'url', itemId, '#pll_switcher' ) );
75 + metabox.append( thus.createHiddenInput( 'pll-detect', itemId, 1 ) );
81 76
82 - ids.forEach( ( optionName ) => {
83 - // Create the checkbox's wrapper.
84 - const inputWrapper = t.createElement( 'p', { class: 'description' } );
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.
85 79
86 - if ( 'hide_current' === optionName && isValDefined && 1 === pll_data.val[ itemId ].dropdown ) {
87 - // Hide the `hide_current` checkbox if `dropdown` is checked.
88 - inputWrapper.classList.add( 'hidden' );
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 );
89 104 }
90 105
91 - metabox.prepend( inputWrapper ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend
106 + // Create the row wrapper.
107 + const inputWrapper = thus.createWrapper( optionData, menuValues );
108 + sectionWrapper.append( inputWrapper );
92 109
93 - // Create the checkbox's label.
94 - const inputId = `edit-menu-item-${ optionName }-${ itemId }`;
95 - const label = t.createElement( 'label', { 'for': inputId } );
96 - label.innerText = ` ${ pll_data.strings[ optionName ] }`;
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 );
97 116
98 - inputWrapper.append( label ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.append
117 + // Create the input.
118 + if ( optionData.choices ) {
119 + const input = thus.createSelectInput(
120 + optionName,
121 + itemId,
122 + optionValue,
123 + optionData.choices
124 + );
125 + label.append( input );
126 + } else {
127 + const input = thus.createCheckboxInput( optionName, itemId, optionValue );
128 + label.prepend( input );
129 + }
130 + }
131 + },
99 132
100 - // Create the checkbox.
101 - const cb = t.createElement( 'input', {
102 - type: 'checkbox',
103 - id: inputId,
104 - name: `menu-item-${ optionName }[${ itemId }]`,
105 - value: 1,
106 - } );
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 + },
107 144
108 - if ( ( isValDefined && 1 === pll_data.val[ itemId ][ optionName ] ) || ( ! isValDefined && 'show_names' === optionName ) ) { // `show_names` as default value.
109 - cb.checked = true;
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;
110 165 }
111 166
112 - label.prepend( cb ); // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.prepend
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 + }
176 + }
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,
113 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;
114 223 },
115 224
116 225 /**
117 - * Creates and returns a `<input type=hidden"/>` element.
226 + * Creates and returns a `<input/>` element.
118 227 *
119 228 * @param {string} id An identifier for this input. It will be part of the final `id` attribute.
120 229 * @param {number} itemId The ID of the menu element (post ID).
121 230 * @param {string|number} value The input's value.
231 + * @param {string} type The type of input.
122 232 * @return {HTMLElement} The input element.
123 233 */
124 - createHiddenInput: ( id, itemId, value ) => {
234 + createInput: ( id, itemId, value, type ) => {
125 235 return pllNavMenu.printMetabox.createElement( 'input', {
126 - type: 'hidden',
127 - id: `edit-menu-item-${ id }-${ itemId }`,
128 - name: `menu-item-${ id }[${ itemId }]`,
129 - value: value,
236 + type,
237 + id: `edit-menu-item-${ id }-${ itemId }`,
238 + name: `menu-item-${ id }[${ itemId }]`,
239 + value,
130 240 } );
131 241 },
132 242
133 243 /**
@@ -132,13 +242,13 @@
132 242
133 243 /**
134 244 * Creates and returns an element.
135 245 *
136 - * @param {string} type Element's type.
137 - * @param {Object} atts Element's attributes.
246 + * @param {string} type Element's type.
247 + * @param {Object} {atts} Optional. Element's attributes.
138 248 * @return {HTMLElement} The element.
139 249 */
140 - createElement: ( type, atts ) => {
250 + createElement: ( type, atts = {} ) => {
141 251 const el = document.createElement( type );
142 252 for ( const [ key, value ] of Object.entries( atts ) ) {
143 253 el.setAttribute( key, value );
144 254 }
@@ -145,73 +255,65 @@
145 255 return el;
146 256 },
147 257 },
148 258
149 - ensureContent: {
150 - regExpr: new RegExp( /^edit-menu-item-show_(names|flags)-(\d+)$/ ),
151 -
259 + manageRowsAndValues: {
152 260 /**
153 - * Event callback that disallows unchecking both `show_names` and `show_flags`.
261 + * Event callback that hides rows and forbids disabling both flag and name/code.
154 262 *
155 263 * @param {Event} event The event.
156 264 */
157 - handleEvent: ( event ) => {
158 - if ( ! event.target.id || event.target.checked ) {
159 - // Now checked, nothing to do.
160 - return;
161 - }
265 + handleEvent: event => {
266 + let value = '';
162 267
163 - const matches = event.target.id.match( pllNavMenu.ensureContent.regExpr );
164 -
165 - if ( ! matches ) {
166 - // Not the checkbox we want.
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 {
167 273 return;
168 274 }
169 275
170 - // Check the other checkbox.
171 - const [ , type, id ] = matches;
172 - const otherType = 'names' === type ? 'flags' : 'names';
173 - document.getElementById( `edit-menu-item-show_${ otherType }-${ id }` ).checked = true;
174 - },
175 - },
276 + const wrapper = event.target.closest( '.polylang-language-switcher-menu-content' );
176 277
177 - showHideRows: {
178 - regExpr: new RegExp( /^edit-menu-item-dropdown-(\d+)$/ ),
179 -
180 - /**
181 - * Event callback that shows or hides the `hide_current` checkbox when `dropdown` is checked.
182 - *
183 - * @param {Event} event The event.
184 - */
185 - handleEvent: ( event ) => {
186 - if ( ! event.target.id ) {
187 - // Not the checkbox we want.
278 + if ( ! wrapper ) {
188 279 return;
189 280 }
190 281
191 - const matches = event.target.id.match( pllNavMenu.showHideRows.regExpr );
282 + // Hide rows.
283 + const key = event.target.getAttribute( 'data-key' );
192 284
193 - if ( ! matches ) {
194 - // Not the checkbox we want.
285 + if ( ! key ) {
195 286 return;
196 287 }
197 288
198 - const hideCb = document.getElementById( `edit-menu-item-hide_current-${ matches[1] }` );
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 + } );
199 300
200 - if ( ! hideCb ) {
201 - // Should not happen.
202 - return;
203 - }
301 + // Forbid disabling both flag and name/code.
302 + if ( 'show_labels' === key && '' === value ) {
303 + const otherInput = wrapper.querySelector( '[data-key="show_flags"]' );
204 304
205 - const description = hideCb.closest( '.description' );
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"]' );
206 311
207 - // Hide or show.
208 - description.classList.toggle( 'hidden', event.target.checked );
209 -
210 - if ( event.target.checked ) {
211 - // Uncheck after hiding.
212 - hideCb.checked = false;
213 - hideCb.dispatchEvent( new Event( 'change' ) );
312 + if ( otherInput && '' === otherInput.value ) {
313 + otherInput.value = 'names';
314 + otherInput.dispatchEvent( new Event( 'change', { bubbles: true } ) );
315 + }
214 316 }
215 317 },
216 318 },
217 319 };