MDL-74054 questions: Fix question bank header widths

Chrome ignores min-width on table headers with table-layout:fixed,
meaning that question bank headers could be resized so that the controls
were overlapping, and could be too narrow by default.

This removes min-width: min-content in the headers and instead uses
Javascript to calculate a constrain a min width based on the content of
the headers.
This commit is contained in:
Mark Johnson 2023-09-22 15:12:51 +01:00 committed by Andrew Nicols
parent 216060b637
commit 0ac2936c11
No known key found for this signature in database
GPG Key ID: 6D1E3157C8CFBF14
11 changed files with 65 additions and 19 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -48,6 +48,9 @@ let currentHeader;
/** Current mouse x postion, to track mouse event on a table header */
let currentX;
/** Minimum size for the column currently being resized. */
let currentMin;
/**
* Flag to temporarily prevent move and resize handles from being shown or hidden.
*
@ -129,6 +132,21 @@ const serialiseColumnSizes = (uiRoot) => {
return JSON.stringify(columnSizes);
};
/**
* Find the minimum width for a header, based on the width of its contents.
*
* This is to simulate `min-width: min-content;`, which doesn't work on Chrome because
* min-width is ignored width `table-layout: fixed;`.
*
* @param {Element} header The table header
* @return {Number} The minimum width in pixels
*/
const getMinWidth = (header) => {
const contents = Array.from(header.querySelector('.header-text').children);
const contentWidth = contents.reduce((width, contentElement) => width + contentElement.getBoundingClientRect().width, 0);
return Math.ceil(contentWidth);
};
/**
* Render resize handles in each container.
*
@ -141,6 +159,11 @@ const setUpResizeHandles = (uiRoot) => {
const resizeActions = uiRoot.querySelectorAll(SELECTORS.resizeAction);
resizeActions.forEach(resizeAction => {
const headerContainer = resizeAction.closest(SELECTORS.headerContainer);
const header = resizeAction.closest(actions.SELECTORS.sortableColumn);
const minWidth = getMinWidth(header);
if (header.offsetWidth < minWidth) {
header.style.width = minWidth + 'px';
}
const handleContainer = headerContainer.querySelector(SELECTORS.handleContainer);
const context = {
action: "resize",
@ -170,6 +193,7 @@ const setUpResizeHandles = (uiRoot) => {
currentX = e.pageX;
// Find the header.
currentHeader = e.target.closest(actions.SELECTORS.sortableColumn);
currentMin = getMinWidth(currentHeader);
moveTracker = false;
suspendShowHideHandles = true;
});
@ -187,7 +211,9 @@ const setUpResizeHandles = (uiRoot) => {
const offset = e.pageX - currentX;
currentX = e.pageX;
const newWidth = currentHeader.offsetWidth + offset;
currentHeader.style.width = newWidth + 'px';
if (newWidth >= currentMin) {
currentHeader.style.width = newWidth + 'px';
}
moveTracker = true;
});
@ -203,6 +229,7 @@ const setUpResizeHandles = (uiRoot) => {
// If the mouse didn't move, display a modal to change the size using a form.
showResizeModal(currentHeader, uiRoot);
}
currentMin = null;
currentHeader = null;
currentResizeHandle = null;
currentX = 0;
@ -238,10 +265,11 @@ const setUpResizeActions = uiRoot => {
*/
const showResizeModal = async(currentHeader, uiRoot) => {
const initialWidth = currentHeader.offsetWidth;
const minWidth = getMinWidth(currentHeader);
const modal = await ModalSaveCancel.create({
title: getString('resizecolumn', 'qbank_columnsortorder', currentHeader.textContent),
body: Templates.render('qbank_columnsortorder/resize_modal', {}),
body: Templates.render('qbank_columnsortorder/resize_modal', {width: initialWidth, min: minWidth}),
show: true,
});
const root = modal.getRoot();
@ -254,11 +282,14 @@ const showResizeModal = async(currentHeader, uiRoot) => {
const body = await modal.bodyPromise;
const input = body.get(0).querySelector('input');
input.value = initialWidth;
input.addEventListener('change', e => {
const newWidth = e.target.value;
currentHeader.style.width = `${newWidth}px`;
const valid = e.target.checkValidity();
e.target.closest('.has-validation').classList.add('was-validated');
if (valid) {
const newWidth = e.target.value;
currentHeader.style.width = `${newWidth}px`;
}
});
};

View File

@ -35,9 +35,14 @@ use templatable;
*/
class column_sort_ui implements renderable, templatable {
/**
* @var int The minimum custom width for a column.
* The minimum custom width for a column.
*
* This is based on the minimum possible width of the smallest core column (question type).
* When viewed, the width will be resized to the minimum width of the column header, if too small.
*
* @var int
*/
const MIN_COLUMN_WIDTH = 10;
const MIN_COLUMN_WIDTH = 30;
public function export_for_template(\renderer_base $output): array {
$columnmanager = new column_manager(true);

View File

@ -27,7 +27,7 @@ $string['addcolumn'] = 'Add column \'{$a}\'';
$string['addcolumns'] = 'Add columns';
$string['auto'] = 'Auto';
$string['columnwidth'] = 'Column width (pixels)';
$string['invalidwidth'] = 'Width must be at least 10.';
$string['invalidwidth'] = 'Width must be at least {$a}.';
$string['movecolumn'] = 'Move column \'{$a}\'';
$string['pluginname'] = 'Column sort order';
$string['privacy:metadata:preference:enabledcol'] = 'The plugin saves user preference of column orders.';

View File

@ -149,7 +149,7 @@
{{#str}}save{{/str}}
</button>
<div class="invalid-feedback">
{{#str}}invalidwidth, qbank_columnsortorder{{/str}}
{{#str}}invalidwidth, qbank_columnsortorder, {{minwidth}}{{/str}}
</div>
</div>
</td>

View File

@ -23,7 +23,11 @@
{
}
}}
<label>
{{#str}}columnwidth, qbank_columnsortorder{{/str}}
<input name="columnwidth" type="number" value="">
</label>
<form class="has-validation">
<label for="columnwidth">{{#str}}columnwidth, qbank_columnsortorder{{/str}}</label>
<input class="form-control" id="columnwidth" type="number" value="{{width}}" min="{{min}}">
<div class="invalid-feedback">
{{#str}}invalidwidth, qbank_columnsortorder, {{min}}{{/str}}
</div>
</form>

View File

@ -97,6 +97,6 @@ class checkbox_column extends column_base {
}
public function get_default_width(): int {
return 25;
return 30;
}
}

View File

@ -189,7 +189,6 @@ table.question-bank-table {
.header {
text-align: left;
min-width: min-content;
&.sortable-list-current-position {
background-color: lighten($primary, 40%);
@ -206,6 +205,9 @@ table.question-bank-table {
.dropdown-toggle::after {
margin-left: 0;
}
&.checkbox .form-check {
padding-left: 0;
}
}
}

View File

@ -32054,7 +32054,6 @@ table.question-bank-table label {
}
table.question-bank-table .header {
text-align: left;
min-width: min-content;
}
table.question-bank-table .header.sortable-list-current-position {
background-color: #a2cff8;
@ -32069,6 +32068,9 @@ table.question-bank-table .header .header-text > div {
table.question-bank-table .header .dropdown-toggle::after {
margin-left: 0;
}
table.question-bank-table .header.checkbox .form-check {
padding-left: 0;
}
#page-mod-quiz-edit div.questionbankwindow div.header {
margin: 0;

View File

@ -32054,7 +32054,6 @@ table.question-bank-table label {
}
table.question-bank-table .header {
text-align: left;
min-width: min-content;
}
table.question-bank-table .header.sortable-list-current-position {
background-color: #a2cff8;
@ -32069,6 +32068,9 @@ table.question-bank-table .header .header-text > div {
table.question-bank-table .header .dropdown-toggle::after {
margin-left: 0;
}
table.question-bank-table .header.checkbox .form-check {
padding-left: 0;
}
#page-mod-quiz-edit div.questionbankwindow div.header {
margin: 0;