From a592dd9f67694aef47bbe7e0a95ba44346360082 Mon Sep 17 00:00:00 2001 From: Caesar-ch <74941512+Caesar-ch@users.noreply.github.com> Date: Thu, 24 Aug 2023 15:29:26 +0800 Subject: [PATCH] feat(rich-text-editor): support code highlight (#440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(rich-text-editor): support code highlight * feat(rich-text-editor): support code highlight * style(rich-text-editor): code style adjust * style(rich-text-editor): code style adjust --------- Co-authored-by: 常浩-BJS21325 --- .../src/rich-text-edtior/code-highlight.tsx | 41 ++ .../renderless/src/rich-text-edtior/vue.ts | 10 +- .../theme/src/rich-text-editor/index.less | 362 +++++++++++------- .../vue/src/rich-text-editor/package.json | 5 + packages/vue/src/rich-text-editor/src/pc.vue | 29 +- 5 files changed, 308 insertions(+), 139 deletions(-) create mode 100644 packages/renderless/src/rich-text-edtior/code-highlight.tsx diff --git a/packages/renderless/src/rich-text-edtior/code-highlight.tsx b/packages/renderless/src/rich-text-edtior/code-highlight.tsx new file mode 100644 index 000000000..879abfe6a --- /dev/null +++ b/packages/renderless/src/rich-text-edtior/code-highlight.tsx @@ -0,0 +1,41 @@ +export default function (NodeViewContent, nodeViewProps, NodeViewWrapper) { + return { + name: 'CodeHighlight', + components: { + NodeViewWrapper, + NodeViewContent, + }, + props: nodeViewProps, + data() { + return { + languages: this.extension.options.lowlight.listLanguages(), + } + }, + computed: { + selectedLanguage: { + get() { + return this.node.attrs.language + }, + set(language) { + this.updateAttributes({ language }) + }, + }, + }, + render() { + return ( + + +
+
+ ) + }, + } +} \ No newline at end of file diff --git a/packages/renderless/src/rich-text-edtior/vue.ts b/packages/renderless/src/rich-text-edtior/vue.ts index a408e3d2c..6a4100678 100644 --- a/packages/renderless/src/rich-text-edtior/vue.ts +++ b/packages/renderless/src/rich-text-edtior/vue.ts @@ -1,10 +1,11 @@ import { handleChange, setLink, handleMove, handleClickOutside, removeClickOutside, handleClick, shouldShow } from './index' +import Codehighlight from './code-highlight' export const api = ['state', 'setLink', 'handleChange', 'box', 'handleMove', 'handleClickOutside', 'removeClickOutside', 'handleClick', 'shouldShow'] export const renderless = ( props, { computed, onMounted, onBeforeUnmount, reactive, ref }, { vm, emit, parent }, - { useEditor, Collaboration, Y, WebrtcProvider, StarterKit, Table, TableCell, TableHeader, TableRow, Color, TextStyle, Image, Highlight, Link, Underline, Subscript, Superscript, TaskItem, TaskList, TextAlign, Paragraph, mergeAttributes } + { useEditor, Collaboration, Y, WebrtcProvider, StarterKit, Table, TableCell, TableHeader, TableRow, Color, TextStyle, Image, Highlight, Link, Underline, Subscript, Superscript, TaskItem, TaskList, TextAlign, Paragraph, mergeAttributes, CodeBlockLowlight, lowlight, VueNodeViewRenderer, NodeViewContent, nodeViewProps, NodeViewWrapper } ) => { const ydoc = new Y.Doc() const provider = new WebrtcProvider('tiny-examsple-document', ydoc) @@ -72,11 +73,18 @@ export const renderless = ( types: ['heading', 'paragraph'], }), CustomParagraph, + CodeBlockLowlight.extend({ + addNodeView() { + return VueNodeViewRenderer(Codehighlight(NodeViewContent, nodeViewProps, NodeViewWrapper)) + }, + }) + .configure({ lowlight }), ], content: 'Example Tesxt', autofocus: true, editable: true, injectCSS: false, + }) const box = ref(null) diff --git a/packages/theme/src/rich-text-editor/index.less b/packages/theme/src/rich-text-editor/index.less index a590b7fe3..0ecf313f2 100644 --- a/packages/theme/src/rich-text-editor/index.less +++ b/packages/theme/src/rich-text-editor/index.less @@ -214,10 +214,12 @@ } } -.bubble-menu { +.tiny-rich-text-editor { + .bubble-menu { - button { - background-color: #f1f3f5; + button { + background-color: #f1f3f5; + } } } @@ -239,163 +241,251 @@ } } -.ProseMirror { - outline: none !important; -} - -.ProseMirror { - >*+* { - margin-top: 0.75em; +.tiny-rich-text-editor { + .ProseMirror { + outline: none !important; } - ul, - ol { - padding: 0 1rem; - } + .ProseMirror { + >*+* { + margin-top: 0.75em; + } - ol { - list-style: auto; - } + ul, + ol { + padding: 0 1rem; + } - ul { - list-style: disc; - } + ol { + list-style: auto; + } - h1, - h2, - h3, - h4, - h5, - h6 { - line-height: 1.1; - } + ul { + list-style: disc; + } - code { - background-color: rgba(#616161, 0.1); - color: #616161; - } - - pre { - background: #0D0D0D; - color: #FFF; - font-family: 'JetBrainsMono', monospace; - padding: 0.75rem 1rem; - border-radius: 0.5rem; + h1, + h2, + h3, + h4, + h5, + h6 { + line-height: 1.1; + } code { - color: inherit; - padding: 0; - background: none; - font-size: 0.8rem; + background-color: rgba(#616161, 0.1); + color: #616161; + } + + pre { + background: #0D0D0D; + color: #FFF; + font-family: 'JetBrainsMono', monospace; + padding: 0.75rem 1rem; + border-radius: 0.5rem; + + code { + color: inherit; + padding: 0; + background: none; + font-size: 0.8rem; + } + } + + img { + max-width: 100%; + height: auto; + } + + blockquote { + padding-left: 1rem; + border-left: 2px solid rgba(#0D0D0D, 0.1); + } + + hr { + border: none; + border-top: 2px solid rgba(#0D0D0D, 0.1); + margin: 2rem 0; } } - img { - max-width: 100%; - height: auto; + .ProseMirror { + .img-button { + resize: both; + overflow: hidden; + } + + table { + border-collapse: collapse; + table-layout: fixed; + width: 100%; + margin: 0; + overflow: hidden; + + td, + th { + min-width: 1em; + border: 2px solid #ced4da; + padding: 3px 5px; + vertical-align: top; + box-sizing: border-box; + position: relative; + + >* { + margin-bottom: 0; + } + } + + th { + font-weight: bold; + text-align: left; + background-color: #f1f3f5; + } + + .selectedCell:after { + z-index: 2; + position: absolute; + content: ""; + left: 0; + right: 0; + top: 0; + bottom: 0; + background: rgba(200, 200, 255, 0.4); + pointer-events: none; + } + + .column-resize-handle { + position: absolute; + right: -2px; + top: 0; + bottom: -2px; + width: 4px; + background-color: #adf; + pointer-events: none; + } + + p { + margin: 0; + } + } + + .tableWrapper { + padding: 1rem 0; + overflow-x: auto; + } + + .resize-cursor { + cursor: ew-resize; + cursor: col-resize; + } } - blockquote { - padding-left: 1rem; - border-left: 2px solid rgba(#0D0D0D, 0.1); + .ProseMirror { + + ul[data-type="taskList"] { + list-style: none; + padding: 0; + + li { + display: flex; + align-items: center; + + >label { + flex: 0 0 auto; + margin-right: 0.5rem; + user-select: none; + } + + >div { + flex: 1 1 auto; + } + } + } } - hr { - border: none; - border-top: 2px solid rgba(#0D0D0D, 0.1); - margin: 2rem 0; - } -} + .ProseMirror { + >*+* { + margin-top: 0.75em; + } -.ProseMirror { - .img-button { - resize: both; - overflow: hidden; - } - - table { - border-collapse: collapse; - table-layout: fixed; - width: 100%; - margin: 0; - overflow: hidden; - - td, - th { - min-width: 1em; - border: 2px solid #ced4da; - padding: 3px 5px; - vertical-align: top; - box-sizing: border-box; + .code-block { position: relative; - >* { - margin-bottom: 0; + select { + position: absolute; + top: .5rem; + right: 0.5rem; + border: 0; + background: gray; + border-radius: 4px; } } - th { - font-weight: bold; - text-align: left; - background-color: #f1f3f5; - } + pre { + background: #0d0d0d; + color: #fff; + font-family: "JetBrainsMono", monospace; + padding: 0.75rem 1rem; + border-radius: 0.5rem; - .selectedCell:after { - z-index: 2; - position: absolute; - content: ""; - left: 0; - right: 0; - top: 0; - bottom: 0; - background: rgba(200, 200, 255, 0.4); - pointer-events: none; - } - - .column-resize-handle { - position: absolute; - right: -2px; - top: 0; - bottom: -2px; - width: 4px; - background-color: #adf; - pointer-events: none; - } - - p { - margin: 0; - } - } - - .tableWrapper { - padding: 1rem 0; - overflow-x: auto; - } - - .resize-cursor { - cursor: ew-resize; - cursor: col-resize; - } -} - -.ProseMirror { - - ul[data-type="taskList"] { - list-style: none; - padding: 0; - - li { - display: flex; - align-items: center; - - >label { - flex: 0 0 auto; - margin-right: 0.5rem; - user-select: none; + code { + color: inherit; + padding: 0; + background: none; + font-size: 0.8rem; } - >div { - flex: 1 1 auto; + .hljs-comment, + .hljs-quote { + color: #616161; + } + + .hljs-variable, + .hljs-template-variable, + .hljs-attribute, + .hljs-tag, + .hljs-name, + .hljs-regexp, + .hljs-link, + .hljs-name, + .hljs-selector-id, + .hljs-selector-class { + color: #f98181; + } + + .hljs-number, + .hljs-meta, + .hljs-built_in, + .hljs-builtin-name, + .hljs-literal, + .hljs-type, + .hljs-params { + color: #fbbc88; + } + + .hljs-string, + .hljs-symbol, + .hljs-bullet { + color: #b9f18d; + } + + .hljs-title, + .hljs-section { + color: #faf594; + } + + .hljs-keyword, + .hljs-selector-tag { + color: #70cff8; + } + + .hljs-emphasis { + font-style: italic; + } + + .hljs-strong { + font-weight: 700; } } } diff --git a/packages/vue/src/rich-text-editor/package.json b/packages/vue/src/rich-text-editor/package.json index 601d495bb..96ec0f818 100644 --- a/packages/vue/src/rich-text-editor/package.json +++ b/packages/vue/src/rich-text-editor/package.json @@ -9,6 +9,7 @@ "author": "", "license": "ISC", "dependencies": { + "@tiptap/core": "^2.1.6", "@tiptap/extension-collaboration": "^2.0.4", "@tiptap/extension-color": "^2.0.4", "@tiptap/extension-highlight": "^2.0.3", @@ -25,12 +26,16 @@ "@tiptap/extension-task-item": "^2.0.4", "@tiptap/extension-task-list": "^2.0.4", "@tiptap/extension-text-align": "^2.0.4", + "@tiptap/extension-paragraph": "^2.1.6", "@tiptap/pm": "^2.0.4", "@tiptap/starter-kit": "^2.0.4", "@tiptap/vue-3": "^2.0.4", "y-prosemirror": "^1.2.1", "y-webrtc": "^10.2.5", "yjs": "^13.6.7", + "@tiptap/extension-code-block-lowlight": "^2.0.4", + "highlight.js": "^11.8.0", + "lowlight": "^2.9.0", "@opentiny/vue-common": "workspace:~", "@opentiny/vue-renderless": "workspace:~", "@opentiny/vue-theme": "workspace:~" diff --git a/packages/vue/src/rich-text-editor/src/pc.vue b/packages/vue/src/rich-text-editor/src/pc.vue index 56c49f6f9..8081bfc32 100644 --- a/packages/vue/src/rich-text-editor/src/pc.vue +++ b/packages/vue/src/rich-text-editor/src/pc.vue @@ -314,7 +314,15 @@ import { iconRichTextUnderline, iconRichTextUndo } from '@opentiny/vue-icon' -import { useEditor, EditorContent, BubbleMenu, VueNodeViewRenderer } from '@tiptap/vue-3' +import { + useEditor, + EditorContent, + BubbleMenu, + VueNodeViewRenderer, + NodeViewContent, + nodeViewProps, + NodeViewWrapper +} from '@tiptap/vue-3' import StarterKit from '@tiptap/starter-kit' // 段落包 import Paragraph from '@tiptap/extension-paragraph' @@ -343,6 +351,17 @@ import TaskItem from '@tiptap/extension-task-item' import TaskList from '@tiptap/extension-task-list' // textalign import TextAlign from '@tiptap/extension-text-align' +// code high light +import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' +import css from 'highlight.js/lib/languages/css' +import js from 'highlight.js/lib/languages/javascript' +import ts from 'highlight.js/lib/languages/typescript' +import html from 'highlight.js/lib/languages/xml' +import { lowlight } from 'lowlight' +lowlight.registerLanguage('html', html) +lowlight.registerLanguage('css', css) +lowlight.registerLanguage('js', js) +lowlight.registerLanguage('ts', ts) // collaboration 包 import Collaboration from '@tiptap/extension-collaboration' import * as Y from 'yjs' @@ -432,7 +451,13 @@ export default defineComponent({ TaskList, TextAlign, Paragraph, - mergeAttributes + mergeAttributes, + CodeBlockLowlight, + lowlight, + VueNodeViewRenderer, + NodeViewContent, + nodeViewProps, + NodeViewWrapper } }) }