Commit 2c9e2e48efa for woocommerce
commit 2c9e2e48efae428ff246a5df934b673e78cc2db2
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date: Wed Jun 24 12:58:45 2026 +0200
Minor enhancements to product swatches and editing Color attributes in the editor (#65693)
* Format wc-enhanced-select.js
* Add changelog
* Make product swatches bigger so they are more accessible
* Add badge to global attributes in the product editor
* Rename 'Add existing' to 'Add global' when adding attributes to variable products
* Add swatches to visual attributes in the product editor
* Update PHPStan
* CodeRabbit suggestions
* PHPStan fix
diff --git a/plugins/woocommerce/changelog/update-swatches-improvements b/plugins/woocommerce/changelog/update-swatches-improvements
new file mode 100644
index 00000000000..a109e6faf58
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-swatches-improvements
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Minor enhancements to product swatches and editing Color attributes in the editor
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss
index 4a43dbc60ac..cdd0d1a403f 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss
@@ -146,8 +146,8 @@
background-position: center;
background-repeat: no-repeat;
display: inline-block;
- width: 1.8em;
- height: 1.8em;
+ width: 2.3em;
+ height: 2.3em;
border-radius: 50%;
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
flex-shrink: 0;
diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss
index 6ddce06519e..636d5337114 100644
--- a/plugins/woocommerce/client/legacy/css/admin.scss
+++ b/plugins/woocommerce/client/legacy/css/admin.scss
@@ -6619,6 +6619,7 @@ img.help_tip {
margin-top: 0.25em;
}
}
+
&.woocommerce_variation h3 {
a.delete,
a.edit,
@@ -6627,6 +6628,19 @@ img.help_tip {
}
}
+ &.woocommerce_attribute h3 .woocommerce-attribute-global-badge {
+ display: inline-block;
+ margin-inline-start: 6px;
+ border-radius: 4px;
+ background: $gray-100;
+ padding: 4px 6px;
+ color: #3c434a;
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 16px;
+ vertical-align: middle;
+ }
+
table {
width: 100%;
position: relative;
@@ -9746,6 +9760,34 @@ body.woocommerce-settings-payments-section_legacy {
}
}
+.wc-admin-visual-attribute-term-option {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+
+ .wc-admin-color-swatch {
+ margin-right: 0;
+ flex-shrink: 0;
+ height: 16px;
+ width: 16px;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ }
+}
+
+.select2-selection__choice .wc-admin-visual-attribute-term-option {
+ gap: 6px;
+ vertical-align: middle;
+ margin-bottom: 1px;
+ margin-inline-start: 2px;
+}
+
+.select2-results__option .wc-admin-visual-attribute-term-option {
+ margin-top: 2px;
+ width: 100%;
+}
+
.wc-admin-visual-attribute-type fieldset {
display: flex;
gap: 1.5em;
diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js
index afff111b241..75343c67b12 100644
--- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js
+++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js
@@ -6,6 +6,28 @@ jQuery( function ( $ ) {
isPageUnloading = true;
} );
+ function appendAttributeTermOption( select, term ) {
+ if (
+ ! select ||
+ ! term ||
+ term.term_id === undefined ||
+ term.name === undefined
+ ) {
+ return;
+ }
+
+ const option = document.createElement( 'option' );
+ option.value = String( term.term_id );
+ option.selected = true;
+ option.textContent = term.name;
+
+ if ( term.visual ) {
+ option.dataset.visual = JSON.stringify( term.visual );
+ }
+
+ select.appendChild( option );
+ }
+
// Scroll to first checked category
// https://github.com/scribu/wp-category-checklist-tree/blob/d1c3c1f449e1144542efa17dde84a9f52ade1739/category-checklist-tree.php
$( function () {
@@ -791,15 +813,12 @@ jQuery( function ( $ ) {
if ( currentItem && currentItem.length > 0 ) {
currentItem.prop( 'selected', 'selected' );
} else {
- $wrapper
- .find( 'select.attribute_values' )
- .append(
- '<option value="' +
- term.term_id +
- '" selected="selected">' +
- term.name +
- '</option>'
- );
+ appendAttributeTermOption(
+ $wrapper.find(
+ 'select.attribute_values'
+ )[ 0 ],
+ term
+ );
}
} );
$wrapper
@@ -1023,11 +1042,11 @@ jQuery( function ( $ ) {
'select.attribute_values'
);
if ( select ) {
- const option = document.createElement( 'option' );
- option.value = String( response.term_id );
- option.selected = true;
- option.textContent = response.name;
- select.appendChild( option );
+ appendAttributeTermOption( select, {
+ term_id: response.term_id,
+ name: response.name,
+ visual: response.visual,
+ } );
// Trigger change event natively.
const changeEvent = new Event( 'change', {
@@ -1062,8 +1081,7 @@ jQuery( function ( $ ) {
const wrapper = this.closest( '.woocommerce_attribute' );
const attribute = wrapper ? wrapper.dataset.taxonomy : '';
- const isVisualAttribute =
- this.dataset.isVisualAttribute === 'yes';
+ const isVisualAttribute = this.dataset.isVisualAttribute === 'yes';
currentAttributeTermCreationContext = {
wrapper,
@@ -1455,9 +1473,9 @@ jQuery( function ( $ ) {
const setProductImageLink = $( '#set-post-thumbnail' );
// Escape the translated label before interpolating into the attribute so a
// translation containing quotes or markup cannot break the rendered span.
- const tooltipMarkup = `<span class="woocommerce-help-tip" tabindex="0" aria-label="${
- _.escape( woocommerce_admin_meta_boxes.i18n_product_image_tip )
- }"></span>`;
+ const tooltipMarkup = `<span class="woocommerce-help-tip" tabindex="0" aria-label="${ _.escape(
+ woocommerce_admin_meta_boxes.i18n_product_image_tip
+ ) }"></span>`;
const tooltipData = {
attribute: 'data-tip',
content: woocommerce_admin_meta_boxes.i18n_product_image_tip,
diff --git a/plugins/woocommerce/client/legacy/js/admin/wc-enhanced-select.js b/plugins/woocommerce/client/legacy/js/admin/wc-enhanced-select.js
index 3a32ad534a2..4cf2793315b 100644
--- a/plugins/woocommerce/client/legacy/js/admin/wc-enhanced-select.js
+++ b/plugins/woocommerce/client/legacy/js/admin/wc-enhanced-select.js
@@ -1,104 +1,235 @@
/*global wc_enhanced_select_params */
-jQuery( function( $ ) {
+jQuery( function ( $ ) {
+ function getVisualTermSwatchStyle( visual ) {
+ if ( ! visual.type || ! visual.value ) {
+ return '';
+ }
+
+ if ( 'image' === visual.type ) {
+ return (
+ "background-image:url('" +
+ String( visual.value ).replace( /'/g, '%27' ) +
+ "')"
+ );
+ }
+
+ if ( 'color' === visual.type ) {
+ return 'background-color:' + visual.value;
+ }
+
+ return '';
+ }
+
+ function formatVisualTermOption( term ) {
+ const swatchStyle = getVisualTermSwatchStyle( term.visual );
+ const swatchClass =
+ 'wc-admin-color-swatch' + ( swatchStyle ? '' : ' is-empty' );
+ const wrapper = document.createElement( 'span' );
+ wrapper.className = 'wc-admin-visual-attribute-term-option';
+
+ const swatch = document.createElement( 'span' );
+ swatch.className = swatchClass;
+ if ( swatchStyle ) {
+ swatch.setAttribute( 'style', swatchStyle );
+ }
+ swatch.setAttribute( 'aria-hidden', 'true' );
+
+ const label = document.createElement( 'span' );
+ label.className = 'wc-admin-visual-attribute-term-label';
+ label.textContent = term.text;
+
+ wrapper.appendChild( swatch );
+ wrapper.appendChild( label );
+
+ return wrapper;
+ }
+
+ function getTermDataWithVisual( $select, data ) {
+ if ( data.visual ) {
+ return data;
+ }
+
+ const $option = $select.find( 'option' ).filter( function () {
+ return $( this ).val() === String( data.id );
+ } );
+
+ if ( ! $option.length ) {
+ return data;
+ }
+
+ const visual = $option.data( 'visual' );
+
+ if ( ! visual ) {
+ return data;
+ }
+
+ return Object.assign( {}, data, { visual: visual } );
+ }
+
+ function updateVisualAttributeTermChoices( $select ) {
+ var $container = $select.next( '.select2-container' );
+
+ if ( ! $container.length ) {
+ return;
+ }
+
+ $container.find( '.select2-selection__choice' ).each( function () {
+ const $choice = $( this );
+ const data = $choice.data( 'data' );
+
+ if ( ! data ) {
+ return;
+ }
+
+ const $removeButton = $choice
+ .find( '.select2-selection__choice__remove' )
+ .detach();
+ const termData = getTermDataWithVisual( $select, data );
+
+ $choice
+ .empty()
+ .append( $removeButton )
+ .append( formatVisualTermOption( termData ) );
+ $choice.prop( 'title', termData.title || termData.text );
+ } );
+ }
function getEnhancedSelectFormatString() {
return {
- 'language': {
- errorLoading: function() {
+ language: {
+ errorLoading: function () {
// Workaround for https://github.com/select2/select2/issues/4355 instead of i18n_ajax_error.
return wc_enhanced_select_params.i18n_searching;
},
- inputTooLong: function( args ) {
+ inputTooLong: function ( args ) {
var overChars = args.input.length - args.maximum;
if ( 1 === overChars ) {
return wc_enhanced_select_params.i18n_input_too_long_1;
}
- return wc_enhanced_select_params.i18n_input_too_long_n.replace( '%qty%', overChars );
+ return wc_enhanced_select_params.i18n_input_too_long_n.replace(
+ '%qty%',
+ overChars
+ );
},
- inputTooShort: function( args ) {
+ inputTooShort: function ( args ) {
var remainingChars = args.minimum - args.input.length;
if ( 1 === remainingChars ) {
return wc_enhanced_select_params.i18n_input_too_short_1;
}
- return wc_enhanced_select_params.i18n_input_too_short_n.replace( '%qty%', remainingChars );
+ return wc_enhanced_select_params.i18n_input_too_short_n.replace(
+ '%qty%',
+ remainingChars
+ );
},
- loadingMore: function() {
+ loadingMore: function () {
return wc_enhanced_select_params.i18n_load_more;
},
- maximumSelected: function( args ) {
+ maximumSelected: function ( args ) {
if ( args.maximum === 1 ) {
return wc_enhanced_select_params.i18n_selection_too_long_1;
}
- return wc_enhanced_select_params.i18n_selection_too_long_n.replace( '%qty%', args.maximum );
+ return wc_enhanced_select_params.i18n_selection_too_long_n.replace(
+ '%qty%',
+ args.maximum
+ );
},
- noResults: function() {
+ noResults: function () {
return wc_enhanced_select_params.i18n_no_matches;
},
- searching: function() {
+ searching: function () {
return wc_enhanced_select_params.i18n_searching;
- }
- }
+ },
+ },
};
}
try {
$( document.body )
-
- .on( 'wc-enhanced-select-init', function() {
-
+ .on( 'wc-enhanced-select-init', function () {
// Regular select boxes
- $( ':input.wc-enhanced-select, :input.chosen_select' ).filter( ':not(.enhanced)' ).each( function() {
- var select2_args = $.extend({
- minimumResultsForSearch: 10,
- allowClear: $( this ).data( 'allow_clear' ) ? true : false,
- placeholder: $( this ).data( 'placeholder' )
- }, getEnhancedSelectFormatString() );
-
- $( this ).selectWoo( select2_args ).addClass( 'enhanced' );
- });
-
- $( ':input.wc-enhanced-select-nostd, :input.chosen_select_nostd' ).filter( ':not(.enhanced)' ).each( function() {
- var select2_args = $.extend({
- minimumResultsForSearch: 10,
- allowClear: true,
- placeholder: $( this ).data( 'placeholder' )
- }, getEnhancedSelectFormatString() );
-
- $( this ).selectWoo( select2_args ).addClass( 'enhanced' );
- });
+ $( ':input.wc-enhanced-select, :input.chosen_select' )
+ .filter( ':not(.enhanced)' )
+ .each( function () {
+ var select2_args = $.extend(
+ {
+ minimumResultsForSearch: 10,
+ allowClear: $( this ).data( 'allow_clear' )
+ ? true
+ : false,
+ placeholder: $( this ).data( 'placeholder' ),
+ },
+ getEnhancedSelectFormatString()
+ );
+
+ $( this )
+ .selectWoo( select2_args )
+ .addClass( 'enhanced' );
+ } );
+
+ $(
+ ':input.wc-enhanced-select-nostd, :input.chosen_select_nostd'
+ )
+ .filter( ':not(.enhanced)' )
+ .each( function () {
+ var select2_args = $.extend(
+ {
+ minimumResultsForSearch: 10,
+ allowClear: true,
+ placeholder: $( this ).data( 'placeholder' ),
+ },
+ getEnhancedSelectFormatString()
+ );
+
+ $( this )
+ .selectWoo( select2_args )
+ .addClass( 'enhanced' );
+ } );
function display_result( self, select2_args ) {
- select2_args = $.extend( select2_args, getEnhancedSelectFormatString() );
+ select2_args = $.extend(
+ select2_args,
+ getEnhancedSelectFormatString()
+ );
$( self ).selectWoo( select2_args ).addClass( 'enhanced' );
if ( $( self ).data( 'sortable' ) ) {
- var $select = $(self);
- var $list = $( self ).next( '.select2-container' ).find( 'ul.select2-selection__rendered' );
-
- $list.sortable({
- placeholder : 'ui-state-highlight select2-selection__choice',
+ var $select = $( self );
+ var $list = $( self )
+ .next( '.select2-container' )
+ .find( 'ul.select2-selection__rendered' );
+
+ $list.sortable( {
+ placeholder:
+ 'ui-state-highlight select2-selection__choice',
forcePlaceholderSize: true,
- items : 'li:not(.select2-search__field)',
- tolerance : 'pointer',
- stop: function() {
- $( $list.find( '.select2-selection__choice' ).get().reverse() ).each( function() {
- var id = $( this ).data( 'data' ).id;
- var option = $select.find( 'option[value="' + id + '"]' )[0];
+ items: 'li:not(.select2-search__field)',
+ tolerance: 'pointer',
+ stop: function () {
+ $(
+ $list
+ .find( '.select2-selection__choice' )
+ .get()
+ .reverse()
+ ).each( function () {
+ var id = $( this ).data( 'data' ).id;
+ var option = $select.find(
+ 'option[value="' + id + '"]'
+ )[ 0 ];
$select.prepend( option );
} );
- }
- });
- // Keep multiselects ordered alphabetically if they are not sortable.
+ },
+ } );
+ // Keep multiselects ordered alphabetically if they are not sortable.
} else if ( $( self ).prop( 'multiple' ) ) {
- $( self ).on( 'change', function(){
+ $( self ).on( 'change', function () {
var $children = $( self ).children();
- $children.sort(function(a, b){
+ $children.sort( function ( a, b ) {
var atext = a.text.toLowerCase();
var btext = b.text.toLowerCase();
@@ -109,308 +240,487 @@ jQuery( function( $ ) {
return -1;
}
return 0;
- });
+ } );
$( self ).html( $children );
- });
+ } );
}
}
// Ajax product search box
- $( ':input.wc-product-search' ).filter( ':not(.enhanced)' ).each( function() {
- var select2_args = {
- allowClear: $( this ).data( 'allow_clear' ) ? true : false,
- placeholder: $( this ).data( 'placeholder' ),
- minimumInputLength: $( this ).data( 'minimum_input_length' ) ? $( this ).data( 'minimum_input_length' ) : '3',
- escapeMarkup: function( m ) {
- return m;
- },
- ajax: {
- url: wc_enhanced_select_params.ajax_url,
- dataType: 'json',
- delay: 250,
- data: function( params ) {
- return {
- term : params.term,
- action : $( this ).data( 'action' ) || 'woocommerce_json_search_products_and_variations',
- security : wc_enhanced_select_params.search_products_nonce,
- exclude : $( this ).data( 'exclude' ),
- exclude_type : $( this ).data( 'exclude_type' ),
- include : $( this ).data( 'include' ),
- limit : $( this ).data( 'limit' ),
- display_stock: $( this ).data( 'display_stock' )
- };
+ $( ':input.wc-product-search' )
+ .filter( ':not(.enhanced)' )
+ .each( function () {
+ var select2_args = {
+ allowClear: $( this ).data( 'allow_clear' )
+ ? true
+ : false,
+ placeholder: $( this ).data( 'placeholder' ),
+ minimumInputLength: $( this ).data(
+ 'minimum_input_length'
+ )
+ ? $( this ).data( 'minimum_input_length' )
+ : '3',
+ escapeMarkup: function ( m ) {
+ return m;
},
- processResults: function( data ) {
- var terms = [];
- if ( data ) {
- $.each( data, function( id, text ) {
- terms.push( { id: id, text: text } );
- });
- }
- return {
- results: terms
- };
+ ajax: {
+ url: wc_enhanced_select_params.ajax_url,
+ dataType: 'json',
+ delay: 250,
+ data: function ( params ) {
+ return {
+ term: params.term,
+ action:
+ $( this ).data( 'action' ) ||
+ 'woocommerce_json_search_products_and_variations',
+ security:
+ wc_enhanced_select_params.search_products_nonce,
+ exclude: $( this ).data( 'exclude' ),
+ exclude_type:
+ $( this ).data( 'exclude_type' ),
+ include: $( this ).data( 'include' ),
+ limit: $( this ).data( 'limit' ),
+ display_stock:
+ $( this ).data( 'display_stock' ),
+ };
+ },
+ processResults: function ( data ) {
+ var terms = [];
+ if ( data ) {
+ $.each( data, function ( id, text ) {
+ terms.push( {
+ id: id,
+ text: text,
+ } );
+ } );
+ }
+ return {
+ results: terms,
+ };
+ },
+ cache: true,
},
- cache: true
- }
- };
+ };
- display_result( this, select2_args );
- });
+ display_result( this, select2_args );
+ } );
// Ajax Page Search.
- $( ':input.wc-page-search' ).filter( ':not(.enhanced)' ).each( function() {
- var select2_args = {
- allowClear: $( this ).data( 'allow_clear' ) ? true : false,
- placeholder: $( this ).data( 'placeholder' ),
- minimumInputLength: $( this ).data( 'minimum_input_length' ) ? $( this ).data( 'minimum_input_length' ) : '3',
- escapeMarkup: function( m ) {
- return m;
- },
- ajax: {
- url: wc_enhanced_select_params.ajax_url,
- dataType: 'json',
- delay: 250,
- data: function( params ) {
- return {
- term : params.term,
- action : $( this ).data( 'action' ) || 'woocommerce_json_search_pages',
- security : wc_enhanced_select_params.search_pages_nonce,
- exclude : $( this ).data( 'exclude' ),
- post_status : $( this ).data( 'post_status' ),
- limit : $( this ).data( 'limit' ),
- };
+ $( ':input.wc-page-search' )
+ .filter( ':not(.enhanced)' )
+ .each( function () {
+ var select2_args = {
+ allowClear: $( this ).data( 'allow_clear' )
+ ? true
+ : false,
+ placeholder: $( this ).data( 'placeholder' ),
+ minimumInputLength: $( this ).data(
+ 'minimum_input_length'
+ )
+ ? $( this ).data( 'minimum_input_length' )
+ : '3',
+ escapeMarkup: function ( m ) {
+ return m;
},
- processResults: function( data ) {
- var terms = [];
- if ( data ) {
- $.each( data, function( id, text ) {
- terms.push( { id: id, text: text } );
- } );
- }
- return {
- results: terms
- };
+ ajax: {
+ url: wc_enhanced_select_params.ajax_url,
+ dataType: 'json',
+ delay: 250,
+ data: function ( params ) {
+ return {
+ term: params.term,
+ action:
+ $( this ).data( 'action' ) ||
+ 'woocommerce_json_search_pages',
+ security:
+ wc_enhanced_select_params.search_pages_nonce,
+ exclude: $( this ).data( 'exclude' ),
+ post_status:
+ $( this ).data( 'post_status' ),
+ limit: $( this ).data( 'limit' ),
+ };
+ },
+ processResults: function ( data ) {
+ var terms = [];
+ if ( data ) {
+ $.each( data, function ( id, text ) {
+ terms.push( {
+ id: id,
+ text: text,
+ } );
+ } );
+ }
+ return {
+ results: terms,
+ };
+ },
+ cache: true,
},
- cache: true
- }
- };
+ };
- $( this ).selectWoo( select2_args ).addClass( 'enhanced' );
- });
+ $( this )
+ .selectWoo( select2_args )
+ .addClass( 'enhanced' );
+ } );
// Ajax customer search boxes
- $( ':input.wc-customer-search' ).filter( ':not(.enhanced)' ).each( function() {
- var select2_args = {
- allowClear: $( this ).data( 'allow_clear' ) ? true : false,
- placeholder: $( this ).data( 'placeholder' ),
- minimumInputLength: $( this ).data( 'minimum_input_length' ) ? $( this ).data( 'minimum_input_length' ) : '1',
- escapeMarkup: function( m ) {
- return m;
- },
- ajax: {
- url: wc_enhanced_select_params.ajax_url,
- dataType: 'json',
- delay: 1000,
- data: function( params ) {
- return {
- term: params.term,
- action: 'woocommerce_json_search_customers',
- security: wc_enhanced_select_params.search_customers_nonce,
- exclude: $( this ).data( 'exclude' )
- };
+ $( ':input.wc-customer-search' )
+ .filter( ':not(.enhanced)' )
+ .each( function () {
+ var select2_args = {
+ allowClear: $( this ).data( 'allow_clear' )
+ ? true
+ : false,
+ placeholder: $( this ).data( 'placeholder' ),
+ minimumInputLength: $( this ).data(
+ 'minimum_input_length'
+ )
+ ? $( this ).data( 'minimum_input_length' )
+ : '1',
+ escapeMarkup: function ( m ) {
+ return m;
},
- processResults: function( data ) {
- var terms = [];
- if ( data ) {
- $.each( data, function( id, text ) {
- terms.push({
- id: id,
- text: text
- });
- });
- }
- return {
- results: terms
- };
+ ajax: {
+ url: wc_enhanced_select_params.ajax_url,
+ dataType: 'json',
+ delay: 1000,
+ data: function ( params ) {
+ return {
+ term: params.term,
+ action: 'woocommerce_json_search_customers',
+ security:
+ wc_enhanced_select_params.search_customers_nonce,
+ exclude: $( this ).data( 'exclude' ),
+ };
+ },
+ processResults: function ( data ) {
+ var terms = [];
+ if ( data ) {
+ $.each( data, function ( id, text ) {
+ terms.push( {
+ id: id,
+ text: text,
+ } );
+ } );
+ }
+ return {
+ results: terms,
+ };
+ },
+ cache: true,
},
- cache: true
+ };
+
+ select2_args = $.extend(
+ select2_args,
+ getEnhancedSelectFormatString()
+ );
+
+ $( this )
+ .selectWoo( select2_args )
+ .addClass( 'enhanced' );
+
+ if ( $( this ).data( 'sortable' ) ) {
+ var $select = $( this );
+ var $list = $( this )
+ .next( '.select2-container' )
+ .find( 'ul.select2-selection__rendered' );
+
+ $list.sortable( {
+ placeholder:
+ 'ui-state-highlight select2-selection__choice',
+ forcePlaceholderSize: true,
+ items: 'li:not(.select2-search__field)',
+ tolerance: 'pointer',
+ stop: function () {
+ $(
+ $list
+ .find(
+ '.select2-selection__choice'
+ )
+ .get()
+ .reverse()
+ ).each( function () {
+ var id = $( this ).data( 'data' ).id;
+ var option = $select.find(
+ 'option[value="' + id + '"]'
+ )[ 0 ];
+ $select.prepend( option );
+ } );
+ },
+ } );
}
- };
-
- select2_args = $.extend( select2_args, getEnhancedSelectFormatString() );
+ } );
- $( this ).selectWoo( select2_args ).addClass( 'enhanced' );
-
- if ( $( this ).data( 'sortable' ) ) {
- var $select = $(this);
- var $list = $( this ).next( '.select2-container' ).find( 'ul.select2-selection__rendered' );
+ // Ajax category search boxes
+ $( ':input.wc-category-search' )
+ .filter( ':not(.enhanced)' )
+ .each( function () {
+ var return_format = $( this ).data( 'return_id' )
+ ? 'id'
+ : 'slug';
+
+ var select2_args = $.extend(
+ {
+ allowClear: $( this ).data( 'allow_clear' )
+ ? true
+ : false,
+ placeholder: $( this ).data( 'placeholder' ),
+ minimumInputLength: $( this ).data(
+ 'minimum_input_length'
+ )
+ ? $( this ).data( 'minimum_input_length' )
+ : '3',
+ escapeMarkup: function ( m ) {
+ return m;
+ },
+ ajax: {
+ url: wc_enhanced_select_params.ajax_url,
+ dataType: 'json',
+ delay: 250,
+ data: function ( params ) {
+ return {
+ term: params.term,
+ action: 'woocommerce_json_search_categories',
+ security:
+ wc_enhanced_select_params.search_categories_nonce,
+ };
+ },
+ processResults: function ( data ) {
+ var terms = [];
+ if ( data ) {
+ $.each(
+ data,
+ function ( id, term ) {
+ terms.push( {
+ id:
+ 'id' ===
+ return_format
+ ? term.term_id
+ : term.slug,
+ text: term.formatted_name,
+ } );
+ }
+ );
+ }
+ return {
+ results: terms,
+ };
+ },
+ cache: true,
+ },
+ },
+ getEnhancedSelectFormatString()
+ );
- $list.sortable({
- placeholder : 'ui-state-highlight select2-selection__choice',
- forcePlaceholderSize: true,
- items : 'li:not(.select2-search__field)',
- tolerance : 'pointer',
- stop: function() {
- $( $list.find( '.select2-selection__choice' ).get().reverse() ).each( function() {
- var id = $( this ).data( 'data' ).id;
- var option = $select.find( 'option[value="' + id + '"]' )[0];
- $select.prepend( option );
- } );
- }
- });
- }
- });
+ $( this )
+ .selectWoo( select2_args )
+ .addClass( 'enhanced' );
+ } );
// Ajax category search boxes
- $( ':input.wc-category-search' ).filter( ':not(.enhanced)' ).each( function() {
- var return_format = $( this ).data( 'return_id' ) ? 'id' : 'slug';
-
- var select2_args = $.extend( {
- allowClear : $( this ).data( 'allow_clear' ) ? true : false,
- placeholder : $( this ).data( 'placeholder' ),
- minimumInputLength: $( this ).data( 'minimum_input_length' ) ? $( this ).data( 'minimum_input_length' ) : '3',
- escapeMarkup : function( m ) {
- return m;
- },
- ajax: {
- url: wc_enhanced_select_params.ajax_url,
- dataType: 'json',
- delay: 250,
- data: function( params ) {
- return {
- term: params.term,
- action: 'woocommerce_json_search_categories',
- security: wc_enhanced_select_params.search_categories_nonce
- };
+ $( ':input.wc-taxonomy-term-search' )
+ .filter( ':not(.enhanced)' )
+ .each( function () {
+ var return_format = $( this ).data( 'return_id' )
+ ? 'id'
+ : 'slug';
+ var isVisualAttribute =
+ 'yes' === $( this ).data( 'isVisualAttribute' );
+
+ var select2_args = $.extend(
+ {
+ allowClear: $( this ).data( 'allow_clear' )
+ ? true
+ : false,
+ placeholder: $( this ).data( 'placeholder' ),
+ minimumInputLength:
+ $( this ).data( 'minimum_input_length' ) !==
+ null &&
+ $( this ).data( 'minimum_input_length' ) !==
+ undefined
+ ? $( this ).data(
+ 'minimum_input_length'
+ )
+ : '3',
+ escapeMarkup: function ( m ) {
+ return m;
+ },
+ ajax: {
+ url: wc_enhanced_select_params.ajax_url,
+ dataType: 'json',
+ delay: 250,
+ data: function ( params ) {
+ return {
+ taxonomy:
+ $( this ).data( 'taxonomy' ),
+ limit: $( this ).data( 'limit' ),
+ orderby:
+ $( this ).data( 'orderby' ),
+ term: params.term,
+ action: 'woocommerce_json_search_taxonomy_terms',
+ security:
+ wc_enhanced_select_params.search_taxonomy_terms_nonce,
+ };
+ },
+ processResults: function ( data ) {
+ var terms = [];
+ if ( data ) {
+ $.each(
+ data,
+ function ( id, term ) {
+ var termData = {
+ id:
+ 'id' ===
+ return_format
+ ? term.term_id
+ : term.slug,
+ text: term.name,
+ };
+
+ if ( isVisualAttribute ) {
+ termData.visual =
+ term.visual || {
+ type: 'none',
+ value: '',
+ };
+ }
+
+ terms.push( termData );
+ }
+ );
+ }
+ return {
+ results: terms,
+ };
+ },
+ cache: true,
+ },
},
- processResults: function( data ) {
- var terms = [];
- if ( data ) {
- $.each( data, function( id, term ) {
- terms.push({
- id: 'id' === return_format ? term.term_id : term.slug,
- text: term.formatted_name
- });
- });
+ getEnhancedSelectFormatString()
+ );
+
+ if ( isVisualAttribute ) {
+ select2_args.templateResult = function ( term ) {
+ if ( term.loading ) {
+ return term.text;
}
- return {
- results: terms
- };
- },
- cache: true
+
+ return formatVisualTermOption( term );
+ };
}
- }, getEnhancedSelectFormatString() );
- $( this ).selectWoo( select2_args ).addClass( 'enhanced' );
- });
+ var $select = $( this );
- // Ajax category search boxes
- $( ':input.wc-taxonomy-term-search' ).filter( ':not(.enhanced)' ).each( function() {
- var return_format = $( this ).data( 'return_id' ) ? 'id' : 'slug';
-
- var select2_args = $.extend( {
- allowClear : $( this ).data( 'allow_clear' ) ? true : false,
- placeholder : $( this ).data( 'placeholder' ),
- minimumInputLength: $( this ).data( 'minimum_input_length' ) !== null && $( this ).data( 'minimum_input_length' ) !== undefined ? $( this ).data( 'minimum_input_length' ) : '3',
- escapeMarkup : function( m ) {
- return m;
- },
- ajax: {
- url: wc_enhanced_select_params.ajax_url,
- dataType: 'json',
- delay: 250,
- data: function( params ) {
- return {
- taxonomy: $( this ).data( 'taxonomy' ),
- limit: $( this ).data( 'limit' ),
- orderby: $( this ).data( 'orderby'),
- term: params.term,
- action: 'woocommerce_json_search_taxonomy_terms',
- security: wc_enhanced_select_params.search_taxonomy_terms_nonce
- };
- },
- processResults: function( data ) {
- var terms = [];
- if ( data ) {
- $.each( data, function( id, term ) {
- terms.push({
- id: 'id' === return_format ? term.term_id : term.slug,
- text: term.name
- });
- });
+ $select
+ .selectWoo( select2_args )
+ .addClass( 'enhanced' );
+
+ if ( isVisualAttribute ) {
+ updateVisualAttributeTermChoices( $select );
+
+ $select.on(
+ 'change.wcVisualAttributeTerms select2:select.wcVisualAttributeTerms',
+ function () {
+ updateVisualAttributeTermChoices( $select );
}
- return {
- results: terms
- };
- },
- cache: true
+ );
}
- }, getEnhancedSelectFormatString() );
-
- $( this ).selectWoo( select2_args ).addClass( 'enhanced' );
- });
-
- $( ':input.wc-attribute-search' ).filter( ':not(.enhanced)' ).each( function() {
- var select2Element = this;
- var select2_args = $.extend( {
- allowClear : $( this ).data( 'allow_clear' ) ? true : false,
- placeholder : $( this ).data( 'placeholder' ),
- minimumInputLength: $( this ).data( 'minimum_input_length' ) !== null && $( this ).data( 'minimum_input_length' ) !== undefined ? $( this ).data( 'minimum_input_length' ) : '3',
- escapeMarkup : function( m ) {
- return m;
- },
- ajax: {
- url: wc_enhanced_select_params.ajax_url,
- dataType: 'json',
- delay: 250,
- data: function( params ) {
- return {
- term: params.term,
- action: 'woocommerce_json_search_product_attributes',
- security: wc_enhanced_select_params.search_product_attributes_nonce
- };
+ } );
+
+ $( ':input.wc-attribute-search' )
+ .filter( ':not(.enhanced)' )
+ .each( function () {
+ var select2Element = this;
+ var select2_args = $.extend(
+ {
+ allowClear: $( this ).data( 'allow_clear' )
+ ? true
+ : false,
+ placeholder: $( this ).data( 'placeholder' ),
+ minimumInputLength:
+ $( this ).data( 'minimum_input_length' ) !==
+ null &&
+ $( this ).data( 'minimum_input_length' ) !==
+ undefined
+ ? $( this ).data(
+ 'minimum_input_length'
+ )
+ : '3',
+ escapeMarkup: function ( m ) {
+ return m;
+ },
+ ajax: {
+ url: wc_enhanced_select_params.ajax_url,
+ dataType: 'json',
+ delay: 250,
+ data: function ( params ) {
+ return {
+ term: params.term,
+ action: 'woocommerce_json_search_product_attributes',
+ security:
+ wc_enhanced_select_params.search_product_attributes_nonce,
+ };
+ },
+ processResults: function ( data ) {
+ var disabledItems =
+ $( select2Element ).data(
+ 'disabled-items'
+ ) || [];
+ var terms = [];
+ if ( data ) {
+ $.each(
+ data,
+ function ( id, term ) {
+ terms.push( {
+ id: term.slug,
+ text: term.name,
+ disabled:
+ disabledItems.includes(
+ term.slug
+ ),
+ } );
+ }
+ );
+ }
+ return {
+ results: terms,
+ };
+ },
+ cache: true,
+ },
},
- processResults: function( data ) {
- var disabledItems = $( select2Element ).data('disabled-items') || [];
- var terms = [];
- if ( data ) {
- $.each( data, function( id, term ) {
- terms.push({
- id: term.slug,
- text: term.name,
- disabled: disabledItems.includes( term.slug )
- });
- });
- }
- return {
- results: terms
- };
- },
- cache: true
- }
- }, getEnhancedSelectFormatString() );
+ getEnhancedSelectFormatString()
+ );
- $( this ).selectWoo( select2_args ).addClass( 'enhanced' );
- });
- })
+ $( this )
+ .selectWoo( select2_args )
+ .addClass( 'enhanced' );
+ } );
+ } )
// WooCommerce Backbone Modal
- .on( 'wc_backbone_modal_before_remove', function() {
- $( '.wc-enhanced-select, :input.wc-product-search, :input.wc-customer-search' ).filter( '.select2-hidden-accessible' )
+ .on( 'wc_backbone_modal_before_remove', function () {
+ $(
+ '.wc-enhanced-select, :input.wc-product-search, :input.wc-customer-search'
+ )
+ .filter( '.select2-hidden-accessible' )
.selectWoo( 'close' );
- })
+ } )
.trigger( 'wc-enhanced-select-init' );
- $( 'html' ).on( 'click', function( event ) {
+ $( 'html' ).on( 'click', function ( event ) {
if ( this === event.target ) {
- $( '.wc-enhanced-select, :input.wc-product-search, :input.wc-customer-search' ).filter( '.select2-hidden-accessible' )
+ $(
+ '.wc-enhanced-select, :input.wc-product-search, :input.wc-customer-search'
+ )
+ .filter( '.select2-hidden-accessible' )
.selectWoo( 'close' );
}
} );
- } catch( err ) {
+ } catch ( err ) {
// If select2 failed (conflict?) log the error but don't stop other scripts breaking.
window.console.log( err );
}
-});
+} );
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php
index 163d8921209..b076419367f 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php
@@ -8,6 +8,8 @@
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
+
+use Automattic\WooCommerce\Internal\ProductAttributes\VisualAttributeTermMeta;
?>
<table cellpadding="0" cellspacing="0">
@@ -39,7 +41,8 @@ if ( ! defined( 'ABSPATH' ) ) {
'select' === $attribute_taxonomy->attribute_type ||
'wc-visual' === $attribute_taxonomy->attribute_type
) {
- $attribute_orderby = ! empty( $attribute_taxonomy->attribute_orderby ) ? $attribute_taxonomy->attribute_orderby : 'name';
+ $is_visual_attribute = 'wc-visual' === $attribute_taxonomy->attribute_type;
+ $attribute_orderby = ! empty( $attribute_taxonomy->attribute_orderby ) ? $attribute_taxonomy->attribute_orderby : 'name';
/**
* Filter the length (number of terms) rendered in the list.
*
@@ -55,11 +58,39 @@ if ( ! defined( 'ABSPATH' ) ) {
data-orderby="<?php echo esc_attr( $attribute_orderby ); ?>"
class="multiselect attribute_values wc-taxonomy-term-search"
name="attribute_values[<?php echo esc_attr( $i ); ?>][]"
- data-taxonomy="<?php echo esc_attr( $attribute->get_taxonomy() ); ?>">
+ data-taxonomy="<?php echo esc_attr( $attribute->get_taxonomy() ); ?>"
+ data-is-visual-attribute="<?php echo esc_attr( wc_bool_to_string( $is_visual_attribute ) ); ?>">
<?php
$selected_terms = $attribute->get_terms();
+ $term_visuals = array();
+
+ if ( $selected_terms && $is_visual_attribute ) {
+ $term_visuals = VisualAttributeTermMeta::get_term_visuals( wp_list_pluck( $selected_terms, 'term_id' ) );
+ }
+
if ( $selected_terms ) {
foreach ( $selected_terms as $selected_term ) {
+ $option_attributes = array(
+ 'value' => $selected_term->term_id,
+ 'selected' => 'selected',
+ );
+
+ if ( $is_visual_attribute ) {
+ $option_attributes['data-visual'] = wp_json_encode(
+ $term_visuals[ $selected_term->term_id ] ?? VisualAttributeTermMeta::get_empty_visual()
+ );
+ }
+
+ $option_attribute_string = '';
+
+ foreach ( $option_attributes as $attribute_name => $attribute_value ) {
+ $option_attribute_string .= sprintf(
+ ' %1$s="%2$s"',
+ esc_attr( $attribute_name ),
+ esc_attr( $attribute_value )
+ );
+ }
+
/**
* Filter the selected attribute term name.
*
@@ -67,7 +98,7 @@ if ( ! defined( 'ABSPATH' ) ) {
* @param string $name Name of selected term.
* @param array $term The selected term object.
*/
- echo '<option value="' . esc_attr( $selected_term->term_id ) . '" selected="selected">' . esc_html( apply_filters( 'woocommerce_product_attribute_term_name', $selected_term->name, $selected_term ) ) . '</option>';
+ echo '<option' . $option_attribute_string . '>' . esc_html( apply_filters( 'woocommerce_product_attribute_term_name', $selected_term->name, $selected_term ) ) . '</option>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
?>
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute.php
index 8fb230e3ce2..618d5b31042 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute.php
@@ -2,6 +2,9 @@
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
+if ( ! isset( $attribute ) ) {
+ return;
+}
?>
<div data-taxonomy="<?php echo esc_attr( $attribute->get_taxonomy() ); ?>" class="woocommerce_attribute wc-metabox postbox closed <?php echo esc_attr( implode( ' ', $metabox_class ) ); ?>" rel="<?php echo esc_attr( $attribute->get_position() ); ?>">
<h3>
@@ -9,6 +12,13 @@ if ( ! defined( 'ABSPATH' ) ) {
<div class="tips sort" data-tip="<?php esc_attr_e( 'Drag and drop to set admin attribute order', 'woocommerce' ); ?>"></div>
<a href="#" class="remove_row delete"><?php esc_html_e( 'Remove', 'woocommerce' ); ?></a>
<strong class="attribute_name<?php echo esc_attr( $attribute->get_name() === '' ? ' placeholder' : '' ); ?>"><?php echo esc_html( $attribute->get_name() !== '' ? wc_attribute_label( $attribute->get_name() ) : __( 'New attribute', 'woocommerce' ) ); ?></strong>
+ <?php if ( $attribute->is_taxonomy() ) : ?>
+ <?php
+ /* translators: 'Global' refers to 'global attribute'. */
+ $global_attribute_badge_label = __( 'Global', 'woocommerce' );
+ ?>
+ <span class="woocommerce-attribute-global-badge"><?php echo esc_html( $global_attribute_badge_label ); ?></span>
+ <?php endif; ?>
</h3>
<div class="woocommerce_attribute_data wc-metabox-content hidden">
<?php require __DIR__ . '/html-product-attribute-inner.php'; ?>
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-attributes.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-attributes.php
index 3a6ccd62c7f..5716ddbf412 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-attributes.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-attributes.php
@@ -45,8 +45,12 @@ $product_attributes = $product_object->get_attributes( 'edit' );
<a href="#" class="expand_all"><?php esc_html_e( 'Expand', 'woocommerce' ); ?></a> / <a href="#" class="close_all"><?php esc_html_e( 'Close', 'woocommerce' ); ?></a>
</span>
<div class="actions">
+ <?php
+ /* translators: 'Global' refers to 'global attribute'. */
+ $add_global_attribute_placeholder = __( 'Add global', 'woocommerce' );
+ ?>
<button type="button" class="button add_custom_attribute"><?php esc_html_e( 'Add new', 'woocommerce' ); ?></button>
- <select class="wc-attribute-search" data-placeholder="<?php esc_attr_e( 'Add existing', 'woocommerce' ); ?>" data-minimum-input-length="0">
+ <select class="wc-attribute-search" data-placeholder="<?php echo esc_attr( $add_global_attribute_placeholder ); ?>" data-minimum-input-length="0">
</select>
</div>
</div>
diff --git a/plugins/woocommerce/includes/class-wc-ajax.php b/plugins/woocommerce/includes/class-wc-ajax.php
index 5ca71e3c2d2..4dea2ad4f33 100644
--- a/plugins/woocommerce/includes/class-wc-ajax.php
+++ b/plugins/woocommerce/includes/class-wc-ajax.php
@@ -771,13 +771,26 @@ class WC_AJAX {
VisualAttributeTermMeta::save_term_visual_from_request( (int) $result['term_id'], $taxonomy, $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
$term = get_term_by( 'id', $result['term_id'], $taxonomy );
- wp_send_json(
- array(
- 'term_id' => $term->term_id,
- 'name' => $term->name,
- 'slug' => $term->slug,
- )
+
+ if ( ! $term ) {
+ wp_send_json(
+ array(
+ 'error' => __( 'Term not found', 'woocommerce' ),
+ )
+ );
+ }
+
+ $response = array(
+ 'term_id' => $term->term_id,
+ 'name' => $term->name,
+ 'slug' => $term->slug,
);
+
+ if ( VisualAttributeTermMeta::is_visual_attribute_taxonomy( $taxonomy ) ) {
+ $response['visual'] = VisualAttributeTermMeta::get_term_visual( (int) $term->term_id );
+ }
+
+ wp_send_json( $response );
}//end if
}//end if
}//end if
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 52d25f58cee..ffbbe1e8be3 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -6132,12 +6132,6 @@ parameters:
count: 9
path: includes/admin/meta-boxes/views/html-product-attribute-inner.php
- -
- message: '#^Variable \$attribute might not be defined\.$#'
- identifier: variable.undefined
- count: 5
- path: includes/admin/meta-boxes/views/html-product-attribute.php
-
-
message: '#^Variable \$metabox_class might not be defined\.$#'
identifier: variable.undefined
@@ -8526,36 +8520,18 @@ parameters:
count: 4
path: includes/class-wc-ajax.php
- -
- message: '#^Cannot access property \$name on WP_Term\|false\.$#'
- identifier: property.nonObject
- count: 1
- path: includes/class-wc-ajax.php
-
-
message: '#^Cannot access property \$parent on WP_Error\|WP_Term\|null\.$#'
identifier: property.nonObject
count: 1
path: includes/class-wc-ajax.php
- -
- message: '#^Cannot access property \$slug on WP_Term\|false\.$#'
- identifier: property.nonObject
- count: 1
- path: includes/class-wc-ajax.php
-
-
message: '#^Cannot access property \$term_id on WP_Error\|WP_Term\|null\.$#'
identifier: property.nonObject
count: 1
path: includes/class-wc-ajax.php
- -
- message: '#^Cannot access property \$term_id on WP_Term\|false\.$#'
- identifier: property.nonObject
- count: 1
- path: includes/class-wc-ajax.php
-
-
message: '#^Cannot call method add_order_note\(\) on WC_Order\|WC_Order_Refund\|false\.$#'
identifier: method.nonObject
@@ -54825,7 +54801,6 @@ parameters:
count: 1
path: src/Blocks/QueryFilters.php
-
-
message: '#^Method Automattic\\WooCommerce\\Blocks\\Registry\\Container\:\:register\(\) has no return type specified\.$#'
identifier: missingType.return
diff --git a/plugins/woocommerce/src/Internal/ProductAttributes/VisualAttributeTermAdmin.php b/plugins/woocommerce/src/Internal/ProductAttributes/VisualAttributeTermAdmin.php
index ff60a0f5499..7d84015dc46 100644
--- a/plugins/woocommerce/src/Internal/ProductAttributes/VisualAttributeTermAdmin.php
+++ b/plugins/woocommerce/src/Internal/ProductAttributes/VisualAttributeTermAdmin.php
@@ -55,6 +55,44 @@ class VisualAttributeTermAdmin implements RegisterHooksInterface {
}
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_visual_attribute_script' ) );
+ add_filter( 'woocommerce_json_search_found_product_attribute_terms', array( $this, 'add_visual_data_to_attribute_terms' ), 10, 2 );
+ }
+
+ /**
+ * Add visual swatch data to attribute term search results.
+ *
+ * @internal
+ *
+ * @param array|\WP_Error $terms The list of matched terms.
+ * @param string $taxonomy The terms taxonomy.
+ * @return array|\WP_Error
+ */
+ public function add_visual_data_to_attribute_terms( $terms, string $taxonomy ) {
+ if ( is_wp_error( $terms ) || empty( $terms ) || ! VisualAttributeTermMeta::is_visual_attribute_taxonomy( $taxonomy ) ) {
+ return $terms;
+ }
+
+ // Because `$terms` might be filtered by a plugin, make sure we only operate on valid terms.
+ $valid_terms = array_filter(
+ $terms,
+ static function ( $term ) {
+ return is_object( $term ) && isset( $term->term_id );
+ }
+ );
+
+ $term_ids = array_map( 'intval', wp_list_pluck( $valid_terms, 'term_id' ) );
+ $visuals = VisualAttributeTermMeta::get_term_visuals( $term_ids );
+
+ foreach ( $valid_terms as $term ) {
+ /**
+ * Term object with dynamic visual property.
+ *
+ * @var \stdClass $term
+ */
+ $term->visual = $visuals[ $term->term_id ] ?? VisualAttributeTermMeta::get_empty_visual();
+ }
+
+ return $terms;
}
/**