-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
NoteDisplay.vue
152 lines (134 loc) · 4.22 KB
/
NoteDisplay.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import type { ClicksContext } from '@slidev/types'
import { CLICKS_MAX } from '../constants'
const props = defineProps<{
class?: string
noteHtml?: string
note?: string
placeholder?: string
clicksContext?: ClicksContext
autoScroll?: boolean
}>()
defineEmits(['click'])
const withClicks = computed(() => props.clicksContext?.current != null && props.noteHtml?.includes('slidev-note-click-mark'))
const noteDisplay = ref<HTMLElement | null>(null)
const CLASS_FADE = 'slidev-note-fade'
const CLASS_MARKER = 'slidev-note-click-mark'
function highlightNote() {
if (!noteDisplay.value || !withClicks.value || props.clicksContext?.current == null)
return
const current = +props.clicksContext?.current ?? CLICKS_MAX
const disabled = current < 0 || current >= CLICKS_MAX
if (disabled) {
Array.from(noteDisplay.value.querySelectorAll('*'))
.forEach(el => el.classList.remove(CLASS_FADE))
return
}
const nodeToIgnores = new Set<Element>()
function ignoreParent(node: Element) {
if (!node || node === noteDisplay.value)
return
nodeToIgnores.add(node)
if (node.parentElement)
ignoreParent(node.parentElement)
}
const markers = Array.from(noteDisplay.value.querySelectorAll(`.${CLASS_MARKER}`)) as HTMLElement[]
const markersMap = new Map<number, HTMLElement>()
// Convert all sibling text nodes to spans, so we attach classes to them
for (const marker of markers) {
const parent = marker.parentElement!
const clicks = Number(marker.dataset!.clicks)
markersMap.set(clicks, marker)
// Ignore the parents of the marker, so the class only applies to the children
ignoreParent(parent)
Array.from(parent!.childNodes)
.forEach((node) => {
if (node.nodeType === 3) { // text node
const span = document.createElement('span')
span.textContent = node.textContent
parent.insertBefore(span, node)
node.remove()
}
})
}
const children = Array.from(noteDisplay.value.querySelectorAll('*'))
let count = 0
// Segmenting notes by clicks
const segments = new Map<number, Element[]>()
for (const child of children) {
if (!segments.has(count))
segments.set(count, [])
segments.get(count)!.push(child)
// Update count when reach marker
if (child.classList.contains(CLASS_MARKER))
count = Number((child as HTMLElement).dataset.clicks) || (count + 1)
}
// Apply
for (const [count, els] of segments) {
els.forEach(el => el.classList.toggle(
CLASS_FADE,
nodeToIgnores.has(el)
? false
: count !== current,
))
}
for (const [clicks, marker] of markersMap) {
marker.classList.remove(CLASS_FADE)
marker.classList.toggle(`${CLASS_MARKER}-past`, clicks < current)
marker.classList.toggle(`${CLASS_MARKER}-active`, clicks === current)
marker.classList.toggle(`${CLASS_MARKER}-next`, clicks === current + 1)
marker.classList.toggle(`${CLASS_MARKER}-future`, clicks > current + 1)
marker.addEventListener('dblclick', (e) => {
props.clicksContext!.current = clicks
e.stopPropagation()
e.stopImmediatePropagation()
})
if (props.autoScroll && clicks === current)
marker.scrollIntoView({ block: 'center', behavior: 'smooth' })
}
}
watch(
() => [props.noteHtml, props.clicksContext?.current],
() => {
nextTick(() => {
highlightNote()
})
},
{ immediate: true },
)
onMounted(() => {
highlightNote()
})
</script>
<template>
<div
v-if="noteHtml"
ref="noteDisplay"
class="prose overflow-auto outline-none slidev-note"
:class="[props.class, withClicks ? 'slidev-note-with-clicks' : '']"
@click="$emit('click')"
v-html="noteHtml"
/>
<div
v-else-if="note"
class="prose overflow-auto outline-none slidev-note"
:class="props.class"
@click="$emit('click')"
>
<p v-text="note" />
</div>
<div
v-else
class="prose overflow-auto outline-none opacity-50 italic select-none slidev-note"
:class="props.class"
@click="$emit('click')"
>
<p v-text="props.placeholder || 'No notes.'" />
</div>
</template>
<style>
.slidev-note :first-child {
margin-top: 0;
}
</style>