diff --git a/client/src/components/form-field/CkEditor.jsx b/client/src/components/form-field/CkEditor.jsx index c312cadf80..7db0958fda 100644 --- a/client/src/components/form-field/CkEditor.jsx +++ b/client/src/components/form-field/CkEditor.jsx @@ -19,277 +19,38 @@ // Until we upgrade to CKEditor 5 v6.0.0, it is necessary to // wrap the CKEditor component in a JS (not TS) component -import { ClassicEditor as ClassicEditorBase } from "@ckeditor/ckeditor5-editor-classic"; -import { Essentials } from "@ckeditor/ckeditor5-essentials"; -import { Autoformat } from "@ckeditor/ckeditor5-autoformat"; -import { Markdown } from "@ckeditor/ckeditor5-markdown-gfm"; -import { - Bold, - Italic, - Underline, - Strikethrough, - Code, - // Subscript, - // Superscript, -} from "@ckeditor/ckeditor5-basic-styles"; -import { BlockQuote } from "@ckeditor/ckeditor5-block-quote"; -import { CodeBlock } from "@ckeditor/ckeditor5-code-block"; -import Math from "@isaul32/ckeditor5-math/src/math"; -import AutoformatMath from "@isaul32/ckeditor5-math/src/autoformatmath"; -import { Heading } from "@ckeditor/ckeditor5-heading"; -import { SourceEditing } from "@ckeditor/ckeditor5-source-editing"; -import { WordCount } from "@ckeditor/ckeditor5-word-count"; -import { List, TodoList } from "@ckeditor/ckeditor5-list"; -// import { Alignment } from "@ckeditor/ckeditor5-alignment"; -import { Link, LinkImage } from "@ckeditor/ckeditor5-link"; -import { - ImageBlockEditing, - // ImageInsert, - ImageInsertViaUrl, - // ImageResize, - // ImageToolbar, - // ImageUpload, -} from "@ckeditor/ckeditor5-image"; -// import { Base64UploadAdapter } from "@ckeditor/ckeditor5-upload"; -import { - Table, - TableCaption, - TableCellProperties, - TableColumnResize, - TableProperties, - TableToolbar, -} from "@ckeditor/ckeditor5-table"; -import { HorizontalLine } from "@ckeditor/ckeditor5-horizontal-line"; - import { CKEditor } from "@ckeditor/ckeditor5-react"; - -import "ckeditor5/ckeditor5.css"; -import "./ckEditor.css"; - -import katex from "katex"; -import "katex/dist/katex.min.css"; - -window.katex = katex; - -class RenkuWordCount extends WordCount { - constructor(editor) { - super(editor); - this.isSourceEditingMode = false; - const sourceEditing = editor.plugins.get("SourceEditing"); - sourceEditing.on( - "change:isSourceEditingMode", - (_eventInfo, _name, value) => { - if (value) { - this.isSourceEditingMode = true; - // Source editing textarea is not yet available. - setTimeout(() => { - const sourceEditingTextarea = - editor.editing.view.getDomRoot().nextSibling.firstChild; - if (sourceEditingTextarea) { - sourceEditingTextarea.addEventListener("input", (event) => { - sourceEditing.updateEditorData(); - this.fire("update", { - exact: true, - words: this.words, - characters: () => event.target.value.length, - }); - }); - this.fire("update", { - exact: true, - words: this.words, - characters: () => sourceEditingTextarea.value.length, - }); - } - }); - } else { - this.isSourceEditingMode = false; - this._refreshStats(); - } - } - ); - } - - _refreshStats() { - if (!this.isSourceEditingMode) { - const words = this.words; - const characters = this.characters; - this.fire("update", { - exact: false, - words, - characters, - }); - } - } -} - -class ClassicEditor extends ClassicEditorBase {} - -ClassicEditor.builtinPlugins = [ - Essentials, - Markdown, - Autoformat, - SourceEditing, - Heading, - Bold, - Italic, - Underline, - Strikethrough, - BlockQuote, - BlockQuote, - Code, - CodeBlock, - List, - TodoList, - // Alignment, - // Superscript, - // Subscript, - Link, - LinkImage, - ImageInsertViaUrl, - ImageBlockEditing, - HorizontalLine, - Table, - TableCaption, - TableCellProperties, - TableColumnResize, - TableProperties, - TableToolbar, - // ImageBlock, - // ImageInsert, - // ImageResize, - // ImageToolbar, - // ImageUpload, - // Base64UploadAdapter, - Math, - AutoformatMath, - RenkuWordCount, -]; +import RenkuCKEditor from "@renku/ckeditor5-build-renku"; function CkEditor({ + data, + disabled, id, + invalid, name, - data, - disabled = false, - invalid = false, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - setInputs = (value) => {}, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - wordCount = (stats) => {}, + outputType, + setInputs, }) { - const editorConfig = { - toolbar: { - items: [ - "undo", - "redo", - "|", - "heading", - "|", - "bold", - "italic", - "underline", - "strikethrough", - "blockQuote", - "|", - "code", - "codeBlock", - "|", - { - label: "List", - withText: true, - items: ["bulletedList", "numberedList", "todoList"], - }, - "|", - // "alignment", - // "|", - // "superscript", - // "subscript", - "math", - "|", - "link", - "insertImage", - "|", - "horizontalLine", - "insertTable", - "|", - /*{ - label: "Other", - withText: false, - items: [ - "insertImage", - "resizeImage", - ], - },*/ - "sourceEditing", - ], - }, - math: { - engine: "katex", - enablePreview: true, - }, - wordCount: { - onUpdate: wordCount, - }, - }; return ( { - if (disabled) { - e.ui.view.toolbar.element.style.display = "none"; - } - - e.data.processor._html2markdown._parser.keep("u"); - e.data.processor._html2markdown._parser.addRule("math", { - filter: ["script"], - replacement: function (content) { - return "$" + content.replace(/(?:\\(.))/g, "$1") + "$"; - }, - }); - const latex = { - name: "latex", - level: "inline", - start(src) { - return src.match(/\$/)?.index; - }, - tokenizer(src) { - // Inspired by https://github.com/markedjs/marked/blob/4c5b974b391f913ac923610bd3740ef27ccdae95/src/Tokenizer.js#L647 - const cap = /^(\$+)([^$]|[^$][\s\S]*?[^$])\1(?!\$)/.exec(src); - if (cap) { - let formula = cap[2].replace(/\n/g, " "); - const hasNonSpaceChars = /[^ ]/.test(formula); - const hasSpaceCharsOnBothEnds = - /^ /.test(formula) && / $/.test(formula); - if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) { - formula = formula.substring(1, formula.length - 1); - } - return { - type: "latex", - raw: cap[0], - text: formula, - }; - } - }, - renderer({ text }) { - return ``; - }, + onChange={(_event, editor) => { + const artificialEvent = { + target: { name: name, value: editor.getData() }, + isPersistent: () => false, }; - e.data.processor._markdown2html._parser.use({ extensions: [latex] }); - e.setData(data); - - e.model.document.on("change:data", () => { - const artificialEvent = { - target: { name: name, value: e.getData() }, - isPersistent: () => false, - }; - setInputs(artificialEvent); - }); + setInputs(artificialEvent); }} /> ); diff --git a/client/src/components/form-field/TextAreaInput.tsx b/client/src/components/form-field/TextAreaInput.tsx index 5ab9af7a70..d53d34244b 100644 --- a/client/src/components/form-field/TextAreaInput.tsx +++ b/client/src/components/form-field/TextAreaInput.tsx @@ -17,36 +17,87 @@ */ // TODO: Upgrade to ckeditor5 v6.0.0 to get TS support -import cx from "classnames"; import React from "react"; +import { Controller } from "react-hook-form"; import type { Control, FieldError, FieldValues, + Path, UseFormRegisterReturn, } from "react-hook-form"; -import { FormGroup, FormText } from "reactstrap"; +import { Input, FormGroup, FormText, Label } from "reactstrap"; import FormLabel from "./FormLabel"; import { ErrorLabel } from "../formlabels/FormLabels"; import LazyCkEditor from "./LazyCkEditor"; -function MarkdownInput(props: TextAreaInputProps) { - const setInputs = async (value: { - target: { name: string; value: unknown }; - }) => { - await props.register.onChange(value); +type EditMarkdownSwitchProps = { + codeView: boolean; + setCodeView: React.Dispatch>; +}; + +function EditMarkdownSwitch(props: EditMarkdownSwitchProps) { + const outputType = "markdown"; + const switchLabel = outputType === "markdown" ? "Raw Markdown" : "Raw HTML"; + return ( +
+ { + props.setCodeView(!props.codeView); + }} + /> + +
+ ); +} + +type MarkdownInputProps = TextAreaInputProps & + Omit; + +function MarkdownInput(props: MarkdownInputProps) { + const setInputs = (value: { target: { name: string; value: unknown } }) => { + props.register.onChange(value); }; + const outputType = "markdown"; + const value = props.getValue(); + if (props.codeView) { + // User wants to input markdown directly + return ( + } + render={({ field }) => ( + + )} + /> + ); + } + // User wants to rich-text input return ( ); } @@ -56,42 +107,28 @@ interface TextAreaInputProps { error?: FieldError; getValue: () => string; help?: string | React.ReactNode; - label?: string; + label: string; name: string; register: UseFormRegisterReturn; required?: boolean; - wordCount?: (stats: { - exact: boolean; - characters: number; - words: number; - }) => void; } function TextAreaInput(props: TextAreaInputProps) { + const [codeView, setCodeView] = React.useState(false); + return (
- {props.label ? ( - - ) : ( -
- )} + +
-
- +
+
{props.help && {props.help}} {props.error && (