mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-06-07 17:43:49 +08:00

* fix: enforce type imports so that the bundler ignores types * chore: add changeset * fix: export types with export type keyword
171 lines
4.5 KiB
TypeScript
171 lines
4.5 KiB
TypeScript
import type { EditorState, Transaction } from '@tiptap/pm/state'
|
|
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
|
import {
|
|
absolutePositionToRelativePosition,
|
|
relativePositionToAbsolutePosition,
|
|
ySyncPluginKey,
|
|
} from '@tiptap/y-tiptap'
|
|
import type * as Y from 'yjs'
|
|
|
|
import { AnnotationItem } from './AnnotationItem.js'
|
|
import { AnnotationPluginKey } from './AnnotationPlugin.js'
|
|
import type { AddAnnotationAction, DeleteAnnotationAction, UpdateAnnotationAction } from './collaboration-annotation.js'
|
|
|
|
export interface AnnotationStateOptions {
|
|
HTMLAttributes: {
|
|
[key: string]: any
|
|
}
|
|
map: Y.Map<any>
|
|
instance: string
|
|
}
|
|
|
|
export class AnnotationState {
|
|
options: AnnotationStateOptions
|
|
|
|
decorations = DecorationSet.empty
|
|
|
|
constructor(options: AnnotationStateOptions) {
|
|
this.options = options
|
|
}
|
|
|
|
randomId() {
|
|
// TODO: That seems … to simple.
|
|
return Math.floor(Math.random() * 0xffffffff).toString()
|
|
}
|
|
|
|
findAnnotation(id: string) {
|
|
const current = this.decorations.find()
|
|
|
|
for (let i = 0; i < current.length; i += 1) {
|
|
if (current[i].spec.id === id) {
|
|
return current[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
addAnnotation(action: AddAnnotationAction, state: EditorState) {
|
|
const ystate = ySyncPluginKey.getState(state)
|
|
const { type, binding } = ystate
|
|
const { map } = this.options
|
|
const { from, to, data } = action
|
|
const absoluteFrom = absolutePositionToRelativePosition(from, type, binding.mapping)
|
|
const absoluteTo = absolutePositionToRelativePosition(to, type, binding.mapping)
|
|
|
|
map.set(this.randomId(), {
|
|
from: absoluteFrom,
|
|
to: absoluteTo,
|
|
data,
|
|
})
|
|
}
|
|
|
|
updateAnnotation(action: UpdateAnnotationAction) {
|
|
const { map } = this.options
|
|
|
|
const annotation = map.get(action.id)
|
|
|
|
map.set(action.id, {
|
|
from: annotation.from,
|
|
to: annotation.to,
|
|
data: action.data,
|
|
})
|
|
}
|
|
|
|
deleteAnnotation(id: string) {
|
|
const { map } = this.options
|
|
|
|
map.delete(id)
|
|
}
|
|
|
|
annotationsAt(position: number) {
|
|
return this.decorations.find(position, position).map(decoration => {
|
|
return new AnnotationItem(decoration)
|
|
})
|
|
}
|
|
|
|
createDecorations(state: EditorState) {
|
|
const { map, HTMLAttributes } = this.options
|
|
const ystate = ySyncPluginKey.getState(state)
|
|
const { doc, type, binding } = ystate
|
|
const decorations: Decoration[] = []
|
|
|
|
map.forEach((annotation, id) => {
|
|
const from = relativePositionToAbsolutePosition(doc, type, annotation.from, binding.mapping)
|
|
const to = relativePositionToAbsolutePosition(doc, type, annotation.to, binding.mapping)
|
|
|
|
if (!from || !to) {
|
|
return
|
|
}
|
|
|
|
// eslint-disable-next-line
|
|
console.log(`[${this.options.instance}] Decoration.inline()`, from, to, HTMLAttributes, {
|
|
id,
|
|
data: annotation.data,
|
|
})
|
|
|
|
if (from === to) {
|
|
console.warn(`[${this.options.instance}] corrupt decoration `, annotation.from, from, annotation.to, to)
|
|
}
|
|
|
|
decorations.push(
|
|
Decoration.inline(from, to, HTMLAttributes, {
|
|
id,
|
|
data: annotation.data,
|
|
inclusiveEnd: true,
|
|
}),
|
|
)
|
|
})
|
|
|
|
this.decorations = DecorationSet.create(state.doc, decorations)
|
|
}
|
|
|
|
apply(transaction: Transaction, state: EditorState) {
|
|
// Add/Remove annotations
|
|
const action = transaction.getMeta(AnnotationPluginKey) as
|
|
| AddAnnotationAction
|
|
| UpdateAnnotationAction
|
|
| DeleteAnnotationAction
|
|
|
|
if (action && action.type) {
|
|
// eslint-disable-next-line
|
|
console.log(`[${this.options.instance}] action: ${action.type}`)
|
|
|
|
if (action.type === 'addAnnotation') {
|
|
this.addAnnotation(action, state)
|
|
}
|
|
|
|
if (action.type === 'updateAnnotation') {
|
|
this.updateAnnotation(action)
|
|
}
|
|
|
|
if (action.type === 'deleteAnnotation') {
|
|
this.deleteAnnotation(action.id)
|
|
}
|
|
|
|
// @ts-ignore
|
|
if (action.type === 'createDecorations') {
|
|
this.createDecorations(state)
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
// Use Y.js to update positions
|
|
const ystate = ySyncPluginKey.getState(state)
|
|
|
|
if (ystate.isChangeOrigin) {
|
|
// eslint-disable-next-line
|
|
console.log(`[${this.options.instance}] isChangeOrigin: true → createDecorations`)
|
|
this.createDecorations(state)
|
|
|
|
return this
|
|
}
|
|
|
|
// Use ProseMirror to update positions
|
|
// eslint-disable-next-line
|
|
console.log(`[${this.options.instance}] isChangeOrigin: false → ProseMirror mapping`)
|
|
this.decorations = this.decorations.map(transaction.mapping, transaction.doc)
|
|
|
|
return this
|
|
}
|
|
}
|