@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.
@@ -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
+ }