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:
Philipp Kühn
2018-09-24 15:44:42 +02:00
30 changed files with 567 additions and 162 deletions

View File

@@ -100,9 +100,11 @@ By default the editor will only support paragraphs. Other nodes and marks are av
<script> <script>
import { Editor } from 'tiptap' import { Editor } from 'tiptap'
import { import {
// Nodes
BlockquoteNode, BlockquoteNode,
BulletListNode, BulletListNode,
CodeBlockNode, CodeBlockNode,
CodeBlockHighlightNode,
HardBreakNode, HardBreakNode,
HeadingNode, HeadingNode,
ImageNode, ImageNode,
@@ -110,11 +112,18 @@ import {
OrderedListNode, OrderedListNode,
TodoItemNode, TodoItemNode,
TodoListNode, TodoListNode,
// Marks
BoldMark, BoldMark,
CodeMark, CodeMark,
ItalicMark, ItalicMark,
LinkMark, LinkMark,
StrikeMark,
UnderlineMark,
// General Extensions
HistoryExtension, HistoryExtension,
PlaceholderExtension,
} from 'tiptap-extensions' } from 'tiptap-extensions'
export default { export default {
@@ -138,7 +147,10 @@ export default {
new CodeMark(), new CodeMark(),
new ItalicMark(), new ItalicMark(),
new LinkMark(), new LinkMark(),
new StrikeMark(),
new UnderlineMark(),
new HistoryExtension(), new HistoryExtension(),
new PlaceholderExtension(),
], ],
} }
}, },
@@ -322,6 +334,7 @@ This is a basic example of building a custom menu. A more advanced menu can be f
<template> <template>
<editor :extensions="extensions"> <editor :extensions="extensions">
<div slot="menubar" slot-scope="{ nodes, marks }"> <div slot="menubar" slot-scope="{ nodes, marks }">
<div v-if="nodes && marks">
<button :class="{ 'is-active': nodes.heading.active({ level: 1 }) }" @click="nodes.heading.command({ level: 1 })"> <button :class="{ 'is-active': nodes.heading.active({ level: 1 }) }" @click="nodes.heading.command({ level: 1 })">
H1 H1
</button> </button>
@@ -329,6 +342,7 @@ This is a basic example of building a custom menu. A more advanced menu can be f
Bold Bold
</button> </button>
</div> </div>
</div>
<div slot="content" slot-scope="props"> <div slot="content" slot-scope="props">
<p>This text can be made bold.</p> <p>This text can be made bold.</p>
</div> </div>

View File

@@ -21,6 +21,22 @@
<icon name="italic" /> <icon name="italic" />
</button> </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 <button
class="menubar__button" class="menubar__button"
@click="marks.code.command" @click="marks.code.command"
@@ -132,6 +148,8 @@ import {
CodeMark, CodeMark,
ItalicMark, ItalicMark,
LinkMark, LinkMark,
StrikeMark,
UnderlineMark,
HistoryExtension, HistoryExtension,
} from 'tiptap-extensions' } from 'tiptap-extensions'
@@ -156,6 +174,8 @@ export default {
new CodeMark(), new CodeMark(),
new ItalicMark(), new ItalicMark(),
new LinkMark(), new LinkMark(),
new StrikeMark(),
new UnderlineMark(),
new HistoryExtension(), new HistoryExtension(),
], ],
} }

View File

