Skip to content

Commit c9a294d

Browse files
committed
Move tabbed fieldsets to the refined model admin base
1 parent 5369eef commit c9a294d

File tree

5 files changed

+168
-87
lines changed

5 files changed

+168
-87
lines changed

content_editor/admin.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,13 @@ def auto_icon_colors(content_editor):
124124

125125
class RefinedModelAdmin(ModelAdmin):
126126
class Media:
127-
js = ["content_editor/save_shortcut.js"]
127+
css = {
128+
"all": ["content_editor/tabbed_fieldsets.css"],
129+
}
130+
js = [
131+
"content_editor/save_shortcut.js",
132+
"content_editor/tabbed_fieldsets.js",
133+
]
128134

129135

130136
class ContentEditor(RefinedModelAdmin):
@@ -220,7 +226,6 @@ def _content_editor_media(self, request, context):
220226
},
221227
js=[
222228
"admin/js/jquery.init.js",
223-
"content_editor/tabbed_fieldsets.js",
224229
JSON(
225230
self._content_editor_context(request, context),
226231
id="content-editor-context",

content_editor/static/content_editor/content_editor.css

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,6 @@ html {
33
overflow-x: hidden;
44
}
55

6-
.clearfix::after {
7-
content: "";
8-
display: table;
9-
clear: both;
10-
}
11-
126
.tabs.regions {
137
display: flex;
148
}
@@ -51,13 +45,6 @@ html {
5145
background: var(--darkened-bg, #f8f8f8);
5246
}
5347

54-
@media (max-width: 767px) {
55-
.tabbed-modules .form-row {
56-
padding-left: 10px;
57-
padding-right: 10px;
58-
}
59-
}
60-
6148
.order-machine-wrapper {
6249
clear: both;
6350
position: relative;
@@ -265,36 +252,6 @@ h3[draggable] {
265252
padding: 0px 1px;
266253
}
267254

268-
/* tabbed_fieldsets.js support */
269-
#tabbed .tabs {
270-
border-bottom: 1px solid var(--hairline-color, #e8e8e8);
271-
}
272-
273-
#tabbed .modules {
274-
margin-bottom: 30px;
275-
}
276-
277-
#tabbed .module {
278-
border: 1px solid var(--hairline-color, #e8e8e8);
279-
border-top: none;
280-
margin-bottom: 0;
281-
}
282-
283-
#tabbed .form-row:last-child {
284-
border: none;
285-
}
286-
287-
.content-editor-invisible {
288-
/* We can't simply use display: none. Some admin widgets need to know
289-
* their dimensions, so we can't have that -- use an alternative way
290-
* to hide the modules. */
291-
visibility: hidden !important;
292-
height: 0 !important;
293-
border: none !important;
294-
padding: 0 !important;
295-
margin: 0 !important;
296-
}
297-
298255
.content-editor-hide {
299256
display: none !important;
300257
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/* Tabbed fieldsets CSS - minimal styles for RefinedModelAdmin */
2+
3+
.content-editor-invisible {
4+
/* We can't simply use display: none. Some admin widgets need to know
5+
* their dimensions, so we can't have that -- use an alternative way
6+
* to hide the modules. */
7+
visibility: hidden !important;
8+
height: 0 !important;
9+
border: none !important;
10+
padding: 0 !important;
11+
margin: 0 !important;
12+
}
13+
14+
/* Basic tabbed interface styles */
15+
#tabbed .tabs {
16+
border-bottom: 1px solid #e8e8e8;
17+
}
18+
19+
#tabbed .modules {
20+
margin-bottom: 30px;
21+
}
22+
23+
.tabs > .tab {
24+
text-transform: none;
25+
letter-spacing: 0;
26+
float: left;
27+
padding: 10px 15px;
28+
margin: 0 4px 0 0;
29+
cursor: pointer;
30+
border: 1px solid #e8e8e8;
31+
border-bottom: none;
32+
background: #f8f8f8;
33+
border-radius: 4px 4px 0 0;
34+
}
35+
36+
.tabs > .tab:hover {
37+
background: #f0f0f0;
38+
}
39+
40+
.tabs > .tab.active {
41+
background: white;
42+
border-color: #e8e8e8;
43+
border-bottom: 1px solid white;
44+
margin-bottom: -1px;
45+
}
46+
47+
.tabs > .has-error {
48+
border-color: #ba2121;
49+
color: #ba2121;
50+
background: #f8f8f8;
51+
}
52+
53+
.clearfix::after {
54+
content: "";
55+
display: table;
56+
clear: both;
57+
}
58+
59+
@media (max-width: 767px) {
60+
.tabbed-modules .form-row {
61+
padding-left: 10px;
62+
padding-right: 10px;
63+
}
64+
}
Lines changed: 80 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,102 @@
1-
/* global django */
2-
django.jQuery(($) => {
3-
const tabbed = $(".tabbed")
4-
if (tabbed.length >= 1) {
5-
let anchor = tabbed.eq(0)
6-
/* Break out of the .inline-related containment, avoids ugly h3's */
7-
if (anchor.parents(".inline-related").length) {
8-
anchor = anchor.parents(".inline-related")
1+
/* Tabbed fieldsets without jQuery dependency */
2+
;(() => {
3+
function initializeTabbedFieldsets() {
4+
const tabbedElements = document.querySelectorAll(".tabbed")
5+
if (tabbedElements.length === 0) return
6+
7+
let anchor = tabbedElements[0]
8+
// Break out of the .inline-related containment, avoids ugly h3's
9+
const inlineRelated = anchor.closest(".inline-related")
10+
if (inlineRelated) {
11+
anchor = inlineRelated
912
}
10-
anchor.before(
11-
'<div id="tabbed" class="clearfix">' +
12-
'<div class="tabs clearfix"></div>' +
13-
'<div class="modules tabbed-modules"></div>' +
14-
"</div>",
15-
)
1613

17-
const $tabs = $("#tabbed > .tabs")
18-
const $modules = $("#tabbed > .modules")
14+
// Create the tabbed container
15+
const tabbedContainer = document.createElement("div")
16+
tabbedContainer.id = "tabbed"
17+
tabbedContainer.className = "clearfix"
18+
tabbedContainer.innerHTML =
19+
'<div class="tabs clearfix"></div>' +
20+
'<div class="modules tabbed-modules"></div>'
21+
22+
anchor.parentNode.insertBefore(tabbedContainer, anchor)
23+
24+
const tabsContainer = document
25+
.getElementById("tabbed")
26+
.querySelector(".tabs")
27+
const modulesContainer = document
28+
.getElementById("tabbed")
29+
.querySelector(".modules")
1930
let errorIndex = -1
2031
let uncollapseIndex = -1
2132

22-
tabbed.each(function createTabs(index) {
23-
const $old = $(this)
24-
const $title = $old.children("h2")
33+
// Process each tabbed element
34+
tabbedElements.forEach((element, index) => {
35+
const title = element.querySelector("h2")
2536

26-
if ($old.find(".errorlist").length) {
27-
$title.addClass("has-error")
37+
if (element.querySelector(".errorlist")) {
38+
title.classList.add("has-error")
2839
errorIndex = errorIndex < 0 ? index : errorIndex
2940
}
30-
if ($old.is(".uncollapse")) {
41+
42+
if (element.classList.contains("uncollapse")) {
3143
uncollapseIndex = uncollapseIndex < 0 ? index : uncollapseIndex
3244
}
3345

34-
$title.attr("data-index", index)
35-
$title.addClass("tab")
36-
$tabs.append($title)
37-
38-
$old.addClass("content-editor-invisible")
46+
title.setAttribute("data-index", index)
47+
title.classList.add("tab")
48+
tabsContainer.appendChild(title)
3949

40-
$modules.append($old)
50+
element.classList.add("content-editor-invisible")
51+
modulesContainer.appendChild(element)
4152
})
4253

43-
$tabs.on("click", "[data-index]", function () {
44-
const $tab = $(this)
45-
if ($tab.hasClass("active")) {
46-
$tab.removeClass("active")
47-
$modules.children().addClass("content-editor-invisible")
54+
// Add click handler for tabs
55+
tabsContainer.addEventListener("click", (event) => {
56+
const target = event.target.closest("[data-index]")
57+
if (!target) return
58+
59+
const index = Number.parseInt(target.getAttribute("data-index"), 10)
60+
const isActive = target.classList.contains("active")
61+
62+
if (isActive) {
63+
target.classList.remove("active")
64+
modulesContainer.querySelectorAll(".tabbed").forEach((module) => {
65+
module.classList.add("content-editor-invisible")
66+
})
4867
} else {
49-
$tabs.find(".active").removeClass("active")
50-
$tab.addClass("active")
51-
$modules
52-
.children()
53-
.addClass("content-editor-invisible")
54-
.eq($tab.data("index"))
55-
.removeClass("content-editor-invisible")
68+
// Remove active from all tabs
69+
tabsContainer.querySelectorAll(".active").forEach((tab) => {
70+
tab.classList.remove("active")
71+
})
72+
target.classList.add("active")
73+
74+
// Hide all modules and show the selected one
75+
const modules = modulesContainer.querySelectorAll(".tabbed")
76+
modules.forEach((module, moduleIndex) => {
77+
if (moduleIndex === index) {
78+
module.classList.remove("content-editor-invisible")
79+
} else {
80+
module.classList.add("content-editor-invisible")
81+
}
82+
})
5683
}
5784
})
5885

86+
// Auto-open tab with errors or marked for uncollapse
5987
if (errorIndex >= 0 || uncollapseIndex >= 0) {
6088
const index = errorIndex >= 0 ? errorIndex : uncollapseIndex
61-
$tabs.find(`[data-index=${index}]`).click()
89+
const targetTab = tabsContainer.querySelector(`[data-index="${index}"]`)
90+
if (targetTab) {
91+
targetTab.click()
92+
}
6293
}
6394
}
64-
})
95+
96+
// Initialize when DOM is ready
97+
if (document.readyState === "loading") {
98+
document.addEventListener("DOMContentLoaded", initializeTabbedFieldsets)
99+
} else {
100+
initializeTabbedFieldsets()
101+
}
102+
})()

docs/admin-classes.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ Currently, ``RefinedModelAdmin`` includes:
9494
- **Save shortcuts**: Keyboard shortcuts (Ctrl+S / Cmd+S) for quickly saving forms
9595
- **Deletion safety check**: Shows a confirmation dialog when attempting to delete
9696
the whole object instead of saving changes (including marked inline deletions)
97+
- **Tabbed fieldsets**: Support for tabbed fieldsets using the ``tabbed`` CSS class
9798

9899
To use ``RefinedModelAdmin`` for your own admin classes:
99100

@@ -109,3 +110,19 @@ To use ``RefinedModelAdmin`` for your own admin classes:
109110
This gives you the save shortcut functionality without the full content editor
110111
interface, making it useful for any Django model admin where you want these
111112
convenience features.
113+
114+
To use tabbed fieldsets, add the ``tabbed`` CSS class to your fieldsets:
115+
116+
.. code-block:: python
117+
118+
class MyModelAdmin(RefinedModelAdmin):
119+
fieldsets = [
120+
('Basic Information', {
121+
'fields': ['title', 'description'],
122+
'classes': ['tabbed'],
123+
}),
124+
('Advanced Settings', {
125+
'fields': ['status', 'priority'],
126+
'classes': ['tabbed'],
127+
}),
128+
]

0 commit comments

Comments
 (0)