mirror of
https://github.com/facebook/lexical.git
synced 2025-08-26 10:40:47 +08:00
Revert "After triggering markdown to bold text, the caret should appear after the space character. (#1293)" (#1303)
This reverts commit e217ca81131813034ab71bca8f9d26108c89742b.
This commit is contained in:

committed by
acywatson

parent
864d518462
commit
c9fbe99309
@ -118,40 +118,28 @@ export function $findNodeWithOffsetFromJoinedText(
|
|||||||
const children = elementNode.getChildren();
|
const children = elementNode.getChildren();
|
||||||
const childrenLength = children.length;
|
const childrenLength = children.length;
|
||||||
let runningLength = 0;
|
let runningLength = 0;
|
||||||
let isPriorNodeTextNode = false;
|
|
||||||
for (let i = 0; i < childrenLength; ++i) {
|
for (let i = 0; i < childrenLength; ++i) {
|
||||||
// We must examine the offsetInJoinedText that is located
|
if (runningLength >= joinedTextLength) {
|
||||||
// at the length of the string.
|
|
||||||
// For example, given "hello", the length is 5, yet
|
|
||||||
// the caller still wants the node + offset at the
|
|
||||||
// right edge of the "o".
|
|
||||||
if (runningLength > joinedTextLength) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const child = children[i];
|
const child = children[i];
|
||||||
const isChildNodeTestNode = $isTextNode(child);
|
const childContentLength = $isTextNode(child)
|
||||||
const childContentLength = isChildNodeTestNode
|
|
||||||
? child.getTextContent().length
|
? child.getTextContent().length
|
||||||
: separatorLength;
|
: separatorLength;
|
||||||
|
|
||||||
const newRunningLength = runningLength + childContentLength;
|
const newRunningLength = runningLength + childContentLength;
|
||||||
|
if (
|
||||||
const isJoinedOffsetWithinNode =
|
runningLength <= offsetInJoinedText &&
|
||||||
(isPriorNodeTextNode === false && runningLength === offsetInJoinedText) ||
|
offsetInJoinedText < newRunningLength &&
|
||||||
(runningLength === 0 && runningLength === offsetInJoinedText) ||
|
$isTextNode(child)
|
||||||
(runningLength < offsetInJoinedText &&
|
) {
|
||||||
offsetInJoinedText <= newRunningLength);
|
|
||||||
|
|
||||||
if (isJoinedOffsetWithinNode && $isTextNode(child)) {
|
|
||||||
// Check isTextNode again for flow.
|
|
||||||
return {
|
return {
|
||||||
node: child,
|
node: child,
|
||||||
offset: offsetInJoinedText - runningLength,
|
offset: offsetInJoinedText - runningLength,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
runningLength = newRunningLength;
|
runningLength = newRunningLength;
|
||||||
isPriorNodeTextNode = isChildNodeTestNode;
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@ function useTypeahead(editor: LexicalEditor): void {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'history-merge',
|
tag: 'without-history',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}, [editor, getTypeaheadTextNode, selectionCollapsed, suggestion]);
|
}, [editor, getTypeaheadTextNode, selectionCollapsed, suggestion]);
|
||||||
|
@ -53,7 +53,6 @@ export type AutoFormatTriggerState = $ReadOnly<{
|
|||||||
// 2. Convert the text formatting: e.g. "**hello**" converts to bold "hello".
|
// 2. Convert the text formatting: e.g. "**hello**" converts to bold "hello".
|
||||||
|
|
||||||
export type NodeTransformationKind =
|
export type NodeTransformationKind =
|
||||||
| 'noTransformation'
|
|
||||||
| 'paragraphH1'
|
| 'paragraphH1'
|
||||||
| 'paragraphH2'
|
| 'paragraphH2'
|
||||||
| 'paragraphH3'
|
| 'paragraphH3'
|
||||||
@ -63,20 +62,6 @@ export type NodeTransformationKind =
|
|||||||
| 'paragraphCodeBlock'
|
| 'paragraphCodeBlock'
|
||||||
| 'textBold';
|
| 'textBold';
|
||||||
|
|
||||||
// The scanning context provides the overall data structure for
|
|
||||||
// locating a auto formatting candidate and then transforming that candidate
|
|
||||||
// into the newly formatted stylized text.
|
|
||||||
// The context is filled out lazily to avoid redundant or up-front expensive
|
|
||||||
// calculations. For example, this includes the parent element's getTextContent() which
|
|
||||||
// ultimately gets deposited into the joinedText field.
|
|
||||||
export type ScanningContext = {
|
|
||||||
autoFormatCriteria: AutoFormatCriteria,
|
|
||||||
joinedText: ?string,
|
|
||||||
matchResultContext: MatchResultContext,
|
|
||||||
textNodeWithOffset: TextNodeWithOffset,
|
|
||||||
triggerState: AutoFormatTriggerState,
|
|
||||||
};
|
|
||||||
|
|
||||||
// The auto formatter runs these steps:
|
// The auto formatter runs these steps:
|
||||||
// 1. Examine the current and prior editor states to see if a potential auto format is triggered.
|
// 1. Examine the current and prior editor states to see if a potential auto format is triggered.
|
||||||
// 2. If triggered, examine the current editor state to see if it matches a particular
|
// 2. If triggered, examine the current editor state to see if it matches a particular
|
||||||
@ -89,12 +74,26 @@ export type ScanningContext = {
|
|||||||
// // //
|
// // //
|
||||||
// Capture groups are defined by the regEx pattern. Certain groups must be removed,
|
// Capture groups are defined by the regEx pattern. Certain groups must be removed,
|
||||||
// For example "*hello*", will require that the "*" be removed and the "hello" become bolded.
|
// For example "*hello*", will require that the "*" be removed and the "hello" become bolded.
|
||||||
|
// We can specify ahead of time which gapture groups shoud be removed using the regExCaptureGroupsToDelete.
|
||||||
export type AutoFormatCriteria = $ReadOnly<{
|
export type AutoFormatCriteria = $ReadOnly<{
|
||||||
nodeTransformationKind: ?NodeTransformationKind,
|
nodeTransformationKind: ?NodeTransformationKind,
|
||||||
regEx: RegExp,
|
regEx: RegExp,
|
||||||
|
regExCaptureGroupsToDelete: ?Array<number>,
|
||||||
|
regExExpectedCaptureGroupCount: number,
|
||||||
requiresParagraphStart: ?boolean,
|
requiresParagraphStart: ?boolean,
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
// While scanning over the text, comparing each
|
||||||
|
// auto format criteria against the text, certain
|
||||||
|
// details may be captured rather than re-calculated.
|
||||||
|
// For example, scanning over each AutoFormatCriteria,
|
||||||
|
// building up the ParagraphNode's text by calling getTextContent()
|
||||||
|
// may be expensive. Rather, load this value lazily and store it for later use.
|
||||||
|
export type ScanningContext = {
|
||||||
|
joinedText: ?string,
|
||||||
|
textNodeWithOffset: TextNodeWithOffset,
|
||||||
|
};
|
||||||
|
|
||||||
// RegEx returns the discovered pattern matches in an array of capture groups.
|
// RegEx returns the discovered pattern matches in an array of capture groups.
|
||||||
// Using this array, we can extract the sub-string per pattern, determine its
|
// Using this array, we can extract the sub-string per pattern, determine its
|
||||||
// offset within the parent (except for non-textNode children) within the overall string and determine its length. Typically the length
|
// offset within the parent (except for non-textNode children) within the overall string and determine its length. Typically the length
|
||||||
@ -111,8 +110,8 @@ type CaptureGroupDetail = {
|
|||||||
// This type stores the result details when a particular
|
// This type stores the result details when a particular
|
||||||
// match is found.
|
// match is found.
|
||||||
export type MatchResultContext = {
|
export type MatchResultContext = {
|
||||||
offsetInJoinedTextForCollapsedSelection: number, // The expected location for the blinking caret.
|
|
||||||
regExCaptureGroups: Array<CaptureGroupDetail>,
|
regExCaptureGroups: Array<CaptureGroupDetail>,
|
||||||
|
triggerState: ?AutoFormatTriggerState,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AutoFormatCriteriaWithMatchResultContext = {
|
export type AutoFormatCriteriaWithMatchResultContext = {
|
||||||
@ -129,6 +128,8 @@ const SEPARATOR_BETWEEN_TEXT_AND_NON_TEXT_NODES = '\u0004'; // Select an unused
|
|||||||
const autoFormatBase: AutoFormatCriteria = {
|
const autoFormatBase: AutoFormatCriteria = {
|
||||||
nodeTransformationKind: null,
|
nodeTransformationKind: null,
|
||||||
regEx: /(?:)/,
|
regEx: /(?:)/,
|
||||||
|
regExCaptureGroupsToDelete: null,
|
||||||
|
regExExpectedCaptureGroupCount: 1,
|
||||||
requiresParagraphStart: false,
|
requiresParagraphStart: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -183,6 +184,7 @@ const markdownOrderedList: AutoFormatCriteria = {
|
|||||||
...paragraphStartBase,
|
...paragraphStartBase,
|
||||||
nodeTransformationKind: 'paragraphOrderedList',
|
nodeTransformationKind: 'paragraphOrderedList',
|
||||||
regEx: /^(\d+)\.\s/,
|
regEx: /^(\d+)\.\s/,
|
||||||
|
regExExpectedCaptureGroupCount: 2 /*e.g. '321. ' returns '321. ' & '321'*/,
|
||||||
};
|
};
|
||||||
|
|
||||||
const markdownBold: AutoFormatCriteria = {
|
const markdownBold: AutoFormatCriteria = {
|
||||||
@ -192,6 +194,10 @@ const markdownBold: AutoFormatCriteria = {
|
|||||||
regEx: /(\*)(\s*\b)([^\*]*)(\b\s*)(\*\s)$/,
|
regEx: /(\*)(\s*\b)([^\*]*)(\b\s*)(\*\s)$/,
|
||||||
// Remove the first and last capture groups. Remeber, the 0th capture group is the entire string.
|
// Remove the first and last capture groups. Remeber, the 0th capture group is the entire string.
|
||||||
// e.g. "*Hello* " requires removing both "*" as well as bolding "Hello".
|
// e.g. "*Hello* " requires removing both "*" as well as bolding "Hello".
|
||||||
|
regExCaptureGroupsToDelete: [1, 5],
|
||||||
|
|
||||||
|
// The $ will find the target at the end of the string.
|
||||||
|
regExExpectedCaptureGroupCount: 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
const allAutoFormatCriteriaForTextNodes = [markdownBold];
|
const allAutoFormatCriteriaForTextNodes = [markdownBold];
|
||||||
@ -216,47 +222,28 @@ export function getAllAutoFormatCriteria(): AutoFormatCriteriaArray {
|
|||||||
return allAutoFormatCriteria;
|
return allAutoFormatCriteria;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInitialScanningContext(
|
|
||||||
textNodeWithOffset: TextNodeWithOffset,
|
|
||||||
triggerState: AutoFormatTriggerState,
|
|
||||||
): ScanningContext {
|
|
||||||
return {
|
|
||||||
autoFormatCriteria: {
|
|
||||||
nodeTransformationKind: 'noTransformation',
|
|
||||||
regEx: /(?:)/, // Empty reg ex will do until the precise criteria is discovered.
|
|
||||||
requiresParagraphStart: null,
|
|
||||||
},
|
|
||||||
joinedText: null,
|
|
||||||
matchResultContext: {
|
|
||||||
offsetInJoinedTextForCollapsedSelection: 0,
|
|
||||||
regExCaptureGroups: [],
|
|
||||||
},
|
|
||||||
textNodeWithOffset,
|
|
||||||
triggerState,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMatchResultContextWithRegEx(
|
function getMatchResultContextWithRegEx(
|
||||||
textToSearch: string,
|
textToSearch: string,
|
||||||
matchMustAppearAtStartOfString: boolean,
|
matchMustAppearAtStartOfString: boolean,
|
||||||
matchMustAppearAtEndOfString: boolean,
|
matchMustAppearAtEndOfString: boolean,
|
||||||
regEx: RegExp,
|
regEx: RegExp,
|
||||||
|
regExExpectedCaptureGroupCount: number,
|
||||||
|
scanningContext: ScanningContext,
|
||||||
): null | MatchResultContext {
|
): null | MatchResultContext {
|
||||||
const matchResultContext: MatchResultContext = {
|
const matchResultContext: MatchResultContext = {
|
||||||
offsetInJoinedTextForCollapsedSelection: 0,
|
|
||||||
regExCaptureGroups: [],
|
regExCaptureGroups: [],
|
||||||
|
triggerState: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const regExMatches = textToSearch.match(regEx);
|
const regExMatches = textToSearch.match(regEx);
|
||||||
if (
|
if (
|
||||||
regExMatches !== null &&
|
regExMatches !== null &&
|
||||||
regExMatches.length > 0 &&
|
regExMatches.length > 0 &&
|
||||||
|
regExMatches.length === regExExpectedCaptureGroupCount &&
|
||||||
(matchMustAppearAtStartOfString === false || regExMatches.index === 0) &&
|
(matchMustAppearAtStartOfString === false || regExMatches.index === 0) &&
|
||||||
(matchMustAppearAtEndOfString === false ||
|
(matchMustAppearAtEndOfString === false ||
|
||||||
regExMatches.index + regExMatches[0].length === textToSearch.length)
|
regExMatches.index + regExMatches[0].length === textToSearch.length)
|
||||||
) {
|
) {
|
||||||
matchResultContext.offsetInJoinedTextForCollapsedSelection =
|
|
||||||
textToSearch.length;
|
|
||||||
const captureGroupsCount = regExMatches.length;
|
const captureGroupsCount = regExMatches.length;
|
||||||
let runningLength = regExMatches.index;
|
let runningLength = regExMatches.index;
|
||||||
for (
|
for (
|
||||||
@ -305,6 +292,8 @@ function getMatchResultContextForParagraphs(
|
|||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
autoFormatCriteria.regEx,
|
autoFormatCriteria.regEx,
|
||||||
|
autoFormatCriteria.regExExpectedCaptureGroupCount,
|
||||||
|
scanningContext,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,6 +321,8 @@ function getMatchResultContextForText(
|
|||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
autoFormatCriteria.regEx,
|
autoFormatCriteria.regEx,
|
||||||
|
autoFormatCriteria.regExExpectedCaptureGroupCount,
|
||||||
|
scanningContext,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
invariant(
|
invariant(
|
||||||
@ -362,13 +353,12 @@ export function getMatchResultContextForCriteria(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNewNodeForCriteria(
|
function getNewNodeForCriteria(
|
||||||
scanningContext: ScanningContext,
|
autoFormatCriteria: AutoFormatCriteria,
|
||||||
|
matchResultContext: MatchResultContext,
|
||||||
children: Array<LexicalNode>,
|
children: Array<LexicalNode>,
|
||||||
): null | ElementNode {
|
): null | ElementNode {
|
||||||
let newNode = null;
|
let newNode = null;
|
||||||
|
|
||||||
const autoFormatCriteria = scanningContext.autoFormatCriteria;
|
|
||||||
const matchResultContext = scanningContext.matchResultContext;
|
|
||||||
if (autoFormatCriteria.nodeTransformationKind != null) {
|
if (autoFormatCriteria.nodeTransformationKind != null) {
|
||||||
switch (autoFormatCriteria.nodeTransformationKind) {
|
switch (autoFormatCriteria.nodeTransformationKind) {
|
||||||
case 'paragraphH1': {
|
case 'paragraphH1': {
|
||||||
@ -415,8 +405,8 @@ function getNewNodeForCriteria(
|
|||||||
case 'paragraphCodeBlock': {
|
case 'paragraphCodeBlock': {
|
||||||
// Toggle code and paragraph nodes.
|
// Toggle code and paragraph nodes.
|
||||||
if (
|
if (
|
||||||
scanningContext.triggerState != null &&
|
matchResultContext.triggerState != null &&
|
||||||
scanningContext.triggerState.isCodeBlock
|
matchResultContext.triggerState.isCodeBlock
|
||||||
) {
|
) {
|
||||||
newNode = $createParagraphNode();
|
newNode = $createParagraphNode();
|
||||||
} else {
|
} else {
|
||||||
@ -443,22 +433,37 @@ function updateTextNode(node: TextNode, count: number): void {
|
|||||||
|
|
||||||
export function transformTextNodeForAutoFormatCriteria(
|
export function transformTextNodeForAutoFormatCriteria(
|
||||||
scanningContext: ScanningContext,
|
scanningContext: ScanningContext,
|
||||||
|
autoFormatCriteria: AutoFormatCriteria,
|
||||||
|
matchResultContext: MatchResultContext,
|
||||||
) {
|
) {
|
||||||
if (scanningContext.autoFormatCriteria.requiresParagraphStart) {
|
if (autoFormatCriteria.requiresParagraphStart) {
|
||||||
transformTextNodeForParagraphs(scanningContext);
|
transformTextNodeForParagraphs(
|
||||||
|
scanningContext,
|
||||||
|
autoFormatCriteria,
|
||||||
|
matchResultContext,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
transformTextNodeForText(scanningContext);
|
transformTextNodeForText(
|
||||||
|
scanningContext,
|
||||||
|
autoFormatCriteria,
|
||||||
|
matchResultContext,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformTextNodeForParagraphs(scanningContext: ScanningContext) {
|
function transformTextNodeForParagraphs(
|
||||||
|
scanningContext: ScanningContext,
|
||||||
|
autoFormatCriteria: AutoFormatCriteria,
|
||||||
|
matchResultContext: MatchResultContext,
|
||||||
|
) {
|
||||||
const textNodeWithOffset = scanningContext.textNodeWithOffset;
|
const textNodeWithOffset = scanningContext.textNodeWithOffset;
|
||||||
const element = textNodeWithOffset.node.getParentOrThrow();
|
const element = textNodeWithOffset.node.getParentOrThrow();
|
||||||
const text = scanningContext.matchResultContext.regExCaptureGroups[0].text;
|
const text = matchResultContext.regExCaptureGroups[0].text;
|
||||||
updateTextNode(textNodeWithOffset.node, text.length);
|
updateTextNode(textNodeWithOffset.node, text.length);
|
||||||
|
|
||||||
const elementNode = getNewNodeForCriteria(
|
const elementNode = getNewNodeForCriteria(
|
||||||
scanningContext,
|
autoFormatCriteria,
|
||||||
|
matchResultContext,
|
||||||
element.getChildren(),
|
element.getChildren(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -467,29 +472,29 @@ function transformTextNodeForParagraphs(scanningContext: ScanningContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformTextNodeForText(scanningContext: ScanningContext) {
|
function transformTextNodeForText(
|
||||||
const autoFormatCriteria = scanningContext.autoFormatCriteria;
|
scanningContext: ScanningContext,
|
||||||
const matchResultContext = scanningContext.matchResultContext;
|
autoFormatCriteria: AutoFormatCriteria,
|
||||||
|
matchResultContext: MatchResultContext,
|
||||||
|
) {
|
||||||
if (autoFormatCriteria.nodeTransformationKind != null) {
|
if (autoFormatCriteria.nodeTransformationKind != null) {
|
||||||
switch (autoFormatCriteria.nodeTransformationKind) {
|
switch (autoFormatCriteria.nodeTransformationKind) {
|
||||||
case 'textBold': {
|
case 'textBold': {
|
||||||
if (matchResultContext.regExCaptureGroups.length !== 6) {
|
|
||||||
// The expected reg ex pattern for bold should have 6 groups.
|
|
||||||
// If it does not, then break and fail silently.
|
|
||||||
// e2e tests validate the regEx pattern.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
matchResultContext.regExCaptureGroups =
|
matchResultContext.regExCaptureGroups =
|
||||||
getCaptureGroupsByResolvingAllDetails(scanningContext);
|
getCaptureGroupsByResolvingAllDetails(
|
||||||
// Remove unwanted text in reg ex pattern.
|
scanningContext,
|
||||||
removeTextInCaptureGroups([1, 5], matchResultContext);
|
autoFormatCriteria,
|
||||||
formatTextInCaptureGroupIndex('bold', 3, matchResultContext);
|
matchResultContext,
|
||||||
makeCollapsedSelectionAtOffsetInJoinedText(
|
|
||||||
matchResultContext.offsetInJoinedTextForCollapsedSelection,
|
|
||||||
matchResultContext.offsetInJoinedTextForCollapsedSelection + 1,
|
|
||||||
scanningContext.textNodeWithOffset.node.getParentOrThrow(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (autoFormatCriteria.regExCaptureGroupsToDelete != null) {
|
||||||
|
// Remove unwanted text in reg ex patterh.
|
||||||
|
removeTextInCaptureGroups(
|
||||||
|
autoFormatCriteria.regExCaptureGroupsToDelete,
|
||||||
|
matchResultContext,
|
||||||
|
);
|
||||||
|
formatTextInCaptureGroupIndex('bold', 3, matchResultContext);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@ -503,10 +508,9 @@ function transformTextNodeForText(scanningContext: ScanningContext) {
|
|||||||
// known, the details may be fully resolved without incurring unwasted performance cost.
|
// known, the details may be fully resolved without incurring unwasted performance cost.
|
||||||
function getCaptureGroupsByResolvingAllDetails(
|
function getCaptureGroupsByResolvingAllDetails(
|
||||||
scanningContext: ScanningContext,
|
scanningContext: ScanningContext,
|
||||||
|
autoFormatCriteria: AutoFormatCriteria,
|
||||||
|
matchResultContext: MatchResultContext,
|
||||||
): Array<CaptureGroupDetail> {
|
): Array<CaptureGroupDetail> {
|
||||||
const autoFormatCriteria = scanningContext.autoFormatCriteria;
|
|
||||||
const matchResultContext = scanningContext.matchResultContext;
|
|
||||||
|
|
||||||
const textNodeWithOffset = scanningContext.textNodeWithOffset;
|
const textNodeWithOffset = scanningContext.textNodeWithOffset;
|
||||||
const regExCaptureGroups = matchResultContext.regExCaptureGroups;
|
const regExCaptureGroups = matchResultContext.regExCaptureGroups;
|
||||||
const captureGroupsCount = regExCaptureGroups.length;
|
const captureGroupsCount = regExCaptureGroups.length;
|
||||||
@ -633,12 +637,6 @@ function shiftCaptureGroupOffsets(
|
|||||||
startingCaptureGroupIndex: number,
|
startingCaptureGroupIndex: number,
|
||||||
matchResultContext: MatchResultContext,
|
matchResultContext: MatchResultContext,
|
||||||
) {
|
) {
|
||||||
matchResultContext.offsetInJoinedTextForCollapsedSelection += delta;
|
|
||||||
invariant(
|
|
||||||
matchResultContext.offsetInJoinedTextForCollapsedSelection > 0,
|
|
||||||
'The text content string length does not correlate with insertions/deletions of new text.',
|
|
||||||
);
|
|
||||||
|
|
||||||
const regExCaptureGroups = matchResultContext.regExCaptureGroups;
|
const regExCaptureGroups = matchResultContext.regExCaptureGroups;
|
||||||
const regExCaptureGroupsCount = regExCaptureGroups.length;
|
const regExCaptureGroupsCount = regExCaptureGroups.length;
|
||||||
for (
|
for (
|
||||||
@ -707,52 +705,6 @@ function formatTextInCaptureGroupIndex(
|
|||||||
const currentSelection = $getSelection();
|
const currentSelection = $getSelection();
|
||||||
if (currentSelection != null) {
|
if (currentSelection != null) {
|
||||||
currentSelection.formatText(formatType);
|
currentSelection.formatText(formatType);
|
||||||
|
|
||||||
const finalSelection = $createRangeSelection();
|
|
||||||
|
|
||||||
finalSelection.anchor.set(
|
|
||||||
focusTextNodeWithOffset.node.getKey(),
|
|
||||||
focusTextNodeWithOffset.offset + 1,
|
|
||||||
'text',
|
|
||||||
);
|
|
||||||
|
|
||||||
finalSelection.focus.set(
|
|
||||||
focusTextNodeWithOffset.node.getKey(),
|
|
||||||
focusTextNodeWithOffset.offset + 1,
|
|
||||||
'text',
|
|
||||||
);
|
|
||||||
$setSelection(finalSelection);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeCollapsedSelectionAtOffsetInJoinedText(
|
|
||||||
offsetInJoinedText: number,
|
|
||||||
joinedTextLength: number,
|
|
||||||
parentElementNode: ElementNode,
|
|
||||||
) {
|
|
||||||
const textNodeWithOffset = $findNodeWithOffsetFromJoinedText(
|
|
||||||
parentElementNode,
|
|
||||||
joinedTextLength,
|
|
||||||
offsetInJoinedText,
|
|
||||||
TRIGGER_STRING_LENGTH,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (textNodeWithOffset != null) {
|
|
||||||
const newSelection = $createRangeSelection();
|
|
||||||
|
|
||||||
newSelection.anchor.set(
|
|
||||||
textNodeWithOffset.node.getKey(),
|
|
||||||
textNodeWithOffset.offset,
|
|
||||||
'text',
|
|
||||||
);
|
|
||||||
|
|
||||||
newSelection.focus.set(
|
|
||||||
textNodeWithOffset.node.getKey(),
|
|
||||||
textNodeWithOffset.offset,
|
|
||||||
'text',
|
|
||||||
);
|
|
||||||
|
|
||||||
$setSelection(newSelection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -24,7 +24,6 @@ import {useEffect} from 'react';
|
|||||||
import {
|
import {
|
||||||
getAllAutoFormatCriteria,
|
getAllAutoFormatCriteria,
|
||||||
getAllAutoFormatCriteriaForTextNodes,
|
getAllAutoFormatCriteriaForTextNodes,
|
||||||
getInitialScanningContext,
|
|
||||||
getMatchResultContextForCriteria,
|
getMatchResultContextForCriteria,
|
||||||
transformTextNodeForAutoFormatCriteria,
|
transformTextNodeForAutoFormatCriteria,
|
||||||
TRIGGER_STRING,
|
TRIGGER_STRING,
|
||||||
@ -32,18 +31,16 @@ import {
|
|||||||
|
|
||||||
function getCriteriaWithMatchResultContext(
|
function getCriteriaWithMatchResultContext(
|
||||||
autoFormatCriteriaArray: AutoFormatCriteriaArray,
|
autoFormatCriteriaArray: AutoFormatCriteriaArray,
|
||||||
|
currentTriggerState: AutoFormatTriggerState,
|
||||||
scanningContext: ScanningContext,
|
scanningContext: ScanningContext,
|
||||||
): AutoFormatCriteriaWithMatchResultContext {
|
): AutoFormatCriteriaWithMatchResultContext {
|
||||||
const currentTriggerState = scanningContext.triggerState;
|
|
||||||
|
|
||||||
const count = autoFormatCriteriaArray.length;
|
const count = autoFormatCriteriaArray.length;
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const autoFormatCriteria = autoFormatCriteriaArray[i];
|
const autoFormatCriteria = autoFormatCriteriaArray[i];
|
||||||
|
|
||||||
// Skip code block nodes, unless the nodeTransformationKind calls for toggling the code block.
|
// Skip code block nodes, unless the nodeTransformationKind calls for toggling the code block.
|
||||||
if (
|
if (
|
||||||
(currentTriggerState != null &&
|
currentTriggerState.isCodeBlock === false ||
|
||||||
currentTriggerState.isCodeBlock === false) ||
|
|
||||||
autoFormatCriteria.nodeTransformationKind === 'paragraphCodeBlock'
|
autoFormatCriteria.nodeTransformationKind === 'paragraphCodeBlock'
|
||||||
) {
|
) {
|
||||||
const matchResultContext = getMatchResultContextForCriteria(
|
const matchResultContext = getMatchResultContextForCriteria(
|
||||||
@ -51,6 +48,7 @@ function getCriteriaWithMatchResultContext(
|
|||||||
scanningContext,
|
scanningContext,
|
||||||
);
|
);
|
||||||
if (matchResultContext != null) {
|
if (matchResultContext != null) {
|
||||||
|
matchResultContext.triggerState = currentTriggerState;
|
||||||
return {
|
return {
|
||||||
autoFormatCriteria: autoFormatCriteria,
|
autoFormatCriteria: autoFormatCriteria,
|
||||||
matchResultContext,
|
matchResultContext,
|
||||||
@ -78,24 +76,9 @@ function getTextNodeForAutoFormatting(
|
|||||||
|
|
||||||
function updateAutoFormatting(
|
function updateAutoFormatting(
|
||||||
editor: LexicalEditor,
|
editor: LexicalEditor,
|
||||||
scanningContext: ScanningContext,
|
|
||||||
): void {
|
|
||||||
editor.update(
|
|
||||||
() => {
|
|
||||||
transformTextNodeForAutoFormatCriteria(scanningContext);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: 'history-push',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findScanningContextWithValidMatch(
|
|
||||||
editorState: EditorState,
|
|
||||||
currentTriggerState: AutoFormatTriggerState,
|
currentTriggerState: AutoFormatTriggerState,
|
||||||
): null | ScanningContext {
|
): void {
|
||||||
let scanningContext = null;
|
editor.update(() => {
|
||||||
editorState.read(() => {
|
|
||||||
const textNodeWithOffset = getTextNodeForAutoFormatting($getSelection());
|
const textNodeWithOffset = getTextNodeForAutoFormatting($getSelection());
|
||||||
|
|
||||||
if (textNodeWithOffset === null) {
|
if (textNodeWithOffset === null) {
|
||||||
@ -103,17 +86,17 @@ function findScanningContextWithValidMatch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Please see the declaration of ScanningContext for a detailed explanation.
|
// Please see the declaration of ScanningContext for a detailed explanation.
|
||||||
const initialScanningContext = getInitialScanningContext(
|
const scanningContext: ScanningContext = {
|
||||||
|
joinedText: null,
|
||||||
textNodeWithOffset,
|
textNodeWithOffset,
|
||||||
currentTriggerState,
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const criteriaWithMatchResultContext = getCriteriaWithMatchResultContext(
|
const criteriaWithMatchResultContext = getCriteriaWithMatchResultContext(
|
||||||
// Do not apply paragraph node changes like blockQuote or H1 to listNodes. Also, do not attempt to transform a list into a list using * or -.
|
// Do not apply paragraph node changes like blockQuote or H1 to listNodes. Also, do not attempt to transform a list into a list using * or -.
|
||||||
currentTriggerState.isParentAListItemNode === false
|
currentTriggerState.isParentAListItemNode === false
|
||||||
? getAllAutoFormatCriteria()
|
? getAllAutoFormatCriteria()
|
||||||
: getAllAutoFormatCriteriaForTextNodes(),
|
: getAllAutoFormatCriteriaForTextNodes(),
|
||||||
initialScanningContext,
|
currentTriggerState,
|
||||||
|
scanningContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -122,23 +105,21 @@ function findScanningContextWithValidMatch(
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
scanningContext = initialScanningContext;
|
|
||||||
// Lazy fill-in the particular format criteria and any matching result information.
|
transformTextNodeForAutoFormatCriteria(
|
||||||
scanningContext.autoFormatCriteria =
|
scanningContext,
|
||||||
criteriaWithMatchResultContext.autoFormatCriteria;
|
criteriaWithMatchResultContext.autoFormatCriteria,
|
||||||
scanningContext.matchResultContext =
|
criteriaWithMatchResultContext.matchResultContext,
|
||||||
criteriaWithMatchResultContext.matchResultContext;
|
);
|
||||||
});
|
});
|
||||||
return scanningContext;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function findScanningContext(
|
function shouldAttemptToAutoFormat(
|
||||||
editorState: EditorState,
|
|
||||||
currentTriggerState: null | AutoFormatTriggerState,
|
currentTriggerState: null | AutoFormatTriggerState,
|
||||||
priorTriggerState: null | AutoFormatTriggerState,
|
priorTriggerState: null | AutoFormatTriggerState,
|
||||||
): null | ScanningContext {
|
): boolean {
|
||||||
if (currentTriggerState == null || priorTriggerState == null) {
|
if (currentTriggerState == null || priorTriggerState == null) {
|
||||||
return null;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The below checks needs to execute relativey quickly, so perform the light-weight ones first.
|
// The below checks needs to execute relativey quickly, so perform the light-weight ones first.
|
||||||
@ -148,8 +129,8 @@ function findScanningContext(
|
|||||||
const currentTextContentLength = currentTriggerState.textContent.length;
|
const currentTextContentLength = currentTriggerState.textContent.length;
|
||||||
const triggerOffset = currentTriggerState.anchorOffset - triggerStringLength;
|
const triggerOffset = currentTriggerState.anchorOffset - triggerStringLength;
|
||||||
|
|
||||||
if (
|
return (
|
||||||
(currentTriggerState.hasParentNode === true &&
|
currentTriggerState.hasParentNode === true &&
|
||||||
currentTriggerState.isSimpleText &&
|
currentTriggerState.isSimpleText &&
|
||||||
currentTriggerState.isSelectionCollapsed &&
|
currentTriggerState.isSelectionCollapsed &&
|
||||||
currentTriggerState.nodeKey === priorTriggerState.nodeKey &&
|
currentTriggerState.nodeKey === priorTriggerState.nodeKey &&
|
||||||
@ -160,13 +141,8 @@ function findScanningContext(
|
|||||||
triggerOffset,
|
triggerOffset,
|
||||||
triggerStringLength,
|
triggerStringLength,
|
||||||
) === TRIGGER_STRING &&
|
) === TRIGGER_STRING &&
|
||||||
currentTriggerState.textContent !== priorTriggerState.textContent) ===
|
currentTriggerState.textContent !== priorTriggerState.textContent
|
||||||
false
|
);
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return findScanningContextWithValidMatch(editorState, currentTriggerState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTriggerState(
|
function getTriggerState(
|
||||||
@ -210,21 +186,16 @@ export default function useAutoFormatter(editor: LexicalEditor): void {
|
|||||||
// However, given "#A B", where the user delets "A" should not.
|
// However, given "#A B", where the user delets "A" should not.
|
||||||
|
|
||||||
let priorTriggerState: null | AutoFormatTriggerState = null;
|
let priorTriggerState: null | AutoFormatTriggerState = null;
|
||||||
return editor.addListener('update', ({tags}) => {
|
editor.addListener('update', ({tags}) => {
|
||||||
// Examine historic so that we are not running autoformatting within markdown.
|
// Examine historic so that we are not running autoformatting within markdown.
|
||||||
if (tags.has('historic') === false) {
|
if (tags.has('historic') === false) {
|
||||||
const editorState = editor.getEditorState();
|
const currentTriggerState = getTriggerState(editor.getEditorState());
|
||||||
const currentTriggerState = getTriggerState(editorState);
|
|
||||||
const scanningContext =
|
if (
|
||||||
currentTriggerState == null
|
shouldAttemptToAutoFormat(currentTriggerState, priorTriggerState) &&
|
||||||
? null
|
currentTriggerState != null
|
||||||
: findScanningContext(
|
) {
|
||||||
editorState,
|
updateAutoFormatting(editor, currentTriggerState);
|
||||||
currentTriggerState,
|
|
||||||
priorTriggerState,
|
|
||||||
);
|
|
||||||
if (scanningContext != null) {
|
|
||||||
updateAutoFormatting(editor, scanningContext);
|
|
||||||
}
|
}
|
||||||
priorTriggerState = currentTriggerState;
|
priorTriggerState = currentTriggerState;
|
||||||
} else {
|
} else {
|
||||||
|
@ -81,7 +81,7 @@ export function useCharacterLimit(
|
|||||||
$wrapOverflowedNodes(offset);
|
$wrapOverflowedNodes(offset);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'history-merge',
|
tag: 'without-history',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,9 @@ import {$getSelection, $isRootNode, $isTextNode} from 'lexical';
|
|||||||
import {useCallback, useEffect, useMemo} from 'react';
|
import {useCallback, useEffect, useMemo} from 'react';
|
||||||
|
|
||||||
type MergeAction = 0 | 1 | 2;
|
type MergeAction = 0 | 1 | 2;
|
||||||
const HISTORY_MERGE = 0;
|
const MERGE = 0;
|
||||||
const HISTORY_PUSH = 1;
|
const NO_MERGE = 1;
|
||||||
const DISCARD_HISTORY_CANDIDATE = 2;
|
const DISCARD = 2;
|
||||||
|
|
||||||
type ChangeType = 0 | 1 | 2 | 3 | 4;
|
type ChangeType = 0 | 1 | 2 | 3 | 4;
|
||||||
const OTHER = 0;
|
const OTHER = 0;
|
||||||
@ -204,7 +204,7 @@ function createMergeActionGetter(
|
|||||||
if (tags.has('historic')) {
|
if (tags.has('historic')) {
|
||||||
prevChangeType = OTHER;
|
prevChangeType = OTHER;
|
||||||
prevChangeTime = changeTime;
|
prevChangeTime = changeTime;
|
||||||
return DISCARD_HISTORY_CANDIDATE;
|
return DISCARD;
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeType = getChangeType(
|
const changeType = getChangeType(
|
||||||
@ -216,40 +216,35 @@ function createMergeActionGetter(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const mergeAction = (() => {
|
const mergeAction = (() => {
|
||||||
const shouldPushHistory = tags.has('history-push');
|
if (tags.has('without-history')) {
|
||||||
const shouldMergeHistory =
|
return MERGE;
|
||||||
!shouldPushHistory && tags.has('history-merge');
|
|
||||||
|
|
||||||
if (shouldMergeHistory) {
|
|
||||||
return HISTORY_MERGE;
|
|
||||||
}
|
}
|
||||||
if (prevEditorState === null) {
|
if (prevEditorState === null) {
|
||||||
return HISTORY_PUSH;
|
return NO_MERGE;
|
||||||
}
|
}
|
||||||
const selection = nextEditorState._selection;
|
const selection = nextEditorState._selection;
|
||||||
const prevSelection = prevEditorState._selection;
|
const prevSelection = prevEditorState._selection;
|
||||||
const hasDirtyNodes = dirtyLeaves.size > 0 || dirtyElements.size > 0;
|
const hasDirtyNodes = dirtyLeaves.size > 0 || dirtyElements.size > 0;
|
||||||
if (!hasDirtyNodes) {
|
if (!hasDirtyNodes) {
|
||||||
if (prevSelection === null && selection !== null) {
|
if (prevSelection === null && selection !== null) {
|
||||||
return HISTORY_MERGE;
|
return MERGE;
|
||||||
}
|
}
|
||||||
return DISCARD_HISTORY_CANDIDATE;
|
return DISCARD;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSameEditor =
|
const isSameEditor =
|
||||||
currentHistoryEntry === null || currentHistoryEntry.editor === editor;
|
currentHistoryEntry === null || currentHistoryEntry.editor === editor;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
shouldPushHistory === false &&
|
|
||||||
changeType !== OTHER &&
|
changeType !== OTHER &&
|
||||||
changeType === prevChangeType &&
|
changeType === prevChangeType &&
|
||||||
changeTime < prevChangeTime + delay &&
|
changeTime < prevChangeTime + delay &&
|
||||||
isSameEditor
|
isSameEditor
|
||||||
) {
|
) {
|
||||||
return HISTORY_MERGE;
|
return MERGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
return HISTORY_PUSH;
|
return NO_MERGE;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
prevChangeTime = changeTime;
|
prevChangeTime = changeTime;
|
||||||
@ -302,7 +297,7 @@ export function useHistory(
|
|||||||
tags,
|
tags,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mergeAction === HISTORY_PUSH) {
|
if (mergeAction === NO_MERGE) {
|
||||||
if (redoStack.length !== 0) {
|
if (redoStack.length !== 0) {
|
||||||
historyState.redoStack = [];
|
historyState.redoStack = [];
|
||||||
}
|
}
|
||||||
@ -313,7 +308,7 @@ export function useHistory(
|
|||||||
});
|
});
|
||||||
editor.execCommand('canUndo', true);
|
editor.execCommand('canUndo', true);
|
||||||
}
|
}
|
||||||
} else if (mergeAction === DISCARD_HISTORY_CANDIDATE) {
|
} else if (mergeAction === DISCARD) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -490,7 +490,7 @@ class BaseLexicalEditor {
|
|||||||
nextRootElement.setAttribute('data-lexical-editor', 'true');
|
nextRootElement.setAttribute('data-lexical-editor', 'true');
|
||||||
this._dirtyType = FULL_RECONCILE;
|
this._dirtyType = FULL_RECONCILE;
|
||||||
initMutationObserver(getSelf(this));
|
initMutationObserver(getSelf(this));
|
||||||
this._updateTags.add('history-merge');
|
this._updateTags.add('without-history');
|
||||||
commitPendingUpdates(getSelf(this));
|
commitPendingUpdates(getSelf(this));
|
||||||
// TODO: remove this flag once we no longer use UEv2 internally
|
// TODO: remove this flag once we no longer use UEv2 internally
|
||||||
if (!this._config.disableEvents) {
|
if (!this._config.disableEvents) {
|
||||||
|
@ -339,7 +339,7 @@ export function markAllNodesAsDirty(editor: LexicalEditor, type: string): void {
|
|||||||
true,
|
true,
|
||||||
editor._pendingEditorState === null
|
editor._pendingEditorState === null
|
||||||
? {
|
? {
|
||||||
tag: 'history-merge',
|
tag: 'without-history',
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
|
@ -62,7 +62,7 @@ export class DecoratorEditor {
|
|||||||
this.editorState = editorState;
|
this.editorState = editorState;
|
||||||
if (editorState !== null) {
|
if (editorState !== null) {
|
||||||
editor.setEditorState(editorState, {
|
editor.setEditorState(editorState, {
|
||||||
tag: 'history-merge',
|
tag: 'without-history',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user