diff --git a/packages/modules.editor/package.json b/packages/modules.editor/package.json index 975aadc0..1615e2a6 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 a9b04ad0..e040b05a 100644 --- a/packages/modules.editor/src/config/editorConfig.ts +++ b/packages/modules.editor/src/config/editorConfig.ts @@ -11,6 +11,8 @@ 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 'highlight.js/styles/base16/atelier-cave-light.min.css'; +import { CustomCodeNode } from '../extensions/code'; /** Курсор в стиле доски: вертикальная линия + шильдик с именем */ function collaborationCaretRender(user: { name?: string; color?: string }): HTMLElement { @@ -65,6 +67,7 @@ export const getExtensions = ( width: 2, color: '#3b82f6', }, + codeBlock: false, }), CustomImage, Underline, @@ -94,6 +97,7 @@ export const getExtensions = ( MoveBlockKeyboard, NormalizeSelection, ExtraShortcuts, + CustomCodeNode, ]; if (!provider || !ydoc) { diff --git a/packages/modules.editor/src/extensions/code/CodeBlockNodeView.tsx b/packages/modules.editor/src/extensions/code/CodeBlockNodeView.tsx new file mode 100644 index 00000000..49e8491e --- /dev/null +++ b/packages/modules.editor/src/extensions/code/CodeBlockNodeView.tsx @@ -0,0 +1,132 @@ +import { NodeViewWrapper, NodeViewProps, NodeViewContent } from '@tiptap/react'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from '@xipkg/dropdown'; +import { Button } from '@xipkg/button'; +import { MoreVert, Copy, Trash } from '@xipkg/icons'; +import { useBlockMenuActions, useYjsContext } from '../../hooks'; +import { cn } from '@xipkg/utils'; +import { useCallback } from 'react'; +import { ActiveBlockT } from '../../types'; +import { common } from 'lowlight'; +import { toast } from 'sonner'; + +export const CodeBlockNodeView = ({ node, getPos, updateAttributes }: NodeViewProps) => { + const currentLang = node.attrs.language || 'plaintext'; + + const { editor, isReadOnly } = useYjsContext(); + + // Функция точного определения текущего блока для хука + const getActiveBlock = useCallback((): ActiveBlockT | undefined => { + if (typeof getPos !== 'function' || !editor) return; + try { + const pos = getPos(); + if (pos == null || pos < 0) return; + const { doc } = editor.state; + if (pos >= doc.content.size) return; + + const $pos = doc.resolve(pos); + const nodeAtPos = $pos.nodeAfter; + + if (nodeAtPos?.type.name === 'codeBlock') { + return { editor, node: nodeAtPos, pos }; + } + } catch { + return; + } + }, [editor, getPos]); + + const { remove } = useBlockMenuActions(editor, getActiveBlock); + + // Копирование содержимого блока кода в буфер обмена + const handleCopyCode = () => { + const codeText = node.textContent; + navigator.clipboard.writeText(codeText); + toast.success('Код скопирован в буфер обмена'); + }; + + // Изменение языка через встроенный метод Tiptap `updateAttributes` + const handleLanguageChange = (newLang: string) => { + updateAttributes({ language: newLang }); + }; + + const availableLanguages = Object.keys(common); + + return ( + +
+ + + + + + handleLanguageChange('plaintext')} + > + Plain text + + {availableLanguages.map((lang) => ( + handleLanguageChange(lang)} + > + {lang} + + ))} + + + + + + + + + + Копировать + + + {!isReadOnly && ( + + + Удалить блок + + )} + + +
+ + {/* Обязательный контейнер Tiptap для рендеринга редактируемого текста */} +
+        
+      
+
+ ); +}; diff --git a/packages/modules.editor/src/extensions/code/CustomCodeNode.tsx b/packages/modules.editor/src/extensions/code/CustomCodeNode.tsx new file mode 100644 index 00000000..7237aac7 --- /dev/null +++ b/packages/modules.editor/src/extensions/code/CustomCodeNode.tsx @@ -0,0 +1,16 @@ +import { ReactNodeViewRenderer } from '@tiptap/react'; +import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'; +import { common, createLowlight } from 'lowlight'; +import { CodeBlockNodeView } from './CodeBlockNodeView'; + +const lowlight = createLowlight(common); + +export const CustomCodeNode = CodeBlockLowlight.configure({ + lowlight, + enableTabIndentation: true, + defaultLanguage: 'plaintext', +}).extend({ + addNodeView() { + return ReactNodeViewRenderer(CodeBlockNodeView); + }, +}); diff --git a/packages/modules.editor/src/extensions/code/index.ts b/packages/modules.editor/src/extensions/code/index.ts new file mode 100644 index 00000000..c711bf34 --- /dev/null +++ b/packages/modules.editor/src/extensions/code/index.ts @@ -0,0 +1,2 @@ +export { CodeBlockNodeView } from './CodeBlockNodeView.tsx'; +export { CustomCodeNode } from './CustomCodeNode.tsx'; diff --git a/packages/modules.editor/src/extensions/image/ImageNodeView.tsx b/packages/modules.editor/src/extensions/image/ImageNodeView.tsx index 27b81813..d941a2d1 100644 --- a/packages/modules.editor/src/extensions/image/ImageNodeView.tsx +++ b/packages/modules.editor/src/extensions/image/ImageNodeView.tsx @@ -10,12 +10,11 @@ import { Button } from '@xipkg/button'; import { ArrowBottom, ArrowUp, Copy, Download, MoreVert, Trash } from '@xipkg/icons'; import { useBlockMenuActions, useProtectedImage, useYjsContext } from '../../hooks'; import { cn } from '@xipkg/utils'; -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import { ActiveBlockT } from '../../types'; import { NodeSelection } from '@tiptap/pm/state'; export const ImageNodeView = ({ node, getPos }: NodeViewProps) => { - const [hovered, setHovered] = useState(false); const src = node.attrs.src; const { editor, storageToken, isReadOnly } = useYjsContext(); @@ -60,12 +59,7 @@ export const ImageNodeView = ({ node, getPos }: NodeViewProps) => { const imageSrc = useProtectedImage(src, storageToken); return ( - setHovered(true)} - onMouseLeave={() => setHovered(false)} - > + {node.attrs.alt { />
diff --git a/packages/modules.editor/src/hooks/useBlockMenuActions.ts b/packages/modules.editor/src/hooks/useBlockMenuActions.ts index 6bb0836d..4c949389 100644 --- a/packages/modules.editor/src/hooks/useBlockMenuActions.ts +++ b/packages/modules.editor/src/hooks/useBlockMenuActions.ts @@ -112,6 +112,23 @@ export const useBlockMenuActions = ( return createBlock(editor, type, activeBlock); }; + 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; const activeBlock = getActiveBlock(); @@ -144,6 +161,7 @@ export const useBlockMenuActions = ( downloadImage, moveDown, moveUp, + insertCode, insertBlock, }; }; diff --git a/packages/modules.editor/src/ui/components/BlockMenu.tsx b/packages/modules.editor/src/ui/components/BlockMenu.tsx index 44566fd9..48d8bb6f 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'; @@ -43,7 +43,7 @@ export const BlockMenu = ({ }: BlockMenuPropsT) => { const isMac = navigator.platform.toUpperCase().includes('MAC'); const { openModal } = useInterfaceStore(); - const { insertBlock, duplicate, remove, moveUp, moveDown } = useBlockMenuActions( + const { insertBlock, duplicate, remove, moveUp, moveDown, insertCode } = useBlockMenuActions( editor, getActiveBlock, ); @@ -56,7 +56,7 @@ export const BlockMenu = ({ } return ( - + {children} Изображение + insertCode('')}> + + Вставить код + diff --git a/packages/modules.editor/src/ui/components/DragHandleWrapper.tsx b/packages/modules.editor/src/ui/components/DragHandleWrapper.tsx index bd67a6c8..8a30938b 100644 --- a/packages/modules.editor/src/ui/components/DragHandleWrapper.tsx +++ b/packages/modules.editor/src/ui/components/DragHandleWrapper.tsx @@ -82,7 +82,7 @@ export const DragHandleWrapper = ({ nested onNodeChange={handleNodeChange} > -
+
=0.4.0'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -9396,6 +9421,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff@8.0.4: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} @@ -9842,6 +9870,10 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} @@ -10296,8 +10328,11 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lru-cache@11.5.1: - resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + lowlight@3.3.0: + resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} + + lru-cache@11.5.0: + resolution: {integrity: sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -12626,13 +12661,13 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@ibodr/draw@0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@ibodr/draw@0.1.9(@floating-ui/dom@1.7.6)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@ibodr/driver': 0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@ibodr/editor': 0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@ibodr/driver': 0.1.9(@floating-ui/dom@1.7.6)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@ibodr/editor': 0.1.9(@floating-ui/dom@1.7.6)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@ibodr/store': 0.1.9(react@19.2.7) '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) - '@tiptap/extension-code': 3.26.1(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) + '@tiptap/extension-code': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) '@tiptap/extension-highlight': 3.26.1(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) '@tiptap/pm': 3.23.6 '@tiptap/react': 3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -12647,9 +12682,9 @@ snapshots: - '@types/react' - '@types/react-dom' - '@ibodr/driver@0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@ibodr/driver@0.1.9(@floating-ui/dom@1.7.6)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@ibodr/editor': 0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@ibodr/editor': 0.1.9(@floating-ui/dom@1.7.6)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@ibodr/utils': 0.1.9 transitivePeerDependencies: - '@floating-ui/dom' @@ -12658,7 +12693,7 @@ snapshots: - react - react-dom - '@ibodr/editor@0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@ibodr/editor@0.1.9(@floating-ui/dom@1.7.6)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@ibodr/schema': 0.1.9(react@19.2.7) '@ibodr/state': 0.1.9 @@ -14461,12 +14496,20 @@ snapshots: dependencies: '@tiptap/extension-list': 3.26.1(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + '@tiptap/extension-code-block-lowlight@3.26.1(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/extension-code-block@3.26.1(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(highlight.js@11.11.1)(lowlight@3.3.0)': + dependencies: + '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) + '@tiptap/extension-code-block': 3.26.1(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) + '@tiptap/pm': 3.23.6 + highlight.js: 11.11.1 + lowlight: 3.3.0 + '@tiptap/extension-code-block@3.26.1(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)': dependencies: '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) '@tiptap/pm': 3.23.6 - '@tiptap/extension-code@3.26.1(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))': + '@tiptap/extension-code@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))': dependencies: '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6) @@ -14641,7 +14684,7 @@ snapshots: '@tiptap/extension-blockquote': 3.26.1(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) '@tiptap/extension-bold': 3.26.1(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) '@tiptap/extension-bullet-list': 3.26.1(@tiptap/extension-list@3.26.1(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)) - '@tiptap/extension-code': 3.26.1(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) + '@tiptap/extension-code': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) '@tiptap/extension-code-block': 3.26.1(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6) '@tiptap/extension-document': 3.26.1(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)) '@tiptap/extension-dropcursor': 3.26.1(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)) @@ -14764,6 +14807,10 @@ snapshots: '@types/estree@1.0.9': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} '@types/node@20.19.43': @@ -14786,6 +14833,8 @@ snapshots: '@types/trusted-types@2.0.7': {} + '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} '@typescript-eslint/eslint-plugin@8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.7.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.7.3)': @@ -15988,10 +16037,16 @@ snapshots: delayed-stream@1.0.0: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + diff@8.0.4: {} doctrine@2.1.0: @@ -16542,6 +16597,8 @@ snapshots: dependencies: function-bind: 1.1.2 + highlight.js@11.11.1: {} + html-parse-stringify@3.0.1: dependencies: void-elements: 3.1.0 @@ -16926,7 +16983,13 @@ snapshots: loupe@3.2.1: {} - lru-cache@11.5.1: {} + lowlight@3.3.0: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + highlight.js: 11.11.1 + + lru-cache@11.5.0: {} lru-cache@5.1.1: dependencies: @@ -17106,7 +17169,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.5.1 + lru-cache: 11.5.0 minipass: 7.1.3 pathe@2.0.3: {}