diff --git a/.yarn/cache/@emotion-is-prop-valid-npm-1.2.0-332d343e3d-cc7a19850a.zip b/.yarn/cache/@emotion-is-prop-valid-npm-1.2.0-332d343e3d-cc7a19850a.zip deleted file mode 100644 index db9df9ffa4..0000000000 Binary files a/.yarn/cache/@emotion-is-prop-valid-npm-1.2.0-332d343e3d-cc7a19850a.zip and /dev/null differ diff --git a/.yarn/cache/@emotion-memoize-npm-0.8.0-c5dd451828-c87bb110b8.zip b/.yarn/cache/@emotion-memoize-npm-0.8.0-c5dd451828-c87bb110b8.zip deleted file mode 100644 index 8561995f4b..0000000000 Binary files a/.yarn/cache/@emotion-memoize-npm-0.8.0-c5dd451828-c87bb110b8.zip and /dev/null differ diff --git a/.yarn/cache/@parcel-watcher-linux-x64-glibc-npm-2.2.0-b05c8da99d-8.zip b/.yarn/cache/@parcel-watcher-linux-x64-glibc-npm-2.2.0-b05c8da99d-8.zip new file mode 100644 index 0000000000..90c26df8c9 Binary files /dev/null and b/.yarn/cache/@parcel-watcher-linux-x64-glibc-npm-2.2.0-b05c8da99d-8.zip differ diff --git a/.yarn/cache/@types-styled-components-npm-5.1.26-aabda06611-84f53b3101.zip b/.yarn/cache/@types-styled-components-npm-5.1.26-aabda06611-84f53b3101.zip deleted file mode 100644 index 8b3291cb7c..0000000000 Binary files a/.yarn/cache/@types-styled-components-npm-5.1.26-aabda06611-84f53b3101.zip and /dev/null differ diff --git a/.yarn/cache/babel-plugin-styled-components-npm-2.0.2-0e7aa5c426-3de729e909.zip b/.yarn/cache/babel-plugin-styled-components-npm-2.0.2-0e7aa5c426-3de729e909.zip deleted file mode 100644 index 0ad2b90fe0..0000000000 Binary files a/.yarn/cache/babel-plugin-styled-components-npm-2.0.2-0e7aa5c426-3de729e909.zip and /dev/null differ diff --git a/.yarn/cache/camelize-npm-1.0.0-5eda108776-769f8d1007.zip b/.yarn/cache/camelize-npm-1.0.0-5eda108776-769f8d1007.zip deleted file mode 100644 index d58e6e1b76..0000000000 Binary files a/.yarn/cache/camelize-npm-1.0.0-5eda108776-769f8d1007.zip and /dev/null differ diff --git a/.yarn/cache/css-color-keywords-npm-1.0.0-fc176df58b-8f125e3ad4.zip b/.yarn/cache/css-color-keywords-npm-1.0.0-fc176df58b-8f125e3ad4.zip deleted file mode 100644 index 9886779c81..0000000000 Binary files a/.yarn/cache/css-color-keywords-npm-1.0.0-fc176df58b-8f125e3ad4.zip and /dev/null differ diff --git a/.yarn/cache/css-to-react-native-npm-3.0.0-ab07d67d74-98a2e9d4fb.zip b/.yarn/cache/css-to-react-native-npm-3.0.0-ab07d67d74-98a2e9d4fb.zip deleted file mode 100644 index c43fc88c1e..0000000000 Binary files a/.yarn/cache/css-to-react-native-npm-3.0.0-ab07d67d74-98a2e9d4fb.zip and /dev/null differ diff --git a/.yarn/cache/shallowequal-npm-1.1.0-6688d419cb-f4c1de0837.zip b/.yarn/cache/shallowequal-npm-1.1.0-6688d419cb-f4c1de0837.zip deleted file mode 100644 index 18e17f43b9..0000000000 Binary files a/.yarn/cache/shallowequal-npm-1.1.0-6688d419cb-f4c1de0837.zip and /dev/null differ diff --git a/.yarn/cache/styled-components-npm-5.3.6-934fe4f344-68eac1e451.zip b/.yarn/cache/styled-components-npm-5.3.6-934fe4f344-68eac1e451.zip deleted file mode 100644 index c2d113f4a1..0000000000 Binary files a/.yarn/cache/styled-components-npm-5.3.6-934fe4f344-68eac1e451.zip and /dev/null differ diff --git a/next.config.js b/next.config.js index bc8f3f887b..c8846cae56 100644 --- a/next.config.js +++ b/next.config.js @@ -41,8 +41,7 @@ module.exports = withBundleAnalyzer({ defaultLocale: 'de', localeDetection: false, }, - // TODO: reactStrictMode with react18 breaks edtr.io atm - reactStrictMode: false, + reactStrictMode: true, productionBrowserSourceMaps: true, transpilePackages: ['ramda'], // context: https://github.com/vercel/next.js/issues/40183 /*experimental: { diff --git a/package.json b/package.json index 2c0392c6c9..12be73b9a4 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,6 @@ "reselect": "^4.1.8", "slate": "^0.91.0", "slate-react": "^0.91.0", - "styled-components": "^5.0.0", "swr": "^2.0.0", "timeago-react": "^3.0.5", "timeago.js": "^4.0.2", @@ -130,7 +129,6 @@ "@types/react-notify-toast": "^0.5.3", "@types/react-syntax-highlighter": "^13.5.2", "@types/slate-react": "^0.50.1", - "@types/styled-components": "^5.1.26", "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4060eab7f6..9de18f60d0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,6 +20,7 @@ model ExerciseSubmission { @@index([entityId]) @@index([timestamp]) + @@index([path(length: 50)]) } model PrivateLinkPrototype { @@ -47,6 +48,6 @@ model MitmachWoche { } model PrototypeThreadStatus { - threadId String @db.VarChar(24) @id - status String @db.VarChar(24) -} \ No newline at end of file + threadId String @id @db.VarChar(24) + status String @db.VarChar(24) +} diff --git a/src/assets-webkit/styles/serlo-tailwind.css b/src/assets-webkit/styles/serlo-tailwind.css index a1af11b159..b05696e761 100644 --- a/src/assets-webkit/styles/serlo-tailwind.css +++ b/src/assets-webkit/styles/serlo-tailwind.css @@ -130,13 +130,6 @@ .serlo-solution-box { @apply mx-side mb-block border-l-8 border-brand-200 py-2.5; } - .serlo-styled-label { - @apply flex cursor-pointer items-center; - > div > * { - /* hacky selector */ - @apply ml-2; - } - } .serlo-image-centered { @apply mb-block px-4 text-center; } @@ -194,6 +187,7 @@ @apply px-0; } + & .serlo-spacing-fix, & .serlo-important, & .serlo-blockquote, & .serlo-spoiler-body, diff --git a/src/components/comments/dropdown-menu.tsx b/src/components/comments/dropdown-menu.tsx index 91b0589f6f..0ff9a2b7f5 100644 --- a/src/components/comments/dropdown-menu.tsx +++ b/src/components/comments/dropdown-menu.tsx @@ -107,10 +107,13 @@ export function DropdownMenu({ ) function onLinkToComment() { - const isOnEntity = window.location.href.includes(entityId.toString()) + const isOnEntity = + entityId && window.location.href.includes(entityId.toString()) onAnyClick() if (isOnEntity) highlight(id) - history.replaceState(null, '', `/${entityId}/#comment-${id}`) + if (entityId) { + history.replaceState(null, '', `/${entityId}/#comment-${id}`) + } copyToClipboad(window.location.href) showToastNotice('👌 ' + strings.share.copySuccess, 'success') } diff --git a/src/components/content/box.tsx b/src/components/content/box.tsx index 2785441e51..ca2117fde6 100644 --- a/src/components/content/box.tsx +++ b/src/components/content/box.tsx @@ -1,6 +1,6 @@ import { FrontendBoxNode } from '@/frontend-node-types' import { RenderNestedFunction } from '@/schema/article-renderer' -import { Renderer } from '@/serlo-editor/plugins/box/renderer' +import { BoxRenderer } from '@/serlo-editor/plugins/box/renderer' type BoxProps = FrontendBoxNode & { renderNested: RenderNestedFunction } @@ -17,7 +17,7 @@ export function Box({ const unwrappedTitle = title?.[0].children return ( - <>{content} - + ) } diff --git a/src/components/content/exercises/sc-mc-exercise.tsx b/src/components/content/exercises/sc-mc-exercise.tsx index b30cb3957d..3c6c15ee23 100644 --- a/src/components/content/exercises/sc-mc-exercise.tsx +++ b/src/components/content/exercises/sc-mc-exercise.tsx @@ -11,7 +11,7 @@ import { RenderNestedFunction } from '@/schema/article-renderer' import { ScMcExerciseRenderer, ScMcExerciseRendererAnswer, -} from '@/serlo-editor/plugins/sc-mc-exercise/renderer/renderer' +} from '@/serlo-editor/plugins/sc-mc-exercise/renderer' export interface ScMcExerciseProps { state: EditorPluginScMcExercise['state'] diff --git a/src/components/frontend-client-base.tsx b/src/components/frontend-client-base.tsx index 166e7ae16e..591ca38052 100644 --- a/src/components/frontend-client-base.tsx +++ b/src/components/frontend-client-base.tsx @@ -117,7 +117,7 @@ export function FrontendClientBase({ - + {kids}} diff --git a/src/components/modal-with-close-button.tsx b/src/components/modal-with-close-button.tsx index 155abdbb91..b3236b29fe 100644 --- a/src/components/modal-with-close-button.tsx +++ b/src/components/modal-with-close-button.tsx @@ -50,6 +50,7 @@ export function ModalWithCloseButton({ cursor-pointer rounded-full border-none bg-transparent text-center leading-tight text-almost-black hover:bg-brand hover:text-white `} + data-qa="modal-close-button" > diff --git a/src/components/user/profile-experimental.tsx b/src/components/user/profile-experimental.tsx index c58ca79c68..7ac885979e 100644 --- a/src/components/user/profile-experimental.tsx +++ b/src/components/user/profile-experimental.tsx @@ -11,6 +11,12 @@ export const features = { activeInDev: true, hideInProduction: true, }, + editorAnchorLinkCopyTool: { + cookieName: 'useEditorAnchorLinkCopyTool', + isActive: false, + activeInDev: true, + hideInProduction: true, + }, legacyDesign: { cookieName: 'useFrontend', isActive: false, @@ -88,6 +94,21 @@ export function ProfileExperimental() { )}
+ {features.editorAnchorLinkCopyTool && ( +
+

+ {renderFeatureButton('editorAnchorLinkCopyTool')} Editor: Anker-Link + Tool +

+

+ Ein neues Tool in der Editor-Toolbar um direkt Anker-Links auf + Editor-Plugins in die Zwischenablage zu kopieren. Wichtig: + Funktioniert nur, wenn der Inhalt ab Juli 2023 eine neue Revision + erhalten hat. +

+
+ )} +
{features.legacyDesign && (

diff --git a/src/contexts/entity-id-context.ts b/src/contexts/entity-id-context.ts index 47e0e540b6..e4563dc2da 100644 --- a/src/contexts/entity-id-context.ts +++ b/src/contexts/entity-id-context.ts @@ -1,12 +1,12 @@ import { createContext, useContext } from 'react' -export const EntityIdContext = createContext(null) +export const EntityIdContext = createContext(null) export const EntityIdProvider = EntityIdContext.Provider export function useEntityId() { const data = useContext(EntityIdContext) - if (!data) { + if (data === null) { throw new Error('Attempt to use entityId outside of provider!') } return data diff --git a/src/data/de/index.ts b/src/data/de/index.ts index 4e28b40e68..7ce9c8dd2e 100644 --- a/src/data/de/index.ts +++ b/src/data/de/index.ts @@ -602,13 +602,14 @@ export const loggedInData = { anchor: { title: "Sprungmarke", description: "Füge eine Sprungmarke innerhalb deines Inhalts hinzu.", - identifier: "Name", + identifier: "Ziel-Name (z.B. \"lange-erlaerung\")", anchorId: "Name der Sprungmarke" }, box: { title: "Box", description: "Ein Rahmen für Beispiele, Zitate, Warnungen, Beweise (math.), …", type: "Art der Box", + typeTooltip: "Wähle die Art der Box", titlePlaceholder: "(optionaler Titel)", anchorId: "Sprungmarke (anchor id)", emptyContentWarning: "Boxen ohne Inhalt werden nicht angezeigt" @@ -644,6 +645,7 @@ export const loggedInData = { geogebra: { title: 'GeoGebra Applet', description: "Binde Applets von GeoGebra Materials via Link oder ID ein.", + chooseApplet: "Applet auswählen", urlOrId: "GeoGebra Materials URL oder ID" }, highlight: { @@ -652,13 +654,14 @@ export const loggedInData = { clickAndEnter: "Klicke hier und füge deinen Quellcode ein…", enterHere: "Füge hier deinen Quellcode ein. Verlasse den Bereich, um eine Vorschau zu sehen.", language: "Programmiersprache", - enterLanguage: "Programmiersprache eingeben", - showLineNumbers: "Zeilennummern anzeigen" + languageTooltip: "Wähle die Sprache für's Syntax-Highlighting", + showLineNumbers: "Zeilennummern", + lineNumbersTooltip: "Sollten die Besucher*innen Zeilennummern sehen?" }, image: { title: "Bild", description: "Lade Bilder hoch oder verwende Bilder, die bereits online sind.", - upload: "Hochladen…", + upload: "Hochladen", imageUrl: "Bild-URL", placeholderEmpty: 'https://example.com/image.png', placeholderUploading: "Wird hochgeladen …", @@ -678,7 +681,7 @@ export const loggedInData = { description: "Binde einen Inhalt von serlo.org via ID ein.", illegalInjectionFound: "Ungültige Injection gefunden", serloEntitySrc: "Serlo Inhalt {{src}}", - serloId: "Serlo ID", + serloId: 'Serlo ID', placeholder: "Serlo ID (z.B. 1565)" }, layout: { @@ -718,6 +721,7 @@ export const loggedInData = { title: "Zeilen", searchForTools: "Suche hier nach Tools…", duplicate: "Duplizieren", + copyAnchorLink: "Link zu diesem Element kopieren", remove: "Löschen", close: "Schließen", dragElement: "Verschiebe das Element innerhalb des Dokuments", @@ -783,6 +787,7 @@ export const loggedInData = { formula: "[neue Formel]", visual: "visuell", latex: 'LaTeX', + latexEditorTitle: "LaTeX-Editor", onlyLatex: "Nur LaTeX verfügbar", shortcuts: "Tastenkürzel", fraction: "Bruch", @@ -798,9 +803,9 @@ export const loggedInData = { }, video: { title: 'Video', - decription: "Binde Videos von YouTube, Vimeo, Wikimedia Commons oder BR ein.", + description: "Binde Videos von YouTube, Vimeo, Wikimedia Commons oder BR ein.", videoUrl: 'Video URL', - description: "Beschreibung", + videoDescription: "Beschreibung", titlePlaceholder: "Titel", url: 'URL', seoTitle: "Titel für Suchmaschinen" @@ -951,7 +956,8 @@ export const loggedInData = { current: "Aktuell", author: "Verfasser", createdAt: "Zeitstempel", - ready: "Bereit zum Speichern?" + ready: "Bereit zum Speichern?", + anchorLinkWarning: "Dieser Link funktioniert nur im Frontend und für Inhalte, die eine etwas relativ neue akzeptierte Bearbeitung haben." }, taxonomy: { title: "Titel" diff --git a/src/data/en/index.ts b/src/data/en/index.ts index a3a3fd29d1..6512c9e57b 100644 --- a/src/data/en/index.ts +++ b/src/data/en/index.ts @@ -670,7 +670,7 @@ export const loggedInData = { anchor: { title: 'Anchor', description: 'Insert an anchor.', - identifier: 'Identifier', + identifier: 'Identifier (e.g. "long-explanation")', anchorId: 'ID of the anchor', }, box: { @@ -678,6 +678,7 @@ export const loggedInData = { description: 'A container for examples, quotes, warnings, theorems, notes…', type: 'Type of box', + typeTooltip: 'Choose the type of the box', titlePlaceholder: '(optional title)', anchorId: 'Anchor ID', emptyContentWarning: 'Boxes without content will not be displayed', @@ -715,6 +716,7 @@ export const loggedInData = { geogebra: { title: 'GeoGebra Applet', description: 'Embed GeoGebra Materials applets via URL or ID.', + chooseApplet: 'Choose Applet', urlOrId: 'GeoGebra URL or ID', }, highlight: { @@ -723,13 +725,14 @@ export const loggedInData = { clickAndEnter: 'Click here and enter your source code…', enterHere: 'Enter your source code here', language: 'Language', - enterLanguage: 'Enter language', - showLineNumbers: 'Show line numbers', + languageTooltip: 'Choose language for syntax highlighting', + showLineNumbers: 'Line numbers', + lineNumbersTooltip: 'Should users see line numbers?', }, image: { title: 'Image', description: 'Upload images.', - upload: 'Upload…', + upload: 'Upload', imageUrl: 'Image URL', placeholderEmpty: 'https://example.com/image.png', placeholderUploading: 'Uploading…', @@ -749,7 +752,7 @@ export const loggedInData = { description: 'Embed serlo.org content via their ID.', illegalInjectionFound: 'Illegal injection found', serloEntitySrc: 'Serlo entity {{src}}', - serloId: 'Serlo ID:', + serloId: 'Serlo ID', placeholder: 'Serlo ID (e.g. 1565)', }, layout: { @@ -792,6 +795,7 @@ export const loggedInData = { title: 'Rows', searchForTools: 'Search for tools…', duplicate: 'Duplicate', + copyAnchorLink: "Copy link to this element", remove: 'Remove', close: 'Close', dragElement: 'Drag the element within the document', @@ -859,6 +863,7 @@ export const loggedInData = { formula: '[formula]', visual: 'visual', latex: 'LaTeX', + latexEditorTitle: 'LaTeX editor', onlyLatex: 'Only LaTeX editor available', shortcuts: 'Shortcuts', fraction: 'Fraction', @@ -874,9 +879,9 @@ export const loggedInData = { }, video: { title: 'Video', - decription: 'Embed YouTube, Vimeo, Wikimedia Commons or BR videos.', + description: 'Embed YouTube, Vimeo, Wikimedia Commons or BR videos.', videoUrl: 'Video URL', - description: 'Description', + videoDescription: 'Description', titlePlaceholder: 'Title', url: 'URL', seoTitle: 'Title for search engines', @@ -1038,6 +1043,7 @@ export const loggedInData = { author: 'Author', createdAt: 'when?', ready: 'Ready to save?', + anchorLinkWarning: 'This link will only work in the frontend and for content that has a somewhat new revision.', }, taxonomy: { title: 'Title', diff --git a/src/data/es/index.ts b/src/data/es/index.ts index 643be131cf..bdb3824488 100644 --- a/src/data/es/index.ts +++ b/src/data/es/index.ts @@ -602,13 +602,14 @@ export const loggedInData = { anchor: { title: "Ancla", description: "Insertar un ancla.", - identifier: "Identificador", + identifier: 'Identifier (e.g. "long-explanation")', anchorId: "ID del ancla" }, box: { title: "Contenedor", description: "Un contenedor para ejemplos, comillas, advertencias, teoremas, notas…", type: "Tipo de contenedor", + typeTooltip: 'Choose the type of the box', titlePlaceholder: "(título opcional)", anchorId: "ID de Ancla (marca de posición)", emptyContentWarning: "Los contenedores sin contenido no se visualizarán" @@ -644,6 +645,7 @@ export const loggedInData = { geogebra: { title: "Aplicación GeoGebra", description: "Insertar el Material de la aplicación GeoGebra a través de URL o ID.", + chooseApplet: 'Choose Applet', urlOrId: "URL o ID de GeoGebra" }, highlight: { @@ -652,13 +654,14 @@ export const loggedInData = { clickAndEnter: "Haz clic aquí e introduce tu código fuente…", enterHere: "Introduce tu código fuente aquí", language: "Idioma", - enterLanguage: "Introducir idioma", - showLineNumbers: "Mostrar números de línea" + languageTooltip: 'Choose language for syntax highlighting', + showLineNumbers: 'Line numbers', + lineNumbersTooltip: 'Should users see line numbers?' }, image: { title: "Imágen", description: "Subir imágenes.", - upload: "Subir…", + upload: 'Upload', imageUrl: "URL de la imagen", placeholderEmpty: 'https://example.com/image.png', placeholderUploading: "Subiendo…", @@ -678,7 +681,7 @@ export const loggedInData = { description: "Insertar el contenido de serlo.org a través de su ID.", illegalInjectionFound: "Entrada ilegal encontrada", serloEntitySrc: "entidad de Serlo {{src}}", - serloId: 'Serlo ID:', + serloId: 'Serlo ID', placeholder: "Serlo ID (p.ej. 1565)" }, layout: { @@ -718,6 +721,7 @@ export const loggedInData = { title: "Filas", searchForTools: "Buscar herramientas…", duplicate: "Duplicar", + copyAnchorLink: "Copy link to this element", remove: "Eliminar", close: "Cerrar", dragElement: "Arrastra el elemento dentro del documento", @@ -783,6 +787,7 @@ export const loggedInData = { formula: "[fórmula]", visual: 'visual', latex: 'LaTeX', + latexEditorTitle: 'LaTeX editor', onlyLatex: "Sólo está disponible el editor LaTeX ", shortcuts: "Acceso directo", fraction: "Fracción", @@ -798,9 +803,9 @@ export const loggedInData = { }, video: { title: "Vídeo", - decription: "Inserta vídeos de YouTube, Vimeo, Wikimedia Commons o BR.", + description: "Inserta vídeos de YouTube, Vimeo, Wikimedia Commons o BR.", videoUrl: "URL del vídeo", - description: "Descripción", + videoDescription: "Descripción", titlePlaceholder: "Título", url: 'URL', seoTitle: "Título para motores de búsqueda" @@ -951,7 +956,8 @@ export const loggedInData = { current: "Actual", author: "Autor", createdAt: "¿Cuándo?", - ready: "¿Listo para guardar?" + ready: "¿Listo para guardar?", + anchorLinkWarning: 'This link will only work in the frontend and for content that has a somewhat new revision.' }, taxonomy: { title: "Título" diff --git a/src/data/fr/index.ts b/src/data/fr/index.ts index f62d179b10..a6c371d833 100644 --- a/src/data/fr/index.ts +++ b/src/data/fr/index.ts @@ -602,13 +602,14 @@ export const loggedInData = { anchor: { title: "Ancre", description: "Insérer une ancre.", - identifier: "identifiant", + identifier: 'Identifier (e.g. "long-explanation")', anchorId: "ID de l'ancre" }, box: { title: 'Container', description: 'A container for examples, quotes, warnings, theorems, notes…', type: 'Type of box', + typeTooltip: 'Choose the type of the box', titlePlaceholder: '(optional title)', anchorId: 'Anchor ID', emptyContentWarning: 'Boxes without content will not be displayed' @@ -644,6 +645,7 @@ export const loggedInData = { geogebra: { title: "Applet GéoGebra", description: "Intégrer une applet GeoGebra par URL ou ID.", + chooseApplet: 'Choose Applet', urlOrId: "URL ou ID de GeoGebra" }, highlight: { @@ -652,13 +654,14 @@ export const loggedInData = { clickAndEnter: "Clique ici pour entrer du code source…", enterHere: "Saisie du code source", language: "Langue", - enterLanguage: "Saisir la langue", - showLineNumbers: "Afficher les numéros de ligne" + languageTooltip: 'Choose language for syntax highlighting', + showLineNumbers: 'Line numbers', + lineNumbersTooltip: 'Should users see line numbers?' }, image: { title: 'Image', description: "Télécharger des images.", - upload: 'Upload…', + upload: 'Upload', imageUrl: 'Image URL', placeholderEmpty: 'https://example.com/image.png', placeholderUploading: 'Uploading…', @@ -678,7 +681,7 @@ export const loggedInData = { description: "Intégrer contenu de serlo.org en utilisant l'ID.", illegalInjectionFound: "Injection illégale trouvée", serloEntitySrc: "Entité Serlo {{src}}", - serloId: 'Serlo ID:', + serloId: 'Serlo ID', placeholder: 'Serlo ID (e.g. 1565)' }, layout: { @@ -718,6 +721,7 @@ export const loggedInData = { title: 'Rows', searchForTools: "Rechercher des outils…", duplicate: "Dupliquer", + copyAnchorLink: "Copy link to this element", remove: "Supprimer", close: "Fermer", dragElement: "Faire glisser l'élément dans le document", @@ -783,6 +787,7 @@ export const loggedInData = { formula: '[formula]', visual: "visuel", latex: 'LaTeX', + latexEditorTitle: 'LaTeX editor', onlyLatex: "Seulement l'éditeur LaTeX est disponible", shortcuts: "Raccourcis", fraction: 'Fraction', @@ -798,9 +803,9 @@ export const loggedInData = { }, video: { title: "Vidéo", - decription: "Intégrer YouTube, Vimeo, Wikimedia Commons ou les vidéos BR.", + description: "Intégrer YouTube, Vimeo, Wikimedia Commons ou les vidéos BR.", videoUrl: "URL de la vidéo", - description: 'Description', + videoDescription: 'Description', titlePlaceholder: "Titre", url: 'URL', seoTitle: "Titre pour les moteurs de recherche" @@ -951,7 +956,8 @@ export const loggedInData = { current: "Actuel", author: "Auteur", createdAt: "Créé le", - ready: 'Ready to save?' + ready: 'Ready to save?', + anchorLinkWarning: 'This link will only work in the frontend and for content that has a somewhat new revision.' }, taxonomy: { title: "Titre" diff --git a/src/data/hi/index.ts b/src/data/hi/index.ts index da83dd3840..33a1b2582d 100644 --- a/src/data/hi/index.ts +++ b/src/data/hi/index.ts @@ -602,13 +602,14 @@ export const loggedInData = { anchor: { title: 'Anchor', description: 'Insert an anchor.', - identifier: 'Identifier', + identifier: 'Identifier (e.g. "long-explanation")', anchorId: 'ID of the anchor' }, box: { title: 'Container', description: 'A container for examples, quotes, warnings, theorems, notes…', type: 'Type of box', + typeTooltip: 'Choose the type of the box', titlePlaceholder: '(optional title)', anchorId: 'Anchor ID', emptyContentWarning: 'Boxes without content will not be displayed' @@ -644,6 +645,7 @@ export const loggedInData = { geogebra: { title: 'GeoGebra Applet', description: 'Embed GeoGebra Materials applets via URL or ID.', + chooseApplet: 'Choose Applet', urlOrId: 'GeoGebra URL or ID' }, highlight: { @@ -652,13 +654,14 @@ export const loggedInData = { clickAndEnter: 'Click here and enter your source code…', enterHere: 'Enter your source code here', language: 'Language', - enterLanguage: 'Enter language', - showLineNumbers: 'Show line numbers' + languageTooltip: 'Choose language for syntax highlighting', + showLineNumbers: 'Line numbers', + lineNumbersTooltip: 'Should users see line numbers?' }, image: { title: 'Image', description: 'Upload images.', - upload: 'Upload…', + upload: 'Upload', imageUrl: 'Image URL', placeholderEmpty: 'https://example.com/image.png', placeholderUploading: 'Uploading…', @@ -678,7 +681,7 @@ export const loggedInData = { description: 'Embed serlo.org content via their ID.', illegalInjectionFound: 'Illegal injection found', serloEntitySrc: 'Serlo entity {{src}}', - serloId: 'Serlo ID:', + serloId: 'Serlo ID', placeholder: 'Serlo ID (e.g. 1565)' }, layout: { @@ -718,6 +721,7 @@ export const loggedInData = { title: 'Rows', searchForTools: 'Search for tools…', duplicate: 'Duplicate', + copyAnchorLink: "Copy link to this element", remove: 'Remove', close: "बंद करें", dragElement: 'Drag the element within the document', @@ -783,6 +787,7 @@ export const loggedInData = { formula: '[formula]', visual: 'visual', latex: 'LaTeX', + latexEditorTitle: 'LaTeX editor', onlyLatex: 'Only LaTeX editor available', shortcuts: 'Shortcuts', fraction: 'Fraction', @@ -798,9 +803,9 @@ export const loggedInData = { }, video: { title: "वीडियो", - decription: 'Embed YouTube, Vimeo, Wikimedia Commons or BR videos.', + description: 'Embed YouTube, Vimeo, Wikimedia Commons or BR videos.', videoUrl: 'Video URL', - description: 'Description', + videoDescription: 'Description', titlePlaceholder: "शीर्षक", url: 'URL', seoTitle: 'Title for search engines' @@ -951,7 +956,8 @@ export const loggedInData = { current: 'Current', author: 'Author', createdAt: 'when?', - ready: 'Ready to save?' + ready: 'Ready to save?', + anchorLinkWarning: 'This link will only work in the frontend and for content that has a somewhat new revision.' }, taxonomy: { title: 'Title' diff --git a/src/data/ta/index.ts b/src/data/ta/index.ts index 4352dd759c..16ac4c873f 100644 --- a/src/data/ta/index.ts +++ b/src/data/ta/index.ts @@ -602,13 +602,14 @@ export const loggedInData = { anchor: { title: 'Anchor', description: 'Insert an anchor.', - identifier: 'Identifier', + identifier: 'Identifier (e.g. "long-explanation")', anchorId: 'ID of the anchor' }, box: { title: 'Container', description: 'A container for examples, quotes, warnings, theorems, notes…', type: 'Type of box', + typeTooltip: 'Choose the type of the box', titlePlaceholder: '(optional title)', anchorId: 'Anchor ID', emptyContentWarning: 'Boxes without content will not be displayed' @@ -644,6 +645,7 @@ export const loggedInData = { geogebra: { title: 'GeoGebra Applet', description: 'Embed GeoGebra Materials applets via URL or ID.', + chooseApplet: 'Choose Applet', urlOrId: 'GeoGebra URL or ID' }, highlight: { @@ -652,13 +654,14 @@ export const loggedInData = { clickAndEnter: 'Click here and enter your source code…', enterHere: 'Enter your source code here', language: "மொழி", - enterLanguage: 'Enter language', - showLineNumbers: 'Show line numbers' + languageTooltip: 'Choose language for syntax highlighting', + showLineNumbers: 'Line numbers', + lineNumbersTooltip: 'Should users see line numbers?' }, image: { title: "படம்", description: 'Upload images.', - upload: 'Upload…', + upload: 'Upload', imageUrl: 'Image URL', placeholderEmpty: 'https://example.com/image.png', placeholderUploading: 'Uploading…', @@ -678,7 +681,7 @@ export const loggedInData = { description: 'Embed serlo.org content via their ID.', illegalInjectionFound: 'Illegal injection found', serloEntitySrc: 'Serlo entity {{src}}', - serloId: 'Serlo ID:', + serloId: 'Serlo ID', placeholder: 'Serlo ID (e.g. 1565)' }, layout: { @@ -718,6 +721,7 @@ export const loggedInData = { title: 'Rows', searchForTools: 'Search for tools…', duplicate: 'Duplicate', + copyAnchorLink: "Copy link to this element", remove: 'Remove', close: "நெருக்கமான", dragElement: 'Drag the element within the document', @@ -783,6 +787,7 @@ export const loggedInData = { formula: '[formula]', visual: 'visual', latex: 'LaTeX', + latexEditorTitle: 'LaTeX editor', onlyLatex: 'Only LaTeX editor available', shortcuts: 'Shortcuts', fraction: 'Fraction', @@ -798,9 +803,9 @@ export const loggedInData = { }, video: { title: "காணொளி", - decription: 'Embed YouTube, Vimeo, Wikimedia Commons or BR videos.', + description: 'Embed YouTube, Vimeo, Wikimedia Commons or BR videos.', videoUrl: 'Video URL', - description: "விவரிப்பு:", + videoDescription: "விவரிப்பு:", titlePlaceholder: "தலைப்பு", url: 'URL', seoTitle: 'Title for search engines' @@ -951,7 +956,8 @@ export const loggedInData = { current: 'Current', author: 'Author', createdAt: 'when?', - ready: "சேமிக்கத் தயாரா?" + ready: "சேமிக்கத் தயாரா?", + anchorLinkWarning: 'This link will only work in the frontend and for content that has a somewhat new revision.' }, taxonomy: { title: 'Title' diff --git a/src/frontend-node-types.ts b/src/frontend-node-types.ts index ca6bc0a407..a54893329a 100644 --- a/src/frontend-node-types.ts +++ b/src/frontend-node-types.ts @@ -119,6 +119,7 @@ export interface FrontendSlatePNode { export interface FrontendSlateContainerNode { type: FrontendNodeType.SlateContainer children?: FrontendContentNode[] + pluginId?: string } export interface FrontendHNode { @@ -144,11 +145,13 @@ export interface FrontendImageNode { maxWidth?: number caption?: FrontendContentNode[] children?: undefined + pluginId?: string } export interface FrontendSpoilerContainerNode { type: FrontendNodeType.SpoilerContainer children: [FrontendSpoilerTitleNode, FrontendSpoilerBodyNode] + pluginId?: string } export interface FrontendSpoilerTitleNode { @@ -181,6 +184,7 @@ export interface FrontendMultiMediaNode { mediaWidth: number media: FrontendContentNode[] children: FrontendContentNode[] + pluginId?: string } export interface FrontendRowNode { @@ -211,6 +215,7 @@ export interface FrontendBoxNode { title?: FrontendContentNode[] anchorId: string children?: FrontendContentNode[] + pluginId?: string } export type FrontendAnchorNode = EditorAnchorPlugin & { @@ -222,6 +227,7 @@ export interface FrontendSerloTableNode { type: FrontendNodeType.SerloTable children?: FrontendSerloTrNode[] tableType: keyof typeof TableType | string + pluginId?: string } export interface FrontendSerloTrNode { @@ -257,11 +263,13 @@ export interface FrontendTdNode { export type FrontendGeogebraNode = EditorGeogebraPlugin & { type: FrontendNodeType.Geogebra children?: undefined + pluginId?: string } export type FrontendInjectionNode = EditorInjectionPlugin & { type: FrontendNodeType.Injection children?: undefined + pluginId?: string } interface BareSolution { @@ -292,6 +300,7 @@ export interface FrontendExerciseNode { children?: undefined href?: string unrevisedRevisions?: number + pluginId?: string } export interface FrontendSolutionNode { @@ -371,11 +380,13 @@ export type FrontendVideoNode = EditorVideoPlugin & { license?: LicenseData type: FrontendNodeType.Video children?: undefined + pluginId?: string } export type FrontendCodeNode = EditorHighlightPlugin & { type: FrontendNodeType.Code children?: undefined + pluginId?: string } export interface FrontendEquationsNode { @@ -393,6 +404,7 @@ export interface FrontendEquationsNode { firstExplanation: FrontendContentNode[] transformationTarget: 'term' | 'equation' | string children?: undefined + pluginId?: string } export interface FrontendPageLayoutNode { @@ -401,16 +413,19 @@ export interface FrontendPageLayoutNode { column2: FrontendContentNode[] widthPercent: number children?: undefined + pluginId?: string } export type FrontendPageTeamNode = PageTeamRendererProps & { type: FrontendNodeType.PageTeam children?: undefined + pluginId?: string } export interface FrontendPagePartnersNode { type: FrontendNodeType.PagePartners children?: undefined + pluginId?: string } export type FrontendVoidNode = diff --git a/src/helper/exercise-submission.ts b/src/helper/exercise-submission.ts index bf1cf0178e..925561aa41 100644 --- a/src/helper/exercise-submission.ts +++ b/src/helper/exercise-submission.ts @@ -1,5 +1,7 @@ import { v4 as uuidv4 } from 'uuid' +import { isProduction } from './is-production' + export interface ExerciseSubmissionData { path: string entityId: number @@ -11,6 +13,8 @@ export interface ExerciseSubmissionData { const sesionStorageKey = 'frontend_exercise_submission_session_id' export function exerciseSubmission(data: ExerciseSubmissionData) { + if (!isProduction) return // don't submit outside of production + if (!sessionStorage.getItem(sesionStorageKey)) { // set new session id sessionStorage.setItem(sesionStorageKey, uuidv4()) diff --git a/src/mutations/thread.ts b/src/mutations/thread.ts index e498362907..40cd1f317d 100644 --- a/src/mutations/thread.ts +++ b/src/mutations/thread.ts @@ -40,6 +40,7 @@ export function useThreadArchivedMutation() { const success = await mutationFetch(threadArchiveMutation, input) if (success) { + if (!entityId) return false await mutate(`comments::${entityId}`) mutateSWRCache(threadCacheShouldMutate) NProgress.done() @@ -68,6 +69,7 @@ export function useSetThreadStateMutation() { const success = await mutationFetch(setThreadStateMutation, input) if (success) { + if (!entityId) return false await mutate(`comments::${entityId}`) mutateSWRCache(threadCacheShouldMutate) } @@ -92,6 +94,7 @@ export function useSetCommentStateMutation() { const { mutate } = useSWRConfig() return async function (input: ThreadSetCommentStateInput) { + if (!entityId) return false const success = await mutationFetch(setCommentStateMutation, input) if (success) { @@ -145,6 +148,7 @@ export function useCreateCommentMutation() { const entityId = useEntityId() return async function (input: ThreadCreateCommentInput) { + if (!entityId) return false const success = await mutationFetch(createCommentMutation, input) if (success) { @@ -172,6 +176,7 @@ export function useEditCommentMutation() { const entityId = useEntityId() return async function (input: ThreadEditCommentInput) { + if (!entityId) return false const success = await mutationFetch(editCommentMutation, input) if (success) { diff --git a/src/schema/article-renderer.tsx b/src/schema/article-renderer.tsx index 47f8d124af..ea21acc2e7 100644 --- a/src/schema/article-renderer.tsx +++ b/src/schema/article-renderer.tsx @@ -128,16 +128,26 @@ function render(value: FrontendContentNode, path: NodePath = []): ReactNode { children.push(render(value, path.concat(index))) }) } - return ( - - {renderElement({ - element: currentNode, - children: children.length === 0 ? null : children, - value, - path, - })} - - ) + + const renderedElement = renderElement({ + element: currentNode, + children: children.length === 0 ? null : children, + value, + path, + }) + + const shortId = + Object.hasOwn(currentNode, 'pluginId') && + currentNode.pluginId?.split('-')[0] + + if (shortId) + return ( +
+ {renderedElement} +
+ ) + + return {renderedElement} } if (currentNode.text === '') return null // avoid rendering empty spans diff --git a/src/schema/convert-edtr-io-state.tsx b/src/schema/convert-edtr-io-state.tsx index 1004b2f7bd..511b6c8208 100644 --- a/src/schema/convert-edtr-io-state.tsx +++ b/src/schema/convert-edtr-io-state.tsx @@ -97,7 +97,11 @@ function convertPlugin( } if (node.plugin === EditorPluginType.Text) { return [ - { type: FrontendNodeType.SlateContainer, children: convert(node.state) }, + { + type: FrontendNodeType.SlateContainer, + children: convert(node.state), + pluginId: node.id, + }, ] } if (node.plugin === EditorPluginType.Image) { @@ -135,6 +139,7 @@ function convertPlugin( maxWidth, href: link?.href, caption: convertedCaption, + pluginId: node.id, }, ] } @@ -167,6 +172,7 @@ function convertPlugin( anchorId: node.state.anchorId, title, children: convert(node.state.content.state as SupportedEditorPlugin), + pluginId: node.id, }, ] } @@ -191,6 +197,7 @@ function convertPlugin( children: convert(node.state.content as SupportedEditorPlugin), }, ], + pluginId: node.id, }, ] } @@ -202,6 +209,7 @@ function convertPlugin( mediaWidth: width, media: convert(node.state.multimedia as SupportedEditorPlugin), children: convert(node.state.explanation as SupportedEditorPlugin), + pluginId: node.id, }, ] } @@ -221,11 +229,11 @@ function convertPlugin( ] } if (node.plugin === EditorPluginType.Injection) { - return [{ ...node, type: FrontendNodeType.Injection }] + return [{ ...node, type: FrontendNodeType.Injection, pluginId: node.id }] } if (node.plugin === EditorPluginType.Highlight) { if (Object.keys(node.state).length === 0) return [] // ignore empty highlight plugin - return [{ ...node, type: FrontendNodeType.Code }] + return [{ ...node, type: FrontendNodeType.Code, pluginId: node.id }] } if (node.plugin === EditorPluginType.Table) { const html = converter.makeHtml(node.state) @@ -253,6 +261,7 @@ function convertPlugin( type: FrontendNodeType.SerloTable, tableType: node.state.tableType, children, + pluginId: node.id, }, ] } @@ -263,6 +272,7 @@ function convertPlugin( plugin: EditorPluginType.Video, type: FrontendNodeType.Video, state: node.state, + pluginId: node.id, }, ] } @@ -279,6 +289,7 @@ function convertPlugin( plugin: EditorPluginType.Geogebra, type: FrontendNodeType.Geogebra, state: id, + pluginId: node.id, }, ] } @@ -302,6 +313,7 @@ function convertPlugin( steps, firstExplanation: convert(firstExplanation), transformationTarget, + pluginId: node.id, }, ] } @@ -313,14 +325,21 @@ function convertPlugin( column1: convert(node.state.column1 as SupportedEditorPlugin), column2: convert(node.state.column2 as SupportedEditorPlugin), widthPercent: node.state.widthPercent, + pluginId: node.id, }, ] } if (node.plugin === EditorPluginType.PageTeam) { - return [{ type: FrontendNodeType.PageTeam, data: node.state.data }] + return [ + { + type: FrontendNodeType.PageTeam, + data: node.state.data, + pluginId: node.id, + }, + ] } if (node.plugin === EditorPluginType.PagePartners) { - return [{ type: FrontendNodeType.PagePartners }] + return [{ type: FrontendNodeType.PagePartners, pluginId: node.id }] } return [] diff --git a/src/serlo-editor-integration/create-plugins.tsx b/src/serlo-editor-integration/create-plugins.tsx index 8edf30b669..6674d031d5 100644 --- a/src/serlo-editor-integration/create-plugins.tsx +++ b/src/serlo-editor-integration/create-plugins.tsx @@ -14,7 +14,7 @@ import IconVideo from '@/assets-webkit/img/editor/icon-video.svg' import { shouldUseFeature } from '@/components/user/profile-experimental' import { LoggedInData, UuidType } from '@/data-types' import { Instance } from '@/fetcher/graphql-types/operations' -import { PluginsContextPlugins } from '@/serlo-editor/core/contexts/plugins-context' +import { PluginsWithData } from '@/serlo-editor/plugin/helpers/editor-plugins' import { importantPlugin } from '@/serlo-editor/plugins/_on-the-way-out/important/important' import { layoutPlugin } from '@/serlo-editor/plugins/_on-the-way-out/layout' import { anchorPlugin } from '@/serlo-editor/plugins/anchor' @@ -62,100 +62,100 @@ export function createPlugins({ editorStrings: LoggedInData['strings']['editor'] instance: Instance parentType?: string -}): PluginsContextPlugins { +}): PluginsWithData { const isPage = parentType === UuidType.Page return [ { type: EditorPluginType.Text, plugin: createTextPlugin({ serloLinkSearch: instance === Instance.De }), - visible: true, + visibleInSuggestions: true, icon: , }, { type: EditorPluginType.Image, plugin: imagePlugin, - visible: true, + visibleInSuggestions: true, icon: , }, { type: EditorPluginType.Multimedia, plugin: createMultimediaPlugin(), - visible: true, + visibleInSuggestions: true, icon: , }, { type: EditorPluginType.Spoiler, plugin: createSpoilerPlugin(), - visible: true, + visibleInSuggestions: true, icon: , }, { type: EditorPluginType.Box, plugin: createBoxPlugin({}), - visible: true, + visibleInSuggestions: true, icon: , }, { type: EditorPluginType.SerloTable, plugin: createSerloTablePlugin(), - visible: true, + visibleInSuggestions: true, icon: , }, { type: EditorPluginType.Injection, plugin: injectionPlugin, - visible: true, + visibleInSuggestions: true, icon: , }, { type: EditorPluginType.Equations, plugin: equationsPlugin, - visible: true, + visibleInSuggestions: true, icon: , }, { type: EditorPluginType.Geogebra, plugin: geoGebraPlugin, - visible: true, + visibleInSuggestions: true, icon: , }, { type: EditorPluginType.Highlight, plugin: createHighlightPlugin(), - visible: true, + visibleInSuggestions: true, icon: , }, { type: EditorPluginType.Video, plugin: videoPlugin, - visible: true, + visibleInSuggestions: true, icon: , }, { type: EditorPluginType.Anchor, plugin: anchorPlugin, - visible: true, + visibleInSuggestions: true, }, { type: EditorPluginType.PasteHack, plugin: pasteHackPlugin, - visible: shouldUseFeature('edtrPasteHack'), + visibleInSuggestions: shouldUseFeature('edtrPasteHack'), }, { type: EditorPluginType.PageLayout, plugin: pageLayoutPlugin, - visible: isPage, + visibleInSuggestions: isPage, }, { type: EditorPluginType.PageTeam, plugin: pageTeamPlugin, - visible: isPage, + visibleInSuggestions: isPage, }, { type: EditorPluginType.PagePartners, plugin: pagePartnersPlugin, - visible: isPage, + visibleInSuggestions: isPage, }, // never visible in suggestions diff --git a/src/serlo-editor-integration/serlo-editor.tsx b/src/serlo-editor-integration/serlo-editor.tsx index 3854b75c6b..d844924ace 100644 --- a/src/serlo-editor-integration/serlo-editor.tsx +++ b/src/serlo-editor-integration/serlo-editor.tsx @@ -14,6 +14,7 @@ import { useInstanceData } from '@/contexts/instance-context' import { useLoggedInData } from '@/contexts/logged-in-data-context' import { SetEntityMutationData } from '@/mutations/use-set-entity-mutation/types' import { Editor, EditorProps } from '@/serlo-editor/core' +import { editorPlugins } from '@/serlo-editor/plugin/helpers/editor-plugins' export interface SerloEditorProps { children?: ReactNode @@ -64,11 +65,15 @@ export function SerloEditor({ const editorStrings = loggedInData.strings.editor - const plugins = createPlugins({ - editorStrings, - instance: lang, - parentType: type, - }) + // simplest way to provide plugins to editor that can also easily be adapted by edusharing + editorPlugins.init( + createPlugins({ + editorStrings, + instance: lang, + parentType: type, + }) + ) + return ( {/* preload formula plugin */} { diff --git a/src/serlo-editor-integration/types/editor-plugins.ts b/src/serlo-editor-integration/types/editor-plugins.ts index 62f851e26b..1d56329b1d 100644 --- a/src/serlo-editor-integration/types/editor-plugins.ts +++ b/src/serlo-editor-integration/types/editor-plugins.ts @@ -40,114 +40,142 @@ export type SlateTextElement = CustomText export interface EditorAnchorPlugin { plugin: EditorPluginType.Anchor state: StateTypeSerializedType + id?: string } export interface EditorArticlePlugin { plugin: EditorPluginType.Article state: StateTypeSerializedType + id?: string } export interface EditorBlockquotePlugin { plugin: EditorPluginType.Blockquote state: StateTypeSerializedType + id?: string } export interface EditorBoxPlugin { plugin: EditorPluginType.Box state: StateTypeSerializedType + id?: string } export interface EditorUnsupportedPlugin { plugin: EditorPluginType.Unsupported state: StateTypeSerializedType + id?: string } export interface EditorEquationsPlugin { plugin: EditorPluginType.Equations state: StateTypeSerializedType + id?: string } export interface EditorExercisePlugin { plugin: EditorPluginType.Exercise state: StateTypeSerializedType + id?: string } export interface EditorGeogebraPlugin { plugin: EditorPluginType.Geogebra state: StateTypeSerializedType + id?: string } export interface EditorHighlightPlugin { plugin: EditorPluginType.Highlight state: StateTypeSerializedType + id?: string } export interface EditorImagePlugin { plugin: EditorPluginType.Image state: StateTypeSerializedType + id?: string } export interface EditorImportantPlugin { plugin: EditorPluginType.Important state: StateTypeSerializedType + id?: string } export interface EditorInjectionPlugin { plugin: EditorPluginType.Injection state: StateTypeSerializedType + id?: string } export interface EditorInputExercisePlugin { plugin: EditorPluginType.InputExercise state: StateTypeSerializedType + id?: string } export interface EditorLayoutPlugin { plugin: EditorPluginType.Layout state: StateTypeSerializedType + id?: string } export interface EditorMultimediaPlugin { plugin: EditorPluginType.Multimedia state: StateTypeSerializedType + id?: string } export interface EditorRowsPlugin { plugin: EditorPluginType.Rows state: StateTypeSerializedType + id?: string } export interface EditorScMcExercisePlugin { plugin: EditorPluginType.ScMcExercise state: StateTypeSerializedType + id?: string } export interface EditorSpoilerPlugin { plugin: EditorPluginType.Spoiler state: StateTypeSerializedType + id?: string } export interface EditorSerloInjectionPlugin { plugin: EditorPluginType.Injection state: StateTypeSerializedType + id?: string } export interface EditorSolutionPlugin { plugin: EditorPluginType.Solution state: StateTypeSerializedType + id?: string } export interface EditorTablePlugin { plugin: EditorPluginType.Table state: StateTypeSerializedType + id?: string } export interface EditorSerloTablePlugin { plugin: EditorPluginType.SerloTable state: StateTypeSerializedType + id?: string } export interface EditorTextPlugin { plugin: EditorPluginType.Text state: StateTypeSerializedType + id?: string } export interface EditorVideoPlugin { plugin: EditorPluginType.Video state: StateTypeSerializedType + id?: string } export interface EditorPageLayoutPlugin { plugin: EditorPluginType.PageLayout state: StateTypeSerializedType + id?: string } export interface EditorPageTeamPlugin { plugin: EditorPluginType.PageTeam state: StateTypeSerializedType + id?: string } export interface EditorPagePartnersPlugin { plugin: EditorPluginType.PagePartners state: StateTypeSerializedType + id?: string } export interface EditorH5PPlugin { plugin: EditorPluginType.H5p state: StateTypeSerializedType + id?: string } export type SupportedEditorPlugin = @@ -176,4 +204,5 @@ export type SupportedEditorPlugin = export interface UnknownEditorPlugin { plugin: string state?: unknown + id?: string } diff --git a/src/serlo-editor/core/contexts/plugins-context.ts b/src/serlo-editor/core/contexts/plugins-context.ts deleted file mode 100644 index 2bd5cd73f4..0000000000 --- a/src/serlo-editor/core/contexts/plugins-context.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { createContext, useContext } from 'react' - -import { EditorPlugin } from '@/serlo-editor/types/internal__plugin' -import { EditorPluginType } from '@/serlo-editor-integration/types/editor-plugin-type' - -export const PluginsContext = createContext([]) - -export function usePlugins() { - return useContext(PluginsContext) -} - -export function usePlugin(type?: string) { - const plugins = useContext(PluginsContext) - return getPluginByType(plugins, type) -} - -export function getPluginByType( - plugins: PluginsContextPlugins, - type?: EditorPluginType | string -) { - return ( - plugins.find(({ type: pluginType }) => pluginType === type) ?? - plugins.find(({ type: pluginType }) => pluginType === 'unsupported') - ) -} - -export interface PluginsContextPlugin { - type: string - plugin: EditorPlugin | EditorPlugin - visible?: boolean // in plugin suggestions - icon?: JSX.Element -} - -export type PluginsContextPlugins = PluginsContextPlugin[] diff --git a/src/serlo-editor/core/editor.tsx b/src/serlo-editor/core/editor.tsx index 43e056a289..f687b6c7f9 100644 --- a/src/serlo-editor/core/editor.tsx +++ b/src/serlo-editor/core/editor.tsx @@ -5,11 +5,6 @@ import { HotkeysProvider, useHotkeys } from 'react-hotkeys-hook' import { Provider } from 'react-redux' import { EditableContext, PreferenceContextProvider } from './contexts' -import { - PluginsContext, - PluginsContextPlugins, - usePlugins, -} from './contexts/plugins-context' import { useBlurOnOutsideClick } from './hooks/use-blur-on-outside-click' import { SubDocument } from './sub-document' import { @@ -30,16 +25,13 @@ import { * Renders a single editor for an Serlo Editor document */ export function Editor(props: EditorProps) { - const { plugins, ...propsWithoutPlugins } = props return ( - - - + @@ -51,10 +43,9 @@ export function InnerDocument({ editable = true, onChange, ...props -}: Omit) { +}: EditorProps) { const id = useAppSelector(selectRoot) const dispatch = useAppDispatch() - const plugins = usePlugins() const wrapperRef = useRef(null) useBlurOnOutsideClick(wrapperRef) @@ -74,16 +65,9 @@ export function InnerDocument({ }) }, [onChange]) - const strippedPlugins = plugins - useEffect(() => { - dispatch( - runInitRootSaga({ - initialState: props.initialState, - plugins: strippedPlugins, - }) - ) - }, [props.initialState, strippedPlugins, dispatch]) + dispatch(runInitRootSaga({ initialState: props.initialState })) + }, [props.initialState, dispatch]) const editableContextValue = useMemo(() => editable, [editable]) useHotkeys( @@ -175,7 +159,6 @@ export function InnerDocument({ export interface EditorProps { children?: ReactNode | ((document: ReactNode) => ReactNode) - plugins: PluginsContextPlugins initialState: { plugin: string state?: unknown diff --git a/src/serlo-editor/core/hooks/use-blur-on-outside-click.ts b/src/serlo-editor/core/hooks/use-blur-on-outside-click.ts index e61e22d6ec..07c0cb5d56 100644 --- a/src/serlo-editor/core/hooks/use-blur-on-outside-click.ts +++ b/src/serlo-editor/core/hooks/use-blur-on-outside-click.ts @@ -11,15 +11,15 @@ export function useBlurOnOutsideClick( const dispatch = useAppDispatch() useEffect(() => { - // If neither the provided wrapper element nor its children were clicked, - // reset the internal editor focus state function handleClickOutside(event: MouseEvent) { + const clickedElement = event.target as Element if ( - document.body.contains(event.target as Node) && - editorWrapperRef.current && - !editorWrapperRef.current.contains(event.target as Node) + document.body.contains(clickedElement) && // clicked element is present in the document + editorWrapperRef.current && // provided wrapper is defined + !editorWrapperRef.current.contains(clickedElement) && // clicked element is not a child of the provided wrapper + !clickedElement.closest('.ReactModalPortal') // clicked element is not a part of a modal ) { - dispatch(focus(null)) + dispatch(focus(null)) // reset the focus state (blur the editor) } } diff --git a/src/serlo-editor/core/sub-document/editor.tsx b/src/serlo-editor/core/sub-document/editor.tsx index d6a4b9b3f3..3c53ee0154 100644 --- a/src/serlo-editor/core/sub-document/editor.tsx +++ b/src/serlo-editor/core/sub-document/editor.tsx @@ -11,29 +11,25 @@ import { selectIsFocused, useAppSelector, useAppDispatch, + selectParent, + store, } from '../../store' import { StateUpdater } from '../../types/internal__plugin-state' -import { usePlugin, usePlugins } from '../contexts/plugins-context' -import { DocumentEditor } from '@/serlo-editor/editor-ui/document-editor' -import { EditorPlugin } from '@/serlo-editor/types/internal__plugin' +import { SideToolbarAndWrapper } from '@/serlo-editor/editor-ui/side-toolbar-and-wrapper' +import { editorPlugins } from '@/serlo-editor/plugin/helpers/editor-plugins' export function SubDocumentEditor({ id, pluginProps }: SubDocumentProps) { - const [hasSettings, setHasSettings] = useState(false) - const [hasToolbar, setHasToolbar] = useState(false) + const [hasSideToolbar, setHasSideToolbar] = useState(false) const dispatch = useAppDispatch() const document = useAppSelector((state) => selectDocument(state, id)) const focused = useAppSelector((state) => selectIsFocused(state, id)) - const plugins = usePlugins() - const plugin = usePlugin(document?.plugin)?.plugin as EditorPlugin - useEnableEditorHotkeys(id, plugin, focused) + const plugin = editorPlugins.getByType(document?.plugin ?? '') - const container = useRef(null) - const settingsRef = useRef( - window.document.createElement('div') - ) - const toolbarRef = useRef( + useEnableEditorHotkeys(id, plugin, focused) + const containerRef = useRef(null) + const sideToolbarRef = useRef( window.document.createElement('div') ) const autofocusRef = useRef(null) @@ -51,12 +47,12 @@ export function SubDocumentEditor({ id, pluginProps }: SubDocumentProps) { useEffect(() => { if ( focused && - container.current && + containerRef.current && document && plugin && !plugin.state.getFocusableChildren(document.state).length ) { - container.current.focus() + containerRef.current.focus() } // `document` should not be part of the dependencies because we only want to call this once when the document gets focused // eslint-disable-next-line react-hooks/exhaustive-deps @@ -66,40 +62,30 @@ export function SubDocumentEditor({ id, pluginProps }: SubDocumentProps) { (e: React.MouseEvent) => { // Find closest document const target = (e.target as HTMLDivElement).closest('[data-document]') - - if (!focused && target === container.current) { - dispatch(focus(id)) + if (!focused && target === containerRef.current) { + if (document?.plugin === 'rows') { + const parent = selectParent(store.getState(), id) + if (parent) dispatch(focus(parent.id)) + } else { + dispatch(focus(id)) + } } }, - [focused, id, dispatch] + [focused, id, dispatch, document] ) - const renderIntoSettings = useCallback( + const renderIntoSideToolbar = useCallback( (children: React.ReactNode) => { return ( - {children} - + ) }, - [settingsRef] - ) - - const renderIntoToolbar = useCallback( - (children: React.ReactNode) => { - return ( - - {children} - - ) - }, - [toolbarRef] + [sideToolbarRef] ) return useMemo(() => { @@ -132,7 +118,6 @@ export function SubDocumentEditor({ id, pluginProps }: SubDocumentProps) { dispatch( runChangeDocumentSaga({ id, - plugins, state: { initial, executor: additional.executor, @@ -146,26 +131,28 @@ export function SubDocumentEditor({ id, pluginProps }: SubDocumentProps) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const state = plugin.state.init(document.state, onChange) + const isInlineChildEditor = + Object.hasOwn(config, 'isInlineChildEditor') && + (config.isInlineChildEditor as boolean) + return (
- - +
) }, [ document, - plugins, plugin, pluginProps, handleFocus, - hasSettings, - hasToolbar, + hasSideToolbar, focused, - renderIntoSettings, - renderIntoToolbar, + renderIntoSideToolbar, id, dispatch, ]) } -function RenderIntoSettings({ - children, - setHasSettings, - settingsRef, -}: { - children: React.ReactNode - setHasSettings: (value: boolean) => void - settingsRef: React.MutableRefObject -}) { - useEffect(() => { - setHasSettings(true) - }) - if (!settingsRef.current) return null - return createPortal(<>{children}, settingsRef.current) -} - -function RenderIntoToolbar({ +function RenderIntoSideToolbar({ children, - setHasToolbar, - toolbarRef, + setHasSideToolbar, + sideToolbarRef, }: { children: React.ReactNode - setHasToolbar: (value: boolean) => void - toolbarRef: React.MutableRefObject + setHasSideToolbar: (value: boolean) => void + sideToolbarRef: React.MutableRefObject }) { useEffect(() => { - setHasToolbar(true) + setHasSideToolbar(true) }) - if (!toolbarRef.current) return null - return createPortal(children, toolbarRef.current) + if (!sideToolbarRef.current) return null + return createPortal(children, sideToolbarRef.current) } diff --git a/src/serlo-editor/core/sub-document/renderer.tsx b/src/serlo-editor/core/sub-document/renderer.tsx index 9b02017dd2..b306e29c9a 100644 --- a/src/serlo-editor/core/sub-document/renderer.tsx +++ b/src/serlo-editor/core/sub-document/renderer.tsx @@ -3,12 +3,12 @@ import { useRef } from 'react' import { SubDocumentProps } from '.' import { selectDocument, useAppSelector } from '../../store' -import { usePlugin } from '../contexts/plugins-context' -import { EditorPlugin } from '@/serlo-editor/types/internal__plugin' +import { editorPlugins } from '@/serlo-editor/plugin/helpers/editor-plugins' export function SubDocumentRenderer({ id, pluginProps }: SubDocumentProps) { const document = useAppSelector((state) => selectDocument(state, id)) - const plugin = usePlugin(document?.plugin)?.plugin as EditorPlugin + const plugin = editorPlugins.getByType(document?.plugin ?? '') + const focusRef = useRef(null) if (!document) return null if (!plugin) { @@ -35,8 +35,7 @@ export function SubDocumentRenderer({ id, pluginProps }: SubDocumentProps) { editable={false} focused={false} autofocusRef={focusRef} - renderIntoSettings={() => null} - renderIntoToolbar={() => null} + renderIntoSideToolbar={() => null} /> ) } diff --git a/src/serlo-editor/core/sub-document/use-enable-editor-hotkeys.tsx b/src/serlo-editor/core/sub-document/use-enable-editor-hotkeys.tsx index ff2a331422..e141cc9729 100644 --- a/src/serlo-editor/core/sub-document/use-enable-editor-hotkeys.tsx +++ b/src/serlo-editor/core/sub-document/use-enable-editor-hotkeys.tsx @@ -15,7 +15,6 @@ import { useAppSelector, selectIsDocumentEmpty, } from '../../store' -import { usePlugins } from '../contexts/plugins-context' import { EditorPlugin } from '@/serlo-editor/plugin' export const useEnableEditorHotkeys = ( @@ -24,7 +23,6 @@ export const useEnableEditorHotkeys = ( isFocused: boolean ) => { const dispatch = useAppDispatch() - const plugins = usePlugins() const isDocumentEmpty = useAppSelector((state) => selectIsDocumentEmpty(state, id) ) @@ -91,7 +89,6 @@ export const useEnableEditorHotkeys = ( insertPluginChildAfter({ parent: parent.id, sibling: id, - plugins, }) ) }) @@ -117,7 +114,7 @@ export const useEnableEditorHotkeys = ( } else if (e.key === 'Delete') { dispatch(focusNext(selectFocusTree(store.getState()))) } - dispatch(removePluginChild({ parent: parent.id, child: id, plugins })) + dispatch(removePluginChild({ parent: parent.id, child: id })) } }) } diff --git a/src/serlo-editor/editor-ui/document-editor.tsx b/src/serlo-editor/editor-ui/document-editor.tsx deleted file mode 100644 index e88ce0e442..0000000000 --- a/src/serlo-editor/editor-ui/document-editor.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import { faCog } from '@fortawesome/free-solid-svg-icons' -import { useState, useMemo, useRef } from 'react' -import styled from 'styled-components' - -import { edtrClose, EdtrIcon } from '.' -import { - OverlayButton, - PluginToolbarOverlayButton, -} from '../plugin/plugin-toolbar' -import { FaIcon } from '@/components/fa-icon' -import { useEditorStrings } from '@/contexts/logged-in-data-context' - -export interface DocumentEditorProps { - children: React.ReactNode // The rendered document - settingsRef: React.RefObject // The rendered settings - toolbarRef: React.RefObject // The rendered toolbar buttons - hasSettings: boolean // `true` if the document has rendered any settings - hasToolbar: boolean // `true` if the document has rendered any toolbar buttons - renderSettings?( // Render prop to override rendering of settings - children: React.ReactNode, // the rendered settings - { close }: { close(): void } - ): React.ReactNode // returns the newly rendered settings - renderToolbar?(children: React.ReactNode): React.ReactNode // Render prop to override rendering of toolbar - focused: boolean // `true` if the document is focused -} - -// @TODO Rename this to `PluginSettings` or something similar after moving the toolbar out of it. -export function DocumentEditor({ - focused, - children, - renderSettings, - renderToolbar, - settingsRef, - toolbarRef, - hasSettings, - hasToolbar, -}: DocumentEditorProps) { - const [hasHover, setHasHover] = useState(false) - const editorStrings = useEditorStrings() - - const shouldShowSettings = showSettings() - const renderSettingsContent = useMemo(() => { - return shouldShowSettings - ? (children, { close }) => ( - <> -
-

{editorStrings.edtrIo.extendedSettings}

- { - close() - }} - label={editorStrings.edtrIo.close} - > - - -
- {renderSettings?.(children, { close }) || children} - - ) - : undefined - }, [renderSettings, shouldShowSettings, editorStrings]) - - const isFocused = focused && (showSettings() || showToolbar()) - const isHovered = hasHover && (showSettings() || showToolbar()) - - const isAppended = useRef(false) - const toolbar = ( - <> - {showSettings() ? ( - } - renderContent={renderSettingsContent} - contentRef={settingsRef} - /> - ) : null} -
{ - // The ref `isAppended` ensures that we only append the content once - // so that we don't lose focus on every render - if (ref && toolbarRef.current && !isAppended.current) { - isAppended.current = true - ref.appendChild(toolbarRef.current) - } else if (!showSettings()) { - isAppended.current = false - } - }} - /> - - ) - - return ( - setHasHover(true)} - onMouseLeave={() => setHasHover(false)} - > - {children} - - - {renderToolbar ? renderToolbar(toolbar) : toolbar} - - - - ) - - function showSettings(): boolean { - return ( - hasSettings || - (renderSettings !== undefined && - renderSettings(null, { - close() { - // noop - }, - }) !== null) - ) - } - - function showToolbar(): boolean { - return hasToolbar || renderToolbar !== undefined - } -} - -interface ToolbarProps { - isFocused: boolean - isHovered: boolean -} - -const ToolbarContent = styled.div(({ isFocused, isHovered }) => ({ - backgroundColor: '#fff', - borderRadius: '5px 0 0 5px', - marginRight: '2px', - paddingBottom: '10px', - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-end', - opacity: isFocused ? 1 : isHovered ? 0.7 : 0, - zIndex: 16, - position: 'relative', - transition: '250ms opacity ease-in-out', -})) - -const ToolbarContainer = styled.div( - ({ isFocused, isHovered }) => ({ - position: 'absolute', - top: 0, - left: 0, - transformOrigin: 'center top', - transform: 'translateX(-100%)', - pointerEvents: isFocused || isHovered ? 'all' : 'none', - zIndex: isHovered ? '21' : 'auto', - '&::before': { - position: 'absolute', - pointerEvents: 'none', - top: 0, - right: 0, - content: '""', - opacity: 1, - height: '100%', - width: '2px', - zIndex: 15, - }, - }) -) - -const Container = styled.div>( - ({ isFocused, isHovered }) => ({ - minHeight: '10px', - marginBottom: '25px', - marginLeft: '-7px', - position: 'relative', - borderLeft: '2px solid transparent', - paddingLeft: '5px', - transition: '250ms all ease-in-out', - - ...(isFocused || isHovered - ? { - borderColor: isFocused ? '#333' : '#eee', - paddingTop: 0, - paddingBottom: 0, - } - : {}), - - ...(!isFocused && isHovered - ? { - [`&:hover:has(.default-document-editor-container:hover) > ${ToolbarContainer} > ${ToolbarContent}`]: - { - opacity: 0, - borderColor: 'transparent', - }, - } - : {}), - }) -) - -const Header = styled.div({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', -}) - -const H4 = styled.h4({ - marginRight: '25px', -}) - -const BorderlessOverlayButton = styled.button({ - border: 'none !important', - padding: '0 !important', - minWidth: '0 !important', -}) diff --git a/src/serlo-editor/editor-ui/editor-bottom-toolbar.tsx b/src/serlo-editor/editor-ui/editor-bottom-toolbar.tsx deleted file mode 100644 index a1e61e265a..0000000000 --- a/src/serlo-editor/editor-ui/editor-bottom-toolbar.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import styled from 'styled-components' - -import { legacyEditorTheme } from '@/helper/colors' - -export const EditorBottomToolbar = styled.div({ - boxShadow: '0 2px 4px 0 rgba(0,0,0,0.50)', - backgroundColor: legacyEditorTheme.backgroundColor, - color: legacyEditorTheme.color, - borderRadius: '4px', - position: 'fixed', - left: '50%', - transform: 'translate(-50%,-50%)', - bottom: '0', - zIndex: 95, - whiteSpace: 'nowrap', -}) diff --git a/src/serlo-editor/editor-ui/editor-button.tsx b/src/serlo-editor/editor-ui/editor-button.tsx deleted file mode 100644 index cf1a4530b3..0000000000 --- a/src/serlo-editor/editor-ui/editor-button.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import styled from 'styled-components' - -import { legacyEditorTheme } from '@/helper/colors' - -export const EditorButton = styled.button(() => { - return { - margin: '3px', - backgroundColor: legacyEditorTheme.backgroundColor, - outline: 'none', - border: 'none', - boxShadow: '0 1px 2px 0 rgba(0,0,0,0.50)', - color: legacyEditorTheme.color, - borderRadius: '4px', - cursor: 'pointer', - '&:hover': { - backgroundColor: legacyEditorTheme.primary.background, - color: legacyEditorTheme.color, - borderColor: legacyEditorTheme.primary.background, - }, - } -}) diff --git a/src/serlo-editor/editor-ui/editor-input.tsx b/src/serlo-editor/editor-ui/editor-input.tsx index 014dce3264..94af01d394 100644 --- a/src/serlo-editor/editor-ui/editor-input.tsx +++ b/src/serlo-editor/editor-ui/editor-input.tsx @@ -1,31 +1,6 @@ import { forwardRef } from 'react' -import styled from 'styled-components' -import { colors } from '@/helper/colors' - -const Label = styled.label<{ width: string | undefined }>(({ width }) => { - return { - width, - color: colors.almostBlack, - } -}) - -const Input = styled.input<{ textWidth: string | undefined }>( - ({ textWidth }) => { - return { - backgroundColor: colors.editorPrimary100, - width: textWidth, - borderRadius: '0.8rem', - border: `2px solid ${colors.editorPrimary100}`, - color: colors.almostBlack, - padding: '3px 10px', - '&:focus': { - outline: 'none', - border: `2px solid ${colors.editorPrimary}`, - }, - } - } -) +import { tw } from '@/helper/tw' interface EditorInputProps extends React.DetailedHTMLProps< @@ -39,11 +14,22 @@ interface EditorInputProps export const EditorInput = forwardRef( function EditorInput({ label, ...props }, ref) { + const inputProps = { ...props } + delete inputProps.inputWidth + return ( - ) } ) diff --git a/src/serlo-editor/editor-ui/editor-textarea.tsx b/src/serlo-editor/editor-ui/editor-textarea.tsx index 00c9e577f2..e9833a8584 100644 --- a/src/serlo-editor/editor-ui/editor-textarea.tsx +++ b/src/serlo-editor/editor-ui/editor-textarea.tsx @@ -1,35 +1,28 @@ -import { forwardRef } from 'react' -import TextareaAutosize, { - TextareaAutosizeProps, -} from 'react-textarea-autosize' +import clsx from 'clsx' +import { TextareaHTMLAttributes, forwardRef } from 'react' -interface EditorTextareaProps - extends Omit { +type EditorTextareaProps = TextareaHTMLAttributes & { onMoveOutRight?(): void onMoveOutLeft?(): void + className?: string } export const EditorTextarea = forwardRef< HTMLTextAreaElement, EditorTextareaProps ->(function EditorTextarea({ onMoveOutLeft, onMoveOutRight, ...props }, ref) { +>(function EditorTextarea( + { onMoveOutLeft, onMoveOutRight, className, ...props }, + ref +) { return ( - { + onKeyDown={(e) => { if (!ref || typeof ref === 'function' || !ref.current) return const { selectionStart, selectionEnd, value } = ref.current diff --git a/src/serlo-editor/editor-ui/editor-tooltip.tsx b/src/serlo-editor/editor-ui/editor-tooltip.tsx index 55aec826be..3e9b23ce25 100644 --- a/src/serlo-editor/editor-ui/editor-tooltip.tsx +++ b/src/serlo-editor/editor-ui/editor-tooltip.tsx @@ -27,7 +27,7 @@ export function EditorTooltip({ className )} > - + {text} {hotkeys ? ( {hotkeysTranslated} diff --git a/src/serlo-editor/editor-ui/exercises/add-button.tsx b/src/serlo-editor/editor-ui/exercises/add-button.tsx new file mode 100644 index 0000000000..cc5f8e2c7e --- /dev/null +++ b/src/serlo-editor/editor-ui/exercises/add-button.tsx @@ -0,0 +1,21 @@ +import { faPlus } from '@fortawesome/free-solid-svg-icons' + +import { FaIcon } from '@/components/fa-icon' + +interface AddButtonProps { + onClick: () => void + children: string + title?: string +} + +export function AddButton({ title, onClick, children }: AddButtonProps) { + return ( + + ) +} diff --git a/src/serlo-editor/editor-ui/exercises/interactive-answer-component.tsx b/src/serlo-editor/editor-ui/exercises/interactive-answer-component.tsx new file mode 100644 index 0000000000..375d1def20 --- /dev/null +++ b/src/serlo-editor/editor-ui/exercises/interactive-answer-component.tsx @@ -0,0 +1,71 @@ +import { faCircle, faSquare } from '@fortawesome/free-regular-svg-icons' +import { + faCheckCircle, + faCheckSquare, + faTrashAlt, +} from '@fortawesome/free-solid-svg-icons' + +import { FaIcon } from '@/components/fa-icon' +import { useInstanceData } from '@/contexts/instance-context' + +interface InteractiveAnswerProps { + isRadio?: boolean + isActive?: boolean + handleChange: () => void + answerID?: string + feedbackID: string + answer: HTMLInputElement | React.ReactNode + feedback: React.ReactNode + focusedElement?: string + remove: () => void +} + +export function InteractiveAnswer({ + isRadio, + isActive, + answer, + feedback, + remove, + handleChange, +}: InteractiveAnswerProps) { + const { strings } = useInstanceData() + + const icon = isRadio + ? isActive + ? faCheckCircle + : faCircle + : isActive + ? faCheckSquare + : faSquare + + return ( +
+
+ {strings.content.exercises.correct}? + +
+
+
+ + <>{answer} +
+ +
+ + {feedback} +
+
+
+ ) +} diff --git a/src/serlo-editor/editor-ui/hover-overlay-old.tsx b/src/serlo-editor/editor-ui/hover-overlay-old.tsx deleted file mode 100644 index 206937f284..0000000000 --- a/src/serlo-editor/editor-ui/hover-overlay-old.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { createRef, useEffect, useState, ReactNode, RefObject } from 'react' -import styled from 'styled-components' - -const OverlayTriangle = styled.div<{ positionAbove?: boolean }>( - ({ positionAbove = false }) => { - const borderPosition = positionAbove ? 'borderTop' : 'borderBottom' - return { - position: 'relative', - width: 0, - height: 0, - borderLeft: '5px solid transparent', - borderRight: '5px solid transparent', - [borderPosition]: '10px solid rgba(51,51,51,0.95)', - } - } -) - -const InlineOverlayWrapper = styled.div({ - position: 'absolute', - top: '-10000px', - left: '-10000px', - opacity: 0, - transition: 'opacity 0.5s', - zIndex: 95, - whiteSpace: 'nowrap', -}) - -const InlineOverlayContentWrapper = styled.div({ - boxShadow: '0 2px 4px 0 rgba(0,0,0,0.50)', - backgroundColor: 'rgba(51,51,51,0.95)', - color: '#ffffff', - borderRadius: '4px', - '& a': { - color: '#ffffff', - '&:hover': { - color: 'rgb(70, 155, 255)', - }, - }, -}) - -export type HoverPosition = 'above' | 'below' - -interface HoverOverlayProps { - children: ReactNode - position: HoverPosition - anchor?: RefObject -} - -// TODO: Once all redesign tasks are done, delete this component -export function HoverOverlayOld(props: HoverOverlayProps) { - const overlay = createRef() - const triangle = createRef() - const [positionAbove, setPositionAbove] = useState(props.position === 'above') - - const windowSelection = window.getSelection() - - const [nativeSelection, setNativeSelection] = useState({ - anchorOffset: windowSelection?.anchorOffset, - focusNode: windowSelection?.focusNode, - }) - const handleSelectionChange = () => { - setNativeSelection({ - anchorOffset: windowSelection?.anchorOffset, - focusNode: windowSelection?.focusNode, - }) - } - document.addEventListener('selectionchange', handleSelectionChange) - useEffect(() => () => { - document.removeEventListener('selectionchange', handleSelectionChange) - }) - - const { anchor, children } = props - - useEffect(() => { - if (!overlay.current || !triangle.current) return - let rect - if (anchor && anchor.current !== null) { - rect = anchor.current.getBoundingClientRect() - } else if (windowSelection && windowSelection.rangeCount > 0) { - const range = windowSelection.getRangeAt(0) - rect = range.getBoundingClientRect() - } - if (!rect) return - if (rect.height === 0) return - const menu = overlay.current - // menu is set to display:none, shouldn't ever happen - if (!menu.offsetParent) return - const parentRect = menu.offsetParent.getBoundingClientRect() - menu.style.opacity = '1' - const aboveValue = rect.top - menu.offsetHeight - 6 - // if top becomes negative, place menu below - setPositionAbove(positionAbove && aboveValue >= 0) - menu.style.top = `${ - (positionAbove ? aboveValue : rect.bottom + 6) - parentRect.top - }px` - - menu.style.left = `${Math.max( - Math.min( - Math.max( - rect.left - parentRect.left - menu.offsetWidth / 2 + rect.width / 2, - 0 - ), - parentRect.width - menu.offsetWidth - 5 - ), - 0 - )}px` - triangle.current.style.left = `${ - rect.left - - menu.offsetLeft - - parentRect.left - - triangle.current.offsetWidth / 2 + - rect.width / 2 - }px` - }, [ - overlay, - triangle, - anchor, - positionAbove, - nativeSelection.focusNode, - nativeSelection.anchorOffset, - windowSelection, - ]) - - return ( - - {!positionAbove && } - {children} - {positionAbove && } - - ) -} diff --git a/src/serlo-editor/editor-ui/hover-overlay.tsx b/src/serlo-editor/editor-ui/hover-overlay.tsx index fc0a38a142..28f08060ea 100644 --- a/src/serlo-editor/editor-ui/hover-overlay.tsx +++ b/src/serlo-editor/editor-ui/hover-overlay.tsx @@ -1,30 +1,8 @@ import { createRef, useEffect, useState, ReactNode, RefObject } from 'react' -import styled from 'styled-components' -import { colors } from '@/helper/colors' +import { tw } from '@/helper/tw' -const HoverOverlayWrapper = styled.div({ - position: 'absolute', - top: '-10000px', - left: '-10000px', - opacity: 0, - transition: 'opacity 0.5s', - zIndex: 95, - whiteSpace: 'nowrap', - boxShadow: '0 1px 4px rgba(0, 0, 0, 0.25)', - backgroundColor: '#fff', - color: colors.almostBlack, - borderRadius: '3px', - overflow: 'auto', - '& a': { - color: colors.almostBlack, - '&:hover': { - color: 'rgb(70, 155, 255)', - }, - }, -}) - -type HoverPosition = 'above' | 'below' +export type HoverPosition = 'above' | 'below' interface HoverOverlayProps { children: ReactNode @@ -38,21 +16,6 @@ export function HoverOverlay(props: HoverOverlayProps) { const windowSelection = window.getSelection() - const [nativeSelection, setNativeSelection] = useState({ - anchorOffset: windowSelection?.anchorOffset, - focusNode: windowSelection?.focusNode, - }) - const handleSelectionChange = () => { - setNativeSelection({ - anchorOffset: windowSelection?.anchorOffset, - focusNode: windowSelection?.focusNode, - }) - } - document.addEventListener('selectionchange', handleSelectionChange) - useEffect(() => () => { - document.removeEventListener('selectionchange', handleSelectionChange) - }) - const { anchor, children } = props useEffect(() => { @@ -90,14 +53,18 @@ export function HoverOverlay(props: HoverOverlayProps) { ), 0 )}px` - }, [ - overlay, - anchor, - positionAbove, - nativeSelection.focusNode, - nativeSelection.anchorOffset, - windowSelection, - ]) + }, [overlay, anchor, positionAbove, windowSelection]) - return {children} + return ( +
+ {children} +
+ ) } diff --git a/src/serlo-editor/editor-ui/icon.tsx b/src/serlo-editor/editor-ui/icon.tsx index a2a8168f8e..22230cc340 100644 --- a/src/serlo-editor/editor-ui/icon.tsx +++ b/src/serlo-editor/editor-ui/icon.tsx @@ -35,7 +35,5 @@ export const edtrColorText = 'M10.63 3.93L6.06 15.58c-.27.68.23 1.42.97 1.42.43 0 .82-.27.98-.68L8.87 14h6.25l.87 2.32c.15.41.54.68.98.68.73 0 1.24-.74.97-1.42L13.37 3.93C13.14 3.37 12.6 3 12 3c-.6 0-1.15.37-1.37.93zM9.62 12L12 5.67 14.38 12H9.62z' export const edtrBold = 'M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H8c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h5.78c2.07 0 3.96-1.69 3.97-3.77.01-1.53-.85-2.84-2.15-3.44zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z' -export const edtrPlus = - 'M12,2 C6.48,2 2,6.48 2,12 C2,17.52 6.48,22 12,22 C17.52,22 22,17.52 22,12 C22,6.48 17.52,2 12,2 Z M16,13 L13,13 L13,16 C13,16.55 12.55,17 12,17 C11.45,17 11,16.55 11,16 L11,13 L8,13 C7.45,13 7,12.55 7,12 C7,11.45 7.45,11 8,11 L11,11 L11,8 C11,7.45 11.45,7 12,7 C12.55,7 13,7.45 13,8 L13,11 L16,11 C16.55,11 17,11.45 17,12 C17,12.55 16.55,13 16,13 Z' export const edtrDragHandle = 'M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z' diff --git a/src/serlo-editor/editor-ui/index.ts b/src/serlo-editor/editor-ui/index.ts index 6ca835ed85..a00285381e 100644 --- a/src/serlo-editor/editor-ui/index.ts +++ b/src/serlo-editor/editor-ui/index.ts @@ -1,9 +1,7 @@ -export * from './editor-bottom-toolbar' -export * from './editor-button' export * from './editor-input' export * from './editor-textarea' export * from './hover-overlay' -export * from './hover-overlay-old' -export * from './interactive-answer-component' +export * from './exercises/interactive-answer-component' +export * from './exercises/add-button' export * from './preview-overlay' export * from './icon' diff --git a/src/serlo-editor/editor-ui/interactive-answer-component.tsx b/src/serlo-editor/editor-ui/interactive-answer-component.tsx deleted file mode 100644 index c25691adce..0000000000 --- a/src/serlo-editor/editor-ui/interactive-answer-component.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons' -import styled from 'styled-components' - -import { FaIcon } from '@/components/fa-icon' -import { useInstanceData } from '@/contexts/instance-context' -import { colors } from '@/helper/colors' - -interface AddButtonProps { - onClick: () => void - children: string - title?: string -} - -export function AddButton({ title, onClick, children }: AddButtonProps) { - return ( - - {children} - - ) -} - -const AddButtonComponent = styled.button({ - margin: '5px', - width: '96%', - borderRadius: '10px', - backgroundColor: colors.editorPrimary100, - textAlign: 'left', - color: colors.almostBlack, - minHeight: '50px', - padding: '5px', - outline: 'none', - '&:hover': { backgroundColor: colors.editorPrimary200 }, -}) - -const AnswerContainer = styled.div({ - marginBottom: '10px', - display: 'flex', - alignItems: 'center', -}) - -const CheckboxContainer = styled.div({ - width: '10%', - textAlign: 'center', - marginRight: '10px', - fontWeight: 'bold', -}) - -const RemoveButton = styled.button({ - borderRadius: '50%', - outline: 'none', - background: 'white', - zIndex: 20, - float: 'right', - transform: 'translate(50%, -40%)', - '&:hover': { - border: `3px solid ${colors.brand}`, - color: colors.brand, - }, -}) - -const FeedbackField = styled.div({ - paddingLeft: '20px', - paddingBottom: '10px', - paddingTop: '10px', - marginTop: '5px', -}) - -const FramedContainer = styled.div<{ focused: boolean }>(({ focused }) => { - const defaultBorders = { - border: '2px solid lightgrey', - [`${RemoveButton}`]: { - border: '2px solid lightgrey', - color: 'lightgrey', - }, - [`${FeedbackField}`]: { - borderTop: '2px solid lightgrey', - }, - } - const focusedBorders = { - border: `3px solid ${colors.brand}`, - [`${RemoveButton}`]: { - border: `3px solid ${colors.brand}`, - color: colors.brand, - }, - [`${FeedbackField}`]: { - borderTop: `2px solid ${colors.brand}`, - }, - } - - return { - width: '100%', - marginLeft: '10px', - borderRadius: '10px', - - ...(focused ? focusedBorders : defaultBorders), - '&:focus-within': focusedBorders, - } -}) -const AnswerField = styled.div({ paddingLeft: '20px', paddingTop: '10px' }) - -const Container = styled.div<{ isRadio: boolean; checked: boolean }>( - ({ isRadio, checked }) => { - return { - cursor: 'pointer', - border: checked - ? isRadio - ? `5px solid ${colors.brand}` - : `2px solid ${colors.brand}` - : '2px solid lightgray', - borderRadius: isRadio ? '50%' : '15%', - width: '20px', - height: '20px', - display: 'inline-block', - verticalAlign: 'middle', - backgroundColor: checked && !isRadio ? colors.brand : 'white', - } - } -) - -const Tick = styled.div<{ checked: boolean }>(({ checked }) => { - return { - opacity: checked ? 1 : 0, - content: '', - position: 'absolute', - - fontWeight: 'bold', - width: '15px', - height: '10px', - border: '3px solid white', - borderTop: 'none', - borderRight: 'none', - borderRadius: '2px', - zIndex: 10, - transform: 'rotate(-45deg)', - } -}) - -export function CheckElement({ - isRadio, - isActive, - handleChange, -}: CheckElementProps) { - return ( - { - handleChange(e) - }} - > - {isRadio ? null : } - - ) -} - -const BlockLabel = styled.label({ - display: 'block', -}) - -export function InteractiveAnswer(props: InteractiveAnswerProps) { - const { strings } = useInstanceData() - - return ( - - - Richtig? - - - - - <> - {strings.content.exercises.answer}: - {props.answer} - - - - - - - {strings.content.exercises.feedback}: - {props.feedback} - - - - ) -} - -interface InteractiveAnswerProps { - isRadio?: boolean - isActive?: boolean - handleChange: () => void - answerID?: string - feedbackID: string - answer: HTMLInputElement | React.ReactNode - feedback: React.ReactNode - focusedElement?: string - remove: () => void -} - -interface CheckElementProps { - isRadio: boolean - isActive: boolean - handleChange: (event: React.MouseEvent) => void -} diff --git a/src/serlo-editor/editor-ui/plugin-toolbar/index.ts b/src/serlo-editor/editor-ui/plugin-toolbar/index.ts new file mode 100644 index 0000000000..ca94f54b7a --- /dev/null +++ b/src/serlo-editor/editor-ui/plugin-toolbar/index.ts @@ -0,0 +1,3 @@ +import { PluginToolbar } from './plugin-toolbar' + +export { PluginToolbar } diff --git a/src/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/anchor-link-copy-tool.tsx b/src/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/anchor-link-copy-tool.tsx new file mode 100644 index 0000000000..ed79ed1ec2 --- /dev/null +++ b/src/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/anchor-link-copy-tool.tsx @@ -0,0 +1,44 @@ +import { faHashtag } from '@fortawesome/free-solid-svg-icons' + +import { DropdownButton } from './dropdown-button' +import { shouldUseFeature } from '@/components/user/profile-experimental' +import { useEntityId } from '@/contexts/entity-id-context' +import { useInstanceData } from '@/contexts/instance-context' +import { useEditorStrings } from '@/contexts/logged-in-data-context' +import { showToastNotice } from '@/helper/show-toast-notice' + +interface AnchorLinkCopyToolProps { + pluginId: string +} + +export function AnchorLinkCopyTool({ pluginId }: AnchorLinkCopyToolProps) { + const serloEntityId = useEntityId() + const editorStrings = useEditorStrings() + const { strings } = useInstanceData() + + if ( + !serloEntityId || + !navigator.clipboard || + !window.location.href.includes('add-revision') || + !shouldUseFeature('editorAnchorLinkCopyTool') + ) { + return null + } + + return ( + { + const url = `https://serlo.org/${serloEntityId}#${ + pluginId.split('-')[0] + }` + void navigator.clipboard.writeText(url) + showToastNotice(strings.share.copySuccess, 'success') + showToastNotice('👉 ' + editorStrings.edtrIo.anchorLinkWarning) + }} + label={editorStrings.plugins.rows.copyAnchorLink} + icon={faHashtag} + className="mt-2.5 border-t pt-2.5" + dataQa="copy-anchor-link-button" + /> + ) +} diff --git a/src/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/dropdown-button.tsx b/src/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/dropdown-button.tsx new file mode 100644 index 0000000000..fe2cac5789 --- /dev/null +++ b/src/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/dropdown-button.tsx @@ -0,0 +1,32 @@ +import { IconDefinition } from '@fortawesome/free-solid-svg-icons' +import clsx from 'clsx' + +import { FaIcon } from '@/components/fa-icon' + +interface DropdownButtonProps { + onClick: () => void + label: string + icon: IconDefinition + className?: string + dataQa?: string +} + +export function DropdownButton({ + onClick, + label, + icon, + className, + dataQa, +}: DropdownButtonProps) { + return ( + + ) +} diff --git a/src/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools.tsx b/src/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools.tsx new file mode 100644 index 0000000000..93e800c228 --- /dev/null +++ b/src/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools.tsx @@ -0,0 +1,87 @@ +import { faClone, faTrashAlt } from '@fortawesome/free-solid-svg-icons' +import { useCallback } from 'react' + +import { AnchorLinkCopyTool } from './anchor-link-copy-tool' +import { DropdownButton } from './dropdown-button' +import { useEditorStrings } from '@/contexts/logged-in-data-context' +import { + insertPluginChildAfter, + removePluginChild, + selectParent, + selectSerializedDocumentWithoutIds, + store, + useAppDispatch, +} from '@/serlo-editor/store' +import { EditorPluginType } from '@/serlo-editor-integration/types/editor-plugin-type' + +interface PluginDefaultToolsProps { + pluginId: string +} + +// tools for most plugins (duplicate / remove) +export function PluginDefaultTools({ pluginId }: PluginDefaultToolsProps) { + const dispatch = useAppDispatch() + const pluginStrings = useEditorStrings().plugins + + const handleDuplicatePlugin = useCallback(() => { + const parent = selectParent(store.getState(), pluginId) + if (!parent) return + + const document = selectSerializedDocumentWithoutIds( + store.getState(), + pluginId + ) + if (!document) return + + dispatch( + insertPluginChildAfter({ + parent: parent.id, + sibling: pluginId, + document, + }) + ) + }, [dispatch, pluginId]) + + const handleRemovePlugin = useCallback(() => { + const parent = selectParent(store.getState(), pluginId) + if (!parent || !parent.children?.length) return + + const isOnlyChild = parent.children?.length === 1 + + // make sure rows plugin is not empty + if (isOnlyChild) { + dispatch( + insertPluginChildAfter({ + parent: parent.id, + sibling: pluginId, + document: { plugin: EditorPluginType.Text }, + }) + ) + } + + dispatch( + removePluginChild({ + parent: parent.id, + child: pluginId, + }) + ) + }, [dispatch, pluginId]) + + return ( + <> + + + + + ) +} diff --git a/src/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-tool-menu.tsx b/src/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-tool-menu.tsx new file mode 100644 index 0000000000..1f39de68c3 --- /dev/null +++ b/src/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-tool-menu.tsx @@ -0,0 +1,37 @@ +import { faEllipsis } from '@fortawesome/free-solid-svg-icons' +import { + Content, + Item, + List, + Root, + Trigger, +} from '@radix-ui/react-navigation-menu' +import { ReactElement } from 'react' + +import { FaIcon } from '@/components/fa-icon' + +interface PluginToolMenuProps { + pluginControls: ReactElement +} + +export function PluginToolMenu({ pluginControls }: PluginToolMenuProps) { + return ( + + + + + + + + + +
+ {pluginControls} +
+
+
+
+
+
+ ) +} diff --git a/src/serlo-editor/editor-ui/plugin-toolbar/plugin-toolbar.tsx b/src/serlo-editor/editor-ui/plugin-toolbar/plugin-toolbar.tsx new file mode 100644 index 0000000000..91253c9a1c --- /dev/null +++ b/src/serlo-editor/editor-ui/plugin-toolbar/plugin-toolbar.tsx @@ -0,0 +1,70 @@ +import clsx from 'clsx' +import { ReactElement } from 'react' + +import { PluginToolMenu } from './plugin-tool-menu/plugin-tool-menu' +import { useEditorStrings } from '@/contexts/logged-in-data-context' +import { tw } from '@/helper/tw' +import { getPluginTitle } from '@/serlo-editor/plugin/helpers/get-plugin-title' +import { EditorPluginType } from '@/serlo-editor-integration/types/editor-plugin-type' + +interface PluginToolbarProps { + pluginType: EditorPluginType | string + contentControls?: ReactElement + pluginSettings?: ReactElement + pluginControls?: ReactElement + className?: string +} + +export function PluginToolbar({ + pluginType, + contentControls, + pluginSettings, + pluginControls, + className, +}: PluginToolbarProps) { + const pluginStrings = useEditorStrings().plugins + + return ( +
+ {/* Content controls */} +
+ {contentControls} +
+
+ +
+ {/* Plugin type indicator */} +
+ {getPluginTitle(pluginStrings, pluginType)} +
+ + {pluginSettings ? ( + <> + {/* Separator */} +
+ {pluginSettings} + + ) : null} + + {/* Separator */} +
+ + {/* Plugin controls dropdown menu */} + {pluginControls ? ( + + ) : null} +
+
+ ) +} diff --git a/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/color-text-icon.tsx b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/color-text-icon.tsx new file mode 100644 index 0000000000..8c9ccfd1de --- /dev/null +++ b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/color-text-icon.tsx @@ -0,0 +1,17 @@ +import { edtrColorText, EdtrIcon } from '@/serlo-editor/editor-ui' + +interface ColorTextIconProps { + color: string +} + +export const ColorTextIcon = ({ color }: ColorTextIconProps) => ( + + + + + + +) diff --git a/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/const.ts b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/const.ts new file mode 100644 index 0000000000..cdf62ae240 --- /dev/null +++ b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/const.ts @@ -0,0 +1,6 @@ +import { articleColors } from '@/helper/colors' + +export const textColors = Object.entries(articleColors).map(([key, value]) => ({ + value, + name: key.charAt(0).toUpperCase() + key.slice(1), +})) diff --git a/src/serlo-editor/plugins/text/hooks/use-formatting-options.tsx b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/hooks/use-formatting-options.tsx similarity index 88% rename from src/serlo-editor/plugins/text/hooks/use-formatting-options.tsx rename to src/serlo-editor/editor-ui/plugin-toolbar/text-controls/hooks/use-formatting-options.tsx index 3a174b2d23..1724443bb8 100644 --- a/src/serlo-editor/plugins/text/hooks/use-formatting-options.tsx +++ b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/hooks/use-formatting-options.tsx @@ -4,15 +4,9 @@ import isHotkey from 'is-hotkey' import React, { useCallback, useMemo } from 'react' import { Node, Editor as SlateEditor } from 'slate' -import { textColors } from './use-text-config' -import { HoveringToolbarColorIcon } from '../components/hovering-toolbar-color-icon' -import { HoveringToolbarColorTextIcon } from '../components/hovering-toolbar-color-text-icon' -import { withLinks, withLists, withMath } from '../plugins' -import { - TextEditorFormattingOption, - ControlButton, - TextEditorPluginConfig, -} from '../types' +import { ColorTextIcon } from '../color-text-icon' +import { textColors } from '../const' +import { TextEditorFormattingOption, ControlButton } from '../types' import { getColorIndex, isAnyColorActive, @@ -56,6 +50,11 @@ import { edtrListNumbered, edtrText, } from '@/serlo-editor/editor-ui' +import { + withLinks, + withLists, + withMath, +} from '@/serlo-editor/plugins/text/plugins' const textPluginsMapper = { [TextEditorFormattingOption.math]: withMath, @@ -115,8 +114,9 @@ const registeredMarkdownShortcuts = [ }, ] -export const useFormattingOptions = (config: TextEditorPluginConfig) => { - const { formattingOptions } = config +export const useFormattingOptions = ( + formattingOptions: TextEditorFormattingOption[] +) => { const { strings } = useInstanceData() const textStrings = useEditorStrings().plugins.text @@ -135,8 +135,9 @@ export const useFormattingOptions = (config: TextEditorPluginConfig) => { ) const toolbarControls: ControlButton[] = useMemo( - () => createToolbarControls(config, textStrings, strings.keys.ctrl), - [config, strings, textStrings] + () => + createToolbarControls(formattingOptions, textStrings, strings.keys.ctrl), + [formattingOptions, strings, textStrings] ) const handleHotkeys = useCallback( @@ -203,6 +204,7 @@ export const useFormattingOptions = (config: TextEditorPluginConfig) => { return { createTextEditor, toolbarControls, + textColors, handleHotkeys, handleMarkdownShortcuts, handleListsShortcuts, @@ -210,7 +212,7 @@ export const useFormattingOptions = (config: TextEditorPluginConfig) => { } function createToolbarControls( - { formattingOptions }: TextEditorPluginConfig, + formattingOptions: TextEditorFormattingOption[], textStrings: LoggedInData['strings']['editor']['plugins']['text'], ctrlKey: string ): ControlButton[] { @@ -247,12 +249,14 @@ function createToolbarControls( isActive: isAnyHeadingActive, renderIcon: () => , renderCloseMenuIcon: () => , - children: ([1, 2, 3] as const).map((level) => ({ + subMenuButtons: ([1, 2, 3] as const).map((level) => ({ name: TextEditorFormattingOption.headings, title: `${textStrings.heading} ${level}`, isActive: isHeadingActive(level), onClick: toggleHeading(level), - renderIcon: () => H{level}, + renderIcon: () => ( + H{level} + ), })), }, // Colors @@ -264,16 +268,18 @@ function createToolbarControls( renderIcon: (editor: SlateEditor) => { const colorIndex = getColorIndex(editor) const color = colorIndex ? textColors[colorIndex].value : 'black' - return + return }, renderCloseMenuIcon: () => , - children: [ + subMenuButtons: [ { name: TextEditorFormattingOption.colors, title: textStrings.resetColor, isActive: (editor: SlateEditor) => !isAnyColorActive(editor), onClick: resetColor, - renderIcon: () => , + renderIcon: () => ( +
+ ), }, ...textColors.map((color, colorIndex) => ({ name: TextEditorFormattingOption.colors, @@ -284,7 +290,12 @@ function createToolbarControls( : color.name, isActive: isColorActive(colorIndex), onClick: toggleColor(colorIndex), - renderIcon: () => , + renderIcon: () => ( +
+ ), })), ], }, diff --git a/src/serlo-editor/plugins/text/components/hovering-toolbar-button.tsx b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/plugin-toolbar-text-control-button.tsx similarity index 55% rename from src/serlo-editor/plugins/text/components/hovering-toolbar-button.tsx rename to src/serlo-editor/editor-ui/plugin-toolbar/text-controls/plugin-toolbar-text-control-button.tsx index d69892bc64..8d05c6e88c 100644 --- a/src/serlo-editor/plugins/text/components/hovering-toolbar-button.tsx +++ b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/plugin-toolbar-text-control-button.tsx @@ -3,18 +3,23 @@ import { MouseEventHandler } from 'react' import { EditorTooltip } from '@/serlo-editor/editor-ui/editor-tooltip' -export function HoveringToolbarButton({ - active, - children, - tooltipText, - onMouseDown, -}: { +interface PluginToolbarTextControlButtonProps { active?: boolean children: React.ReactNode tooltipText?: string onMouseDown: MouseEventHandler -}) { - const textParts = tooltipText?.split('(') +} + +export function PluginToolbarTextControlButton({ + active, + children, + tooltipText, + onMouseDown, +}: PluginToolbarTextControlButtonProps) { + const [text, hotkeyWithClosingBracket] = tooltipText?.split('(') || [ + undefined, + undefined, + ] return ( diff --git a/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/plugin-toolbar-text-controls.tsx b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/plugin-toolbar-text-controls.tsx new file mode 100644 index 0000000000..567720247f --- /dev/null +++ b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/plugin-toolbar-text-controls.tsx @@ -0,0 +1,91 @@ +import { useState } from 'react' +import { Editor as SlateEditor } from 'slate' + +import { PluginToolbarTextControlButton } from './plugin-toolbar-text-control-button' +import type { NestedControlButton, ControlButton } from './types' + +export interface PluginToolbarTextControlsProps { + controls: ControlButton[] + editor: SlateEditor +} + +function isNestedControlButton( + control: ControlButton +): control is NestedControlButton { + return Object.hasOwn(control, 'subMenuButtons') +} + +export function PluginToolbarTextControls({ + controls, + editor, +}: PluginToolbarTextControlsProps) { + const [subMenu, setSubMenu] = useState() + + const isMath = (control: ControlButton) => + Object.hasOwn(control, 'name') && control.name === 'math' + + const mathActive = controls.find(isMath)?.isActive(editor) + + if (typeof subMenu !== 'number') { + return ( + <> + {controls.map((control, index) => { + if (mathActive && !isMath(control)) return null + return ( + { + event.preventDefault() + event.stopPropagation() + isNestedControlButton(control) + ? setSubMenu(index) + : control.onClick(editor) + }} + key={index} + > + {control.renderIcon(editor)} + + ) + })} + + ) + } + + const activeControl = controls[subMenu] + + if (!isNestedControlButton(activeControl)) return null + + const closeSubMenuControl = { + isActive() { + return false + }, + renderIcon() { + return activeControl.renderCloseMenuIcon() + }, + onClick() { + setSubMenu(undefined) + }, + title: activeControl.closeMenuTitle, + } + const subMenuControls = [...activeControl.subMenuButtons, closeSubMenuControl] + + return ( + <> + {subMenuControls.map((control, index) => ( + { + event.preventDefault() + control.onClick(editor) + setSubMenu(undefined) + }} + key={index} + > + {control.renderIcon(editor)} + + ))} + + ) +} diff --git a/src/serlo-editor/plugins/text/types/hovering-toolbar.ts b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/types.ts similarity index 66% rename from src/serlo-editor/plugins/text/types/hovering-toolbar.ts rename to src/serlo-editor/editor-ui/plugin-toolbar/text-controls/types.ts index 546c0dbc5f..57ac513fbe 100644 --- a/src/serlo-editor/plugins/text/types/hovering-toolbar.ts +++ b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/types.ts @@ -1,6 +1,16 @@ import { Editor as SlateEditor } from 'slate' -import type { TextEditorFormattingOption } from '.' +export enum TextEditorFormattingOption { + code = 'code', + colors = 'colors', + headings = 'headings', + katex = 'katex', + links = 'links', + lists = 'lists', + math = 'math', + paragraphs = 'paragraphs', + richText = 'richText', +} export type ControlButton = ActionControlButton | NestedControlButton @@ -15,7 +25,7 @@ interface ActionControlButton { export interface NestedControlButton { title: string closeMenuTitle: string - children: ActionControlButton[] + subMenuButtons: ActionControlButton[] isActive(editor: SlateEditor): boolean renderIcon(editor: SlateEditor): React.ReactNode renderCloseMenuIcon(): React.ReactNode diff --git a/src/serlo-editor/plugins/text/utils/color.ts b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/color.ts similarity index 100% rename from src/serlo-editor/plugins/text/utils/color.ts rename to src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/color.ts diff --git a/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/document.ts b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/document.ts new file mode 100644 index 0000000000..9e7ee46b50 --- /dev/null +++ b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/document.ts @@ -0,0 +1,17 @@ +import { Element, Editor, Location } from 'slate' + +export function existsInAncestors( + predicate: (element: Element) => boolean, + { location }: { location: Location }, + editor: Editor +) { + const matchingNodes = Array.from( + Editor.nodes(editor, { + at: location, + match: (node) => + !Editor.isEditor(node) && Element.isElement(node) && predicate(node), + }) + ) + + return matchingNodes.length !== 0 +} diff --git a/src/serlo-editor/plugins/text/utils/link.ts b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/link.ts similarity index 95% rename from src/serlo-editor/plugins/text/utils/link.ts rename to src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/link.ts index ac8e2313ca..78682f54ae 100644 --- a/src/serlo-editor/plugins/text/utils/link.ts +++ b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/link.ts @@ -1,7 +1,7 @@ import { Editor as SlateEditor, Element, Node, Range, Transforms } from 'slate' import { selectionHasElement, trimSelection } from './selection' -import type { Link } from '../types' +import type { Link } from '@/serlo-editor/plugins/text' function matchLinks(node: Node) { return Element.isElement(node) && node.type === 'a' diff --git a/src/serlo-editor/plugins/text/utils/list.ts b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/list.ts similarity index 96% rename from src/serlo-editor/plugins/text/utils/list.ts rename to src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/list.ts index 440206d024..7a4c20602e 100644 --- a/src/serlo-editor/plugins/text/utils/list.ts +++ b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/list.ts @@ -3,7 +3,7 @@ import { Element, Editor as SlateEditor } from 'slate' import { ReactEditor } from 'slate-react' import { existsInAncestors } from './document' -import { ListElementType } from '../types' +import { ListElementType } from '@/serlo-editor/plugins/text/types' export function isElementWithinList(element: Element, editor: SlateEditor) { return existsInAncestors( diff --git a/src/serlo-editor/plugins/text/utils/math.ts b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/math.ts similarity index 100% rename from src/serlo-editor/plugins/text/utils/math.ts rename to src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/math.ts diff --git a/src/serlo-editor/plugins/text/utils/rich-text.ts b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/rich-text.ts similarity index 96% rename from src/serlo-editor/plugins/text/utils/rich-text.ts rename to src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/rich-text.ts index a641e70e44..541f801f90 100644 --- a/src/serlo-editor/plugins/text/utils/rich-text.ts +++ b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/rich-text.ts @@ -1,7 +1,7 @@ import { Editor as SlateEditor, Transforms } from 'slate' import { selectionHasElement, trimSelection } from './selection' -import type { Heading } from '../types' +import type { Heading } from '@/serlo-editor/plugins/text' export function isBoldActive(editor: SlateEditor) { return SlateEditor.marks(editor)?.strong === true diff --git a/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/selection.ts b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/selection.ts new file mode 100644 index 0000000000..c4f8aefae6 --- /dev/null +++ b/src/serlo-editor/editor-ui/plugin-toolbar/text-controls/utils/selection.ts @@ -0,0 +1,45 @@ +import { Editor as SlateEditor, Element, Range, Transforms } from 'slate' + +import { existsInAncestors } from './document' + +export function selectionHasElement( + predicate: (element: Element) => boolean, + editor: SlateEditor +) { + if (!editor.selection) return false + + return existsInAncestors( + predicate, + { location: SlateEditor.unhangRange(editor, editor.selection) }, + editor + ) +} + +export function trimSelection(editor: SlateEditor): Partial | null { + const selection = editor.selection + + if (!selection) return null + + let selectedText = SlateEditor.string(editor, selection) + const isBackwardSelection = Range.isBackward(selection) + let anchorOffset = selection.anchor.offset + let focusOffset = selection.focus.offset + + while (selectedText.startsWith(' ')) { + isBackwardSelection ? focusOffset++ : anchorOffset++ + selectedText = selectedText.substring(1) + } + while (selectedText.endsWith(' ')) { + isBackwardSelection ? anchorOffset-- : focusOffset-- + selectedText = selectedText.substring(0, selectedText.length - 1) + } + + const trimmedSelection = { + anchor: { ...selection.anchor, offset: anchorOffset }, + focus: { ...selection.focus, offset: focusOffset }, + } + + Transforms.setSelection(editor, trimmedSelection) + + return trimmedSelection +} diff --git a/src/serlo-editor/editor-ui/preview-overlay.tsx b/src/serlo-editor/editor-ui/preview-overlay.tsx index 36df3ee832..aa8ad4ac47 100644 --- a/src/serlo-editor/editor-ui/preview-overlay.tsx +++ b/src/serlo-editor/editor-ui/preview-overlay.tsx @@ -1,47 +1,8 @@ +import clsx from 'clsx' import { useState, useCallback, useEffect } from 'react' -import styled from 'styled-components' import { EditableContext } from '../core' -const NoClickArea = styled.div<{ active: boolean }>((props) => { - return { - pointerEvents: props.active ? 'unset' : 'none', - position: 'relative', - } -}) - -const Overlay = styled.div<{ active: boolean; blur: boolean }>((props) => { - return { - display: props.active ? 'none' : undefined, - position: 'absolute', - width: '100%', - height: '100%', - top: 0, - backgroundColor: props.blur ? 'rgba(255,255,255,0.8)' : undefined, - zIndex: 20, - } -}) - -const ButtonWrapper = styled.div({ - width: '100%', - height: '100%', - textAlign: 'center', - display: 'flex', -}) - -const ActivateButton = styled.button({ - pointerEvents: 'all', - color: 'white', - border: 'none', - borderRadius: '5px', - padding: '2px 10px', - textAlign: 'center', - outline: 'none', - backgroundColor: 'rgb(0,126,193)', - zIndex: 10, - margin: 'auto', -}) - export function PreviewOverlay(props: PreviewOverlayProps) { const [active, setActiveState] = useState(false) const { onChange } = props @@ -62,20 +23,25 @@ export function PreviewOverlay(props: PreviewOverlayProps) { }, [props.focused, active, setActive]) return ( - - +
+
{props.focused ? ( - - { - setActive(true) - }} +
+ +
) : null} - +
{!props.editable ? ( {props.children} @@ -84,17 +50,16 @@ export function PreviewOverlay(props: PreviewOverlayProps) { props.children )} {active ? ( - - { - setActive(false) - }} +
+ +
) : null} - +
) } diff --git a/src/serlo-editor/editor-ui/side-toolbar-and-wrapper.tsx b/src/serlo-editor/editor-ui/side-toolbar-and-wrapper.tsx new file mode 100644 index 0000000000..583d2da3e5 --- /dev/null +++ b/src/serlo-editor/editor-ui/side-toolbar-and-wrapper.tsx @@ -0,0 +1,92 @@ +import clsx from 'clsx' +import { useState, useRef } from 'react' + +import { tw } from '@/helper/tw' + +export interface SideToolbarAndWrapperProps { + children: React.ReactNode // The rendered document + sideToolbarRef: React.RefObject // The rendered toolbar buttons + hasSideToolbar: boolean // `true` if the document has rendered any toolbar buttons + renderSideToolbar?(children: React.ReactNode): React.ReactNode // Render prop to override rendering of toolbar + focused: boolean // `true` if the document is focused + isInlineChildEditor: boolean +} + +// Container that includes a plugin and its sideToolbar and handles some hover&focus styling +export function SideToolbarAndWrapper({ + focused, + children, + renderSideToolbar, + sideToolbarRef, + hasSideToolbar, + isInlineChildEditor, +}: SideToolbarAndWrapperProps) { + const [hasHover, setHasHover] = useState(false) + + const showToolbar = hasSideToolbar || renderSideToolbar !== undefined + + const isFocused = focused && showToolbar + const isHovered = hasHover && showToolbar + + const isAppended = useRef(false) + const sideToolbar = ( +
{ + // The ref `isAppended` ensures that we only append the content once + // so that we don't lose focus on every render + if (ref && sideToolbarRef.current && !isAppended.current) { + isAppended.current = true + ref.appendChild(sideToolbarRef.current) + } + }} + /> + ) + + if (isInlineChildEditor) return <>{children} + + return ( +
.toolbar-container>div]:border-transparent + hover:[&:has(.default-plugin-wrapper-container):hover>.toolbar-container>div]:opacity-0 + ` + : '' + )} + onMouseEnter={() => setHasHover(true)} + onMouseLeave={() => setHasHover(false)} + > + {children} +
+
+ {renderSideToolbar ? renderSideToolbar(sideToolbar) : sideToolbar} +
+
+
+ ) +} diff --git a/src/serlo-editor/math/button.tsx b/src/serlo-editor/math/button.tsx deleted file mode 100644 index 3fcf8f6d19..0000000000 --- a/src/serlo-editor/math/button.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import styled from 'styled-components' - -import { legacyEditorTheme } from '@/helper/colors' - -export const Button = styled.button((props: { active?: boolean }) => { - return { - backgroundColor: props.active ? '#b6b6b6' : 'transparent', - cursor: 'pointer', - boxShadow: props.active ? 'inset 0 1px 3px 0 rgba(0,0,0,0.50)' : undefined, - color: props.active - ? legacyEditorTheme.backgroundColor - : legacyEditorTheme.color, - outline: 'none', - height: '25px', - border: 'none', - borderRadius: '4px', - margin: '5px', - padding: '0px', - width: '25px', - '&:hover': { - color: legacyEditorTheme.primary.background, - }, - } -}) diff --git a/src/serlo-editor/math/dropdown.tsx b/src/serlo-editor/math/dropdown.tsx deleted file mode 100644 index f7bdc01153..0000000000 --- a/src/serlo-editor/math/dropdown.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import styled from 'styled-components' - -import { legacyEditorTheme } from '@/helper/colors' - -export const Dropdown = styled.select({ - backgroundColor: legacyEditorTheme.backgroundColor, - cursor: 'pointer', - color: legacyEditorTheme.color, - outline: 'none', - height: '25px', - border: 'none', - borderRadius: '4px', - margin: '5px', - '&:hover': { - color: legacyEditorTheme.primary.background, - }, -}) - -export const Option = styled.option<{ - active?: boolean -}>((props) => { - return { - backgroundColor: props.active - ? '#b6b6b6' - : legacyEditorTheme.backgroundColor, - color: props.active - ? legacyEditorTheme.backgroundColor - : legacyEditorTheme.color, - cursor: 'pointer', - '&:hover': { - color: legacyEditorTheme.primary.background, - }, - } -}) diff --git a/src/serlo-editor/math/editor.tsx b/src/serlo-editor/math/editor.tsx index 57c66b84ed..3df72a70ea 100644 --- a/src/serlo-editor/math/editor.tsx +++ b/src/serlo-editor/math/editor.tsx @@ -1,41 +1,16 @@ +import { faCheckCircle, faCircle } from '@fortawesome/free-regular-svg-icons' import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons' +import clsx from 'clsx' import { useState, useCallback, createRef, useEffect } from 'react' +import { createPortal } from 'react-dom' import Modal from 'react-modal' -import styled from 'styled-components' -import { Button } from './button' -import { Dropdown, Option } from './dropdown' -import { InlineCheckbox } from './inline-checkbox' import { MathRenderer } from './renderer' import { VisualEditor } from './visual-editor' -import { EditorTextarea, HoverOverlayOld } from '../editor-ui' +import { EditorTextarea } from '../editor-ui' import { FaIcon } from '@/components/fa-icon' import { useEditorStrings } from '@/contexts/logged-in-data-context' - -const EditorWrapper = styled.div<{ inline?: boolean }>((props) => { - return { - whiteSpace: undefined, - overflowWrap: undefined, - ...(props.inline - ? { - display: 'inline-block', - } - : { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - marginTop: '0.9em', - marginBottom: '0.9em', - }), - } -}) - -const mathEditorTextareaStyle = { - color: 'black', - margin: 2, - width: '80vw', - maxWidth: 600, -} +import { tw } from '@/helper/tw' interface MathEditorTextAreaProps extends Pick< @@ -73,14 +48,13 @@ const MathEditorTextArea = (props: MathEditorTextAreaProps) => { return ( { - event.stopPropagation() - }} - onCut={(event: React.ClipboardEvent) => { - event.stopPropagation() - }} + onCopy={(e) => e.stopPropagation()} + onCut={(e) => e.stopPropagation()} onMoveOutRight={props.onMoveOutRight} onMoveOutLeft={props.onMoveOutLeft} value={latex} @@ -89,15 +63,6 @@ const MathEditorTextArea = (props: MathEditorTextAreaProps) => { ) } -const KeySpan = styled.span({ - background: '#ddd', - padding: '2px 4px', - borderRadius: 5, - color: '#1d1c1d', - textAlign: 'center', - minWidth: 20, -}) - export interface MathEditorProps { autofocus?: boolean state: string @@ -161,35 +126,33 @@ export function MathEditor(props: MathEditorProps) {

- {mathStrings.fraction}: / + {mathStrings.fraction}: {renderKey('/')}

- {mathStrings.superscript}: {mathStrings.or}{' '} - ^ + {mathStrings.superscript}: {renderKey('↑')} {mathStrings.or}{' '} + {renderKey('^')}

- {mathStrings.subscript}: {mathStrings.or}{' '} - _ + {mathStrings.subscript}: {renderKey('↓')} {mathStrings.or}{' '} + {renderKey('_')}

- π, α, β, γ: pi, alpha,{' '} - beta,gamma + π, α, β, γ: {renderKey('pi')}, {renderKey('alpha')},{' '} + {renderKey('beta')},{renderKey('gamma')}

- ≤, ≥: {'<='}, {'>='} + ≤, ≥: {renderKey('<=')}, {renderKey('>=')}

- {mathStrings.root}: \sqrt,{' '} - \nthroot + {mathStrings.root}: {renderKey('\\sqrt')}, {renderKey('\\nthroot')}

- {mathStrings.mathSymbols}: {'\\'},{' '} - {mathStrings.eG} \neq (≠), \pm{' '} - (±), ... + {mathStrings.mathSymbols}: {renderKey('\\')}, {mathStrings.eG}{' '} + {renderKey('\\neq')} (≠), {renderKey('\\pm')} (±), …

- {mathStrings.functions}: sin,{' '} - cos, ln, ... + {mathStrings.functions}: {renderKey('sin')}, {renderKey('cos')},{' '} + {renderKey('ln')}, …

@@ -197,6 +160,14 @@ export function MathEditor(props: MathEditorProps) { ) + function renderKey(text: string) { + return ( + + {text} + + ) + } + function renderChildren() { if (readOnly) { return state ? ( @@ -211,13 +182,15 @@ export function MathEditor(props: MathEditorProps) { return ( <> {useVisualEditor ? ( - { - e.stopPropagation() - }} - inline={props.inline} +
e.stopPropagation()} ref={anchorRef} {...props.additionalContainerProps} + className={clsx( + props.inline + ? 'inline-block' + : 'my-[0.9em] flex flex-col items-center' + )} > - +
) : ( - +
+ +
)} - {helpOpen ? null : ( - -
{ - e.stopPropagation() + + {renderControlsPortal( +
e.stopPropagation()} // stops editor from setting focus to other plugin + className="inline-block" + > + + {!disableBlock && ( + - )} - {hasError && ( - <> - {mathStrings.onlyLatex} -    - - )} -
- {!useVisualEditor && ( - - )} -
- + {mathStrings.displayAsBlock}{' '} + + + )} + {useVisualEditor && ( + + )} +
)} + + {hasError || !useVisualEditor ? renderOverlayPortal() : null} ) } + + function renderControlsPortal(children: JSX.Element) { + const target = + typeof window !== undefined && + document.querySelector('.toolbar-controls-target') + if (!target) return null + + return createPortal(children, target) + } + + function renderOverlayPortal() { + const children = ( +
e.stopPropagation()} // double/triple clicks close overlay otherwise (#2700) + > +

+ {hasError ? mathStrings.onlyLatex : mathStrings.latexEditorTitle} +

+ {!useVisualEditor && ( + + )} +
+ ) + return children + } } diff --git a/src/serlo-editor/math/inline-checkbox.tsx b/src/serlo-editor/math/inline-checkbox.tsx deleted file mode 100644 index e8f7e3b5f0..0000000000 --- a/src/serlo-editor/math/inline-checkbox.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import styled from 'styled-components' - -const CheckboxInlineLabel = styled.label({ - color: '#ffffff', - verticalAlign: 'middle', - margin: '5px 10px', - display: 'inline-block', -}) - -const CheckboxInlineLabelInner = styled.span({ - marginRight: '10px', - verticalAlign: 'middle', -}) - -const CheckboxToggleContainer = styled.div<{ - value?: boolean -}>(({ value }) => { - return { - cursor: 'pointer', - border: '2px solid #ffffff', - borderRadius: '15%', - width: '20px', - height: '20px', - display: 'inline-block', - verticalAlign: 'middle', - backgroundColor: value ? '#ffffff' : 'rgba(51,51,51,0.95)', - } -}) - -const CheckboxToggle = styled.div<{ value?: boolean }>(({ value }) => { - return { - opacity: value ? 1 : 0, - content: '', - position: 'absolute', - fontWeight: 'bold', - width: '20px', - height: '10px', - border: '3px solid rgba(51,51,51,0.95)', - borderTop: 'none', - borderRight: 'none', - borderRadius: '1px', - - transform: 'rotate(-45deg)', - zIndex: 1000, - } -}) - -export function InlineCheckbox({ checked, onChange, label }: CheckboxProps) { - return ( - - {label} - { - // avoid loosing focus - e.stopPropagation() - }} - onClick={() => { - if (onChange) { - onChange(!checked) - } - }} - value={checked} - > - - - - ) -} - -export interface CheckboxProps { - checked?: boolean - onChange?: (checked: boolean) => void - label?: string -} diff --git a/src/serlo-editor/math/visual-editor.tsx b/src/serlo-editor/math/visual-editor.tsx index f64b5787af..ab14bde398 100644 --- a/src/serlo-editor/math/visual-editor.tsx +++ b/src/serlo-editor/math/visual-editor.tsx @@ -1,6 +1,7 @@ import * as MQ from 'react-mathquill' import { MathEditorProps } from './editor' +import { tw } from '@/helper/tw' if (typeof window !== 'undefined') { MQ.addStyles() @@ -89,21 +90,32 @@ export function VisualEditor(props: VisualEditorProps) { } return ( - { - props.onChange(ref.latex()) - }} - onCopy={(event: React.ClipboardEvent) => { - event.stopPropagation() - }} - onCut={(event: React.ClipboardEvent) => { - event.stopPropagation() - }} - // @ts-expect-error https://github.com/serlo/serlo-editor-issues-and-documentation/issues/67 - config={mathQuillConfig} - mathquillDidMount={onMount} - /> +
+ { + // Should always be defined after first render cycle! + if (mathFieldRef?.latex) { + props.onChange(mathFieldRef.latex()) + } + }} + onCopy={(event: React.ClipboardEvent) => { + event.stopPropagation() + }} + onCut={(event: React.ClipboardEvent) => { + event.stopPropagation() + }} + // @ts-expect-error https://github.com/serlo/serlo-editor-issues-and-documentation/issues/67 + config={mathQuillConfig} + mathquillDidMount={onMount} + /> +
) function onMount(ref: MathField) { diff --git a/src/serlo-editor/plugin/helpers/editor-plugins.tsx b/src/serlo-editor/plugin/helpers/editor-plugins.tsx new file mode 100644 index 0000000000..65cdb32ca2 --- /dev/null +++ b/src/serlo-editor/plugin/helpers/editor-plugins.tsx @@ -0,0 +1,41 @@ +import { EditorPlugin } from '../internal-plugin' + +interface PluginWithData { + type: string + plugin: EditorPlugin | EditorPlugin + visibleInSuggestions?: boolean + icon?: JSX.Element +} + +export type PluginsWithData = PluginWithData[] + +export const editorPlugins = (function () { + let allPlugins: PluginsWithData | null = null + + function init(plugins: PluginsWithData) { + if (allPlugins) return // only initialize once + + allPlugins = plugins + + // Ensure the highest integrity level that JS provides + Object.freeze(allPlugins) + } + + function getAllWithData() { + if (!allPlugins) throw new Error('init editor plugins first') + + return allPlugins + } + + function getByType(pluginType: string) { + const plugins = getAllWithData() + + const contextPlugin = + plugins.find((plugin) => plugin.type === pluginType) ?? + plugins.find((plugin) => plugin.type === 'unsupported') + + return (contextPlugin?.plugin as EditorPlugin) ?? null + } + + return { init, getAllWithData, getByType } +})() diff --git a/src/serlo-editor/plugin/helpers/get-plugin-title.tsx b/src/serlo-editor/plugin/helpers/get-plugin-title.tsx new file mode 100644 index 0000000000..387e4829ab --- /dev/null +++ b/src/serlo-editor/plugin/helpers/get-plugin-title.tsx @@ -0,0 +1,10 @@ +import { useEditorStrings } from '@/contexts/logged-in-data-context' + +export function getPluginTitle( + pluginStrings: ReturnType['plugins'], + pluginType: string +) { + return Object.hasOwn(pluginStrings, pluginType) + ? pluginStrings[pluginType as keyof typeof pluginStrings].title + : pluginType +} diff --git a/src/serlo-editor/plugin/helpers/inline-input.tsx b/src/serlo-editor/plugin/helpers/inline-input.tsx index 07952629f4..c7dcd38197 100644 --- a/src/serlo-editor/plugin/helpers/inline-input.tsx +++ b/src/serlo-editor/plugin/helpers/inline-input.tsx @@ -44,9 +44,7 @@ export function InlineInput(props: { placeholder={placeholder} onFocus={() => { setTimeout(() => { - if (typeof props.onFocus === 'function') { - props.onFocus() - } + if (props.onFocus) props.onFocus() }) }} /> diff --git a/src/serlo-editor/plugin/helpers/inline-settings-input.tsx b/src/serlo-editor/plugin/helpers/inline-settings-input.tsx index 578f4ef19f..9f1feb765e 100644 --- a/src/serlo-editor/plugin/helpers/inline-settings-input.tsx +++ b/src/serlo-editor/plugin/helpers/inline-settings-input.tsx @@ -1,22 +1,16 @@ import { forwardRef } from 'react' -import styled from 'styled-components' - -const InlineInputInner = styled.input({ - backgroundColor: 'transparent', - border: 'none', - borderBottom: '2px solid #ffffff', - color: '#ffffff', - '&:focus': { - outline: 'none', - borderBottom: '2px solid rgb(70, 155, 255)', - }, -}) const InlineInputRefForward: React.ForwardRefRenderFunction< HTMLInputElement, InputProps > = (props, ref) => { - return + return ( + + ) } export const InlineSettingsInput = forwardRef(InlineInputRefForward) diff --git a/src/serlo-editor/plugin/helpers/inline-settings.tsx b/src/serlo-editor/plugin/helpers/inline-settings.tsx index 863194c14d..fcd878473d 100644 --- a/src/serlo-editor/plugin/helpers/inline-settings.tsx +++ b/src/serlo-editor/plugin/helpers/inline-settings.tsx @@ -1,25 +1,7 @@ import { faTrashAlt } from '@fortawesome/free-solid-svg-icons' -import styled from 'styled-components' import { FaIcon } from '@/components/fa-icon' -import { - HoverOverlayOld, - HoverPosition, -} from '@/serlo-editor/editor-ui/hover-overlay-old' - -const InlinePreview = styled.span({ - padding: '0px 8px', -}) -const ChangeButton = styled.div({ - padding: '5px 5px 5px 10px', - display: 'inline-block', - borderLeft: '2px solid rgba(51,51,51,0.95)', - cursor: 'pointer', - margin: '2px', - '&:hover': { - color: 'rgb(70, 155, 255)', - }, -}) +import { HoverOverlay, HoverPosition } from '@/serlo-editor/editor-ui' export function InlineSettings({ position = 'below', @@ -31,13 +13,16 @@ export function InlineSettings({ anchor?: React.RefObject }) { return ( - - {props.children} + + {props.children} {props.onDelete ? ( - +
- +
) : null} -
+ ) } diff --git a/src/serlo-editor/plugin/plugin-toolbar/icon-container.ts b/src/serlo-editor/plugin/plugin-toolbar/icon-container.ts deleted file mode 100644 index 4333cd3d36..0000000000 --- a/src/serlo-editor/plugin/plugin-toolbar/icon-container.ts +++ /dev/null @@ -1,22 +0,0 @@ -import styled from 'styled-components' - -import { colors } from '@/helper/colors' - -export const StyledIconContainer = styled.div({ - height: '30px', - width: '30px', - cursor: 'pointer', - color: colors.almostBlack, - borderRadius: '100rem', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - - '&:hover': { - backgroundColor: colors.editorPrimary200, - }, - - '& > svg': { - margin: '0 !important', - }, -}) diff --git a/src/serlo-editor/plugin/plugin-toolbar/index.ts b/src/serlo-editor/plugin/plugin-toolbar/index.ts index 81afbf444b..4257953a48 100644 --- a/src/serlo-editor/plugin/plugin-toolbar/index.ts +++ b/src/serlo-editor/plugin/plugin-toolbar/index.ts @@ -1,7 +1,3 @@ -export * from './overlay-button' -export * from './overlay-checkbox' export * from './overlay-input' -export * from './overlay-select' -export * from './overlay-textarea' export * from './plugin-toolbar-button' export * from './plugin-toolbar-overlay-button' diff --git a/src/serlo-editor/plugin/plugin-toolbar/overlay-button.tsx b/src/serlo-editor/plugin/plugin-toolbar/overlay-button.tsx deleted file mode 100644 index ee53be7232..0000000000 --- a/src/serlo-editor/plugin/plugin-toolbar/overlay-button.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import styled from 'styled-components' - -import { colors } from '@/helper/colors' - -export interface OverlayButtonProps { - className?: string - onClick?: React.MouseEventHandler - children?: React.ReactNode - label: string -} - -export function OverlayButton({ - children, - label, - ...props -}: OverlayButtonProps) { - return ( - - ) -} - -const Button = styled.button({ - margin: '3px', - backgroundColor: '#ffffff', - outline: 'none', - border: '2px solid rgba(51,51,51,0.95)', - color: 'rgba(51,51,51,0.95)', - padding: '10px', - borderRadius: '4px', - minWidth: '125px', - cursor: 'pointer', - '&:hover': { - backgroundColor: 'transparent', - color: 'rgb(70, 155, 255)', - borderColor: colors.editorPrimary, - }, -}) diff --git a/src/serlo-editor/plugin/plugin-toolbar/overlay-checkbox.tsx b/src/serlo-editor/plugin/plugin-toolbar/overlay-checkbox.tsx deleted file mode 100644 index 9d9cb29d2f..0000000000 --- a/src/serlo-editor/plugin/plugin-toolbar/overlay-checkbox.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import styled from 'styled-components' - -export interface OverlayCheckboxProps { - checked?: boolean - onChange?: (checked: boolean) => void - label: string -} - -export function OverlayCheckbox({ - checked, - onChange, - label, -}: OverlayCheckboxProps) { - return ( - - {label} - - { - if (onChange) { - onChange(!checked) - } - }} - value={checked} - > - - - - - ) -} - -const OverlayCheckboxLabel = styled.label({ - color: 'rgba(51,51,51,0.95)', - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - marginTop: '20px', -}) - -const OverlayCheckboxToggleContainer = styled.div<{ - value?: boolean -}>(({ value }) => { - return { - cursor: 'pointer', - border: '2px solid rgba(51,51,51,0.95)', - borderRadius: '15%', - width: '20px', - height: '20px', - display: 'inline-block', - verticalAlign: 'middle', - backgroundColor: value ? 'rgba(51,51,51,0.95)' : '#ffffff', - } -}) - -const OverlayCheckboxLabelInner = styled.span({ width: '20%' }) - -const OverlayCheckboxToggle = styled.div<{ value?: boolean }>(({ value }) => { - return { - opacity: value ? 1 : 0, - content: '', - position: 'absolute', - fontWeight: 'bold', - width: '20px', - height: '10px', - border: '3px solid #ffffff', - borderTop: 'none', - borderRight: 'none', - borderRadius: '1px', - - transform: 'rotate(-45deg)', - zIndex: 1000, - } -}) - -const OverlayCheckboxInner = styled.div({ - width: '75%', - textAlign: 'left', -}) diff --git a/src/serlo-editor/plugin/plugin-toolbar/overlay-input.tsx b/src/serlo-editor/plugin/plugin-toolbar/overlay-input.tsx index 57c885d82b..1b88b4face 100644 --- a/src/serlo-editor/plugin/plugin-toolbar/overlay-input.tsx +++ b/src/serlo-editor/plugin/plugin-toolbar/overlay-input.tsx @@ -1,7 +1,6 @@ import { forwardRef } from 'react' -import styled from 'styled-components' -import { colors } from '@/helper/colors' +import { tw } from '@/helper/tw' export interface OverlayInputProps extends React.DetailedHTMLProps< @@ -14,32 +13,18 @@ export interface OverlayInputProps export const OverlayInput = forwardRef( function OverlayInput({ label, ...props }, ref) { return ( - - {label} - - + ) } -) as unknown as React.ComponentType - -const OverlayInputLabel = styled.label({ - color: 'rgba(51,51,51,0.95)', - margin: '20px auto 0px', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', -}) - -const OverlayInputLabelInner = styled.span({ width: '20%' }) - -const OverlayInputInner = styled.input({ - backgroundColor: '#ffffff', - border: 'none', - borderBottom: '2px solid rgba(51,51,51,0.95)', - color: 'rgba(51,51,51,0.95)', - width: '75%', - '&:focus': { - outline: 'none', - borderBottom: `2px solid ${colors.editorPrimary}`, - }, -}) +) diff --git a/src/serlo-editor/plugin/plugin-toolbar/overlay-select.tsx b/src/serlo-editor/plugin/plugin-toolbar/overlay-select.tsx deleted file mode 100644 index a1ac471441..0000000000 --- a/src/serlo-editor/plugin/plugin-toolbar/overlay-select.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import styled from 'styled-components' - -export interface OverlaySelectProps - extends React.DetailedHTMLProps< - React.SelectHTMLAttributes, - HTMLSelectElement - > { - label: string - options: string[] - width?: string -} - -export function OverlaySelect({ - label, - options, - ...props -}: OverlaySelectProps) { - return ( - - {label} - - - - - ) -} - -const OverlayInputLabel = styled.label({ - color: 'rgba(51,51,51,0.95)', - margin: '20px auto 0px', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', -}) - -const OverlayInputLabelInner = styled.span({ width: '20%' }) - -const OverlaySelectInner = styled.div({ - width: '75%', - textAlign: 'left', -}) - -const Select = styled.select<{ selectBoxWidth?: string }>((props) => { - return { - width: props.selectBoxWidth, - borderRadius: '5px', - outline: 'none', - } -}) diff --git a/src/serlo-editor/plugin/plugin-toolbar/overlay-textarea.tsx b/src/serlo-editor/plugin/plugin-toolbar/overlay-textarea.tsx deleted file mode 100644 index 3b6392027b..0000000000 --- a/src/serlo-editor/plugin/plugin-toolbar/overlay-textarea.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import styled from 'styled-components' - -import { colors } from '@/helper/colors' - -export interface OverlayTextareaProps - extends React.DetailedHTMLProps< - React.TextareaHTMLAttributes, - HTMLTextAreaElement - > { - label: string -} - -export function OverlayTextarea({ label, ...props }: OverlayTextareaProps) { - return ( - - {label} - - - ) -} - -const OverlayTextareaLabel = styled.label({ - color: 'rgba(51,51,51,0.95)', - display: 'flex', - flexDirection: 'row', - margin: '20px auto 0', - justifyContent: 'space-between', -}) - -const OverlayTextareaLabelInner = styled.span({ width: '20%' }) - -const OverlayTextareaInner = styled.textarea({ - backgroundColor: '#ffffff', - border: '2px solid rgba(51,51,51,0.95)', - marginTop: '5px', - borderRadius: '5px', - color: '2px solid rgba(51,51,51,0.95)', - padding: '10px', - resize: 'none', - outline: 'none', - minHeight: '100px', - width: '75%', - '&:focus': { - border: `2px solid ${colors.editorPrimary}`, - }, -}) diff --git a/src/serlo-editor/plugin/plugin-toolbar/plugin-toolbar-button.tsx b/src/serlo-editor/plugin/plugin-toolbar/plugin-toolbar-button.tsx index 886beefccb..63cf7c59ec 100644 --- a/src/serlo-editor/plugin/plugin-toolbar/plugin-toolbar-button.tsx +++ b/src/serlo-editor/plugin/plugin-toolbar/plugin-toolbar-button.tsx @@ -1,7 +1,7 @@ import { forwardRef } from 'react' -import { StyledIconContainer } from './icon-container' import { EditorTooltip } from '../../editor-ui/editor-tooltip' +import { tw } from '@/helper/tw' export interface PluginToolbarButtonProps { className?: string @@ -22,7 +22,14 @@ export const PluginToolbarButton = forwardRef< onClick={onClick} > - +
svg]:!m-0 `} + aria-hidden="true" + > + {icon} +
) }) diff --git a/src/serlo-editor/plugin/plugin-toolbar/plugin-toolbar-overlay-button.tsx b/src/serlo-editor/plugin/plugin-toolbar/plugin-toolbar-overlay-button.tsx index 6534d72033..ab5f3b9be4 100644 --- a/src/serlo-editor/plugin/plugin-toolbar/plugin-toolbar-overlay-button.tsx +++ b/src/serlo-editor/plugin/plugin-toolbar/plugin-toolbar-overlay-button.tsx @@ -1,8 +1,8 @@ import { useRef, useState } from 'react' import Modal from 'react-modal' -import { StyledIconContainer } from './icon-container' import { EditorTooltip } from '../../editor-ui/editor-tooltip' +import { tw } from '@/helper/tw' export interface PluginToolbarOverlayButtonProps { className?: string @@ -38,7 +38,14 @@ export function PluginToolbarOverlayButton({ }} > - +
svg]:!m-0 `} + aria-hidden="true" + > + {icon} +
) diff --git a/src/serlo-editor/plugin/upload.ts b/src/serlo-editor/plugin/upload.ts index ae17072e33..6a4b25a8d0 100644 --- a/src/serlo-editor/plugin/upload.ts +++ b/src/serlo-editor/plugin/upload.ts @@ -3,9 +3,6 @@ import { useEffect, useState } from 'react' import { StateType } from './internal-plugin-state' import { asyncScalar } from './scalar' -/** - * @param defaultState - The default state - */ export function upload(defaultState: T): UploadStateType { const state = asyncScalar(defaultState, isTempFile) return { diff --git a/src/serlo-editor/plugins/_on-the-way-out/layout/editor.tsx b/src/serlo-editor/plugins/_on-the-way-out/layout/editor.tsx index 410d34e73e..7a53c78d67 100644 --- a/src/serlo-editor/plugins/_on-the-way-out/layout/editor.tsx +++ b/src/serlo-editor/plugins/_on-the-way-out/layout/editor.tsx @@ -1,8 +1,5 @@ -import styled from 'styled-components' - import { LayoutPluginState } from '.' import { useEditorStrings } from '@/contexts/logged-in-data-context' -import { usePlugins } from '@/serlo-editor/core/contexts/plugins-context' import { EditorPluginProps, StateTypeReturnType } from '@/serlo-editor/plugin' import { store, @@ -14,40 +11,6 @@ import { import { EditorPluginType } from '@/serlo-editor-integration/types/editor-plugin-type' import { RowsPlugin } from '@/serlo-editor-integration/types/legacy-editor-to-editor-types' -const LayoutContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - flexWrap: 'wrap', - alignItems: 'flex-start', -}) - -const ChildContainer = styled.div<{ width: number }>(({ width }) => { - return { - width: `${(width / 12) * 100}%`, - '@media (max-width: 480px)': { - width: '100%', - }, - } -}) -const ConvertInfo = styled.div({ - padding: '5px', - backgroundColor: '#f2dede', - color: '#a94442', - border: '1px solid #ebccd1', - textAlign: 'center', -}) - -const ButtonContainer = styled.div({ display: 'flex', flexDirection: 'row' }) - -const ConvertButton = styled.button({ - borderRadius: '5px', - margin: '5px', - border: 'none', - outline: 'none', - backgroundColor: 'white', - '&:hover': { backgroundColor: '#ebccd1' }, -}) - export const LayoutRenderer: React.FunctionComponent< EditorPluginProps & { insert?: (options?: DocumentState) => void @@ -58,34 +21,42 @@ export const LayoutRenderer: React.FunctionComponent< const editorStrings = useEditorStrings() - const plugins = usePlugins() - return ( <> {props.editable ? ( - +
{editorStrings.plugins.layout.toDragConvert} - - +
+ {canConvertToMultimedia() ? ( - + ) : null} - - +
+
) : null} - +
{props.state.map((item, index) => { return ( - +
{item.child.render()} - +
) })} - +
) @@ -108,7 +79,6 @@ export const LayoutRenderer: React.FunctionComponent< dispatch( runReplaceDocumentSaga({ id: props.id, - plugins, pluginType: EditorPluginType.Rows, state: documents, }) @@ -157,7 +127,6 @@ export const LayoutRenderer: React.FunctionComponent< dispatch( runReplaceDocumentSaga({ id: props.id, - plugins, pluginType: EditorPluginType.Multimedia, state: { explanation, diff --git a/src/serlo-editor/plugins/_on-the-way-out/table/editor.tsx b/src/serlo-editor/plugins/_on-the-way-out/table/editor.tsx index 4fdb737eda..38039ddcd5 100644 --- a/src/serlo-editor/plugins/_on-the-way-out/table/editor.tsx +++ b/src/serlo-editor/plugins/_on-the-way-out/table/editor.tsx @@ -1,20 +1,14 @@ -import styled from 'styled-components' - import { TableProps } from '.' import { TableRenderer } from './renderer' import { EditorTextarea } from '../../../editor-ui' -const Form = styled.form({ - marginTop: '10px', -}) - export function TableEditor(props: TableProps) { const { focused, state } = props return (
{focused ? ( -
+
-
+ ) : ( )} diff --git a/src/serlo-editor/plugins/_on-the-way-out/table/renderer.tsx b/src/serlo-editor/plugins/_on-the-way-out/table/renderer.tsx index 224dcc6007..28e2cdeb45 100644 --- a/src/serlo-editor/plugins/_on-the-way-out/table/renderer.tsx +++ b/src/serlo-editor/plugins/_on-the-way-out/table/renderer.tsx @@ -1,38 +1,19 @@ import { faTable } from '@fortawesome/free-solid-svg-icons' -import styled from 'styled-components' import { TableProps } from '.' import { useTableConfig } from './config' import { FaIcon } from '@/components/fa-icon' -const TableContainer = styled.div({ - overflowX: 'auto', - '& tr': { - borderTop: '1px solid #c6cbd1', - }, - '& th, & td': { - padding: '6px 13px', - border: '1px 1px solid #dfe2e5', - }, - '& table tr:nth-child(2n)': { - background: '#f6f8fa', - }, - '& table': { - width: '100%', - maxWidth: '100%', - }, -}) - export function TableRenderer(props: TableProps) { const { editable, state } = props const config = useTableConfig(props.config) return ( - +
{editable && state.value.trim() === '' ? ( ) : null} - +
) } diff --git a/src/serlo-editor/plugins/anchor/editor.tsx b/src/serlo-editor/plugins/anchor/editor.tsx index c946fa1ccf..3f049d6dd5 100644 --- a/src/serlo-editor/plugins/anchor/editor.tsx +++ b/src/serlo-editor/plugins/anchor/editor.tsx @@ -2,30 +2,47 @@ import { faLink } from '@fortawesome/free-solid-svg-icons' import { AnchorProps } from '.' import { AnchorRenderer } from './renderer' -import { EditorInput } from '../../editor-ui' import { FaIcon } from '@/components/fa-icon' import { useEditorStrings } from '@/contexts/logged-in-data-context' +import { tw } from '@/helper/tw' +import { EditorTooltip } from '@/serlo-editor/editor-ui/editor-tooltip' +import { PluginToolbar } from '@/serlo-editor/editor-ui/plugin-toolbar' +import { PluginDefaultTools } from '@/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools' +import { EditorPluginType } from '@/serlo-editor-integration/types/editor-plugin-type' export const AnchorEditor = (props: AnchorProps) => { - const { editable, focused, state } = props + const { editable, focused, state, id } = props const editorStrings = useEditorStrings() return ( <> - {editable ? : null} - {focused ? ( - { - state.set(e.target.value) - }} - ref={props.autofocusRef} + } + pluginSettings={ + + } /> ) : null} + {editable ? : null} + ) } diff --git a/src/serlo-editor/plugins/article/add-modal/article-related-magic-input.tsx b/src/serlo-editor/plugins/article/add-modal/article-related-magic-input.tsx index 26aa37e2a5..351814a264 100644 --- a/src/serlo-editor/plugins/article/add-modal/article-related-magic-input.tsx +++ b/src/serlo-editor/plugins/article/add-modal/article-related-magic-input.tsx @@ -35,7 +35,7 @@ export function ArticleRelatedMagicInput({ UuidType.TaxonomyTerm, ]} supportedTaxonomyTypes={[TaxonomyTermType.ExerciseFolder]} - unsupportedIds={[entityId]} + unsupportedIds={entityId ? [entityId] : []} /> ) diff --git a/src/serlo-editor/plugins/article/add-modal/article-related-taxonomy.tsx b/src/serlo-editor/plugins/article/add-modal/article-related-taxonomy.tsx index 61d0f00eb4..f1878d7df1 100644 --- a/src/serlo-editor/plugins/article/add-modal/article-related-taxonomy.tsx +++ b/src/serlo-editor/plugins/article/add-modal/article-related-taxonomy.tsx @@ -25,13 +25,13 @@ export function ArticleRelatedTaxonomy({ showExerciseFolderPreview, }: ArticleRelatedTaxonomyProps) { const entityId = useEntityId() - const { data, error } = useFetchParentTaxonomy(entityId) + const { data, error } = useFetchParentTaxonomy(entityId ?? 0) const { strings } = useInstanceData() const articleStrings = useEditorStrings().templatePlugins.article const dataAndTerm = getCategorisedDataAndTerm(data, error) - if (!dataAndTerm) { + if (!dataAndTerm || !entityId) { const isNew = typeof window !== undefined && window.location.pathname.startsWith('/entity/create') diff --git a/src/serlo-editor/plugins/article/editor-renderer/article-exercises.tsx b/src/serlo-editor/plugins/article/editor-renderer/article-exercises.tsx index 1c3fa4e778..67de60cab8 100644 --- a/src/serlo-editor/plugins/article/editor-renderer/article-exercises.tsx +++ b/src/serlo-editor/plugins/article/editor-renderer/article-exercises.tsx @@ -24,14 +24,16 @@ export function ArticleExercises({ {exercises.map((exercise, index) => ( {exercise.render({ - renderToolbar: editable ? () => renderToolbar(index) : undefined, + renderSideToolbar: editable + ? () => renderSideToolbar(index) + : undefined, })} ))} ) - function renderToolbar(index: number) { + function renderSideToolbar(index: number) { const buttonClass = 'serlo-button-editor-secondary mb-2 mr-2 w-8' return ( <> diff --git a/src/serlo-editor/plugins/box/editor.tsx b/src/serlo-editor/plugins/box/editor.tsx index dff7155ec1..e487eeb7ab 100644 --- a/src/serlo-editor/plugins/box/editor.tsx +++ b/src/serlo-editor/plugins/box/editor.tsx @@ -3,18 +3,26 @@ import clsx from 'clsx' import { BoxProps } from '.' import { BoxType, - Renderer, + BoxRenderer, boxTypeStyle, defaultStyle, types, } from './renderer' +import { BoxToolbar } from './toolbar' import { FaIcon } from '@/components/fa-icon' import { useInstanceData } from '@/contexts/instance-context' import { useEditorStrings } from '@/contexts/logged-in-data-context' +import { TextEditorFormattingOption } from '@/serlo-editor/editor-ui/plugin-toolbar/text-controls/types' import { selectIsEmptyRows } from '@/serlo-editor/plugins/rows' -import { useAppSelector } from '@/serlo-editor/store' +import { selectIsFocused, useAppSelector } from '@/serlo-editor/store' + +const titleFormattingOptions = [ + TextEditorFormattingOption.math, + TextEditorFormattingOption.code, +] export function BoxEditor(props: BoxProps) { + const { focused } = props const { title, type, content, anchorId } = props.state const hasNoType = type.value === '' const typedValue = (hasNoType ? 'blank' : type.value) as BoxType @@ -30,6 +38,12 @@ export function BoxEditor(props: BoxProps) { const { strings } = useInstanceData() const editorStrings = useEditorStrings() + const isTitleFocused = useAppSelector((state) => + selectIsFocused(state, title.id) + ) + + const hasFocus = focused || isTitleFocused + if (hasNoType) { return ( <> @@ -39,22 +53,26 @@ export function BoxEditor(props: BoxProps) { borderColorClass )} > - {renderInlineSettings()} + {editorStrings.plugins.box.type} +
    {renderSettingsLis()}
- {renderSettings()} ) } return ( <> - : null} + + {title.render({ config: { placeholder: editorStrings.plugins.box.titlePlaceholder, + formattingOptions: titleFormattingOptions, + isInlineChildEditor: true, }, })}
@@ -62,38 +80,11 @@ export function BoxEditor(props: BoxProps) { anchorId={anchorId.value} >
{content.render()}
- + {renderWarning()} - {renderSettings()} ) - function renderInlineSettings() { - return ( - <> - {editorStrings.plugins.box.type} -
    {renderSettingsLis()}
- - ) - } - - function renderSettings() { - return props.renderIntoSettings( - <> - - {editorStrings.plugins.box.type}: - -
    {renderSettingsLis()}
- - {anchorId.value === '' ? null : ( -

- {editorStrings.plugins.box.anchorId}: #{anchorId.value} -

- )} - - ) - } - function renderSettingsLis() { return types.map((boxType) => { const typedBoxType = boxType as BoxType diff --git a/src/serlo-editor/plugins/box/renderer.tsx b/src/serlo-editor/plugins/box/renderer.tsx index 41e4170b38..75fc250fc7 100644 --- a/src/serlo-editor/plugins/box/renderer.tsx +++ b/src/serlo-editor/plugins/box/renderer.tsx @@ -46,7 +46,7 @@ interface BoxProps { title?: JSX.Element | string } -export function Renderer({ boxType, title, anchorId, children }: BoxProps) { +export function BoxRenderer({ boxType, title, anchorId, children }: BoxProps) { const { strings } = useInstanceData() if (!children || !boxType) return null diff --git a/src/serlo-editor/plugins/box/toolbar.tsx b/src/serlo-editor/plugins/box/toolbar.tsx new file mode 100644 index 0000000000..9fa508dc1e --- /dev/null +++ b/src/serlo-editor/plugins/box/toolbar.tsx @@ -0,0 +1,47 @@ +import { BoxProps } from '.' +import { BoxType, types } from './renderer' +import { useInstanceData } from '@/contexts/instance-context' +import { useEditorStrings } from '@/contexts/logged-in-data-context' +import { tw } from '@/helper/tw' +import { EditorTooltip } from '@/serlo-editor/editor-ui/editor-tooltip' +import { PluginToolbar } from '@/serlo-editor/editor-ui/plugin-toolbar' +import { PluginDefaultTools } from '@/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools' +import { EditorPluginType } from '@/serlo-editor-integration/types/editor-plugin-type' + +export const BoxToolbar = ({ id, state }: BoxProps) => { + const boxStrings = useEditorStrings().plugins.box + const { strings } = useInstanceData() + + return ( + +
+ + +
+ + } + pluginControls={} + /> + ) +} diff --git a/src/serlo-editor/plugins/equations/editor-renderer.tsx b/src/serlo-editor/plugins/equations/editor/editor-renderer.tsx similarity index 100% rename from src/serlo-editor/plugins/equations/editor-renderer.tsx rename to src/serlo-editor/plugins/equations/editor/editor-renderer.tsx diff --git a/src/serlo-editor/plugins/equations/editor.tsx b/src/serlo-editor/plugins/equations/editor/editor.tsx similarity index 50% rename from src/serlo-editor/plugins/equations/editor.tsx rename to src/serlo-editor/plugins/equations/editor/editor.tsx index ef79084bd2..ceafca1a11 100644 --- a/src/serlo-editor/plugins/equations/editor.tsx +++ b/src/serlo-editor/plugins/equations/editor/editor.tsx @@ -4,24 +4,25 @@ import { faTrashAlt, } from '@fortawesome/free-solid-svg-icons' import { includes } from 'ramda' -import { useContext, useEffect, useState } from 'react' +import { useEffect, useRef } from 'react' import { useHotkeys } from 'react-hotkeys-hook' -import { EquationsProps, stepProps } from '.' import { toTransformationTarget, TransformationTarget } from './editor-renderer' +import { useGridFocus } from './grid-focus' +import { StepEditor } from './step-editor' +import { EquationsProps } from '..' import { EquationsRenderer, EquationsRendererStep, renderDownArrow, -} from './renderer' -import { renderSignToString, Sign } from './sign' +} from '../renderer' +import { Sign } from '../sign' +import { EquationsToolbar } from '../toolbar' import { FaIcon } from '@/components/fa-icon' import { useEditorStrings } from '@/contexts/logged-in-data-context' import { tw } from '@/helper/tw' -import { PreferenceContext, setDefaultPreference } from '@/serlo-editor/core' import { EditorTooltip } from '@/serlo-editor/editor-ui/editor-tooltip' -import { MathEditor, MathRenderer } from '@/serlo-editor/math' -import { StateTypeReturnType, StringStateType } from '@/serlo-editor/plugin' +import { MathRenderer } from '@/serlo-editor/math' import { store, focus, @@ -35,22 +36,17 @@ import { } from '@/serlo-editor/store' import { EditorPluginType } from '@/serlo-editor-integration/types/editor-plugin-type' -enum StepSegment { +export enum StepSegment { Left = 0, Right = 1, Transform = 2, Explanation = 3, } -const preferenceKey = 'katex:usevisualmath' - -setDefaultPreference(preferenceKey, true) - export function EquationsEditor(props: EquationsProps) { const { focused, state } = props const dispatch = useAppDispatch() - const focusTree = useAppSelector(selectFocusTree) const focusedElement = useAppSelector(selectFocused) const nestedFocus = focused || @@ -64,22 +60,7 @@ export function EquationsEditor(props: EquationsProps) { state.transformationTarget.value ) - const gridFocus = useGridFocus({ - rows: state.steps.length, - columns: 4, - focusNext: () => dispatch(focusNext(focusTree)), - focusPrevious: () => dispatch(focusPrevious(focusTree)), - transformationTarget, - onFocusChanged: (state) => { - if (state === 'firstExplanation') { - dispatch(focus(props.state.firstExplanation.id)) - } else if (state.column === StepSegment.Explanation) { - dispatch(focus(props.state.steps[state.row].explanation.id)) - } else { - dispatch(focus(props.id)) - } - }, - }) + const pluginFocusWrapper = useRef(null) useHotkeys( 'tab', @@ -133,6 +114,29 @@ export function EquationsEditor(props: EquationsProps) { } ) + const gridFocus = useGridFocus({ + rows: state.steps.length, + columns: 4, + focusNext: () => { + const focusTree = selectFocusTree(store.getState()) + dispatch(focusNext(focusTree)) + }, + focusPrevious: () => { + const focusTree = selectFocusTree(store.getState()) + dispatch(focusPrevious(focusTree)) + }, + transformationTarget, + onFocusChanged: (state) => { + if (state === 'firstExplanation') { + dispatch(focus(props.state.firstExplanation.id)) + } else if (state.column === StepSegment.Explanation) { + dispatch(focus(props.state.steps[state.row].explanation.id)) + } else { + dispatch(focus(props.id)) + } + }, + }) + useEffect(() => { if (nestedFocus) { gridFocus.setFocus({ @@ -155,7 +159,7 @@ export function EquationsEditor(props: EquationsProps) { store.getState(), state.firstExplanation.id ) ? null : ( -
+
{state.firstExplanation.render()}
) @@ -179,7 +183,7 @@ export function EquationsEditor(props: EquationsProps) { store.getState(), explanation.id ) ? null : ( -
+
{explanation.render()}
), @@ -187,25 +191,14 @@ export function EquationsEditor(props: EquationsProps) { }) } + const hasFocusWithin = + typeof window !== 'undefined' && pluginFocusWrapper.current + ? pluginFocusWrapper.current.contains(document.activeElement) + : false + return ( - <> - {props.renderIntoSettings( -
- {' '} - -
- )} +
+ {props.focused || hasFocusWithin ? : null}
{renderFirstExplanation()} @@ -252,6 +245,7 @@ export function EquationsEditor(props: EquationsProps) { @@ -355,262 +350,13 @@ export function EquationsEditor(props: EquationsProps) { } } -interface StepEditorProps { - gridFocus: GridFocus - row: number - state: StateTypeReturnType - transformationTarget: TransformationTarget -} - -function StepEditor(props: StepEditorProps) { - const equationsStrings = useEditorStrings().plugins.equations - const { gridFocus, row, state, transformationTarget } = props - - return ( - <> - {transformationTarget === TransformationTarget.Equation && ( - - )} - - - {transformationTarget === TransformationTarget.Equation && ( - - )} - - ) -} - -interface InlineMathProps { - state: StateTypeReturnType - placeholder: string - onChange: (state: string) => void - onFocusNext: () => void - onFocusPrevious: () => void - focused?: boolean - prefix?: string - suffix?: string -} - -function InlineMath(props: InlineMathProps) { - const { - focused, - onFocusNext, - onFocusPrevious, - onChange, - state, - prefix = '', - suffix = '', - } = props - - const preferences = useContext(PreferenceContext) - - return ( - { - preferences.setKey(preferenceKey, visual) - }} - onInlineChange={() => {}} - onChange={onChange} - onMoveOutRight={onFocusNext} - onMoveOutLeft={onFocusPrevious} - /> - ) -} - -type GridFocusState = - | { - row: number - column: number - } - | 'firstExplanation' - -interface GridFocus { - focus: GridFocusState | null - isFocused: (cell: GridFocusState) => boolean - setFocus: (cell: GridFocusState) => void - moveRight: () => void - moveLeft: () => void -} - -function useGridFocus({ - rows, - columns, - focusNext, - focusPrevious, - onFocusChanged, - transformationTarget, -}: { - rows: number - columns: number - focusNext: () => void - focusPrevious: () => void - onFocusChanged: (args: GridFocusState) => void - transformationTarget: TransformationTarget -}): GridFocus { - const [focus, setFocusState] = useState(null) - const setFocus = (state: GridFocusState) => { - onFocusChanged(state) - setFocusState(state) - } - const isFocused = (state: GridFocusState) => { - if (focus === null) return false - if (focus === 'firstExplanation') return state === focus - - return ( - state !== 'firstExplanation' && - focus.row === state.row && - focus.column === state.column - ) - } - - return { - focus, - isFocused, - setFocus(state) { - if (!isFocused(state)) setFocus(state) - }, - moveRight() { - if (focus === null) return - if (focus === 'firstExplanation') { - setFocus({ row: 0, column: firstColumn(transformationTarget) }) - return - } - - if ( - focus.row === rows - 1 && - focus.column === lastColumn(transformationTarget) - ) { - focusNext() - } else if (transformationTarget === TransformationTarget.Term) { - if (focus.column === StepSegment.Right) { - setFocus({ row: focus.row, column: StepSegment.Explanation }) - } else { - setFocus({ - row: focus.row + 1, - column: firstColumn(transformationTarget), - }) - } - } else if (focus.column === columns - 1) { - setFocus({ row: focus.row + 1, column: StepSegment.Left }) - } else { - setFocus({ row: focus.row, column: focus.column + 1 }) - } - }, - moveLeft() { - if (focus === null) return - if (focus === 'firstExplanation') { - focusPrevious() - return - } - - if (transformationTarget === TransformationTarget.Term) { - if (focus.row === 0 && focus.column === StepSegment.Right) { - focusPrevious() - } else if (focus.column === StepSegment.Right) { - setFocus({ row: focus.row - 1, column: StepSegment.Explanation }) - } else { - setFocus({ row: focus.row, column: StepSegment.Right }) - } - } else { - if (focus.column === 0) { - if (focus.row === 0) { - setFocus('firstExplanation') - } else { - setFocus({ row: focus.row - 1, column: columns - 1 }) - } - } else { - setFocus({ row: focus.row, column: focus.column - 1 }) - } - } - }, - } -} - -function firstColumn(transformationTarget: TransformationTarget) { +export function firstColumn(transformationTarget: TransformationTarget) { return transformationTarget === TransformationTarget.Term ? StepSegment.Right : StepSegment.Left } -function lastColumn(transformationTarget: TransformationTarget) { +export function lastColumn(transformationTarget: TransformationTarget) { return transformationTarget === TransformationTarget.Term ? StepSegment.Right : StepSegment.Transform diff --git a/src/serlo-editor/plugins/equations/editor/grid-focus.tsx b/src/serlo-editor/plugins/equations/editor/grid-focus.tsx new file mode 100644 index 0000000000..f9223732cb --- /dev/null +++ b/src/serlo-editor/plugins/equations/editor/grid-focus.tsx @@ -0,0 +1,125 @@ +import { useState } from 'react' + +import { StepSegment } from './editor' +import { TransformationTarget } from './editor-renderer' + +type GridFocusState = + | { + row: number + column: number + } + | 'firstExplanation' + +export interface GridFocus { + focus: GridFocusState | null + isFocused: (cell: GridFocusState) => boolean + setFocus: (cell: GridFocusState) => void + moveRight: () => void + moveLeft: () => void +} + +export function useGridFocus({ + rows, + columns, + focusNext, + focusPrevious, + onFocusChanged, + transformationTarget, +}: { + rows: number + columns: number + focusNext: () => void + focusPrevious: () => void + onFocusChanged: (args: GridFocusState) => void + transformationTarget: TransformationTarget +}): GridFocus { + const [focus, setFocusState] = useState(null) + const setFocus = (state: GridFocusState) => { + onFocusChanged(state) + setFocusState(state) + } + const isFocused = (state: GridFocusState) => { + if (focus === null) return false + if (focus === 'firstExplanation') return state === focus + + return ( + state !== 'firstExplanation' && + focus.row === state.row && + focus.column === state.column + ) + } + + return { + focus, + isFocused, + setFocus(state) { + if (!isFocused(state)) setFocus(state) + }, + moveRight() { + if (focus === null) return + if (focus === 'firstExplanation') { + setFocus({ row: 0, column: firstColumn(transformationTarget) }) + return + } + + if ( + focus.row === rows - 1 && + focus.column === lastColumn(transformationTarget) + ) { + focusNext() + } else if (transformationTarget === TransformationTarget.Term) { + if (focus.column === StepSegment.Right) { + setFocus({ row: focus.row, column: StepSegment.Explanation }) + } else { + setFocus({ + row: focus.row + 1, + column: firstColumn(transformationTarget), + }) + } + } else if (focus.column === columns - 1) { + setFocus({ row: focus.row + 1, column: StepSegment.Left }) + } else { + setFocus({ row: focus.row, column: focus.column + 1 }) + } + }, + moveLeft() { + if (focus === null) return + if (focus === 'firstExplanation') { + focusPrevious() + return + } + + if (transformationTarget === TransformationTarget.Term) { + if (focus.row === 0 && focus.column === StepSegment.Right) { + focusPrevious() + } else if (focus.column === StepSegment.Right) { + setFocus({ row: focus.row - 1, column: StepSegment.Explanation }) + } else { + setFocus({ row: focus.row, column: StepSegment.Right }) + } + } else { + if (focus.column === 0) { + if (focus.row === 0) { + setFocus('firstExplanation') + } else { + setFocus({ row: focus.row - 1, column: columns - 1 }) + } + } else { + setFocus({ row: focus.row, column: focus.column - 1 }) + } + } + }, + } +} + +function firstColumn(transformationTarget: TransformationTarget) { + return transformationTarget === TransformationTarget.Term + ? StepSegment.Right + : StepSegment.Left +} + +function lastColumn(transformationTarget: TransformationTarget) { + return transformationTarget === TransformationTarget.Term + ? StepSegment.Right + : StepSegment.Transform +} diff --git a/src/serlo-editor/plugins/equations/editor/inline-math.tsx b/src/serlo-editor/plugins/equations/editor/inline-math.tsx new file mode 100644 index 0000000000..ee67b29990 --- /dev/null +++ b/src/serlo-editor/plugins/equations/editor/inline-math.tsx @@ -0,0 +1,54 @@ +import { useContext } from 'react' + +import { + PreferenceContext, + setDefaultPreference, +} from '@/serlo-editor/core/contexts' +import { MathEditor } from '@/serlo-editor/math' +import { StateTypeReturnType, StringStateType } from '@/serlo-editor/plugin' + +interface InlineMathProps { + state: StateTypeReturnType + placeholder: string + onChange: (state: string) => void + onFocusNext: () => void + onFocusPrevious: () => void + focused?: boolean + prefix?: string + suffix?: string +} + +const preferenceKey = 'katex:usevisualmath' + +setDefaultPreference(preferenceKey, true) + +export function InlineMath(props: InlineMathProps) { + const { + focused, + onFocusNext, + onFocusPrevious, + onChange, + state, + prefix = '', + suffix = '', + } = props + + const preferences = useContext(PreferenceContext) + + return ( + { + preferences.setKey(preferenceKey, visual) + }} + onInlineChange={() => {}} + onChange={onChange} + onMoveOutRight={onFocusNext} + onMoveOutLeft={onFocusPrevious} + /> + ) +} diff --git a/src/serlo-editor/plugins/equations/editor/step-editor.tsx b/src/serlo-editor/plugins/equations/editor/step-editor.tsx new file mode 100644 index 0000000000..c4e3f93b0d --- /dev/null +++ b/src/serlo-editor/plugins/equations/editor/step-editor.tsx @@ -0,0 +1,106 @@ +import { StepSegment } from './editor' +import { TransformationTarget } from './editor-renderer' +import { GridFocus } from './grid-focus' +import { InlineMath } from './inline-math' +import { stepProps } from '..' +import { renderSignToString, Sign } from '../sign' +import { useEditorStrings } from '@/contexts/logged-in-data-context' +import { StateTypeReturnType } from '@/serlo-editor/plugin' + +export interface StepEditorProps { + gridFocus: GridFocus + row: number + state: StateTypeReturnType + transformationTarget: TransformationTarget +} + +export function StepEditor(props: StepEditorProps) { + const equationsStrings = useEditorStrings().plugins.equations + const { gridFocus, row, state, transformationTarget } = props + + return ( + <> + {transformationTarget === TransformationTarget.Equation && ( + + )} + + + {transformationTarget === TransformationTarget.Equation && ( + + )} + + ) +} diff --git a/src/serlo-editor/plugins/equations/index.ts b/src/serlo-editor/plugins/equations/index.ts index 050cfdf10f..a8a82e5442 100644 --- a/src/serlo-editor/plugins/equations/index.ts +++ b/src/serlo-editor/plugins/equations/index.ts @@ -1,4 +1,4 @@ -import { EquationsEditor } from './editor' +import { EquationsEditor } from './editor/editor' import { Sign } from './sign' import { child, diff --git a/src/serlo-editor/plugins/equations/toolbar.tsx b/src/serlo-editor/plugins/equations/toolbar.tsx new file mode 100644 index 0000000000..87682d7def --- /dev/null +++ b/src/serlo-editor/plugins/equations/toolbar.tsx @@ -0,0 +1,36 @@ +import { EquationsProps } from '.' +import { TransformationTarget } from './editor/editor-renderer' +import { useEditorStrings } from '@/contexts/logged-in-data-context' +import { tw } from '@/helper/tw' +import { PluginToolbar } from '@/serlo-editor/editor-ui/plugin-toolbar' +import { PluginDefaultTools } from '@/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools' +import { EditorPluginType } from '@/serlo-editor-integration/types/editor-plugin-type' + +export const EquationsToolbar = ({ id, state }: EquationsProps) => { + const equationsStrings = useEditorStrings().plugins.equations + + return ( + state.transformationTarget.set(e.target.value)} + className={tw` + mr-2 cursor-pointer rounded-md !border border-gray-500 + bg-editor-primary-100 px-1 py-[1px] text-sm transition-all + hover:bg-editor-primary-200 focus:bg-editor-primary-200 focus:outline-none + `} + value={state.transformationTarget.value} + > + + + + } + pluginControls={} + /> + ) +} diff --git a/src/serlo-editor/plugins/exercise/editor.tsx b/src/serlo-editor/plugins/exercise/editor.tsx index 1545488731..5f33e86f39 100644 --- a/src/serlo-editor/plugins/exercise/editor.tsx +++ b/src/serlo-editor/plugins/exercise/editor.tsx @@ -24,7 +24,7 @@ export function ExerciseEditor({ editable, state }: ExerciseProps) { <> {content.render()} {interactive.defined ? ( - interactive.render({ renderToolbar }) + interactive.render({ renderSideToolbar }) ) : editable ? ( <>

@@ -47,7 +47,7 @@ export function ExerciseEditor({ editable, state }: ExerciseProps) { ) - function renderToolbar(children: ReactNode) { + function renderSideToolbar(children: ReactNode) { return ( <>

setShowOptions(false)}> diff --git a/src/serlo-editor/plugins/geogebra/editor.tsx b/src/serlo-editor/plugins/geogebra/editor.tsx index 0a783cafc4..cc8707183f 100644 --- a/src/serlo-editor/plugins/geogebra/editor.tsx +++ b/src/serlo-editor/plugins/geogebra/editor.tsx @@ -1,21 +1,28 @@ +import { useState } from 'react' + import { GeogebraProps } from '.' import { GeogebraRenderer, parseId } from './renderer' -import { EditorInput } from '../../editor-ui' +import { GeogebraToolbar } from './toolbar' import { FaIcon } from '@/components/fa-icon' -import { useEditorStrings } from '@/contexts/logged-in-data-context' import { entityIconMapping } from '@/helper/icon-by-entity-type' import { EmbedWrapper } from '@/serlo-editor/editor-ui/embed-wrapper' export function GeogebraEditor(props: GeogebraProps) { const { focused, editable, state } = props - - const editorStrings = useEditorStrings() + const [showSettingsModal, setShowSettingsModal] = useState(false) const { cleanId, url } = parseId(state.value) const couldBeValidId = cleanId.length === 8 return ( <> + {focused && ( + + )} {couldBeValidId ? ( ) : ( -
+
setShowSettingsModal(true)} + >
)} - {editable && focused ? renderInput() : null} ) - - function renderInput() { - return ( -
- ) => { - state.set(e.target.value) - }} - inputWidth="40%" - width="100%" - ref={props.autofocusRef} - className="ml-1" - /> -
- ) - } } diff --git a/src/serlo-editor/plugins/geogebra/toolbar.tsx b/src/serlo-editor/plugins/geogebra/toolbar.tsx new file mode 100644 index 0000000000..67b591dcf5 --- /dev/null +++ b/src/serlo-editor/plugins/geogebra/toolbar.tsx @@ -0,0 +1,65 @@ +import { faPencilAlt } from '@fortawesome/free-solid-svg-icons' +import { Dispatch, SetStateAction } from 'react' + +import { GeogebraProps } from '.' +import { EditorInput } from '../../editor-ui' +import { FaIcon } from '@/components/fa-icon' +import { ModalWithCloseButton } from '@/components/modal-with-close-button' +import { useEditorStrings } from '@/contexts/logged-in-data-context' +import { PluginToolbar } from '@/serlo-editor/editor-ui/plugin-toolbar' +import { PluginDefaultTools } from '@/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools' +import { EditorPluginType } from '@/serlo-editor-integration/types/editor-plugin-type' + +export const GeogebraToolbar = ({ + id, + state, + showSettingsModal, + setShowSettingsModal, +}: GeogebraProps & { + showSettingsModal: boolean + setShowSettingsModal: Dispatch> +}) => { + const editorStrings = useEditorStrings() + const geogebraStrings = editorStrings.plugins.geogebra + + return ( + + + {/* In the future we want a popovers per setting, but this is faster for now */} + {showSettingsModal ? ( + setShowSettingsModal(false)} + className="!top-1/3 !max-w-xl" + > +

+ {editorStrings.edtrIo.settings}: {geogebraStrings.title} +

+ +
+ state.set(e.target.value)} + inputWidth="100%" + width="100%" + className="block" + /> +
+
+ ) : null} + + } + pluginControls={} + /> + ) +} diff --git a/src/serlo-editor/plugins/h5p/index.tsx b/src/serlo-editor/plugins/h5p/index.tsx index ae6017bd8c..8140213e5b 100644 --- a/src/serlo-editor/plugins/h5p/index.tsx +++ b/src/serlo-editor/plugins/h5p/index.tsx @@ -102,15 +102,16 @@ function H5pEditor({ state, autofocusRef }: H5pProps) { if (mode === 'edit' || mode === 'loading') { return ( - <> -

Einfügen von H5P-Inhalt

+
+

Einfügen von H5P-Inhalt

-
-
- ) => { - const val = e.target.value - validateInput(val) - state.set(val) - }} - inputWidth="70%" - width="100%" - ref={autofocusRef} - /> -
-
- {error &&

{error}

} -

+

+ ) => { + const val = e.target.value + validateInput(val) + state.set(val) + }} + inputWidth="70%" + width="100%" + ref={autofocusRef} + /> +
+ {error &&

{error}

} +

+ Hinweis: Um existierende Inhalte zu nutzen, lade diese herunter, lade + sie in deinen Account hoch und stelle sie dort bereit.

-

- - Hinweis: Um existierende Inhalte zu nutzen, lade diese herunter, - lade sie in deinen Account hoch und stelle sie dort bereit. - -

- +
) } return ( <> -

+

H5P-Inhalt: {state.value} diff --git a/src/serlo-editor/plugins/highlight/editor.tsx b/src/serlo-editor/plugins/highlight/editor.tsx index fcce699db8..b6202bb3b2 100644 --- a/src/serlo-editor/plugins/highlight/editor.tsx +++ b/src/serlo-editor/plugins/highlight/editor.tsx @@ -1,10 +1,9 @@ import { useState } from 'react' import { HighlightProps } from '.' +import { HighlightToolbar } from './toolbar' import { useEditorStrings } from '@/contexts/logged-in-data-context' -const languages = ['text', 'c', 'javascript', 'jsx', 'markup', 'java', 'python'] - export function HighlightEditor(props: HighlightProps) { const { config, state, focused, editable } = props const { Renderer } = config @@ -15,12 +14,10 @@ export function HighlightEditor(props: HighlightProps) { const editorStrings = useEditorStrings() if (edit !== throttledEdit) { - if (!edit) { - setTimeout(() => { - setEditThrottled(false) - }, 100) - } else { + if (edit) { setEditThrottled(true) + } else { + setTimeout(() => setEditThrottled(false), 100) } } @@ -28,6 +25,7 @@ export function HighlightEditor(props: HighlightProps) { return throttledEdit || edit ? ( <> + {focused && } - {renderSettings()} ) : ( ) - - function renderSettings() { - return ( -

- - -
- ) - } } diff --git a/src/serlo-editor/plugins/highlight/toolbar.tsx b/src/serlo-editor/plugins/highlight/toolbar.tsx new file mode 100644 index 0000000000..a165ea8e19 --- /dev/null +++ b/src/serlo-editor/plugins/highlight/toolbar.tsx @@ -0,0 +1,69 @@ +import { faCheckCircle, faCircle } from '@fortawesome/free-regular-svg-icons' + +import { HighlightProps } from '.' +import { FaIcon } from '@/components/fa-icon' +import { useEditorStrings } from '@/contexts/logged-in-data-context' +import { tw } from '@/helper/tw' +import { EditorTooltip } from '@/serlo-editor/editor-ui/editor-tooltip' +import { PluginToolbar } from '@/serlo-editor/editor-ui/plugin-toolbar' +import { PluginDefaultTools } from '@/serlo-editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools' +import { EditorPluginType } from '@/serlo-editor-integration/types/editor-plugin-type' + +const languages = ['text', 'c', 'javascript', 'jsx', 'markup', 'java', 'python'] + +export const HighlightToolbar = ({ id, state }: HighlightProps) => { + const highlightStrings = useEditorStrings().plugins.highlight + + return ( + +
+ + +
+ + + } + pluginControls={} + /> + ) +} diff --git a/src/serlo-editor/plugins/image/controls.tsx b/src/serlo-editor/plugins/image/controls.tsx deleted file mode 100644 index e2621905f8..0000000000 --- a/src/serlo-editor/plugins/image/controls.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { faRedoAlt } from '@fortawesome/free-solid-svg-icons' - -import { ImageProps } from '.' -import { Upload } from './upload' -import { FaIcon } from '@/components/fa-icon' -import { useEditorStrings } from '@/contexts/logged-in-data-context' -import { EditorButton, EditorInput } from '@/serlo-editor/editor-ui' -import { isTempFile } from '@/serlo-editor/plugin' -import { - OverlayButton, - OverlayInput, - OverlayTextarea, -} from '@/serlo-editor/plugin/plugin-toolbar' - -export function PrimaryControls({ config, state, autofocusRef }: ImageProps) { - const imageStrings = useEditorStrings().plugins.image - const { src } = state - - const placeholder = !isTempFile(src.value) - ? imageStrings.placeholderEmpty - : !src.value.failed - ? imageStrings.placeholderUploading - : imageStrings.placeholderFailed - - return ( -

- state.src.set(e.target.value)} - width="70%" - inputWidth="80%" - ref={autofocusRef} - /> - {isTempFile(src.value) && src.value.failed ? ( - { - if (isTempFile(src.value) && src.value.failed) { - void src.upload(src.value.failed, config.upload) - } - }} - > - - - ) : null} - src.upload(file, config.upload)} /> -

- ) -} - -export function SettingsControls(props: ImageProps) { - const { state, config } = props - const { link, alt } = state - const imageStrings = useEditorStrings().plugins.image - - const isTemp = isTempFile(state.src.value) - const isFailed = isTempFile(state.src.value) && state.src.value.failed - - return ( - <> - state.src.set(e.target.value)} - /> -
- {isFailed ? ( - { - if (isTempFile(state.src.value) && state.src.value.failed) { - void state.src.upload( - state.src.value.failed, - props.config.upload - ) - } - }} - label={imageStrings.retry} - > - - - ) : null} - state.src.upload(file, config.upload)} /> -
- { - const { value } = target - if (alt.defined) { - if (value) alt.set(value) - else alt.remove() - } else alt.create(value) - }} - /> - { - const newHref = e.target.value - if (link.defined) { - if (newHref) link.href.set(newHref) - else link.remove() - } else link.create({ href: newHref }) - }} - /> - { - const value = parseInt(event.target.value) - if (state.maxWidth.defined) { - state.maxWidth.set(value) - } else { - state.maxWidth.create(value) - } - }} - /> - - ) -} diff --git a/src/serlo-editor/plugins/image/controls/inline-src-controls.tsx b/src/serlo-editor/plugins/image/controls/inline-src-controls.tsx new file mode 100644 index 0000000000..b05d8b55e1 --- /dev/null +++ b/src/serlo-editor/plugins/image/controls/inline-src-controls.tsx @@ -0,0 +1,33 @@ +import { ImageProps } from '..' +import { useEditorStrings } from '@/contexts/logged-in-data-context' +import { tw } from '@/helper/tw' +import { isTempFile } from '@/serlo-editor/plugin' + +export function InlineSrcControls({ state }: ImageProps) { + const imageStrings = useEditorStrings().plugins.image + const { src } = state + + const placeholder = !isTempFile(src.value) + ? imageStrings.placeholderEmpty + : !src.value.failed + ? imageStrings.placeholderUploading + : imageStrings.placeholderFailed + + return ( +

+ +

+ ) +} diff --git a/src/serlo-editor/plugins/image/controls/settings-modal-controls.tsx b/src/serlo-editor/plugins/image/controls/settings-modal-controls.tsx new file mode 100644 index 0000000000..559dfc7ddb --- /dev/null +++ b/src/serlo-editor/plugins/image/controls/settings-modal-controls.tsx @@ -0,0 +1,78 @@ +import { ImageProps } from '..' +import { useEditorStrings } from '@/contexts/logged-in-data-context' +import { tw } from '@/helper/tw' +import { isTempFile } from '@/serlo-editor/plugin' +import { OverlayInput } from '@/serlo-editor/plugin/plugin-toolbar' + +export function SettingsModalControls({ state }: Pick) { + const { link, alt, src, maxWidth } = state + const imageStrings = useEditorStrings().plugins.image + + const isTemp = isTempFile(src.value) + const isFailed = isTempFile(src.value) && src.value.failed + + return ( + <> + src.set(e.target.value)} + /> +
{step.explanation.render({ config: { + isInlineChildEditor: true, placeholder: row === 0 && transformationTarget === TransformationTarget.Term @@ -268,7 +262,7 @@ export function EquationsEditor(props: EquationsProps) { {renderAddButton()} - + ) function renderFirstExplanation() { @@ -281,6 +275,7 @@ export function EquationsEditor(props: EquationsProps) { {state.firstExplanation.render({ config: { placeholder: equationsStrings.firstExplanation, + isInlineChildEditor: true, }, })} gridFocus.setFocus({ row, column: StepSegment.Left })} - > - state.left.set(src)} - onFocusNext={() => gridFocus.moveRight()} - onFocusPrevious={() => gridFocus.moveLeft()} - /> - - {(transformationTarget === 'equation' || row !== 0) && ( - - )} - gridFocus.setFocus({ row, column: StepSegment.Right })} - > - state.right.set(src)} - onFocusNext={() => gridFocus.moveRight()} - onFocusPrevious={() => gridFocus.moveLeft()} - /> - - gridFocus.setFocus({ row, column: StepSegment.Transform }) - } - > - |{' '} - state.transform.set(src)} - onFocusNext={() => gridFocus.moveRight()} - onFocusPrevious={() => gridFocus.moveLeft()} - /> - gridFocus.setFocus({ row, column: StepSegment.Left })} + > + state.left.set(src)} + onFocusNext={() => gridFocus.moveRight()} + onFocusPrevious={() => gridFocus.moveLeft()} + /> + + {(transformationTarget === 'equation' || row !== 0) && ( + + )} + gridFocus.setFocus({ row, column: StepSegment.Right })} + > + state.right.set(src)} + onFocusNext={() => gridFocus.moveRight()} + onFocusPrevious={() => gridFocus.moveLeft()} + /> + + gridFocus.setFocus({ row, column: StepSegment.Transform }) + } + > + |{' '} + state.transform.set(src)} + onFocusNext={() => gridFocus.moveRight()} + onFocusPrevious={() => gridFocus.moveLeft()} + /> +