432 lines
11 KiB
Vue
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>
|