返回 slidev
NoteEditable.vue
根目录 / packages / client / internals / NoteEditable.vue
1 <script setup lang="ts">
2 import type { ClicksContext } from '@slidev/types'
3 import type { PropType } from 'vue'
4 import { ignorableWatch, onClickOutside, useVModel } from '@vueuse/core'
5 import { nextTick, ref, toRef, watch, watchEffect } from 'vue'
6 import { useDynamicSlideInfo } from '../composables/useSlideInfo'
7 import NoteDisplay from './NoteDisplay.vue'
8
9 const props = defineProps({
10 no: {
11 type: Number,
12 required: true,
13 },
14 class: {
15 default: '',
16 },
17 editing: {
18 default: false,
19 },
20 style: {
21 default: () => ({}),
22 },
23 placeholder: {
24 default: 'No notes for this slide',
25 },
26 clicksContext: {
27 type: Object as PropType<ClicksContext>,
28 },
29 highlight: {
30 default: true,
31 },
32 autoHeight: {
33 default: false,
34 },
35 })
36
37 const emit = defineEmits<{
38 (type: 'update:editing', value: boolean): void
39 (type: 'markerDblclick', e: MouseEvent, clicks: number): void
40 (type: 'markerClick', e: MouseEvent, clicks: number): void
41 }>()
42
43 const editing = useVModel(props, 'editing', emit, { passive: true })
44
45 const { info, update } = useDynamicSlideInfo(toRef(props, 'no'))
46
47 const note = ref('')
48 let timer: any
49
50 // Send back the note on changes
51 const { ignoreUpdates } = ignorableWatch(
52 note,
53 (v) => {
54 if (!editing.value)
55 return
56 const id = props.no
57 clearTimeout(timer)
58 timer = setTimeout(() => {
59 update({ note: v }, id)
60 }, 500)
61 },
62 )
63
64 // Update note value when info changes
65 watch(
66 () => info.value?.note,
67 (value = '') => {
68 if (editing.value)
69 return
70 clearTimeout(timer)
71 ignoreUpdates(() => {
72 note.value = value
73 })
74 },
75 { immediate: true, flush: 'sync' },
76 )
77
78 const inputEl = ref<HTMLTextAreaElement>()
79 const inputHeight = ref<number | null>()
80
81 watchEffect(() => {
82 if (editing.value)
83 inputEl.value?.focus()
84 })
85
86 onClickOutside(inputEl, () => {
87 editing.value = false
88 })
89
90 function calculateEditorHeight() {
91 if (!props.autoHeight || !inputEl.value || !editing.value)
92 return
93 if (inputEl.value.scrollHeight > inputEl.value.clientHeight)
94 inputEl.value.style.height = `${inputEl.value.scrollHeight}px`
95 }
96
97 function onKeyDown(e: KeyboardEvent) {
98 // Override save shortcut on editing mode
99 if (editing.value && e.metaKey && e.key === 's') {
100 e.preventDefault()
101 update({ note: note.value }, props.no)
102 }
103 }
104
105 watch(
106 [note, editing],
107 () => {
108 nextTick(() => {
109 calculateEditorHeight()
110 })
111 },
112 { flush: 'post', immediate: true },
113 )
114 </script>
115
116 <template>
117 <NoteDisplay
118 v-if="!editing"
119 class="border-transparent border-2"
120 :class="[props.class, note ? '' : 'opacity-25 italic select-none']"
121 :style="props.style"
122 :note="note || placeholder"
123 :note-html="info?.noteHTML"
124 :clicks-context="clicksContext"
125 :auto-scroll="!autoHeight"
126 :highlight="props.highlight"
127 @marker-click="(e, clicks) => emit('markerClick', e, clicks)"
128 @marker-dblclick="(e, clicks) => emit('markerDblclick', e, clicks)"
129 />
130 <textarea
131 v-else
132 ref="inputEl"
133 v-model="note"
134 class="prose dark:prose-invert resize-none overflow-auto outline-none bg-transparent block border-primary border-2 slidev-note placeholder:op25"
135 :style="[props.style, inputHeight != null ? { height: `${inputHeight}px` } : {}]"
136 :class="[props.class, note ? '' : 'italic']"
137 :placeholder="placeholder"
138 @keydown.esc="editing = false"
139 @keydown="onKeyDown"
140 />
141 </template>
142
142 lines Plain Text