CentreForDigitalHumanities / CentreForDigitalHumanities/readit-interface

Restructure explorer controller using the Command pattern

Open
#346 6 comments 0 reactions 1 assignee Claimed by @jgonggrijp View on GitHub
bug code quality enhancement question
Dominant language
TypeScript
Stars
1
Forks
0
PR merge metrics
No merged PRs in 30d

Description

This is a follow-up on #344, which strives to remove some quirkyness from the way the browser's back button interacts with panel overlays.

When the user presses the "edit" button of a panel, the corresponding editing panel is overlaid and the route in the browser address bar is updated accordingly. The issue at hand regards what happens when the user presses the browser's "back" button, to go back to the route she visited just before she opened the editing panel.

As of #344, the following events happen in quick succession, leaving the overall impression to the user that pressing the "back" button had no effect:

1. The browser resets the previous route in the address bar and issues the `popstate` event.
2. The `popstate` event produces the `cid` of the panel that the reinstated route corresponds to. In most cases, this is the panel under the editing panel (I will stick with this assumption because the problem is most apparent in this way).
3. Through event bindings in the exploration aspect module, the explorer scrolls to *the position of* the panel with this `cid`.
4. The explorer tells the *topmost* panel at this position, i.e., the overlaid editing panel, to announce its route over the explorer radio channel.
5. Through other event bindings in the exploration aspect module, the route of the overlaid editing panel is set as the new route in the browser address bar with `pushState`.
6. The route that the user wanted to navigate back to has become the "previous" entry in history again. In other words, pressing the "back" button again will repeat the above five steps.

The actual result is slightly worse than no effect: if there was any navigation history in the "forward" direction, i.e., if the user visited additional routes after pressing the "edit" button, then that history will be lost due to step 5. The user can however still access all "future" panels by scrolling.

There are some non-obvious ways to escape this cycle: pressing the back button twice in quick succession, or press-holding the back button and choosing an earlier history entry. There is however no way to navigate exactly to the panel under the overlaid panel (except for pressing the "close" button on the overlaid panel, which pushes a new state to the history).

Steps 3 and 4 suggest simple solutions, but I will argue that they are *too* simple, giving an overall result that is worse than the present situation:

- In step 3, the problem could be avoided by removing any overlays that cover the panel to which the `cid` belongs. Alas, this creates a corrupt state were the "forward" history is preserved, but the editing panel is not. If the user presses the "forward" button after this, the entire explorer will be cleared in order to accomodate recreating the editing panel.
- In step 4, the problem could be avoided by keeping track of the specific panel that prompted the scroll (rather than just the position of its stack) and telling that panel to announce its route, even if it is not topmost. This however introduces the possibility that the route does not reflect the visible panel, which is probably more confusing than a back button that doesn't work.

Instead, I'm suggesting a more radical solution based on the Command pattern. This introduces a new data structure, the *command history*, which is a queue of commands. Each command represents a reversible user-initiated manipulation of the explorer: push, pop, add/remove overlay, reset, scroll. Every command can be executed, undone and re-executed repeatedly, provided that all commands before it have been executed in order and that all commands after it have been undone in reverse order. This gives infinite undo/redo capability, like in text editors and image processors.

The command history and the explorer view are duals: the former is diachronic and synchoric while the latter is synchronic and diachoric. In other words, the command history gives the full story through time but only a snapshot in space, while the explorer view gives only a snapshot in time but the full story in space.

When the user visits a new route, instead of pushing the `cid` of the corresponding panel to the browser history, we push the most recently executed command. When the user navigates *backward* to this route, we *undo* all commands that *followed* it. When the user navigates *forward* to this route, we *redo* all commands that *preceded* it, as well as the command itself. Essentially, traversing the browser history with the back and forward buttons truly becomes a travel in time, instead of a travel in space.

The overlaid panel problem is solved automatically; navigating back removes the overlay. Contrary to the naieve solution I mentioned above, this is nondestructive; navigating forward will put the overlay back again. Scrolling to the right position in the explorer also happens automatically, as a side effect of command (un-/re-)execution.

Commands can be composed into larger commands, macros. Under the above proposal, the ExplorerEventController would become a factory of macros. The exploration aspect module would mediate between the explorer, the command history and the macro factory.

The reset and scroll commands take some special care in order to make them reversible. The reverse of a reset conceptually replays all of the preceding commands; rather than doing this literally, history playback should take smart shortcuts. The scroll command must store the starting position in addition to the ending position.

Online sources on the command pattern (repeated from #344):

- https://w3sdesign.com/?gr=b02&ugr=proble#gf
- https://refactoring.guru/design-patterns/command

@BeritJanssen @JeltevanBoheemen Please let me know what you think.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.