@textbus/collaborate 2.0.0-alpha.38 → 2.0.0-alpha.39

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textbus/collaborate",
3
- "version": "2.0.0-alpha.38",
3
+ "version": "2.0.0-alpha.39",
4
4
  "description": "TextBus is a rich text editor and framework that is highly customizable and extensible to achieve rich wysiwyg effects.",
5
5
  "main": "./bundles/public-api.js",
6
6
  "module": "./bundles/public-api.js",
@@ -27,8 +27,8 @@
27
27
  "dependencies": {
28
28
  "@tanbo/di": "^1.0.5",
29
29
  "@tanbo/stream": "^0.0.12",
30
- "@textbus/browser": "^2.0.0-alpha.38",
31
- "@textbus/core": "^2.0.0-alpha.38",
30
+ "@textbus/browser": "^2.0.0-alpha.39",
31
+ "@textbus/core": "^2.0.0-alpha.39",
32
32
  "reflect-metadata": "^0.1.13",
33
33
  "y-protocols": "^1.0.5",
34
34
  "yjs": "^13.5.27"
@@ -44,5 +44,5 @@
44
44
  "bugs": {
45
45
  "url": "https://github.com/textbus/textbus.git/issues"
46
46
  },
47
- "gitHead": "ee9175a3ad0ecf1e3470bd36faa5a2b336df2cea"
47
+ "gitHead": "a3de1ffbf0b8527eff875cd415b5d8d16df91ecb"
48
48
  }
@@ -1,4 +1,3 @@
1
1
  export * from './collaborate-cursor'
2
- export * from './collaborate-history'
3
2
  export * from './local-to-remote'
4
3
  export * from './remote-to-local'
@@ -1,184 +1,158 @@
1
- import { Action, Operation, SlotLiteral, ComponentLiteral, Format, FormatRange, FormatValue } from '@textbus/core'
2
- import { Array as YArray, Map as YMap } from 'yjs'
1
+ import { Action, Operation, SlotLiteral, ComponentLiteral } from '@textbus/core'
2
+ import { Array as YArray, Map as YMap, Text as YText } from 'yjs'
3
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
4
+ export class LocalToRemote {
5
+ transform(operation: Operation, root: YText) {
6
+ const path = [...operation.path]
7
+ path.shift()
8
+ if (path.length) {
9
+ const componentIndex = path.shift()!
10
+ const sharedComponent = this.getSharedComponentByIndex(root, componentIndex)
11
+ if (sharedComponent) {
12
+ this.applyComponentOperationToSharedComponent(path, operation.apply, sharedComponent)
13
+ }
14
+ return
15
+ }
16
+ this.mergeActionsToSharedSlot(root, operation.apply)
11
17
  }
12
- insertContent(root, operation.apply)
13
- }
14
18
 
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
19
+ private applyComponentOperationToSharedComponent(path: number[], actions: Action[], componentYMap: YMap<any>) {
20
+ const sharedSlots = componentYMap.get('slots') as YArray<any>
21
+ if (path.length) {
22
+ const slotIndex = path.shift()!
23
+ const sharedSlot = sharedSlots.get(slotIndex)
24
+ this.applySlotOperationToSharedSlot(path, actions, sharedSlot)
25
+ return
26
+ }
27
+ let index: number
28
+ actions.forEach(action => {
29
+ switch (action.type) {
30
+ case 'retain':
31
+ index = action.offset
32
+ break
33
+ case 'insertSlot':
34
+ sharedSlots.insert(index, [this.makeSharedSlotBySlotLiteral(action.slot)])
35
+ index++
36
+ break
37
+ case 'apply':
38
+ componentYMap.set('state', action.value)
39
+ break
40
+ case 'delete':
41
+ sharedSlots.delete(index, action.count)
42
+ break
43
+ }
44
+ })
22
45
  }
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
46
+
47
+ private applySlotOperationToSharedSlot(path: number[], actions: Action[], slotYMap: YMap<any>) {
48
+ if (path.length) {
49
+ const componentIndex = path.shift()!
50
+ const sharedContent = slotYMap.get('content') as YText
51
+ const sharedComponent = this.getSharedComponentByIndex(sharedContent, componentIndex)!
52
+ this.applyComponentOperationToSharedComponent(path, actions, sharedComponent)
53
+ return
38
54
  }
39
- })
40
- }
55
+ const content = slotYMap.get('content') as YText
41
56
 
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
57
+ this.mergeActionsToSharedSlot(content, actions)
49
58
  }
