Files
AtreidesAssetsFree/client/ui/match-popover.js
T

130 lines
5.6 KiB
JavaScript
Raw Normal View History

2026-10-29 11:57:29 +01:00
// Generated, do not edit. sha256:864ef84f3268d175b0ba11b7c30d7395b9e816338f8739d95658a7e8e95ba768
import { localDateNl } from '../../shared/time.js';
import {
linkTxEvent, unlinkTxEvent, onchainAsset, linksForTx, linksForEvent, eventCandidatesForTx,
txCandidatesForEvent
} from '../data/tx-links.js';
import { t } from '../i18n/index.js';
import { fmtInt } from './format.js';
import { openPopover, closePopover } from './popover.js';
import { esc } from './escape.js';
const trunc = s => (s && s.length > 22) ? `${s.slice(0, 10)}…${s.slice(-8)}` : (s || '');
const MANUAL = 'manual';
export function matchDot({ matched, method, kind, id }) {
const title = matched ? t('link.' + method) : t('match.add');
return `<span class="tx-link${matched ? ' is-matched' : ''}" data-match-trigger`
+ ` data-match-kind="${kind}" data-match-id="${id}" role="button" tabindex="0"`
+ ` title="${esc(title)}" aria-label="${esc(title)}"></span>`;
}
export function closeMatchPopover() { closePopover(); }
export async function openMatchPopover(anchorEl, { kind, id, assetId, onChange }) {
let changed = false;
const unit = (await onchainAsset() || {}).subunit || '';
const sats = n => `${fmtInt(n)} ${unit}`.trim();
const dateOf = iso => iso ? localDateNl(iso) : t('match.unconfirmed');
let filter = '';
const currentLinks = () => kind === 'tx' ? linksForTx(id) : linksForEvent(id);
const rawCands = () => kind === 'tx' ? eventCandidatesForTx(id, assetId) : txCandidatesForEvent(id);
const pairFor = itemId => kind === 'tx' ? { tx: id, ev: itemId } : { tx: itemId, ev: id };
const eventLine = e => ({
l1: `${esc(t('enums.event_type.' + e.type))} · ${sats(e.amount)}`,
l2: `${dateOf(e.occurredAt)} · ${esc(e.from_name || '-')} → ${esc(e.to_name || '-')}`,
text: `${t('enums.event_type.' + e.type)} ${e.amount} ${e.occurredAt} ${e.from_name || ''} ${e.to_name || ''}`,
});
const txLine = x => ({
l1: `${x.direction === 'in' ? '↓' : '↑'} ${sats(x.valueSats)}`,
l2: `${dateOf(x.blockTime)} · ${esc(x.account_name || '-')}${x.address ? ' · ' + esc(trunc(x.address)) : ''}`,
text: `${x.direction} ${x.valueSats} ${x.blockTime || ''} ${x.account_name || ''} ${x.txid || ''} ${x.address || ''}`,
});
const lineOf = item => kind === 'tx' ? eventLine(item) : txLine(item);
async function candidates() {
const list = await rawCands();
if (!filter) return list;
const f = filter.toLowerCase();
return list.filter(c => lineOf(c).text.toLowerCase().includes(f));
}
async function syncAnchor() {
if (anchorEl.isConnected) anchorEl.classList.toggle('is-matched', (await currentLinks()).length > 0);
}
const { el: pop, position } = openPopover(anchorEl, {
className: 'match-pop',
onClose: () => {
document.querySelectorAll('.tx-link.is-open').forEach(d => d.classList.remove('is-open'));
if (changed && onChange) onChange();
},
});
anchorEl.classList.add('is-open');
async function draw() {
const links = await currentLinks();
const cands = await candidates();
const addLabel = kind === 'tx' ? t('match.toEvent') : t('match.toTx');
const linksHtml = links.length ? `
<div class="t-label match-pop-head">${t('match.linked')}</div>
${links.map(it => {
const ln = lineOf(it);
return `<div class="match-pop-link">
<div class="match-pop-main"><div class="l1">${ln.l1}</div><div class="l2">${ln.l2}</div><span class="match-pop-method">${esc(t('link.' + it.method))}</span></div>
<button class="match-pop-x" data-unlink="${it.id}" title="${esc(t('match.unlink'))}" aria-label="${esc(t('match.unlink'))}">×</button>
</div>`;
}).join('')}
<div class="match-pop-sep"></div>` : '';
const listHtml = cands.length
? cands.map(c => {
const ln = lineOf(c);
return `<div class="match-pop-cand" data-link="${c.id}">
<div class="match-pop-main"><div class="l1">${ln.l1}</div><div class="l2">${ln.l2}</div></div>
</div>`;
}).join('')
: `<div class="match-pop-empty">${t('match.noCandidates')}</div>`;
pop.innerHTML = `${linksHtml}
<div class="t-label match-pop-head">${esc(addLabel)}</div>
<input class="input match-pop-filter" data-filter placeholder="${esc(t('match.filter'))}" value="${esc(filter)}">
<div class="match-pop-list">${listHtml}</div>`;
position();
}
pop.addEventListener('click', async e => {
const un = e.target.closest('[data-unlink]');
if (un) { const p = pairFor(Number(un.dataset.unlink)); await unlinkTxEvent(p.tx, p.ev); changed = true; await syncAnchor(); await draw(); return; }
const lk = e.target.closest('[data-link]');
if (lk) { const p = pairFor(Number(lk.dataset.link)); await linkTxEvent(p.tx, p.ev, MANUAL); changed = true; await syncAnchor(); await draw(); return; }
});
pop.addEventListener('input', e => {
if (e.target.matches('[data-filter]')) { filter = e.target.value; draw(); }
});
await draw();
}
export function wireMatchTriggers(container, optsFactory) {
const open = el => openMatchPopover(el, optsFactory(el.dataset.matchKind, Number(el.dataset.matchId)));
const onClick = e => {
const dot = e.target.closest('[data-match-trigger]');
if (!dot || !container.contains(dot)) return;
e.preventDefault(); e.stopPropagation();
open(dot);
};
const onKey = e => {
if (e.key !== 'Enter' && e.key !== ' ') return;
const dot = e.target.closest('[data-match-trigger]');
if (!dot || !container.contains(dot)) return;
e.preventDefault(); e.stopPropagation();
open(dot);
};
container.addEventListener('click', onClick, true);
container.addEventListener('keydown', onKey, true);
}