@@ -879,3 +879,87 @@ function applyTheme(theme) {
879879}
880880
881881applyTheme ( savedTheme )
882+
883+ const GstCardHelpers = {
884+ shuffleImmutableArray : ( array ) => {
885+ const newArray = [ ...array ] ;
886+ for ( let i = newArray . length - 1 ; i > 0 ; i -- ) {
887+ const j = Math . floor ( Math . random ( ) * ( i + 1 ) ) ;
888+ [ newArray [ i ] , newArray [ j ] ] = [ newArray [ j ] , newArray [ i ] ] ;
889+ }
890+ return newArray ;
891+ } ,
892+
893+ shuffleDOMChildrenWithLimit : ( parent , limit ) => {
894+ const children = Array . from ( parent . children ) ;
895+ const replacements = GstCardHelpers . shuffleImmutableArray ( children ) . slice ( 0 , limit ) ;
896+
897+ while ( parent . children . length ) {
898+ parent . removeChild ( parent . children [ 0 ] ) ;
899+ }
900+
901+ replacements . forEach ( n => parent . appendChild ( n ) ) ;
902+ } ,
903+
904+ dynamicAlignment : ( parent , targetWidth ) => {
905+ parent . style . display = 'flex' ;
906+ parent . style . flexWrap = 'wrap' ;
907+ parent . style . justifyContent = 'center' ;
908+
909+ let resizeTimer ;
910+
911+ const resizeObserver = new ResizeObserver ( ( entries ) => {
912+ // Instantly apply the class that kills transitions on children
913+ // the transition is there for the hover effects but it makes it ugly when resizing
914+ parent . classList . add ( 'gst-no-transition' ) ;
915+
916+ // Clear previous timeout and set a new one to restore transitions
917+ // 100ms after the resize completely stops.
918+ clearTimeout ( resizeTimer ) ;
919+ resizeTimer = setTimeout ( ( ) => {
920+ parent . classList . remove ( 'gst-no-transition' ) ;
921+ } , 100 ) ;
922+
923+ for ( const entry of entries ) {
924+ const containerWidth = entry . contentRect . width ;
925+ const children = Array . from ( parent . children ) ;
926+ const itemCount = children . length ;
927+
928+ // Prevent dividing by zero or calculating for empty containers
929+ if ( itemCount === 0 || containerWidth === 0 ) continue ;
930+
931+ const computedStyle = window . getComputedStyle ( parent ) ;
932+ const columnGap = parseFloat ( computedStyle . columnGap ) || 0 ;
933+
934+ // Calculate the max number of items that can theoretically fit in one row
935+ let columnsPerRow = Math . floor ( ( containerWidth + columnGap ) / ( targetWidth + columnGap ) ) ;
936+ if ( columnsPerRow < 1 ) columnsPerRow = 1 ;
937+
938+ let itemWidth ;
939+
940+ if ( itemCount >= columnsPerRow ) {
941+ // We have enough items to completely fill at least one row.
942+ // Calculate exact width needed to stretch across the full container.
943+ const exactWidth = ( containerWidth - ( columnsPerRow - 1 ) * columnGap ) / columnsPerRow ;
944+
945+ // Subtract 0.5px margin of error to guarantee that browser rounding
946+ // engines never accidentally push the final item onto the next row.
947+ itemWidth = exactWidth - 0.5 ;
948+ } else {
949+ // We DO NOT have enough items to completely fill a single row.
950+ // Constrain them to the targetWidth, clamping it if the container is tiny.
951+ itemWidth = Math . min ( targetWidth , containerWidth ) ;
952+ }
953+
954+ // Apply synchronously so it paints in the same frame
955+ children . forEach ( ( child ) => {
956+ child . style . boxSizing = 'border-box' ;
957+ child . style . width = `${ itemWidth } px` ;
958+ child . style . flex = `0 0 ${ itemWidth } px` ;
959+ } ) ;
960+ }
961+ } ) ;
962+
963+ resizeObserver . observe ( parent ) ;
964+ }
965+ } ;
0 commit comments