返回 slidev
CodeBlockWrapper.vue
根目录 / packages / client / builtin / CodeBlockWrapper.vue
1 <!--
2 Line highlighting for code blocks.
3 (auto transformed, you don't need to use this component directly)
4
5 Usage:
6
7 ```ts {1,3-5|2,4}
8 const your_code = 'here'
9 ```
10
11 Learn more: https://sli.dev/guide/syntax.html#line-highlighting
12 -->
13
14 <script setup lang="ts">
15 import type { PropType, Ref } from 'vue'
16 import { useClipboard } from '@vueuse/core'
17 import { computed, inject, onMounted, onUnmounted, ref, watchEffect } from 'vue'
18 import { CLASS_VCLICK_HIDDEN, CLICKS_MAX } from '../constants'
19 import { useSlideContext } from '../context'
20 import { configs } from '../env'
21 import TitleIcon from '../internals/TitleIcon.vue'
22 import { makeId, updateCodeHighlightRange } from '../logic/utils'
23
24 const props = defineProps({
25 ranges: {
26 type: Array as PropType<string[]>,
27 default: () => [],
28 },
29 finally: {
30 type: [String, Number],
31 default: 'last',
32 },
33 startLine: {
34 type: Number,
35 default: 1,
36 },
37 lines: {
38 type: Boolean,
39 default: configs.lineNumbers,
40 },
41 at: {
42 type: [String, Number],
43 default: '+1',
44 },
45 maxHeight: {
46 type: String,
47 default: undefined,
48 },
49 title: {
50 type: String,
51 default: '',
52 },
53 })
54 const { $frontmatter, $clicksContext: clicks } = useSlideContext()
55 const isCodeCopy = computed(() => $frontmatter?.codeCopy ?? configs.codeCopy)
56
57 const el = ref<HTMLDivElement>()
58 const id = makeId()
59
60 onUnmounted(() => {
61 clicks!.unregister(id)
62 })
63
64 watchEffect(() => {
65 el.value?.classList.toggle('slidev-code-line-numbers', props.lines)
66 })
67
68 onMounted(() => {
69 if (!clicks || !props.ranges?.length)
70 return
71
72 const clicksInfo = clicks.calculateSince(props.at, props.ranges.length - 1)
73 clicks.register(id, clicksInfo)
74
75 const index = computed(() => clicksInfo ? Math.max(0, clicks.current - clicksInfo.start + 1) : CLICKS_MAX)
76
77 const finallyRange = computed(() => {
78 return props.finally === 'last' ? props.ranges.at(-1) : props.finally.toString()
79 })
80
81 let hiddenByRange = false
82
83 watchEffect(() => {
84 if (!el.value)
85 return
86
87 let rangeStr = props.ranges[index.value] ?? finallyRange.value
88 const hide = rangeStr === 'hide'
89 // Only toggle the class when a `hide` range set it, so a click directive on the same element keeps control
90 if (hide || hiddenByRange) {
91 el.value.classList.toggle(CLASS_VCLICK_HIDDEN, hide)
92 hiddenByRange = hide
93 }
94 if (hide)
95 rangeStr = props.ranges[index.value + 1] ?? finallyRange.value
96
97 const pre = el.value.querySelector('.shiki')!
98 const lines = Array.from(pre.querySelectorAll('code > .line'))
99 const linesCount = lines.length
100
101 updateCodeHighlightRange(
102 rangeStr,
103 linesCount,
104 props.startLine,
105 no => [lines[no]],
106 )
107
108 // Scroll to the highlighted line if `maxHeight` is set
109 if (props.maxHeight) {
110 const highlightedEls = Array.from(pre.querySelectorAll('.line.highlighted')) as HTMLElement[]
111 const height = highlightedEls.reduce((acc, el) => el.offsetHeight + acc, 0)
112 if (height > el.value.offsetHeight) {
113 highlightedEls[0].scrollIntoView({ behavior: 'smooth', block: 'start' })
114 }
115 else if (highlightedEls.length > 0) {
116 const middleEl = highlightedEls[Math.round((highlightedEls.length - 1) / 2)]
117 middleEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
118 }
119 }
120 })
121 })
122
123 const { copied, copy } = useClipboard()
124
125 function copyCode() {
126 const code = el.value?.querySelector('.slidev-code')?.textContent
127 if (code)
128 copy(code)
129 }
130
131 // code block title
132 const activeTitle = inject<Ref<string> | null>('activeTitle', null)
133
134 const isBlockTitleShow = computed(() => {
135 return activeTitle === null && props.title
136 })
137 </script>
138
139 <template>
140 <div
141 ref="el"
142 class="slidev-code-wrapper relative group"
143 :class="{
144 'slidev-code-line-numbers': props.lines,
145 'active': activeTitle === title,
146 }"
147 :style="{
148 'max-height': props.maxHeight,
149 'overflow-y': props.maxHeight ? 'scroll' : undefined,
150 '--start': props.startLine,
151 }"
152 :data-title="title"
153 >
154 <div v-if="isBlockTitleShow" class="slidev-code-block-title">
155 <TitleIcon :title="title" />
156 <div class="leading-1em">
157 {{ title.replace(/~([^~]+)~/g, '').trim() }}
158 </div>
159 </div>
160 <slot />
161 <button
162 v-if="isCodeCopy"
163 class="slidev-code-copy absolute right-0 transition opacity-0 group-hover:opacity-20 hover:!opacity-100"
164 :class="isBlockTitleShow ? 'top-10' : 'top-0'"
165 :title="copied ? 'Copied' : 'Copy'" @click="copyCode()"
166 >
167 <ph-check-circle v-if="copied" class="p-2 w-8 h-8" />
168 <ph-clipboard v-else class="p-2 w-8 h-8" />
169 </button>
170 </div>
171 </template>
172
172 lines Plain Text