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

View File

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

View File

@ -1,75 +1,75 @@
import { useState, useEffect, useRef } from 'react';
import { CKEditor } from '@ckeditor/ckeditor5-react';
import Field from './plugins/field/field';
import { useState, useEffect, useRef } from "react";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import Field from "./plugins/field/field";
import {
DecoupledEditor,
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
} from 'ckeditor5';
DecoupledEditor,
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,
} from "ckeditor5";
import 'ckeditor5/ckeditor5.css';
import "ckeditor5/ckeditor5.css";
// export interface TextEditorProps {
// className : String;
@ -79,278 +79,319 @@ import 'ckeditor5/ckeditor5.css';
// onChange : ( name : String, data : String ) => void;
// }
export default function TextEditor( props ) {
const editorContainerRef = useRef(null);
const editorMenuBarRef = useRef(null);
const editorToolbarRef = useRef(null);
const editorRef = useRef(null);
const [isLayoutReady, setIsLayoutReady] = useState(false);
export default function TextEditor(props) {
const editorContainerRef = useRef(null);
const editorMenuBarRef = useRef(null);
const editorToolbarRef = useRef(null);
const editorRef = useRef(null);
const [isLayoutReady, setIsLayoutReady] = useState(false);
useEffect(() => {
setIsLayoutReady(true);
useEffect(() => {
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 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 = [
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
];
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,
];
let customfields = []
let customfields = [];
if ( props.customFields && props.customFields.length > 0 ) {
toobarItems.push( '|' );
toobarItems.push( 'Field' );
plugins.push( Field );
customfields = props.customFields;
}
if (props.customFields && props.customFields.length > 0) {
toobarItems.push("|");
toobarItems.push("Field");
plugins.push(Field);
customfields = props.customFields;
}
const editorConfig = {
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 licenseKey = window.__RUNTIME_CONFIG__?.CKEDITOR_LICENSE_KEY || "GPL";
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 (
<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();
const editorClasses = props.className || "";
props.onChange( props.name, data );
//console.log( { event, editor, data } );
} }
/>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
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 FieldEditing from './fieldediting';
import FieldUI from './fieldui';
import { Plugin } from "ckeditor5";
import FieldEditing from "./fieldediting";
import FieldUI from "./fieldui";
export default class Field extends Plugin {
static get requires() {
return [ FieldEditing, FieldUI ];
}
static get requires() {
return [FieldEditing, FieldUI];
}
}

View File

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