toggleMark with attrs

Can toggleMark be changed so that if attrs is given, the mark with the same attributes is toggled ?

Consider for example a mark that sets classes like in How do I restrict the types of tags and classes allowed? and consider menu buttons for toggling the different attributes of the mark (r/g/b), you want toogleMark for the ‘r’ mark to remove marks for ‘g’ and ‘b’ but add the mark with the attributes for ‘r’.

This should work both for selection or the current set.

Any thoughts ?

You’ll have to implement your own version. In general, don’t treat the bundle of comments that are part of the core libraries as the whole story – these are just some common ones, but in order to do specialized things, you’ll have to write specialized commands.

ended up with this which seems to work:

function toggleMark(markType, attrs) {
	function shallowEqual(objA, objB) {
		if (Object.is(objA, objB)) return true;
		if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) return false;
		const keysA = Object.keys(objA),
			keysB = Object.keys(objB);
		if (keysA.length !== keysB.length) return false;
		for (var i = 0; i < keysA.length; i++)
			if (!hasOwnProperty.call(objB, keysA[i]) || !Object.is(objA[keysA[i]], objB[keysA[i]])) return false;
		return true;
	}

	function markApplies(doc, from, to, type) {
		let can = doc.contentMatchAt(0).allowsMark(type)
		doc.nodesBetween(from, to, node => {
			if (can) return false
			can = node.inlineContent && node.contentMatchAt(0).allowsMark(type)
		})
		return can
	}

	// return true iff all nodes in range have the mark with the same attrs
	function rangeHasMark(doc, from, to, type, attrs) {
		let hasMark = null
		doc.nodesBetween(from, to, node => {
			for (let i = 0; i < node.marks.length; i++) {
				let markMatch = node.marks[i].type == type && (!attrs || shallowEqual(node.marks[i].attrs, attrs))
				hasMark = (markMatch && (hasMark === null || hasMark === true));
			}
			return hasMark
		})
		return !!hasMark
	}

	return function(state, dispatch) {
		let { empty, from, to, $from } = state.selection
		if (!markApplies(state.doc, from, to, markType)) return false
		if (dispatch) {
			if (empty) {
				const markInSet = markType.isInSet(state.storedMarks || $from.marks())
				if (markInSet && (!attrs || shallowEqual(markInSet.attrs, attrs))) {
					dispatch(state.tr.removeStoredMark(markType))
				} else {
					dispatch(state.tr.addStoredMark(markType.create(attrs)))
				}
			} else {
				if (rangeHasMark(state.doc, from, to, markType, attrs)) {
					dispatch(state.tr.removeMark(from, to, markType).scrollIntoView())
				} else {
					dispatch(state.tr.addMark(from, to, markType.create(attrs)).scrollIntoView())
				}
			}
		}
		return true
	}
}