mirror of
https://github.com/tridactyl/tridactyl.git
synced 2026-09-10 07:16:33 -04:00
- resolveDisplayText() looks up per-flag descriptions from excmd metadata (falls back to raw exstr when no flags map exists) so e.g. ';t' shows 'new tab' instead of 'hint -t' - completions sharing the next key token collapse into a single '+N' or '+heading' group row instead of listing every leaf individually - count-prefix keystrokes (e.g. '1', '2' before a repeat count) now patch the header text directly via updateHeader() instead of tearing down and rebuilding the whole binding list, avoiding a resize/reflow flash - wk-group rows get a distinct color in whichkey.css
206 lines
7 KiB
TypeScript
206 lines
7 KiB
TypeScript
import * as SELF from "@src/whichkey_frame"
|
|
import Logger from "@src/lib/logging"
|
|
import * as Messaging from "@src/lib/messaging"
|
|
import * as Metadata from "@src/.metadata.generated"
|
|
import { theme } from "@src/content/styling"
|
|
|
|
const logger = new Logger("whichkey_frame")
|
|
|
|
const bindsEl = window.document.getElementById("whichkey-binds")
|
|
|
|
const TOKEN_RE = /^(<[^>]+>|.)(.*)/s
|
|
|
|
/** Split keyStr into [first n tokens, remainder] */
|
|
function splitAtPrefix(keyStr: string, n: number): [string, string] {
|
|
let remaining = keyStr
|
|
let pressed = ""
|
|
for (let i = 0; i < n; i++) {
|
|
const m = TOKEN_RE.exec(remaining)
|
|
if (!m) return [pressed, remaining]
|
|
pressed += m[1]
|
|
remaining = m[2]
|
|
}
|
|
return [pressed, remaining]
|
|
}
|
|
|
|
function argAcceptsNumber(t: any): boolean {
|
|
if (!t) return false
|
|
if (t.type === "intrinsic" && t.name === "number") return true
|
|
if (t.type === "union" && Array.isArray(t.types))
|
|
return t.types.some((u: any) => argAcceptsNumber(u))
|
|
return false
|
|
}
|
|
|
|
function commandAcceptsCount(exstr: string): boolean {
|
|
const cmdWord = exstr.trim().split(/\s+/)[0]
|
|
if (!cmdWord) return false
|
|
const fn = Metadata.excmdsFunctions[cmdWord]
|
|
if (!fn) return false
|
|
return fn.params.some((p: any) => argAcceptsNumber(p.type))
|
|
}
|
|
|
|
function buildColumn(
|
|
matches: [string, string, string?][],
|
|
prefixLen: number,
|
|
): HTMLDivElement {
|
|
const col = document.createElement("div")
|
|
col.className = "wk-column"
|
|
for (const [keyStr, exstr, displayText] of matches) {
|
|
const row = document.createElement("div")
|
|
row.className = exstr ? "wk-row" : "wk-row wk-group"
|
|
|
|
const keySpan = document.createElement("span")
|
|
keySpan.className = "wk-key"
|
|
|
|
// Only show the part after the pressed prefix; prefix is in the header.
|
|
const [, remaining] = splitAtPrefix(keyStr, prefixLen)
|
|
if (remaining) {
|
|
const nextSpan = document.createElement("span")
|
|
nextSpan.className = "wk-key-next"
|
|
nextSpan.textContent = remaining
|
|
keySpan.appendChild(nextSpan)
|
|
}
|
|
|
|
const arrowSpan = document.createElement("span")
|
|
arrowSpan.className = "wk-arrow"
|
|
arrowSpan.textContent = "→"
|
|
|
|
const exstrEl = document.createElement("a")
|
|
exstrEl.className = "wk-exstr"
|
|
exstrEl.textContent = displayText ?? exstr
|
|
const cmdWord = exstr.trim().split(/\s+/)[0]
|
|
if (cmdWord) {
|
|
if (displayText) {
|
|
exstrEl.title = exstr
|
|
} else {
|
|
const doc = Metadata.getDoc(Metadata.excmdsFunctions[cmdWord])
|
|
if (doc) exstrEl.title = doc
|
|
}
|
|
exstrEl.href =
|
|
browser.runtime.getURL(
|
|
"static/docs/modules/_src_excmds_.html",
|
|
) +
|
|
"#" +
|
|
cmdWord
|
|
exstrEl.target = "_blank"
|
|
}
|
|
|
|
row.appendChild(keySpan)
|
|
row.appendChild(arrowSpan)
|
|
row.appendChild(exstrEl)
|
|
col.appendChild(row)
|
|
}
|
|
return col
|
|
}
|
|
|
|
/** Measure rendered content and report the height/width back to content */
|
|
function measureAndReportSize(generation: number, headerEl: HTMLElement | null) {
|
|
requestAnimationFrame(() => {
|
|
const firstRow = bindsEl.querySelector<HTMLElement>(".wk-row")
|
|
const rowHeightPx = firstRow
|
|
? parseFloat(getComputedStyle(firstRow).height)
|
|
: 20
|
|
const bindsStyle = getComputedStyle(bindsEl)
|
|
const bindsBorderPx =
|
|
(parseFloat(bindsStyle.borderTopWidth) || 0) +
|
|
(parseFloat(bindsStyle.borderBottomWidth) || 0)
|
|
const firstCol = bindsEl.querySelector<HTMLElement>(".wk-column")
|
|
const colStyle = firstCol ? getComputedStyle(firstCol) : null
|
|
const colPaddingPx = colStyle
|
|
? (parseFloat(colStyle.paddingTop) || 0) +
|
|
(parseFloat(colStyle.paddingBottom) || 0)
|
|
: 0
|
|
const rows = bindsEl.querySelectorAll(".wk-row").length
|
|
const cols = bindsEl.querySelectorAll(".wk-column").length || 1
|
|
const rowsPerCol = Math.ceil(rows / cols)
|
|
const headerHeightPx = headerEl ? headerEl.offsetHeight : 0
|
|
const naturalHeight = Math.ceil(
|
|
rowsPerCol * rowHeightPx +
|
|
bindsBorderPx +
|
|
colPaddingPx +
|
|
headerHeightPx,
|
|
)
|
|
// scrollWidth gives full text width even when clipped by overflow:hidden on parent.
|
|
// 24 = outer-border(2+2) + margin(6+6) + cushion(8)
|
|
const headerMinWidthPx = headerEl
|
|
? Math.ceil(headerEl.scrollWidth + 24)
|
|
: 0
|
|
Messaging.messageOwnTab("whichkey_content", "resize", [
|
|
naturalHeight,
|
|
generation,
|
|
headerMinWidthPx,
|
|
])
|
|
})
|
|
}
|
|
|
|
/** Render matches split across `columnCount` columns, then report actual height back to content */
|
|
export function update(
|
|
matches: [string, string, string?][],
|
|
columnCount: number,
|
|
generation: number,
|
|
prefixLen: number,
|
|
location = "left",
|
|
prefixStr = "",
|
|
hasCountPrefix = false,
|
|
headingText?: string,
|
|
) {
|
|
if (!bindsEl) {
|
|
logger.error("whichkey_frame: DOM elements not found")
|
|
return
|
|
}
|
|
document.body.dataset.location = location
|
|
|
|
if (hasCountPrefix)
|
|
matches = matches.filter(([, exstr]) => commandAcceptsCount(exstr))
|
|
|
|
while (bindsEl.firstChild) bindsEl.removeChild(bindsEl.firstChild)
|
|
|
|
const headerLabel = headingText ? `+${headingText}` : prefixStr
|
|
let headerEl: HTMLElement | null = null
|
|
if (headerLabel) {
|
|
headerEl = document.createElement("div")
|
|
headerEl.className = "wk-header"
|
|
headerEl.textContent = headerLabel
|
|
bindsEl.appendChild(headerEl)
|
|
}
|
|
|
|
const columnsEl = document.createElement("div")
|
|
columnsEl.className = "wk-columns"
|
|
|
|
const cols = Math.max(1, columnCount)
|
|
const rowsPerCol = Math.ceil(matches.length / cols)
|
|
for (let i = 0; i < cols; i++) {
|
|
const slice = matches.slice(i * rowsPerCol, (i + 1) * rowsPerCol)
|
|
if (slice.length > 0)
|
|
columnsEl.appendChild(buildColumn(slice, prefixLen))
|
|
}
|
|
bindsEl.appendChild(columnsEl)
|
|
|
|
measureAndReportSize(generation, headerEl)
|
|
}
|
|
|
|
/** Handle header-only changes, skip rebuild and update header directly */
|
|
export function updateHeader(headerLabel: string, generation: number) {
|
|
if (!bindsEl) {
|
|
logger.error("whichkey_frame: DOM elements not found")
|
|
return
|
|
}
|
|
let headerEl = bindsEl.querySelector<HTMLElement>(".wk-header")
|
|
if (headerLabel) {
|
|
if (!headerEl) {
|
|
headerEl = document.createElement("div")
|
|
headerEl.className = "wk-header"
|
|
bindsEl.insertBefore(headerEl, bindsEl.firstChild)
|
|
}
|
|
headerEl.textContent = headerLabel
|
|
} else if (headerEl) {
|
|
headerEl.remove()
|
|
headerEl = null
|
|
}
|
|
measureAndReportSize(generation, headerEl)
|
|
}
|
|
|
|
Messaging.addListener("whichkey_frame", Messaging.attributeCaller(SELF))
|
|
|
|
theme(document.querySelector(":root"))
|