snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
This commit is contained in:
1863
packages/extension-collaboration-caret/CHANGELOG.md
Normal file
1863
packages/extension-collaboration-caret/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
18
packages/extension-collaboration-caret/README.md
Normal file
18
packages/extension-collaboration-caret/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# @tiptap/extension-collaboration-caret
|
||||
|
||||
[](https://www.npmjs.com/package/@tiptap/extension-collaboration-caret)
|
||||
[](https://npmcharts.com/compare/tiptap?minimal=true)
|
||||
[](https://www.npmjs.com/package/@tiptap/extension-collaboration-caret)
|
||||
[](https://github.com/sponsors/ueberdosis)
|
||||
|
||||
## Introduction
|
||||
|
||||
Tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as _New York Times_, _The Guardian_ or _Atlassian_.
|
||||
|
||||
## Official Documentation
|
||||
|
||||
Documentation can be found on the [Tiptap website](https://tiptap.dev).
|
||||
|
||||
## License
|
||||
|
||||
Tiptap is open sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap/blob/main/LICENSE.md).
|
||||
60
packages/extension-collaboration-caret/package.json
Normal file
60
packages/extension-collaboration-caret/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@tiptap/extension-collaboration-caret",
|
||||
"version": "3.30.3",
|
||||
"description": "collaboration caret extension for tiptap",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
"tiptap extension"
|
||||
],
|
||||
"homepage": "https://tiptap.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ueberdosis/tiptap/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ueberdosis/tiptap",
|
||||
"directory": "packages/extension-collaboration-caret"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": {
|
||||
"import": "./dist/index.d.ts",
|
||||
"require": "./dist/index.d.cts"
|
||||
},
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vp pack"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tiptap/core": "workspace:^",
|
||||
"@tiptap/extension-collaboration": "workspace:^",
|
||||
"@tiptap/extension-document": "workspace:^",
|
||||
"@tiptap/extension-paragraph": "workspace:^",
|
||||
"@tiptap/extension-table": "workspace:^",
|
||||
"@tiptap/extension-text": "workspace:^",
|
||||
"@tiptap/pm": "workspace:^",
|
||||
"@tiptap/y-tiptap": "^3.0.7",
|
||||
"yjs": "^13.6.23"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "workspace:*",
|
||||
"@tiptap/pm": "workspace:*",
|
||||
"@tiptap/y-tiptap": "^3.0.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, test } from 'vite-plus/test'
|
||||
|
||||
import { CollaborationCaret } from './collaboration-caret.js'
|
||||
|
||||
const maliciousColor =
|
||||
'red; position:fixed; top:0; left:0; width:100vw; height:100vh; z-index:2147483647; background:black'
|
||||
|
||||
describe('default collaboration caret renderers', () => {
|
||||
test('does not apply extra declarations from a user color', () => {
|
||||
const cursor = CollaborationCaret.options.render({ name: 'Bob', color: maliciousColor })
|
||||
const label = cursor.querySelector<HTMLElement>('.collaboration-carets__label')
|
||||
|
||||
expect(cursor.style.borderColor).toBe('transparent')
|
||||
expect(cursor.style.position).toBe('')
|
||||
expect(cursor.style.width).toBe('')
|
||||
expect(label?.style.backgroundColor).toBe('transparent')
|
||||
})
|
||||
|
||||
test('does not add extra declarations to a selection', () => {
|
||||
const attributes = CollaborationCaret.options.selectionRender({
|
||||
name: 'Bob',
|
||||
color: maliciousColor,
|
||||
})
|
||||
|
||||
expect(attributes.style).toBeUndefined()
|
||||
})
|
||||
|
||||
test('applies a six-digit hex color to a selection', () => {
|
||||
const attributes = CollaborationCaret.options.selectionRender({
|
||||
name: 'Bob',
|
||||
color: '#ff0000',
|
||||
})
|
||||
|
||||
expect(attributes.style).toBe('background-color: #ff000070')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,225 @@
|
||||
import { Extension } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import type { DecorationAttrs } from '@tiptap/pm/view'
|
||||
import { defaultSelectionBuilder, yCursorPlugin } from '@tiptap/y-tiptap'
|
||||
import { isValidColor } from './lib/isValidColor.js'
|
||||
|
||||
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 sanitizeUserColor = (user: Record<string, any>) => ({
|
||||
...user,
|
||||
color: isValidColor(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 (!isValidColor(user.color)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return defaultSelectionBuilder(user)
|
||||
},
|
||||
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)),
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
5
packages/extension-collaboration-caret/src/index.ts
Normal file
5
packages/extension-collaboration-caret/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { CollaborationCaret } from './collaboration-caret.js'
|
||||
|
||||
export * from './collaboration-caret.js'
|
||||
|
||||
export default CollaborationCaret
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, test } from 'vite-plus/test'
|
||||
|
||||
import { isValidColor } from './isValidColor.js'
|
||||
|
||||
const maliciousColor =
|
||||
'red; position:fixed; top:0; left:0; width:100vw; height:100vh; z-index:2147483647; background:black'
|
||||
|
||||
describe('isValidColor', () => {
|
||||
// we only expect 6-digit hex values because thats
|
||||
// what y-tiptap is exposing to the client
|
||||
test.each(['#ffffff', '#1a2b3c', '#ABCDEF'])('returns true for a six-digit hex color', color => {
|
||||
expect(isValidColor(color)).toBe(true)
|
||||
})
|
||||
|
||||
test.each([
|
||||
null,
|
||||
undefined,
|
||||
'#fff',
|
||||
'#ffff',
|
||||
'#ffffffff',
|
||||
'red',
|
||||
'rgb(255, 0, 0)',
|
||||
maliciousColor,
|
||||
'red; background-image: url(https://example.com)',
|
||||
'red } body { display: none',
|
||||
])('returns false for an unsupported color', color => {
|
||||
expect(isValidColor(color)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isValidColor(color: unknown): color is string {
|
||||
return typeof color === 'string' && /^#[0-9a-fA-F]{6}$/.test(color)
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import { Editor } from '@tiptap/core'
|
||||
import Collaboration from '@tiptap/extension-collaboration'
|
||||
import CollaborationCaret from '@tiptap/extension-collaboration-caret'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import { Table, TableCell, TableHeader, TableRow } from '@tiptap/extension-table'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
import * as Y from 'yjs'
|
||||
|
||||
/**
|
||||
* Tests for CollaborationCaret extension, especially edge cases
|
||||
* with initial HTML content and complex structures like tables.
|
||||
*/
|
||||
describe('extension-collaboration-caret', () => {
|
||||
const editorElClass = 'tiptap'
|
||||
|
||||
const createEditorEl = () => {
|
||||
const editorEl = document.createElement('div')
|
||||
|
||||
editorEl.classList.add(editorElClass)
|
||||
document.body.appendChild(editorEl)
|
||||
return editorEl
|
||||
}
|
||||
|
||||
const getEditorEl = () => document.querySelector(`.${editorElClass}`)
|
||||
|
||||
/**
|
||||
* Test that the editor can be initialized with HTML content containing tables
|
||||
* when using Collaboration and CollaborationCaret extensions.
|
||||
*
|
||||
* This test reproduces the issue from GitHub #6979 where the editor crashes
|
||||
* with "Cannot read properties of undefined (reading 'doc')" when initialized
|
||||
* with HTML content containing tables.
|
||||
*/
|
||||
it('should not crash when initialized with HTML content containing tables', () => {
|
||||
const ydoc = new Y.Doc()
|
||||
|
||||
// Create a mock provider with minimal awareness API
|
||||
const mockProvider = {
|
||||
awareness: {
|
||||
states: new Map(),
|
||||
setLocalStateField: () => {},
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
getStates: () => new Map(),
|
||||
},
|
||||
}
|
||||
|
||||
const editorEl = createEditorEl()
|
||||
|
||||
// This should not throw an error
|
||||
expect(() => {
|
||||
const editor = new Editor({
|
||||
element: editorEl,
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Table,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
}),
|
||||
CollaborationCaret.configure({
|
||||
provider: mockProvider,
|
||||
user: {
|
||||
name: 'Test User',
|
||||
color: '#ff0000',
|
||||
},
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<table>
|
||||
<tr>
|
||||
<th>Header 1</th>
|
||||
<th>Header 2</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cell 1</td>
|
||||
<td>Cell 2</td>
|
||||
</tr>
|
||||
</table>
|
||||
`,
|
||||
})
|
||||
|
||||
editor.destroy()
|
||||
}).not.toThrow()
|
||||
|
||||
getEditorEl()?.remove()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that the editor can be initialized with simple HTML content
|
||||
* when using Collaboration and CollaborationCaret extensions.
|
||||
*/
|
||||
it('should not crash when initialized with simple HTML content', () => {
|
||||
const ydoc = new Y.Doc()
|
||||
|
||||
const mockProvider = {
|
||||
awareness: {
|
||||
states: new Map(),
|
||||
setLocalStateField: () => {},
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
getStates: () => new Map(),
|
||||
},
|
||||
}
|
||||
|
||||
const editorEl = createEditorEl()
|
||||
|
||||
expect(() => {
|
||||
const editor = new Editor({
|
||||
element: editorEl,
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
}),
|
||||
CollaborationCaret.configure({
|
||||
provider: mockProvider,
|
||||
user: {
|
||||
name: 'Test User',
|
||||
color: '#ff0000',
|
||||
},
|
||||
}),
|
||||
],
|
||||
content: '<p>Hello world</p>',
|
||||
})
|
||||
|
||||
editor.destroy()
|
||||
}).not.toThrow()
|
||||
|
||||
getEditorEl()?.remove()
|
||||
})
|
||||
|
||||
/**
|
||||
* Regression test: when an editor is destroyed but the provider/awareness
|
||||
* lives on (e.g. multiple editors sharing one provider), the awareness
|
||||
* 'update' listener registered by CollaborationCaret must be removed.
|
||||
* Otherwise the closure keeps the destroyed editor reachable and leaks memory.
|
||||
*/
|
||||
it('should remove its awareness listener when the editor is destroyed', () => {
|
||||
const ydoc = new Y.Doc()
|
||||
|
||||
const updateListeners = new Set<(...args: any[]) => void>()
|
||||
const mockAwareness = {
|
||||
states: new Map(),
|
||||
setLocalStateField: () => {},
|
||||
on: (event: string, fn: (...args: any[]) => void) => {
|
||||
if (event === 'update') updateListeners.add(fn)
|
||||
},
|
||||
off: (event: string, fn: (...args: any[]) => void) => {
|
||||
if (event === 'update') updateListeners.delete(fn)
|
||||
},
|
||||
getStates: () => new Map(),
|
||||
}
|
||||
const mockProvider = { awareness: mockAwareness }
|
||||
|
||||
const editorEl = createEditorEl()
|
||||
|
||||
const editor = new Editor({
|
||||
element: editorEl,
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Collaboration.configure({ document: ydoc }),
|
||||
CollaborationCaret.configure({
|
||||
provider: mockProvider,
|
||||
user: { name: 'Test User', color: '#ff0000' },
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
expect(updateListeners.size).toBe(1)
|
||||
|
||||
editor.destroy()
|
||||
|
||||
expect(updateListeners.size).toBe(0)
|
||||
|
||||
getEditorEl()?.remove()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that the editor can be initialized without any content
|
||||
* when using Collaboration and CollaborationCaret extensions.
|
||||
*/
|
||||
it('should work correctly when initialized without content', () => {
|
||||
const ydoc = new Y.Doc()
|
||||
|
||||
const mockProvider = {
|
||||
awareness: {
|
||||
states: new Map(),
|
||||
setLocalStateField: () => {},
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
getStates: () => new Map(),
|
||||
},
|
||||
}
|
||||
|
||||
const editorEl = createEditorEl()
|
||||
|
||||
expect(() => {
|
||||
const editor = new Editor({
|
||||
element: editorEl,
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
}),
|
||||
CollaborationCaret.configure({
|
||||
provider: mockProvider,
|
||||
user: {
|
||||
name: 'Test User',
|
||||
color: '#ff0000',
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
editor.destroy()
|
||||
}).not.toThrow()
|
||||
|
||||
getEditorEl()?.remove()
|
||||
})
|
||||
|
||||
it('should handle null or undefined awareness state values without crashing', () => {
|
||||
const ydoc = new Y.Doc()
|
||||
|
||||
const states = new Map<number, Record<string, any> | null>()
|
||||
|
||||
states.set(1, { user: { name: 'Alice', color: '#ff0000' } })
|
||||
states.set(2, null as any)
|
||||
states.set(3, undefined as any)
|
||||
states.set(4, { user: { name: 'Bob', color: '#00ff00' } })
|
||||
|
||||
const mockProvider = {
|
||||
awareness: {
|
||||
states,
|
||||
setLocalStateField: () => {},
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
getStates: () => states,
|
||||
},
|
||||
}
|
||||
|
||||
const editorEl = createEditorEl()
|
||||
|
||||
const editor = new Editor({
|
||||
element: editorEl,
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
}),
|
||||
CollaborationCaret.configure({
|
||||
provider: mockProvider,
|
||||
user: {
|
||||
name: 'Test User',
|
||||
color: '#0000ff',
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const users = editor.storage.collaborationCaret.users
|
||||
|
||||
expect(users).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ clientId: 1, name: 'Alice', color: '#ff0000' }),
|
||||
expect.objectContaining({ clientId: 4, name: 'Bob', color: '#00ff00' }),
|
||||
]),
|
||||
)
|
||||
|
||||
const nullUser = users.find((u: any) => u.clientId === 2)
|
||||
|
||||
expect(nullUser).toEqual({ clientId: 2 })
|
||||
|
||||
const undefinedUser = users.find((u: any) => u.clientId === 3)
|
||||
|
||||
expect(undefinedUser).toEqual({ clientId: 3 })
|
||||
|
||||
editor.destroy()
|
||||
getEditorEl()?.remove()
|
||||
})
|
||||
})
|
||||
9
packages/extension-collaboration-caret/vite.config.ts
Normal file
9
packages/extension-collaboration-caret/vite.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vite-plus'
|
||||
import { basePackConfig } from '../../pack.config.mjs'
|
||||
|
||||
export default defineConfig({
|
||||
pack: {
|
||||
entry: ['src/index.ts'],
|
||||
...basePackConfig(),
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user