From 6c19005d0a1825e54de7cb5501c28da0e3876bd9 Mon Sep 17 00:00:00 2001
From: smmercuri <smercuri@ed.ac.uk>
Date: Wed, 29 May 2024 12:52:11 +0100
Subject: [PATCH] Auto-resizing of grid-items in matching questions

---
 corsscripts/sortable.css                    |  9 ++-
 corsscripts/sortable.min.css                |  2 +-
 corsscripts/stacksortable.js                | 81 ++++++++++++++++-----
 corsscripts/stacksortable.min.js            | 16 ++--
 doc/en/Topics/Matching.md                   |  6 +-
 stack/cas/castext2/blocks/parsons.block.php |  8 +-
 6 files changed, 90 insertions(+), 32 deletions(-)

diff --git a/corsscripts/sortable.css b/corsscripts/sortable.css
index c7585fd58..1c4d7ff9a 100644
--- a/corsscripts/sortable.css
+++ b/corsscripts/sortable.css
@@ -77,7 +77,8 @@ button {
     min-height: 50px;
     height: auto; 
     background-color: inherit; text-align: left; border-right: thick solid rgb(196, 131, 10); 
-    padding: 10px; margin: 12px;
+    padding: 10px; 
+    margin: 6px;
 }
 
 .grid-item {
@@ -85,7 +86,7 @@ button {
 	background-color: #fff;
 	border: solid 1px rgb(0,0,0,0.2);
 	padding: 10px;
-	margin: 12px;
+	margin: 6px;
     display: flex;
     text-align: center;
 }
@@ -96,7 +97,7 @@ button {
 	background-color: #fff;
 	border: solid 1px rgb(0,0,0,0.2);
 	padding: 10px;
-	margin: 12px;
+	margin: 6px;
     display: flex;
     text-align: center;
 }
@@ -109,7 +110,7 @@ button {
     height: 50px;
     background-color: floralwhite; border: 1px solid rgb(155, 199, 206); 
     padding: 10px;
-	margin: 12px;
+	margin: 6px;
     text-align: center;
     display: flex;
 }
diff --git a/corsscripts/sortable.min.css b/corsscripts/sortable.min.css
index 674e41b13..69bcb6c89 100644
--- a/corsscripts/sortable.min.css
+++ b/corsscripts/sortable.min.css
@@ -1,2 +1,2 @@
 @import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css");@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css");@import url("../styles.css");body{background-color:inherit;}
