Skip to content

Commit

Permalink
Fix strict strictNullChecks to src/editor/* (#10428
Browse files Browse the repository at this point in the history
* Fix strict `strictNullChecks` to `src/editor/*`

* Fix autoComplete creation

* Fix dom regression

* Remove changes
  • Loading branch information
florianduros committed Mar 23, 2023
1 parent e19127f commit e4dfb21
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 59 deletions.
16 changes: 10 additions & 6 deletions src/components/views/rooms/BasicMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -573,24 +573,28 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
this.onFormatAction(Formatting.InsertLink);
handled = true;
break;
case KeyBindingAction.EditRedo:
if (this.historyManager.canRedo()) {
const { parts, caret } = this.historyManager.redo();
case KeyBindingAction.EditRedo: {
const history = this.historyManager.redo();
if (history) {
const { parts, caret } = history;
// pass matching inputType so historyManager doesn't push echo
// when invoked from rerender callback.
model.reset(parts, caret, "historyRedo");
}
handled = true;
break;
case KeyBindingAction.EditUndo:
if (this.historyManager.canUndo()) {
const { parts, caret } = this.historyManager.undo(this.props.model);
}
case KeyBindingAction.EditUndo: {
const history = this.historyManager.undo(this.props.model);
if (history) {
const { parts, caret } = history;
// pass matching inputType so historyManager doesn't push echo
// when invoked from rerender callback.
model.reset(parts, caret, "historyUndo");
}
handled = true;
break;
}
case KeyBindingAction.NewLine:
this.insertText("\n");
handled = true;
Expand Down
9 changes: 6 additions & 3 deletions src/editor/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,15 @@ export default class AutocompleteWrapperModel {
const text = completion.completion;
switch (completion.type) {
case "room":
return [this.partCreator.roomPill(text, completionId), this.partCreator.plain(completion.suffix)];
return [this.partCreator.roomPill(text, completionId), this.partCreator.plain(completion.suffix || "")];
case "at-room":
return [this.partCreator.atRoomPill(completionId), this.partCreator.plain(completion.suffix)];
return [
this.partCreator.atRoomPill(completionId || ""),
this.partCreator.plain(completion.suffix || ""),
];
case "user":
// Insert suffix only if the pill is the part with index 0 - we are at the start of the composer
return this.partCreator.createMentionParts(this.partIndex === 0, text, completionId);
return this.partCreator.createMentionParts(this.partIndex === 0, text, completionId || "");
case "command":
// command needs special handling for auto complete, but also renders as plain texts
return [(this.partCreator as CommandPartCreator).command(text)];
Expand Down
2 changes: 1 addition & 1 deletion src/editor/commands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,5 @@ export async function shouldSendAnyway(commandText: string): Promise<boolean> {
button: _t("Send as message"),
});
const [sendAnyway] = await finished;
return sendAnyway;
return sendAnyway || false;
}
8 changes: 5 additions & 3 deletions src/editor/deserialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function parseLink(n: Node, pc: PartCreator, opts: IParseOptions): Part[] {

switch (resourceId?.[0]) {
case "@":
return [pc.userPill(n.textContent, resourceId)];
return [pc.userPill(n.textContent || "", resourceId)];
case "#":
return [pc.roomPill(resourceId)];
}
Expand All @@ -97,6 +97,8 @@ function parseImage(n: Node, pc: PartCreator, opts: IParseOptions): Part[] {
}

function parseCodeBlock(n: Node, pc: PartCreator, opts: IParseOptions): Part[] {
if (!n.textContent) return [];

let language = "";
if (n.firstChild?.nodeName === "CODE") {
for (const className of (n.firstChild as HTMLElement).classList) {
Expand Down Expand Up @@ -170,7 +172,7 @@ function parseNode(n: Node, pc: PartCreator, opts: IParseOptions, mkListItem?: (

switch (n.nodeType) {
case Node.TEXT_NODE:
return parseAtRoomMentions(n.nodeValue, pc, opts);
return parseAtRoomMentions(n.nodeValue || "", pc, opts);
case Node.ELEMENT_NODE:
switch (n.nodeName) {
case "H1":
Expand Down Expand Up @@ -204,7 +206,7 @@ function parseNode(n: Node, pc: PartCreator, opts: IParseOptions, mkListItem?: (
return parseCodeBlock(n, pc, opts);
case "CODE": {
// Escape backticks by using multiple backticks for the fence if necessary
const fence = "`".repeat(longestBacktickSequence(n.textContent) + 1);
const fence = "`".repeat(longestBacktickSequence(n.textContent || "") + 1);
return pc.plainWithEmoji(`${fence}${n.textContent}${fence}`);
}
case "BLOCKQUOTE": {
Expand Down
26 changes: 15 additions & 11 deletions src/editor/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ export function walkDOMDepthFirst(rootNode: Node, enterNodeCallback: Predicate,
} else {
while (node && !node.nextSibling && node !== rootNode) {
node = node.parentElement;
if (node !== rootNode) {
if (node && node !== rootNode) {
leaveNodeCallback(node);
}
}
if (node !== rootNode) {
if (node && node !== rootNode) {
node = node.nextSibling;
}
}
Expand All @@ -57,10 +57,10 @@ export function getCaretOffsetAndText(
}

function tryReduceSelectionToTextNode(
selectionNode: Node,
selectionNode: Node | null,
selectionOffset: number,
): {
node: Node;
node: Node | null;
characterOffset: number;
} {
// if selectionNode is an element, the selected location comes after the selectionOffset-th child node,
Expand All @@ -73,8 +73,8 @@ function tryReduceSelectionToTextNode(
if (childNodeCount) {
if (selectionOffset >= childNodeCount) {
selectionNode = selectionNode.lastChild;
if (selectionNode.nodeType === Node.TEXT_NODE) {
selectionOffset = selectionNode.textContent.length;
if (selectionNode?.nodeType === Node.TEXT_NODE) {
selectionOffset = selectionNode.textContent?.length || 0;
} else {
// this will select the last child node in the next iteration
selectionOffset = Number.MAX_SAFE_INTEGER;
Expand All @@ -101,7 +101,7 @@ function tryReduceSelectionToTextNode(

function getSelectionOffsetAndText(
editor: HTMLDivElement,
selectionNode: Node,
selectionNode: Node | null,
selectionOffset: number,
): {
offset: DocumentOffset;
Expand All @@ -115,14 +115,15 @@ function getSelectionOffsetAndText(

// gets the caret position details, ignoring and adjusting to
// the ZWS if you're typing in a caret node
function getCaret(node: Node, offsetToNode: number, offsetWithinNode: number): DocumentOffset {
function getCaret(node: Node | null, offsetToNode: number, offsetWithinNode: number): DocumentOffset {
// if no node is selected, return an offset at the start
if (!node) {
return new DocumentOffset(0, false);
}
let atNodeEnd = offsetWithinNode === node.textContent.length;
let atNodeEnd = offsetWithinNode === node.textContent?.length;
if (node.nodeType === Node.TEXT_NODE && isCaretNode(node.parentElement)) {
const zwsIdx = node.nodeValue.indexOf(CARET_NODE_CHAR);
const nodeValue = node.nodeValue || "";
const zwsIdx = nodeValue.indexOf(CARET_NODE_CHAR);
if (zwsIdx !== -1 && zwsIdx < offsetWithinNode) {
offsetWithinNode -= 1;
}
Expand All @@ -138,7 +139,10 @@ function getCaret(node: Node, offsetToNode: number, offsetWithinNode: number): D
// gets the text of the editor as a string,
// and the offset in characters where the selectionNode starts in that string
// all ZWS from caret nodes are filtered out
function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node): { offsetToNode: number; text: string } {
function getTextAndOffsetToNode(
editor: HTMLDivElement,
selectionNode: Node | null,
): { offsetToNode: number; text: string } {
let offsetToNode = 0;
let foundNode = false;
let text = "";
Expand Down
8 changes: 4 additions & 4 deletions src/editor/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { IDiff } from "./diff";
import { SerializedPart } from "./parts";
import { Caret } from "./caret";

interface IHistory {
export interface IHistory {
parts: SerializedPart[];
caret: Caret;
}
Expand Down Expand Up @@ -121,7 +121,7 @@ export default class HistoryManager {
}

public ensureLastChangesPushed(model: EditorModel): void {
if (this.changedSinceLastPush) {
if (this.changedSinceLastPush && this.lastCaret) {
this.pushState(model, this.lastCaret);
}
}
Expand All @@ -135,7 +135,7 @@ export default class HistoryManager {
}

// returns state that should be applied to model
public undo(model: EditorModel): IHistory {
public undo(model: EditorModel): IHistory | void {
if (this.canUndo()) {
this.ensureLastChangesPushed(model);
this.currentIndex -= 1;
Expand All @@ -144,7 +144,7 @@ export default class HistoryManager {
}

// returns state that should be applied to model
public redo(): IHistory {
public redo(): IHistory | void {
if (this.canRedo()) {
this.changedSinceLastPush = false;
this.currentIndex += 1;
Expand Down
31 changes: 19 additions & 12 deletions src/editor/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,18 @@ export default class EditorModel {
}

public clone(): EditorModel {
const clonedParts = this.parts.map((p) => this.partCreator.deserializePart(p.serialize()));
const clonedParts = this.parts
.map((p) => this.partCreator.deserializePart(p.serialize()))
.filter((p): p is Part => Boolean(p));
return new EditorModel(clonedParts, this._partCreator, this.updateCallback);
}

private insertPart(index: number, part: Part): void {
this._parts.splice(index, 0, part);
if (this.activePartIdx >= index) {
if (this.activePartIdx && this.activePartIdx >= index) {
++this.activePartIdx;
}
if (this.autoCompletePartIdx >= index) {
if (this.autoCompletePartIdx && this.autoCompletePartIdx >= index) {
++this.autoCompletePartIdx;
}
}
Expand All @@ -109,12 +111,12 @@ export default class EditorModel {
this._parts.splice(index, 1);
if (index === this.activePartIdx) {
this.activePartIdx = null;
} else if (this.activePartIdx > index) {
} else if (this.activePartIdx && this.activePartIdx > index) {
--this.activePartIdx;
}
if (index === this.autoCompletePartIdx) {
this.autoCompletePartIdx = null;
} else if (this.autoCompletePartIdx > index) {
} else if (this.autoCompletePartIdx && this.autoCompletePartIdx > index) {
--this.autoCompletePartIdx;
}
}
Expand Down Expand Up @@ -160,7 +162,9 @@ export default class EditorModel {
}

public reset(serializedParts: SerializedPart[], caret?: Caret, inputType?: string): void {
this._parts = serializedParts.map((p) => this._partCreator.deserializePart(p));
this._parts = serializedParts
.map((p) => this._partCreator.deserializePart(p))
.filter((p): p is Part => Boolean(p));
if (!caret) {
caret = this.getPositionAtEnd();
}
Expand Down Expand Up @@ -194,7 +198,7 @@ export default class EditorModel {

public update(newValue: string, inputType: string, caret: DocumentOffset): Promise<void> {
const diff = this.diff(newValue, inputType, caret);
const position = this.positionForOffset(diff.at, caret.atNodeEnd);
const position = this.positionForOffset(diff.at || 0, caret.atNodeEnd);
let removedOffsetDecrease = 0;
if (diff.removed) {
removedOffsetDecrease = this.removeText(position, diff.removed.length);
Expand All @@ -204,7 +208,7 @@ export default class EditorModel {
addedLen = this.addText(position, diff.added, inputType);
}
this.mergeAdjacentParts();
const caretOffset = diff.at - removedOffsetDecrease + addedLen;
const caretOffset = (diff.at || 0) - removedOffsetDecrease + addedLen;
let newPosition = this.positionForOffset(caretOffset, true);
const canOpenAutoComplete = inputType !== "insertFromPaste" && inputType !== "insertFromDrop";
const acPromise = this.setActivePart(newPosition, canOpenAutoComplete);
Expand Down Expand Up @@ -254,10 +258,11 @@ export default class EditorModel {
private onAutoComplete = ({ replaceParts, close }: ICallback): void => {
let pos: DocumentPosition | undefined;
if (replaceParts) {
this._parts.splice(this.autoCompletePartIdx, this.autoCompletePartCount, ...replaceParts);
const autoCompletePartIdx = this.autoCompletePartIdx || 0;
this._parts.splice(autoCompletePartIdx, this.autoCompletePartCount, ...replaceParts);
this.autoCompletePartCount = replaceParts.length;
const lastPart = replaceParts[replaceParts.length - 1];
const lastPartIndex = this.autoCompletePartIdx + replaceParts.length - 1;
const lastPartIndex = autoCompletePartIdx + replaceParts.length - 1;
pos = new DocumentPosition(lastPartIndex, lastPart.text.length);
}
if (close) {
Expand Down Expand Up @@ -360,10 +365,13 @@ export default class EditorModel {
const { offset } = pos;
let addLen = str.length;
const part = this._parts[index];

let it: string | undefined = str;

if (part) {
if (part.canEdit) {
if (part.validateAndInsert(offset, str, inputType)) {
str = null;
it = undefined;
} else {
const splitPart = part.split(offset);
index += 1;
Expand All @@ -381,7 +389,6 @@ export default class EditorModel {
index = 0;
}

let it: string | undefined = str;
while (it) {
const newPart = this._partCreator.createPartForInput(it, index, inputType);
const oldStr = it;
Expand Down
12 changes: 9 additions & 3 deletions src/editor/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix
const { partCreator } = model;

// compute paragraph [start, end] indexes
const paragraphIndexes = [];
const paragraphIndexes: [number, number][] = [];
let startIndex = 0;

// start at i=2 because we look at i and up to two parts behind to detect paragraph breaks at their end
Expand Down Expand Up @@ -285,12 +285,18 @@ export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix
// remove prefix and suffix formatting string
const partWithoutPrefix = parts[base].serialize();
partWithoutPrefix.text = partWithoutPrefix.text.slice(prefix.length);
parts[base] = partCreator.deserializePart(partWithoutPrefix);
let deserializedPart = partCreator.deserializePart(partWithoutPrefix);
if (deserializedPart) {
parts[base] = deserializedPart;
}

const partWithoutSuffix = parts[index - 1].serialize();
const suffixPartText = partWithoutSuffix.text;
partWithoutSuffix.text = suffixPartText.substring(0, suffixPartText.length - suffix.length);
parts[index - 1] = partCreator.deserializePart(partWithoutSuffix);
deserializedPart = partCreator.deserializePart(partWithoutSuffix);
if (deserializedPart) {
parts[index - 1] = deserializedPart;
}
} else {
parts.splice(index, 0, partCreator.plain(suffix)); // splice in the later one first to not change offset
parts.splice(base, 0, partCreator.plain(prefix));
Expand Down
Loading

0 comments on commit e4dfb21

Please sign in to comment.