Ctrl K
ComunidadGuías…

packages/extension-collaboration-caret/src/collaboration-caret.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { Extension, isValidCSSStyleValue } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import type { DecorationAttrs } from '@tiptap/pm/view'
import { defaultSelectionBuilder, yCursorPlugin } from '@tiptap/y-tiptap'

type CollaborationCaretStorage = {
  users: { clientId: number; [key: string]: any }[]
}

export interface CollaborationCaretOptions {
  /**
   * The Hocuspocus provider instance. This can also be a TiptapCloudProvider instance.
   * @type {HocuspocusProvider | TiptapCloudProvider}
   * @example new HocuspocusProvider()
   */
  provider: any

  /**
   * The user details object – feel free to add properties to this object as needed
   * @example { name: 'John Doe', color: '#305500' }
   */
  user: Record<string, any>

  /**
   * A function that returns a DOM element for the cursor.
   * @param user The user details object
   * @example
   * render: user => {
   *  const cursor = document.createElement('span')
   *  cursor.classList.add('collaboration-carets__caret')
   *  cursor.style.borderColor = user.color
   *
   *  const label = document.createElement('div')
   *  label.classList.add('collaboration-carets__label')
   *  label.style.backgroundColor = user.color
   *  label.insertBefore(document.createTextNode(user.name), null)
   *
   *  cursor.insertBefore(label, null)
   *  return cursor
   * }
   */
  render(user: Record<string, any>): HTMLElement

  /**
   * A function that returns a ProseMirror DecorationAttrs object for the selection.
   * @param user The user details object
   * @example
   * selectionRender: user => {
   * return {
   *  nodeName: 'span',
   *  class: 'collaboration-carets__selection',
   *  'data-user': user.name,
   * }
   */
  selectionRender(user: Record<string, any>): DecorationAttrs

  /**
   * @deprecated The "onUpdate" option is deprecated. Please use `editor.storage.collaborationCaret.users` instead. Read more: https://tiptap.dev/api/extensions/collaboration-caret
   */
  onUpdate: (users: { clientId: number; [key: string]: any }[]) => null
}

declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    collaborationCaret: {
      /**
       * Update details of the current user
       * @example editor.commands.updateUser({ name: 'John Doe', color: '#305500' })
       */
      updateUser: (attributes: Record<string, any>) => ReturnType
      /**
       * Update details of the current user
       *
       * @deprecated The "user" command is deprecated. Please use "updateUser" instead. Read more: https://tiptap.dev/api/extensions/collaboration-caret
       */
      user: (attributes: Record<string, any>) => ReturnType
    }
  }

  interface Storage {
    collaborationCaret: CollaborationCaretStorage
  }
}

const awarenessStatesToArray = (states: Map<number, Record<string, any> | null | undefined>) => {
  return Array.from(states.entries()).map(([key, value]) => {
    if (value && value.user) {
      return {
        clientId: key,
        ...value.user,
      }
    }
    return {
      clientId: key,
    }
  })
}

const defaultOnUpdate = () => null

const isValidUserColor = (color: unknown): color is string => {
  return isValidCSSStyleValue(color) && /^#[0-9a-fA-F]{6}$/.test(color)
}

const sanitizeUserColor = (user: Record<string, any>) => ({
  ...user,
  color: isValidUserColor(user.color) ? user.color : 'transparent',
})

/**
 * This extension allows you to add collaboration carets to your editor.
 * @see https://tiptap.dev/api/extensions/collaboration-caret
 */
export const CollaborationCaret = Extension.create<
  CollaborationCaretOptions,
  CollaborationCaretStorage
>({
  name: 'collaborationCaret',

  priority: 999,

  addOptions() {
    return {
      provider: null,
      user: {
        name: null,
        color: null,
      },
      render: user => {
        const cursor = document.createElement('span')
        const { color } = sanitizeUserColor(user)

        cursor.classList.add('collaboration-carets__caret')
        cursor.style.borderColor = color

        const label = document.createElement('div')

        label.classList.add('collaboration-carets__label')
        label.style.backgroundColor = color
        label.insertBefore(document.createTextNode(user.name), null)
        cursor.insertBefore(label, null)

        return cursor
      },
      selectionRender: user => {
        if (!isValidUserColor(user.color)) {
          return {}
        }

        return defaultSelectionBuilder({ name: user.name, color: user.color })
      },
      onUpdate: defaultOnUpdate,
    }
  },

  onCreate() {
    if (this.options.onUpdate !== defaultOnUpdate) {
      console.warn(
        '[tiptap warn]: DEPRECATED: The "onUpdate" option is deprecated. Please use `editor.storage.collaborationCaret.users` instead. Read more: https://tiptap.dev/api/extensions/collaboration-caret',
      )
    }
    if (!this.options.provider) {
      throw new Error('The "provider" option is required for the CollaborationCaret extension')
    }
  },

  addStorage() {
    return {
      users: [],
    }
  },

  addCommands() {
    return {
      updateUser: attributes => () => {
        this.options.provider.awareness.setLocalStateField('user', attributes)
        return true
      },
      user:
        attributes =>
        ({ editor }) => {
          console.warn(
            '[tiptap warn]: DEPRECATED: The "user" command is deprecated. Please use "updateUser" instead. Read more: https://tiptap.dev/api/extensions/collaboration-caret',
          )

          return editor.commands.updateUser(attributes)
        },
    }
  },

  addProseMirrorPlugins() {
    const { provider } = this.options
    const storage = this.storage
    const user = this.options.user

    // Owns the awareness 'update' subscription so it's torn down when the editor
    // is destroyed. Without this, the listener would keep the editor reachable
    // from a shared provider's awareness emitter and leak memory across editors.
    const awarenessListenerPlugin = new Plugin({
      key: new PluginKey('collaborationCaretAwarenessListener'),
      view: () => {
        const onAwarenessUpdate = () => {
          storage.users = awarenessStatesToArray(provider.awareness.states)
        }

        provider.awareness.setLocalStateField('user', user)
        storage.users = awarenessStatesToArray(provider.awareness.states)
        provider.awareness.on('update', onAwarenessUpdate)

        return {
          destroy: () => {
            provider.awareness.off('update', onAwarenessUpdate)
            storage.users = []
          },
        }
      },
    })

    return [
      awarenessListenerPlugin,
      yCursorPlugin(provider.awareness, {
        cursorBuilder: currentUser => this.options.render(sanitizeUserColor(currentUser)),
        selectionBuilder: currentUser =>
          this.options.selectionRender(sanitizeUserColor(currentUser)),
      }),
    ]
  },
})