TinyMce for posts
This commit is contained in:
@@ -16,8 +16,8 @@ import Wallet from '../views/main/Wallet.vue'
|
||||
import ProfilePage from '@/views/profile/ProfilePage.vue'
|
||||
import CreatorList from '@/views/browser/CreatorList.vue'
|
||||
import PostContent from "@/views/contents/PostContent.vue";
|
||||
import ContentEditorPage from "@/views/contents/ContentEditorPage.vue";
|
||||
import {useAuthStore} from "@/stores/authStore.js";
|
||||
|
||||
import CreatorLayout from "@/views/creators/CreatorLayout.vue";
|
||||
import ContentPage from "@/views/contents/ContentPage.vue";
|
||||
import CreatorContent from "@/views/creators/CreatorContent.vue";
|
||||
@@ -26,7 +26,6 @@ import CTA01 from "@/views/CTA01.vue";
|
||||
import SubscriptionMenu from "@/views/creators/SubscriptionMenu.vue";
|
||||
import Utilitylinks from "@/views/documentation/utilitylinks.vue";
|
||||
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/cta01',
|
||||
@@ -44,6 +43,10 @@ const routes = [
|
||||
path: '/browse',
|
||||
component: CreatorList
|
||||
},
|
||||
{
|
||||
path: '/content/editor',
|
||||
component: ContentEditorPage
|
||||
},
|
||||
{
|
||||
path: '/content/:contentId',
|
||||
component: ContentPage
|
||||
|
||||
150
src/views/contents/ContentEditorPage.vue
Normal file
150
src/views/contents/ContentEditorPage.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import Editor from '@tinymce/tinymce-vue';
|
||||
import '@tinymce/tinymce-vue';
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useUserProfileStore} from "@/stores/userProfileStore.js";
|
||||
import {v7} from "uuid";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const client = useClient();
|
||||
|
||||
const tinymceScriptSrc = '/tinymce/js/tinymce/tinymce.min.js';
|
||||
const content = ref('');
|
||||
const title = ref('');
|
||||
let lastUploadedFileName = '';
|
||||
const isSnackbarOpen = ref(false);
|
||||
const snackbarTimeout = ref(2000);
|
||||
const snackbarText = ref('');
|
||||
const snackbarColor = ref('red');
|
||||
|
||||
const userStore = useUserProfileStore();
|
||||
|
||||
// Custom image upload handler
|
||||
const imagesUploadHandler = async (blobInfo) => {
|
||||
const formData = new FormData();
|
||||
formData.append('id', v7());
|
||||
formData.append('files', blobInfo.blob(), lastUploadedFileName);
|
||||
formData.append('creatorId', userStore.user.id);
|
||||
|
||||
let response = await client.post("/api/content/insert-image", formData);
|
||||
let imageUrl = response.data[0];
|
||||
|
||||
/* global tinymce */
|
||||
const editor = tinymce.activeEditor;
|
||||
const images = editor.dom.select('img');
|
||||
const lastImage = images.find(x => x.alt = lastUploadedFileName);
|
||||
|
||||
if (lastImage) {
|
||||
// Replace the source of the image
|
||||
editor.dom.setAttrib(lastImage, 'src', imageUrl);
|
||||
editor.dom.setAttrib(lastImage, 'alt', lastUploadedFileName);
|
||||
|
||||
// Adds the change to the undo stack
|
||||
editor.undoManager.add();
|
||||
} else {
|
||||
console.error('No image found in the content.');
|
||||
}
|
||||
};
|
||||
|
||||
const filePickerCallback = (callback, value, meta) => {
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
input.setAttribute('accept', 'image/*');
|
||||
|
||||
input.onchange = function () {
|
||||
const file = input.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function () {
|
||||
lastUploadedFileName = file.name;
|
||||
callback(reader.result);
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
input.click();
|
||||
};
|
||||
|
||||
const saveAsync = async () => {
|
||||
if (title.value === '') {
|
||||
snackbarText.value = "Vous avez besoin d'un titre";
|
||||
isSnackbarOpen.value = true;
|
||||
}
|
||||
|
||||
const fullHtmlContent = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title.value}</title>
|
||||
</head>
|
||||
<body>
|
||||
${content.value}
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', v7());
|
||||
formData.append('creatorId', userStore.user.id);
|
||||
formData.append('title', title.value);
|
||||
formData.append('htmlContent', fullHtmlContent);
|
||||
const response = await client.post("/api/contents/html", formData);
|
||||
|
||||
|
||||
if (response.status === 200) {
|
||||
snackbarText.value = "Publier";
|
||||
snackbarColor.value = "green";
|
||||
isSnackbarOpen.value = true;
|
||||
router.go(-1)
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-snackbar v-model="isSnackbarOpen" :timeout="snackbarTimeout">
|
||||
{{ snackbarText }}
|
||||
|
||||
<template v-slot:actions>
|
||||
<v-btn :color="snackbarColor" variant="text" @click="isSnackbarOpen = false">
|
||||
Fermer
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
|
||||
<div class="w-full h-full flex flex-col items-center justify-start">
|
||||
<v-btn class="mb-4 text-xl px-6 py-3" @click="router.go(-1)">Return</v-btn>
|
||||
<v-text-field
|
||||
v-model="title"
|
||||
placeholder="Title"
|
||||
style="width: 40%; font-size: 1.5rem; padding: 10px;"
|
||||
></v-text-field>
|
||||
<Editor
|
||||
style="max-width: 400px; width: 50%; font-size: 1.5rem; padding: 10px; height: 120%"
|
||||
:tinymceScriptSrc="tinymceScriptSrc"
|
||||
v-model="content"
|
||||
:init="{
|
||||
branding: false,
|
||||
promotion: false,
|
||||
plugins: 'lists link emoticons image imagetools code help wordcount media autoresize',
|
||||
block_formats: 'Paragraph=p; Header 1=h1; Header 2=h2; Header 3=h3',
|
||||
toolbar: 'undo redo image align',
|
||||
automatic_uploads: true,
|
||||
file_picker_types: 'image',
|
||||
min_height: 600,
|
||||
max_height: 1200,
|
||||
images_upload_handler: imagesUploadHandler,
|
||||
file_picker_callback: filePickerCallback
|
||||
}"
|
||||
/>
|
||||
<v-btn @click="saveAsync()">POST</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -40,6 +40,26 @@
|
||||
<div>
|
||||
{{ props.content.description }}
|
||||
</div>
|
||||
<div v-if="props.content.htmlFileUrl !== ''" class="html-content">
|
||||
|
||||
<div v-if="isIframeLoading">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
</div>
|
||||
|
||||
<iframe
|
||||
ref="iframe"
|
||||
:src="props.content.htmlFileUrl"
|
||||
class="w-full"
|
||||
:style="{ height: iframeHeight + 'px', overflow: 'hidden' }"
|
||||
allowfullscreen
|
||||
@load="isIframeLoading = false"
|
||||
v-show="!isIframeLoading"
|
||||
></iframe>
|
||||
<!-- Expand button to toggle full size -->
|
||||
<v-btn v-if="showExpandButton" @click="toggleExpand">
|
||||
{{ isExpanded ? 'Collapse' : 'Expand' }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<v-carousel
|
||||
@@ -131,7 +151,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, onBeforeMount, ref} from 'vue';
|
||||
import {computed, onBeforeMount, onMounted, onUnmounted, ref} from 'vue';
|
||||
import {time_ago} from "@/internal_time_ago.js";
|
||||
import MessageList from "@/views/messages/MessageList.vue";
|
||||
import PostMessage from "@/views/messages/PostMessage.vue";
|
||||
@@ -166,6 +186,12 @@ const messagesVisible = ref(false);
|
||||
const messages = ref([]);
|
||||
const hasMessages = computed(() => messages.value.length > 0);
|
||||
|
||||
const iframeHeight = ref(300);
|
||||
const minHeight = ref(200);
|
||||
const maxHeight = ref(1200);
|
||||
const isExpanded = ref(false);
|
||||
const showExpandButton = ref(false);
|
||||
const isIframeLoading = ref(true);
|
||||
|
||||
onBeforeMount(async () => {
|
||||
messageCount.value = await messageStore.fetchMessageCount(contentId.value)
|
||||
@@ -205,6 +231,33 @@ async function deleteContent() {
|
||||
}
|
||||
}
|
||||
|
||||
const observeIframeSize = () => {
|
||||
const iframe = document.querySelector('iframe');
|
||||
|
||||
if (iframe && iframe.contentWindow) {
|
||||
// Access the iframe's content document and measure the scrollHeight
|
||||
const iframeDocument = iframe.contentWindow.document;
|
||||
const contentHeight = iframeDocument.body.scrollHeight;
|
||||
|
||||
console.log('Content height:', contentHeight);
|
||||
|
||||
// If content height exceeds 200px, show the expand button
|
||||
if (contentHeight > 200) {
|
||||
showExpandButton.value = true;
|
||||
} else {
|
||||
showExpandButton.value = false;
|
||||
}
|
||||
|
||||
// Set iframe height initially to the smaller value (200px) or full content height if expanded
|
||||
iframeHeight.value = isExpanded.value ? contentHeight : Math.min(contentHeight, 200);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleExpand = () => {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
observeIframeSize();
|
||||
};
|
||||
|
||||
function redirectToContent() {
|
||||
window.location.href = `/content/${props.content.id}`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div class="w-full h-full">
|
||||
<v-btn class="flex justify-end" @click="createHtmlContent">Create Custom</v-btn>
|
||||
<v-btn class="flex justify-end" @click="createContent">Create</v-btn>
|
||||
<div v-if="brandingStore.value.loading">
|
||||
<v-progress-linear indeterminate></v-progress-linear>
|
||||
</div>
|
||||
@@ -13,5 +15,16 @@
|
||||
<script async setup>
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
import ContentList from "@/views/contents/ContentList.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
const brandingStore = useBrandingStore()
|
||||
const router = useRouter();
|
||||
|
||||
const createHtmlContent = () => {
|
||||
router.push('/content/editor');
|
||||
}
|
||||
|
||||
const createContent = () => {
|
||||
router.push('/content/post');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<template>
|
||||
|
||||
<div class="flex flex-col min-h-screen max-w-[1500px] mx-auto">
|
||||
|
||||
<div v-if="brandingStore.loading">
|
||||
<v-progress-linear indeterminate></v-progress-linear>
|
||||
</div>
|
||||
<div v-else>
|
||||
<creator-banner></creator-banner>
|
||||
</div>
|
||||
|
||||
<div class="py-8 flex-grow">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user