| 1 | <script setup lang="ts"> |
| 2 | import { throttledWatch, useEventListener } from '@vueuse/core' |
| 3 | import { computed, ref, watch } from 'vue' |
| 4 | import { useNav } from '../composables/useNav' |
| 5 | import { useDynamicSlideInfo } from '../composables/useSlideInfo' |
| 6 | import { parseSideEditorContent } from '../logic/sideEditor' |
| 7 | import { activeElement, editorHeight, editorWidth, isInputting, showEditor, isEditorVertical as vertical } from '../state' |
| 8 | import IconButton from './IconButton.vue' |
| 9 | import ShikiEditor from './ShikiEditor.vue' |
| 10 | |
| 11 | const props = defineProps<{ |
| 12 | resize?: boolean |
| 13 | }>() |
| 14 | |
| 15 | const { currentSlideNo, openInEditor } = useNav() |
| 16 | |
| 17 | const tab = ref<'content' | 'note'>('content') |
| 18 | const content = ref('') |
| 19 | const note = ref('') |
| 20 | const dirty = ref(false) |
| 21 | |
| 22 | const { info, update } = useDynamicSlideInfo(currentSlideNo) |
| 23 | |
| 24 | watch( |
| 25 | info, |
| 26 | (v) => { |
| 27 | if (!isInputting.value) { |
| 28 | note.value = (v?.note || '').trim() |
| 29 | const frontmatterPart = v?.frontmatterRaw?.trim() ? `---\n${v.frontmatterRaw.trim()}\n---\n\n` : '' |
| 30 | content.value = frontmatterPart + (v?.source.contentRaw || '').trim() |
| 31 | dirty.value = false |
| 32 | } |
| 33 | }, |
| 34 | { immediate: true }, |
| 35 | ) |
| 36 | |
| 37 | async function save() { |
| 38 | dirty.value = false |
| 39 | |
| 40 | const { content: contentOnly, frontmatterRaw } = parseSideEditorContent(content.value) |
| 41 | |
| 42 | await update({ |
| 43 | note: note.value || undefined, |
| 44 | content: contentOnly, |
| 45 | frontmatterRaw, |
| 46 | }) |
| 47 | } |
| 48 | |
| 49 | function close() { |
| 50 | showEditor.value = false |
| 51 | } |
| 52 | |
| 53 | useEventListener('keydown', (e) => { |
| 54 | if (activeElement.value?.tagName === 'TEXTAREA' && e.code === 'KeyS' && (e.ctrlKey || e.metaKey)) { |
| 55 | save() |
| 56 | e.preventDefault() |
| 57 | } |
| 58 | }) |
| 59 | |
| 60 | const contentRef = computed({ |
| 61 | get() { return content.value }, |
| 62 | set(v) { |
| 63 | if (content.value.trim() !== v.trim()) |
| 64 | dirty.value = true |
| 65 | content.value = v |
| 66 | }, |
| 67 | }) |
| 68 | |
| 69 | const noteRef = computed({ |
| 70 | get() { return note.value }, |
| 71 | set(v) { |
| 72 | note.value = v |
| 73 | dirty.value = true |
| 74 | }, |
| 75 | }) |
| 76 | |
| 77 | const handlerDown = ref(false) |
| 78 | function onHandlerDown() { |
| 79 | handlerDown.value = true |
| 80 | } |
| 81 | function updateSize(v?: number) { |
| 82 | if (vertical.value) |
| 83 | editorHeight.value = Math.min(Math.max(300, v ?? editorHeight.value), window.innerHeight - 200) |
| 84 | else |
| 85 | editorWidth.value = Math.min(Math.max(318, v ?? editorWidth.value), window.innerWidth - 200) |
| 86 | } |
| 87 | function switchTab(newTab: typeof tab.value) { |
| 88 | tab.value = newTab |
| 89 | // @ts-expect-error force cast |
| 90 | document.activeElement?.blur?.() |
| 91 | } |
| 92 | |
| 93 | if (props.resize) { |
| 94 | useEventListener('pointermove', (e) => { |
| 95 | if (handlerDown.value) { |
| 96 | updateSize(vertical.value |
| 97 | ? window.innerHeight - e.pageY |
| 98 | : window.innerWidth - e.pageX) |
| 99 | } |
| 100 | }, { passive: true }) |
| 101 | useEventListener('pointerup', () => { |
| 102 | handlerDown.value = false |
| 103 | }) |
| 104 | useEventListener('resize', () => { |
| 105 | updateSize() |
| 106 | }) |
| 107 | } |
| 108 | |
| 109 | throttledWatch( |
| 110 | [content, note], |
| 111 | () => { |
| 112 | if (dirty.value) |
| 113 | save() |
| 114 | }, |
| 115 | { throttle: 500 }, |
| 116 | ) |
| 117 | </script> |
| 118 | |
| 119 | <template> |
| 120 | <div |
| 121 | v-if="resize" class="fixed bg-gray-400 select-none opacity-0 hover:opacity-10 z-dragging" |
| 122 | :class="vertical ? 'left-0 right-0 w-full h-10px' : 'top-0 bottom-0 w-10px h-full'" :style="{ |
| 123 | opacity: handlerDown ? '0.3' : undefined, |
| 124 | bottom: vertical ? `${editorHeight - 5}px` : undefined, |
| 125 | right: !vertical ? `${editorWidth - 5}px` : undefined, |
| 126 | cursor: vertical ? 'row-resize' : 'col-resize', |
| 127 | }" @pointerdown="onHandlerDown" |
| 128 | /> |
| 129 | <div |
| 130 | class="shadow bg-main p-2 pt-4 grid grid-rows-[max-content_1fr] h-full overflow-hidden" |
| 131 | :class="resize ? 'border-l border-gray-400 border-opacity-20' : ''" |
| 132 | :style="resize ? { |
| 133 | height: vertical ? `${editorHeight}px` : undefined, |
| 134 | width: !vertical ? `${editorWidth}px` : undefined, |
| 135 | } : {}" |
| 136 | > |
| 137 | <div class="flex pb-2 text-xl -mt-1"> |
| 138 | <div class="mr-4 rounded flex"> |
| 139 | <IconButton |
| 140 | title="Switch to content tab" :class="tab === 'content' ? 'text-primary' : ''" |
| 141 | @click="switchTab('content')" |
| 142 | > |
| 143 | <div class="i-carbon:account" /> |
| 144 | </IconButton> |
| 145 | <IconButton |
| 146 | title="Switch to notes tab" :class="tab === 'note' ? 'text-primary' : ''" |
| 147 | @click="switchTab('note')" |
| 148 | > |
| 149 | <div class="i-carbon:align-box-bottom-right" /> |
| 150 | </IconButton> |
| 151 | </div> |
| 152 | <span class="text-2xl pt-1"> |
| 153 | {{ tab === 'content' ? 'Slide' : 'Notes' }} |
| 154 | </span> |
| 155 | <div class="flex-auto" /> |
| 156 | <template v-if="resize"> |
| 157 | <IconButton v-if="vertical" title="Dock to right" @click="vertical = false"> |
| 158 | <div class="i-carbon:open-panel-right" /> |
| 159 | </IconButton> |
| 160 | <IconButton v-else title="Dock to bottom" @click="vertical = true"> |
| 161 | <div class="i-carbon:open-panel-bottom" /> |
| 162 | </IconButton> |
| 163 | </template> |
| 164 | <IconButton title="Open in editor" @click="openInEditor()"> |
| 165 | <div class="i-carbon:launch" /> |
| 166 | </IconButton> |
| 167 | <IconButton title="Close" @click="close"> |
| 168 | <div class="i-carbon:close" /> |
| 169 | </IconButton> |
| 170 | </div> |
| 171 | <div class="relative overflow-hidden rounded" style="background-color: var(--slidev-code-background)"> |
| 172 | <ShikiEditor v-show="tab === 'content'" v-model="contentRef" placeholder="Create slide content..." /> |
| 173 | <ShikiEditor v-show="tab === 'note'" v-model="noteRef" placeholder="Write some notes..." /> |
| 174 | </div> |
| 175 | </div> |
| 176 | </template> |
| 177 |