mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support incremental updates for textblock's delta. (#3216)
* feat: support incremental to update textblock's delta * fix: update test code * fix: remove console * fix: update test * feat: integrate increamental delta in Flutter * fix: delete quill editor * fix: delete quill editor * feat: add csharp in codeblock (#3371) * chore: pt-PT & pt-BR translation updated (#3353) * chore: Ensure Cargo.lock Is Updated Alongside Changes to Cargo.toml (#3361) * ci: add cargo check workflow * ci: test cargo.toml * fix: update test * fix: code review * fix: update cargo.toml and cargo.lock * fix: code review * fix: rust format --------- Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io> Co-authored-by: Mayur Mahajan <47064215+MayurSMahajan@users.noreply.github.com> Co-authored-by: Carlos Silva <mtbf99@gmail.com>
This commit is contained in:
parent
9565173baf
commit
c7af04b317
1
.github/workflows/tauri_ci.yaml
vendored
1
.github/workflows/tauri_ci.yaml
vendored
@ -94,6 +94,7 @@ jobs:
|
|||||||
mkdir dist
|
mkdir dist
|
||||||
pnpm install
|
pnpm install
|
||||||
cargo make --cwd .. tauri_build
|
cargo make --cwd .. tauri_build
|
||||||
|
pnpm test
|
||||||
pnpm test:errors
|
pnpm test:errors
|
||||||
|
|
||||||
- name: Check for uncommitted changes
|
- name: Check for uncommitted changes
|
||||||
|
@ -5,7 +5,6 @@ import 'package:appflowy/plugins/document/application/document_data_pb_extension
|
|||||||
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
|
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
|
||||||
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||||
import 'package:appflowy/user/application/user_service.dart';
|
import 'package:appflowy/user/application/user_service.dart';
|
||||||
import 'package:appflowy/util/json_print.dart';
|
|
||||||
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
|
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||||
@ -122,10 +121,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
/// subscribe to the document content change
|
/// subscribe to the document content change
|
||||||
void _onDocumentChanged() {
|
void _onDocumentChanged() {
|
||||||
_documentListener.start(
|
_documentListener.start(
|
||||||
didReceiveUpdate: (docEvent) {
|
didReceiveUpdate: syncDocumentDataPB,
|
||||||
// todo: integrate the document change to the editor
|
|
||||||
// prettyPrintJson(docEvent.toProto3Json());
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,10 +139,6 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initAppFlowyEditorState(DocumentDataPB data) async {
|
Future<void> _initAppFlowyEditorState(DocumentDataPB data) async {
|
||||||
if (kDebugMode) {
|
|
||||||
prettyPrintJson(data.toProto3Json());
|
|
||||||
}
|
|
||||||
|
|
||||||
final document = data.toDocument();
|
final document = data.toDocument();
|
||||||
if (document == null) {
|
if (document == null) {
|
||||||
assert(false, 'document is null');
|
assert(false, 'document is null');
|
||||||
@ -213,6 +205,24 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
await editorState.apply(transaction);
|
await editorState.apply(transaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void syncDocumentDataPB(DocEventPB docEvent) {
|
||||||
|
// prettyPrintJson(docEvent.toProto3Json());
|
||||||
|
// todo: integrate the document change to the editor
|
||||||
|
// for (final event in docEvent.events) {
|
||||||
|
// for (final blockEvent in event.event) {
|
||||||
|
// switch (blockEvent.command) {
|
||||||
|
// case DeltaTypePB.Inserted:
|
||||||
|
// break;
|
||||||
|
// case DeltaTypePB.Updated:
|
||||||
|
// break;
|
||||||
|
// case DeltaTypePB.Removed:
|
||||||
|
// break;
|
||||||
|
// default:
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
|
||||||
class DocumentService {
|
class DocumentService {
|
||||||
// unused now.
|
// unused now.
|
||||||
@ -46,4 +45,42 @@ class DocumentService {
|
|||||||
final result = await DocumentEventApplyAction(payload).send();
|
final result = await DocumentEventApplyAction(payload).send();
|
||||||
return result.swap();
|
return result.swap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new external text.
|
||||||
|
///
|
||||||
|
/// Normally, it's used to the block that needs sync long text.
|
||||||
|
///
|
||||||
|
/// the delta parameter is the json representation of the delta.
|
||||||
|
Future<Either<FlowyError, Unit>> createExternalText({
|
||||||
|
required String documentId,
|
||||||
|
required String textId,
|
||||||
|
String? delta,
|
||||||
|
}) async {
|
||||||
|
final payload = TextDeltaPayloadPB(
|
||||||
|
documentId: documentId,
|
||||||
|
textId: textId,
|
||||||
|
delta: delta,
|
||||||
|
);
|
||||||
|
final result = await DocumentEventCreateText(payload).send();
|
||||||
|
return result.swap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the external text.
|
||||||
|
///
|
||||||
|
/// this function is compatible with the [createExternalText] function.
|
||||||
|
///
|
||||||
|
/// the delta parameter is the json representation of the delta too.
|
||||||
|
Future<Either<FlowyError, Unit>> updateExternalText({
|
||||||
|
required String documentId,
|
||||||
|
required String textId,
|
||||||
|
String? delta,
|
||||||
|
}) async {
|
||||||
|
final payload = TextDeltaPayloadPB(
|
||||||
|
documentId: documentId,
|
||||||
|
textId: textId,
|
||||||
|
delta: delta,
|
||||||
|
);
|
||||||
|
final result = await DocumentEventApplyTextDeltaEvent(payload).send();
|
||||||
|
return result.swap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,27 @@ import 'dart:convert';
|
|||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||||
show Document, Node, Attributes, Delta, ParagraphBlockKeys, NodeIterator;
|
show
|
||||||
|
Document,
|
||||||
|
Node,
|
||||||
|
Attributes,
|
||||||
|
Delta,
|
||||||
|
ParagraphBlockKeys,
|
||||||
|
NodeIterator,
|
||||||
|
NodeExternalValues;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:nanoid/nanoid.dart';
|
import 'package:nanoid/nanoid.dart';
|
||||||
|
|
||||||
|
class ExternalValues extends NodeExternalValues {
|
||||||
|
const ExternalValues({
|
||||||
|
required this.externalId,
|
||||||
|
required this.externalType,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String externalId;
|
||||||
|
final String externalType;
|
||||||
|
}
|
||||||
|
|
||||||
extension DocumentDataPBFromTo on DocumentDataPB {
|
extension DocumentDataPBFromTo on DocumentDataPB {
|
||||||
static DocumentDataPB? fromDocument(Document document) {
|
static DocumentDataPB? fromDocument(Document document) {
|
||||||
final startNode = document.first;
|
final startNode = document.first;
|
||||||
@ -84,24 +101,51 @@ extension DocumentDataPBFromTo on DocumentDataPB {
|
|||||||
children.addAll(childrenIds.map((e) => buildNode(e)).whereNotNull());
|
children.addAll(childrenIds.map((e) => buildNode(e)).whereNotNull());
|
||||||
}
|
}
|
||||||
|
|
||||||
return block?.toNode(children: children);
|
return block?.toNode(
|
||||||
|
children: children,
|
||||||
|
meta: meta,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BlockToNode on BlockPB {
|
extension BlockToNode on BlockPB {
|
||||||
Node toNode({
|
Node toNode({
|
||||||
Iterable<Node>? children,
|
Iterable<Node>? children,
|
||||||
|
required MetaPB meta,
|
||||||
}) {
|
}) {
|
||||||
return Node(
|
final node = Node(
|
||||||
id: id,
|
id: id,
|
||||||
type: ty,
|
type: ty,
|
||||||
attributes: _dataAdapter(ty, data),
|
attributes: _dataAdapter(ty, data, meta),
|
||||||
children: children ?? [],
|
children: children ?? [],
|
||||||
);
|
);
|
||||||
|
node.externalValues = ExternalValues(
|
||||||
|
externalId: externalId,
|
||||||
|
externalType: externalType,
|
||||||
|
);
|
||||||
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
Attributes _dataAdapter(String ty, String data) {
|
Attributes _dataAdapter(String ty, String data, MetaPB meta) {
|
||||||
final map = Attributes.from(jsonDecode(data));
|
final map = Attributes.from(jsonDecode(data));
|
||||||
|
|
||||||
|
// it used in the delta case now.
|
||||||
|
final externalType = this.externalType;
|
||||||
|
final externalId = this.externalId;
|
||||||
|
if (externalType.isNotEmpty && externalId.isNotEmpty) {
|
||||||
|
// the 'text' type is the only type that is supported now.
|
||||||
|
if (externalType == 'text') {
|
||||||
|
final deltaString = meta.textMap[externalId];
|
||||||
|
if (deltaString != null) {
|
||||||
|
final delta = jsonDecode(deltaString);
|
||||||
|
map.putIfAbsent(
|
||||||
|
'delta',
|
||||||
|
() => delta,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final adapter = {
|
final adapter = {
|
||||||
ParagraphBlockKeys.type: (Attributes map) => map
|
ParagraphBlockKeys.type: (Attributes map) => map
|
||||||
..putIfAbsent(
|
..putIfAbsent(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/document/application/doc_service.dart';
|
import 'package:appflowy/plugins/document/application/doc_service.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||||
@ -15,6 +16,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'
|
|||||||
PathExtensions,
|
PathExtensions,
|
||||||
Node,
|
Node,
|
||||||
Path,
|
Path,
|
||||||
|
Delta,
|
||||||
composeAttributes;
|
composeAttributes;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:nanoid/nanoid.dart';
|
import 'package:nanoid/nanoid.dart';
|
||||||
@ -32,28 +34,66 @@ class TransactionAdapter {
|
|||||||
final DocumentService documentService;
|
final DocumentService documentService;
|
||||||
final String documentId;
|
final String documentId;
|
||||||
|
|
||||||
|
final bool _enableDebug = false;
|
||||||
|
|
||||||
Future<void> apply(Transaction transaction, EditorState editorState) async {
|
Future<void> apply(Transaction transaction, EditorState editorState) async {
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
Log.debug('transaction => ${transaction.toJson()}');
|
Log.debug('transaction => ${transaction.toJson()}');
|
||||||
final actions = transaction.operations
|
final actions = transaction.operations
|
||||||
.map((op) => op.toBlockAction(editorState))
|
.map((op) => op.toBlockAction(editorState, documentId))
|
||||||
.whereNotNull()
|
.whereNotNull()
|
||||||
.expand((element) => element)
|
.expand((element) => element)
|
||||||
.toList(growable: false); // avoid lazy evaluation
|
.toList(growable: false); // avoid lazy evaluation
|
||||||
Log.debug('actions => $actions');
|
final textActions = actions.where(
|
||||||
|
(e) =>
|
||||||
|
e.textDeltaType != TextDeltaType.none && e.textDeltaPayloadPB != null,
|
||||||
|
);
|
||||||
|
final actionCostTime = stopwatch.elapsedMilliseconds;
|
||||||
|
for (final textAction in textActions) {
|
||||||
|
final payload = textAction.textDeltaPayloadPB!;
|
||||||
|
final type = textAction.textDeltaType;
|
||||||
|
if (type == TextDeltaType.create) {
|
||||||
|
await documentService.createExternalText(
|
||||||
|
documentId: payload.documentId,
|
||||||
|
textId: payload.textId,
|
||||||
|
delta: payload.delta,
|
||||||
|
);
|
||||||
|
Log.debug('create external text: ${payload.delta}');
|
||||||
|
} else if (type == TextDeltaType.update) {
|
||||||
|
await documentService.updateExternalText(
|
||||||
|
documentId: payload.documentId,
|
||||||
|
textId: payload.textId,
|
||||||
|
delta: payload.delta,
|
||||||
|
);
|
||||||
|
Log.debug('update external text: ${payload.delta}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final blockActions =
|
||||||
|
actions.map((e) => e.blockActionPB).toList(growable: false);
|
||||||
await documentService.applyAction(
|
await documentService.applyAction(
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
actions: actions,
|
actions: blockActions,
|
||||||
);
|
);
|
||||||
|
final elapsed = stopwatch.elapsedMilliseconds;
|
||||||
|
stopwatch.stop();
|
||||||
|
if (_enableDebug) {
|
||||||
|
Log.debug(
|
||||||
|
'apply transaction cost: total $elapsed ms, converter action $actionCostTime ms, apply action ${elapsed - actionCostTime} ms',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BlockAction on Operation {
|
extension BlockAction on Operation {
|
||||||
List<BlockActionPB> toBlockAction(EditorState editorState) {
|
List<BlockActionWrapper> toBlockAction(
|
||||||
|
EditorState editorState,
|
||||||
|
String documentId,
|
||||||
|
) {
|
||||||
final op = this;
|
final op = this;
|
||||||
if (op is InsertOperation) {
|
if (op is InsertOperation) {
|
||||||
return op.toBlockAction(editorState);
|
return op.toBlockAction(editorState, documentId);
|
||||||
} else if (op is UpdateOperation) {
|
} else if (op is UpdateOperation) {
|
||||||
return op.toBlockAction(editorState);
|
return op.toBlockAction(editorState, documentId);
|
||||||
} else if (op is DeleteOperation) {
|
} else if (op is DeleteOperation) {
|
||||||
return op.toBlockAction(editorState);
|
return op.toBlockAction(editorState);
|
||||||
}
|
}
|
||||||
@ -62,12 +102,13 @@ extension BlockAction on Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension on InsertOperation {
|
extension on InsertOperation {
|
||||||
List<BlockActionPB> toBlockAction(
|
List<BlockActionWrapper> toBlockAction(
|
||||||
EditorState editorState, {
|
EditorState editorState,
|
||||||
|
String documentId, {
|
||||||
Node? previousNode,
|
Node? previousNode,
|
||||||
}) {
|
}) {
|
||||||
Path currentPath = path;
|
Path currentPath = path;
|
||||||
final List<BlockActionPB> actions = [];
|
final List<BlockActionWrapper> actions = [];
|
||||||
for (final node in nodes) {
|
for (final node in nodes) {
|
||||||
final parentId = node.parent?.id ??
|
final parentId = node.parent?.id ??
|
||||||
editorState.getNodeAtPath(currentPath.parent)?.id ??
|
editorState.getNodeAtPath(currentPath.parent)?.id ??
|
||||||
@ -82,22 +123,58 @@ extension on InsertOperation {
|
|||||||
} else {
|
} else {
|
||||||
assert(prevId.isNotEmpty && prevId != node.id);
|
assert(prevId.isNotEmpty && prevId != node.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create the external text if the node contains the delta in its data.
|
||||||
|
final delta = node.delta;
|
||||||
|
TextDeltaPayloadPB? textDeltaPayloadPB;
|
||||||
|
if (delta != null) {
|
||||||
|
final textId = nanoid(6);
|
||||||
|
|
||||||
|
textDeltaPayloadPB = TextDeltaPayloadPB(
|
||||||
|
documentId: documentId,
|
||||||
|
textId: textId,
|
||||||
|
delta: jsonEncode(node.delta!.toJson()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// sync the text id to the node
|
||||||
|
node.externalValues = ExternalValues(
|
||||||
|
externalId: textId,
|
||||||
|
externalType: 'text',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the delta from the data when the incremental update is stable.
|
||||||
final payload = BlockActionPayloadPB()
|
final payload = BlockActionPayloadPB()
|
||||||
..block = node.toBlock(childrenId: nanoid(10))
|
..block = node.toBlock(childrenId: nanoid(6))
|
||||||
..parentId = parentId
|
..parentId = parentId
|
||||||
..prevId = prevId;
|
..prevId = prevId;
|
||||||
|
|
||||||
|
// pass the external text id to the payload.
|
||||||
|
if (textDeltaPayloadPB != null) {
|
||||||
|
payload.textId = textDeltaPayloadPB.textId;
|
||||||
|
}
|
||||||
|
|
||||||
assert(payload.block.childrenId.isNotEmpty);
|
assert(payload.block.childrenId.isNotEmpty);
|
||||||
|
final blockActionPB = BlockActionPB()
|
||||||
|
..action = BlockActionTypePB.Insert
|
||||||
|
..payload = payload;
|
||||||
|
|
||||||
actions.add(
|
actions.add(
|
||||||
BlockActionPB()
|
BlockActionWrapper(
|
||||||
..action = BlockActionTypePB.Insert
|
blockActionPB: blockActionPB,
|
||||||
..payload = payload,
|
textDeltaPayloadPB: textDeltaPayloadPB,
|
||||||
|
textDeltaType: TextDeltaType.create,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (node.children.isNotEmpty) {
|
if (node.children.isNotEmpty) {
|
||||||
Node? prevChild;
|
Node? prevChild;
|
||||||
for (final child in node.children) {
|
for (final child in node.children) {
|
||||||
actions.addAll(
|
actions.addAll(
|
||||||
InsertOperation(currentPath + child.path, [child])
|
InsertOperation(currentPath + child.path, [child]).toBlockAction(
|
||||||
.toBlockAction(editorState, previousNode: prevChild),
|
editorState,
|
||||||
|
documentId,
|
||||||
|
previousNode: prevChild,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
prevChild = child;
|
prevChild = child;
|
||||||
}
|
}
|
||||||
@ -110,8 +187,11 @@ extension on InsertOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension on UpdateOperation {
|
extension on UpdateOperation {
|
||||||
List<BlockActionPB> toBlockAction(EditorState editorState) {
|
List<BlockActionWrapper> toBlockAction(
|
||||||
final List<BlockActionPB> actions = [];
|
EditorState editorState,
|
||||||
|
String documentId,
|
||||||
|
) {
|
||||||
|
final List<BlockActionWrapper> actions = [];
|
||||||
|
|
||||||
// if the attributes are both empty, we don't need to update
|
// if the attributes are both empty, we don't need to update
|
||||||
if (const DeepCollectionEquality().equals(attributes, oldAttributes)) {
|
if (const DeepCollectionEquality().equals(attributes, oldAttributes)) {
|
||||||
@ -125,23 +205,74 @@ extension on UpdateOperation {
|
|||||||
final parentId =
|
final parentId =
|
||||||
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
||||||
assert(parentId.isNotEmpty);
|
assert(parentId.isNotEmpty);
|
||||||
|
|
||||||
|
// create the external text if the node contains the delta in its data.
|
||||||
|
final prevDelta = oldAttributes['delta'];
|
||||||
|
final delta = attributes['delta'];
|
||||||
|
final diff = prevDelta != null && delta != null
|
||||||
|
? Delta.fromJson(prevDelta).diff(
|
||||||
|
Delta.fromJson(delta),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
final payload = BlockActionPayloadPB()
|
final payload = BlockActionPayloadPB()
|
||||||
..block = node.toBlock(
|
..block = node.toBlock(
|
||||||
parentId: parentId,
|
parentId: parentId,
|
||||||
attributes: composeAttributes(oldAttributes, attributes),
|
attributes: composeAttributes(oldAttributes, attributes),
|
||||||
)
|
)
|
||||||
..parentId = parentId;
|
..parentId = parentId;
|
||||||
actions.add(
|
final blockActionPB = BlockActionPB()
|
||||||
BlockActionPB()
|
..action = BlockActionTypePB.Update
|
||||||
..action = BlockActionTypePB.Update
|
..payload = payload;
|
||||||
..payload = payload,
|
|
||||||
);
|
final textId = (node.externalValues as ExternalValues?)?.externalId;
|
||||||
|
if (textId == null || textId.isEmpty) {
|
||||||
|
// to be compatible with the old version, we create a new text id if the text id is empty.
|
||||||
|
final textId = nanoid(6);
|
||||||
|
final textDeltaPayloadPB = delta == null
|
||||||
|
? null
|
||||||
|
: TextDeltaPayloadPB(
|
||||||
|
documentId: documentId,
|
||||||
|
textId: textId,
|
||||||
|
delta: jsonEncode(delta),
|
||||||
|
);
|
||||||
|
|
||||||
|
node.externalValues = ExternalValues(
|
||||||
|
externalId: textId,
|
||||||
|
externalType: 'text',
|
||||||
|
);
|
||||||
|
|
||||||
|
actions.add(
|
||||||
|
BlockActionWrapper(
|
||||||
|
blockActionPB: blockActionPB,
|
||||||
|
textDeltaPayloadPB: textDeltaPayloadPB,
|
||||||
|
textDeltaType: TextDeltaType.create,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final textDeltaPayloadPB = delta == null
|
||||||
|
? null
|
||||||
|
: TextDeltaPayloadPB(
|
||||||
|
documentId: documentId,
|
||||||
|
textId: textId,
|
||||||
|
delta: jsonEncode(diff),
|
||||||
|
);
|
||||||
|
|
||||||
|
actions.add(
|
||||||
|
BlockActionWrapper(
|
||||||
|
blockActionPB: blockActionPB,
|
||||||
|
textDeltaPayloadPB: textDeltaPayloadPB,
|
||||||
|
textDeltaType: TextDeltaType.update,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on DeleteOperation {
|
extension on DeleteOperation {
|
||||||
List<BlockActionPB> toBlockAction(EditorState editorState) {
|
List<BlockActionWrapper> toBlockAction(EditorState editorState) {
|
||||||
final List<BlockActionPB> actions = [];
|
final List<BlockActionPB> actions = [];
|
||||||
for (final node in nodes) {
|
for (final node in nodes) {
|
||||||
final parentId =
|
final parentId =
|
||||||
@ -158,6 +289,26 @@ extension on DeleteOperation {
|
|||||||
..payload = payload,
|
..payload = payload,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return actions;
|
return actions
|
||||||
|
.map((e) => BlockActionWrapper(blockActionPB: e))
|
||||||
|
.toList(growable: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TextDeltaType {
|
||||||
|
none,
|
||||||
|
create,
|
||||||
|
update,
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlockActionWrapper {
|
||||||
|
BlockActionWrapper({
|
||||||
|
required this.blockActionPB,
|
||||||
|
this.textDeltaType = TextDeltaType.none,
|
||||||
|
this.textDeltaPayloadPB,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BlockActionPB blockActionPB;
|
||||||
|
final TextDeltaPayloadPB? textDeltaPayloadPB;
|
||||||
|
final TextDeltaType textDeltaType;
|
||||||
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// import 'dart:convert';
|
import 'dart:convert';
|
||||||
// import 'package:appflowy_backend/log.dart';
|
|
||||||
// const JsonEncoder _encoder = JsonEncoder.withIndent(' ');
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
|
||||||
|
const JsonEncoder _encoder = JsonEncoder.withIndent(' ');
|
||||||
void prettyPrintJson(Object? object) {
|
void prettyPrintJson(Object? object) {
|
||||||
// Log.trace(_encoder.convert(object));
|
Log.trace(_encoder.convert(object));
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,6 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
||||||
ref: a183c57
|
ref: a183c57
|
||||||
# appflowy_editor: 1.2.3
|
|
||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('TransactionAdapter', () {
|
group('TransactionAdapter', () {
|
||||||
@ -24,81 +24,81 @@ void main() {
|
|||||||
expect(transaction.operations.length, 1);
|
expect(transaction.operations.length, 1);
|
||||||
expect(transaction.operations[0] is InsertOperation, true);
|
expect(transaction.operations[0] is InsertOperation, true);
|
||||||
|
|
||||||
final actions = transaction.operations[0].toBlockAction(editorState);
|
final actions = transaction.operations[0].toBlockAction(editorState, '');
|
||||||
|
|
||||||
expect(actions.length, 7);
|
expect(actions.length, 7);
|
||||||
for (final action in actions) {
|
for (final action in actions) {
|
||||||
expect(action.action, BlockActionTypePB.Insert);
|
expect(action.blockActionPB.action, BlockActionTypePB.Insert);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
actions[0].payload.parentId,
|
actions[0].blockActionPB.payload.parentId,
|
||||||
editorState.document.root.id,
|
editorState.document.root.id,
|
||||||
reason: '0 - parent id',
|
reason: '0 - parent id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[0].payload.prevId,
|
actions[0].blockActionPB.payload.prevId,
|
||||||
editorState.document.root.children.first.id,
|
editorState.document.root.children.first.id,
|
||||||
reason: '0 - prev id',
|
reason: '0 - prev id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[1].payload.parentId,
|
actions[1].blockActionPB.payload.parentId,
|
||||||
actions[0].payload.block.id,
|
actions[0].blockActionPB.payload.block.id,
|
||||||
reason: '1 - parent id',
|
reason: '1 - parent id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[1].payload.prevId,
|
actions[1].blockActionPB.payload.prevId,
|
||||||
'',
|
'',
|
||||||
reason: '1 - prev id',
|
reason: '1 - prev id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[2].payload.parentId,
|
actions[2].blockActionPB.payload.parentId,
|
||||||
actions[1].payload.block.id,
|
actions[1].blockActionPB.payload.block.id,
|
||||||
reason: '2 - parent id',
|
reason: '2 - parent id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[2].payload.prevId,
|
actions[2].blockActionPB.payload.prevId,
|
||||||
'',
|
'',
|
||||||
reason: '2 - prev id',
|
reason: '2 - prev id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[3].payload.parentId,
|
actions[3].blockActionPB.payload.parentId,
|
||||||
actions[0].payload.block.id,
|
actions[0].blockActionPB.payload.block.id,
|
||||||
reason: '3 - parent id',
|
reason: '3 - parent id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[3].payload.prevId,
|
actions[3].blockActionPB.payload.prevId,
|
||||||
actions[1].payload.block.id,
|
actions[1].blockActionPB.payload.block.id,
|
||||||
reason: '3 - prev id',
|
reason: '3 - prev id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[4].payload.parentId,
|
actions[4].blockActionPB.payload.parentId,
|
||||||
actions[0].payload.block.id,
|
actions[0].blockActionPB.payload.block.id,
|
||||||
reason: '4 - parent id',
|
reason: '4 - parent id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[4].payload.prevId,
|
actions[4].blockActionPB.payload.prevId,
|
||||||
actions[3].payload.block.id,
|
actions[3].blockActionPB.payload.block.id,
|
||||||
reason: '4 - prev id',
|
reason: '4 - prev id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[5].payload.parentId,
|
actions[5].blockActionPB.payload.parentId,
|
||||||
actions[4].payload.block.id,
|
actions[4].blockActionPB.payload.block.id,
|
||||||
reason: '5 - parent id',
|
reason: '5 - parent id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[5].payload.prevId,
|
actions[5].blockActionPB.payload.prevId,
|
||||||
'',
|
'',
|
||||||
reason: '5 - prev id',
|
reason: '5 - prev id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[6].payload.parentId,
|
actions[6].blockActionPB.payload.parentId,
|
||||||
actions[0].payload.block.id,
|
actions[0].blockActionPB.payload.block.id,
|
||||||
reason: '6 - parent id',
|
reason: '6 - parent id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[6].payload.prevId,
|
actions[6].blockActionPB.payload.prevId,
|
||||||
actions[4].payload.block.id,
|
actions[4].blockActionPB.payload.block.id,
|
||||||
reason: '6 - prev id',
|
reason: '6 - prev id',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -120,31 +120,31 @@ void main() {
|
|||||||
expect(transaction.operations.length, 1);
|
expect(transaction.operations.length, 1);
|
||||||
expect(transaction.operations[0] is InsertOperation, true);
|
expect(transaction.operations[0] is InsertOperation, true);
|
||||||
|
|
||||||
final actions = transaction.operations[0].toBlockAction(editorState);
|
final actions = transaction.operations[0].toBlockAction(editorState, '');
|
||||||
|
|
||||||
expect(actions.length, 2);
|
expect(actions.length, 2);
|
||||||
for (final action in actions) {
|
for (final action in actions) {
|
||||||
expect(action.action, BlockActionTypePB.Insert);
|
expect(action.blockActionPB.action, BlockActionTypePB.Insert);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
actions[0].payload.parentId,
|
actions[0].blockActionPB.payload.parentId,
|
||||||
editorState.document.root.children.first.id,
|
editorState.document.root.children.first.id,
|
||||||
reason: '0 - parent id',
|
reason: '0 - parent id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[0].payload.prevId,
|
actions[0].blockActionPB.payload.prevId,
|
||||||
'',
|
'',
|
||||||
reason: '0 - prev id',
|
reason: '0 - prev id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[1].payload.parentId,
|
actions[1].blockActionPB.payload.parentId,
|
||||||
editorState.document.root.children.first.id,
|
editorState.document.root.children.first.id,
|
||||||
reason: '1 - parent id',
|
reason: '1 - parent id',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
actions[1].payload.prevId,
|
actions[1].blockActionPB.payload.prevId,
|
||||||
actions[0].payload.block.id,
|
actions[0].blockActionPB.payload.block.id,
|
||||||
reason: '1 - prev id',
|
reason: '1 - prev id',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
4
frontend/appflowy_tauri/.gitignore
vendored
4
frontend/appflowy_tauri/.gitignore
vendored
@ -25,4 +25,6 @@ dist-ssr
|
|||||||
|
|
||||||
**/src/services/backend/models/
|
**/src/services/backend/models/
|
||||||
**/src/services/backend/events/
|
**/src/services/backend/events/
|
||||||
**/src/appflowy_app/i18n/translations/
|
**/src/appflowy_app/i18n/translations/
|
||||||
|
|
||||||
|
coverage
|
18
frontend/appflowy_tauri/jest.config.cjs
Normal file
18
frontend/appflowy_tauri/jest.config.cjs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const { compilerOptions } = require('./tsconfig.json');
|
||||||
|
const { pathsToModuleNameMapper } = require("ts-jest");
|
||||||
|
|
||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
roots: ['<rootDir>'],
|
||||||
|
modulePaths: [compilerOptions.baseUrl],
|
||||||
|
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths),
|
||||||
|
"transform": {
|
||||||
|
"(.*)/node_modules/nanoid/.+\\.(j|t)sx?$": "ts-jest"
|
||||||
|
},
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"node_modules/(?!nanoid/.*)"
|
||||||
|
],
|
||||||
|
"testRegex": "(/__tests__/.*\.(test|spec))\\.(jsx?|tsx?)$",
|
||||||
|
};
|
@ -14,7 +14,8 @@
|
|||||||
"tauri:clean": "cargo make --cwd .. tauri_clean",
|
"tauri:clean": "cargo make --cwd .. tauri_clean",
|
||||||
"tauri:dev": "pnpm sync:i18n && tauri dev",
|
"tauri:dev": "pnpm sync:i18n && tauri dev",
|
||||||
"sync:i18n": "node scripts/i18n/index.cjs",
|
"sync:i18n": "node scripts/i18n/index.cjs",
|
||||||
"css:variables": "node style-dictionary/config.cjs"
|
"css:variables": "node style-dictionary/config.cjs",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emoji-mart/data": "^1.1.2",
|
"@emoji-mart/data": "^1.1.2",
|
||||||
@ -70,6 +71,7 @@
|
|||||||
"@tauri-apps/cli": "^1.2.2",
|
"@tauri-apps/cli": "^1.2.2",
|
||||||
"@types/google-protobuf": "^3.15.6",
|
"@types/google-protobuf": "^3.15.6",
|
||||||
"@types/is-hotkey": "^0.1.7",
|
"@types/is-hotkey": "^0.1.7",
|
||||||
|
"@types/jest": "^29.5.3",
|
||||||
"@types/katex": "^0.16.0",
|
"@types/katex": "^0.16.0",
|
||||||
"@types/node": "^18.7.10",
|
"@types/node": "^18.7.10",
|
||||||
"@types/prismjs": "^1.26.0",
|
"@types/prismjs": "^1.26.0",
|
||||||
@ -86,17 +88,22 @@
|
|||||||
"@typescript-eslint/parser": "^5.51.0",
|
"@typescript-eslint/parser": "^5.51.0",
|
||||||
"@vitejs/plugin-react": "^3.0.0",
|
"@vitejs/plugin-react": "^3.0.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
|
"babel-jest": "^29.6.2",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"jest-environment-jsdom": "^29.6.2",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"prettier": "2.8.4",
|
"prettier": "2.8.4",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||||
"style-dictionary": "^3.8.0",
|
"style-dictionary": "^3.8.0",
|
||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.2.7",
|
||||||
|
"ts-jest": "^29.1.1",
|
||||||
|
"ts-node-dev": "^2.0.0",
|
||||||
|
"tsconfig-paths-jest": "^0.0.1",
|
||||||
"typescript": "^4.6.4",
|
"typescript": "^4.6.4",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"vite": "^4.0.0",
|
"vite": "^4.0.0",
|
||||||
"vite-plugin-svgr": "^3.2.0"
|
"vite-plugin-svgr": "^3.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
40
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
40
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -140,7 +140,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "appflowy-integrate"
|
name = "appflowy-integrate"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -729,7 +729,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -748,7 +748,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-database"
|
name = "collab-database"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -777,7 +777,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-define"
|
name = "collab-define"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -789,7 +789,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-derive"
|
name = "collab-derive"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -801,12 +801,13 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-document"
|
name = "collab-document"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
"collab-derive",
|
"collab-derive",
|
||||||
"collab-persistence",
|
"collab-persistence",
|
||||||
|
"lib0",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"serde",
|
"serde",
|
||||||
@ -820,7 +821,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-folder"
|
name = "collab-folder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -840,7 +841,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-persistence"
|
name = "collab-persistence"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bincode",
|
"bincode",
|
||||||
@ -861,16 +862,17 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-plugins"
|
name = "collab-plugins"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"collab",
|
"collab",
|
||||||
"collab-define",
|
"collab-define",
|
||||||
"collab-persistence",
|
"collab-persistence",
|
||||||
"collab-sync",
|
"collab-sync-protocol",
|
||||||
"collab-ws",
|
"collab-ws",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"lib0",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
@ -887,23 +889,15 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-sync"
|
name = "collab-sync-protocol"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab",
|
"collab",
|
||||||
"futures-util",
|
|
||||||
"lib0",
|
|
||||||
"md5",
|
"md5",
|
||||||
"parking_lot",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
"tokio-stream",
|
|
||||||
"tokio-util",
|
|
||||||
"tracing",
|
|
||||||
"y-sync",
|
"y-sync",
|
||||||
"yrs",
|
"yrs",
|
||||||
]
|
]
|
||||||
@ -911,7 +905,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-user"
|
name = "collab-user"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -927,10 +921,10 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-ws"
|
name = "collab-ws"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab-sync",
|
"collab-sync-protocol",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -34,15 +34,15 @@ default = ["custom-protocol"]
|
|||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
|
|
||||||
#collab = { path = "../../../../AppFlowy-Collab/collab" }
|
#collab = { path = "../../../../AppFlowy-Collab/collab" }
|
||||||
#collab-folder = { path = "../../../../AppFlowy-Collab/collab-folder" }
|
#collab-folder = { path = "../../../../AppFlowy-Collab/collab-folder" }
|
||||||
|
@ -40,11 +40,10 @@ export function useRangeKeyDown() {
|
|||||||
},
|
},
|
||||||
handler: (e: KeyboardEvent) => {
|
handler: (e: KeyboardEvent) => {
|
||||||
if (!controller) return;
|
if (!controller) return;
|
||||||
const insertDelta = new Delta().insert(e.key);
|
|
||||||
dispatch(
|
dispatch(
|
||||||
deleteRangeAndInsertThunk({
|
deleteRangeAndInsertThunk({
|
||||||
controller,
|
controller,
|
||||||
insertDelta,
|
insertChar: e.key,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -104,6 +103,7 @@ export function useRangeKeyDown() {
|
|||||||
handler: (e: KeyboardEvent) => {
|
handler: (e: KeyboardEvent) => {
|
||||||
if (!controller) return;
|
if (!controller) return;
|
||||||
const format = parseFormat(e);
|
const format = parseFormat(e);
|
||||||
|
|
||||||
if (!format) return;
|
if (!format) return;
|
||||||
dispatch(
|
dispatch(
|
||||||
toggleFormatThunk({
|
toggleFormatThunk({
|
||||||
@ -122,19 +122,25 @@ export function useRangeKeyDown() {
|
|||||||
if (!rangeRef.current) {
|
if (!rangeRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { anchor, focus } = rangeRef.current;
|
const { anchor, focus } = rangeRef.current;
|
||||||
|
|
||||||
if (!anchor || !focus) return;
|
if (!anchor || !focus) return;
|
||||||
|
|
||||||
if (anchor.id === focus.id) {
|
if (anchor.id === focus.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const filteredEvents = interceptEvents.filter((event) => event.canHandle(e));
|
const filteredEvents = interceptEvents.filter((event) => event.canHandle(e));
|
||||||
const lastIndex = filteredEvents.length - 1;
|
const lastIndex = filteredEvents.length - 1;
|
||||||
|
|
||||||
if (lastIndex < 0) {
|
if (lastIndex < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastEvent = filteredEvents[lastIndex];
|
const lastEvent = filteredEvents[lastIndex];
|
||||||
|
|
||||||
if (!lastEvent) return;
|
if (!lastEvent) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
lastEvent.handler(e);
|
lastEvent.handler(e);
|
||||||
|
@ -22,11 +22,11 @@ import {
|
|||||||
SlashCommandOptionKey,
|
SlashCommandOptionKey,
|
||||||
} from '$app/interfaces/document';
|
} from '$app/interfaces/document';
|
||||||
import { useAppDispatch } from '$app/stores/store';
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
import { triggerSlashCommandActionThunk } from '$app_reducers/document/async-actions/menu';
|
|
||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
import { slashCommandActions } from '$app_reducers/document/slice';
|
import { slashCommandActions } from '$app_reducers/document/slice';
|
||||||
import { Keyboard } from '$app/constants/document/keyboard';
|
import { Keyboard } from '$app/constants/document/keyboard';
|
||||||
import { selectOptionByUpDown } from '$app/utils/document/menu';
|
import { selectOptionByUpDown } from '$app/utils/document/menu';
|
||||||
|
import { turnToBlockThunk } from '$app_reducers/document/async-actions';
|
||||||
|
|
||||||
function BlockSlashMenu({
|
function BlockSlashMenu({
|
||||||
id,
|
id,
|
||||||
@ -48,13 +48,11 @@ function BlockSlashMenu({
|
|||||||
async (type: BlockType, data?: BlockData<any>) => {
|
async (type: BlockType, data?: BlockData<any>) => {
|
||||||
if (!controller) return;
|
if (!controller) return;
|
||||||
await dispatch(
|
await dispatch(
|
||||||
triggerSlashCommandActionThunk({
|
turnToBlockThunk({
|
||||||
controller,
|
controller,
|
||||||
id,
|
id,
|
||||||
props: {
|
type,
|
||||||
type,
|
data,
|
||||||
data,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
onClose?.();
|
onClose?.();
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { useAppDispatch } from '$app/stores/store';
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { slashCommandActions } from '$app_reducers/document/slice';
|
import { slashCommandActions } from '$app_reducers/document/slice';
|
||||||
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
|
|
||||||
import Delta from 'quill-delta';
|
|
||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
import { useSubscribeSlashState } from '$app/components/document/_shared/SubscribeSlash.hooks';
|
import { useSubscribeSlashState } from '$app/components/document/_shared/SubscribeSlash.hooks';
|
||||||
import { getDeltaText } from '$app/utils/document/delta';
|
import { useSubscribePanelSearchText } from '$app/components/document/_shared/usePanelSearchText';
|
||||||
|
|
||||||
export function useBlockSlash() {
|
export function useBlockSlash() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -68,28 +66,12 @@ export function useSubscribeSlash() {
|
|||||||
const slashCommandState = useSubscribeSlashState();
|
const slashCommandState = useSubscribeSlashState();
|
||||||
const visible = slashCommandState.isSlashCommand;
|
const visible = slashCommandState.isSlashCommand;
|
||||||
const blockId = slashCommandState.blockId;
|
const blockId = slashCommandState.blockId;
|
||||||
const rightDistanceRef = useRef<number>(0);
|
const { searchText } = useSubscribePanelSearchText({ blockId: '', open: visible });
|
||||||
|
|
||||||
const { node } = useSubscribeNode(blockId || '');
|
|
||||||
|
|
||||||
const slashText = useMemo(() => {
|
|
||||||
if (!node) return '';
|
|
||||||
const delta = new Delta(node.data.delta);
|
|
||||||
const length = delta.length();
|
|
||||||
const slicedDelta = delta.slice(0, length - rightDistanceRef.current);
|
|
||||||
|
|
||||||
return getDeltaText(slicedDelta);
|
|
||||||
}, [node]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!visible) return;
|
|
||||||
rightDistanceRef.current = new Delta(node.data.delta).length();
|
|
||||||
}, [visible]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
visible,
|
visible,
|
||||||
blockId,
|
blockId,
|
||||||
slashText,
|
slashText: searchText,
|
||||||
hoverOption: slashCommandState.hoverOption,
|
hoverOption: slashCommandState.hoverOption,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import Delta, { Op } from 'quill-delta';
|
|
||||||
import { getDeltaText } from '$app/utils/document/delta';
|
|
||||||
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
|
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import { Page } from '$app_reducers/pages/slice';
|
import { Page } from '$app_reducers/pages/slice';
|
||||||
|
|
||||||
export function useSubscribeMentionSearchText({ blockId, open }: { blockId: string; open: boolean }) {
|
|
||||||
const [searchText, setSearchText] = useState<string>('');
|
|
||||||
const beforeOpenDeltaRef = useRef<Op[]>([]);
|
|
||||||
const { node } = useSubscribeNode(blockId);
|
|
||||||
const handleSearch = useCallback((newDelta: Delta) => {
|
|
||||||
const diff = new Delta(beforeOpenDeltaRef.current).diff(newDelta);
|
|
||||||
const text = getDeltaText(diff);
|
|
||||||
|
|
||||||
setSearchText(text);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
handleSearch(new Delta(node?.data?.delta));
|
|
||||||
}, [handleSearch, node?.data?.delta, open]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
beforeOpenDeltaRef.current = node?.data?.delta;
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
searchText,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export function useMentionPopoverProps({ open }: { open: boolean }) {
|
export function useMentionPopoverProps({ open }: { open: boolean }) {
|
||||||
const [anchorPosition, setAnchorPosition] = useState<
|
const [anchorPosition, setAnchorPosition] = useState<
|
||||||
| {
|
| {
|
||||||
@ -43,12 +14,14 @@ export function useMentionPopoverProps({ open }: { open: boolean }) {
|
|||||||
const getPosition = useCallback(() => {
|
const getPosition = useCallback(() => {
|
||||||
const range = document.getSelection()?.getRangeAt(0);
|
const range = document.getSelection()?.getRangeAt(0);
|
||||||
const rangeRect = range?.getBoundingClientRect();
|
const rangeRect = range?.getBoundingClientRect();
|
||||||
|
|
||||||
return rangeRect;
|
return rangeRect;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
const position = getPosition();
|
const position = getPosition();
|
||||||
|
|
||||||
if (!position) return;
|
if (!position) return;
|
||||||
setAnchorPosition({
|
setAnchorPosition({
|
||||||
top: position.top + position.height || 0,
|
top: position.top + position.height || 0,
|
||||||
@ -75,10 +48,9 @@ export function useLoadRecentPages(searchText: string) {
|
|||||||
return page;
|
return page;
|
||||||
})
|
})
|
||||||
.filter((page) => {
|
.filter((page) => {
|
||||||
const text = searchText.slice(1, searchText.length);
|
return page.name.toLowerCase().includes(searchText.toLowerCase());
|
||||||
if (!text) return true;
|
|
||||||
return page.name.toLowerCase().includes(text.toLowerCase());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setRecentPages(recentPages);
|
setRecentPages(recentPages);
|
||||||
}, [pages, searchText]);
|
}, [pages, searchText]);
|
||||||
|
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useSubscribeMentionState } from '$app/components/document/_shared/SubscribeMention.hooks';
|
import { useSubscribeMentionState } from '$app/components/document/_shared/SubscribeMention.hooks';
|
||||||
import Popover from '@mui/material/Popover';
|
import Popover from '@mui/material/Popover';
|
||||||
import { useAppDispatch } from '$app/stores/store';
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
import { mentionActions } from '$app_reducers/document/mention_slice';
|
import { mentionActions } from '$app_reducers/document/mention_slice';
|
||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
import { useMentionPopoverProps, useSubscribeMentionSearchText } from '$app/components/document/Mention/Mention.hooks';
|
import { useMentionPopoverProps } from '$app/components/document/Mention/Mention.hooks';
|
||||||
import RecentPages from '$app/components/document/Mention/RecentPages';
|
import RecentPages from '$app/components/document/Mention/RecentPages';
|
||||||
import { formatMention, MentionType } from '$app_reducers/document/async-actions/mention';
|
import { formatMention, MentionType } from '$app_reducers/document/async-actions/mention';
|
||||||
|
import { useSubscribePanelSearchText } from '$app/components/document/_shared/usePanelSearchText';
|
||||||
|
|
||||||
function MentionPopover() {
|
function MentionPopover() {
|
||||||
const { docId, controller } = useSubscribeDocument();
|
const { docId, controller } = useSubscribeDocument();
|
||||||
const { open, blockId } = useSubscribeMentionState();
|
const { open, blockId } = useSubscribeMentionState();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const onClose = useCallback(() => {
|
const onClose = useCallback(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -20,7 +22,7 @@ function MentionPopover() {
|
|||||||
);
|
);
|
||||||
}, [dispatch, docId]);
|
}, [dispatch, docId]);
|
||||||
|
|
||||||
const { searchText } = useSubscribeMentionSearchText({
|
const { searchText } = useSubscribePanelSearchText({
|
||||||
blockId,
|
blockId,
|
||||||
open,
|
open,
|
||||||
});
|
});
|
||||||
@ -29,12 +31,6 @@ function MentionPopover() {
|
|||||||
open,
|
open,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (searchText === '' && popoverOpen) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}, [searchText, popoverOpen, onClose]);
|
|
||||||
|
|
||||||
const onSelectPage = useCallback(
|
const onSelectPage = useCallback(
|
||||||
async (pageId: string) => {
|
async (pageId: string) => {
|
||||||
await dispatch(
|
await dispatch(
|
||||||
@ -70,8 +66,7 @@ function MentionPopover() {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
boxShadow:
|
boxShadow: 'var(--shadow-resize-popover)',
|
||||||
"var(--shadow-resize-popover)",
|
|
||||||
}}
|
}}
|
||||||
className={'flex w-[420px] flex-col rounded-md bg-bg-body px-4 py-2'}
|
className={'flex w-[420px] flex-col rounded-md bg-bg-body px-4 py-2'}
|
||||||
>
|
>
|
||||||
|
@ -52,8 +52,11 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) =>
|
|||||||
isActive,
|
isActive,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
const actived = await isFormatActive();
|
||||||
|
|
||||||
|
setIsActive(actived);
|
||||||
},
|
},
|
||||||
[controller, dispatch, isActive]
|
[controller, dispatch, isActive, isFormatActive]
|
||||||
);
|
);
|
||||||
|
|
||||||
const addTemporaryInput = useCallback(
|
const addTemporaryInput = useCallback(
|
||||||
|
@ -83,18 +83,9 @@ export function useKeyDown(id: string) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
// handle @ key for mention panel
|
|
||||||
canHandle: (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
return e.key === '@';
|
|
||||||
},
|
|
||||||
handler: (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
dispatch(openMention({ docId }));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...turnIntoEvents,
|
...turnIntoEvents,
|
||||||
];
|
];
|
||||||
}, [docId, commonKeyEvents, controller, dispatch, id, turnIntoEvents]);
|
}, [commonKeyEvents, controller, dispatch, id, turnIntoEvents]);
|
||||||
|
|
||||||
const onKeyDown = useCallback(
|
const onKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
@ -6,7 +6,7 @@ import { blockConfig } from '$app/constants/document/config';
|
|||||||
|
|
||||||
import Delta, { Op } from 'quill-delta';
|
import Delta, { Op } from 'quill-delta';
|
||||||
import { useRangeRef } from '$app/components/document/_shared/SubscribeSelection.hooks';
|
import { useRangeRef } from '$app/components/document/_shared/SubscribeSelection.hooks';
|
||||||
import { getBlock } from '$app/components/document/_shared/SubscribeNode.hooks';
|
import { getBlock, getBlockDelta } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||||
import isHotkey from 'is-hotkey';
|
import isHotkey from 'is-hotkey';
|
||||||
import { slashCommandActions } from '$app_reducers/document/slice';
|
import { slashCommandActions } from '$app_reducers/document/slice';
|
||||||
import { getDeltaText } from '$app/utils/document/delta';
|
import { getDeltaText } from '$app/utils/document/delta';
|
||||||
@ -23,9 +23,10 @@ export function useTurnIntoBlockEvents(id: string) {
|
|||||||
const range = rangeRef.current?.caret;
|
const range = rangeRef.current?.caret;
|
||||||
|
|
||||||
if (!range || range.id !== id) return;
|
if (!range || range.id !== id) return;
|
||||||
const node = getBlock(docId, id);
|
|
||||||
const delta = new Delta(node.data.delta || []);
|
|
||||||
|
|
||||||
|
const delta = getBlockDelta(docId, id);
|
||||||
|
|
||||||
|
if (!delta) return '';
|
||||||
return getDeltaText(delta.slice(0, range.index));
|
return getDeltaText(delta.slice(0, range.index));
|
||||||
}, [docId, id, rangeRef]);
|
}, [docId, id, rangeRef]);
|
||||||
|
|
||||||
@ -33,8 +34,9 @@ export function useTurnIntoBlockEvents(id: string) {
|
|||||||
const range = rangeRef.current?.caret;
|
const range = rangeRef.current?.caret;
|
||||||
|
|
||||||
if (!range || range.id !== id) return;
|
if (!range || range.id !== id) return;
|
||||||
const node = getBlock(docId, id);
|
const delta = getBlockDelta(docId, id);
|
||||||
const delta = new Delta(node.data.delta || []);
|
|
||||||
|
if (!delta) return '';
|
||||||
const content = delta.slice(range.index);
|
const content = delta.slice(range.index);
|
||||||
|
|
||||||
return new Delta(content);
|
return new Delta(content);
|
||||||
@ -174,9 +176,7 @@ export function useTurnIntoBlockEvents(id: string) {
|
|||||||
id,
|
id,
|
||||||
controller,
|
controller,
|
||||||
type: BlockType.DividerBlock,
|
type: BlockType.DividerBlock,
|
||||||
data: {
|
data: {},
|
||||||
delta: delta?.ops as Op[],
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -187,12 +187,17 @@ export function useTurnIntoBlockEvents(id: string) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!controller) return;
|
if (!controller) return;
|
||||||
const defaultData = blockConfig[BlockType.CodeBlock].defaultData;
|
const defaultData = blockConfig[BlockType.CodeBlock].defaultData;
|
||||||
const data = {
|
|
||||||
...defaultData,
|
|
||||||
delta: getDeltaContent()?.ops as Op[],
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch(turnToBlockThunk({ id, data, type: BlockType.CodeBlock, controller }));
|
dispatch(
|
||||||
|
turnToBlockThunk({
|
||||||
|
id,
|
||||||
|
data: {
|
||||||
|
...defaultData,
|
||||||
|
},
|
||||||
|
type: BlockType.CodeBlock,
|
||||||
|
controller,
|
||||||
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { BlockType, NestedBlock } from '$app/interfaces/document';
|
import { BlockType, NestedBlock } from '$app/interfaces/document';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import Delta from 'quill-delta';
|
import Delta, { Op } from 'quill-delta';
|
||||||
import { useDelta } from '$app/components/document/_shared/EditorHooks/useDelta';
|
import { useDelta } from '$app/components/document/_shared/EditorHooks/useDelta';
|
||||||
|
|
||||||
export function useChange(node: NestedBlock<BlockType.TextBlock | BlockType.CodeBlock>) {
|
export function useChange(node: NestedBlock<BlockType.TextBlock | BlockType.CodeBlock>) {
|
||||||
@ -15,13 +15,10 @@ export function useChange(node: NestedBlock<BlockType.TextBlock | BlockType.Code
|
|||||||
}, [delta]);
|
}, [delta]);
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(newContents: Delta, oldContents: Delta, _source?: string) => {
|
async (ops: Op[], newDelta: Delta) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
if (ops.length === 0) return;
|
||||||
// @ts-ignore
|
setValue(newDelta);
|
||||||
const isSame = newContents.diff(oldContents).ops.length === 0;
|
await update(ops, newDelta);
|
||||||
if (isSame) return;
|
|
||||||
setValue(newContents);
|
|
||||||
update(newContents);
|
|
||||||
},
|
},
|
||||||
[update]
|
[update]
|
||||||
);
|
);
|
||||||
|
@ -2,31 +2,34 @@ import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode
|
|||||||
import { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useAppDispatch } from '$app/stores/store';
|
import { useAppDispatch } from '$app/stores/store';
|
||||||
import { updateNodeDeltaThunk } from '$app_reducers/document/async-actions';
|
import { updateNodeDeltaThunk } from '$app_reducers/document/async-actions';
|
||||||
import Delta from 'quill-delta';
|
import Delta, { Op } from 'quill-delta';
|
||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
|
|
||||||
export function useDelta({ id, onDeltaChange }: { id: string; onDeltaChange?: (delta: Delta) => void }) {
|
export function useDelta({ id, onDeltaChange }: { id: string; onDeltaChange?: (delta: Delta) => void }) {
|
||||||
const { controller } = useSubscribeDocument();
|
const { controller } = useSubscribeDocument();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const penddingRef = useRef(false);
|
const penddingRef = useRef(false);
|
||||||
const { node } = useSubscribeNode(id);
|
const { delta: deltaStr } = useSubscribeNode(id);
|
||||||
|
|
||||||
const delta = useMemo(() => {
|
const delta = useMemo(() => {
|
||||||
if (!node || !node.data.delta) return new Delta();
|
if (!deltaStr) return new Delta();
|
||||||
return new Delta(node.data.delta);
|
const deltaJson = JSON.parse(deltaStr);
|
||||||
}, [node]);
|
|
||||||
|
return new Delta(deltaJson);
|
||||||
|
}, [deltaStr]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDeltaChange?.(delta);
|
onDeltaChange?.(delta);
|
||||||
}, [delta, onDeltaChange]);
|
}, [delta, onDeltaChange]);
|
||||||
|
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
async (delta: Delta) => {
|
async (ops: Op[], newDelta: Delta) => {
|
||||||
if (!controller) return;
|
if (!controller) return;
|
||||||
await dispatch(
|
await dispatch(
|
||||||
updateNodeDeltaThunk({
|
updateNodeDeltaThunk({
|
||||||
id,
|
id,
|
||||||
delta: delta.ops,
|
ops,
|
||||||
|
newDelta,
|
||||||
controller,
|
controller,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
.ql-container.ql-snow {
|
|
||||||
border: none;
|
|
||||||
font-family: 'Poppins', sans-serif;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
}
|
|
||||||
.ql-editor {
|
|
||||||
outline: none;
|
|
||||||
max-width: 100%;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
padding: 4px 2px;
|
|
||||||
text-align: left;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ql-editor.ql-blank::before {
|
|
||||||
left: 2px;
|
|
||||||
right: 2px;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useEditor } from '$app/components/document/_shared/QuillEditor/useEditor';
|
|
||||||
import 'quill/dist/quill.snow.css';
|
|
||||||
import './Editor.css';
|
|
||||||
import { EditorProps } from '$app/interfaces/document';
|
|
||||||
|
|
||||||
function Editor({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onSelectionChange,
|
|
||||||
selection,
|
|
||||||
placeholder = "Type '/' for commands",
|
|
||||||
...props
|
|
||||||
}: EditorProps) {
|
|
||||||
const { ref, editor } = useEditor({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onSelectionChange,
|
|
||||||
selection,
|
|
||||||
placeholder,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div className={'min-h-[30px]'}>
|
|
||||||
<div ref={ref} {...props} />
|
|
||||||
{!editor && <div className={'px-0.5 py-1 text-text-caption'}>{placeholder}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(Editor);
|
|
@ -1,100 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import Quill, { Sources } from 'quill';
|
|
||||||
import Delta from 'quill-delta';
|
|
||||||
import { adaptDeltaForQuill } from '$app/utils/document/quill_editor';
|
|
||||||
import { EditorProps } from '$app/interfaces/document';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Here we can use ts-ignore because the quill-delta's version of quill is not uploaded to DefinitelyTyped
|
|
||||||
*/
|
|
||||||
export function useEditor({ placeholder, value, onChange, onSelectionChange, selection }: EditorProps) {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const [editor, setEditor] = useState<Quill>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!ref.current) return;
|
|
||||||
const editor = new Quill(ref.current, {
|
|
||||||
modules: {
|
|
||||||
toolbar: false, // Snow includes toolbar by default
|
|
||||||
},
|
|
||||||
theme: 'snow',
|
|
||||||
formats: ['bold', 'italic', 'underline', 'strike', 'code'],
|
|
||||||
placeholder: placeholder || 'Please enter some text...',
|
|
||||||
});
|
|
||||||
const keyboard = editor.getModule('keyboard');
|
|
||||||
// clear all keyboard bindings
|
|
||||||
keyboard.bindings = {};
|
|
||||||
const initialDelta = new Delta(adaptDeltaForQuill(value?.ops || []));
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
editor.setContents(initialDelta);
|
|
||||||
setEditor(editor);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// listen to text-change event
|
|
||||||
useEffect(() => {
|
|
||||||
if (!editor) return;
|
|
||||||
const onTextChange = (delta: Delta, oldContents: Delta, source: Sources) => {
|
|
||||||
const newContents = oldContents.compose(delta);
|
|
||||||
const newOps = adaptDeltaForQuill(newContents.ops, true);
|
|
||||||
const newDelta = new Delta(newOps);
|
|
||||||
onChange?.(newDelta, oldContents, source);
|
|
||||||
if (source === 'user') {
|
|
||||||
const selection = editor.getSelection(false);
|
|
||||||
onSelectionChange?.(selection, null, source);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
editor.on('text-change', onTextChange);
|
|
||||||
return () => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
editor.off('text-change', onTextChange);
|
|
||||||
};
|
|
||||||
}, [editor, onChange, onSelectionChange]);
|
|
||||||
|
|
||||||
// listen to selection-change event
|
|
||||||
useEffect(() => {
|
|
||||||
const handleSelectionChange = () => {
|
|
||||||
if (!editor) return;
|
|
||||||
const selection = editor.getSelection(false);
|
|
||||||
onSelectionChange?.(selection, null, 'user');
|
|
||||||
};
|
|
||||||
document.addEventListener('selectionchange', handleSelectionChange);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('selectionchange', handleSelectionChange);
|
|
||||||
};
|
|
||||||
}, [editor, onSelectionChange]);
|
|
||||||
|
|
||||||
// set value
|
|
||||||
useEffect(() => {
|
|
||||||
if (!editor) return;
|
|
||||||
const content = editor.getContents();
|
|
||||||
|
|
||||||
const newOps = adaptDeltaForQuill(value?.ops || []);
|
|
||||||
const newDelta = new Delta(newOps);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
const diffDelta = content.diff(newDelta);
|
|
||||||
const isSame = diffDelta.ops.length === 0;
|
|
||||||
if (isSame) return;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
editor.updateContents(diffDelta, 'api');
|
|
||||||
}, [editor, value]);
|
|
||||||
|
|
||||||
// set Selection
|
|
||||||
useEffect(() => {
|
|
||||||
if (!editor || !selection) return;
|
|
||||||
if (JSON.stringify(selection) === JSON.stringify(editor.getSelection())) return;
|
|
||||||
|
|
||||||
editor.setSelection(selection);
|
|
||||||
}, [selection, editor]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
ref,
|
|
||||||
editor,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,19 +1,13 @@
|
|||||||
import { EditorProps } from '$app/interfaces/document';
|
import { EditorProps } from '$app/interfaces/document';
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { ReactEditor } from 'slate-react';
|
import { ReactEditor } from 'slate-react';
|
||||||
import { BaseRange, Descendant, Editor, NodeEntry, Range, Selection, Transforms } from 'slate';
|
import { BaseRange, Editor, NodeEntry, Range, Selection, Transforms } from 'slate';
|
||||||
import {
|
import { converToIndexLength, convertToSlateSelection, indent, outdent } from '$app/utils/document/slate_editor';
|
||||||
converToIndexLength,
|
|
||||||
convertToDelta,
|
|
||||||
convertToSlateSelection,
|
|
||||||
indent,
|
|
||||||
outdent,
|
|
||||||
} from '$app/utils/document/slate_editor';
|
|
||||||
import { focusNodeByIndex } from '$app/utils/document/node';
|
import { focusNodeByIndex } from '$app/utils/document/node';
|
||||||
import { Keyboard } from '$app/constants/document/keyboard';
|
import { Keyboard } from '$app/constants/document/keyboard';
|
||||||
import Delta from 'quill-delta';
|
|
||||||
import isHotkey from 'is-hotkey';
|
import isHotkey from 'is-hotkey';
|
||||||
import { useSlateYjs } from '$app/components/document/_shared/SlateEditor/useSlateYjs';
|
import { useSlateYjs } from '$app/components/document/_shared/SlateEditor/useSlateYjs';
|
||||||
|
import { openMention } from '$app_reducers/document/async-actions/mention';
|
||||||
|
|
||||||
const AFTER_RENDER_DELAY = 100;
|
const AFTER_RENDER_DELAY = 100;
|
||||||
|
|
||||||
@ -27,7 +21,7 @@ export function useEditor({
|
|||||||
isCodeBlock,
|
isCodeBlock,
|
||||||
temporarySelection,
|
temporarySelection,
|
||||||
}: EditorProps) {
|
}: EditorProps) {
|
||||||
const { editor } = useSlateYjs({ delta });
|
const { editor } = useSlateYjs({ delta, onChange });
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const newValue = useMemo(() => [], []);
|
const newValue = useMemo(() => [], []);
|
||||||
const onSelectionChangeHandler = useCallback(
|
const onSelectionChangeHandler = useCallback(
|
||||||
@ -39,15 +33,9 @@ export function useEditor({
|
|||||||
[editor, onSelectionChange]
|
[editor, onSelectionChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onChangeHandler = useCallback(
|
const onChangeHandler = useCallback(() => {
|
||||||
(slateValue: Descendant[]) => {
|
onSelectionChangeHandler(editor.selection);
|
||||||
const oldContents = delta || new Delta();
|
}, [editor, onSelectionChangeHandler]);
|
||||||
const newContents = convertToDelta(slateValue);
|
|
||||||
onChange?.(newContents, oldContents);
|
|
||||||
onSelectionChangeHandler(editor.selection);
|
|
||||||
},
|
|
||||||
[delta, editor, onChange, onSelectionChangeHandler]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Prevent attributes from being applied when entering text at the beginning or end of an inline block.
|
// Prevent attributes from being applied when entering text at the beginning or end of an inline block.
|
||||||
// For example, when entering text before or after a mentioned page,
|
// For example, when entering text before or after a mentioned page,
|
||||||
@ -62,11 +50,13 @@ export function useEditor({
|
|||||||
const currentSelection = editor.selection || [];
|
const currentSelection = editor.selection || [];
|
||||||
let removeMark = markKeys.length > 0;
|
let removeMark = markKeys.length > 0;
|
||||||
const [_, path] = editor.node(currentSelection);
|
const [_, path] = editor.node(currentSelection);
|
||||||
|
|
||||||
if (removeMark) {
|
if (removeMark) {
|
||||||
const selectionStart = editor.start(currentSelection);
|
const selectionStart = editor.start(currentSelection);
|
||||||
const selectionEnd = editor.end(currentSelection);
|
const selectionEnd = editor.end(currentSelection);
|
||||||
const isNodeEnd = editor.isEnd(selectionEnd, path);
|
const isNodeEnd = editor.isEnd(selectionEnd, path);
|
||||||
const isNodeStart = editor.isStart(selectionStart, path);
|
const isNodeStart = editor.isStart(selectionStart, path);
|
||||||
|
|
||||||
removeMark = isNodeStart || isNodeEnd;
|
removeMark = isNodeStart || isNodeEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,6 +75,7 @@ export function useEditor({
|
|||||||
if (e.inputType === 'insertFromComposition') {
|
if (e.inputType === 'insertFromComposition') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
preventInlineBlockAttributeOverride();
|
preventInlineBlockAttributeOverride();
|
||||||
},
|
},
|
||||||
[preventInlineBlockAttributeOverride]
|
[preventInlineBlockAttributeOverride]
|
||||||
@ -195,6 +186,7 @@ export function useEditor({
|
|||||||
if (!slateSelection) return;
|
if (!slateSelection) return;
|
||||||
|
|
||||||
const isEqual = JSON.stringify(slateSelection) === JSON.stringify(editor.selection);
|
const isEqual = JSON.stringify(slateSelection) === JSON.stringify(editor.selection);
|
||||||
|
|
||||||
if (isFocused && isEqual) return;
|
if (isFocused && isEqual) return;
|
||||||
|
|
||||||
// why we didn't use slate api to change selection?
|
// why we didn't use slate api to change selection?
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Delta from 'quill-delta';
|
import Delta, { Op } from 'quill-delta';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
import { convertToSlateValue } from '$app/utils/document/slate_editor';
|
import { convertToSlateValue } from '$app/utils/document/slate_editor';
|
||||||
@ -7,7 +7,7 @@ import { withReact } from 'slate-react';
|
|||||||
import { createEditor } from 'slate';
|
import { createEditor } from 'slate';
|
||||||
import { withMarkdown } from '$app/components/document/_shared/SlateEditor/markdown';
|
import { withMarkdown } from '$app/components/document/_shared/SlateEditor/markdown';
|
||||||
|
|
||||||
export function useSlateYjs({ delta }: { delta?: Delta }) {
|
export function useSlateYjs({ delta, onChange }: { delta?: Delta; onChange: (ops: Op[], newDelta: Delta) => void }) {
|
||||||
const [yText, setYText] = useState<Y.Text | undefined>(undefined);
|
const [yText, setYText] = useState<Y.Text | undefined>(undefined);
|
||||||
const sharedType = useMemo(() => {
|
const sharedType = useMemo(() => {
|
||||||
const yDoc = new Y.Doc();
|
const yDoc = new Y.Doc();
|
||||||
@ -26,15 +26,25 @@ export function useSlateYjs({ delta }: { delta?: Delta }) {
|
|||||||
// Connect editor in useEffect to comply with concurrent mode requirements.
|
// Connect editor in useEffect to comply with concurrent mode requirements.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
YjsEditor.connect(editor);
|
YjsEditor.connect(editor);
|
||||||
|
const observer = (event: Y.YTextEvent) => {
|
||||||
|
const ops = event.changes.delta as Op[];
|
||||||
|
const newDelta = new Delta(yText?.toDelta());
|
||||||
|
|
||||||
|
onChange(ops, newDelta);
|
||||||
|
};
|
||||||
|
|
||||||
|
yText?.observe(observer);
|
||||||
return () => {
|
return () => {
|
||||||
YjsEditor.disconnect(editor);
|
YjsEditor.disconnect(editor);
|
||||||
|
yText?.unobserve(observer);
|
||||||
};
|
};
|
||||||
}, [editor]);
|
}, [editor, yText, onChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!yText) return;
|
if (!yText) return;
|
||||||
const oldContents = new Delta(yText.toDelta());
|
const oldContents = new Delta(yText.toDelta());
|
||||||
const diffDelta = oldContents.diff(delta || new Delta());
|
const diffDelta = oldContents.diff(delta || new Delta());
|
||||||
|
|
||||||
if (diffDelta.ops.length === 0) return;
|
if (diffDelta.ops.length === 0) return;
|
||||||
yText.applyDelta(diffDelta.ops);
|
yText.applyDelta(diffDelta.ops);
|
||||||
}, [delta, editor, yText]);
|
}, [delta, editor, yText]);
|
||||||
|
@ -3,6 +3,7 @@ import { createContext, useMemo } from 'react';
|
|||||||
import { Node } from '$app/interfaces/document';
|
import { Node } from '$app/interfaces/document';
|
||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
import { DOCUMENT_NAME, RECT_RANGE_NAME } from '$app/constants/document/name';
|
import { DOCUMENT_NAME, RECT_RANGE_NAME } from '$app/constants/document/name';
|
||||||
|
import Delta from 'quill-delta';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe node information
|
* Subscribe node information
|
||||||
@ -11,10 +12,18 @@ import { DOCUMENT_NAME, RECT_RANGE_NAME } from '$app/constants/document/name';
|
|||||||
export function useSubscribeNode(id: string) {
|
export function useSubscribeNode(id: string) {
|
||||||
const { docId } = useSubscribeDocument();
|
const { docId } = useSubscribeDocument();
|
||||||
|
|
||||||
const node = useAppSelector<Node>((state) => {
|
const { node, delta } = useAppSelector<{
|
||||||
|
node: Node;
|
||||||
|
delta: string;
|
||||||
|
}>((state) => {
|
||||||
const documentState = state[DOCUMENT_NAME][docId];
|
const documentState = state[DOCUMENT_NAME][docId];
|
||||||
|
const node = documentState?.nodes[id];
|
||||||
|
const externalId = node?.externalId;
|
||||||
|
|
||||||
return documentState?.nodes[id];
|
return {
|
||||||
|
node,
|
||||||
|
delta: externalId ? documentState?.deltaMap[externalId] : '',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const childIds = useAppSelector<string[] | undefined>((state) => {
|
const childIds = useAppSelector<string[] | undefined>((state) => {
|
||||||
@ -40,6 +49,7 @@ export function useSubscribeNode(id: string) {
|
|||||||
return {
|
return {
|
||||||
node: memoizedNode,
|
node: memoizedNode,
|
||||||
childIds: memoizedChildIds,
|
childIds: memoizedChildIds,
|
||||||
|
delta,
|
||||||
isSelected,
|
isSelected,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -48,4 +58,15 @@ export function getBlock(docId: string, id: string) {
|
|||||||
return store.getState().document[docId]?.nodes[id];
|
return store.getState().document[docId]?.nodes[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getBlockDelta(docId: string, id: string) {
|
||||||
|
const node = getBlock(docId, id);
|
||||||
|
|
||||||
|
if (!node?.externalId) return;
|
||||||
|
const deltaStr = store.getState().document[docId]?.deltaMap[node.externalId];
|
||||||
|
const deltaJson = JSON.parse(deltaStr);
|
||||||
|
const delta = new Delta(deltaJson);
|
||||||
|
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
|
||||||
export const NodeIdContext = createContext<string>('');
|
export const NodeIdContext = createContext<string>('');
|
||||||
|
@ -4,8 +4,6 @@ import { BlockData, BlockType, NestedBlock } from '$app/interfaces/document';
|
|||||||
import { blockConfig } from '$app/constants/document/config';
|
import { blockConfig } from '$app/constants/document/config';
|
||||||
import { turnToBlockThunk } from '$app_reducers/document/async-actions';
|
import { turnToBlockThunk } from '$app_reducers/document/async-actions';
|
||||||
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
|
||||||
import Delta from 'quill-delta';
|
|
||||||
import { getDeltaText } from '$app/utils/document/delta';
|
|
||||||
import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection';
|
import { setRectSelectionThunk } from '$app_reducers/document/async-actions/rect_selection';
|
||||||
|
|
||||||
export function useTurnInto({ node, onClose }: { node: NestedBlock; onClose?: () => void }) {
|
export function useTurnInto({ node, onClose }: { node: NestedBlock; onClose?: () => void }) {
|
||||||
@ -13,34 +11,6 @@ export function useTurnInto({ node, onClose }: { node: NestedBlock; onClose?: ()
|
|||||||
|
|
||||||
const { controller, docId } = useSubscribeDocument();
|
const { controller, docId } = useSubscribeDocument();
|
||||||
|
|
||||||
const getTurnIntoData = useCallback(
|
|
||||||
(targetType: BlockType, sourceNode: NestedBlock) => {
|
|
||||||
if (targetType === sourceNode.type) return;
|
|
||||||
const config = blockConfig[targetType];
|
|
||||||
const defaultData = config.defaultData;
|
|
||||||
const data: BlockData<any> = {
|
|
||||||
...defaultData,
|
|
||||||
delta: sourceNode?.data?.delta || [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (targetType === BlockType.EquationBlock) {
|
|
||||||
data.formula = getDeltaText(new Delta(sourceNode.data.delta));
|
|
||||||
delete data.delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceNode.type === BlockType.EquationBlock) {
|
|
||||||
data.delta = [
|
|
||||||
{
|
|
||||||
insert: node.data.formula,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
[node.data.formula]
|
|
||||||
);
|
|
||||||
|
|
||||||
const turnIntoBlock = useCallback(
|
const turnIntoBlock = useCallback(
|
||||||
async (type: BlockType, isSelected: boolean, data?: BlockData<any>) => {
|
async (type: BlockType, isSelected: boolean, data?: BlockData<any>) => {
|
||||||
if (!controller || isSelected) {
|
if (!controller || isSelected) {
|
||||||
@ -48,8 +18,10 @@ export function useTurnInto({ node, onClose }: { node: NestedBlock; onClose?: ()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const config = blockConfig[type];
|
||||||
|
const defaultData = config.defaultData;
|
||||||
const updateData = {
|
const updateData = {
|
||||||
...getTurnIntoData(type, node),
|
...defaultData,
|
||||||
...data,
|
...data,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -70,7 +42,7 @@ export function useTurnInto({ node, onClose }: { node: NestedBlock; onClose?: ()
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[controller, getTurnIntoData, node, dispatch, onClose, docId]
|
[controller, node, dispatch, onClose, docId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const turnIntoHeading = useCallback(
|
const turnIntoHeading = useCallback(
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import Delta, { Op } from 'quill-delta';
|
||||||
|
import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks';
|
||||||
|
import { getDeltaText } from '$app/utils/document/delta';
|
||||||
|
|
||||||
|
export function useSubscribePanelSearchText({ blockId, open }: { blockId: string; open: boolean }) {
|
||||||
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
|
const beforeOpenDeltaRef = useRef<Op[]>([]);
|
||||||
|
const { delta } = useSubscribeNode(blockId);
|
||||||
|
const handleSearch = useCallback((newDelta: Delta) => {
|
||||||
|
const diff = new Delta(beforeOpenDeltaRef.current).diff(newDelta);
|
||||||
|
const text = getDeltaText(diff);
|
||||||
|
|
||||||
|
setSearchText(text);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !delta) return;
|
||||||
|
handleSearch(new Delta(JSON.parse(delta)));
|
||||||
|
}, [handleSearch, delta, open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
beforeOpenDeltaRef.current = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeOpenDeltaRef.current = new Delta(JSON.parse(delta)).ops;
|
||||||
|
handleSearch(new Delta(JSON.parse(delta)));
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchText,
|
||||||
|
};
|
||||||
|
}
|
@ -7,9 +7,7 @@ import { randomEmoji } from '$app/utils/document/emoji';
|
|||||||
export const blockConfig: Record<string, BlockConfig> = {
|
export const blockConfig: Record<string, BlockConfig> = {
|
||||||
[BlockType.TextBlock]: {
|
[BlockType.TextBlock]: {
|
||||||
canAddChild: true,
|
canAddChild: true,
|
||||||
defaultData: {
|
defaultData: {},
|
||||||
delta: [],
|
|
||||||
},
|
|
||||||
splitProps: {
|
splitProps: {
|
||||||
nextLineRelationShip: SplitRelationship.NextSibling,
|
nextLineRelationShip: SplitRelationship.NextSibling,
|
||||||
nextLineBlockType: BlockType.TextBlock,
|
nextLineBlockType: BlockType.TextBlock,
|
||||||
@ -25,7 +23,6 @@ export const blockConfig: Record<string, BlockConfig> = {
|
|||||||
[BlockType.TodoListBlock]: {
|
[BlockType.TodoListBlock]: {
|
||||||
canAddChild: true,
|
canAddChild: true,
|
||||||
defaultData: {
|
defaultData: {
|
||||||
delta: [],
|
|
||||||
checked: false,
|
checked: false,
|
||||||
},
|
},
|
||||||
splitProps: {
|
splitProps: {
|
||||||
@ -36,7 +33,6 @@ export const blockConfig: Record<string, BlockConfig> = {
|
|||||||
[BlockType.BulletedListBlock]: {
|
[BlockType.BulletedListBlock]: {
|
||||||
canAddChild: true,
|
canAddChild: true,
|
||||||
defaultData: {
|
defaultData: {
|
||||||
delta: [],
|
|
||||||
format: 'default',
|
format: 'default',
|
||||||
},
|
},
|
||||||
splitProps: {
|
splitProps: {
|
||||||
@ -47,7 +43,6 @@ export const blockConfig: Record<string, BlockConfig> = {
|
|||||||
[BlockType.NumberedListBlock]: {
|
[BlockType.NumberedListBlock]: {
|
||||||
canAddChild: true,
|
canAddChild: true,
|
||||||
defaultData: {
|
defaultData: {
|
||||||
delta: [],
|
|
||||||
format: 'default',
|
format: 'default',
|
||||||
},
|
},
|
||||||
splitProps: {
|
splitProps: {
|
||||||
@ -58,7 +53,6 @@ export const blockConfig: Record<string, BlockConfig> = {
|
|||||||
[BlockType.QuoteBlock]: {
|
[BlockType.QuoteBlock]: {
|
||||||
canAddChild: true,
|
canAddChild: true,
|
||||||
defaultData: {
|
defaultData: {
|
||||||
delta: [],
|
|
||||||
size: 'default',
|
size: 'default',
|
||||||
},
|
},
|
||||||
splitProps: {
|
splitProps: {
|
||||||
@ -69,7 +63,6 @@ export const blockConfig: Record<string, BlockConfig> = {
|
|||||||
[BlockType.CalloutBlock]: {
|
[BlockType.CalloutBlock]: {
|
||||||
canAddChild: true,
|
canAddChild: true,
|
||||||
defaultData: {
|
defaultData: {
|
||||||
delta: [],
|
|
||||||
icon: randomEmoji(),
|
icon: randomEmoji(),
|
||||||
},
|
},
|
||||||
splitProps: {
|
splitProps: {
|
||||||
@ -80,7 +73,6 @@ export const blockConfig: Record<string, BlockConfig> = {
|
|||||||
[BlockType.ToggleListBlock]: {
|
[BlockType.ToggleListBlock]: {
|
||||||
canAddChild: true,
|
canAddChild: true,
|
||||||
defaultData: {
|
defaultData: {
|
||||||
delta: [],
|
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
},
|
},
|
||||||
splitProps: {
|
splitProps: {
|
||||||
@ -92,7 +84,6 @@ export const blockConfig: Record<string, BlockConfig> = {
|
|||||||
[BlockType.CodeBlock]: {
|
[BlockType.CodeBlock]: {
|
||||||
canAddChild: false,
|
canAddChild: false,
|
||||||
defaultData: {
|
defaultData: {
|
||||||
delta: [],
|
|
||||||
language: 'javascript',
|
language: 'javascript',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -12,4 +12,5 @@ export const BLOCK_MAP_NAME = 'blocks';
|
|||||||
export const META_NAME = 'meta';
|
export const META_NAME = 'meta';
|
||||||
export const CHILDREN_MAP_NAME = 'children_map';
|
export const CHILDREN_MAP_NAME = 'children_map';
|
||||||
|
|
||||||
|
export const TEXT_MAP_NAME = 'text_map';
|
||||||
export const EQUATION_PLACEHOLDER = '$';
|
export const EQUATION_PLACEHOLDER = '$';
|
||||||
|
@ -62,9 +62,7 @@ export interface CalloutBlockData extends TextBlockData {
|
|||||||
icon: string;
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TextBlockData {
|
export type TextBlockData = Record<string, any>;
|
||||||
delta: Op[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DividerBlockData {}
|
export interface DividerBlockData {}
|
||||||
|
|
||||||
@ -120,9 +118,11 @@ export type BlockData<Type> = Type extends BlockType.HeadingBlock
|
|||||||
export interface NestedBlock<Type = any> {
|
export interface NestedBlock<Type = any> {
|
||||||
id: string;
|
id: string;
|
||||||
type: BlockType;
|
type: BlockType;
|
||||||
data: BlockData<Type>;
|
data: BlockData<Type> | any;
|
||||||
parent: string | null;
|
parent: string | null;
|
||||||
children: string;
|
children: string;
|
||||||
|
externalId?: string;
|
||||||
|
externalType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Node = NestedBlock;
|
export type Node = NestedBlock;
|
||||||
@ -133,12 +133,15 @@ export interface DocumentData {
|
|||||||
nodes: Record<string, Node>;
|
nodes: Record<string, Node>;
|
||||||
// map of block id to children block ids
|
// map of block id to children block ids
|
||||||
children: Record<string, string[]>;
|
children: Record<string, string[]>;
|
||||||
|
|
||||||
|
deltaMap: Record<string, string>;
|
||||||
}
|
}
|
||||||
export interface DocumentState {
|
export interface DocumentState {
|
||||||
// map of block id to block
|
// map of block id to block
|
||||||
nodes: Record<string, Node>;
|
nodes: Record<string, Node>;
|
||||||
// map of block id to children block ids
|
// map of block id to children block ids
|
||||||
children: Record<string, string[]>;
|
children: Record<string, string[]>;
|
||||||
|
deltaMap: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SlashCommandState {
|
export interface SlashCommandState {
|
||||||
@ -219,6 +222,9 @@ export enum ChangeType {
|
|||||||
ChildrenMapInsert,
|
ChildrenMapInsert,
|
||||||
ChildrenMapUpdate,
|
ChildrenMapUpdate,
|
||||||
ChildrenMapDelete,
|
ChildrenMapDelete,
|
||||||
|
DeltaMapInsert,
|
||||||
|
DeltaMapUpdate,
|
||||||
|
DeltaMapDelete,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BlockPBValue {
|
export interface BlockPBValue {
|
||||||
@ -227,6 +233,8 @@ export interface BlockPBValue {
|
|||||||
parent: string;
|
parent: string;
|
||||||
children: string;
|
children: string;
|
||||||
data: string;
|
data: string;
|
||||||
|
external_id?: string;
|
||||||
|
external_type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SplitRelationship {
|
export enum SplitRelationship {
|
||||||
@ -308,7 +316,7 @@ export interface EditorProps {
|
|||||||
decorateSelection?: RangeStaticNoId;
|
decorateSelection?: RangeStaticNoId;
|
||||||
temporarySelection?: RangeStaticNoId;
|
temporarySelection?: RangeStaticNoId;
|
||||||
onSelectionChange?: (range: RangeStaticNoId | null, oldRange: RangeStaticNoId | null, source?: Sources) => void;
|
onSelectionChange?: (range: RangeStaticNoId | null, oldRange: RangeStaticNoId | null, source?: Sources) => void;
|
||||||
onChange?: (delta: Delta, oldDelta: Delta, source?: Sources) => void;
|
onChange: (ops: Op[], newDelta: Delta) => void;
|
||||||
onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,22 +2,23 @@ import {
|
|||||||
FlowyError,
|
FlowyError,
|
||||||
DocumentDataPB,
|
DocumentDataPB,
|
||||||
OpenDocumentPayloadPB,
|
OpenDocumentPayloadPB,
|
||||||
CreateDocumentPayloadPB,
|
|
||||||
ApplyActionPayloadPB,
|
ApplyActionPayloadPB,
|
||||||
BlockActionPB,
|
BlockActionPB,
|
||||||
CloseDocumentPayloadPB,
|
CloseDocumentPayloadPB,
|
||||||
DocumentRedoUndoPayloadPB,
|
DocumentRedoUndoPayloadPB,
|
||||||
DocumentRedoUndoResponsePB,
|
DocumentRedoUndoResponsePB,
|
||||||
|
TextDeltaPayloadPB,
|
||||||
} from '@/services/backend';
|
} from '@/services/backend';
|
||||||
import { Result } from 'ts-results';
|
import { Result } from 'ts-results';
|
||||||
import {
|
import {
|
||||||
DocumentEventApplyAction,
|
DocumentEventApplyAction,
|
||||||
DocumentEventCloseDocument,
|
DocumentEventCloseDocument,
|
||||||
DocumentEventOpenDocument,
|
DocumentEventOpenDocument,
|
||||||
DocumentEventCreateDocument,
|
|
||||||
DocumentEventCanUndoRedo,
|
DocumentEventCanUndoRedo,
|
||||||
DocumentEventRedo,
|
DocumentEventRedo,
|
||||||
DocumentEventUndo,
|
DocumentEventUndo,
|
||||||
|
DocumentEventCreateText,
|
||||||
|
DocumentEventApplyTextDeltaEvent,
|
||||||
} from '@/services/backend/events/flowy-document2';
|
} from '@/services/backend/events/flowy-document2';
|
||||||
|
|
||||||
export class DocumentBackendService {
|
export class DocumentBackendService {
|
||||||
@ -27,6 +28,7 @@ export class DocumentBackendService {
|
|||||||
const payload = OpenDocumentPayloadPB.fromObject({
|
const payload = OpenDocumentPayloadPB.fromObject({
|
||||||
document_id: this.viewId,
|
document_id: this.viewId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return DocumentEventOpenDocument(payload);
|
return DocumentEventOpenDocument(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -35,13 +37,35 @@ export class DocumentBackendService {
|
|||||||
document_id: this.viewId,
|
document_id: this.viewId,
|
||||||
actions: actions,
|
actions: actions,
|
||||||
});
|
});
|
||||||
|
|
||||||
return DocumentEventApplyAction(payload);
|
return DocumentEventApplyAction(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
createText = (textId: string, defaultDelta?: string): Promise<Result<void, FlowyError>> => {
|
||||||
|
const payload = TextDeltaPayloadPB.fromObject({
|
||||||
|
document_id: this.viewId,
|
||||||
|
text_id: textId,
|
||||||
|
delta: defaultDelta,
|
||||||
|
});
|
||||||
|
|
||||||
|
return DocumentEventCreateText(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
applyTextDelta = (textId: string, delta: string): Promise<Result<void, FlowyError>> => {
|
||||||
|
const payload = TextDeltaPayloadPB.fromObject({
|
||||||
|
document_id: this.viewId,
|
||||||
|
text_id: textId,
|
||||||
|
delta: delta,
|
||||||
|
});
|
||||||
|
|
||||||
|
return DocumentEventApplyTextDeltaEvent(payload);
|
||||||
|
};
|
||||||
|
|
||||||
close = (): Promise<Result<void, FlowyError>> => {
|
close = (): Promise<Result<void, FlowyError>> => {
|
||||||
const payload = CloseDocumentPayloadPB.fromObject({
|
const payload = CloseDocumentPayloadPB.fromObject({
|
||||||
document_id: this.viewId,
|
document_id: this.viewId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return DocumentEventCloseDocument(payload);
|
return DocumentEventCloseDocument(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -49,6 +73,7 @@ export class DocumentBackendService {
|
|||||||
const payload = DocumentRedoUndoPayloadPB.fromObject({
|
const payload = DocumentRedoUndoPayloadPB.fromObject({
|
||||||
document_id: this.viewId,
|
document_id: this.viewId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return DocumentEventCanUndoRedo(payload);
|
return DocumentEventCanUndoRedo(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,6 +81,7 @@ export class DocumentBackendService {
|
|||||||
const payload = DocumentRedoUndoPayloadPB.fromObject({
|
const payload = DocumentRedoUndoPayloadPB.fromObject({
|
||||||
document_id: this.viewId,
|
document_id: this.viewId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return DocumentEventUndo(payload);
|
return DocumentEventUndo(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -63,6 +89,7 @@ export class DocumentBackendService {
|
|||||||
const payload = DocumentRedoUndoPayloadPB.fromObject({
|
const payload = DocumentRedoUndoPayloadPB.fromObject({
|
||||||
document_id: this.viewId,
|
document_id: this.viewId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return DocumentEventRedo(payload);
|
return DocumentEventRedo(payload);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -10,11 +10,10 @@ import {
|
|||||||
ChildrenPB,
|
ChildrenPB,
|
||||||
} from '@/services/backend';
|
} from '@/services/backend';
|
||||||
import { DocumentObserver } from './document_observer';
|
import { DocumentObserver } from './document_observer';
|
||||||
import * as Y from 'yjs';
|
|
||||||
import { get } from '@/appflowy_app/utils/tool';
|
import { get } from '@/appflowy_app/utils/tool';
|
||||||
import { blockPB2Node } from '$app/utils/document/block';
|
import { blockPB2Node } from '$app/utils/document/block';
|
||||||
import { Log } from '$app/utils/log';
|
import { Log } from '$app/utils/log';
|
||||||
import { BLOCK_MAP_NAME, CHILDREN_MAP_NAME, META_NAME } from '$app/constants/document/name';
|
import { BLOCK_MAP_NAME, CHILDREN_MAP_NAME, META_NAME, TEXT_MAP_NAME } from '$app/constants/document/name';
|
||||||
|
|
||||||
export class DocumentController {
|
export class DocumentController {
|
||||||
private readonly backendService: DocumentBackendService;
|
private readonly backendService: DocumentBackendService;
|
||||||
@ -28,6 +27,10 @@ export class DocumentController {
|
|||||||
this.observer = new DocumentObserver(documentId);
|
this.observer = new DocumentObserver(documentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get backend() {
|
||||||
|
return this.backendService;
|
||||||
|
}
|
||||||
|
|
||||||
open = async (): Promise<DocumentData> => {
|
open = async (): Promise<DocumentData> => {
|
||||||
await this.observer.subscribe({
|
await this.observer.subscribe({
|
||||||
didReceiveUpdate: this.updated,
|
didReceiveUpdate: this.updated,
|
||||||
@ -44,20 +47,36 @@ export class DocumentController {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
const children: Record<string, string[]> = {};
|
const children: Record<string, string[]> = {};
|
||||||
|
const deltaMap: Record<string, string> = {};
|
||||||
|
|
||||||
get<Map<string, ChildrenPB>>(document.val, [META_NAME, CHILDREN_MAP_NAME]).forEach((child, key) => {
|
get<Map<string, ChildrenPB>>(document.val, [META_NAME, CHILDREN_MAP_NAME]).forEach((child, key) => {
|
||||||
children[key] = child.children;
|
children[key] = child.children;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
get<Map<string, string>>(document.val, [META_NAME, TEXT_MAP_NAME]).forEach((delta, key) => {
|
||||||
|
deltaMap[key] = delta;
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
rootId: document.val.page_id,
|
rootId: document.val.page_id,
|
||||||
nodes,
|
nodes,
|
||||||
children,
|
children,
|
||||||
|
deltaMap,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(document.val);
|
return Promise.reject(document.val);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
applyTextDelta = async (textId: string, delta: string) => {
|
||||||
|
const result = await this.backendService.applyTextDelta(textId, delta);
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(result.err);
|
||||||
|
};
|
||||||
|
|
||||||
applyActions = async (actions: ReturnType<typeof BlockActionPB.prototype.toObject>[]) => {
|
applyActions = async (actions: ReturnType<typeof BlockActionPB.prototype.toObject>[]) => {
|
||||||
Log.debug('applyActions', actions);
|
Log.debug('applyActions', actions);
|
||||||
if (actions.length === 0) return;
|
if (actions.length === 0) return;
|
||||||
@ -65,17 +84,40 @@ export class DocumentController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getInsertAction = (node: Node, prevId: string | null) => {
|
getInsertAction = (node: Node, prevId: string | null) => {
|
||||||
// Here to make sure the delta is correct
|
|
||||||
this.composeDelta(node);
|
|
||||||
return {
|
return {
|
||||||
action: BlockActionTypePB.Insert,
|
action: BlockActionTypePB.Insert,
|
||||||
payload: this.getActionPayloadByNode(node, prevId),
|
payload: this.getActionPayloadByNode(node, prevId),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getInsertTextActions = (node: Node, delta: string, prevId: string | null) => {
|
||||||
|
const textId = node.externalId;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
action: BlockActionTypePB.InsertText,
|
||||||
|
payload: {
|
||||||
|
text_id: textId,
|
||||||
|
delta,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
this.getInsertAction(node, prevId),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
getApplyTextDeltaAction = (node: Node, delta: string) => {
|
||||||
|
const textId = node.externalId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
action: BlockActionTypePB.ApplyTextDelta,
|
||||||
|
payload: {
|
||||||
|
text_id: textId,
|
||||||
|
delta,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
getUpdateAction = (node: Node) => {
|
getUpdateAction = (node: Node) => {
|
||||||
// Here to make sure the delta is correct
|
|
||||||
this.composeDelta(node);
|
|
||||||
return {
|
return {
|
||||||
action: BlockActionTypePB.Update,
|
action: BlockActionTypePB.Update,
|
||||||
payload: this.getActionPayloadByNode(node, ''),
|
payload: this.getActionPayloadByNode(node, ''),
|
||||||
@ -152,31 +194,15 @@ export class DocumentController {
|
|||||||
children_id: node.children,
|
children_id: node.children,
|
||||||
data: JSON.stringify(node.data),
|
data: JSON.stringify(node.data),
|
||||||
ty: node.type,
|
ty: node.type,
|
||||||
|
external_id: node.externalId,
|
||||||
|
external_type: node.externalType,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
private composeDelta = (node: Node) => {
|
|
||||||
const delta = node.data.delta;
|
|
||||||
|
|
||||||
if (!delta) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we use yjs to compose delta, it can make sure the delta is correct
|
|
||||||
// for example, if we insert a text at the end of the line, the delta will be [{ insert: 'hello' }, { insert: " world" }]
|
|
||||||
// but if we use yjs to compose the delta, the delta will be [{ insert: 'hello world' }]
|
|
||||||
const ydoc = new Y.Doc();
|
|
||||||
const ytext = ydoc.getText(node.id);
|
|
||||||
|
|
||||||
ytext.applyDelta(delta);
|
|
||||||
Object.assign(node.data, { delta: ytext.toDelta() });
|
|
||||||
};
|
|
||||||
|
|
||||||
private updated = (payload: Uint8Array) => {
|
private updated = (payload: Uint8Array) => {
|
||||||
if (!this.onDocChange) return;
|
if (!this.onDocChange) return;
|
||||||
const { events, is_remote } = DocEventPB.deserializeBinary(payload);
|
const { events, is_remote } = DocEventPB.deserializeBinary(payload);
|
||||||
|
|
||||||
Log.debug('DocumentController', 'updated', { events, is_remote });
|
|
||||||
events.forEach((blockEvent) => {
|
events.forEach((blockEvent) => {
|
||||||
blockEvent.event.forEach((_payload) => {
|
blockEvent.event.forEach((_payload) => {
|
||||||
this.onDocChange?.({
|
this.onDocChange?.({
|
||||||
|
@ -115,8 +115,9 @@ export class AuthBackendService {
|
|||||||
return UserEventSignIn(payload);
|
return UserEventSignIn(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
signUp = (params: { name: string; email: string; password: string }) => {
|
signUp = (params: { name: string; email: string; password: string; }) => {
|
||||||
const payload = SignUpPayloadPB.fromObject({ name: params.name, email: params.email, password: params.password });
|
const deviceId = nanoid(8);
|
||||||
|
const payload = SignUpPayloadPB.fromObject({ name: params.name, email: params.email, password: params.password, device_id: deviceId });
|
||||||
|
|
||||||
return UserEventSignUp(payload);
|
return UserEventSignUp(payload);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { rectSelectionActions } from '$app_reducers/document/slice';
|
|
||||||
import { getDuplicateActions } from '$app/utils/document/action';
|
import { getDuplicateActions } from '$app/utils/document/action';
|
||||||
import { RootState } from '$app/stores/store';
|
import { RootState } from '$app/stores/store';
|
||||||
import { DOCUMENT_NAME } from '$app/constants/document/name';
|
import { DOCUMENT_NAME } from '$app/constants/document/name';
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { DocumentState } from '$app/interfaces/document';
|
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { blockConfig } from '$app/constants/document/config';
|
import { blockConfig } from '$app/constants/document/config';
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
export * from './delete';
|
export * from './delete';
|
||||||
export * from './duplicate';
|
export * from './duplicate';
|
||||||
export * from './insert';
|
export * from './insert';
|
||||||
export * from './merge';
|
|
||||||
export * from './update';
|
export * from './update';
|
||||||
export * from './indent';
|
export * from './indent';
|
||||||
export * from './outdent';
|
export * from './outdent';
|
||||||
|
@ -1,47 +1,75 @@
|
|||||||
import { BlockData, BlockType, DocumentState } from '$app/interfaces/document';
|
import { BlockData, BlockType } from '$app/interfaces/document';
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { newBlock } from '$app/utils/document/block';
|
import { generateId, newBlock } from '$app/utils/document/block';
|
||||||
import { RootState } from '$app/stores/store';
|
import { RootState } from '$app/stores/store';
|
||||||
import { DOCUMENT_NAME } from '$app/constants/document/name';
|
import { DOCUMENT_NAME } from '$app/constants/document/name';
|
||||||
|
import { BlockDeltaOperator } from '$app/utils/document/block_delta';
|
||||||
|
import Delta from 'quill-delta';
|
||||||
|
|
||||||
export const insertAfterNodeThunk = createAsyncThunk(
|
export const insertAfterNodeThunk = createAsyncThunk(
|
||||||
'document/insertAfterNode',
|
'document/insertAfterNode',
|
||||||
async (payload: { id: string; controller: DocumentController; data?: BlockData<any>; type?: BlockType }, thunkAPI) => {
|
async (
|
||||||
const {
|
payload: {
|
||||||
controller,
|
id: string;
|
||||||
type = BlockType.TextBlock,
|
controller: DocumentController;
|
||||||
data = {
|
type: BlockType;
|
||||||
delta: [],
|
data?: BlockData<any>;
|
||||||
},
|
defaultDelta?: Delta;
|
||||||
id,
|
},
|
||||||
} = payload;
|
thunkAPI
|
||||||
|
) => {
|
||||||
|
const { controller, id, type, data, defaultDelta } = payload;
|
||||||
const { getState } = thunkAPI;
|
const { getState } = thunkAPI;
|
||||||
const state = getState() as RootState;
|
const state = getState() as RootState;
|
||||||
const docId = controller.documentId;
|
const docId = controller.documentId;
|
||||||
const docState = state[DOCUMENT_NAME][docId];
|
const documentState = state[DOCUMENT_NAME][docId];
|
||||||
const node = docState.nodes[id];
|
const node = documentState.nodes[id];
|
||||||
|
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
const parentId = node.parent;
|
const parentId = node.parent;
|
||||||
|
|
||||||
if (!parentId) return;
|
if (!parentId) return;
|
||||||
// create new node
|
// create new node
|
||||||
const newNode = newBlock<any>(type, parentId, data);
|
const actions = [];
|
||||||
let nodeId = newNode.id;
|
let newNodeId;
|
||||||
const actions = [controller.getInsertAction(newNode, node.id)];
|
const deltaOperator = new BlockDeltaOperator(documentState, controller);
|
||||||
|
|
||||||
|
if (defaultDelta) {
|
||||||
|
newNodeId = generateId();
|
||||||
|
actions.push(
|
||||||
|
...deltaOperator.getNewTextLineActions({
|
||||||
|
blockId: newNodeId,
|
||||||
|
parentId,
|
||||||
|
prevId: node.id,
|
||||||
|
delta: defaultDelta,
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const newNode = newBlock<any>(type, parentId, data);
|
||||||
|
|
||||||
|
actions.push(controller.getInsertAction(newNode, node.id));
|
||||||
|
newNodeId = newNode.id;
|
||||||
|
}
|
||||||
|
|
||||||
if (type === BlockType.DividerBlock) {
|
if (type === BlockType.DividerBlock) {
|
||||||
const newTextNode = newBlock<any>(BlockType.TextBlock, parentId, {
|
const nodeId = generateId();
|
||||||
delta: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
nodeId = newTextNode.id;
|
actions.push(
|
||||||
actions.push(controller.getInsertAction(newTextNode, newNode.id));
|
...deltaOperator.getNewTextLineActions({
|
||||||
|
blockId: nodeId,
|
||||||
|
parentId,
|
||||||
|
prevId: newNodeId,
|
||||||
|
delta: new Delta([{ insert: '' }]),
|
||||||
|
type: BlockType.TextBlock,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
newNodeId = nodeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
await controller.applyActions(actions);
|
await controller.applyActions(actions);
|
||||||
|
|
||||||
return nodeId;
|
return newNodeId;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
|
||||||
import { DocumentState } from '$app/interfaces/document';
|
|
||||||
import Delta from 'quill-delta';
|
|
||||||
import { blockConfig } from '$app/constants/document/config';
|
|
||||||
import { getMoveChildrenActions } from '$app/utils/document/action';
|
|
||||||
import { RootState } from '$app/stores/store';
|
|
||||||
import { DOCUMENT_NAME } from '$app/constants/document/name';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge two blocks
|
|
||||||
* 1. merge delta
|
|
||||||
* 2. move children
|
|
||||||
* 3. delete current block
|
|
||||||
*/
|
|
||||||
export const mergeDeltaThunk = createAsyncThunk(
|
|
||||||
'document/mergeDelta',
|
|
||||||
async (payload: { sourceId: string; targetId: string; controller: DocumentController }, thunkAPI) => {
|
|
||||||
const { sourceId, targetId, controller } = payload;
|
|
||||||
const { getState } = thunkAPI;
|
|
||||||
const state = getState() as RootState;
|
|
||||||
const docId = controller.documentId;
|
|
||||||
const docState = state[DOCUMENT_NAME][docId];
|
|
||||||
const target = docState.nodes[targetId];
|
|
||||||
const source = docState.nodes[sourceId];
|
|
||||||
|
|
||||||
if (!target || !source) return;
|
|
||||||
const targetDelta = new Delta(target.data.delta);
|
|
||||||
const sourceDelta = new Delta(source.data.delta);
|
|
||||||
const mergeDelta = targetDelta.concat(sourceDelta);
|
|
||||||
const ops = mergeDelta.ops;
|
|
||||||
const updateAction = controller.getUpdateAction({
|
|
||||||
...target,
|
|
||||||
data: {
|
|
||||||
...target.data,
|
|
||||||
delta: ops,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const actions = [updateAction];
|
|
||||||
// move children
|
|
||||||
const children = docState.children[source.children].map((id) => docState.nodes[id]);
|
|
||||||
const moveActions = getMoveChildrenActions({
|
|
||||||
controller,
|
|
||||||
children,
|
|
||||||
target,
|
|
||||||
});
|
|
||||||
|
|
||||||
actions.push(...moveActions);
|
|
||||||
// delete current block
|
|
||||||
const deleteAction = controller.getDeleteAction(source);
|
|
||||||
|
|
||||||
actions.push(deleteAction);
|
|
||||||
|
|
||||||
await controller.applyActions(actions);
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,4 +1,4 @@
|
|||||||
import { BlockData, DocumentState } from '$app/interfaces/document';
|
import { BlockData } from '$app/interfaces/document';
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import Delta, { Op } from 'quill-delta';
|
import Delta, { Op } from 'quill-delta';
|
||||||
@ -6,19 +6,51 @@ import { RootState } from '$app/stores/store';
|
|||||||
import { DOCUMENT_NAME } from '$app/constants/document/name';
|
import { DOCUMENT_NAME } from '$app/constants/document/name';
|
||||||
import { updatePageName } from '$app_reducers/pages/async_actions';
|
import { updatePageName } from '$app_reducers/pages/async_actions';
|
||||||
import { getDeltaText } from '$app/utils/document/delta';
|
import { getDeltaText } from '$app/utils/document/delta';
|
||||||
|
import { BlockDeltaOperator } from '$app/utils/document/block_delta';
|
||||||
|
import { openMention, closeMention } from '$app_reducers/document/async-actions/mention';
|
||||||
|
|
||||||
|
const updateNodeDeltaAfterThunk = createAsyncThunk(
|
||||||
|
'document/updateNodeDeltaAfter',
|
||||||
|
async (
|
||||||
|
payload: { docId: string; id: string; ops: Op[]; newDelta: Delta; oldDelta: Delta; controller: DocumentController },
|
||||||
|
thunkAPI
|
||||||
|
) => {
|
||||||
|
const { dispatch } = thunkAPI;
|
||||||
|
const { docId, ops, oldDelta, newDelta } = payload;
|
||||||
|
const insertOps = ops.filter((op) => op.insert !== undefined);
|
||||||
|
|
||||||
|
const deleteOps = ops.filter((op) => op.delete !== undefined);
|
||||||
|
const oldText = getDeltaText(oldDelta);
|
||||||
|
const newText = getDeltaText(newDelta);
|
||||||
|
const deleteText = oldText.slice(newText.length);
|
||||||
|
|
||||||
|
if (insertOps.length === 1 && insertOps[0].insert === '@') {
|
||||||
|
dispatch(openMention({ docId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteOps.length === 1 && deleteText === '@') {
|
||||||
|
dispatch(closeMention({ docId }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const updateNodeDeltaThunk = createAsyncThunk(
|
export const updateNodeDeltaThunk = createAsyncThunk(
|
||||||
'document/updateNodeDelta',
|
'document/updateNodeDelta',
|
||||||
async (payload: { id: string; delta: Op[]; controller: DocumentController }, thunkAPI) => {
|
async (payload: { id: string; ops: Op[]; newDelta: Delta; controller: DocumentController }, thunkAPI) => {
|
||||||
const { id, delta, controller } = payload;
|
const { id, ops, newDelta, controller } = payload;
|
||||||
const { getState, dispatch } = thunkAPI;
|
const { getState, dispatch } = thunkAPI;
|
||||||
const state = getState() as RootState;
|
const state = getState() as RootState;
|
||||||
const docId = controller.documentId;
|
const docId = controller.documentId;
|
||||||
const docState = state[DOCUMENT_NAME][docId];
|
const docState = state[DOCUMENT_NAME][docId];
|
||||||
const node = docState.nodes[id];
|
const node = docState.nodes[id];
|
||||||
const oldDelta = new Delta(node.data.delta);
|
|
||||||
const newDelta = new Delta(delta);
|
|
||||||
|
|
||||||
|
const deltaOperator = new BlockDeltaOperator(docState, controller);
|
||||||
|
const oldDelta = deltaOperator.getDeltaWithBlockId(id);
|
||||||
|
|
||||||
|
if (!oldDelta) return;
|
||||||
|
const diff = oldDelta?.diff(newDelta);
|
||||||
|
|
||||||
|
if (ops.length === 0 || diff?.ops.length === 0) return;
|
||||||
// If the node is the root node, update the page name
|
// If the node is the root node, update the page name
|
||||||
if (!node.parent) {
|
if (!node.parent) {
|
||||||
await dispatch(
|
await dispatch(
|
||||||
@ -30,18 +62,10 @@ export const updateNodeDeltaThunk = createAsyncThunk(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const diffDelta = newDelta.diff(oldDelta);
|
if (!node.externalId) return;
|
||||||
|
|
||||||
if (diffDelta.ops.length === 0) return;
|
await controller.applyTextDelta(node.externalId, JSON.stringify(ops));
|
||||||
|
await dispatch(updateNodeDeltaAfterThunk({ docId, id, ops, newDelta, oldDelta, controller }));
|
||||||
const newData = { ...node.data, delta };
|
|
||||||
|
|
||||||
await controller.applyActions([
|
|
||||||
controller.getUpdateAction({
|
|
||||||
...node,
|
|
||||||
data: newData,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,19 +1,6 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { RootState } from '$app/stores/store';
|
import { BlockCopyData } from '$app/interfaces/document';
|
||||||
import { getMiddleIds, getMoveChildrenActions, getStartAndEndIdsByRange } from '$app/utils/document/action';
|
|
||||||
import { BlockCopyData, BlockType, DocumentBlockJSON } from '$app/interfaces/document';
|
|
||||||
import Delta from 'quill-delta';
|
|
||||||
import { getDeltaByRange } from '$app/utils/document/delta';
|
|
||||||
import { deleteRangeAndInsertThunk } from '$app_reducers/document/async-actions/range';
|
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import {
|
|
||||||
generateBlocks,
|
|
||||||
getAppendBlockDeltaAction,
|
|
||||||
getCopyBlock,
|
|
||||||
getInsertBlockActions,
|
|
||||||
} from '$app/utils/document/copy_paste';
|
|
||||||
import { rangeActions } from '$app_reducers/document/slice';
|
|
||||||
import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name';
|
|
||||||
|
|
||||||
export const copyThunk = createAsyncThunk<
|
export const copyThunk = createAsyncThunk<
|
||||||
void,
|
void,
|
||||||
@ -23,70 +10,7 @@ export const copyThunk = createAsyncThunk<
|
|||||||
setClipboardData: (data: BlockCopyData) => void;
|
setClipboardData: (data: BlockCopyData) => void;
|
||||||
}
|
}
|
||||||
>('document/copy', async (payload, thunkAPI) => {
|
>('document/copy', async (payload, thunkAPI) => {
|
||||||
const { getState, dispatch } = thunkAPI;
|
// TODO: Migrate to Rust implementation.
|
||||||
const { setClipboardData, isCut = false, controller } = payload;
|
|
||||||
const docId = controller.documentId;
|
|
||||||
const state = getState() as RootState;
|
|
||||||
const document = state[DOCUMENT_NAME][docId];
|
|
||||||
const documentRange = state[RANGE_NAME][docId];
|
|
||||||
const startAndEndIds = getStartAndEndIdsByRange(documentRange);
|
|
||||||
|
|
||||||
if (startAndEndIds.length === 0) return;
|
|
||||||
const result: DocumentBlockJSON[] = [];
|
|
||||||
|
|
||||||
if (startAndEndIds.length === 1) {
|
|
||||||
// copy single block
|
|
||||||
const id = startAndEndIds[0];
|
|
||||||
const node = document.nodes[id];
|
|
||||||
const nodeDelta = new Delta(node.data.delta);
|
|
||||||
const range = documentRange.ranges[id] || { index: 0, length: 0 };
|
|
||||||
const isFull = range.index === 0 && range.length === nodeDelta.length();
|
|
||||||
|
|
||||||
if (isFull) {
|
|
||||||
result.push(getCopyBlock(id, document, documentRange));
|
|
||||||
} else {
|
|
||||||
result.push({
|
|
||||||
type: BlockType.TextBlock,
|
|
||||||
children: [],
|
|
||||||
data: {
|
|
||||||
delta: getDeltaByRange(nodeDelta, range).ops,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// copy multiple blocks
|
|
||||||
const copyIds: string[] = [];
|
|
||||||
const [startId, endId] = startAndEndIds;
|
|
||||||
const middleIds = getMiddleIds(document, startId, endId);
|
|
||||||
|
|
||||||
copyIds.push(startId, ...middleIds, endId);
|
|
||||||
const map = new Map<string, DocumentBlockJSON>();
|
|
||||||
|
|
||||||
copyIds.forEach((id) => {
|
|
||||||
const block = getCopyBlock(id, document, documentRange);
|
|
||||||
|
|
||||||
map.set(id, block);
|
|
||||||
const node = document.nodes[id];
|
|
||||||
const parent = node.parent;
|
|
||||||
|
|
||||||
if (parent && map.has(parent)) {
|
|
||||||
map.get(parent)!.children.push(block);
|
|
||||||
} else {
|
|
||||||
result.push(block);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setClipboardData({
|
|
||||||
json: JSON.stringify(result),
|
|
||||||
// TODO: implement plain text and html
|
|
||||||
text: '',
|
|
||||||
html: '',
|
|
||||||
});
|
|
||||||
if (isCut) {
|
|
||||||
// delete range blocks
|
|
||||||
await dispatch(deleteRangeAndInsertThunk({ controller }));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -106,139 +30,5 @@ export const pasteThunk = createAsyncThunk<
|
|||||||
controller: DocumentController;
|
controller: DocumentController;
|
||||||
}
|
}
|
||||||
>('document/paste', async (payload, thunkAPI) => {
|
>('document/paste', async (payload, thunkAPI) => {
|
||||||
const { getState, dispatch } = thunkAPI;
|
// TODO: Migrate to Rust implementation.
|
||||||
const { data, controller } = payload;
|
|
||||||
|
|
||||||
// delete range blocks
|
|
||||||
await dispatch(deleteRangeAndInsertThunk({ controller }));
|
|
||||||
|
|
||||||
const state = getState() as RootState;
|
|
||||||
const docId = controller.documentId;
|
|
||||||
const document = state[DOCUMENT_NAME][docId];
|
|
||||||
const documentRange = state[RANGE_NAME][docId];
|
|
||||||
|
|
||||||
let pasteData;
|
|
||||||
|
|
||||||
if (data.json) {
|
|
||||||
pasteData = JSON.parse(data.json) as DocumentBlockJSON[];
|
|
||||||
} else if (data.text) {
|
|
||||||
// TODO: implement plain text
|
|
||||||
} else if (data.html) {
|
|
||||||
// TODO: implement html
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pasteData) return;
|
|
||||||
const { caret } = documentRange;
|
|
||||||
|
|
||||||
if (!caret) return;
|
|
||||||
const currentBlock = document.nodes[caret.id];
|
|
||||||
|
|
||||||
if (!currentBlock.parent) return;
|
|
||||||
const pasteBlocks = generateBlocks(pasteData, currentBlock.parent);
|
|
||||||
const currentBlockDelta = new Delta(currentBlock.data.delta);
|
|
||||||
const type = currentBlock.type;
|
|
||||||
const actions = getInsertBlockActions(pasteBlocks, currentBlock.id, controller);
|
|
||||||
const firstPasteBlock = pasteBlocks[0];
|
|
||||||
const firstPasteBlockChildren = pasteBlocks.filter((block) => block.parent === firstPasteBlock.id);
|
|
||||||
|
|
||||||
const lastPasteBlock = pasteBlocks[pasteBlocks.length - 1];
|
|
||||||
|
|
||||||
if (type === BlockType.TextBlock && currentBlockDelta.length() === 0) {
|
|
||||||
// move current block children to first paste block
|
|
||||||
const children = document.children[currentBlock.children].map((id) => document.nodes[id]);
|
|
||||||
const firstPasteBlockLastChild =
|
|
||||||
firstPasteBlockChildren.length > 0 ? firstPasteBlockChildren[firstPasteBlockChildren.length - 1] : undefined;
|
|
||||||
const prevId = firstPasteBlockLastChild ? firstPasteBlockLastChild.id : undefined;
|
|
||||||
const moveChildrenActions = getMoveChildrenActions({
|
|
||||||
target: firstPasteBlock,
|
|
||||||
children,
|
|
||||||
controller,
|
|
||||||
prevId,
|
|
||||||
});
|
|
||||||
|
|
||||||
actions.push(...moveChildrenActions);
|
|
||||||
// delete current block
|
|
||||||
actions.push(controller.getDeleteAction(currentBlock));
|
|
||||||
await controller.applyActions(actions);
|
|
||||||
// set caret to the end of the last paste block
|
|
||||||
dispatch(
|
|
||||||
rangeActions.setCaret({
|
|
||||||
docId,
|
|
||||||
caret: {
|
|
||||||
id: lastPasteBlock.id,
|
|
||||||
index: new Delta(lastPasteBlock.data.delta).length(),
|
|
||||||
length: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// split current block
|
|
||||||
const currentBeforeDelta = getDeltaByRange(currentBlockDelta, { index: 0, length: caret.index });
|
|
||||||
const currentAfterDelta = getDeltaByRange(currentBlockDelta, {
|
|
||||||
index: caret.index,
|
|
||||||
length: currentBlockDelta.length() - caret.index,
|
|
||||||
});
|
|
||||||
|
|
||||||
let newCaret: {
|
|
||||||
id: string;
|
|
||||||
index: number;
|
|
||||||
length: number;
|
|
||||||
};
|
|
||||||
const firstPasteBlockDelta = new Delta(firstPasteBlock.data.delta);
|
|
||||||
const lastPasteBlockDelta = new Delta(lastPasteBlock.data.delta);
|
|
||||||
let mergeDelta = new Delta(currentBeforeDelta.ops).concat(firstPasteBlockDelta);
|
|
||||||
|
|
||||||
if (firstPasteBlock.id !== lastPasteBlock.id) {
|
|
||||||
// update the last block of paste data
|
|
||||||
actions.push(getAppendBlockDeltaAction(lastPasteBlock, currentAfterDelta, false, controller));
|
|
||||||
newCaret = {
|
|
||||||
id: lastPasteBlock.id,
|
|
||||||
index: lastPasteBlockDelta.length(),
|
|
||||||
length: 0,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
newCaret = {
|
|
||||||
id: currentBlock.id,
|
|
||||||
index: mergeDelta.length(),
|
|
||||||
length: 0,
|
|
||||||
};
|
|
||||||
mergeDelta = mergeDelta.concat(currentAfterDelta);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update current block and merge the first block of paste data
|
|
||||||
actions.push(
|
|
||||||
controller.getUpdateAction({
|
|
||||||
...currentBlock,
|
|
||||||
data: {
|
|
||||||
...currentBlock.data,
|
|
||||||
delta: mergeDelta.ops,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// move the first block children of paste data to current block
|
|
||||||
if (firstPasteBlockChildren.length > 0) {
|
|
||||||
const moveChildrenActions = getMoveChildrenActions({
|
|
||||||
target: currentBlock,
|
|
||||||
children: firstPasteBlockChildren,
|
|
||||||
controller,
|
|
||||||
});
|
|
||||||
|
|
||||||
actions.push(...moveChildrenActions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete first block of paste data
|
|
||||||
actions.push(controller.getDeleteAction(firstPasteBlock));
|
|
||||||
await controller.applyActions(actions);
|
|
||||||
// set caret to the end of the last paste block
|
|
||||||
if (!newCaret) return;
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
rangeActions.setCaret({
|
|
||||||
docId,
|
|
||||||
caret: newCaret,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { rangeActions } from '$app_reducers/document/slice';
|
||||||
|
|
||||||
|
export const setCursorRangeThunk = createAsyncThunk(
|
||||||
|
'document/setCursorRange',
|
||||||
|
async (payload: { docId: string; blockId: string; index: number; length?: number }, thunkAPI) => {
|
||||||
|
const { blockId, index, docId, length = 0 } = payload;
|
||||||
|
const { dispatch } = thunkAPI;
|
||||||
|
|
||||||
|
dispatch(rangeActions.initialState(docId));
|
||||||
|
dispatch(
|
||||||
|
rangeActions.setCaret({
|
||||||
|
docId,
|
||||||
|
caret: {
|
||||||
|
id: blockId,
|
||||||
|
index,
|
||||||
|
length,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -4,6 +4,8 @@ import { TextAction } from '$app/interfaces/document';
|
|||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import Delta from 'quill-delta';
|
import Delta from 'quill-delta';
|
||||||
import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name';
|
import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name';
|
||||||
|
import { BlockDeltaOperator } from '$app/utils/document/block_delta';
|
||||||
|
import { BlockActionPB } from '@/services/backend';
|
||||||
|
|
||||||
type FormatValues = Record<string, (boolean | string | undefined)[]>;
|
type FormatValues = Record<string, (boolean | string | undefined)[]>;
|
||||||
|
|
||||||
@ -15,6 +17,7 @@ export const getFormatValuesThunk = createAsyncThunk(
|
|||||||
const document = state[DOCUMENT_NAME][docId];
|
const document = state[DOCUMENT_NAME][docId];
|
||||||
const documentRange = state[RANGE_NAME][docId];
|
const documentRange = state[RANGE_NAME][docId];
|
||||||
const { ranges } = documentRange;
|
const { ranges } = documentRange;
|
||||||
|
const deltaOperator = new BlockDeltaOperator(document);
|
||||||
const mapAttrs = (delta: Delta, format: TextAction) => {
|
const mapAttrs = (delta: Delta, format: TextAction) => {
|
||||||
return delta.ops.map((op) => op.attributes?.[format] as boolean | string | undefined);
|
return delta.ops.map((op) => op.attributes?.[format] as boolean | string | undefined);
|
||||||
};
|
};
|
||||||
@ -23,12 +26,13 @@ export const getFormatValuesThunk = createAsyncThunk(
|
|||||||
|
|
||||||
Object.entries(ranges).forEach(([id, range]) => {
|
Object.entries(ranges).forEach(([id, range]) => {
|
||||||
const node = document.nodes[id];
|
const node = document.nodes[id];
|
||||||
const delta = new Delta(node.data?.delta);
|
|
||||||
const index = range?.index || 0;
|
const index = range?.index || 0;
|
||||||
const length = range?.length || 0;
|
const length = range?.length || 0;
|
||||||
const rangeDelta = delta.slice(index, index + length);
|
const rangeDelta = deltaOperator.sliceDeltaWithBlockId(node.id, index, index + length);
|
||||||
|
|
||||||
formatValues[id] = mapAttrs(rangeDelta, format);
|
if (rangeDelta) {
|
||||||
|
formatValues[id] = mapAttrs(rangeDelta, format);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return formatValues;
|
return formatValues;
|
||||||
}
|
}
|
||||||
@ -73,6 +77,7 @@ export const toggleFormatThunk = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatValue = isActive ? null : true;
|
const formatValue = isActive ? null : true;
|
||||||
|
|
||||||
await dispatch(formatThunk({ format, value: formatValue, controller }));
|
await dispatch(formatThunk({ format, value: formatValue, controller }));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -87,23 +92,24 @@ export const formatThunk = createAsyncThunk(
|
|||||||
const document = state[DOCUMENT_NAME][docId];
|
const document = state[DOCUMENT_NAME][docId];
|
||||||
const documentRange = state[RANGE_NAME][docId];
|
const documentRange = state[RANGE_NAME][docId];
|
||||||
const { ranges } = documentRange;
|
const { ranges } = documentRange;
|
||||||
|
const deltaOperator = new BlockDeltaOperator(document, controller);
|
||||||
|
const actions: ReturnType<typeof BlockActionPB.prototype.toObject>[] = [];
|
||||||
|
|
||||||
const actions = Object.entries(ranges).map(([id, range]) => {
|
Object.entries(ranges).forEach(([id, range]) => {
|
||||||
const node = document.nodes[id];
|
const node = document.nodes[id];
|
||||||
const delta = new Delta(node.data?.delta);
|
const delta = deltaOperator.getDeltaWithBlockId(node.id);
|
||||||
|
|
||||||
|
if (!delta) return;
|
||||||
const index = range?.index || 0;
|
const index = range?.index || 0;
|
||||||
const length = range?.length || 0;
|
const length = range?.length || 0;
|
||||||
const diffDelta: Delta = new Delta();
|
const diffDelta: Delta = new Delta();
|
||||||
diffDelta.retain(index).retain(length, { [format]: value });
|
|
||||||
const newDelta = delta.compose(diffDelta);
|
|
||||||
|
|
||||||
return controller.getUpdateAction({
|
diffDelta.retain(index).retain(length, { [format]: value });
|
||||||
...node,
|
const action = deltaOperator.getApplyDeltaAction(node.id, diffDelta);
|
||||||
data: {
|
|
||||||
...node.data,
|
if (action) {
|
||||||
delta: newDelta.ops,
|
actions.push(action);
|
||||||
},
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await controller.applyActions(actions);
|
await controller.applyActions(actions);
|
||||||
|
@ -1,35 +1,29 @@
|
|||||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { DocumentController } from "$app/stores/effects/document/document_controller";
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { BlockType, RangeStatic, SplitRelationship } from "$app/interfaces/document";
|
import { BlockType, RangeStatic } from '$app/interfaces/document';
|
||||||
import { turnToTextBlockThunk } from "$app_reducers/document/async-actions/turn_to";
|
import { turnToTextBlockThunk } from '$app_reducers/document/async-actions/turn_to';
|
||||||
import {
|
import {
|
||||||
findNextHasDeltaNode,
|
|
||||||
findPrevHasDeltaNode,
|
|
||||||
getInsertEnterNodeAction,
|
|
||||||
getLeftCaretByRange,
|
getLeftCaretByRange,
|
||||||
getRightCaretByRange,
|
getRightCaretByRange,
|
||||||
transformToNextLineCaret,
|
transformToNextLineCaret,
|
||||||
transformToPrevLineCaret
|
transformToPrevLineCaret,
|
||||||
} from "$app/utils/document/action";
|
} from '$app/utils/document/action';
|
||||||
import Delta from "quill-delta";
|
import { indentNodeThunk, outdentNodeThunk } from '$app_reducers/document/async-actions/blocks';
|
||||||
import { indentNodeThunk, mergeDeltaThunk, outdentNodeThunk } from "$app_reducers/document/async-actions/blocks";
|
import { rangeActions } from '$app_reducers/document/slice';
|
||||||
import { rangeActions } from "$app_reducers/document/slice";
|
import { RootState } from '$app/stores/store';
|
||||||
import { RootState } from "$app/stores/store";
|
import { Keyboard } from '$app/constants/document/keyboard';
|
||||||
import { blockConfig } from "$app/constants/document/config";
|
import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name';
|
||||||
import { Keyboard } from "$app/constants/document/keyboard";
|
import { getPreviousWordIndex } from '$app/utils/document/delta';
|
||||||
import { DOCUMENT_NAME, RANGE_NAME } from "$app/constants/document/name";
|
import { updatePageName } from '$app_reducers/pages/async_actions';
|
||||||
import { getDeltaText, getPreviousWordIndex } from "$app/utils/document/delta";
|
import { BlockDeltaOperator } from '$app/utils/document/block_delta';
|
||||||
import { updatePageName } from "$app_reducers/pages/async_actions";
|
import { setCursorRangeThunk } from '$app_reducers/document/async-actions/cursor';
|
||||||
import { newBlock } from "$app/utils/document/block";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
- Deletes a block using the backspace or delete key.
|
- Deletes a block using the backspace or delete key.
|
||||||
- If the block is not a text block, it is converted into a text block.
|
- If the block is not a text block, it is converted into a text block.
|
||||||
- If the block is a text block:
|
- If the block is a text block:
|
||||||
- - If the block is the first line, it is merged into the document title, and a new line is inserted.
|
- - If the block has a next sibling, it is merged into the prev line (including its children).
|
||||||
- - If the block is not the first line and it has a next sibling, it is merged into the previous line (including the previous sibling and its parent).
|
- - If the block has no next sibling, it is outdented (moved to a higher level in the hierarchy).
|
||||||
- - If the block has no next sibling and is not a top-level block, it is outdented (moved to a higher level in the hierarchy).
|
|
||||||
*/
|
*/
|
||||||
export const backspaceDeleteActionForBlockThunk = createAsyncThunk(
|
export const backspaceDeleteActionForBlockThunk = createAsyncThunk(
|
||||||
'document/backspaceDeleteActionForBlock',
|
'document/backspaceDeleteActionForBlock',
|
||||||
@ -41,6 +35,14 @@ export const backspaceDeleteActionForBlockThunk = createAsyncThunk(
|
|||||||
const node = state.nodes[id];
|
const node = state.nodes[id];
|
||||||
|
|
||||||
if (!node.parent) return;
|
if (!node.parent) return;
|
||||||
|
const deltaOperator = new BlockDeltaOperator(state, controller, async (name: string) => {
|
||||||
|
await dispatch(
|
||||||
|
updatePageName({
|
||||||
|
id: docId,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
const parent = state.nodes[node.parent];
|
const parent = state.nodes[node.parent];
|
||||||
const children = state.children[parent.children];
|
const children = state.children[parent.children];
|
||||||
const index = children.indexOf(id);
|
const index = children.indexOf(id);
|
||||||
@ -53,65 +55,31 @@ export const backspaceDeleteActionForBlockThunk = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isTopLevel = parent.type === BlockType.PageBlock;
|
const isTopLevel = parent.type === BlockType.PageBlock;
|
||||||
const isFirstLine = isTopLevel && index === 0;
|
|
||||||
|
|
||||||
if (isTopLevel && isFirstLine) {
|
|
||||||
// merge to document title and insert a new line
|
|
||||||
const parentDelta = new Delta(parent.data.delta);
|
|
||||||
const caretIndex = parentDelta.length();
|
|
||||||
const caret = {
|
|
||||||
id: parent.id,
|
|
||||||
index: caretIndex,
|
|
||||||
length: 0,
|
|
||||||
};
|
|
||||||
const titleDelta = parentDelta.concat(new Delta(node.data.delta));
|
|
||||||
await dispatch(updatePageName({ id: docId, name: getDeltaText(titleDelta) }));
|
|
||||||
const actions = [
|
|
||||||
controller.getDeleteAction(node),
|
|
||||||
]
|
|
||||||
|
|
||||||
if (!nextNodeId) {
|
|
||||||
// insert a new line
|
|
||||||
const block = newBlock<any>(BlockType.TextBlock, parent.id, {
|
|
||||||
delta: [{ insert: "" }]
|
|
||||||
});
|
|
||||||
actions.push(controller.getInsertAction(block, null));
|
|
||||||
}
|
|
||||||
await controller.applyActions(actions);
|
|
||||||
dispatch(rangeActions.initialState(docId));
|
|
||||||
dispatch(
|
|
||||||
rangeActions.setCaret({
|
|
||||||
docId,
|
|
||||||
caret,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isTopLevel || nextNodeId) {
|
if (isTopLevel || nextNodeId) {
|
||||||
// merge to previous line
|
// merge to previous line
|
||||||
const prevLine = findPrevHasDeltaNode(state, id);
|
const prevLineId = deltaOperator.findPrevTextLine(id);
|
||||||
if (!prevLine) return;
|
|
||||||
const caretIndex = new Delta(prevLine.data.delta).length();
|
if (!prevLineId) return;
|
||||||
|
|
||||||
|
const res = await deltaOperator.mergeText(prevLineId, id);
|
||||||
|
|
||||||
|
if (!res) return;
|
||||||
const caret = {
|
const caret = {
|
||||||
id: prevLine.id,
|
id: res.id,
|
||||||
index: caretIndex,
|
index: res.index,
|
||||||
length: 0,
|
length: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
await dispatch(
|
|
||||||
mergeDeltaThunk({
|
|
||||||
sourceId: id,
|
|
||||||
targetId: prevLine.id,
|
|
||||||
controller,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dispatch(rangeActions.initialState(docId));
|
|
||||||
dispatch(
|
dispatch(
|
||||||
rangeActions.setCaret({
|
setCursorRangeThunk({
|
||||||
docId,
|
docId,
|
||||||
caret,
|
blockId: caret.id,
|
||||||
|
index: caret.index,
|
||||||
|
length: caret.length,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,10 +89,9 @@ export const backspaceDeleteActionForBlockThunk = createAsyncThunk(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert a new node after the current node by pressing enter.
|
* enter key handler
|
||||||
* 1. Split the current node into two nodes.
|
* 1. If node is empty, and it is not a text block, turn it into a text block.
|
||||||
* 2. Insert a new node after the current node.
|
* 2. Otherwise, split the node into two nodes.
|
||||||
* 3. Move the children of the current node to the new node if needed.
|
|
||||||
*/
|
*/
|
||||||
export const enterActionForBlockThunk = createAsyncThunk(
|
export const enterActionForBlockThunk = createAsyncThunk(
|
||||||
'document/insertNodeByEnter',
|
'document/insertNodeByEnter',
|
||||||
@ -138,81 +105,45 @@ export const enterActionForBlockThunk = createAsyncThunk(
|
|||||||
const caret = state[RANGE_NAME][docId]?.caret;
|
const caret = state[RANGE_NAME][docId]?.caret;
|
||||||
|
|
||||||
if (!node || !caret || caret.id !== id) return;
|
if (!node || !caret || caret.id !== id) return;
|
||||||
const delta = new Delta(node.data.delta);
|
|
||||||
|
|
||||||
const nodeDelta = delta.slice(0, caret.index);
|
const deltaOperator = new BlockDeltaOperator(documentState, controller, async (name: string) => {
|
||||||
|
await dispatch(
|
||||||
const insertNodeDelta = new Delta(node.data.delta).slice(caret.index + caret.length);
|
updatePageName({
|
||||||
|
id: docId,
|
||||||
const isDocumentTitle = !node.parent;
|
name,
|
||||||
// update page title and insert a new line
|
|
||||||
if (isDocumentTitle) {
|
|
||||||
// update page title
|
|
||||||
await dispatch(updatePageName({
|
|
||||||
id: docId,
|
|
||||||
name: getDeltaText(nodeDelta),
|
|
||||||
}));
|
|
||||||
// insert a new line
|
|
||||||
const block = newBlock<any>(BlockType.TextBlock, node.id, {
|
|
||||||
delta: insertNodeDelta.ops,
|
|
||||||
});
|
|
||||||
const insertNodeAction = controller.getInsertAction(block, null);
|
|
||||||
await controller.applyActions([insertNodeAction]);
|
|
||||||
dispatch(rangeActions.initialState(docId));
|
|
||||||
dispatch(
|
|
||||||
rangeActions.setCaret({
|
|
||||||
docId,
|
|
||||||
caret: {
|
|
||||||
id: block.id,
|
|
||||||
index: 0,
|
|
||||||
length: 0,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return;
|
});
|
||||||
}
|
const isDocumentTitle = !node.parent;
|
||||||
|
let newLineId;
|
||||||
|
|
||||||
if (delta.length() === 0 && node.type !== BlockType.TextBlock) {
|
const delta = deltaOperator.getDeltaWithBlockId(node.id);
|
||||||
|
|
||||||
|
if (!delta) return;
|
||||||
|
if (!isDocumentTitle && delta.length() === 0 && node.type !== BlockType.TextBlock) {
|
||||||
// If the node is not a text block, turn it to a text block
|
// If the node is not a text block, turn it to a text block
|
||||||
await dispatch(turnToTextBlockThunk({ id, controller }));
|
await dispatch(turnToTextBlockThunk({ id, controller }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newLineId = await deltaOperator.splitText(
|
||||||
const insertNodeAction = getInsertEnterNodeAction(node, insertNodeDelta, controller);
|
{
|
||||||
|
id: node.id,
|
||||||
if (!insertNodeAction) return;
|
index: caret.index,
|
||||||
|
|
||||||
const updateNode = {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
delta: nodeDelta.ops,
|
|
||||||
},
|
},
|
||||||
};
|
{
|
||||||
|
id: node.id,
|
||||||
|
index: caret.index + caret.length,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const children = documentState.children[node.children];
|
if (!newLineId) return;
|
||||||
const needMoveChildren = blockConfig[node.type].splitProps?.nextLineRelationShip === SplitRelationship.NextSibling;
|
|
||||||
const moveChildrenAction = needMoveChildren
|
|
||||||
? controller.getMoveChildrenAction(
|
|
||||||
children.map((id) => documentState.nodes[id]),
|
|
||||||
insertNodeAction.id,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
const actions = [insertNodeAction.action, controller.getUpdateAction(updateNode), ...moveChildrenAction];
|
|
||||||
|
|
||||||
await controller.applyActions(actions);
|
|
||||||
|
|
||||||
dispatch(rangeActions.initialState(docId));
|
|
||||||
dispatch(
|
dispatch(
|
||||||
rangeActions.setCaret({
|
setCursorRangeThunk({
|
||||||
docId,
|
docId,
|
||||||
caret: {
|
blockId: newLineId,
|
||||||
id: insertNodeAction.id,
|
index: 0,
|
||||||
index: 0,
|
length: 0,
|
||||||
length: 0,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -275,7 +206,10 @@ export const leftActionForBlockThunk = createAsyncThunk(
|
|||||||
|
|
||||||
if (!node || !caret || id !== caret.id) return;
|
if (!node || !caret || id !== caret.id) return;
|
||||||
let newCaret: RangeStatic;
|
let newCaret: RangeStatic;
|
||||||
|
const deltaOperator = new BlockDeltaOperator(documentState);
|
||||||
|
const delta = deltaOperator.getDeltaWithBlockId(node.id);
|
||||||
|
|
||||||
|
if (!delta) return;
|
||||||
if (caret.length > 0) {
|
if (caret.length > 0) {
|
||||||
newCaret = {
|
newCaret = {
|
||||||
id,
|
id,
|
||||||
@ -284,7 +218,6 @@ export const leftActionForBlockThunk = createAsyncThunk(
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
if (caret.index > 0) {
|
if (caret.index > 0) {
|
||||||
const delta = new Delta(node.data.delta);
|
|
||||||
const newIndex = getPreviousWordIndex(delta, caret.index);
|
const newIndex = getPreviousWordIndex(delta, caret.index);
|
||||||
|
|
||||||
newCaret = {
|
newCaret = {
|
||||||
@ -293,13 +226,14 @@ export const leftActionForBlockThunk = createAsyncThunk(
|
|||||||
length: 0,
|
length: 0,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const prevNode = findPrevHasDeltaNode(documentState, id);
|
const prevNodeId = deltaOperator.findPrevTextLine(id);
|
||||||
|
|
||||||
if (!prevNode) return;
|
if (!prevNodeId) return;
|
||||||
const prevDelta = new Delta(prevNode.data.delta);
|
const prevDelta = deltaOperator.getDeltaWithBlockId(prevNodeId);
|
||||||
|
|
||||||
|
if (!prevDelta) return;
|
||||||
newCaret = {
|
newCaret = {
|
||||||
id: prevNode.id,
|
id: prevNodeId,
|
||||||
index: prevDelta.length(),
|
index: prevDelta.length(),
|
||||||
length: 0,
|
length: 0,
|
||||||
};
|
};
|
||||||
@ -333,7 +267,10 @@ export const rightActionForBlockThunk = createAsyncThunk(
|
|||||||
|
|
||||||
if (!node || !caret || id !== caret.id) return;
|
if (!node || !caret || id !== caret.id) return;
|
||||||
let newCaret: RangeStatic;
|
let newCaret: RangeStatic;
|
||||||
const delta = new Delta(node.data.delta);
|
const deltaOperator = new BlockDeltaOperator(documentState);
|
||||||
|
const delta = deltaOperator.getDeltaWithBlockId(node.id);
|
||||||
|
|
||||||
|
if (!delta) return;
|
||||||
const deltaLength = delta.length();
|
const deltaLength = delta.length();
|
||||||
|
|
||||||
if (caret.length > 0) {
|
if (caret.length > 0) {
|
||||||
@ -352,11 +289,11 @@ export const rightActionForBlockThunk = createAsyncThunk(
|
|||||||
length: 0,
|
length: 0,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const nextNode = findNextHasDeltaNode(documentState, id);
|
const nextNodeId = deltaOperator.findNextTextLine(id);
|
||||||
|
|
||||||
if (!nextNode) return;
|
if (!nextNodeId) return;
|
||||||
newCaret = {
|
newCaret = {
|
||||||
id: nextNode.id,
|
id: nextNodeId,
|
||||||
index: 0,
|
index: 0,
|
||||||
length: 0,
|
length: 0,
|
||||||
};
|
};
|
||||||
|
@ -2,10 +2,10 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
|
|||||||
import { RootState } from '$app/stores/store';
|
import { RootState } from '$app/stores/store';
|
||||||
import { DOCUMENT_NAME, MENTION_NAME, RANGE_NAME } from '$app/constants/document/name';
|
import { DOCUMENT_NAME, MENTION_NAME, RANGE_NAME } from '$app/constants/document/name';
|
||||||
import Delta from 'quill-delta';
|
import Delta from 'quill-delta';
|
||||||
import { getDeltaText } from '$app/utils/document/delta';
|
|
||||||
import { mentionActions } from '$app_reducers/document/mention_slice';
|
import { mentionActions } from '$app_reducers/document/mention_slice';
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { rangeActions } from '$app_reducers/document/slice';
|
import { BlockDeltaOperator } from '$app/utils/document/block_delta';
|
||||||
|
import { setCursorRangeThunk } from '$app_reducers/document/async-actions/cursor';
|
||||||
|
|
||||||
export enum MentionType {
|
export enum MentionType {
|
||||||
PAGE = 'page',
|
PAGE = 'page',
|
||||||
@ -15,27 +15,16 @@ export const openMention = createAsyncThunk('document/mention/open', async (payl
|
|||||||
const { dispatch, getState } = thunkAPI;
|
const { dispatch, getState } = thunkAPI;
|
||||||
const state = getState() as RootState;
|
const state = getState() as RootState;
|
||||||
const rangeState = state[RANGE_NAME][docId];
|
const rangeState = state[RANGE_NAME][docId];
|
||||||
|
const documentState = state[DOCUMENT_NAME][docId];
|
||||||
const { caret } = rangeState;
|
const { caret } = rangeState;
|
||||||
|
|
||||||
if (!caret) return;
|
if (!caret) return;
|
||||||
const { id, index } = caret;
|
const { id } = caret;
|
||||||
const node = state[DOCUMENT_NAME][docId].nodes[id];
|
const node = documentState.nodes[id];
|
||||||
|
|
||||||
if (!node.parent) {
|
if (!node.parent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nodeDelta = new Delta(node.data?.delta);
|
|
||||||
|
|
||||||
const beforeDelta = nodeDelta.slice(0, index);
|
|
||||||
const beforeText = getDeltaText(beforeDelta);
|
|
||||||
let canOpenMention = !beforeText;
|
|
||||||
if (!canOpenMention) {
|
|
||||||
if (index === 1) {
|
|
||||||
canOpenMention = beforeText.endsWith('@');
|
|
||||||
} else {
|
|
||||||
canOpenMention = beforeText.endsWith(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!canOpenMention) return;
|
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
mentionActions.open({
|
mentionActions.open({
|
||||||
@ -45,6 +34,17 @@ export const openMention = createAsyncThunk('document/mention/open', async (payl
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const closeMention = createAsyncThunk('document/mention/close', async (payload: { docId: string }, thunkAPI) => {
|
||||||
|
const { docId } = payload;
|
||||||
|
const { dispatch } = thunkAPI;
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
mentionActions.close({
|
||||||
|
docId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const formatMention = createAsyncThunk(
|
export const formatMention = createAsyncThunk(
|
||||||
'document/mention/format',
|
'document/mention/format',
|
||||||
async (
|
async (
|
||||||
@ -58,12 +58,17 @@ export const formatMention = createAsyncThunk(
|
|||||||
const mentionState = state[MENTION_NAME][docId];
|
const mentionState = state[MENTION_NAME][docId];
|
||||||
const { blockId } = mentionState;
|
const { blockId } = mentionState;
|
||||||
const rangeState = state[RANGE_NAME][docId];
|
const rangeState = state[RANGE_NAME][docId];
|
||||||
|
const documentState = state[DOCUMENT_NAME][docId];
|
||||||
const caret = rangeState.caret;
|
const caret = rangeState.caret;
|
||||||
|
|
||||||
if (!caret) return;
|
if (!caret) return;
|
||||||
const index = caret.index - searchTextLength;
|
const index = caret.index - searchTextLength;
|
||||||
|
|
||||||
const node = state[DOCUMENT_NAME][docId].nodes[blockId];
|
const deltaOperator = new BlockDeltaOperator(documentState);
|
||||||
const nodeDelta = new Delta(node.data?.delta);
|
|
||||||
|
const nodeDelta = deltaOperator.getDeltaWithBlockId(blockId);
|
||||||
|
|
||||||
|
if (!nodeDelta) return;
|
||||||
const diffDelta = new Delta()
|
const diffDelta = new Delta()
|
||||||
.retain(index)
|
.retain(index)
|
||||||
.delete(searchTextLength)
|
.delete(searchTextLength)
|
||||||
@ -73,18 +78,17 @@ export const formatMention = createAsyncThunk(
|
|||||||
[type]: value,
|
[type]: value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const newDelta = nodeDelta.compose(diffDelta);
|
const applyTextDeltaAction = deltaOperator.getApplyDeltaAction(blockId, diffDelta);
|
||||||
const updateAction = controller.getUpdateAction({
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
delta: newDelta.ops,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await controller.applyActions([updateAction]);
|
if (!applyTextDeltaAction) return;
|
||||||
|
await controller.applyActions([applyTextDeltaAction]);
|
||||||
dispatch(rangeActions.initialState(docId));
|
dispatch(
|
||||||
dispatch(rangeActions.setCaret({ docId, caret: { id: blockId, index, length: 0 } }));
|
setCursorRangeThunk({
|
||||||
|
docId,
|
||||||
|
blockId,
|
||||||
|
index,
|
||||||
|
length: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -8,8 +8,9 @@ import { blockConfig } from '$app/constants/document/config';
|
|||||||
import Delta, { Op } from 'quill-delta';
|
import Delta, { Op } from 'quill-delta';
|
||||||
import { getDeltaText } from '$app/utils/document/delta';
|
import { getDeltaText } from '$app/utils/document/delta';
|
||||||
import { RootState } from '$app/stores/store';
|
import { RootState } from '$app/stores/store';
|
||||||
import { DOCUMENT_NAME } from '$app/constants/document/name';
|
import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name';
|
||||||
import { blockEditActions } from '$app_reducers/document/block_edit_slice';
|
import { blockEditActions } from '$app_reducers/document/block_edit_slice';
|
||||||
|
import { BlockDeltaOperator } from '$app/utils/document/block_delta';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* add block below click
|
* add block below click
|
||||||
@ -26,13 +27,19 @@ export const addBlockBelowClickThunk = createAsyncThunk(
|
|||||||
const node = state.nodes[id];
|
const node = state.nodes[id];
|
||||||
|
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
const delta = (node.data.delta as Op[]) || [];
|
const deltaOperator = new BlockDeltaOperator(state, controller);
|
||||||
const text = delta.map((d) => d.insert).join('');
|
const delta = deltaOperator.getDeltaWithBlockId(id);
|
||||||
|
|
||||||
// if current block is not empty, insert a new block after current block
|
// if current block is not empty, insert a new block after current block
|
||||||
if (node.type !== BlockType.TextBlock || text !== '') {
|
if (!delta || delta.length() > 1) {
|
||||||
const { payload: newBlockId } = await dispatch(
|
const { payload: newBlockId } = await dispatch(
|
||||||
insertAfterNodeThunk({ id: id, type: BlockType.TextBlock, controller, data: { delta: [] } })
|
insertAfterNodeThunk({
|
||||||
|
id: id,
|
||||||
|
type: BlockType.TextBlock,
|
||||||
|
controller,
|
||||||
|
data: {},
|
||||||
|
defaultDelta: new Delta([{ insert: '' }]),
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newBlockId) {
|
if (newBlockId) {
|
||||||
@ -59,99 +66,3 @@ export const addBlockBelowClickThunk = createAsyncThunk(
|
|||||||
dispatch(slashCommandActions.openSlashCommand({ docId, blockId: id }));
|
dispatch(slashCommandActions.openSlashCommand({ docId, blockId: id }));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* slash command action be triggered
|
|
||||||
* 1. if current block is empty, operate on current block
|
|
||||||
* 2. if current block is not empty, insert a new block after current block and operate on new block
|
|
||||||
*/
|
|
||||||
export const triggerSlashCommandActionThunk = createAsyncThunk(
|
|
||||||
'document/slashCommandAction',
|
|
||||||
async (
|
|
||||||
payload: {
|
|
||||||
id: string;
|
|
||||||
controller: DocumentController;
|
|
||||||
props: {
|
|
||||||
data?: BlockData<any>;
|
|
||||||
type: BlockType;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
thunkAPI
|
|
||||||
) => {
|
|
||||||
const { id, controller, props } = payload;
|
|
||||||
const { dispatch, getState } = thunkAPI;
|
|
||||||
const docId = controller.documentId;
|
|
||||||
const state = getState() as RootState;
|
|
||||||
const document = state[DOCUMENT_NAME][docId];
|
|
||||||
const node = document.nodes[id];
|
|
||||||
|
|
||||||
if (!node) return;
|
|
||||||
const delta = new Delta(node.data.delta);
|
|
||||||
const text = getDeltaText(delta);
|
|
||||||
const defaultData = blockConfig[props.type].defaultData;
|
|
||||||
|
|
||||||
if (node.type === BlockType.TextBlock && (text === '' || text === '/')) {
|
|
||||||
const { payload: newId } = await dispatch(
|
|
||||||
turnToBlockThunk({
|
|
||||||
id,
|
|
||||||
controller,
|
|
||||||
type: props.type,
|
|
||||||
data: {
|
|
||||||
...defaultData,
|
|
||||||
...props.data,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
blockEditActions.setBlockEditState({
|
|
||||||
id: docId,
|
|
||||||
state: {
|
|
||||||
id: newId as string,
|
|
||||||
editing: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if current block has slash command, remove slash command
|
|
||||||
if (text.slice(0, 1) === '/') {
|
|
||||||
const updateNode = {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
delta: delta.slice(1, delta.length()).ops,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await controller.applyActions([controller.getUpdateAction(updateNode)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertNodePayload = await dispatch(
|
|
||||||
insertAfterNodeThunk({
|
|
||||||
id,
|
|
||||||
controller,
|
|
||||||
type: props.type,
|
|
||||||
data: defaultData,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const newBlockId = insertNodePayload.payload as string;
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
rangeActions.setCaret({
|
|
||||||
docId,
|
|
||||||
caret: { id: newBlockId, index: 0, length: 0 },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
blockEditActions.setBlockEditState({
|
|
||||||
id: docId,
|
|
||||||
state: {
|
|
||||||
id: newBlockId,
|
|
||||||
editing: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
@ -1,19 +1,13 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { RootState } from '$app/stores/store';
|
import { RootState } from '$app/stores/store';
|
||||||
import { rangeActions } from '$app_reducers/document/slice';
|
import { rangeActions } from '$app_reducers/document/slice';
|
||||||
import Delta from 'quill-delta';
|
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import {
|
import { getMiddleIds, getStartAndEndIdsByRange } from '$app/utils/document/action';
|
||||||
getAfterMergeCaretByRange,
|
import { RangeState } from '$app/interfaces/document';
|
||||||
getInsertEnterNodeAction,
|
|
||||||
getMergeEndDeltaToStartActionsByRange,
|
|
||||||
getMiddleIds,
|
|
||||||
getMiddleIdsByRange,
|
|
||||||
getStartAndEndExtentDelta,
|
|
||||||
} from '$app/utils/document/action';
|
|
||||||
import { RangeState, SplitRelationship } from '$app/interfaces/document';
|
|
||||||
import { blockConfig } from '$app/constants/document/config';
|
|
||||||
import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name';
|
import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name';
|
||||||
|
import { BlockDeltaOperator } from '$app/utils/document/block_delta';
|
||||||
|
import { setCursorRangeThunk } from '$app_reducers/document/async-actions/cursor';
|
||||||
|
import { updatePageName } from '$app_reducers/pages/async_actions';
|
||||||
|
|
||||||
interface storeRangeThunkPayload {
|
interface storeRangeThunkPayload {
|
||||||
docId: string;
|
docId: string;
|
||||||
@ -71,18 +65,20 @@ export const storeRangeThunk = createAsyncThunk('document/storeRange', (payload:
|
|||||||
|
|
||||||
// amend anchor range because slatejs will stop update selection when dragging quickly
|
// amend anchor range because slatejs will stop update selection when dragging quickly
|
||||||
const isForward = anchor.point.y < focus.point.y;
|
const isForward = anchor.point.y < focus.point.y;
|
||||||
const anchorDelta = new Delta(documentState.nodes[anchor.id].data.delta);
|
const deltaOperator = new BlockDeltaOperator(documentState);
|
||||||
|
|
||||||
if (isForward) {
|
if (isForward) {
|
||||||
const selectedDelta = anchorDelta.slice(anchorIndex);
|
const selectedDelta = deltaOperator.sliceDeltaWithBlockId(anchor.id, anchorIndex);
|
||||||
|
|
||||||
|
if (!selectedDelta) return;
|
||||||
ranges[anchor.id] = {
|
ranges[anchor.id] = {
|
||||||
index: anchorIndex,
|
index: anchorIndex,
|
||||||
length: selectedDelta.length(),
|
length: selectedDelta.length(),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const selectedDelta = anchorDelta.slice(0, anchorIndex + anchorLength);
|
const selectedDelta = deltaOperator.sliceDeltaWithBlockId(anchor.id, 0, anchorIndex + anchorLength);
|
||||||
|
|
||||||
|
if (!selectedDelta) return;
|
||||||
ranges[anchor.id] = {
|
ranges[anchor.id] = {
|
||||||
index: 0,
|
index: 0,
|
||||||
length: selectedDelta.length(),
|
length: selectedDelta.length(),
|
||||||
@ -98,8 +94,10 @@ export const storeRangeThunk = createAsyncThunk('document/storeRange', (payload:
|
|||||||
middleIds.forEach((id) => {
|
middleIds.forEach((id) => {
|
||||||
const node = documentState.nodes[id];
|
const node = documentState.nodes[id];
|
||||||
|
|
||||||
if (!node || !node.data.delta) return;
|
if (!node) return;
|
||||||
const delta = new Delta(node.data.delta);
|
const delta = deltaOperator.getDeltaWithBlockId(node.id);
|
||||||
|
|
||||||
|
if (!delta) return;
|
||||||
const rangeStatic = {
|
const rangeStatic = {
|
||||||
index: 0,
|
index: 0,
|
||||||
length: delta.length(),
|
length: delta.length(),
|
||||||
@ -120,48 +118,52 @@ export const storeRangeThunk = createAsyncThunk('document/storeRange', (payload:
|
|||||||
* delete range and insert delta
|
* delete range and insert delta
|
||||||
* 1. merge start and end delta to start node and delete end node
|
* 1. merge start and end delta to start node and delete end node
|
||||||
* 2. delete middle nodes
|
* 2. delete middle nodes
|
||||||
|
* 3. move end node's children to start node
|
||||||
* 3. clear range
|
* 3. clear range
|
||||||
*/
|
*/
|
||||||
export const deleteRangeAndInsertThunk = createAsyncThunk(
|
export const deleteRangeAndInsertThunk = createAsyncThunk(
|
||||||
'document/deleteRange',
|
'document/deleteRange',
|
||||||
async (payload: { controller: DocumentController; insertDelta?: Delta }, thunkAPI) => {
|
async (payload: { controller: DocumentController; insertChar?: string }, thunkAPI) => {
|
||||||
const { controller, insertDelta } = payload;
|
const { controller, insertChar } = payload;
|
||||||
const docId = controller.documentId;
|
const docId = controller.documentId;
|
||||||
const { getState, dispatch } = thunkAPI;
|
const { getState, dispatch } = thunkAPI;
|
||||||
const state = getState() as RootState;
|
const state = getState() as RootState;
|
||||||
const rangeState = state[RANGE_NAME][docId];
|
const rangeState = state[RANGE_NAME][docId];
|
||||||
const documentState = state[DOCUMENT_NAME][docId];
|
const documentState = state[DOCUMENT_NAME][docId];
|
||||||
|
const deltaOperator = new BlockDeltaOperator(documentState, controller, async (name: string) => {
|
||||||
const actions = [];
|
await dispatch(
|
||||||
// get merge actions
|
updatePageName({
|
||||||
const mergeActions = getMergeEndDeltaToStartActionsByRange(state, controller, insertDelta);
|
id: docId,
|
||||||
|
name,
|
||||||
if (mergeActions) {
|
|
||||||
actions.push(...mergeActions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get middle nodes
|
|
||||||
const middleIds = getMiddleIdsByRange(rangeState, documentState);
|
|
||||||
// delete middle nodes
|
|
||||||
const deleteMiddleNodesActions = middleIds?.map((id) => controller.getDeleteAction(documentState.nodes[id])) || [];
|
|
||||||
|
|
||||||
actions.push(...deleteMiddleNodesActions);
|
|
||||||
|
|
||||||
const caret = getAfterMergeCaretByRange(rangeState, insertDelta);
|
|
||||||
|
|
||||||
if (actions.length === 0) return;
|
|
||||||
// apply actions
|
|
||||||
await controller.applyActions(actions);
|
|
||||||
// clear range
|
|
||||||
dispatch(rangeActions.initialState(docId));
|
|
||||||
if (caret) {
|
|
||||||
dispatch(
|
|
||||||
rangeActions.setCaret({
|
|
||||||
docId,
|
|
||||||
caret,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
const [startId, endId] = getStartAndEndIdsByRange(rangeState);
|
||||||
|
const startSelection = rangeState.ranges[startId];
|
||||||
|
const endSelection = rangeState.ranges[endId];
|
||||||
|
|
||||||
|
if (!startSelection || !endSelection) return;
|
||||||
|
const id = await deltaOperator.deleteText(
|
||||||
|
{
|
||||||
|
id: startId,
|
||||||
|
index: startSelection.index,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: endId,
|
||||||
|
index: endSelection.length,
|
||||||
|
},
|
||||||
|
insertChar
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!id) return;
|
||||||
|
dispatch(
|
||||||
|
setCursorRangeThunk({
|
||||||
|
docId,
|
||||||
|
blockId: id,
|
||||||
|
index: insertChar ? startSelection.index + insertChar.length : startSelection.index,
|
||||||
|
length: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -169,7 +171,7 @@ export const deleteRangeAndInsertThunk = createAsyncThunk(
|
|||||||
* delete range and insert enter
|
* delete range and insert enter
|
||||||
* 1. if shift key, insert '\n' to start node and concat end node delta
|
* 1. if shift key, insert '\n' to start node and concat end node delta
|
||||||
* 2. if not shift key
|
* 2. if not shift key
|
||||||
* 2.1 insert node under start node, and concat end node delta to insert node
|
* 2.1 insert node under start node
|
||||||
* 2.2 filter rest children and move to insert node, if need
|
* 2.2 filter rest children and move to insert node, if need
|
||||||
* 3. delete middle nodes
|
* 3. delete middle nodes
|
||||||
* 4. clear range
|
* 4. clear range
|
||||||
@ -183,84 +185,39 @@ export const deleteRangeAndInsertEnterThunk = createAsyncThunk(
|
|||||||
const state = getState() as RootState;
|
const state = getState() as RootState;
|
||||||
const rangeState = state[RANGE_NAME][docId];
|
const rangeState = state[RANGE_NAME][docId];
|
||||||
const documentState = state[DOCUMENT_NAME][docId];
|
const documentState = state[DOCUMENT_NAME][docId];
|
||||||
const actions = [];
|
const deltaOperator = new BlockDeltaOperator(documentState, controller, async (name: string) => {
|
||||||
|
await dispatch(
|
||||||
const { startDelta, endDelta, endNode, startNode } = getStartAndEndExtentDelta(documentState, rangeState) || {};
|
updatePageName({
|
||||||
|
id: docId,
|
||||||
if (!startDelta || !endDelta || !endNode || !startNode) return;
|
name,
|
||||||
|
|
||||||
// get middle nodes
|
|
||||||
const middleIds = getMiddleIds(documentState, startNode.id, endNode.id);
|
|
||||||
|
|
||||||
let newStartDelta = new Delta(startDelta);
|
|
||||||
let caret = null;
|
|
||||||
|
|
||||||
if (shiftKey) {
|
|
||||||
newStartDelta = newStartDelta.insert('\n').concat(endDelta);
|
|
||||||
caret = getAfterMergeCaretByRange(rangeState, new Delta().insert('\n'));
|
|
||||||
} else {
|
|
||||||
const insertNodeDelta = new Delta(endDelta);
|
|
||||||
const insertNodeAction = getInsertEnterNodeAction(startNode, insertNodeDelta, controller);
|
|
||||||
|
|
||||||
if (!insertNodeAction) return;
|
|
||||||
actions.push(insertNodeAction.action);
|
|
||||||
caret = {
|
|
||||||
id: insertNodeAction.id,
|
|
||||||
index: 0,
|
|
||||||
length: 0,
|
|
||||||
};
|
|
||||||
// move start node children to insert node
|
|
||||||
const needMoveChildren =
|
|
||||||
blockConfig[startNode.type].splitProps?.nextLineRelationShip === SplitRelationship.NextSibling;
|
|
||||||
|
|
||||||
if (needMoveChildren) {
|
|
||||||
// filter children by delete middle ids
|
|
||||||
const children = documentState.children[startNode.children].filter((id) => middleIds?.includes(id));
|
|
||||||
const moveChildrenAction = needMoveChildren
|
|
||||||
? controller.getMoveChildrenAction(
|
|
||||||
children.map((id) => documentState.nodes[id]),
|
|
||||||
insertNodeAction.id,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
actions.push(...moveChildrenAction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// udpate start node
|
|
||||||
const updateAction = controller.getUpdateAction({
|
|
||||||
...startNode,
|
|
||||||
data: {
|
|
||||||
...startNode.data,
|
|
||||||
delta: newStartDelta.ops,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (endNode.id !== startNode.id) {
|
|
||||||
// delete end node
|
|
||||||
const deleteAction = controller.getDeleteAction(endNode);
|
|
||||||
|
|
||||||
actions.push(updateAction, deleteAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete middle nodes
|
|
||||||
const deleteMiddleNodesActions = middleIds?.map((id) => controller.getDeleteAction(documentState.nodes[id])) || [];
|
|
||||||
|
|
||||||
actions.push(...deleteMiddleNodesActions);
|
|
||||||
|
|
||||||
// apply actions
|
|
||||||
await controller.applyActions(actions);
|
|
||||||
|
|
||||||
// clear range
|
|
||||||
dispatch(rangeActions.initialState(docId));
|
|
||||||
if (caret) {
|
|
||||||
dispatch(
|
|
||||||
rangeActions.setCaret({
|
|
||||||
docId,
|
|
||||||
caret,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
const [startId, endId] = getStartAndEndIdsByRange(rangeState);
|
||||||
|
const startSelection = rangeState.ranges[startId];
|
||||||
|
const endSelection = rangeState.ranges[endId];
|
||||||
|
|
||||||
|
if (!startSelection || !endSelection) return;
|
||||||
|
const newLineId = await deltaOperator.splitText(
|
||||||
|
{
|
||||||
|
id: startId,
|
||||||
|
index: startSelection.index,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: endId,
|
||||||
|
index: endSelection.length,
|
||||||
|
},
|
||||||
|
shiftKey
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!newLineId) return;
|
||||||
|
dispatch(
|
||||||
|
setCursorRangeThunk({
|
||||||
|
docId,
|
||||||
|
blockId: newLineId,
|
||||||
|
index: shiftKey ? startSelection.index + 1 : 0,
|
||||||
|
length: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -7,6 +7,7 @@ import { TemporaryState, TemporaryType } from '$app/interfaces/document';
|
|||||||
import { temporaryActions } from '$app_reducers/document/temporary_slice';
|
import { temporaryActions } from '$app_reducers/document/temporary_slice';
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { rangeActions } from '$app_reducers/document/slice';
|
import { rangeActions } from '$app_reducers/document/slice';
|
||||||
|
import { BlockDeltaOperator } from '$app/utils/document/block_delta';
|
||||||
|
|
||||||
export const createTemporary = createAsyncThunk(
|
export const createTemporary = createAsyncThunk(
|
||||||
'document/temporary/create',
|
'document/temporary/create',
|
||||||
@ -15,6 +16,8 @@ export const createTemporary = createAsyncThunk(
|
|||||||
const { dispatch, getState } = thunkAPI;
|
const { dispatch, getState } = thunkAPI;
|
||||||
const state = getState() as RootState;
|
const state = getState() as RootState;
|
||||||
let temporaryState = payload.state;
|
let temporaryState = payload.state;
|
||||||
|
const documentState = state[DOCUMENT_NAME][docId];
|
||||||
|
const deltaOperator = new BlockDeltaOperator(documentState);
|
||||||
|
|
||||||
if (!temporaryState && type) {
|
if (!temporaryState && type) {
|
||||||
const caret = state[RANGE_NAME][docId].caret;
|
const caret = state[RANGE_NAME][docId].caret;
|
||||||
@ -28,12 +31,22 @@ export const createTemporary = createAsyncThunk(
|
|||||||
index,
|
index,
|
||||||
length,
|
length,
|
||||||
};
|
};
|
||||||
|
|
||||||
const node = state[DOCUMENT_NAME][docId].nodes[id];
|
const node = state[DOCUMENT_NAME][docId].nodes[id];
|
||||||
const nodeDelta = new Delta(node.data?.delta);
|
const nodeDelta = deltaOperator.getDeltaWithBlockId(node.id);
|
||||||
const rangeDelta = getDeltaByRange(nodeDelta, selection);
|
|
||||||
const text = getDeltaText(rangeDelta);
|
if (!nodeDelta) return;
|
||||||
|
const rangeDelta = deltaOperator.sliceDeltaWithBlockId(
|
||||||
|
node.id,
|
||||||
|
selection.index,
|
||||||
|
selection.index + selection.length
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!rangeDelta) return;
|
||||||
|
const text = deltaOperator.getDeltaText(rangeDelta);
|
||||||
|
|
||||||
const data = newDataWithTemporaryType(type, text);
|
const data = newDataWithTemporaryType(type, text);
|
||||||
|
|
||||||
temporaryState = {
|
temporaryState = {
|
||||||
id,
|
id,
|
||||||
selection,
|
selection,
|
||||||
@ -71,17 +84,17 @@ export const formatTemporary = createAsyncThunk(
|
|||||||
async (payload: { controller: DocumentController }, thunkAPI) => {
|
async (payload: { controller: DocumentController }, thunkAPI) => {
|
||||||
const { controller } = payload;
|
const { controller } = payload;
|
||||||
const docId = controller.documentId;
|
const docId = controller.documentId;
|
||||||
const { dispatch, getState } = thunkAPI;
|
const { getState } = thunkAPI;
|
||||||
const state = getState() as RootState;
|
const state = getState() as RootState;
|
||||||
const temporaryState = state[TEMPORARY_NAME][docId];
|
const temporaryState = state[TEMPORARY_NAME][docId];
|
||||||
|
const documentState = state[DOCUMENT_NAME][docId];
|
||||||
|
const deltaOperator = new BlockDeltaOperator(documentState, controller);
|
||||||
|
|
||||||
if (!temporaryState) {
|
if (!temporaryState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, selection, type, data } = temporaryState;
|
const { id, selection, type, data } = temporaryState;
|
||||||
const node = state[DOCUMENT_NAME][docId].nodes[id];
|
|
||||||
const nodeDelta = new Delta(node.data?.delta);
|
|
||||||
const { index, length } = selection;
|
const { index, length } = selection;
|
||||||
const diffDelta: Delta = new Delta();
|
const diffDelta: Delta = new Delta();
|
||||||
let newSelection = selection;
|
let newSelection = selection;
|
||||||
@ -106,6 +119,7 @@ export const formatTemporary = createAsyncThunk(
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case TemporaryType.Link: {
|
case TemporaryType.Link: {
|
||||||
if (!data.text) return;
|
if (!data.text) return;
|
||||||
if (!data.href) {
|
if (!data.href) {
|
||||||
@ -115,6 +129,7 @@ export const formatTemporary = createAsyncThunk(
|
|||||||
href: data.href,
|
href: data.href,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
newSelection = {
|
newSelection = {
|
||||||
index: selection.index,
|
index: selection.index,
|
||||||
length: data.text.length,
|
length: data.text.length,
|
||||||
@ -126,17 +141,10 @@ export const formatTemporary = createAsyncThunk(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDelta = nodeDelta.compose(diffDelta);
|
const applyTextDeltaAction = deltaOperator.getApplyDeltaAction(id, diffDelta);
|
||||||
|
|
||||||
const updateAction = controller.getUpdateAction({
|
if (!applyTextDeltaAction) return;
|
||||||
...node,
|
await controller.applyActions([applyTextDeltaAction]);
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
delta: newDelta.ops,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await controller.applyActions([updateAction]);
|
|
||||||
return {
|
return {
|
||||||
...temporaryState,
|
...temporaryState,
|
||||||
selection: newSelection,
|
selection: newSelection,
|
||||||
|
@ -2,9 +2,13 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
|
|||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
import { BlockData, BlockType } from '$app/interfaces/document';
|
import { BlockData, BlockType } from '$app/interfaces/document';
|
||||||
import { blockConfig } from '$app/constants/document/config';
|
import { blockConfig } from '$app/constants/document/config';
|
||||||
import { newBlock } from '$app/utils/document/block';
|
import { generateId, newBlock } from '$app/utils/document/block';
|
||||||
import { rangeActions } from '$app_reducers/document/slice';
|
|
||||||
import { RootState } from '$app/stores/store';
|
import { RootState } from '$app/stores/store';
|
||||||
|
import { BlockDeltaOperator } from '$app/utils/document/block_delta';
|
||||||
|
import Delta from 'quill-delta';
|
||||||
|
import { setCursorRangeThunk } from '$app_reducers/document/async-actions/cursor';
|
||||||
|
import { blockEditActions } from '$app_reducers/document/block_edit_slice';
|
||||||
|
import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* transform to block
|
* transform to block
|
||||||
@ -20,45 +24,94 @@ export const turnToBlockThunk = createAsyncThunk(
|
|||||||
const { id, controller, type, data } = payload;
|
const { id, controller, type, data } = payload;
|
||||||
const docId = controller.documentId;
|
const docId = controller.documentId;
|
||||||
const { dispatch, getState } = thunkAPI;
|
const { dispatch, getState } = thunkAPI;
|
||||||
const state = (getState() as RootState).document[docId];
|
const state = getState() as RootState;
|
||||||
|
const documentState = state[DOCUMENT_NAME][docId];
|
||||||
const node = state.nodes[id];
|
const caret = state[RANGE_NAME][docId].caret;
|
||||||
|
const node = documentState.nodes[id];
|
||||||
|
|
||||||
if (!node.parent) return;
|
if (!node.parent) return;
|
||||||
|
|
||||||
const parent = state.nodes[node.parent];
|
const parent = documentState.nodes[node.parent];
|
||||||
const children = state.children[node.children].map((id) => state.nodes[id]);
|
const children = documentState.children[node.children].map((id) => documentState.nodes[id]);
|
||||||
|
let caretId,
|
||||||
const block = newBlock<any>(type, parent.id, type === BlockType.DividerBlock ? {} : data);
|
caretIndex = caret?.index || 0;
|
||||||
let caretId = block.id;
|
const deltaOperator = new BlockDeltaOperator(documentState, controller);
|
||||||
|
let delta = deltaOperator.getDeltaWithBlockId(node.id);
|
||||||
// insert new block after current block
|
// insert new block after current block
|
||||||
const insertActions = [controller.getInsertAction(block, node.id)];
|
const insertActions = [];
|
||||||
|
|
||||||
if (type === BlockType.DividerBlock) {
|
if (node.type === BlockType.EquationBlock) {
|
||||||
const newTextNode = newBlock<any>(BlockType.TextBlock, parent.id, data);
|
delta = new Delta([{ insert: node.data.formula }]);
|
||||||
|
|
||||||
insertActions.push(controller.getInsertAction(newTextNode, block.id));
|
|
||||||
caretId = newTextNode.id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (delta && type === BlockType.EquationBlock) {
|
||||||
|
data.formula = deltaOperator.getDeltaText(delta);
|
||||||
|
const block = newBlock<any>(type, parent.id, data);
|
||||||
|
|
||||||
|
insertActions.push(controller.getInsertAction(block, node.id));
|
||||||
|
caretId = block.id;
|
||||||
|
caretIndex = 0;
|
||||||
|
} else if (delta && type === BlockType.DividerBlock) {
|
||||||
|
const block = newBlock<any>(type, parent.id, data);
|
||||||
|
|
||||||
|
insertActions.push(controller.getInsertAction(block, node.id));
|
||||||
|
const nodeId = generateId();
|
||||||
|
const actions = deltaOperator.getNewTextLineActions({
|
||||||
|
blockId: nodeId,
|
||||||
|
parentId: parent.id,
|
||||||
|
prevId: block.id || null,
|
||||||
|
delta: delta ? delta : new Delta([{ insert: '' }]),
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
caretId = nodeId;
|
||||||
|
caretIndex = 0;
|
||||||
|
insertActions.push(...actions);
|
||||||
|
} else if (delta) {
|
||||||
|
caretId = generateId();
|
||||||
|
|
||||||
|
const actions = deltaOperator.getNewTextLineActions({
|
||||||
|
blockId: caretId,
|
||||||
|
parentId: parent.id,
|
||||||
|
prevId: node.id,
|
||||||
|
delta: delta,
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
insertActions.push(...actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!caretId) return;
|
||||||
// check if prev node is allowed to have children
|
// check if prev node is allowed to have children
|
||||||
const config = blockConfig[block.type];
|
const config = blockConfig[type];
|
||||||
// if new block is not allowed to have children, move children to parent
|
// if new block is not allowed to have children, move children to parent
|
||||||
const newParent = config.canAddChild ? block : parent;
|
const newParentId = config.canAddChild ? caretId : parent.id;
|
||||||
// if move children to parent, set prev to current block, otherwise the prev is empty
|
// if move children to parent, set prev to current block, otherwise the prev is empty
|
||||||
const newPrev = newParent.id === parent.id ? block.id : '';
|
const newPrev = config.canAddChild ? null : caretId;
|
||||||
const moveChildrenActions = controller.getMoveChildrenAction(children, newParent.id, newPrev);
|
const moveChildrenActions = controller.getMoveChildrenAction(children, newParentId, newPrev);
|
||||||
|
|
||||||
// delete current block
|
// delete current block
|
||||||
const deleteAction = controller.getDeleteAction(node);
|
const deleteAction = controller.getDeleteAction(node);
|
||||||
|
|
||||||
// submit actions
|
// submit actions
|
||||||
await controller.applyActions([...insertActions, ...moveChildrenActions, deleteAction]);
|
await controller.applyActions([...insertActions, ...moveChildrenActions, deleteAction]);
|
||||||
// set cursor in new block
|
|
||||||
dispatch(
|
dispatch(
|
||||||
rangeActions.setCaret({
|
setCursorRangeThunk({
|
||||||
docId,
|
docId,
|
||||||
caret: { id: caretId, index: 0, length: 0 },
|
blockId: caretId,
|
||||||
|
index: caretIndex,
|
||||||
|
length: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
blockEditActions.setBlockEditState({
|
||||||
|
id: docId,
|
||||||
|
state: {
|
||||||
|
id: caretId,
|
||||||
|
editing: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return caretId;
|
return caretId;
|
||||||
@ -75,20 +128,14 @@ export const turnToTextBlockThunk = createAsyncThunk(
|
|||||||
'document/turnToTextBlock',
|
'document/turnToTextBlock',
|
||||||
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
async (payload: { id: string; controller: DocumentController }, thunkAPI) => {
|
||||||
const { id, controller } = payload;
|
const { id, controller } = payload;
|
||||||
const { dispatch, getState } = thunkAPI;
|
const { dispatch } = thunkAPI;
|
||||||
const docId = controller.documentId;
|
|
||||||
const state = (getState() as RootState).document[docId];
|
|
||||||
const node = state.nodes[id];
|
|
||||||
const data = {
|
|
||||||
delta: node.data.delta,
|
|
||||||
};
|
|
||||||
|
|
||||||
await dispatch(
|
await dispatch(
|
||||||
turnToBlockThunk({
|
turnToBlockThunk({
|
||||||
id,
|
id,
|
||||||
controller,
|
controller,
|
||||||
type: BlockType.TextBlock,
|
type: BlockType.TextBlock,
|
||||||
data,
|
data: {},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ export const mentionSlice = createSlice({
|
|||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const { docId, blockId } = action.payload;
|
const { docId, blockId } = action.payload;
|
||||||
|
|
||||||
state[docId] = {
|
state[docId] = {
|
||||||
open: true,
|
open: true,
|
||||||
blockId,
|
blockId,
|
||||||
@ -28,6 +29,7 @@ export const mentionSlice = createSlice({
|
|||||||
},
|
},
|
||||||
close: (state, action: { payload: { docId: string } }) => {
|
close: (state, action: { payload: { docId: string } }) => {
|
||||||
const { docId } = action.payload;
|
const { docId } = action.payload;
|
||||||
|
|
||||||
delete state[docId];
|
delete state[docId];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -15,6 +15,7 @@ import { DOCUMENT_NAME, RANGE_NAME, RECT_RANGE_NAME, SLASH_COMMAND_NAME } from '
|
|||||||
import { blockEditSlice } from '$app_reducers/document/block_edit_slice';
|
import { blockEditSlice } from '$app_reducers/document/block_edit_slice';
|
||||||
import { Op } from 'quill-delta';
|
import { Op } from 'quill-delta';
|
||||||
import { mentionSlice } from '$app_reducers/document/mention_slice';
|
import { mentionSlice } from '$app_reducers/document/mention_slice';
|
||||||
|
import { generateId } from '$app/utils/document/block';
|
||||||
|
|
||||||
const initialState: Record<string, DocumentState> = {};
|
const initialState: Record<string, DocumentState> = {};
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ export const documentSlice = createSlice({
|
|||||||
state[docId] = {
|
state[docId] = {
|
||||||
nodes: {},
|
nodes: {},
|
||||||
children: {},
|
children: {},
|
||||||
|
deltaMap: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
clear: (state, action: PayloadAction<string>) => {
|
clear: (state, action: PayloadAction<string>) => {
|
||||||
@ -52,13 +54,15 @@ export const documentSlice = createSlice({
|
|||||||
docId: string;
|
docId: string;
|
||||||
nodes: Record<string, Node>;
|
nodes: Record<string, Node>;
|
||||||
children: Record<string, string[]>;
|
children: Record<string, string[]>;
|
||||||
|
deltaMap: Record<string, string>;
|
||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const { docId, nodes, children } = action.payload;
|
const { docId, nodes, children, deltaMap } = action.payload;
|
||||||
|
|
||||||
state[docId] = {
|
state[docId] = {
|
||||||
nodes,
|
nodes,
|
||||||
children,
|
children,
|
||||||
|
deltaMap,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -72,10 +76,16 @@ export const documentSlice = createSlice({
|
|||||||
) => {
|
) => {
|
||||||
const { docId, delta, rootId } = action.payload;
|
const { docId, delta, rootId } = action.payload;
|
||||||
const documentState = state[docId];
|
const documentState = state[docId];
|
||||||
|
|
||||||
if (!documentState) return;
|
if (!documentState) return;
|
||||||
const rootNode = documentState.nodes[rootId];
|
const rootNode = documentState.nodes[rootId];
|
||||||
|
|
||||||
if (!rootNode) return;
|
if (!rootNode) return;
|
||||||
rootNode.data.delta = delta;
|
let externalId = rootNode.externalId;
|
||||||
|
|
||||||
|
if (!externalId) externalId = generateId();
|
||||||
|
rootNode.externalId = externalId;
|
||||||
|
documentState.deltaMap[externalId] = JSON.stringify(delta);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
This function listens for changes in the data layer triggered by the data API,
|
This function listens for changes in the data layer triggered by the data API,
|
||||||
|
@ -0,0 +1,277 @@
|
|||||||
|
import { BlockDeltaOperator } from '$app/utils/document/block_delta';
|
||||||
|
import { mockDocument } from './document_state';
|
||||||
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
|
import { generateId } from '$app/utils/document/block';
|
||||||
|
|
||||||
|
jest.mock('nanoid', () => ({ nanoid: jest.fn().mockReturnValue(String(Math.random())) }));
|
||||||
|
|
||||||
|
jest.mock('$app/utils/document/emoji', () => ({
|
||||||
|
randomEmoji: jest.fn().mockReturnValue('👍'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('$app/stores/effects/document/document_observer', () => ({
|
||||||
|
DocumentObserver: jest.fn().mockImplementation(() => ({
|
||||||
|
subscribe: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('$app/stores/effects/document/document_bd_svc', () => ({
|
||||||
|
DocumentBackendService: jest.fn().mockImplementation(() => ({
|
||||||
|
open: jest.fn().mockReturnValue(Promise.resolve({ ok: true, val: mockDocument })),
|
||||||
|
applyActions: jest.fn().mockReturnValue(Promise.resolve({ ok: true })),
|
||||||
|
createText: jest.fn().mockReturnValue(Promise.resolve({ ok: true })),
|
||||||
|
applyTextDelta: jest.fn().mockReturnValue(Promise.resolve({ ok: true })),
|
||||||
|
close: jest.fn().mockReturnValue(Promise.resolve({ ok: true })),
|
||||||
|
canUndoRedo: jest.fn().mockReturnValue(Promise.resolve({ ok: true })),
|
||||||
|
undo: jest.fn().mockReturnValue(Promise.resolve({ ok: true })),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Test BlockDeltaOperator', () => {
|
||||||
|
let operator: BlockDeltaOperator;
|
||||||
|
let controller: DocumentController;
|
||||||
|
beforeEach(() => {
|
||||||
|
controller = new DocumentController(generateId());
|
||||||
|
operator = new BlockDeltaOperator(mockDocument, controller);
|
||||||
|
});
|
||||||
|
test('get block', () => {
|
||||||
|
const block = operator.getBlock('1');
|
||||||
|
expect(block).toEqual(undefined);
|
||||||
|
|
||||||
|
const blockId = Object.keys(mockDocument.nodes)[0];
|
||||||
|
const block2 = operator.getBlock(blockId);
|
||||||
|
expect(block2).toEqual(mockDocument.nodes[blockId]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get delta with block id', () => {
|
||||||
|
const blockId = 'gtYcSzwLYw';
|
||||||
|
const delta = operator.getDeltaWithBlockId(blockId);
|
||||||
|
expect(delta).toBeTruthy();
|
||||||
|
const deltaStr = JSON.stringify(delta!.ops);
|
||||||
|
const externalId = mockDocument.nodes[blockId].externalId;
|
||||||
|
expect(externalId).toBeTruthy();
|
||||||
|
expect(deltaStr).toEqual(mockDocument.deltaMap[externalId!]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get delta text', () => {
|
||||||
|
const blockId = 'gtYcSzwLYw';
|
||||||
|
const delta = operator.getDeltaWithBlockId(blockId);
|
||||||
|
expect(delta).toBeTruthy();
|
||||||
|
const text = operator.getDeltaText(delta!);
|
||||||
|
expect(text).toEqual('Welcome to AppFlowy!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get split delta', () => {
|
||||||
|
const blockId = 'gtYcSzwLYw';
|
||||||
|
const splitDeltaResult = operator.getSplitDelta(blockId, 7, 4);
|
||||||
|
expect(splitDeltaResult).toBeTruthy();
|
||||||
|
const { updateDelta, diff, insertDelta } = splitDeltaResult!;
|
||||||
|
expect(updateDelta).toBeTruthy();
|
||||||
|
expect(diff).toBeTruthy();
|
||||||
|
expect(insertDelta).toBeTruthy();
|
||||||
|
expect(updateDelta.ops).toEqual([{ insert: 'Welcome' }]);
|
||||||
|
expect(diff.ops).toEqual([{ retain: 7 }, { delete: 13 }]);
|
||||||
|
expect(insertDelta.ops).toEqual([{ insert: 'AppFlowy!' }]);
|
||||||
|
|
||||||
|
const blockId1 = 'wh475aelU_';
|
||||||
|
const splitDeltaResult1 = operator.getSplitDelta(blockId1, 14, 0);
|
||||||
|
expect(splitDeltaResult1).toBeTruthy();
|
||||||
|
const { updateDelta: updateDelta1, diff: diff1, insertDelta: insertDelta1 } = splitDeltaResult1!;
|
||||||
|
expect(updateDelta1).toBeTruthy();
|
||||||
|
expect(diff1).toBeTruthy();
|
||||||
|
expect(insertDelta1).toBeTruthy();
|
||||||
|
expect(updateDelta1.ops).toEqual([
|
||||||
|
{ insert: 'Markdown ' },
|
||||||
|
{ insert: 'refer', attributes: { href: 'https://appflowy.gitbook.io/docs/essential-documentation/markdown' } },
|
||||||
|
]);
|
||||||
|
expect(diff1.ops).toEqual([{ retain: 14 }, { delete: 4 }]);
|
||||||
|
expect(insertDelta1.ops).toEqual([
|
||||||
|
{ insert: 'ence', attributes: { href: 'https://appflowy.gitbook.io/docs/essential-documentation/markdown' } },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('split a line text', async () => {
|
||||||
|
const startId = 'gtYcSzwLYw';
|
||||||
|
const endId = 'gtYcSzwLYw';
|
||||||
|
const index = 7;
|
||||||
|
await operator.splitText(
|
||||||
|
{
|
||||||
|
id: startId,
|
||||||
|
index,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: endId,
|
||||||
|
index,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const backendService = controller.backend;
|
||||||
|
expect(backendService.applyActions).toBeCalledTimes(1);
|
||||||
|
// @ts-ignore
|
||||||
|
const actions = backendService.applyActions.mock.calls[0][0];
|
||||||
|
expect(actions).toBeTruthy();
|
||||||
|
expect(actions.length).toEqual(3);
|
||||||
|
expect(actions[0].action).toEqual(5);
|
||||||
|
expect(actions[0].payload).toEqual({
|
||||||
|
delta: '[{"retain":7},{"delete":13}]',
|
||||||
|
text_id: 'KbkL-wXQrN',
|
||||||
|
});
|
||||||
|
expect(actions[1].action).toEqual(4);
|
||||||
|
expect(actions[1].payload).toHaveProperty('text_id');
|
||||||
|
expect(actions[1].payload).toHaveProperty('delta');
|
||||||
|
expect(actions[1].payload.delta).toEqual('[{"insert":" to AppFlowy!"}]');
|
||||||
|
expect(actions[1].payload.text_id).toEqual(actions[2].payload.block.external_id);
|
||||||
|
expect(actions[2].action).toEqual(0);
|
||||||
|
expect(actions[2].payload).toHaveProperty('block');
|
||||||
|
expect(actions[2].payload.block.parent_id).toEqual('ifF_PvQeOu');
|
||||||
|
expect(actions[2].payload.block.ty).toEqual('paragraph');
|
||||||
|
expect(actions[2].payload.block).toHaveProperty('external_id');
|
||||||
|
expect(actions[2].payload.block.external_id).toBeTruthy();
|
||||||
|
expect(actions[2].payload.parent_id).toEqual('ifF_PvQeOu');
|
||||||
|
expect(actions[2].payload.prev_id).toEqual('gtYcSzwLYw');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('split multi line text', async () => {
|
||||||
|
const startId = 'pYV_AGVqEE';
|
||||||
|
const endId = 'eqf0luv-Fy';
|
||||||
|
const startIndex = 8;
|
||||||
|
const endIndex = 5;
|
||||||
|
await operator.splitText(
|
||||||
|
{
|
||||||
|
id: startId,
|
||||||
|
index: startIndex,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: endId,
|
||||||
|
index: endIndex,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const backendService = controller.backend;
|
||||||
|
expect(backendService.applyActions).toBeCalledTimes(1);
|
||||||
|
// @ts-ignore
|
||||||
|
const actions = backendService.applyActions.mock.calls[0][0];
|
||||||
|
expect(actions).toBeTruthy();
|
||||||
|
expect(actions.length).toEqual(6);
|
||||||
|
expect(actions[0].action).toEqual(5);
|
||||||
|
expect(actions[0].payload.text_id).toEqual('F3zvDsXHha');
|
||||||
|
expect(actions[0].payload.delta).toEqual('[{"retain":8},{"delete":87}]');
|
||||||
|
expect(actions[1].action).toEqual(2);
|
||||||
|
expect(actions[1].payload.parent_id).toEqual('ifF_PvQeOu');
|
||||||
|
expect(actions[1].payload.prev_id).toEqual('');
|
||||||
|
expect(actions[2].action).toEqual(2);
|
||||||
|
expect(actions[2].payload.parent_id).toEqual('ifF_PvQeOu');
|
||||||
|
expect(actions[2].payload.prev_id).toEqual('');
|
||||||
|
expect(actions[3].action).toEqual(4);
|
||||||
|
expect(actions[3].payload.text_id).toEqual(actions[4].payload.block.external_id);
|
||||||
|
expect(actions[3].payload.delta).toEqual(
|
||||||
|
'[{"insert":" "},{"attributes":{"code":true},"insert":"+"},{"insert":" next to any page title in the sidebar to "},{"attributes":{"font_color":"0xff8427e0"},"insert":"quickly"},{"insert":" add a new subpage, "},{"attributes":{"code":true},"insert":"Document"},{"attributes":{"code":false},"insert":", "},{"attributes":{"code":true},"insert":"Grid"},{"attributes":{"code":false},"insert":", or "},{"attributes":{"code":true},"insert":"Kanban Board"},{"attributes":{"code":false},"insert":"."}]'
|
||||||
|
);
|
||||||
|
expect(actions[4].action).toEqual(0);
|
||||||
|
expect(actions[4].payload.parent_id).toEqual('ifF_PvQeOu');
|
||||||
|
expect(actions[4].payload.prev_id).toEqual('pYV_AGVqEE');
|
||||||
|
expect(actions[5].action).toEqual(2);
|
||||||
|
expect(actions[5].payload.parent_id).toEqual('ifF_PvQeOu');
|
||||||
|
expect(actions[5].payload.prev_id).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete a line text', async () => {
|
||||||
|
const startId = 'gtYcSzwLYw';
|
||||||
|
const endId = 'gtYcSzwLYw';
|
||||||
|
await operator.deleteText(
|
||||||
|
{
|
||||||
|
id: startId,
|
||||||
|
index: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: endId,
|
||||||
|
index: 8,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const backendService = controller.backend;
|
||||||
|
expect(backendService.applyActions).toBeCalledTimes(1);
|
||||||
|
// @ts-ignore
|
||||||
|
const actions = backendService.applyActions.mock.calls[0][0];
|
||||||
|
expect(actions).toBeTruthy();
|
||||||
|
expect(actions.length).toEqual(1);
|
||||||
|
expect(actions[0].action).toEqual(5);
|
||||||
|
expect(actions[0].payload).toEqual({
|
||||||
|
delta: '[{"retain":7},{"delete":1}]',
|
||||||
|
text_id: 'KbkL-wXQrN',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete multi line text', async () => {
|
||||||
|
const startId = 'pYV_AGVqEE';
|
||||||
|
const endId = 'eqf0luv-Fy';
|
||||||
|
const startIndex = 8;
|
||||||
|
const endIndex = 5;
|
||||||
|
await operator.splitText(
|
||||||
|
{
|
||||||
|
id: startId,
|
||||||
|
index: startIndex,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: endId,
|
||||||
|
index: endIndex,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const backendService = controller.backend;
|
||||||
|
expect(backendService.applyActions).toBeCalledTimes(1);
|
||||||
|
// @ts-ignore
|
||||||
|
const actions = backendService.applyActions.mock.calls[0][0];
|
||||||
|
expect(actions).toBeTruthy();
|
||||||
|
expect(actions.length).toEqual(6);
|
||||||
|
expect(actions[0].action).toEqual(5);
|
||||||
|
expect(actions[0].payload.text_id).toEqual('F3zvDsXHha');
|
||||||
|
expect(actions[0].payload.delta).toEqual('[{"retain":8},{"delete":87}]');
|
||||||
|
expect(actions[1].action).toEqual(2);
|
||||||
|
expect(actions[1].payload.parent_id).toEqual('ifF_PvQeOu');
|
||||||
|
expect(actions[1].payload.prev_id).toEqual('');
|
||||||
|
expect(actions[2].action).toEqual(2);
|
||||||
|
expect(actions[2].payload.parent_id).toEqual('ifF_PvQeOu');
|
||||||
|
expect(actions[2].payload.prev_id).toEqual('');
|
||||||
|
expect(actions[3].action).toEqual(4);
|
||||||
|
expect(actions[3].payload.delta).toEqual(
|
||||||
|
'[{"insert":" "},{"attributes":{"code":true},"insert":"+"},{"insert":" next to any page title in the sidebar to "},{"attributes":{"font_color":"0xff8427e0"},"insert":"quickly"},{"insert":" add a new subpage, "},{"attributes":{"code":true},"insert":"Document"},{"attributes":{"code":false},"insert":", "},{"attributes":{"code":true},"insert":"Grid"},{"attributes":{"code":false},"insert":", or "},{"attributes":{"code":true},"insert":"Kanban Board"},{"attributes":{"code":false},"insert":"."}]'
|
||||||
|
);
|
||||||
|
expect(actions[3].payload.text_id).toEqual(actions[4].payload.block.external_id);
|
||||||
|
expect(actions[4].action).toEqual(0);
|
||||||
|
expect(actions[4].payload.parent_id).toEqual('ifF_PvQeOu');
|
||||||
|
expect(actions[4].payload.prev_id).toEqual('pYV_AGVqEE');
|
||||||
|
expect(actions[5].action).toEqual(2);
|
||||||
|
expect(actions[5].payload.parent_id).toEqual('ifF_PvQeOu');
|
||||||
|
expect(actions[5].payload.prev_id).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('merge two line text', async () => {
|
||||||
|
const startId = 'gtYcSzwLYw';
|
||||||
|
const endId = 'YsJ-DVO-sC';
|
||||||
|
await operator.mergeText(startId, endId);
|
||||||
|
const backendService = controller.backend;
|
||||||
|
expect(backendService.applyActions).toBeCalledTimes(1);
|
||||||
|
// @ts-ignore
|
||||||
|
const actions = backendService.applyActions.mock.calls[0][0];
|
||||||
|
expect(actions).toBeTruthy();
|
||||||
|
expect(actions.length).toEqual(2);
|
||||||
|
expect(actions[0].action).toEqual(5);
|
||||||
|
expect(actions[0].payload).toEqual({
|
||||||
|
delta: '[{"retain":20},{"insert":"Here are the basics"}]',
|
||||||
|
text_id: 'KbkL-wXQrN',
|
||||||
|
});
|
||||||
|
expect(actions[1].action).toEqual(2);
|
||||||
|
expect(actions[1].payload).toEqual({
|
||||||
|
block: {
|
||||||
|
id: 'YsJ-DVO-sC',
|
||||||
|
ty: 'heading',
|
||||||
|
parent_id: 'ifF_PvQeOu',
|
||||||
|
children_id: 'PM5MctaruD',
|
||||||
|
data: '{"level":2}',
|
||||||
|
external_id: 'QHPzz4O1mV',
|
||||||
|
external_type: 'text',
|
||||||
|
},
|
||||||
|
parent_id: 'ifF_PvQeOu',
|
||||||
|
prev_id: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export {};
|
@ -0,0 +1,322 @@
|
|||||||
|
import { DocumentState } from '$app/interfaces/document';
|
||||||
|
|
||||||
|
export const mockDocument = {
|
||||||
|
nodes: {
|
||||||
|
wh475aelU_: {
|
||||||
|
id: 'wh475aelU_',
|
||||||
|
type: 'numbered_list',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'VcfuvGuodm',
|
||||||
|
data: {},
|
||||||
|
externalId: 'sUF-3L5JHd',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
pYV_AGVqEE: {
|
||||||
|
id: 'pYV_AGVqEE',
|
||||||
|
type: 'todo_list',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'e6ByZ0nZk9',
|
||||||
|
data: { checked: false },
|
||||||
|
externalId: 'F3zvDsXHha',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
'0whp025621': {
|
||||||
|
id: '0whp025621',
|
||||||
|
type: 'callout',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'b5ypKcGf5_',
|
||||||
|
data: { bgColor: '#F0F0F0', icon: '🥰' },
|
||||||
|
externalId: 'P_ODpxtY-S',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
d4Qo2OFOpX: {
|
||||||
|
id: 'd4Qo2OFOpX',
|
||||||
|
type: 'paragraph',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: '2lNOUVOJJ5',
|
||||||
|
data: {},
|
||||||
|
externalId: 'QT_VkSHge-',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
tLi0Tg4dBc: {
|
||||||
|
id: 'tLi0Tg4dBc',
|
||||||
|
type: 'paragraph',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'rgDc-GrgOa',
|
||||||
|
data: {},
|
||||||
|
externalId: '7FQuBVPxeZ',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
'-sili1kmaR': {
|
||||||
|
id: '-sili1kmaR',
|
||||||
|
type: 'todo_list',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'mAxPJngROh',
|
||||||
|
data: { checked: false },
|
||||||
|
externalId: 'VGLCGgx_rk',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
'5I64JF3Hzw': {
|
||||||
|
id: '5I64JF3Hzw',
|
||||||
|
type: 'numbered_list',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'TzJU1gv2PE',
|
||||||
|
data: {},
|
||||||
|
externalId: 'zYOHSlXpWE',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
'eqf0luv-Fy': {
|
||||||
|
id: 'eqf0luv-Fy',
|
||||||
|
type: 'todo_list',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'oxKR2cHeeH',
|
||||||
|
data: {
|
||||||
|
checked: false,
|
||||||
|
},
|
||||||
|
externalId: '6BnmM6ZkJV',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
ZMPoVs7lC4: {
|
||||||
|
id: 'ZMPoVs7lC4',
|
||||||
|
type: 'numbered_list',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'jwB_QmOn21',
|
||||||
|
data: {},
|
||||||
|
externalId: 'qIDnwwdSQF',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
'PM-4wkNVlu': {
|
||||||
|
id: 'PM-4wkNVlu',
|
||||||
|
type: 'paragraph',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'HdHqxm7-e-',
|
||||||
|
data: {},
|
||||||
|
externalId: 'lPI1KU7usc',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
'5qS3sKv9C2': {
|
||||||
|
id: '5qS3sKv9C2',
|
||||||
|
type: 'heading',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'LaCFrFbNeA',
|
||||||
|
data: { level: 2 },
|
||||||
|
externalId: 'fy82xqO08a',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
tEGSjQM2LP: {
|
||||||
|
id: 'tEGSjQM2LP',
|
||||||
|
type: 'todo_list',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'G_zBND8YZl',
|
||||||
|
data: { checked: true },
|
||||||
|
externalId: 'xWJGGIB-fp',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
IteP77UNrr: {
|
||||||
|
id: 'IteP77UNrr',
|
||||||
|
type: 'divider',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: '8ZAdHr4H4J',
|
||||||
|
data: {},
|
||||||
|
externalId: '',
|
||||||
|
externalType: '',
|
||||||
|
},
|
||||||
|
vMc1WwxjJu: {
|
||||||
|
id: 'vMc1WwxjJu',
|
||||||
|
type: 'quote',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'zWkL_b8_Mi',
|
||||||
|
data: {},
|
||||||
|
externalId: 'oOxRotTYg2',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
gtYcSzwLYw: {
|
||||||
|
id: 'gtYcSzwLYw',
|
||||||
|
type: 'heading',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'WhIA288H8O',
|
||||||
|
data: { level: 1 },
|
||||||
|
externalId: 'KbkL-wXQrN',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
jk7YrtfAgz: {
|
||||||
|
id: 'jk7YrtfAgz',
|
||||||
|
type: 'paragraph',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'KIO68twg3J',
|
||||||
|
data: {},
|
||||||
|
externalId: 'b3BIaLzS_o',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
jAl6GnPNB_: {
|
||||||
|
id: 'jAl6GnPNB_',
|
||||||
|
type: 'todo_list',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'HR3s1f_gpD',
|
||||||
|
data: { checked: false },
|
||||||
|
externalId: 'qiW6xN-o5Q',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
NFtEOGjXEm: {
|
||||||
|
id: 'NFtEOGjXEm',
|
||||||
|
type: 'paragraph',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'kDx1WbW6ni',
|
||||||
|
data: {},
|
||||||
|
externalId: 'r19i_oNV3O',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
'4f6_TWg8x5': {
|
||||||
|
id: '4f6_TWg8x5',
|
||||||
|
type: 'paragraph',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'RGXTAjco5O',
|
||||||
|
data: {},
|
||||||
|
externalId: 'pf1dV9EJer',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
xFhJgOxACc: {
|
||||||
|
id: 'xFhJgOxACc',
|
||||||
|
type: 'heading',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'CMqq7y9JTX',
|
||||||
|
data: { level: 2 },
|
||||||
|
externalId: 'b3mbfhloLa',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
'kih-t9tRZr': {
|
||||||
|
id: 'kih-t9tRZr',
|
||||||
|
type: 'code',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'fnWMHsa5if',
|
||||||
|
data: { language: 'rust' },
|
||||||
|
externalId: 'HBZkdYM6Ka',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
ifF_PvQeOu: {
|
||||||
|
id: 'ifF_PvQeOu',
|
||||||
|
type: 'page',
|
||||||
|
parent: '',
|
||||||
|
children: '5_bawmri6x',
|
||||||
|
data: {},
|
||||||
|
externalId: 'm_SX-ck0GL',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
'YsJ-DVO-sC': {
|
||||||
|
id: 'YsJ-DVO-sC',
|
||||||
|
type: 'heading',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'PM5MctaruD',
|
||||||
|
data: { level: 2 },
|
||||||
|
externalId: 'QHPzz4O1mV',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
JcIU0PjpyD: {
|
||||||
|
id: 'JcIU0PjpyD',
|
||||||
|
type: 'todo_list',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'xcYFnxMXai',
|
||||||
|
data: { checked: false },
|
||||||
|
externalId: 'g4WQvF8doI',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
Oi2cxSuUls: {
|
||||||
|
id: 'Oi2cxSuUls',
|
||||||
|
type: 'paragraph',
|
||||||
|
parent: 'ifF_PvQeOu',
|
||||||
|
children: 'NI4TCeq2Lv',
|
||||||
|
data: {},
|
||||||
|
externalId: 'D27H4Hf9re',
|
||||||
|
externalType: 'text',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
xcYFnxMXai: [],
|
||||||
|
'5_bawmri6x': [
|
||||||
|
'gtYcSzwLYw',
|
||||||
|
'YsJ-DVO-sC',
|
||||||
|
'jAl6GnPNB_',
|
||||||
|
'-sili1kmaR',
|
||||||
|
'pYV_AGVqEE',
|
||||||
|
'JcIU0PjpyD',
|
||||||
|
'tEGSjQM2LP',
|
||||||
|
'eqf0luv-Fy',
|
||||||
|
'4f6_TWg8x5',
|
||||||
|
'IteP77UNrr',
|
||||||
|
'PM-4wkNVlu',
|
||||||
|
'5qS3sKv9C2',
|
||||||
|
'5I64JF3Hzw',
|
||||||
|
'wh475aelU_',
|
||||||
|
'ZMPoVs7lC4',
|
||||||
|
'kih-t9tRZr',
|
||||||
|
'Oi2cxSuUls',
|
||||||
|
'xFhJgOxACc',
|
||||||
|
'vMc1WwxjJu',
|
||||||
|
'd4Qo2OFOpX',
|
||||||
|
'0whp025621',
|
||||||
|
'tLi0Tg4dBc',
|
||||||
|
'jk7YrtfAgz',
|
||||||
|
'NFtEOGjXEm',
|
||||||
|
],
|
||||||
|
'rgDc-GrgOa': [],
|
||||||
|
jwB_QmOn21: [],
|
||||||
|
b5ypKcGf5_: [],
|
||||||
|
LaCFrFbNeA: [],
|
||||||
|
'8ZAdHr4H4J': [],
|
||||||
|
'HdHqxm7-e-': [],
|
||||||
|
G_zBND8YZl: [],
|
||||||
|
CMqq7y9JTX: [],
|
||||||
|
WhIA288H8O: [],
|
||||||
|
HR3s1f_gpD: [],
|
||||||
|
zWkL_b8_Mi: [],
|
||||||
|
KIO68twg3J: [],
|
||||||
|
oxKR2cHeeH: [],
|
||||||
|
fnWMHsa5if: [],
|
||||||
|
kDx1WbW6ni: [],
|
||||||
|
'2lNOUVOJJ5': [],
|
||||||
|
PM5MctaruD: [],
|
||||||
|
TzJU1gv2PE: [],
|
||||||
|
RGXTAjco5O: [],
|
||||||
|
e6ByZ0nZk9: [],
|
||||||
|
VcfuvGuodm: [],
|
||||||
|
mAxPJngROh: [],
|
||||||
|
NI4TCeq2Lv: [],
|
||||||
|
},
|
||||||
|
deltaMap: {
|
||||||
|
VGLCGgx_rk:
|
||||||
|
'[{"insert":"Highlight ","attributes":{"bg_color":"0x4dffeb3b"}},{"insert":"any text, and use the editing menu to "},{"insert":"style","attributes":{"italic":true}},{"insert":" "},{"insert":"your","attributes":{"bold":true}},{"insert":" "},{"insert":"writing","attributes":{"underline":true}},{"insert":" "},{"insert":"however","attributes":{"code":true}},{"insert":" you "},{"insert":"like.","attributes":{"strikethrough":true}}]',
|
||||||
|
'6BnmM6ZkJV':
|
||||||
|
'[{"insert":"Click "},{"insert":"+","attributes":{"code":true}},{"insert":" next to any page title in the sidebar to "},{"insert":"quickly","attributes":{"font_color":"0xff8427e0"}},{"insert":" add a new subpage, "},{"insert":"Document","attributes":{"code":true}},{"insert":", ","attributes":{"code":false}},{"insert":"Grid","attributes":{"code":true}},{"insert":", or ","attributes":{"code":false}},{"insert":"Kanban Board","attributes":{"code":true}},{"insert":".","attributes":{"code":false}}]',
|
||||||
|
g4WQvF8doI:
|
||||||
|
'[{"insert":"Type "},{"insert":"/","attributes":{"code":true}},{"insert":" followed by "},{"insert":"/bullet","attributes":{"code":true}},{"insert":" or "},{"insert":"/num","attributes":{"code":true}},{"insert":" to create a list.","attributes":{"code":false}}]',
|
||||||
|
HBZkdYM6Ka:
|
||||||
|
'[{"insert":"// This is the main function.\\nfn main() {\\n // Print text to the console.\\n println!(\\"Hello World!\\");\\n}"}]',
|
||||||
|
'qiW6xN-o5Q': '[{"insert":"Click anywhere and just start typing."}]',
|
||||||
|
'KbkL-wXQrN': '[{"insert":"Welcome to AppFlowy!"}]',
|
||||||
|
lPI1KU7usc: '[]',
|
||||||
|
D27H4Hf9re: '[]',
|
||||||
|
oOxRotTYg2:
|
||||||
|
'[{"insert":"Click "},{"insert":"?","attributes":{"code":true}},{"insert":" at the bottom right for help and support."}]',
|
||||||
|
'P_ODpxtY-S':
|
||||||
|
'[{"insert":"\\nLike AppFlowy? Follow us:\\n"},{"insert":"GitHub","attributes":{"href":"https://github.com/AppFlowy-IO/AppFlowy"}},{"insert":"\\n"},{"insert":"Twitter","attributes":{"href":"https://twitter.com/appflowy"}},{"insert":": @appflowy\\n"},{"insert":"Newsletter","attributes":{"href":"https://blog-appflowy.ghost.io/"}},{"insert":"\\n"}]',
|
||||||
|
F3zvDsXHha:
|
||||||
|
'[{"insert":"As soon as you type "},{"insert":"/","attributes":{"code":true,"font_color":"0xff00b5ff"}},{"insert":" a menu will pop up. Select "},{"insert":"different types","attributes":{"bg_color":"0x4d9c27b0"}},{"insert":" of content blocks you can add."}]',
|
||||||
|
fy82xqO08a: '[{"insert":"Keyboard shortcuts, markdown, and code block"}]',
|
||||||
|
'sUF-3L5JHd':
|
||||||
|
'[{"insert":"Markdown "},{"insert":"reference","attributes":{"href":"https://appflowy.gitbook.io/docs/essential-documentation/markdown"}}]',
|
||||||
|
r19i_oNV3O: '[]',
|
||||||
|
'm_SX-ck0GL': '[]',
|
||||||
|
b3mbfhloLa: '[{"insert":"Have a question❓"}]',
|
||||||
|
'xWJGGIB-fp':
|
||||||
|
'[{"insert":"Click "},{"insert":"+ New Page ","attributes":{"code":true}},{"insert":"button at the bottom of your sidebar to add a new page."}]',
|
||||||
|
'QT_VkSHge-': '[]',
|
||||||
|
zYOHSlXpWE:
|
||||||
|
'[{"insert":"Keyboard shortcuts "},{"insert":"guide","attributes":{"href":"https://appflowy.gitbook.io/docs/essential-documentation/shortcuts"}}]',
|
||||||
|
b3BIaLzS_o: '[]',
|
||||||
|
'7FQuBVPxeZ': '[]',
|
||||||
|
pf1dV9EJer: '[]',
|
||||||
|
QHPzz4O1mV: '[{"insert":"Here are the basics"}]',
|
||||||
|
qIDnwwdSQF:
|
||||||
|
'[{"insert":"Type "},{"insert":"/code","attributes":{"code":true}},{"insert":" to insert a code block","attributes":{"code":false}}]',
|
||||||
|
},
|
||||||
|
} as DocumentState;
|
@ -24,6 +24,7 @@ import {
|
|||||||
transformIndexToPrevLine,
|
transformIndexToPrevLine,
|
||||||
} from '$app/utils/document/delta';
|
} from '$app/utils/document/delta';
|
||||||
import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name';
|
import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name';
|
||||||
|
import { BlockDeltaOperator } from '$app/utils/document/block_delta';
|
||||||
|
|
||||||
export function getMiddleIds(document: DocumentState, startId: string, endId: string) {
|
export function getMiddleIds(document: DocumentState, startId: string, endId: string) {
|
||||||
const middleIds = [];
|
const middleIds = [];
|
||||||
@ -54,207 +55,6 @@ export function getStartAndEndIdsByRange(rangeState: RangeState) {
|
|||||||
return [startId, endId];
|
return [startId, endId];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMiddleIdsByRange(rangeState: RangeState, document: DocumentState) {
|
|
||||||
const ids = getStartAndEndIdsByRange(rangeState);
|
|
||||||
|
|
||||||
if (ids.length < 2) return;
|
|
||||||
const [startId, endId] = ids;
|
|
||||||
|
|
||||||
return getMiddleIds(document, startId, endId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAfterMergeCaretByRange(rangeState: RangeState, insertDelta?: Delta) {
|
|
||||||
const { anchor, focus, ranges } = rangeState;
|
|
||||||
|
|
||||||
if (!anchor || !focus) return;
|
|
||||||
|
|
||||||
const isForward = anchor.point.y < focus.point.y;
|
|
||||||
const startId = isForward ? anchor.id : focus.id;
|
|
||||||
const startRange = ranges[startId];
|
|
||||||
|
|
||||||
if (!startRange) return;
|
|
||||||
const offset = insertDelta ? insertDelta.length() : 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: startId,
|
|
||||||
index: startRange.index + offset,
|
|
||||||
length: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStartAndEndExtentDelta(documentState: DocumentState, rangeState: RangeState) {
|
|
||||||
const ids = getStartAndEndIdsByRange(rangeState);
|
|
||||||
|
|
||||||
if (ids.length === 0) return;
|
|
||||||
const startId = ids[0];
|
|
||||||
const endId = ids[ids.length - 1];
|
|
||||||
const { ranges } = rangeState;
|
|
||||||
// get start and end delta
|
|
||||||
const startRange = ranges[startId];
|
|
||||||
const endRange = ranges[endId];
|
|
||||||
|
|
||||||
if (!startRange || !endRange) return;
|
|
||||||
const startNode = documentState.nodes[startId];
|
|
||||||
const startNodeDelta = new Delta(startNode.data.delta);
|
|
||||||
const startBeforeExtentDelta = getBeofreExtentDeltaByRange(startNodeDelta, startRange);
|
|
||||||
|
|
||||||
const endNode = documentState.nodes[endId];
|
|
||||||
const endNodeDelta = new Delta(endNode.data.delta);
|
|
||||||
const endAfterExtentDelta = getAfterExtentDeltaByRange(endNodeDelta, endRange);
|
|
||||||
|
|
||||||
return {
|
|
||||||
startNode,
|
|
||||||
endNode,
|
|
||||||
startDelta: startBeforeExtentDelta,
|
|
||||||
endDelta: endAfterExtentDelta,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMergeEndDeltaToStartActionsByRange(
|
|
||||||
state: RootState,
|
|
||||||
controller: DocumentController,
|
|
||||||
insertDelta?: Delta
|
|
||||||
) {
|
|
||||||
const actions = [];
|
|
||||||
const docId = controller.documentId;
|
|
||||||
const documentState = state[DOCUMENT_NAME][docId];
|
|
||||||
const rangeState = state[RANGE_NAME][docId];
|
|
||||||
const { startDelta, endDelta, endNode, startNode } = getStartAndEndExtentDelta(documentState, rangeState) || {};
|
|
||||||
|
|
||||||
if (!startDelta || !endDelta || !endNode || !startNode) return;
|
|
||||||
// merge start and end nodes
|
|
||||||
const mergeDelta = startDelta.concat(insertDelta || new Delta()).concat(endDelta);
|
|
||||||
|
|
||||||
actions.push(
|
|
||||||
controller.getUpdateAction({
|
|
||||||
...startNode,
|
|
||||||
data: {
|
|
||||||
delta: mergeDelta.ops,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (endNode.id !== startNode.id) {
|
|
||||||
const children = documentState.children[endNode.children].map((id) => documentState.nodes[id]);
|
|
||||||
|
|
||||||
const moveChildrenActions = getMoveChildrenActions({
|
|
||||||
target: startNode,
|
|
||||||
children,
|
|
||||||
controller,
|
|
||||||
});
|
|
||||||
|
|
||||||
actions.push(...moveChildrenActions);
|
|
||||||
// delete end node
|
|
||||||
actions.push(controller.getDeleteAction(endNode));
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMoveChildrenActions({
|
|
||||||
target,
|
|
||||||
children,
|
|
||||||
controller,
|
|
||||||
prevId = '',
|
|
||||||
}: {
|
|
||||||
target: NestedBlock;
|
|
||||||
children: NestedBlock[];
|
|
||||||
controller: DocumentController;
|
|
||||||
prevId?: string;
|
|
||||||
}) {
|
|
||||||
// move children
|
|
||||||
const config = blockConfig[target.type];
|
|
||||||
const targetParentId = config.canAddChild ? target.id : target.parent;
|
|
||||||
|
|
||||||
if (!targetParentId) return [];
|
|
||||||
const targetPrevId = targetParentId === target.id ? prevId : target.id;
|
|
||||||
const moveActions = controller.getMoveChildrenAction(children, targetParentId, targetPrevId);
|
|
||||||
|
|
||||||
return moveActions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getInsertEnterNodeFields(sourceNode: NestedBlock) {
|
|
||||||
if (!sourceNode.parent) return;
|
|
||||||
const parentId = sourceNode.parent;
|
|
||||||
|
|
||||||
const config = blockConfig[sourceNode.type].splitProps || {
|
|
||||||
nextLineRelationShip: SplitRelationship.NextSibling,
|
|
||||||
nextLineBlockType: BlockType.TextBlock,
|
|
||||||
};
|
|
||||||
|
|
||||||
const newNodeType = config.nextLineBlockType;
|
|
||||||
const relationShip = config.nextLineRelationShip;
|
|
||||||
const defaultData = blockConfig[newNodeType].defaultData;
|
|
||||||
|
|
||||||
// if the defaultData property is not defined for the new block type, we throw an error.
|
|
||||||
if (!defaultData) {
|
|
||||||
throw new Error(`Cannot split node of type ${sourceNode.type} to ${newNodeType}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newParentId = relationShip === SplitRelationship.NextSibling ? parentId : sourceNode.id;
|
|
||||||
const newPrevId = relationShip === SplitRelationship.NextSibling ? sourceNode.id : '';
|
|
||||||
|
|
||||||
return {
|
|
||||||
parentId: newParentId,
|
|
||||||
prevId: newPrevId,
|
|
||||||
type: newNodeType,
|
|
||||||
data: defaultData,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getInsertEnterNodeAction(
|
|
||||||
sourceNode: NestedBlock,
|
|
||||||
insertNodeDelta: Delta,
|
|
||||||
controller: DocumentController
|
|
||||||
) {
|
|
||||||
const insertNodeFields = getInsertEnterNodeFields(sourceNode);
|
|
||||||
|
|
||||||
if (!insertNodeFields) return;
|
|
||||||
const { type, data, parentId, prevId } = insertNodeFields;
|
|
||||||
const insertNode = newBlock<any>(type, parentId, {
|
|
||||||
...data,
|
|
||||||
delta: insertNodeDelta.ops,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: insertNode.id,
|
|
||||||
action: controller.getInsertAction(insertNode, prevId),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findPrevHasDeltaNode(state: DocumentState, id: string) {
|
|
||||||
const prevLineId = getPrevLineId(state, id);
|
|
||||||
|
|
||||||
if (!prevLineId) return;
|
|
||||||
let prevLine = state.nodes[prevLineId];
|
|
||||||
|
|
||||||
// Find the prev line that has delta
|
|
||||||
while (prevLine && !prevLine.data.delta) {
|
|
||||||
const id = getPrevLineId(state, prevLine.id);
|
|
||||||
|
|
||||||
if (!id) return;
|
|
||||||
prevLine = state.nodes[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
return prevLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findNextHasDeltaNode(state: DocumentState, id: string) {
|
|
||||||
const nextLineId = getNextLineId(state, id);
|
|
||||||
|
|
||||||
if (!nextLineId) return;
|
|
||||||
let nextLine = state.nodes[nextLineId];
|
|
||||||
|
|
||||||
// Find the next line that has delta
|
|
||||||
while (nextLine && !nextLine.data.delta) {
|
|
||||||
const id = getNextLineId(state, nextLine.id);
|
|
||||||
|
|
||||||
if (!id) return;
|
|
||||||
nextLine = state.nodes[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isPrintableKeyEvent(event: KeyboardEvent) {
|
export function isPrintableKeyEvent(event: KeyboardEvent) {
|
||||||
const key = event.key;
|
const key = event.key;
|
||||||
const isPrintable = key.length === 1;
|
const isPrintable = key.length === 1;
|
||||||
@ -298,7 +98,10 @@ export function getRightCaretByRange(rangeState: RangeState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function transformToPrevLineCaret(document: DocumentState, caret: RangeStatic) {
|
export function transformToPrevLineCaret(document: DocumentState, caret: RangeStatic) {
|
||||||
const delta = new Delta(document.nodes[caret.id].data.delta);
|
const deltaOperator = new BlockDeltaOperator(document);
|
||||||
|
const delta = deltaOperator.getDeltaWithBlockId(caret.id);
|
||||||
|
|
||||||
|
if (!delta) return;
|
||||||
const inTopEdge = caretInTopEdgeByDelta(delta, caret.index);
|
const inTopEdge = caretInTopEdgeByDelta(delta, caret.index);
|
||||||
|
|
||||||
if (!inTopEdge) {
|
if (!inTopEdge) {
|
||||||
@ -311,25 +114,31 @@ export function transformToPrevLineCaret(document: DocumentState, caret: RangeSt
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevLine = findPrevHasDeltaNode(document, caret.id);
|
const prevLineId = deltaOperator.findPrevTextLine(caret.id);
|
||||||
|
|
||||||
if (!prevLine) return;
|
if (!prevLineId) return;
|
||||||
const relativeIndex = getIndexRelativeEnter(delta, caret.index);
|
const relativeIndex = getIndexRelativeEnter(delta, caret.index);
|
||||||
const prevLineIndex = getLastLineIndex(new Delta(prevLine.data.delta));
|
const prevLineDelta = deltaOperator.getDeltaWithBlockId(prevLineId);
|
||||||
const prevLineText = getDeltaText(new Delta(prevLine.data.delta));
|
|
||||||
|
if (!prevLineDelta) return;
|
||||||
|
const prevLineIndex = getLastLineIndex(prevLineDelta);
|
||||||
|
const prevLineText = deltaOperator.getDeltaText(prevLineDelta);
|
||||||
const newPrevLineIndex = prevLineIndex + relativeIndex;
|
const newPrevLineIndex = prevLineIndex + relativeIndex;
|
||||||
const prevLineLength = prevLineText.length;
|
const prevLineLength = prevLineText.length;
|
||||||
const index = newPrevLineIndex > prevLineLength ? prevLineLength : newPrevLineIndex;
|
const index = newPrevLineIndex > prevLineLength ? prevLineLength : newPrevLineIndex;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: prevLine.id,
|
id: prevLineId,
|
||||||
index,
|
index,
|
||||||
length: 0,
|
length: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformToNextLineCaret(document: DocumentState, caret: RangeStatic) {
|
export function transformToNextLineCaret(document: DocumentState, caret: RangeStatic) {
|
||||||
const delta = new Delta(document.nodes[caret.id].data.delta);
|
const deltaOperator = new BlockDeltaOperator(document);
|
||||||
|
const delta = deltaOperator.getDeltaWithBlockId(caret.id);
|
||||||
|
|
||||||
|
if (!delta) return;
|
||||||
const inBottomEdge = caretInBottomEdgeByDelta(delta, caret.index);
|
const inBottomEdge = caretInBottomEdgeByDelta(delta, caret.index);
|
||||||
|
|
||||||
if (!inBottomEdge) {
|
if (!inBottomEdge) {
|
||||||
@ -343,15 +152,18 @@ export function transformToNextLineCaret(document: DocumentState, caret: RangeSt
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextLine = findNextHasDeltaNode(document, caret.id);
|
const nextLineId = deltaOperator.findNextTextLine(caret.id);
|
||||||
|
|
||||||
if (!nextLine) return;
|
if (!nextLineId) return;
|
||||||
const nextLineText = getDeltaText(new Delta(nextLine.data.delta));
|
const nextLineDelta = deltaOperator.getDeltaWithBlockId(nextLineId);
|
||||||
|
|
||||||
|
if (!nextLineDelta) return;
|
||||||
|
const nextLineText = deltaOperator.getDeltaText(nextLineDelta);
|
||||||
const relativeIndex = getIndexRelativeEnter(delta, caret.index);
|
const relativeIndex = getIndexRelativeEnter(delta, caret.index);
|
||||||
const index = relativeIndex >= nextLineText.length ? nextLineText.length : relativeIndex;
|
const index = relativeIndex >= nextLineText.length ? nextLineText.length : relativeIndex;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: nextLine.id,
|
id: nextLineId,
|
||||||
index,
|
index,
|
||||||
length: 0,
|
length: 0,
|
||||||
};
|
};
|
||||||
|
@ -18,6 +18,8 @@ export function blockPB2Node(block: BlockPB) {
|
|||||||
parent: block.parent_id,
|
parent: block.parent_id,
|
||||||
children: block.children_id,
|
children: block.children_id,
|
||||||
data,
|
data,
|
||||||
|
externalId: block.external_id,
|
||||||
|
externalType: block.external_type,
|
||||||
};
|
};
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
@ -97,12 +99,12 @@ export function getPrevNodeId(state: DocumentState, id: string) {
|
|||||||
return prevNodeId;
|
return prevNodeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function newBlock<Type>(type: BlockType, parentId: string, data: BlockData<Type>): NestedBlock<Type> {
|
export function newBlock<Type>(type: BlockType, parentId: string, data?: BlockData<Type>): NestedBlock<Type> {
|
||||||
return {
|
return {
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
type,
|
type,
|
||||||
parent: parentId,
|
parent: parentId,
|
||||||
children: generateId(),
|
children: generateId(),
|
||||||
data,
|
data: data ? data : {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,453 @@
|
|||||||
|
import { BlockData, BlockType, DocumentState, NestedBlock, SplitRelationship } from '$app/interfaces/document';
|
||||||
|
import { generateId, getNextLineId, getPrevLineId } from '$app/utils/document/block';
|
||||||
|
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
||||||
|
import Delta, { Op } from 'quill-delta';
|
||||||
|
import { blockConfig } from '$app/constants/document/config';
|
||||||
|
|
||||||
|
export class BlockDeltaOperator {
|
||||||
|
constructor(
|
||||||
|
private state: DocumentState,
|
||||||
|
private controller?: DocumentController,
|
||||||
|
private updatePageName?: (name: string) => Promise<void>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getBlock = (blockId: string) => {
|
||||||
|
return this.state.nodes[blockId];
|
||||||
|
};
|
||||||
|
|
||||||
|
getExternalId = (blockId: string) => {
|
||||||
|
return this.getBlock(blockId)?.externalId;
|
||||||
|
};
|
||||||
|
|
||||||
|
getDeltaStrWithExternalId = (externalId: string) => {
|
||||||
|
return this.state.deltaMap[externalId];
|
||||||
|
};
|
||||||
|
|
||||||
|
getDeltaWithExternalId = (externalId: string) => {
|
||||||
|
const deltaStr = this.getDeltaStrWithExternalId(externalId);
|
||||||
|
|
||||||
|
if (!deltaStr) return;
|
||||||
|
return new Delta(JSON.parse(deltaStr));
|
||||||
|
};
|
||||||
|
|
||||||
|
getDeltaWithBlockId = (blockId: string) => {
|
||||||
|
const externalId = this.getExternalId(blockId);
|
||||||
|
|
||||||
|
if (!externalId) return;
|
||||||
|
return this.getDeltaWithExternalId(externalId);
|
||||||
|
};
|
||||||
|
|
||||||
|
hasDelta = (blockId: string) => {
|
||||||
|
const externalId = this.getExternalId(blockId);
|
||||||
|
|
||||||
|
if (!externalId) return false;
|
||||||
|
return !!this.getDeltaStrWithExternalId(externalId);
|
||||||
|
};
|
||||||
|
|
||||||
|
getDeltaText = (delta: Delta) => {
|
||||||
|
return delta.ops.map((op) => op.insert).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
sliceDeltaWithBlockId = (blockId: string, startIndex: number, endIndex?: number) => {
|
||||||
|
const delta = this.getDeltaWithBlockId(blockId);
|
||||||
|
|
||||||
|
return delta?.slice(startIndex, endIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
getSplitDelta = (blockId: string, index: number, length: number) => {
|
||||||
|
const externalId = this.getExternalId(blockId);
|
||||||
|
|
||||||
|
if (!externalId) return;
|
||||||
|
const delta = this.getDeltaWithExternalId(externalId);
|
||||||
|
|
||||||
|
if (!delta) return;
|
||||||
|
const diff = new Delta().retain(index).delete(delta.length() - index);
|
||||||
|
const updateDelta = delta.slice(0, index);
|
||||||
|
const insertDelta = delta.slice(index + length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
diff,
|
||||||
|
updateDelta,
|
||||||
|
insertDelta,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
getApplyDeltaAction = (blockId: string, delta: Delta) => {
|
||||||
|
const block = this.getBlock(blockId);
|
||||||
|
const deltaStr = JSON.stringify(delta.ops);
|
||||||
|
|
||||||
|
return this.controller?.getApplyTextDeltaAction(block, deltaStr);
|
||||||
|
};
|
||||||
|
|
||||||
|
getNewTextLineActions = ({
|
||||||
|
blockId,
|
||||||
|
delta,
|
||||||
|
parentId,
|
||||||
|
type = BlockType.TextBlock,
|
||||||
|
prevId,
|
||||||
|
data = {},
|
||||||
|
}: {
|
||||||
|
blockId: string;
|
||||||
|
delta: Delta;
|
||||||
|
parentId: string;
|
||||||
|
type: BlockType;
|
||||||
|
prevId: string | null;
|
||||||
|
data?: BlockData<any>;
|
||||||
|
}) => {
|
||||||
|
const externalId = generateId();
|
||||||
|
const block = {
|
||||||
|
id: blockId,
|
||||||
|
type,
|
||||||
|
externalId,
|
||||||
|
externalType: 'text',
|
||||||
|
parent: parentId,
|
||||||
|
children: generateId(),
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
const deltaStr = JSON.stringify(delta.ops);
|
||||||
|
|
||||||
|
if (!this.controller) return [];
|
||||||
|
return this.controller?.getInsertTextActions(block, deltaStr, prevId);
|
||||||
|
};
|
||||||
|
|
||||||
|
splitText = async (
|
||||||
|
startBlock: {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
},
|
||||||
|
endBlock: {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
},
|
||||||
|
shiftKey?: boolean
|
||||||
|
) => {
|
||||||
|
if (!this.controller) return;
|
||||||
|
|
||||||
|
const startNode = this.getBlock(startBlock.id);
|
||||||
|
const endNode = this.getBlock(endBlock.id);
|
||||||
|
const startNodeIsRoot = !startNode.parent;
|
||||||
|
|
||||||
|
if (!startNode || !endNode) return;
|
||||||
|
const startNodeDelta = this.getDeltaWithBlockId(startNode.id);
|
||||||
|
const endNodeDelta = this.getDeltaWithBlockId(endNode.id);
|
||||||
|
|
||||||
|
if (!startNodeDelta || !endNodeDelta) return;
|
||||||
|
let diff: Delta, insertDelta;
|
||||||
|
|
||||||
|
if (startNode.id === endNode.id) {
|
||||||
|
const splitResult = this.getSplitDelta(startNode.id, startBlock.index, endBlock.index - startBlock.index);
|
||||||
|
|
||||||
|
if (!splitResult) return;
|
||||||
|
diff = splitResult.diff;
|
||||||
|
insertDelta = splitResult.insertDelta;
|
||||||
|
} else {
|
||||||
|
const startSplitResult = this.getSplitDelta(
|
||||||
|
startNode.id,
|
||||||
|
startBlock.index,
|
||||||
|
startNodeDelta.length() - startBlock.index
|
||||||
|
);
|
||||||
|
|
||||||
|
const endSplitResult = this.getSplitDelta(endNode.id, 0, endBlock.index);
|
||||||
|
|
||||||
|
if (!startSplitResult || !endSplitResult) return;
|
||||||
|
diff = startSplitResult.diff;
|
||||||
|
insertDelta = endSplitResult.insertDelta;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!diff || !insertDelta) return;
|
||||||
|
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
const { nextLineBlockType, nextLineRelationShip } = blockConfig[startNode.type]?.splitProps || {
|
||||||
|
nextLineBlockType: BlockType.TextBlock,
|
||||||
|
nextLineRelationShip: SplitRelationship.NextSibling,
|
||||||
|
};
|
||||||
|
const parentId =
|
||||||
|
nextLineRelationShip === SplitRelationship.NextSibling && startNode.parent ? startNode.parent : startNode.id;
|
||||||
|
const prevId = nextLineRelationShip === SplitRelationship.NextSibling && startNode.parent ? startNode.id : null;
|
||||||
|
|
||||||
|
let newLineId = startNode.id;
|
||||||
|
|
||||||
|
// delete middle nodes
|
||||||
|
if (startNode.id !== endNode.id) {
|
||||||
|
actions.push(...this.getDeleteMiddleNodesActions(startNode.id, endNode.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shiftKey) {
|
||||||
|
const enter = new Delta().insert('\n');
|
||||||
|
const newOps = diff.ops.concat(enter.ops.concat(insertDelta.ops));
|
||||||
|
|
||||||
|
diff = new Delta(newOps);
|
||||||
|
if (startNode.id !== endNode.id) {
|
||||||
|
// move the children of endNode to startNode
|
||||||
|
actions.push(...this.getMoveChildrenActions(endNode.id, startNode));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newLineId = generateId();
|
||||||
|
actions.push(
|
||||||
|
...this.getNewTextLineActions({
|
||||||
|
blockId: newLineId,
|
||||||
|
delta: insertDelta,
|
||||||
|
parentId,
|
||||||
|
type: nextLineBlockType,
|
||||||
|
prevId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (!startNodeIsRoot) {
|
||||||
|
// move the children of startNode to newLine
|
||||||
|
actions.push(
|
||||||
|
...this.getMoveChildrenActions(
|
||||||
|
startNode.id,
|
||||||
|
{
|
||||||
|
id: newLineId,
|
||||||
|
type: nextLineBlockType,
|
||||||
|
},
|
||||||
|
[endNode.id]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startNode.id !== endNode.id) {
|
||||||
|
// move the children of endNode to newLine
|
||||||
|
actions.push(
|
||||||
|
...this.getMoveChildrenActions(endNode.id, {
|
||||||
|
id: newLineId,
|
||||||
|
type: nextLineBlockType,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startNode.id !== endNode.id) {
|
||||||
|
// delete end node
|
||||||
|
const deleteEndNodeAction = this.controller.getDeleteAction(endNode);
|
||||||
|
|
||||||
|
actions.push(deleteEndNodeAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startNode.parent) {
|
||||||
|
// apply delta
|
||||||
|
const applyDeltaAction = this.getApplyDeltaAction(startNode.id, diff);
|
||||||
|
|
||||||
|
if (applyDeltaAction) actions.unshift(applyDeltaAction);
|
||||||
|
} else {
|
||||||
|
await this.updateRootNodeDelta(startNode.id, diff);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.controller.applyActions(actions);
|
||||||
|
|
||||||
|
return newLineId;
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteText = async (
|
||||||
|
startBlock: {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
},
|
||||||
|
endBlock: {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
},
|
||||||
|
insertChar?: string
|
||||||
|
) => {
|
||||||
|
if (!this.controller) return;
|
||||||
|
const startNode = this.getBlock(startBlock.id);
|
||||||
|
const endNode = this.getBlock(endBlock.id);
|
||||||
|
|
||||||
|
if (!startNode || !endNode) return;
|
||||||
|
const startNodeDelta = this.getDeltaWithBlockId(startNode.id);
|
||||||
|
const endNodeDelta = this.getDeltaWithBlockId(endNode.id);
|
||||||
|
|
||||||
|
if (!startNodeDelta || !endNodeDelta) return;
|
||||||
|
|
||||||
|
let startDiff: Delta | undefined;
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
if (startNode.id === endNode.id) {
|
||||||
|
const length = endBlock.index - startBlock.index;
|
||||||
|
|
||||||
|
const newOps: Op[] = [
|
||||||
|
{
|
||||||
|
retain: startBlock.index,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delete: length,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (insertChar) {
|
||||||
|
newOps.push({
|
||||||
|
insert: insertChar,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startDiff = new Delta(newOps);
|
||||||
|
} else {
|
||||||
|
const startSplitResult = this.getSplitDelta(
|
||||||
|
startNode.id,
|
||||||
|
startBlock.index,
|
||||||
|
startNodeDelta.length() - startBlock.index
|
||||||
|
);
|
||||||
|
const endSplitResult = this.getSplitDelta(endNode.id, 0, endBlock.index);
|
||||||
|
|
||||||
|
if (!startSplitResult || !endSplitResult) return;
|
||||||
|
const insertDelta = endSplitResult.insertDelta;
|
||||||
|
const newOps = [...startSplitResult.diff.ops];
|
||||||
|
|
||||||
|
if (insertChar) {
|
||||||
|
newOps.push({
|
||||||
|
insert: insertChar,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newOps.push(...insertDelta.ops);
|
||||||
|
startDiff = new Delta(newOps);
|
||||||
|
// delete middle nodes
|
||||||
|
actions.push(...this.getDeleteMiddleNodesActions(startNode.id, endNode.id));
|
||||||
|
// move the children of endNode to startNode
|
||||||
|
actions.push(...this.getMoveChildrenActions(endNode.id, startNode));
|
||||||
|
// delete end node
|
||||||
|
const deleteEndNodeAction = this.controller.getDeleteAction(endNode);
|
||||||
|
|
||||||
|
actions.push(deleteEndNodeAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startDiff) return;
|
||||||
|
if (startNode.parent) {
|
||||||
|
const applyDeltaAction = this.getApplyDeltaAction(startNode.id, startDiff);
|
||||||
|
|
||||||
|
if (applyDeltaAction) actions.unshift(applyDeltaAction);
|
||||||
|
} else {
|
||||||
|
await this.updateRootNodeDelta(startNode.id, startDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.controller.applyActions(actions);
|
||||||
|
|
||||||
|
return startNode.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
mergeText = async (targetId: string, sourceId: string) => {
|
||||||
|
if (!this.controller || targetId === sourceId) return;
|
||||||
|
const startNode = this.getBlock(targetId);
|
||||||
|
const endNode = this.getBlock(sourceId);
|
||||||
|
|
||||||
|
if (!startNode || !endNode) return;
|
||||||
|
const startNodeDelta = this.getDeltaWithBlockId(startNode.id);
|
||||||
|
const endNodeDelta = this.getDeltaWithBlockId(endNode.id);
|
||||||
|
|
||||||
|
if (!startNodeDelta || !endNodeDelta) return;
|
||||||
|
|
||||||
|
const startNodeIsRoot = !startNode.parent;
|
||||||
|
const actions = [];
|
||||||
|
const index = startNodeDelta.length();
|
||||||
|
const retain = new Delta().retain(startNodeDelta.length());
|
||||||
|
const newOps = [...retain.ops, ...endNodeDelta.ops];
|
||||||
|
const diff = new Delta(newOps);
|
||||||
|
|
||||||
|
if (!startNodeIsRoot) {
|
||||||
|
const applyDeltaAction = this.getApplyDeltaAction(startNode.id, diff);
|
||||||
|
|
||||||
|
if (applyDeltaAction) actions.push(applyDeltaAction);
|
||||||
|
} else {
|
||||||
|
await this.updateRootNodeDelta(startNode.id, diff);
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveChildrenActions = this.getMoveChildrenActions(endNode.id, startNode);
|
||||||
|
|
||||||
|
// move the children of endNode to startNode
|
||||||
|
actions.push(...moveChildrenActions);
|
||||||
|
// delete end node
|
||||||
|
const deleteEndNodeAction = this.controller.getDeleteAction(endNode);
|
||||||
|
|
||||||
|
actions.push(deleteEndNodeAction);
|
||||||
|
|
||||||
|
await this.controller.applyActions(actions);
|
||||||
|
return {
|
||||||
|
id: targetId,
|
||||||
|
index,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
updateRootNodeDelta = async (id: string, diff: Delta) => {
|
||||||
|
const nodeDelta = this.getDeltaWithBlockId(id);
|
||||||
|
const delta = nodeDelta?.compose(diff);
|
||||||
|
|
||||||
|
const name = delta ? this.getDeltaText(delta) : '';
|
||||||
|
|
||||||
|
await this.updatePageName?.(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
getMoveChildrenActions = (
|
||||||
|
blockId: string,
|
||||||
|
newParent: {
|
||||||
|
id: string;
|
||||||
|
type: BlockType;
|
||||||
|
},
|
||||||
|
excludeIds?: string[]
|
||||||
|
) => {
|
||||||
|
if (!this.controller) return [];
|
||||||
|
const block = this.getBlock(blockId);
|
||||||
|
const config = blockConfig[newParent.type];
|
||||||
|
|
||||||
|
if (!config.canAddChild) return [];
|
||||||
|
const childrenId = block.children;
|
||||||
|
const children = this.state.children[childrenId]
|
||||||
|
.filter((id) => !excludeIds || (excludeIds && !excludeIds.includes(id)))
|
||||||
|
.map((id) => this.getBlock(id));
|
||||||
|
|
||||||
|
return this.controller.getMoveChildrenAction(children, newParent.id, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
getDeleteMiddleNodesActions = (startId: string, endId: string) => {
|
||||||
|
const controller = this.controller;
|
||||||
|
|
||||||
|
if (!controller) return [];
|
||||||
|
const middleIds = this.getMiddleIds(startId, endId);
|
||||||
|
|
||||||
|
return middleIds.map((id) => controller.getDeleteAction(this.getBlock(id)));
|
||||||
|
};
|
||||||
|
|
||||||
|
getMiddleIds = (startId: string, endId: string) => {
|
||||||
|
const middleIds = [];
|
||||||
|
let currentId: string | undefined = startId;
|
||||||
|
|
||||||
|
while (currentId && currentId !== endId) {
|
||||||
|
const nextId = getNextLineId(this.state, currentId);
|
||||||
|
|
||||||
|
if (nextId && nextId !== endId) {
|
||||||
|
middleIds.push(nextId);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentId = nextId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return middleIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
findPrevTextLine = (blockId: string) => {
|
||||||
|
let currentId: string | undefined = blockId;
|
||||||
|
|
||||||
|
while (currentId) {
|
||||||
|
const prevId = getPrevLineId(this.state, currentId);
|
||||||
|
|
||||||
|
if (prevId && this.hasDelta(prevId)) {
|
||||||
|
return prevId;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentId = prevId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
findNextTextLine = (blockId: string) => {
|
||||||
|
let currentId: string | undefined = blockId;
|
||||||
|
|
||||||
|
while (currentId) {
|
||||||
|
const nextId = getNextLineId(this.state, currentId);
|
||||||
|
|
||||||
|
if (nextId && this.hasDelta(nextId)) {
|
||||||
|
return nextId;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentId = nextId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -1,85 +1,3 @@
|
|||||||
import { BlockData, DocumentBlockJSON, DocumentState, NestedBlock, RangeState } from '$app/interfaces/document';
|
|
||||||
import { getDeltaByRange } from '$app/utils/document/delta';
|
|
||||||
import Delta from 'quill-delta';
|
|
||||||
import { generateId } from '$app/utils/document/block';
|
|
||||||
import { DocumentController } from '$app/stores/effects/document/document_controller';
|
|
||||||
|
|
||||||
export function getCopyData(
|
|
||||||
node: NestedBlock,
|
|
||||||
range: {
|
|
||||||
index: number;
|
|
||||||
length: number;
|
|
||||||
}
|
|
||||||
): BlockData<any> {
|
|
||||||
const nodeDeltaOps = node.data.delta;
|
|
||||||
if (!nodeDeltaOps) {
|
|
||||||
return {
|
|
||||||
...node.data,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const delta = getDeltaByRange(new Delta(node.data.delta), range);
|
|
||||||
return {
|
|
||||||
...node.data,
|
|
||||||
delta: delta.ops,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCopyBlock(id: string, document: DocumentState, documentRange: RangeState): DocumentBlockJSON {
|
|
||||||
const node = document.nodes[id];
|
|
||||||
const range = documentRange.ranges[id] || { index: 0, length: 0 };
|
|
||||||
const copyData = getCopyData(node, range);
|
|
||||||
return {
|
|
||||||
type: node.type,
|
|
||||||
data: copyData,
|
|
||||||
children: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateBlocks(data: DocumentBlockJSON[], parentId: string) {
|
|
||||||
const blocks: NestedBlock[] = [];
|
|
||||||
function dfs(data: DocumentBlockJSON[], parentId: string) {
|
|
||||||
data.forEach((item) => {
|
|
||||||
const block = {
|
|
||||||
id: generateId(),
|
|
||||||
type: item.type,
|
|
||||||
data: item.data,
|
|
||||||
parent: parentId,
|
|
||||||
children: generateId(),
|
|
||||||
};
|
|
||||||
blocks.push(block);
|
|
||||||
if (item.children) {
|
|
||||||
dfs(item.children, block.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
dfs(data, parentId);
|
|
||||||
return blocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getInsertBlockActions(blocks: NestedBlock[], prevId: string, controller: DocumentController) {
|
|
||||||
return blocks.map((block, index) => {
|
|
||||||
const prevBlockId = index === 0 ? prevId : blocks[index - 1].id;
|
|
||||||
return controller.getInsertAction(block, prevBlockId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAppendBlockDeltaAction(
|
|
||||||
block: NestedBlock,
|
|
||||||
appendDelta: Delta,
|
|
||||||
isForward: boolean,
|
|
||||||
controller: DocumentController
|
|
||||||
) {
|
|
||||||
const nodeDelta = new Delta(block.data.delta);
|
|
||||||
const mergeDelta = isForward ? appendDelta.concat(nodeDelta) : nodeDelta.concat(appendDelta);
|
|
||||||
return controller.getUpdateAction({
|
|
||||||
...block,
|
|
||||||
data: {
|
|
||||||
...block.data,
|
|
||||||
delta: mergeDelta.ops,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function copyText(text: string) {
|
export function copyText(text: string) {
|
||||||
return navigator.clipboard.writeText(text);
|
return navigator.clipboard.writeText(text);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,8 @@ import { DeltaTypePB } from '@/services/backend/models/flowy-document2';
|
|||||||
import { BlockPBValue, BlockType, ChangeType, DocumentState, NestedBlock } from '$app/interfaces/document';
|
import { BlockPBValue, BlockType, ChangeType, DocumentState, NestedBlock } from '$app/interfaces/document';
|
||||||
import { Log } from '../log';
|
import { Log } from '../log';
|
||||||
import { isEqual } from '$app/utils/tool';
|
import { isEqual } from '$app/utils/tool';
|
||||||
import { BLOCK_MAP_NAME, CHILDREN_MAP_NAME, META_NAME } from '$app/constants/document/name';
|
import { BLOCK_MAP_NAME, CHILDREN_MAP_NAME, META_NAME, TEXT_MAP_NAME } from '$app/constants/document/name';
|
||||||
|
import Delta, { Op } from 'quill-delta';
|
||||||
|
|
||||||
// This is a list of all the possible changes that can happen to document data
|
// This is a list of all the possible changes that can happen to document data
|
||||||
const matchCases = [
|
const matchCases = [
|
||||||
@ -12,6 +13,9 @@ const matchCases = [
|
|||||||
{ match: matchChildrenMapInsert, type: ChangeType.ChildrenMapInsert, onMatch: onMatchChildrenInsert },
|
{ match: matchChildrenMapInsert, type: ChangeType.ChildrenMapInsert, onMatch: onMatchChildrenInsert },
|
||||||
{ match: matchChildrenMapUpdate, type: ChangeType.ChildrenMapUpdate, onMatch: onMatchChildrenUpdate },
|
{ match: matchChildrenMapUpdate, type: ChangeType.ChildrenMapUpdate, onMatch: onMatchChildrenUpdate },
|
||||||
{ match: matchChildrenMapDelete, type: ChangeType.ChildrenMapDelete, onMatch: onMatchChildrenDelete },
|
{ match: matchChildrenMapDelete, type: ChangeType.ChildrenMapDelete, onMatch: onMatchChildrenDelete },
|
||||||
|
{ match: matchDeltaMapInsert, type: ChangeType.DeltaMapInsert, onMatch: onMatchDeltaInsert },
|
||||||
|
{ match: matchDeltaMapUpdate, type: ChangeType.DeltaMapUpdate, onMatch: onMatchDeltaUpdate },
|
||||||
|
{ match: matchDeltaMapDelete, type: ChangeType.DeltaMapDelete, onMatch: onMatchDeltaDelete },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function matchChange(
|
export function matchChange(
|
||||||
@ -25,7 +29,7 @@ export function matchChange(
|
|||||||
command: DeltaTypePB;
|
command: DeltaTypePB;
|
||||||
path: string[];
|
path: string[];
|
||||||
id: string;
|
id: string;
|
||||||
value: BlockPBValue & string[];
|
value: BlockPBValue & string[] & Op[];
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const matchCase = matchCases.find((item) => item.match(command, path));
|
const matchCase = matchCases.find((item) => item.match(command, path));
|
||||||
@ -99,6 +103,39 @@ function matchChildrenMapDelete(command: DeltaTypePB, path: string[]) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param command DeltaTypePB.Inserted
|
||||||
|
* @param command
|
||||||
|
* @param path [META_NAME, TEXT_MAP_NAME]
|
||||||
|
*/
|
||||||
|
function matchDeltaMapInsert(command: DeltaTypePB, path: string[]) {
|
||||||
|
if (path.length !== 2) return false;
|
||||||
|
return command === DeltaTypePB.Inserted && path[0] === META_NAME && path[1] === TEXT_MAP_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param command DeltaTypePB.Updated
|
||||||
|
* @param command
|
||||||
|
* @param path [META_NAME, TEXT_MAP_NAME, id]
|
||||||
|
*/
|
||||||
|
function matchDeltaMapUpdate(command: DeltaTypePB, path: string[]) {
|
||||||
|
if (path.length !== 3) return false;
|
||||||
|
return (
|
||||||
|
command === DeltaTypePB.Updated && path[0] === META_NAME && path[1] === TEXT_MAP_NAME && typeof path[2] === 'string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param command DeltaTypePB.Removed
|
||||||
|
* @param path [META_NAME, TEXT_MAP_NAME, id]
|
||||||
|
*/
|
||||||
|
function matchDeltaMapDelete(command: DeltaTypePB, path: string[]) {
|
||||||
|
if (path.length !== 3) return false;
|
||||||
|
return (
|
||||||
|
command === DeltaTypePB.Removed && path[0] === META_NAME && path[1] === TEXT_MAP_NAME && typeof path[2] === 'string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function onMatchBlockInsert(state: DocumentState, blockId: string, blockValue: BlockPBValue) {
|
function onMatchBlockInsert(state: DocumentState, blockId: string, blockValue: BlockPBValue) {
|
||||||
state.nodes[blockId] = blockChangeValue2Node(blockValue);
|
state.nodes[blockId] = blockChangeValue2Node(blockValue);
|
||||||
}
|
}
|
||||||
@ -133,6 +170,22 @@ function onMatchChildrenDelete(state: DocumentState, id: string, _children: stri
|
|||||||
delete state.children[id];
|
delete state.children[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onMatchDeltaInsert(state: DocumentState, id: string, ops: Op[]) {
|
||||||
|
state.deltaMap[id] = JSON.stringify(ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMatchDeltaUpdate(state: DocumentState, id: string, ops: Op[]) {
|
||||||
|
const delta = new Delta(ops);
|
||||||
|
const oldDelta = new Delta(JSON.parse(state.deltaMap[id]));
|
||||||
|
const newDelta = oldDelta.compose(delta);
|
||||||
|
|
||||||
|
state.deltaMap[id] = JSON.stringify(newDelta.ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMatchDeltaDelete(state: DocumentState, id: string, _ops: Op[]) {
|
||||||
|
delete state.deltaMap[id];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* convert block change value to node
|
* convert block change value to node
|
||||||
* @param value
|
* @param value
|
||||||
@ -143,9 +196,9 @@ export function blockChangeValue2Node(value: BlockPBValue): NestedBlock {
|
|||||||
type: value.ty as BlockType,
|
type: value.ty as BlockType,
|
||||||
parent: value.parent,
|
parent: value.parent,
|
||||||
children: value.children,
|
children: value.children,
|
||||||
data: {
|
data: {},
|
||||||
delta: [],
|
externalId: value.external_id,
|
||||||
},
|
externalType: value.external_type,
|
||||||
};
|
};
|
||||||
|
|
||||||
if ('data' in value && typeof value.data === 'string') {
|
if ('data' in value && typeof value.data === 'string') {
|
||||||
@ -168,7 +221,7 @@ export function parseValue(value: string) {
|
|||||||
valueJson = JSON.parse(value);
|
valueJson = JSON.parse(value);
|
||||||
} catch {
|
} catch {
|
||||||
Log.error('[onDataChange] json parse error', value);
|
Log.error('[onDataChange] json parse error', value);
|
||||||
return;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return valueJson;
|
return valueJson;
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
export class Log {
|
export class Log {
|
||||||
static error(...msg: unknown[]) {
|
static error(...msg: unknown[]) {
|
||||||
console.log(...msg);
|
console.error(...msg);
|
||||||
}
|
}
|
||||||
static info(...msg: unknown[]) {
|
static info(...msg: unknown[]) {
|
||||||
console.log(...msg);
|
console.info(...msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
static debug(...msg: unknown[]) {
|
static debug(...msg: unknown[]) {
|
||||||
console.log(...msg);
|
console.debug(...msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
static trace(...msg: unknown[]) {
|
static trace(...msg: unknown[]) {
|
||||||
console.log(...msg);
|
console.trace(...msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
static warn(...msg: unknown[]) {
|
static warn(...msg: unknown[]) {
|
||||||
console.log(...msg);
|
console.warn(...msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
export {}
|
|
||||||
// import { AuthBackendService, UserBackendService } from '../appflowy_app/stores/effects/user/user_bd_svc';
|
|
||||||
// import { randomFillSync } from 'crypto';
|
|
||||||
// import { nanoid } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
// beforeAll(() => {
|
|
||||||
// //@ts-ignore
|
|
||||||
// window.crypto = {
|
|
||||||
// // @ts-ignore
|
|
||||||
// getRandomValues: function (buffer) {
|
|
||||||
// // @ts-ignore
|
|
||||||
// return randomFillSync(buffer);
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
|
|
||||||
// describe('User backend service', () => {
|
|
||||||
// it('sign up', async () => {
|
|
||||||
// const service = new AuthBackendService();
|
|
||||||
// const result = await service.autoSignUp();
|
|
||||||
// expect(result.ok).toBeTruthy;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it('sign in', async () => {
|
|
||||||
// const authService = new AuthBackendService();
|
|
||||||
// const email = nanoid(4) + '@appflowy.io';
|
|
||||||
// const password = nanoid(10);
|
|
||||||
// const signUpResult = await authService.signUp({ name: 'nathan', email: email, password: password });
|
|
||||||
// expect(signUpResult.ok).toBeTruthy;
|
|
||||||
|
|
||||||
// const signInResult = await authService.signIn({ email: email, password: password });
|
|
||||||
// expect(signInResult.ok).toBeTruthy;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it('get user profile', async () => {
|
|
||||||
// const service = new AuthBackendService();
|
|
||||||
// const result = await service.autoSignUp();
|
|
||||||
// const userProfile = result.unwrap();
|
|
||||||
|
|
||||||
// const userService = new UserBackendService(userProfile.id);
|
|
||||||
// expect((await userService.getUserProfile()).unwrap()).toBe(userProfile);
|
|
||||||
// });
|
|
||||||
// });
|
|
@ -20,8 +20,8 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"],
|
"@/*": ["src/*"],
|
||||||
"$app/*": ["src/appflowy_app/*"],
|
"$app/*": ["src/appflowy_app/*"],
|
||||||
"$app_reducers/*": ["src/appflowy_app/stores/reducers/*"],
|
"$app_reducers/*": ["src/appflowy_app/stores/reducers/*"]
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"include": ["src", "vite.config.ts", "../app_flowy/assets/translations"],
|
"include": ["src", "vite.config.ts", "../app_flowy/assets/translations"],
|
||||||
"exclude": ["node_modules"],
|
"exclude": ["node_modules"],
|
||||||
|
40
frontend/rust-lib/Cargo.lock
generated
40
frontend/rust-lib/Cargo.lock
generated
@ -120,7 +120,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "appflowy-integrate"
|
name = "appflowy-integrate"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -612,7 +612,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -631,7 +631,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-database"
|
name = "collab-database"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -660,7 +660,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-define"
|
name = "collab-define"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -672,7 +672,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-derive"
|
name = "collab-derive"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -684,12 +684,13 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-document"
|
name = "collab-document"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
"collab-derive",
|
"collab-derive",
|
||||||
"collab-persistence",
|
"collab-persistence",
|
||||||
|
"lib0",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"serde",
|
"serde",
|
||||||
@ -703,7 +704,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-folder"
|
name = "collab-folder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -723,7 +724,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-persistence"
|
name = "collab-persistence"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bincode",
|
"bincode",
|
||||||
@ -744,16 +745,17 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-plugins"
|
name = "collab-plugins"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"collab",
|
"collab",
|
||||||
"collab-define",
|
"collab-define",
|
||||||
"collab-persistence",
|
"collab-persistence",
|
||||||
"collab-sync",
|
"collab-sync-protocol",
|
||||||
"collab-ws",
|
"collab-ws",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"lib0",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
@ -770,23 +772,15 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-sync"
|
name = "collab-sync-protocol"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab",
|
"collab",
|
||||||
"futures-util",
|
|
||||||
"lib0",
|
|
||||||
"md5",
|
"md5",
|
||||||
"parking_lot",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
"tokio-stream",
|
|
||||||
"tokio-util",
|
|
||||||
"tracing",
|
|
||||||
"y-sync",
|
"y-sync",
|
||||||
"yrs",
|
"yrs",
|
||||||
]
|
]
|
||||||
@ -794,7 +788,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-user"
|
name = "collab-user"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -810,10 +804,10 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-ws"
|
name = "collab-ws"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6ce82#a6ce82e00345fb34eccaf2fad55ec722365d38fa"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=eaa9844#eaa9844c17bd64b2ef00c26245b5cb44756dda4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab-sync",
|
"collab-sync-protocol",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -49,14 +49,14 @@ lto = false
|
|||||||
incremental = false
|
incremental = false
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6ce82" }
|
collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "eaa9844" }
|
||||||
|
|
||||||
#collab = { path = "../AppFlowy-Collab/collab" }
|
#collab = { path = "../AppFlowy-Collab/collab" }
|
||||||
#collab-folder = { path = "../AppFlowy-Collab/collab-folder" }
|
#collab-folder = { path = "../AppFlowy-Collab/collab-folder" }
|
||||||
|
@ -52,11 +52,6 @@ fn subscribe_document_changed(doc_id: &str, document: &MutexDocument) {
|
|||||||
document
|
document
|
||||||
.lock()
|
.lock()
|
||||||
.subscribe_block_changed(move |events, is_remote| {
|
.subscribe_block_changed(move |events, is_remote| {
|
||||||
tracing::trace!(
|
|
||||||
"document changed: {:?}, from remote: {}",
|
|
||||||
&events,
|
|
||||||
is_remote
|
|
||||||
);
|
|
||||||
// send notification to the client.
|
// send notification to the client.
|
||||||
send_notification(&doc_id, DocumentNotification::DidReceiveUpdate)
|
send_notification(&doc_id, DocumentNotification::DidReceiveUpdate)
|
||||||
.payload::<DocEventPB>((events, is_remote).into())
|
.payload::<DocEventPB>((events, is_remote).into())
|
||||||
|
@ -17,12 +17,17 @@ impl From<DocumentData> for DocumentDataPB {
|
|||||||
.map(|(id, children)| (id, children.into()))
|
.map(|(id, children)| (id, children.into()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let text_map = data.meta.text_map.unwrap_or_default();
|
||||||
|
|
||||||
let page_id = data.page_id;
|
let page_id = data.page_id;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
page_id,
|
page_id,
|
||||||
blocks,
|
blocks,
|
||||||
meta: MetaPB { children_map },
|
meta: MetaPB {
|
||||||
|
children_map,
|
||||||
|
text_map,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,12 +47,16 @@ impl From<DocumentDataPB> for DocumentData {
|
|||||||
.map(|(id, children)| (id, children.children))
|
.map(|(id, children)| (id, children.children))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let text_map = data.meta.text_map;
|
||||||
let page_id = data.page_id;
|
let page_id = data.page_id;
|
||||||
|
|
||||||
DocumentData {
|
DocumentData {
|
||||||
page_id,
|
page_id,
|
||||||
blocks,
|
blocks,
|
||||||
meta: DocumentMeta { children_map },
|
meta: DocumentMeta {
|
||||||
|
children_map,
|
||||||
|
text_map: Some(text_map),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,6 +69,8 @@ impl From<Block> for BlockPB {
|
|||||||
data: serde_json::to_string(&block.data).unwrap_or_default(),
|
data: serde_json::to_string(&block.data).unwrap_or_default(),
|
||||||
parent_id: block.parent,
|
parent_id: block.parent,
|
||||||
children_id: block.children,
|
children_id: block.children,
|
||||||
|
external_id: block.external_id,
|
||||||
|
external_type: block.external_type,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,12 +166,20 @@ pub struct BlockPB {
|
|||||||
|
|
||||||
#[pb(index = 5)]
|
#[pb(index = 5)]
|
||||||
pub children_id: String,
|
pub children_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 6, one_of)]
|
||||||
|
pub external_id: Option<String>,
|
||||||
|
|
||||||
|
#[pb(index = 7, one_of)]
|
||||||
|
pub external_type: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf, Debug)]
|
#[derive(Default, ProtoBuf, Debug)]
|
||||||
pub struct MetaPB {
|
pub struct MetaPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub children_map: HashMap<String, ChildrenPB>,
|
pub children_map: HashMap<String, ChildrenPB>,
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub text_map: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf, Debug)]
|
#[derive(Default, ProtoBuf, Debug)]
|
||||||
@ -191,14 +199,26 @@ pub struct BlockActionPB {
|
|||||||
|
|
||||||
#[derive(Default, ProtoBuf, Debug)]
|
#[derive(Default, ProtoBuf, Debug)]
|
||||||
pub struct BlockActionPayloadPB {
|
pub struct BlockActionPayloadPB {
|
||||||
#[pb(index = 1)]
|
// When action = Insert, Update, Delete or Move, block needs to be passed.
|
||||||
pub block: BlockPB,
|
#[pb(index = 1, one_of)]
|
||||||
|
pub block: Option<BlockPB>,
|
||||||
|
|
||||||
|
// When action = Insert or Move, prev_id needs to be passed.
|
||||||
#[pb(index = 2, one_of)]
|
#[pb(index = 2, one_of)]
|
||||||
pub prev_id: Option<String>,
|
pub prev_id: Option<String>,
|
||||||
|
|
||||||
|
// When action = Insert or Move, parent_id needs to be passed.
|
||||||
#[pb(index = 3, one_of)]
|
#[pb(index = 3, one_of)]
|
||||||
pub parent_id: Option<String>,
|
pub parent_id: Option<String>,
|
||||||
|
|
||||||
|
// When action = InsertText or ApplyTextDelta, text_id needs to be passed.
|
||||||
|
#[pb(index = 4, one_of)]
|
||||||
|
pub text_id: Option<String>,
|
||||||
|
|
||||||
|
// When action = InsertText or ApplyTextDelta, delta needs to be passed.
|
||||||
|
// The format of delta is a JSON string, similar to the serialization result of [{ "insert": "Hello World" }].
|
||||||
|
#[pb(index = 5, one_of)]
|
||||||
|
pub delta: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ProtoBuf_Enum, Debug)]
|
#[derive(ProtoBuf_Enum, Debug)]
|
||||||
@ -207,6 +227,8 @@ pub enum BlockActionTypePB {
|
|||||||
Update = 1,
|
Update = 1,
|
||||||
Delete = 2,
|
Delete = 2,
|
||||||
Move = 3,
|
Move = 3,
|
||||||
|
InsertText = 4,
|
||||||
|
ApplyTextDelta = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for BlockActionTypePB {
|
impl Default for BlockActionTypePB {
|
||||||
@ -384,3 +406,36 @@ impl From<SyncState> for DocumentSyncStatePB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, ProtoBuf, Debug)]
|
||||||
|
pub struct TextDeltaPayloadPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub document_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub text_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 3, one_of)]
|
||||||
|
pub delta: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TextDeltaParams {
|
||||||
|
pub document_id: String,
|
||||||
|
pub text_id: String,
|
||||||
|
pub delta: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryInto<TextDeltaParams> for TextDeltaPayloadPB {
|
||||||
|
type Error = ErrorCode;
|
||||||
|
fn try_into(self) -> Result<TextDeltaParams, Self::Error> {
|
||||||
|
let document_id =
|
||||||
|
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
|
||||||
|
let text_id = NotEmptyStr::parse(self.text_id).map_err(|_| ErrorCode::TextIdIsEmpty)?;
|
||||||
|
let delta = self.delta.map_or_else(|| "".to_string(), |delta| delta);
|
||||||
|
Ok(TextDeltaParams {
|
||||||
|
document_id: document_id.0,
|
||||||
|
text_id: text_id.0,
|
||||||
|
delta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -91,6 +91,36 @@ pub(crate) async fn apply_action_handler(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for creating a text
|
||||||
|
pub(crate) async fn create_text_handler(
|
||||||
|
data: AFPluginData<TextDeltaPayloadPB>,
|
||||||
|
manager: AFPluginState<Weak<DocumentManager>>,
|
||||||
|
) -> FlowyResult<()> {
|
||||||
|
let manager = upgrade_document(manager)?;
|
||||||
|
let params: TextDeltaParams = data.into_inner().try_into()?;
|
||||||
|
let doc_id = params.document_id;
|
||||||
|
let document = manager.get_document(&doc_id).await?;
|
||||||
|
let document = document.lock();
|
||||||
|
document.create_text(¶ms.text_id, params.delta);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handler for applying delta to a text
|
||||||
|
pub(crate) async fn apply_text_delta_handler(
|
||||||
|
data: AFPluginData<TextDeltaPayloadPB>,
|
||||||
|
manager: AFPluginState<Weak<DocumentManager>>,
|
||||||
|
) -> FlowyResult<()> {
|
||||||
|
let manager = upgrade_document(manager)?;
|
||||||
|
let params: TextDeltaParams = data.into_inner().try_into()?;
|
||||||
|
let doc_id = params.document_id;
|
||||||
|
let document = manager.get_document(&doc_id).await?;
|
||||||
|
let text_id = params.text_id;
|
||||||
|
let delta = params.delta;
|
||||||
|
let document = document.lock();
|
||||||
|
document.apply_text_delta(&text_id, delta);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn convert_data_to_document(
|
pub(crate) async fn convert_data_to_document(
|
||||||
data: AFPluginData<ConvertDataPayloadPB>,
|
data: AFPluginData<ConvertDataPayloadPB>,
|
||||||
) -> DataResult<DocumentDataPB, FlowyError> {
|
) -> DataResult<DocumentDataPB, FlowyError> {
|
||||||
@ -198,6 +228,8 @@ impl From<BlockActionTypePB> for BlockActionType {
|
|||||||
BlockActionTypePB::Update => Self::Update,
|
BlockActionTypePB::Update => Self::Update,
|
||||||
BlockActionTypePB::Delete => Self::Delete,
|
BlockActionTypePB::Delete => Self::Delete,
|
||||||
BlockActionTypePB::Move => Self::Move,
|
BlockActionTypePB::Move => Self::Move,
|
||||||
|
BlockActionTypePB::InsertText => Self::InsertText,
|
||||||
|
BlockActionTypePB::ApplyTextDelta => Self::ApplyTextDelta,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -205,9 +237,11 @@ impl From<BlockActionTypePB> for BlockActionType {
|
|||||||
impl From<BlockActionPayloadPB> for BlockActionPayload {
|
impl From<BlockActionPayloadPB> for BlockActionPayload {
|
||||||
fn from(pb: BlockActionPayloadPB) -> Self {
|
fn from(pb: BlockActionPayloadPB) -> Self {
|
||||||
Self {
|
Self {
|
||||||
block: pb.block.into(),
|
block: pb.block.map(|b| b.into()),
|
||||||
parent_id: pb.parent_id,
|
parent_id: pb.parent_id,
|
||||||
prev_id: pb.prev_id,
|
prev_id: pb.prev_id,
|
||||||
|
text_id: pb.text_id,
|
||||||
|
delta: pb.delta,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,8 +258,8 @@ impl From<BlockPB> for Block {
|
|||||||
children: pb.children_id,
|
children: pb.children_id,
|
||||||
parent: pb.parent_id,
|
parent: pb.parent_id,
|
||||||
data,
|
data,
|
||||||
external_id: None,
|
external_id: pb.external_id,
|
||||||
external_type: None,
|
external_type: pb.external_type,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,8 @@ pub fn init(document_manager: Weak<DocumentManager>) -> AFPlugin {
|
|||||||
.event(DocumentEvent::Undo, undo_handler)
|
.event(DocumentEvent::Undo, undo_handler)
|
||||||
.event(DocumentEvent::CanUndoRedo, can_undo_redo_handler)
|
.event(DocumentEvent::CanUndoRedo, can_undo_redo_handler)
|
||||||
.event(DocumentEvent::GetDocumentSnapshots, get_snapshot_handler)
|
.event(DocumentEvent::GetDocumentSnapshots, get_snapshot_handler)
|
||||||
|
.event(DocumentEvent::CreateText, create_text_handler)
|
||||||
|
.event(DocumentEvent::ApplyTextDeltaEvent, apply_text_delta_handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
|
||||||
@ -68,4 +70,10 @@ pub enum DocumentEvent {
|
|||||||
|
|
||||||
#[event(input = "OpenDocumentPayloadPB", output = "RepeatedDocumentSnapshotPB")]
|
#[event(input = "OpenDocumentPayloadPB", output = "RepeatedDocumentSnapshotPB")]
|
||||||
GetDocumentSnapshots = 9,
|
GetDocumentSnapshots = 9,
|
||||||
|
|
||||||
|
#[event(input = "TextDeltaPayloadPB")]
|
||||||
|
CreateText = 10,
|
||||||
|
|
||||||
|
#[event(input = "TextDeltaPayloadPB")]
|
||||||
|
ApplyTextDeltaEvent = 11,
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ use super::block::Block;
|
|||||||
|
|
||||||
pub struct JsonToDocumentParser;
|
pub struct JsonToDocumentParser;
|
||||||
|
|
||||||
|
const DELTA: &str = "delta";
|
||||||
|
const TEXT_EXTERNAL_TYPE: &str = "text";
|
||||||
impl JsonToDocumentParser {
|
impl JsonToDocumentParser {
|
||||||
pub fn json_str_to_document(json_str: &str) -> FlowyResult<DocumentDataPB> {
|
pub fn json_str_to_document(json_str: &str) -> FlowyResult<DocumentDataPB> {
|
||||||
let root = serde_json::from_str::<Block>(json_str)?;
|
let root = serde_json::from_str::<Block>(json_str)?;
|
||||||
@ -19,15 +21,20 @@ impl JsonToDocumentParser {
|
|||||||
|
|
||||||
// generate the blocks
|
// generate the blocks
|
||||||
// the root's parent id is empty
|
// the root's parent id is empty
|
||||||
let blocks = Self::generate_blocks(&root, Some(page_id.clone()), "".to_string());
|
let (blocks, text_map) = Self::generate_blocks(&root, Some(page_id.clone()), "".to_string());
|
||||||
|
|
||||||
// generate the children map
|
// generate the children map
|
||||||
let children_map = Self::generate_children_map(&blocks);
|
let children_map = Self::generate_children_map(&blocks);
|
||||||
|
|
||||||
|
// generate the text map
|
||||||
|
let text_map = Self::generate_text_map(&text_map);
|
||||||
Ok(DocumentDataPB {
|
Ok(DocumentDataPB {
|
||||||
page_id,
|
page_id,
|
||||||
blocks: blocks.into_iter().collect(),
|
blocks: blocks.into_iter().collect(),
|
||||||
meta: MetaPB { children_map },
|
meta: MetaPB {
|
||||||
|
children_map,
|
||||||
|
text_map,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,15 +42,31 @@ impl JsonToDocumentParser {
|
|||||||
block: &Block,
|
block: &Block,
|
||||||
id: Option<String>,
|
id: Option<String>,
|
||||||
parent_id: String,
|
parent_id: String,
|
||||||
) -> IndexMap<String, BlockPB> {
|
) -> (IndexMap<String, BlockPB>, IndexMap<String, String>) {
|
||||||
let block_pb = Self::block_to_block_pb(block, id, parent_id);
|
let (block_pb, delta) = Self::block_to_block_pb(block, id, parent_id);
|
||||||
let mut blocks = IndexMap::new();
|
let mut blocks = IndexMap::new();
|
||||||
|
let mut text_map = IndexMap::new();
|
||||||
for child in &block.children {
|
for child in &block.children {
|
||||||
let child_blocks = Self::generate_blocks(child, None, block_pb.id.clone());
|
let (child_blocks, child_blocks_text_map) =
|
||||||
|
Self::generate_blocks(child, None, block_pb.id.clone());
|
||||||
blocks.extend(child_blocks);
|
blocks.extend(child_blocks);
|
||||||
|
text_map.extend(child_blocks_text_map);
|
||||||
}
|
}
|
||||||
|
let external_id = block_pb.external_id.clone();
|
||||||
blocks.insert(block_pb.id.clone(), block_pb);
|
blocks.insert(block_pb.id.clone(), block_pb);
|
||||||
blocks
|
if let Some(delta) = delta {
|
||||||
|
if let Some(external_id) = external_id {
|
||||||
|
text_map.insert(external_id, delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(blocks, text_map)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_text_map(text_map: &IndexMap<String, String>) -> HashMap<String, String> {
|
||||||
|
text_map
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.clone(), v.clone()))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_children_map(blocks: &IndexMap<String, BlockPB>) -> HashMap<String, ChildrenPB> {
|
fn generate_children_map(blocks: &IndexMap<String, BlockPB>) -> HashMap<String, ChildrenPB> {
|
||||||
@ -69,14 +92,32 @@ impl JsonToDocumentParser {
|
|||||||
children_map
|
children_map
|
||||||
}
|
}
|
||||||
|
|
||||||
fn block_to_block_pb(block: &Block, id: Option<String>, parent_id: String) -> BlockPB {
|
fn block_to_block_pb(
|
||||||
|
block: &Block,
|
||||||
|
id: Option<String>,
|
||||||
|
parent_id: String,
|
||||||
|
) -> (BlockPB, Option<String>) {
|
||||||
let id = id.unwrap_or_else(|| nanoid!(10));
|
let id = id.unwrap_or_else(|| nanoid!(10));
|
||||||
BlockPB {
|
let mut data = block.data.clone();
|
||||||
id,
|
|
||||||
ty: block.ty.clone(),
|
let delta = data.remove(DELTA).map(|d| d.to_string());
|
||||||
data: serde_json::to_string(&block.data).unwrap(),
|
|
||||||
parent_id,
|
let (external_id, external_type) = match delta {
|
||||||
children_id: nanoid!(10),
|
None => (None, None),
|
||||||
}
|
Some(_) => (Some(nanoid!(10)), Some(TEXT_EXTERNAL_TYPE.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
BlockPB {
|
||||||
|
id,
|
||||||
|
ty: block.ty.clone(),
|
||||||
|
data: serde_json::to_string(&data).unwrap(),
|
||||||
|
parent_id,
|
||||||
|
children_id: nanoid!(10),
|
||||||
|
external_id,
|
||||||
|
external_type,
|
||||||
|
},
|
||||||
|
delta,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,11 @@ async fn document_apply_insert_block_with_empty_parent_id() {
|
|||||||
let insert_text_action = BlockAction {
|
let insert_text_action = BlockAction {
|
||||||
action: BlockActionType::Insert,
|
action: BlockActionType::Insert,
|
||||||
payload: BlockActionPayload {
|
payload: BlockActionPayload {
|
||||||
block: text_block,
|
block: Some(text_block),
|
||||||
parent_id: Some(page_id.clone()),
|
parent_id: Some(page_id.clone()),
|
||||||
prev_id: None,
|
prev_id: None,
|
||||||
|
delta: None,
|
||||||
|
text_id: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
document.lock().apply_action(vec![insert_text_action]);
|
document.lock().apply_action(vec![insert_text_action]);
|
||||||
|
@ -37,9 +37,11 @@ async fn undo_redo_test() {
|
|||||||
let insert_text_action = BlockAction {
|
let insert_text_action = BlockAction {
|
||||||
action: BlockActionType::Insert,
|
action: BlockActionType::Insert,
|
||||||
payload: BlockActionPayload {
|
payload: BlockActionPayload {
|
||||||
block: text_block,
|
block: Some(text_block),
|
||||||
parent_id: Some(page_id),
|
parent_id: Some(page_id),
|
||||||
prev_id: None,
|
prev_id: None,
|
||||||
|
delta: None,
|
||||||
|
text_id: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
document.apply_action(vec![insert_text_action]);
|
document.apply_action(vec![insert_text_action]);
|
||||||
|
@ -21,7 +21,6 @@ async fn restore_document() {
|
|||||||
let data_a = document_a.lock().get_document_data().unwrap();
|
let data_a = document_a.lock().get_document_data().unwrap();
|
||||||
assert_eq!(data_a, data);
|
assert_eq!(data_a, data);
|
||||||
|
|
||||||
// open a document
|
|
||||||
let data_b = test
|
let data_b = test
|
||||||
.get_document(&doc_id)
|
.get_document(&doc_id)
|
||||||
.await
|
.await
|
||||||
@ -76,9 +75,11 @@ async fn document_apply_insert_action() {
|
|||||||
let insert_text_action = BlockAction {
|
let insert_text_action = BlockAction {
|
||||||
action: BlockActionType::Insert,
|
action: BlockActionType::Insert,
|
||||||
payload: BlockActionPayload {
|
payload: BlockActionPayload {
|
||||||
block: text_block,
|
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
prev_id: None,
|
prev_id: None,
|
||||||
|
block: Some(text_block),
|
||||||
|
delta: None,
|
||||||
|
text_id: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
document.lock().apply_action(vec![insert_text_action]);
|
document.lock().apply_action(vec![insert_text_action]);
|
||||||
@ -123,9 +124,11 @@ async fn document_apply_update_page_action() {
|
|||||||
let action = BlockAction {
|
let action = BlockAction {
|
||||||
action: BlockActionType::Update,
|
action: BlockActionType::Update,
|
||||||
payload: BlockActionPayload {
|
payload: BlockActionPayload {
|
||||||
block: page_block_clone,
|
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
prev_id: None,
|
prev_id: None,
|
||||||
|
block: Some(page_block_clone),
|
||||||
|
delta: None,
|
||||||
|
text_id: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let actions = vec![action];
|
let actions = vec![action];
|
||||||
@ -169,9 +172,11 @@ async fn document_apply_update_action() {
|
|||||||
let insert_text_action = BlockAction {
|
let insert_text_action = BlockAction {
|
||||||
action: BlockActionType::Insert,
|
action: BlockActionType::Insert,
|
||||||
payload: BlockActionPayload {
|
payload: BlockActionPayload {
|
||||||
block: text_block,
|
block: Some(text_block),
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
prev_id: None,
|
prev_id: None,
|
||||||
|
delta: None,
|
||||||
|
text_id: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
document.lock().apply_action(vec![insert_text_action]);
|
document.lock().apply_action(vec![insert_text_action]);
|
||||||
@ -192,9 +197,11 @@ async fn document_apply_update_action() {
|
|||||||
let update_text_action = BlockAction {
|
let update_text_action = BlockAction {
|
||||||
action: BlockActionType::Update,
|
action: BlockActionType::Update,
|
||||||
payload: BlockActionPayload {
|
payload: BlockActionPayload {
|
||||||
block: updated_text_block,
|
block: Some(updated_text_block),
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
prev_id: None,
|
prev_id: None,
|
||||||
|
delta: None,
|
||||||
|
text_id: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
document.lock().apply_action(vec![update_text_action]);
|
document.lock().apply_action(vec![update_text_action]);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use collab_document::blocks::json_str_to_hashmap;
|
||||||
use flowy_document2::parser::json::parser::JsonToDocumentParser;
|
use flowy_document2::parser::json::parser::JsonToDocumentParser;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
@ -101,3 +102,22 @@ fn test_parser_nested_children() {
|
|||||||
assert_eq!(page_first_child.ty, "paragraph");
|
assert_eq!(page_first_child.ty, "paragraph");
|
||||||
assert_eq!(page_first_child.parent_id, page_id.to_owned());
|
assert_eq!(page_first_child.parent_id, page_id.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn parse_readme_test() {
|
||||||
|
let json = include_str!("../../../../flowy-core/assets/read_me.json");
|
||||||
|
let document = JsonToDocumentParser::json_str_to_document(json).unwrap();
|
||||||
|
|
||||||
|
document.blocks.iter().for_each(|(_, block)| {
|
||||||
|
let data = json_str_to_hashmap(&block.data).ok();
|
||||||
|
assert!(data.is_some());
|
||||||
|
if let Some(data) = data {
|
||||||
|
assert!(data.get("delta").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(external_id) = &block.external_id {
|
||||||
|
let text = document.meta.text_map.get(external_id);
|
||||||
|
assert!(text.is_some());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -238,6 +238,8 @@ pub enum ErrorCode {
|
|||||||
|
|
||||||
#[error("Parse url failed")]
|
#[error("Parse url failed")]
|
||||||
InvalidURL = 78,
|
InvalidURL = 78,
|
||||||
|
#[error("Text id is empty")]
|
||||||
|
TextIdIsEmpty = 79,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorCode {
|
impl ErrorCode {
|
||||||
|
@ -2,8 +2,10 @@ use flowy_document2::entities::*;
|
|||||||
use flowy_document2::event_map::DocumentEvent;
|
use flowy_document2::event_map::DocumentEvent;
|
||||||
use flowy_folder2::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB};
|
use flowy_folder2::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB};
|
||||||
use flowy_folder2::event_map::FolderEvent;
|
use flowy_folder2::event_map::FolderEvent;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::document::utils::{gen_id, gen_text_block_data};
|
use crate::document::utils::{gen_delta_str, gen_id, gen_text_block_data};
|
||||||
use crate::event_builder::EventBuilder;
|
use crate::event_builder::EventBuilder;
|
||||||
use crate::FlowyCoreTest;
|
use crate::FlowyCoreTest;
|
||||||
|
|
||||||
@ -90,6 +92,11 @@ impl DocumentEventTest {
|
|||||||
children_map.get(&children_id).map(|c| c.children.clone())
|
children_map.get(&children_id).map(|c| c.children.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_block_text_delta(&self, doc_id: &str, text_id: &str) -> Option<String> {
|
||||||
|
let document_data = self.get_document_data(doc_id).await;
|
||||||
|
document_data.meta.text_map.get(text_id).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn apply_actions(&self, payload: ApplyActionPayloadPB) {
|
pub async fn apply_actions(&self, payload: ApplyActionPayloadPB) {
|
||||||
let core = &self.inner;
|
let core = &self.inner;
|
||||||
EventBuilder::new(core.clone())
|
EventBuilder::new(core.clone())
|
||||||
@ -99,6 +106,24 @@ impl DocumentEventTest {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_text(&self, payload: TextDeltaPayloadPB) {
|
||||||
|
let core = &self.inner;
|
||||||
|
EventBuilder::new(core.clone())
|
||||||
|
.event(DocumentEvent::CreateText)
|
||||||
|
.payload(payload)
|
||||||
|
.async_send()
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn apply_text_delta(&self, payload: TextDeltaPayloadPB) {
|
||||||
|
let core = &self.inner;
|
||||||
|
EventBuilder::new(core.clone())
|
||||||
|
.event(DocumentEvent::ApplyTextDeltaEvent)
|
||||||
|
.payload(payload)
|
||||||
|
.async_send()
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn undo(&self, doc_id: String) -> DocumentRedoUndoResponsePB {
|
pub async fn undo(&self, doc_id: String) -> DocumentRedoUndoResponsePB {
|
||||||
let core = &self.inner;
|
let core = &self.inner;
|
||||||
let payload = DocumentRedoUndoPayloadPB {
|
let payload = DocumentRedoUndoPayloadPB {
|
||||||
@ -138,6 +163,19 @@ impl DocumentEventTest {
|
|||||||
.parse::<DocumentRedoUndoResponsePB>()
|
.parse::<DocumentRedoUndoResponsePB>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn apply_delta_for_block(&self, document_id: &str, block_id: &str, delta: String) {
|
||||||
|
let block = self.get_block(document_id, block_id).await;
|
||||||
|
// Here is unsafe, but it should be fine for testing.
|
||||||
|
let text_id = block.unwrap().external_id.unwrap();
|
||||||
|
self
|
||||||
|
.apply_text_delta(TextDeltaPayloadPB {
|
||||||
|
document_id: document_id.to_string(),
|
||||||
|
text_id,
|
||||||
|
delta: Some(delta),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert a new text block at the index of parent's children.
|
/// Insert a new text block at the index of parent's children.
|
||||||
/// return the new block id.
|
/// return the new block id.
|
||||||
pub async fn insert_index(
|
pub async fn insert_index(
|
||||||
@ -171,7 +209,18 @@ impl DocumentEventTest {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let new_block_id = gen_id();
|
let new_block_id = gen_id();
|
||||||
let data = gen_text_block_data(&text);
|
let data = gen_text_block_data();
|
||||||
|
|
||||||
|
let external_id = gen_id();
|
||||||
|
let external_type = "text".to_string();
|
||||||
|
|
||||||
|
self
|
||||||
|
.create_text(TextDeltaPayloadPB {
|
||||||
|
document_id: document_id.to_string(),
|
||||||
|
text_id: external_id.clone(),
|
||||||
|
delta: Some(gen_delta_str(&text)),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
let new_block = BlockPB {
|
let new_block = BlockPB {
|
||||||
id: new_block_id.clone(),
|
id: new_block_id.clone(),
|
||||||
@ -179,13 +228,17 @@ impl DocumentEventTest {
|
|||||||
data,
|
data,
|
||||||
parent_id: parent_id.clone(),
|
parent_id: parent_id.clone(),
|
||||||
children_id: gen_id(),
|
children_id: gen_id(),
|
||||||
|
external_id: Some(external_id),
|
||||||
|
external_type: Some(external_type),
|
||||||
};
|
};
|
||||||
let action = BlockActionPB {
|
let action = BlockActionPB {
|
||||||
action: BlockActionTypePB::Insert,
|
action: BlockActionTypePB::Insert,
|
||||||
payload: BlockActionPayloadPB {
|
payload: BlockActionPayloadPB {
|
||||||
block: new_block,
|
block: Some(new_block),
|
||||||
prev_id,
|
prev_id,
|
||||||
parent_id: Some(parent_id),
|
parent_id: Some(parent_id),
|
||||||
|
text_id: None,
|
||||||
|
delta: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let payload = ApplyActionPayloadPB {
|
let payload = ApplyActionPayloadPB {
|
||||||
@ -196,20 +249,22 @@ impl DocumentEventTest {
|
|||||||
new_block_id
|
new_block_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update(&self, document_id: &str, block_id: &str, text: &str) {
|
pub async fn update_data(&self, document_id: &str, block_id: &str, data: HashMap<String, Value>) {
|
||||||
let block = self.get_block(document_id, block_id).await.unwrap();
|
let block = self.get_block(document_id, block_id).await.unwrap();
|
||||||
let data = gen_text_block_data(text);
|
|
||||||
let new_block = {
|
let new_block = {
|
||||||
let mut new_block = block.clone();
|
let mut new_block = block.clone();
|
||||||
new_block.data = data;
|
new_block.data = serde_json::to_string(&data).unwrap();
|
||||||
new_block
|
new_block
|
||||||
};
|
};
|
||||||
let action = BlockActionPB {
|
let action = BlockActionPB {
|
||||||
action: BlockActionTypePB::Update,
|
action: BlockActionTypePB::Update,
|
||||||
payload: BlockActionPayloadPB {
|
payload: BlockActionPayloadPB {
|
||||||
block: new_block,
|
block: Some(new_block),
|
||||||
prev_id: None,
|
prev_id: None,
|
||||||
parent_id: Some(block.parent_id.clone()),
|
parent_id: Some(block.parent_id.clone()),
|
||||||
|
text_id: None,
|
||||||
|
delta: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let payload = ApplyActionPayloadPB {
|
let payload = ApplyActionPayloadPB {
|
||||||
@ -225,9 +280,11 @@ impl DocumentEventTest {
|
|||||||
let action = BlockActionPB {
|
let action = BlockActionPB {
|
||||||
action: BlockActionTypePB::Delete,
|
action: BlockActionTypePB::Delete,
|
||||||
payload: BlockActionPayloadPB {
|
payload: BlockActionPayloadPB {
|
||||||
block,
|
block: Some(block),
|
||||||
prev_id: None,
|
prev_id: None,
|
||||||
parent_id: Some(parent_id),
|
parent_id: Some(parent_id),
|
||||||
|
text_id: None,
|
||||||
|
delta: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let payload = ApplyActionPayloadPB {
|
let payload = ApplyActionPayloadPB {
|
||||||
|
@ -8,13 +8,12 @@ pub fn gen_id() -> String {
|
|||||||
nanoid!(10)
|
nanoid!(10)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gen_text_block_data(text: &str) -> String {
|
pub fn gen_text_block_data() -> String {
|
||||||
json!({
|
json!({}).to_string()
|
||||||
"delta": [{
|
}
|
||||||
"insert": text
|
|
||||||
}]
|
pub fn gen_delta_str(text: &str) -> String {
|
||||||
})
|
json!([{ "insert": text }]).to_string()
|
||||||
.to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ParseDocumentData {
|
pub struct ParseDocumentData {
|
||||||
@ -56,13 +55,17 @@ pub fn gen_insert_block_action(document: OpenDocumentData) -> BlockActionPB {
|
|||||||
data,
|
data,
|
||||||
parent_id: page_id.clone(),
|
parent_id: page_id.clone(),
|
||||||
children_id: gen_id(),
|
children_id: gen_id(),
|
||||||
|
external_id: None,
|
||||||
|
external_type: None,
|
||||||
};
|
};
|
||||||
BlockActionPB {
|
BlockActionPB {
|
||||||
action: BlockActionTypePB::Insert,
|
action: BlockActionTypePB::Insert,
|
||||||
payload: BlockActionPayloadPB {
|
payload: BlockActionPayloadPB {
|
||||||
block: new_block,
|
block: Some(new_block),
|
||||||
prev_id: Some(first_block_id),
|
prev_id: Some(first_block_id),
|
||||||
parent_id: Some(page_id),
|
parent_id: Some(page_id),
|
||||||
|
text_id: None,
|
||||||
|
delta: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
use collab_document::blocks::json_str_to_hashmap;
|
||||||
use flowy_document2::entities::*;
|
use flowy_document2::entities::*;
|
||||||
use flowy_test::document::document_event::DocumentEventTest;
|
use flowy_test::document::document_event::DocumentEventTest;
|
||||||
use flowy_test::document::utils::*;
|
use flowy_test::document::utils::*;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn get_document_event_test() {
|
async fn get_document_event_test() {
|
||||||
@ -70,20 +73,50 @@ async fn insert_text_block_test() {
|
|||||||
let block = test.get_block(&view.id, &block_id).await;
|
let block = test.get_block(&view.id, &block_id).await;
|
||||||
assert!(block.is_some());
|
assert!(block.is_some());
|
||||||
let block = block.unwrap();
|
let block = block.unwrap();
|
||||||
let data = gen_text_block_data(text);
|
assert!(block.external_id.is_some());
|
||||||
assert_eq!(block.data, data);
|
let external_id = block.external_id.unwrap();
|
||||||
|
let delta = test.get_block_text_delta(&view.id, &external_id).await;
|
||||||
|
assert_eq!(delta.unwrap(), json!([{ "insert": text }]).to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn update_text_block_test() {
|
async fn update_block_test() {
|
||||||
let test = DocumentEventTest::new().await;
|
let test = DocumentEventTest::new().await;
|
||||||
let view = test.create_document().await;
|
let view = test.create_document().await;
|
||||||
let block_id = test.insert_index(&view.id, "Hello World", 1, None).await;
|
let block_id = test.insert_index(&view.id, "Hello World", 1, None).await;
|
||||||
let update_text = "Hello World 2";
|
let data: HashMap<String, Value> = HashMap::from([
|
||||||
test.update(&view.id, &block_id, update_text).await;
|
(
|
||||||
|
"bg_color".to_string(),
|
||||||
|
serde_json::to_value("#000000").unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"text_color".to_string(),
|
||||||
|
serde_json::to_value("#ffffff").unwrap(),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
test.update_data(&view.id, &block_id, data.clone()).await;
|
||||||
let block = test.get_block(&view.id, &block_id).await;
|
let block = test.get_block(&view.id, &block_id).await;
|
||||||
assert!(block.is_some());
|
assert!(block.is_some());
|
||||||
let block = block.unwrap();
|
let block = block.unwrap();
|
||||||
let update_data = gen_text_block_data(update_text);
|
let block_data = json_str_to_hashmap(&block.data).ok().unwrap();
|
||||||
assert_eq!(block.data, update_data);
|
assert_eq!(block_data, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn apply_text_delta_test() {
|
||||||
|
let test = DocumentEventTest::new().await;
|
||||||
|
let view = test.create_document().await;
|
||||||
|
let text = "Hello World";
|
||||||
|
let block_id = test.insert_index(&view.id, text, 1, None).await;
|
||||||
|
let update_delta = json!([{ "retain": 5 }, { "insert": "!" }]).to_string();
|
||||||
|
test
|
||||||
|
.apply_delta_for_block(&view.id, &block_id, update_delta)
|
||||||
|
.await;
|
||||||
|
let block = test.get_block(&view.id, &block_id).await;
|
||||||
|
let text_id = block.unwrap().external_id.unwrap();
|
||||||
|
let block_delta = test.get_block_text_delta(&view.id, &text_id).await;
|
||||||
|
assert_eq!(
|
||||||
|
block_delta.unwrap(),
|
||||||
|
json!([{ "insert": "Hello! World" }]).to_string()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user