532 lines
15 KiB
Vue
532 lines
15 KiB
Vue
<template>
|
|
<div class="sm-editor">
|
|
<Editor
|
|
ref="tinyeditor"
|
|
v-model="editorContent"
|
|
model-events="change blur focus"
|
|
output-format="html"
|
|
:init="init"
|
|
:disabled="props.disabled"
|
|
@blur="handleBlur"
|
|
@focus="handleFocus"
|
|
@change="handleChange" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import Editor from "@tinymce/tinymce-vue";
|
|
import "tinymce/tinymce";
|
|
import "tinymce/themes/silver";
|
|
|
|
import "tinymce/icons/default";
|
|
import "tinymce/models/dom";
|
|
|
|
import "tinymce/plugins/advlist";
|
|
import "tinymce/plugins/anchor";
|
|
import "tinymce/plugins/autolink";
|
|
import "tinymce/plugins/autosave";
|
|
import "tinymce/plugins/charmap";
|
|
import "tinymce/plugins/code";
|
|
import "tinymce/plugins/codesample";
|
|
import "tinymce/plugins/directionality";
|
|
import "tinymce/plugins/emoticons";
|
|
import "tinymce/plugins/fullscreen";
|
|
import "tinymce/plugins/help";
|
|
import "tinymce/plugins/image";
|
|
import "tinymce/plugins/importcss";
|
|
import "tinymce/plugins/insertdatetime";
|
|
import "tinymce/plugins/link";
|
|
import "tinymce/plugins/lists";
|
|
import "tinymce/plugins/media";
|
|
import "tinymce/plugins/nonbreaking";
|
|
import "tinymce/plugins/pagebreak";
|
|
import "tinymce/plugins/preview";
|
|
import "tinymce/plugins/searchreplace";
|
|
import "tinymce/plugins/table";
|
|
import "tinymce/plugins/template";
|
|
import "tinymce/plugins/visualblocks";
|
|
import "tinymce/plugins/visualchars";
|
|
import "tinymce/plugins/wordcount";
|
|
|
|
import { computed, ref, watch } from "vue";
|
|
import { api } from "../helpers/api";
|
|
import { MediaCollection, MediaResponse } from "../helpers/api.types";
|
|
import { routes } from "../router";
|
|
|
|
interface PageList {
|
|
title: string;
|
|
value: string;
|
|
}
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
});
|
|
|
|
const useDarkMode = false; // window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
const tinyeditor = ref(null);
|
|
|
|
const init = {
|
|
promotion: false,
|
|
emoticons_database_url: "/tinymce/plugins/emoticons/js/emojis.min.js",
|
|
template_cdate_format: "[Date Created (CDATE): %m/%d/%Y : %H:%M:%S]",
|
|
template_mdate_format: "[Date Modified (MDATE): %m/%d/%Y : %H:%M:%S]",
|
|
relative_urls: false,
|
|
skin_url: useDarkMode
|
|
? "/tinymce/skins/ui/oxide-dark"
|
|
: "/tinymce/skins/ui/oxide",
|
|
content_css: useDarkMode
|
|
? "/tinymce/skins/content/default/dark.min.css"
|
|
: "/tinymce/skins/content/default/content.min.css",
|
|
height: 600,
|
|
plugins: [
|
|
"link",
|
|
"autolink",
|
|
"lists",
|
|
"advlist",
|
|
"image",
|
|
"table",
|
|
"charmap",
|
|
"searchreplace",
|
|
"visualblocks",
|
|
"code",
|
|
"anchor",
|
|
"insertdatetime",
|
|
"media",
|
|
"help",
|
|
"codesample",
|
|
"nonbreaking",
|
|
"importcss",
|
|
"directionality",
|
|
"visualchars",
|
|
"emoticons",
|
|
"autosave",
|
|
"searchreplace",
|
|
],
|
|
toolbar:
|
|
"h1 h2 h3 blockquote | bold italic underline strikethrough | numlist bullist | image media link anchor codesample | alignleft aligncenter alignright alignjustify | forecolor backcolor removeformat | outdent indent | charmap emoticons | undo redo",
|
|
branding: false,
|
|
menubar: false,
|
|
toolbar_mode: "sliding",
|
|
autosave_ask_before_unload: true,
|
|
autosave_interval: "30s",
|
|
autosave_prefix: "{path}{query}-{id}-",
|
|
autosave_restore_when_empty: false,
|
|
autosave_retention: "2m",
|
|
image_advtab: true,
|
|
codesample_global_prismjs: true,
|
|
codesample_languages: [
|
|
{ text: "Bash", value: "bash" },
|
|
{ text: "C", value: "c" },
|
|
{ text: "C++", value: "cpp" },
|
|
{ text: "C#", value: "csharp" },
|
|
{ text: "CSS", value: "css" },
|
|
{ text: "HTML/XML", value: "markup" },
|
|
{ text: "Java", value: "java" },
|
|
{ text: "JavaScript", value: "javascript" },
|
|
{ text: "Objective-C", value: "objectivec" },
|
|
{ text: "Perl", value: "perl" },
|
|
{ text: "PHP", value: "php" },
|
|
{ text: "Python", value: "python" },
|
|
{ text: "Regex", value: "regex" },
|
|
{ text: "Ruby", value: "ruby" },
|
|
{ text: "SQL", value: "sql" },
|
|
{ text: "Swift", value: "swift" },
|
|
{ text: "YAML", value: "yml" },
|
|
],
|
|
link_title: false,
|
|
link_list: (success) => {
|
|
const links = fetchLinkList();
|
|
success(links);
|
|
},
|
|
file_picker_callback: function (callback, value, meta) {
|
|
imageBrowser(callback, value, meta);
|
|
},
|
|
};
|
|
|
|
const editorContent = ref(props.modelValue);
|
|
|
|
const emits = defineEmits(["input", "update:modelValue", "blur", "focus"]);
|
|
|
|
/* Updating value */
|
|
const handleInitialContentChange = (newContent) => {
|
|
newContent = newContent === undefined ? "" : newContent;
|
|
editorContent.value = newContent;
|
|
};
|
|
|
|
const initialContent = computed(() => {
|
|
return props.modelValue;
|
|
});
|
|
|
|
watch(initialContent, handleInitialContentChange);
|
|
|
|
const handleBlur = (event) => {
|
|
emits("blur", event);
|
|
};
|
|
|
|
const handleFocus = (event) => {
|
|
emits("focus", event);
|
|
};
|
|
|
|
const handleChange = (event, editor) => {
|
|
emits("update:modelValue", editor.getContent());
|
|
};
|
|
|
|
const fetchLinkList = () => {
|
|
const buildPageList = (
|
|
pageList,
|
|
routeEntries,
|
|
prefix_url = "",
|
|
prefix_title = ""
|
|
) => {
|
|
routeEntries.forEach((entry) => {
|
|
if (
|
|
"path" in entry &&
|
|
entry.path.includes(":") == false &&
|
|
"meta" in entry &&
|
|
"title" in entry.meta &&
|
|
("hideInEditor" in entry.meta == false ||
|
|
entry.meta.hideInEditor == false) &&
|
|
("middleware" in entry.meta == false ||
|
|
("showInEditor" in entry.meta == true &&
|
|
entry.meta.showInEditor == true))
|
|
) {
|
|
const sep = entry.path.substring(0, 1) == "/" ? "" : "/";
|
|
pageList[prefix_url + sep + entry.path] =
|
|
prefix_title.length > 0
|
|
? `${prefix_title} ${sep} ${entry.meta.title}`
|
|
: entry.meta.title.toLowerCase() == "home"
|
|
? entry.meta.title
|
|
: `Home / ${entry.meta.title}`;
|
|
}
|
|
|
|
if ("children" in entry) {
|
|
buildPageList(
|
|
pageList,
|
|
entry.children,
|
|
prefix_url + entry.path,
|
|
prefix_title + (entry.meta?.title || "")
|
|
);
|
|
}
|
|
});
|
|
};
|
|
|
|
let pageRoutes: { [key: string]: string } = {};
|
|
buildPageList(pageRoutes, routes);
|
|
|
|
const pageList: PageList[] = [];
|
|
for (const [key, value] of Object.entries(pageRoutes)) {
|
|
pageList.push({ title: value, value: key });
|
|
}
|
|
|
|
pageList.sort((a, b) => {
|
|
const titleA = a.title.toLowerCase();
|
|
const titleB = b.title.toLowerCase();
|
|
|
|
if (titleA < titleB) {
|
|
return -1;
|
|
}
|
|
|
|
if (titleA > titleB) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
});
|
|
|
|
return pageList;
|
|
};
|
|
|
|
const imageBrowser = (callback, value, meta) => {
|
|
var galleryPage = 1;
|
|
var galleryMax = 1;
|
|
|
|
// Open a dialog to select a file
|
|
const input = document.createElement("input");
|
|
input.setAttribute("type", "file");
|
|
input.setAttribute("accept", "image/*");
|
|
input.onchange = function () {
|
|
if (input.files) {
|
|
let formData = new FormData();
|
|
formData.append("file", input.files[0]);
|
|
|
|
api.post({
|
|
url: "/media",
|
|
body: formData,
|
|
})
|
|
.then((result) => {
|
|
input.value = "";
|
|
const data = result.data as MediaResponse;
|
|
|
|
if (data.medium) {
|
|
callback(data.medium.url);
|
|
dialog.close();
|
|
} else {
|
|
alert("The server responded with an unknown error");
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
input.value = "";
|
|
alert(
|
|
error.data.message ||
|
|
"An unexpected error occurred uploading the file to the server."
|
|
);
|
|
});
|
|
}
|
|
};
|
|
|
|
// create the header element
|
|
const header = document.createElement("div");
|
|
header.id = "tinymce-gallery-header";
|
|
|
|
// create the gallery element
|
|
const gallery = document.createElement("div");
|
|
gallery.id = "tinymce-gallery";
|
|
|
|
const updateGallery = () => {
|
|
api.get({
|
|
url: "/media",
|
|
params: {
|
|
limit: 12,
|
|
page: galleryPage,
|
|
mime: "image/",
|
|
},
|
|
})
|
|
.then((result) => {
|
|
const data = result.data as MediaCollection;
|
|
galleryMax = Math.ceil(data.total / 12);
|
|
|
|
const infoElement = document.querySelector(
|
|
"#tinymce-gallery-header .info"
|
|
);
|
|
if (infoElement != null) {
|
|
infoElement.innerHTML = `${galleryPage} / ${galleryMax}`;
|
|
}
|
|
|
|
const galleryContainer =
|
|
document.getElementById("tinymce-gallery");
|
|
if (galleryContainer != null) {
|
|
// delete existing items
|
|
const divElements =
|
|
galleryContainer.querySelectorAll("div");
|
|
divElements.forEach((div) => {
|
|
div.remove();
|
|
});
|
|
|
|
// add new items
|
|
data.media.forEach((medium) => {
|
|
const img = document.createElement("div");
|
|
img.classList.add("gallery-image");
|
|
img.style.backgroundImage = `url('${medium.url}?w=200')`;
|
|
img.style.cursor = "pointer";
|
|
img.onclick = function () {
|
|
callback(medium.url);
|
|
dialog.close();
|
|
};
|
|
|
|
galleryContainer.appendChild(img);
|
|
});
|
|
}
|
|
})
|
|
.catch(() => {
|
|
/* empty */
|
|
});
|
|
};
|
|
|
|
// Add the container and file input to the dialog
|
|
const dialog = tinymce.activeEditor.windowManager.open({
|
|
title: "Insert image",
|
|
size: "large",
|
|
body: {
|
|
type: "panel",
|
|
items: [
|
|
{
|
|
type: "htmlpanel",
|
|
html: header.outerHTML,
|
|
},
|
|
{
|
|
type: "htmlpanel",
|
|
html: gallery.outerHTML,
|
|
},
|
|
],
|
|
},
|
|
buttons: [
|
|
{
|
|
type: "custom",
|
|
text: "Upload",
|
|
name: "upload",
|
|
},
|
|
{
|
|
type: "cancel",
|
|
text: "Cancel",
|
|
},
|
|
],
|
|
onAction: function (_dialogApi, details) {
|
|
if (details.name === "upload") {
|
|
input.click();
|
|
}
|
|
},
|
|
});
|
|
|
|
// create the child elements
|
|
const heading = document.createElement("div");
|
|
heading.className = "heading";
|
|
heading.textContent = "Select an image or upload a new one";
|
|
|
|
const pagination = document.createElement("div");
|
|
pagination.className = "pagination";
|
|
|
|
const prevButton = document.createElement("button");
|
|
prevButton.className = "prev";
|
|
prevButton.addEventListener("click", () => {
|
|
if (galleryPage > 1) {
|
|
galleryPage--;
|
|
updateGallery();
|
|
}
|
|
});
|
|
|
|
const infoDiv = document.createElement("div");
|
|
infoDiv.className = "info";
|
|
infoDiv.textContent = `${galleryPage} / ${galleryMax}`;
|
|
|
|
const nextButton = document.createElement("button");
|
|
nextButton.className = "next";
|
|
nextButton.addEventListener("click", () => {
|
|
if (galleryPage < galleryMax) {
|
|
galleryPage++;
|
|
updateGallery();
|
|
}
|
|
// handle click on the next button
|
|
});
|
|
|
|
// add the child elements to the parent element
|
|
pagination.appendChild(prevButton);
|
|
pagination.appendChild(infoDiv);
|
|
pagination.appendChild(nextButton);
|
|
|
|
const renderedHeader = document.getElementById("tinymce-gallery-header");
|
|
if (renderedHeader) {
|
|
renderedHeader.appendChild(heading);
|
|
renderedHeader.appendChild(pagination);
|
|
}
|
|
|
|
updateGallery();
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.editor {
|
|
width: 100%;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
#tinymce-gallery-header {
|
|
display: flex;
|
|
|
|
div.heading {
|
|
flex: 1;
|
|
}
|
|
|
|
div.pagination {
|
|
display: flex;
|
|
text-align: right;
|
|
justify-content: center;
|
|
|
|
div.info {
|
|
display: inline-block;
|
|
}
|
|
}
|
|
|
|
button {
|
|
height: 24px;
|
|
width: 24px;
|
|
position: relative;
|
|
margin: 0 8px;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
background-color: #f0f0f0;
|
|
|
|
&:before {
|
|
content: "";
|
|
position: absolute;
|
|
height: 8px;
|
|
width: 8px;
|
|
}
|
|
|
|
&:hover {
|
|
background-color: #e3e3e3;
|
|
}
|
|
}
|
|
|
|
button.prev {
|
|
&:before {
|
|
content: "";
|
|
border-left: 2px solid #222f3e;
|
|
border-bottom: 2px solid #222f3e;
|
|
top: 7px;
|
|
left: 9px;
|
|
transform: rotate(45deg);
|
|
}
|
|
}
|
|
|
|
button.next {
|
|
&:before {
|
|
content: "";
|
|
border-right: 2px solid #222f3e;
|
|
border-bottom: 2px solid #222f3e;
|
|
top: 7px;
|
|
right: 9px;
|
|
transform: rotate(-45deg);
|
|
}
|
|
}
|
|
}
|
|
|
|
#tinymce-gallery {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
margin-top: 12px;
|
|
border: 1px solid #eee;
|
|
justify-content: center;
|
|
gap: 1rem;
|
|
overflow-y: auto;
|
|
padding: 0.5rem;
|
|
max-height: 468px;
|
|
|
|
.gallery-image {
|
|
width: 18vw;
|
|
height: 14vh;
|
|
min-height: 113px;
|
|
min-width: 200px;
|
|
background-repeat: no-repeat;
|
|
background-size: cover;
|
|
background-position: center;
|
|
border: 3px solid #fff;
|
|
padding: 2px;
|
|
background-clip: content-box;
|
|
|
|
&:hover {
|
|
border: 3px solid #0060ce;
|
|
}
|
|
}
|
|
}
|
|
|
|
@media only screen and (max-height: 600px) {
|
|
#tinymce-gallery {
|
|
max-height: 428px;
|
|
}
|
|
}
|
|
|
|
@media only screen and (max-height: 570px) {
|
|
#tinymce-gallery {
|
|
height: 60vh;
|
|
}
|
|
}
|
|
</style>
|