Closed marbuser closed 4 years ago
I completely agree with you. I have also found this plugin recently as I had a new feature request from a client. The plugin it self is really good and provides a simple to use VueJS wrapper, but only for built examples. If you need to add anything custom it gets really hard, because there is no documentation.
I have been following the issue board here and some people failed to implement new request because of lack of documentation. Hopefully we as a community can improve this.
As I have put a lot of project hours in this library and got around how it works here is a solution for you.
This whole library is designed around using custom nodes, but as said before there is no documentation how to do it.
What you need is make a custom node. For the sake of example lets embed a Rick Astley video.
I am not sure how the user will input the video but I will guess its a modal.
Here is a video we want to embed:
https://www.youtube.com/watch?v=dQw4w9WgXcQ
This is the embed code given by Youtube
<iframe width="560" height="315" src="https://www.youtube.com/embed/dQw4w9WgXcQ" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
We see that we need the to extract the Youtube video ID from the URL and add it to the embed, like this:
`<iframe width="560" height="315" src="https://www.youtube.com/embed/${videoID}" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`
To extract the video ID we will user regex function found on SO
function youtubeParser(url) {
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
const match = url.match(regExp);
return (match && match[7].length === 11) ? match[7] : false;
}
This covers all youtube URLs, even short ones
youtu.be
Now that we got that sorted out let do the editor thing. As I said before you need to create a new custom node, for example iframe
Here is how it will look:
import { Node } from "tiptap";
export default class Iframe extends Node {
get name() {
return "iframe";
}
get schema() {
return {
attrs: {
src: {
default: null
}
},
group: "block",
selectable: false,
parseDOM: [
{
tag: "iframe",
getAttrs: dom => ({
src: dom.getAttribute("src")
})
}
],
toDOM: node => [
"iframe",
{
src: `https://www.youtube.com/embed/${node.attrs.src}`,
frameborder: 0,
allowfullscreen: "true",
allow:
"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
// You can set the width and height here also
}
]
};
}
commands({ type }) {
return attrs => (state, dispatch) => {
const { selection } = state;
const position = selection.$cursor
? selection.$cursor.pos
: selection.$to.pos;
const node = type.create(attrs);
const transaction = state.tr.insert(position, node);
dispatch(transaction);
};
}
}
Now when you register this to your editor you will have an iframe
command that can insert the required node where the cursor is. Just need to add it to your editor
editor: new Editor({
extensions: [
new Bold(),
new Italic(),
new Strike(),
// Custom extension
new Iframe
],
content: "",
})
This is now simple you just need to send the command to the modal, like this:
<EditorMenuBar :editor="editor">
<div class="menubar editor-toolbar" slot-scope="{ commands }">
<button class="menubar-button" @click="showVideoModal(commands.iframe)">
<Icon name="video"/>
</button>
</div>
</EditorMenuBar>
Where your showVideoModal()
function will look like this:
showVideoModal(command) {
this.$refs.videoModal.showModal(command)
}
So you sent the command to your modal, what now?
Add one more function which will detect when user clicks Add.
addCommand(data) {
if (data.command !== null) {
data.command(data.data)
}
}
Then bind this to your modal:
<VideoModal ref="videoModal" @onConfirm="addCommand"/>
You are now done, just implement the modal how you like and when emit the event back. Here is how a modal should look.
<template>
<div class="modal" v-if="show">
<input v-model="url" /> <button @click="insertVideo">Add Video</button>
</div>
</template>
<script>
export default {
data() {
return {
url: "",
command: null,
show: false
};
},
computed: {
youtubeID() {
return this.youtubeParser(this.url);
}
},
methods: {
youtubeParser(url) {
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
const match = url.match(regExp);
return match && match[7].length === 11 ? match[7] : false;
},
showModal(command) {
// Add the sent command
this.command = command;
this.show = true;
},
insertVideo() {
// Some check can be done here, like if `youtubeID` is not false.
const data = {
command: this.command,
data: {
src: this.youtubeID
}
};
this.$emit("onConfirm", data);
this.show = false;
}
}
};
</script>
Hope I helped, I will update the answer with a sandbox when I can.
Working sandbox: https://codesandbox.io/s/pk8wk2xpoj
Screenshot:
@marbuser You are absolutely right! Unfortunately it's not my biggest strength and I do not find the time to improve the docs for now but it's definitely on my list!
@ilicmarko Thanks! Absolutely fantastic answer! This worked perfectly for me and is the sort of explanations we need on the docs!
@philippkuehn No worries at all, we aren't all perfect. ;) Perhaps for future reference, you could try using something like VuePress (https://vuepress.vuejs.org/) and hosting the actual docs in the repo in a folder called docs or something similar. This way, if someone finds a mistake or wants to add some extra documentation, they can just do a PR on the relevant docpage. :)
yes please at least allow the users to add PR for improving documentation. I'm having difficulty just trying to reproduce the examples in a simple vue app without having to write out the whole css.
@Neophen feel free to open a PR!
Thanks a lot @ilicmarko for the explanation. It works well! However I would like to nest the iframe in a div in order to make it work in a responsive way (kind of like this https://benmarshall.me/responsive-iframes/).
What do you think is the best way to embed the iframe inside a div?
After some trial and error I actually got this to work, but I have no idea if this is a good way to do it. I modified the toDOM part of the schema method like so:
get schema() {
return {
attrs: {
src: {
default: null
}
},
group: 'block',
selectable: false,
parseDOM: [
{
tag: 'iframe',
getAttrs: dom => ({
src: dom.getAttribute('src')
})
}
],
toDOM: node => [
'div',
{
class: 'video'
},
[
'iframe',
{
src: node.attrs.src,
frameborder: 0,
allowfullscreen: 'true',
width: '100%',
allow:
'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'
// You can set the width and height here also
}
]
]
};
}
I would be interested to know if this is the right way to do it or if there's a better way.
@svennerberg Yes this is ok. You just change the DOM structure (toDOM
).
That piece of info right there helped me out a lot when I was trying to make my own extension for the <picture>
node:
import {Image} from "tiptap-extensions"
export default class Picture extends Image {
get name() {
return "picture"
}
get schema() {
return {
inline: true,
attrs: {
src: {
default: null
},
alt: {
default: null,
},
title: {
default: null,
},
"data-id": {
default: null
},
sources: {
default: []
}
},
group: "inline",
draggable: true,
parseDOM: [
{
tag: "picture",
getAttrs: dom => ({
src: dom.getElementsByTagName("img")[0].getAttribute("src"),
title: dom.getElementsByTagName("img")[0].getAttribute("title"),
alt: dom.getElementsByTagName("img")[0].getAttribute("alt"),
"data-id": dom.dataset["id"],
sources: [...dom.getElementsByTagName("source")].map(source => {
return {
media: source.getAttribute("media"),
srcset: source.getAttribute("srcset")
}
})
}),
},
],
toDOM: node => [
"picture",
{"data-id": node.attrs["data-id"]},
...node.attrs.sources.map(source => ["source", source]),
[
"img",
{
src: node.attrs.src,
title: node.attrs.title,
alt: node.attrs.alt
}
]
],
}
}
}
Thanx 💝
@ilicmarko Your example was wonderful. Thank you! I was wondering if you could show me how you would adjust your extension from being an iFrame to a Vue component. In the docs, there’s a Vue component, but when I use it there is no command() method so I have no idea how to actually call the example.
@bswank
there is no command() method so I have no idea how to actually call the example
I'm having this problem too. Did you manage to find a solution?
Hi ! I think there is a small error in this part (I think that @svennerberg saw that) :
parseDOM: [
{
tag: "iframe",
getAttrs: dom => ({
src: dom.getAttribute("src")
})
}
],
I guess this part is call when we have to render the editor ? (like when edit)
It will take the src
with this for instance https://www.youtube.com/watch?v=yoWDxUVIHPU
And give it back to :
toDOM: node => [
"iframe",
{
src: `https://www.youtube.com/embed/${node.attrs.src}`, // <- here
frameborder: 0,
allowfullscreen: "true",
allow: "accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
}
]
It will generate an url like : https://www.youtube.com/embed/https://www.youtube.com/watch?v=yoWDxUVIHPU
BUT we want : https://www.youtube.com/embed/yoWDxUVIHPU
So we need to change toDom:
toDOM: node => [
"iframe",
{
src: node.attrs.src`,
frameborder: 0,
allowfullscreen: "true",
allow: "accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
}
]
But also :
insertVideo() {
// Some check can be done here, like if `youtubeID` is not false.
const data = {
command: this.command,
data: {
src: `https://www.youtube.com/embed/${this.youtubeID}` // <- changed
}
};
this.$emit("onConfirm", data);
this.show = false;
}
So would anyone have an idea on how to render the iframe example like the docs have it? Where there is an input field below the video that you can alter when the editor is able to be edited?
The modal example given above seems.... awesome? But also a little overkill, right? Shouldn't I be able to just conditionally render an input field to input the source for images/videos/embeds like the example? Then when you're not editing it, the input shoudl just be hidden (v-if).
Is the get view()
basically a renderless component? It's returning an object, so are those the component options? TipTap's example has a template
option so that would make me think you could have a render()
function instead, but I don't know how that relates to the toDom
function in the schema
above it in the example... : (
@rasliche I also thought the example was a little overkill and I wasn't able to modify to insert other elements. I'm hoping for better docs too
SOLVED Had to listen for the paste event and stop propagation, like in the example of course.
I'm having an issue being able to copy/paste into the rendered component in TipTap. I mostly got my version of an iframe working, but for some reason in the editor I can't paste into the that gets rendered. If I Cmd+V with my cursor in the input, the URL I'm trying to paste appears above or below the iframe "component" i'm trying to make.
Any thoughts?
import { Node } from 'tiptap'
export default class Iframe extends Node {
get name() {
return 'iframe'
}
get schema() {
return {
attrs: {
// These have defaults. Here's the attribute spec:
// https://prosemirror.net/docs/ref/#model.AttributeSpec
frameborder: {
default: 0
},
allowfullscreen: {
default: 'true'
},
allow: {
default: 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'
},
width: {
default: '560'
},
height: {
default: '315'
},
src: {
default: 'https://youtube.com/embed/IHv0nVxnycw'
}
},
group: 'block',
selectable: false,
parseDOM: [
{
tag: 'iframe[src]',
getAttrs: dom => ({
src: dom.getAttribute('src')
})
}
],
toDOM: node => ['iframe', node.attrs],
}
}
get view() {
// This is rendered as a Vue component and so it has
// the same interface as a renderless component.
return {
// Give it a name to work with Vue dev tools
name: 'YoutubeEmbed',
// Accept these props from... something? tiptap?
props: ['node', 'updateAttrs', 'view'],
computed: {
src: {
get() {
return this.node.attrs.src
},
// Can't mutate src directly so tiptap
// provides the updateAttrs function
set(src) {
this.updateAttrs({
src,
})
},
},
},
// Render Function
// Because the version of vue doesn't ship with
// the template compiler
render: function(createElement) {
// If the view is editable show an input field
if (this.view.editable) {
// console.log(this.view)
// console.log(this.node)
// console.log(`Computed src: ${this.src}`)
// Wrap it all in a div
return createElement('div', {
class: 'text-center'
},
[
// https://vuejs.org/v2/guide/render-function.html#Complete-Example
createElement('iframe', {
class: 'mx-auto',
attrs: {
...this.node.attrs
}
}),
createElement('label', {
class: 'block'
},
[
'Video Embed URL',
createElement('input', {
class: 'ml-2 w-auto',
domProps: {
value: this.src
},
on: {
input: event => {
this.src = event.target.value
// console.log(event.target.value)
},
paste: event => {
event.stopPropagation()
this.src = event.target.value
}
}
})
])
])
} else {
// Wrap it all in a div
return createElement('div', [
// https://vuejs.org/v2/guide/render-function.html#Complete-Example
createElement('iframe', {
attrs: {
...this.node.attrs
}
}),
])
}
}
}
}
// type in the commands function is destructured from
//
commands({ type }) {
return (attrs) => (state, dispatch) => {
const { selection } = state
const position = selection.$cursor
? selection.$cursor.pos
: selection.$to.pos
const node = type.create(attrs)
const transaction = state.tr.insert(position, node)
dispatch(transaction)
}
}
}
Closing this here. We started to work on the documentation for tiptap 2. 🙃
Hey there,
I recently discovered tiptap and have enjoyed using it in my projects. When it comes to the basics, it's relatively self explanatory. However, I've recently had a project that is requiring video embeds (specifically youtube).
This isn't that big of a deal since tiptap supports it, but for the life of me I cannot figure out HOW to set it up because the examples aren't really examples since no context is given on how they should be used. The code is just given to you and you are expected to understand it.
I was hoping to get some further clarification on how this works since I've tried the way the code suggests and no luck. Or maybe it did work and I'm just not doing something correctly because no explanation is given. I'm not entirely sure really.
It should also be said that the docs don't really explain if you can/how you can add a button for video embeds.
Hopefully someone can explain this to me and hopefully this can also be marked as a request for some slightly better docs. The code and layout of it itself is good, but the examples just need some added context about how they can be used and such I think?
Thanks.