Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserve active element's focus and selection by morphing around it, when possible #85

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

botandrose
Copy link

@botandrose botandrose commented Dec 28, 2024

Note: Builds on top of #84 , so please review just the diff of the last commit: 9aed82c . If #84 gets merged, I'll rebase for a easier diff.

Overview

The new two-pass algorithm is a big improvement maintaining hidden state and element identity, but since more nodes are getting moved around vs v0.3.0, its more likely that the currently focused element (or its ancestor) will get moved, and focus and/or selection will be lost. Aside from being a regression in fidelity, this has frustrating real-world consequences, at least in the context of a real-time collaborative app.

This PR aims to preserve focus and selection across morphs by morphing around the active element and its ancestors, thus ensuring they never get moved.

Implementation

This strategy has two steps:

  1. When we build the morph context, we take the active element, find its match in the new content, and trace them both back to their respective top-most element that was passed to Idiomorph.morph(old, new). If both traced paths are have the same sequence of tagName#id, preserving the focused element is possible, and we save a map of this information in the context.
  2. Later on, while morphing, we check the map to see if we're within the critical ancestor path of the active element, and if we're about to morph the next node in that critical path. If we are, and the node is too far to the right, it would normally be moved into place, breaking focus. So if all this is true, we prime the situation by "moving" the active node into place, by repeatedly moving the current node to the end of the list until the active node is in the correct insertionPoint position. Once this is done, we can just continue morphing as normal. In this way, the active element and its ancestors are never moved, and focus is preserved.

Notes / Caveats / Limitations:

When we have moveBefore, none of this is necessary, so we check for its existence while building the morph context, and if so none of this gets run. However, when we don't have moveBefore, there are three situations where this PR doesn't preserve focus. All of them might become the subject of future improvements, but they're out of scope for this PR.

  1. If the active element changes levels in the DOM heirarchy, or if any element along the crticial path changes their tag name. This is simply impossible with current APIs. We could look into manually restoring focus post-morph in these cases, but again, that's out-of-scope for this PR.
  2. A element along the critical path changes ids. Technically we could mutate the old id to the new id in this case and preserve focus, but that breaks our rule of maintaining identity for IDed elements. This might be reasonable to special-case, in that maybe there's no circumstance where a parent node of a focused element could have hidden state, and thus it would be safe to perform this mutation, but I haven't thought it through yet.
  3. The active element is anonymous, i.e. has no id attribute. This may or may not be solvable in a deterministic way. We might want to fall back to just trying to avoid moving the current element or its ancestors, but I haven't thought it through yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants