How to autoJoin all the time?

For example, given the following text:

  1. List item

non list

  1. List item again

If the user delete the whole line of “non list”, I would like both list items to be merged into one.

There’s an autoJoin function that I can use to wrap most of the commands, e.g. backspace. However, it doesn’t seem to handle all of the cases. For example, cut with ctrl+x doesn’t seem to be handled by the keymap, and I probably don’t want to override the default browser behavior. Similarly, what if “non list” is actually some image block that the user can drag away, causing the 2 lists to be merged together?

Will the right approach here be writing a plugin that does ‘appendTransaction’ to normalized all disjoined lists?

2 Likes

Yes.

1 Like

Did anyone figure out how to use autoJoin inside appendTransaction?

appendTransaction: (transactions, oldState, newState) => {
	for (const transaction of transactions) {
		autoJoin( // hmm...

Here’s a working solution :slight_smile:

import { Transaction, Plugin } from "prosemirror-state"
import { NodeType } from "prosemirror-model"
import { canJoin } from "prosemirror-transform"

// Ripped out from prosemirror-commands wrapDispatchForJoin
function autoJoin(
	tr: Transaction, // An old transaction
	newTr: Transaction, // The latest state
	nodeType: NodeType // The node type to join
) {
	if (!tr.isGeneric) return false

	// Find all ranges where we might want to join.
	let ranges: Array<number> = []
	for (let i = 0; i < tr.mapping.maps.length; i++) {
		let map = tr.mapping.maps[i]
		for (let j = 0; j < ranges.length; j++) ranges[j] = map.map(ranges[j])
		map.forEach((_s, _e, from, to) => ranges.push(from, to))
	}

	// Figure out which joinable points exist inside those ranges,
	// by checking all node boundaries in their parent nodes.
	let joinable = []
	for (let i = 0; i < ranges.length; i += 2) {
		let from = ranges[i],
			to = ranges[i + 1]
		let $from = tr.doc.resolve(from),
			depth = $from.sharedDepth(to),
			parent = $from.node(depth)
		for (
			let index = $from.indexAfter(depth), pos = $from.after(depth + 1);
			pos <= to;
			++index
		) {
			let after = parent.maybeChild(index)
			if (!after) break
			if (index && joinable.indexOf(pos) == -1) {
				let before = parent.child(index - 1)
				if (before.type == after.type && before.type === nodeType)
					joinable.push(pos)
			}
			pos += after.nodeSize
		}
	}

	let joined = false

	// Join the joinable points
	joinable.sort((a, b) => a - b)
	for (let i = joinable.length - 1; i >= 0; i--) {
		if (canJoin(tr.doc, joinable[i])) {
			newTr.join(joinable[i])
			joined = true
		}
	}

	return joined
}

export class AutojoinPlugin extends Plugin {
	constructor(nodeType: NodeType) {
		super({
			appendTransaction(transactions, oldState, newState) {
				// Create a new transaction.
				let newTr = newState.tr

				let joined = false
				for (const transaction of transactions) {
					const anotherJoin = autoJoin(transaction, newTr, nodeType)
					joined = anotherJoin || joined
				}
				if (joined) {
					return newTr
				}
			},
		})
	}
}