Files
linkding/bookmarks/templates/bookmarks/form.html
Sascha Ißbrücker 4e8318d0ae Improve bookmark form accessibility (#1116)
* Bump Django

* Render error messages in English

* Remove unused USE_L10N option

* Associate errors and help texts with form fields

* Make checkbox inputs clickable

* Change cancel button text

* Fix tests
2025-07-03 08:44:41 +02:00

251 lines
10 KiB
HTML

{% load widget_tweaks %}
{% load static %}
{% load shared %}
<div class="bookmarks-form">
{% csrf_token %}
{{ form.auto_close|attr:"type:hidden" }}
<div class="form-group {% if form.url.errors %}has-error{% endif %}">
<label for="{{ form.url.id_for_label }}" class="form-label">URL</label>
<div class="has-icon-right">
{{ form.url|form_field:"validation"|add_class:"form-input"|attr:"autofocus" }}
<i class="form-icon loading"></i>
</div>
{% if form.url.errors %}
<div class="form-input-hint">
{{ form.url.errors }}
</div>
{% endif %}
<div class="form-input-hint bookmark-exists">
This URL is already bookmarked.
The form has been pre-filled with the existing bookmark, and saving the form will update the existing bookmark.
</div>
</div>
<div class="form-group" ld-tag-autocomplete>
<label for="{{ form.tag_string.id_for_label }}" class="form-label">Tags</label>
{{ form.tag_string|form_field:"help"|add_class:"form-input"|attr:"autocomplete:off"|attr:"autocapitalize:off" }}
<div id="{{ form.tag_string.auto_id }}_help" class="form-input-hint">
Enter any number of tags separated by space and <strong>without</strong> the hash (#).
If a tag does not exist it will be automatically created.
</div>
<div class="form-input-hint auto-tags"></div>
{{ form.tag_string.errors }}
</div>
<div class="form-group">
<div class="d-flex justify-between align-baseline">
<label for="{{ form.title.id_for_label }}" class="form-label">Title</label>
<div class="flex">
<button id="refresh-button" class="btn btn-link suffix-button" type="button">Refresh from website</button>
<button ld-clear-button data-for="{{ form.title.id_for_label }}"
class="ml-2 btn btn-link suffix-button clear-button"
type="button">Clear
</button>
</div>
</div>
{{ form.title|add_class:"form-input"|attr:"autocomplete:off" }}
{{ form.title.errors }}
</div>
<div class="form-group">
<div class="d-flex justify-between align-baseline">
<label for="{{ form.description.id_for_label }}" class="form-label">Description</label>
<button ld-clear-button data-for="{{ form.description.id_for_label }}"
class="btn btn-link suffix-button clear-button"
type="button">Clear
</button>
</div>
{{ form.description|add_class:"form-input"|attr:"rows:3" }}
{{ form.description.errors }}
</div>
<div class="form-group">
<details class="notes"{% if form.has_notes %} open{% endif %}>
<summary>
<span class="form-label d-inline-block">Notes</span>
</summary>
<label for="{{ form.notes.id_for_label }}" class="text-assistive">Notes</label>
{{ form.notes|form_field:"help"|add_class:"form-input"|attr:"rows:8" }}
<div id="{{ form.notes.auto_id }}_help" class="form-input-hint">
Additional notes, supports Markdown.
</div>
</details>
{{ form.notes.errors }}
</div>
<div class="form-group">
<div class="form-checkbox">
{{ form.unread|form_field:"help" }}
<i class="form-icon"></i>
<label for="{{ form.unread.id_for_label }}">Mark as unread</label>
</div>
<div id="{{ form.unread.auto_id }}_help" class="form-input-hint">
Unread bookmarks can be filtered for, and marked as read after you had a chance to look at them.
</div>
</div>
{% if request.user_profile.enable_sharing %}
<div class="form-group">
<div class="form-checkbox">
{{ form.shared|form_field:"help" }}
<i class="form-icon"></i>
<label for="{{ form.shared.id_for_label }}">Share</label>
</div>
<div id="{{ form.shared.auto_id }}_help" class="form-input-hint">
{% if request.user_profile.enable_public_sharing %}
Share this bookmark with other registered users and anonymous users.
{% else %}
Share this bookmark with other registered users.
{% endif %}
</div>
</div>
{% endif %}
<div class="divider"></div>
<div class="form-group d-flex justify-between">
{% if form.is_auto_close %}
<input type="submit" value="Save and close" class="btn btn-primary btn-wide">
{% else %}
<input type="submit" value="Save" class="btn btn-primary btn btn-primary btn-wide">
{% endif %}
<a href="{{ return_url }}" class="btn">Cancel</a>
</div>
<script type="application/javascript">
/**
* - Pre-fill title and description with metadata from website as soon as URL changes
* - Show hint if URL is already bookmarked
*/
(function init() {
const urlInput = document.getElementById('{{ form.url.id_for_label }}');
const titleInput = document.getElementById('{{ form.title.id_for_label }}');
const descriptionInput = document.getElementById('{{ form.description.id_for_label }}');
const notesDetails = document.querySelector('form details.notes');
const notesInput = document.getElementById('{{ form.notes.id_for_label }}');
const unreadCheckbox = document.getElementById('{{ form.unread.id_for_label }}');
const sharedCheckbox = document.getElementById('{{ form.shared.id_for_label }}');
const refreshButton = document.getElementById('refresh-button');
const bookmarkExistsHint = document.querySelector('.form-input-hint.bookmark-exists');
const editedBookmarkId = {{ form.instance.id|default:0 }};
let isTitleModified = !!titleInput.value;
let isDescriptionModified = !!descriptionInput.value;
function toggleLoadingIcon(input, show) {
const icon = input.parentNode.querySelector('i.form-icon');
icon.style['visibility'] = show ? 'visible' : 'hidden';
}
function updateInput(input, value) {
if (!input) {
return;
}
input.value = value;
input.dispatchEvent(new Event('value-changed'));
}
function updateCheckbox(input, value) {
if (!input) {
return;
}
input.checked = value;
}
function checkUrl() {
if (!urlInput.value) {
return;
}
toggleLoadingIcon(urlInput, true);
const websiteUrl = encodeURIComponent(urlInput.value);
const requestUrl = `{% url 'linkding:api-root' %}bookmarks/check?url=${websiteUrl}`;
fetch(requestUrl)
.then(response => response.json())
.then(data => {
const metadata = data.metadata;
toggleLoadingIcon(urlInput, false);
// Display hint if URL is already bookmarked
const existingBookmark = data.bookmark;
bookmarkExistsHint.style['display'] = existingBookmark ? 'block' : 'none';
refreshButton.style['display'] = existingBookmark ? 'inline-block' : 'none';
// Prefill form with existing bookmark data
if (existingBookmark) {
// Workaround: tag input will be replaced by tag autocomplete, so
// defer getting the input until we need it
const tagsInput = document.getElementById('{{ form.tag_string.id_for_label }}');
bookmarkExistsHint.style['display'] = 'block';
notesDetails.open = !!existingBookmark.notes;
updateInput(titleInput, existingBookmark.title);
updateInput(descriptionInput, existingBookmark.description);
updateInput(notesInput, existingBookmark.notes);
updateInput(tagsInput, existingBookmark.tag_names.join(" "));
updateCheckbox(unreadCheckbox, existingBookmark.unread);
updateCheckbox(sharedCheckbox, existingBookmark.shared);
} else {
// Update title and description with website metadata, unless they have been modified
if (!isTitleModified) {
updateInput(titleInput, metadata.title);
}
if (!isDescriptionModified) {
updateInput(descriptionInput, metadata.description);
}
}
// Preview auto tags
const autoTags = data.auto_tags;
const autoTagsHint = document.querySelector('.form-input-hint.auto-tags');
if (autoTags.length > 0) {
autoTags.sort();
autoTagsHint.style['display'] = 'block';
autoTagsHint.innerHTML = `Auto tags: ${autoTags.join(" ")}`;
} else {
autoTagsHint.style['display'] = 'none';
}
});
}
function refreshMetadata() {
if (!urlInput.value) {
return;
}
toggleLoadingIcon(urlInput, true);
const websiteUrl = encodeURIComponent(urlInput.value);
const requestUrl = `{% url 'linkding:api-root' %}bookmarks/check?url=${websiteUrl}&ignore_cache=true`;
fetch(requestUrl)
.then(response => response.json())
.then(data => {
const metadata = data.metadata;
const existingBookmark = data.bookmark;
toggleLoadingIcon(urlInput, false);
if (metadata.title && metadata.title !== existingBookmark?.title) {
titleInput.value = metadata.title;
titleInput.classList.add("modified");
}
if (metadata.description && metadata.description !== existingBookmark?.description) {
descriptionInput.value = metadata.description;
descriptionInput.classList.add("modified");
}
});
}
refreshButton.addEventListener('click', refreshMetadata);
// Fetch website metadata when page loads and when URL changes, unless we are editing an existing bookmark
if (!editedBookmarkId) {
checkUrl();
urlInput.addEventListener('input', checkUrl);
titleInput.addEventListener('input', () => {
isTitleModified = true;
});
descriptionInput.addEventListener('input', () => {
isDescriptionModified = true;
});
} else {
refreshButton.style['display'] = 'inline-block';
}
})();
</script>
</div>