From a83ea46ebc31f8d710bde492867f6198c66ae314 Mon Sep 17 00:00:00 2001 From: Abramova Elizaveta Date: Tue, 16 Jun 2026 16:44:52 +0400 Subject: [PATCH 1/4] feat(165): add code block --- packages/modules.editor/package.json | 16 ++-- .../modules.editor/src/config/editorConfig.ts | 11 +++ .../src/hooks/useBlockMenuActions.ts | 17 ++++ .../src/store/interfaceStore.ts | 6 +- .../src/ui/components/BlockMenu.tsx | 6 +- .../src/ui/components/CodeInsertDialog.tsx | 85 +++++++++++++++++++ .../src/ui/components/EditorToolkit.tsx | 2 + packages/modules.editor/src/ui/editor.css | 20 +++++ pnpm-lock.yaml | 74 ++++++++++++++++ 9 files changed, 227 insertions(+), 10 deletions(-) create mode 100644 packages/modules.editor/src/ui/components/CodeInsertDialog.tsx diff --git a/packages/modules.editor/package.json b/packages/modules.editor/package.json index 975aadc09..1615e2a64 100644 --- a/packages/modules.editor/package.json +++ b/packages/modules.editor/package.json @@ -11,23 +11,24 @@ "dev": "tsc --watch" }, "dependencies": { - "@tanstack/react-router": "^1.166.6", "@dnd-kit/core": "6.3.1", "@dnd-kit/modifiers": "9.0.0", "@dnd-kit/sortable": "10.0.0", "@floating-ui/core": "1.7.0", "@floating-ui/react": "0.27.8", "@hocuspocus/provider": "3.4.4", + "@tanstack/react-router": "^1.166.6", "@tiptap/core": "^3.23.6", + "@tiptap/extension-code-block-lowlight": "^3.26.1", "@tiptap/extension-collaboration": "^3.23.6", "@tiptap/extension-collaboration-caret": "^3.23.6", "@tiptap/extension-drag-handle": "^3.23.6", "@tiptap/extension-drag-handle-react": "^3.23.6", - "@tiptap/extension-underline": "^3.23.6", - "@tiptap/extension-text-align": "^3.23.6", + "@tiptap/extension-image": "^3.23.6", "@tiptap/extension-list": "^3.23.6", + "@tiptap/extension-text-align": "^3.23.6", + "@tiptap/extension-underline": "^3.23.6", "@tiptap/extension-unique-id": "^3.23.6", - "@tiptap/extension-image": "^3.23.6", "@tiptap/pm": "^3.23.6", "@tiptap/react": "^3.23.6", "@tiptap/starter-kit": "^3.23.6", @@ -44,17 +45,18 @@ "common.config": "workspace:*", "common.env": "workspace:*", "common.services": "workspace:*", - "common.ui": "workspace:*", "common.types": "workspace:*", + "common.ui": "workspace:*", + "lowlight": "^3.3.0", "nanoid": "5.1.5", "prismjs": "^1.30.0", "randomcolor": "0.6.2", "sonner": "^2.0.7", + "webpfy": "1.0.9", "y-prosemirror": "^1.3.7", "y-protocols": "^1.0.6", "yjs": "^13.6.27", - "zustand": "^5.0.4", - "webpfy": "1.0.9" + "zustand": "^5.0.4" }, "devDependencies": { "@eslint/js": "^9.19.0", diff --git a/packages/modules.editor/src/config/editorConfig.ts b/packages/modules.editor/src/config/editorConfig.ts index a9b04ad08..942407502 100644 --- a/packages/modules.editor/src/config/editorConfig.ts +++ b/packages/modules.editor/src/config/editorConfig.ts @@ -11,6 +11,9 @@ import { UniqueID } from '@tiptap/extension-unique-id'; import * as Y from 'yjs'; import { CustomImage, MoveBlockKeyboard, NormalizeSelection } from '../extensions'; import { ExtraShortcuts } from '../extensions/extra-keyboard-shortcuts'; +import { common, createLowlight } from 'lowlight'; +import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'; +import 'highlight.js/styles/tokyo-night-dark.css'; /** Курсор в стиле доски: вертикальная линия + шильдик с именем */ function collaborationCaretRender(user: { name?: string; color?: string }): HTMLElement { @@ -39,6 +42,8 @@ function collaborationSelectionRender(user: { name?: string; color?: string }): }; } +const lowlight = createLowlight(common); + export const getExtensions = ( provider: HocuspocusProvider | undefined, ydoc: Y.Doc | undefined, @@ -65,6 +70,7 @@ export const getExtensions = ( width: 2, color: '#3b82f6', }, + codeBlock: false, }), CustomImage, Underline, @@ -94,6 +100,11 @@ export const getExtensions = ( MoveBlockKeyboard, NormalizeSelection, ExtraShortcuts, + CodeBlockLowlight.configure({ + lowlight, + enableTabIndentation: true, + defaultLanguage: 'plaintext', + }), ]; if (!provider || !ydoc) { diff --git a/packages/modules.editor/src/hooks/useBlockMenuActions.ts b/packages/modules.editor/src/hooks/useBlockMenuActions.ts index ea49b1446..2d2190c98 100644 --- a/packages/modules.editor/src/hooks/useBlockMenuActions.ts +++ b/packages/modules.editor/src/hooks/useBlockMenuActions.ts @@ -77,6 +77,22 @@ export const useBlockMenuActions = ( link.click(); }; + const insertCode = (codeText: string, language: string = 'plaintext') => { + if (!editor || !editor.isEditable) return; + + const endPos = editor.state.doc.content.size; + editor + .chain() + .focus() + .insertContentAt(endPos, { + type: 'codeBlock', + attrs: { language: language || 'plaintext' }, + content: codeText ? [{ type: 'text', text: codeText }] : [], + }) + .run(); + return; + }; + // В момент вызова получаем свежую позицию const moveUp = () => { if (!getActiveBlock) return; @@ -110,6 +126,7 @@ export const useBlockMenuActions = ( downloadImage, moveDown, moveUp, + insertCode, }; }; diff --git a/packages/modules.editor/src/store/interfaceStore.ts b/packages/modules.editor/src/store/interfaceStore.ts index cc5418c55..8f7524bdb 100644 --- a/packages/modules.editor/src/store/interfaceStore.ts +++ b/packages/modules.editor/src/store/interfaceStore.ts @@ -1,12 +1,14 @@ import { create } from 'zustand'; +type ModalType = 'uploadImage' | 'insertCode' | null; + type useInterfaceStoreT = { activeCellControls: string | null; isAddNewNode: string | null; setActiveCellControls: (newValue: string | null) => void; setIsAddNewNode: (newValue: string | null) => void; - activeModal: 'uploadImage' | null; - openModal: (modal: 'uploadImage') => void; + activeModal: ModalType; + openModal: (modal: ModalType) => void; closeModal: () => void; }; diff --git a/packages/modules.editor/src/ui/components/BlockMenu.tsx b/packages/modules.editor/src/ui/components/BlockMenu.tsx index 7488f4966..754600cff 100644 --- a/packages/modules.editor/src/ui/components/BlockMenu.tsx +++ b/packages/modules.editor/src/ui/components/BlockMenu.tsx @@ -5,7 +5,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@xipkg/dropdown'; -import { Copy, H1, H2, H3, Text, Trash, Image, ArrowUp, ArrowBottom } from '@xipkg/icons'; +import { Copy, H1, H2, H3, Text, Trash, Image, ArrowUp, ArrowBottom, Code } from '@xipkg/icons'; import { ReactNode } from 'react'; import { useBlockMenuActions } from '../../hooks'; import { Editor } from '@tiptap/core'; @@ -89,6 +89,10 @@ export const BlockMenu = ({ Изображение + openModal('insertCode')}> + + Вставить код + diff --git a/packages/modules.editor/src/ui/components/CodeInsertDialog.tsx b/packages/modules.editor/src/ui/components/CodeInsertDialog.tsx new file mode 100644 index 000000000..f35fd1c7b --- /dev/null +++ b/packages/modules.editor/src/ui/components/CodeInsertDialog.tsx @@ -0,0 +1,85 @@ +import { useState } from 'react'; +import { useInterfaceStore } from '../../store/interfaceStore'; +import { useBlockMenuActions } from '../../hooks/useBlockMenuActions'; +import { Modal, ModalContent, ModalTitle } from '@xipkg/modal'; +import { Button } from '@xipkg/button'; +import { useYjsContext } from '../../hooks'; +import { common } from 'lowlight'; + +export const InsertCodeModal = () => { + const { activeModal, closeModal } = useInterfaceStore(); + const { editor } = useYjsContext(); + + const { insertCode } = useBlockMenuActions(editor); + + const [code, setCode] = useState(''); + const [lang, setLang] = useState('plaintext'); + const isOpen = activeModal === 'insertCode'; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + insertCode(code, lang); + + setCode(''); + closeModal(); + }; + + const availableLanguages = Object.keys(common); + + return ( + + +
+ +
+ + +
+
+
+ Выбрать язык: + +
+ +
+