feat: improve bookmark dialog UX with distinct modes, inline validation, and accessibility
Refine bookmark create/edit and group create/edit dialogs to enterprise quality: distinct titles per mode (Add bookmark vs Edit bookmark), URL preview when creating, inline field validation with aria-invalid instead of toast-only errors, loading spinner on submit, compact inline new-group link, human-readable icon picker aria-labels, and aria-pressed on selected icons. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,5 +39,29 @@
|
|||||||
"Edit group": "Gruppe bearbeiten",
|
"Edit group": "Gruppe bearbeiten",
|
||||||
"Group updated": "Gruppe aktualisiert",
|
"Group updated": "Gruppe aktualisiert",
|
||||||
"Group name is required": "Gruppenname ist erforderlich",
|
"Group name is required": "Gruppenname ist erforderlich",
|
||||||
"Maximum groups reached": "Maximale Anzahl an Gruppen erreicht"
|
"Maximum groups reached": "Maximale Anzahl an Gruppen erreicht",
|
||||||
|
"Add bookmark": "Lesezeichen hinzufügen",
|
||||||
|
"Saving...": "Speichern...",
|
||||||
|
"Create group": "Gruppe erstellen",
|
||||||
|
"Bookmarking this page:": "Lesezeichen für diese Seite:",
|
||||||
|
"Icon: Home": "Zuhause",
|
||||||
|
"Icon: People": "Personen",
|
||||||
|
"Icon: Person": "Person",
|
||||||
|
"Icon: Folder": "Ordner",
|
||||||
|
"Icon: Document": "Dokument",
|
||||||
|
"Icon: Settings": "Einstellungen",
|
||||||
|
"Icon: Security": "Sicherheit",
|
||||||
|
"Icon: Chart": "Diagramm",
|
||||||
|
"Icon: Mail": "E-Mail",
|
||||||
|
"Icon: Calendar": "Kalender",
|
||||||
|
"Icon: Clock": "Uhr",
|
||||||
|
"Icon: Star": "Stern",
|
||||||
|
"Icon: Heart": "Herz",
|
||||||
|
"Icon: Flag": "Flagge",
|
||||||
|
"Icon: Bookmark": "Lesezeichen",
|
||||||
|
"Icon: Location": "Standort",
|
||||||
|
"Icon: Lightning": "Blitz",
|
||||||
|
"Icon: Database": "Datenbank",
|
||||||
|
"Icon: Globe": "Globus",
|
||||||
|
"Icon: Code": "Code"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,5 +39,29 @@
|
|||||||
"Edit group": "Edit group",
|
"Edit group": "Edit group",
|
||||||
"Group updated": "Group updated",
|
"Group updated": "Group updated",
|
||||||
"Group name is required": "Group name is required",
|
"Group name is required": "Group name is required",
|
||||||
"Maximum groups reached": "Maximum groups reached"
|
"Maximum groups reached": "Maximum groups reached",
|
||||||
|
"Add bookmark": "Add bookmark",
|
||||||
|
"Saving...": "Saving...",
|
||||||
|
"Create group": "Create group",
|
||||||
|
"Bookmarking this page:": "Bookmarking this page:",
|
||||||
|
"Icon: Home": "Home",
|
||||||
|
"Icon: People": "People",
|
||||||
|
"Icon: Person": "Person",
|
||||||
|
"Icon: Folder": "Folder",
|
||||||
|
"Icon: Document": "Document",
|
||||||
|
"Icon: Settings": "Settings",
|
||||||
|
"Icon: Security": "Security",
|
||||||
|
"Icon: Chart": "Chart",
|
||||||
|
"Icon: Mail": "Mail",
|
||||||
|
"Icon: Calendar": "Calendar",
|
||||||
|
"Icon: Clock": "Clock",
|
||||||
|
"Icon: Star": "Star",
|
||||||
|
"Icon: Heart": "Heart",
|
||||||
|
"Icon: Flag": "Flag",
|
||||||
|
"Icon: Bookmark": "Bookmark",
|
||||||
|
"Icon: Location": "Location",
|
||||||
|
"Icon: Lightning": "Lightning",
|
||||||
|
"Icon: Database": "Database",
|
||||||
|
"Icon: Globe": "Globe",
|
||||||
|
"Icon: Code": "Code"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,40 @@ $layoutNav = is_array($layoutNav ?? null) ? $layoutNav : [];
|
|||||||
$bookmarkNav = is_array($layoutNav['bookmarks.nav'] ?? null) ? $layoutNav['bookmarks.nav'] : [];
|
$bookmarkNav = is_array($layoutNav['bookmarks.nav'] ?? null) ? $layoutNav['bookmarks.nav'] : [];
|
||||||
$bookmarkData = is_array($bookmarkNav['grouped'] ?? null) ? $bookmarkNav['grouped'] : [];
|
$bookmarkData = is_array($bookmarkNav['grouped'] ?? null) ? $bookmarkNav['grouped'] : [];
|
||||||
$bookmarkGroups = is_array($bookmarkData['groups'] ?? null) ? $bookmarkData['groups'] : [];
|
$bookmarkGroups = is_array($bookmarkData['groups'] ?? null) ? $bookmarkData['groups'] : [];
|
||||||
|
|
||||||
|
// Human-readable icon labels — keep in sync with BookmarkService::ALLOWED_GROUP_ICONS
|
||||||
|
$iconLabels = [
|
||||||
|
'bi-house' => t('Icon: Home'),
|
||||||
|
'bi-people' => t('Icon: People'),
|
||||||
|
'bi-person' => t('Icon: Person'),
|
||||||
|
'bi-folder' => t('Icon: Folder'),
|
||||||
|
'bi-file-text' => t('Icon: Document'),
|
||||||
|
'bi-gear' => t('Icon: Settings'),
|
||||||
|
'bi-shield-lock' => t('Icon: Security'),
|
||||||
|
'bi-bar-chart' => t('Icon: Chart'),
|
||||||
|
'bi-envelope' => t('Icon: Mail'),
|
||||||
|
'bi-calendar' => t('Icon: Calendar'),
|
||||||
|
'bi-clock' => t('Icon: Clock'),
|
||||||
|
'bi-star' => t('Icon: Star'),
|
||||||
|
'bi-heart' => t('Icon: Heart'),
|
||||||
|
'bi-flag' => t('Icon: Flag'),
|
||||||
|
'bi-bookmark' => t('Icon: Bookmark'),
|
||||||
|
'bi-pin-map' => t('Icon: Location'),
|
||||||
|
'bi-lightning' => t('Icon: Lightning'),
|
||||||
|
'bi-database' => t('Icon: Database'),
|
||||||
|
'bi-globe' => t('Icon: Globe'),
|
||||||
|
'bi-code-slash' => t('Icon: Code'),
|
||||||
|
];
|
||||||
?>
|
?>
|
||||||
<dialog
|
<dialog
|
||||||
data-app-bookmark-dialog
|
data-app-bookmark-dialog
|
||||||
data-app-component="bookmark-save"
|
data-app-component="bookmark-save"
|
||||||
data-max-message="<?php e(t('Maximum bookmarks reached')); ?>"
|
data-max-message="<?php e(t('Maximum bookmarks reached')); ?>"
|
||||||
data-title-create="<?php e(t('Bookmarks')); ?>"
|
data-title-create="<?php e(t('Add bookmark')); ?>"
|
||||||
data-title-edit="<?php e(t('Bookmarks')); ?>"
|
data-title-edit="<?php e(t('Edit bookmark')); ?>"
|
||||||
data-label-save="<?php e(t('Save')); ?>"
|
data-label-save="<?php e(t('Add bookmark')); ?>"
|
||||||
data-label-update="<?php e(t('Save')); ?>"
|
data-label-update="<?php e(t('Save changes')); ?>"
|
||||||
|
data-label-saving="<?php e(t('Saving...')); ?>"
|
||||||
data-msg-saved="<?php e(t('Bookmark saved')); ?>"
|
data-msg-saved="<?php e(t('Bookmark saved')); ?>"
|
||||||
data-msg-updated="<?php e(t('Bookmark updated')); ?>"
|
data-msg-updated="<?php e(t('Bookmark updated')); ?>"
|
||||||
data-msg-group-created="<?php e(t('Group created')); ?>"
|
data-msg-group-created="<?php e(t('Group created')); ?>"
|
||||||
@@ -23,18 +48,27 @@ $bookmarkGroups = is_array($bookmarkData['groups'] ?? null) ? $bookmarkData['gro
|
|||||||
data-msg-name-required="<?php e(t('Bookmark name is required')); ?>"
|
data-msg-name-required="<?php e(t('Bookmark name is required')); ?>"
|
||||||
data-msg-url-required="<?php e(t('Bookmark URL is invalid')); ?>"
|
data-msg-url-required="<?php e(t('Bookmark URL is invalid')); ?>"
|
||||||
data-msg-group-not-found="<?php e(t('Group not found')); ?>"
|
data-msg-group-not-found="<?php e(t('Group not found')); ?>"
|
||||||
|
data-url-preview-label="<?php e(t('Bookmarking this page:')); ?>"
|
||||||
aria-labelledby="app-bookmark-dialog-title"
|
aria-labelledby="app-bookmark-dialog-title"
|
||||||
>
|
>
|
||||||
<article class="app-bookmark-dialog">
|
<article class="app-bookmark-dialog">
|
||||||
<header>
|
<header>
|
||||||
<h2 id="app-bookmark-dialog-title"><?php e(t('Bookmarks')); ?></h2>
|
<h2 id="app-bookmark-dialog-title"><?php e(t('Add bookmark')); ?></h2>
|
||||||
<button type="button" class="close" data-bookmark-dialog-close aria-label="<?php e(t('Close')); ?>" title="<?php e(t('Close')); ?>"></button>
|
<button type="button" class="close" data-bookmark-dialog-close aria-label="<?php e(t('Close')); ?>" title="<?php e(t('Close')); ?>"></button>
|
||||||
</header>
|
</header>
|
||||||
<form data-bookmark-save-form>
|
<form data-bookmark-save-form>
|
||||||
|
<small class="app-bookmark-url-preview" data-bookmark-url-preview hidden></small>
|
||||||
<label for="bookmark-name"><?php e(t('Name')); ?></label>
|
<label for="bookmark-name"><?php e(t('Name')); ?></label>
|
||||||
<input type="text" id="bookmark-name" name="name" maxlength="120" required
|
<input type="text" id="bookmark-name" name="name" maxlength="120" required
|
||||||
placeholder="<?php e(t('Bookmark name')); ?>">
|
placeholder="<?php e(t('Bookmark name')); ?>"
|
||||||
<label for="bookmark-group"><?php e(t('Assign to group')); ?></label>
|
aria-describedby="bookmark-name-error">
|
||||||
|
<small class="app-bookmark-field-error" id="bookmark-name-error" data-field-error="name"></small>
|
||||||
|
<div class="app-bookmark-group-label-row">
|
||||||
|
<label for="bookmark-group"><?php e(t('Assign to group')); ?></label>
|
||||||
|
<button type="button" class="app-bookmark-new-group-link" data-bookmark-new-group>
|
||||||
|
<i class="bi bi-plus"></i> <?php e(t('New group')); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<select id="bookmark-group" name="group_id" data-bookmark-group-select>
|
<select id="bookmark-group" name="group_id" data-bookmark-group-select>
|
||||||
<option value=""><?php e(t('No group')); ?></option>
|
<option value=""><?php e(t('No group')); ?></option>
|
||||||
<?php foreach ($bookmarkGroups as $group):
|
<?php foreach ($bookmarkGroups as $group):
|
||||||
@@ -45,12 +79,9 @@ $bookmarkGroups = is_array($bookmarkData['groups'] ?? null) ? $bookmarkData['gro
|
|||||||
<option value="<?php e($groupId); ?>"><?php e($groupName); ?></option>
|
<option value="<?php e($groupId); ?>"><?php e($groupName); ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
<button type="button" class="outline secondary app-bookmark-new-group-btn" data-bookmark-new-group>
|
|
||||||
<i class="bi bi-plus"></i> <?php e(t('New group')); ?>
|
|
||||||
</button>
|
|
||||||
<footer>
|
<footer>
|
||||||
<button type="button" class="secondary outline" data-bookmark-dialog-close><?php e(t('Cancel')); ?></button>
|
<button type="button" class="secondary outline" data-bookmark-dialog-close><?php e(t('Cancel')); ?></button>
|
||||||
<button type="submit" class="app-action-success" data-bookmark-submit-label><?php e(t('Save')); ?></button>
|
<button type="submit" class="app-action-success" data-bookmark-submit-label><?php e(t('Add bookmark')); ?></button>
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
@@ -60,8 +91,9 @@ $bookmarkGroups = is_array($bookmarkData['groups'] ?? null) ? $bookmarkData['gro
|
|||||||
data-app-bookmark-group-dialog
|
data-app-bookmark-group-dialog
|
||||||
data-title-create="<?php e(t('New group')); ?>"
|
data-title-create="<?php e(t('New group')); ?>"
|
||||||
data-title-edit="<?php e(t('Edit group')); ?>"
|
data-title-edit="<?php e(t('Edit group')); ?>"
|
||||||
data-label-save="<?php e(t('Save')); ?>"
|
data-label-save="<?php e(t('Create group')); ?>"
|
||||||
data-label-update="<?php e(t('Save')); ?>"
|
data-label-update="<?php e(t('Save changes')); ?>"
|
||||||
|
data-label-saving="<?php e(t('Saving...')); ?>"
|
||||||
data-max-message="<?php e(t('Maximum groups reached')); ?>"
|
data-max-message="<?php e(t('Maximum groups reached')); ?>"
|
||||||
data-msg-created="<?php e(t('Group created')); ?>"
|
data-msg-created="<?php e(t('Group created')); ?>"
|
||||||
data-msg-updated="<?php e(t('Group updated')); ?>"
|
data-msg-updated="<?php e(t('Group updated')); ?>"
|
||||||
@@ -77,13 +109,18 @@ $bookmarkGroups = is_array($bookmarkData['groups'] ?? null) ? $bookmarkData['gro
|
|||||||
<form data-bookmark-group-save-form>
|
<form data-bookmark-group-save-form>
|
||||||
<label for="bookmark-group-name"><?php e(t('Group name')); ?></label>
|
<label for="bookmark-group-name"><?php e(t('Group name')); ?></label>
|
||||||
<input type="text" id="bookmark-group-name" name="name" maxlength="100" required
|
<input type="text" id="bookmark-group-name" name="name" maxlength="100" required
|
||||||
placeholder="<?php e(t('Group name')); ?>">
|
placeholder="<?php e(t('Group name')); ?>"
|
||||||
|
aria-describedby="bookmark-group-name-error">
|
||||||
|
<small class="app-bookmark-field-error" id="bookmark-group-name-error" data-field-error="group-name"></small>
|
||||||
<label><?php e(t('Icon')); ?></label>
|
<label><?php e(t('Icon')); ?></label>
|
||||||
<div class="app-bookmark-group-icon-picker" data-bookmark-group-icon-picker>
|
<div class="app-bookmark-group-icon-picker" data-bookmark-group-icon-picker>
|
||||||
<?php foreach (BookmarkService::allowedGroupIcons() as $allowedIcon): ?>
|
<?php foreach (BookmarkService::allowedGroupIcons() as $allowedIcon):
|
||||||
|
$readableLabel = $iconLabels[$allowedIcon] ?? $allowedIcon;
|
||||||
|
?>
|
||||||
<button type="button" class="app-bookmark-group-icon-option"
|
<button type="button" class="app-bookmark-group-icon-option"
|
||||||
data-icon="<?php e($allowedIcon); ?>"
|
data-icon="<?php e($allowedIcon); ?>"
|
||||||
aria-label="<?php e($allowedIcon); ?>">
|
aria-label="<?php e($readableLabel); ?>"
|
||||||
|
aria-pressed="false">
|
||||||
<i class="bi <?php e($allowedIcon); ?>"></i>
|
<i class="bi <?php e($allowedIcon); ?>"></i>
|
||||||
</button>
|
</button>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -91,7 +128,7 @@ $bookmarkGroups = is_array($bookmarkData['groups'] ?? null) ? $bookmarkData['gro
|
|||||||
<input type="hidden" name="icon" value="bi-folder">
|
<input type="hidden" name="icon" value="bi-folder">
|
||||||
<footer>
|
<footer>
|
||||||
<button type="button" class="secondary outline" data-bookmark-group-dialog-close><?php e(t('Cancel')); ?></button>
|
<button type="button" class="secondary outline" data-bookmark-group-dialog-close><?php e(t('Cancel')); ?></button>
|
||||||
<button type="submit" class="app-action-success" data-bookmark-group-submit-label><?php e(t('Save')); ?></button>
|
<button type="submit" class="app-action-success" data-bookmark-group-submit-label><?php e(t('Create group')); ?></button>
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -56,6 +56,68 @@
|
|||||||
padding-top: var(--app-spacing);
|
padding-top: var(--app-spacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* URL preview */
|
||||||
|
.app-bookmark-url-preview {
|
||||||
|
color: var(--app-muted-color);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline field errors */
|
||||||
|
.app-bookmark-field-error {
|
||||||
|
color: var(--app-action-danger-background);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bookmark-field-error:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog[data-app-bookmark-dialog] form input[aria-invalid="true"],
|
||||||
|
dialog[data-app-bookmark-group-dialog] form input[aria-invalid="true"] {
|
||||||
|
border-color: var(--app-action-danger-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Group label row with inline new-group link */
|
||||||
|
.app-bookmark-group-label-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bookmark-new-group-link {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--app-primary);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bookmark-new-group-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bookmark-new-group-link:focus-visible {
|
||||||
|
outline: 2px solid var(--app-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bookmark-new-group-link > i {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Delete button */
|
||||||
.app-bookmark-delete-btn {
|
.app-bookmark-delete-btn {
|
||||||
color: var(--app-action-danger-background);
|
color: var(--app-action-danger-background);
|
||||||
border-color: var(--app-action-danger-background);
|
border-color: var(--app-action-danger-background);
|
||||||
@@ -67,10 +129,7 @@
|
|||||||
color: var(--app-action-danger-color);
|
color: var(--app-action-danger-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-bookmark-new-group-btn {
|
/* Icon picker */
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-bookmark-group-icon-picker {
|
.app-bookmark-group-icon-picker {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -53,12 +53,16 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
const closeBookmarkBtns = bookmarkDialog.querySelectorAll('[data-bookmark-dialog-close]');
|
const closeBookmarkBtns = bookmarkDialog.querySelectorAll('[data-bookmark-dialog-close]');
|
||||||
const submitBtn = bookmarkDialog.querySelector('[data-bookmark-submit-label]');
|
const submitBtn = bookmarkDialog.querySelector('[data-bookmark-submit-label]');
|
||||||
const titleEl = bookmarkDialog.querySelector('#app-bookmark-dialog-title');
|
const titleEl = bookmarkDialog.querySelector('#app-bookmark-dialog-title');
|
||||||
|
const urlPreviewEl = bookmarkDialog.querySelector('[data-bookmark-url-preview]');
|
||||||
|
const nameErrorEl = bookmarkForm.querySelector('[data-field-error="name"]');
|
||||||
|
|
||||||
const maxBookmarkMessage = bookmarkDialog.dataset.maxMessage || '';
|
const maxBookmarkMessage = bookmarkDialog.dataset.maxMessage || '';
|
||||||
const bookmarkMessageSaved = bookmarkDialog.dataset.msgSaved || 'Bookmark saved';
|
const bookmarkMessageSaved = bookmarkDialog.dataset.msgSaved || 'Bookmark saved';
|
||||||
const bookmarkMessageUpdated = bookmarkDialog.dataset.msgUpdated || 'Bookmark updated';
|
const bookmarkMessageUpdated = bookmarkDialog.dataset.msgUpdated || 'Bookmark updated';
|
||||||
const bookmarkMessageErrorGeneric = bookmarkDialog.dataset.msgErrorGeneric || 'Bookmark action failed';
|
const bookmarkMessageErrorGeneric = bookmarkDialog.dataset.msgErrorGeneric || 'Bookmark action failed';
|
||||||
const bookmarkMessageNameRequired = bookmarkDialog.dataset.msgNameRequired || 'Bookmark name is required';
|
const bookmarkMessageNameRequired = bookmarkDialog.dataset.msgNameRequired || 'Bookmark name is required';
|
||||||
|
const labelSaving = bookmarkDialog.dataset.labelSaving || 'Saving...';
|
||||||
|
const urlPreviewLabel = bookmarkDialog.dataset.urlPreviewLabel || 'Bookmarking this page:';
|
||||||
const bookmarkApiErrorMessages = {
|
const bookmarkApiErrorMessages = {
|
||||||
name_required: bookmarkDialog.dataset.msgNameRequired || bookmarkMessageErrorGeneric,
|
name_required: bookmarkDialog.dataset.msgNameRequired || bookmarkMessageErrorGeneric,
|
||||||
url_required: bookmarkDialog.dataset.msgUrlRequired || bookmarkMessageErrorGeneric,
|
url_required: bookmarkDialog.dataset.msgUrlRequired || bookmarkMessageErrorGeneric,
|
||||||
@@ -70,11 +74,55 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
|
|
||||||
let editingBookmarkId = null;
|
let editingBookmarkId = null;
|
||||||
let bookmarkReturnState = null;
|
let bookmarkReturnState = null;
|
||||||
|
let savedSubmitLabel = '';
|
||||||
|
|
||||||
let groupEditingId = null;
|
let groupEditingId = null;
|
||||||
let groupMode = 'create';
|
let groupMode = 'create';
|
||||||
let groupSource = 'sidebar';
|
let groupSource = 'sidebar';
|
||||||
|
|
||||||
|
// --- Inline validation helpers ---
|
||||||
|
|
||||||
|
const showFieldError = (input, errorEl, message) => {
|
||||||
|
if (errorEl instanceof HTMLElement) {
|
||||||
|
errorEl.textContent = message;
|
||||||
|
}
|
||||||
|
if (input instanceof HTMLElement) {
|
||||||
|
input.setAttribute('aria-invalid', 'true');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFieldError = (input, errorEl) => {
|
||||||
|
if (errorEl instanceof HTMLElement) {
|
||||||
|
errorEl.textContent = '';
|
||||||
|
}
|
||||||
|
if (input instanceof HTMLElement) {
|
||||||
|
input.removeAttribute('aria-invalid');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear inline error when user starts typing
|
||||||
|
if (nameInput instanceof HTMLElement) {
|
||||||
|
nameInput.addEventListener('input', () => {
|
||||||
|
clearFieldError(nameInput, nameErrorEl);
|
||||||
|
}, { signal });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- URL preview ---
|
||||||
|
|
||||||
|
const updateUrlPreview = (show) => {
|
||||||
|
if (!(urlPreviewEl instanceof HTMLElement)) return;
|
||||||
|
if (show) {
|
||||||
|
const url = currentRelativeUrl();
|
||||||
|
urlPreviewEl.textContent = urlPreviewLabel + ' ' + (url || '/');
|
||||||
|
urlPreviewEl.hidden = false;
|
||||||
|
} else {
|
||||||
|
urlPreviewEl.hidden = true;
|
||||||
|
urlPreviewEl.textContent = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Core helpers ---
|
||||||
|
|
||||||
const syncModalBodyClass = () => {
|
const syncModalBodyClass = () => {
|
||||||
const isOpen = bookmarkDialog.open || (hasGroupDialog && groupDialog.open);
|
const isOpen = bookmarkDialog.open || (hasGroupDialog && groupDialog.open);
|
||||||
document.body.classList.toggle('modal-is-open', isOpen);
|
document.body.classList.toggle('modal-is-open', isOpen);
|
||||||
@@ -111,6 +159,7 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
bookmarkForm.removeAttribute('aria-busy');
|
bookmarkForm.removeAttribute('aria-busy');
|
||||||
|
clearFieldError(nameInput, nameErrorEl);
|
||||||
syncModalBodyClass();
|
syncModalBodyClass();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,7 +176,14 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
|
|
||||||
const setBookmarkSubmitPending = (pending) => {
|
const setBookmarkSubmitPending = (pending) => {
|
||||||
if (submitBtn instanceof HTMLButtonElement) {
|
if (submitBtn instanceof HTMLButtonElement) {
|
||||||
submitBtn.disabled = pending;
|
if (pending) {
|
||||||
|
savedSubmitLabel = submitBtn.textContent || '';
|
||||||
|
submitBtn.textContent = labelSaving;
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
} else {
|
||||||
|
submitBtn.textContent = savedSubmitLabel || submitBtn.textContent;
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bookmarkForm.setAttribute('aria-busy', pending ? 'true' : 'false');
|
bookmarkForm.setAttribute('aria-busy', pending ? 'true' : 'false');
|
||||||
};
|
};
|
||||||
@@ -142,15 +198,18 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
const applyBookmarkModeUi = (isEditMode) => {
|
const applyBookmarkModeUi = (isEditMode) => {
|
||||||
if (titleEl) {
|
if (titleEl) {
|
||||||
titleEl.textContent = isEditMode
|
titleEl.textContent = isEditMode
|
||||||
? (bookmarkDialog.dataset.titleEdit || 'Bookmarks')
|
? (bookmarkDialog.dataset.titleEdit || 'Edit bookmark')
|
||||||
: (bookmarkDialog.dataset.titleCreate || 'Bookmarks');
|
: (bookmarkDialog.dataset.titleCreate || 'Add bookmark');
|
||||||
}
|
}
|
||||||
setBookmarkSubmitLabel(isEditMode
|
setBookmarkSubmitLabel(isEditMode
|
||||||
? (bookmarkDialog.dataset.labelUpdate || 'Save')
|
? (bookmarkDialog.dataset.labelUpdate || 'Save changes')
|
||||||
: (bookmarkDialog.dataset.labelSave || 'Save'));
|
: (bookmarkDialog.dataset.labelSave || 'Add bookmark'));
|
||||||
|
updateUrlPreview(!isEditMode);
|
||||||
};
|
};
|
||||||
|
|
||||||
const configureBookmarkModeFromTopbar = () => {
|
const configureBookmarkModeFromTopbar = () => {
|
||||||
|
clearFieldError(nameInput, nameErrorEl);
|
||||||
|
|
||||||
if (!(openBtn instanceof HTMLButtonElement)) {
|
if (!(openBtn instanceof HTMLButtonElement)) {
|
||||||
applyBookmarkModeUi(false);
|
applyBookmarkModeUi(false);
|
||||||
editingBookmarkId = null;
|
editingBookmarkId = null;
|
||||||
@@ -275,7 +334,8 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
|
|
||||||
const name = nameInput instanceof HTMLInputElement ? nameInput.value.trim() : '';
|
const name = nameInput instanceof HTMLInputElement ? nameInput.value.trim() : '';
|
||||||
if (!name) {
|
if (!name) {
|
||||||
showAsyncFlash('warning', bookmarkMessageNameRequired);
|
showFieldError(nameInput, nameErrorEl, bookmarkMessageNameRequired);
|
||||||
|
if (nameInput instanceof HTMLInputElement) nameInput.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,12 +425,14 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
const groupCloseBtns = groupDialog.querySelectorAll('[data-bookmark-group-dialog-close]');
|
const groupCloseBtns = groupDialog.querySelectorAll('[data-bookmark-group-dialog-close]');
|
||||||
const groupTitleEl = groupDialog.querySelector('#app-bookmark-group-dialog-title');
|
const groupTitleEl = groupDialog.querySelector('#app-bookmark-group-dialog-title');
|
||||||
const groupSubmitBtn = groupDialog.querySelector('[data-bookmark-group-submit-label]');
|
const groupSubmitBtn = groupDialog.querySelector('[data-bookmark-group-submit-label]');
|
||||||
|
const groupNameErrorEl = groupForm.querySelector('[data-field-error="group-name"]');
|
||||||
|
|
||||||
const groupMessageCreated = groupDialog.dataset.msgCreated || 'Group created';
|
const groupMessageCreated = groupDialog.dataset.msgCreated || 'Group created';
|
||||||
const groupMessageUpdated = groupDialog.dataset.msgUpdated || 'Group updated';
|
const groupMessageUpdated = groupDialog.dataset.msgUpdated || 'Group updated';
|
||||||
const groupMessageErrorGeneric = groupDialog.dataset.msgErrorGeneric || 'Group action failed';
|
const groupMessageErrorGeneric = groupDialog.dataset.msgErrorGeneric || 'Group action failed';
|
||||||
const groupMessageNameRequired = groupDialog.dataset.msgNameRequired || 'Group name is required';
|
const groupMessageNameRequired = groupDialog.dataset.msgNameRequired || 'Group name is required';
|
||||||
const maxGroupMessage = groupDialog.dataset.maxMessage || 'Maximum groups reached';
|
const maxGroupMessage = groupDialog.dataset.maxMessage || 'Maximum groups reached';
|
||||||
|
const groupLabelSaving = groupDialog.dataset.labelSaving || 'Saving...';
|
||||||
const groupApiErrorMessages = {
|
const groupApiErrorMessages = {
|
||||||
name_required: groupMessageNameRequired,
|
name_required: groupMessageNameRequired,
|
||||||
not_found: groupMessageErrorGeneric,
|
not_found: groupMessageErrorGeneric,
|
||||||
@@ -378,11 +440,20 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
update_failed: groupMessageErrorGeneric
|
update_failed: groupMessageErrorGeneric
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let savedGroupSubmitLabel = '';
|
||||||
|
|
||||||
const resolveGroupApiErrorMessage = (errorCode) => {
|
const resolveGroupApiErrorMessage = (errorCode) => {
|
||||||
if (!errorCode) return groupMessageErrorGeneric;
|
if (!errorCode) return groupMessageErrorGeneric;
|
||||||
return groupApiErrorMessages[errorCode] || groupMessageErrorGeneric;
|
return groupApiErrorMessages[errorCode] || groupMessageErrorGeneric;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Clear group name inline error on input
|
||||||
|
if (groupNameInput instanceof HTMLElement) {
|
||||||
|
groupNameInput.addEventListener('input', () => {
|
||||||
|
clearFieldError(groupNameInput, groupNameErrorEl);
|
||||||
|
}, { signal });
|
||||||
|
}
|
||||||
|
|
||||||
const closeGroupDialog = () => {
|
const closeGroupDialog = () => {
|
||||||
try {
|
try {
|
||||||
if (groupDialog.open) groupDialog.close();
|
if (groupDialog.open) groupDialog.close();
|
||||||
@@ -390,6 +461,7 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
groupForm.removeAttribute('aria-busy');
|
groupForm.removeAttribute('aria-busy');
|
||||||
|
clearFieldError(groupNameInput, groupNameErrorEl);
|
||||||
syncModalBodyClass();
|
syncModalBodyClass();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -406,7 +478,14 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
|
|
||||||
const setGroupSubmitPending = (pending) => {
|
const setGroupSubmitPending = (pending) => {
|
||||||
if (groupSubmitBtn instanceof HTMLButtonElement) {
|
if (groupSubmitBtn instanceof HTMLButtonElement) {
|
||||||
groupSubmitBtn.disabled = pending;
|
if (pending) {
|
||||||
|
savedGroupSubmitLabel = groupSubmitBtn.textContent || '';
|
||||||
|
groupSubmitBtn.textContent = groupLabelSaving;
|
||||||
|
groupSubmitBtn.disabled = true;
|
||||||
|
} else {
|
||||||
|
groupSubmitBtn.textContent = savedGroupSubmitLabel || groupSubmitBtn.textContent;
|
||||||
|
groupSubmitBtn.disabled = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
groupForm.setAttribute('aria-busy', pending ? 'true' : 'false');
|
groupForm.setAttribute('aria-busy', pending ? 'true' : 'false');
|
||||||
};
|
};
|
||||||
@@ -424,11 +503,11 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
groupIconInput.value = value;
|
groupIconInput.value = value;
|
||||||
}
|
}
|
||||||
if (groupIconPicker instanceof HTMLElement) {
|
if (groupIconPicker instanceof HTMLElement) {
|
||||||
groupIconPicker.querySelectorAll('.app-bookmark-icon-active').forEach((item) => item.classList.remove('app-bookmark-icon-active'));
|
groupIconPicker.querySelectorAll('[data-icon]').forEach((item) => {
|
||||||
const selected = groupIconPicker.querySelector(`[data-icon="${value}"]`);
|
const isActive = item.dataset.icon === value;
|
||||||
if (selected instanceof HTMLElement) {
|
item.classList.toggle('app-bookmark-icon-active', isActive);
|
||||||
selected.classList.add('app-bookmark-icon-active');
|
item.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -460,6 +539,7 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
bookmarkReturnState = groupSource === 'bookmark' ? restoreState : null;
|
bookmarkReturnState = groupSource === 'bookmark' ? restoreState : null;
|
||||||
|
|
||||||
groupForm.reset();
|
groupForm.reset();
|
||||||
|
clearFieldError(groupNameInput, groupNameErrorEl);
|
||||||
setGroupIcon(icon);
|
setGroupIcon(icon);
|
||||||
if (groupNameInput instanceof HTMLInputElement) {
|
if (groupNameInput instanceof HTMLInputElement) {
|
||||||
groupNameInput.value = String(name || '');
|
groupNameInput.value = String(name || '');
|
||||||
@@ -471,8 +551,8 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
: (groupDialog.dataset.titleCreate || 'New group');
|
: (groupDialog.dataset.titleCreate || 'New group');
|
||||||
}
|
}
|
||||||
setGroupSubmitLabel(groupMode === 'edit'
|
setGroupSubmitLabel(groupMode === 'edit'
|
||||||
? (groupDialog.dataset.labelUpdate || 'Save')
|
? (groupDialog.dataset.labelUpdate || 'Save changes')
|
||||||
: (groupDialog.dataset.labelSave || 'Save'));
|
: (groupDialog.dataset.labelSave || 'Create group'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGroupCancel = () => {
|
const handleGroupCancel = () => {
|
||||||
@@ -498,7 +578,7 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (newGroupBtn instanceof HTMLButtonElement) {
|
if (newGroupBtn instanceof HTMLElement) {
|
||||||
newGroupBtn.addEventListener('click', (event) => {
|
newGroupBtn.addEventListener('click', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
openCreateGroupFromBookmarkDialog();
|
openCreateGroupFromBookmarkDialog();
|
||||||
@@ -563,7 +643,8 @@ export function initBookmarkSave(root = document, options = {}) {
|
|||||||
|
|
||||||
const name = groupNameInput instanceof HTMLInputElement ? groupNameInput.value.trim() : '';
|
const name = groupNameInput instanceof HTMLInputElement ? groupNameInput.value.trim() : '';
|
||||||
if (!name) {
|
if (!name) {
|
||||||
showAsyncFlash('warning', groupMessageNameRequired);
|
showFieldError(groupNameInput, groupNameErrorEl, groupMessageNameRequired);
|
||||||
|
if (groupNameInput instanceof HTMLInputElement) groupNameInput.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const icon = groupIconInput instanceof HTMLInputElement ? groupIconInput.value : 'bi-folder';
|
const icon = groupIconInput instanceof HTMLInputElement ? groupIconInput.value : 'bi-folder';
|
||||||
|
|||||||
Reference in New Issue
Block a user