EZQMS-398: Update CollaborationDiffViewer (#4075)

* EZQMS-398: Update CollaborationDiffViewer

Signed-off-by: Anna No <anna.no@xored.com>

* EZQMS-398: Update CollaborationDiffViewer

Signed-off-by: Anna No <anna.no@xored.com>

* EZQMS-398: Update CollaborationDiffViewer

Signed-off-by: Anna No <anna.no@xored.com>

* EZQMS-398: Update CollaborationDiffViewer

Signed-off-by: Anna No <anna.no@xored.com>

---------

Signed-off-by: Anna No <anna.no@xored.com>
This commit is contained in:
Anna No 2023-11-28 12:35:15 +07:00 committed by GitHub
parent 7e1ad06c4d
commit 99a56ba60b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 192 additions and 163 deletions

View File

@ -16,9 +16,8 @@
-->
<script lang="ts">
import { onDestroy, setContext } from 'svelte'
import * as Y from 'yjs'
import { TiptapCollabProvider } from '../provider'
import { TiptapCollabProvider, createTiptapCollaborationData } from '../provider'
import { CollaborationIds } from '../types'
export let documentId: string
@ -36,18 +35,16 @@
if (provider !== undefined) {
provider.disconnect()
}
const ydoc: Y.Doc = new Y.Doc()
provider = new TiptapCollabProvider({
url: collaboratorURL,
name: documentId,
document: ydoc,
token,
parameters: {
initialContentId: initialContentId ?? ''
}
const data = createTiptapCollaborationData({
collaboratorURL,
documentId,
initialContentId,
token
})
setContext(CollaborationIds.Doc, ydoc)
provider = data.provider
setContext(CollaborationIds.Doc, data.ydoc)
setContext(CollaborationIds.Provider, provider)
provider.on('status', (event: any) => {
console.log('Collaboration:', documentId, event.status) // logs "connected" or "disconnected"
})

View File

@ -15,25 +15,24 @@
//
-->
<script lang="ts">
import { onDestroy, onMount } from 'svelte'
import { Doc as Ydoc } from 'yjs'
import { Editor, Extension, mergeAttributes } from '@tiptap/core'
import Collaboration from '@tiptap/extension-collaboration'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { DecorationSet } from '@tiptap/pm/view'
import { onDestroy, onMount } from 'svelte'
import { Markup } from '@hcengineering/core'
import { IconObjects, IconSize } from '@hcengineering/ui'
import textEditorPlugin from '../plugin'
import { calculateDecorations } from './diff/decorations'
import { calculateDecorations, createYdocDocument } from './diff/decorations'
import { defaultEditorAttributes } from './editor/editorProps'
import { defaultExtensions } from './extensions'
import StyleButton from './StyleButton.svelte'
export let content: Markup
export let buttonSize: IconSize = 'small'
export let comparedVersion: Markup | undefined = undefined
export let noButton: boolean = false
export let readonly = false
export let ydoc: Ydoc
export let field: string | undefined = undefined
export let comparedYdoc: Ydoc | undefined = undefined
export let comparedField: string | undefined = undefined
// export let mode: 'unified' = 'unified'
let element: HTMLElement
let editor: Editor
@ -41,8 +40,8 @@
let _decoration = DecorationSet.empty
let oldContent = ''
function updateEditor (editor?: Editor, comparedVersion?: Markup | ArrayBuffer): void {
const r = calculateDecorations(editor, oldContent, undefined, comparedVersion)
function updateEditor (editor: Editor, ydoc: Ydoc, field?: string): void {
const r = calculateDecorations(editor, oldContent, createYdocDocument(editor.schema, ydoc, field))
if (r !== undefined) {
oldContent = r.oldContent
_decoration = r.decorations
@ -50,8 +49,8 @@
}
const updateDecorations = () => {
if (editor?.schema) {
updateEditor(editor, comparedVersion)
if (editor?.schema && comparedYdoc) {
updateEditor(editor, comparedYdoc, comparedField)
}
}
@ -61,12 +60,9 @@
new Plugin({
key: new PluginKey('diffs'),
props: {
decorations (state) {
decorations () {
updateDecorations()
if (showDiff) {
return _decoration
}
return undefined
return _decoration
}
}
})
@ -74,21 +70,17 @@
}
})
$: updateEditor(editor, comparedVersion)
$: if (editor && comparedYdoc) {
updateEditor(editor, comparedYdoc, comparedField)
}
onMount(() => {
editor = new Editor({
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, { class: 'flex-grow' }) },
element,
content,
editable: true,
extensions: [...defaultExtensions, DecorationExtension],
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor
}
editable: false,
extensions: [...defaultExtensions, DecorationExtension, Collaboration.configure({ document: ydoc, field })]
})
editor.setEditable(!readonly)
})
onDestroy(() => {
@ -96,27 +88,9 @@
editor.destroy()
}
})
let showDiff = true
</script>
<div class="ref-container">
{#if comparedVersion !== undefined && !noButton}
<div class="flex">
<div class="flex-grow" />
<div class="formatPanel buttons-group xsmall-gap mb-4">
<StyleButton
icon={IconObjects}
size={buttonSize}
selected={showDiff}
showTooltip={{ label: textEditorPlugin.string.EnableDiffMode }}
on:click={() => {
showDiff = !showDiff
editor.chain().focus()
}}
/>
</div>
</div>
{/if}
<div class="textInput">
<div class="select-text" style="width: 100%;" bind:this={element} />
</div>

View File

@ -15,15 +15,14 @@
//
-->
<script lang="ts">
import { Markup, getCurrentAccount } from '@hcengineering/core'
import { getCurrentAccount } from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform'
import { IconObjects, IconSize, Loading, getPlatformColorForText, registerFocus, themeStore } from '@hcengineering/ui'
import { AnyExtension, Editor, Extension, FocusPosition, getMarkRange, mergeAttributes } from '@tiptap/core'
import { IconSize, Loading, getPlatformColorForText, registerFocus, themeStore } from '@hcengineering/ui'
import { AnyExtension, Editor, FocusPosition, getMarkRange, mergeAttributes } from '@tiptap/core'
import Collaboration, { isChangeOrigin } from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import Placeholder from '@tiptap/extension-placeholder'
import { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state'
import { DecorationSet } from '@tiptap/pm/view'
import { TextSelection } from '@tiptap/pm/state'
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
import * as Y from 'yjs'
@ -35,9 +34,7 @@
import { copyDocumentContent, copyDocumentField } from '../utils'
import ImageStyleToolbar from './ImageStyleToolbar.svelte'
import StyleButton from './StyleButton.svelte'
import TextEditorStyleToolbar from './TextEditorStyleToolbar.svelte'
import { calculateDecorations } from './diff/decorations'
import { noSelectionRender } from './editor/collaboration'
import { defaultEditorAttributes } from './editor/editorProps'
import { FileAttachFunction, ImageExtension } from './extension/imageExt'
@ -50,14 +47,13 @@
export let readonly = false
export let visible = true
export let token: string
export let collaboratorURL: string
export let token: string = ''
export let collaboratorURL: string = ''
export let buttonSize: IconSize = 'small'
export let focusable: boolean = false
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
export let initialContentId: string | undefined = undefined
export let comparedVersion: Markup | ArrayBuffer | undefined = undefined
export let field: string | undefined = undefined
@ -188,22 +184,6 @@
copyDocumentField(documentId, srcFieldId, dstFieldId, { provider }, initialContentId)
}
export function unregisterPlugin (nameOrPluginKey: string | PluginKey) {
if (!editor) {
return
}
editor.unregisterPlugin(nameOrPluginKey)
}
export function registerPlugin (plugin: Plugin) {
if (!editor) {
return
}
editor.registerPlugin(plugin)
}
let needFocus = false
let focused = false
let posFocus: FocusPosition | undefined = undefined
@ -225,44 +205,6 @@
editor.setEditable(!readonly)
}
let _decoration = DecorationSet.empty
let oldContent = ''
function updateEditor (editor?: Editor, field?: string, comparedVersion?: Markup | ArrayBuffer): void {
const r = calculateDecorations(editor, oldContent, field, comparedVersion)
if (r !== undefined) {
oldContent = r.oldContent
_decoration = r.decorations
}
}
const updateDecorations = () => {
if (editor?.schema) {
updateEditor(editor, field, comparedVersion)
}
}
const DecorationExtension = Extension.create({
addProseMirrorPlugins () {
return [
new Plugin({
key: new PluginKey('diffs'),
props: {
decorations (state) {
updateDecorations()
if (showDiff) {
return _decoration
}
return undefined
}
}
})
]
}
})
$: updateEditor(editor, field, comparedVersion)
$: if (editor) dispatch('editor', editor)
$: isStyleToolbarSupported = (!readonly || textNodeActions.length > 0) && canShowPopups
$: tippyOptions = {
@ -335,7 +277,6 @@
},
selectionRender: noSelectionRender
}),
DecorationExtension,
Completion.configure({
...completionConfig,
showDoc (event: MouseEvent, _id: string, _class: string) {
@ -385,8 +326,6 @@
}
})
let showDiff = true
export let focusIndex = -1
const { idx, focusManager } = registerFocus(focusIndex, {
focus: () => {
@ -421,27 +360,11 @@
{/if}
{#if visible}
{#if comparedVersion !== undefined || $$slots.tools}
{#if $$slots.tools}
<div class="ref-container" style:overflow>
{#if comparedVersion !== undefined}
<div class="flex-row-center buttons-group xsmall-gap">
<StyleButton
icon={IconObjects}
size={buttonSize}
selected={showDiff}
showTooltip={{ label: textEditorPlugin.string.EnableDiffMode }}
on:click={() => {
showDiff = !showDiff
editor.chain().focus()
}}
/>
<slot name="tools" />
</div>
{:else}
<div class="text-editor-toolbar buttons-group xsmall-gap">
<slot name="tools" />
</div>
{/if}
<div class="text-editor-toolbar buttons-group xsmall-gap">
<slot name="tools" />
</div>
</div>
{/if}

View File

@ -0,0 +1,100 @@
<!--
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
-->
<script lang="ts">
import { Editor, Extension, mergeAttributes } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { DecorationSet } from '@tiptap/pm/view'
import { onDestroy, onMount } from 'svelte'
import { Markup } from '@hcengineering/core'
import { calculateDecorations, createMarkupDocument } from './diff/decorations'
import { defaultEditorAttributes } from './editor/editorProps'
import { defaultExtensions } from './extensions'
export let content: Markup
export let comparedVersion: Markup | undefined = undefined
let element: HTMLElement
let editor: Editor
let _decoration = DecorationSet.empty
let oldContent = ''
function updateEditor (editor: Editor, comparedVersion?: Markup): void {
if (!comparedVersion) {
return
}
const r = calculateDecorations(editor, oldContent, createMarkupDocument(editor.schema, comparedVersion))
if (r !== undefined) {
oldContent = r.oldContent
_decoration = r.decorations
}
}
const updateDecorations = () => {
if (editor?.schema) {
updateEditor(editor, comparedVersion)
}
}
const DecorationExtension = Extension.create({
addProseMirrorPlugins () {
return [
new Plugin({
key: new PluginKey('diffs'),
props: {
decorations () {
updateDecorations()
return _decoration
}
}
})
]
}
})
$: if (editor && comparedVersion) {
updateEditor(editor, comparedVersion)
}
onMount(() => {
editor = new Editor({
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, { class: 'flex-grow' }) },
element,
content,
editable: false,
extensions: [...defaultExtensions, DecorationExtension],
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor
}
})
})
onDestroy(() => {
if (editor) {
editor.destroy()
}
})
</script>
<div class="ref-container">
<div class="textInput">
<div class="select-text" style="width: 100%;" bind:this={element} />
</div>
</div>

View File

@ -19,13 +19,26 @@ import { ChangeSet } from '@tiptap/pm/changeset'
import { DOMParser, type Node, type Schema } from '@tiptap/pm/model'
import { Decoration, DecorationSet } from '@tiptap/pm/view'
import { yDocToProsemirrorJSON } from 'y-prosemirror'
import { Doc, applyUpdate } from 'yjs'
import { Doc as Ydoc, applyUpdate } from 'yjs'
import { recreateTransform } from './recreate'
/**
* @public
*/
export function createDocument (schema: Schema, content: Markup | ArrayBuffer, field?: string): Node {
export function createYdocDocument (schema: Schema, ydoc: Ydoc, field?: string): Node {
try {
const body = yDocToProsemirrorJSON(ydoc, field)
return schema.nodeFromJSON(body)
} catch (err: any) {
console.error(err)
return schema.node(schema.topNodeType)
}
}
/**
* @public
*/
export function createMarkupDocument (schema: Schema, content: Markup | ArrayBuffer, field?: string): Node {
if (typeof content === 'string') {
const wrappedValue = `<body>${content}</body>`
@ -34,7 +47,7 @@ export function createDocument (schema: Schema, content: Markup | ArrayBuffer, f
return DOMParser.fromSchema(schema).parse(body)
} else {
try {
const ydoc = new Doc()
const ydoc = new Ydoc()
const uint8arr = new Uint8Array(content)
applyUpdate(ydoc, uint8arr)
@ -53,8 +66,7 @@ export function createDocument (schema: Schema, content: Markup | ArrayBuffer, f
export function calculateDecorations (
editor?: Editor,
oldContent?: string,
field?: string,
comparedVersion?: Markup | ArrayBuffer
comparedDoc?: Node
):
| {
decorations: DecorationSet
@ -65,11 +77,9 @@ export function calculateDecorations (
if (editor?.schema === undefined) {
return
}
if (comparedVersion === undefined) {
if (comparedDoc === undefined) {
return
}
const schema = editor.schema
const docOld = createDocument(schema, comparedVersion, field)
const docNew = editor.state.doc
const c = editor.getHTML()
@ -77,8 +87,8 @@ export function calculateDecorations (
return
}
const tr = recreateTransform(docOld, docNew)
const changeSet = ChangeSet.create(docOld).addSteps(tr.doc, tr.mapping.maps, undefined)
const tr = recreateTransform(comparedDoc, docNew)
const changeSet = ChangeSet.create(comparedDoc).addSteps(tr.doc, tr.mapping.maps, undefined)
const changes = changeSet.changes
const decorations: Decoration[] = []
@ -91,18 +101,18 @@ export function calculateDecorations (
function deleted (prob: any): any {
const icon = document.createElement('span')
icon.className = 'deletion'
icon.className = 'text-editor-highlighted-node-delete'
icon.innerText = prob
return icon
}
changes.forEach((change) => {
if (change.inserted.length > 0) {
decorations.push(Decoration.inline(change.fromB, change.toB, { class: 'diff insertion' }, {}))
decorations.push(Decoration.inline(change.fromB, change.toB, { class: 'text-editor-highlighted-node-add' }, {}))
decorations.push(Decoration.widget(change.fromB, lintIcon('add')))
}
if (change.deleted.length > 0) {
const cont = docOld.textBetween(change.fromA, change.toA)
const cont = comparedDoc.textBetween(change.fromA, change.toA)
decorations.push(Decoration.widget(change.fromB, deleted(cont)))
decorations.push(Decoration.widget(change.fromB, lintIcon('delete')))
}

View File

@ -21,6 +21,7 @@ export { default as Collaboration } from './components/Collaboration.svelte'
export { default as CollaborationDiffViewer } from './components/CollaborationDiffViewer.svelte'
export { default as CollaboratorEditor } from './components/CollaboratorEditor.svelte'
export { default as FullDescriptionBox } from './components/FullDescriptionBox.svelte'
export { default as MarkupDiffViewer } from './components/MarkupDiffViewer.svelte'
export { default as ReferenceInput } from './components/ReferenceInput.svelte'
export { default as StyleButton } from './components/StyleButton.svelte'
export { default as StyledTextArea } from './components/StyledTextArea.svelte'
@ -65,4 +66,7 @@ export {
export { ImageExtension, type ImageOptions } from './components/extension/imageExt'
export { TodoItemExtension, TodoListExtension } from './components/extension/todo'
export { TiptapCollabProvider, type TiptapCollabProviderConfiguration, createTiptapCollaborationData } from './provider'
export { CollaborationIds } from './types'
export { textEditorId }

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Doc as Ydoc } from 'yjs'
import { HocuspocusProvider, type HocuspocusProviderConfiguration } from '@hocuspocus/provider'
export type TiptapCollabProviderConfiguration = HocuspocusProviderConfiguration &
@ -50,3 +50,24 @@ export class TiptapCollabProvider extends HocuspocusProvider {
super.destroy()
}
}
export const createTiptapCollaborationData = (params: {
collaboratorURL: string
documentId: string
initialContentId: string | undefined
token: string
}): { provider: TiptapCollabProvider, ydoc: Ydoc } => {
const ydoc: Ydoc = new Ydoc()
return {
ydoc,
provider: new TiptapCollabProvider({
url: params.collaboratorURL,
name: params.documentId,
document: ydoc,
token: params.token,
parameters: {
initialContentId: params.initialContentId ?? ''
}
})
}
}

View File

@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import { CollaborationDiffViewer } from '@hcengineering/text-editor'
import { MarkupDiffViewer } from '@hcengineering/text-editor'
import { ShowMore } from '@hcengineering/ui'
export let value: string | undefined
@ -43,6 +43,6 @@
<ShowMore>
{#key [value, prevValue]}
<CollaborationDiffViewer content={value ?? ''} comparedVersion={prevValue ?? ''} noButton readonly />
<MarkupDiffViewer content={value ?? ''} comparedVersion={prevValue ?? ''} />
{/key}
</ShowMore>