add basic vite setup

This commit is contained in:
Philipp Kühn 2021-08-25 11:52:20 +02:00
parent 96a7310b9d
commit 15c7e1955a
28 changed files with 2452 additions and 27 deletions

12
demos/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Index</title>
</head>
<body>
<div id="app"></div>
<a href="/preview/">Preview</a>
</body>
</html>

50
demos/package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "tiptap-demos",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "vite",
"build": "vite build",
"_build": "vue-tsc --noEmit && vite build",
"serve": "vite preview --port 3000"
},
"dependencies": {
"@tiptap/core": "^2.0.0-beta.101",
"@tiptap/starter-kit": "^2.0.0-beta.99",
"@tiptap/vue-3": "^2.0.0-beta.52",
"@types/prosemirror-commands": "^1.0.4",
"@types/prosemirror-inputrules": "^1.0.4",
"@types/prosemirror-keymap": "^1.0.4",
"@types/prosemirror-model": "^1.13.1",
"@types/prosemirror-schema-list": "^1.0.3",
"@types/prosemirror-state": "^1.2.7",
"@types/prosemirror-transform": "^1.1.4",
"@types/prosemirror-view": "^1.17.2",
"@vitejs/plugin-react-refresh": "^1.3.6",
"autoprefixer": "^10.3.1",
"iframe-resizer": "^4.3.2",
"postcss": "^8.3.6",
"prosemirror-view": "^1.18.11",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.2.0",
"shiki": "^0.9.7",
"tailwindcss": "^2.2.7",
"uuid": "^8.3.2",
"vue": "^3.0.5",
"vue-router": "^4.0.11",
"y-prosemirror": "^1.0.9",
"y-webrtc": "^10.2.0",
"y-websocket": "^1.3.16",
"yjs": "^13.5.11"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.5.0",
"@vue/compiler-sfc": "^3.1.4",
"globby": "^11.0.4",
"sass": "^1.35.2",
"typescript": "^4.3.5",
"vite": "^2.5.1",
"vue-tsc": "^0.3.0"
}
}

6
demos/postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

241
demos/preview/Demo.vue Normal file
View File

