Unify modals

This commit is contained in:
2026-04-22 18:26:31 +02:00
parent 7c0d856081
commit 093b5c2ef0
6 changed files with 337 additions and 49 deletions
+2 -2
View File
@@ -142,11 +142,11 @@
function makeDropdown(triggerBtn, items) {
var menu = document.createElement('div');
menu.className = 'toolbar-dropdown-menu';
menu.className = 'dropdown-menu';
items.forEach(function (item) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn btn-tool toolbar-dropdown-item';
btn.className = 'btn btn-tool dropdown-item';
btn.textContent = item.label;
btn.addEventListener('mousedown', function (e) {
e.preventDefault();
+8 -11
View File
@@ -1,10 +1,3 @@
function newPage() {
const name = prompt('New page name:');
if (!name || !name.trim()) return;
const slug = name.trim().replace(/\s+/g, '-');
window.location.href = window.location.pathname + slug + '/?edit';
}
(function () {
document.addEventListener('keydown', function (e) {
if (!e.altKey || !e.shiftKey) return;
@@ -12,11 +5,15 @@ function newPage() {
case 'E':
e.preventDefault();
window.location.href = window.location.pathname + '?edit';
break;
break;
case 'N':
e.preventDefault();
newPage();
break;
e.preventDefault();
if (typeof newPage === 'function') newPage();
break;
case 'M':
e.preventDefault();
if (window.location.pathname !== '/' && typeof movePage === 'function') movePage();
break;
}
});
})();
+146
View File
@@ -0,0 +1,146 @@
(function () {
var backdrop = null;
var modal = null;
var titleEl = null;
var bodyEl = null;
var cancelBtn = null;
var confirmBtn = null;
var footerEl = null;
var prevFocus = null;
var currentOpts = null;
var onKeydown = null;
function build() {
backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop';
modal = document.createElement('div');
modal.className = 'modal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
var header = document.createElement('div');
header.className = 'modal-header';
titleEl = document.createElement('span');
header.appendChild(titleEl);
bodyEl = document.createElement('div');
bodyEl.className = 'modal-body';
footerEl = document.createElement('div');
footerEl.className = 'modal-footer';
cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
confirmBtn = document.createElement('button');
confirmBtn.type = 'button';
modal.appendChild(header);
modal.appendChild(bodyEl);
modal.appendChild(footerEl);
backdrop.appendChild(modal);
backdrop.addEventListener('mousedown', function (e) {
if (e.target === backdrop) close();
});
cancelBtn.addEventListener('click', close);
confirmBtn.addEventListener('click', function () {
if (confirmBtn.disabled) return;
if (currentOpts && currentOpts.confirm && currentOpts.confirm.onConfirm) {
currentOpts.confirm.onConfirm();
}
});
}
function open(opts) {
if (backdrop && backdrop.parentNode) close();
if (!backdrop) build();
currentOpts = opts;
prevFocus = document.activeElement;
titleEl.textContent = opts.title || '';
bodyEl.textContent = '';
if (opts.body instanceof Node) {
bodyEl.appendChild(opts.body);
} else if (typeof opts.body === 'string') {
bodyEl.textContent = opts.body;
}
var confirmOpts = opts.confirm || {};
var cancelOpts = opts.cancel || {};
confirmBtn.textContent = confirmOpts.label || 'OK';
confirmBtn.className = 'btn' + (confirmOpts.danger ? ' danger' : '');
confirmBtn.disabled = !!confirmOpts.initiallyDisabled;
cancelBtn.textContent = cancelOpts.label || 'CANCEL';
cancelBtn.className = 'btn';
footerEl.textContent = '';
if (opts.swapButtons) {
footerEl.appendChild(confirmBtn);
footerEl.appendChild(cancelBtn);
} else {
footerEl.appendChild(cancelBtn);
footerEl.appendChild(confirmBtn);
}
document.body.appendChild(backdrop);
setTimeout(function () {
if (cancelOpts.autofocus) {
cancelBtn.focus();
return;
}
var firstInput = bodyEl.querySelector('input, textarea, select');
if (firstInput) {
firstInput.focus();
if (firstInput.select) firstInput.select();
} else {
confirmBtn.focus();
}
}, 0);
var enterConfirms = confirmOpts.enterConfirms !== false;
onKeydown = function (e) {
if (e.key === 'Escape') {
e.preventDefault();
close();
return;
}
if (e.key === 'Enter' && enterConfirms) {
var tag = (e.target && e.target.tagName) || '';
if (tag === 'TEXTAREA') return;
e.preventDefault();
if (!confirmBtn.disabled) confirmBtn.click();
}
};
document.addEventListener('keydown', onKeydown);
return {
close: close,
setConfirmDisabled: function (d) { confirmBtn.disabled = !!d; },
confirmButton: confirmBtn
};
}
function close() {
if (!backdrop || !backdrop.parentNode) return;
if (onKeydown) {
document.removeEventListener('keydown', onKeydown);
onKeydown = null;
}
backdrop.parentNode.removeChild(backdrop);
currentOpts = null;
var toRestore = prevFocus;
prevFocus = null;
if (toRestore && toRestore.focus) {
try { toRestore.focus(); } catch (e) {}
}
}
window.openModal = open;
window.closeModal = close;
})();
+86 -20
View File
@@ -1,25 +1,91 @@
function newPage() {
var input = document.createElement('input');
input.type = 'text';
input.className = 'modal-input';
input.placeholder = 'New page name';
openModal({
title: 'New page',
body: input,
confirm: {
label: 'CREATE',
onConfirm: function () {
var name = input.value.trim();
if (!name) return;
window.location.href = window.location.pathname +
encodeURIComponent(name) + '/?edit';
}
}
});
}
function movePage() {
const current = window.location.pathname;
const target = prompt('Move this page to (absolute path):', current);
if (target === null) return;
const clean = target.trim();
if (!clean || !clean.startsWith('/')) {
alert('Move target must be an absolute path starting with /');
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = current + '?move=' + encodeURIComponent(clean);
document.body.appendChild(form);
form.submit();
var input = document.createElement('input');
input.type = 'text';
input.className = 'modal-input';
input.value = decodeURIComponent(window.location.pathname);
openModal({
title: 'Move page',
body: input,
confirm: {
label: 'MOVE',
onConfirm: function () {
var target = input.value.trim();
if (!target || !target.startsWith('/')) return;
var form = document.createElement('form');
form.method = 'POST';
form.action = window.location.pathname + '?move=' +
encodeURIComponent(target);
document.body.appendChild(form);
form.submit();
}
}
});
}
function deletePage() {
const current = window.location.pathname;
if (!confirm('Delete ' + current + ' and everything inside it?')) return;
const form = document.createElement('form');
form.method = 'POST';
form.action = current + '?delete=1';
document.body.appendChild(form);
form.submit();
var decodedPath = decodeURIComponent(window.location.pathname);
openModal({
title: 'Delete page',
body: 'Delete ' + decodedPath + ' and everything inside it?',
confirm: {
label: 'DELETE',
danger: true,
enterConfirms: false,
onConfirm: function () {
var form = document.createElement('form');
form.method = 'POST';
form.action = window.location.pathname + '?delete=1';
document.body.appendChild(form);
form.submit();
}
},
cancel: { autofocus: true },
swapButtons: true
});
}
document.addEventListener('DOMContentLoaded', function () {
var trigger = document.querySelector('[data-action="actions-drop"]');
if (!trigger) return;
var menu = trigger.parentElement.querySelector('.dropdown-menu');
if (!menu) return;
trigger.addEventListener('click', function (e) {
e.stopPropagation();
menu.classList.toggle('is-open');
});
menu.addEventListener('click', function () {
menu.classList.remove('is-open');
});
document.addEventListener('click', function (e) {
if (!trigger.contains(e.target) && !menu.contains(e.target)) {
menu.classList.remove('is-open');
}
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') menu.classList.remove('is-open');
});
});
+14 -8
View File
@@ -8,6 +8,7 @@
<link rel="preload" href="/_/fonts/IosevkaEtoile.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/_/fonts/IosevkaSlab.woff2" as="font" type="font/woff2" crossorigin />
<link rel="stylesheet" href="/_/style.css" />
<script src="/_/modal.js"></script>
<script src="/_/global-shortcuts.js"></script>
<script src="/_/page-actions.js"></script>
</head>
@@ -23,12 +24,17 @@
<a class="btn" href="{{.PostURL}}">CANCEL</a>
<button class="btn" type="submit" form="edit-form" data-action="save" data-key="S" title="Save (S)">SAVE</button>
{{else if .CanEdit}}
<button class="btn" onclick="newPage()" title="New page (N)">NEW</button>
<a class="btn" href="?edit" title="Edit page (E)">EDIT</a>
{{if not .IsRoot}}
<button class="btn" onclick="movePage()" title="Move page">MOVE</button>
<button class="btn danger" onclick="deletePage()" title="Delete page">DELETE</button>
{{end}}
<div class="dropdown">
<button class="btn" data-action="actions-drop" title="Actions">ACTIONS ▾</button>
<div class="dropdown-menu align-right">
<button class="btn dropdown-item" onclick="newPage()" title="New page (N)">NEW</button>
<a class="btn dropdown-item" href="?edit" title="Edit page (E)">EDIT</a>
{{if not .IsRoot}}
<button class="btn dropdown-item" onclick="movePage()" title="Move page (M)">MOVE</button>
<button class="btn dropdown-item danger" onclick="deletePage()" title="Delete page">DELETE</button>
{{end}}
</div>
</div>
{{end}}
</header>
<main>
@@ -52,8 +58,8 @@
<button type="button" class="btn btn-tool" data-action="ol" data-key="O" title="Ordered list (O)">1.</button>
<button type="button" class="btn btn-tool" data-action="hr" data-key="R" title="Horizontal rule (R)">---</button>
<span class="toolbar-sep"></span>
<button type="button" class="btn btn-tool toolbar-dropdown" data-action="tbldrop" title="Table (T)">T▾</button>
<button type="button" class="btn btn-tool toolbar-dropdown" data-action="datedrop" title="Insert date (D/W)">D▾</button>
<button type="button" class="btn btn-tool dropdown" data-action="tbldrop" title="Table (T)">T▾</button>
<button type="button" class="btn btn-tool dropdown" data-action="datedrop" title="Insert date (D/W)">D▾</button>
</div>
<textarea name="content" id="editor" autofocus>{{.RawContent}}</textarea>
</form>
+81 -8
View File
@@ -22,6 +22,8 @@
--secondary: #c48401;
--link: #01b6c4;
--link-hover: #d6d24d;
--danger: #c40141;
--danger-hover: #d03467;
}
/* === Base === */
@@ -135,10 +137,10 @@ header {
/* Destructive action */
.danger {
color: var(--primary-hover);
color: var(--danger);
}
.danger:hover {
color: var(--link-hover);
color: var(--danger-hover);
}
/* === Main === */
@@ -291,12 +293,12 @@ main {
align-self: stretch;
}
/* === Toolbar dropdowns === */
.toolbar-dropdown {
/* === Dropdowns === */
.dropdown {
position: relative;
}
.toolbar-dropdown-menu {
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
@@ -307,17 +309,23 @@ main {
min-width: 9rem;
}
.toolbar-dropdown-menu.is-open {
.dropdown-menu.align-right {
left: auto;
right: 0;
}
.dropdown-menu.is-open {
display: block;
}
.toolbar-dropdown-item {
.dropdown-item {
display: block;
width: 100%;
text-align: left;
border: none;
border-radius: 0;
padding: 0.2rem 0.5rem;
padding: 0.3rem 0.75rem;
white-space: nowrap;
}
/* === Edit form === */
@@ -451,6 +459,63 @@ hr {
display: none;
}
/* === Modal === */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal {
background: var(--bg-panel);
border: 1px solid var(--secondary);
width: 100%;
max-width: 500px;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 0.6rem 1rem;
border-bottom: 1px dashed var(--secondary);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text);
}
.modal-body {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
word-break: break-word;
}
.modal-footer {
padding: 0.6rem 1rem;
border-top: 1px dashed var(--secondary);
display: flex;
justify-content: space-between;
gap: 0.5rem;
}
.modal-input {
width: 100%;
background: var(--bg);
border: 1px solid var(--secondary);
color: var(--text);
font: inherit;
font-family: "Iosevka Slab", monospace;
padding: 0.4rem 0.6rem;
outline: none;
}
/* === Responsive === */
@media (max-width: 1100px) {
.toc-toggle {
@@ -502,4 +567,12 @@ hr {
.toc.is-open {
width: calc(100% - 1.5rem);
}
.modal-backdrop {
padding: 0.5rem;
align-items: flex-start;
}
.modal {
max-width: none;
margin-top: 1rem;
}
}