Merge branch 'feature/landingpage' of https://github.com/ueberdosis/tiptap-next into feature/landingpage
This commit is contained in:
7
docs/src/demos/Examples/Images/index.spec.js
Normal file
7
docs/src/demos/Examples/Images/index.spec.js
Normal file
@@ -0,0 +1,7 @@
|
||||
context('/demos/Examples/Images', () => {
|
||||
before(() => {
|
||||
cy.visit('/demos/Examples/Images')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
79
docs/src/demos/Examples/Images/index.vue
Normal file
79
docs/src/demos/Examples/Images/index.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div v-if="editor">
|
||||
<button @click="addImage">
|
||||
add image from URL
|
||||
</button>
|
||||
<editor-content :editor="editor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { EditorContent } from '@tiptap/vue'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import Image from '@tiptap/extension-image'
|
||||
import Dropcursor from '@tiptap/extension-dropcursor'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
addImage() {
|
||||
const url = window.prompt('URL')
|
||||
|
||||
if (url) {
|
||||
this.editor.chain().focus().setImage({ src: url }).run()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Image,
|
||||
Dropcursor,
|
||||
],
|
||||
content: `
|
||||
<p>This is a basic example of implementing images. Drag to re-order.</p>
|
||||
<img src="https://source.unsplash.com/8xznAGy4HcY/800x400" />
|
||||
<img src="https://source.unsplash.com/K9QHL52rE2k/800x400" />
|
||||
`,
|
||||
})
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* Basic editor styles */
|
||||
.ProseMirror {
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
|
||||
&.ProseMirror-selectednode {
|
||||
outline: 3px solid #68CEF8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
70
docs/src/demos/Experiments/Embeds/iframe.ts
Normal file
70
docs/src/demos/Experiments/Embeds/iframe.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Node, Command } from '@tiptap/core'
|
||||
|
||||
export interface IframeOptions {
|
||||
allowFullscreen: boolean,
|
||||
HTMLAttributes: {
|
||||
[key: string]: any
|
||||
},
|
||||
}
|
||||
|
||||
export default Node.create({
|
||||
name: 'iframe',
|
||||
|
||||
group: 'block',
|
||||
|
||||
// selectable: false,
|
||||
|
||||
defaultOptions: <IframeOptions>{
|
||||
allowFullscreen: true,
|
||||
HTMLAttributes: {
|
||||
class: 'iframe-wrapper',
|
||||
},
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
},
|
||||
frameborder: {
|
||||
default: 0,
|
||||
},
|
||||
allowfullscreen: {
|
||||
default: this.options.allowFullscreen,
|
||||
parseHTML: () => {
|
||||
return {
|
||||
allowfullscreen: this.options.allowFullscreen,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{
|
||||
tag: 'iframe',
|
||||
}]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', this.options.HTMLAttributes, ['iframe', HTMLAttributes, 0]]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
/**
|
||||
* Add an iframe
|
||||
*/
|
||||
setIframe: (options: { src: string }): Command => ({ tr, dispatch }) => {
|
||||
const { selection } = tr
|
||||
const node = this.type.create(options)
|
||||
|
||||
if (dispatch) {
|
||||
tr.replaceRangeWith(selection.from, selection.to, node)
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
84
docs/src/demos/Experiments/Embeds/index.vue
Normal file
84
docs/src/demos/Experiments/Embeds/index.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div v-if="editor">
|
||||
<button @click="addImage">
|
||||
add iframe
|
||||
</button>
|
||||
|
||||
<editor-content :editor="editor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
Editor, EditorContent, defaultExtensions,
|
||||
} from '@tiptap/vue-starter-kit'
|
||||
import Iframe from './iframe'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
extensions: [
|
||||
...defaultExtensions(),
|
||||
Iframe,
|
||||
],
|
||||
content: `
|
||||
<p>Here is an exciting video:</p>
|
||||
<iframe src="https://www.youtube.com/embed/XIMLoLxmTDw" frameborder="0" allowfullscreen></iframe>
|
||||
`,
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
addImage() {
|
||||
const url = window.prompt('URL')
|
||||
|
||||
if (url) {
|
||||
this.editor.chain().focus().setIframe({ src: url }).run()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ProseMirror {
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
.iframe-wrapper {
|
||||
position: relative;
|
||||
padding-bottom: 100/16*9%;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
&.ProseMirror-selectednode {
|
||||
outline: 3px solid #68CEF8;
|
||||
}
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Extension } from '@tiptap/core'
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||
import { Plugin } from 'prosemirror-state'
|
||||
|
||||
export interface PlaceholderOptions {
|
||||
emptyEditorClass: string,
|
||||
emptyNodeClass: string,
|
||||
placeholder: string | Function,
|
||||
showOnlyWhenEditable: boolean,
|
||||
showOnlyCurrent: boolean,
|
||||
}
|
||||
|
||||
export default Extension.create({
|
||||
name: 'placeholder',
|
||||
|
||||
defaultOptions: <PlaceholderOptions>{
|
||||
emptyEditorClass: 'is-editor-empty',
|
||||
emptyNodeClass: 'is-empty',
|
||||
placeholder: 'Write something …',
|
||||
showOnlyWhenEditable: true,
|
||||
showOnlyCurrent: true,
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
decorations: ({ doc, selection }) => {
|
||||
const active = this.editor.isEditable || !this.options.showOnlyWhenEditable
|
||||
const { anchor } = selection
|
||||
const decorations: Decoration[] = []
|
||||
const isEditorEmpty = doc.textContent.length === 0
|
||||
|
||||
if (!active) {
|
||||
return
|
||||
}
|
||||
|
||||
doc.descendants((node, pos) => {
|
||||
const hasAnchor = anchor >= pos && anchor <= (pos + node.nodeSize)
|
||||
const isNodeEmpty = node.content.size === 0
|
||||
|
||||
if ((hasAnchor || !this.options.showOnlyCurrent) && isNodeEmpty) {
|
||||
const classes = [this.options.emptyNodeClass]
|
||||
|
||||
if (isEditorEmpty) {
|
||||
classes.push(this.options.emptyEditorClass)
|
||||
}
|
||||
|
||||
const decoration = Decoration.node(pos, pos + node.nodeSize, {
|
||||
class: classes.join(' '),
|
||||
'data-empty-text': typeof this.options.placeholder === 'function'
|
||||
? this.options.placeholder(node)
|
||||
: this.options.placeholder,
|
||||
})
|
||||
decorations.push(decoration)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return DecorationSet.create(doc, decorations)
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
56
docs/src/demos/Experiments/Placeholder/index.vue
Normal file
56
docs/src/demos/Experiments/Placeholder/index.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<editor-content :editor="editor" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Editor, EditorContent } from '@tiptap/vue-starter-kit'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import Placeholder from './extension/placeholder'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Placeholder,
|
||||
],
|
||||
})
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* Basic editor styles */
|
||||
.ProseMirror {
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Placeholder */
|
||||
.ProseMirror p.is-editor-empty:first-child::before {
|
||||
content: attr(data-empty-text);
|
||||
float: left;
|
||||
color: #ced4da;
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -31,7 +31,9 @@ export default {
|
||||
addImage() {
|
||||
const url = window.prompt('URL')
|
||||
|
||||
this.editor.chain().focus().setImage({ src: url }).run()
|
||||
if (url) {
|
||||
this.editor.chain().focus().setImage({ src: url }).run()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -68,6 +70,10 @@ export default {
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
|
||||
&.ProseMirror-selectednode {
|
||||
outline: 3px solid #68CEF8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user