BlockNote
BlockNote copied to clipboard
(Question) Change numbering style of nested ordered lists
At https://github.com/TypeCellOS/BlockNote/issues/62, @brandondrew raised the issue of incorrect numbering.
https://github.com/TypeCellOS/BlockNote/pull/42 fixed the numbering, but it doesn't solve the other problem: sublists commonly have a different number style.
For example, a common numbered list shall look like this, which is more readable:
1. sth
a. sth
i. sth
ii. sth
1. sth
iii.sth
iv. sth
while current presentation is:
1. sth
1. sth
1. sth
2. sth
1. sth
3. sth
4. sth
common nested styles are:
1st | 2nd | 3rd | 4th |
---|---|---|---|
1. | a. | i. | 1. |
1. | 1) | a) | 1. |
1. | 1.1. | 1.1.1. | 1.1.1.1. |
Further more, there are multiple variants of one number style, which is useful in different scenarios. For example, a.
could also be a)
or (a)
.
If possible, consider implementing this, or provide a way to customize this behavior easily. Maybe do something like this:
function listNumberProvider(numeric: number, layer: number) {
if (layer % 3 === 2) {
return `${toAlphabet(numeric)}.`; // a.
} else if (layer % 3 === 0) {
return `${toRomeNumber(numeric)}.`; // i.
} else {
return `${numeric}.` // 1.
}
}
const editor: BlockNoteEditor = useBlockNote({
listNumberProvider: listNumberProvider
});
}
I'm just a user of this library, but implementing this to the core list block would make lists much more complex than they need to be for most users (I can only speak for myself). Writing a custom block using the core Blocknote ListBlock as a basis would be what I would do in the short term.
You can also just use the slash menu in any nested block too:
Just to offer some alternatives for you in the meantime.
I think this is a good suggestion! While customizing the numbering style for each nesting level is probably overkill, I agree that nested numbered lists should have distinct numbering styles.
Having this in the base NumberedListItemBlock
seems very useful. Unless I'm missing something, I don't think it's super easy to implement this even in a custom block today. If there's a path towards implementing it, I'd love to know so I can take a stab at it.
Yeah agreed it's not a super easy change, but definitely take a look at the NumberedListIndexingPlugin
. I think in theory you would just need to check the depth from blockInfo
and have a function which converts the regular index to/from the other formats, like roman numerals. Also had a quick look and the TipTap node doesn't care what the index
attribute is since it just stores, renders, and parses it, so doing e.g. index: "iii"
shouldn't cause any issues.
@matthewlipski I know a lot more about BlockNote now, so was more comfortable taking this on.
I'm not sure how to actually work in the dev version of the project. I was hoping to be able to write some tests or examples to test this feature, but I think they're based on published version of blocknote and not local.
I'm more than happy to work through putting this into BlockNote proper, or if you want to use the code that is also good with me.
A few notes on the code:
- I wasn't sure how to best pass the function into the plugin. Because I am overriding the plugin itself, I decided to put it into the closure. I don't think this would work in the main project.
- The plugin actually becomes simpler because we calculate the list index. This means that the requirement of firstInDoc + firstOfType is not really needed.
- getListCharacter could be default-included in this project. I kept it dependency free
- I could not use the existing index attribute to calculate "previous index", because it's no longer guaranteed to be a digit. I did not want to extend the existing types at all as part of this, and think the re-calculation of depth/index is probably good overall.
-
BUG: I realized that
depth
is not calculate properly here. It should be the depth of the list entry and NOT the depth relative to the document. I think that calculation would need to change to be accurate. Impact: a numeric list started at depth will not necessarily start at 1
// NumberListIndexingPlugin.ts
import { Plugin, PluginKey } from "prosemirror-state"
import { getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos"
// ProseMirror Plugin which automatically assigns indices to ordered list items per nesting level.
const PLUGIN_KEY = new PluginKey(`numbered-list-indexing`)
interface Options {
getListCharacter?: (positionDetails: { depth: number; index: number }) => string
}
const defaultGetListCharacter = (position: { index: number }) => position.index.toString()
export const NumberedListIndexingPlugin = (opts: Options = {}) => {
const getListCharacter = opts.getListCharacter || defaultGetListCharacter
return new Plugin({
key: PLUGIN_KEY,
appendTransaction: (_transactions, _oldState, newState) => {
const tr = newState.tr
tr.setMeta("numberedListIndexing", true)
let modified = false
// Traverses each node the doc using DFS, so blocks which are on the same nesting level will be traversed in the
// same order they appear. This means the index of each list item block can be calculated by incrementing the
// index of the previous list item block.
newState.doc.descendants((node, pos) => {
if (node.type.name === "blockContainer" && node.firstChild!.type.name === "numberedListItem") {
const blockInfo = getBlockInfoFromPos(tr.doc, pos + 1)!
if (blockInfo === undefined) {
return
}
// Divide by 2 because each block has a nesting around it
const depth = blockInfo.depth / 2
let firstListBlock = blockInfo
let blockIndex = 1
while (firstListBlock) {
const prevBlockInfo = getBlockInfoFromPos(tr.doc, firstListBlock.startPos - 2)!
if (
prevBlockInfo &&
prevBlockInfo.id !== firstListBlock.id &&
prevBlockInfo.depth === firstListBlock.depth &&
prevBlockInfo.contentType === firstListBlock.contentType
) {
blockIndex++
firstListBlock = prevBlockInfo
} else {
break
}
}
const newIndex = getListCharacter({ depth, index: blockIndex })
const contentNode = blockInfo.contentNode
const index = contentNode.attrs["index"]
if (index !== newIndex) {
modified = true
tr.setNodeMarkup(pos + 1, undefined, {
index: newIndex
})
}
}
})
return modified ? tr : null
}
})
}
// getListCharacter.ts
export function getListCharacter(positionDetails: { depth: number; index: number }) {
const style = (positionDetails.depth - 1) % 3
if (style === 1) {
return getColumnLetters(positionDetails.index)
} else if (style === 2) {
return romanize(positionDetails.index).toLowerCase()
} else {
return `${positionDetails.index}`
}
}
// from https://dev.to/all_stacks_developer/how-to-convert-column-index-of-a-spreadsheet-into-letters-31k0
// prettier-ignore
const ALPHABET = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
function getColumnLetters(columnIndexStartFromOne: number): string {
if (columnIndexStartFromOne < 27) {
return ALPHABET[columnIndexStartFromOne - 1]
} else {
var res = columnIndexStartFromOne % 26
var div = Math.floor(columnIndexStartFromOne / 26)
if (res === 0) {
div = div - 1
res = 26
}
return getColumnLetters(div) + ALPHABET[res - 1]
}
}
// from https://stackoverflow.com/questions/9083037/convert-a-number-into-a-roman-numeral-in-javascript
function romanize(num: number): string {
const digits = `${num}`.split("")
// prettier-ignore
const key = ["","C","CC","CCC","CD","D","DC","DCC","DCCC","CM",
"","X","XX","XXX","XL","L","LX","LXX","LXXX","XC",
"","I","II","III","IV","V","VI","VII","VIII","IX"]
let roman = ""
let i = 3
while (i--) roman = (key[+digits.pop()! + i * 10] || "") + roman
return Array(+digits.join("") + 1).join("M") + roman
}
// editor.tsx
defaultBlockSpecs.numberedListItem.implementation.node.config.addProseMirrorPlugins = () => {
return [
NumberedListIndexingPlugin({
getListCharacter
})
]
}
This updated version handles broken lists properly, by finding the closest parent of the same type: https://gist.github.com/sb8244/c59acf0836eda8c79852e623afe936d1