50
- const content = slotYMap.get('content') as YArray<any>
51
59
 
52
- let index: number
53
- let len: number
54
- actions.forEach(action => {
55
- switch (action.type) {
56
- case 'retain':
60
+ private mergeActionsToSharedSlot(content: YText, actions: Action[]) {
61
+ let index: number
62
+ let length: number
63
+
64
+ actions.forEach(action => {
65
+ if (action.type === 'retain') {
57
66
  if (action.formats) {
58
- mergeSharedFormats(index, action.index, action.formats, slotYMap)
67
+ content.format(index, action.offset, action.formats)
68
+ } else {
69
+ index = action.offset
59
70
  }
60
- index = action.index
61
- break
62
- case 'insert':
71
+ } else if (action.type === 'insert') {
72
+ const delta = content.toDelta()
73
+ const isEmpty = delta.length === 1 && delta[0].insert === '\n'
74
+
63
75
  if (typeof action.content === 'string') {
64
- len = action.content.length
65
- content.insert(index, action.content.split(''))
76
+ length = action.content.length
77
+ content.insert(index, action.content)
66
78
  } else {
67
- len = 1
68
- content.insert(index, [makeSharedComponentByComponentLiteral(action.content)])
79
+ length = 1
80
+ content.insertEmbed(index, this.makeSharedComponentByComponentLiteral(action.content))
69
81
  }
70
82
  if (action.formats) {
71
- insertSharedFormats(index, len, action.formats, slotYMap)
83
+ content.format(index, length, action.formats)
72
84
  }
73
- break
74
- case 'delete':
75
- if (content.length === 0) {
76
- // 当内容为空时,slot 实例内容为 ['\n'],触发删除会导致长度溢出
77
- return
85
+ if (isEmpty && index === 0) {
86
+ content.delete(content.length - 1, 1)
78
87
  }
88
+ index += length
89
+ } else if (action.type === 'delete') {
90
+ const delta = content.toDelta()
79
91
  content.delete(index, action.count)
80
- break
81
- case 'apply':
82
- slotYMap.set('state', action.value)
83
- break
84
- }
85
- })
86
- }
92
+ if (content.length === 0) {
93
+ content.insert(0, '\n', delta[0]?.attributes)
94
+ }
95
+ } else if (action.type === 'apply') {
96
+ content.setAttribute('state', action.value)
97
+ }
98
+ })
99
+ }
87
100
 
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
+ private makeSharedSlotBySlotLiteral(slotLiteral: SlotLiteral): YMap<any> {
102
+ const content = new YText()
103
+ let index = 0
104
+ slotLiteral.content.forEach(i => {
105
+ let size: number
106
+ if (typeof i === 'string') {
107
+ size = i.length
108
+ content.insert(index, i)
109
+ } else {
110
+ size = 1
111
+ content.insertEmbed(index, this.makeSharedComponentByComponentLiteral(i))
112
+ }
113
+ index += size
114
+ })
115
+ const formats = slotLiteral.formats
116
+ Object.keys(formats).forEach(key => {
117
+ const formatRanges = formats[key]
118
+ formatRanges.forEach(formatRange => {
119
+ content.format(formatRange.startIndex, formatRange.endIndex - formatRange.startIndex, {
120
+ [key]: formatRange.value
121
+ })
122
+ })
123
+ })
101
124
 
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
- }
125
+ const sharedSlot = new YMap()
126
+ sharedSlot.set('content', content)
127
+ sharedSlot.set('schema', slotLiteral.schema)
128
+ return sharedSlot
129
+ }
112
130
 
113
- const oldFormatRanges = sharedFormats.get(key)!
114
- const formatRanges = Format.normalizeFormatRange(oldFormatRanges, {
115
- startIndex,
116
- endIndex,
117
- value: formats[key]
131
+ private makeSharedComponentByComponentLiteral(componentLiteral: ComponentLiteral): YMap<any> {
132
+ const slots = new YArray()
133
+ componentLiteral.slots.forEach(item => {
134
+ slots.push([this.makeSharedSlotBySlotLiteral(item)])
118
135
  })
119
- sharedFormats.set(key, formatRanges)
120
- })
121
- }
136
+ const sharedComponent = new YMap()
137
+ sharedComponent.set('name', componentLiteral.name)
138
+ sharedComponent.set('slots', slots)
139
+ sharedComponent.set('state', componentLiteral.state)
140
+ return sharedComponent
141
+ }
122
142
 
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 根节点样式
143
+ private getSharedComponentByIndex(host: YText, index: number): YMap<any> | null {
144
+ const delta = host.toDelta()
145
+ let i = 0
146
+ for (const action of delta) {
147
+ if (action.insert) {
148
+ if (i === index) {
149
+ return action.insert instanceof YMap ? action.insert : null
138
150
  }
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)])
151
+ i += action.insert instanceof YMap ? 1 : action.insert.length
152
+ } else {
153
+ throw new Error('xxx')
154
+ }
158
155
  }
