Skip to content

Commit

Permalink
Merge pull request #1328 from microsoft/u/juliaroldi/trigger-list-types
Browse files Browse the repository at this point in the history
Fix list trigger
  • Loading branch information
juliaroldi authored Oct 17, 2022
2 parents f973090 + f4dd769 commit 69bec31
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 31 deletions.
5 changes: 4 additions & 1 deletion packages/roosterjs-editor-api/lib/utils/toggleListType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ export default function toggleListType(
)?.collapseToSingleElement(),
startNumber
)
: createVListFromRegion(region, includeSiblingLists);
: createVListFromRegion(
region,
startNumber === 1 ? false : includeSiblingLists
);

if (vList) {
vList.changeListType(start, end, listType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import {
createVListFromRegion,
isBlockElement,
cacheGetEventData,
createObjectDefinition,
createNumberDefinition,
getMetadata,
} from 'roosterjs-editor-dom';
import {
BuildInEditFeature,
Expand All @@ -30,8 +33,34 @@ import {
RegionBase,
ListType,
ExperimentalFeatures,
NumberingListType,
BulletListType,
} from 'roosterjs-editor-types';

interface ListStyleMetadata {
orderedStyleType?: NumberingListType;
unorderedStyleType?: BulletListType;
}

const ListStyleDefinitionMetadata = createObjectDefinition<ListStyleMetadata>(
{
orderedStyleType: createNumberDefinition(
true /** isOptional */,
undefined /** value **/,
NumberingListType.Min,
NumberingListType.Max
),
unorderedStyleType: createNumberDefinition(
true /** isOptional */,
undefined /** value **/,
BulletListType.Min,
BulletListType.Max
),
},
true /** isOptional */,
true /** allowNull */
);

/**
* IndentWhenTab edit feature, provides the ability to indent current list when user press TAB
*/
Expand Down Expand Up @@ -220,7 +249,7 @@ const AutoBulletList: BuildInEditFeature<PluginKeyboardEvent> = {
!cacheGetListElement(event, editor) &&
editor.isFeatureEnabled(ExperimentalFeatures.AutoFormatList)
) {
return shouldTriggerList(event, editor, getAutoBulletListStyle);
return shouldTriggerList(event, editor, getAutoBulletListStyle, ListType.Unordered);
}
return false;
},
Expand Down Expand Up @@ -257,7 +286,7 @@ const AutoNumberingList: BuildInEditFeature<PluginKeyboardEvent> = {
!cacheGetListElement(event, editor) &&
editor.isFeatureEnabled(ExperimentalFeatures.AutoFormatList)
) {
return shouldTriggerList(event, editor, getAutoNumberingListStyle);
return shouldTriggerList(event, editor, getAutoNumberingListStyle, ListType.Ordered);
}
return false;
},
Expand All @@ -271,18 +300,16 @@ const AutoNumberingList: BuildInEditFeature<PluginKeyboardEvent> = {
const textRange = searcher.getRangeFromText(textBeforeCursor, true /*exactMatch*/);

if (textRange) {
const number = parseInt(textBeforeCursor);
const previousNode = editor
.getBodyTraverser(textRange.startContainer)
.getPreviousBlockElement();
const isLi = previousNode
? getTagOfNode(previousNode?.collapseToSingleElement()) === 'LI'
: false;
const number = isFirstItemOfAList(textBeforeCursor)
? 1
: parseInt(textBeforeCursor);

const isLi = getPreviousList(editor, textRange);
const listStyle = getAutoNumberingListStyle(textBeforeCursor);
prepareAutoBullet(editor, textRange);
toggleNumbering(
editor,
isLi ? undefined : number /** startNumber */,
isLi && number !== 1 ? undefined : number /** startNumber */,
listStyle,
'autoToggleList' /** apiNameOverride */
);
Expand All @@ -295,6 +322,33 @@ const AutoNumberingList: BuildInEditFeature<PluginKeyboardEvent> = {
},
};

const getPreviousList = (editor: IEditor, textRange: Range) => {
const previousNode = editor
.getBodyTraverser(textRange?.startContainer)
.getPreviousBlockElement()
?.collapseToSingleElement();
return getTagOfNode(previousNode) === 'LI' ? previousNode : undefined;
};

const getPreviousListType = (editor: IEditor, textRange: Range, listType: ListType) => {
const type = listType === ListType.Ordered ? 'orderedStyleType' : 'unorderedStyleType';
const previousNode = getPreviousList(editor, textRange);

return previousNode && getTagOfNode(previousNode) === 'LI'
? getMetadata(previousNode.parentElement, ListStyleDefinitionMetadata)[type]
: null;
};

const isFirstItemOfAList = (item: string) => {
const number = parseInt(item);
if (number && number === 1) {
return 1;
} else {
const letter = item.replace(/\(|\)|\-|\./g, '').trim();
return letter.length === 1 && ['i', 'a', 'I', 'A'].indexOf(letter) > -1 ? 1 : undefined;
}
};

/**
* Maintain the list numbers in list chain
* e.g. we have two lists:
Expand Down Expand Up @@ -373,16 +427,32 @@ function cacheGetListElement(event: PluginKeyboardEvent, editor: IEditor) {
function shouldTriggerList(
event: PluginKeyboardEvent,
editor: IEditor,
getListStyle: (text: string, previousListChain?: VListChain[]) => number
getListStyle: (
text: string,
previousListChain?: VListChain[],
previousListStyle?: NumberingListType | BulletListType
) => number,
listType: ListType
) {
const searcher = editor.getContentSearcherOfCursor(event);
const textBeforeCursor = searcher.getSubStringBefore(4);
const itHasSpace = /\s/g.test(textBeforeCursor);
const listChains = getListChains(editor);
const textRange = searcher.getRangeFromText(textBeforeCursor, true /*exactMatch*/);
const previousListType = getPreviousListType(editor, textRange, listType);
const isFirstItem = isFirstItemOfAList(textBeforeCursor);
const listStyle = getListStyle(textBeforeCursor, listChains, previousListType);
const shouldTriggerNewListStyle =
isFirstItem ||
!previousListType ||
previousListType === listStyle ||
listType === ListType.Unordered;

return (
!itHasSpace &&
!searcher.getNearestNonTextInlineElement() &&
getListStyle(textBeforeCursor, listChains)
listStyle &&
shouldTriggerNewListStyle
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,43 @@ const characters: Record<string, number> = {
')': Character.Parenthesis,
};

const lowerRomanTypes = [
NumberingListType.LowerRoman,
NumberingListType.LowerRomanDash,
NumberingListType.LowerRomanDoubleParenthesis,
NumberingListType.LowerRomanParenthesis,
];
const upperRomanTypes = [
NumberingListType.UpperRoman,
NumberingListType.UpperRomanDash,
NumberingListType.UpperRomanDoubleParenthesis,
NumberingListType.UpperRomanParenthesis,
];
const numberingTriggers = ['1', 'a', 'A', 'I', 'i'];
const lowerRomanNumbers = ['i', 'v', 'x', 'l', 'c', 'd', 'm'];
const upperRomanNumbers = ['I', 'V', 'X', 'L', 'C', 'D', 'M'];

const identifyNumberingType = (text: string) => {
const identifyNumberingType = (text: string, previousListStyle?: NumberingListType) => {
if (!isNaN(parseInt(text))) {
return NumberingTypes.Decimal;
} else if (/[a-z]+/g.test(text)) {
if (text === 'i') {
if (
(lowerRomanTypes.indexOf(previousListStyle) > -1 &&
lowerRomanNumbers.indexOf(text[0]) > -1) ||
(!previousListStyle && text === 'i')
) {
return NumberingTypes.LowerRoman;
} else {
} else if (previousListStyle || (!previousListStyle && text === 'a')) {
return NumberingTypes.LowerAlpha;
}
} else if (/[A-Z]+/g.test(text)) {
if (text === 'I') {
if (
(upperRomanTypes.indexOf(previousListStyle) > -1 &&
upperRomanNumbers.indexOf(text[0]) > -1) ||
(!previousListStyle && text === 'I')
) {
return NumberingTypes.UpperRoman;
} else {
} else if (previousListStyle || (!previousListStyle && text === 'A')) {
return NumberingTypes.UpperAlpha;
}
}
Expand Down Expand Up @@ -87,15 +109,16 @@ const DecimalsTypes: Record<number, number> = {

const identifyNumberingListType = (
numbering: string,
isDoubleParenthesis: boolean
isDoubleParenthesis: boolean,
previousListStyle?: NumberingListType
): NumberingListType | null => {
const separatorCharacter = isDoubleParenthesis
? Character.DoubleParenthesis
: characters[numbering[1]];
: characters[numbering[numbering.length - 1]];
// if separator is not valid, no need to check if the number is valid.
if (separatorCharacter) {
const number = numbering[numbering.length - 2];
const numberingType = identifyNumberingType(number);
const number = isDoubleParenthesis ? numbering.slice(1, -1) : numbering.slice(0, -1);
const numberingType = identifyNumberingType(number, previousListStyle);
return numberingType ? numberingListTypes[numberingType](separatorCharacter) : null;
}
return null;
Expand All @@ -104,12 +127,14 @@ const identifyNumberingListType = (
/**
* @internal
* @param textBeforeCursor The trigger character
* @param previousListChain (Optional) This parameters is used to keep the list chain, if the is not a new list
* @param previousListChain @optional This parameters is used to keep the list chain, if the is not a new list
* @param previousListStyle @optional The list style of the previous list
* @returns The style of a numbering list triggered by a string
*/
export default function getAutoNumberingListStyle(
textBeforeCursor: string,
previousListChain?: VListChain[]
previousListChain?: VListChain[],
previousListStyle?: NumberingListType
): NumberingListType {
const trigger = textBeforeCursor.trim();
//Only the staring items ['1', 'a', 'A', 'I', 'i'] must trigger a new list. All the other triggers is used to keep the list chain.
Expand All @@ -127,12 +152,11 @@ export default function getAutoNumberingListStyle(
}
}

// the marker must be a combination of 2 or 3 characters, so if the length is less than 2, no need to check
// If the marker length is 3, the marker style is double parenthesis such as (1), (A).
const isDoubleParenthesis = trigger.length === 3 && trigger[0] === '(' && trigger[2] === ')';
const numberingType =
trigger.length === 2 || isDoubleParenthesis
? identifyNumberingListType(trigger, isDoubleParenthesis)
: null;
const isDoubleParenthesis = trigger[0] === '(' && trigger[trigger.length - 1] === ')';
const numberingType = identifyNumberingListType(
trigger,
isDoubleParenthesis,
previousListStyle
);
return numberingType;
}

0 comments on commit 69bec31

Please sign in to comment.