Compare commits

...

9 Commits

Author SHA1 Message Date
d3d0e7c44d Upgrade to Vue 3 2023-09-24 11:57:57 +02:00
d9b5af7388 Improve warning and embed block 2021-01-06 17:26:46 +01:00
cefdf76045 Improve attaches block 2020-11-09 17:04:46 +01:00
92933de4bd Adjustments to attaches block 2020-11-09 10:18:20 +01:00
5dd1b7c0ef Optimize for server side rendering 2020-11-02 13:11:22 +01:00
945bef0523 Update for lazy loading images 2020-10-20 18:42:43 +02:00
fcf12f7dcf Add anchor ids to headers 2020-10-11 12:22:14 +02:00
91b7922464 Add math block 2020-09-30 22:06:01 +02:00
540333d431 Improve and fix button block 2020-09-28 22:06:47 +02:00
31 changed files with 2702 additions and 17762 deletions

View File

@@ -1,14 +1,44 @@
module.exports = { module.exports = {
root: true, root: true,
env: { env: {
node: true node: true,
}, },
extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"],
parserOptions: { parserOptions: {
parser: "babel-eslint" parser: 'babel-eslint',
}, },
rules: { rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off", semi: [
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" 'error',
} 'always',
],
quotes: [
2,
'single',
],
'comma-dangle': [
'error',
{
'arrays': 'always-multiline',
'objects': 'always-multiline',
'imports': 'always-multiline',
'exports': 'always-multiline',
'functions': 'never',
},
],
'space-before-function-paren': [
'error',
'never',
],
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'max-len': ['warn', {
'code': 100,
'ignoreStrings': true,
}],
},
extends: [
'plugin:vue/recommended',
'eslint:recommended',
],
}; };

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
.DS_Store .DS_Store
node_modules node_modules
dist
# local env files # local env files
.env.local .env.local

View File

@@ -1,3 +1,3 @@
module.exports = { module.exports = {
presets: ["@vue/cli-plugin-babel/preset"] presets: ['@vue/cli-plugin-babel/preset'],
}; };

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

10
dist/demo.html vendored
View File

@@ -1,10 +0,0 @@
<meta charset="utf-8">
<title>block-renderer demo</title>
<script src="./block-renderer.umd.js"></script>
<link rel="stylesheet" href="./block-renderer.css">
<script>
console.log(block-renderer)
</script>

13748
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +1,46 @@
{ {
"name": "block-renderer", "name": "block-renderer",
"description": "Content renderer for JSON blocks from Editor.js", "description": "Content renderer for JSON blocks from Editor.js",
"version": "0.1.14", "version": "0.7.7",
"author": "KingOfDog <info@kingofdog.de>", "author": "KingOfDog <info@kingofdog.de>",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.kingofdog.de/KingOfDog/block-renderer" "url": "https://git.kingofdog.de/KingOfDog/block-renderer"
}, },
"homepage": "https://git.kingofdog.de/KingOfDog/block-renderer", "homepage": "https://git.kingofdog.de/KingOfDog/block-renderer",
"scripts": { "type": "module",
"serve": "vue-cli-service serve", "files": [
"build": "vue-cli-service build", "dist"
"build-package": "vue-cli-service build --target lib --name block-renderer ./src/main.js", ],
"lint": "vue-cli-service lint" "main": "./dist/block-renderer.umd.js",
"module": "./dist/block-renderer.es.js",
"exports": {
".": {
"import": "./dist/block-renderer.es.js",
"require": "./dist/block-renderer.umd.js"
},
"./dist/block-renderer.css": {
"import": "./dist/block-renderer.css",
"require": "./dist/block-renderer.css"
}
},
"types": "./dist/main.d.ts",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
}, },
"main": "./dist/block-renderer.common.js",
"dependencies": { "dependencies": {
"core-js": "^3.6.5", "katex": "^0.16.8",
"vue": "^2.6.11" "vue": "^3.3.4"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0", "@vitejs/plugin-vue": "^4.0.0",
"@vue/cli-plugin-eslint": "~4.5.0", "path": "^0.12.7",
"@vue/cli-service": "~4.5.0", "rollup-plugin-typescript2": "^0.34.1",
"@vue/eslint-config-prettier": "^6.0.0", "typescript": "^4.9.3",
"babel-eslint": "^10.1.0", "vite": "^4.0.0",
"eslint": "^6.7.2", "vite-plugin-dts": "^1.7.1",
"eslint-plugin-prettier": "^3.1.3", "vue-tsc": "^1.0.11"
"eslint-plugin-vue": "^6.2.2",
"prettier": "^1.19.1",
"vue-template-compiler": "^2.6.11"
} }
} }

