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 = [
'undo',
'redo',
'|',
'heading',
'|',
'fontSize',
'fontFamily',
'fontColor',
'fontBackgroundColor',
'|',
'bold',
'italic',
'underline',
'|',
'link',
'insertImage',
'insertTable',
'highlight',
'blockQuote',
'codeBlock',
'|',
'alignment',
'|',
'bulletedList',
'numberedList',
'todoList',
'outdent',
'indent',
];
let plugins = [ let toobarItems = [
AccessibilityHelp, "undo",
Alignment, "redo",
Autoformat, "|",
AutoImage, "heading",
AutoLink, "|",
Autosave, "fontSize",
BalloonToolbar, "fontFamily",
Base64UploadAdapter, "fontColor",
BlockQuote, "fontBackgroundColor",
Bold, "|",
Code, "bold",
CodeBlock, "italic",
Essentials, "underline",
FindAndReplace, "|",
FontBackgroundColor, "link",
FontColor, "insertImage",
FontFamily, "insertTable",
FontSize, "highlight",
Heading, "blockQuote",
Highlight, "codeBlock",
HorizontalLine, "|",
ImageBlock, "alignment",
ImageCaption, "|",
ImageInline, "bulletedList",
ImageInsert, "numberedList",
ImageInsertViaUrl, "todoList",
ImageResize, "outdent",
ImageStyle, "indent",
ImageTextAlternative, ];
ImageToolbar,
ImageUpload,
Indent,
IndentBlock,
Italic,
Link,
LinkImage,
List,
ListProperties,
// Mention,
Paragraph,
PasteFromOffice,
RemoveFormat,
SelectAll,
SpecialCharacters,
SpecialCharactersArrows,
SpecialCharactersCurrency,
SpecialCharactersEssentials,
SpecialCharactersLatin,
SpecialCharactersMathematical,
SpecialCharactersText,
Strikethrough,
Subscript,
Superscript,
Table,
TableCaption,
TableCellProperties,
TableColumnResize,
TableProperties,
TableToolbar,
TextTransformation,
TodoList,
Underline,
Undo
];
let customfields = [] let plugins = [
AccessibilityHelp,
Alignment,
Autoformat,
AutoImage,
AutoLink,
Autosave,
BalloonToolbar,
Base64UploadAdapter,
BlockQuote,
Bold,
Code,
CodeBlock,
Essentials,
FindAndReplace,
FontBackgroundColor,
FontColor,
FontFamily,
FontSize,
Heading,
Highlight,
HorizontalLine,
ImageBlock,
ImageCaption,
ImageInline,
ImageInsert,
ImageInsertViaUrl,
ImageResize,
ImageStyle,
ImageTextAlternative,
ImageToolbar,
ImageUpload,
Indent,
IndentBlock,
Italic,
Link,
LinkImage,
List,
ListProperties,
// Mention,
Paragraph,
PasteFromOffice,
RemoveFormat,
SelectAll,
SpecialCharacters,
SpecialCharactersArrows,
SpecialCharactersCurrency,
SpecialCharactersEssentials,
SpecialCharactersLatin,
SpecialCharactersMathematical,
SpecialCharactersText,
Strikethrough,
Subscript,
Superscript,
Table,
TableCaption,
TableCellProperties,
TableColumnResize,
TableProperties,
TableToolbar,
TextTransformation,
TodoList,
Underline,
Undo,
];
if ( props.customFields && props.customFields.length > 0 ) { let customfields = [];
toobarItems.push( '|' );
toobarItems.push( 'Field' );
plugins.push( Field );
customfields = props.customFields;
}
const editorConfig = { if (props.customFields && props.customFields.length > 0) {
toolbar: { toobarItems.push("|");
items: toobarItems, toobarItems.push("Field");
shouldNotGroupWhenFull: false plugins.push(Field);
}, customfields = props.customFields;
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 licenseKey = window.__RUNTIME_CONFIG__?.CKEDITOR_LICENSE_KEY || "GPL";
return ( const editorConfig = {
<div> licenseKey: licenseKey,
<div className="ck-main-container"> toolbar: {
<div className="editor-container editor-container_document-editor" ref={editorContainerRef}> items: toobarItems,
<div className="editor-container__menu-bar" ref={editorMenuBarRef}></div> shouldNotGroupWhenFull: false,
<div className="editor-container__toolbar" ref={editorToolbarRef}></div> },
<div className="editor-container__editor-wrapper"> plugins: plugins,
<div className={editorClasses}> balloonToolbar: [
<div ref={editorRef}> "bold",
{isLayoutReady && ( "italic",
<CKEditor "|",
onReady={editor => { "link",
Array.from(editorToolbarRef.current?.children).forEach(child => child.remove()); "insertImage",
Array.from(editorMenuBarRef.current?.children).forEach(child => child.remove()); "|",
editorToolbarRef.current.appendChild(editor.ui.view.toolbar.element); "bulletedList",
editorMenuBarRef.current.appendChild(editor.ui.view.menuBarView.element); "numberedList",
}} "|",
onAfterDestroy={() => { "field",
Array.from(editorToolbarRef.current.children).forEach(child => child.remove()); ],
Array.from(editorMenuBarRef.current.children).forEach(child => child.remove()); fontFamily: {
}} supportAllValues: true,
editor={DecoupledEditor} },
config={editorConfig} fontSize: {
data={props.data} options: [10, 12, 14, "default", 18, 20, 22],
onChange={ ( event, editor ) => { supportAllValues: true,
const data = editor.getData(); },
heading: {
props.onChange( props.name, data ); options: [
//console.log( { event, editor, data } ); {
} } model: "paragraph",
/> title: "Paragraph",
)} class: "ck-heading_paragraph",
</div> },
</div> {
</div> model: "heading1",
</div> view: "h1",
</div> title: "Heading 1",
</div> 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 = props.className || "";
return (
<div className={editorClasses}>
<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="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;
} }