Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit e4dfb21

Browse files
authored
Fix strict strictNullChecks to src/editor/* (#10428
* Fix strict `strictNullChecks` to `src/editor/*` * Fix autoComplete creation * Fix dom regression * Remove changes
1 parent e19127f commit e4dfb21

File tree

11 files changed

+85
-59
lines changed

11 files changed

+85
-59
lines changed

src/components/views/rooms/BasicMessageComposer.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -573,24 +573,28 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
573573
this.onFormatAction(Formatting.InsertLink);
574574
handled = true;
575575
break;
576-
case KeyBindingAction.EditRedo:
577-
if (this.historyManager.canRedo()) {
578-
const { parts, caret } = this.historyManager.redo();
576+
case KeyBindingAction.EditRedo: {
577+
const history = this.historyManager.redo();
578+
if (history) {
579+
const { parts, caret } = history;
579580
// pass matching inputType so historyManager doesn't push echo
580581
// when invoked from rerender callback.
581582
model.reset(parts, caret, "historyRedo");
582583
}
583584
handled = true;
584585
break;
585-
case KeyBindingAction.EditUndo:
586-
if (this.historyManager.canUndo()) {
587-
const { parts, caret } = this.historyManager.undo(this.props.model);
586+
}
587+
case KeyBindingAction.EditUndo: {
588+
const history = this.historyManager.undo(this.props.model);
589+
if (history) {
590+
const { parts, caret } = history;
588591
// pass matching inputType so historyManager doesn't push echo
589592
// when invoked from rerender callback.
590593
model.reset(parts, caret, "historyUndo");
591594
}
592595
handled = true;
593596
break;
597+
}
594598
case KeyBindingAction.NewLine:
595599
this.insertText("\n");
596600
handled = true;

src/editor/autocomplete.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,15 @@ export default class AutocompleteWrapperModel {
9999
const text = completion.completion;
100100
switch (completion.type) {
101101
case "room":
102-
return [this.partCreator.roomPill(text, completionId), this.partCreator.plain(completion.suffix)];
102+
return [this.partCreator.roomPill(text, completionId), this.partCreator.plain(completion.suffix || "")];
103103
case "at-room":
104-
return [this.partCreator.atRoomPill(completionId), this.partCreator.plain(completion.suffix)];
104+
return [
105+
this.partCreator.atRoomPill(completionId || ""),
106+
this.partCreator.plain(completion.suffix || ""),
107+
];
105108
case "user":
106109
// Insert suffix only if the pill is the part with index 0 - we are at the start of the composer
107-
return this.partCreator.createMentionParts(this.partIndex === 0, text, completionId);
110+
return this.partCreator.createMentionParts(this.partIndex === 0, text, completionId || "");
108111
case "command":
109112
// command needs special handling for auto complete, but also renders as plain texts
110113
return [(this.partCreator as CommandPartCreator).command(text)];

src/editor/commands.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,5 +137,5 @@ export async function shouldSendAnyway(commandText: string): Promise<boolean> {
137137
button: _t("Send as message"),
138138
});
139139
const [sendAnyway] = await finished;
140-
return sendAnyway;
140+
return sendAnyway || false;
141141
}

src/editor/deserialize.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function parseLink(n: Node, pc: PartCreator, opts: IParseOptions): Part[] {
7878

7979
switch (resourceId?.[0]) {
8080
case "@":
81-
return [pc.userPill(n.textContent, resourceId)];
81+
return [pc.userPill(n.textContent || "", resourceId)];
8282
case "#":
8383
return [pc.roomPill(resourceId)];
8484
}
@@ -97,6 +97,8 @@ function parseImage(n: Node, pc: PartCreator, opts: IParseOptions): Part[] {
9797
}
9898

9999
function parseCodeBlock(n: Node, pc: PartCreator, opts: IParseOptions): Part[] {
100+
if (!n.textContent) return [];
101+
100102
let language = "";
101103
if (n.firstChild?.nodeName === "CODE") {
102104
for (const className of (n.firstChild as HTMLElement).classList) {
@@ -170,7 +172,7 @@ function parseNode(n: Node, pc: PartCreator, opts: IParseOptions, mkListItem?: (
170172

171173
switch (n.nodeType) {
172174
case Node.TEXT_NODE:
173-
return parseAtRoomMentions(n.nodeValue, pc, opts);
175+
return parseAtRoomMentions(n.nodeValue || "", pc, opts);
174176
case Node.ELEMENT_NODE:
175177
switch (n.nodeName) {
176178
case "H1":
@@ -204,7 +206,7 @@ function parseNode(n: Node, pc: PartCreator, opts: IParseOptions, mkListItem?: (
204206
return parseCodeBlock(n, pc, opts);
205207
case "CODE": {
206208
// Escape backticks by using multiple backticks for the fence if necessary
207-
const fence = "`".repeat(longestBacktickSequence(n.textContent) + 1);
209+
const fence = "`".repeat(longestBacktickSequence(n.textContent || "") + 1);
208210
return pc.plainWithEmoji(`${fence}${n.textContent}${fence}`);
209211
}
210212
case "BLOCKQUOTE": {

src/editor/dom.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ export function walkDOMDepthFirst(rootNode: Node, enterNodeCallback: Predicate,
3434
} else {
3535
while (node && !node.nextSibling && node !== rootNode) {
3636
node = node.parentElement;
37-
if (node !== rootNode) {
37+
if (node && node !== rootNode) {
3838
leaveNodeCallback(node);
3939
}
4040
}
41-
if (node !== rootNode) {
41+
if (node && node !== rootNode) {
4242
node = node.nextSibling;
4343
}
4444
}
@@ -57,10 +57,10 @@ export function getCaretOffsetAndText(
5757
}
5858

5959
function tryReduceSelectionToTextNode(
60-
selectionNode: Node,
60+
selectionNode: Node | null,
6161
selectionOffset: number,
6262
): {
63-
node: Node;
63+
node: Node | null;
6464
characterOffset: number;
6565
} {
6666
// if selectionNode is an element, the selected location comes after the selectionOffset-th child node,
@@ -73,8 +73,8 @@ function tryReduceSelectionToTextNode(
7373
if (childNodeCount) {
7474
if (selectionOffset >= childNodeCount) {
7575
selectionNode = selectionNode.lastChild;
76-
if (selectionNode.nodeType === Node.TEXT_NODE) {
77-
selectionOffset = selectionNode.textContent.length;
76+
if (selectionNode?.nodeType === Node.TEXT_NODE) {
77+
selectionOffset = selectionNode.textContent?.length || 0;
7878
} else {
7979
// this will select the last child node in the next iteration
8080
selectionOffset = Number.MAX_SAFE_INTEGER;
@@ -101,7 +101,7 @@ function tryReduceSelectionToTextNode(
101101

102102
function getSelectionOffsetAndText(
103103
editor: HTMLDivElement,
104-
selectionNode: Node,
104+
selectionNode: Node | null,
105105
selectionOffset: number,
106106
): {
107107
offset: DocumentOffset;
@@ -115,14 +115,15 @@ function getSelectionOffsetAndText(
115115

116116
// gets the caret position details, ignoring and adjusting to
117117
// the ZWS if you're typing in a caret node
118-
function getCaret(node: Node, offsetToNode: number, offsetWithinNode: number): DocumentOffset {
118+
function getCaret(node: Node | null, offsetToNode: number, offsetWithinNode: number): DocumentOffset {
119119
// if no node is selected, return an offset at the start
120120
if (!node) {
121121
return new DocumentOffset(0, false);
122122
}
123-
let atNodeEnd = offsetWithinNode === node.textContent.length;
123+
let atNodeEnd = offsetWithinNode === node.textContent?.length;
124124
if (node.nodeType === Node.TEXT_NODE && isCaretNode(node.parentElement)) {
125-
const zwsIdx = node.nodeValue.indexOf(CARET_NODE_CHAR);
125+
const nodeValue = node.nodeValue || "";
126+
const zwsIdx = nodeValue.indexOf(CARET_NODE_CHAR);
126127
if (zwsIdx !== -1 && zwsIdx < offsetWithinNode) {
127128
offsetWithinNode -= 1;
128129
}
@@ -138,7 +139,10 @@ function getCaret(node: Node, offsetToNode: number, offsetWithinNode: number): D
138139
// gets the text of the editor as a string,
139140
// and the offset in characters where the selectionNode starts in that string
140141
// all ZWS from caret nodes are filtered out
141-
function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node): { offsetToNode: number; text: string } {
142+
function getTextAndOffsetToNode(
143+
editor: HTMLDivElement,
144+
selectionNode: Node | null,
145+
): { offsetToNode: number; text: string } {
142146
let offsetToNode = 0;
143147
let foundNode = false;
144148
let text = "";

src/editor/history.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { IDiff } from "./diff";
1919
import { SerializedPart } from "./parts";
2020
import { Caret } from "./caret";
2121

22-
interface IHistory {
22+
export interface IHistory {
2323
parts: SerializedPart[];
2424
caret: Caret;
2525
}
@@ -121,7 +121,7 @@ export default class HistoryManager {
121121
}
122122

123123
public ensureLastChangesPushed(model: EditorModel): void {
124-
if (this.changedSinceLastPush) {
124+
if (this.changedSinceLastPush && this.lastCaret) {
125125
this.pushState(model, this.lastCaret);
126126
}
127127
}
@@ -135,7 +135,7 @@ export default class HistoryManager {
135135
}
136136

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

146146
// returns state that should be applied to model
147-
public redo(): IHistory {
147+
public redo(): IHistory | void {
148148
if (this.canRedo()) {
149149
this.changedSinceLastPush = false;
150150
this.currentIndex += 1;

src/editor/model.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,18 @@ export default class EditorModel {
9191
}
9292

9393
public clone(): EditorModel {
94-
const clonedParts = this.parts.map((p) => this.partCreator.deserializePart(p.serialize()));
94+
const clonedParts = this.parts
95+
.map((p) => this.partCreator.deserializePart(p.serialize()))
96+
.filter((p): p is Part => Boolean(p));
9597
return new EditorModel(clonedParts, this._partCreator, this.updateCallback);
9698
}
9799

98100
private insertPart(index: number, part: Part): void {
99101
this._parts.splice(index, 0, part);
100-
if (this.activePartIdx >= index) {
102+
if (this.activePartIdx && this.activePartIdx >= index) {
101103
++this.activePartIdx;
102104
}
103-
if (this.autoCompletePartIdx >= index) {
105+
if (this.autoCompletePartIdx && this.autoCompletePartIdx >= index) {
104106
++this.autoCompletePartIdx;
105107
}
106108
}
@@ -109,12 +111,12 @@ export default class EditorModel {
109111
this._parts.splice(index, 1);
110112
if (index === this.activePartIdx) {
111113
this.activePartIdx = null;
112-
} else if (this.activePartIdx > index) {
114+
} else if (this.activePartIdx && this.activePartIdx > index) {
113115
--this.activePartIdx;
114116
}
115117
if (index === this.autoCompletePartIdx) {
116118
this.autoCompletePartIdx = null;
117-
} else if (this.autoCompletePartIdx > index) {
119+
} else if (this.autoCompletePartIdx && this.autoCompletePartIdx > index) {
118120
--this.autoCompletePartIdx;
119121
}
120122
}
@@ -160,7 +162,9 @@ export default class EditorModel {
160162
}
161163

162164
public reset(serializedParts: SerializedPart[], caret?: Caret, inputType?: string): void {
163-
this._parts = serializedParts.map((p) => this._partCreator.deserializePart(p));
165+
this._parts = serializedParts
166+
.map((p) => this._partCreator.deserializePart(p))
167+
.filter((p): p is Part => Boolean(p));
164168
if (!caret) {
165169
caret = this.getPositionAtEnd();
166170
}
@@ -194,7 +198,7 @@ export default class EditorModel {
194198

195199
public update(newValue: string, inputType: string, caret: DocumentOffset): Promise<void> {
196200
const diff = this.diff(newValue, inputType, caret);
197-
const position = this.positionForOffset(diff.at, caret.atNodeEnd);
201+
const position = this.positionForOffset(diff.at || 0, caret.atNodeEnd);
198202
let removedOffsetDecrease = 0;
199203
if (diff.removed) {
200204
removedOffsetDecrease = this.removeText(position, diff.removed.length);
@@ -204,7 +208,7 @@ export default class EditorModel {
204208
addedLen = this.addText(position, diff.added, inputType);
205209
}
206210
this.mergeAdjacentParts();
207-
const caretOffset = diff.at - removedOffsetDecrease + addedLen;
211+
const caretOffset = (diff.at || 0) - removedOffsetDecrease + addedLen;
208212
let newPosition = this.positionForOffset(caretOffset, true);
209213
const canOpenAutoComplete = inputType !== "insertFromPaste" && inputType !== "insertFromDrop";
210214
const acPromise = this.setActivePart(newPosition, canOpenAutoComplete);
@@ -254,10 +258,11 @@ export default class EditorModel {
254258
private onAutoComplete = ({ replaceParts, close }: ICallback): void => {
255259
let pos: DocumentPosition | undefined;
256260
if (replaceParts) {
257-
this._parts.splice(this.autoCompletePartIdx, this.autoCompletePartCount, ...replaceParts);
261+
const autoCompletePartIdx = this.autoCompletePartIdx || 0;
262+
this._parts.splice(autoCompletePartIdx, this.autoCompletePartCount, ...replaceParts);
258263
this.autoCompletePartCount = replaceParts.length;
259264
const lastPart = replaceParts[replaceParts.length - 1];
260-
const lastPartIndex = this.autoCompletePartIdx + replaceParts.length - 1;
265+
const lastPartIndex = autoCompletePartIdx + replaceParts.length - 1;
261266
pos = new DocumentPosition(lastPartIndex, lastPart.text.length);
262267
}
263268
if (close) {
@@ -360,10 +365,13 @@ export default class EditorModel {
360365
const { offset } = pos;
361366
let addLen = str.length;
362367
const part = this._parts[index];
368+
369+
let it: string | undefined = str;
370+
363371
if (part) {
364372
if (part.canEdit) {
365373
if (part.validateAndInsert(offset, str, inputType)) {
366-
str = null;
374+
it = undefined;
367375
} else {
368376
const splitPart = part.split(offset);
369377
index += 1;
@@ -381,7 +389,6 @@ export default class EditorModel {
381389
index = 0;
382390
}
383391

384-
let it: string | undefined = str;
385392
while (it) {
386393
const newPart = this._partCreator.createPartForInput(it, index, inputType);
387394
const oldStr = it;

src/editor/operations.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix
241241
const { partCreator } = model;
242242

243243
// compute paragraph [start, end] indexes
244-
const paragraphIndexes = [];
244+
const paragraphIndexes: [number, number][] = [];
245245
let startIndex = 0;
246246

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

290293
const partWithoutSuffix = parts[index - 1].serialize();
291294
const suffixPartText = partWithoutSuffix.text;
292295
partWithoutSuffix.text = suffixPartText.substring(0, suffixPartText.length - suffix.length);
293-
parts[index - 1] = partCreator.deserializePart(partWithoutSuffix);
296+
deserializedPart = partCreator.deserializePart(partWithoutSuffix);
297+
if (deserializedPart) {
298+
parts[index - 1] = deserializedPart;
299+
}
294300
} else {
295301
parts.splice(index, 0, partCreator.plain(suffix)); // splice in the later one first to not change offset
296302
parts.splice(base, 0, partCreator.plain(prefix));

0 commit comments

Comments
 (0)