View File

@@ -1,83 +1,84 @@
<template> <template>
<div class="attaches-block"> <div class="attaches-block">
<div <a :href="url" target="blank">
:data-extension="data.file.extension" <div :data-extension="data.file.extension" class="attaches-block-file-icon" :style="{ color: color }">
:style="{ color: color }" <svg width="32" height="40">
class="attaches-block-file-icon" <path v-if="color"
> d="M17 0l15 14V3v34a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h20-6zm0 2H3a1 1 0 0 0-1 1v34a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1V14H17V2zm2 10h7.926L19 4.602V12z" />
<svg width="32" height="40"> <g v-else fill="#A8ACB8" fill-rule="evenodd">
<path <path fill-rule="nonzero"
v-if="color" d="M17 0l15 14V3v34a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h20-6zm0 2H3a1 1 0 0 0-1 1v34a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1V14H17V2zm2 10h7.926L19 4.602V12z" />
d="M17 0l15 14V3v34a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h20-6zm0 2H3a1 1 0 0 0-1 1v34a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1V14H17V2zm2 10h7.926L19 4.602V12z" <path d="M7 22h18v2H7zm0 4h18v2H7zm0 4h18v2H7z" />
/> </g>
<g v-else fill="#A8ACB8" fill-rule="evenodd"> </svg>
<path </div>
fill-rule="nonzero" </a>
d="M17 0l15 14V3v34a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h20-6zm0 2H3a1 1 0 0 0-1 1v34a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1V14H17V2zm2 10h7.926L19 4.602V12z"
/>
<path d="M7 22h18v2H7zm0 4h18v2H7zm0 4h18v2H7z" />
</g>
</svg>
</div>
<div class="attaches-block-file-info"> <div class="attaches-block-file-info">
<p class="attaches-block-title">{{ data.file.name }}</p> <a :href="url" target="blank">
<p class="attaches-block-size">{{ data.file.size | byteFormatter }}</p> <p class="attaches-block-title">{{ data.title }}</p>
</a>
<p class="attaches-block-size">
{{ fileSize }}
</p>
</div> </div>
<a :href="data.file.url" target="_blank" class="attaches-block-file-download-button"> <a :href="url" target="_blank" class="attaches-block-file-download-button">
<svg xmlns="http://www.w3.org/2000/svg" width="17pt" height="17pt" viewBox="0 0 17 17"> <svg xmlns="http://www.w3.org/2000/svg" width="17pt" height="17pt" viewBox="0 0 17 17">
<path <path
d="M9.457 8.945V2.848A.959.959 0 0 0 8.5 1.89a.959.959 0 0 0-.957.957v6.097L4.488 5.891a.952.952 0 0 0-1.351 0 .952.952 0 0 0 0 1.351l4.687 4.688a.955.955 0 0 0 1.352 0l4.687-4.688a.952.952 0 0 0 0-1.351.952.952 0 0 0-1.351 0zM3.59 14.937h9.82a.953.953 0 0 0 .953-.957.952.952 0 0 0-.953-.953H3.59a.952.952 0 0 0-.953.953c0 .532.425.957.953.957zm0 0" d="M9.457 8.945V2.848A.959.959 0 0 0 8.5 1.89a.959.959 0 0 0-.957.957v6.097L4.488 5.891a.952.952 0 0 0-1.351 0 .952.952 0 0 0 0 1.351l4.687 4.688a.955.955 0 0 0 1.352 0l4.687-4.688a.952.952 0 0 0 0-1.351.952.952 0 0 0-1.351 0zM3.59 14.937h9.82a.953.953 0 0 0 .953-.957.952.952 0 0 0-.953-.953H3.59a.952.952 0 0 0-.953.953c0 .532.425.957.953.957zm0 0"
fill-rule="evenodd" fill-rule="evenodd" />
/>
</svg> </svg>
</a> </a>
</div> </div>
</template> </template>
<script> <script>
import byteFormatter from "../filters/byteFormatter"; import byteFormatter from '../filters/byteFormatter';
const extensions = { const extensions = {
doc: "#3e74da", doc: '#3e74da',
docx: "#3e74da", docx: '#3e74da',
odt: "#3e74da", odt: '#3e74da',
pdf: "#d47373", pdf: '#d47373',
rtf: "#656ecd", rtf: '#656ecd',
tex: "#5a5a5b", tex: '#5a5a5b',
txt: "#5a5a5b", txt: '#5a5a5b',
pptx: "#e07066", pptx: '#e07066',
ppt: "#e07066", ppt: '#e07066',
mp3: "#eab456", mp3: '#eab456',
mp4: "#f676a6", mp4: '#f676a6',
xls: "#3f9e64", xls: '#3f9e64',
html: "#2988f0", html: '#2988f0',
htm: "#2988f0", htm: '#2988f0',
png: "#f676a6", png: '#f676a6',
jpg: "#f67676", jpg: '#f67676',
jpeg: "#f67676", jpeg: '#f67676',
gif: "#f6af76", gif: '#f6af76',
zip: "#4f566f", zip: '#4f566f',
rar: "#4f566f", rar: '#4f566f',
exe: "#e26f6f", exe: '#e26f6f',
svg: "#bf5252", svg: '#bf5252',
key: "#e07066", key: '#e07066',
sketch: "#df821c", sketch: '#df821c',
ai: "#df821c", ai: '#df821c',
psd: "#388ae5", psd: '#388ae5',
dmg: "#e26f6f", dmg: '#e26f6f',
json: "#2988f0", json: '#2988f0',
csv: "#3f9e64" csv: '#3f9e64',
}; };
export default { export default {
filters: { props: ['data'],
byteFormatter inject: ['$baseFileUrl'],
},
props: ["data"],
computed: { computed: {
color() { color() {
return extensions[this.data.file.extension]; return extensions[this.data.file.extension.toLowerCase()];
},
url() {
return `${this.$baseFileUrl}/${this.data.file.id}?name=${this.data.file.name}`;
},
fileSize() {
return byteFormatter(this.data.file.size)
} }
} },
}; };
</script> </script>

