superlistapp / super_editor

A Flutter toolkit for building document editors and readers
https://superlist.com/SuperEditor/
MIT License
1.67k stars 245 forks source link

[SuperEditor] Feat: Add ability to add `EditRules` depending on `TextEdigingDelta` #994

Open raulmabe-labhouse opened 1 year ago

raulmabe-labhouse commented 1 year ago

Description of the feature

For our company it would be necessary that SuperEditor allows us to customise the experience of the editor.

For example, if the user presses enter (on its software OR hardware keyboard) after a header, the editor will add a ListItemNode afterwards automatically.

Another example would be to split a TaskNode when the user presses enter while the selection is inside a TaskNode. As far as I know this is only available through keyboardActions right now, which are only valid for Hardware Keyboards.

Proposed solution

For this purpose Flutter Quill uses "rules". These rules are executed as a chain of responsibility and each one of these rules have access to the delta text.

Once #870 is merged, I propose to add a new concept called EditorRule, which would look like this:

abstract class EditorRule {
  ExecutionInstruction apply(
    DocumentEditor editor,
    DocumentComposer composer,
    TextEditingDelta delta,
  );
}

class EditorRules {
  const EditorRules({
    this.rules = const [],
  });
  final List<EditorRule> rules;

  ExecutionInstruction apply(DocumentEditor editor, DocumentComposer composer, TextEditingDelta delta) {
    for (final rule in rules) {
      final execution = rule.apply(editor, composer, delta);
      if (execution == ExecutionInstruction.blocked || execution == ExecutionInstruction.haltExecution) {
        return execution;
      }
    }

    return ExecutionInstruction.continueExecution;
  }
}

Then, the DocumentEditor would accept these rules through its constructor, and delegate the execution of these rules to a SoftwareKeyboardHandler or a HardwareKeyboardHandler.

In my case I just added these two lines

      final execution = editor.rules.apply(editor, composer, delta);
      if (execution != ExecutionInstruction.continueExecution) continue;

on the applyDeltas method:

 void applyDeltas(List<TextEditingDelta> textEditingDeltas) {
    editorImeLog.info("Applying ${textEditingDeltas.length} IME deltas to document");

    for (final delta in textEditingDeltas) {
      editorImeLog.info("Applying delta: $delta");

      final execution = editor.rules.apply(editor, composer, delta);
      if (execution != ExecutionInstruction.continueExecution) continue;

      if (delta is TextEditingDeltaInsertion) {
        _applyInsertion(delta);
      } else if (delta is TextEditingDeltaReplacement) {
        _applyReplacement(delta);
      } else if (delta is TextEditingDeltaDeletion) {
        _applyDeletion(delta);
      } else if (delta is TextEditingDeltaNonTextUpdate) {
        _applyNonTextChange(delta);
      } else {
        editorImeLog.shout("Unknown IME delta type: ${delta.runtimeType}");
      }
    }
  }

Usage

Now we are able to add the SplitTaskRequest whenever we detect the user has pressed enter while the selection is in a TaskNode:

class SplitTaskRule implements EditorRule {
  @override
  ExecutionInstruction apply(DocumentEditor editor, DocumentComposer composer, TextEditingDelta delta) {
    if (delta is! TextEditingDeltaInsertion || delta.textInserted != '\n') return ExecutionInstruction.continueExecution;

    final selection = composer.selectionComponent.selection;
    if (selection == null || !selection.isCollapsed) return ExecutionInstruction.continueExecution;

    final nodeId = selection.base.nodeId;
    final node = editor.document.getNodeById(nodeId);
    if (node is! TaskNode) return ExecutionInstruction.continueExecution;

    if (node.text.text.isEmpty) {
      editor.execute(NodeToParagraphRequest(nodeId: nodeId));
    } else {
      editor.execute(
        SplitTaskRequest(
          nodeId: nodeId,
          splitPosition: selection.extent.nodePosition as TextNodePosition,
          newNodeId: DocumentEditor.createNodeId(),
        ),
      );
    }
    return ExecutionInstruction.haltExecution;
  }
}

Opinions

matthew-carroll commented 1 year ago

@raulmabe-labhouse can you look at the current state of Super Editor on GitHub and let me know if you're able to accomplish these goals? We've introduced an edit pipeline, which has requests, commands, and reactions. You still don't have direct access to the IME behaviors, but I think you might have enough access to accomplish the goals you listed in this ticket.

raulmabe-labhouse commented 1 year ago

You are right, with the current edit pipeline our features can be accomplished through some custom commands and reactions.

However, we still have to adapt our code to undo some default logic from Super Editor (i.e. pressing enter on a paragraph node) because this default logic sits at the start of the pipeline and developers can not opt out for this logic.

I guess that my main pro with my approach with EditRules is that they sit before this default logic, allowing us (developers) to opt out for this default logic Super Editor has.

matthew-carroll commented 1 year ago

Can you please provide two code examples: First, the current workaround that you have to do with Super Editor that you'd prefer not to do. Second, a hypothetical example demonstrating your ideal setup?

raulmabe-labhouse commented 1 year ago

Sounds good, I will as soon as I have some spare time 👍🏼