Remember scroll position per person in side-by-side group study viewer (108 LOC)#103
Closed
hendriebeats wants to merge 16 commits into
Closed
Remember scroll position per person in side-by-side group study viewer (108 LOC)#103hendriebeats wants to merge 16 commits into
hendriebeats wants to merge 16 commits into
Conversation
92d9ad1 to
dc86b0d
Compare
…table cells ProseMirror's default up/down navigation follows document order, causing the cursor to jump to the wrong column in the two-column study block layout. The plugin intercepts arrow keys at cell boundaries and navigates visually, preserving horizontal cursor position via coordinate probing. Also fixes a boundary-position bug where posAtCoords could return a position between block siblings, causing TextSelection to throw. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Persists each viewed document's scroll position in localStorage so switching between group members' documents restores where you left off. Also fixes a bug where StopListenToDoc was sent with the new doc ID instead of the old one. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dc86b0d to
0c2fe6d
Compare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- currentDocId typed as T.DocId | undefined for clarity - editor?.dispatchSteps() null guard against early DocUpdated events - navigateToTarget returns true (consume key) when position unchanged - Remove unnecessary TextSelection cast in navigateOutOfStudyBlocks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix boundary comparisons in resolveLinePos to use >= / <= so cursor positions exactly at block edges are not excluded - Pass navigation direction directly to resolveLinePos instead of hardcoding ±1 so up/down arrows behave symmetrically - Return false (not true) when navigation produces no cursor movement, allowing the event to fall through to the browser - Fix coordsAtPos crash when section header starts on a block boundary by snapping to the first inline position before querying coords - Guard scroll restore against a null currentDocId - Tighten parseInt fallback: treat non-numeric stored values as NaN rather than silently scrolling to 0 - Remove stale console.log - Drop unused EditorStateConfig and Selection imports from Editor.tsx
Use nullish coalescing instead of a ternary when parsing the saved scroll value, keeping the same NaN-fallback behaviour.
If the select element has no selected option, groupStudySelector.value returns an empty string. Without this guard, an invalid doc ID would be sent to the server via ListenToDoc on the first loadEditor call.
To enable a multi-tab side panel, the WebSocket layer needed to track
multiple side documents per connection instead of a single optional one.
- Add DocUpdatedMsg wrapping docId + update so the frontend can route
updates to the correct tab editor
- Replace SocketState.sideDoc (Maybe DocId) with sideDocs ([DocId])
- Handle InStopListenToDoc properly via closeSideDoc (previously fell
through to the catch-all logger)
- Disconnect cleanup now iterates all sideDocs and unsubscribes each
- Frontend DocUpdatedEvent.contents now carries { docId, update }
instead of a bare update value
- Embed groupStudy JSON and pageDocId into study.html via <script> tags
for XSS-safe access from TypeScript
The old side panel only supported one member's document at a time via a dropdown selector. This lays the HTML/CSS foundation for the new multi-tab experience. HTML (study.html): - Replace #splitPicker / p-select / #sideBySideEditor with a tab bar (#tabBar), a "+" add button, a close button, and an empty #splitEditorArea where editors are injected by JS - Move #memberPickerPopover outside #splitside so position:fixed works - Remove the inline split() function (wired via JS in GroupStudy.ts) CSS (study.css): - Rewrite #splitside as a flex column with a sticky header strip and absolutely-positioned tab editor panes (.splitTabEditor) - Add .splitTab styles (active state, close button, label ellipsis) - Add #memberPickerPopover with fixed positioning, z-index 200 - Add mobile override: at ≤768px the panel becomes a fixed full-screen overlay (z-index 300) instead of a grid column
Replaces the single-dropdown side panel with a Chrome-style tab system
where users can open any group member's document in a parallel tab.
Key behaviours:
- Toolbar button opens a member picker popover (position:fixed,
anchored to the button) listing all group members
- Each row shows a checkmark if already open, greyed if no doc yet
- openTab() creates a .splitTabEditor pane and a .splitTab button,
sends ListenToDoc, and activates the tab immediately
- Editors are created lazily on DocListenStart via a FIFO listenQueue
so concurrent opens don't cross-wire responses
- closeTab() sends StopListenToDoc, destroys the editor, and activates
an adjacent tab; closing the last tab collapses the panel
- closeSidePanel() unsubscribes all tabs in one pass
- Scroll position per tab persisted to localStorage under
split-scroll:{pageDocId}:{docId}; saved on tab switch, restored on
activate and after editor mounts
- 4-tier name disambiguation avoids duplicate tab labels for members
sharing a first name
- WS reconnect re-queues ListenToDoc for all currently open tabs
- groupStudy data read from #groupStudyData <script> tag (XSS-safe);
pageDocId from window.pageDocId injected by the template
- Use unsafeRawHtml to embed pre-encoded JSON in the groupStudyData script tag. The Ginger | safe filter is broken in 0.10.5.2 (its asText path returns empty for Text GVals), so we bypass it by setting asHtml directly. </script> sequences are replaced with <\/script> to prevent HTML-parser breakout from the script tag. - Remove the current user from the "No document yet" section of the member picker — their doc is intentionally excluded from allDocs, so they were incorrectly appearing as having no document.
- Replace empty checkmark column with avatar circles (two initials, matching the profile button style at 28×28px) - Show a "Viewing" pill on rows whose tab is already open - Toolbar splitscreen picker stays open for multi-select: click to open or close individual tabs without dismissing the dropdown - Tab-bar plus button filters out already-open members and closes after a single selection (unchanged single-select behaviour) - Fix dismiss handler ignoring clicks on DOM nodes removed during re-render (prevented spurious picker close on row click)
Tabs can now be reordered by dragging within the tab bar. The implementation uses pointer capture for reliable tracking and the FLIP animation technique to smoothly animate displaced siblings into their new positions. A .dragging class lifts the dragged tab with a shadow; dropping snaps it back with a short ease transition. Also fixes the splitscreen button to not pre-select the current user in the member picker (was passing `true` for self-exclusion). CSS: darken tab bar and inactive tab backgrounds slightly for better contrast; tighten the add-tab button to a fixed 34×34 square. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR does two things:
frontend/src/GroupStudy.ts— scroll position memorymakeScrollMemoryA small closure that wraps
localStoragereads/writes for scroll position, scoped to a specific container element. Keys are namespaced assplit-scroll:{pageDocId}:{docId}so positions don't collide across different study pages.save(docId)— writescontainer.scrollToptolocalStorage.restore(docId)— reads the saved value and applies it inside arequestAnimationFrame, which is necessary because the editor isn't fully painted yet whenDocListenStartfires.watch(getCurrentDocId)— attaches a debounced scroll listener (200 ms) so saves don't hammerlocalStorageon every pixel of scroll.loadEditorStopListenToDocfor the old document and saves its scroll position before switching.if (!docId) returnto avoid sending an empty doc ID if the select has no selected option.ListenToDocfor the newly selected document.DocListenStarthandlerCalls
scroll?.restore(currentDocId)after mounting the editor so the panel jumps to the remembered position.backend/lib/Api/Htmx/GroupStudy.hs— group study modal refactorcreateStudyGroupHTML×) icon button and a descriptive paragraph explaining what a group study is.field-label/field-descclasses. The people label was renamed to "Invite people (optional)" with a note that invites can be sent later.shareHTMLred trash) to a text "Remove" button (remove-btn) for clarity.person-row/person-info/person-actionsgrid classes.memberHTML(you)indicator is now a styledyou-labelspan.groupStudyHTMLfield-label, with a read-only fallback for non-owners.#groupStudyInnerwith the updated panel — no separate page needed.postInvitehtml =<< getInviteNewMemberHTMLresponse (that rendered the now-deleted separate invite page). The endpoint now ends withgetGroupStudy' user groupId, returning the full refreshed panel, which htmx swaps into#groupStudyInner.backend/lib/Entity/User.hsAdded
emailto theGetUserprojection so member rows can display email addresses in the group study modal.backend/static/scripts/studyGroup.jsAfter htmx settles inside the open modal, focuses the email input in the inline invite form and attaches an Enter-key handler (guarded with
_inviteHandlerAttachedto avoid duplicate listeners across htmx swaps).backend/static/styles/component/groupstudy.cssFull visual refresh of the modal to match the redesigned HTML:
person-rowgrid,person-info,person-actions,person-name,person-email,you-label,person-role— shared layout primitives for both member and invite rows.badge/badge-pending/badge-error— pill badges for invite status.ghost-blue/remove-btn— low-visual-weight action buttons.groupStudy-invite-row— three-column grid (email | role | button) for the inline invite form.p-selectpart styles scoped to the modal.frontend/src/Editor/StudyBlockArrowPlugin.ts+Editor.tsxArrow-key navigation plugin that fixes cursor movement at study block cell boundaries in the two-column table layout. ProseMirror's default navigation follows document order, which causes the cursor to jump to the wrong column. This plugin intercepts
ArrowUp/ArrowDownat cell edges and uses coordinate probing (coordsAtPos/posAtCoords) to land the cursor on the correct visual line in the adjacent cell.Also fixed
verseRefWidgetto useonmousedown+getPos()instead of a stale closure overposition, and removed the erroneouscontentEditable = "true"from verse ref spans.Minor fixes
study.html: fixed typo "grad" → "drag".SidebarSections.ts: removed two lines of commented-out dead code.StudyBlockEditor.ts: removed an unusedcurrentIndexvariable read inside the dragmouseuphandler.