diff --git a/web/app.js b/web/app.js
index 1e5fe57..9ece617 100644
--- a/web/app.js
+++ b/web/app.js
@@ -32,6 +32,7 @@
runChecked: new Set(),
fillValues: {},
fillActive: 0,
+ searchQuery: '',
};
const $ = (sel) => document.querySelector(sel);
@@ -406,30 +407,31 @@
function renderEntityList() {
const list = $('#entity-list');
+ const filtered = filterBySearch(state.entities);
- if (state.entities.length === 0) {
- list.innerHTML = '
no entities yet
';
- renderCardsHeader(false);
+ if (filtered.length === 0) {
+ list.innerHTML = `${state.searchQuery ? 'no matches' : 'no entities yet'}
`;
+ renderCardsHeader(state.view === 'cards');
return;
}
let html = '';
if (state.view === 'stream') {
renderCardsHeader(false);
- const groups = groupByDate(state.entities);
+ const groups = groupByDate(filtered);
let idx = 0;
for (const g of groups) {
html += ``;
for (const e of g.entities) {
- html += renderEntityItem(e, idx);
+ const realIdx = state.entities.indexOf(e);
+ html += renderEntityItem(e, realIdx);
idx++;
}
}
} else {
renderCardsHeader(true);
- const pinned = state.entities.filter(e => e.pinned);
- const rest = state.entities.filter(e => !e.pinned);
- let idx = 0;
+ const pinned = filtered.filter(e => e.pinned);
+ const rest = filtered.filter(e => !e.pinned);
if (pinned.length) {
html += '★ pinned
';
for (const e of pinned) {
@@ -889,7 +891,7 @@
function startEditBody() {
const e = state.entities[state.selectedIndex];
if (!e) return;
- const el = $(`.detail-body[data-id="${e.id}"]`);
+ const el = $(`.peek-body[data-id="${e.id}"]`);
if (!el || el.tagName === 'TEXTAREA') return;
const ta = document.createElement('textarea');
@@ -921,8 +923,7 @@
function startEditField(field) {
const e = state.entities[state.selectedIndex];
if (!e) return;
- const cls = field === 'title' ? '.detail-title' : '.detail-desc';
- const el = $(`${cls}[data-id="${e.id}"]`);
+ const el = $(`.peek-title[data-id="${e.id}"]`);
if (!el || el.tagName === 'INPUT') return;
const input = document.createElement('input');
@@ -1381,6 +1382,39 @@
window.addEventListener('hashchange', handleHash);
+ // ========== Search ==========
+
+ const searchInput = $('#search-input');
+ let searchDebounce = null;
+
+ searchInput.addEventListener('input', () => {
+ clearTimeout(searchDebounce);
+ searchDebounce = setTimeout(() => {
+ state.searchQuery = searchInput.value.trim().toLowerCase();
+ renderEntityList();
+ }, 150);
+ });
+
+ searchInput.addEventListener('keydown', (ev) => {
+ if (ev.key === 'Escape') { searchInput.value = ''; state.searchQuery = ''; renderEntityList(); searchInput.blur(); }
+ });
+
+ function filterBySearch(entities) {
+ if (!state.searchQuery) return entities;
+ let query = state.searchQuery;
+ let filterTags = [];
+ query = query.replace(/#(\S+)/g, (_, tag) => { filterTags.push(tag); return ''; }).trim();
+ return entities.filter(e => {
+ if (filterTags.length) {
+ const eTags = (e.tags || []).map(t => t.toLowerCase());
+ if (!filterTags.every(ft => eTags.includes(ft))) return false;
+ }
+ if (!query) return true;
+ const haystack = ((e.body || '') + ' ' + (e.title || '') + ' ' + (e.description || '')).toLowerCase();
+ return haystack.includes(query);
+ });
+ }
+
// ========== Utils ==========
function escHtml(s) {