better UX/UI
This commit is contained in:
@@ -3,6 +3,113 @@
|
||||
|
||||
{% block title %}{{ nav.tr("td-presentation-title") }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
let activeDrag = null;
|
||||
|
||||
function rows(editor) {
|
||||
return Array.from(editor.querySelectorAll("[data-order-item]"));
|
||||
}
|
||||
|
||||
function ids(editor) {
|
||||
return rows(editor).map(function (row) { return row.dataset.columnId; });
|
||||
}
|
||||
|
||||
function refresh(editor) {
|
||||
rows(editor).forEach(function (row, index) {
|
||||
row.querySelector(".order-position").textContent = index + 1;
|
||||
});
|
||||
|
||||
const dirty = ids(editor).join(",") !== editor.dataset.originalOrder;
|
||||
editor.classList.toggle("is-dirty", dirty);
|
||||
editor.querySelector("[data-order-status]").textContent = dirty
|
||||
? editor.dataset.dirtyText
|
||||
: editor.dataset.cleanText;
|
||||
editor.querySelector("[data-order-reset]").disabled = !dirty;
|
||||
editor.querySelector("[data-order-save]").disabled = !dirty;
|
||||
}
|
||||
|
||||
function initialise(root) {
|
||||
root.querySelectorAll("[data-order-editor]").forEach(function (editor) {
|
||||
editor.dataset.originalOrder = ids(editor).join(",");
|
||||
refresh(editor);
|
||||
});
|
||||
}
|
||||
|
||||
function finishDrag(event) {
|
||||
if (!activeDrag || (event.pointerId !== undefined && event.pointerId !== activeDrag.pointerId)) return;
|
||||
activeDrag.row.classList.remove("is-dragging");
|
||||
if (activeDrag.handle.hasPointerCapture(activeDrag.pointerId)) {
|
||||
activeDrag.handle.releasePointerCapture(activeDrag.pointerId);
|
||||
}
|
||||
refresh(activeDrag.editor);
|
||||
activeDrag = null;
|
||||
}
|
||||
|
||||
document.addEventListener("pointerdown", function (event) {
|
||||
const handle = event.target.closest("[data-drag-handle]");
|
||||
if (!handle || (event.button !== undefined && event.button !== 0)) return;
|
||||
|
||||
const row = handle.closest("[data-order-item]");
|
||||
activeDrag = {
|
||||
editor: handle.closest("[data-order-editor]"),
|
||||
handle: handle,
|
||||
list: row.parentElement,
|
||||
pointerId: event.pointerId,
|
||||
row: row
|
||||
};
|
||||
handle.setPointerCapture(event.pointerId);
|
||||
row.classList.add("is-dragging");
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener("pointermove", function (event) {
|
||||
if (!activeDrag || event.pointerId !== activeDrag.pointerId) return;
|
||||
const hovered = document.elementFromPoint(event.clientX, event.clientY);
|
||||
const target = hovered && hovered.closest("[data-order-item]");
|
||||
if (!target || target === activeDrag.row || target.parentElement !== activeDrag.list) return;
|
||||
|
||||
const before = event.clientY < target.getBoundingClientRect().top + target.offsetHeight / 2;
|
||||
activeDrag.list.insertBefore(activeDrag.row, before ? target : target.nextElementSibling);
|
||||
refresh(activeDrag.editor);
|
||||
});
|
||||
|
||||
document.addEventListener("pointerup", finishDrag);
|
||||
document.addEventListener("pointercancel", finishDrag);
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
const handle = event.target.closest("[data-drag-handle]");
|
||||
if (!handle || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
|
||||
|
||||
const row = handle.closest("[data-order-item]");
|
||||
const sibling = event.key === "ArrowUp" ? row.previousElementSibling : row.nextElementSibling;
|
||||
if (!sibling) return;
|
||||
if (event.key === "ArrowUp") row.parentElement.insertBefore(row, sibling);
|
||||
else row.parentElement.insertBefore(sibling, row);
|
||||
refresh(handle.closest("[data-order-editor]"));
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
const reset = event.target.closest("[data-order-reset]");
|
||||
if (!reset) return;
|
||||
|
||||
const editor = reset.closest("[data-order-editor]");
|
||||
const byId = new Map(rows(editor).map(function (row) { return [row.dataset.columnId, row]; }));
|
||||
const list = editor.querySelector("[data-order-list]");
|
||||
editor.dataset.originalOrder.split(",").forEach(function (id) { list.appendChild(byId.get(id)); });
|
||||
refresh(editor);
|
||||
});
|
||||
|
||||
initialise(document);
|
||||
document.body.addEventListener("htmx:afterSwap", function (event) {
|
||||
initialise(event.detail.target);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
{% include "pages/admin/table_definition/context.html" %}
|
||||
|
||||
@@ -5,54 +5,76 @@
|
||||
{% if let Some(detail) = page.detail %}
|
||||
{# One form per column, each carrying its own column id: an alias is never
|
||||
posted apart from the column it was typed into. #}
|
||||
<section class="panel">
|
||||
<div class="presentation-layout">
|
||||
<section class="panel presentation-panel">
|
||||
<h2>{{ nav.tr("td-rename-column") }}</h2>
|
||||
<p class="hint">{{ nav.tr("td-rename-hint") }}</p>
|
||||
<div class="alias-list">
|
||||
<div class="alias-list-head" aria-hidden="true">
|
||||
<span>{{ nav.tr("td-current-alias") }}</span>
|
||||
<span></span>
|
||||
<span>{{ nav.tr("td-new-name") }}</span>
|
||||
<span></span>
|
||||
</div>
|
||||
{% for column in detail.columns %}
|
||||
{% if column.renameable %}
|
||||
<form hx-post="/admin/tables/presentation/alias"
|
||||
<form class="alias-row" hx-post="/admin/tables/presentation/alias"
|
||||
hx-target="#table-panel" hx-swap="innerHTML">
|
||||
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
|
||||
<input type="hidden" name="table" value="{{ page.selection.table }}">
|
||||
<input type="hidden" name="expected_row_version" value="{{ detail.row_version }}">
|
||||
<input type="hidden" name="column_id" value="{{ column.column_id }}">
|
||||
<div class="form-grid">
|
||||
<label>{{ nav.tr("td-column-label") }}
|
||||
<input name="alias" value="{{ column.name }}">
|
||||
</label>
|
||||
<div class="form-actions">
|
||||
<button type="submit">{{ nav.tr("td-rename-button") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<code class="alias-current">{{ column.name }}</code>
|
||||
<span class="alias-arrow" aria-hidden="true">→</span>
|
||||
<input name="alias" aria-label="{{ nav.tr("td-new-name") }}: {{ column.name }}"
|
||||
placeholder="{{ nav.tr("td-new-name") }}" required>
|
||||
<button type="submit">{{ nav.tr("td-rename-button") }}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="hint"><code>{{ column.name }}</code> — {{ nav.tr("td-alias-locked") }}</p>
|
||||
<div class="alias-row alias-row-locked">
|
||||
<code class="alias-current">{{ column.name }}</code>
|
||||
<span class="locked-label">{{ nav.tr("td-alias-locked") }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# The order forms carry no alias at all, so moving a column cannot rename
|
||||
one. The names below are labels, not inputs. #}
|
||||
<section class="panel">
|
||||
{# Ordering is staged in the browser and submitted as one id-only form.
|
||||
Aliases are still absent, so saving an order cannot rename anything. #}
|
||||
<section class="panel presentation-panel">
|
||||
<h2>{{ nav.tr("td-order-heading") }}</h2>
|
||||
<p class="hint">{{ nav.tr("td-order-hint") }}</p>
|
||||
<ol class="column-order">
|
||||
{% for column in detail.columns %}
|
||||
<li>
|
||||
<code>{{ column.name }}</code>
|
||||
<form hx-post="/admin/tables/presentation/order"
|
||||
hx-target="#table-panel" hx-swap="innerHTML">
|
||||
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
|
||||
<input type="hidden" name="table" value="{{ page.selection.table }}">
|
||||
<input type="hidden" name="expected_row_version" value="{{ detail.row_version }}">
|
||||
<input type="hidden" name="column_id" value="{{ column.column_id }}">
|
||||
<button type="submit" name="direction" value="up" {% if loop.first %}disabled{% endif %}>↑</button>
|
||||
<button type="submit" name="direction" value="down" {% if loop.last %}disabled{% endif %}>↓</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
<form class="order-editor" data-order-editor
|
||||
data-clean-text="{{ nav.tr("td-order-clean") }}"
|
||||
data-dirty-text="{{ nav.tr("td-order-unsaved") }}"
|
||||
hx-post="/admin/tables/presentation/order"
|
||||
hx-target="#table-panel" hx-swap="innerHTML">
|
||||
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
|
||||
<input type="hidden" name="table" value="{{ page.selection.table }}">
|
||||
<input type="hidden" name="expected_row_version" value="{{ detail.row_version }}">
|
||||
<ol class="column-order" data-order-list>
|
||||
{% for column in detail.columns %}
|
||||
<li data-order-item data-column-id="{{ column.column_id }}">
|
||||
<input type="hidden" name="column_ids" value="{{ column.column_id }}">
|
||||
<button class="drag-handle" type="button" data-drag-handle
|
||||
aria-label="{{ nav.tr("td-drag-column") }}: {{ column.name }}"
|
||||
title="{{ nav.tr("td-drag-column") }}">
|
||||
<span aria-hidden="true">⠿</span>
|
||||
</button>
|
||||
<span class="order-position" aria-hidden="true">{{ loop.index }}</span>
|
||||
<code class="order-name">{{ column.name }}</code>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
<div class="order-actions">
|
||||
<span class="order-status" data-order-status aria-live="polite">{{ nav.tr("td-order-clean") }}</span>
|
||||
<button class="secondary" type="button" data-order-reset disabled>{{ nav.tr("td-order-reset") }}</button>
|
||||
<button type="submit" data-order-save disabled>{{ nav.tr("td-order-save") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user