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 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)

View File

@ -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;
}
}
}

View File

@ -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:~"

View File

@ -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
}
})
}