From 604d067ed368011cfe589440b5d78807b8ecb24b Mon Sep 17 00:00:00 2001 From: Hans Pagel Date: Sat, 6 Feb 2021 23:20:13 +0100 Subject: [PATCH] docs: add Vue 3 snippets --- .../src/docPages/guide/getting-started/vue.md | 441 +++++++++++++++--- .../docPages/guide/getting-started/vue2.md | 104 +++++ docs/src/links.yaml | 4 + 3 files changed, 473 insertions(+), 76 deletions(-) create mode 100644 docs/src/docPages/guide/getting-started/vue2.md diff --git a/docs/src/docPages/guide/getting-started/vue.md b/docs/src/docPages/guide/getting-started/vue.md index 237b4f80..419dec7e 100644 --- a/docs/src/docPages/guide/getting-started/vue.md +++ b/docs/src/docPages/guide/getting-started/vue.md @@ -1,104 +1,393 @@ -# Vue.js +# Vue.js 3 ## toc ## Introduction -The following guide describes how to integrate tiptap with your [Vue](https://vuejs.org/) CLI project. +The `@tiptap/vue` package is not yet ported to Vue 3. Meanwhile, you can find compatible version contributed by [@samwillis](https://github.com/samwillis) and [@areknawo](https://github.com/areknawo) here. -## Requirements -* [Node](https://nodejs.org/en/download/) installed on your machine -* [Vue CLI](https://cli.vuejs.org/) installed on your machine -* Experience with [Vue](https://vuejs.org/v2/guide/#Getting-Started) +## EditorContent.ts +https://github.com/ueberdosis/tiptap-next/issues/85#issuecomment-774520164 -## 1. Create a project (optional) -If you already have an existing Vue project, that’s fine too. Just skip this step and proceed with the next step. +```ts +import { defineComponent, h, ref, Teleport, onBeforeUpdate } from 'vue' +import VueRenderer from '../VueRenderer' -For the sake of this guide, let’s start with a fresh Vue project called `tiptap-example`. The Vue CLI sets up everything we need, just select the default Vue 2 template. +function setupVueRenderers(){ + const vueRenderers = ref(([] as VueRenderer[])) + const vueRendererEls = ref(new Map()) + const addVueRenderer = (vueRenderer: VueRenderer) => { + vueRenderers.value.push(vueRenderer) + } + const deleteVueRenderer = (vueRenderer: VueRenderer) => { + const index = vueRenderers.value.indexOf(vueRenderer) + if (index > -1) { + vueRenderers.value.splice(index, 1) + } + } + onBeforeUpdate(() => { + vueRendererEls.value = new Map() + }) + return { + vueRenderers, + vueRendererEls, + addVueRenderer, + deleteVueRenderer, + } +} -```bash -# create a project -vue create tiptap-example +export default defineComponent({ + name: 'EditorContent', -# change directory -cd tiptap-example -``` - -## 2. Install the dependencies -Okay, enough of the boring boilerplate work. Let’s finally install tiptap! For the following example you’ll need `@tiptap/core` (the actual editor) and the `@tiptap/vue-starter-kit` which has everything to get started quickly, for example a few default extensions and a basic Vue component. - -```bash -# install with npm -npm install @tiptap/core @tiptap/vue-starter-kit - -# install with Yarn -yarn add @tiptap/core @tiptap/vue-starter-kit -``` - -If you followed step 1 and 2, you can now start your project with `npm run dev` or `yarn dev`, and open [http://localhost:8080/](http://localhost:3000/) in your favorite browser. This might be different, if you’re working with an existing project. - -## 3. Create a new component -To actually start using tiptap, you’ll need to add a new component to your app. Let’s call it `Tiptap` and put the following example code in `components/Tiptap.vue`. - -This is the fastest way to get tiptap up and running with Vue. It will give you a very basic version of tiptap, without any buttons. No worries, you will be able to add more functionality soon. - -```html - - - +}) ``` -## 4. Add it to your app -Now, let’s replace the content of `src/App.vue` with the following example code to use our new `Tiptap` component in our app. +## VueRenderer.ts -```html - +https://github.com/ueberdosis/tiptap-next/issues/85#issuecomment-774520164) - ``` -You should now see tiptap in your browser. You’ve successfully set up tiptap! Time to give yourself a pat on the back. Let’s start to configure your editor in the next step. +## BubbleMenu.ts -## 5. Use v-model (optional) -You’re probably used to bind your data with `v-model` in forms, that’s also possible with tiptap. Here is a working example component, that you can integrate in your project: +https://github.com/ueberdosis/tiptap-next/issues/62#issuecomment-750914155 - +```ts +import { Editor } from '@tiptap/core' +import { EditorState, Plugin, PluginKey } from 'prosemirror-state' +import { EditorView } from 'prosemirror-view' + +interface BubbleMenuSettings { + bottom: number + isActive: boolean + left: number + top: number +} +interface BubbleMenuPluginOptions { + editor: Editor + element: HTMLElement + keepInBounds: boolean + onUpdate(menu: BubbleMenuSettings): void +} +type DOMRectSide = 'bottom' | 'left' | 'right' | 'top' + +function textRange(node: Node, from?: number, to?: number) { + const range = document.createRange() + range.setEnd( + node, + typeof to === 'number' ? to : (node.nodeValue || ').length + ) + range.setStart(node, from || 0) + return range +} + +function singleRect(object: Range | Element, bias: number) { + const rects = object.getClientRects() + return !rects.length + ? object.getBoundingClientRect() + : rects[bias < 0 ? 0 : rects.length - 1] +} + +function coordsAtPos(view: EditorView, pos: number, end = false) { + const { node, offset } = view.domAtPos(pos) //view.docView.domFromPos(pos) + let side: DOMRectSide | null = null + let rect: DOMRect | null = null + if (node.nodeType === 3) { + const nodeValue = node.nodeValue || ' + if (end && offset < nodeValue.length) { + rect = singleRect(textRange(node, offset - 1, offset), -1) + side = 'right' + } else if (offset < nodeValue.length) { + rect = singleRect(textRange(node, offset, offset + 1), -1) + side = 'left' + } + } else if (node.firstChild) { + if (offset < node.childNodes.length) { + const child = node.childNodes[offset] + rect = singleRect( + child.nodeType === 3 ? textRange(child) : (child as Element), + -1 + ) + side = 'left' + } + if ((!rect || rect.top === rect.bottom) && offset) { + const child = node.childNodes[offset - 1] + rect = singleRect( + child.nodeType === 3 ? textRange(child) : (child as Element), + 1 + ) + side = 'right' + } + } else { + const element = node as Element + rect = element.getBoundingClientRect() + side = 'left' + } + + if (rect && side) { + const x = rect[side] + + return { + top: rect.top, + bottom: rect.bottom, + left: x, + right: x, + } + } + return { + top: 0, + bottom: 0, + left: 0, + right: 0, + } +} + +class Menu { + public options: BubbleMenuPluginOptions + public editorView: EditorView + public isActive = false + public left = 0 + public bottom = 0 + public top = 0 + public preventHide = false + + constructor({ + options, + editorView, + }: { + options: BubbleMenuPluginOptions + editorView: EditorView + }) { + this.options = { + ...{ + element: null, + keepInBounds: true, + onUpdate: () => false, + }, + ...options, + } + this.editorView = editorView + this.options.element.addEventListener('mousedown', this.mousedownHandler, { + capture: true, + }) + this.options.editor.on('focus', this.focusHandler) + this.options.editor.on('blur', this.blurHandler) + } + + mousedownHandler = () => { + this.preventHide = true + } + focusHandler = () => { + this.update(this.options.editor.view) + } + blurHandler = ({ event }: { event: FocusEvent }) => { + if (this.preventHide) { + this.preventHide = false + return + } + + this.hide(event) + } + + update(view: EditorView, lastState?: EditorState) { + const { state } = view + + if (view.composing) { + return + } + + if ( + lastState && + lastState.doc.eq(state.doc) && + lastState.selection.eq(state.selection) + ) { + return + } + + if (state.selection.empty) { + this.hide() + return + } + + const { from, to } = state.selection + const start = coordsAtPos(view, from) + const end = coordsAtPos(view, to, true) + const parent = this.options.element.offsetParent + + if (!parent) { + this.hide() + return + } + + const box = parent.getBoundingClientRect() + const el = this.options.element.getBoundingClientRect() + const left = (start.left + end.left) / 2 - box.left + + this.left = Math.round( + this.options.keepInBounds + ? Math.min(box.width - el.width / 2, Math.max(left, el.width / 2)) + : left + ) + this.bottom = Math.round(box.bottom - start.top) + this.top = Math.round(end.bottom - box.top) + this.isActive = true + + this.sendUpdate() + } + + sendUpdate() { + this.options.onUpdate({ + isActive: this.isActive, + left: this.left, + bottom: this.bottom, + top: this.top, + }) + } + + hide(event?: FocusEvent) { + if ( + event && + event.relatedTarget && + this.options.element.parentNode && + this.options.element.parentNode.contains(event.relatedTarget as Node) + ) { + return + } + + this.isActive = false + this.sendUpdate() + } + + destroy() { + this.options.element.removeEventListener( + 'mousedown', + this.mousedownHandler + ) + this.options.editor.off('focus', this.focusHandler) + this.options.editor.off('blur', this.blurHandler) + } +} + +const BubbleMenu = (options: BubbleMenuPluginOptions) => { + return new Plugin({ + key: new PluginKey('menu_bubble'), + view(editorView) { + return new Menu({ editorView, options }) + }, + }) +} + +export { BubbleMenu } +``` diff --git a/docs/src/docPages/guide/getting-started/vue2.md b/docs/src/docPages/guide/getting-started/vue2.md new file mode 100644 index 00000000..7f004807 --- /dev/null +++ b/docs/src/docPages/guide/getting-started/vue2.md @@ -0,0 +1,104 @@ +# Vue.js 2 + +## toc + +## Introduction +The following guide describes how to integrate tiptap with your [Vue](https://vuejs.org/) CLI project. + +## Requirements +* [Node](https://nodejs.org/en/download/) installed on your machine +* [Vue CLI](https://cli.vuejs.org/) installed on your machine +* Experience with [Vue](https://vuejs.org/v2/guide/#Getting-Started) + +## 1. Create a project (optional) +If you already have an existing Vue project, that’s fine too. Just skip this step and proceed with the next step. + +For the sake of this guide, let’s start with a fresh Vue project called `tiptap-example`. The Vue CLI sets up everything we need, just select the default Vue 2 template. + +```bash +# create a project +vue create tiptap-example + +# change directory +cd tiptap-example +``` + +## 2. Install the dependencies +Okay, enough of the boring boilerplate work. Let’s finally install tiptap! For the following example you’ll need `@tiptap/core` (the actual editor) and the `@tiptap/vue-starter-kit` which has everything to get started quickly, for example a few default extensions and a basic Vue component. + +```bash +# install with npm +npm install @tiptap/core @tiptap/vue-starter-kit + +# install with Yarn +yarn add @tiptap/core @tiptap/vue-starter-kit +``` + +If you followed step 1 and 2, you can now start your project with `npm run dev` or `yarn dev`, and open [http://localhost:8080/](http://localhost:3000/) in your favorite browser. This might be different, if you’re working with an existing project. + +## 3. Create a new component +To actually start using tiptap, you’ll need to add a new component to your app. Let’s call it `Tiptap` and put the following example code in `components/Tiptap.vue`. + +This is the fastest way to get tiptap up and running with Vue. It will give you a very basic version of tiptap, without any buttons. No worries, you will be able to add more functionality soon. + +```html + + + +``` + +## 4. Add it to your app +Now, let’s replace the content of `src/App.vue` with the following example code to use our new `Tiptap` component in our app. + +```html + + + +``` + +You should now see tiptap in your browser. You’ve successfully set up tiptap! Time to give yourself a pat on the back. Let’s start to configure your editor in the next step. + +## 5. Use v-model (optional) +You’re probably used to bind your data with `v-model` in forms, that’s also possible with tiptap. Here is a working example component, that you can integrate in your project: + + diff --git a/docs/src/links.yaml b/docs/src/links.yaml index 69faee28..33cbe0f3 100644 --- a/docs/src/links.yaml +++ b/docs/src/links.yaml @@ -50,7 +50,11 @@ link: /guide/getting-started items: - title: Vue.js 2 + link: /guide/getting-started/vue2 + skip: true + - title: Vue.js 3 link: /guide/getting-started/vue + type: draft skip: true - title: Nuxt.js link: /guide/getting-started/nuxt