Files
hamlab/web/src/TermTabs.tsx
T

48 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from 'react'
import { Term } from './api'
import { Spinner } from './ui'
interface Props {
terms: Term[]; active: string | null; creating?: boolean
onSelect: (id: string) => void; onNew: () => void
onRename: (t: Term) => void; onClose: (t: Term) => void
onMove: (id: string, before: string | null) => void // put a tab before another, or (null) at the end
onFocusTerm: () => void
}
// Tab strip across the top of the terminal page: one tab per session, "+" at the end.
export default function TermTabs({ terms, active, creating, onSelect, onNew, onRename, onClose, onMove, onFocusTerm }: Props) {
const [dragging, setDragging] = useState<string | null>(null)
const [over, setOver] = useState<string | null>(null)
const strip = useRef<HTMLDivElement>(null)
// keep the active tab in sight when the strip overflows
useEffect(() => { strip.current?.querySelector('.ttab.active')?.scrollIntoView({ block: 'nearest', inline: 'nearest' }) }, [active, terms.length])
return (
// Buttons must not take focus from the terminal; the tabs themselves must stay draggable, which a
// prevented mousedown would stop — so a tab click hands focus back afterwards instead.
<div className="term-tabs" ref={strip} onMouseDown={(e) => { if ((e.target as HTMLElement).closest('button')) e.preventDefault() }}>
{terms.map((t, i) => {
const sel = t.id === active
return (
<div key={t.id} className={'ttab' + (sel ? ' active' : '') + (t.running ? '' : ' exited') + (over === t.id && dragging !== t.id ? ' drop' : '')}
draggable onDragStart={(e) => { setDragging(t.id); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', t.id) }}
onDragOver={(e) => { if (dragging) { e.preventDefault(); setOver(t.id) } }} onDragLeave={() => setOver((o) => (o === t.id ? null : o))}
onDrop={(e) => { e.preventDefault(); if (dragging && dragging !== t.id) onMove(dragging, t.id); setDragging(null); setOver(null); onFocusTerm() }} onDragEnd={() => { setDragging(null); setOver(null) }}
onClick={() => { onSelect(t.id); onFocusTerm() }} onDoubleClick={() => onRename(t)}
onAuxClick={(e) => { if (e.button === 1) { e.preventDefault(); onClose(t) } }}>
<span className={'sdot' + (t.running ? ' run' : '')} />
<span className="tname" title={t.name}>{t.name}</span>
{sel && terms.length > 1 && <span className="ttab-move">
<button className="icon-btn" title="move left" disabled={i === 0} onClick={(e) => { e.stopPropagation(); onMove(t.id, terms[i - 1].id) }}>‹</button>
<button className="icon-btn" title="move right" disabled={i === terms.length - 1} onClick={(e) => { e.stopPropagation(); onMove(t.id, terms[i + 2]?.id ?? null) }}>›</button>
</span>}
{sel && <button className="icon-btn" title="rename" onClick={(e) => { e.stopPropagation(); onRename(t) }}>✎</button>}
<button className="icon-btn" title="close" onClick={(e) => { e.stopPropagation(); onClose(t) }}>✕</button>
</div>
)
})}
<button className="ttab new" title="new terminal (Alt+Shift+T) · Alt+Shift+←/→ switches tabs" disabled={creating} onClick={onNew}>{creating ? <Spinner /> : '+'}</button>
</div>
)
}