159
- index += size
160
- })
161
- const formats = new YMap()
162
- Object.keys(slotLiteral.formats).forEach(key => {
163
- formats.set(key, slotLiteral.formats[key])
164
- })
165
-
166
- const sharedSlot = new YMap()
167
- sharedSlot.set('state', slotLiteral.state)
168
- sharedSlot.set('content', content)
169
- sharedSlot.set('schema', slotLiteral.schema)
170
- sharedSlot.set('formats', formats)
171
- return sharedSlot
172
- }
173
-
174
- function makeSharedComponentByComponentLiteral(componentLiteral: ComponentLiteral): YMap<any> {
175
- const slots = new YArray()
176
- componentLiteral.slots.forEach(item => {
177
- slots.push([makeSharedSlotBySlotLiteral(item)])
178
- })
179
- const sharedComponent = new YMap()
180
- sharedComponent.set('name', componentLiteral.name)
181
- sharedComponent.set('slots', slots)
182
- sharedComponent.set('state', componentLiteral.state)
183
- return sharedComponent
156
+ return null
157
+ }
184
158
  }
@@ -1,140 +1,151 @@
1
- import { Map as YMap, YArrayEvent, YEvent, YMapEvent } from 'yjs'
2
- import { ComponentInstance, FormatType, Registry, Slot, Translator } from '@textbus/core'
1
+ import { Map as YMap, YArrayEvent, YEvent, YMapEvent, Text as YText, YTextEvent, Array as YArray } from 'yjs'
2
+ import { ComponentInstance, ComponentLiteral, Registry, Slot, Translator } from '@textbus/core'
3
3
 
4
4
  type YPath = [number, string][]
5
5
 
6
- export function remoteToLocal(events: YEvent[], slot: Slot, translator: Translator, registry: Registry) {
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, registry)
18
- return
19
- }
6
+ export class RemoteToLocal {
7
+ constructor(private translator: Translator,
8
+ private registry: Registry) {
9
+ }
20
10
 
21
- apply(ev, slot, translator)
22
- })
23
- }
11
+ transform(events: YEvent[], slot: Slot) {
12
+ events.forEach(ev => {
13
+ const path: YPath = []
24
14
 
25
- function applySharedComponentToComponent(ev: YEvent, path: YPath, component: ComponentInstance, translator: Translator, registry: Registry,) {
26
- if (path.length) {
27
- const childPath = path.shift()!
28
- const slot = component.slots.get(childPath[0])!
29
- applySharedSlotToSlot(ev, path, slot, translator, registry, 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
- })
15
+ for (let i = 0; i < ev.path.length; i += 2) {
16
+ path.push(ev.path.slice(i, i + 2) as [number, string])
39
17
  }
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)
18
+
19
+ if (path.length) {
20
+ const componentIndex = path.shift()![0] as number
21
+ const component = slot.getContentAtIndex(componentIndex) as ComponentInstance
22
+ this.applySharedComponentToComponent(ev, path, component)
23
+ return
53
24
  }
54
- })
55
- }
56
- }
57
25
 
58
- function applySharedSlotToSlot(ev: YEvent, path: YPath, slot: Slot, translator: Translator, registry: Registry, 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, registry)
63
- return
26
+ this.applySharedSlotToSlot(ev, path, slot)
27
+ })
64
28
  }
