auto save
This commit is contained in:
+36
-3
@@ -577,7 +577,7 @@ def create_app():
|
||||
def notes_create():
|
||||
title = request.form.get("title", "").strip()
|
||||
content = request.form.get("content", "").strip()
|
||||
if not title or not content:
|
||||
if not title:
|
||||
return redirect(url_for("notes_page"))
|
||||
note = Note(
|
||||
user_id=current_user.id,
|
||||
@@ -588,13 +588,40 @@ def create_app():
|
||||
user_db.session.commit()
|
||||
return _note_redirect(note.id)
|
||||
|
||||
@app.route("/notes/autosave", methods=["POST"])
|
||||
@login_required
|
||||
def notes_autosave():
|
||||
data = request.get_json(silent=True)
|
||||
if not data:
|
||||
return jsonify({"error": "json required"}), 400
|
||||
note_id = data.get("id")
|
||||
title = (data.get("title") or "").strip()[:200]
|
||||
content = (data.get("content") or "").strip()[:2000]
|
||||
if not title:
|
||||
return jsonify({"error": "title required"}), 400
|
||||
if note_id:
|
||||
note = _note_or_404(int(note_id))
|
||||
note.title = title
|
||||
note.content = content
|
||||
note.touch()
|
||||
user_db.session.commit()
|
||||
return jsonify({"id": note.id, "status": "saved"})
|
||||
note = Note(
|
||||
user_id=current_user.id,
|
||||
title=title,
|
||||
content=content,
|
||||
)
|
||||
user_db.session.add(note)
|
||||
user_db.session.commit()
|
||||
return jsonify({"id": note.id, "status": "created"})
|
||||
|
||||
@app.route("/notes/<int:note_id>", methods=["POST"])
|
||||
@login_required
|
||||
def notes_update(note_id):
|
||||
note = _note_or_404(note_id)
|
||||
title = request.form.get("title", "").strip()
|
||||
content = request.form.get("content", "").strip()
|
||||
if title and content:
|
||||
if title:
|
||||
note.title = title[:200]
|
||||
note.content = content[:2000]
|
||||
note.touch()
|
||||
@@ -628,11 +655,17 @@ def create_app():
|
||||
def notes_update_item(note_id, item_id):
|
||||
note = _note_or_404(note_id)
|
||||
item = NoteItem.query.filter_by(id=item_id, note_id=note.id).first_or_404()
|
||||
text = request.form.get("text", "").strip()
|
||||
if request.is_json:
|
||||
data = request.get_json(silent=True) or {}
|
||||
text = (data.get("text") or "").strip()
|
||||
else:
|
||||
text = request.form.get("text", "").strip()
|
||||
if text:
|
||||
item.text = text[:255]
|
||||
note.touch()
|
||||
user_db.session.commit()
|
||||
if request.is_json:
|
||||
return jsonify({"id": item.id, "status": "saved"})
|
||||
return _note_redirect(note.id)
|
||||
|
||||
@app.route(
|
||||
|
||||
+55
-3
@@ -400,9 +400,61 @@ textarea {
|
||||
gap: 0.5em;
|
||||
}
|
||||
.add-item-form input { flex: 1; }
|
||||
.note-delete-form { margin-top: 0.75rem; }
|
||||
.note-delete-form button { background: var(--mc-red); color: var(--mc-bright); }
|
||||
.note-delete-form button:hover { background: var(--mc-red-bright); }
|
||||
.note-menu-btn {
|
||||
background: transparent;
|
||||
color: var(--mc-dim);
|
||||
border: 2px solid var(--mc-dim);
|
||||
box-shadow: none;
|
||||
padding: 0.1em 0.4em;
|
||||
font-size: 1em;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.note-menu-btn:hover { color: var(--mc-bright); border-color: var(--mc-bright); }
|
||||
|
||||
.note-title-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.note-save-status {
|
||||
font-size: 0.8em;
|
||||
color: var(--mc-dim);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.note-save-status.saved { color: var(--mc-yellow); }
|
||||
.note-save-status.unsaved { color: var(--mc-red); }
|
||||
|
||||
.note-menu-dropdown {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
background: var(--mc-deep);
|
||||
border: 3px double var(--mc-bright);
|
||||
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.45);
|
||||
min-width: 140px;
|
||||
z-index: 10;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
.note-menu-dropdown button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0.4em 0.8em;
|
||||
color: var(--mc-bright);
|
||||
font: inherit;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.note-menu-dropdown button:hover { background: var(--mc-cyan); color: var(--mc-black); }
|
||||
|
||||
|
||||
/* --- Notifications --- */
|
||||
|
||||
+142
-13
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user