@slidev/client 0.48.8 → 0.49.0-beta.1

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,44 @@
1
+ import type { App } from 'vue'
2
+ import { watch } from 'vue'
3
+ import type { DragElementState } from '../composables/useDragElements'
4
+ import { useDragElement } from '../composables/useDragElements'
5
+
6
+ export function createVDragDirective() {
7
+ return {
8
+ install(app: App) {
9
+ app.directive<HTMLElement & { draggingState: DragElementState }>('drag', {
10
+ // @ts-expect-error extra prop
11
+ name: 'v-drag',
12
+
13
+ created(el, binding, vnode) {
14
+ const state = useDragElement(binding, binding.value, vnode.props?.markdownSource)
15
+ if (vnode.props) {
16
+ vnode.props = { ...vnode.props }
17
+ delete vnode.props.markdownSource
18
+ }
19
+ state.container.value = el
20
+ el.draggingState = state
21
+ el.dataset.dragId = state.id
22
+ state.watchStopHandles.push(
23
+ watch(state.containerStyle, (style) => {
24
+ for (const [k, v] of Object.entries(style)) {
25
+ if (v)
26
+ el.style[k as any] = v
27
+ }
28
+ }, { immediate: true }),
29
+ )
30
+ el.addEventListener('dblclick', state.startDragging)
31
+ },
32
+ mounted(el) {
33
+ el.draggingState.mounted()
34
+ },
35
+ unmounted(el) {
36
+ const state = el.draggingState
37
+ state.unmounted()
38
+ el.removeEventListener('dblclick', state.startDragging)
39
+ state.watchStopHandles.forEach(fn => fn())
40
+ },
41
+ })
42
+ },
43
+ }
44
+ }
package/modules/v-mark.ts CHANGED
@@ -121,7 +121,8 @@ export function createVMarkDirective() {
121
121
  return
122
122
  }
123
123
 
124
- watchEffect(() => {
124
+ // @ts-expect-error extra prop
125
+ el.watchStopHandle = watchEffect(() => {
125
126
  let shouldShow: boolean | undefined
126
127
 
127
128
  if (options.value.class)
@@ -147,6 +148,11 @@ export function createVMarkDirective() {
147
148
  annotation.hide()
148
149
  })
149
150
  },
151
+
152
+ unmounted: (el) => {
153
+ // @ts-expect-error extra prop
154
+ el.watchStopHandle?.()
155
+ },
150
156
  })
151
157
  },
152
158
  }