@ -0,0 +1,241 @@
<template>
<demo-frame
v-if="inline"
:src="currentIframeUrl"
:key="currentIframeUrl"
/>
<div class="antialiased" v-else>
<div v-if="showTabs">
<button
v-for="(language, index) in sortedTabs"
:key="index"
@click="setTab(language.name)"
class="px-4 py-2 rounded-t-lg text-xs uppercase font-bold tracking-wide"
:class="[currentTab === language.name
? 'bg-black text-white'
: 'text-black'
]"
>
{{ language.name }}
</button>
</div>
<div class="overflow-hidden rounded-b-xl">
<div
class="border-2 border-black last:rounded-b-xl"
:class="[
showTabs && firstTabSelected
? 'rounded-tr-xl'
: 'rounded-t-xl',
]"
>
<demo-frame
:src="currentIframeUrl"
:key="currentIframeUrl"
/>
</div>
<div class="bg-black text-white" v-if="!hideSource && currentFile">
<div class="flex overflow-x-auto">
<div class="flex flex-auto px-4 border-b-2 border-gray-800">
<button
class="inline-flex relative mr-4 py-2 pb-[calc(0.3rem + 2px)] mb-[-2px] border-b-2 border-transparent font-mono text-sm"
:class="[!showDebug && currentFile.content === file.content
? 'text-white border-white font-bold'
: 'text-gray-400'
]"
v-for="(file, index) in source"
:key="index"
@click="setFile(file.name)"
>
{{ file.name }}
</button>
<button
v-if="debugJSON"
class="inline-flex relative py-2 pb-[calc(0.3rem + 2px)] mb-[-2px] border-b-2 border-transparent font-mono text-sm ml-auto"
:class="[showDebug
? 'text-white border-white font-bold'
: 'text-gray-400'
]"
@click="showDebug = !showDebug"
>
Positions
</button>
</div>
</div>
<div class="overflow-dark overflow-auto max-h-[500px] relative text-white">
<shiki
class="overflow-visible p-4"
:language="debugJSON && showDebug ? 'js' : getFileExtension(currentFile.name)"
:code="debugJSON && showDebug ? debugJSON : currentFile.content"
key="debug"
/>
</div>
<div class="flex justify-between px-4 py-2 text-md text-gray-400 border-t border-gray-800">
<a :href="currentIframeUrl">
{{ name }}/{{ currentTab }}
</a>
<a :href="githubUrl" target="_blank">
Edit on GitHub
</a>
</div>
</div>
</div>
</div>
</template>
<script>
import { getDebugJSON } from '@tiptap/core'
import DemoFrame from './DemoFrame.vue'
import Shiki from './Shiki.vue'
export default {
components: {
DemoFrame,
Shiki,
},
props: {
name: {
type: String,
required: true,
},
tabs: {
type: Object,
required: true,
},
},
data() {
return {
data: [],
sources: {},
currentTab: null,
currentFile: null,
tabOrder: ['Vue', 'React'],
debugJSON: null,
showDebug: false,
}
},
computed: {
showTabs() {
return this.sortedTabs.length > 1
},
currentIframeUrl() {
return `/src/${this.name}/${this.currentTab}/`
},
firstTabSelected() {
return this.sortedTabs[0].name === this.currentTab
},
sortedTabs() {
return [...this.tabs].sort((a, b) => {
return this.tabOrder.indexOf(a.name) - this.tabOrder.indexOf(b.name)
})
},
query() {
return Object.fromEntries(Object
.entries(this.$route.query)
.map(([key, value]) => [key, this.fromString(value)]))
},
inline() {
return this.query.inline || false
},
hideSource() {
return this.query.hideSource || false
},
githubUrl() {
return `https://github.com/ueberdosis/tiptap-pro-extensions/tree/main/demos/src/${this.name}`
},
source() {
return this.sources[this.currentTab]
},
},
methods: {
getFileExtension(name) {
return name.split('.').pop()
},
setTab(name) {
this.currentTab = name
this.sources = {}
this.currentFile = null
},
setFile(name) {
this.showDebug = false
this.currentFile = this.source.find(item => item.name === name)
},
onSource(event) {
this.sources[this.currentTab] = event.detail
this.setFile(this.source[0].name)
},
onEditor(event) {
const editor = event.detail
if (!editor) {
this.debugJSON = null
return
}
this.debugJSON = JSON.stringify(getDebugJSON(editor.state.doc), null, ' ')
editor.on('update', () => {
this.debugJSON = JSON.stringify(getDebugJSON(editor.state.doc), null, ' ')
})
},
fromString(value) {
if (typeof value !== 'string') {
return value
}
if (value.match(/^\d*(\.\d+)?$/)) {
return Number(value)
}
if (value === 'true') {
return true
}
if (value === 'false') {
return false
}
if (value === 'null') {
return null
}
return value
},
},
mounted() {
// TODO: load language from url params
this.setTab(this.sortedTabs[0]?.name)
window.document.addEventListener('editor', this.onEditor, false)
window.document.addEventListener('source', this.onSource, false)
},
beforeUnmount() {
window.document.removeEventListener('editor', this.onEditor)
window.document.removeEventListener('source', this.onSource)
},
}
</script>

View File

@ -0,0 +1,58 @@
<template>
<div class="flex flex-col relative min-h-[5rem]">
<div class="absolute top-0 left-0 w-full h-full flex justify-center items-center pointer-events-none" v-if="isLoading">
<svg
class="animate-spin -ml-1 mr-3 h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
</div>
<iframe
class="bg-transparent max-h-[400px]"
v-resize.quiet="{ scrolling: 'omit' }"
:src="src"
width="100%"
height="0"
frameborder="0"
@load="onLoad"
/>
</div>
</template>
<script>
export default {
props: {
src: {
required: true,
type: String,
},
},
data() {
return {
isLoading: true,
}
},
methods: {
onLoad() {
this.isLoading = false
},
},
}
</script>
<style>
</style>

