| @@ -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 | }; |