@@ -18,49 +18,34 @@
</template> </template>
<script> <script>
import Icon from 'Components/Icon'
import { Editor } from 'tiptap' import { Editor } from 'tiptap'
import { import {
BlockquoteNode,
BulletListNode,
CodeBlockHighlightNode, CodeBlockHighlightNode,
HardBreakNode, HardBreakNode,
HeadingNode, HeadingNode,
ListItemNode,
OrderedListNode,
TodoItemNode,
TodoListNode,
BoldMark, BoldMark,
CodeMark, CodeMark,
ItalicMark, ItalicMark,
LinkMark,
HistoryExtension,
} from 'tiptap-extensions' } from 'tiptap-extensions'
import { javascript, css } from './examples' import {
javascript,
css,
} from './examples'
export default { export default {
components: { components: {
Editor, Editor,
Icon,
}, },
data() { data() {
return { return {
extensions: [ extensions: [
new BlockquoteNode(),
new BulletListNode(),
new CodeBlockHighlightNode(), new CodeBlockHighlightNode(),
new HardBreakNode(), new HardBreakNode(),
new HeadingNode({ maxLevel: 3 }), new HeadingNode({ maxLevel: 3 }),
new ListItemNode(),
new OrderedListNode(),
new TodoItemNode(),
new TodoListNode(),
new BoldMark(), new BoldMark(),
new CodeMark(), new CodeMark(),
new ItalicMark(), new ItalicMark(),
new LinkMark(),
new HistoryExtension(),
], ],
javascript, javascript,
css, css,

View File

@@ -17,22 +17,12 @@
</template> </template>
<script> <script>
import Icon from 'Components/Icon'
import { Editor } from 'tiptap' import { Editor } from 'tiptap'
import { import {
BlockquoteNode,
BulletListNode,
CodeBlockNode,
HardBreakNode, HardBreakNode,
HeadingNode, HeadingNode,
ListItemNode,
OrderedListNode,
TodoItemNode,
TodoListNode,
BoldMark, BoldMark,
CodeMark,
ItalicMark, ItalicMark,
LinkMark,
HistoryExtension, HistoryExtension,
} from 'tiptap-extensions' } from 'tiptap-extensions'
import IframeNode from './Iframe.js' import IframeNode from './Iframe.js'
@@ -40,24 +30,14 @@ import IframeNode from './Iframe.js'
export default { export default {
components: { components: {
Editor, Editor,
Icon,
}, },
data() { data() {
return { return {
extensions: [ extensions: [
new BlockquoteNode(),
new BulletListNode(),
new CodeBlockNode(),
new HardBreakNode(), new HardBreakNode(),
new HeadingNode({ maxLevel: 3 }), new HeadingNode({ maxLevel: 3 }),
new ListItemNode(),
new OrderedListNode(),
new TodoItemNode(),
new TodoListNode(),
new BoldMark(), new BoldMark(),
new CodeMark(),
new ItalicMark(), new ItalicMark(),
new LinkMark(),
new HistoryExtension(), new HistoryExtension(),
// custom extension // custom extension
new IframeNode(), new IframeNode(),

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <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 class="menubar" slot="menubar" slot-scope="{ nodes, marks }">
<div v-if="nodes && marks"> <div v-if="nodes && marks">
@@ -99,6 +99,15 @@
</editor> </editor>
<div class="actions">
<button class="button" @click="clearContent">
Clear Content
</button>
<button class="button" @click="setContent">
Set Content
</button>
</div>
<div class="export"> <div class="export">
<h3>JSON</h3> <h3>JSON</h3>
<pre><code v-html="json"></code></pre> <pre><code v-html="json"></code></pre>
@@ -161,6 +170,25 @@ export default {
this.json = getJSON() this.json = getJSON()
this.html = getHTML() 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> </script>
@@ -168,10 +196,15 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~variables"; @import "~variables";
.actions {
max-width: 30rem;
margin: 0 auto 2rem auto;
}
.export { .export {
max-width: 30rem; max-width: 30rem;
margin: 0 auto 5rem auto; margin: 0 auto 2rem auto;
pre { pre {
padding: 1rem; padding: 1rem;

View File

@@ -17,49 +17,29 @@
</template> </template>
<script> <script>
import Icon from 'Components/Icon'
import { Editor } from 'tiptap' import { Editor } from 'tiptap'
import { import {
BlockquoteNode,
BulletListNode,
CodeBlockNode,
HardBreakNode, HardBreakNode,
HeadingNode, HeadingNode,
ImageNode, ImageNode,
ListItemNode,
OrderedListNode,
TodoItemNode,
TodoListNode,
BoldMark, BoldMark,
CodeMark, CodeMark,
ItalicMark, ItalicMark,
LinkMark,
HistoryExtension,
} from 'tiptap-extensions' } from 'tiptap-extensions'
export default { export default {
components: { components: {
Editor, Editor,
Icon,
}, },
data() { data() {
return { return {
extensions: [ extensions: [
new BlockquoteNode(),
new BulletListNode(),
new CodeBlockNode(),
new HardBreakNode(), new HardBreakNode(),
new HeadingNode({ maxLevel: 3 }), new HeadingNode({ maxLevel: 3 }),
new ImageNode(), new ImageNode(),
new ListItemNode(),
new OrderedListNode(),
new TodoItemNode(),
new TodoListNode(),
new BoldMark(), new BoldMark(),
new CodeMark(), new CodeMark(),
new ItalicMark(), new ItalicMark(),
new LinkMark(),
new HistoryExtension(),
], ],
} }
}, },

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

View File

@@ -16,47 +16,29 @@
</template> </template>
<script> <script>
import Icon from 'Components/Icon'
import { Editor } from 'tiptap' import { Editor } from 'tiptap'
import { import {
BlockquoteNode,
BulletListNode,
CodeBlockNode,
HardBreakNode, HardBreakNode,
HeadingNode, HeadingNode,
ListItemNode,
OrderedListNode,
TodoItemNode,
TodoListNode,
BoldMark, BoldMark,
CodeMark, CodeMark,
ItalicMark, ItalicMark,
LinkMark, LinkMark,
HistoryExtension,
} from 'tiptap-extensions' } from 'tiptap-extensions'
export default { export default {
components: { components: {
Editor, Editor,
Icon,
}, },
data() { data() {
return { return {
extensions: [ extensions: [
new BlockquoteNode(),
new BulletListNode(),
new CodeBlockNode(),
new HardBreakNode(), new HardBreakNode(),
new HeadingNode({ maxLevel: 3 }), new HeadingNode({ maxLevel: 3 }),
new ListItemNode(),
new OrderedListNode(),
new TodoItemNode(),
new TodoListNode(),
new BoldMark(), new BoldMark(),
new CodeMark(), new CodeMark(),
new ItalicMark(), new ItalicMark(),
new LinkMark(), new LinkMark(),
new HistoryExtension(),
], ],
} }
}, },

View File

@@ -71,20 +71,14 @@
import Icon from 'Components/Icon' import Icon from 'Components/Icon'
import { Editor } from 'tiptap' import { Editor } from 'tiptap'
import { import {
BlockquoteNode,
BulletListNode,
CodeBlockNode, CodeBlockNode,
HardBreakNode, HardBreakNode,
HeadingNode, HeadingNode,
ListItemNode,
OrderedListNode,
TodoItemNode, TodoItemNode,
TodoListNode, TodoListNode,
BoldMark, BoldMark,
CodeMark, CodeMark,
ItalicMark, ItalicMark,
LinkMark,
HistoryExtension,
} from 'tiptap-extensions' } from 'tiptap-extensions'
export default { export default {
@@ -96,22 +90,60 @@ export default {
return { return {
customProp: 2, customProp: 2,
extensions: [ extensions: [
new BlockquoteNode(),
new BulletListNode(),
new CodeBlockNode(), new CodeBlockNode(),
new HardBreakNode(), new HardBreakNode(),
new HeadingNode({ maxLevel: 3 }), new HeadingNode({ maxLevel: 3 }),
new ListItemNode(),
new OrderedListNode(),
new TodoItemNode(), new TodoItemNode(),
new TodoListNode(), new TodoListNode(),
new BoldMark(), new BoldMark(),
new CodeMark(), new CodeMark(),
new ItalicMark(), 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>

View File

@@ -33,6 +33,9 @@
<router-link class="subnavigation__link" to="/mentions"> <router-link class="subnavigation__link" to="/mentions">
Mentions Mentions
</router-link> </router-link>
<router-link class="subnavigation__link" to="/placeholder">
Placeholder
</router-link>
<router-link class="subnavigation__link" to="/export"> <router-link class="subnavigation__link" to="/export">
Export HTML or JSON Export HTML or JSON
</router-link> </router-link>

View 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

View 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

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

View File

@@ -60,3 +60,20 @@ h2,
h3 { h3 {
line-height: 1.3; 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";

View 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);
}
}
}

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

View File

@@ -3,18 +3,6 @@ import Vue from 'vue'
import VueRouter from 'vue-router' import VueRouter from 'vue-router'
import svgSpriteLoader from 'helpers/svg-sprite-loader' import svgSpriteLoader from 'helpers/svg-sprite-loader'
import App from 'Components/App' 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' } const __svg__ = { path: './assets/images/icons/*.svg', name: 'assets/images/[hash].sprite.svg' }
svgSpriteLoader(__svg__.filename) svgSpriteLoader(__svg__.filename)
@@ -26,84 +14,91 @@ Vue.use(VueRouter)
const routes = [ const routes = [
{ {
path: '/', path: '/',
component: RouteBasic, component: () => import('Components/Routes/Basic'),
meta: { meta: {
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Basic', githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Basic',
}, },
}, },
{ {
path: '/menu-bubble', path: '/menu-bubble',
component: RouteMenuBubble, component: () => import('Components/Routes/MenuBubble'),
meta: { meta: {
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/MenuBubble', githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/MenuBubble',
}, },
}, },
{ {
path: '/links', path: '/links',
component: RouteLinks, component: () => import('Components/Routes/Links'),
meta: { meta: {
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Links', githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Links',
}, },
}, },
{ {
path: '/images', path: '/images',
component: RouteImages, component: () => import('Components/Routes/Images'),
meta: { meta: {
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Images', githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Images',
}, },
}, },
{ {
path: '/hiding-menu-bar', path: '/hiding-menu-bar',
component: RouteHidingMenuBar, component: () => import('Components/Routes/HidingMenuBar'),
meta: { meta: {
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/HidingMenuBar', githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/HidingMenuBar',
}, },
}, },
{ {
path: '/todo-list', path: '/todo-list',
component: RouteTodoList, component: () => import('Components/Routes/TodoList'),
meta: { meta: {
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/TodoList', githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/TodoList',
}, },
}, },
{ {
path: '/markdown-shortcuts', path: '/markdown-shortcuts',
component: RouteMarkdownShortcuts, component: () => import('Components/Routes/MarkdownShortcuts'),
meta: { meta: {
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/MarkdownShortcuts', githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/MarkdownShortcuts',
}, },
}, },
{ {
path: '/code-highlighting', path: '/code-highlighting',
component: RouteCodeHighlighting, component: () => import('Components/Routes/CodeHighlighting'),
meta: { meta: {
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/CodeHighlighting', githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/CodeHighlighting',
}, },
}, },
{ {
path: '/read-only', path: '/read-only',
component: RouteReadOnly, component: () => import('Components/Routes/ReadOnly'),
meta: { meta: {
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/ReadOnly', githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/ReadOnly',
}, },
}, },
{ {
path: '/embeds', path: '/embeds',
component: RouteEmbeds, component: () => import('Components/Routes/Embeds'),
meta: { meta: {
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Embeds', githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Embeds',
}, },
}, },
{ {
path: '/mentions', path: '/mentions',
component: RouteMentions, component: () => import('Components/Routes/Mentions'),
meta: { meta: {
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Mentions', 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', path: '/export',
component: RouteExport, component: () => import('Components/Routes/Export'),
meta: { meta: {
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Export', githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Export',
}, },

View File

@@ -1,6 +1,6 @@
{ {
"name": "tiptap-extensions", "name": "tiptap-extensions",
"version": "0.8.0", "version": "0.14.1",
"description": "Extensions for tiptap", "description": "Extensions for tiptap",
"homepage": "https://tiptap.scrumpy.io", "homepage": "https://tiptap.scrumpy.io",
"license": "MIT", "license": "MIT",
@@ -8,6 +8,7 @@
"module": "dist/extensions.esm.js", "module": "dist/extensions.esm.js",
"unpkg": "dist/extensions.js", "unpkg": "dist/extensions.js",
"jsdelivr": "dist/extensions.js", "jsdelivr": "dist/extensions.js",
"sideEffects": false,
"files": [ "files": [
"src", "src",
"dist" "dist"
@@ -24,7 +25,7 @@
"prosemirror-history": "^1.0.2", "prosemirror-history": "^1.0.2",
"prosemirror-state": "^1.2.2", "prosemirror-state": "^1.2.2",
"prosemirror-view": "^1.5.1", "prosemirror-view": "^1.5.1",
"tiptap": "^0.10.0", "tiptap": "^0.12.1",
"tiptap-commands": "^0.3.0" "tiptap-commands": "^0.3.0"
} }
} }

View 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)
},
},
}),
]
}
}

View File

@@ -15,5 +15,8 @@ export { default as BoldMark } from './marks/Bold'
export { default as CodeMark } from './marks/Code' export { default as CodeMark } from './marks/Code'
export { default as ItalicMark } from './marks/Italic' export { default as ItalicMark } from './marks/Italic'
export { default as LinkMark } from './marks/Link' 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 HistoryExtension } from './extensions/History'
export { default as PlaceholderExtension } from './extensions/Placeholder'

View File

@@ -7,20 +7,6 @@ export default class LinkMark extends Mark {
return 'link' 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() { get schema() {
return { return {
attrs: { attrs: {

View 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),
]
}
}

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

View File

@@ -1,5 +1,5 @@
import { Node } from 'tiptap' import { Node } from 'tiptap'
import { wrappingInputRule, wrapInList, toggleList } from 'tiptap-commands' import { wrappingInputRule, toggleList } from 'tiptap-commands'
export default class BulletNode extends Node { export default class BulletNode extends Node {
@@ -22,9 +22,9 @@ export default class BulletNode extends Node {
return toggleList(type, schema.nodes.list_item) return toggleList(type, schema.nodes.list_item)
} }
keys({ type }) { keys({ type, schema }) {
return { return {
'Shift-Ctrl-8': wrapInList(type), 'Shift-Ctrl-8': toggleList(type, schema.nodes.list_item),
} }
} }

View File

@@ -1,7 +1,7 @@
import { Node } from 'tiptap' import { Node } from 'tiptap'
import { splitListItem, liftListItem, sinkListItem } from 'tiptap-commands' import { splitListItem, liftListItem, sinkListItem } from 'tiptap-commands'
export default class OrderedListNode extends Node { export default class ListItemNode extends Node {
get name() { get name() {
return 'list_item' return 'list_item'

View File

@@ -1,5 +1,5 @@
import { Node } from 'tiptap' import { Node } from 'tiptap'
import { wrappingInputRule, wrapInList, toggleList } from 'tiptap-commands' import { wrappingInputRule, toggleList } from 'tiptap-commands'
export default class OrderedListNode extends Node { export default class OrderedListNode extends Node {
@@ -32,9 +32,9 @@ export default class OrderedListNode extends Node {
return toggleList(type, schema.nodes.list_item) return toggleList(type, schema.nodes.list_item)
} }
keys({ type }) { keys({ type, schema }) {
return { return {
'Shift-Ctrl-9': wrapInList(type), 'Shift-Ctrl-9': toggleList(type, schema.nodes.list_item),
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "tiptap", "name": "tiptap",
"version": "0.10.0", "version": "0.12.1",
"description": "A rich-text editor for Vue.js", "description": "A rich-text editor for Vue.js",
"homepage": "https://tiptap.scrumpy.io", "homepage": "https://tiptap.scrumpy.io",
"license": "MIT", "license": "MIT",

View File

@@ -1,6 +1,6 @@
import { EditorState, Plugin } from 'prosemirror-state' import { EditorState, Plugin } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view' import { EditorView } from 'prosemirror-view'
import { Schema, DOMParser } from 'prosemirror-model' import { Schema, DOMParser, DOMSerializer } from 'prosemirror-model'
import { gapCursor } from 'prosemirror-gapcursor' import { gapCursor } from 'prosemirror-gapcursor'
import { keymap } from 'prosemirror-keymap' import { keymap } from 'prosemirror-keymap'
import { baseKeymap } from 'prosemirror-commands' import { baseKeymap } from 'prosemirror-commands'
@@ -56,6 +56,17 @@ export default {
} }
}, },
watch: {
doc: {
deep: true,
handler() {
this.setContent(this.doc, true)
},
},
},
render(createElement) { render(createElement) {
const slots = [] const slots = []
@@ -70,7 +81,7 @@ export default {
nodes: this.menuActions ? this.menuActions.nodes : null, nodes: this.menuActions ? this.menuActions.nodes : null,
marks: this.menuActions ? this.menuActions.marks : null, marks: this.menuActions ? this.menuActions.marks : null,
focused: this.view ? this.view.focused : false, focused: this.view ? this.view.focused : false,
focus: () => this.view.focus(), focus: this.focus,
}) })
slots.push(this.menubarNode) slots.push(this.menubarNode)
} else if (name === 'menububble') { } else if (name === 'menububble') {
@@ -78,7 +89,7 @@ export default {
nodes: this.menuActions ? this.menuActions.nodes : null, nodes: this.menuActions ? this.menuActions.nodes : null,
marks: this.menuActions ? this.menuActions.marks : null, marks: this.menuActions ? this.menuActions.marks : null,
focused: this.view ? this.view.focused : false, focused: this.view ? this.view.focused : false,
focus: () => this.view.focus(), focus: this.focus,
}) })
slots.push(this.menububbleNode) slots.push(this.menububbleNode)
} }
@@ -195,6 +206,12 @@ export default {
}) })
}, },
destroyEditor() {
if (this.view) {
this.view.destroy()
}
},
updateMenuActions() { updateMenuActions() {
this.menuActions = buildMenuActions({ this.menuActions = buildMenuActions({
schema: this.schema, schema: this.schema,
@@ -212,6 +229,25 @@ export default {
return 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', { this.$emit('update', {
getHTML: this.getHTML, getHTML: this.getHTML,
getJSON: this.getJSON, getJSON: this.getJSON,
@@ -219,12 +255,31 @@ export default {
}) })
}, },
getHTML() { setContent(content = {}, emitUpdate = false) {
return this.view.dom.innerHTML 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() { clearContent(emitUpdate = false) {
return this.state.doc.toJSON() this.setContent({
type: 'doc',
content: [{
type: 'paragraph',
}],
}, emitUpdate)
},
focus() {
this.view.focus()
}, },
}, },
@@ -233,4 +288,8 @@ export default {
this.initEditor() this.initEditor()
}, },
beforeDestroy() {
this.destroyEditor()
},
} }

View File

@@ -16,12 +16,12 @@ export default class ComponentView {
this.editable = editable this.editable = editable
this.dom = this.createDOM() this.dom = this.createDOM()
this.contentDOM = this._vm.$refs.content this.contentDOM = this.vm.$refs.content
} }
createDOM() { createDOM() {
const Component = Vue.extend(this.component) const Component = Vue.extend(this.component)
this._vm = new Component({ this.vm = new Component({
propsData: { propsData: {
node: this.node, node: this.node,
view: this.view, view: this.view,
@@ -32,7 +32,7 @@ export default class ComponentView {
updateContent: content => this.updateContent(content), updateContent: content => this.updateContent(content),
}, },
}).$mount() }).$mount()
return this._vm.$el return this.vm.$el
} }
updateAttrs(attrs) { updateAttrs(attrs) {
@@ -75,12 +75,12 @@ export default class ComponentView {
this.node = node this.node = node
this.decorations = decorations this.decorations = decorations
this._vm._props.node = node this.vm._props.node = node
this._vm._props.decorations = decorations this.vm._props.decorations = decorations
return true return true
} }
destroy() { destroy() {
this._vm.$destroy() this.vm.$destroy()
} }
} }

View File

@@ -94,11 +94,15 @@ export default class ExtensionManager {
...commands, ...commands,
[name]: attrs => { [name]: attrs => {
view.focus() view.focus()
command({
const provider = command({
type: schema[`${type}s`][name], type: schema[`${type}s`][name],
attrs, attrs,
schema, schema,
})(view.state, view.dispatch, view) })
const callbacks = Array.isArray(provider) ? provider : [provider]
callbacks.forEach(callback => callback(view.state, view.dispatch, view))
}, },
}), {}) }), {})
} }