appflowy-editor icon indicating copy to clipboard operation
appflowy-editor copied to clipboard

[Bug][Web] Unwanted space on click and overall space bugs

Open MichalNemec opened this issue 10 months ago • 5 comments

Bug Description

Im running appflowy_editor in web platform and i encountered issue where multiple lines of text do weird stuff when i click within to edit. It seems like its happening only on web version. When i load it up: Image

When i click "unchanged.|": Image

cursor ends up on " | It was popularised..." and i can just hold backspace, it removes the spaces and then creates the spaces again and this can go forever.

EDIT: also when i click within (ignoring the creation of spaces) and i do cmd+A -> everything is cleared out instead of selecting the text.

How to Reproduce

EDIT: can be reproduced in example of appflowy, just replace example.json with the value of test variable below.

Image

Tested string, used like this:

var test = {
        "document": {
          "type": "page",
          "children": [
            {
              "type": "paragraph",
              "data": {
                "delta": [
                  {
                    "insert":
                        "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.\r"
                  }
                ]
              }
            },
          ]
        }
      };

EditorState(document: Document.fromJson(test))
Log output when i click within the text:
[DEBUG][editor]: 2025-01-28 17:32:00.602: keyboard service - attach text input service: TextEditingValue(text: ┤Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into
electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.├, selection: TextSelection.collapsed(offset: 366, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
[DEBUG][input]: 2025-01-28 17:32:00.626: attach text editing value: TextEditingValue(text: ┤Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting,
remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.├, selection: TextSelection.collapsed(offset: 366, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
[DEBUG][editor]: 2025-01-28 17:32:00.638: keyboard service - request focus
[DEBUG][editor]: 2025-01-28 17:32:00.640: keyboard service - focus changed: true}
[DEBUG][input]: 2025-01-28 17:32:00.668: onReplace: TextEditingDeltaReplacement#2e28e(oldText: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic
typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum., textReplaced:, replacementText:
  , replacedRange: TextRange(start: 574, end: 575), selection: TextSelection.collapsed(offset: 366, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
}]}}]}][editor]: 2025-01-28 17:32:00.677: apply op (local): {op: insert, path: [1], nodes: [{type: paragraph, data: {delta: [{insert:  It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
}]}, children: []) at path [1]}32:00.678: insert Node Node(id: 848Fc4, type: paragraph, attributes: {delta: [{insert:  It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
[DEBUG][editor]: 2025-01-28 17:32:00.680: apply op (local): {op: update, path: [0], attributes: {delta: [{insert: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into
electronic typesetting, remaining essentially unchanged.}]}, oldAttributes: {delta: [{insert: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting,
remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.}]}}
[DEBUG][editor]: 2025-01-28 17:32:00.683: keyboard service - attach text input service: TextEditingValue(text: ┤ It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.├, selection: TextSelection.collapsed(offset: 0, affinity: TextAffinity.downstream, isDirectional: false),
composing: TextRange(start: -1, end: -1))
[DEBUG][input]: 2025-01-28 17:32:00.686: attach text editing value: TextEditingValue(text: ┤ It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.├, selection: TextSelection.collapsed(offset: 0, affinity: TextAffinity.downstream, isDirectional: false), composing:
TextRange(start: -1, end: -1))
[DEBUG][editor]: 2025-01-28 17:32:00.687: keyboard service - selection changed: start = path = [1], offset = 0, end = path = [1], offset = 0
[DEBUG][input]: 2025-01-28 17:32:00.689: keyboard service - handled by character shortcut event: CharacterShortcutEvent(key: insert a new line, character: 
, handler: Closure: (EditorState) => Future<bool> from: editorState => {
        let t$3687;
        let t$goto = 0, t$completer = async._makeAsyncAwaitCompleter(dart_rti._Universe.eval(dart_rti._theUniverse(), "core|bool", true)), t$returnValue, asyncScope = Object.create(null);
        var t$36asyncBody = async._wrapJsFunctionForAsync((t$errorCode, t$result) => {
          if (t$errorCode === 1) return async._asyncRethrow(t$result, t$completer);
          while (true)
            switch (t$goto) {
              case 0:
                // Function start
                if (platform_extension['PlatformExtension|isNotMobile'] && hardware_keyboard.HardwareKeyboard.instance.isShiftPressed) {
                  t$returnValue = false;
                  // goto return
                  t$goto = 2;
                  break;
                }
                asyncScope.selection = (t$3687 = editorState.selection, t$3687 == null ? null : t$3687.normalized);
                if (asyncScope.selection == null) {
                  t$returnValue = false;
                  // goto return
                  t$goto = 2;
                  break;
                }
                t$goto = 3;
                return async._asyncAwait(selection_commands['SelectionTransform|deleteSelection'](editorState, asyncScope.selection), t$36asyncBody, t$completer);
              case 3:
                // returning from await.
                t$goto = 4;
                return async._asyncAwait(text_commands['TextTransforms|insertNewLine'](editorState, {position: asyncScope.selection.start}), t$36asyncBody, t$completer);
              case 4:
                // returning from await.
                t$returnValue = true;
                // goto return
                t$goto = 2;
                break;
              case 2:
                // return
                return async._asyncReturn(t$returnValue, t$completer);
            }
        });
        return async._asyncStartSync(t$36asyncBody, t$completer);
      })
[DEBUG][editor]: 2025-01-28 17:32:00.701: node is rebuilding...: type: page 
[DEBUG][editor]: 2025-01-28 17:32:00.709: node is rebuilding...: type: paragraph 
[DEBUG][editor]: 2025-01-28 17:32:00.725: node is rebuilding...: type: paragraph 
[DEBUG][input]: 2025-01-28 17:32:00.770: onReplace: TextEditingDeltaReplacement#573c9(oldText:  It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum., textReplaced:, replacementText:
  , replacedRange: TextRange(start: 208, end: 209), selection: TextSelection.collapsed(offset: 0, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
}]}}]}][editor]: 2025-01-28 17:32:00.772: apply op (local): {op: insert, path: [2], nodes: [{type: paragraph, data: {delta: [{insert:  It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
}]}, children: []) at path [2]}32:00.772: insert Node Node(id: If_YYY, type: paragraph, attributes: {delta: [{insert:  It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
}]}}UG][editor]: 2025-01-28 17:32:00.773: apply op (local): {op: update, path: [1], attributes: {delta: []}, oldAttributes: {delta: [{insert:  It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
[DEBUG][editor]: 2025-01-28 17:32:00.774: keyboard service - attach text input service: TextEditingValue(text: ┤ It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.├, selection: TextSelection.collapsed(offset: 0, affinity: TextAffinity.downstream, isDirectional: false),
composing: TextRange(start: -1, end: -1))
[DEBUG][input]: 2025-01-28 17:32:00.774: attach text editing value: TextEditingValue(text: ┤ It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.├, selection: TextSelection.collapsed(offset: 0, affinity: TextAffinity.downstream, isDirectional: false), composing:
TextRange(start: -1, end: -1))
[DEBUG][editor]: 2025-01-28 17:32:00.775: keyboard service - selection changed: start = path = [2], offset = 0, end = path = [2], offset = 0
[DEBUG][input]: 2025-01-28 17:32:00.775: keyboard service - handled by character shortcut event: CharacterShortcutEvent(key: insert a new line, character: 
, handler: Closure: (EditorState) => Future<bool> from: editorState => {
        let t$3687;
        let t$goto = 0, t$completer = async._makeAsyncAwaitCompleter(dart_rti._Universe.eval(dart_rti._theUniverse(), "core|bool", true)), t$returnValue, asyncScope = Object.create(null);
        var t$36asyncBody = async._wrapJsFunctionForAsync((t$errorCode, t$result) => {
          if (t$errorCode === 1) return async._asyncRethrow(t$result, t$completer);
          while (true)
            switch (t$goto) {
              case 0:
                // Function start
                if (platform_extension['PlatformExtension|isNotMobile'] && hardware_keyboard.HardwareKeyboard.instance.isShiftPressed) {
                  t$returnValue = false;
                  // goto return
                  t$goto = 2;
                  break;
                }
                asyncScope.selection = (t$3687 = editorState.selection, t$3687 == null ? null : t$3687.normalized);
                if (asyncScope.selection == null) {
                  t$returnValue = false;
                  // goto return
                  t$goto = 2;
                  break;
                }
                t$goto = 3;
                return async._asyncAwait(selection_commands['SelectionTransform|deleteSelection'](editorState, asyncScope.selection), t$36asyncBody, t$completer);
              case 3:
                // returning from await.
                t$goto = 4;
                return async._asyncAwait(text_commands['TextTransforms|insertNewLine'](editorState, {position: asyncScope.selection.start}), t$36asyncBody, t$completer);
              case 4:
                // returning from await.
                t$returnValue = true;
                // goto return
                t$goto = 2;
                break;
              case 2:
                // return
                return async._asyncReturn(t$returnValue, t$completer);
            }
        });
        return async._asyncStartSync(t$36asyncBody, t$completer);
      })
[DEBUG][input]: 2025-01-28 17:32:00.780: onReplace: TextEditingDeltaReplacement#bb5e1(oldText:  It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum., textReplaced:, replacementText:
  , replacedRange: TextRange(start: 208, end: 209), selection: TextSelection.collapsed(offset: 0, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
}]}}]}][editor]: 2025-01-28 17:32:00.783: apply op (local): {op: insert, path: [3], nodes: [{type: paragraph, data: {delta: [{insert:  It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
}]}, children: []) at path [3]}32:00.783: insert Node Node(id: UEKp-_, type: paragraph, attributes: {delta: [{insert:  It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
}]}}UG][editor]: 2025-01-28 17:32:00.783: apply op (local): {op: update, path: [2], attributes: {delta: []}, oldAttributes: {delta: [{insert:  It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
[DEBUG][editor]: 2025-01-28 17:32:00.784: keyboard service - attach text input service: TextEditingValue(text: ┤ It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.├, selection: TextSelection.collapsed(offset: 0, affinity: TextAffinity.downstream, isDirectional: false),
composing: TextRange(start: -1, end: -1))
[DEBUG][input]: 2025-01-28 17:32:00.784: attach text editing value: TextEditingValue(text: ┤ It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.├, selection: TextSelection.collapsed(offset: 0, affinity: TextAffinity.downstream, isDirectional: false), composing:
TextRange(start: -1, end: -1))
[DEBUG][editor]: 2025-01-28 17:32:00.784: keyboard service - selection changed: start = path = [3], offset = 0, end = path = [3], offset = 0
[DEBUG][input]: 2025-01-28 17:32:00.784: keyboard service - handled by character shortcut event: CharacterShortcutEvent(key: insert a new line, character: 
, handler: Closure: (EditorState) => Future<bool> from: editorState => {
        let t$3687;
        let t$goto = 0, t$completer = async._makeAsyncAwaitCompleter(dart_rti._Universe.eval(dart_rti._theUniverse(), "core|bool", true)), t$returnValue, asyncScope = Object.create(null);
        var t$36asyncBody = async._wrapJsFunctionForAsync((t$errorCode, t$result) => {
          if (t$errorCode === 1) return async._asyncRethrow(t$result, t$completer);
          while (true)
            switch (t$goto) {
              case 0:
                // Function start
                if (platform_extension['PlatformExtension|isNotMobile'] && hardware_keyboard.HardwareKeyboard.instance.isShiftPressed) {
                  t$returnValue = false;
                  // goto return
                  t$goto = 2;
                  break;
                }
                asyncScope.selection = (t$3687 = editorState.selection, t$3687 == null ? null : t$3687.normalized);
                if (asyncScope.selection == null) {
                  t$returnValue = false;
                  // goto return
                  t$goto = 2;
                  break;
                }
                t$goto = 3;
                return async._asyncAwait(selection_commands['SelectionTransform|deleteSelection'](editorState, asyncScope.selection), t$36asyncBody, t$completer);
              case 3:
                // returning from await.
                t$goto = 4;
                return async._asyncAwait(text_commands['TextTransforms|insertNewLine'](editorState, {position: asyncScope.selection.start}), t$36asyncBody, t$completer);
              case 4:
                // returning from await.
                t$returnValue = true;
                // goto return
                t$goto = 2;
                break;
              case 2:
                // return
                return async._asyncReturn(t$returnValue, t$completer);
            }
        });
        return async._asyncStartSync(t$36asyncBody, t$completer);
      })
[DEBUG][editor]: 2025-01-28 17:32:00.785: node is rebuilding...: type: page 
[DEBUG][editor]: 2025-01-28 17:32:00.791: node is rebuilding...: type: paragraph 
[DEBUG][editor]: 2025-01-28 17:32:00.797: node is rebuilding...: type: paragraph 
[DEBUG][editor]: 2025-01-28 17:32:00.801: node is rebuilding...: type: paragraph 
[DEBUG][editor]: 2025-01-28 17:32:00.807: node is rebuilding...: type: paragraph 
[DEBUG][editor]: 2025-01-28 17:32:00.987: Seal history item
[DEBUG][editor]: 2025-01-28 17:32:10.421: keyboard service - focus changed: false}

Expected Behavior

Without creating spaces and just work as it should.

Operating System

MacOS - Chrome Version 120.0.6099.216 (Official Build) (arm64)

AppFlowy Editor Version(s)

5.0.0

Screenshots

Shown in the bug description.

Additional Context

No response

MichalNemec avatar Jan 26 '25 01:01 MichalNemec

Any specific reason why '\r' is at the end of the text? I was able to reproduce the bug with the '\r' although when I removed it the bug did not happen.

rileyhawk1417 avatar Jan 31 '25 21:01 rileyhawk1417

@rileyhawk1417 oh, yeah this fixed the unwanted spaces. the \r had to be placed there by the editor, i checked other contents and we have \r present in a lot of them.

MichalNemec avatar Feb 01 '25 00:02 MichalNemec

Okay so with using the editor does it place '\r' when you are editing?

rileyhawk1417 avatar Feb 01 '25 20:02 rileyhawk1417

@rileyhawk1417 we have section of content, give or take 250 articles that are written by appflowy editor. We started at v 2.4.*, so im not sure how newest version does things, but yeah, it did the \r there. No one touched the json that the editor produces.

MichalNemec avatar Feb 02 '25 00:02 MichalNemec

I experience the same issue. In my case it happens, because some users are using the web version of my app on Windows and copy-pasting multi-line text. I assume this is due to Windows typically using CR+LF (\r\n) rather than just LF (\n) as on *nix.

As a work-around I currently listen on editorState.transactionStream with something like the below to remove the problematic characters. I also have something similar when loading documents, as some have already been saved with \r's in them.

  void _checkTransactions((TransactionTime, Transaction, ApplyOptions) event) {
    if (event.$1 == TransactionTime.before) {
      // Handle potential insertions of problematic characters (like Windows
      // line breaks). We hope there are no other operations/nodes to consider
      // then the ones addressed here.
      final transaction = event.$2;
      for (final operation in transaction.operations) {
        if (operation is InsertOperation) {
          for (final node in operation.nodes) {
            for (final listOfMaps in node.attributes.values) {
              if (listOfMaps is List<Map<String, dynamic>>) {
                _cleanupInserts(listOfMaps);
              }
            }
          }
        } else if (operation is UpdateOperation) {
          for (final listOfMaps in operation.attributes.values) {
            if (listOfMaps is List<Map<String, dynamic>>) {
              _cleanupInserts(listOfMaps);
            }
          }
        }
      }
    } else if (event.$1 == TransactionTime.after) {
      // Handle the case, where a user deletes all the text, so the user does
      // not end up in a situation, where she can't add any text.
      if (_editorState.document.first == null) {
        WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
          _editorState.document.insert([0], [paragraphNode()]);
          _editorState.selectionNotifier.value =
              Selection(start: Position(path: [0]), end: Position(path: [0]));
        });
      }
    }
  }

  void _cleanupInserts(List<Map<String, dynamic>> listOfMaps) {
    for (final map in listOfMaps) {
      if (map['insert'] is String) {
        String text = map['insert'] as String;
        text = _removeProblematicCharacters(text, true);
        map['insert'] = text;
      }
    }
  }

  String _removeProblematicCharacters(String text,
      [bool addSpace = false]) {
    text = text.replaceAll(_problematicCharacters, addSpace ? ' ' : '');
    return text;
  }

  static final RegExp _problematicCharacters = RegExp(r'\r|\\r');

Now that I'm looking at it again, I don't remember fully why I'm replacing with a space, but I think it had to do with deltas in transactions, where other operations would not align and work well if I ended up changing the number of characters.

mo-edumo avatar Apr 07 '25 15:04 mo-edumo