148
demos/preview/Shiki.vue Normal file
View File

@ -0,0 +1,148 @@
<template>
<div v-if="html" v-html="html" />
<pre v-else><code>{{ code }}</code></pre>
</template>
<script>
import * as shiki from 'shiki'
import onigasm from 'shiki/dist/onigasm.wasm?url'
import theme from 'shiki/themes/material-darker.json'
import langHTML from 'shiki/languages/html.tmLanguage.json'
import langJS from 'shiki/languages/javascript.tmLanguage.json'
import langJSX from 'shiki/languages/jsx.tmLanguage.json'
import langTS from 'shiki/languages/typescript.tmLanguage.json'
import langTSX from 'shiki/languages/tsx.tmLanguage.json'
import langVueHTML from 'shiki/languages/vue-html.tmLanguage.json'
import langVue from 'shiki/languages/vue.tmLanguage.json'
import langCSS from 'shiki/languages/css.tmLanguage.json'
import langSCSS from 'shiki/languages/scss.tmLanguage.json'
export default {
props: {
code: {
default: '',
type: String,
},
language: {
default: 'js',
type: String,
},
},
data() {
return {
html: null,
highlighter: window?.highlighter,
}
},
watch: {
code: {
immediate: true,
handler() {
this.render()
},
},
highlighter: {
immediate: true,
handler() {
this.render()
},
},
},
methods: {
render() {
try {
requestAnimationFrame(() => {
this.html = this.highlighter?.codeToHtml(this.code, this.language)
})
} catch {
console.warn(`[shiki]: missing language: ${this.language}`)
}
},
async initHighlighter() {
if (window.highlighter) {
return
}
const arrayBuffer = await fetch(onigasm).then(response => response.arrayBuffer())
shiki.setOnigasmWASM(arrayBuffer)
const highlighter = await shiki.getHighlighter({
theme,
langs: [
{
id: 'html',
scopeName: langHTML.scopeName,
grammar: langHTML,
embeddedLangs: ['javascript', 'css'],
},
{
id: 'javascript',
scopeName: langJS.scopeName,
grammar: langJS,
aliases: ['js'],
},
{
id: 'jsx',
scopeName: langJSX.scopeName,
grammar: langJSX,
},
{
id: 'typescript',
scopeName: langTS.scopeName,
grammar: langTS,
aliases: ['ts'],
},
{
id: 'tsx',
scopeName: langTSX.scopeName,
grammar: langTSX,
},
{
id: 'vue-html',
scopeName: langVueHTML.scopeName,
grammar: langVueHTML,
embeddedLangs: ['vue', 'javascript'],
},
{
id: 'vue',
scopeName: langVue.scopeName,
grammar: langVue,
embeddedLangs: ['json', 'markdown', 'pug', 'haml', 'vue-html', 'sass', 'scss', 'less', 'stylus', 'postcss', 'css', 'typescript', 'coffee', 'javascript'],
},
{
id: 'css',
scopeName: langCSS.scopeName,
grammar: langCSS,
},
{
id: 'scss',
scopeName: langSCSS.scopeName,
grammar: langSCSS,
embeddedLangs: ['css'],
},
],
})
window.highlighter = highlighter
this.highlighter = highlighter
},
},
created() {
this.initHighlighter()
},
}
</script>
<style>
.shiki {
background-color: transparent !important;
}
</style>

12
demos/preview/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Preview</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/preview/index.js"></script>
</body>
</html>

57
demos/preview/index.js Normal file
View File

@ -0,0 +1,57 @@
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './index.vue'
import Demo from './Demo.vue'
import { demos } from '@demos'
import 'iframe-resizer/js/iframeResizer.contentWindow'
import iframeResize from 'iframe-resizer/js/iframeResizer'
import './style.css'
const routes = demos
.map(({ name, tabs }) => {
return {
path: `/${name}`,
component: Demo,
props: {
name,
tabs,
},
}
})
const router = createRouter({
history: createWebHistory('preview'),
routes,
})
createApp(App)
.directive('resize', {
beforeMount: (el, { value = {} }) => {
el.addEventListener('load', () => {
iframeResize({
...value,
// messageCallback(messageData) {
// if (messageData.message.type !== 'resize') {
// return
// }
// const style = window.getComputedStyle(el.parentElement)
// const maxHeight = parseInt(style.getPropertyValue('max-height'), 10)
// if (messageData.message.height > maxHeight) {
// el.setAttribute('scrolling', 'auto')
// } else {
// el.setAttribute('scrolling', 'no')
// }
// el?.iFrameResizer?.resize?.()
// },
}, el)
})
},
unmounted(el) {
el?.iFrameResizer?.removeListeners?.()
},
})
.use(router)
.mount('#app')

10
demos/preview/index.vue Normal file
View File

@ -0,0 +1,10 @@
<template>
<ul v-if="$route.path === '/'">
<li v-for="route in $router.options.routes" :key="route.path">
<router-link :to="route.path">
{{ route.path }}
</router-link>
</li>
</ul>
<router-view v-else />
</template>

138
demos/preview/style.css Normal file
View File

@ -0,0 +1,138 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("https://rsms.me/inter/font-files/Inter-Regular.woff2?v=3.19") format("woff2"),
url("https://rsms.me/inter/font-files/Inter-Regular.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url("https://rsms.me/inter/font-files/Inter-Italic.woff2?v=3.19") format("woff2"),
url("https://rsms.me/inter/font-files/Inter-Italic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("https://rsms.me/inter/font-files/Inter-Medium.woff2?v=3.19") format("woff2"),
url("https://rsms.me/inter/font-files/Inter-Medium.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url("https://rsms.me/inter/font-files/Inter-MediumItalic.woff2?v=3.19") format("woff2"),
url("https://rsms.me/inter/font-files/Inter-MediumItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("https://rsms.me/inter/font-files/Inter-SemiBold.woff2?v=3.19") format("woff2"),
url("https://rsms.me/inter/font-files/Inter-SemiBold.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url("https://rsms.me/inter/font-files/Inter-SemiBoldItalic.woff2?v=3.19") format("woff2"),
url("https://rsms.me/inter/font-files/Inter-SemiBoldItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("https://rsms.me/inter/font-files/Inter-Bold.woff2?v=3.19") format("woff2"),
url("https://rsms.me/inter/font-files/Inter-Bold.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url("https://rsms.me/inter/font-files/Inter-BoldItalic.woff2?v=3.19") format("woff2"),
url("https://rsms.me/inter/font-files/Inter-BoldItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src:
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff2") format("woff2"),
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff") format("woff"),
;
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 700;
font-display: swap;
src:
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold.woff2") format("woff2"),
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold.woff") format("woff"),
;
}
::-webkit-scrollbar {
width: 14px;
height: 14px;
}
::-webkit-scrollbar-track {
border: 4px solid transparent;
background-clip: padding-box;
border-radius: 8px;
background-color: transparent;
}
::-webkit-scrollbar-thumb {
border: 4px solid rgba(0, 0, 0, 0);
background-clip: padding-box;
border-radius: 8px;
background-color: rgba(0, 0, 0, 0);
}
:hover::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.15);
}
.overflow-dark:hover::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.2);
}
.overflow-dark::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.3);
}
::-webkit-scrollbar-button {
display: none;
width: 0;
height: 0;
}
::-webkit-scrollbar-corner {
background-color: transparent;
}

1
demos/public/_redirects Normal file
View File

@ -0,0 +1 @@
/preview/* /preview/index.html 200

38
demos/setup/helper.ts Normal file
View File

@ -0,0 +1,38 @@
const waitUntilElementExists = (selector: any, callback: (element: Element) => void) => {
const element = document.querySelector(selector)
if (element) {
return callback(element)
}
setTimeout(() => waitUntilElementExists(selector, callback), 500)
}
const sendData = (eventName: string, data: any) => {
const event = new CustomEvent(eventName, { detail: data })
window.parent.document.dispatchEvent(event)
}
export function splitName(name: string) {
const parts = name.split('/')
if (parts.length !== 2) {
throw Error('Demos must always be within exactly one category. Nested categories are not supported.')
}
return parts
}
export function debug() {
sendData('editor', null)
// @ts-ignore
sendData('source', window.source)
waitUntilElementExists('.ProseMirror', element => {
// @ts-ignore
const editor = element.editor
sendData('editor', editor)
})
}

19
demos/setup/react.tsx Normal file
View File

@ -0,0 +1,19 @@
import React from 'react'
import ReactDOM from 'react-dom'
import 'iframe-resizer/js/iframeResizer.contentWindow'
import { debug, splitName } from './helper'
import './style.scss'
export default function init(name: string, source: any) {
// @ts-ignore
window.source = source
document.title = name
const [demoCategory, demoName] = splitName(name)
import(`../src/${demoCategory}/${demoName}/React/index.jsx`)
.then(module => {
ReactDOM.render(<module.default />, document.getElementById('app'))
debug()
})
}

59
demos/setup/style.scss Normal file
View File

@ -0,0 +1,59 @@
$colorBlack: #000;
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 1rem;
}
::-webkit-scrollbar {
width: 14px;
height: 14px;
}
::-webkit-scrollbar-track {
border: 4px solid transparent;
background-clip: padding-box;
border-radius: 8px;
background-color: transparent;
}
::-webkit-scrollbar-thumb {
border: 4px solid rgba(0, 0, 0, 0);
background-clip: padding-box;
border-radius: 8px;
background-color: rgba(0, 0, 0, 0);
}
:hover::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.15);
}
::-webkit-scrollbar-button {
display: none;
width: 0;
height: 0;
}
::-webkit-scrollbar-corner {
background-color: transparent;
}
.ProseMirror:focus {
outline: none;
}

18
demos/setup/vue.ts Normal file
View File

@ -0,0 +1,18 @@
import { createApp } from 'vue'
import 'iframe-resizer/js/iframeResizer.contentWindow'
import { debug, splitName } from './helper'
import './style.scss'
export default function init(name: string, source: any) {
// @ts-ignore
window.source = source
document.title = name
const [demoCategory, demoName] = splitName(name)
import(`../src/${demoCategory}/${demoName}/Vue/index.vue`)
.then(module => {
createApp(module.default).mount('#app')
debug()
})
}

6
demos/shims-vue.d.ts vendored Normal file
View File

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

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/react.tsx'
import source from '@source'
setup('Examples/Default', source)
</script>
</body>
</html>

View File

@ -0,0 +1,168 @@
import React from 'react'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import './styles.scss'
const MenuBar = ({ editor }) => {
if (!editor) {
return null
}
return (
<>
<button
onClick={() => editor.chain().focus().toggleBold().run()}
className={editor.isActive('bold') ? 'is-active' : ''}
>
bold
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
className={editor.isActive('italic') ? 'is-active' : ''}
>
italic
</button>
<button
onClick={() => editor.chain().focus().toggleStrike().run()}
className={editor.isActive('strike') ? 'is-active' : ''}
>
strike
</button>
<button
onClick={() => editor.chain().focus().toggleCode().run()}
className={editor.isActive('code') ? 'is-active' : ''}
>
code
</button>
<button onClick={() => editor.chain().focus().unsetAllMarks().run()}>
clear marks
</button>
<button onClick={() => editor.chain().focus().clearNodes().run()}>
clear nodes
</button>
<button
onClick={() => editor.chain().focus().setParagraph().run()}
className={editor.isActive('paragraph') ? 'is-active' : ''}
>
paragraph
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
>
h1
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
>
h2
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''}
>
h3
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
className={editor.isActive('heading', { level: 4 }) ? 'is-active' : ''}
>
h4
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
className={editor.isActive('heading', { level: 5 }) ? 'is-active' : ''}
>
h5
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
className={editor.isActive('heading', { level: 6 }) ? 'is-active' : ''}
>
h6
</button>
<button
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={editor.isActive('bulletList') ? 'is-active' : ''}
>
bullet list
</button>
<button
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={editor.isActive('orderedList') ? 'is-active' : ''}
>
ordered list
</button>
<button
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={editor.isActive('codeBlock') ? 'is-active' : ''}
>
code block
</button>
<button
onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={editor.isActive('blockquote') ? 'is-active' : ''}
>
blockquote
</button>
<button onClick={() => editor.chain().focus().setHorizontalRule().run()}>
horizontal rule
</button>
<button onClick={() => editor.chain().focus().setHardBreak().run()}>
hard break
</button>
<button onClick={() => editor.chain().focus().undo().run()}>
undo
</button>
<button onClick={() => editor.chain().focus().redo().run()}>
redo
</button>
</>
)
}
export default () => {
const editor = useEditor({
extensions: [
StarterKit,
],
content: `
<h2>
Hi there,
</h2>
<p>
this is a <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
</p>
<ul>
<li>
Thats a bullet list with one
</li>
<li>
or two list items.
</li>
</ul>
<p>
Isnt that great? And all of that is editable. But wait, theres more. Lets try a code block:
</p>
<pre><code class="language-css">body {
display: none;
}</code></pre>
<p>
I know, I know, this is impressive. Its only the tip of the iceberg though. Give it a try and click a little bit around. Dont forget to check the other examples too.
</p>
<blockquote>
Wow, thats amazing. Good work, boy! 👏
<br />
Mom
</blockquote>
`,
})
return (
<div>
<MenuBar editor={editor} />
<EditorContent editor={editor} />
</div>
)
}

View File

@ -0,0 +1,22 @@
context('/demos/Examples/Default/React', () => {
before(() => {
cy.visit('/demos/Examples/Default/React')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<h1>Example Text</h1>')
cy.get('.ProseMirror').type('{selectall}')
})
})
it('should apply the paragraph style when the keyboard shortcut is pressed', () => {
cy.get('.ProseMirror h1').should('exist')
cy.get('.ProseMirror p').should('not.exist')
cy.get('.ProseMirror')
.trigger('keydown', { modKey: true, altKey: true, key: '0' })
.find('p')
.should('contain', 'Example Text')
})
})

View File

@ -0,0 +1,56 @@
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
img {
max-width: 100%;
height: auto;
}
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
hr {
border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1);
margin: 2rem 0;
}
}

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Examples/Default', source)
</script>
</body>
</html>

View File

@ -0,0 +1,22 @@
context('/demos/Examples/Default/Vue', () => {
before(() => {
cy.visit('/demos/Examples/Default/Vue')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<h1>Example Text</h1>')
cy.get('.ProseMirror').type('{selectall}')
})
})
it('should apply the paragraph style when the keyboard shortcut is pressed', () => {
cy.get('.ProseMirror h1').should('exist')
cy.get('.ProseMirror p').should('not.exist')
cy.get('.ProseMirror')
.trigger('keydown', { modKey: true, altKey: true, key: '0' })
.find('p')
.should('contain', 'Example Text')
})
})

View File

@ -0,0 +1,188 @@
<template>
<div>
<div v-if="editor">
<button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
bold
</button>
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
italic
</button>
<button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }">
strike
</button>
<button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }">
code
</button>
<button @click="editor.chain().focus().unsetAllMarks().run()">
clear marks
</button>
<button @click="editor.chain().focus().clearNodes().run()">
clear nodes
</button>
<button @click="editor.chain().focus().setParagraph().run()" :class="{ 'is-active': editor.isActive('paragraph') }">
paragraph
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }">
h1
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }">
h2
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }">
h3
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 4 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 4 }) }">
h4
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 5 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 5 }) }">
h5
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 6 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 6 }) }">
h6
</button>
<button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }">
bullet list
</button>
<button @click="editor.chain().focus().toggleOrderedList().run()" :class="{ 'is-active': editor.isActive('orderedList') }">
ordered list
</button>
<button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }">
code block
</button>
<button @click="editor.chain().focus().toggleBlockquote().run()" :class="{ 'is-active': editor.isActive('blockquote') }">
blockquote
</button>
<button @click="editor.chain().focus().setHorizontalRule().run()">
horizontal rule
</button>
<button @click="editor.chain().focus().setHardBreak().run()">
hard break
</button>
<button @click="editor.chain().focus().undo().run()">
undo
</button>
<button @click="editor.chain().focus().redo().run()">
redo
</button>
</div>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
StarterKit,
],
content: `
<h2>
Hi there,
</h2>
<p>
this is a <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
</p>
<ul>
<li>
Thats a bullet list with one
</li>
<li>
or two list items.
</li>
</ul>
<p>
Isnt that great? And all of that is editable. But wait, theres more. Lets try a code block:
</p>
<pre><code class="language-css">body {
display: none;
}</code></pre>
<p>
I know, I know, this is impressive. Its only the tip of the iceberg though. Give it a try and click a little bit around. Dont forget to check the other examples too.
</p>
<blockquote>
Wow, thats amazing. Good work, boy! 👏
<br />
Mom
</blockquote>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
img {
max-width: 100%;
height: auto;
}
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
hr {
border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1);
margin: 2rem 0;
}
}
</style>

106
demos/tailwind.config.js Normal file
View File

@ -0,0 +1,106 @@
const defaultTheme = require('tailwindcss/defaultTheme')
module.exports = {
mode: 'jit',
purge: [
'./preview/**/*.{vue,js,ts,jsx,tsx}',
],
theme: {
extend: {
colors: {
code: {
attrName: '#faf594',
attrValue: '##b9f18d',
doctype: '#616161',
keyword: '##958df1',
punctuation: '##70cff8',
string: '#b9f18d',
tag: '#f98181',
},
transparency: {
box: {
3: 'rgba(13, 13, 13, 0.03)',
5: 'rgba(13, 13, 13, 0.05)',
},
},
gray: {
DEFAULT: '',
900: '#0d0d0d',
800: '#262626',
700: '#3a3a3a',
600: '#4e4e4e',
500: '#616161',
400: '#737373',
300: '#919191',
200: '#b3b3b3',
100: '#d6d6d6',
50: '#e8e8e8',
},
accent: {
DEFAULT: '#faf594',
50: '#fffffa',
100: '#fffef4',
200: '#fefde4',
300: '#fdfbd4',
400: '#fcf8b4',
500: '#faf594',
600: '#e1dd85',
700: '#bcb86f',
800: '#969359',
900: '#7b7849',
},
success: {
DEFAULT: '#b9f18d',
50: '#fcfef9',
100: '#f8fef4',
200: '#eefce3',
300: '#e3f9d1',
400: '#cef5af',
500: '#b9f18d',
600: '#a7d97f',
700: '#8bb56a',
800: '#6f9155',
900: '#5b7645',
},
},
spacing: {},
borderRadius: {
xxs: '0.4rem',
box: '0.75rem',
},
fontSize: {
'2xl': '2.75rem',
xl: '1.5rem',
lg: '1.17rem',
sm: '0.85rem',
},
lineHeight: {
DEFAULT: '1.7rem',
'2xl': '3.16rem',
xl: '1.8rem',
lg: '1.6rem',
},
transition: {},
height: {
18: '4.5rem',
},
padding: {
18: '4.5rem',
},
zIndex: {},
boxShadow: {},
textShadow: {},
fontFamily: {
sans: ['Inter', ...defaultTheme.fontFamily.sans],
mono: ['JetBrains Mono', ...defaultTheme.fontFamily.mono],
},
},
},
variants: {
extend: {
opacity: ['disabled'],
},
},
}

7
demos/tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"types": ["vite/client"],
"compilerOptions": {
"baseUrl": "."
}
}

143
demos/vite.config.ts Normal file
View File

@ -0,0 +1,143 @@
// @ts-nocheck
import {
resolve,
basename,
dirname,
join,
} from 'path'
import { v4 as uuid } from 'uuid'
import fs from 'fs'
import globby from 'globby'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import reactRefresh from '@vitejs/plugin-react-refresh'
export default defineConfig({
build: {
rollupOptions: {
input: globby.sync('./**/index.html', {
ignore: ['dist'],
}),
},
},
plugins: [
vue(),
reactRefresh(),
{
name: 'raw',
resolveId(id, importer) {
if (id.startsWith('raw!')) {
const [, relativePath] = id.split('raw!')
const fullPath = join(dirname(importer), relativePath)
return `virtual!${fullPath}!!${uuid()}`
}
},
load(id) {
if (id.startsWith('virtual!')) {
const path = id.split('!!')[0].replace('virtual!', '')
const data = fs.readFileSync(path, 'utf8')
return `export default ${JSON.stringify(data)}`
}
},
},
{
name: 'demos',
resolveId(id) {
if (id === '@demos') {
return '@demos'
}
},
load(id) {
if (id === '@demos') {
const demos = globby
.sync('./src/*/*', { onlyDirectories: true })
.map(demoPath => {
const name = demoPath.replace('./src/', '')
const tabs = globby
.sync(`./src/${name}/*`, { onlyDirectories: true })
.map(tabPath => ({
name: basename(tabPath),
}))
return {
name,
tabs,
}
})
return `export const demos = ${JSON.stringify(demos)}`
}
},
},
{
name: 'source',
resolveId(id, importer) {
if (id === '@source') {
return `source!${dirname(importer)}!!${uuid()}`
}
},
load(id) {
if (id.startsWith('source!')) {
const path = id.split('!!')[0].replace('source!', '')
const files = globby
.sync(`${path}/**/*`, {
ignore: [
'**/index.html',
'**/*.spec.js',
'**/*.spec.ts',
],
})
.map(filePath => {
const name = filePath.replace(`${path}/`, '')
return {
name,
content: fs.readFileSync(`${path}/${name}`, 'utf8'),
}
})
return `export default ${JSON.stringify(files)}`
}
},
},
{
name: 'middleware',
apply: 'serve',
configureServer(viteDevServer) {
return () => {
viteDevServer.middlewares.use(async (req, res, next) => {
if (req.originalUrl.startsWith('/preview')) {
req.url = '/preview/index.html'
}
next()
})
}
},
},
],
resolve: {
alias: [
...globby.sync('../packages/*', { onlyDirectories: true })
.map(name => name.replace('../packages/', ''))
.map(name => {
return { find: `@tiptap/${name}`, replacement: resolve(`../packages/${name}/src/index.ts`) }
}),
],
},
// server: {
// fs: {
// // Allow serving files from one level up to the project root
// allow: ['..']
// }
// }
})

View File

@ -2,6 +2,7 @@
"private": true, "private": true,
"workspaces": [ "workspaces": [
"docs", "docs",
"demos",
"packages/*" "packages/*"
], ],
"browserslist": [ "browserslist": [
@ -11,6 +12,7 @@
], ],
"scripts": { "scripts": {
"start": "yarn --cwd ./docs start", "start": "yarn --cwd ./docs start",
"start:demos": "yarn --cwd ./demos start",
"lint": "eslint --quiet --no-error-on-unmatched-pattern ./", "lint": "eslint --quiet --no-error-on-unmatched-pattern ./",
"lint:fix": "eslint --fix --quiet --no-error-on-unmatched-pattern ./", "lint:fix": "eslint --fix --quiet --no-error-on-unmatched-pattern ./",
"test:open": "cypress open --project tests", "test:open": "cypress open --project tests",

862
yarn.lock

File diff suppressed because it is too large Load Diff