Skip to content

Commit

Permalink
Cut and paste based interface for moving pages
Browse files Browse the repository at this point in the history
  • Loading branch information
matthiask committed Jun 1, 2024
1 parent 9971261 commit 5f83190
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 74 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Next version
scenarios to allow the root middleware to run even after views return a 404
response.
- Switched from ESLint to biome.
- Changed the move node interface to a cut-paste based interface which works
directly in the admin changelist.


4.6 (2024-02-26)
Expand Down
35 changes: 28 additions & 7 deletions feincms3/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.contrib.admin.utils import unquote
from django.core.exceptions import PermissionDenied
from django.db import router, transaction
from django.http import HttpResponse
from django.shortcuts import redirect
from django.urls import re_path, reverse
from django.utils.html import format_html, mark_safe
Expand Down Expand Up @@ -131,6 +132,25 @@ def move_column(self, instance):
Show a ``move`` link which leads to a separate page where the move
destination may be selected.
"""
return format_html(
"""\
<button class="move-cut button" type="button" data-pk="{}" data-title="{}">{}</button>
<select class="move-paste button" data-pk="{}">
<option value="">{}</option>
<!-- <option value="left">before</option> -->
<option value="first">{}</option>
<option value="right">{}</option>
</select>
""",
instance.pk,
_("Move '{}' to a new location").format(instance),
_("cut"),
instance.pk,
_("paste"),
_("as first child"),
_("after"),
)

opts = self.model._meta
return format_html(
'<a href="{}">{}</a>',
Expand All @@ -141,7 +161,7 @@ def move_column(self, instance):
_("move"),
)

move_column.short_description = ""
move_column.short_description = _("move")

def get_urls(self):
"""
Expand All @@ -163,9 +183,13 @@ def get_urls(self):
] + super().get_urls()

def move_view(self, request, obj):
return self.action_form_view(
request, obj, form_class=MoveForm, title=_("Move %s") % obj
)
kw = {"request": request, "obj": obj, "modeladmin": self}
form = MoveForm(request.POST, **kw)
if form.is_valid():
form.process()
else:
messages.error(request, str(form.errors))
return HttpResponse("")

def clone_view(self, request, obj):
return self.action_form_view(
Expand Down Expand Up @@ -225,9 +249,6 @@ class MoveForm(forms.Form):
Requires the node to be moved as ``obj`` keyword argument.
"""

class Media:
css = {"screen": ["feincms3/move-form.css"]}

def __init__(self, *args, **kwargs):
self.instance = kwargs.pop("obj")
self.modeladmin = kwargs.pop("modeladmin")
Expand Down
25 changes: 25 additions & 0 deletions feincms3/static/feincms3/box-drawing.css
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,28 @@
.collapse-hide {
display: none !important;
}

.move-status {
background: var(--selected-row);
padding: 4px 10px;
cursor: pointer;
}

.moving .move-selected .move-paste {
display: none;
}

#changelist tbody tr.move-selected {
background: var(--selected-row);
}

.move-paste {
appearance: none;
height: auto;
padding-right: 0;
display: none;
}

.moving .move-paste {
display: revert;
}
90 changes: 90 additions & 0 deletions feincms3/static/feincms3/box-drawing.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,93 @@ document.addEventListener("DOMContentLoaded", () => {
)
initiallyCollapse(context.initiallyCollapseDepth)
})

document.addEventListener("DOMContentLoaded", () => {
let statusElement
const showMoving = (moving) => {
if (!statusElement) {
statusElement = document.createElement("div")
statusElement.className = "move-status"
document.querySelector("#result_list").before(statusElement)
}

for (const el of document.querySelectorAll(".move-selected"))
el.classList.remove("move-selected")

if (moving) {
statusElement.textContent = `${moving.title} (click to cancel)`
statusElement.style.display = "block"
document.body.classList.add("moving")

document
.querySelector(`[data-pk="${moving.pk}"]`)
.closest("tr")
.classList.add("move-selected")
} else {
statusElement.style.display = "none"
document.body.classList.remove("moving")
}
}

document.addEventListener("click", (e) => {
const btn = e.target.closest(".move-cut")
if (btn) {
setMoving(
_moving?.pk === btn.dataset.pk
? null
: { pk: btn.dataset.pk, title: btn.dataset.title },
)
}

const el = e.target.closest(".move-status")
if (el) {
setMoving(null)
}
})

document.addEventListener("change", (e) => {
const select = e.target.closest(".move-paste")
if (select?.value && _moving) {
const csrf = document.querySelector(
"input[name=csrfmiddlewaretoken]",
).value
const body = new FormData()
body.append("csrfmiddlewaretoken", csrf)
body.append("new_location", `${select.dataset.pk}:${select.value}`)
fetch(`${_moving.pk}/move/`, {
credentials: "include",
method: "POST",
body,
}).then(() => {
setMoving(null)
window.location.reload()
})

// console.debug(JSON.stringify({ _moving, where: `${select.dataset.pk}:${select.value}` }))
}
})

document.body.addEventListener("keyup", (e) => {
if (e.key === "Escape") setMoving(null)
})

const _key = `f3moving:${location.pathname}`
let _moving
try {
_moving = JSON.parse(sessionStorage.getItem(_key))
} catch (e) {
console.error(e)
}

const setMoving = (moving) => {
_moving = moving
if (_moving) {
sessionStorage.setItem(_key, JSON.stringify(_moving))
} else {
sessionStorage.removeItem(_key)
}
showMoving(_moving)
}

showMoving(_moving)
})
67 changes: 0 additions & 67 deletions feincms3/static/feincms3/move-form.css

This file was deleted.

0 comments on commit 5f83190

Please sign in to comment.