diff --git a/docs/markdown_editor_viewer.md b/docs/markdown_editor_viewer.md deleted file mode 100644 index 0f8c6d58dc..0000000000 --- a/docs/markdown_editor_viewer.md +++ /dev/null @@ -1,98 +0,0 @@ - -# Markdown Editor/Viewer - -We use TOAST UI (TUI) Editor v2 as a basis for our markdown [editor](../contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue) and [viewer](../contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownViewer/MarkdownViewer.vue). - -- [Documentation](https://ui.toast.com/tui-editor/) -- [GitHub](https://github.com/nhn/tui.editor) -- [API documentation](https://nhn.github.io/tui.editor/latest/) - - -## WYSIWYG/Markdown - -TUI Editor provides both WYSIWYG and markdown mode. Currently, we use WYSIWYG mode only. Implementation-wise that means that although we need to define conversions both ways (we store markdown on our servers), there is currently no emphasis on making markdown editor work completely because it's not visible to users at all. Some additional steps might be needed (e.g. initialization of MathQuill fields). - -However, one of the reasons to choose this library was its potential to scale (maybe to allow users familiar with markdown syntax to use it in the future). - -**Development tip**: As mentioned, our custom logic might not work entirely in markdown mode. However, when developing conversions, it might be helpful to set `hideModeSwitch` option to `false` when initializing the editor so you can see whether your basic conversion logic is working both ways properly. - - -## Viewer - -Besides the editor, TUI also provides the [viewer](https://github.com/nhn/tui.editor/blob/master/apps/editor/docs/viewer.md) that is a leaner version of the editor. Currently, we use it for assessment item's questions, answers, and hints preview. - - -## Under the hood - -TUI editor uses the following 3rd party libraries and exposes their API for public use. We use some of them for our custom plugins: - -**WYSIWYG mode** is built around [Squire](https://github.com/neilj/Squire). Sometimes TUI's editor API is sufficient though there are some cases when we need to access [Squire's API](https://github.com/neilj/Squire#api) directly. That can be done via [`getSquire()`](https://nhn.github.io/tui.editor/latest/ToastUIEditor#getSquire) method: - -```javascript - import Editor from '@toast-ui/editor'; - - const editor = new Editor() - const squire = editor.getSquire() -``` - -**Markdown mode** is built on top of [CodeMirror](https://codemirror.net/). We currently don't need to do that though it's API can be similarly accessed via [`getCodeMirror()`](https://nhn.github.io/tui.editor/latest/ToastUIEditor#getCodeMirror) if needed in the future. - -**Conversions from markdown to HTML** are processed by TUI editor's own markdown parser [ToastMark](https://github.com/nhn/tui.editor/tree/master/libs/toastmark). It can be extended by using [Custom HTML Renderer](https://github.com/nhn/tui.editor/blob/master/apps/editor/docs/custom-html-renderer.md). - -**Conversions from HTML to markdown** are processed by [to-mark](https://github.com/nhn/tui.editor/tree/master/libs/to-mark), another TUI editor's own library. Default conversion can be overwritten by defining a custom convertor and passing it into `customConvertor` parameter of [initialization`options`](https://nhn.github.io/tui.editor/latest/ToastUIEditor) . Unfortunately, it seems that there is no way to extend the default convertor using TUI Editor's public API at this point. That would be ideal for our use-case when we don't need to define the whole conversion logic but rather process some additional conversions (e.g. formulas). However, it can be extended using [this hackish solution](https://github.com/nhn/tui.editor/issues/615#issuecomment-527802641) which is how we currently extend conversions in this direction until there will be public API available. - - -## Custom plugins - -Our custom plugins are located in [plugins directory](../contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins). - -### [Formulas editor plugin](../contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/formulas) - -#### Adding new formulas - -1. A new formula's LaTeX representation is inserted as a new HTML element using `getSquire().insertHTML()`. It is also assigned a special class to denote that it is a new math field so that later we know which fields should be initialized as MathQuill static math fields. - -2. HTML is converted to markdown (conversions logic will be described later) - -3. All new math fields are initialized as MathQuill static math fields - -#### Editing existing formulas - -The steps are the same as when adding a new formula, except that instead of inserting a new formula element with `getSquire().insertHTML()`, an active element being edited is replaced by a new HTML using `parentNode.replaceChild()`. - -#### MathQuill - -We use [MathQuill's static math fields](http://docs.mathquill.com/en/latest/Getting_Started/#static-math-rendering) to render formulas in a list of all formulas in the formulas menu and in the editor. [Editable math fields](http://docs.mathquill.com/en/latest/Getting_Started/#editable-math-fields) are used in the edit part of the formulas menu. - -##### Customizations - -There is one customization in MathQuill code related to formulas logic: When initializing MathQuill fields (MathQuill replaces an element with its own HTML during that process), we add `data-formula` attribute to the root math element. Its value is the original formula's LaTeX representation. This attribute is used as a basis for conversion from HTML to markdown. - -**Important** -All MathQuill customizations are saved in [this commit](https://github.com/learningequality/studio/commit/9c85577761a75d1c3c216496f4e3373e57623699). There's a need to be careful to reflect them if we upgrade MathQuill one day (or create MathQuill fork for the sake of clarity if there's a need to upgrade more often or add more customizations). - -#### [HTML to Markdown conversion](../contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/formulas/formula-html-to-md.js) - -All elements in the input HTML containing `data-formula` attribute are replaced by a value saved in that attribute and wrapped in double `$`. - -#### [Markdown to HTML conversion](../contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/formulas/formula-md-to-html.js) - -All markdown substrings wrapped in double `$` are converted to `span` element and assigned `math-field` class. We use this class to know which elements should be initialized with MathQuill. - -This conversion is needed for rendering content in WYSIWYG after the first load (and eventually in the future if we allow users to switch to markdown mode on the fly) because we store markdown on our servers. - - -### [Image upload plugin](../contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload) - -#### Adding/editing images - -New images can be added in two ways: An image can be dropped into the editor's -content area, or it can be uploaded via the images menu. - -If a new image is dropped into the content area, then the images menu opens -with the image and takes over the file upload process. - -After an image has been successfully uploaded using the images menu, a dummy -image element is inserted into the editor's content area, and a Vue image -field component is mounted on it. This component is responsible for resizing, -editing, and removing an image. diff --git a/docs/rich_text_editor.md b/docs/rich_text_editor.md index a924925da9..c08bba23c6 100644 --- a/docs/rich_text_editor.md +++ b/docs/rich_text_editor.md @@ -1,277 +1,60 @@ -# Rich Text Editor Documentation +## Rich Text Editor Documentation -This Component replaces Studio’s text editor with a future-ready implementation, deliberately scoped to immediate needs: --​ Swap Toast UI while preserving Markdown storage --​ Support for core formatting --​ Support for more advanced formats (code and math blocks, image uploading) +Studio has a Rich text Editor that is currently being used in the exercise editor: (questions / answers / hints) -It uses TipTap which is a headless, framework-agnostic rich-text editor built on top of ProseMirror. -[https://tiptap.dev/docs](https://tiptap.dev/docs) +We use [TipTap](https://tiptap.dev/) that is a headless framework based on ProseMirror. It was built to replace a past long-lived Toast UI (TUI) based editor; that influenced some implementation decisions to maintain backward compatibility. ---- - -Currently the editor is accessible through hidden routes: -- `http://localhost:8080/en/channels//#/editor-dev` which requires a valid channel id first -- `http://localhost:8080/#editor-dev` - -## Current Folder Structure - -``` -TipTapEditor/ -├── assets/ -│ └── # icons -└── TipTapEditor/ -| ├── components/ -| │ ├── toolbar/ -| │ ├── ToolbarButton.vue -| │ ├── FormatDropdown.vue -| │ ├── PasteDropdown.vue -| │ └── ToolbarDivider.vue -| │ ├── EditorToolbar.vue -| | └── EditorContentWrapper.vue -| ├── composables/ -| │ ├── useDropdowns.js -| │ ├── useEditor.js -| │ └── useToolbarActions.js -| ├── extensions/ -| │ ├── SmallTextExtension.js -| └── TipTapEditor.vue # Main container -└── TipTapEditorStrings.js -``` -## Current Key Features - -- **Rich Text Formatting**: Bold, italic, underline, strikethrough -- **Typography**: Multiple heading levels (H1, H2, H3), normal text, and small text -- **Lists**: Bullet lists and numbered lists -- **Advanced Clipboard**: Copy with formatting, paste with/without formatting -- **History**: Undo/redo functionality -- **Accessibility**: Full keyboard navigation and ARIA support -- **RTL Support**: Right-to-left text direction support -- **Internationalization**: Built-in string management system -- **Custom Extensions**: Extensible architecture for additional features - -## Core Components - -### 1. TipTapEditor.vue - -**Main container component that orchestrates the entire editor** - -- **Purpose**: Acts as the root component that provides editor context to all child components -- **Key Features**: - - Uses composition API with `useEditor` composable - - Provides editor instance and ready state to child components via Vue's provide/inject - - Contains global typography styles for the editor content -- **Dependencies**: - - `EditorToolbar.vue` - - `EditorContentWrapper.vue` - - `useEditor` composable - -```vue -// Example usage - -``` - -### 2. EditorToolbar.vue - -**Main toolbar component containing all editing controls** - -- **Purpose**: Renders the complete toolbar with all formatting options -- **Structure**: Organized into logical groups with dividers: - - History actions (undo/redo) - - Format dropdown - - Text formatting (bold, italic, underline, strikethrough) - - Copy/paste controls - - List formatting - - Script formatting (subscript, superscript) - - Insert tools (image, link, math, code) -- **Accessibility**: Full ARIA support with role groups and labels -- **Dependencies**: Uses `useToolbarActions` composable for all action handlers - -### 3. EditorContentWrapper.vue - -**Wrapper for the actual editor content area** - -- **Purpose**: Provides the editable content area with proper styling -- **Features**: - - Proper padding and spacing - - Typography styles for all content types - - RTL text direction support -- **Styling**: Deep selectors for ProseMirror content styling - -## Toolbar Components - -### 4. ToolbarButton.vue - -**Reusable button component for toolbar actions** - -- **Props**: - - `title`: Button tooltip and accessibility label - - `icon`: Path to button icon - - `rtlIcon`: Optional RTL-specific icon - - `isActive`: Boolean indicating if button is in active state - - `isAvailable`: Boolean controlling button availability - - `shouldFlipInRtl`: Boolean for RTL icon flipping -- **Features**: - - Automatic RTL icon switching - - Disabled state handling - - Keyboard navigation (Enter/Space) - - Focus management with outline styles - - Active state styling - -### 5. FormatDropdown.vue - -**Dropdown for text format selection (Normal, Small, H1, H2, H3)** - -- **Features**: - - Dynamic format detection and display - - Live preview of formats in dropdown - - Full keyboard navigation (arrows, Enter, Escape, Home, End) - - Focus management - - ARIA menu implementation -- **Format Options**: - - Small text (12px) - - Normal paragraph (16px) - - Header 3 (18px) - - Header 2 (24px) - - Header 1 (32px) - -### 6. PasteDropdown.vue - -**Split button for paste operations** - -- **Structure**: - - Main paste button (standard paste with formatting) - - Dropdown arrow for paste options -- **Options**: - - Paste (with formatting) - - Paste without formatting (plain text) -- **Features**: - - Clipboard API integration - - HTML and plain text handling - - Keyboard navigation - - Split button interaction pattern - -### 7. ToolbarDivider.vue +Currently editor code lives in: https://github.com/learningequality/studio/tree/unstable/contentcuration/contentcuration/frontend/shared/views/TipTapEditor -**Visual separator between toolbar groups** +Another point that had an impact on our architectural decisions is that there are future plans to extract the editor to be part of Kolibri-Design-System to be used in Kolibri too. That meant we had to keep the editor as decoupled from the rest of the codebase as much as possible. -- Simple component providing consistent spacing and visual separation -- Helps organize toolbar into logical sections - -## Composables (Business Logic) - -### 8. useEditor.js - -**Core editor initialization and lifecycle management** - -- **Purpose**: Creates and manages the TipTap editor instance -- **Extensions Used**: - - StarterKit (basic functionality) - - Underline extension - - Custom Small text extension -- **Lifecycle**: Handles editor creation on mount and cleanup on unmount -- **Returns**: Editor instance and ready state - -```javascript -const { editor, isReady } = useEditor() -``` - -### 9. useToolbarActions.js - -**All toolbar action handlers and state management** - -- **Individual Handlers**: Each formatting action (bold, italic, etc.) -- **Action Groups**: Organized arrays of related actions -- **Features**: - - Copy with HTML and plain text formats - - Intelligent paste handling - - Active state detection for buttons - - Button availability checking (undo/redo) - - Format application logic -- **Internationalization**: Uses translator function for button labels - -### 10. useDropdowns.js - -**Dropdown state management and interaction logic** - -- **State Management**: - - Current format detection - - Dropdown open/close states - - Format options configuration -- **Format Detection**: Real-time monitoring of cursor position to update selected format -- **Event Handling**: Click outside detection for dropdown closing -- **Editor Integration**: Listens to editor transactions for format updates - -## Extensions - -### 11. SmallTextExtension.js - -**Custom TipTap extension for small text formatting** - -- **Type**: Block node extension -- **Features**: - - Creates `` HTML elements - - Block-level content with inline children - - Custom CSS class (`small-text`) - - Keyboard shortcut (Mod+Shift+S) -- **Commands**: - - `setSmall()`: Convert current block to small text - - `toggleSmall()`: Toggle between small text and paragraph - - `unsetSmall()`: Convert back to paragraph -- **Priority**: High priority (1000) for proper node precedence - ->[!NOTE] -> Why a Node instead of a Mark? -> - Semantic structure: ` `is semantically a block-level structure in our context, representing a complete unit of small text rather than just formatted text spans. -> - Content integrity: As a Node, we can ensure the entire content maintains consistent styling and behavior. -> - Block-level control: Using a Node allows us to treat small text as a distinct content block that can be manipulated as a whole unit in the editor. -> - DOM structure: We want a proper `` element in the output HTML rather than just applying a class or style to spans of text. - - -### 12. TipTapEditorStrings.js - -**Centralized string management for internationalization** - -- **Structure**: - - Namespace-based organization - - Message key-value pairs - - Translator factory function -- **Coverage**: All user-facing strings including: - - Button labels and tooltips - - Format options - - Accessibility labels - - Dropdown options -- **Usage**: Provides `getTranslator()` function for components - ->[!NOTE] ->Uses Lazy Loading pattern to be able to use it without the need for it to be inside a vue lie cycle hook. -## Styling Architecture - -asheres to [the suggested figma design](https://www.figma.com/design/uw8lx88ZKZU8X7kN9SdLeo/Rich-text-editor---GSOC-2025?node-id=377-633&t=0XAXleYjjGY2Fxzc-0) -### Component Styles - -- Scoped styles for component isolation -- Deep selectors for ProseMirror content -- CSS custom properties for theming -- Focus management with outline styles +## Useful Links +- Original figma design [link](https://www.figma.com/design/uw8lx88ZKZU8X7kN9SdLeo/Rich-text-editor---GSOC-2025?node-id=377-422&p=f&t=HIkJ8pF9xudcOnLd-0) +- Original Tracking issue for creating the editor [link](https://github.com/learningequality/studio/issues/5049) +- Tiptap basic editor [docs](https://tiptap.dev/docs/editor/getting-started/overview) +--- +## Custom extensions +For non-text elements, we create [custom extensions optionally with their node views](https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views/vue). +We currently have custom extensions for: +- images +- formulas +- syntax highlighted code blocks +- links +- `` text nodes + +### How to add a custom plugin? +This is a very high level guide, you'll still need to check the docs but make sure you check all the boxes in this list: +1. Create a new file in + `TipTapEditor/extensions/` +2. Define your node or mark using TipTap’s `Node.create()` or `Mark.create()`. +3. Add the new extension to the editor’s extension list in `TipTapEditor/composables/useEditor.js`. +4. If your node needs Markdown support, update the custom serializer in `TipTapEditor/utils/MarkdownSerializer.js` and don't forget to update the tests accordingly! +--- +## Content Conversion Flow +The old content API saved markdown in the database, the following data conversion flow maintains backward compatibility by implementing dual conversion between the strcutured JSON format TipTap uses and markdown. -### Accessibility Features +We support the conversion for: +- Standard Markdown elements previously handled by the ToastUI editor and its Showdown converter. +- A specific, legacy format for custom nodes, particularly for Images `(![alt](placeholder/checksum.ext =WxH))` and Math Formulas `$$latex$$` -- High contrast focus indicators -- ARIA roles and properties -- Keyboard navigation support -- Screen reader friendly labels +The formats for the custom nodes are adapted from the old editor's standard syntax conversion. +We have our own custom markdown serializer for that too! The following graph illustrates the whole flow. +image -## Technical Specifications +--- +## Mobile View +As per the figma design, the mobile view is different from the desktop design to a point where it can't just be fixed with just CSS tweaks or media queries. We did some thinking&research and decided to take a Conditional Toolbar Layout approach where We've created different components for different screen sizes. -### Dependencies +That means, if you add a new button in the desktop's toolbar, you'll have to add it to the Mobile's toolbar component too, and make sure you keep the functionality extracted in a reusable way so you only repeat the template logic and not the whole javascript! -- **TipTap**: Core editor framework -- **TipTap Extensions**: StarterKit, Underline -- **Clipboard API**: Modern clipboard operations +As per the Figma design, the **mobile view** differs significantly from the desktop layout — more than what simple CSS tweaks or media queries can handle. -### Browser Support +We decided to take a **Conditional Toolbar Layout** approach: +- Different toolbar components are used for desktop and mobile. +- The logic (commands, editor state, etc.) is shared and reusable. +- Only the **template structure** differs. -- Modern browsers with Clipboard API support -- RTL text direction support -- Keyboard navigation compatibility +>[!TIP] +>That means: +> If you add a new button to the desktop toolbar, you’ll also need to add it to the mobile toolbar component. +> Keep the functionality extracted and reusable, so you only duplicate the **template**, not the **JavaScript logic**.