From 60237a28370ab57d1bdca9acdbce2fcb4cb6609b Mon Sep 17 00:00:00 2001 From: Jonathon Herbert Date: Thu, 19 Jun 2025 10:24:10 +0100 Subject: [PATCH 1/4] Move demo rendering to React, pt. 1 --- demo/index.html | 2 +- demo/{index.ts => index.tsx} | 250 ++++++++++++++++++++--------------- webpack.config.js | 2 +- 3 files changed, 145 insertions(+), 109 deletions(-) rename demo/{index.ts => index.tsx} (76%) diff --git a/demo/index.html b/demo/index.html index 2853ae3b..c06d3052 100644 --- a/demo/index.html +++ b/demo/index.html @@ -11,7 +11,7 @@ -
+
diff --git a/demo/index.ts b/demo/index.tsx similarity index 76% rename from demo/index.ts rename to demo/index.tsx index 69aaadb6..6c02dd2b 100644 --- a/demo/index.ts +++ b/demo/index.tsx @@ -1,3 +1,5 @@ +import React, { useEffect, useRef, useState } from "react"; +import { render } from "react-dom"; import { FocusStyleManager } from "@guardian/src-foundations/utils"; import { UserTelemetryEventSender } from "@guardian/user-telemetry-client"; import omit from "lodash/omit"; @@ -80,6 +82,7 @@ import { sampleVine, } from "./sampleElements"; import type { WindowType } from "./types"; +import { Option, Select } from "@guardian/src-select"; // Enable collaboration and serialisation. Disabling can be useful when measuring performance improvements. const enableExpensiveFeatures = true; @@ -266,11 +269,7 @@ const schema = new Schema({ const { serializer, parser } = createParsers(schema); -const editorsContainer = document.querySelector("#editor-container"); const btnContainer = document.getElementById("button-container"); -if (!editorsContainer || !btnContainer) { - throw new Error("No #editor element present in DOM"); -} const get = () => { const state = window.localStorage.getItem("pm"); @@ -290,13 +289,13 @@ let editorNo = 0; let firstCollabPlugin: ReturnType | undefined; let firstEditor: EditorView | undefined; -const createEditor = (server: CollabServer) => { +const createEditor = (editorContainer: HTMLElement) => { // Add the editor nodes to the DOM const isFirstEditor = !firstEditor; const editorElement = document.createElement("div"); editorElement.id = `editor-${editorNo}`; editorElement.classList.add("Editor"); - editorsContainer.appendChild(editorElement); + editorContainer.appendChild(editorElement); const contentElement = document.getElementById(`content-${editorNo}`); if (contentElement?.parentElement) { @@ -380,71 +379,6 @@ const createEditor = (server: CollabServer) => { firstEditor = view; } - const createElementButton = ( - buttonText: string, - elementName: keyof typeof elements, - values: Record - ) => { - const elementButton = document.createElement("button"); - elementButton.innerHTML = `Add ${buttonText}`; - elementButton.id = elementName; - elementButton.addEventListener("click", () => { - insertElement({ elementName, values })(view.state, view.dispatch); - }); - btnContainer.appendChild(elementButton); - }; - - const buttonData = [ - { - label: "Campaign Callout List", - name: campaignCalloutListElementName, - values: sampleCampaignCalloutList, - }, - { label: "Embed", name: embedElementName, values: sampleEmbed }, - { label: "Callout", name: embedElementName, values: sampleCallout }, - { label: "Demo image", name: demoImageElementName, values: sampleImage }, - { label: "Rich-link", name: richlinkElementName, values: sampleRichLink }, - { label: "Video", name: videoElementName, values: sampleVideo }, - { label: "Audio", name: audioElementName, values: sampleAudio }, - { label: "Map", name: mapElementName, values: sampleMap }, - { label: "Document", name: documentElementName, values: sampleDocument }, - { label: "Table", name: tableElementName, values: sampleTable }, - { - label: "Membership", - name: membershipElementName, - values: sampleMembership, - }, - { - label: "Interactive", - name: interactiveElementName, - values: sampleInteractive, - }, - { label: "Pullquote", name: pullquoteElementName, values: samplePullquote }, - { label: "Code", name: codeElementName, values: sampleCode }, - { label: "Form", name: formElementName, values: sampleForm }, - { label: "Vine", name: vineElementName, values: sampleVine }, - { label: "Tweet", name: tweetElementName, values: sampleTweet }, - { label: "Recipe", name: recipeElementName, values: sampleRecipe }, - { label: "Recipe atom", name: contentAtomName, values: sampleContentAtom }, - { - label: "Interactive atom", - name: contentAtomName, - values: sampleInteractiveAtom, - }, - { label: "Comment", name: commentElementName, values: sampleComment }, - { - label: "Alt Style", - name: altStyleElementName, - values: sampleAltStylesElement, - }, - { label: "Repeater", name: repeaterElementName, values: sampleRepeater }, - { label: "Nested", name: nestedElementName, values: sampleNested }, - ] as const; - - buttonData.map(({ label, name, values }) => - createElementButton(label, name, values) - ); - const imageElementButton = document.createElement("button"); imageElementButton.innerHTML = "Add Image"; imageElementButton.id = imageElementName; @@ -471,7 +405,6 @@ const createEditor = (server: CollabServer) => { }; onCropImage(setMedia); }); - btnContainer.appendChild(imageElementButton); const cartoonElementButton = document.createElement("button"); cartoonElementButton.innerHTML = "Add Cartoon"; @@ -497,7 +430,6 @@ const createEditor = (server: CollabServer) => { }; onCropImage(setMedia); }); - btnContainer.appendChild(cartoonElementButton); // Add a button allowing you to toggle the image role fields const toggleImageFields = document.createElement("button"); @@ -507,7 +439,6 @@ const createEditor = (server: CollabServer) => { [...additionalRoleOptions].splice(Math.floor(Math.random() * 3), 2) ); }); - btnContainer.appendChild(toggleImageFields); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- for dev use if (enableExpensiveFeatures) { @@ -530,20 +461,146 @@ const createEditor = (server: CollabServer) => { }; const server = new CollabServer(); -firstEditor = createEditor(server); -const doc = firstEditor.state.doc; -// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- for dev use -if (enableExpensiveFeatures) { - server.init(doc); -} +const buttonData = [ + { + label: "Campaign Callout List", + name: campaignCalloutListElementName, + values: sampleCampaignCalloutList, + }, + { label: "Embed", name: embedElementName, values: sampleEmbed }, + { label: "Callout", name: embedElementName, values: sampleCallout }, + { label: "Demo image", name: demoImageElementName, values: sampleImage }, + { label: "Rich-link", name: richlinkElementName, values: sampleRichLink }, + { label: "Video", name: videoElementName, values: sampleVideo }, + { label: "Audio", name: audioElementName, values: sampleAudio }, + { label: "Map", name: mapElementName, values: sampleMap }, + { label: "Document", name: documentElementName, values: sampleDocument }, + { label: "Table", name: tableElementName, values: sampleTable }, + { + label: "Membership", + name: membershipElementName, + values: sampleMembership, + }, + { + label: "Interactive", + name: interactiveElementName, + values: sampleInteractive, + }, + { label: "Pullquote", name: pullquoteElementName, values: samplePullquote }, + { label: "Code", name: codeElementName, values: sampleCode }, + { label: "Form", name: formElementName, values: sampleForm }, + { label: "Vine", name: vineElementName, values: sampleVine }, + { label: "Tweet", name: tweetElementName, values: sampleTweet }, + { label: "Recipe", name: recipeElementName, values: sampleRecipe }, + { label: "Recipe atom", name: contentAtomName, values: sampleContentAtom }, + { + label: "Interactive atom", + name: contentAtomName, + values: sampleInteractiveAtom, + }, + { label: "Comment", name: commentElementName, values: sampleComment }, + { + label: "Alt Style", + name: altStyleElementName, + values: sampleAltStylesElement, + }, + { label: "Repeater", name: repeaterElementName, values: sampleRepeater }, + { label: "Nested", name: nestedElementName, values: sampleNested }, +] as const; + +const App = () => { + const editorContainerRef = useRef(null); + // const createElementButton = ( + // buttonText: string, + // elementName: keyof typeof elements, + // values: Record + // ) => { + // const elementButton = document.createElement("button"); + // elementButton.innerHTML = `Add ${buttonText}`; + // elementButton.id = elementName; + // elementButton.addEventListener("click", () => { + // + // }); + // btnContainer.appendChild(elementButton); + // }; + useEffect(() => { + if (!editorContainerRef.current) { + return; + } + firstEditor = createEditor(editorContainerRef.current); + const doc = firstEditor.state.doc; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- for dev use + if (enableExpensiveFeatures) { + server.init(doc); + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- for dev use + if (enableExpensiveFeatures) { + applyDevTools(firstEditor); + } + + window.PM_ELEMENTS = { + view: firstEditor, + insertElement: insertElement, + docToHtml: () => + firstEditor ? docToHtml(serializer, firstEditor.state.doc) : "", + htmlToDoc: (html: string) => { + const node = htmlToDoc(parser, html); + firstEditor?.updateState( + EditorState.create({ + doc: node, + plugins: firstEditor.state.plugins, + }) + ); + }, + }; + }, [editorContainerRef]); + + const [elementType, setElementType] = useState(buttonData[0].label); + + const addElement = () => { + const { values, name } = buttonData.find((e) => e.label === elementType)!; + + if (!firstEditor) { + return; + } + + insertElement({ elementName: name as any, values: values as any })( + firstEditor.state, + firstEditor.dispatch + ); + }; + + return ( +
+ App +
+ + +
+
+ +
+
+
+ ); +}; + +const appContainer = document.getElementById("app"); -// Add more editors -const addEditorButton = document.createElement("button"); -addEditorButton.innerHTML = "Add another editor"; -addEditorButton.id = "add-editor"; -addEditorButton.addEventListener("click", () => createEditor(server)); -btnContainer.appendChild(addEditorButton); +render(, appContainer); // Handy debugging tools. We assign a few things to window for our integration tests, // and to facilitate debugging. @@ -552,24 +609,3 @@ declare global { // eslint-disable-next-line @typescript-eslint/no-empty-interface -- necessary to extend the Window object interface Window extends WindowType {} } - -// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- for dev use -if (enableExpensiveFeatures) { - applyDevTools(firstEditor); -} - -window.PM_ELEMENTS = { - view: firstEditor, - insertElement: insertElement, - docToHtml: () => - firstEditor ? docToHtml(serializer, firstEditor.state.doc) : "", - htmlToDoc: (html: string) => { - const node = htmlToDoc(parser, html); - firstEditor?.updateState( - EditorState.create({ - doc: node, - plugins: firstEditor.state.plugins, - }) - ); - }, -}; diff --git a/webpack.config.js b/webpack.config.js index 711c400f..b4b64053 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -21,7 +21,7 @@ export default { path: path.resolve(dirName, "dist"), }, devtool: "inline-source-map", - entry: "./demo/index.ts", + entry: "./demo/index.tsx", mode: "development", devServer: { static: { From 1de7389bef941959d9496651b5ac32251e91917d Mon Sep 17 00:00:00 2001 From: Jonathon Herbert Date: Thu, 19 Jun 2025 11:30:20 +0100 Subject: [PATCH 2/4] Layout changes; heading typeface; menu styling; correct scroll behaviour for horizontally stacked editors; editor border now matches collab caret color --- demo/collab/SelectionPlugin.ts | 2 +- demo/index.tsx | 229 +++++++++++++++++++-------------- demo/style.css | 144 +++++++++++++-------- 3 files changed, 226 insertions(+), 149 deletions(-) diff --git a/demo/collab/SelectionPlugin.ts b/demo/collab/SelectionPlugin.ts index 20203f37..156ca23f 100644 --- a/demo/collab/SelectionPlugin.ts +++ b/demo/collab/SelectionPlugin.ts @@ -220,7 +220,7 @@ const notEmpty = ( return value !== null && value !== undefined; }; -const selectColor = (index: number, isBackground = false) => { +export const selectColor = (index: number, isBackground = false) => { const hue = index * 137.508; // Use golden angle approximation return `hsl(${hue},50%,${isBackground ? 90 : 50}%)`; }; diff --git a/demo/index.tsx b/demo/index.tsx index 6c02dd2b..7ab30859 100644 --- a/demo/index.tsx +++ b/demo/index.tsx @@ -1,6 +1,9 @@ -import React, { useEffect, useRef, useState } from "react"; -import { render } from "react-dom"; +import { css } from "@emotion/react"; +import { Button } from "@guardian/src-button"; +import { headline } from "@guardian/src-foundations/typography"; import { FocusStyleManager } from "@guardian/src-foundations/utils"; +import { Column, Columns, Stack } from "@guardian/src-layout"; +import { Option, Select } from "@guardian/src-select"; import { UserTelemetryEventSender } from "@guardian/user-telemetry-client"; import omit from "lodash/omit"; import { collab } from "prosemirror-collab"; @@ -11,6 +14,8 @@ import { Schema } from "prosemirror-model"; import { schema as basicSchema, marks } from "prosemirror-schema-basic"; import { EditorState } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; +import React, { useEffect, useRef, useState } from "react"; +import { render } from "react-dom"; import { buildElementPlugin, undefinedDropdownValue } from "../src"; import { keyTakeawaysElement } from "../src/elements/alt-style/AltStyleElementForm"; import { createCalloutElement } from "../src/elements/callout/Callout"; @@ -45,7 +50,10 @@ import { testWidgetDecorationPlugin, } from "../src/plugin/helpers/test"; import { CollabServer, EditorConnection } from "./collab/CollabServer"; -import { createSelectionCollabPlugin } from "./collab/SelectionPlugin"; +import { + createSelectionCollabPlugin, + selectColor, +} from "./collab/SelectionPlugin"; import { getImageFromMediaPayload, onCropCartoon, @@ -82,7 +90,6 @@ import { sampleVine, } from "./sampleElements"; import type { WindowType } from "./types"; -import { Option, Select } from "@guardian/src-select"; // Enable collaboration and serialisation. Disabling can be useful when measuring performance improvements. const enableExpensiveFeatures = true; @@ -304,6 +311,7 @@ const createEditor = (editorContainer: HTMLElement) => { // Create the editor const clientID = editorNo.toString(); + editorElement.style.border = `2px solid ${selectColor(parseInt(clientID))}`; const currentVersion = firstEditor && firstCollabPlugin ? (firstCollabPlugin.getState(firstEditor.state) as number) @@ -379,58 +387,6 @@ const createEditor = (editorContainer: HTMLElement) => { firstEditor = view; } - const imageElementButton = document.createElement("button"); - imageElementButton.innerHTML = "Add Image"; - imageElementButton.id = imageElementName; - imageElementButton.addEventListener("click", () => { - const setMedia = (mediaPayload: MediaPayload) => { - const { - mediaId, - mediaApiUri, - assets, - suppliersReference, - caption, - photographer, - source, - } = mediaPayload; - insertElement({ - elementName: imageElementName, - values: { - caption, - photographer, - source, - mainImage: { assets, suppliersReference, mediaId, mediaApiUri }, - }, - })(view.state, view.dispatch); - }; - onCropImage(setMedia); - }); - - const cartoonElementButton = document.createElement("button"); - cartoonElementButton.innerHTML = "Add Cartoon"; - cartoonElementButton.id = cartoonElementName; - cartoonElementButton.addEventListener("click", () => { - const setMedia = (mediaPayload: MediaPayload) => { - const { photographer, caption, source } = mediaPayload; - - const imageToInsert = getImageFromMediaPayload(mediaPayload); - - // TODO: handle this error - if (!imageToInsert) return; - - insertElement({ - elementName: cartoonElementName, - values: { - largeImages: [imageToInsert], - photographer, - source, - caption, - }, - })(view.state, view.dispatch); - }; - onCropImage(setMedia); - }); - // Add a button allowing you to toggle the image role fields const toggleImageFields = document.createElement("button"); toggleImageFields.innerHTML = "Randomise image role options"; @@ -462,7 +418,12 @@ const createEditor = (editorContainer: HTMLElement) => { const server = new CollabServer(); -const buttonData = [ +const buttonData: Array<{ + label: string; + name: string; + values?: any; + callback?: (view: EditorView) => void; +}> = [ { label: "Campaign Callout List", name: campaignCalloutListElementName, @@ -471,6 +432,58 @@ const buttonData = [ { label: "Embed", name: embedElementName, values: sampleEmbed }, { label: "Callout", name: embedElementName, values: sampleCallout }, { label: "Demo image", name: demoImageElementName, values: sampleImage }, + { + label: "Image", + name: imageElementName, + callback: (view: EditorView) => { + const setMedia = (mediaPayload: MediaPayload) => { + const { + mediaId, + mediaApiUri, + assets, + suppliersReference, + caption, + photographer, + source, + } = mediaPayload; + insertElement({ + elementName: imageElementName, + values: { + caption, + photographer, + source, + mainImage: { assets, suppliersReference, mediaId, mediaApiUri }, + }, + })(view.state, view.dispatch); + }; + onCropImage(setMedia); + }, + }, + { + label: "Cartoon", + name: cartoonElementName, + callback: (view: EditorView) => { + const setMedia = (mediaPayload: MediaPayload) => { + const { photographer, caption, source } = mediaPayload; + + const imageToInsert = getImageFromMediaPayload(mediaPayload); + + // TODO: handle this error + if (!imageToInsert) return; + + insertElement({ + elementName: cartoonElementName, + values: { + largeImages: [imageToInsert], + photographer, + source, + caption, + }, + })(view.state, view.dispatch); + }; + onCropImage(setMedia); + }, + }, { label: "Rich-link", name: richlinkElementName, values: sampleRichLink }, { label: "Video", name: videoElementName, values: sampleVideo }, { label: "Audio", name: audioElementName, values: sampleAudio }, @@ -511,19 +524,7 @@ const buttonData = [ const App = () => { const editorContainerRef = useRef(null); - // const createElementButton = ( - // buttonText: string, - // elementName: keyof typeof elements, - // values: Record - // ) => { - // const elementButton = document.createElement("button"); - // elementButton.innerHTML = `Add ${buttonText}`; - // elementButton.id = elementName; - // elementButton.addEventListener("click", () => { - // - // }); - // btnContainer.appendChild(elementButton); - // }; + useEffect(() => { if (!editorContainerRef.current) { return; @@ -561,40 +562,78 @@ const App = () => { const [elementType, setElementType] = useState(buttonData[0].label); const addElement = () => { - const { values, name } = buttonData.find((e) => e.label === elementType)!; + const { values, callback, name } = buttonData.find( + (e) => e.label === elementType + )!; if (!firstEditor) { return; } - insertElement({ elementName: name as any, values: values as any })( - firstEditor.state, - firstEditor.dispatch - ); + if (values) { + insertElement({ elementName: name as any, values: values as any })( + firstEditor.state, + firstEditor.dispatch + ); + } + + if (callback) { + callback(firstEditor); + } }; return ( -
- App -
- setElementType(e.target.value)} + > + {buttonData.map(({ label }) => ( + + ))} + + + + + + - {buttonData.map(({ label }) => ( - - ))} - - -
-
- -
-
-
+
+ + + ); }; diff --git a/demo/style.css b/demo/style.css index e640e670..cbf79679 100644 --- a/demo/style.css +++ b/demo/style.css @@ -1,9 +1,10 @@ html, body { - font-family: 'TE31 Text Egyptian'; + font-family: "TE31 Text Egyptian"; width: 100%; height: 100%; margin: 0; + overflow: hidden; } html * { @@ -17,7 +18,7 @@ h4, h5, h6 { font-weight: 400; - font-family: 'DE5 Display Egyptian SemiBold' + font-family: "DE5 Display Egyptian SemiBold"; } .TestDecoration { @@ -27,8 +28,8 @@ h6 { @font-face { font-family: "Guardian Agate Sans"; src: url("fonts/GuardianAgateSans1Web-Regular.woff2") format("woff2"), - url("fonts/GuardianAgateSans1Web-Regular.woff") format("woff"), - url("fonts/GuardianAgateSans1Web-Regular.ttf") format("truetype"); + url("fonts/GuardianAgateSans1Web-Regular.woff") format("woff"), + url("fonts/GuardianAgateSans1Web-Regular.ttf") format("truetype"); font-weight: 400; font-style: normal; font-stretch: normal; @@ -64,6 +65,25 @@ h6 { font-stretch: normal; } +#app { + margin: 0 10px 10px 10px; + height: 100%; + width: 100%; + overflow-y: scroll; + overflow-x: hidden; +} + +.ProseMirror-menubar { + font-family: "GuardianTextSans", "Guardian Text Sans Web", "Helvetica Neue", + "Helvetica", "Arial", "Lucida Grande", sans-serif; + display: flex; +} + +.ProseMirror-menuitem { + display: flex; + align-items: center; +} + .modal { position: fixed; left: 0; @@ -97,7 +117,7 @@ h6 { .modal__dismiss { float: right; color: #333333; - background-color: #DCDCDC; + background-color: #dcdcdc; } #content-container { @@ -113,7 +133,8 @@ h6 { min-height: 0; } -#editor-container, #content-data { +#editor-container, +#content-data { margin: 10px; overflow-y: scroll; } @@ -178,9 +199,15 @@ h6 { position: relative; } -.ProseMirror-hideselection *::selection { background: transparent; } -.ProseMirror-hideselection *::-moz-selection { background: transparent; } -.ProseMirror-hideselection { caret-color: transparent; } +.ProseMirror-hideselection *::selection { + background: transparent; +} +.ProseMirror-hideselection *::-moz-selection { + background: transparent; +} +.ProseMirror-hideselection { + caret-color: transparent; +} .ProseMirror-selectednode { outline: 2px solid #8cf; @@ -196,7 +223,9 @@ li.ProseMirror-selectednode:after { content: ""; position: absolute; left: -32px; - right: -2px; top: -2px; bottom: -2px; + right: -2px; + top: -2px; + bottom: -2px; border: 2px solid #8cf; pointer-events: none; } @@ -223,17 +252,13 @@ img.ProseMirror-separator { white-space: pre; } -.ProseMirror-menuitem { - margin-right: 3px; - display: inline-block; -} - .ProseMirror-menuseparator { border-right: 1px solid #ddd; margin-right: 3px; } -.ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu { +.ProseMirror-menu-dropdown, +.ProseMirror-menu-dropdown-menu { font-size: 90%; white-space: nowrap; } @@ -256,13 +281,14 @@ img.ProseMirror-separator { border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 4px solid currentColor; - opacity: .6; + opacity: 0.6; position: absolute; right: 4px; top: calc(50% - 2px); } -.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu { +.ProseMirror-menu-dropdown-menu, +.ProseMirror-menu-submenu { position: absolute; background: white; color: #666; @@ -294,7 +320,7 @@ img.ProseMirror-separator { border-top: 4px solid transparent; border-bottom: 4px solid transparent; border-left: 4px solid currentColor; - opacity: .6; + opacity: 0.6; position: absolute; right: 4px; top: calc(50% - 4px); @@ -313,10 +339,11 @@ img.ProseMirror-separator { } .ProseMirror-menu-disabled { - opacity: .3; + opacity: 0.3; } -.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu { +.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, +.ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu { display: block; } @@ -327,7 +354,9 @@ img.ProseMirror-separator { min-height: 1em; color: #666; padding: 1px 6px; - top: 0; left: 0; right: 0; + top: 0; + left: 0; + right: 0; border-bottom: 1px solid silver; background: white; z-index: 10; @@ -338,7 +367,7 @@ img.ProseMirror-separator { .ProseMirror-icon { display: inline-block; - line-height: .8; + line-height: 0.8; vertical-align: -2px; /* Compensate for padding */ padding: 2px 8px; cursor: pointer; @@ -397,14 +426,16 @@ img.ProseMirror-separator { line-height: 2px; } -.ProseMirror ul, .ProseMirror ol { +.ProseMirror ul, +.ProseMirror ol { padding-left: 30px; } .ProseMirror blockquote { padding-left: 1em; border-left: 3px solid #eee; - margin-left: 0; margin-right: 0; + margin-left: 0; + margin-right: 0; } .ProseMirror-example-setup-style img { @@ -418,7 +449,7 @@ img.ProseMirror-separator { position: fixed; border-radius: 3px; z-index: 11; - box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2); + box-shadow: -0.5px 2px 5px rgba(0, 0, 0, 0.2); } .ProseMirror-prompt h5 { @@ -441,9 +472,12 @@ img.ProseMirror-separator { .ProseMirror-prompt-close { position: absolute; - left: 2px; top: 1px; + left: 2px; + top: 1px; color: #666; - border: none; background: transparent; padding: 0; + border: none; + background: transparent; + padding: 0; } .ProseMirror-prompt-close:after { @@ -464,7 +498,8 @@ img.ProseMirror-separator { margin-top: 5px; display: none; } -#editor, .editor { +#editor, +.editor { background: white; color: black; background-clip: padding-box; @@ -490,7 +525,9 @@ img.ProseMirror-separator { outline: none; } -.ProseMirror p { margin-bottom: 1em } +.ProseMirror p { + margin-bottom: 1em; +} .ProseMirror-menubar-wrapper { width: 100%; @@ -517,21 +554,15 @@ img.ProseMirror-separator { color: white; } - /* Editor styles */ - -.ProseMirror-menuitem { - margin-right: 3px; - display: inline-block; -} - .ProseMirror-menuseparator { border-right: 1px solid #ddd; margin-right: 3px; } -.ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu { +.ProseMirror-menu-dropdown, +.ProseMirror-menu-dropdown-menu { font-size: 90%; white-space: nowrap; } @@ -554,13 +585,14 @@ img.ProseMirror-separator { border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 4px solid currentColor; - opacity: .6; + opacity: 0.6; position: absolute; right: 4px; top: calc(50% - 2px); } -.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu { +.ProseMirror-menu-dropdown-menu, +.ProseMirror-menu-submenu { position: absolute; background: white; color: #666; @@ -592,7 +624,7 @@ img.ProseMirror-separator { border-top: 4px solid transparent; border-bottom: 4px solid transparent; border-left: 4px solid currentColor; - opacity: .6; + opacity: 0.6; position: absolute; right: 4px; top: calc(50% - 4px); @@ -611,10 +643,11 @@ img.ProseMirror-separator { } .ProseMirror-menu-disabled { - opacity: .3; + opacity: 0.3; } -.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu { +.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, +.ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu { display: block; } @@ -625,7 +658,9 @@ img.ProseMirror-separator { min-height: 1em; color: #666; padding: 1px 6px; - top: 0; left: 0; right: 0; + top: 0; + left: 0; + right: 0; border-bottom: 1px solid silver; background: white; z-index: 10; @@ -636,7 +671,7 @@ img.ProseMirror-separator { .ProseMirror-icon { display: inline-block; - line-height: .8; + line-height: 0.8; vertical-align: -2px; /* Compensate for padding */ padding: 2px 8px; cursor: pointer; @@ -703,7 +738,7 @@ img.ProseMirror-separator { position: fixed; border-radius: 3px; z-index: 11; - box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2); + box-shadow: -0.5px 2px 5px rgba(0, 0, 0, 0.2); } .ProseMirror-prompt h5 { @@ -726,9 +761,12 @@ img.ProseMirror-separator { .ProseMirror-prompt-close { position: absolute; - left: 2px; top: 1px; + left: 2px; + top: 1px; color: #666; - border: none; background: transparent; padding: 0; + border: none; + background: transparent; + padding: 0; } .ProseMirror-prompt-close:after { @@ -766,9 +804,9 @@ img.ProseMirror-separator { /* Match Composer styling */ .table { - background: #F6F6F6; + background: #f6f6f6; border-collapse: collapse; - border-top: 2px solid #00ADEE; + border-top: 2px solid #00adee; width: 100%; } .table th { @@ -778,13 +816,13 @@ img.ProseMirror-separator { vertical-align: top; } .table tbody td { - border-top: 1px solid #EDEDED; + border-top: 1px solid #ededed; padding: 7px; } .table .table-row--highlight { - background: #F6F6F6; + background: #f6f6f6; font-weight: bold; } .table .table-row--divider td { - border-top: 1px solid #2B2B29; + border-top: 1px solid #2b2b29; } From 785acc98c9b352db3bda9f26274c075690573226 Mon Sep 17 00:00:00 2001 From: Jonathon Herbert Date: Thu, 19 Jun 2025 11:34:16 +0100 Subject: [PATCH 3/4] Remove some type errors and lint warnings --- demo/index.tsx | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/demo/index.tsx b/demo/index.tsx index 7ab30859..5522bb5b 100644 --- a/demo/index.tsx +++ b/demo/index.tsx @@ -7,7 +7,7 @@ import { Option, Select } from "@guardian/src-select"; import { UserTelemetryEventSender } from "@guardian/user-telemetry-client"; import omit from "lodash/omit"; import { collab } from "prosemirror-collab"; -import applyDevTools from "prosemirror-dev-tools"; +import prosemirrorDevTools from "prosemirror-dev-tools"; import { exampleSetup } from "prosemirror-example-setup"; import type { MarkSpec, Node } from "prosemirror-model"; import { Schema } from "prosemirror-model"; @@ -276,8 +276,6 @@ const schema = new Schema({ const { serializer, parser } = createParsers(schema); -const btnContainer = document.getElementById("button-container"); - const get = () => { const state = window.localStorage.getItem("pm"); return state @@ -421,7 +419,7 @@ const server = new CollabServer(); const buttonData: Array<{ label: string; name: string; - values?: any; + values?: Record; callback?: (view: EditorView) => void; }> = [ { @@ -539,7 +537,7 @@ const App = () => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- for dev use if (enableExpensiveFeatures) { - applyDevTools(firstEditor); + prosemirrorDevTools(firstEditor); } window.PM_ELEMENTS = { @@ -562,16 +560,18 @@ const App = () => { const [elementType, setElementType] = useState(buttonData[0].label); const addElement = () => { - const { values, callback, name } = buttonData.find( - (e) => e.label === elementType - )!; + const elementData = buttonData.find((e) => e.label === elementType); + if (!elementData) { + return; + } + const { values, callback, name: elementName } = elementData; if (!firstEditor) { return; } if (values) { - insertElement({ elementName: name as any, values: values as any })( + insertElement({ elementName, values } as any)( firstEditor.state, firstEditor.dispatch ); @@ -611,7 +611,10 @@ const App = () => { From 2eca4d2f558e9c9465572673e8edde2b1c924612 Mon Sep 17 00:00:00 2001 From: Jonathon Herbert Date: Thu, 26 Jun 2025 13:13:33 +0100 Subject: [PATCH 4/4] Correct client ID colors and typeface for selection carets --- demo/collab/SelectionPlugin.ts | 18 ++++-------------- demo/index.tsx | 4 ++-- demo/style.css | 1 + 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/demo/collab/SelectionPlugin.ts b/demo/collab/SelectionPlugin.ts index 156ca23f..713d07f4 100644 --- a/demo/collab/SelectionPlugin.ts +++ b/demo/collab/SelectionPlugin.ts @@ -132,14 +132,11 @@ const getStateForNewUserSelection = ( ) ); - const newClientIDs = oldState.clientIDs.add(selectionChange.clientID); - if (selectionChange.selection) { const decorations = getDecosForSelection( selectionChange.userName, selectionChange.clientID, - selectionChange.selection, - newClientIDs + selectionChange.selection ); newDecSet = newDecSet.add(doc, decorations); } @@ -148,25 +145,18 @@ const getStateForNewUserSelection = ( ...oldState, decorations: newDecSet, selections: newSels, - clientIDs: newClientIDs, }; }; const getDecosForSelection = ( userName: string, clientID: ClientID, - { head, from, to, empty }: Selection, - clientIDs: Set + { head, from, to, empty }: Selection ): Decoration[] => { - const clientIDIndex = Array.from(clientIDs).indexOf(clientID); - if (clientIDIndex === -1) { - return []; - } - - const cursorColor = selectColor(clientIDIndex); + const cursorColor = selectColor(parseInt(clientID)); const cursorDeco = getCursorDeco(head, clientID, userName, cursorColor); - const selectionColor = selectColor(clientIDIndex, true); + const selectionColor = selectColor(parseInt(clientID), true); const selectionDeco = empty ? undefined : getSelectionDeco(from, to, clientID, selectionColor); diff --git a/demo/index.tsx b/demo/index.tsx index 5522bb5b..04e68f4e 100644 --- a/demo/index.tsx +++ b/demo/index.tsx @@ -13,7 +13,7 @@ import type { MarkSpec, Node } from "prosemirror-model"; import { Schema } from "prosemirror-model"; import { schema as basicSchema, marks } from "prosemirror-schema-basic"; import { EditorState } from "prosemirror-state"; -import { EditorView } from "prosemirror-view"; +import { DecorationSet, EditorView } from "prosemirror-view"; import React, { useEffect, useRef, useState } from "react"; import { render } from "react-dom"; import { buildElementPlugin, undefinedDropdownValue } from "../src"; @@ -518,7 +518,7 @@ const buttonData: Array<{ }, { label: "Repeater", name: repeaterElementName, values: sampleRepeater }, { label: "Nested", name: nestedElementName, values: sampleNested }, -] as const; +]; const App = () => { const editorContainerRef = useRef(null); diff --git a/demo/style.css b/demo/style.css index cbf79679..35e1cc28 100644 --- a/demo/style.css +++ b/demo/style.css @@ -552,6 +552,7 @@ img.ProseMirror-separator { font-weight: bold; white-space: nowrap; color: white; + font-family: "Guardian Agate Sans"; } /* Editor styles */