auto save

This commit is contained in:
2026-11-07 16:54:40 +01:00
parent 0a9da4f9c5
commit df06a4129f
3 changed files with 233 additions and 19 deletions
+142 -13
View File
@@ -24,13 +24,13 @@
<div class="panel">
<h2>New note</h2>
<form method="post" action="{{ url_for('notes_create') }}">
<form method="post" action="{{ url_for('notes_create') }}" data-note-id="new">
<label for="new-note-title">Title</label>
<input type="text" id="new-note-title" name="title" maxlength="200"
placeholder="Note title" required>
<label for="new-note-content">Text</label>
<textarea id="new-note-content" name="content" rows="3" maxlength="2000"
placeholder="Write a short note..." required></textarea>
placeholder="Write a short note..."></textarea>
<button type="submit">[ Add note ]</button>
</form>
</div>
@@ -41,15 +41,21 @@
{% for note in notes %}
<section class="note-card" id="note-{{ note.id }}">
<form method="post" action="{{ url_for('notes_update', note_id=note.id) }}">
<label for="note-title-{{ note.id }}">Title</label>
<form method="post" action="{{ url_for('notes_update', note_id=note.id) }}" data-note-id="{{ note.id }}">
<div class="note-title-row">
<label for="note-title-{{ note.id }}">Title</label>
<span class="note-save-status"></span>
<button class="note-menu-btn" type="button" aria-label="More options">⋮</button>
<div class="note-menu-dropdown hidden">
<button type="button" class="menu-delete" data-note-id="{{ note.id }}">Delete note</button>
</div>
</div>
<input type="text" id="note-title-{{ note.id }}" name="title"
value="{{ note.title }}" maxlength="200" required>
<label for="note-content-{{ note.id }}">Text</label>
<textarea id="note-content-{{ note.id }}" name="content" rows="2"
maxlength="2000" required>{{ note.content }}</textarea>
maxlength="2000">{{ note.content }}</textarea>
<div class="note-actions">
<button type="submit">[ Save ]</button>
<span class="note-meta">{{ note.updated_at.strftime("%Y-%m-%d %H:%M") }}</span>
</div>
</form>
@@ -64,14 +70,13 @@
{% if item.is_done %}checked{% endif %}
onchange="this.form.submit()"
aria-label="Mark '{{ item.text }}' as done">
<noscript><button type="submit">[ Update ]</button></noscript>
</form>
<form method="post"
action="{{ url_for('notes_update_item', note_id=note.id, item_id=item.id) }}"
class="item-edit-form">
<input type="text" name="text" value="{{ item.text }}"
maxlength="255" required aria-label="Checklist item text">
<button type="submit">[ Save ]</button>
maxlength="255" required aria-label="Checklist item text"
data-autosave-url="{{ url_for('notes_update_item', note_id=note.id, item_id=item.id) }}">
</form>
<form method="post"
action="{{ url_for('notes_delete_item', note_id=note.id, item_id=item.id) }}"
@@ -90,15 +95,139 @@
<button type="submit">[ Add item ]</button>
</form>
<form method="post" action="{{ url_for('notes_delete', note_id=note.id) }}"
class="note-delete-form">
<button type="submit">[ Delete note ]</button>
</form>
<form id="delete-note-form-{{ note.id }}" method="post"
action="{{ url_for('notes_delete', note_id=note.id) }}"
class="hidden"></form>
</section>
{% endfor %}
<footer class="statusbar">
<span>{{ user.username }}@yggdrasil:~/notes$</span>
</footer>
<script>
(function () {
var timers = {};
function autosave(form, url, payload) {
var id = form.getAttribute('data-note-id');
var key = id === 'new' ? 'new' : id;
clearTimeout(timers[key]);
timers[key] = setTimeout(function () {
fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
}).then(function (r) { return r.json(); }).then(function (data) {
if (data && data.id && id === 'new') {
form.setAttribute('data-note-id', data.id);
}
});
}, 3000);
}
document.querySelectorAll('form[data-note-id]').forEach(function (form) {
var id = form.getAttribute('data-note-id');
var key = id === 'new' ? 'new' : id;
var statusEl = form.querySelector('.note-save-status');
var dirty = false;
function setStatus(text, cls) {
if (!statusEl) return;
statusEl.textContent = text;
statusEl.className = 'note-save-status' + (cls ? ' ' + cls : '');
}
form.querySelectorAll('input[name="title"], textarea[name="content"]').forEach(function (input) {
input.addEventListener('input', function () {
dirty = true;
setStatus('not saved', 'unsaved');
clearTimeout(timers[key]);
timers[key] = setTimeout(function () {
var title = form.querySelector('input[name="title"]').value;
var content = form.querySelector('textarea[name="content"]').value;
fetch('/notes/autosave', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
id: id === 'new' ? null : parseInt(id, 10),
title: title,
content: content
})
}).then(function (r) { return r.json(); }).then(function (data) {
dirty = false;
setStatus('saved', 'saved');
setTimeout(function () {
if (!dirty) setStatus('', '');
}, 3000);
if (data && data.id && id === 'new') {
form.setAttribute('data-note-id', data.id);
}
});
}, 3000);
});
});
});
document.querySelectorAll('.item-edit-form input[name="text"]').forEach(function (input) {
var form = input.closest('form');
var url = input.getAttribute('data-autosave-url');
var card = input.closest('.note-card');
var statusEl = card.querySelector('.note-save-status');
var key = form.getAttribute('action') + ':' + input.name;
var dirty = false;
function setStatus(text, cls) {
if (!statusEl) return;
statusEl.textContent = text;
statusEl.className = 'note-save-status' + (cls ? ' ' + cls : '');
}
input.addEventListener('input', function () {
dirty = true;
setStatus('not saved', 'unsaved');
clearTimeout(timers[key]);
timers[key] = setTimeout(function () {
var text = input.value;
fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ text: text })
}).then(function (r) { return r.json(); }).then(function () {
dirty = false;
setStatus('saved', 'saved');
setTimeout(function () {
if (!dirty) setStatus('', '');
}, 3000);
});
}, 3000);
});
});
document.querySelectorAll('.note-menu-btn').forEach(function (btn) {
var card = btn.closest('.note-card');
var dropdown = card.querySelector('.note-menu-dropdown');
var deleteBtn = card.querySelector('.menu-delete');
var form = card.querySelector('form[id^="delete-note-form-"]');
btn.addEventListener('click', function (e) {
e.stopPropagation();
document.querySelectorAll('.note-menu-dropdown').forEach(function (d) {
if (d !== dropdown) d.classList.add('hidden');
});
dropdown.classList.toggle('hidden');
});
deleteBtn.addEventListener('click', function (e) {
e.stopPropagation();
if (confirm('Are you sure you want to delete this note?')) {
form.submit();
}
});
});
document.addEventListener('click', function () {
document.querySelectorAll('.note-menu-dropdown').forEach(function (d) {
d.classList.add('hidden');
});
});
})();
</script>
</body>
</html>