65
29
 
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 = registry.getFormatter(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 {
30
+ private applySharedComponentToComponent(ev: YEvent, path: YPath, component: ComponentInstance) {
31
+ if (path.length) {
32
+ const childPath = path.shift()!
33
+ const slot = component.slots.get(childPath[0])!
34
+ this.applySharedSlotToSlot(ev, path, slot)
35
+ return
36
+ }
37
+ if (ev instanceof YMapEvent) {
105
38
  ev.keysChanged.forEach(key => {
106
39
  if (key === 'state') {
107
40
  const state = (ev.target as YMap<any>).get('state')
108
- slot.updateState(draft => {
41
+ component.updateState(draft => {
109
42
  Object.assign(draft, state)
110
43
  })
111
44
  }
112
45
  })
46
+ } else if (ev instanceof YArrayEvent) {
47
+ const slots = component.slots
48
+ ev.delta.forEach(action => {
49
+ if (Reflect.has(action, 'retain')) {
50
+ slots.retain(action.retain!)
51
+ } else if (action.insert) {
52
+ (action.insert as Array<any>).forEach(item => {
53
+ slots.insert(this.translator.createSlot(item.toJSON())!)
54
+ })
55
+ } else if (action.delete) {
56
+ slots.retain(slots.index)
57
+ slots.delete(action.delete)
58
+ }
59
+ })
113
60
  }
114
61
  }
115
- }
116
62
 
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)
63
+ private applySharedSlotToSlot(ev: YEvent, path: YPath, slot: Slot) {
64
+ if (path.length) {
65
+ path.shift()
66
+ const delta = (ev.target.parent as YText).toDelta()
67
+ let componentIndex = 0
68
+ for (let i = 0; i < delta.length; i++) {
69
+ const action = delta[i]
70
+ if (action.insert === ev.target) {
71
+ break
72
+ }
73
+ componentIndex += typeof action.insert === 'string' ? action.insert.length : 1
74
+ }
75
+ const component = slot.getContentAtIndex(componentIndex) as ComponentInstance
76
+ this.applySharedComponentToComponent(ev, path, component)
77
+ return
78
+ }
79
+
80
+ if (ev instanceof YTextEvent) {
81
+ slot.retain(0)
82
+ ev.delta.forEach(action => {
83
+ if (Reflect.has(action, 'retain')) {
84
+ if (action.attributes) {
85
+ slot.retain(action.retain!, Object.keys(action.attributes).map(key => {
86
+ return [this.registry.getFormatter(key)!, action.attributes![key]]
87
+ }))
88
+ }
89
+ slot.retain(action.retain!)
90
+ } else if (action.insert) {
91
+ if (typeof action.insert === 'string') {
92
+ slot.insert(action.insert, action.attributes ? Object.keys(action.attributes).map(key => {
93
+ return [this.registry.getFormatter(key)!, action.attributes![key]]
94
+ }) : [])
126
95
  } else {
127
- const json = item.toJSON()
128
- const component = translator.createComponent(json)!
96
+ const component = this.createComponentBySharedComponent(action.insert as YMap<any>)
129
97
  slot.insert(component)
130
98
  }
131
- })
132
- } else if (Reflect.has(action, 'retain')) {
133
- slot.retain(action.retain!)
134
- } else if (action.delete) {
135
- slot.retain(slot.index)
136
- slot.delete(action.delete)
137
- }
99
+ } else if (action.delete) {
100
+ slot.retain(slot.index)
101
+ slot.delete(action.delete)
102
+ } else if (action.attributes) {
103
+ slot.updateState(draft => {
104
+ Object.assign(draft, action.attributes)
105
+ })
106
+ }
107
+ })
108
+ } else {
109
+ throw new Error('xxx')
110
+ }
111
+ }
112
+
113
+ private createComponentBySharedComponent(yMap: YMap<any>): ComponentInstance {
114
+ const slots = yMap.get('slots') as YArray<YMap<any>>
115
+ const componentLiteral: ComponentLiteral = {
116
+ state: yMap.get('state'),
117
+ name: yMap.get('name'),
118
+ slots: slots.map(sharedSlot => {
119
+ return this.createSlotBySharedSlot(sharedSlot).toJSON()
120
+ })
121
+ }
122
+ return this.translator.createComponent(componentLiteral)!
123
+ }
124
+
125
+ private createSlotBySharedSlot(sharedSlot: YMap<any>): Slot {
126
+ const content = sharedSlot.get('content') as YText
127
+ const delta = content.toDelta()
128
+
129
+ const slot = this.translator.createSlot({
130
+ schema: sharedSlot.get('schema'),
131
+ state: sharedSlot.get('state'),
132
+ formats: {},
133
+ content: []
138
134
  })
135
+
136
+ for (const action of delta) {
137
+ if (action.insert) {
138
+ if (typeof action.insert === 'string') {
139
+ slot.insert(action.insert, action.attributes ? Object.keys(action.attributes).map(key => {
140
+ return [this.registry.getFormatter(key)!, action.attributes![key]]
141
+ }) : [])
142
+ } else {
143
+ slot.insert(this.createComponentBySharedComponent(action.insert))
144
+ }
145
+ } else {
146
+ throw new Error('xxx')
147
+ }
148
+ }
149
+ return slot
139
150
  }
140
151
  }