mirror of
https://github.com/facebook/lexical.git
synced 2025-08-26 02:39:24 +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 childrenLength = children.length;
|
||||
let runningLength = 0;
|
||||
let isPriorNodeTextNode = false;
|
||||
for (let i = 0; i < childrenLength; ++i) {
|
||||
// We must examine the offsetInJoinedText that is located
|
||||
// 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) {
|
||||
if (runningLength >= joinedTextLength) {
|
||||
break;
|
||||
}
|
||||
|
||||
const child = children[i];
|
||||
const isChildNodeTestNode = $isTextNode(child);
|
||||
const childContentLength = isChildNodeTestNode
|
||||
const childContentLength = $isTextNode(child)
|
||||
? child.getTextContent().length
|
||||
: separatorLength;
|
||||
|
||||
const newRunningLength = runningLength + childContentLength;
|
||||
|
||||
const isJoinedOffsetWithinNode =
|
||||
(isPriorNodeTextNode === false && runningLength === offsetInJoinedText) ||
|
||||
(runningLength === 0 && runningLength === offsetInJoinedText) ||
|
||||
(runningLength < offsetInJoinedText &&
|
||||
offsetInJoinedText <= newRunningLength);
|
||||
|
||||
if (isJoinedOffsetWithinNode && $isTextNode(child)) {
|
||||
// Check isTextNode again for flow.
|
||||
if (
|
||||
runningLength <= offsetInJoinedText &&
|
||||
offsetInJoinedText < newRunningLength &&
|
||||
$isTextNode(child)
|
||||
) {
|
||||
return {
|
||||
node: child,
|
||||
offset: offsetInJoinedText - runningLength,
|
||||
};
|
||||
}
|
||||
runningLength = newRunningLength;
|
||||
isPriorNodeTextNode = isChildNodeTestNode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ function useTypeahead(editor: LexicalEditor): void {
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'history-merge',
|
||||
tag: 'without-history',
|
||||
},
|
||||
);
|
||||
}, [editor, getTypeaheadTextNode, selectionCollapsed, suggestion]);
|
||||
|
@ -53,7 +53,6 @@ export type AutoFormatTriggerState = $ReadOnly<{
|
||||
// 2. Convert the text formatting: e.g. "**hello**" converts to bold "hello".
|
||||
|
||||
export type NodeTransformationKind =
|
||||
| 'noTransformation'
|
||||
| 'paragraphH1'
|
||||
| 'paragraphH2'
|
||||
| 'paragraphH3'
|
||||
@ -63,20 +62,6 @@ export type NodeTransformationKind =
|
||||
| 'paragraphCodeBlock'
|
||||
| '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:
|
||||
// 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
|
||||
@ -89,12 +74,26 @@ export type ScanningContext = {
|
||||
// // //
|
||||
// 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.
|
||||
// We can specify ahead of time which gapture groups shoud be removed using the regExCaptureGroupsToDelete.
|
||||
export type AutoFormatCriteria = $ReadOnly<{
|
||||
nodeTransformationKind: ?NodeTransformationKind,
|
||||
regEx: RegExp,
|
||||
regExCaptureGroupsToDelete: ?Array<number>,
|
||||
regExExpectedCaptureGroupCount: number,
|
||||
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.
|
||||
// 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
|
||||
@ -111,8 +110,8 @@ type CaptureGroupDetail = {
|
||||
// This type stores the result details when a particular
|
||||
// match is found.
|
||||
export type MatchResultContext = {
|
||||
offsetInJoinedTextForCollapsedSelection: number, // The expected location for the blinking caret.
|
||||
regExCaptureGroups: Array<CaptureGroupDetail>,
|
||||
triggerState: ?AutoFormatTriggerState,
|
||||
};
|
||||
|
||||
export type AutoFormatCriteriaWithMatchResultContext = {
|
||||
@ -129,6 +128,8 @@ const SEPARATOR_BETWEEN_TEXT_AND_NON_TEXT_NODES = '\u0004'; // Select an unused
|
||||
const autoFormatBase: AutoFormatCriteria = {
|
||||
nodeTransformationKind: null,
|
||||
regEx: /(?:)/,
|
||||
regExCaptureGroupsToDelete: null,
|
||||
regExExpectedCaptureGroupCount: 1,
|
||||
requiresParagraphStart: false,
|
||||
};
|
||||
|
||||
@ -183,6 +184,7 @@ const markdownOrderedList: AutoFormatCriteria = {
|
||||
...paragraphStartBase,
|
||||
nodeTransformationKind: 'paragraphOrderedList',
|
||||
regEx: /^(\d+)\.\s/,
|
||||
regExExpectedCaptureGroupCount: 2 /*e.g. '321. ' returns '321. ' & '321'*/,
|
||||
};
|
||||
|
||||
const markdownBold: AutoFormatCriteria = {
|
||||
@ -192,6 +194,10 @@ const markdownBold: AutoFormatCriteria = {
|
||||
regEx: /(\*)(\s*\b)([^\*]*)(\b\s*)(\*\s)$/,
|
||||
// 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".
|
||||
regExCaptureGroupsToDelete: [1, 5],
|
||||
|
||||
// The $ will find the target at the end of the string.
|
||||
regExExpectedCaptureGroupCount: 6,
|
||||
};
|
||||
|
||||
const allAutoFormatCriteriaForTextNodes = [markdownBold];
|
||||
@ -216,47 +222,28 @@ export function getAllAutoFormatCriteria(): AutoFormatCriteriaArray {
|
||||
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(
|
||||
textToSearch: string,
|
||||
matchMustAppearAtStartOfString: boolean,
|
||||
matchMustAppearAtEndOfString: boolean,
|
||||
regEx: RegExp,
|
||||
regExExpectedCaptureGroupCount: number,
|
||||
scanningContext: ScanningContext,
|
||||
): null | MatchResultContext {
|
||||
const matchResultContext: MatchResultContext = {
|
||||
offsetInJoinedTextForCollapsedSelection: 0,
|
||||
regExCaptureGroups: [],
|
||||
triggerState: null,
|
||||
};
|
||||
|
||||
const regExMatches = textToSearch.match(regEx);
|
||||
if (
|
||||
regExMatches !== null &&
|
||||
regExMatches.length > 0 &&
|
||||
regExMatches.length === regExExpectedCaptureGroupCount &&
|
||||
(matchMustAppearAtStartOfString === false || regExMatches.index === 0) &&
|
||||
(matchMustAppearAtEndOfString === false ||
|
||||
regExMatches.index + regExMatches[0].length === textToSearch.length)
|
||||
) {
|
||||
matchResultContext.offsetInJoinedTextForCollapsedSelection =
|
||||
textToSearch.length;
|
||||
const captureGroupsCount = regExMatches.length;
|
||||
let runningLength = regExMatches.index;
|
||||
for (
|
||||
@ -305,6 +292,8 @@ function getMatchResultContextForParagraphs(
|
||||
true,
|
||||
false,
|
||||
autoFormatCriteria.regEx,
|
||||
autoFormatCriteria.regExExpectedCaptureGroupCount,
|
||||
scanningContext,
|
||||
);
|
||||
}
|
||||
|
||||
@ -332,6 +321,8 @@ function getMatchResultContextForText(
|
||||
false,
|
||||
true,
|
||||
autoFormatCriteria.regEx,
|
||||
autoFormatCriteria.regExExpectedCaptureGroupCount,
|
||||
scanningContext,
|
||||
);
|
||||
} else {
|
||||
invariant(
|
||||
@ -362,13 +353,12 @@ export function getMatchResultContextForCriteria(
|
||||
}
|
||||
|
||||
function getNewNodeForCriteria(
|
||||
scanningContext: ScanningContext,
|
||||
autoFormatCriteria: AutoFormatCriteria,
|
||||
matchResultContext: MatchResultContext,
|
||||
children: Array<LexicalNode>,
|
||||
): null | ElementNode {
|
||||
let newNode = null;
|
||||
|
||||
const autoFormatCriteria = scanningContext.autoFormatCriteria;
|
||||
const matchResultContext = scanningContext.matchResultContext;
|
||||
if (autoFormatCriteria.nodeTransformationKind != null) {
|
||||
switch (autoFormatCriteria.nodeTransformationKind) {
|
||||
case 'paragraphH1': {
|
||||
@ -415,8 +405,8 @@ function getNewNodeForCriteria(
|
||||
case 'paragraphCodeBlock': {
|
||||
// Toggle code and paragraph nodes.
|
||||
if (
|
||||
scanningContext.triggerState != null &&
|
||||
scanningContext.triggerState.isCodeBlock
|
||||
matchResultContext.triggerState != null &&
|
||||
matchResultContext.triggerState.isCodeBlock
|
||||
) {
|
||||
newNode = $createParagraphNode();
|
||||
} else {
|
||||
@ -443,22 +433,37 @@ function updateTextNode(node: TextNode, count: number): void {
|
||||
|
||||
export function transformTextNodeForAutoFormatCriteria(
|
||||
scanningContext: ScanningContext,
|
||||
autoFormatCriteria: AutoFormatCriteria,
|
||||
matchResultContext: MatchResultContext,
|
||||
) {
|
||||
if (scanningContext.autoFormatCriteria.requiresParagraphStart) {
|
||||
transformTextNodeForParagraphs(scanningContext);
|
||||
if (autoFormatCriteria.requiresParagraphStart) {
|
||||
transformTextNodeForParagraphs(
|
||||
scanningContext,
|
||||
autoFormatCriteria,
|
||||
matchResultContext,
|
||||
);
|
||||
} else {
|
||||
transformTextNodeForText(scanningContext);
|
||||
transformTextNodeForText(
|
||||
scanningContext,
|
||||
autoFormatCriteria,
|
||||
matchResultContext,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function transformTextNodeForParagraphs(scanningContext: ScanningContext) {
|
||||
function transformTextNodeForParagraphs(
|
||||
scanningContext: ScanningContext,
|
||||
autoFormatCriteria: AutoFormatCriteria,
|
||||
matchResultContext: MatchResultContext,
|
||||
) {
|
||||
const textNodeWithOffset = scanningContext.textNodeWithOffset;
|
||||
const element = textNodeWithOffset.node.getParentOrThrow();
|
||||
const text = scanningContext.matchResultContext.regExCaptureGroups[0].text;
|
||||
const text = matchResultContext.regExCaptureGroups[0].text;
|
||||
updateTextNode(textNodeWithOffset.node, text.length);
|
||||
|
||||
const elementNode = getNewNodeForCriteria(
|
||||
scanningContext,
|
||||
autoFormatCriteria,
|
||||
matchResultContext,
|
||||
element.getChildren(),
|
||||
);
|
||||
|
||||
@ -467,29 +472,29 @@ function transformTextNodeForParagraphs(scanningContext: ScanningContext) {
|
||||
}
|
||||
}
|
||||
|
||||
function transformTextNodeForText(scanningContext: ScanningContext) {
|
||||
const autoFormatCriteria = scanningContext.autoFormatCriteria;
|
||||
const matchResultContext = scanningContext.matchResultContext;
|
||||
|
||||
function transformTextNodeForText(
|
||||
scanningContext: ScanningContext,
|
||||
autoFormatCriteria: AutoFormatCriteria,
|
||||
matchResultContext: MatchResultContext,
|
||||
) {
|
||||
if (autoFormatCriteria.nodeTransformationKind != null) {
|
||||
switch (autoFormatCriteria.nodeTransformationKind) {
|
||||
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 =
|
||||
getCaptureGroupsByResolvingAllDetails(scanningContext);
|
||||
// Remove unwanted text in reg ex pattern.
|
||||
removeTextInCaptureGroups([1, 5], matchResultContext);
|
||||
formatTextInCaptureGroupIndex('bold', 3, matchResultContext);
|
||||
makeCollapsedSelectionAtOffsetInJoinedText(
|
||||
matchResultContext.offsetInJoinedTextForCollapsedSelection,
|
||||
matchResultContext.offsetInJoinedTextForCollapsedSelection + 1,
|
||||
scanningContext.textNodeWithOffset.node.getParentOrThrow(),
|
||||
getCaptureGroupsByResolvingAllDetails(
|
||||
scanningContext,
|
||||
autoFormatCriteria,
|
||||
matchResultContext,
|
||||
);
|
||||
|
||||
if (autoFormatCriteria.regExCaptureGroupsToDelete != null) {
|
||||
// Remove unwanted text in reg ex patterh.
|
||||
removeTextInCaptureGroups(
|
||||
autoFormatCriteria.regExCaptureGroupsToDelete,
|
||||
matchResultContext,
|
||||
);
|
||||
formatTextInCaptureGroupIndex('bold', 3, matchResultContext);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@ -503,10 +508,9 @@ function transformTextNodeForText(scanningContext: ScanningContext) {
|
||||
// known, the details may be fully resolved without incurring unwasted performance cost.
|
||||
function getCaptureGroupsByResolvingAllDetails(
|
||||
scanningContext: ScanningContext,
|
||||
autoFormatCriteria: AutoFormatCriteria,
|
||||
matchResultContext: MatchResultContext,
|
||||
): Array<CaptureGroupDetail> {
|
||||
const autoFormatCriteria = scanningContext.autoFormatCriteria;
|
||||
const matchResultContext = scanningContext.matchResultContext;
|
||||
|
||||
const textNodeWithOffset = scanningContext.textNodeWithOffset;
|
||||
const regExCaptureGroups = matchResultContext.regExCaptureGroups;
|
||||
const captureGroupsCount = regExCaptureGroups.length;
|
||||
@ -633,12 +637,6 @@ function shiftCaptureGroupOffsets(
|
||||
startingCaptureGroupIndex: number,
|
||||
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 regExCaptureGroupsCount = regExCaptureGroups.length;
|
||||
for (
|
||||
@ -707,52 +705,6 @@ function formatTextInCaptureGroupIndex(
|
||||
const currentSelection = $getSelection();
|
||||
if (currentSelection != null) {
|
||||
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 {
|
||||
getAllAutoFormatCriteria,
|
||||
getAllAutoFormatCriteriaForTextNodes,
|
||||
getInitialScanningContext,
|
||||
getMatchResultContextForCriteria,
|
||||
transformTextNodeForAutoFormatCriteria,
|
||||
TRIGGER_STRING,
|
||||
@ -32,18 +31,16 @@ import {
|
||||
|
||||
function getCriteriaWithMatchResultContext(
|
||||
autoFormatCriteriaArray: AutoFormatCriteriaArray,
|
||||
currentTriggerState: AutoFormatTriggerState,
|
||||
scanningContext: ScanningContext,
|
||||
): AutoFormatCriteriaWithMatchResultContext {
|
||||
const currentTriggerState = scanningContext.triggerState;
|
||||
|
||||
const count = autoFormatCriteriaArray.length;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const autoFormatCriteria = autoFormatCriteriaArray[i];
|
||||
|
||||
// Skip code block nodes, unless the nodeTransformationKind calls for toggling the code block.
|
||||
if (
|
||||
(currentTriggerState != null &&
|
||||
currentTriggerState.isCodeBlock === false) ||
|
||||
currentTriggerState.isCodeBlock === false ||
|
||||
autoFormatCriteria.nodeTransformationKind === 'paragraphCodeBlock'
|
||||
) {
|
||||
const matchResultContext = getMatchResultContextForCriteria(
|
||||
@ -51,6 +48,7 @@ function getCriteriaWithMatchResultContext(
|
||||
scanningContext,
|
||||
);
|
||||
if (matchResultContext != null) {
|
||||
matchResultContext.triggerState = currentTriggerState;
|
||||
return {
|
||||
autoFormatCriteria: autoFormatCriteria,
|
||||
matchResultContext,
|
||||
@ -78,24 +76,9 @@ function getTextNodeForAutoFormatting(
|
||||
|
||||
function updateAutoFormatting(
|
||||
editor: LexicalEditor,
|
||||
scanningContext: ScanningContext,
|
||||
): void {
|
||||
editor.update(
|
||||
() => {
|
||||
transformTextNodeForAutoFormatCriteria(scanningContext);
|
||||
},
|
||||
{
|
||||
tag: 'history-push',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function findScanningContextWithValidMatch(
|
||||
editorState: EditorState,
|
||||
currentTriggerState: AutoFormatTriggerState,
|
||||
): null | ScanningContext {
|
||||
let scanningContext = null;
|
||||
editorState.read(() => {
|
||||
): void {
|
||||
editor.update(() => {
|
||||
const textNodeWithOffset = getTextNodeForAutoFormatting($getSelection());
|
||||
|
||||
if (textNodeWithOffset === null) {
|
||||
@ -103,17 +86,17 @@ function findScanningContextWithValidMatch(
|
||||
}
|
||||
|
||||
// Please see the declaration of ScanningContext for a detailed explanation.
|
||||
const initialScanningContext = getInitialScanningContext(
|
||||
const scanningContext: ScanningContext = {
|
||||
joinedText: null,
|
||||
textNodeWithOffset,
|
||||
currentTriggerState,
|
||||
);
|
||||
|
||||
};
|
||||
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 -.
|
||||
currentTriggerState.isParentAListItemNode === false
|
||||
? getAllAutoFormatCriteria()
|
||||
: getAllAutoFormatCriteriaForTextNodes(),
|
||||
initialScanningContext,
|
||||
currentTriggerState,
|
||||
scanningContext,
|
||||
);
|
||||
|
||||
if (
|
||||
@ -122,23 +105,21 @@ function findScanningContextWithValidMatch(
|
||||
) {
|
||||
return;
|
||||
}
|
||||
scanningContext = initialScanningContext;
|
||||
// Lazy fill-in the particular format criteria and any matching result information.
|
||||
scanningContext.autoFormatCriteria =
|
||||
criteriaWithMatchResultContext.autoFormatCriteria;
|
||||
scanningContext.matchResultContext =
|
||||
criteriaWithMatchResultContext.matchResultContext;
|
||||
|
||||
transformTextNodeForAutoFormatCriteria(
|
||||
scanningContext,
|
||||
criteriaWithMatchResultContext.autoFormatCriteria,
|
||||
criteriaWithMatchResultContext.matchResultContext,
|
||||
);
|
||||
});
|
||||
return scanningContext;
|
||||
}
|
||||
|
||||
function findScanningContext(
|
||||
editorState: EditorState,
|
||||
function shouldAttemptToAutoFormat(
|
||||
currentTriggerState: null | AutoFormatTriggerState,
|
||||
priorTriggerState: null | AutoFormatTriggerState,
|
||||
): null | ScanningContext {
|
||||
): boolean {
|
||||
if (currentTriggerState == null || priorTriggerState == null) {
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 triggerOffset = currentTriggerState.anchorOffset - triggerStringLength;
|
||||
|
||||
if (
|
||||
(currentTriggerState.hasParentNode === true &&
|
||||
return (
|
||||
currentTriggerState.hasParentNode === true &&
|
||||
currentTriggerState.isSimpleText &&
|
||||
currentTriggerState.isSelectionCollapsed &&
|
||||
currentTriggerState.nodeKey === priorTriggerState.nodeKey &&
|
||||
@ -160,13 +141,8 @@ function findScanningContext(
|
||||
triggerOffset,
|
||||
triggerStringLength,
|
||||
) === TRIGGER_STRING &&
|
||||
currentTriggerState.textContent !== priorTriggerState.textContent) ===
|
||||
false
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return findScanningContextWithValidMatch(editorState, currentTriggerState);
|
||||
currentTriggerState.textContent !== priorTriggerState.textContent
|
||||
);
|
||||
}
|
||||
|
||||
function getTriggerState(
|
||||
@ -210,21 +186,16 @@ export default function useAutoFormatter(editor: LexicalEditor): void {
|
||||
// However, given "#A B", where the user delets "A" should not.
|
||||
|
||||
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.
|
||||
if (tags.has('historic') === false) {
|
||||
const editorState = editor.getEditorState();
|
||||
const currentTriggerState = getTriggerState(editorState);
|
||||
const scanningContext =
|
||||
currentTriggerState == null
|
||||
? null
|
||||
: findScanningContext(
|
||||
editorState,
|
||||
currentTriggerState,
|
||||
priorTriggerState,
|
||||
);
|
||||
if (scanningContext != null) {
|
||||
updateAutoFormatting(editor, scanningContext);
|
||||
const currentTriggerState = getTriggerState(editor.getEditorState());
|
||||
|
||||
if (
|
||||
shouldAttemptToAutoFormat(currentTriggerState, priorTriggerState) &&
|
||||
currentTriggerState != null
|
||||
) {
|
||||
updateAutoFormatting(editor, currentTriggerState);
|
||||
}
|
||||
priorTriggerState = currentTriggerState;
|
||||
} else {
|
||||
|
@ -81,7 +81,7 @@ export function useCharacterLimit(
|
||||
$wrapOverflowedNodes(offset);
|
||||
},
|
||||
{
|
||||
tag: 'history-merge',
|
||||
tag: 'without-history',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -22,9 +22,9 @@ import {$getSelection, $isRootNode, $isTextNode} from 'lexical';
|
||||
import {useCallback, useEffect, useMemo} from 'react';
|
||||
|
||||
type MergeAction = 0 | 1 | 2;
|
||||
const HISTORY_MERGE = 0;
|
||||
const HISTORY_PUSH = 1;
|
||||
const DISCARD_HISTORY_CANDIDATE = 2;
|
||||
const MERGE = 0;
|
||||
const NO_MERGE = 1;
|
||||
const DISCARD = 2;
|
||||
|
||||
type ChangeType = 0 | 1 | 2 | 3 | 4;
|
||||
const OTHER = 0;
|
||||
@ -204,7 +204,7 @@ function createMergeActionGetter(
|
||||
if (tags.has('historic')) {
|
||||
prevChangeType = OTHER;
|
||||
prevChangeTime = changeTime;
|
||||
return DISCARD_HISTORY_CANDIDATE;
|
||||
return DISCARD;
|
||||
}
|
||||
|
||||
const changeType = getChangeType(
|
||||
@ -216,40 +216,35 @@ function createMergeActionGetter(
|
||||
);
|
||||
|
||||
const mergeAction = (() => {
|
||||
const shouldPushHistory = tags.has('history-push');
|
||||
const shouldMergeHistory =
|
||||
!shouldPushHistory && tags.has('history-merge');
|
||||
|
||||
if (shouldMergeHistory) {
|
||||
return HISTORY_MERGE;
|
||||
if (tags.has('without-history')) {
|
||||
return MERGE;
|
||||
}
|
||||
if (prevEditorState === null) {
|
||||
return HISTORY_PUSH;
|
||||
return NO_MERGE;
|
||||
}
|
||||
const selection = nextEditorState._selection;
|
||||
const prevSelection = prevEditorState._selection;
|
||||
const hasDirtyNodes = dirtyLeaves.size > 0 || dirtyElements.size > 0;
|
||||
if (!hasDirtyNodes) {
|
||||
if (prevSelection === null && selection !== null) {
|
||||
return HISTORY_MERGE;
|
||||
return MERGE;
|
||||
}
|
||||
return DISCARD_HISTORY_CANDIDATE;
|
||||
return DISCARD;
|
||||
}
|
||||
|
||||
const isSameEditor =
|
||||
currentHistoryEntry === null || currentHistoryEntry.editor === editor;
|
||||
|
||||
if (
|
||||
shouldPushHistory === false &&
|
||||
changeType !== OTHER &&
|
||||
changeType === prevChangeType &&
|
||||
changeTime < prevChangeTime + delay &&
|
||||
isSameEditor
|
||||
) {
|
||||
return HISTORY_MERGE;
|
||||
return MERGE;
|
||||
}
|
||||
|
||||
return HISTORY_PUSH;
|
||||
return NO_MERGE;
|
||||
})();
|
||||
|
||||
prevChangeTime = changeTime;
|
||||
@ -302,7 +297,7 @@ export function useHistory(
|
||||
tags,
|
||||
);
|
||||
|
||||
if (mergeAction === HISTORY_PUSH) {
|
||||
if (mergeAction === NO_MERGE) {
|
||||
if (redoStack.length !== 0) {
|
||||
historyState.redoStack = [];
|
||||
}
|
||||
@ -313,7 +308,7 @@ export function useHistory(
|
||||
});
|
||||
editor.execCommand('canUndo', true);
|
||||
}
|
||||
} else if (mergeAction === DISCARD_HISTORY_CANDIDATE) {
|
||||
} else if (mergeAction === DISCARD) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -490,7 +490,7 @@ class BaseLexicalEditor {
|
||||
nextRootElement.setAttribute('data-lexical-editor', 'true');
|
||||
this._dirtyType = FULL_RECONCILE;
|
||||
initMutationObserver(getSelf(this));
|
||||
this._updateTags.add('history-merge');
|
||||
this._updateTags.add('without-history');
|
||||
commitPendingUpdates(getSelf(this));
|
||||
// TODO: remove this flag once we no longer use UEv2 internally
|
||||
if (!this._config.disableEvents) {
|
||||
|
@ -339,7 +339,7 @@ export function markAllNodesAsDirty(editor: LexicalEditor, type: string): void {
|
||||
true,
|
||||
editor._pendingEditorState === null
|
||||
? {
|
||||
tag: 'history-merge',
|
||||
tag: 'without-history',
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
@ -62,7 +62,7 @@ export class DecoratorEditor {
|
||||
this.editorState = editorState;
|
||||
if (editorState !== null) {
|
||||
editor.setEditorState(editorState, {
|
||||
tag: 'history-merge',
|
||||
tag: 'without-history',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user