Closes #13283: Add context to dropdown options (#15104)

* Initial work on #13283

* Enable passing TomSelect HTML template attibutes on DynamicModelChoiceField

* Merge disabled_indicator into option_attrs

* Add support for annotating a numeric count on dropdown options

* Annotate parent object on relevant fields

* Improve rendering of color options

* Improve rendering of color options

* Rename option_attrs to context

* Expose option context on ObjectVar for custom scripts

* Document dropdown context variables
This commit is contained in:
Jeremy Stretch
2024-02-13 16:31:17 -05:00
committed by GitHub
parent f41105d5e3
commit 20824ceb25
13 changed files with 176 additions and 34 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

@@ -31,6 +31,15 @@ export class DynamicTomSelect extends TomSelect {
// Glean the REST API endpoint URL from the <select> element
this.api_url = this.input.getAttribute('data-url') as string;
// Override any field names set as widget attributes
this.valueField = this.input.getAttribute('ts-value-field') || this.settings.valueField;
this.labelField = this.input.getAttribute('ts-label-field') || this.settings.labelField;
this.disabledField = this.input.getAttribute('ts-disabled-field') || this.settings.disabledField;
this.descriptionField = this.input.getAttribute('ts-description-field') || 'description';
this.depthField = this.input.getAttribute('ts-depth-field') || '_depth';
this.parentField = this.input.getAttribute('ts-parent-field') || null;
this.countField = this.input.getAttribute('ts-count-field') || null;
// Set the null option (if any)
const nullOption = this.input.getAttribute('data-null-option');
if (nullOption) {
@@ -82,10 +91,20 @@ export class DynamicTomSelect extends TomSelect {
// Make the API request
fetch(url)
.then(response => response.json())
.then(json => {
self.loadCallback(json.results, []);
.then(apiData => {
const results: Dict[] = apiData.results;
let options: Dict[] = []
for (let result of results) {
const option = self.getOptionFromData(result);
options.push(option);
}
return options;
})
// Pass the options to the callback function
.then(options => {
self.loadCallback(options, []);
}).catch(()=>{
self.loadCallback([], []);
self.loadCallback([], []);
});
}
@@ -126,6 +145,27 @@ export class DynamicTomSelect extends TomSelect {
return queryString.stringifyUrl({ url, query });
}
// Compile TomOption data from an API result
getOptionFromData(data: Dict) {
let option: Dict = {
id: data[this.valueField],
display: data[this.labelField],
depth: data[this.depthField] || null,
description: data[this.descriptionField] || null,
};
if (data[this.parentField]) {
let parent: Dict = data[this.parentField] as Dict;
option['parent'] = parent[this.labelField];
}
if (data[this.countField]) {
option['count'] = data[this.countField];
}
if (data[this.disabledField]) {
option['disabled'] = data[this.disabledField];
}
return option
}
/**
* Transitional methods
*/

View File

@@ -10,12 +10,34 @@ const MAX_OPTIONS = 100;
// Render the HTML for a dropdown option
function renderOption(data: TomOption, escape: typeof escape_html) {
// If the option has a `_depth` property, indent its label
if (typeof data._depth === 'number' && data._depth > 0) {
return `<div>${'─'.repeat(data._depth)} ${escape(data[LABEL_FIELD])}</div>`;
let html = '<div>';
// If the option has a `depth` property, indent its label
if (typeof data.depth === 'number' && data.depth > 0) {
html = `${html}${'─'.repeat(data.depth)} `;
}
return `<div>${escape(data[LABEL_FIELD])}</div>`;
html = `${html}${escape(data[LABEL_FIELD])}`;
if (data['parent']) {
html = `${html} <span class="text-secondary">${escape(data['parent'])}</span>`;
}
if (data['count']) {
html = `${html} <span class="badge">${escape(data['count'])}</span>`;
}
if (data['description']) {
html = `${html}<br /><small class="text-secondary">${escape(data['description'])}</small>`;
}
html = `${html}</div>`;
return html;
}
// Render the HTML for a selected item
function renderItem(data: TomOption, escape: typeof escape_html) {
if (data['parent']) {
return `<div>${escape(data['parent'])} > ${escape(data[LABEL_FIELD])}</div>`;
}
return `<div>${escape(data[LABEL_FIELD])}<div>`;
}
// Initialize <select> elements which are populated via a REST API call
@@ -30,16 +52,13 @@ export function initDynamicSelects(): void {
// Disable local search (search is performed on the backend)
searchField: [],
// Reference the disabled-indicator attr on the <select> element to determine
// the name of the attribute which indicates whether an option should be disabled
disabledField: select.getAttribute('disabled-indicator') || undefined,
// Load options from API immediately on focus
preload: 'focus',
// Define custom rendering functions
render: {
option: renderOption,
item: renderItem,
},
// By default, load() will be called only if query.length > 0

View File

@@ -17,13 +17,18 @@ export function initStaticSelects(): void {
// Initialize color selection fields
export function initColorSelects(): void {
function renderColor(item: TomOption, escape: typeof escape_html) {
return `<div><span class="dropdown-item-indicator color-label" style="background-color: #${escape(
item.value,
)}"></span> ${escape(item.text)}</div>`;
}
for (const select of getElements<HTMLSelectElement>('select.color-select')) {
new TomSelect(select, {
...config,
render: {
option: function (item: TomOption, escape: typeof escape_html) {
return `<div style="background-color: #${escape(item.value)}">${escape(item.text)}</div>`;
},
option: renderColor,
item: renderColor,
},
});
}