add topic sorting & js to support it
This commit is contained in:
@@ -2,6 +2,7 @@ from flask import Blueprint, abort, redirect, url_for, request, render_template,
|
||||
from ..constants import InfoboxKind, PermissionLevel, MOTD_BANNED_TAGS
|
||||
from ..auth import is_logged_in, get_active_user, csrf_verified
|
||||
from ..models import Topics, Threads, Users, MOTD
|
||||
from ..db import db
|
||||
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION
|
||||
from slugify import slugify
|
||||
from functools import wraps
|
||||
@@ -26,7 +27,8 @@ def admin_only(view_func):
|
||||
@bp.get('/')
|
||||
def index():
|
||||
motd = MOTD.get_all()[0] if MOTD.has_motd() else None
|
||||
return render_template('mod/panel.html', MOTD_BANNED_TAGS=MOTD_BANNED_TAGS, motd=motd)
|
||||
topics = Topics.get_list()
|
||||
return render_template('mod/panel.html', MOTD_BANNED_TAGS=MOTD_BANNED_TAGS, motd=motd, topics=topics)
|
||||
|
||||
@bp.post('/motd/set/')
|
||||
def set_motd():
|
||||
@@ -143,6 +145,15 @@ def sticky_thread(thread_id):
|
||||
thread.update({'is_stickied': request.form.get('sticky')})
|
||||
return redirect(url_for('threads.thread_by_id', thread_id=thread.id))
|
||||
|
||||
@bp.post('/threads/sort/')
|
||||
def sort_topics():
|
||||
topics_list = request.form.getlist('topics[]')
|
||||
with db.transaction():
|
||||
for new_order, topic_id in enumerate(topics_list):
|
||||
db.execute('UPDATE topics SET sort_order = ? WHERE id = ?', new_order, topic_id)
|
||||
|
||||
return redirect(url_for('.index', _anchor='sort-topics'))
|
||||
|
||||
@bp.post('/users/<int:user_id>/make-guest/')
|
||||
@csrf_verified
|
||||
def make_user_guest(user_id):
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
{%- endwith -%}
|
||||
{%- block content -%}{%- endblock -%}
|
||||
{%- include 'common/footer.html' -%}
|
||||
<script src="{{'/static/js/ui.js' | cachebust}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -25,3 +25,7 @@
|
||||
{%- macro icn_stickied(width=16) -%}
|
||||
<img src="/static/icons/sticky.svg" title="Stickied" alt="paper held by pushpin" style="width: {{width}}px;">
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro icn_dragger(width=24) -%}
|
||||
<img src="/static/icons/dragger.svg" style="width: {{width}}px;">
|
||||
{%- endmacro -%}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{%- from 'common/icons.html' import icn_info, icn_warn, icn_error, icn_bookmark, icn_megaphone -%}
|
||||
{%- from 'common/icons.html' import icn_info, icn_warn, icn_error, icn_bookmark, icn_megaphone, icn_dragger -%}
|
||||
|
||||
{% macro dict_to_attr(attrs) -%}
|
||||
{%- for key, value in attrs.items() if value is not none -%}{{' '}}{{key}}="{{value}}"{%- endfor -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro timestamp(unix_ts) -%}
|
||||
<time datetime="{{ unix_ts | iso8601 }}">{{ unix_ts | ts_datetime('%Y-%m-%d %H:%M')}} <abbr title="Server Time">ST</abbr></time>
|
||||
@@ -253,3 +257,18 @@
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro sortable_list(attr=none) -%}
|
||||
<ol class="sortable-list plank even no-shadow minimal tertiary-bg" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
|
||||
{%- if caller -%}
|
||||
{{ caller() }}
|
||||
{%- endif -%}
|
||||
</ol>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro sortable_list_item(key, immovable=false, attr=none) -%}
|
||||
<li class="sortable-item{{ ' immovable' if immovable else '' }} plank even no-shadow {{'tertiary-bg' if immovable else ''}}" data-sortable-list-key="{{key}}" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
|
||||
<span class="dragger plank minimal even no-shadow tertiary-bg" draggable="{{ 'true' if not immovable else 'false' }}">{{ icn_dragger() }}</span>
|
||||
<div class="sortable-item-inner">{{ caller() }}</div>
|
||||
</li>
|
||||
{%- endmacro %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{%- from 'common/macros.html' import subheader, babycode_editor_component -%}
|
||||
{%- from 'common/macros.html' import subheader, babycode_editor_component, sortable_list, sortable_list_item -%}
|
||||
{%- extends 'base.html' -%}
|
||||
{%- block title -%}settings{%- endblock -%}
|
||||
{%- block content -%}
|
||||
@@ -19,5 +19,18 @@
|
||||
</fieldset>
|
||||
<fieldset class="plank" id="sort-topics">
|
||||
<legend>Sort topics</legend>
|
||||
<p>Drag topics around to reorder them. Press "Save order" when done.</p>
|
||||
<form method="POST" action="{{url_for('mod.sort_topics')}}">
|
||||
<input type="submit" value="Save order">
|
||||
{%- call() sortable_list() -%}
|
||||
{%- for topic in topics -%}
|
||||
{%- call() sortable_list_item(key="topics") -%}
|
||||
<h2 class="info">{{ topic.name }}</h2>
|
||||
<div>{{topic.description}}</div>
|
||||
<input type="hidden" name="topics[]" value="{{ topic.id }}">
|
||||
{%- endcall -%}
|
||||
{%- endfor -%}
|
||||
{%- endcall -%}
|
||||
</form>
|
||||
</fieldset>
|
||||
{%- endblock -%}
|
||||
|
||||
@@ -859,6 +859,53 @@ a.mention {
|
||||
}
|
||||
}
|
||||
|
||||
ol.sortable-list {
|
||||
display: flex;
|
||||
gap: var(--base-padding);
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
gap: var(--big-padding);
|
||||
}
|
||||
|
||||
li.immovable .dragger {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.plank.dragger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/*background-color: var(--bg-color-tertiary);*/
|
||||
padding: var(--base-padding);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.sortable-item-inner {
|
||||
display: flex;
|
||||
gap: var(--base-padding);
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
|
||||
& > * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&:not(.row) > * {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
margin-left: 0;
|
||||
|
||||
46
data/static/icons/dragger.svg
Normal file
46
data/static/icons/dragger.svg
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#000000;stroke:none;stroke-width:0.999996;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect1"
|
||||
width="16"
|
||||
height="2"
|
||||
x="4"
|
||||
y="8" />
|
||||
<rect
|
||||
style="fill:#000000;stroke:none;stroke-width:0.999996;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect2"
|
||||
width="16"
|
||||
height="2"
|
||||
x="4"
|
||||
y="11" />
|
||||
<rect
|
||||
style="fill:#000000;stroke:none;stroke-width:0.999996;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect3"
|
||||
width="16"
|
||||
height="2"
|
||||
x="4"
|
||||
y="14" />
|
||||
<path
|
||||
style="fill:#000000;stroke:none;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 6,6 6,-6 6,6 H 16 L 12,2 8,6 Z"
|
||||
id="path3" />
|
||||
<path
|
||||
style="fill:#000000;stroke:none;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 6,18 6,6 6,-6 h -2 l -4,4 -4,-4 z"
|
||||
id="path4" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
97
data/static/js/ui.js
Normal file
97
data/static/js/ui.js
Normal file
@@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
{
|
||||
function isBefore(el1, el2) {
|
||||
if (el2.parentNode === el1.parentNode) {
|
||||
for (let cur = el1.previousSibling; cur; cur = cur.previousSibling) {
|
||||
if (cur === el2) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let draggedItem = null;
|
||||
|
||||
function sortableItemDragStart(e, item) {
|
||||
const box = item.getBoundingClientRect();
|
||||
const oX = e.clientX - box.left;
|
||||
const oY = e.clientY - box.top;
|
||||
draggedItem = item;
|
||||
item.classList.add('contrast-bg');
|
||||
e.dataTransfer.setDragImage(item, oX, oY);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
|
||||
function sortableItemDragEnd(e, item) {
|
||||
draggedItem = null;
|
||||
item.classList.remove('contrast-bg');
|
||||
}
|
||||
|
||||
function sortableItemDragOver(e, item) {
|
||||
const target = e.target.closest('.sortable-item');
|
||||
if (!target || target === draggedItem) {
|
||||
return;
|
||||
}
|
||||
const inSameList = draggedItem.dataset.sortableListKey === target.dataset.sortableListKey;
|
||||
if (!inSameList) {
|
||||
return;
|
||||
}
|
||||
const targetList = draggedItem.closest('.sortable-list');
|
||||
if (isBefore(draggedItem, target)) {
|
||||
targetList.insertBefore(draggedItem, target);
|
||||
} else {
|
||||
targetList.insertBefore(draggedItem, target.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
const listItemsHandled = new Map();
|
||||
|
||||
const getListItemsHandled = (list) => {
|
||||
return listItemsHandled.get(list) || new Set();
|
||||
}
|
||||
|
||||
function registerSortableList(list) {
|
||||
list.querySelectorAll('li:not(.immovable)').forEach(item => {
|
||||
const listItems = getListItemsHandled(list);
|
||||
listItems.add(item);
|
||||
listItemsHandled.set(list, listItems);
|
||||
const dragger = item.querySelector('.dragger');
|
||||
dragger.addEventListener('dragstart', e => { sortableItemDragStart(e, item) });
|
||||
dragger.addEventListener('dragend', e => { sortableItemDragEnd(e, item) });
|
||||
item.addEventListener('dragover', e => { sortableItemDragOver(e, item) });
|
||||
});
|
||||
|
||||
const obs = new MutationObserver(records => {
|
||||
for (const mutation of records) {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (!(node instanceof HTMLElement)) return;
|
||||
if (!node.classList.contains('sortable-item')) return;
|
||||
const listItems = getListItemsHandled(list);
|
||||
if (listItems.has(node)) return;
|
||||
|
||||
const dragger = node.querySelector('.dragger');
|
||||
dragger.addEventListener('dragstart', e => { sortableItemDragStart(e, item) });
|
||||
dragger.addEventListener('dragend', e => { sortableItemDragEnd(e, item) });
|
||||
node.addEventListener('dragover', e => { sortableItemDragOver(e, node) });
|
||||
listItems.add(node);
|
||||
listItemsHandled.set(list, listItems);
|
||||
});
|
||||
}
|
||||
});
|
||||
obs.observe(list, { childList: true });
|
||||
}
|
||||
|
||||
document.querySelectorAll('.sortable-list').forEach(registerSortableList);
|
||||
|
||||
const listsObs = new MutationObserver(records => {
|
||||
for (const mutation of records) {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (!(node instanceof HTMLElement)) return;
|
||||
if (!node.classList.contains('sortable-list')) return;
|
||||
|
||||
registerSortableList(node);
|
||||
});
|
||||
}
|
||||
});
|
||||
listsObs.observe(document.body, { childList: true, subtree: true })
|
||||
}
|
||||
Reference in New Issue
Block a user