View File

@@ -1,67 +1,50 @@
<template> <template>
<div> <div>
<component <component :is="block.component" v-for="(block, index) in blocks" :key="index" :data="block.data" />
v-for="(block, index) in blocks"
:key="index"
:is="block.component"
:data="block.data"
/>
</div> </div>
</template> </template>
<script> <script>
import AttachesBlock from "./AttachesBlock"; import AttachesBlock from './AttachesBlock.vue';
import EmbedBlock from "./EmbedBlock"; import ButtonBlock from './ButtonBlock.vue';
import HeaderBlock from "./HeaderBlock"; import EmbedBlock from './EmbedBlock.vue';
import ImageBlock from "./ImageBlock"; import HeaderBlock from './HeaderBlock.vue';
import LinkBlock from "./LinkBlock"; import ImageBlock from './ImageBlock.vue';
import ListBlock from "./ListBlock"; import LinkBlock from './LinkBlock.vue';
import ParagraphBlock from "./ParagraphBlock"; import ListBlock from './ListBlock.vue';
import QuoteBlock from "./QuoteBlock"; import MathBlock from './MathBlock.vue';
import WarningBlock from "./WarningBlock"; import ParagraphBlock from './ParagraphBlock.vue';
import QuoteBlock from './QuoteBlock.vue';
import WarningBlock from './WarningBlock.vue';
const componentTypes = { const componentTypes = {
attaches: AttachesBlock, attaches: AttachesBlock,
button: ButtonBlock,
embed: EmbedBlock, embed: EmbedBlock,
header: HeaderBlock, header: HeaderBlock,
image: ImageBlock, image: ImageBlock,
link: LinkBlock, linkEmbed: LinkBlock,
list: ListBlock, list: ListBlock,
math: MathBlock,
paragraph: ParagraphBlock, paragraph: ParagraphBlock,
quote: QuoteBlock, quote: QuoteBlock,
warning: WarningBlock warning: WarningBlock,
}; };
export default { export default {
props: ["content"], props: ['content'],
data() { computed: {
return { blocks() {
blocks: []
};
},
mounted() {
if (this.content) {
this.prepareBlocks();
}
},
methods: {
prepareBlocks() {
if (!this.content.blocks) { if (!this.content.blocks) {
return; return [];
} }
this.blocks = this.content.blocks.map(block => ({
return this.content.blocks.map(block => ({
component: componentTypes[block.type], component: componentTypes[block.type],
type: block.type, type: block.type,
data: block.data data: block.data,
})); }));
} },
}, },
watch: {
content() {
if (this.content) {
this.prepareBlocks();
}
}
}
}; };
</script> </script>

View File

@@ -1,14 +1,15 @@
<template>
<div class="button-block">
<a
:href="data.url"
target="_blank"
v-html="data.label"
/>
</div>
</template>
<script> <script>
export default { export default {
props: ["data"], props: ['data'],
render(createElement) {
return createElement("a", {
domProps: {
innerHTML: this.data.label,
href: this.data.url
},
class: ["button", "button-primary"]
});
}
}; };
</script> </script>

