Browse Source

drag-reorder articles on edit magazine page

move edit button up to login
gitcitadel
Silberengel 2 weeks ago
parent
commit
688c0909d9
  1. 5
      Dockerfile
  2. 6
      assets/controllers/footer_magazine_edit_controller.js
  3. 108
      assets/controllers/magazine_hierarchy_editor_controller.js
  4. 5
      assets/controllers/user_highlight_tooltip_controller.js
  5. 54
      assets/styles/magazine-editor.css
  6. 7
      templates/components/Footer.html.twig
  7. 7
      templates/components/UserMenu.html.twig
  8. 42
      templates/pages/magazine_edit.html.twig

5
Dockerfile

@ -107,6 +107,11 @@ RUN set -eux; \
composer dump-autoload --classmap-authoritative --no-dev; \ composer dump-autoload --classmap-authoritative --no-dev; \
composer dump-env prod; \ composer dump-env prod; \
rm -f .env; \ rm -f .env; \
# Strip deployment secrets from the compiled .env.local.php so they cannot be read from the
# image layers. APP_SECRET, DATABASE_URL, and MYSQL_* passwords must be injected as real
# environment variables at runtime. If they are absent at runtime Symfony will raise an error
# rather than silently using the public .env.dist defaults.
php -r '$e=include(".env.local.php"); unset($e["APP_SECRET"],$e["DATABASE_URL"],$e["MYSQL_USER"],$e["MYSQL_PASSWORD"],$e["MYSQL_ROOT_PASSWORD"]); file_put_contents(".env.local.php","<?php return ".var_export($e,true).";".PHP_EOL);' ; \
composer run-script --no-dev post-install-cmd; \ composer run-script --no-dev post-install-cmd; \
php bin/console asset-map:compile --no-debug; \ php bin/console asset-map:compile --no-debug; \
chmod +x bin/console; sync; chmod +x bin/console; sync;

6
assets/controllers/footer_magazine_edit_controller.js

@ -23,12 +23,10 @@ export default class extends Controller {
if (!d) { if (!d) {
return; return;
} }
if (d.loggedIn === false) {
this.element.hidden = true;
return;
}
if (d.loggedIn && d.npub === this.publisherNpubValue) { if (d.loggedIn && d.npub === this.publisherNpubValue) {
this.element.hidden = false; this.element.hidden = false;
} else {
this.element.hidden = true;
} }
} }
} }

108
assets/controllers/magazine_hierarchy_editor_controller.js

