diff --git a/webapp/cypress/component/TiptapInlineCompatibilityTest.cy.jsx b/webapp/cypress/component/TiptapInlineCompatibilityTest.cy.jsx new file mode 100644 index 000000000..613078b46 --- /dev/null +++ b/webapp/cypress/component/TiptapInlineCompatibilityTest.cy.jsx @@ -0,0 +1,228 @@ +import TiptapInline from "@/components/TiptapInline.vue"; +import { createStore } from "vuex"; + +describe("TiptapInline - TinyMCE HTML Compatibility", () => { + let store; + + beforeEach(() => { + store = createStore({ + state: {}, + mutations: {}, + getters: {}, + }); + }); + + it("renders basic formatting from TinyMCE HTML", () => { + const tinyMCEHtml = `

Bold text and italic text and underlined text and strikethrough

`; + + cy.mount(TiptapInline, { + props: { + modelValue: tinyMCEHtml, + }, + global: { + plugins: [store], + }, + }); + + cy.get(".ProseMirror").should("contain.html", "Bold text"); + cy.get(".ProseMirror").should("contain.html", "italic text"); + cy.get(".ProseMirror").should("contain.html", "underlined text"); + cy.get(".ProseMirror").should("contain.html", "strikethrough"); + }); + + it("renders headings from TinyMCE HTML", () => { + const tinyMCEHtml = `

Heading 1

Heading 2

Heading 3

`; + + cy.mount(TiptapInline, { + props: { + modelValue: tinyMCEHtml, + }, + global: { + plugins: [store], + }, + }); + + cy.get(".ProseMirror h1").should("contain.text", "Heading 1"); + cy.get(".ProseMirror h2").should("contain.text", "Heading 2"); + cy.get(".ProseMirror h3").should("contain.text", "Heading 3"); + }); + + it("renders lists from TinyMCE HTML", () => { + const tinyMCEHtml = `
  1. First
  2. Second
`; + + cy.mount(TiptapInline, { + props: { + modelValue: tinyMCEHtml, + }, + global: { + plugins: [store], + }, + }); + + cy.get(".ProseMirror ul li").should("have.length", 2); + cy.get(".ProseMirror ul li").first().should("contain.text", "Item 1"); + cy.get(".ProseMirror ol li").should("have.length", 2); + cy.get(".ProseMirror ol li").first().should("contain.text", "First"); + }); + + it("renders links from TinyMCE HTML", () => { + const tinyMCEHtml = `

Visit this link

`; + + cy.mount(TiptapInline, { + props: { + modelValue: tinyMCEHtml, + }, + global: { + plugins: [store], + }, + }); + + cy.get(".ProseMirror a") + .should("have.attr", "href", "https://example.com") + .and("have.attr", "target", "_blank") + .and("contain.text", "this link"); + }); + + it("renders images from TinyMCE HTML", () => { + const tinyMCEHtml = `

Test image

`; + + cy.mount(TiptapInline, { + props: { + modelValue: tinyMCEHtml, + }, + global: { + plugins: [store], + }, + }); + + cy.get(".ProseMirror img") + .should("have.attr", "src", "https://example.com/image.png") + .and("have.attr", "alt", "Test image"); + }); + + it("renders tables from TinyMCE HTML", () => { + const tinyMCEHtml = ` + + + + + + + + + + + + + +
Header 1Header 2
Cell 1Cell 2
+ `; + + cy.mount(TiptapInline, { + props: { + modelValue: tinyMCEHtml, + }, + global: { + plugins: [store], + }, + }); + + cy.get(".ProseMirror table").should("exist"); + cy.get(".ProseMirror th").should("have.length", 2); + cy.get(".ProseMirror th").first().should("contain.text", "Header 1"); + cy.get(".ProseMirror td").should("have.length", 2); + cy.get(".ProseMirror td").first().should("contain.text", "Cell 1"); + }); + + it("renders colored text from TinyMCE HTML", () => { + const tinyMCEHtml = `

Red text

`; + + cy.mount(TiptapInline, { + props: { + modelValue: tinyMCEHtml, + }, + global: { + plugins: [store], + }, + }); + + cy.get(".ProseMirror span").should("have.attr", "style").and("include", "color"); + }); + + it("renders horizontal rules from TinyMCE HTML", () => { + const tinyMCEHtml = `

Before


After

`; + + cy.mount(TiptapInline, { + props: { + modelValue: tinyMCEHtml, + }, + global: { + plugins: [store], + }, + }); + + cy.get(".ProseMirror hr").should("exist"); + }); + + it("renders cross-references from datalab HTML", () => { + const datalabHtml = `

See sample for details

`; + + cy.mount(TiptapInline, { + props: { + modelValue: datalabHtml, + }, + global: { + plugins: [store], + }, + }); + + cy.get(".ProseMirror .cross-reference-wrapper").should("exist"); + cy.get(".formatted-item-name").should("contain.text", "sample1"); + }); + + it("renders complex nested formatting from TinyMCE HTML", () => { + const tinyMCEHtml = ` +

Complex Document

+

This is a bold and italic text with a link.

+ + + + + + +
Bold cellBlue cell
+ `; + + cy.mount(TiptapInline, { + props: { + modelValue: tinyMCEHtml, + }, + global: { + plugins: [store], + }, + }); + + cy.get(".ProseMirror h2").should("contain.text", "Complex Document"); + cy.get(".ProseMirror strong").should("exist"); + cy.get(".ProseMirror em").should("exist"); + cy.get(".ProseMirror a").should("have.attr", "href"); + cy.get(".ProseMirror ul li").should("have.length", 2); + cy.get(".ProseMirror table td").should("have.length", 2); + }); + + it("handles empty content gracefully", () => { + cy.mount(TiptapInline, { + props: { + modelValue: "", + }, + global: { + plugins: [store], + }, + }); + + cy.get(".ProseMirror").should("exist"); + }); +}); diff --git a/webapp/cypress/e2e/editPage.cy.js b/webapp/cypress/e2e/editPage.cy.js index 8101d0d67..11752c1d6 100644 --- a/webapp/cypress/e2e/editPage.cy.js +++ b/webapp/cypress/e2e/editPage.cy.js @@ -226,7 +226,12 @@ describe("Edit Page", () => { cy.get(".datablock-content div").eq(0).type("\nThe first comment box; further changes."); cy.contains("Unsaved changes"); - cy.get('[data-testid="block-description"]').eq(0).type("The second comment box"); + cy.get('[data-testid="block-description"]').first().find(".ProseMirror").click(); + + cy.get('[data-testid="block-description"]') + .first() + .find(".ProseMirror") + .type("The second comment box"); cy.contains("Unsaved changes"); cy.get('.datablock-header [aria-label="updateBlock"]').eq(1).click(); cy.wait(500).then(() => { @@ -234,10 +239,13 @@ describe("Edit Page", () => { }); cy.get('.datablock-header [aria-label="updateBlock"]').eq(0).click(); cy.contains("Unsaved changes").should("not.exist"); + cy.get('[data-testid="block-description"]').first().find(".ProseMirror").click(); cy.get('[data-testid="block-description"]') - .eq(0) + .first() + .find(".ProseMirror") .type("\nThe second comment box; further changes"); + cy.findByLabelText("Name").type("name change"); cy.contains("Unsaved changes"); diff --git a/webapp/package.json b/webapp/package.json index 784a50d74..7ed4fdcf5 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -10,9 +10,11 @@ "lint": "vue-cli-service lint" }, "resolutions": { - "cross-spawn": "^7.0.5" + "cross-spawn": "^7.0.5", + "prosemirror-view": "1.41.1" }, "dependencies": { + "@floating-ui/dom": "^1.7.4", "@fortawesome/fontawesome-svg-core": "^1.2.26-2", "@fortawesome/free-brands-svg-icons": "^5.15.4", "@fortawesome/free-regular-svg-icons": "^5.15.2", @@ -20,7 +22,20 @@ "@fortawesome/vue-fontawesome": "^3.0.0-3", "@popperjs/core": "^2.11.8", "@primevue/themes": "^4.0.0", - "@tinymce/tinymce-vue": "^4.0.0", + "@tiptap/core": "^3.6.1", + "@tiptap/extension-color": "^3.6.1", + "@tiptap/extension-highlight": "^3.6.1", + "@tiptap/extension-image": "^3.6.1", + "@tiptap/extension-link": "^3.6.1", + "@tiptap/extension-mathematics": "^3.6.1", + "@tiptap/extension-placeholder": "^3.6.1", + "@tiptap/extension-table": "^3.6.1", + "@tiptap/extension-text-style": "^3.6.1", + "@tiptap/extension-typography": "^3.6.1", + "@tiptap/extension-underline": "^3.6.1", + "@tiptap/pm": "^3.6.1", + "@tiptap/starter-kit": "^3.6.1", + "@tiptap/vue-3": "^3.6.1", "@uppy/core": "^4.4.6", "@uppy/dashboard": "^4.3.4", "@uppy/webcam": "^4.2.0", @@ -36,14 +51,16 @@ "date-fns": "^2.29.3", "highlight.js": "^11.7.0", "js-md5": "^0.8.3", - "markdown-it": "^13.0.1", + "katex": "^0.16.22", + "markdown-it": "^14.1.0", "mermaid": "^11.10.0", "primeicons": "^7.0.0", "primevue": "^4.0.0", "process": "^0.11.10", + "prosemirror-tables": "^1.8.1", "qrcode-vue3": "^1.6.8", "serve": "^14.2.1", - "tinymce": "^5.10.9", + "turndown": "^7.2.1", "vue": "^3.2.4", "vue-qrcode-reader": "^5.5.7", "vue-router": "^4.0.0-0", diff --git a/webapp/public/index.html b/webapp/public/index.html index 527cc131b..db1582dc4 100644 --- a/webapp/public/index.html +++ b/webapp/public/index.html @@ -30,6 +30,14 @@ >
- + + + diff --git a/webapp/src/components/CellInformation.vue b/webapp/src/components/CellInformation.vue index dc09845be..1715dc011 100644 --- a/webapp/src/components/CellInformation.vue +++ b/webapp/src/components/CellInformation.vue @@ -85,10 +85,10 @@
- + >
@@ -106,7 +106,7 @@ + + diff --git a/webapp/src/components/EquipmentInformation.vue b/webapp/src/components/EquipmentInformation.vue index 23afc7c84..90b517209 100644 --- a/webapp/src/components/EquipmentInformation.vue +++ b/webapp/src/components/EquipmentInformation.vue @@ -56,7 +56,7 @@ - + import { createComputedSetterForItemField } from "@/field_utils.js"; -import TinyMceInline from "@/components/TinyMceInline"; +import TiptapInline from "@/components/TiptapInline"; import TableOfContents from "@/components/TableOfContents"; import CollectionList from "@/components/CollectionList"; import FormattedRefcode from "@/components/FormattedRefcode"; @@ -76,7 +76,7 @@ import Creators from "@/components/Creators"; export default { components: { - TinyMceInline, + TiptapInline, CollectionList, TableOfContents, FormattedRefcode, diff --git a/webapp/src/components/MermaidComponent.vue b/webapp/src/components/MermaidComponent.vue new file mode 100644 index 000000000..0a1a6b2ef --- /dev/null +++ b/webapp/src/components/MermaidComponent.vue @@ -0,0 +1,104 @@ + + + diff --git a/webapp/src/components/MermaidModal.vue b/webapp/src/components/MermaidModal.vue new file mode 100644 index 000000000..18e3519d5 --- /dev/null +++ b/webapp/src/components/MermaidModal.vue @@ -0,0 +1,145 @@ + + + diff --git a/webapp/src/components/SampleInformation.vue b/webapp/src/components/SampleInformation.vue index 25f18135c..0a9101a3d 100644 --- a/webapp/src/components/SampleInformation.vue +++ b/webapp/src/components/SampleInformation.vue @@ -43,7 +43,7 @@
- +
@@ -64,7 +64,7 @@ import ChemFormulaInput from "@/components/ChemFormulaInput"; import FormattedRefcode from "@/components/FormattedRefcode"; import ToggleableCollectionFormGroup from "@/components/ToggleableCollectionFormGroup"; import ToggleableCreatorsFormGroup from "@/components/ToggleableCreatorsFormGroup"; -import TinyMceInline from "@/components/TinyMceInline"; +import TiptapInline from "@/components/TiptapInline"; import SynthesisInformation from "@/components/SynthesisInformation"; import TableOfContents from "@/components/TableOfContents"; import ItemRelationshipVisualization from "@/components/ItemRelationshipVisualization"; @@ -72,7 +72,7 @@ import ItemRelationshipVisualization from "@/components/ItemRelationshipVisualiz export default { components: { ChemFormulaInput, - TinyMceInline, + TiptapInline, SynthesisInformation, TableOfContents, ItemRelationshipVisualization, diff --git a/webapp/src/components/StartingMaterialInformation.vue b/webapp/src/components/StartingMaterialInformation.vue index e8a3ca59f..0d1fbda4e 100644 --- a/webapp/src/components/StartingMaterialInformation.vue +++ b/webapp/src/components/StartingMaterialInformation.vue @@ -84,7 +84,7 @@ - + import { createComputedSetterForItemField } from "@/field_utils.js"; -import TinyMceInline from "@/components/TinyMceInline"; +import TiptapInline from "@/components/TiptapInline"; import ChemicalFormula from "@/components/ChemicalFormula"; import ChemFormulaInput from "@/components/ChemFormulaInput"; import TableOfContents from "@/components/TableOfContents"; @@ -118,7 +118,7 @@ export default { ChemicalFormula, ChemFormulaInput, ItemRelationshipVisualization, - TinyMceInline, + TiptapInline, ToggleableCollectionFormGroup, TableOfContents, FormattedRefcode, diff --git a/webapp/src/components/SynthesisInformation.vue b/webapp/src/components/SynthesisInformation.vue index 53625b4d1..7be139757 100644 --- a/webapp/src/components/SynthesisInformation.vue +++ b/webapp/src/components/SynthesisInformation.vue @@ -25,22 +25,22 @@ Procedure - + >
- - diff --git a/webapp/src/components/TiptapInline.vue b/webapp/src/components/TiptapInline.vue new file mode 100644 index 000000000..2fdb05f73 --- /dev/null +++ b/webapp/src/components/TiptapInline.vue @@ -0,0 +1,844 @@ +