prosemirror-view 1.27.0 → 1.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/decoration.ts CHANGED
@@ -550,20 +550,24 @@ function mapChildren(
550
550
 
551
551
  // Mark the children that are directly touched by changes, and
552
552
  // move those that are after the changes.
553
- let shift = (oldStart: number, oldEnd: number, newStart: number, newEnd: number) => {
554
- for (let i = 0; i < children.length; i += 3) {
555
- let end = children[i + 1] as number, dSize
556
- if (end < 0 || oldStart > end + oldOffset) continue
557
- let start = (children[i] as number) + oldOffset
558
- if (oldEnd >= start) {
559
- children[i + 1] = oldStart <= start ? -2 : -1
560
- } else if (newStart >= offset && (dSize = (newEnd - newStart) - (oldEnd - oldStart))) {
561
- ;(children[i] as number) += dSize
562
- ;(children[i + 1] as number) += dSize
553
+ for (let i = 0; i < mapping.maps.length; i++) {
554
+ let moved = 0
555
+ mapping.maps[i].forEach((oldStart: number, oldEnd: number, newStart: number, newEnd: number) => {
556
+ let dSize = (newEnd - newStart) - (oldEnd - oldStart)
557
+ for (let i = 0; i < children.length; i += 3) {
558
+ let end = children[i + 1] as number
559
+ if (end < 0 || oldStart > end + oldOffset - moved) continue
560
+ let start = (children[i] as number) + oldOffset - moved
561
+ if (oldEnd >= start) {
562
+ children[i + 1] = oldStart <= start ? -2 : -1
563
+ } else if (newStart >= offset && dSize) {
564
+ ;(children[i] as number) += dSize
565
+ ;(children[i + 1] as number) += dSize
566
+ }
563
567
  }
564
- }
568
+ moved += dSize
569
+ })
565
570
  }
566
- for (let i = 0; i < mapping.maps.length; i++) mapping.maps[i].forEach(shift)
567
571
 
568
572
  // Find the child nodes that still correspond to a single node,
569
573
  // recursively call mapInner on them and update their positions.
package/src/domcoords.ts CHANGED
@@ -341,7 +341,7 @@ export function coordsAtPos(view: EditorView, pos: number, side: number): Rect {
341
341
  else if (side >= 0 && offset == node.nodeValue!.length) { from--; takeSide = 1 }
342
342
  else if (side < 0) { from-- }
343
343
  else { to ++ }
344
- return flattenV(singleRect(textRange(node as Text, from, to), takeSide), takeSide < 0)
344
+ return flattenV(singleRect(textRange(node as Text, from, to), 1), takeSide < 0)
345
345
  }
346
346
  }
347
347
 
@@ -1,6 +1,7 @@
1
+ import {Selection} from "prosemirror-state"
1
2
  import * as browser from "./browser"
2
- import {domIndex, isEquivalentPosition} from "./dom"
3
- import {hasFocusAndSelection, selectionToDOM} from "./selection"
3
+ import {domIndex, isEquivalentPosition, selectionCollapsed, DOMSelection} from "./dom"
4
+ import {hasFocusAndSelection, selectionToDOM, selectionFromDOM} from "./selection"
4
5
  import {EditorView} from "./index"
5
6
 
6
7
  const observeOptions = {
@@ -20,7 +21,7 @@ class SelectionState {
20
21
  focusNode: Node | null = null
21
22
  focusOffset: number = 0
22
23
 
23
- set(sel: Selection) {
24
+ set(sel: DOMSelection) {
24
25
  this.anchorNode = sel.anchorNode; this.anchorOffset = sel.anchorOffset
25
26
  this.focusNode = sel.focusNode; this.focusOffset = sel.focusOffset
26
27
  }
@@ -29,7 +30,7 @@ class SelectionState {
29
30
  this.anchorNode = this.focusNode = null
30
31
  }
31
32
 
32
- eq(sel: Selection) {
33
+ eq(sel: DOMSelection) {
33
34
  return sel.anchorNode == this.anchorNode && sel.anchorOffset == this.anchorOffset &&
34
35
  sel.focusNode == this.focusNode && sel.focusOffset == this.focusOffset
35
36
  }
@@ -138,7 +139,7 @@ export class DOMObserver {
138
139
  this.currentSelection.set(this.view.domSelection())
139
140
  }
140
141
 
141
- ignoreSelectionChange(sel: Selection) {
142
+ ignoreSelectionChange(sel: DOMSelection) {
142
143
  if (sel.rangeCount == 0) return true
143
144
  let container = sel.getRangeAt(0).commonAncestorContainer
144
145
  let desc = this.view.docView.nearestDesc(container)
@@ -152,18 +153,19 @@ export class DOMObserver {
152
153
  }
153
154
 
154
155
  flush() {
155
- if (!this.view.docView || this.flushingSoon > -1) return
156
+ let {view} = this
157
+ if (!view.docView || this.flushingSoon > -1) return
156
158
  let mutations = this.observer ? this.observer.takeRecords() : []
157
159
  if (this.queue.length) {
158
160
  mutations = this.queue.concat(mutations)
159
161
  this.queue.length = 0
160
162
  }
161
163
 
162
- let sel = this.view.domSelection()
163
- let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(this.view) && !this.ignoreSelectionChange(sel)
164
+ let sel = view.domSelection()
165
+ let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel)
164
166
 
165
167
  let from = -1, to = -1, typeOver = false, added: Node[] = []
166
- if (this.view.editable) {
168
+ if (view.editable) {
167
169
  for (let i = 0; i < mutations.length; i++) {
168
170
  let result = this.registerMutation(mutations[i], added)
169
171
  if (result) {
@@ -183,14 +185,26 @@ export class DOMObserver {
183
185
  }
184
186
  }
185
187
 
186
- if (from > -1 || newSel) {
188
+ let readSel: Selection | null = null
189
+ // If it looks like the browser has reset the selection to the
190
+ // start of the document after focus, restore the selection from
191
+ // the state
192
+ if (from < 0 && newSel && view.input.lastFocus > Date.now() - 200 &&
193
+ view.input.lastTouch < Date.now() - 300 &&
194
+ selectionCollapsed(sel) && (readSel = selectionFromDOM(view)) &&
195
+ readSel.eq(Selection.near(view.state.doc.resolve(0), 1))) {
196
+ view.input.lastFocus = 0
197
+ selectionToDOM(view)
198
+ this.currentSelection.set(sel)
199
+ view.scrollToSelection()
200
+ } else if (from > -1 || newSel) {
187
201
  if (from > -1) {
188
- this.view.docView.markDirty(from, to)
189
- checkCSS(this.view)
202
+ view.docView.markDirty(from, to)
203
+ checkCSS(view)
190
204
  }
191
205
  this.handleDOMChange(from, to, typeOver, added)
192
- if (this.view.docView && this.view.docView.dirty) this.view.updateState(this.view.state)
193
- else if (!this.currentSelection.eq(sel)) selectionToDOM(this.view)
206
+ if (view.docView && view.docView.dirty) view.updateState(view.state)
207
+ else if (!this.currentSelection.eq(sel)) selectionToDOM(view)
194
208
  this.currentSelection.set(sel)
195
209
  }
196
210
  }
@@ -243,11 +257,16 @@ export class DOMObserver {
243
257
  }
244
258
  }
245
259
 
246
- let cssChecked = false
260
+ let cssChecked: WeakMap<EditorView, null> = new WeakMap()
261
+ let cssCheckWarned: boolean = false
247
262
 
248
263
  function checkCSS(view: EditorView) {
249
- if (cssChecked) return
250
- cssChecked = true
251
- if (getComputedStyle(view.dom).whiteSpace == "normal")
264
+ if (cssChecked.has(view)) return
265
+ cssChecked.set(view, null)
266
+ if (['normal', 'nowrap', 'pre-line'].indexOf(getComputedStyle(view.dom).whiteSpace) !== -1) {
267
+ view.requiresGeckoHackNode = browser.gecko
268
+ if (cssCheckWarned) return
252
269
  console["warn"]("ProseMirror expects the CSS white-space property to be set, preferably to 'pre-wrap'. It is recommended to load style/prosemirror.css from the prosemirror-view package.")
270
+ cssCheckWarned = true
271
+ }
253
272
  }
package/src/index.ts CHANGED
@@ -53,6 +53,10 @@ export class EditorView {
53
53
  private pluginViews: PluginView[] = []
54
54
  /// @internal
55
55
  domObserver!: DOMObserver
56
+ /// Holds `true` when a hack node is needed in Firefox to prevent the
57
+ /// [space is eaten issue](https://github.com/ProseMirror/prosemirror/issues/651)
58
+ /// @internal
59
+ requiresGeckoHackNode: boolean = false
56
60
 
57
61
  /// The view's current [state](#state.EditorState).
58
62
  public state: EditorState
@@ -214,20 +218,25 @@ export class EditorView {
214
218
  if (scroll == "reset") {
215
219
  this.dom.scrollTop = 0
216
220
  } else if (scroll == "to selection") {
217
- let startDOM = this.domSelection().focusNode!
218
- if (this.someProp("handleScrollToSelection", f => f(this))) {
219
- // Handled
220
- } else if (state.selection instanceof NodeSelection) {
221
- let target = this.docView.domAfterPos(state.selection.from)
222
- if (target.nodeType == 1) scrollRectIntoView(this, (target as HTMLElement).getBoundingClientRect(), startDOM)
223
- } else {
224
- scrollRectIntoView(this, this.coordsAtPos(state.selection.head, 1), startDOM)
225
- }
221
+ this.scrollToSelection()
226
222
  } else if (oldScrollPos) {
227
223
  resetScrollPos(oldScrollPos)
228
224
  }
229
225
  }
230
226
 
227
+ /// @internal
228
+ scrollToSelection() {
229
+ let startDOM = this.domSelection().focusNode!
230
+ if (this.someProp("handleScrollToSelection", f => f(this))) {
231
+ // Handled
232
+ } else if (this.state.selection instanceof NodeSelection) {
233
+ let target = this.docView.domAfterPos(this.state.selection.from)
234
+ if (target.nodeType == 1) scrollRectIntoView(this, (target as HTMLElement).getBoundingClientRect(), startDOM)
235
+ } else {
236
+ scrollRectIntoView(this, this.coordsAtPos(this.state.selection.head, 1), startDOM)
237
+ }
238
+ }
239
+
231
240
  private destroyPluginViews() {
232
241
  let view
233
242
  while (view = this.pluginViews.pop()) if (view.destroy) view.destroy()
@@ -623,8 +632,8 @@ export interface EditorProps<P = any> {
623
632
  /// The `plain` flag will be true when the text is pasted as plain text.
624
633
  clipboardTextParser?: (this: P, text: string, $context: ResolvedPos, plain: boolean) => Slice
625
634
 
626
- /// Can be used to transform pasted content before it is applied to
627
- /// the document.
635
+ /// Can be used to transform pasted or dragged-and-dropped content
636
+ /// before it is applied to the document.
628
637
  transformPasted?: (this: P, slice: Slice) => Slice
629
638
 
630
639
  /// Allows you to pass custom rendering and behavior logic for
package/src/input.ts CHANGED
@@ -13,7 +13,8 @@ import {ViewDesc} from "./viewdesc"
13
13
  // A collection of DOM events that occur within the editor, and callback functions
14
14
  // to invoke when the event fires.
15
15
  const handlers: {[event: string]: (view: EditorView, event: Event) => void} = {}
16
- let editHandlers: {[event: string]: (view: EditorView, event: Event) => void} = {}
16
+ const editHandlers: {[event: string]: (view: EditorView, event: Event) => void} = {}
17
+ const passiveHandlers: Record<string, boolean> = {touchstart: true, touchmove: true}
17
18
 
18
19
  export class InputState {
19
20
  shiftKey = false
@@ -25,6 +26,8 @@ export class InputState {
25
26
  lastSelectionTime = 0
26
27
  lastIOSEnter = 0
27
28
  lastIOSEnterFallbackTimeout = -1
29
+ lastFocus = 0
30
+ lastTouch = 0
28
31
  lastAndroidDelete = 0
29
32
  composing = false
30
33
  composingTimeout = -1
@@ -42,7 +45,7 @@ export function initInput(view: EditorView) {
42
45
  if (eventBelongsToView(view, event) && !runCustomHandler(view, event) &&
43
46
  (view.editable || !(event.type in editHandlers)))
44
47
  handler(view, event)
45
- })
48
+ }, passiveHandlers[event] ? {passive: true} : undefined)
46
49
  }
47
50
  // On Safari, for reasons beyond my understanding, adding an input
48
51
  // event handler makes an issue where the composition vanishes when
@@ -403,11 +406,17 @@ class MouseDown {
403
406
  }
404
407
  }
405
408
 
406
- handlers.touchdown = view => {
409
+ handlers.touchstart = view => {
410
+ view.input.lastTouch = Date.now()
407
411
  forceDOMFlush(view)
408
412
  setSelectionOrigin(view, "pointer")
409
413
  }
410
414
 
415
+ handlers.touchmove = view => {
416
+ view.input.lastTouch = Date.now()
417
+ setSelectionOrigin(view, "pointer")
418
+ }
419
+
411
420
  handlers.contextmenu = view => forceDOMFlush(view)
412
421
 
413
422
  function inOrNearComposition(view: EditorView, event: Event) {
@@ -650,7 +659,6 @@ editHandlers.drop = (view, _event) => {
650
659
  let eventPos = view.posAtCoords(eventCoords(event))
651
660
  if (!eventPos) return
652
661
  let $mouse = view.state.doc.resolve(eventPos.pos)
653
- if (!$mouse) return
654
662
  let slice = dragging && dragging.slice
655
663
  if (slice) {
656
664
  view.someProp("transformPasted", f => { slice = f(slice!) })
@@ -695,6 +703,7 @@ editHandlers.drop = (view, _event) => {
695
703
  }
696
704
 
697
705
  handlers.focus = view => {
706
+ view.input.lastFocus = Date.now()
698
707
  if (!view.focused) {
699
708
  view.domObserver.stop()
700
709
  view.dom.classList.add("ProseMirror-focused")
package/src/viewdesc.ts CHANGED
@@ -709,7 +709,7 @@ export class NodeViewDesc extends ViewDesc {
709
709
  let composition = view.composing ? this.localCompositionInfo(view, pos) : null
710
710
  let localComposition = composition && composition.pos > -1 ? composition : null
711
711
  let compositionInChild = composition && composition.pos < 0
712
- let updater = new ViewTreeUpdater(this, localComposition && localComposition.node)
712
+ let updater = new ViewTreeUpdater(this, localComposition && localComposition.node, view)
713
713
  iterDeco(this.node, this.innerDeco, (widget, i, insideNode) => {
714
714
  if (widget.spec.marks)
715
715
  updater.syncToMarks(widget.spec.marks, inline, view)
@@ -1109,7 +1109,7 @@ class ViewTreeUpdater {
1109
1109
  preMatch: {index: number, matched: Map<ViewDesc, number>, matches: readonly ViewDesc[]}
1110
1110
  top: ViewDesc
1111
1111
 
1112
- constructor(top: NodeViewDesc, readonly lock: DOMNode | null) {
1112
+ constructor(top: NodeViewDesc, readonly lock: DOMNode | null, private readonly view: EditorView) {
1113
1113
  this.top = top
1114
1114
  this.preMatch = preMatch(top.node.content, top)
1115
1115
  }
@@ -1273,7 +1273,8 @@ class ViewTreeUpdater {
1273
1273
 
1274
1274
  if (!lastChild || // Empty textblock
1275
1275
  !(lastChild instanceof TextViewDesc) ||
1276
- /\n$/.test(lastChild.node.text!)) {
1276
+ /\n$/.test(lastChild.node.text!) ||
1277
+ (this.view.requiresGeckoHackNode && /\s$/.test(lastChild.node.text!))) {
1277
1278
  // Avoid bugs in Safari's cursor drawing (#1165) and Chrome's mouse selection (#1152)
1278
1279
  if ((browser.safari || browser.chrome) && lastChild && (lastChild.dom as HTMLElement).contentEditable == "false")
1279
1280
  this.addHackNode("IMG", parent)