mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 19:56:47 +03:00
Switch to a discriminated unions
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
2bf5e4b142
commit
ed0d9973b7
9 changed files with 130 additions and 74 deletions
|
@ -17,13 +17,13 @@ limitations under the License.
|
|||
|
||||
import {KeyboardEvent} from "react";
|
||||
|
||||
import {BasePart, CommandPartCreator, PartCreator} from "./parts";
|
||||
import {Part, CommandPartCreator, PartCreator} from "./parts";
|
||||
import DocumentPosition from "./position";
|
||||
import {ICompletion} from "../autocomplete/Autocompleter";
|
||||
import Autocomplete from "../components/views/rooms/Autocomplete";
|
||||
|
||||
export interface ICallback {
|
||||
replaceParts?: BasePart[];
|
||||
replaceParts?: Part[];
|
||||
close?: boolean;
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ export type GetAutocompleterComponent = () => Autocomplete;
|
|||
export type UpdateQuery = (test: string) => Promise<void>;
|
||||
|
||||
export default class AutocompleteWrapperModel {
|
||||
private queryPart: BasePart;
|
||||
private queryPart: Part;
|
||||
private partIndex: number;
|
||||
|
||||
constructor(
|
||||
|
@ -89,7 +89,7 @@ export default class AutocompleteWrapperModel {
|
|||
this.getAutocompleterComponent().moveSelection(+1);
|
||||
}
|
||||
|
||||
onPartUpdate(part: BasePart, pos: DocumentPosition) {
|
||||
onPartUpdate(part: Part, pos: DocumentPosition) {
|
||||
// cache the typed value and caret here
|
||||
// so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text)
|
||||
this.queryPart = part;
|
||||
|
|
|
@ -19,7 +19,7 @@ import {needsCaretNodeBefore, needsCaretNodeAfter} from "./render";
|
|||
import Range from "./range";
|
||||
import EditorModel from "./model";
|
||||
import DocumentPosition, {IPosition} from "./position";
|
||||
import {BasePart} from "./parts";
|
||||
import {Part} from "./parts";
|
||||
|
||||
export type Caret = Range | DocumentPosition;
|
||||
|
||||
|
@ -104,7 +104,7 @@ export function getLineAndNodePosition(model: EditorModel, caretPosition: IPosit
|
|||
return {lineIndex, nodeIndex, offset};
|
||||
}
|
||||
|
||||
function findNodeInLineForPart(parts: BasePart[], partIndex: number) {
|
||||
function findNodeInLineForPart(parts: Part[], partIndex: number) {
|
||||
let lineIndex = 0;
|
||||
let nodeIndex = -1;
|
||||
|
||||
|
@ -140,7 +140,7 @@ function findNodeInLineForPart(parts: BasePart[], partIndex: number) {
|
|||
return {lineIndex, nodeIndex};
|
||||
}
|
||||
|
||||
function moveOutOfUneditablePart(parts: BasePart[], partIndex: number, nodeIndex: number, offset: number) {
|
||||
function moveOutOfUneditablePart(parts: Part[], partIndex: number, nodeIndex: number, offset: number) {
|
||||
// move caret before or after uneditable part
|
||||
const part = parts[partIndex];
|
||||
if (part && !part.canEdit) {
|
||||
|
|
|
@ -16,12 +16,11 @@ limitations under the License.
|
|||
|
||||
import EditorModel from "./model";
|
||||
import {IDiff} from "./diff";
|
||||
import {ISerializedPart} from "./parts";
|
||||
import Range from "./range";
|
||||
import {SerializedPart} from "./parts";
|
||||
import {Caret} from "./caret";
|
||||
|
||||
interface IHistory {
|
||||
parts: ISerializedPart[];
|
||||
parts: SerializedPart[];
|
||||
caret: Caret;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
import {diffAtCaret, diffDeletion, IDiff} from "./diff";
|
||||
import DocumentPosition, {IPosition} from "./position";
|
||||
import Range from "./range";
|
||||
import {BasePart, ISerializedPart, PartCreator} from "./parts";
|
||||
import {SerializedPart, Part, PartCreator} from "./parts";
|
||||
import AutocompleteWrapperModel, {ICallback} from "./autocomplete";
|
||||
import DocumentOffset from "./offset";
|
||||
import {Caret} from "./caret";
|
||||
|
@ -49,7 +49,7 @@ type UpdateCallback = (caret: Caret, inputType?: string, diff?: IDiff) => void;
|
|||
type ManualTransformCallback = () => Caret;
|
||||
|
||||
export default class EditorModel {
|
||||
private _parts: BasePart[];
|
||||
private _parts: Part[];
|
||||
private readonly _partCreator: PartCreator;
|
||||
private activePartIdx: number = null;
|
||||
private _autoComplete: AutocompleteWrapperModel = null;
|
||||
|
@ -57,7 +57,7 @@ export default class EditorModel {
|
|||
private autoCompletePartCount = 0;
|
||||
private transformCallback: TransformCallback = null;
|
||||
|
||||
constructor(parts: BasePart[], partCreator: PartCreator, private updateCallback: UpdateCallback = null) {
|
||||
constructor(parts: Part[], partCreator: PartCreator, private updateCallback: UpdateCallback = null) {
|
||||
this._parts = parts;
|
||||
this._partCreator = partCreator;
|
||||
this.transformCallback = null;
|
||||
|
@ -94,7 +94,7 @@ export default class EditorModel {
|
|||
return new EditorModel(this._parts, this._partCreator, this.updateCallback);
|
||||
}
|
||||
|
||||
private insertPart(index: number, part: BasePart) {
|
||||
private insertPart(index: number, part: Part) {
|
||||
this._parts.splice(index, 0, part);
|
||||
if (this.activePartIdx >= index) {
|
||||
++this.activePartIdx;
|
||||
|
@ -118,7 +118,7 @@ export default class EditorModel {
|
|||
}
|
||||
}
|
||||
|
||||
private replacePart(index: number, part: BasePart) {
|
||||
private replacePart(index: number, part: Part) {
|
||||
this._parts.splice(index, 1, part);
|
||||
}
|
||||
|
||||
|
@ -158,7 +158,7 @@ export default class EditorModel {
|
|||
}
|
||||
}
|
||||
|
||||
reset(serializedParts: ISerializedPart[], caret: Caret, inputType: string) {
|
||||
reset(serializedParts: SerializedPart[], caret: Caret, inputType: string) {
|
||||
this._parts = serializedParts.map(p => this._partCreator.deserializePart(p));
|
||||
if (!caret) {
|
||||
caret = this.getPositionAtEnd();
|
||||
|
@ -180,7 +180,7 @@ export default class EditorModel {
|
|||
* @param {DocumentPosition} position the position to start inserting at
|
||||
* @return {Number} the amount of characters added
|
||||
*/
|
||||
insert(parts: BasePart[], position: IPosition) {
|
||||
insert(parts: Part[], position: IPosition) {
|
||||
const insertIndex = this.splitAt(position);
|
||||
let newTextLength = 0;
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
|
@ -420,7 +420,7 @@ export default class EditorModel {
|
|||
return new Range(this, positionA, positionB);
|
||||
}
|
||||
|
||||
replaceRange(startPosition: DocumentPosition, endPosition: DocumentPosition, parts: BasePart[]) {
|
||||
replaceRange(startPosition: DocumentPosition, endPosition: DocumentPosition, parts: Part[]) {
|
||||
// convert end position to offset, so it is independent of how the document is split into parts
|
||||
// which we'll change when splitting up at the start position
|
||||
const endOffset = endPosition.asOffset(this);
|
||||
|
|
|
@ -15,13 +15,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import Range from "./range";
|
||||
import {BasePart} from "./parts";
|
||||
import {Part} from "./parts";
|
||||
|
||||
/**
|
||||
* Some common queries and transformations on the editor model
|
||||
*/
|
||||
|
||||
export function replaceRangeAndExpandSelection(range: Range, newParts: BasePart[]) {
|
||||
export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]) {
|
||||
const {model} = range;
|
||||
model.transform(() => {
|
||||
const oldLen = range.length;
|
||||
|
@ -32,7 +32,7 @@ export function replaceRangeAndExpandSelection(range: Range, newParts: BasePart[
|
|||
});
|
||||
}
|
||||
|
||||
export function replaceRangeAndMoveCaret(range: Range, newParts: BasePart[]) {
|
||||
export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]) {
|
||||
const {model} = range;
|
||||
model.transform(() => {
|
||||
const oldLen = range.length;
|
||||
|
|
|
@ -19,15 +19,66 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
|
|||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import AutocompleteWrapperModel, {GetAutocompleterComponent, UpdateCallback, UpdateQuery} from "./autocomplete";
|
||||
import AutocompleteWrapperModel, {
|
||||
GetAutocompleterComponent,
|
||||
UpdateCallback,
|
||||
UpdateQuery
|
||||
} from "./autocomplete";
|
||||
import * as Avatar from "../Avatar";
|
||||
|
||||
export interface ISerializedPart {
|
||||
type: string;
|
||||
interface ISerializedPart {
|
||||
type: Type.Plain | Type.Newline | Type.Command | Type.PillCandidate;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export abstract class BasePart {
|
||||
interface ISerializedPillPart {
|
||||
type: Type.AtRoomPill | Type.RoomPill | Type.UserPill;
|
||||
text: string;
|
||||
resourceId: string;
|
||||
}
|
||||
|
||||
export type SerializedPart = ISerializedPart | ISerializedPillPart;
|
||||
|
||||
enum Type {
|
||||
Plain = "plain",
|
||||
Newline = "newline",
|
||||
Command = "command",
|
||||
UserPill = "user-pill",
|
||||
RoomPill = "room-pill",
|
||||
AtRoomPill = "at-room-pill",
|
||||
PillCandidate = "pill-candidate",
|
||||
}
|
||||
|
||||
interface IBasePart {
|
||||
text: string;
|
||||
type: Type.Plain | Type.Newline;
|
||||
canEdit: boolean;
|
||||
|
||||
createAutoComplete(updateCallback: UpdateCallback): void;
|
||||
|
||||
serialize(): SerializedPart;
|
||||
remove(offset: number, len: number): string;
|
||||
split(offset: number): IBasePart;
|
||||
validateAndInsert(offset: number, str: string, inputType: string): boolean;
|
||||
appendUntilRejected(str: string, inputType: string): string;
|
||||
updateDOMNode(node: Node);
|
||||
canUpdateDOMNode(node: Node);
|
||||
toDOMNode(): Node;
|
||||
}
|
||||
|
||||
interface IPillCandidatePart extends Omit<IBasePart, "type" | "createAutoComplete"> {
|
||||
type: Type.PillCandidate | Type.Command;
|
||||
createAutoComplete(updateCallback: UpdateCallback): AutocompleteWrapperModel;
|
||||
}
|
||||
|
||||
interface IPillPart extends Omit<IBasePart, "type" | "resourceId"> {
|
||||
type: Type.AtRoomPill | Type.RoomPill | Type.UserPill;
|
||||
resourceId: string;
|
||||
}
|
||||
|
||||
export type Part = IBasePart | IPillCandidatePart | IPillPart;
|
||||
|
||||
abstract class BasePart {
|
||||
protected _text: string;
|
||||
|
||||
constructor(text = "") {
|
||||
|
@ -42,7 +93,7 @@ export abstract class BasePart {
|
|||
return true;
|
||||
}
|
||||
|
||||
merge(part: BasePart) {
|
||||
merge(part: Part) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -94,7 +145,7 @@ export abstract class BasePart {
|
|||
return true;
|
||||
}
|
||||
|
||||
createAutoComplete(updateCallback): AutocompleteWrapperModel | void {}
|
||||
createAutoComplete(updateCallback: UpdateCallback): void {}
|
||||
|
||||
trim(len: number) {
|
||||
const remaining = this._text.substr(len);
|
||||
|
@ -106,7 +157,7 @@ export abstract class BasePart {
|
|||
return this._text;
|
||||
}
|
||||
|
||||
abstract get type(): string;
|
||||
abstract get type(): Type;
|
||||
|
||||
get canEdit() {
|
||||
return true;
|
||||
|
@ -116,8 +167,11 @@ export abstract class BasePart {
|
|||
return `${this.type}(${this.text})`;
|
||||
}
|
||||
|
||||
serialize(): ISerializedPart {
|
||||
return {type: this.type, text: this.text};
|
||||
serialize(): SerializedPart {
|
||||
return {
|
||||
type: this.type as ISerializedPart["type"],
|
||||
text: this.text,
|
||||
};
|
||||
}
|
||||
|
||||
abstract updateDOMNode(node: Node);
|
||||
|
@ -125,8 +179,7 @@ export abstract class BasePart {
|
|||
abstract toDOMNode(): Node;
|
||||
}
|
||||
|
||||
// exported for unit tests, should otherwise only be used through PartCreator
|
||||
export class PlainPart extends BasePart {
|
||||
abstract class PlainBasePart extends BasePart {
|
||||
acceptsInsertion(chr: string, offset: number, inputType: string) {
|
||||
if (chr === "\n") {
|
||||
return false;
|
||||
|
@ -150,10 +203,6 @@ export class PlainPart extends BasePart {
|
|||
return false;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return "plain";
|
||||
}
|
||||
|
||||
updateDOMNode(node: Node) {
|
||||
if (node.textContent !== this.text) {
|
||||
node.textContent = this.text;
|
||||
|
@ -165,7 +214,14 @@ export class PlainPart extends BasePart {
|
|||
}
|
||||
}
|
||||
|
||||
export abstract class PillPart extends BasePart {
|
||||
// exported for unit tests, should otherwise only be used through PartCreator
|
||||
export class PlainPart extends PlainBasePart implements IBasePart {
|
||||
get type(): IBasePart["type"] {
|
||||
return Type.Plain;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class PillPart extends BasePart implements IPillPart {
|
||||
constructor(public resourceId: string, label) {
|
||||
super(label);
|
||||
}
|
||||
|
@ -223,12 +279,14 @@ export abstract class PillPart extends BasePart {
|
|||
return false;
|
||||
}
|
||||
|
||||
abstract get type(): IPillPart["type"];
|
||||
|
||||
abstract get className(): string;
|
||||
|
||||
abstract setAvatar(node: HTMLElement): void;
|
||||
}
|
||||
|
||||
class NewlinePart extends BasePart {
|
||||
class NewlinePart extends BasePart implements IBasePart {
|
||||
acceptsInsertion(chr: string, offset: number) {
|
||||
return offset === 0 && chr === "\n";
|
||||
}
|
||||
|
@ -251,8 +309,8 @@ class NewlinePart extends BasePart {
|
|||
return node.tagName === "BR";
|
||||
}
|
||||
|
||||
get type() {
|
||||
return "newline";
|
||||
get type(): IBasePart["type"] {
|
||||
return Type.Newline;
|
||||
}
|
||||
|
||||
// this makes the cursor skip this part when it is inserted
|
||||
|
@ -283,8 +341,8 @@ class RoomPillPart extends PillPart {
|
|||
this._setAvatarVars(node, avatarUrl, initialLetter);
|
||||
}
|
||||
|
||||
get type() {
|
||||
return "room-pill";
|
||||
get type(): IPillPart["type"] {
|
||||
return Type.RoomPill;
|
||||
}
|
||||
|
||||
get className() {
|
||||
|
@ -293,8 +351,8 @@ class RoomPillPart extends PillPart {
|
|||
}
|
||||
|
||||
class AtRoomPillPart extends RoomPillPart {
|
||||
get type() {
|
||||
return "at-room-pill";
|
||||
get type(): IPillPart["type"] {
|
||||
return Type.AtRoomPill;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -321,28 +379,29 @@ class UserPillPart extends PillPart {
|
|||
this._setAvatarVars(node, avatarUrl, initialLetter);
|
||||
}
|
||||
|
||||
get type() {
|
||||
return "user-pill";
|
||||
get type(): IPillPart["type"] {
|
||||
return Type.UserPill;
|
||||
}
|
||||
|
||||
get className() {
|
||||
return "mx_UserPill mx_Pill";
|
||||
}
|
||||
|
||||
serialize() {
|
||||
serialize(): ISerializedPillPart {
|
||||
return {
|
||||
...super.serialize(),
|
||||
type: this.type,
|
||||
text: this.text,
|
||||
resourceId: this.resourceId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class PillCandidatePart extends PlainPart {
|
||||
class PillCandidatePart extends PlainBasePart implements IPillCandidatePart {
|
||||
constructor(text: string, private autoCompleteCreator: IAutocompleteCreator) {
|
||||
super(text);
|
||||
}
|
||||
|
||||
createAutoComplete(updateCallback): AutocompleteWrapperModel {
|
||||
createAutoComplete(updateCallback: UpdateCallback): AutocompleteWrapperModel {
|
||||
return this.autoCompleteCreator.create(updateCallback);
|
||||
}
|
||||
|
||||
|
@ -362,8 +421,8 @@ class PillCandidatePart extends PlainPart {
|
|||
return true;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return "pill-candidate";
|
||||
get type(): IPillCandidatePart["type"] {
|
||||
return Type.PillCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -399,7 +458,7 @@ export class PartCreator {
|
|||
this.autoCompleteCreator.create = autoCompleteCreator(this);
|
||||
}
|
||||
|
||||
createPartForInput(input: string, partIndex: number, inputType?: string): BasePart {
|
||||
createPartForInput(input: string, partIndex: number, inputType?: string): Part {
|
||||
switch (input[0]) {
|
||||
case "#":
|
||||
case "@":
|
||||
|
@ -416,20 +475,20 @@ export class PartCreator {
|
|||
return this.plain(text);
|
||||
}
|
||||
|
||||
deserializePart(part: ISerializedPart) {
|
||||
deserializePart(part: SerializedPart): Part {
|
||||
switch (part.type) {
|
||||
case "plain":
|
||||
case Type.Plain:
|
||||
return this.plain(part.text);
|
||||
case "newline":
|
||||
case Type.Newline:
|
||||
return this.newline();
|
||||
case "at-room-pill":
|
||||
case Type.AtRoomPill:
|
||||
return this.atRoomPill(part.text);
|
||||
case "pill-candidate":
|
||||
case Type.PillCandidate:
|
||||
return this.pillCandidate(part.text);
|
||||
case "room-pill":
|
||||
case Type.RoomPill:
|
||||
return this.roomPill(part.text);
|
||||
case "user-pill":
|
||||
return this.userPill(part.text, (part as PillPart).resourceId);
|
||||
case Type.UserPill:
|
||||
return this.userPill(part.text, part.resourceId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -491,7 +550,7 @@ export class CommandPartCreator extends PartCreator {
|
|||
return new CommandPart(text, this.autoCompleteCreator);
|
||||
}
|
||||
|
||||
deserializePart(part: BasePart) {
|
||||
deserializePart(part: Part): Part {
|
||||
if (part.type === "command") {
|
||||
return this.command(part.text);
|
||||
} else {
|
||||
|
@ -501,7 +560,7 @@ export class CommandPartCreator extends PartCreator {
|
|||
}
|
||||
|
||||
class CommandPart extends PillCandidatePart {
|
||||
get type() {
|
||||
return "command";
|
||||
get type(): IPillCandidatePart["type"] {
|
||||
return Type.Command;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,15 +16,15 @@ limitations under the License.
|
|||
|
||||
import DocumentOffset from "./offset";
|
||||
import EditorModel from "./model";
|
||||
import {BasePart} from "./parts";
|
||||
import {Part} from "./parts";
|
||||
|
||||
export interface IPosition {
|
||||
index: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
type Callback = (part: BasePart, startIdx: number, endIdx: number) => void;
|
||||
type Predicate = (index: number, offset: number, part: BasePart) => boolean;
|
||||
type Callback = (part: Part, startIdx: number, endIdx: number) => void;
|
||||
type Predicate = (index: number, offset: number, part: Part) => boolean;
|
||||
|
||||
export default class DocumentPosition implements IPosition {
|
||||
constructor(public readonly index: number, public readonly offset: number) {
|
||||
|
|
|
@ -15,16 +15,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {BasePart} from "./parts";
|
||||
import {Part} from "./parts";
|
||||
import EditorModel from "./model";
|
||||
import {instanceOf} from "prop-types";
|
||||
|
||||
export function needsCaretNodeBefore(part: BasePart, prevPart: BasePart) {
|
||||
export function needsCaretNodeBefore(part: Part, prevPart: Part) {
|
||||
const isFirst = !prevPart || prevPart.type === "newline";
|
||||
return !part.canEdit && (isFirst || !prevPart.canEdit);
|
||||
}
|
||||
|
||||
export function needsCaretNodeAfter(part: BasePart, isLastOfLine: boolean) {
|
||||
export function needsCaretNodeAfter(part: Part, isLastOfLine: boolean) {
|
||||
return !part.canEdit && isLastOfLine;
|
||||
}
|
||||
|
||||
|
@ -83,7 +82,7 @@ function removeChildren(parent: HTMLElement) {
|
|||
}
|
||||
}
|
||||
|
||||
function reconcileLine(lineContainer: ChildNode, parts: BasePart[]) {
|
||||
function reconcileLine(lineContainer: ChildNode, parts: Part[]) {
|
||||
let currentNode;
|
||||
let prevPart;
|
||||
const lastPart = parts[parts.length - 1];
|
||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
import Markdown from '../Markdown';
|
||||
import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
|
||||
import EditorModel from "./model";
|
||||
import {PillPart} from "./parts";
|
||||
|
||||
export function mdSerialize(model: EditorModel) {
|
||||
return model.parts.reduce((html, part) => {
|
||||
|
@ -32,7 +31,7 @@ export function mdSerialize(model: EditorModel) {
|
|||
return html + part.text;
|
||||
case "room-pill":
|
||||
case "user-pill":
|
||||
return html + `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink((part as PillPart).resourceId)})`;
|
||||
return html + `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`;
|
||||
}
|
||||
}, "");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue