add experiment demos
This commit is contained in:
200
demos/src/Experiments/Figure/Vue/figure.ts
Normal file
200
demos/src/Experiments/Figure/Vue/figure.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import {
|
||||
Node,
|
||||
nodeInputRule,
|
||||
mergeAttributes,
|
||||
findChildrenInRange,
|
||||
Tracker,
|
||||
} from '@tiptap/core'
|
||||
|
||||
export interface FigureOptions {
|
||||
HTMLAttributes: Record<string, any>,
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
figure: {
|
||||
/**
|
||||
* Add a figure element
|
||||
*/
|
||||
setFigure: (options: {
|
||||
src: string,
|
||||
alt?: string,
|
||||
title?: string,
|
||||
caption?: string,
|
||||
}) => ReturnType,
|
||||
|
||||
/**
|
||||
* Converts an image to a figure
|
||||
*/
|
||||
imageToFigure: () => ReturnType,
|
||||
|
||||
/**
|
||||
* Converts a figure to an image
|
||||
*/
|
||||
figureToImage: () => ReturnType,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const inputRegex = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/
|
||||
|
||||
export const Figure = Node.create<FigureOptions>({
|
||||
name: 'figure',
|
||||
|
||||
defaultOptions: {
|
||||
HTMLAttributes: {},
|
||||
},
|
||||
|
||||
group: 'block',
|
||||
|
||||
content: 'inline*',
|
||||
|
||||
draggable: true,
|
||||
|
||||
isolating: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
parseHTML: element => {
|
||||
return {
|
||||
src: element.querySelector('img')?.getAttribute('src'),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
alt: {
|
||||
default: null,
|
||||
parseHTML: element => {
|
||||
return {
|
||||
alt: element.querySelector('img')?.getAttribute('alt'),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
title: {
|
||||
default: null,
|
||||
parseHTML: element => {
|
||||
return {
|
||||
title: element.querySelector('img')?.getAttribute('title'),
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'figure',
|
||||
contentElement: 'figcaption',
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
'figure', this.options.HTMLAttributes,
|
||||
['img', mergeAttributes(HTMLAttributes, { draggable: false, contenteditable: false })],
|
||||
['figcaption', 0],
|
||||
]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setFigure: ({ caption, ...attrs }) => ({ chain }) => {
|
||||
return chain()
|
||||
.insertContent({
|
||||
type: this.name,
|
||||
attrs,
|
||||
content: caption
|
||||
? [{ type: 'text', text: caption }]
|
||||
: [],
|
||||
})
|
||||
// set cursor at end of caption field
|
||||
.command(({ tr, commands }) => {
|
||||
const { doc, selection } = tr
|
||||
const position = doc.resolve(selection.to - 2).end()
|
||||
|
||||
return commands.setTextSelection(position)
|
||||
})
|
||||
.run()
|
||||
},
|
||||
|
||||
imageToFigure: () => ({ tr, commands }) => {
|
||||
const { doc, selection } = tr
|
||||
const { from, to } = selection
|
||||
const images = findChildrenInRange(doc, { from, to }, node => node.type.name === 'image')
|
||||
|
||||
if (!images.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
const tracker = new Tracker(tr)
|
||||
|
||||
return commands.forEach(images, ({ node, pos }) => {
|
||||
const mapResult = tracker.map(pos)
|
||||
|
||||
if (mapResult.deleted) {
|
||||
return false
|
||||
}
|
||||
|
||||
const range = {
|
||||
from: mapResult.position,
|
||||
to: mapResult.position + node.nodeSize,
|
||||
}
|
||||
|
||||
return commands.insertContentAt(range, {
|
||||
type: this.name,
|
||||
attrs: {
|
||||
src: node.attrs.src,
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
figureToImage: () => ({ tr, commands }) => {
|
||||
const { doc, selection } = tr
|
||||
const { from, to } = selection
|
||||
const figures = findChildrenInRange(doc, { from, to }, node => node.type.name === this.name)
|
||||
|
||||
if (!figures.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
const tracker = new Tracker(tr)
|
||||
|
||||
return commands.forEach(figures, ({ node, pos }) => {
|
||||
const mapResult = tracker.map(pos)
|
||||
|
||||
if (mapResult.deleted) {
|
||||
return false
|
||||
}
|
||||
|
||||
const range = {
|
||||
from: mapResult.position,
|
||||
to: mapResult.position + node.nodeSize,
|
||||
}
|
||||
|
||||
return commands.insertContentAt(range, {
|
||||
type: 'image',
|
||||
attrs: {
|
||||
src: node.attrs.src,
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule(inputRegex, this.type, match => {
|
||||
const [, alt, src, title] = match
|
||||
|
||||
return { src, alt, title }
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
15
demos/src/Experiments/Figure/Vue/index.html
Normal file
15
demos/src/Experiments/Figure/Vue/index.html
Normal 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('Experiments/Figure', source)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
115
demos/src/Experiments/Figure/Vue/index.vue
Normal file
115
demos/src/Experiments/Figure/Vue/index.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div v-if="editor">
|
||||
<button @click="addFigure">
|
||||
figure
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().imageToFigure().run()"
|
||||
:disabled="!editor.can().imageToFigure()"
|
||||
>
|
||||
image to figure
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().figureToImage().run()"
|
||||
:disabled="!editor.can().figureToImage()"
|
||||
>
|
||||
figure to image
|
||||
</button>
|
||||
<editor-content :editor="editor" />
|
||||
|
||||
<h2>HTML</h2>
|
||||
{{ editor.getHTML() }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Image from '@tiptap/extension-image'
|
||||
import { Figure } from './figure'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
addFigure() {
|
||||
const url = window.prompt('URL')
|
||||
const caption = window.prompt('caption')
|
||||
|
||||
if (url) {
|
||||
this.editor
|
||||
.chain()
|
||||
.focus()
|
||||
.setFigure({ src: url, caption })
|
||||
.run()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Figure,
|
||||
Image,
|
||||
],
|
||||
content: `
|
||||
<p>Figure + Figcaption</p>
|
||||
<figure>
|
||||
<img src="https://source.unsplash.com/8xznAGy4HcY/800x400" alt="Random photo of something" title="Who’s dat?">
|
||||
<figcaption>
|
||||
<p>Amazing caption</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
<img src="https://source.unsplash.com/K9QHL52rE2k/800x400">
|
||||
<img src="https://source.unsplash.com/8xznAGy4HcY/800x400">
|
||||
<img src="https://source.unsplash.com/K9QHL52rE2k/800x400">
|
||||
<p>That’s it.</p>
|
||||
`,
|
||||
})
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ProseMirror {
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
figure {
|
||||
max-width: 25rem;
|
||||
border: 3px solid #0D0D0D;
|
||||
border-radius: 0.5rem;
|
||||
margin: 1rem 0;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
margin-top: 0.25rem;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
border: 2px dashed #0D0D0D20;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: min(100%, 25rem);
|
||||
height: auto;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user