Add getTextLabel handling to all default bound widget classes

pull/13245/head
Sage Abdullah 2025-07-22 17:15:32 +01:00 zatwierdzone przez Thibaud Colas
rodzic 08018d565b
commit b1b7b6e696
5 zmienionych plików z 83 dodań i 11 usunięć

Wyświetl plik

@ -34,7 +34,8 @@ Changelog
* Add a keyboard shortcut to easily toggle the visibility of the minimap side panel (Dhruvi Patel)
* Add API for extracting preview page content (Sage Abdullah)
* Add toggle from grid to list layout for image listings (Joel William)
* Add support for opt-in collapsible `StructBlock`s (Sage Abdullah)
* Make `StructBlock`s collapsible when nested to support block settings (Sage Abdullah)
* Improve `label_format` support for more widget types in StreamField (Sage Abdullah)
* Add `form_attrs` support to all StreamField blocks (Sage Abdullah)
* Update project template documentation to include testing instructions and include starting test file in template (Aditya (megatrron))
* Add support for `preview_value` and `default` in `Block` meta options as callables for dynamic previews within StreamField (Ziyao Yan, Sage Abdullah)

Wyświetl plik

@ -1,4 +1,5 @@
import { setAttrs } from '../../utils/attrs';
import { gettext } from '../../utils/gettext';
import { runInlineScripts } from '../../utils/runInlineScripts';
/**
@ -70,14 +71,20 @@ export class BoundWidget {
}
}
getValueForLabel() {
return this.getValue();
}
getTextLabel(opts) {
const val = this.getValue();
if (typeof val !== 'string') return null;
const val = this.getValueForLabel();
const allowedTypes = ['string', 'number', 'boolean'];
if (!allowedTypes.includes(typeof val)) return null;
const valString = String(val).trim();
const maxLength = opts && opts.maxLength;
if (maxLength && val.length > maxLength) {
return val.substring(0, maxLength - 1) + '…';
if (maxLength && valString.length > maxLength) {
return valString.substring(0, maxLength - 1) + '…';
}
return val;
return valString;
}
focus() {
@ -156,6 +163,10 @@ export class BoundCheckboxInput extends BoundWidget {
setState(state) {
this.input.checked = state;
}
getValueForLabel() {
return this.getValue() ? gettext('Yes') : gettext('No');
}
}
export class CheckboxInput extends Widget {
@ -173,6 +184,27 @@ export class BoundRadioSelect {
this.selector = `input[name="${name}"]:checked`;
}
getValueForLabel() {
const getLabels = (input) => {
const labels = Array.from(input?.labels || [])
.map((label) => label.textContent.trim())
.filter(Boolean);
return labels.join(', ');
};
if (this.isMultiple) {
return Array.from(this.element.querySelectorAll(this.selector))
.map(getLabels)
.join(', ');
}
return getLabels(this.element.querySelector(this.selector));
}
getTextLabel() {
// This class does not extend BoundWidget, so we don't have the truncating
// logic without duplicating the code here. Skip it for now.
return this.getValueForLabel();
}
getValue() {
if (this.isMultiple) {
return Array.from(this.element.querySelectorAll(this.selector)).map(
@ -217,7 +249,7 @@ export class RadioSelect extends Widget {
}
export class BoundSelect extends BoundWidget {
getTextLabel() {
getValueForLabel() {
return Array.from(this.input.selectedOptions)
.map((option) => option.text)
.join(', ');

Wyświetl plik

@ -238,6 +238,20 @@ describe('RadioSelect', () => {
).toBeNull();
});
test('getTextLabel() returns the text of selected option', () => {
expect(boundWidget.getTextLabel()).toBe('Tea');
});
test('getTextLabel() safely handles input with no labels', () => {
// Disassociate the label from the input by removing the "for" attribute
// and moving the input outside of the label.
const label = document.querySelector('label[for="the-id_0"]');
label.removeAttribute('for');
const input = document.querySelector('input[value="tea"]');
document.body.appendChild(input);
expect(boundWidget.getTextLabel()).toBe('');
});
test('focus() focuses the first element', () => {
boundWidget.focus();
@ -299,6 +313,20 @@ describe('RadioSelect for CheckboxSelectMultiple', () => {
expect(document.querySelector('input[value="green"]').checked).toBe(true);
expect(document.querySelector('input[value="blue"]').checked).toBe(false);
});
test('getTextLabel() returns the text of selected options', () => {
expect(boundWidget.getTextLabel()).toBe('Red, Blue');
});
test('getTextLabel() safely handles input with no labels', () => {
// Disassociate the label from the input by removing the "for" attribute
// and moving the input outside of the label.
const label = document.querySelector('label[for="the-id_0"]');
label.removeAttribute('for');
const input = document.querySelector('input[value="red"]');
document.body.appendChild(input);
expect(boundWidget.getTextLabel()).toBe('Blue');
});
});
describe('CheckboxInput', () => {
@ -338,6 +366,12 @@ describe('CheckboxInput', () => {
expect(document.querySelector('input[id="id-sugar"]').checked).toBe(true);
});
test('getTextLabel() returns a human-readable value', () => {
expect(boundWidget.getTextLabel()).toBe('Yes');
boundWidget.setState(false);
expect(boundWidget.getTextLabel()).toBe('No');
});
test('focus() focuses the checkbox', () => {
boundWidget.focus();
@ -377,8 +411,9 @@ describe('Select', () => {
expect(selectedOptions[0].value).toBe('1');
});
test('getTextLabel() returns the text of selected option', () => {
test('getTextLabel() returns the truncated text of selected option', () => {
expect(boundWidget.getTextLabel()).toBe('Option 1');
expect(boundWidget.getTextLabel({ maxLength: 6 })).toBe('Optio…');
});
test('getValue() returns the current value', () => {
@ -429,8 +464,9 @@ describe('Select multiple', () => {
expect(selectedOptions[1].value).toBe('blue');
});
test('getTextLabel() returns the text of selected options', () => {
test('getTextLabel() returns the truncated text of selected options', () => {
expect(boundWidget.getTextLabel()).toBe('Red, Blue');
expect(boundWidget.getTextLabel({ maxLength: 6 })).toBe('Red, …');
});
test('getValue() returns the current values', () => {

Wyświetl plik

@ -51,8 +51,10 @@ class SettingsBlock(blocks.StructBlock):
class Meta:
icon = 'cog'
collapsed = True # This block will be initially collapsed
label_format = "Settings (Theme: {theme})" # The label when collapsed
# This block will be initially collapsed
collapsed = True
# The label when the block is collapsed
label_format = "Settings (Theme: {theme}, Available: {available})"
class PersonBlock(blocks.StructBlock):

Wyświetl plik

@ -82,6 +82,7 @@ For more details, see the documentation on [enabling the user bar](headless_user
* Add a keyboard shortcut to easily toggle the visibility of the minimap side panel (Dhruvi Patel)
* Add API for extracting preview page content (Sage Abdullah)
* Add `form_attrs` support to all StreamField blocks (Sage Abdullah)
* Improve `label_format` support for more widget types in StreamField (Sage Abdullah)
* Update project template documentation to include testing instructions and include starting test file in template (Aditya (megatrron))
* Add support for `preview_value` and `default` in `Block` meta options as callables for [dynamic previews within StreamField](configuring_block_previews) (Ziyao Yan, Sage Abdullah)
* Provide client-side access to the editing form panel structure (Matt Westcott)