|
|
|
|
@ -1,6 +1,11 @@ |
|
|
|
|
<template> |
|
|
|
|
<div class="base-tiny-editor"> |
|
|
|
|
<label v-if="title" :for="editorId">{{ title }}</label> |
|
|
|
|
<label |
|
|
|
|
v-if="title" |
|
|
|
|
:for="editorId" |
|
|
|
|
> |
|
|
|
|
{{ title }} |
|
|
|
|
</label> |
|
|
|
|
<TinyEditor |
|
|
|
|
:id="editorId" |
|
|
|
|
:model-value="modelValue" |
|
|
|
|
@ -9,30 +14,64 @@ |
|
|
|
|
@update:model-value="updateValue" |
|
|
|
|
@input="updateValue" |
|
|
|
|
/> |
|
|
|
|
<p v-if="helpText" class="help-text">{{ helpText }}</p> |
|
|
|
|
<p |
|
|
|
|
v-if="helpText" |
|
|
|
|
class="help-text" |
|
|
|
|
> |
|
|
|
|
{{ helpText }} |
|
|
|
|
</p> |
|
|
|
|
</div> |
|
|
|
|
</template> |
|
|
|
|
|
|
|
|
|
<script setup> |
|
|
|
|
import { ref, computed, watch } from 'vue' |
|
|
|
|
import TinyEditor from '@tinymce/tinymce-vue' |
|
|
|
|
import { computed, ref } from "vue" |
|
|
|
|
import TinyEditor from "@tinymce/tinymce-vue" |
|
|
|
|
import { useRoute, useRouter } from "vue-router" |
|
|
|
|
import { useCidReqStore } from "../../store/cidReq" |
|
|
|
|
import { storeToRefs } from "pinia" |
|
|
|
|
import { useStore } from "vuex" |
|
|
|
|
import { TINYEDITOR_MODE_DOCUMENTS, TINYEDITOR_MODE_PERSONAL_FILES, TINYEDITOR_MODES } from "./TinyEditorOptions" |
|
|
|
|
|
|
|
|
|
const props = defineProps({ |
|
|
|
|
editorId: String, |
|
|
|
|
modelValue: String, |
|
|
|
|
required: Boolean, |
|
|
|
|
editorConfig: Object, |
|
|
|
|
title: String, |
|
|
|
|
helpText: String, |
|
|
|
|
mode: { type: String, default: 'personal_files' }, |
|
|
|
|
useFileManager: { type: Boolean, default: false } |
|
|
|
|
editorId: { |
|
|
|
|
type: String, |
|
|
|
|
required: true, |
|
|
|
|
}, |
|
|
|
|
modelValue: { |
|
|
|
|
type: String, |
|
|
|
|
required: true, |
|
|
|
|
}, |
|
|
|
|
required: { |
|
|
|
|
type: Boolean, |
|
|
|
|
default: false, |
|
|
|
|
}, |
|
|
|
|
title: { |
|
|
|
|
type: String, |
|
|
|
|
default: "", |
|
|
|
|
}, |
|
|
|
|
editorConfig: { |
|
|
|
|
type: Object, |
|
|
|
|
default: () => {}, |
|
|
|
|
}, |
|
|
|
|
// A helper text shown below editor |
|
|
|
|
helpText: { |
|
|
|
|
type: String, |
|
|
|
|
default: "", |
|
|
|
|
}, |
|
|
|
|
// if true the Chamilo inner file manager will be shown |
|
|
|
|
// if false the system file picker will be shown |
|
|
|
|
useFileManager: { |
|
|
|
|
type: Boolean, |
|
|
|
|
default: false, |
|
|
|
|
}, |
|
|
|
|
// change mode when useFileManager=True |
|
|
|
|
mode: { |
|
|
|
|
type: String, |
|
|
|
|
default: TINYEDITOR_MODE_PERSONAL_FILES, |
|
|
|
|
validator: (value) => TINYEDITOR_MODES.includes(value), |
|
|
|
|
}, |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const emit = defineEmits(['update:modelValue']) |
|
|
|
|
const emit = defineEmits(["update:modelValue"]) |
|
|
|
|
const router = useRouter() |
|
|
|
|
const route = useRoute() |
|
|
|
|
const parentResourceNodeId = ref(0) |
|
|
|
|
@ -47,115 +86,188 @@ if (route.params.node) { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const updateValue = (value) => { |
|
|
|
|
emit('update:modelValue', value) |
|
|
|
|
emit("update:modelValue", value) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const toolbarUndo = "undo redo" |
|
|
|
|
const toolbarFormatText = "bold italic underline strikethrough" |
|
|
|
|
const toolbarInsertMedia = "image media template link" |
|
|
|
|
const toolbarFontConfig = "fontselect fontsizeselect formatselect" |
|
|
|
|
const toolbarAlign = "alignleft aligncenter alignright alignjustify" |
|
|
|
|
const toolbarIndent = "outdent indent" |
|
|
|
|
const toolbarList = "numlist bullist" |
|
|
|
|
const toolbarColor = "forecolor backcolor removeformat" |
|
|
|
|
const toolbarPageBreak = "pagebreak" |
|
|
|
|
const toolbarSpecialSymbols = "charmap emoticons" |
|
|
|
|
const toolbarOther = "fullscreen preview save print" |
|
|
|
|
const toolbarCode = "code codesample" |
|
|
|
|
const toolbarTextDirection = "ltr rtl" |
|
|
|
|
|
|
|
|
|
const defaultEditorConfig = { |
|
|
|
|
skin_url: '/build/libs/tinymce/skins/ui/oxide', |
|
|
|
|
content_css: '/build/libs/tinymce/skins/content/default/content.css', |
|
|
|
|
skin_url: "/build/libs/tinymce/skins/ui/oxide", |
|
|
|
|
content_css: "/build/libs/tinymce/skins/content/default/content.css", |
|
|
|
|
branding: false, |
|
|
|
|
relative_urls: false, |
|
|
|
|
height: 280, |
|
|
|
|
toolbar_mode: 'sliding', |
|
|
|
|
height: 500, |
|
|
|
|
toolbar_mode: "sliding", |
|
|
|
|
autosave_ask_before_unload: true, |
|
|
|
|
plugins: [ |
|
|
|
|
'advlist autolink lists link image charmap print preview anchor', |
|
|
|
|
'searchreplace visualblocks code fullscreen', |
|
|
|
|
'insertdatetime media table paste wordcount emoticons', |
|
|
|
|
"advlist", |
|
|
|
|
"anchor", |
|
|
|
|
"autolink", |
|
|
|
|
"charmap", |
|
|
|
|
"code", |
|
|
|
|
"codesample", |
|
|
|
|
"directionality", |
|
|
|
|
"fullpage", |
|
|
|
|
"fullscreen", |
|
|
|
|
"emoticons", |
|
|
|
|
"image", |
|
|
|
|
"insertdatetime", |
|
|
|
|
"link", |
|
|
|
|
"lists", |
|
|
|
|
"media", |
|
|
|
|
"paste", |
|
|
|
|
"preview", |
|
|
|
|
"print", |
|
|
|
|
"pagebreak", |
|
|
|
|
"save", |
|
|
|
|
"searchreplace", |
|
|
|
|
"table", |
|
|
|
|
"template", |
|
|
|
|
"visualblocks", |
|
|
|
|
"wordcount", |
|
|
|
|
], |
|
|
|
|
toolbar: 'undo redo | bold italic underline strikethrough | ...', |
|
|
|
|
file_picker_callback: filePickerCallback |
|
|
|
|
toolbar: |
|
|
|
|
toolbarUndo + |
|
|
|
|
" | " + |
|
|
|
|
toolbarFormatText + |
|
|
|
|
" | " + |
|
|
|
|
toolbarInsertMedia + |
|
|
|
|
" | " + |
|
|
|
|
toolbarFontConfig + |
|
|
|
|
" | " + |
|
|
|
|
toolbarAlign + |
|
|
|
|
" | " + |
|
|
|
|
toolbarIndent + |
|
|
|
|
" | " + |
|
|
|
|
toolbarList + |
|
|
|
|
" | " + |
|
|
|
|
toolbarColor + |
|
|
|
|
" | " + |
|
|
|
|
toolbarPageBreak + |
|
|
|
|
" | " + |
|
|
|
|
toolbarSpecialSymbols + |
|
|
|
|
" | " + |
|
|
|
|
toolbarOther + |
|
|
|
|
" | " + |
|
|
|
|
toolbarCode + |
|
|
|
|
" | " + |
|
|
|
|
toolbarTextDirection, |
|
|
|
|
file_picker_callback: filePickerCallback, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const editorConfig = computed(() => ({ |
|
|
|
|
...defaultEditorConfig, |
|
|
|
|
...props.editorConfig |
|
|
|
|
...props.editorConfig, |
|
|
|
|
})) |
|
|
|
|
|
|
|
|
|
function filePickerCallback(callback, value, meta) { |
|
|
|
|
async function filePickerCallback(callback, value, meta) { |
|
|
|
|
if (!props.useFileManager) { |
|
|
|
|
const input = document.createElement('input'); |
|
|
|
|
input.setAttribute('type', 'file'); |
|
|
|
|
input.style.display = 'none'; |
|
|
|
|
|
|
|
|
|
input.onchange = () => { |
|
|
|
|
const file = input.files[0]; |
|
|
|
|
const title = file.name; |
|
|
|
|
const comment = ''; |
|
|
|
|
const fileType = 'file'; |
|
|
|
|
const resourceLinkList = []; |
|
|
|
|
|
|
|
|
|
const formData = new FormData(); |
|
|
|
|
formData.append('uploadFile', file); |
|
|
|
|
formData.append('title', title); |
|
|
|
|
formData.append('comment', comment); |
|
|
|
|
formData.append('parentResourceNodeId', parentResourceNodeId.value); |
|
|
|
|
formData.append('filetype', fileType); |
|
|
|
|
formData.append('resourceLinkList', resourceLinkList); |
|
|
|
|
|
|
|
|
|
fetch('/file-manager/upload-image', { |
|
|
|
|
method: 'POST', |
|
|
|
|
body: formData, |
|
|
|
|
}) |
|
|
|
|
.then(response => response.json()) |
|
|
|
|
.then(data => { |
|
|
|
|
if (data.location) { |
|
|
|
|
callback(data.location); |
|
|
|
|
} else { |
|
|
|
|
console.error('Failed to upload file'); |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
.catch(error => console.error('Error uploading file:', error)) |
|
|
|
|
.finally(() => document.body.removeChild(input)); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
document.body.appendChild(input); |
|
|
|
|
input.click(); |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let url; |
|
|
|
|
if (props.mode === 'personal_files') { |
|
|
|
|
url = '/resources/filemanager/personal_list/' + parentResourceNodeId.value; |
|
|
|
|
} else if (props.mode === 'documents') { |
|
|
|
|
const cidReqStore = useCidReqStore(); |
|
|
|
|
const { course, session } = storeToRefs(cidReqStore); |
|
|
|
|
|
|
|
|
|
let nodeId = course.value && course.value.resourceNode ? course.value.resourceNode.id : null; |
|
|
|
|
|
|
|
|
|
if (!nodeId) { |
|
|
|
|
console.error('Resource node ID is not available.'); |
|
|
|
|
return; |
|
|
|
|
const input = document.createElement("input") |
|
|
|
|
input.setAttribute("type", "file") |
|
|
|
|
if ("image" === meta.filetype) { |
|
|
|
|
input.accept = "image/*" |
|
|
|
|
} else if ("media" === meta.filetype) { |
|
|
|
|
input.accept = "audio/*, video/*" |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let folderParams = Object.entries(route.query).map(([key, value]) => `${key}=${value}`).join('&'); |
|
|
|
|
url = router.resolve({ name: "DocumentForHtmlEditor", params: { id: nodeId }, query: route.query }).href; |
|
|
|
|
input.style.display = "none" |
|
|
|
|
input.onchange = inputFileHandler(callback, input) |
|
|
|
|
document.body.appendChild(input) |
|
|
|
|
input.click() |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (meta.filetype === 'image') { |
|
|
|
|
url += "&type=images"; |
|
|
|
|
let url = getUrlForTinyEditor(props.mode) |
|
|
|
|
if (meta.filetype === "image") { |
|
|
|
|
url += "&type=images" |
|
|
|
|
} else { |
|
|
|
|
url += "&type=files"; |
|
|
|
|
url += "&type=files" |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
window.addEventListener("message", function (event) { |
|
|
|
|
var data = event.data; |
|
|
|
|
let data = event.data |
|
|
|
|
if (data.url) { |
|
|
|
|
url = data.url; |
|
|
|
|
callback(url); |
|
|
|
|
url = data.url |
|
|
|
|
callback(url) |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// tinymce is already in the global scope, set by backend and php |
|
|
|
|
tinymce.activeEditor.windowManager.openUrl({ |
|
|
|
|
url: url, |
|
|
|
|
title: "File manager", |
|
|
|
|
onMessage: (api, message) => { |
|
|
|
|
if (message.mceAction === 'fileSelected') { |
|
|
|
|
const fileUrl = message.content; |
|
|
|
|
callback(fileUrl); |
|
|
|
|
api.close(); |
|
|
|
|
if (message.mceAction === "fileSelected") { |
|
|
|
|
const fileUrl = message.content |
|
|
|
|
callback(fileUrl) |
|
|
|
|
api.close() |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function inputFileHandler(callback, input) { |
|
|
|
|
return async () => { |
|
|
|
|
const file = input.files[0] |
|
|
|
|
const title = file.name |
|
|
|
|
const comment = "" |
|
|
|
|
const fileType = "file" |
|
|
|
|
const resourceLinkList = [] |
|
|
|
|
|
|
|
|
|
const formData = new FormData() |
|
|
|
|
formData.append("uploadFile", file) |
|
|
|
|
formData.append("title", title) |
|
|
|
|
formData.append("comment", comment) |
|
|
|
|
formData.append("parentResourceNodeId", parentResourceNodeId.value) |
|
|
|
|
formData.append("filetype", fileType) |
|
|
|
|
formData.append("resourceLinkList", resourceLinkList) |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
let response = await fetch("/file-manager/upload-image", { |
|
|
|
|
method: "POST", |
|
|
|
|
body: formData, |
|
|
|
|
}) |
|
|
|
|
const { data, location } = await response.json() |
|
|
|
|
if (location) { |
|
|
|
|
callback(location, { alt: data.title }) |
|
|
|
|
} else { |
|
|
|
|
console.error("Failed to upload file") |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error("Error uploading file:", error) |
|
|
|
|
} finally { |
|
|
|
|
document.body.removeChild(input) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getUrlForTinyEditor(mode) { |
|
|
|
|
if (props.mode === TINYEDITOR_MODE_PERSONAL_FILES) { |
|
|
|
|
return "/resources/filemanager/personal_list/" + parentResourceNodeId.value |
|
|
|
|
} else if (props.mode === TINYEDITOR_MODE_DOCUMENTS) { |
|
|
|
|
const cidReqStore = useCidReqStore() |
|
|
|
|
const { course } = storeToRefs(cidReqStore) |
|
|
|
|
|
|
|
|
|
let nodeId = course.value && course.value.resourceNode ? course.value.resourceNode.id : null |
|
|
|
|
if (!nodeId) { |
|
|
|
|
console.error("Resource node ID is not available.") |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
return router.resolve({ name: "DocumentForHtmlEditor", params: { id: nodeId }, query: route.query }).href |
|
|
|
|
} else { |
|
|
|
|
console.error(`Mode "${mode}" is not valid. Check valid modes on TinyEditorOptions.js`) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
</script> |
|
|
|
|
|