Hi, I am studying Prosemirror, testing the examples and guidance, Tooltip and dispatchTransaction.
I found out that dispatchTransaction was called before plugin’s update, which stopped Tooltip example from working, because the code of dispatchTransaction example here
whould make the code of Tooltip here always return, as lastState.selection.eq(state.selection) will always be true, making the lastState selection always equals to state.selection.
if (lastState && lastState.doc.eq(state.doc) &&
lastState.selection.eq(state.selection)) return
Shouldn’t plugins’ update be called before dispatchTransaction?
I understand this logic. It could be a bug with lastState and current state’s selection processing code.
When I combined the dispatchTransaction example in your guidance and also the tooltip example, I found that that dispatchTransaction is called before plugin’s update, the code
if (event.type == "EDITOR_TRANSACTION") {
editorView.state = editorView.state.apply(event.transaction)
would make this test in your tooltip example always return true when selection is changed:
if (lastState && lastState.doc.eq(state.doc) &&
lastState.selection.eq(state.selection))
I added a console.log in tooltip’s update method to verify and it confirms that lastState.selection always equal to state.selection.
if( lastState && lastState.selection )
console.log( lastState.selection.eq(state.selection) ) //always print true when selection is changed.
if (lastState && lastState.doc.eq(state.doc) &&
lastState.selection.eq(state.selection)) return
The lastState passed to the plugin’s update method comes from the editor view, which won’t have the new state yet no matter what dispatchTransaction is doing, so I really think you’re misdiagnosing this.
Let me post the whole code of update from the tooltip example as below. This line: if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return checks if the selection has changed. If not, then it does nothing, i.e. not to update tooltip.
It is quite clear that lastState should be previous state if selection has changed, while ‘view.state’ should be the new state. With dispatchTransaction called, 'lastState.selectionis always the same toview.state.selection`, even when selection has changed. That’s why tooltip example stopped working when dispatchTransaction example was merged with tooltip example. I don’t think this is right.
update(view, lastState) {
let state = view.state
// Don't do anything if the document/selection didn't change
if (lastState && lastState.doc.eq(state.doc) &&
lastState.selection.eq(state.selection)) return
// Hide the tooltip if the selection is empty
if (state.selection.empty) {
this.tooltip.style.display = "none"
return
}
// Otherwise, reposition it and update its content
this.tooltip.style.display = ""
let {from, to} = state.selection
// These are in screen coordinates
let start = view.coordsAtPos(from), end = view.coordsAtPos(to)
// The box in which the tooltip is positioned, to use as base
let box = this.tooltip.offsetParent.getBoundingClientRect()
// Find a center-ish x position from the selection endpoints (when
// crossing lines, end may be more to the left)
let left = Math.max((start.left + end.left) / 2, start.left + 3)
this.tooltip.style.left = (left - box.left) + "px"
this.tooltip.style.bottom = (box.bottom - start.top) + "px"
this.tooltip.textContent = to - from
}
destroy() { this.tooltip.remove() }
}
Below code integrates dispatchTransaction example and Tooltip example. Tooltip stopped working because
in update, this test lastState.selection.eq(state.selection) is always true when selection is changed. I believe it is a bug of Prosemirror that after dispatchTransaction is called, when plugin’s update is called, either lastState is not the old state or view.state is not new state as they are supposed to be.
import { EditorState,Plugin } from "prosemirror-state"
import { EditorView } from "prosemirror-view"
import { Schema, DOMParser } from "prosemirror-model"
import { schema } from "prosemirror-schema-basic"
import { addListNodes } from "prosemirror-schema-list"
import menu from './plugins/qieditor-menu'
import {exampleSetup} from "prosemirror-example-setup"
// Mix the nodes from prosemirror-schema-list into the basic schema to
// create a schema with list support.
const mySchema = new Schema({
nodes: addListNodes(schema.spec.nodes as any, "paragraph block*", "block"),
marks: schema.spec.marks
})
const doc = DOMParser.fromSchema(mySchema).parse(document.querySelector("#content"))
/**
* setup plugins
*
*/
let selectionSizePlugin = new Plugin({
view(editorView) { return new SelectionSizeTooltip(editorView) }
})
class SelectionSizeTooltip {
tooltip: any
constructor(view: EditorView) {
this.tooltip = document.createElement("div")
this.tooltip.className = "tooltip"
view.dom.parentNode.appendChild(this.tooltip)
this.update(view, null)
}
update(view: EditorView, lastState:EditorState) {
let state = view.state
// Don't do anything if the document/selection didn't change
if( lastState && lastState.selection )
console.log( lastState.selection.eq(state.selection) ) //aways print true.
if (lastState && lastState.doc.eq(state.doc) &&
lastState.selection.eq(state.selection)) return
// Hide the tooltip if the selection is empty
if (state.selection.empty) {
this.tooltip.style.display = "none"
return
}
console.log("updating tooltip")
// Otherwise, reposition it and update its content
this.tooltip.style.display = ""
let {from, to} = state.selection
// These are in screen coordinates
let start = view.coordsAtPos(from), end = view.coordsAtPos(to)
// The box in which the tooltip is positioned, to use as base
let box = this.tooltip.offsetParent.getBoundingClientRect()
// Find a center-ish x position from the selection endpoints (when
// crossing lines, end may be more to the left)
let left = Math.max((start.left + end.left) / 2, start.left + 3)
this.tooltip.style.left = (left - box.left) + "px"
this.tooltip.style.bottom = (box.bottom - start.top) + "px"
this.tooltip.textContent = to - from
}
destroy() { this.tooltip.remove() }
}
const plugins = exampleSetup({ schema: mySchema }).concat(selectionSizePlugin)
/**
* Create editor view and mount to node
*
*/
const editorArea = "#editorArea"
const editorView = new EditorView(document.querySelector(editorArea), {
state: EditorState.create({
doc,
plugins
}),
dispatchTransaction(transaction) {
update({type: "EDITOR_TRANSACTION", transaction})
}
})
/**
* Update the whole application's state
* Do post transaction processing.
* @param {any} event - Applicationevent
*/
function update(event: any) {
if (event.type == "EDITOR_TRANSACTION") {
editorView.state = editorView.state.apply(event.transaction)
}
// else if (event.type == "SCORE_POINT")
// appState.score++
draw()
}
/**
* Update editor view
*
*/
function draw() {
editorView.updateState(editorView.state)
}
I got it. Thanks. But the code is from your guidance section data flow. Suggest to update it.
// A crude app state update function, which takes an update object,
// updates the `appState`, and then refreshes the UI.
function update(event) {
if (event.type == "EDITOR_TRANSACTION")
appState.editor = appState.editor.apply(event.transaction)
else if (event.type == "SCORE_POINT")
appState.score++
draw()
}
Yes technically you are right. But wouldn’t it be more helpful to users of your library in your guide to use the recommended way to update the state inside dispatchTransaction?