prosemirror-view 1.39.0 → 1.39.2

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/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 1.39.2 (2025-04-22)
2
+
3
+ ### Bug fixes
4
+
5
+ Default to using `trustedTypes.defaultPolicy` for clipboard input, when available.
6
+
7
+ Fix an issue where pasting multiple lines from the virtual keyboard on Android would have only the effect of an enter press.
8
+
9
+ ## 1.39.1 (2025-04-04)
10
+
1
11
  ## 1.39.0 (2025-04-04)
2
12
 
3
13
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -2813,7 +2813,7 @@ var _policy = null;
2813
2813
  function maybeWrapTrusted(html) {
2814
2814
  var trustedTypes = window.trustedTypes;
2815
2815
  if (!trustedTypes) return html;
2816
- if (!_policy) _policy = trustedTypes.createPolicy("ProseMirrorClipboard", {
2816
+ if (!_policy) _policy = trustedTypes.defaultPolicy || trustedTypes.createPolicy("ProseMirrorClipboard", {
2817
2817
  createHTML: function createHTML(s) {
2818
2818
  return s;
2819
2819
  }
@@ -3411,7 +3411,6 @@ editHandlers.paste = function (view, _event) {
3411
3411
  if (view.composing && !android) return;
3412
3412
  var data = brokenClipboardAPI ? null : event.clipboardData;
3413
3413
  var plain = view.input.shiftKey && view.input.lastKeyCode != 45;
3414
- console.log("paste event", !!data, view.state.selection.from, view.state.selection.to);
3415
3414
  if (data && doPaste(view, getText(data), data.getData("text/html"), plain, event)) event.preventDefault();else capturePaste(view, event);
3416
3415
  };
3417
3416
  var Dragging = _createClass(function Dragging(slice, move, node) {
@@ -4669,7 +4668,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4669
4668
  var nextSel;
4670
4669
  if ((ios && view.input.lastIOSEnter > Date.now() - 225 && (!inlineChange || addedNodes.some(function (n) {
4671
4670
  return n.nodeName == "DIV" || n.nodeName == "P";
4672
- })) || !inlineChange && $from.pos < parse.doc.content.size && (!$from.sameParent($to) || !$from.parent.inlineContent) && (nextSel = prosemirrorState.Selection.findFrom(parse.doc.resolve($from.pos + 1), 1, true)) && nextSel.head > $from.pos) && view.someProp("handleKeyDown", function (f) {
4671
+ })) || !inlineChange && $from.pos < parse.doc.content.size && (!$from.sameParent($to) || !$from.parent.inlineContent) && !/\S/.test(parse.doc.textBetween($from.pos, $to.pos, "", "")) && (nextSel = prosemirrorState.Selection.findFrom(parse.doc.resolve($from.pos + 1), 1, true)) && nextSel.head > $from.pos) && view.someProp("handleKeyDown", function (f) {
4673
4672
  return f(view, keyEvent(13, "Enter"));
4674
4673
  })) {
4675
4674
  view.input.lastIOSEnter = 0;
package/dist/index.js CHANGED
@@ -2984,7 +2984,7 @@ function maybeWrapTrusted(html) {
2984
2984
  // innerHTML, even on a detached document. This wraps the string in
2985
2985
  // a way that makes the browser allow us to use its parser again.
2986
2986
  if (!_policy)
2987
- _policy = trustedTypes.createPolicy("ProseMirrorClipboard", { createHTML: (s) => s });
2987
+ _policy = trustedTypes.defaultPolicy || trustedTypes.createPolicy("ProseMirrorClipboard", { createHTML: (s) => s });
2988
2988
  return _policy.createHTML(html);
2989
2989
  }
2990
2990
  function readHTML(html) {
@@ -3663,7 +3663,6 @@ editHandlers.paste = (view, _event) => {
3663
3663
  return;
3664
3664
  let data = brokenClipboardAPI ? null : event.clipboardData;
3665
3665
  let plain = view.input.shiftKey && view.input.lastKeyCode != 45;
3666
- console.log("paste event", !!data, view.state.selection.from, view.state.selection.to);
3667
3666
  if (data && doPaste(view, getText(data), data.getData("text/html"), plain, event))
3668
3667
  event.preventDefault();
3669
3668
  else
@@ -5024,6 +5023,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
5024
5023
  (!inlineChange || addedNodes.some(n => n.nodeName == "DIV" || n.nodeName == "P"))) ||
5025
5024
  (!inlineChange && $from.pos < parse.doc.content.size &&
5026
5025
  (!$from.sameParent($to) || !$from.parent.inlineContent) &&
5026
+ !/\S/.test(parse.doc.textBetween($from.pos, $to.pos, "", "")) &&
5027
5027
  (nextSel = Selection.findFrom(parse.doc.resolve($from.pos + 1), 1, true)) &&
5028
5028
  nextSel.head > $from.pos)) &&
5029
5029
  view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.39.0",
3
+ "version": "1.39.2",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/#index.ts# ADDED
@@ -0,0 +1,822 @@
1
+ import {NodeSelection, EditorState, Plugin, PluginView, Transaction, Selection} from "prosemirror-state"
2
+ import {Slice, ResolvedPos, DOMParser, DOMSerializer, Node, Mark} from "prosemirror-model"
3
+
4
+ import {scrollRectIntoView, posAtCoords, coordsAtPos, endOfTextblock, storeScrollPos,
5
+ resetScrollPos, focusPreventScroll} from "./domcoords"
6
+ import {docViewDesc, ViewDesc, NodeView, NodeViewDesc, MarkView} from "./viewdesc"
7
+ import {initInput, destroyInput, dispatchEvent, ensureListeners, clearComposition,
8
+ InputState, doPaste, Dragging, findCompositionNode} from "./input"
9
+ import {selectionToDOM, anchorInRightPlace, syncNodeSelection} from "./selection"
10
+ import {Decoration, viewDecorations, DecorationSource} from "./decoration"
11
+ import {DOMObserver, safariShadowSelectionRange} from "./domobserver"
12
+ import {DOMChange} from "./domchange"
13
+ import {DOMSelection, DOMNode, DOMSelectionRange, deepActiveElement, clearReusedRange} from "./dom"
14
+ import * as browser from "./browser"
15
+
16
+ export {Decoration, DecorationSet, DecorationAttrs, DecorationSource} from "./decoration"
17
+ export {NodeView, MarkView, ViewMutationRecord} from "./viewdesc"
18
+
19
+ // Exported for testing
20
+ import {serializeForClipboard, parseFromClipboard} from "./clipboard"
21
+ import {endComposition} from "./input"
22
+ /// @internal
23
+ export const __parseFromClipboard = parseFromClipboard
24
+ /// @internal
25
+ export const __endComposition = endComposition
26
+
27
+ /// An editor view manages the DOM structure that represents an
28
+ /// editable document. Its state and behavior are determined by its
29
+ /// [props](#view.DirectEditorProps).
30
+ export class EditorView {
31
+ private _props: DirectEditorProps
32
+ private directPlugins: readonly Plugin[]
33
+ private _root: Document | ShadowRoot | null = null
34
+ /// @internal
35
+ focused = false
36
+ /// Kludge used to work around a Chrome bug @internal
37
+ trackWrites: DOMNode | null = null
38
+ private mounted = false
39
+ /// @internal
40
+ markCursor: readonly Mark[] | null = null
41
+ /// @internal
42
+ cursorWrapper: {dom: DOMNode, deco: Decoration} | null = null
43
+ /// @internal
44
+ nodeViews: NodeViewSet
45
+ /// @internal
46
+ lastSelectedViewDesc: ViewDesc | undefined = undefined
47
+ /// @internal
48
+ docView: NodeViewDesc
49
+ /// @internal
50
+ input = new InputState
51
+ private prevDirectPlugins: readonly Plugin[] = []
52
+ private pluginViews: PluginView[] = []
53
+ /// @internal
54
+ declare domObserver: DOMObserver
55
+ /// Holds `true` when a hack node is needed in Firefox to prevent the
56
+ /// [space is eaten issue](https://github.com/ProseMirror/prosemirror/issues/651)
57
+ /// @internal
58
+ requiresGeckoHackNode: boolean = false
59
+
60
+ /// The view's current [state](#state.EditorState).
61
+ public state: EditorState
62
+
63
+ /// Create a view. `place` may be a DOM node that the editor should
64
+ /// be appended to, a function that will place it into the document,
65
+ /// or an object whose `mount` property holds the node to use as the
66
+ /// document container. If it is `null`, the editor will not be
67
+ /// added to the document.
68
+ constructor(place: null | DOMNode | ((editor: HTMLElement) => void) | {mount: HTMLElement}, props: DirectEditorProps) {
69
+ this._props = props
70
+ this.state = props.state
71
+ this.directPlugins = props.plugins || []
72
+ this.directPlugins.forEach(checkStateComponent)
73
+
74
+ this.dispatch = this.dispatch.bind(this)
75
+
76
+ this.dom = (place && (place as {mount: HTMLElement}).mount) || document.createElement("div")
77
+ if (place) {
78
+ if ((place as DOMNode).appendChild) (place as DOMNode).appendChild(this.dom)
79
+ else if (typeof place == "function") place(this.dom)
80
+ else if ((place as {mount: HTMLElement}).mount) this.mounted = true
81
+ }
82
+
83
+ this.editable = getEditable(this)
84
+ updateCursorWrapper(this)
85
+ this.nodeViews = buildNodeViews(this)
86
+ this.docView = docViewDesc(this.state.doc, computeDocDeco(this), viewDecorations(this), this.dom, this)
87
+
88
+ this.domObserver = new DOMObserver(this, change => change.apply(this))
89
+ this.domObserver.start()
90
+ initInput(this)
91
+ this.updatePluginViews()
92
+ }
93
+
94
+ /// An editable DOM node containing the document. (You probably
95
+ /// should not directly interfere with its content.)
96
+ readonly dom: HTMLElement
97
+
98
+ /// Indicates whether the editor is currently [editable](#view.EditorProps.editable).
99
+ editable: boolean
100
+
101
+ /// When editor content is being dragged, this object contains
102
+ /// information about the dragged slice and whether it is being
103
+ /// copied or moved. At any other time, it is null.
104
+ dragging: null | {slice: Slice, move: boolean} = null
105
+
106
+ /// Holds `true` when a
107
+ /// [composition](https://w3c.github.io/uievents/#events-compositionevents)
108
+ /// is active.
109
+ get composing() { return this.input.composing }
110
+
111
+ /// The view's current [props](#view.EditorProps).
112
+ get props() {
113
+ if (this._props.state != this.state) {
114
+ let prev = this._props
115
+ this._props = {} as any
116
+ for (let name in prev) (this._props as any)[name] = (prev as any)[name]
117
+ this._props.state = this.state
118
+ }
119
+ return this._props
120
+ }
121
+
122
+ /// Update the view's props. Will immediately cause an update to
123
+ /// the DOM.
124
+ update(props: DirectEditorProps) {
125
+ if (props.handleDOMEvents != this._props.handleDOMEvents) ensureListeners(this)
126
+ let prevProps = this._props
127
+ this._props = props
128
+ if (props.plugins) {
129
+ props.plugins.forEach(checkStateComponent)
130
+ this.directPlugins = props.plugins
131
+ }
132
+ this.updateStateInner(props.state, prevProps)
133
+ }
134
+
135
+ /// Update the view by updating existing props object with the object
136
+ /// given as argument. Equivalent to `view.update(Object.assign({},
137
+ /// view.props, props))`.
138
+ setProps(props: Partial<DirectEditorProps>) {
139
+ let updated = {} as DirectEditorProps
140
+ for (let name in this._props) (updated as any)[name] = (this._props as any)[name]
141
+ updated.state = this.state
142
+ for (let name in props) (updated as any)[name] = (props as any)[name]
143
+ this.update(updated)
144
+ }
145
+
146
+ /// Update the editor's `state` prop, without touching any of the
147
+ /// other props.
148
+ updateState(state: EditorState) {
149
+ this.updateStateInner(state, this._props)
150
+ }
151
+
152
+ private updateStateInner(state: EditorState, prevProps: DirectEditorProps) {
153
+ let prev = this.state, redraw = false, updateSel = false
154
+ // When stored marks are added, stop composition, so that they can
155
+ // be displayed.
156
+ if (state.storedMarks && this.composing) {
157
+ clearComposition(this)
158
+ updateSel = true
159
+ }
160
+ this.state = state
161
+ let pluginsChanged = prev.plugins != state.plugins || this._props.plugins != prevProps.plugins
162
+ if (pluginsChanged || this._props.plugins != prevProps.plugins || this._props.nodeViews != prevProps.nodeViews) {
163
+ let nodeViews = buildNodeViews(this)
164
+ if (changedNodeViews(nodeViews, this.nodeViews)) {
165
+ this.nodeViews = nodeViews
166
+ redraw = true
167
+ }
168
+ }
169
+ if (pluginsChanged || prevProps.handleDOMEvents != this._props.handleDOMEvents) {
170
+ ensureListeners(this)
171
+ }
172
+
173
+ this.editable = getEditable(this)
174
+ updateCursorWrapper(this)
175
+ let innerDeco = viewDecorations(this), outerDeco = computeDocDeco(this)
176
+
177
+ let scroll = prev.plugins != state.plugins && !prev.doc.eq(state.doc) ? "reset"
178
+ : (state as any).scrollToSelection > (prev as any).scrollToSelection ? "to selection" : "preserve"
179
+ let updateDoc = redraw || !this.docView.matchesNode(state.doc, outerDeco, innerDeco)
180
+ if (updateDoc || !state.selection.eq(prev.selection)) updateSel = true
181
+ let oldScrollPos = scroll == "preserve" && updateSel && this.dom.style.overflowAnchor == null && storeScrollPos(this)
182
+
183
+ if (updateSel) {
184
+ this.domObserver.stop()
185
+ // Work around an issue in Chrome, IE, and Edge where changing
186
+ // the DOM around an active selection puts it into a broken
187
+ // state where the thing the user sees differs from the
188
+ // selection reported by the Selection object (#710, #973,
189
+ // #1011, #1013, #1035).
190
+ let forceSelUpdate = updateDoc && (browser.ie || browser.chrome) && !this.composing &&
191
+ !prev.selection.empty && !state.selection.empty && selectionContextChanged(prev.selection, state.selection)
192
+ if (updateDoc) {
193
+ // If the node that the selection points into is written to,
194
+ // Chrome sometimes starts misreporting the selection, so this
195
+ // tracks that and forces a selection reset when our update
196
+ // did write to the node.
197
+ let chromeKludge = browser.chrome ? (this.trackWrites = this.domSelectionRange().focusNode) : null
198
+ if (this.composing) this.input.compositionNode = findCompositionNode(this)
199
+ if (redraw || !this.docView.update(state.doc, outerDeco, innerDeco, this)) {
200
+ this.docView.updateOuterDeco(outerDeco)
201
+ this.docView.destroy()
202
+ this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this)
203
+ }
204
+ if (chromeKludge && !this.trackWrites) forceSelUpdate = true
205
+ }
206
+ // Work around for an issue where an update arriving right between
207
+ // a DOM selection change and the "selectionchange" event for it
208
+ // can cause a spurious DOM selection update, disrupting mouse
209
+ // drag selection.
210
+ if (forceSelUpdate ||
211
+ !(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) &&
212
+ anchorInRightPlace(this))) {
213
+ selectionToDOM(this, forceSelUpdate)
214
+ } else {
215
+ syncNodeSelection(this, state.selection)
216
+ this.domObserver.setCurSelection()
217
+ }
218
+ this.domObserver.start()
219
+ }
220
+
221
+ this.updatePluginViews(prev)
222
+ if ((this.dragging as Dragging)?.node && !prev.doc.eq(state.doc))
223
+ this.updateDraggedNode(this.dragging as Dragging, prev)
224
+
225
+ if (scroll == "reset") {
226
+ this.dom.scrollTop = 0
227
+ } else if (scroll == "to selection") {
228
+ this.scrollToSelection()
229
+ } else if (oldScrollPos) {
230
+ resetScrollPos(oldScrollPos)
231
+ }
232
+ }
233
+
234
+ /// @internal
235
+ scrollToSelection() {
236
+ let startDOM = this.domSelectionRange().focusNode
237
+ if (!startDOM || !this.dom.contains(startDOM.nodeType == 1 ? startDOM : startDOM.parentNode)) {
238
+ // Ignore selections outside the editor
239
+ } else if (this.someProp("handleScrollToSelection", f => f(this))) {
240
+ // Handled
241
+ } else if (this.state.selection instanceof NodeSelection) {
242
+ let target = this.docView.domAfterPos(this.state.selection.from)
243
+ if (target.nodeType == 1) scrollRectIntoView(this, (target as HTMLElement).getBoundingClientRect(), startDOM)
244
+ } else {
245
+ scrollRectIntoView(this, this.coordsAtPos(this.state.selection.head, 1), startDOM)
246
+ }
247
+ }
248
+
249
+ private destroyPluginViews() {
250
+ let view
251
+ while (view = this.pluginViews.pop()) if (view.destroy) view.destroy()
252
+ }
253
+
254
+ private updatePluginViews(prevState?: EditorState) {
255
+ if (!prevState || prevState.plugins != this.state.plugins || this.directPlugins != this.prevDirectPlugins) {
256
+ this.prevDirectPlugins = this.directPlugins
257
+ this.destroyPluginViews()
258
+ for (let i = 0; i < this.directPlugins.length; i++) {
259
+ let plugin = this.directPlugins[i]
260
+ if (plugin.spec.view) this.pluginViews.push(plugin.spec.view(this))
261
+ }
262
+ for (let i = 0; i < this.state.plugins.length; i++) {
263
+ let plugin = this.state.plugins[i]
264
+ if (plugin.spec.view) this.pluginViews.push(plugin.spec.view(this))
265
+ }
266
+ } else {
267
+ for (let i = 0; i < this.pluginViews.length; i++) {
268
+ let pluginView = this.pluginViews[i]
269
+ if (pluginView.update) pluginView.update(this, prevState)
270
+ }
271
+ }
272
+ }
273
+
274
+ private updateDraggedNode(dragging: Dragging, prev: EditorState) {
275
+ let sel = dragging.node!, found = -1
276
+ if (this.state.doc.nodeAt(sel.from) == sel.node) {
277
+ found = sel.from
278
+ } else {
279
+ let movedPos = sel.from + (this.state.doc.content.size - prev.doc.content.size)
280
+ let moved = movedPos > 0 && this.state.doc.nodeAt(movedPos)
281
+ if (moved == sel.node) found = movedPos
282
+ }
283
+ this.dragging = new Dragging(dragging.slice, dragging.move,
284
+ found < 0 ? undefined : NodeSelection.create(this.state.doc, found))
285
+ }
286
+
287
+ /// Goes over the values of a prop, first those provided directly,
288
+ /// then those from plugins given to the view, then from plugins in
289
+ /// the state (in order), and calls `f` every time a non-undefined
290
+ /// value is found. When `f` returns a truthy value, that is
291
+ /// immediately returned. When `f` isn't provided, it is treated as
292
+ /// the identity function (the prop value is returned directly).
293
+ someProp<PropName extends keyof EditorProps, Result>(
294
+ propName: PropName,
295
+ f: (value: NonNullable<EditorProps[PropName]>) => Result
296
+ ): Result | undefined
297
+ someProp<PropName extends keyof EditorProps>(propName: PropName): NonNullable<EditorProps[PropName]> | undefined
298
+ someProp<PropName extends keyof EditorProps, Result>(
299
+ propName: PropName,
300
+ f?: (value: NonNullable<EditorProps[PropName]>) => Result
301
+ ): Result | undefined {
302
+ let prop = this._props && this._props[propName], value
303
+ if (prop != null && (value = f ? f(prop as any) : prop)) return value as any
304
+ for (let i = 0; i < this.directPlugins.length; i++) {
305
+ let prop = this.directPlugins[i].props[propName]
306
+ if (prop != null && (value = f ? f(prop as any) : prop)) return value as any
307
+ }
308
+ let plugins = this.state.plugins
309
+ if (plugins) for (let i = 0; i < plugins.length; i++) {
310
+ let prop = plugins[i].props[propName]
311
+ if (prop != null && (value = f ? f(prop as any) : prop)) return value as any
312
+ }
313
+ }
314
+
315
+ /// Query whether the view has focus.
316
+ hasFocus() {
317
+ // Work around IE not handling focus correctly if resize handles are shown.
318
+ // If the cursor is inside an element with resize handles, activeElement
319
+ // will be that element instead of this.dom.
320
+ if (browser.ie) {
321
+ // If activeElement is within this.dom, and there are no other elements
322
+ // setting `contenteditable` to false in between, treat it as focused.
323
+ let node = this.root.activeElement
324
+ if (node == this.dom) return true
325
+ if (!node || !this.dom.contains(node)) return false
326
+ while (node && this.dom != node && this.dom.contains(node)) {
327
+ if ((node as HTMLElement).contentEditable == 'false') return false
328
+ node = node.parentElement
329
+ }
330
+ return true
331
+ }
332
+ return this.root.activeElement == this.dom
333
+ }
334
+
335
+ /// Focus the editor.
336
+ focus() {
337
+ this.domObserver.stop()
338
+ if (this.editable) focusPreventScroll(this.dom)
339
+ selectionToDOM(this)
340
+ this.domObserver.start()
341
+ }
342
+
343
+ /// Get the document root in which the editor exists. This will
344
+ /// usually be the top-level `document`, but might be a [shadow
345
+ /// DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM)
346
+ /// root if the editor is inside one.
347
+ get root(): Document | ShadowRoot {
348
+ let cached = this._root
349
+ if (cached == null) for (let search = this.dom.parentNode; search; search = search.parentNode) {
350
+ if (search.nodeType == 9 || (search.nodeType == 11 && (search as any).host)) {
351
+ if (!(search as any).getSelection)
352
+ Object.getPrototypeOf(search).getSelection = () => (search as DOMNode).ownerDocument!.getSelection()
353
+ return this._root = search as Document | ShadowRoot
354
+ }
355
+ }
356
+ return cached || document
357
+ }
358
+
359
+ /// When an existing editor view is moved to a new document or
360
+ /// shadow tree, call this to make it recompute its root.
361
+ updateRoot() {
362
+ this._root = null
363
+ }
364
+
365
+ /// Given a pair of viewport coordinates, return the document
366
+ /// position that corresponds to them. May return null if the given
367
+ /// coordinates aren't inside of the editor. When an object is
368
+ /// returned, its `pos` property is the position nearest to the
369
+ /// coordinates, and its `inside` property holds the position of the
370
+ /// inner node that the position falls inside of, or -1 if it is at
371
+ /// the top level, not in any node.
372
+ posAtCoords(coords: {left: number, top: number}): {pos: number, inside: number} | null {
373
+ return posAtCoords(this, coords)
374
+ }
375
+
376
+ /// Returns the viewport rectangle at a given document position.
377
+ /// `left` and `right` will be the same number, as this returns a
378
+ /// flat cursor-ish rectangle. If the position is between two things
379
+ /// that aren't directly adjacent, `side` determines which element
380
+ /// is used. When < 0, the element before the position is used,
381
+ /// otherwise the element after.
382
+ coordsAtPos(pos: number, side = 1): {left: number, right: number, top: number, bottom: number} {
383
+ return coordsAtPos(this, pos, side)
384
+ }
385
+
386
+ /// Find the DOM position that corresponds to the given document
387
+ /// position. When `side` is negative, find the position as close as
388
+ /// possible to the content before the position. When positive,
389
+ /// prefer positions close to the content after the position. When
390
+ /// zero, prefer as shallow a position as possible.
391
+ ///
392
+ /// Note that you should **not** mutate the editor's internal DOM,
393
+ /// only inspect it (and even that is usually not necessary).
394
+ domAtPos(pos: number, side = 0): {node: DOMNode, offset: number} {
395
+ return this.docView.domFromPos(pos, side)
396
+ }
397
+
398
+ /// Find the DOM node that represents the document node after the
399
+ /// given position. May return `null` when the position doesn't point
400
+ /// in front of a node or if the node is inside an opaque node view.
401
+ ///
402
+ /// This is intended to be able to call things like
403
+ /// `getBoundingClientRect` on that DOM node. Do **not** mutate the
404
+ /// editor DOM directly, or add styling this way, since that will be
405
+ /// immediately overriden by the editor as it redraws the node.
406
+ nodeDOM(pos: number): DOMNode | null {
407
+ let desc = this.docView.descAt(pos)
408
+ return desc ? (desc as NodeViewDesc).nodeDOM : null
409
+ }
410
+
411
+ /// Find the document position that corresponds to a given DOM
412
+ /// position. (Whenever possible, it is preferable to inspect the
413
+ /// document structure directly, rather than poking around in the
414
+ /// DOM, but sometimes—for example when interpreting an event
415
+ /// target—you don't have a choice.)
416
+ ///
417
+ /// The `bias` parameter can be used to influence which side of a DOM
418
+ /// node to use when the position is inside a leaf node.
419
+ posAtDOM(node: DOMNode, offset: number, bias = -1): number {
420
+ let pos = this.docView.posFromDOM(node, offset, bias)
421
+ if (pos == null) throw new RangeError("DOM position not inside the editor")
422
+ return pos
423
+ }
424
+
425
+ /// Find out whether the selection is at the end of a textblock when
426
+ /// moving in a given direction. When, for example, given `"left"`,
427
+ /// it will return true if moving left from the current cursor
428
+ /// position would leave that position's parent textblock. Will apply
429
+ /// to the view's current state by default, but it is possible to
430
+ /// pass a different state.
431
+ endOfTextblock(dir: "up" | "down" | "left" | "right" | "forward" | "backward", state?: EditorState): boolean {
432
+ return endOfTextblock(this, state || this.state, dir)
433
+ }
434
+
435
+ /// Run the editor's paste logic with the given HTML string. The
436
+ /// `event`, if given, will be passed to the
437
+ /// [`handlePaste`](#view.EditorProps.handlePaste) hook.
438
+ pasteHTML(html: string, event?: ClipboardEvent) {
439
+ return doPaste(this, "", html, false, event || new ClipboardEvent("paste"))
440
+ }
441
+
442
+ /// Run the editor's paste logic with the given plain-text input.
443
+ pasteText(text: string, event?: ClipboardEvent) {
444
+ return doPaste(this, text, null, true, event || new ClipboardEvent("paste"))
445
+ }
446
+
447
+ /// Serialize the given slice as it would be if it was copied from
448
+ /// this editor. Returns a DOM element that contains a
449
+ /// representation of the slice as its children, a textual
450
+ /// representation, and the transformed slice (which can be
451
+ /// different from the given input due to hooks like
452
+ /// [`transformCopied`](#view.EditorProps.transformCopied)).
453
+ serializeForClipboard(slice: Slice): {dom: HTMLElement, text: string, slice: Slice} {
454
+ return serializeForClipboard(this, slice)
455
+ }
456
+
457
+ /// Removes the editor from the DOM and destroys all [node
458
+ /// views](#view.NodeView).
459
+ destroy() {
460
+ if (!this.docView) return
461
+ destroyInput(this)
462
+ this.destroyPluginViews()
463
+ if (this.mounted) {
464
+ this.docView.update(this.state.doc, [], viewDecorations(this), this)
465
+ this.dom.textContent = ""
466
+ } else if (this.dom.parentNode) {
467
+ this.dom.parentNode.removeChild(this.dom)
468
+ }
469
+ this.docView.destroy()
470
+ ;(this as any).docView = null
471
+ clearReusedRange();
472
+ }
473
+
474
+ /// This is true when the view has been
475
+ /// [destroyed](#view.EditorView.destroy) (and thus should not be
476
+ /// used anymore).
477
+ get isDestroyed() {
478
+ return this.docView == null
479
+ }
480
+
481
+ /// Used for testing.
482
+ dispatchEvent(event: Event) {
483
+ return dispatchEvent(this, event)
484
+ }
485
+
486
+ /// Dispatch a transaction. Will call
487
+ /// [`dispatchTransaction`](#view.DirectEditorProps.dispatchTransaction)
488
+ /// when given, and otherwise defaults to applying the transaction to
489
+ /// the current state and calling
490
+ /// [`updateState`](#view.EditorView.updateState) with the result.
491
+ /// This method is bound to the view instance, so that it can be
492
+ /// easily passed around.
493
+ dispatch(tr: Transaction) {
494
+ let pendingDOMChange = this.domObserver.getDOMChange()
495
+ let dispatchTransaction = this._props.dispatchTransaction
496
+ if (dispatchTransaction) dispatchTransaction.call(this, tr)
497
+ else this.updateState(this.state.apply(tr))
498
+ }
499
+
500
+ /// @internal
501
+ domSelectionRange(): DOMSelectionRange {
502
+ let sel = this.domSelection()
503
+ if (!sel) return {focusNode: null, focusOffset: 0, anchorNode: null, anchorOffset: 0}
504
+ return browser.safari && this.root.nodeType === 11 &&
505
+ deepActiveElement(this.dom.ownerDocument) == this.dom && safariShadowSelectionRange(this, sel) || sel
506
+ }
507
+
508
+ /// @internal
509
+ domSelection(): DOMSelection | null {
510
+ return (this.root as Document).getSelection()
511
+ }
512
+ }
513
+
514
+ function computeDocDeco(view: EditorView) {
515
+ let attrs = Object.create(null)
516
+ attrs.class = "ProseMirror"
517
+ attrs.contenteditable = String(view.editable)
518
+
519
+ view.someProp("attributes", value => {
520
+ if (typeof value == "function") value = value(view.state)
521
+ if (value) for (let attr in value) {
522
+ if (attr == "class")
523
+ attrs.class += " " + value[attr]
524
+ else if (attr == "style")
525
+ attrs.style = (attrs.style ? attrs.style + ";" : "") + value[attr]
526
+ else if (!attrs[attr] && attr != "contenteditable" && attr != "nodeName")
527
+ attrs[attr] = String(value[attr])
528
+ }
529
+ })
530
+ if (!attrs.translate) attrs.translate = "no"
531
+
532
+ return [Decoration.node(0, view.state.doc.content.size, attrs)]
533
+ }
534
+
535
+ function updateCursorWrapper(view: EditorView) {
536
+ if (view.markCursor) {
537
+ let dom = document.createElement("img")
538
+ dom.className = "ProseMirror-separator"
539
+ dom.setAttribute("mark-placeholder", "true")
540
+ dom.setAttribute("alt", "")
541
+ view.cursorWrapper = {dom, deco: Decoration.widget(view.state.selection.from,
542
+ dom, {raw: true, marks: view.markCursor} as any)}
543
+ } else {
544
+ view.cursorWrapper = null
545
+ }
546
+ }
547
+
548
+ function getEditable(view: EditorView) {
549
+ return !view.someProp("editable", value => value(view.state) === false)
550
+ }
551
+
552
+ function selectionContextChanged(sel1: Selection, sel2: Selection) {
553
+ let depth = Math.min(sel1.$anchor.sharedDepth(sel1.head), sel2.$anchor.sharedDepth(sel2.head))
554
+ return sel1.$anchor.start(depth) != sel2.$anchor.start(depth)
555
+ }
556
+
557
+ function buildNodeViews(view: EditorView) {
558
+ let result: NodeViewSet = Object.create(null)
559
+ function add(obj: NodeViewSet) {
560
+ for (let prop in obj) if (!Object.prototype.hasOwnProperty.call(result, prop))
561
+ result[prop] = obj[prop]
562
+ }
563
+ view.someProp("nodeViews", add)
564
+ view.someProp("markViews", add)
565
+ return result
566
+ }
567
+
568
+ function changedNodeViews(a: NodeViewSet, b: NodeViewSet) {
569
+ let nA = 0, nB = 0
570
+ for (let prop in a) {
571
+ if (a[prop] != b[prop]) return true
572
+ nA++
573
+ }
574
+ for (let _ in b) nB++
575
+ return nA != nB
576
+ }
577
+
578
+ function checkStateComponent(plugin: Plugin) {
579
+ if (plugin.spec.state || plugin.spec.filterTransaction || plugin.spec.appendTransaction)
580
+ throw new RangeError("Plugins passed directly to the view must not have a state component")
581
+ }
582
+
583
+ /// The type of function [provided](#view.EditorProps.nodeViews) to
584
+ /// create [node views](#view.NodeView).
585
+ export type NodeViewConstructor = (node: Node, view: EditorView, getPos: () => number | undefined,
586
+ decorations: readonly Decoration[], innerDecorations: DecorationSource) => NodeView
587
+
588
+ /// The function types [used](#view.EditorProps.markViews) to create
589
+ /// mark views.
590
+ export type MarkViewConstructor = (mark: Mark, view: EditorView, inline: boolean) => MarkView
591
+
592
+ type NodeViewSet = {[name: string]: NodeViewConstructor | MarkViewConstructor}
593
+
594
+ /// Helper type that maps event names to event object types, but
595
+ /// includes events that TypeScript's HTMLElementEventMap doesn't know
596
+ /// about.
597
+ export interface DOMEventMap extends HTMLElementEventMap {
598
+ [event: string]: any
599
+ }
600
+
601
+ /// Props are configuration values that can be passed to an editor view
602
+ /// or included in a plugin. This interface lists the supported props.
603
+ ///
604
+ /// The various event-handling functions may all return `true` to
605
+ /// indicate that they handled the given event. The view will then take
606
+ /// care to call `preventDefault` on the event, except with
607
+ /// `handleDOMEvents`, where the handler itself is responsible for that.
608
+ ///
609
+ /// How a prop is resolved depends on the prop. Handler functions are
610
+ /// called one at a time, starting with the base props and then
611
+ /// searching through the plugins (in order of appearance) until one of
612
+ /// them returns true. For some props, the first plugin that yields a
613
+ /// value gets precedence.
614
+ ///
615
+ /// The optional type parameter refers to the type of `this` in prop
616
+ /// functions, and is used to pass in the plugin type when defining a
617
+ /// [plugin](#state.Plugin).
618
+ export interface EditorProps<P = any> {
619
+ /// Can be an object mapping DOM event type names to functions that
620
+ /// handle them. Such functions will be called before any handling
621
+ /// ProseMirror does of events fired on the editable DOM element.
622
+ /// Contrary to the other event handling props, when returning true
623
+ /// from such a function, you are responsible for calling
624
+ /// `preventDefault` yourself (or not, if you want to allow the
625
+ /// default behavior).
626
+ handleDOMEvents?: {
627
+ [event in keyof DOMEventMap]?: (this: P, view: EditorView, event: DOMEventMap[event]) => boolean | void
628
+ }
629
+
630
+ /// Called when the editor receives a `keydown` event.
631
+ handleKeyDown?: (this: P, view: EditorView, event: KeyboardEvent) => boolean | void
632
+
633
+ /// Handler for `keypress` events.
634
+ handleKeyPress?: (this: P, view: EditorView, event: KeyboardEvent) => boolean | void
635
+
636
+ /// Whenever the user directly input text, this handler is called
637
+ /// before the input is applied. If it returns `true`, the default
638
+ /// behavior of actually inserting the text is suppressed.
639
+ handleTextInput?: (this: P, view: EditorView, from: number, to: number, text: string) => boolean | void
640
+
641
+ /// Called for each node around a click, from the inside out. The
642
+ /// `direct` flag will be true for the inner node.
643
+ handleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void
644
+
645
+ /// Called when the editor is clicked, after `handleClickOn` handlers
646
+ /// have been called.
647
+ handleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void
648
+
649
+ /// Called for each node around a double click.
650
+ handleDoubleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void
651
+
652
+ /// Called when the editor is double-clicked, after `handleDoubleClickOn`.
653
+ handleDoubleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void
654
+
655
+ /// Called for each node around a triple click.
656
+ handleTripleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void
657
+
658
+ /// Called when the editor is triple-clicked, after `handleTripleClickOn`.
659
+ handleTripleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void
660
+
661
+ /// Can be used to override the behavior of pasting. `slice` is the
662
+ /// pasted content parsed by the editor, but you can directly access
663
+ /// the event to get at the raw content.
664
+ handlePaste?: (this: P, view: EditorView, event: ClipboardEvent, slice: Slice) => boolean | void
665
+
666
+ /// Called when something is dropped on the editor. `moved` will be
667
+ /// true if this drop moves from the current selection (which should
668
+ /// thus be deleted).
669
+ handleDrop?: (this: P, view: EditorView, event: DragEvent, slice: Slice, moved: boolean) => boolean | void
670
+
671
+ /// Called when the view, after updating its state, tries to scroll
672
+ /// the selection into view. A handler function may return false to
673
+ /// indicate that it did not handle the scrolling and further
674
+ /// handlers or the default behavior should be tried.
675
+ handleScrollToSelection?: (this: P, view: EditorView) => boolean
676
+
677
+ /// Determines whether an in-editor drag event should copy or move
678
+ /// the selection. When not given, the event's `altKey` property is
679
+ /// used on macOS, `ctrlKey` on other platforms.
680
+ dragCopies?: (event: DragEvent) => boolean
681
+
682
+ /// Can be used to override the way a selection is created when
683
+ /// reading a DOM selection between the given anchor and head.
684
+ createSelectionBetween?: (this: P, view: EditorView, anchor: ResolvedPos, head: ResolvedPos) => Selection | null
685
+
686
+ /// The [parser](#model.DOMParser) to use when reading editor changes
687
+ /// from the DOM. Defaults to calling
688
+ /// [`DOMParser.fromSchema`](#model.DOMParser^fromSchema) on the
689
+ /// editor's schema.
690
+ domParser?: DOMParser
691
+
692
+ /// Can be used to transform pasted HTML text, _before_ it is parsed,
693
+ /// for example to clean it up.
694
+ transformPastedHTML?: (this: P, html: string, view: EditorView) => string
695
+
696
+ /// The [parser](#model.DOMParser) to use when reading content from
697
+ /// the clipboard. When not given, the value of the
698
+ /// [`domParser`](#view.EditorProps.domParser) prop is used.
699
+ clipboardParser?: DOMParser
700
+
701
+ /// Transform pasted plain text. The `plain` flag will be true when
702
+ /// the text is pasted as plain text.
703
+ transformPastedText?: (this: P, text: string, plain: boolean, view: EditorView) => string
704
+
705
+ /// A function to parse text from the clipboard into a document
706
+ /// slice. Called after
707
+ /// [`transformPastedText`](#view.EditorProps.transformPastedText).
708
+ /// The default behavior is to split the text into lines, wrap them
709
+ /// in `<p>` tags, and call
710
+ /// [`clipboardParser`](#view.EditorProps.clipboardParser) on it.
711
+ /// The `plain` flag will be true when the text is pasted as plain text.
712
+ clipboardTextParser?: (this: P, text: string, $context: ResolvedPos, plain: boolean, view: EditorView) => Slice
713
+
714
+ /// Can be used to transform pasted or dragged-and-dropped content
715
+ /// before it is applied to the document.
716
+ transformPasted?: (this: P, slice: Slice, view: EditorView) => Slice
717
+
718
+ /// Can be used to transform copied or cut content before it is
719
+ /// serialized to the clipboard.
720
+ transformCopied?: (this: P, slice: Slice, view: EditorView) => Slice
721
+
722
+ /// Allows you to pass custom rendering and behavior logic for
723
+ /// nodes. Should map node names to constructor functions that
724
+ /// produce a [`NodeView`](#view.NodeView) object implementing the
725
+ /// node's display behavior. The third argument `getPos` is a
726
+ /// function that can be called to get the node's current position,
727
+ /// which can be useful when creating transactions to update it.
728
+ /// Note that if the node is not in the document, the position
729
+ /// returned by this function will be `undefined`.
730
+ ///
731
+ /// `decorations` is an array of node or inline decorations that are
732
+ /// active around the node. They are automatically drawn in the
733
+ /// normal way, and you will usually just want to ignore this, but
734
+ /// they can also be used as a way to provide context information to
735
+ /// the node view without adding it to the document itself.
736
+ ///
737
+ /// `innerDecorations` holds the decorations for the node's content.
738
+ /// You can safely ignore this if your view has no content or a
739
+ /// `contentDOM` property, since the editor will draw the decorations
740
+ /// on the content. But if you, for example, want to create a nested
741
+ /// editor with the content, it may make sense to provide it with the
742
+ /// inner decorations.
743
+ ///
744
+ /// (For backwards compatibility reasons, [mark
745
+ /// views](#view.EditorProps.markViews) can also be included in this
746
+ /// object.)
747
+ nodeViews?: {[node: string]: NodeViewConstructor}
748
+
749
+ /// Pass custom mark rendering functions. Note that these cannot
750
+ /// provide the kind of dynamic behavior that [node
751
+ /// views](#view.NodeView) can—they just provide custom rendering
752
+ /// logic. The third argument indicates whether the mark's content
753
+ /// is inline.
754
+ markViews?: {[mark: string]: MarkViewConstructor}
755
+
756
+ /// The DOM serializer to use when putting content onto the
757
+ /// clipboard. If not given, the result of
758
+ /// [`DOMSerializer.fromSchema`](#model.DOMSerializer^fromSchema)
759
+ /// will be used. This object will only have its
760
+ /// [`serializeFragment`](#model.DOMSerializer.serializeFragment)
761
+ /// method called, and you may provide an alternative object type
762
+ /// implementing a compatible method.
763
+ clipboardSerializer?: DOMSerializer
764
+
765
+ /// A function that will be called to get the text for the current
766
+ /// selection when copying text to the clipboard. By default, the
767
+ /// editor will use [`textBetween`](#model.Node.textBetween) on the
768
+ /// selected range.
769
+ clipboardTextSerializer?: (this: P, content: Slice, view: EditorView) => string
770
+
771
+ /// A set of [document decorations](#view.Decoration) to show in the
772
+ /// view.
773
+ decorations?: (this: P, state: EditorState) => DecorationSource | null | undefined
774
+
775
+ /// When this returns false, the content of the view is not directly
776
+ /// editable.
777
+ editable?: (this: P, state: EditorState) => boolean
778
+
779
+ /// Control the DOM attributes of the editable element. May be either
780
+ /// an object or a function going from an editor state to an object.
781
+ /// By default, the element will get a class `"ProseMirror"`, and
782
+ /// will have its `contentEditable` attribute determined by the
783
+ /// [`editable` prop](#view.EditorProps.editable). Additional classes
784
+ /// provided here will be added to the class. For other attributes,
785
+ /// the value provided first (as in
786
+ /// [`someProp`](#view.EditorView.someProp)) will be used.
787
+ attributes?: {[name: string]: string} | ((state: EditorState) => {[name: string]: string})
788
+
789
+ /// Determines the distance (in pixels) between the cursor and the
790
+ /// end of the visible viewport at which point, when scrolling the
791
+ /// cursor into view, scrolling takes place. Defaults to 0.
792
+ scrollThreshold?: number | {top: number, right: number, bottom: number, left: number}
793
+
794
+ /// Determines the extra space (in pixels) that is left above or
795
+ /// below the cursor when it is scrolled into view. Defaults to 5.
796
+ scrollMargin?: number | {top: number, right: number, bottom: number, left: number}
797
+ }
798
+
799
+ /// The props object given directly to the editor view supports some
800
+ /// fields that can't be used in plugins:
801
+ export interface DirectEditorProps extends EditorProps {
802
+ /// The current state of the editor.
803
+ state: EditorState
804
+
805
+ /// A set of plugins to use in the view, applying their [plugin
806
+ /// view](#state.PluginSpec.view) and
807
+ /// [props](#state.PluginSpec.props). Passing plugins with a state
808
+ /// component (a [state field](#state.PluginSpec.state) field or a
809
+ /// [transaction](#state.PluginSpec.filterTransaction) filter or
810
+ /// appender) will result in an error, since such plugins must be
811
+ /// present in the state to work.
812
+ plugins?: readonly Plugin[]
813
+
814
+ /// The callback over which to send transactions (state updates)
815
+ /// produced by the view. If you specify this, you probably want to
816
+ /// make sure this ends up calling the view's
817
+ /// [`updateState`](#view.EditorView.updateState) method with a new
818
+ /// state that has the transaction
819
+ /// [applied](#state.EditorState.apply). The callback will be bound to have
820
+ /// the view instance as its `this` binding.
821
+ dispatchTransaction?: (tr: Transaction) => void
822
+ }
package/src/clipboard.ts CHANGED
@@ -212,7 +212,8 @@ function maybeWrapTrusted(html: string): string {
212
212
  // With the require-trusted-types-for CSP, Chrome will block
213
213
  // innerHTML, even on a detached document. This wraps the string in
214
214
  // a way that makes the browser allow us to use its parser again.
215
- if (!_policy) _policy = trustedTypes.createPolicy("ProseMirrorClipboard", {createHTML: (s: string) => s})
215
+ if (!_policy)
216
+ _policy = trustedTypes.defaultPolicy || trustedTypes.createPolicy("ProseMirrorClipboard", {createHTML: (s: string) => s})
216
217
  return _policy.createHTML(html)
217
218
  }
218
219
 
package/src/domchange.ts CHANGED
@@ -183,6 +183,7 @@ export function readDOMChange(view: EditorView, from: number, to: number, typeOv
183
183
  (!inlineChange || addedNodes.some(n => n.nodeName == "DIV" || n.nodeName == "P"))) ||
184
184
  (!inlineChange && $from.pos < parse.doc.content.size &&
185
185
  (!$from.sameParent($to) || !$from.parent.inlineContent) &&
186
+ !/\S/.test(parse.doc.textBetween($from.pos, $to.pos, "", "")) &&
186
187
  (nextSel = Selection.findFrom(parse.doc.resolve($from.pos + 1), 1, true)) &&
187
188
  nextSel.head > $from.pos)) &&
188
189
  view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) {
package/src/input.ts CHANGED
@@ -648,7 +648,6 @@ editHandlers.paste = (view, _event) => {
648
648
  if (view.composing && !browser.android) return
649
649
  let data = brokenClipboardAPI ? null : event.clipboardData
650
650
  let plain = view.input.shiftKey && view.input.lastKeyCode != 45
651
- console.log("paste event", !!data, view.state.selection.from, view.state.selection.to)
652
651
  if (data && doPaste(view, getText(data), data.getData("text/html"), plain, event))
653
652
  event.preventDefault()
654
653
  else