slidev-excalidraw 0.1.0
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.
- package/components/Excalidraw.vue +165 -0
- package/index.ts +27 -0
- package/package.json +36 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, watch, onBeforeUnmount } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
src: string
|
|
6
|
+
darkMode?: boolean
|
|
7
|
+
editable?: boolean
|
|
8
|
+
persist?: boolean
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const container = ref<HTMLDivElement>()
|
|
12
|
+
const editorContainer = ref<HTMLDivElement>()
|
|
13
|
+
const editing = ref(false)
|
|
14
|
+
let fileData: any = null
|
|
15
|
+
let reactRoot: any = null
|
|
16
|
+
|
|
17
|
+
async function loadData() {
|
|
18
|
+
const res = await fetch(props.src)
|
|
19
|
+
fileData = await res.json()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function renderSvg() {
|
|
23
|
+
if (!container.value || !fileData) return
|
|
24
|
+
|
|
25
|
+
const { exportToSvg } = await import('@excalidraw/utils')
|
|
26
|
+
|
|
27
|
+
const svg = await exportToSvg({
|
|
28
|
+
elements: fileData.elements,
|
|
29
|
+
appState: {
|
|
30
|
+
...fileData.appState,
|
|
31
|
+
exportWithDarkMode: props.darkMode ?? false,
|
|
32
|
+
exportBackground: false,
|
|
33
|
+
},
|
|
34
|
+
files: fileData.files || null,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
svg.style.width = '100%'
|
|
38
|
+
svg.style.height = '100%'
|
|
39
|
+
container.value.innerHTML = ''
|
|
40
|
+
container.value.appendChild(svg)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function startEditing() {
|
|
44
|
+
if (!editorContainer.value || !fileData) return
|
|
45
|
+
editing.value = true
|
|
46
|
+
|
|
47
|
+
const React = await import('react')
|
|
48
|
+
const ReactDOM = await import('react-dom/client')
|
|
49
|
+
const { Excalidraw } = await import('@excalidraw/excalidraw')
|
|
50
|
+
await import('@excalidraw/excalidraw/index.css')
|
|
51
|
+
|
|
52
|
+
if (reactRoot) {
|
|
53
|
+
reactRoot.unmount()
|
|
54
|
+
}
|
|
55
|
+
reactRoot = ReactDOM.createRoot(editorContainer.value)
|
|
56
|
+
|
|
57
|
+
const onChange = (elements: any, appState: any, files: any) => {
|
|
58
|
+
fileData = { elements, appState, files }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
reactRoot.render(
|
|
62
|
+
React.createElement(Excalidraw, {
|
|
63
|
+
initialData: fileData,
|
|
64
|
+
onChange,
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function stopEditing() {
|
|
70
|
+
if (reactRoot) {
|
|
71
|
+
reactRoot.unmount()
|
|
72
|
+
reactRoot = null
|
|
73
|
+
}
|
|
74
|
+
editing.value = false
|
|
75
|
+
|
|
76
|
+
if (props.persist !== false) {
|
|
77
|
+
const filename = props.src.replace(/^\//, '')
|
|
78
|
+
await fetch('/api/save-excalidraw', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { 'Content-Type': 'application/json' },
|
|
81
|
+
body: JSON.stringify({ file: filename, data: fileData }),
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
renderSvg()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onMounted(async () => {
|
|
89
|
+
await loadData()
|
|
90
|
+
await renderSvg()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
onBeforeUnmount(() => {
|
|
94
|
+
if (reactRoot) {
|
|
95
|
+
reactRoot.unmount()
|
|
96
|
+
reactRoot = null
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
watch(() => [props.src, props.darkMode], async () => {
|
|
101
|
+
await loadData()
|
|
102
|
+
await renderSvg()
|
|
103
|
+
})
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<template>
|
|
107
|
+
<div class="excalidraw-wrapper">
|
|
108
|
+
<div v-show="!editing" ref="container" class="excalidraw-container" @dblclick="editable !== false && startEditing()" />
|
|
109
|
+
<div v-show="editing" ref="editorContainer" class="excalidraw-editor" />
|
|
110
|
+
<div class="btn-group">
|
|
111
|
+
<button v-if="!editing && editable !== false" class="edit-btn" @click="startEditing" title="Edit">
|
|
112
|
+
✏️
|
|
113
|
+
</button>
|
|
114
|
+
<button v-if="editing" class="edit-btn done-btn" @click="stopEditing" title="Done">
|
|
115
|
+
✓
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</template>
|
|
120
|
+
|
|
121
|
+
<style scoped>
|
|
122
|
+
.excalidraw-wrapper {
|
|
123
|
+
width: 100%;
|
|
124
|
+
height: 100%;
|
|
125
|
+
position: relative;
|
|
126
|
+
}
|
|
127
|
+
.excalidraw-container {
|
|
128
|
+
width: 100%;
|
|
129
|
+
height: 100%;
|
|
130
|
+
display: flex;
|
|
131
|
+
justify-content: center;
|
|
132
|
+
align-items: center;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
}
|
|
135
|
+
.excalidraw-editor {
|
|
136
|
+
width: 100%;
|
|
137
|
+
height: 80vh;
|
|
138
|
+
}
|
|
139
|
+
.btn-group {
|
|
140
|
+
position: absolute;
|
|
141
|
+
top: 4px;
|
|
142
|
+
right: 4px;
|
|
143
|
+
z-index: 10;
|
|
144
|
+
display: flex;
|
|
145
|
+
gap: 4px;
|
|
146
|
+
}
|
|
147
|
+
.edit-btn {
|
|
148
|
+
background: rgba(255,255,255,0.8);
|
|
149
|
+
border: 1px solid #ddd;
|
|
150
|
+
border-radius: 4px;
|
|
151
|
+
padding: 2px 6px;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
font-size: 14px;
|
|
154
|
+
opacity: 0.5;
|
|
155
|
+
transition: opacity 0.2s;
|
|
156
|
+
}
|
|
157
|
+
.edit-btn:hover {
|
|
158
|
+
opacity: 1;
|
|
159
|
+
}
|
|
160
|
+
.done-btn {
|
|
161
|
+
opacity: 0.8;
|
|
162
|
+
background: rgba(34,197,94,0.8);
|
|
163
|
+
color: white;
|
|
164
|
+
}
|
|
165
|
+
</style>
|
package/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Plugin } from 'vite'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
export function excalidrawSavePlugin(publicDir: string): Plugin {
|
|
6
|
+
return {
|
|
7
|
+
name: 'slidev-excalidraw-save',
|
|
8
|
+
configureServer(server) {
|
|
9
|
+
server.middlewares.use('/api/save-excalidraw', (req, res) => {
|
|
10
|
+
if (req.method !== 'POST') {
|
|
11
|
+
res.statusCode = 405
|
|
12
|
+
res.end()
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
let body = ''
|
|
16
|
+
req.on('data', (chunk: string) => { body += chunk })
|
|
17
|
+
req.on('end', () => {
|
|
18
|
+
const { file, data } = JSON.parse(body)
|
|
19
|
+
const filePath = path.join(publicDir, file)
|
|
20
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2))
|
|
21
|
+
res.statusCode = 200
|
|
22
|
+
res.end('ok')
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "slidev-excalidraw",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Excalidraw integration for Slidev with inline editing and auto-save",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"slidev",
|
|
9
|
+
"slidev-addon",
|
|
10
|
+
"excalidraw",
|
|
11
|
+
"slides",
|
|
12
|
+
"presentation",
|
|
13
|
+
"diagram"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Seonglae Cho",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/seonglae/slidev-excalidraw"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"components",
|
|
23
|
+
"index.ts",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@excalidraw/excalidraw": ">=0.18",
|
|
28
|
+
"@excalidraw/utils": ">=0.1",
|
|
29
|
+
"react": "^18",
|
|
30
|
+
"react-dom": "^18",
|
|
31
|
+
"@slidev/cli": ">=52"
|
|
32
|
+
},
|
|
33
|
+
"slidev": {
|
|
34
|
+
"components": "./components"
|
|
35
|
+
}
|
|
36
|
+
}
|