@textbus/collaborate 2.0.0-alpha.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +10 -0
- package/bundles/collaborate.d.ts +15 -0
- package/bundles/collaborate.js +69 -0
- package/bundles/collaborate.js.map +1 -0
- package/bundles/local-to-remote.d.ts +3 -0
- package/bundles/local-to-remote.js +173 -0
- package/bundles/local-to-remote.js.map +1 -0
- package/bundles/public-api.d.ts +1 -0
- package/bundles/public-api.js +2 -0
- package/bundles/public-api.js.map +1 -0
- package/bundles/remote-to-local.d.ts +3 -0
- package/bundles/remote-to-local.js +143 -0
- package/bundles/remote-to-local.js.map +1 -0
- package/package.json +47 -0
- package/src/collaborate.ts +74 -0
- package/src/local-to-remote.ts +180 -0
- package/src/public-api.ts +1 -0
- package/src/remote-to-local.ts +140 -0
- package/tsconfig-build.json +26 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Action, Operation, SlotLiteral, ComponentLiteral, Format, FormatRange, FormatValue } from '@textbus/core'
|
|
2
|
+
import { Array as YArray, Map as YMap } from 'yjs'
|
|
3
|
+
|
|
4
|
+
export function localToRemote(operation: Operation, root: YArray<any>) {
|
|
5
|
+
const path = [...operation.path]
|
|
6
|
+
path.shift()
|
|
7
|
+
if (path.length) {
|
|
8
|
+
const componentIndex = path.shift()!
|
|
9
|
+
applyComponentOperationToSharedComponent(path, operation.apply, root.get(componentIndex))
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
insertContent(root, operation.apply)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function applyComponentOperationToSharedComponent(path: number[], actions: Action[], componentYMap: YMap<any>) {
|
|
16
|
+
const sharedSlots = componentYMap.get('slots') as YArray<any>
|
|
17
|
+
if (path.length) {
|
|
18
|
+
const slotIndex = path.shift()!
|
|
19
|
+
const sharedSlot = sharedSlots.get(slotIndex)
|
|
20
|
+
applySlotOperationToSharedSlot(path, actions, sharedSlot)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
let index: number
|
|
24
|
+
actions.forEach(action => {
|
|
25
|
+
switch (action.type) {
|
|
26
|
+
case 'retain':
|
|
27
|
+
index = action.index
|
|
28
|
+
break
|
|
29
|
+
case 'insertSlot':
|
|
30
|
+
sharedSlots.insert(index, [makeSharedSlotBySlotLiteral(action.slot)])
|
|
31
|
+
break
|
|
32
|
+
case 'apply':
|
|
33
|
+
componentYMap.set('state', action.value)
|
|
34
|
+
break
|
|
35
|
+
case 'delete':
|
|
36
|
+
sharedSlots.delete(index, action.count)
|
|
37
|
+
break
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function applySlotOperationToSharedSlot(path: number[], actions: Action[], slotYMap: YMap<any>) {
|
|
43
|
+
if (path.length) {
|
|
44
|
+
const componentIndex = path.shift()!
|
|
45
|
+
const sharedContent = slotYMap.get('content') as YArray<any>
|
|
46
|
+
const sharedComponent = sharedContent.get(componentIndex)
|
|
47
|
+
applyComponentOperationToSharedComponent(path, actions, sharedComponent)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
const content = slotYMap.get('content') as YArray<any>
|
|
51
|
+
|
|
52
|
+
let index: number
|
|
53
|
+
let len: number
|
|
54
|
+
actions.forEach(action => {
|
|
55
|
+
switch (action.type) {
|
|
56
|
+
case 'retain':
|
|
57
|
+
if (action.formats) {
|
|
58
|
+
mergeSharedFormats(index, action.index, action.formats, slotYMap)
|
|
59
|
+
}
|
|
60
|
+
index = action.index
|
|
61
|
+
break
|
|
62
|
+
case 'insert':
|
|
63
|
+
if (typeof action.content === 'string') {
|
|
64
|
+
len = action.content.length
|
|
65
|
+
content.insert(index, action.content.split(''))
|
|
66
|
+
} else {
|
|
67
|
+
len = 1
|
|
68
|
+
content.insert(index, [makeSharedComponentByComponentLiteral(action.content)])
|
|
69
|
+
}
|
|
70
|
+
if (action.formats) {
|
|
71
|
+
insertSharedFormats(index, len, action.formats, slotYMap)
|
|
72
|
+
}
|
|
73
|
+
break
|
|
74
|
+
case 'delete':
|
|
75
|
+
if (content.length === 0) {
|
|
76
|
+
// 当内容为空时,slot 实例内容为 ['\n'],触发删除会导致长度溢出
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
content.delete(index, action.count)
|
|
80
|
+
break
|
|
81
|
+
case 'apply':
|
|
82
|
+
slotYMap.set('state', action.value)
|
|
83
|
+
break
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function insertSharedFormats(index: number, distance: number, formats: Record<string, FormatValue>, sharedSlots: YMap<any>) {
|
|
89
|
+
const sharedFormats = sharedSlots.get('formats') as YMap<FormatRange[]>
|
|
90
|
+
const keys = Array.from(sharedFormats.keys())
|
|
91
|
+
const expandedValues = Array.from<string>({length: distance})
|
|
92
|
+
keys.forEach(key => {
|
|
93
|
+
const formatRanges = sharedFormats.get(key)!
|
|
94
|
+
const values = Format.tileRanges(formatRanges)
|
|
95
|
+
values.splice(index, 0, ...expandedValues)
|
|
96
|
+
const newRanges = Format.toRanges(values)
|
|
97
|
+
sharedFormats.set(key, newRanges)
|
|
98
|
+
})
|
|
99
|
+
mergeSharedFormats(index, index + distance, formats, sharedSlots)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function mergeSharedFormats(startIndex: number, endIndex: number, formats: Record<string, FormatValue>, sharedSlots: YMap<any>) {
|
|
103
|
+
const sharedFormats = sharedSlots.get('formats') as YMap<FormatRange[]>
|
|
104
|
+
Object.keys(formats).forEach(key => {
|
|
105
|
+
if (!sharedFormats.has(key)) {
|
|
106
|
+
sharedFormats.set(key, [{
|
|
107
|
+
startIndex,
|
|
108
|
+
endIndex,
|
|
109
|
+
value: formats[key]
|
|
110
|
+
}])
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const oldFormatRanges = sharedFormats.get(key)!
|
|
114
|
+
const formatRanges = Format.normalizeFormatRange(oldFormatRanges, {
|
|
115
|
+
startIndex,
|
|
116
|
+
endIndex,
|
|
117
|
+
value: formats[key]
|
|
118
|
+
})
|
|
119
|
+
sharedFormats.set(key, formatRanges)
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function insertContent(content: YArray<any>, actions: Action[]) {
|
|
124
|
+
let index: number
|
|
125
|
+
actions.forEach(action => {
|
|
126
|
+
switch (action.type) {
|
|
127
|
+
case 'retain':
|
|
128
|
+
index = action.index
|
|
129
|
+
break
|
|
130
|
+
case 'insert':
|
|
131
|
+
content.insert(index!, [
|
|
132
|
+
typeof action.content === 'string' ?
|
|
133
|
+
action.content :
|
|
134
|
+
makeSharedComponentByComponentLiteral(action.content)
|
|
135
|
+
])
|
|
136
|
+
if (action.formats) {
|
|
137
|
+
// TODO 根节点样式
|
|
138
|
+
}
|
|
139
|
+
break
|
|
140
|
+
case 'delete':
|
|
141
|
+
content.delete(index, action.count)
|
|
142
|
+
break
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function makeSharedSlotBySlotLiteral(slotLiteral: SlotLiteral): YMap<any> {
|
|
148
|
+
const content = new YArray()
|
|
149
|
+
let index = 0
|
|
150
|
+
slotLiteral.content.forEach(i => {
|
|
151
|
+
let size: number
|
|
152
|
+
if (typeof i === 'string') {
|
|
153
|
+
size = i.length
|
|
154
|
+
content.insert(index, [i])
|
|
155
|
+
} else {
|
|
156
|
+
size = 1
|
|
157
|
+
content.insert(index, [makeSharedComponentByComponentLiteral(i)])
|
|
158
|
+
}
|
|
159
|
+
index += size
|
|
160
|
+
})
|
|
161
|
+
const formats = new YMap()
|
|
162
|
+
const sharedSlot = new YMap()
|
|
163
|
+
sharedSlot.set('state', slotLiteral.state)
|
|
164
|
+
sharedSlot.set('content', content)
|
|
165
|
+
sharedSlot.set('schema', slotLiteral.schema)
|
|
166
|
+
sharedSlot.set('formats', formats)
|
|
167
|
+
return sharedSlot
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function makeSharedComponentByComponentLiteral(componentLiteral: ComponentLiteral): YMap<any> {
|
|
171
|
+
const slots = new YArray()
|
|
172
|
+
componentLiteral.slots.forEach(item => {
|
|
173
|
+
slots.push([makeSharedSlotBySlotLiteral(item)])
|
|
174
|
+
})
|
|
175
|
+
const sharedComponent = new YMap()
|
|
176
|
+
sharedComponent.set('name', componentLiteral.name)
|
|
177
|
+
sharedComponent.set('slots', slots)
|
|
178
|
+
sharedComponent.set('state', componentLiteral.state)
|
|
179
|
+
return sharedComponent
|
|
180
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './collaborate'
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Map as YMap, YArrayEvent, YEvent, YMapEvent } from 'yjs'
|
|
2
|
+
import { ComponentInstance, FormatterList, FormatType, Slot, Translator } from '@textbus/core'
|
|
3
|
+
|
|
4
|
+
type YPath = [number, string][]
|
|
5
|
+
|
|
6
|
+
export function remoteToLocal(events: YEvent[], slot: Slot, translator: Translator, formatterList: FormatterList) {
|
|
7
|
+
events.forEach(ev => {
|
|
8
|
+
const path: YPath = []
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < ev.path.length; i += 2) {
|
|
11
|
+
path.push(ev.path.slice(i, i + 2) as [number, string])
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (path.length) {
|
|
15
|
+
const componentIndex = path.shift()![0] as number
|
|
16
|
+
const component = slot.getContentAtIndex(componentIndex) as ComponentInstance
|
|
17
|
+
applySharedComponentToComponent(ev, path, component, translator, formatterList)
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
apply(ev, slot, translator)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function applySharedComponentToComponent(ev: YEvent, path: YPath, component: ComponentInstance, translator: Translator, formatterList: FormatterList,) {
|
|
26
|
+
if (path.length) {
|
|
27
|
+
const childPath = path.shift()!
|
|
28
|
+
const slot = component.slots.get(childPath[0])!
|
|
29
|
+
applySharedSlotToSlot(ev, path, slot, translator, formatterList, childPath[1] === 'formats')
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
if (ev instanceof YMapEvent) {
|
|
33
|
+
ev.keysChanged.forEach(key => {
|
|
34
|
+
if (key === 'state') {
|
|
35
|
+
const state = (ev.target as YMap<any>).get('state')
|
|
36
|
+
component.updateState(draft => {
|
|
37
|
+
Object.assign(draft, state)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
} else if (ev instanceof YArrayEvent) {
|
|
42
|
+
const slots = component.slots
|
|
43
|
+
ev.delta.forEach(action => {
|
|
44
|
+
if (Reflect.has(action, 'retain')) {
|
|
45
|
+
slots.retain(action.retain!)
|
|
46
|
+
} else if (action.insert) {
|
|
47
|
+
(action.insert as Array<any>).forEach(item => {
|
|
48
|
+
slots.insert(translator.createSlot(item.toJSON())!)
|
|
49
|
+
})
|
|
50
|
+
} else if (action.delete) {
|
|
51
|
+
slots.retain(slots.index)
|
|
52
|
+
slots.delete(action.delete)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function applySharedSlotToSlot(ev: YEvent, path: YPath, slot: Slot, translator: Translator, formatterList: FormatterList, isUpdateFormats: boolean) {
|
|
59
|
+
if (path.length) {
|
|
60
|
+
const componentIndex = path.shift()![0]
|
|
61
|
+
const component = slot.getContentAtIndex(componentIndex) as ComponentInstance
|
|
62
|
+
applySharedComponentToComponent(ev, path, component, translator, formatterList)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (ev instanceof YArrayEvent) {
|
|
67
|
+
ev.delta.forEach(action => {
|
|
68
|
+
if (Reflect.has(action, 'retain')) {
|
|
69
|
+
slot.retain(action.retain!)
|
|
70
|
+
} else if (action.insert) {
|
|
71
|
+
(action.insert as Array<any>).forEach(item => {
|
|
72
|
+
if (typeof item === 'string') {
|
|
73
|
+
slot.insert(item)
|
|
74
|
+
} else {
|
|
75
|
+
slot.insert(translator.createComponent(item.toJSON())!)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
} else if (action.delete) {
|
|
79
|
+
slot.retain(slot.index)
|
|
80
|
+
slot.delete(action.delete)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
} else if (ev instanceof YMapEvent) {
|
|
84
|
+
if (isUpdateFormats) {
|
|
85
|
+
const json = ev.target.toJSON()
|
|
86
|
+
ev.keysChanged.forEach(key => {
|
|
87
|
+
const formats = json[key]
|
|
88
|
+
const formatter = formatterList.get(key)!
|
|
89
|
+
if (formatter.type !== FormatType.Block) {
|
|
90
|
+
slot.applyFormat(formatter, {
|
|
91
|
+
startIndex: 0,
|
|
92
|
+
endIndex: slot.length,
|
|
93
|
+
value: null
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
formats.forEach(item => {
|
|
97
|
+
if (formatter.type === FormatType.Block) {
|
|
98
|
+
slot.applyFormat(formatter, item.value)
|
|
99
|
+
} else {
|
|
100
|
+
slot.applyFormat(formatter, item)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
} else {
|
|
105
|
+
ev.keysChanged.forEach(key => {
|
|
106
|
+
if (key === 'state') {
|
|
107
|
+
const state = (ev.target as YMap<any>).get('state')
|
|
108
|
+
slot.updateState(draft => {
|
|
109
|
+
Object.assign(draft, state)
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function apply(ev: YEvent, slot: Slot, translator: Translator) {
|
|
118
|
+
if (ev instanceof YArrayEvent) {
|
|
119
|
+
slot.retain(0)
|
|
120
|
+
const delta = ev.delta
|
|
121
|
+
delta.forEach(action => {
|
|
122
|
+
if (action.insert) {
|
|
123
|
+
(action.insert as Array<string | YMap<any>>).forEach(item => {
|
|
124
|
+
if (typeof item === 'string') {
|
|
125
|
+
slot.insert(item)
|
|
126
|
+
} else {
|
|
127
|
+
const json = item.toJSON()
|
|
128
|
+
const component = translator.createComponent(json)!
|
|
129
|
+
slot.insert(component)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
} else if (action.retain) {
|
|
133
|
+
slot.retain(action.retain)
|
|
134
|
+
} else if (action.delete) {
|
|
135
|
+
slot.retain(slot.index)
|
|
136
|
+
slot.delete(action.delete)
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"declaration": true,
|
|
4
|
+
"emitDecoratorMetadata": true,
|
|
5
|
+
"experimentalDecorators": true,
|
|
6
|
+
"allowSyntheticDefaultImports": true,
|
|
7
|
+
"lib": ["esnext", "dom"],
|
|
8
|
+
"target": "es6",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"module": "es2020",
|
|
11
|
+
"moduleResolution": "node",
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"noImplicitAny": false,
|
|
14
|
+
"suppressImplicitAnyIndexErrors": true,
|
|
15
|
+
"outDir": "bundles/",
|
|
16
|
+
"downlevelIteration": true,
|
|
17
|
+
"jsx": "react",
|
|
18
|
+
"jsxFactory": "VElement.createElement",
|
|
19
|
+
"paths": {
|
|
20
|
+
"@textbus/collaborate": ["./src/public-api.ts"],
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"include": [
|
|
24
|
+
"src/"
|
|
25
|
+
]
|
|
26
|
+
}
|