-button{margin:2px 3px;}.container{display:flex;flex-wrap:wrap;}.usedList>li[data-id]{background-color:rgb(176,221,228);border-left:thick solid rgb(155,199,206);float:left;flex-shrink:1;}.usedList.col-rigid>li[data-id]{margin-left:0;}.row{margin-right:0;margin-left:0;margin-bottom:0;}.col-rigid{width:175px;flex:0 0 auto;display:inline-block}#availableList>li{background-color:rgb(243,189,88);float:left;}#availableList:empty{height:50px;background-color:lightpink;}#availableList>.sortable-chosen{box-shadow:0px 0px 0px 3px rgb(226,150,9)inset;}.usedList>.sortable-chosen{box-shadow:0px 0px 0px 3px rgb(155,199,206)inset;}.usedList>.header{height:50px;background-color:inherit;text-align:center;border-bottom:thick solid;padding:10px;display:inline-block;}.usedList>.index{background-color:inherit;text-align:left;border-right:thick solid;display:inline-block;margin-right:12px;}#availableList>.header{min-height:50px;height:auto;width:max(100%,150px);background-color:inherit;text-align:center;border-bottom:thick solid rgb(196,131,10);padding:10px;display:inline-block;}#availableList>.index{min-height:50px;height:auto;background-color:inherit;text-align:left;border-right:thick solid rgb(196,131,10);padding:10px;margin:12px;}.grid-item{height:50px;background-color:#fff;border:solid 1px rgb(0,0,0,0.2);padding:10px;margin:12px;display:flex;text-align:center;}.grid-item-rigid{height:50px;width:150px;background-color:#fff;border:solid 1px rgb(0,0,0,0.2);padding:10px;margin:12px;display:flex;text-align:center;}#availableList>.grid-item-rigid.index{width:150px;}.usedList.empty{height:50px;background-color:floralwhite;border:1px solid rgb(155,199,206);padding:10px;margin:12px;text-align:center;display:flex;}.usedList.col-rigid:empty{width:150px;}.parsons-button{display:inline-flex;align-items:center;padding:0.5rem 0.75rem;margin:2px 2px;font-size:25px;line-height:1.5;text-align:center;white-space:nowrap;vertical-align:middle;user-select:none;cursor:pointer;background-color:#6c757d;border:1px solid#6c757d;color:#ffffff;border-radius:0.35rem;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;}.parsons-button:hover{background-color:#5a6268;border-color:#545b62;color:#ffffff;}.parsons-bin{width:33%;cursor:default;}.bin-icon{margin-right:0.75rem;}.drop-zone{vertical-align:middle;display:inline-flex;flex:1;border:2px dashed#6c757d;border-radius:0.25rem;background-color:lightgray;align-self:stretch;}.drop-zone>li{font-size:0.75rem;background-color:lightcoral;max-height:100%;max-width:100%;text-align:left;flex:1;box-shadow:0px 0px 0px 2px rgb(167,89,89)inset;}.sortable-warning{float:right;font-size:0.8em;margin:-0.3em-0.3em;}
\ No newline at end of file
+button{margin:2px 3px;}.container{display:flex;flex-wrap:wrap;}.usedList>li[data-id]{background-color:rgb(176,221,228);border-left:thick solid rgb(155,199,206);float:left;flex-shrink:1;}.usedList.col-rigid>li[data-id]{margin-left:0;}.row{margin-right:0;margin-left:0;margin-bottom:0;}.col-rigid{width:175px;flex:0 0 auto;display:inline-block}#availableList>li{background-color:rgb(243,189,88);float:left;}#availableList:empty{height:50px;background-color:lightpink;}#availableList>.sortable-chosen{box-shadow:0px 0px 0px 3px rgb(226,150,9)inset;}.usedList>.sortable-chosen{box-shadow:0px 0px 0px 3px rgb(155,199,206)inset;}.usedList>.header{height:50px;background-color:inherit;text-align:center;border-bottom:thick solid;padding:10px;display:inline-block;}.usedList>.index{background-color:inherit;text-align:left;border-right:thick solid;display:inline-block;margin-right:12px;}#availableList>.header{min-height:50px;height:auto;width:max(100%,150px);background-color:inherit;text-align:center;border-bottom:thick solid rgb(196,131,10);padding:10px;display:inline-block;}#availableList>.index{min-height:50px;height:auto;background-color:inherit;text-align:left;border-right:thick solid rgb(196,131,10);padding:10px;margin:6px;}.grid-item{height:50px;background-color:#fff;border:solid 1px rgb(0,0,0,0.2);padding:10px;margin:6px;display:flex;text-align:center;}.grid-item-rigid{height:50px;width:150px;background-color:#fff;border:solid 1px rgb(0,0,0,0.2);padding:10px;margin:6px;display:flex;text-align:center;}#availableList>.grid-item-rigid.index{width:150px;}.usedList.empty{height:50px;background-color:floralwhite;border:1px solid rgb(155,199,206);padding:10px;margin:6px;text-align:center;display:flex;}.usedList.col-rigid:empty{width:150px;}.parsons-button{display:inline-flex;align-items:center;padding:0.5rem 0.75rem;margin:2px 2px;font-size:25px;line-height:1.5;text-align:center;white-space:nowrap;vertical-align:middle;user-select:none;cursor:pointer;background-color:#6c757d;border:1px solid#6c757d;color:#ffffff;border-radius:0.35rem;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;}.parsons-button:hover{background-color:#5a6268;border-color:#545b62;color:#ffffff;}.parsons-bin{width:33%;cursor:default;}.bin-icon{margin-right:0.75rem;}.drop-zone{vertical-align:middle;display:inline-flex;flex:1;border:2px dashed#6c757d;border-radius:0.25rem;background-color:lightgray;align-self:stretch;}.drop-zone>li{font-size:0.75rem;background-color:lightcoral;max-height:100%;max-width:100%;text-align:left;flex:1;box-shadow:0px 0px 0px 2px rgb(167,89,89)inset;}.sortable-warning{float:right;font-size:0.8em;margin:-0.3em-0.3em;}
\ No newline at end of file
diff --git a/corsscripts/stacksortable.js b/corsscripts/stacksortable.js
index 6570642a5..d362eed6b 100644
--- a/corsscripts/stacksortable.js
+++ b/corsscripts/stacksortable.js
@@ -276,6 +276,8 @@ export function get_iframe_height() {
  * @property {boolean} use_index - Whether an index has been passed to the constructor or not.
  * @property {boolean} grid - Whether grid styling is to be applied (i.e., false if using for proof).
  * @property {string} item_class - The style class to use, different for proof vs. matching.
+ * @property {boolean} override_item_height - Whether a custom item height has been passed.
+ * @property {boolean} override_item_width - Whether a custom item width has been passed.
  * @property {Object} item_height_width - If item_height and item_width are passed to the constructor, object containing them.
  * @property {Object} container_height_width - Add padding to this.item_height_width to allow custom heights/widths to work.
  * @property {Object} ids - Contains DOM ids for used and available lists.
@@ -292,7 +294,8 @@ export function get_iframe_height() {
  * @method generate_available - Generates the available list based on the current state.
  * @method generate_used - Generates the used list based on the current state.
  * @method update_state - Updates the state based on changes in the used and available lists.
- * @method resize_grid_items - Resizes the height of divs with `grid-item` class according to the max height across grid items.
+ * @method update_grid_css - Updates the CSS styling of grid-items.
+ * @method resize_grid_items - Resizes the height and widths of elements with `grid-item` class according to the max values across grid items.
  * @method validate_options - Validates the sortable user options.
  *
  * @example
@@ -367,13 +370,14 @@ export const stack_sortable = class stack_sortable {
         this.item_class = (
             this.grid ? (this.orientation === "row" ? "grid-item-rigid" : "grid-item") : "list-group-item"
         );
+        this.override_item_height = (item_height !== "");
+        this.override_item_width = (item_width !== "");
         this.item_height_width = {"style" : ""};
-        for (const [key, val] of [["height", item_height], ["width", item_width]]) {
-            if (val !== "") {this.item_height_width["style"] += `${key}:${val}px;`};
-        };
+        if (this.override_item_height) {this.item_height_width["style"] += `height:${item_height}px;`}
+        if (this.override_item_width) {this.item_height_width["style"] += `width:${item_width}px;`}
         this.item_height_width = (this.item_height_width["style"] === "") ? {} : this.item_height_width;
         this.container_height_width = (Object.keys(this.item_height_width).length !== 0) ?
-            {"style" : this.item_height_width["style"] + "margin: 12px;"} : {};
+            {"style" : this.item_height_width["style"]} : {};
         this.state = this._generate_state(this.steps, inputId, Number(this.columns), Number(this.rows));
         if (inputId !== null) {
             this.input = document.getElementById(this.inputId);
@@ -593,8 +597,13 @@ export const stack_sortable = class stack_sortable {
     }
 
     /**
-     * Resizes all grid-item heights according to the maximum content height across all grid-items, adding an extra `10px` padding.
+     * Resizes all grid-item heights and widths according to the maximum content height across all grid-items.
+     * Only resizes widths if in "row" orientation, where we have to fix widths witht the grid-item-rigid class.
+     * Otherwise widths are already automatically resized.
      * Avoids affecting proof mode by virtue of grid-item and grid-item-rigid classes not being used there.
+     * 
+     * If `item_height` parameter has been passed, then heights will not be autoresized.
+     * Likewise, if `item_width` parameter has been passed, then widths will not be autoresized.
      *
      * @method
      * @returns {void}
@@ -602,19 +611,34 @@ export const stack_sortable = class stack_sortable {
     resize_grid_items() {
             const allGridItems = document.querySelectorAll('.grid-item, .grid-item-rigid');
             const maxHeight = Math.max(...[...allGridItems].map((item) => item.scrollHeight));
-            const maxWidth = Math.max(...[...allGridItems].map((item) => item.clientWidth));
+            const maxWidth = Math.max(...[...allGridItems].map((item) => item.scrollWidth));
+
+            // Resize all grid-item or grid-item-rigid divs heights.
+            allGridItems.forEach((item) => {
+                item.style.height = (this.override_item_height)? this.item_height_width['style']['height'] : `${maxHeight}px`;
+                // If the orientation is horizontal, then we also need to adjust widths due to rigid styles.
+                if (this.orientation === "row") {
+                    item.style.width = (this.override_item_width)? this.item_height_width['style']['width'] : `${maxWidth}px`; 
+                } else {
+                    // Reset the width to default if switching from horizontal to vertical.
+                    item.style.width = (this.override_item_width)? this.item_height_width['style']['width'] : '';
+                }
+            });
+
             // In grid fixed grid mode we also need to resize the individual item containers.
             if (this.rows !== "" && this.columns !== "") {
                 for (const [i, value] of this.state.used.entries()) 
                     for (const [j, val] of value.entries()) {
-                        this._apply_attrs(this.used[i][j], {'style' : 'height: ' + `${maxHeight + 10}px;` + 'margin: 12px;'});
+                        this.used[i][j].style.height = (this.override_item_height)? this.item_height_width['style']['height'] : `${maxHeight + 12}px`;
+                        // For horizontal orientaion, also adjust width.
+                        if (this.orientation === "row") {
+                            this.used[i][j].style.width = (this.override_item_width)? this.item_height_width['style']['width'] : `${maxWidth + 12}px`; 
+                        } else {
+                            // Reset the width to default if switching from horizontal to vertical.
+                            this.used[i][j].style.width = (this.override_item_width)? this.item_height_width['style']['width'] : '';
+                        }
                     }
                 }
-            // In all cases resize all grid-item or grid-item-rigid divs.
-            allGridItems.forEach((item) => {
-                item.style.height = `${maxHeight + 10}px`;
-                //item.style.width = `${maxWidth + 10}px`; 
-            });
     }
 
     /**
@@ -632,13 +656,33 @@ export const stack_sortable = class stack_sortable {
             this.input.dispatchEvent(new Event("change"));
         }
         this.state = newState;
+    }
 
+    /**
+     * Updates empty class elements according as whether they are no longer empty or become empty.
+     * Also updates the autosizing of all grid-item and grid-item-rigid elements.
+     * This should be passed to `onSort` option of sortables so that every time an element is dragged, 
+     * the CSS updates accordingly.
+     *
+     * @method
+     * @returns {void}
+     */
+    update_grid_css() {
+        // Remove empty class if no longer empty.
         const empties = document.querySelectorAll('.empty');
-        empties.forEach((div) => {if (!this._is_empty(div)) {
-            div.classList.remove('empty');
-            div.style.height = '';
-            div.style.margin = '';
+        empties.forEach((el) => {if (!this._is_empty(el)) {
+            el.classList.remove('empty');
+            el.style.height = '';
+            el.style.margin = '';
+        }})
+
+        // re-assign empty class if empty
+        const usedLists = document.querySelectorAll('.usedList');
+        usedLists.forEach((el) => {if (this._is_empty(el)) {
+            el.classList.add('empty');
         }})
+
+        this.resize_grid_items();
     }
 
     /**
@@ -960,6 +1004,9 @@ export const stack_sortable = class stack_sortable {
 
         // Keep track of current orientation.
         this.orientation = (this.orientation === "row") ? "col" : "row";
+
+        // Resize the grid-items as appropriate
+        this.resize_grid_items();
     }
 
     /**
diff --git a/corsscripts/stacksortable.min.js b/corsscripts/stacksortable.min.js
index ebda1e95d..aa62f77c3 100644
--- a/corsscripts/stacksortable.min.js
+++ b/corsscripts/stacksortable.min.js
@@ -14,7 +14,9 @@ function _validate_flat_steps(steps){if(typeof(steps)=="string"){steps=_stackstr
 return Object.values(steps).every((val)=>typeof(val)=="string");}
 function _validate_top_level_keys_JSON(JSON,validKeys,requiredKeys){const keys=Object.keys(JSON);const missingRequiredKeys=requiredKeys.filter((key)=>!keys.includes(key));const invalidKeys=keys.filter((key)=>!validKeys.includes(key));return invalidKeys.length===0&&missingRequiredKeys.length===0;}
 export function get_iframe_height(){return document.documentElement.offsetHeight;}
-export const stack_sortable=class stack_sortable{constructor(steps,inputId=null,options=null,clone=false,columns=1,rows=null,orientation="col",index="",grid=false,item_height=null,item_width=null){this.steps=steps;this.inputId=inputId;this.orientation=orientation;this.columns=((this.orientation==="col")?columns:rows);this.rows=((this.orientation==="col")?rows:columns);this.index=index;this.use_index=this.index!=="";this.grid=grid;this.item_class=(this.grid?(this.orientation==="row"?"grid-item-rigid":"grid-item"):"list-group-item");this.item_height_width={"style":""};for(const[key,val]of[["height",item_height],["width",item_width]]){if(val!==""){this.item_height_width["style"]+=`${key}:${val}px;`};};this.item_height_width=(this.item_height_width["style"]==="")?{}:this.item_height_width;this.container_height_width=(Object.keys(this.item_height_width).length!==0)?{"style":this.item_height_width["style"]+"margin: 12px;"}:{};this.state=this._generate_state(this.steps,inputId,Number(this.columns),Number(this.rows));if(inputId!==null){this.input=document.getElementById(this.inputId);this.submitted=this.input.getAttribute("readonly")==="readonly"}
+export const stack_sortable=class stack_sortable{constructor(steps,inputId=null,options=null,clone=false,columns=1,rows=null,orientation="col",index="",grid=false,item_height=null,item_width=null){this.steps=steps;this.inputId=inputId;this.orientation=orientation;this.columns=((this.orientation==="col")?columns:rows);this.rows=((this.orientation==="col")?rows:columns);this.index=index;this.use_index=this.index!=="";this.grid=grid;this.item_class=(this.grid?(this.orientation==="row"?"grid-item-rigid":"grid-item"):"list-group-item");this.override_item_height=(item_height!=="");this.override_item_width=(item_width!=="");this.item_height_width={"style":""};if(this.override_item_height){this.item_height_width["style"]+=`height:${item_height}px;`}
+if(this.override_item_width){this.item_height_width["style"]+=`width:${item_width}px;`}
+this.item_height_width=(this.item_height_width["style"]==="")?{}:this.item_height_width;this.container_height_width=(Object.keys(this.item_height_width).length!==0)?{"style":this.item_height_width["style"]}:{};this.state=this._generate_state(this.steps,inputId,Number(this.columns),Number(this.rows));if(inputId!==null){this.input=document.getElementById(this.inputId);this.submitted=this.input.getAttribute("readonly")==="readonly"}
 this.ids=this._create_ids(this.rows,this.columns);this.availableId=this.ids.available;this.usedId=this.ids.used;this.clone=clone;this.defaultOptions={used:{animation:50},available:{animation:50}};this.userOptions=this._set_user_options(options);this.options=this._set_ghostClass_group_and_disabled_options();}
 add_dblclick_listeners(newUsed,newAvailable){this.available.addEventListener("dblclick",(e)=>{if(this._double_clickable(e.target)){var li=this._get_moveable_parent_li(e.target);li=(this.clone==="true")?li.cloneNode(true):this.available.removeChild(li);this.used[0][0].append(li);this.update_state(newUsed,newAvailable);}});this.used[0][0].addEventListener("dblclick",(e)=>{if(this._double_clickable(e.target)){var li=this._get_moveable_parent_li(e.target);this.used[0][0].removeChild(li);if(this.clone!=="true"){this.available.insertBefore(li,this.available.children[1]);}
 this.update_state(newUsed,newAvailable);}});}
@@ -30,11 +32,13 @@ colDiv.append(divRowCol);})})};var availDiv=document.createElement("ul");availDi
 this.used=this.usedId.map(idList=>idList.map(id=>document.getElementById(id)));this.available=document.getElementById(this.availableId);}
 generate_available(){this.state.available.forEach(key=>this.available.append(this._create_li(key,this.item_height_width)));}
 generate_used(){for(const[i,value]of this.state.used.entries()){if(this.rows!==""&&this.columns!==""){for(const[j,val]of value.entries()){this._apply_attrs(this.used[i][j],this.container_height_width);val.forEach(key=>this.used[i][j].append(this._create_li(key,this.item_height_width)));}}else{value[0].forEach(key=>this.used[i][0].append(this._create_li(key,this.item_height_width)));}}}
-resize_grid_items(){const allGridItems=document.querySelectorAll('.grid-item, .grid-item-rigid');const maxHeight=Math.max(...[...allGridItems].map((item)=>item.scrollHeight));const maxWidth=Math.max(...[...allGridItems].map((item)=>item.clientWidth));if(this.rows!==""&&this.columns!==""){for(const[i,value]of this.state.used.entries())
-for(const[j,val]of value.entries()){this._apply_attrs(this.used[i][j],{'style':'height: '+`${maxHeight+10}px;`+'margin: 12px;'});}}
-allGridItems.forEach((item)=>{item.style.height=`${maxHeight+10}px`;});}
+resize_grid_items(){const allGridItems=document.querySelectorAll('.grid-item, .grid-item-rigid');const maxHeight=Math.max(...[...allGridItems].map((item)=>item.scrollHeight));const maxWidth=Math.max(...[...allGridItems].map((item)=>item.scrollWidth));allGridItems.forEach((item)=>{item.style.height=(this.override_item_height)?this.item_height_width['style']['height']:`${maxHeight}px`;if(this.orientation==="row"){item.style.width=(this.override_item_width)?this.item_height_width['style']['width']:`${maxWidth}px`;}else{item.style.width=(this.override_item_width)?this.item_height_width['style']['width']:'';}});if(this.rows!==""&&this.columns!==""){for(const[i,value]of this.state.used.entries())
+for(const[j,val]of value.entries()){this.used[i][j].style.height=(this.override_item_height)?this.item_height_width['style']['height']:`${maxHeight+12}px`;if(this.orientation==="row"){this.used[i][j].style.width=(this.override_item_width)?this.item_height_width['style']['width']:`${maxWidth+12}px`;}else{this.used[i][j].style.width=(this.override_item_width)?this.item_height_width['style']['width']:'';}}}}
 update_state(newUsed,newAvailable){var newState={used:newUsed.map((usedList)=>usedList.map((used)=>used.toArray())),available:newAvailable.toArray()};if(this.inputId!==null){this.input.value=JSON.stringify(newState);this.input.dispatchEvent(new Event("change"));}
-this.state=newState;const empties=document.querySelectorAll('.empty');empties.forEach((div)=>{if(!this._is_empty(div)){div.classList.remove('empty');div.style.height='';div.style.margin='';}})}
+this.state=newState;}
+update_grid_css(){const empties=document.querySelectorAll('.empty');empties.forEach((el)=>{if(!this._is_empty(el)){el.classList.remove('empty');el.style.height='';el.style.margin='';}})
+const usedLists=document.querySelectorAll('.usedList');usedLists.forEach((el)=>{if(this._is_empty(el)){el.classList.add('empty');}})
+this.resize_grid_items();}
 validate_options(possibleOptionKeys,unknownErr,overwrittenErr){var err="";var keysRecognised=true;var invalidKeys=[];Object.keys(this.options.used).forEach(key=>{if(!this._validate_option_key(key,possibleOptionKeys)){keysRecognised=false;if(!invalidKeys.includes(key)){invalidKeys.push(key);}}});Object.keys(this.options.available).forEach(key=>{if(!this._validate_option_key(key,possibleOptionKeys)){keysRecognised=false;if(!invalidKeys.includes(key)){invalidKeys.push(key);}}});if(!keysRecognised){err+=unknownErr+invalidKeys.join(", ")+". ";}
 var overwrittenKeys=[];var keysPreserved=true;["ghostClass","group","onSort"].forEach(key=>{if(Object.keys(this.userOptions.used).includes(key)||Object.keys(this.userOptions.available).includes(key))
 {keysPreserved=false;overwrittenKeys.push(key);}});if(!keysPreserved){err+=overwrittenErr+overwrittenKeys.join(", ")+".";}
@@ -55,7 +59,7 @@ var gridItems=document.querySelectorAll(`.${currGridClass}`);gridItems.forEach((
 if(this.rows!==""){[].concat(...this.used).forEach((div)=>{if(this.orientation==="col"){div.classList.remove("row");div.classList.add("col","col-rigid");}else{div.classList.remove("col","col-rigid");div.classList.add("row");}})}}else{var removeClass=(this.orientation==="row")?["row"]:["col"];}
 this.colIds.forEach((colId)=>{var ul=document.getElementById(colId);ul.classList.remove(...removeClass);ul.classList.add(...addClass);});this.available.classList.remove(...removeClass);this.available.classList.add(...addClass);if(this.orientation==="col"){this.available.parentNode.insertBefore(this.available,this.available.parentNode.firstChild);}else{this.available.parentNode.append(this.available);}
 if(this.grid){if(this.orientation==="col"){document.querySelectorAll(".header").forEach((header)=>{if(!header.classList.contains("index")){header.classList.remove("header");header.classList.add("index");}});}else{document.querySelectorAll(".index").forEach((index)=>{if(!index.classList.contains("header")){index.classList.remove("index");index.classList.add("header");}})}
-if(this.use_index){var indexDiv=document.getElementById("index");indexDiv.classList.remove(...removeClass);indexDiv.classList.add(...addClass);if(this.orientation==="col"){document.querySelectorAll("#index > .index").forEach((idx)=>{if(!idx.classList.contains("header")){idx.classList.remove("index");idx.classList.add("header");}})}else{document.querySelectorAll("#index > .header").forEach((header)=>{if(!header.classList.contains("index")){header.classList.remove("header");header.classList.add("index");}})}}};this.orientation=(this.orientation==="row")?"col":"row";}
+if(this.use_index){var indexDiv=document.getElementById("index");indexDiv.classList.remove(...removeClass);indexDiv.classList.add(...addClass);if(this.orientation==="col"){document.querySelectorAll("#index > .index").forEach((idx)=>{if(!idx.classList.contains("header")){idx.classList.remove("index");idx.classList.add("header");}})}else{document.querySelectorAll("#index > .header").forEach((header)=>{if(!header.classList.contains("index")){header.classList.remove("header");header.classList.add("index");}})}}};this.orientation=(this.orientation==="row")?"col":"row";this.resize_grid_items();}
 _generate_state(steps,inputId,columns,rows){const usedState=(rows===0||columns===0)?Array(columns).fill().map(()=>[[]]):Array(columns).fill().map(()=>Array(rows).fill([]));let stateStore=document.getElementById(inputId);if(stateStore===null){return{used:usedState,available:[...Object.keys(steps)]};}
 return(stateStore.value&&stateStore.value!="")?JSON.parse(stateStore.value):{used:usedState,available:[...Object.keys(steps)]};}
 _get_moveable_parent_li(target){var li=target;while(!li.matches(".list-group-item")){li=li.parentNode;}
diff --git a/doc/en/Topics/Matching.md b/doc/en/Topics/Matching.md
index b4fadef83..52d7151be 100644
--- a/doc/en/Topics/Matching.md
+++ b/doc/en/Topics/Matching.md
@@ -71,9 +71,11 @@ See the examples below for more details.
 
 ## Item style
 
-In **Grouping** and **Grid** configurations, the height and width of individual items can be changed via the `item-height` and `item-width` header parameters. Parameter values should be a string containing a number, and this will set the pixels of the height/width. 
+In **Grouping** and **Grid** configurations, the height and width of individual items will by default auto-resize so that all their heights and widths are set to contain the largest item content. 
+
+This can be overriden via the `item-height` and `item-width` header parameters. Parameter values should be a string containing a number, and this will set the pixels of the height/width. 
 The default is `50px` (`item-height="50"`) for height, and the width is automatically deduced from the page layout and number of columns for the vertical orientation, or it is `100px` (`item-width="100"`) for the horizontal configuration. 
-This may be needed if you have images or other large overflowing items (including header titles).
+This may be needed if autosizing does not quite work as expected.
 
 ## The matching library
 
diff --git a/stack/cas/castext2/blocks/parsons.block.php b/stack/cas/castext2/blocks/parsons.block.php
index 8b4a8a683..8cfe552f2 100644
--- a/stack/cas/castext2/blocks/parsons.block.php
+++ b/stack/cas/castext2/blocks/parsons.block.php
@@ -311,10 +311,14 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block {
         $code .= 'sortableUsed.forEach((sortableList) =>
             sortableList.forEach((sortable) =>
                 sortable.option("onSort", () => {
-                    stackSortable.update_state(sortableUsed, sortableAvailable);})
+                    stackSortable.update_state(sortableUsed, sortableAvailable);
+                    stackSortable.update_grid_css();})
             )
         );' . "\n";
-        $code .= 'sortableAvailable.option("onSort", () => {stackSortable.update_state(sortableUsed, sortableAvailable);});' . "\n";
+        $code .= 'sortableAvailable.option("onSort", 
+            () => {
+                stackSortable.update_state(sortableUsed, sortableAvailable);
+                stackSortable.update_grid_css();});' . "\n";
 
         // Options can now be validated since sortable objects have been instantiated, we throw warnings only.
         $code .= 'stackSortable.validate_options(
-- 
GitLab