@@ -0,0 +1,120 @@
1
+ import type { App, ObjectDirective } from 'vue'
2
+ import { watch } from 'vue'
3
+ import { MotionDirective } from '@vueuse/motion'
4
+ import type { ResolvedClicksInfo } from '@slidev/types'
5
+ import { injectionClickVisibility, injectionClicksContext, injectionCurrentPage, injectionRenderContext } from '../constants'
6
+ import { useNav } from '../composables/useNav'
7
+ import { makeId } from '../logic/utils'
8
+ import { directiveInject } from '../utils'
9
+ import type { VClickValue } from './v-click'
10
+ import { resolveClick } from './v-click'
11
+
12
+ export type MotionDirectiveValue = undefined | VClickValue | {
13
+ key?: string
14
+ at?: VClickValue
15
+ }
16
+
17
+ export function createVMotionDirectives() {
18
+ return {
19
+ install(app: App) {
20
+ const original = MotionDirective() as ObjectDirective
21
+ app.directive<HTMLElement | SVGElement, string>('motion', {
22
+ // @ts-expect-error extra prop
23
+ name: 'v-motion',
24
+ mounted(el, binding, node, prevNode) {
25
+ const props = node.props = { ...node.props }
26
+
27
+ const variantInitial = { ...props.initial, ...props.variants?.['slidev-initial'] }
28
+ const variantEnter = { ...props.enter, ...props.variants?.['slidev-enter'] }
29
+ const variantLeave = { ...props.leave, ...props.variants?.['slidev-leave'] }
30
+ delete props.initial
31
+ delete props.enter
32
+ delete props.leave
33
+
34
+ const idPrefix = `${makeId()}-`
35
+ const clicks: {
36
+ id: string
37
+ at: number | [number, number]
38
+ variant: Record<string, unknown>
39
+ resolved: ResolvedClicksInfo | null
40
+ }[] = []
41
+
42
+ for (const k of Object.keys(props)) {
43
+ if (k.startsWith('click-')) {
44
+ const s = k.slice(6)
45
+ const at = s.includes('-') ? s.split('-').map(Number) as [number, number] : +s
46
+ const id = idPrefix + s
47
+ clicks.push({
48
+ id,
49
+ at,
50
+ variant: { ...props[k] },
51
+ resolved: resolveClick(id, binding, at),
52
+ })
53
+ delete props[k]
54
+ }
55
+ }
56
+
57
+ clicks.sort((a, b) => (Array.isArray(a.at) ? a.at[0] : a.at) - (Array.isArray(b.at) ? b.at[0] : b.at))
58
+
59
+ original.created!(el, binding, node, prevNode)
60
+ original.mounted!(el, binding, node, prevNode)
61
+
62
+ const thisPage = directiveInject(binding, injectionCurrentPage)
63
+ const renderContext = directiveInject(binding, injectionRenderContext)
64
+ const clickVisibility = directiveInject(binding, injectionClickVisibility)
65
+ const clicksContext = directiveInject(binding, injectionClicksContext)
66
+ const { currentPage, clicks: currentClicks, isPrintMode } = useNav()
67
+ // @ts-expect-error extra prop
68
+ const motion = el.motionInstance
69
+ motion.clickIds = clicks.map(i => i.id)
70
+ motion.set(variantInitial)
71
+ motion.watchStopHandle = watch(
72
+ [thisPage, currentPage, currentClicks].filter(Boolean),
73
+ () => {
74
+ const visibility = clickVisibility?.value ?? true
75
+ if (!clicksContext?.value || !['slide', 'presenter'].includes(renderContext?.value ?? '')) {
76
+ const mixedVariant: Record<string, unknown> = { ...variantInitial, ...variantEnter }
77
+ for (const { variant } of clicks)
78
+ Object.assign(mixedVariant, variant)
79
+
80
+ motion.set(mixedVariant)
81
+ }
82
+ else if (isPrintMode.value || thisPage?.value === currentPage.value) {
83
+ if (visibility === true) {
84
+ const mixedVariant: Record<string, unknown> = { ...variantInitial, ...variantEnter }
85
+ for (const { variant, resolved: resolvedClick } of clicks) {
86
+ if (!resolvedClick || resolvedClick.isActive.value)
87
+ Object.assign(mixedVariant, variant)
88
+ }
89
+ if (isPrintMode.value)
90
+ motion.set(mixedVariant) // print with clicks
91
+ else
92
+ motion.apply(mixedVariant)
93
+ }
94
+ else {
95
+ motion.apply(visibility === 'before' ? variantInitial : variantLeave)
96
+ }
97
+ }
98
+ else {
99
+ motion.apply((thisPage?.value ?? -1) > currentPage.value ? variantInitial : variantLeave)
100
+ }
101
+ },
102
+ {
103
+ immediate: true,
104
+ },
105
+ )
106
+ },
107
+ unmounted(el, dir) {
108
+ if (!directiveInject(dir, injectionClicksContext)?.value)
109
+ return
110
+
111
+ const ctx = directiveInject(dir, injectionClicksContext)?.value
112
+ // @ts-expect-error extra prop
113
+ const motion = el.motionInstance
114
+ motion.clickIds.map((id: string) => ctx?.unregister(id))
115
+ motion.watchStopHandle()
116
+ },
117
+ })
118
+ },
119
+ }
120
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "0.48.8",
4
+ "version": "0.49.0-beta.1",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "antfu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -30,14 +30,14 @@
30
30
  "dependencies": {
31
31
  "@antfu/utils": "^0.7.7",
32
32
  "@iconify-json/carbon": "^1.1.31",
33
- "@iconify-json/ph": "^1.1.11",
33
+ "@iconify-json/ph": "^1.1.12",
34
34
  "@iconify-json/svg-spinners": "^1.1.2",
35
- "@shikijs/monaco": "^1.2.1",
36
- "@shikijs/vitepress-twoslash": "^1.2.1",
35
+ "@shikijs/monaco": "^1.3.0",
36
+ "@shikijs/vitepress-twoslash": "^1.3.0",
37
37
  "@slidev/rough-notation": "^0.1.0",
38
38
  "@typescript/ata": "^0.9.4",
39
- "@unhead/vue": "^1.9.3",
40
- "@unocss/reset": "^0.58.8",
39
+ "@unhead/vue": "^1.9.4",
40
+ "@unocss/reset": "^0.59.0",
41
41
  "@vueuse/core": "^10.9.0",
42
42
  "@vueuse/math": "^10.9.0",
43
43
  "@vueuse/motion": "^2.1.0",
@@ -53,17 +53,17 @@
53
53
  "monaco-editor": "^0.47.0",
54
54
  "prettier": "^3.2.5",
55
55
  "recordrtc": "^5.6.2",
56
- "shiki": "^1.2.1",
57
- "shiki-magic-move": "^0.3.4",
58
- "typescript": "^5.4.3",
59
- "unocss": "^0.58.8",
56
+ "shiki": "^1.3.0",
57
+ "shiki-magic-move": "^0.3.5",
58
+ "typescript": "^5.4.4",
59
+ "unocss": "^0.59.0",
60
60
  "vue": "^3.4.21",
61
61
  "vue-demi": "^0.14.7",
62
62
  "vue-router": "^4.3.0",
63
- "@slidev/parser": "0.48.8",
64
- "@slidev/types": "0.48.8"
63
+ "@slidev/parser": "0.49.0-beta.1",
64
+ "@slidev/types": "0.49.0-beta.1"
65
65
  },
66
66
  "devDependencies": {
67
- "vite": "^5.2.7"
67
+ "vite": "^5.2.8"
68
68
  }
69
69
  }
