ldc-volena/resources/ts/bo/components/CkEditorComponent.vue
2025-03-14 10:50:26 +01:00

432 lines
11 KiB
Vue

<template>
<div
ref="ckEditorComponent"
class="ck-editor-component"
>
<div ref="wordCounter" />
<div ref="ckEditor" />
<input
:id="id"
class="form-control"
type="hidden"
:name="name"
:value="text"
:required="required"
>
</div>
</template>
<script lang="ts" setup>
import { computed, defineOptions, onMounted, ref, useAttrs, type PropType } from "vue";
// @ts-ignore
import CkUploadAdapterPlugin from "./../../../js/ckUploadAdapter";
import "../../../js/vendor/ckeditor";
import parseLaravelErrorMessages from "../../modules/parseLaravelErrorMessages";
import trans from "../../modules/trans";
// @ts-ignore
defineOptions({
name: "CkEditorComponent"
});
// * EMITS
const emits = defineEmits<{
input: [string: string],
}>();
// * ATTRIBUTES
const attrs = useAttrs();
// * REFS
const ckEditorComponent = ref<HTMLDivElement|null>(null);
const ckEditor = ref<HTMLDivElement|null>(null);
const wordCounter = ref<HTMLDivElement|null>(null);
// * PROPS
const props = defineProps({
id: {
type: String,
default: String(Math.pow(10, 16) / Math.random())
},
name: {
type: String,
default: ""
},
dbtext: {
type: String,
default: ""
},
placeholder: {
type: String,
default: ""
},
required: {
type: Boolean,
default: false
},
errors: {
type: Object as PropType<LaravelValidationErrors>,
default: () => { return []; }
},
editorConfig: {
type: String,
default: "basic"
},
modelName: {
type: String,
default: ""
},
});
// * DATA
const id = ref<string>(props.id);
const name = ref<string>(props.name);
const text = ref<string>(props.dbtext);
const placeholder = ref<string>(props.placeholder);
const required = ref<boolean>(props.required);
const allErrors = ref<LaravelValidationErrors>(props.errors);
const editorConfig = ref<string>(props.editorConfig);
const modelName = ref<string>(props.modelName);
// * COMPUTED
const isUsedWithProps = computed<boolean>(() => attrs.json === undefined);
// * LIFECYCLE
onMounted(() => {
const json = String(attrs.json ?? "{}"),
data = JSON.parse(json);
if (!isUsedWithProps.value) {
(data.id) ? id.value = String(data.id) : "";
name.value = String(data.name);
text.value = String(data.text);
placeholder.value = String(data.placeholder ?? trans.methods.__("Votre texte ici."));
modelName.value = String(data.modelName);
allErrors.value = data.errors;
(data.required) ? required.value = Boolean(data.required) : "";
editorConfig.value = String(data.editorConfig);
}
setTimeout(() => {
customNodes();
}, 100);
// Top Menu
const verticalTopMenuOffset = document.querySelector(
"body > #top-navbar"
) as HTMLElement | null;
if (!verticalTopMenuOffset) {
console.warn("Could not find bo #top-navbar");
}
// * CkEditor Global Parts.
// WordCounter
const ckEditorGlobals = {
wordCount: {
container: wordCounter.value,
// @ts-ignore
onUpdate: (stats) => {
/** Hack to stylize words/caracters counter */
/** Add word counter */
const wordCounterNode = wordCounter.value?.querySelector(".ck-word-count__words");
const spanWords = document.createElement("span");
spanWords.classList.add("fw-bold");
spanWords.innerHTML = stats.words;
wordCounterNode!.textContent = "Mots : ";
wordCounterNode!.appendChild(spanWords);
/** Add character counter */
const characterCounterNode = wordCounter.value?.querySelector(".ck-word-count__characters");
const spanCharacters = document.createElement("span");
spanCharacters.classList.add("fw-bold");
spanCharacters.innerHTML = stats.characters;
characterCounterNode!.textContent = "Caractères : ";
characterCounterNode!.appendChild(spanCharacters);
},
},
ui: verticalTopMenuOffset ? {
viewportOffset: {
top: verticalTopMenuOffset.getBoundingClientRect().height,
right: 0,
bottom: 0,
left: 0,
},
} : undefined,
typing: {
transformations: {
extra: [
{ from: ":)", to: "😊" },
{ from: ":-P", to: "😛" },
{ from: "8-)", to: "😎" },
{ from: ";-)", to: "😉" },
{ from: ":-D", to: "😄" },
{ from: ":twisted:", to: "😈" },
{ from: ":mrgreen:", to: "😁" },
{ from: ":lol:", to: "😆" },
{ from: ":roll:", to: "🙄" },
{ from: ":-?", to: "😕" },
{ from: ":cry:", to: "😢" },
{ from: ":-o", to: "😮" },
{ from: ":evil:", to: "😡" },
{ from: ":-|", to: "😐" },
{ from: ":oops:", to: "😳" },
{ from: ":-x", to: "😠" },
{ from: "8-O", to: "😲" },
{ from: ":-(", to: "😧" },
{ from: ":arrow:", to: "➡" },
{ from: ":idea:", to: "💡" },
],
},
},
link: {
// Automatically add target="_blank" and rel="noopener noreferrer" to all external links.
addTargetToExternalLinks: true,
// Let the users control the "download" attribute of each link.
decorators: {
openInNewTab: {
mode: "manual",
label: "Ouvrir dans un nouvel onglet",
attributes: {
target: "_blank",
}
}
}
}
};
// * Differents CkEditor configs.
const minimalConfig = {
extraPlugins: [],
wordCount: ckEditorGlobals.wordCount,
ui: ckEditorGlobals.ui,
placeholder: placeholder.value,
link: ckEditorGlobals.link,
typing: ckEditorGlobals.typing,
mediaEmbed: {
previewsInData: false,
removeProviders: [
"dailymotion",
"spotify",
"vimeo",
"youtube",
"instagram",
"twitter",
"googleMaps",
"flickr",
"facebook",
],
},
toolbar: {
items: [
"findAndReplace",
"|",
"bold",
"italic",
"underline",
"strikethrough",
"|",
"undo",
"redo",
"selectAll",
"|",
"removeFormat",
],
},
image: {
toolbar: []
},
width: "auto",
language: "fr",
folder: ""
};
const basicConfig = {
extraPlugins: [
CkUploadAdapterPlugin,
],
placeholder: placeholder.value,
wordCount: ckEditorGlobals.wordCount,
ui: ckEditorGlobals.ui,
link: ckEditorGlobals.link,
typing: ckEditorGlobals.typing,
mediaEmbed: {
previewsInData: false,
removeProviders: [
"dailymotion",
"spotify",
"youtube",
"vimeo",
"instagram",
"twitter",
"googleMaps",
"flickr",
"facebook",
],
},
toolbar: {
items: [
"heading",
"|",
"findAndReplace",
"|",
"bold",
"italic",
"underline",
"strikethrough",
"link",
"bulletedList",
"numberedList",
"|",
"outdent",
"indent",
"alignment",
"|",
"undo",
"redo",
"selectAll",
"|",
"imageUpload",
"blockQuote",
"|",
"fontSize",
"horizontalLine",
"|",
"removeFormat",
"|",
"sourceEditing",
],
},
width: "auto",
language: "fr",
folder: modelName.value,
image: {
toolbar: [
"toggleImageCaption",
"imageTextAlternative",
"imageStyle:inline",
"imageStyle:alignLeft",
"imageStyle:alignRight",
"imageStyle:alignCenter",
"imageStyle:side",
"imageResize",
"linkImage",
],
},
};
let config = basicConfig;
switch (editorConfig.value) {
case "minimal":
config = minimalConfig;
break;
case "basic":
config = basicConfig;
break;
default:
console.warn(editorConfig.value + " is not a correct configuration or is not defined. The basic config is loaded.");
}
// Generate Editor.
makeEditorBuild(config);
});
// * METHODS
// @ts-ignore
function makeEditorBuild(config: Config<EditorConfig>): void {
// @ts-ignore
ClassicEditor.create(ckEditor.value, config)
// @ts-ignore
.then((editor: ClassicEditor) => {
let timer = null as number|null;
const updateContent = () => {
const data = editor.getData();
text.value = data;
emits("input", data);
timer = null;
};
// Display all toolbar elements
ckEditor.value = Object.freeze(editor);
editor.setData(text.value);
editor.model.document.on("change:data", () => {
if(timer) {
clearTimeout(timer);
}
// * Keep track of internal editor
timer = setTimeout(updateContent, 300) as never as number;
});
})
// @ts-ignore
.catch((error) => {
console.error("Oops, something went wrong!");
console.error(
"Please, report the following error on https://github.com/ckeditor/ckeditor5/issues with the build id and the error stack trace:"
);
console.warn("Build id: x0tjc6omqq4d-iydt761tuj51");
console.error(error);
});
}
/** Customize input. */
function customNodes(): void {
if (getErrorMessage() !== null) {
const fakeInput = ckEditorComponent.value?.querySelector(".ck.ck-editor__main") as HTMLDivElement|null;
fakeInput!.classList.add("ck-invalid");
}
}
/** Return error message or null. */
function getErrorMessage(): string|null {
return parseLaravelErrorMessages.methods.parseValidationInput(
`${name.value}`,
allErrors.value
);
}
</script>
<style lang="scss" scopped>
@import url("./../../../js/vendor/ckeditor/build/styles.css");
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
.ck-editor-component{
.ck-rounded-corners {
border-radius: 0 0 0.375rem 0.375rem !important;
}
.ck-word-count {
background-color: rgb(var(--bs-secondary-bg-rgb));
border: var(--bs-border-width) solid var(--bs-border-color);
border-bottom: 0px;
border-top-left-radius: 0.375rem;
border-top-right-radius: 0.375rem;
color: var(--bs-body-color);
display: flex;
flex-direction: row;
justify-content: space-around !important;
}
.ck-content {
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.ck-focused {
border-color: var(--bs-primary-border-subtle) !important;
box-shadow: 0 0 0 .25rem rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity)) !important;
outline: 0 !important;
}
.ck-editor__main{
color: $body-color;
}
.ck.ck-editor__main.ck-invalid .ck-blurred, .ck.ck-editor__main.ck-invalid .ck-focused {
border-color: var(--bs-form-invalid-border-color) !important;
}
.ck.ck-editor__main.ck-invalid .ck-focused {
box-shadow: 0 0 0 .25rem rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity)) !important;
}
}
.ck.ck-reset.ck-rounded-corners .ck-toolbar {
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
border: var(--bs-border-width) solid var(--bs-border-color);
}
.ck.ck-powered-by-balloon {
display: none !important;
}
</style>