mirror of
https://github.com/outline/outline.git
synced 2025-12-30 07:19:52 -06:00
feat: Ordered alphabetical lists (#10079)
* feat: letter-list * simplify list toggle * use more common shortcuts for list toggle * fix toggle list conflict * wrap letter index to avoid overflow * ensure the markdown letter representation matches the css representation on overflow * improve list style validation * fix list indexing * fix: Toggling ordered lists from formatting menu * fix: Ordered list in block menu --------- Co-authored-by: Tom Moor <tom@getoutline.com>
This commit is contained in:
@@ -8,7 +8,8 @@ import clearNodes from "./clearNodes";
|
||||
|
||||
export default function toggleList(
|
||||
listType: NodeType,
|
||||
itemType: NodeType
|
||||
itemType: NodeType,
|
||||
listStyle?: string
|
||||
): Command {
|
||||
return (state, dispatch) => {
|
||||
const { schema, selection } = state;
|
||||
@@ -25,36 +26,49 @@ export default function toggleList(
|
||||
);
|
||||
|
||||
if (range.depth >= 1 && parentList && range.depth - parentList.depth <= 1) {
|
||||
if (parentList.node.type === listType) {
|
||||
const currentStyle = parentList.node.attrs.listStyle;
|
||||
const differentListStyle = currentStyle && currentStyle !== listStyle;
|
||||
|
||||
if (
|
||||
parentList.node.type === listType &&
|
||||
(!differentListStyle || !listStyle)
|
||||
) {
|
||||
return liftListItem(itemType)(state, dispatch);
|
||||
}
|
||||
|
||||
const currentItemType = parentList.node.content.firstChild?.type;
|
||||
if (currentItemType && currentItemType !== itemType) {
|
||||
return chainTransactions(clearNodes(), wrapInList(listType))(
|
||||
state,
|
||||
dispatch
|
||||
);
|
||||
const differentType = currentItemType && currentItemType !== itemType;
|
||||
|
||||
if (differentType || differentListStyle) {
|
||||
return chainTransactions(
|
||||
clearNodes(),
|
||||
wrapInList(listType, { listStyle })
|
||||
)(state, dispatch);
|
||||
}
|
||||
|
||||
if (
|
||||
isList(parentList.node, schema) &&
|
||||
listType.validContent(parentList.node.content)
|
||||
) {
|
||||
tr.setNodeMarkup(parentList.pos, listType);
|
||||
tr.setNodeMarkup(
|
||||
parentList.pos,
|
||||
listType,
|
||||
listStyle ? { listStyle } : {}
|
||||
);
|
||||
|
||||
dispatch?.(tr);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const canWrapInList = wrapInList(listType)(state);
|
||||
const attrs = listStyle ? { listStyle } : undefined;
|
||||
const canWrapInList = wrapInList(listType, attrs)(state);
|
||||
|
||||
if (canWrapInList) {
|
||||
return wrapInList(listType)(state, dispatch);
|
||||
return wrapInList(listType, attrs)(state, dispatch);
|
||||
}
|
||||
|
||||
return chainTransactions(clearNodes(), wrapInList(listType))(
|
||||
return chainTransactions(clearNodes(), wrapInList(listType, attrs))(
|
||||
state,
|
||||
dispatch
|
||||
);
|
||||
|
||||
@@ -22,6 +22,11 @@ export default class OrderedList extends Node {
|
||||
default: 1,
|
||||
validate: "number",
|
||||
},
|
||||
listStyle: {
|
||||
default: "number",
|
||||
validate: (style: string) =>
|
||||
["number", "lower-alpha", "upper-alpha"].includes(style),
|
||||
},
|
||||
},
|
||||
content: "list_item+",
|
||||
group: "block list",
|
||||
@@ -32,23 +37,43 @@ export default class OrderedList extends Node {
|
||||
order: dom.hasAttribute("start")
|
||||
? parseInt(dom.getAttribute("start") || "1", 10)
|
||||
: 1,
|
||||
listStyle: dom.style.listStyleType,
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: (node) =>
|
||||
node.attrs.order === 1
|
||||
? ["ol", 0]
|
||||
: ["ol", { start: node.attrs.order }, 0],
|
||||
toDOM: (node) => {
|
||||
const attrs: { start?: number; style?: string } = {};
|
||||
|
||||
if (node.attrs.order !== 1) {
|
||||
attrs.start = node.attrs.order;
|
||||
}
|
||||
|
||||
if (node.attrs.listStyle !== "number") {
|
||||
attrs.style = `list-style-type: ${node.attrs.listStyle}`;
|
||||
}
|
||||
|
||||
return ["ol", attrs, 0];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
commands({ type, schema }: { type: NodeType; schema: Schema }) {
|
||||
return () => toggleList(type, schema.nodes.list_item);
|
||||
return {
|
||||
ordered_list: () => toggleList(type, schema.nodes.list_item),
|
||||
|
||||
toggleLowerLetterList: () =>
|
||||
toggleList(type, schema.nodes.list_item, "lower-alpha"),
|
||||
|
||||
toggleUpperLetterList: () =>
|
||||
toggleList(type, schema.nodes.list_item, "upper-alpha"),
|
||||
};
|
||||
}
|
||||
|
||||
keys({ type, schema }: { type: NodeType; schema: Schema }) {
|
||||
return {
|
||||
"Shift-Ctrl-9": toggleList(type, schema.nodes.list_item),
|
||||
"Shift-Ctrl-5": this.commands({ type, schema }).toggleUpperLetterList(),
|
||||
"Shift-Ctrl-6": this.commands({ type, schema }).toggleLowerLetterList(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,9 +82,39 @@ export default class OrderedList extends Node {
|
||||
wrappingInputRule(
|
||||
/^(\d+)\.\s$/,
|
||||
type,
|
||||
(match) => ({ order: +match[1] }),
|
||||
(match) => ({ order: +match[1], listStyle: "number" }),
|
||||
(match, node) => node.childCount + node.attrs.order === +match[1]
|
||||
),
|
||||
|
||||
wrappingInputRule(
|
||||
/^([a-z])\.\s$/,
|
||||
type,
|
||||
(match) => ({
|
||||
order: match[1].charCodeAt(0) - 96,
|
||||
listStyle: "lower-alpha",
|
||||
}),
|
||||
(match, node) => {
|
||||
const expectedChar = String.fromCharCode(
|
||||
96 + node.childCount + node.attrs.order
|
||||
);
|
||||
return expectedChar === match[1];
|
||||
}
|
||||
),
|
||||
|
||||
wrappingInputRule(
|
||||
/^([A-Z])\.\s$/,
|
||||
type,
|
||||
(match) => ({
|
||||
order: match[1].charCodeAt(0) - 64,
|
||||
listStyle: "upper-alpha",
|
||||
}),
|
||||
(match, node) => {
|
||||
const expectedChar = String.fromCharCode(
|
||||
64 + node.childCount + node.attrs.order
|
||||
);
|
||||
return expectedChar === match[1];
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -67,21 +122,59 @@ export default class OrderedList extends Node {
|
||||
state.write("\n");
|
||||
|
||||
const start = node.attrs.order !== undefined ? node.attrs.order : 1;
|
||||
const maxW = `${start + node.childCount - 1}`.length;
|
||||
const space = state.repeat(" ", maxW + 2);
|
||||
const upperOrLowerAlpha =
|
||||
node.attrs.listStyle === "lower-alpha" ||
|
||||
node.attrs.listStyle === "upper-alpha";
|
||||
|
||||
state.renderList(node, space, (index: number) => {
|
||||
const nStr = `${start + index}`;
|
||||
return state.repeat(" ", maxW - nStr.length) + nStr + ". ";
|
||||
});
|
||||
if (upperOrLowerAlpha) {
|
||||
const space = state.repeat(" ", 4);
|
||||
|
||||
state.renderList(node, space, (index: number) => {
|
||||
const alphabetSize = 26;
|
||||
const position = start + index;
|
||||
const asciiStart = node.attrs.listStyle === "lower-alpha" ? 97 : 65;
|
||||
|
||||
let n = position - 1;
|
||||
let result = "";
|
||||
|
||||
do {
|
||||
const charCode = asciiStart + (n % alphabetSize);
|
||||
result = String.fromCharCode(charCode) + result;
|
||||
|
||||
n = Math.floor(n / alphabetSize) - 1;
|
||||
} while (n >= 0);
|
||||
|
||||
return result + ". ";
|
||||
});
|
||||
} else {
|
||||
const maxW = `${start + node.childCount - 1}`.length;
|
||||
const space = state.repeat(" ", maxW + 2);
|
||||
|
||||
state.renderList(node, space, (index: number) => {
|
||||
const nStr = `${start + index}`;
|
||||
return state.repeat(" ", maxW - nStr.length) + nStr + ". ";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
parseMarkdown() {
|
||||
return {
|
||||
block: "ordered_list",
|
||||
getAttrs: (tok: Token) => ({
|
||||
order: parseInt(tok.attrGet("start") || "1", 10),
|
||||
}),
|
||||
getAttrs: (tok: Token) => {
|
||||
const start = tok.attrGet("start") || "1";
|
||||
|
||||
let listStyle = "number";
|
||||
if (tok.markup && /^[a-z]/.test(tok.markup)) {
|
||||
listStyle = "lower-alpha";
|
||||
} else if (tok.markup && /^[A-Z]/.test(tok.markup)) {
|
||||
listStyle = "upper-alpha";
|
||||
}
|
||||
|
||||
return {
|
||||
order: parseInt(start, 10),
|
||||
listStyle,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user