summaryrefslogtreecommitdiff
path: root/django/contrib/admin/static
diff options
context:
space:
mode:
authorseanhelvey <sean.helvey@gmail.com>2024-12-13 11:56:53 -0800
committerJacob Walls <jacobtylerwalls@gmail.com>2026-01-22 21:12:23 -0500
commitb1ffa9a9d78b0c2c5ad6ed5a1d84e380d5cfd010 (patch)
tree0fcfd9b90c788e21e58cb9249f4119062b8bfc4e /django/contrib/admin/static
parent3851601b2e080df34fb9227fe5d2fd43af604263 (diff)
Fixed #13883 -- Rendered named choice groups with <optgroup> in FilteredSelectMultiple.
This patch adds support for <optgroup>s in FilteredSelectMultiple widgets. When a popup returns a new object, if the source field contains optgroup choices, the optgroup is now also included in the response data. Additionally, this adds error handling for invalid source_model parameters to prevent crashes and display user-friendly error messages instead. Co-authored-by: Michael McLarnon <mmclar@gmail.com>
Diffstat (limited to 'django/contrib/admin/static')
-rw-r--r--django/contrib/admin/static/admin/js/SelectBox.js51
-rw-r--r--django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js15
-rw-r--r--django/contrib/admin/static/admin/js/popup_response.js2
3 files changed, 53 insertions, 15 deletions
diff --git a/django/contrib/admin/static/admin/js/SelectBox.js b/django/contrib/admin/static/admin/js/SelectBox.js
index 3db4ec7fa6..17c182c53f 100644
--- a/django/contrib/admin/static/admin/js/SelectBox.js
+++ b/django/contrib/admin/static/admin/js/SelectBox.js
@@ -1,5 +1,6 @@
'use strict';
{
+ const getOptionGroupName = (option) => option.parentElement.label;
const SelectBox = {
cache: {},
init: function(id) {
@@ -7,7 +8,12 @@
SelectBox.cache[id] = [];
const cache = SelectBox.cache[id];
for (const node of box.options) {
- cache.push({value: node.value, text: node.text, displayed: 1});
+ const group = getOptionGroupName(node);
+ cache.push({group, value: node.value, text: node.text, displayed: 1});
+ }
+ // Only sort if there are any groups (to preserve existing behavior for non-grouped selects)
+ if (cache.some(item => item.group)) {
+ SelectBox.sort(id);
}
},
redisplay: function(id) {
@@ -15,12 +21,25 @@
const box = document.getElementById(id);
const scroll_value_from_top = box.scrollTop;
box.innerHTML = '';
- for (const node of SelectBox.cache[id]) {
- if (node.displayed) {
- const new_option = new Option(node.text, node.value, false, false);
+ let node = box;
+ let currentOptgroup = null;
+ for (const option of SelectBox.cache[id]) {
+ if (option.displayed) {
+ // Create a new optgroup when the group changes
+ if (option.group && option.group !== currentOptgroup) {
+ currentOptgroup = option.group;
+ node = document.createElement('optgroup');
+ node.setAttribute('label', option.group);
+ box.appendChild(node);
+ } else if (!option.group && currentOptgroup !== null) {
+ // Back to ungrouped options
+ currentOptgroup = null;
+ node = box;
+ }
+ const new_option = new Option(option.text, option.value, false, false);
// Shows a tooltip when hovering over the option
- new_option.title = node.text;
- box.appendChild(new_option);
+ new_option.title = option.text;
+ node.appendChild(new_option);
}
}
box.scrollTop = scroll_value_from_top;
@@ -57,7 +76,7 @@
cache.splice(delete_index, 1);
},
add_to_cache: function(id, option) {
- SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1});
+ SelectBox.cache[id].push({group: option.group, value: option.value, text: option.text, displayed: 1});
},
cache_contains: function(id, value) {
// Check if an item is contained in the cache
@@ -73,10 +92,15 @@
for (const option of from_box.options) {
const option_value = option.value;
if (option.selected && SelectBox.cache_contains(from, option_value)) {
- SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1});
+ const group = getOptionGroupName(option);
+ SelectBox.add_to_cache(to, {group, value: option_value, text: option.text, displayed: 1});
SelectBox.delete_from_cache(from, option_value);
}
}
+ // Only sort if there are any groups (to preserve existing behavior for non-grouped selects)
+ if (SelectBox.cache[to].some(item => item.group)) {
+ SelectBox.sort(to);
+ }
SelectBox.redisplay(from);
SelectBox.redisplay(to);
},
@@ -85,17 +109,22 @@
for (const option of from_box.options) {
const option_value = option.value;
if (SelectBox.cache_contains(from, option_value)) {
- SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1});
+ const group = getOptionGroupName(option);
+ SelectBox.add_to_cache(to, {group, value: option_value, text: option.text, displayed: 1});
SelectBox.delete_from_cache(from, option_value);
}
}
+ // Only sort if there are any groups (to preserve existing behavior for non-grouped selects)
+ if (SelectBox.cache[to].some(item => item.group)) {
+ SelectBox.sort(to);
+ }
SelectBox.redisplay(from);
SelectBox.redisplay(to);
},
sort: function(id) {
SelectBox.cache[id].sort(function(a, b) {
- a = a.text.toLowerCase();
- b = b.text.toLowerCase();
+ a = (a.group && a.group.toLowerCase() || '') + a.text.toLowerCase();
+ b = (b.group && b.group.toLowerCase() || '') + b.text.toLowerCase();
if (a > b) {
return 1;
}
diff --git a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
index 1fc03c6232..f366f30f7e 100644
--- a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
+++ b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
@@ -113,6 +113,10 @@
// Update SelectBox cache for related fields.
if (window.SelectBox !== undefined && !SelectBox.cache[currentSelect.id]) {
SelectBox.add_to_cache(select.id, option);
+ // Sort if there are any groups present
+ if (SelectBox.cache[select.id].some(item => item.group)) {
+ SelectBox.sort(select.id);
+ }
SelectBox.redisplay(select.id);
}
return;
@@ -123,7 +127,7 @@
});
}
- function dismissAddRelatedObjectPopup(win, newId, newRepr) {
+ function dismissAddRelatedObjectPopup(win, newId, newRepr, optgroup) {
const name = removePopupIndex(win.name);
const elem = document.getElementById(name);
if (elem) {
@@ -143,8 +147,13 @@
} else {
const toId = name + "_to";
const toElem = document.getElementById(toId);
- const o = new Option(newRepr, newId);
- SelectBox.add_to_cache(toId, o);
+ const newOption = new Option(newRepr, newId);
+ newOption.group = optgroup;
+ SelectBox.add_to_cache(toId, newOption);
+ // Sort if there are any groups present
+ if (SelectBox.cache[toId].some(item => item.group)) {
+ SelectBox.sort(toId);
+ }
SelectBox.redisplay(toId);
if (toElem && toElem.nodeName.toUpperCase() === 'SELECT') {
const skipIds = [name + "_from"];
diff --git a/django/contrib/admin/static/admin/js/popup_response.js b/django/contrib/admin/static/admin/js/popup_response.js
index fecf0f4798..bdd93a6eb5 100644
--- a/django/contrib/admin/static/admin/js/popup_response.js
+++ b/django/contrib/admin/static/admin/js/popup_response.js
@@ -9,7 +9,7 @@
opener.dismissDeleteRelatedObjectPopup(window, initData.value);
break;
default:
- opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj);
+ opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj, initData.optgroup);
break;
}
}