fix: address code review findings for bookmark feature (H1–L5)

Fix broken tests (H1), align interface signatures with implementations (H3),
remove dead code (H4), add missing input guard (M1), replace raw $_SESSION
access with SessionStoreInterface (M2), sync update script schema (M4),
use shared getAppBase utility (L2), document fragment stripping (L3),
and apply app- CSS class prefix convention (L5).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 22:58:07 +01:00
parent 9688848401
commit d9805c45d3
10 changed files with 195 additions and 91 deletions

View File

@@ -5,29 +5,43 @@
import { showAsyncFlash } from './app-async-flash.js';
import { warnOnce } from '../core/app-dom.js';
import { getAppBase } from '../pages/app-list-utils.js';
export function init() {
const openBtn = document.querySelector('[data-bookmark-open-dialog]');
const bookmarkDialog = document.querySelector('[data-app-bookmark-dialog]');
if (!(bookmarkDialog instanceof HTMLDialogElement) || !(openBtn instanceof HTMLButtonElement)) return;
export function initBookmarkSave(root = document) {
const host = root && typeof root.querySelector === 'function' ? root : document;
const openBtn = host.querySelector('[data-bookmark-open-dialog]') || document.querySelector('[data-bookmark-open-dialog]');
const bookmarkDialog = host.matches?.('[data-app-bookmark-dialog]')
? host
: (host.querySelector('[data-app-bookmark-dialog]') || document.querySelector('[data-app-bookmark-dialog]'));
if (!(bookmarkDialog instanceof HTMLDialogElement) || !(openBtn instanceof HTMLButtonElement)) {
return { destroy: () => {} };
}
if (bookmarkDialog.dataset.bookmarkSaveBound === '1' && bookmarkDialog._bookmarkSaveApi) {
return bookmarkDialog._bookmarkSaveApi;
}
const bookmarkForm = bookmarkDialog.querySelector('[data-bookmark-save-form]');
if (!(bookmarkForm instanceof HTMLFormElement)) return;
if (!(bookmarkForm instanceof HTMLFormElement)) {
return { destroy: () => {} };
}
const groupDialog = document.querySelector('[data-app-bookmark-group-dialog]');
const groupForm = groupDialog instanceof HTMLDialogElement
? groupDialog.querySelector('[data-bookmark-group-save-form]')
: null;
const hasGroupDialog = groupDialog instanceof HTMLDialogElement && groupForm instanceof HTMLFormElement;
const abortController = new AbortController();
const { signal } = abortController;
const timerIds = [];
const root = document.documentElement;
const csrfKey = String(root.dataset.csrfKey || '').trim();
const csrfToken = String(root.dataset.csrfToken || '').trim();
const documentRoot = document.documentElement;
const csrfKey = String(documentRoot.dataset.csrfKey || '').trim();
const csrfToken = String(documentRoot.dataset.csrfToken || '').trim();
const moduleContext = { module: 'bookmark-save' };
if (!csrfKey || !csrfToken) {
warnOnce('UI_DATA_MISSING', 'Missing CSRF metadata for bookmark dialog', moduleContext);
return;
return { destroy: () => {} };
}
const nameInput = bookmarkForm.querySelector('input[name="name"]');
@@ -83,7 +97,8 @@ export function init() {
const runReloadWithSuccess = (message) => {
showAsyncFlash('success', message, 2500);
window.setTimeout(() => window.location.reload(), 450);
const timerId = window.setTimeout(() => window.location.reload(), 450);
timerIds.push(timerId);
};
const closeBookmarkDialog = () => {
@@ -210,33 +225,33 @@ export function init() {
if (openBookmarkDialog() && nameInput instanceof HTMLInputElement) {
nameInput.select();
}
});
}, { signal });
document.addEventListener('app:bookmark-edit', (event) => {
const detail = event instanceof CustomEvent ? event.detail : null;
openBookmarkEditFromSidebar(detail);
});
}, { signal });
closeBookmarkBtns.forEach((btn) => {
btn.addEventListener('click', (event) => {
event.preventDefault();
closeBookmarkDialog();
});
}, { signal });
});
bookmarkDialog.addEventListener('click', (event) => {
if (event.target === bookmarkDialog) {
closeBookmarkDialog();
}
});
}, { signal });
bookmarkDialog.addEventListener('close', () => {
bookmarkForm.removeAttribute('aria-busy');
syncModalBodyClass();
});
}, { signal });
bookmarkDialog.addEventListener('cancel', () => {
bookmarkForm.removeAttribute('aria-busy');
syncModalBodyClass();
});
}, { signal });
// Save bookmark
bookmarkForm.addEventListener('submit', async (event) => {
@@ -310,10 +325,22 @@ export function init() {
} finally {
setBookmarkSubmitPending(false);
}
});
}, { signal });
const destroy = () => {
abortController.abort();
timerIds.forEach((timerId) => window.clearTimeout(timerId));
timerIds.length = 0;
bookmarkForm.removeAttribute('aria-busy');
delete bookmarkDialog.dataset.bookmarkSaveBound;
delete bookmarkDialog._bookmarkSaveApi;
};
const api = { destroy };
bookmarkDialog.dataset.bookmarkSaveBound = '1';
bookmarkDialog._bookmarkSaveApi = api;
if (!hasGroupDialog) {
return;
return api;
}
const groupNameInput = groupForm.querySelector('input[name="name"]');
@@ -381,10 +408,10 @@ export function init() {
groupIconInput.value = value;
}
if (groupIconPicker instanceof HTMLElement) {
groupIconPicker.querySelectorAll('.active').forEach((item) => item.classList.remove('active'));
groupIconPicker.querySelectorAll('.app-bookmark-icon-active').forEach((item) => item.classList.remove('app-bookmark-icon-active'));
const selected = groupIconPicker.querySelector(`[data-icon="${value}"]`);
if (selected instanceof HTMLElement) {
selected.classList.add('active');
selected.classList.add('app-bookmark-icon-active');
}
}
};
@@ -459,7 +486,7 @@ export function init() {
newGroupBtn.addEventListener('click', (event) => {
event.preventDefault();
openCreateGroupFromBookmarkDialog();
});
}, { signal });
}
if (groupIconPicker instanceof HTMLElement) {
@@ -468,29 +495,29 @@ export function init() {
if (!(target instanceof HTMLElement)) return;
event.preventDefault();
setGroupIcon(target.dataset.icon || 'bi-folder');
});
}, { signal });
}
groupCloseBtns.forEach((btn) => {
btn.addEventListener('click', (event) => {
event.preventDefault();
handleGroupCancel();
});
}, { signal });
});
groupDialog.addEventListener('click', (event) => {
if (event.target === groupDialog) {
handleGroupCancel();
}
});
}, { signal });
groupDialog.addEventListener('cancel', (event) => {
event.preventDefault();
handleGroupCancel();
});
}, { signal });
groupDialog.addEventListener('close', () => {
groupForm.removeAttribute('aria-busy');
syncModalBodyClass();
});
}, { signal });
document.addEventListener('app:bookmark-group-edit', (event) => {
const detail = event instanceof CustomEvent ? event.detail : null;
@@ -513,7 +540,7 @@ export function init() {
groupNameInput.focus();
groupNameInput.select();
}
});
}, { signal });
groupForm.addEventListener('submit', async (event) => {
event.preventDefault();
@@ -574,12 +601,9 @@ export function init() {
} finally {
setGroupSubmitPending(false);
}
});
}
}, { signal });
function getAppBase() {
const base = document.querySelector('base');
return base ? base.href : '/';
return api;
}
function currentRelativeUrl() {