mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 10:15:27 -04:00
Email library chip-bar: AND across pills, plain Enter commits text, pill × up 4px
1. Multiple pills now AND together — 'alice + bob' means both alice AND bob are somewhere on the email, not 'from alice OR from bob'. (some → every in the filter.) 2. Default autocomplete focus is now -1 (no row pre-selected) so plain Enter commits the input as a text pill — typing then Enter behaves like a normal search. ArrowDown / ArrowUp + Enter still picks a contact suggestion. Tab still autocompletes the most-relevant match regardless of arrow state. 3. Pill × button nudged up 4px so it sits on the visual centerline inside the 18px pill height.
This commit is contained in:
+24
-11
@@ -1797,7 +1797,9 @@ function _applyPillFilter() {
|
|||||||
const source = _libPreSearchEmails || state._libEmails || [];
|
const source = _libPreSearchEmails || state._libEmails || [];
|
||||||
const draftPill = draft.length >= 1 ? { type: 'text', text: draft } : null;
|
const draftPill = draft.length >= 1 ? { type: 'text', text: draft } : null;
|
||||||
const effective = draftPill ? pills.concat([draftPill]) : pills;
|
const effective = draftPill ? pills.concat([draftPill]) : pills;
|
||||||
const filtered = source.filter(em => effective.some(p => _emailMatchesPill(em, p)));
|
// AND across pills — "alice + bob" should mean both alice AND bob are
|
||||||
|
// somewhere on the email (from/to/cc), not "from alice OR from bob".
|
||||||
|
const filtered = source.filter(em => effective.every(p => _emailMatchesPill(em, p)));
|
||||||
state._libEmails = filtered;
|
state._libEmails = filtered;
|
||||||
_renderGrid();
|
_renderGrid();
|
||||||
}
|
}
|
||||||
@@ -1818,7 +1820,7 @@ function _renderSearchPills() {
|
|||||||
const label = p.type === 'contact' ? (p.name || p.email || '?') : (p.text || '');
|
const label = p.type === 'contact' ? (p.name || p.email || '?') : (p.text || '');
|
||||||
return `<span class="email-lib-pill" data-pill-idx="${i}" style="display:inline-flex;align-items:center;gap:2px;padding:0 4px 0 6px;border-radius:999px;background:color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);color:var(--accent, var(--red));font-size:10px;line-height:18px;height:18px;font-weight:600;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:0;">
|
return `<span class="email-lib-pill" data-pill-idx="${i}" style="display:inline-flex;align-items:center;gap:2px;padding:0 4px 0 6px;border-radius:999px;background:color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);color:var(--accent, var(--red));font-size:10px;line-height:18px;height:18px;font-weight:600;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:0;">
|
||||||
<span style="overflow:hidden;text-overflow:ellipsis;">${esc(label)}</span>
|
<span style="overflow:hidden;text-overflow:ellipsis;">${esc(label)}</span>
|
||||||
<button type="button" class="email-lib-pill-x" data-pill-idx="${i}" title="Remove" style="background:transparent;border:0;color:inherit;cursor:pointer;font-size:11px;line-height:1;padding:0 2px;opacity:0.7;">×</button>
|
<button type="button" class="email-lib-pill-x" data-pill-idx="${i}" title="Remove" style="background:transparent;border:0;color:inherit;cursor:pointer;font-size:11px;line-height:1;padding:0 2px;opacity:0.7;position:relative;top:-4px;">×</button>
|
||||||
</span>`;
|
</span>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
wrap.querySelectorAll('.email-lib-pill-x').forEach(btn => {
|
wrap.querySelectorAll('.email-lib-pill-x').forEach(btn => {
|
||||||
@@ -1920,7 +1922,10 @@ async function _initEmailSearchChipBar() {
|
|||||||
const _refreshSuggestions = async () => {
|
const _refreshSuggestions = async () => {
|
||||||
await _ensureSuggestionCache();
|
await _ensureSuggestionCache();
|
||||||
_itemsRef = _filterSuggestions(input.value);
|
_itemsRef = _filterSuggestions(input.value);
|
||||||
_libSuggestionFocusIdx = 0;
|
// Default to no focused suggestion — text typing should feel like
|
||||||
|
// regular search; the user has to ArrowDown / Tab explicitly to
|
||||||
|
// pick a contact. Enter without a focused row commits as text.
|
||||||
|
_libSuggestionFocusIdx = -1;
|
||||||
_renderSearchSuggestions(_itemsRef);
|
_renderSearchSuggestions(_itemsRef);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1944,28 +1949,36 @@ async function _initEmailSearchChipBar() {
|
|||||||
}
|
}
|
||||||
if (e.key === 'ArrowDown' && menuOpen) {
|
if (e.key === 'ArrowDown' && menuOpen) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
_libSuggestionFocusIdx = Math.min(_libSuggestionFocusIdx + 1, _itemsRef.length - 1);
|
// -1 → 0 → 1 → … → length-1, then wraps back to -1 (no selection)
|
||||||
|
const next = _libSuggestionFocusIdx + 1;
|
||||||
|
_libSuggestionFocusIdx = next >= _itemsRef.length ? -1 : next;
|
||||||
_renderSearchSuggestions(_itemsRef);
|
_renderSearchSuggestions(_itemsRef);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === 'ArrowUp' && menuOpen) {
|
if (e.key === 'ArrowUp' && menuOpen) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
_libSuggestionFocusIdx = Math.max(_libSuggestionFocusIdx - 1, 0);
|
// -1 → length-1 → length-2 → … → 0 → -1
|
||||||
|
const next = _libSuggestionFocusIdx - 1;
|
||||||
|
_libSuggestionFocusIdx = next < -1 ? _itemsRef.length - 1 : next;
|
||||||
_renderSearchSuggestions(_itemsRef);
|
_renderSearchSuggestions(_itemsRef);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === 'Tab' && menuOpen && _itemsRef[_libSuggestionFocusIdx]) {
|
if (e.key === 'Tab' && menuOpen) {
|
||||||
e.preventDefault();
|
// Tab autocompletes the FIRST suggestion (most-relevant), regardless
|
||||||
_acceptSuggestion(_itemsRef[_libSuggestionFocusIdx]);
|
// of whether the user arrowed down yet — matches the user's mental
|
||||||
return;
|
// model of "type a name and tab to pick".
|
||||||
|
const pick = _libSuggestionFocusIdx >= 0 ? _itemsRef[_libSuggestionFocusIdx] : _itemsRef[0];
|
||||||
|
if (pick) { e.preventDefault(); _acceptSuggestion(pick); return; }
|
||||||
}
|
}
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (menuOpen && _itemsRef[_libSuggestionFocusIdx]) {
|
// Only commit a contact if the user explicitly focused one. Plain
|
||||||
|
// Enter should default to a text pill so regular text search works
|
||||||
|
// without forcing a contact pick.
|
||||||
|
if (menuOpen && _libSuggestionFocusIdx >= 0 && _itemsRef[_libSuggestionFocusIdx]) {
|
||||||
_acceptSuggestion(_itemsRef[_libSuggestionFocusIdx]);
|
_acceptSuggestion(_itemsRef[_libSuggestionFocusIdx]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// No autocomplete match — fall back to a free-text pill.
|
|
||||||
const v = input.value.trim();
|
const v = input.value.trim();
|
||||||
if (v) {
|
if (v) {
|
||||||
_addSearchPill({ type: 'text', text: v });
|
_addSearchPill({ type: 'text', text: v });
|
||||||
|
|||||||
Reference in New Issue
Block a user