mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-06-07 17:43:49 +08:00
635 lines
15 KiB
Vue
635 lines
15 KiB
Vue
<template>
|
||
<div v-if="editor" class="container">
|
||
<div class="control-group">
|
||
<div class="button-group">
|
||
<button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
|
||
Bold
|
||
</button>
|
||
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
|
||
Italic
|
||
</button>
|
||
<button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }">
|
||
Strike
|
||
</button>
|
||
<button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }">
|
||
Code
|
||
</button>
|
||
<button @click="editor.chain().focus().unsetAllMarks().run()">
|
||
Clear marks
|
||
</button>
|
||
<button @click="editor.chain().focus().clearNodes().run()">
|
||
Clear nodes
|
||
</button>
|
||
<button @click="editor.chain().focus().setParagraph().run()" :class="{ 'is-active': editor.isActive('paragraph') }">
|
||
Paragraph
|
||
</button>
|
||
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }">
|
||
H1
|
||
</button>
|
||
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }">
|
||
H2
|
||
</button>
|
||
<button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }">
|
||
H3
|
||
</button>
|
||
<button @click="editor.chain().focus().toggleHeading({ level: 4 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 4 }) }">
|
||
H4
|
||
</button>
|
||
<button @click="editor.chain().focus().toggleHeading({ level: 5 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 5 }) }">
|
||
H5
|
||
</button>
|
||
<button @click="editor.chain().focus().toggleHeading({ level: 6 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 6 }) }">
|
||
h6
|
||
</button>
|
||
<button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }">
|
||
Bullet list
|
||
</button>
|
||
<button @click="editor.chain().focus().toggleOrderedList().run()" :class="{ 'is-active': editor.isActive('orderedList') }">
|
||
Ordered list
|
||
</button>
|
||
<button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }">
|
||
Code block
|
||
</button>
|
||
<button @click="editor.chain().focus().toggleBlockquote().run()" :class="{ 'is-active': editor.isActive('blockquote') }">
|
||
Blockquote
|
||
</button>
|
||
<button @click="editor.chain().focus().setHorizontalRule().run()">
|
||
Horizontal rule
|
||
</button>
|
||
<button @click="editor.chain().focus().setHardBreak().run()">
|
||
Hard break
|
||
</button>
|
||
<button @click="editor.chain().focus().undo().run()">
|
||
Undo
|
||
</button>
|
||
<button @click="editor.chain().focus().redo().run()">
|
||
Redo
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<editor-content :editor="editor" />
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import Blockquote from '@tiptap/extension-blockquote'
|
||
import Bold from '@tiptap/extension-bold'
|
||
import BulletList from '@tiptap/extension-bullet-list'
|
||
import Code from '@tiptap/extension-code'
|
||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
||
import Color from '@tiptap/extension-color'
|
||
import Document from '@tiptap/extension-document'
|
||
import Dropcursor from '@tiptap/extension-dropcursor'
|
||
import Gapcursor from '@tiptap/extension-gapcursor'
|
||
import HardBreak from '@tiptap/extension-hard-break'
|
||
import Heading from '@tiptap/extension-heading'
|
||
import Highlight from '@tiptap/extension-highlight'
|
||
import History from '@tiptap/extension-history'
|
||
import HorizontalRule from '@tiptap/extension-horizontal-rule'
|
||
import Image from '@tiptap/extension-image'
|
||
import Italic from '@tiptap/extension-italic'
|
||
import Link from '@tiptap/extension-link'
|
||
import ListItem from '@tiptap/extension-list-item'
|
||
import Mention from '@tiptap/extension-mention'
|
||
import OrderedList from '@tiptap/extension-ordered-list'
|
||
import Paragraph from '@tiptap/extension-paragraph'
|
||
import Placeholder from '@tiptap/extension-placeholder'
|
||
import Strike from '@tiptap/extension-strike'
|
||
import Subscript from '@tiptap/extension-subscript'
|
||
import Superscript from '@tiptap/extension-superscript'
|
||
import Table from '@tiptap/extension-table'
|
||
import TableCell from '@tiptap/extension-table-cell'
|
||
import TableHeader from '@tiptap/extension-table-header'
|
||
import TableRow from '@tiptap/extension-table-row'
|
||
import TaskItem from '@tiptap/extension-task-item'
|
||
import TaskList from '@tiptap/extension-task-list'
|
||
import Text from '@tiptap/extension-text'
|
||
import TextAlign from '@tiptap/extension-text-align'
|
||
import TextStyle from '@tiptap/extension-text-style'
|
||
import Underline from '@tiptap/extension-underline'
|
||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||
import { all, createLowlight } from 'lowlight'
|
||
|
||
const lowlight = createLowlight(all)
|
||
|
||
export default {
|
||
components: {
|
||
EditorContent,
|
||
},
|
||
|
||
data() {
|
||
return {
|
||
editor: null,
|
||
}
|
||
},
|
||
|
||
mounted() {
|
||
this.editor = new Editor({
|
||
extensions: [
|
||
Blockquote,
|
||
Bold,
|
||
BulletList,
|
||
Code,
|
||
CodeBlockLowlight.configure({
|
||
lowlight,
|
||
}),
|
||
Document,
|
||
Dropcursor,
|
||
Gapcursor,
|
||
HardBreak,
|
||
Heading,
|
||
History,
|
||
HorizontalRule,
|
||
Italic,
|
||
ListItem,
|
||
OrderedList,
|
||
Paragraph,
|
||
Strike,
|
||
Text,
|
||
Underline,
|
||
Superscript,
|
||
Subscript,
|
||
Link,
|
||
Mention.configure({
|
||
HTMLAttributes: {
|
||
class: 'mention',
|
||
},
|
||
}),
|
||
Table.configure({
|
||
resizable: true,
|
||
}),
|
||
TableRow,
|
||
TableHeader,
|
||
TableCell,
|
||
Image,
|
||
TaskList,
|
||
TaskItem,
|
||
Placeholder.configure({
|
||
placeholder: 'This is a placeholder …',
|
||
includeChildren: true,
|
||
}),
|
||
TextAlign.configure({
|
||
types: ['heading', 'paragraph'],
|
||
}),
|
||
TextStyle,
|
||
Color,
|
||
Highlight.configure({ multicolor: true }),
|
||
],
|
||
content: `
|
||
<h2>
|
||
Hi there,
|
||
</h2>
|
||
<p>
|
||
this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:
|
||
</p>
|
||
<ul>
|
||
<li>
|
||
That's a bullet list with one …
|
||
</li>
|
||
<li>
|
||
… or two list items.
|
||
</li>
|
||
</ul>
|
||
<p>
|
||
Isn't that great? And all of that is editable. But wait, there's more. Let's try a code block:
|
||
</p>
|
||
<pre><code class="language-javascript">for (var i=1; i <= 20; i++)
|
||
{
|
||
if (i % 15 == 0)
|
||
console.log("FizzBuzz");
|
||
else if (i % 3 == 0)
|
||
console.log("Fizz");
|
||
else if (i % 5 == 0)
|
||
console.log("Buzz");
|
||
else
|
||
console.log(i);
|
||
}</code></pre>
|
||
<p>
|
||
I know, I know, this is impressive. It's only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too.
|
||
</p>
|
||
<blockquote>
|
||
Wow, that's amazing. Good work, boy! 👏
|
||
<br />
|
||
— Mom
|
||
</blockquote>
|
||
<h2>Text align</h2>
|
||
<p style="text-align: center">first paragraph</p>
|
||
<p style="text-align: right">second paragraph</p>
|
||
<h2>Color</h2>
|
||
<p><span style="color: #958DF1">Oh, for some reason that's purple.</span></p>
|
||
<h2>Highlight</h2>
|
||
<p>This isn't highlighted.</s></p>
|
||
<p><mark>But that one is.</mark></p>
|
||
<p><mark style="background-color: red;">And this is highlighted too, but in a different color.</mark></p>
|
||
<p><mark data-color="#ffa8a8">And this one has a data attribute.</mark></p>
|
||
<h2>Task list</h2>
|
||
<ul data-type="taskList">
|
||
<li data-type="taskItem" data-checked="true">A list item</li>
|
||
<li data-type="taskItem" data-checked="false">And another one</li>
|
||
</ul>
|
||
<p>Some text with a mention <span data-type="mention" data-id="Mention 1"></span> and another one <span data-type="mention" data-id="Mention 2"></span>. Great.</p>
|
||
<table>
|
||
<tbody>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th colspan="3">Description</th>
|
||
</tr>
|
||
<tr>
|
||
<td>Cyndi Lauper</td>
|
||
<td>Singer</td>
|
||
<td>Songwriter</td>
|
||
<td>Actress</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Marie Curie</td>
|
||
<td>Scientist</td>
|
||
<td>Chemist</td>
|
||
<td>Physicist</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Indira Gandhi</td>
|
||
<td>Prime minister</td>
|
||
<td colspan="2">Politician</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<p>This is a basic example of implementing images. Drag to re-order.</p>
|
||
<img src="https://placehold.co/800x400" />
|
||
<img src="https://placehold.co/800x400/6A00F5/white" />
|
||
`,
|
||
})
|
||
},
|
||
|
||
beforeUnmount() {
|
||
this.editor.destroy()
|
||
},
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
/* Basic editor styles */
|
||
.tiptap {
|
||
:first-child {
|
||
margin-top: 0;
|
||
}
|
||
|
||
/* Placeholder (on every new line) */
|
||
.is-empty::before {
|
||
color: var(--gray-4);
|
||
content: attr(data-placeholder);
|
||
float: left;
|
||
height: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
a {
|
||
color: var(--purple);
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
color: var(--purple-contrast);
|
||
}
|
||
}
|
||
|
||
/* List styles */
|
||
ul,
|
||
ol {
|
||
padding: 0 1rem;
|
||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||
|
||
li p {
|
||
margin-top: 0.25em;
|
||
margin-bottom: 0.25em;
|
||
}
|
||
}
|
||
|
||
/* Task list specific styles */
|
||
ul[data-type="taskList"] {
|
||
list-style: none;
|
||
margin-left: 0;
|
||
padding: 0;
|
||
|
||
li {
|
||
align-items: flex-start;
|
||
display: flex;
|
||
|
||
> label {
|
||
flex: 0 0 auto;
|
||
margin-right: 0.5rem;
|
||
user-select: none;
|
||
}
|
||
|
||
> div {
|
||
flex: 1 1 auto;
|
||
}
|
||
}
|
||
|
||
input[type="checkbox"] {
|
||
cursor: pointer;
|
||
}
|
||
|
||
ul[data-type="taskList"] {
|
||
margin: 0;
|
||
}
|
||
}
|
||
|
||
/* Heading styles */
|
||
h1,
|
||
h2,
|
||
h3,
|
||
h4,
|
||
h5,
|
||
h6 {
|
||
line-height: 1.1;
|
||
margin-top: 2.5rem;
|
||
text-wrap: pretty;
|
||
}
|
||
|
||
h1,
|
||
h2 {
|
||
margin-top: 3.5rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 1.4rem;
|
||
}
|
||
|
||
h2 {
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
h3 {
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
h4,
|
||
h5,
|
||
h6 {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
/* Display empty p */
|
||
p:empty::before {
|
||
content: '\00a0';
|
||
}
|
||
|
||
/* Code and preformatted text styles */
|
||
code {
|
||
background-color: var(--purple-light);
|
||
border-radius: 0.4rem;
|
||
color: var(--black);
|
||
font-size: 0.85rem;
|
||
padding: 0.25em 0.3em;
|
||
}
|
||
|
||
pre {
|
||
background: var(--black);
|
||
border-radius: 0.5rem;
|
||
color: var(--white);
|
||
font-family: 'JetBrainsMono', monospace;
|
||
margin: 1.5rem 0;
|
||
padding: 0.75rem 1rem;
|
||
|
||
code {
|
||
background: none;
|
||
color: inherit;
|
||
font-size: 0.8rem;
|
||
padding: 0;
|
||
}
|
||
|
||
/* Code styling */
|
||
.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;
|
||
}
|
||
}
|
||
|
||
.code-block {
|
||
position: relative;
|
||
|
||
select {
|
||
position: absolute;
|
||
background-color: var(--white);
|
||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="Black" d="M7 10l5 5 5-5z"/></svg>');
|
||
right: 0.5rem;
|
||
top: 0.5rem;
|
||
}
|
||
}
|
||
|
||
mark {
|
||
background-color: #FAF594;
|
||
border-radius: 0.4rem;
|
||
box-decoration-break: clone;
|
||
padding: 0.1rem 0.3rem;
|
||
}
|
||
|
||
img {
|
||
display: block;
|
||
height: auto;
|
||
margin: 1.5rem 0;
|
||
max-width: 100%;
|
||
|
||
&.ProseMirror-selectednode {
|
||
outline: 3px solid var(--purple);
|
||
transition: outline 0.15s;
|
||
}
|
||
}
|
||
|
||
blockquote {
|
||
border-left: 3px solid var(--gray-3);
|
||
margin: 1.5rem 0;
|
||
padding-left: 1rem;
|
||
}
|
||
|
||
hr {
|
||
border: none;
|
||
border-top: 1px solid var(--gray-2);
|
||
margin: 2rem 0;
|
||
}
|
||
|
||
.mention {
|
||
background-color: var(--purple-light);
|
||
border-radius: 0.4rem;
|
||
box-decoration-break: clone;
|
||
color: var(--purple);
|
||
padding: 0.1rem 0.3rem;
|
||
}
|
||
|
||
/* Color swatches */
|
||
.color {
|
||
white-space: nowrap;
|
||
|
||
&::before {
|
||
background-color: var(--color);
|
||
border: 1px solid rgba(128, 128, 128, 0.3);
|
||
border-radius: 2px;
|
||
content: " ";
|
||
display: inline-block;
|
||
height: 1em;
|
||
margin-bottom: 0.15em;
|
||
margin-right: 0.1em;
|
||
vertical-align: middle;
|
||
width: 1em;
|
||
}
|
||
}
|
||
|
||
/* Table-specific styling */
|
||
table {
|
||
border-collapse: collapse;
|
||
margin: 0;
|
||
overflow: hidden;
|
||
table-layout: fixed;
|
||
width: 100%;
|
||
|
||
td,
|
||
th {
|
||
border: 1px solid var(--gray-3);
|
||
box-sizing: border-box;
|
||
min-width: 1em;
|
||
padding: 6px 8px;
|
||
position: relative;
|
||
vertical-align: top;
|
||
|
||
> * {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
th {
|
||
background-color: var(--gray-1);
|
||
font-weight: bold;
|
||
text-align: left;
|
||
}
|
||
|
||
.selectedCell:after {
|
||
background: var(--gray-2);
|
||
content: "";
|
||
left: 0; right: 0; top: 0; bottom: 0;
|
||
pointer-events: none;
|
||
position: absolute;
|
||
z-index: 2;
|
||
}
|
||
|
||
.column-resize-handle {
|
||
background-color: var(--purple);
|
||
bottom: -2px;
|
||
pointer-events: none;
|
||
position: absolute;
|
||
right: -2px;
|
||
top: 0;
|
||
width: 4px;
|
||
}
|
||
}
|
||
|
||
.tableWrapper {
|
||
margin: 1.5rem 0;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
&.resize-cursor {
|
||
cursor: ew-resize;
|
||
cursor: col-resize;
|
||
}
|
||
}
|
||
|
||
/* Floating/Bubble Menus */
|
||
.bubble-menu {
|
||
background-color: var(--white);
|
||
border: 1px solid var(--gray-1);
|
||
border-radius: 0.7rem;
|
||
box-shadow: var(--shadow);
|
||
display: flex;
|
||
padding: 0.2rem;
|
||
|
||
button {
|
||
background-color: unset;
|
||
|
||
&:hover {
|
||
background-color: var(--gray-3);
|
||
}
|
||
|
||
&.is-active {
|
||
background-color: var(--purple);
|
||
|
||
&:hover {
|
||
background-color: var(--purple-contrast);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.floating-menu {
|
||
display: flex;
|
||
background-color: var(--gray-3);
|
||
padding: 0.1rem;
|
||
border-radius: 0.5rem;
|
||
|
||
button {
|
||
background-color: unset;
|
||
padding: 0.275rem 0.425rem;
|
||
border-radius: 0.3rem;
|
||
|
||
&:hover {
|
||
background-color: var(--gray-3);
|
||
}
|
||
|
||
&.is-active {
|
||
background-color: var(--white);
|
||
color: var(--purple);
|
||
|
||
&:hover {
|
||
color: var(--purple-contrast);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|