View File

@@ -1,19 +1,54 @@
<template> <template>
<figure> <figure class="embed-block">
<iframe <div class="embed-container">
:src="data.embed" <iframe
:height="data.height" v-if="accepted"
frameborder="0" :src="data.embed"
allowfullscreen :height="data.height"
class="w-full" frameborder="0"
></iframe> allowfullscreen
/>
<div
v-else
class="embed-accept"
>
<h2>Externer Inhalt</h2>
<h3>
Originaler Link: <a
:href="data.source"
target="_blank"
>{{ displayUrl }}</a>
</h3>
<p>
An dieser Stelle ist externer Inhalt eingebunden. Um den Datenschutz
zu gewährleisten, ist er derzeit ausgeblendet. Deine Privatsphäre kann
auf externen Seiten nicht sichergestellt werden. Möchtest du den
Inhalt dennoch anzeigen?
</p>
<button @click="accepted = true">
Anzeigen
</button>
</div>
</div>
<figcaption v-html="data.caption"></figcaption> <figcaption v-html="data.caption" />
</figure> </figure>
</template> </template>
<script> <script>
export default { export default {
props: ["data"] props: ['data'],
data() {
return { accepted: false };
},
computed: {
displayUrl() {
if (!this.data) {
return '';
}
const url = new URL(this.data.source);
return url.hostname;
},
},
}; };
</script> </script>

View File

@@ -1,12 +1,11 @@
<template>
<component :is="`h${data.level}`" :id="data.anchor">
<span v-html="data.text" />
</component>
</template>
<script> <script>
export default { export default {
props: ["data"], props: ['data'],
render(createElement) {
return createElement("h" + this.data.level, {
domProps: { innerHTML: this.data.text }
});
}
}; };
</script> </script>
<style></style>

View File

@@ -1,13 +1,27 @@
<template> <template>
<figure> <figure>
<img :src="data.file.url" /> <img v-lazyload :src="lowQualityUrl" :data-src="highQualityUrl">
<figcaption v-html="data.caption"></figcaption> <figcaption v-html="data.caption" />
</figure> </figure>
</template> </template>
<script> <script>
import LazyLoadImage from '../directives/lazy-load-image';
export default { export default {
props: ["data"] directives: {
lazyload: LazyLoadImage,
},
props: ['data'],
inject: ['$baseFileUrl'],
computed: {
lowQualityUrl() {
return `${this.$baseFileUrl}/${this.data.file.id}?type=jpg&w=100&b=50`;
},
highQualityUrl() {
return `${this.$baseFileUrl}/${this.data.file.id}?w=1200`;
},
},
}; };
</script> </script>

View File

@@ -1,24 +1,31 @@
<template> <template>
<div class="link-block"> <div class="link-block">
<a :href="data.link" target="_blank" class="link-block-href"> <a
:href="data.link"
target="_blank"
class="link-block-href"
>
<div class="link-block-meta"> <div class="link-block-meta">
<div class="link-block-title">{{ data.meta.title }}</div> <div class="link-block-title">{{ data.meta.title }}</div>
<p class="link-block-description">{{ data.meta.description }}</p> <p class="link-block-description">{{ data.meta.description }}</p>
<div class="link-block-anchor">{{ domain }}</div> <div class="link-block-anchor">{{ domain }}</div>
</div> </div>
<div class="link-block-image" :style="{ backgroundImage: `url(${data.meta.image.url}` }"></div> <div
class="link-block-image"
:style="{ backgroundImage: `url(${data.meta.image.url}` }"
/>
</a> </a>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ["data"], props: ['data'],
computed: { computed: {
domain() { domain() {
return this.data.link.replace(/(https|http):\/\//, "").split(/[/?#]/)[0]; return this.data.link.replace(/(https|http):\/\//, '').split(/[/?#]/)[0];
} },
} },
}; };
</script> </script>

View File

@@ -1,19 +1,12 @@
<template>
<component :is="data.style === 'ordered' ? 'ol' : 'ul'">
<li v-for="item in data.items" :key="item" v-html="item"></li>
</component>
</template>
<script> <script>
export default { export default {
props: ["data"], props: ['data'],
render(createElement) {
return createElement(
this.data.style === "ordered" ? "ol" : "ul",
this.data.items.map(item =>
createElement("li", {
domProps: {
innerHTML: item
}
})
)
);
}
}; };
</script> </script>
<style></style>

