Skip to content

Commit

Permalink
Restore selection from the state when the browser resets it on focus
Browse files Browse the repository at this point in the history
FIX: Make sure that when the editor receives focus via tab or calling
`.focus()` on its DOM element, the existing selection is restored.

Issue #137
  • Loading branch information
marijnh committed Aug 12, 2022
1 parent 978011f commit 81e61eb
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 26 deletions.
41 changes: 27 additions & 14 deletions src/domobserver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Selection} from "prosemirror-state"
import * as browser from "./browser"
import {domIndex, isEquivalentPosition} from "./dom"
import {hasFocusAndSelection, selectionToDOM} from "./selection"
import {domIndex, isEquivalentPosition, selectionCollapsed, DOMSelection} from "./dom"
import {hasFocusAndSelection, selectionToDOM, selectionFromDOM} from "./selection"
import {EditorView} from "./index"

const observeOptions = {
Expand All @@ -20,7 +21,7 @@ class SelectionState {
focusNode: Node | null = null
focusOffset: number = 0

set(sel: Selection) {
set(sel: DOMSelection) {
this.anchorNode = sel.anchorNode; this.anchorOffset = sel.anchorOffset
this.focusNode = sel.focusNode; this.focusOffset = sel.focusOffset
}
Expand All @@ -29,7 +30,7 @@ class SelectionState {
this.anchorNode = this.focusNode = null
}

eq(sel: Selection) {
eq(sel: DOMSelection) {
return sel.anchorNode == this.anchorNode && sel.anchorOffset == this.anchorOffset &&
sel.focusNode == this.focusNode && sel.focusOffset == this.focusOffset
}
Expand Down Expand Up @@ -138,7 +139,7 @@ export class DOMObserver {
this.currentSelection.set(this.view.domSelection())
}

ignoreSelectionChange(sel: Selection) {
ignoreSelectionChange(sel: DOMSelection) {
if (sel.rangeCount == 0) return true
let container = sel.getRangeAt(0).commonAncestorContainer
let desc = this.view.docView.nearestDesc(container)
Expand All @@ -152,18 +153,19 @@ export class DOMObserver {
}

flush() {
if (!this.view.docView || this.flushingSoon > -1) return
let {view} = this
if (!view.docView || this.flushingSoon > -1) return
let mutations = this.observer ? this.observer.takeRecords() : []
if (this.queue.length) {
mutations = this.queue.concat(mutations)
this.queue.length = 0
}

let sel = this.view.domSelection()
let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(this.view) && !this.ignoreSelectionChange(sel)
let sel = view.domSelection()
let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel)

let from = -1, to = -1, typeOver = false, added: Node[] = []
if (this.view.editable) {
if (view.editable) {
for (let i = 0; i < mutations.length; i++) {
let result = this.registerMutation(mutations[i], added)
if (result) {
Expand All @@ -183,14 +185,25 @@ export class DOMObserver {
}
}

if (from > -1 || newSel) {
let readSel: Selection | null = null
// If it looks like the browser has reset the selection to the
// start of the document after focus, restore the selection from
// the state
if (from < 0 && newSel && view.input.lastFocus > Date.now() - 200 &&
view.input.lastTouch < Date.now() - 300 &&
selectionCollapsed(sel) && (readSel = selectionFromDOM(view)) &&
readSel.eq(Selection.near(view.state.doc.resolve(0), 1))) {
selectionToDOM(view)
this.currentSelection.set(sel)
view.scrollToSelection()
} else if (from > -1 || newSel) {
if (from > -1) {
this.view.docView.markDirty(from, to)
checkCSS(this.view)
view.docView.markDirty(from, to)
checkCSS(view)
}
this.handleDOMChange(from, to, typeOver, added)
if (this.view.docView && this.view.docView.dirty) this.view.updateState(this.view.state)
else if (!this.currentSelection.eq(sel)) selectionToDOM(this.view)
if (view.docView && view.docView.dirty) view.updateState(view.state)
else if (!this.currentSelection.eq(sel)) selectionToDOM(view)
this.currentSelection.set(sel)
}
}
Expand Down
23 changes: 14 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,20 +214,25 @@ export class EditorView {
if (scroll == "reset") {
this.dom.scrollTop = 0
} else if (scroll == "to selection") {
let startDOM = this.domSelection().focusNode!
if (this.someProp("handleScrollToSelection", f => f(this))) {
// Handled
} else if (state.selection instanceof NodeSelection) {
let target = this.docView.domAfterPos(state.selection.from)
if (target.nodeType == 1) scrollRectIntoView(this, (target as HTMLElement).getBoundingClientRect(), startDOM)
} else {
scrollRectIntoView(this, this.coordsAtPos(state.selection.head, 1), startDOM)
}
this.scrollToSelection()
} else if (oldScrollPos) {
resetScrollPos(oldScrollPos)
}
}

/// @internal
scrollToSelection() {
let startDOM = this.domSelection().focusNode!
if (this.someProp("handleScrollToSelection", f => f(this))) {
// Handled
} else if (this.state.selection instanceof NodeSelection) {
let target = this.docView.domAfterPos(this.state.selection.from)
if (target.nodeType == 1) scrollRectIntoView(this, (target as HTMLElement).getBoundingClientRect(), startDOM)
} else {
scrollRectIntoView(this, this.coordsAtPos(this.state.selection.head, 1), startDOM)
}
}

private destroyPluginViews() {
let view
while (view = this.pluginViews.pop()) if (view.destroy) view.destroy()
Expand Down
16 changes: 13 additions & 3 deletions src/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {ViewDesc} from "./viewdesc"
// A collection of DOM events that occur within the editor, and callback functions
// to invoke when the event fires.
const handlers: {[event: string]: (view: EditorView, event: Event) => void} = {}
let editHandlers: {[event: string]: (view: EditorView, event: Event) => void} = {}
const editHandlers: {[event: string]: (view: EditorView, event: Event) => void} = {}
const passiveHandlers: Record<string, boolean> = {touchstart: true, touchmove: true}

export class InputState {
shiftKey = false
Expand All @@ -25,6 +26,8 @@ export class InputState {
lastSelectionTime = 0
lastIOSEnter = 0
lastIOSEnterFallbackTimeout = -1
lastFocus = 0
lastTouch = 0
lastAndroidDelete = 0
composing = false
composingTimeout = -1
Expand All @@ -42,7 +45,7 @@ export function initInput(view: EditorView) {
if (eventBelongsToView(view, event) && !runCustomHandler(view, event) &&
(view.editable || !(event.type in editHandlers)))
handler(view, event)
})
}, passiveHandlers[event] ? {passive: true} : undefined)
}
// On Safari, for reasons beyond my understanding, adding an input
// event handler makes an issue where the composition vanishes when
Expand Down Expand Up @@ -403,11 +406,17 @@ class MouseDown {
}
}

handlers.touchdown = view => {
handlers.touchstart = view => {
view.input.lastTouch = Date.now()
forceDOMFlush(view)
setSelectionOrigin(view, "pointer")
}

handlers.touchmove = view => {
view.input.lastTouch = Date.now()
setSelectionOrigin(view, "pointer")
}

handlers.contextmenu = view => forceDOMFlush(view)

function inOrNearComposition(view: EditorView, event: Event) {
Expand Down Expand Up @@ -694,6 +703,7 @@ editHandlers.drop = (view, _event) => {
}

handlers.focus = view => {
view.input.lastFocus = Date.now()
if (!view.focused) {
view.domObserver.stop()
view.dom.classList.add("ProseMirror-focused")
Expand Down

0 comments on commit 81e61eb

Please sign in to comment.