144 lines
3.7 KiB
Vue
Raw Permalink Normal View History

2026-05-31 13:21:13 +02:00
<!--
Line highlighting for code blocks.
(auto transformed, you don't need to use this component directly)
Usage:
```ts {1,3-5|2,4}
const your_code = 'here'
```
Learn more: https://sli.dev/guide/syntax.html#line-highlighting
-->
<script setup lang="ts">
import type { PropType } from 'vue'
import { useClipboard } from '@vueuse/core'
import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
import { CLASS_VCLICK_HIDDEN, CLICKS_MAX } from '../constants'
import { useSlideContext } from '../context'
import { configs } from '../env'
import { makeId, updateCodeHighlightRange } from '../logic/utils'
const props = defineProps({
ranges: {
type: Array as PropType<string[]>,
default: () => [],
},
finally: {
type: [String, Number],
default: 'last',
},
startLine: {
type: Number,
default: 1,
},
lines: {
type: Boolean,
default: configs.lineNumbers,
},
at: {
type: [String, Number],
default: '+1',
},
maxHeight: {
type: String,
default: undefined,
},
})
const { $clicksContext: clicks } = useSlideContext()
const el = ref<HTMLDivElement>()
const id = makeId()
onUnmounted(() => {
clicks!.unregister(id)
})
watchEffect(() => {
el.value?.classList.toggle('slidev-code-line-numbers', props.lines)
})
onMounted(() => {
if (!clicks || !props.ranges?.length)
return
const clicksInfo = clicks.calculateSince(props.at, props.ranges.length - 1)
clicks.register(id, clicksInfo)
const index = computed(() => clicksInfo ? Math.max(0, clicks.current - clicksInfo.start + 1) : CLICKS_MAX)
const finallyRange = computed(() => {
return props.finally === 'last' ? props.ranges.at(-1) : props.finally.toString()
})
watchEffect(() => {
if (!el.value)
return
let rangeStr = props.ranges[index.value] ?? finallyRange.value
const hide = rangeStr === 'hide'
el.value.classList.toggle(CLASS_VCLICK_HIDDEN, hide)
if (hide)
rangeStr = props.ranges[index.value + 1] ?? finallyRange.value
const pre = el.value.querySelector('.shiki')!
const lines = Array.from(pre.querySelectorAll('code > .line'))
const linesCount = lines.length
updateCodeHighlightRange(
rangeStr,
linesCount,
props.startLine,
no => [lines[no]],
)
// Scroll to the highlighted line if `maxHeight` is set
if (props.maxHeight) {
const highlightedEls = Array.from(pre.querySelectorAll('.line.highlighted')) as HTMLElement[]
const height = highlightedEls.reduce((acc, el) => el.offsetHeight + acc, 0)
if (height > el.value.offsetHeight) {
highlightedEls[0].scrollIntoView({ behavior: 'smooth', block: 'start' })
}
else if (highlightedEls.length > 0) {
const middleEl = highlightedEls[Math.round((highlightedEls.length - 1) / 2)]
middleEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}
})
})
const { copied, copy } = useClipboard()
function copyCode() {
const code = el.value?.querySelector('.slidev-code')?.textContent
if (code)
copy(code)
}
</script>
<template>
<div
ref="el"
class="slidev-code-wrapper relative group"
:class="{
'slidev-code-line-numbers': props.lines,
}"
:style="{
'max-height': props.maxHeight,
'overflow-y': props.maxHeight ? 'scroll' : undefined,
'--start': props.startLine,
}"
>
<slot />
<button
v-if="configs.codeCopy"
class="slidev-code-copy absolute top-0 right-0 transition opacity-0 group-hover:opacity-20 hover:!opacity-100"
:title="copied ? 'Copied' : 'Copy'" @click="copyCode()"
>
<ph-check-circle v-if="copied" class="p-2 w-8 h-8" />
<ph-clipboard v-else class="p-2 w-8 h-8" />
</button>
</div>
</template>