diff --git a/src/domobserver.ts b/src/domobserver.ts index 3c1adc22..412faa8f 100644 --- a/src/domobserver.ts +++ b/src/domobserver.ts @@ -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 = { @@ -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 } @@ -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 } @@ -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) @@ -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) { @@ -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) } } diff --git a/src/index.ts b/src/index.ts index 4bc8f94c..9c9a37e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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() diff --git a/src/input.ts b/src/input.ts index 13c8494c..18b4684e 100644 --- a/src/input.ts +++ b/src/input.ts @@ -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 = {touchstart: true, touchmove: true} export class InputState { shiftKey = false @@ -25,6 +26,8 @@ export class InputState { lastSelectionTime = 0 lastIOSEnter = 0 lastIOSEnterFallbackTimeout = -1 + lastFocus = 0 + lastTouch = 0 lastAndroidDelete = 0 composing = false composingTimeout = -1 @@ -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 @@ -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) { @@ -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")