View File

@@ -0,0 +1,23 @@
<template>
<figure>
<div v-html="content" />
</figure>
</template>
<script>
import katex from 'katex';
export default {
props: ['data'],
data() {
return {
content: '',
};
},
mounted() {
this.content = katex.renderToString(this.data.text, {
throwOnError: false,
});
},
};
</script>

View File

@@ -1,10 +1,10 @@
<template> <template>
<p v-html="data.text"></p> <p v-html="data.text" />
</template> </template>
<script> <script>
export default { export default {
props: ["data"], props: ['data'],
}; };
</script> </script>

View File

@@ -1,14 +1,14 @@
<template> <template>
<blockquote> <blockquote>
<p v-html="data.text"></p> <p v-html="data.text" />
<footer v-html="data.caption"></footer> <footer v-html="data.caption" />
</blockquote> </blockquote>
</template> </template>
<script> <script>
export default { export default {
props: ["data"], props: ['data'],
}; };
</script> </script>

View File

@@ -1,15 +1,17 @@
<template> <template>
<div class="warning-block"> <div class="warning-block">
<p class="font-semibold mb-4 text-white">{{data.title}}</p> <b v-html="title" />
<p v-html="data.message" class="text-white"></p> <p v-html="data.message" />
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ["data"], props: ['data'],
computed: {
title() {
return this.data.title ? this.data.title.replace(/<br>/g, '') : '';
},
},
}; };
</script> </script>
<style>
</style>

View File

@@ -0,0 +1,38 @@
export default {
mounted: (el) => {
function loadImage() {
const image = new Image();
image.src = el.dataset.src;
image.onload = () => {
el.src = el.dataset.src;
};
}
function handleIntersect(entries, observer) {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
loadImage();
observer.unobserve(el);
});
}
function createObserver() {
const options = {
root: null,
threshold: "0",
};
const observer = new IntersectionObserver(handleIntersect, options);
observer.observe(el);
}
if (!window.IntersectionObserver) {
loadImage();
} else {
createObserver();
}
},
};

View File

@@ -1,4 +1,4 @@
module.exports = function(size) { export default function (size) {
var result; var result;
switch (true) { switch (true) {
@@ -27,4 +27,4 @@ module.exports = function(size) {
} }
return result; return result;
}; }

View File

@@ -1,6 +0,0 @@
import BlockContentRenderer from './components/BlockContentRenderer.vue';
export default BlockContentRenderer;
export {
BlockContentRenderer
};

13
src/main.ts Normal file
View File

@@ -0,0 +1,13 @@
import type { App } from 'vue';
import BlockContentRenderer from './components/BlockContentRenderer.vue';
export default {
install: (app: App, options: {
baseFileUrl?: string
}) => {
app.provide('$baseFileUrl', options.baseFileUrl);
app.component('BlockContentRenderer', BlockContentRenderer);
},
};
export { BlockContentRenderer };

5
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

38
tsconfig.json Normal file
View File

@@ -0,0 +1,38 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": [
"ESNext",
"DOM"
],
"skipLibCheck": true,
"noEmit": true,
"allowJs": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"declaration": true,
"outDir": "dist",
"declarationDir": "dist",
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
}

60
vite.config.ts Normal file
View File

@@ -0,0 +1,60 @@
import * as path from 'path'
import vue from '@vitejs/plugin-vue'
import typescript2 from 'rollup-plugin-typescript2';
import { defineConfig } from 'vite';
import dts from "vite-plugin-dts";
export default defineConfig({
plugins: [
vue(),
dts({
insertTypesEntry: true,
}),
typescript2({
check: false,
include: ["src/components/**/*.vue"],
tsconfigOverride: {
compilerOptions: {
outDir: "dist",
sourceMap: true,
declaration: true,
declarationMap: true,
},
},
exclude: ["vite.config.ts"]
})
],
build: {
cssCodeSplit: true,
lib: {
// Could also be a dictionary or array of multiple entry points
entry: "src/main.ts",
name: 'blockRenderer',
formats: ["es", "cjs", "umd"],
fileName: format => `block-renderer.${format}.js`
},
rollupOptions: {
// make sure to externalize deps that should not be bundled
// into your library
input: {
main: path.resolve(__dirname, "src/main.ts")
},
external: ['vue'],
output: {
assetFileNames: (assetInfo) => {
if (assetInfo.name === 'main.css') return 'block-renderer.css';
return assetInfo.name;
},
exports: "named",
globals: {
vue: 'Vue',
},
},
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
})