131 lines
5.6 KiB
JavaScript
131 lines
5.6 KiB
JavaScript
// 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);
|
||
}
|