Files
dragonroll/frontend/app/components/viewer/content/Note.vue
2026-05-09 20:40:27 +02:00

245 lines
6.0 KiB
Vue

<script setup>
import { onMounted, onUnmounted, ref, createApp } from 'vue';
import { GetWidget, ParseMarkdown } from '~/services/Marker';
import Server from '~/services/Server';
import { DeleteNote, FetchCampaignNotes } from '~/services/Content';
// import { GetNote, GetContent } from '@/services/Content';
const props = defineProps(['text', 'title', 'noteKey']);
const noteContent = ref(null); // Markdown text
const sourceText = ref(''); // Original markdown source, used for editing
const displayText = ref(''); // Compiled HTML from markdown
const editingMode = ref(false);
const title = ref(props.title);
const displayTitle = ref('');
function closeNote(){
DeleteNote(props.noteKey);
}
const compiledMarkdown = computed(() => {
return ParseMarkdown(sourceText.value);
});
function mountComponents() {
// Should no need more
const widget_types = ['display', 'inline', 'link'];
widget_types.forEach((widget_type) => {
const nodes = document.querySelectorAll('.vue-component-' + widget_type);
nodes.forEach(el => {
const app = createApp(GetWidget(widget_type, el.dataset.component), { content: el.dataset.content });
app.mount(el);
});
});
}
///
function update(){
displayText.value = compiledMarkdown.value;
setTimeout(() => {
setupCallout()
mountComponents();
}, 0);
}
watch(sourceText, (newText) => {
// update();
});
onMounted(() => {
sourceText.value = props.text;
title.value = props.title;
displayTitle.value = props.title;
// window.addEventListener('keydown', handleKeydown);
setTimeout(() => setupCallout(), 0);
update();
});
onUnmounted(() => {
// window.removeEventListener('keydown', handleKeydown);
});
function handleKeydown(e) {
// Check for Ctrl + E (or Cmd + E on Mac)
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'e') {
e.preventDefault(); // prevent browser default behavior
editingMode.value = !editingMode.value;
if(!editingMode.value){
update();
SaveNote(); // Save when switching to display mode
}
return;
}
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
e.preventDefault(); // prevent browser default behavior
// Save the note (you can emit an event or call a method here)
SaveNote();
return;
}
}
function SaveNote(){
Server().post('/note/update', {
id: props.noteKey,
content: sourceText.value,
title: title.value,
}).then((response) => {
if(response.data.status !== 'ok'){
// Handle error (e.g., show a notification)
return;
}
// DisplayToast('green', "Note saved successfully.", 500);
FetchCampaignNotes();
}).catch((error) => {
// Handle error (e.g., show a notification)
});
}
function toggleCallout() {
const outerBlock = this.parentElement;
outerBlock.classList.toggle("is-collapsed")
const collapsed = outerBlock.classList.contains("is-collapsed")
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
outerBlock.style.maxHeight = height + "px"
// walk and adjust height of all parents
let current = outerBlock
let parent = outerBlock.parentElement
while (parent) {
if (!parent.classList.contains("callout")) {
return
}
const collapsed = parent.classList.contains("is-collapsed")
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
parent.style.maxHeight = height + "px"
current = parent
parent = parent.parentElement
}
}
function setupCallout() {
if (!noteContent.value) {
return;
}
const collapsible = noteContent.value.getElementsByClassName(
`callout is-collapsible`,
);
for (const div of collapsible) {
const title = div.firstElementChild;
if (title) {
title.addEventListener("click", toggleCallout)
const collapsed = div.classList.contains("is-collapsed")
const height = collapsed ? title.scrollHeight : div.scrollHeight
div.style.maxHeight = height + "px"
}
}
}
const editTitle = (e) => {
title.value = e.target.innerText;
SaveNote();
}
</script>
<template>
<div class="note" @keydown="handleKeydown" tabindex="0">
<div class="note-stunt">
<div class="close-button" v-on:click="closeNote">
<img class="icon" src="/icons/Pixelarticons/white/close.svg" alt="My Happy SVG"/>
</div>
<span>{{ title }}</span>
</div>
<div class="note-content-container">
<textarea v-model="sourceText" class="full-editor" v-if="editingMode"></textarea>
<div v-else class="note-content" ref="noteContent">
<h1 contenteditable="true" ref="editableTitle" @input="editTitle">{{ displayTitle }}</h1>
<div ref="noteContent" v-html="displayText"></div>
</div>
</div>
</div>
</template>
<style scoped>
.note-stunt {
writing-mode: vertical-lr;
position: sticky;
top: 0px;
left: 0px;
bottom: 0px;
padding: 10px;
display: flex;
user-select: none;
}
.full-editor {
width: 100%;
height: 100%;
box-sizing: border-box;
border: none;
outline: none;
resize: none;
padding: 20px;
font-size: 16px;
font-family: monospace; /* optional, gives document/editor feel */
padding-bottom: 400px; /* Small bottom margin */
}
.close-button {
height: 20px;
width: 20px;
margin-bottom: 5px;
display: flex;
justify-content: center;
cursor: pointer;
filter: invert(var(--color-icon-invert));
}
.note {
min-width: 700px;
max-width: 700px;
overflow-y: auto;
border-color: var(--color-note-border);
border-width: 0px;
border-right-width: 1px;
border-style: solid;
display: flex;
background-color: var(--color-background);
position: sticky;
top: 0px;
}
/* Contingut de cada nota */
.note-content {
padding-bottom: 400px;
overflow-y: auto;
max-width: 600px;
padding: 20px;
}
.note-content-container {
width: 100%;
}
.note-content :deep(img) {
max-width: 100%;
height: auto;
display: block; /* optional: avoids inline spacing issues */
}
</style>