Skip to content

Instantly share code, notes, and snippets.

@bettysteger
Last active November 30, 2024 08:35
Show Gist options
  • Save bettysteger/d7f2b1a52bb1c23a0c24f3a9ff5832d9 to your computer and use it in GitHub Desktop.
Save bettysteger/d7f2b1a52bb1c23a0c24f3a9ff5832d9 to your computer and use it in GitHub Desktop.
Vue 3 SFC for Editor.js using v-model with own custom vue component
<template>
<div class="editorjs" ref="htmlelement"></div>
</template>
<script setup>
import EditorJS from '@editorjs/editorjs';
import EmbedTool from '@editorjs/embed';
import ListTool from '@editorjs/list';
import ImageTool from '@editorjs/image';
import VideoTool from './editorjs/video.js';
import { onMounted, onUnmounted, ref, watch } from 'vue';
const htmlelement = ref(null);
const props = defineProps(['modelValue', 'placeholder'])
const emit = defineEmits(['update:modelValue'])
let editor;
let updatingModel = false;
// model -> view
function modelToView() {
if (!props.modelValue) { return; }
if (typeof props.modelValue === 'string') {
editor.blocks.renderFromHTML(props.modelValue);
return;
}
editor.render(props.modelValue);
}
// view -> model
function viewToModel(api, event) {
updatingModel = true;
editor.save().then((outputData) => {
console.log(event, 'Saving completed: ', outputData)
emit('update:modelValue', outputData);
}).catch((error) => {
console.log(event, 'Saving failed: ', error)
}).finally(() => {
updatingModel = false;
})
}
onMounted(() => {
editor = new EditorJS({
holder: htmlelement.value,
placeholder: props.placeholder,
inlineToolbar: ['bold', 'italic', 'link'],
tools: {
embed: EmbedTool,
list: ListTool,
image: ImageTool,
video: VideoTool,
},
minHeight: 'auto',
data: props.modelValue,
onReady: modelToView,
onChange: viewToModel,
})
})
watch(() => props.modelValue, () => {
if (!updatingModel) {
modelToView()
}
})
onUnmounted(() => {
editor.destroy()
})
</script>
// Video Block Tool, based on Image Tool
import ImageTool from '@editorjs/image';
// renders custom Videojs Vue component
import { render, h } from 'vue'
import Videojs from '../Videojs.vue'
const IconVideo = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 489.2 489.2"><path d="M439.6,0h-390C22.2,0,0,22.2,0,49.6v390c0,27.4,22.2,49.6,49.6,49.6h390c27.4,0,49.6-22.2,49.6-49.6V49.7 C489.3,22.3,467,0,439.6,0z M300.6,47.8h42.5v42.5h-42.5V47.8z M223.4,47.8h42.5v42.5h-42.5V47.8L223.4,47.8z M146.1,47.8h42.5 v42.5h-42.5V47.8z M111.3,441.6H68.8v-42.5h42.5V441.6z M111.3,90.3H68.8V47.8h42.5V90.3z M188.6,441.6h-42.5v-42.5h42.5V441.6z M265.8,441.6h-42.5v-42.5h42.5V441.6z M343.1,441.6h-42.5v-42.5h42.5V441.6z M352.5,256.7l-163.1,94.2c-9.2,5.3-20.8-1.3-20.8-12 V150.5c0-10.7,11.6-17.3,20.8-12l163.1,94.2C361.8,238,361.8,251.4,352.5,256.7z M420.4,441.6h-42.5v-42.5h42.5V441.6z M420.4,90.3 h-42.5V47.8h42.5V90.3z"/></svg>'
export default class Video extends ImageTool {
static get toolbox() {
return { title: 'Video', icon: IconVideo };
}
/**
* @param {object} tool - tool properties got from editor.js
* @param {ImageToolData} tool.data - previously saved data
* @param {ImageConfig} tool.config - user config for Tool
* @param {object} tool.api - Editor.js API
* @param {boolean} tool.readOnly - read-only mode flag
* @param {BlockAPI|{}} tool.block - current Block API
*/
constructor({ data, config, api, readOnly, block }) {
config.buttonContent = config.buttonContent || `${IconVideo} ${api.i18n.t('Select a Video')}`;
config.types = config.types || 'video/*';
super({ data, config, api, readOnly, block });
}
// original render() method from image tool is used
// render() {
// }
/**
* Set new video file (override default method)
*
* @private
*
* @param {object} file - uploaded file data
*/
set image(file) {
this._data.file = file || {};
if (file && file.url) {
// this.ui.fillImage(file.url);
this.fillVideo(file.url);
}
}
/**
* Shows a video
*
* @param {string} url - video source
* @returns {void}
*/
fillVideo(url) {
const vueComponent = h(Videojs, {
src: url,
class: this.ui.CSS.imageEl,
'data-mutation-free': 'true',
onLoadeddata: () => {
this.ui.toggleStatus('filled');
if (this.ui.nodes.imagePreloader) {
this.ui.nodes.imagePreloader.style.backgroundImage = '';
}
}
});
render(vueComponent, this.ui.nodes.imageContainer);
}
static get pasteConfig() {
return {
tags: [{
videojs: { src: true },
video: { src: true }
}],
patterns: {
video: /https?:\/\/\S+\.(mov|ogv|webm|mp4|m4v|m3u8)(\?[a-z0-9=]*)?$/i,
},
files: {
mimeTypes: [ 'video/*' ],
},
};
}
async onPaste(event) {
switch (event.type) {
case 'tag': {
const video = event.detail.data;
// need getAttribute for custom HTML element videojs
this.uploadUrl(video.getAttribute('src'));
break;
}
case 'pattern': {
const url = event.detail.data;
this.uploadUrl(url);
break;
}
case 'file': {
const file = event.detail.file;
this.uploadFile(file);
break;
}
}
}
destroy() {
render(null, this.ui.nodes.imageContainer);
}
}
<template>
<video class="video-js" controls playsinline preload="auto" crossorigin="anonymous" ref="videoEl">
<source :src="currentSrc">
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a>
</p>
</video>
</template>
<script setup>
/**
* VideoJs component
* @see https://github.com/videojs/video.js
*/
import videojs from 'video.js'
import { ref, onMounted, onUnmounted } from 'vue'
// declare a ref to hold the element reference
// the name must match template ref value
const videoEl = ref(null)
let player;
const props = defineProps({
src: {
type: String,
required: true
},
poster: String,
subtitleUrls: Object,
mandatory: Boolean,
})
const emit = defineEmits(['loadeddata', 'end'])
const currentSrc = ref(props.src)
/**
* Manually setup video.js player
*/
onMounted(() => {
const options = {
fluid: true,
poster: props.poster,
playbackRates: [0.5, 0.75, 1, 1.25, 1.5]
};
player = videojs(videoEl.value, options);
player.on('loadeddata', (e) => emit('loadeddata', e));
player.on('ended', function () {
emit('end', props.src);
if(player.isFullscreen()) {
player.exitFullscreen();
}
});
})
/**
* Destroys the video player and does any necessary cleanup.
* @see https://docs.videojs.com/player
*/
onUnmounted(() => {
if (player) {
player.dispose();
}
})
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment