diff --git a/docs/src/demos/Experiments/Annotation/extension/AnnotationItem.ts b/docs/src/demos/Experiments/Annotation/extension/AnnotationItem.ts new file mode 100644 index 00000000..56b85459 --- /dev/null +++ b/docs/src/demos/Experiments/Annotation/extension/AnnotationItem.ts @@ -0,0 +1,10 @@ +export class AnnotationItem { + public id!: number + + public text!: string + + constructor(id: number, text: string) { + this.id = id + this.text = text + } +} diff --git a/docs/src/demos/Experiments/Annotation/extension/AnnotationPlugin.ts b/docs/src/demos/Experiments/Annotation/extension/AnnotationPlugin.ts new file mode 100644 index 00000000..290b9cef --- /dev/null +++ b/docs/src/demos/Experiments/Annotation/extension/AnnotationPlugin.ts @@ -0,0 +1,33 @@ +import { Plugin, PluginKey } from 'prosemirror-state' +import { AnnotationState } from './AnnotationState' + +export const AnnotationPluginKey = new PluginKey('annotation') + +export const AnnotationPlugin = (options: any) => new Plugin({ + key: AnnotationPluginKey, + state: { + init: AnnotationState.init, + apply(transaction, prevState) { + return prevState.apply(transaction) + }, + }, + props: { + decorations(state) { + const { decorations } = this.getState(state) + const { selection } = state + + if (!selection.empty) { + return decorations + } + + const annotations = this + .getState(state) + .annotationsAt(selection.from) + + options.onUpdate(annotations) + + return decorations + }, + + }, +}) diff --git a/docs/src/demos/Experiments/Annotation/extension/AnnotationState.ts b/docs/src/demos/Experiments/Annotation/extension/AnnotationState.ts new file mode 100644 index 00000000..23f65823 --- /dev/null +++ b/docs/src/demos/Experiments/Annotation/extension/AnnotationState.ts @@ -0,0 +1,95 @@ +import { Decoration, DecorationSet } from 'prosemirror-view' +import { ySyncPluginKey } from 'y-prosemirror' +import { AnnotationPluginKey } from './AnnotationPlugin' + +export class AnnotationState { + private decorations: any + + constructor(decorations: any) { + this.decorations = decorations + } + + findAnnotation(id: number) { + const current = this.decorations.find() + + for (let i = 0; i < current.length; i += 1) { + if (current[i].spec.data.id === id) { + return current[i] + } + } + } + + annotationsAt(position: number) { + return this.decorations.find(position, position) + } + + apply(transaction: any) { + console.log('transaction', transaction.meta, transaction.docChanged, transaction) + + const yjsTransaction = transaction.getMeta(ySyncPluginKey) + if (yjsTransaction) { + // TODO: Map positions + // absolutePositionToRelativePosition(state.selection.anchor, pmbinding.type, pmbinding.mapping) + console.log('map positions', transaction, yjsTransaction) + + return this + + // const { binding } = yjsTransaction + // console.log({ binding }, { transaction }, transaction.docChanged) + // console.log('yjsTransaction.isChangeOrigin', yjsTransaction.isChangeOrigin) + + // console.log('yjs mapping', yjsTransaction.binding?.mapping) + // console.log('all decorations', this.decorations.find()) + // console.log('original prosemirror mapping', this.decorations.map(transaction.mapping, transaction.doc)) + // console.log('difference between ProseMirror & Y.js', transaction.mapping, yjsTransaction.binding?.mapping) + + // Code to sync the selection: + // export const getRelativeSelection = (pmbinding, state) => ({ + // anchor: absolutePositionToRelativePosition(state.selection.anchor, pmbinding.type, pmbinding.mapping), + // head: absolutePositionToRelativePosition(state.selection.head, pmbinding.type, pmbinding.mapping) + // }) + + // console.log(yjsTransaction.binding.mapping, transaction.curSelection.anchor) + } + + if (transaction.docChanged) { + // TODO: Fixes the initial load (complete replace of the document) + // return this + + // TODO: Fixes later changes (typing before the annotation) + const decorations = this.decorations.map(transaction.mapping, transaction.doc) + + return new AnnotationState(decorations) + } + + const action = transaction.getMeta(AnnotationPluginKey) + const actionType = action && action.type + + if (action) { + let { decorations } = this + + if (actionType === 'addAnnotation') { + decorations = decorations.add(transaction.doc, [ + Decoration.inline(action.from, action.to, { class: 'annotation' }, { data: action.data }), + ]) + } else if (actionType === 'deleteAnnotation') { + decorations = decorations.remove([ + this.findAnnotation(action.id), + ]) + } + + return new AnnotationState(decorations) + } + + return this + } + + static init(config: any, state: any) { + // TODO: Load initial decorations from Y.js? + const decorations = DecorationSet.create(state.doc, [ + Decoration.inline(105, 190, { class: 'annotation' }, { data: { id: 123, content: 'foobar' } }), + ]) + + return new AnnotationState(decorations) + } +} diff --git a/docs/src/demos/Experiments/Annotation/extension/annotation.ts b/docs/src/demos/Experiments/Annotation/extension/annotation.ts new file mode 100644 index 00000000..06c666b6 --- /dev/null +++ b/docs/src/demos/Experiments/Annotation/extension/annotation.ts @@ -0,0 +1,70 @@ +import { Extension, Command } from '@tiptap/core' +import { AnnotationItem } from './AnnotationItem' +import { AnnotationPlugin, AnnotationPluginKey } from './AnnotationPlugin' + +function randomId() { + return Math.floor(Math.random() * 0xffffffff) +} + +export interface AnnotationOptions { + HTMLAttributes: { + [key: string]: any + }, + onUpdate: (items: [any?]) => {}, +} + +export const Annotation = Extension.create({ + name: 'annotation', + + defaultOptions: { + HTMLAttributes: { + class: 'annotation', + }, + onUpdate: decorations => decorations, + }, + + addCommands() { + return { + addAnnotation: (content: any): Command => ({ dispatch, state }) => { + const { selection } = state + + if (selection.empty) { + return false + } + + if (dispatch && content) { + dispatch(state.tr.setMeta(AnnotationPluginKey, { + type: 'addAnnotation', + from: selection.from, + to: selection.to, + data: new AnnotationItem( + randomId(), + content, + ), + })) + } + + return true + }, + deleteAnnotation: (id: number): Command => ({ dispatch, state }) => { + if (dispatch) { + dispatch(state.tr.setMeta(AnnotationPluginKey, { type: 'deleteAnnotation', id })) + } + + return true + }, + } + }, + + addProseMirrorPlugins() { + return [ + AnnotationPlugin(this.options), + ] + }, +}) + +declare module '@tiptap/core' { + interface AllExtensions { + Annotation: typeof Annotation, + } +} diff --git a/docs/src/demos/Experiments/Annotation/extension/index.ts b/docs/src/demos/Experiments/Annotation/extension/index.ts new file mode 100644 index 00000000..7c86e27d --- /dev/null +++ b/docs/src/demos/Experiments/Annotation/extension/index.ts @@ -0,0 +1,5 @@ +import { Annotation } from './annotation' + +export * from './annotation' + +export default Annotation diff --git a/docs/src/demos/Experiments/Annotation/index.spec.js b/docs/src/demos/Experiments/Annotation/index.spec.js new file mode 100644 index 00000000..6a218007 --- /dev/null +++ b/docs/src/demos/Experiments/Annotation/index.spec.js @@ -0,0 +1,7 @@ +context('/api/extensions/annotations', () => { + before(() => { + cy.visit('/api/extensions/annotations') + }) + + // TODO: Write tests +}) diff --git a/docs/src/demos/Experiments/Annotation/index.vue b/docs/src/demos/Experiments/Annotation/index.vue new file mode 100644 index 00000000..9ab11811 --- /dev/null +++ b/docs/src/demos/Experiments/Annotation/index.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/docs/src/demos/Experiments/Comments/index.spec.js b/docs/src/demos/Experiments/Comments/index.spec.js new file mode 100644 index 00000000..6f90b595 --- /dev/null +++ b/docs/src/demos/Experiments/Comments/index.spec.js @@ -0,0 +1,7 @@ +context('/examples/annotations', () => { + before(() => { + cy.visit('/examples/annotations') + }) + + // TODO: Write tests +}) diff --git a/docs/src/demos/Experiments/Comments/index.vue b/docs/src/demos/Experiments/Comments/index.vue new file mode 100644 index 00000000..c2388200 --- /dev/null +++ b/docs/src/demos/Experiments/Comments/index.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/docs/src/docPages/api/extensions/annotation.md b/docs/src/docPages/api/extensions/annotation.md new file mode 100644 index 00000000..c65fea66 --- /dev/null +++ b/docs/src/docPages/api/extensions/annotation.md @@ -0,0 +1,8 @@ +# Annotation +TODO + +## Source code +[packages/extension-annotation/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-annotation/) + +## Usage + diff --git a/docs/src/docPages/experiments.md b/docs/src/docPages/experiments.md index 70f7d340..34e2966d 100644 --- a/docs/src/docPages/experiments.md +++ b/docs/src/docPages/experiments.md @@ -2,3 +2,5 @@ Congratulations! You’ve found our secret playground with a list of experiments. Be aware, that nothing here is ready to use. Feel free to play around, but please, don’t open an issue for a bug you’ve found here or send pull requests. :-) * [Linter](/experiments/linter) +* [Annotation](/experiments/annotation) +* [Comments](/experiments/comments) diff --git a/docs/src/docPages/experiments/annotation.md b/docs/src/docPages/experiments/annotation.md new file mode 100644 index 00000000..4d4ebbe5 --- /dev/null +++ b/docs/src/docPages/experiments/annotation.md @@ -0,0 +1,5 @@ +# Annotation + +⚠️ Experiment + + diff --git a/docs/src/docPages/experiments/comments.md b/docs/src/docPages/experiments/comments.md new file mode 100644 index 00000000..42fda436 --- /dev/null +++ b/docs/src/docPages/experiments/comments.md @@ -0,0 +1,5 @@ +# Comments + +⚠️ Experiment + + diff --git a/docs/src/links.yaml b/docs/src/links.yaml index e3176c41..d03fc02f 100644 --- a/docs/src/links.yaml +++ b/docs/src/links.yaml @@ -35,6 +35,10 @@ link: /examples/drawing - title: Multiple editors link: /examples/multiple-editors + - title: Comments + link: /examples/comments + draft: true + - title: Guide items: @@ -151,6 +155,9 @@ - title: Extensions link: /api/extensions items: + - title: Annotation + link: /api/extensions/annotation + draft: true - title: Collaboration link: /api/extensions/collaboration type: pro