@@ -1,13 +1,14 @@
1
- import { createSingletonPromise } from '@antfu/utils'
1
+ import { createSingletonPromise, ensurePrefix, slash } from '@antfu/utils'
2
2
  import type { CodeRunner, CodeRunnerContext, CodeRunnerOutput, CodeRunnerOutputText, CodeRunnerOutputs } from '@slidev/types'
3
3
  import type { CodeToHastOptions } from 'shiki'
4
+ import type ts from 'typescript'
4
5
  import { isDark } from '../logic/dark'
5
6
  import setups from '#slidev/setups/code-runners'
6
7
 
7
8
  export default createSingletonPromise(async () => {
8
9
  const runners: Record<string, CodeRunner> = {
9
- javascript: runJavaScript,
10
- js: runJavaScript,
10
+ javascript: runTypeScript,
11
+ js: runTypeScript,
11
12
  typescript: runTypeScript,
12
13
  ts: runTypeScript,
13
14
  }
@@ -24,6 +25,18 @@ export default createSingletonPromise(async () => {
24
25
  ...options,
25
26
  })
26
27
 
28
+ const resolveId = async (specifier: string) => {
29
+ if (!/^(@[^\/:]+?\/)?[^\/:]+$/.test(specifier))
30
+ return specifier
31
+ const res = await fetch(`/@slidev/resolve-id/${specifier}`)
32
+ if (!res.ok)
33
+ return null
34
+ const id = await res.text()
35
+ if (!id)
36
+ return null
37
+ return `/@fs${ensurePrefix('/', slash(id))}`
38
+ }
39
+
27
40
  const run = async (code: string, lang: string, options: Record<string, unknown>): Promise<CodeRunnerOutputs> => {
28
41
  try {
29
42
  const runner = runners[lang]
@@ -34,6 +47,7 @@ export default createSingletonPromise(async () => {
34
47
  {
35
48
  options,
36
49
  highlight,
50
+ resolveId,
37
51
  run: async (code, lang) => {
38
52
  return await run(code, lang, options)
39
53
  },
@@ -60,7 +74,7 @@ export default createSingletonPromise(async () => {
60
74
  })
61
75
 
62
76
  // Ported from https://github.com/microsoft/TypeScript-Website/blob/v2/packages/playground/src/sidebar/runtime.ts
63
- export async function runJavaScript(code: string): Promise<CodeRunnerOutputs> {
77
+ async function runJavaScript(code: string): Promise<CodeRunnerOutputs> {
64
78
  const allLogs: CodeRunnerOutput[] = []
65
79
 
66
80
  const replace = {} as any
@@ -159,10 +173,80 @@ export async function runJavaScript(code: string): Promise<CodeRunnerOutputs> {
159
173
  let tsModule: typeof import('typescript') | undefined
160
174
 
161
175
  export async function runTypeScript(code: string, context: CodeRunnerContext) {
162
- const { transpile } = tsModule ??= await import('typescript')
163
- code = transpile(code, {
164
- module: tsModule.ModuleKind.ESNext,
165
- target: tsModule.ScriptTarget.ES2022,
166
- })
167
- return await context.run(code, 'javascript')
176
+ tsModule ??= await import('typescript')
177
+
178
+ code = tsModule.transpileModule(code, {
179
+ compilerOptions: {
180
+ module: tsModule.ModuleKind.ESNext,
181
+ target: tsModule.ScriptTarget.ES2022,
182
+ },
183
+ transformers: {
184
+ after: [transformImports],
185
+ },
186
+ }).outputText
187
+
188
+ const importRegex = /import\s*\(\s*(['"])(.+?)['"]\s*\)/g
189
+ const idMap: Record<string, string> = {}
190
+ for (const [,,specifier] of code.matchAll(importRegex)!)
191
+ idMap[specifier] = await context.resolveId(specifier) ?? specifier
192
+ code = code.replace(importRegex, (_full, quote, specifier) => `import(${quote}${idMap[specifier] ?? specifier}${quote})`)
193
+
194
+ return await runJavaScript(code)
195
+ }
196
+
197
+ /**
198
+ * Transform import statements to dynamic imports
199
+ */
200
+ function transformImports(context: ts.TransformationContext): ts.Transformer<ts.SourceFile> {
201
+ const { factory } = context
202
+ const { isImportDeclaration, isNamedImports, NodeFlags } = tsModule!
203
+ return (sourceFile: ts.SourceFile) => {
204
+ const statements = [...sourceFile.statements]
205
+ for (let i = 0; i < statements.length; i++) {
206
+ const statement = statements[i]
207
+ if (!isImportDeclaration(statement))
208
+ continue
209
+ let bindingPattern: ts.ObjectBindingPattern | ts.Identifier
210
+ const namedBindings = statement.importClause?.namedBindings
211
+ const bindings: ts.BindingElement[] = []
212
+ if (statement.importClause?.name)
213
+ bindings.push(factory.createBindingElement(undefined, factory.createIdentifier('default'), statement.importClause.name))
214
+ if (namedBindings) {
215
+ if (isNamedImports(namedBindings)) {
216
+ for (const specifier of namedBindings.elements)
217
+ bindings.push(factory.createBindingElement(undefined, specifier.propertyName, specifier.name))
218
+ bindingPattern = factory.createObjectBindingPattern(bindings)
219
+ }
220
+ else {
221
+ bindingPattern = factory.createIdentifier(namedBindings.name.text)
222
+ }
223
+ }
224
+ else {
225
+ bindingPattern = factory.createObjectBindingPattern(bindings)
226
+ }
227
+
228
+ const newStatement = factory.createVariableStatement(
229
+ undefined,
230
+ factory.createVariableDeclarationList(
231
+ [
232
+ factory.createVariableDeclaration(
233
+ bindingPattern,
234
+ undefined,
235
+ undefined,
236
+ factory.createAwaitExpression(
237
+ factory.createCallExpression(
238
+ factory.createIdentifier('import'),
239
+ undefined,
240
+ [statement.moduleSpecifier],
241
+ ),
242
+ ),
243
+ ),
244
+ ],
245
+ NodeFlags.Const,
246
+ ),
247
+ )
248
+ statements[i] = newStatement
249
+ }
250
+ return factory.updateSourceFile(sourceFile, statements)
251
+ }
168
252
  }
package/setup/main.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { AppContext } from '@slidev/types'
2
- import { MotionPlugin } from '@vueuse/motion'
3
2
  import TwoSlashFloatingVue from '@shikijs/vitepress-twoslash/client'
4
3
  import type { App } from 'vue'
5
4
  import { nextTick } from 'vue'
@@ -8,6 +7,8 @@ import { createHead } from '@unhead/vue'
8
7
  import { routeForceRefresh } from '../logic/route'
9
8
  import { createVClickDirectives } from '../modules/v-click'
10
9
  import { createVMarkDirective } from '../modules/v-mark'
10
+ import { createVDragDirective } from '../modules/v-drag'
11
+ import { createVMotionDirectives } from '../modules/v-motion'
11
12
  import { routes } from '../routes'
12
13
  import setups from '#slidev/setups/main'
13
14
 
@@ -34,7 +35,8 @@ export default async function setupMain(app: App) {
34
35
  app.use(createHead())
35
36
  app.use(createVClickDirectives())
36
37
  app.use(createVMarkDirective())
37
- app.use(MotionPlugin)
38
+ app.use(createVDragDirective())
39
+ app.use(createVMotionDirectives())
38
40
  app.use(TwoSlashFloatingVue as any, { container: '#twoslash-container' })
39
41
 
40
42
  const context: AppContext = {
@@ -2,7 +2,7 @@ import { and, not, or } from '@vueuse/math'
2
2
  import type { NavOperations, ShortcutOptions } from '@slidev/types'
3
3
  import { downloadPDF } from '../utils'
4
4
  import { toggleDark } from '../logic/dark'
5
- import { magicKeys, showGotoDialog, showOverview, toggleOverview } from '../state'
5
+ import { activeDragElement, magicKeys, showGotoDialog, showOverview, toggleOverview } from '../state'
6
6
  import { useNav } from '../composables/useNav'
7
7
  import { useDrawings } from '../composables/useDrawings'
8
8
  import { currentOverviewPage, downOverviewPage, nextOverviewPage, prevOverviewPage, upOverviewPage } from './../logic/overview'
@@ -29,15 +29,17 @@ export default function setupShortcuts() {
29
29
  showGotoDialog: () => showGotoDialog.value = !showGotoDialog.value,
30
30
  }
31
31
 
32
+ const navViaArrowKeys = and(not(showOverview), not(activeDragElement))
33
+
32
34
  let shortcuts: ShortcutOptions[] = [
33
35
  { name: 'next_space', key: and(space, not(shift)), fn: next, autoRepeat: true },
34
36
  { name: 'prev_space', key: and(space, shift), fn: prev, autoRepeat: true },
35
- { name: 'next_right', key: and(right, not(shift), not(showOverview)), fn: next, autoRepeat: true },
36
- { name: 'prev_left', key: and(left, not(shift), not(showOverview)), fn: prev, autoRepeat: true },
37
+ { name: 'next_right', key: and(right, not(shift), navViaArrowKeys), fn: next, autoRepeat: true },
38
+ { name: 'prev_left', key: and(left, not(shift), navViaArrowKeys), fn: prev, autoRepeat: true },
37
39
  { name: 'next_page_key', key: 'pageDown', fn: next, autoRepeat: true },
38
40
  { name: 'prev_page_key', key: 'pageUp', fn: prev, autoRepeat: true },
39
- { name: 'next_down', key: and(down, not(showOverview)), fn: nextSlide, autoRepeat: true },
40
- { name: 'prev_up', key: and(up, not(showOverview)), fn: () => prevSlide(false), autoRepeat: true },
41
+ { name: 'next_down', key: and(down, navViaArrowKeys), fn: nextSlide, autoRepeat: true },
42
+ { name: 'prev_up', key: and(up, navViaArrowKeys), fn: () => prevSlide(false), autoRepeat: true },
41
43
  { name: 'next_shift', key: and(right, shift), fn: nextSlide, autoRepeat: true },
42
44
  { name: 'prev_shift', key: and(left, shift), fn: () => prevSlide(false), autoRepeat: true },
43
45
  { name: 'toggle_dark', key: and(d, not(drawingEnabled)), fn: toggleDark },
package/state/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { breakpointsTailwind, isClient, useActiveElement, useBreakpoints, useFullscreen, useLocalStorage, useMagicKeys, useToggle, useWindowSize } from '@vueuse/core'
2
- import { computed, ref } from 'vue'
2
+ import { computed, ref, shallowRef } from 'vue'
3
3
  import { slideAspect } from '../env'
4
+ import type { DragElementState } from '../composables/useDragElements'
4
5
 
5
6
  export const showRecordingDialog = ref(false)
6
7
  export const showInfoDialog = ref(false)
@@ -31,6 +32,8 @@ export const isEditorVertical = useLocalStorage('slidev-editor-vertical', false,
31
32
  export const editorWidth = useLocalStorage('slidev-editor-width', isClient ? window.innerWidth * 0.4 : 318, { listenToStorageChanges: false })
32
33
  export const editorHeight = useLocalStorage('slidev-editor-height', isClient ? window.innerHeight * 0.4 : 300, { listenToStorageChanges: false })
33
34
 
35
+ export const activeDragElement = shallowRef<DragElementState | null>(null)
36
+
34
37
  export const presenterNotesFontSize = useLocalStorage('slidev-presenter-font-size', 1, { listenToStorageChanges: false })
35
38
  export const presenterLayout = useLocalStorage('slidev-presenter-layout', 1, { listenToStorageChanges: false })
36
39
 
package/utils.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { SlideRoute } from '@slidev/types'
2
+ import type { DirectiveBinding, InjectionKey } from 'vue'
2
3
  import { configs } from './env'
3
4
 
4
5
  export function getSlideClass(route?: SlideRoute, extra = '') {
@@ -22,3 +23,18 @@ export async function downloadPDF() {
22
23
  `${configs.title}.pdf`,
23
24
  )
24
25
  }
26
+
27
+ export function directiveInject<T = unknown>(dir: DirectiveBinding<any>, key: InjectionKey<T> | string, defaultValue?: T): T | undefined {
28
+ return (dir.instance?.$ as any).provides[key as any] ?? defaultValue
29
+ }
30
+
31
+ export function directiveProvide<T = unknown>(dir: DirectiveBinding<any>, key: InjectionKey<T> | string, value?: T) {
32
+ const instance = dir.instance?.$ as any
33
+ if (instance) {
34
+ let provides = instance.provides
35
+ const parentProvides = instance.parent?.provides
36
+ if (provides === parentProvides)
37
+ provides = instance.provides = Object.create(parentProvides)
38
+ provides[key as any] = value
39
+ }
40
+ }