@slidev/client 0.49.11 → 0.49.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ <svg width="115" height="115" viewBox="0 0 115 115" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g clip-path="url(#clip0)">
3
+ <g filter="url(#filter0_d)">
4
+ <rect x="14" y="14" width="90" height="90" rx="15" fill="url(#paint0_linear)"/>
5
+ </g>
6
+ <g filter="url(#filter1_d)">
7
+ <rect width="90" height="90" rx="45" fill="url(#paint1_linear)"/>
8
+ </g>
9
+ <g filter="url(#filter2_d)">
10
+ <path d="M56.127 63.925C54.9501 59.5327 54.3617 57.3366 54.9439 55.8199C55.4517 54.497 56.497 53.4517 57.8199 52.9439C59.3366 52.3617 61.5327 52.9501 65.925 54.127L87.7563 59.9767C92.1485 61.1536 94.3446 61.742 95.367 63.0046C96.2588 64.1058 96.6414 65.5338 96.4197 66.9334C96.1656 68.5379 94.5579 70.1456 91.3426 73.3609L75.3609 89.3426C72.1456 92.5579 70.5379 94.1656 68.9333 94.4197C67.5337 94.6414 66.1058 94.2588 65.0046 93.367C63.742 92.3446 63.1536 90.1485 61.9767 85.7563L56.127 63.925Z" fill="url(#paint2_linear)"/>
11
+ </g>
12
+ </g>
13
+ <defs>
14
+ <filter id="filter0_d" x="8" y="8" width="110" height="110" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
15
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
16
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
17
+ <feOffset dx="4" dy="4"/>
18
+ <feGaussianBlur stdDeviation="5"/>
19
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
20
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
21
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
22
+ </filter>
23
+ <filter id="filter1_d" x="-6" y="-6" width="110" height="110" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
24
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
25
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
26
+ <feOffset dx="4" dy="4"/>
27
+ <feGaussianBlur stdDeviation="5"/>
28
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
29
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
30
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
31
+ </filter>
32
+ <filter id="filter2_d" x="34.8833" y="32.8833" width="77.6614" height="77.6614" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
33
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
34
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
35
+ <feOffset dx="4" dy="4"/>
36
+ <feGaussianBlur stdDeviation="5"/>
37
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
38
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
39
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
40
+ </filter>
41
+ <linearGradient id="paint0_linear" x1="14" y1="6" x2="104" y2="104" gradientUnits="userSpaceOnUse">
42
+ <stop stop-color="#3ACBD4"/>
43
+ <stop offset="1" stop-color="#2988B1"/>
44
+ </linearGradient>
45
+ <linearGradient id="paint1_linear" x1="-9.5" y1="-11" x2="76.0825" y2="90" gradientUnits="userSpaceOnUse">
46
+ <stop stop-color="#95F0CF"/>
47
+ <stop offset="1" stop-color="#3AB9D5"/>
48
+ </linearGradient>
49
+ <linearGradient id="paint2_linear" x1="54.6616" y1="49.3453" x2="59.8793" y2="96.3585" gradientUnits="userSpaceOnUse">
50
+ <stop stop-color="#FFEB83"/>
51
+ <stop offset="0.0001" stop-color="#FFEB83"/>
52
+ <stop offset="0.0833333" stop-color="#FFDD35"/>
53
+ <stop offset="0.601773" stop-color="#FFBB13"/>
54
+ <stop offset="1" stop-color="#FFA800"/>
55
+ </linearGradient>
56
+ <clipPath id="clip0">
57
+ <rect width="115" height="115" fill="white"/>
58
+ </clipPath>
59
+ </defs>
60
+ </svg>
@@ -0,0 +1,8 @@
1
+ <template>
2
+ <span inline-flex items-center>
3
+ <span text-main>Powered by</span>
4
+ <a href="https://sli.dev" class="!border-none">
5
+ <img alt="Slidev logo" src="../assets/logo-title-horizontal.png" h-1.5em>
6
+ </a>
7
+ </span>
8
+ </template>
@@ -30,18 +30,42 @@ export function createClicksContextBase(
30
30
  clicksStart = 0,
31
31
  clicksTotalOverrides?: number,
32
32
  ): ClicksContext {
33
+ const isMounted = ref(false)
34
+ let relativeSizeMap: ClicksContext['relativeSizeMap'] = new Map()
35
+ let maxMap: ClicksContext['maxMap'] = new Map()
33
36
  const context: ClicksContext = {
34
37
  get current() {
35
- // Here we haven't know clicksTotal yet.
36
38
  return clamp(+current.value, clicksStart, context.total)
37
39
  },
38
40
  set current(value) {
39
- current.value = clamp(+value, clicksStart, context.total)
41
+ current.value = isMounted.value
42
+ ? clamp(value, clicksStart, context.total)
43
+ : value /* context.total is not available yet */
40
44
  },
41
45
  clicksStart,
42
- relativeOffsets: new Map(),
43
- maxMap: shallowReactive(new Map()),
44
- onMounted() { },
46
+ get relativeSizeMap() {
47
+ if (__DEV__ && isMounted.value)
48
+ console.warn('[slidev] ClicksContext: Unexpected access to relativeSizeMap after mounted')
49
+ return relativeSizeMap
50
+ },
51
+ get maxMap() {
52
+ return maxMap
53
+ },
54
+ get isMounted() {
55
+ return isMounted.value
56
+ },
57
+ onMounted: () => {
58
+ isMounted.value = true
59
+ // Convert maxMap to reactive
60
+ maxMap = shallowReactive(maxMap)
61
+ // Make sure the query is not greater than the total
62
+ context.current = current.value
63
+ },
64
+ onUnmounted: () => {
65
+ isMounted.value = false
66
+ relativeSizeMap = new Map()
67
+ maxMap = new Map()
68
+ },
45
69
  calculateSince(rawAt, size = 1) {
46
70
  const at = normalizeSingleAtValue(rawAt)
47
71
  if (at == null)
@@ -109,23 +133,29 @@ export function createClicksContextBase(
109
133
  register(el, info) {
110
134
  if (!info)
111
135
  return
136
+ if (__DEV__ && isMounted.value)
137
+ console.warn('[slidev] ClicksContext: Unexpected register after mounted')
112
138
  const { delta, max } = info
113
- context.relativeOffsets.set(el, delta)
114
- context.maxMap.set(el, max)
139
+ relativeSizeMap.set(el, delta)
140
+ maxMap.set(el, max)
115
141
  },
116
142
  unregister(el) {
117
- context.relativeOffsets.delete(el)
118
- context.maxMap.delete(el)
143
+ relativeSizeMap.delete(el)
144
+ maxMap.delete(el)
119
145
  },
120
146
  get currentOffset() {
121
147
  // eslint-disable-next-line no-unused-expressions
122
148
  routeForceRefresh.value
123
- return sum(...context.relativeOffsets.values())
149
+ return sum(...relativeSizeMap.values())
124
150
  },
125
151
  get total() {
126
152
  // eslint-disable-next-line no-unused-expressions
127
153
  routeForceRefresh.value
128
- return clicksTotalOverrides ?? Math.max(0, ...context.maxMap.values())
154
+ return clicksTotalOverrides
155
+ ?? (isMounted.value
156
+ ? Math.max(0, ...maxMap.values())
157
+ : 0 /* fallback value */
158
+ )
129
159
  },
130
160
  }
131
161
  return context
@@ -327,19 +327,13 @@ const useNavState = createSharedComposable((): SlidevContextNavState => {
327
327
  },
328
328
  set(v) {
329
329
  if (currentSlideNo.value === thisNo)
330
- queryClicksRaw.value = clamp(v, context.clicksStart, context.total).toString()
330
+ queryClicksRaw.value = v.toString()
331
331
  },
332
332
  }),
333
333
  route?.meta.slide?.frontmatter.clicksStart ?? 0,
334
334
  route?.meta.clicks,
335
335
  )
336
336
 
337
- // On slide mounted, make sure the query is not greater than the total
338
- context.onMounted = () => {
339
- if (currentSlideNo.value === thisNo)
340
- queryClicksRaw.value = clamp(+queryClicksRaw.value, context.clicksStart, context.total).toString()
341
- }
342
-
343
337
  if (route?.meta)
344
338
  route.meta.__clicksContext = context
345
339
 
@@ -4,7 +4,7 @@ import { computed } from 'vue'
4
4
  import { getSlidePath } from '../logic/slides'
5
5
 
6
6
  function addToTree(tree: TocItem[], route: SlideRoute, level = 1) {
7
- const titleLevel = route.meta?.slide?.level
7
+ const titleLevel = route.meta.slide.level ?? level
8
8
  if (titleLevel && titleLevel > level && tree.length > 0) {
9
9
  addToTree(tree[tree.length - 1].children, route, level + 1)
10
10
  }
@@ -13,6 +13,7 @@ function addToTree(tree: TocItem[], route: SlideRoute, level = 1) {
13
13
  no: route.no,
14
14
  children: [],
15
15
  level,
16
+ titleLevel,
16
17
  path: getSlidePath(route.meta.slide?.frontmatter?.routeAlias ?? route.no, false),
17
18
  hideInToc: Boolean(route.meta?.slide?.frontmatter?.hideInToc),
18
19
  title: route.meta?.slide?.title,
package/index.ts CHANGED
@@ -7,6 +7,7 @@ export { useSlideContext } from './context'
7
7
  export { useNav } from './composables/useNav'
8
8
  export { useDrawings } from './composables/useDrawings'
9
9
  export { useDarkMode } from './composables/useDarkMode'
10
+ export { useIsSlideActive, onSlideEnter, onSlideLeave } from './logic/slides'
10
11
 
11
12
  export * from './layoutHelper'
12
13
  export * from './env'
@@ -39,7 +39,7 @@ function onMousedown() {
39
39
  <div
40
40
  class="flex gap-1 items-center select-none"
41
41
  :title="`Clicks in this slide: ${length}`"
42
- :class="length ? '' : 'op50'"
42
+ :class="length && props.clicksContext.isMounted ? '' : 'op50'"
43
43
  >
44
44
  <div class="flex gap-0.5 items-center min-w-16 font-mono mr1">
45
45
  <carbon:cursor-1 text-sm op50 />
@@ -1,8 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import { debounce, toArray } from '@antfu/utils'
3
3
  import { useVModel } from '@vueuse/core'
4
- import type { CodeRunnerOutput, RawAtValue } from '@slidev/types'
5
- import { computed, onMounted, onUnmounted, ref, shallowRef, watch, watchSyncEffect } from 'vue'
4
+ import type { CodeRunnerOutputs, RawAtValue } from '@slidev/types'
5
+ import { computed, onMounted, onUnmounted, ref, shallowRef, toValue, watch, watchSyncEffect } from 'vue'
6
6
  import { useSlideContext } from '../context'
7
7
  import setupCodeRunners from '../setup/code-runners'
8
8
  import { useNav } from '../composables/useNav'
@@ -31,7 +31,7 @@ const disabled = computed(() => !['slide', 'presenter'].includes($renderContext.
31
31
 
32
32
  const autorun = isPrintMode.value ? 'once' : props.autorun
33
33
  const isRunning = ref(autorun)
34
- const outputs = shallowRef<CodeRunnerOutput[]>()
34
+ const outputs = shallowRef<CodeRunnerOutputs>()
35
35
  const runCount = ref(0)
36
36
  const highlightFn = ref<(code: string, lang: string) => string>()
37
37
 
@@ -64,7 +64,7 @@ const triggerRun = debounce(200, async () => {
64
64
  isRunning.value = true
65
65
  }, 500)
66
66
 
67
- outputs.value = toArray(await run(code.value, props.lang, props.runnerOptions ?? {}))
67
+ outputs.value = await run(code.value, props.lang, props.runnerOptions ?? {})
68
68
  runCount.value += 1
69
69
  isRunning.value = false
70
70
 
@@ -90,11 +90,11 @@ else if (autorun)
90
90
  <div v-else-if="isRunning" class="text-sm text-center opacity-50">
91
91
  Running...
92
92
  </div>
93
- <div v-else-if="!outputs?.length" class="text-sm text-center opacity-50">
93
+ <div v-else-if="!outputs" class="text-sm text-center opacity-50">
94
94
  Click the play button to run the code
95
95
  </div>
96
96
  <div v-else :key="`run-${runCount}`" class="slidev-runner-output">
97
- <template v-for="output, _idx1 of outputs" :key="_idx1">
97
+ <template v-for="output, _idx1 of toArray(toValue(outputs))" :key="_idx1">
98
98
  <div v-if="'html' in output" v-html="output.html" />
99
99
  <div v-else-if="'error' in output" class="text-red-500">
100
100
  {{ output.error }}
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, nextTick, onMounted, ref, watch } from 'vue'
2
+ import { computed, nextTick, onMounted, ref, watch, watchEffect } from 'vue'
3
3
  import type { ClicksContext } from '@slidev/types'
4
4
  import { CLICKS_MAX } from '../constants'
5
5
 
@@ -29,108 +29,118 @@ const noteDisplay = ref<HTMLElement | null>(null)
29
29
  const CLASS_FADE = 'slidev-note-fade'
30
30
  const CLASS_MARKER = 'slidev-note-click-mark'
31
31
 
32
- function highlightNote() {
32
+ function processNote() {
33
33
  if (!noteDisplay.value || !withClicks.value)
34
34
  return
35
35
 
36
36
  const markers = Array.from(noteDisplay.value.querySelectorAll(`.${CLASS_MARKER}`)) as HTMLElement[]
37
37
 
38
- const current = +(props.clicksContext?.current ?? CLICKS_MAX)
39
- const disabled = current < 0 || current >= CLICKS_MAX || !props.highlight
40
-
41
- const nodeToIgnores = new Set<Element>()
42
- function ignoreParent(node: Element) {
43
- if (!node || node === noteDisplay.value)
44
- return
45
- nodeToIgnores.add(node)
46
- if (node.parentElement)
47
- ignoreParent(node.parentElement)
48
- }
49
-
50
- const markersMap = new Map<number, HTMLElement>()
51
-
52
- // Convert all sibling text nodes to spans, so we attach classes to them
38
+ const markersMap = new Map<HTMLElement, number>()
39
+ const parentsMap = new Map<HTMLElement, [divider: HTMLElement | null, dividerClicks: number][]>()
40
+ let lastClicks = 0
53
41
  for (const marker of markers) {
54
- const parent = marker.parentElement!
55
42
  const clicks = Number(marker.dataset!.clicks)
56
- markersMap.set(clicks, marker)
57
- // Ignore the parents of the marker, so the class only applies to the children
58
- ignoreParent(parent)
59
- Array.from(parent!.childNodes)
60
- .forEach((node) => {
61
- if (node.nodeType === 3) { // text node
62
- const span = document.createElement('span')
63
- span.textContent = node.textContent
64
- parent.insertBefore(span, node)
65
- node.remove()
66
- }
67
- })
68
- }
69
- const children = Array.from(noteDisplay.value.querySelectorAll('*'))
70
-
71
- let count = 0
72
-
73
- // Segmenting notes by clicks
74
- const segments = new Map<number, Element[]>()
75
- for (const child of children) {
76
- if (!segments.has(count))
77
- segments.set(count, [])
78
- segments.get(count)!.push(child)
79
- // Update count when reach marker
80
- if (child.classList.contains(CLASS_MARKER))
81
- count = Number((child as HTMLElement).dataset.clicks) || (count + 1)
43
+ markersMap.set(marker, clicks)
44
+
45
+ // Set parent clicks map
46
+ let n = marker
47
+ let p = marker.parentElement
48
+ while (p && n !== noteDisplay.value) {
49
+ if (!parentsMap.has(p))
50
+ parentsMap.set(p, [[null, lastClicks]])
51
+ parentsMap.get(p)!.push([n, clicks])
52
+ n = p
53
+ p = p.parentElement
54
+ }
55
+
56
+ lastClicks = clicks
82
57
  }
83
58
 
84
- // Apply
85
- for (const [count, els] of segments) {
86
- if (disabled) {
87
- els.forEach(el => el.classList.remove(CLASS_FADE))
88
- }
89
- else {
90
- els.forEach(el => el.classList.toggle(
91
- CLASS_FADE,
92
- nodeToIgnores.has(el)
93
- ? false
94
- : count !== current,
95
- ))
59
+ const siblingsMap = new Map<HTMLElement, number>()
60
+ for (const [parent, dividers] of parentsMap) {
61
+ let hasPrefix = false
62
+ let dividerIdx = 0
63
+ for (const sibling of Array.from(parent.childNodes)) {
64
+ let skip = false
65
+ while (sibling === dividers[dividerIdx + 1]?.[0]) {
66
+ skip = true
67
+ dividerIdx++
68
+ }
69
+ if (skip)
70
+ continue
71
+
72
+ // Convert sibling text nodes to spans
73
+ let siblingEl = sibling as HTMLElement
74
+ if (sibling.nodeType === 3 /* text node */) {
75
+ if (!sibling.textContent?.trim())
76
+ continue
77
+ siblingEl = document.createElement('span')
78
+ siblingEl.textContent = sibling.textContent
79
+ parent.insertBefore(siblingEl, sibling)
80
+ sibling.remove()
81
+ }
82
+
83
+ hasPrefix ||= dividerIdx === 0
84
+ siblingsMap.set(siblingEl, dividers[dividerIdx][1])
96
85
  }
86
+ if (!hasPrefix)
87
+ dividers[0][1] = -1
97
88
  }
98
89
 
99
- for (const [clicks, marker] of markersMap) {
100
- marker.classList.remove(CLASS_FADE)
101
- marker.classList.toggle(`${CLASS_MARKER}-past`, disabled ? false : clicks < current)
102
- marker.classList.toggle(`${CLASS_MARKER}-active`, disabled ? false : clicks === current)
103
- marker.classList.toggle(`${CLASS_MARKER}-next`, disabled ? false : clicks === current + 1)
104
- marker.classList.toggle(`${CLASS_MARKER}-future`, disabled ? false : clicks > current + 1)
105
- marker.ondblclick = (e) => {
106
- emit('markerDblclick', e, clicks)
107
- if (e.defaultPrevented)
108
- return
109
- props.clicksContext!.current = clicks
110
- e.stopPropagation()
111
- e.stopImmediatePropagation()
112
- }
113
- marker.onclick = (e) => {
114
- emit('markerClick', e, clicks)
90
+ // Apply
91
+ return (current: number) => {
92
+ const enabled = props.highlight
93
+ for (const [parent, clicks] of parentsMap)
94
+ parent.classList.toggle(CLASS_FADE, enabled && !clicks.some(([_, c]) => c === current))
95
+ for (const [parent, clicks] of siblingsMap)
96
+ parent.classList.toggle(CLASS_FADE, enabled && clicks !== current)
97
+ for (const [marker, clicks] of markersMap) {
98
+ marker.classList.remove(CLASS_FADE)
99
+ marker.classList.toggle(`${CLASS_MARKER}-past`, enabled && clicks < current)
100
+ marker.classList.toggle(`${CLASS_MARKER}-active`, enabled && clicks === current)
101
+ marker.classList.toggle(`${CLASS_MARKER}-next`, enabled && clicks === current + 1)
102
+ marker.classList.toggle(`${CLASS_MARKER}-future`, enabled && clicks > current + 1)
103
+ marker.ondblclick = enabled
104
+ ? (e) => {
105
+ emit('markerDblclick', e, clicks)
106
+ if (e.defaultPrevented)
107
+ return
108
+ props.clicksContext!.current = clicks
109
+ e.stopPropagation()
110
+ e.stopImmediatePropagation()
111
+ }
112
+ : null
113
+ marker.onclick = enabled
114
+ ? (e) => {
115
+ emit('markerClick', e, clicks)
116
+ }
117
+ : null
118
+
119
+ if (!enabled && props.autoScroll && clicks === current)
120
+ marker.scrollIntoView({ block: 'center', behavior: 'smooth' })
115
121
  }
116
-
117
- if (props.autoScroll && clicks === current)
118
- marker.scrollIntoView({ block: 'center', behavior: 'smooth' })
119
122
  }
120
123
  }
121
124
 
125
+ const applyHighlight = ref<ReturnType<typeof processNote>>()
126
+
122
127
  watch(
123
- () => [props.noteHtml, props.clicksContext?.current, props.highlight],
128
+ () => [props.noteHtml, props.highlight],
124
129
  () => {
125
130
  nextTick(() => {
126
- highlightNote()
131
+ applyHighlight.value = processNote()
127
132
  })
128
133
  },
129
134
  { immediate: true },
130
135
  )
131
136
 
132
137
  onMounted(() => {
133
- highlightNote()
138
+ processNote()
139
+ })
140
+
141
+ watchEffect(() => {
142
+ const current = props.clicksContext?.current ?? CLICKS_MAX
143
+ applyHighlight.value?.(current)
134
144
  })
135
145
  </script>
136
146
 
@@ -0,0 +1,44 @@
1
+ <script setup lang="ts">
2
+ import { ref, shallowRef } from 'vue'
3
+ import { getHighlighter } from '#slidev/shiki'
4
+
5
+ const props = defineProps<{
6
+ placeholder?: string
7
+ }>()
8
+ const content = defineModel<string>({ required: true })
9
+
10
+ const textareaEl = ref<HTMLTextAreaElement | null>(null)
11
+
12
+ const highlight = shallowRef<Awaited<ReturnType<typeof getHighlighter>> | null>(null)
13
+ getHighlighter().then(h => highlight.value = h)
14
+ </script>
15
+
16
+ <template>
17
+ <div class="absolute left-3 right-0 inset-y-2 font-mono overflow-x-hidden overflow-y-auto cursor-text">
18
+ <div v-if="highlight" class="relative w-full h-max min-h-full">
19
+ <div class="relative w-full h-max" v-html="`${highlight(content, 'markdown')}&nbsp;`" />
20
+ <textarea
21
+ ref="textareaEl" v-model="content" :placeholder="props.placeholder"
22
+ class="absolute inset-0 resize-none text-transparent bg-transparent focus:outline-none caret-black dark:caret-white overflow-y-hidden"
23
+ />
24
+ </div>
25
+ </div>
26
+ </template>
27
+
28
+ <style scoped>
29
+ :deep(code),
30
+ textarea {
31
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
32
+ 'Liberation Mono', 'Courier New', monospace;
33
+ font-feature-settings: normal;
34
+ font-variation-settings: normal;
35
+ font-size: 1em;
36
+ text-wrap: wrap;
37
+ word-break: break-all;
38
+ display: block;
39
+ width: 100%;
40
+ }
41
+ :deep(pre.shiki) {
42
+ background-color: transparent;
43
+ }
44
+ </style>
@@ -1,11 +1,11 @@
1
1
  <script setup lang="ts">
2
- import { throttledWatch, useEventListener, watchThrottled } from '@vueuse/core'
3
- import { computed, nextTick, onMounted, ref, watch } from 'vue'
4
- import { activeElement, editorHeight, editorWidth, isEditorVertical, isInputting, showEditor, isEditorVertical as vertical } from '../state'
5
- import { useCodeMirror } from '../modules/codemirror'
2
+ import { throttledWatch, useEventListener } from '@vueuse/core'
3
+ import { computed, ref, watch } from 'vue'
4
+ import { activeElement, editorHeight, editorWidth, isInputting, showEditor, isEditorVertical as vertical } from '../state'
6
5
  import { useNav } from '../composables/useNav'
7
6
  import { useDynamicSlideInfo } from '../composables/useSlideInfo'
8
7
  import IconButton from './IconButton.vue'
8
+ import ShikiEditor from './ShikiEditor.vue'
9
9
 
10
10
  const props = defineProps<{
11
11
  resize?: boolean
@@ -18,8 +18,6 @@ const content = ref('')
18
18
  const note = ref('')
19
19
  const dirty = ref(false)
20
20
  const frontmatter = ref<any>({})
21
- const contentInput = ref<HTMLTextAreaElement>()
22
- const noteInput = ref<HTMLTextAreaElement>()
23
21
 
24
22
  const { info, update } = useDynamicSlideInfo(currentSlideNo)
25
23
 
@@ -57,65 +55,21 @@ useEventListener('keydown', (e) => {
57
55
  }
58
56
  })
59
57
 
60
- onMounted(async () => {
61
- const contentEditor = await useCodeMirror(
62
- contentInput,
63
- computed({
64
- get() { return content.value },
65
- set(v) {
66
- if (content.value.trim() !== v.trim()) {
67
- content.value = v
68
- dirty.value = true
69
- }
70
- },
71
- }),
72
- {
73
- mode: 'markdown',
74
- lineWrapping: true,
75
- // @ts-expect-error missing types
76
- highlightFormatting: true,
77
- fencedCodeBlockDefaultMode: 'javascript',
78
- },
79
- )
80
-
81
- const noteEditor = await useCodeMirror(
82
- noteInput,
83
- computed({
84
- get() { return note.value },
85
- set(v) {
86
- note.value = v
87
- dirty.value = true
88
- },
89
- }),
90
- {
91
- mode: 'markdown',
92
- lineWrapping: true,
93
- // @ts-expect-error missing types
94
- highlightFormatting: true,
95
- fencedCodeBlockDefaultMode: 'javascript',
96
- },
97
- )
98
-
99
- watchThrottled(
100
- [tab, vertical, isEditorVertical, editorWidth, editorHeight],
101
- () => {
102
- nextTick(() => {
103
- if (tab.value === 'content')
104
- contentEditor.refresh()
105
- else
106
- noteEditor.refresh()
107
- })
108
- },
109
- {
110
- throttle: 100,
111
- flush: 'post',
112
- },
113
- )
58
+ const contentRef = computed({
59
+ get() { return content.value },
60
+ set(v) {
61
+ if (content.value.trim() !== v.trim())
62
+ dirty.value = true
63
+ content.value = v
64
+ },
65
+ })
114
66
 
115
- watch(currentSlideNo, () => {
116
- contentEditor.clearHistory()
117
- noteEditor.clearHistory()
118
- }, { flush: 'post' })
67
+ const noteRef = computed({
68
+ get() { return note.value },
69
+ set(v) {
70
+ note.value = v
71
+ dirty.value = true
72
+ },
119
73
  })
120
74
 
121
75
  const handlerDown = ref(false)
@@ -171,7 +125,7 @@ throttledWatch(
171
125
  }" @pointerdown="onHandlerDown"
172
126
  />
173
127
  <div
174
- class="shadow bg-main p-4 grid grid-rows-[max-content_1fr] h-full overflow-hidden"
128
+ class="shadow bg-main p-2 pt-4 grid grid-rows-[max-content_1fr] h-full overflow-hidden"
175
129
  :class="resize ? 'border-l border-gray-400 border-opacity-20' : ''"
176
130
  :style="resize ? {
177
131
  height: vertical ? `${editorHeight}px` : undefined,
@@ -212,19 +166,9 @@ throttledWatch(
212
166
  <carbon:close />
213
167
  </IconButton>
214
168
  </div>
215
- <div class="overflow-hidden">
216
- <div v-show="tab === 'content'" class="w-full h-full">
217
- <textarea ref="contentInput" placeholder="Create slide content..." />
218
- </div>
219
- <div v-show="tab === 'note'" class="w-full h-full">
220
- <textarea ref="noteInput" placeholder="Write some notes..." />
221
- </div>
169
+ <div class="relative overflow-hidden rounded" style="background-color: var(--slidev-code-background)">
170
+ <ShikiEditor v-show="tab === 'content'" v-model="contentRef" placeholder="Create slide content..." />
171
+ <ShikiEditor v-show="tab === 'note'" v-model="noteRef" placeholder="Write some notes..." />
222
172
  </div>
223
173
  </div>
224
174
  </template>
225
-
226
- <style lang="postcss">
227
- .CodeMirror {
228
- @apply px-3 py-2 h-full overflow-hidden bg-transparent font-mono text-sm z-0;
229
- }
230
- </style>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, defineAsyncComponent, defineComponent, h, onMounted, ref, toRef } from 'vue'
2
+ import { computed, defineAsyncComponent, defineComponent, h, ref, toRef } from 'vue'
3
3
  import type { CSSProperties, PropType } from 'vue'
4
4
  import { provideLocal } from '@vueuse/core'
5
5
  import type { ClicksContext, RenderContext, SlideRoute } from '@slidev/types'
@@ -51,10 +51,9 @@ const SlideComponent = computed(() => props.route && defineAsyncComponent({
51
51
  loader: async () => {
52
52
  const component = await props.route.component()
53
53
  return defineComponent({
54
- setup(_, { attrs }) {
55
- onMounted(() => props.clicksContext?.onMounted?.())
56
- return () => h(component.default, attrs)
57
- },
54
+ mounted: props.clicksContext?.onMounted,
55
+ unmounted: props.clicksContext?.onUnmounted,
56
+ render: () => h(component.default),
58
57
  })
59
58
  },
60
59
  delay: 300,
package/logic/slides.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import type { SlideRoute } from '@slidev/types'
2
+ import { computed, watch, watchEffect } from 'vue'
3
+ import { useSlideContext } from '../context'
4
+ import { useNav } from '../composables/useNav'
2
5
  import { slides } from '#slidev/slides'
3
6
 
4
7
  export { slides }
@@ -18,3 +21,19 @@ export function getSlidePath(
18
21
  const no = route.meta.slide?.frontmatter.routeAlias ?? route.no
19
22
  return presenter ? `/presenter/${no}` : `/${no}`
20
23
  }
24
+
25
+ export function useIsSlideActive() {
26
+ const { $page } = useSlideContext()
27
+ const { currentSlideNo } = useNav()
28
+ return computed(() => $page.value === currentSlideNo.value)
29
+ }
30
+
31
+ export function onSlideEnter(cb: () => void) {
32
+ const isActive = useIsSlideActive()
33
+ watchEffect(() => isActive.value && cb())
34
+ }
35
+
36
+ export function onSlideLeave(cb: () => void) {
37
+ const isActive = useIsSlideActive()
38
+ watch(isActive, () => !isActive.value && cb())
39
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "0.49.11",
4
+ "version": "0.49.13",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "antfu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -28,20 +28,19 @@
28
28
  "node": ">=18.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@antfu/utils": "^0.7.8",
32
- "@iconify-json/carbon": "^1.1.34",
31
+ "@antfu/utils": "^0.7.10",
32
+ "@iconify-json/carbon": "^1.1.36",
33
33
  "@iconify-json/ph": "^1.1.13",
34
34
  "@iconify-json/svg-spinners": "^1.1.2",
35
- "@shikijs/monaco": "^1.6.1",
36
- "@shikijs/vitepress-twoslash": "^1.6.1",
35
+ "@shikijs/monaco": "^1.10.0",
36
+ "@shikijs/vitepress-twoslash": "^1.10.0",
37
37
  "@slidev/rough-notation": "^0.1.0",
38
- "@typescript/ata": "^0.9.5",
39
- "@unhead/vue": "^1.9.11",
40
- "@unocss/reset": "^0.60.4",
41
- "@vueuse/core": "^10.10.0",
42
- "@vueuse/math": "^10.10.0",
38
+ "@typescript/ata": "^0.9.6",
39
+ "@unhead/vue": "^1.9.14",
40
+ "@unocss/reset": "^0.61.0",
41
+ "@vueuse/core": "^10.11.0",
42
+ "@vueuse/math": "^10.11.0",
43
43
  "@vueuse/motion": "^2.2.3",
44
- "codemirror": "^5.65.16",
45
44
  "drauu": "^0.4.0",
46
45
  "file-saver": "^2.0.5",
47
46
  "floating-vue": "^5.2.2",
@@ -49,20 +48,20 @@
49
48
  "katex": "^0.16.10",
50
49
  "lz-string": "^1.5.0",
51
50
  "mermaid": "^10.9.1",
52
- "monaco-editor": "^0.49.0",
53
- "prettier": "^3.3.0",
51
+ "monaco-editor": "^0.50.0",
52
+ "prettier": "^3.3.2",
54
53
  "recordrtc": "^5.6.2",
55
- "shiki": "^1.6.1",
54
+ "shiki": "^1.10.0",
56
55
  "shiki-magic-move": "^0.4.2",
57
- "typescript": "^5.4.5",
58
- "unocss": "^0.60.4",
59
- "vue": "^3.4.27",
60
- "vue-router": "^4.3.2",
61
- "yaml": "^2.4.2",
62
- "@slidev/parser": "0.49.11",
63
- "@slidev/types": "0.49.11"
56
+ "typescript": "^5.5.2",
57
+ "unocss": "^0.61.0",
58
+ "vue": "^3.4.31",
59
+ "vue-router": "^4.4.0",
60
+ "yaml": "^2.4.5",
61
+ "@slidev/types": "0.49.13",
62
+ "@slidev/parser": "0.49.13"
64
63
  },
65
64
  "devDependencies": {
66
- "vite": "^5.2.12"
65
+ "vite": "^5.3.2"
67
66
  }
68
67
  }
package/pages/404.vue CHANGED
@@ -1,4 +1,4 @@
1
- <script setup>
1
+ <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
3
  import { useRouter } from 'vue-router'
4
4
  import { useNav } from '../composables/useNav'
@@ -19,22 +19,22 @@ const guessedSlide = computed(() => {
19
19
  </script>
20
20
 
21
21
  <template>
22
- <div class="grid justify-center pt-15%">
22
+ <div class="grid justify-center text-center pt-15% gap-5">
23
23
  <div>
24
- <h1 class="text-9xl font-bold">
24
+ <h1 class="text-9xl font-light">
25
25
  404
26
26
  </h1>
27
27
  <p class="text-2xl">
28
- Page not found<code class="op-70">:{{ currentRoute.path }}</code>
28
+ Page <code class="op-60">{{ currentRoute.path }}</code> not found
29
29
  </p>
30
- <div class="mt-3 flex flex-col gap-2">
31
- <RouterLink v-if="guessedSlide !== 1" to="/" class="page-link">
32
- Go Home
33
- </RouterLink>
34
- <RouterLink v-if="guessedSlide" :to="`/${guessedSlide}`" class="page-link">
35
- Go to Slide {{ guessedSlide }}
36
- </RouterLink>
37
- </div>
30
+ </div>
31
+ <div class="mt-3 flex flex-col gap-2 max-w-xs mx-auto w-full">
32
+ <RouterLink v-if="guessedSlide !== 1" to="/" class="page-link">
33
+ Go Home
34
+ </RouterLink>
35
+ <RouterLink v-if="guessedSlide" :to="`/${guessedSlide}`" class="page-link">
36
+ Go to Slide {{ guessedSlide }}
37
+ </RouterLink>
38
38
  </div>
39
39
  </div>
40
40
  </template>
@@ -165,7 +165,7 @@ onMounted(() => {
165
165
  <carbon:presentation-file />
166
166
  </IconButton>
167
167
  <IconButton
168
- v-if="route.meta?.slide"
168
+ v-if="__DEV__ && route.meta?.slide"
169
169
  class="mr--3 op0 group-hover:op80"
170
170
  title="Open in editor"
171
171
  @click="openInEditor(`${route.meta.slide.filepath}:${route.meta.slide.start}`)"
package/pages/play.vue CHANGED
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref, shallowRef } from 'vue'
3
- import { isEditorVertical, isScreenVertical, showEditor, windowSize } from '../state'
3
+ import { useStyleTag } from '@vueuse/core'
4
+ import { editorHeight, editorWidth, isEditorVertical, isScreenVertical, showEditor, windowSize } from '../state'
4
5
  import { useSwipeControls } from '../composables/useSwipeControls'
5
6
  import { registerShortcuts } from '../logic/shortcuts'
6
7
  import Controls from '../internals/Controls.vue'
@@ -36,6 +37,19 @@ registerShortcuts()
36
37
  if (__SLIDEV_FEATURE_WAKE_LOCK__)
37
38
  useWakeLock()
38
39
 
40
+ useStyleTag(computed(() => `
41
+ vite-error-overlay {
42
+ --width: calc(100vw - ${isEditorVertical.value ? 0 : editorWidth.value}px);
43
+ --height: calc(100vh - ${isEditorVertical.value ? editorHeight.value : 0}px);
44
+ position: fixed;
45
+ left: 0;
46
+ top: 0;
47
+ width: calc(var(--width) / var(--slidev-slide-scale));
48
+ height: calc(var(--height) / var(--slidev-slide-scale));
49
+ transform-origin: top left;
50
+ transform: scale(var(--slidev-slide-scale));
51
+ }`))
52
+
39
53
  const persistNav = computed(() => isScreenVertical.value || showEditor.value)
40
54
 
41
55
  const SideEditor = shallowRef<any>()
@@ -40,7 +40,6 @@ const {
40
40
  hasNext,
41
41
  nextRoute,
42
42
  slides,
43
- queryClicks,
44
43
  getPrimaryClicks,
45
44
  total,
46
45
  } = useNav()
@@ -67,10 +66,10 @@ const nextFrameClicksCtx = computed(() => {
67
66
  })
68
67
 
69
68
  watch(
70
- [currentSlideRoute, queryClicks],
69
+ nextFrame,
71
70
  () => {
72
- if (nextFrameClicksCtx.value)
73
- nextFrameClicksCtx.value.current = nextFrame.value![1]
71
+ if (nextFrameClicksCtx.value && nextFrame.value)
72
+ nextFrameClicksCtx.value.current = nextFrame.value[1]
74
73
  },
75
74
  { immediate: true },
76
75
  )
@@ -1,8 +1,8 @@
1
1
  import { createSingletonPromise } from '@antfu/utils'
2
2
  import type { CodeRunner, CodeRunnerOutput, CodeRunnerOutputText, CodeRunnerOutputs } from '@slidev/types'
3
- import type { CodeToHastOptions } from 'shiki'
3
+
4
4
  import type ts from 'typescript'
5
- import { isDark } from '../logic/dark'
5
+ import { ref } from 'vue'
6
6
  import deps from '#slidev/monaco-run-deps'
7
7
  import setups from '#slidev/setups/code-runners'
8
8
 
@@ -14,17 +14,8 @@ export default createSingletonPromise(async () => {
14
14
  ts: runTypeScript,
15
15
  }
16
16
 
17
- const { shiki, themes } = await import('#slidev/shiki')
18
- const highlighter = await shiki
19
- const highlight = (code: string, lang: string, options: Partial<CodeToHastOptions> = {}) => highlighter.codeToHtml(code, {
20
- lang,
21
- theme: typeof themes === 'string'
22
- ? themes
23
- : isDark.value
24
- ? themes.dark || 'vitesse-dark'
25
- : themes.light || 'vitesse-light',
26
- ...options,
27
- })
17
+ const { getHighlighter } = await import('#slidev/shiki')
18
+ const highlight = await getHighlighter()
28
19
 
29
20
  const run = async (code: string, lang: string, options: Record<string, unknown>): Promise<CodeRunnerOutputs> => {
30
21
  try {
@@ -62,32 +53,32 @@ export default createSingletonPromise(async () => {
62
53
  })
63
54
 
64
55
  // Ported from https://github.com/microsoft/TypeScript-Website/blob/v2/packages/playground/src/sidebar/runtime.ts
65
- async function runJavaScript(code: string): Promise<CodeRunnerOutputs> {
66
- const allLogs: CodeRunnerOutput[] = []
67
-
68
- const replace = {} as any
69
- const logger = function (...objs: any[]) {
70
- allLogs.push(objs.map(printObject))
71
- }
72
- replace.info = replace.log = replace.debug = replace.warn = replace.error = logger
73
- replace.clear = () => allLogs.length = 0
74
- const vmConsole = Object.assign({}, console, replace)
56
+ function runJavaScript(code: string): CodeRunnerOutputs {
57
+ const result = ref<CodeRunnerOutput[]>([])
58
+
59
+ const onError = (error: any) => result.value.push({ error: String(error) })
60
+ const logger = (...objs: any[]) => result.value.push(objs.map(printObject))
61
+ const vmConsole = Object.assign({}, console)
62
+ vmConsole.info = vmConsole.log = vmConsole.debug = vmConsole.warn = vmConsole.error = logger
63
+ vmConsole.clear = () => result.value.length = 0
75
64
  try {
76
- const safeJS = `return async (console, __slidev_import) => {
77
- ${sanitizeJS(code)}
65
+ const safeJS = `return async (console, __slidev_import, __slidev_on_error) => {
66
+ try {
67
+ ${sanitizeJS(code)}
68
+ } catch (e) {
69
+ __slidev_on_error(e)
70
+ }
78
71
  }`
79
72
  // eslint-disable-next-line no-new-func
80
- await (new Function(safeJS)())(vmConsole, (specifier: string) => {
73
+ ;(new Function(safeJS)())(vmConsole, (specifier: string) => {
81
74
  const mod = deps[specifier]
82
75
  if (!mod)
83
76
  throw new Error(`Module not found: ${specifier}.\nAvailable modules: ${Object.keys(deps).join(', ')}. Please refer to https://sli.dev/custom/config-code-runners#additional-runner-dependencies`)
84
77
  return mod
85
- })
78
+ }, onError)
86
79
  }
87
80
  catch (error) {
88
- return {
89
- error: String(error),
90
- }
81
+ onError(error)
91
82
  }
92
83
 
93
84
  function printObject(arg: any): CodeRunnerOutputText {
@@ -162,7 +153,7 @@ async function runJavaScript(code: string): Promise<CodeRunnerOutputs> {
162
153
  return code
163
154
  }
164
155
 
165
- return allLogs
156
+ return result
166
157
  }
167
158
 
168
159
  let tsModule: typeof import('typescript') | undefined
@@ -183,7 +174,7 @@ export async function runTypeScript(code: string) {
183
174
  const importRegex = /import\s*\((.+)\)/g
184
175
  code = code.replace(importRegex, (_full, specifier) => `__slidev_import(${specifier})`)
185
176
 
186
- return await runJavaScript(code)
177
+ return runJavaScript(code)
187
178
  }
188
179
 
189
180
  /**
package/setup/main.ts CHANGED
@@ -13,7 +13,6 @@ import setupRoutes from '../setup/routes'
13
13
  import setups from '#slidev/setups/main'
14
14
 
15
15
  import '#slidev/styles'
16
- import 'shiki-magic-move/style.css'
17
16
 
18
17
  export default async function setupMain(app: App) {
19
18
  function setMaxHeight() {
package/setup/root.ts CHANGED
@@ -21,7 +21,7 @@ export default function setupRoot() {
21
21
  configs,
22
22
  themeConfigs: computed(() => configs.themeConfig),
23
23
  })
24
- app.provide(injectionRenderContext, ref('none'))
24
+ app.provide(injectionRenderContext, ref('none' as const))
25
25
  app.provide(injectionSlidevContext, context)
26
26
  app.provide(injectionCurrentPage, computed(() => context.nav.currentSlideNo))
27
27
  app.provide(injectionClicksContext, shallowRef(createFixedClicks()))
package/state/index.ts CHANGED
@@ -19,7 +19,7 @@ export const isScreenVertical = computed(() => windowSize.height.value - windowS
19
19
  export const fullscreen = useFullscreen(isClient ? document.body : null)
20
20
 
21
21
  export const activeElement = useActiveElement()
22
- export const isInputting = computed(() => ['INPUT', 'TEXTAREA'].includes(activeElement.value?.tagName || '') || activeElement.value?.classList.contains('CodeMirror-code'))
22
+ export const isInputting = computed(() => ['INPUT', 'TEXTAREA'].includes(activeElement.value?.tagName || ''))
23
23
  export const isOnFocus = computed(() => ['BUTTON', 'A'].includes(activeElement.value?.tagName || ''))
24
24
 
25
25
  export const currentCamera = useLocalStorage<string>('slidev-camera', 'default', { listenToStorageChanges: false })
package/styles/code.css CHANGED
@@ -5,6 +5,7 @@ html.dark:root {
5
5
  /* Shiki */
6
6
  html.dark .shiki {
7
7
  color: var(--shiki-dark, inherit);
8
+ background: var(--shiki-dark-bg, inherit);
8
9
  --twoslash-popup-bg: var(--shiki-dark-bg, inherit);
9
10
  }
10
11
 
@@ -82,8 +83,3 @@ html:not(.dark) .shiki span {
82
83
  .katex :before {
83
84
  border-color: currentColor;
84
85
  }
85
-
86
- /* CodeMirror */
87
- .CodeMirror pre.CodeMirror-placeholder {
88
- opacity: 0.4;
89
- }
package/uno.config.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { fileURLToPath } from 'node:url'
2
+ import { readFileSync } from 'node:fs'
2
3
  import {
3
4
  defineConfig,
4
5
  presetAttributify,
@@ -42,6 +43,14 @@ export default defineConfig({
42
43
  presetAttributify(),
43
44
  presetIcons({
44
45
  collectionsNodeResolvePath: fileURLToPath(import.meta.url),
46
+ collections: {
47
+ slidev: {
48
+ logo: async () => {
49
+ const content = readFileSync(fileURLToPath(new URL('assets/logo.svg', import.meta.url)), 'utf-8')
50
+ return content
51
+ },
52
+ },
53
+ },
45
54
  }),
46
55
  presetTypography(),
47
56
  ],
@@ -1,56 +0,0 @@
1
- import type { Ref, WritableComputedRef } from 'vue'
2
- import { onClickOutside } from '@vueuse/core'
3
- import { watch } from 'vue'
4
- import CodeMirror from 'codemirror'
5
- import 'codemirror/mode/javascript/javascript'
6
- import 'codemirror/mode/css/css'
7
- import 'codemirror/mode/markdown/markdown'
8
- import 'codemirror/mode/xml/xml'
9
- import 'codemirror/mode/htmlmixed/htmlmixed'
10
- import 'codemirror/addon/display/placeholder'
11
- import 'codemirror/lib/codemirror.css'
12
-
13
- export async function useCodeMirror(
14
- textarea: Ref<HTMLTextAreaElement | null | undefined>,
15
- input: Ref<string> | WritableComputedRef<string>,
16
- options: CodeMirror.EditorConfiguration = {},
17
- ) {
18
- const cm = CodeMirror.fromTextArea(
19
- textarea.value!,
20
- {
21
- theme: 'vars',
22
- ...options,
23
- },
24
- )
25
-
26
- let skip = false
27
-
28
- cm.on('change', () => {
29
- if (skip) {
30
- skip = false
31
- return
32
- }
33
- input.value = cm.getValue()
34
- })
35
-
36
- watch(
37
- input,
38
- (v) => {
39
- if (v !== cm.getValue()) {
40
- skip = true
41
- const selections = cm.listSelections()
42
- cm.replaceRange(v, cm.posFromIndex(0), cm.posFromIndex(Number.POSITIVE_INFINITY))
43
- cm.setSelections(selections)
44
- }
45
- },
46
- { immediate: true },
47
- )
48
-
49
- onClickOutside(cm.getWrapperElement(), () => {
50
- const el = cm.getInputField()
51
- if (document.activeElement === el)
52
- el.blur()
53
- })
54
-
55
- return cm
56
- }