Skip to content

Commit ef38db3

Browse files
committed
Allow grouping plugins visually into sections
1 parent 709767d commit ef38db3

File tree

3 files changed

+120
-4
lines changed

3 files changed

+120
-4
lines changed

content_editor/admin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class ContentEditorInline(StackedInline):
8888
button = ""
8989
icon = ""
9090
color = ""
91+
sections = 0
9192

9293
def formfield_for_dbfield(self, db_field, *args, **kwargs):
9394
"""Ensure ``region`` and ``ordering`` use a HiddenInput widget"""
@@ -171,6 +172,7 @@ def _content_editor_context(self, request, context):
171172
"prefix": iaf.formset.prefix,
172173
"button": button,
173174
"color": iaf.opts.color,
175+
"sections": iaf.opts.sections,
174176
}
175177
)
176178
regions = [

content_editor/static/content_editor/content_editor.css

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ html {
6060

6161
.order-machine-wrapper {
6262
clear: both;
63+
position: relative;
6364
}
6465

6566
.order-machine {
@@ -73,10 +74,16 @@ html {
7374
overflow: hidden;
7475
}
7576

77+
.order-machine-section {
78+
position: absolute;
79+
background: rgba(0 0 0 / 0.1);
80+
pointer-events: none;
81+
}
82+
7683
.order-machine .inline-related {
7784
border: 1px solid var(--hairline-color, #e8e8e8);
7885
border-bottom: 0;
79-
margin-top: 10px;
86+
margin-top: 15px;
8087
}
8188

8289
.order-machine .inline-related > h3 {

content_editor/static/content_editor/content_editor.js

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ django.jQuery(($) => {
8282
ContentEditor.regions.forEach((region) => {
8383
ContentEditor.regionsByKey[region.key] = region
8484
})
85+
ContentEditor.hasSections = ContentEditor.plugins.some(
86+
(plugin) => plugin.sections,
87+
)
8588

8689
// Add basic structure. There is always at least one inline group if
8790
// we even have any plugins.
@@ -262,13 +265,110 @@ django.jQuery(($) => {
262265
row[0].classList.remove("selected")
263266
})
264267
window.__fs_dragging = null
268+
269+
updateSections()
265270
}
266271
})
267272

268273
arg.find(">h3, .card-title").attr("draggable", true) // Default admin, Jazzmin
269274
arg.addClass("fs-draggable")
270275
}
271276

277+
function findInlinesInOrder(context) {
278+
const inlines = (context || orderMachine).find(
279+
`.inline-related:not(.empty-form)[data-region="${ContentEditor.currentRegion}`,
280+
)
281+
inlines.sort((a, b) => a.style.order - b.style.order)
282+
return inlines
283+
}
284+
285+
let sectionsMap = new Map()
286+
287+
function updateSections(context) {
288+
/* Bail out early if we wouldn't do nothing anyway */
289+
if (!ContentEditor.hasSections) return
290+
291+
const inlines = findInlinesInOrder(context)
292+
293+
let indent = 0
294+
let nextIndent
295+
const stack = []
296+
const wrapper = orderMachineWrapper[0]
297+
const wrapperRect = wrapper.getBoundingClientRect()
298+
299+
const newSectionsMap = new Map()
300+
301+
function closeSection(atInline) {
302+
const fromInline = stack.pop()
303+
const from = fromInline.getBoundingClientRect()
304+
const until = atInline.getBoundingClientRect()
305+
306+
let div = sectionsMap.get(fromInline)
307+
if (div) {
308+
sectionsMap.delete(fromInline)
309+
} else {
310+
div = document.createElement("div")
311+
div.classList.add("order-machine-section")
312+
wrapper.prepend(div)
313+
}
314+
315+
newSectionsMap.set(fromInline, div)
316+
div.style.top = `${from.top - wrapperRect.top - 5}px`
317+
div.style.left = `${from.left - wrapperRect.left - 5}px`
318+
div.style.right = "5px"
319+
div.style.height = `${until.top - from.top + until.height + 10}px`
320+
}
321+
322+
for (const inline of inlines) {
323+
const prefix = inline.id.replace(/-[0-9]+$/, "")
324+
inline.style.marginInlineStart = `${30 * indent}px`
325+
nextIndent = Math.max(
326+
0,
327+
indent + ContentEditor.pluginsByPrefix[prefix].sections,
328+
)
329+
330+
while (indent < nextIndent) {
331+
stack.push(inline)
332+
++indent
333+
}
334+
335+
while (indent > nextIndent) {
336+
closeSection(inline)
337+
--indent
338+
}
339+
340+
indent = nextIndent
341+
}
342+
343+
while (stack.length) {
344+
closeSection(inlines[inlines.length - 1])
345+
}
346+
347+
for (const section of sectionsMap.values()) {
348+
section.remove()
349+
}
350+
sectionsMap = newSectionsMap
351+
}
352+
353+
if (ContentEditor.hasSections) {
354+
/* From https://www.freecodecamp.org/news/javascript-debounce-example/ */
355+
function debounce(func, timeout = 300) {
356+
let timer
357+
return (...args) => {
358+
clearTimeout(timer)
359+
timer = setTimeout(() => {
360+
func.apply(this, args)
361+
}, timeout)
362+
}
363+
}
364+
const debouncedIndentInlines = debounce(updateSections, 10)
365+
366+
const resizeObserver = new ResizeObserver((entries) => {
367+
debouncedIndentInlines()
368+
})
369+
resizeObserver.observe(orderMachineWrapper[0])
370+
}
371+
272372
function reorderInlines(context) {
273373
const inlines = (context || orderMachine).find(".inline-related")
274374
inlines.not(".empty-form").each(function () {
@@ -512,12 +612,13 @@ django.jQuery(($) => {
512612
e.target.classList.add("selected")
513613

514614
const pos = e.target.getBoundingClientRect()
615+
const wrapperRect = orderMachineWrapper[0].getBoundingClientRect()
515616
const buttons = qs(".plugin-buttons")
516-
buttons.style.left = `${pos.left + window.scrollX + 30}px`
617+
buttons.style.left = `${pos.left - wrapperRect.left + 30}px`
517618

518619
const y =
519-
pos.top +
520-
window.scrollY +
620+
pos.top -
621+
wrapperRect.top +
521622
(e.target.classList.contains("last")
522623
? 30 - buttons.getBoundingClientRect().height
523624
: 0)
@@ -554,6 +655,8 @@ django.jQuery(($) => {
554655
$(document).trigger("content-editor:activate", [$row])
555656

556657
$row.find("input, select, textarea").first().focus()
658+
659+
updateSections()
557660
}
558661

559662
function handleFormsetRemoved(prefix) {
@@ -580,6 +683,8 @@ django.jQuery(($) => {
580683
.each(function () {
581684
$(document).trigger("content-editor:activate", [$(this)])
582685
})
686+
687+
updateSections()
583688
}, 0)
584689
}
585690

@@ -623,6 +728,8 @@ django.jQuery(($) => {
623728

624729
// Make sure only allowed plugins are in select
625730
hideNotAllowedPluginButtons()
731+
732+
updateSections()
626733
})
627734

628735
const collapseAllInput = $(".collapse-items input")

0 commit comments

Comments
 (0)