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,6 +1,6 @@
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,
@ -42,7 +42,7 @@ import {
LinkImage, LinkImage,
List, List,
ListProperties, ListProperties,
// Mention, // Mention,
Paragraph, Paragraph,
PasteFromOffice, PasteFromOffice,
RemoveFormat, RemoveFormat,
@ -66,10 +66,10 @@ import {
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,7 +79,7 @@ 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);
@ -93,34 +93,34 @@ export default function TextEditor( props ) {
}, []); }, []);
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 = [
@ -162,7 +162,7 @@ export default function TextEditor( props ) {
LinkImage, LinkImage,
List, List,
ListProperties, ListProperties,
// Mention, // Mention,
Paragraph, Paragraph,
PasteFromOffice, PasteFromOffice,
RemoveFormat, RemoveFormat,
@ -186,109 +186,123 @@ export default function TextEditor( props ) {
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 licenseKey = window.__RUNTIME_CONFIG__?.CKEDITOR_LICENSE_KEY || "GPL";
const editorConfig = { const editorConfig = {
licenseKey: licenseKey,
toolbar: { toolbar: {
items: toobarItems, items: toobarItems,
shouldNotGroupWhenFull: false shouldNotGroupWhenFull: false,
}, },
plugins: plugins, plugins: plugins,
balloonToolbar: ['bold', 'italic', '|', 'link', 'insertImage', '|', 'bulletedList', 'numberedList'], balloonToolbar: [
"bold",
"italic",
"|",
"link",
"insertImage",
"|",
"bulletedList",
"numberedList",
"|",
"field",
],
fontFamily: { fontFamily: {
supportAllValues: true supportAllValues: true,
}, },
fontSize: { fontSize: {
options: [10, 12, 14, 'default', 18, 20, 22], options: [10, 12, 14, "default", 18, 20, 22],
supportAllValues: true supportAllValues: true,
}, },
heading: { heading: {
options: [ options: [
{ {
model: 'paragraph', model: "paragraph",
title: 'Paragraph', title: "Paragraph",
class: 'ck-heading_paragraph' class: "ck-heading_paragraph",
}, },
{ {
model: 'heading1', model: "heading1",
view: 'h1', view: "h1",
title: 'Heading 1', title: "Heading 1",
class: 'ck-heading_heading1' class: "ck-heading_heading1",
}, },
{ {
model: 'heading2', model: "heading2",
view: 'h2', view: "h2",
title: 'Heading 2', title: "Heading 2",
class: 'ck-heading_heading2' class: "ck-heading_heading2",
}, },
{ {
model: 'heading3', model: "heading3",
view: 'h3', view: "h3",
title: 'Heading 3', title: "Heading 3",
class: 'ck-heading_heading3' class: "ck-heading_heading3",
}, },
{ {
model: 'heading4', model: "heading4",
view: 'h4', view: "h4",
title: 'Heading 4', title: "Heading 4",
class: 'ck-heading_heading4' class: "ck-heading_heading4",
}, },
{ {
model: 'heading5', model: "heading5",
view: 'h5', view: "h5",
title: 'Heading 5', title: "Heading 5",
class: 'ck-heading_heading5' class: "ck-heading_heading5",
}, },
{ {
model: 'heading6', model: "heading6",
view: 'h6', view: "h6",
title: 'Heading 6', title: "Heading 6",
class: 'ck-heading_heading6' class: "ck-heading_heading6",
} },
] ],
}, },
image: { image: {
toolbar: [ toolbar: [
'toggleImageCaption', "toggleImageCaption",
'imageTextAlternative', "imageTextAlternative",
'|', "|",
'imageStyle:inline', "imageStyle:inline",
'imageStyle:wrapText', "imageStyle:wrapText",
'imageStyle:breakText', "imageStyle:breakText",
'|', "|",
'resizeImage' "resizeImage",
] ],
}, },
//initialData: props.data, //initialData: props.data,
link: { link: {
addTargetToExternalLinks: true, addTargetToExternalLinks: true,
defaultProtocol: 'https://', defaultProtocol: "https://",
decorators: { decorators: {
toggleDownloadable: { toggleDownloadable: {
mode: 'manual', mode: "manual",
label: 'Downloadable', label: "Downloadable",
attributes: { attributes: {
download: 'file' download: "file",
} },
} },
} },
}, },
list: { list: {
properties: { properties: {
styles: true, styles: true,
startIndex: true, startIndex: true,
reversed: true reversed: true,
} },
}, },
// mention: { // mention: {
// feeds: [ // feeds: [
@ -301,49 +315,76 @@ export default function TextEditor( props ) {
// ] // ]
// }, // },
menuBar: { menuBar: {
isVisible: true isVisible: true,
}, },
placeholder: 'Type or paste your content here!', placeholder: "Type or paste your content here!",
table: { table: {
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties'] contentToolbar: [
"tableColumn",
"tableRow",
"mergeTableCells",
"tableProperties",
"tableCellProperties",
],
}, },
fieldConfig: { fieldConfig: {
fields : customfields fields: customfields,
} },
}; };
const editorClasses = "editor-container__editor " + props.className; const editorClasses = props.className || "";
return ( 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 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}> <div ref={editorRef}>
{isLayoutReady && ( {isLayoutReady && (
<CKEditor <CKEditor
onReady={editor => { onReady={(editor) => {
Array.from(editorToolbarRef.current?.children).forEach(child => child.remove()); Array.from(editorToolbarRef.current?.children).forEach(
Array.from(editorMenuBarRef.current?.children).forEach(child => child.remove()); (child) => child.remove(),
editorToolbarRef.current.appendChild(editor.ui.view.toolbar.element); );
editorMenuBarRef.current.appendChild(editor.ui.view.menuBarView.element); 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={() => { onAfterDestroy={() => {
Array.from(editorToolbarRef.current.children).forEach(child => child.remove()); Array.from(editorToolbarRef.current.children).forEach(
Array.from(editorMenuBarRef.current.children).forEach(child => child.remove()); (child) => child.remove(),
);
Array.from(editorMenuBarRef.current.children).forEach(
(child) => child.remove(),
);
}} }}
editor={DecoupledEditor} editor={DecoupledEditor}
config={editorConfig} config={editorConfig}
data={props.data} data={props.data}
onChange={ ( event, editor ) => { onChange={(event, editor) => {
const data = editor.getData(); const data = editor.getData();
props.onChange( props.name, data ); props.onChange(props.name, data);
//console.log( { event, editor, data } ); //console.log( { event, editor, data } );
} } }}
/> />
)} )}
</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,65 +1,65 @@
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);
} }
} }