@ -43,13 +43,25 @@ export default class MagazineHierarchyEditorController extends Controller {
// reference must be stable so removeEventListener in disconnect() can match it. // reference must be stable so removeEventListener in disconnect() can match it.
this._onDocClickCapture ??= this._onDocClickCapture.bind(this); this._onDocClickCapture ??= this._onDocClickCapture.bind(this);
this._onPanelFocusOut ??= this._onPanelFocusOut.bind(this); this._onPanelFocusOut ??= this._onPanelFocusOut.bind(this);
this._onDragStart ??= this._onDragStart.bind(this);
this._onDragOver ??= this._onDragOver.bind(this);
this._onDrop ??= this._onDrop.bind(this);
this._onDragEnd ??= this._onDragEnd.bind(this);
document.addEventListener('click', this._onDocClickCapture, true); document.addEventListener('click', this._onDocClickCapture, true);
this.element.addEventListener('focusout', this._onPanelFocusOut); this.element.addEventListener('focusout', this._onPanelFocusOut);
this.element.addEventListener('dragstart', this._onDragStart);
this.element.addEventListener('dragover', this._onDragOver);
this.element.addEventListener('drop', this._onDrop);
this.element.addEventListener('dragend', this._onDragEnd);
} }
disconnect() { disconnect() {
document.removeEventListener('click', this._onDocClickCapture, true); document.removeEventListener('click', this._onDocClickCapture, true);
this.element.removeEventListener('focusout', this._onPanelFocusOut); this.element.removeEventListener('focusout', this._onPanelFocusOut);
this.element.removeEventListener('dragstart', this._onDragStart);
this.element.removeEventListener('dragover', this._onDragOver);
this.element.removeEventListener('drop', this._onDrop);
this.element.removeEventListener('dragend', this._onDragEnd);
} }
/** /**
@ -127,6 +139,102 @@ export default class MagazineHierarchyEditorController extends Controller {
this.commitDTag(ev); this.commitDTag(ev);
} }
// -------------------------------------------------------------------------
// Drag-and-drop reordering for article (`a`-tag) rows within a list
// -------------------------------------------------------------------------
/**
* @param {DragEvent} ev
*/
_onDragStart(ev) {
const row = ev.target instanceof Element ? ev.target.closest('[data-mag-a-row]') : null;
if (!row) {
return;
}
this._dragRow = row;
ev.dataTransfer.effectAllowed = 'move';
// Firefox requires at least one dataTransfer item for drag to start.
ev.dataTransfer.setData('text/plain', '');
// Defer the opacity change so the browser captures the ghost image at full opacity first.
requestAnimationFrame(() => {
if (this._dragRow === row) {
row.dataset.dragging = '1';
}
});
}
/**
* @param {DragEvent} ev
*/
_onDragOver(ev) {
if (!this._dragRow) {
return;
}
const row = ev.target instanceof Element ? ev.target.closest('[data-mag-a-row]') : null;
if (!row || row === this._dragRow) {
this._clearDragOver();
return;
}
// Only allow reordering within the same `[data-mag-a-list]` container.
const dragList = this._dragRow.closest('[data-mag-a-list]');
if (!dragList || dragList !== row.closest('[data-mag-a-list]')) {
this._clearDragOver();
return;
}
ev.preventDefault();
ev.dataTransfer.dropEffect = 'move';
// Re-evaluate insert position as the cursor moves within the same row.
this._clearDragOver();
this._dragOverRow = row;
const rect = row.getBoundingClientRect();
row.dataset[ev.clientY < rect.top + rect.height / 2 ? 'dragBefore' : 'dragAfter'] = '1';
}
/**
* @param {DragEvent} ev
*/
_onDrop(ev) {
ev.preventDefault();
const overRow = this._dragOverRow;
const dragRow = this._dragRow;
if (!overRow || !dragRow || overRow === dragRow) {
this._clearDrag();
return;
}
const dragList = dragRow.closest('[data-mag-a-list]');
if (!dragList || dragList !== overRow.closest('[data-mag-a-list]')) {
this._clearDrag();
return;
}
const insertBefore = 'dragBefore' in overRow.dataset;
this._clearDrag();
if (insertBefore) {
dragList.insertBefore(dragRow, overRow);
} else {
overRow.insertAdjacentElement('afterend', dragRow);
}
}
_onDragEnd() {
this._clearDrag();
}
_clearDragOver() {
if (this._dragOverRow) {
delete this._dragOverRow.dataset.dragBefore;
delete this._dragOverRow.dataset.dragAfter;
this._dragOverRow = null;
}
}
_clearDrag() {
if (this._dragRow) {
delete this._dragRow.dataset.dragging;
this._dragRow = null;
}
this._clearDragOver();
}
captureBaselines() { captureBaselines() {
if (!this.hasNodesTarget) { if (!this.hasNodesTarget) {
return; return;

5
assets/controllers/user_highlight_tooltip_controller.js

@ -109,7 +109,10 @@ export default class extends Controller {
if (!(t instanceof Node)) { if (!(t instanceof Node)) {
return; return;
} }
const m = t.nodeType === 1 ? t.closest('mark.user-highlight__marker[data-hl]') : null; const m =
t.nodeType === 1
? /** @type {Element} */ (t).closest('mark.user-highlight__marker[data-hl]')
: t.parentElement?.closest('mark.user-highlight__marker[data-hl]') ?? null;
if (m) { if (m) {
const to = e.relatedTarget; const to = e.relatedTarget;
if (to && (m.contains(to) || (to instanceof Node && this.tip.contains(/** @type {Node} */ (to))))) { if (to && (m.contains(to) || (to instanceof Node && this.tip.contains(/** @type {Node} */ (to))))) {

54
assets/styles/magazine-editor.css

@ -228,16 +228,66 @@
gap: 0.5rem; gap: 0.5rem;
} }
/* Grid: two columns regardless of flex quirks inside fieldsets / % widths on inputs */ /* Grid: drag-handle | input | remove-btn */
.magazine-editor__a-row { .magazine-editor__a-row {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center; align-items: center;
gap: 0.35rem; gap: 0.35rem;
width: 100%; width: 100%;
min-width: 0; min-width: 0;
} }
.magazine-editor__a-drag-handle {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.1rem 0.15rem;
margin: 0;
background: none;
border: none;
color: var(--color-text-mid, rgba(160, 160, 160, 0.55));
cursor: grab;
touch-action: none;
line-height: 0;
font-size: 0;
text-transform: none;
font-weight: normal;
border-radius: 2px;
box-shadow: none;
}
.magazine-editor__a-drag-handle:active {
cursor: grabbing;
}
.magazine-editor__a-drag-handle:hover {
color: var(--color-text, inherit);
}
.magazine-editor__a-drag-handle:focus-visible {
outline: 2px solid var(--color-focus, currentColor);
outline-offset: 2px;
}
.magazine-editor__a-drag-handle svg {
display: block;
}
/* Ghost image: fade the row being dragged */
.magazine-editor__a-row[data-dragging] {
opacity: 0.35;
}
/* Drop-target indicator: a coloured line above or below the target row */
.magazine-editor__a-row[data-drag-before] {
box-shadow: 0 -2px 0 0 var(--color-accent, #5b9) inset;
}
.magazine-editor__a-row[data-drag-after] {
box-shadow: 0 2px 0 0 var(--color-accent, #5b9) inset;
}
.magazine-editor__a-line-field { .magazine-editor__a-line-field {
min-width: 0; min-width: 0;
max-width: 100%; max-width: 100%;

7
templates/components/Footer.html.twig

@ -10,13 +10,6 @@
<nav class="site-footer__nav" aria-label="Sitemap, feeds, and index"> <nav class="site-footer__nav" aria-label="Sitemap, feeds, and index">
<ul class="site-footer__syndication-list"> <ul class="site-footer__syndication-list">
<li><a class="site-footer__link" href="{{ path('featured_authors') }}">Featured authors</a></li> <li><a class="site-footer__link" href="{{ path('featured_authors') }}">Featured authors</a></li>
<li
data-controller="footer-magazine-edit"
data-footer-magazine-edit-publisher-npub-value="{{ publisher_npub }}"
{% if not (app.user and app.user.userIdentifier == publisher_npub) %}hidden{% endif %}
>
<a class="site-footer__link" href="{{ path('magazine_edit') }}">Edit magazine</a>
</li>
<li><a class="site-footer__link" href="{{ path('sitemap') }}">Sitemap (XML)</a></li> <li><a class="site-footer__link" href="{{ path('sitemap') }}">Sitemap (XML)</a></li>
<li><a class="site-footer__link" href="{{ path('robots_txt') }}">Robots</a></li> <li><a class="site-footer__link" href="{{ path('robots_txt') }}">Robots</a></li>
<li class="site-footer__syndication-list__feeds"> <li class="site-footer__syndication-list__feeds">

7
templates/components/UserMenu.html.twig

@ -25,6 +25,13 @@
<li> <li>
<a href="/logout" data-action="click->login#authLogout click->live#$render">{{ 'heading.logout'|trans }}</a> <a href="/logout" data-action="click->login#authLogout click->live#$render">{{ 'heading.logout'|trans }}</a>
</li> </li>
<li
data-controller="footer-magazine-edit"
data-footer-magazine-edit-publisher-npub-value="{{ publisher_npub }}"
{% if not (app.user.userIdentifier == publisher_npub) %}hidden{% endif %}
>
<a href="{{ path('magazine_edit') }}">Edit magazine</a>
</li>
<li> <li>
<a href="{{ path('search') }}">{{ 'heading.search'|trans }}</a> <a href="{{ path('search') }}">{{ 'heading.search'|trans }}</a>
</li> </li>

42
templates/pages/magazine_edit.html.twig

@ -8,7 +8,7 @@
<style id="magazine-editor-a-row-critical"> <style id="magazine-editor-a-row-critical">
.card.magazine-editor [data-mag-a-row] { .card.magazine-editor [data-mag-a-row] {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center; align-items: center;
gap: 0.35rem; gap: 0.35rem;
width: 100%; width: 100%;
@ -24,6 +24,28 @@
min-width: 0; min-width: 0;
box-sizing: border-box; box-sizing: border-box;
} }
.card.magazine-editor [data-mag-a-row] > button.magazine-editor__a-drag-handle {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: auto !important;
min-width: 0 !important;
padding: 0.1rem 0.15rem !important;
margin: 0 !important;
background: none !important;
border: none !important;
color: var(--color-text-mid, rgba(160,160,160,0.55)) !important;
cursor: grab !important;
text-transform: none !important;
font-weight: normal !important;
font-size: 0 !important;
line-height: 0 !important;
box-shadow: none !important;
border-radius: 2px !important;
}
.card.magazine-editor [data-mag-a-row] > button.magazine-editor__a-drag-handle svg {
display: block;
}
.card.magazine-editor [data-mag-a-row] > button.magazine-editor__a-remove-icon { .card.magazine-editor [data-mag-a-row] > button.magazine-editor__a-remove-icon {
box-sizing: border-box; box-sizing: border-box;
display: inline-flex; display: inline-flex;
@ -87,7 +109,14 @@
</div> </div>
<template data-magazine-hierarchy-editor-target="aRowTemplate"> <template data-magazine-hierarchy-editor-target="aRowTemplate">
<div class="magazine-editor__a-row" data-mag-a-row> <div class="magazine-editor__a-row" data-mag-a-row draggable="true">
<button type="button" class="magazine-editor__a-drag-handle" aria-label="Drag to reorder" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 16" width="10" height="16" fill="currentColor" aria-hidden="true">
<circle cx="3" cy="3" r="1.2"/><circle cx="7" cy="3" r="1.2"/>
<circle cx="3" cy="8" r="1.2"/><circle cx="7" cy="8" r="1.2"/>
<circle cx="3" cy="13" r="1.2"/><circle cx="7" cy="13" r="1.2"/>
</svg>
</button>
<div class="magazine-editor__a-line-field"> <div class="magazine-editor__a-line-field">
<input type="text" class="magazine-editor__input magazine-editor__input--mono magazine-editor__a-line-input" data-mag-a-line value="" spellcheck="false" autocomplete="off"> <input type="text" class="magazine-editor__input magazine-editor__input--mono magazine-editor__a-line-input" data-mag-a-line value="" spellcheck="false" autocomplete="off">
</div> </div>
@ -230,7 +259,14 @@
<p class="magazine-editor__a-hint">Each field is one <code>a</code> value: <code>kind:hex64pubkey:identifier</code>, long-form <code>30023</code>/<code>30024</code>, or NIP-19 <code>naddr1…</code> / <code>nostr:naddr1…</code> (expanded when signing).</p> <p class="magazine-editor__a-hint">Each field is one <code>a</code> value: <code>kind:hex64pubkey:identifier</code>, long-form <code>30023</code>/<code>30024</code>, or NIP-19 <code>naddr1…</code> / <code>nostr:naddr1…</code> (expanded when signing).</p>
<div class="magazine-editor__a-list" data-mag-a-list> <div class="magazine-editor__a-list" data-mag-a-list>
{% for coord in node.a_coordinates %} {% for coord in node.a_coordinates %}
<div class="magazine-editor__a-row" data-mag-a-row> <div class="magazine-editor__a-row" data-mag-a-row draggable="true">
<button type="button" class="magazine-editor__a-drag-handle" aria-label="Drag to reorder" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 16" width="10" height="16" fill="currentColor" aria-hidden="true">
<circle cx="3" cy="3" r="1.2"/><circle cx="7" cy="3" r="1.2"/>
<circle cx="3" cy="8" r="1.2"/><circle cx="7" cy="8" r="1.2"/>
<circle cx="3" cy="13" r="1.2"/><circle cx="7" cy="13" r="1.2"/>
</svg>
</button>
<div class="magazine-editor__a-line-field"> <div class="magazine-editor__a-line-field">
<input type="text" class="magazine-editor__input magazine-editor__input--mono magazine-editor__a-line-input" data-mag-a-line value="{{ coord|e('html_attr') }}" spellcheck="false" autocomplete="off"> <input type="text" class="magazine-editor__input magazine-editor__input--mono magazine-editor__a-line-input" data-mag-a-line value="{{ coord|e('html_attr') }}" spellcheck="false" autocomplete="off">
</div> </div>

Loading…
Cancel
Save