feat(rich-text-editor): support code highlight (#440)

* 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 <changhao01@youdao>
This commit is contained in:
Caesar-ch 2023-08-24 15:29:26 +08:00 committed by GitHub
parent b2cd6ab80f
commit a592dd9f67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 308 additions and 139 deletions

View File

@ -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 (
<NodeViewWrapper class="code-block">
<select contenteditable="false" v-model={this.selectedLanguage}>
<option value="null">
auto
</option>
<option disabled>
</option>
{this.languages.map((item, index) => <option value={item} key={index}> {item} </option>)}
</select>
<pre><code><NodeViewContent /></code></pre>
</NodeViewWrapper>
)
},
}
}

View File

@ -1,10 +1,11 @@
import { handleChange, setLink, handleMove, handleClickOutside, removeClickOutside, handleClick, shouldShow } from './index' 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 api = ['state', 'setLink', 'handleChange', 'box', 'handleMove', 'handleClickOutside', 'removeClickOutside', 'handleClick', 'shouldShow']
export const renderless = ( export const renderless = (
props, props,
{ computed, onMounted, onBeforeUnmount, reactive, ref }, { computed, onMounted, onBeforeUnmount, reactive, ref },
{ vm, emit, parent }, { 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 ydoc = new Y.Doc()
const provider = new WebrtcProvider('tiny-examsple-document', ydoc) const provider = new WebrtcProvider('tiny-examsple-document', ydoc)
@ -72,11 +73,18 @@ export const renderless = (
types: ['heading', 'paragraph'], types: ['heading', 'paragraph'],
}), }),
CustomParagraph, CustomParagraph,
CodeBlockLowlight.extend({
addNodeView() {
return VueNodeViewRenderer(Codehighlight(NodeViewContent, nodeViewProps, NodeViewWrapper))
},
})
.configure({ lowlight }),
], ],
content: 'Example Tesxt', content: 'Example Tesxt',
autofocus: true, autofocus: true,
editable: true, editable: true,
injectCSS: false, injectCSS: false,
}) })
const box = ref(null) const box = ref(null)

View File

@ -214,10 +214,12 @@
} }
} }
.bubble-menu { .tiny-rich-text-editor {
.bubble-menu {
button { button {
background-color: #f1f3f5; background-color: #f1f3f5;
}
} }
} }
@ -239,163 +241,251 @@
} }
} }
.ProseMirror { .tiny-rich-text-editor {
outline: none !important; .ProseMirror {
} outline: none !important;
.ProseMirror {
>*+* {
margin-top: 0.75em;
} }
ul, .ProseMirror {
ol { >*+* {
padding: 0 1rem; margin-top: 0.75em;
} }
ol { ul,
list-style: auto; ol {
} padding: 0 1rem;
}
ul { ol {
list-style: disc; list-style: auto;
} }
h1, ul {
h2, list-style: disc;
h3, }
h4,
h5,
h6 {
line-height: 1.1;
}
code { h1,
background-color: rgba(#616161, 0.1); h2,
color: #616161; h3,
} h4,
h5,
pre { h6 {
background: #0D0D0D; line-height: 1.1;
color: #FFF; }
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code { code {
color: inherit; background-color: rgba(#616161, 0.1);
padding: 0; color: #616161;
background: none; }
font-size: 0.8rem;
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 { .ProseMirror {
max-width: 100%; .img-button {
height: auto; 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 { .ProseMirror {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1); 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 { .ProseMirror {
border: none; >*+* {
border-top: 2px solid rgba(#0D0D0D, 0.1); margin-top: 0.75em;
margin: 2rem 0; }
}
}
.ProseMirror { .code-block {
.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; position: relative;
>* { select {
margin-bottom: 0; position: absolute;
top: .5rem;
right: 0.5rem;
border: 0;
background: gray;
border-radius: 4px;
} }
} }
th { pre {
font-weight: bold; background: #0d0d0d;
text-align: left; color: #fff;
background-color: #f1f3f5; font-family: "JetBrainsMono", monospace;
} padding: 0.75rem 1rem;
border-radius: 0.5rem;
.selectedCell:after { code {
z-index: 2; color: inherit;
position: absolute; padding: 0;
content: ""; background: none;
left: 0; font-size: 0.8rem;
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;
} }
>div { .hljs-comment,
flex: 1 1 auto; .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;
} }
} }
} }

View File

@ -9,6 +9,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@tiptap/core": "^2.1.6",
"@tiptap/extension-collaboration": "^2.0.4", "@tiptap/extension-collaboration": "^2.0.4",
"@tiptap/extension-color": "^2.0.4", "@tiptap/extension-color": "^2.0.4",
"@tiptap/extension-highlight": "^2.0.3", "@tiptap/extension-highlight": "^2.0.3",
@ -25,12 +26,16 @@
"@tiptap/extension-task-item": "^2.0.4", "@tiptap/extension-task-item": "^2.0.4",
"@tiptap/extension-task-list": "^2.0.4", "@tiptap/extension-task-list": "^2.0.4",
"@tiptap/extension-text-align": "^2.0.4", "@tiptap/extension-text-align": "^2.0.4",
"@tiptap/extension-paragraph": "^2.1.6",
"@tiptap/pm": "^2.0.4", "@tiptap/pm": "^2.0.4",
"@tiptap/starter-kit": "^2.0.4", "@tiptap/starter-kit": "^2.0.4",
"@tiptap/vue-3": "^2.0.4", "@tiptap/vue-3": "^2.0.4",
"y-prosemirror": "^1.2.1", "y-prosemirror": "^1.2.1",
"y-webrtc": "^10.2.5", "y-webrtc": "^10.2.5",
"yjs": "^13.6.7", "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-common": "workspace:~",
"@opentiny/vue-renderless": "workspace:~", "@opentiny/vue-renderless": "workspace:~",
"@opentiny/vue-theme": "workspace:~" "@opentiny/vue-theme": "workspace:~"

View File

@ -314,7 +314,15 @@ import {
iconRichTextUnderline, iconRichTextUnderline,
iconRichTextUndo iconRichTextUndo
} from '@opentiny/vue-icon' } 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 StarterKit from '@tiptap/starter-kit'
// //
import Paragraph from '@tiptap/extension-paragraph' import Paragraph from '@tiptap/extension-paragraph'
@ -343,6 +351,17 @@ import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list' import TaskList from '@tiptap/extension-task-list'
// textalign // textalign
import TextAlign from '@tiptap/extension-text-align' 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 // collaboration
import Collaboration from '@tiptap/extension-collaboration' import Collaboration from '@tiptap/extension-collaboration'
import * as Y from 'yjs' import * as Y from 'yjs'
@ -432,7 +451,13 @@ export default defineComponent({
TaskList, TaskList,
TextAlign, TextAlign,
Paragraph, Paragraph,
mergeAttributes mergeAttributes,
CodeBlockLowlight,
lowlight,
VueNodeViewRenderer,
NodeViewContent,
nodeViewProps,
NodeViewWrapper
} }
}) })
} }