�PNG  IHDR22?�� pHYs  �� OiCCPPhotoshop ICC profilexڝSgTS�=���BK���KoR RB���&*! J�!��Q�EEȠ�����Q, � ��!���������{�kּ������>���� �H3Q5� �B�������.@� $p�d!s�#�~<<+"��x� �M��0���B�\���t�8K�@z�B�@F���&S�`�cb�P-`'������{[�!�� e�Dh;��V�EX0fK�9�-0IWfH���� � 0Q��){`�##x��F�W<�+��*x��<�$9E�[-qWW.(�I+6aa�@.�y�2�4���������x����6��_-��"bb���ϫp@�t~��,/��;�m��%�h^ �u��f�@����W�p�~<�5�j>{�-�]c�K'Xt����o��(�h���w��?�G�%�fI�q^D$.Tʳ?�D��*�A��,���� �`6�B$��BB d�r`)��B(�Ͱ*`/�@4�Qh��p.�U�=p�a��(�� A�a!ڈb�X#����!�H�$ ɈQ"K�5H1R�T UH�=r9�\F��;�2����G1���Q=� �C��7�F� �dt1�����r�=�6��Ыhڏ>C�0��3�l0.��B�8, �c˱"� ���V����cϱw�E� 6wB aAHXLXN�H� $4� 7 �Q�'"��K�&���b21�XH,#��/{�C�7$�C2'��I��T��F�nR#�,��4H#���dk�9�, +ȅ����3��!�[ �b@q��S�(R�jJ��4�e�2AU��Rݨ�T5�ZB���R�Q��4u�9̓IK�����hh�i��t�ݕN��W���G���w ��Ljg(�gw��L�Ӌ�T071���oUX*�*|�� �J�&�*/T����ު U�U�T��^S}�FU3S� Ԗ�U��P�SSg�;���g�oT?�~Y��Y�L�OC�Q��_�� c�x,!k ��u�5�&���|v*�����=���9C3J3W�R�f?�q��tN �(���~���)�)�4L�1e\k����X�H�Q�G�6������E�Y��A�J'\'Gg����S�Sݧ �M=:��.�k���Dw�n��^��Lo��y���}/�T�m���G X� $� �<�5qo</���QC]�@C�a�a�ᄑ��<��F�F�i�\�$�m�mƣ&&!&KM�M�RM��)�;L;L���͢�֙5�=1�2��כ߷`ZxZ,����eI��Z�Yn�Z9Y�XUZ]�F���%ֻ�����N�N���gð�ɶ�����ۮ�m�}agbg�Ů��}�}��= ���Z~s�r:V:ޚΜ�?}����/gX���3��)�i�S��Ggg�s�󈋉K��.�>.���Ƚ�Jt�q]�z���������ۯ�6�i�ܟ�4�)�Y3s���C�Q��? ��0k߬~OCO�g��#/c/�W�װ��w��a�>�>r��>�<7�2�Y_�7��ȷ�O�o�_��C#�d�z����%g��A�[��z|!��?:�e����A���AA�������!h�쐭!��Α�i�P~���a�a��~ '���W�?�p�X�1�5w��Cs�D�D�Dޛg1O9�-J5*>�.j<�7�4�?�.fY��X�XIlK9.*�6nl������� �{�/�]py�����.,:�@L�N8��A*��%�w%� y��g"/�6ш�C\*N�H*Mz�쑼5y$�3�,幄'���L Lݛ:��v m2=:�1����qB�!M��g�g�fvˬe����n��/��k���Y- �B��TZ(�*�geWf�͉�9���+��̳�ې7�����ᒶ��KW-X潬j9�������(�x��oʿ�ܔ���Ĺd�f�f���-�[����n �ڴ �V����E�/��(ۻ��C���<��e����;?T�T�T�T6��ݵa��n��{��4���[���>ɾ�UUM�f�e�I���?�������m]�Nmq����#�׹���=TR��+�G�����w- 6 U����#pDy��� �� :�v�{���vg/jB��F�S��[b[�O�>����z�G��499�?r����C�d�&����ˮ/~�����јѡ�򗓿m|������������x31^�V���w�w��O�| (�h���SЧ��������c3-� cHRMz%������u0�`:�o�_�F5IDATx��ytUս�?�;���fN$$��@B�!�k��� -*�gE���j�O�kQ�ʠ`U�B_ �AÐ0%�Ȕ�@r3ޛ;߳��$�@��v����^�s����������$��ߢ !�W��5�̱'-٘������4�>�Rƫ$�G��0U�H}�_ _� #��k�MJ/=�9Du *ڲ�k�PQ;0��j|�*�$�^컂脛y���>�z×� B��� ��(�:$�ʔ_�6��C!B����MVx��a��ζyY����j��ƋY2�9�F�{r��r�딟F��Q��hY6��� x��[l��RC��������nt@��;P��3��� F�w��7��:�P���ɋ'���mj���Bƪ�$�Q�L��y��߷g��\���z����v5�p�v$�v櫻�`6~K �Y�X����,�:��"�o�e0II"(��,������l_�C���/4�+f�E�!q��:����u����c��樼.UE�@� �]K�W���$-�h]�JG���Dz)�`��.�A/ ���mᏣc�3jK� �!�>���$�Q�!�_�? \�C2(E �M$Z I{�U���s�ǣ���=��@Բe ޻��W�����@ �w����}�9v,)��.}*|"�s��di���,���G�]W�AC�������|<����[��ߕ������K�*�G nߴ!��‹��J�!��6(L�a)��Am��������;adf��:�xm��nh�;֦I��(���o�C~ډa?�=��s����h��E������xp����)���_���t��W�����´;8���`dz=(��@@�+,�;�`�fhl��XȌ�3,uI�~z(J����� �ZX�A����H'�À�㜦�82|��a�n�z2��I���� ���EY�h�ε<�0�q�{��C��v�˅��?"@��χ�x�WS+��N�~�KQ�J�8ل�� _.W��� !TU{�����,��"�� �;�������HI S� ��))�o�1�H�D� ���� (� ;����"Ҕ��,���� �F=���O��&�Ph>�`���y��߲ &S��P�<��'��1s�1cĂ�N����2)����@�u��tv��<�+�-&�����h��[!FSG���' � z z0��<����8d&�����E0�,��<��"!P�b4��� �{S�����X����.�囤���T��hEF��X�q0"-<����h���]�Py*����F⩍��@v�˂�Y08�� .q��a��s����(!<̫1�3�L�H��7sYW�`��qtu�\7��?�@�Y�� %Ka�x�GZ�*W�P��y�����N0r9��Z�c�%���q,��k�����!!,��>���IB��� �x1�Xu�Y���v-���ֺ��N������JP��a�2`�e5��w��u;�6�i���̃��(K���ťa��(l51��JL��O'%���]���ٖ뿉!��{*�p+f ���i8��D�ȑ���ܩ�M��V��ӟz��x�7Z�1i}�^͜���6e����x��)u�d�ˏ�V�eG�\�X1�����=n�U'�~Y����L�\,�(d!x ,�:�TW8� M`r���@��r+v�Q0��>^;˴�VbF��('����� �*O��i��j�Ӆ����^�S3&� ���{������.ی�� N;��NU%d0�Ce\r"�C�b��&t7N�7��� �ȪD�[%l�۪�$��'m?�A9tI�9>��NVL ��� 6v~M�����ъ�B���0����9$&=ώm��W�0���L���&��E���f�f��'+�9��hv��7,���M��6�D�&�^� �����8 D֮�����o��k^z��O��hP�8q�5d*��YVq3�{��������#LX4�?��jP=3�~�؅c�ݰ�̱�ұ��8̲��2�G�����j��έ�9��w7���U�X�w�85C��?!I&���o&����L���I#�^�eV�,�^t5�k� �xs˛\��U�8��."���{���ǵ�l4��ݼ�;��r"���G�/~���gJV�N糦��ZZ^k+NN�D��I$�=OE�z����|*�*{��%k���G��>�`9��b;�s���8,�qs��c��.b��q�%�>r��n�R�Ƨ�%�,�A��le#�8���}; 21�0�b�&�QX�f���q�00����$>�h�N�'�a3��A���/�r ��L5�8�GԲ'�����w;��c#�q1���h$*��V^�k��QĽ�b:i������� ����H F#�_���,��]g����Ը����`RUU���� ��"�qD���>-��ϤI�~��3�̴�����NZ[[555�eeek7�m����YF#[��Đ��R��i��L�H�r ��@��J-�Pŝ��_|�-��6y��ks�V�U~��6���w.����(��0A� 4��f��� /���C�xŊ�ٳ����z=6�����rg���mKo��o�/����a�g���?'9%9��P��斓��~���L{�� �H��{AW��B��$����v�1b�&Mb�ر��ر#�0`�`xMBp! )Z�Nmo�mZ�g�}�eذaWTVV ���'99��C5��&�&+5�bT>|8��PUUETT����&���t��~TE%��'������b�tEGG;RSS�S�N�v��ѯ��t����u1@�ޚ"f��͈���r'L�Ph��m��ƌ�74o��`�x�q�P���NRSS�X,�����l&&&��MFFA!-��`��ҷ�n��Yuu�����x<I�LB%����KԒ50�!�3�bz�����ʸKL���;---�l6[��`��������Ȏ;�����egg4��!D�T��jWujyZ��������K����%����p�^���X,6��sV=�ܖ����6�3�X ���4ҿ��A2�&����_k�;d�� �VfOIEND�B`�
Warning: session_start(): Session cannot be started after headers have already been sent in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 124

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 130

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 131

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 132

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 133

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 134

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 135

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 547

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 548

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 549

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 550

Warning: Cannot modify header information - headers already sent by (output started at /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php:1) in /home/mciraet/www/wp-includes/php-ai-client/src/Providers/Enums/Documentsoliders.php on line 551
/* global jQuery, wp, wcVariationGalleryL10n */ /* * Variation gallery (classic admin). * * Embedded inside the variation meta box on the classic Edit Product screen. */ jQuery( function ( $ ) { 'use strict'; const SELECTORS = { productOptionsRoot: '#variable_product_options', productData: '#woocommerce-product-data', field: '.wc-variation-gallery-field', fieldThumbList: '.wc-variation-gallery-field__thumbs', fieldHero: '.wc-variation-gallery-field__hero', fieldHeroImg: '.wc-variation-gallery-field__hero-img', fieldHeroBroken: '.wc-variation-gallery-field__hero-broken', fieldHeroEmptyCta: '.wc-variation-gallery-field__empty-cta', fieldHint: '.wc-variation-gallery-field__hint', fieldCount: '.wc-variation-gallery-field__count', fieldImageIdsInput: '.wc-variation-gallery-image-ids', thumb: '.wc-variation-gallery-thumb', thumbButton: '.wc-variation-gallery-thumb__button', thumbRemove: '.wc-variation-gallery-thumb__remove', manageTrigger: '.wc-variation-gallery-manage', replaceTrigger: '.wc-variation-gallery-replace', primaryBadge: '[data-primary-badge]', missingFileLabel: '.screen-reader-text[data-missing-file-label]', // Legacy variation-row fields that we keep in sync. variationRow: '.woocommerce_variation', legacyInput: '.upload_image_id', legacyButton: '.upload_image_button', }; const CLASSES = { isEmpty: 'is-empty', isActive: 'is-active', isBroken: 'is-broken', fieldHeroImg: 'wc-variation-gallery-field__hero-img', fieldHeroBroken: 'wc-variation-gallery-field__hero-broken', }; const l10n = window.wcVariationGalleryL10n || {}; const a11y = ( wp && wp.a11y ) || null; /** * Speak a message via wp.a11y.speak when available. No-op otherwise. * * @param {string} message */ const announce = ( message ) => { if ( message && a11y && typeof a11y.speak === 'function' ) { a11y.speak( message ); } }; /** * Pick the URL of the largest reasonable preview for the hero slot. * Prefers `medium`, then `full`, then the raw attachment URL. * * @param {{ sizes?: Object, url?: string }} attachmentJson * @return {string} */ const pickAttachmentDisplayUrl = ( attachmentJson ) => { const sizes = attachmentJson.sizes || {}; const preferred = sizes.medium || sizes.full; return ( preferred && preferred.url ) || attachmentJson.url || ''; }; /** * Pick the thumbnail URL for use in small previews (gallery thumbs and * the legacy inline preview slot). Falls back to the raw URL when no * thumbnail variant is registered for this attachment. * * @param {{ sizes?: Object, url?: string }} attachmentJson * @return {string} */ const pickAttachmentThumbnailUrl = ( attachmentJson ) => { const sizes = attachmentJson.sizes || {}; return ( ( sizes.thumbnail && sizes.thumbnail.url ) || attachmentJson.url || '' ); }; /** * Map an image count to the i18n template key used to label the field. * * @param {number} count * @return {string} */ const getCountTemplateKey = ( count ) => { if ( count === 0 ) { return 'countZero'; } if ( count === 1 ) { return 'countSingular'; } return 'countPlural'; }; /** * Remove all hero-state overlays. * * @param {jQuery} $hero */ const clearHeroOverlays = ( $hero ) => { $hero.find( SELECTORS.fieldHeroEmptyCta ).remove(); $hero.find( SELECTORS.fieldHeroBroken ).remove(); $hero.find( SELECTORS.missingFileLabel ).remove(); }; /** * Return the existing hero if one is present, or create and * prepend a fresh one. * * @param {jQuery} $hero * @return {jQuery} */ const getOrCreateHeroImage = ( $hero ) => { const $existing = $hero.find( SELECTORS.fieldHeroImg ); if ( $existing.length ) { return $existing; } const $img = $( '' ) .addClass( CLASSES.fieldHeroImg ) .attr( 'loading', 'lazy' ) .attr( 'decoding', 'async' ); $hero.prepend( $img ); return $img; }; const variationGallery = { /** @type {wp.media.frames.MediaFrame|null} */ manageFrame: null, /** @type {wp.media.frames.MediaFrame|null} */ replaceFrame: null, /** @type {jQuery|null} */ activeField: null, /** @type {number[]} */ activePreloadIds: [], /** @type {number|null} */ activeIndexForReplace: null, wpMediaPostId: wp.media.model.settings.post.id, init() { const $root = $( SELECTORS.productOptionsRoot ); $root.on( 'click', SELECTORS.manageTrigger, this.onManage.bind( this ) ); $root.on( 'click', SELECTORS.replaceTrigger, this.onReplace.bind( this ) ); $root.on( 'click', SELECTORS.thumbButton, this.onThumbClick.bind( this ) ); $root.on( 'click', SELECTORS.thumbRemove, this.onRemoveClick.bind( this ) ); // The meta box re-fires these events when variation rows are paginated // in or appended after a save, so re-initialize sortables each time. $root.on( 'woocommerce_variations_added', this.initializeSortables.bind( this ) ); $( SELECTORS.productData ).on( 'woocommerce_variations_loaded', this.initializeSortables.bind( this ) ); this.initializeSortables(); }, initializeSortables() { $( SELECTORS.fieldThumbList ).each( function () { const $list = $( this ); const $field = $list.closest( SELECTORS.field ); variationGallery.updateFromDom( $field ); if ( $list.data( 'wc-variation-gallery-sortable' ) ) { return; } $list.sortable( { items: 'li' + SELECTORS.thumb, cancel: SELECTORS.thumbRemove, cursor: 'grabbing', scrollSensitivity: 40, forcePlaceholderSize: true, helper: 'clone', opacity: 0.65, placeholder: 'wc-metabox-sortable-placeholder', start( _event, ui ) { ui.item.addClass( 'is-dragging' ); $list.addClass( 'is-sorting' ); }, stop( _event, ui ) { ui.item.removeClass( 'is-dragging' ); $list.removeClass( 'is-sorting' ); }, update() { const wasPrimary = variationGallery.getActiveAttachmentId( $field ); variationGallery.setActiveIndex( $field, 0 ); variationGallery.syncField( $field ); const isPrimary = variationGallery.getActiveAttachmentId( $field ); announce( wasPrimary !== isPrimary ? l10n.announcePrimary : l10n.announceReorder ); }, } ); $list.data( 'wc-variation-gallery-sortable', true ); } ); }, /** * Click on a thumbnail: surface that image in the hero slot. * * Does not change the gallery order or the primary image. * * @param {jQuery.Event} event */ onThumbClick( event ) { const $button = $( event.currentTarget ); const $thumb = $button.closest( SELECTORS.thumb ); const $field = $thumb.closest( SELECTORS.field ); const index = $thumb.index(); event.preventDefault(); this.setActiveIndex( $field, index ); }, /** * Click on a thumbnail's remove button: drop that image from the * gallery. * * @param {jQuery.Event} event */ onRemoveClick( event ) { event.preventDefault(); event.stopPropagation(); const $trigger = $( event.currentTarget ); const $thumb = $trigger.closest( SELECTORS.thumb ); const $field = $thumb.closest( SELECTORS.field ); const removedIndex = $thumb.index(); const currentActive = this.getActiveIndex( $field ); const ids = this.getFieldIds( $field ); ids.splice( removedIndex, 1 ); let nextActive; if ( removedIndex < currentActive ) { nextActive = currentActive - 1; } else if ( removedIndex === currentActive ) { nextActive = Math.min( removedIndex, ids.length - 1 ); } else { nextActive = currentActive; } this.writeGallery( $field, ids, Math.max( 0, nextActive ) ); announce( l10n.announceRemoved ); }, /** * "Manage" button: open the WP media frame in multi-select mode, * preselect the variation's current gallery, and rewrite the * gallery from whatever the merchant selects. * * @param {jQuery.Event} event */ onManage( event ) { const $trigger = $( event.currentTarget ); const $field = $trigger.closest( SELECTORS.field ); const variationId = $field.data( 'variationId' ); event.preventDefault(); // Scope newly-uploaded attachments to this variation post. wp.media.model.settings.post.id = variationId; // Update per-open state read by the cached frame's handlers. this.activeField = $field; this.activePreloadIds = this.getFieldIds( $field ); if ( ! this.manageFrame ) { this.manageFrame = wp.media( { title: l10n.manageTitle, library: { type: 'image' }, button: { text: l10n.manageButton }, multiple: 'add', } ); this.manageFrame.on( 'open', () => this.preloadFrameSelection( this.manageFrame, this.activePreloadIds ) ); this.manageFrame.on( 'select', () => this.onManageSelect( this.manageFrame, this.activeField ) ); this.manageFrame.on( 'close', () => this.restoreMediaPostId() ); } this.manageFrame.open(); }, /** * "Replace" button on the hero slot: open the WP media frame in * single-select mode and swap the currently-active gallery slot * with the chosen attachment. * * @param {jQuery.Event} event */ onReplace( event ) { const $trigger = $( event.currentTarget ); const $field = $trigger.closest( SELECTORS.field ); const variationId = $field.data( 'variationId' ); event.preventDefault(); wp.media.model.settings.post.id = variationId; this.activeField = $field; this.activeIndexForReplace = this.getActiveIndex( $field ); if ( ! this.replaceFrame ) { this.replaceFrame = wp.media( { title: l10n.replaceTitle, library: { type: 'image' }, button: { text: l10n.replaceButton }, multiple: false, } ); this.replaceFrame.on( 'select', () => this.onReplaceSelect( this.replaceFrame, this.activeField ) ); this.replaceFrame.on( 'close', () => this.restoreMediaPostId() ); } this.replaceFrame.open(); }, /** * When the manage frame opens, populate its selection with the * variation's current gallery. * * @param {wp.media.frames.MediaFrame} frame * @param {number[]} currentIds */ preloadFrameSelection( frame, currentIds ) { if ( ! frame ) { return; } const selection = frame.state().get( 'selection' ); if ( ! selection ) { return; } selection.reset(); currentIds.forEach( ( id ) => { const attachment = wp.media.attachment( id ); attachment.fetch(); selection.add( attachment ); } ); }, /** * Manage frame "select" handler: read attachments out of the * frame's selection model and rewrite the gallery to match. * * @param {wp.media.frames.MediaFrame} frame * @param {jQuery} $field */ onManageSelect( frame, $field ) { const selection = frame.state().get( 'selection' ); const nextIds = []; selection.each( ( attachment ) => { const json = attachment.toJSON(); if ( json.id ) { nextIds.push( Number( json.id ) ); } } ); this.writeGallery( $field, nextIds ); announce( l10n.announceUpdated ); this.restoreMediaPostId(); }, /** * Replace frame "select" handler: swap the attachment at the * cached active index with the chosen one and keep that slot * surfaced as the hero image. * * @param {wp.media.frames.MediaFrame} frame * @param {jQuery} $field */ onReplaceSelect( frame, $field ) { const selection = frame.state().get( 'selection' ); const attachment = selection.first(); if ( ! attachment ) { return; } const newId = Number( attachment.toJSON().id ); const index = this.activeIndexForReplace; const ids = this.getFieldIds( $field ); if ( index === null || index < 0 || index >= ids.length ) { return; } ids[ index ] = newId; this.writeGallery( $field, ids, index ); announce( l10n.announceReplaced ); this.restoreMediaPostId(); }, /** * Replace the field's gallery with the given ID list, dedupe, * re-render thumbs, surface the active slot, and sync the field. * * @param {jQuery} $field * @param {number[]} nextIds * @param {number} [activeIndex=0] */ writeGallery( $field, nextIds, activeIndex = 0 ) { const uniqueIds = Array.from( new Set( nextIds ) ).filter( ( id ) => Number.isInteger( id ) && id > 0 ); this.rebuildThumbs( $field, uniqueIds ); this.setActiveIndex( $field, Math.min( activeIndex, Math.max( uniqueIds.length - 1, 0 ) ) ); this.syncField( $field ); }, /** * Read the cached thumbnail/hero `src` for an attachment from the * existing field DOM. Server-rendered tags carry valid URLs * even for attachments that haven't been loaded into wp.media's * client cache (e.g. migrator-imported images), so prefer the DOM * over `wp.media.attachment(id).attributes`. * * @param {jQuery} $field * @param {number} id * @return {string} */ getCachedAttachmentUrl( $field, id ) { const $existingThumb = $field.find( SELECTORS.thumb + '[data-attachment_id="' + id + '"] ' + SELECTORS.thumbButton + ' img' ); if ( $existingThumb.length ) { return $existingThumb.attr( 'src' ) || ''; } const $existingHero = $field.find( SELECTORS.fieldHeroImg + '[data-id="' + id + '"]' ); if ( $existingHero.length ) { return $existingHero.attr( 'src' ) || ''; } return ''; }, /** * Resolve a usable URL for the given attachment, preferring the * server-rendered DOM and falling back to the media-frame cache * (which is hydrated for attachments the merchant just selected * via wp.media). * * @param {jQuery} $field * @param {number} id * @param {(json: Object) => string} pick * @return {string} */ resolveAttachmentUrl( $field, id, pick ) { const cached = this.getCachedAttachmentUrl( $field, id ); if ( cached ) { return cached; } const attachment = wp.media.attachment( id ); return pick( attachment.attributes || {} ); }, /** * Re-render the thumbnail list from scratch for the given IDs. * Caller is responsible for ensuring the IDs are unique and * non-empty before this is invoked. * * @param {jQuery} $field * @param {number[]} ids */ rebuildThumbs( $field, ids ) { const $list = $field.find( SELECTORS.fieldThumbList ); const urls = ids.map( ( id ) => this.resolveAttachmentUrl( $field, id, pickAttachmentThumbnailUrl ) ); $list.empty(); ids.forEach( ( id, index ) => { $list.append( this.buildThumbMarkup( id, urls[ index ], index === 0 ) ); } ); if ( $list.data( 'wc-variation-gallery-sortable' ) ) { $list.sortable( 'refresh' ); } }, /** * Build the markup for a single thumbnail list item. * * Renders a "missing file" placeholder when no thumbnail * URL is available. * * @param {number} id * @param {string} thumbnailUrl * @param {boolean} isActive * @return {jQuery} */ buildThumbMarkup( id, thumbnailUrl, isActive ) { const labelTemplate = l10n.thumbLabel || 'Show gallery image %d'; const label = labelTemplate.replace( '%d', id ); const $li = $( '
  • ' ) .addClass( 'wc-variation-gallery-thumb' ) .toggleClass( CLASSES.isActive, isActive ) .attr( 'data-attachment_id', id ); const $button = $( '' ) .addClass( 'wc-variation-gallery-thumb__button' ) .attr( 'aria-label', label ); const $remove = $( '' ) .addClass( 'wc-variation-gallery-thumb__remove' ) .attr( 'aria-label', l10n.removeLabel || 'Remove image' ) .append( $( '' ) .addClass( 'dashicons dashicons-no-alt' ) .attr( 'aria-hidden', 'true' ) ); if ( thumbnailUrl ) { const $img = $( '' ) .attr( 'src', thumbnailUrl ) .attr( 'alt', '' ); $button.append( $img ); return $li.append( $button, $remove ); } $li.addClass( CLASSES.isBroken ); const $brokenIcon = $( '' ).addClass( 'dashicons dashicons-format-image' ); const $brokenWrapper = $( '' ) .addClass( 'wc-variation-gallery-thumb__broken' ) .attr( 'aria-hidden', 'true' ) .append( $brokenIcon ); const $srLabel = $( '' ) .addClass( 'screen-reader-text' ) .text( l10n.missingFileLabel || 'Attachment file missing' ); $button.append( $brokenWrapper, $srLabel ); return $li.append( $button, $remove ); }, /** * Surface the slot at `index` in the hero area and mark its thumb * as active. Falls back to the empty state if there are no images. * * @param {jQuery} $field * @param {number} index */ setActiveIndex( $field, index ) { const ids = this.getFieldIds( $field ); if ( ! ids.length ) { this.setHeroEmpty( $field ); return; } const safeIndex = Math.max( 0, Math.min( index, ids.length - 1 ) ); const activeId = ids[ safeIndex ]; $field .find( SELECTORS.fieldHero ) .attr( 'data-active-index', safeIndex ); this.setHeroImage( $field, activeId, safeIndex === 0 ); $field.find( SELECTORS.thumb ).removeClass( CLASSES.isActive ); $field .find( SELECTORS.thumb + '[data-attachment_id="' + activeId + '"]' ) .addClass( CLASSES.isActive ); }, /** * @param {jQuery} $field * @return {number} */ getActiveIndex( $field ) { return Number( $field .find( SELECTORS.fieldHero ) .attr( 'data-active-index' ) || 0 ); }, /** * @param {jQuery} $field * @return {number} */ getActiveAttachmentId( $field ) { const ids = this.getFieldIds( $field ); if ( ! ids.length ) { return 0; } return ids[ this.getActiveIndex( $field ) ] || ids[ 0 ]; }, /** * Render the given attachment in the hero slot. Falls through to * the missing-file state if the attachment record has no usable * URL (e.g. the underlying file has been deleted). * * @param {jQuery} $field * @param {number} attachmentId * @param {boolean} isPrimary */ setHeroImage( $field, attachmentId, isPrimary ) { const $hero = $field.find( SELECTORS.fieldHero ); const url = this.resolveAttachmentUrl( $field, attachmentId, pickAttachmentDisplayUrl ); if ( ! url ) { this.setHeroMissingFile( $field, isPrimary ); return; } clearHeroOverlays( $hero ); const $img = getOrCreateHeroImage( $hero ); $img.attr( 'src', url ) .attr( 'data-id', attachmentId ) .attr( 'alt', '' ); this.ensureHeroControls( $hero, isPrimary ); }, /** * Render the "attachment file is missing" placeholder in the hero * slot. Used when an attachment row exists but the underlying file * has been deleted or is otherwise unreachable. * * @param {jQuery} $field * @param {boolean} isPrimary */ setHeroMissingFile( $field, isPrimary ) { const $hero = $field.find( SELECTORS.fieldHero ); $hero.find( SELECTORS.fieldHeroEmptyCta ).remove(); $hero.find( SELECTORS.fieldHeroImg ).remove(); if ( ! $hero.find( SELECTORS.fieldHeroBroken ).length ) { const $brokenIcon = $( '' ).addClass( 'dashicons dashicons-format-image' ); const $brokenWrapper = $( '' ) .addClass( CLASSES.fieldHeroBroken ) .attr( 'aria-hidden', 'true' ) .append( $brokenIcon ); const $srLabel = $( '' ) .addClass( 'screen-reader-text' ) .attr( 'data-missing-file-label', 'true' ) .text( l10n.missingFileLabel || 'Attachment file missing' ); $hero.prepend( $brokenWrapper, $srLabel ); } this.ensureHeroControls( $hero, isPrimary ); }, /** * Ensure the primary-image badge and the Replace button are * present in the hero slot. Idempotent. * * @param {jQuery} $hero * @param {boolean} isPrimary */ ensureHeroControls( $hero, isPrimary ) { if ( ! $hero.find( SELECTORS.primaryBadge ).length ) { const $badgeIcon = $( '' ).addClass( 'dashicons dashicons-star-filled' ); const badgeLabel = document.createTextNode( ' ' + ( l10n.primaryLabel || 'Primary' ) ); const $badge = $( '' ) .addClass( 'wc-variation-gallery-field__badge' ) .attr( 'data-primary-badge', '' ) .attr( 'aria-hidden', 'true' ) .append( $badgeIcon ) .append( badgeLabel ); $hero.append( $badge ); } $hero.find( SELECTORS.primaryBadge ).toggle( Boolean( isPrimary ) ); if ( ! $hero.find( SELECTORS.replaceTrigger ).length ) { const $replace = $( '' ) .addClass( 'button wc-variation-gallery-replace' ) .text( l10n.replaceLabel || 'Replace' ); $hero.append( $replace ); } }, /** * Render the empty-gallery state in the hero slot: a single CTA * that opens the WP media frame. * * @param {jQuery} $field */ setHeroEmpty( $field ) { const $hero = $field.find( SELECTORS.fieldHero ); const $ctaIcon = $( '' ) .addClass( 'dashicons dashicons-plus-alt2' ) .attr( 'aria-hidden', 'true' ); const ctaLabel = document.createTextNode( ' ' + ( l10n.emptyCtaLabel || 'Add variation images' ) ); const $cta = $( '' ) .addClass( 'wc-variation-gallery-field__empty-cta wc-variation-gallery-manage' ) .append( $ctaIcon ) .append( ctaLabel ); $hero.empty().attr( 'data-active-index', 0 ).append( $cta ); $field.addClass( CLASSES.isEmpty ); }, /** * @param {jQuery} $field * @return {number[]} */ getFieldIds( $field ) { return $field .find( SELECTORS.thumb ) .map( function () { return Number( $( this ).attr( 'data-attachment_id' ) ); } ) .get() .filter( ( id ) => Number.isInteger( id ) && id > 0 ); }, /** * Persist current DOM state back into the hidden form input and * the legacy single-image slot, then refresh the count label. * * @param {jQuery} $field */ syncField( $field ) { const ids = this.getFieldIds( $field ); const primaryId = ids[ 0 ] || ''; $field .find( SELECTORS.fieldImageIdsInput ) .val( ids.join( ',' ) ) .trigger( 'change' ); this.syncLegacyImageSlot( $field, primaryId ); this.updateFromDom( $field, ids.length ); }, /** * Keep the existing single-image variation field (`upload_image_id`, * its upload-image button, and the inline preview) in sync with the * gallery's primary image. This lets the existing variation save * path persist the featured image without changes. * * @param {jQuery} $field * @param {number|string} primaryId Attachment ID, or empty string when no primary is set. */ syncLegacyImageSlot( $field, primaryId ) { const $row = $field.closest( SELECTORS.variationRow ); this.updateLegacyInput( $row, primaryId ); this.updateLegacyButton( $row, primaryId ); this.updateLegacyPreview( $row, primaryId ); }, /** * Mirror the gallery's primary image into the hidden * `upload_image_id[ loop ]` input that the variation save path * already reads. * * @param {jQuery} $row * @param {number|string} primaryId */ updateLegacyInput( $row, primaryId ) { const $input = $row.find( SELECTORS.legacyInput ); if ( ! $input.length ) { return; } $input.val( primaryId ).trigger( 'change' ); }, /** * Toggle the upload-image button's "remove" affordance based on * whether a primary image is currently set. * * @param {jQuery} $row * @param {number|string} primaryId */ updateLegacyButton( $row, primaryId ) { const $button = $row.find( SELECTORS.legacyButton ); if ( ! $button.length ) { return; } $button.toggleClass( 'remove', Boolean( primaryId ) ); }, /** * Update the inline thumbnail preview inside the upload-image * button. Stashes the original placeholder src on the first call * so it can be restored when the merchant clears the gallery. * * @param {jQuery} $row * @param {number|string} primaryId */ updateLegacyPreview( $row, primaryId ) { const $preview = $row .find( SELECTORS.legacyButton ) .find( 'img' ) .first(); if ( ! $preview.length ) { return; } // Stash the placeholder src on first call so we can restore it later. if ( ! $preview.attr( 'data-placeholder-src' ) ) { $preview.attr( 'data-placeholder-src', $preview.attr( 'src' ) || '' ); } if ( ! primaryId ) { $preview.attr( 'src', $preview.attr( 'data-placeholder-src' ) || '' ); return; } const $field = $row.find( SELECTORS.field ); const url = this.resolveAttachmentUrl( $field, primaryId, pickAttachmentThumbnailUrl ); if ( url ) { $preview.attr( 'src', url ); } }, /** * Refresh the count label, the empty-state class, and the hint * visibility from the field's current image count. Pass an * explicit count to skip the DOM lookup. * * @param {jQuery} $field * @param {number|null} [precomputedCount=null] */ updateFromDom( $field, precomputedCount = null ) { const count = precomputedCount === null ? this.getFieldIds( $field ).length : precomputedCount; const template = l10n[ getCountTemplateKey( count ) ] || '%d'; const label = template.replace( '%d', count ); $field.toggleClass( CLASSES.isEmpty, count === 0 ); $field.find( SELECTORS.fieldCount ).text( label ); $field.find( SELECTORS.fieldHint ).prop( 'hidden', count === 0 ); }, restoreMediaPostId() { wp.media.model.settings.post.id = this.wpMediaPostId; }, }; variationGallery.init(); } );