136 lines
3.6 KiB
Vue
136 lines
3.6 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed } from 'vue'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
data: Uint8Array
|
|
height?: number
|
|
highlight?: { start: number; end: number } | null
|
|
}>(), { height: 400, highlight: null })
|
|
|
|
const emit = defineEmits<{
|
|
'byte-hover': [offset: number | null]
|
|
}>()
|
|
|
|
const BYTES = 16
|
|
const ROW_H = 20
|
|
|
|
const wrapEl = ref<HTMLElement | null>(null)
|
|
const scrollTop = ref(0)
|
|
|
|
const totalRows = computed(() => Math.ceil(props.data.length / BYTES))
|
|
const firstRow = computed(() => Math.floor(scrollTop.value / ROW_H))
|
|
const lastRow = computed(() => Math.min(firstRow.value + Math.ceil(props.height / ROW_H) + 4, totalRows.value))
|
|
|
|
const rows = computed(() => {
|
|
const out = []
|
|
for (let r = firstRow.value; r < lastRow.value; r++) {
|
|
const off = r * BYTES
|
|
const slice = props.data.subarray(off, off + BYTES)
|
|
const hex = Array.from(slice).map(b => b.toString(16).padStart(2, '0'))
|
|
const asc = Array.from(slice).map(b => (b >= 0x20 && b < 0x7f) ? String.fromCharCode(b) : '.')
|
|
out.push({ off, hex, asc })
|
|
}
|
|
return out
|
|
})
|
|
|
|
function isHi(offset: number): boolean {
|
|
return props.highlight != null
|
|
&& offset >= props.highlight.start
|
|
&& offset < props.highlight.end
|
|
}
|
|
|
|
function onScroll(e: Event) {
|
|
scrollTop.value = (e.target as HTMLElement).scrollTop
|
|
}
|
|
|
|
defineExpose({
|
|
scrollTo(offset: number) {
|
|
const row = Math.floor(offset / BYTES)
|
|
wrapEl.value?.scrollTo({ top: row * ROW_H, behavior: 'smooth' })
|
|
},
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="wrapEl" class="hd-wrap" :style="{ height: height + 'px' }" @scroll="onScroll">
|
|
<div :style="{ height: totalRows * ROW_H + 'px', position: 'relative' }">
|
|
<div :style="{ transform: `translateY(${firstRow * ROW_H}px)` }">
|
|
<div v-for="row in rows" :key="row.off" class="hd-row">
|
|
<span class="hd-off">{{ row.off.toString(16).padStart(8, '0') }}</span>
|
|
|
|
<span class="hd-hex">
|
|
<span
|
|
v-for="(h, bi) in row.hex"
|
|
:key="bi"
|
|
:class="['hd-byte', { 'hd-byte-hi': isHi(row.off + bi) }, bi === 8 ? 'hd-byte-gap' : '']"
|
|
@mouseenter="emit('byte-hover', row.off + bi)"
|
|
@mouseleave="emit('byte-hover', null)"
|
|
>{{ h }}</span>
|
|
</span>
|
|
|
|
<span class="hd-asc">|<span
|
|
v-for="(a, bi) in row.asc"
|
|
:key="bi"
|
|
:class="['hd-asc-char', { 'hd-byte-hi': isHi(row.off + bi) }]"
|
|
@mouseenter="emit('byte-hover', row.off + bi)"
|
|
@mouseleave="emit('byte-hover', null)"
|
|
>{{ a }}</span>|</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.hd-wrap {
|
|
overflow-y: auto;
|
|
font-family: 'Courier New', Courier, monospace;
|
|
font-size: 12px;
|
|
border: 1px solid var(--el-border-color);
|
|
border-radius: 4px;
|
|
background: var(--el-fill-color-blank);
|
|
}
|
|
.hd-row {
|
|
display: flex;
|
|
height: 20px;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 0 8px;
|
|
}
|
|
.hd-row:hover {
|
|
background: var(--el-fill-color-light);
|
|
}
|
|
.hd-off {
|
|
color: var(--el-text-color-placeholder);
|
|
min-width: 6em;
|
|
user-select: none;
|
|
}
|
|
.hd-hex {
|
|
display: inline-flex;
|
|
color: var(--el-text-color-primary);
|
|
}
|
|
.hd-byte {
|
|
display: inline-block;
|
|
width: 3ch;
|
|
cursor: default;
|
|
border-radius: 2px;
|
|
}
|
|
/* Extra left margin to create the double-space gap between the two groups of 8 */
|
|
.hd-byte-gap {
|
|
margin-left: 1ch;
|
|
}
|
|
.hd-byte-hi {
|
|
background: var(--el-color-primary-light-7);
|
|
color: var(--el-color-primary);
|
|
}
|
|
.hd-asc {
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
.hd-asc-char {
|
|
display: inline-block;
|
|
width: 1ch;
|
|
cursor: default;
|
|
border-radius: 2px;
|
|
}
|
|
</style>
|