mirror of
https://github.com/mempool/mempool.git
synced 2026-08-13 12:33:11 +02:00
improve cluster diagram mobile UX
This commit is contained in:
parent
95eb1372f4
commit
a4b46c2b48
6 changed files with 142 additions and 28 deletions
|
|
@ -48,10 +48,12 @@
|
|||
[class.inactive]="outline.chunkIndex !== effectiveActiveChunk"
|
||||
[attr.d]="outline.path"
|
||||
[ngbTooltip]="chunkLabelTooltip"
|
||||
[disableTooltip]="isMobile"
|
||||
container="body"
|
||||
placement="top"
|
||||
(mouseenter)="onChunkEnter(outline.chunkIndex)"
|
||||
(mouseleave)="onChunkLeave()" />
|
||||
(mouseleave)="onChunkLeave()"
|
||||
(click)="onChunkClick(outline.chunkIndex, $event)" />
|
||||
|
||||
<text *ngFor="let outline of chunkOutlines; trackBy: trackByChunkIndex"
|
||||
class="chunk-label"
|
||||
|
|
@ -59,10 +61,12 @@
|
|||
[attr.x]="outline.labelX" [attr.y]="outline.labelY"
|
||||
text-anchor="middle"
|
||||
[ngbTooltip]="chunkLabelTooltip"
|
||||
[disableTooltip]="isMobile"
|
||||
container="body"
|
||||
placement="top"
|
||||
(mouseenter)="onChunkEnter(outline.chunkIndex)"
|
||||
(mouseleave)="onChunkLeave()">
|
||||
(mouseleave)="onChunkLeave()"
|
||||
(click)="onChunkClick(outline.chunkIndex, $event)">
|
||||
{{ outline.feerate | feeRounding }} sat/vB
|
||||
</text>
|
||||
</ng-container>
|
||||
|
|
@ -76,7 +80,8 @@
|
|||
stroke-width="12"
|
||||
(mouseenter)="onEdgeEnter(i, $event)"
|
||||
(mousemove)="onEdgeMove($event)"
|
||||
(mouseleave)="onEdgeLeave()" />
|
||||
(mouseleave)="onEdgeLeave()"
|
||||
(click)="onEdgeClick(i, $event)" />
|
||||
<path class="edge-line"
|
||||
[class.highlighted]="edge.highlighted"
|
||||
[class.ancestor]="edge.highlightKind === 'ancestor'"
|
||||
|
|
@ -96,7 +101,7 @@
|
|||
(mouseenter)="onNodeEnter(node, $event)"
|
||||
(mousemove)="onNodeMove($event)"
|
||||
(mouseleave)="onNodeLeave()"
|
||||
(click)="onNodeClick(node)">
|
||||
(click)="onNodeClick(node, $event)">
|
||||
<rect class="node-rect"
|
||||
[class.current]="node.isCurrent"
|
||||
[class.hovered]="node.hovered"
|
||||
|
|
@ -120,7 +125,7 @@
|
|||
|
||||
<div #tooltip
|
||||
class="cluster-tooltip"
|
||||
*ngIf="hoverNode || hoverEdge"
|
||||
*ngIf="!isMobile && (hoverNode || hoverEdge)"
|
||||
[style.left.px]="tooltipPosition.x"
|
||||
[style.top.px]="tooltipPosition.y">
|
||||
<ng-container *ngIf="hoverNode">
|
||||
|
|
@ -154,3 +159,39 @@
|
|||
<div><span class="swatch descendant"></span><span i18n="cluster.child|child">child</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="cluster-mobile-panel"
|
||||
*ngIf="isMobile && (hoverNode || hoverEdge)"
|
||||
(click)="$event.stopPropagation()">
|
||||
<ng-container *ngIf="hoverNode">
|
||||
<div class="tx-id-row">
|
||||
<a [routerLink]="['/tx/' | relativeUrl, hoverNode.tx.txid]">{{ hoverNode.tx.txid | shortenString }}</a>
|
||||
<span class="this-tx-badge" *ngIf="hoverNode.isCurrent" i18n="cluster.this-transaction|this transaction">this transaction</span>
|
||||
</div>
|
||||
<div class="tooltip-body">
|
||||
<table class="stats">
|
||||
<tr>
|
||||
<th i18n="transaction.fee|Transaction fee">Fee</th>
|
||||
<td>{{ hoverNode.tx.fee | number }} <span class="unit" i18n="shared.sats">sats</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th i18n="transaction.vsize|Virtual size">Size</th>
|
||||
<td><span [innerHTML]="hoverNode.tx.weight / 4 | vbytes: 2"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
|
||||
<td>{{ hoverNode.feerate | feeRounding }} <span class="unit">sat/vB</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="legend">
|
||||
<div><span class="swatch ancestor"></span><span i18n="cluster.parents|parents">parents</span></div>
|
||||
<div><span class="swatch descendant"></span><span i18n="cluster.children|children">children</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="legend edge-legend" *ngIf="!hoverNode && hoverEdge">
|
||||
<div><span class="swatch ancestor"></span><span i18n="cluster.parent|parent">parent</span></div>
|
||||
<div><span class="swatch descendant"></span><span i18n="cluster.child|child">child</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -144,17 +144,14 @@
|
|||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cluster-tooltip {
|
||||
position: absolute;
|
||||
.cluster-tooltip,
|
||||
.cluster-mobile-panel {
|
||||
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.5);
|
||||
color: var(--tooltip-grey);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
pointer-events: none;
|
||||
max-width: 360px;
|
||||
white-space: nowrap;
|
||||
|
||||
.tx-id-row {
|
||||
display: flex;
|
||||
|
|
@ -225,3 +222,15 @@
|
|||
&.descendant { background: var(--cluster-descendant-color); }
|
||||
}
|
||||
}
|
||||
|
||||
.cluster-tooltip {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
max-width: 360px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cluster-mobile-panel {
|
||||
margin-top: 8px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export class ClusterDiagramComponent implements OnChanges, AfterViewInit, OnDest
|
|||
@Input() cluster: { txs: CpfpClusterTx[]; chunks: CpfpClusterChunk[]; chunkIndex: number };
|
||||
@Input() txid: string;
|
||||
@Input() preview = false;
|
||||
@Input() isMobile = false;
|
||||
|
||||
@ViewChild('graphContainer', { static: true }) graphContainer: ElementRef;
|
||||
@ViewChild('tooltip') tooltipElement: ElementRef;
|
||||
|
|
@ -125,8 +126,17 @@ export class ClusterDiagramComponent implements OnChanges, AfterViewInit, OnDest
|
|||
}
|
||||
|
||||
onNodeEnter(node: RenderedNode, event: MouseEvent): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.applyNodeHighlight(node);
|
||||
this.updateTooltipPosition(event);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
private applyNodeHighlight(node: RenderedNode): void {
|
||||
this.hoverNode = node;
|
||||
this.hoverEdge = null;
|
||||
this.hoverChunkIndex = null;
|
||||
this.applyEffectiveChunk();
|
||||
this.clearHighlights();
|
||||
node.hovered = true;
|
||||
for (const edge of this.edges) {
|
||||
|
|
@ -140,58 +150,63 @@ export class ClusterDiagramComponent implements OnChanges, AfterViewInit, OnDest
|
|||
edge.highlightKind = 'ancestor';
|
||||
}
|
||||
}
|
||||
this.updateTooltipPosition(event);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onNodeMove(event: MouseEvent): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.updateTooltipPosition(event);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onNodeLeave(): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.hoverNode = null;
|
||||
this.clearHighlights();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onEdgeEnter(edgeIndex: number, event: MouseEvent): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.applyEdgeHighlight(edgeIndex);
|
||||
this.updateTooltipPosition(event);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
private applyEdgeHighlight(edgeIndex: number): void {
|
||||
this.clearHighlights();
|
||||
this.hoverNode = null;
|
||||
this.hoverChunkIndex = null;
|
||||
this.applyEffectiveChunk();
|
||||
const edge = this.edges[edgeIndex];
|
||||
edge.highlighted = true;
|
||||
edge.highlightKind = 'direct';
|
||||
this.nodes[edge.parentIndex].relation = 'ancestor';
|
||||
this.nodes[edge.childIndex].relation = 'descendant';
|
||||
this.hoverEdge = edge;
|
||||
this.updateTooltipPosition(event);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onEdgeMove(event: MouseEvent): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.updateTooltipPosition(event);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onEdgeLeave(): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.hoverEdge = null;
|
||||
this.clearHighlights();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onChunkEnter(chunkIndex: number): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.hoverChunkIndex = chunkIndex;
|
||||
this.applyEffectiveChunk();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onChunkLeave(): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.hoverChunkIndex = null;
|
||||
this.applyEffectiveChunk();
|
||||
this.cd.markForCheck();
|
||||
|
|
@ -212,13 +227,61 @@ export class ClusterDiagramComponent implements OnChanges, AfterViewInit, OnDest
|
|||
}
|
||||
}
|
||||
|
||||
onNodeClick(node: RenderedNode): void {
|
||||
onNodeClick(node: RenderedNode, event: MouseEvent): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.isMobile) {
|
||||
event.stopPropagation();
|
||||
if (this.hoverNode?.index === node.index) {
|
||||
this.clearMobileSelection();
|
||||
} else {
|
||||
this.applyNodeHighlight(node);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const network = this.stateService.network;
|
||||
const prefix = network && network !== 'mainnet' ? `/${network}` : '';
|
||||
this.router.navigate([prefix + '/tx/', node.tx.txid]);
|
||||
}
|
||||
|
||||
onEdgeClick(edgeIndex: number, event: MouseEvent): void {
|
||||
if (this.preview || !this.isMobile) { return; }
|
||||
event.stopPropagation();
|
||||
const edge = this.edges[edgeIndex];
|
||||
if (this.hoverEdge === edge) {
|
||||
this.clearMobileSelection();
|
||||
} else {
|
||||
this.applyEdgeHighlight(edgeIndex);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
onChunkClick(chunkIndex: number, event: MouseEvent): void {
|
||||
if (this.preview || !this.isMobile) { return; }
|
||||
event.stopPropagation();
|
||||
this.hoverNode = null;
|
||||
this.hoverEdge = null;
|
||||
this.clearHighlights();
|
||||
this.hoverChunkIndex = chunkIndex;
|
||||
this.applyEffectiveChunk();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
onBackgroundClick(): void {
|
||||
if (this.preview || !this.isMobile) { return; }
|
||||
this.clearMobileSelection();
|
||||
}
|
||||
|
||||
private clearMobileSelection(): void {
|
||||
this.hoverNode = null;
|
||||
this.hoverEdge = null;
|
||||
this.hoverChunkIndex = null;
|
||||
this.clearHighlights();
|
||||
this.applyEffectiveChunk();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
private clearHighlights(): void {
|
||||
for (const node of this.nodes) {
|
||||
node.hovered = false;
|
||||
|
|
|
|||
|
|
@ -248,13 +248,14 @@
|
|||
<ng-template #clusterPreviewButton let-cluster="cluster" let-txid="txid">
|
||||
<div class="cluster-preview-inline" role="button" tabindex="0"
|
||||
[ngbTooltip]="clusterPreviewTooltip"
|
||||
[disableTooltip]="isMobile"
|
||||
placement="bottom"
|
||||
tooltipClass="cluster-preview-ngb-tooltip"
|
||||
container="body"
|
||||
(click)="toggleCpfp()"
|
||||
(keydown.enter)="toggleCpfp()"
|
||||
(keydown.space)="toggleCpfp(); $event.preventDefault()">
|
||||
<app-cluster-diagram class="cluster-preview-diagram" [cluster]="cluster" [txid]="txid" [preview]="true"></app-cluster-diagram>
|
||||
<app-cluster-diagram class="cluster-preview-diagram" [cluster]="cluster" [txid]="txid" [preview]="true" [isMobile]="isMobile"></app-cluster-diagram>
|
||||
<fa-icon class="cluster-preview-expand" [icon]="['fas', cpfpMode ? 'compress' : 'expand']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
<ng-template #clusterPreviewTooltip>
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@
|
|||
<ng-container *ngIf="cpfpInfo?.cluster; else legacyCpfpRaw">
|
||||
<div class="box">
|
||||
<app-cluster-diagram
|
||||
[cluster]="cpfpInfo.cluster" [txid]="transaction.txid">
|
||||
[cluster]="cpfpInfo.cluster" [txid]="transaction.txid" [isMobile]="isMobile">
|
||||
</app-cluster-diagram>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
@ -248,4 +248,4 @@
|
|||
</h3>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
<ng-container *ngIf="cpfpInfo?.cluster && !isAcceleration; else legacyCpfp">
|
||||
<div class="box">
|
||||
<app-cluster-diagram
|
||||
[cluster]="cpfpInfo.cluster" [txid]="tx.txid">
|
||||
[cluster]="cpfpInfo.cluster" [txid]="tx.txid" [isMobile]="isMobile">
|
||||
</app-cluster-diagram>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
@ -393,4 +393,4 @@
|
|||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue