feat: autolink URLs in list items, link picker, and inline edit

- Add autolink Jinja2 filter to detect URLs and make them clickable
- Add link picker dropdown to insert existing link URLs into list item content
- Add inline edit with link picker on each list item row
- Apply autolink filter on list detail and focus available list items

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 17:58:21 +00:00
parent 7a2c6d3f2a
commit 97027f2de4
6 changed files with 199 additions and 8 deletions

View File

@@ -139,7 +139,7 @@
{% for li in available_list_items %}
<div class="list-row{% if loop.index > 25 %} focus-hidden-item hidden{% endif %}" style="padding:3px 8px;min-height:0;">
<span style="color:var(--muted);font-size:0.85rem;margin-right:4px">&#9776;</span>
<span class="row-title">{{ li.content }}</span>
<span class="row-title">{{ li.content|autolink }}</span>
{% if li.list_name %}<span class="row-tag" style="background:var(--purple);color:#fff">{{ li.list_name }}</span>{% endif %}
<form action="/focus/add" method="post" style="display:inline">
<input type="hidden" name="list_item_id" value="{{ li.id }}">

View File

@@ -37,7 +37,15 @@
<!-- Add item form -->
<form class="quick-add mt-3" action="/lists/{{ item.id }}/items/add" method="post">
<input type="text" name="content" placeholder="Add item..." required>
<input type="text" name="content" id="add-item-content" placeholder="Add item..." required style="flex:1">
{% if all_links %}
<select class="link-picker" data-target="add-item-content" style="max-width:180px">
<option value="">Insert link...</option>
{% for lnk in all_links %}
<option value="{{ lnk.url }}">{{ lnk.label }}</option>
{% endfor %}
</select>
{% endif %}
<button type="submit" class="btn btn-primary btn-sm">Add</button>
</form>
@@ -58,14 +66,29 @@
</form>
</div>
{% endif %}
<span class="row-title" style="{{ 'text-decoration: line-through; opacity: 0.6;' if li.completed }}">
{{ li.content }}
<span class="row-title" data-display-for="{{ li.id }}" style="{{ 'text-decoration: line-through; opacity: 0.6;' if li.completed }}">
{{ li.content|autolink }}
</span>
<div class="row-actions">
<div class="row-actions" data-display-for="{{ li.id }}">
<button type="button" class="btn btn-ghost btn-xs edit-item-btn" data-item-id="{{ li.id }}">Edit</button>
<form action="/lists/{{ item.id }}/items/{{ li.id }}/delete" method="post" style="display:inline">
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
</form>
</div>
<!-- Inline edit form -->
<form class="inline-edit-form" data-edit-for="{{ li.id }}" action="/lists/{{ item.id }}/items/{{ li.id }}/edit" method="post" style="display:none;">
<input type="text" name="content" id="edit-content-{{ li.id }}" value="{{ li.content }}" required style="flex:1">
{% if all_links %}
<select class="link-picker" data-target="edit-content-{{ li.id }}" style="max-width:160px">
<option value="">Insert link...</option>
{% for lnk in all_links %}
<option value="{{ lnk.url }}">{{ lnk.label }}</option>
{% endfor %}
</select>
{% endif %}
<button type="submit" class="btn btn-primary btn-xs">Save</button>
<button type="button" class="btn btn-ghost btn-xs cancel-edit-btn" data-item-id="{{ li.id }}">Cancel</button>
</form>
</div>
<!-- Child items -->
@@ -83,14 +106,29 @@
</form>
</div>
{% endif %}
<span class="row-title" style="{{ 'text-decoration: line-through; opacity: 0.6;' if child.completed }}">
{{ child.content }}
<span class="row-title" data-display-for="{{ child.id }}" style="{{ 'text-decoration: line-through; opacity: 0.6;' if child.completed }}">
{{ child.content|autolink }}
</span>
<div class="row-actions">
<div class="row-actions" data-display-for="{{ child.id }}">
<button type="button" class="btn btn-ghost btn-xs edit-item-btn" data-item-id="{{ child.id }}">Edit</button>
<form action="/lists/{{ item.id }}/items/{{ child.id }}/delete" method="post" style="display:inline">
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
</form>
</div>
<!-- Inline edit form -->
<form class="inline-edit-form" data-edit-for="{{ child.id }}" action="/lists/{{ item.id }}/items/{{ child.id }}/edit" method="post" style="display:none;">
<input type="text" name="content" id="edit-content-{{ child.id }}" value="{{ child.content }}" required style="flex:1">
{% if all_links %}
<select class="link-picker" data-target="edit-content-{{ child.id }}" style="max-width:160px">
<option value="">Insert link...</option>
{% for lnk in all_links %}
<option value="{{ lnk.url }}">{{ lnk.label }}</option>
{% endfor %}
</select>
{% endif %}
<button type="submit" class="btn btn-primary btn-xs">Save</button>
<button type="button" class="btn btn-ghost btn-xs cancel-edit-btn" data-item-id="{{ child.id }}">Cancel</button>
</form>
</div>
{% endfor %}
{% endfor %}
@@ -135,4 +173,49 @@
<div style="padding: 12px; color: var(--muted); font-size: 0.85rem;">No contacts linked</div>
{% endfor %}
</div>
<script>
(function() {
// Link picker: insert URL at cursor position in target input
document.querySelectorAll('.link-picker').forEach(function(sel) {
sel.addEventListener('change', function() {
var url = sel.value;
if (!url) return;
var targetId = sel.getAttribute('data-target');
var input = document.getElementById(targetId);
if (!input) return;
var start = input.selectionStart || input.value.length;
var end = input.selectionEnd || input.value.length;
var before = input.value.substring(0, start);
var after = input.value.substring(end);
// Add space padding if needed
if (before.length > 0 && before[before.length - 1] !== ' ') before += ' ';
if (after.length > 0 && after[0] !== ' ') url += ' ';
input.value = before + url + after;
input.focus();
var pos = before.length + url.length;
input.setSelectionRange(pos, pos);
sel.selectedIndex = 0;
});
});
// Inline edit: toggle edit form
document.querySelectorAll('.edit-item-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var id = btn.getAttribute('data-item-id');
document.querySelectorAll('[data-display-for="' + id + '"]').forEach(function(el) { el.style.display = 'none'; });
var form = document.querySelector('[data-edit-for="' + id + '"]');
if (form) { form.style.display = 'flex'; form.querySelector('input[name="content"]').focus(); }
});
});
document.querySelectorAll('.cancel-edit-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var id = btn.getAttribute('data-item-id');
document.querySelectorAll('[data-display-for="' + id + '"]').forEach(function(el) { el.style.display = ''; });
var form = document.querySelector('[data-edit-for="' + id + '"]');
if (form) form.style.display = 'none';
});
});
})();
</script>
{% endblock %}