Merge branch '321-text-editor-fallback' into 'master'

Text editor: Add a toggle button to switch to simple text editor and add emoji and fullscreen to ckeditor toolbar

Closes #321

See merge request Chill-Projet/chill-bundles!827
This commit is contained in:
Julien Fastré 2025-05-27 09:16:31 +00:00
commit 992e6d29d0
15 changed files with 200 additions and 92 deletions

View File

@ -0,0 +1,6 @@
kind: Feature
body: Add Emoji and Fullscreen feature to ckeditor configuration
time: 2025-05-23T13:33:41.645095128+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@ -0,0 +1,6 @@
kind: Feature
body: Create editor which allow us to toggle between rich and simple text editor
time: 2025-05-23T13:34:34.56795603+02:00
custom:
Issue: "321"
SchemaChange: No schema change

View File

@ -20,7 +20,7 @@
"bindings": "^1.5.0",
"bootstrap": "5.2.3",
"chokidar": "^3.5.1",
"ckeditor5": "^44.1.0",
"ckeditor5": "^45.1.0",
"dompurify": "^3.1.0",
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",

View File

@ -41,9 +41,7 @@ const i18nGendoc = _createI18n({});
document.querySelectorAll("div[data-docgen-template-picker]").forEach((el) => {
fetchTemplates(el.dataset.entityClass).then((templates) => {
const picker = {
template:
'<pick-template :templates="this.templates" :preventDefaultMoveToGenerate="true" ' +
':entityClass="faked" @go-to-generate-document="generateDoc"></pick-template>',
template: `<pick-template :templates="this.templates" :preventDefaultMoveToGenerate="true" @go-to-generate-document="generateDoc"></pick-template>`,
components: {
PickTemplate,
},
@ -54,7 +52,7 @@ document.querySelectorAll("div[data-docgen-template-picker]").forEach((el) => {
};
},
methods: {
generateDoc({ event, link, template }) {
generateDoc({ link, template }) {
console.log("generateDoc");
console.log("link", link);
console.log("template", template);

View File

@ -8,6 +8,9 @@ import {
Heading,
Link,
List,
Emoji,
Mention,
Fullscreen,
} from "ckeditor5";
import coreTranslations from "ckeditor5/translations/fr.js";
@ -26,6 +29,11 @@ export default {
Link,
List,
Paragraph,
// both Emoji and Mention are required for Emoji feature
Emoji,
Mention,
// to enable fullscreen
Fullscreen,
],
toolbar: {
items: [
@ -37,8 +45,13 @@ export default {
"bulletedList",
"numberedList",
"blockQuote",
"|",
"emoji",
"|",
"undo",
"redo",
"|",
"fullscreen",
],
},
translations: [coreTranslations],

View File

@ -1,12 +1,32 @@
import config from "./editor_config";
import { ClassicEditor } from "ckeditor5";
import { createApp } from "vue";
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
const ckeditorFields: NodeListOf<HTMLTextAreaElement> =
document.querySelectorAll("textarea[ckeditor]");
ckeditorFields.forEach((field: HTMLTextAreaElement): void => {
ClassicEditor.create(field, config).catch((error) => {
console.error(error.stack);
throw error;
const content = field.value;
const div = document.createElement("div");
if (field.parentNode !== null) {
field.parentNode.insertBefore(div, field);
} else {
throw "parent is null";
}
createApp({
components: { CommentEditor },
template: `<comment-editor v-model="content" @input="handleInput"></comment-editor>`,
data() {
return {
content,
};
},
methods: {
handleInput() {
field.value = this.content;
},
},
}).mount(div);
field.style.display = "none";
});
});
//Fields.push.apply(Fields, document.querySelectorAll('.cf-fields textarea'));

View File

@ -0,0 +1,121 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from "vue";
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import { ClassicEditor } from "ckeditor5";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import {
trans,
EDITOR_SWITCH_TO_SIMPLE,
EDITOR_SWITCH_TO_COMPLEX,
} from "translator";
const EDITOR_MODE_KEY = "editorMode";
const kind = ref<"simple" | "rich">("simple");
const value = defineModel({ required: true });
const isSimple = computed(() => kind.value === "simple");
const toggleButtonClass = computed(() => {
return {
["toggle-button"]: true,
onEditor: !isSimple.value,
onSimple: isSimple.value,
};
});
const toggleEditor = () => {
console.log("toggleEditor");
let newValue;
newValue = kind.value === "simple" ? "rich" : "simple";
kind.value = "rich";
window.localStorage.setItem(EDITOR_MODE_KEY, newValue);
window.dispatchEvent(new Event("toggleEditorKind"));
console.log("new storage", window.localStorage.getItem(EDITOR_MODE_KEY));
};
const onKindChange = function (/* event: StorageEvent | Event */) {
const newValue = window.localStorage.getItem(EDITOR_MODE_KEY);
if (null === newValue || !(newValue === "rich" || newValue === "simple")) {
throw "invalid new value: " + newValue;
}
if (kind.value !== newValue) {
kind.value = newValue;
}
};
onMounted(function () {
const storage = window.localStorage;
const savedKind = storage.getItem(EDITOR_MODE_KEY);
if (
null !== kind.value &&
(savedKind === "simple" || savedKind === "rich")
) {
kind.value = savedKind;
}
window.addEventListener("storage", onKindChange);
window.addEventListener("toggleEditorKind", onKindChange);
});
onUnmounted(function () {
window.removeEventListener("storage", onKindChange);
window.removeEventListener("toggleEditorKind", onKindChange);
});
</script>
<template>
<div>
<div v-if="'rich' === kind">
<Ckeditor
:editor="ClassicEditor"
:config="classicEditorConfig"
v-model="value"
/>
</div>
<div v-else>
<textarea
v-model="value"
class="form-control simple-editor"
></textarea>
</div>
<button :class="toggleButtonClass" type="button" @click="toggleEditor">
{{
isSimple
? trans(EDITOR_SWITCH_TO_COMPLEX)
: trans(EDITOR_SWITCH_TO_SIMPLE)
}}
</button>
</div>
</template>
<style scoped lang="scss">
.toggle-button {
background: white;
border: none;
padding: 0.15rem;
color: #069;
cursor: pointer;
&.onEditor {
position: relative;
left: 1rem;
top: -1.5rem;
}
&.onSimple {
position: relative;
top: -0.75rem;
left: 1rem;
}
}
.simple-editor {
min-height: 190px;
}
</style>

View File

@ -915,3 +915,7 @@ multiselect:
select_group_label: Appuyer sur "Entrée" pour sélectionner ce groupe
deselect_group_label: Appuyer sur "Entrée" pour désélectionner ce groupe
selected_label: Sélectionné'
editor:
switch_to_simple: Éditeur simple
switch_to_complex: Éditeur riche

View File

@ -13,14 +13,7 @@
$t("comment.label")
}}</label>
<ckeditor
name="content"
:placeholder="$t('comment.content')"
:editor="classicEditor"
:config="editorConfig"
v-model="content"
tag-name="textarea"
/>
<comment-editor v-model="content" />
<div class="sub-comment">
<div
@ -61,15 +54,13 @@
</template>
<script>
import { ClassicEditor } from "ckeditor5";
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
import { mapState } from "vuex";
export default {
name: "Comment",
components: {
ckeditor: Ckeditor,
CommentEditor,
},
data() {
return {

View File

@ -18,14 +18,7 @@
</h3>
</template>
<template #body>
<ckeditor
name="content"
:placeholder="$t('comment_placeholder')"
:editor="editor"
:config="editorConfig"
v-model="content"
tag-name="textarea"
/>
<comment-editor v-model="content" />
</template>
<template #footer>
<a class="btn btn-save" @click="saveAction">
@ -39,15 +32,13 @@
<script>
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import { ClassicEditor } from "ckeditor5";
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
export default {
name: "WriteComment",
components: {
Modal,
ckeditor: Ckeditor,
CommentEditor,
},
props: ["resource"],
emits: ["updateComment"],

View File

@ -34,22 +34,12 @@
<div id="privateComment" class="action-row">
<label class="col-form-label">{{ $t("private_comment") }}</label>
<ckeditor
v-model="privateComment"
:editor="classicEditor"
:config="editorConfig"
tag-name="textarea"
></ckeditor>
<comment-editor v-model="privateComment"></comment-editor>
</div>
<div id="comment" class="action-row">
<label class="col-form-label">{{ $t("comments") }}</label>
<ckeditor
v-model="note"
:editor="classicEditor"
:config="editorConfig"
tag-name="textarea"
></ckeditor>
<comment-editor v-model="note"></comment-editor>
</div>
<div id="objectives" class="action-row">
@ -461,9 +451,7 @@
<script>
import { mapState, mapGetters } from "vuex";
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import { ClassicEditor } from "ckeditor5";
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
import AddResult from "./components/AddResult.vue";
import AddEvaluation from "./components/AddEvaluation.vue";
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
@ -529,7 +517,7 @@ const i18n = {
export default {
name: "App",
components: {
ckeditor: Ckeditor,
CommentEditor,
AddResult,
AddEvaluation,
AddPersons,

View File

@ -88,13 +88,7 @@
$t("evaluation_public_comment")
}}</label>
<div class="col-sm-12">
<ckeditor
:editor="classicEditor"
:config="editorConfig"
:placeholder="$t('evaluation_comment_placeholder')"
v-model="comment"
tag-name="textarea"
></ckeditor>
<comment-editor v-model:value="comment"></comment-editor>
</div>
</div>
@ -273,9 +267,6 @@
<script>
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import { ClassicEditor } from "ckeditor5";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import { mapState } from "vuex";
import PickTemplate from "ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue";
import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator";
@ -284,6 +275,7 @@ import { buildLinkCreate } from "ChillMainAssets/lib/entity-workflow/api";
import { buildLinkCreate as buildLinkCreateNotification } from "ChillMainAssets/lib/entity-notification/api";
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
import DropFileModal from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileModal.vue";
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
const i18n = {
messages: {
@ -322,8 +314,8 @@ export default {
name: "FormEvaluation",
props: ["evaluation", "docAnchorId"],
components: {
CommentEditor,
DropFileModal,
ckeditor: Ckeditor,
PickTemplate,
ListWorkflowModal,
DocumentActionButtonsGroup,
@ -371,8 +363,6 @@ export default {
},
computed: {
...mapState(["isPosting", "work", "me"]),
classicEditor: () => ClassicEditor,
editorConfig: () => classicEditorConfig,
AmIRefferer() {
return !(
this.$store.state.work.accompanyingPeriod.user &&

View File

@ -29,12 +29,7 @@
</div>
<div class="item-row comment">
<ckeditor
:editor="classicEditor"
:config="editorConfig"
v-model="comment"
tag-name="textarea"
/>
<comment-editor v-model="comment" />
</div>
<div class="item-row participation-details">
@ -103,15 +98,13 @@ div.participation-details {
<script>
import { mapGetters } from "vuex";
import PersonRenderBox from "ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue";
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import { ClassicEditor } from "ckeditor5";
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
export default {
name: "MemberDetails",
components: {
PersonRenderBox,
ckeditor: Ckeditor,
CommentEditor,
},
props: ["conc"],
computed: {

View File

@ -1,30 +1,17 @@
<template>
<ckeditor
name="content"
:placeholder="
$t('household_members_editor.positioning.comment_placeholder')
"
:editor="editor"
:config="editorConfig"
v-model="content"
tag-name="textarea"
/>
<comment-editor v-model="content" />
</template>
<script>
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import { ClassicEditor } from "ckeditor5";
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
export default {
name: "PersonComment.vue",
components: {
ckeditor: Ckeditor,
CommentEditor,
},
props: ["conc"],
computed: {
editor: () => ClassicEditor,
editorConfig: () => classicEditorConfig,
content: {
get() {
return this.$props.conc.comment || "";

View File

@ -8,7 +8,7 @@ L'usager {{ oldPersonLocation|chill_entity_render_string }} a déménagé.
Son adresse était utilisée pour localiser le parcours n°{{ period.id }}, dont vous êtes
le référent.
En conséquence de ce déménage, le parcours est toujours localisé à cette adresse, mais à l'aide d'une
En conséquence de ce déménagement, le parcours est toujours localisé à cette adresse, mais à l'aide d'une
adresse temporaire.
Si vous continuez à suivre le parcours, vous pouvez le localiser à nouveau auprès de l'adresse de