Merge branch 'master' into feature/suggestions
# Conflicts: # examples/Components/App/style.scss # examples/Components/Subnavigation/index.vue # examples/main.js # packages/tiptap-extensions/package.json
This commit is contained in:
26
README.md
26
README.md
@@ -100,9 +100,11 @@ By default the editor will only support paragraphs. Other nodes and marks are av
|
||||
<script>
|
||||
import { Editor } from 'tiptap'
|
||||
import {
|
||||
// Nodes
|
||||
BlockquoteNode,
|
||||
BulletListNode,
|
||||
CodeBlockNode,
|
||||
CodeBlockHighlightNode,
|
||||
HardBreakNode,
|
||||
HeadingNode,
|
||||
ImageNode,
|
||||
@@ -110,11 +112,18 @@ import {
|
||||
OrderedListNode,
|
||||
TodoItemNode,
|
||||
TodoListNode,
|
||||
|
||||
// Marks
|
||||
BoldMark,
|
||||
CodeMark,
|
||||
ItalicMark,
|
||||
LinkMark,
|
||||
StrikeMark,
|
||||
UnderlineMark,
|
||||
|
||||
// General Extensions
|
||||
HistoryExtension,
|
||||
PlaceholderExtension,
|
||||
} from 'tiptap-extensions'
|
||||
|
||||
export default {
|
||||
@@ -138,7 +147,10 @@ export default {
|
||||
new CodeMark(),
|
||||
new ItalicMark(),
|
||||
new LinkMark(),
|
||||
new StrikeMark(),
|
||||
new UnderlineMark(),
|
||||
new HistoryExtension(),
|
||||
new PlaceholderExtension(),
|
||||
],
|
||||
}
|
||||
},
|
||||
@@ -322,12 +334,14 @@ This is a basic example of building a custom menu. A more advanced menu can be f
|
||||
<template>
|
||||
<editor :extensions="extensions">
|
||||
<div slot="menubar" slot-scope="{ nodes, marks }">
|
||||
<button :class="{ 'is-active': nodes.heading.active({ level: 1 }) }" @click="nodes.heading.command({ level: 1 })">
|
||||
H1
|
||||
</button>
|
||||
<button :class="{ 'is-active': marks.bold.active() }" @click="marks.bold.command()">
|
||||
Bold
|
||||
</button>
|
||||
<div v-if="nodes && marks">
|
||||
<button :class="{ 'is-active': nodes.heading.active({ level: 1 }) }" @click="nodes.heading.command({ level: 1 })">
|
||||
H1
|
||||
</button>
|
||||
<button :class="{ 'is-active': marks.bold.active() }" @click="marks.bold.command()">
|
||||
Bold
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="content" slot-scope="props">
|
||||
<p>This text can be made bold.</p>
|
||||
|
||||
@@ -21,6 +21,22 @@
|
||||
<icon name="italic" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': marks.strike.active() }"
|
||||
@click="marks.strike.command"
|
||||
>
|
||||
<icon name="strike" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': marks.underline.active() }"
|
||||
@click="marks.underline.command"
|
||||
>
|
||||
<icon name="underline" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="menubar__button"
|
||||
@click="marks.code.command"
|
||||
@@ -132,6 +148,8 @@ import {
|
||||
CodeMark,
|
||||
ItalicMark,
|
||||
LinkMark,
|
||||
StrikeMark,
|
||||
UnderlineMark,
|
||||
HistoryExtension,
|
||||
} from 'tiptap-extensions'
|
||||
|
||||
@@ -156,6 +174,8 @@ export default {
|
||||
new CodeMark(),
|
||||
new ItalicMark(),
|
||||
new LinkMark(),
|
||||
new StrikeMark(),
|
||||
new UnderlineMark(),
|
||||
new HistoryExtension(),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -18,49 +18,34 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icon from 'Components/Icon'
|
||||
import { Editor } from 'tiptap'
|
||||
import {
|
||||
BlockquoteNode,
|
||||
BulletListNode,
|
||||
CodeBlockHighlightNode,
|
||||
HardBreakNode,
|
||||
HeadingNode,
|
||||
ListItemNode,
|
||||
OrderedListNode,
|
||||
TodoItemNode,
|
||||
TodoListNode,
|
||||
BoldMark,
|
||||
CodeMark,
|
||||
ItalicMark,
|
||||
LinkMark,
|
||||
HistoryExtension,
|
||||
} from 'tiptap-extensions'
|
||||
|
||||
import { javascript, css } from './examples'
|
||||
import {
|
||||
javascript,
|
||||
css,
|
||||
} from './examples'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Editor,
|
||||
Icon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
extensions: [
|
||||
new BlockquoteNode(),
|
||||
new BulletListNode(),
|
||||
new CodeBlockHighlightNode(),
|
||||
new HardBreakNode(),
|
||||
new HeadingNode({ maxLevel: 3 }),
|
||||
new ListItemNode(),
|
||||
new OrderedListNode(),
|
||||
new TodoItemNode(),
|
||||
new TodoListNode(),
|
||||
new BoldMark(),
|
||||
new CodeMark(),
|
||||
new ItalicMark(),
|
||||
new LinkMark(),
|
||||
new HistoryExtension(),
|
||||
],
|
||||
javascript,
|
||||
css,
|
||||
|
||||
@@ -17,22 +17,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icon from 'Components/Icon'
|
||||
import { Editor } from 'tiptap'
|
||||
import {
|
||||
BlockquoteNode,
|
||||
BulletListNode,
|
||||
CodeBlockNode,
|
||||
HardBreakNode,
|
||||
HeadingNode,
|
||||
ListItemNode,
|
||||
OrderedListNode,
|
||||
TodoItemNode,
|
||||
TodoListNode,
|
||||
BoldMark,
|
||||
CodeMark,
|
||||
ItalicMark,
|
||||
LinkMark,
|
||||
HistoryExtension,
|
||||
} from 'tiptap-extensions'
|
||||
import IframeNode from './Iframe.js'
|
||||
@@ -40,24 +30,14 @@ import IframeNode from './Iframe.js'
|
||||
export default {
|
||||
components: {
|
||||
Editor,
|
||||
Icon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
extensions: [
|
||||
new BlockquoteNode(),
|
||||
new BulletListNode(),
|
||||
new CodeBlockNode(),
|
||||
new HardBreakNode(),
|
||||
new HeadingNode({ maxLevel: 3 }),
|
||||
new ListItemNode(),
|
||||
new OrderedListNode(),
|
||||
new TodoItemNode(),
|
||||
new TodoListNode(),
|
||||
new BoldMark(),
|
||||
new CodeMark(),
|
||||
new ItalicMark(),
|
||||
new LinkMark(),
|
||||
new HistoryExtension(),
|
||||
// custom extension
|
||||
new IframeNode(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<editor class="editor" :extensions="extensions" @update="onUpdate">
|
||||
<editor class="editor" :extensions="extensions" @update="onUpdate" ref="editor">
|
||||
|
||||
<div class="menubar" slot="menubar" slot-scope="{ nodes, marks }">
|
||||
<div v-if="nodes && marks">
|
||||
@@ -99,6 +99,15 @@
|
||||
|
||||
</editor>
|
||||
|
||||
<div class="actions">
|
||||
<button class="button" @click="clearContent">
|
||||
Clear Content
|
||||
</button>
|
||||
<button class="button" @click="setContent">
|
||||
Set Content
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="export">
|
||||
<h3>JSON</h3>
|
||||
<pre><code v-html="json"></code></pre>
|
||||
@@ -161,6 +170,25 @@ export default {
|
||||
this.json = getJSON()
|
||||
this.html = getHTML()
|
||||
},
|
||||
clearContent() {
|
||||
this.$refs.editor.clearContent()
|
||||
this.$refs.editor.focus()
|
||||
},
|
||||
setContent() {
|
||||
this.$refs.editor.setContent({
|
||||
type: 'doc',
|
||||
content: [{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'This is some inserted text. 👋',
|
||||
},
|
||||
],
|
||||
}],
|
||||
})
|
||||
this.$refs.editor.focus()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -168,10 +196,15 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
@import "~variables";
|
||||
|
||||
.actions {
|
||||
max-width: 30rem;
|
||||
margin: 0 auto 2rem auto;
|
||||
}
|
||||
|
||||
.export {
|
||||
|
||||
max-width: 30rem;
|
||||
margin: 0 auto 5rem auto;
|
||||
margin: 0 auto 2rem auto;
|
||||
|
||||
pre {
|
||||
padding: 1rem;
|
||||
|
||||
@@ -17,49 +17,29 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icon from 'Components/Icon'
|
||||
import { Editor } from 'tiptap'
|
||||
import {
|
||||
BlockquoteNode,
|
||||
BulletListNode,
|
||||
CodeBlockNode,
|
||||
HardBreakNode,
|
||||
HeadingNode,
|
||||
ImageNode,
|
||||
ListItemNode,
|
||||
OrderedListNode,
|
||||
TodoItemNode,
|
||||
TodoListNode,
|
||||
BoldMark,
|
||||
CodeMark,
|
||||
ItalicMark,
|
||||
LinkMark,
|
||||
HistoryExtension,
|
||||
} from 'tiptap-extensions'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Editor,
|
||||
Icon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
extensions: [
|
||||
new BlockquoteNode(),
|
||||
new BulletListNode(),
|
||||
new CodeBlockNode(),
|
||||
new HardBreakNode(),
|
||||
new HeadingNode({ maxLevel: 3 }),
|
||||
new ImageNode(),
|
||||
new ListItemNode(),
|
||||
new OrderedListNode(),
|
||||
new TodoItemNode(),
|
||||
new TodoListNode(),
|
||||
new BoldMark(),
|
||||
new CodeMark(),
|
||||
new ItalicMark(),
|
||||
new LinkMark(),
|
||||
new HistoryExtension(),
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
44
examples/Components/Routes/Placeholder/index.vue
Normal file
44
examples/Components/Routes/Placeholder/index.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<editor class="editor" :extensions="extensions">
|
||||
<div class="editor__content" slot="content" slot-scope="props"></div>
|
||||
</editor>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Editor } from 'tiptap'
|
||||
import {
|
||||
BulletListNode,
|
||||
ListItemNode,
|
||||
PlaceholderExtension,
|
||||
} from 'tiptap-extensions'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Editor,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
extensions: [
|
||||
new BulletListNode(),
|
||||
new ListItemNode(),
|
||||
new PlaceholderExtension({
|
||||
emptyNodeClass: 'is-empty',
|
||||
}),
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.editor p.is-empty:first-child::before {
|
||||
content: 'Start typing…';
|
||||
float: left;
|
||||
color: #aaa;
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
@@ -16,47 +16,29 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icon from 'Components/Icon'
|
||||
import { Editor } from 'tiptap'
|
||||
import {
|
||||
BlockquoteNode,
|
||||
BulletListNode,
|
||||
CodeBlockNode,
|
||||
HardBreakNode,
|
||||
HeadingNode,
|
||||
ListItemNode,
|
||||
OrderedListNode,
|
||||
TodoItemNode,
|
||||
TodoListNode,
|
||||
BoldMark,
|
||||
CodeMark,
|
||||
ItalicMark,
|
||||
LinkMark,
|
||||
HistoryExtension,
|
||||
} from 'tiptap-extensions'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Editor,
|
||||
Icon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
extensions: [
|
||||
new BlockquoteNode(),
|
||||
new BulletListNode(),
|
||||
new CodeBlockNode(),
|
||||
new HardBreakNode(),
|
||||
new HeadingNode({ maxLevel: 3 }),
|
||||
new ListItemNode(),
|
||||
new OrderedListNode(),
|
||||
new TodoItemNode(),
|
||||
new TodoListNode(),
|
||||
new BoldMark(),
|
||||
new CodeMark(),
|
||||
new ItalicMark(),
|
||||
new LinkMark(),
|
||||
new HistoryExtension(),
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
@@ -71,20 +71,14 @@
|
||||
import Icon from 'Components/Icon'
|
||||
import { Editor } from 'tiptap'
|
||||
import {
|
||||
BlockquoteNode,
|
||||
BulletListNode,
|
||||
CodeBlockNode,
|
||||
HardBreakNode,
|
||||
HeadingNode,
|
||||
ListItemNode,
|
||||
OrderedListNode,
|
||||
TodoItemNode,
|
||||
TodoListNode,
|
||||
BoldMark,
|
||||
CodeMark,
|
||||
ItalicMark,
|
||||
LinkMark,
|
||||
HistoryExtension,
|
||||
} from 'tiptap-extensions'
|
||||
|
||||
export default {
|
||||
@@ -96,22 +90,60 @@ export default {
|
||||
return {
|
||||
customProp: 2,
|
||||
extensions: [
|
||||
new BlockquoteNode(),
|
||||
new BulletListNode(),
|
||||
new CodeBlockNode(),
|
||||
new HardBreakNode(),
|
||||
new HeadingNode({ maxLevel: 3 }),
|
||||
new ListItemNode(),
|
||||
new OrderedListNode(),
|
||||
new TodoItemNode(),
|
||||
new TodoListNode(),
|
||||
new BoldMark(),
|
||||
new CodeMark(),
|
||||
new ItalicMark(),
|
||||
new LinkMark(),
|
||||
new HistoryExtension(),
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~variables";
|
||||
|
||||
ul[data-type="todo_list"] {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
li[data-type="todo_item"] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.todo-checkbox {
|
||||
border: 2px solid $color-black;
|
||||
height: 0.9em;
|
||||
width: 0.9em;
|
||||
box-sizing: border-box;
|
||||
margin-right: 10px;
|
||||
margin-top: 0.3rem;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: pointer;
|
||||
border-radius: 0.2em;
|
||||
background-color: transparent;
|
||||
transition: 0.4s background;
|
||||
}
|
||||
|
||||
.todo-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
li[data-done="true"] {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
li[data-done="true"] .todo-checkbox {
|
||||
background-color: $color-black;
|
||||
}
|
||||
|
||||
li[data-done="false"] {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -33,6 +33,9 @@
|
||||
<router-link class="subnavigation__link" to="/mentions">
|
||||
Mentions
|
||||
</router-link>
|
||||
<router-link class="subnavigation__link" to="/placeholder">
|
||||
Placeholder
|
||||
</router-link>
|
||||
<router-link class="subnavigation__link" to="/export">
|
||||
Export HTML or JSON
|
||||
</router-link>
|
||||
|
||||
1
examples/assets/images/icons/strike.svg
Normal file
1
examples/assets/images/icons/strike.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>text-strike-through</title><path d="M23.75,12.952A1.25,1.25,0,0,0,22.5,11.7H13.564a.492.492,0,0,1-.282-.09c-.722-.513-1.482-.981-2.218-1.432-2.8-1.715-4.5-2.9-4.5-4.863,0-2.235,2.207-2.569,3.523-2.569a4.54,4.54,0,0,1,3.081.764A2.662,2.662,0,0,1,13.615,5.5l0,.3a1.25,1.25,0,1,0,2.5,0l0-.268A4.887,4.887,0,0,0,14.95,1.755C13.949.741,12.359.248,10.091.248c-3.658,0-6.023,1.989-6.023,5.069,0,2.773,1.892,4.512,4,5.927a.25.25,0,0,1-.139.458H1.5a1.25,1.25,0,0,0,0,2.5H12.477a.251.251,0,0,1,.159.058,4.339,4.339,0,0,1,1.932,3.466c0,3.268-3.426,3.522-4.477,3.522-1.814,0-3.139-.405-3.834-1.173a3.394,3.394,0,0,1-.65-2.7,1.25,1.25,0,0,0-2.488-.246A5.76,5.76,0,0,0,4.4,21.753c1.2,1.324,3.114,2,5.688,2,4.174,0,6.977-2.42,6.977-6.022a6.059,6.059,0,0,0-.849-3.147.25.25,0,0,1,.216-.377H22.5A1.25,1.25,0,0,0,23.75,12.952Z"/></svg>
|
||||
|
After Width: | Height: | Size: 884 B |
1
examples/assets/images/icons/underline.svg
Normal file
1
examples/assets/images/icons/underline.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>text-underline</title><path d="M22.5,21.248H1.5a1.25,1.25,0,0,0,0,2.5h21a1.25,1.25,0,0,0,0-2.5Z"/><path d="M1.978,2.748H3.341a.25.25,0,0,1,.25.25v8.523a8.409,8.409,0,0,0,16.818,0V3a.25.25,0,0,1,.25-.25h1.363a1.25,1.25,0,0,0,0-2.5H16.3a1.25,1.25,0,0,0,0,2.5h1.363a.25.25,0,0,1,.25.25v8.523a5.909,5.909,0,0,1-11.818,0V3a.25.25,0,0,1,.25-.25H7.7a1.25,1.25,0,1,0,0-2.5H1.978a1.25,1.25,0,0,0,0,2.5Z"/></svg>
|
||||
|
After Width: | Height: | Size: 469 B |
56
examples/assets/sass/editor.scss
Normal file
56
examples/assets/sass/editor.scss
Normal file
@@ -0,0 +1,56 @@
|
||||
.editor {
|
||||
position: relative;
|
||||
max-width: 30rem;
|
||||
margin: 0 auto 5rem auto;
|
||||
|
||||
&__content {
|
||||
pre {
|
||||
padding: 0.7rem 1rem;
|
||||
border-radius: 5px;
|
||||
background: $color-black;
|
||||
color: $color-white;
|
||||
font-size: 0.8rem;
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
p code {
|
||||
display: inline-block;
|
||||
padding: 0 0.4rem;
|
||||
border-radius: 5px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
background: rgba($color-black, 0.1);
|
||||
color: rgba($color-black, 0.8);
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid rgba($color-black, 0.1);
|
||||
color: rgba($color-black, 0.8);
|
||||
padding-left: 0.8rem;
|
||||
font-style: italic;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -59,4 +59,21 @@ h1,
|
||||
h2,
|
||||
h3 {
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
font-weight: bold;
|
||||
display: inline-flex;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: $color-black;
|
||||
padding: 0.2rem 0.5rem;
|
||||
margin-right: 0.2rem;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
background-color: rgba($color-black, 0.1);
|
||||
}
|
||||
|
||||
@import "./editor";
|
||||
@import "./menubar";
|
||||
@import "./menububble";
|
||||
37
examples/assets/sass/menubar.scss
Normal file
37
examples/assets/sass/menubar.scss
Normal file
@@ -0,0 +1,37 @@
|
||||
.menubar {
|
||||
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
transition: visibility 0.2s 0.4s, opacity 0.2s 0.4s;
|
||||
|
||||
&.is-hidden {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.is-focused {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: visibility 0.2s, opacity 0.2s;
|
||||
}
|
||||
|
||||
&__button {
|
||||
font-weight: bold;
|
||||
display: inline-flex;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: $color-black;
|
||||
padding: 0.2rem 0.5rem;
|
||||
margin-right: 0.2rem;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color-black, 0.05);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: rgba($color-black, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
examples/assets/sass/menububble.scss
Normal file
48
examples/assets/sass/menububble.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
.menububble {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
z-index: 20;
|
||||
background: $color-black;
|
||||
border-radius: 5px;
|
||||
padding: 0.3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
transform: translateX(-50%);
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, visibility 0.2s;
|
||||
|
||||
&__button {
|
||||
display: inline-flex;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: $color-white;
|
||||
padding: 0.2rem 0.5rem;
|
||||
margin-right: 0.2rem;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color-white, 0.1);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: rgba($color-white, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&__form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__input {
|
||||
font: inherit;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,6 @@ import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import svgSpriteLoader from 'helpers/svg-sprite-loader'
|
||||
import App from 'Components/App'
|
||||
import RouteBasic from 'Components/Routes/Basic'
|
||||
import RouteMenuBubble from 'Components/Routes/MenuBubble'
|
||||
import RouteLinks from 'Components/Routes/Links'
|
||||
import RouteImages from 'Components/Routes/Images'
|
||||
import RouteHidingMenuBar from 'Components/Routes/HidingMenuBar'
|
||||
import RouteTodoList from 'Components/Routes/TodoList'
|
||||
import RouteMarkdownShortcuts from 'Components/Routes/MarkdownShortcuts'
|
||||
import RouteCodeHighlighting from 'Components/Routes/CodeHighlighting'
|
||||
import RouteReadOnly from 'Components/Routes/ReadOnly'
|
||||
import RouteEmbeds from 'Components/Routes/Embeds'
|
||||
import RouteMentions from 'Components/Routes/Mentions'
|
||||
import RouteExport from 'Components/Routes/Export'
|
||||
|
||||
const __svg__ = { path: './assets/images/icons/*.svg', name: 'assets/images/[hash].sprite.svg' }
|
||||
svgSpriteLoader(__svg__.filename)
|
||||
@@ -26,84 +14,91 @@ Vue.use(VueRouter)
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: RouteBasic,
|
||||
component: () => import('Components/Routes/Basic'),
|
||||
meta: {
|
||||
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Basic',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/menu-bubble',
|
||||
component: RouteMenuBubble,
|
||||
component: () => import('Components/Routes/MenuBubble'),
|
||||
meta: {
|
||||
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/MenuBubble',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/links',
|
||||
component: RouteLinks,
|
||||
component: () => import('Components/Routes/Links'),
|
||||
meta: {
|
||||
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Links',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/images',
|
||||
component: RouteImages,
|
||||
component: () => import('Components/Routes/Images'),
|
||||
meta: {
|
||||
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Images',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/hiding-menu-bar',
|
||||
component: RouteHidingMenuBar,
|
||||
component: () => import('Components/Routes/HidingMenuBar'),
|
||||
meta: {
|
||||
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/HidingMenuBar',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/todo-list',
|
||||
component: RouteTodoList,
|
||||
component: () => import('Components/Routes/TodoList'),
|
||||
meta: {
|
||||
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/TodoList',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/markdown-shortcuts',
|
||||
component: RouteMarkdownShortcuts,
|
||||
component: () => import('Components/Routes/MarkdownShortcuts'),
|
||||
meta: {
|
||||
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/MarkdownShortcuts',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/code-highlighting',
|
||||
component: RouteCodeHighlighting,
|
||||
component: () => import('Components/Routes/CodeHighlighting'),
|
||||
meta: {
|
||||
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/CodeHighlighting',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/read-only',
|
||||
component: RouteReadOnly,
|
||||
component: () => import('Components/Routes/ReadOnly'),
|
||||
meta: {
|
||||
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/ReadOnly',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/embeds',
|
||||
component: RouteEmbeds,
|
||||
component: () => import('Components/Routes/Embeds'),
|
||||
meta: {
|
||||
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Embeds',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/mentions',
|
||||
component: RouteMentions,
|
||||
component: () => import('Components/Routes/Mentions'),
|
||||
meta: {
|
||||
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Mentions',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/placeholder',
|
||||
component: () => import('Components/Routes/Placeholder'),
|
||||
meta: {
|
||||
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Placeholder',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/export',
|
||||
component: RouteExport,
|
||||
component: () => import('Components/Routes/Export'),
|
||||
meta: {
|
||||
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Export',
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tiptap-extensions",
|
||||
"version": "0.8.0",
|
||||
"version": "0.14.1",
|
||||
"description": "Extensions for tiptap",
|
||||
"homepage": "https://tiptap.scrumpy.io",
|
||||
"license": "MIT",
|
||||
@@ -8,6 +8,7 @@
|
||||
"module": "dist/extensions.esm.js",
|
||||
"unpkg": "dist/extensions.js",
|
||||
"jsdelivr": "dist/extensions.js",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"src",
|
||||
"dist"
|
||||
@@ -24,7 +25,7 @@
|
||||
"prosemirror-history": "^1.0.2",
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-view": "^1.5.1",
|
||||
"tiptap": "^0.10.0",
|
||||
"tiptap": "^0.12.1",
|
||||
"tiptap-commands": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
42
packages/tiptap-extensions/src/extensions/Placeholder.js
Normal file
42
packages/tiptap-extensions/src/extensions/Placeholder.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Extension, Plugin } from 'tiptap'
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||
|
||||
export default class PlaceholderExtension extends Extension {
|
||||
|
||||
get name() {
|
||||
return 'placeholder'
|
||||
}
|
||||
|
||||
get defaultOptions() {
|
||||
return {
|
||||
emptyNodeClass: 'is-empty',
|
||||
}
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
decorations: ({ doc }) => {
|
||||
const decorations = []
|
||||
const completelyEmpty = doc.textContent === '' && doc.childCount <= 1 && doc.content.size <= 2
|
||||
|
||||
doc.descendants((node, pos) => {
|
||||
if (!completelyEmpty) {
|
||||
return
|
||||
}
|
||||
|
||||
const decoration = Decoration.node(pos, pos + node.nodeSize, {
|
||||
class: this.options.emptyNodeClass,
|
||||
})
|
||||
decorations.push(decoration)
|
||||
})
|
||||
|
||||
return DecorationSet.create(doc, decorations)
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,5 +15,8 @@ export { default as BoldMark } from './marks/Bold'
|
||||
export { default as CodeMark } from './marks/Code'
|
||||
export { default as ItalicMark } from './marks/Italic'
|
||||
export { default as LinkMark } from './marks/Link'
|
||||
export { default as StrikeMark } from './marks/Strike'
|
||||
export { default as UnderlineMark } from './marks/Underline'
|
||||
|
||||
export { default as HistoryExtension } from './extensions/History'
|
||||
export { default as PlaceholderExtension } from './extensions/Placeholder'
|
||||
|
||||
@@ -7,20 +7,6 @@ export default class LinkMark extends Mark {
|
||||
return 'link'
|
||||
}
|
||||
|
||||
get view() {
|
||||
return {
|
||||
props: ['node'],
|
||||
methods: {
|
||||
onClick() {
|
||||
console.log('click on link')
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<a :href="node.attrs.href" rel="noopener noreferrer nofollow" ref="content" @click="onClick"></a>
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
|
||||
47
packages/tiptap-extensions/src/marks/Strike.js
Normal file
47
packages/tiptap-extensions/src/marks/Strike.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Mark } from 'tiptap'
|
||||
import { toggleMark, markInputRule } from 'tiptap-commands'
|
||||
|
||||
export default class StrikeMark extends Mark {
|
||||
|
||||
get name() {
|
||||
return 'strike'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 's',
|
||||
},
|
||||
{
|
||||
tag: 'del',
|
||||
},
|
||||
{
|
||||
tag: 'strike',
|
||||
},
|
||||
{
|
||||
style: 'text-decoration',
|
||||
getAttrs: value => value === 'line-through',
|
||||
},
|
||||
],
|
||||
toDOM: () => ['s', 0],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-d': toggleMark(type),
|
||||
}
|
||||
}
|
||||
|
||||
command({ type }) {
|
||||
return toggleMark(type)
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
markInputRule(/~([^~]+)~$/, type),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
35
packages/tiptap-extensions/src/marks/Underline.js
Normal file
35
packages/tiptap-extensions/src/marks/Underline.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Mark } from 'tiptap'
|
||||
import { toggleMark } from 'tiptap-commands'
|
||||
|
||||
export default class UnderlineMark extends Mark {
|
||||
|
||||
get name() {
|
||||
return 'underline'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'u',
|
||||
},
|
||||
{
|
||||
style: 'text-decoration',
|
||||
getAttrs: value => value === 'underline',
|
||||
},
|
||||
],
|
||||
toDOM: () => ['u', 0],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-u': toggleMark(type),
|
||||
}
|
||||
}
|
||||
|
||||
command({ type }) {
|
||||
return toggleMark(type)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Node } from 'tiptap'
|
||||
import { wrappingInputRule, wrapInList, toggleList } from 'tiptap-commands'
|
||||
import { wrappingInputRule, toggleList } from 'tiptap-commands'
|
||||
|
||||
export default class BulletNode extends Node {
|
||||
|
||||
@@ -22,9 +22,9 @@ export default class BulletNode extends Node {
|
||||
return toggleList(type, schema.nodes.list_item)
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
keys({ type, schema }) {
|
||||
return {
|
||||
'Shift-Ctrl-8': wrapInList(type),
|
||||
'Shift-Ctrl-8': toggleList(type, schema.nodes.list_item),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Node } from 'tiptap'
|
||||
import { splitListItem, liftListItem, sinkListItem } from 'tiptap-commands'
|
||||
|
||||
export default class OrderedListNode extends Node {
|
||||
export default class ListItemNode extends Node {
|
||||
|
||||
get name() {
|
||||
return 'list_item'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Node } from 'tiptap'
|
||||
import { wrappingInputRule, wrapInList, toggleList } from 'tiptap-commands'
|
||||
import { wrappingInputRule, toggleList } from 'tiptap-commands'
|
||||
|
||||
export default class OrderedListNode extends Node {
|
||||
|
||||
@@ -32,9 +32,9 @@ export default class OrderedListNode extends Node {
|
||||
return toggleList(type, schema.nodes.list_item)
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
keys({ type, schema }) {
|
||||
return {
|
||||
'Shift-Ctrl-9': wrapInList(type),
|
||||
'Shift-Ctrl-9': toggleList(type, schema.nodes.list_item),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tiptap",
|
||||
"version": "0.10.0",
|
||||
"version": "0.12.1",
|
||||
"description": "A rich-text editor for Vue.js",
|
||||
"homepage": "https://tiptap.scrumpy.io",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EditorState, Plugin } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { Schema, DOMParser } from 'prosemirror-model'
|
||||
import { Schema, DOMParser, DOMSerializer } from 'prosemirror-model'
|
||||
import { gapCursor } from 'prosemirror-gapcursor'
|
||||
import { keymap } from 'prosemirror-keymap'
|
||||
import { baseKeymap } from 'prosemirror-commands'
|
||||
@@ -56,6 +56,17 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
doc: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.setContent(this.doc, true)
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
render(createElement) {
|
||||
const slots = []
|
||||
|
||||
@@ -70,7 +81,7 @@ export default {
|
||||
nodes: this.menuActions ? this.menuActions.nodes : null,
|
||||
marks: this.menuActions ? this.menuActions.marks : null,
|
||||
focused: this.view ? this.view.focused : false,
|
||||
focus: () => this.view.focus(),
|
||||
focus: this.focus,
|
||||
})
|
||||
slots.push(this.menubarNode)
|
||||
} else if (name === 'menububble') {
|
||||
@@ -78,7 +89,7 @@ export default {
|
||||
nodes: this.menuActions ? this.menuActions.nodes : null,
|
||||
marks: this.menuActions ? this.menuActions.marks : null,
|
||||
focused: this.view ? this.view.focused : false,
|
||||
focus: () => this.view.focus(),
|
||||
focus: this.focus,
|
||||
})
|
||||
slots.push(this.menububbleNode)
|
||||
}
|
||||
@@ -195,6 +206,12 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
destroyEditor() {
|
||||
if (this.view) {
|
||||
this.view.destroy()
|
||||
}
|
||||
},
|
||||
|
||||
updateMenuActions() {
|
||||
this.menuActions = buildMenuActions({
|
||||
schema: this.schema,
|
||||
@@ -212,6 +229,25 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
this.emitUpdate()
|
||||
},
|
||||
|
||||
getHTML() {
|
||||
const div = document.createElement('div')
|
||||
const fragment = DOMSerializer
|
||||
.fromSchema(this.schema)
|
||||
.serializeFragment(this.state.doc.content)
|
||||
|
||||
div.appendChild(fragment)
|
||||
|
||||
return div.innerHTML
|
||||
},
|
||||
|
||||
getJSON() {
|
||||
return this.state.doc.toJSON()
|
||||
},
|
||||
|
||||
emitUpdate() {
|
||||
this.$emit('update', {
|
||||
getHTML: this.getHTML,
|
||||
getJSON: this.getJSON,
|
||||
@@ -219,12 +255,31 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
getHTML() {
|
||||
return this.view.dom.innerHTML
|
||||
setContent(content = {}, emitUpdate = false) {
|
||||
this.state = EditorState.create({
|
||||
schema: this.state.schema,
|
||||
doc: this.schema.nodeFromJSON(content),
|
||||
plugins: this.state.plugins,
|
||||
})
|
||||
|
||||
this.view.updateState(this.state)
|
||||
|
||||
if (emitUpdate) {
|
||||
this.emitUpdate()
|
||||
}
|
||||
},
|
||||
|
||||
getJSON() {
|
||||
return this.state.doc.toJSON()
|
||||
clearContent(emitUpdate = false) {
|
||||
this.setContent({
|
||||
type: 'doc',
|
||||
content: [{
|
||||
type: 'paragraph',
|
||||
}],
|
||||
}, emitUpdate)
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.view.focus()
|
||||
},
|
||||
|
||||
},
|
||||
@@ -233,4 +288,8 @@ export default {
|
||||
this.initEditor()
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.destroyEditor()
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@ export default class ComponentView {
|
||||
this.editable = editable
|
||||
|
||||
this.dom = this.createDOM()
|
||||
this.contentDOM = this._vm.$refs.content
|
||||
this.contentDOM = this.vm.$refs.content
|
||||
}
|
||||
|
||||
createDOM() {
|
||||
const Component = Vue.extend(this.component)
|
||||
this._vm = new Component({
|
||||
this.vm = new Component({
|
||||
propsData: {
|
||||
node: this.node,
|
||||
view: this.view,
|
||||
@@ -32,7 +32,7 @@ export default class ComponentView {
|
||||
updateContent: content => this.updateContent(content),
|
||||
},
|
||||
}).$mount()
|
||||
return this._vm.$el
|
||||
return this.vm.$el
|
||||
}
|
||||
|
||||
updateAttrs(attrs) {
|
||||
@@ -75,12 +75,12 @@ export default class ComponentView {
|
||||
|
||||
this.node = node
|
||||
this.decorations = decorations
|
||||
this._vm._props.node = node
|
||||
this._vm._props.decorations = decorations
|
||||
this.vm._props.node = node
|
||||
this.vm._props.decorations = decorations
|
||||
return true
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._vm.$destroy()
|
||||
this.vm.$destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,11 +94,15 @@ export default class ExtensionManager {
|
||||
...commands,
|
||||
[name]: attrs => {
|
||||
view.focus()
|
||||
command({
|
||||
|
||||
const provider = command({
|
||||
type: schema[`${type}s`][name],
|
||||
attrs,
|
||||
schema,
|
||||
})(view.state, view.dispatch, view)
|
||||
})
|
||||
const callbacks = Array.isArray(provider) ? provider : [provider]
|
||||
|
||||
callbacks.forEach(callback => callback(view.state, view.dispatch, view))
|
||||
},
|
||||
}), {})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user