Fixed ui errors to do with ck-editor

This commit is contained in:
Colin Dawson 2026-02-01 22:43:11 +00:00
parent 49bac1091a
commit 9565b80fb1
5 changed files with 468 additions and 404 deletions

View File

@ -5,7 +5,8 @@
} }
.mailTemplate-editor { .mailTemplate-editor {
width: calc(100% - ($gridGap + $mailtemplateNameListWidth)); //width: calc(100% - ($gridGap + $mailtemplateNameListWidth));
width: 100%;
} }
@media print { @media print {
@ -17,7 +18,7 @@
.ck-main-container { .ck-main-container {
// --ckeditor5-preview-height: 700px; // --ckeditor5-preview-height: 700px;
font-family: "Lato"; font-family: "Lato";
width: fit-content; width: 100%;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@ -35,6 +36,7 @@
font-family: "Lato"; font-family: "Lato";
line-height: 1.6; line-height: 1.6;
word-break: break-word; word-break: break-word;
color: var(--bs-body-color);
.table table { .table table {
overflow: auto; overflow: auto;
@ -42,8 +44,7 @@
} }
.editor-container__editor-wrapper { .editor-container__editor-wrapper {
display: flex; width: 100%;
width: fit-content;
} }
.editor-container_document-editor { .editor-container_document-editor {
@ -96,12 +97,12 @@
//min-width: calc(100vw - ($leftMenuWidth + ($frameWorkAreaPadding * 2))); //min-width: calc(100vw - ($leftMenuWidth + ($frameWorkAreaPadding * 2)));
//max-width: calc(100vw - ($leftMenuWidth + ($frameWorkAreaPadding * 2))); //max-width: calc(100vw - ($leftMenuWidth + ($frameWorkAreaPadding * 2)));
min-width: 100%; width: 100%;
max-width: 100%;
// min-width: calc(210mm + 2px); // min-width: calc(210mm + 2px);
// max-width: calc(210mm + 2px); // max-width: calc(210mm + 2px);
height: fit-content; height: fit-content;
// padding: 20mm 12mm; // padding: 20mm 12mm;
border: 1px hsl(0, 0%, 82.7%) solid; border: 1px hsl(0, 0%, 82.7%) solid;
//background: hsl(0, 0%, 100%); //background: hsl(0, 0%, 100%);

View File

@ -11,6 +11,7 @@ export interface SelectProps {
value: unknown; value: unknown;
options?: Option[]; options?: Option[];
includeBlankFirstEntry?: boolean; includeBlankFirstEntry?: boolean;
multiple?: boolean;
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void; onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
} }
@ -20,6 +21,24 @@ function GenerateValue(value: unknown) {
return value as string | number | readonly string[] | undefined; return value as string | number | readonly string[] | undefined;
} }
function NormalizeSelectValue(value: unknown, isMultiple: boolean) {
const actualValue = GenerateValue(value);
if (!isMultiple) {
return actualValue;
}
if (Array.isArray(actualValue)) {
return actualValue;
}
if (actualValue === undefined || actualValue === null || actualValue === "") {
return [] as readonly string[];
}
return [actualValue] as readonly (string | number)[];
}
export default function Select({ export default function Select({
includeLabel, includeLabel,
name, name,
@ -28,11 +47,13 @@ export default function Select({
value, value,
options, options,
includeBlankFirstEntry, includeBlankFirstEntry,
multiple,
onChange, onChange,
...rest ...rest
}: SelectProps) { }: SelectProps) {
const { t } = useTranslation<typeof Namespaces.Common>(); const { t } = useTranslation<typeof Namespaces.Common>();
const actualValue = GenerateValue(value); const isMultiple = multiple ?? options === undefined;
const actualValue = NormalizeSelectValue(value, isMultiple);
return ( return (
<div className="form-group"> <div className="form-group">
@ -41,7 +62,7 @@ export default function Select({
)} )}
{!options && ( {!options && (
<select <select
multiple multiple={isMultiple}
{...rest} {...rest}
id={name} id={name}
value={actualValue} value={actualValue}
@ -54,6 +75,7 @@ export default function Select({
)} )}
{options && ( {options && (
<select <select
multiple={isMultiple}
{...rest} {...rest}
id={name} id={name}
value={actualValue} value={actualValue}

View File

@ -1,75 +1,75 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from "react";
import { CKEditor } from '@ckeditor/ckeditor5-react'; import { CKEditor } from "@ckeditor/ckeditor5-react";
import Field from './plugins/field/field'; import Field from "./plugins/field/field";
import { import {
DecoupledEditor, DecoupledEditor,
AccessibilityHelp, AccessibilityHelp,
Alignment, Alignment,
Autoformat, Autoformat,
AutoImage, AutoImage,
AutoLink, AutoLink,
Autosave, Autosave,
BalloonToolbar, BalloonToolbar,
Base64UploadAdapter, Base64UploadAdapter,
BlockQuote, BlockQuote,
Bold, Bold,
Code, Code,
CodeBlock, CodeBlock,
Essentials, Essentials,
FindAndReplace, FindAndReplace,
FontBackgroundColor, FontBackgroundColor,
FontColor, FontColor,
FontFamily, FontFamily,
FontSize, FontSize,
Heading, Heading,
Highlight, Highlight,
HorizontalLine, HorizontalLine,
ImageBlock, ImageBlock,
ImageCaption, ImageCaption,
ImageInline, ImageInline,
ImageInsert, ImageInsert,
ImageInsertViaUrl, ImageInsertViaUrl,
ImageResize, ImageResize,
ImageStyle, ImageStyle,
ImageTextAlternative, ImageTextAlternative,
ImageToolbar, ImageToolbar,
ImageUpload, ImageUpload,
Indent, Indent,
IndentBlock, IndentBlock,
Italic, Italic,
Link, Link,
LinkImage, LinkImage,
List, List,
ListProperties, ListProperties,
// Mention, // Mention,
Paragraph, Paragraph,
PasteFromOffice, PasteFromOffice,
RemoveFormat, RemoveFormat,
SelectAll, SelectAll,
SpecialCharacters, SpecialCharacters,
SpecialCharactersArrows, SpecialCharactersArrows,
SpecialCharactersCurrency, SpecialCharactersCurrency,
SpecialCharactersEssentials, SpecialCharactersEssentials,
SpecialCharactersLatin, SpecialCharactersLatin,
SpecialCharactersMathematical, SpecialCharactersMathematical,
SpecialCharactersText, SpecialCharactersText,
Strikethrough, Strikethrough,
Subscript, Subscript,
Superscript, Superscript,
Table, Table,
TableCaption, TableCaption,
TableCellProperties, TableCellProperties,
TableColumnResize, TableColumnResize,
TableProperties, TableProperties,
TableToolbar, TableToolbar,
TextTransformation, TextTransformation,
TodoList, TodoList,
Underline, Underline,
Undo Undo,
} from 'ckeditor5'; } from "ckeditor5";
import 'ckeditor5/ckeditor5.css'; import "ckeditor5/ckeditor5.css";
// export interface TextEditorProps { // export interface TextEditorProps {
// className : String; // className : String;
@ -79,278 +79,319 @@ import 'ckeditor5/ckeditor5.css';
// onChange : ( name : String, data : String ) => void; // onChange : ( name : String, data : String ) => void;
// } // }
export default function TextEditor( props ) { export default function TextEditor(props) {
const editorContainerRef = useRef(null); const editorContainerRef = useRef(null);
const editorMenuBarRef = useRef(null); const editorMenuBarRef = useRef(null);
const editorToolbarRef = useRef(null); const editorToolbarRef = useRef(null);
const editorRef = useRef(null); const editorRef = useRef(null);
const [isLayoutReady, setIsLayoutReady] = useState(false); const [isLayoutReady, setIsLayoutReady] = useState(false);
useEffect(() => { useEffect(() => {
setIsLayoutReady(true); setIsLayoutReady(true);
return () => setIsLayoutReady(false); return () => setIsLayoutReady(false);
}, []); }, []);
let toobarItems = [ let toobarItems = [
'undo', "undo",
'redo', "redo",
'|', "|",
'heading', "heading",
'|', "|",
'fontSize', "fontSize",
'fontFamily', "fontFamily",
'fontColor', "fontColor",
'fontBackgroundColor', "fontBackgroundColor",
'|', "|",
'bold', "bold",
'italic', "italic",
'underline', "underline",
'|', "|",
'link', "link",
'insertImage', "insertImage",
'insertTable', "insertTable",
'highlight', "highlight",
'blockQuote', "blockQuote",
'codeBlock', "codeBlock",
'|', "|",
'alignment', "alignment",
'|', "|",
'bulletedList', "bulletedList",
'numberedList', "numberedList",
'todoList', "todoList",
'outdent', "outdent",
'indent', "indent",
]; ];
let plugins = [ let plugins = [
AccessibilityHelp, AccessibilityHelp,
Alignment, Alignment,
Autoformat, Autoformat,
AutoImage, AutoImage,
AutoLink, AutoLink,
Autosave, Autosave,
BalloonToolbar, BalloonToolbar,
Base64UploadAdapter, Base64UploadAdapter,
BlockQuote, BlockQuote,
Bold, Bold,
Code, Code,
CodeBlock, CodeBlock,
Essentials, Essentials,
FindAndReplace, FindAndReplace,
FontBackgroundColor, FontBackgroundColor,
FontColor, FontColor,
FontFamily, FontFamily,
FontSize, FontSize,
Heading, Heading,
Highlight, Highlight,
HorizontalLine, HorizontalLine,
ImageBlock, ImageBlock,
ImageCaption, ImageCaption,
ImageInline, ImageInline,
ImageInsert, ImageInsert,
ImageInsertViaUrl, ImageInsertViaUrl,
ImageResize, ImageResize,
ImageStyle, ImageStyle,
ImageTextAlternative, ImageTextAlternative,
ImageToolbar, ImageToolbar,
ImageUpload, ImageUpload,
Indent, Indent,
IndentBlock, IndentBlock,
Italic, Italic,
Link, Link,
LinkImage, LinkImage,
List, List,
ListProperties, ListProperties,
// Mention, // Mention,
Paragraph, Paragraph,
PasteFromOffice, PasteFromOffice,
RemoveFormat, RemoveFormat,
SelectAll, SelectAll,
SpecialCharacters, SpecialCharacters,
SpecialCharactersArrows, SpecialCharactersArrows,
SpecialCharactersCurrency, SpecialCharactersCurrency,
SpecialCharactersEssentials, SpecialCharactersEssentials,
SpecialCharactersLatin, SpecialCharactersLatin,
SpecialCharactersMathematical, SpecialCharactersMathematical,
SpecialCharactersText, SpecialCharactersText,
Strikethrough, Strikethrough,
Subscript, Subscript,
Superscript, Superscript,
Table, Table,
TableCaption, TableCaption,
TableCellProperties, TableCellProperties,
TableColumnResize, TableColumnResize,
TableProperties, TableProperties,
TableToolbar, TableToolbar,
TextTransformation, TextTransformation,
TodoList, TodoList,
Underline, Underline,
Undo Undo,
]; ];
let customfields = [] let customfields = [];
if ( props.customFields && props.customFields.length > 0 ) { if (props.customFields && props.customFields.length > 0) {
toobarItems.push( '|' ); toobarItems.push("|");
toobarItems.push( 'Field' ); toobarItems.push("Field");
plugins.push( Field ); plugins.push(Field);
customfields = props.customFields; customfields = props.customFields;
} }
const editorConfig = { const licenseKey = window.__RUNTIME_CONFIG__?.CKEDITOR_LICENSE_KEY || "GPL";
toolbar: {
items: toobarItems,
shouldNotGroupWhenFull: false
},
plugins: plugins,
balloonToolbar: ['bold', 'italic', '|', 'link', 'insertImage', '|', 'bulletedList', 'numberedList'],
fontFamily: {
supportAllValues: true
},
fontSize: {
options: [10, 12, 14, 'default', 18, 20, 22],
supportAllValues: true
},
heading: {
options: [
{
model: 'paragraph',
title: 'Paragraph',
class: 'ck-heading_paragraph'
},
{
model: 'heading1',
view: 'h1',
title: 'Heading 1',
class: 'ck-heading_heading1'
},
{
model: 'heading2',
view: 'h2',
title: 'Heading 2',
class: 'ck-heading_heading2'
},
{
model: 'heading3',
view: 'h3',
title: 'Heading 3',
class: 'ck-heading_heading3'
},
{
model: 'heading4',
view: 'h4',
title: 'Heading 4',
class: 'ck-heading_heading4'
},
{
model: 'heading5',
view: 'h5',
title: 'Heading 5',
class: 'ck-heading_heading5'
},
{
model: 'heading6',
view: 'h6',
title: 'Heading 6',
class: 'ck-heading_heading6'
}
]
},
image: {
toolbar: [
'toggleImageCaption',
'imageTextAlternative',
'|',
'imageStyle:inline',
'imageStyle:wrapText',
'imageStyle:breakText',
'|',
'resizeImage'
]
},
//initialData: props.data,
link: {
addTargetToExternalLinks: true,
defaultProtocol: 'https://',
decorators: {
toggleDownloadable: {
mode: 'manual',
label: 'Downloadable',
attributes: {
download: 'file'
}
}
}
},
list: {
properties: {
styles: true,
startIndex: true,
reversed: true
}
},
// mention: {
// feeds: [
// {
// marker: '@',
// feed: [
// /* See: https://ckeditor.com/docs/ckeditor5/latest/features/mentions.html */
// ]
// }
// ]
// },
menuBar: {
isVisible: true
},
placeholder: 'Type or paste your content here!',
table: {
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties']
},
fieldConfig: {
fields : customfields
}
};
const editorClasses = "editor-container__editor " + props.className; const editorConfig = {
licenseKey: licenseKey,
toolbar: {
items: toobarItems,
shouldNotGroupWhenFull: false,
},
plugins: plugins,
balloonToolbar: [
"bold",
"italic",
"|",
"link",
"insertImage",
"|",
"bulletedList",
"numberedList",
"|",
"field",
],
fontFamily: {
supportAllValues: true,
},
fontSize: {
options: [10, 12, 14, "default", 18, 20, 22],
supportAllValues: true,
},
heading: {
options: [
{
model: "paragraph",
title: "Paragraph",
class: "ck-heading_paragraph",
},
{
model: "heading1",
view: "h1",
title: "Heading 1",
class: "ck-heading_heading1",
},
{
model: "heading2",
view: "h2",
title: "Heading 2",
class: "ck-heading_heading2",
},
{
model: "heading3",
view: "h3",
title: "Heading 3",
class: "ck-heading_heading3",
},
{
model: "heading4",
view: "h4",
title: "Heading 4",
class: "ck-heading_heading4",
},
{
model: "heading5",
view: "h5",
title: "Heading 5",
class: "ck-heading_heading5",
},
{
model: "heading6",
view: "h6",
title: "Heading 6",
class: "ck-heading_heading6",
},
],
},
image: {
toolbar: [
"toggleImageCaption",
"imageTextAlternative",
"|",
"imageStyle:inline",
"imageStyle:wrapText",
"imageStyle:breakText",
"|",
"resizeImage",
],
},
//initialData: props.data,
link: {
addTargetToExternalLinks: true,
defaultProtocol: "https://",
decorators: {
toggleDownloadable: {
mode: "manual",
label: "Downloadable",
attributes: {
download: "file",
},
},
},
},
list: {
properties: {
styles: true,
startIndex: true,
reversed: true,
},
},
// mention: {
// feeds: [
// {
// marker: '@',
// feed: [
// /* See: https://ckeditor.com/docs/ckeditor5/latest/features/mentions.html */
// ]
// }
// ]
// },
menuBar: {
isVisible: true,
},
placeholder: "Type or paste your content here!",
table: {
contentToolbar: [
"tableColumn",
"tableRow",
"mergeTableCells",
"tableProperties",
"tableCellProperties",
],
},
fieldConfig: {
fields: customfields,
},
};
return ( const editorClasses = props.className || "";
<div>
<div className="ck-main-container">
<div className="editor-container editor-container_document-editor" ref={editorContainerRef}>
<div className="editor-container__menu-bar" ref={editorMenuBarRef}></div>
<div className="editor-container__toolbar" ref={editorToolbarRef}></div>
<div className="editor-container__editor-wrapper">
<div className={editorClasses}>
<div ref={editorRef}>
{isLayoutReady && (
<CKEditor
onReady={editor => {
Array.from(editorToolbarRef.current?.children).forEach(child => child.remove());
Array.from(editorMenuBarRef.current?.children).forEach(child => child.remove());
editorToolbarRef.current.appendChild(editor.ui.view.toolbar.element);
editorMenuBarRef.current.appendChild(editor.ui.view.menuBarView.element);
}}
onAfterDestroy={() => {
Array.from(editorToolbarRef.current.children).forEach(child => child.remove());
Array.from(editorMenuBarRef.current.children).forEach(child => child.remove());
}}
editor={DecoupledEditor}
config={editorConfig}
data={props.data}
onChange={ ( event, editor ) => {
const data = editor.getData();
props.onChange( props.name, data ); return (
//console.log( { event, editor, data } ); <div className={editorClasses}>
} } <div className="ck-main-container">
/> <div
)} className="editor-container editor-container_document-editor"
</div> ref={editorContainerRef}
</div> >
</div> <div
</div> className="editor-container__menu-bar"
</div> ref={editorMenuBarRef}
</div> ></div>
); <div
className="editor-container__toolbar"
ref={editorToolbarRef}
></div>
<div className="editor-container__editor-wrapper">
<div className="editor-container__editor">
<div ref={editorRef}>
{isLayoutReady && (
<CKEditor
onReady={(editor) => {
Array.from(editorToolbarRef.current?.children).forEach(
(child) => child.remove(),
);
Array.from(editorMenuBarRef.current?.children).forEach(
(child) => child.remove(),
);
editorToolbarRef.current.appendChild(
editor.ui.view.toolbar.element,
);
editorMenuBarRef.current.appendChild(
editor.ui.view.menuBarView?.element,
);
}}
onAfterDestroy={() => {
Array.from(editorToolbarRef.current.children).forEach(
(child) => child.remove(),
);
Array.from(editorMenuBarRef.current.children).forEach(
(child) => child.remove(),
);
}}
editor={DecoupledEditor}
config={editorConfig}
data={props.data}
onChange={(event, editor) => {
const data = editor.getData();
props.onChange(props.name, data);
//console.log( { event, editor, data } );
}}
/>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
} }

View File

@ -1,9 +1,9 @@
import { Plugin } from 'ckeditor5'; import { Plugin } from "ckeditor5";
import FieldEditing from './fieldediting'; import FieldEditing from "./fieldediting";
import FieldUI from './fieldui'; import FieldUI from "./fieldui";
export default class Field extends Plugin { export default class Field extends Plugin {
static get requires() { static get requires() {
return [ FieldEditing, FieldUI ]; return [FieldEditing, FieldUI];
} }
} }

View File

@ -1,67 +1,67 @@
import { Plugin, Collection, addListToDropdown, createDropdown, ViewModel } from 'ckeditor5'; import {
import './styles.css'; Plugin,
Collection,
addListToDropdown,
createDropdown,
ViewModel,
} from "ckeditor5";
import "./styles.css";
export default class FieldUI extends Plugin { export default class FieldUI extends Plugin {
init() { init() {
const editor = this.editor; const editor = this.editor;
const t = editor.t; const t = editor.t;
const fields = editor.config.get( 'fieldConfig' ).fields; const fields = editor.config.get("fieldConfig").fields;
// The "field" dropdown must be registered among the UI components of the editor // The "field" dropdown must be registered among the UI components of the editor
// to be displayed in the toolbar. // to be displayed in the toolbar.
editor.ui.componentFactory.add( 'field', locale => { editor.ui.componentFactory.add("field", (locale) => {
const dropdownView = createDropdown( locale ); const dropdownView = createDropdown(locale);
// Populate the list in the dropdown with items. // Populate the list in the dropdown with items.
addListToDropdown( dropdownView, getDropdownItemsDefinitions( fields ) ); addListToDropdown(dropdownView, getDropdownItemsDefinitions(fields));
dropdownView.buttonView.set( { dropdownView.buttonView.set({
// The t() function helps localize the editor. All strings enclosed in t() can be // The t() function helps localize the editor. All strings enclosed in t() can be
// translated and change when the language of the editor changes. // translated and change when the language of the editor changes.
label: t( 'Insert Field' ), label: t("Insert Field"),
tooltip: true, tooltip: true,
withText: true withText: true,
} ); });
// Disable the field button when the command is disabled. // Disable the field button when the command is disabled.
const command = editor.commands.get( 'field' ); const command = editor.commands.get("field");
dropdownView.bind( 'isEnabled' ).to( command ); dropdownView.bind("isEnabled").to(command);
// Execute the command when the dropdown item is clicked (executed). // Execute the command when the dropdown item is clicked (executed).
this.listenTo( dropdownView, 'execute', evt => { this.listenTo(dropdownView, "execute", (evt) => {
editor.execute( 'field', { value: evt.source.commandParam } ); editor.execute("field", { value: evt.source.commandParam });
editor.editing.view.focus(); editor.editing.view.focus();
} ); });
return dropdownView; return dropdownView;
} ); });
}
editor.ui.extendMenuBar( {
item: 'field',
position: 'after:bold'
} );
}
} }
function getDropdownItemsDefinitions( fields ) { function getDropdownItemsDefinitions(fields) {
const itemDefinitions = new Collection(); const itemDefinitions = new Collection();
if (fields !== undefined) if (fields !== undefined) {
{ for (const field of fields) {
for ( const field of fields ) { const definition = {
const definition = { type: "button",
type: 'button', model: new ViewModel({
model: new ViewModel( { commandParam: field,
commandParam:field, label: field.name,
label: field.name, withText: true,
withText: true }),
} ) };
};
// Add the item definition to the collection. // Add the item definition to the collection.
itemDefinitions.add( definition ); itemDefinitions.add(definition);
}
} }
}
return itemDefinitions; return itemDefinitions;
} }